refactor: 精简导航并扩大工作区内容显示面积

This commit is contained in:
GouJ
2026-09-01 17:45:40 +08:00
parent 70ab4ba48f
commit 0279c701b4
15 changed files with 1256 additions and 821 deletions
+219 -188
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
import WorkspaceTools from '../../components/ui/WorkspaceTools.vue'
import {
NScrollbar,
NAlert,
@@ -81,174 +82,235 @@ function exportEpisode() {
)
}
/** 回执到达后打开诊断区;平时留出镜头检查空间。 */
const overview = ref<string[]>([])
const toolsOpen = ref(false)
const overview = ref<string[]>(['generation'])
watch(
() => session.value.receipt,
receipt => {
if (receipt) overview.value = ['generation']
},
{ immediate: true }
if (receipt) {
overview.value = ['generation']
toolsOpen.value = true
}
}
)
</script>
<template>
<WorkspacePage split class="storyboard-workspace-page"
<WorkspacePage split compact 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>
<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
>
<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"
class="flex items-center gap-2 pb-3 text-xs"
>批量覆盖已有结果</NCheckbox
><div class="workspace-toolbar">
<label class="toolbar-episode"
><span>剧集</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>
<div class="toolbar-metrics">
<span v-for="item in coverage" :key="item.title"
>{{ item.title }} {{ item.count }} / {{ item.total }}</span
>
</div>
</section></template
<div class="toolbar-actions">
<div class="flex gap-3">
<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>
<WorkspaceTools
v-model:open="toolsOpen"
title="分镜生成配置与回执"
:has-receipt="!!session.receipt"
>
<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>
<section class="mt-3" aria-label="分镜生成配置">
<div class="storyboard-controls">
<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
>
<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"
class="flex items-center gap-2 pb-3 text-xs"
>批量覆盖已有结果</NCheckbox
>
</div>
</section>
<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>
<NProgress
:aria-label="item.title + '已保存覆盖率'"
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 class="workspace-overview-panel">
<NCollapse v-model:expanded-names="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>
<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>
</WorkspaceTools>
</div></div
></template>
<NScrollbar
v-if="
query.error.value ||
!prerequisites.directionEpisode ||
!prerequisites.visualEpisode ||
(data && !directionComplete && data.directions.shotCount)
"
class="workspace-feedback"
>
<NScrollbar class="workspace-overview" content-class="overview-scroll-content"
><NCollapse v-model:expanded-names="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>
<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
></NScrollbar>
<NScrollbar 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"
@@ -275,37 +337,6 @@ watch(
本集导演设计尚未补齐,请先完成第 01 步,再生成本集即时状态。
</NAlert>
</NScrollbar>
<div class="my-6 flex flex-wrap items-center justify-between gap-4">
<div>
<h3 class="text-sm font-semibold"> {{ episodeNo || '—' }} · 镜头检查</h3>
<p class="mt-2 text-[11px] text-muted">
当前剧集数据库结果每 6 秒刷新覆盖率不是批量任务进度批量诊断在请求返回后显示
</p>
</div>
<div class="flex gap-3">
<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="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>
<NProgress
:aria-label="item.title + '已保存覆盖率'"
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">
正在读取正式分镜数据……
</div>