sa-access.vue 33 KB

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