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:
@@ -6,6 +6,15 @@ export type PublicPhoto = components['schemas']['PublicPhoto'];
|
||||
export type PublicPhotoList = components['schemas']['PublicPhotoList'];
|
||||
export type PublicPhotoDetail = components['schemas']['PublicPhotoDetail'];
|
||||
|
||||
// P2 Task 10 互动端点类型(likes/visits,ov 域部署;路径同形、域名区分)。
|
||||
export type LikeBatchRequest = components['schemas']['LikeBatchRequest'];
|
||||
export type LikeBatchResponse = components['schemas']['LikeBatchResponse'];
|
||||
export type VisitReportRequest = components['schemas']['VisitReportRequest'];
|
||||
export type VisitReportResponse = components['schemas']['VisitReportResponse'];
|
||||
|
||||
/** 公开照片排序模式(sort=hot 仅 ov 实现:likeTotal 倒序短期快照,§7)。 */
|
||||
export type SortMode = 'latest' | 'hot';
|
||||
|
||||
/** 客户端合成码:fetch 本身失败(离线/DNS),非服务端错误信封。 */
|
||||
export const NETWORK_ERROR_CODE = 'NETWORK_ERROR';
|
||||
|
||||
@@ -106,6 +115,7 @@ export type PublicPhotosQuery = {
|
||||
cursor?: string;
|
||||
limit?: number;
|
||||
locale?: string;
|
||||
sort?: SortMode;
|
||||
};
|
||||
|
||||
/** 公开照片列表(游标分页;Task 7 瀑布流数据源)。 */
|
||||
@@ -119,3 +129,20 @@ export function getPublicPhoto(token: string, photoId: string, locale?: string):
|
||||
withQuery(`/public/albums/${encodeURIComponent(token)}/photos/${encodeURIComponent(photoId)}`, { locale }),
|
||||
);
|
||||
}
|
||||
|
||||
/** 点赞批次上报(Task 10):失败重试必须复用同一 batchId(服务端幂等键)。 */
|
||||
export function postLikeBatch(token: string, photoId: string, body: LikeBatchRequest): Promise<LikeBatchResponse> {
|
||||
return apiRequest<LikeBatchResponse>(
|
||||
`/public/albums/${encodeURIComponent(token)}/photos/${encodeURIComponent(photoId)}/likes`,
|
||||
{ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) },
|
||||
);
|
||||
}
|
||||
|
||||
/** 浏览上报(Task 10):requestId 为幂等键,counted 确认前跨重试持久复用。 */
|
||||
export function postAlbumVisit(token: string, body: VisitReportRequest): Promise<VisitReportResponse> {
|
||||
return apiRequest<VisitReportResponse>(`/public/albums/${encodeURIComponent(token)}/visits`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user