sa-access.vue 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115
  1. <template>
  2. <el-container class="sa-access panel-block">
  3. <el-main id="scrollWrap" class="sa-p-0" v-loading="state.loading">
  4. <template v-for="(val, level) in state.show" :key="level">
  5. <el-scrollbar height="100%" v-loading="val.loading">
  6. <div class="title sa-flex sa-row-between">
  7. <span>{{ val.pdata?.title }}</span>
  8. <slot name="add" :pdata="val.pdata">
  9. <template v-if="val.pdata">
  10. <el-checkbox
  11. v-if="multiple"
  12. class="sa-m-r-8"
  13. v-model="val.pdata.checked"
  14. :indeterminate="val.pdata.indeterminate"
  15. label="全选"
  16. @update:model-value="handleSelect($event, val.pdata)"
  17. />
  18. </template>
  19. <template v-if="type == 'list'">
  20. <el-button class="is-link" type="primary" @click="onAdd(val.pdata.id, level)"
  21. >+添加</el-button
  22. >
  23. <!-- <el-button
  24. class="is-link"
  25. type="success"
  26. @click="initMenuPermissions"
  27. :loading="initLoading"
  28. >
  29. 初始化菜单
  30. </el-button> -->
  31. </template>
  32. </slot>
  33. </div>
  34. <template v-if="val.data?.length > 0">
  35. <sa-draggable
  36. v-model="val.data"
  37. :animation="300"
  38. handle=".sortable-drag"
  39. item-key="element"
  40. @end="onEnd($event, level, val.pdata)"
  41. >
  42. <template #item="{ element, index }">
  43. <div
  44. :class="['node', 'sa-flex sa-row-between', val.index == index ? 'is-active' : '']"
  45. @click="onClick(element, index, level)"
  46. >
  47. <!-- multiple -->
  48. <el-checkbox
  49. v-if="multiple"
  50. class="sa-m-r-8"
  51. v-model="element.checked"
  52. :indeterminate="element.indeterminate"
  53. @click.stop
  54. @change="handleSelect($event, element)"
  55. />
  56. <slot class="label" :data="element" :level="level">
  57. <div class="item sa-flex sa-row-between">
  58. <div class="sa-flex">
  59. <sa-svg
  60. v-if="type == 'list'"
  61. class="sortable-drag sa-m-r-8"
  62. name="sa-round"
  63. ></sa-svg>
  64. <sa-icon class="icon sa-m-r-4" :icon="element.icon" size="16" />
  65. <div>{{ element.title }}</div>
  66. </div>
  67. <div v-if="type == 'list'" class="sa-flex">
  68. <el-icon
  69. class="edit sa-m-r-8"
  70. @click.stop="onEdit(element.id, index, level)"
  71. >
  72. <Edit />
  73. </el-icon>
  74. <el-popconfirm
  75. width="fit-content"
  76. confirm-button-text="确认"
  77. cancel-button-text="取消"
  78. title="确认删除这条记录?"
  79. @confirm="onDelete(element.id, index, level)"
  80. >
  81. <template #reference>
  82. <el-icon class="delete sa-m-r-8" @click.stop>
  83. <Delete />
  84. </el-icon>
  85. </template>
  86. </el-popconfirm>
  87. </div>
  88. </div>
  89. </slot>
  90. <div class="arrow-right">
  91. <el-icon v-if="!isEmpty(element.children)">
  92. <ArrowRight />
  93. </el-icon>
  94. </div>
  95. </div>
  96. </template>
  97. </sa-draggable>
  98. </template>
  99. <template v-if="!val.loading && val.data.length == 0">
  100. <div class="empty">暂无数据</div>
  101. </template>
  102. </el-scrollbar>
  103. </template>
  104. </el-main>
  105. </el-container>
  106. </template>
  107. <script setup>
  108. import { nextTick, onMounted, reactive, watch, ref } from 'vue';
  109. import admin from '@/app/admin/api';
  110. import SaDraggable from 'vuedraggable';
  111. import { useModal } from '@/sheep/hooks';
  112. import { ElMessage, ElMessageBox } from 'element-plus';
  113. import { menuRulesData } from '@/sheep/local-data/admin';
  114. import { isEmpty } from 'lodash';
  115. import AccessEdit from '../edit.vue';
  116. const emit = defineEmits(['update:modelValue']);
  117. const props = defineProps({
  118. type: String,
  119. isChangeParentId: Boolean,
  120. role_id: {
  121. type: [String, Number],
  122. default: 0,
  123. },
  124. modelValue: {
  125. type: Array,
  126. default: [],
  127. },
  128. multiple: {
  129. type: Boolean,
  130. default: false,
  131. },
  132. });
  133. let manualChecked = false;
  134. let isSelectingPermission = false; // 新增标志,表示正在进行权限选择操作
  135. const state = reactive({
  136. loading: false,
  137. app: [],
  138. checkedIds: props.modelValue,
  139. newIds: [],
  140. show: {},
  141. });
  142. // 初始化loading状态
  143. const initLoading = ref(false);
  144. watch(
  145. () => props.modelValue,
  146. (newValue) => {
  147. // 如果正在进行权限选择操作,只更新checkedIds,不触发其他计算
  148. if (isSelectingPermission) {
  149. state.checkedIds = newValue || [];
  150. return;
  151. }
  152. state.checkedIds = newValue || [];
  153. // 当modelValue变化时,重新计算权限状态(无论数据是否为空都要计算)
  154. if (state.app.length > 0) {
  155. let appItem = initAppItem();
  156. initCalculate(state.app, appItem);
  157. calculateShow(appItem);
  158. }
  159. },
  160. { immediate: true },
  161. );
  162. // 监听app数据变化,确保新增时也能显示
  163. watch(
  164. () => state.app,
  165. (newApp) => {
  166. // 如果正在进行权限选择操作,跳过所有处理
  167. if (isSelectingPermission) {
  168. return;
  169. }
  170. // 无论数据是否为空都要初始化显示状态
  171. let appItem = initAppItem();
  172. if (newApp && newApp.length > 0) {
  173. initCalculate(newApp, appItem);
  174. }
  175. calculateShow(appItem);
  176. },
  177. );
  178. // 数据转换函数:将新接口数据转换为组件需要的格式
  179. function transformPermissionData(data) {
  180. if (!Array.isArray(data)) {
  181. console.warn('transformPermissionData: data is not an array', data);
  182. return [];
  183. }
  184. const result = data.map((item) => {
  185. const transformed = {
  186. id: item.id,
  187. title: item.name || item.label, // 优先使用name,备用label
  188. icon: item.logo || 'menu', // 使用logo作为icon,默认为menu
  189. children:
  190. item.children && Array.isArray(item.children)
  191. ? transformPermissionData(item.children)
  192. : [],
  193. // 根据hasRelevance字段设置选中状态
  194. checked: item.hasRelevance === '1',
  195. indeterminate: false,
  196. // 保留原始数据以备后用
  197. _original: item,
  198. };
  199. console.log('🔄 transformPermissionData - 转换结果:', transformed);
  200. return transformed;
  201. });
  202. console.log('🔄 transformPermissionData - 最终结果:', result);
  203. return result;
  204. }
  205. async function getData() {
  206. state.loading = true;
  207. try {
  208. let permissionData = [];
  209. // 根据type类型获取不同的数据
  210. if (props.type === 'select' && props.role_id) {
  211. // 权限选择模式且有角色ID,获取角色详情中的权限数据
  212. console.log('🔍 获取角色权限数据, role_id:', props.role_id);
  213. const roleResponse = await admin.auth.role.detail(props.role_id);
  214. if (roleResponse.code == 200 && roleResponse.data && roleResponse.data.permissions) {
  215. console.log('🔍 getData - 角色权限数据:', roleResponse.data.permissions);
  216. permissionData = roleResponse.data.permissions;
  217. }
  218. } else {
  219. // 其他模式,获取完整权限树
  220. const response = await admin.auth.access.getTree();
  221. if (response.success && response.data !== null && response.data !== undefined) {
  222. console.log('🔍 getData - 权限树数据:', response.data);
  223. permissionData = response.data;
  224. }
  225. }
  226. // 转换数据格式(即使是空数组也要处理)
  227. const transformedData = Array.isArray(permissionData)
  228. ? transformPermissionData(permissionData)
  229. : [];
  230. console.log('🔍 getData - 转换后数据:', transformedData);
  231. state.app = transformedData;
  232. // 初始化显示状态(无论数据是否为空都要初始化)
  233. let appItem = initAppItem();
  234. if (transformedData.length > 0) {
  235. initCalculate(transformedData, appItem);
  236. }
  237. // 如果不是权限选择操作导致的变化,才重置展开状态
  238. if (!isSelectingPermission) {
  239. calculateShow(appItem);
  240. }
  241. } catch (error) {
  242. console.error('获取权限数据异常:', error);
  243. state.app = [];
  244. // 异常情况下也要初始化显示状态
  245. let appItem = initAppItem();
  246. if (!isSelectingPermission) {
  247. calculateShow(appItem);
  248. }
  249. }
  250. state.loading = false;
  251. }
  252. // 🎯 新方案:保持选中状态的数据刷新函数
  253. async function refreshDataKeepSelected() {
  254. // 1. 保存当前所有层级的选中状态
  255. const selectedStates = [];
  256. for (let level = 0; level < Object.keys(state.show).length; level++) {
  257. const showData = state.show[level];
  258. if (showData && showData.index !== null && showData.index !== undefined && showData.data) {
  259. const selectedItem = showData.data[showData.index];
  260. if (selectedItem && selectedItem.id) {
  261. selectedStates.push({
  262. level: level,
  263. itemId: selectedItem.id,
  264. itemTitle: selectedItem.title,
  265. selectedIndex: showData.index,
  266. });
  267. console.log(
  268. `💾 保存第${level}层选中状态: ${selectedItem.title} (ID: ${selectedItem.id}, 索引: ${showData.index})`,
  269. );
  270. }
  271. }
  272. }
  273. console.log('💾 保存的选中状态:', selectedStates);
  274. // 2. 重新获取数据(这会重置state.show)
  275. await getData();
  276. // 3. 恢复选中状态
  277. if (selectedStates.length > 0) {
  278. console.log('🔄 开始恢复选中状态...');
  279. // 逐层恢复选中状态
  280. for (const selectedState of selectedStates) {
  281. const { level, itemId, itemTitle } = selectedState;
  282. console.log(`🔄 恢复第${level}层选中状态: ${itemTitle} (ID: ${itemId})`);
  283. // 在当前层级查找目标项目
  284. const currentLevelData = state.show[level] ? state.show[level].data : [];
  285. const foundIndex = currentLevelData.findIndex((item) => item.id === itemId);
  286. if (foundIndex !== -1) {
  287. const foundItem = currentLevelData[foundIndex];
  288. console.log(`✅ 找到目标项目: ${foundItem.title} (索引: ${foundIndex})`);
  289. // 使用calculateShow来正确展开并选中这一层
  290. await calculateShow(foundItem, foundIndex, level);
  291. console.log(`✅ 第${level}层选中状态恢复完成`);
  292. } else {
  293. console.log(`⚠️ 第${level}层找不到目标项目 ID: ${itemId}`);
  294. break; // 如果某一层找不到,停止恢复
  295. }
  296. }
  297. }
  298. console.log('✅ 选中状态恢复完成');
  299. }
  300. // 🎯 新方案:局部更新数据函数
  301. async function updateLocalData(operation, newData, parentId, targetId) {
  302. console.log(`🔄 局部更新数据: ${operation}`, { newData, parentId, targetId });
  303. if (operation === 'add') {
  304. // 新增:将新数据添加到指定父级的children中
  305. await handleLocalAdd(newData, parentId);
  306. } else if (operation === 'edit') {
  307. // 编辑:找到目标项目并更新其数据
  308. await handleLocalEdit(newData, targetId);
  309. } else if (operation === 'delete') {
  310. // 删除:从数据中移除目标项目
  311. await handleLocalDelete(targetId);
  312. }
  313. console.log('✅ 局部数据更新完成');
  314. }
  315. // 处理新增操作
  316. async function handleLocalAdd(newData, parentId) {
  317. console.log(`📝 处理新增: 父级ID=${parentId}`, newData);
  318. if (!newData) {
  319. console.error('❌ 新增数据为空');
  320. return;
  321. }
  322. // 转换新数据格式
  323. const transformedNewData = {
  324. id: newData.id,
  325. title: newData.name || '新权限',
  326. icon: newData.logo || 'menu',
  327. children: [],
  328. _original: newData,
  329. };
  330. if (!parentId || parentId === '' || parentId === '0') {
  331. // 添加到根级
  332. state.app.push(transformedNewData);
  333. // 更新第0层的数据
  334. if (state.show[0]) {
  335. state.show[0].data = state.app;
  336. }
  337. console.log('✅ 已添加到根级');
  338. } else {
  339. // 添加到指定父级
  340. const parentItem = findItemById(state.app, parentId);
  341. if (parentItem) {
  342. if (!parentItem.children) {
  343. parentItem.children = [];
  344. }
  345. parentItem.children.push(transformedNewData);
  346. // 更新对应层级的显示数据
  347. updateShowDataForParent(parentId, parentItem.children);
  348. console.log(`✅ 已添加到父级 ${parentId}`);
  349. } else {
  350. console.error(`❌ 找不到父级项目 ID: ${parentId}`);
  351. }
  352. }
  353. }
  354. // 处理编辑操作
  355. async function handleLocalEdit(newData, targetId) {
  356. console.log(`✏️ 处理编辑: 目标ID=${targetId}`, newData);
  357. if (!newData) {
  358. console.error('❌ 编辑数据为空');
  359. return;
  360. }
  361. // 🔧 验证数据格式
  362. if (!newData.name && !newData.id) {
  363. console.error('❌ 编辑数据格式不正确,缺少必要字段:', newData);
  364. return;
  365. }
  366. const targetItem = findItemById(state.app, targetId);
  367. if (targetItem) {
  368. // 更新项目数据
  369. targetItem.title = newData.name || newData.label || targetItem.title;
  370. targetItem.icon = newData.logo || newData.icon || targetItem.icon;
  371. targetItem._original = { ...targetItem._original, ...newData };
  372. // 更新所有相关的显示数据
  373. updateAllShowData();
  374. console.log(`✅ 已更新项目 ${targetId}:`, {
  375. title: targetItem.title,
  376. icon: targetItem.icon,
  377. });
  378. } else {
  379. console.error(`❌ 找不到目标项目 ID: ${targetId}`);
  380. }
  381. }
  382. // 处理删除操作
  383. async function handleLocalDelete(targetId) {
  384. console.log(`🗑️ 处理删除: 目标ID=${targetId}`);
  385. const result = removeItemById(state.app, targetId);
  386. if (result.success) {
  387. // 更新所有相关的显示数据
  388. updateAllShowData();
  389. console.log(`✅ 已删除项目 ${targetId}`);
  390. } else {
  391. console.error(`❌ 找不到要删除的项目 ID: ${targetId}`);
  392. }
  393. }
  394. // 递归查找项目
  395. function findItemById(items, targetId) {
  396. for (const item of items) {
  397. if (item.id === targetId) {
  398. return item;
  399. }
  400. if (item.children && item.children.length > 0) {
  401. const found = findItemById(item.children, targetId);
  402. if (found) return found;
  403. }
  404. }
  405. return null;
  406. }
  407. // 递归删除项目
  408. function removeItemById(items, targetId) {
  409. for (let i = 0; i < items.length; i++) {
  410. if (items[i].id === targetId) {
  411. items.splice(i, 1);
  412. return { success: true };
  413. }
  414. if (items[i].children && items[i].children.length > 0) {
  415. const result = removeItemById(items[i].children, targetId);
  416. if (result.success) return result;
  417. }
  418. }
  419. return { success: false };
  420. }
  421. // 更新指定父级的显示数据
  422. function updateShowDataForParent(parentId, newChildren) {
  423. // 遍历所有层级,找到对应的父级并更新其子级数据
  424. for (const [level, showData] of Object.entries(state.show)) {
  425. if (showData.data) {
  426. const parentItem = showData.data.find((item) => item.id === parentId);
  427. if (parentItem) {
  428. // 找到了父级,更新下一层级的数据
  429. const nextLevel = parseInt(level) + 1;
  430. if (state.show[nextLevel]) {
  431. state.show[nextLevel].data = newChildren;
  432. }
  433. break;
  434. }
  435. }
  436. }
  437. }
  438. // 更新所有显示数据
  439. function updateAllShowData() {
  440. // 更新第0层
  441. if (state.show[0]) {
  442. state.show[0].data = state.app;
  443. }
  444. // 更新其他层级
  445. for (const [level, showData] of Object.entries(state.show)) {
  446. if (parseInt(level) > 0 && showData.pdata && showData.pdata.children) {
  447. showData.data = showData.pdata.children;
  448. }
  449. }
  450. }
  451. function initAppItem() {
  452. let appItem = {
  453. id: 0,
  454. title: '应用',
  455. checked: true,
  456. indeterminate: false,
  457. children: state.app,
  458. };
  459. let allData = [];
  460. flattenData(state.app, allData);
  461. if (state.checkedIds.length == 0) {
  462. appItem.checked = false;
  463. appItem.indeterminate = false;
  464. } else {
  465. if (allData.length == state.checkedIds.length) {
  466. appItem.checked = true;
  467. appItem.indeterminate = false;
  468. } else {
  469. appItem.checked = false;
  470. appItem.indeterminate = true;
  471. }
  472. }
  473. return appItem;
  474. }
  475. // 初始化选中数据
  476. function initCalculate(data, parent = {}) {
  477. data.forEach((item) => {
  478. item.parent = parent;
  479. // 兼容数字和字符串ID的比较
  480. const itemIdStr = String(item.id);
  481. const itemIdNum = Number(item.id);
  482. const isChecked = state.checkedIds.some((checkedId) => {
  483. const checkedIdStr = String(checkedId);
  484. const checkedIdNum = Number(checkedId);
  485. return checkedIdStr === itemIdStr || checkedIdNum === itemIdNum;
  486. });
  487. // 先递归处理子项
  488. if (!isEmpty(item.children)) {
  489. initCalculate(item.children, item);
  490. // 检查子项的选中状态
  491. const checkedChildren = item.children.filter((child) => child.checked);
  492. const indeterminateChildren = item.children.filter((child) => child.indeterminate);
  493. // 如果父级本身被明确选中,保持选中状态
  494. if (isChecked) {
  495. item.checked = true;
  496. item.indeterminate = false;
  497. } else if (checkedChildren.length === item.children.length) {
  498. // 所有子项都选中
  499. item.checked = true;
  500. item.indeterminate = false;
  501. } else if (checkedChildren.length > 0 || indeterminateChildren.length > 0) {
  502. // 部分子项选中或有半选状态
  503. item.checked = false;
  504. item.indeterminate = true;
  505. } else {
  506. // 没有子项选中
  507. item.checked = false;
  508. item.indeterminate = false;
  509. }
  510. } else {
  511. // 叶子节点,直接根据是否在选中列表中设置状态
  512. item.checked = isChecked;
  513. item.indeterminate = false;
  514. }
  515. });
  516. }
  517. // 扁平化数据
  518. function flattenData(data, arr) {
  519. data.forEach((item) => {
  520. arr.push(item);
  521. if (!isEmpty(item.children)) {
  522. flattenData(item.children, arr);
  523. }
  524. });
  525. }
  526. async function calculateShow(item, index = null, level = 0) {
  527. // 选中展开
  528. if (level != 0) {
  529. state.show[level].index = index;
  530. }
  531. // 清除多余数据
  532. for (let key in state.show) {
  533. if (key > level) {
  534. delete state.show[key];
  535. }
  536. }
  537. // loading
  538. state.show[Number(level) + 1] = {
  539. loading: true,
  540. };
  541. if (isEmpty(item.children)) {
  542. item.children = [];
  543. }
  544. state.show[Number(level) + 1] = {
  545. index: null,
  546. data: item.children,
  547. pdata: item,
  548. loading: false,
  549. };
  550. }
  551. async function onClick(item, index, level) {
  552. calculateShow(item, index, level);
  553. nextTick(() => {
  554. let left =
  555. document.getElementById('scrollWrap').scrollWidth -
  556. document.getElementById('scrollWrap').offsetWidth;
  557. document.getElementById('scrollWrap').scrollTo({
  558. top: 0,
  559. left: left,
  560. behavior: 'smooth',
  561. });
  562. });
  563. }
  564. function handleSelect(checked, item) {
  565. // 设置标志,阻止watch触发
  566. isSelectingPermission = true;
  567. manualChecked = true;
  568. // 计算所有子元素
  569. doChecked(item, checked);
  570. // 计算父元素
  571. doCheckedParent(item);
  572. // 收集选中的权限ID
  573. const newIds = [];
  574. getCheckedIds(state.app, newIds);
  575. // 直接发送事件,不更新state.newIds避免触发watch
  576. emit('update:modelValue', newIds);
  577. // 延迟重置标志
  578. setTimeout(() => {
  579. isSelectingPermission = false;
  580. }, 50);
  581. }
  582. function doChecked(item, checked) {
  583. item.checked = checked;
  584. item.indeterminate = false;
  585. if (!isEmpty(item.children)) {
  586. item.children.forEach((i) => {
  587. i.checked = checked;
  588. i.indeterminate = false;
  589. doChecked(i, checked);
  590. });
  591. }
  592. }
  593. function doCheckedParent(i) {
  594. // 如果有父级
  595. if (!isEmpty(i.parent)) {
  596. if (!isEmpty(i.parent.children)) {
  597. // 部分选中
  598. i.parent.checked = false;
  599. i.parent.indeterminate = true;
  600. // 全选中
  601. if (i.parent.children.every((k) => k.checked)) {
  602. i.parent.checked = true;
  603. i.parent.indeterminate = false;
  604. }
  605. // 未选中
  606. if (!i.parent.children.some((k) => k.checked || k.indeterminate)) {
  607. i.parent.checked = false;
  608. i.parent.indeterminate = false;
  609. }
  610. }
  611. doCheckedParent(i.parent);
  612. }
  613. }
  614. async function onEnd(e, level, pdata) {
  615. if (e.newIndex != e.oldIndex) {
  616. try {
  617. // 拖动元素修改seq(排序)
  618. const draggedItem = pdata.children[e.oldIndex];
  619. const targetItem = pdata.children[e.newIndex];
  620. // 更新排序值
  621. const newSeq = targetItem._original?.seq ? targetItem._original.seq - 1 : e.newIndex - 1;
  622. // 调用编辑接口更新排序
  623. await admin.auth.access.edit({
  624. id: draggedItem.id,
  625. seq: newSeq,
  626. });
  627. // 🎯 新方案:拖拽排序后保持选中状态刷新
  628. console.log('✅ 拖拽排序成功,保持选中状态刷新数据');
  629. await refreshDataKeepSelected();
  630. } catch (error) {
  631. console.error('更新排序失败:', error);
  632. // 如果更新失败,重新获取数据恢复状态
  633. await getData();
  634. }
  635. }
  636. }
  637. function onAdd(id = null, level) {
  638. useModal(
  639. AccessEdit,
  640. {
  641. title: '新建',
  642. type: 'add',
  643. parent_id: id,
  644. },
  645. {
  646. confirm: async (result) => {
  647. console.log('📝 新增回调数据:', result);
  648. if (result && result.event === 'confirm') {
  649. // 🎯 新方案:新增操作成功,保持选中状态刷新
  650. console.log('✅ 新增操作成功,保持选中状态刷新数据');
  651. await refreshDataKeepSelected();
  652. } else {
  653. console.warn('⚠️ 新增操作未确认,不执行任何操作');
  654. }
  655. },
  656. },
  657. );
  658. }
  659. function onEdit(id, index, level) {
  660. useModal(
  661. AccessEdit,
  662. {
  663. title: '编辑',
  664. type: 'edit',
  665. id: id,
  666. },
  667. {
  668. confirm: async (result) => {
  669. console.log('📝 编辑回调数据:', result);
  670. if (result && result.event === 'confirm') {
  671. // 🎯 新方案:编辑操作成功,保持选中状态刷新
  672. console.log('✅ 编辑操作成功,保持选中状态刷新数据');
  673. await refreshDataKeepSelected();
  674. } else {
  675. console.warn('⚠️ 编辑操作未确认,不执行任何操作');
  676. }
  677. },
  678. },
  679. );
  680. }
  681. async function onDelete(id, index, level) {
  682. try {
  683. const { code } = await admin.auth.access.delete({ id });
  684. if (code == '200') {
  685. // 🎯 新方案:删除操作成功,保持选中状态刷新
  686. console.log('✅ 删除操作成功,保持选中状态刷新数据');
  687. await refreshDataKeepSelected();
  688. }
  689. } catch (error) {
  690. console.error('删除权限失败:', error);
  691. }
  692. }
  693. // 移除可能导致递归更新的watch监听器
  694. // 权限选择的更新通过handleSelect函数直接处理
  695. watch(
  696. () => props.role_id,
  697. () => {
  698. if (props.isChangeParentId) {
  699. state.checkedIds = [];
  700. }
  701. getData();
  702. },
  703. );
  704. function getCheckedIds(data, targetArray = null) {
  705. const idsArray = targetArray || state.newIds;
  706. data.forEach((i) => {
  707. // 收集真正选中的权限(全选状态)
  708. if (i.checked && !i.indeterminate) {
  709. idsArray.push(i.id + '');
  710. }
  711. // 收集半选状态的父级权限(当子级被选中时,父级也要包含)
  712. else if (i.indeterminate) {
  713. idsArray.push(i.id + '');
  714. }
  715. if (!isEmpty(i.children)) {
  716. getCheckedIds(i.children, idsArray);
  717. }
  718. });
  719. }
  720. // 递归收集所有权限ID
  721. const collectAllPermissionIds = (items) => {
  722. const ids = [];
  723. items.forEach((item) => {
  724. ids.push(item.id);
  725. if (item.children && item.children.length > 0) {
  726. ids.push(...collectAllPermissionIds(item.children));
  727. }
  728. });
  729. return ids;
  730. };
  731. // 清空所有现有权限
  732. const clearAllPermissions = async () => {
  733. console.log('🗑️ 开始清空现有权限...');
  734. // 获取现有权限树
  735. const response = await admin.auth.access.getTree();
  736. if (response.success && response.data && response.data.length > 0) {
  737. // 收集所有权限ID
  738. const allIds = collectAllPermissionIds(response.data);
  739. console.log(`📋 找到 ${allIds.length} 个权限需要删除`);
  740. // 逐个删除(从子级开始删除,避免外键约束问题)
  741. for (let i = allIds.length - 1; i >= 0; i--) {
  742. const id = allIds[i];
  743. try {
  744. const deleteResponse = await admin.auth.access.delete({ id });
  745. if (deleteResponse.code === '200') {
  746. console.log(`✅ 删除权限成功: ID ${id}`);
  747. } else {
  748. console.warn(`⚠️ 删除权限失败: ID ${id}`, deleteResponse);
  749. }
  750. } catch (error) {
  751. console.warn(`⚠️ 删除权限异常: ID ${id}`, error);
  752. }
  753. // 延迟避免请求过快
  754. await new Promise((resolve) => setTimeout(resolve, 100));
  755. }
  756. console.log('🎯 现有权限清空完成');
  757. } else {
  758. console.log('📝 没有找到现有权限,跳过清空步骤');
  759. }
  760. };
  761. // 菜单权限初始化方法
  762. const initMenuPermissions = async () => {
  763. try {
  764. // 确认对话框
  765. await ElMessageBox.confirm(
  766. '此操作将清空所有现有权限并重新初始化菜单权限,是否继续?\n⚠️ 注意:此操作不可逆!',
  767. '初始化确认',
  768. {
  769. confirmButtonText: '确定',
  770. cancelButtonText: '取消',
  771. type: 'warning',
  772. dangerouslyUseHTMLString: true,
  773. },
  774. );
  775. initLoading.value = true;
  776. console.log('🚀 开始菜单权限初始化...');
  777. // 1. 首先清空所有现有权限
  778. await clearAllPermissions();
  779. // 数据转换函数
  780. const transformMenuData = (item, parentId = '') => ({
  781. id: 0,
  782. parentId: parentId,
  783. name: item.title,
  784. eName: item.name,
  785. logo: item.icon || '',
  786. composingKey: item.name,
  787. type: item.type === 'menu' ? 0 : item.type === 'page' ? 1 : 2,
  788. status: item.status === 'show' ? 0 : 1,
  789. seq: item.weigh || 0,
  790. url: '',
  791. isAction: item.type === 'api' ? '1' : '0',
  792. children: [],
  793. createTime: '',
  794. createUserId: '',
  795. updateTime: '',
  796. updateUserId: '',
  797. });
  798. // 根据权限名称和父ID查找新创建的权限ID
  799. const findPermissionId = async (name, parentId = '') => {
  800. console.log(`🔍 查找权限: ${name}, 父ID: "${parentId}"`);
  801. const response = await admin.auth.access.getTree();
  802. if (response.success && response.data) {
  803. console.log(`🔍 获取到的权限树数据:`, response.data);
  804. // 🎯 关键修复:对获取到的数据进行转换,确保有_original字段
  805. const transformedData = Array.isArray(response.data)
  806. ? transformPermissionData(response.data)
  807. : [];
  808. const findInTree = (items, level = 0) => {
  809. const indent = ' '.repeat(level);
  810. for (const item of items) {
  811. // 现在可以安全使用_original字段了
  812. const originalData = item._original || {};
  813. const itemName = originalData.name || '';
  814. const itemParentId = originalData.parentId || '';
  815. // 备用匹配:如果原始数据没有name,尝试使用转换后的title
  816. const fallbackName = item.title || '';
  817. // 匹配权限名称和父ID(优先使用原始数据的name字段,备用title字段)
  818. const nameMatches = itemName === name || (itemName === '' && fallbackName === name);
  819. if (nameMatches) {
  820. // 对于顶级权限,parentId可能是空字符串、null、undefined或0
  821. const isTopLevel = !parentId || parentId === '' || parentId === '0';
  822. const itemIsTopLevel = !itemParentId || itemParentId === '' || itemParentId === '0';
  823. // 🎯 修复:父ID匹配逻辑
  824. const parentIdMatches =
  825. (isTopLevel && itemIsTopLevel) || String(itemParentId) === String(parentId);
  826. if (parentIdMatches) {
  827. const matchedName = itemName || fallbackName;
  828. return item.id;
  829. } else {
  830. console.log(`${indent}❌ 父ID不匹配,跳过此权限`);
  831. }
  832. }
  833. if (item.children && item.children.length > 0) {
  834. const found = findInTree(item.children, level + 1);
  835. if (found) return found;
  836. }
  837. }
  838. return null;
  839. };
  840. const result = findInTree(transformedData); // 🎯 使用转换后的数据
  841. console.log(`🎯 查找结果: ${result ? `找到ID ${result}` : '未找到'}`);
  842. return result;
  843. }
  844. console.log('❌ 获取权限树失败');
  845. return null;
  846. };
  847. // 递归创建函数(修改版:处理后端不返回ID的情况)
  848. const createRecursive = async (items, parentId = '', level = 0) => {
  849. const indent = ' '.repeat(level);
  850. for (let i = 0; i < items.length; i++) {
  851. const item = items[i];
  852. console.log(`${indent}📝 创建权限: ${item.title} (${item.type}), 父ID: "${parentId}"`);
  853. // 创建当前权限
  854. const permissionData = transformMenuData(item, parentId);
  855. console.log(`${indent}📋 提交数据:`, permissionData);
  856. const response = await admin.auth.access.add(permissionData);
  857. if (response.code === '200') {
  858. console.log(`${indent}✅ 创建成功: ${item.title}`);
  859. // 如果有子权限,需要先查找新创建的权限ID
  860. if (item.children && item.children.length > 0) {
  861. console.log(
  862. `${indent}🔍 需要创建 ${item.children.length} 个子权限,正在查找父权限ID...`,
  863. );
  864. // 等待一下确保数据已保存
  865. await new Promise((resolve) => setTimeout(resolve, 500));
  866. // 查找新创建的权限ID
  867. const newId = await findPermissionId(item.title, parentId);
  868. if (newId) {
  869. console.log(`${indent}🎯 找到权限ID: ${newId}`);
  870. console.log(`${indent}📁 开始创建子权限...`);
  871. await createRecursive(item.children, newId, level + 1);
  872. } else {
  873. console.error(`${indent}❌ 无法找到新创建的权限ID: ${item.title}`);
  874. console.error(`${indent}🔍 查找条件: 名称="${item.title}", 父ID="${parentId}"`);
  875. // 尝试再次查找,增加等待时间
  876. console.log(`${indent}⏳ 等待更长时间后重试...`);
  877. await new Promise((resolve) => setTimeout(resolve, 1000));
  878. const retryId = await findPermissionId(item.title, parentId);
  879. if (retryId) {
  880. console.log(`${indent}🎯 重试成功,找到权限ID: ${retryId}`);
  881. await createRecursive(item.children, retryId, level + 1);
  882. } else {
  883. throw new Error(`无法找到新创建的权限ID: ${item.title}`);
  884. }
  885. }
  886. }
  887. } else {
  888. console.error(`${indent}❌ 创建失败: ${item.title}`, response);
  889. throw new Error(`创建权限失败: ${item.title} - ${response.message || '未知错误'}`);
  890. }
  891. // 延迟避免请求过快
  892. await new Promise((resolve) => setTimeout(resolve, 300));
  893. }
  894. };
  895. // 2. 开始创建新的权限结构
  896. console.log('🏗️ 开始创建新的权限结构...');
  897. const menuData = menuRulesData.data.menu;
  898. await createRecursive(menuData);
  899. console.log('🎉 菜单权限初始化完成!');
  900. ElMessage.success('菜单权限初始化成功!');
  901. // 3. 重新获取数据刷新界面
  902. await getData();
  903. } catch (error) {
  904. if (error.message !== 'cancel') {
  905. console.error('💥 初始化失败:', error);
  906. ElMessage.error(`初始化失败: ${error.message}`);
  907. }
  908. } finally {
  909. initLoading.value = false;
  910. }
  911. };
  912. onMounted(() => {
  913. // 无论是新增还是编辑都需要加载权限数据
  914. getData();
  915. });
  916. </script>
  917. <style lang="scss" scoped>
  918. .sa-access {
  919. font-size: 14px;
  920. color: var(--sa-subtitle);
  921. .el-main {
  922. display: flex;
  923. transition: all 3s;
  924. }
  925. .el-scrollbar {
  926. flex-shrink: 0;
  927. width: 258px;
  928. border-right: 1px solid var(--sa-border);
  929. border-left: 1px solid var(--sa-border);
  930. margin: 0 8px;
  931. background: var(--sa-background-assist);
  932. &:first-of-type {
  933. margin-left: 0;
  934. }
  935. &:last-of-type {
  936. margin-right: 0;
  937. }
  938. }
  939. .title {
  940. height: 32px;
  941. padding: 0 16px;
  942. border-bottom: 1px solid var(--sa-border);
  943. }
  944. .node {
  945. width: inherit;
  946. padding: 0 16px;
  947. cursor: pointer;
  948. .item {
  949. flex: 1;
  950. height: 32px;
  951. display: flex;
  952. align-items: center;
  953. justify-content: space-between;
  954. }
  955. .sa-icon {
  956. width: 16px !important;
  957. height: 16px !important;
  958. }
  959. .edit {
  960. color: var(--el-color-primary);
  961. }
  962. .delete {
  963. color: var(--el-color-danger);
  964. }
  965. .arrow-right {
  966. width: 14px;
  967. height: 14px;
  968. display: flex;
  969. align-items: center;
  970. }
  971. &.is-active {
  972. background: var(--t-bg-active);
  973. color: var(--el-color-primary);
  974. }
  975. }
  976. .empty {
  977. width: inherit;
  978. text-align: center;
  979. min-height: 400px;
  980. line-height: 400px;
  981. }
  982. }
  983. </style>