RoleAssignMenuForm.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import React, { useState, useImperativeHandle, forwardRef, useEffect } from 'react';
  2. import { roleApi, RoleVO } from '../api/Role';
  3. import { menuApi, MenuVO } from '../api/Menu';
  4. import { toast } from 'sonner';
  5. import { Modal, Form, Tag, Card, Switch, Tree, Spin, Space } from 'antd';
  6. import { handleTree } from '../utils/tree';
  7. import type { DataNode } from 'antd/es/tree';
  8. import { useTranslation } from 'react-i18next';
  9. interface RoleAssignMenuFormProps {
  10. onSuccess?: () => void;
  11. }
  12. export interface RoleAssignMenuFormRef {
  13. open: (row: RoleVO) => void;
  14. }
  15. const RoleAssignMenuForm = forwardRef<RoleAssignMenuFormRef, RoleAssignMenuFormProps>(({ onSuccess }, ref) => {
  16. const { t } = useTranslation();
  17. const [dialogVisible, setDialogVisible] = useState(false);
  18. const [formLoading, setFormLoading] = useState(false);
  19. const [formData, setFormData] = useState<{
  20. id?: number;
  21. name: string;
  22. code: string;
  23. menuIds: number[];
  24. }>({
  25. id: undefined,
  26. name: '',
  27. code: '',
  28. menuIds: [],
  29. });
  30. const [menuOptions, setMenuOptions] = useState<DataNode[]>([]);
  31. const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([]);
  32. const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
  33. const [menuExpand, setMenuExpand] = useState(false);
  34. const [treeNodeAll, setTreeNodeAll] = useState(false);
  35. const [form] = Form.useForm();
  36. // 将菜单数据转换为 Tree 组件需要的格式
  37. const convertMenuToTreeData = (menus: MenuVO[]): DataNode[] => {
  38. return menus.map((menu) => ({
  39. title: menu.name,
  40. key: menu.id!,
  41. children: menu.children ? convertMenuToTreeData(menu.children) : undefined,
  42. }));
  43. };
  44. // 暴露方法给父组件
  45. useImperativeHandle(ref, () => ({
  46. open: async (row: RoleVO) => {
  47. setDialogVisible(true);
  48. resetForm();
  49. // 设置数据
  50. setFormData({
  51. id: row.id,
  52. name: row.name,
  53. code: row.code,
  54. menuIds: [],
  55. });
  56. // 加载菜单列表
  57. try {
  58. const menus = await menuApi.getSimpleMenusList();
  59. const menuData = (menus as any)?.data || menus;
  60. const treeData = handleTree(menuData || []);
  61. const treeNodes = convertMenuToTreeData(treeData);
  62. setMenuOptions(treeNodes);
  63. // 获取角色菜单权限列表
  64. setFormLoading(true);
  65. try {
  66. const menuIds = await roleApi.getRoleMenuList(row.id!);
  67. const menuIdsData = (menuIds as any)?.data || menuIds;
  68. setFormData(prev => ({ ...prev, menuIds: menuIdsData || [] }));
  69. setCheckedKeys((menuIdsData || []).map((id: number) => id));
  70. } catch (error: any) {
  71. console.error('获取角色菜单权限失败:', error);
  72. } finally {
  73. setFormLoading(false);
  74. }
  75. } catch (error: any) {
  76. console.error('加载菜单列表失败:', error);
  77. toast.error('加载菜单列表失败');
  78. }
  79. },
  80. }));
  81. // 提交表单
  82. const submitForm = async () => {
  83. try {
  84. setFormLoading(true);
  85. // 获取所有选中的节点(包括半选中的父节点)
  86. const checked = checkedKeys as number[];
  87. const data = {
  88. roleId: formData.id!,
  89. menuIds: checked,
  90. };
  91. await roleApi.assignRoleMenu(data);
  92. toast.success('更新成功');
  93. setDialogVisible(false);
  94. onSuccess?.();
  95. } catch (error: any) {
  96. toast.error(error.message || '操作失败');
  97. } finally {
  98. setFormLoading(false);
  99. }
  100. };
  101. // 重置表单
  102. const resetForm = () => {
  103. setTreeNodeAll(false);
  104. setMenuExpand(false);
  105. setFormData({
  106. id: undefined,
  107. name: '',
  108. code: '',
  109. menuIds: [],
  110. });
  111. setCheckedKeys([]);
  112. setExpandedKeys([]);
  113. form.resetFields();
  114. };
  115. // 全选/全不选
  116. const handleCheckedTreeNodeAll = (checked: boolean) => {
  117. setTreeNodeAll(checked);
  118. if (checked) {
  119. // 获取所有节点的 key
  120. const getAllKeys = (nodes: DataNode[]): React.Key[] => {
  121. let keys: React.Key[] = [];
  122. nodes.forEach((node) => {
  123. keys.push(node.key);
  124. if (node.children) {
  125. keys = keys.concat(getAllKeys(node.children));
  126. }
  127. });
  128. return keys;
  129. };
  130. setCheckedKeys(getAllKeys(menuOptions));
  131. } else {
  132. setCheckedKeys([]);
  133. }
  134. };
  135. // 展开/折叠全部
  136. const handleCheckedTreeExpand = (expanded: boolean) => {
  137. setMenuExpand(expanded);
  138. if (expanded) {
  139. const getAllKeys = (nodes: DataNode[]): React.Key[] => {
  140. let keys: React.Key[] = [];
  141. nodes.forEach((node) => {
  142. keys.push(node.key);
  143. if (node.children) {
  144. keys = keys.concat(getAllKeys(node.children));
  145. }
  146. });
  147. return keys;
  148. };
  149. setExpandedKeys(getAllKeys(menuOptions));
  150. } else {
  151. setExpandedKeys([]);
  152. }
  153. };
  154. // 树节点选中变化
  155. const onCheck = (checked: React.Key[]) => {
  156. setCheckedKeys(checked);
  157. // 如果全部选中,设置全选状态
  158. const getAllKeys = (nodes: DataNode[]): React.Key[] => {
  159. let keys: React.Key[] = [];
  160. nodes.forEach((node) => {
  161. keys.push(node.key);
  162. if (node.children) {
  163. keys = keys.concat(getAllKeys(node.children));
  164. }
  165. });
  166. return keys;
  167. };
  168. const allKeys = getAllKeys(menuOptions);
  169. setTreeNodeAll(checked.length === allKeys.length && allKeys.length > 0);
  170. };
  171. return (
  172. <Modal
  173. title="菜单权限"
  174. open={dialogVisible}
  175. onCancel={() => setDialogVisible(false)}
  176. onOk={submitForm}
  177. confirmLoading={formLoading}
  178. okText={t('common.confirm')}
  179. cancelText={t('common.cancel')}
  180. width={800}
  181. destroyOnClose
  182. >
  183. <Spin spinning={formLoading}>
  184. <Form
  185. form={form}
  186. layout="horizontal"
  187. labelCol={{ span: 4 }}
  188. wrapperCol={{ span: 20 }}
  189. >
  190. <Form.Item label="角色名称">
  191. <Tag>{formData.name}</Tag>
  192. </Form.Item>
  193. <Form.Item label="角色标识">
  194. <Tag>{formData.code}</Tag>
  195. </Form.Item>
  196. <Form.Item label="菜单权限">
  197. <Card
  198. title={
  199. <Space>
  200. <span>全选/全不选:</span>
  201. <Switch
  202. checked={treeNodeAll}
  203. onChange={handleCheckedTreeNodeAll}
  204. checkedChildren="是"
  205. unCheckedChildren="否"
  206. />
  207. <span style={{ marginLeft: 16 }}>全部展开/折叠:</span>
  208. <Switch
  209. checked={menuExpand}
  210. onChange={handleCheckedTreeExpand}
  211. checkedChildren="展开"
  212. unCheckedChildren="折叠"
  213. />
  214. </Space>
  215. }
  216. style={{ maxHeight: 400, overflowY: 'auto' }}
  217. >
  218. <Tree
  219. checkable
  220. checkedKeys={checkedKeys}
  221. expandedKeys={expandedKeys}
  222. onCheck={onCheck}
  223. onExpand={setExpandedKeys}
  224. treeData={menuOptions}
  225. defaultExpandAll={false}
  226. />
  227. </Card>
  228. </Form.Item>
  229. </Form>
  230. </Spin>
  231. </Modal>
  232. );
  233. });
  234. RoleAssignMenuForm.displayName = 'RoleAssignMenuForm';
  235. export default RoleAssignMenuForm;