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, } from '@ant-design/icons'; import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { useAuthStore } from '../../store/useAuthStore'; import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getNotifications, markNotificationRead, getSiteInfo } 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: , }; const EXPANDED_W = 240; const COLLAPSED_W = 68; const GRADIENTS = [ { gradient: 'linear-gradient(135deg, #f59e0b, #f97316)', shadow: 'rgba(245,158,11,0.3)', icon: }, { gradient: 'linear-gradient(135deg, #6366f1, #8b5cf6)', shadow: 'rgba(99,102,241,0.3)', icon: }, { gradient: 'linear-gradient(135deg, #06b6d4, #0ea5e9)', shadow: 'rgba(6,182,212,0.3)', icon: }, { gradient: 'linear-gradient(135deg, #10b981, #059669)', shadow: 'rgba(16,185,129,0.3)', 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 [collapsed, setCollapsed] = useState(false); const [toggleHover, setToggleHover] = useState(false); const [selectedPlan, setSelectedPlan] = useState(null); const [menuItems, setMenuItems] = useState([]); const [rechargeOptions, setRechargeOptions] = useState([]); const [collapsedGroups, setCollapsedGroups] = useState>({}); const [msgModalOpen, setMsgModalOpen] = useState(false); const [allNotifications, setAllNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [siteName, setSiteName] = useState('VideoGen.AI'); const [siteLogo, setSiteLogo] = useState(''); 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(() => { const handleModalOpen = () => { setToggleHover(false); }; window.addEventListener('previewOpen', handleModalOpen); return () => window.removeEventListener('previewOpen', handleModalOpen); }, []); useEffect(() => { getSiteInfo().then(info => { setSiteName(info.siteName || 'VideoGen.AI'); setSiteLogo(info.siteLogo || ''); document.title = info.siteName || 'VideoGen.AI'; }).catch(() => {}); }, []); const loadNotifications = () => { getNotifications().then(data => { setAllNotifications(data); setUnreadCount(data.filter((n: any) => !(n.isRead ?? n.is_read)).length); }).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(() => {}); loadNotifications(); }, [user]); const selectedKey = location.pathname.startsWith('/records') ? '/records' : location.pathname; const sidebarW = collapsed ? COLLAPSED_W : EXPANDED_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: '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); } }; 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 + Toggle */}
{siteLogo ? ( logo ) : ( )}
{!collapsed && ( {siteName} )}
{/* Credits pill */}
navigate('/credits')} style={{ margin: collapsed ? '16px auto 4px' : '16px 14px 4px', padding: collapsed ? '10px' : '12px 14px', borderRadius: 12, background: 'linear-gradient(135deg, rgba(99,102,241,0.12), rgba(139,92,246,0.12))', border: '1px solid rgba(99,102,241,0.15)', cursor: 'pointer', transition: 'all 0.25s', textAlign: collapsed ? 'center' : 'left', }}> {collapsed ? ( ) : ( <>
可用积分
{user?.credits ?? 0} )}
{/* Recharge button - opens modal */}
setRechargeModalOpen(true)} style={{ margin: collapsed ? '8px auto 4px' : '8px 14px 4px', padding: collapsed ? '10px' : '10px 14px', borderRadius: 12, cursor: 'pointer', textAlign: 'center', background: 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.2))', border: '1px solid rgba(99,102,241,0.25)', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, transition: 'all 0.2s', }} onMouseEnter={(e) => { e.currentTarget.style.background = 'linear-gradient(135deg, rgba(99,102,241,0.3), rgba(139,92,246,0.3))'; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.2))'; }} > {!collapsed && 充值积分}
{/* 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] || ; const el = (
item.path && navigate(item.path)} style={{ display: 'flex', alignItems: 'center', justifyContent: collapsed ? 'center' : 'flex-start', gap: collapsed ? 0 : 10, padding: collapsed ? '10px 0' : depth > 0 ? '8px 14px 8px 32px' : '10px 14px', borderRadius: 10, margin: '2px 0', cursor: 'pointer', fontSize: depth > 0 ? 13 : 14, fontWeight: isActive ? 600 : 400, color: isActive ? '#fff' : 'rgba(148,163,184,0.75)', background: isActive ? 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.15))' : 'transparent', border: isActive ? '1px solid rgba(99,102,241,0.2)' : '1px solid transparent', transition: 'all 0.2s ease', }}> 0 ? 15 : 17, flexShrink: 0 }}>{menuIcon} {!collapsed && {item.label}}
); return collapsed ? {el} : el; }; // 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)); if (!collapsed) { const isGroupOpen = !collapsedGroups[g.id]; items.push(
setCollapsedGroups(prev => ({ ...prev, [g.id]: !prev[g.id] }))} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 14px', margin: '8px 0 2px', cursor: 'pointer', color: 'rgba(148,163,184,0.5)', fontSize: 11, fontWeight: 600, letterSpacing: 0.5, }}> {g.label}
{isGroupOpen && children.map(c => renderMenuItem(c, 1))}
); } else { children.forEach(c => items.push(renderMenuItem(c))); } }); // Render top-level pages topLevel.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)).forEach(m => { items.push(renderMenuItem(m)); }); return items; })()}
{/* Message button */}
{ setMsgModalOpen(true); loadNotifications(); }} style={{ margin: collapsed ? '4px auto' : '4px 14px', padding: collapsed ? '10px' : '10px 14px', borderRadius: 12, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: collapsed ? 'center' : 'flex-start', gap: collapsed ? 0 : 10, color: 'rgba(148,163,184,0.75)', fontSize: 14, background: 'rgba(255,255,255,0.03)', transition: 'all 0.2s', }} onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.06)'; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.03)'; }} > {unreadCount > 0 && ( {unreadCount > 99 ? '99+' : unreadCount} )} {!collapsed && 消息中心}
{/* User block at bottom-left */}
e.currentTarget.style.background = 'rgba(255,255,255,0.06)'} onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.03)'} > } style={{ background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', flexShrink: 0 }} /> {!collapsed && (
{user?.username}
查看账号
)}
{/* Floating sidebar toggle hover zone */}
setToggleHover(true)} onMouseLeave={() => setToggleHover(false)} style={{ position: 'fixed', left: sidebarW - 16, top: 0, bottom: 0, zIndex: 110, width: 32, cursor: 'default', transition: 'left 0.25s ease', }} > {toggleHover && (
setCollapsed(prev => !prev)} style={{ position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%, -50%)', width: 24, height: 56, borderRadius: '0 8px 8px 0', background: '#1a1a35', border: '1px solid rgba(255,255,255,0.1)', borderLeft: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', boxShadow: '2px 0 12px rgba(0,0,0,0.2)', animation: 'fadeInRight 0.2s ease both', }}> {collapsed ? : }
)}
{/* Main Content */}
{/* Mobile Bottom Nav */}
{menuItems.map((item) => { 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 */}
{/* Message Center Modal */} { setMsgModalOpen(false); }} footer={null} width={520} closable={false} styles={{ body: { padding: 0, borderRadius: 16, overflow: 'hidden' }, }} > {/* Header */}
消息中心 {unreadCount > 0 && ( {unreadCount} 条未读 )}
{/* List */}
{allNotifications.length === 0 ? (
暂无消息
) : ( allNotifications.map((n: any) => { const isRead = n.isRead ?? n.is_read; const typeConfig: Record = { system: { color: '#6366f1', bg: 'rgba(99,102,241,0.1)', label: '系统' }, credit: { color: '#f59e0b', bg: 'rgba(245,158,11,0.1)', label: '积分' }, promo: { color: '#8b5cf6', bg: 'rgba(139,92,246,0.1)', label: '活动' }, }; const tc = typeConfig[n.type] || typeConfig.system; return (
{ if (!isRead) { await markNotificationRead(n.id); loadNotifications(); } }} onMouseEnter={(e) => { e.currentTarget.style.boxShadow = '0 2px 12px rgba(0,0,0,0.04)'; }} onMouseLeave={(e) => { e.currentTarget.style.boxShadow = 'none'; }} >
{/* Type icon */}
{/* Content */}
{!isRead && } {n.title}
{n.createdAt ? n.createdAt.replace('T', ' ').slice(0, 16) : ''}
{n.content}
{tc.label}
); }) )}
); }; export default AppLayout;