Files
short-drama-agent-front/src/features/subject-images/components/ImageGalleryDialog.vue
T

198 lines
9.0 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, NTag } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { ExternalLink, RefreshCw } from '@lucide/vue'
import { AppDialog, AssetImage, StatusBadge } from '../../../components/ui'
import { usePolling } from '../../../composables/usePolling'
import { referenceImageUrl } from '../../../lib/assets'
import { formatDate } from '../../../lib/format'
import { getOperation, runOperation } from '../../workflows/operations'
import { subjectImagesApi } from '../api'
import { hasRunningImages, primaryImage, currentIdentityAnchorId } from '../model'
import type { SubjectFormAsset } from '../types'
/** 形态图片历史与主图选择;历史查询只在弹窗打开时进行。 */
const props = defineProps<{ projectId: string; form: SubjectFormAsset | null; disabled: boolean }>()
const open = defineModel<boolean>('open', { default: false })
const emit = defineEmits<{ changed: [] }>()
const selectedId = ref('')
const confirming = ref(false)
const key = computed(() => (open.value ? (props.form?.id ?? '') : ''))
const query = usePolling(
key,
async (id, signal) => {
if (!id) return null
const rows = await subjectImagesApi.listImages(id, signal)
if (rows.some(image => image.subjectFormId !== id)) throw new Error('图片记录与当前形态不匹配,请刷新后重试。')
return rows
},
false
)
const images = computed(() => query.data.value ?? [])
const selected = computed(
() => images.value.find(image => image.id === selectedId.value) ?? primaryImage(images.value) ?? images.value[0]
)
const operation = computed(() => getOperation(props.projectId))
const canSetPrimary = computed(
() =>
!props.disabled &&
!operation.value.pending &&
!query.error.value &&
!hasRunningImages(images.value) &&
selected.value?.status === 'completed' &&
!!selected.value.imageUrl &&
!selected.value.isPrimary
)
const imageUrl = computed(() => (selected.value?.imageUrl ? referenceImageUrl(selected.value.imageUrl) : null))
watch(key, () => {
selectedId.value = ''
confirming.value = false
})
watch(
() => selected.value?.id,
() => {
confirming.value = false
}
)
/** 切换主图只调用 PUT,不调用模型;目标在提交时固定。 */
async function choosePrimary() {
if (!canSetPrimary.value || !confirming.value || !selected.value || !props.form) return
const formId = props.form.id
const imageId = selected.value.id
const projectId = props.projectId
confirming.value = false
await runOperation(projectId, `设置 ${props.form.subject.name} · ${props.form.name} 主图`, async () => {
const result = await subjectImagesApi.setPrimary(formId, imageId)
if (
!result ||
result.id !== imageId ||
result.subjectFormId !== formId ||
!result.isPrimary ||
result.status !== 'completed'
)
throw new Error('接口未确认主图切换,请先刷新核对后端状态。')
})
emit('changed')
await query.refresh()
}
</script>
<template>
<AppDialog
v-model:open="open"
:title="`${form?.subject.name || ''} · ${form?.name || '形态图片'}`"
description="主参考图会供后续分镜使用。切换主图不会重新生成已有视频提示词。"
wide
>
<div class="mt-5 flex items-center justify-between">
<span class="text-xs text-muted">{{ images.length }} 条图片记录</span
><NButton :disabled="query.loading.value" @click="query.refresh" text size="small"
><RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新图片
</NButton>
</div>
<NAlert v-if="query.error.value" role="alert" type="error" :show-icon="false" class="mt-3"
>{{ query.error.value }} 当前保留上次读取的记录主图操作已暂停
</NAlert>
<NAlert v-if="operation.error" role="alert" type="error" :show-icon="false" class="mt-3">{{
operation.error
}}</NAlert>
<div v-if="selected" class="mt-4">
<AssetImage
:key="selected.id"
:src="selected.status === 'completed' ? selected.imageUrl : null"
:alt="`${form?.subject.name} · ${form?.name}`"
:empty-text="selected.status === 'failed' ? '本次生成失败' : '等待后端生成结果'"
class="asset-image-preview"
object-fit="contain"
preview
/>
<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="`${form?.name}候选图`"
:retryable="false"
:empty-text="
{ pending: '排队中', generating: '生成中', failed: '生成失败', completed: '图片缺失' }[
image.status
]
"
/><span class="image-history-label">{{
image.isPrimary
? '主参考图'
: { completed: '候选图', failed: '失败记录', pending: '排队中', generating: '生成中' }[
image.status
]
}}</span></NButton
>
</NScrollbar>
<div class="mt-4 flex flex-wrap items-center justify-between gap-3">
<div class="flex items-center gap-2">
<StatusBadge :status="selected.status" /><NTag
v-if="selected.isPrimary"
size="small"
:bordered="false"
>当前主参考图</NTag
><span class="text-xs text-muted">{{ selected.width || '—' }} × {{ selected.height || '—' }}</span>
</div>
<a
v-if="imageUrl && selected.status === 'completed'"
:href="imageUrl"
target="_blank"
rel="noopener noreferrer"
class="text-button"
>打开原图<ExternalLink :size="12"
/></a>
</div>
<NAlert
v-if="selected.error"
role="alert"
type="error"
:show-icon="false"
class="mt-3 whitespace-pre-wrap"
>{{ selected.error }}</NAlert
>
<div class="mt-4 flex flex-wrap items-center gap-3">
<NButton v-if="!selected.isPrimary" :disabled="!canSetPrimary" @click="confirming = true">
设为主参考图</NButton
><span class="text-[11px] text-muted"
>{{ selected.provider }} · {{ selected.model }} · {{ formatDate(selected.createdAt) }}</span
>
</div>
<NAlert v-if="confirming" type="info" :show-icon="false" class="mt-3"
><p class="text-xs">确认将这张图用于此形态后续的分镜参考旧主图仍会保留此操作不调用模型</p>
<div class="mt-3 flex gap-3">
<NButton :disabled="!canSetPrimary" @click="choosePrimary" type="primary"> 确认切换主图</NButton
><NButton @click="confirming = false" text size="small">取消</NButton>
</div></NAlert
>
<p class="mt-3 break-all font-mono text-[10px] text-muted">Image ID · {{ selected.id }}</p>
<p v-if="form && currentIdentityAnchorId(form)" class="mt-2 break-all text-[11px] text-muted">
当前已锁定身份母版 ID{{ currentIdentityAnchorId(form) }}
</p>
</div>
<p v-else class="py-10 text-center text-sm text-muted">
{{ query.loading.value ? '正在读取图片记录…' : '此形态尚无图片记录,可以先生成一张。' }}
</p>
</AppDialog>
</template>