167 lines
8.2 KiB
Vue
167 lines
8.2 KiB
Vue
<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
|
||
:src="selected.status === 'completed' ? selected.imageUrl : null"
|
||
:alt="`${subjectName} · ${identityViewLabels[selected.viewType]}`"
|
||
class="asset-image-preview mt-4"
|
||
:empty-text="selected.status === 'failed' ? '本次生成失败' : '等待后端图片结果'"
|
||
/>
|
||
<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" 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>
|
||
<NScrollbar
|
||
x-scrollable
|
||
content-class="image-history-content"
|
||
class="image-history mt-4"
|
||
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="image.status === 'failed' ? '失败' : '无图'"
|
||
/><span class="mt-1 block text-[10px]">{{
|
||
image.isAnchor
|
||
? '当前母版'
|
||
: image.viewType === 'primary'
|
||
? '母版候选'
|
||
: identityViewLabels[image.viewType]
|
||
}}</span></NButton
|
||
>
|
||
</NScrollbar>
|
||
</template>
|
||
<p v-else class="py-8 text-sm text-muted">尚无身份参考图。先保存身份提示词,再生成第一张母版。</p>
|
||
</section>
|
||
</template>
|