Skip to content

Commit fad4f03

Browse files
committed
feat: 完成角色管理的编写
1 parent 8b34e4d commit fad4f03

File tree

7 files changed

+702
-1
lines changed

7 files changed

+702
-1
lines changed

src/features/router/routes.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { TFunction } from 'i18next';
2+
import type { RouteObject } from 'react-router-dom';
3+
4+
type FlatRoute = {
5+
name?: string;
6+
path: string;
7+
};
8+
9+
// 返回仅保留想要的字段
10+
interface SimpleRoute {
11+
children?: SimpleRoute[];
12+
key: string;
13+
title?: string;
14+
}
15+
16+
/**
17+
* 将多级路由递归拍平成一个数组
18+
*
19+
* @param routes 原始路由数组
20+
* @param parentPath 父级拼接后的 path,初始可传空字符串或 '/'
21+
* @returns 拍平后的一维路由数组
22+
*/
23+
export function flattenLeafRoutes(routes: RouteObject[]) {
24+
const flat: { name?: string; path: string }[] = [];
25+
26+
for (const route of routes) {
27+
// 如果还有 children,就递归往下找叶子
28+
if (route.children && route.children.length > 0) {
29+
const childLeafs = flattenLeafRoutes(route.children);
30+
31+
flat.push(...childLeafs);
32+
} else {
33+
// 没有 children => 叶子路由
34+
35+
// eslint-disable-next-line no-lonely-if
36+
if (route.path) {
37+
const isHasIndex = Boolean(route.children?.[0]?.index);
38+
39+
if (isHasIndex || !route.children || route.children?.length < 1) {
40+
flat.push({
41+
name: route.id, // 根据需要保存 name,这里拿 route.id
42+
path: route.path // path 可能为空字符串或undefined,统一成''
43+
});
44+
}
45+
}
46+
// 如果是 index 或者 有有效的 path => 收集
47+
}
48+
}
49+
50+
return flat;
51+
}
52+
53+
// 判断是否是“路由组”
54+
// 约定:以 "(" 开头、")" 结尾的 id 为路由组
55+
function isRouteGroup(routeId?: string): boolean {
56+
if (!routeId) return false;
57+
return routeId.endsWith(')');
58+
}
59+
60+
/** 过滤并展开路由组,同时过滤 handle.constant 为 true 的路由 保留 name、path、children */
61+
export function filterAndFlattenRoutes(routes: RouteObject[], t: TFunction): SimpleRoute[] {
62+
const result: SimpleRoute[] = [];
63+
64+
for (const route of routes) {
65+
// 1. 如果有 handle?.constant === true,直接跳过(不放到结果里)
66+
if (
67+
route.handle?.constant ||
68+
route?.index ||
69+
(route.children && route.children[0] && route.children[0].index && route.children[0].handle?.constant)
70+
) {
71+
// eslint-disable-next-line no-continue
72+
continue;
73+
}
74+
75+
// 2. 如果是路由组,则“展开”它的子路由到同级
76+
if (isRouteGroup(route.id)) {
77+
// 继续处理它的 children,并将处理后的结果直接合并到当前 result
78+
if (route.children && route.children.length > 0) {
79+
const flattenedChildren = filterAndFlattenRoutes(route.children, t);
80+
result.push(...flattenedChildren);
81+
}
82+
} else {
83+
// 3. 否则是正常路由,保留 name、path、children
84+
const newRoute: SimpleRoute = {
85+
key: route.path || '',
86+
title: t(`route.${route.id}`)
87+
};
88+
89+
// 递归处理它的子路由
90+
if (route.children && route.children.length > 0) {
91+
newRoute.children = filterAndFlattenRoutes(route.children, t);
92+
}
93+
94+
result.push(newRoute);
95+
}
96+
}
97+
98+
return result;
99+
}
100+
101+
export function getFlatBaseRoutes(routes: FlatRoute[], t: TFunction) {
102+
return routes.map(({ name, path }) => ({
103+
label: t(`route.${name}`),
104+
value: path
105+
}));
106+
}
107+
108+
export function getBaseChildrenRoutes(routes: RouteObject[]) {
109+
const baseRoutes = routes[0].children?.find(item => item.id === '(base)')?.children;
110+
111+
return baseRoutes || [];
112+
}
Lines changed: 185 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,189 @@
1+
import { Suspense } from 'react';
2+
3+
import { enableStatusRecord } from '@/constants/business';
4+
import { ATG_MAP } from '@/constants/common';
5+
import { TableHeaderOperation, useTable, useTableOperate, useTableScroll } from '@/features/table';
6+
import { fetchGetRoleList } from '@/service/api';
7+
8+
import RoleSearch from './modules/role-search';
9+
10+
const RoleOperateDrawer = lazy(() => import('./modules/role-operate-drawer'));
11+
112
const Role = () => {
2-
return <div>Role</div>;
13+
const { t } = useTranslation();
14+
15+
const isMobile = useMobile();
16+
17+
const { scrollConfig, tableWrapperRef } = useTableScroll();
18+
19+
const { columnChecks, data, run, searchProps, setColumnChecks, tableProps } = useTable({
20+
apiFn: fetchGetRoleList,
21+
apiParams: {
22+
current: 1,
23+
roleCode: undefined,
24+
roleName: undefined,
25+
size: 10,
26+
status: undefined
27+
},
28+
columns: () => [
29+
{
30+
align: 'center',
31+
dataIndex: 'index',
32+
key: 'index',
33+
title: t('common.index'),
34+
width: 64
35+
},
36+
{
37+
align: 'center',
38+
dataIndex: 'roleName',
39+
key: 'roleName',
40+
minWidth: 120,
41+
title: t('page.manage.role.roleName')
42+
},
43+
{
44+
align: 'center',
45+
dataIndex: 'roleCode',
46+
key: 'roleCode',
47+
minWidth: 120,
48+
title: t('page.manage.role.roleCode')
49+
},
50+
{
51+
dataIndex: 'roleDesc',
52+
key: 'roleDesc',
53+
minWidth: 120,
54+
title: t('page.manage.role.roleDesc')
55+
},
56+
{
57+
align: 'center',
58+
dataIndex: 'status',
59+
key: 'status',
60+
render: (_, record) => {
61+
if (record.status === null) {
62+
return null;
63+
}
64+
const label = t(enableStatusRecord[record.status]);
65+
return <ATag color={ATG_MAP[record.status]}>{label}</ATag>;
66+
},
67+
title: t('page.manage.user.userStatus'),
68+
width: 100
69+
},
70+
{
71+
align: 'center',
72+
key: 'operate',
73+
render: (_, record) => (
74+
<div className="flex-center gap-8px">
75+
<AButton
76+
ghost
77+
size="small"
78+
type="primary"
79+
onClick={() => edit(record.id)}
80+
>
81+
{t('common.edit')}
82+
</AButton>
83+
84+
<APopconfirm
85+
title={t('common.confirmDelete')}
86+
onConfirm={() => handleDelete(record.id)}
87+
>
88+
<AButton
89+
danger
90+
size="small"
91+
>
92+
{t('common.delete')}
93+
</AButton>
94+
</APopconfirm>
95+
</div>
96+
),
97+
title: t('common.operate'),
98+
width: 195
99+
}
100+
]
101+
});
102+
103+
const {
104+
checkedRowKeys,
105+
editingData,
106+
generalPopupOperation,
107+
handleAdd,
108+
handleEdit,
109+
onBatchDeleted,
110+
onDeleted,
111+
rowSelection
112+
} = useTableOperate(data, run, async (res, type) => {
113+
if (type === 'add') {
114+
// add request 调用新增的接口
115+
console.log(res);
116+
} else {
117+
// edit request 调用编辑的接口
118+
console.log(res);
119+
}
120+
});
121+
122+
async function handleBatchDelete() {
123+
// request
124+
console.log(checkedRowKeys);
125+
onBatchDeleted();
126+
}
127+
128+
function handleDelete(id: number) {
129+
// request
130+
console.log(id);
131+
132+
onDeleted();
133+
}
134+
135+
function edit(id: number) {
136+
handleEdit(id);
137+
}
138+
139+
return (
140+
<div className="h-full min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
141+
<ACollapse
142+
bordered={false}
143+
className="card-wrapper"
144+
defaultActiveKey={isMobile ? undefined : '1'}
145+
items={[
146+
{
147+
children: <RoleSearch {...searchProps} />,
148+
key: '1',
149+
label: t('common.search')
150+
}
151+
]}
152+
/>
153+
154+
<ACard
155+
className="flex-col-stretch sm:flex-1-hidden card-wrapper"
156+
ref={tableWrapperRef}
157+
title={t('page.manage.role.title')}
158+
variant="borderless"
159+
extra={
160+
<TableHeaderOperation
161+
add={handleAdd}
162+
columns={columnChecks}
163+
disabledDelete={checkedRowKeys.length === 0}
164+
loading={tableProps.loading}
165+
refresh={run}
166+
setColumnChecks={setColumnChecks}
167+
onDelete={handleBatchDelete}
168+
/>
169+
}
170+
>
171+
<ATable
172+
rowSelection={rowSelection}
173+
scroll={scrollConfig}
174+
size="small"
175+
{...tableProps}
176+
/>
177+
178+
<Suspense>
179+
<RoleOperateDrawer
180+
{...generalPopupOperation}
181+
rowId={editingData?.id || -1}
182+
/>
183+
</Suspense>
184+
</ACard>
185+
</div>
186+
);
3187
};
4188

5189
export default Role;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { DataNode } from 'antd/es/tree';
2+
3+
import type { ModulesProps } from './type';
4+
5+
const ButtonAuthModal: FC<ModulesProps> = memo(({ onClose, open, roleId }) => {
6+
const { t } = useTranslation();
7+
8+
const title = t('common.edit') + t('page.manage.role.buttonAuth');
9+
10+
const [checks, setChecks] = useState<number[]>();
11+
12+
const [tree, setTree] = useState<DataNode[]>();
13+
14+
async function getChecks() {
15+
console.log(roleId);
16+
// request
17+
setChecks([1, 2, 3, 4, 5]);
18+
}
19+
20+
async function getAllButtons() {
21+
// request
22+
setTree([
23+
{ key: 1, title: 'button1' },
24+
{ key: 2, title: 'button2' },
25+
{ key: 3, title: 'button3' },
26+
{ key: 4, title: 'button4' },
27+
{ key: 5, title: 'button5' },
28+
{ key: 6, title: 'button6' },
29+
{ key: 7, title: 'button7' },
30+
{ key: 8, title: 'button8' },
31+
{ key: 9, title: 'button9' },
32+
{ key: 10, title: 'button10' }
33+
]);
34+
}
35+
36+
function handleSubmit() {
37+
console.log(checks, roleId);
38+
// request
39+
40+
window.$message?.success?.(t('common.modifySuccess'));
41+
42+
onClose();
43+
}
44+
45+
function init() {
46+
getAllButtons();
47+
getChecks();
48+
}
49+
50+
useMount(() => {
51+
init();
52+
});
53+
54+
return (
55+
<AModal
56+
className="w-480px"
57+
open={open}
58+
title={title}
59+
footer={
60+
<ASpace className="mt-16px">
61+
<AButton
62+
size="small"
63+
onClick={onClose}
64+
>
65+
{t('common.cancel')}
66+
</AButton>
67+
<AButton
68+
size="small"
69+
type="primary"
70+
onClick={handleSubmit}
71+
>
72+
{t('common.confirm')}
73+
</AButton>
74+
</ASpace>
75+
}
76+
onCancel={onClose}
77+
>
78+
<ATree
79+
checkable
80+
checkedKeys={checks}
81+
className="h-280px"
82+
height={280}
83+
treeData={tree}
84+
onCheck={value => setChecks(value as number[])}
85+
/>
86+
</AModal>
87+
);
88+
});
89+
90+
export default ButtonAuthModal;

0 commit comments

Comments
 (0)