Roles.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  1. import React, { useState, useEffect } from 'react';
  2. import {
  3. Card,
  4. Table,
  5. Button,
  6. Input,
  7. Modal,
  8. message,
  9. Space,
  10. Row,
  11. Col,
  12. Form,
  13. Tree,
  14. Tabs,
  15. Tag,
  16. Radio,
  17. Switch,
  18. } from 'antd';
  19. import {
  20. PlusOutlined,
  21. SearchOutlined,
  22. ReloadOutlined,
  23. EditOutlined,
  24. SafetyOutlined,
  25. } from '@ant-design/icons';
  26. import type { ColumnsType } from 'antd/es/table';
  27. import type { DataNode } from 'antd/es/tree';
  28. import CustomPagination from '../../components/CustomPagination';
  29. import { queryGroupList, saveGroup, getGroupMenuDetail, saveGroupMenu, type GroupItem } from '../../api/system';
  30. import { getMenuTree } from '../../api/menu';
  31. const { TabPane } = Tabs;
  32. /**
  33. * 检查响应是否成功(兼容字符串 '0' 和数字 0)
  34. */
  35. const isSuccess = (errorCode: string | number | undefined): boolean => {
  36. return errorCode === '0' || errorCode === 0;
  37. };
  38. // 分页参数
  39. interface PaginationParams {
  40. current: number;
  41. pageSize: number;
  42. total: number;
  43. }
  44. // 安全级别选项
  45. const safeClassOptions = [
  46. { value: '1', label: '普通' },
  47. { value: '2', label: '重要' },
  48. { value: '3', label: '核心' },
  49. ];
  50. // 默认菜单类型选项
  51. const menuTypeOptions = [
  52. { value: '1', label: '类型1' },
  53. { value: '2', label: '类型2' },
  54. ];
  55. const Roles: React.FC = () => {
  56. const [form] = Form.useForm();
  57. const [data, setData] = useState<GroupItem[]>([]);
  58. const [loading, setLoading] = useState(false);
  59. const [pagination, setPagination] = useState<PaginationParams>({
  60. current: 1,
  61. pageSize: 10,
  62. total: 0
  63. });
  64. // 查询条件
  65. const [searchCode, setSearchCode] = useState('');
  66. const [searchName, setSearchName] = useState('');
  67. // 角色编码序号(从100000开始)
  68. const [roleCodeCounter, setRoleCodeCounter] = useState<number>(100000);
  69. // 弹窗状态
  70. const [modalVisible, setModalVisible] = useState(false);
  71. const [modalTitle, setModalTitle] = useState('新增角色');
  72. const [editingRecord, setEditingRecord] = useState<GroupItem | null>(null);
  73. // 权限配置弹窗
  74. const [authVisible, setAuthVisible] = useState(false);
  75. const [authRecord, setAuthRecord] = useState<GroupItem | null>(null);
  76. const [menuTreeData, setMenuTreeData] = useState<DataNode[]>([]);
  77. const [selectedMenuIds, setSelectedMenuIds] = useState<string[]>([]);
  78. const [activeTab, setActiveTab] = useState('menu');
  79. const [authLoading, setAuthLoading] = useState(false);
  80. // 表格列定义
  81. const columns: ColumnsType<GroupItem> = [
  82. {
  83. title: '序号',
  84. key: 'index',
  85. width: 60,
  86. render: (_, __, index) => (pagination.current - 1) * pagination.pageSize + index + 1
  87. },
  88. {
  89. title: '角色编码',
  90. dataIndex: 'code',
  91. key: 'code',
  92. width: 120
  93. },
  94. {
  95. title: '角色名称',
  96. dataIndex: 'descripts',
  97. key: 'descripts',
  98. width: 150
  99. },
  100. {
  101. title: '英文名称',
  102. dataIndex: 'enDesc',
  103. key: 'enDesc',
  104. width: 150
  105. },
  106. {
  107. title: '安全级别',
  108. dataIndex: 'safeClassificat',
  109. key: 'safeClassificat',
  110. width: 100,
  111. render: (value: string) => {
  112. const option = safeClassOptions.find(o => o.value === value);
  113. return option?.label || value || '-';
  114. }
  115. },
  116. {
  117. title: '允许列编辑',
  118. dataIndex: 'columnEdit',
  119. key: 'columnEdit',
  120. width: 100,
  121. render: (value: string) => (
  122. <Tag color={value === 'Y' ? 'green' : 'default'}>
  123. {value === 'Y' ? '是' : '否'}
  124. </Tag>
  125. )
  126. },
  127. {
  128. title: '允许布局编辑',
  129. dataIndex: 'layoutEdit',
  130. key: 'layoutEdit',
  131. width: 105,
  132. render: (value: string) => (
  133. <Tag color={value === 'Y' ? 'green' : 'default'}>
  134. {value === 'Y' ? '是' : '否'}
  135. </Tag>
  136. )
  137. },
  138. {
  139. title: '生效日期',
  140. dataIndex: 'startDate',
  141. key: 'startDate',
  142. width: 120,
  143. render: (value) => value ? new Date(value).toLocaleDateString() : '-'
  144. },
  145. {
  146. title: '状态',
  147. key: 'status',
  148. width: 80,
  149. render: (_, record) => {
  150. // 根据生效日期和失效日期判断状态
  151. const now = new Date();
  152. const startDate = record.startDate ? new Date(record.startDate) : null;
  153. const endDate = record.endDate ? new Date(record.endDate) : null;
  154. const isActive = (!startDate || startDate <= now) && (!endDate || endDate >= now);
  155. return (
  156. <Tag color={isActive ? 'green' : 'red'}>
  157. {isActive ? '在用' : '停用'}
  158. </Tag>
  159. );
  160. }
  161. },
  162. {
  163. title: '失效日期',
  164. dataIndex: 'endDate',
  165. key: 'endDate',
  166. width: 120,
  167. render: (value) => value ? new Date(value).toLocaleDateString() : '-'
  168. },
  169. {
  170. title: '操作',
  171. key: 'action',
  172. fixed: 'right',
  173. width: 150,
  174. render: (_, record) => (
  175. <Space size="small">
  176. <Button
  177. type="link"
  178. size="small"
  179. icon={<SafetyOutlined />}
  180. onClick={() => handleAuth(record)}
  181. >
  182. 权限
  183. </Button>
  184. <Button
  185. type="link"
  186. size="small"
  187. icon={<EditOutlined />}
  188. onClick={() => handleEdit(record)}
  189. >
  190. 编辑
  191. </Button>
  192. </Space>
  193. )
  194. }
  195. ];
  196. // 查询角色列表
  197. const fetchData = async (page = pagination.current, size = pagination.pageSize) => {
  198. setLoading(true);
  199. try {
  200. const res = await queryGroupList(
  201. { code: searchCode, descripts: searchName },
  202. { pageSize: size, currentPage: page }
  203. );
  204. // 兼容字符串和数字类型的 errorCode
  205. if (isSuccess(res.errorCode) && res.result) {
  206. const resultData = res.result as any;
  207. setData(resultData.rows || []);
  208. setPagination(prev => ({
  209. ...prev,
  210. total: resultData.total || 0
  211. }));
  212. } else {
  213. message.error(res.errorMessage || '获取角色列表失败');
  214. }
  215. } catch (error) {
  216. console.error('查询角色失败:', error);
  217. message.error('查询角色失败');
  218. } finally {
  219. setLoading(false);
  220. }
  221. };
  222. // 初始化加载
  223. useEffect(() => {
  224. fetchData(1, pagination.pageSize);
  225. }, []);
  226. // 搜索
  227. const handleSearch = () => {
  228. setPagination(prev => ({ ...prev, current: 1 }));
  229. fetchData(1, pagination.pageSize);
  230. };
  231. // 重置
  232. const handleReset = () => {
  233. setSearchCode('');
  234. setSearchName('');
  235. setPagination(prev => ({ ...prev, current: 1 }));
  236. fetchData(1, pagination.pageSize);
  237. };
  238. // 按序号生成6位数字编码
  239. const generateCode = () => {
  240. // 获取当前序号
  241. const currentCounter = roleCodeCounter;
  242. // 确保编码不超过999999
  243. if (currentCounter > 999999) {
  244. message.error('角色编码已达到最大值999999');
  245. return '999999';
  246. }
  247. // 直接使用当前序号作为编码
  248. const code = currentCounter.toString();
  249. console.log('生成的顺序编码:', code, '当前序号:', currentCounter);
  250. // 递增序号
  251. setRoleCodeCounter(prev => prev + 1);
  252. return code;
  253. };
  254. // 新增
  255. const handleAdd = () => {
  256. setEditingRecord(null);
  257. setModalTitle('新增角色');
  258. form.resetFields();
  259. // 先生成角色编码
  260. const newCode = generateCode();
  261. // 设置默认日期:生效日期为今天,失效日期为空
  262. const today = new Date();
  263. const todayStr = today.toISOString().split('T')[0];
  264. setModalVisible(true);
  265. // 使用 nextTick 确保在弹框渲染完成后设置表单值
  266. setTimeout(() => {
  267. form.setFieldsValue({
  268. code: newCode,
  269. operCodeTable: 'N',
  270. sendMsgToAllUser: 'N',
  271. safeClassificat: '1',
  272. columnEdit: 'N',
  273. layoutEdit: 'N',
  274. startDate: todayStr,
  275. endDate: undefined
  276. });
  277. }, 0);
  278. };
  279. // 编辑
  280. const handleEdit = (record: GroupItem) => {
  281. setEditingRecord(record);
  282. setModalTitle('编辑角色');
  283. form.resetFields();
  284. setModalVisible(true);
  285. setTimeout(() => {
  286. form.setFieldsValue({
  287. id: record.id,
  288. code: record.code,
  289. descripts: record.descripts,
  290. enDesc: record.enDesc,
  291. mainInterface: record.mainInterface,
  292. operCodeTable: record.operCodeTable || 'N',
  293. sendMsgToAllUser: record.sendMsgToAllUser || 'N',
  294. safeClassificat: record.safeClassificat || '1',
  295. columnEdit: record.columnEdit || 'N',
  296. layoutEdit: record.layoutEdit || 'N',
  297. defaultMenuType: record.defaultMenuType,
  298. startDate: record.startDate,
  299. endDate: record.endDate
  300. });
  301. }, 0);
  302. };
  303. // 更新角色状态(在用/停用)
  304. const handleStatusChange = async (record: GroupItem, checked: boolean) => {
  305. const now = new Date();
  306. const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  307. try {
  308. const params = {
  309. id: String(record.id),
  310. code: record.code,
  311. descripts: record.descripts,
  312. startDate: record.startDate,
  313. endDate: checked ? undefined : today.toISOString().split('T')[0] // 停用时设置失效日期为今天
  314. };
  315. const res = await saveGroup(params as any);
  316. // 兼容字符串和数字类型的 errorCode
  317. if (isSuccess(res.errorCode)) {
  318. message.success(checked ? '已启用' : '已停用');
  319. fetchData(pagination.current, pagination.pageSize);
  320. } else {
  321. message.error(res.errorMessage || '状态更新失败');
  322. }
  323. } catch (error) {
  324. console.error('更新角色状态失败:', error);
  325. message.error('更新角色状态失败');
  326. }
  327. };
  328. // 保存角色
  329. const handleSave = async () => {
  330. try {
  331. const values = await form.validateFields();
  332. const params = {
  333. id: editingRecord ? String(editingRecord.id) : undefined,
  334. code: values.code,
  335. descripts: values.descripts,
  336. enDesc: values.enDesc,
  337. mainInterface: values.mainInterface,
  338. operCodeTable: values.operCodeTable,
  339. sendMsgToAllUser: values.sendMsgToAllUser,
  340. safeClassificat: values.safeClassificat,
  341. columnEdit: values.columnEdit,
  342. layoutEdit: values.layoutEdit,
  343. defaultMenuType: values.defaultMenuType,
  344. startDate: values.startDate,
  345. endDate: values.endDate || undefined,
  346. };
  347. const res = await saveGroup(params);
  348. // 兼容字符串和数字类型的 errorCode
  349. if (isSuccess(res.errorCode)) {
  350. message.success(editingRecord ? '修改成功' : '新增成功');
  351. setModalVisible(false);
  352. fetchData(pagination.current, pagination.pageSize);
  353. } else {
  354. message.error(res.errorMessage || '保存失败');
  355. }
  356. } catch (error) {
  357. console.error('保存角色失败:', error);
  358. message.error('保存角色失败');
  359. }
  360. };
  361. // 打开权限配置弹窗
  362. const handleAuth = async (record: GroupItem) => {
  363. setAuthRecord(record);
  364. setAuthVisible(true);
  365. setActiveTab('menu');
  366. setSelectedMenuIds([]);
  367. setMenuTreeData([]); // 清空旧数据
  368. setAuthLoading(true);
  369. try {
  370. // 先加载菜单树
  371. const menuRes = await getMenuTree({});
  372. console.log('菜单树响应:', menuRes);
  373. console.log('errorCode类型:', typeof menuRes.errorCode, '值:', menuRes.errorCode);
  374. console.log('result:', menuRes.result);
  375. // 兼容字符串和数字类型的 errorCode
  376. if (isSuccess(menuRes.errorCode) && menuRes.result) {
  377. const menuList = menuRes.result as any[];
  378. console.log('菜单列表长度:', menuList.length);
  379. const convertToTreeData = (menus: any[]): DataNode[] => {
  380. return menus.map(menu => ({
  381. key: String(menu.id || ''),
  382. title: menu.label || '',
  383. children: menu.children && menu.children.length > 0
  384. ? convertToTreeData(menu.children)
  385. : undefined
  386. }));
  387. };
  388. const treeData = convertToTreeData(menuList);
  389. console.log('转换后的树数据:', treeData);
  390. setMenuTreeData(treeData);
  391. // 再获取角色已配置的菜单
  392. const res = await getGroupMenuDetail({ groupID: String(record.id), type: '1' });
  393. console.log('角色菜单详情:', res);
  394. // 兼容字符串和数字类型的 errorCode
  395. if (isSuccess(res.errorCode) && res.result) {
  396. const resultData = res.result as any;
  397. if (resultData.rows && resultData.rows.length > 0) {
  398. const selectedIds = resultData.rows.map((r: any) => String(r.menuDetailID));
  399. console.log('已选中菜单IDs:', selectedIds);
  400. setSelectedMenuIds(selectedIds);
  401. }
  402. }
  403. } else {
  404. message.warning('获取菜单树失败: ' + (menuRes.errorMessage || '未知错误'));
  405. }
  406. } catch (error) {
  407. console.error('加载权限数据失败:', error);
  408. message.error('加载权限数据失败');
  409. } finally {
  410. setAuthLoading(false);
  411. }
  412. };
  413. // 保存权限配置
  414. const handleSaveAuth = async () => {
  415. if (!authRecord) return;
  416. try {
  417. const menuDetail = selectedMenuIds.map(menuId => ({
  418. menuDetailID: menuId,
  419. seqNo: '',
  420. preMenuGroupID: ''
  421. }));
  422. const res = await saveGroupMenu({
  423. groupID: String(authRecord.id),
  424. type: '1',
  425. preMenuGroupID: '',
  426. menuDetail
  427. });
  428. // 兼容字符串和数字类型的 errorCode
  429. if (isSuccess(res.errorCode)) {
  430. message.success('权限配置保存成功');
  431. setAuthVisible(false);
  432. // 提示用户重新登录以使权限生效
  433. Modal.confirm({
  434. title: '权限已更新',
  435. content: `角色"${authRecord.descripts}"的权限配置已保存。请重新登录使新权限生效。`,
  436. okText: '立即重新登录',
  437. cancelText: '稍后登录',
  438. onOk: () => {
  439. // 清除 session 并刷新页面回到登录页
  440. localStorage.removeItem('drg_session');
  441. window.location.href = '/';
  442. }
  443. });
  444. } else {
  445. message.error(res.errorMessage || '保存权限失败');
  446. }
  447. } catch (error) {
  448. console.error('保存权限失败:', error);
  449. message.error('保存权限失败');
  450. }
  451. };
  452. // Tree复选事件处理
  453. const handleTreeCheck = (checkedKeys: any) => {
  454. if (Array.isArray(checkedKeys)) {
  455. setSelectedMenuIds(checkedKeys.map(String));
  456. } else if (checkedKeys && typeof checkedKeys === 'object' && 'checked' in checkedKeys) {
  457. setSelectedMenuIds((checkedKeys.checked || []).map(String));
  458. }
  459. };
  460. return (
  461. <div style={{ padding: 16 }}>
  462. {/* 查询条件 */}
  463. <Card size="small" style={{ marginBottom: 16 }}>
  464. <Row gutter={16} align="middle">
  465. <Col>
  466. <Input
  467. placeholder="角色编码"
  468. value={searchCode}
  469. onChange={e => setSearchCode(e.target.value)}
  470. style={{ width: 140 }}
  471. allowClear
  472. />
  473. </Col>
  474. <Col>
  475. <Input
  476. placeholder="角色名称"
  477. value={searchName}
  478. onChange={e => setSearchName(e.target.value)}
  479. style={{ width: 140 }}
  480. allowClear
  481. />
  482. </Col>
  483. <Col>
  484. <Space>
  485. <Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>查询</Button>
  486. <Button icon={<ReloadOutlined />} onClick={handleReset}>重置</Button>
  487. </Space>
  488. </Col>
  489. </Row>
  490. </Card>
  491. {/* 工具栏 + 表格 */}
  492. <Card size="small">
  493. <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 12 }}>
  494. <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增角色</Button>
  495. <span>共 {pagination.total} 条记录</span>
  496. </div>
  497. <Table
  498. columns={columns}
  499. dataSource={data}
  500. rowKey="id"
  501. loading={loading}
  502. scroll={{ x: 1200 }}
  503. size="small"
  504. pagination={false}
  505. />
  506. <div style={{ marginTop: 12, display: 'flex', justifyContent: 'flex-end' }}>
  507. <CustomPagination
  508. current={pagination.current}
  509. pageSize={pagination.pageSize}
  510. total={pagination.total}
  511. onChange={(page, size) => {
  512. setPagination(prev => ({ ...prev, current: page, pageSize: size }));
  513. fetchData(page, size);
  514. }}
  515. />
  516. </div>
  517. </Card>
  518. {/* 新增/编辑弹窗 */}
  519. <Modal
  520. title={modalTitle}
  521. open={modalVisible}
  522. onOk={handleSave}
  523. onCancel={() => setModalVisible(false)}
  524. okText="确定"
  525. cancelText="取消"
  526. width={700}
  527. >
  528. <Form form={form} layout="vertical">
  529. <Row gutter={16}>
  530. <Col span={12}>
  531. <Form.Item
  532. name="code"
  533. label="角色编码"
  534. rules={[{ required: true, message: '请输入角色编码' }]}
  535. >
  536. <Input placeholder="系统自动生成6位数字编码" maxLength={6} disabled readOnly />
  537. </Form.Item>
  538. </Col>
  539. <Col span={12}>
  540. <Form.Item
  541. name="descripts"
  542. label="角色名称"
  543. rules={[{ required: true, message: '请输入角色名称' }]}
  544. >
  545. <Input placeholder="请输入角色名称" maxLength={50} />
  546. </Form.Item>
  547. </Col>
  548. </Row>
  549. <Row gutter={16}>
  550. <Col span={12}>
  551. <Form.Item name="enDesc" label="英文名称">
  552. <Input placeholder="请输入英文名称" maxLength={50} />
  553. </Form.Item>
  554. </Col>
  555. <Col span={12}>
  556. <Form.Item
  557. name="safeClassificat"
  558. label="安全级别"
  559. rules={[{ required: true, message: '请选择安全级别' }]}
  560. >
  561. <Radio.Group>
  562. {safeClassOptions.map(opt => (
  563. <Radio key={opt.value} value={opt.value}>{opt.label}</Radio>
  564. ))}
  565. </Radio.Group>
  566. </Form.Item>
  567. </Col>
  568. </Row>
  569. <Row gutter={16}>
  570. <Col span={12}>
  571. <Form.Item
  572. name="columnEdit"
  573. label="允许列编辑"
  574. rules={[{ required: true, message: '请选择是否允许列编辑' }]}
  575. >
  576. <Radio.Group>
  577. <Radio value="Y">是</Radio>
  578. <Radio value="N">否</Radio>
  579. </Radio.Group>
  580. </Form.Item>
  581. </Col>
  582. <Col span={12}>
  583. <Form.Item
  584. name="layoutEdit"
  585. label="允许布局编辑"
  586. rules={[{ required: true, message: '请选择是否允许布局编辑' }]}
  587. >
  588. <Radio.Group>
  589. <Radio value="Y">是</Radio>
  590. <Radio value="N">否</Radio>
  591. </Radio.Group>
  592. </Form.Item>
  593. </Col>
  594. </Row>
  595. <Row gutter={16}>
  596. <Col span={12}>
  597. <Form.Item
  598. name="operCodeTable"
  599. label="操作码表"
  600. rules={[{ required: true, message: '请选择' }]}
  601. >
  602. <Radio.Group>
  603. <Radio value="Y">是</Radio>
  604. <Radio value="N">否</Radio>
  605. </Radio.Group>
  606. </Form.Item>
  607. </Col>
  608. <Col span={12}>
  609. <Form.Item
  610. name="sendMsgToAllUser"
  611. label="发送消息给所有用户"
  612. rules={[{ required: true, message: '请选择' }]}
  613. >
  614. <Radio.Group>
  615. <Radio value="Y">是</Radio>
  616. <Radio value="N">否</Radio>
  617. </Radio.Group>
  618. </Form.Item>
  619. </Col>
  620. </Row>
  621. <Row gutter={16}>
  622. <Col span={12}>
  623. <Form.Item name="defaultMenuType" label="默认菜单类型">
  624. <Radio.Group>
  625. {menuTypeOptions.map(opt => (
  626. <Radio key={opt.value} value={opt.value}>{opt.label}</Radio>
  627. ))}
  628. </Radio.Group>
  629. </Form.Item>
  630. </Col>
  631. <Col span={12}>
  632. <Form.Item name="mainInterface" label="主界面">
  633. <Input placeholder="请输入主界面" maxLength={50} />
  634. </Form.Item>
  635. </Col>
  636. </Row>
  637. <Row gutter={16}>
  638. <Col span={12}>
  639. <Form.Item
  640. name="startDate"
  641. label="生效日期"
  642. rules={[{ required: true, message: '请选择生效日期' }]}
  643. >
  644. <Input type="date" placeholder="请选择生效日期" />
  645. </Form.Item>
  646. </Col>
  647. <Col span={12}>
  648. <Form.Item name="endDate" label="失效日期">
  649. <Input type="date" placeholder="请选择失效日期(可选)" />
  650. </Form.Item>
  651. </Col>
  652. </Row>
  653. </Form>
  654. </Modal>
  655. {/* 权限配置弹窗 */}
  656. <Modal
  657. title={`配置权限 - ${authRecord?.descripts || ''}`}
  658. open={authVisible}
  659. onOk={handleSaveAuth}
  660. onCancel={() => setAuthVisible(false)}
  661. okText="保存"
  662. cancelText="取消"
  663. width={600}
  664. >
  665. <Tabs activeKey={activeTab} onChange={setActiveTab}>
  666. <TabPane tab="菜单权限" key="menu">
  667. <div style={{ maxHeight: 400, overflow: 'auto', minHeight: 200 }}>
  668. {/* 调试信息 */}
  669. <div style={{ fontSize: 12, color: '#999', padding: '5px 0' }}>
  670. 调试: loading={authLoading.toString()}, menuTreeData.length={menuTreeData.length}
  671. </div>
  672. {authLoading ? (
  673. <div style={{ textAlign: 'center', padding: '40px 0' }}>
  674. <span>加载中...</span>
  675. </div>
  676. ) : menuTreeData.length === 0 ? (
  677. <div style={{ textAlign: 'center', padding: '40px 0', color: '#999' }}>
  678. <p>暂无菜单数据</p>
  679. <p style={{ fontSize: 12 }}>请确保后端 IRIS 服务正常运行,并已初始化菜单数据</p>
  680. <Button type="primary" size="small" onClick={() => handleAuth(authRecord!)} style={{ marginTop: 10 }}>
  681. 重新加载
  682. </Button>
  683. </div>
  684. ) : (
  685. <Tree
  686. checkable
  687. treeData={menuTreeData}
  688. checkedKeys={selectedMenuIds}
  689. onCheck={handleTreeCheck}
  690. selectable={false}
  691. />
  692. )}
  693. </div>
  694. </TabPane>
  695. <TabPane tab="数据权限" key="data">
  696. <div style={{ padding: '20px 0', textAlign: 'center', color: '#999' }}>
  697. <p>数据权限配置功能开发中...</p>
  698. </div>
  699. </TabPane>
  700. <TabPane tab="操作权限" key="operation">
  701. <div style={{ padding: '20px 0', textAlign: 'center', color: '#999' }}>
  702. <p>操作权限配置功能开发中...</p>
  703. </div>
  704. </TabPane>
  705. </Tabs>
  706. </Modal>
  707. </div>
  708. );
  709. };
  710. export default Roles;