ffcffef6d2
新增 PlatformToolModalShell 承接白底工具弹窗壳层和固定可访问名称 新增 PlatformSegmentedTabPresets 沉淀频道下划线、创作 pill rail 与二列 option segment 迁移拼图、抓大鹅、历史素材弹窗和首页 / 作品架 / 充值切换的重复组件写法 同步 PlatformUiKit 文档和 Hermes 决策记录
271 lines
8.1 KiB
TypeScript
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>
|
|
);
|
|
}
|