From 4f888d8c94782edb8ef9e6e2e59b25c8082c9d9d Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Tue, 6 Oct 2026 18:14:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=B1=E5=88=9B=E6=8E=88=E6=9D=83:=20?= =?UTF-8?q?=E8=A1=8D=E7=94=9F=E4=BD=9C=E5=93=81=E9=9A=90=E8=97=8F=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E5=85=A5=E5=8F=A3=EF=BC=8C=E6=94=B9=E4=B8=BA=E5=8F=AA?= =?UTF-8?q?=E8=AF=BB=E5=B1=95=E7=A4=BA=E3=80=8C=E7=BB=A7=E6=89=BF=E8=87=AA?= =?UTF-8?q?=E7=88=B6=E4=BD=9C=E5=93=81=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../game-distribution/MyGamesPage.test.tsx | 41 +- .../game-distribution/MyGamesPage.tsx | 1022 +++++++++-------- 2 files changed, 559 insertions(+), 504 deletions(-) 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}