1
This commit is contained in:
@@ -72,3 +72,42 @@ test('creation hub reflects updated draft title summary and counts after rerende
|
||||
expect(screen.getByText('角色 5')).toBeTruthy();
|
||||
expect(screen.getByText('地点 6')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('creation hub mixes puzzle works into the same grid and uses puzzle tag to distinguish', () => {
|
||||
render(
|
||||
<CustomWorldCreationHub
|
||||
items={[baseDraftItem]}
|
||||
puzzleItems={[
|
||||
{
|
||||
workId: 'puzzle:work-1',
|
||||
profileId: 'puzzle-profile-1',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '拼图作者',
|
||||
levelName: '沉钟拼图',
|
||||
summary: '拼图作品会与其他创作作品一起展示。',
|
||||
themeTags: ['潮雾', '沉钟'],
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'published',
|
||||
updatedAt: new Date('2026-04-22T12:00:00.000Z').toISOString(),
|
||||
publishedAt: new Date('2026-04-22T12:10:00.000Z').toISOString(),
|
||||
playCount: 8,
|
||||
publishReady: true,
|
||||
},
|
||||
]}
|
||||
loading={false}
|
||||
error={null}
|
||||
onBack={() => {}}
|
||||
onRetry={() => {}}
|
||||
onCreateNew={() => {}}
|
||||
onOpenDraft={() => {}}
|
||||
onEnterPublished={() => {}}
|
||||
onOpenPuzzleDetail={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('潮雾列岛')).toBeTruthy();
|
||||
expect(screen.getByText('沉钟拼图')).toBeTruthy();
|
||||
expect(screen.getAllByText('拼图').length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('我的拼图作品')).toBeNull();
|
||||
expect(screen.queryByText('拼图玩法')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -42,3 +42,41 @@ test('creation hub draft card renders compiled work summary fields', () => {
|
||||
expect(html).toContain('玩家是失职返乡的守灯人');
|
||||
expect(html).toContain('守灯会与沉船商盟争夺航道解释权');
|
||||
});
|
||||
|
||||
test('creation hub renders puzzle works in the same unified list with puzzle tag', () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<CustomWorldCreationHub
|
||||
items={[]}
|
||||
puzzleItems={[
|
||||
{
|
||||
workId: 'puzzle:work-1',
|
||||
profileId: 'puzzle-profile-1',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '测试作者',
|
||||
levelName: '潮雾拼图',
|
||||
summary: '一张被切成拼图的潮雾港口主视觉。',
|
||||
themeTags: ['潮雾', '港口'],
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'published',
|
||||
updatedAt: new Date('2026-04-22T10:00:00.000Z').toISOString(),
|
||||
publishedAt: new Date('2026-04-22T10:05:00.000Z').toISOString(),
|
||||
playCount: 12,
|
||||
publishReady: true,
|
||||
},
|
||||
]}
|
||||
loading={false}
|
||||
error={null}
|
||||
onBack={() => {}}
|
||||
onRetry={() => {}}
|
||||
onCreateNew={() => {}}
|
||||
onOpenDraft={() => {}}
|
||||
onEnterPublished={() => {}}
|
||||
onOpenPuzzleDetail={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(html).toContain('潮雾拼图');
|
||||
expect(html).toContain('拼图');
|
||||
expect(html).not.toContain('我的拼图作品');
|
||||
expect(html).not.toContain('拼图玩法');
|
||||
});
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
import { CustomWorldCreationStartCard } from './CustomWorldCreationStartCard';
|
||||
import { CustomWorldWorkCard } from './CustomWorldWorkCard';
|
||||
import {
|
||||
CustomWorldWorkCard,
|
||||
type UnifiedCreationWorkItem,
|
||||
} from './CustomWorldWorkCard';
|
||||
import {
|
||||
type CustomWorldWorkFilter,
|
||||
CustomWorldWorkTabs,
|
||||
@@ -17,6 +21,8 @@ type CustomWorldCreationHubProps = {
|
||||
onCreateNew: () => void;
|
||||
onOpenDraft: (item: CustomWorldWorkSummary) => void;
|
||||
onEnterPublished: (profileId: string) => void;
|
||||
puzzleItems?: PuzzleWorkSummary[];
|
||||
onOpenPuzzleDetail?: (profileId: string) => void;
|
||||
};
|
||||
|
||||
function EmptyState({ title }: { title: string }) {
|
||||
@@ -38,19 +44,38 @@ export function CustomWorldCreationHub({
|
||||
onCreateNew,
|
||||
onOpenDraft,
|
||||
onEnterPublished,
|
||||
puzzleItems = [],
|
||||
onOpenPuzzleDetail,
|
||||
}: CustomWorldCreationHubProps) {
|
||||
const [activeFilter, setActiveFilter] =
|
||||
useState<CustomWorldWorkFilter>('all');
|
||||
const draftCount = items.filter((item) => item.status === 'draft').length;
|
||||
const publishedCount = items.filter(
|
||||
(item) => item.status === 'published',
|
||||
const unifiedItems = useMemo<UnifiedCreationWorkItem[]>(
|
||||
() => [
|
||||
...items.map((item) => ({ kind: 'rpg', item }) as const),
|
||||
...puzzleItems.map((item) => ({ kind: 'puzzle', item }) as const),
|
||||
],
|
||||
[items, puzzleItems],
|
||||
);
|
||||
const draftCount = unifiedItems.filter((entry) =>
|
||||
entry.kind === 'puzzle'
|
||||
? entry.item.publicationStatus === 'draft'
|
||||
: entry.item.status === 'draft',
|
||||
).length;
|
||||
const publishedCount = unifiedItems.filter((entry) =>
|
||||
entry.kind === 'puzzle'
|
||||
? entry.item.publicationStatus === 'published'
|
||||
: entry.item.status === 'published',
|
||||
).length;
|
||||
const filteredItems = useMemo(
|
||||
() =>
|
||||
items.filter((item) =>
|
||||
activeFilter === 'all' ? true : item.status === activeFilter,
|
||||
unifiedItems.filter((entry) =>
|
||||
activeFilter === 'all'
|
||||
? true
|
||||
: entry.kind === 'puzzle'
|
||||
? entry.item.publicationStatus === activeFilter
|
||||
: entry.item.status === activeFilter,
|
||||
),
|
||||
[activeFilter, items],
|
||||
[activeFilter, unifiedItems],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -125,22 +150,30 @@ export function CustomWorldCreationHub({
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{filteredItems.map((item) => (
|
||||
<CustomWorldWorkCard
|
||||
key={item.workId}
|
||||
key={`${item.kind}-${item.item.workId}`}
|
||||
item={item}
|
||||
onClick={() => {
|
||||
if (item.sourceType === 'agent_session' && item.sessionId) {
|
||||
onOpenDraft(item);
|
||||
if (item.kind === 'puzzle') {
|
||||
onOpenPuzzleDetail?.(item.item.profileId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.profileId) {
|
||||
onEnterPublished(item.profileId);
|
||||
if (
|
||||
item.item.sourceType === 'agent_session' &&
|
||||
item.item.sessionId
|
||||
) {
|
||||
onOpenDraft(item.item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.item.profileId) {
|
||||
onEnterPublished(item.item.profileId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : items.length === 0 ? (
|
||||
) : unifiedItems.length === 0 ? (
|
||||
<EmptyState title="还没有作品" />
|
||||
) : (
|
||||
<EmptyState title="当前筛选下没有内容" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
import { CustomWorldCoverArtwork } from '../CustomWorldCoverArtwork';
|
||||
|
||||
function formatUpdatedAt(value: string) {
|
||||
@@ -15,8 +16,18 @@ function formatUpdatedAt(value: string) {
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export type UnifiedCreationWorkItem =
|
||||
| {
|
||||
kind: 'rpg';
|
||||
item: CustomWorldWorkSummary;
|
||||
}
|
||||
| {
|
||||
kind: 'puzzle';
|
||||
item: PuzzleWorkSummary;
|
||||
};
|
||||
|
||||
type CustomWorldWorkCardProps = {
|
||||
item: CustomWorldWorkSummary;
|
||||
item: UnifiedCreationWorkItem;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
@@ -24,24 +35,36 @@ export function CustomWorldWorkCard({
|
||||
item,
|
||||
onClick,
|
||||
}: CustomWorldWorkCardProps) {
|
||||
const isDraft = item.status === 'draft';
|
||||
const hasFoundationDraft =
|
||||
item.playableNpcCount > 0 || item.landmarkCount > 0;
|
||||
const actionLabel = isDraft
|
||||
? hasFoundationDraft
|
||||
? '继续完善'
|
||||
: '继续创作'
|
||||
: '进入世界';
|
||||
const roleCountLabel = isDraft ? '角色' : '可扮演角色';
|
||||
const isPuzzle = item.kind === 'puzzle';
|
||||
const isDraft =
|
||||
item.kind === 'puzzle'
|
||||
? item.item.publicationStatus === 'draft'
|
||||
: item.item.status === 'draft';
|
||||
const actionLabel = isPuzzle
|
||||
? '查看详情'
|
||||
: isDraft
|
||||
? item.item.playableNpcCount > 0 || item.item.landmarkCount > 0
|
||||
? '继续完善'
|
||||
: '继续创作'
|
||||
: '进入世界';
|
||||
const title = isPuzzle ? item.item.levelName : item.item.title;
|
||||
const subtitle = isPuzzle ? item.item.authorDisplayName : item.item.subtitle;
|
||||
const summary = item.item.summary;
|
||||
const updatedAt = item.item.updatedAt;
|
||||
const coverImageSrc = item.item.coverImageSrc ?? null;
|
||||
const coverRenderMode =
|
||||
item.kind === 'rpg' ? item.item.coverRenderMode : 'image';
|
||||
const coverCharacterImageSrcs =
|
||||
item.kind === 'rpg' ? item.item.coverCharacterImageSrcs : [];
|
||||
|
||||
return (
|
||||
<div className="platform-surface platform-interactive-card relative min-h-[13.5rem] overflow-hidden px-4 py-4 sm:min-h-[14rem]">
|
||||
<CustomWorldCoverArtwork
|
||||
imageSrc={item.coverImageSrc}
|
||||
title={item.title}
|
||||
imageSrc={coverImageSrc}
|
||||
title={title}
|
||||
fallbackLabel="封面"
|
||||
renderMode={item.coverRenderMode}
|
||||
characterImageSrcs={item.coverCharacterImageSrcs}
|
||||
renderMode={coverRenderMode}
|
||||
characterImageSrcs={coverCharacterImageSrcs}
|
||||
className="platform-cover-artwork absolute inset-0"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-[var(--platform-card-overlay-strong)]" />
|
||||
@@ -57,52 +80,78 @@ export function CustomWorldWorkCard({
|
||||
>
|
||||
{isDraft ? '草稿' : '已发布'}
|
||||
</span>
|
||||
{item.stageLabel ? (
|
||||
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
|
||||
{isPuzzle ? '拼图' : 'RPG'}
|
||||
</span>
|
||||
{!isPuzzle && item.item.stageLabel ? (
|
||||
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
|
||||
{item.stageLabel}
|
||||
{item.item.stageLabel}
|
||||
</span>
|
||||
) : null}
|
||||
{isPuzzle
|
||||
? item.item.themeTags.slice(0, 2).map((tag) => (
|
||||
<span
|
||||
key={`${item.item.profileId}-${tag}`}
|
||||
className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
<div className="shrink-0 text-[11px] text-[var(--platform-text-soft)]">
|
||||
{formatUpdatedAt(item.updatedAt)}
|
||||
{formatUpdatedAt(updatedAt)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="text-2xl font-black text-[var(--platform-text-strong)]">
|
||||
{item.title}
|
||||
{title}
|
||||
</div>
|
||||
<div className="mt-1 text-xs tracking-[0.12em] text-[var(--platform-text-soft)]">
|
||||
{item.subtitle}
|
||||
{subtitle}
|
||||
</div>
|
||||
<div className="mt-3 line-clamp-3 text-sm leading-7 text-[color:color-mix(in_srgb,var(--platform-text-base)_92%,transparent)]">
|
||||
{item.summary}
|
||||
{summary}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex flex-col gap-3 pt-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
|
||||
{roleCountLabel} {item.playableNpcCount}
|
||||
</span>
|
||||
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
|
||||
地点 {item.landmarkCount}
|
||||
</span>
|
||||
{item.roleVisualReadyCount ? (
|
||||
<span className="platform-pill platform-pill--warm px-3 py-1 text-[10px]">
|
||||
主图 {item.roleVisualReadyCount}
|
||||
</span>
|
||||
) : null}
|
||||
{item.roleAnimationReadyCount ? (
|
||||
<span className="platform-pill platform-pill--success px-3 py-1 text-[10px]">
|
||||
动作 {item.roleAnimationReadyCount}
|
||||
</span>
|
||||
) : null}
|
||||
{item.roleAssetSummaryLabel ? (
|
||||
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
|
||||
{item.roleAssetSummaryLabel}
|
||||
</span>
|
||||
) : null}
|
||||
{isPuzzle ? (
|
||||
<>
|
||||
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
|
||||
作者 {item.item.authorDisplayName}
|
||||
</span>
|
||||
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
|
||||
游玩 {item.item.playCount}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
|
||||
{isDraft ? '角色' : '可扮演角色'} {item.item.playableNpcCount}
|
||||
</span>
|
||||
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
|
||||
地点 {item.item.landmarkCount}
|
||||
</span>
|
||||
{item.item.roleVisualReadyCount ? (
|
||||
<span className="platform-pill platform-pill--warm px-3 py-1 text-[10px]">
|
||||
主图 {item.item.roleVisualReadyCount}
|
||||
</span>
|
||||
) : null}
|
||||
{item.item.roleAnimationReadyCount ? (
|
||||
<span className="platform-pill platform-pill--success px-3 py-1 text-[10px]">
|
||||
动作 {item.item.roleAnimationReadyCount}
|
||||
</span>
|
||||
) : null}
|
||||
{item.item.roleAssetSummaryLabel ? (
|
||||
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
|
||||
{item.item.roleAssetSummaryLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user