- web/: React + Vite + TypeScript 前端 - backend/: Django + DRF + SimpleJWT 后端 - prototype/: HTML 设计原型 - docs/: PRD 和设计评审文档 - test: 单元测试 + E2E 极限测试
87 lines
4.4 KiB
TypeScript
87 lines
4.4 KiB
TypeScript
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
|
|
import { useAuthStore } from '../store/auth';
|
|
import { useState } from 'react';
|
|
import styles from './AdminLayout.module.css';
|
|
|
|
const navItems = [
|
|
{ path: '/admin/dashboard', label: '仪表盘', icon: 'M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z' },
|
|
{ path: '/admin/users', label: '用户管理', icon: 'M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z' },
|
|
{ path: '/admin/records', label: '消费记录', icon: 'M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z' },
|
|
{ path: '/admin/settings', label: '系统设置', icon: 'M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 00.12-.61l-1.92-3.32a.488.488 0 00-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 00-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 00-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z' },
|
|
];
|
|
|
|
export function AdminLayout() {
|
|
const user = useAuthStore((s) => s.user);
|
|
const logout = useAuthStore((s) => s.logout);
|
|
const navigate = useNavigate();
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
|
|
const handleLogout = () => {
|
|
logout();
|
|
navigate('/login', { replace: true });
|
|
};
|
|
|
|
return (
|
|
<div className={styles.layout}>
|
|
<aside className={`${styles.sidebar} ${collapsed ? styles.collapsed : ''}`}>
|
|
<div className={styles.sidebarHeader}>
|
|
<div className={styles.logo}>
|
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="var(--color-primary)">
|
|
<path d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v2h8v-2h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 14H3V5h18v12z"/>
|
|
</svg>
|
|
{!collapsed && <span className={styles.logoText}>Jimeng Admin</span>}
|
|
</div>
|
|
<button className={styles.collapseBtn} onClick={() => setCollapsed(!collapsed)}>
|
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="var(--color-text-secondary)">
|
|
{collapsed ? (
|
|
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
|
|
) : (
|
|
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
|
|
)}
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<nav className={styles.nav}>
|
|
{navItems.map((item) => (
|
|
<NavLink
|
|
key={item.path}
|
|
to={item.path}
|
|
className={({ isActive }) =>
|
|
`${styles.navItem} ${isActive ? styles.navItemActive : ''}`
|
|
}
|
|
>
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
<path d={item.icon} />
|
|
</svg>
|
|
{!collapsed && <span>{item.label}</span>}
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
|
|
<div className={styles.sidebarFooter}>
|
|
<button className={styles.backBtn} onClick={() => navigate('/')}>
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
|
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/>
|
|
</svg>
|
|
{!collapsed && <span>返回首页</span>}
|
|
</button>
|
|
<div className={styles.userInfo}>
|
|
<div className={styles.userAvatar}>{user?.username.charAt(0).toUpperCase()}</div>
|
|
{!collapsed && (
|
|
<div className={styles.userMeta}>
|
|
<span className={styles.userName}>{user?.username}</span>
|
|
<button className={styles.logoutLink} onClick={handleLogout}>退出</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<main className={`${styles.content} ${collapsed ? styles.contentExpanded : ''}`}>
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|