feat(viewer): likes, bubbles, visit reporting and hot sorting

Task 10 viewer 互动化:

- 点赞:useLikes 批次状态机(500ms 防抖 / 5 次满额即冲 / 失败同 batchId
  退避重试 3 次后回滚丢弃),乐观计数 + 服务端 total 权威校准
  (countVersion 单调),大图与格子双入口;
- WS 实时:RealtimeClient 纯状态机(半频心跳、指数退避重连、三败降级、
  重连成功 resync 第 1 页重取),消息路由器做 likes.burst 自身过滤
  (originBatchIds 求交)、隐藏页暂停与减少动画下仅校准不出气泡;
  photo.hidden/deleted 即时移出序列并回退邻居大图;降级后 8s 轮询接管;
- 浏览上报:visitorId 跨标签 BroadcastChannel first-writer-wins 协调,
  可见时上报、requestId 持久幂等、网络失败指数重试;
- 排序:最新/热门 chips(热门走 ov 冷快照),大图打开期间禁用切换;
  顶栏「减少动画」偏好开关;
- 契约包新增 ./realtime 子路径出口(浏览器面不可引桶出口——其连带
  internal-sync 的 node:crypto 会被 Vite 外置化致整页崩溃);
- e2e 注入钩子 __viewerWsUrl/__viewerWsRetryBaseMs/__viewerRealtimePollMs/
  __viewerBubbleMs 与 .activity[data-connection] 连接态锚。

vitest:viewer 105(新增 36:likes 批次 8、visitor 协调 5、上报门控 4、
路由 8、WS 状态机 7、排序 4);Playwright:viewer 21(新增 likes.spec 7、
journey 补互动腿断言)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-18 19:52:27 +08:00
parent 955bea49b5
commit cdead49838
29 changed files with 2581 additions and 29 deletions
+38
View File
@@ -0,0 +1,38 @@
// apps/viewer/src/ui/prefs.ts —— 「减少动画」偏好(P2 Task 10)。
// localStorage 'viewer.reducedMotion'('1' 开 / 缺省关),模块级单例 ref:
// App.vue 顶栏开关与 ActivityPage 气泡门禁共享同一状态。开启时根节点挂
// viewer-reduced-motion 类,CSS 一并禁用心跳等小动画(仅保数字跳动)。
import { ref, watch } from 'vue';
export const REDUCED_MOTION_KEY = 'viewer.reducedMotion';
function readStored(): boolean {
try {
return localStorage.getItem(REDUCED_MOTION_KEY) === '1';
} catch {
return false;
}
}
export const reducedMotion = ref(readStored());
export function setReducedMotion(enabled: boolean): void {
reducedMotion.value = enabled;
try {
if (enabled) localStorage.setItem(REDUCED_MOTION_KEY, '1');
else localStorage.removeItem(REDUCED_MOTION_KEY);
} catch {
// 存储不可用:仅会话内生效
}
}
export function toggleReducedMotion(): void {
setReducedMotion(!reducedMotion.value);
}
// 根节点类联动(模块加载即生效,无需组件挂载;SSR 无此环境)。
if (typeof document !== 'undefined') {
watch(reducedMotion, (enabled) => {
document.documentElement.classList.toggle('viewer-reduced-motion', enabled);
}, { immediate: true });
}