feat: 补齐前端主流程并收紧工作台布局

统一工作台紧凑头部与形态图库间距,修复主体身份布局,并接入正式严格视频质量流水线。
This commit is contained in:
GouJ
2026-09-08 09:25:14 +08:00
committed by GitHub
parent 0f8b2074e4
commit 4edb0ff6cc
25 changed files with 355 additions and 233 deletions
+1 -13
View File
@@ -224,17 +224,11 @@ watch(
>导演摄影设计
</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
@@ -259,12 +253,6 @@ watch(
</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
+2 -16
View File
@@ -1,6 +1,5 @@
import { request } from '../../lib/http'
import type {
DirectionGeneration,
EpisodeDirections,
EpisodeVisualStates,
PromptBatchResult,
@@ -8,8 +7,7 @@ import type {
ShotReferences,
StoryboardBatchInput,
StoryboardBatchResult,
VisualStateBatchInput,
VisualStateGeneration
VisualStateBatchInput
} from './types'
import type { ShotGenerationSpec } from './generation-spec.types'
@@ -23,30 +21,18 @@ function shotPath(id: string) {
return `/storyboard-shots/${encodeURIComponent(id)}`
}
/** Storyboard 路由契约;单集入口虽名为 generate-test,但 persist=true 才适用于工作台。 */
/** Storyboard 正式路由契约;测试路由不暴露为前端生产入口。 */
export const storyboardApi = {
directions: (id: string, episodeNo: number, signal?: AbortSignal) =>
request<EpisodeDirections>(`${projectPath(id)}/storyboard-directions?episodeNo=${episodeNo}`, { signal }),
visualStates: (id: string, episodeNo: number, signal?: AbortSignal) =>
request<EpisodeVisualStates>(`${projectPath(id)}/storyboard-visual-states?episodeNo=${episodeNo}`, { signal }),
generateDirection: (id: string, episodeNo: number) =>
request<DirectionGeneration>(`${projectPath(id)}/storyboard-directions/generate-test`, {
method: 'POST',
body: { episodeNo, persist: true },
timeoutMs: 0
}),
generateDirections: (id: string, input: StoryboardBatchInput) =>
request<StoryboardBatchResult>(`${projectPath(id)}/storyboard-directions/generate`, {
method: 'POST',
body: input,
timeoutMs: 0
}),
generateVisualState: (id: string, episodeNo: number, maxRepairAttempts: number) =>
request<VisualStateGeneration>(`${projectPath(id)}/storyboard-visual-states/generate-test`, {
method: 'POST',
body: { episodeNo, persist: true, maxRepairAttempts },
timeoutMs: 0
}),
generateVisualStates: (id: string, input: VisualStateBatchInput) =>
request<StoryboardBatchResult>(`${projectPath(id)}/storyboard-visual-states/generate`, {
method: 'POST',
@@ -3,24 +3,13 @@ import { NScrollbar, NAlert, NButton, NTable } from 'naive-ui'
import { computed } from 'vue'
import { Download } from '@lucide/vue'
import { downloadText } from '../../../lib/format'
import type { StoryboardIssue, StoryboardReceipt } from '../types'
import type { StoryboardReceipt } from '../types'
/** 独立展示提交结果,不能将 HTTP 成功或局部成功显示为整批完成。 */
const props = defineProps<{ receipt: StoryboardReceipt }>()
const issues = computed<StoryboardIssue[]>(() => {
if (props.receipt.kind === 'direction') return props.receipt.result.validation.issues
if (props.receipt.kind === 'visual-state')
return props.receipt.result.remainingIssues.length
? props.receipt.result.remainingIssues
: props.receipt.result.validation.issues
return []
})
const failed = computed(() => {
if (props.receipt.kind === 'batch') return props.receipt.result.failedEpisodes > 0
if (props.receipt.kind === 'prompts') return props.receipt.result.failed > 0
if (props.receipt.kind === 'visual-state' && !props.receipt.result.persisted) return true
return !props.receipt.result.validation.valid
})
const failed = computed(() =>
props.receipt.kind === 'batch' ? props.receipt.result.failedEpisodes > 0 : props.receipt.result.failed > 0
)
/** 导出原始回执,保留后端提供的完整失败诊断。 */
function exportReceipt() {
@@ -91,24 +80,6 @@ function exportReceipt() {
当前批量接口不返回成功镜头的 Prompt 正文选择镜头后点击读取生成提示词已有内容会直接复用
</p>
</template>
<template v-else>
<p class="mt-4 text-sm" :class="failed ? 'text-danger' : 'text-success'">
{{ receipt.result.validation.valid ? '生成校验通过' : '校验失败,未保存本次生成结果'
}}<span v-if="receipt.kind === 'visual-state'">
· 自动修复 {{ receipt.result.repairAttempts }} ·
{{ receipt.result.persisted ? '已持久化' : '未持久化' }}</span
>
</p>
<p v-if="receipt.kind === 'direction' && receipt.result.validation.valid" class="mt-2 text-xs text-muted">
已请求持久化数据库实际覆盖率以下方刷新后的数据为准
</p>
<ul class="mt-3 space-y-2 text-xs text-danger">
<li v-for="(issue, index) in issues" :key="index">
{{ issue.episodeNo }} / Beat {{ issue.beatNo ?? '' }} / Shot {{ issue.shotNo ?? '' }}
{{ issue.subjectRef }}{{ issue.message }}
</li>
</ul>
</template>
<p class="mt-4 text-[11px] text-muted">回执仅保留在当前浏览器会话中数据库结果是覆盖率和镜头详情的依据</p>
</section>
</template>
+1 -30
View File
@@ -23,7 +23,7 @@ export interface SubjectVisualState {
transientState: string
}
/** GET 返回扁平化环境字段,与 generate-test 的嵌套 environment 不同。 */
/** GET 返回已经持久化的扁平化环境字段。 */
export interface ShotVisualState {
timeOfDay: string
weather: string
@@ -62,33 +62,6 @@ export interface StoryboardIssue {
message: string
}
/** 单集生成结果中的校验信息,HTTP 200 不代表 valid。 */
export interface StoryboardValidation {
valid: boolean
issues: StoryboardIssue[]
}
/** 单集导演设计返回领域结构;是否落库应再查 GET 覆盖率确认。 */
export interface DirectionGeneration {
episodeNo: number
direction: {
episodeNo: number
beatDirections: { beatNo: number; shots: (ShotDirection & { shotNo: number })[] }[]
}
validation: StoryboardValidation
}
/** 单集 VisualState 的自动修复与落库回执。 */
export interface VisualStateGeneration {
episodeNo: number
visualState: unknown
validation: StoryboardValidation
repaired: boolean
repairAttempts: number
remainingIssues: StoryboardIssue[]
persisted: boolean
}
/** 项目批量生成中的单集结果;保留失败原因和修复诊断。 */
export interface EpisodeGenerationResult {
episodeNo: number
@@ -178,8 +151,6 @@ export interface DesignedShot {
/** 生成回执按操作区分,切换剧集时仍保留原始目标信息。 */
export type StoryboardReceipt =
| { kind: 'direction'; title: string; episodeNo: number; result: DirectionGeneration }
| { kind: 'visual-state'; title: string; episodeNo: number; result: VisualStateGeneration }
| { kind: 'batch'; title: string; result: StoryboardBatchResult }
| { kind: 'prompts'; title: string; result: PromptBatchResult }
+2 -24
View File
@@ -6,8 +6,8 @@ import { getOperation, runOperation } from '../workflows/operations'
import { storyboardApi } from './api'
import { getStoryboardSession, mergeDesignedShots, storyboardPrerequisites } from './model'
/** 分镜页允许的生成命令,保持单集与批量参数边界。 */
export type StoryboardCommand = 'direction-one' | 'direction-all' | 'visual-one' | 'visual-all' | 'prompts'
/** 分镜页只提供正式项目级生成命令,不暴露后端测试路由。 */
export type StoryboardCommand = 'direction-all' | 'visual-all' | 'prompts'
/** 分镜查询、依赖判断与操作入口;不修改后端 graph。 */
export function useStoryboard() {
@@ -87,10 +87,7 @@ export function useStoryboard() {
data.value.directions.coverage === 1
)
const allowed = computed<Record<StoryboardCommand, boolean>>(() => ({
'direction-one': !blocked.value && prerequisites.value.directionEpisode && !!data.value?.directions.shotCount,
'direction-all': !blocked.value && prerequisites.value.directionProject && batchValid.value,
'visual-one':
!blocked.value && prerequisites.value.visualEpisode && directionComplete.value && repairsValid.value,
'visual-all': !blocked.value && prerequisites.value.visualProject && batchValid.value && repairsValid.value,
prompts: !blocked.value && shots.value.length > 0 && batchValid.value
}))
@@ -99,28 +96,17 @@ export function useStoryboard() {
async function run(command: StoryboardCommand) {
if (!allowed.value[command]) return
const projectId = id.value
const number = episodeNo.value
const batch = { concurrency: concurrency.value, force: force.value }
const repairs = maxRepairAttempts.value
const target = getStoryboardSession(projectId)
const titles: Record<StoryboardCommand, string> = {
'direction-one': `${number} 集导演设计`,
'direction-all': '项目导演设计',
'visual-one': `${number} 集即时状态`,
'visual-all': '项目即时状态',
prompts: '项目视频提示词'
}
target.receipt = null
await runOperation(projectId, `生成${titles[command]}`, async () => {
switch (command) {
case 'direction-one':
target.receipt = {
kind: 'direction',
title: titles[command],
episodeNo: number,
result: await storyboardApi.generateDirection(projectId, number)
}
break
case 'direction-all':
target.receipt = {
kind: 'batch',
@@ -128,14 +114,6 @@ export function useStoryboard() {
result: await storyboardApi.generateDirections(projectId, batch)
}
break
case 'visual-one':
target.receipt = {
kind: 'visual-state',
title: titles[command],
episodeNo: number,
result: await storyboardApi.generateVisualState(projectId, number, repairs)
}
break
case 'visual-all':
target.receipt = {
kind: 'batch',