feat(viewer): UI-02 dark immersive photo viewer with thumbnail strip
V04 (方案 §5):PhotoLightbox 模板重构为深色沉浸布局——顶栏(关闭/位置 计数/⋯ 减少动画菜单)+ 舞台(contain 大图 + 边缘 ‹› + 粒子/互动占位层)+ 缩略图条 + 底部操作栏;业务逻辑(序列导航/缩放手势/签名刷新/下载/点赞) 原样保留。 - 新增 viewer/position.ts:§5.2 位置计数真实回退(solo→照片预览;无 total 未到末页→已加载 N 张;全部加载→i/n;不伪造总数) - 新增 ThumbnailStrip:当前 ±3 窗口(≤7 项)、主色 2px 描边、thumb 变体、 滚入可见(遵守减少动画)、点击选择不新增历史 - 新增 PhotoActionBar:Like(coral 心形+计数+连续点赞提示)/Share/ Download 均分大触控;下载关闭整项移除;原图二级菜单(▾ + 长按) - 模态可用性:body 滚动锁、焦点进入/困住/归还触发元素、Esc 先收菜单、 菜单内左右键不抢占、dvh + 安全区、chrome 区手势/滚轮隔离 - 分享 V04 最小实现(复制当前深链,support/clipboard 从 AlbumHeader 提取共用);navigator.share 编排属 Task 5 - zh-CN/en 新词条;vitest 新增 23 例(位置回退/缩略图/操作栏/焦点/滚锁) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -122,7 +122,7 @@ describe('viewer url sync and deep link (Task 8)', () => {
|
||||
scrollTo.mockClear();
|
||||
keydown('Escape');
|
||||
await vi.waitFor(() => expect(router.currentRoute.value.path).toBe('/a/tok-view'));
|
||||
expect(wrapper.find('.lightbox').exists()).toBe(false);
|
||||
expect(wrapper.find('.photo-viewer').exists()).toBe(false);
|
||||
expect(scrollTo).toHaveBeenCalledWith(0, 650);
|
||||
expect(replaceSpy).toHaveBeenCalledTimes(1); // 关闭未额外 replace
|
||||
});
|
||||
@@ -137,13 +137,13 @@ describe('viewer url sync and deep link (Task 8)', () => {
|
||||
const scrollTo = vi.spyOn(window, 'scrollTo').mockImplementation(() => {});
|
||||
await wrapper.findAll('.cell')[0].trigger('click');
|
||||
await flushPromises();
|
||||
expect(wrapper.find('.lightbox').exists()).toBe(true);
|
||||
expect(wrapper.find('.photo-viewer').exists()).toBe(true);
|
||||
|
||||
scrollTo.mockClear();
|
||||
router.back(); // 等价浏览器后退(popstate 异步派发)
|
||||
await vi.waitFor(() => expect(router.currentRoute.value.path).toBe('/a/tok-view'));
|
||||
await flushPromises();
|
||||
expect(wrapper.find('.lightbox').exists()).toBe(false);
|
||||
expect(wrapper.find('.photo-viewer').exists()).toBe(false);
|
||||
expect(scrollTo).toHaveBeenCalledWith(0, 320);
|
||||
});
|
||||
|
||||
@@ -164,15 +164,15 @@ describe('viewer url sync and deep link (Task 8)', () => {
|
||||
const wrapper = mountApp();
|
||||
await flushPromises();
|
||||
|
||||
// 单图先行:列表仍在加载,大图已可看(solo 序列 1/1)。
|
||||
// 单图先行:列表仍在加载,大图已可看(solo 位置未知 → 「照片预览」,§5.2 不伪造排名)。
|
||||
expect(mockedGetPublicPhoto).toHaveBeenCalledWith('tok-view', 'p5');
|
||||
expect(wrapper.find('img.photo').attributes('src')).toBe('http://img.test/p5-browse.jpg?s=1');
|
||||
expect(wrapper.find('.counter').text()).toBe('1 / 1');
|
||||
expect(wrapper.find('.position-indicator').text()).toBe('照片预览');
|
||||
|
||||
// 第 1 页到达(无 p5)→ 深链 watcher 追拉第 2 页 → p5 归位自然顺序(5/5)。
|
||||
resolveFirstPage(makeList([makePhoto('p1'), makePhoto('p2'), makePhoto('p3')], 'cur-1'));
|
||||
await vi.waitFor(() => expect(wrapper.findAll('.cell')).toHaveLength(5));
|
||||
expect(wrapper.find('.counter').text()).toBe('5 / 5');
|
||||
expect(wrapper.find('.position-indicator').text()).toBe('5 / 5');
|
||||
|
||||
// 序列已构成:左切到前一张。
|
||||
keydown('ArrowLeft');
|
||||
@@ -189,7 +189,7 @@ describe('viewer url sync and deep link (Task 8)', () => {
|
||||
const wrapper = mountApp();
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.find('.lightbox').exists()).toBe(false);
|
||||
expect(wrapper.find('.photo-viewer').exists()).toBe(false);
|
||||
const notice = wrapper.find('.deep-link-notice[role="alert"]');
|
||||
expect(notice.text()).toContain('照片不存在或已下线');
|
||||
expect(notice.text()).not.toContain('PHOTO_NOT_PUBLIC'); // 不裸露错误码
|
||||
|
||||
Reference in New Issue
Block a user