fix: 补齐过期首帧素材定位与图库影响提示

This commit is contained in:
GouJ
2026-09-02 18:49:20 +08:00
parent f71d2bf0a4
commit 771b66cafc
11 changed files with 826 additions and 23 deletions
@@ -3,7 +3,7 @@ import WorkspacePage from '../../components/ui/WorkspacePage.vue'
import WorkspaceTools from '../../components/ui/WorkspaceTools.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 { useRoute, useRouter } from 'vue-router'
import { Search, RefreshCw, Download, ImagePlus, ArrowRight, AlertTriangle } from '@lucide/vue'
import { AssetImage, EmptyState, StatusBadge } from '../../components/ui'
import { downloadText } from '../../lib/format'
@@ -15,9 +15,12 @@ import GenerateImageDialog from './components/GenerateImageDialog.vue'
import ImageGalleryDialog from './components/ImageGalleryDialog.vue'
import FormPromptDialog from './components/FormPromptDialog.vue'
import FormPromptBatch from './components/FormPromptBatch.vue'
import { useKeyframeImpact } from './useKeyframeImpact'
import { queryText, referenceLocation, referenceTargets } from '../production/asset-links'
/** 形态图库以正式数据库为准,自动显示已在后端生成的图片。 */
const route = useRoute()
const router = useRouter()
const {
id,
forms,
@@ -40,6 +43,28 @@ const {
generateProject,
generateStale
} = useSubjectImages()
const sourceShotId = computed(() => queryText(route.query.sourceShotId))
const targetFormId = computed(() => queryText(route.query.subjectFormId))
const targetForm = computed(() => forms.value.find(form => form.id === targetFormId.value))
const impact = useKeyframeImpact(id, sourceShotId)
const sourceShot = impact.source
const linkedReferences = computed(() => referenceTargets(impact.references.data.value, []))
const sourceLocation = computed(() =>
sourceShot.value
? {
path: `/projects/${encodeURIComponent(id.value)}/production`,
query: { episodeNo: String(sourceShot.value.episodeNo), shotId: sourceShot.value.shotId }
}
: null
)
const impactOptions = computed(() => {
const items = [...impact.stale.value]
if (sourceShot.value && !items.some(item => item.shotId === sourceShot.value?.shotId)) items.push(sourceShot.value)
return items.map(item => ({
value: item.shotId,
label: `${item.episodeNo} 集 · BEAT ${item.beatNo} / 镜头 ${item.shotNo}${item.inconsistent ? ' · 状态待核对' : item.primaryKeyframeStale ? ' · 首帧过期' : ' · 当前镜头'}`
}))
})
const toolsOpen = ref(false)
/** 新批量回执到达时显示诊断,进入页面不自动遮挡图库。 */
watch(
@@ -71,6 +96,7 @@ const completeCount = computed(() => forms.value.filter(form => primaryImage(for
const filtered = computed(() =>
forms.value.filter(
form =>
(!targetFormId.value || form.id === targetFormId.value) &&
(module.value === 'all' || form.subject.module === module.value) &&
(!onlyMissing.value || !primaryImage(form.images)) &&
(!onlyStale.value || isPrimaryIdentityStale(form)) &&
@@ -80,9 +106,15 @@ const filtered = computed(() =>
)
)
watch(
() => route.query.subjectRef,
value => {
search.value = typeof value === 'string' ? value : ''
() => [route.query.subjectRef, route.query.subjectFormId, route.query.sourceShotId],
() => {
search.value = targetFormId.value ? '' : queryText(route.query.subjectRef)
module.value = 'all'
onlyMissing.value = false
onlyStale.value = false
galleryOpen.value = false
generateOpen.value = false
promptOpen.value = false
},
{ immediate: true }
)
@@ -117,6 +149,32 @@ function resetFilters() {
module.value = 'all'
onlyMissing.value = false
onlyStale.value = false
void router.replace({
query: { ...route.query, subjectFormId: undefined, subjectRef: undefined, sourceShotId: undefined }
})
}
/** 切换受影响镜头时清空之前的素材过滤,避免相关素材被旧筛选隐藏。 */
function selectImpactShot(shotId: string | null) {
void router.replace({
query: { ...route.query, sourceShotId: shotId || undefined, subjectFormId: undefined, subjectRef: undefined }
})
}
/** 同时刷新素材和镜头影响状态,查询仍不启动生成。 */
async function refreshAssets() {
await Promise.all([query.refresh(), impact.query.refresh(), impact.references.refresh()])
}
/** 全局过期筛选不沿用深链接的单个素材限制。 */
async function showStaleForms() {
await router.replace({
query: { ...route.query, subjectFormId: undefined, subjectRef: undefined, sourceShotId: undefined }
})
search.value = ''
module.value = 'all'
onlyMissing.value = false
onlyStale.value = true
}
</script>
@@ -129,7 +187,11 @@ function resetFilters() {
>{{ staleForms.length }} 个形态主图过期</NButton
>
<div class="toolbar-actions">
<NButton :disabled="query.loading.value" @click="query.refresh" text size="small"
<NButton
:disabled="query.loading.value || impact.query.loading.value"
@click="refreshAssets"
text
size="small"
><RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新图库 </NButton
><WorkspaceTools
v-model:open="toolsOpen"
@@ -289,6 +351,122 @@ function resetFilters() {
</WorkspaceTools>
</div>
</div>
<NAlert v-if="staleForms.length" type="error" :show-icon="false" class="mt-3" title="形态主图需要更新">
{{ staleForms.length }}
个形态主图与当前已锁定身份母版不一致请生成候选验图并切换主图再重建相关首帧
<NButton text size="small" @click="showStaleForms">筛选身份过期形态</NButton>
</NAlert>
<NAlert
v-if="impact.stale.value.length"
type="warning"
:show-icon="false"
class="mt-3"
title="下游主首帧需要处理"
data-keyframe-impact
>
{{ impact.stale.value.length }}
个镜头被后端报告主首帧过期此警告不表示形态图片本身失效请选择镜头核对当前素材确认过期后再重建首帧
<p v-if="impact.stale.value.some(item => item.inconsistent)" class="mt-2">
其中
{{ impact.stale.value.filter(item => item.inconsistent).length }}
个镜头的首帧与视频检查结果不一致不建议直接重复生图
</p>
</NAlert>
<NAlert v-if="impact.query.error.value" type="warning" :show-icon="false" class="mt-3"
>镜头影响检查失败暂时无法确认有无过期首帧{{ impact.query.error.value }}
<NButton text size="small" @click="impact.query.refresh">重试检查</NButton></NAlert
>
<div
v-if="impactOptions.length || sourceShotId"
class="asset-impact-context mt-3"
aria-label="镜头与素材定位"
>
<div class="flex flex-wrap items-center gap-3">
<NSelect
:value="sourceShot?.shotId ?? null"
:options="impactOptions"
placeholder="选择过期镜头,查看关联素材"
clearable
aria-label="选择关联镜头"
class="asset-impact-select"
@update:value="selectImpactShot"
/>
<RouterLink v-if="sourceLocation" :to="sourceLocation" class="text-button"
>返回第 {{ sourceShot?.episodeNo }} · 镜头 {{ sourceShot?.shotNo }} </RouterLink
>
<NButton v-if="targetFormId || sourceShotId" text size="small" @click="resetFilters"
>查看全部素材</NButton
>
</div>
<p
v-if="sourceShotId && !sourceShot && !impact.query.loading.value && !impact.query.error.value"
class="mt-3 text-danger"
>
来源镜头不存在或不属于当前项目未读取其素材
</p>
<p
v-if="
sourceShot &&
!sourceShot.primaryKeyframeStale &&
!impact.query.error.value &&
!impact.query.loading.value
"
class="mt-3 text-xs"
>
当前检查:{{
sourceShot.primaryKeyframeId ? '该镜头主首帧未被标记为过期' : '该镜头暂无主首帧'
}}。请以后端最新就绪状态为准。
</p>
<p v-if="sourceShot?.inconsistent" class="mt-3 text-xs text-danger">
当前镜头:首帧检查未标记过期,但视频检查报告过期。请核对后端检查逻辑与身份母版,暂不建议重复生图。
</p>
<p
v-if="
targetForm &&
impact.references.data.value &&
!impact.references.error.value &&
!impact.relatedForms.value.has(targetForm.id)
"
class="mt-3 text-xs text-danger"
>
指定形态不在该镜头当前的有效引用列表中,关联可能已调整或主图缺失,请从下方关联素材重新核对。
</p>
<div
v-if="linkedReferences.length && !impact.references.error.value"
class="mt-3 flex flex-wrap items-center gap-3"
>
<span class="text-xs text-muted">本镜头当前关联素材:</span
><RouterLink
v-for="target in linkedReferences"
:key="target.formId || target.subjectRef"
:to="referenceLocation(id, sourceShotId, target)"
class="text-button"
>{{ target.label }} →</RouterLink
>
</div>
<p v-if="impact.references.loading.value" class="mt-2 text-xs text-muted">正在读取镜头关联素材…</p>
<p v-if="impact.references.error.value" class="mt-2 text-xs text-danger">
{{ impact.references.error.value }}
<NButton text size="small" @click="impact.references.refresh">重试读取素材</NButton>
</p>
</div>
<NAlert
v-if="targetFormId"
:type="targetForm ? 'info' : 'warning'"
:show-icon="false"
class="mt-3"
data-focused-material
>
<template v-if="targetForm"
>已定位:{{ targetForm.subject.name }} · {{ targetForm.name }}{{
targetForm.subject.ref
}})。可直接查看下方图片与记录、主体身份与母版。</template
>
<template v-else>{{
query.loading.value ? '正在定位素材…' : '指定形态不存在或已被移除,请从本镜头当前关联素材重新选择。'
}}</template>
<NButton text size="small" @click="resetFilters">清除定位</NButton>
</NAlert>
<div class="form-image-filter-region">
<div class="form-image-filters" role="group" aria-label="形态图片筛选">
<NInput
@@ -329,7 +507,11 @@ function resetFilters() {
v-for="form in filtered"
:key="form.id"
class="panel form-image-card"
:class="{ 'ring-1 ring-danger': isPrimaryIdentityStale(form) }"
:class="{
'ring-1 ring-danger': isPrimaryIdentityStale(form),
'form-image-card-focused': targetFormId === form.id
}"
:data-form-id="form.id"
>
<AssetImage
:src="coverImage(form)?.imageUrl"
@@ -373,6 +555,29 @@ function resetFilters() {
当前主图未能匹配最新已锁定母版。请基于当前 Identity Anchor
重新生成候选图,并在确认后设置为新的主参考图。
</NAlert>
<NAlert
v-if="
sourceShot?.primaryKeyframeStale &&
impact.relatedForms.value.has(form.id) &&
!impact.references.error.value
"
type="warning"
:show-icon="false"
class="mt-3 text-xs leading-6"
>
第 {{ sourceShot.episodeNo }} 集 · BEAT {{ sourceShot.beatNo }} / 镜头
{{ sourceShot.shotNo }} 的待处理首帧关联此素材。
{{
sourceShot.inconsistent
? '后端首帧与视频检查结果不一致,请先核对,不要据此重复生图。'
: isPrimaryIdentityStale(form)
? '请先更新该形态主图,再重建首帧。'
: '关联不等于本素材失效;核对形态主图与身份母版,确认无误后只需重建首帧。'
}}
<RouterLink v-if="sourceLocation" :to="sourceLocation" class="text-button mt-2"
>返回该镜头处理 →</RouterLink
>
</NAlert>
<p v-if="form.images[0]?.status === 'failed'" class="mt-2 line-clamp-2 text-xs text-danger">
{{ form.images[0].error }}
</p>
@@ -419,6 +624,6 @@ function resetFilters() {
:project-id="id"
:form="galleryForm"
:disabled="blocked || !galleryForm"
@changed="query.refresh"
@changed="refreshAssets"
/></WorkspacePage>
</template>