RoleManage.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. <script setup lang="ts">
  2. import { nextTick, onMounted, ref, useTemplateRef, watch } from 'vue';
  3. import { message } from 'ant-design-vue';
  4. import ConfirmModal from '@/components/ConfirmModal.vue';
  5. import OrganizationalStructure from '@/components/OrganizationalStructure.vue';
  6. import SvgIcon from '@/components/SvgIcon.vue';
  7. import { useRequest } from '@/hooks/request';
  8. import { useUserInfoStore } from '@/stores/user-info';
  9. import { t } from '@/i18n';
  10. import {
  11. addCharacter,
  12. addDevicePermissions,
  13. addGrantRolePermissions,
  14. deleteCharacter,
  15. getAllGroupList,
  16. getFindRolesByOrgIds,
  17. getPermissionCheckTree,
  18. getSubOrgsByToken,
  19. updateCharacter,
  20. } from '@/api';
  21. import { OperatePermission } from '@/utils/permission-type';
  22. import type { DataNode, TreeProps } from 'ant-design-vue/es/tree';
  23. import type { Key } from 'ant-design-vue/es/vc-tree/interface';
  24. import type { CheckInfo } from 'ant-design-vue/es/vc-tree/props';
  25. import type { OperationPermissions, TreeStructure } from '@/types';
  26. interface HaracterItem {
  27. name: string;
  28. id?: number;
  29. show: boolean;
  30. }
  31. const { booleanPermission } = useUserInfoStore();
  32. const characterList = ref<HaracterItem[]>([]);
  33. const { handleRequest } = useRequest();
  34. const characterListId = ref();
  35. const inputRef = ref<HTMLInputElement[]>([]); // 明确的类型声明
  36. const permissions = ref<string>('dataPermissions');
  37. const equipmentChecked = ref<boolean>(false);
  38. const editorChecked = ref<boolean>(true);
  39. // const valueTime = ref<string>('1');
  40. const operationpermissions = ref<OperationPermissions[]>([]);
  41. const devicePermissionsExpandedKeys = ref<number[]>([]);
  42. const checkedFatherKeys = ref<number[]>([]);
  43. const devicePermissionsCheckedKeys = ref({
  44. checked: [] as number[], // 完全选中的节点
  45. halfChecked: [] as number[], // 半选中的节点(在严格模式下通常为空)
  46. });
  47. const allKeys = ref<number[]>([]);
  48. const indeterminate = ref<boolean>(false);
  49. const orgId = ref<number>();
  50. const characterId = ref<number>();
  51. const enterShow = ref<boolean>(false);
  52. const modalComponentRef = useTemplateRef('modalComponent');
  53. const expandedKeys = ref<number[]>([]);
  54. const checkedKeys = ref<number[]>([]);
  55. const treeStructure = ref<DataNode[]>([]);
  56. const fieldNamesCheck: TreeProps['fieldNames'] = {
  57. children: 'subPermissions',
  58. title: 'menuName',
  59. key: 'id',
  60. };
  61. const pagePermissionsTree = ref<DataNode[]>([]);
  62. const fieldNames: TreeProps['fieldNames'] = {
  63. children: 'deviceGroupChilds',
  64. title: 'groupName',
  65. key: 'id',
  66. };
  67. const addCharacterName = async () => {
  68. if (characterList.value.some((item) => !isValidString(item.name))) {
  69. return;
  70. }
  71. characterList.value.push({
  72. name: '',
  73. id: undefined,
  74. show: true,
  75. });
  76. await nextTick();
  77. inputRef.value[0].focus();
  78. };
  79. const clickCharacter = (id: number) => {
  80. if (characterList.value.some((item) => !isValidString(item.name))) {
  81. return;
  82. }
  83. characterListId.value = id;
  84. getDeviceGroupList();
  85. getFunctionPermList();
  86. };
  87. const addEditor = async (index: number) => {
  88. if (characterList.value.some((item) => !isValidString(item.name))) {
  89. return;
  90. }
  91. characterList.value[index].show = true;
  92. await nextTick();
  93. inputRef.value[0].focus();
  94. };
  95. const addDelete = (id: number | undefined) => {
  96. if (!id) {
  97. return message.warning(t('deviceList.pleaseSelectItemDelete'));
  98. }
  99. if (characterList.value.some((item) => !isValidString(item.name))) {
  100. return;
  101. }
  102. characterId.value = id;
  103. modalComponentRef.value?.showView();
  104. };
  105. const confirm = () => {
  106. handleRequest(async () => {
  107. if (characterId.value) {
  108. await deleteCharacter(characterId.value);
  109. message.success(t('common.deleteSuccess'));
  110. if (orgId.value) {
  111. getFindRolesByOrg(orgId.value);
  112. }
  113. }
  114. modalComponentRef.value?.hideView();
  115. });
  116. };
  117. const editorCharacterBlur = (index: number, name: string) => {
  118. if (!enterShow.value) {
  119. editorCharacter(index, name);
  120. }
  121. };
  122. /**
  123. * 校验字符串是否非空且不包含任何空白字符
  124. * @param str 待检测的字符串
  125. * @returns 是否有效(非空且无空白)
  126. */
  127. const isValidString = (str: string): boolean => {
  128. return /^[\S]+$/.test(str);
  129. // ^ 开头 | \S 非空白字符 | + 至少一个 | $ 结尾
  130. };
  131. const editorCharacter = (index: number, name: string) => {
  132. if (name) {
  133. if (isValidString(name)) {
  134. enterShow.value = true;
  135. characterList.value[index].show = false;
  136. const id = characterList.value[index].id;
  137. handleRequest(async () => {
  138. if (id) {
  139. await updateCharacter({ roleName: name, orgId: orgId.value, id });
  140. message.success(t('common.editSuccess'));
  141. } else {
  142. await addCharacter({ roleName: name, orgId: orgId.value });
  143. message.success(t('common.addSuccess'));
  144. }
  145. enterShow.value = false;
  146. if (orgId.value) {
  147. getFindRolesByOrg(orgId.value);
  148. }
  149. });
  150. } else {
  151. return message.warning(t('roleManage.nameCannotContainSpaces'));
  152. }
  153. } else {
  154. return message.warning(t('roleManage.nameCannotBeEmpty'));
  155. }
  156. };
  157. const editorPermission = () => {
  158. editorChecked.value = false;
  159. };
  160. const cancelPermission = () => {
  161. if (permissions.value === 'dataPermissions') {
  162. getDeviceGroupList();
  163. } else {
  164. getFunctionPermList();
  165. }
  166. editorChecked.value = true;
  167. };
  168. const addTree = (checked: Key[] | { checked: Key[]; halfChecked: Key[] }, info: CheckInfo) => {
  169. if (info.node.parent) {
  170. checkedFatherKeys.value.push(info.node.parent.node.id);
  171. }
  172. };
  173. const savePermission = () => {
  174. if (permissions.value === 'dataPermissions') {
  175. handleRequest(async () => {
  176. await addDevicePermissions({
  177. roleId: characterListId.value,
  178. groupIds: devicePermissionsCheckedKeys.value.checked,
  179. });
  180. getDeviceGroupList();
  181. });
  182. } else {
  183. let data: number[] = [];
  184. operationpermissions.value.forEach((item) => {
  185. if (item.list.length) {
  186. data = [...new Set([...data, ...item.list])];
  187. }
  188. });
  189. handleRequest(async () => {
  190. if (!checkedKeys.value.length) {
  191. checkedFatherKeys.value = [];
  192. }
  193. await addGrantRolePermissions({
  194. roleId: characterListId.value,
  195. permissionIds: [...new Set([...checkedKeys.value, ...checkedFatherKeys.value, ...data])],
  196. });
  197. getFunctionPermList();
  198. });
  199. }
  200. editorChecked.value = true;
  201. };
  202. const clickOrganizationChange = (id: number) => {
  203. orgId.value = id;
  204. getFindRolesByOrg(id);
  205. };
  206. const getFindRolesByOrg = (id: number) => {
  207. handleRequest(async () => {
  208. const data = await getFindRolesByOrgIds([id]);
  209. characterList.value = [];
  210. if (data.length) {
  211. data.forEach((item) => {
  212. const { roleName, id } = item;
  213. characterList.value.push({
  214. name: roleName,
  215. id,
  216. show: false,
  217. });
  218. });
  219. if (!characterListId.value) {
  220. characterListId.value = data[0].id;
  221. getDeviceGroupList();
  222. getFunctionPermList();
  223. } else {
  224. getDeviceGroupList();
  225. getFunctionPermList();
  226. }
  227. } else {
  228. pagePermissionsTree.value = [];
  229. treeStructure.value = [];
  230. operationpermissions.value = [];
  231. }
  232. });
  233. };
  234. const getDeviceGroupList = () => {
  235. handleRequest(async () => {
  236. const list = await getAllGroupList({
  237. roleId: undefined,
  238. });
  239. const data = await getAllGroupList({
  240. roleId: characterListId.value,
  241. });
  242. devicePermissionsExpandedKeys.value = [];
  243. devicePermissionsCheckedKeys.value.checked = [];
  244. allKeys.value = [];
  245. list.forEach((item) => {
  246. item.key = item.id;
  247. allKeys.value.push(item.id);
  248. devicePermissionsExpandedKeys.value.push(item.id);
  249. if (item.deviceGroupChilds.length) {
  250. item.deviceGroupChilds.forEach((i) => {
  251. i.key = i.id;
  252. allKeys.value.push(i.id);
  253. });
  254. }
  255. });
  256. data.forEach((item) => {
  257. devicePermissionsCheckedKeys.value.checked.push(item.id);
  258. item.deviceGroupChilds.forEach((i) => {
  259. devicePermissionsCheckedKeys.value.checked.push(i.id);
  260. });
  261. });
  262. pagePermissionsTree.value = list as DataNode[];
  263. });
  264. };
  265. const transformTreeData = (data: TreeStructure[]): DataNode[] => {
  266. return data.map((item) => ({
  267. ...item,
  268. key: item.id, // 关键:将 id 映射到 key
  269. title: item.menuName,
  270. children: item.subPermissions ? transformTreeData(item.subPermissions) : undefined,
  271. }));
  272. };
  273. const transformData = (data: TreeStructure[]): OperationPermissions[] => {
  274. return data.map((item) => {
  275. // 转换当前层级的属性
  276. const transformed = {
  277. value: item.id,
  278. label: item.menuName,
  279. list: item.subPermissions?.filter((sub) => sub.checked === 1).map((sub) => sub.id) || [],
  280. ...item,
  281. // 保留其他属性(如果需要)
  282. ...(item.subPermissions && {
  283. subPermissions: transformData(item.subPermissions),
  284. }),
  285. };
  286. return transformed;
  287. });
  288. };
  289. const getFunctionPermList = () => {
  290. handleRequest(async () => {
  291. const dataCheck = await getPermissionCheckTree(characterListId.value);
  292. checkedKeys.value = [];
  293. expandedKeys.value = [];
  294. if (dataCheck.length) {
  295. treeStructure.value = transformTreeData(dataCheck[0].subPermissions[0].subPermissions);
  296. operationpermissions.value = transformData(dataCheck[0].subPermissions[1].subPermissions);
  297. dataCheck[0].subPermissions[0].subPermissions.forEach((item) => {
  298. if (item.subPermissions) {
  299. item.subPermissions.forEach((i) => {
  300. if (i.checked === 1) {
  301. checkedKeys.value.push(i.id);
  302. }
  303. });
  304. expandedKeys.value.push(item.id);
  305. } else {
  306. if (item.checked === 1) {
  307. checkedKeys.value.push(item.id);
  308. }
  309. }
  310. });
  311. }
  312. });
  313. };
  314. const selectAll = () => {
  315. indeterminate.value = false;
  316. if (equipmentChecked.value) {
  317. devicePermissionsCheckedKeys.value.checked = allKeys.value;
  318. } else {
  319. devicePermissionsCheckedKeys.value.checked = [];
  320. }
  321. };
  322. const addRadioGroup = () => {
  323. if (permissions.value === 'functionPermissions') {
  324. getDeviceGroupList();
  325. } else {
  326. getFunctionPermList();
  327. }
  328. editorChecked.value = true;
  329. };
  330. watch(
  331. () => devicePermissionsCheckedKeys.value.checked,
  332. (count) => {
  333. if (count) {
  334. indeterminate.value = !!count.length && count.length < allKeys.value.length;
  335. equipmentChecked.value = count.length === allKeys.value.length;
  336. }
  337. },
  338. );
  339. onMounted(() => {
  340. handleRequest(async () => {
  341. await getSubOrgsByToken();
  342. getFunctionPermList();
  343. });
  344. });
  345. </script>
  346. <template>
  347. <div>
  348. <div class="text-top">{{ t('navigation.roleManage') }}</div>
  349. <AFlex>
  350. <OrganizationalStructure @change="clickOrganizationChange" />
  351. <div class="content">
  352. <AFlex justify="space-between" align="center" class="content-top">
  353. <div class="content-text">{{ t('userManage.role') }}</div>
  354. <div
  355. class="icon-style pointer"
  356. @click="addCharacterName"
  357. v-if="booleanPermission(OperatePermission.新增角色)"
  358. >
  359. <AFlex align="center"
  360. ><SvgIcon name="plus" />
  361. <div class="text-left">{{ t('common.add') }}</div>
  362. </AFlex>
  363. </div>
  364. </AFlex>
  365. <div>
  366. <div v-for="(item, index) in characterList" :key="index">
  367. <div class="character-input" v-if="item.show">
  368. <AInput
  369. class="input-heught"
  370. v-model:value="item.name"
  371. :bordered="false"
  372. ref="inputRef"
  373. @pressEnter="editorCharacter(index, item.name)"
  374. @blur="editorCharacterBlur(index, item.name)"
  375. />
  376. </div>
  377. <AFlex
  378. v-else
  379. justify="space-between"
  380. align="center"
  381. :class="item.id === characterListId ? 'character-list character-list-color' : 'character-list'"
  382. >
  383. <div class="pointer text-height" @click="clickCharacter(item.id!)">{{ item.name }}</div>
  384. <div v-if="item.id === characterListId">
  385. <SvgIcon
  386. v-if="booleanPermission(OperatePermission.编辑角色)"
  387. class="pointer"
  388. name="edit-o"
  389. @click="addEditor(index)"
  390. />
  391. <SvgIcon
  392. v-if="booleanPermission(OperatePermission.删除角色)"
  393. class="pointer icon-left"
  394. @click="addDelete(item.id)"
  395. name="delete"
  396. />
  397. </div>
  398. </AFlex>
  399. </div>
  400. </div>
  401. </div>
  402. <div class="permission-management">
  403. <AFlex justify="space-between" align="center" class="content-top">
  404. <div class="content-text">{{ t('roleManage.permission') }}</div>
  405. <div
  406. class="pointer"
  407. @click="editorPermission"
  408. v-if="editorChecked && booleanPermission(OperatePermission.编辑角色)"
  409. >
  410. <AFlex align="center"
  411. ><SvgIcon name="edit-o" />
  412. <div class="text-left">{{ t('common.editor') }}</div>
  413. </AFlex>
  414. </div>
  415. <AFlex v-if="!editorChecked">
  416. <div class="pointer" @click="cancelPermission">
  417. <AFlex align="center"
  418. ><SvgIcon name="close" />
  419. <div class="text-left">{{ t('common.cancel') }}</div>
  420. </AFlex>
  421. </div>
  422. <div class="pointer pointer-left" @click="savePermission">
  423. <AFlex align="center"
  424. ><SvgIcon name="save" />
  425. <div class="text-left">{{ t('common.save') }}</div>
  426. </AFlex>
  427. </div>
  428. </AFlex>
  429. </AFlex>
  430. <ARadioGroup v-model:value="permissions" button-style="solid" size="large" @change="addRadioGroup">
  431. <ARadioButton value="dataPermissions">{{ t('roleManage.dataPermission') }}</ARadioButton>
  432. <ARadioButton value="functionPermissions">{{ t('roleManage.functionalPermission') }}</ARadioButton>
  433. </ARadioGroup>
  434. <div v-if="permissions === 'dataPermissions'">
  435. <AFlex align="center" class="device-permissions">
  436. <ACheckbox
  437. class="select-all"
  438. v-model:checked="equipmentChecked"
  439. :indeterminate="indeterminate"
  440. :disabled="editorChecked"
  441. @change="selectAll"
  442. >{{ t('roleManage.deviceGroupPermission') }}</ACheckbox
  443. >
  444. </AFlex>
  445. <div class="permission-div">
  446. <ATree
  447. v-model:expanded-keys="devicePermissionsExpandedKeys"
  448. v-model:checked-keys="devicePermissionsCheckedKeys"
  449. :tree-data="pagePermissionsTree"
  450. checkable
  451. default-expand-all
  452. :field-names="fieldNames"
  453. :disabled="editorChecked"
  454. :check-strictly="true"
  455. />
  456. </div>
  457. <!-- <AFlex align="center" class="device-permissions div-top">
  458. <ACheckbox class="select-all" :disabled="editorChecked" v-model:checked="equipmentChecked"
  459. >启用时间查询颗粒度设置</ACheckbox
  460. >
  461. </AFlex>
  462. <ARadioGroup v-model:value="valueTime" name="radioGroup" class="radio-group" :disabled="editorChecked">
  463. <ARadio value="1">分钟</ARadio>
  464. <ARadio value="2">小时</ARadio>
  465. <ARadio value="3">天</ARadio>
  466. <ARadio value="4">月</ARadio>
  467. </ARadioGroup> -->
  468. </div>
  469. <div v-if="permissions === 'functionPermissions'">
  470. <AFlex align="center" class="device-permissions">
  471. <div>{{ t('roleManage.viewPermission') }}</div>
  472. </AFlex>
  473. <div class="check-div">
  474. <ATree
  475. v-model:expanded-keys="expandedKeys"
  476. v-model:checked-keys="checkedKeys"
  477. :tree-data="treeStructure"
  478. checkable
  479. :field-names="fieldNamesCheck"
  480. :disabled="editorChecked"
  481. class="tree-permissions"
  482. default-expand-all
  483. @check="addTree"
  484. />
  485. </div>
  486. <AFlex align="center" class="device-permissions div-top">
  487. <div>{{ $t('roleManage.operationPermission') }}</div>
  488. </AFlex>
  489. <div class="operation">
  490. <AFlex align="center" v-for="(item, index) in operationpermissions" :key="index" class="operation-div">
  491. <ACheckboxGroup
  492. v-if="item.subPermissions"
  493. v-model:value="item.list"
  494. :options="item.subPermissions"
  495. :disabled="editorChecked"
  496. />
  497. </AFlex>
  498. </div>
  499. </div>
  500. </div>
  501. </AFlex>
  502. <ConfirmModal
  503. ref="modalComponent"
  504. :title="t('common.deleteConfirmation')"
  505. :description-text="t('common.confirmDeletion')"
  506. :icon="{ name: 'delete' }"
  507. :icon-bg-color="'#F56C6C'"
  508. @confirm="confirm"
  509. />
  510. </div>
  511. </template>
  512. <style lang="scss" scoped>
  513. .operation {
  514. height: 200px;
  515. overflow: auto;
  516. }
  517. .operation-div {
  518. width: 100%;
  519. height: 48px;
  520. padding-left: 52px;
  521. border-bottom: 1px solid #e4e7ed;
  522. }
  523. .check-div {
  524. height: calc(100vh - 494px);
  525. overflow: auto;
  526. }
  527. .permission-div {
  528. height: calc(100vh - 244px);
  529. overflow: auto;
  530. }
  531. .pointer-left {
  532. margin-left: 24px;
  533. }
  534. :deep(.permission-management) {
  535. .ant-tree-list-holder-inner > div {
  536. display: flex;
  537. align-items: center;
  538. width: 100%;
  539. height: 48px;
  540. padding-left: 26px;
  541. }
  542. .tree-permissions .ant-tree-list-holder-inner > div {
  543. border-bottom: 1px solid #e4e7ed;
  544. }
  545. .ant-tree-list-holder-inner > div > .ant-tree-checkbox {
  546. margin-block-start: 0;
  547. }
  548. .ant-tree-list-holder-inner > div > .ant-tree-switcher > span {
  549. margin-top: 16px;
  550. }
  551. }
  552. .radio-group {
  553. margin-top: 13px;
  554. margin-left: 48px;
  555. }
  556. :deep(.radio-group) {
  557. .ant-radio-wrapper {
  558. margin-inline-end: 20px;
  559. }
  560. }
  561. .device-permissions {
  562. width: 100%;
  563. height: 48px;
  564. padding-left: 24px;
  565. margin-top: 16px;
  566. background: #f5f7fa;
  567. border-radius: 4px;
  568. }
  569. .permission-management .div-top {
  570. margin-top: 0;
  571. }
  572. .permission-management {
  573. width: 100%;
  574. height: calc(100vh - 80px);
  575. padding: 16px;
  576. background: #fff;
  577. border-radius: 16px;
  578. }
  579. .input-heught {
  580. height: 38px;
  581. }
  582. .character-input {
  583. width: 214px;
  584. height: 40px;
  585. background: rgb(255 255 255 / 15%);
  586. border: 1px solid var(--antd-color-primary);
  587. border-radius: 4px;
  588. }
  589. .text-left {
  590. margin-left: 10px;
  591. }
  592. .icon-style {
  593. color: var(--antd-color-primary);
  594. }
  595. .text-height {
  596. height: 40px;
  597. line-height: 40px;
  598. }
  599. .icon-left {
  600. margin-left: 13px;
  601. }
  602. .pointer {
  603. cursor: pointer;
  604. }
  605. .character-list {
  606. width: 214px;
  607. height: 40px;
  608. padding: 0 12px;
  609. font-size: 14px;
  610. font-style: normal;
  611. font-weight: 400;
  612. line-height: 22px;
  613. color: #000;
  614. text-align: left;
  615. border-radius: 4px;
  616. }
  617. .character-list-color {
  618. color: var(--antd-color-primary);
  619. background: var(--antd-color-primary-opacity-15);
  620. }
  621. .content-top {
  622. margin-bottom: 16px;
  623. }
  624. .button-style {
  625. color: var(--antd-color-primary);
  626. }
  627. .content-text {
  628. font-size: 16px;
  629. font-style: normal;
  630. font-weight: 600;
  631. line-height: 24px;
  632. color: #333;
  633. text-align: left;
  634. }
  635. .content {
  636. width: 246px;
  637. height: calc(100vh - 80px);
  638. padding: 16px;
  639. margin-right: 16px;
  640. background: #fff;
  641. border-radius: 16px;
  642. }
  643. .text-top {
  644. margin-bottom: 16px;
  645. font-size: 20px;
  646. font-style: normal;
  647. font-weight: 500;
  648. line-height: 32px;
  649. color: rgb(0 0 0 / 85%);
  650. text-align: left;
  651. }
  652. </style>