diff --git a/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishFormView.tsx b/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishFormView.tsx index 0f7ed1194..0193cd116 100644 --- a/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishFormView.tsx +++ b/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishFormView.tsx @@ -12,26 +12,19 @@ import { } from './useGameDistributionPublishForm'; /** - * 发布资料表单的表现层:弹窗与工作台内联共用同一份 DOM 结构。 + * 发布资料表单的表现层(工作台内联形态)。 * - * 状态全部来自 `form`(`useGameDistributionPublishForm` 的返回值);内联形态把主行动换成 - * 内容末尾整行橙色的「打包并发布」。 + * 状态全部来自 `form`(`useGameDistributionPublishForm` 的返回值);主行动是内容末尾整行 + * 橙色的「打包并发布」,校验 / 提交错误由面板根的「发布提示」弹窗承载。 */ export function GameDistributionPublishFormView({ form, - inline = false, - open, - onClose, packaging = false, }: { form: GameDistributionPublishFormState; - inline?: boolean; - open: boolean; - onClose: () => void; packaging?: boolean; }) { const { - packageResult, title, setTitle, summary, @@ -39,7 +32,6 @@ export function GameDistributionPublishFormView({ category, setCategory, busy, - error, clearError, result, cover, @@ -66,7 +58,6 @@ export function GameDistributionPublishFormView({ handleGenerateCover, handleScreenshotsSelected, removeScreenshot, - handleSubmit, handlePackageAndPublish, } = form; @@ -349,160 +340,60 @@ export function GameDistributionPublishFormView({ ); - if (inline) { - return ( -
-
- - 发布到游戏广场 - -

- 让玩家现在就能试玩 -

-
- {result ? ( -
- 已提交审核 -

版本已进入审核队列,审核通过后才会在游戏广场公开展示。

-

- 版本 {result.versionNumber} ·{' '} - {result.packageSizeBytes.toLocaleString()} B ·{' '} - {result.packageSha256.slice(0, 16)}… -

-
- ) : ( - <> - {fieldsAndMedia} - - - )} - {coverConfirmDialog} - {previewDialog} -
- ); - } - return ( - <> - undefined : onClose} - panelClassName="game-distribution-publish-panel" - closeOnBackdrop={!busy && !generatingCover} - closeOnEscape={!busy && !generatingCover} - > -
-
- - 发布到游戏广场 - -

让玩家现在就能试玩

-
+
+
+ + 发布到游戏广场 + +

+ 让玩家现在就能试玩 +

+
+ {result ? ( +
+ 已提交审核 +

版本已进入审核队列,审核通过后才会在游戏广场公开展示。

+

+ 版本 {result.versionNumber} ·{' '} + {result.packageSizeBytes.toLocaleString()} B ·{' '} + {result.packageSha256.slice(0, 16)}… +

+
+ ) : ( + <> + {fieldsAndMedia} -
- {result ? ( -
- 已提交审核 -

版本已进入审核队列,审核通过后才会在游戏广场公开展示。

-

- 版本 {result.versionNumber} ·{' '} - {result.packageSizeBytes.toLocaleString()} B ·{' '} - {result.packageSha256.slice(0, 16)}… -

-
- -
-
- ) : ( - <> - {fieldsAndMedia} - {error ? ( -

- {error} -

- ) : null} -
- - -
- - )} -
+ + )} {coverConfirmDialog} {previewDialog} - + ); } diff --git a/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx b/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx deleted file mode 100644 index 56b954f8b..000000000 --- a/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import type { GameCreationAppManifest } from '../../../../../packages/shared/src/contracts/gameCreationApp'; -import type { LocalProjectExportPackageResult } from '../../app/types'; -import type { GameDistributionPublishResult } from '../../services/gameDistributionPublish'; -import { GameDistributionPublishFormView } from './GameDistributionPublishFormView'; -import { useGameDistributionPublishForm } from './useGameDistributionPublishForm'; - -/** - * 发布资料表单的弹窗外壳:点按钮弹出的独立面板。 - * - * 表单状态在 `useGameDistributionPublishForm`;工作台内联形态把它提升到导出面板根 - * (`useTaonierTab`),切 tab 不丢。这里保留「弹窗 + 关闭」的入口形态。 - */ -export function GameDistributionPublishPanel({ - open, - projectPath, - manifest, - packageResult, - onClose, - onPublished, -}: { - open: boolean; - projectPath: string; - manifest: GameCreationAppManifest; - packageResult: LocalProjectExportPackageResult | null; - onClose: () => void; - onPublished?: (result: GameDistributionPublishResult) => void; -}) { - const form = useGameDistributionPublishForm({ - open, - inline: false, - projectPath, - manifest, - packageResult, - onPublished, - }); - return ( - - ); -} diff --git a/apps/ai-game-creator-shell/src/components/game-distribution/useGameDistributionPublishForm.ts b/apps/ai-game-creator-shell/src/components/game-distribution/useGameDistributionPublishForm.ts index 64875aab6..2723cfc0c 100644 --- a/apps/ai-game-creator-shell/src/components/game-distribution/useGameDistributionPublishForm.ts +++ b/apps/ai-game-creator-shell/src/components/game-distribution/useGameDistributionPublishForm.ts @@ -148,14 +148,11 @@ export type GameDistributionPublishFormState = ReturnType< /** * 发布资料表单的全部状态与动作。 * - * 弹窗形态由 `GameDistributionPublishPanel` 自己调用;工作台内联形态把它提升到导出面板根 - * (`useTaonierTab`),这样切走 tab、页面组件卸载也不会丢掉已经填好的发布资料。 - * - * `inline` 决定初始化开关:弹窗每次带着新试玩包打开都重来;内联常驻左栏,只在换项目时重来。 + * 由导出面板根(`useTaonierTab`)持有,这样切走 tab、页面组件卸载也不会丢掉已经填好的发布资料; + * 初始化只在换项目 / 打开时重来,不因为「打包结果变化」清掉用户填好的资料。 */ export function useGameDistributionPublishForm({ open, - inline, projectPath, manifest, packageResult, @@ -164,17 +161,13 @@ export function useGameDistributionPublishForm({ onError, }: { open: boolean; - inline: boolean; projectPath: string; manifest: GameCreationAppManifest | null; packageResult: LocalProjectExportPackageResult | null; onPublished?: (result: GameDistributionPublishResult) => void; onEnsurePackage?: () => Promise; - /** - * 错误出口:内联工作台把表单校验 / 提交错误交给面板根的 `GamePublishBlockedDialog` - * (发布提示弹窗)展示;弹窗形态不传,退回表单里的内联文案。`null` 表示清掉弹窗。 - */ - onError?: (message: string | null) => void; + /** 表单校验 / 提交错误的出口:面板根用它驱动「发布提示」弹窗(`GamePublishBlockedDialog`)。 */ + onError: (message: string | null) => void; }) { const manifestRef = useRef(manifest); manifestRef.current = manifest; @@ -211,14 +204,6 @@ export function useGameDistributionPublishForm({ const uploadedAssetCacheRef = useRef(new Map()); const objectUrlsRef = useRef(new Set()); - /** - * 表单初始化开关:弹窗每次带着新试玩包打开都要重来;内联常驻左栏,只在换项目时重来, - * 不能因为「打包完包结果变化」就把用户已经填好的资料清掉。 - */ - const resetKey = inline - ? 'inline' - : (packageResult?.packageRelativePath ?? ''); - useEffect(() => { if (!open) return; const currentManifest = manifestRef.current; @@ -231,7 +216,7 @@ export function useGameDistributionPublishForm({ setCategory('其他'); setBusy(false); setError(''); - onError?.(null); + onError(null); setResult(null); setMetadataSuggestionPending(false); setMetadataSuggestionReady(false); @@ -268,7 +253,7 @@ export function useGameDistributionPublishForm({ setMetadataSuggestionPending(false); } }); - }, [manifest?.projectId, open, resetKey, onError]); + }, [manifest?.projectId, open, onError]); useEffect(() => { if (open) { @@ -519,7 +504,7 @@ export function useGameDistributionPublishForm({ */ function reportError(message: string) { setError(message); - onError?.(message.trim() ? message : null); + onError(message.trim() ? message : null); } /** 用户一改字段就把上一次的校验错误收起来,别让已经修好的字段继续挂着红字。 */ @@ -587,14 +572,6 @@ export function useGameDistributionPublishForm({ } } - async function handleSubmit() { - if (!packageResult) { - reportError('请先导出有效的试玩包'); - return; - } - await submitPackage(packageResult); - } - /** * 内联主行动:一次点击完成「打包 → 发布」。 * @@ -665,7 +642,6 @@ export function useGameDistributionPublishForm({ handleGenerateCover, handleScreenshotsSelected, removeScreenshot, - handleSubmit, handlePackageAndPublish, }; } diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/state/useTaonierTab.ts b/apps/ai-game-creator-shell/src/view/project-development/export/state/useTaonierTab.ts index efa4f0faa..e5b9b2a85 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/state/useTaonierTab.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/export/state/useTaonierTab.ts @@ -231,7 +231,6 @@ export function useTaonierTab({ */ const publishForm = useGameDistributionPublishForm({ open: formEnabled, - inline: true, projectPath: projectPath ?? '', manifest, packageResult, diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/tabs/taonier/PublishPane.tsx b/apps/ai-game-creator-shell/src/view/project-development/export/tabs/taonier/PublishPane.tsx index 5162fba49..837c95d89 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/tabs/taonier/PublishPane.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/export/tabs/taonier/PublishPane.tsx @@ -32,9 +32,6 @@ export function PublishPane({ tab }: { tab: TaonierTabState }) { > undefined} packaging={tab.progress?.phase === 'running'} /> diff --git a/apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx b/apps/ai-game-creator-shell/tests/gameDistributionPublishForm.test.tsx similarity index 86% rename from apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx rename to apps/ai-game-creator-shell/tests/gameDistributionPublishForm.test.tsx index 98de312bf..790641d41 100644 --- a/apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx +++ b/apps/ai-game-creator-shell/tests/gameDistributionPublishForm.test.tsx @@ -13,13 +13,16 @@ import { waitFor, within, } from '@testing-library/react'; +import { useState } from 'react'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp'; import type { LocalProjectExportPackageResult } from '../src/app/types'; -import { GameDistributionPublishPanel } from '../src/components/game-distribution/GameDistributionPublishPanel'; +import { GameDistributionPublishFormView } from '../src/components/game-distribution/GameDistributionPublishFormView'; +import { useGameDistributionPublishForm } from '../src/components/game-distribution/useGameDistributionPublishForm'; import { uploadPlatformMediaAsset } from '../src/services/assetDirectUpload'; import { + type GameDistributionPublishResult, generateGameDistributionCover, publishLocalProjectGame, readGameCoverGenerationPrice, @@ -94,13 +97,49 @@ async function selectCover( await screen.findByText(new RegExp(`已选择「${file.name}」`, 'u')); } +/** + * 内联化之后,原弹窗外壳已退役;测试直接挂在现役的内联表单上(去掉面板根那层状态提升), + * 继续覆盖预填、门禁、上传与成功 / 失败边界。 + */ +function PublishFormHarness({ + open, + projectPath, + manifest, + packageResult, + onPublished, +}: { + open: boolean; + projectPath: string; + manifest: GameCreationAppManifest; + packageResult: LocalProjectExportPackageResult | null; + onClose: () => void; + onPublished?: (result: GameDistributionPublishResult) => void; +}) { + const [notice, setNotice] = useState(null); + const form = useGameDistributionPublishForm({ + open, + projectPath, + manifest, + packageResult, + onPublished, + // 内联形态在面板根用「发布提示」弹窗承载错误;测试里用一个等价的 alert 节点接住。 + onError: setNotice, + }); + return ( + <> + {notice ?

{notice}

: null} + + + ); +} + function renderPanel( - overrides: Partial[0]> = {}, + overrides: Partial[0]> = {}, ) { const onClose = vi.fn(); const onPublished = vi.fn(); render( - { (window.localStorage as Storage | undefined)?.clear(); }); -describe('GameDistributionPublishPanel', () => { +describe('AGC 发布资料表单(内联形态)', () => { test('打开时预填游戏资料且不展示发行包技术摘要', async () => { installTauriInvoke(async () => undefined); renderPanel(); @@ -260,7 +299,7 @@ describe('GameDistributionPublishPanel', () => { ), ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '发布游戏' })); + fireEvent.click(screen.getByRole('button', { name: '打包并发布' })); await waitFor(() => expect(publishLocalProjectGame).toHaveBeenCalledTimes(1), ); @@ -298,7 +337,7 @@ describe('GameDistributionPublishPanel', () => { target: { files: [buildImageFile('shot-1.png')] }, }); await screen.findByRole('button', { name: '删除截图 1' }); - fireEvent.click(screen.getByRole('button', { name: '发布游戏' })); + fireEvent.click(screen.getByRole('button', { name: '打包并发布' })); await waitFor(() => expect(publishLocalProjectGame).toHaveBeenCalledTimes(1), @@ -335,7 +374,7 @@ describe('GameDistributionPublishPanel', () => { renderPanel(); await selectCover(); - const submit = screen.getByRole('button', { name: '发布游戏' }); + const submit = screen.getByRole('button', { name: '打包并发布' }); fireEvent.click(submit); fireEvent.click(submit); @@ -362,7 +401,7 @@ describe('GameDistributionPublishPanel', () => { renderPanel(); await selectCover(); - fireEvent.click(screen.getByRole('button', { name: '发布游戏' })); + fireEvent.click(screen.getByRole('button', { name: '打包并发布' })); expect( await screen.findByText( @@ -376,7 +415,7 @@ describe('GameDistributionPublishPanel', () => { installTauriInvoke(async () => undefined); renderPanel(); - fireEvent.click(screen.getByRole('button', { name: '发布游戏' })); + fireEvent.click(screen.getByRole('button', { name: '打包并发布' })); expect( await screen.findByText('请先选择游戏封面(JPG/PNG/WebP)'), @@ -398,7 +437,7 @@ describe('GameDistributionPublishPanel', () => { }), ); - fireEvent.click(screen.getByRole('button', { name: '发布游戏' })); + fireEvent.click(screen.getByRole('button', { name: '打包并发布' })); await waitFor(() => expect(publishLocalProjectGame).toHaveBeenCalledTimes(1), ); @@ -499,7 +538,7 @@ describe('GameDistributionPublishPanel', () => { expect(screen.queryByRole('dialog', { name: '截图预览' })).toBeNull(); }); - fireEvent.click(screen.getByRole('button', { name: '发布游戏' })); + fireEvent.click(screen.getByRole('button', { name: '打包并发布' })); await waitFor(() => expect(publishLocalProjectGame).toHaveBeenCalledTimes(1), ); @@ -541,19 +580,23 @@ describe('GameDistributionPublishPanel', () => { expect(publishLocalProjectGame).not.toHaveBeenCalled(); }); - test('不在 Tauri 宿主或缺少试玩包时失败关闭且不调用发布接口', async () => { + test('缺少封面或试玩包时失败关闭且不调用发布接口', async () => { renderPanel(); - fireEvent.click(screen.getByRole('button', { name: '发布游戏' })); - expect(await screen.findByText('需要在 Tauri App 内发布')).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '打包并发布' })); + // 校验前置:没有封面时在点击当下就给出提示,不进入打包也不提交发布。 + expect( + await screen.findByText('请先选择游戏封面(JPG/PNG/WebP)'), + ).not.toBeNull(); expect(publishLocalProjectGame).not.toHaveBeenCalled(); cleanup(); installTauriInvoke(async () => undefined); renderPanel({ packageResult: null }); - expect(screen.getByRole('button', { name: '发布游戏' })).toHaveProperty( - 'disabled', - true, - ); + await selectCover(); + fireEvent.click(screen.getByRole('button', { name: '打包并发布' })); + // 没有试玩包、也没有打包回调时,同样在点击当下就地给出原因,而不是灰掉按钮。 + expect(await screen.findByText('请先导出有效的试玩包')).not.toBeNull(); + expect(publishLocalProjectGame).not.toHaveBeenCalled(); expect(screen.queryByLabelText('发行包摘要')).toBeNull(); }); });