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