Commit 2781f66
Changed files (8)
src
components
aside
pages
src/components/aside/recent-comments/Artalk.ts
@@ -1,71 +1,58 @@
-import {
- cleanCommentHtml,
- cleanPlaceholders,
- createCommentItem,
- getTemplate,
-} from './utils.ts';
+import type { CommentData, CommentProvider } from './types';
+import { cleanCommentHtml } from './utils';
import { asideConfig, commentConfig } from '@/config';
-async function setup() {
- const artalkConfig = commentConfig.artalk!;
- const commentCount = asideConfig.recentComment.count;
-
- const apiCommentUrl = new URL(
- `api/v2/stats/latest_comments?limit=${commentCount}`,
- artalkConfig.serverURL
- ).toString();
- const apiConfigUrl = new URL(`api/v2/conf`, artalkConfig.serverURL).toString();
-
- const responseComment = fetch(apiCommentUrl, {
- method: 'GET',
- });
- const responseConfig = fetch(apiConfigUrl, {
- method: 'GET',
- });
- if (!(await responseComment).ok) {
- throw new Error('Failed to fetch recent comments');
- }
- if (!(await responseConfig).ok) {
- throw new Error('Failed to fetch comment config');
- }
-
- const commentData: {
- id: number;
- nick: string;
- content_marked: string;
- date: string;
- email_encrypted: string;
- page_url: string;
- }[] = (await (await responseComment).json()).data;
- const configData: {
- gravatar: {
- mirror: string;
- params: string;
+export const ArtalkProvider: CommentProvider = {
+ async setup() {
+ const artalkConfig = commentConfig.artalk!;
+ const commentCount = asideConfig.recentComment.count;
+
+ const apiCommentUrl = new URL(
+ `api/v2/stats/latest_comments?limit=${commentCount}`,
+ artalkConfig.serverURL
+ ).toString();
+ const apiConfigUrl = new URL(`api/v2/conf`, artalkConfig.serverURL).toString();
+
+ const responseComment = fetch(apiCommentUrl, {
+ method: 'GET',
+ });
+ const responseConfig = fetch(apiConfigUrl, {
+ method: 'GET',
+ });
+ if (!(await responseComment).ok) {
+ throw new Error('Failed to fetch recent comments');
+ }
+ if (!(await responseConfig).ok) {
+ throw new Error('Failed to fetch comment config');
+ }
+
+ const commentData: {
+ id: number;
+ nick: string;
+ content_marked: string;
+ date: string;
+ email_encrypted: string;
+ page_url: string;
+ }[] = (await (await responseComment).json()).data;
+ const configData: {
+ gravatar: {
+ mirror: string;
+ params: string;
+ };
+ } = (await (await responseConfig).json()).frontend_conf;
+
+ const getAvatarUrl = (email: string) => {
+ return `${configData.gravatar.mirror}${email}?${configData.gravatar.params}`;
};
- } = (await (await responseConfig).json()).frontend_conf;
-
- const getAvatarUrl = (email: string) => {
- return `${configData.gravatar.mirror}${email}?${configData.gravatar.params}`;
- };
-
- const { container, template } = getTemplate()!;
- if (container && !template) {
- // 说明已经加载完毕, 模板被删除了
- return;
- }
-
- commentData.forEach((item) =>
- createCommentItem(container, template!, {
- avatarUrl: getAvatarUrl(item.email_encrypted),
- commentContent: cleanCommentHtml(item.content_marked),
- commentUrl: `${item.page_url}#atk-comment-${item.id}`,
- author: item.nick,
- time: new Date(item.date),
- })
- );
-
- cleanPlaceholders(container, template!);
-}
-document.addEventListener('astro:page-load', setup);
-await setup();
+ return commentData.map(
+ (item): CommentData => ({
+ avatarUrl: getAvatarUrl(item.email_encrypted),
+ commentContent: cleanCommentHtml(item.content_marked),
+ commentUrl: `${item.page_url}#atk-comment-${item.id}`,
+ author: item.nick,
+ time: new Date(item.date),
+ })
+ );
+ },
+};
src/components/aside/recent-comments/Twikoo.ts
@@ -1,53 +1,40 @@
-import {
- cleanCommentHtml,
- cleanPlaceholders,
- createCommentItem,
- getTemplate,
-} from './utils.ts';
+import type { CommentData, CommentProvider } from './types';
+import { cleanCommentHtml } from './utils';
import { asideConfig, commentConfig } from '@/config';
import { loadScript } from '@/scripts/utils';
import { CDN } from '@constants/cdn';
const twikooConfig = commentConfig.twikoo!;
-async function setup() {
- const waitTwikoo = () => {
- if (typeof twikoo === 'undefined') {
- setTimeout(waitTwikoo, 100);
+export const TwikooProvider: CommentProvider = {
+ async setup() {
+ const waitTwikoo = () => {
+ if (typeof twikoo === 'undefined') {
+ setTimeout(waitTwikoo, 100);
+ }
+ };
+
+ // 判断当前页面是否已经加载了 Twikoo 的脚本
+ // 如果没有加载,则动态加载
+ const twikooWrapper = document.getElementById('twikoo-wrap');
+ if (!twikooWrapper) {
+ await loadScript(CDN.twikoo);
}
- };
-
- // 判断当前页面是否已经加载了 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),
- })
- );
-
- cleanPlaceholders(container, template!);
-}
-
-document.addEventListener('astro:page-load', setup);
-await setup();
+ waitTwikoo();
+
+ const recentComments = await twikoo.getRecentComments({
+ envId: twikooConfig.envId,
+ pageSize: asideConfig.recentComment.count,
+ });
+
+ return recentComments.map(
+ (comment): CommentData => ({
+ avatarUrl: comment.avatar,
+ commentContent: cleanCommentHtml(comment.comment),
+ commentUrl: `${comment.url}#${comment.id}`,
+ author: comment.nick,
+ time: new Date(comment.created),
+ })
+ );
+ },
+};
src/components/aside/recent-comments/types.ts
@@ -0,0 +1,11 @@
+export interface CommentData {
+ avatarUrl: string;
+ commentContent: string;
+ commentUrl: string;
+ author: string;
+ time: Date;
+}
+
+export interface CommentProvider {
+ setup: () => Promise<CommentData[]>;
+}
src/components/aside/recent-comments/utils.ts
@@ -1,21 +1,6 @@
-import { siteConfig } from '@/config';
import I18nKey from '@i18n/I18nKey';
import { i18n } from '@i18n/translation';
-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)!)
@@ -26,47 +11,3 @@ export function cleanCommentHtml(htmlString: string) {
.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);
-}
-
-export function cleanPlaceholders(container: HTMLUListElement, template: HTMLTemplateElement) {
- template.remove();
- const placeholders = container.querySelectorAll('.comment-placeholder');
- placeholders.forEach((placeholder) => {
- placeholder.remove();
- });
-}
src/components/aside/recent-comments/Waline.ts
@@ -1,61 +1,48 @@
-import {
- cleanCommentHtml,
- cleanPlaceholders,
- createCommentItem,
- getTemplate,
-} from './utils.ts';
+import type { CommentData, CommentProvider } from './types';
+import { cleanCommentHtml } from './utils';
import { asideConfig, commentConfig } from '@/config';
-async function setup() {
- const walineConfig = commentConfig.waline!;
- const commentCount = asideConfig.recentComment.count;
- const apiUrl = `${walineConfig.serverURL}/api/comment?type=recent&count=${commentCount}`;
+export const WalineProvider: CommentProvider = {
+ async setup() {
+ const walineConfig = commentConfig.waline!;
+ const commentCount = asideConfig.recentComment.count;
+ const apiUrl = `${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 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 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),
- })
- );
-
- cleanPlaceholders(container, template!);
-}
-
-document.addEventListener('astro:page-load', setup);
-await setup();
+ return data.map(
+ (item): CommentData => ({
+ avatarUrl: item.avatar,
+ commentContent: cleanCommentHtml(item.comment),
+ commentUrl: `${item.url}#${item.objectId}`,
+ author: item.nick,
+ time: new Date(item.time),
+ })
+ );
+ },
+};
src/components/aside/RecentCommentsCard.vue
@@ -0,0 +1,99 @@
+<script setup lang="ts">
+import { ArtalkProvider } from './recent-comments/Artalk';
+import { TwikooProvider } from './recent-comments/Twikoo';
+import { WalineProvider } from './recent-comments/Waline';
+import type { CommentData } from './recent-comments/types';
+import { asideConfig, commentConfig, siteConfig } from '@/config';
+import I18nKey from '@i18n/I18nKey';
+import { i18n } from '@i18n/translation';
+import { onMounted, ref } from 'vue';
+
+const comments = ref<CommentData[]>([]);
+const loading = ref(true);
+
+// 根据评论系统类型加载不同的评论数据
+async function loadComments() {
+ const provider = (() => {
+ switch (commentConfig.provider) {
+ case 'twikoo':
+ return TwikooProvider;
+ case 'waline':
+ return WalineProvider;
+ case 'artalk':
+ return ArtalkProvider;
+ default:
+ throw new Error(
+ `Unsupported comment provider: '${commentConfig.provider}' for recent comments`
+ );
+ }
+ })();
+
+ try {
+ comments.value = await provider.setup();
+ } catch (error) {
+ console.error('Failed to load recent comments:', error);
+ comments.value = [];
+ } finally {
+ loading.value = false;
+ }
+}
+
+onMounted(() => {
+ loadComments();
+});
+</script>
+
+<template>
+ <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 v-if="!loading">
+ <li v-for="comment in comments" :key="comment.commentUrl" class="list-row px-0">
+ <a class="avatar" :href="comment.commentUrl">
+ <div class="w-16 min-w-16 rounded-md">
+ <img :src="comment.avatarUrl" :alt="comment.author" />
+ </div>
+ </a>
+ <div class="flex w-full flex-col justify-between">
+ <a
+ :href="comment.commentUrl"
+ class="hover:text-primary line-clamp-2 w-full overflow-clip"
+ v-html="comment.commentContent"
+ ></a>
+ <time :datetime="comment.time.toISOString()" class="text-base-content/60 text-xs">
+ {{
+ comment.time.toLocaleDateString(siteConfig.lang.replace('_', '-'), {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ })
+ }}
+ </time>
+ </div>
+ </li>
+ </template>
+ <template v-else>
+ <li
+ v-for="n in asideConfig.recentComment.count"
+ :key="n"
+ class="list-row comment-placeholder px-0"
+ >
+ <a class="avatar">
+ <div class="skeleton w-16 min-w-16 rounded-md" />
+ </a>
+ <div class="flex w-full flex-col justify-between">
+ <div class="flex flex-col gap-2">
+ <div class="skeleton h-4 w-full" />
+ <div class="skeleton h-4 w-[100%-2rem]" />
+ </div>
+ <div class="skeleton h-4 w-10" />
+ </div>
+ </li>
+ </template>
+ </ul>
+ </div>
+ </div>
+</template>
src/components/aside/ResentCommentsCard.astro
@@ -1,59 +0,0 @@
----
-import { asideConfig, commentConfig } from '@/config';
-import I18nKey from '@i18n/I18nKey';
-import { i18n } from '@i18n/translation';
----
-
-<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>
- {
- Array.from({ length: asideConfig.recentComment.count }).map(() => (
- <li class="list-row comment-placeholder px-0">
- <a class="avatar">
- <div class="skeleton w-16 min-w-16 rounded-md" />
- </a>
- <div class="flex w-full flex-col justify-between">
- <div class="flex flex-col gap-2">
- <div class="skeleton h-4 w-full" />
- <div class="skeleton h-4 w-[100%-2rem]" />
- </div>
- <div class="skeleton h-4 w-10" />
- </div>
- </li>
- ))
- }
- </ul>
- </div>
-</div>
-
-{
- (() => {
- switch (commentConfig.provider) {
- case 'twikoo':
- return <script src="recent-comments/Twikoo.ts" />;
- case 'waline':
- return <script src="recent-comments/Waline.ts" />;
- case 'artalk':
- return <script src="recent-comments/Artalk.ts" />;
- default:
- throw new Error(
- `Unsupported comment provider: '${commentConfig.provider}' for recent comments`
- );
- }
- })()
-}
src/pages/[...page].astro
@@ -1,7 +1,7 @@
---
import { asideConfig, commentConfig, siteConfig } from '@/config';
import ProfileCard from '@components/aside/ProfileCard.astro';
-import ResentCommentsCard from '@components/aside/ResentCommentsCard.astro';
+import ResentCommentsCard from '@components/aside/RecentCommentsCard.vue';
import SiteInfoCard from '@components/aside/SiteInfoCard.astro';
import CategoryBar from '@components/misc/CategoryBar.astro';
import PostsPage from '@components/PostsPage.astro';
@@ -36,6 +36,10 @@ const categories = await getCategories();
<SiteInfoCard />
</Fragment>
<Fragment slot="aside-sticky">
- {commentConfig.enable && asideConfig.recentComment.enable && <ResentCommentsCard />}
+ {
+ commentConfig.enable && asideConfig.recentComment.enable && (
+ <ResentCommentsCard client:visible />
+ )
+ }
</Fragment>
</MainLayout>