fix: 优化移动端工作台筛选与工具栏

将形态图片状态筛选合并为下拉框,统一筛选容器内边距并调整背景。重新排列分镜与镜头生产移动端工具栏,隐藏重复统计并补充交互测试。
This commit is contained in:
GouJ
2026-09-08 09:59:05 +08:00
committed by GitHub
parent 4edb0ff6cc
commit 769407cb14
8 changed files with 102 additions and 58 deletions
+28 -27
View File
@@ -1,49 +1,48 @@
# Design QA # Design QA
- Source visual truth: - Source visual truth:
- `/workspace/scratch/6e52996114b8/upload/83e0987d-5b6e-46b1-b39b-0674bf374f43.png` - `/workspace/scratch/6e52996114b8/upload/75026374-fe20-45b3-9596-b704e404f14e.png`
- `/workspace/scratch/6e52996114b8/upload/60e23391-685b-4538-95a2-46f6334f5f6d.png` - `/workspace/scratch/6e52996114b8/upload/672c002e-13b3-4a89-88ae-657cce7b1554.png`
- `/workspace/scratch/6e52996114b8/upload/430bec04-93fd-429f-a820-1edefdafc792.png` - `/workspace/scratch/6e52996114b8/upload/bddc4895-360d-48d0-b248-d384283085bc.png`
- Source pixel dimensions: `2048 × 819`, `447 × 203`, `238 × 198`. - Source pixel dimensions: `466 × 307`, `486 × 213`, `496 × 358`.
- Intended implementation viewport: desktop dark theme; the primary reference is `2048 × 819`. - Intended implementation viewport: mobile dark theme, approximately `393px` CSS width.
- Implementation screenshot: unavailable. - Implementation screenshot: unavailable.
- CSS size and density normalization: unavailable because the implementation could not be opened in the cloud browser. - CSS size and density normalization: unavailable because the implementation could not be opened in the cloud browser.
- State: 主体身份详情页已有身份母版;形态图默认筛选和布局切换状态 - State: 形态图默认筛选、分镜设计已完成统计、镜头生产首帧工具栏
## Full-view comparison evidence ## Full-view comparison evidence
The source images were opened and inspected. The first source shows the generic `.asset-image` width expanding the identity-anchor summary image across the detail panel and squeezing its explanatory copy into a narrow column on the far right. The second source shows the gallery summary/actions and filter panel consuming too much vertical space. The third source establishes the compact dark workspace density used by the surrounding pages. The three source images were opened and inspected. The first source shows two checkbox filters consuming a full mobile row and a redundant result count beside the layout switch. The second and third sources show desktop flex toolbar behavior wrapping on mobile, leaving unused horizontal regions before actions or after metrics.
The implementation passed lint, formatting, TypeScript, component/unit tests, and the production build. A local Sites preview started successfully at the required port, but the cloud browser rejected `terminal.local` with `net::ERR_BLOCKED_BY_CLIENT` on both the existing tab and one fresh tab. Therefore no browser-rendered implementation evidence is available. The implementation passed formatting, TypeScript, focused component tests, and the production build. A local Sites preview started successfully at the required port, but the cloud browser rejected `terminal.local` with `net::ERR_BLOCKED_BY_CLIENT`. Therefore no browser-rendered implementation evidence is available.
## Focused region comparison evidence ## Focused region comparison evidence
- Identity anchor: `.identity-anchor > .asset-image` now has a component-local `80 × 80` square constraint with higher selector specificity than the generic AssetImage width rule. The adjacent copy has `min-width: 0`. - 形态筛选:两个复选框合并为“全部状态 / 缺少主图 / 身份过期”单选下拉;筛选容器使用统一 `12px` 内边距和更浅的混合背景;`460px` 以下隐藏结果数量,并让状态下拉与布局切换并排。
- Identity detail container: the detail scrollbar, its container/content, and the panel now explicitly use `width: 100%`, `min-width: 0`, and `max-width: 100%` to contain long content. - 分镜设计:`760px` 以下工具栏改为单列网格,剧集、统计、操作分别占满整行;统计两端分布,操作从左侧开始排列。
- Gallery density: the summary/actions moved into the `WorkspacePage` header and the filter region's vertical padding changed from `14px` to `5px`. - 镜头生产:`760px` 以下隐藏与镜头目录重复的“本集 N 镜”统计,工具栏操作从左侧连续排列。
- Page headers: every `WorkspacePage` caller now uses compact mode.
These are code-level checks only and do not replace a rendered focused-region comparison. These are code-level checks only and do not replace a rendered focused-region comparison.
## Required fidelity surfaces ## Required fidelity surfaces
- Fonts and typography: existing project font stack, sizes, weights, and wrapping were preserved; browser comparison is blocked. - Fonts and typography: existing project font stack, sizes, weights, truncation, and copy were preserved; browser comparison is blocked.
- Spacing and layout rhythm: the requested header compaction and `5px` gallery filter padding are implemented; browser measurement is blocked. - Spacing and layout rhythm: component-local responsive rules remove the marked mobile gaps and keep the existing compact header density; exact browser measurement is blocked.
- Colors and visual tokens: existing dark/light CSS variables were preserved; browser comparison is blocked. - Colors and visual tokens: the filter background remains based on `--app-subtle` and is lightened with `color-mix`; browser sampling is blocked.
- Image quality and asset fidelity: no source assets were replaced or generated; existing backend images remain in use. Browser crop verification is blocked. - Image quality and asset fidelity: no image assets or crop rules changed.
- Copy and content: existing product copy is retained except where video generation now describes the formal strict-quality workflow. - Copy and content: existing product copy remains unchanged except the two checkbox labels now appear as equivalent dropdown options; the mobile-only duplicate shot count is visually hidden.
## Findings ## Findings
- [P1] Browser-rendered comparison unavailable - [P1] Browser-rendered comparison unavailable
- Location: all three target states. - Location: all three target mobile states.
- Evidence: the preview process is healthy, but the required cloud browser returns `net::ERR_BLOCKED_BY_CLIENT` for `http://terminal.local:4173/`. - Evidence: the preview process is healthy, but the required cloud browser returns `net::ERR_BLOCKED_BY_CLIENT` for `http://terminal.local:4173/`.
- Impact: exact visual fidelity, responsive behavior, interactions, and console state cannot be certified from the browser. - Impact: exact responsive spacing, rendered Naive UI select behavior, primary interactions, and console state cannot be visually certified.
- Fix: open the same preview in an available Work Mode cloud browser, reproduce the target data/theme, capture the three states, and rerun this QA. - Fix: open the same branch in an available browser with project data, capture the three target routes at `393px` width, and rerun this comparison.
## Primary interactions tested ## Primary interactions tested
Automated component tests cover filters, layout switching, workspace drawers, project-level formal storyboard actions, video quality API parameters, disabled states, receipts, and navigation. Browser interactions were not available. Automated component tests cover search, subject type and image-status dropdown filtering, result count changes, layout switching, affected-shot selection, and reset behavior. Browser interactions were not available.
## Console errors checked ## Console errors checked
@@ -51,14 +50,16 @@ Not checked because the page could not be opened in the cloud browser.
## Comparison history ## Comparison history
- Iteration 1: identified the P1 implementation-capture blocker after the local preview started. No visual iteration could be completed. - Iteration 1: replaced the checkbox row, removed the mobile result count, and changed both workflow toolbars to deterministic mobile rows based on the three source annotations.
- Iteration 2: updated component contracts and interaction tests after the filter changed from checkboxes to a select. All focused tests passed; browser capture remained blocked.
## Implementation checklist ## Implementation checklist
- [x] Constrain the identity anchor thumbnail and detail width. - [x] Replace the two image-state checkboxes with one dropdown.
- [x] Move gallery summary/actions into the compact header. - [x] Hide the mobile result count and keep the layout switch available.
- [x] Change gallery filter vertical padding from 14px to 5px. - [x] Use equal filter-container padding and a lighter token-derived background.
- [x] Apply compact mode to all workspace pages. - [x] Remove marked mobile toolbar gaps in storyboard and production.
- [ ] Capture and compare the rendered identity and gallery states in the cloud browser. - [x] Hide the duplicate mobile shot count in production.
- [ ] Capture and compare the three rendered states in the cloud browser.
final result: blocked final result: blocked
+15 -1
View File
@@ -167,7 +167,7 @@ watch(
</script> </script>
<template> <template>
<WorkspacePage split compact <WorkspacePage split compact class="production-workspace-page"
><template #header ><template #header
><div class="workspace-toolbar"> ><div class="workspace-toolbar">
<div class="toolbar-episode"> <div class="toolbar-episode">
@@ -733,6 +733,20 @@ watch(
} }
} }
@media (max-width: 760px) { @media (max-width: 760px) {
/* 移动端隐藏可由镜头目录获知的重复统计,并让操作从左侧连续排列。 */
.production-workspace-page .workspace-toolbar {
@apply grid grid-cols-[minmax(0,_1fr)] gap-2;
}
.production-workspace-page .toolbar-episode,
.production-workspace-page .toolbar-actions {
@apply w-full min-w-0 m-0;
}
.production-workspace-page .toolbar-summary {
@apply hidden;
}
.production-workspace-page .toolbar-actions {
@apply justify-start;
}
.production-workspace { .production-workspace {
@apply grid-cols-[minmax(0,_1fr)] grid-rows-[184px_minmax(0,_1fr)]; @apply grid-cols-[minmax(0,_1fr)] grid-rows-[184px_minmax(0,_1fr)];
} }
@@ -468,6 +468,21 @@ watch(
@apply p-0; @apply p-0;
} }
@media (max-width: 760px) { @media (max-width: 760px) {
/* 移动端按整行排列剧集、统计和操作,避免 flex 自动换行留下不规则空白。 */
.storyboard-workspace-page .workspace-toolbar {
@apply grid grid-cols-[minmax(0,_1fr)] gap-2;
}
.storyboard-workspace-page .toolbar-episode,
.storyboard-workspace-page .toolbar-metrics,
.storyboard-workspace-page .toolbar-actions {
@apply w-full min-w-0 m-0;
}
.storyboard-workspace-page .toolbar-metrics {
@apply justify-between;
}
.storyboard-workspace-page .toolbar-actions {
@apply justify-start;
}
.storyboard-workspace { .storyboard-workspace {
@apply grid-cols-[minmax(0,_1fr)] grid-rows-[184px_minmax(0,_1fr)]; @apply grid-cols-[minmax(0,_1fr)] grid-rows-[184px_minmax(0,_1fr)];
} }
@@ -137,8 +137,14 @@ const search = ref('')
/** 切换排版复用同一份卡片和查询结果,不重新读取图库或清空筛选。 */ /** 切换排版复用同一份卡片和查询结果,不重新读取图库或清空筛选。 */
const layout = ref<GalleryLayout>('grid') const layout = ref<GalleryLayout>('grid')
const module = ref('all') const module = ref('all')
const onlyMissing = ref(false) /** 形态图片状态筛选;单选下拉避免移动端复选项占据两行。 */
const onlyStale = ref(false) type ImageStatusFilter = 'all' | 'missing' | 'stale'
const statusFilter = ref<ImageStatusFilter>('all')
const statusFilterOptions: Array<{ label: string; value: ImageStatusFilter }> = [
{ label: '全部状态', value: 'all' },
{ label: '缺少主图', value: 'missing' },
{ label: '身份过期', value: 'stale' }
]
/** 保存每张卡片的尺寸观察器,切换回网格时及时释放。 */ /** 保存每张卡片的尺寸观察器,切换回网格时及时释放。 */
const masonryCardObservers = new WeakMap<HTMLElement, ResizeObserver>() const masonryCardObservers = new WeakMap<HTMLElement, ResizeObserver>()
@@ -223,8 +229,8 @@ const filtered = computed(() =>
form => form =>
(!targetFormId.value || form.id === targetFormId.value) && (!targetFormId.value || form.id === targetFormId.value) &&
(module.value === 'all' || form.subject.module === module.value) && (module.value === 'all' || form.subject.module === module.value) &&
(!onlyMissing.value || !primaryImage(form.images)) && (statusFilter.value !== 'missing' || !primaryImage(form.images)) &&
(!onlyStale.value || isPrimaryIdentityStale(form)) && (statusFilter.value !== 'stale' || isPrimaryIdentityStale(form)) &&
`${form.subject.name} ${form.subject.ref} ${form.name}` `${form.subject.name} ${form.subject.ref} ${form.name}`
.toLowerCase() .toLowerCase()
.includes(search.value.trim().toLowerCase()) .includes(search.value.trim().toLowerCase())
@@ -235,8 +241,7 @@ watch(
() => { () => {
search.value = targetFormId.value ? '' : queryText(route.query.subjectRef) search.value = targetFormId.value ? '' : queryText(route.query.subjectRef)
module.value = 'all' module.value = 'all'
onlyMissing.value = false statusFilter.value = 'all'
onlyStale.value = false
galleryOpen.value = false galleryOpen.value = false
generateOpen.value = false generateOpen.value = false
promptOpen.value = false promptOpen.value = false
@@ -272,8 +277,7 @@ function exportReceipt() {
function resetFilters() { function resetFilters() {
search.value = '' search.value = ''
module.value = 'all' module.value = 'all'
onlyMissing.value = false statusFilter.value = 'all'
onlyStale.value = false
void router.replace({ void router.replace({
query: { ...route.query, subjectFormId: undefined, subjectRef: undefined, sourceShotId: undefined } query: { ...route.query, subjectFormId: undefined, subjectRef: undefined, sourceShotId: undefined }
}) })
@@ -298,8 +302,7 @@ async function showStaleForms() {
}) })
search.value = '' search.value = ''
module.value = 'all' module.value = 'all'
onlyMissing.value = false statusFilter.value = 'stale'
onlyStale.value = true
} }
</script> </script>
@@ -571,10 +574,12 @@ async function showStaleForms() {
{ label: String('道具'), value: 'prop' } { label: String('道具'), value: 'prop' }
]" ]"
></NSelect> ></NSelect>
<div class="filter-toggles"> <NSelect
<NCheckbox v-model:checked="onlyMissing">仅看缺少主图</NCheckbox> v-model:value="statusFilter"
<NCheckbox v-model:checked="onlyStale">仅看身份过期</NCheckbox> :options="statusFilterOptions"
</div> aria-label="筛选图片状态"
class="status-filter"
/>
<div class="form-image-display-controls"> <div class="form-image-display-controls">
<span class="filter-result-count">{{ filtered.length }} 个形态</span> <span class="filter-result-count">{{ filtered.length }} 个形态</span>
<div class="gallery-layout-switch" role="group" aria-label="图库显示方式"> <div class="gallery-layout-switch" role="group" aria-label="图库显示方式">
@@ -853,7 +858,8 @@ async function showStaleForms() {
@apply [overflow-anchor:none]; @apply [overflow-anchor:none];
} }
.form-image-filter-region { .form-image-filter-region {
@apply my-4 py-[5px] px-4 bg-(--app-subtle); @apply my-4 p-3;
background: color-mix(in srgb, var(--app-subtle) 96%, white);
container-type: inline-size; container-type: inline-size;
} }
.form-image-toolbar { .form-image-toolbar {
@@ -875,17 +881,11 @@ async function showStaleForms() {
@apply min-w-0; @apply min-w-0;
} }
.form-image-filters { .form-image-filters {
@apply grid grid-cols-[minmax(200px,_360px)_152px_minmax(260px,_1fr)_auto] items-center gap-y-3 gap-x-4; @apply grid grid-cols-[minmax(200px,_360px)_152px_160px_auto] items-center gap-y-3 gap-x-4;
} }
.form-image-filters > * { .form-image-filters > * {
@apply min-w-0; @apply min-w-0;
} }
.filter-toggles {
@apply flex items-center flex-wrap gap-y-2.5 gap-x-4 text-xs;
}
.filter-toggles .n-checkbox__label {
@apply whitespace-nowrap;
}
.filter-result-count { .filter-result-count {
@apply justify-self-end text-muted text-xs whitespace-nowrap; @apply justify-self-end text-muted text-xs whitespace-nowrap;
} }
@@ -905,14 +905,20 @@ async function showStaleForms() {
.form-image-filters { .form-image-filters {
@apply grid-cols-[minmax(0,_1fr)_152px]; @apply grid-cols-[minmax(0,_1fr)_152px];
} }
.status-filter {
@apply col-start-1 row-start-2;
}
.form-image-display-controls { .form-image-display-controls {
@apply col-[1/-1]; @apply col-start-2 row-start-2;
} }
} }
@container (max-width: 460px) { @container (max-width: 460px) {
.form-image-filters { .form-image-filters {
@apply grid-cols-[minmax(0,_1fr)_120px] gap-x-2.5; @apply grid-cols-[minmax(0,_1fr)_120px] gap-x-2.5;
} }
.filter-result-count {
@apply hidden;
}
} }
.asset-impact-context { .asset-impact-context {
@apply flex flex-col gap-2.5 py-[5px] px-4 bg-(--app-subtle) m-0 min-w-0; @apply flex flex-col gap-2.5 py-[5px] px-4 bg-(--app-subtle) m-0 min-w-0;
+1 -1
View File
@@ -89,7 +89,7 @@ describe('统一目录条目', () => {
expect(css).toMatch(/\.workspace-tabs\.n-tabs\s*\{[^}]*width:\s*100%;[^}]*min-width:\s*0/) expect(css).toMatch(/\.workspace-tabs\.n-tabs\s*\{[^}]*width:\s*100%;[^}]*min-width:\s*0/)
expect(css).toMatch(/\.workspace-tabs \.n-tabs-nav-scroll-wrapper\s*\{[^}]*min-width:\s*0/) expect(css).toMatch(/\.workspace-tabs \.n-tabs-nav-scroll-wrapper\s*\{[^}]*min-width:\s*0/)
expect(css).toMatch( expect(css).toMatch(
/\.form-image-filters\s*\{[^}]*grid-template-columns:\s*minmax\(200px, 360px\) 152px minmax\(260px, 1fr\) auto/ /\.form-image-filters\s*\{[^}]*grid-template-columns:\s*minmax\(200px, 360px\) 152px 160px auto/
) )
expect(css).toMatch( expect(css).toMatch(
/@container \(max-width: 900px\)\s*\{\s*\.form-image-filters\s*\{[^}]*minmax\(0, 1fr\) 152px/ /@container \(max-width: 900px\)\s*\{\s*\.form-image-filters\s*\{[^}]*minmax\(0, 1fr\) 152px/
+4 -1
View File
@@ -184,7 +184,7 @@ describe('全站平面主题', () => {
// DOM 环境不计算几何;保护容器宽度断点和居中契约,真实坐标仍需浏览器验收。 // DOM 环境不计算几何;保护容器宽度断点和居中契约,真实坐标仍需浏览器验收。
const css = readAllStyles() const css = readAllStyles()
expect(css).toMatch( expect(css).toMatch(
/\.form-image-filter-region\s*\{[^}]*padding(?:-block)?:\s*5px;[^}]*background:\s*var\(--app-subtle\);/ /\.form-image-filter-region\s*\{[^}]*padding:\s*12px;[^}]*background:\s*color-mix\(in srgb, var\(--app-subtle\) 96%, white\);/
) )
expect(css).toMatch(/\.form-image-toolbar\s*\{[^}]*align-items:\s*center;[^}]*gap:\s*12px 16px;/) expect(css).toMatch(/\.form-image-toolbar\s*\{[^}]*align-items:\s*center;[^}]*gap:\s*12px 16px;/)
expect(css).toMatch( expect(css).toMatch(
@@ -197,6 +197,9 @@ describe('全站平面主题', () => {
expect(css).toMatch(/\.asset-impact-context\s*\{[^}]*gap:\s*10px/) expect(css).toMatch(/\.asset-impact-context\s*\{[^}]*gap:\s*10px/)
expect(css).toMatch(/\.asset-impact-context\s*\{[^}]*padding(?:-block)?:\s*5px/) expect(css).toMatch(/\.asset-impact-context\s*\{[^}]*padding(?:-block)?:\s*5px/)
expect(css).toMatch(/\.asset-impact-context\s*\{[^}]*margin:\s*0/) expect(css).toMatch(/\.asset-impact-context\s*\{[^}]*margin:\s*0/)
expect(css).toMatch(
/@container \(max-width: 460px\)\s*\{[^}]*\.form-image-filters\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\) 120px;[^}]*\}[^}]*\.filter-result-count\s*\{[^}]*display:\s*none;/
)
}) })
it('单图标按钮以当前控件高度为边长,尺寸与内容按钮互不影响', () => { it('单图标按钮以当前控件高度为边长,尺寸与内容按钮互不影响', () => {
@@ -347,7 +347,11 @@ describe('过期首帧到具体素材定位', () => {
expect(toolbar.classes()).toContain('has-impact-picker') expect(toolbar.classes()).toContain('has-impact-picker')
expect(picker.nextElementSibling).toBe(toolbar.get('.form-image-filters').element) expect(picker.nextElementSibling).toBe(toolbar.get('.form-image-filters').element)
expect(toolbar.find('input[aria-label="搜索形态图片"]').exists()).toBe(true) expect(toolbar.find('input[aria-label="搜索形态图片"]').exists()).toBe(true)
expect(toolbar.findAll('.filter-toggles [role="checkbox"]')).toHaveLength(2) expect(
toolbar
.findAllComponents(NSelect)
.some((item: VueWrapper) => item.attributes('aria-label') === '筛选图片状态')
).toBe(true)
expect(wrapper!.find('.asset-impact-context').exists()).toBe(false) expect(wrapper!.find('.asset-impact-context').exists()).toBe(false)
const select = wrapper! const select = wrapper!
.findAllComponents(NSelect) .findAllComponents(NSelect)
+5 -4
View File
@@ -806,7 +806,7 @@ describe('视觉风格与主体身份工作区', () => {
}) })
describe('工作台页面交互', () => { describe('工作台页面交互', () => {
it('形态筛选搜索、类型和勾选组合仍只过滤本地显示,控件与结果数处于同一工具栏', async () => { it('形态筛选搜索、类型和图片状态仍只过滤本地显示,控件与结果数处于同一工具栏', async () => {
const person = formFixture() const person = formFixture()
const scene = { const scene = {
...formFixture(), ...formFixture(),
@@ -823,8 +823,8 @@ describe('工作台页面交互', () => {
expect(wrapper!.findAll('.form-image-filters')).toHaveLength(1) expect(wrapper!.findAll('.form-image-filters')).toHaveLength(1)
expect(filters.get('.filter-result-count').text()).toBe('2 个形态') expect(filters.get('.filter-result-count').text()).toBe('2 个形态')
expect(filters.find('input[aria-label="搜索形态图片"]').exists()).toBe(true) expect(filters.find('input[aria-label="搜索形态图片"]').exists()).toBe(true)
expect(filters.findAll('.filter-toggles [role="checkbox"]')).toHaveLength(2) selectControl(wrapper!, 'aria-label', '筛选图片状态').vm.$emit('update:value', 'missing')
await filters.findAll('.filter-toggles [role="checkbox"]')[0]!.trigger('click') await flushPromises()
expect(wrapper!.findAll('.form-image-card')).toHaveLength(1) expect(wrapper!.findAll('.form-image-card')).toHaveLength(1)
expect(wrapper!.get('.form-image-card').text()).toContain('旧书店') expect(wrapper!.get('.form-image-card').text()).toContain('旧书店')
selectControl(wrapper!, 'aria-label', '筛选主体类型').vm.$emit('update:value', 'character') selectControl(wrapper!, 'aria-label', '筛选主体类型').vm.$emit('update:value', 'character')
@@ -833,7 +833,8 @@ describe('工作台页面交互', () => {
selectControl(wrapper!, 'aria-label', '筛选主体类型').vm.$emit('update:value', 'all') selectControl(wrapper!, 'aria-label', '筛选主体类型').vm.$emit('update:value', 'all')
await filters.get('input[aria-label="搜索形态图片"]').setValue('旧书店') await filters.get('input[aria-label="搜索形态图片"]').setValue('旧书店')
expect(filters.get('.filter-result-count').text()).toBe('1 个形态') expect(filters.get('.filter-result-count').text()).toBe('1 个形态')
await filters.findAll('.filter-toggles [role="checkbox"]')[1]!.trigger('click') selectControl(wrapper!, 'aria-label', '筛选图片状态').vm.$emit('update:value', 'stale')
await flushPromises()
expect(filters.get('.filter-result-count').text()).toBe('0 个形态') expect(filters.get('.filter-result-count').text()).toBe('0 个形态')
expect(fetcher.mock.calls.every(([, init]) => init?.method === 'GET')).toBe(true) expect(fetcher.mock.calls.every(([, init]) => init?.method === 'GET')).toBe(true)
}) })