diff --git a/README.md b/README.md index c4ea095..4327e99 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S - 拆解分镜区的剧集选择、节拍/镜头统计与“进入分镜设计”共用一条紧凑工具栏;“选择剧集”不拆行。选择器默认显示正文对应的真实剧集,轮询移除当前剧集时同步回退;没有剧情目标与情绪弧线时不保留空信息面板。 - 拆解人物、场景、道具共用固定斑马纹列表:奇数行常驻原悬停灰底,偶数行保留次级底色,整行不随鼠标或键盘焦点变色;内部控件仍保留交互与焦点反馈,搜索后的条纹顺序按可见列表重新计算。此类记录(含剧本角色设定)首项同样保留 20px 内边距,主题切换时背景色使用 160ms 过渡,并遵循系统减少动态效果设置。 - 拆解主体列表的搜索框统一靠左,图标使用 `NInput` 的 `prefix` 插槽;统计紧随输入框弱化显示,搜索时显示匹配数/总数,清空后恢复总数。搜索与统计垂直居中,窄内容区允许换行,不再左右分散。 +- 展开后的每个形态使用独立直角色块,与所在主体条纹采用相反的主题灰阶;保留 16px 内边距、12px 块间距和名称/默认标签间距,奇偶行及搜索重排后均可区分内外层,不新增悬停变色。 - 窄屏默认收起侧栏,目录与正文改用紧凑布局;执行记录仍可查看。长弹窗在自身内部滚动。 布局样式集中在 `src/admin.css`,业务排版保留在 `src/styles.css`。新增页面应使用 `WorkspacePage`(内容优先页开启 `compact`),低频操作使用 `WorkspaceTools`;避免通过 `body` 或 `window.scrollTo` 管理滚动。 diff --git a/src/features/breakdown/BreakdownPage.test.ts b/src/features/breakdown/BreakdownPage.test.ts index 80c34bb..d54f464 100644 --- a/src/features/breakdown/BreakdownPage.test.ts +++ b/src/features/breakdown/BreakdownPage.test.ts @@ -178,6 +178,60 @@ describe('拆解页内容滚动', () => { expect(css + readFileSync('src/styles.css', 'utf8')).not.toContain('.search-field') }) + it.each([ + ['人物', 'character'], + ['场景', 'scene'], + ['道具', 'prop'] + ] as const)('%s 的奇偶主体展开后均使用独立形态块,搜索重排不影响形态归属', async (label, module) => { + const record = checkpoint() + // 前两个主体各准备两种形态,覆盖奇偶条纹和默认标签,不调用模型接口。 + record.state.breakdownResult!.subjectForms = [0, 1].flatMap(subjectIndex => + [0, 1].map(formIndex => ({ + formId: `${module}-${subjectIndex}-form-${formIndex}`, + profileId: `${module}-${subjectIndex}`, + type: module, + name: `主体 ${subjectIndex + 1} 形态 ${formIndex + 1}`, + isDefault: formIndex === 0, + description: '形态描述', + appearancePrompt: '形态提示词' + })) + ) + mountPage([record]) + await selectTab(label) + const articles = wrapper!.findAll('.subject-record-list > article') + for (const index of [0, 1]) { + const article = articles[index]! + await article.get('.n-collapse-item__header-main').trigger('click') + await flushPromises() + const cards = article.findAll('.subject-form-card') + expect(cards).toHaveLength(2) + expect(cards[0]!.get('.n-tag').text()).toBe('默认') + for (const card of cards) { + expect(card.classes()).not.toContain('surface-inset') + expect(card.text()).toContain(`主体 ${index + 1} 形态`) + expect(card.text()).toContain('形态描述') + expect(card.text()).toContain('形态提示词') + } + } + // 原偶数主体过滤后成为首项,仍由当前 DOM 的奇偶选择器决定内外层底色。 + await wrapper!.get('input[aria-label="搜索主体"]').setValue(`@${module}1`) + const first = wrapper!.get('.subject-record-list > article') + expect(first.findAll('.subject-form-card')).toHaveLength(2) + expect(first.get('.subject-form-card').text()).toContain('主体 2 形态 1') + }) + + it('形态块在两种主体条纹上采用相反灰阶,以间距分组且没有整块悬停态', () => { + // DOM 环境不提供真实主题绘制,检查奇偶行底色、内边距和状态选择器契约。 + const css = readFileSync('src/styles.css', 'utf8') + expect(css).toMatch( + /\.subject-form-card\s*\{[^}]*margin-top:\s*12px;[^}]*padding:\s*16px;[^}]*background:\s*var\(--app-subtle\);/ + ) + expect(css).toMatch( + /\.subject-record-list > article:nth-child\(even\) \.subject-form-card\s*\{\s*background:\s*var\(--app-control\);/ + ) + expect(css).not.toMatch(/\.subject-form-card[^{}]*(?::hover|:focus-within)/) + }) + it('主体奇数行常驻原悬停底色,偶数行不变,整行不再随鼠标或焦点变色', () => { // DOM 环境无法模拟浏览器 :hover 命中;检查静态底色与无整行交互选择器的契约。 const css = readFileSync('src/styles.css', 'utf8') diff --git a/src/features/breakdown/components/SubjectList.vue b/src/features/breakdown/components/SubjectList.vue index dfae614..edba227 100644 --- a/src/features/breakdown/components/SubjectList.vue +++ b/src/features/breakdown/components/SubjectList.vue @@ -70,9 +70,9 @@ const filtered = computed(() =>
+
{{ form.formName || form.name
}}