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 ? ( 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 && setContactModalOpen(true) }} />}
setMobileMenuOpen(true)}>
{siteName}
setRechargeModalOpen(true)}> {user?.credits || 0}
{isMobile && setContactModalOpen(true) }} />}
{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('两次密码不一致')); }, }), ]}> } />
{/* 充值全屏弹窗 */} 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}
{/* 常见问题 - 浅色背景 */}
常见问题
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;