调整前后台导航样式

This commit is contained in:
2026-06-16 12:41:21 +08:00
parent e78a1cd65f
commit 496759ccdf
4 changed files with 242 additions and 155 deletions
File diff suppressed because one or more lines are too long
+21 -1
View File
@@ -8,7 +8,27 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<title>后台管理</title>
<script type="module" crossorigin src="/assets/index-ECGzn5Wr.js"></script>
<script>
(function() {
var cached = localStorage.getItem('siteInfo');
if (cached) {
try {
var info = JSON.parse(cached);
if (info.siteName) {
document.title = info.siteName + ' - 管理后台';
}
if (info.siteLogo) {
var link = document.querySelector('link[rel="icon"]');
if (link) {
link.href = info.siteLogo;
link.type = 'image/png';
}
}
} catch (e) {}
}
})();
</script>
<script type="module" crossorigin src="/assets/index-C5wDengw.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
</head>
<body>
+88 -30
View File
@@ -49,7 +49,6 @@ const AdminLayout: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const { user, loading, logout } = useAdminStore();
const [collapsed, setCollapsed] = useState(false);
const [menuItems, setMenuItems] = useState<any[]>([]);
const [pwdModal, setPwdModal] = useState(false);
const [pwdForm] = Form.useForm();
@@ -204,42 +203,61 @@ const AdminLayout: React.FC = () => {
});
return (
<Layout style={{ minHeight: '100vh' }}>
<Layout style={{ minHeight: '100vh', background: '#f1f5f9' }}>
<Sider
collapsible
collapsed={collapsed}
onCollapse={setCollapsed}
width={220}
width={240}
theme="light"
style={{
background: '#ffffff',
borderRight: '1px solid #e5e7eb',
position: 'fixed',
left: 16,
top: 16,
bottom: 16,
width: 240,
background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)',
borderRadius: '20px',
boxShadow: '0 4px 32px rgba(0, 0, 0, 0.06), 0 1px 8px rgba(0, 0, 0, 0.04)',
border: '1px solid rgba(0, 0, 0, 0.06)',
zIndex: 100,
}}
>
{/* Logo */}
<div style={{
height: 64, display: 'flex', alignItems: 'center',
justifyContent: 'center', gap: 10,
borderBottom: '1px solid #e5e7eb',
height: 80,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 12,
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.06) 0%, rgba(139, 92, 246, 0.04) 100%)',
}}>
<div style={{
width: 32, height: 32, borderRadius: 8,
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: '0 2px 8px rgba(99,102,241,0.3)',
width: 42,
height: 42,
borderRadius: 14,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a78bfa 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.35)',
overflow: 'hidden',
}}>
{siteLogo ? (
<img src={siteLogo} alt="logo" style={{ width: 24, height: 24, objectFit: 'contain' }} />
<img src={siteLogo} alt="logo" style={{ width: 28, height: 28, objectFit: 'contain' }} />
) : (
<ThunderboltOutlined style={{ fontSize: 16, color: '#fff' }} />
<ThunderboltOutlined style={{ fontSize: 20, color: '#ffffff' }} />
)}
</div>
{!collapsed && (
<span style={{ color: '#1f2937', fontSize: 15, fontWeight: 600, letterSpacing: -0.02 }}>
<span style={{
color: '#1e293b',
fontSize: 17,
fontWeight: 700,
letterSpacing: -0.02,
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
}}>
{siteName}
</span>
)}
</div>
{/* Menu */}
@@ -263,15 +281,26 @@ const AdminLayout: React.FC = () => {
</Sider>
<Layout>
<Layout style={{ marginLeft: 272, marginTop: 16, marginRight: 16, marginBottom: 16, background: 'transparent' }}>
{/* Header */}
<div style={{
height: 56, background: '#fff', borderBottom: '1px solid #f0f0f5',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '0 24px',
height: 72,
background: '#ffffff',
borderRadius: '16px 16px 0 0',
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.04)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 32px',
fontFamily: 'var(--font-sans)',
border: '1px solid rgba(0, 0, 0, 0.06)',
borderBottom: 'none',
}}>
<Typography.Text strong style={{ fontSize: 16 }}>
<Typography.Text strong style={{
fontSize: 18,
color: '#1e293b',
fontWeight: 600,
}}>
{antMenuItems.find(m => m.key === activeKey)?.label
|| antMenuItems.flatMap(m => m.children || []).find((c: any) => c.key === activeKey)?.label
|| '管理后台'}
@@ -288,10 +317,31 @@ const AdminLayout: React.FC = () => {
if (key === 'changePwd') { setPwdModal(true); pwdForm.resetFields(); }
},
}} placement="bottomRight" arrow>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', padding: '4px 8px', borderRadius: 8, transition: 'background 0.2s' }}>
<Avatar size={28} icon={<UserOutlined />}
style={{ background: 'linear-gradient(135deg, #6366f1, #8b5cf6)' }} />
<Typography.Text style={{ fontSize: 13, fontWeight: 500 }}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 10,
cursor: 'pointer',
padding: '8px 12px',
borderRadius: 12,
transition: 'all 0.25s ease',
background: 'linear-gradient(135deg, rgba(248, 250, 252, 0.9) 0%, rgba(241, 245, 249, 0.9) 100%)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#ffffff';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.08)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(248, 250, 252, 0.9) 0%, rgba(241, 245, 249, 0.9) 100%)';
e.currentTarget.style.boxShadow = 'none';
}}
>
<Avatar size={32} icon={<UserOutlined />}
style={{
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)',
}} />
<Typography.Text style={{ fontSize: 14, fontWeight: 600, color: '#1e293b' }}>
{user?.username}
</Typography.Text>
</div>
@@ -299,7 +349,15 @@ const AdminLayout: React.FC = () => {
</div>
{/* Content */}
<Content style={{ padding: 24, background: '#f5f6fa', overflow: 'auto' }}>
<Content style={{
padding: 32,
background: '#ffffff',
borderRadius: '0 0 16px 16px',
boxShadow: '0 4px 24px rgba(0, 0, 0, 0.06)',
overflow: 'auto',
border: '1px solid rgba(0, 0, 0, 0.06)',
borderTop: 'none',
}}>
{antMenuItems.length === 0 && !isAdminUser ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '60vh', color: '#94a3b8' }}>
<LockOutlined style={{ fontSize: 48, marginBottom: 16, color: '#cbd5e1' }} />
+132 -123
View File
@@ -115,8 +115,7 @@ const iconMap: Record<string, React.ReactNode> = {
CloudServerOutlined: <CloudServerOutlined />,
};
const EXPANDED_W = 240;
const COLLAPSED_W = 68;
const SIDEBAR_W = 240;
const GRADIENTS = [
{ gradient: 'linear-gradient(135deg, #c9a96e, #a67c52)', shadow: 'rgba(201,169,110,0.25)', icon: <StarFilled /> },
@@ -132,8 +131,6 @@ const AppLayout: React.FC = () => {
const [pwdModalOpen, setPwdModalOpen] = useState(false);
const [rechargeModalOpen, setRechargeModalOpen] = useState(false);
const [pwdForm] = Form.useForm();
const [collapsed, setCollapsed] = useState(false);
const [toggleHover, setToggleHover] = useState(false);
const [selectedPlan, setSelectedPlan] = useState<number | null>(null);
const [menuItems, setMenuItems] = useState<MenuConfig[]>([]);
const [rechargeOptions, setRechargeOptions] = useState<any[]>([]);
@@ -174,14 +171,7 @@ const AppLayout: React.FC = () => {
// LocalStorage keys
const PENDING_ORDER_KEY = 'pending_payment_order';
// 监听预览弹窗状态,关闭浮动按钮
useEffect(() => {
const handleModalOpen = () => {
setToggleHover(false);
};
window.addEventListener('previewOpen', handleModalOpen);
return () => window.removeEventListener('previewOpen', handleModalOpen);
}, []);
useEffect(() => {
getSiteInfo().then(info => {
@@ -298,7 +288,7 @@ const AppLayout: React.FC = () => {
}, [user]);
const selectedKey = location.pathname.startsWith('/records') ? '/records' : location.pathname;
const sidebarW = collapsed ? COLLAPSED_W : EXPANDED_W;
const sidebarW = SIDEBAR_W;
const userMenuItems = [
{ key: 'profile', icon: <UserOutlined />, label: `账号: ${user?.username}`, disabled: true },
@@ -406,43 +396,47 @@ const AppLayout: React.FC = () => {
<Layout style={{ minHeight: '100vh' }}>
{/* Desktop Sidebar */}
<div className="desktop-sidebar" style={{
width: sidebarW, position: 'fixed', left: 0, top: 0, bottom: 0, zIndex: 100,
background: 'var(--nav-bg)',
borderRight: '1px solid var(--nav-border)',
width: sidebarW, position: 'fixed', left: 16, top: 16, bottom: 16, zIndex: 100,
background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)',
borderRadius: '20px',
boxShadow: '0 4px 32px rgba(0, 0, 0, 0.06), 0 1px 8px rgba(0, 0, 0, 0.04)',
display: 'flex', flexDirection: 'column',
transition: 'width 0.25s ease',
transition: 'width 0.25s ease, left 0.25s ease',
overflow: 'hidden',
border: '1px solid rgba(0, 0, 0, 0.06)',
}}>
{/* Logo + Toggle */}
{/* Logo */}
<div style={{
height: 72, display: 'flex', alignItems: 'center',
justifyContent: collapsed ? 'center' : 'space-between',
borderBottom: '1px solid #e5e7eb',
padding: collapsed ? '0' : '0 16px 0 20px',
height: 80, display: 'flex', alignItems: 'center',
justifyContent: 'flex-start',
padding: '0 20px',
flexShrink: 0,
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.06) 0%, rgba(139, 92, 246, 0.04) 100%)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, overflow: 'hidden' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
<div style={{
width: 38, height: 38, borderRadius: 12, flexShrink: 0,
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
width: 42, height: 42, borderRadius: 14, flexShrink: 0,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a78bfa 100%)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: '0 2px 12px rgba(99,102,241,0.3)', overflow: 'hidden',
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.35)',
overflow: 'hidden',
}}>
{siteLogo ? (
<img src={siteLogo} alt="logo" style={{ width: 28, height: 28, objectFit: 'contain' }} />
) : (
<ThunderboltOutlined style={{ fontSize: 18, color: '#1f2937' }} />
<ThunderboltOutlined style={{ fontSize: 20, color: '#ffffff' }} />
)}
</div>
{!collapsed && (
<span style={{
color: 'var(--nav-text)', fontSize: 16, fontWeight: 600, letterSpacing: -0.02,
whiteSpace: 'nowrap', opacity: collapsed ? 0 : 1,
transition: 'opacity 0.2s ease',
}}>
{siteName}
</span>
)}
<span style={{
color: '#1e293b', fontSize: 17, fontWeight: 700, letterSpacing: -0.02,
whiteSpace: 'nowrap',
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
}}>
{siteName}
</span>
</div>
</div>
@@ -468,43 +462,42 @@ const AppLayout: React.FC = () => {
const renderMenuItem = (item: MenuConfig, depth: number = 0) => {
const isActive = item.path === selectedKey;
const menuIcon = iconMap[item.icon] || <HomeOutlined />;
const el = (
return (
<div key={item.id} onClick={() => item.path && navigate(item.path)} style={{
display: 'flex', alignItems: 'center',
justifyContent: collapsed ? 'center' : 'flex-start',
gap: collapsed ? 0 : 12,
padding: collapsed ? '10px 0' : depth > 0 ? '7px 14px 7px 32px' : '9px 16px',
borderRadius: 8, margin: '1px 0', cursor: 'pointer',
fontSize: depth > 0 ? 12 : 13, fontWeight: isActive ? 500 : 400,
color: isActive ? 'var(--nav-text)' : 'var(--nav-text-muted)',
background: isActive ? 'var(--nav-active)' : 'transparent',
justifyContent: 'flex-start',
gap: 12,
padding: depth > 0 ? '8px 14px 8px 36px' : '10px 16px',
borderRadius: 12, margin: '2px 6px', cursor: 'pointer',
fontSize: depth > 0 ? 13 : 14, fontWeight: isActive ? 600 : 400,
color: isActive ? '#4f46e5' : '#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',
}}>
<span style={{ fontSize: depth > 0 ? 14 : 16, flexShrink: 0 }}>{menuIcon}</span>
{!collapsed && <span style={{ whiteSpace: 'nowrap' }}>{item.label}</span>}
<span style={{
fontSize: depth > 0 ? 14 : 16,
flexShrink: 0,
color: isActive ? '#6366f1' : '#64748b',
}}>{menuIcon}</span>
<span style={{ whiteSpace: 'nowrap' }}>{item.label}</span>
</div>
);
return collapsed ? <Tooltip key={item.id} title={item.label} placement="right">{el}</Tooltip> : el;
};
// Render groups with children
groups.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)).forEach(g => {
const children = (childMap[g.id] || []).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
if (!collapsed) {
items.push(
<div key={g.id}>
<div style={{
color: 'var(--nav-text-muted)', fontSize: 10, fontWeight: 500,
padding: '8px 16px 6px', letterSpacing: 0.05, textTransform: 'uppercase',
}}>
{g.label}
</div>
{children.map(c => renderMenuItem(c, 1))}
items.push(
<div key={g.id}>
<div style={{
color: '#94a3b8', fontSize: 12, fontWeight: 600,
padding: '12px 16px 6px', letterSpacing: 0.5, textTransform: 'uppercase',
}}>
{g.label}
</div>
);
} else {
children.forEach(c => items.push(renderMenuItem(c)));
}
{children.map(c => renderMenuItem(c, 1))}
</div>
);
});
// Render top-level pages
@@ -518,91 +511,107 @@ const AppLayout: React.FC = () => {
{/* Recharge button - opens modal */}
<div onClick={() => setRechargeModalOpen(true)} style={{
margin: collapsed ? '4px auto' : '4px 14px',
padding: collapsed ? '10px' : '10px 14px',
borderRadius: 10, cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: collapsed ? 'center' : 'flex-start',
gap: collapsed ? 0 : 10,
color: '#c9a96e', fontSize: 13, fontWeight: 500,
background: 'rgba(201,169,110,0.08)',
border: '1px solid rgba(201,169,110,0.15)',
transition: 'all 0.2s',
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 = 'rgba(201,169,110,0.15)'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(201,169,110,0.08)'; }}
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)';
}}
>
<PlusOutlined style={{ fontSize: 14 }} />
{!collapsed && <span></span>}
<PlusOutlined style={{ fontSize: 16 }} />
<span></span>
</div>
{/* User block at bottom-left */}
<div style={{ padding: collapsed ? '12px 8px' : '14px 14px', borderTop: '1px solid #e5e7eb', flexShrink: 0 }}>
<div style={{ padding: '16px 16px', flexShrink: 0 }}>
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }} placement="topRight" arrow>
<div style={{
display: 'flex', alignItems: 'center',
justifyContent: collapsed ? 'center' : 'flex-start',
gap: collapsed ? 0 : 10,
padding: collapsed ? '8px' : '8px 10px',
borderRadius: 12, cursor: 'pointer', transition: 'background 0.2s',
background: 'rgba(0,0,0,0.02)',
justifyContent: 'flex-start',
gap: 12,
padding: '10px 14px',
borderRadius: 14, cursor: 'pointer',
transition: 'all 0.25s ease',
background: 'linear-gradient(135deg, rgba(248, 250, 252, 0.9) 0%, rgba(241, 245, 249, 0.9) 100%)',
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.04)',
}}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(0,0,0,0.04)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(0,0,0,0.02)'}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)';
e.currentTarget.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.08)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(248, 250, 252, 0.9) 0%, rgba(241, 245, 249, 0.9) 100%)';
e.currentTarget.style.boxShadow = '0 2px 12px rgba(0, 0, 0, 0.04)';
}}
>
<Avatar size={32} icon={<UserOutlined />}
style={{ background: 'linear-gradient(135deg, #4a5568, #2d3748)', flexShrink: 0 }} />
{!collapsed && (
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ color: 'var(--nav-text)', fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: -0.01 }}>
{user?.username}
</div>
<div style={{ color: 'var(--nav-text-muted)', fontSize: 11, letterSpacing: 0 }}>: {user?.credits || 0}</div>
<Avatar size={36} icon={<UserOutlined />}
style={{
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
flexShrink: 0,
boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)',
}} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ color: '#1e293b', fontSize: 14, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: -0.01 }}>
{user?.username}
</div>
)}
<div style={{
color: '#6366f1',
fontSize: 12,
fontWeight: 500,
letterSpacing: 0,
background: 'rgba(99, 102, 241, 0.08)',
padding: '2px 8px',
borderRadius: 6,
display: 'inline-block',
}}>: {user?.credits || 0}</div>
</div>
</div>
</Dropdown>
</div>
</div>
{/* Floating sidebar toggle hover zone */}
<div
className="desktop-sidebar-toggle-zone"
onMouseEnter={() => setToggleHover(true)}
onMouseLeave={() => setToggleHover(false)}
style={{
position: 'fixed', left: sidebarW - 16, top: 0, bottom: 0,
zIndex: 110, width: 32, cursor: 'default',
transition: 'left 0.25s ease',
}}
>
{toggleHover && (
<div onClick={() => setCollapsed(prev => !prev)} style={{
position: 'absolute', left: '50%', top: '50%',
transform: 'translate(-50%, -50%)',
width: 24, height: 56, borderRadius: '0 8px 8px 0',
background: '#f8f9fb', border: '1px solid #e5e7eb', borderLeft: 'none',
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
boxShadow: '2px 0 12px rgba(0,0,0,0.2)',
animation: 'fadeInRight 0.2s ease both',
}}>
{collapsed
? <RightOutlined style={{ color: '#818cf8', fontSize: 12 }} />
: <LeftOutlined style={{ color: '#818cf8', fontSize: 12 }} />}
</div>
)}
</div>
{/* Main Content */}
<div className="desktop-content" style={{
marginLeft: sidebarW, flex: 1, minHeight: '100vh', background: '#f8f9fc',
padding: '24px 32px 32px', transition: 'margin-left 0.25s ease',
marginLeft: sidebarW + 48,
marginRight: 32,
marginTop: 16,
marginBottom: 16,
flex: 1,
minHeight: 'calc(100vh - 32px)',
background: 'transparent',
padding: 0,
transition: 'margin-left 0.25s ease',
}}>
<Outlet />
<div style={{
background: '#ffffff',
borderRadius: '20px',
boxShadow: '0 4px 32px rgba(0, 0, 0, 0.06), 0 1px 8px rgba(0, 0, 0, 0.04)',
minHeight: '100%',
padding: '24px 32px 32px',
border: '1px solid rgba(0, 0, 0, 0.06)',
}}>
<Outlet />
</div>
</div>
{/* Mobile Bottom Nav */}
<div className="mobile-bottom-nav">
{menuItems.map((item) => {
{menuItems.filter((item) => (item.menu_type ?? item.menuType) !== 'group').map((item) => {
if (!item.path) return null;
const isActive = item.path === selectedKey;
return (
<div key={item.id}