feat(short-drama-agent-front): 调整工作台布局与图库瀑布流
This commit is contained in:
@@ -236,7 +236,7 @@ API 层也提供 `state` 与 `breakdown/latest` 方法。页面通过共享 chec
|
||||
11. 提示词批量入口有独立并发、覆盖和回执;默认补缺,覆盖会重写已有正式文本,但不改变历史图片、实际 Prompt 或主图。未完成剧本、运行中的任务及查询错误均阻止生成。
|
||||
12. 生产页过期提示可点击直达镜头实际使用的形态;图库支持 `sourceShotId`/`subjectFormId` 定位、高亮和返回原镜头。顶部独立显示身份主图过期与下游首帧影响,检查失败不当作“没有过期”。两种后端就绪检查结果冲突时提示“状态待核对”,不建议据此重复生图;已知后端原因见 [覆盖说明](docs/backend-coverage.md#过期首帧与素材定位)。
|
||||
|
||||
13. 筛选栏右侧可切换网格/瀑布流,默认保留原网格。两种布局复用同一份卡片和筛选结果,切换不发请求、不清除筛选;瀑布流按列从上到下排列,列数随内容宽度适配,使用封面元数据预留图片比例,缺少有效尺寸时回退 4:3。搜索区与列表的 12px 间距放在列表 `padding-top`,随正文滚走,不在吸顶栏底部保留固定色带。
|
||||
13. 筛选栏右侧可切换网格/瀑布流,默认保留原网格。两种布局复用同一份卡片和筛选结果,切换不发请求、不清除筛选;瀑布流按从左到右的素材顺序进入当前可用位置,列数随内容宽度适配,图片按自身原始比例自然撑开,不设置统一高度。搜索区与列表的 12px 间距放在列表 `padding-top`,随正文滚走,不在吸顶栏底部保留固定色带。
|
||||
|
||||
### 视觉风格、身份与形态的衔接
|
||||
|
||||
|
||||
+2
-6
@@ -229,11 +229,7 @@ const menuOptions = computed<MenuOption[]>(() => [
|
||||
<main id="main-content" tabindex="-1" class="admin-content"><RouterView /></main>
|
||||
</NLayout>
|
||||
</div>
|
||||
<AppDialog
|
||||
v-model:open="settingsOpen"
|
||||
title="后端连接"
|
||||
description="页面使用真实后端接口,生成操作会实际提交。"
|
||||
>
|
||||
<AppDialog v-model:open="settingsOpen" title="后端连接" description="查看当前 API 地址与代理配置。">
|
||||
<dl class="detail-grid mt-6">
|
||||
<dt>API 地址</dt>
|
||||
<dd>
|
||||
@@ -244,7 +240,7 @@ const menuOptions = computed<MenuOption[]>(() => [
|
||||
<dt>生产环境</dt>
|
||||
<dd>为 /api、/storage 配置反向代理,并为长工作流设置足够的超时。</dd>
|
||||
<dt>状态更新</dt>
|
||||
<dd>已有刷新按钮的页面使用手动更新;剧本创作和拆解仍每 6 秒同步项目状态。关闭页面不会取消后端任务。</dd>
|
||||
<dd>已有刷新按钮的页面使用手动更新;剧本创作和拆解仍每 6 秒同步项目状态。</dd>
|
||||
</dl>
|
||||
</AppDialog>
|
||||
</NConfigProvider>
|
||||
|
||||
@@ -44,7 +44,6 @@ const open = defineModel<boolean>('open', { default: false })
|
||||
></NButton></div
|
||||
></template>
|
||||
<slot />
|
||||
<template #footer><span class="text-xs text-muted">关闭面板不会取消已提交的后台任务。</span></template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</template>
|
||||
@@ -52,8 +51,7 @@ const open = defineModel<boolean>('open', { default: false })
|
||||
<style>
|
||||
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||
@reference "../../styles/styles.css";
|
||||
.workspace-tools-drawer .n-drawer-header,
|
||||
.workspace-tools-drawer .n-drawer-footer {
|
||||
.workspace-tools-drawer .n-drawer-header {
|
||||
@apply bg-(--app-subtle) p-4;
|
||||
}
|
||||
.workspace-tools-drawer .n-drawer-body {
|
||||
@@ -62,7 +60,6 @@ const open = defineModel<boolean>('open', { default: false })
|
||||
.workspace-tools-drawer {
|
||||
--n-header-padding: 16px;
|
||||
--n-body-padding: 16px;
|
||||
--n-footer-padding: 16px;
|
||||
@apply max-w-full;
|
||||
}
|
||||
.workspace-tools-heading {
|
||||
|
||||
@@ -66,7 +66,7 @@ function exportReceipt() {
|
||||
<p v-else class="mt-2 text-sm">前置检查通过,提交前仍会再次校验。</p>
|
||||
</div>
|
||||
<p v-if="operation.pending && operation.label === '高级串联生产'" role="status" class="mt-3 text-sm">
|
||||
请求进行中;关闭面板不会取消后端任务,请勿重复提交。
|
||||
请求进行中,请勿重复提交。
|
||||
</p>
|
||||
<section v-if="receipt" aria-label="串联生产回执" class="surface-inset mt-4 p-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
|
||||
@@ -239,7 +239,7 @@ async function submit() {
|
||||
>
|
||||
</div>
|
||||
<p v-if="operation.pending" role="status" class="mt-3 text-xs text-muted">
|
||||
{{ operation.label }}进行中。关闭面板不会取消后端任务;请勿重复提交。
|
||||
{{ operation.label }}进行中,请勿重复提交。
|
||||
</p>
|
||||
<NAlert v-if="session.error" role="alert" type="error" :show-icon="false" class="mt-4">{{
|
||||
session.error
|
||||
|
||||
@@ -54,7 +54,7 @@ onScopeDispose(() => {
|
||||
></NAlert
|
||||
>
|
||||
<NAlert v-if="operation.pending" :show-icon="false" role="status"
|
||||
>{{ operation.label }}。可切换页面查看结果,请勿重复提交;关闭页面不会取消后端任务。</NAlert
|
||||
>{{ operation.label }}。可切换页面查看结果,请勿重复提交。</NAlert
|
||||
>
|
||||
<NAlert v-if="operation.error" type="error" :show-icon="false">{{ operation.error }}</NAlert>
|
||||
<NAlert v-if="operation.notice" :show-icon="false" role="status">{{ operation.notice }}</NAlert>
|
||||
|
||||
@@ -158,9 +158,10 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WorkspacePage split compact
|
||||
><template #header
|
||||
><div class="workspace-toolbar">
|
||||
<WorkspacePage split compact class="identity-workspace-page"
|
||||
><template #header>
|
||||
<!-- 以下是类型筛选、主体数量与操作入口 -->
|
||||
<div class="workspace-toolbar">
|
||||
<div class="toolbar-type-filter">
|
||||
<NSelect
|
||||
aria-label="身份主体类型"
|
||||
@@ -414,6 +415,7 @@ watch(
|
||||
</NAlert></NScrollbar
|
||||
>
|
||||
<div v-if="subjects.length" class="identity-workspace">
|
||||
<!-- 以下是主体目录 -->
|
||||
<aside class="panel directory-panel identity-directory">
|
||||
<div class="directory-heading">
|
||||
<h3>
|
||||
@@ -654,8 +656,14 @@ watch(
|
||||
@apply grid-cols-[repeat(2,_minmax(0,_1fr))];
|
||||
}
|
||||
}
|
||||
.identity-workspace-page {
|
||||
container: identity-header / inline-size;
|
||||
}
|
||||
.toolbar-type-filter {
|
||||
@apply w-[152px];
|
||||
@apply w-[152px] min-w-0;
|
||||
}
|
||||
.toolbar-type-filter .n-select {
|
||||
@apply w-full min-w-0;
|
||||
}
|
||||
.identity-tools-intro {
|
||||
@apply mb-6;
|
||||
@@ -728,10 +736,47 @@ watch(
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.identity-workspace {
|
||||
@apply grid-cols-[minmax(0,_1fr)] grid-rows-[250px_minmax(0,_1fr)] gap-2.5;
|
||||
@apply grid-cols-[minmax(0,_1fr)] grid-rows-[auto_minmax(0,_1fr)] gap-2.5;
|
||||
}
|
||||
.identity-directory {
|
||||
@apply h-auto;
|
||||
}
|
||||
.identity-subject-list {
|
||||
@apply flex-none h-auto max-h-none;
|
||||
}
|
||||
.identity-directory .identity-subject-list.n-scrollbar,
|
||||
.identity-directory .identity-subject-list .n-scrollbar-container,
|
||||
.identity-directory .identity-subject-list .n-scrollbar-content {
|
||||
@apply h-auto;
|
||||
}
|
||||
.directory-filters {
|
||||
@apply grid-cols-[minmax(0,_1fr)_120px];
|
||||
@apply grid-cols-[minmax(0,_1fr)];
|
||||
}
|
||||
}
|
||||
/* 窄内容区去掉数量文案,操作入口收成图标与类型筛选同一行。 */
|
||||
@container identity-header (max-width: 760px) {
|
||||
.identity-workspace-page .workspace-toolbar {
|
||||
@apply flex-nowrap gap-2;
|
||||
}
|
||||
.toolbar-type-filter {
|
||||
@apply w-auto min-w-[120px] flex-1;
|
||||
}
|
||||
.identity-workspace-page .toolbar-summary {
|
||||
@apply hidden;
|
||||
}
|
||||
.identity-workspace-page .toolbar-actions {
|
||||
@apply ml-0 shrink-0;
|
||||
}
|
||||
.identity-workspace-page .workspace-tools-label,
|
||||
.identity-workspace-page .workspace-tools-trigger .text-muted {
|
||||
@apply hidden;
|
||||
}
|
||||
.identity-workspace-page .workspace-tools-trigger {
|
||||
@apply h-[34px] w-[34px] p-0;
|
||||
min-width: 34px;
|
||||
}
|
||||
.identity-workspace-page .workspace-tools-trigger .n-button__icon {
|
||||
@apply m-0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import { coverImage } from './model'
|
||||
import type { SubjectFormAsset } from './types'
|
||||
|
||||
/** 图库展示方式;仅改变卡片排版,不影响素材顺序、筛选及后端查询。 */
|
||||
export type GalleryLayout = 'grid' | 'masonry'
|
||||
|
||||
/** 按正式封面尺寸预留瀑布流图片高度,避免懒加载时卡片跳动;旧记录缺少尺寸时使用 4:3。 */
|
||||
export function formCoverAspectRatio(form: SubjectFormAsset): string {
|
||||
const image = coverImage(form)
|
||||
const width = image?.width
|
||||
const height = image?.height
|
||||
return typeof width === 'number' &&
|
||||
typeof height === 'number' &&
|
||||
Number.isFinite(width) &&
|
||||
Number.isFinite(height) &&
|
||||
width > 0 &&
|
||||
height > 0
|
||||
? `${width} / ${height}`
|
||||
: '4 / 3'
|
||||
/**
|
||||
* 计算瀑布流卡片需要跨越的网格行数。
|
||||
* @param height 卡片实际高度
|
||||
* @param rowHeight 隐式网格行高
|
||||
* @param rowGap 网格行间距
|
||||
* @returns 卡片需要跨越的网格行数
|
||||
*/
|
||||
export function masonryRowSpan(height: number, rowHeight: number, rowGap: number): number {
|
||||
if (![height, rowHeight, rowGap].every(Number.isFinite) || height <= 0 || rowHeight <= 0 || rowGap < 0) return 1
|
||||
return Math.max(1, Math.ceil((height + rowGap) / (rowHeight + rowGap)))
|
||||
}
|
||||
|
||||
@@ -106,26 +106,17 @@ function removeImage(image: VisualStyleImage) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WorkspacePage
|
||||
><template #header
|
||||
><div class="flex flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">项目视觉风格</h2>
|
||||
</div>
|
||||
<RouterLink :to="`/projects/${projectId}/subject-identity`" class="text-button"
|
||||
>下一步:主体身份 →</RouterLink
|
||||
>
|
||||
</div></template
|
||||
>
|
||||
<div class="visual-style-toolbar">
|
||||
<WorkspacePage compact class="visual-style-page"
|
||||
><template #header>
|
||||
<!-- 以下是标题、锁定状态与生成操作 -->
|
||||
<div class="workspace-toolbar visual-style-toolbar">
|
||||
<h2 class="font-semibold">项目视觉风格</h2>
|
||||
<div class="visual-style-status" role="status">
|
||||
<component :is="statusInfo.icon" :size="18" aria-hidden="true" />
|
||||
<div>
|
||||
<component :is="statusInfo.icon" :size="16" aria-hidden="true" />
|
||||
<strong>{{ statusInfo.label }}</strong>
|
||||
<p>{{ statusInfo.hint }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="visual-style-actions">
|
||||
<div class="visual-style-actions toolbar-actions">
|
||||
<ConfirmAction
|
||||
:label="style ? 'AI 重新生成风格' : 'AI 生成风格'"
|
||||
:disabled="disabled || !!style?.isLocked || dirty"
|
||||
@@ -142,8 +133,12 @@ function removeImage(image: VisualStyleImage) {
|
||||
title="刷新风格"
|
||||
><template #icon><RefreshCw :size="16" /></template
|
||||
></NButton>
|
||||
<RouterLink :to="`/projects/${projectId}/subject-identity`" class="text-button"
|
||||
>下一步:主体身份 →</RouterLink
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<NAlert v-if="query.error.value" role="alert" type="error" :show-icon="false" class="mb-4"
|
||||
>{{ query.error.value }} 请确认后端 dev 已更新并重启;查询失败不等同于尚未创建。
|
||||
</NAlert>
|
||||
@@ -174,22 +169,31 @@ function removeImage(image: VisualStyleImage) {
|
||||
<style>
|
||||
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||
@reference "../../styles/styles.css";
|
||||
.visual-style-toolbar {
|
||||
@apply flex items-center justify-between flex-wrap gap-y-4 gap-x-6 my-5 py-3.5 px-4 bg-(--app-subtle);
|
||||
.visual-style-page {
|
||||
container: visual-style-header / inline-size;
|
||||
}
|
||||
.visual-style-status {
|
||||
@apply flex items-center gap-3 min-w-0;
|
||||
@apply flex items-center gap-2 min-w-0 flex-1 flex-nowrap;
|
||||
}
|
||||
.visual-style-status > svg {
|
||||
@apply shrink-0 text-muted;
|
||||
}
|
||||
.visual-style-status strong {
|
||||
@apply text-sm font-medium;
|
||||
@apply text-xs font-medium whitespace-nowrap;
|
||||
}
|
||||
.visual-style-status p {
|
||||
@apply mt-1 mx-0 mb-0 text-muted text-xs leading-[1.6];
|
||||
@apply mt-0 mx-0 mb-0 min-w-0 truncate text-muted text-xs leading-[1.6];
|
||||
}
|
||||
.visual-style-actions {
|
||||
@apply flex items-center flex-wrap gap-2.5 ml-auto;
|
||||
@apply shrink-0;
|
||||
}
|
||||
/* 窄内容区让说明占满首行剩余空间,操作独占下一行并从左侧排开。 */
|
||||
@container visual-style-header (max-width: 760px) {
|
||||
.visual-style-page .workspace-toolbar {
|
||||
@apply gap-x-3 gap-y-2;
|
||||
}
|
||||
.visual-style-actions.toolbar-actions {
|
||||
@apply ml-0 w-full justify-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -38,9 +38,6 @@ function confirm() {
|
||||
label
|
||||
}}</NButton></template
|
||||
>
|
||||
<p class="mt-5 text-sm leading-6 text-muted">
|
||||
此操作会提交到真实后端,可能调用模型并产生费用。请勿在其他页面或终端同时启动相同工作流。
|
||||
</p>
|
||||
<NCheckbox
|
||||
v-model:checked="acknowledged"
|
||||
v-if="acknowledgement"
|
||||
|
||||
@@ -166,16 +166,17 @@ describe('全站平面主题', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('瀑布流自适应列宽,卡片保持完整并按封面尺寸预留高度', () => {
|
||||
it('瀑布流按行填充自适应列宽,卡片与图片均保留实际高度', () => {
|
||||
const css = readAllStyles()
|
||||
expect(css).toMatch(
|
||||
/\.gallery-workspace-page \.form-image-masonry\s*\{[^}]*display:\s*block;[^}]*columns:\s*260px;[^}]*column-gap:\s*20px;/
|
||||
/\.gallery-workspace-page \.form-image-masonry\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*repeat\(auto-fill, minmax\(min\(260px, 100%\), 1fr\)\);[^}]*grid-auto-flow:\s*row dense;[^}]*grid-auto-rows:\s*1px;/
|
||||
)
|
||||
expect(css).toMatch(
|
||||
/\.form-image-masonry > \.form-image-card\s*\{[^}]*break-inside:\s*avoid;[^}]*margin-bottom:\s*20px;/
|
||||
/\.form-image-masonry > \.form-image-card\s*\{[^}]*grid-column-start:\s*var\(--form-image-column,\s*auto\);[^}]*grid-row-start:\s*var\(--form-image-row-start,\s*auto\);[^}]*grid-row-end:\s*span var\(--form-image-row-span,\s*1\);/
|
||||
)
|
||||
expect(css).toMatch(/\.form-image-masonry \.asset-image\s*\{[^}]*aspect-ratio:\s*auto;/)
|
||||
expect(css).toMatch(
|
||||
/\.form-image-masonry \.asset-image\s*\{[^}]*aspect-ratio:\s*var\(--form-image-aspect,\s*4\s*\/\s*3\);/
|
||||
/\.form-image-masonry \.asset-image \.n-image,\s*\.form-image-masonry \.asset-image \.n-image img\s*\{[^}]*height:\s*auto;/
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ describe('过期首帧到具体素材定位', () => {
|
||||
expect(scroll.scrollTop).toBe(240)
|
||||
expect(
|
||||
wrapper!.get<HTMLElement>('.form-image-card').element.style.getPropertyValue('--form-image-aspect')
|
||||
).toBe('2048 / 2048')
|
||||
).toBe('')
|
||||
await wrapper!.get('input[aria-label="搜索形态图片"]').setValue('雨夜')
|
||||
await wrapper!.get('[aria-label="网格布局"]').trigger('click')
|
||||
expect(wrapper!.find('.form-image-masonry').exists()).toBe(false)
|
||||
|
||||
@@ -1,31 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { formCoverAspectRatio } from '@/features/subject-images/layout'
|
||||
import { formFixture, imageFixture } from '@/features/subject-images/testing/fixtures'
|
||||
import { masonryRowSpan } from '@/features/subject-images/layout'
|
||||
|
||||
describe('图库瀑布流图片比例', () => {
|
||||
it.each([
|
||||
[800, 1200],
|
||||
[1600, 900],
|
||||
[1024, 1024]
|
||||
])('保留正式图片尺寸 %s × %s', (width, height) => {
|
||||
const form = formFixture()
|
||||
form.images = [imageFixture({ width, height })]
|
||||
expect(formCoverAspectRatio(form)).toBe(`${width} / ${height}`)
|
||||
})
|
||||
|
||||
it.each([null, 0, -1, NaN, Infinity])('尺寸 %s 无效时使用稳定占位比例', width => {
|
||||
const form = formFixture()
|
||||
form.images = [imageFixture({ width })]
|
||||
expect(formCoverAspectRatio(form)).toBe('4 / 3')
|
||||
form.images = [imageFixture({ height: width })]
|
||||
expect(formCoverAspectRatio(form)).toBe('4 / 3')
|
||||
})
|
||||
|
||||
it('无图片时保留占位,候选封面也使用自己的尺寸', () => {
|
||||
const form = formFixture()
|
||||
form.images = []
|
||||
expect(formCoverAspectRatio(form)).toBe('4 / 3')
|
||||
form.images = [imageFixture({ isPrimary: false, width: 800, height: 1200 })]
|
||||
expect(formCoverAspectRatio(form)).toBe('800 / 1200')
|
||||
describe('图库瀑布流卡片布局', () => {
|
||||
it('按卡片实际高度计算网格跨度', () => {
|
||||
expect(masonryRowSpan(420, 1, 20)).toBe(21)
|
||||
expect(masonryRowSpan(421, 1, 20)).toBe(21)
|
||||
expect(masonryRowSpan(422, 1, 20)).toBe(22)
|
||||
expect(masonryRowSpan(0, 1, 20)).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user