共创模块C: 我的作品分类标签(全部/原创/我发布的共创/草稿) + 世代与子衍生数 + 数据概览
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
This commit is contained in:
@@ -763,6 +763,58 @@ test('衍生列表读取失败时行内给出错误与重试', async () => {
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('分类标签按文档口径过滤,并给出总作品数 / 累计被 Fork 次数 / 世代数', async () => {
|
||||
vi.mocked(listMyGames).mockResolvedValue([
|
||||
createGame({ id: 'g-original', title: '母版甲', status: 'published', forkCount: 2 }),
|
||||
createGame({
|
||||
id: 'g-fork',
|
||||
title: '共创乙',
|
||||
status: 'published',
|
||||
forkCount: 1,
|
||||
lineage: {
|
||||
generation: 1,
|
||||
rootGameId: 'g-original',
|
||||
rootTitle: '母版甲',
|
||||
parentGameId: 'g-original',
|
||||
parentTitle: '母版甲',
|
||||
parentAuthorName: '原作者',
|
||||
},
|
||||
}),
|
||||
createGame({ id: 'g-draft', title: '草稿丙', status: 'unpublished', forkCount: 0 }),
|
||||
]);
|
||||
|
||||
renderPage(createAuthValue());
|
||||
|
||||
// 数据概览:总作品数 + 累计被 Fork 次数(世代数字段缺失 → 该项不渲染)。
|
||||
expect(await screen.findByText('总作品数 3')).toBeTruthy();
|
||||
expect(screen.getByText('累计被 Fork 次数 3')).toBeTruthy();
|
||||
expect(screen.queryByText(/累计衍生世代数/u)).toBeNull();
|
||||
|
||||
// 卡片上的世代数与子衍生作品数量。
|
||||
expect(screen.getByText('子衍生作品 2')).toBeTruthy();
|
||||
expect(screen.getByText('子衍生作品 1')).toBeTruthy();
|
||||
|
||||
// 分类:原创作品只看无父作品的;我发布的共创只看有父且已公开的;草稿只看非公开的。
|
||||
fireEvent.click(screen.getByRole('tab', { name: '原创作品' }));
|
||||
expect(screen.getByText('母版甲')).toBeTruthy();
|
||||
expect(screen.queryByText('共创乙')).toBeNull();
|
||||
expect(screen.queryByText('草稿丙')).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: '我发布的共创' }));
|
||||
expect(screen.getByText('共创乙')).toBeTruthy();
|
||||
expect(screen.queryByText('母版甲')).toBeNull();
|
||||
expect(screen.queryByText('草稿丙')).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: '我的草稿' }));
|
||||
expect(screen.getByText('草稿丙')).toBeTruthy();
|
||||
expect(screen.queryByText('母版甲')).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: '全部作品' }));
|
||||
expect(screen.getByText('母版甲')).toBeTruthy();
|
||||
expect(screen.getByText('共创乙')).toBeTruthy();
|
||||
expect(screen.getByText('草稿丙')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('未公开作品不渲染「被改编 N」入口', async () => {
|
||||
vi.mocked(listMyGames).mockResolvedValue([
|
||||
createGame({ status: 'unpublished', forkCount: 0 }),
|
||||
|
||||
@@ -34,6 +34,7 @@ import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformDangerConfirmDialog } from '../common/PlatformDangerConfirmDialog';
|
||||
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { coCreationGenerationShortLabel } from './coCreationCopy';
|
||||
import { GameCover } from './GameCover';
|
||||
import {
|
||||
type GamePublishDraft,
|
||||
@@ -221,6 +222,15 @@ export function MyGamesPage({
|
||||
* 不要求作者身份,门禁只要求登录,与这一页的既有门禁一致。
|
||||
*/
|
||||
const [tab, setTab] = useState<'works' | 'collections'>('works');
|
||||
/**
|
||||
* 「我的作品」下的分类标签(文档:全部作品 / 原创作品 / 我发布的共创 / 我的草稿)。
|
||||
*
|
||||
* 口径说明:原创 = 没有父作品(`lineage` 缺省或 generation 0);我发布的共创 = 有父作品
|
||||
* 且已公开;我的草稿 = 状态非 `published`(与卡片上的状态标签同一口径)。
|
||||
*/
|
||||
const [workCategory, setWorkCategory] = useState<
|
||||
'all' | 'original' | 'coCreated' | 'draft'
|
||||
>('all');
|
||||
const [collections, setCollections] = useState<GameDistributionGame[]>([]);
|
||||
const [isLoadingCollections, setIsLoadingCollections] = useState(false);
|
||||
const [collectionsError, setCollectionsError] = useState('');
|
||||
@@ -330,7 +340,40 @@ export function MyGamesPage({
|
||||
}),
|
||||
[games, statusFilter, versionFilter],
|
||||
);
|
||||
const hasActiveFilter = statusFilter !== 'all' || versionFilter !== 'all';
|
||||
/** 分类标签(文档口径):原创 = 无父作品;我发布的共创 = 有父作品且已公开;草稿 = 非公开。 */
|
||||
const categoryGames = useMemo(() => {
|
||||
if (workCategory === 'all') return visibleGames;
|
||||
return visibleGames.filter((game) => {
|
||||
const isCoCreated = (game.lineage?.generation ?? 0) > 0;
|
||||
// 三个分类互斥:草稿优先(非公开),其余按是否二创分原创 / 共创。
|
||||
if (workCategory === 'draft') return game.status !== 'published';
|
||||
if (game.status !== 'published') return false;
|
||||
return workCategory === 'original' ? !isCoCreated : isCoCreated;
|
||||
});
|
||||
}, [visibleGames, workCategory]);
|
||||
const totalForkCount = useMemo(
|
||||
() => games.reduce((sum, game) => sum + (game.forkCount ?? 0), 0),
|
||||
[games],
|
||||
);
|
||||
/**
|
||||
* 累计衍生世代数:`my-games` 目前没有这个字段(后端待补),因此**有才渲染**,
|
||||
* 缺失时概览少一项而不是拿别的数字凑。
|
||||
*/
|
||||
const totalDerivedGenerations = useMemo(() => {
|
||||
const values = games
|
||||
.map((game) =>
|
||||
'descendantGenerationCount' in game &&
|
||||
typeof game.descendantGenerationCount === 'number'
|
||||
? game.descendantGenerationCount
|
||||
: null,
|
||||
)
|
||||
.filter((value): value is number => value !== null);
|
||||
return values.length > 0
|
||||
? values.reduce((sum, value) => sum + value, 0)
|
||||
: null;
|
||||
}, [games]);
|
||||
const hasActiveFilter =
|
||||
statusFilter !== 'all' || versionFilter !== 'all' || workCategory !== 'all';
|
||||
|
||||
function resolveLatestRecoveryAction(game: GameDistributionMyGame) {
|
||||
const latest = game.latestVersion;
|
||||
@@ -645,6 +688,42 @@ export function MyGamesPage({
|
||||
|
||||
{tab === 'works' ? (
|
||||
<>
|
||||
{/*
|
||||
分类标签 + 数据概览(文档「个人二创作品管理页」)。
|
||||
概览里的「累计衍生世代数」等后端字段存在时才渲染,缺失不拿别的数字凑。
|
||||
*/}
|
||||
<div className="game-tabs" role="tablist" aria-label="我的作品分类">
|
||||
{(
|
||||
[
|
||||
['all', '全部作品'],
|
||||
['original', '原创作品'],
|
||||
['coCreated', '我发布的共创'],
|
||||
['draft', '我的草稿'],
|
||||
] as const
|
||||
).map(([key, label]) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={workCategory === key}
|
||||
className="game-tabs__button"
|
||||
onClick={() => setWorkCategory(key)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="co-creation-overview" aria-label="创作数据概览">
|
||||
<span className="co-creation-stat">总作品数 {games.length}</span>
|
||||
<span className="co-creation-stat">
|
||||
累计被 Fork 次数 {totalForkCount}
|
||||
</span>
|
||||
{totalDerivedGenerations === null ? null : (
|
||||
<span className="co-creation-stat">
|
||||
累计衍生世代数 {totalDerivedGenerations}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{error ? (
|
||||
<PlatformStatusMessage tone="error" surface="platform">
|
||||
{error}
|
||||
@@ -729,16 +808,16 @@ export function MyGamesPage({
|
||||
|
||||
{canLoad &&
|
||||
games.length > 0 &&
|
||||
visibleGames.length === 0 &&
|
||||
categoryGames.length === 0 &&
|
||||
!isLoading ? (
|
||||
<PlatformEmptyState surface="subpanel" size="panel">
|
||||
<span>没有符合当前筛选条件的作品。</span>
|
||||
</PlatformEmptyState>
|
||||
) : null}
|
||||
|
||||
{canLoad && visibleGames.length > 0 ? (
|
||||
{canLoad && categoryGames.length > 0 ? (
|
||||
<div className="my-game-list">
|
||||
{visibleGames.map((game) => {
|
||||
{categoryGames.map((game) => {
|
||||
const version = resolveVersionStatus(game);
|
||||
const latestVersion = game.latestVersion;
|
||||
const actionCopy = resolveLatestActionCopy(game);
|
||||
@@ -813,6 +892,13 @@ export function MyGamesPage({
|
||||
) : null}
|
||||
<span>·</span>
|
||||
<span>共创:{forkAuthorizationLabel}</span>
|
||||
{/* 文档要求卡片显示世代数与子衍生作品数量(世代取血缘 generation)。 */}
|
||||
<span>·</span>
|
||||
<span>
|
||||
{coCreationGenerationShortLabel(game.lineage?.generation ?? 0)}
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span>子衍生作品 {forkCount}</span>
|
||||
</div>
|
||||
{version?.reviewReason ? (
|
||||
<p className="my-game-card__reason">
|
||||
|
||||
Reference in New Issue
Block a user