import React, { useEffect, useState, useCallback, useRef } from 'react'; import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, Tooltip, Tag, Button, Typography, Radio } from 'antd'; import { QRCodeSVG } from 'qrcode.react'; import { PlayCircleOutlined, WalletOutlined, LogoutOutlined, UserOutlined, ThunderboltOutlined, HomeOutlined, DashboardOutlined, CodeOutlined, LockOutlined, PlusCircleOutlined, LeftOutlined, RightOutlined, PlusOutlined, GiftOutlined, BellOutlined, StarFilled, FireFilled, CrownFilled, BankFilled, CloseOutlined, WechatOutlined, AlipayCircleOutlined, SettingOutlined, AppstoreOutlined, FileTextOutlined, StarOutlined, HeartOutlined, CameraOutlined, CalculatorOutlined, DollarOutlined, FireOutlined, CloudOutlined, SmileOutlined, TrophyOutlined, RocketOutlined, BulbOutlined, PictureOutlined, VideoCameraOutlined, AudioOutlined, MailOutlined, PhoneOutlined, GlobalOutlined, ShoppingCartOutlined, TeamOutlined, BarChartOutlined, PieChartOutlined, LineChartOutlined, SecurityScanOutlined, ApiOutlined, DatabaseOutlined, CloudServerOutlined, } from '@ant-design/icons'; import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { useAuthStore } from '../../store/useAuthStore'; import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount } from '../../api'; import NotificationPopup from '../NotificationPopup'; interface MenuConfig { id: string; key?: string; label: string; path: string; icon: string; sortOrder: number; isActive: boolean; parentId?: string | null; parent_id?: string | null; menuType?: string; menu_type?: string; } const iconMap: Record = { HomeOutlined: , DashboardOutlined: , CodeOutlined: , PlayCircleOutlined: , WalletOutlined: , SettingOutlined: , BellOutlined: , UserOutlined: , AppstoreOutlined: , FileTextOutlined: , StarOutlined: , HeartOutlined: , CameraOutlined: , RobotOutlined: , CalculatorOutlined: , DollarOutlined: , GiftOutlined: , ThunderboltOutlined: , FireOutlined: , CloudOutlined: , SmileOutlined: , TrophyOutlined: , RocketOutlined: , BulbOutlined: , PictureOutlined: , VideoCameraOutlined: , AudioOutlined: , MailOutlined: , PhoneOutlined: , GlobalOutlined: , ShoppingCartOutlined: , TeamOutlined: , BarChartOutlined: , PieChartOutlined: , LineChartOutlined: , SecurityScanOutlined: , ApiOutlined: , DatabaseOutlined: , CloudServerOutlined: , }; const SIDEBAR_W = 240; const GRADIENTS = [ { gradient: 'linear-gradient(135deg, #c9a96e, #a67c52)', shadow: 'rgba(201,169,110,0.25)', icon: }, { gradient: 'linear-gradient(135deg, #4a5568, #2d3748)', shadow: 'rgba(74,85,104,0.25)', icon: }, { gradient: 'linear-gradient(135deg, #718096, #4a5568)', shadow: 'rgba(113,128,150,0.25)', icon: }, { gradient: 'linear-gradient(135deg, #5a67d8, #434190)', shadow: 'rgba(90,103,216,0.25)', icon: }, ]; const AppLayout: React.FC = () => { const navigate = useNavigate(); const location = useLocation(); const { user, logout } = useAuthStore(); const [pwdModalOpen, setPwdModalOpen] = useState(false); const [rechargeModalOpen, setRechargeModalOpen] = useState(false); const [pwdForm] = Form.useForm(); const [selectedPlan, setSelectedPlan] = useState(null); const [menuItems, setMenuItems] = useState([]); const [rechargeOptions, setRechargeOptions] = useState([]); const [collapsedGroups, setCollapsedGroups] = useState>({}); const [unreadCount, setUnreadCount] = useState(0); const [siteName, setSiteName] = useState(() => { const cached = localStorage.getItem('siteInfo'); const name = cached ? JSON.parse(cached).siteName || '' : ''; if (name) document.title = name; return name; }); const [siteLogo, setSiteLogo] = useState(() => { const cached = localStorage.getItem('siteInfo'); const logo = cached ? JSON.parse(cached).siteLogo || '' : ''; if (logo) { let faviconLink = document.querySelector('link[rel="icon"]') as HTMLLinkElement; if (!faviconLink) { faviconLink = document.createElement('link'); faviconLink.rel = 'icon'; document.head.appendChild(faviconLink); } faviconLink.href = logo; faviconLink.type = 'image/png'; } return logo; }); const [siteInfoLoading, setSiteInfoLoading] = useState(!localStorage.getItem('siteInfo')); const [qrCodeModalOpen, setQrCodeModalOpen] = useState(false); const [currentPaymentInfo, setCurrentPaymentInfo] = useState<{ price: number; credits: number; qrCode: string; method: string } | null>(null); const [paymentMethod, setPaymentMethod] = useState('alipay'); const [paying, setPaying] = useState(false); const [countdown, setCountdown] = useState(180); // 默认180秒超时 const pollingTimerRef = useRef | null>(null); const countdownTimerRef = useRef | null>(null); const currentOrderNoRef = useRef(null); const [enabledMethods, setEnabledMethods] = useState<{ alipay: boolean; wechat: boolean }>({ alipay: false, wechat: false }); // LocalStorage keys const PENDING_ORDER_KEY = 'pending_payment_order'; useEffect(() => { 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; if (!faviconLink) { faviconLink = document.createElement('link'); faviconLink.rel = 'icon'; document.head.appendChild(faviconLink); } faviconLink.href = logo; faviconLink.type = 'image/png'; } localStorage.setItem('siteInfo', JSON.stringify({ siteName: name, siteLogo: logo })); }).catch(() => {}); }, []); const loadUnreadCount = () => { getUnreadCount().then(count => { setUnreadCount(count); }).catch(() => {}); }; // 检查并恢复待处理的支付订单 useEffect(() => { const checkPendingOrder = async () => { const savedOrderStr = localStorage.getItem(PENDING_ORDER_KEY); if (savedOrderStr) { try { const savedOrder = JSON.parse(savedOrderStr); // 查询订单状态 const order = await getPaymentOrder(savedOrder.orderNo); if (order.status === 'pending') { // 订单仍然待支付,恢复弹窗 setCurrentPaymentInfo({ price: savedOrder.price, credits: savedOrder.credits, qrCode: savedOrder.qrCode, method: savedOrder.method, }); currentOrderNoRef.current = savedOrder.orderNo; // 计算剩余时间 const now = Date.now(); const createdAt = new Date(savedOrder.createdAt).getTime(); 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); } else { // 已超时,清除 localStorage.removeItem(PENDING_ORDER_KEY); } } else if (order.status === 'paid') { // 已支付 message.success('支付成功!积分已到账'); useAuthStore.getState().refreshUser(); localStorage.removeItem(PENDING_ORDER_KEY); } else { // 订单已取消或其他状态,清除 localStorage.removeItem(PENDING_ORDER_KEY); } } catch { // 查询失败,清除 localStorage.removeItem(PENDING_ORDER_KEY); } } }; checkPendingOrder(); }, []); useEffect(() => { getMenuConfigs().then(data => { let items = data.filter((m: any) => m.is_active !== false && m.isActive !== false); // Filter by user's allowed menus if set if (user?.allowedMenus && user.allowedMenus.length > 0) { const allowed = new Set(user.allowedMenus); const groupIds = new Set(); items.forEach((m: any) => { const pid = m.parent_id ?? m.parentId; if (pid && allowed.has(m.path)) groupIds.add(pid); }); items = items.filter((m: any) => { const mt = m.menu_type ?? m.menuType; if (mt === 'group' && groupIds.has(m.id)) return true; return allowed.has(m.path); }); } setMenuItems(items); }).catch(() => {}); getRechargePackages().then(data => { setRechargeOptions(data.filter((p: any) => p.is_active !== false && p.isActive !== false)); }).catch(() => {}); getPaymentMethods().then(data => { setEnabledMethods(data); // Auto-select the first enabled method if (data.alipay) setPaymentMethod('alipay'); else if (data.wechat) setPaymentMethod('wechat'); }).catch(() => {}); loadUnreadCount(); }, [user]); const selectedKey = location.pathname.startsWith('/records') ? '/records' : location.pathname; const sidebarW = SIDEBAR_W; const userMenuItems = [ { key: 'profile', icon: , label: `账号: ${user?.username}`, disabled: true }, { key: 'credits', icon: , label: `积分: ${user?.credits ?? 0}`, disabled: true }, { type: 'divider' as const }, { key: 'myCredits', icon: , label: '积分明细' }, { key: 'orderRecords', icon: , label: '订单记录' }, { key: 'messages', icon: , label: `消息中心${unreadCount > 0 ? `(${unreadCount})` : ''}` }, // { key: 'recharge', icon: , label: '充值积分' }, { type: 'divider' as const }, { key: 'changePwd', icon: , label: '修改密码' }, { type: 'divider' as const }, { key: 'logout', icon: , label: '退出登录', danger: true }, ]; const handleUserMenuClick = ({ key }: { key: string }) => { if (key === 'logout') { logout(); navigate('/login'); } else if (key === 'changePwd') { setPwdModalOpen(true); } else if (key === 'messages') { navigate('/messages'); } else if (key === 'recharge') { setRechargeModalOpen(true); } else if (key === 'myCredits') { navigate('/user-center?tab=credits'); } else if (key === 'orderRecords') { navigate('/user-center?tab=orders'); } }; const handleChangePwd = async () => { try { await pwdForm.validateFields(); message.success('密码修改成功(演示)'); setPwdModalOpen(false); pwdForm.resetFields(); } catch { /* validation */ } }; const handleLogout = () => { logout(); navigate('/login'); }; const handleMobileRecharge = () => { setRechargeModalOpen(true); }; const stopPolling = useCallback(() => { if (pollingTimerRef.current) { clearInterval(pollingTimerRef.current); pollingTimerRef.current = null; } if (countdownTimerRef.current) { clearInterval(countdownTimerRef.current); countdownTimerRef.current = null; } }, []); const startPolling = useCallback((orderNo: string, timeoutSeconds: number = 180) => { stopPolling(); setCountdown(timeoutSeconds); // 订单状态轮询(每2秒查询一次,只查询当前订单 const pollingTimer = setInterval(async () => { try { const order = await getPaymentOrder(orderNo); if (order.status === 'paid') { stopPolling(); currentOrderNoRef.current = null; localStorage.removeItem(PENDING_ORDER_KEY); message.success('支付成功!积分已到账'); useAuthStore.getState().refreshUser(); setQrCodeModalOpen(false); setCurrentPaymentInfo(null); setSelectedPlan(null); } else if (order.status === 'cancelled') { stopPolling(); currentOrderNoRef.current = null; localStorage.removeItem(PENDING_ORDER_KEY); } } catch { // ignore polling errors } }, 2000); pollingTimerRef.current = pollingTimer; // 倒计时 const countdownTimer = setInterval(() => { setCountdown(prev => { if (prev <= 1) { // 超时自动取消 stopPolling(); if (currentOrderNoRef.current) { cancelPaymentOrder(currentOrderNoRef.current).catch(() => {}); currentOrderNoRef.current = null; } localStorage.removeItem(PENDING_ORDER_KEY); message.warning('订单已超时,请重新充值'); setQrCodeModalOpen(false); setCurrentPaymentInfo(null); setSelectedPlan(null); return 0; } return prev - 1; }); }, 1000); countdownTimerRef.current = countdownTimer; }, [stopPolling]); return ( {/* Desktop Sidebar */}
{/* Logo */}
{siteLogo ? ( logo ) : ( )}
{siteName}
{/* Menu */}
{/* {!collapsed && (
导航
)} */} {(() => { const groups = menuItems.filter(m => (m.menu_type ?? m.menuType) === 'group'); const pages = menuItems.filter(m => (m.menu_type ?? m.menuType) !== 'group'); const childMap: Record = {}; pages.filter(m => m.parent_id ?? m.parentId).forEach(m => { const pid = (m.parent_id ?? m.parentId) as string; if (!childMap[pid]) childMap[pid] = []; childMap[pid].push(m); }); const topLevel = pages.filter(m => !(m.parent_id ?? m.parentId)); const items: React.ReactNode[] = []; const renderMenuItem = (item: MenuConfig, depth: number = 0) => { const isActive = item.path === selectedKey; const menuIcon = iconMap[item.icon] || ; return (
item.path && navigate(item.path)} style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-start', gap: 12, padding: depth > 0 ? '8px 14px 8px 36px' : '10px 16px', borderRadius: 12, margin: '2px 6px', cursor: 'pointer', fontSize: depth > 0 ? 13 : 14, fontWeight: isActive ? 600 : 400, color: isActive ? '#4f46e5' : '#475569', 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', }}> 0 ? 14 : 16, flexShrink: 0, color: isActive ? '#6366f1' : '#64748b', }}>{menuIcon} {item.label}
); }; // Render groups with children groups.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)).forEach(g => { const children = (childMap[g.id] || []).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)); items.push(
{g.label}
{children.map(c => renderMenuItem(c, 1))}
); }); // Render top-level pages topLevel.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)).forEach(m => { items.push(renderMenuItem(m)); }); return items; })()}
{/* Recharge button - opens modal */}
setRechargeModalOpen(true)} style={{ margin: '8px 12px', padding: '12px 20px', borderRadius: 14, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, color: '#ffffff', fontSize: 14, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', 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%)'; 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%)'; e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = '0 4px 16px rgba(99, 102, 241, 0.4)'; }} > 充值积分
{/* User block at bottom-left */}
{ e.currentTarget.style.background = 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)'; e.currentTarget.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.08)'; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'linear-gradient(135deg, rgba(248, 250, 252, 0.9) 0%, rgba(241, 245, 249, 0.9) 100%)'; e.currentTarget.style.boxShadow = '0 2px 12px rgba(0, 0, 0, 0.04)'; }} > } style={{ background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', flexShrink: 0, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)', }} />
{user?.username}
积分: {user?.credits || 0}
{/* Main Content */}
{/* Mobile Bottom Nav */}
{menuItems.filter((item) => (item.menu_type ?? item.menuType) !== 'group').map((item) => { if (!item.path) return null; const isActive = item.path === selectedKey; return (
navigate(item.path)}> {iconMap[item.icon] || } {item.label}
); })}
充值
退出
{/* Change Password Modal */} 修改密码} open={pwdModalOpen} onOk={handleChangePwd} onCancel={() => { setPwdModalOpen(false); pwdForm.resetFields(); }} okText="确认修改" cancelText="取消" width={440}>
} /> } /> ({ validator(_: any, value: string) { if (!value || getFieldValue('newPwd') === value) return Promise.resolve(); return Promise.reject(new Error('两次密码不一致')); }, }), ]}> } />
{/* Recharge Modal */} 积分充值} open={rechargeModalOpen} onCancel={() => { setRechargeModalOpen(false); setSelectedPlan(null); }} footer={null} width={680}>
当前积分余额 {user?.credits ?? 0}
{rechargeOptions.map((opt, idx) => { const g = GRADIENTS[idx % GRADIENTS.length]; const totalCredits = (opt.credits || 0) + (opt.bonus_credits || opt.bonusCredits || 0); return (
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 && ( {opt.description} )}
{g.icon}
{opt.name} {totalCredits.toLocaleString()} 积分
¥{opt.price}
); })}
{/* Payment method selection */} {(!enabledMethods.alipay && !enabledMethods.wechat) ? (
⚠️ 暂无可用的支付方式,请联系管理员开启支付功能
) : (
选择支付方式 setPaymentMethod(e.target.value)} style={{ display: 'flex', gap: 12 }}> {enabledMethods.alipay && ( 支付宝 )} {enabledMethods.wechat && ( 微信支付 )}
)}
{/* QR Code Payment Modal */} { stopPolling(); // Mark order as cancelled if it's still pending if (currentOrderNoRef.current) { try { await cancelPaymentOrder(currentOrderNoRef.current); } catch {} currentOrderNoRef.current = null; } localStorage.removeItem(PENDING_ORDER_KEY); setQrCodeModalOpen(false); setCurrentPaymentInfo(null); }} footer={null} width={400} closable={false} styles={{ body: { padding: 0, borderRadius: 16, overflow: 'hidden' }, }} >
{/* Header */}
{currentPaymentInfo?.method === 'alipay' ? : }
{currentPaymentInfo?.method === 'alipay' ? '支付宝支付' : '微信支付'} {currentPaymentInfo?.method === 'alipay' ? '请使用支付宝扫描二维码完成支付' : '请使用微信扫描二维码完成支付'}
{/* QR Code */}
{currentPaymentInfo && ( )}
¥{currentPaymentInfo?.price || 0}
购买 {currentPaymentInfo?.credits || 0} 积分
{/* 倒计时显示 */}
订单将在 {countdown} 秒后关闭
{/* Tips */}
💡
支付提示
  • 请在支付后等待页面自动跳转
  • 如支付成功但未到账,请联系客服
{/* Footer Buttons */}
); }; export default AppLayout;