mirror of
https://codeberg.org/HPCesia/AstralHalo.git
synced 2025-04-08 17:34:27 +08:00
feat(aside): recent comments card
This commit is contained in:
parent
5af071b0e7
commit
5e341cdfad
104
src/components/aside/ResentCommentsCard.astro
Normal file
104
src/components/aside/ResentCommentsCard.astro
Normal file
@ -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`
|
||||
);
|
||||
}
|
||||
})()
|
||||
}
|
54
src/components/aside/recent-comments/Twikoo.astro
Normal file
54
src/components/aside/recent-comments/Twikoo.astro
Normal file
@ -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>
|
66
src/components/aside/recent-comments/Waline.astro
Normal file
66
src/components/aside/recent-comments/Waline.astro
Normal file
@ -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>
|
@ -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 = {
|
||||
|
@ -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',
|
||||
}
|
||||
|
@ -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`.',
|
||||
};
|
||||
|
@ -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`。',
|
||||
};
|
||||
|
@ -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`。',
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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 = {
|
||||
|
Loading…
Reference in New Issue
Block a user