第七章:项目实战 - 第一节 - Tailwind CSS 企业级后台系统开发

发布于:2025-03-01 ⋅ 阅读:(14) ⋅ 点赞:(0)

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

系统布局

布局框架

// components/layout/AdminLayout.tsx
import { useState } from 'react';

interface AdminLayoutProps {
  children: React.ReactNode;
}

const AdminLayout: React.FC<AdminLayoutProps> = ({ children }) => {
  const [sidebarOpen, setSidebarOpen] = useState(false);

  return (
    <div className="min-h-screen bg-gray-100">
      {/* 顶部导航 */}
      <nav className="bg-white shadow-sm">
        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
          <div className="flex justify-between h-16">
            <div className="flex">
              {/* 移动端菜单按钮 */}
              <button
                className="md:hidden p-2"
                onClick={() => setSidebarOpen(!sidebarOpen)}
              >
                <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
                </svg>
              </button>
              
              {/* Logo */}
              <div className="flex-shrink-0 flex items-center">
                <img
                  className="h-8 w-auto"
                  src="/logo.svg"
                  alt="Logo"
                />
              </div>
            </div>
            
            {/* 用户菜单 */}
            <div className="flex items-center">
              <div className="ml-3 relative">
                <div className="flex items-center space-x-4">
                  <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">
                    <img
                      className="h-8 w-8 rounded-full"
                      src="/avatar.jpg"
                      alt="User avatar"
                    />
                  </button>
                </div>
              </div>
            </div>
          </div>
        </div>
      </nav>

      {/* 侧边栏 */}
      <div className={`
        fixed inset-y-0 left-0 w-64 bg-white shadow-lg transform transition-transform duration-300 ease-in-out
        ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
        md:translate-x-0 md:static
      `}>
        <div className="h-full flex flex-col">
          <nav className="flex-1 py-4 overflow-y-auto">
            <div className="px-2 space-y-1">
              {/* 导航菜单项 */}
              <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">
                <svg className="mr-3 h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <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" />
                </svg>
                仪表盘
              </a>
            </div>
          </nav>
        </div>
      </div>

      {/* 主要内容区域 */}
      <main className="flex-1 py-6">
        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
          {children}
        </div>
      </main>
    </div>
  );
};

组件系统

数据表格组件

// components/DataTable/index.tsx
interface Column<T> {
  title: string;
  key: keyof T;
  render?: (value: any, record: T) => React.ReactNode;
}

interface DataTableProps<T> {
  columns: Column<T>[];
  dataSource: T[];
  loading?: boolean;
  pagination?: {
    current: number;
    pageSize: number;
    total: number;
    onChange: (page: number, pageSize: number) => void;
  };
}

function DataTable<T extends { id: string | number }>({
  columns,
  dataSource,
  loading,
  pagination
}: DataTableProps<T>) {
  return (
    <div className="overflow-x-auto">
      <table className="min-w-full divide-y divide-gray-200">
        <thead className="bg-gray-50">
          <tr>
            {columns.map((column) => (
              <th
                key={String(column.key)}
                className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
              >
                {column.title}
              </th>
            ))}
          </tr>
        </thead>
        <tbody className="bg-white divide-y divide-gray-200">
          {loading ? (
            <tr>
              <td colSpan={columns.length} className="px-6 py-4 text-center">
                <div className="flex justify-center">
                  <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900"></div>
                </div>
              </td>
            </tr>
          ) : (
            dataSource.map((record) => (
              <tr key={record.id}>
                {columns.map((column) => (
                  <td
                    key={String(column.key)}
                    className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"
                  >
                    {column.render
                      ? column.render(record[column.key], record)
                      : record[column.key]}
                  </td>
                ))}
              </tr>
            ))
          )}
        </tbody>
      </table>

      {pagination && (
        <div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
          <div className="flex-1 flex justify-between items-center">
            <div>
              <p className="text-sm text-gray-700">
                显示 {(pagination.current - 1) * pagination.pageSize + 1}{' '}
                {Math.min(pagination.current * pagination.pageSize, pagination.total)} 条,
                共 {pagination.total}</p>
            </div>
            <div className="flex space-x-2">
              <button
                onClick={() => pagination.onChange(pagination.current - 1, pagination.pageSize)}
                disabled={pagination.current === 1}
                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"
              >
                上一页
              </button>
              <button
                onClick={() => pagination.onChange(pagination.current + 1, pagination.pageSize)}
                disabled={pagination.current * pagination.pageSize >= pagination.total}
                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"
              >
                下一页
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

表单组件

// components/Form/index.tsx
interface FormItemProps {
  label: string;
  required?: boolean;
  error?: string;
  children: React.ReactNode;
}

const FormItem: React.FC<FormItemProps> = ({
  label,
  required,
  error,
  children
}) => {
  return (
    <div className="mb-4">
      <label className="block text-sm font-medium text-gray-700">
        {required && <span className="text-red-500 mr-1">*</span>}
        {label}
      </label>
      <div className="mt-1">
        {children}
      </div>
      {error && (
        <p className="mt-1 text-sm text-red-600">
          {error}
        </p>
      )}
    </div>
  );
};

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  error?: boolean;
}

const Input: React.FC<InputProps> = ({ error, ...props }) => {
  return (
    <input
      {...props}
      className={`
        block w-full rounded-md shadow-sm
        sm:text-sm
        ${error
          ? 'border-red-300 text-red-900 placeholder-red-300 focus:ring-red-500 focus:border-red-500'
          : 'border-gray-300 focus:ring-blue-500 focus:border-blue-500'
        }
      `}
    />
  );
};

业务功能实现

用户管理页面

// pages/UserManagement.tsx
import { useState, useEffect } from 'react';
import DataTable from '../components/DataTable';
import { Form, FormItem, Input } from '../components/Form';

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  status: 'active' | 'inactive';
}

const UserManagement = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(false);
  const [pagination, setPagination] = useState({
    current: 1,
    pageSize: 10,
    total: 0
  });

  const columns = [
    {
      title: '用户名',
      key: 'name',
    },
    {
      title: '邮箱',
      key: 'email',
    },
    {
      title: '角色',
      key: 'role',
    },
    {
      title: '状态',
      key: 'status',
      render: (value: string) => (
        <span className={`
          px-2 py-1 text-xs rounded-full
          ${value === 'active'
            ? 'bg-green-100 text-green-800'
            : 'bg-red-100 text-red-800'
          }
        `}>
          {value === 'active' ? '激活' : '禁用'}
        </span>
      )
    },
    {
      title: '操作',
      key: 'action',
      render: (_: any, record: User) => (
        <div className="flex space-x-2">
          <button
            onClick={() => handleEdit(record)}
            className="text-blue-600 hover:text-blue-800"
          >
            编辑
          </button>
          <button
            onClick={() => handleDelete(record.id)}
            className="text-red-600 hover:text-red-800"
          >
            删除
          </button>
        </div>
      )
    }
  ];

  const handleEdit = (user: User) => {
    // 实现编辑逻辑
  };

  const handleDelete = (id: number) => {
    // 实现删除逻辑
  };

  return (
    <div className="space-y-6">
      <div className="bg-white shadow rounded-lg">
        <div className="px-4 py-5 sm:p-6">
          <h3 className="text-lg leading-6 font-medium text-gray-900">
            用户管理
          </h3>
          <div className="mt-4">
            <DataTable
              columns={columns}
              dataSource={users}
              loading={loading}
              pagination={pagination}
            />
          </div>
        </div>
      </div>
    </div>
  );
};

主题定制

主题配置

// tailwind.config.js
const colors = require('tailwindcss/colors');

module.exports = {
  theme: {
    extend: {
      colors: {
        primary: colors.blue,
        success: colors.green,
        warning: colors.yellow,
        danger: colors.red,
        // 自定义企业主题色
        brand: {
          light: '#60A5FA',
          DEFAULT: '#3B82F6',
          dark: '#2563EB',
        },
      },
      spacing: {
        '18': '4.5rem',
        '72': '18rem',
        '84': '21rem',
        '96': '24rem',
      },
    },
  },
  variants: {
    extend: {
      backgroundColor: ['active', 'disabled'],
      textColor: ['active', 'disabled'],
      opacity: ['disabled'],
    },
  },
};

暗色主题支持

// hooks/useTheme.ts
import { useState, useEffect } from 'react';

type Theme = 'light' | 'dark';

export const useTheme = () => {
  const [theme, setTheme] = useState<Theme>('light');

  useEffect(() => {
    const savedTheme = localStorage.getItem('theme') as Theme;
    if (savedTheme) {
      setTheme(savedTheme);
    } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
      setTheme('dark');
    }
  }, []);

  const toggleTheme = () => {
    const newTheme = theme === 'light' ? 'dark' : 'light';
    setTheme(newTheme);
    localStorage.setItem('theme', newTheme);
    document.documentElement.classList.toggle('dark');
  };

  return { theme, toggleTheme };
};

性能优化

路由懒加载

// routes/index.tsx
import { lazy, Suspense } from 'react';

const UserManagement = lazy(() => import('../pages/UserManagement'));
const RoleManagement = lazy(() => import('../pages/RoleManagement'));
const Dashboard = lazy(() => import('../pages/Dashboard'));

const Routes = () => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route path="/dashboard" component={Dashboard} />
        <Route path="/users" component={UserManagement} />
        <Route path="/roles" component={RoleManagement} />
      </Switch>
    </Suspense>
  );
};

状态管理优化

// store/slices/userSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

export const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async (params: { page: number; pageSize: number }) => {
    const response = await fetch(`/api/users?page=${params.page}&pageSize=${params.pageSize}`);
    return response.json();
  }
);

const userSlice = createSlice({
  name: 'users',
  initialState: {
    list: [],
    loading: false,
    error: null,
    pagination: {
      current: 1,
      pageSize: 10,
      total: 0
    }
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = false;
        state.list = action.payload.data;
        state.pagination = action.payload.pagination;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message;
      });
  }
});

扩展功能

权限控制

// components/AuthWrapper.tsx
interface AuthWrapperProps {
  permission: string;
  children: React.ReactNode;
}

const AuthWrapper: React.FC<AuthWrapperProps> = ({ permission, children }) => {
  const { permissions } = useAuth();
  
  if (!permissions.includes(permission)) {
    return null;
  }
  
  return <>{children}</>;
};

// 使用示例
<AuthWrapper permission="user.edit">
  <button onClick={handleEdit}>编辑用户</button>
</AuthWrapper>

操作日志

// hooks/useLogger.ts
export const useLogger = () => {
  const logOperation = async (params: {
    module: string;
    action: string;
    details: any;
  }) => {
    try {
      await fetch('/api/logs', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          ...params,
          timestamp: new Date().toISOString(),
          userId: getCurrentUserId()
        })
      });
    } catch (error) {
      console.error('Failed to log operation:', error);
    }
  };

  return { logOperation };
};

// 使用示例
const { logOperation } = useLogger();

const handleUserUpdate = async (userData) => {
  try {
    await updateUser(userData);
    await logOperation({
      module: 'User',
      action: 'Update',
      details: userData
    });
  } catch (error) {
    // 错误处理
  }
};

数据导出

// utils/exportData.ts
import { saveAs } from 'file-saver';
import * as XLSX from 'xlsx';

export const exportToExcel = (data: any[], filename: string) => {
  const worksheet = XLSX.utils.json_to_sheet(data);
  const workbook = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
  
  const excelBuffer = XLSX.write(workbook, {
    bookType: 'xlsx',
    type: 'array'
  });
  
  const dataBlob = new Blob([excelBuffer], {
    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  });
  
  saveAs(dataBlob, `${filename}.xlsx`);
};

// 使用示例
const handleExport = () => {
  const formattedData = users.map(user => ({
    用户名: user.name,
    邮箱: user.email,
    角色: user.role,
    状态: user.status === 'active' ? '激活' : '禁用'
  }));
  
  exportToExcel(formattedData, '用户列表');
};

最佳实践

  1. 布局规范

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

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

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

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

网站公告

今日签到

点亮在社区的每一天
去签到