style: 全站使用背景分层替代装饰边框并统一直角

This commit is contained in:
GouJ
2026-09-01 21:39:05 +08:00
parent 799f16d336
commit 03bf87ca9a
25 changed files with 292 additions and 116 deletions
+3 -2
View File
@@ -94,13 +94,14 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S
- 拆解页固定精简状态栏与结果标签,设置、预览及恢复操作收进“拆解设置与恢复”面板;人物、场景、道具、分镜和任务明细共用独立结果滚动区,执行记录单独滚动。结果区占满剩余高度,切换标签回到顶部,轮询刷新或开关设置不重置阅读位置;失败与校验提示在面板关闭时仍然可见。
- 剧集正文按编号连续展示全部已写入剧集;点击目录定位正文,滚动正文同步高亮并保持当前目录项可见。目录分行显示集数和完整标题,执行记录可按需展开。切换角色/世界观等标签不会销毁阅读器或重置位置。
- 主体身份、分镜设计和镜头生产共用 `DirectoryItem`:编号、完整标题和状态分层展示,桌面目录为 240–280px,窄屏改为可横向滚动的条目。目录标题与筛选固定,只有列表滚动。拆解结果的导出按钮与标签同排右对齐;形态图片筛选按内容区宽度自动排成一行或两列网格。
- 侧栏导航与主体/剧集/镜头目录采用直角满行背景,移除列表两侧的内缩,悬停和选中态都铺满所在列。条目内部保留文字间距;窄屏横向目录仍保持独立滚动,不影响表单与弹窗的圆角
- 主体目录以母版缩略图搭配名称、编号与状态,默认保留行背景和细分隔线。人物优先复用选角接口的母版;缺少地址的条目进入可视区域后才读取正式身份图库,不为整份目录轮询。无母版或加载失败显示人物/场景/道具图标,候选与形态图不会替代母版;点击缩略图只切换主体,刷新目录可重试读取。
- 侧栏导航与主体/剧集/镜头目录采用直角满行背景,移除列表两侧的内缩,悬停和选中态都铺满所在列。条目内部保留文字间距;窄屏横向目录仍保持独立滚动。
- 主体目录以母版缩略图搭配名称、编号与状态,默认行背景和轻微间距区分条目。人物优先复用选角接口的母版;缺少地址的条目进入可视区域后才读取正式身份图库,不为整份目录轮询。无母版或加载失败显示人物/场景/道具图标,候选与形态图不会替代母版;点击缩略图只切换主体,刷新目录可重试读取。
- 分镜设计与镜头生产共用两级镜头目录:分组头展示 `BEAT 01` 和镜头数,组内仅显示“镜头 01”、标题及状态。分组头在本组内吸顶;窄屏横向模式才在条目上补回 Beat 归属。点击仍使用正式 Shot ID,不会混淆不同 Beat 的同号镜头。
- 身份、分镜、生产与图库的批量配置、说明和诊断统一放入 `WorkspaceTools`Naive UI Drawer),按需覆盖当前工作区,不挤压列表。面板支持 Esc 或关闭按钮;关闭不会取消任务或清除配置。历史回执可从入口查看,新回执主动打开诊断;查询错误始终在外层可见。
- 生产步骤的操作按钮保留上下间距;`ConfirmAction` 用稳定根元素承接外部间距,避免多根弹窗组件吞掉 `class`
- 右上角提供浅色、暗黑、跟随系统三种模式,默认跟随系统,选择保存在本机浏览器。存储被禁用时仍可在当前会话切换。
- 采用微信风格配色:浅色为浅灰背景与白色内容,暗色为近黑背景与深灰内容;绿色用于主操作、选中态和进度。按钮文字、目录文字与主绿色分别配置以保证对比度,错误/警告仍使用独立语义色。Naive UI 和业务内容共用主题,浏览器主题色随之切换;加载前应用偏好,减少主题闪烁。
- 全站采用低边框、直角样式:面板、提示框、标签、图片卡片和表单用背景层次代替静态描边;表格与长记录列表使用交替底色,抽屉用灰底承托内容。保留输入框聚焦/校验反馈、复选框、选中指示和必要的浮层阴影;开关、加载等功能图形不强制改形状。
- 窄屏默认收起侧栏,目录与正文改用紧凑布局;执行记录仍可查看。长弹窗在自身内部滚动。
布局样式集中在 `src/admin.css`,业务排版保留在 `src/styles.css`。新增页面应使用 `WorkspacePage`(内容优先页开启 `compact`),低频操作使用 `WorkspaceTools`;避免通过 `body``window.scrollTo` 管理滚动。
+1 -2
View File
@@ -84,7 +84,6 @@ const menuOptions = computed<MenuOption[]>(() => [
<a href="#main-content" class="skip-link">跳到主要内容</a>
<NLayout has-sider class="admin-layout">
<NLayoutSider
bordered
collapse-mode="width"
:collapsed="collapsed"
:width="208"
@@ -106,7 +105,7 @@ const menuOptions = computed<MenuOption[]>(() => [
/>
</NLayoutSider>
<NLayout class="admin-main">
<NLayoutHeader bordered class="admin-topbar">
<NLayoutHeader class="admin-topbar">
<div class="flex min-w-0 items-center gap-3">
<NButton
quaternary
+26 -15
View File
@@ -5,6 +5,8 @@
--app-body: #ededed;
--app-surface: #ffffff;
--app-subtle: #f7f7f7;
--app-control: #f0f0f0;
--app-control-hover: #e5e5e5;
--app-border: #e5e5e5;
--app-inverse: #ffffff;
--app-accent: #07c160;
@@ -22,6 +24,8 @@
--app-body: #111111;
--app-surface: #191919;
--app-subtle: #232323;
--app-control: #2b2b2b;
--app-control-hover: #353535;
--app-border: #333333;
--app-inverse: #111111;
--app-accent-text: #5cd693;
@@ -100,6 +104,21 @@ body {
padding: 0 20px;
gap: 12px;
}
/* 图标按钮保留原有方形点击热区;仅取消 circle 属性产生的圆形外观。 */
.n-button {
border-radius: 0;
}
.admin-sider .n-menu-divider {
height: 12px;
background: transparent;
}
.workspace-tools-drawer .n-drawer-header,
.workspace-tools-drawer .n-drawer-footer {
background: var(--app-subtle);
}
.workspace-tools-drawer .n-drawer-body {
background: var(--app-body);
}
.theme-select {
width: 120px;
}
@@ -121,7 +140,6 @@ body {
flex-shrink: 0;
padding: 9px 20px;
background: var(--app-surface);
border-bottom: 1px solid var(--app-border);
}
.project-header .n-page-header__main,
.project-header .n-page-header__title {
@@ -267,8 +285,7 @@ body {
gap: 8px 16px;
padding: 7px 12px;
flex-shrink: 0;
border: 1px solid var(--app-border);
border-radius: 4px;
border-radius: 0;
background: var(--app-subtle);
font-size: 12px;
}
@@ -446,7 +463,6 @@ body {
gap: 10px;
padding: 0 12px 12px;
flex-shrink: 0;
border-bottom: 1px solid var(--app-border);
}
.directory-filters > * {
min-width: 0;
@@ -470,8 +486,7 @@ body {
gap: 12px;
flex-shrink: 0;
padding: 10px 14px;
border-block: 1px solid var(--app-border);
background: var(--app-subtle);
background: var(--app-control);
font:
600 11px/1.6 ui-monospace,
monospace;
@@ -533,13 +548,12 @@ body {
flex: 1;
}
.identity-subject-list-content {
gap: 0;
gap: 2px;
padding: 0 0 8px;
}
.n-button.identity-subject-item {
min-height: 86px;
padding: 10px 14px;
border-bottom: 1px solid var(--app-border);
}
.n-button.identity-subject-item:not(.selected) {
background: var(--app-surface);
@@ -616,8 +630,8 @@ body {
align-items: center;
gap: 5px;
padding: 1px 5px;
border: 1px solid var(--app-border);
border-radius: 3px;
border-radius: 0;
background: var(--app-control);
}
.directory-status svg {
flex-shrink: 0;
@@ -746,7 +760,6 @@ body {
min-height: 0;
min-width: 0;
background: var(--app-subtle);
border-right: 1px solid var(--app-border);
}
.reader-directory-heading {
display: flex;
@@ -827,7 +840,6 @@ body {
}
.reader-chapter + .reader-chapter {
margin-top: 48px;
border-top: 1px solid var(--app-border);
padding-top: 36px;
}
.reader-chapter:last-child {
@@ -1026,6 +1038,7 @@ body {
height: auto;
padding: 6px;
white-space: normal;
background: var(--app-subtle);
}
.n-button.image-history-item.selected {
background: var(--app-selected);
@@ -1066,7 +1079,6 @@ body {
gap: 12px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--app-border);
}
.n-button.casting-item {
width: 100%;
@@ -1076,7 +1088,7 @@ body {
padding: 12px 14px;
white-space: normal;
text-align: left;
background: var(--app-surface);
background: var(--app-subtle);
}
.n-button.casting-item.selected {
background: var(--app-selected);
@@ -1223,7 +1235,6 @@ body {
.directory-panel {
height: 100%;
border-right: 0;
border-bottom: 1px solid var(--app-border);
}
.directory-heading {
padding: 12px 14px 8px;
+3 -2
View File
@@ -59,8 +59,9 @@ describe('统一目录条目', () => {
expect(css).toMatch(/\.directory-item-title\s*\{[^}]*overflow-wrap:\s*anywhere/)
expect(css).toMatch(/\.directory-list-content\s*\{[^}]*flex-direction:\s*row;[^}]*width:\s*max-content/)
expect(css).toContain('grid-template-columns: clamp(240px, 22%, 280px) minmax(0, 1fr)')
expect(css).toMatch(/\.identity-subject-list-content\s*\{[^}]*gap:\s*0;[^}]*padding:\s*0 0 8px/)
expect(css).toMatch(/\.n-button\.identity-subject-item\s*\{[^}]*min-height:\s*86px;[^}]*border-bottom:/)
expect(css).toMatch(/\.identity-subject-list-content\s*\{[^}]*gap:\s*2px;[^}]*padding:\s*0 0 8px/)
expect(css).toMatch(/\.n-button\.identity-subject-item\s*\{[^}]*min-height:\s*86px;[^}]*padding:\s*10px 14px/)
expect(css).not.toMatch(/\.n-button\.identity-subject-item\s*\{[^}]*border-bottom:/)
expect(css).toMatch(/\.identity-thumbnail\s*\{[^}]*width:\s*48px;[^}]*height:\s*64px/)
expect(css).toMatch(/\.n-button\.identity-subject-item:not\(\.selected\)\s*\{[^}]*var\(--app-surface\)/)
})
+89
View File
@@ -0,0 +1,89 @@
import { h } from 'vue'
import { mount, type VueWrapper } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { NAlert, NButton, NCard, NConfigProvider, NInput, NSelect, NTable, NTag } from 'naive-ui'
import { readFileSync, readdirSync } from 'node:fs'
import { useTheme } from '../../composables/useTheme'
let wrapper: VueWrapper | undefined
afterEach(() => {
wrapper?.unmount()
wrapper = undefined
localStorage.clear()
delete document.documentElement.dataset.theme
document.documentElement.style.colorScheme = ''
vi.unstubAllGlobals()
})
describe('全站平面主题', () => {
it.each([false, true])('模式 %s 的真实控件无静态描边、使用直角,并保留聚焦与错误反馈', dark => {
localStorage.setItem('drama-studio-theme', dark ? 'dark' : 'light')
vi.stubGlobal('matchMedia', () => ({ matches: dark, addEventListener() {}, removeEventListener() {} }))
wrapper = mount({
setup() {
const { theme, overrides } = useTheme()
return () =>
h(NConfigProvider, { theme: theme.value, themeOverrides: overrides.value }, () => [
h(NButton, {}, () => '生成'),
h(NInput, { class: 'normal-input' }),
h(NInput, { status: 'error', class: 'error-input' }),
h(NSelect, { options: [{ label: '第一集', value: 1 }] }),
h(NAlert, { type: 'error' }, () => '生成失败'),
h(NCard, {}, () => '内容'),
h(NTag, {}, () => '待设计'),
h(NTable, { striped: true }, () => h('tbody', [h('tr', [h('td', '记录')])]))
])
}
})
const value = (selector: string, name: string) =>
(wrapper!.get(selector).element as HTMLElement).style.getPropertyValue(name)
const control = dark ? '#2b2b2b' : '#f0f0f0'
for (const selector of ['.n-button', '.normal-input', '.n-base-selection', '.n-alert', '.n-card', '.n-tag']) {
expect(value(selector, '--n-border-radius')).toBe('0px')
}
for (const selector of ['.n-button', '.normal-input', '.n-base-selection']) {
expect(value(selector, '--n-border')).toBe('none')
expect(value(selector, '--n-color')).toBe(control)
expect(value(selector, '--n-border-focus')).toContain(dark ? '#5cd693' : '#087c42')
}
expect(value('.n-alert', '--n-border')).toBe('none')
expect(value('.n-tag', '--n-border')).toBe('none')
expect(value('.n-card', '--n-border-color')).toBe('transparent')
expect(value('.n-table', '--n-border-color')).toBe('transparent')
expect(value('.error-input', '--n-border-error')).toContain(dark ? '#fa7373' : '#a93232')
expect(wrapper.get('.error-input').classes()).toContain('n-input--error-status')
// 不全局抹掉边框色:未勾选复选框和错误态继续由 Naive 的主题处理。
const theme = wrapper.getComponent(NConfigProvider).props('themeOverrides')!
expect(theme.common?.borderColor).not.toBe('transparent')
expect(theme.Checkbox).not.toHaveProperty('border')
expect(theme.Tabs?.barColor).toBe('#07c160')
})
it('业务面板、列表分组和标签以背景区分,保留键盘焦点与选中指示', () => {
// happy-dom 不计算布局;此项保护 CSS 契约,不能替代浏览器视觉验收。
const admin = readFileSync('src/admin.css', 'utf8')
const css = readFileSync('src/styles.css', 'utf8')
expect(admin + css).not.toMatch(/border-radius:\s*[1-9]/)
expect(admin + css).not.toMatch(/border(?:-[\w]+)?:\s*1px (?:solid|dashed) var\(--(?:app-border|color-line)\)/)
expect(css).toMatch(/\.surface-inset\s*\{\s*background:\s*var\(--app-subtle\)/)
expect(css).toMatch(/\.record-list > article:nth-child\(even\)\s*\{\s*background:/)
expect(admin).toMatch(/\.directory-group-heading\s*\{[^}]*background:\s*var\(--app-control\)/)
expect(admin).toMatch(/\.directory-status\s*\{[^}]*background:\s*var\(--app-control\)/)
expect(admin).toMatch(/\.n-button\.directory-item:focus-visible\s*\{[^}]*outline:\s*2px/)
expect(admin).toMatch(
/\.n-button\.directory-item\.selected\s*\{[^}]*var\(--app-selected\)[^}]*var\(--app-accent\)/
)
})
it('所有页面不再通过工具类添加装饰边框或圆角', () => {
const files = readdirSync('src', { recursive: true, encoding: 'utf8' }).filter(path => path.endsWith('.vue'))
for (const path of files) {
const source = readFileSync(`src/${path}`, 'utf8')
for (const match of source.matchAll(/\bclass="([^"]*)"/g)) {
expect(match[1]).not.toMatch(
/(?:^|\s)(?:rounded(?:-[\w-]+)?|border|border-[trbl](?:-\d+)?|divide-[xy])(?:\s|$)/
)
}
}
})
})
+8 -3
View File
@@ -89,7 +89,7 @@ describe('微信风格明暗主题', () => {
it.each([false, true])('明暗模式 %s 的组件表面、语义色与业务 CSS 一致,主要文字保持可读', dark => {
const state = setup(dark)
const { common, Menu, Button, Layout } = state.overrides.value
const { common, Menu, Button, Layout, Input, Select } = state.overrides.value
const css = readFileSync('src/admin.css', 'utf8')
const lightTokens = css.match(/:root\s*\{([^}]+)\}/)![1]!
const darkTokens = css.match(/:root\[data-theme='dark'\]\s*\{([^}]+)\}/)![1]!
@@ -119,8 +119,13 @@ describe('微信风格明暗主题', () => {
itemColorActiveCollapsed: tokens.selected,
itemTextColorActive: tokens['accent-text']
})
// 圆角调整只用于导航,表单、普通按钮和弹窗仍保留原有主题
expect(common?.borderRadius).toBe('6px')
// 全站直角;输入区域与普通按钮共享底色,保持明暗模式一致
expect(common?.borderRadius).toBe('0px')
expect(common?.borderRadiusSmall).toBe('0px')
expect(Input?.color).toBe(tokens.control)
expect(Button?.color).toBe(tokens.control)
expect(Button?.colorHover).toBe(tokens['control-hover'])
expect(Select?.peers?.InternalSelection?.color).toBe(tokens.control)
expect(Button?.textColorPrimary).toBe(tokens['on-accent'])
for (const background of [common!.primaryColor!, common!.primaryColorHover!, common!.primaryColorPressed!]) {
expect(contrast(String(Button!.textColorPrimary), background)).toBeGreaterThanOrEqual(4.5)
+88 -4
View File
@@ -39,6 +39,18 @@ export function useTheme() {
const info = dark ? '#91a8d2' : '#576b95'
const danger = dark ? '#fa7373' : '#a93232'
const warning = dark ? '#e8b66a' : '#916019'
const control = dark ? '#2b2b2b' : '#f0f0f0'
const controlHover = dark ? '#353535' : '#e5e5e5'
// 静态边界交给底色;聚焦与错误边框仍由控件的状态主题负责。
const focusBorder = `1px solid ${accentText}`
const selection = {
color: control,
colorActive: control,
border: 'none',
borderHover: 'none',
borderFocus: focusBorder,
borderActive: focusBorder
}
return {
common: {
primaryColor: primary,
@@ -61,7 +73,8 @@ export function useTheme() {
warningColorHover: warning,
warningColorPressed: warning,
warningColorSuppl: warning,
borderRadius: '6px',
borderRadius: '0px',
borderRadiusSmall: '0px',
bodyColor: body,
cardColor: surface,
modalColor: surface,
@@ -80,6 +93,15 @@ export function useTheme() {
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif"
},
Button: {
color: control,
colorHover: controlHover,
colorPressed: controlHover,
colorFocus: controlHover,
border: 'none',
borderHover: 'none',
borderPressed: 'none',
borderDisabled: 'none',
borderFocus: focusBorder,
textColorPrimary: onPrimary,
textColorHoverPrimary: onPrimary,
textColorPressedPrimary: onPrimary,
@@ -88,6 +110,55 @@ export function useTheme() {
textColorTextPrimary: accentText,
textColorGhostPrimary: accentText
},
Input: {
color: control,
colorHover: controlHover,
colorFocus: control,
border: 'none',
borderHover: 'none',
borderDisabled: 'none',
borderFocus: focusBorder,
groupLabelBorder: 'none'
},
Select: { peers: { InternalSelection: selection } },
Card: { borderColor: 'transparent' },
Drawer: { headerBorderBottom: 'none', footerBorderTop: 'none' },
Alert: {
border: 'none',
borderInfo: 'none',
borderSuccess: 'none',
borderWarning: 'none',
borderError: 'none'
},
Collapse: { dividerColor: 'transparent' },
Table: {
borderColor: 'transparent',
borderColorModal: 'transparent',
borderColorPopover: 'transparent',
thColor: control,
thColorModal: control,
thColorPopover: control,
tdColorStriped: subtle,
tdColorStripedModal: subtle,
tdColorStripedPopover: subtle
},
DataTable: {
borderColor: 'transparent',
borderColorModal: 'transparent',
borderColorPopover: 'transparent',
thColor: control,
tdColorStriped: subtle,
tdColorHover: selected
},
Radio: {
buttonBorderColor: 'transparent',
buttonBorderColorActive: 'transparent',
buttonBorderColorHover: 'transparent',
buttonColor: control,
buttonColorActive: selected,
buttonTextColorActive: accentText,
buttonBoxShadowFocus: `inset 0 0 0 1px ${accentText}`
},
Layout: {
color: body,
headerColor: surface,
@@ -104,12 +175,25 @@ export function useTheme() {
itemIconColorActive: accentText,
itemIconColorActiveHover: accentText
},
Tabs: { tabTextColorActiveLine: accentText, tabTextColorHoverLine: accentText, barColor: primary },
Tag: { textColorPrimary: accentText },
Tabs: {
tabTextColorActiveLine: accentText,
tabTextColorHoverLine: accentText,
barColor: primary,
tabBorderColor: 'transparent'
},
Tag: {
textColorPrimary: accentText,
border: 'none',
borderPrimary: 'none',
borderInfo: 'none',
borderSuccess: 'none',
borderWarning: 'none',
borderError: 'none'
},
Checkbox: { checkMarkColor: onPrimary },
Switch: { railColorActive: primary },
Progress: { fillColor: primary }
}
} satisfies GlobalThemeOverrides
})
/** 系统切换只更新 system 模式下的实际显示。 */
function syncSystem(event: MediaQueryListEvent) {
+1 -1
View File
@@ -393,7 +393,7 @@ function exportResult() {
</div>
<div v-if="tab === 'tasks'" class="p-5">
<NScrollbar x-scrollable v-if="snapshot?.tasks?.length" class="table-scroll">
<NTable :single-line="false" class="project-table"
<NTable :bordered="false" :single-line="true" striped class="project-table"
><thead>
<tr>
<th>分组</th>
@@ -53,7 +53,7 @@ const purposeLabels: Record<string, string> = {
{{ episode?.beatShots.reduce((sum, beat) => sum + beat.shots.length, 0) ?? 0 }} 个镜头</span
>
</div>
<div class="mb-7 border-l-2 border-accent pl-4">
<div class="mb-7 surface-inset p-4">
<p class="font-medium">{{ plan.storyGoal }}</p>
<p class="mt-2 text-sm leading-6 text-muted">{{ plan.emotionalArc }}</p>
</div>
@@ -30,7 +30,7 @@ const filtered = computed(() =>
></NInput>
</div>
</div>
<div v-if="filtered.length" class="divide-y divide-line">
<div v-if="filtered.length" class="record-list">
<article v-for="subject in filtered" :key="subject.profileId" class="py-5 first:pt-0">
<div class="mb-3 flex flex-wrap items-center gap-3">
<h3 class="font-semibold">{{ subject.name }}</h3>
@@ -66,7 +66,7 @@ const filtered = computed(() =>
<div
v-for="form in forms.filter(item => item.profileId === subject.profileId)"
:key="form.formId"
class="mt-3 border-l-2 border-line pl-4"
class="mt-3 surface-inset p-4"
>
<p class="text-sm font-medium">
{{ form.formName || form.name
@@ -148,7 +148,7 @@ function exportScript() {
</div>
<NScrollbar v-if="tab !== 'episodes'" class="panel-scroll"
><div v-if="tab === 'characters'" class="p-6 lg:p-8">
<div v-if="project?.characters.length" class="divide-y divide-line">
<div v-if="project?.characters.length" class="record-list">
<article
v-for="character in project.characters"
:key="character.id"
@@ -208,11 +208,7 @@ function exportScript() {
</div>
<div v-if="tab === 'review'" class="p-6 lg:p-8">
<div v-if="project?.reviews.length" class="space-y-5">
<article
v-for="review in project.reviews"
:key="review.id"
class="border-b border-line pb-5"
>
<article v-for="review in project.reviews" :key="review.id" class="pb-5">
<div class="mb-3 flex justify-between gap-3">
<span
class="text-sm font-medium"
+1 -1
View File
@@ -464,7 +464,7 @@ watch(
</nav>
<article class="production-detail min-w-0">
<NScrollbar class="panel-scroll" content-class="p-5 lg:p-7">
<header class="mb-5 border-b border-line pb-5">
<header class="mb-5 pb-5">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<p class="eyebrow">BEAT {{ shot.beatNo }} / SHOT {{ shot.shotNo }}</p>
@@ -401,7 +401,7 @@ async function inspectSpecs() {
</div>
</div>
<NCollapse v-if="keyframeSpec || videoSpec" class="mt-5 border-t border-line pt-4 text-xs"
<NCollapse v-if="keyframeSpec || videoSpec" class="mt-5 pt-4 text-xs"
><NCollapseItem name="details"
><template #header>本镜确定性生成规格</template>
<div class="mt-4 grid gap-5 lg:grid-cols-2">
+3
View File
@@ -100,6 +100,9 @@ function clearFilters() {
<div class="project-table-area">
<NDataTable
class="project-data-table"
:bordered="false"
:bottom-bordered="false"
striped
:columns="columns"
:data="filtered"
:row-key="row => row.id"
+1 -1
View File
@@ -271,7 +271,7 @@ watch(
修改或覆盖上游设计后后端不会自动使旧状态旧提示词失效请按顺序重生成下游内容自动修复次数仅用于
VisualState
</p>
<NCollapse class="mt-3 border-t border-line pt-3"
<NCollapse class="mt-3 pt-3"
><NCollapseItem name="details"
><template #header>视频提示词批量操作</template>
<p class="my-3 text-xs leading-6 text-muted">
@@ -43,7 +43,7 @@ function exportReceipt() {
{{ receipt.result.skippedEpisodes }} · 失败 {{ receipt.result.failedEpisodes }}
</p>
<NScrollbar x-scrollable class="table-scroll">
<NTable :single-line="false" class="project-table"
<NTable :bordered="false" :single-line="true" striped class="project-table"
><thead>
<tr>
<th>剧集</th>
@@ -40,7 +40,7 @@ defineProps<{ shot: DesignedShot }>()
>
<p v-else class="text-sm text-muted">此镜头尚无已保存的导演设计。</p>
</section>
<section class="border-t border-line pt-6">
<section class="pt-6">
<h3 class="mb-4 text-sm font-semibold">
即时视觉状态 <span class="ml-2 font-mono text-[10px] font-normal text-muted">VisualState</span>
</h3>
@@ -60,7 +60,7 @@ defineProps<{ shot: DesignedShot }>()
<article
v-for="subject in shot.visualState.subjects"
:key="subject.subjectId"
class="mt-5 border-l-2 border-line pl-4"
class="mt-5 surface-inset p-4"
>
<div class="mb-3 flex flex-wrap items-center gap-2">
<code class="subject-ref">{{ subject.subjectRef }}</code
@@ -152,7 +152,7 @@ function exportResult(kind: 'spec' | 'prompt') {
</p>
</template>
</section>
<section class="border-t border-line pt-6">
<section class="pt-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<h3 class="text-sm font-semibold">镜头生成规格</h3>
<NButton
@@ -179,7 +179,7 @@ function exportResult(kind: 'spec' | 'prompt') {
</NScrollbar>
</template>
</section>
<section class="border-t border-line pt-6">
<section class="pt-6">
<h3 class="text-sm font-semibold">视频提示词</h3>
<p class="mt-2 text-xs leading-6 text-muted">
已有提示词可直接复用没有则依据 GenerationSpec 与参考图调用模型生成此处只处理
+1 -1
View File
@@ -52,7 +52,7 @@ describe('Beat 两级镜头目录', () => {
// happy-dom 不计算吸顶位置,保护 CSS 边界,实际滚动仍需浏览器验收。
const css = readFileSync('src/admin.css', 'utf8')
expect(css).toMatch(
/\.directory-group-heading\s*\{[^}]*position:\s*sticky;[^}]*top:\s*0;[^}]*background:\s*var\(--app-subtle\)/
/\.directory-group-heading\s*\{[^}]*position:\s*sticky;[^}]*top:\s*0;[^}]*background:\s*var\(--app-control\)/
)
expect(css).toMatch(/\.directory-mobile-beat\s*\{\s*display:\s*none/)
const mobile = css.slice(css.lastIndexOf('@media (max-width: 760px)'))
@@ -250,7 +250,7 @@ watch(
<dd>{{ casting.ready }}</dd>
</div>
</dl>
<div class="mt-5 flex flex-wrap items-end gap-4 border-t border-line pt-4">
<div class="mt-5 flex flex-wrap items-end gap-4 pt-4">
<label class="w-28">
<span class="field-label">并发数</span>
<NInputNumber
@@ -543,12 +543,12 @@ watch(
@save="save"
@dirty="dirty = $event"
/>
<div v-if="anchor" class="mt-5 flex items-center gap-4 border-t border-line pt-5">
<div v-if="anchor" class="mt-5 flex items-center gap-4 pt-5">
<AssetImage
:src="anchor.imageUrl"
:alt="`${subject.name}当前身份母版`"
preview
class="h-20 w-20 shrink-0 rounded"
class="h-20 w-20 shrink-0"
/>
<div>
<p class="text-sm font-medium">当前身份母版</p>
@@ -575,7 +575,7 @@ watch(
@anchor="chooseAnchor"
/>
</template>
<div class="mt-5 border-t border-line pt-5">
<div class="mt-5 pt-5">
<h4 class="text-xs font-medium">此主体的形态</h4>
<div class="mt-3 flex flex-wrap gap-2">
<NTag v-for="form in subject.forms" :key="form.id" size="small" :bordered="false"
@@ -47,7 +47,7 @@ function chooseAnchor() {
</script>
<template>
<section class="mt-5 border-t border-line pt-5" aria-label="身份参考图库">
<section class="mt-5 pt-5" aria-label="身份参考图库">
<div class="flex items-center justify-between gap-3">
<h3 class="text-sm font-medium">身份参考图 · {{ images.length }}</h3>
<NTag size="small" :bordered="false">{{ anchor ? '已选母版' : '尚无母版' }}</NTag>
@@ -158,7 +158,7 @@ function resetFilters() {
@confirm="generateStale"
/></div
></NAlert>
<NCollapse class="mt-4 border-t border-line pt-4"
<NCollapse class="mt-4 pt-4"
><NCollapseItem name="details"
><template #header>项目批量生图</template>
<div class="mt-4 flex flex-wrap items-end gap-4">
@@ -80,7 +80,7 @@ function remove(image: VisualStyleImage) {
请填写有效图片地址和整数排序
</p>
<div v-if="images.length" class="mt-5 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<article v-for="image in images" :key="image.id" class="min-w-0 rounded border border-line p-3">
<article v-for="image in images" :key="image.id" class="surface-inset min-w-0 p-3">
<AssetImage
:src="image.imageUrl"
:alt="`${categories[image.category]}风格参考图`"
+1 -1
View File
@@ -37,7 +37,7 @@ const history = computed(() => workflowCheckpoints(props.checkpoints, props.work
>
</NTimelineItem>
</NTimeline>
<p class="mt-5 border-t border-line pt-4 text-[11px] leading-5 text-muted">
<p class="mt-5 pt-4 text-[11px] leading-5 text-muted">
记录在节点或批次结束后更新长时间无新记录不一定意味着任务失败
</p>
</NScrollbar>
+48 -61
View File
@@ -80,7 +80,7 @@
}
.casting-stats div {
padding: 10px 12px;
border-radius: 4px;
border-radius: 0;
background: var(--app-subtle);
}
.casting-stats dt {
@@ -143,7 +143,7 @@
padding: 9px 14px;
min-height: 36px;
border: 1px solid transparent;
border-radius: 5px;
border-radius: 0;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
@@ -154,19 +154,16 @@
.button-primary {
background: var(--app-accent);
color: var(--app-on-accent);
border-color: var(--app-accent);
}
.button-primary:hover:not(:disabled) {
background: var(--app-accent-hover);
}
.button-secondary {
background: var(--app-surface);
border-color: var(--app-border);
background: var(--app-control);
color: var(--app-ink);
}
.button-secondary:hover:not(:disabled) {
background: var(--app-subtle);
border-color: var(--app-border);
background: var(--app-control-hover);
}
.text-button {
@@ -182,8 +179,18 @@
}
.panel {
background: var(--app-surface);
border: 1px solid var(--color-line);
border-radius: 7px;
border-radius: 0;
}
/* 次级信息用低对比底色成组,避免用边框把工作台切成碎片。 */
.surface-inset {
background: var(--app-subtle);
}
.record-list > article {
padding: 20px;
}
.record-list > article:nth-child(even) {
background: var(--app-subtle);
}
.search-field {
@@ -191,8 +198,7 @@
align-items: center;
gap: 8px;
background: var(--app-surface);
border: 1px solid var(--color-line);
border-radius: 5px;
border-radius: 0;
padding: 7px 10px;
color: var(--color-muted);
}
@@ -204,8 +210,9 @@
width: 145px;
color: var(--color-ink);
}
.search-field:focus-within {
border-color: var(--color-accent);
.search-field:has(> input):focus-within {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
.table-scroll {
overflow: hidden;
@@ -218,22 +225,23 @@
}
.project-table th {
padding: 13px 20px;
background: var(--app-surface);
background: var(--app-control);
color: var(--color-muted);
font-size: 10px;
font-weight: 500;
border-bottom: 1px solid var(--color-line);
white-space: nowrap;
}
.project-table td {
padding: 21px 20px;
border-bottom: 1px solid var(--app-subtle);
}
.project-table tr:last-child td {
border-bottom: none;
}
.project-table tbody tr:hover {
background: var(--app-surface);
.project-table tbody tr:nth-child(even) td {
background: var(--app-subtle);
}
.project-table tbody tr:hover td {
background: var(--app-selected);
}
.production-controls {
@@ -256,8 +264,7 @@
width: 36px;
height: 36px;
flex-shrink: 0;
border: 1px solid var(--app-border);
border-radius: 5px;
border-radius: 0;
background: var(--app-subtle);
color: var(--app-ink);
}
@@ -269,7 +276,7 @@
.production-stats div,
.production-status-grid div {
padding: 10px;
border-radius: 4px;
border-radius: 0;
background: var(--app-subtle);
}
.production-stats dt,
@@ -297,13 +304,11 @@
.production-shot-list {
max-height: 760px;
overflow: hidden;
border-right: 1px solid var(--color-line);
background: var(--app-surface);
padding-block: 8px;
}
.production-stage {
padding: 20px 0;
border-top: 1px solid var(--color-line);
}
.production-stage:first-of-type {
border-top: none;
@@ -321,9 +326,8 @@
}
.keyframe-card {
overflow: hidden;
border: 1px solid var(--color-line);
border-radius: 5px;
background: var(--app-surface);
border-radius: 0;
background: var(--app-subtle);
}
.keyframe-card .asset-image {
aspect-ratio: 16 / 9;
@@ -332,9 +336,8 @@
.video-record {
display: flex;
overflow: hidden;
border: 1px solid var(--color-line);
border-radius: 5px;
background: var(--app-surface);
border-radius: 0;
background: var(--app-subtle);
}
.production-video {
width: min(44%, 390px);
@@ -361,7 +364,6 @@
max-height: none;
overflow: hidden;
border-right: none;
border-bottom: 1px solid var(--color-line);
}
.production-status-grid,
.keyframe-grid {
@@ -389,7 +391,6 @@
justify-content: flex-end;
gap: 10px;
padding-top: 20px;
border-top: 1px solid var(--color-line);
}
.field-label {
display: block;
@@ -400,10 +401,9 @@
}
.input {
width: 100%;
background: var(--app-surface);
background: var(--app-control);
color: var(--color-ink);
border: 1px solid var(--app-border);
border-radius: 5px;
border-radius: 0;
padding: 9px 11px;
font-size: 12px;
line-height: 1.7;
@@ -417,8 +417,7 @@
}
.alert {
padding: 12px 15px;
border: 1px solid var(--app-border);
border-radius: 5px;
border-radius: 0;
background: var(--app-subtle);
color: var(--app-ink);
font-size: 12px;
@@ -426,9 +425,8 @@
overflow-wrap: anywhere;
}
.alert-error {
border-color: var(--app-border);
background: var(--app-subtle);
color: var(--app-ink);
color: var(--app-danger);
}
.stage-strip {
@@ -460,8 +458,7 @@
}
.history-panel {
padding: 23px 21px;
background: var(--app-surface);
border-left: 1px solid var(--color-line);
background: var(--app-subtle);
}
.form-image-grid {
@@ -521,7 +518,6 @@
gap: 20px;
padding-top: 20px;
margin-top: 20px;
border-top: 1px solid var(--color-line);
}
.generation-row > div:last-child {
flex-shrink: 0;
@@ -534,7 +530,6 @@
.shot-list {
max-height: 1000px;
overflow: hidden;
border-right: 1px solid var(--color-line);
background: var(--app-surface);
padding: 0 8px 20px;
}
@@ -544,8 +539,8 @@
gap: 14px;
}
.reference-card {
border: 1px solid var(--color-line);
border-radius: 4px;
background: var(--app-subtle);
border-radius: 0;
overflow: hidden;
}
.reference-placeholder {
@@ -558,7 +553,7 @@
}
.storyboard-json {
padding: 16px;
border-radius: 4px;
border-radius: 0;
background: var(--app-subtle);
font:
11px/1.9 ui-monospace,
@@ -570,7 +565,6 @@
line-height: 1.9;
padding: 17px 0 22px;
margin-bottom: 23px;
border-bottom: 1px solid var(--color-line);
}
.script-body {
white-space: pre-wrap;
@@ -582,7 +576,6 @@
.script-notes {
margin-top: 36px;
padding-top: 20px;
border-top: 1px solid var(--color-line);
font-size: 12px;
}
.script-notes dt {
@@ -614,7 +607,7 @@
overflow-wrap: anywhere;
padding: 16px;
background: var(--app-subtle);
border-radius: 4px;
border-radius: 0;
font:
11px/1.9 ui-monospace,
monospace;
@@ -629,34 +622,32 @@
.group-preview {
margin-top: 24px;
border-top: 1px solid var(--color-line);
padding-top: 20px;
}
.group-chip {
display: flex;
align-items: center;
gap: 10px;
border: 1px solid var(--app-subtle);
background: var(--app-surface);
border-radius: 4px;
background: var(--app-subtle);
border-radius: 0;
padding: 8px 11px;
font-size: 11px;
}
.recovery-strip {
background: var(--app-subtle);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
align-items: center;
gap: 15px;
padding: 17px;
border: 1px dashed var(--app-border);
border-radius: 5px;
border-radius: 0;
}
.subject-ref {
display: inline-flex;
background: var(--app-subtle);
padding: 3px 6px;
border-radius: 3px;
border-radius: 0;
color: var(--app-ink);
font-size: 10px;
}
@@ -677,7 +668,6 @@
.beat-section {
margin-top: 25px;
padding-top: 23px;
border-top: 1px solid var(--color-line);
}
.beat-heading {
display: flex;
@@ -694,9 +684,8 @@
display: flex;
gap: 17px;
padding: 17px;
border: 1px solid var(--app-subtle);
border-radius: 5px;
background: var(--app-surface);
border-radius: 0;
background: var(--app-subtle);
}
.shot-label {
flex-shrink: 0;
@@ -737,7 +726,6 @@
}
.history-panel {
border-left: 0;
border-top: 1px solid var(--color-line);
}
.breakdown-config {
@@ -760,7 +748,6 @@
.shot-list {
max-height: 240px;
border-right: 0;
border-bottom: 1px solid var(--color-line);
}
.page-container {