From 900ff2a74d544466da8bf11a17aa708bccc42450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 22 Sep 2026 13:49:15 +0800 Subject: [PATCH] feat(viewer): refine visual details to match design reference Co-Authored-By: Claude Code --- apps/viewer/src/App.vue | 28 ++-- apps/viewer/src/__tests__/activity.spec.ts | 3 +- .../viewer/src/__tests__/album-header.spec.ts | 6 +- .../viewer/src/__tests__/viewer-shell.spec.ts | 3 +- apps/viewer/src/components/PhotoFeed.vue | 54 +++++--- apps/viewer/src/components/PhotoLightbox.vue | 46 +++---- .../src/components/album/AlbumHeader.vue | 123 ++++++++---------- .../src/components/viewer/PhotoActionBar.vue | 22 +++- .../src/components/viewer/ThumbnailStrip.vue | 6 +- apps/viewer/src/styles/viewer-tokens.css | 3 +- 10 files changed, 162 insertions(+), 132 deletions(-) diff --git a/apps/viewer/src/App.vue b/apps/viewer/src/App.vue index b875ab8..7236cf7 100644 --- a/apps/viewer/src/App.vue +++ b/apps/viewer/src/App.vue @@ -57,11 +57,8 @@ onBeforeUnmount(() => {
- + + {{ t('viewer.brand') }}
@@ -133,7 +130,7 @@ body { display: flex; align-items: center; justify-content: space-between; - height: 50px; + height: 52px; padding: 0 var(--viewer-pad-info); border-bottom: 1px solid var(--viewer-divider); background: var(--viewer-surface); @@ -144,15 +141,24 @@ body { gap: 8px; min-width: 0; } +/* 参考稿品牌印记:26px 主色圆 + 白 ◈ */ .brand-logo { - width: 24px; - height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; flex: none; + border-radius: 50%; + background: var(--viewer-primary); + color: #ffffff; + font-size: 15px; + line-height: 1; } .brand-name { - font-size: 1.0625rem; - font-weight: 700; - letter-spacing: 0.01em; + font-size: 1.3125rem; + font-weight: 750; + letter-spacing: -0.7px; color: var(--viewer-text); } .topbar-actions { diff --git a/apps/viewer/src/__tests__/activity.spec.ts b/apps/viewer/src/__tests__/activity.spec.ts index 08ffa44..3e50f3a 100644 --- a/apps/viewer/src/__tests__/activity.spec.ts +++ b/apps/viewer/src/__tests__/activity.spec.ts @@ -93,8 +93,9 @@ describe('activity page (Task 6)', () => { expect(wrapper.find('.empty-notice').text()).toBe('照片即将上线'); // 无 banner 资产 → 不渲染 img;viewCount 未知(null)→ 浏览项显示「—」而非 0(UI-02 §4.1)。 + // (参考稿:浏览项前置 ◉ Unicode 图标,属渲染内容的一部分。) expect(wrapper.find('img.banner').exists()).toBe(false); - expect(wrapper.find('.view-count').text()).toBe('—'); + expect(wrapper.find('.view-count').text()).toBe('◉—'); expect(document.title).toBe('2026 公司年会'); }); diff --git a/apps/viewer/src/__tests__/album-header.spec.ts b/apps/viewer/src/__tests__/album-header.spec.ts index 39bafbe..c80907d 100644 --- a/apps/viewer/src/__tests__/album-header.spec.ts +++ b/apps/viewer/src/__tests__/album-header.spec.ts @@ -73,8 +73,8 @@ describe('album header info area (UI-02 V02)', () => { expect(wrapper.find('h1.title').text()).toBe('2026 公司年会'); // pre-line:换行保留;文本插值(非 HTML 注入)。 expect(wrapper.find('.description').text()).toBe('年度盛会\n多行介绍'); - // Intl.NumberFormat 分组 + 词条拼接(§4.1)。 - expect(wrapper.find('.view-count').text()).toBe('1,286 次浏览'); + // Intl.NumberFormat 分组 + 词条拼接(§4.1);参考稿:浏览项前置 ◉ Unicode 图标。 + expect(wrapper.find('.view-count').text()).toBe('◉1,286 次浏览'); expect(wrapper.find('.meta').text()).toContain('大宴会厅 · 上海市浦东新区世纪大道 100 号'); // Asia/Shanghai(夹具时区):UTC 10-01 → 本地 10月1日。 expect(wrapper.find('.meta').text()).toContain('10月1日'); @@ -98,7 +98,7 @@ describe('album header info area (UI-02 V02)', () => { expect(wrapper.find('img.banner').exists()).toBe(false); expect(wrapper.find('.description').exists()).toBe(false); expect(wrapper.findAll('.meta-item')).toHaveLength(2); // 浏览(—)+ 日期;地点整项省略 - expect(wrapper.find('.view-count').text()).toBe('—'); + expect(wrapper.find('.view-count').text()).toBe('◉—'); expect(wrapper.find('.categories').exists()).toBe(false); }); diff --git a/apps/viewer/src/__tests__/viewer-shell.spec.ts b/apps/viewer/src/__tests__/viewer-shell.spec.ts index a451611..6ea2430 100644 --- a/apps/viewer/src/__tests__/viewer-shell.spec.ts +++ b/apps/viewer/src/__tests__/viewer-shell.spec.ts @@ -59,7 +59,8 @@ describe('viewer shell tokens and embed isolation (UI-02 V01)', () => { '--viewer-like-accent': '#FF667D', '--viewer-font-family': "system-ui, -apple-system, 'Segoe UI', 'PingFang SC', sans-serif", '--viewer-radius-img': '4px', - '--viewer-radius-pill': '20px', + '--viewer-radius-pill': '22px', + '--viewer-chip-bg': '#F5F7FA', '--viewer-pad-info': '16px', '--viewer-pad-photo': '4px', '--viewer-touch-target': '44px', diff --git a/apps/viewer/src/components/PhotoFeed.vue b/apps/viewer/src/components/PhotoFeed.vue index ae22f70..f79a457 100644 --- a/apps/viewer/src/components/PhotoFeed.vue +++ b/apps/viewer/src/components/PhotoFeed.vue @@ -311,23 +311,26 @@ onBeforeUnmount(() => { white-space: nowrap; } -/* —— 新照片提示 pill:白底蓝字蓝icon,悬浮照片区顶部 —— */ +/* —— 新照片提示 pill(参考稿):顶部 8px 居中悬浮、白底蓝字、34px 高、 + * 22px 圆角、轻投影 0 3px 15px —— */ .new-photo-bar { position: sticky; - top: 10px; + top: 8px; z-index: 2; display: flex; align-items: center; + justify-content: center; gap: 6px; width: fit-content; + min-height: 34px; margin: 0 auto 12px; border: none; - border-radius: var(--viewer-radius-pill, 20px); + border-radius: var(--viewer-radius-pill, 22px); background: var(--viewer-surface, #fff); - box-shadow: 0 2px 10px rgba(23, 32, 51, 0.16); + box-shadow: 0 3px 15px rgba(23, 32, 51, 0.14); color: var(--viewer-primary, #1a73e8); - padding: 8px 16px; - font-size: 0.8125rem; + padding: 0 17px; + font-size: 0.75rem; font-weight: 600; cursor: pointer; } @@ -357,6 +360,18 @@ onBeforeUnmount(() => { background: var(--viewer-divider, #e5eaf1); cursor: pointer; } +/* 参考稿:底部 40% 高度的轻渐变(transparent → rgba(0,0,0,.25)), + * 承托右下角白色点赞角标;不拦截指针(点击仍开大图)。 */ +.cell::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 40%; + background: linear-gradient(transparent, rgba(0, 0, 0, 0.25)); + pointer-events: none; +} .cell-img { display: block; width: 100%; @@ -364,32 +379,36 @@ onBeforeUnmount(() => { object-fit: cover; } -/* —— 点赞角标:右下角半透明暗底(明亮照片上可读),白心 + 白色计数 —— */ +/* —— 点赞角标(参考稿):右下 3px 贴边、无底色胶囊——白心 19px + 白色计数, + * 仅靠 text-shadow 保证明亮照片上的可读性 —— */ .cell-like { position: absolute; - right: 6px; - bottom: 6px; + right: 3px; + bottom: 3px; + z-index: 1; display: inline-flex; align-items: center; gap: 4px; border: none; - border-radius: var(--viewer-radius-pill, 20px); - background: linear-gradient(to right, rgba(14, 18, 24, 0.42), rgba(14, 18, 24, 0.62)); + background: transparent; color: #fff; - padding: 3px 8px; + padding: 2px 3px; font-size: 0.75rem; line-height: 1; + text-shadow: 0 1px 4px #222; cursor: pointer; } .cell-like .heart { - width: 16px; - height: 16px; + width: 19px; + height: 19px; fill: #fff; + filter: drop-shadow(0 1px 4px #222); animation: heart-pop 400ms ease-out; } .cell-like .count { color: #fff; font-variant-numeric: tabular-nums; + text-shadow: 0 1px 4px #222; } @keyframes heart-pop { 0% { @@ -459,11 +478,12 @@ onBeforeUnmount(() => { text-align: center; padding: 8px 0; } +/* 参考稿:到底提示 #96a0b0、12px、24px 16px 收底 */ .end-note { text-align: center; - color: var(--viewer-text-secondary, #647084); - font-size: 0.875rem; - padding: 12px 0; + color: #96a0b0; + font-size: 0.75rem; + padding: 24px 16px; margin: 0; } .sentinel { diff --git a/apps/viewer/src/components/PhotoLightbox.vue b/apps/viewer/src/components/PhotoLightbox.vue index 8b43174..132d82d 100644 --- a/apps/viewer/src/components/PhotoLightbox.vue +++ b/apps/viewer/src/components/PhotoLightbox.vue @@ -602,6 +602,7 @@ onBeforeUnmount(() => {
+ @@ -849,23 +846,25 @@ onBeforeUnmount(() => { padding: 4px 12px; cursor: pointer; } +/* 参考稿切换箭头:44px 宽 48px 高圆钮、半透明黑底 rgba(0,0,0,.13)、 + * 42px/200 细体字形、白字 + 深色 text-shadow;top calc(50% - 24px)。 */ .nav { position: absolute; - top: 50%; - transform: translateY(-50%); + top: calc(50% - 24px); display: flex; align-items: center; justify-content: center; width: var(--viewer-touch-target, 44px); - height: var(--viewer-touch-target, 44px); + height: 48px; border: none; border-radius: 50%; - background: rgba(255, 255, 255, 0.1); + background: rgba(0, 0, 0, 0.13); color: #ffffff; + padding: 0; cursor: pointer; } .nav:hover:not(:disabled) { - background: rgba(255, 255, 255, 0.2); + background: rgba(0, 0, 0, 0.3); } .nav:focus-visible { outline: 2px solid var(--viewer-primary, #1a73e8); @@ -874,20 +873,20 @@ onBeforeUnmount(() => { opacity: 0.3; cursor: default; } -.nav svg { - width: 28px; - height: 28px; - fill: none; - stroke: currentColor; - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; +.nav-glyph { + font-size: 2.625rem; + font-weight: 200; + line-height: 1; + text-shadow: 0 1px 8px #000; + /* 字形基线在圆内视觉居中 */ + transform: translateY(-2px); + user-select: none; } .nav.prev { - left: 8px; + left: 4px; } .nav.next { - right: 8px; + right: 4px; } /* 粒子层/互动提示层的定位与样式由 HeartParticles / InteractionNotice 自带 @@ -936,8 +935,9 @@ onBeforeUnmount(() => { * PhotoActionBar(§5.4:下载关闭移除整项并均分剩余栏宽)。 */ .viewer-actionbar { flex: none; - padding: 6px 12px; - padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px)); + /* 参考稿:5px 16px(+ 底部安全区) */ + padding: 5px 16px; + padding-bottom: calc(5px + env(safe-area-inset-bottom, 0px)); background: rgba(14, 18, 24, 0.72); } diff --git a/apps/viewer/src/components/album/AlbumHeader.vue b/apps/viewer/src/components/album/AlbumHeader.vue index c2ab286..e336375 100644 --- a/apps/viewer/src/components/album/AlbumHeader.vue +++ b/apps/viewer/src/components/album/AlbumHeader.vue @@ -184,13 +184,9 @@ onBeforeUnmount(() => {

{{ album.translation.title }}

+
+
- + {{ viewsText }} - + {{ dateText }} - + {{ venueText }}
@@ -297,12 +285,12 @@ onBeforeUnmount(() => { background: var(--viewer-surface, #fff); } -/* —— 宣传图:手机约 2.4:1;桌面限高 320px;contain 保海报文字(§3.3) —— */ +/* —— 宣传图:手机约 2.46:1;桌面限高 320px;contain 保海报文字(§3.3;占位 #dbe6ee) —— */ .banner-wrap { width: 100%; - aspect-ratio: 2.4 / 1; + aspect-ratio: 2.46 / 1; max-height: 320px; - background: var(--viewer-surface, #fff); + background: #dbe6ee; } .banner { display: block; @@ -311,9 +299,9 @@ onBeforeUnmount(() => { object-fit: contain; } -/* —— 信息区:16px 内边距由本区负责(页面无全局包围,§3.2) —— */ +/* —— 信息区:16px 内边距由本区负责(页面无全局包围,§3.2;参考稿 12px 16px 0) —— */ .album-info { - padding: 12px var(--viewer-pad-info, 16px) 4px; + padding: 12px var(--viewer-pad-info, 16px) 0; } .title-row { @@ -321,13 +309,15 @@ onBeforeUnmount(() => { align-items: flex-start; gap: 12px; } +/* 参考稿:22px/28px、字距 -0.6px */ .title { flex: 1; min-width: 0; margin: 0; font-size: 1.375rem; - line-height: 1.3; + line-height: 28px; font-weight: 700; + letter-spacing: -0.6px; color: var(--viewer-text, #172033); overflow-wrap: break-word; } @@ -337,34 +327,35 @@ onBeforeUnmount(() => { -webkit-line-clamp: 2; overflow: hidden; } +/* 参考稿:44px 主色圆形分享钮(白 ↗,无边框) */ .share-btn { flex: none; display: inline-flex; align-items: center; - gap: 6px; - min-height: var(--viewer-touch-target, 44px); - margin: -6px calc(var(--viewer-pad-info, 16px) * -1) -6px 0; + justify-content: center; + width: var(--viewer-touch-target, 44px); + height: var(--viewer-touch-target, 44px); + margin: -6px 0 -6px 0; border: none; - background: transparent; - color: var(--viewer-primary, #1a73e8); - padding: 0 6px; + border-radius: 50%; + background: var(--viewer-primary, #1a73e8); + color: var(--viewer-surface, #fff); + padding: 0; align-self: flex-start; - font-size: 0.875rem; - font-weight: 600; cursor: pointer; } -.share-btn svg { - width: 18px; - height: 18px; - fill: none; - stroke: currentColor; - stroke-width: 1.8; - stroke-linecap: round; - stroke-linejoin: round; +.share-btn:hover { + filter: brightness(0.94); +} +.share-icon { + font-size: 1.25rem; + line-height: 1; + /* 视觉基线微调:↗ 略下沉 */ + transform: translateY(-1px); } .share-btn:focus-visible { outline: 2px solid var(--viewer-primary, #1a73e8); - border-radius: 8px; + outline-offset: 2px; } .share-status { display: block; @@ -394,9 +385,10 @@ onBeforeUnmount(() => { border-radius: 4px; } -/* —— 说明:≤3 行 + 展开/收起;保留换行(pre-line),文本插值非 HTML 注入 —— */ +/* —— 说明:≤3 行 + 展开/收起;保留换行(pre-line),文本插值非 HTML 注入。 + * 参考稿:15px、次级色、上下外距 3px/9px —— */ .description { - margin: 8px 0 0; + margin: 3px 0 9px; color: var(--viewer-text-secondary, #647084); font-size: 0.9375rem; line-height: 1.55; @@ -410,15 +402,18 @@ onBeforeUnmount(() => { overflow: hidden; } -/* —— 元信息行:窄屏自然换行(§4.1);数字 tabular-nums —— */ +/* —— 元信息行:窄屏自然换行(§4.1);数字 tabular-nums。 + * 参考稿:12px、行距 8px 20px、底部 1px 分隔线 + 10px 收底 —— */ .meta { display: flex; flex-wrap: wrap; - gap: 6px 20px; - margin: 12px 0 0; + gap: 8px 20px; + margin: 9px 0 0; + padding-bottom: 10px; + border-bottom: 1px solid var(--viewer-divider, #e5eaf1); color: var(--viewer-text-secondary, #647084); - font-size: 0.8125rem; - line-height: 1.4; + font-size: 0.75rem; + line-height: 1.5; } .meta-item { display: inline-flex; @@ -430,28 +425,24 @@ onBeforeUnmount(() => { .meta-item span { overflow-wrap: break-word; } +/* Unicode 图标与正文同尺寸(参考稿:简洁字符图标) */ .meta-icon { flex: none; - width: 16px; - height: 16px; - fill: none; - stroke: currentColor; - stroke-width: 1.8; - stroke-linecap: round; - stroke-linejoin: round; + width: 1em; + line-height: 1; + text-align: center; } .view-count { font-variant-numeric: tabular-nums; } -/* —— 排序 tab —— */ +/* —— 排序 tab(参考稿:分隔线由上方元信息行承担;选中项 2px 底线) —— */ .sort-tabs { display: flex; gap: 24px; margin-top: 8px; - /* 标签与信息区 16px 对齐(效果图 §3.1/§4.2);分隔线保持通栏。 */ + /* 标签与信息区 16px 对齐(效果图 §3.1/§4.2)。 */ padding: 0 var(--viewer-pad-info, 16px); - border-bottom: 1px solid var(--viewer-divider, #e5eaf1); } .sort-tab { position: relative; @@ -467,13 +458,13 @@ onBeforeUnmount(() => { .sort-tab.active { color: var(--viewer-primary, #1a73e8); } -/* 选中底部 2px 线(§4.2) */ +/* 选中底部 2px 线(参考稿:absolute bottom 0 / left 0 / right 0) */ .sort-tab.active::after { content: ''; position: absolute; left: 0; right: 0; - bottom: -1px; + bottom: 0; height: 2px; background: var(--viewer-primary, #1a73e8); } @@ -501,22 +492,22 @@ onBeforeUnmount(() => { margin: -2px 0 0; scrollbar-width: thin; } +/* 参考稿:胶囊 22px 圆角、#f5f7fa 底、32px 高、12px 字号;选中主色实底白字 */ .chip { flex: none; - min-height: 36px; + min-height: 32px; border: 1px solid var(--viewer-divider, #e5eaf1); - border-radius: var(--viewer-radius-pill, 20px); - background: var(--viewer-surface, #fff); + border-radius: var(--viewer-radius-pill, 22px); + background: var(--viewer-chip-bg, #f5f7fa); color: var(--viewer-text, #172033); - padding: 0 16px; - font-size: 0.8125rem; + padding: 0 15px; + font-size: 0.75rem; cursor: pointer; } .chip.active { border-color: var(--viewer-primary, #1a73e8); background: var(--viewer-primary, #1a73e8); color: var(--viewer-surface, #fff); - font-weight: 600; } .chip:focus-visible { outline: 2px solid var(--viewer-primary, #1a73e8); diff --git a/apps/viewer/src/components/viewer/PhotoActionBar.vue b/apps/viewer/src/components/viewer/PhotoActionBar.vue index ecd4025..d9c042f 100644 --- a/apps/viewer/src/components/viewer/PhotoActionBar.vue +++ b/apps/viewer/src/components/viewer/PhotoActionBar.vue @@ -158,13 +158,18 @@ onBeforeUnmount(cancelPress);