style: 对齐拆解设置控件并修复单位换行

This commit is contained in:
GouJ
2026-09-02 17:06:06 +08:00
parent 6bc07b559e
commit 7d8074f90e
5 changed files with 164 additions and 43 deletions
+1
View File
@@ -114,6 +114,7 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S
- 拆解人物、场景、道具共用固定斑马纹列表:奇数行常驻原悬停灰底,偶数行保留次级底色,整行不随鼠标或键盘焦点变色;内部控件仍保留交互与焦点反馈,搜索后的条纹顺序按可见列表重新计算。此类记录(含剧本角色设定)首项同样保留 20px 内边距,主题切换时背景色使用 160ms 过渡,并遵循系统减少动态效果设置。
- 拆解主体列表的搜索框统一靠左,图标使用 `NInput``prefix` 插槽;统计紧随输入框弱化显示,搜索时显示匹配数/总数,清空后恢复总数。搜索与统计垂直居中,窄内容区允许换行,不再左右分散。
- 展开后的每个形态使用独立直角色块,与所在主体条纹采用相反的主题灰阶;保留 16px 内边距、12px 块间距和名称/默认标签间距,奇偶行及搜索重排后均可区分内外层,不新增悬停变色。
- 拆解设置中“每组集数”和“抽取模块”使用相同标题间距;输入框、复选框与预览按钮对齐到 34px 控件行,模块说明独立放在下方。“集 / 组”保持单行,窄抽屉按实际内容宽度将操作和完整模块分组换行,校验与预览逻辑不变。
- 窄屏默认收起侧栏,目录与正文改用紧凑布局;执行记录仍可查看。长弹窗在自身内部滚动。
布局样式集中在 `src/admin.css`,业务排版保留在 `src/styles.css`。新增页面应使用 `WorkspacePage`(内容优先页开启 `compact`),低频操作使用 `WorkspaceTools`;避免通过 `body``window.scrollTo` 管理滚动。
+80
View File
@@ -343,6 +343,86 @@ body {
.control-row-checkbox .n-checkbox-box-wrapper {
align-self: center;
}
/* 拆解配置跟随抽屉实际宽度;标题 20px + 间距 8px,下方统一使用 34px 控件行。 */
.breakdown-settings-panel {
container: breakdown-settings / inline-size;
}
.breakdown-config {
--breakdown-label-offset: 28px;
display: grid;
grid-template-columns: 180px minmax(0, 1fr) auto;
align-items: start;
gap: 16px 24px;
}
.breakdown-config .field-label {
margin: 0 0 8px;
padding: 0;
line-height: 20px;
}
.breakdown-group-field,
.breakdown-modules-field,
.breakdown-module-option {
min-width: 0;
}
.breakdown-group-input {
display: flex;
align-items: center;
gap: 12px;
}
.breakdown-group-number.n-input-number {
flex: 0 0 120px;
width: 120px;
}
.breakdown-group-unit {
flex-shrink: 0;
white-space: nowrap;
color: var(--app-muted);
font-size: 14px;
}
.breakdown-modules-field {
margin: 0;
padding: 0;
border: 0;
}
.breakdown-module-options {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px 16px;
}
.breakdown-module-description {
margin: 4px 0 0;
padding-left: 24px;
color: var(--app-muted);
font-size: 11px;
line-height: 18px;
overflow-wrap: anywhere;
}
.breakdown-preview-button.n-button {
align-self: start;
margin-top: var(--breakdown-label-offset);
white-space: nowrap;
}
@container breakdown-settings (max-width: 780px) {
.breakdown-config {
grid-template-columns: 180px minmax(0, 1fr);
}
.breakdown-preview-button.n-button {
grid-column: 2;
justify-self: end;
margin-top: 0;
}
}
@container breakdown-settings (max-width: 560px) {
.breakdown-config {
grid-template-columns: minmax(0, 1fr);
}
.breakdown-module-options {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 160px), 1fr));
}
.breakdown-preview-button.n-button {
grid-column: 1;
}
}
.workspace-tools-body .storyboard-coverage {
margin-block: 20px;
}
@@ -140,6 +140,58 @@ async function selectTab(label: string) {
}
describe('拆解页内容滚动', () => {
it('拆解设置保留输入标签、单行单位与模块说明,改版后仍校验模块和剧本状态', async () => {
const fetcher = vi.fn<typeof fetch>()
vi.stubGlobal('fetch', fetcher)
const provided = mountPage()
await wrapper!.get('[data-workspace-tools]').trigger('click')
await flushPromises()
const config = wrapper!.get('.breakdown-config')
expect(config.get('label[for="group-size"]').text()).toBe('每组集数')
expect(config.get<HTMLInputElement>('#group-size').element.value).toBe('3')
expect(config.get('.breakdown-group-unit').text()).toBe('集 / 组')
expect(config.get('fieldset > legend').text()).toBe('抽取模块')
const options = config.findAll('.breakdown-module-option')
expect(options).toHaveLength(3)
const preview = config.get<HTMLButtonElement>('.breakdown-preview-button')
expect(preview.element.disabled).toBe(false)
for (const option of options) {
const checkbox = option.get('[role="checkbox"]')
const description = option.get('.breakdown-module-description')
expect(checkbox.attributes('aria-describedby')).toBe(description.attributes('id'))
expect(checkbox.find('.breakdown-module-description').exists()).toBe(false)
expect(description.text()).not.toBe('')
await checkbox.trigger('click')
}
expect(preview.element.disabled).toBe(true)
expect(wrapper!.text()).toContain('至少选择一个抽取模块')
await options[1]!.get('[role="checkbox"]').trigger('click')
expect(preview.element.disabled).toBe(false)
provided.data.value!.project.status = 'generating'
await flushPromises()
expect(preview.element.disabled).toBe(true)
expect(fetcher).not.toHaveBeenCalled()
})
it('拆解配置按抽屉宽度换行,单位不收缩,三类控件对齐且说明位于控件下方', () => {
// DOM 环境不计算坐标,检查统一标题偏移、34px 控件行和容器断点契约。
const css = readFileSync('src/admin.css', 'utf8')
expect(css).toMatch(/\.breakdown-settings-panel\s*\{\s*container:\s*breakdown-settings \/ inline-size;/)
expect(css).toMatch(/\.breakdown-config\s*\{[^}]*--breakdown-label-offset:\s*28px;/)
expect(css).toMatch(/\.breakdown-config \.field-label\s*\{[^}]*margin:\s*0 0 8px;[^}]*line-height:\s*20px;/)
expect(css).toMatch(/\.breakdown-group-unit\s*\{[^}]*flex-shrink:\s*0;[^}]*white-space:\s*nowrap;/)
expect(css).toMatch(
/\.n-checkbox\.control-row-checkbox\s*\{[^}]*align-items:\s*center;[^}]*min-height:\s*34px;/
)
expect(css).toMatch(
/\.breakdown-preview-button\.n-button\s*\{[^}]*align-self:\s*start;[^}]*margin-top:\s*var\(--breakdown-label-offset\);/
)
expect(css).toContain('@container breakdown-settings (max-width: 780px)')
expect(css).toContain('@container breakdown-settings (max-width: 560px)')
expect(css).toContain('repeat(auto-fit, minmax(min(100%, 160px), 1fr))')
expect(readFileSync('src/styles.css', 'utf8')).not.toContain('.breakdown-config')
})
it.each(['人物', '场景', '道具'])('%s 使用共用固定斑马纹样式,搜索后保持列表条目结构', async label => {
mountPage()
await selectTab(label)
+31 -23
View File
@@ -155,7 +155,7 @@ function exportResult() {
</div>
<div v-if="project?.episodes.length" class="toolbar-actions">
<WorkspaceTools v-model:open="toolsOpen" title="拆解设置与恢复" label="拆解设置与恢复">
<section class="panel p-5 lg:p-6">
<section class="panel breakdown-settings-panel p-5 lg:p-6">
<div class="mb-5 flex flex-wrap items-start justify-between gap-3">
<div>
<h2 class="flex items-center gap-2 font-semibold"><Layers :size="17" />拆解设置</h2>
@@ -168,38 +168,46 @@ function exportResult() {
<NTag size="small" :bordered="false">来源正式剧集</NTag>
</div>
<div class="breakdown-config">
<div>
<div class="breakdown-group-field">
<label class="field-label" for="group-size">每组集数</label>
<div class="flex items-center gap-3">
<div class="breakdown-group-input">
<NInputNumber
:disabled="operation.pending"
:input-props="{ id: 'group-size' }"
class="w-24"
class="breakdown-group-number"
:value="typeof groupSize === 'number' ? groupSize : null"
@update:value="groupSize = $event ?? 0"
:min="1"
:step="1"
></NInputNumber
><span class="text-sm text-muted"> / </span>
><span class="breakdown-group-unit"> / </span>
</div>
</div>
<fieldset class="flex flex-wrap gap-5 border-0 p-0">
<legend class="field-label mb-3">抽取模块</legend>
<NCheckbox
:checked="modules.includes(option.value)"
:disabled="operation.pending"
:aria-label="option.label"
@update:checked="toggleModule(option.value, $event)"
v-for="option in moduleOptions"
:key="option.value"
class="flex cursor-pointer items-start gap-2.5"
><span
><span class="block text-sm font-medium">{{ option.label }}</span
><span class="mt-1 block text-[11px] text-muted">{{
option.description
}}</span></span
></NCheckbox
>
<fieldset class="breakdown-modules-field">
<legend class="field-label">抽取模块</legend>
<div class="breakdown-module-options">
<div
v-for="option in moduleOptions"
:key="option.value"
class="breakdown-module-option"
>
<NCheckbox
:checked="modules.includes(option.value)"
:disabled="operation.pending"
:aria-label="option.label"
:aria-describedby="`breakdown-module-${option.value}-help`"
@update:checked="toggleModule(option.value, $event)"
class="control-row-checkbox"
><span class="text-sm font-medium">{{ option.label }}</span></NCheckbox
>
<p
:id="`breakdown-module-${option.value}-help`"
class="breakdown-module-description"
>
{{ option.description }}
</p>
</div>
</div>
</fieldset>
<NButton
:disabled="
@@ -210,7 +218,7 @@ function exportResult() {
!!error
"
@click="loadPreview"
class="self-end"
class="breakdown-preview-button"
><LoaderCircle v-if="previewBusy" :size="14" class="animate-spin" />预览分组
</NButton>
</div>
-20
View File
@@ -621,13 +621,6 @@
monospace;
}
.breakdown-config {
display: grid;
grid-template-columns: 145px minmax(0, 1fr) auto;
align-items: start;
gap: 28px;
}
.group-preview {
margin-top: 24px;
padding-top: 20px;
@@ -735,15 +728,6 @@
.history-panel {
border-left: 0;
}
.breakdown-config {
grid-template-columns: 120px minmax(0, 1fr);
gap: 20px;
}
.breakdown-config > button {
grid-column: 1 / -1;
justify-self: start;
}
}
@media (max-width: 760px) {
.storyboard-controls {
@@ -771,10 +755,6 @@
font-size: 23px;
}
.breakdown-config {
grid-template-columns: minmax(0, 1fr);
}
.stage-strip {
gap: 9px;
}