- ant design pro 如何去保存颜色
- ant design pro v6 如何做好角色管理
- ant design 的 tree 如何作为角色中的权限选择之一
- ant design 的 tree 如何作为角色中的权限选择之二
- ant design pro access.ts 是如何控制多角色的权限的
- ant design pro 中用户的表单如何控制多个角色
- ant design pro 如何实现动态菜单带上 icon 的
如上图这样,经常我们要加一些分类表,但是这些表是有层次的,比如父级,这种应该如何来表达得更好呢。
首先得存数据了
后端得先处理好。
我是这样弄的。
一般我存一个 parent ,指向父级就好,这样能通过 parent 找到所有的 children.
当然你要两边都存也行,但是每次修改都要操作就比较麻烦,但胜在读取数据,得到 children 比较简单。
我一般只存 parent.
import mongoose, { Document } from 'mongoose';export interface IPermissionGroup extends Document {name: string;parent?: IPermissionGroup;children?: IPermissionGroup[];createdAt?: Date;updatedAt?: Date;
}const permissionGroupSchema = new mongoose.Schema({name: { type: String, required: true, unique: true },parent: { type: mongoose.Schema.Types.ObjectId, ref: 'PermissionGroup' },},{ timestamps: true },
);const PermissionGroup = mongoose.model<IPermissionGroup>('PermissionGroup',permissionGroupSchema,
);export default PermissionGroup;
这里是没有存 children 的。但是需要把 children 返回给前端的。
// 获取权限组列表
const getPermissionGroups = handleAsync(async (req: Request, res: Response) => {const { current = '1', pageSize = '10' } = req.query;const query = buildQuery(req.query);// 执行查询const permissionGroups = await PermissionGroup.find(query).populate('parent') // Assuming you want to populate parent.sort('-createdAt') // Sort by creation time in descending order.skip((+current - 1) * +pageSize).limit(+pageSize).exec();const total = await PermissionGroup.countDocuments(query).exec();const getChildren = async (parentId: string | null): Promise<any[]> => {const children = await PermissionGroup.find({ parent: parentId }).populate('parent').exec();return Promise.all(children.map(async (child) => ({...child.toObject(),children: await getChildren(child._id),})),);};const getPermissionGroupsWithChildren = async (permissionGroups: any[],): Promise<any[]> => {return Promise.all(permissionGroups.map(async (permissionGroup) => ({...permissionGroup.toObject(),children: await getChildren(permissionGroup._id),})),);};const permissionGroupsWithChildren =await getPermissionGroupsWithChildren(permissionGroups);res.json({success: true,data: permissionGroupsWithChildren,total,current: +current,pageSize: +pageSize,});
});
多加了个循环,但胜在数据不会太多,没啥问题。
返回给前端的数据是这样的:
{"success": true,"data": [{"_id": "66b1b54ef8871ea52a7e3de9","name": "认证管理","createdAt": "2024-08-06T05:31:58.495Z","updatedAt": "2024-08-10T02:24:31.070Z","__v": 0,"children": [{"_id": "66b1b00bb5d937a0aef34034","name": "权限","createdAt": "2024-08-06T05:09:31.292Z","updatedAt": "2024-08-10T02:24:41.759Z","__v": 0,"parent": {"_id": "66b1b54ef8871ea52a7e3de9","name": "认证管理","createdAt": "2024-08-06T05:31:58.495Z","updatedAt": "2024-08-10T02:24:31.070Z","__v": 0},"children": []},{"_id": "66b6d2c9b9ad87dfa985f34f","name": "用户","parent": {"_id": "66b1b54ef8871ea52a7e3de9","name": "认证管理","createdAt": "2024-08-06T05:31:58.495Z","updatedAt": "2024-08-10T02:24:31.070Z","__v": 0},"createdAt": "2024-08-10T02:39:05.563Z","updatedAt": "2024-08-10T02:39:05.563Z","__v": 0,"children": []},{"_id": "66b6d2ddb9ad87dfa985f362","name": "菜单","parent": {"_id": "66b1b54ef8871ea52a7e3de9","name": "认证管理","createdAt": "2024-08-06T05:31:58.495Z","updatedAt": "2024-08-10T02:24:31.070Z","__v": 0},"createdAt": "2024-08-10T02:39:25.628Z","updatedAt": "2024-08-10T02:39:25.628Z","__v": 0,"children": []},{"_id": "66b6d2e9b9ad87dfa985f377","name": "角色","parent": {"_id": "66b1b54ef8871ea52a7e3de9","name": "认证管理","createdAt": "2024-08-06T05:31:58.495Z","updatedAt": "2024-08-10T02:24:31.070Z","__v": 0},"createdAt": "2024-08-10T02:39:37.339Z","updatedAt": "2024-08-10T02:39:37.339Z","__v": 0,"children": []},{"_id": "66b6d2fdb9ad87dfa985f38e","name": "数据权限","parent": {"_id": "66b1b54ef8871ea52a7e3de9","name": "认证管理","createdAt": "2024-08-06T05:31:58.495Z","updatedAt": "2024-08-10T02:24:31.070Z","__v": 0},"createdAt": "2024-08-10T02:39:57.756Z","updatedAt": "2024-08-10T02:39:57.756Z","__v": 0,"children": []},{"_id": "66b6d314b9ad87dfa985f3a7","name": "权限组","parent": {"_id": "66b1b54ef8871ea52a7e3de9","name": "认证管理","createdAt": "2024-08-06T05:31:58.495Z","updatedAt": "2024-08-10T02:24:31.070Z","__v": 0},"createdAt": "2024-08-10T02:40:20.528Z","updatedAt": "2024-08-10T02:40:20.528Z","__v": 0,"children": []},{"_id": "66b9ad348554e602536acc67","name": "认证管理菜单","parent": {"_id": "66b1b54ef8871ea52a7e3de9","name": "认证管理","createdAt": "2024-08-06T05:31:58.495Z","updatedAt": "2024-08-10T02:24:31.070Z","__v": 0},"createdAt": "2024-08-12T06:35:32.560Z","updatedAt": "2024-08-12T06:35:32.560Z","__v": 0,"children": []}]},{"_id": "66adec30d647a4fde5546b1c","name": "材料类目","createdAt": "2024-08-03T08:37:04.433Z","updatedAt": "2024-08-10T02:24:51.188Z","__v": 0,"children": []}],"total": 2,"current": 1,"pageSize": 20
}
children 就是显示出层次。
前端的代码是这样的:
import { useIntl } from '@umijs/max';
import { addItem, queryList, removeItem, updateItem } from '@/services/ant-design-pro/api';
import { PlusOutlined } from '@ant-design/icons';
import type { ActionType, ProColumns, ProDescriptionsItemProps } from '@ant-design/pro-components';
import { FooterToolbar, PageContainer, ProFormText, ProTable } from '@ant-design/pro-components';
import { FormattedMessage, useAccess } from '@umijs/max';
import { Button, message, TreeSelect } from 'antd';
import React, { useRef, useState } from 'react';
import type { FormValueType } from './components/Update';
import Update from './components/Update';
import Create from './components/Create';
import useQueryList from '@/hooks/useQueryList';
import Show from './components/Show';
import DeleteButton from '@/components/DeleteButton';
import DeleteLink from '@/components/DeleteLink';/*** @en-US Add node* @zh-CN 添加节点* @param fields*/
const handleAdd = async (fields: API.ItemData) => {const hide = message.loading(<FormattedMessage id="adding" defaultMessage="Adding..." />);try {await addItem('/permission-groups', { ...fields });hide();message.success(<FormattedMessage id="add_successful" defaultMessage="Added successfully" />);return true;} catch (error: any) {hide();message.error(error?.response?.data?.message ?? (<FormattedMessage id="upload_failed" defaultMessage="Upload failed, please try again!" />),);return false;}
};/*** @en-US Update node* @zh-CN 更新节点** @param fields*/
const handleUpdate = async (fields: FormValueType) => {const hide = message.loading(<FormattedMessage id="updating" defaultMessage="Updating..." />);try {await updateItem(`/permission-groups/${fields._id}`, fields);hide();message.success(<FormattedMessage id="update_successful" defaultMessage="Update successful" />);return true;} catch (error: any) {hide();message.error(error?.response?.data?.message ?? (<FormattedMessage id="update_failed" defaultMessage="Update failed, please try again!" />),);return false;}
};/*** Delete node* @zh-CN 删除节点** @param selectedRows*/
const handleRemove = async (ids: string[]) => {const hide = message.loading(<FormattedMessage id="deleting" defaultMessage="Deleting..." />);if (!ids) return true;try {await removeItem('/permission-groups', {ids,});hide();message.success(<FormattedMessageid="delete_successful"defaultMessage="Deleted successfully and will refresh soon"/>,);return true;} catch (error: any) {hide();message.error(error.response.data.message ?? (<FormattedMessage id="delete_failed" defaultMessage="Delete failed, please try again" />),);return false;}
};const TableList: React.FC = () => {const intl = useIntl();/*** @en-US Pop-up window of new window* @zh-CN 新建窗口的弹窗* */const [createModalOpen, handleModalOpen] = useState<boolean>(false);/**2024fc.xyz* @en-US The pop-up window of the distribution update window* @zh-CN 分布更新窗口的弹窗* */const [updateModalOpen, handleUpdateModalOpen] = useState<boolean>(false);// const [batchUploadPriceModalOpen, setBatchUploadPriceModalOpen] = useState<boolean>(false);const actionRef = useRef<ActionType>();const [currentRow, setCurrentRow] = useState<API.ItemData>();const [selectedRowsState, setSelectedRows] = useState<API.ItemData[]>([]);const [showDetail, setShowDetail] = useState<boolean>(false);const access = useAccess();const { items: permissionGroup, loading } = useQueryList('/permission-groups');/*** @en-US International configuration* @zh-CN 国际化配置* */// Define roles object with index signatureconst columns: ProColumns<API.ItemData>[] = [{title: intl.formatMessage({ id: 'name' }),dataIndex: 'name',copyable: true,renderFormItem: (item, { ...rest }) => {return <ProFormText {...rest} placeholder={intl.formatMessage({ id: 'enter_name' })} />;},render: (dom, entity) => {return (<aonClick={() => {setCurrentRow(entity);setShowDetail(true);}}>{dom}</a>);},},{title: intl.formatMessage({ id: 'parent_permissionGroup' }),dataIndex: ['parent', 'name'],hideInSearch: true,// @ts-ignore// eslint-disable-next-line @typescript-eslint/no-unused-varsrenderFormItem: (_, { type, defaultRender, formItemProps, fieldProps, ...rest }, form) => {if (type === 'form') {return null;}return (<TreeSelectshowSearchstyle={{ width: '100%' }}dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}placeholder={intl.formatMessage({ id: 'parent_permissionGroup' })}allowCleartreeNodeFilterProp="name"fieldNames={{ label: 'name', value: '_id', children: 'children' }}treeDefaultExpandAlltreeData={permissionGroup}loading={loading}{...fieldProps}/>);},},{title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="Operating" />,dataIndex: 'option',valueType: 'option',render: (_, record) => [access.canSuperAdmin && (<akey="edit"onClick={() => {// Replace `handleUpdateModalOpen` and `setCurrentRow` with your actual functionshandleUpdateModalOpen(true);setCurrentRow(record);}}>{intl.formatMessage({ id: 'edit' })}</a>),access.canSuperAdmin && (<DeleteLinkonOk={async () => {await handleRemove([record._id!]);setSelectedRows([]);actionRef.current?.reloadAndRest?.();}}/>),],},];return (<PageContainer><ProTable<API.ItemData, API.PageParams>headerTitle={intl.formatMessage({ id: 'list' })}actionRef={actionRef}rowKey="_id"search={{labelWidth: 100,}}toolBarRender={() => [(access.canSuperAdmin || access.canUpdatePermissionGroup) && (<Buttontype="primary"key="primary"onClick={() => {handleModalOpen(true);}}><PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="New" /></Button>),]}request={async (params, sort, filter) =>queryList('/permission-groups', params, sort, filter)}columns={columns}rowSelection={access.canSuperAdmin && {onChange: (_, selectedRows) => {setSelectedRows(selectedRows);},}}/>{selectedRowsState?.length > 0 && (<FooterToolbarextra={<div><FormattedMessage id="pages.searchTable.chosen" defaultMessage="Chosen" />{' '}<a style={{ fontWeight: 600 }}>{selectedRowsState.length}</a>{' '}<FormattedMessage id="pages.searchTable.item" defaultMessage="项" /></div>}>{(access.canSuperAdmin || access.canDeletePermissionGroup) && (<DeleteButtononOk={async () => {await handleRemove(selectedRowsState?.map((item: any) => item._id!));setSelectedRows([]);actionRef.current?.reloadAndRest?.();}}/>)}</FooterToolbar>)}{(access.canSuperAdmin || access.canCreatePermissionGroup) && (<Createopen={createModalOpen}onOpenChange={handleModalOpen}onFinish={async (value) => {const success = await handleAdd(value as API.ItemData);if (success) {handleModalOpen(false);if (actionRef.current) {actionRef.current.reload();}}}}/>)}{(access.canSuperAdmin || access.canUpdatePermissionGroup) && (<UpdateonSubmit={async (value) => {const success = await handleUpdate(value);if (success) {handleUpdateModalOpen(false);setCurrentRow(undefined);if (actionRef.current) {actionRef.current.reload();}}}}onCancel={handleUpdateModalOpen}updateModalOpen={updateModalOpen}values={currentRow || {}}/>)}<Showopen={showDetail}currentRow={currentRow as API.ItemData}columns={columns as ProDescriptionsItemProps<API.ItemData>[]}onClose={() => {setCurrentRow(undefined);setShowDetail(false);}}/></PageContainer>);
};export default TableList;
表单的话比较简单:
import { useIntl } from '@umijs/max';
import React from 'react';
import { ProForm, ProFormText } from '@ant-design/pro-components';
import { Form, Input } from 'antd';
import PermissionGroupSelect from '@/components/PermissionGroupSelect';interface Props {newRecord?: boolean;onFinish: (formData: any) => Promise<void>;values?: any;
}const BasicForm: React.FC<Props> = ({ newRecord, onFinish, values }) => {const intl = useIntl();return (<ProForminitialValues={{...values,parent: values?.parent?._id,}}onFinish={async (values) => {await onFinish({...values,});}}><ProForm.Group><ProFormTextrules={[{ required: true, message: intl.formatMessage({ id: 'enter_name' }) }]}width="md"label={intl.formatMessage({ id: 'name' })}name="name"/><PermissionGroupSelect name="parent" label="permission_group" /></ProForm.Group>{!newRecord && (<Form.Item name="_id" label={false}><Input type="hidden" /></Form.Item>)}</ProForm>);
};export default BasicForm;
initialValues={{
…values,
parent: values?.parent?._id,
}}
这块仍然跟编辑的时候有关,可以填上它的值
PermissionGroupSelect 的源码是这样的:
import React from 'react';
import { ProFormTreeSelect } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max';
import useQueryList from '@/hooks/useQueryList';const PermissionGroupSelect = ({ name, label }: { name: string; label: string }) => {const intl = useIntl();const { items: permissionGroups, loading } = useQueryList('/permission-groups');return (<ProFormTreeSelectname={name}rules={[{ required: false }]}width="md"label={intl.formatMessage({ id: label })}allowClearsecondaryfieldProps={{showArrow: false,treeDefaultExpandAll: true,filterTreeNode: true,showSearch: true,dropdownMatchSelectWidth: false,autoClearSearchValue: true,treeNodeFilterProp: 'name',fieldNames: {label: 'name',value: '_id',children: 'children',},treeData: permissionGroups,loading,}}/>);
};export default PermissionGroupSelect;
我们拥有 12 年建站编程经验
- 虚拟产品交易平台定制开发
- WordPress 外贸电商独立站建站
我的网站