AGC 发布面板接入买断制定价并抽取共享定价组件
Project CI / Backend tests (pull_request) Failing after 18s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m0s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m47s
Project CI / Repository checks (pull_request) Failing after 56s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m28s
Project CI / Frontend tests (pull_request) Successful in 4m18s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m7s
Project CI / Native shell tests (pull_request) Successful in 9m27s

packages/shared/src/components:新增 PlatformGamePricingField(.tsx/.css/.test.tsx)与 platformGamePricingModel(上限常量与解析),并从 components/index.ts 导出;组件只表达付费方式单选、买断价输入、上限提示、禁用态与错误行,校验由使用方持有。
src/components/common/PlatformGamePricingField.tsx:按既有的 common 适配约定新增网页端再导出。
src/components/game-distribution/GamePublishPage.tsx:把内联的付费方式 fieldset 与买断价输入替换为共享组件,文案、校验、禁用逻辑与既有用例保持不变。
src/components/game-distribution/gamePublishMetadata.ts:买断价上限与解析改为复用共享常量与共享函数,不再各写一份。
apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx:首次发布与更新两条路径都加入共享定价组件,默认免费;提交前本地校验并透传价格。
apps/ai-game-creator-shell/src/services/gameDistributionPublish.ts:发布命令载荷透传 priceMudPoints,并做 0..=1000000 兜底校验;发布草稿回读新增 priceMudPoints 字段。
apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs:Tauri 命令新增可选 price_mud_points(旧前端不传按免费,向后兼容)并替换硬编码的 0;回读作者作品时解析 priceMudPoints,按「最近版本冻结价优先、回退游戏行当前价」预填,避免已上线付费作品被静默改成免费;发布摘要纳入价格,只改价格也会换幂等键,不会重放上一次收据。
apps/ai-game-creator-shell/tests:补充买断价透传、本地校验拦截、更新模式价格预填与免费兜底用例。
packages/shared/src/components/PlatformGamePricingField.test.tsx:覆盖免费/买断制切换、价格输入、上限提示、禁用态与错误行。
This commit is contained in:
2026-10-05 20:20:53 +08:00
parent 93de2a0d6c
commit ce737f4e8b
13 changed files with 665 additions and 75 deletions
@@ -0,0 +1,2 @@
export type { PlatformGamePricingFieldProps } from '@genarrative/shared/components';
export { PlatformGamePricingField } from '@genarrative/shared/components';
@@ -20,6 +20,7 @@ import {
} from '../../services/gameDistributionClient';
import { useAuthUi } from '../auth/AuthUiContext';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformGamePricingField } from '../common/PlatformGamePricingField';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import {
GAME_METADATA_CATEGORIES,
@@ -40,7 +41,6 @@ import { applyVersionMediaDefaults } from './gamePublishMediaDefaults';
import { GamePublishMediaFieldset } from './GamePublishMediaFieldset';
import {
type GamePublishPriceMode,
MAX_GAME_PRICE_MUD_POINTS,
resolveGamePublishPriceMudPoints,
resolvePublishMetadataError,
} from './gamePublishMetadata';
@@ -804,45 +804,13 @@ export function GamePublishPage({
</label>
))}
</fieldset>
<fieldset className="game-publish-fieldset">
<legend>付费方式</legend>
<label className="game-publish-checkbox">
<input
type="radio"
name="game-price-mode"
checked={priceMode === 'free'}
disabled={isResumingDraft}
onChange={() => setPriceMode('free')}
/>
免费
</label>
<label className="game-publish-checkbox">
<input
type="radio"
name="game-price-mode"
checked={priceMode === 'paid'}
disabled={isResumingDraft}
onChange={() => setPriceMode('paid')}
/>
买断制
</label>
</fieldset>
{priceMode === 'paid' ? (
<label className="game-publish-field">
买断价(泥点)
<input
type="number"
inputMode="numeric"
value={priceInput}
disabled={isResumingDraft}
onChange={(event) => setPriceInput(event.target.value)}
/>
<span className="game-publish-hint">
买断一次永久拥有;价格随本次版本审核通过后生效,最多{' '}
{MAX_GAME_PRICE_MUD_POINTS} 泥点
</span>
</label>
) : null}
<PlatformGamePricingField
mode={priceMode}
priceInput={priceInput}
disabled={isResumingDraft}
onModeChange={setPriceMode}
onPriceInputChange={setPriceInput}
/>
<GamePublishMediaFieldset
cover={coverAsset}
screenshots={screenshotAssets}
@@ -1,39 +1,24 @@
import {
PLATFORM_GAME_MAX_PRICE_MUD_POINTS,
type PlatformGamePricingMode,
resolvePlatformGamePriceMudPoints,
} from '../../../packages/shared/src/components/platformGamePricingModel';
import type { GameDistributionDeviceSupport } from '../../../packages/shared/src/contracts/gameDistribution';
import { MAX_GAME_SCREENSHOTS } from './gamePublishAssets';
/** 买断价上限,与服务端 `MAX_GAME_PRICE_MUD_POINTS` 保持一致。 */
export const MAX_GAME_PRICE_MUD_POINTS = 1_000_000;
/** 买断价上限,复用共享定价组件常量,网页与 AGC 及服务端保持同一口径。 */
export const MAX_GAME_PRICE_MUD_POINTS = PLATFORM_GAME_MAX_PRICE_MUD_POINTS;
export type GamePublishPriceMode = 'free' | 'paid';
export type GamePublishPriceMode = PlatformGamePricingMode;
/**
* 解析发布表单的付费方式与买断价输入。
*
* 口径与服务端一致:免费等价于 `0`;买断制必须是 `1..=1_000_000` 的整数泥点。
* 返回的 `error` 为空字符串表示可用,否则 `priceMudPoints` 无意义。
* 实现落在共享 `platformGamePricingModel`,网页与 AGC 发布面板共用同一份校验。
*/
export function resolveGamePublishPriceMudPoints(
mode: GamePublishPriceMode,
rawValue: string,
): { priceMudPoints: number; error: string } {
if (mode === 'free') return { priceMudPoints: 0, error: '' };
const value = rawValue.trim();
if (!/^\d+$/u.test(value)) {
return { priceMudPoints: 0, error: '买断价必须是整数泥点' };
}
const priceMudPoints = Number(value);
if (
!Number.isSafeInteger(priceMudPoints) ||
priceMudPoints < 1 ||
priceMudPoints > MAX_GAME_PRICE_MUD_POINTS
) {
return {
priceMudPoints: 0,
error: `买断价必须是 1 到 ${MAX_GAME_PRICE_MUD_POINTS} 之间的整数泥点`,
};
}
return { priceMudPoints, error: '' };
}
export const resolveGamePublishPriceMudPoints =
resolvePlatformGamePriceMudPoints;
/** 作者侧元数据校验;口径与服务端 `validate_game_metadata` 保持一致。 */
export function resolvePublishMetadataError(input: {