feat: 形态图库展示身份母版过期状态
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { Search, RefreshCw, Download, ImagePlus, ArrowRight } from '@lucide/vue'
|
import { Search, RefreshCw, Download, ImagePlus, ArrowRight, AlertTriangle } from '@lucide/vue'
|
||||||
import { AssetImage, EmptyState, StatusBadge } from '../../components/ui'
|
import { AssetImage, EmptyState, StatusBadge } from '../../components/ui'
|
||||||
import { downloadText } from '../../lib/format'
|
import { downloadText } from '../../lib/format'
|
||||||
import ConfirmAction from '../workflows/ConfirmAction.vue'
|
import ConfirmAction from '../workflows/ConfirmAction.vue'
|
||||||
import { useSubjectImages } from './useSubjectImages'
|
import { useSubjectImages } from './useSubjectImages'
|
||||||
import { coverImage, hasRunningImages, primaryImage } from './model'
|
import { coverImage, hasRunningImages, isPrimaryIdentityStale, primaryImage } from './model'
|
||||||
import type { GenerateFormImageInput, SubjectFormAsset } from './types'
|
import type { GenerateFormImageInput, SubjectFormAsset } from './types'
|
||||||
import GenerateImageDialog from './components/GenerateImageDialog.vue'
|
import GenerateImageDialog from './components/GenerateImageDialog.vue'
|
||||||
import ImageGalleryDialog from './components/ImageGalleryDialog.vue'
|
import ImageGalleryDialog from './components/ImageGalleryDialog.vue'
|
||||||
@@ -16,6 +16,7 @@ const route = useRoute()
|
|||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
forms,
|
forms,
|
||||||
|
staleForms,
|
||||||
query,
|
query,
|
||||||
operation,
|
operation,
|
||||||
session,
|
session,
|
||||||
@@ -25,11 +26,13 @@ const {
|
|||||||
running,
|
running,
|
||||||
batchValid,
|
batchValid,
|
||||||
generate,
|
generate,
|
||||||
generateProject
|
generateProject,
|
||||||
|
generateStale
|
||||||
} = useSubjectImages()
|
} = useSubjectImages()
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const module = ref('all')
|
const module = ref('all')
|
||||||
const onlyMissing = ref(false)
|
const onlyMissing = ref(false)
|
||||||
|
const onlyStale = ref(false)
|
||||||
const generateOpen = ref(false)
|
const generateOpen = ref(false)
|
||||||
const galleryOpen = ref(false)
|
const galleryOpen = ref(false)
|
||||||
const generateId = ref('')
|
const generateId = ref('')
|
||||||
@@ -42,6 +45,7 @@ const filtered = computed(() =>
|
|||||||
form =>
|
form =>
|
||||||
(module.value === 'all' || form.subject.module === module.value) &&
|
(module.value === 'all' || form.subject.module === module.value) &&
|
||||||
(!onlyMissing.value || !primaryImage(form.images)) &&
|
(!onlyMissing.value || !primaryImage(form.images)) &&
|
||||||
|
(!onlyStale.value || isPrimaryIdentityStale(form)) &&
|
||||||
`${form.subject.name} ${form.subject.ref} ${form.name}`
|
`${form.subject.name} ${form.subject.ref} ${form.name}`
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(search.value.trim().toLowerCase())
|
.includes(search.value.trim().toLowerCase())
|
||||||
@@ -78,6 +82,7 @@ function resetFilters() {
|
|||||||
search.value = ''
|
search.value = ''
|
||||||
module.value = 'all'
|
module.value = 'all'
|
||||||
onlyMissing.value = false
|
onlyMissing.value = false
|
||||||
|
onlyStale.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -93,46 +98,63 @@ function resetFilters() {
|
|||||||
/></RouterLink>
|
/></RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<p class="alert mt-5 text-xs">
|
<p class="alert mt-5 text-xs">
|
||||||
人物与场景形态在有身份母版时自动引用,以保持人物身份或空间结构一致;无母版仍可独立生成。道具暂不自动引用。
|
Character 形态图会记录生成时使用的 Identity Anchor。更换演员母版后,旧主图会标记为“身份母版已变更”,且后端会阻止它继续进入分镜首帧;请基于当前母版重新生成候选并确认新的主参考图。
|
||||||
<RouterLink :to="`/projects/${id}/subject-identity`" class="text-button ml-2">管理主体身份 →</RouterLink>
|
<RouterLink :to="`/projects/${id}/subject-identity`" class="text-button ml-2">管理主体身份 →</RouterLink>
|
||||||
更换母版后,已有形态主图仍保留,需主动生图并选择新主图。
|
|
||||||
</p>
|
</p>
|
||||||
<div class="panel mt-5 p-5">
|
<div class="panel mt-5 p-5">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
<strong>{{ completeCount }}</strong> / {{ forms.length }} 个形态已有主图<span
|
<strong>{{ completeCount }}</strong> / {{ forms.length }} 个形态已有主图
|
||||||
class="ml-3 text-xs text-muted"
|
<span v-if="staleForms.length" class="ml-3 text-xs text-danger">
|
||||||
>Seedream · 后端配置</span
|
{{ staleForms.length }} 个人物形态主图已过期
|
||||||
>
|
</span>
|
||||||
|
<span class="ml-3 text-xs text-muted">Seedream · 后端配置</span>
|
||||||
</p>
|
</p>
|
||||||
<button class="text-button" :disabled="query.loading.value" @click="query.refresh">
|
<button class="text-button" :disabled="query.loading.value" @click="query.refresh">
|
||||||
<RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新图库
|
<RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新图库
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-[11px] leading-6 text-muted">
|
<p class="mt-2 text-[11px] leading-6 text-muted">
|
||||||
每 6 秒更新图片记录;主图供分镜引用,候选图和历史失败记录均保留。覆盖率不是当前批次进度。
|
每 6 秒更新图片记录;主图供分镜引用,候选图和历史失败记录均保留。过期主图不会自动删除,也不会被批量任务静默替换。
|
||||||
</p>
|
</p>
|
||||||
|
<div v-if="staleForms.length" class="alert alert-error 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。
|
||||||
|
</span>
|
||||||
|
<ConfirmAction
|
||||||
|
label="重新生成过期形态"
|
||||||
|
:disabled="blocked || !batchValid || !staleForms.length"
|
||||||
|
acknowledgement
|
||||||
|
description="只为身份母版已经变化的 Character Form 调用 Seedream,各新增一张基于当前 Anchor 的候选图;不会自动替换旧主图,生成后请人工确认并设置新的主参考图。"
|
||||||
|
@confirm="generateStale"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<details class="mt-4 border-t border-line pt-4">
|
<details class="mt-4 border-t border-line pt-4">
|
||||||
<summary class="cursor-pointer text-xs font-medium">项目批量生图</summary>
|
<summary class="cursor-pointer text-xs font-medium">项目批量生图</summary>
|
||||||
<div class="mt-4 flex flex-wrap items-end gap-4">
|
<div class="mt-4 flex flex-wrap items-end gap-4">
|
||||||
<label class="w-28"
|
<label class="w-28">
|
||||||
><span class="field-label">批量并发</span
|
<span class="field-label">批量并发</span>
|
||||||
><input
|
<input
|
||||||
id="image-concurrency"
|
id="image-concurrency"
|
||||||
v-model.number="concurrency"
|
v-model.number="concurrency"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
step="1"
|
step="1"
|
||||||
class="input"
|
class="input"
|
||||||
:disabled="operation.pending" /></label
|
:disabled="operation.pending"
|
||||||
><label class="flex items-center gap-2 pb-2 text-xs"
|
/>
|
||||||
><input
|
</label>
|
||||||
|
<label class="flex items-center gap-2 pb-2 text-xs">
|
||||||
|
<input
|
||||||
v-model="force"
|
v-model="force"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="accent-accent"
|
class="accent-accent"
|
||||||
:disabled="operation.pending"
|
:disabled="operation.pending"
|
||||||
/>已有主图也新增候选图</label
|
/>已有主图也新增候选图
|
||||||
><ConfirmAction
|
</label>
|
||||||
|
<ConfirmAction
|
||||||
:label="force ? '为全项目新增候选图' : '补齐项目主参考图'"
|
:label="force ? '为全项目新增候选图' : '补齐项目主参考图'"
|
||||||
:disabled="blocked || !batchValid || !forms.length"
|
:disabled="blocked || !batchValid || !forms.length"
|
||||||
acknowledgement
|
acknowledgement
|
||||||
@@ -167,36 +189,47 @@ function resetFilters() {
|
|||||||
{{ session.receipt.result.failed }}
|
{{ session.receipt.result.failed }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="session.receipt.result.failed" class="alert alert-error mt-3" role="alert">
|
<p v-if="session.receipt.result.failed" class="alert alert-error mt-3" role="alert">
|
||||||
部分形态生图失败。已生成的图片保留;先查看失败原因,再按形态重新生图,或关闭新增候选图后补齐缺失主图。
|
部分形态生图失败。已生成的图片保留;先查看失败原因,再按形态重新生图。
|
||||||
|
</p>
|
||||||
|
<p v-else-if="session.receipt.title === '刷新过期人物形态图'" class="alert mt-3 text-xs">
|
||||||
|
新图当前仍是候选图。请进入对应形态的“查看图片与记录”,确认人物身份和造型后再设为主参考图;旧主图在此之前仍保持过期状态。
|
||||||
</p>
|
</p>
|
||||||
<ul class="mt-3 space-y-2 text-xs text-danger">
|
<ul class="mt-3 space-y-2 text-xs text-danger">
|
||||||
<li v-for="failure in session.receipt.result.failures" :key="failure.subjectFormId">
|
<li v-for="failure in session.receipt.result.failures" :key="failure.subjectFormId">
|
||||||
<code>{{ failure.subjectFormId }}</code
|
<code>{{ failure.subjectFormId }}</code>:{{ failure.error }}
|
||||||
>:{{ failure.error }}
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="mt-3 text-[11px] text-muted">回执仅保留于当前浏览器会话;图片以刷新后的数据库记录为准。</p>
|
<p class="mt-3 text-[11px] text-muted">回执仅保留于当前浏览器会话;图片以刷新后的数据库记录为准。</p>
|
||||||
</section>
|
</section>
|
||||||
<div class="my-6 flex flex-wrap items-center gap-4">
|
<div class="my-6 flex flex-wrap items-center gap-4">
|
||||||
<label class="search-field"
|
<label class="search-field">
|
||||||
><Search :size="14" /><input
|
<Search :size="14" />
|
||||||
v-model="search"
|
<input v-model="search" placeholder="搜索主体、引用或形态" aria-label="搜索形态图片" />
|
||||||
placeholder="搜索主体、引用或形态"
|
</label>
|
||||||
aria-label="搜索形态图片" /></label
|
<select v-model="module" class="input w-auto" aria-label="筛选主体类型">
|
||||||
><select v-model="module" class="input w-auto" aria-label="筛选主体类型">
|
|
||||||
<option value="all">全部类型</option>
|
<option value="all">全部类型</option>
|
||||||
<option value="character">人物</option>
|
<option value="character">人物</option>
|
||||||
<option value="scene">场景</option>
|
<option value="scene">场景</option>
|
||||||
<option value="prop">道具</option></select
|
<option value="prop">道具</option>
|
||||||
><label class="flex items-center gap-2 text-xs"
|
</select>
|
||||||
><input v-model="onlyMissing" type="checkbox" class="accent-accent" />仅看缺少主图</label
|
<label class="flex items-center gap-2 text-xs">
|
||||||
><span class="ml-auto text-xs text-muted">{{ filtered.length }} 个形态</span>
|
<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" />仅看身份过期
|
||||||
|
</label>
|
||||||
|
<span class="ml-auto text-xs text-muted">{{ filtered.length }} 个形态</span>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!query.data.value && query.loading.value" class="py-10 text-sm text-muted" role="status">
|
<p v-if="!query.data.value && query.loading.value" class="py-10 text-sm text-muted" role="status">
|
||||||
正在读取形态和已有图片…
|
正在读取形态和已有图片…
|
||||||
</p>
|
</p>
|
||||||
<div v-else-if="filtered.length" class="form-image-grid">
|
<div v-else-if="filtered.length" class="form-image-grid">
|
||||||
<article v-for="form in filtered" :key="form.id" class="panel form-image-card">
|
<article
|
||||||
|
v-for="form in filtered"
|
||||||
|
:key="form.id"
|
||||||
|
class="panel form-image-card"
|
||||||
|
:class="{ 'ring-1 ring-danger': isPrimaryIdentityStale(form) }"
|
||||||
|
>
|
||||||
<AssetImage
|
<AssetImage
|
||||||
:src="coverImage(form)?.imageUrl"
|
:src="coverImage(form)?.imageUrl"
|
||||||
:alt="`${form.subject.name} · ${form.name}`"
|
:alt="`${form.subject.name} · ${form.name}`"
|
||||||
@@ -216,25 +249,26 @@ function resetFilters() {
|
|||||||
<RouterLink
|
<RouterLink
|
||||||
:to="{ path: `/projects/${id}/subject-identity`, query: { subjectId: form.subjectId } }"
|
:to="{ path: `/projects/${id}/subject-identity`, query: { subjectId: form.subjectId } }"
|
||||||
class="text-button mt-3"
|
class="text-button mt-3"
|
||||||
>查看主体身份与母版 →</RouterLink
|
>查看主体身份与母版 →</RouterLink>
|
||||||
>
|
|
||||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||||
<StatusBadge v-if="hasRunningImages(form.images)" status="generating" /><span
|
<StatusBadge v-if="hasRunningImages(form.images)" status="generating" />
|
||||||
v-if="primaryImage(form.images)"
|
<span v-if="isPrimaryIdentityStale(form)" class="tag text-danger">身份母版已变更</span>
|
||||||
class="tag"
|
<span v-else-if="primaryImage(form.images)" class="tag">主参考图</span>
|
||||||
>主参考图</span
|
<span v-else-if="coverImage(form)" 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>
|
||||||
><span v-else-if="form.images[0]?.status === 'failed'" class="text-xs text-danger"
|
<span class="text-[10px] text-muted">{{ form.images.length }} 条记录</span>
|
||||||
>最近一次生成失败</span
|
|
||||||
><span class="text-[10px] text-muted">{{ form.images.length }} 条记录</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="isPrimaryIdentityStale(form)" class="alert alert-error mt-3 text-xs leading-6">
|
||||||
|
当前主图由旧演员母版生成,后续分镜已禁止继续引用。请基于当前 Identity Anchor 重新生成候选图,并在确认后设置为新的主参考图。
|
||||||
|
</p>
|
||||||
<p v-if="form.images[0]?.status === 'failed'" class="mt-2 line-clamp-2 text-xs text-danger">
|
<p v-if="form.images[0]?.status === 'failed'" class="mt-2 line-clamp-2 text-xs text-danger">
|
||||||
{{ form.images[0].error }}
|
{{ form.images[0].error }}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-4 flex flex-wrap items-center justify-between gap-2">
|
<div class="mt-4 flex flex-wrap items-center justify-between gap-2">
|
||||||
<button class="text-button" @click="openGallery(form)">查看图片与记录</button
|
<button class="text-button" @click="openGallery(form)">查看图片与记录</button>
|
||||||
><button class="button button-secondary" :disabled="blocked" @click="openGenerate(form)">
|
<button class="button button-secondary" :disabled="blocked" @click="openGenerate(form)">
|
||||||
<ImagePlus :size="14" />{{ coverImage(form) ? '再生成一张' : '生成图片' }}
|
<ImagePlus :size="14" />
|
||||||
|
{{ isPrimaryIdentityStale(form) ? '基于当前母版重新生成' : coverImage(form) ? '再生成一张' : '生成图片' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -248,11 +282,10 @@ 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
|
|
||||||
>
|
>
|
||||||
|
<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
|
<GenerateImageDialog
|
||||||
v-model:open="generateOpen"
|
v-model:open="generateOpen"
|
||||||
:form="generateForm"
|
:form="generateForm"
|
||||||
@@ -267,4 +300,4 @@ function resetFilters() {
|
|||||||
@changed="query.refresh"
|
@changed="query.refresh"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
Reference in New Issue
Block a user