本节将介绍如何使用 Tailwind CSS 开发一个功能完整的企业级后台管理系统,包括布局设计、组件开发、主题定制等方面。

系统布局

布局框架

  1. // components/layout/AdminLayout.tsx
  2. import { useState } from 'react';
  3. interface AdminLayoutProps {
  4. children: React.ReactNode;
  5. }
  6. const AdminLayout: React.FC<AdminLayoutProps> = ({ children }) => {
  7. const [sidebarOpen, setSidebarOpen] = useState(false);
  8. return (
  9. <div className="min-h-screen bg-gray-100">
  10. {/* 顶部导航 */}
  11. <nav className="bg-white shadow-sm">
  12. <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
  13. <div className="flex justify-between h-16">
  14. <div className="flex">
  15. {/* 移动端菜单按钮 */}
  16. <button
  17. className="md:hidden p-2"
  18. onClick={() => setSidebarOpen(!sidebarOpen)}
  19. >
  20. <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  21. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
  22. </svg>
  23. </button>
  24. {/* Logo */}
  25. <div className="flex-shrink-0 flex items-center">
  26. <img
  27. className="h-8 w-auto"
  28. src="/logo.svg"
  29. alt="Logo"
  30. />
  31. </div>
  32. </div>
  33. {/* 用户菜单 */}
  34. <div className="flex items-center">
  35. <div className="ml-3 relative">
  36. <div className="flex items-center space-x-4">
  37. <button className="bg-gray-800 flex text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">
  38. <img
  39. className="h-8 w-8 rounded-full"
  40. src="/avatar.jpg"
  41. alt="User avatar"
  42. />
  43. </button>
  44. </div>
  45. </div>
  46. </div>
  47. </div>
  48. </div>
  49. </nav>
  50. {/* 侧边栏 */}
  51. <div className={`
  52. fixed inset-y-0 left-0 w-64 bg-white shadow-lg transform transition-transform duration-300 ease-in-out
  53. ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
  54. md:translate-x-0 md:static
  55. `}>
  56. <div className="h-full flex flex-col">
  57. <nav className="flex-1 py-4 overflow-y-auto">
  58. <div className="px-2 space-y-1">
  59. {/* 导航菜单项 */}
  60. <a href="/dashboard" className="group flex items-center px-2 py-2 text-sm font-medium text-gray-600 rounded-md hover:bg-gray-50 hover:text-gray-900">
  61. <svg className="mr-3 h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  62. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
  63. </svg>
  64. 仪表盘
  65. </a>
  66. </div>
  67. </nav>
  68. </div>
  69. </div>
  70. {/* 主要内容区域 */}
  71. <main className="flex-1 py-6">
  72. <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
  73. {children}
  74. </div>
  75. </main>
  76. </div>
  77. );
  78. };

组件系统

数据表格组件

  1. // components/DataTable/index.tsx
  2. interface Column<T> {
  3. title: string;
  4. key: keyof T;
  5. render?: (value: any, record: T) => React.ReactNode;
  6. }
  7. interface DataTableProps<T> {
  8. columns: Column<T>[];
  9. dataSource: T[];
  10. loading?: boolean;
  11. pagination?: {
  12. current: number;
  13. pageSize: number;
  14. total: number;
  15. onChange: (page: number, pageSize: number) => void;
  16. };
  17. }
  18. function DataTable<T extends { id: string | number }>({
  19. columns,
  20. dataSource,
  21. loading,
  22. pagination
  23. }: DataTableProps<T>) {
  24. return (
  25. <div className="overflow-x-auto">
  26. <table className="min-w-full divide-y divide-gray-200">
  27. <thead className="bg-gray-50">
  28. <tr>
  29. {columns.map((column) => (
  30. <th
  31. key={String(column.key)}
  32. className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
  33. >
  34. {column.title}
  35. </th>
  36. ))}
  37. </tr>
  38. </thead>
  39. <tbody className="bg-white divide-y divide-gray-200">
  40. {loading ? (
  41. <tr>
  42. <td colSpan={columns.length} className="px-6 py-4 text-center">
  43. <div className="flex justify-center">
  44. <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900"></div>
  45. </div>
  46. </td>
  47. </tr>
  48. ) : (
  49. dataSource.map((record) => (
  50. <tr key={record.id}>
  51. {columns.map((column) => (
  52. <td
  53. key={String(column.key)}
  54. className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"
  55. >
  56. {column.render
  57. ? column.render(record[column.key], record)
  58. : record[column.key]}
  59. </td>
  60. ))}
  61. </tr>
  62. ))
  63. )}
  64. </tbody>
  65. </table>
  66. {pagination && (
  67. <div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
  68. <div className="flex-1 flex justify-between items-center">
  69. <div>
  70. <p className="text-sm text-gray-700">
  71. 显示 {(pagination.current - 1) * pagination.pageSize + 1} 到{' '}
  72. {Math.min(pagination.current * pagination.pageSize, pagination.total)} 条,
  73. {pagination.total}
  74. </p>
  75. </div>
  76. <div className="flex space-x-2">
  77. <button
  78. onClick={() => pagination.onChange(pagination.current - 1, pagination.pageSize)}
  79. disabled={pagination.current === 1}
  80. className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
  81. >
  82. 上一页
  83. </button>
  84. <button
  85. onClick={() => pagination.onChange(pagination.current + 1, pagination.pageSize)}
  86. disabled={pagination.current * pagination.pageSize >= pagination.total}
  87. className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
  88. >
  89. 下一页
  90. </button>
  91. </div>
  92. </div>
  93. </div>
  94. )}
  95. </div>
  96. );
  97. }

表单组件

  1. // components/Form/index.tsx
  2. interface FormItemProps {
  3. label: string;
  4. required?: boolean;
  5. error?: string;
  6. children: React.ReactNode;
  7. }
  8. const FormItem: React.FC<FormItemProps> = ({
  9. label,
  10. required,
  11. error,
  12. children
  13. }) => {
  14. return (
  15. <div className="mb-4">
  16. <label className="block text-sm font-medium text-gray-700">
  17. {required && <span className="text-red-500 mr-1">*</span>}
  18. {label}
  19. </label>
  20. <div className="mt-1">
  21. {children}
  22. </div>
  23. {error && (
  24. <p className="mt-1 text-sm text-red-600">
  25. {error}
  26. </p>
  27. )}
  28. </div>
  29. );
  30. };
  31. interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  32. error?: boolean;
  33. }
  34. const Input: React.FC<InputProps> = ({ error, ...props }) => {
  35. return (
  36. <input
  37. {...props}
  38. className={`
  39. block w-full rounded-md shadow-sm
  40. sm:text-sm
  41. ${error
  42. ? 'border-red-300 text-red-900 placeholder-red-300 focus:ring-red-500 focus:border-red-500'
  43. : 'border-gray-300 focus:ring-blue-500 focus:border-blue-500'
  44. }
  45. `}
  46. />
  47. );
  48. };

业务功能实现

用户管理页面

  1. // pages/UserManagement.tsx
  2. import { useState, useEffect } from 'react';
  3. import DataTable from '../components/DataTable';
  4. import { Form, FormItem, Input } from '../components/Form';
  5. interface User {
  6. id: number;
  7. name: string;
  8. email: string;
  9. role: string;
  10. status: 'active' | 'inactive';
  11. }
  12. const UserManagement = () => {
  13. const [users, setUsers] = useState<User[]>([]);
  14. const [loading, setLoading] = useState(false);
  15. const [pagination, setPagination] = useState({
  16. current: 1,
  17. pageSize: 10,
  18. total: 0
  19. });
  20. const columns = [
  21. {
  22. title: '用户名',
  23. key: 'name',
  24. },
  25. {
  26. title: '邮箱',
  27. key: 'email',
  28. },
  29. {
  30. title: '角色',
  31. key: 'role',
  32. },
  33. {
  34. title: '状态',
  35. key: 'status',
  36. render: (value: string) => (
  37. <span className={`
  38. px-2 py-1 text-xs rounded-full
  39. ${value === 'active'
  40. ? 'bg-green-100 text-green-800'
  41. : 'bg-red-100 text-red-800'
  42. }
  43. `}>
  44. {value === 'active' ? '激活' : '禁用'}
  45. </span>
  46. )
  47. },
  48. {
  49. title: '操作',
  50. key: 'action',
  51. render: (_: any, record: User) => (
  52. <div className="flex space-x-2">
  53. <button
  54. onClick={() => handleEdit(record)}
  55. className="text-blue-600 hover:text-blue-800"
  56. >
  57. 编辑
  58. </button>
  59. <button
  60. onClick={() => handleDelete(record.id)}
  61. className="text-red-600 hover:text-red-800"
  62. >
  63. 删除
  64. </button>
  65. </div>
  66. )
  67. }
  68. ];
  69. const handleEdit = (user: User) => {
  70. // 实现编辑逻辑
  71. };
  72. const handleDelete = (id: number) => {
  73. // 实现删除逻辑
  74. };
  75. return (
  76. <div className="space-y-6">
  77. <div className="bg-white shadow rounded-lg">
  78. <div className="px-4 py-5 sm:p-6">
  79. <h3 className="text-lg leading-6 font-medium text-gray-900">
  80. 用户管理
  81. </h3>
  82. <div className="mt-4">
  83. <DataTable
  84. columns={columns}
  85. dataSource={users}
  86. loading={loading}
  87. pagination={pagination}
  88. />
  89. </div>
  90. </div>
  91. </div>
  92. </div>
  93. );
  94. };

主题定制

主题配置

  1. // tailwind.config.js
  2. const colors = require('tailwindcss/colors');
  3. module.exports = {
  4. theme: {
  5. extend: {
  6. colors: {
  7. primary: colors.blue,
  8. success: colors.green,
  9. warning: colors.yellow,
  10. danger: colors.red,
  11. // 自定义企业主题色
  12. brand: {
  13. light: '#60A5FA',
  14. DEFAULT: '#3B82F6',
  15. dark: '#2563EB',
  16. },
  17. },
  18. spacing: {
  19. '18': '4.5rem',
  20. '72': '18rem',
  21. '84': '21rem',
  22. '96': '24rem',
  23. },
  24. },
  25. },
  26. variants: {
  27. extend: {
  28. backgroundColor: ['active', 'disabled'],
  29. textColor: ['active', 'disabled'],
  30. opacity: ['disabled'],
  31. },
  32. },
  33. };

暗色主题支持

  1. // hooks/useTheme.ts
  2. import { useState, useEffect } from 'react';
  3. type Theme = 'light' | 'dark';
  4. export const useTheme = () => {
  5. const [theme, setTheme] = useState<Theme>('light');
  6. useEffect(() => {
  7. const savedTheme = localStorage.getItem('theme') as Theme;
  8. if (savedTheme) {
  9. setTheme(savedTheme);
  10. } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  11. setTheme('dark');
  12. }
  13. }, []);
  14. const toggleTheme = () => {
  15. const newTheme = theme === 'light' ? 'dark' : 'light';
  16. setTheme(newTheme);
  17. localStorage.setItem('theme', newTheme);
  18. document.documentElement.classList.toggle('dark');
  19. };
  20. return { theme, toggleTheme };
  21. };

性能优化

路由懒加载

  1. // routes/index.tsx
  2. import { lazy, Suspense } from 'react';
  3. const UserManagement = lazy(() => import('../pages/UserManagement'));
  4. const RoleManagement = lazy(() => import('../pages/RoleManagement'));
  5. const Dashboard = lazy(() => import('../pages/Dashboard'));
  6. const Routes = () => {
  7. return (
  8. <Suspense fallback={<div>Loading...</div>}>
  9. <Switch>
  10. <Route path="/dashboard" component={Dashboard} />
  11. <Route path="/users" component={UserManagement} />
  12. <Route path="/roles" component={RoleManagement} />
  13. </Switch>
  14. </Suspense>
  15. );
  16. };

状态管理优化

  1. // store/slices/userSlice.ts
  2. import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
  3. export const fetchUsers = createAsyncThunk(
  4. 'users/fetchUsers',
  5. async (params: { page: number; pageSize: number }) => {
  6. const response = await fetch(`/api/users?page=${params.page}&pageSize=${params.pageSize}`);
  7. return response.json();
  8. }
  9. );
  10. const userSlice = createSlice({
  11. name: 'users',
  12. initialState: {
  13. list: [],
  14. loading: false,
  15. error: null,
  16. pagination: {
  17. current: 1,
  18. pageSize: 10,
  19. total: 0
  20. }
  21. },
  22. reducers: {},
  23. extraReducers: (builder) => {
  24. builder
  25. .addCase(fetchUsers.pending, (state) => {
  26. state.loading = true;
  27. })
  28. .addCase(fetchUsers.fulfilled, (state, action) => {
  29. state.loading = false;
  30. state.list = action.payload.data;
  31. state.pagination = action.payload.pagination;
  32. })
  33. .addCase(fetchUsers.rejected, (state, action) => {
  34. state.loading = false;
  35. state.error = action.error.message;
  36. });
  37. }
  38. });

扩展功能

权限控制

  1. // components/AuthWrapper.tsx
  2. interface AuthWrapperProps {
  3. permission: string;
  4. children: React.ReactNode;
  5. }
  6. const AuthWrapper: React.FC<AuthWrapperProps> = ({ permission, children }) => {
  7. const { permissions } = useAuth();
  8. if (!permissions.includes(permission)) {
  9. return null;
  10. }
  11. return <>{children}</>;
  12. };
  13. // 使用示例
  14. <AuthWrapper permission="user.edit">
  15. <button onClick={handleEdit}>编辑用户</button>
  16. </AuthWrapper>

操作日志

  1. // hooks/useLogger.ts
  2. export const useLogger = () => {
  3. const logOperation = async (params: {
  4. module: string;
  5. action: string;
  6. details: any;
  7. }) => {
  8. try {
  9. await fetch('/api/logs', {
  10. method: 'POST',
  11. headers: {
  12. 'Content-Type': 'application/json'
  13. },
  14. body: JSON.stringify({
  15. ...params,
  16. timestamp: new Date().toISOString(),
  17. userId: getCurrentUserId()
  18. })
  19. });
  20. } catch (error) {
  21. console.error('Failed to log operation:', error);
  22. }
  23. };
  24. return { logOperation };
  25. };
  26. // 使用示例
  27. const { logOperation } = useLogger();
  28. const handleUserUpdate = async (userData) => {
  29. try {
  30. await updateUser(userData);
  31. await logOperation({
  32. module: 'User',
  33. action: 'Update',
  34. details: userData
  35. });
  36. } catch (error) {
  37. // 错误处理
  38. }
  39. };

数据导出

  1. // utils/exportData.ts
  2. import { saveAs } from 'file-saver';
  3. import * as XLSX from 'xlsx';
  4. export const exportToExcel = (data: any[], filename: string) => {
  5. const worksheet = XLSX.utils.json_to_sheet(data);
  6. const workbook = XLSX.utils.book_new();
  7. XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
  8. const excelBuffer = XLSX.write(workbook, {
  9. bookType: 'xlsx',
  10. type: 'array'
  11. });
  12. const dataBlob = new Blob([excelBuffer], {
  13. type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  14. });
  15. saveAs(dataBlob, `${filename}.xlsx`);
  16. };
  17. // 使用示例
  18. const handleExport = () => {
  19. const formattedData = users.map(user => ({
  20. 用户名: user.name,
  21. 邮箱: user.email,
  22. 角色: user.role,
  23. 状态: user.status === 'active' ? '激活' : '禁用'
  24. }));
  25. exportToExcel(formattedData, '用户列表');
  26. };

最佳实践

  1. 布局规范

    • 采用响应式设计
    • 保持导航结构清晰
    • 合理使用空间层级
  2. 组件设计

    • 组件职责单一
    • 保持可复用性
    • 统一的样式规范
  3. 性能优化

    • 按需加载组件
    • 优化数据请求
    • 合理使用缓存
  4. 开发建议

    • 遵循代码规范
    • 完善错误处理
    • 做好单元测试