feat: 同步精简接口并统一表单校验
This commit is contained in:
@@ -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 WorkspaceTools from '../../components/ui/WorkspaceTools.vue'
|
||||
import {
|
||||
@@ -155,6 +158,8 @@ watch(
|
||||
}
|
||||
}
|
||||
)
|
||||
const formModel = computed(() => ({ concurrency: concurrency.value, candidateLimit: candidateLimit.value }))
|
||||
const rules = { concurrency: integerRule('并发数'), candidateLimit: integerRule('本批上限') }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -251,64 +256,73 @@ watch(
|
||||
<dd>{{ casting.ready }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="mt-5 flex flex-wrap items-end gap-4 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="control-row-checkbox 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 调用 图片模型,各自独立生成一张停用的 primary 候选,不复用旧身份母版。已有候选、已有母版或已完成选角的角色不会重复处理。"
|
||||
@confirm="generateCastingCandidates"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-if="!batchValid || !candidateLimitValid"
|
||||
class="mt-3 text-xs text-danger"
|
||||
<AppForm
|
||||
:model="formModel"
|
||||
:rules="rules"
|
||||
:disabled="operation.pending"
|
||||
validate-on-change
|
||||
>
|
||||
并发数与本批上限都必须是正整数。
|
||||
</p>
|
||||
<div class="form-controls mt-5 flex flex-wrap gap-4 pt-4">
|
||||
<NFormItem class="w-28" path="concurrency" label="并发数">
|
||||
<NInputNumber
|
||||
:input-props="{ 'aria-label': '并发数' }"
|
||||
:disabled="operation.pending"
|
||||
:value="
|
||||
typeof concurrency === 'number' ? concurrency : null
|
||||
"
|
||||
@update:value="concurrency = $event ?? 0"
|
||||
:min="1"
|
||||
:step="1"
|
||||
></NInputNumber>
|
||||
</NFormItem>
|
||||
<NFormItem class="w-28" path="candidateLimit" label="本批上限">
|
||||
<NInputNumber
|
||||
:input-props="{ 'aria-label': '本批上限' }"
|
||||
:disabled="operation.pending"
|
||||
:value="
|
||||
typeof candidateLimit === 'number'
|
||||
? candidateLimit
|
||||
: null
|
||||
"
|
||||
@update:value="candidateLimit = $event ?? 0"
|
||||
:min="1"
|
||||
:step="1"
|
||||
></NInputNumber>
|
||||
</NFormItem>
|
||||
<NCheckbox
|
||||
v-model:checked="force"
|
||||
:disabled="operation.pending"
|
||||
class="control-row-checkbox 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 调用 图片模型,各自独立生成一张停用的 primary 候选,不复用旧身份母版。已有候选、已有母版或已完成选角的角色不会重复处理。"
|
||||
@confirm="generateCastingCandidates"
|
||||
/>
|
||||
</div>
|
||||
</AppForm>
|
||||
<div class="casting-list" role="group" aria-label="角色选角进度">
|
||||
<NButton
|
||||
v-for="item in casting.items"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import AppForm from '../../../components/ui/AppForm.vue'
|
||||
import { NFormItem } from 'naive-ui'
|
||||
import { sizeRules } from '../../../lib/form-rules'
|
||||
import { NAlert, NButton, NCheckbox, NInput, NInputNumber } from 'naive-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { AppDialog } from '../../../components/ui'
|
||||
@@ -33,17 +36,31 @@ function submit() {
|
||||
})
|
||||
open.value = false
|
||||
}
|
||||
const formModel = computed(() => ({ width: width.value, height: height.value, prompt: prompt.value }))
|
||||
const rules = { ...sizeRules(() => formModel.value) }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppDialog v-model:open="open" title="生成角色选角候选" :description="`${subjectName} · 候选不会自动成为身份母版`">
|
||||
<form class="mt-5 space-y-4" @submit.prevent="submit">
|
||||
<AppForm
|
||||
:model="formModel"
|
||||
:rules="rules"
|
||||
:disabled="disabled"
|
||||
:reset-key="`${open}:${subjectId}`"
|
||||
validate-on-change
|
||||
class="mt-5 space-y-4"
|
||||
@submit="submit"
|
||||
>
|
||||
<NAlert type="info" :show-icon="false" class="text-xs">
|
||||
后端会依据角色 Identity、项目视觉风格和默认选角约束独立生成一张 primary
|
||||
候选,不会复用当前身份母版。生成完成后仍需人工点击“确认选角”。
|
||||
</NAlert>
|
||||
<label class="block">
|
||||
<span class="field-label">覆盖本次完整提示词(可选)</span>
|
||||
<NFormItem
|
||||
class="block"
|
||||
path="prompt"
|
||||
label="覆盖本次完整提示词(可选)"
|
||||
:label-props="{ for: 'casting-candidate-prompt' }"
|
||||
>
|
||||
<NInput
|
||||
placeholder="建议留空,使用后端编译的稳定身份和选角约束;未明确人物背景时默认中国人物"
|
||||
:input-props="{ id: 'casting-candidate-prompt' }"
|
||||
@@ -52,31 +69,30 @@ function submit() {
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 12 }"
|
||||
></NInput>
|
||||
</label>
|
||||
</NFormItem>
|
||||
<p v-if="prompt.trim()" class="text-xs text-danger">
|
||||
自定义文本会替换后端完整提示词,请自行包含人物身份、基础服装和中性参考图要求。
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<label
|
||||
><span class="field-label">宽度(px)</span
|
||||
<NFormItem path="width" label="宽度(px)"
|
||||
><NInputNumber
|
||||
:input-props="{ 'aria-label': '宽度(px)' }"
|
||||
placeholder="后端默认"
|
||||
:value="typeof width === 'number' ? width : null"
|
||||
@update:value="width = $event ?? ''"
|
||||
:min="1"
|
||||
></NInputNumber
|
||||
></label>
|
||||
<label
|
||||
><span class="field-label">高度(px)</span
|
||||
></NFormItem>
|
||||
<NFormItem path="height" label="高度(px)"
|
||||
><NInputNumber
|
||||
:input-props="{ 'aria-label': '高度(px)' }"
|
||||
placeholder="后端默认"
|
||||
:value="typeof height === 'number' ? height : null"
|
||||
@update:value="height = $event ?? ''"
|
||||
:min="1"
|
||||
></NInputNumber
|
||||
></label>
|
||||
></NFormItem>
|
||||
</div>
|
||||
<p v-if="!valid" class="text-xs text-danger">宽高需要同时留空,或同时填写正整数。</p>
|
||||
<NCheckbox
|
||||
id="casting-candidate-cost"
|
||||
v-model:checked="acknowledged"
|
||||
@@ -89,6 +105,6 @@ function submit() {
|
||||
确认生成候选
|
||||
</NButton>
|
||||
</div>
|
||||
</form>
|
||||
</AppForm>
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import AppForm from '../../../components/ui/AppForm.vue'
|
||||
import { NFormItem } from 'naive-ui'
|
||||
import DetailDisclosure from '../../../components/ui/DetailDisclosure.vue'
|
||||
import { NButton, NCheckbox, NInput } from 'naive-ui'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
@@ -32,10 +34,13 @@ function save() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="mt-5" @submit.prevent="save">
|
||||
<AppForm :model="form" :disabled="disabled" class="mt-5" @submit="save">
|
||||
<fieldset :disabled="disabled" class="space-y-4">
|
||||
<label class="block"
|
||||
><span class="field-label">稳定身份描述</span
|
||||
<NFormItem
|
||||
class="block"
|
||||
path="description"
|
||||
label="稳定身份描述"
|
||||
:label-props="{ for: 'identity-description' }"
|
||||
><NInput
|
||||
:disabled="disabled"
|
||||
placeholder="只描述跨形态不变的面部、结构、材质等特征"
|
||||
@@ -45,9 +50,12 @@ function save() {
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 12 }"
|
||||
></NInput>
|
||||
</label>
|
||||
<label class="block"
|
||||
><span class="field-label">身份核心提示词(稳定事实)</span
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
class="block"
|
||||
path="generationPrompt"
|
||||
label="身份核心提示词(稳定事实)"
|
||||
:label-props="{ for: 'identity-prompt' }"
|
||||
><NInput
|
||||
:disabled="disabled"
|
||||
placeholder="不固定某一形态的服装、伤势或临时状态"
|
||||
@@ -57,7 +65,7 @@ function save() {
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 12 }"
|
||||
></NInput>
|
||||
</label>
|
||||
</NFormItem>
|
||||
<DetailDisclosure title="身份填写与锁定规则"
|
||||
><p class="text-xs leading-6 text-muted">
|
||||
{{
|
||||
@@ -87,5 +95,5 @@ function save() {
|
||||
><NButton :disabled="disabled" type="primary" attr-type="submit">保存主体身份</NButton>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</AppForm>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { NScrollbar, NAlert, NButton, NCollapse, NCollapseItem, NTag } from 'naive-ui'
|
||||
import { NScrollbar, NAlert, NButton, NTag } from 'naive-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { AssetImage, StatusBadge } from '../../../components/ui'
|
||||
import { referenceImageUrl } from '../../../lib/assets'
|
||||
import { formatDate } from '../../../lib/format'
|
||||
import { canBeAnchor, currentAnchor, identityViewLabels, readImageProvenance } from '../model'
|
||||
import { canBeAnchor, currentAnchor, identityViewLabels } from '../model'
|
||||
import type { IdentityImage } from '../types'
|
||||
|
||||
/** 身份图库区分权威母版、primary 候选和辅助视图,保留后端原始状态。 */
|
||||
@@ -104,9 +104,7 @@ function chooseAnchor() {
|
||||
? '母版候选'
|
||||
: identityViewLabels[image.viewType]
|
||||
}}</span>
|
||||
<span class="image-history-meta"
|
||||
><StatusBadge :status="image.status" /><span>{{ image.enabled ? '启用' : '停用' }}</span></span
|
||||
>
|
||||
<span class="image-history-meta"><StatusBadge :status="image.status" /></span>
|
||||
</NButton>
|
||||
</NScrollbar>
|
||||
<div class="mt-3 flex flex-wrap items-center justify-between gap-3">
|
||||
@@ -118,10 +116,7 @@ function chooseAnchor() {
|
||||
? '母版候选'
|
||||
: identityViewLabels[selected.viewType]
|
||||
}}</NTag
|
||||
><span class="text-xs text-muted"
|
||||
>{{ selected.enabled ? '启用' : '停用' }} · {{ selected.width || '—' }} ×
|
||||
{{ selected.height || '—' }}</span
|
||||
>
|
||||
><span class="text-xs text-muted">{{ selected.width || '—' }} × {{ selected.height || '—' }}</span>
|
||||
</div>
|
||||
<a
|
||||
v-if="imageUrl && selected.status === 'completed'"
|
||||
@@ -163,19 +158,6 @@ function chooseAnchor() {
|
||||
><NButton @click="confirming = false" text size="small">取消</NButton>
|
||||
</div></NAlert
|
||||
>
|
||||
<!-- 默认展示实际提示词;用户手动收起后,轮询不会强制重新展开。 -->
|
||||
<NCollapse v-if="selected.prompt" :default-expanded-names="['details']" class="mt-4 text-xs"
|
||||
><NCollapseItem name="details"
|
||||
><template #header>本次实际提示词</template>
|
||||
<p class="mt-3 whitespace-pre-wrap leading-6">{{ selected.prompt }}</p></NCollapseItem
|
||||
></NCollapse
|
||||
>
|
||||
<p
|
||||
v-if="readImageProvenance(selected.rawJson).referenceImageId"
|
||||
class="mt-3 break-all text-[11px] text-muted"
|
||||
>
|
||||
本次锚定图片 ID:{{ readImageProvenance(selected.rawJson).referenceImageId }}
|
||||
</p>
|
||||
<p class="mt-2 break-all font-mono text-[10px] text-muted">Identity image ID · {{ selected.id }}</p>
|
||||
</template>
|
||||
<p v-else class="py-8 text-sm text-muted">尚无身份参考图。先保存身份提示词,再生成第一张母版。</p>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import AppForm from '../../../components/ui/AppForm.vue'
|
||||
import { NFormItem } from 'naive-ui'
|
||||
import { sizeRules, fieldRule } from '../../../lib/form-rules'
|
||||
import { NAlert, NButton, NCheckbox, NInput, NInputNumber, NSelect } from 'naive-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { AppDialog } from '../../../components/ui'
|
||||
@@ -56,26 +59,51 @@ function submit() {
|
||||
})
|
||||
open.value = false
|
||||
}
|
||||
const formModel = computed(() => ({
|
||||
width: width.value,
|
||||
height: height.value,
|
||||
prompt: prompt.value,
|
||||
viewType: viewType.value,
|
||||
referenceImageId: referenceImageId.value,
|
||||
availableReferenceIds: references.value.map(image => image.id)
|
||||
}))
|
||||
const rules = {
|
||||
...sizeRules(() => formModel.value),
|
||||
referenceImageId: fieldRule(
|
||||
value => !value || references.value.some(image => image.id === value),
|
||||
'请选择此主体当前可用的参考图'
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppDialog v-model:open="open" title="生成身份参考图" :description="subjectName">
|
||||
<form class="mt-5 space-y-4" @submit.prevent="submit">
|
||||
<AppForm
|
||||
:model="formModel"
|
||||
:rules="rules"
|
||||
:disabled="disabled"
|
||||
:reset-key="`${open}:${subjectId}`"
|
||||
validate-on-change
|
||||
class="mt-5 space-y-4"
|
||||
@submit="submit"
|
||||
>
|
||||
<NAlert type="info" :show-icon="false" class="text-xs">
|
||||
调用 图片模型,每次新增一张图片,可能产生费用。第一张成功的 primary 图自动成为母版;已有母版时,新
|
||||
primary 图仅作为停用候选保留。
|
||||
</NAlert>
|
||||
<label class="block"
|
||||
><span class="field-label">参考视角</span
|
||||
<NFormItem class="block" path="viewType" label="参考视角" :label-props="{ for: 'identity-view' }"
|
||||
><NSelect
|
||||
id="identity-view"
|
||||
v-model:value="viewType"
|
||||
:options="allowedViews.map(([value, label]) => ({ label: label + '(' + value + ')', value }))"
|
||||
class="select-control"
|
||||
></NSelect
|
||||
></label>
|
||||
<label class="block"
|
||||
><span class="field-label">身份锚定来源</span
|
||||
></NFormItem>
|
||||
<NFormItem
|
||||
class="block"
|
||||
path="referenceImageId"
|
||||
label="身份锚定来源"
|
||||
:label-props="{ for: 'identity-reference' }"
|
||||
><NSelect
|
||||
id="identity-reference"
|
||||
v-model:value="referenceImageId"
|
||||
@@ -91,12 +119,15 @@ function submit() {
|
||||
]"
|
||||
class="select-control"
|
||||
></NSelect
|
||||
></label>
|
||||
></NFormItem>
|
||||
<p class="text-xs leading-6 text-muted">
|
||||
只能选择此主体已完成的图片。后端需取得 Provider 可访问的远程地址;本地预览可见不保证远程地址仍有效。
|
||||
</p>
|
||||
<label class="block"
|
||||
><span class="field-label">覆盖本次完整提示词(可选)</span
|
||||
<NFormItem
|
||||
class="block"
|
||||
path="prompt"
|
||||
label="覆盖本次完整提示词(可选)"
|
||||
:label-props="{ for: 'identity-image-prompt' }"
|
||||
><NInput
|
||||
placeholder="建议留空,使用后端编译的身份约束和视角要求"
|
||||
:input-props="{ id: 'identity-image-prompt' }"
|
||||
@@ -105,13 +136,12 @@ function submit() {
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 12 }"
|
||||
></NInput>
|
||||
</label>
|
||||
</NFormItem>
|
||||
<p v-if="prompt.trim()" class="text-xs text-danger">
|
||||
自定义文本会替换后端编译的完整提示词,请自行包含身份约束和视角要求。
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<label
|
||||
><span class="field-label">宽度(px)</span
|
||||
<NFormItem path="width" label="宽度(px)" :label-props="{ for: 'identity-width' }"
|
||||
><NInputNumber
|
||||
placeholder="后端默认"
|
||||
:input-props="{ id: 'identity-width' }"
|
||||
@@ -119,9 +149,8 @@ function submit() {
|
||||
@update:value="width = $event ?? ''"
|
||||
:min="1"
|
||||
:step="1"
|
||||
></NInputNumber></label
|
||||
><label
|
||||
><span class="field-label">高度(px)</span
|
||||
></NInputNumber></NFormItem
|
||||
><NFormItem path="height" label="高度(px)" :label-props="{ for: 'identity-height' }"
|
||||
><NInputNumber
|
||||
placeholder="后端默认"
|
||||
:input-props="{ id: 'identity-height' }"
|
||||
@@ -130,9 +159,8 @@ function submit() {
|
||||
:min="1"
|
||||
:step="1"
|
||||
></NInputNumber
|
||||
></label>
|
||||
></NFormItem>
|
||||
</div>
|
||||
<p v-if="!valid" class="text-xs text-danger" role="alert">尺寸须成对填写正整数;指定的参考图必须仍可用。</p>
|
||||
<NCheckbox
|
||||
id="identity-image-cost"
|
||||
v-model:checked="acknowledged"
|
||||
@@ -145,6 +173,6 @@ function submit() {
|
||||
确认生成身份图
|
||||
</NButton>
|
||||
</div>
|
||||
</form>
|
||||
</AppForm>
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** 主体身份模块公共入口。 */
|
||||
export { subjectIdentityApi } from './api'
|
||||
export { currentAnchor, readImageProvenance } from './model'
|
||||
export { currentAnchor } from './model'
|
||||
export type { SubjectIdentity, IdentityImage, GenerateIdentityImageInput } from './types'
|
||||
|
||||
@@ -67,28 +67,9 @@ export function canBeAnchor(image: IdentityImage): boolean {
|
||||
return image.viewType === 'primary' && image.status === 'completed' && !!image.imageUrl
|
||||
}
|
||||
|
||||
/** 使用后端 isAnchor,不把 front/full-body 的 enabled 错当成母版。 */
|
||||
/** 使用后端明确的 isAnchor 标记,并校验图片已完成且为 primary 视角。 */
|
||||
export function currentAnchor(images: IdentityImage[]): IdentityImage | undefined {
|
||||
return images.find(image => image.isAnchor && image.enabled && canBeAnchor(image))
|
||||
}
|
||||
|
||||
/** 仅读取追溯所需 ID,不把原始 Provider 数据渲染为 HTML。 */
|
||||
export function readImageProvenance(rawJson?: string | null): {
|
||||
referenceImageId?: string
|
||||
identityAnchorImageId?: string
|
||||
} {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(rawJson || '{}')
|
||||
if (!parsed || typeof parsed !== 'object') return {}
|
||||
const value = parsed as Record<string, unknown>
|
||||
return {
|
||||
referenceImageId: typeof value.referenceImageId === 'string' ? value.referenceImageId : undefined,
|
||||
identityAnchorImageId:
|
||||
typeof value.identityAnchorImageId === 'string' ? value.identityAnchorImageId : undefined
|
||||
}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
return images.find(image => image.isAnchor && canBeAnchor(image))
|
||||
}
|
||||
|
||||
/** 批量回执按项目保存在当前会话,切页不丢失,刷新浏览器后不伪造恢复。 */
|
||||
|
||||
@@ -8,7 +8,6 @@ export function identityFixture(overrides: Partial<SubjectIdentity> = {}): Subje
|
||||
description: '稳定面部特征',
|
||||
generationPrompt: '保持相同五官与骨相',
|
||||
isLocked: false,
|
||||
images: [],
|
||||
createdAt: '2026-08-28T00:00:00Z',
|
||||
updatedAt: '2026-08-28T00:00:00Z',
|
||||
...overrides
|
||||
@@ -24,12 +23,10 @@ export function identityImageFixture(overrides: Partial<IdentityImage> = {}): Id
|
||||
viewType: 'primary',
|
||||
provider: 'seedream',
|
||||
model: 'configured-model',
|
||||
prompt: '<script>不执行</script>',
|
||||
imageUrl: '/storage/identity.png',
|
||||
width: 2048,
|
||||
height: 2048,
|
||||
status: 'completed',
|
||||
enabled: true,
|
||||
isAnchor: true,
|
||||
error: null,
|
||||
createdAt: '2026-08-28T00:00:00Z',
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { SubjectImageStatus, SubjectFormAsset } from '../subject-images/typ
|
||||
/** 只有 primary 视图可以选为身份母版,其余视图是辅助参考。 */
|
||||
export type IdentityViewType = 'primary' | 'front' | 'three-quarter' | 'full-body'
|
||||
|
||||
/** 身份参考图;isAnchor 仅由专用图片查询接口返回,不能用 enabled 代替。 */
|
||||
/** 身份参考图公开 DTO;查询与变更均返回明确的 isAnchor 标记。 */
|
||||
export interface IdentityImage {
|
||||
id: string
|
||||
identityId: string
|
||||
@@ -11,15 +11,12 @@ export interface IdentityImage {
|
||||
viewType: IdentityViewType
|
||||
provider: string | null
|
||||
model: string | null
|
||||
prompt: string | null
|
||||
imageUrl: string | null
|
||||
width: number | null
|
||||
height: number | null
|
||||
status: SubjectImageStatus
|
||||
enabled: boolean
|
||||
isAnchor?: boolean
|
||||
isAnchor: boolean
|
||||
error: string | null
|
||||
rawJson?: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
@@ -31,7 +28,6 @@ export interface SubjectIdentity {
|
||||
description: string | null
|
||||
generationPrompt: string | null
|
||||
isLocked: boolean
|
||||
images: IdentityImage[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
@@ -138,7 +134,9 @@ export interface CastingBatchResult {
|
||||
|
||||
/** 确认选角会在同一事务中切换 Anchor 并锁定 Identity。 */
|
||||
export interface ConfirmCastingResult {
|
||||
identity: Omit<SubjectIdentity, 'images'>
|
||||
identityId: string
|
||||
subjectId: string
|
||||
isLocked: boolean
|
||||
anchor: IdentityImage
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,7 @@ import type {
|
||||
|
||||
/** 单主体保存响应必须与提交时固定的正式 ID 一致。 */
|
||||
function assertIdentity(value: SubjectIdentity, id: string) {
|
||||
if (!value || value.subjectId !== id || value.images.some(image => image.identityId !== value.id))
|
||||
throw new Error('后端未返回匹配的主体身份,请刷新核对。')
|
||||
if (!value || value.subjectId !== id) throw new Error('后端未返回匹配的主体身份,请刷新核对。')
|
||||
}
|
||||
|
||||
/** 主体目录、项目风格和当前身份分开查询,未创建 Identity 不误报为空图库错误。 */
|
||||
@@ -256,7 +255,7 @@ export function useSubjectIdentity() {
|
||||
!result ||
|
||||
result.id !== imageId ||
|
||||
result.identityId !== identityId ||
|
||||
!result.enabled ||
|
||||
!result.isAnchor ||
|
||||
!canBeAnchor(result)
|
||||
)
|
||||
throw new Error('接口未确认身份母版切换,请刷新核对。')
|
||||
@@ -282,10 +281,13 @@ export function useSubjectIdentity() {
|
||||
const result = await subjectIdentityApi.confirmCasting(subjectId, imageId)
|
||||
if (
|
||||
!result ||
|
||||
result.identity.id !== identityId ||
|
||||
!result.identity.isLocked ||
|
||||
result.identityId !== identityId ||
|
||||
result.subjectId !== subjectId ||
|
||||
!result.isLocked ||
|
||||
result.anchor.id !== imageId ||
|
||||
!result.anchor.enabled
|
||||
!result.anchor.isAnchor ||
|
||||
result.anchor.identityId !== identityId ||
|
||||
!canBeAnchor(result.anchor)
|
||||
)
|
||||
throw new Error('接口未确认演员母版与身份锁定,请刷新核对。')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user