Files
Genarrative/src/components/platform-entry/PlatformProfileRechargeModal.tsx
T
kdletters ffcffef6d2 继续收口工具弹窗与分段切换预设
新增 PlatformToolModalShell 承接白底工具弹窗壳层和固定可访问名称

新增 PlatformSegmentedTabPresets 沉淀频道下划线、创作 pill rail 与二列 option segment

迁移拼图、抓大鹅、历史素材弹窗和首页 / 作品架 / 充值切换的重复组件写法

同步 PlatformUiKit 文档和 Hermes 决策记录
2026-06-11 16:32:56 +08:00

271 lines
8.1 KiB
TypeScript

import QRCode from 'qrcode';
import { useEffect, useState } from 'react';
import type {
ProfileRechargeCenterResponse,
ProfileRechargeProduct,
} from '../../../packages/shared/src/contracts/runtime';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformProfileSkeletonList } from '../common/PlatformProfileSkeletonList';
import { PlatformOptionSegment } from '../common/PlatformSegmentedTabPresets';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import { PlatformProfileModalShell } from './PlatformProfileModalShell';
import type {
NativeWechatPaymentState,
RechargeTab,
} from './usePlatformProfileCenterController';
import { buildMembershipLabel } from '../rpg-entry/rpgEntryProfileFundsViewModel';
import { formatSnapshotTime } from '../rpg-entry/rpgEntryProfileDashboardPresentation';
import {
buildRechargeProductValueLabel,
formatRechargePrice,
} from '../rpg-entry/rpgEntryProfileFundsViewModel';
const WECHAT_NATIVE_PAY_QR_IMAGE_SIZE = 180;
const RECHARGE_TAB_ITEMS: Array<{ id: RechargeTab; label: string }> = [
{ id: 'points', label: '泥点充值' },
{ id: 'membership', label: '会员卡' },
];
export type PlatformProfileRechargeModalProps = {
center: ProfileRechargeCenterResponse | null;
isLoading: boolean;
error: string | null;
submittingProductId: string | null;
nativePayment: NativeWechatPaymentState | null;
activeTab: RechargeTab;
onTabChange: (tab: RechargeTab) => void;
onClose: () => void;
onRetry: () => void;
onBuy: (product: ProfileRechargeProduct) => void;
onConfirmNativePayment: () => void;
};
/**
* 生成微信 Native 支付二维码图片,保持首页现有二维码尺寸与容错行为。
*/
function useWechatNativeQrCode(codeUrl: string | null) {
const [qrImageUrl, setQrImageUrl] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setQrImageUrl(null);
if (!codeUrl) {
return () => {
cancelled = true;
};
}
void QRCode.toDataURL(codeUrl, {
errorCorrectionLevel: 'M',
margin: 1,
width: WECHAT_NATIVE_PAY_QR_IMAGE_SIZE,
}).then((dataUrl) => {
if (!cancelled) {
setQrImageUrl(dataUrl);
}
});
return () => {
cancelled = true;
};
}, [codeUrl]);
return qrImageUrl;
}
/**
* 充值套餐卡片,沿用 RPG 首页当前视觉和交互语义。
*/
function RechargeProductCard({
product,
submittingProductId,
onBuy,
}: {
product: ProfileRechargeProduct;
submittingProductId: string | null;
onBuy: (product: ProfileRechargeProduct) => void;
}) {
const submitting = submittingProductId === product.productId;
const badgeLabel = product.badgeLabel;
const value = buildRechargeProductValueLabel(product);
return (
<PlatformSubpanel
as="button"
type="button"
surface="platform"
onClick={() => onBuy(product)}
disabled={Boolean(submittingProductId)}
interactive
radius="sm"
padding="none"
className="platform-interactive-card relative min-h-[7.25rem] px-3.5 py-3.5 text-left"
>
{badgeLabel ? (
<PlatformPillBadge
tone="warning"
size="xxs"
className="absolute right-3 top-3 max-w-[7rem] truncate px-2 py-0.5 tracking-[0.18em]"
>
{badgeLabel}
</PlatformPillBadge>
) : null}
<div className="pr-20 text-sm font-black text-[var(--platform-text-strong)]">
{product.title}
</div>
<div className="mt-3 text-2xl font-black text-[var(--platform-text-strong)]">
{value}
</div>
<div className="mt-2 flex items-center justify-between gap-3">
<span className="text-sm font-bold text-[var(--platform-text-soft)]">
{formatRechargePrice(product.priceCents)}
</span>
<span className="platform-primary-button rounded-full px-3 py-1.5 text-xs font-black">
{submitting ? '处理中' : '购买'}
</span>
</div>
</PlatformSubpanel>
);
}
/**
* 个人中心充值弹窗,共享给不同入口复用,但保持现有 props 与文案不变。
*/
export function PlatformProfileRechargeModal({
center,
isLoading,
error,
submittingProductId,
nativePayment,
activeTab,
onTabChange,
onClose,
onRetry,
onBuy,
onConfirmNativePayment,
}: PlatformProfileRechargeModalProps) {
const nativeQrImageUrl = useWechatNativeQrCode(
nativePayment?.codeUrl ?? null,
);
const products =
activeTab === 'points'
? (center?.pointProducts ?? [])
: (center?.membershipProducts ?? []);
const memberLabel = buildMembershipLabel(
center?.membership,
formatSnapshotTime,
);
return (
<PlatformProfileModalShell
title="账户充值"
description={
center ? `${center.walletBalance}泥点 · ${memberLabel}` : '读取中'
}
onClose={onClose}
closeLabel="关闭账户充值"
size="md"
panelClassName="platform-recharge-modal !max-w-[34rem] rounded-[1.4rem]"
bodyClassName="max-h-[min(76vh,36rem)] overflow-y-auto px-5 py-5"
>
<PlatformOptionSegment
items={RECHARGE_TAB_ITEMS}
activeId={activeTab}
onChange={onTabChange}
variant="profile"
ariaLabel="充值类型"
/>
<PlatformAsyncStatePanel
errorState={
error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="xs"
className="mt-4 rounded-2xl font-semibold"
>
<div>{error}</div>
<PlatformActionButton
surface="profile"
size="xs"
className="mt-3"
onClick={onRetry}
>
重新加载
</PlatformActionButton>
</PlatformStatusMessage>
) : null
}
isLoading={isLoading}
loadingState={
<PlatformProfileSkeletonList
count={4}
containerClassName="mt-4 grid gap-3 sm:grid-cols-2"
itemClassName="h-28 rounded-[1.15rem] bg-white/10"
/>
}
isEmpty={products.length === 0}
emptyState={
<PlatformEmptyState
surface="subpanel"
size="inline"
className="mt-4"
>
暂无可购买套餐
</PlatformEmptyState>
}
>
<div className="mt-4 grid gap-3 sm:grid-cols-2">
{products.map((product) => (
<RechargeProductCard
key={product.productId}
product={product}
submittingProductId={submittingProductId}
onBuy={onBuy}
/>
))}
</div>
</PlatformAsyncStatePanel>
{nativePayment ? (
<PlatformSubpanel
as="div"
radius="sm"
padding="md"
className="mt-4 text-center"
>
<div className="text-sm font-black">微信扫码支付</div>
<div className="mx-auto mt-3 flex h-[180px] w-[180px] items-center justify-center rounded-xl bg-white p-2">
{nativeQrImageUrl ? (
<img
src={nativeQrImageUrl}
alt="微信 Native 支付二维码"
className="h-full w-full"
/>
) : (
<span className="text-xs font-semibold text-slate-500">
生成中
</span>
)}
</div>
<PlatformActionButton
surface="profile"
size="xs"
className="mt-4 disabled:cursor-wait"
onClick={onConfirmNativePayment}
disabled={nativePayment.isConfirming}
>
{nativePayment.isConfirming ? '确认中' : '我已支付'}
</PlatformActionButton>
</PlatformSubpanel>
) : null}
</PlatformProfileModalShell>
);
}