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();
});
});