style: 对齐拆解设置控件并修复单位换行
This commit is contained in:
@@ -114,6 +114,7 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S
|
|||||||
- 拆解人物、场景、道具共用固定斑马纹列表:奇数行常驻原悬停灰底,偶数行保留次级底色,整行不随鼠标或键盘焦点变色;内部控件仍保留交互与焦点反馈,搜索后的条纹顺序按可见列表重新计算。此类记录(含剧本角色设定)首项同样保留 20px 内边距,主题切换时背景色使用 160ms 过渡,并遵循系统减少动态效果设置。
|
- 拆解人物、场景、道具共用固定斑马纹列表:奇数行常驻原悬停灰底,偶数行保留次级底色,整行不随鼠标或键盘焦点变色;内部控件仍保留交互与焦点反馈,搜索后的条纹顺序按可见列表重新计算。此类记录(含剧本角色设定)首项同样保留 20px 内边距,主题切换时背景色使用 160ms 过渡,并遵循系统减少动态效果设置。
|
||||||
- 拆解主体列表的搜索框统一靠左,图标使用 `NInput` 的 `prefix` 插槽;统计紧随输入框弱化显示,搜索时显示匹配数/总数,清空后恢复总数。搜索与统计垂直居中,窄内容区允许换行,不再左右分散。
|
- 拆解主体列表的搜索框统一靠左,图标使用 `NInput` 的 `prefix` 插槽;统计紧随输入框弱化显示,搜索时显示匹配数/总数,清空后恢复总数。搜索与统计垂直居中,窄内容区允许换行,不再左右分散。
|
||||||
- 展开后的每个形态使用独立直角色块,与所在主体条纹采用相反的主题灰阶;保留 16px 内边距、12px 块间距和名称/默认标签间距,奇偶行及搜索重排后均可区分内外层,不新增悬停变色。
|
- 展开后的每个形态使用独立直角色块,与所在主体条纹采用相反的主题灰阶;保留 16px 内边距、12px 块间距和名称/默认标签间距,奇偶行及搜索重排后均可区分内外层,不新增悬停变色。
|
||||||
|
- 拆解设置中“每组集数”和“抽取模块”使用相同标题间距;输入框、复选框与预览按钮对齐到 34px 控件行,模块说明独立放在下方。“集 / 组”保持单行,窄抽屉按实际内容宽度将操作和完整模块分组换行,校验与预览逻辑不变。
|
||||||
- 窄屏默认收起侧栏,目录与正文改用紧凑布局;执行记录仍可查看。长弹窗在自身内部滚动。
|
- 窄屏默认收起侧栏,目录与正文改用紧凑布局;执行记录仍可查看。长弹窗在自身内部滚动。
|
||||||
|
|
||||||
布局样式集中在 `src/admin.css`,业务排版保留在 `src/styles.css`。新增页面应使用 `WorkspacePage`(内容优先页开启 `compact`),低频操作使用 `WorkspaceTools`;避免通过 `body` 或 `window.scrollTo` 管理滚动。
|
布局样式集中在 `src/admin.css`,业务排版保留在 `src/styles.css`。新增页面应使用 `WorkspacePage`(内容优先页开启 `compact`),低频操作使用 `WorkspaceTools`;避免通过 `body` 或 `window.scrollTo` 管理滚动。
|
||||||
|
|||||||
@@ -343,6 +343,86 @@ body {
|
|||||||
.control-row-checkbox .n-checkbox-box-wrapper {
|
.control-row-checkbox .n-checkbox-box-wrapper {
|
||||||
align-self: center;
|
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 {
|
.workspace-tools-body .storyboard-coverage {
|
||||||
margin-block: 20px;
|
margin-block: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,58 @@ async function selectTab(label: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('拆解页内容滚动', () => {
|
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 => {
|
it.each(['人物', '场景', '道具'])('%s 使用共用固定斑马纹样式,搜索后保持列表条目结构', async label => {
|
||||||
mountPage()
|
mountPage()
|
||||||
await selectTab(label)
|
await selectTab(label)
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ function exportResult() {
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="project?.episodes.length" class="toolbar-actions">
|
<div v-if="project?.episodes.length" class="toolbar-actions">
|
||||||
<WorkspaceTools v-model:open="toolsOpen" title="拆解设置与恢复" label="拆解设置与恢复">
|
<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 class="mb-5 flex flex-wrap items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="flex items-center gap-2 font-semibold"><Layers :size="17" />拆解设置</h2>
|
<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>
|
<NTag size="small" :bordered="false">来源:正式剧集</NTag>
|
||||||
</div>
|
</div>
|
||||||
<div class="breakdown-config">
|
<div class="breakdown-config">
|
||||||
<div>
|
<div class="breakdown-group-field">
|
||||||
<label class="field-label" for="group-size">每组集数</label>
|
<label class="field-label" for="group-size">每组集数</label>
|
||||||
<div class="flex items-center gap-3">
|
<div class="breakdown-group-input">
|
||||||
<NInputNumber
|
<NInputNumber
|
||||||
:disabled="operation.pending"
|
:disabled="operation.pending"
|
||||||
:input-props="{ id: 'group-size' }"
|
:input-props="{ id: 'group-size' }"
|
||||||
class="w-24"
|
class="breakdown-group-number"
|
||||||
:value="typeof groupSize === 'number' ? groupSize : null"
|
:value="typeof groupSize === 'number' ? groupSize : null"
|
||||||
@update:value="groupSize = $event ?? 0"
|
@update:value="groupSize = $event ?? 0"
|
||||||
:min="1"
|
:min="1"
|
||||||
:step="1"
|
:step="1"
|
||||||
></NInputNumber
|
></NInputNumber
|
||||||
><span class="text-sm text-muted">集 / 组</span>
|
><span class="breakdown-group-unit">集 / 组</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<fieldset class="flex flex-wrap gap-5 border-0 p-0">
|
<fieldset class="breakdown-modules-field">
|
||||||
<legend class="field-label mb-3">抽取模块</legend>
|
<legend class="field-label">抽取模块</legend>
|
||||||
|
<div class="breakdown-module-options">
|
||||||
|
<div
|
||||||
|
v-for="option in moduleOptions"
|
||||||
|
:key="option.value"
|
||||||
|
class="breakdown-module-option"
|
||||||
|
>
|
||||||
<NCheckbox
|
<NCheckbox
|
||||||
:checked="modules.includes(option.value)"
|
:checked="modules.includes(option.value)"
|
||||||
:disabled="operation.pending"
|
:disabled="operation.pending"
|
||||||
:aria-label="option.label"
|
:aria-label="option.label"
|
||||||
|
:aria-describedby="`breakdown-module-${option.value}-help`"
|
||||||
@update:checked="toggleModule(option.value, $event)"
|
@update:checked="toggleModule(option.value, $event)"
|
||||||
v-for="option in moduleOptions"
|
class="control-row-checkbox"
|
||||||
:key="option.value"
|
><span class="text-sm font-medium">{{ option.label }}</span></NCheckbox
|
||||||
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
|
|
||||||
>
|
>
|
||||||
|
<p
|
||||||
|
:id="`breakdown-module-${option.value}-help`"
|
||||||
|
class="breakdown-module-description"
|
||||||
|
>
|
||||||
|
{{ option.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<NButton
|
<NButton
|
||||||
:disabled="
|
:disabled="
|
||||||
@@ -210,7 +218,7 @@ function exportResult() {
|
|||||||
!!error
|
!!error
|
||||||
"
|
"
|
||||||
@click="loadPreview"
|
@click="loadPreview"
|
||||||
class="self-end"
|
class="breakdown-preview-button"
|
||||||
><LoaderCircle v-if="previewBusy" :size="14" class="animate-spin" />预览分组
|
><LoaderCircle v-if="previewBusy" :size="14" class="animate-spin" />预览分组
|
||||||
</NButton>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -621,13 +621,6 @@
|
|||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breakdown-config {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 145px minmax(0, 1fr) auto;
|
|
||||||
align-items: start;
|
|
||||||
gap: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-preview {
|
.group-preview {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
@@ -735,15 +728,6 @@
|
|||||||
.history-panel {
|
.history-panel {
|
||||||
border-left: 0;
|
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) {
|
@media (max-width: 760px) {
|
||||||
.storyboard-controls {
|
.storyboard-controls {
|
||||||
@@ -771,10 +755,6 @@
|
|||||||
font-size: 23px;
|
font-size: 23px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breakdown-config {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage-strip {
|
.stage-strip {
|
||||||
gap: 9px;
|
gap: 9px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user