feat(viewer): UI-02 heart particles, interaction notices, share and download
Task 5 (V05 §5.4/§6): decorative heart particles on the lightbox stage (local like clicks + others' WS bursts for the current photo), compact cross-photo interaction notices above the action bar (click to locate), navigator.share-first share orchestration with clipboard fallback for album and photo deep links (lang param), and download failures attributed to the photo that triggered them. Self-echo dedup stays in the realtime router (own batches never produce particles/notices); particles cap at 12 with visibility/unmount cleanup and are suppressed under reduced motion; notices cap at 3 with ~4s auto-dismiss and throttled SR summary. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,10 @@ const MY_BATCHES_CAP = 100;
|
||||
/** sessionStorage 键:会话标识(每标签页会话一枚,跨刷新复用)。 */
|
||||
const SESSION_KEY = 'viewer.likeSessionId';
|
||||
|
||||
/** 本地点击动效监听器(UI-02 V05):每次有效点击立即通知——与批次发送/重试
|
||||
* 语义完全解耦(不为动画补帧额外发送点赞,§6.1)。 */
|
||||
export type LocalLikeListener = (photoId: string) => void;
|
||||
|
||||
/** 单照片状态(base=权威累计,pending=未落定乐观量,pop=动画 nonce)。 */
|
||||
interface PhotoLikeState {
|
||||
base: number;
|
||||
@@ -88,6 +92,7 @@ export function useLikes(inputs: LikesInputs) {
|
||||
|
||||
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const retryTimers = new Set<ReturnType<typeof setTimeout>>();
|
||||
const localLikeListeners = new Set<LocalLikeListener>();
|
||||
|
||||
function stateOf(photoId: string, seedCount?: number): PhotoLikeState {
|
||||
let state = states.get(photoId);
|
||||
@@ -110,6 +115,8 @@ export function useLikes(inputs: LikesInputs) {
|
||||
state.pending += 1;
|
||||
state.pop += 1;
|
||||
queued.set(photoId, (queued.get(photoId) ?? 0) + 1);
|
||||
// 本地点击动效事件(UI-02 V05):仅通知订阅方(粒子等装饰层),不影响批次。
|
||||
for (const listener of localLikeListeners) listener(photoId);
|
||||
if ((queued.get(photoId) ?? 0) >= LIKE_FLUSH_CAP) {
|
||||
flushQueue();
|
||||
return;
|
||||
@@ -119,6 +126,14 @@ export function useLikes(inputs: LikesInputs) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 订阅本地点击动效事件(UI-02 V05):返回退订函数(订阅方随上下文卸载)。 */
|
||||
function onLocalLike(listener: LocalLikeListener): () => void {
|
||||
localLikeListeners.add(listener);
|
||||
return () => {
|
||||
localLikeListeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
/** 冲队:每照片一批(新 batchId);未满防抖窗的部分留给下一窗。 */
|
||||
function flushQueue(): void {
|
||||
if (flushTimer !== null) {
|
||||
@@ -213,6 +228,7 @@ export function useLikes(inputs: LikesInputs) {
|
||||
if (flushTimer !== null) clearTimeout(flushTimer);
|
||||
for (const timer of retryTimers) clearTimeout(timer);
|
||||
retryTimers.clear();
|
||||
localLikeListeners.clear();
|
||||
});
|
||||
|
||||
return reactive({
|
||||
@@ -221,6 +237,8 @@ export function useLikes(inputs: LikesInputs) {
|
||||
/** 点击动画 nonce(每次点击 +1,模板 :key 重挂触发动画)。 */
|
||||
popNonce: (photoId: string) => states.get(photoId)?.pop ?? 0,
|
||||
like,
|
||||
/** 订阅本地点击动效事件(UI-02 V05 粒子等)。 */
|
||||
onLocalLike,
|
||||
calibrate,
|
||||
ownDeltaIn,
|
||||
recalibrateFromList,
|
||||
|
||||
Reference in New Issue
Block a user