mirror of
https://github.com/ClovertaTheTrilobita/SanYeCao-blog.git
synced 2026-04-01 17:50:13 +00:00
added comments preview in home
This commit is contained in:
parent
a4279b1a59
commit
3ac19d8593
10 changed files with 447 additions and 2 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
407
src/components/Comments.astro
Normal file
407
src/components/Comments.astro
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import { getLangFromUrl } from "@/i18n";
|
||||
|
||||
const token = import.meta.env.GITHUB_TOKEN;
|
||||
const owner = import.meta.env.GISCUS_REPO_OWNER ?? "ClovertaTheTrilobita";
|
||||
const name = import.meta.env.GISCUS_REPO_NAME ?? "SanYeCao-blog";
|
||||
const categoryId = import.meta.env.GISCUS_CATEGORY_ID ?? "DIC_kwDORvuVpM4C5MDE";
|
||||
const limit = Number(Astro.props.limit ?? 5);
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const allPosts = await getCollection("blog");
|
||||
|
||||
type CommentAuthor = {
|
||||
login?: string;
|
||||
avatarUrl?: string;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
type RawReply = {
|
||||
id: string;
|
||||
body?: string;
|
||||
updatedAt: string;
|
||||
author?: CommentAuthor | null;
|
||||
};
|
||||
|
||||
type RawComment = {
|
||||
id: string;
|
||||
body?: string;
|
||||
updatedAt: string;
|
||||
author?: CommentAuthor | null;
|
||||
replies?: {
|
||||
nodes?: RawReply[];
|
||||
};
|
||||
};
|
||||
|
||||
type DiscussionNode = {
|
||||
title: string;
|
||||
comments?: {
|
||||
nodes?: RawComment[];
|
||||
};
|
||||
};
|
||||
|
||||
type LatestCommentItem = {
|
||||
id: string;
|
||||
body?: string;
|
||||
updatedAt: string;
|
||||
author?: CommentAuthor | null;
|
||||
postId: string;
|
||||
postTitle: string;
|
||||
localUrl: string;
|
||||
isReply: boolean;
|
||||
replyTo?: string;
|
||||
};
|
||||
|
||||
function normalizePath(path: string) {
|
||||
return path.replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
|
||||
function excerpt(text = "", max = 110) {
|
||||
const plain = text.replace(/\s+/g, " ").trim();
|
||||
if (plain.length <= max) return plain;
|
||||
return plain.slice(0, max) + "…";
|
||||
}
|
||||
|
||||
function formatDate(iso: string) {
|
||||
return new Date(iso).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
async function fetchLatestComments(): Promise<LatestCommentItem[]> {
|
||||
if (!token) return [];
|
||||
|
||||
const query = `
|
||||
query($owner: String!, $name: String!, $categoryId: ID!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
discussions(
|
||||
first: 20
|
||||
categoryId: $categoryId
|
||||
orderBy: { field: UPDATED_AT, direction: DESC }
|
||||
) {
|
||||
nodes {
|
||||
title
|
||||
comments(last: 10) {
|
||||
nodes {
|
||||
id
|
||||
body
|
||||
updatedAt
|
||||
author {
|
||||
... on User {
|
||||
login
|
||||
avatarUrl
|
||||
url
|
||||
}
|
||||
... on Organization {
|
||||
login
|
||||
avatarUrl
|
||||
url
|
||||
}
|
||||
}
|
||||
replies(first: 10) {
|
||||
nodes {
|
||||
id
|
||||
body
|
||||
updatedAt
|
||||
author {
|
||||
... on User {
|
||||
login
|
||||
avatarUrl
|
||||
url
|
||||
}
|
||||
... on Organization {
|
||||
login
|
||||
avatarUrl
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const res = await fetch("https://api.github.com/graphql", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
variables: { owner, name, categoryId },
|
||||
}),
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
const discussions = (json?.data?.repository?.discussions?.nodes ??
|
||||
[]) as DiscussionNode[];
|
||||
|
||||
// 用 post.id 建本地文章索引
|
||||
const postMap = new Map(
|
||||
allPosts.map((post: any) => [
|
||||
normalizePath(post.id),
|
||||
{
|
||||
title: post.data.title,
|
||||
url: `/${lang}/posts/${post.id}/#comments`,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const comments: LatestCommentItem[] = [];
|
||||
|
||||
for (const discussion of discussions) {
|
||||
const discussionKey = normalizePath(discussion.title);
|
||||
|
||||
let matchedPostId: string | undefined;
|
||||
|
||||
if (postMap.has(discussionKey)) {
|
||||
matchedPostId = discussionKey;
|
||||
} else {
|
||||
matchedPostId = [...postMap.keys()].find(
|
||||
(postId) =>
|
||||
discussionKey === postId ||
|
||||
discussionKey.includes(postId) ||
|
||||
postId.includes(discussionKey),
|
||||
);
|
||||
}
|
||||
|
||||
if (!matchedPostId) continue;
|
||||
|
||||
const postInfo = postMap.get(matchedPostId);
|
||||
if (!postInfo) continue;
|
||||
|
||||
for (const comment of discussion.comments?.nodes ?? []) {
|
||||
comments.push({
|
||||
id: comment.id,
|
||||
body: comment.body,
|
||||
updatedAt: comment.updatedAt,
|
||||
author: comment.author,
|
||||
postId: matchedPostId,
|
||||
postTitle: postInfo.title,
|
||||
localUrl: postInfo.url,
|
||||
isReply: false,
|
||||
});
|
||||
|
||||
for (const reply of comment.replies?.nodes ?? []) {
|
||||
comments.push({
|
||||
id: reply.id,
|
||||
body: reply.body,
|
||||
updatedAt: reply.updatedAt,
|
||||
author: reply.author,
|
||||
postId: matchedPostId,
|
||||
postTitle: postInfo.title,
|
||||
localUrl: postInfo.url,
|
||||
isReply: true,
|
||||
replyTo: comment.author?.login ?? "Unknown",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return comments
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.updatedAt).getTime() -
|
||||
new Date(a.updatedAt).getTime(),
|
||||
)
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
const comments = await fetchLatestComments();
|
||||
---
|
||||
|
||||
<section class="latest-comments">
|
||||
<h2>{lang === "zh" ? "最新评论" : "Latest Comments"}</h2>
|
||||
|
||||
{
|
||||
comments.length === 0 ? (
|
||||
<p class="empty">
|
||||
{lang === "zh" ? "还没有评论。" : "No comments yet."}
|
||||
</p>
|
||||
) : (
|
||||
<ul class="comment-list">
|
||||
{comments.map((comment) => (
|
||||
<li
|
||||
class={`comment-card ${comment.isReply ? "is-reply" : ""}`}
|
||||
>
|
||||
<a href={comment.localUrl} class="comment-link">
|
||||
<div class="comment-top">
|
||||
{comment.author?.avatarUrl && (
|
||||
<img
|
||||
src={comment.author.avatarUrl}
|
||||
alt={comment.author?.login ?? "author"}
|
||||
class="comment-avatar"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div class="comment-meta">
|
||||
<span class="comment-author">
|
||||
{comment.author?.login ?? "Unknown"}
|
||||
</span>
|
||||
<span class="comment-date">
|
||||
{formatDate(comment.updatedAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="comment-post-title"
|
||||
title={comment.postTitle}
|
||||
>
|
||||
{comment.postTitle}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{comment.isReply && (
|
||||
<p class="comment-kind">
|
||||
{lang === "zh"
|
||||
? `回复 @${comment.replyTo ?? "Unknown"}`
|
||||
: `Reply to @${comment.replyTo ?? "Unknown"}`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p class="comment-body">{excerpt(comment.body)}</p>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.comment-kind {
|
||||
margin: 0 0 0.35rem 0;
|
||||
font-size: 0.82rem;
|
||||
color: #8c8c8c;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.comment-card.is-reply .comment-link {
|
||||
border-style: dashed;
|
||||
background: rgba(160, 175, 190, 0.1);
|
||||
}
|
||||
|
||||
:global(.dark) .comment-kind {
|
||||
color: #b8b8b8;
|
||||
}
|
||||
|
||||
:global(.dark) .comment-card.is-reply .comment-link {
|
||||
background: rgba(180, 190, 200, 0.1);
|
||||
}
|
||||
.latest-comments {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.latest-comments h2 {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.comment-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.comment-card {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.comment-link {
|
||||
display: block;
|
||||
padding: 0.9rem 1rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border: 1px dashed #aeb8c2;
|
||||
background: rgba(160, 175, 190, 0.06);
|
||||
}
|
||||
|
||||
.comment-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.comment-post-title {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 700;
|
||||
color: #6f8090;
|
||||
max-width: 45%;
|
||||
line-height: 1.35;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
:global(.dark) .comment-post-title {
|
||||
color: #c8d2dc;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.comment-top {
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.comment-post-title {
|
||||
margin-left: 0;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
font-size: 0.9rem;
|
||||
color: #6f8090;
|
||||
}
|
||||
|
||||
.comment-body {
|
||||
margin: 0;
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #6f8090;
|
||||
}
|
||||
|
||||
:global(.dark) .comment-link {
|
||||
border-color: #7f8c97;
|
||||
background: rgba(180, 190, 200, 0.06);
|
||||
}
|
||||
|
||||
:global(.dark) .comment-date,
|
||||
:global(.dark) .empty {
|
||||
color: #aab7c4;
|
||||
}
|
||||
|
||||
:global(.dark) .comment-body {
|
||||
color: #d3d7db;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,7 +4,7 @@ const data = Astro.props;
|
|||
---
|
||||
|
||||
<li class="post-card">
|
||||
<a href={data.url} class="post-link">
|
||||
<a href={data.url} class="post-link" data-astro-reload>
|
||||
<div class="post-text">
|
||||
<span class="post-title">{data.title}</span>
|
||||
<span class="post-description">{data.description}</span>
|
||||
|
|
|
|||
|
|
@ -73,11 +73,18 @@ async function fetchDiscussionStats(): Promise<DiscussionNode[]> {
|
|||
}
|
||||
|
||||
const discussions = await fetchDiscussionStats();
|
||||
|
||||
const sortedPosts = [...allPosts].sort(
|
||||
(a, b) =>
|
||||
new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime(),
|
||||
);
|
||||
|
||||
const latestPosts = sortedPosts.slice(0, 5);
|
||||
---
|
||||
|
||||
<ul>
|
||||
{
|
||||
allPosts.map((post: any) => {
|
||||
latestPosts.map((post: any) => {
|
||||
const formattedDate = new Date(post.data.pubDate)
|
||||
.toISOString()
|
||||
.split("T")[0];
|
||||
|
|
@ -112,9 +119,35 @@ const discussions = await fetchDiscussionStats();
|
|||
}
|
||||
</ul>
|
||||
|
||||
<div class="more-link-wrap">
|
||||
<a href={`/${lang}/timeline`} class="more-link">
|
||||
{lang === "zh" ? ">>>更多" : ">>>More"}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.more-link-wrap {
|
||||
margin-top: 1rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.more-link {
|
||||
color: #6f8090;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
font-size: 1.14rem;
|
||||
}
|
||||
|
||||
.more-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
:global(.dark) .more-link {
|
||||
color: #aab7c4;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||
import PostList from "@/components/Posts/PostList.astro";
|
||||
import Comments from "@/components/Comments.astro";
|
||||
import { getLangFromUrl, getTranslations } from "@/i18n";
|
||||
import "@/styles/global.css";
|
||||
|
||||
|
|
@ -23,6 +24,10 @@ const pageTitle = t.home.title;
|
|||
<div class="section-divider"></div>
|
||||
|
||||
<PostList />
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<Comments limit={7} />
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue