feat(short-drama-agent-front): 调整工作台布局与图库瀑布流

This commit is contained in:
GJ
2026-09-07 14:17:53 +08:00
parent a37b0b613f
commit 0f8b2074e4
14 changed files with 209 additions and 118 deletions
@@ -15,7 +15,7 @@ import {
NSelect,
NTag
} from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { computed, ref, watch, type Directive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
Search,
@@ -40,7 +40,7 @@ import FormPromptDialog from './components/FormPromptDialog.vue'
import FormPromptBatch from './components/FormPromptBatch.vue'
import { useKeyframeImpact } from './useKeyframeImpact'
import { queryText, referenceLocation, referenceTargets } from '../production/asset-links'
import { formCoverAspectRatio, type GalleryLayout } from './layout'
import { masonryRowSpan, type GalleryLayout } from './layout'
/** 形态图库以正式数据库为准;首次读取、手动刷新和自身操作完成后更新,不定时刷新。 */
const route = useRoute()
@@ -139,6 +139,75 @@ const layout = ref<GalleryLayout>('grid')
const module = ref('all')
const onlyMissing = ref(false)
const onlyStale = ref(false)
/** 保存每张卡片的尺寸观察器,切换回网格时及时释放。 */
const masonryCardObservers = new WeakMap<HTMLElement, ResizeObserver>()
/**
* 按素材顺序重新计算整个瀑布流的位置。
* @param container 瀑布流容器元素
*/
function syncMasonryLayout(container: HTMLElement) {
if (!container.classList.contains('form-image-masonry')) return
const styles = getComputedStyle(container)
const rowHeight = Number.parseFloat(styles.gridAutoRows)
const rowGap = Number.parseFloat(styles.rowGap)
const columnCount = Math.max(1, styles.gridTemplateColumns.split(/\s+/).filter(Boolean).length)
const nextRows = Array<number>(columnCount).fill(1)
const cards = Array.from(container.children).filter(
(element): element is HTMLElement =>
element instanceof HTMLElement && element.classList.contains('form-image-card')
)
cards.forEach((card, index) => {
const column = index % columnCount
const span = masonryRowSpan(card.getBoundingClientRect().height, rowHeight, rowGap)
card.style.setProperty('--form-image-column', String(column + 1))
const rowStart = nextRows[column] ?? 1
card.style.setProperty('--form-image-row-start', String(rowStart))
card.style.setProperty('--form-image-row-span', String(span))
nextRows[column] = rowStart + span
})
}
/**
* 开始监听瀑布流卡片尺寸变化。
* @param element 形态图片卡片元素
*/
function observeMasonryCard(element: HTMLElement) {
if (masonryCardObservers.has(element) || typeof ResizeObserver === 'undefined') return
const observer = new ResizeObserver(() => {
if (element.parentElement) syncMasonryLayout(element.parentElement)
})
observer.observe(element)
masonryCardObservers.set(element, observer)
if (element.parentElement) syncMasonryLayout(element.parentElement)
}
/**
* 停止监听瀑布流卡片并清理网格跨度。
* @param element 形态图片卡片元素
*/
function unobserveMasonryCard(element: HTMLElement) {
masonryCardObservers.get(element)?.disconnect()
masonryCardObservers.delete(element)
element.style.removeProperty('--form-image-column')
element.style.removeProperty('--form-image-row-start')
element.style.removeProperty('--form-image-row-span')
}
/** 让卡片按实际高度占据网格行,保持从左到右的 DOM 排列顺序。 */
const vMasonryCard: Directive<HTMLElement, boolean> = {
mounted(element, binding) {
if (binding.value) observeMasonryCard(element)
},
updated(element, binding) {
if (binding.value === binding.oldValue) return
if (binding.value) observeMasonryCard(element)
else unobserveMasonryCard(element)
},
beforeUnmount(element) {
unobserveMasonryCard(element)
}
}
const generateOpen = ref(false)
const galleryOpen = ref(false)
const generateId = ref('')
@@ -593,7 +662,7 @@ async function showStaleForms() {
<NAlert v-if="query.error.value" role="alert" type="error" :show-icon="false" class="mt-4"
>{{ query.error.value }}<span v-if="forms.length"> 当前展示上次读取的数据</span></NAlert
><NAlert v-if="running" role="status" type="info" :show-icon="false" class="mt-4">
数据库中仍有排队或生成中的图片已暂停新的生图操作长时间无变化时请先核对后台任务刷新或关闭页面不会取消任务
数据库中仍有排队或生成中的图片已暂停新的生图操作长时间无变化时请先核对任务状态
</NAlert>
<p v-if="!query.data.value && query.loading.value" class="py-10 text-sm text-muted" role="status">
正在读取形态和已有图片
@@ -605,6 +674,7 @@ async function showStaleForms() {
>
<article
v-for="form in filtered"
v-masonry-card="layout === 'masonry'"
:key="form.id"
class="panel form-image-card"
:class="{
@@ -612,7 +682,6 @@ async function showStaleForms() {
'form-image-card-focused': targetFormId === form.id
}"
:data-form-id="form.id"
:style="{ '--form-image-aspect': formCoverAspectRatio(form) }"
>
<AssetImage
:src="coverImage(form)?.imageUrl"
@@ -759,13 +828,21 @@ async function showStaleForms() {
@apply pt-3;
}
.gallery-workspace-page .form-image-masonry {
@apply block columns-[260px] gap-x-5;
@apply grid grid-cols-[repeat(auto-fill,_minmax(min(260px,_100%),_1fr))] gap-x-5 items-start;
grid-auto-flow: row dense;
grid-auto-rows: 1px;
}
.form-image-masonry > .form-image-card {
@apply break-inside-avoid mb-5;
grid-column-start: var(--form-image-column, auto);
grid-row-start: var(--form-image-row-start, auto);
grid-row-end: span var(--form-image-row-span, 1);
}
.form-image-masonry .asset-image {
@apply aspect-[var(--form-image-aspect,4/3)];
@apply aspect-auto;
}
.form-image-masonry .asset-image .n-image,
.form-image-masonry .asset-image .n-image img {
@apply h-auto;
}
.gallery-sticky-controls > .form-image-filter-region {
@apply my-0;