diff --git a/README.md b/README.md index 4dee3fc..2c8e89c 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S - 项目列表采用固定表头表格;剧本、分镜、身份与生产页的目录和详情独立滚动,其他工作区只滚动内容区。 - 目录、详情、执行记录、折叠操作区、图片历史和 JSON 查看区统一使用 Naive UI `NScrollbar`;外层只负责尺寸与裁切,不再设置原生 `overflow: auto`。内边距与横向排列放在 `content-class` 内容层,避免双滚动条。 - 身份与形态图库采用限高预览及紧邻其下的横向历史栏;缩略图固定宽度,保留全部候选、辅助视图、失败及进行中记录。点击仅切换预览,不自动切换母版或主图;图片尺寸规则放在非分层组件覆盖样式中,避免被 Naive 按钮默认宽度覆盖。 -- 形态图库卡片采用 `cover` 铺满,点击图片调用 `NImage` 原生预览查看完整原图;“查看图片与记录”仍单独打开历史管理。历史缩略图和其他资源默认保持 `contain` 且不触发原图预览,避免与候选选择混淆。 +- 身份母版、图库大图、历史缩略图、形态/风格/分镜参考图及首帧统一通过 `NImage` 采用 `cover` 铺满。母版与展示图片可点击原生预览查看完整原图;历史缩略图只切换选中记录,再点击大图查看原图,避免与候选选择混淆。“查看图片与记录”仍单独打开历史管理。 - 拆解页固定精简状态栏与结果标签,设置、预览及恢复操作收进“拆解设置与恢复”面板;人物、场景、道具、分镜和任务明细共用独立结果滚动区,执行记录单独滚动。结果区占满剩余高度,切换标签回到顶部,轮询刷新或开关设置不重置阅读位置;失败与校验提示在面板关闭时仍然可见。 - 剧集正文按编号连续展示全部已写入剧集;点击目录定位正文,滚动正文同步高亮并保持当前目录项可见。目录分行显示集数和完整标题,执行记录可按需展开。切换角色/世界观等标签不会销毁阅读器或重置位置。 - 主体身份、分镜设计和镜头生产共用 `DirectoryItem`:编号、完整标题和状态分层展示,桌面目录为 240–280px,窄屏改为可横向滚动的条目。目录标题与筛选固定,只有列表滚动。拆解结果的导出按钮与标签同排右对齐;形态图片筛选按内容区宽度自动排成一行或两列网格。 diff --git a/src/admin.css b/src/admin.css index 365d942..b759bee 100644 --- a/src/admin.css +++ b/src/admin.css @@ -899,7 +899,6 @@ body { .asset-image .n-image img { width: 100%; height: 100%; - object-fit: contain; } /* 预览尺寸不随原图分辨率增长,历史缩略图也不能被按钮默认宽度覆盖。 */ .asset-image.asset-image-preview { diff --git a/src/components/ui/AssetImage.test.ts b/src/components/ui/AssetImage.test.ts index 0393ce9..cfe53cf 100644 --- a/src/components/ui/AssetImage.test.ts +++ b/src/components/ui/AssetImage.test.ts @@ -11,17 +11,26 @@ afterEach(() => { }) describe('资源图片展示与原图预览', () => { - it('默认保持 contain 并禁用预览,不干扰历史缩略图选择', async () => { + it('默认使用 cover 并禁用预览,不干扰历史缩略图选择', async () => { wrapper = mount(AssetImage, { attachTo: document.body, props: { src: '/storage/history.png', alt: '历史图片' } }) - expect(wrapper.getComponent(NImage).props()).toMatchObject({ objectFit: 'contain', previewDisabled: true }) + expect(wrapper.getComponent(NImage).props()).toMatchObject({ objectFit: 'cover', previewDisabled: true }) + expect(wrapper.get('img').element.style.objectFit).toBe('cover') await wrapper.get('img').trigger('click') await flushPromises() expect(document.querySelector('.n-image-preview-container')).toBeNull() }) + it('特殊展示仍可显式选择 contain,样式交由 NImage 控制', () => { + wrapper = mount(AssetImage, { + props: { src: '/storage/reference.png', alt: '完整参考图', objectFit: 'contain' } + }) + expect(wrapper.getComponent(NImage).props('objectFit')).toBe('contain') + expect(wrapper.get('img').element.style.objectFit).toBe('contain') + }) + it('cover 仅裁切卡片,点击使用 Naive 原生预览展示同一完整资源,遮罩可关闭', async () => { wrapper = mount(AssetImage, { attachTo: document.body, diff --git a/src/components/ui/AssetImage.vue b/src/components/ui/AssetImage.vue index 7ca0a82..8f9b761 100644 --- a/src/components/ui/AssetImage.vue +++ b/src/components/ui/AssetImage.vue @@ -4,7 +4,7 @@ import { computed, ref, watch } from 'vue' import { ImageOff } from '@lucide/vue' import { referenceImageUrl } from '../../lib/assets' -/** 安全图片展示;铺满与原图预览按场景开启,历史选择按钮默认不触发预览。 */ +/** 图片统一铺满容器;原图预览按场景开启,历史选择按钮不触发预览。 */ const props = withDefaults( defineProps<{ src?: string | null @@ -14,7 +14,7 @@ const props = withDefaults( objectFit?: 'contain' | 'cover' preview?: boolean }>(), - { retryable: true, objectFit: 'contain', preview: false } + { retryable: true, objectFit: 'cover', preview: false } ) const failed = ref(false) const url = computed(() => (props.src ? referenceImageUrl(props.src) : null)) diff --git a/src/features/production/components/ShotProductionAssets.vue b/src/features/production/components/ShotProductionAssets.vue index 2ecee37..8a47943 100644 --- a/src/features/production/components/ShotProductionAssets.vue +++ b/src/features/production/components/ShotProductionAssets.vue @@ -260,6 +260,7 @@ async function inspectSpecs() {
diff --git a/src/features/storyboard/components/ShotTools.vue b/src/features/storyboard/components/ShotTools.vue index 824ba82..9d2340c 100644 --- a/src/features/storyboard/components/ShotTools.vue +++ b/src/features/storyboard/components/ShotTools.vue @@ -2,6 +2,7 @@ import { NScrollbar, NAlert, NButton, NCheckbox } from 'naive-ui' import { computed, onScopeDispose, ref, watch } from 'vue' import { Download, ExternalLink, RefreshCw } from '@lucide/vue' +import { AssetImage } from '../../../components/ui' import { storyboardApi } from '../api' import { getStoryboardSession, referenceImageUrl } from '../model' import type { DesignedShot, ShotReferences } from '../types' @@ -17,7 +18,6 @@ const references = ref(null) const spec = ref(null) const errors = ref({ references: '', spec: '' }) const busy = ref({ references: false, spec: false }) -const brokenImages = ref([]) const force = ref(false) const prompt = computed(() => getStoryboardSession(props.projectId).prompts[props.shot.shotId]) const operation = computed(() => getOperation(props.projectId)) @@ -33,7 +33,6 @@ function reset() { spec.value = null errors.value = { references: '', spec: '' } busy.value = { references: false, spec: false } - brokenImages.value = [] } watch(() => JSON.stringify([props.projectId, props.shot.shotId, props.shot.direction, props.shot.visualState]), reset) onScopeDispose(reset) @@ -54,7 +53,6 @@ async function load(kind: 'references' | 'spec') { if (version !== revision) return if (result.shotId !== shotId) throw new Error('参考图返回的 Shot ID 不匹配。') references.value = result - brokenImages.value = [] } else { const result = await storyboardApi.generationSpec(shotId, controller.signal) if (version !== revision) return @@ -117,20 +115,13 @@ function exportResult(kind: 'spec' | 'prompt') {