合并 master 最新更新
合入 master 的 BgFilter、CI、运维与现役平台改造。 保留 AI 游戏创作 Runtime、独立锁文件与原生壳验证链路。 修复共享充值账单组件、LLM 网关与退役 Agent 兼容边界。 同步冲突文档、锁文件和开发脚本。
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { AuthUser } from '../../../packages/shared/src/contracts/auth';
|
||||
import * as hostBridgeServices from '../../services/host-bridge/hostBridge';
|
||||
import { PlatformActiveProfileView } from './PlatformActiveProfileView';
|
||||
|
||||
const updateAuthProfileMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../../services/authService', () => ({
|
||||
updateAuthProfile: updateAuthProfileMock,
|
||||
}));
|
||||
|
||||
const callbacks = {
|
||||
onLogin: vi.fn(),
|
||||
onOpenApiKeys: vi.fn(),
|
||||
onOpenCommunity: vi.fn(),
|
||||
onOpenFeedback: vi.fn(),
|
||||
onOpenRecharge: vi.fn(),
|
||||
onOpenRewardCode: vi.fn(),
|
||||
onOpenSettings: vi.fn(),
|
||||
onOpenWalletLedger: vi.fn(),
|
||||
onUserUpdated: vi.fn(),
|
||||
};
|
||||
|
||||
const authenticatedUser: AuthUser = {
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
displayName: '测试玩家',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'password' as const,
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
};
|
||||
|
||||
describe('PlatformActiveProfileView', () => {
|
||||
beforeEach(() => {
|
||||
Object.values(callbacks).forEach((callback) => callback.mockReset());
|
||||
updateAuthProfileMock.mockReset();
|
||||
});
|
||||
|
||||
it('shows the login entry for guests', () => {
|
||||
render(
|
||||
<PlatformActiveProfileView
|
||||
{...callbacks}
|
||||
dashboard={null}
|
||||
isLoadingDashboard={false}
|
||||
user={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('main', { name: '我的' })).toBeTruthy();
|
||||
expect(screen.getByText('尚未登录')).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '登录' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders active account, wallet, and settings capabilities', () => {
|
||||
render(
|
||||
<PlatformActiveProfileView
|
||||
{...callbacks}
|
||||
dashboard={{
|
||||
walletBalance: 108,
|
||||
totalPlayTimeMs: 5_400_000,
|
||||
playedWorldCount: 7,
|
||||
updatedAt: '2026-07-18T00:00:00.000Z',
|
||||
}}
|
||||
isLoadingDashboard={false}
|
||||
user={authenticatedUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('测试玩家')).toBeTruthy();
|
||||
expect(screen.getByText(/陶泥号:\s*100001/u)).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '泥点余额 108' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: /泥点充值/u })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: /兑换码/u })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: /玩家社区/u })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: /反馈与建议/u })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: /通用设置/u })).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /开发者 API Key/u }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('edits the nickname through the profile identity action', async () => {
|
||||
updateAuthProfileMock.mockResolvedValueOnce({
|
||||
...authenticatedUser,
|
||||
displayName: '新昵称',
|
||||
});
|
||||
render(
|
||||
<PlatformActiveProfileView
|
||||
{...callbacks}
|
||||
dashboard={null}
|
||||
isLoadingDashboard={false}
|
||||
user={authenticatedUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '修改昵称' }));
|
||||
const input = screen.getByRole('textbox', { name: '新昵称' });
|
||||
fireEvent.change(input, { target: { value: 'a' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
||||
|
||||
expect(screen.getByText('昵称需要 2 到 20 位')).toBeTruthy();
|
||||
expect(updateAuthProfileMock).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.change(input, { target: { value: '新昵称' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateAuthProfileMock).toHaveBeenCalledWith({
|
||||
displayName: '新昵称',
|
||||
});
|
||||
expect(callbacks.onUserUpdated).toHaveBeenCalledWith({
|
||||
...authenticatedUser,
|
||||
displayName: '新昵称',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps avatar validation on the direct camera action', () => {
|
||||
render(
|
||||
<PlatformActiveProfileView
|
||||
{...callbacks}
|
||||
dashboard={null}
|
||||
isLoadingDashboard={false}
|
||||
user={authenticatedUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
const avatarInput = screen
|
||||
.getAllByLabelText('上传头像')
|
||||
.find((element) => element instanceof HTMLInputElement);
|
||||
expect(avatarInput).toBeTruthy();
|
||||
fireEvent.change(avatarInput as HTMLInputElement, {
|
||||
target: {
|
||||
files: [new File(['avatar'], 'avatar.txt', { type: 'text/plain' })],
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByText('头像仅支持 jpg、png、webp')).toBeTruthy();
|
||||
expect(updateAuthProfileMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('imports avatar images through the native HostBridge', async () => {
|
||||
class MockFileReader {
|
||||
result: string | null = null;
|
||||
onload: null | (() => void) = null;
|
||||
onerror: null | (() => void) = null;
|
||||
|
||||
readAsDataURL() {
|
||||
this.result = 'data:image/webp;base64,YXZhdGFy';
|
||||
this.onload?.();
|
||||
}
|
||||
}
|
||||
class MockImage {
|
||||
onload: null | (() => void) = null;
|
||||
onerror: null | (() => void) = null;
|
||||
naturalWidth = 720;
|
||||
naturalHeight = 720;
|
||||
|
||||
set src(_value: string) {
|
||||
this.onload?.();
|
||||
}
|
||||
}
|
||||
vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader);
|
||||
vi.stubGlobal('Image', MockImage as unknown as typeof Image);
|
||||
const inputClickSpy = vi
|
||||
.spyOn(HTMLInputElement.prototype, 'click')
|
||||
.mockImplementation(() => undefined);
|
||||
vi.spyOn(
|
||||
hostBridgeServices,
|
||||
'canUseNativeHostCapability',
|
||||
).mockImplementation((capability) => capability === 'file.importImage');
|
||||
vi.spyOn(hostBridgeServices, 'importHostImageFile').mockResolvedValue({
|
||||
action: 'selected',
|
||||
fileName: 'native-avatar.webp',
|
||||
base64Data: 'YXZhdGFy',
|
||||
mimeType: 'image/webp',
|
||||
bytes: 6,
|
||||
});
|
||||
|
||||
try {
|
||||
render(
|
||||
<PlatformActiveProfileView
|
||||
{...callbacks}
|
||||
dashboard={null}
|
||||
isLoadingDashboard={false}
|
||||
user={authenticatedUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '上传头像' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('dialog', { name: '裁剪头像' })).toBeTruthy();
|
||||
});
|
||||
expect(hostBridgeServices.importHostImageFile).toHaveBeenCalledTimes(1);
|
||||
expect(inputClickSpy).not.toHaveBeenCalled();
|
||||
expect(screen.getByLabelText('头像裁剪操作区')).toBeTruthy();
|
||||
} finally {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,296 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { PlatformEntryFlowShellImpl } from './PlatformEntryActiveFlowShell';
|
||||
|
||||
const authUiMock = vi.hoisted(() => ({
|
||||
value: {
|
||||
user: null,
|
||||
canAccessProtectedData: false,
|
||||
openLoginModal: vi.fn(),
|
||||
openAccountModal: vi.fn(),
|
||||
openSettingsModal: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../auth/AuthUiContext', () => ({
|
||||
useAuthUi: () => authUiMock.value,
|
||||
}));
|
||||
|
||||
vi.mock('../creation-home/CreationLandingView', () => ({
|
||||
CreationLandingView: ({
|
||||
onOpenProject,
|
||||
onOpenProjects,
|
||||
searchKeyword,
|
||||
}: {
|
||||
onOpenProject: (
|
||||
projectId: string,
|
||||
options?: { guide?: boolean; tool?: string },
|
||||
) => void;
|
||||
onOpenProjects: () => void;
|
||||
searchKeyword?: string;
|
||||
}) => (
|
||||
<main aria-label="陶泥儿创作主页" data-search={searchKeyword}>
|
||||
创作主页
|
||||
<button type="button" onClick={onOpenProjects}>
|
||||
查看项目
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onOpenProject('tool-project', { tool: 'background-music' })
|
||||
}
|
||||
>
|
||||
打开音乐工具
|
||||
</button>
|
||||
</main>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../project/ProjectGalleryView', () => ({
|
||||
ProjectGalleryView: ({
|
||||
onOpenProject,
|
||||
searchKeyword,
|
||||
}: {
|
||||
onOpenProject: (projectId: string, options?: { guide?: boolean }) => void;
|
||||
searchKeyword?: string;
|
||||
}) => (
|
||||
<main aria-label="项目" data-search={searchKeyword}>
|
||||
项目
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenProject('guide-project', { guide: true })}
|
||||
>
|
||||
打开引导项目
|
||||
</button>
|
||||
</main>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../image-editor/ImageCanvasEditorView', () => ({
|
||||
ImageCanvasEditorView: () => <main aria-label="图片画布编辑器" />,
|
||||
}));
|
||||
|
||||
vi.mock('../../services/platform-entry/platformProfileClient', () => ({
|
||||
getPlatformProfileDashboard: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./usePlatformProfileCenterController', () => ({
|
||||
usePlatformProfileCenterController: () => ({
|
||||
buyRechargeProduct: vi.fn(),
|
||||
closeNativeWechatPayment: vi.fn(),
|
||||
confirmNativeWechatPayment: vi.fn(),
|
||||
isLoadingRechargeCenter: false,
|
||||
isLoadingWalletLedger: false,
|
||||
isRechargeOpen: false,
|
||||
isWalletLedgerOpen: false,
|
||||
loadRechargeCenter: vi.fn(),
|
||||
nativeWechatPayment: null,
|
||||
openWalletLedgerPanel: vi.fn(),
|
||||
rechargeCenter: null,
|
||||
rechargeError: null,
|
||||
rechargePaymentResult: null,
|
||||
setIsRechargeOpen: vi.fn(),
|
||||
setIsWalletLedgerOpen: vi.fn(),
|
||||
setRechargePaymentResult: vi.fn(),
|
||||
submittingRechargeProductId: null,
|
||||
walletLedger: null,
|
||||
walletLedgerError: null,
|
||||
wechatRechargeOrderConfirmationState: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('PlatformEntryActiveFlowShell', () => {
|
||||
beforeEach(() => {
|
||||
window.history.replaceState(null, '', '/creation');
|
||||
authUiMock.value.openLoginModal.mockReset();
|
||||
});
|
||||
|
||||
it('keeps the active desktop rail and the shared account capsule', async () => {
|
||||
const setSelectionStage = vi.fn();
|
||||
const { container, rerender } = render(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="creation-home"
|
||||
setSelectionStage={setSelectionStage}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByRole('main', { name: '陶泥儿创作主页' }),
|
||||
).toBeTruthy();
|
||||
const navigation = screen.getByRole('navigation', { name: '平台导航' });
|
||||
expect(
|
||||
within(navigation)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.getAttribute('aria-label')),
|
||||
).toEqual(['创作', '项目', '我的']);
|
||||
expect(
|
||||
within(navigation)
|
||||
.getByRole('button', { name: '创作' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('page');
|
||||
|
||||
const topbar = container.querySelector('.platform-desktop-topbar');
|
||||
expect(topbar).toBeTruthy();
|
||||
expect(
|
||||
topbar?.querySelectorAll('button.platform-desktop-search'),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
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(window.location.pathname).toBe('/profile');
|
||||
expect(setSelectionStage).toHaveBeenCalledWith('profile', {
|
||||
path: '/profile',
|
||||
});
|
||||
|
||||
rerender(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="profile"
|
||||
setSelectionStage={setSelectionStage}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('main', { name: '我的' })).toBeTruthy();
|
||||
expect(
|
||||
within(navigation)
|
||||
.getByRole('button', { name: '我的' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('page');
|
||||
});
|
||||
|
||||
it('keeps creation, project, and profile reachable from the mobile dock', async () => {
|
||||
const setSelectionStage = vi.fn();
|
||||
render(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="project"
|
||||
setSelectionStage={setSelectionStage}
|
||||
/>,
|
||||
);
|
||||
|
||||
const navigation = await screen.findByRole('navigation', {
|
||||
name: '移动平台导航',
|
||||
});
|
||||
expect(
|
||||
within(navigation)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.getAttribute('aria-label')),
|
||||
).toEqual(['创作', '项目', '我的']);
|
||||
expect(
|
||||
within(navigation)
|
||||
.getByRole('button', { name: '项目' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('page');
|
||||
|
||||
fireEvent.click(within(navigation).getByRole('button', { name: '我的' }));
|
||||
expect(window.location.pathname).toBe('/profile');
|
||||
expect(setSelectionStage).toHaveBeenCalledWith('profile', {
|
||||
path: '/profile',
|
||||
});
|
||||
});
|
||||
|
||||
it('switches between creation and projects and passes search to the active page', async () => {
|
||||
const setSelectionStage = vi.fn();
|
||||
const { rerender } = render(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="creation-home"
|
||||
setSelectionStage={setSelectionStage}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="project"
|
||||
setSelectionStage={setSelectionStage}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="project"
|
||||
setSelectionStage={setSelectionStage}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="creation-home"
|
||||
setSelectionStage={setSelectionStage}
|
||||
/>,
|
||||
);
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,14 @@
|
||||
import { PlatformEntryFlowShellImpl } from './PlatformEntryFlowShellImpl';
|
||||
import { PlatformEntryFlowShellImpl } from './PlatformEntryActiveFlowShell';
|
||||
import type {
|
||||
PlatformEntryFlowShellProps,
|
||||
SelectionStage,
|
||||
} from './platformEntryTypes';
|
||||
} from './platformEntryActiveTypes';
|
||||
|
||||
export type { PlatformEntryFlowShellProps, SelectionStage };
|
||||
|
||||
/**
|
||||
* 平台入口通用壳层。
|
||||
* RPG、Big Fish 等玩法创作入口在这里并列分流。
|
||||
* 平台运行入口承载新创作主页、项目、编辑器与账户公共能力。
|
||||
*/
|
||||
export function PlatformEntryFlowShell(props: PlatformEntryFlowShellProps) {
|
||||
return <PlatformEntryFlowShellImpl {...props} />;
|
||||
|
||||
@@ -6,10 +6,10 @@ import type {
|
||||
ExternalApiKeyProfile,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import {
|
||||
createRpgProfileExternalApiKey,
|
||||
listRpgProfileExternalApiKeys,
|
||||
revokeRpgProfileExternalApiKey,
|
||||
} from '../../services/rpg-entry/rpgProfileClient';
|
||||
createPlatformProfileExternalApiKey,
|
||||
listPlatformProfileExternalApiKeys,
|
||||
revokePlatformProfileExternalApiKey,
|
||||
} from '../../services/platform-entry/platformProfileClient';
|
||||
import { CopyFeedbackButton } from '../common/CopyFeedbackButton';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel';
|
||||
@@ -20,7 +20,6 @@ import { PlatformProfileSummaryHeader } from '../common/PlatformProfileSummaryHe
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformTextField } from '../common/PlatformTextField';
|
||||
import { useCopyFeedback } from '../common/useCopyFeedback';
|
||||
import { formatPlatformWorldTime } from '../rpg-entry/rpgEntryWorldPresentation';
|
||||
import { PlatformProfileSecondaryModalShell } from './PlatformProfileModalShell';
|
||||
|
||||
type PlatformProfileApiKeysModalProps = {
|
||||
@@ -32,7 +31,17 @@ function isActiveExternalApiKey(key: ExternalApiKeyProfile) {
|
||||
}
|
||||
|
||||
function buildApiKeyTimeLabel(value: string | null) {
|
||||
return value ? formatPlatformWorldTime(value) : '尚未使用';
|
||||
if (!value) {
|
||||
return '尚未使用';
|
||||
}
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
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}`;
|
||||
}
|
||||
|
||||
function buildApiKeyScopeLabel(scopes: string[]) {
|
||||
@@ -56,21 +65,22 @@ export function PlatformProfileApiKeysModal({
|
||||
const [revokingKeyId, setRevokingKeyId] = useState<string | null>(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));
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { AuthUser } from '../../../packages/shared/src/contracts/auth';
|
||||
|
||||
export function resolveActivePublicUserCode(
|
||||
user: AuthUser | null | undefined,
|
||||
) {
|
||||
if (user?.publicUserCode?.trim()) {
|
||||
return user.publicUserCode.trim();
|
||||
}
|
||||
|
||||
const raw =
|
||||
user?.id.replace(/[^a-zA-Z0-9]/gu, '').toUpperCase() || '00000000';
|
||||
return `SY-${raw.slice(-8).padStart(8, '0')}`;
|
||||
}
|
||||
|
||||
export function resolveActiveUserAvatarLabel(
|
||||
user: AuthUser | null | undefined,
|
||||
) {
|
||||
return (user?.displayName || '叙').slice(0, 1).toUpperCase();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export type SelectionStage =
|
||||
| 'platform'
|
||||
| 'creation-home'
|
||||
| 'project'
|
||||
| 'profile'
|
||||
| 'image-editor';
|
||||
|
||||
export type PlatformEntryFlowShellProps = {
|
||||
selectionStage: SelectionStage;
|
||||
setSelectionStage: (
|
||||
stage: SelectionStage,
|
||||
options?: { path?: string },
|
||||
) => void;
|
||||
};
|
||||
@@ -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<ProfileWalletLedgerEntry['sourceType'], string>;
|
||||
|
||||
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}`;
|
||||
}
|
||||
@@ -7,14 +7,10 @@ import {
|
||||
type ProfileRechargeOrder,
|
||||
type ProfileRechargeProduct,
|
||||
type ProfileReferralInviteCenterResponse,
|
||||
type ProfileTaskCenterResponse,
|
||||
type ProfileWalletLedgerResponse,
|
||||
type RedeemProfileRewardCodeResponse,
|
||||
} 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;
|
||||
@@ -106,21 +97,11 @@ type UsePlatformProfileCenterControllerArgs = {
|
||||
activeTab: string;
|
||||
isAuthenticated: boolean;
|
||||
showRechargeEntry: boolean;
|
||||
profileTaskRefreshKey?: number;
|
||||
onRechargeSuccess?: () => void | Promise<void>;
|
||||
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) {
|
||||
@@ -217,7 +198,7 @@ function isWechatRechargeOrderTerminalForConfirmation(
|
||||
async function confirmWechatRechargeOrderUntilSettled(
|
||||
orderId: string,
|
||||
): Promise<ConfirmWechatProfileRechargeOrderResponse> {
|
||||
let latestResponse = await confirmWechatRpgProfileRechargeOrder(orderId);
|
||||
let latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId);
|
||||
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
|
||||
return latestResponse;
|
||||
}
|
||||
@@ -225,14 +206,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;
|
||||
@@ -242,7 +224,7 @@ async function confirmWechatRechargeOrderUntilSettled(
|
||||
async function confirmWechatRechargeOrderQuickly(
|
||||
orderId: string,
|
||||
): Promise<ConfirmWechatProfileRechargeOrderResponse> {
|
||||
let latestResponse = await confirmWechatRpgProfileRechargeOrder(orderId);
|
||||
let latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId);
|
||||
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
|
||||
return latestResponse;
|
||||
}
|
||||
@@ -250,7 +232,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;
|
||||
}
|
||||
@@ -309,7 +291,6 @@ export function usePlatformProfileCenterController({
|
||||
activeTab,
|
||||
isAuthenticated,
|
||||
showRechargeEntry,
|
||||
profileTaskRefreshKey = 0,
|
||||
onRechargeSuccess,
|
||||
requestLogin,
|
||||
currentUser,
|
||||
@@ -345,14 +326,6 @@ export function usePlatformProfileCenterController({
|
||||
null,
|
||||
);
|
||||
const [isLoadingWalletLedger, setIsLoadingWalletLedger] = useState(false);
|
||||
const [isTaskCenterOpen, setIsTaskCenterOpen] = useState(false);
|
||||
const [taskCenter, setTaskCenter] =
|
||||
useState<ProfileTaskCenterResponse | null>(null);
|
||||
const [taskCenterError, setTaskCenterError] = useState<string | null>(null);
|
||||
const [isLoadingTaskCenter, setIsLoadingTaskCenter] = useState(false);
|
||||
const taskCenterRequestIdRef = useRef(0);
|
||||
const [claimingTaskId, setClaimingTaskId] = useState<string | null>(null);
|
||||
const [taskClaimSuccess, setTaskClaimSuccess] = useState<string | null>(null);
|
||||
const [profilePopupPanel, setProfilePopupPanel] =
|
||||
useState<ProfilePopupPanel | null>(null);
|
||||
const [referralCenter, setReferralCenter] =
|
||||
@@ -406,7 +379,7 @@ export function usePlatformProfileCenterController({
|
||||
const loadWalletLedger = useCallback(() => {
|
||||
setWalletLedgerError(null);
|
||||
setIsLoadingWalletLedger(true);
|
||||
void getRpgProfileWalletLedger()
|
||||
void getPlatformProfileWalletLedger()
|
||||
.then(setWalletLedger)
|
||||
.catch((error: unknown) => {
|
||||
setWalletLedger(null);
|
||||
@@ -436,7 +409,7 @@ export function usePlatformProfileCenterController({
|
||||
const revision = ++rechargeCenterReadRevisionRef.current;
|
||||
setRechargeError(null);
|
||||
setIsLoadingRechargeCenter(true);
|
||||
void getRpgProfileRechargeCenter()
|
||||
void getPlatformProfileRechargeCenter()
|
||||
.then((center) => {
|
||||
if (revision === rechargeCenterReadRevisionRef.current) {
|
||||
setRechargeCenter(center);
|
||||
@@ -639,7 +612,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;
|
||||
@@ -851,9 +824,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 ||
|
||||
@@ -970,90 +946,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);
|
||||
@@ -1133,7 +1029,7 @@ export function usePlatformProfileCenterController({
|
||||
setIsSubmittingReferralRedeem(true);
|
||||
setReferralError(null);
|
||||
setReferralSuccess(null);
|
||||
void redeemRpgProfileReferralInviteCode(inviteCode)
|
||||
void redeemPlatformProfileReferralInviteCode(inviteCode)
|
||||
.then((response) => {
|
||||
setReferralCenter(response.center);
|
||||
setReferralRedeemCode('');
|
||||
@@ -1156,7 +1052,7 @@ export function usePlatformProfileCenterController({
|
||||
setIsSubmittingRewardCode(true);
|
||||
setRewardCodeError(null);
|
||||
setRewardCodeSuccess(null);
|
||||
void redeemRpgProfileRewardCode(rewardCodeInput)
|
||||
void redeemPlatformProfileRewardCode(rewardCodeInput)
|
||||
.then((response: RedeemProfileRewardCodeResponse) => {
|
||||
setRewardCodeInput('');
|
||||
setRewardCodeSuccess(`已到账 ${response.amountGranted} 泥点`);
|
||||
@@ -1168,57 +1064,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,
|
||||
@@ -1233,7 +1098,6 @@ export function usePlatformProfileCenterController({
|
||||
rewardCodeSuccess,
|
||||
setIsRechargeOpen,
|
||||
setIsRewardCodeOpen,
|
||||
setIsTaskCenterOpen,
|
||||
setIsWalletLedgerOpen,
|
||||
setRechargePaymentResult,
|
||||
setReferralRedeemCode,
|
||||
@@ -1242,9 +1106,6 @@ export function usePlatformProfileCenterController({
|
||||
submittingRechargeProductId,
|
||||
submitReferralRedeemCode,
|
||||
submitRewardCode,
|
||||
taskCenter,
|
||||
taskCenterError,
|
||||
taskClaimSuccess,
|
||||
walletLedger,
|
||||
walletLedgerError,
|
||||
wechatRechargeOrderConfirmationState,
|
||||
|
||||
Reference in New Issue
Block a user