style: 区分主体条纹与形态块背景
This commit is contained in:
@@ -113,6 +113,7 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S
|
|||||||
- 拆解分镜区的剧集选择、节拍/镜头统计与“进入分镜设计”共用一条紧凑工具栏;“选择剧集”不拆行。选择器默认显示正文对应的真实剧集,轮询移除当前剧集时同步回退;没有剧情目标与情绪弧线时不保留空信息面板。
|
- 拆解分镜区的剧集选择、节拍/镜头统计与“进入分镜设计”共用一条紧凑工具栏;“选择剧集”不拆行。选择器默认显示正文对应的真实剧集,轮询移除当前剧集时同步回退;没有剧情目标与情绪弧线时不保留空信息面板。
|
||||||
- 拆解人物、场景、道具共用固定斑马纹列表:奇数行常驻原悬停灰底,偶数行保留次级底色,整行不随鼠标或键盘焦点变色;内部控件仍保留交互与焦点反馈,搜索后的条纹顺序按可见列表重新计算。此类记录(含剧本角色设定)首项同样保留 20px 内边距,主题切换时背景色使用 160ms 过渡,并遵循系统减少动态效果设置。
|
- 拆解人物、场景、道具共用固定斑马纹列表:奇数行常驻原悬停灰底,偶数行保留次级底色,整行不随鼠标或键盘焦点变色;内部控件仍保留交互与焦点反馈,搜索后的条纹顺序按可见列表重新计算。此类记录(含剧本角色设定)首项同样保留 20px 内边距,主题切换时背景色使用 160ms 过渡,并遵循系统减少动态效果设置。
|
||||||
- 拆解主体列表的搜索框统一靠左,图标使用 `NInput` 的 `prefix` 插槽;统计紧随输入框弱化显示,搜索时显示匹配数/总数,清空后恢复总数。搜索与统计垂直居中,窄内容区允许换行,不再左右分散。
|
- 拆解主体列表的搜索框统一靠左,图标使用 `NInput` 的 `prefix` 插槽;统计紧随输入框弱化显示,搜索时显示匹配数/总数,清空后恢复总数。搜索与统计垂直居中,窄内容区允许换行,不再左右分散。
|
||||||
|
- 展开后的每个形态使用独立直角色块,与所在主体条纹采用相反的主题灰阶;保留 16px 内边距、12px 块间距和名称/默认标签间距,奇偶行及搜索重排后均可区分内外层,不新增悬停变色。
|
||||||
- 窄屏默认收起侧栏,目录与正文改用紧凑布局;执行记录仍可查看。长弹窗在自身内部滚动。
|
- 窄屏默认收起侧栏,目录与正文改用紧凑布局;执行记录仍可查看。长弹窗在自身内部滚动。
|
||||||
|
|
||||||
布局样式集中在 `src/admin.css`,业务排版保留在 `src/styles.css`。新增页面应使用 `WorkspacePage`(内容优先页开启 `compact`),低频操作使用 `WorkspaceTools`;避免通过 `body` 或 `window.scrollTo` 管理滚动。
|
布局样式集中在 `src/admin.css`,业务排版保留在 `src/styles.css`。新增页面应使用 `WorkspacePage`(内容优先页开启 `compact`),低频操作使用 `WorkspaceTools`;避免通过 `body` 或 `window.scrollTo` 管理滚动。
|
||||||
|
|||||||
@@ -178,6 +178,60 @@ describe('拆解页内容滚动', () => {
|
|||||||
expect(css + readFileSync('src/styles.css', 'utf8')).not.toContain('.search-field')
|
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('主体奇数行常驻原悬停底色,偶数行不变,整行不再随鼠标或焦点变色', () => {
|
it('主体奇数行常驻原悬停底色,偶数行不变,整行不再随鼠标或焦点变色', () => {
|
||||||
// DOM 环境无法模拟浏览器 :hover 命中;检查静态底色与无整行交互选择器的契约。
|
// DOM 环境无法模拟浏览器 :hover 命中;检查静态底色与无整行交互选择器的契约。
|
||||||
const css = readFileSync('src/styles.css', 'utf8')
|
const css = readFileSync('src/styles.css', 'utf8')
|
||||||
|
|||||||
@@ -70,9 +70,9 @@ const filtered = computed(() =>
|
|||||||
<div
|
<div
|
||||||
v-for="form in forms.filter(item => item.profileId === subject.profileId)"
|
v-for="form in forms.filter(item => item.profileId === subject.profileId)"
|
||||||
:key="form.formId"
|
:key="form.formId"
|
||||||
class="mt-3 surface-inset p-4"
|
class="subject-form-card"
|
||||||
>
|
>
|
||||||
<p class="text-sm font-medium">
|
<p class="flex flex-wrap items-center gap-2 text-sm font-medium">
|
||||||
{{ form.formName || form.name
|
{{ form.formName || form.name
|
||||||
}}<NTag v-if="form.isDefault" size="small" :bordered="false">默认</NTag>
|
}}<NTag v-if="form.isDefault" size="small" :bordered="false">默认</NTag>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -210,6 +210,17 @@
|
|||||||
.subject-record-list > article:nth-child(odd) {
|
.subject-record-list > article:nth-child(odd) {
|
||||||
background: var(--app-control);
|
background: var(--app-control);
|
||||||
}
|
}
|
||||||
|
/* 形态块与所在主体条纹使用相反灰阶,避免偶数行内外同色;不增加边框或悬停态。 */
|
||||||
|
.subject-form-card {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
background: var(--app-subtle);
|
||||||
|
}
|
||||||
|
.subject-record-list > article:nth-child(even) .subject-form-card {
|
||||||
|
background: var(--app-control);
|
||||||
|
}
|
||||||
|
|
||||||
.table-scroll {
|
.table-scroll {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
Reference in New Issue
Block a user