refactor: 使用 Naive UI 重构后台布局与黑白双主题

This commit is contained in:
GouJ
2026-09-01 16:31:21 +08:00
parent 55516537bf
commit db8c176b55
51 changed files with 3277 additions and 2442 deletions
+134 -114
View File
@@ -1,6 +1,20 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRoute } from 'vue-router'
import { computed, h, ref } from 'vue'
import { RouterLink, useRoute } from 'vue-router'
import {
NButton,
NConfigProvider,
NGlobalStyle,
NLayout,
NLayoutHeader,
NLayoutSider,
NMenu,
NSelect,
NTooltip,
zhCN,
dateZhCN,
type MenuOption
} from 'naive-ui'
import {
Clapperboard,
Camera,
@@ -14,127 +28,133 @@ import {
PanelLeftOpen,
Settings2
} from '@lucide/vue'
import { DialogTrigger } from 'reka-ui'
import { AppDialog } from './components/ui'
import { useTheme } from './composables/useTheme'
/** 应用框架只承载导航与连接说明,不混入 graph 业务。 */
/** 应用外壳固定在视口内,业务数据仍由各工作区自行加载。 */
const route = useRoute()
const collapsed = ref(false)
const collapsed = ref(window.matchMedia('(max-width: 800px)').matches)
const settingsOpen = ref(false)
const projectId = computed(() => (route.params.projectId ? String(route.params.projectId) : ''))
const { preference, theme, overrides } = useTheme()
const projectId = computed(() => String(route.params.projectId || ''))
const apiBase = import.meta.env.VITE_API_BASE_URL || '/api'
const themeOptions = [
{ label: '浅色模式', value: 'light' },
{ label: '暗黑模式', value: 'dark' },
{ label: '跟随系统', value: 'system' }
]
const workflowItems = [
['create-drama', '剧本创作', FileText],
['breakdown', '剧本拆解', Layers],
['visual-style', '视觉风格', Palette],
['subject-identity', '主体身份', Fingerprint],
['subject-images', '形态图片', Images],
['storyboard', '分镜设计', Camera],
['production', '镜头生产', Clapperboard]
] as const
/** 菜单链接使用 RouterLink,保留新标签打开和浏览器导航行为。 */
const menuOptions = computed<MenuOption[]>(() => [
{
key: '/projects',
label: () => h(RouterLink, { to: '/projects' }, () => '我的剧本'),
icon: () => h(FolderOpen, { size: 18 })
},
...(projectId.value
? [
{ type: 'divider' as const, key: 'divider' },
...workflowItems.map(([path, label, icon]) => ({
key: '/projects/' + projectId.value + '/' + path,
label: () => h(RouterLink, { to: '/projects/' + projectId.value + '/' + path }, () => label),
icon: () => h(icon, { size: 18 })
}))
]
: [])
])
</script>
<template>
<div class="app-shell" :class="{ 'sidebar-collapsed': collapsed }">
<NConfigProvider
:theme="theme"
:theme-overrides="overrides"
:locale="zhCN"
:date-locale="dateZhCN"
class="app-provider"
>
<NGlobalStyle />
<a href="#main-content" class="skip-link">跳到主要内容</a>
<aside class="sidebar">
<RouterLink to="/projects" class="brand"
><span class="brand-mark"><Clapperboard :size="21" :stroke-width="1.5" /></span
><span class="sidebar-label"
><strong>短剧工作台</strong><small>SHORT DRAMA STUDIO</small></span
></RouterLink
<NLayout has-sider class="admin-layout">
<NLayoutSider
bordered
collapse-mode="width"
:collapsed="collapsed"
:width="208"
:collapsed-width="64"
:native-scrollbar="false"
class="admin-sider"
>
<div class="sidebar-section-label sidebar-label">工作空间</div>
<nav aria-label="主导航">
<RouterLink to="/projects" class="side-link" :class="{ selected: !projectId }" title="我的剧本"
><FolderOpen :size="18" /><span class="sidebar-label">我的剧本</span></RouterLink
>
</nav>
<template v-if="projectId"
><div class="sidebar-section-label sidebar-label mt-8">当前项目</div>
<nav aria-label="工作流导航">
<RouterLink
:to="`/projects/${projectId}/create-drama`"
class="side-link"
active-class="selected"
title="剧本创作"
><FileText :size="18" /><span class="sidebar-label">剧本创作</span
><span class="sidebar-label ml-auto text-[10px] text-stone-500">01</span></RouterLink
><RouterLink
:to="`/projects/${projectId}/breakdown`"
class="side-link"
active-class="selected"
title="剧本拆解"
><Layers :size="18" /><span class="sidebar-label">剧本拆解</span
><span class="sidebar-label ml-auto text-[10px] text-stone-500">02</span></RouterLink
><RouterLink
:to="`/projects/${projectId}/visual-style`"
class="side-link"
active-class="selected"
title="视觉风格"
><Palette :size="18" /><span class="sidebar-label">视觉风格</span
><span class="sidebar-label ml-auto text-[10px] text-stone-500">03</span></RouterLink
><RouterLink
:to="`/projects/${projectId}/subject-identity`"
class="side-link"
active-class="selected"
title="主体身份"
><Fingerprint :size="18" /><span class="sidebar-label">主体身份</span
><span class="sidebar-label ml-auto text-[10px] text-stone-500">04</span></RouterLink
><RouterLink
:to="`/projects/${projectId}/subject-images`"
class="side-link"
active-class="selected"
title="形态图片"
><Images :size="18" /><span class="sidebar-label">形态图片</span
><span class="sidebar-label ml-auto text-[10px] text-stone-500">05</span></RouterLink
><RouterLink
:to="`/projects/${projectId}/storyboard`"
class="side-link"
active-class="selected"
title="分镜设计"
><Camera :size="18" /><span class="sidebar-label">分镜设计</span
><span class="sidebar-label ml-auto text-[10px] text-stone-500">06</span></RouterLink
<RouterLink to="/projects" class="admin-brand" aria-label="短剧工作台首页">
<Clapperboard :size="24" /><span v-if="!collapsed"
>短剧工作台<small>SHORT DRAMA STUDIO</small></span
>
</nav></template
>
<div class="sidebar-footer">
<AppDialog
v-model:open="settingsOpen"
title="后端连接"
description="页面直接使用现有后端接口,所有生成操作都会真实提交。"
><template #trigger
><DialogTrigger class="side-link w-full" title="后端连接"
><Settings2 :size="17" /><span class="sidebar-label">连接说明</span></DialogTrigger
></template
>
<dl class="detail-grid mt-7">
<dt>API 地址</dt>
<dd>
<code>{{ apiBase }}</code>
</dd>
<dt>开发环境</dt>
<dd>复制 .env.example .env API_PROXY_TARGET 指向你的后端服务然后重启 Vite</dd>
<dt>生产环境</dt>
<dd> /api /storage 配置反向代理并为生成拆解和恢复接口设置足够长的读取超时</dd>
<dt>状态更新</dt>
<dd>项目详情每 6 秒刷新一次checkpoint 只在节点或批次结束后更新</dd>
</dl></AppDialog
>
<div class="sidebar-divider"></div>
<button
class="side-link w-full"
:aria-label="collapsed ? '展开侧栏' : '折叠侧栏'"
@click="collapsed = !collapsed"
>
<PanelLeftOpen v-if="collapsed" :size="17" /><PanelLeftClose v-else :size="17" /><span
class="sidebar-label"
>收起侧栏</span
>
</button>
<p class="sidebar-label mt-3 px-3 text-[10px] text-stone-500">创作 · 整理 · 逐镜成形</p>
</div>
</aside>
<div class="app-main">
<header class="topbar">
<span class="text-xs text-muted"
>短剧制作 / <span class="text-ink">{{ route.meta.title || '工作空间' }}</span></span
><span class="topbar-note"
>创作工作区 <span class="ml-2 font-mono text-[10px] text-faint">v0.1</span></span
>
</header>
<main id="main-content" tabindex="-1"><RouterView /></main>
</div>
</div>
</RouterLink>
<NMenu
:value="route.path"
:options="menuOptions"
:collapsed="collapsed"
:collapsed-width="64"
:collapsed-icon-size="19"
/>
</NLayoutSider>
<NLayout class="admin-main">
<NLayoutHeader bordered class="admin-topbar">
<div class="flex min-w-0 items-center gap-3">
<NButton
quaternary
circle
:aria-label="collapsed ? '展开侧栏' : '折叠侧栏'"
@click="collapsed = !collapsed"
>
<PanelLeftOpen v-if="collapsed" :size="18" /><PanelLeftClose v-else :size="18" />
</NButton>
<span class="truncate text-sm">{{ route.meta.title || '工作空间' }}</span>
</div>
<div class="flex items-center gap-3">
<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>
<main id="main-content" tabindex="-1" class="admin-content"><RouterView /></main>
</NLayout>
</NLayout>
<AppDialog
v-model:open="settingsOpen"
title="后端连接"
description="页面使用真实后端接口,生成操作会实际提交。"
>
<dl class="detail-grid mt-6">
<dt>API 地址</dt>
<dd>
<code>{{ apiBase }}</code>
</dd>
<dt>开发环境</dt>
<dd> .env 中配置 API_PROXY_TARGET修改后重启 Vite</dd>
<dt>生产环境</dt>
<dd> /api/storage 配置反向代理并为长工作流设置足够的超时</dd>
<dt>状态更新</dt>
<dd>项目详情每 6 秒刷新关闭页面不会取消后端任务</dd>
</dl>
</AppDialog>
</NConfigProvider>
</template>