import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, Tooltip, Tag, Button, Typography, Radio, Drawer } 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,
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 } from '../../api';
import NotificationPopup from '../NotificationPopup';
import './AppLayout.css';
// ── 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 } = useAuthStore();
const [pwdModalOpen, setPwdModalOpen] = useState(false);
const [rechargeModalOpen, setRechargeModalOpen] = useState(false);
const [contactModalOpen, setContactModalOpen] = useState(false);
const [pwdForm] = 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);
}
localStorage.setItem('siteInfo', JSON.stringify({ siteName: name, siteLogo: logo }));
}).catch(() => { });
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(() => { });
loadUnreadCount();
}, [user]);
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 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 ? (

) : (
)}
{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 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}
setMobileMenuOpen(true)}>
{siteName}
setRechargeModalOpen(true)}>
{user?.credits || 0}
{isMobile && }
{siteLogo ? (

) : (
)}
{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}
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('两次密码不一致'));
},
}),
]}>
} />
积分充值} 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;