Vite 6 + React 18 + TypeScript + Ant Design 5 + SCSS 的后台管理平台。
源码位于src/,使用 BrowserRouter 的 SPA 架构,包含多级路由菜单、登录 / 退出、路由守卫与按钮级权限。
npm install # 安装依赖
npm run dev # 启动开发服务器(http://localhost:5173)
npm run typecheck # TypeScript 类型检查
npm run build # 类型检查 + 生产构建
npm run preview # 预览生产构建
改动代码后至少执行 npm run typecheck;涉及构建配置或样式时再执行 npm run build。
@ant-design/iconssrc/
├── api/ # 接口层;当前只有模拟登录 auth.ts
├── components/ # 通用组件:RouteGuard / AuthButton / Permission
├── constants/ # 权限码与中文说明
├── hooks/ # usePermission
├── layouts/ # BasicLayout:侧边栏、头部、内容区、面包屑
├── pages/ # 页面,按业务域分目录
│ ├── login/
│ ├── dashboard/
│ ├── system/
│ │ ├── users/
│ │ ├── roles/
│ │ └── permission/{menu,button}/
│ ├── business/{orders,customers}/
│ └── error/{Forbidden,NotFound}.tsx
├── router/
│ ├── routeConfig.tsx # 路由 + 菜单唯一数据源(含权限码、页面组件)
│ ├── menu.tsx # 菜单过滤与 antd Menu 结构生成
│ └── index.tsx # 配置转 React Router 嵌套路由
├── store/ # Zustand 登录态
├── styles/ # 全局 SCSS、通用页面样式
├── types/ # 全局类型
└── main.tsx # ConfigProvider + AntdApp + BrowserRouter
src/router/routeConfig.tsx 是**唯一数据源**。新增页面只需在这里配置 path、title、icon、permission、element,侧边栏、面包屑、路由守卫会自动读取。
系统管理 → 权限中心 → 菜单权限。src/router/index.tsx 把菜单配置转换成 React Router 的嵌套路由。element)通过 <Outlet /> 渲染子页面,默认跳转到第一个有权限的子页面。permission 过滤;分组下所有子项都无权限时整组隐藏。BasicLayout 内。src/store/auth.ts:Zustand store,持久化 token 与 user。src/api/auth.ts 提供模拟登录。admin / admin123:权限为 ['*'],拥有全部权限。user / user123:仅有工作台、用户、订单、客户的查看权限。/login,登录成功后返回原地址。src/constants/permissions.ts。permission + src/components/RouteGuard.tsx。/login/403buildMenuItems 按权限码过滤。<AuthButton permission="system:user:add">:无权限默认隐藏,fallback="disabled" 时禁用。<Permission permission="...">:包裹任意 UI。usePermission():页面内自定义判断。* 表示全部权限。BasicLayout,页面只负责内容区域。--app-primary、--app-bg、--app-border、--app-text、--app-text-secondary 等。.page-header、.page-toolbar、.filter-card、.table-card、.text-muted。.user-cell__name)。src/main.tsx 的 ConfigProvider 中统一配置。组件、工具函数、Hook、事件处理函数都使用箭头函数,不新增 function 声明。
// ✅ 组件
const UserList = () => {
const handleSubmit = async () => {
// ...
};
return <Table />;
};
export default UserList;
// ✅ 工具 / Hook
export const usePermission = () => {
// ...
};
export const formatAmount = (value: number) => `¥${value.toLocaleString()}`;
// ❌ 不要这样写
export default function UserList() {}
export function formatAmount(value: number) {}
写功能前先找现成能力,不要重复造轮子:
src/components/AuthButton.tsxsrc/components/Permission.tsxsrc/components/RouteGuard.tsxsrc/layouts/BasicLayout.tsx<App> 提供的 AntdApp.useApp() 中的 message、modal,**不要**直接使用 message.success() / Modal.confirm() 静态方法。src/components/ 是否已有可复用实现,再考虑新增。variant="borderless",不要使用 bordered={false}。所有表格的 columns 不得内联在 <Table /> 的 JSX 中,也不写在页面组件内部。统一提取到页面目录单独维护。
推荐结构:
src/pages/system/users/
├── index.tsx # 页面组件
├── columns.tsx # 表格列定义
└── Users.scss # 页面样式
// columns.tsx
import type { TableProps } from 'antd';
export const userColumns: TableProps<UserRecord>['columns'] = [
{
title: '用户名',
dataIndex: 'username',
},
// ...
];
// 需要事件回调时,导出工厂函数
export const createUserColumns = (options: {
onEdit: (record: UserRecord) => void;
onDelete: (record: UserRecord) => void;
}): TableProps<UserRecord>['columns'] => [
// ...
{
title: '操作',
key: 'action',
render: (_value, record) => (
<AuthButton permission={PERMISSION.USER_EDIT} onClick={() => options.onEdit(record)}>
编辑
</AuthButton>
),
},
];
// index.tsx
const columns = useMemo(() => createUserColumns({ onEdit, onDelete }), []);
<Table<UserRecord> rowKey="key" columns={columns} dataSource={dataSource} />;
约定:
TableProps<T>['columns'],不要使用 any。AuthButton / Permission(它们自己读取 store),无需把权限状态传进 columns。createColumns(options) 工厂 + useMemo,不要每次渲染重新创建。说明:存量页面中部分 columns 仍定义在页面组件内部、部分函数使用
function声明。新增和修改代码一律遵循上述规范,存量代码在后续迭代中逐步迁移。
src/pages/<业务域>/<页面>/ 下创建 index.tsx(有表格时增加 columns.tsx,有独立样式时增加同名 .scss)。src/constants/permissions.ts 新增权限码与中文说明。src/router/routeConfig.tsx 中增加路由配置:/system/users)。title、icon、permission、element。children。src/api/auth.ts)或真实后端中给角色分配权限码。AuthButton,路由级权限由 RouteGuard 自动处理。npm run typecheck 验证。示例:
{
path: '/business/invoices',
title: '发票管理',
icon: <FileTextOutlined />,
permission: PERMISSION.INVOICE_VIEW,
element: <Invoices />,
}
src/api/ 下,按业务域拆分文件;不要在页面内直接写请求实现。src/api/auth.ts 是前端模拟接口。接入真实后端时替换为实际请求,并保留登录态写入 store 的流程。src/store/auth.ts;页面局部状态使用 useState / useMemo。style。aria-label。scroll={{ x: ... }},左右布局使用栅格或 flex 换行。index.html。dist/。