This commit is contained in:
孙佳艺
2026-06-17 16:33:52 +08:00
parent 5afe837dec
commit fa0317ce0c
9 changed files with 1847 additions and 419 deletions
@@ -167,7 +167,7 @@ const AppLayout: React.FC = () => {
const countdownTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const currentOrderNoRef = useRef<string | null>(null);
const [enabledMethods, setEnabledMethods] = useState<{ alipay: boolean; wechat: boolean }>({ alipay: false, wechat: false });
// LocalStorage keys
const PENDING_ORDER_KEY = 'pending_payment_order';
@@ -177,12 +177,12 @@ const AppLayout: React.FC = () => {
getSiteInfo().then(info => {
const name = info.siteName || '民众智创';
const logo = info.siteLogo || '';
if (name !== siteName) {
setSiteName(name);
document.title = name;
}
if (logo && logo !== siteLogo) {
setSiteLogo(logo);
let faviconLink = document.querySelector('link[rel="icon"]') as HTMLLinkElement;
@@ -194,15 +194,15 @@ const AppLayout: React.FC = () => {
faviconLink.href = logo;
faviconLink.type = 'image/png';
}
localStorage.setItem('siteInfo', JSON.stringify({ siteName: name, siteLogo: logo }));
}).catch(() => {});
}).catch(() => { });
}, []);
const loadUnreadCount = () => {
getUnreadCount().then(count => {
setUnreadCount(count);
}).catch(() => {});
}).catch(() => { });
};
// 检查并恢复待处理的支付订单
@@ -229,7 +229,7 @@ const AppLayout: React.FC = () => {
const timeoutSeconds = savedOrder.timeoutSeconds || 180;
const elapsedSeconds = Math.floor((now - createdAt) / 1000);
const remainingSeconds = Math.max(0, timeoutSeconds - elapsedSeconds);
if (remainingSeconds > 0) {
setQrCodeModalOpen(true);
startPolling(savedOrder.orderNo, remainingSeconds);
@@ -252,7 +252,7 @@ const AppLayout: React.FC = () => {
}
}
};
checkPendingOrder();
}, []);
@@ -274,16 +274,16 @@ const AppLayout: React.FC = () => {
});
}
setMenuItems(items);
}).catch(() => {});
}).catch(() => { });
getRechargePackages().then(data => {
setRechargeOptions(data.filter((p: any) => p.is_active !== false && p.isActive !== false));
}).catch(() => {});
}).catch(() => { });
getPaymentMethods().then(data => {
setEnabledMethods(data);
// Auto-select the first enabled method
if (data.alipay) setPaymentMethod('alipay');
else if (data.wechat) setPaymentMethod('wechat');
}).catch(() => {});
}).catch(() => { });
loadUnreadCount();
}, [user]);
@@ -344,7 +344,7 @@ const AppLayout: React.FC = () => {
const startPolling = useCallback((orderNo: string, timeoutSeconds: number = 180) => {
stopPolling();
setCountdown(timeoutSeconds);
// 订单状态轮询(每2秒查询一次,只查询当前订单
const pollingTimer = setInterval(async () => {
try {
@@ -368,7 +368,7 @@ const AppLayout: React.FC = () => {
}
}, 2000);
pollingTimerRef.current = pollingTimer;
// 倒计时
const countdownTimer = setInterval(() => {
setCountdown(prev => {
@@ -376,7 +376,7 @@ const AppLayout: React.FC = () => {
// 超时自动取消
stopPolling();
if (currentOrderNoRef.current) {
cancelPaymentOrder(currentOrderNoRef.current).catch(() => {});
cancelPaymentOrder(currentOrderNoRef.current).catch(() => { });
currentOrderNoRef.current = null;
}
localStorage.removeItem(PENDING_ORDER_KEY);
@@ -474,8 +474,8 @@ const AppLayout: React.FC = () => {
background: isActive ? 'linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.08) 100%)' : 'transparent',
transition: 'all 0.2s ease',
}}>
<span style={{
fontSize: depth > 0 ? 14 : 16,
<span style={{
fontSize: depth > 0 ? 14 : 16,
flexShrink: 0,
color: isActive ? '#6366f1' : '#64748b',
}}>{menuIcon}</span>
@@ -521,13 +521,13 @@ const AppLayout: React.FC = () => {
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.4)',
transition: 'all 0.3s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)';
onMouseEnter={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 6px 20px rgba(99, 102, 241, 0.5)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)';
onMouseLeave={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 4px 16px rgba(99, 102, 241, 0.4)';
}}
@@ -544,7 +544,7 @@ const AppLayout: React.FC = () => {
justifyContent: 'flex-start',
gap: 12,
padding: '10px 14px',
borderRadius: 14, cursor: 'pointer',
borderRadius: 14, cursor: 'pointer',
transition: 'all 0.25s ease',
background: 'linear-gradient(135deg, rgba(248, 250, 252, 0.9) 0%, rgba(241, 245, 249, 0.9) 100%)',
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.04)',
@@ -559,8 +559,8 @@ const AppLayout: React.FC = () => {
}}
>
<Avatar size={36} icon={<UserOutlined />}
style={{
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
style={{
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
flexShrink: 0,
boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)',
}} />
@@ -568,9 +568,9 @@ const AppLayout: React.FC = () => {
<div style={{ color: '#1e293b', fontSize: 14, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: -0.01 }}>
{user?.username}
</div>
<div style={{
color: '#6366f1',
fontSize: 12,
<div style={{
color: '#6366f1',
fontSize: 12,
fontWeight: 500,
letterSpacing: 0,
background: 'rgba(99, 102, 241, 0.08)',
@@ -586,18 +586,21 @@ const AppLayout: React.FC = () => {
{/* Main Content */}
<div className="desktop-content" style={{
marginLeft: sidebarW + 48,
marginLeft: sidebarW + 48,
marginRight: 32,
marginTop: 16,
marginBottom: 16,
flex: 1,
minHeight: 'calc(100vh - 32px)',
flex: 1,
// height: '100%',
minHeight: 'calc(100vh - 32px)',
background: 'transparent',
padding: 0,
padding: 0,
transition: 'margin-left 0.25s ease',
}}>
<div style={{
background: '#ffffff',
boxSizing: 'border-box',
height: '100%',
background: '#ffffffff',
borderRadius: '20px',
boxShadow: '0 4px 32px rgba(0, 0, 0, 0.06), 0 1px 8px rgba(0, 0, 0, 0.04)',
minHeight: '100%',
@@ -672,33 +675,33 @@ const AppLayout: React.FC = () => {
const g = GRADIENTS[idx % GRADIENTS.length];
const totalCredits = (opt.credits || 0) + (opt.bonus_credits || opt.bonusCredits || 0);
return (
<div key={opt.id} onClick={() => setSelectedPlan(opt.id)} style={{
flex: '1 1 45%', minWidth: 200, borderRadius: 16, padding: '20px 16px',
background: selectedPlan === opt.id ? 'rgba(99,102,241,0.04)' : '#fafbff',
border: selectedPlan === opt.id ? '2px solid #6366f1' : '1px solid #f0f0f5',
cursor: 'pointer', position: 'relative', transition: 'all 0.2s',
}}>
{opt.description && (
<Tag color="purple" style={{ position: 'absolute', top: -10, left: '50%', transform: 'translateX(-50%)', borderRadius: 8, fontSize: 11 }}>{opt.description}</Tag>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
width: 44, height: 44, borderRadius: 12, flexShrink: 0,
background: g.gradient, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 18, color: '#fff', boxShadow: `0 6px 16px ${g.shadow}`,
}}>{g.icon}</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
<Typography.Text strong style={{ fontSize: 15 }}>{opt.name}</Typography.Text>
<Typography.Text style={{ fontSize: 12, color: '#94a3b8' }}>{totalCredits.toLocaleString()} </Typography.Text>
</div>
<div key={opt.id} onClick={() => setSelectedPlan(opt.id)} style={{
flex: '1 1 45%', minWidth: 200, borderRadius: 16, padding: '20px 16px',
background: selectedPlan === opt.id ? 'rgba(99,102,241,0.04)' : '#fafbff',
border: selectedPlan === opt.id ? '2px solid #6366f1' : '1px solid #f0f0f5',
cursor: 'pointer', position: 'relative', transition: 'all 0.2s',
}}>
{opt.description && (
<Tag color="purple" style={{ position: 'absolute', top: -10, left: '50%', transform: 'translateX(-50%)', borderRadius: 8, fontSize: 11 }}>{opt.description}</Tag>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
fontSize: 22, fontWeight: 800, marginTop: 4,
background: g.gradient, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
}}>¥{opt.price}</div>
width: 44, height: 44, borderRadius: 12, flexShrink: 0,
background: g.gradient, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 18, color: '#fff', boxShadow: `0 6px 16px ${g.shadow}`,
}}>{g.icon}</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
<Typography.Text strong style={{ fontSize: 15 }}>{opt.name}</Typography.Text>
<Typography.Text style={{ fontSize: 12, color: '#94a3b8' }}>{totalCredits.toLocaleString()} </Typography.Text>
</div>
<div style={{
fontSize: 22, fontWeight: 800, marginTop: 4,
background: g.gradient, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
}}>¥{opt.price}</div>
</div>
</div>
</div>
</div>
);
})}
</div>
@@ -763,7 +766,7 @@ const AppLayout: React.FC = () => {
setRechargeModalOpen(false);
setQrCodeModalOpen(true);
currentOrderNoRef.current = order.orderNo;
// 保存到 localStorage
localStorage.setItem(PENDING_ORDER_KEY, JSON.stringify({
orderNo: order.orderNo,
@@ -774,7 +777,7 @@ const AppLayout: React.FC = () => {
createdAt: order.createdAt || new Date().toISOString(),
timeoutSeconds: 180,
}));
// Start polling for payment status
startPolling(order.orderNo);
} else {
@@ -808,7 +811,7 @@ const AppLayout: React.FC = () => {
stopPolling();
// Mark order as cancelled if it's still pending
if (currentOrderNoRef.current) {
try { await cancelPaymentOrder(currentOrderNoRef.current); } catch {}
try { await cancelPaymentOrder(currentOrderNoRef.current); } catch { }
currentOrderNoRef.current = null;
}
localStorage.removeItem(PENDING_ORDER_KEY);
@@ -934,7 +937,7 @@ const AppLayout: React.FC = () => {
onClick={async () => {
stopPolling();
if (currentOrderNoRef.current) {
try { await cancelPaymentOrder(currentOrderNoRef.current); } catch {}
try { await cancelPaymentOrder(currentOrderNoRef.current); } catch { }
currentOrderNoRef.current = null;
}
localStorage.removeItem(PENDING_ORDER_KEY);