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
+76 -85
View File
@@ -1,16 +1,8 @@
<script setup lang="ts">
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
import { NAlert, NButton, NCheckbox, NInputNumber, NProgress, NTab, NTable, NTabs, NTag } from 'naive-ui'
import { computed, onScopeDispose, ref, watch } from 'vue'
import {
TabsRoot,
TabsList,
TabsTrigger,
TabsContent,
CheckboxRoot,
CheckboxIndicator,
ProgressRoot,
ProgressIndicator
} from 'reka-ui'
import { Check, Download, Layers, LoaderCircle, ArrowRight } from '@lucide/vue'
import { Download, Layers, LoaderCircle, ArrowRight } from '@lucide/vue'
import { EmptyState, StatusBadge } from '../../components/ui'
import { useProjectContext } from '../projects/context'
import { breakdownApi } from './api'
@@ -88,7 +80,7 @@ watch(
)
onScopeDispose(invalidatePreview)
/** Reka Checkbox 的不确定值不视为选择。 */
/** 只接受明确勾选状态,不把不确定值作为模块选择。 */
function toggleModule(module: BreakdownModule, checked: boolean | 'indeterminate') {
if (checked === true && !modules.value.includes(module)) modules.value.push(module)
else modules.value = modules.value.filter(item => item !== module)
@@ -140,7 +132,11 @@ function exportResult() {
</script>
<template>
<div class="mt-6">
<WorkspacePage
><template #header
><h2 class="text-lg font-semibold">剧本拆解</h2>
<p class="text-xs text-muted">分组抽取主体整理与分镜结果</p></template
>
<div v-if="!project?.episodes.length" class="panel">
<EmptyState
title="还没有可拆解的剧集"
@@ -159,54 +155,53 @@ function exportResult() {
{{ project.episodes.length }} 集剧本每个分组分别抽取所选模块再生成主体与分镜
</p>
</div>
<span class="tag">来源正式剧集</span>
<NTag size="small" :bordered="false">来源正式剧集</NTag>
</div>
<div class="breakdown-config">
<div>
<label class="field-label" for="group-size">每组集数</label>
<div class="flex items-center gap-3">
<input
id="group-size"
v-model.number="groupSize"
type="number"
class="input w-24"
min="1"
step="1"
<NInputNumber
:disabled="operation.pending"
/><span class="text-sm text-muted"> / </span>
:input-props="{ id: 'group-size' }"
class="w-24"
:value="typeof groupSize === 'number' ? groupSize : null"
@update:value="groupSize = $event ?? 0"
:min="1"
:step="1"
></NInputNumber
><span class="text-sm text-muted"> / </span>
</div>
</div>
<fieldset class="flex flex-wrap gap-5 border-0 p-0">
<legend class="field-label mb-3">抽取模块</legend>
<label
<NCheckbox
:checked="modules.includes(option.value)"
:disabled="operation.pending"
:aria-label="option.label"
@update:checked="toggleModule(option.value, $event)"
v-for="option in moduleOptions"
:key="option.value"
class="flex cursor-pointer items-start gap-2.5"
><CheckboxRoot
:model-value="modules.includes(option.value)"
:disabled="operation.pending"
class="checkbox mt-0.5"
:aria-label="option.label"
@update:model-value="toggleModule(option.value, $event)"
><CheckboxIndicator><Check :size="12" /></CheckboxIndicator></CheckboxRoot
><span
><span class="block text-sm font-medium">{{ option.label }}</span
><span class="mt-1 block text-[11px] text-muted">{{ option.description }}</span></span
></label
></NCheckbox
>
</fieldset>
<button
class="button button-secondary self-end"
<NButton
:disabled="!configValid || previewBusy || operation.pending || !!error"
@click="loadPreview"
>
<LoaderCircle v-if="previewBusy" :size="14" class="animate-spin" />预览分组
</button>
class="self-end"
><LoaderCircle v-if="previewBusy" :size="14" class="animate-spin" />预览分组
</NButton>
</div>
<p v-if="!configValid" class="mt-4 text-xs text-danger">
每组集数必须是正整数并至少选择一个抽取模块
</p>
<p v-if="previewError" class="alert alert-error mt-4" role="alert">{{ previewError }}</p>
<NAlert v-if="previewError" role="alert" type="error" :show-icon="false" class="mt-4">{{
previewError
}}</NAlert>
<div v-if="preview" class="group-preview">
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
<p class="text-sm">
@@ -266,29 +261,28 @@ function exportResult() {
><span>主体 {{ subjects.length }}</span
><span>分镜剧集 {{ shots.length }} / {{ plans.length }}</span>
</div>
<ProgressRoot
<NProgress
v-if="summary?.total"
class="progress-track mt-3"
:model-value="summary.completed"
:max="summary.total"
aria-label="已完成抽取任务"
><ProgressIndicator
class="progress-fill"
:style="{ width: Math.min(100, (summary.completed / summary.total) * 100) + '%' }"
/></ProgressRoot>
<p v-if="execution?.errorMessage" class="alert alert-error mt-4" role="alert">
{{ execution.errorMessage }}
</p>
<div v-if="validation && !validation.valid" class="alert alert-error mt-4">
<p class="font-medium">分镜校验未通过</p>
type="line"
:show-indicator="false"
:height="4"
:percentage="Math.min(100, Math.max(0, (summary.completed / Math.max(1, summary.total)) * 100))"
class="mt-3"
></NProgress>
<NAlert v-if="execution?.errorMessage" role="alert" type="error" :show-icon="false" class="mt-4">{{
execution.errorMessage
}}</NAlert>
<NAlert v-if="validation && !validation.valid" type="error" :show-icon="false" class="mt-4"
><p class="font-medium">分镜校验未通过</p>
<ul class="mt-2 list-inside list-disc space-y-1">
<li v-for="(issue, index) in validation.issues" :key="index">
{{ issue.episodeNo }} <span v-if="issue.beatNo"> / Beat {{ issue.beatNo }}</span
><span v-if="issue.shotNo"> / Shot {{ issue.shotNo }}</span
>{{ issue.message }}
</li>
</ul>
</div>
</ul></NAlert
>
<div v-if="showRecovery" class="recovery-strip mt-4">
<div>
<p class="text-sm font-medium">从中断处继续</p>
@@ -319,42 +313,39 @@ function exportResult() {
</div>
</div>
</section>
<TabsRoot v-model="tab" class="mt-7">
<div class="mt-7">
<div class="flex flex-wrap items-center justify-between gap-3">
<TabsList class="tabs-list" aria-label="拆解结果"
><TabsTrigger
v-for="option in moduleOptions"
:key="option.value"
:value="option.value"
class="tab-trigger"
<NTabs v-model:value="tab" type="line" size="small" aria-label="拆解结果"
><NTab v-for="option in moduleOptions" :key="option.value" :name="option.value"
>{{ option.label
}}<span>{{
subjects.filter(item => item.module === option.value).length
}}</span></TabsTrigger
><TabsTrigger value="storyboard" class="tab-trigger">分镜</TabsTrigger
><TabsTrigger value="tasks" class="tab-trigger">任务明细</TabsTrigger></TabsList
><button class="text-button" :disabled="!snapshot" @click="exportResult">
<Download :size="14" />导出 JSON
</button>
}}<span>{{ subjects.filter(item => item.module === option.value).length }}</span></NTab
><NTab name="storyboard">分镜</NTab><NTab name="tasks">任务明细</NTab></NTabs
><NButton :disabled="!snapshot" @click="exportResult" text size="small"
><Download :size="14" />导出 JSON
</NButton>
</div>
<div class="content-with-history panel mt-4">
<main class="min-w-0">
<TabsContent v-for="option in moduleOptions" :key="option.value" :value="option.value"
><SubjectList
:project-id="project?.id"
:subjects="subjects.filter(item => item.module === option.value)"
:forms="forms" /></TabsContent
><TabsContent value="storyboard"
><div class="mb-4 flex justify-end">
<template v-for="option in moduleOptions" :key="option.value"
><div v-if="tab === option.value">
<SubjectList
:project-id="project?.id"
:subjects="subjects.filter(item => item.module === option.value)"
:forms="forms"
/></div
></template>
<div v-if="tab === 'storyboard'">
<div class="mb-4 flex justify-end">
<RouterLink :to="`/projects/${project?.id}/storyboard`" class="text-button"
>进入分镜设计<ArrowRight :size="14"
/></RouterLink>
</div>
<StoryboardList :plans="plans" :episodes="shots" /></TabsContent
><TabsContent value="tasks" class="p-5"
><div v-if="snapshot?.tasks?.length" class="table-scroll">
<table class="project-table">
<thead>
<StoryboardList :plans="plans" :episodes="shots" />
</div>
<div v-if="tab === 'tasks'" class="p-5">
<div v-if="snapshot?.tasks?.length" class="table-scroll">
<NTable :single-line="false" class="project-table"
><thead>
<tr>
<th>分组</th>
<th>模块</th>
@@ -390,18 +381,18 @@ function exportResult() {
<td colspan="4" class="text-danger">{{ task.errorMessage }}</td>
</tr></template
>
</tbody>
</table>
</tbody></NTable
>
</div>
<EmptyState
v-else
title="尚无抽取任务记录"
description="启动拆解后,任务会在后端保存 checkpoint 时更新。预览中的任务尚未执行。"
/></TabsContent>
/>
</div>
</main>
<HistoryPanel :checkpoints="checkpoints" workflow="breakdown" />
</div>
</TabsRoot>
</template>
</div>
</div> </template
></WorkspacePage>
</template>