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:
浅念
2026-09-21 22:20:19 +08:00
parent 89f45896b7
commit 160e7e5f1a
18 changed files with 1042 additions and 58 deletions
+6
View File
@@ -68,6 +68,9 @@ export function useDownload(inputs: DownloadInputs) {
/** 失败文案键(''=无失败):默认通用失败;DOWNLOAD_DISABLED 给专属文案
* (T15 漏译修复——相册中途关闭下载开关时观众可理解的确定性提示)。 */
const failed = ref<'' | 'download.failed' | 'errors.downloadDisabled'>('');
/** 失败归属照片(UI-02 §5.4):异步错误绑定触发下载的 photoId——大图内切图
* 后错误提示只在切回该照片时展示,不张冠李戴到当前照片。 */
const failedFor = ref<string | null>(null);
/** 微信引导浮层可见(点击下载时处于微信内置浏览器)。 */
const wechatGuide = ref(false);
const copyNotice = ref<'' | 'copied' | 'failed'>('');
@@ -95,6 +98,7 @@ export function useDownload(inputs: DownloadInputs) {
if (photoId === null || busy.value) return;
busy.value = true;
failed.value = '';
failedFor.value = null;
try {
let credential = await getPhotoDownload(inputs.token.value, photoId, variant);
if (isExpired(credential.expiresAt)) {
@@ -108,6 +112,7 @@ export function useDownload(inputs: DownloadInputs) {
error instanceof ApiError && error.code === ErrorCodes.DOWNLOAD_DISABLED
? 'errors.downloadDisabled'
: 'download.failed';
failedFor.value = photoId; // 异步错误归属触发时的照片(§5.4)
} finally {
busy.value = false;
}
@@ -140,6 +145,7 @@ export function useDownload(inputs: DownloadInputs) {
return reactive({
busy,
failed,
failedFor,
wechatGuide,
copyNotice,
canDownload,