feat: 同步精简接口并统一表单校验

This commit is contained in:
GJ
2026-09-10 22:34:47 +08:00
parent 82d7bde631
commit 2eea0ea72e
66 changed files with 1611 additions and 1100 deletions
+58 -119
View File
@@ -1,4 +1,7 @@
<script setup lang="ts">
import AppForm from '../../components/ui/AppForm.vue'
import { NFormItem } from 'naive-ui'
import { integerRule } from '../../lib/form-rules'
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
import ActionMenu from '../../components/ui/ActionMenu.vue'
import DetailDisclosure from '../../components/ui/DetailDisclosure.vue'
@@ -27,14 +30,13 @@ import {
ArrowRight,
LayoutGrid,
Columns3,
Settings2,
AlertTriangle
Settings2
} from '@lucide/vue'
import { AssetImage, EmptyState, StatusBadge } from '../../components/ui'
import { downloadText } from '../../lib/format'
import ConfirmAction from '../workflows/ConfirmAction.vue'
import { useSubjectImages } from './useSubjectImages'
import { coverImage, hasRunningImages, isPrimaryIdentityStale, primaryImage } from './model'
import { coverImage, hasRunningImages, primaryImage } from './model'
import type { GenerateFormImageInput, SubjectFormAsset } from './types'
import GenerateImageDialog from './components/GenerateImageDialog.vue'
import ImageGalleryDialog from './components/ImageGalleryDialog.vue'
@@ -50,7 +52,6 @@ const router = useRouter()
const {
id,
forms,
staleForms,
query,
refreshProject,
operation,
@@ -60,15 +61,13 @@ const {
limit,
promptConcurrency,
promptForce,
concurrencyValid,
generatePrompt,
generatePrompts,
blocked,
running,
batchValid,
generate,
generateProject,
generateStale
generateProject
} = useSubjectImages()
const sourceShotId = computed(() => queryText(route.query.sourceShotId))
const targetFormId = computed(() => queryText(route.query.subjectFormId))
@@ -149,12 +148,11 @@ const search = ref('')
const layout = ref<GalleryLayout>('grid')
const module = ref('all')
/** 形态图片状态筛选;单选下拉避免移动端复选项占据两行。 */
type ImageStatusFilter = 'all' | 'missing' | 'stale'
type ImageStatusFilter = 'all' | 'missing'
const statusFilter = ref<ImageStatusFilter>('all')
const statusFilterOptions: Array<{ label: string; value: ImageStatusFilter }> = [
{ label: '全部状态', value: 'all' },
{ label: '缺少主图', value: 'missing' },
{ label: '身份过期', value: 'stale' }
{ label: '缺少主图', value: 'missing' }
]
/** 保存每张卡片的尺寸观察器,切换回网格时及时释放。 */
const masonryCardObservers = new WeakMap<HTMLElement, ResizeObserver>()
@@ -241,7 +239,6 @@ const filtered = computed(() =>
(!targetFormId.value || form.id === targetFormId.value) &&
(module.value === 'all' || form.subject.module === module.value) &&
(statusFilter.value !== 'missing' || !primaryImage(form.images)) &&
(statusFilter.value !== 'stale' || isPrimaryIdentityStale(form)) &&
`${form.subject.name} ${form.subject.ref} ${form.name}`
.toLowerCase()
.includes(search.value.trim().toLowerCase())
@@ -305,16 +302,8 @@ function selectImpactShot(shotId: string | null) {
async function refreshAssets() {
await Promise.all([refreshProject(), query.refresh(), impact.query.refresh(), impact.references.refresh()])
}
/** 全局过期筛选不沿用深链接的单个素材限制。 */
async function showStaleForms() {
await router.replace({
query: { ...route.query, subjectFormId: undefined, subjectRef: undefined, sourceShotId: undefined }
})
search.value = ''
module.value = 'all'
statusFilter.value = 'stale'
}
const formModel = computed(() => ({ concurrency: concurrency.value, limit: limit.value }))
const rules = { concurrency: integerRule('批量并发'), limit: integerRule('本批上限', 1, true) }
</script>
<template>
@@ -340,7 +329,7 @@ async function showStaleForms() {
</div>
<DetailDisclosure title="母版继承与过期处理" class="mt-4">
人物场景道具形态图均可继承已锁定 Identity
的母版并记录本次引用来源母版变化后可筛选过期重新生成候选并人工确认新的主参考不要直接沿用旧资产进入下游
的母版母版变化后请核对形态按需生成候选并确认主当前图库不提供历史母版引用无法自动判断形态图是否过期下游状态以镜头就绪检查为准
<RouterLink :to="`/projects/${id}/subject-identity`" class="text-button ml-2"
>管理主体身份 </RouterLink
></DetailDisclosure
@@ -358,76 +347,61 @@ async function showStaleForms() {
<div class="flex flex-wrap items-center justify-between gap-4">
<p class="text-sm">
<strong>{{ completeCount }}</strong> / {{ forms.length }} 个形态已有主图
<span v-if="staleForms.length" class="ml-3 text-xs text-danger">
{{ staleForms.length }} 个形态主图已过期
</span>
<span class="ml-3 text-xs text-muted">图片模型 · 后端配置</span>
</p>
</div>
<p class="mt-2 text-[11px] leading-6 text-muted">
图库不自动刷新可点击刷新图库读取最新记录主图供分镜引用候选图和历史失败记录均保留过期主图不会自动删除或替换
</p>
<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 }} 个形态 仍在使用旧 Identity
Anchor
</span>
<ConfirmAction
label="重新生成过期形态"
:disabled="blocked || !concurrencyValid || !staleForms.length"
acknowledgement
description="只为身份母版已经变化的形态调用 图片模型,各新增一张基于当前已锁定母版的候选图;不会自动替换旧主图,生成后请人工确认并设置新的主参考图。"
@confirm="generateStale"
/></div
></NAlert>
<NCollapse class="mt-4 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
<AppForm :model="formModel" :rules="rules" :disabled="operation.pending" validate-on-change>
<div class="form-controls mt-4 flex flex-wrap gap-4">
<NFormItem
class="w-28"
path="concurrency"
label="批量并发"
:label-props="{ for: 'image-concurrency' }"
>
<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>
</NFormItem>
<NFormItem class="w-36" path="limit" label="本批上限(可选)"
><NInputNumber
:disabled="operation.pending"
:value="typeof limit === 'number' ? limit : null"
@update:value="limit = $event ?? ''"
:min="1"
:step="1"
placeholder="不限制"
:input-props="{ 'aria-label': '生图本批上限' }"
/></NFormItem>
<NCheckbox
v-model:checked="force"
: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>
<label class="w-36"
><span class="field-label">本批上限可选</span
><NInputNumber
:disabled="operation.pending"
:value="typeof limit === 'number' ? limit : null"
@update:value="limit = $event ?? ''"
:min="1"
:step="1"
placeholder="不限制"
:input-props="{ 'aria-label': '生图本批上限' }"
/></label>
<NCheckbox
v-model:checked="force"
:disabled="operation.pending"
class="control-row-checkbox text-xs"
>已有主图也新增候选图
</NCheckbox>
<ConfirmAction
:label="force ? '为全项目新增候选图' : '补齐项目主参考图'"
:disabled="blocked || !batchValid || !forms.length"
acknowledgement
:description="
force
? '对项目全部形态调用图片模型新增一张图片。已有主图不会被替换,生成后可在图片记录中手动选择主图。'
: '仅对没有主图的形态调用图片模型生图,已有主图的形态跳过。操作面向整个项目,不受列表筛选影响。'
"
@confirm="generateProject"
/>
</div>
<p v-if="!batchValid" class="mt-2 text-xs text-danger">
并发和填写的数量上限必须是正整数
</p>
class="control-row-checkbox text-xs"
>已有主图也新增候选图
</NCheckbox>
<ConfirmAction
:label="force ? '为全项目新增候选图' : '补齐项目主参考图'"
:disabled="blocked || !batchValid || !forms.length"
acknowledgement
:description="
force
? '对项目全部形态调用图片模型新增一张图片。已有主图不会被替换,生成后可在图片记录中手动选择主图。'
: '仅对没有主图的形态调用图片模型生图,已有主图的形态跳过。操作面向整个项目,不受列表筛选影响。'
"
@confirm="generateProject"
/>
</div>
</AppForm>
<p class="mt-3 text-xs leading-6 text-muted">
作用于整个项目不受下方筛选影响上限只限制本次真正生图数量其余显示为待后续处理不计入跳过默认使用后端尺寸与正式提示词不会启动视频生成
</p></NCollapseItem
@@ -457,14 +431,6 @@ async function showStaleForms() {
>
部分形态生图失败已生成的图片保留先查看失败原因再按形态重新生图
</NAlert>
<NAlert
v-else-if="session.receipt.title === '刷新过期形态图'"
type="info"
:show-icon="false"
class="mt-3 text-xs"
>
新图当前仍是候选图请进入对应形态的查看图片与记录确认人物身份和造型后再设为主参考图旧主图在此之前仍保持过期状态
</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
@@ -477,11 +443,6 @@ async function showStaleForms() {
</section>
</WorkspaceTools>
<!-- 关联说明筛选和图片共用正文滚动区避免镜头切换时两块滚动区域高度不同步 -->
<NAlert v-if="staleForms.length" type="error" :show-icon="false" class="mt-3" title="形态主图需要更新">
{{ staleForms.length }}
个形态主图与当前已锁定身份母版不一致请生成候选验图并切换主图再重建相关首帧
<NButton text size="small" @click="showStaleForms">筛选身份过期形态</NButton>
</NAlert>
<NAlert
v-if="impact.stale.value.length"
type="warning"
@@ -730,7 +691,6 @@ async function showStaleForms() {
:key="form.id"
class="panel form-image-card"
:class="{
'ring-1 ring-danger': isPrimaryIdentityStale(form),
'form-image-card-focused': targetFormId === form.id
}"
:data-form-id="form.id"
@@ -755,9 +715,6 @@ async function showStaleForms() {
</p>
<div class="mt-3 flex flex-wrap items-center gap-2">
<StatusBadge v-if="hasRunningImages(form.images)" status="generating" />
<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"
@@ -765,15 +722,6 @@ async function showStaleForms() {
>
<span class="text-[10px] text-muted">{{ form.images.length }} 条记录</span>
</div>
<NAlert
v-if="isPrimaryIdentityStale(form)"
type="error"
:show-icon="false"
class="mt-3 text-xs leading-6"
>
当前主图未能匹配最新已锁定母版请基于当前 Identity Anchor
重新生成候选图并在确认后设置为新的主参考图
</NAlert>
<NAlert
v-if="
sourceShot?.primaryKeyframeStale &&
@@ -789,9 +737,7 @@ async function showStaleForms() {
{{
sourceShot.inconsistent
? '后端首帧与视频检查结果不一致,请先核对,不要据此重复生图。'
: isPrimaryIdentityStale(form)
? '请先更新该形态主图,再重建首帧。'
: '关联不等于本素材失效;核对形态主图与身份母版,确认无误后只需重建首帧。'
: '关联不等于本素材失效;核对形态主图与身份母版,确认无误后只需重建首帧。'
}}
<RouterLink v-if="sourceLocation" :to="sourceLocation" class="text-button mt-2"
>返回该镜头处理 →</RouterLink
@@ -818,14 +764,7 @@ async function showStaleForms() {
"
/>
<NButton :disabled="blocked" @click="openGenerate(form)"
><ImagePlus :size="14" />
{{
isPrimaryIdentityStale(form)
? '基于当前母版重新生成'
: coverImage(form)
? '再生成一张'
: '生成图片'
}}</NButton
><ImagePlus :size="14" /> {{ coverImage(form) ? '再生成一张' : '生成图片' }}</NButton
>
</div>
</div>