refactor: 使用 Naive UI 重构后台布局与黑白双主题
This commit is contained in:
+134
-114
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user