wsj
2026-10-17 5630cce7759c27d910fbeeec370a2e9f6b4b26b1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import { useMemo } from 'react';
import type { ReactNode } from 'react';
import { App as AntdApp, Alert, Card, Space, Table, Tag } from 'antd';
import type { TableProps } from 'antd';
import AuthButton from '@/components/AuthButton';
import { PERMISSION } from '@/constants/permissions';
import { appRoutes, type AppRouteItem } from '@/router/routeConfig';
import './MenuPermission.scss';
 
interface MenuNode {
  key: string;
  title: string;
  path: string;
  permission: string;
  type: '目录' | '菜单';
  icon?: ReactNode;
  children?: MenuNode[];
}
 
function toMenuData(routes: AppRouteItem[]): MenuNode[] {
  return routes.map((route) => ({
    key: route.path,
    title: route.title,
    path: route.path,
    permission: route.permission ?? '-',
    type: route.children?.length ? '目录' : '菜单',
    icon: route.icon,
    children: route.children ? toMenuData(route.children) : undefined,
  }));
}
 
export default function MenuPermission() {
  const { message } = AntdApp.useApp();
  // appRoutes 与当前页面存在模块循环引用,放在渲染期读取可避免模块初始化的 TDZ
  const dataSource = useMemo(() => toMenuData(appRoutes), []);
 
  const columns: TableProps<MenuNode>['columns'] = [
    {
      title: '菜单名称',
      dataIndex: 'title',
      width: 240,
      render: (value: string, record) => (
        <span className="menu-name">
          {record.icon}
          <span>{value}</span>
        </span>
      ),
    },
    {
      title: '路由路径',
      dataIndex: 'path',
      width: 220,
      render: (value: string) => <code className="menu-code">{value}</code>,
    },
    {
      title: '权限标识',
      dataIndex: 'permission',
      width: 240,
      render: (value: string) =>
        value === '-' ? (
          <span className="text-muted">-</span>
        ) : (
          <code className="menu-code">{value}</code>
        ),
    },
    {
      title: '类型',
      dataIndex: 'type',
      width: 90,
      render: (value: MenuNode['type']) => (
        <Tag color={value === '目录' ? 'purple' : 'blue'} bordered={false}>
          {value}
        </Tag>
      ),
    },
    {
      title: '状态',
      key: 'status',
      width: 90,
      render: () => (
        <Tag color="green" bordered={false}>
          启用
        </Tag>
      ),
    },
    {
      title: '操作',
      key: 'action',
      width: 110,
      render: (_value, record) => (
        <AuthButton
          type="link"
          size="small"
          permission={PERMISSION.PERMISSION_EDIT}
          onClick={() => message.info(`演示:编辑「${record.title}」的菜单配置`)}
        >
          编辑
        </AuthButton>
      ),
    },
  ];
 
  return (
    <div className="menu-permission-page">
      <div className="page-header">
        <div>
          <h1 className="page-header__title">菜单权限</h1>
          <p className="page-header__desc">
            菜单由路由配置自动生成,支持两级、三级嵌套;无权限的菜单会自动从侧边栏隐藏。
          </p>
        </div>
        <div className="page-header__actions">
          <Space wrap>
            <AuthButton
              permission={PERMISSION.PERMISSION_EDIT}
              onClick={() => message.info('演示:打开新增菜单弹窗')}
            >
              新增菜单
            </AuthButton>
            <AuthButton
              type="primary"
              permission={PERMISSION.PERMISSION_EDIT}
              onClick={() => message.success('菜单权限配置已保存(演示)')}
            >
              保存配置
            </AuthButton>
          </Space>
        </div>
      </div>
 
      <Alert
        className="menu-permission-alert"
        type="info"
        showIcon
        message="路由即菜单"
        description="所有页面统一在 src/router/routeConfig.tsx 中配置 path、title、permission 与组件,侧边栏、面包屑以及路由守卫都会读取同一份配置。"
      />
 
      <Card className="table-card" variant="borderless">
        <Table<MenuNode>
          rowKey="key"
          columns={columns}
          dataSource={dataSource}
          pagination={false}
          defaultExpandAllRows
          scroll={{ x: 900 }}
        />
      </Card>
    </div>
  );
}