fix: 补齐过期首帧素材定位与图库影响提示
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user