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
+230 -187
View File
@@ -1,6 +1,18 @@
<script setup lang="ts">
import { computed } from 'vue'
import { TabsRoot, TabsList, TabsTrigger, TabsContent, ProgressRoot, ProgressIndicator } from 'reka-ui'
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
import {
NAlert,
NButton,
NCheckbox,
NCollapse,
NCollapseItem,
NInputNumber,
NProgress,
NSelect,
NTab,
NTabs
} from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { ArrowLeft, Download, RefreshCw } from '@lucide/vue'
import { EmptyState } from '../../components/ui'
import { downloadText } from '../../lib/format'
@@ -10,6 +22,8 @@ import ShotDesign from './components/ShotDesign.vue'
import ShotTools from './components/ShotTools.vue'
import { useStoryboard } from './useStoryboard'
const detailTab = ref('design')
/** 分镜工作区按单集查看正式数据,生成配置与镜头检查分开。 */
const {
id,
@@ -65,167 +79,197 @@ function exportEpisode() {
'application/json'
)
}
/** 回执到达后打开诊断区;平时留出镜头检查空间。 */
const overview = ref<string[]>([])
watch(
() => session.value.receipt,
receipt => {
if (receipt) overview.value = ['generation']
},
{ immediate: true }
)
</script>
<template>
<div class="mt-7">
<div class="flex flex-wrap items-end justify-between gap-4">
<div>
<h2 class="text-lg font-semibold">分镜设计</h2>
<p class="mt-2 text-sm text-muted">从镜头拆解到摄影设计再补齐每一镜的即时状态</p>
<WorkspacePage split class="storyboard-workspace-page"
><template #header
><div class="flex flex-wrap items-end justify-between gap-4">
<div>
<h2 class="text-lg font-semibold">分镜设计</h2>
<p class="mt-2 text-sm text-muted">从镜头拆解到摄影设计再补齐每一镜的即时状态</p>
</div>
<RouterLink :to="`/projects/${id}/breakdown`" class="text-button"
><ArrowLeft :size="14" />回到拆解</RouterLink
>
</div>
<RouterLink :to="`/projects/${id}/breakdown`" class="text-button"
><ArrowLeft :size="14" />回到拆解</RouterLink
>
</div>
<section class="panel mt-5 p-5" aria-label="分镜生成配置">
<div class="storyboard-controls">
<label
><span class="field-label">当前剧集</span
><select
:value="episodeNo"
class="input"
aria-label="选择分镜剧集"
@change="selectedEpisode = Number(($event.target as HTMLSelectElement).value)"
<section class="mt-3" aria-label="分镜生成配置">
<div class="storyboard-controls">
<label
><span class="field-label">当前剧集</span
><NSelect
:value="episodeNo"
aria-label="选择分镜剧集"
@update:value="selectedEpisode = Number($event)"
:options="[
...(!episodeOptions.length ? [{ label: String('暂无剧集'), value: 0 }] : []),
...episodeOptions.map(episode => ({
label: String(' 第 ' + episode.episodeNo + ' 集 · ' + episode.title),
value: episode.episodeNo
}))
]"
class="select-control"
></NSelect
></label>
<label
><span class="field-label">批量并发</span
><NInputNumber
:disabled="operation.pending"
:input-props="{ id: 'storyboard-concurrency' }"
:value="typeof concurrency === 'number' ? concurrency : null"
@update:value="concurrency = $event ?? 0"
:min="1"
:step="1"
></NInputNumber
><span v-if="!batchValid" class="mt-1 block text-xs text-danger">请输入正整数</span></label
>
<option v-if="!episodeOptions.length" :value="0">暂无剧集</option>
<option v-for="episode in episodeOptions" :key="episode.episodeNo" :value="episode.episodeNo">
{{ episode.episodeNo }} · {{ episode.title }}
</option>
</select></label
>
<label
><span class="field-label">批量并发</span
><input
id="storyboard-concurrency"
v-model.number="concurrency"
class="input"
type="number"
min="1"
step="1"
<label
><span class="field-label">状态自动修复次数</span
><NInputNumber
:disabled="operation.pending"
:input-props="{ id: 'storyboard-repairs' }"
:value="typeof maxRepairAttempts === 'number' ? maxRepairAttempts : null"
@update:value="maxRepairAttempts = $event ?? 0"
:min="0"
:step="1"
></NInputNumber
><span v-if="!repairsValid" class="mt-1 block text-xs text-danger"
>请输入非负整数0 表示不修复</span
></label
>
<NCheckbox
v-model:checked="force"
:disabled="operation.pending"
/><span v-if="!batchValid" class="mt-1 block text-xs text-danger">请输入正整数</span></label
>
<label
><span class="field-label">状态自动修复次数</span
><input
id="storyboard-repairs"
v-model.number="maxRepairAttempts"
class="input"
type="number"
min="0"
step="1"
:disabled="operation.pending"
/><span v-if="!repairsValid" class="mt-1 block text-xs text-danger"
>请输入非负整数0 表示不修复</span
></label
>
<label class="flex items-center gap-2 pb-3 text-xs"
><input
v-model="force"
type="checkbox"
class="accent-accent"
:disabled="operation.pending"
/>批量覆盖已有结果</label
>
</div>
<div class="generation-row">
<div>
<h3 class="text-sm font-semibold">
<span class="mr-2 font-mono text-xs text-muted">01</span>导演摄影设计
</h3>
<p class="mt-2 text-xs leading-6 text-muted">
景别机位运镜与构图单集操作会重新生成并覆盖批量默认跳过完整剧集
class="flex items-center gap-2 pb-3 text-xs"
>批量覆盖已有结果</NCheckbox
>
</div>
</section></template
>
<NCollapse v-model:expanded-names="overview" class="workspace-overview"
><NCollapseItem name="generation" title="生成配置、批量操作与回执"
><div class="overview-content">
<div class="generation-row">
<div>
<h3 class="text-sm font-semibold">
<span class="mr-2 font-mono text-xs text-muted">01</span>导演摄影设计
</h3>
<p class="mt-2 text-xs leading-6 text-muted">
景别机位运镜与构图单集操作会重新生成并覆盖批量默认跳过完整剧集
</p>
</div>
<div class="flex flex-wrap gap-2">
<ConfirmAction
label="生成本集导演设计"
:disabled="!allowed['direction-one']"
acknowledgement
description="调用模型生成当前整集的导演设计,校验通过后保存。如果此集已有设计,将覆盖原结果。"
@confirm="run('direction-one')"
/><ConfirmAction
:label="force ? '重生成全部导演设计' : '补齐项目导演设计'"
:disabled="!allowed['direction-all']"
acknowledgement
:description="
force
? '重新生成项目全部剧集的导演设计,覆盖已有结果。'
: '对整个项目生成导演设计,跳过已完整保存的剧集,可用于重试失败或未完成的剧集。'
"
@confirm="run('direction-all')"
/>
</div>
</div>
<div class="generation-row">
<div>
<h3 class="text-sm font-semibold">
<span class="mr-2 font-mono text-xs text-muted">02</span>即时视觉状态
</h3>
<p class="mt-2 text-xs leading-6 text-muted">
表情姿态位置环境与连续性每集需要完整导演设计批量中缺少前置设计的剧集会单独报错
</p>
</div>
<div class="flex flex-wrap gap-2">
<ConfirmAction
label="生成本集即时状态"
:disabled="!allowed['visual-one']"
acknowledgement
description="依据本集导演设计和主体形态生成即时视觉状态,按配置尝试自动修复。校验通过才保存,已有状态将被覆盖。"
@confirm="run('visual-one')"
/><ConfirmAction
:label="force ? '重生成全部即时状态' : '补齐项目即时状态'"
:disabled="!allowed['visual-all']"
acknowledgement
:description="
force
? '重新生成整个项目的即时状态,覆盖已有结果。每集必须先有完整导演设计。'
: '补齐项目即时状态,跳过完整剧集;未完成导演设计的剧集会返回失败原因。'
"
@confirm="run('visual-all')"
/>
</div>
</div>
<p class="mt-4 text-[11px] leading-6 text-muted">
修改或覆盖上游设计后后端不会自动使旧状态旧提示词失效请按顺序重生成下游内容自动修复次数仅用于
VisualState
</p>
</div>
<div class="flex flex-wrap gap-2">
<ConfirmAction
label="生成本集导演设计"
:disabled="!allowed['direction-one']"
acknowledgement
description="调用模型生成当前整集的导演设计,校验通过后保存。如果此集已有设计,将覆盖原结果。"
@confirm="run('direction-one')"
/><ConfirmAction
:label="force ? '重生成全部导演设计' : '补齐项目导演设计'"
:disabled="!allowed['direction-all']"
acknowledgement
:description="
force
? '重新生成项目全部剧集的导演设计,覆盖已有结果。'
: '对整个项目生成导演设计,跳过已完整保存的剧集,可用于重试失败或未完成的剧集。'
"
@confirm="run('direction-all')"
/>
</div>
</div>
<div class="generation-row">
<div>
<h3 class="text-sm font-semibold">
<span class="mr-2 font-mono text-xs text-muted">02</span>即时视觉状态
</h3>
<p class="mt-2 text-xs leading-6 text-muted">
表情姿态位置环境与连续性每集需要完整导演设计批量中缺少前置设计的剧集会单独报错
</p>
</div>
<div class="flex flex-wrap gap-2">
<ConfirmAction
label="生成本集即时状态"
:disabled="!allowed['visual-one']"
acknowledgement
description="依据本集导演设计和主体形态生成即时视觉状态,按配置尝试自动修复。校验通过才保存,已有状态将被覆盖。"
@confirm="run('visual-one')"
/><ConfirmAction
:label="force ? '重生成全部即时状态' : '补齐项目即时状态'"
:disabled="!allowed['visual-all']"
acknowledgement
:description="
force
? '重新生成整个项目的即时状态,覆盖已有结果。每集必须先有完整导演设计。'
: '补齐项目即时状态,跳过完整剧集;未完成导演设计的剧集会返回失败原因。'
"
@confirm="run('visual-all')"
/>
</div>
</div>
<p class="mt-4 text-[11px] leading-6 text-muted">
修改或覆盖上游设计后后端不会自动使旧状态旧提示词失效请按顺序重生成下游内容自动修复次数仅用于
VisualState
</p>
<details class="mt-3 border-t border-line pt-3">
<summary class="cursor-pointer text-xs font-medium">视频提示词批量操作</summary>
<p class="my-3 text-xs leading-6 text-muted">
后端会先校验 GenerationSpec
与主体参考图这里只生成提示词首帧视频任务就绪诊断与失败重试已整理到
<RouterLink :to="`/projects/${id}/production`" class="text-button mx-1">镜头生产</RouterLink>
</p>
<ConfirmAction
:label="force ? '重生成全部视频提示词' : '补齐项目视频提示词'"
:disabled="!allowed.prompts"
acknowledgement
:description="
force
? '调用模型重新生成全项目镜头的视频提示词,覆盖已有正文。'
: '调用模型生成缺少视频提示词的镜头,已有提示词直接跳过。参考图缺失会返回失败。'
"
@confirm="run('prompts')"
/>
</details>
</section>
<p v-if="query.error.value" class="alert alert-error mt-4" role="alert">
{{ query.error.value }} 当前保留上次成功读取的数据暂停生成操作<button
class="text-button ml-3"
@click="query.refresh"
<NCollapse class="mt-3 border-t border-line pt-3"
><NCollapseItem name="details"
><template #header>视频提示词批量操作</template>
<p class="my-3 text-xs leading-6 text-muted">
后端会先校验 GenerationSpec
与主体参考图这里只生成提示词首帧视频任务就绪诊断与失败重试已整理到
<RouterLink :to="`/projects/${id}/production`" class="text-button mx-1"
>镜头生产</RouterLink
>
</p>
<ConfirmAction
:label="force ? '重生成全部视频提示词' : '补齐项目视频提示词'"
:disabled="!allowed.prompts"
acknowledgement
:description="
force
? '调用模型重新生成全项目镜头的视频提示词,覆盖已有正文。'
: '调用模型生成缺少视频提示词的镜头,已有提示词直接跳过。参考图缺失会返回失败。'
"
@confirm="run('prompts')" /></NCollapseItem
></NCollapse>
<GenerationReceipt v-if="session.receipt" :receipt="session.receipt" /></div></NCollapseItem
></NCollapse>
<div class="workspace-feedback">
<NAlert v-if="query.error.value" role="alert" type="error" :show-icon="false" class="mt-4"
>{{ query.error.value }} 当前保留上次成功读取的数据暂停生成操作<NButton
@click="query.refresh"
text
size="small"
class="ml-3"
>
重试查询
</NButton></NAlert
><NAlert
v-if="!prerequisites.directionEpisode || !prerequisites.visualEpisode"
type="info"
:show-icon="false"
class="mt-4"
>
重试查询
</button>
</p>
<p v-if="!prerequisites.directionEpisode || !prerequisites.visualEpisode" class="alert mt-4">
最新 Breakdown 缺少本集生成所需的镜头主体或形态数据已保存设计仍可查看请先在拆解页完成或恢复相应阶段
</p>
<p v-else-if="data && !directionComplete && data.directions.shotCount" class="alert mt-4">
本集导演设计尚未补齐请先完成第 01 再生成本集即时状态
</p>
<GenerationReceipt v-if="session.receipt" :receipt="session.receipt" />
最新 Breakdown
缺少本集生成所需的镜头主体或形态数据已保存设计仍可查看请先在拆解页完成或恢复相应阶段 </NAlert
><NAlert
v-else-if="data && !directionComplete && data.directions.shotCount"
type="info"
:show-icon="false"
class="mt-4"
>
本集导演设计尚未补齐请先完成第 01 再生成本集即时状态
</NAlert>
</div>
<div class="my-6 flex flex-wrap items-center justify-between gap-4">
<div>
<h3 class="text-sm font-semibold"> {{ episodeNo || '—' }} · 镜头检查</h3>
@@ -234,28 +278,27 @@ function exportEpisode() {
</p>
</div>
<div class="flex gap-3">
<button class="text-button" :disabled="query.loading.value" @click="query.refresh">
<RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新分镜</button
><button class="text-button" :disabled="!shots.length" @click="exportEpisode">
<Download :size="13" />导出本集
</button>
<NButton :disabled="query.loading.value" @click="query.refresh" text size="small"
><RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新分镜</NButton
><NButton :disabled="!shots.length" @click="exportEpisode" text size="small"
><Download :size="13" />导出本集
</NButton>
</div>
</div>
<div class="mb-5 grid gap-4 sm:grid-cols-2">
<div v-for="item in coverage" :key="item.title" class="panel px-5 py-4">
<div class="storyboard-coverage grid grid-cols-2 gap-3">
<div v-for="item in coverage" :key="item.title" class="panel px-3 py-2">
<div class="mb-3 flex items-center justify-between text-xs">
<span>{{ item.title }}</span
><span class="font-mono text-muted">{{ item.count }} / {{ item.total }} 镜头</span>
</div>
<ProgressRoot
:model-value="item.count"
:max="Math.max(item.total, 1)"
<NProgress
:aria-label="item.title + '已保存覆盖率'"
class="progress-track"
><ProgressIndicator
class="progress-fill"
:style="{ width: (item.total ? Math.min(100, (item.count / item.total) * 100) : 0) + '%' }"
/></ProgressRoot>
type="line"
:show-indicator="false"
:height="4"
:percentage="Math.min(100, Math.max(0, (item.count / Math.max(1, Math.max(item.total, 1))) * 100))"
class=""
></NProgress>
</div>
</div>
<div v-if="!data && query.loading.value" class="panel p-8 text-sm text-muted" role="status">
@@ -271,21 +314,20 @@ function exportEpisode() {
>
BEAT {{ String(item.beatNo).padStart(2, '0') }}
</h4>
<button
class="shot-link"
:class="{ selected: shot.shotId === item.shotId }"
<NButton
:aria-current="shot.shotId === item.shotId ? 'true' : undefined"
@click="selectedShot = item.shotId"
>
<span class="font-mono text-[10px] text-muted">{{ String(item.shotNo).padStart(2, '0') }}</span
class="shot-link"
:class="{ selected: shot.shotId === item.shotId }"
><span class="font-mono text-[10px] text-muted">{{ String(item.shotNo).padStart(2, '0') }}</span
><span class="min-w-0"
><span class="block truncate text-xs font-medium">{{ item.title }}</span
><span class="mt-1 block text-[10px] text-muted"
>{{ item.direction ? '设计已保存' : '待设计' }} ·
{{ item.visualState ? '状态已保存' : '待状态' }}</span
></span
>
</button></template
></NButton
></template
>
</nav>
<article class="min-w-0 p-5 lg:p-7">
@@ -306,14 +348,15 @@ function exportEpisode() {
标题与剧情描述取自最近可用 Breakdown 快照以下设计与状态取自数据库
</p>
</header>
<TabsRoot default-value="design"
><TabsList class="tabs-list" aria-label="镜头详情"
><TabsTrigger value="design" class="tab-trigger">设计与状态</TabsTrigger
><TabsTrigger value="tools" class="tab-trigger">参考图与生成规格</TabsTrigger></TabsList
><TabsContent value="design" class="pt-6"><ShotDesign :shot="shot" /></TabsContent
><TabsContent value="tools" class="pt-6"
><ShotTools :key="shot.shotId" :project-id="id" :shot="shot" :disabled="blocked" /></TabsContent
></TabsRoot>
<div>
<NTabs v-model:value="detailTab" type="line" size="small" aria-label="镜头详情"
><NTab name="design">设计与状态</NTab><NTab name="tools">参考图与生成规格</NTab></NTabs
>
<div v-if="detailTab === 'design'" class="pt-6"><ShotDesign :shot="shot" /></div>
<div v-if="detailTab === 'tools'" class="pt-6">
<ShotTools :key="shot.shotId" :project-id="id" :shot="shot" :disabled="blocked" />
</div>
</div>
</article>
</div>
<EmptyState
@@ -323,6 +366,6 @@ function exportEpisode() {
><RouterLink :to="`/projects/${id}/breakdown`" class="button button-secondary mt-4"
>前往拆解</RouterLink
></EmptyState
>
</div>
></WorkspacePage
>
</template>
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { NAlert, NButton, NTable } from 'naive-ui'
import { computed } from 'vue'
import { Download } from '@lucide/vue'
import { downloadText } from '../../../lib/format'
@@ -31,19 +32,19 @@ function exportReceipt() {
<section class="panel mt-5 p-5" aria-label="生成回执">
<div class="flex items-center justify-between gap-3">
<h3 class="text-sm font-semibold">{{ receipt.title }} · 本次回执</h3>
<button class="text-button" @click="exportReceipt"><Download :size="13" />导出诊断</button>
<NButton @click="exportReceipt" text size="small"><Download :size="13" />导出诊断</NButton>
</div>
<p v-if="failed" class="alert alert-error mt-3" role="alert">
<NAlert v-if="failed" role="alert" type="error" :show-icon="false" class="mt-3">
本次存在失败校验未通过或未保存项关闭批量覆盖已有结果后再补齐可跳过已完整保存项
</p>
</NAlert>
<template v-if="receipt.kind === 'batch'">
<p class="my-4 text-xs text-muted">
{{ receipt.result.episodeCount }} · 完成 {{ receipt.result.completedEpisodes }} · 跳过
{{ receipt.result.skippedEpisodes }} · 失败 {{ receipt.result.failedEpisodes }}
</p>
<div class="table-scroll">
<table class="project-table">
<thead>
<NTable :single-line="false" class="project-table"
><thead>
<tr>
<th>剧集</th>
<th>结果</th>
@@ -71,8 +72,8 @@ function exportReceipt() {
</p>
</td>
</tr>
</tbody>
</table>
</tbody></NTable
>
</div>
</template>
<template v-else-if="receipt.kind === 'prompts'">
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { NTag } from 'naive-ui'
import { directionLabel } from '../model'
import type { DesignedShot } from '../types'
@@ -14,7 +15,7 @@ defineProps<{ shot: DesignedShot }>()
</h3>
<template v-if="shot.direction"
><div class="mb-5 flex flex-wrap gap-2">
<span
<NTag
v-for="field in [
{ key: 'shotSize', label: '景别' },
{ key: 'cameraAngle', label: '角度' },
@@ -23,8 +24,9 @@ defineProps<{ shot: DesignedShot }>()
{ key: 'depthOfField', label: '景深' }
] as const"
:key="field.key"
class="tag"
>{{ field.label }} · {{ directionLabel(shot.direction[field.key], field.key) }}</span
size="small"
:bordered="false"
>{{ field.label }} · {{ directionLabel(shot.direction[field.key], field.key) }}</NTag
>
</div>
<dl class="detail-grid">
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { NAlert, NButton, NCheckbox } from 'naive-ui'
import { computed, onScopeDispose, ref, watch } from 'vue'
import { Download, ExternalLink, RefreshCw } from '@lucide/vue'
import { storyboardApi } from '../api'
@@ -101,20 +102,18 @@ function exportResult(kind: 'spec' | 'prompt') {
<section>
<div class="flex flex-wrap items-center justify-between gap-3">
<h3 class="text-sm font-semibold">主体参考图</h3>
<button
class="button button-secondary"
:disabled="busy.references || locked"
@click="load('references')"
>
<RefreshCw :size="13" :class="{ 'animate-spin': busy.references }" />{{
<NButton :disabled="busy.references || locked" @click="load('references')"
><RefreshCw :size="13" :class="{ 'animate-spin': busy.references }" />{{
references ? '刷新参考图' : '读取参考图'
}}
</button>
}}</NButton
>
</div>
<p class="mt-2 text-xs leading-6 text-muted">
读取镜头已绑定形态的图片不生成新图参考图缺失会影响视频提示词生成
</p>
<p v-if="errors.references" class="alert alert-error mt-3" role="alert">{{ errors.references }}</p>
<NAlert v-if="errors.references" role="alert" type="error" :show-icon="false" class="mt-3">{{
errors.references
}}</NAlert>
<template v-if="references">
<div class="reference-grid mt-4">
<article v-for="item in references.references" :key="item.shotSubjectId" class="reference-card">
@@ -165,24 +164,24 @@ function exportResult(kind: 'spec' | 'prompt') {
<section class="border-t border-line pt-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<h3 class="text-sm font-semibold">镜头生成规格</h3>
<button
class="button button-secondary"
<NButton
:disabled="locked || busy.spec || !shot.direction || !shot.visualState"
@click="load('spec')"
>{{ busy.spec ? '编译中…' : '读取生成规格' }}</NButton
>
{{ busy.spec ? '编译中…' : '读取生成规格' }}
</button>
</div>
<p class="mt-2 text-xs leading-6 text-muted">
后端合并主体稳定身份具体形态摄影设计与即时状态编译为 GenerationSpec不调用模型需要本镜头已有
Direction VisualState
</p>
<p v-if="errors.spec" class="alert alert-error mt-3" role="alert">{{ errors.spec }}</p>
<NAlert v-if="errors.spec" role="alert" type="error" :show-icon="false" class="mt-3">{{
errors.spec
}}</NAlert>
<template v-if="spec"
><div class="mt-4 flex items-center justify-between">
<span class="text-xs text-muted"
>{{ spec.subjects.length }} 个主体 · {{ spec.durationSeconds }} </span
><button class="text-button" @click="exportResult('spec')"><Download :size="13" />导出规格</button>
><NButton @click="exportResult('spec')" text size="small"><Download :size="13" />导出规格</NButton>
</div>
<pre class="storyboard-json mt-3">{{ JSON.stringify(spec, null, 2) }}</pre>
</template>
@@ -204,19 +203,14 @@ function exportResult(kind: 'spec' | 'prompt') {
: '读取此镜头已有提示词;若不存在,将调用模型并保存结果。后端会检查参考图是否齐全。'
"
@confirm="generatePrompt"
/><label class="flex items-center gap-2 text-xs"
><input
v-model="force"
type="checkbox"
class="accent-accent"
:disabled="locked"
/>覆盖此镜头提示词</label
/><NCheckbox v-model:checked="force" :disabled="locked" class="flex items-center gap-2 text-xs"
>覆盖此镜头提示词</NCheckbox
>
</div>
<div v-if="prompt" class="mt-5">
<div class="mb-3 flex items-center justify-between">
<span class="text-xs font-medium">本会话已读取的正文</span
><button class="text-button" @click="exportResult('prompt')"><Download :size="13" />导出</button>
><NButton @click="exportResult('prompt')" text size="small"><Download :size="13" />导出</NButton>
</div>
<p class="whitespace-pre-wrap text-sm leading-7">{{ prompt.videoPrompt }}</p>
<template v-if="prompt.negativePrompt"