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
@@ -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,
@@ -129,27 +130,265 @@ function chooseAnchor(imageId: string) {
else void setAnchor(imageId)
}
/** 新回执自动展开,避免折叠区隐藏部分失败诊断。 */
const overview = ref<string[]>([])
const toolsOpen = ref(false)
const overview = ref<string[]>(['casting'])
watch(
() => session.value.receipt,
receipt => {
if (receipt) overview.value = ['casting']
},
{ immediate: true }
if (receipt) {
overview.value = ['casting']
toolsOpen.value = true
}
}
)
</script>
<template>
<WorkspacePage split
<WorkspacePage split compact
><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 class="workspace-toolbar">
<div class="toolbar-type-filter">
<NSelect
aria-label="身份主体类型"
v-model:value="module"
:options="[
{ label: String('全部类型'), value: 'all' },
{ label: String('人物'), value: 'character' },
{ label: String('场景'), value: 'scene' },
{ label: String('道具'), value: 'prop' }
]"
></NSelect>
</div>
<span class="toolbar-summary"> {{ subjects.length }} 个主体</span>
<div class="toolbar-actions">
<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/${projectId}/subject-images`" class="text-button"
>下一步形态图片 </RouterLink
>
</div>
<div class="workspace-overview-panel">
<NCollapse v-model:expanded-names="overview"
><NCollapseItem name="casting" title="角色选角总览与批量操作"
><div class="overview-content">
<NAlert type="info" :show-icon="false" class="mt-5 text-xs">
Character 现在需要依次完成身份文本选角候选确认演员并锁定
Identity之后首帧才能就绪未明确人物背景时AI
默认以中国人物作为选角基线项目和角色的明确事实始终优先场景和道具仍使用普通身份母版流程
</NAlert>
<NAlert
v-if="styleQuery.data.value && !hasStyle"
type="info"
:show-icon="false"
class="mt-4"
>
先创建项目视觉风格才能 AI 生成身份或身份图片<RouterLink
:to="`/projects/${projectId}/visual-style`"
class="text-button ml-2"
>设置视觉风格 </RouterLink
></NAlert
>
<section v-if="casting" class="panel my-5 p-5" aria-label="角色选角流程">
<div class="flex flex-wrap items-start justify-between gap-4">
<div class="flex items-start gap-3">
<span class="production-step"><UserCheck :size="17" /></span>
<div>
<h3 class="text-sm font-semibold">角色选角</h3>
<p class="mt-2 text-xs leading-6 text-muted">
只处理 Character每张候选都从 Identity 独立生成不复用旧
Anchor确认选择时才会同时切换 Anchor 并锁定 Identity
</p>
</div>
</div>
<span class="font-mono text-xs text-muted"
>{{ casting.ready }} / {{ casting.total }} 已完成</span
>
</div>
<dl class="casting-stats mt-4">
<div>
<dt>缺身份文本</dt>
<dd>{{ casting.missingIdentity }}</dd>
</div>
<div>
<dt>缺候选图</dt>
<dd>{{ casting.missingAnchor }}</dd>
</div>
<div>
<dt>待确认演员</dt>
<dd>{{ casting.candidatePending }}</dd>
</div>
<div>
<dt>母版未锁定</dt>
<dd>{{ casting.unlocked }}</dd>
</div>
<div>
<dt>选角完成</dt>
<dd>{{ casting.ready }}</dd>
</div>
</dl>
<div class="mt-5 flex flex-wrap items-end gap-4 border-t border-line pt-4">
<label class="w-28">
<span class="field-label">并发数</span>
<NInputNumber
:disabled="operation.pending"
:value="typeof concurrency === 'number' ? concurrency : null"
@update:value="concurrency = $event ?? 0"
:min="1"
:step="1"
></NInputNumber>
</label>
<label class="w-28">
<span class="field-label">本批上限</span>
<NInputNumber
:disabled="operation.pending"
:value="
typeof candidateLimit === 'number' ? candidateLimit : null
"
@update:value="candidateLimit = $event ?? 0"
:min="1"
:step="1"
></NInputNumber>
</label>
<NCheckbox
v-model:checked="force"
:disabled="operation.pending"
class="flex gap-2 pb-2 text-xs"
>覆盖未锁定身份文本
</NCheckbox>
<ConfirmAction
:label="force ? '重生成角色身份文本' : '补齐角色身份文本'"
:disabled="
blocked || !hasStyle || !batchValid || dirty || !casting.total
"
acknowledgement
description="只生成 Character Identity 文本,不生成图片。已锁定角色始终跳过;覆盖文本不会自动更新已有候选图。"
@confirm="generateProject(true)"
/>
<ConfirmAction
label="生成选角候选"
:disabled="
castingBlocked ||
!hasStyle ||
!batchValid ||
!candidateLimitValid ||
!casting.missingAnchor
"
acknowledgement
description="最多按本批上限为缺少 Anchor 的 Character 调用 Seedream,各自独立生成一张停用的 primary 候选,不复用旧身份母版。已有候选、已有母版或已完成选角的角色不会重复处理。"
@confirm="generateCastingCandidates"
/>
</div>
<p
v-if="!batchValid || !candidateLimitValid"
class="mt-3 text-xs text-danger"
>
并发数与本批上限都必须是正整数
</p>
<div class="casting-list mt-5">
<NButton
v-for="item in casting.items"
:key="item.subjectId"
@click="select(item.subjectId)"
class="casting-item"
:class="{ selected: selectedId === item.subjectId }"
><span class="min-w-0">
<span class="block truncate text-xs font-medium">{{
item.subjectName
}}</span>
<span class="mt-1 block font-mono text-[10px] text-muted">{{
item.subjectRef
}}</span> </span
><StatusBadge
:status="item.status"
:label="castingStatusLabel(item.status)"
/></NButton>
</div>
</section>
<NCollapse class="panel mb-5 p-5"
><NCollapseItem name="details"
><template #header>高级全部主体身份文本</template>
<p class="mt-3 text-xs leading-6 text-muted">
同时处理 CharacterScene
Prop角色选角建议优先使用上方专用入口已锁定身份始终跳过
</p>
<ConfirmAction
class="mt-4"
:label="force ? '重生成全部未锁定身份' : '补齐全部主体身份文本'"
:disabled="
blocked || !hasStyle || !batchValid || dirty || !subjects.length
"
acknowledgement
description="生成全项目主体身份描述与提示词,不生成图片。覆盖不会自动更新旧身份图、形态图、首帧或视频。"
@confirm="generateProject(false)" /></NCollapseItem
></NCollapse>
<section
v-if="session.receipt"
class="panel mb-5 p-5"
aria-label="身份文本批量回执"
>
<div class="flex justify-between gap-3">
<h3 class="text-sm font-medium">
{{ session.receipt.title }} · 本次回执
</h3>
<NButton @click="exportReceipt" text size="small">导出诊断</NButton>
</div>
<p
v-if="session.receipt.kind === 'identities'"
class="mt-3 text-xs text-muted"
>
{{ session.receipt.result.total }} 个主体 · 目标
{{ session.receipt.result.targetCount }} · 生成
{{ session.receipt.result.generated }} · 跳过
{{ session.receipt.result.skipped }}含锁定
{{ session.receipt.result.skippedLocked }}· 失败
{{ session.receipt.result.failed }}
</p>
<p v-else class="mt-3 text-xs text-muted">
{{ session.receipt.result.totalCharacters }} 个角色 · 缺候选
{{ session.receipt.result.missingAnchor }} · 本批目标
{{ session.receipt.result.targetCount }} · 已生成
{{ session.receipt.result.generated }} · 失败
{{ session.receipt.result.failed }}
</p>
<NAlert
v-if="session.receipt.result.failed"
role="alert"
type="error"
:show-icon="false"
class="mt-3"
>
本次存在部分失败成功结果不会回滚请检查逐角色原因后再重试
</NAlert>
<ul class="mt-3 space-y-2 text-xs text-danger">
<li
v-for="failure in session.receipt.result.failures"
:key="failure.subjectId"
>
{{ failure.subjectRef }} · {{ failure.subjectId }}{{
failure.error
}}
</li>
</ul>
<p class="mt-3 text-[11px] text-muted">
回执仅保留于当前浏览器会话最终状态以刷新后的正式记录为准
</p>
</section>
</div></NCollapseItem
></NCollapse
>
</div>
</WorkspaceTools>
</div>
<RouterLink :to="`/projects/${projectId}/subject-images`" class="text-button"
>下一步形态图片 </RouterLink
>
</div></template
>
<NScrollbar
@@ -160,181 +399,6 @@ watch(
当前相关操作已暂停请刷新核对后端
</NAlert></NScrollbar
>
<NScrollbar class="workspace-overview" content-class="overview-scroll-content"
><NCollapse v-model:expanded-names="overview"
><NCollapseItem name="casting" title="角色选角总览与批量操作"
><div class="overview-content">
<NAlert type="info" :show-icon="false" class="mt-5 text-xs">
Character 现在需要依次完成身份文本选角候选确认演员并锁定
Identity之后首帧才能就绪未明确人物背景时AI
默认以中国人物作为选角基线项目和角色的明确事实始终优先场景和道具仍使用普通身份母版流程
</NAlert>
<NAlert v-if="styleQuery.data.value && !hasStyle" type="info" :show-icon="false" class="mt-4">
先创建项目视觉风格才能 AI 生成身份或身份图片<RouterLink
:to="`/projects/${projectId}/visual-style`"
class="text-button ml-2"
>设置视觉风格 </RouterLink
></NAlert
>
<section v-if="casting" class="panel my-5 p-5" aria-label="角色选角流程">
<div class="flex flex-wrap items-start justify-between gap-4">
<div class="flex items-start gap-3">
<span class="production-step"><UserCheck :size="17" /></span>
<div>
<h3 class="text-sm font-semibold">角色选角</h3>
<p class="mt-2 text-xs leading-6 text-muted">
只处理 Character每张候选都从 Identity 独立生成不复用旧
Anchor确认选择时才会同时切换 Anchor 并锁定 Identity
</p>
</div>
</div>
<span class="font-mono text-xs text-muted"
>{{ casting.ready }} / {{ casting.total }} 已完成</span
>
</div>
<dl class="casting-stats mt-4">
<div>
<dt>缺身份文本</dt>
<dd>{{ casting.missingIdentity }}</dd>
</div>
<div>
<dt>缺候选图</dt>
<dd>{{ casting.missingAnchor }}</dd>
</div>
<div>
<dt>待确认演员</dt>
<dd>{{ casting.candidatePending }}</dd>
</div>
<div>
<dt>母版未锁定</dt>
<dd>{{ casting.unlocked }}</dd>
</div>
<div>
<dt>选角完成</dt>
<dd>{{ casting.ready }}</dd>
</div>
</dl>
<div class="mt-5 flex flex-wrap items-end gap-4 border-t border-line pt-4">
<label class="w-28">
<span class="field-label">并发数</span>
<NInputNumber
:disabled="operation.pending"
:value="typeof concurrency === 'number' ? concurrency : null"
@update:value="concurrency = $event ?? 0"
:min="1"
:step="1"
></NInputNumber>
</label>
<label class="w-28">
<span class="field-label">本批上限</span>
<NInputNumber
:disabled="operation.pending"
:value="typeof candidateLimit === 'number' ? candidateLimit : null"
@update:value="candidateLimit = $event ?? 0"
:min="1"
:step="1"
></NInputNumber>
</label>
<NCheckbox
v-model:checked="force"
:disabled="operation.pending"
class="flex gap-2 pb-2 text-xs"
>覆盖未锁定身份文本
</NCheckbox>
<ConfirmAction
:label="force ? '重生成角色身份文本' : '补齐角色身份文本'"
:disabled="blocked || !hasStyle || !batchValid || dirty || !casting.total"
acknowledgement
description="只生成 Character Identity 文本,不生成图片。已锁定角色始终跳过;覆盖文本不会自动更新已有候选图。"
@confirm="generateProject(true)"
/>
<ConfirmAction
label="生成选角候选"
:disabled="
castingBlocked ||
!hasStyle ||
!batchValid ||
!candidateLimitValid ||
!casting.missingAnchor
"
acknowledgement
description="最多按本批上限为缺少 Anchor 的 Character 调用 Seedream,各自独立生成一张停用的 primary 候选,不复用旧身份母版。已有候选、已有母版或已完成选角的角色不会重复处理。"
@confirm="generateCastingCandidates"
/>
</div>
<p v-if="!batchValid || !candidateLimitValid" class="mt-3 text-xs text-danger">
并发数与本批上限都必须是正整数
</p>
<div class="casting-list mt-5">
<NButton
v-for="item in casting.items"
:key="item.subjectId"
@click="select(item.subjectId)"
class="casting-item"
:class="{ selected: selectedId === item.subjectId }"
><span class="min-w-0">
<span class="block truncate text-xs font-medium">{{ item.subjectName }}</span>
<span class="mt-1 block font-mono text-[10px] text-muted">{{
item.subjectRef
}}</span> </span
><StatusBadge :status="item.status" :label="castingStatusLabel(item.status)"
/></NButton>
</div>
</section>
<NCollapse class="panel mb-5 p-5"
><NCollapseItem name="details"
><template #header>高级全部主体身份文本</template>
<p class="mt-3 text-xs leading-6 text-muted">
同时处理 CharacterScene
Prop角色选角建议优先使用上方专用入口已锁定身份始终跳过
</p>
<ConfirmAction
class="mt-4"
:label="force ? '重生成全部未锁定身份' : '补齐全部主体身份文本'"
:disabled="blocked || !hasStyle || !batchValid || dirty || !subjects.length"
acknowledgement
description="生成全项目主体身份描述与提示词,不生成图片。覆盖不会自动更新旧身份图、形态图、首帧或视频。"
@confirm="generateProject(false)" /></NCollapseItem
></NCollapse>
<section v-if="session.receipt" class="panel mb-5 p-5" aria-label="身份文本批量回执">
<div class="flex justify-between gap-3">
<h3 class="text-sm font-medium">{{ session.receipt.title }} · 本次回执</h3>
<NButton @click="exportReceipt" text size="small">导出诊断</NButton>
</div>
<p v-if="session.receipt.kind === 'identities'" class="mt-3 text-xs text-muted">
{{ session.receipt.result.total }} 个主体 · 目标
{{ session.receipt.result.targetCount }} · 生成 {{ session.receipt.result.generated }} ·
跳过 {{ session.receipt.result.skipped }}含锁定
{{ session.receipt.result.skippedLocked }}· 失败 {{ session.receipt.result.failed }}
</p>
<p v-else class="mt-3 text-xs text-muted">
{{ session.receipt.result.totalCharacters }} 个角色 · 缺候选
{{ session.receipt.result.missingAnchor }} · 本批目标
{{ session.receipt.result.targetCount }} · 已生成
{{ session.receipt.result.generated }} · 失败 {{ session.receipt.result.failed }}
</p>
<NAlert
v-if="session.receipt.result.failed"
role="alert"
type="error"
:show-icon="false"
class="mt-3"
>
本次存在部分失败成功结果不会回滚请检查逐角色原因后再重试
</NAlert>
<ul class="mt-3 space-y-2 text-xs text-danger">
<li v-for="failure in session.receipt.result.failures" :key="failure.subjectId">
{{ failure.subjectRef }} · {{ failure.subjectId }}{{ failure.error }}
</li>
</ul>
<p class="mt-3 text-[11px] text-muted">
回执仅保留于当前浏览器会话最终状态以刷新后的正式记录为准
</p>
</section>
</div></NCollapseItem
></NCollapse
></NScrollbar
>
<div v-if="subjects.length" class="identity-workspace">
<aside class="panel directory-panel identity-directory">
<div class="directory-heading">
@@ -360,16 +424,6 @@ watch(
v-model:value="search"
clearable
></NInput>
<NSelect
aria-label="身份主体类型"
v-model:value="module"
:options="[
{ label: String('全部类型'), value: 'all' },
{ label: String('人物'), value: 'character' },
{ label: String('场景'), value: 'scene' },
{ label: String('道具'), value: 'prop' }
]"
></NSelect>
</div>
<NScrollbar
class="identity-subject-list panel-scroll directory-scroll"