fix(front): restore grouped sidebar with IM Bot section (channels + wechat-bot); normalize brand color #5B5FED

This commit is contained in:
qyyfw committed 2026-07-13 23:35:11 +08:00
1 parent 9c684076f3
commit 2847289921
2 files changed
+254 -165

No files matched your search

+71 -19
View File
@@ -1,16 +1,18 @@
/* ===== Front Layout ===== */ /* ===== Front Layout V3 - Light Theme ===== */
.front-layout { .front-layout {
display: flex; display: flex;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
background: #F5F7FC;
color: #1A1F36;
} }
/* Sidebar */ /* Sidebar */
.front-sidebar { .front-sidebar {
width: var(--sidebar-width); width: var(--sidebar-width);
min-width: var(--sidebar-width); min-width: var(--sidebar-width);
background: var(--color-sidebar-bg); background: linear-gradient(180deg, #FFFFFF 0%, #FAFBFE 100%);
border-right: 1px solid var(--color-border-light); border-right: 1px solid rgba(0, 0, 0, 0.08);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transition: width var(--transition-slow), min-width var(--transition-slow); transition: width var(--transition-slow), min-width var(--transition-slow);
@@ -29,7 +31,7 @@
justify-content: space-between; justify-content: space-between;
padding: var(--space-lg) var(--space-lg); padding: var(--space-lg) var(--space-lg);
height: var(--header-height); height: var(--header-height);
border-bottom: 1px solid var(--color-border-light); border-bottom: 1px solid rgba(0, 0, 0, 0.08);
} }
.sidebar-logo { .sidebar-logo {
@@ -41,7 +43,7 @@
.sidebar-brand { .sidebar-brand {
font-size: var(--text-lg); font-size: var(--text-lg);
font-weight: 700; font-weight: 700;
color: var(--color-text-primary); color: #1A1F36;
letter-spacing: -0.02em; letter-spacing: -0.02em;
white-space: nowrap; white-space: nowrap;
} }
@@ -53,13 +55,14 @@
width: 32px; width: 32px;
height: 32px; height: 32px;
border-radius: var(--radius-md); border-radius: var(--radius-md);
color: var(--color-text-tertiary); color: #8892B0;
transition: all var(--transition-fast); transition: all var(--transition-fast);
cursor: pointer;
} }
.collapse-btn:hover { .collapse-btn:hover {
background: var(--color-bg-hover); background: rgba(0, 0, 0, 0.08);
color: var(--color-text-primary); color: #8892B0;
} }
/* Navigation */ /* Navigation */
@@ -68,7 +71,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
padding: var(--space-sm) var(--space-sm); padding: var(--space-sm);
overflow-y: auto; overflow-y: auto;
} }
@@ -77,14 +80,23 @@
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
} }
.nav-group-title {
padding: 14px var(--space-md) 6px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #9CA3AF;
}
.nav-item { .nav-item {
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-md); gap: var(--space-md);
padding: var(--space-md) var(--space-md); padding: 10px var(--space-md);
border-radius: var(--radius-md); border-radius: var(--radius-md);
color: var(--color-text-secondary); color: #8892B0;
text-decoration: none; text-decoration: none;
font-size: var(--text-base); font-size: var(--text-base);
font-weight: 500; font-weight: 500;
@@ -93,13 +105,13 @@
} }
.nav-item:hover { .nav-item:hover {
background: var(--color-sidebar-hover); background: rgba(91, 95, 237, 0.06);
color: var(--color-text-primary); color: #1A1F36;
} }
.nav-item.active { .nav-item.active {
background: var(--color-sidebar-active); background: rgba(91, 95, 237, 0.12);
color: var(--color-accent); color: #5B5FED;
} }
.nav-icon { .nav-icon {
@@ -115,10 +127,30 @@
flex: 1; flex: 1;
} }
/* Active dot indicator */
.nav-active-dot {
display: none;
}
.nav-item.active .nav-active-dot {
display: block;
position: absolute;
right: 8px;
width: 6px;
height: 6px;
border-radius: 50%;
background: #5B5FED;
box-shadow: 0 0 8px rgba(91, 95, 237, 0.5);
}
/* Sidebar Footer */ /* Sidebar Footer */
.sidebar-footer { .sidebar-footer {
padding: var(--space-md); padding: var(--space-md);
border-top: 1px solid var(--color-border-light); border-top: 1px solid rgba(0, 0, 0, 0.08);
}
.sidebar-footer .nav-item {
margin-top: 2px;
} }
.user-info-mini { .user-info-mini {
@@ -126,13 +158,14 @@
align-items: center; align-items: center;
gap: var(--space-md); gap: var(--space-md);
padding: var(--space-sm); padding: var(--space-sm);
margin-bottom: 4px;
} }
.user-avatar-sm { .user-avatar-sm {
width: 32px; width: 32px;
height: 32px; height: 32px;
border-radius: var(--radius-full); border-radius: var(--radius-full);
background: linear-gradient(135deg, var(--color-accent), #8185F5); background: linear-gradient(135deg, #5B5FED, #4A4ECC);
color: white; color: white;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -151,16 +184,21 @@
.user-name { .user-name {
font-size: var(--text-sm); font-size: var(--text-sm);
font-weight: 600; font-weight: 600;
color: var(--color-text-primary); color: #1A1F36;
line-height: 1.3; line-height: 1.3;
} }
.user-plan { .user-plan {
font-size: var(--text-xs); font-size: var(--text-xs);
color: var(--color-accent); color: #5B5FED;
line-height: 1.3; line-height: 1.3;
} }
.logout-btn:hover {
color: #EF4444 !important;
background: rgba(239, 68, 68, 0.1) !important;
}
/* Main Content */ /* Main Content */
.front-main { .front-main {
flex: 1; flex: 1;
@@ -168,6 +206,12 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
background: #F5F7FC;
}
/* Forward-slash route for apps - hidden default route */
.front-main > * {
height: 100%;
} }
/* Responsive */ /* Responsive */
@@ -187,3 +231,11 @@
transform: translateX(0); transform: translateX(0);
} }
} }
/* Home & Admin links */
.nav-home-link {
margin-bottom: 4px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
padding-bottom: 8px;
}
+183 -146
View File
@@ -1,146 +1,183 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Outlet, NavLink, useNavigate } from 'react-router-dom'; import { Outlet, NavLink, useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import './FrontLayout.css'; import './FrontLayout.css';
const navItems = [ const navGroups = [
{ path: '/chat', icon: 'chat', label: 'AI聊天' }, {
{ path: '/channels', icon: 'channel', label: '频道' }, title: '智能体',
{ path: '/knowledge', icon: 'knowledge', label: '知识库' }, items: [
{ path: '/market', icon: 'market', label: '应用市场' }, { path: '/chat', icon: 'chat', label: 'AI聊天' },
{ path: '/token', icon: 'token', label: 'Token驾驶舱' }, { path: '/knowledge', icon: 'knowledge', label: '知识库' },
{ path: '/pricing', icon: 'pricing', label: '付费中心' }, { path: '/market', icon: 'market', label: '技能市场' },
{ path: '/assets', icon: 'assets', label: '资产' }, { path: '/evolution', icon: 'evolution', label: 'AI自进化' },
{ path: '/evolution', icon: 'evolution', label: 'AI进化' }, ],
]; },
{
const bottomItems = [ title: 'IM Bot',
{ path: '/profile', icon: 'profile', label: '个人中心' }, items: [
{ path: '/notifications', icon: 'notify', label: '消息通知' }, { path: '/channels', icon: 'channel', label: '频道管理' },
]; { path: '/wechat-bot', icon: 'wechat', label: '微信机器人' },
],
const iconPaths: Record<string, JSX.Element> = { },
chat: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>, {
channel: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>, title: '资源中心',
knowledge: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>, items: [
market: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></svg>, { path: '/assets', icon: 'assets', label: '资产文件' },
token: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>, { path: '/token', icon: 'token', label: 'Token驾驶舱' },
pricing: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>, { path: '/pricing', icon: 'pricing', label: '付费中心' },
profile: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>, ],
notify: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>, },
assets: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>, ];
evolution: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>,
}; const bottomItems = [
{ path: '/profile', icon: 'profile', label: '个人中心' },
const FrontLayout: React.FC = () => { { path: '/notifications', icon: 'notify', label: '消息通知' },
const { user, logout } = useAuth(); ];
const navigate = useNavigate();
const [collapsed, setCollapsed] = useState(false); const iconPaths: Record<string, JSX.Element> = {
chat: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>,
if (!user) { channel: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>,
navigate('/login'); knowledge: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>,
return null; market: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></svg>,
} apps: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="9" height="9" rx="2"/><rect x="13" y="2" width="9" height="9" rx="2"/><rect x="2" y="13" width="9" height="9" rx="2"/><rect x="13" y="13" width="5" height="5" rx="1.5"/><path d="M18 18v3"/><path d="M19.5 16.5v6"/></svg>,
token: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>,
return ( pricing: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>,
<div className="front-layout"> profile: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>,
{/* Sidebar */} notify: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>,
<aside className={`front-sidebar ${collapsed ? 'collapsed' : ''}`}> assets: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>,
<div className="sidebar-header"> evolution: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>,
<div className="sidebar-logo"> wechat: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/><circle cx="9" cy="11" r="1"/><circle cx="15" cy="11" r="1"/></svg>,
<svg width="28" height="28" viewBox="0 0 32 32" fill="none"> };
<rect width="32" height="32" rx="8" fill="#5B5FED"/>
<path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/> const FrontLayout: React.FC = () => {
<path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/> const { user, logout } = useAuth();
</svg> const navigate = useNavigate();
{!collapsed && <span className="sidebar-brand">Qihu-AIFlow</span>} const [collapsed, setCollapsed] = useState(false);
</div>
<button className="collapse-btn" onClick={() => setCollapsed(!collapsed)}> if (!user) {
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> navigate('/login');
<line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/> return null;
</svg> }
</button>
</div> return (
<div className="front-layout">
<nav className="sidebar-nav"> {/* Sidebar */}
<div className="nav-section"> <aside className={`front-sidebar ${collapsed ? 'collapsed' : ''}`}>
{navItems.map(item => ( {/* Header */}
<NavLink <div className="sidebar-header">
key={item.path} <div className="sidebar-logo">
to={item.path} <svg width="32" height="32" viewBox="0 0 32 32" fill="none">
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`} <defs>
> <linearGradient id="flGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<span className="nav-icon">{iconPaths[item.icon]}</span> <stop offset="0%" stopColor="#5B5FED"/>
{!collapsed && <span className="nav-label">{item.label}</span>} <stop offset="100%" stopColor="#4A4ECC"/>
</NavLink> </linearGradient>
))} </defs>
</div> <rect width="32" height="32" rx="8" fill="url(#flGrad)"/>
<div className="nav-section nav-bottom"> <path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
{bottomItems.map(item => ( <path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
<NavLink </svg>
key={item.path} {!collapsed && <span className="sidebar-brand">七虎AI</span>}
to={item.path} </div>
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`} <button className="collapse-btn" onClick={() => setCollapsed(!collapsed)}>
> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<span className="nav-icon">{iconPaths[item.icon]}</span> {collapsed ? <><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></> : <><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></>}
{!collapsed && <span className="nav-label">{item.label}</span>} </svg>
</NavLink> </button>
))} </div>
</div>
</nav> {/* Navigation */}
<nav className="sidebar-nav">
<div className="sidebar-footer"> <NavLink to="/" className={({ isActive }) => `nav-item nav-home-link`} end>
<div className="user-info-mini"> <span className="nav-item-icon">
<div className="user-avatar-sm"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
{user.username.charAt(0).toUpperCase()} </span>
</div> {!collapsed && <span className="nav-item-label">返回首页</span>}
{!collapsed && ( </NavLink>
<div className="user-details"> {navGroups.map(group => (
<span className="user-name">{user.username}</span> <div className="nav-section" key={group.title}>
<span className="user-plan">{user.membership?.planName || '免费版'}</span> {!collapsed && <div className="nav-group-title">{group.title}</div>}
</div> {group.items.map(item => (
)} <NavLink
</div> key={item.path}
{user.role === 'admin' && ( to={item.path}
<NavLink className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
to="/admin" title={collapsed ? item.label : undefined}
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`} >
title="返回后台" <span className="nav-icon">{iconPaths[item.icon]}</span>
> {!collapsed && <span className="nav-label">{item.label}</span>}
<span className="nav-icon"> {!collapsed && <span className="nav-active-dot" />}
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"> </NavLink>
<rect x="3" y="3" width="7" height="7" rx="1.2"/> ))}
<rect x="14" y="3" width="7" height="7" rx="1.2"/> </div>
<rect x="3" y="14" width="7" height="7" rx="1.2"/> ))}
<rect x="14" y="14" width="7" height="7" rx="1.2"/> <div className="nav-section nav-bottom">
</svg> {bottomItems.map(item => (
</span> <NavLink
{!collapsed && <span className="nav-label">返回后台</span>} key={item.path}
</NavLink> to={item.path}
)} className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
<button title={collapsed ? item.label : undefined}
className="nav-item logout-btn" >
onClick={() => { logout(); navigate('/login'); }} <span className="nav-icon">{iconPaths[item.icon]}</span>
title="退出登录" {!collapsed && <span className="nav-label">{item.label}</span>}
> {!collapsed && <span className="nav-active-dot" />}
<span className="nav-icon"> </NavLink>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"> ))}
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/> </div>
<polyline points="16 17 21 12 16 7"/> </nav>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg> {/* Footer */}
</span> <div className="sidebar-footer">
{!collapsed && <span className="nav-label">退出登录</span>} <div className="user-info-mini">
</button> <div className="user-avatar-sm">
</div> {user.username.charAt(0).toUpperCase()}
</aside> </div>
{!collapsed && (
{/* Main Content */} <div className="user-details">
<main className="front-main"> <span className="user-name">{user.username}</span>
<Outlet /> <span className="user-plan">{user.membership?.planName || '免费版'}</span>
</main> </div>
</div> )}
); </div>
}; {user.role === 'admin' && (
<NavLink
export default FrontLayout; to="/admin"
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
title="管理后台"
>
<span className="nav-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
</span>
{!collapsed && <span className="nav-label">管理后台</span>}
</NavLink>
)}
<button
className="nav-item logout-btn"
onClick={() => { logout(); navigate('/login'); }}
title="退出登录"
>
<span className="nav-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
</span>
{!collapsed && <span className="nav-label">退出登录</span>}
</button>
</div>
</aside>
{/* Main Content */}
<main className="front-main">
<Outlet />
</main>
</div>
);
};
export default FrontLayout;