feat(viewer): share sheet with QR code + link + copy (replaces inline toast, App-style)

- ShareSheet.vue: bottom sheet (mobile) / centered card (desktop) — QR canvas (qrcode dep), selectable deep link, copy with on-button feedback (2.5s), optional navigator.share button, Esc/scrim/close dismiss, focus on open
- AlbumHeader + PhotoLightbox: share buttons open the sheet (album URL / photo deep link); page-level copied strips removed
- tests rewritten for sheet behavior — viewer 244 green

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-23 11:04:11 +08:00
parent e3f40c696d
commit 3d45350711
2 changed files with 53 additions and 18 deletions
+21 -5
View File
@@ -597,7 +597,12 @@ describe('photo lightbox interactions (UI-02 V05)', () => {
expect(current.value).toBe('p2');
});
it('shares via navigator.share with the album title and photo deep link; success is silent', async () => {
/** 分享弹层(Teleport 到 body):从 document 定位。 */
function lbSheet(): HTMLElement | null {
return document.querySelector('.share-sheet');
}
it('share opens the sheet with the photo deep link; native share via sheet button', async () => {
const share = vi.fn(() => Promise.resolve());
Object.defineProperty(navigator, 'share', { value: share, configurable: true });
const { wrapper } = mountV05('p1', {
@@ -608,12 +613,20 @@ describe('photo lightbox interactions (UI-02 V05)', () => {
await wrapper.find('button[aria-label="分享照片"]').trigger('click');
await flushPromises();
expect(share).toHaveBeenCalledWith({ title: '2026 公司年会', url: '/a/tok-lb/p/p1?lang=zh-CN' });
// 原生分享成功:无应用内轻提示(面板已收尾,§5.4)。
// 弹层出现并携带照片深链(页面级提示已退役)。
expect(lbSheet()).not.toBeNull();
expect(
lbSheet()!.querySelector('[data-test="share-sheet-url"]')!.textContent,
).toBe('/a/tok-lb/p/p1?lang=zh-CN');
expect(wrapper.find('.share-status').exists()).toBe(false);
const nativeBtn = lbSheet()!.querySelector<HTMLButtonElement>('[data-test="share-sheet-native"]')!;
nativeBtn.click();
await flushPromises();
expect(share).toHaveBeenCalledWith({ title: '2026 公司年会', url: '/a/tok-lb/p/p1?lang=zh-CN' });
});
it('falls back to the clipboard with feedback when native share is unavailable', async () => {
it('sheet copy button writes the deep link with on-button feedback', async () => {
const writeText = vi.fn(() => Promise.resolve());
Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
const { wrapper } = mountV05('p1', { shareUrl: '/a/tok-lb/p/p1?lang=zh-CN' });
@@ -621,8 +634,11 @@ describe('photo lightbox interactions (UI-02 V05)', () => {
await wrapper.find('button[aria-label="分享照片"]').trigger('click');
await flushPromises();
const copyBtn = lbSheet()!.querySelector<HTMLButtonElement>('[data-test="share-sheet-copy"]')!;
copyBtn.click();
await flushPromises();
expect(writeText).toHaveBeenCalledWith('/a/tok-lb/p/p1?lang=zh-CN');
expect(wrapper.find('.share-status').text()).toContain('链接已复制');
expect(copyBtn.textContent).toContain('已复制');
});
it('shows the download failure only on the photo that triggered it', async () => {