1+ ---
2+ import LiquidGlassLess from " @components/common/LiquidGlassLess.astro" ;
3+ import { getEntries } from " @lib/contentParser" ;
4+ import type { BlogEntry , PagesEntry } from " @/types" ;
5+
6+ const { limit = 8 } = Astro .props as { limit? : number };
7+ const allPosts = (await getEntries (" blog" )) as BlogEntry [];
8+ const pages = (await getEntries (" pages" )) as PagesEntry [];
9+ const titleMap = Object .fromEntries (
10+ [... allPosts ,... pages ].map ((p ) => [p .id , p .data .title ])
11+ );
12+ ---
13+
14+ <section class =" rounded-lg pt-3" >
15+ <div class =" flex items-center justify-between mb-4 px-3" >
16+ <h3 class =" text-xl font-bold text-txt-p dark:text-darkmode-txt-p" >
17+ 最近留言
18+ </h3 >
19+ </div >
20+ <ul id =" recent-comments-list" class =" px-2 md:px-3 space-y-1" ></ul >
21+ </section >
22+
23+ <script define:vars ={ {
24+ twikooEnvId: import .meta .env .PUBLIC_TWIKOO_ENV_ID || " " ,
25+ limit ,
26+ titleMap
27+ }} >
28+ const list = document.getElementById("recent-comments-list");
29+ const maxLen=100;
30+ function showLoading(){
31+ if(!list) return;
32+ list.setAttribute('aria-busy','true');
33+ list.innerHTML = `
34+ <li class="py-4">
35+ <div class="flex items-center justify-center gap-2 text-slate-500 dark:text-slate-200">
36+ <span class="inline-block w-5 h-5 border-2 border-slate-400 border-t-transparent rounded-full animate-spin"></span>
37+ <span>正在加载留言…</span>
38+ </div>
39+ </li>
40+ `;
41+ }
42+ function showError(msg){
43+ if(!list) return;
44+ list.removeAttribute('aria-busy');
45+ list.innerHTML = `
46+ <li class="py-4">
47+ <div class="text-center text-red-600 dark:text-red-400">
48+ ${msg || "评论获取失败,请稍后重试。"}
49+ <button id="rc-retry-btn" class="ml-2 px-2 py-1 text-sm rounded border border-red-300 dark:border-red-700 bg-red-50 hover:bg-red-100 dark:bg-red-900/20">重试</button>
50+ </div>
51+ </li>
52+ `;
53+ const btn = document.getElementById("rc-retry-btn");
54+ if(btn){ btn.addEventListener("click", load, { once: true }); }
55+ }
56+ function stripHtml(s){
57+ try{
58+ return (s||"").replace(/<[^>]*>/g,"").trim()
59+ }catch(e){
60+ console.error('stripHtml报错:',e)
61+ return "";
62+ }
63+ }
64+ function pathFromUrl(u){
65+ try{
66+ //根据/截取路径,获取最后一个路径段
67+ if(u.indexOf("/")!=-1){
68+ u=u.split("/")[u.split("/").length-1];
69+ }
70+ return decodeURIComponent(u)||u
71+ }catch(e){
72+ console.error('格式化路径报错:',e)
73+ return u;
74+ }
75+ }
76+ function ensureTwikoo(cb){
77+ if(window.twikoo){cb();return}
78+ const s=document.createElement("script");
79+ s.src="https://cdn.jsdelivr.net/npm/twikoo@1.6.44/dist/twikoo.min.js";
80+ s.async=true;
81+ s.onload=cb;
82+ document.body.appendChild(s);
83+ }
84+ function render(items){
85+ if(list){ list.removeAttribute('aria-busy'); }
86+ if(!list || !items || items.length==0) {
87+ //显示一个空提示
88+ list.innerHTML=`
89+ <li class="text-slate-500 dark:text-slate-200 text-center">还没有留言哦</li>
90+ `;
91+ return;
92+ }
93+ list.innerHTML="";
94+ items.forEach((it,index)=>{
95+ const url=it.url||it.link||"";
96+ const path=pathFromUrl(url);
97+ const title=titleMap[path]||path;
98+ const full=stripHtml(it.comment||it.content||"");
99+ const short=full.length>maxLen?full.slice(0,maxLen)+"…":full;
100+ const li=document.createElement("li");
101+ li.className="group rounded-[25px] hover:bg-primary/5 transition-all duration-300 intersect:animate-fadeUp opacity-0 p-2 hover:bg-orange-500 dark:hover:bg-orange-500 hover:text-darkmode-txt-light [&_*]:hover:text-darkmode-txt-p";
102+ li.style.animationDelay=`${index * 200}ms`;
103+ li.innerHTML=`
104+ <a class="no-underline hover:text-primary" href="${url}">
105+ <div class="text-txt-light dark:text-darkmode-txt-light flex items-center gap-2 mt-1 h-8">
106+ <span class="glass w-9 h-9 rounded-full overflow-hidden hover:rotate-12 duration-300">
107+ <img src="${it.avatar||it.avatarUrl||""}" alt="${it.nick||it.nickname||"匿名"}" class="w-9 h-9 rounded-full">
108+ </span>
109+ <div class="flex flex-col justify-center leading-tight h-8">
110+ <span class="text-base text-txt-p dark:text-darkmode-txt-p">${it.nick||"匿名"}</span>
111+ <span class="text-xs text-slate-500 dark:text-slate-200">${new Date(it.created||Date.now()).toLocaleString()}</span>
112+ </div>
113+ </div>
114+ <div class="text-[#3e5f6a] dark:text-darkmode-txt-p leading-7 line-clamp-2 font-semibold mt-3" title="${full.replace(/"/g,'"')}">${short}</div>
115+ <div class="text-sm italic opacity-0 group-hover:opacity-100 text-slate-600 dark:text-slate-100/50 border-t border-slate-300/30 dark:border-darkmode-border/30 pt-1 text-right transition-opacity duration-300">
116+ 来自:${title}
117+ </div>
118+ </a>
119+ `;
120+ list.appendChild(li);
121+ });
122+ }
123+ function load(){
124+ if(!list){return}
125+ if(!twikooEnvId){
126+ showError("未配置 Twikoo 环境 ID");
127+ return;
128+ }
129+ showLoading();
130+ ensureTwikoo(()=>{
131+ if(!window.twikoo){
132+ showError("评论系统初始化失败");
133+ return;
134+ }
135+ window.twikoo.getRecentComments({ envId: twikooEnvId, pageSize: limit })
136+ .then((res)=>{
137+ const arr=res?.comments||res||[];
138+ console.log('从twikoo获取评论列表',arr)
139+ render(arr);
140+ })
141+ .catch((err)=>{
142+ console.error('获取最近评论失败', err);
143+ showError("评论获取失败,请稍后重试。");
144+ });
145+ });
146+ }
147+ // document.addEventListener("DOMContentLoaded", load);
148+ document.addEventListener("astro:page-load", load);
149+ function ensureTwikoo(cb){
150+ if(window.twikoo){cb();return}
151+ const s=document.createElement("script");
152+ s.src="https://cdn.jsdelivr.net/npm/twikoo@1.6.44/dist/twikoo.min.js";
153+ s.async=true;
154+ s.onload=cb;
155+ s.onerror=()=>{
156+ showError("评论系统加载失败,请稍后重试。");
157+ };
158+ document.body.appendChild(s);
159+ }
160+ </script >
0 commit comments