From 51c47c952b7be974660e7cc4bc3079d95d5be1af Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 6 Jul 2026 18:58:58 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=89=8D=E5=8F=B0=E7=B2=BE?= =?UTF-8?q?=E9=80=89=E5=88=86=E7=B1=BB=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前台精选分类新增全部并作为默认 Tab。 移除素材包 Tab 和素材包组合逻辑,未设置分类素材保留在全部中。 活动卡支持从 imageObjectKey 兜底图片地址。 精选卡片改为网格布局并让图片素材铺满预览区域。 更新前台精选模型和创作主页测试。 --- .../CreationLandingView.test.tsx | 29 ++++---- .../creation-home/CreationLandingView.tsx | 6 +- .../creationShowcaseModel.test.ts | 72 ++++++++++--------- .../creation-home/creationShowcaseModel.ts | 64 ++++------------- src/index.css | 27 ++++--- 5 files changed, 89 insertions(+), 109 deletions(-) diff --git a/src/components/creation-home/CreationLandingView.test.tsx b/src/components/creation-home/CreationLandingView.test.tsx index e54762ba5..a403c7f45 100644 --- a/src/components/creation-home/CreationLandingView.test.tsx +++ b/src/components/creation-home/CreationLandingView.test.tsx @@ -167,7 +167,8 @@ describe('CreationLandingView', () => { expect(screen.getByRole('heading', { name: '创作工具' })).toBeTruthy(); expect(screen.getByText('游戏视觉规范')).toBeTruthy(); expect(screen.getByRole('heading', { name: '陶泥儿精选' })).toBeTruthy(); - expect(screen.getByRole('tab', { name: '素材包' })).toBeTruthy(); + expect(screen.getByRole('tab', { name: '全部' })).toBeTruthy(); + expect(screen.queryByRole('tab', { name: '素材包' })).toBeNull(); expect(screen.queryByRole('tab', { name: '特效' })).toBeNull(); expect(screen.queryByRole('tab', { name: '场景' })).toBeNull(); expect(screen.queryByRole('tab', { name: '游戏' })).toBeNull(); @@ -557,12 +558,12 @@ describe('CreationLandingView', () => { { resourceId: 'resource-pack-a', projectId: 'project-newest', - label: '素材包 A', + label: '精选素材 A', imageSrc: 'data:image/png;base64,a', width: 512, height: 640, sourceType: 'generated', - prompt: '素材包提示词', + prompt: '精选素材提示词', actualPrompt: null, model: null, provider: null, @@ -585,7 +586,7 @@ describe('CreationLandingView', () => { { resourceId: 'resource-pack-b', projectId: 'project-old', - label: '素材包 B', + label: '精选素材 B', imageSrc: 'data:image/png;base64,b', width: 512, height: 512, @@ -600,8 +601,8 @@ describe('CreationLandingView', () => { const { container } = renderCreationLanding(); - expect(await screen.findByText('素材包 A')).toBeTruthy(); - expect(screen.getByText('素材包 B')).toBeTruthy(); + expect(await screen.findByText('精选素材 A')).toBeTruthy(); + expect(screen.getByText('精选素材 B')).toBeTruthy(); expect(screen.queryByText('上传素材')).toBeNull(); expect(screen.getByLabelText('用户素材瀑布流').className).toContain( 'creation-landing__asset-waterfall', @@ -623,7 +624,7 @@ describe('CreationLandingView', () => { { resourceId: 'resource-pack-a', projectId: 'project-newest', - label: '素材包 A', + label: '精选素材 A', imageSrc: 'data:image/png;base64,a', width: 512, height: 640, @@ -642,7 +643,7 @@ describe('CreationLandingView', () => { { resourceId: 'resource-pack-b', projectId: 'project-old', - label: '素材包 B', + label: '精选素材 B', imageSrc: 'data:image/png;base64,b', width: 512, height: 512, @@ -659,12 +660,12 @@ describe('CreationLandingView', () => { renderCreationLanding(); - expect(await screen.findByText('素材包 A')).toBeTruthy(); + expect(await screen.findByText('精选素材 A')).toBeTruthy(); await waitFor(() => expect(observers).toHaveLength(1)); - expect(screen.queryByText('素材包 B')).toBeNull(); + expect(screen.queryByText('精选素材 B')).toBeNull(); triggerIntersection(observers[0]!); triggerIntersection(observers[0]!); - expect(await screen.findByText('素材包 B')).toBeTruthy(); + expect(await screen.findByText('精选素材 B')).toBeTruthy(); await waitFor(() => expect(listPublicEditorProjectResourcesMock).toHaveBeenLastCalledWith({ cursor: 'cursor-page-2', @@ -682,7 +683,7 @@ describe('CreationLandingView', () => { { resourceId: 'resource-pack-a', projectId: 'project-newest', - label: '素材包 A', + label: '精选素材 A', imageSrc: 'data:image/png;base64,a', width: 512, height: 640, @@ -700,14 +701,14 @@ describe('CreationLandingView', () => { renderCreationLanding(); - expect(await screen.findByText('素材包 A')).toBeTruthy(); + expect(await screen.findByText('精选素材 A')).toBeTruthy(); await waitFor(() => expect(observers).toHaveLength(1)); triggerIntersection(observers[0]!); expect( await screen.findByRole('button', { name: '读取更多素材失败,点击重试' }), ).toBeTruthy(); - expect(screen.getByText('素材包 A')).toBeTruthy(); + expect(screen.getByText('精选素材 A')).toBeTruthy(); expect(screen.getByLabelText('用户素材瀑布流')).toBeTruthy(); }); diff --git a/src/components/creation-home/CreationLandingView.tsx b/src/components/creation-home/CreationLandingView.tsx index 64f8a2cf1..ec07c8e04 100644 --- a/src/components/creation-home/CreationLandingView.tsx +++ b/src/components/creation-home/CreationLandingView.tsx @@ -247,7 +247,7 @@ function ShowcaseImagePreview({ className={ variant === 'modal' ? 'creation-landing__showcase-media' - : 'h-full w-full object-contain' + : 'creation-landing__asset-preview-media' } /> ); @@ -269,7 +269,7 @@ function ShowcaseVideoPreview({ className={ variant === 'modal' ? 'creation-landing__showcase-media' - : 'h-full w-full object-contain' + : 'creation-landing__asset-preview-media' } controls={controls} muted={!controls} @@ -470,7 +470,7 @@ export function CreationLandingView({ string | null >(null); const [activeShowcaseTab, setActiveShowcaseTab] = - useState('packs'); + useState('all'); const [selectedShowcaseItem, setSelectedShowcaseItem] = useState(null); const [selectedShowcaseIndex, setSelectedShowcaseIndex] = useState(0); diff --git a/src/components/creation-home/creationShowcaseModel.test.ts b/src/components/creation-home/creationShowcaseModel.test.ts index e108cb496..53afccd73 100644 --- a/src/components/creation-home/creationShowcaseModel.test.ts +++ b/src/components/creation-home/creationShowcaseModel.test.ts @@ -26,7 +26,7 @@ function createResource( describe('creationShowcaseModel', () => { it('only builds showcase items from generated project resources', () => { const items = buildCreationShowcaseItems({ - activeTab: 'packs', + activeTab: 'all', projectResources: [ createResource({ resourceId: 'generated-1', @@ -55,7 +55,7 @@ describe('creationShowcaseModel', () => { it('filters out generated showcase records with display disabled', () => { const items = buildCreationShowcaseItems({ - activeTab: 'packs', + activeTab: 'all', projectResources: [ createResource({ resourceId: 'public-generated', @@ -78,7 +78,7 @@ describe('creationShowcaseModel', () => { it('prepends enabled campaign cards and keeps showcase metrics', () => { const items = buildCreationShowcaseItems({ - activeTab: 'packs', + activeTab: 'all', campaign: { enabled: true, title: '活动精选', @@ -109,7 +109,7 @@ describe('creationShowcaseModel', () => { cost: '50泥点', }); expect(items[1]).toMatchObject({ - id: 'standalone:asset-1', + id: 'asset:asset-1', label: '审核通过素材', likeCount: 12, showcaseId: 'showcase-1', @@ -215,9 +215,9 @@ describe('creationShowcaseModel', () => { expect(items[0]?.previews[0]?.id).toBe('resource-original'); }); - it('groups pack items by the same referenced spec image and sums known cost', () => { + it('shows every real asset in the all tab without requiring a category', () => { const items = buildCreationShowcaseItems({ - activeTab: 'packs', + activeTab: 'all', projectResources: [ createResource({ resourceId: 'spec-1', @@ -247,51 +247,55 @@ describe('creationShowcaseModel', () => { ], }); - expect(items).toHaveLength(1); - expect(items[0]?.label).toBe('角色规范图'); - expect(items[0]?.previews.map((preview) => preview.label)).toEqual([ - '角色规范图', - '角色主图', - ]); + expect(items.map((item) => item.label)).toEqual(['角色规范图', '角色主图']); expect(items[0]?.prompt).toBe('规范提示词'); expect(items[0]?.author).toBe('创作者A'); - expect(items[0]?.cost).toBe('17泥点'); + expect(items[0]?.cost).toBe('5泥点'); + expect(items[1]?.cost).toBe('12泥点'); }); - it('groups assets when references point to the source project resource', () => { + it('keeps unset category visible in all tab and out of concrete category tabs', () => { const items = buildCreationShowcaseItems({ - activeTab: 'packs', + activeTab: 'all', projectResources: [ createResource({ resourceId: 'spec-1', - label: '项目规范图', + label: '未设置分类素材', assetKind: 'spec', - sourceResourceId: 'resource-spec-1', + showcaseCategory: null, }), createResource({ - resourceId: 'character-1', - label: '项目角色图', + resourceId: 'ui-1', + label: '人工 UI 素材', assetKind: 'character', - generationInputs: { - fields: [{ title: '角色设定', value: '项目角色提示词' }], - references: [ - { - title: '角色规范', - label: '项目规范图', - refType: 'project-resource', - refId: 'resource-spec-1', - }, - ], - }, + showcaseCategory: 'ui', }), ], }); - expect(items).toHaveLength(1); - expect(items[0]?.previews.map((preview) => preview.label)).toEqual([ - '项目规范图', - '项目角色图', + expect(items.map((item) => item.label)).toEqual([ + '未设置分类素材', + '人工 UI 素材', ]); + expect( + buildCreationShowcaseItems({ + activeTab: 'ui', + projectResources: [ + createResource({ + resourceId: 'spec-1', + label: '未设置分类素材', + assetKind: 'spec', + showcaseCategory: null, + }), + createResource({ + resourceId: 'ui-1', + label: '人工 UI 素材', + assetKind: 'character', + showcaseCategory: 'ui', + }), + ], + }).map((item) => item.label), + ).toEqual(['人工 UI 素材']); }); it('groups character animations under the referenced character image', () => { diff --git a/src/components/creation-home/creationShowcaseModel.ts b/src/components/creation-home/creationShowcaseModel.ts index 68dd913a3..fe17d6407 100644 --- a/src/components/creation-home/creationShowcaseModel.ts +++ b/src/components/creation-home/creationShowcaseModel.ts @@ -17,12 +17,7 @@ import { DEFAULT_VIDEO_MODEL, } from '../image-editor/ImageCanvasGenerationModel'; -export type ShowcaseTabId = - | 'packs' - | 'characters' - | 'ui' - | 'music' - | 'marketing'; +export type ShowcaseTabId = 'all' | 'characters' | 'ui' | 'music' | 'marketing'; export type ShowcaseAssetMediaType = 'image' | 'video' | 'audio'; @@ -71,15 +66,15 @@ const SHOWCASE_AUTHOR_KEYS = [ ] as const; export const SHOWCASE_TABS: Array<{ id: ShowcaseTabId; label: string }> = [ - { id: 'packs', label: '素材包' }, + { id: 'all', label: '全部' }, { id: 'characters', label: '角色' }, { id: 'ui', label: 'UI' }, { id: 'music', label: '音乐' }, { id: 'marketing', label: '美宣' }, ]; -const SHOWCASE_TAB_IDS = new Set( - SHOWCASE_TABS.map((tab) => tab.id), +const SHOWCASE_CATEGORY_IDS = new Set( + SHOWCASE_TABS.filter((tab) => tab.id !== 'all').map((tab) => tab.id), ); function assetRecord(asset: EditorAssetSnapshot) { @@ -218,7 +213,7 @@ function projectResourceToShowcaseAsset( } function normalizeShowcaseCategory(value: string): ShowcaseTabId | null { - return SHOWCASE_TAB_IDS.has(value as ShowcaseTabId) + return SHOWCASE_CATEGORY_IDS.has(value as Exclude) ? (value as ShowcaseTabId) : null; } @@ -470,41 +465,6 @@ function sortGroupAssets(assets: EditorAssetSnapshot[]) { }); } -function groupPackAssets(assets: EditorAssetSnapshot[]) { - const groups = new Map(); - assets.forEach((asset) => { - const specReference = findSpecReference(asset); - if (specReference) { - addAssetToGroup( - groups, - getReferenceKey(specReference), - asset, - specReference.label, - ); - return; - } - if (isSpecAsset(asset)) { - addAssetToGroup(groups, getSelfAssetKey(asset), asset, asset.label); - return; - } - addAssetToGroup(groups, `standalone:${asset.assetId}`, asset, asset.label); - }); - const specAssets = assets.filter(isSpecAsset); - specAssets.forEach((asset) => { - const group = mergeAssetAliasGroups(groups, asset, asset.label); - if ( - group && - !group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId) - ) { - group.assets.unshift(asset); - } - }); - return [...groups.values()].map((group) => ({ - ...group, - assets: sortGroupAssets(group.assets), - })); -} - function groupCharacterAssets(assets: EditorAssetSnapshot[]) { const groups = new Map(); assets @@ -613,15 +573,15 @@ function groupSingleAssets( } function getGroupsForTab(assets: EditorAssetSnapshot[], tab: ShowcaseTabId) { + if (tab === 'all') { + return groupSingleAssets(assets, () => true); + } if (assets.some((asset) => resolveAssetShowcaseCategory(asset))) { return groupSingleAssets( assets, (asset) => resolveAssetShowcaseCategory(asset) === tab, ); } - if (tab === 'packs') { - return groupPackAssets(assets); - } if (tab === 'characters') { return groupCharacterAssets(assets); } @@ -829,7 +789,10 @@ function toLibraryShowcaseItem( function campaignToShowcaseItem( campaign?: EditorShowcaseCampaignSnapshot | null, ): ShowcaseAssetItem | null { - if (!campaign?.enabled || !campaign.imageSrc.trim()) { + const objectKey = campaign?.imageObjectKey?.trim().replace(/^\/+/u, '') ?? ''; + const imageSrc = + campaign?.imageSrc.trim() || (objectKey ? `/${objectKey}` : ''); + if (!campaign?.enabled || !imageSrc) { return null; } return { @@ -839,7 +802,8 @@ function campaignToShowcaseItem( { id: 'campaign:global:image', label: campaign.title.trim() || '陶泥儿精选', - src: campaign.imageSrc, + src: imageSrc, + objectKey: objectKey || null, mediaType: 'image', }, ], diff --git a/src/index.css b/src/index.css index f1022184c..1402c9eef 100644 --- a/src/index.css +++ b/src/index.css @@ -3801,20 +3801,20 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock { } .creation-landing__asset-waterfall { + display: grid; width: 100%; min-width: 0; - column-count: 3; - column-gap: 0.92rem; + grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); + gap: 0.92rem; + align-items: start; } .creation-landing__asset-card { position: relative; - display: inline-block; + display: block; width: 100%; min-width: 0; overflow: hidden; - margin: 0 0 0.92rem; - break-inside: avoid; border: 1px solid rgba(220, 189, 163, 0.78); border-radius: 1rem; background: rgba(255, 254, 251, 0.88); @@ -3911,10 +3911,11 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock { .creation-landing__asset-preview { position: relative; display: grid; + width: 100%; min-height: 9.5rem; max-height: 23rem; overflow: hidden; - place-items: center; + place-items: stretch; background: linear-gradient(180deg, #fffdfa, #fff4ea); } @@ -3923,6 +3924,16 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock { object-position: center; } +.creation-landing__asset-preview-media { + display: block; + width: calc(100% + 2px); + max-width: none; + height: calc(100% + 2px); + max-height: none; + object-fit: cover; + object-position: center; +} + .creation-landing__asset-preview--bundle { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: stretch; @@ -4291,7 +4302,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock { } .creation-landing__asset-waterfall { - column-count: 2; + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); } .creation-landing__showcase-modal-panel { @@ -4351,7 +4362,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock { } .creation-landing__asset-waterfall { - column-count: 1; + grid-template-columns: 1fr; } .creation-landing__showcase-modal-overlay {