refactor: 使用 Naive UI 重构后台布局与黑白双主题
This commit is contained in:
@@ -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 { UserCheck } from '@lucide/vue'
|
||||
@@ -115,198 +117,244 @@ function chooseAnchor(imageId: string) {
|
||||
if (subject.value?.module === 'character') void confirmCasting(imageId)
|
||||
else void setAnchor(imageId)
|
||||
}
|
||||
/** 新回执自动展开,避免折叠区隐藏部分失败诊断。 */
|
||||
const overview = ref<string[]>([])
|
||||
watch(
|
||||
() => session.value.receipt,
|
||||
receipt => {
|
||||
if (receipt) overview.value = ['casting']
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</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/${projectId}/subject-images`" class="text-button"
|
||||
>下一步:形态图片 →</RouterLink
|
||||
>
|
||||
</div>
|
||||
<p class="alert mt-5 text-xs">
|
||||
Character 现在需要依次完成身份文本、选角候选、确认演员并锁定
|
||||
Identity,之后首帧才能就绪。未明确人物背景时,AI
|
||||
默认以中国人物作为选角基线;项目和角色的明确事实始终优先。场景和道具仍使用普通身份母版流程。
|
||||
</p>
|
||||
<p
|
||||
v-if="catalog.error.value || styleQuery.error.value || castingQuery.error.value"
|
||||
class="alert alert-error mt-4"
|
||||
role="alert"
|
||||
>
|
||||
{{ catalog.error.value || styleQuery.error.value || castingQuery.error.value }}
|
||||
当前相关操作已暂停,请刷新核对后端。
|
||||
</p>
|
||||
<p v-if="styleQuery.data.value && !hasStyle" class="alert mt-4">
|
||||
先创建项目视觉风格,才能 AI 生成身份或身份图片。<RouterLink
|
||||
:to="`/projects/${projectId}/visual-style`"
|
||||
class="text-button ml-2"
|
||||
>设置视觉风格 →</RouterLink
|
||||
>
|
||||
</p>
|
||||
<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">
|
||||
<WorkspacePage split
|
||||
><template #header
|
||||
><div class="flex flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<dt>缺身份文本</dt>
|
||||
<dd>{{ casting.missingIdentity }}</dd>
|
||||
<h2 class="text-lg font-semibold">主体身份</h2>
|
||||
<p class="mt-2 text-sm text-muted">为同一个人物、场景或道具固定稳定特征,再生成不同造型。</p>
|
||||
</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>
|
||||
<input
|
||||
v-model.number="concurrency"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
class="input"
|
||||
:disabled="operation.pending"
|
||||
/>
|
||||
</label>
|
||||
<label class="w-28">
|
||||
<span class="field-label">本批上限</span>
|
||||
<input
|
||||
v-model.number="candidateLimit"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
class="input"
|
||||
:disabled="operation.pending"
|
||||
/>
|
||||
</label>
|
||||
<label class="flex gap-2 pb-2 text-xs">
|
||||
<input v-model="force" type="checkbox" :disabled="operation.pending" />覆盖未锁定身份文本
|
||||
</label>
|
||||
<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">
|
||||
<button
|
||||
v-for="item in casting.items"
|
||||
:key="item.subjectId"
|
||||
class="casting-item"
|
||||
:class="{ selected: selectedId === item.subjectId }"
|
||||
@click="select(item.subjectId)"
|
||||
<RouterLink :to="`/projects/${projectId}/subject-images`" class="text-button"
|
||||
>下一步:形态图片 →</RouterLink
|
||||
>
|
||||
<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)" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<details class="panel mb-5 p-5">
|
||||
<summary class="cursor-pointer text-sm font-medium">高级:全部主体身份文本</summary>
|
||||
<p class="mt-3 text-xs leading-6 text-muted">
|
||||
同时处理 Character、Scene 和 Prop;角色选角建议优先使用上方专用入口。已锁定身份始终跳过。
|
||||
</p>
|
||||
<ConfirmAction
|
||||
class="mt-4"
|
||||
:label="force ? '重生成全部未锁定身份' : '补齐全部主体身份文本'"
|
||||
:disabled="blocked || !hasStyle || !batchValid || dirty || !subjects.length"
|
||||
acknowledgement
|
||||
description="生成全项目主体身份描述与提示词,不生成图片。覆盖不会自动更新旧身份图、形态图、首帧或视频。"
|
||||
@confirm="generateProject(false)"
|
||||
/>
|
||||
</details>
|
||||
<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>
|
||||
<button class="text-button" @click="exportReceipt">导出诊断</button>
|
||||
</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>
|
||||
<p v-if="session.receipt.result.failed" class="alert alert-error mt-3" role="alert">
|
||||
本次存在部分失败,成功结果不会回滚。请检查逐角色原因后再重试。
|
||||
</p>
|
||||
<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></template
|
||||
>
|
||||
<NAlert
|
||||
v-if="catalog.error.value || styleQuery.error.value || castingQuery.error.value"
|
||||
role="alert"
|
||||
type="error"
|
||||
:show-icon="false"
|
||||
class="mt-4"
|
||||
>{{ catalog.error.value || styleQuery.error.value || castingQuery.error.value }}
|
||||
当前相关操作已暂停,请刷新核对后端。
|
||||
</NAlert>
|
||||
<NCollapse v-model:expanded-names="overview" class="workspace-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">
|
||||
同时处理 Character、Scene 和
|
||||
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 v-if="subjects.length" class="identity-workspace">
|
||||
<aside class="panel min-w-0 p-4">
|
||||
<h3 class="text-sm font-medium">
|
||||
主体目录 <span class="ml-2 text-xs text-muted">{{ subjects.length }}</span>
|
||||
</h3>
|
||||
<input v-model="search" class="input mt-4" placeholder="搜索主体或引用" aria-label="搜索主体身份" />
|
||||
<select v-model="module" class="input mt-3" aria-label="身份主体类型">
|
||||
<option value="all">全部类型</option>
|
||||
<option value="character">人物</option>
|
||||
<option value="scene">场景</option>
|
||||
<option value="prop">道具</option>
|
||||
</select>
|
||||
<NInput
|
||||
placeholder="搜索主体或引用"
|
||||
aria-label="搜索主体身份"
|
||||
:input-props="{ 'aria-label': '搜索主体身份' }"
|
||||
class="mt-4"
|
||||
v-model:value="search"
|
||||
></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' }
|
||||
]"
|
||||
class="mt-3"
|
||||
></NSelect>
|
||||
<div class="identity-subject-list mt-4">
|
||||
<button
|
||||
<NButton
|
||||
v-for="item in filtered"
|
||||
:key="item.id"
|
||||
class="identity-subject-item"
|
||||
:class="{ selected: selectedId === item.id }"
|
||||
:aria-pressed="selectedId === item.id"
|
||||
@click="select(item.id)"
|
||||
>
|
||||
<span class="block font-medium">{{ item.name }}</span
|
||||
class="identity-subject-item"
|
||||
:class="{ selected: selectedId === item.id }"
|
||||
><span class="block font-medium">{{ item.name }}</span
|
||||
><span class="mt-1 block text-[11px] text-muted"
|
||||
>{{ item.ref }} · {{ item.forms.length }} 个形态<span
|
||||
v-if="casting?.items.find(row => row.subjectId === item.id)"
|
||||
@@ -316,26 +364,28 @@ function chooseAnchor(imageId: string) {
|
||||
castingStatusLabel(casting.items.find(row => row.subjectId === item.id)!.status)
|
||||
}}</span
|
||||
></span
|
||||
>
|
||||
</button>
|
||||
></NButton
|
||||
>
|
||||
<p v-if="!filtered.length" class="py-6 text-xs text-muted">没有匹配主体,请调整筛选。</p>
|
||||
</div>
|
||||
<button
|
||||
class="text-button mt-4"
|
||||
<NButton
|
||||
:disabled="catalog.loading.value || castingQuery.loading.value"
|
||||
@click="refreshDirectory"
|
||||
text
|
||||
size="small"
|
||||
class="mt-4"
|
||||
>
|
||||
刷新主体目录
|
||||
</button>
|
||||
</NButton>
|
||||
</aside>
|
||||
<div class="min-w-0">
|
||||
<div v-if="pendingSelection" class="alert mb-4">
|
||||
<p class="text-xs">当前主体有未保存修改,切换会丢弃草稿。</p>
|
||||
<NAlert v-if="pendingSelection" type="info" :show-icon="false" class="mb-4"
|
||||
><p class="text-xs">当前主体有未保存修改,切换会丢弃草稿。</p>
|
||||
<div class="mt-3 flex gap-3">
|
||||
<button class="button button-secondary" @click="discardAndSelect">放弃草稿并切换</button
|
||||
><button class="text-button" @click="pendingSelection = ''">继续编辑</button>
|
||||
</div>
|
||||
</div>
|
||||
<NButton @click="discardAndSelect">放弃草稿并切换</NButton
|
||||
><NButton @click="pendingSelection = ''" text size="small">继续编辑</NButton>
|
||||
</div></NAlert
|
||||
>
|
||||
<section v-if="subject" class="panel p-5 sm:p-6">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
@@ -357,13 +407,13 @@ function chooseAnchor(imageId: string) {
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
<button class="text-button" :disabled="detail.loading.value" @click="detail.refresh">
|
||||
<NButton :disabled="detail.loading.value" @click="detail.refresh" text size="small">
|
||||
刷新身份
|
||||
</button>
|
||||
</NButton>
|
||||
</div>
|
||||
<p v-if="detail.error.value" class="alert alert-error mt-4" role="alert">
|
||||
{{ detail.error.value }} 查询失败时不允许生成或覆盖。
|
||||
</p>
|
||||
<NAlert v-if="detail.error.value" role="alert" type="error" :show-icon="false" class="mt-4"
|
||||
>{{ detail.error.value }} 查询失败时不允许生成或覆盖。
|
||||
</NAlert>
|
||||
<p v-if="!detail.data.value && detail.loading.value" class="py-8 text-sm text-muted" role="status">
|
||||
正在读取主体身份…
|
||||
</p>
|
||||
@@ -375,21 +425,18 @@ function chooseAnchor(imageId: string) {
|
||||
acknowledgement
|
||||
description="调用文本模型生成稳定身份描述和提示词。旧图片不会自动重生成;已锁定身份需先解锁并保存。"
|
||||
@confirm="generate"
|
||||
/><button
|
||||
/><NButton
|
||||
v-if="subject.module === 'character'"
|
||||
class="button button-secondary"
|
||||
:disabled="!canGenerateCandidate"
|
||||
@click="castingOpen = true"
|
||||
>
|
||||
生成选角候选</button
|
||||
><button
|
||||
生成选角候选</NButton
|
||||
><NButton
|
||||
v-if="subject.module !== 'character' || !!anchor"
|
||||
class="button button-secondary"
|
||||
:disabled="!canGenerateImage"
|
||||
@click="imageOpen = true"
|
||||
>{{ subject.module === 'character' ? '生成辅助身份图' : '生成身份参考图' }}</NButton
|
||||
>
|
||||
{{ subject.module === 'character' ? '生成辅助身份图' : '生成身份参考图' }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="dirty" class="mt-3 text-xs text-muted">请先保存身份修改,再使用 AI 或生成图片。</p>
|
||||
<IdentityEditor
|
||||
@@ -434,8 +481,8 @@ function chooseAnchor(imageId: string) {
|
||||
<div class="mt-5 border-t border-line pt-5">
|
||||
<h4 class="text-xs font-medium">此主体的形态</h4>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<span v-for="form in subject.forms" :key="form.id" class="tag"
|
||||
>{{ form.name }}{{ form.isDefault ? ' · 默认' : '' }}</span
|
||||
<NTag v-for="form in subject.forms" :key="form.id" size="small" :bordered="false"
|
||||
>{{ form.name }}{{ form.isDefault ? ' · 默认' : '' }}</NTag
|
||||
>
|
||||
</div>
|
||||
<p v-if="!subject.forms.length" class="mt-3 text-xs text-muted">
|
||||
@@ -467,8 +514,7 @@ function chooseAnchor(imageId: string) {
|
||||
:module="subject.module"
|
||||
:images="images"
|
||||
:disabled="!canGenerateImage"
|
||||
@generate="generateImage"
|
||||
/>
|
||||
@generate="generateImage" />
|
||||
<CastingCandidateDialog
|
||||
v-if="subject?.module === 'character'"
|
||||
v-model:open="castingOpen"
|
||||
@@ -476,6 +522,5 @@ function chooseAnchor(imageId: string) {
|
||||
:subject-name="subject.name"
|
||||
:disabled="!canGenerateCandidate"
|
||||
@generate="generateCastingCandidate"
|
||||
/>
|
||||
</section>
|
||||
/></WorkspacePage>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { NAlert, NButton, NCheckbox, NInput, NInputNumber } from 'naive-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { AppDialog } from '../../../components/ui'
|
||||
import { validImageSize } from '../../subject-images/model'
|
||||
@@ -38,18 +39,20 @@ function submit() {
|
||||
<template>
|
||||
<AppDialog v-model:open="open" title="生成角色选角候选" :description="`${subjectName} · 候选不会自动成为身份母版`">
|
||||
<form class="mt-5 space-y-4" @submit.prevent="submit">
|
||||
<p class="alert text-xs">
|
||||
<NAlert type="info" :show-icon="false" class="text-xs">
|
||||
后端会依据角色 Identity、项目视觉风格和默认选角约束独立生成一张 primary
|
||||
候选,不会复用当前身份母版。生成完成后仍需人工点击“确认选角”。
|
||||
</p>
|
||||
</NAlert>
|
||||
<label class="block">
|
||||
<span class="field-label">覆盖本次完整提示词(可选)</span>
|
||||
<textarea
|
||||
id="casting-candidate-prompt"
|
||||
v-model="prompt"
|
||||
class="input min-h-24"
|
||||
<NInput
|
||||
placeholder="建议留空,使用后端编译的稳定身份和选角约束;未明确人物背景时默认中国人物"
|
||||
/>
|
||||
:input-props="{ id: 'casting-candidate-prompt' }"
|
||||
class="min-h-24"
|
||||
v-model:value="prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 12 }"
|
||||
></NInput>
|
||||
</label>
|
||||
<p v-if="prompt.trim()" class="text-xs text-danger">
|
||||
自定义文本会替换后端完整提示词,请自行包含人物身份、基础服装和中性参考图要求。
|
||||
@@ -57,27 +60,35 @@ function submit() {
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<label
|
||||
><span class="field-label">宽度(px)</span
|
||||
><input v-model.number="width" class="input" type="number" min="1" placeholder="后端默认"
|
||||
/></label>
|
||||
><NInputNumber
|
||||
placeholder="后端默认"
|
||||
:value="typeof width === 'number' ? width : null"
|
||||
@update:value="width = $event ?? ''"
|
||||
:min="1"
|
||||
></NInputNumber
|
||||
></label>
|
||||
<label
|
||||
><span class="field-label">高度(px)</span
|
||||
><input v-model.number="height" class="input" type="number" min="1" placeholder="后端默认"
|
||||
/></label>
|
||||
><NInputNumber
|
||||
placeholder="后端默认"
|
||||
:value="typeof height === 'number' ? height : null"
|
||||
@update:value="height = $event ?? ''"
|
||||
:min="1"
|
||||
></NInputNumber
|
||||
></label>
|
||||
</div>
|
||||
<p v-if="!valid" class="text-xs text-danger">宽高需要同时留空,或同时填写正整数。</p>
|
||||
<label class="flex items-start gap-2 text-xs leading-6">
|
||||
<input
|
||||
id="casting-candidate-cost"
|
||||
v-model="acknowledged"
|
||||
type="checkbox"
|
||||
class="mt-1.5 accent-accent"
|
||||
/>我已确认调用 Seedream 可能产生费用,且当前没有同项目生成任务。
|
||||
</label>
|
||||
<NCheckbox
|
||||
id="casting-candidate-cost"
|
||||
v-model:checked="acknowledged"
|
||||
class="flex items-start gap-2 text-xs leading-6"
|
||||
>我已确认调用 Seedream 可能产生费用,且当前没有同项目生成任务。
|
||||
</NCheckbox>
|
||||
<div class="dialog-footer">
|
||||
<button type="button" class="button button-secondary" @click="open = false">取消</button>
|
||||
<button type="submit" class="button button-primary" :disabled="disabled || !valid || !acknowledged">
|
||||
<NButton @click="open = false">取消</NButton>
|
||||
<NButton :disabled="disabled || !valid || !acknowledged" type="primary" attr-type="submit">
|
||||
确认生成候选
|
||||
</button>
|
||||
</NButton>
|
||||
</div>
|
||||
</form>
|
||||
</AppDialog>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NCheckbox, NInput } from 'naive-ui'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import type { SaveIdentityInput, SubjectIdentity } from '../types'
|
||||
|
||||
@@ -34,21 +35,27 @@ function save() {
|
||||
<fieldset :disabled="disabled" class="space-y-4">
|
||||
<label class="block"
|
||||
><span class="field-label">稳定身份描述</span
|
||||
><textarea
|
||||
id="identity-description"
|
||||
v-model="form.description"
|
||||
class="input min-h-28"
|
||||
><NInput
|
||||
:disabled="disabled"
|
||||
placeholder="只描述跨形态不变的面部、结构、材质等特征"
|
||||
/>
|
||||
:input-props="{ id: 'identity-description' }"
|
||||
class="min-h-28"
|
||||
v-model:value="form.description"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 12 }"
|
||||
></NInput>
|
||||
</label>
|
||||
<label class="block"
|
||||
><span class="field-label">身份核心提示词(稳定事实)</span
|
||||
><textarea
|
||||
id="identity-prompt"
|
||||
v-model="form.generationPrompt"
|
||||
class="input min-h-36"
|
||||
><NInput
|
||||
:disabled="disabled"
|
||||
placeholder="不固定某一形态的服装、伤势或临时状态"
|
||||
/>
|
||||
:input-props="{ id: 'identity-prompt' }"
|
||||
class="min-h-36"
|
||||
v-model:value="form.generationPrompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 12 }"
|
||||
></NInput>
|
||||
</label>
|
||||
<p class="text-xs leading-6 text-muted">
|
||||
{{
|
||||
@@ -60,18 +67,22 @@ function save() {
|
||||
}}
|
||||
这里保存身份事实;背景、视角和参考图约束由后端另行编译,不是最终整段生图 Prompt。
|
||||
</p>
|
||||
<label class="flex items-start gap-2 text-xs leading-6"
|
||||
><input id="identity-lock" v-model="form.isLocked" type="checkbox" class="mt-1.5 accent-accent" />{{
|
||||
<NCheckbox
|
||||
:disabled="disabled"
|
||||
id="identity-lock"
|
||||
v-model:checked="form.isLocked"
|
||||
class="flex items-start gap-2 text-xs leading-6"
|
||||
>{{
|
||||
module === 'character'
|
||||
? '确认并锁定演员身份。Character 必须同时具有身份母版并锁定 Identity,首帧才能进入就绪状态。'
|
||||
: '确认并锁定身份文本。锁定后仍可人工保存、生成图片及切换母版。'
|
||||
}}</label
|
||||
}}</NCheckbox
|
||||
>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<span class="text-xs text-muted">{{
|
||||
dirty ? '有未保存修改,自动刷新不会覆盖草稿。' : '图片以已保存的身份提示词为准。'
|
||||
}}</span
|
||||
><button class="button button-primary" type="submit" :disabled="disabled">保存主体身份</button>
|
||||
><NButton :disabled="disabled" type="primary" attr-type="submit">保存主体身份</NButton>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { NAlert, NButton, NCollapse, NCollapseItem, NTag } from 'naive-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { AssetImage, StatusBadge } from '../../../components/ui'
|
||||
import { referenceImageUrl } from '../../../lib/assets'
|
||||
@@ -49,14 +50,14 @@ function chooseAnchor() {
|
||||
<section class="mt-5 border-t border-line pt-5" aria-label="身份参考图库">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h3 class="text-sm font-medium">身份参考图 · {{ images.length }}</h3>
|
||||
<span class="tag">{{ anchor ? '已选母版' : '尚无母版' }}</span>
|
||||
<NTag size="small" :bordered="false">{{ anchor ? '已选母版' : '尚无母版' }}</NTag>
|
||||
</div>
|
||||
<p class="mt-2 text-xs leading-6 text-muted">
|
||||
身份母版确定“是谁”;形态主图确定“该造型长什么样”。切换母版仅影响之后的生成,不会替换已有形态图、分镜参考图或提示词。
|
||||
</p>
|
||||
<p v-if="character && anchor" class="alert mt-3 text-xs leading-6">
|
||||
<NAlert v-if="character && anchor" type="info" :show-icon="false" class="mt-3 text-xs leading-6">
|
||||
如果要更换演员身份,请生成新的“选角候选”并确认选角。正面、三分之四、全身等辅助身份图会继续引用当前母版,只用于保持同一演员,不会重新选择人物。
|
||||
</p>
|
||||
</NAlert>
|
||||
<template v-if="selected">
|
||||
<AssetImage
|
||||
:src="selected.status === 'completed' ? selected.imageUrl : null"
|
||||
@@ -66,13 +67,13 @@ function chooseAnchor() {
|
||||
/>
|
||||
<div class="mt-3 flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<StatusBadge :status="selected.status" /><span class="tag">{{
|
||||
<StatusBadge :status="selected.status" /><NTag size="small" :bordered="false">{{
|
||||
selected.isAnchor
|
||||
? '当前身份母版'
|
||||
: selected.viewType === 'primary'
|
||||
? '母版候选'
|
||||
: identityViewLabels[selected.viewType]
|
||||
}}</span
|
||||
}}</NTag
|
||||
><span class="text-xs text-muted"
|
||||
>{{ selected.enabled ? '启用' : '停用' }} · {{ selected.width || '—' }} ×
|
||||
{{ selected.height || '—' }}</span
|
||||
@@ -87,15 +88,15 @@ function chooseAnchor() {
|
||||
>打开原图</a
|
||||
>
|
||||
</div>
|
||||
<p v-if="selected.error" class="alert alert-error mt-3" role="alert">{{ selected.error }}</p>
|
||||
<NAlert v-if="selected.error" role="alert" type="error" :show-icon="false" class="mt-3">{{
|
||||
selected.error
|
||||
}}</NAlert>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
<NButton
|
||||
v-if="!selected.isAnchor || (character && !identityLocked)"
|
||||
class="button button-secondary"
|
||||
:disabled="!canSelect"
|
||||
@click="confirming = true"
|
||||
>
|
||||
{{ character ? '确认选角并锁定' : '设为身份母版' }}</button
|
||||
>{{ character ? '确认选角并锁定' : '设为身份母版' }}</NButton
|
||||
><span class="text-[11px] text-muted"
|
||||
>{{ selected.provider }} · {{ selected.model }} · {{ formatDate(selected.createdAt) }}</span
|
||||
>
|
||||
@@ -103,8 +104,8 @@ function chooseAnchor() {
|
||||
<p v-if="selected.viewType !== 'primary'" class="mt-2 text-xs text-muted">
|
||||
辅助视图不能直接设为母版;它会继承当前身份母版,不适合用来更换演员。
|
||||
</p>
|
||||
<div v-if="confirming" class="alert mt-3">
|
||||
<p class="text-xs">
|
||||
<NAlert v-if="confirming" type="info" :show-icon="false" class="mt-3"
|
||||
><p class="text-xs">
|
||||
{{
|
||||
character
|
||||
? '确认选择这张图片作为正式演员?后端会在同一事务中切换身份母版并锁定 Identity,完成后首帧才具备稳定人物身份。'
|
||||
@@ -112,15 +113,18 @@ function chooseAnchor() {
|
||||
}}
|
||||
</p>
|
||||
<div class="mt-3 flex gap-3">
|
||||
<button class="button button-primary" :disabled="!canSelect" @click="chooseAnchor">
|
||||
{{ character ? '确认演员选择' : '确认切换身份母版' }}</button
|
||||
><button class="text-button" @click="confirming = false">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
<details v-if="selected.prompt" class="mt-4 text-xs">
|
||||
<summary class="cursor-pointer text-muted">本次实际提示词</summary>
|
||||
<p class="mt-3 whitespace-pre-wrap leading-6">{{ selected.prompt }}</p>
|
||||
</details>
|
||||
<NButton :disabled="!canSelect" @click="chooseAnchor" type="primary">{{
|
||||
character ? '确认演员选择' : '确认切换身份母版'
|
||||
}}</NButton
|
||||
><NButton @click="confirming = false" text size="small">取消</NButton>
|
||||
</div></NAlert
|
||||
>
|
||||
<NCollapse v-if="selected.prompt" 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"
|
||||
@@ -129,16 +133,15 @@ function chooseAnchor() {
|
||||
</p>
|
||||
<p class="mt-2 break-all font-mono text-[10px] text-muted">Identity image ID · {{ selected.id }}</p>
|
||||
<div class="image-history mt-4" aria-label="身份图片历史">
|
||||
<button
|
||||
<NButton
|
||||
v-for="image in images"
|
||||
:key="image.id"
|
||||
class="image-history-item"
|
||||
:class="{ selected: image.id === selected.id }"
|
||||
:aria-label="`查看身份图片 ${image.id}`"
|
||||
:aria-pressed="image.id === selected.id"
|
||||
@click="selectedId = image.id"
|
||||
>
|
||||
<AssetImage
|
||||
class="image-history-item"
|
||||
:class="{ selected: image.id === selected.id }"
|
||||
><AssetImage
|
||||
:src="image.status === 'completed' ? image.imageUrl : null"
|
||||
:alt="identityViewLabels[image.viewType]"
|
||||
:retryable="false"
|
||||
@@ -149,8 +152,8 @@ function chooseAnchor() {
|
||||
: image.viewType === 'primary'
|
||||
? '母版候选'
|
||||
: identityViewLabels[image.viewType]
|
||||
}}</span>
|
||||
</button>
|
||||
}}</span></NButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<p v-else class="py-8 text-sm text-muted">尚无身份参考图。先保存身份提示词,再生成第一张母版。</p>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { NAlert, NButton, NCheckbox, NInput, NInputNumber, NSelect } from 'naive-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { AppDialog } from '../../../components/ui'
|
||||
import { validImageSize } from '../../subject-images/model'
|
||||
@@ -61,40 +62,50 @@ function submit() {
|
||||
<template>
|
||||
<AppDialog v-model:open="open" title="生成身份参考图" :description="subjectName">
|
||||
<form class="mt-5 space-y-4" @submit.prevent="submit">
|
||||
<p class="alert text-xs">
|
||||
<NAlert type="info" :show-icon="false" class="text-xs">
|
||||
调用 Seedream,每次新增一张图片,可能产生费用。第一张成功的 primary 图自动成为母版;已有母版时,新
|
||||
primary 图仅作为停用候选保留。
|
||||
</p>
|
||||
</NAlert>
|
||||
<label class="block"
|
||||
><span class="field-label">参考视角</span
|
||||
><select id="identity-view" v-model="viewType" class="input">
|
||||
<option v-for="[value, label] in allowedViews" :key="value" :value="value">
|
||||
{{ label }}({{ value }})
|
||||
</option>
|
||||
</select></label
|
||||
>
|
||||
><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
|
||||
><select id="identity-reference" v-model="referenceImageId" class="input">
|
||||
<option value="">
|
||||
{{ anchor ? '自动引用当前身份母版' : '自动选择(当前无母版,将按文本生成)' }}
|
||||
</option>
|
||||
<option v-for="image in references" :key="image.id" :value="image.id">
|
||||
{{ identityViewLabels[image.viewType] }} · {{ image.id }}
|
||||
</option>
|
||||
</select></label
|
||||
>
|
||||
><NSelect
|
||||
id="identity-reference"
|
||||
v-model:value="referenceImageId"
|
||||
:options="[
|
||||
{
|
||||
label: String(anchor ? '自动引用当前身份母版' : '自动选择(当前无母版,将按文本生成)'),
|
||||
value: ''
|
||||
},
|
||||
...references.map(image => ({
|
||||
label: String(identityViewLabels[image.viewType] + ' · ' + image.id),
|
||||
value: image.id
|
||||
}))
|
||||
]"
|
||||
class="select-control"
|
||||
></NSelect
|
||||
></label>
|
||||
<p class="text-xs leading-6 text-muted">
|
||||
只能选择此主体已完成的图片。后端需取得 Provider 可访问的远程地址;本地预览可见不保证远程地址仍有效。
|
||||
</p>
|
||||
<label class="block"
|
||||
><span class="field-label">覆盖本次完整提示词(可选)</span
|
||||
><textarea
|
||||
id="identity-image-prompt"
|
||||
v-model="prompt"
|
||||
class="input min-h-24"
|
||||
><NInput
|
||||
placeholder="建议留空,使用后端编译的身份约束和视角要求"
|
||||
/>
|
||||
:input-props="{ id: 'identity-image-prompt' }"
|
||||
class="min-h-24"
|
||||
v-model:value="prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 12 }"
|
||||
></NInput>
|
||||
</label>
|
||||
<p v-if="prompt.trim()" class="text-xs text-danger">
|
||||
自定义文本会替换后端编译的完整提示词,请自行包含身份约束和视角要求。
|
||||
@@ -102,40 +113,38 @@ function submit() {
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<label
|
||||
><span class="field-label">宽度(px)</span
|
||||
><input
|
||||
id="identity-width"
|
||||
v-model.number="width"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
class="input"
|
||||
placeholder="后端默认" /></label
|
||||
><NInputNumber
|
||||
placeholder="后端默认"
|
||||
:input-props="{ id: 'identity-width' }"
|
||||
:value="typeof width === 'number' ? width : null"
|
||||
@update:value="width = $event ?? ''"
|
||||
:min="1"
|
||||
:step="1"
|
||||
></NInputNumber></label
|
||||
><label
|
||||
><span class="field-label">高度(px)</span
|
||||
><input
|
||||
id="identity-height"
|
||||
v-model.number="height"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
class="input"
|
||||
><NInputNumber
|
||||
placeholder="后端默认"
|
||||
/></label>
|
||||
:input-props="{ id: 'identity-height' }"
|
||||
:value="typeof height === 'number' ? height : null"
|
||||
@update:value="height = $event ?? ''"
|
||||
:min="1"
|
||||
:step="1"
|
||||
></NInputNumber
|
||||
></label>
|
||||
</div>
|
||||
<p v-if="!valid" class="text-xs text-danger" role="alert">尺寸须成对填写正整数;指定的参考图必须仍可用。</p>
|
||||
<label class="flex items-start gap-2 text-xs leading-6"
|
||||
><input
|
||||
id="identity-image-cost"
|
||||
v-model="acknowledged"
|
||||
type="checkbox"
|
||||
class="mt-1.5 accent-accent"
|
||||
/>我已确认后台没有同项目任务,了解本次会调用模型并可能产生费用。</label
|
||||
<NCheckbox
|
||||
id="identity-image-cost"
|
||||
v-model:checked="acknowledged"
|
||||
class="flex items-start gap-2 text-xs leading-6"
|
||||
>我已确认后台没有同项目任务,了解本次会调用模型并可能产生费用。</NCheckbox
|
||||
>
|
||||
<div class="dialog-footer">
|
||||
<button type="button" class="button button-secondary" @click="open = false">取消</button
|
||||
><button class="button button-primary" type="submit" :disabled="disabled || !valid || !acknowledged">
|
||||
<NButton @click="open = false">取消</NButton
|
||||
><NButton :disabled="disabled || !valid || !acknowledged" type="primary" attr-type="submit">
|
||||
确认生成身份图
|
||||
</button>
|
||||
</NButton>
|
||||
</div>
|
||||
</form>
|
||||
</AppDialog>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from './model'
|
||||
import { identityImageFixture } from './testing/fixtures'
|
||||
import { formFixture } from '../subject-images/testing/fixtures'
|
||||
import { expandSections, selectControl } from '../../testing/naive'
|
||||
|
||||
let wrapper: VueWrapper | undefined
|
||||
afterEach(() => {
|
||||
@@ -23,7 +24,7 @@ afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
/** 从真实 Reka Portal 内获取确认按钮。 */
|
||||
/** 从真实 Naive 弹窗内获取确认按钮。 */
|
||||
function button(label: string) {
|
||||
const item = [...document.querySelectorAll('button')].find(element => element.textContent?.trim() === label)
|
||||
if (!item) throw new Error(`缺少按钮 ${label}`)
|
||||
@@ -32,6 +33,10 @@ function button(label: string) {
|
||||
|
||||
/** 修改 Portal 表单控件并触发 Vue 绑定。 */
|
||||
function input(selector: string, value: string) {
|
||||
if (selector === '#identity-view' || selector === '#identity-reference') {
|
||||
selectControl(wrapper!, 'id', selector.slice(1)).vm.$emit('update:value', value)
|
||||
return
|
||||
}
|
||||
const item = document.querySelector<HTMLInputElement | HTMLSelectElement>(selector)!
|
||||
item.value = value
|
||||
item.dispatchEvent(new Event(item.tagName === 'SELECT' ? 'change' : 'input', { bubbles: true }))
|
||||
@@ -136,8 +141,8 @@ describe('身份图与母版契约', () => {
|
||||
await wrapper.setProps({ images: [] })
|
||||
expect(button('确认生成身份图').disabled).toBe(true)
|
||||
await wrapper.setProps({ subjectId: 's2', subjectName: '陆川' })
|
||||
expect(document.querySelector<HTMLSelectElement>('#identity-reference')!.value).toBe('')
|
||||
expect(document.querySelector<HTMLInputElement>('#identity-image-cost')!.checked).toBe(false)
|
||||
expect(selectControl(wrapper!, 'id', 'identity-reference').props('value')).toBe('')
|
||||
expect(document.querySelector('#identity-image-cost')!.getAttribute('aria-checked')).toBe('false')
|
||||
})
|
||||
|
||||
it('Character 普通身份图入口只提供辅助视角,primary 必须走选角候选接口', async () => {
|
||||
@@ -153,9 +158,9 @@ describe('身份图与母版契约', () => {
|
||||
}
|
||||
})
|
||||
await flushPromises()
|
||||
const options = [...document.querySelectorAll<HTMLOptionElement>('#identity-view option')]
|
||||
expect(options.map(item => item.value)).toEqual(['front', 'three-quarter', 'full-body'])
|
||||
expect(document.querySelector<HTMLSelectElement>('#identity-view')!.value).toBe('front')
|
||||
const select = selectControl(wrapper!, 'id', 'identity-view')
|
||||
expect(select.props('options')!.map(item => item.value)).toEqual(['front', 'three-quarter', 'full-body'])
|
||||
expect(select.props('value')).toBe('front')
|
||||
})
|
||||
|
||||
it('辅助图不能切换母版,提示词按纯文本展示', async () => {
|
||||
@@ -170,6 +175,7 @@ describe('身份图与母版契约', () => {
|
||||
}
|
||||
})
|
||||
expect(button('确认选角并锁定').disabled).toBe(true)
|
||||
await expandSections()
|
||||
expect(wrapper.text()).toContain('<script>不执行</script>')
|
||||
expect(wrapper.find('script').exists()).toBe(false)
|
||||
expect(wrapper.emitted('anchor')).toBeUndefined()
|
||||
|
||||
Reference in New Issue
Block a user