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