diff --git a/src/components/image-editor/ImageCanvasEditorView.test.tsx b/src/components/image-editor/ImageCanvasEditorView.test.tsx index 0d10fcf7f..8b8ee140c 100644 --- a/src/components/image-editor/ImageCanvasEditorView.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.test.tsx @@ -662,7 +662,7 @@ describe('ImageCanvasEditorView', () => { , ); - expect(await screen.findByLabelText('泥点 1,234')).toBeTruthy(); + expect(await screen.findByLabelText(/可用总额 1,234/)).toBeTruthy(); expect(getPlatformProfileDashboardMock).not.toHaveBeenCalled(); }); @@ -729,12 +729,12 @@ describe('ImageCanvasEditorView', () => { ); const walletButton = await screen.findByRole('button', { - name: '泥点 --', + name: /可用总额 --/, }); await user.hover(walletButton); const details = await screen.findByRole('dialog', { - name: '泥点账户详情', + name: '会员与泥点账户详情', }); expect(within(details).getByText('泥点明细读取失败')).toBeTruthy(); expect(within(details).queryByText('充值中心响应缺少泥点余额')).toBeNull(); @@ -764,7 +764,7 @@ describe('ImageCanvasEditorView', () => { ); expect( - (await screen.findByRole('button', { name: '泥点 --' })).getAttribute( + (await screen.findByRole('button', { name: /可用总额 --/ })).getAttribute( 'aria-busy', ), ).toBe('true'); @@ -807,13 +807,13 @@ describe('ImageCanvasEditorView', () => { ); const walletButton = await screen.findByRole('button', { - name: '泥点 1,234', + name: /可用总额 1,234/, }); await user.hover(walletButton); const details = await screen.findByRole('dialog', { - name: '泥点账户详情', + name: '会员与泥点账户详情', }); expect(within(details).getByText('永久泥点')).toBeTruthy(); expect(within(details).getByText('月度泥点')).toBeTruthy(); @@ -911,11 +911,11 @@ describe('ImageCanvasEditorView', () => { const viewport = screen.getByLabelText('画布工作区'); fireEvent.click( await screen.findByRole('button', { - name: '充值', + name: '会员与泥点', }), ); expect( - await screen.findByRole('dialog', { name: '购买更多泥点' }), + await screen.findByRole('dialog', { name: '会员与泥点' }), ).toBeTruthy(); fireEvent.keyDown(window, { key: 'Delete', code: 'Delete' }); @@ -941,7 +941,7 @@ describe('ImageCanvasEditorView', () => { ).toBe('true'); expect(viewport.getAttribute('aria-disabled')).toBe('true'); - fireEvent.click(screen.getByRole('button', { name: '关闭购买更多泥点' })); + fireEvent.click(screen.getByRole('button', { name: '关闭会员与泥点' })); const resumedToolbar = await screen.findByRole('toolbar', { name: '快速编辑框选工具', diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx index 8aca4173e..e417b1dee 100644 --- a/src/components/image-editor/ImageCanvasEditorView.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.tsx @@ -561,6 +561,7 @@ export function ImageCanvasEditorView({ buyRechargeProduct, closeNativeWechatPayment, confirmNativeWechatPayment, + isLoadingMembershipQuote, isLoadingRechargeCenter, isLoadingWalletLedger, isRechargeOpen, @@ -569,12 +570,15 @@ export function ImageCanvasEditorView({ isWalletLedgerOpen, loadRechargeCenter, loadWalletLedger, + membershipQuote, + membershipQuoteError, nativeWechatPayment, openRechargeOrRewardCodeModal, openWalletLedgerPanel, rechargeCenter, rechargeError, rechargePaymentResult, + requestMembershipQuote, rewardCodeError, rewardCodeInput, rewardCodeSuccess, @@ -584,6 +588,7 @@ export function ImageCanvasEditorView({ setRechargePaymentResult, setRewardCodeInput, submittingRechargeProductId, + submitMembershipCheckout, submitRewardCode, walletLedger, walletLedgerError, @@ -2746,9 +2751,14 @@ export function ImageCanvasEditorView({ error={rechargeError} submittingProductId={submittingRechargeProductId} nativePayment={nativeWechatPayment} + membershipQuote={membershipQuote} + isLoadingMembershipQuote={isLoadingMembershipQuote} + membershipQuoteError={membershipQuoteError} onClose={() => setIsRechargeOpen(false)} onRetry={loadRechargeCenter} onBuy={buyRechargeProduct} + onRequestMembershipQuote={requestMembershipQuote} + onSubmitMembershipCheckout={submitMembershipCheckout} onConfirmNativePayment={confirmNativeWechatPayment} onCloseNativePayment={closeNativeWechatPayment} /> diff --git a/src/components/image-editor/ImageCanvasTopbarView.test.tsx b/src/components/image-editor/ImageCanvasTopbarView.test.tsx index 4a4c511df..da67d9064 100644 --- a/src/components/image-editor/ImageCanvasTopbarView.test.tsx +++ b/src/components/image-editor/ImageCanvasTopbarView.test.tsx @@ -107,13 +107,15 @@ describe('ImageCanvasTopbarView', () => { walletBalance: 12_345, }); const walletChip = screen.getByRole('button', { - name: '泥点 12,345', + name: /可用总额 12,345/, }); - expect(walletChip.textContent).toBe('泥点 1.2万'); + expect(walletChip.textContent).toContain('可用总额 1.2万'); await user.hover(walletChip); - const details = screen.getByRole('dialog', { name: '泥点账户详情' }); + const details = screen.getByRole('dialog', { + name: '会员与泥点账户详情', + }); expect(within(details).getByText('永久泥点')).toBeTruthy(); expect(within(details).getByText('月度泥点')).toBeTruthy(); expect(within(details).getByText('每日免费泥点')).toBeTruthy(); @@ -121,7 +123,7 @@ describe('ImageCanvasTopbarView', () => { await user.click(within(details).getByRole('button', { name: '使用详情' })); expect(props.onOpenWalletLedger).toHaveBeenCalledTimes(1); - await user.click(screen.getByRole('button', { name: '充值' })); + await user.click(screen.getByRole('button', { name: '会员与泥点' })); expect(props.onRecharge).toHaveBeenCalledTimes(1); expect(props.onRequestWalletDetails).toHaveBeenCalledTimes(1); }); diff --git a/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx b/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx index e71e02398..f92b919eb 100644 --- a/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx +++ b/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx @@ -268,7 +268,7 @@ describe('PlatformEntryActiveFlowShell', () => { />, ); - expect(await screen.findByLabelText('泥点 207')).toBeTruthy(); + expect(await screen.findByLabelText(/可用总额 207/)).toBeTruthy(); expect(screen.getByRole('button', { name: '下载客户端' })).toBeTruthy(); rerender( @@ -363,7 +363,7 @@ describe('PlatformEntryActiveFlowShell', () => { setSelectionStage={vi.fn()} />, ); - await screen.findByLabelText('泥点 66'); + await screen.findByLabelText(/可用总额 66/); let resolveNextOwner!: (value: unknown) => void; profileClientMock.getPlatformProfileRechargeCenter.mockImplementation( @@ -380,7 +380,7 @@ describe('PlatformEntryActiveFlowShell', () => { />, ); - expect(screen.queryByLabelText('泥点 66')).toBeNull(); + expect(screen.queryByLabelText(/可用总额 66/)).toBeNull(); await waitFor(() => { expect(usePlatformWalletStore.getState()).toMatchObject({ ownerUserId: 'user-2', @@ -397,7 +397,7 @@ describe('PlatformEntryActiveFlowShell', () => { dailyFreeResetsAt: '2026-08-04T00:00:00+08:00', }, }); - await screen.findByLabelText('泥点 67'); + await screen.findByLabelText(/可用总额 67/); }); it('coalesces visibility and focus refreshes when the page returns to the foreground', async () => { @@ -429,7 +429,7 @@ describe('PlatformEntryActiveFlowShell', () => { setSelectionStage={vi.fn()} />, ); - await screen.findByLabelText('泥点 10'); + await screen.findByLabelText(/可用总额 10/); await act(async () => { document.dispatchEvent(new Event('visibilitychange')); diff --git a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx index a2af89877..f83811020 100644 --- a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx +++ b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx @@ -872,9 +872,14 @@ export function PlatformEntryFlowShellImpl({ error={profileCenter.rechargeError} submittingProductId={profileCenter.submittingRechargeProductId} nativePayment={profileCenter.nativeWechatPayment} + membershipQuote={profileCenter.membershipQuote} + isLoadingMembershipQuote={profileCenter.isLoadingMembershipQuote} + membershipQuoteError={profileCenter.membershipQuoteError} onClose={() => profileCenter.setIsRechargeOpen(false)} onRetry={profileCenter.loadRechargeCenter} onBuy={profileCenter.buyRechargeProduct} + onRequestMembershipQuote={profileCenter.requestMembershipQuote} + onSubmitMembershipCheckout={profileCenter.submitMembershipCheckout} onConfirmNativePayment={profileCenter.confirmNativeWechatPayment} onCloseNativePayment={profileCenter.closeNativeWechatPayment} /> diff --git a/src/components/platform-entry/usePlatformProfileCenterController.ts b/src/components/platform-entry/usePlatformProfileCenterController.ts index eb27c29bb..5ae54d6f8 100644 --- a/src/components/platform-entry/usePlatformProfileCenterController.ts +++ b/src/components/platform-entry/usePlatformProfileCenterController.ts @@ -1,8 +1,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import type { PlatformProfileRechargeNativePaymentState } from '../../../packages/shared/src/components/PlatformProfileRechargeModal'; +import { type PlatformProfileRechargeNativePaymentState } from '../../../packages/shared/src/components/PlatformProfileRechargeModal'; +import { resolveMembershipProductId } from '../../../packages/shared/src/components/PlatformProfileRechargeModal/membershipProduct'; import { type ConfirmWechatProfileRechargeOrderResponse, + type ProfileMembershipCycleKind, + type ProfileMembershipPlan, + type ProfileMembershipUpgradeQuote, type ProfileRechargeCenterResponse, type ProfileRechargeOrder, type ProfileRechargeProduct, @@ -29,6 +33,7 @@ import { requestWechatJsapiPayment } from '../../services/payment/wechatJsapiPay import { confirmWechatPlatformProfileRechargeOrder, createPlatformProfileRechargeOrder, + getPlatformProfileMembershipUpgradeQuote, getPlatformProfileRechargeCenter, getPlatformProfileReferralInviteCenter, getPlatformProfileWalletLedger, @@ -392,6 +397,13 @@ export function usePlatformProfileCenterController({ useState(null); const [submittingRechargeProductId, setSubmittingRechargeProductId] = useState(null); + const [membershipQuote, setMembershipQuote] = + useState(null); + const [isLoadingMembershipQuote, setIsLoadingMembershipQuote] = + useState(false); + const [membershipQuoteError, setMembershipQuoteError] = useState< + string | null + >(null); const [isWalletLedgerOpen, setIsWalletLedgerOpen] = useState(false); const [walletLedger, setWalletLedger] = useState(null); @@ -1333,6 +1345,65 @@ export function usePlatformProfileCenterController({ ], ); + const requestMembershipQuote = useCallback( + (plan: ProfileMembershipPlan) => { + const account = captureAccountLifecycle(); + const requestSignal = accountWriteAbortControllerRef.current.signal; + if ( + !account.ownerUserId || + !isAccountLifecycleCurrent(account) || + requestSignal.aborted + ) { + return; + } + setMembershipQuote(null); + setMembershipQuoteError(null); + setIsLoadingMembershipQuote(true); + void getPlatformProfileMembershipUpgradeQuote(plan, { + signal: requestSignal, + }) + .then((quote) => { + if (!isAccountLifecycleCurrent(account)) { + return; + } + setMembershipQuote(quote); + }) + .catch((error: unknown) => { + if (!isAccountLifecycleCurrent(account)) { + return; + } + setMembershipQuoteError( + error instanceof Error ? error.message : '读取升级报价失败', + ); + }) + .finally(() => { + if (isAccountLifecycleCurrent(account)) { + setIsLoadingMembershipQuote(false); + } + }); + }, + [captureAccountLifecycle, isAccountLifecycleCurrent], + ); + + const submitMembershipCheckout = useCallback( + (plan: ProfileMembershipPlan, cycleKind: ProfileMembershipCycleKind) => { + // 会员商品没有商品配置行:标题与价格由后端按档位与周期现算, + // 这里只需要把 productId 交给同一条下单链路。 + buyRechargeProduct({ + productId: resolveMembershipProductId(plan, cycleKind), + title: plan, + priceCents: 0, + kind: 'membership', + pointsAmount: 0, + bonusPoints: 0, + durationDays: 0, + badgeLabel: '', + description: '', + }); + }, + [buyRechargeProduct], + ); + const confirmNativeWechatPayment = useCallback(() => { if (!nativeWechatPayment || nativeWechatPayment.isConfirming) { return; @@ -1872,6 +1943,11 @@ export function usePlatformProfileCenterController({ closeNativeWechatPayment, closeProfilePopupPanel, confirmNativeWechatPayment, + isLoadingMembershipQuote, + membershipQuote, + membershipQuoteError, + requestMembershipQuote, + submitMembershipCheckout, copyInviteCode, inviteCodeCopyState: inviteCodeCopyState as CopyFeedbackState, inviteLinkCopyState: inviteLinkCopyState as CopyFeedbackState, diff --git a/src/services/platform-entry/platformProfileClient.ts b/src/services/platform-entry/platformProfileClient.ts index 7164a8118..f8a0aec1c 100644 --- a/src/services/platform-entry/platformProfileClient.ts +++ b/src/services/platform-entry/platformProfileClient.ts @@ -5,6 +5,8 @@ import type { ExternalApiKeyListResponse, ExternalApiKeyMutationResponse, ProfileDashboardSummary, + ProfileMembershipPlan, + ProfileMembershipUpgradeQuote, ProfileRechargeCenterResponse, ProfileRechargeOrder, ProfileReferralInviteCenterResponse, @@ -168,6 +170,22 @@ export function createPlatformProfileRechargeOrder( ); } +export function getPlatformProfileMembershipUpgradeQuote( + targetPlan: ProfileMembershipPlan, + options: PlatformProfileRequestOptions = {}, +) { + return requestPlatformProfileJson( + '/membership/upgrade-quote', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ targetPlan }), + }, + '读取升级报价失败', + options, + ); +} + export function confirmWechatPlatformProfileRechargeOrder( orderId: string, options: PlatformProfileRequestOptions = {},