Commit 5e341cd
Changed files (10)
src
components
aside
recent-comments
i18n
pages
types
src/components/aside/recent-comments/Twikoo.astro
@@ -0,0 +1,54 @@
+<script>
+ import { asideConfig, commentConfig } from '@/config';
+ import { loadScript } from '@/scripts/utils';
+ import { CDN } from '@constants/cdn.mjs';
+ import {
+ cleanCommentHtml,
+ createCommentItem,
+ getTemplate,
+ } from '../ResentCommentsCard.astro';
+
+ const twikooConfig = commentConfig.twikoo!;
+
+ async function setup() {
+ const waitTwikoo = () => {
+ if (typeof twikoo === 'undefined') {
+ setTimeout(waitTwikoo, 100);
+ }
+ };
+
+ // 判断当前页面是否已经加载了 Twikoo 的脚本
+ // 如果没有加载,则动态加载
+ const twikooWrapper = document.getElementById('twikoo-wrap');
+ if (!twikooWrapper) {
+ await loadScript(CDN.twikoo);
+ }
+ waitTwikoo();
+
+ const recentComments = await twikoo.getRecentComments({
+ envId: twikooConfig.envId,
+ pageSize: asideConfig.recentComment.count,
+ });
+
+ const { container, template } = getTemplate()!;
+ if (container && !template) {
+ // 说明已经加载完毕, 模板被删除了
+ return;
+ }
+
+ recentComments.forEach((comment) =>
+ createCommentItem(container, template!, {
+ avatarUrl: comment.avatar,
+ commentContent: cleanCommentHtml(comment.comment),
+ commentUrl: `${comment.url}#${comment.id}`,
+ author: comment.nick,
+ time: new Date(comment.created),
+ })
+ );
+
+ container.removeChild(template!);
+ }
+
+ document.addEventListener('astro:page-load', setup);
+ await setup();
+</script>
src/components/aside/recent-comments/Waline.astro
@@ -0,0 +1,66 @@
+<script>
+ import { asideConfig, commentConfig } from '@/config';
+ import { joinUrl } from '@utils/url-utils';
+ import {
+ cleanCommentHtml,
+ createCommentItem,
+ getTemplate,
+ } from '../ResentCommentsCard.astro';
+
+ async function setup() {
+ const walineConfig = commentConfig.waline!;
+ const commentCount = asideConfig.recentComment.count;
+ const apiUrl = joinUrl(
+ walineConfig.serverURL,
+ `api/comment?type=recent&count=${commentCount}`
+ );
+
+ const response = await fetch(apiUrl, {
+ method: 'GET',
+ });
+ if (!response.ok) {
+ throw new Error('Failed to fetch recent comments');
+ }
+
+ const data: {
+ nick: string;
+ sticky: 0 | 1;
+ status: string;
+ link: string;
+ comment: string;
+ url: string;
+ user_id: string;
+ objectId: string;
+ browser: string;
+ os: string;
+ type: string;
+ label: string;
+ avatar: string;
+ orig: string;
+ addr: string;
+ like: number;
+ time: number;
+ }[] = (await response.json()).data;
+
+ const { container, template } = getTemplate()!;
+ if (container && !template) {
+ // 说明已经加载完毕, 模板被删除了
+ return;
+ }
+
+ data.forEach((item) =>
+ createCommentItem(container, template!, {
+ avatarUrl: item.avatar,
+ commentContent: cleanCommentHtml(item.comment),
+ commentUrl: `${item.url}#${item.objectId}`,
+ author: item.nick,
+ time: new Date(item.time),
+ })
+ );
+
+ container.removeChild(template!);
+ }
+
+ document.addEventListener('astro:page-load', setup);
+ await setup();
+</script>
src/components/aside/ResentCommentsCard.astro
@@ -0,0 +1,104 @@
+---
+import { commentConfig, siteConfig } from '@/config';
+import I18nKey from '@i18n/I18nKey';
+import { i18n } from '@i18n/translation';
+import TwikooRecentCommentScript from './recent-comments/Twikoo.astro';
+import WalineRecentCommentScript from './recent-comments/Waline.astro';
+
+export function getTemplate() {
+ const recentCommentsCard = document.getElementById('recent-comments-card');
+ if (!recentCommentsCard) {
+ console.error('Recent comments card not found');
+ return;
+ }
+ const recentCommentsList = recentCommentsCard.querySelector('ul')!;
+ const recentCommentTemplate = recentCommentsList.querySelector('template');
+ return {
+ container: recentCommentsList,
+ template: recentCommentTemplate,
+ };
+}
+
+export function cleanCommentHtml(htmlString: string) {
+ return htmlString
+ .replaceAll(/<img.*?src="(.*?)"?[^>]+>/gi, i18n(I18nKey.commentReplaceImage)!)
+ .replaceAll(
+ /<a[^>]+?href=["']?([^"']+)["']?[^>]*>([^<]+)<\/a>/gi,
+ i18n(I18nKey.commentReplaceLink)!
+ )
+ .replaceAll(/<pre><code[^>]+?>.*?<\/pre>/gis, i18n(I18nKey.commentReplaceCode)!)
+ .replaceAll(/<[^>]+>/g, '');
+}
+
+export function createCommentItem(
+ container: HTMLUListElement,
+ template: HTMLTemplateElement,
+ data: {
+ avatarUrl: string;
+ commentContent: string;
+ commentUrl: string;
+ author: string;
+ time: Date;
+ }
+) {
+ const item = template.content.firstElementChild!.cloneNode(true) as HTMLLIElement;
+ const avatarLinkEl = item.querySelector('a.avatar')!;
+ const avatarWrapper = avatarLinkEl.querySelector('div')!;
+ const avatarImg = new Image();
+ const comment = item.querySelector('a.line-clamp-2')!;
+ const time = item.querySelector('time')!;
+
+ avatarLinkEl.setAttribute('href', data.commentUrl);
+ comment.setAttribute('href', data.commentUrl);
+
+ avatarImg.src = data.avatarUrl;
+ avatarImg.alt = data.author;
+ avatarWrapper.appendChild(avatarImg);
+
+ comment.innerHTML = data.commentContent;
+ time.setAttribute('datetime', data.time.toISOString());
+ time.innerText = data.time.toLocaleDateString(siteConfig.lang.replace('_', '-'), {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ });
+
+ container.appendChild(item);
+}
+---
+
+<div id="recent-comments-card" class="card border-base-300 bg-base-200/25 border">
+ <div class="card-body px-4 py-2">
+ <div class="card-title">
+ {i18n(I18nKey.recentComments)}
+ </div>
+ <ul class="list">
+ <template>
+ <li class="list-row px-0">
+ <a class="avatar">
+ <div class="w-16 min-w-16 rounded-md"></div>
+ </a>
+ <div class="flex w-full flex-col justify-between">
+ <a class="hover:text-primary line-clamp-2 w-full overflow-clip"></a>
+ <time class="text-base-content/60 text-xs"></time>
+ </div>
+ </li>
+ </template>
+ </ul>
+ </div>
+</div>
+
+{
+ (() => {
+ switch (commentConfig.provider) {
+ case 'twikoo':
+ return <TwikooRecentCommentScript />;
+ case 'waline':
+ return <WalineRecentCommentScript />;
+ default:
+ throw new Error(
+ `Unsupported comment provider: '${commentConfig.provider}' for recent comments`
+ );
+ }
+ })()
+}
src/i18n/langs/en.ts
@@ -15,6 +15,7 @@ export const en: Translation = {
[Key.randomPost]: 'Random Post',
[Key.comments]: 'Comments',
+ [Key.recentComments]: 'Recent Comments',
[Key.subscribe]: 'Subscribe',
[Key.backLinks]: 'Back Links',
@@ -48,6 +49,10 @@ export const en: Translation = {
[Key.publishedAt]: 'Published at',
[Key.license]: 'License',
+ [Key.commentReplaceLink]: '[Link]',
+ [Key.commentReplaceImage]: '[Image]',
+ [Key.commentReplaceCode]: '[Code]',
+
[Key.draftDevNote]:
'This is a draft and will only be displayed in `DEV` mode. To disable draft preview, please modify `buildConfig.showDraftsOnDev` to `false` in `src/config.ts`.',
};
src/i18n/langs/zh_CN.ts
@@ -15,6 +15,7 @@ export const zh_CN: Translation = {
[Key.randomPost]: '随机文章',
[Key.comments]: '评论',
+ [Key.recentComments]: '最新评论',
[Key.subscribe]: '订阅',
[Key.backLinks]: '反向链接',
@@ -48,6 +49,10 @@ export const zh_CN: Translation = {
[Key.publishedAt]: '发布于',
[Key.license]: '许可协议',
+ [Key.commentReplaceLink]: '[链接]',
+ [Key.commentReplaceImage]: '[图片]',
+ [Key.commentReplaceCode]: '[代码]',
+
[Key.draftDevNote]:
'这是一篇草稿,只会在 `DEV` 模式下显示。关闭草稿预览,请修改 `src/config.ts` 中的 `buildConfig.showDraftsOnDev` 为 `false`。',
};
src/i18n/langs/zh_TW.ts
@@ -15,6 +15,7 @@ export const zh_TW: Translation = {
[Key.randomPost]: '隨機文章',
[Key.comments]: '評論',
+ [Key.recentComments]: '最新評論',
[Key.subscribe]: '訂閱',
[Key.backLinks]: '反向連結',
@@ -48,6 +49,10 @@ export const zh_TW: Translation = {
[Key.publishedAt]: '發佈於',
[Key.license]: '許可協議',
+ [Key.commentReplaceLink]: '[連結]',
+ [Key.commentReplaceImage]: '[圖片]',
+ [Key.commentReplaceCode]: '[程式碼]',
+
[Key.draftDevNote]:
'這是一篇草稿,只會在 `DEV` 模式下顯示。關閉草稿預覽,請修改 `src/config.ts` 中的 `buildConfig.showDraftsOnDev` 為 `false`。',
};
src/i18n/I18nKey.ts
@@ -12,6 +12,7 @@ enum I18nKey {
randomPost = 'randomPost',
comments = 'comments',
+ recentComments = 'recentComments',
subscribe = 'subscribe',
backLinks = 'backLinks',
@@ -45,6 +46,11 @@ enum I18nKey {
publishedAt = 'publishedAt',
license = 'license',
+ /** The replace text for the comment content, used in Recent Comments. */
+ commentReplaceLink = 'commentReplaceLink',
+ commentReplaceImage = 'commentReplaceImage',
+ commentReplaceCode = 'commentReplaceCode',
+
/** Note in the top of drafts content in dev mode. This key supports markdown syntax, using `markdown-it`. */
draftDevNote = 'draftDevNote',
}
src/pages/[...page].astro
@@ -1,6 +1,7 @@
---
-import { siteConfig } from '@/config';
+import { commentConfig, siteConfig } from '@/config';
import ProfileCard from '@components/aside/ProfileCard.astro';
+import ResentCommentsCard from '@components/aside/ResentCommentsCard.astro';
import SiteInfoCard from '@components/aside/SiteInfoCard.astro';
import CategoryBar from '@components/misc/CategoryBar.astro';
import PostsPage from '@components/PostsPage.astro';
@@ -34,4 +35,7 @@ const categories = await getCategories();
<ProfileCard />
<SiteInfoCard />
</Fragment>
+ <Fragment slot="aside-sticky">
+ {commentConfig.enable && commentConfig.provider !== 'giscus' && <ResentCommentsCard />}
+ </Fragment>
</MainLayout>
src/types/config.ts
@@ -361,6 +361,31 @@ export type AsideConfig = {
*/
stats: ('post-count' | 'last-updated' | 'site-words-count' | 'site-run-days')[];
};
+ /**
+ * Recent comments card.
+ *
+ * 最近评论卡片
+ */
+ recentComment: {
+ /**
+ * Whether to enable the recent comments card.
+ *
+ * 是否启用最近评论卡片。
+ */
+ enable: boolean;
+ /**
+ * The number of recent comments displayed.
+ *
+ * 显示的最近评论数量。
+ */
+ count: number;
+ /**
+ * Whether to show the avatar of the commenter.
+ *
+ * 是否显示评论者的头像。
+ */
+ showAvatar: boolean;
+ };
};
export type LicenseConfig = {
src/config.ts
@@ -129,6 +129,11 @@ export const asideConfig: AsideConfig = {
contents: ['stats', 'tags'],
stats: ['post-count', 'last-updated', 'site-words-count', 'site-run-days'],
},
+ recentComment: {
+ enable: true,
+ count: 5,
+ showAvatar: true,
+ },
};
export const licenseConfig: LicenseConfig = {