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
+138 -110
View File
@@ -1,4 +1,6 @@
<script setup lang="ts">
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
import { NAlert, NButton, NCheckbox, NCollapse, NCollapseItem, NInput, NInputNumber, NSelect, NTag } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { Search, RefreshCw, Download, ImagePlus, ArrowRight, AlertTriangle } from '@lucide/vue'
@@ -87,20 +89,23 @@ function resetFilters() {
</script>
<template>
<section 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>
</div>
<RouterLink :to="`/projects/${id}/storyboard`" class="text-button"
>去检查分镜参考图<ArrowRight :size="14"
/></RouterLink>
</div>
<p class="alert mt-5 text-xs">
Character 形态图会记录生成时使用的 Identity Anchor更换演员母版后旧主图会标记为身份母版已变更且后端会阻止它继续进入分镜首帧请基于当前母版重新生成候选并确认新的主参考图
<RouterLink :to="`/projects/${id}/subject-identity`" class="text-button ml-2">管理主体身份 </RouterLink>
</p>
<WorkspacePage
><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}/storyboard`" class="text-button"
>去检查分镜参考图<ArrowRight :size="14"
/></RouterLink></div></template
><NAlert type="info" :show-icon="false" class="mt-5 text-xs">
Character 形态图会记录生成时使用的 Identity
Anchor更换演员母版后旧主图会标记为身份母版已变更且后端会阻止它继续进入分镜首帧请基于当前母版重新生成候选并确认新的主参考图
<RouterLink :to="`/projects/${id}/subject-identity`" class="text-button ml-2"
>管理主体身份 </RouterLink
></NAlert
>
<div class="panel mt-5 p-5">
<div class="flex flex-wrap items-center justify-between gap-4">
<p class="text-sm">
@@ -110,17 +115,19 @@ function resetFilters() {
</span>
<span class="ml-3 text-xs text-muted">Seedream · 后端配置</span>
</p>
<button class="text-button" :disabled="query.loading.value" @click="query.refresh">
<RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新图库
</button>
<NButton :disabled="query.loading.value" @click="query.refresh" text size="small"
><RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新图库
</NButton>
</div>
<p class="mt-2 text-[11px] leading-6 text-muted">
6 秒更新图片记录主图供分镜引用候选图和历史失败记录均保留过期主图不会自动删除也不会被批量任务静默替换
6
秒更新图片记录主图供分镜引用候选图和历史失败记录均保留过期主图不会自动删除也不会被批量任务静默替换
</p>
<div v-if="staleForms.length" class="alert alert-error mt-4">
<div class="flex flex-wrap items-center justify-between gap-3">
<NAlert v-if="staleForms.length" type="error" :show-icon="false" class="mt-4"
><div class="flex flex-wrap items-center justify-between gap-3">
<span class="flex items-center gap-2 text-xs">
<AlertTriangle :size="14" />检测到 {{ staleForms.length }} Character Form 仍在使用旧 Identity Anchor
<AlertTriangle :size="14" />检测到 {{ staleForms.length }} Character Form 仍在使用旧 Identity
Anchor
</span>
<ConfirmAction
label="重新生成过期形态"
@@ -128,75 +135,78 @@ function resetFilters() {
acknowledgement
description="只为身份母版已经变化的 Character Form 调用 Seedream,各新增一张基于当前 Anchor 的候选图;不会自动替换旧主图,生成后请人工确认并设置新的主参考图。"
@confirm="generateStale"
/>
</div>
</div>
<details class="mt-4 border-t border-line pt-4">
<summary class="cursor-pointer text-xs font-medium">项目批量生图</summary>
<div class="mt-4 flex flex-wrap items-end gap-4">
<label class="w-28">
<span class="field-label">批量并发</span>
<input
id="image-concurrency"
v-model.number="concurrency"
type="number"
min="1"
step="1"
class="input"
/></div
></NAlert>
<NCollapse class="mt-4 border-t border-line pt-4"
><NCollapseItem name="details"
><template #header>项目批量生图</template>
<div class="mt-4 flex flex-wrap items-end gap-4">
<label class="w-28">
<span class="field-label">批量并发</span>
<NInputNumber
:disabled="operation.pending"
:input-props="{ id: 'image-concurrency' }"
:value="typeof concurrency === 'number' ? concurrency : null"
@update:value="concurrency = $event ?? 0"
:min="1"
:step="1"
></NInputNumber>
</label>
<NCheckbox
v-model:checked="force"
:disabled="operation.pending"
class="flex items-center gap-2 pb-2 text-xs"
>已有主图也新增候选图
</NCheckbox>
<ConfirmAction
:label="force ? '为全项目新增候选图' : '补齐项目主参考图'"
:disabled="blocked || !batchValid || !forms.length"
acknowledgement
:description="
force
? '对项目全部形态调用 Seedream 新增一张图片。已有主图不会被替换,生成后可在图片记录中手动选择主图。'
: '仅对没有主图的形态调用 Seedream 生图,已有主图的形态跳过。操作面向整个项目,不受列表筛选影响。'
"
@confirm="generateProject"
/>
</label>
<label class="flex items-center gap-2 pb-2 text-xs">
<input
v-model="force"
type="checkbox"
class="accent-accent"
:disabled="operation.pending"
/>已有主图也新增候选图
</label>
<ConfirmAction
:label="force ? '为全项目新增候选图' : '补齐项目主参考图'"
:disabled="blocked || !batchValid || !forms.length"
acknowledgement
:description="
force
? '对项目全部形态调用 Seedream 新增一张图片。已有主图不会被替换,生成后可在图片记录中手动选择主图。'
: '仅对没有主图的形态调用 Seedream 生图,已有主图的形态跳过。操作面向整个项目,不受列表筛选影响。'
"
@confirm="generateProject"
/>
</div>
<p v-if="!batchValid" class="mt-2 text-xs text-danger">并发必须是正整数</p>
<p class="mt-3 text-xs leading-6 text-muted">
作用于整个项目不受下方筛选影响批量使用后端默认尺寸与形态提示词不会启动视频生成
</p>
</details>
</div>
<p v-if="!batchValid" class="mt-2 text-xs text-danger">并发必须是正整数</p>
<p class="mt-3 text-xs leading-6 text-muted">
作用于整个项目不受下方筛选影响批量使用后端默认尺寸与形态提示词不会启动视频生成
</p></NCollapseItem
></NCollapse
>
</div>
<p v-if="query.error.value" class="alert alert-error mt-4" role="alert">
{{ query.error.value }}<span v-if="forms.length"> 当前展示上次读取的数据</span>
</p>
<p v-if="running" class="alert mt-4" role="status">
<NAlert v-if="query.error.value" role="alert" type="error" :show-icon="false" class="mt-4"
>{{ query.error.value }}<span v-if="forms.length"> 当前展示上次读取的数据</span></NAlert
><NAlert v-if="running" role="status" type="info" :show-icon="false" class="mt-4">
数据库中仍有排队或生成中的图片已暂停新的生图操作长时间无变化时请先核对后台任务刷新或关闭页面不会取消任务
</p>
</NAlert>
<section v-if="session.receipt" class="panel mt-4 p-5" aria-label="生图批量回执">
<div class="flex items-center justify-between gap-3">
<h3 class="text-sm font-medium">{{ session.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 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.failed }}
</p>
<p v-if="session.receipt.result.failed" class="alert alert-error mt-3" role="alert">
<NAlert v-if="session.receipt.result.failed" role="alert" type="error" :show-icon="false" class="mt-3">
部分形态生图失败已生成的图片保留先查看失败原因再按形态重新生图
</p>
<p v-else-if="session.receipt.title === '刷新过期人物形态图'" class="alert mt-3 text-xs">
</NAlert>
<NAlert
v-else-if="session.receipt.title === '刷新过期人物形态图'"
type="info"
:show-icon="false"
class="mt-3 text-xs"
>
新图当前仍是候选图请进入对应形态的查看图片与记录确认人物身份和造型后再设为主参考图旧主图在此之前仍保持过期状态
</p>
</NAlert>
<ul class="mt-3 space-y-2 text-xs text-danger">
<li v-for="failure in session.receipt.result.failures" :key="failure.subjectFormId">
<code>{{ failure.subjectFormId }}</code>{{ failure.error }}
<code>{{ failure.subjectFormId }}</code
>{{ failure.error }}
</li>
</ul>
<p class="mt-3 text-[11px] text-muted">回执仅保留于当前浏览器会话图片以刷新后的数据库记录为准</p>
@@ -204,20 +214,25 @@ function resetFilters() {
<div class="my-6 flex flex-wrap items-center gap-4">
<label class="search-field">
<Search :size="14" />
<input v-model="search" placeholder="搜索主体、引用或形态" aria-label="搜索形态图片" />
</label>
<select v-model="module" class="input w-auto" aria-label="筛选主体类型">
<option value="all">全部类型</option>
<option value="character">人物</option>
<option value="scene">场景</option>
<option value="prop">道具</option>
</select>
<label class="flex items-center gap-2 text-xs">
<input v-model="onlyMissing" type="checkbox" class="accent-accent" />仅看缺少主图
</label>
<label class="flex items-center gap-2 text-xs">
<input v-model="onlyStale" type="checkbox" class="accent-accent" />仅看身份过期
<NInput
placeholder="搜索主体、引用或形态"
:input-props="{ 'aria-label': '搜索形态图片' }"
v-model:value="search"
></NInput>
</label>
<NSelect
aria-label="筛选主体类型"
v-model:value="module"
:options="[
{ label: String('全部类型'), value: 'all' },
{ label: String('人物'), value: 'character' },
{ label: String('场景'), value: 'scene' },
{ label: String('道具'), value: 'prop' }
]"
class="select-control"
></NSelect>
<NCheckbox v-model:checked="onlyMissing" class="flex items-center gap-2 text-xs">仅看缺少主图 </NCheckbox>
<NCheckbox v-model:checked="onlyStale" class="flex items-center gap-2 text-xs">仅看身份过期 </NCheckbox>
<span class="ml-auto text-xs text-muted">{{ filtered.length }} 个形态</span>
</div>
<p v-if="!query.data.value && query.loading.value" class="py-10 text-sm text-muted" role="status">
@@ -241,7 +256,7 @@ function resetFilters() {
<code class="subject-ref">{{ form.subject.ref }}</code>
</div>
<p class="mt-2 text-xs">
{{ form.name }}<span v-if="form.isDefault" class="tag ml-2">默认形态</span>
{{ form.name }}<NTag v-if="form.isDefault" size="small" :bordered="false">默认形态</NTag>
</p>
<p v-if="form.description" class="mt-3 line-clamp-2 text-xs leading-6 text-muted">
{{ form.description }}
@@ -249,27 +264,42 @@ function resetFilters() {
<RouterLink
:to="{ path: `/projects/${id}/subject-identity`, query: { subjectId: form.subjectId } }"
class="text-button mt-3"
>查看主体身份与母版 </RouterLink>
>查看主体身份与母版 </RouterLink
>
<div class="mt-3 flex flex-wrap items-center gap-2">
<StatusBadge v-if="hasRunningImages(form.images)" status="generating" />
<span v-if="isPrimaryIdentityStale(form)" class="tag text-danger">身份母版已变更</span>
<span v-else-if="primaryImage(form.images)" class="tag">主参考图</span>
<span v-else-if="coverImage(form)" class="tag">候选图 · 尚未设主图</span>
<span v-else-if="form.images[0]?.status === 'failed'" class="text-xs text-danger">最近一次生成失败</span>
<NTag v-if="isPrimaryIdentityStale(form)" size="small" :bordered="false">身份母版已变更</NTag>
<NTag v-else-if="primaryImage(form.images)" size="small" :bordered="false">主参考图</NTag>
<NTag v-else-if="coverImage(form)" size="small" :bordered="false">候选图 · 尚未设主图</NTag>
<span v-else-if="form.images[0]?.status === 'failed'" class="text-xs text-danger"
>最近一次生成失败</span
>
<span class="text-[10px] text-muted">{{ form.images.length }} 条记录</span>
</div>
<p v-if="isPrimaryIdentityStale(form)" class="alert alert-error mt-3 text-xs leading-6">
当前主图由旧演员母版生成后续分镜已禁止继续引用请基于当前 Identity Anchor 重新生成候选图并在确认后设置为新的主参考图
</p>
<NAlert
v-if="isPrimaryIdentityStale(form)"
type="error"
:show-icon="false"
class="mt-3 text-xs leading-6"
>
当前主图由旧演员母版生成后续分镜已禁止继续引用请基于当前 Identity Anchor
重新生成候选图并在确认后设置为新的主参考图
</NAlert>
<p v-if="form.images[0]?.status === 'failed'" class="mt-2 line-clamp-2 text-xs text-danger">
{{ form.images[0].error }}
</p>
<div class="mt-4 flex flex-wrap items-center justify-between gap-2">
<button class="text-button" @click="openGallery(form)">查看图片与记录</button>
<button class="button button-secondary" :disabled="blocked" @click="openGenerate(form)">
<ImagePlus :size="14" />
{{ isPrimaryIdentityStale(form) ? '基于当前母版重新生成' : coverImage(form) ? '再生成一张' : '生成图片' }}
</button>
<NButton @click="openGallery(form)" text size="small">查看图片与记录</NButton>
<NButton :disabled="blocked" @click="openGenerate(form)"
><ImagePlus :size="14" />
{{
isPrimaryIdentityStale(form)
? '基于当前母版重新生成'
: coverImage(form)
? '再生成一张'
: '生成图片'
}}</NButton
>
</div>
</div>
</article>
@@ -283,21 +313,19 @@ function resetFilters() {
: '先完成剧本拆解和主体形态持久化,再来生成参考图。'
"
>
<button v-if="forms.length" class="button button-secondary" @click="resetFilters">清除筛选</button>
<RouterLink v-else :to="`/projects/${id}/breakdown`" class="button button-secondary">前往剧本拆解</RouterLink>
</EmptyState>
<GenerateImageDialog
<NButton v-if="forms.length" @click="resetFilters">清除筛选</NButton>
<RouterLink v-else :to="`/projects/${id}/breakdown`" class="button button-secondary"
>前往剧本拆解</RouterLink
> </EmptyState
><GenerateImageDialog
v-model:open="generateOpen"
:form="generateForm"
:disabled="blocked || !generateForm"
@generate="submitGenerate"
/>
<ImageGalleryDialog
@generate="submitGenerate" /><ImageGalleryDialog
v-model:open="galleryOpen"
:project-id="id"
:form="galleryForm"
:disabled="blocked || !galleryForm"
@changed="query.refresh"
/>
</section>
</template>
/></WorkspacePage>
</template>