feat(counts): 浏览/评论徽标接入文章与瞬间页,修同载双计(5h T4)

用户要「加一个访客数量、评论数量,采用类似于开源页面从 GitHub 移植过来的那个
眼睛图标」——落地为 ViewMeta 本地 SVG 徽标,无第三方图片依赖。

接入(op-artalk 车道):
- 文章详情页 reading/tech 两分支各挂一枚 mode="page"(两分支互斥,同页仅一处计数)
- ArtalkComments 顶部旧的纯文本 pv 行与自成一套的 bumpPv 脚本整段删除,改为
  readonly chip:评论区只读展示,不再各计一次
- 瞬间页顶部整页 pv 行同样改 chip;每条瞬间卡片底部为 readonly 徽标

组件修复(本人,op-artalk 实测揪出):
- **同载双计**:脚本既注册 astro:page-load 又立即调用 run(),首屏两者都会触发
  → 每元素 POST 两次、浏览量翻倍(实测 15→17→19 每载 +2)。加每元素一次性
  守卫后 1/2/3 每载 +1;软导航新元素仍照常计数
- **同页两枚数字差 1**:readonly 实例独立 GET 到的 pv 可能比计过数的少 1;
  改为同键存在 page 实例时 readonly 让位,由 page 实例回填同一份结果
- page 实例请求失败时释放兄弟自读,避免评论区永远停在「–」

验证: 构建 exit 0;E2E 37/37;Playwright 拦截对账——
      每载 POST=1、同页两枚徽标文本一致(101/101)、刷新后累计 2、
      接口全挂时两枚均显示占位「–」不报错

Spec: 2026-11-02_blog-5h-content-social (T4)

Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-11-02 20:29:44 +08:00
1 parent 8e74bead62
commit 63c9c8ad4a
6 files changed
+52 -67

No files matched your search

+31 -2
View File
@@ -111,6 +111,12 @@ const L = {
const loadOne = async (root: HTMLElement) => {
const key = root.dataset.pageKey;
if (!key) return;
// 每元素只处理一次(2026-11-02 实测教训):首屏 astro:page-load 与本脚本的
// 立即调用会双触发,page 模式每元素会 POST 两次 → 浏览量翻倍(实测
// 15→17→19 每载 +2,加此守卫后 1/2/3 每载 +1)。软导航新页面元素是新的,
// 故不牺牲「换页重计」语义。
if (root.dataset.vmDone === '1') return;
root.dataset.vmDone = '1';
const mode = root.dataset.mode ?? 'page';
try {
// page 模式先计一次浏览(POST 绕 ESA 缓存),再读评论数;
@@ -126,7 +132,15 @@ const L = {
`${API}/comments?site_name=${encodeURIComponent(SITE)}&page_key=${encodeURIComponent(key)}&limit=1`,
);
const meta = (await res.json()) as Meta;
fill(root, { count: meta.count, page: { pv: data.pv ?? meta.page?.pv } });
const merged: Meta = { count: meta.count, page: { pv: data.pv ?? meta.page?.pv } };
fill(root, merged);
// 同页同键的其它实例(如评论区 chip)复用本次结果:否则它独立 GET 到的
// pv 可能比计过数的这份少 1,两个徽标同页显示不同数字会像 bug。
for (const other of document.querySelectorAll<HTMLElement>('[data-viewmeta]')) {
if (other === root || other.dataset.pageKey !== key) continue;
other.dataset.vmDone = '1';
fill(other, merged);
}
return;
}
const res = await fetch(
@@ -134,12 +148,27 @@ const L = {
);
fill(root, (await res.json()) as Meta);
} catch {
// 页模式失败:同键 readonly 兄弟此前被跳过,这里放它们自己读,避免永远「–」
for (const other of document.querySelectorAll<HTMLElement>('[data-viewmeta]')) {
if (other === root || other.dataset.pageKey !== key || other.dataset.vmDone === '1') continue;
delete other.dataset.vmDone;
void loadOne(other);
}
/* 接口不可用:保留占位「–」,不显示 0 误导读者 */
}
};
const run = () => {
for (const root of document.querySelectorAll<HTMLElement>('[data-viewmeta]')) {
const all = [...document.querySelectorAll<HTMLElement>('[data-viewmeta]')];
// 同键存在 page 实例时,readonly 实例等它回填(它拿到的是计过数的 pv,
// 与 section 内另一枚徽标必然一致);否则同页两枚会显示差 1 的两个数字。
const pageKeys = new Set(
all
.filter((el) => (el.dataset.mode ?? 'page') === 'page' && el.dataset.pageKey)
.map((el) => el.dataset.pageKey as string),
);
for (const root of all) {
if ((root.dataset.mode ?? 'page') === 'readonly' && pageKeys.has(root.dataset.pageKey ?? '')) continue;
void loadOne(root);
}
};