调整前后台导航样式
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user