diff --git a/README.md b/README.md index cd62eef..80efa0c 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S - 采用微信风格配色:浅色为浅灰背景与白色内容,暗色为近黑背景与深灰内容;绿色用于主操作、选中态和进度。按钮文字、目录文字与主绿色分别配置以保证对比度,错误/警告仍使用独立语义色。Naive UI 和业务内容共用主题,浏览器主题色随之切换;加载前应用偏好,减少主题闪烁。 - 全站采用低边框、直角样式:面板、提示框、标签、图片卡片和表单用背景层次代替静态描边;表格与长记录列表使用交替底色,抽屉用灰底承托内容。保留输入框聚焦/校验反馈、复选框、选中指示和必要的浮层阴影;开关、加载等功能图形不强制改形状。 - Tabs 例外保留底部分隔线,浅色和暗黑模式均区分导航与正文;选中下划线仍使用绿色。视觉风格工具栏将“图标+状态说明”与等高操作按钮分组,读取失败不再显示为“尚未创建”,锁定与未保存草稿的生成限制不变。 +- 剧本正文的执行记录/导出/进入拆解,以及拆解结果的 JSON 导出统一放在 `NTabs` 的 `suffix` 插槽,与标签垂直居中并共用底线。浅色 Tabs 分隔线单独加深,不影响其它组件的低边框风格;窄内容区由容器断点把操作放到下一行,标签仍使用内置横向滚动。 - 窄屏默认收起侧栏,目录与正文改用紧凑布局;执行记录仍可查看。长弹窗在自身内部滚动。 布局样式集中在 `src/admin.css`,业务排版保留在 `src/styles.css`。新增页面应使用 `WorkspacePage`(内容优先页开启 `compact`),低频操作使用 `WorkspaceTools`;避免通过 `body` 或 `window.scrollTo` 管理滚动。 diff --git a/src/admin.css b/src/admin.css index 4ae3a25..48ac41c 100644 --- a/src/admin.css +++ b/src/admin.css @@ -431,11 +431,6 @@ body { .script-section > :first-child { flex-shrink: 0; } -.script-section > :first-child .n-tabs { - flex: 1; - min-width: 220px; - width: auto; -} .script-section .content-with-history { flex: 1; height: auto; @@ -724,20 +719,61 @@ body { outline: 2px solid var(--app-accent); outline-offset: -2px; } -/* Tabs 的默认整行宽度限制在左侧网格内,导出按钮始终处于同一行右端。 */ -.result-toolbar { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: center; - gap: 16px; -} -.result-toolbar > .n-tabs { +/* 标签与操作由同一 NTabs 导航行管理,suffix 共享底线及垂直中心。 */ +.tabs-toolbar { + container: tabs-toolbar / inline-size; + width: 100%; min-width: 0; } -.result-toolbar > .n-button { - justify-self: end; +.workspace-tabs.n-tabs { + width: 100%; + min-width: 0; +} +.workspace-tabs > .n-tabs-nav { + align-items: stretch; +} +.workspace-tabs .n-tabs-tab { + min-height: 40px; + box-sizing: border-box; +} +.workspace-tabs .n-tabs-nav-scroll-wrapper { + min-width: 0; +} +.workspace-tabs .n-tabs-nav__suffix { + align-items: center; + flex-shrink: 0; + padding-left: 20px; +} +.tabs-toolbar-actions { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 4px 14px; + min-height: 40px; +} +.tabs-toolbar-actions .n-button, +.tabs-toolbar-actions .text-button { + display: inline-flex; + align-items: center; + justify-content: center; + height: 32px; white-space: nowrap; } +/* 窄内容区允许 suffix 独占一行;Tabs 本身继续使用组件内置横向滚动。 */ +@container tabs-toolbar (max-width: 600px) { + .workspace-tabs > .n-tabs-nav { + flex-wrap: wrap; + } + .workspace-tabs .n-tabs-nav-scroll-wrapper { + flex-basis: 100%; + } + .workspace-tabs .n-tabs-nav__suffix { + width: 100%; + justify-content: flex-end; + padding-left: 0; + } +} .result-toolbar .n-tabs-tab__label { display: inline-flex; gap: 6px; diff --git a/src/components/ui/DirectoryItem.test.ts b/src/components/ui/DirectoryItem.test.ts index e53bf98..c0a5fed 100644 --- a/src/components/ui/DirectoryItem.test.ts +++ b/src/components/ui/DirectoryItem.test.ts @@ -86,9 +86,10 @@ describe('统一目录条目', () => { expect(css).toMatch(/\.directory-list-content\s*\{[^}]*flex-direction:\s*row;[^}]*width:\s*max-content/) }) - it('导出工具栏与筛选器使用受约束的网格,不让 Tabs 和 Select 默认宽度强制换行', () => { + it('Tabs 工具栏与筛选网格限制最小宽度,不让组件默认宽度挤压内容', () => { const css = readFileSync('src/admin.css', 'utf8') - expect(css).toMatch(/\.result-toolbar\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\) auto/) + 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( /\.form-image-filters\s*\{[^}]*grid-template-columns:\s*minmax\(200px, 360px\) 152px minmax\(260px, 1fr\) auto/ ) diff --git a/src/components/ui/flatTheme.test.ts b/src/components/ui/flatTheme.test.ts index d3d6ab8..ecaf80b 100644 --- a/src/components/ui/flatTheme.test.ts +++ b/src/components/ui/flatTheme.test.ts @@ -119,7 +119,7 @@ describe('全站平面主题', () => { expect(theme.common?.borderColor).not.toBe('transparent') expect(theme.Checkbox).not.toHaveProperty('border') expect(theme.Tabs?.barColor).toBe('#07c160') - expect(value('.n-tabs', '--n-tab-border-color')).toBe(dark ? '#333333' : '#e5e5e5') + expect(value('.n-tabs', '--n-tab-border-color')).toBe(dark ? '#333333' : '#bfbfbf') }) it('单图标按钮以当前控件高度为边长,尺寸与内容按钮互不影响', () => { diff --git a/src/components/ui/layout.test.ts b/src/components/ui/layout.test.ts index 02ff947..bb5526f 100644 --- a/src/components/ui/layout.test.ts +++ b/src/components/ui/layout.test.ts @@ -22,6 +22,16 @@ afterEach(() => { }) describe('管理后台组件边界', () => { + it('Tabs suffix 与标签共用导航行,窄屏换行但不产生外层滚动', () => { + // DOM 测试不计算坐标;保护对齐尺寸与容器断点,视觉验收仍需浏览器。 + const css = readFileSync('src/admin.css', 'utf8') + expect(css).toMatch(/\.workspace-tabs > \.n-tabs-nav\s*\{[^}]*align-items:\s*stretch;/) + expect(css).toMatch(/\.workspace-tabs \.n-tabs-nav__suffix\s*\{[^}]*align-items:\s*center;/) + expect(css).toMatch(/\.tabs-toolbar-actions\s*\{[^}]*align-items:\s*center;[^}]*min-height:\s*40px;/) + expect(css).toContain('@container tabs-toolbar (max-width: 600px)') + expect(css).not.toContain('.result-toolbar > .n-button') + }) + it('滚动边界样式包含 document 锁定和工作区收缩约束', () => { // happy-dom 不计算视口几何;这里只保护 CSS 契约,真实滚动仍需浏览器验收。 const adminCss = readFileSync('src/admin.css', 'utf8') diff --git a/src/composables/useTheme.test.ts b/src/composables/useTheme.test.ts index 92c74e3..137b9ee 100644 --- a/src/composables/useTheme.test.ts +++ b/src/composables/useTheme.test.ts @@ -47,6 +47,15 @@ function contrast(foreground: string, background: string) { } describe('微信风格明暗主题', () => { + it('浅色 Tabs 分隔线与页面、内容表面保持可辨识的灰阶差异', () => { + const { overrides } = setup() + const color = overrides.value.Tabs!.tabBorderColor! + if (typeof color !== 'string') throw new Error('Tabs 分隔线颜色必须为字符串') + expect(color).toBe('#bfbfbf') + expect(contrast(color, '#ededed')).toBeGreaterThan(1.5) + expect(contrast(color, '#ffffff')).toBeGreaterThan(1.5) + }) + it('默认跟随系统并实时响应,销毁时移除监听', async () => { const state = setup() expect(state.preference.value).toBe('system') diff --git a/src/composables/useTheme.ts b/src/composables/useTheme.ts index 4e208b8..2c1cbfc 100644 --- a/src/composables/useTheme.ts +++ b/src/composables/useTheme.ts @@ -216,7 +216,7 @@ export function useTheme() { tabTextColorHoverLine: accentText, barColor: primary, // Tabs 保留横向分隔线,明确导航与内容边界;选中下划线仍使用主绿色。 - tabBorderColor: dark ? '#333333' : '#e5e5e5' + tabBorderColor: dark ? '#333333' : '#bfbfbf' }, Tag: { textColorPrimary: accentText, diff --git a/src/features/breakdown/BreakdownPage.vue b/src/features/breakdown/BreakdownPage.vue index dd31ab0..cf3441e 100644 --- a/src/features/breakdown/BreakdownPage.vue +++ b/src/features/breakdown/BreakdownPage.vue @@ -382,15 +382,20 @@ function exportResult() {
-
- + {{ option.label }}{{ subjects.filter(item => item.module === option.value).length }}分镜任务明细导出 JSON - + >分镜任务明细 + +
diff --git a/src/features/create-drama/CreateDramaPage.vue b/src/features/create-drama/CreateDramaPage.vue index f61e710..9867502 100644 --- a/src/features/create-drama/CreateDramaPage.vue +++ b/src/features/create-drama/CreateDramaPage.vue @@ -115,26 +115,28 @@ function exportScript() { class="mt-4" >
-
- + 剧集正文 {{ episodes.length }}角色设定世界观审核记录 -
- {{ showHistory ? '收起执行记录' : '执行记录' }} - 导出剧本进入拆解 -
+ >审核记录 + +
diff --git a/src/features/workflows/pages.test.ts b/src/features/workflows/pages.test.ts index 155ca18..183d229 100644 --- a/src/features/workflows/pages.test.ts +++ b/src/features/workflows/pages.test.ts @@ -829,14 +829,15 @@ describe('工作台页面交互', () => { expect(fetcher.mock.calls.every(([, init]) => init?.method === 'GET')).toBe(true) }) - it('拆解导出按钮紧邻标签栏且保持空快照禁用状态', () => { + it('拆解导出按钮位于 Tabs suffix,且保持空快照禁用状态', () => { wrapper = mount(BreakdownPage, { attachTo: document.body, global: { provide: { [projectContextKey as symbol]: context() }, stubs: { RouterLink: true } } }) const toolbar = wrapper.get('.result-toolbar') expect(toolbar.find('[aria-label="拆解结果"]').exists()).toBe(true) - const exportButton = toolbar.get(':scope > button') + expect(toolbar.find(':scope > button').exists()).toBe(false) + const exportButton = toolbar.get('.n-tabs-nav__suffix button') expect(exportButton.text()).toBe('导出 JSON') expect(exportButton.element.disabled).toBe(true) }) @@ -1422,6 +1423,9 @@ describe('工作台页面交互', () => { attachTo: document.body, global: { provide: { [projectContextKey as symbol]: context() }, stubs: { RouterLink: true } } }) + const suffix = wrapper.get('.script-section .n-tabs-nav__suffix') + expect(suffix.findAll('button').map(item => item.text())).toEqual(['执行记录', '导出剧本']) + expect(suffix.get('router-link-stub').attributes('to')).toBe(`/projects/${fixture.id}/breakdown`) const reader = wrapper.get('.reader-scroll .n-scrollbar-container').element reader.scrollTop = 120 expect(wrapper.find('.history-panel').exists()).toBe(false)