feat: move creation entry config to database

This commit is contained in:
2026-05-11 11:23:24 +08:00
parent 7f2461313e
commit 793d82cccd
37 changed files with 1458 additions and 204 deletions

View File

@@ -1,13 +1,88 @@
/* @vitest-environment jsdom */
/* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, expect, test, vi } from 'vitest';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import { derivePlatformCreationTypes } from '../platform-entry/platformEntryCreationTypes';
import type { CreationEntryConfig } from '../../services/creationEntryConfigService';
import { CustomWorldCreationHub } from './CustomWorldCreationHub';
const noopCreateType = () => {};
const testEntryConfig = {
startCard: {
title: '新建作品',
description: '选择模板后进入对应的创作表单。',
idleBadge: '模板 Tab',
busyBadge: '正在开启',
},
typeModal: {
title: '选择创作类型',
description: '先选玩法类型,再进入对应创作工作台。',
},
creationTypes: [
{
id: 'puzzle',
title: '拼图',
subtitle: '拼图关卡创作',
badge: '可创建',
imageSrc: '/creation-type-references/puzzle.webp',
visible: true,
open: true,
sortOrder: 30,
updatedAtMicros: 1,
},
{
id: 'match3d',
title: '抓大鹅',
subtitle: '3D 消除关卡',
badge: '可创建',
imageSrc: '/creation-type-references/match3d.webp',
visible: true,
open: true,
sortOrder: 40,
updatedAtMicros: 1,
},
{
id: 'square-hole',
title: '方洞',
subtitle: '形状投放挑战',
badge: '可创建',
imageSrc: '/creation-type-references/square-hole.webp',
visible: true,
open: true,
sortOrder: 50,
updatedAtMicros: 1,
},
{
id: 'visual-novel',
title: '视觉小说',
subtitle: '分支叙事体验',
badge: '可创建',
imageSrc: '/creation-type-references/visual-novel.webp',
visible: true,
open: true,
sortOrder: 60,
updatedAtMicros: 1,
},
{
id: 'airp',
title: 'AI RPG',
subtitle: '原生角色扮演',
badge: '即将开放',
imageSrc: '/creation-type-references/airp.webp',
visible: true,
open: false,
sortOrder: 70,
updatedAtMicros: 1,
},
],
} satisfies CreationEntryConfig;
const testCreationTypes = derivePlatformCreationTypes(testEntryConfig.creationTypes);
const originalClipboard = navigator.clipboard;
afterEach(() => {
@@ -58,6 +133,8 @@ test('creation hub shows published metric growth from cached page snapshot', asy
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
onOpenPuzzleDetail={() => {}}
/>,
);
@@ -99,6 +176,8 @@ test('creation hub reflects updated draft title summary and counts after rerende
onCreateType={onCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
@@ -142,6 +221,8 @@ test('creation hub reflects updated draft title summary and counts after rerende
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
@@ -182,6 +263,8 @@ test('creation hub mixes puzzle works into the same grid and uses puzzle tag to
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
onOpenPuzzleDetail={() => {}}
/>,
);
@@ -238,6 +321,8 @@ test('creation hub shows puzzle point incentive and claims without opening card'
onEnterPublished={() => {}}
onOpenPuzzleDetail={onOpenPuzzleDetail}
onClaimPuzzlePointIncentive={onClaimPuzzlePointIncentive}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
@@ -296,6 +381,8 @@ test('creation hub shows RPG public work code from published library entry', ()
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
@@ -318,6 +405,8 @@ test('creation hub shows delete action for persisted rpg drafts', () => {
onOpenDraft={() => {}}
onEnterPublished={() => {}}
onDeletePublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
@@ -359,6 +448,8 @@ test('creation hub published work delete action is available beside share withou
onEnterPublished={() => {}}
onOpenPuzzleDetail={onOpenPuzzleDetail}
onDeletePuzzle={onDeletePuzzle}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
@@ -396,6 +487,8 @@ test('creation hub opens persisted rpg drafts by card click', async () => {
openedItems.push(item);
}}
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
@@ -444,6 +537,8 @@ test('creation hub published share button copies share text without opening the
onOpenDraft={() => {}}
onEnterPublished={() => {}}
onOpenPuzzleDetail={onOpenPuzzleDetail}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);

View File

@@ -1,10 +1,85 @@
import { renderToStaticMarkup } from 'react-dom/server';
import { expect, test } from 'vitest';
import { derivePlatformCreationTypes } from '../platform-entry/platformEntryCreationTypes';
import type { CreationEntryConfig } from '../../services/creationEntryConfigService';
import { CustomWorldCreationHub } from './CustomWorldCreationHub';
const noopCreateType = () => {};
const testEntryConfig = {
startCard: {
title: '新建作品',
description: '选择模板后进入对应的创作表单。',
idleBadge: '模板 Tab',
busyBadge: '正在开启',
},
typeModal: {
title: '选择创作类型',
description: '先选玩法类型,再进入对应创作工作台。',
},
creationTypes: [
{
id: 'puzzle',
title: '拼图',
subtitle: '拼图关卡创作',
badge: '可创建',
imageSrc: '/creation-type-references/puzzle.webp',
visible: true,
open: true,
sortOrder: 30,
updatedAtMicros: 1,
},
{
id: 'match3d',
title: '抓大鹅',
subtitle: '3D 消除关卡',
badge: '可创建',
imageSrc: '/creation-type-references/match3d.webp',
visible: true,
open: true,
sortOrder: 40,
updatedAtMicros: 1,
},
{
id: 'square-hole',
title: '方洞',
subtitle: '形状投放挑战',
badge: '可创建',
imageSrc: '/creation-type-references/square-hole.webp',
visible: true,
open: true,
sortOrder: 50,
updatedAtMicros: 1,
},
{
id: 'visual-novel',
title: '视觉小说',
subtitle: '分支叙事体验',
badge: '可创建',
imageSrc: '/creation-type-references/visual-novel.webp',
visible: true,
open: true,
sortOrder: 60,
updatedAtMicros: 1,
},
{
id: 'airp',
title: 'AI RPG',
subtitle: '原生角色扮演',
badge: '即将开放',
imageSrc: '/creation-type-references/airp.webp',
visible: true,
open: false,
sortOrder: 70,
updatedAtMicros: 1,
},
],
} satisfies CreationEntryConfig;
const testCreationTypes = derivePlatformCreationTypes(testEntryConfig.creationTypes);
test('creation hub draft card renders compiled work summary fields', () => {
const html = renderToStaticMarkup(
<CustomWorldCreationHub
@@ -36,6 +111,8 @@ test('creation hub draft card renders compiled work summary fields', () => {
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
@@ -79,6 +156,8 @@ test('creation hub renders puzzle works in the same unified list with puzzle tag
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
onOpenPuzzleDetail={() => {}}
/>,
);
@@ -123,6 +202,8 @@ test('creation hub published work spans full mobile row', () => {
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
onOpenPuzzleDetail={() => {}}
/>,
);

View File

@@ -8,7 +8,11 @@ import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contr
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
import type { CustomWorldProfile } from '../../types';
import type { PlatformCreationTypeId } from '../platform-entry/platformEntryCreationTypes';
import type { CreationEntryConfig } from '../../services/creationEntryConfigService';
import type {
PlatformCreationTypeCard,
PlatformCreationTypeId,
} from '../platform-entry/platformEntryCreationTypes';
import {
buildCreationWorkShelfItems,
type CreationWorkShelfItem,
@@ -38,6 +42,8 @@ type CustomWorldCreationHubProps = {
onRetry: () => void;
createError?: string | null;
createBusy?: boolean;
entryConfig: CreationEntryConfig;
creationTypes: readonly PlatformCreationTypeCard[];
onCreateType: (type: PlatformCreationTypeId) => void;
onOpenDraft: (item: CustomWorldWorkSummary) => void;
onEnterPublished: (profileId: string) => void;
@@ -134,6 +140,8 @@ export function CustomWorldCreationHub({
onRetry,
createError = null,
createBusy = false,
entryConfig,
creationTypes,
onCreateType,
onOpenDraft,
onEnterPublished,
@@ -309,6 +317,8 @@ export function CustomWorldCreationHub({
<CustomWorldCreationStartCard
busy={createBusy}
error={createError}
entryConfig={entryConfig}
creationTypes={creationTypes}
onCreateType={onCreateType}
/>
) : null}

View File

@@ -1,25 +1,30 @@
import { ArrowRight } from 'lucide-react';
import { NEW_WORK_ENTRY_CONFIG } from '../../config/newWorkEntryConfig';
import type { CreationEntryConfig } from '../../services/creationEntryConfigService';
import {
getVisiblePlatformCreationTypes,
type PlatformCreationTypeCard,
type PlatformCreationTypeId,
} from '../platform-entry/platformEntryCreationTypes';
type CustomWorldCreationStartCardProps = {
busy?: boolean;
error?: string | null;
entryConfig: CreationEntryConfig;
creationTypes: readonly PlatformCreationTypeCard[];
onCreateType: (type: PlatformCreationTypeId) => void;
};
export function CustomWorldCreationStartCard({
busy = false,
error = null,
entryConfig,
creationTypes,
onCreateType,
}: CustomWorldCreationStartCardProps) {
// 创作首页首屏卡带与创作类型弹层保持同一份展示口径,
// 避免某个玩法只在其中一个入口被隐藏而出现状态漂移。
const visibleCreationTypes = getVisiblePlatformCreationTypes();
const visibleCreationTypes = getVisiblePlatformCreationTypes(creationTypes);
return (
// 移动端限制模块高度,模板入口改为横向滚动,避免挤占作品列表首屏空间。
@@ -28,15 +33,15 @@ export function CustomWorldCreationStartCard({
<div className="relative z-10 space-y-2.5 sm:space-y-4 xl:space-y-3">
<div className="flex items-center justify-between gap-3 xl:items-end">
<div className="text-xl font-black leading-none text-white sm:text-3xl xl:text-2xl">
{NEW_WORK_ENTRY_CONFIG.startCard.title}
{entryConfig.startCard.title}
</div>
<div className="hidden text-sm leading-6 text-zinc-200/88 sm:block xl:text-xs xl:leading-5">
{NEW_WORK_ENTRY_CONFIG.startCard.description}
{entryConfig.startCard.description}
</div>
<span className="platform-pill platform-pill--neutral shrink-0 border-white/25 bg-white/14 px-2.5 text-xs text-white sm:hidden">
{busy
? NEW_WORK_ENTRY_CONFIG.startCard.busyBadge
: NEW_WORK_ENTRY_CONFIG.startCard.idleBadge}
? entryConfig.startCard.busyBadge
: entryConfig.startCard.idleBadge}
</span>
</div>