Files
short-drama-agent-front/src/features/subject-identity/components/IdentityGallery.vue
T

183 lines
9.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { NScrollbar, 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'
import { formatDate } from '../../../lib/format'
import { canBeAnchor, currentAnchor, identityViewLabels, readImageProvenance } from '../model'
import type { IdentityImage } from '../types'
/** 身份图库区分权威母版、primary 候选和辅助视图,保留后端原始状态。 */
const props = defineProps<{
images: IdentityImage[]
subjectName: string
module: string
identityLocked: boolean
disabled: boolean
}>()
const emit = defineEmits<{ anchor: [imageId: string] }>()
const selectedId = ref('')
const confirming = ref(false)
const anchor = computed(() => currentAnchor(props.images))
const selected = computed(
() => props.images.find(image => image.id === selectedId.value) ?? anchor.value ?? props.images[0]
)
const character = computed(() => props.module === 'character')
const canSelect = computed(
() =>
!props.disabled &&
!!selected.value &&
canBeAnchor(selected.value) &&
(!selected.value.isAnchor || (character.value && !props.identityLocked))
)
const imageUrl = computed(() => referenceImageUrl(selected.value?.imageUrl ?? ''))
watch(
() => selected.value?.id,
() => {
confirming.value = false
}
)
/** 只发出用户确认过的目标 ID,父页面再核验图片归属。 */
function chooseAnchor() {
if (!canSelect.value || !confirming.value || !selected.value) return
emit('anchor', selected.value.id)
confirming.value = false
}
</script>
<template>
<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>
<NTag size="small" :bordered="false">{{ anchor ? '已选母版' : '尚无母版' }}</NTag>
</div>
<p class="mt-2 text-xs leading-6 text-muted">
身份母版确定是谁”;形态主图确定该造型长什么样”。切换母版仅影响之后的生成不会替换已有形态图分镜参考图或提示词
</p>
<NAlert v-if="character && anchor" type="info" :show-icon="false" class="mt-3 text-xs leading-6">
如果要更换演员身份请生成新的选角候选并确认选角正面三分之四全身等辅助身份图会继续引用当前母版只用于保持同一演员不会重新选择人物
</NAlert>
<template v-if="selected">
<AssetImage
:key="selected.id"
:src="selected.status === 'completed' ? selected.imageUrl : null"
:alt="`${subjectName} · ${identityViewLabels[selected.viewType]}`"
class="asset-image-preview mt-4"
preview
:empty-text="selected.status === 'failed' ? '本次生成失败' : '等待后端图片结果'"
/>
<div class="image-history-heading">
<h4 class="font-medium">历史记录 · {{ images.length }} </h4>
<span class="text-muted">点击缩略图切换预览可横向滚动</span>
</div>
<NScrollbar
x-scrollable
trigger="none"
content-class="image-history-content"
:content-style="{ width: 'max-content' }"
class="image-history"
aria-label="身份图片历史"
>
<NButton
v-for="image in images"
:key="image.id"
:aria-label="`查看身份图片 ${image.id}`"
:aria-pressed="image.id === selected.id"
@click="selectedId = image.id"
class="image-history-item"
:class="{ selected: image.id === selected.id }"
><AssetImage
:src="image.status === 'completed' ? image.imageUrl : null"
:alt="identityViewLabels[image.viewType]"
:retryable="false"
:empty-text="
{ pending: '排队中', generating: '生成中', failed: '生成失败', completed: '图片缺失' }[
image.status
]
"
/><span class="image-history-label">{{
image.isAnchor
? '当前母版'
: image.viewType === 'primary'
? '母版候选'
: identityViewLabels[image.viewType]
}}</span>
<span class="image-history-meta"
><StatusBadge :status="image.status" /><span>{{ image.enabled ? '启用' : '停用' }}</span></span
>
</NButton>
</NScrollbar>
<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" /><NTag size="small" :bordered="false">{{
selected.isAnchor
? '当前身份母版'
: selected.viewType === 'primary'
? '母版候选'
: identityViewLabels[selected.viewType]
}}</NTag
><span class="text-xs text-muted"
>{{ selected.enabled ? '启用' : '停用' }} · {{ selected.width || '—' }} ×
{{ selected.height || '—' }}</span
>
</div>
<a
v-if="imageUrl && selected.status === 'completed'"
:href="imageUrl"
target="_blank"
rel="noopener noreferrer"
class="text-button"
>打开原图</a
>
</div>
<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">
<NButton
v-if="!selected.isAnchor || (character && !identityLocked)"
:disabled="!canSelect"
@click="confirming = true"
>{{ character ? '确认选角并锁定' : '设为身份母版' }}</NButton
><span class="text-[11px] text-muted"
>{{ selected.provider }} · {{ selected.model }} · {{ formatDate(selected.createdAt) }}</span
>
</div>
<p v-if="selected.viewType !== 'primary'" class="mt-2 text-xs text-muted">
辅助视图不能直接设为母版它会继承当前身份母版不适合用来更换演员
</p>
<NAlert v-if="confirming" type="info" :show-icon="false" class="mt-3"
><p class="text-xs">
{{
character
? '确认选择这张图片作为正式演员?后端会在同一事务中切换身份母版并锁定 Identity,完成后首帧才具备稳定人物身份。'
: '确认切换身份母版?旧 primary 母版将停用并保留。后续场景形态生图会引用新母版。此操作不调用模型。'
}}
</p>
<div class="mt-3 flex gap-3">
<NButton :disabled="!canSelect" @click="chooseAnchor" type="primary">{{
character ? '确认演员选择' : '确认切换身份母版'
}}</NButton
><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>
</section>
</template>