style: 优化按钮禁用态与主题入口并将设置移至侧栏底部

This commit is contained in:
GouJ
2026-09-01 21:58:32 +08:00
parent 03bf87ca9a
commit a24326b85d
11 changed files with 278 additions and 71 deletions
+2 -1
View File
@@ -99,7 +99,8 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S
- 分镜设计与镜头生产共用两级镜头目录:分组头展示 `BEAT 01` 和镜头数,组内仅显示“镜头 01”、标题及状态。分组头在本组内吸顶;窄屏横向模式才在条目上补回 Beat 归属。点击仍使用正式 Shot ID,不会混淆不同 Beat 的同号镜头。 - 分镜设计与镜头生产共用两级镜头目录:分组头展示 `BEAT 01` 和镜头数,组内仅显示“镜头 01”、标题及状态。分组头在本组内吸顶;窄屏横向模式才在条目上补回 Beat 归属。点击仍使用正式 Shot ID,不会混淆不同 Beat 的同号镜头。
- 身份、分镜、生产与图库的批量配置、说明和诊断统一放入 `WorkspaceTools`Naive UI Drawer),按需覆盖当前工作区,不挤压列表。面板支持 Esc 或关闭按钮;关闭不会取消任务或清除配置。历史回执可从入口查看,新回执主动打开诊断;查询错误始终在外层可见。 - 身份、分镜、生产与图库的批量配置、说明和诊断统一放入 `WorkspaceTools`Naive UI Drawer),按需覆盖当前工作区,不挤压列表。面板支持 Esc 或关闭按钮;关闭不会取消任务或清除配置。历史回执可从入口查看,新回执主动打开诊断;查询错误始终在外层可见。
- 生产步骤的操作按钮保留上下间距;`ConfirmAction` 用稳定根元素承接外部间距,避免多根弹窗组件吞掉 `class` - 生产步骤的操作按钮保留上下间距;`ConfirmAction` 用稳定根元素承接外部间距,避免多根弹窗组件吞掉 `class`
- 右上角提供浅色、暗黑跟随系统三种模式,默认跟随系统,选择保存在本机浏览器存储被禁用时仍可在当前会话切换。 - 右上角主题入口以太阳/月亮/显示器图标显示当前偏好,点击选择浅色、暗黑跟随系统默认跟随系统,选择保存在本机浏览器存储被禁用时仍可在当前会话切换。后端连接设置固定在左侧导航底部,折叠时仅显示图标,导航菜单独立滚动。
- 实心绿色按钮采用白色文字,正常/悬停/按下底色分别调整以保证可读性;禁用按钮为灰底灰字,保留原生 disabled 行为。分镜与生产页的剧集选择器不重复显示“剧集”前缀,保留可访问名称。
- 采用微信风格配色:浅色为浅灰背景与白色内容,暗色为近黑背景与深灰内容;绿色用于主操作、选中态和进度。按钮文字、目录文字与主绿色分别配置以保证对比度,错误/警告仍使用独立语义色。Naive UI 和业务内容共用主题,浏览器主题色随之切换;加载前应用偏好,减少主题闪烁。 - 采用微信风格配色:浅色为浅灰背景与白色内容,暗色为近黑背景与深灰内容;绿色用于主操作、选中态和进度。按钮文字、目录文字与主绿色分别配置以保证对比度,错误/警告仍使用独立语义色。Naive UI 和业务内容共用主题,浏览器主题色随之切换;加载前应用偏好,减少主题闪烁。
- 全站采用低边框、直角样式:面板、提示框、标签、图片卡片和表单用背景层次代替静态描边;表格与长记录列表使用交替底色,抽屉用灰底承托内容。保留输入框聚焦/校验反馈、复选框、选中指示和必要的浮层阴影;开关、加载等功能图形不强制改形状。 - 全站采用低边框、直角样式:面板、提示框、标签、图片卡片和表单用背景层次代替静态描边;表格与长记录列表使用交替底色,抽屉用灰底承托内容。保留输入框聚焦/校验反馈、复选框、选中指示和必要的浮层阴影;开关、加载等功能图形不强制改形状。
- 窄屏默认收起侧栏,目录与正文改用紧凑布局;执行记录仍可查看。长弹窗在自身内部滚动。 - 窄屏默认收起侧栏,目录与正文改用紧凑布局;执行记录仍可查看。长弹窗在自身内部滚动。
+24 -22
View File
@@ -9,7 +9,7 @@ import {
NLayoutHeader, NLayoutHeader,
NLayoutSider, NLayoutSider,
NMenu, NMenu,
NSelect, NScrollbar,
NTooltip, NTooltip,
zhCN, zhCN,
dateZhCN, dateZhCN,
@@ -30,6 +30,7 @@ import {
} from '@lucide/vue' } from '@lucide/vue'
import { AppDialog } from './components/ui' import { AppDialog } from './components/ui'
import { useTheme } from './composables/useTheme' import { useTheme } from './composables/useTheme'
import ThemeToggle from './components/ui/ThemeToggle.vue'
/** 应用外壳固定在视口内,业务数据仍由各工作区自行加载。 */ /** 应用外壳固定在视口内,业务数据仍由各工作区自行加载。 */
const route = useRoute() const route = useRoute()
@@ -38,11 +39,6 @@ const settingsOpen = ref(false)
const { preference, theme, overrides } = useTheme() const { preference, theme, overrides } = useTheme()
const projectId = computed(() => String(route.params.projectId || '')) const projectId = computed(() => String(route.params.projectId || ''))
const apiBase = import.meta.env.VITE_API_BASE_URL || '/api' const apiBase = import.meta.env.VITE_API_BASE_URL || '/api'
const themeOptions = [
{ label: '浅色模式', value: 'light' },
{ label: '暗黑模式', value: 'dark' },
{ label: '跟随系统', value: 'system' }
]
const workflowItems = [ const workflowItems = [
['create-drama', '剧本创作', FileText], ['create-drama', '剧本创作', FileText],
['breakdown', '剧本拆解', Layers], ['breakdown', '剧本拆解', Layers],
@@ -90,12 +86,14 @@ const menuOptions = computed<MenuOption[]>(() => [
:collapsed-width="64" :collapsed-width="64"
:native-scrollbar="false" :native-scrollbar="false"
class="admin-sider" class="admin-sider"
content-class="admin-sider-content"
> >
<RouterLink to="/projects" class="admin-brand" aria-label="短剧工作台首页"> <RouterLink to="/projects" class="admin-brand" aria-label="短剧工作台首页">
<Clapperboard :size="24" /><span v-if="!collapsed" <Clapperboard :size="24" /><span v-if="!collapsed"
>短剧工作台<small>SHORT DRAMA STUDIO</small></span >短剧工作台<small>SHORT DRAMA STUDIO</small></span
> >
</RouterLink> </RouterLink>
<NScrollbar class="admin-nav-scroll">
<NMenu <NMenu
:value="route.path" :value="route.path"
:options="menuOptions" :options="menuOptions"
@@ -103,6 +101,25 @@ const menuOptions = computed<MenuOption[]>(() => [
:collapsed-width="64" :collapsed-width="64"
:collapsed-icon-size="19" :collapsed-icon-size="19"
/> />
</NScrollbar>
<footer class="admin-sider-footer">
<NTooltip placement="right" :disabled="!collapsed">
<template #trigger>
<NButton
quaternary
block
class="admin-settings-button"
:class="{ 'is-collapsed': collapsed }"
aria-label="后端连接"
@click="settingsOpen = true"
>
<template #icon><Settings2 :size="18" /></template>
<span v-if="!collapsed">后端连接</span>
</NButton>
</template>
后端连接
</NTooltip>
</footer>
</NLayoutSider> </NLayoutSider>
<NLayout class="admin-main"> <NLayout class="admin-main">
<NLayoutHeader class="admin-topbar"> <NLayoutHeader class="admin-topbar">
@@ -117,22 +134,7 @@ const menuOptions = computed<MenuOption[]>(() => [
</NButton> </NButton>
<span class="truncate text-sm">{{ route.meta.title || '工作空间' }}</span> <span class="truncate text-sm">{{ route.meta.title || '工作空间' }}</span>
</div> </div>
<div class="flex items-center gap-3"> <ThemeToggle v-model="preference" />
<NSelect
v-model:value="preference"
:options="themeOptions"
:virtual-scroll="false"
size="small"
class="theme-select"
aria-label="主题模式"
/>
<NTooltip
><template #trigger
><NButton quaternary circle aria-label="后端连接" @click="settingsOpen = true"
><Settings2 :size="18" /></NButton></template
>后端连接</NTooltip
>
</div>
</NLayoutHeader> </NLayoutHeader>
<main id="main-content" tabindex="-1" class="admin-content"><RouterView /></main> <main id="main-content" tabindex="-1" class="admin-content"><RouterView /></main>
</NLayout> </NLayout>
+46 -10
View File
@@ -12,7 +12,12 @@
--app-accent: #07c160; --app-accent: #07c160;
--app-accent-hover: #2dcb79; --app-accent-hover: #2dcb79;
--app-accent-text: #087c42; --app-accent-text: #087c42;
--app-on-accent: #082b17; --app-on-accent: #ffffff;
--app-button-primary: #078640;
--app-button-hover: #087c42;
--app-button-pressed: #066b36;
--app-disabled-bg: #e4e4e4;
--app-disabled-text: #8c8c8c;
--app-selected: #e4f4e9; --app-selected: #e4f4e9;
--app-shadow: #00000018; --app-shadow: #00000018;
--app-danger: #a93232; --app-danger: #a93232;
@@ -26,6 +31,8 @@
--app-subtle: #232323; --app-subtle: #232323;
--app-control: #2b2b2b; --app-control: #2b2b2b;
--app-control-hover: #353535; --app-control-hover: #353535;
--app-disabled-bg: #303030;
--app-disabled-text: #808080;
--app-border: #333333; --app-border: #333333;
--app-inverse: #111111; --app-inverse: #111111;
--app-accent-text: #5cd693; --app-accent-text: #5cd693;
@@ -65,6 +72,34 @@ body {
.admin-sider { .admin-sider {
height: 100%; height: 100%;
} }
/* 外壳固定品牌与底部设置;只有菜单自身使用 NScrollbar 滚动。 */
.admin-sider-content {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
overflow: hidden;
}
.admin-nav-scroll.n-scrollbar {
flex: 1;
min-height: 0;
}
.admin-sider-footer {
flex-shrink: 0;
padding-block: 8px max(8px, env(safe-area-inset-bottom));
}
.n-button.admin-settings-button {
justify-content: flex-start;
height: 44px;
padding-inline: 20px;
}
.n-button.admin-settings-button.is-collapsed {
justify-content: center;
padding-inline: 0;
}
.admin-settings-button.is-collapsed .n-button__icon {
margin: 0;
}
/* Naive 菜单背景默认左右内缩 8px;只铺满导航行,不改图标与文字缩进。 */ /* Naive 菜单背景默认左右内缩 8px;只铺满导航行,不改图标与文字缩进。 */
.admin-sider .n-menu .n-menu-item-content::before { .admin-sider .n-menu .n-menu-item-content::before {
left: 0; left: 0;
@@ -73,6 +108,7 @@ body {
} }
.admin-brand { .admin-brand {
display: flex; display: flex;
flex-shrink: 0;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
height: 74px; height: 74px;
@@ -108,6 +144,10 @@ body {
.n-button { .n-button {
border-radius: 0; border-radius: 0;
} }
.n-button:focus-visible {
outline: 2px solid var(--app-accent-text);
outline-offset: 2px;
}
.admin-sider .n-menu-divider { .admin-sider .n-menu-divider {
height: 12px; height: 12px;
background: transparent; background: transparent;
@@ -119,8 +159,10 @@ body {
.workspace-tools-drawer .n-drawer-body { .workspace-tools-drawer .n-drawer-body {
background: var(--app-body); background: var(--app-body);
} }
.theme-select { .n-button.theme-toggle {
width: 120px; flex-shrink: 0;
width: 34px;
height: 34px;
} }
.admin-content { .admin-content {
flex: 1; flex: 1;
@@ -213,10 +255,6 @@ body {
min-width: 180px; min-width: 180px;
font-size: 12px; font-size: 12px;
} }
.toolbar-episode > span {
flex-shrink: 0;
color: var(--app-muted);
}
.toolbar-episode .n-select { .toolbar-episode .n-select {
min-width: 0; min-width: 0;
} }
@@ -1160,9 +1198,6 @@ body {
.production-video { .production-video {
background: #080808; background: #080808;
} }
.button-primary {
color: var(--app-on-accent);
}
.skip-link:focus { .skip-link:focus {
z-index: 3000; z-index: 3000;
} }
@@ -1185,6 +1220,7 @@ body {
position: absolute; position: absolute;
top: 44px; top: 44px;
bottom: 0; bottom: 0;
height: auto;
z-index: 20; z-index: 20;
box-shadow: 6px 0 24px var(--app-shadow); box-shadow: 6px 0 24px var(--app-shadow);
} }
+42
View File
@@ -0,0 +1,42 @@
<script setup lang="ts">
import { computed, h, ref } from 'vue'
import { NButton, NDropdown, NTooltip } from 'naive-ui'
import { Monitor, Moon, Sun } from '@lucide/vue'
import type { ThemePreference } from '../../composables/useTheme'
/** 图标表示用户偏好;保留系统跟随选项,不以简单二态切换覆盖偏好。 */
const preference = defineModel<ThemePreference>({ required: true })
const open = ref(false)
const modes = [
{ key: 'light', label: '浅色模式', icon: Sun },
{ key: 'dark', label: '暗黑模式', icon: Moon },
{ key: 'system', label: '跟随系统', icon: Monitor }
] as const
const current = computed(() => modes.find(mode => mode.key === preference.value) ?? modes[2])
const options = modes.map(mode => ({ ...mode, icon: () => h(mode.icon, { size: 16 }) }))
/** Dropdown 的键类型较宽,只接收受支持的主题值。 */
function selectTheme(key: string | number) {
if (key === 'light' || key === 'dark' || key === 'system') preference.value = key
}
</script>
<template>
<NDropdown v-model:show="open" trigger="click" :value="preference" :options="options" @select="selectTheme">
<NTooltip :disabled="open">
<template #trigger>
<NButton
quaternary
circle
class="theme-toggle"
:aria-label="`主题模式:${current.label}`"
aria-haspopup="menu"
:aria-expanded="open"
>
<component :is="current.icon" :size="18" aria-hidden="true" />
</NButton>
</template>
{{ current.label }} · 切换主题
</NTooltip>
</NDropdown>
</template>
+42
View File
@@ -16,6 +16,48 @@ afterEach(() => {
}) })
describe('全站平面主题', () => { describe('全站平面主题', () => {
it.each([false, true])('模式 %s 的绿色按钮使用白字,禁用后灰底灰字且不可提交', dark => {
localStorage.setItem('drama-studio-theme', dark ? 'dark' : 'light')
vi.stubGlobal('matchMedia', () => ({ matches: dark, addEventListener() {}, removeEventListener() {} }))
const click = vi.fn<() => void>()
wrapper = mount({
setup() {
const { theme, overrides } = useTheme()
return () =>
h(NConfigProvider, { theme: theme.value, themeOverrides: overrides.value }, () =>
(['primary', 'success'] as const).flatMap(type => [
h(NButton, { type, class: `${type}-button`, onClick: click }, () => '提交'),
h(
NButton,
{ type, disabled: true, class: `${type}-disabled`, onClick: click },
() => '提交'
)
])
)
}
})
for (const type of ['primary', 'success']) {
const active = wrapper.get<HTMLButtonElement>(`.${type}-button`).element
const disabled = wrapper.get<HTMLButtonElement>(`.${type}-disabled`).element
for (const property of [
'--n-text-color',
'--n-text-color-hover',
'--n-text-color-pressed',
'--n-text-color-focus'
]) {
expect(active.style.getPropertyValue(property)).toBe('#ffffff')
}
expect(active.style.getPropertyValue('--n-color')).toBe('#078640')
expect(disabled.style.getPropertyValue('--n-color-disabled')).toBe(dark ? '#303030' : '#e4e4e4')
expect(disabled.style.getPropertyValue('--n-text-color-disabled')).toBe(dark ? '#808080' : '#8c8c8c')
expect(disabled.disabled).toBe(true)
disabled.click()
expect(click).not.toHaveBeenCalled()
}
wrapper.get<HTMLButtonElement>('.primary-button').element.click()
expect(click).toHaveBeenCalledOnce()
})
it.each([false, true])('模式 %s 的真实控件无静态描边、使用直角,并保留聚焦与错误反馈', dark => { it.each([false, true])('模式 %s 的真实控件无静态描边、使用直角,并保留聚焦与错误反馈', dark => {
localStorage.setItem('drama-studio-theme', dark ? 'dark' : 'light') localStorage.setItem('drama-studio-theme', dark ? 'dark' : 'light')
vi.stubGlobal('matchMedia', () => ({ matches: dark, addEventListener() {}, removeEventListener() {} })) vi.stubGlobal('matchMedia', () => ({ matches: dark, addEventListener() {}, removeEventListener() {} }))
+46 -9
View File
@@ -92,7 +92,7 @@ describe('管理后台组件边界', () => {
expect(wrapper.get('.workspace-split').findAll(':scope > *')).toHaveLength(2) expect(wrapper.get('.workspace-split').findAll(':scope > *')).toHaveLength(2)
}) })
it('真实主题选择器切换暗色,侧栏保持全部七个工作流入口', async () => { it('图标主题菜单保留三种模式,设置入口位于侧栏底部并支持折叠', async () => {
const router = createRouter({ const router = createRouter({
history: createMemoryHistory(), history: createMemoryHistory(),
routes: [ routes: [
@@ -108,19 +108,56 @@ describe('管理后台组件边界', () => {
await flushPromises() await flushPromises()
expect(wrapper.findAll('.n-menu a')).toHaveLength(8) expect(wrapper.findAll('.n-menu a')).toHaveLength(8)
expect(wrapper.get('#main-content .workspace-page').text()).toBe('镜头详情') expect(wrapper.get('#main-content .workspace-page').text()).toBe('镜头详情')
await wrapper.get('.theme-select .n-base-selection').trigger('click') expect(wrapper.get('.theme-toggle').text()).toBe('')
expect(wrapper.get('.theme-toggle').attributes('aria-haspopup')).toBe('menu')
expect(wrapper.get('.theme-toggle svg').classes()).toContain('lucide-monitor')
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
for (const [key, label, icon] of [
['dark', '暗黑模式', 'moon'],
['light', '浅色模式', 'sun'],
['system', '跟随系统', 'monitor']
] as const) {
await wrapper.get('.theme-toggle').trigger('click')
await flushPromises() await flushPromises()
const darkOption = [...document.querySelectorAll<HTMLElement>('.n-base-select-option')].find( const option = [...document.querySelectorAll<HTMLElement>('.n-dropdown-option-body')].find(
item => item.textContent?.trim() === '暗黑模式' item => item.textContent?.trim() === label
) )
expect(darkOption).toBeDefined() expect(option).toBeDefined()
darkOption!.click() option!.click()
await flushPromises() await flushPromises()
expect(document.documentElement.dataset.theme).toBe('dark') expect(localStorage.getItem('drama-studio-theme')).toBe(key)
expect(wrapper.getComponent(NConfigProvider).props('theme')?.name).toBe('dark') expect(wrapper.get('.theme-toggle svg').classes()).toContain(`lucide-${icon}`)
expect(localStorage.getItem('drama-studio-theme')).toBe('dark') expect(wrapper.get('.theme-toggle').attributes('aria-label')).toBe(`主题模式:${label}`)
const expectedTheme = key === 'system' ? systemTheme : key
expect(document.documentElement.dataset.theme).toBe(expectedTheme)
expect(wrapper.getComponent(NConfigProvider).props('theme')?.name ?? 'light').toBe(expectedTheme)
}
expect(wrapper.find('.admin-topbar [aria-label="后端连接"]').exists()).toBe(false)
expect(wrapper.find('.admin-nav-scroll [aria-label="后端连接"]').exists()).toBe(false)
expect(wrapper.get('.admin-sider-footer [aria-label="后端连接"]').text()).toBe('后端连接')
await wrapper.get('[aria-label="折叠侧栏"]').trigger('click') await wrapper.get('[aria-label="折叠侧栏"]').trigger('click')
expect(wrapper.find('[aria-label="展开侧栏"]').exists()).toBe(true) expect(wrapper.find('[aria-label="展开侧栏"]').exists()).toBe(true)
const settings = wrapper.get('.admin-sider-footer [aria-label="后端连接"]')
expect(settings.text()).toBe('')
await settings.trigger('click')
await flushPromises()
expect(document.querySelector('[role="dialog"][aria-label="后端连接"]')).not.toBeNull()
expect(wrapper.findAll('.admin-nav-scroll .n-menu a')).toHaveLength(8)
})
it('侧栏菜单独立滚动,页脚不参与滚动且窄屏不超出视口底部', () => {
const css = readFileSync('src/admin.css', 'utf8')
expect(css).toMatch(
/\.admin-sider-content\s*\{[^}]*height:\s*100%;[^}]*min-height:\s*0;[^}]*overflow:\s*hidden/
)
expect(css).toMatch(/\.admin-nav-scroll\.n-scrollbar\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0/)
expect(css).toMatch(/\.admin-sider-footer\s*\{[^}]*flex-shrink:\s*0/)
expect(css).toMatch(/\.admin-sider:not\([^)]*\)\s*\{[^}]*bottom:\s*0;[^}]*height:\s*auto/)
for (const path of ['storyboard/StoryboardPage.vue', 'production/ProductionPage.vue']) {
const source = readFileSync(`src/features/${path}`, 'utf8')
expect(source).not.toContain('<span>剧集</span')
expect(source).toMatch(/aria-label="选择(?:分镜|生产)剧集"/)
}
}) })
it('长弹窗使用内部滚动,提交期间禁止遮罩和 Esc 关闭', async () => { it('长弹窗使用内部滚动,提交期间禁止遮罩和 Esc 关闭', async () => {
+8 -3
View File
@@ -83,7 +83,7 @@ describe('微信风格明暗主题', () => {
expect(state.theme.value?.name).toBe('dark') expect(state.theme.value?.name).toBe('dark')
expect(state.overrides.value.common?.primaryColor).toBe('#07c160') expect(state.overrides.value.common?.primaryColor).toBe('#07c160')
expect(state.overrides.value.common?.bodyColor).toBe('#111111') expect(state.overrides.value.common?.bodyColor).toBe('#111111')
expect(state.overrides.value.Button?.textColorPrimary).toBe('#082b17') expect(state.overrides.value.Button?.textColorPrimary).toBe('#ffffff')
expect(document.documentElement.dataset.theme).toBe('dark') expect(document.documentElement.dataset.theme).toBe('dark')
}) })
@@ -127,8 +127,13 @@ describe('微信风格明暗主题', () => {
expect(Button?.colorHover).toBe(tokens['control-hover']) expect(Button?.colorHover).toBe(tokens['control-hover'])
expect(Select?.peers?.InternalSelection?.color).toBe(tokens.control) expect(Select?.peers?.InternalSelection?.color).toBe(tokens.control)
expect(Button?.textColorPrimary).toBe(tokens['on-accent']) expect(Button?.textColorPrimary).toBe(tokens['on-accent'])
for (const background of [common!.primaryColor!, common!.primaryColorHover!, common!.primaryColorPressed!]) { expect(Button?.colorPrimary).toBe(tokens['button-primary'])
expect(contrast(String(Button!.textColorPrimary), background)).toBeGreaterThanOrEqual(4.5) expect(Button?.colorHoverPrimary).toBe(tokens['button-hover'])
expect(Button?.colorPressedPrimary).toBe(tokens['button-pressed'])
expect(Button?.colorDisabledPrimary).toBe(tokens['disabled-bg'])
expect(Button?.textColorDisabledPrimary).toBe(tokens['disabled-text'])
for (const background of [Button!.colorPrimary!, Button!.colorHoverPrimary!, Button!.colorPressedPrimary!]) {
expect(contrast(String(Button!.textColorPrimary), String(background))).toBeGreaterThanOrEqual(4.5)
} }
expect(contrast(tokens.ink!, tokens.surface!)).toBeGreaterThanOrEqual(4.5) expect(contrast(tokens.ink!, tokens.surface!)).toBeGreaterThanOrEqual(4.5)
expect(contrast(tokens.muted!, tokens.subtle!)).toBeGreaterThanOrEqual(4.5) expect(contrast(tokens.muted!, tokens.subtle!)).toBeGreaterThanOrEqual(4.5)
+40 -4
View File
@@ -28,7 +28,13 @@ export function useTheme() {
const primary = '#07c160' const primary = '#07c160'
const hover = '#2dcb79' const hover = '#2dcb79'
const pressed = '#06ad56' const pressed = '#06ad56'
const onPrimary = '#082b17' // 实心绿按钮使用白字;按钮底色独立于品牌绿,兼顾正常、悬停和按下态的可读性。
const onPrimary = '#ffffff'
const buttonPrimary = '#078640'
const buttonHover = '#087c42'
const buttonPressed = '#066b36'
const disabledColor = dark ? '#303030' : '#e4e4e4'
const disabledText = dark ? '#808080' : '#8c8c8c'
const accentText = dark ? '#5cd693' : '#087c42' const accentText = dark ? '#5cd693' : '#087c42'
const body = dark ? '#111111' : '#ededed' const body = dark ? '#111111' : '#ededed'
const surface = dark ? '#191919' : '#ffffff' const surface = dark ? '#191919' : '#ffffff'
@@ -102,13 +108,43 @@ export function useTheme() {
borderPressed: 'none', borderPressed: 'none',
borderDisabled: 'none', borderDisabled: 'none',
borderFocus: focusBorder, borderFocus: focusBorder,
opacityDisabled: '1',
colorDisabled: disabledColor,
textColorDisabled: disabledText,
colorPrimary: buttonPrimary,
colorHoverPrimary: buttonHover,
colorPressedPrimary: buttonPressed,
colorFocusPrimary: buttonHover,
colorDisabledPrimary: disabledColor,
borderPrimary: 'none',
borderHoverPrimary: 'none',
borderPressedPrimary: 'none',
borderFocusPrimary: 'none',
borderDisabledPrimary: 'none',
textColorPrimary: onPrimary, textColorPrimary: onPrimary,
textColorHoverPrimary: onPrimary, textColorHoverPrimary: onPrimary,
textColorPressedPrimary: onPrimary, textColorPressedPrimary: onPrimary,
textColorFocusPrimary: onPrimary, textColorFocusPrimary: onPrimary,
textColorDisabledPrimary: onPrimary, textColorDisabledPrimary: disabledText,
textColorTextPrimary: accentText, textColorTextPrimary: accentText,
textColorGhostPrimary: accentText textColorGhostPrimary: accentText,
colorSuccess: buttonPrimary,
colorHoverSuccess: buttonHover,
colorPressedSuccess: buttonPressed,
colorFocusSuccess: buttonHover,
colorDisabledSuccess: disabledColor,
borderSuccess: 'none',
borderHoverSuccess: 'none',
borderPressedSuccess: 'none',
borderFocusSuccess: 'none',
borderDisabledSuccess: 'none',
textColorSuccess: onPrimary,
textColorHoverSuccess: onPrimary,
textColorPressedSuccess: onPrimary,
textColorFocusSuccess: onPrimary,
textColorDisabledSuccess: disabledText,
textColorTextSuccess: accentText,
textColorGhostSuccess: accentText
}, },
Input: { Input: {
color: control, color: control,
@@ -190,7 +226,7 @@ export function useTheme() {
borderWarning: 'none', borderWarning: 'none',
borderError: 'none' borderError: 'none'
}, },
Checkbox: { checkMarkColor: onPrimary }, Checkbox: { checkMarkColor: '#082b17' },
Switch: { railColorActive: primary }, Switch: { railColorActive: primary },
Progress: { fillColor: primary } Progress: { fillColor: primary }
} satisfies GlobalThemeOverrides } satisfies GlobalThemeOverrides
+4 -5
View File
@@ -123,9 +123,8 @@ watch(
<WorkspacePage split compact <WorkspacePage split compact
><template #header ><template #header
><div class="workspace-toolbar"> ><div class="workspace-toolbar">
<label class="toolbar-episode" <div class="toolbar-episode">
><span>剧集</span <NSelect
><NSelect
:value="episodeNo" :value="episodeNo"
aria-label="选择生产剧集" aria-label="选择生产剧集"
@update:value="selectedEpisode = Number($event)" @update:value="selectedEpisode = Number($event)"
@@ -137,8 +136,8 @@ watch(
})) }))
]" ]"
class="select-control" class="select-control"
></NSelect ></NSelect>
></label> </div>
<span class="toolbar-summary" <span class="toolbar-summary"
>本集 {{ shots.length }} <span v-if="videoRunning"> >本集 {{ shots.length }} <span v-if="videoRunning">
· {{ videoRunning }} 个视频任务进行中</span · {{ videoRunning }} 个视频任务进行中</span
+4 -5
View File
@@ -100,9 +100,8 @@ watch(
<WorkspacePage split compact class="storyboard-workspace-page" <WorkspacePage split compact class="storyboard-workspace-page"
><template #header ><template #header
><div class="workspace-toolbar"> ><div class="workspace-toolbar">
<label class="toolbar-episode" <div class="toolbar-episode">
><span>剧集</span <NSelect
><NSelect
:value="episodeNo" :value="episodeNo"
aria-label="选择分镜剧集" aria-label="选择分镜剧集"
@update:value="selectedEpisode = Number($event)" @update:value="selectedEpisode = Number($event)"
@@ -114,8 +113,8 @@ watch(
})) }))
]" ]"
class="select-control" class="select-control"
></NSelect ></NSelect>
></label> </div>
<div class="toolbar-metrics"> <div class="toolbar-metrics">
<span v-for="item in coverage" :key="item.title" <span v-for="item in coverage" :key="item.title"
>{{ item.title }} {{ item.count }} / {{ item.total }}</span >{{ item.title }} {{ item.count }} / {{ item.total }}</span
+10 -2
View File
@@ -152,11 +152,19 @@
border-color 0.15s; border-color 0.15s;
} }
.button-primary { .button-primary {
background: var(--app-accent); background: var(--app-button-primary);
color: var(--app-on-accent); color: var(--app-on-accent);
} }
.button-primary:hover:not(:disabled) { .button-primary:hover:not(:disabled) {
background: var(--app-accent-hover); background: var(--app-button-hover);
}
.button-primary:active:not(:disabled) {
background: var(--app-button-pressed);
}
.button:disabled {
background: var(--app-disabled-bg);
color: var(--app-disabled-text);
opacity: 1;
} }
.button-secondary { .button-secondary {
background: var(--app-control); background: var(--app-control);