本节将介绍如何使用 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, '用户列表');
};
最佳实践
布局规范
- 采用响应式设计
- 保持导航结构清晰
- 合理使用空间层级
组件设计
- 组件职责单一
- 保持可复用性
- 统一的样式规范
性能优化
- 按需加载组件
- 优化数据请求
- 合理使用缓存
开发建议
- 遵循代码规范
- 完善错误处理
- 做好单元测试