@@ -122,9 +432,9 @@ export function PlatformActiveProfileView({
+
+ handleAvatarFileChange(event.target.files?.[0] ?? null)
+ }
+ />
-
- {user.displayName}
+
+
+ {user.displayName}
+
+
}
+ onClick={openNicknameModal}
+ className="platform-profile-edit-button"
+ />
-
- 陶泥号:{publicUserCode}
+
+ 陶泥号: {publicUserCode}
+ {
+ void copyText(publicUserCode);
+ }}
+ />
@@ -201,7 +541,10 @@ export function PlatformActiveProfileView({
-
+
+
+
setActiveLegalDocumentId(null)}
/>
+ {isNicknameModalOpen ? (
+ {
+ setNicknameInput(value);
+ setNicknameError(null);
+ }}
+ onClose={() => setIsNicknameModalOpen(false)}
+ onSubmit={submitNickname}
+ />
+ ) : null}
+ {avatarSource && avatarImageSize ? (
+ {
+ setAvatarSource(null);
+ setAvatarImageSize(null);
+ setAvatarError(null);
+ }}
+ onSubmit={submitAvatar}
+ />
+ ) : null}
+ {avatarError && !avatarSource ? (
+
+ {avatarError}
+
+ ) : null}
);
}
diff --git a/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx b/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx
index e0d256e83..3fec31e2e 100644
--- a/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx
+++ b/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx
@@ -20,13 +20,53 @@ vi.mock('../auth/AuthUiContext', () => ({
}));
vi.mock('../creation-home/CreationLandingView', () => ({
- CreationLandingView: () => (
- 创作主页
+ CreationLandingView: ({
+ onOpenProject,
+ onOpenProjects,
+ searchKeyword,
+ }: {
+ onOpenProject: (
+ projectId: string,
+ options?: { guide?: boolean; tool?: string },
+ ) => void;
+ onOpenProjects: () => void;
+ searchKeyword?: string;
+ }) => (
+
+ 创作主页
+
+
+
),
}));
vi.mock('../project/ProjectGalleryView', () => ({
- ProjectGalleryView: () => 项目,
+ ProjectGalleryView: ({
+ onOpenProject,
+ searchKeyword,
+ }: {
+ onOpenProject: (projectId: string, options?: { guide?: boolean }) => void;
+ searchKeyword?: string;
+ }) => (
+
+ 项目
+
+
+ ),
}));
vi.mock('../image-editor/ImageCanvasEditorView', () => ({
@@ -101,6 +141,17 @@ describe('PlatformEntryActiveFlowShell', () => {
within(topbar as HTMLElement).queryByRole('button', { name: '设置' }),
).toBeNull();
+ fireEvent.change(
+ screen.getByRole('searchbox', { name: '搜索项目和素材' }),
+ { target: { value: '角色' } },
+ );
+ fireEvent.click(screen.getByRole('button', { name: '搜索' }));
+ expect(
+ screen
+ .getByRole('main', { name: '陶泥儿创作主页' })
+ .getAttribute('data-search'),
+ ).toBe('角色');
+
fireEvent.click(within(navigation).getByRole('button', { name: '我的' }));
expect(await screen.findByRole('main', { name: '我的' })).toBeTruthy();
@@ -112,4 +163,94 @@ describe('PlatformEntryActiveFlowShell', () => {
.getAttribute('aria-current'),
).toBe('page');
});
+
+ it('switches between creation and projects and passes search to the active page', async () => {
+ const setSelectionStage = vi.fn();
+ const { rerender } = render(
+ ,
+ );
+
+ const navigation = await screen.findByRole('navigation', {
+ name: '平台导航',
+ });
+ fireEvent.click(within(navigation).getByRole('button', { name: '项目' }));
+
+ expect(window.location.pathname).toBe('/project');
+ expect(setSelectionStage).toHaveBeenCalledWith('project', {
+ path: '/project',
+ });
+
+ rerender(
+ ,
+ );
+
+ const projectPage = await screen.findByRole('main', { name: '项目' });
+ expect(
+ within(navigation)
+ .getByRole('button', { name: '项目' })
+ .getAttribute('aria-current'),
+ ).toBe('page');
+ expect(
+ within(navigation)
+ .getByRole('button', { name: '创作' })
+ .getAttribute('aria-current'),
+ ).toBeNull();
+
+ fireEvent.change(
+ screen.getByRole('searchbox', { name: '搜索项目和素材' }),
+ { target: { value: '场景' } },
+ );
+ fireEvent.click(screen.getByRole('button', { name: '搜索' }));
+ expect(projectPage.getAttribute('data-search')).toBe('场景');
+
+ fireEvent.click(within(navigation).getByRole('button', { name: '创作' }));
+ expect(window.location.pathname).toBe('/creation');
+ expect(setSelectionStage).toHaveBeenCalledWith('creation-home', {
+ path: '/creation',
+ });
+ });
+
+ it('keeps guide and tool intent in editor navigation URLs', async () => {
+ const setSelectionStage = vi.fn();
+ const { rerender } = render(
+ ,
+ );
+
+ fireEvent.click(
+ await screen.findByRole('button', { name: '打开引导项目' }),
+ );
+ expect(`${window.location.pathname}${window.location.search}`).toBe(
+ '/editor/canvas?projectid=guide-project&guide=toolbar',
+ );
+ expect(setSelectionStage).toHaveBeenCalledWith('image-editor', {
+ path: '/editor/canvas?projectid=guide-project&guide=toolbar',
+ });
+
+ window.history.replaceState(null, '', '/creation');
+ setSelectionStage.mockClear();
+ rerender(
+ ,
+ );
+ fireEvent.click(
+ await screen.findByRole('button', { name: '打开音乐工具' }),
+ );
+ expect(`${window.location.pathname}${window.location.search}`).toBe(
+ '/editor/canvas?projectid=tool-project&tool=background-music',
+ );
+ expect(setSelectionStage).toHaveBeenCalledWith('image-editor', {
+ path: '/editor/canvas?projectid=tool-project&tool=background-music',
+ });
+ });
});
diff --git a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx
index 6d9ec2a4a..020bc3ca2 100644
--- a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx
+++ b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx
@@ -1,10 +1,7 @@
-import {
- FolderKanban,
- Palette,
- UserRound,
-} from 'lucide-react';
+import { FolderKanban, Palette, Search, UserRound } from 'lucide-react';
import {
type ComponentType,
+ type FormEvent,
lazy,
Suspense,
useCallback,
@@ -20,13 +17,17 @@ import {
} from '../../routing/activeAppPageRoutes';
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
import { useAuthUi } from '../auth/AuthUiContext';
+import { FLOATING_FEEDBACK_FORM_URL } from '../common/floatingFeedbackEntryModel';
import {
resolveActivePublicUserCode,
resolveActiveUserAvatarLabel,
} from './platformActiveProfileModel';
import { PlatformActiveProfileView } from './PlatformActiveProfileView';
import type { PlatformEntryFlowShellProps } from './platformEntryActiveTypes';
+import { PlatformProfileApiKeysModal } from './PlatformProfileApiKeysModal';
import { PlatformProfileRechargeModal } from './PlatformProfileRechargeModal';
+import { PlatformProfileReferralModal } from './PlatformProfileReferralModal';
+import { PlatformProfileRewardCodeRedeemModal } from './PlatformProfileRewardCodeRedeemModal';
import { PlatformProfileWalletLedgerModal } from './PlatformProfileWalletLedgerModal';
import {
PlatformRechargePaymentConfirmationMask,
@@ -138,6 +139,9 @@ export function PlatformEntryFlowShellImpl({
);
const [isLoadingDashboard, setIsLoadingDashboard] = useState(false);
const [isProfileStage, setIsProfileStage] = useState(false);
+ const [isApiKeysOpen, setIsApiKeysOpen] = useState(false);
+ const [searchInput, setSearchInput] = useState('');
+ const [activeSearchKeyword, setActiveSearchKeyword] = useState('');
const refreshDashboard = useCallback(async () => {
if (!authUi?.user || !authUi.canAccessProtectedData) {
@@ -248,6 +252,17 @@ export function PlatformEntryFlowShellImpl({
}
authUi?.openLoginModal();
};
+ const openFeedback = () => {
+ window.open(FLOATING_FEEDBACK_FORM_URL, '_blank', 'noopener,noreferrer');
+ };
+ const submitSearch = (event: FormEvent) => {
+ event.preventDefault();
+ const keyword = searchInput.trim();
+ setActiveSearchKeyword(keyword);
+ if (isProfileStage) {
+ openCreation();
+ }
+ };
return (
<>
@@ -292,13 +307,41 @@ export function PlatformEntryFlowShellImpl({
-
- {isProfileStage
- ? '我的'
- : isCreationStage
- ? '创作工具'
- : '项目'}
-
+
{isAuthenticated ? (
@@ -355,7 +398,7 @@ export function PlatformEntryFlowShellImpl({
{isProfileStage ? (
authUi?.openLoginModal()}
- onOpenAccount={() => authUi?.openAccountModal()}
- onOpenRecharge={openRecharge}
- onOpenSettings={() => authUi?.openSettingsModal()}
- onOpenWalletLedger={
- profileCenter.openWalletLedgerPanel
+ onOpenApiKeys={() => setIsApiKeysOpen(true)}
+ onOpenCommunity={() =>
+ profileCenter.openProfilePopupPanel('community')
}
+ onOpenFeedback={openFeedback}
+ onOpenRecharge={openRecharge}
+ onOpenRewardCode={profileCenter.openRewardCodeModal}
+ onOpenSettings={() => authUi?.openSettingsModal()}
+ onOpenWalletLedger={profileCenter.openWalletLedgerPanel}
+ onUserUpdated={(user) => authUi?.setCurrentUser(user)}
/>
) : isCreationStage ? (
) : (
}>
-
+
)}
@@ -404,6 +455,36 @@ export function PlatformEntryFlowShellImpl({
onCloseNativePayment={profileCenter.closeNativeWechatPayment}
/>
) : null}
+ {profileCenter.isRewardCodeOpen ? (
+
profileCenter.setIsRewardCodeOpen(false)}
+ />
+ ) : null}
+ {profileCenter.profilePopupPanel ? (
+
+ ) : null}
+ {isApiKeysOpen ? (
+ setIsApiKeysOpen(false)} />
+ ) : null}
{profileCenter.isWalletLedgerOpen ? (
(null);
const { copyState, copyText } = useCopyFeedback();
- const activeKeys = useMemo(
- () => keys.filter(isActiveExternalApiKey),
- [keys],
+ const activeKeys = useMemo(() => keys.filter(isActiveExternalApiKey), [keys]);
+ const shouldShowBlockingError = Boolean(
+ error && !isLoading && keys.length === 0,
);
- const shouldShowBlockingError = Boolean(error && !isLoading && keys.length === 0);
const loadKeys = useCallback(() => {
setIsLoading(true);
setError(null);
- void listRpgProfileExternalApiKeys()
+ void listPlatformProfileExternalApiKeys()
.then((response) => {
setKeys(response.keys);
})
.catch((loadError: unknown) => {
- setError(loadError instanceof Error ? loadError.message : '读取 API Key 失败');
+ setError(
+ loadError instanceof Error ? loadError.message : '读取 API Key 失败',
+ );
})
.finally(() => setIsLoading(false));
}, []);
@@ -86,7 +96,7 @@ export function PlatformProfileApiKeysModal({
setIsCreating(true);
setError(null);
setCreatedKey(null);
- void createRpgProfileExternalApiKey(nameInput)
+ void createPlatformProfileExternalApiKey(nameInput)
.then((response) => {
setCreatedKey(response);
setKeys((current) => [
@@ -97,7 +107,9 @@ export function PlatformProfileApiKeysModal({
})
.catch((createError: unknown) => {
setError(
- createError instanceof Error ? createError.message : '创建 API Key 失败',
+ createError instanceof Error
+ ? createError.message
+ : '创建 API Key 失败',
);
})
.finally(() => setIsCreating(false));
@@ -106,7 +118,7 @@ export function PlatformProfileApiKeysModal({
const revokeKey = useCallback((keyId: string) => {
setRevokingKeyId(keyId);
setError(null);
- void revokeRpgProfileExternalApiKey(keyId)
+ void revokePlatformProfileExternalApiKey(keyId)
.then((response) => {
setKeys((current) =>
current.map((key) =>
@@ -119,7 +131,9 @@ export function PlatformProfileApiKeysModal({
})
.catch((revokeError: unknown) => {
setError(
- revokeError instanceof Error ? revokeError.message : '撤销 API Key 失败',
+ revokeError instanceof Error
+ ? revokeError.message
+ : '撤销 API Key 失败',
);
})
.finally(() => setRevokingKeyId(null));
diff --git a/src/components/platform-entry/PlatformProfileRechargeModal.tsx b/src/components/platform-entry/PlatformProfileRechargeModal.tsx
index f5acb0023..9d6de2d84 100644
--- a/src/components/platform-entry/PlatformProfileRechargeModal.tsx
+++ b/src/components/platform-entry/PlatformProfileRechargeModal.tsx
@@ -14,7 +14,7 @@ import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformProfileSkeletonList } from '../common/PlatformProfileSkeletonList';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
-import { formatRechargePrice } from '../rpg-entry/rpgEntryProfileFundsViewModel';
+import { formatPlatformRechargePrice } from './platformProfileFundsModel';
import { PlatformProfileModalShell } from './PlatformProfileModalShell';
import type { NativeWechatPaymentState } from './usePlatformProfileCenterController';
@@ -123,7 +123,7 @@ function RechargeProductCard({
interactive
radius="sm"
padding="none"
- aria-label={`${formatMudPointCount(product.pointsAmount)}泥点${bonusLabel ? ` ${bonusLabel}` : ''} ${formatRechargePrice(product.priceCents)} 购买`}
+ aria-label={`${formatMudPointCount(product.pointsAmount)}泥点${bonusLabel ? ` ${bonusLabel}` : ''} ${formatPlatformRechargePrice(product.priceCents)} 购买`}
className="platform-recharge-product-row platform-interactive-card relative grid min-h-[4.5rem] grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-3 px-3.5 py-3 text-left"
>
@@ -148,7 +148,7 @@ function RechargeProductCard({
- {formatRechargePrice(product.priceCents)}
+ {formatPlatformRechargePrice(product.priceCents)}
{submitting ? '处理中' : '购买'}
@@ -194,7 +194,7 @@ function PlatformProfileWechatNativePaymentModal({
支付金额
- {formatRechargePrice(nativePayment.amountCents)}
+ {formatPlatformRechargePrice(nativePayment.amountCents)}
剩余时间
diff --git a/src/components/platform-entry/PlatformProfileWalletLedgerModal.tsx b/src/components/platform-entry/PlatformProfileWalletLedgerModal.tsx
index e02c9bb3a..2340b65ee 100644
--- a/src/components/platform-entry/PlatformProfileWalletLedgerModal.tsx
+++ b/src/components/platform-entry/PlatformProfileWalletLedgerModal.tsx
@@ -9,8 +9,10 @@ import { PlatformProfileContentRow } from '../common/PlatformProfileContentRow';
import { PlatformProfileSkeletonList } from '../common/PlatformProfileSkeletonList';
import { PlatformProfileSummaryHeader } from '../common/PlatformProfileSummaryHeader';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
-import { buildWalletLedgerPresentation } from '../rpg-entry/rpgEntryProfileFundsViewModel';
-import { formatPlatformWorldTime } from '../rpg-entry/rpgEntryWorldPresentation';
+import {
+ buildPlatformWalletLedgerPresentation,
+ formatPlatformProfileTime,
+} from './platformProfileFundsModel';
import { PlatformProfileSecondaryModalShell } from './PlatformProfileModalShell';
export type PlatformProfileWalletLedgerModalProps = {
@@ -34,7 +36,7 @@ export function PlatformProfileWalletLedgerModal({
onClose,
onRetry,
}: PlatformProfileWalletLedgerModalProps) {
- const walletLedgerPresentation = buildWalletLedgerPresentation(
+ const walletLedgerPresentation = buildPlatformWalletLedgerPresentation(
ledger,
fallbackBalance,
);
@@ -116,7 +118,7 @@ export function PlatformProfileWalletLedgerModal({
{entry.sourceLabel}
- {formatPlatformWorldTime(entry.createdAt)}
+ {formatPlatformProfileTime(entry.createdAt)}
diff --git a/src/components/platform-entry/platformProfileFundsModel.ts b/src/components/platform-entry/platformProfileFundsModel.ts
new file mode 100644
index 000000000..23a83a59d
--- /dev/null
+++ b/src/components/platform-entry/platformProfileFundsModel.ts
@@ -0,0 +1,127 @@
+import type {
+ ProfileWalletLedgerEntry,
+ ProfileWalletLedgerResponse,
+} from '../../../packages/shared/src/contracts/runtime';
+
+const PLATFORM_WALLET_LEDGER_SOURCE_LABELS = {
+ new_user_registration_reward: '注册赠送',
+ points_recharge: '泥点充值',
+ invite_inviter_reward: '邀请奖励',
+ invite_invitee_reward: '填写邀请码奖励',
+ snapshot_sync: '账户同步',
+ membership_period_grant: '会员周期发放',
+ membership_period_reset: '会员周期重置',
+ daily_free_grant: '每日免费发放',
+ daily_free_reset: '每日免费重置',
+ asset_operation_consume: '资产操作消耗',
+ asset_operation_refund: '资产操作退回',
+ recharge_refund_recovery: '充值退款追回',
+ redeem_code_reward: '兑换码奖励',
+ puzzle_author_incentive_claim: '拼图作者奖励',
+ daily_task_reward: '每日任务奖励',
+} satisfies Record
;
+
+export type PlatformWalletLedgerEntryPresentation = {
+ amountLabel: string;
+ balanceLabel: string;
+ createdAt: string;
+ id: string;
+ isIncome: boolean;
+ sourceLabel: string;
+};
+
+export type PlatformWalletLedgerPresentation = {
+ balance: number;
+ balanceLabel: string;
+ entries: PlatformWalletLedgerEntryPresentation[];
+};
+
+function getPlatformWalletLedgerSourceLabel(
+ sourceType: string | null | undefined,
+) {
+ const normalizedSourceType = sourceType?.trim() ?? '';
+ if (!normalizedSourceType) {
+ return '未知来源';
+ }
+
+ return (
+ PLATFORM_WALLET_LEDGER_SOURCE_LABELS[
+ normalizedSourceType as ProfileWalletLedgerEntry['sourceType']
+ ] ?? normalizedSourceType
+ );
+}
+
+function buildPlatformWalletLedgerEntryPresentation(
+ entry: ProfileWalletLedgerEntry,
+): PlatformWalletLedgerEntryPresentation {
+ return {
+ amountLabel:
+ entry.amountDelta > 0 ? `+${entry.amountDelta}` : `${entry.amountDelta}`,
+ balanceLabel: `余额 ${entry.balanceAfter}`,
+ createdAt: entry.createdAt,
+ id: entry.id,
+ isIncome: entry.amountDelta > 0,
+ sourceLabel: getPlatformWalletLedgerSourceLabel(entry.sourceType),
+ };
+}
+
+export function buildPlatformWalletLedgerPresentation(
+ ledger: ProfileWalletLedgerResponse | null,
+ fallbackBalance: number,
+): PlatformWalletLedgerPresentation {
+ const entries = ledger?.entries ?? [];
+ const balance = entries[0]?.balanceAfter ?? fallbackBalance;
+
+ return {
+ balance,
+ balanceLabel: `${balance}泥点`,
+ entries: entries.map(buildPlatformWalletLedgerEntryPresentation),
+ };
+}
+
+export function formatPlatformRechargePrice(priceCents: number) {
+ const yuan = priceCents / 100;
+ return `¥${Number.isInteger(yuan) ? yuan.toFixed(0) : yuan.toFixed(2)}`;
+}
+
+function parsePlatformProfileDate(value: string) {
+ const normalized = value.trim();
+ const numericTimestamp = normalized.match(/^(-?\d+(?:\.\d+)?)(?:Z)?$/u);
+ if (numericTimestamp?.[1]) {
+ const rawTimestamp = Number(numericTimestamp[1]);
+ if (Number.isFinite(rawTimestamp)) {
+ const absoluteTimestamp = Math.abs(rawTimestamp);
+ const timestampMs =
+ absoluteTimestamp >= 1_000_000_000_000_000
+ ? rawTimestamp / 1000
+ : absoluteTimestamp >= 1_000_000_000_000
+ ? rawTimestamp
+ : absoluteTimestamp >= 1_000_000_000
+ ? rawTimestamp * 1000
+ : Number.NaN;
+ const date = new Date(timestampMs);
+ if (!Number.isNaN(date.getTime())) {
+ return date;
+ }
+ }
+ }
+
+ const date = new Date(normalized);
+ return Number.isNaN(date.getTime()) ? null : date;
+}
+
+export function formatPlatformProfileTime(value: string | null) {
+ if (!value) {
+ return '未记录';
+ }
+
+ const date = parsePlatformProfileDate(value);
+ if (!date) {
+ return value;
+ }
+
+ const year = date.getUTCFullYear();
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
+ const day = String(date.getUTCDate()).padStart(2, '0');
+ return `${year}-${month}-${day}`;
+}
diff --git a/src/components/platform-entry/usePlatformProfileCenterController.ts b/src/components/platform-entry/usePlatformProfileCenterController.ts
index dafbd71c9..35e446970 100644
--- a/src/components/platform-entry/usePlatformProfileCenterController.ts
+++ b/src/components/platform-entry/usePlatformProfileCenterController.ts
@@ -6,15 +6,11 @@ import {
type ProfileRechargeOrder,
type ProfileRechargeProduct,
type ProfileReferralInviteCenterResponse,
- type ProfileTaskCenterResponse,
type ProfileWalletLedgerResponse,
type RedeemProfileRewardCodeResponse,
type WechatNativePayment,
} from '../../../packages/shared/src/contracts/runtime';
-import {
- clearStoredAccessToken,
- refreshStoredAccessToken,
-} from '../../services/apiClient';
+import { clearStoredAccessToken } from '../../services/apiClient';
import { type AuthUser, startWechatBind } from '../../services/authService';
import {
getHostRuntime,
@@ -31,25 +27,20 @@ import {
import { redirectToPaymentUrl } from '../../services/payment/paymentRedirect';
import { requestWechatJsapiPayment } from '../../services/payment/wechatJsapiPayment';
import {
- claimRpgProfileTaskReward,
- confirmWechatRpgProfileRechargeOrder,
- createRpgProfileRechargeOrder,
- getRpgProfileRechargeCenter,
- getRpgProfileReferralInviteCenter,
- getRpgProfileTasks,
- getRpgProfileWalletLedger,
- redeemRpgProfileReferralInviteCode,
- redeemRpgProfileRewardCode,
- watchWechatRpgProfileRechargeOrder,
-} from '../../services/rpg-entry/rpgProfileClient';
+ confirmWechatPlatformProfileRechargeOrder,
+ createPlatformProfileRechargeOrder,
+ getPlatformProfileRechargeCenter,
+ getPlatformProfileReferralInviteCenter,
+ getPlatformProfileWalletLedger,
+ redeemPlatformProfileReferralInviteCode,
+ redeemPlatformProfileRewardCode,
+ watchWechatPlatformProfileRechargeOrder,
+} from '../../services/platform-entry/platformProfileClient';
import {
type CopyFeedbackState,
useCopyFeedback,
} from '../common/useCopyFeedback';
-const PROFILE_TASK_DAY_MS = 24 * 60 * 60 * 1000;
-const PROFILE_TASK_BEIJING_OFFSET_MS = 8 * 60 * 60 * 1000;
-const PROFILE_TASK_MIN_RESET_DELAY_MS = 1000;
const PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code'] as const;
const WECHAT_NATIVE_CONFIRM_RETRY_DELAYS_MS = [800, 1600] as const;
const WECHAT_NATIVE_WATCH_RETRY_DELAY_MS = 1000;
@@ -111,21 +102,11 @@ type UsePlatformProfileCenterControllerArgs = {
activeTab: string;
isAuthenticated: boolean;
showRechargeEntry: boolean;
- profileTaskRefreshKey?: number;
onRechargeSuccess?: () => void | Promise;
requestLogin: () => void;
currentUser: AuthUser | null | undefined;
};
-function getDelayUntilNextProfileTaskReset(nowMs = Date.now()) {
- const shiftedNow = nowMs + PROFILE_TASK_BEIJING_OFFSET_MS;
- const nextDayStart =
- Math.floor(shiftedNow / PROFILE_TASK_DAY_MS) * PROFILE_TASK_DAY_MS +
- PROFILE_TASK_DAY_MS;
- const nextResetAt = nextDayStart - PROFILE_TASK_BEIJING_OFFSET_MS;
- return Math.max(PROFILE_TASK_MIN_RESET_DELAY_MS, nextResetAt - nowMs);
-}
-
function readProfileInviteCodeFromLocationSearch(search: string) {
const params = new URLSearchParams(search);
for (const key of PROFILE_INVITE_QUERY_KEYS) {
@@ -222,7 +203,7 @@ function isWechatRechargeOrderTerminalForConfirmation(
async function confirmWechatRechargeOrderUntilSettled(
orderId: string,
): Promise {
- let latestResponse = await confirmWechatRpgProfileRechargeOrder(orderId);
+ let latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId);
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
return latestResponse;
}
@@ -230,14 +211,15 @@ async function confirmWechatRechargeOrderUntilSettled(
for (const delayMs of WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS) {
await waitWechatPayConfirmDelay(delayMs);
- latestResponse = await confirmWechatRpgProfileRechargeOrder(orderId);
+ latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId);
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
return latestResponse;
}
}
try {
- const streamedResponse = await watchWechatRpgProfileRechargeOrder(orderId);
+ const streamedResponse =
+ await watchWechatPlatformProfileRechargeOrder(orderId);
return streamedResponse;
} catch {
return latestResponse;
@@ -247,7 +229,7 @@ async function confirmWechatRechargeOrderUntilSettled(
async function confirmWechatRechargeOrderQuickly(
orderId: string,
): Promise {
- let latestResponse = await confirmWechatRpgProfileRechargeOrder(orderId);
+ let latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId);
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
return latestResponse;
}
@@ -255,7 +237,7 @@ async function confirmWechatRechargeOrderQuickly(
for (const delayMs of WECHAT_NATIVE_CONFIRM_RETRY_DELAYS_MS) {
await waitWechatPayConfirmDelay(delayMs);
- latestResponse = await confirmWechatRpgProfileRechargeOrder(orderId);
+ latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId);
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
return latestResponse;
}
@@ -314,7 +296,6 @@ export function usePlatformProfileCenterController({
activeTab,
isAuthenticated,
showRechargeEntry,
- profileTaskRefreshKey = 0,
onRechargeSuccess,
requestLogin,
currentUser,
@@ -350,14 +331,6 @@ export function usePlatformProfileCenterController({
null,
);
const [isLoadingWalletLedger, setIsLoadingWalletLedger] = useState(false);
- const [isTaskCenterOpen, setIsTaskCenterOpen] = useState(false);
- const [taskCenter, setTaskCenter] =
- useState(null);
- const [taskCenterError, setTaskCenterError] = useState(null);
- const [isLoadingTaskCenter, setIsLoadingTaskCenter] = useState(false);
- const taskCenterRequestIdRef = useRef(0);
- const [claimingTaskId, setClaimingTaskId] = useState(null);
- const [taskClaimSuccess, setTaskClaimSuccess] = useState(null);
const [profilePopupPanel, setProfilePopupPanel] =
useState(null);
const [referralCenter, setReferralCenter] =
@@ -410,7 +383,7 @@ export function usePlatformProfileCenterController({
const loadWalletLedger = useCallback(() => {
setWalletLedgerError(null);
setIsLoadingWalletLedger(true);
- void getRpgProfileWalletLedger()
+ void getPlatformProfileWalletLedger()
.then(setWalletLedger)
.catch((error: unknown) => {
setWalletLedger(null);
@@ -429,7 +402,7 @@ export function usePlatformProfileCenterController({
const loadRechargeCenter = useCallback(() => {
setRechargeError(null);
setIsLoadingRechargeCenter(true);
- void getRpgProfileRechargeCenter()
+ void getPlatformProfileRechargeCenter()
.then(setRechargeCenter)
.catch((error: unknown) => {
setRechargeCenter(null);
@@ -621,7 +594,7 @@ export function usePlatformProfileCenterController({
setRechargePaymentResult(null);
setWechatRechargeOrderConfirmationState(null);
setNativeWechatPayment(null);
- void createRpgProfileRechargeOrder(product.productId, paymentChannel)
+ void createPlatformProfileRechargeOrder(product.productId, paymentChannel)
.then(async (response) => {
if (paymentChannel === WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_CHANNEL) {
pendingWechatRechargeOrderIdRef.current = response.order.orderId;
@@ -833,9 +806,12 @@ export function usePlatformProfileCenterController({
const watchUntilSettled = async () => {
while (!cancelled && Date.now() < expiresAtMs) {
try {
- const response = await watchWechatRpgProfileRechargeOrder(orderId, {
- signal: abortController.signal,
- });
+ const response = await watchWechatPlatformProfileRechargeOrder(
+ orderId,
+ {
+ signal: abortController.signal,
+ },
+ );
if (
cancelled ||
!response ||
@@ -951,90 +927,10 @@ export function usePlatformProfileCenterController({
wechatRechargeOrderConfirmationState,
]);
- const loadTaskCenter = useCallback(() => {
- const requestId = ++taskCenterRequestIdRef.current;
- setTaskCenterError(null);
- setIsLoadingTaskCenter(true);
- void getRpgProfileTasks()
- .then((center) => {
- if (requestId === taskCenterRequestIdRef.current) {
- setTaskCenter(center);
- }
- })
- .catch((error: unknown) => {
- if (requestId !== taskCenterRequestIdRef.current) {
- return;
- }
- setTaskCenter(null);
- setTaskCenterError(
- error instanceof Error ? error.message : '读取每日任务失败',
- );
- })
- .finally(() => {
- if (requestId === taskCenterRequestIdRef.current) {
- setIsLoadingTaskCenter(false);
- }
- });
- }, []);
-
- useEffect(() => {
- if (activeTab !== 'profile' || !isAuthenticated) {
- taskCenterRequestIdRef.current += 1;
- setTaskCenter(null);
- setTaskCenterError(null);
- return;
- }
-
- loadTaskCenter();
- }, [activeTab, isAuthenticated, loadTaskCenter, profileTaskRefreshKey]);
-
- useEffect(() => {
- if (activeTab !== 'profile' || !isAuthenticated) {
- return undefined;
- }
-
- // 中文注释:每日任务重置依赖北京时间跨天与 access token 刷新,继续留在 controller 里集中托管。
- let cancelled = false;
- let timer: number | null = null;
-
- const scheduleNextReset = () => {
- if (cancelled) {
- return;
- }
- timer = window.setTimeout(() => {
- void refreshStoredAccessToken({ clearOnFailure: false })
- .catch(() => undefined)
- .finally(() => {
- if (cancelled) {
- return;
- }
- loadTaskCenter();
- scheduleNextReset();
- });
- }, getDelayUntilNextProfileTaskReset());
- };
-
- scheduleNextReset();
- return () => {
- cancelled = true;
- if (timer !== null) {
- window.clearTimeout(timer);
- }
- };
- }, [activeTab, isAuthenticated, loadTaskCenter]);
-
- const openTaskCenterPanel = useCallback(() => {
- setIsTaskCenterOpen(true);
- setTaskClaimSuccess(null);
- if (!taskCenter) {
- loadTaskCenter();
- }
- }, [loadTaskCenter, taskCenter]);
-
const loadReferralCenter = useCallback(() => {
setIsLoadingReferral(true);
setIsReferralCenterInitialized(false);
- void getRpgProfileReferralInviteCenter()
+ void getPlatformProfileReferralInviteCenter()
.then(setReferralCenter)
.catch((error: unknown) => {
setReferralCenter(null);
@@ -1114,7 +1010,7 @@ export function usePlatformProfileCenterController({
setIsSubmittingReferralRedeem(true);
setReferralError(null);
setReferralSuccess(null);
- void redeemRpgProfileReferralInviteCode(inviteCode)
+ void redeemPlatformProfileReferralInviteCode(inviteCode)
.then((response) => {
setReferralCenter(response.center);
setReferralRedeemCode('');
@@ -1137,7 +1033,7 @@ export function usePlatformProfileCenterController({
setIsSubmittingRewardCode(true);
setRewardCodeError(null);
setRewardCodeSuccess(null);
- void redeemRpgProfileRewardCode(rewardCodeInput)
+ void redeemPlatformProfileRewardCode(rewardCodeInput)
.then((response: RedeemProfileRewardCodeResponse) => {
setRewardCodeInput('');
setRewardCodeSuccess(`已到账 ${response.amountGranted} 泥点`);
@@ -1149,57 +1045,26 @@ export function usePlatformProfileCenterController({
.finally(() => setIsSubmittingRewardCode(false));
}, [isSubmittingRewardCode, onRechargeSuccess, rewardCodeInput]);
- const claimTaskReward = useCallback(
- (taskId: string) => {
- if (claimingTaskId) {
- return;
- }
-
- setClaimingTaskId(taskId);
- setTaskCenterError(null);
- setTaskClaimSuccess(null);
- void claimRpgProfileTaskReward(taskId)
- .then((response) => {
- setTaskCenter(response.center);
- setTaskClaimSuccess(`已领取 ${response.rewardPoints} 泥点`);
- void onRechargeSuccess?.();
- })
- .catch((error: unknown) => {
- setTaskCenterError(
- error instanceof Error ? error.message : '领取任务奖励失败',
- );
- })
- .finally(() => setClaimingTaskId(null));
- },
- [claimingTaskId, onRechargeSuccess],
- );
-
return {
- claimTaskReward,
- claimingTaskId,
closeNativeWechatPayment,
closeProfilePopupPanel,
confirmNativeWechatPayment,
inviteCopyState: inviteCopyState as CopyFeedbackState,
isLoadingRechargeCenter,
isLoadingReferral,
- isLoadingTaskCenter,
isLoadingWalletLedger,
isRechargeOpen,
isRewardCodeOpen,
isSubmittingReferralRedeem,
isSubmittingRewardCode,
- isTaskCenterOpen,
isWalletLedgerOpen,
loadRechargeCenter,
loadReferralCenter,
- loadTaskCenter,
loadWalletLedger,
nativeWechatPayment,
openProfilePopupPanel,
openRechargeOrRewardCodeModal,
openRewardCodeModal,
- openTaskCenterPanel,
openWalletLedgerPanel,
profilePopupPanel,
rechargeCenter,
@@ -1214,7 +1079,6 @@ export function usePlatformProfileCenterController({
rewardCodeSuccess,
setIsRechargeOpen,
setIsRewardCodeOpen,
- setIsTaskCenterOpen,
setIsWalletLedgerOpen,
setRechargePaymentResult,
setReferralRedeemCode,
@@ -1223,9 +1087,6 @@ export function usePlatformProfileCenterController({
submittingRechargeProductId,
submitReferralRedeemCode,
submitRewardCode,
- taskCenter,
- taskCenterError,
- taskClaimSuccess,
walletLedger,
walletLedgerError,
wechatRechargeOrderConfirmationState,
diff --git a/src/components/project/ProjectGalleryView.test.tsx b/src/components/project/ProjectGalleryView.test.tsx
index a118a2c76..dedd8d5b1 100644
--- a/src/components/project/ProjectGalleryView.test.tsx
+++ b/src/components/project/ProjectGalleryView.test.tsx
@@ -113,6 +113,27 @@ describe('ProjectGalleryView', () => {
expect(onOpenProject).toHaveBeenCalledWith('editor-project-1');
});
+ it('filters projects by title and renders the search empty state', async () => {
+ listEditorProjectsMock.mockResolvedValue(projectItems);
+ const { rerender } = renderProjectGalleryView({
+ searchKeyword: '角色',
+ });
+
+ expect(await screen.findByText('角色设定板')).toBeTruthy();
+ expect(screen.queryByText('场景草图')).toBeNull();
+
+ rerender(
+ ,
+ );
+
+ expect(await screen.findByText('没有匹配项目')).toBeTruthy();
+ expect(screen.queryByText('角色设定板')).toBeNull();
+ expect(screen.queryByText('场景草图')).toBeNull();
+ });
+
it('uses the saved project cover snapshot resource as the project cover', async () => {
listEditorProjectsMock.mockResolvedValueOnce([
{
diff --git a/src/components/project/ProjectGalleryView.tsx b/src/components/project/ProjectGalleryView.tsx
index 4d8286020..eb7da172e 100644
--- a/src/components/project/ProjectGalleryView.tsx
+++ b/src/components/project/ProjectGalleryView.tsx
@@ -38,6 +38,7 @@ import {
type ProjectGalleryViewProps = {
onOpenProject: (projectId: string, options?: { guide?: boolean }) => void;
+ searchKeyword?: string;
};
type RenameDraft = {
@@ -68,7 +69,10 @@ function formatProjectUpdatedAt(value: string) {
}).format(date);
}
-export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) {
+export function ProjectGalleryView({
+ onOpenProject,
+ searchKeyword = '',
+}: ProjectGalleryViewProps) {
const authUi = useAuthUi();
const [projects, setProjects] = useState([]);
const [isLoading, setIsLoading] = useState(true);
@@ -88,6 +92,15 @@ export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) {
const selectedCount = selectedProjectIds.size;
const allSelected =
projects.length > 0 && selectedProjectIds.size === projects.length;
+ const normalizedSearchKeyword = searchKeyword.trim().toLocaleLowerCase();
+ const visibleProjects = useMemo(() => {
+ if (!normalizedSearchKeyword) {
+ return projects;
+ }
+ return projects.filter((project) =>
+ project.title.toLocaleLowerCase().includes(normalizedSearchKeyword),
+ );
+ }, [normalizedSearchKeyword, projects]);
const localCoverUrlByProjectId = useMemo(
() =>
@@ -252,7 +265,7 @@ export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) {
const projectCards = useMemo(
() =>
- projects.map((project) => {
+ visibleProjects.map((project) => {
const selected = selectedProjectIds.has(project.projectId);
const localCover = localCoverUrlByProjectId.get(project.projectId);
const projectWithLocalCover = localCover
@@ -377,9 +390,9 @@ export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) {
isSelectionMode,
localCoverUrlByProjectId,
onOpenProject,
- projects,
selectedProjectIds,
toggleProjectSelection,
+ visibleProjects,
],
);
@@ -421,6 +434,10 @@ export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) {
正在读取项目
+ ) : visibleProjects.length === 0 && normalizedSearchKeyword ? (
+
+ 没有匹配项目
+
) : projects.length === 0 ? (
{
const nextVolume = clampVolume(settings.musicVolume);
const nextPlatformTheme = normalizePlatformTheme(settings.platformTheme);
@@ -133,7 +133,7 @@ export function useGameSettings(authenticatedUserId: string | null = null) {
setIsPersistingSettings(true);
setSettingsError(null);
- void putRpgProfileSettings(
+ void putPlatformRuntimeSettings(
{
musicVolume,
platformTheme,
diff --git a/src/services/platform-entry/platformProfileClient.ts b/src/services/platform-entry/platformProfileClient.ts
index 91380ae48..7164a8118 100644
--- a/src/services/platform-entry/platformProfileClient.ts
+++ b/src/services/platform-entry/platformProfileClient.ts
@@ -1,5 +1,364 @@
-/**
- * 平台首页资料读取入口。
- * 复用 RPG profile 聚合出口,避免平台入口和 RPG 入口测试、鉴权包装出现两套读取口径。
- */
-export { getRpgProfileDashboard as getPlatformProfileDashboard } from '../rpg-entry';
+import type {
+ ConfirmWechatProfileRechargeOrderResponse,
+ CreateProfileRechargeOrderResponse,
+ ExternalApiKeyCreateResponse,
+ ExternalApiKeyListResponse,
+ ExternalApiKeyMutationResponse,
+ ProfileDashboardSummary,
+ ProfileRechargeCenterResponse,
+ ProfileRechargeOrder,
+ ProfileReferralInviteCenterResponse,
+ ProfileWalletLedgerResponse,
+ RedeemProfileReferralInviteCodeResponse,
+ RedeemProfileRewardCodeResponse,
+} from '../../../packages/shared/src/contracts/runtime';
+import {
+ appendApiErrorRequestId,
+ parseApiErrorMessage,
+} from '../../../packages/shared/src/http';
+import {
+ type ApiAuthImpact,
+ type ApiRetryOptions,
+ fetchWithApiAuth,
+ requestJson,
+} from '../apiClient';
+import { readSseJsonStream } from '../sseStream';
+
+const PLATFORM_PROFILE_API_BASE = '/api/profile';
+const PLATFORM_PROFILE_READ_RETRY: ApiRetryOptions = {
+ maxRetries: 1,
+ baseDelayMs: 180,
+ maxDelayMs: 480,
+};
+const PLATFORM_PROFILE_WRITE_RETRY: ApiRetryOptions = {
+ maxRetries: 1,
+ baseDelayMs: 240,
+ maxDelayMs: 640,
+ retryUnsafeMethods: true,
+};
+
+export type PlatformProfileRequestOptions = {
+ signal?: AbortSignal;
+ retry?: ApiRetryOptions;
+ skipAuth?: boolean;
+ skipRefresh?: boolean;
+ authImpact?: ApiAuthImpact;
+ notifyAuthStateChange?: boolean;
+ clearAuthOnUnauthorized?: boolean;
+};
+
+function requestPlatformProfileJson(
+ path: string,
+ init: RequestInit,
+ fallbackMessage: string,
+ options: PlatformProfileRequestOptions = {},
+) {
+ const method = (init.method ?? 'GET').toUpperCase();
+ const retry =
+ options.retry ??
+ (method === 'GET'
+ ? PLATFORM_PROFILE_READ_RETRY
+ : PLATFORM_PROFILE_WRITE_RETRY);
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
+
+ return requestJson(
+ `${PLATFORM_PROFILE_API_BASE}${normalizedPath}`,
+ {
+ ...init,
+ signal: options.signal,
+ },
+ fallbackMessage,
+ {
+ retry,
+ skipAuth: options.skipAuth,
+ skipRefresh: options.skipRefresh,
+ authImpact: options.authImpact,
+ notifyAuthStateChange: options.notifyAuthStateChange,
+ clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
+ },
+ );
+}
+
+export function getPlatformProfileDashboard(
+ options: PlatformProfileRequestOptions = {},
+) {
+ return requestPlatformProfileJson(
+ '/dashboard',
+ { method: 'GET' },
+ '读取个人看板失败',
+ options,
+ );
+}
+
+export function getPlatformProfileWalletLedger(
+ options: PlatformProfileRequestOptions = {},
+) {
+ return requestPlatformProfileJson(
+ '/wallet-ledger',
+ { method: 'GET' },
+ '读取资产流水失败',
+ options,
+ );
+}
+
+export function listPlatformProfileExternalApiKeys(
+ options: PlatformProfileRequestOptions = {},
+) {
+ return requestPlatformProfileJson(
+ '/api-keys',
+ { method: 'GET' },
+ '读取 API Key 失败',
+ options,
+ );
+}
+
+export function createPlatformProfileExternalApiKey(
+ name: string,
+ options: PlatformProfileRequestOptions = {},
+) {
+ return requestPlatformProfileJson(
+ '/api-keys',
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name }),
+ },
+ '创建 API Key 失败',
+ options,
+ );
+}
+
+export function revokePlatformProfileExternalApiKey(
+ keyId: string,
+ options: PlatformProfileRequestOptions = {},
+) {
+ return requestPlatformProfileJson(
+ `/api-keys/${encodeURIComponent(keyId)}`,
+ { method: 'DELETE' },
+ '撤销 API Key 失败',
+ options,
+ );
+}
+
+export function getPlatformProfileRechargeCenter(
+ options: PlatformProfileRequestOptions = {},
+) {
+ return requestPlatformProfileJson(
+ '/recharge-center',
+ { method: 'GET' },
+ '读取泥点购买信息失败',
+ options,
+ );
+}
+
+export function createPlatformProfileRechargeOrder(
+ productId: string,
+ paymentChannel: string,
+ options: PlatformProfileRequestOptions = {},
+) {
+ return requestPlatformProfileJson(
+ '/recharge/orders',
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ productId, paymentChannel }),
+ },
+ '充值失败',
+ options,
+ );
+}
+
+export function confirmWechatPlatformProfileRechargeOrder(
+ orderId: string,
+ options: PlatformProfileRequestOptions = {},
+) {
+ return requestPlatformProfileJson(
+ `/recharge/orders/${encodeURIComponent(orderId)}/wechat/confirm`,
+ { method: 'POST' },
+ '确认微信支付订单失败',
+ options,
+ );
+}
+
+type PlatformProfileRechargeOrderSseEvent =
+ | {
+ type: 'order';
+ payload: ConfirmWechatProfileRechargeOrderResponse;
+ }
+ | {
+ type: 'done';
+ payload: { orderId: string; status: string };
+ }
+ | {
+ type: 'error';
+ payload: { message: string };
+ };
+
+function normalizePlatformProfileRechargeOrderSseEvent(
+ eventName: string,
+ parsed: Record,
+): PlatformProfileRechargeOrderSseEvent | null {
+ if (eventName === 'order' && parsed.order && parsed.center) {
+ return {
+ type: 'order',
+ payload: parsed as ConfirmWechatProfileRechargeOrderResponse,
+ };
+ }
+
+ if (eventName === 'done') {
+ const orderId =
+ typeof parsed.orderId === 'string' ? parsed.orderId.trim() : '';
+ const status =
+ typeof parsed.status === 'string' ? parsed.status.trim() : '';
+ if (orderId && status) {
+ return {
+ type: 'done',
+ payload: { orderId, status },
+ };
+ }
+ }
+
+ if (eventName === 'error') {
+ const message =
+ typeof parsed.message === 'string' && parsed.message.trim()
+ ? parsed.message.trim()
+ : '';
+ return {
+ type: 'error',
+ payload: { message },
+ };
+ }
+
+ return null;
+}
+
+function isPlatformProfileRechargeOrderTerminal(
+ order: Pick,
+) {
+ if (order.status === 'pending') {
+ return false;
+ }
+ if (order.status === 'expired' && !order.expirationCheckedAt) {
+ return false;
+ }
+ return true;
+}
+
+export async function watchWechatPlatformProfileRechargeOrder(
+ orderId: string,
+ options: PlatformProfileRequestOptions = {},
+): Promise {
+ const response = await fetchWithApiAuth(
+ `${PLATFORM_PROFILE_API_BASE}/recharge/orders/${encodeURIComponent(orderId)}/wechat/events`,
+ {
+ method: 'GET',
+ headers: { Accept: 'text/event-stream' },
+ signal: options.signal,
+ },
+ {
+ skipRefresh: options.skipRefresh,
+ skipAuth: options.skipAuth,
+ authImpact: options.authImpact,
+ notifyAuthStateChange: options.notifyAuthStateChange,
+ clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
+ },
+ );
+
+ if (!response.ok) {
+ const responseText = await response.text();
+ throw new Error(
+ appendApiErrorRequestId(
+ parseApiErrorMessage(responseText, '订阅充值订单状态失败'),
+ response.headers.get('x-request-id'),
+ ),
+ );
+ }
+
+ if (!response.body) {
+ throw new Error('streaming response body is unavailable');
+ }
+
+ let finalResponse: ConfirmWechatProfileRechargeOrderResponse | null = null;
+ let lastResponse: ConfirmWechatProfileRechargeOrderResponse | null = null;
+
+ await readSseJsonStream(response, ({ eventName, parsed }) => {
+ const normalized = normalizePlatformProfileRechargeOrderSseEvent(
+ eventName,
+ parsed,
+ );
+ if (!normalized) {
+ return;
+ }
+
+ if (normalized.type === 'order') {
+ lastResponse = normalized.payload;
+ if (isPlatformProfileRechargeOrderTerminal(normalized.payload.order)) {
+ finalResponse = normalized.payload;
+ return false;
+ }
+ return;
+ }
+
+ if (normalized.type === 'done') {
+ if (
+ !finalResponse &&
+ lastResponse &&
+ isPlatformProfileRechargeOrderTerminal(lastResponse.order)
+ ) {
+ finalResponse = lastResponse;
+ }
+ return false;
+ }
+
+ throw new Error(normalized.payload.message || '订阅充值订单状态失败');
+ });
+
+ if (!finalResponse) {
+ throw new Error('充值订单状态流返回不完整');
+ }
+
+ return finalResponse;
+}
+
+export function getPlatformProfileReferralInviteCenter(
+ options: PlatformProfileRequestOptions = {},
+) {
+ return requestPlatformProfileJson(
+ '/referrals/invite-center',
+ { method: 'GET' },
+ '读取邀请码失败',
+ options,
+ );
+}
+
+export function redeemPlatformProfileReferralInviteCode(
+ inviteCode: string,
+ options: PlatformProfileRequestOptions = {},
+) {
+ return requestPlatformProfileJson(
+ '/referrals/redeem-code',
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ inviteCode }),
+ },
+ '填写邀请码失败',
+ options,
+ );
+}
+
+export function redeemPlatformProfileRewardCode(
+ code: string,
+ options: PlatformProfileRequestOptions = {},
+) {
+ return requestPlatformProfileJson(
+ '/redeem-codes/redeem',
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ code }),
+ },
+ '兑换失败',
+ options,
+ );
+}
diff --git a/src/services/platform-entry/platformSettingsClient.ts b/src/services/platform-entry/platformSettingsClient.ts
new file mode 100644
index 000000000..49979d7a6
--- /dev/null
+++ b/src/services/platform-entry/platformSettingsClient.ts
@@ -0,0 +1,59 @@
+import type { RuntimeSettings } from '../../../packages/shared/src/contracts/runtime';
+import {
+ type ApiRequestOptions,
+ type ApiRetryOptions,
+ requestJson,
+} from '../apiClient';
+
+const PLATFORM_SETTINGS_API_PATH = '/api/runtime/settings';
+const PLATFORM_SETTINGS_READ_RETRY: ApiRetryOptions = {
+ maxRetries: 1,
+ baseDelayMs: 180,
+ maxDelayMs: 480,
+};
+const PLATFORM_SETTINGS_WRITE_RETRY: ApiRetryOptions = {
+ maxRetries: 1,
+ baseDelayMs: 240,
+ maxDelayMs: 640,
+ retryUnsafeMethods: true,
+};
+
+type PlatformSettingsRequestOptions = ApiRequestOptions & {
+ signal?: AbortSignal;
+};
+
+export function getPlatformRuntimeSettings(
+ options: PlatformSettingsRequestOptions = {},
+) {
+ const { signal, ...requestOptions } = options;
+ return requestJson(
+ PLATFORM_SETTINGS_API_PATH,
+ { method: 'GET', signal },
+ '读取设置失败',
+ {
+ ...requestOptions,
+ retry: requestOptions.retry ?? PLATFORM_SETTINGS_READ_RETRY,
+ },
+ );
+}
+
+export function putPlatformRuntimeSettings(
+ settings: RuntimeSettings,
+ options: PlatformSettingsRequestOptions = {},
+) {
+ const { signal, ...requestOptions } = options;
+ return requestJson(
+ PLATFORM_SETTINGS_API_PATH,
+ {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(settings),
+ signal,
+ },
+ '保存设置失败',
+ {
+ ...requestOptions,
+ retry: requestOptions.retry ?? PLATFORM_SETTINGS_WRITE_RETRY,
+ },
+ );
+}
diff --git a/vite.config.ts b/vite.config.ts
index 9fb329b3a..b05da0b06 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -9,6 +9,87 @@ import { defineConfig, loadEnv, type Plugin } from 'vite';
const RETIRED_TEMPLATE_CSS_PATTERN =
/(?:baby-object|bark-battle|big-fish|child-motion|creation-(?:agent|work)|creative-agent|custom-world|jump-hop|match3d|platform-recommend|platform-work-detail|public-work|puzzle|rpg|square-hole|unified-creation|visual-novel|wooden-fish)/iu;
+const RETIRED_COMPONENT_MODULE_DIRECTORIES = new Set([
+ 'asset-studio',
+ 'bark-battle-creation',
+ 'big-fish-creation',
+ 'big-fish-result',
+ 'big-fish-runtime',
+ 'child-motion-demo',
+ 'creation-agent',
+ 'creative-agent',
+ 'custom-world-agent',
+ 'custom-world-home',
+ 'edutainment-creation',
+ 'edutainment-result',
+ 'edutainment-runtime',
+ 'game-canvas',
+ 'jump-hop-result',
+ 'jump-hop-runtime',
+ 'match3d-result',
+ 'match3d-runtime',
+ 'puzzle-clear-creation',
+ 'puzzle-clear-result',
+ 'puzzle-clear-runtime',
+ 'puzzle-gallery',
+ 'puzzle-result',
+ 'puzzle-runtime',
+ 'rpg-creation-asset-studio',
+ 'rpg-creation-editor',
+ 'rpg-creation-result',
+ 'rpg-entry',
+ 'rpg-runtime-panels',
+ 'rpg-runtime-shell',
+ 'square-hole-creation',
+ 'square-hole-result',
+ 'square-hole-runtime',
+ 'unified-creation',
+ 'visual-novel-creation',
+ 'visual-novel-result',
+ 'visual-novel-runtime',
+ 'wooden-fish-result',
+ 'wooden-fish-runtime',
+]);
+
+const RETIRED_SERVICE_MODULE_DIRECTORIES = new Set([
+ 'bark-battle-creation',
+ 'bark-battle-runtime',
+ 'big-fish-creation',
+ 'big-fish-gallery',
+ 'big-fish-runtime',
+ 'big-fish-works',
+ 'child-motion-demo',
+ 'creation-agent',
+ 'creation-audio',
+ 'creative-agent',
+ 'edutainment-baby-drawing',
+ 'edutainment-baby-object',
+ 'jump-hop',
+ 'match3d-creation',
+ 'match3d-runtime',
+ 'match3d-works',
+ 'puzzle-agent',
+ 'puzzle-clear',
+ 'puzzle-gallery',
+ 'puzzle-onboarding',
+ 'puzzle-runtime',
+ 'puzzle-works',
+ 'rpg-creation',
+ 'rpg-entry',
+ 'rpg-runtime',
+ 'square-hole-creation',
+ 'square-hole-runtime',
+ 'square-hole-works',
+ 'storyEngine',
+ 'visual-novel-creation',
+ 'visual-novel-runtime',
+ 'visual-novel-works',
+ 'wooden-fish',
+]);
+
+const RETIRED_PLATFORM_ENTRY_MODULE_PATTERN =
+ /\/(?:Platform(?:Draft|EntryCreation|EntryFlowShellImpl|EntryHome|EntryWorld|Error|MobileHome|Profile(?:Generation|Played|Qr|Task)|Task|Work)|barkBattle|platform(?:Creation|Dialog|Draft|Edutainment|EntryCreation|External|Generation|Host|MiniGame|Played|Public|Puzzle|Recommend|Rpg|Selection)|puzzleDraft|usePlatform(?:Creation|Entry))[^/]*\.(?:ts|tsx)$/u;
+
const ACTIVE_TAILWIND_SOURCES = `@import 'tailwindcss' source(none);
@source "./active-main.tsx";
@source "./ActiveApp.tsx";
@@ -22,8 +103,11 @@ const ACTIVE_TAILWIND_SOURCES = `@import 'tailwindcss' source(none);
@source "./components/platform-entry/PlatformEntryActiveFlowShell.tsx";
@source "./components/platform-entry/PlatformActiveProfileView.tsx";
@source "./components/platform-entry/PlatformProfilePrimitives.tsx";
+@source "./components/platform-entry/PlatformProfileApiKeysModal.tsx";
@source "./components/platform-entry/PlatformProfileModalShell.tsx";
@source "./components/platform-entry/PlatformProfileRechargeModal.tsx";
+@source "./components/platform-entry/PlatformProfileReferralModal.tsx";
+@source "./components/platform-entry/PlatformProfileRewardCodeRedeemModal.tsx";
@source "./components/platform-entry/PlatformProfileWalletLedgerModal.tsx";
@source "./components/platform-entry/PlatformRechargePaymentStatusDialogs.tsx";
@source "./editor";
@@ -55,6 +139,66 @@ const RETIRED_PUBLIC_ASSET_PATHS = [
'/wooden-fish',
] as const;
+function normalizeViteModuleId(id: string) {
+ return id.split('?', 1)[0]?.replaceAll('\\', '/') ?? id;
+}
+
+function readTopLevelSourceDirectory(
+ normalizedId: string,
+ sourceKind: 'components' | 'services',
+) {
+ const marker = `/src/${sourceKind}/`;
+ const markerIndex = normalizedId.lastIndexOf(marker);
+ if (markerIndex < 0) {
+ return null;
+ }
+ return (
+ normalizedId.slice(markerIndex + marker.length).split('/', 1)[0] ?? null
+ );
+}
+
+function isRetiredFrontendModuleId(id: string) {
+ const normalizedId = normalizeViteModuleId(id);
+ const componentDirectory = readTopLevelSourceDirectory(
+ normalizedId,
+ 'components',
+ );
+ if (
+ componentDirectory &&
+ RETIRED_COMPONENT_MODULE_DIRECTORIES.has(componentDirectory)
+ ) {
+ return true;
+ }
+
+ const serviceDirectory = readTopLevelSourceDirectory(
+ normalizedId,
+ 'services',
+ );
+ if (
+ serviceDirectory &&
+ RETIRED_SERVICE_MODULE_DIRECTORIES.has(serviceDirectory)
+ ) {
+ return true;
+ }
+
+ return RETIRED_PLATFORM_ENTRY_MODULE_PATTERN.test(normalizedId);
+}
+
+function retiredCreationTemplateModulesPlugin(): Plugin {
+ return {
+ name: 'retired-creation-template-modules',
+ enforce: 'pre',
+ transform(_code, id) {
+ if (!isRetiredFrontendModuleId(id)) {
+ return null;
+ }
+ throw new Error(
+ `退役创作模板模块不得进入现役 Vite 依赖图:${normalizeViteModuleId(id)}`,
+ );
+ },
+ };
+}
+
function isRetiredPublicAssetPath(pathname: string) {
return RETIRED_PUBLIC_ASSET_PATHS.some(
(prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`),
@@ -189,6 +333,7 @@ export default defineConfig(({ mode }) => {
'**/src/components/match3d-result/**',
'**/src/components/match3d-runtime/**',
'**/src/components/puzzle-*/**',
+ '**/src/components/rpg-entry/**',
'**/src/components/rpg-creation-*/**',
'**/src/components/rpg-runtime-*/**',
'**/src/components/square-hole-*/**',
@@ -211,7 +356,9 @@ export default defineConfig(({ mode }) => {
'**/src/services/match3dGeneratedModelCache*',
'**/src/services/match3dSpritesheetParser*',
'**/src/services/puzzle-*/**',
+ '**/src/services/rpg-entry/**',
'**/src/services/rpg-creation/**',
+ '**/src/services/rpg-runtime/**',
'**/src/services/square-hole-*/**',
'**/src/services/visual-novel-*/**',
'**/src/services/wooden-fish/**',
@@ -240,6 +387,7 @@ export default defineConfig(({ mode }) => {
root: __dirname,
envDir: __dirname,
plugins: [
+ retiredCreationTemplateModulesPlugin(),
retiredCreationTemplateCssPlugin(),
react(),
tailwindcss(),