497 lines
14 KiB
TypeScript
497 lines
14 KiB
TypeScript
import { FC, useState, useEffect } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { BillingService } from '../../services/billingService';
|
|
import { UserAccount, UserType, RechargeRecord } from '../../types/billing';
|
|
|
|
const MobileRecharge: FC = () => {
|
|
const navigate = useNavigate();
|
|
const billingService = BillingService.getInstance();
|
|
|
|
const [userAccount, setUserAccount] = useState<UserAccount | null>(null);
|
|
const [selectedAmount, setSelectedAmount] = useState<number>(0);
|
|
const [customAmount, setCustomAmount] = useState<string>('');
|
|
const [selectedPayment, setSelectedPayment] = useState<'wechat' | 'alipay' | 'card'>('wechat');
|
|
const [rechargeHistory, setRechargeHistory] = useState<RechargeRecord[]>([]);
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
|
|
// 预设充值金额(分)
|
|
const presetAmounts = [
|
|
{ value: 5000, label: '¥50', bonus: 0, popular: false },
|
|
{ value: 10000, label: '¥100', bonus: 500, popular: true },
|
|
{ value: 20000, label: '¥200', bonus: 1500, popular: false },
|
|
{ value: 50000, label: '¥500', bonus: 5000, popular: false },
|
|
{ value: 100000, label: '¥1000', bonus: 15000, popular: false },
|
|
];
|
|
|
|
const paymentMethods = [
|
|
{ id: 'wechat', name: '微信支付', icon: '💚', color: '#07c160' },
|
|
{ id: 'alipay', name: '支付宝', icon: '💙', color: '#1677ff' },
|
|
{ id: 'card', name: '银行卡', icon: '💳', color: '#722ed1' },
|
|
];
|
|
|
|
useEffect(() => {
|
|
const account = billingService.getUserAccount();
|
|
setUserAccount(account);
|
|
|
|
// 模拟充值历史
|
|
const mockHistory: RechargeRecord[] = [
|
|
{
|
|
id: '1',
|
|
userId: account?.id || '1',
|
|
amount: 10000,
|
|
bonus: 500,
|
|
paymentMethod: 'wechat',
|
|
status: 'completed',
|
|
createdAt: new Date(Date.now() - 86400000), // 1天前
|
|
},
|
|
{
|
|
id: '2',
|
|
userId: account?.id || '1',
|
|
amount: 5000,
|
|
bonus: 0,
|
|
paymentMethod: 'alipay',
|
|
status: 'completed',
|
|
createdAt: new Date(Date.now() - 7 * 86400000), // 7天前
|
|
},
|
|
];
|
|
setRechargeHistory(mockHistory);
|
|
}, []);
|
|
|
|
const formatCurrency = (cents: number) => {
|
|
return (cents / 100).toFixed(2);
|
|
};
|
|
|
|
const handleAmountSelect = (amount: number) => {
|
|
setSelectedAmount(amount);
|
|
setCustomAmount('');
|
|
};
|
|
|
|
const handleCustomAmountChange = (value: string) => {
|
|
const numValue = parseFloat(value);
|
|
if (!isNaN(numValue) && numValue > 0) {
|
|
setSelectedAmount(Math.round(numValue * 100));
|
|
setCustomAmount(value);
|
|
} else {
|
|
setSelectedAmount(0);
|
|
setCustomAmount(value);
|
|
}
|
|
};
|
|
|
|
const getBonus = (amount: number) => {
|
|
const preset = presetAmounts.find(p => p.value === amount);
|
|
return preset?.bonus || 0;
|
|
};
|
|
|
|
const getTotalAmount = () => {
|
|
return selectedAmount + getBonus(selectedAmount);
|
|
};
|
|
|
|
const handleRecharge = async () => {
|
|
if (selectedAmount < 100) { // 最低1元
|
|
alert('充值金额不能少于1元');
|
|
return;
|
|
}
|
|
|
|
if (!userAccount) {
|
|
alert('用户账户信息未加载');
|
|
return;
|
|
}
|
|
|
|
setIsProcessing(true);
|
|
|
|
try {
|
|
// 模拟支付处理
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
// 执行充值
|
|
const success = billingService.rechargeAccount(selectedAmount);
|
|
|
|
if (success) {
|
|
// 更新用户账户
|
|
setUserAccount(billingService.getUserAccount());
|
|
|
|
// 添加充值记录
|
|
const newRecord: RechargeRecord = {
|
|
id: Date.now().toString(),
|
|
userId: userAccount.id,
|
|
amount: selectedAmount,
|
|
bonus: getBonus(selectedAmount),
|
|
paymentMethod: selectedPayment,
|
|
status: 'completed',
|
|
createdAt: new Date(),
|
|
};
|
|
setRechargeHistory(prev => [newRecord, ...prev]);
|
|
|
|
alert(`充值成功!到账金额:¥${formatCurrency(getTotalAmount())}`);
|
|
setSelectedAmount(0);
|
|
setCustomAmount('');
|
|
} else {
|
|
alert('充值失败,请重试');
|
|
}
|
|
} catch (error) {
|
|
alert('充值失败,请重试');
|
|
} finally {
|
|
setIsProcessing(false);
|
|
}
|
|
};
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'completed':
|
|
return '#52c41a';
|
|
case 'pending':
|
|
return '#fa8c16';
|
|
case 'failed':
|
|
return '#ff4d4f';
|
|
default:
|
|
return '#d9d9d9';
|
|
}
|
|
};
|
|
|
|
const getStatusText = (status: string) => {
|
|
switch (status) {
|
|
case 'completed':
|
|
return '成功';
|
|
case 'pending':
|
|
return '处理中';
|
|
case 'failed':
|
|
return '失败';
|
|
default:
|
|
return '未知';
|
|
}
|
|
};
|
|
|
|
const styles = {
|
|
container: {
|
|
padding: '16px',
|
|
backgroundColor: '#f5f5f5',
|
|
minHeight: '100vh',
|
|
},
|
|
header: {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
marginBottom: '20px',
|
|
},
|
|
backButton: {
|
|
background: 'none',
|
|
border: 'none',
|
|
fontSize: '24px',
|
|
cursor: 'pointer',
|
|
marginRight: '12px',
|
|
},
|
|
title: {
|
|
fontSize: '20px',
|
|
fontWeight: '600' as const,
|
|
color: '#333',
|
|
},
|
|
balanceCard: {
|
|
backgroundColor: 'white',
|
|
borderRadius: '16px',
|
|
padding: '20px',
|
|
marginBottom: '20px',
|
|
textAlign: 'center' as const,
|
|
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)',
|
|
},
|
|
balanceLabel: {
|
|
fontSize: '14px',
|
|
color: '#666',
|
|
marginBottom: '8px',
|
|
},
|
|
balanceAmount: {
|
|
fontSize: '32px',
|
|
fontWeight: '700' as const,
|
|
color: '#1890ff',
|
|
marginBottom: '8px',
|
|
},
|
|
userType: {
|
|
fontSize: '12px',
|
|
color: '#999',
|
|
backgroundColor: '#f0f8ff',
|
|
padding: '4px 12px',
|
|
borderRadius: '12px',
|
|
display: 'inline-block',
|
|
},
|
|
section: {
|
|
backgroundColor: 'white',
|
|
borderRadius: '16px',
|
|
padding: '20px',
|
|
marginBottom: '20px',
|
|
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)',
|
|
},
|
|
sectionTitle: {
|
|
fontSize: '18px',
|
|
fontWeight: '600' as const,
|
|
color: '#333',
|
|
marginBottom: '16px',
|
|
},
|
|
amountGrid: {
|
|
display: 'grid',
|
|
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
gap: '12px',
|
|
marginBottom: '16px',
|
|
},
|
|
amountCard: (selected: boolean, popular: boolean) => ({
|
|
padding: '16px',
|
|
borderRadius: '12px',
|
|
border: selected ? '2px solid #1890ff' : '2px solid transparent',
|
|
backgroundColor: selected ? '#e6f7ff' : '#fafafa',
|
|
cursor: 'pointer',
|
|
textAlign: 'center' as const,
|
|
position: 'relative' as const,
|
|
...(popular && {
|
|
borderColor: '#fa8c16',
|
|
backgroundColor: '#fff7e6',
|
|
}),
|
|
}),
|
|
popularBadge: {
|
|
position: 'absolute' as const,
|
|
top: '-8px',
|
|
right: '-8px',
|
|
backgroundColor: '#fa8c16',
|
|
color: 'white',
|
|
fontSize: '10px',
|
|
padding: '2px 6px',
|
|
borderRadius: '8px',
|
|
fontWeight: '500' as const,
|
|
},
|
|
amountValue: {
|
|
fontSize: '18px',
|
|
fontWeight: '600' as const,
|
|
color: '#333',
|
|
marginBottom: '4px',
|
|
},
|
|
bonusText: {
|
|
fontSize: '12px',
|
|
color: '#52c41a',
|
|
fontWeight: '500' as const,
|
|
},
|
|
customAmountInput: {
|
|
width: '100%',
|
|
padding: '12px',
|
|
borderRadius: '8px',
|
|
border: '1px solid #d9d9d9',
|
|
fontSize: '16px',
|
|
marginBottom: '16px',
|
|
},
|
|
paymentMethods: {
|
|
display: 'flex',
|
|
gap: '12px',
|
|
marginBottom: '20px',
|
|
},
|
|
paymentMethod: (selected: boolean, color: string) => ({
|
|
flex: 1,
|
|
padding: '16px',
|
|
borderRadius: '12px',
|
|
border: selected ? `2px solid ${color}` : '2px solid #f0f0f0',
|
|
backgroundColor: selected ? `${color}10` : 'white',
|
|
cursor: 'pointer',
|
|
textAlign: 'center' as const,
|
|
}),
|
|
paymentIcon: {
|
|
fontSize: '24px',
|
|
marginBottom: '8px',
|
|
},
|
|
paymentName: {
|
|
fontSize: '14px',
|
|
fontWeight: '500' as const,
|
|
color: '#333',
|
|
},
|
|
summaryCard: {
|
|
backgroundColor: '#f8f9fa',
|
|
borderRadius: '12px',
|
|
padding: '16px',
|
|
marginBottom: '20px',
|
|
},
|
|
summaryRow: {
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: '8px',
|
|
},
|
|
summaryLabel: {
|
|
fontSize: '14px',
|
|
color: '#666',
|
|
},
|
|
summaryValue: {
|
|
fontSize: '14px',
|
|
fontWeight: '500' as const,
|
|
color: '#333',
|
|
},
|
|
totalRow: {
|
|
borderTop: '1px solid #e8e8e8',
|
|
paddingTop: '8px',
|
|
marginTop: '8px',
|
|
},
|
|
totalValue: {
|
|
fontSize: '18px',
|
|
fontWeight: '600' as const,
|
|
color: '#1890ff',
|
|
},
|
|
rechargeButton: {
|
|
width: '100%',
|
|
padding: '16px',
|
|
borderRadius: '12px',
|
|
border: 'none',
|
|
backgroundColor: selectedAmount > 0 && !isProcessing ? '#1890ff' : '#d9d9d9',
|
|
color: 'white',
|
|
fontSize: '16px',
|
|
fontWeight: '600' as const,
|
|
cursor: selectedAmount > 0 && !isProcessing ? 'pointer' : 'not-allowed',
|
|
marginBottom: '20px',
|
|
},
|
|
historyItem: {
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
padding: '16px 0',
|
|
borderBottom: '1px solid #f0f0f0',
|
|
},
|
|
historyLeft: {
|
|
flex: 1,
|
|
},
|
|
historyAmount: {
|
|
fontSize: '16px',
|
|
fontWeight: '600' as const,
|
|
color: '#333',
|
|
marginBottom: '4px',
|
|
},
|
|
historyDate: {
|
|
fontSize: '12px',
|
|
color: '#999',
|
|
},
|
|
historyStatus: (status: string) => ({
|
|
fontSize: '12px',
|
|
fontWeight: '500' as const,
|
|
color: getStatusColor(status),
|
|
backgroundColor: `${getStatusColor(status)}15`,
|
|
padding: '2px 8px',
|
|
borderRadius: '8px',
|
|
}),
|
|
};
|
|
|
|
return (
|
|
<div style={styles.container}>
|
|
{/* 头部 */}
|
|
<div style={styles.header}>
|
|
<button style={styles.backButton} onClick={() => navigate(-1)}>
|
|
←
|
|
</button>
|
|
<div style={styles.title}>账户充值</div>
|
|
</div>
|
|
|
|
{/* 余额显示 */}
|
|
<div style={styles.balanceCard}>
|
|
<div style={styles.balanceLabel}>当前余额</div>
|
|
<div style={styles.balanceAmount}>
|
|
¥{userAccount ? formatCurrency(userAccount.balance) : '0.00'}
|
|
</div>
|
|
<div style={styles.userType}>
|
|
{userAccount?.userType === UserType.ENTERPRISE ? '企业用户' : '个人用户'}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 充值金额选择 */}
|
|
<div style={styles.section}>
|
|
<div style={styles.sectionTitle}>选择充值金额</div>
|
|
<div style={styles.amountGrid}>
|
|
{presetAmounts.map((amount) => (
|
|
<div
|
|
key={amount.value}
|
|
style={styles.amountCard(selectedAmount === amount.value, amount.popular)}
|
|
onClick={() => handleAmountSelect(amount.value)}
|
|
>
|
|
{amount.popular && <div style={styles.popularBadge}>推荐</div>}
|
|
<div style={styles.amountValue}>{amount.label}</div>
|
|
{amount.bonus > 0 && (
|
|
<div style={styles.bonusText}>
|
|
送¥{formatCurrency(amount.bonus)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<input
|
|
type="number"
|
|
placeholder="自定义金额(元)"
|
|
style={styles.customAmountInput}
|
|
value={customAmount}
|
|
onChange={(e) => handleCustomAmountChange(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
{/* 支付方式 */}
|
|
<div style={styles.section}>
|
|
<div style={styles.sectionTitle}>选择支付方式</div>
|
|
<div style={styles.paymentMethods}>
|
|
{paymentMethods.map((method) => (
|
|
<div
|
|
key={method.id}
|
|
style={styles.paymentMethod(
|
|
selectedPayment === method.id,
|
|
method.color
|
|
)}
|
|
onClick={() => setSelectedPayment(method.id as any)}
|
|
>
|
|
<div style={styles.paymentIcon}>{method.icon}</div>
|
|
<div style={styles.paymentName}>{method.name}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 费用明细 */}
|
|
{selectedAmount > 0 && (
|
|
<div style={styles.summaryCard}>
|
|
<div style={styles.summaryRow}>
|
|
<span style={styles.summaryLabel}>充值金额</span>
|
|
<span style={styles.summaryValue}>¥{formatCurrency(selectedAmount)}</span>
|
|
</div>
|
|
{getBonus(selectedAmount) > 0 && (
|
|
<div style={styles.summaryRow}>
|
|
<span style={styles.summaryLabel}>赠送金额</span>
|
|
<span style={{...styles.summaryValue, color: '#52c41a'}}>
|
|
+¥{formatCurrency(getBonus(selectedAmount))}
|
|
</span>
|
|
</div>
|
|
)}
|
|
<div style={{...styles.summaryRow, ...styles.totalRow}}>
|
|
<span style={styles.summaryLabel}>到账金额</span>
|
|
<span style={styles.totalValue}>¥{formatCurrency(getTotalAmount())}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 充值按钮 */}
|
|
<button
|
|
style={styles.rechargeButton}
|
|
onClick={handleRecharge}
|
|
disabled={selectedAmount === 0 || isProcessing}
|
|
>
|
|
{isProcessing ? '处理中...' : `确认充值 ¥${formatCurrency(selectedAmount)}`}
|
|
</button>
|
|
|
|
{/* 充值历史 */}
|
|
<div style={styles.section}>
|
|
<div style={styles.sectionTitle}>充值记录</div>
|
|
{rechargeHistory.map((record) => (
|
|
<div key={record.id} style={styles.historyItem}>
|
|
<div style={styles.historyLeft}>
|
|
<div style={styles.historyAmount}>
|
|
+¥{formatCurrency(record.amount + record.bonus)}
|
|
</div>
|
|
<div style={styles.historyDate}>
|
|
{record.createdAt.toLocaleDateString()} {record.createdAt.toLocaleTimeString()}
|
|
</div>
|
|
</div>
|
|
<div style={styles.historyStatus(record.status)}>
|
|
{getStatusText(record.status)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MobileRecharge;
|