fix: 统一图库滚动区域并吸顶镜头筛选栏

This commit is contained in:
GouJ
2026-09-04 11:21:39 +08:00
parent 12f7e69e88
commit acc0a1e9ad
4 changed files with 265 additions and 139 deletions
+157 -138
View File
@@ -3,7 +3,18 @@ import WorkspacePage from '../../components/ui/WorkspacePage.vue'
import ActionMenu from '../../components/ui/ActionMenu.vue'
import DetailDisclosure from '../../components/ui/DetailDisclosure.vue'
import WorkspaceTools from '../../components/ui/WorkspaceTools.vue'
import { NAlert, NButton, NCheckbox, NCollapse, NCollapseItem, NInput, NInputNumber, NSelect, NTag } from 'naive-ui'
import {
NAlert,
NButton,
NCheckbox,
NCollapse,
NCollapseItem,
NInput,
NInputNumber,
NScrollbar,
NSelect,
NTag
} from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Search, RefreshCw, Download, ImagePlus, ArrowLeft, ArrowRight, LayoutGrid, AlertTriangle } from '@lucide/vue'
@@ -211,7 +222,8 @@ async function showStaleForms() {
<template>
<WorkspacePage compact class="gallery-workspace-page"
><template #header>
><template #default>
<!-- 筛选关联说明和图片共用正文滚动区避免镜头切换时两块滚动区域高度不同步 -->
<div class="workspace-toolbar">
<span class="toolbar-summary">主图 {{ completeCount }} / {{ forms.length }}</span>
<NButton v-if="staleForms.length" text type="error" size="small" @click="toolsOpen = true"
@@ -495,9 +507,15 @@ async function showStaleForms() {
<p v-if="targetUnlinked" class="text-xs text-danger">
指定形态不在该镜头当前的有效引用列表中关联可能已调整或主图缺失请从下方关联素材重新核对
</p>
<div
<NScrollbar
v-if="linkedReferences.length && !impact.references.error.value"
class="flex flex-wrap items-center gap-3"
:key="sourceShotId"
class="asset-impact-links-scroll"
content-class="asset-impact-links"
x-scrollable
trigger="none"
role="region"
aria-label="本镜头关联素材可横向滚动"
>
<span class="text-xs text-muted">本镜头当前关联素材</span
><RouterLink
@@ -507,7 +525,7 @@ async function showStaleForms() {
class="text-button"
>{{ target.label }} </RouterLink
>
</div>
</NScrollbar>
<p v-if="impact.references.loading.value" class="text-xs text-muted">正在读取镜头关联素材</p>
<p v-if="impact.references.error.value" class="text-xs text-danger">
{{ impact.references.error.value }}
@@ -536,141 +554,142 @@ async function showStaleForms() {
><NAlert v-if="running" role="status" type="info" :show-icon="false" class="mt-4">
数据库中仍有排队或生成中的图片已暂停新的生图操作长时间无变化时请先核对后台任务刷新或关闭页面不会取消任务
</NAlert>
</template>
<p v-if="!query.data.value && query.loading.value" class="py-10 text-sm text-muted" role="status">
正在读取形态和已有图片
</p>
<div v-else-if="filtered.length" class="form-image-grid">
<article
v-for="form in filtered"
:key="form.id"
class="panel form-image-card"
:class="{
'ring-1 ring-danger': isPrimaryIdentityStale(form),
'form-image-card-focused': targetFormId === form.id
}"
:data-form-id="form.id"
>
<AssetImage
:src="coverImage(form)?.imageUrl"
:alt="`${form.subject.name} · ${form.name}`"
object-fit="cover"
preview
:empty-text="hasRunningImages(form.images) ? '后台正在生成图片' : '尚未生成图片'"
/>
<div class="p-4">
<div class="flex flex-wrap items-center gap-2">
<h3 class="text-sm font-semibold">{{ form.subject.name }}</h3>
<code class="subject-ref">{{ form.subject.ref }}</code>
</div>
<p class="mt-2 text-xs">
{{ form.name }}<NTag v-if="form.isDefault" size="small" :bordered="false">默认形态</NTag>
</p>
<p v-if="form.description" class="mt-3 line-clamp-2 text-xs leading-6 text-muted">
{{ form.description }}
</p>
<div class="mt-3 flex flex-wrap items-center gap-2">
<StatusBadge v-if="hasRunningImages(form.images)" status="generating" />
<NTag v-if="isPrimaryIdentityStale(form)" size="small" :bordered="false">身份母版已变更</NTag>
<NTag v-else-if="primaryImage(form.images)" size="small" :bordered="false">主参考图</NTag>
<NTag v-else-if="coverImage(form)" size="small" :bordered="false">候选图 · 尚未设主图</NTag>
<span v-else-if="form.images[0]?.status === 'failed'" class="text-xs text-danger"
>最近一次生成失败</span
<p v-if="!query.data.value && query.loading.value" class="py-10 text-sm text-muted" role="status">
正在读取形态和已有图片
</p>
<div v-else-if="filtered.length" class="form-image-grid">
<article
v-for="form in filtered"
:key="form.id"
class="panel form-image-card"
:class="{
'ring-1 ring-danger': isPrimaryIdentityStale(form),
'form-image-card-focused': targetFormId === form.id
}"
:data-form-id="form.id"
>
<AssetImage
:src="coverImage(form)?.imageUrl"
:alt="`${form.subject.name} · ${form.name}`"
object-fit="cover"
preview
:empty-text="hasRunningImages(form.images) ? '后台正在生成图片' : '尚未生成图片'"
/>
<div class="p-4">
<div class="flex flex-wrap items-center gap-2">
<h3 class="text-sm font-semibold">{{ form.subject.name }}</h3>
<code class="subject-ref">{{ form.subject.ref }}</code>
</div>
<p class="mt-2 text-xs">
{{ form.name }}<NTag v-if="form.isDefault" size="small" :bordered="false">默认形态</NTag>
</p>
<p v-if="form.description" class="mt-3 line-clamp-2 text-xs leading-6 text-muted">
{{ form.description }}
</p>
<div class="mt-3 flex flex-wrap items-center gap-2">
<StatusBadge v-if="hasRunningImages(form.images)" status="generating" />
<NTag v-if="isPrimaryIdentityStale(form)" size="small" :bordered="false"
>身份母版已变更</NTag
>
<NTag v-else-if="primaryImage(form.images)" size="small" :bordered="false">参考</NTag>
<NTag v-else-if="coverImage(form)" size="small" :bordered="false">候选图 · 尚未设主图</NTag>
<span v-else-if="form.images[0]?.status === 'failed'" class="text-xs text-danger"
>最近一次生成失败</span
>
<span class="text-[10px] text-muted">{{ form.images.length }} 条记录</span>
</div>
<NAlert
v-if="isPrimaryIdentityStale(form)"
type="error"
:show-icon="false"
class="mt-3 text-xs leading-6"
>
<span class="text-[10px] text-muted">{{ form.images.length }} 条记录</span>
</div>
<NAlert
v-if="isPrimaryIdentityStale(form)"
type="error"
:show-icon="false"
class="mt-3 text-xs leading-6"
>
当前主图未能匹配最新已锁定母版请基于当前 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>
<div class="mt-4 flex flex-wrap items-center justify-between gap-2">
<NButton @click="openGallery(form)" text size="small">查看图片与记录</NButton>
<ActionMenu
:label="`${form.subject.name} · ${form.name} 更多操作`"
:items="[
{ key: 'prompt', label: '提示词与生成' },
{ key: 'identity', label: '主体身份与母版' }
]"
@select="
$event === 'prompt'
? openPrompt(form)
: router.push({
path: `/projects/${id}/subject-identity`,
query: { subjectId: form.subjectId }
})
当前主图未能匹配最新已锁定母版请基于当前 Identity Anchor
重新生成候选图并在确认后设置为新的主参考图
</NAlert>
<NAlert
v-if="
sourceShot?.primaryKeyframeStale &&
impact.relatedForms.value.has(form.id) &&
!impact.references.error.value
"
/>
<NButton :disabled="blocked" @click="openGenerate(form)"
><ImagePlus :size="14" />
{{
isPrimaryIdentityStale(form)
? '基于当前母版重新生成'
: coverImage(form)
? '再生成一张'
: '生成图片'
}}</NButton
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>
<div class="mt-4 flex flex-wrap items-center justify-between gap-2">
<NButton @click="openGallery(form)" text size="small">查看图片与记录</NButton>
<ActionMenu
:label="`${form.subject.name} · ${form.name} 更多操作`"
:items="[
{ key: 'prompt', label: '提示词与生成' },
{ key: 'identity', label: '主体身份与母版' }
]"
@select="
$event === 'prompt'
? openPrompt(form)
: router.push({
path: `/projects/${id}/subject-identity`,
query: { subjectId: form.subjectId }
})
"
/>
<NButton :disabled="blocked" @click="openGenerate(form)"
><ImagePlus :size="14" />
{{
isPrimaryIdentityStale(form)
? '基于当前母版重新生成'
: coverImage(form)
? '再生成一张'
: '生成图片'
}}</NButton
>
</div>
</div>
</div>
</article>
</div>
<EmptyState
v-else-if="query.data.value"
:title="forms.length ? '没有匹配的形态' : '还没有正式形态'"
:description="
forms.length
? '调整筛选条件后再查看。批量生图不受筛选影响。'
: '先完成剧本拆解和主体形态持久化,再来生成参考图。'
"
>
<NButton v-if="forms.length" @click="resetFilters">清除筛选</NButton>
<RouterLink v-else :to="`/projects/${id}/breakdown`" class="button button-secondary"
>前往剧本拆解</RouterLink
> </EmptyState
><FormPromptDialog
v-model:open="promptOpen"
:form="promptForm"
:disabled="blocked || !promptForm"
@generate="generatePrompt" /><GenerateImageDialog
v-model:open="generateOpen"
:form="generateForm"
:disabled="blocked || !generateForm"
@generate="submitGenerate" /><ImageGalleryDialog
v-model:open="galleryOpen"
:project-id="id"
:form="galleryForm"
:disabled="blocked || !galleryForm"
@changed="refreshAssets"
/></WorkspacePage>
</article>
</div>
<EmptyState
v-else-if="query.data.value"
:title="forms.length ? '没有匹配的形态' : '还没有正式形态'"
:description="
forms.length
? '调整筛选条件后再查看。批量生图不受筛选影响。'
: '先完成剧本拆解和主体形态持久化,再来生成参考图。'
"
>
<NButton v-if="forms.length" @click="resetFilters">清除筛选</NButton>
<RouterLink v-else :to="`/projects/${id}/breakdown`" class="button button-secondary"
>前往剧本拆解</RouterLink
> </EmptyState
><FormPromptDialog
v-model:open="promptOpen"
:form="promptForm"
:disabled="blocked || !promptForm"
@generate="generatePrompt" /><GenerateImageDialog
v-model:open="generateOpen"
:form="generateForm"
:disabled="blocked || !generateForm"
@generate="submitGenerate" /><ImageGalleryDialog
v-model:open="galleryOpen"
:project-id="id"
:form="galleryForm"
:disabled="blocked || !galleryForm"
@changed="refreshAssets" /></template
></WorkspacePage>
</template>