diff --git a/src/components/game-distribution/MyGamesPage.test.tsx b/src/components/game-distribution/MyGamesPage.test.tsx
index 2a5bd3dae..780c271a3 100644
--- a/src/components/game-distribution/MyGamesPage.test.tsx
+++ b/src/components/game-distribution/MyGamesPage.test.tsx
@@ -758,14 +758,44 @@ test('衍生列表读取失败时行内给出错误与重试', async () => {
expect(await screen.findByText('网络错误')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '重试' }));
expect(await screen.findByText('衍生作品')).toBeTruthy();
+ expect(screen.getByText('衍生作品较多,仅展示前 1 部。')).toBeTruthy();
+});
+
+test('衍生作品的授权入口改为只读展示(授权继承父作品,不可单独提升)', async () => {
+ vi.mocked(listMyGames).mockResolvedValue([
+ createGame({
+ id: 'g-fork',
+ title: '共创乙',
+ status: 'published',
+ forkAuthorization: 'nonCommercial',
+ lineage: {
+ generation: 1,
+ rootGameId: 'g-original',
+ rootTitle: '母版甲',
+ parentGameId: 'g-original',
+ parentTitle: '母版甲',
+ parentAuthorName: '原作者',
+ },
+ }),
+ ]);
+
+ renderPage(createAuthValue());
+
expect(
- screen.getByText('衍生作品较多,仅展示前 1 部。'),
+ await screen.findByText(/共创授权继承自父作品(允许非商用共创)/u),
).toBeTruthy();
+ // 提升入口不再出现:服务端会拒绝对衍生作品的授权 PUT。
+ expect(screen.queryByRole('button', { name: '共创授权' })).toBeNull();
});
test('分类标签按文档口径过滤,并给出总作品数 / 累计被 Fork 次数 / 世代数', async () => {
vi.mocked(listMyGames).mockResolvedValue([
- createGame({ id: 'g-original', title: '母版甲', status: 'published', forkCount: 2 }),
+ createGame({
+ id: 'g-original',
+ title: '母版甲',
+ status: 'published',
+ forkCount: 2,
+ }),
createGame({
id: 'g-fork',
title: '共创乙',
@@ -780,7 +810,12 @@ test('分类标签按文档口径过滤,并给出总作品数 / 累计被 Fork
parentAuthorName: '原作者',
},
}),
- createGame({ id: 'g-draft', title: '草稿丙', status: 'unpublished', forkCount: 0 }),
+ createGame({
+ id: 'g-draft',
+ title: '草稿丙',
+ status: 'unpublished',
+ forkCount: 0,
+ }),
]);
renderPage(createAuthValue());
diff --git a/src/components/game-distribution/MyGamesPage.tsx b/src/components/game-distribution/MyGamesPage.tsx
index 03786e99b..12aac0b08 100644
--- a/src/components/game-distribution/MyGamesPage.tsx
+++ b/src/components/game-distribution/MyGamesPage.tsx
@@ -507,9 +507,7 @@ export function MyGamesPage({
await loadGames();
} catch (updateError: unknown) {
setError(
- updateError instanceof Error
- ? updateError.message
- : '设置共创授权失败',
+ updateError instanceof Error ? updateError.message : '设置共创授权失败',
);
} finally {
setSavingForkAuthorizationGameId('');
@@ -637,7 +635,9 @@ export function MyGamesPage({
authUi?.openLoginModal(() => void loadCollections())}
+ onClick={() =>
+ authUi?.openLoginModal(() => void loadCollections())
+ }
>
登录
@@ -688,554 +688,574 @@ export function MyGamesPage({
{tab === 'works' ? (
<>
- {/*
+ {/*
分类标签 + 数据概览(文档「个人二创作品管理页」)。
概览里的「累计衍生世代数」等后端字段存在时才渲染,缺失不拿别的数字凑。
*/}
-
- {(
- [
- ['all', '全部作品'],
- ['original', '原创作品'],
- ['coCreated', '我发布的共创'],
- ['draft', '我的草稿'],
- ] as const
- ).map(([key, label]) => (
-
- ))}
-
-
- 总作品数 {games.length}
-
- 累计被 Fork 次数 {totalForkCount}
-
- {totalDerivedGenerations === null ? null : (
-
- 累计衍生世代数 {totalDerivedGenerations}
-
- )}
-
- {error ? (
-
- {error}
-
- ) : null}
-
- {notice ? (
-
- {notice}
-
- ) : null}
-
- {!canLoad ? (
-
- 登录后可以查看已发布的游戏与审核状态。
- authUi?.openLoginModal(() => void loadGames())}
- >
- 登录
-
-
- ) : null}
-
- {canLoad && isLoading ? (
- 正在加载我的游戏…
- ) : null}
-
- {canLoad && !isLoading && games.length === 0 && !error ? (
-
- 还没有在平台上发布过游戏。
-
- ) : null}
-
- {canLoad && games.length > 0 ? (
-
-
-
- {hasActiveFilter ? (
-
{
- setStatusFilter('all');
- setVersionFilter('all');
- }}
- >
- 重置筛选
-
+
+ {(
+ [
+ ['all', '全部作品'],
+ ['original', '原创作品'],
+ ['coCreated', '我发布的共创'],
+ ['draft', '我的草稿'],
+ ] as const
+ ).map(([key, label]) => (
+
+ ))}
+
+
+ 总作品数 {games.length}
+
+ 累计被 Fork 次数 {totalForkCount}
+
+ {totalDerivedGenerations === null ? null : (
+
+ 累计衍生世代数 {totalDerivedGenerations}
+
+ )}
+
+ {error ? (
+
+ {error}
+
) : null}
-
- ) : null}
- {canLoad &&
- games.length > 0 &&
- categoryGames.length === 0 &&
- !isLoading ? (
-
- 没有符合当前筛选条件的作品。
-
- ) : null}
+ {notice ? (
+
+ {notice}
+
+ ) : null}
- {canLoad && categoryGames.length > 0 ? (
-
- {categoryGames.map((game) => {
- const version = resolveVersionStatus(game);
- const latestVersion = game.latestVersion;
- const actionCopy = resolveLatestActionCopy(game);
- const resumeLabel = resolveDraftResumeLabel(game);
- const versions = game.versions;
- const isHistoryOpen = expandedGameId === game.id;
- const forkAuthorization = resolveGameForkAuthorization(
- game.forkAuthorization,
- );
- const forkCount = game.forkCount ?? 0;
- const isDerivedOpen = expandedDerivedGameId === game.id;
- const derivedEntry = derivedByGameId[game.id] ?? null;
- const derivedError = derivedErrorByGameId[game.id] ?? '';
- const isDerivedLoading = loadingDerivedGameId === game.id;
- const forkAuthorizationLabel =
- GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS[
- forkAuthorization
- ];
- const isForkAuthorizationEditing =
- forkAuthorizationEditorGameId === game.id;
- const forkAuthorizationTarget =
- pendingForkAuthorization?.gameId === game.id
- ? pendingForkAuthorization.target
- : null;
- const isSavingForkAuthorization =
- savingForkAuthorizationGameId === game.id;
- const forkAuthorizationTargetLabel = forkAuthorizationTarget
- ? GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS[
- forkAuthorizationTarget
- ]
- : '';
- const forkAuthorizationOptions =
- GAME_DISTRIBUTION_FORK_AUTHORIZATIONS.map((option) => ({
- option,
- label: GAME_DISTRIBUTION_FORK_AUTHORIZATION_SHORT_LABELS[option],
- fullLabel: GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS[option],
- canUpgrade: canUpgradeGameForkAuthorization(
- forkAuthorization,
- option,
- ),
- }));
- return (
-
-
-
-
-
-
- {GAME_STATUS_LABEL[game.status] ?? game.status}
-
-
-
{game.summary}
-
-
{game.category}
-
·
-
{game.playCount.toLocaleString()} 次游玩
- {version ? (
- <>
+ {!canLoad ? (
+
+ 登录后可以查看已发布的游戏与审核状态。
+ authUi?.openLoginModal(() => void loadGames())}
+ >
+ 登录
+
+
+ ) : null}
+
+ {canLoad && isLoading ? (
+
正在加载我的游戏…
+ ) : null}
+
+ {canLoad && !isLoading && games.length === 0 && !error ? (
+
+ 还没有在平台上发布过游戏。
+
+ ) : null}
+
+ {canLoad && games.length > 0 ? (
+
+
+
+ {hasActiveFilter ? (
+
{
+ setStatusFilter('all');
+ setVersionFilter('all');
+ }}
+ >
+ 重置筛选
+
+ ) : null}
+
+ ) : null}
+
+ {canLoad &&
+ games.length > 0 &&
+ categoryGames.length === 0 &&
+ !isLoading ? (
+
+ 没有符合当前筛选条件的作品。
+
+ ) : null}
+
+ {canLoad && categoryGames.length > 0 ? (
+
+ {categoryGames.map((game) => {
+ const version = resolveVersionStatus(game);
+ const latestVersion = game.latestVersion;
+ const actionCopy = resolveLatestActionCopy(game);
+ const resumeLabel = resolveDraftResumeLabel(game);
+ const versions = game.versions;
+ const isHistoryOpen = expandedGameId === game.id;
+ const forkAuthorization = resolveGameForkAuthorization(
+ game.forkAuthorization,
+ );
+ const forkCount = game.forkCount ?? 0;
+ const isDerivedOpen = expandedDerivedGameId === game.id;
+ const derivedEntry = derivedByGameId[game.id] ?? null;
+ const derivedError = derivedErrorByGameId[game.id] ?? '';
+ const isDerivedLoading = loadingDerivedGameId === game.id;
+ const forkAuthorizationLabel =
+ GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS[
+ forkAuthorization
+ ];
+ const isForkAuthorizationEditing =
+ forkAuthorizationEditorGameId === game.id;
+ const forkAuthorizationTarget =
+ pendingForkAuthorization?.gameId === game.id
+ ? pendingForkAuthorization.target
+ : null;
+ const isSavingForkAuthorization =
+ savingForkAuthorizationGameId === game.id;
+ const forkAuthorizationTargetLabel = forkAuthorizationTarget
+ ? GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS[
+ forkAuthorizationTarget
+ ]
+ : '';
+ const forkAuthorizationOptions =
+ GAME_DISTRIBUTION_FORK_AUTHORIZATIONS.map((option) => ({
+ option,
+ label:
+ GAME_DISTRIBUTION_FORK_AUTHORIZATION_SHORT_LABELS[option],
+ fullLabel:
+ GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS[option],
+ canUpgrade: canUpgradeGameForkAuthorization(
+ forkAuthorization,
+ option,
+ ),
+ }));
+ return (
+
+
+
+
+
+
+ {GAME_STATUS_LABEL[game.status] ?? game.status}
+
+
+
{game.summary}
+
+ {game.category}
+ ·
+ {game.playCount.toLocaleString()} 次游玩
+ {version ? (
+ <>
+ ·
+
+ 版本 v{version.versionNumber} · {version.label}
+
+ >
+ ) : null}
+ ·
+ 共创:{forkAuthorizationLabel}
+ {/* 文档要求卡片显示世代数与子衍生作品数量(世代取血缘 generation)。 */}
·
- 版本 v{version.versionNumber} · {version.label}
+ {coCreationGenerationShortLabel(
+ game.lineage?.generation ?? 0,
+ )}
- >
- ) : null}
- ·
- 共创:{forkAuthorizationLabel}
- {/* 文档要求卡片显示世代数与子衍生作品数量(世代取血缘 generation)。 */}
- ·
-
- {coCreationGenerationShortLabel(game.lineage?.generation ?? 0)}
-
- ·
- 子衍生作品 {forkCount}
-
- {version?.reviewReason ? (
-
- 驳回理由:{version.reviewReason}
-
- ) : null}
- {actionCopy ? (
-
- 下一步:{actionCopy}
-
- ) : null}
- {versions.length > 0 ? (
-
- ) : null}
- {/*
+
·
+
子衍生作品 {forkCount}
+
+ {version?.reviewReason ? (
+
+ 驳回理由:{version.reviewReason}
+
+ ) : null}
+ {actionCopy ? (
+
+ 下一步:{actionCopy}
+
+ ) : null}
+ {versions.length > 0 ? (
+
+ ) : null}
+ {/*
「被改编 N」与版本历史同属作品关系的展开信息,放在同一节奏里,
不占用下方动作行的位置;点开后才按需拉取直接子代。
只在已公开作品上渲染:衍生接口要求锚点公开可读,未公开/草稿作品点进去必然 404。
*/}
- {game.status === 'published' ? (
- <>
-
- {isDerivedOpen ? (
-
- {isDerivedLoading ? (
-
- 正在加载衍生作品…
-
- ) : null}
- {!isDerivedLoading && derivedError ? (
-
- {derivedError}
-
-
- ) : null}
- {!isDerivedLoading && !derivedError && derivedEntry
- ? derivedEntry.nodes.map((node) => (
-
+ {game.status === 'published' ? (
+ <>
+
+ {isDerivedOpen ? (
+
+ {isDerivedLoading ? (
+
+ 正在加载衍生作品…
+
+ ) : null}
+ {!isDerivedLoading && derivedError ? (
+
+ {derivedError}
-
- {node.authorName?.trim() || '未署名作者'} ·
- 第 {node.generation} 代 ·{' '}
- {GAME_STATUS_LABEL[node.status] ??
- node.status}
-
-
- ))
- : null}
- {!isDerivedLoading &&
- !derivedError &&
- derivedEntry &&
- derivedEntry.nodes.length === 0 ? (
-
- 还没有衍生作品。
-
+
+ ) : null}
+ {!isDerivedLoading &&
+ !derivedError &&
+ derivedEntry
+ ? derivedEntry.nodes.map((node) => (
+
+
+
+ {node.authorName?.trim() ||
+ '未署名作者'}{' '}
+ · 第 {node.generation} 代 ·{' '}
+ {GAME_STATUS_LABEL[node.status] ??
+ node.status}
+
+
+ ))
+ : null}
+ {!isDerivedLoading &&
+ !derivedError &&
+ derivedEntry &&
+ derivedEntry.nodes.length === 0 ? (
+
+ 还没有衍生作品。
+
+ ) : null}
+ {!isDerivedLoading &&
+ !derivedError &&
+ derivedEntry?.truncated ? (
+
+ 衍生作品较多,仅展示前{' '}
+ {derivedEntry.nodes.length} 部。
+
+ ) : null}
+
) : null}
- {!isDerivedLoading &&
- !derivedError &&
- derivedEntry?.truncated ? (
-
- 衍生作品较多,仅展示前{' '}
- {derivedEntry.nodes.length} 部。
-
- ) : null}
-
- ) : null}
- >
- ) : null}
- {isHistoryOpen ? (
-
- ) : null}
-
-
onOpenDetail(game.id)}
- >
- 查看详情
-
- {onPublishVersion ? (
-
onPublishVersion(game.id)}
- >
- 发布新版本
-
- ) : null}
- {publishEnabled ? (
-
setEditingGame(game)}
- >
- 编辑资料
-
- ) : null}
- {resumeLabel ? (
-
onResumePublish?.()}
- >
- {resumeLabel}
-
- ) : null}
- {resolveLatestRecoveryAction(game) === 'submit' &&
- latestVersion &&
- !resumeLabel ? (
-
void handleSubmitVersion(game)}
- >
- 提交审核
-
- ) : null}
- {game.status === 'published' ? (
-
void handleUnpublish(game)}
- >
- 下架
-
- ) : null}
- {latestVersion &&
- CANCELLABLE_VERSION_STATUS.has(latestVersion.status) ? (
- confirmingVersionId === latestVersion.versionId ? (
- <>
-
- void handleCancelVersion(game, latestVersion)
- }
- >
- 确认撤回
-
-
setConfirmingVersionId('')}
- >
- 取消
-
>
- ) : (
+ ) : null}
+ {isHistoryOpen ? (
+
+ ) : null}
+
- setConfirmingVersionId(latestVersion.versionId)
- }
+ onClick={() => onOpenDetail(game.id)}
>
- 撤回审核
+ 查看详情
- )
- ) : null}
- {forkAuthorization === 'full' ? (
-
- 共创授权已达上限
-
- ) : isForkAuthorizationEditing ? (
- forkAuthorizationTarget ? (
- <>
-
- 确认提升为「{forkAuthorizationTargetLabel}」?
- 提升后不可撤销。
-
+ {onPublishVersion ? (
- void handleUpdateForkAuthorization(
- game,
- forkAuthorizationTarget,
- forkAuthorization,
- )
- }
+ onClick={() => onPublishVersion(game.id)}
>
- 确认提升
+ 发布新版本
+ ) : null}
+ {publishEnabled ? (
setPendingForkAuthorization(null)}
+ disabled={busyGameId === game.id}
+ onClick={() => setEditingGame(game)}
>
- 取消
+ 编辑资料
- >
- ) : (
- <>
- {forkAuthorizationOptions.map(
- ({ option, label, canUpgrade, fullLabel }) => (
+ ) : null}
+ {resumeLabel ? (
+
onResumePublish?.()}
+ >
+ {resumeLabel}
+
+ ) : null}
+ {resolveLatestRecoveryAction(game) === 'submit' &&
+ latestVersion &&
+ !resumeLabel ? (
+
void handleSubmitVersion(game)}
+ >
+ 提交审核
+
+ ) : null}
+ {game.status === 'published' ? (
+
void handleUnpublish(game)}
+ >
+ 下架
+
+ ) : null}
+ {latestVersion &&
+ CANCELLABLE_VERSION_STATUS.has(latestVersion.status) ? (
+ confirmingVersionId === latestVersion.versionId ? (
+ <>
- setPendingForkAuthorization({
- gameId: game.id,
- target: option,
- })
+ void handleCancelVersion(game, latestVersion)
}
>
- {label}
+ 确认撤回
- ),
- )}
+
setConfirmingVersionId('')}
+ >
+ 取消
+
+ >
+ ) : (
+
+ setConfirmingVersionId(latestVersion.versionId)
+ }
+ >
+ 撤回审核
+
+ )
+ ) : null}
+ {/*
+ 授权提升入口**只给母版**:服务端现在会拒绝对衍生作品的授权 PUT
+ (衍生作品授权继承父作品,不可单独提升),所以衍生作品这里只做只读展示。
+ */}
+ {(game.lineage?.generation ?? 0) > 0 ? (
+
+ 共创授权继承自父作品({forkAuthorizationLabel})
+
+ ) : forkAuthorization === 'full' ? (
+
+ 共创授权已达上限
+
+ ) : isForkAuthorizationEditing ? (
+ forkAuthorizationTarget ? (
+ <>
+
+ 确认提升为「{forkAuthorizationTargetLabel}」?
+ 提升后不可撤销。
+
+
+ void handleUpdateForkAuthorization(
+ game,
+ forkAuthorizationTarget,
+ forkAuthorization,
+ )
+ }
+ >
+ 确认提升
+
+
+ setPendingForkAuthorization(null)
+ }
+ >
+ 取消
+
+ >
+ ) : (
+ <>
+ {forkAuthorizationOptions.map(
+ ({ option, label, canUpgrade, fullLabel }) => (
+
+ setPendingForkAuthorization({
+ gameId: game.id,
+ target: option,
+ })
+ }
+ >
+ {label}
+
+ ),
+ )}
+
+ setForkAuthorizationEditorGameId('')
+ }
+ >
+ 取消
+
+ >
+ )
+ ) : (
- setForkAuthorizationEditorGameId('')
- }
+ disabled={isSavingForkAuthorization}
+ onClick={() => {
+ setPendingForkAuthorization(null);
+ setForkAuthorizationEditorGameId(game.id);
+ }}
>
- 取消
+ 共创授权
- >
- )
- ) : (
-
{
- setPendingForkAuthorization(null);
- setForkAuthorizationEditorGameId(game.id);
- }}
- >
- 共创授权
-
- )}
-
setDeletingGame(game)}
- >
- 删除作品
-
-
-
-
- );
- })}
-
- ) : null}
-
+ )}
+
setDeletingGame(game)}
+ >
+ 删除作品
+
+
+
+
+ );
+ })}
+
+ ) : null}
>
) : null}