梨尽兴 | Li's Blog

Back

an image targetting my articlean image targetting my article
机器人
Li's AI Summary
Li's AI
文章介绍了如何通过WalineAPI获取网站最新评论数据并展示在组件中,首先,浏览了Waline的官方接口文档,发现可以通过API返回最新评论数据,然后,编写了一个适用于当前主题的评论展示组件,并在GitHub上开源了代码片段,最后,添加了必要的文件内容,包括导入RecentComments组件和定义组件结构。
本摘要由AI生成,仅供参考,内容准确性请以原文为准。

起因#

浏览 Waline的官方接口文档 时发现通过 /api/comment?type=recent 接口能返回网站最新评论数据,于是“奴役”AI给我写了一个适用当前主题的评论展示组件,目前没出啥大差错🤣

引入#

代码片段Gist开源地址:Recent Comments for Astro-Theme-Pure

recentcomment.ts文件#

src/plugins 目录下新建 recentcomment.ts 文件并填入以下内容(注意需要替换的部分):

recentcomment.ts
/**
 * 获取并处理最新评论数据
 * 通过Waline API获取最新评论并格式化显示
 */

// 定义评论数据接口
interface WalineComment {
  nick: string // 评论者昵称
  comment: string // 评论内容
  url: string // 评论页面URL
  avatar: string // 头像URL
  time: number // 评论时间戳
  like?: number // 点赞数
  addr?: string // 地址
}

// 定义API响应接口
interface WalineResponse {
  data: WalineComment[]
}

/**
 * 获取最新评论数据
 * @param limit 获取评论数量限制
 * @returns 格式化后的评论数据
 */
export async function fetchRecentComments(limit: number = 5): Promise<WalineComment[]> {
  try {
    // <Waline后端地址> 请将该部分替换为你自己搭建的Waline后端服务地址
    const response = await fetch('https://<Waline后端地址>/api/comment?type=recent')
    if (!response.ok) {
      throw new Error(`获取评论失败: ${response.status}`)
    }

    const data: WalineResponse = await response.json()
    return data.data.slice(0, limit)
  } catch (error) {
    console.error('获取最新评论出错:', error)
    return []
  }
}

/**
 * 格式化评论时间
 * @param timestamp 时间戳
 * @returns 格式化后的时间字符串
 */
export function formatCommentTime(timestamp: number): string {
  const date = new Date(timestamp)
  const now = new Date()
  const diff = now.getTime() - 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')}`
}

/**
 * 清理HTML标签
 * @param html HTML字符串
 * @returns 清理后的纯文本
 */
export function stripHtml(html: string): string {
  // 使用正则表达式替代DOM操作,可在服务器端运行
  return html.replace(/<[^>]*>/g, '')
}

/**
 * 截断文本
 * @param text 原始文本
 * @param length 最大长度
 * @returns 截断后的文本
 */
export function truncateText(text: string, length: number = 100): string {
  if (text.length <= length) return text
  return text.substring(0, length) + '...'
}
ts

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;
  }
}
css

RecentComments.astro文件#

  1. 在目录下 src/components 目录下新建 RecentComments.astro 文件并填入以下内容:
RecentComments.astro
---
import { Icon } from 'astro-pure/user'
import {
  fetchRecentComments,
  formatCommentTime,
  stripHtml,
  truncateText
} from '@/plugins/recentcomment'

// 获取最新评论数据
const comments = await fetchRecentComments(5)
---

<div class='recent-comments'>
  <div class='recent-comments-title'>
    <Icon name='list' />
    <span>最新评论</span>
  </div>

  <div class='comment-list'>
    {
      comments.length > 0 ? (
        comments.map((comment) => (
          <div class='comment-item'>
            <div class='comment-avatar'>
              <img src={comment.avatar} alt={comment.nick} loading='lazy' />
            </div>
            <div class='comment-content'>
              <div class='comment-meta'>
                <span class='comment-author'>{comment.nick}</span>
                <span class='comment-time'>{formatCommentTime(comment.time)}</span>
              </div>
              <div class='comment-text' set:html={truncateText(stripHtml(comment.comment), 100)} />
              <a href={comment.url ? comment.url.replace(/\/$/, '') : '#'} class='comment-link'>
                查看详情 →
              </a>
            </div>
          </div>
        ))
      ) : (
        <div class='comment-empty'>暂无评论数据</div>
      )
    }
  </div>
</div>

<script>
  // 客户端清理HTML标签
  document.addEventListener('astro:page-load', () => {
    const commentTexts = document.querySelectorAll('.comment-text')
    commentTexts.forEach((element) => {
      // 确保链接在新窗口打开
      const links = element.querySelectorAll('a')
      links.forEach((link) => {
        link.setAttribute('target', '_blank')
        link.setAttribute('rel', 'noopener noreferrer')
      })
    })
  })
</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
通过Waline API获取最新评论并显示
https://blog.ljx.icu/blog/show-recent-comments
Author Ljx
Published at November 2, 2025
Comment seems to stuck. Try to refresh?✨