调整前台精选分类展示

前台精选分类新增全部并作为默认 Tab。

移除素材包 Tab 和素材包组合逻辑,未设置分类素材保留在全部中。

活动卡支持从 imageObjectKey 兜底图片地址。

精选卡片改为网格布局并让图片素材铺满预览区域。

更新前台精选模型和创作主页测试。
This commit is contained in:
2026-07-06 18:58:58 +08:00
parent 68e9087552
commit 51c47c952b
5 changed files with 89 additions and 109 deletions
@@ -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();
});
@@ -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<ShowcaseTabId>('packs');
useState<ShowcaseTabId>('all');
const [selectedShowcaseItem, setSelectedShowcaseItem] =
useState<ShowcaseAssetItem | null>(null);
const [selectedShowcaseIndex, setSelectedShowcaseIndex] = useState(0);
@@ -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', () => {
@@ -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<ShowcaseTabId>(
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<ShowcaseTabId, 'all'>)
? (value as ShowcaseTabId)
: null;
}
@@ -470,41 +465,6 @@ function sortGroupAssets(assets: EditorAssetSnapshot[]) {
});
}
function groupPackAssets(assets: EditorAssetSnapshot[]) {
const groups = new Map<string, ShowcaseAssetGroup>();
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<string, ShowcaseAssetGroup>();
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',
},
],
+19 -8
View File
@@ -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 {