调整前台精选分类展示
前台精选分类新增全部并作为默认 Tab。 移除素材包 Tab 和素材包组合逻辑,未设置分类素材保留在全部中。 活动卡支持从 imageObjectKey 兜底图片地址。 精选卡片改为网格布局并让图片素材铺满预览区域。 更新前台精选模型和创作主页测试。
This commit is contained in:
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user