From 07fbc4b731fbb570fe56a752c7513497206f0150 Mon Sep 17 00:00:00 2001 From: ClovertaTheTrilobita Date: Mon, 13 Apr 2026 22:57:37 +0300 Subject: [PATCH] feat(latest comments): added reply to --- src/components/LatestComments.astro | 132 +++++++++++++++++++++++++++- src/styles/global.css | 14 +++ 2 files changed, 142 insertions(+), 4 deletions(-) diff --git a/src/components/LatestComments.astro b/src/components/LatestComments.astro index 4566eba..4ad0a7b 100644 --- a/src/components/LatestComments.astro +++ b/src/components/LatestComments.astro @@ -75,6 +75,110 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments"; return item?.user?.name || (lang === "zh" ? "匿名用户" : "Anonymous"); } + function normalizePath(url) { + if (!url) return ""; + + try { + const u = new URL(url, window.location.origin); + let path = u.pathname; + + if (!path.startsWith("/")) path = "/" + path; + if (!path.endsWith("/")) path += "/"; + + return path; + } catch { + let path = String(url); + + path = path.replace(/^https?:\/\/[^/]+/i, ""); + if (!path.startsWith("/")) path = "/" + path; + if (!path.endsWith("/")) path += "/"; + + return path; + } + } + + function buildUrlCandidates(rawUrl) { + const path = normalizePath(rawUrl); + if (!path) return []; + + const noSlash = path.endsWith("/") ? path.slice(0, -1) : path; + + const candidates = [ + path, + noSlash, + `/zh${path}`, + `/zh${noSlash}`, + `/en${path}`, + `/en${noSlash}`, + `${window.location.origin}${path}`, + `${window.location.origin}${noSlash}`, + ]; + + return [...new Set(candidates.filter(Boolean))]; + } + + async function fetchPostComments(host, siteId, rawUrl) { + const candidates = buildUrlCandidates(rawUrl); + + for (const candidate of candidates) { + try { + const res = await fetch( + `${host}/api/v1/find?site=${encodeURIComponent(siteId)}&url=${encodeURIComponent(candidate)}&format=plain`, + { credentials: "omit" }, + ); + + if (!res.ok) continue; + + const data = await res.json(); + + const comments = Array.isArray(data) + ? data + : Array.isArray(data?.comments) + ? data.comments + : []; + + if (comments.length > 0) { + return comments; + } + } catch {} + } + + return []; + } + + async function enrichReplyTargets(host, siteId, comments) { + const urls = [ + ...new Set( + comments.map((item) => item?.locator?.url).filter(Boolean), + ), + ]; + + const postMap = new Map(); + + await Promise.all( + urls.map(async (url) => { + const postComments = await fetchPostComments(host, siteId, url); + postMap.set(url, postComments); + }), + ); + + return comments.map((item) => { + if (!item?.pid) { + return { ...item, replyToName: "" }; + } + + const postComments = postMap.get(item?.locator?.url) || []; + const parent = postComments.find( + (c) => String(c?.id) === String(item?.pid), + ); + + return { + ...item, + replyToName: parent?.user?.name || "", + }; + }); + } + function renderComments(root, comments, lang) { const list = root.querySelector(".latest-comments-list"); const loading = root.querySelector(".comments-loading"); @@ -90,20 +194,35 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments"; list.innerHTML = comments .map((item) => { - const author = escapeHtml(getUserName(item, lang)); + const rawAuthor = getUserName(item, lang); + const author = escapeHtml(rawAuthor); + const fallbackInitial = escapeHtml( + rawAuthor.slice(0, 1).toUpperCase(), + ); + const avatar = getAvatar(item); const safeAvatar = escapeHtml(avatar || ""); + const title = escapeHtml( item?.title || (lang === "zh" ? "未命名文章" : "Untitled post"), ); const href = rewritePostUrl(item?.locator?.url || "", lang); + + // Remark42 返回的 text 是处理后的 HTML,适合直接渲染 const html = item?.text || ""; const time = formatTime(item?.time, lang); + const replyTo = escapeHtml(item?.replyToName || ""); + const authorLine = replyTo + ? `@${author}${ + lang === "zh" ? "回复" : "replied to" + }@${replyTo}` + : `@${author}`; + const avatarHtml = avatar ? `${author}` - : ``; + : ``; return `
@@ -115,7 +234,7 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
- @${author} + ${authorLine} ${time ? `${escapeHtml(time)}` : ""}
@@ -159,7 +278,12 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments"; } const data = await res.json(); - renderComments(section, data, lang); + const enriched = await enrichReplyTargets( + host, + siteId, + Array.isArray(data) ? data : [], + ); + renderComments(section, enriched, lang); } catch (err) { loading.style.display = "none"; list.innerHTML = `

${ diff --git a/src/styles/global.css b/src/styles/global.css index 5e5ead5..7830272 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -361,6 +361,20 @@ img { font-style: italic; } +.comment-reply-sep { + font-size: 0.8rem; + opacity: 0.7; +} + +.latest-comments .comment-reply-to { + font-weight: 400; + font-size: 0.8rem; + color: gray; + opacity: 0.9; + overflow-wrap: anywhere; + word-break: break-word; +} + html.dark .latest-comments .comment-card, .dark .latest-comments .comment-card { border-color: #7f8c97;