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:
@@ -2,6 +2,7 @@
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { PhotoSequence } from '../feed/sequence';
|
||||
import type { LikesController } from '../likes/useLikes';
|
||||
import { MIN_SCALE, panBy, resetZoom, wheelFactor, zoomAt, type ZoomState } from '../viewer/zoom';
|
||||
|
||||
// 大图查看器(Task 8):browse URL 全屏展示 + 左右切换(保持列表浏览序列,越过
|
||||
@@ -24,6 +25,8 @@ import { MIN_SCALE, panBy, resetZoom, wheelFactor, zoomAt, type ZoomState } from
|
||||
const props = defineProps<{
|
||||
sequence: PhotoSequence;
|
||||
photoId: string;
|
||||
/** 点赞控制器(Task 10):缺省不渲染点赞位(组件单测/降级组装)。 */
|
||||
likes?: LikesController;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -253,6 +256,13 @@ function preload(url: string): void {
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
// —— 点赞(Task 10):乐观 +1 与心跳动画由 LikesController 承担 ——
|
||||
|
||||
function likeCurrent(): void {
|
||||
if (photo.value === null || props.likes === undefined) return;
|
||||
props.likes.like(photo.value.photoId);
|
||||
}
|
||||
|
||||
// —— 签名过期/下线处理 ——
|
||||
|
||||
/** 大图加载失败:静默重取单图接口换新签名 URL;404 → 通知父级移除;其他失败可重试。 */
|
||||
@@ -327,7 +337,17 @@ onBeforeUnmount(() => {
|
||||
|
||||
<header class="bar">
|
||||
<span class="counter">{{ t('viewer.counter', { current: index + 1, total }) }}</span>
|
||||
<span class="likes">{{ t('viewer.likeCount', { count: photo.likeCount }) }}</span>
|
||||
<button
|
||||
v-if="likes !== undefined"
|
||||
type="button"
|
||||
class="control like-button"
|
||||
:aria-label="t('viewer.like')"
|
||||
@click="likeCurrent"
|
||||
>
|
||||
<span :key="likes.popNonce(photo.photoId)" class="heart" aria-hidden="true">♥</span>
|
||||
<span class="like-count">{{ likes.displayFor(photo) }}</span>
|
||||
</button>
|
||||
<span v-else class="likes">{{ t('viewer.likeCount', { count: photo.likeCount }) }}</span>
|
||||
<!-- P4a 预留:下载 / 分享操作位(P1b 不实现)。 -->
|
||||
<button type="button" class="control close" :aria-label="t('viewer.close')" @click="emit('close')">
|
||||
✕
|
||||
@@ -415,6 +435,40 @@ onBeforeUnmount(() => {
|
||||
color: #d1d5db;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.like-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 999px;
|
||||
padding: 4px 12px;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid rgba(249, 250, 251, 0.35);
|
||||
}
|
||||
.like-button .heart {
|
||||
color: #ef4444;
|
||||
display: inline-block;
|
||||
}
|
||||
/* :key 重挂触发心跳放大;「减少动画」时禁用(数字仍即时跳动)。 */
|
||||
.like-button .heart {
|
||||
animation: heart-pop 400ms ease-out;
|
||||
}
|
||||
.like-count {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
@keyframes heart-pop {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
40% {
|
||||
transform: scale(1.6);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.viewer-reduced-motion .like-button .heart {
|
||||
animation: none;
|
||||
}
|
||||
.control {
|
||||
border: none;
|
||||
background: rgba(17, 24, 39, 0.55);
|
||||
|
||||
Reference in New Issue
Block a user