fix: 合并图库镜头定位与筛选工具栏并统一对齐

This commit is contained in:
GouJ
2026-09-04 10:40:19 +08:00
parent 37f11911b1
commit 543c85ff43
5 changed files with 81 additions and 33 deletions
+21 -1
View File
@@ -862,6 +862,21 @@ body {
.form-image-filter-region { .form-image-filter-region {
container-type: inline-size; container-type: inline-size;
margin-block: 16px; margin-block: 16px;
padding: 14px 16px;
background: var(--app-subtle);
}
/* 镜头选择和图库筛选共用一行,以控件中心对齐;窄屏保留完整筛选组,不挤压复选框。 */
.form-image-toolbar {
display: grid;
grid-template-columns: minmax(0, 1fr);
align-items: center;
gap: 12px 16px;
}
.form-image-toolbar.has-impact-picker {
grid-template-columns: minmax(220px, 340px) minmax(0, 1fr);
}
.form-image-toolbar > * {
min-width: 0;
} }
.form-image-filters { .form-image-filters {
display: grid; display: grid;
@@ -888,6 +903,11 @@ body {
font-size: 12px; font-size: 12px;
white-space: nowrap; white-space: nowrap;
} }
@container (max-width: 1200px) {
.form-image-toolbar.has-impact-picker {
grid-template-columns: minmax(0, 1fr);
}
}
@container (max-width: 900px) { @container (max-width: 900px) {
.form-image-filters { .form-image-filters {
grid-template-columns: minmax(0, 1fr) 152px; grid-template-columns: minmax(0, 1fr) 152px;
@@ -1466,8 +1486,8 @@ body {
background: var(--app-subtle); background: var(--app-subtle);
} }
.asset-impact-select { .asset-impact-select {
flex: 0 1 340px;
min-width: 0; min-width: 0;
width: 100%;
} }
.form-image-card-focused { .form-image-card-focused {
box-shadow: inset 3px 0 0 var(--app-accent-text); box-shadow: inset 3px 0 0 var(--app-accent-text);
+15
View File
@@ -141,6 +141,21 @@ describe('全站平面主题', () => {
expect(tools).not.toContain(':focusable="false"') expect(tools).not.toContain(':focusable="false"')
}) })
it('图库顶部统一镜头选择和筛选的背景与垂直对齐,窄屏整组换行', () => {
// DOM 环境不计算几何;保护容器宽度断点和居中契约,真实坐标仍需浏览器验收。
const css = readFileSync('src/admin.css', 'utf8')
expect(css).toMatch(
/\.form-image-filter-region\s*\{[^}]*padding:\s*14px 16px;[^}]*background:\s*var\(--app-subtle\);/
)
expect(css).toMatch(/\.form-image-toolbar\s*\{[^}]*align-items:\s*center;[^}]*gap:\s*12px 16px;/)
expect(css).toMatch(
/\.form-image-toolbar\.has-impact-picker\s*\{[^}]*grid-template-columns:\s*minmax\(220px, 340px\) minmax\(0, 1fr\);/
)
expect(css).toMatch(
/@container \(max-width: 1200px\)\s*\{\s*\.form-image-toolbar\.has-impact-picker\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\);/
)
})
it('单图标按钮以当前控件高度为边长,尺寸与内容按钮互不影响', () => { it('单图标按钮以当前控件高度为边长,尺寸与内容按钮互不影响', () => {
// happy-dom 不计算几何尺寸,此项约束共用样式和调用方,实际布局仍需浏览器验收。 // happy-dom 不计算几何尺寸,此项约束共用样式和调用方,实际布局仍需浏览器验收。
const css = readFileSync('src/admin.css', 'utf8') const css = readFileSync('src/admin.css', 'utf8')
@@ -379,13 +379,10 @@ async function showStaleForms() {
>镜头影响检查失败暂时无法确认有无过期首帧{{ impact.query.error.value }} >镜头影响检查失败暂时无法确认有无过期首帧{{ impact.query.error.value }}
<NButton text size="small" @click="impact.query.refresh">重试检查</NButton></NAlert <NButton text size="small" @click="impact.query.refresh">重试检查</NButton></NAlert
> >
<div <div class="form-image-filter-region">
v-if="impactOptions.length || sourceShotId" <div class="form-image-toolbar" :class="{ 'has-impact-picker': impactOptions.length || sourceShotId }">
class="asset-impact-context mt-3"
aria-label="镜头与素材定位"
>
<div class="flex flex-wrap items-center gap-3">
<NSelect <NSelect
v-if="impactOptions.length || sourceShotId"
:value="sourceShot?.shotId ?? null" :value="sourceShot?.shotId ?? null"
:options="impactOptions" :options="impactOptions"
placeholder="选择过期镜头,查看关联素材" placeholder="选择过期镜头,查看关联素材"
@@ -394,6 +391,34 @@ async function showStaleForms() {
class="asset-impact-select" class="asset-impact-select"
@update:value="selectImpactShot" @update:value="selectImpactShot"
/> />
<div class="form-image-filters" role="group" aria-label="形态图片筛选">
<NInput
placeholder="搜索主体、引用或形态"
:input-props="{ 'aria-label': '搜索形态图片' }"
v-model:value="search"
clearable
><template #prefix><Search :size="14" /></template
></NInput>
<NSelect
aria-label="筛选主体类型"
v-model:value="module"
:options="[
{ label: String('全部类型'), value: 'all' },
{ label: String('人物'), value: 'character' },
{ label: String('场景'), value: 'scene' },
{ label: String('道具'), value: 'prop' }
]"
></NSelect>
<div class="filter-toggles">
<NCheckbox v-model:checked="onlyMissing">仅看缺少主图</NCheckbox>
<NCheckbox v-model:checked="onlyStale">仅看身份过期</NCheckbox>
</div>
<span class="filter-result-count">{{ filtered.length }} 个形态</span>
</div>
</div>
</div>
<div v-if="sourceShotId" class="asset-impact-context mt-3" aria-label="镜头与素材定位">
<div class="flex flex-wrap items-center gap-3">
<RouterLink v-if="sourceLocation" :to="sourceLocation" class="text-button" <RouterLink v-if="sourceLocation" :to="sourceLocation" class="text-button"
>返回第 {{ sourceShot?.episodeNo }} · 镜头 {{ sourceShot?.shotNo }} </RouterLink >返回第 {{ sourceShot?.episodeNo }} · 镜头 {{ sourceShot?.shotNo }} </RouterLink
> >
@@ -470,32 +495,6 @@ async function showStaleForms() {
}}</template> }}</template>
<NButton text size="small" @click="resetFilters">清除定位</NButton> <NButton text size="small" @click="resetFilters">清除定位</NButton>
</NAlert> </NAlert>
<div class="form-image-filter-region">
<div class="form-image-filters" role="group" aria-label="形态图片筛选">
<NInput
placeholder="搜索主体、引用或形态"
:input-props="{ 'aria-label': '搜索形态图片' }"
v-model:value="search"
clearable
><template #prefix><Search :size="14" /></template
></NInput>
<NSelect
aria-label="筛选主体类型"
v-model:value="module"
:options="[
{ label: String('全部类型'), value: 'all' },
{ label: String('人物'), value: 'character' },
{ label: String('场景'), value: 'scene' },
{ label: String('道具'), value: 'prop' }
]"
></NSelect>
<div class="filter-toggles">
<NCheckbox v-model:checked="onlyMissing">仅看缺少主图</NCheckbox>
<NCheckbox v-model:checked="onlyStale">仅看身份过期</NCheckbox>
</div>
<span class="filter-result-count">{{ filtered.length }} 个形态</span>
</div>
</div>
<NAlert v-if="query.error.value" role="alert" type="error" :show-icon="false" class="mt-4" <NAlert v-if="query.error.value" role="alert" type="error" :show-icon="false" class="mt-4"
>{{ query.error.value }}<span v-if="forms.length"> 当前展示上次读取的数据。</span></NAlert >{{ query.error.value }}<span v-if="forms.length"> 当前展示上次读取的数据。</span></NAlert
><NAlert v-if="running" role="status" type="info" :show-icon="false" class="mt-4"> ><NAlert v-if="running" role="status" type="info" :show-icon="false" class="mt-4">
@@ -229,11 +229,22 @@ describe('过期首帧到具体素材定位', () => {
expect(wrapper!.findAll('.form-image-card')).toHaveLength(3) expect(wrapper!.findAll('.form-image-card')).toHaveLength(3)
expect(wrapper!.text()).not.toContain('个形态主图与当前已锁定身份母版不一致') expect(wrapper!.text()).not.toContain('个形态主图与当前已锁定身份母版不一致')
expect(fetcher.mock.calls.some(([url]) => String(url).endsWith('/references'))).toBe(false) expect(fetcher.mock.calls.some(([url]) => String(url).endsWith('/references'))).toBe(false)
// 镜头选择和图库筛选共享上方工具栏;未选择镜头时不留下空的关联说明面板。
const toolbar = wrapper!.get('.form-image-toolbar')
const picker = toolbar.get('.asset-impact-select').element
expect(toolbar.classes()).toContain('has-impact-picker')
expect(picker.nextElementSibling).toBe(toolbar.get('.form-image-filters').element)
expect(toolbar.find('input[aria-label="搜索形态图片"]').exists()).toBe(true)
expect(toolbar.findAll('.filter-toggles [role="checkbox"]')).toHaveLength(2)
expect(wrapper!.find('.asset-impact-context').exists()).toBe(false)
const select = wrapper! const select = wrapper!
.findAllComponents(NSelect) .findAllComponents(NSelect)
.find(item => item.attributes('aria-label') === '选择关联镜头')! .find(item => item.attributes('aria-label') === '选择关联镜头')!
select.vm.$emit('update:value', 'shot-target') select.vm.$emit('update:value', 'shot-target')
await flushPromises() await flushPromises()
expect(wrapper!.get('.asset-impact-context').element.previousElementSibling).toBe(
wrapper!.get('.form-image-filter-region').element
)
expect(wrapper!.text()).toContain('待处理首帧关联此素材') expect(wrapper!.text()).toContain('待处理首帧关联此素材')
expect(fetcher.mock.calls.filter(([url]) => String(url).endsWith('/references'))).toHaveLength(1) expect(fetcher.mock.calls.filter(([url]) => String(url).endsWith('/references'))).toHaveLength(1)
}) })
+3
View File
@@ -817,6 +817,9 @@ describe('工作台页面交互', () => {
vi.stubGlobal('fetch', fetcher) vi.stubGlobal('fetch', fetcher)
await mountSubjectImages() await mountSubjectImages()
const filters = wrapper!.get('.form-image-filters') const filters = wrapper!.get('.form-image-filters')
expect(wrapper!.get('.form-image-toolbar').classes()).not.toContain('has-impact-picker')
expect(filters.element.parentElement).toBe(wrapper!.get('.form-image-toolbar').element)
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) expect(filters.findAll('.filter-toggles [role="checkbox"]')).toHaveLength(2)