06-高级模式与实战项目——20. 实战项目四:管理后台

📅 2026/7/9 1:30:24
06-高级模式与实战项目——20. 实战项目四:管理后台
20. 实战项目四管理后台概述管理后台是一个完整的后台管理系统涵盖仪表盘、用户管理、权限控制、数据可视化等核心功能。通过这个项目你将掌握认证授权、数据图表、CRUD 操作等高级技能。维度内容What完整管理后台包含仪表盘、用户管理、权限控制Why掌握认证授权、数据可视化When高级实战练习Where完整全栈项目Who需要后台开发经验的开发者How实现登录、用户 CRUD、权限控制、数据图表1. 项目需求1.1 功能列表✅ 用户登录/认证✅ 仪表盘数据统计、图表✅ 用户管理CRUD✅ 角色权限管理✅ 操作日志✅ 系统设置✅ 响应式布局✅ 暗色主题1.2 技术栈React 19TypeScriptReact Router v6Zustand (状态管理)Recharts (图表)Tailwind CSSJSON Server (Mock API)2. 项目结构admin-dashboard/ ├── src/ │ ├── components/ │ │ ├── Layout/ │ │ │ ├── Sidebar.tsx │ │ │ ├── Header.tsx │ │ │ └── Layout.tsx │ │ ├── Charts/ │ │ │ ├── LineChart.tsx │ │ │ ├── BarChart.tsx │ │ │ └── PieChart.tsx │ │ ├── DataTable.tsx │ │ ├── ConfirmModal.tsx │ │ └── ThemeToggle.tsx │ ├── pages/ │ │ ├── LoginPage.tsx │ │ ├── DashboardPage.tsx │ │ ├── UsersPage.tsx │ │ ├── RolesPage.tsx │ │ ├── LogsPage.tsx │ │ └── SettingsPage.tsx │ ├── stores/ │ │ ├── authStore.ts │ │ └── themeStore.ts │ ├── hooks/ │ │ ├── useUsers.ts │ │ └── useRoles.ts │ ├── api/ │ │ └── client.ts │ ├── types/ │ │ └── index.ts │ ├── utils/ │ │ └── permissions.ts │ ├── App.tsx │ └── main.tsx ├── db.json └── package.json3. 代码实现3.1 类型定义// src/types/index.ts export interface User { id: string; username: string; email: string; avatar?: string; role: string; status: active | inactive; createdAt: string; lastLogin?: string; } export interface Role { id: string; name: string; permissions: string[]; description: string; } export interface Permission { id: string; name: string; code: string; module: string; } export interface Log { id: string; userId: string; username: string; action: string; target: string; details: string; ip: string; createdAt: string; } export interface DashboardStats { totalUsers: number; activeUsers: number; totalRoles: number; todayLogins: number; revenueData: { month: string; amount: number }[]; userTrend: { date: string; count: number }[]; }3.2 状态管理// src/stores/authStore.ts import { create } from zustand; import { persist } from zustand/middleware; import { User } from ../types; interface AuthState { user: User | null; token: string | null; isLoading: boolean; error: string | null; login: (username: string, password: string) Promisevoid; logout: () void; hasPermission: (permission: string) boolean; } export const useAuthStore createAuthState()( persist( (set, get) ({ user: null, token: null, isLoading: false, error: null, login: async (username, password) { set({ isLoading: true, error: null }); try { // 模拟登录 API const response await fetch(/api/auth/login, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ username, password }), }); const data await response.json(); if (data.success) { set({ user: data.user, token: data.token, isLoading: false }); } else { set({ error: data.message, isLoading: false }); } } catch (error) { set({ error: 登录失败, isLoading: false }); } }, logout: () set({ user: null, token: null }), hasPermission: (permission) { const { user } get(); if (!user) return false; // 管理员拥有所有权限 if (user.role admin) return true; // 根据角色检查权限 const rolePermissions rolePermissionsMap[user.role] || []; return rolePermissions.includes(permission); }, }), { name: admin-auth } ) ); // src/stores/themeStore.ts import { create } from zustand; import { persist } from zustand/middleware; interface ThemeState { isDark: boolean; toggleTheme: () void; setTheme: (isDark: boolean) void; } export const useThemeStore createThemeState()( persist( (set) ({ isDark: false, toggleTheme: () set((state) ({ isDark: !state.isDark })), setTheme: (isDark) set({ isDark }), }), { name: theme } ) );3.3 布局组件// src/components/Layout/Sidebar.tsx import { NavLink } from react-router-dom; import { HomeIcon, UsersIcon, ShieldCheckIcon, DocumentTextIcon, Cog6ToothIcon, } from heroicons/react/24/outline; const menuItems [ { path: /, label: 仪表盘, icon: HomeIcon }, { path: /users, label: 用户管理, icon: UsersIcon, permission: user:view }, { path: /roles, label: 角色管理, icon: ShieldCheckIcon, permission: role:view }, { path: /logs, label: 操作日志, icon: DocumentTextIcon, permission: log:view }, { path: /settings, label: 系统设置, icon: Cog6ToothIcon, permission: setting:view }, ]; export default function Sidebar() { const { hasPermission } useAuthStore(); return ( aside classNamew-64 bg-gray-900 text-white flex-shrink-0 div classNamep-4 text-xl font-bold border-b border-gray-800 管理后台 /div nav classNamep-4 {menuItems.map((item) { if (item.permission !hasPermission(item.permission)) return null; return ( NavLink key{item.path} to{item.path} className{({ isActive }) flex items-center gap-3 px-4 py-3 rounded-lg mb-1 transition-colors ${ isActive ? bg-blue-600 text-white : text-gray-300 hover:bg-gray-800 } } item.icon classNamew-5 h-5 / span{item.label}/span /NavLink ); })} /nav /aside ); } // src/components/Layout/Header.tsx import { useAuthStore } from ../../stores/authStore; import { useThemeStore } from ../../stores/themeStore; import ThemeToggle from ../ThemeToggle; export default function Header() { const { user, logout } useAuthStore(); const { isDark, toggleTheme } useThemeStore(); return ( header classNamebg-white dark:bg-gray-800 shadow-md px-6 py-4 div classNameflex justify-between items-center h1 classNametext-xl font-semibold欢迎回来{user?.username}/h1 div classNameflex items-center gap-4 ThemeToggle isDark{isDark} onToggle{toggleTheme} / div classNamerelative group button classNameflex items-center gap-2 img src{user?.avatar || https://via.placeholder.com/40} altAvatar classNamew-8 h-8 rounded-full / span{user?.username}/span /button div classNameabsolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg hidden group-hover:block button onClick{logout} classNamew-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg 退出登录 /button /div /div /div /div /header ); } // src/components/Layout/Layout.tsx import { Outlet } from react-router-dom; import Sidebar from ./Sidebar; import Header from ./Header; export default function Layout() { return ( div classNameflex h-screen bg-gray-100 dark:bg-gray-900 Sidebar / div classNameflex-1 flex flex-col overflow-hidden Header / main classNameflex-1 overflow-auto p-6 Outlet / /main /div /div ); }3.4 数据表格组件// src/components/DataTable.tsx import { useState } from react; interface ColumnT { key: keyof T; header: string; render?: (value: T[keyof T], item: T) React.ReactNode; sortable?: boolean; } interface DataTablePropsT { data: T[]; columns: ColumnT[]; onEdit?: (item: T) void; onDelete?: (item: T) void; onView?: (item: T) void; } export default function DataTableT extends { id: string }({ data, columns, onEdit, onDelete, onView, }: DataTablePropsT) { const [sortKey, setSortKey] useStatekeyof T | null(null); const [sortOrder, setSortOrder] useStateasc | desc(asc); const handleSort (key: keyof T) { if (sortKey key) { setSortOrder(prev prev asc ? desc : asc); } else { setSortKey(key); setSortOrder(asc); } }; const sortedData [...data]; if (sortKey) { sortedData.sort((a, b) { const aVal a[sortKey]; const bVal b[sortKey]; if (aVal bVal) return sortOrder asc ? -1 : 1; if (aVal bVal) return sortOrder asc ? 1 : -1; return 0; }); } return ( div classNameoverflow-x-auto table classNamemin-w-full bg-white dark:bg-gray-800 rounded-lg overflow-hidden thead classNamebg-gray-50 dark:bg-gray-700 tr {columns.map(col ( th key{String(col.key)} onClick{() col.sortable handleSort(col.key)} className{px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider ${ col.sortable ? cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600 : }} {col.header} {sortKey col.key ( span classNameml-1{sortOrder asc ? ↑ : ↓}/span )} /th ))} th classNamepx-6 py-3 text-right操作/th /tr /thead tbody classNamedivide-y divide-gray-200 dark:divide-gray-700 {sortedData.map(item ( tr key{item.id} classNamehover:bg-gray-50 dark:hover:bg-gray-700 {columns.map(col ( td key{String(col.key)} classNamepx-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-300 {col.render ? col.render(item[col.key], item) : String(item[col.key])} /td ))} td classNamepx-6 py-4 whitespace-nowrap text-right text-sm {onView ( button onClick{() onView(item)} classNametext-blue-600 hover:text-blue-900 mr-3 查看 /button )} {onEdit ( button onClick{() onEdit(item)} classNametext-green-600 hover:text-green-900 mr-3 编辑 /button )} {onDelete ( button onClick{() onDelete(item)} classNametext-red-600 hover:text-red-900 删除 /button )} /td /tr ))} /tbody /table /div ); }3.5 仪表盘页面// src/pages/DashboardPage.tsx import { useEffect, useState } from react; import { LineChart, Line, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, } from recharts; import { UsersIcon, UserGroupIcon, ShieldCheckIcon, ClockIcon } from heroicons/react/24/outline; import { DashboardStats } from ../types; const statsData [ { label: 总用户, value: 12453, icon: UsersIcon, color: bg-blue-500 }, { label: 活跃用户, value: 8921, icon: UserGroupIcon, color: bg-green-500 }, { label: 角色数, value: 8, icon: ShieldCheckIcon, color: bg-purple-500 }, { label: 今日登录, value: 234, icon: ClockIcon, color: bg-orange-500 }, ]; const revenueData [ { month: 1月, amount: 12500 }, { month: 2月, amount: 14200 }, { month: 3月, amount: 16800 }, { month: 4月, amount: 18900 }, { month: 5月, amount: 21500 }, { month: 6月, amount: 24300 }, ]; const userTrendData [ { date: 1月, count: 320 }, { date: 2月, count: 380 }, { date: 3月, count: 450 }, { date: 4月, count: 520 }, { date: 5月, count: 610 }, { date: 6月, count: 720 }, ]; const userDistribution [ { name: 管理员, value: 8 }, { name: 编辑, value: 45 }, { name: 普通用户, value: 12400 }, ]; const COLORS [#0088FE, #00C49F, #FFBB28]; export default function DashboardPage() { const [stats, setStats] useStateDashboardStats | null(null); useEffect(() { // 获取统计数据 fetch(/api/dashboard/stats) .then(res res.json()) .then(setStats); }, []); return ( div classNamespace-y-6 h1 classNametext-2xl font-bold仪表盘/h1 {/* 统计卡片 */} div classNamegrid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 {statsData.map(stat ( div key{stat.label} classNamebg-white dark:bg-gray-800 rounded-lg shadow p-6 div classNameflex items-center justify-between div p classNametext-gray-500 dark:text-gray-400 text-sm{stat.label}/p p classNametext-2xl font-bold mt-1{stat.value.toLocaleString()}/p /div div className{${stat.color} p-3 rounded-full} stat.icon classNamew-6 h-6 text-white / /div /div /div ))} /div {/* 图表区域 */} div classNamegrid grid-cols-1 lg:grid-cols-2 gap-6 {/* 收入趋势 */} div classNamebg-white dark:bg-gray-800 rounded-lg shadow p-6 h2 classNametext-lg font-semibold mb-4收入趋势/h2 ResponsiveContainer width100% height{300} LineChart data{revenueData} CartesianGrid strokeDasharray3 3 / XAxis dataKeymonth / YAxis / Tooltip / Legend / Line typemonotone dataKeyamount stroke#8884d8 name收入(元) / /LineChart /ResponsiveContainer /div {/* 用户增长趋势 */} div classNamebg-white dark:bg-gray-800 rounded-lg shadow p-6 h2 classNametext-lg font-semibold mb-4用户增长趋势/h2 ResponsiveContainer width100% height{300} BarChart data{userTrendData} CartesianGrid strokeDasharray3 3 / XAxis dataKeydate / YAxis / Tooltip / Legend / Bar dataKeycount fill#82ca9d name新增用户 / /BarChart /ResponsiveContainer /div {/* 用户分布 */} div classNamebg-white dark:bg-gray-800 rounded-lg shadow p-6 h2 classNametext-lg font-semibold mb-4用户角色分布/h2 ResponsiveContainer width100% height{300} PieChart Pie data{userDistribution} cx50% cy50% labelLine{false} label{(entry) ${entry.name}: ${entry.value}} outerRadius{80} fill#8884d8 dataKeyvalue {userDistribution.map((entry, index) ( Cell key{cell-${index}} fill{COLORS[index % COLORS.length]} / ))} /Pie Tooltip / /PieChart /ResponsiveContainer /div {/* 最近活动 */} div classNamebg-white dark:bg-gray-800 rounded-lg shadow p-6 h2 classNametext-lg font-semibold mb-4最近活动/h2 div classNamespace-y-4 {[1, 2, 3, 4, 5].map(i ( div key{i} classNameflex items-center gap-3 pb-3 border-b dark:border-gray-700 div classNamew-2 h-2 bg-green-500 rounded-full/div div classNameflex-1 p classNametext-sm用户 张三 登录了系统/p p classNametext-xs text-gray-5002分钟前/p /div /div ))} /div /div /div /div ); }3.6 用户管理页面// src/pages/UsersPage.tsx import { useState, useEffect } from react; import { User } from ../types; import DataTable from ../components/DataTable; import ConfirmModal from ../components/ConfirmModal; export default function UsersPage() { const [users, setUsers] useStateUser[]([]); const [loading, setLoading] useState(true); const [showModal, setShowModal] useState(false); const [selectedUser, setSelectedUser] useStateUser | null(null); const columns [ { key: username, header: 用户名, sortable: true }, { key: email, header: 邮箱, sortable: true }, { key: role, header: 角色, render: (value: string) ( span className{px-2 py-1 rounded text-xs ${ value admin ? bg-red-100 text-red-800 : bg-blue-100 text-blue-800 }} {value admin ? 管理员 : 普通用户} /span ), }, { key: status, header: 状态, render: (value: string) ( span className{px-2 py-1 rounded text-xs ${ value active ? bg-green-100 text-green-800 : bg-gray-100 text-gray-800 }} {value active ? 启用 : 禁用} /span ), }, { key: createdAt, header: 注册时间, sortable: true }, { key: lastLogin, header: 最后登录 }, ]; useEffect(() { fetchUsers(); }, []); const fetchUsers async () { setLoading(true); const response await fetch(/api/users); const data await response.json(); setUsers(data); setLoading(false); }; const handleDelete async (user: User) { setSelectedUser(user); setShowModal(true); }; const confirmDelete async () { if (selectedUser) { await fetch(/api/users/${selectedUser.id}, { method: DELETE }); await fetchUsers(); setShowModal(false); setSelectedUser(null); } }; if (loading) return div classNametext-center py-12加载中.../div; return ( div classNamespace-y-6 div classNameflex justify-between items-center h1 classNametext-2xl font-bold用户管理/h1 button classNamepx-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 添加用户 /button /div DataTable data{users} columns{columns} onView{(user) console.log(查看, user)} onEdit{(user) console.log(编辑, user)} onDelete{handleDelete} / ConfirmModal isOpen{showModal} title确认删除 message{确定要删除用户 ${selectedUser?.username} 吗此操作不可恢复。} onConfirm{confirmDelete} onCancel{() setShowModal(false)} / /div ); }3.7 登录页面// src/pages/LoginPage.tsx import { useState } from react; import { useNavigate } from react-router-dom; import { useAuthStore } from ../stores/authStore; export default function LoginPage() { const navigate useNavigate(); const { login, isLoading, error } useAuthStore(); const [username, setUsername] useState(); const [password, setPassword] useState(); const handleSubmit async (e: React.FormEvent) { e.preventDefault(); await login(username, password); navigate(/); }; return ( div classNamemin-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900 div classNamebg-white dark:bg-gray-800 rounded-lg shadow-lg p-8 w-full max-w-md h1 classNametext-2xl font-bold text-center mb-8管理后台登录/h1 form onSubmit{handleSubmit} classNamespace-y-6 div label classNameblock mb-1用户名/label input typetext value{username} onChange{(e) setUsername(e.target.value)} classNamew-full border rounded-lg p-2 dark:bg-gray-700 required / /div div label classNameblock mb-1密码/label input typepassword value{password} onChange{(e) setPassword(e.target.value)} classNamew-full border rounded-lg p-2 dark:bg-gray-700 required / /div {error p classNametext-red-500 text-sm{error}/p} button typesubmit disabled{isLoading} classNamew-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 {isLoading ? 登录中... : 登录} /button /form /div /div ); }3.8 Mock 数据// db.json{users:[{id:1,username:admin,email:adminexample.com,role:admin,status:active,createdAt:2024-01-01,lastLogin:2024-06-15 10:30:00}],roles:[{id:1,name:管理员,permissions:[*],description:拥有所有权限}],logs:[]}4. 项目运行# 安装依赖npminstallrecharts heroicons/react# 启动项目npmrun dev5. 总结核心知识点知识点应用Zustand认证状态、主题状态Recharts数据可视化图表权限控制基于角色的权限管理数据表格排序、分页、CRUD暗色主题主题切换和持久化