import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, notification, Tag, Button, Typography, Radio, Drawer, Tabs, Progress, Collapse, ConfigProvider } from 'antd';
import { QRCodeSVG } from 'qrcode.react';
import {
PlayCircleOutlined,
WalletOutlined,
LogoutOutlined,
UserOutlined,
ThunderboltOutlined,
HomeOutlined,
DashboardOutlined,
CodeOutlined,
LockOutlined,
PlusOutlined,
GiftOutlined,
BellOutlined,
SettingOutlined,
AppstoreOutlined,
FileTextOutlined,
StarOutlined,
HeartOutlined,
CameraOutlined,
CloudOutlined,
SmileOutlined,
TrophyOutlined,
RocketOutlined,
BulbOutlined,
PictureOutlined,
VideoCameraOutlined,
AudioOutlined,
MailOutlined,
PhoneOutlined,
GlobalOutlined,
TeamOutlined,
BarChartOutlined,
PieChartOutlined,
LineChartOutlined,
SecurityScanOutlined,
ApiOutlined,
DatabaseOutlined,
CloudServerOutlined,
MessageOutlined,
MenuOutlined,
RobotOutlined,
ProfileOutlined,
CloseOutlined,
CrownOutlined,
CrownFilled,
FireOutlined,
BankFilled,
CheckCircleFilled,
WechatOutlined,
AlipayCircleOutlined,
ShoppingCartOutlined,
DownOutlined,
UpOutlined,
RightOutlined,
StarFilled,
FireFilled,
} from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../../store/useAuthStore';
import { getMenuConfigs, getCreditProductCatalog, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount, createContactRequest, getUser, changePassword, changeUsername } from '../../api';
import type { CreditProduct, CreditProductCatalog } from '../../types';
import NotificationPopup from '../NotificationPopup';
import ActivityBanner from './ActivityBanner';
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 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: ,
GiftOutlined: ,
ThunderboltOutlined: ,
CloudOutlined: ,
SmileOutlined: ,
TrophyOutlined: ,
RocketOutlined: ,
BulbOutlined: ,
PictureOutlined: ,
VideoCameraOutlined: ,
AudioOutlined: ,
MailOutlined: ,
PhoneOutlined: ,
GlobalOutlined: ,
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 MEMBERSHIP_GRADIENTS = [
{ gradient: 'linear-gradient(135deg, #f6ad55, #dd6b20)', shadow: 'rgba(246,173,85,0.25)', icon: },
{ gradient: 'linear-gradient(135deg, #667eea, #5a67d8)', shadow: 'rgba(102,126,234,0.25)', icon: },
{ gradient: 'linear-gradient(135deg, #94a3b8, #64748b)', shadow: 'rgba(148,163,184,0.25)', icon: },
{ gradient: 'linear-gradient(135deg, #c9a96e, #a67c52)', shadow: 'rgba(201,169,110,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 FAQ_ITEMS = [
{
key: 'points-validity',
label: '积分有效期规则',
children: (
1、会员积分:月卡与年卡的积分配额均按月发放,自到账起 31 天内有效,到期自动重置(上周期未使用积分清零 + 下发下周期度积分);
2、充值通用积分:自到账起 2 年内有效,到期清零,不退不换;
3、模型专属积分有效期升级:2026年8月7日(含)起购买,有效期为2年;此前购买,有效期为6个月。有效期自积分到账之日起计算,到期清零,不退不换;
4、每日登录积分:赠送 20 积分,仅限当日使用,次日自动清零。
),
},
{
key: 'refund-rules',
label: '会员&积分 退款规则',
children: (
会员与积分属于虚拟数字商品,开通后权益即时生效,因此一经购买不支持任何理由的退款或转让。建议您在支付前确认所选内容,如遇重复扣款或系统异常,请联系客服,我们会尽快为您处理。感谢理解 💛
),
},
{
key: 'points-return',
label: '积分返还规则',
children: (
若生成分成失败,被扣的对应积分将在 2 小时内返还至原账户,在「积分明细页」原消耗记录条目中会展示"任务生成失败,积分已返还"提示。因个别原因导致延迟或未返还的情况,可联系平台客服进行处理。
),
},
{
key: 'points-consumption',
label: '积分消耗顺序',
children: (
积分消耗顺序规则:分为平台默认顺序、用户自定义顺序。
1、平台默认消耗顺序:免费积分 > 模型专享积分 > 订阅会员积分 > 通用充值类积分;
其中,免费积分包含 赠送的TV专属积分、赠送的指定模型专享积分、每日登录奖励积分等。若同一类型中的积分包含多个细分子类型,子类型之间的消耗顺序按到期时间,优先消耗早到期的;
2、自定义积分消耗顺序:用户可在「个人中心-充值入口」、「模型购买页面」、「积分明细页」自主设置积分的消耗顺序。
特别注意:模型赠送的限时免费生成次数,会优先于积分被最先使用。
),
},
{
key: 'get-more-points',
label: '如何获取更多积分',
children: (
若当前积分不足,可通过以下方式补充:
- 升级会员:购买后立即生效,积分即时到账;
- 单独充值通用积分:按需购买,灵活补充;
- 单独充值模型专享积分:随用随充,性价比高,适用于对特定模型重度使用的用户。
),
},
{
key: 'installment',
label: '关于分期支付',
children: (
部分商品支持花呗分期,目前分期产生的利息由平台承担,用户限时享受0利息。使用分期支付的商品不支持退款,如有逾期还款或产生退费,平台将收取相应费用。
),
},
{
key: 'invoice',
label: '发票申请与联系方式',
children: (
发票可在「订阅与开票」→「购买记录」中自助申请。如需企业合作,请联系 bd@liblib.ai;其他问题欢迎前往"帮助中心"查询。
),
},
{
key: 'price-protection',
label: '会员权益7天保护计划',
children: (
7天内购买的会员,若遇同档位会员有新活动,且赠品力度更高,可申请按差额补发赠品。
1、保护范围:指定模型的免费生成次数、专享积分赠送(会员)。
2、保护条件:支付成功 ≤7 天,且会员仍在有效期内,仅限同档位、同周期、同类型赠品。每笔订单针对同一模型赠品,仅限申请 1 次;
3、权益有效期:补发的权益与原会员同效期,到期未自动清零,不可退款、转让。
4、不参与保护范围:会员订单价格本身(即不退会员差价);已下线/已结束的活动;通过平台赠送、邀请奖励等非现金方式开通的会员;
5、特别注意:如未在保护有效期内主动领取保护权益,过期将不会补发;Happy Horse 1.0模型不参与保护。
),
},
{
key: 'free-points',
label: '免积分畅享卡 优惠活动如何执行的?',
children: (
免积分权益可让您在权益有效期内,使用对应模型生成不消耗积分,同一模型同时最多运行 1 个免积分任务,高峰期生成可能排队,排队时间受到使用人数、高峰时段、模型后台资源情况、用户当日已生成数量等多种因素有关。
具体规则如下:
- 独立于订阅周期:免限权益按固定期限计算,与订阅周期无关。无论订阅自动续费、取消还是重新开通,都不会重置或延长该权益的有效期。
- 有效期内升级套餐:如果您在免积分权益仍然有效期内升级套餐,将立即解锁更高等级套餐对应的模型能力,并刷新您的有效期。
- 合理使用与系统保护:免积分权益仅支持真人手动操作,暂不支持 CLI 和其他自动化脚本等生成。为了保障 GPU 集群稳定运行,如检测到自动化脚本、机器人操作或非真人高频使用行为,系统可能会暂时暂停您的权限进行审核。确认是真人创作者后,将恢复访问权限。
),
},
];
const AppLayout: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const { user, logout, refreshUser } = useAuthStore();
const [pwdModalOpen, setPwdModalOpen] = useState(false);
const [contactModalOpen, setContactModalOpen] = useState(false);
const [rechargeModalOpen, setRechargeModalOpen] = useState(false);
const [pwdForm] = Form.useForm();
const [usernameForm] = Form.useForm();
const [selectedPlan, setSelectedPlan] = useState(null);
const [menuItems, setMenuItems] = useState([]);
const [rechargeOptions, setRechargeOptions] = useState([]);
const [subscriptionProducts, setSubscriptionProducts] = useState([]);
const [currentSubscription, setCurrentSubscription] = useState(null);
const loadCreditCatalog = useCallback(async () => {
try {
const data = await getCreditProductCatalog();
setSubscriptionProducts((data.subscriptionProducts || []).filter((product) => product.isActive));
setRechargeOptions((data.creditAddons || []).filter((product) => product.isActive));
setCurrentSubscription(data.currentSubscription || null);
} catch {
setSubscriptionProducts([]);
setRechargeOptions([]);
setCurrentSubscription(null);
}
}, []);
const [creditsModalOpen, setCreditsModalOpen] = useState(false);
const [selectedPeriod, setSelectedPeriod] = useState<'monthly' | 'quarterly' | 'yearly'>('monthly');
const [qrCodeModalOpen, setQrCodeModalOpen] = useState(false);
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 });
const [currentPaymentInfo, setCurrentPaymentInfo] = useState<{
price: number;
credits: number;
qrCode: string;
method: string;
title: string;
originalPrice?: number;
discountAmount?: number;
tierName?: string;
periodLabel?: string;
isSubscription?: boolean;
} | null>(null);
const PENDING_ORDER_KEY = 'pending_payment_order';
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 [bannerVisible, setBannerVisible] = useState(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('');
// 监听"被踢出"事件 — 单设备登录互斥,显示顶部通知
useEffect(() => {
const handleKickedOut = () => {
notification.warning({
message: '登录提示',
description: '您的账号已在其他设备登录,请重新登录',
placement: 'top',
duration: 0, // 不自动关闭
btn: (
),
});
};
window.addEventListener('kicked-out', handleKickedOut);
return () => window.removeEventListener('kicked-out', handleKickedOut);
}, [navigate]);
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);
}
// 相对路径拼接 API 基础地址,确保 favicon 正确加载
const base = import.meta.env.VITE_API_BASE || '';
const fullUrl = logo.startsWith('http') || logo.startsWith('data:') ? logo : `${base}${logo.startsWith('/') ? '' : '/'}${logo}`;
faviconLink.href = fullUrl;
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(() => {
// 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(() => {
getMenuConfigs().then(data => {
const items = data.filter((m: any) => m.is_active !== false && m.isActive !== false);
setMenuItems(items);
}).catch(() => { });
loadCreditCatalog();
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]);
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,
title: savedOrder.title || '会员服务',
});
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();
}, []);
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: 'invoice', 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 === 'invoice') { navigate('/invoice'); }
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 stopPolling = useCallback(() => {
if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null;
}
if (countdownTimerRef.current) {
clearInterval(countdownTimerRef.current);
countdownTimerRef.current = null;
}
}, []);
const handleDirectPurchase = useCallback(async (
productId: string,
product: { name?: string; price?: number; currentPrice?: number; monthlyGrantCredits?: number | string; credits?: number; grantCredits?: number; regularPrice?: number; originalPrice?: number; billingCycle?: string; tierCode?: string; description?: string; }
) => {
if (!enabledMethods.alipay && !enabledMethods.wechat) {
message.error('暂无可用支付方式');
return;
}
setPaying(true);
try {
const order = await createRechargeOrder(productId, paymentMethod);
const qrCode = order.qrUrl || order.codeUrl || order.qr_code || order.code_url;
const finalPrice = Number(order.amount ?? product.currentPrice ?? product.price ?? 0);
const credits = Number(product.grantCredits || product.monthlyGrantCredits || 0);
const originalPrice = product.regularPrice ?? product.originalPrice;
const cycleMap: Record = { monthly: '月套餐', quarterly: '季套餐', yearly: '年套餐' };
const isSubscription = !!product.billingCycle;
const periodLabel = product.billingCycle ? cycleMap[product.billingCycle] : '';
const tierName = product.name || '积分充值';
const displayTitle = isSubscription ? `${tierName} ${periodLabel}` : tierName;
const paymentInfo = {
price: finalPrice,
credits,
qrCode,
method: order.paymentMethod,
title: displayTitle,
originalPrice: originalPrice ? Number(originalPrice) : undefined,
discountAmount: originalPrice ? Number(originalPrice) - finalPrice : undefined,
tierName,
periodLabel,
isSubscription,
};
if ((order.paymentMethod === 'alipay' || order.paymentMethod === 'wechat') && qrCode) {
setCurrentPaymentInfo(paymentInfo);
setCreditsModalOpen(false);
setQrCodeModalOpen(true);
currentOrderNoRef.current = order.orderNo;
localStorage.setItem(PENDING_ORDER_KEY, JSON.stringify({
orderNo: order.orderNo,
...paymentInfo,
createdAt: order.createdAt || new Date().toISOString(),
timeoutSeconds: 180,
}));
startPolling(order.orderNo);
} else {
message.success('购买成功,积分已到账');
await useAuthStore.getState().refreshUser();
await loadCreditCatalog();
setSelectedPlan(null);
}
} catch (err: any) {
message.error(err?.message || '创建订单失败');
} finally {
setPaying(false);
}
}, [paymentMethod, enabledMethods]);
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();
loadCreditCatalog();
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;
}, [loadCreditCatalog, stopPolling]);
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 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 (
setBannerVisible(v)} />
{
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 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 && setContactModalOpen(true) }} />}
setMobileMenuOpen(true)}>
{siteName}
setRechargeModalOpen(true)}>
{user?.credits || 0}
{isMobile && setContactModalOpen(true) }} />}
{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}
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('两次密码不一致'));
},
}),
]}>
} />
{/* 充值全屏弹窗 */}
setRechargeModalOpen(false)}
footer={null}
width="100%"
centered={false}
style={{ top: 0, maxWidth: '100vw', margin: 0, paddingBottom: 0 }}
styles={{
body: { padding: 0, height: '100vh', overflow: 'auto', scrollbarWidth: 'none', msOverflowStyle: 'none' },
mask: { background: 'rgba(0,0,0,0.45)' },
header: { display: 'none' },
}}
closable
className="recharge-fullscreen-modal"
title={null}
closeIcon={}
>
{/* 订阅套餐区 */}
订阅套餐
订阅积分按自然月逐月发放;有效订阅只能升级同周期更高等级套餐,不能提前续费。
{currentSubscription && (
当前订阅:{currentSubscription.tierCode || '订阅套餐'}
{currentSubscription.billingCycle === 'monthly' ? '月' : currentSubscription.billingCycle === 'quarterly' ? '季' : '年'}
已发放 {currentSubscription.grantedCount}/{currentSubscription.grantCount} 期
· 每月 {Number(currentSubscription.monthlyGrantCredits || 0).toLocaleString()} 积分
订阅到期:{new Date(currentSubscription.expiresAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false })}
)}
{ setSelectedPeriod(e.target.value); setSelectedPlan(null); }} buttonStyle="solid">
月套餐
季套餐
年套餐
{subscriptionProducts.filter((item) => item.billingCycle === selectedPeriod).length === 0 ? (
暂无可订阅套餐,请联系管理员配置并上架套餐。
) : (
{subscriptionProducts.filter((item) => item.billingCycle === selectedPeriod).map((product, index) => {
const gradient = MEMBERSHIP_GRADIENTS[index % MEMBERSHIP_GRADIENTS.length];
const originalPrice = product.regularPrice || product.price;
const currentPrice = product.currentPrice ?? product.price;
const hasDiscount = originalPrice && Number(originalPrice) > Number(currentPrice);
const creditsLabel = product.monthlyGrantCredits
? `${Number(product.monthlyGrantCredits).toLocaleString()} 积分/月`
: `${Number(product.grantCredits || 0).toLocaleString()} 积分`;
return (
{product.priceType === 'first_purchase' && (
首充价
)}
{product.priceType === 'activity' && (
活动价
)}
{gradient.icon}
{product.name}
{product.description || product.tierCode}
¥{currentPrice}
{hasDiscount && (
¥{originalPrice}
)}
{product.canUpgrade && (
升级价
)}
{product.canUpgrade && Number(product.deductionAmount || 0) > 0 && (
目标套餐¥{product.targetPrice},抵扣未生效¥{product.deductionAmount}
)}
{creditsLabel}
共 {product.grantCount} 次
{product.unavailableReason && (
{product.unavailableReason}
)}
{product.features && product.features.length > 0 && (
{product.features.map((feature: string, fi: number) => (
{feature}
))}
)}
{(product.priceType === 'first_purchase' || product.priceType === 'activity') && (
限时活动
{product.features?.slice(0, 3).map((feature: string, fi: number) => (
· {feature}
))}
)}
);
})}
)}
{/* 积分充值入口 */}
积分充值
当前积分:{user?.credits ?? 0}
}
onClick={() => setCreditsModalOpen(true)}
style={{
borderRadius: 10,
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
border: 'none',
height: 42,
paddingLeft: 20,
paddingRight: 20,
}}
>
积分商城
{/* 常见问题 - 浅色背景 */}
常见问题
isActive ? : }
style={{ background: '#fff', border: 'none' }}
items={FAQ_ITEMS.map(item => ({
key: item.key,
label: {item.label},
children: item.children,
style: { background: '#fff', borderBottom: '1px solid #e8eaed' },
}))}
/>
{/* 联系客服按钮 */}
{ setContactModalOpen(true); }}
onMouseEnter={(e) => { e.currentTarget.style.background = '#f8fafc'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = '#fff'; }}
>
联系客服
{/* 积分商城弹窗 */}
积分商城}
open={creditsModalOpen}
onCancel={() => setCreditsModalOpen(false)}
footer={null}
width={720}
closable
styles={{ body: { padding: 20 } }}
>
当前积分:
{user?.credits ?? 0}
{/*
{enabledMethods.alipay && (
setPaymentMethod('alipay')} style={{ flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px', border: paymentMethod === 'alipay' ? '2px solid #1677ff' : '1px solid #e8eaed', color: paymentMethod === 'alipay' ? '#1677ff' : '#64748b', cursor: 'pointer', background: paymentMethod === 'alipay' ? '#f0f7ff' : '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, transition: 'all 0.2s' }}>
支付宝
)}
{enabledMethods.wechat && (
setPaymentMethod('wechat')} style={{ flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px', border: paymentMethod === 'wechat' ? '2px solid #07c160' : '1px solid #e8eaed', color: paymentMethod === 'wechat' ? '#07c160' : '#64748b', cursor: 'pointer', background: paymentMethod === 'wechat' ? '#f0fff4' : '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, transition: 'all 0.2s' }}>
微信支付
)}
*/}
{rechargeOptions.length === 0 &&
暂无可购买的积分增值包
}
{rechargeOptions.map((opt, idx) => {
const g = GRADIENTS[idx % GRADIENTS.length];
const totalCredits = Number(opt.grantCredits || 0);
const currentPrice = opt.currentPrice || opt.price;
return (
{opt.description && (
{opt.description}
)}
{g.icon}
{opt.name}
{totalCredits.toLocaleString()} 积分
¥{currentPrice}
有效期 {Number(opt.validityMonths || 1)} 个月
);
})}
{/* QR 支付弹窗 */}
{
stopPolling();
if (currentOrderNoRef.current) {
try { await cancelPaymentOrder(currentOrderNoRef.current); } catch { }
currentOrderNoRef.current = null;
}
localStorage.removeItem(PENDING_ORDER_KEY);
setQrCodeModalOpen(false);
setCurrentPaymentInfo(null);
}}
footer={
}
width={700}
closable
title={
开通{currentPaymentInfo?.title || '会员服务'}{currentPaymentInfo?.credits ? ` ${Number(currentPaymentInfo.credits).toLocaleString()} 积分` : ''}
}
styles={{
body: { padding: 0, borderRadius: 16, overflow: 'hidden' },
footer: { borderRadius: 16, borderTop: '1px solid #f0f0f0', padding: '16px 24px' },
}}
>
商品信息
{currentPaymentInfo?.tierName || currentPaymentInfo?.title || '积分充值'}
{currentPaymentInfo?.periodLabel && (
{currentPaymentInfo.periodLabel}
)}
¥{currentPaymentInfo?.price || 0}
{currentPaymentInfo?.originalPrice && Number(currentPaymentInfo.originalPrice) > Number(currentPaymentInfo?.price || 0) && (
¥{currentPaymentInfo.originalPrice}
)}
订单明细
积分数量
{currentPaymentInfo?.credits != null ? `${Number(currentPaymentInfo.credits).toLocaleString()} 积分` : '-'}
续费信息
{currentPaymentInfo?.isSubscription ? '次年按原价 可随时取消' : '一次性购买'}
商品原价
¥{currentPaymentInfo?.originalPrice || currentPaymentInfo?.price || 0}.00
应付金额
¥{currentPaymentInfo?.price || 0}.00
{currentPaymentInfo?.method === 'alipay'
?
:
}
请用{currentPaymentInfo?.method === 'alipay' ? '支付宝' : '微信'}扫码支付
{currentPaymentInfo?.qrCode && (
)}
{countdown}秒后二维码失效
开通即代表同意
《用户服务协议》
联系我们
联系我们}
open={contactModalOpen}
onCancel={() => { setContactModalOpen(false); contactForm.resetFields(); }}
footer={null}
width={480}
className="contact-modal"
>
);
};
export default AppLayout;