From 7f9789eb46e899d82e6f97fc8afcd02f76d68eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 29 Sep 2026 01:08:13 +0800 Subject: [PATCH] =?UTF-8?q?polish(viewer):=20=E5=B9=BF=E5=91=8A=E8=BD=AE?= =?UTF-8?q?=E6=92=AD=20a11y=20=E8=A1=A5=E5=85=A8=E2=80=94=E2=80=94aria-cur?= =?UTF-8?q?rent/focus-visible/touch-action/=E9=94=AE=E5=90=8D=E5=AF=B9?= =?UTF-8?q?=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 评审跟进(Task 10 a11y 两项 + 三个顺手项): - 圆点当前项程序化状态 :aria-current(ThumbnailStrip:103 同形),active 圆点 类名对齐 sibling 约定 ad-dot--active → .active(viewer 无 BEM -- 修饰符) - .ad-slide/.ad-close/.ad-dot 补 :focus-visible 2px 主色描边 (AlbumHeader/ThumbnailStrip 同规;主色对暗色衬底/蒙层对比 ≈4:1) - .ad-carousel 补 touch-action: pan-y(ThumbnailStrip pan-x 对偶: 水平翻页自持、纵向滚动交还页面) - sessionStorage 键名 viewer:ads-dismissed → viewer.adsDismissed(sibling 点号命名约定;仅 ActivityPage 两处引用,测试无引用) - ad-carousel.spec:类名断言随改 + aria-current 前后迁移断言 Co-Authored-By: Claude Code --- apps/viewer/src/__tests__/ad-carousel.spec.ts | 6 +++++- apps/viewer/src/components/ads/AdCarousel.vue | 16 ++++++++++++++-- apps/viewer/src/pages/ActivityPage.vue | 4 ++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/apps/viewer/src/__tests__/ad-carousel.spec.ts b/apps/viewer/src/__tests__/ad-carousel.spec.ts index 3896599..01ecf81 100644 --- a/apps/viewer/src/__tests__/ad-carousel.spec.ts +++ b/apps/viewer/src/__tests__/ad-carousel.spec.ts @@ -56,9 +56,13 @@ describe('AdCarousel', () => { const dots = wrapper.findAll('.ad-dots button'); expect(dots.length).toBe(3); + expect(dots[0].attributes('aria-current')).toBe('true'); // 当前项程序化状态 + expect(dots[1].attributes('aria-current')).toBeUndefined(); await dots[1].trigger('click'); expect(currentSrc(wrapper)).toBe('https://ad.test/b.jpg'); - expect(dots[1].classes()).toContain('ad-dot--active'); + expect(dots[1].classes()).toContain('active'); + expect(dots[1].attributes('aria-current')).toBe('true'); + expect(dots[0].attributes('aria-current')).toBeUndefined(); }); it('emits close when the ✕ button is clicked', async () => { diff --git a/apps/viewer/src/components/ads/AdCarousel.vue b/apps/viewer/src/components/ads/AdCarousel.vue index 2111e2e..a15139c 100644 --- a/apps/viewer/src/components/ads/AdCarousel.vue +++ b/apps/viewer/src/components/ads/AdCarousel.vue @@ -108,7 +108,8 @@ function openAd(ad: PublicAd): void { @@ -127,6 +128,9 @@ function openAd(ad: PublicAd): void { border-radius: var(--viewer-radius-img); background: var(--viewer-dark-bg); /* 图像加载期的暗色衬底 */ font-family: var(--viewer-font-family); + /* 水平翻页手势自持(touchstart/end 判定),纵向滚动交还页面—— + * ThumbnailStrip pan-x 的对偶(pan-y)。 */ + touch-action: pan-y; } .ad-slide { display: block; @@ -191,7 +195,15 @@ function openAd(ad: PublicAd): void { border-radius: 50%; background: color-mix(in srgb, var(--viewer-surface) 55%, transparent); } -.ad-dot--active::before { +.ad-dot.active::before { background: var(--viewer-surface); } +/* 键盘焦点指示(AlbumHeader/ThumbnailStrip 同规:2px 主色描边)。主色对 + * 暗色衬底/color-mix 蒙层对比 ≈4:1(非文本 ≥3:1)。 */ +.ad-slide:focus-visible, +.ad-close:focus-visible, +.ad-dot:focus-visible { + outline: 2px solid var(--viewer-primary); + outline-offset: 1px; +} diff --git a/apps/viewer/src/pages/ActivityPage.vue b/apps/viewer/src/pages/ActivityPage.vue index 3d69d19..7a90f89 100644 --- a/apps/viewer/src/pages/ActivityPage.vue +++ b/apps/viewer/src/pages/ActivityPage.vue @@ -77,7 +77,7 @@ async function load(): Promise { // 广告独立拉取(Task 10):失败静默(广告缺席不阻塞相册浏览);会话级 // 关闭记忆先行(命中则连请求都省)。沿用 sequence 防跨相册竞态落地。 ads.value = []; - if (sessionStorage.getItem('viewer:ads-dismissed') !== '1') { + if (sessionStorage.getItem('viewer.adsDismissed') !== '1') { getPublicAds(token.value) .then((result) => { if (sequence !== loadSequence) return; @@ -122,7 +122,7 @@ function selectCategory(categoryId: string | null): void { /** 关闭广告(Task 10):会话级记忆(本次会话不再拉取/渲染),组件只 emit。 */ function dismissAds(): void { - sessionStorage.setItem('viewer:ads-dismissed', '1'); + sessionStorage.setItem('viewer.adsDismissed', '1'); ads.value = []; }