import React, { useEffect, useState, useCallback, useRef } from 'react'; import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, Tooltip, Tag, Button, Typography, Radio, Drawer, Tabs } 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, CaretDownOutlined, DollarOutlined, FireOutlined, CloudOutlined, SmileOutlined, TrophyOutlined, RocketOutlined, BulbOutlined, PictureOutlined, VideoCameraOutlined, AudioOutlined, MailOutlined, PhoneOutlined, GlobalOutlined, ShoppingCartOutlined, TeamOutlined, BarChartOutlined, PieChartOutlined, LineChartOutlined, SecurityScanOutlined, ApiOutlined, DatabaseOutlined, CloudServerOutlined, MessageOutlined, DownOutlined, InfoOutlined, MenuOutlined, ArrowLeftOutlined, RobotOutlined, } 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, createContactRequest, getUser, changePassword, changeUsername } from '../../api'; import NotificationPopup from '../NotificationPopup'; import './AppLayout.css'; import bg1 from '../../assets/bg1.png'; // ── ResourceCapacity 类型(与 src/types/index.ts 保持一致) ── type ResourceCapacityData = { enabled: boolean; usedBytes: number; totalBytes: number; availableBytes: number; usagePercent: number; exceeded: boolean; limitValue: string; limitUnit: string; }; /** * 资源存储展示卡片 * - enabled === true:显示「已用 / 总额」+ 进度条(超额时变红并提示) * - enabled === false:按 usedBytes 大小自动选 KB / MB / GB / TB 单位显示「当前使用」 */ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data }) => { if (!data) return null; // 单位:后端返回的 limitUnit(KB / MB / GB) const unit = (data.limitUnit || 'GB').toUpperCase(); const divisor = unit === 'GB' ? 1024 ** 3 : unit === 'MB' ? 1024 ** 2 : 1024; // 百分比(可能 > 100) const rawPercent = Number(data.usagePercent) || 0; // 进度条宽度最多 100% const barPercent = Math.min(100, Math.max(0, rawPercent)); // 总额:优先用后端 limitValue,兜底 totalBytes / divisor const total = data.limitValue ? parseFloat(data.limitValue) : data.totalBytes / divisor; // 已用 = 总额 × 百分比 / 100(保证数字与 percent 自洽) const used = (total * rawPercent) / 100; // 剩余 = 总额 - 已用 const available = total - used; // 超额判断 const isOver = data.exceeded || rawPercent >= 100; // enabled === false 时按 usedBytes 自动选单位 const KB = 1024; const MB = 1024 ** 2; const GB = 1024 ** 3; const TB = 1024 ** 4; const formatUsedAuto = (bytes: number) => { const b = Number(bytes) || 0; if (b >= TB) return { val: (b / TB).toFixed(2), unit: 'TB' }; if (b >= GB) return { val: (b / GB).toFixed(2), unit: 'GB' }; if (b >= MB) return { val: (b / MB).toFixed(2), unit: 'MB' }; return { val: (b / KB).toFixed(2), unit: 'KB' }; }; const usedAuto = formatUsedAuto(data.usedBytes); return (
{/* {rawPercent.toFixed(1)}% */} {data.enabled ? ( <> ) : ( 当前使用 {usedAuto.val} {usedAuto.unit} )}
{data.enabled && ( <>
{rawPercent.toFixed(1)}% {data.enabled ? ( {used.toFixed(2)} / {total.toFixed(2)} {unit} ) : ( 当前使用 {usedAuto.val} {usedAuto.unit} )}
{/* {isOver ? `存储超额 · 超用 ${Math.abs(available).toFixed(2)} ${unit}` : `剩余 ${available.toFixed(2)} ${unit}`} */} {/* {rawPercent.toFixed(1)}% */}
)}
); }; 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, refreshUser, setOptimizeHoldCredits } = useAuthStore(); const [pwdModalOpen, setPwdModalOpen] = useState(false); const [rechargeModalOpen, setRechargeModalOpen] = useState(false); const [contactModalOpen, setContactModalOpen] = useState(false); const [pwdForm] = Form.useForm(); const [usernameForm] = Form.useForm(); const [selectedPlan, setSelectedPlan] = useState(null); const [menuItems, setMenuItems] = useState([]); const [rechargeOptions, setRechargeOptions] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [contactHovered, setContactHovered] = useState(false); const [contactForm] = Form.useForm(); const [submittingContact, setSubmittingContact] = useState(false); const [contactPosition, setContactPosition] = useState<{ x: number; y: number }>(() => ({ x: 24, y: window.innerHeight * 0.75 })); const [isDragging, setIsDragging] = useState(false); const hasMovedRef = useRef(false); const dragStartRef = useRef({ x: 0, y: 0 }); const [mobileExpandedMenus, setMobileExpandedMenus] = useState>({}); 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); 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 }); // 资源存储容量(从 getUser().resource_capacity 获取) const [resourceCapacity, setResourceCapacity] = useState<{ enabled: boolean; usedBytes: number; totalBytes: number; availableBytes: number; usagePercent: number; exceeded: boolean; limitValue: string; limitUnit: string; } | null>(null); const [isMobile, setIsMobile] = useState(false); const [operationManualUrl, setOperationManualUrl] = useState(''); 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'; } if (info.operationManual) { setOperationManualUrl(info.operationManual); } if (info.optimizeHoldCredits !== undefined) { setOptimizeHoldCredits(info.optimizeHoldCredits); } else { } localStorage.setItem('siteInfo', JSON.stringify({ siteName: name, siteLogo: logo })); }).catch((err) => { }); getUser().then((res: any) => { // console.log('[Storage] getUser 返回:', res); const rc = res?.resourceCapacity; if (rc) { setResourceCapacity({ enabled: !!rc.enabled, usedBytes: Number(rc.usedBytes) || 0, totalBytes: Number(rc.totalBytes) || 0, availableBytes: Number(rc.availableBytes) || 0, usagePercent: Number(rc.usagePercent) || 0, exceeded: !!rc.exceeded, limitValue: rc.limitValue ?? '', limitUnit: rc.limitUnit || 'GB', }); } // 没数据时不显示 }).catch((err: any) => { // console.error('[Storage] getUser 失败:', err); // 接口失败也不显示 }); }, []); useEffect(() => { const checkMobile = () => setIsMobile(window.innerWidth <= 767); checkMobile(); window.addEventListener('resize', checkMobile); return () => window.removeEventListener('resize', checkMobile); }, []); const handleContactMouseDown = (e: React.MouseEvent) => { if (e.button === 0) { setIsDragging(true); hasMovedRef.current = false; dragStartRef.current = { x: e.clientX, y: e.clientY, }; } }; const handleContactMouseMove = (e: MouseEvent) => { if (!isDragging) return; const deltaX = Math.abs(e.clientX - dragStartRef.current.x); const deltaY = Math.abs(e.clientY - dragStartRef.current.y); if (deltaX > 5 || deltaY > 5) { hasMovedRef.current = true; } const newY = Math.max(60, Math.min(window.innerHeight - 60, e.clientY - (dragStartRef.current.y - contactPosition.y))); setContactPosition(prev => ({ x: prev.x, y: newY })); }; const handleContactMouseUp = () => { const moved = hasMovedRef.current; setIsDragging(false); hasMovedRef.current = false; if (!moved) { setContactModalOpen(true); } }; useEffect(() => { if (isDragging) { document.addEventListener('mousemove', handleContactMouseMove); document.addEventListener('mouseup', handleContactMouseUp); return () => { document.removeEventListener('mousemove', handleContactMouseMove); document.removeEventListener('mouseup', handleContactMouseUp); }; } }, [isDragging]); 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 => { const items = data.filter((m: any) => m.is_active !== false && m.isActive !== false); setMenuItems(items); }).catch(() => { }); getRechargePackages().then(data => { setRechargeOptions(data.filter((p: any) => p.is_active !== false && p.isActive !== false)); }).catch(() => { }); getPaymentMethods().then(data => { setEnabledMethods(data); if (data.alipay) setPaymentMethod('alipay'); else if (data.wechat) setPaymentMethod('wechat'); }).catch(() => { }); }, []); useEffect(() => { if (user) { loadUnreadCount(); } }, [user?.id, user?.credits]); const selectedKey = location.pathname.startsWith('/records') ? '/records' : location.pathname; const sidebarW = SIDEBAR_W; const childMap: Record = {}; menuItems.forEach(m => { const pid = m.parent_id ?? m.parentId; if (pid) { if (!childMap[pid]) childMap[pid] = []; childMap[pid].push(m); } }); const topLevelItems = menuItems.filter(m => !(m.parent_id ?? m.parentId)); topLevelItems.sort((a, b) => { const orderA = typeof a.sortOrder === 'number' ? a.sortOrder : Infinity; const orderB = typeof b.sortOrder === 'number' ? b.sortOrder : Infinity; return orderA - orderB; }); Object.keys(childMap).forEach(key => { childMap[key].sort((a, b) => { const orderA = typeof a.sortOrder === 'number' ? a.sortOrder : Infinity; const orderB = typeof b.sortOrder === 'number' ? b.sortOrder : Infinity; return orderA - orderB; }); }); const handleMobileMenuClick = (item: MenuConfig) => { const menuType = item.menu_type ?? item.menuType; const hasChildren = childMap[item.id] && childMap[item.id].length > 0; if (menuType === 'group' || hasChildren) { setMobileExpandedMenus(prev => ({ ...prev, [item.id]: !prev[item.id] })); } else if (item.path) { navigate(item.path); setMobileMenuOpen(false); } }; const userMenuItems = [ { key: 'profile', icon: , label: `账号: ${user?.username}`, disabled: true }, { key: 'credits', icon: , label: `积分: ${user?.credits ?? 0}`, disabled: true }, { type: 'divider' as const }, ...(user?.isTeamManager ? [{ key: 'teamManagement' as const, icon: , label: '团队管理' }] : []), { key: 'myCredits', icon: , label: '积分明细' }, { key: 'orderRecords', icon: , label: '订单记录' }, { key: 'messages', icon: , label: `消息中心${unreadCount > 0 ? `(${unreadCount})` : ''}` }, ...(operationManualUrl ? [{ key: 'manual' as const, 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 === 'teamManagement') { navigate('/team-management'); } else if (key === 'myCredits') { navigate('/user-center?tab=credits'); } else if (key === 'orderRecords') { navigate('/user-center?tab=orders'); } else if (key === 'manual') { window.open(operationManualUrl, '_blank'); } }; const handleChangePwd = async () => { try { const values = await pwdForm.validateFields(); await changePassword(values.oldPwd, values.newPwd); message.success('密码修改成功'); setPwdModalOpen(false); pwdForm.resetFields(); } catch (error: any) { if (error?.response?.data?.detail) { message.error(error.response.data.detail); } else if (error?.message) { message.error(error.message); } } }; const handleChangeUsername = async () => { try { const values = await usernameForm.validateFields(); const result = await changeUsername(values.username); message.success(result.message || '操作成功'); await refreshUser(); getUser().then((res: any) => { const rc = res?.resourceCapacity; if (rc) { setResourceCapacity({ enabled: !!rc.enabled, usedBytes: Number(rc.usedBytes) || 0, totalBytes: Number(rc.totalBytes) || 0, availableBytes: Number(rc.availableBytes) || 0, usagePercent: Number(rc.usagePercent) || 0, exceeded: !!rc.exceeded, limitValue: rc.limitValue ?? '', limitUnit: rc.limitUnit || 'GB', }); } }).catch(() => { }); setPwdModalOpen(false); usernameForm.resetFields(); } catch (error: any) { if (error?.response?.data?.message) { message.error(error.response.data.message); } else if (error?.response?.data?.detail) { message.error(error.response.data.detail); } else if (error?.message) { message.error(error.message); } } }; const handleLogout = () => { logout(); navigate('/login'); }; const handleMobileRecharge = () => { setRechargeModalOpen(true); }; const handleContactSubmit = async () => { if (!user) { message.warning('请先登录'); return; } try { const values = await contactForm.validateFields(); setSubmittingContact(true); await createContactRequest({ phone: values.phone, company_name: values.companyName, industry: values.industry, name: values.name, message: values.message, }); message.success('提交成功,我们会尽快与您联系'); setContactModalOpen(false); contactForm.resetFields(); } catch (err: any) { message.error(err?.message || '提交失败'); } finally { setSubmittingContact(false); } }; 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); 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 { } }, 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]); const renderMenuItem = (item: MenuConfig, depth: number = 0) => { const isActive = item.path === selectedKey; const menuIcon = iconMap[item.icon] || ; const hasChildren = childMap[item.id] && childMap[item.id].length > 0; const menuType = item.menu_type ?? item.menuType; const isGroup = menuType === 'group'; return (
{ if (!(isGroup || hasChildren) && item.path) { navigate(item.path); } }} style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-start', gap: 10, padding: depth > 0 ? '6px 12px 6px 32px' : '6px 14px', borderRadius: 12, margin: '1px 4px', cursor: isGroup || hasChildren ? 'default' : 'pointer', fontSize: depth > 0 ? 13 : 14, fontWeight: isActive ? 600 : 400, color: isActive ? '#4f46e5' : (isGroup ? '#94a3b8' : '#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', }} onMouseEnter={(e) => { if (!isGroup && !isActive && !(isGroup || hasChildren)) { e.currentTarget.style.background = 'rgba(99, 102, 241, 0.05)'; } }} onMouseLeave={(e) => { if (!isActive) { e.currentTarget.style.background = 'transparent'; } }} > {!isGroup && ( 0 ? 14 : 16, flexShrink: 0, color: isActive ? '#6366f1' : '#64748b', }}>{menuIcon} )} 0 ? 13 : 14), textTransform: isGroup ? 'uppercase' : 'none', letterSpacing: isGroup ? 0.5 : 0 }}> {item.label}
{(isGroup || hasChildren) && (
{(childMap[item.id] || []).map(c => renderMenuItem(c, depth + 1))}
)}
); }; return (
{ navigate('/home'); }}>
{siteLogo ? ( logo ) : ( )}
{siteName}
{topLevelItems.map(item => renderMenuItem(item))}
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)'; }} > 充值积分
{/* 资源存储容量展示(来自 getUser.resourceCapacity) */}
{ e.currentTarget.style.background = 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)'; e.currentTarget.style.boxShadow = '0 8px 24px rgba(99, 102, 241, 0.25), inset 0 1px 0 rgba(255,255,255,0.9)'; e.currentTarget.style.transform = 'translateY(-3px) scale(1.01)'; e.currentTarget.style.borderColor = 'rgba(99, 102, 241, 0.4)'; e.currentTarget.style.animation = 'none'; e.currentTarget.querySelector('.user-card-arrow')?.classList.add('arrow-hover'); }} onMouseLeave={(e) => { e.currentTarget.style.background = 'linear-gradient(135deg, rgba(248, 250, 252, 0.95) 0%, rgba(241, 245, 249, 0.95) 100%)'; e.currentTarget.style.boxShadow = '0 2px 12px rgba(0, 0, 0, 0.04), inset 0 1px 0 rgba(255,255,255,0.8)'; e.currentTarget.style.transform = 'translateY(0) scale(1)'; e.currentTarget.style.borderColor = 'rgba(99, 102, 241, 0.15)'; e.currentTarget.style.animation = 'cardBreath 3s ease-in-out infinite'; e.currentTarget.querySelector('.user-card-arrow')?.classList.remove('arrow-hover'); }} >
} style={{ background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', flexShrink: 0, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)', transition: 'all 0.35s cubic-bezier(0.4,0,0.2,1)', position: 'relative', zIndex: 1, animation: 'avatarPulse 2s ease-in-out infinite', }} />
{user?.username}
积分: {user?.credits || 0}
{!isMobile && }
setMobileMenuOpen(true)}>
{siteName}
setRechargeModalOpen(true)}> {user?.credits || 0}
{isMobile && }
{siteLogo ? ( logo ) : ( )}
{siteName}
} placement="left" onClose={() => setMobileMenuOpen(false)} open={mobileMenuOpen} size={280} closable={true} className="mobile-menu-drawer" styles={{ header: { borderBottom: '1px solid #f1f5f9', padding: '16px 20px' }, body: { padding: '12px 8px', display: 'flex', flexDirection: 'column' }, }} >
{user?.username || '用户'}
积分: {user?.credits ?? 0}
{/* 资源存储容量展示(与桌面端共用 StorageCard) */} {topLevelItems.map(item => { const menuType = item.menu_type ?? item.menuType; const hasChildren = childMap[item.id] && childMap[item.id].length > 0; const isActive = item.path === selectedKey; const isExpanded = mobileExpandedMenus[item.id]; const menuIcon = iconMap[item.icon] || ; return (
handleMobileMenuClick(item)} > {menuType !== 'group' && ( {menuIcon} )} {item.label} {(menuType === 'group' || hasChildren) && ( )}
{(menuType === 'group' || hasChildren) && isExpanded && (
{(childMap[item.id] || []).map(child => { const childActive = child.path === selectedKey; const childIcon = iconMap[child.icon] || ; return (
handleMobileMenuClick(child)} > {childIcon} {child.label}
); })}
)}
); })}
{ navigate('/user-center?tab=credits'); setMobileMenuOpen(false); }} > 积分明细
{ navigate('/user-center?tab=orders'); setMobileMenuOpen(false); }} > 订单记录
{ navigate('/messages'); setMobileMenuOpen(false); }} > 消息中心 {unreadCount > 0 && ( {unreadCount} )}
{ setPwdModalOpen(true); setMobileMenuOpen(false); }} > 个人信息
{ setRechargeModalOpen(true); setMobileMenuOpen(false); }} > 充值积分
{ handleLogout(); setMobileMenuOpen(false); }} > 退出登录
账号设置} open={pwdModalOpen} onCancel={() => { setPwdModalOpen(false); pwdForm.resetFields(); usernameForm.resetFields(); }} width={440} footer={null}>
{ }}> } />
{ }}> } /> } /> ({ validator(_: any, value: string) { if (!value || getFieldValue('newPwd') === value) return Promise.resolve(); return Promise.reject(new Error('两次密码不一致')); }, }), ]}> } />
积分充值} open={rechargeModalOpen} onCancel={() => { setRechargeModalOpen(false); setSelectedPlan(null); }} width={680} className="recharge-modal" footer={
}>
当前积分余额 {user?.credits ?? 0} {(!enabledMethods.alipay && !enabledMethods.wechat) ? (
⚠️ 暂无可用的支付方式,请联系管理员开启支付功能
) : (
选择支付方式 setPaymentMethod(e.target.value)} style={{ display: 'flex', gap: 12 }}> {enabledMethods.alipay && ( 支付宝 )} {enabledMethods.wechat && ( 微信支付 )}
)}
当前平台仅支持支付宝/微信扫码充值,如需转账支付请 { setRechargeModalOpen(false); setContactModalOpen(true); }} >联系我们
{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}
); })}
{ stopPolling(); 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' }, }} >
{currentPaymentInfo?.method === 'alipay' ? : }
{currentPaymentInfo?.method === 'alipay' ? '支付宝支付' : '微信支付'} {currentPaymentInfo?.method === 'alipay' ? '请使用支付宝扫描二维码完成支付' : '请使用微信扫描二维码完成支付'}
{currentPaymentInfo && ( )}
¥{currentPaymentInfo?.price || 0}
购买 {currentPaymentInfo?.credits || 0} 积分
订单将在 {countdown} 秒后关闭
💡
支付提示
  • 请在支付后等待页面自动跳转
  • 如支付成功但未到账,请联系客服
联系我们
联系我们} open={contactModalOpen} onCancel={() => { setContactModalOpen(false); contactForm.resetFields(); }} footer={null} width={480} className="contact-modal" >
); }; export default AppLayout;