

Li's AI Summary
Li's AI
文章介绍了如何通过Waline API获取最新评论并显示在网站中。首先,浏览了Waline的官方接口文档,发现可以通过 /api/comment?type=recent 接口获取最新评论数据。然后,编写了一个适用于当前主题的评论展示组件。接下来,引入了代码片段Gist,并在src/plugins目录下新建了recentcomment.ts文件,并在src/assets/styles目录下新建了rc.css文件。最后,在src/components目录下新建了RecentComments.astro文件,并在Pure主题主页中使用了这个组件。
本摘要由AI生成,仅供参考,内容准确性请以原文为准
起因#
浏览 Waline的官方接口文档 ↗ 时发现通过 /api/comment?type=recent 接口能返回网站最新评论数据,于是“奴役”AI给我写了一个适用当前主题的评论展示组件,目前没出啥大差错🤣
引入#
代码片段Gist开源地址:Recent Comments for Astro-Theme-Pure ↗
rc.css文件#
在 src/assets/styles 目录下新建 rc.css 文件并填入以下内容:
rc.css
/* 最新评论组件样式 */
.recent-comments {
background-color: hsl(var(--background) / var(--un-bg-opacity, 1));
border: 1px solid hsl(var(--border) / var(--un-border-opacity, 1));
border-radius: var(--radius);
padding: 1rem;
margin-bottom: 1.5rem;
margin-top: 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.recent-comments-title {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 1rem;
color: hsl(var(--foreground) / var(--un-text-opacity, 1));
display: flex;
align-items: center;
gap: 0.5rem;
}
.recent-comments-title svg {
width: 1.2rem;
height: 1.2rem;
}
.comment-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.comment-item {
display: flex;
gap: 0.75rem;
padding-bottom: 1rem;
border-bottom: 1px solid hsl(var(--border) / 0.5);
}
.comment-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.comment-avatar {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
}
.comment-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.comment-content {
flex: 1;
min-width: 0;
}
.comment-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.25rem;
}
.comment-author {
font-weight: 500;
color: hsl(var(--foreground) / var(--un-text-opacity, 1));
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.comment-time {
font-size: 0.8rem;
color: hsl(var(--muted-foreground) / var(--un-text-opacity, 1));
}
.comment-text {
font-size: 0.9rem;
color: hsl(var(--muted-foreground) / var(--un-text-opacity, 1));
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-word;
}
.comment-link {
display: block;
font-size: 0.8rem;
color: hsl(var(--primary) / var(--un-text-opacity, 1));
margin-top: 0.25rem;
text-decoration: none;
transition: opacity 0.2s;
}
.comment-link:hover {
opacity: 0.8;
}
.comment-empty {
text-align: center;
padding: 1rem 0;
color: hsl(var(--muted-foreground) / var(--un-text-opacity, 1));
font-size: 0.9rem;
}
/* 响应式设计 */
@media (max-width: 768px) {
.comment-avatar {
width: 2rem;
height: 2rem;
}
.comment-author {
font-size: 0.85rem;
}
.comment-time {
font-size: 0.75rem;
}
.comment-text {
font-size: 0.85rem;
-webkit-line-clamp: 2;
}
}cssRecentComments.astro文件#
- 在目录下
src/components目录下新建RecentComments.astro文件并填入以下内容:
RecentComments.astro
---
import { Icon } from 'astro-pure/user'
import config from '@/site-config'
const limit = 5
const refreshMs = 60_000
---
<div class='recent-comments' data-recent-comments data-limit={limit} data-refresh-ms={refreshMs}>
<div class='recent-comments-title'>
<Icon name='list' class='me-2' color='#A6923F' />
<span>最新评论</span>
</div>
<div class='comment-list'>
<div class='comment-empty'>加载中...</div>
</div>
</div>
<script
is:inline
type='module'
data-astro-rerun
define:vars={{
walineServer: config.integ.waline.server,
defaultLimit: limit,
defaultRefreshMs: refreshMs
}}
>
const normalizeServer = (server) => String(server || '').replace(/\/$/, '')
const formatCommentTime = (timestamp) => {
const date = new Date(timestamp)
const diff = Date.now() - date.getTime()
if (diff < 60 * 1000) return '刚刚'
if (diff < 60 * 60 * 1000) return `${Math.floor(diff / (60 * 1000))}分钟前`
if (diff < 24 * 60 * 60 * 1000) return `${Math.floor(diff / (60 * 60 * 1000))}小时前`
if (diff < 7 * 24 * 60 * 60 * 1000) return `${Math.floor(diff / (24 * 60 * 60 * 1000))}天前`
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
}
const stripHtml = (html) => {
const doc = new DOMParser().parseFromString(String(html || ''), 'text/html')
return doc.body.textContent || ''
}
const truncateText = (text, length = 100) => {
const s = String(text || '')
if (s.length <= length) return s
return s.slice(0, length) + '...'
}
const normalizeUrl = (url) => {
const s = String(url || '')
if (!s) return '#'
return s.endsWith('/') ? s.slice(0, -1) : s
}
const buildKey = (items) =>
items.map((c) => `${c.nick ?? ''}|${c.time ?? ''}|${c.url ?? ''}|${c.avatar ?? ''}`).join('||')
const renderComments = (root, comments) => {
const listEl = root.querySelector('.comment-list')
if (!listEl) return
listEl.replaceChildren()
if (!comments.length) {
const empty = document.createElement('div')
empty.className = 'comment-empty'
empty.textContent = '暂无评论数据'
listEl.appendChild(empty)
return
}
const frag = document.createDocumentFragment()
for (const comment of comments) {
const item = document.createElement('div')
item.className = 'comment-item'
const avatarWrap = document.createElement('div')
avatarWrap.className = 'comment-avatar'
const img = document.createElement('img')
img.src = comment.avatar || ''
img.alt = comment.nick || ''
img.loading = 'lazy'
avatarWrap.appendChild(img)
const content = document.createElement('div')
content.className = 'comment-content'
const meta = document.createElement('div')
meta.className = 'comment-meta'
const author = document.createElement('span')
author.className = 'comment-author'
author.textContent = comment.nick || ''
const time = document.createElement('span')
time.className = 'comment-time'
time.dataset.time = String(comment.time || '')
time.textContent = comment.time ? formatCommentTime(comment.time) : ''
meta.append(author, time)
const text = document.createElement('a')
text.className = 'comment-text'
text.textContent = truncateText(stripHtml(comment.comment), 100)
text.href = normalizeUrl(comment.url) + (comment.objectId ? `#${comment.objectId}` : '')
content.append(meta, text)
item.append(avatarWrap, content)
frag.appendChild(item)
}
listEl.appendChild(frag)
}
const updateTimesOnly = (root) => {
const timeEls = root.querySelectorAll('.comment-time[data-time]')
timeEls.forEach((el) => {
const timestamp = Number(el.dataset.time)
if (!Number.isFinite(timestamp) || timestamp <= 0) return
el.textContent = formatCommentTime(timestamp)
})
}
const fetchRecentComments = async (server, signal) => {
const base = normalizeServer(server)
if (!base) return []
const res = await fetch(`${base}/api/comment?type=recent`, { signal })
if (!res.ok) return []
const json = await res.json()
const data = Array.isArray(json?.data) ? json.data : []
return data
}
const initRecentComments = (root) => {
const limit = Number(root.dataset.limit || defaultLimit) || defaultLimit
const refreshMs = Number(root.dataset.refreshMs || defaultRefreshMs) || defaultRefreshMs
let lastKey = ''
const aborter = new AbortController()
const tick = async () => {
try {
const list = await fetchRecentComments(walineServer, aborter.signal)
const items = list.slice(0, limit)
const key = items.length ? buildKey(items) : '__EMPTY__'
if (key !== lastKey) {
lastKey = key
renderComments(root, items)
} else {
updateTimesOnly(root)
}
} catch (e) {
if (e?.name === 'AbortError') return
console.error('Recent comments error:', e)
const listEl = root.querySelector('.comment-list')
if (listEl) {
listEl.innerHTML = `<div class="comment-empty error">加载失败:${e.message}</div>`
}
}
}
tick()
const timer = window.setInterval(() => {
if (document.hidden) return
tick()
}, refreshMs)
const onVisible = () => {
if (!document.hidden) tick()
}
document.addEventListener('visibilitychange', onVisible)
return () => {
aborter.abort()
window.clearInterval(timer)
document.removeEventListener('visibilitychange', onVisible)
}
}
const disposers = new Set()
const boot = () => {
document.querySelectorAll('[data-recent-comments]').forEach((root) => {
if (root.__recentCommentsInited) return
root.__recentCommentsInited = true
disposers.add(initRecentComments(root))
})
}
const disposeAll = () => {
disposers.forEach((fn) => fn())
disposers.clear()
}
const start = () => boot()
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start, { once: true })
} else {
start()
}
document.addEventListener('astro:page-load', boot)
document.addEventListener('astro:before-swap', disposeAll)
</script>
<style is:global>
@import '@/assets/styles/rc.css';
</style>
astro使用#
在Pure主题主页中使用#
增添 src/pages/index.astro 文件内容如下:
index.astro
---
import RecentComments from '@/components/RecentComments.astro' //引入组件
---
<PageLayout meta={{ title: '首页' }} highlightColor='#FFF8DC'>
<main class='flex w-full flex-col items-center'>
<div
id='content'
class='animate flex flex-col md:flex-row gap-y-10 md:gap-x-6 md:w-4/5 lg:w-5/6'
>
<!-- 省略中间代码 -->
<div class='md:w-1/3 md:mt-0'>
<RecentComments />
</div>
<!-- 省略此后代码 -->
</div>
<Quote class='mt-12' />
</main>
</PageLayout>astro最小化使用#
---
import RecentComments from '@/components/RecentComments.astro'
---
<RecentComments />astro