centaline-data-driven-v3 0.1.44 → 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,23 +2,28 @@
2
2
  <div id="ct-tree" class="ct-tree" oncontextmenu="event.preventDefault()" style="overflow:auto;" v-loading="loading"
3
3
  :style="{ height: props.treeHeight - 10 + 'px' }">
4
4
  <el-tree class="tree-line" :props="defaultProps" :expand-on-click-node="false" @node-click="handleNodeClick"
5
- :filter-node-method="filterNode" lazy :load="loadNode" ref="refTree" @node-contextmenu="rightClick"
5
+ :filter-node-method="filterNode" lazy :load="loadNode" ref="refTree" @node-contextmenu="handleContextMenu"
6
6
  node-key="code" current-node-key="currentNodeKey" :default-expanded-keys="defaultExpandedKeys">
7
7
  <template #default="{ node, data }">
8
- <span class="node_content">
8
+ <span class="node_content" @mouseenter="handleMouseEnter(data, node)">
9
9
  <el-icon class="node-icon" :class="{ 'is-leaf': node.isLeaf }">
10
10
  <Document v-if="node.isLeaf" :size="14" />
11
11
  <Folder v-else-if="!node.expanded" :size="14" />
12
12
  <FolderOpened v-else :size="14" />
13
13
  </el-icon>
14
- <span style="" :ref="(el: refItem) => handleSetInputMap(el, data)">{{
15
- data.name }}</span>
14
+ <span style="" :ref="(el: refItem) => handleSetInputMap(el, data)">
15
+ {{ data.name }}
16
+ </span>
16
17
  <span @click.stop>
17
- <el-popover placement="right" :virtual-ref="nodeRef" virtual-triggering
18
- :visible="((data.code == currentData.code && menuVisible && allowedRoutes?.length > 0))">
19
- <div class="box-menu">
18
+ <el-popover placement="right" :virtual-ref="nodeRef" virtual-triggering :visible="showPopover(data)"
19
+ :show-after="300" :hide-after="300" :popper-class="allIconsMode ? 'popper-transparent' : ''">
20
+ <div class="box-menu" :class="{ 'box-menu-icons': allIconsMode }">
20
21
  <template v-for="(item, index) in allowedRoutes">
21
- <div class="opertion" @click="routerClickHandler(item)">
22
+ <template v-if="allIconsMode">
23
+ <img class="opertion" v-if="item.imgUrl" :src="item.imgUrl" :alt="item.controlLabel || ''"
24
+ :title="item.controlLabel" @click="routerClickHandler(item)" />
25
+ </template>
26
+ <div class="opertion" @click="routerClickHandler(item)" v-else>
22
27
  <div>
23
28
  <img v-if="item.imgUrl" :src="item.imgUrl" alt="" />
24
29
  {{ item.controlLabel }}
@@ -90,6 +95,13 @@ const retryCount = ref(0);
90
95
  const treeNodeRefMap = ref({});
91
96
  const allowedRoutes = ref([]);
92
97
 
98
+ // 新增:鼠标悬停相关状态
99
+ const hoverTimer = ref(null)
100
+ const hoverNodeCode = ref('')
101
+ const isHoverMode = ref(false) // 是否是悬停模式
102
+ const isManualClose = ref(false) // 是否是手动关闭
103
+ const allIconsMode = ref(false) // 是否全是图标模式
104
+
93
105
  // 默认展开的节点key数组
94
106
  const defaultExpandedKeys = ref([])
95
107
  // 是否已设置默认展开
@@ -104,6 +116,7 @@ const fullTreeData = ref([])
104
116
  const nodeCodeMap = ref(new Map())
105
117
 
106
118
  const isExpandAll = ref(false);
119
+
107
120
  /** 动态设置Input Ref */
108
121
  const handleSetInputMap = (el: refItem, item) => {
109
122
  if (el) {
@@ -117,53 +130,136 @@ onBeforeUnmount(() => {
117
130
  clearTimeout(qrtimer.value);
118
131
  qrtimer.value = null;
119
132
  }
133
+ if (hoverTimer.value) {
134
+ clearTimeout(hoverTimer.value);
135
+ hoverTimer.value = null;
136
+ }
120
137
  })
121
138
 
122
- function getFirstNLevelKeys(nodes, targetLevel = 1) {
123
- const result = [];
139
+ // 检查allowedRoutes是否全是图标
140
+ function checkIfAllIcons(routes) {
141
+ if (!routes || routes.length === 0) return false;
142
+ return routes.every(item => item.imgUrl && item.imgUrl.trim() !== '');
143
+ }
124
144
 
125
- if (!nodes || nodes.length === 0 || targetLevel < 0) {
126
- return result;
145
+ // 显示弹出框的条件
146
+ function showPopover(data) {
147
+ if (isManualClose.value) {
148
+ return false;
127
149
  }
128
150
 
129
- const traverse = (nodes, currentLevel) => {
130
- if (currentLevel > targetLevel || !nodes) return;
151
+ const isCurrentNode = data.code === currentData.value.code;
152
+ const hasRoutes = allowedRoutes.value.length > 0;
131
153
 
132
- nodes.forEach(node => {
133
- if (node.code) {
134
- result.push(node.code);
135
- }
154
+ if (isHoverMode.value) {
155
+ // 悬停模式:当前节点且有路由就显示
156
+ return isCurrentNode && hasRoutes && menuVisible.value;
157
+ } else {
158
+ // 右键模式:右键触发才显示
159
+ return isCurrentNode && hasRoutes && menuVisible.value;
160
+ }
161
+ }
136
162
 
137
- // 如果还有下一层且没超过目标层级,继续遍历
138
- if (currentLevel < targetLevel && node.children && node.children.length > 0) {
139
- traverse(node.children, currentLevel + 1);
140
- }
141
- });
142
- };
163
+ // 鼠标进入节点
164
+ function handleMouseEnter(data, node) {
165
+ if (isManualClose.value) {
166
+ isManualClose.value = false;
167
+ return;
168
+ }
169
+
170
+ // 检查当前节点的allowedRoutes是否全是图标
171
+ const routes = getAllowedRoutesForNode(data);
172
+ const allIcons = checkIfAllIcons(routes);
173
+ allIconsMode.value = allIcons;
174
+ if (allIcons) {
175
+ isHoverMode.value = true;
176
+ hoverNodeCode.value = data.code;
143
177
 
144
- traverse(nodes, 0);
145
- return result;
178
+
179
+ if (hoverNodeCode.value === data.code) {
180
+ showNodeMenu(data, node, true);
181
+ }
182
+ }
146
183
  }
147
184
 
148
- // 递归获取所有节点的key(用于搜索时全展开)
149
- function getAllNodeKeys(nodes, result = []) {
150
- if (!nodes || nodes.length === 0) {
151
- return result;
185
+
186
+
187
+ // 右键菜单事件
188
+ function handleContextMenu(event, object, node) {
189
+ event.preventDefault();
190
+
191
+ // 检查当前节点的allowedRoutes是否全是图标
192
+ const routes = getAllowedRoutesForNode(object);
193
+ const allIcons = checkIfAllIcons(routes);
194
+
195
+ // 如果全是图标,右键时不执行后面代码(不显示菜单)
196
+ if (allIcons) {
197
+ return;
152
198
  }
153
199
 
154
- nodes.forEach(node => {
155
- if (node.code) {
156
- result.push(node.code);
157
- }
200
+ showNodeMenu(object, node, false);
201
+ }
202
+
203
+ // 共用方法:显示节点菜单
204
+ function showNodeMenu(object, node, isHoverTrigger = false) {
205
+ nodeRef.value = treeNodeRefMap.value[`node_Ref_${object.code}`]
206
+
207
+ currentData.value = object
208
+ currentNode.value = node
209
+
210
+ allowedRoutes.value = getAllowedRoutesForNode(object);
211
+
158
212
 
159
- if (node.children && node.children.length > 0) {
160
- getAllNodeKeys(node.children, result);
161
- }
162
- });
163
213
 
164
- return result;
214
+ if (allowedRoutes.value.length > 0) {
215
+ menuVisible.value = true;
216
+
217
+ // if (isHoverTrigger && allIcons) {
218
+ // // 悬停模式:自动关闭定时器
219
+ // setTimeout(() => {
220
+ // if (menuVisible.value && isHoverMode.value) {
221
+ // closeMenu();
222
+ // }
223
+ // }, 300); // 3秒后自动关闭
224
+ // }
225
+
226
+ // 添加点击事件监听
227
+ document.addEventListener('click', closeMenuOnClick);
228
+ }
165
229
  }
166
230
 
231
+ // 点击关闭菜单
232
+ function closeMenuOnClick(e) {
233
+ // 检查点击是否在菜单内部
234
+ const menu = document.querySelector('.box-menu');
235
+ if (menu && !menu.contains(e.target)) {
236
+ closeMenu();
237
+ }
238
+ }
239
+
240
+ //关闭菜单
241
+ function closeMenu() {
242
+ isManualClose.value = true;
243
+ menuVisible.value = false;
244
+ isHoverMode.value = false;
245
+
246
+ hoverNodeCode.value = '';
247
+ nodeRef.value = null;
248
+ // currentData.value = { ...currentData.value, code: "" };
249
+
250
+ // 移除事件监听
251
+ document.removeEventListener('click', closeMenuOnClick);
252
+
253
+ if (hoverTimer.value) {
254
+ clearTimeout(hoverTimer.value);
255
+ hoverTimer.value = null;
256
+ }
257
+ }
258
+
259
+
260
+
261
+
262
+
167
263
  // 设置默认展开节点
168
264
  function setDefaultExpandedNodes() {
169
265
  if (!props.expandTwoLevels || !fullTreeData.value.length) {
@@ -172,70 +268,44 @@ function setDefaultExpandedNodes() {
172
268
 
173
269
  if (isSearching.value) {
174
270
  isExpandAll.value = true;
175
- const allKeys = getAllNodeKeys(fullTreeData.value);
271
+ const allKeys = Tree.getAllNodeKeys(fullTreeData.value);
176
272
  defaultExpandedKeys.value = allKeys;
177
273
  } else {
178
274
  isExpandAll.value = false;
179
275
  // 检查是否有全展开搜索条件
180
276
  const expandValue = findSearchFieldValue(screenPara.value, 'expanded', {
181
- caseInsensitive: true, // 添加不区分大小写的选项
182
- keyFields: ['fieldName1', 'groupName', 'fieldLabel'] // 扩展可能的key字段
277
+ caseInsensitive: true,
278
+ keyFields: ['fieldName1', 'groupName', 'fieldLabel']
183
279
  });
184
280
 
185
- // 如果找到expanded=all的搜索条件,设置全展开
186
281
  if (expandValue && expandValue.toString().toLowerCase() === 'all') {
187
282
  isExpandAll.value = true;
188
- const allKeys = getAllNodeKeys(fullTreeData.value);
283
+ const allKeys = Tree.getAllNodeKeys(fullTreeData.value);
189
284
  defaultExpandedKeys.value = allKeys;
190
285
  } else {
191
- // 处理非'all'的情况
192
- let expandLevel = 1; // 默认展开级别为1
286
+ let expandLevel = 1;
193
287
 
194
288
  if (expandValue) {
195
289
  const numericValue = Number(expandValue);
196
290
  if (!isNaN(numericValue) && numericValue > 0) {
197
- expandLevel = Math.max(0, numericValue - 1); // 减1但最小为1
198
-
291
+ expandLevel = Math.max(0, numericValue - 1);
199
292
  }
200
293
  }
201
294
 
202
- defaultExpandedKeys.value = getFirstNLevelKeys(fullTreeData.value, expandLevel);
295
+ defaultExpandedKeys.value = Tree.getFirstNLevelKeys(fullTreeData.value, expandLevel);
203
296
  }
204
297
  }
205
298
 
206
299
  hasSetDefaultExpand.value = true;
207
300
  }
208
301
 
209
- // 新增:构建节点Map,便于快速查找
210
- function buildNodeCodeMap(nodes) {
211
- const map = new Map();
212
-
213
- function traverse(nodeList) {
214
- nodeList.forEach(node => {
215
- if (node.code) {
216
- map.set(node.code, node);
217
- }
218
302
 
219
- if (node.children && node.children.length > 0) {
220
- traverse(node.children);
221
- }
222
- });
223
- }
224
303
 
225
- traverse(nodes);
226
- return map;
227
- }
228
304
 
229
- // 新增:复制节点(深拷贝)
230
- function cloneNode(node) {
231
- const cloned = { ...node };
232
- cloned.children = [];
233
- return cloned;
234
- }
235
305
 
236
306
  // 搜索(查询条件调用)
237
307
  function search(m) {
238
- const hasValidSearchConditions = checkHasValidSearchConditions(m);
308
+ const hasValidSearchConditions = Tree.checkHasValidSearchConditions(m);
239
309
 
240
310
  isSearching.value = hasValidSearchConditions;
241
311
  screenPara.value = m;
@@ -265,109 +335,6 @@ function search(m) {
265
335
  }
266
336
  }
267
337
 
268
- // 检查是否有有效的搜索条件
269
- function checkHasValidSearchConditions(filter) {
270
- if (!filter) return false;
271
-
272
- let searchConditions = [];
273
-
274
- if (filter?.searchData?.fields) {
275
- searchConditions = filter.searchData.fields;
276
- } else if (filter?.fields) {
277
- searchConditions = filter.fields;
278
- }
279
-
280
- const validConditions = filterValidConditions(searchConditions);
281
- return validConditions && validConditions.length > 0;
282
- }
283
-
284
-
285
-
286
- // 过滤掉 operation 为 1 的条件
287
- function filterValidConditions(conditions) {
288
- if (!conditions || !Array.isArray(conditions)) return [];
289
-
290
- return conditions.filter(condition => {
291
- return condition.operation !== 1;
292
- });
293
- }
294
-
295
- // 判断节点是否匹配所有搜索条件
296
- function nodeMatchesAllConditions(node, conditions) {
297
- const validConditions = filterValidConditions(conditions);
298
-
299
- if (!validConditions || validConditions.length === 0) return true;
300
-
301
- return validConditions.every(condition => {
302
- const { fieldName1, operation, searchValue1, searchValue2, searchDataType } = condition;
303
- let nodeValue = node[fieldName1];
304
-
305
- if (nodeValue === undefined) {
306
- const mappedField = Object.keys(node).find(key =>
307
- key.toLowerCase() === fieldName1.toLowerCase()
308
- );
309
- if (mappedField) {
310
- nodeValue = node[mappedField];
311
- } else {
312
- return false;
313
- }
314
- }
315
-
316
- const strNodeValue = String(nodeValue || '').toLowerCase();
317
- const strSearchValue = String(searchValue1 || '').toLowerCase();
318
-
319
- switch (operation) {
320
- case 2:
321
- return strNodeValue === strSearchValue;
322
- case 3:
323
- return strNodeValue !== strSearchValue;
324
- case 4:
325
- return Number(nodeValue) > Number(searchValue1);
326
- case 5:
327
- return Number(nodeValue) >= Number(searchValue1);
328
- case 6:
329
- return Number(nodeValue) < Number(searchValue1);
330
- case 7:
331
- return Number(nodeValue) <= Number(searchValue1);
332
- case 8:
333
- return strNodeValue.includes(strSearchValue);
334
- case 9:
335
- return strNodeValue.startsWith(strSearchValue);
336
- case 10:
337
- return strNodeValue.endsWith(strSearchValue);
338
- case 11:
339
- return !strNodeValue.includes(strSearchValue);
340
- case 12:
341
- try {
342
- const regex = new RegExp(searchValue1, 'i');
343
- return regex.test(nodeValue);
344
- } catch (e) {
345
- console.warn('正则表达式错误:', e);
346
- return false;
347
- }
348
- case 13:
349
- if (!searchValue1) return false;
350
- try {
351
- const values = searchValue1.split(',').map(v => v.trim().toLowerCase());
352
- return values.includes(strNodeValue);
353
- } catch (e) {
354
- console.warn('解析IN条件错误:', e);
355
- return false;
356
- }
357
- case 14:
358
- if (!searchValue1) return true;
359
- try {
360
- const values = searchValue1.split(',').map(v => v.trim().toLowerCase());
361
- return !values.includes(strNodeValue);
362
- } catch (e) {
363
- console.warn('解析NotIN条件错误:', e);
364
- return false;
365
- }
366
- default:
367
- return false;
368
- }
369
- });
370
- }
371
338
 
372
339
  // 修改:构建完整的过滤树,保持原始层级结构
373
340
  function buildFilteredTreeWithOriginalHierarchy(matchedNodes) {
@@ -381,7 +348,7 @@ function buildFilteredTreeWithOriginalHierarchy(matchedNodes) {
381
348
  // 第一步:创建所有匹配节点的副本
382
349
  matchedNodes.forEach(node => {
383
350
  if (!resultMap.has(node.code)) {
384
- const clonedNode = cloneNode(node);
351
+ const clonedNode = Tree.cloneNode(node);
385
352
  resultMap.set(node.code, clonedNode);
386
353
  }
387
354
  });
@@ -424,12 +391,11 @@ function buildFilteredTreeWithOriginalHierarchy(matchedNodes) {
424
391
  if (!originalNode) continue;
425
392
 
426
393
  // 创建节点副本
427
- const clonedNode = cloneNode(originalNode);
394
+ const clonedNode = Tree.cloneNode(originalNode);
428
395
  resultMap.set(currentCode, clonedNode);
429
396
 
430
397
  // 如果是根节点,添加到根节点数组
431
398
  if (i === 0) {
432
- // 检查是否已经存在于根节点中
433
399
  const existingRoot = rootNodes.find(n => n.code === currentCode);
434
400
  if (!existingRoot) {
435
401
  rootNodes.push(clonedNode);
@@ -442,7 +408,6 @@ function buildFilteredTreeWithOriginalHierarchy(matchedNodes) {
442
408
  const parentNode = resultMap.get(parentCode);
443
409
 
444
410
  if (parentNode) {
445
- // 检查是否已经包含该子节点
446
411
  const existingChild = parentNode.children.find(child => child.code === currentCode);
447
412
  if (!existingChild) {
448
413
  parentNode.children.push(clonedNode);
@@ -458,7 +423,6 @@ function buildFilteredTreeWithOriginalHierarchy(matchedNodes) {
458
423
  const currentNode = resultMap.get(matchedNode.code);
459
424
 
460
425
  if (parentNode && currentNode) {
461
- // 检查是否已经包含该子节点
462
426
  const existingChild = parentNode.children.find(child => child.code === matchedNode.code);
463
427
  if (!existingChild) {
464
428
  parentNode.children.push(currentNode);
@@ -469,11 +433,10 @@ function buildFilteredTreeWithOriginalHierarchy(matchedNodes) {
469
433
 
470
434
  // 第三步:清理根节点,确保它们没有重复的父节点
471
435
  const finalRootNodes = [];
472
- const usedNodes = new Set();
473
436
 
474
437
  function isNodeUsedInTree(node) {
475
438
  for (const root of rootNodes) {
476
- if (isNodeInChildren(root, node.code)) {
439
+ if (Tree.isNodeInChildren(root, node.code)) {
477
440
  return true;
478
441
  }
479
442
  }
@@ -481,7 +444,6 @@ function buildFilteredTreeWithOriginalHierarchy(matchedNodes) {
481
444
  }
482
445
 
483
446
  rootNodes.forEach(rootNode => {
484
- // 如果这个节点已经是其他节点的子节点,则不作为根节点
485
447
  if (!isNodeUsedInTree(rootNode)) {
486
448
  finalRootNodes.push(rootNode);
487
449
  }
@@ -490,6 +452,7 @@ function buildFilteredTreeWithOriginalHierarchy(matchedNodes) {
490
452
  return finalRootNodes;
491
453
  }
492
454
 
455
+
493
456
  // 新增:在全量树中查找父节点code
494
457
  function findParentCodeInFullTree(nodeCode) {
495
458
  function findParent(nodes, targetCode, parentCode = null) {
@@ -511,29 +474,6 @@ function findParentCodeInFullTree(nodeCode) {
511
474
  return findParent(fullTreeData.value, nodeCode);
512
475
  }
513
476
 
514
- // 新增:检查节点是否在子树中
515
- function isNodeInChildren(parentNode, targetCode) {
516
- if (parentNode.code === targetCode) {
517
- return false; // 不检查自身
518
- }
519
-
520
- if (!parentNode.children || parentNode.children.length === 0) {
521
- return false;
522
- }
523
-
524
- for (const child of parentNode.children) {
525
- if (child.code === targetCode) {
526
- return true;
527
- }
528
-
529
- if (isNodeInChildren(child, targetCode)) {
530
- return true;
531
- }
532
- }
533
-
534
- return false;
535
- }
536
-
537
477
  // 修改:过滤全量树数据,保持原始层级
538
478
  function filterFullTreeData(filter) {
539
479
  if (!fullTreeData.value.length) {
@@ -550,7 +490,7 @@ function filterFullTreeData(filter) {
550
490
  searchConditions = filter.fields;
551
491
  }
552
492
 
553
- const validConditions = filterValidConditions(searchConditions);
493
+ const validConditions = Tree.filterValidConditions(searchConditions);
554
494
 
555
495
  if (!validConditions || validConditions.length === 0) {
556
496
  refTree.value.store.setData(fullTreeData.value);
@@ -559,7 +499,6 @@ function filterFullTreeData(filter) {
559
499
  hasSetDefaultExpand.value = false;
560
500
  defaultExpandedKeys.value = [];
561
501
 
562
- // 先收起所有节点
563
502
  if (refTree.value && refTree.value.store) {
564
503
  Object.values(refTree.value.store.nodesMap).forEach((node: any) => {
565
504
  if (node) {
@@ -575,7 +514,7 @@ function filterFullTreeData(filter) {
575
514
  // 递归查找所有匹配的节点
576
515
  const findMatchedNodes = (nodes, result = []) => {
577
516
  nodes.forEach(node => {
578
- const nodeMatches = nodeMatchesAllConditions(node, validConditions);
517
+ const nodeMatches = Tree.nodeMatchesAllConditions(node, validConditions);
579
518
 
580
519
  if (nodeMatches) {
581
520
  result.push(node);
@@ -590,18 +529,15 @@ function filterFullTreeData(filter) {
590
529
 
591
530
  const matchedNodes = findMatchedNodes(fullTreeData.value);
592
531
 
593
- // 构建保持原始层级的过滤树
594
532
  let filteredData = [];
595
533
  if (matchedNodes.length > 0) {
596
534
  filteredData = buildFilteredTreeWithOriginalHierarchy(matchedNodes);
597
535
  }
598
536
 
599
- // 设置过滤后的数据
600
537
  refTree.value.store.setData(filteredData);
601
538
 
602
- // 搜索状态时展开所有节点
603
539
  if (filteredData.length > 0 && isSearching.value) {
604
- const allKeys = getAllNodeKeys(filteredData);
540
+ const allKeys = Tree.getAllNodeKeys(filteredData);
605
541
  defaultExpandedKeys.value = allKeys;
606
542
 
607
543
  nextTick(() => {
@@ -614,7 +550,6 @@ function filterFullTreeData(filter) {
614
550
  }
615
551
  });
616
552
 
617
- // 默认选中第一个可选节点
618
553
  const findFirstSelectableNode = (nodes) => {
619
554
  for (const node of nodes) {
620
555
  if (!node.children || node.children.length === 0) {
@@ -632,7 +567,8 @@ function filterFullTreeData(filter) {
632
567
  refTree.value.setCurrentKey(firstNode.code);
633
568
  handleNodeClick(firstNode, true);
634
569
  });
635
- }
570
+ }
571
+
636
572
  }
637
573
 
638
574
  } catch (error) {
@@ -642,9 +578,19 @@ function filterFullTreeData(filter) {
642
578
  }
643
579
  }
644
580
 
581
+ // 获取节点的allowedRoutes
582
+ function getAllowedRoutesForNode(data) {
583
+ if (!model.value || !model.value.actionRouter) return [];
584
+
585
+ return model.value.actionRouter.filter(v => {
586
+ const val = data[v.rightField];
587
+ const hit = val == '1' || val === undefined || val === null;
588
+ return hit;
589
+ });
590
+ }
591
+
645
592
  // 修改:加载节点,保存节点Map
646
593
  function loadNode(node, resolve) {
647
-
648
594
  if (!lazy.value) {
649
595
  const nodeData = node.data;
650
596
  resolve(nodeData.children)
@@ -659,16 +605,14 @@ function loadNode(node, resolve) {
659
605
  return SearchTree(screenPara.value || undefined).then(data => {
660
606
  loading.value = false;
661
607
 
662
-
663
- const { tree, hasAnyLeaf } = buildDeptTreeByField(data.rows, {
608
+ const { tree, hasAnyLeaf } = Tree.buildDeptTreeByField(data.rows, {
664
609
  pathKey: 'path',
665
610
  separator: '.'
666
611
  });
667
612
 
668
- // 缓存全量数据并构建节点Map
669
613
  if (hasAnyLeaf) {
670
614
  fullTreeData.value = tree;
671
- nodeCodeMap.value = buildNodeCodeMap(tree);
615
+ nodeCodeMap.value = Tree.buildNodeCodeMap(tree);
672
616
  resolve(tree);
673
617
  if (props.expandTwoLevels) {
674
618
  nextTick(() => {
@@ -805,7 +749,6 @@ function clearSearch() {
805
749
 
806
750
  //节点点击事件
807
751
  function handleNodeClick(data, formType) {
808
-
809
752
  closeMenu();
810
753
  if (data && formType) {
811
754
  refTree.value.setCurrentKey(data.code)
@@ -833,40 +776,9 @@ function handleNodeClick(data, formType) {
833
776
  }
834
777
  }
835
778
 
836
- //右键菜单
837
- function rightClick(event, object, Node) {
838
- nodeRef.value = treeNodeRefMap.value[`node_Ref_${object.code}`]
839
-
840
- currentData.value = object
841
- currentNode.value = Node
842
- let actionIndex = 0;
843
-
844
- allowedRoutes.value = (model.value.actionRouter || []).filter(v => {
845
- const val = currentData.value[v.rightField]
846
- const hit = val == '1' || val === undefined || val === null;
847
- return hit;
848
- })
849
-
850
- allowedRoutes.value?.forEach(() => {
851
- menuVisible.value = true
852
- actionIndex += 1
853
- })
854
-
855
- if (menuVisible.value) {
856
- document.addEventListener('click', closeMenu)
857
- }
858
- }
859
-
860
- //关闭菜单
861
- function closeMenu() {
862
- menuVisible.value = false;
863
- nodeRef.value = null
864
- currentData.value = { ...currentData.value, code: "" }
865
- document.removeEventListener('click', closeMenu)
866
- }
867
-
868
779
  // 菜单路由
869
780
  function routerClickHandler(field) {
781
+ isManualClose.value = true;
870
782
  var submitData = {};
871
783
  field.submitFormField.forEach(v => {
872
784
  submitData[v] = currentData.value[v];
@@ -893,29 +805,232 @@ function routerClickHandler(field) {
893
805
  })
894
806
  }
895
807
 
896
- //删除树节点
897
- function removeNode(newData) {
898
- getNextClickNode(currentNode.value)
899
- refTree.value.remove(currentNode.value);
900
-
901
- }
902
-
903
808
  //新增树节点
904
809
  function addNode(newData) {
905
- refTree.value.append(newData, currentNode.value)
906
- handleNodeClick(newData, true)
907
- }
810
+ if (!lazy.value) {
811
+ // 全量数据模式:手动更新全量数据和树显示
812
+ const parentCode = currentData.value.code;
813
+ const parentPath = currentData.value.path || '';
814
+
815
+ // 设置新节点的属性
816
+ const newNode = {
817
+ ...newData,
818
+ // 如果后端没有返回path,需要构建
819
+ path: parentPath ? `${parentPath}.${newData.code}` : newData.code,
820
+ // 确保children存在
821
+ children: newData.children || [],
822
+ // 标识是否为叶子节点
823
+ isLeaf: true
824
+ };
825
+
826
+ // 在全量数据中找到父节点并添加
827
+ const findAndAdd = (nodes, parentCode) => {
828
+ for (let i = 0; i < nodes.length; i++) {
829
+ if (nodes[i].code === parentCode) {
830
+ if (!nodes[i].children) {
831
+ nodes[i].children = [];
832
+ }
833
+ nodes[i].children.push(newNode);
834
+ nodes[i].isLeaf = false; // 父节点不再是叶子节点
835
+ return true;
836
+ }
837
+
838
+ if (nodes[i].children && nodes[i].children.length > 0) {
839
+ if (findAndAdd(nodes[i].children, parentCode)) {
840
+ return true;
841
+ }
842
+ }
843
+ }
844
+ return false;
845
+ };
846
+
847
+ // 更新全量数据
848
+ findAndAdd(fullTreeData.value, parentCode);
849
+
850
+ // 更新nodeCodeMap
851
+ nodeCodeMap.value.set(newNode.code, newNode);
852
+
853
+ // 如果是搜索状态,需要同时更新当前显示的数据
854
+ if (isSearching.value && screenPara.value) {
855
+ filterFullTreeData(screenPara.value);
856
+ } else {
857
+ // 非搜索状态,直接重新设置数据
858
+ refTree.value.store.setData([...fullTreeData.value]);
859
+ }
860
+
861
+ // 刷新树显示
862
+ nextTick(() => {
863
+ // 展开父节点
864
+ const parentNode = refTree.value.getNode(parentCode);
865
+ if (parentNode && !parentNode.expanded) {
866
+ refTree.value.store.setCurrentNode(parentNode);
867
+ parentNode.expanded = true;
868
+ }
869
+
870
+ // 设置新节点为当前选中
871
+ handleNodeClick(newNode, true);
872
+ });
873
+ } else {
874
+ // 懒加载模式:使用原有方法
875
+ refTree.value.append(newData, currentNode.value);
876
+ handleNodeClick(newData, true);
877
+ }
878
+ }
908
879
 
909
880
  //修改树节点
910
881
  function updateNode(newData) {
911
- for (let key in newData) {
912
- if (typeof newData[key] === 'object') {
913
- currentData.value[key] = { ...currentData.value[key], ...newData[key] }
882
+ if (!lazy.value) {
883
+ // 全量数据模式:更新全量数据
884
+ const updateInTree = (nodes, nodeCode) => {
885
+ for (let i = 0; i < nodes.length; i++) {
886
+ if (nodes[i].code === nodeCode) {
887
+ // 合并数据
888
+ nodes[i] = { ...nodes[i], ...newData };
889
+ return true;
890
+ }
891
+
892
+ if (nodes[i].children && nodes[i].children.length > 0) {
893
+ if (updateInTree(nodes[i].children, nodeCode)) {
894
+ return true;
895
+ }
896
+ }
897
+ }
898
+ return false;
899
+ };
900
+
901
+ // 更新全量数据
902
+ updateInTree(fullTreeData.value, currentData.value.code);
903
+
904
+ // 更新nodeCodeMap
905
+ nodeCodeMap.value.set(currentData.value.code, {
906
+ ...nodeCodeMap.value.get(currentData.value.code),
907
+ ...newData
908
+ });
909
+
910
+ // 如果是搜索状态,重新过滤显示
911
+ if (isSearching.value && screenPara.value) {
912
+ filterFullTreeData(screenPara.value);
914
913
  } else {
915
- currentData.value[key] = newData[key];
914
+ // 非搜索状态,直接重新设置数据
915
+ refTree.value.store.setData([...fullTreeData.value]);
916
916
  }
917
+
918
+ // 刷新显示并保持选中状态
919
+ nextTick(() => {
920
+ refTree.value.setCurrentKey(currentData.value.code);
921
+ handleNodeClick({ ...currentData.value, ...newData }, true);
922
+ });
923
+ } else {
924
+ // 懒加载模式:使用原有方法
925
+ for (let key in newData) {
926
+ if (typeof newData[key] === 'object') {
927
+ currentData.value[key] = { ...currentData.value[key], ...newData[key] }
928
+ } else {
929
+ currentData.value[key] = newData[key];
930
+ }
931
+ }
932
+ handleNodeClick(newData, true);
917
933
  }
918
- handleNodeClick(newData, true)
934
+ }
935
+
936
+ //删除树节点
937
+ function removeNode(newData) {
938
+ if (!lazy.value) {
939
+ // 全量数据模式:从全量数据中删除
940
+ let parentNode = null;
941
+ let parentCode = null;
942
+
943
+ const removeFromTree = (nodes, nodeCode) => {
944
+ for (let i = 0; i < nodes.length; i++) {
945
+ if (nodes[i].code === nodeCode) {
946
+ // 记录父节点信息(如果是当前循环的节点)
947
+ parentCode = findParentCodeInFullTree(nodeCode);
948
+ if (parentCode) {
949
+ const parent = findNodeByCode(fullTreeData.value, parentCode);
950
+ if (parent) {
951
+ parentNode = parent;
952
+ }
953
+ }
954
+
955
+ // 删除节点
956
+ nodes.splice(i, 1);
957
+ nodeCodeMap.value.delete(nodeCode);
958
+ return true;
959
+ }
960
+
961
+ if (nodes[i].children && nodes[i].children.length > 0) {
962
+ if (removeFromTree(nodes[i].children, nodeCode)) {
963
+ return true;
964
+ }
965
+ }
966
+ }
967
+ return false;
968
+ };
969
+
970
+ // 从全量数据中删除
971
+ removeFromTree(fullTreeData.value, currentData.value.code);
972
+
973
+ // 更新父节点的 isLeaf 状态
974
+ if (parentCode && parentNode) {
975
+ // 重新检查父节点是否还有子节点
976
+ const hasChildren = parentNode.children && parentNode.children.length > 0;
977
+ parentNode.isLeaf = !hasChildren;
978
+
979
+ // 同步更新 nodeCodeMap
980
+ if (nodeCodeMap.value.has(parentCode)) {
981
+ const updatedParent = nodeCodeMap.value.get(parentCode);
982
+ updatedParent.isLeaf = !hasChildren;
983
+ nodeCodeMap.value.set(parentCode, updatedParent);
984
+ }
985
+ } else {
986
+ // 如果是根节点被删除,不需要更新 isLeaf
987
+ }
988
+
989
+ // 如果是搜索状态,重新过滤显示
990
+ if (isSearching.value && screenPara.value) {
991
+ filterFullTreeData(screenPara.value);
992
+ } else {
993
+ // 非搜索状态,直接重新设置数据
994
+ refTree.value.store.setData([...fullTreeData.value]);
995
+ }
996
+
997
+ // 查找下一个可点击的节点
998
+ getNextClickNode();
999
+ } else {
1000
+ // 懒加载模式:使用原有方法
1001
+ getNextClickNode();
1002
+ refTree.value.remove(currentNode.value);
1003
+ }
1004
+ }
1005
+
1006
+ // 根据 code 查找节点
1007
+ function findNodeByCode(nodes, targetCode) {
1008
+ for (const node of nodes) {
1009
+ if (node.code === targetCode) {
1010
+ return node;
1011
+ }
1012
+
1013
+ if (node.children && node.children.length > 0) {
1014
+ const found = findNodeByCode(node.children, targetCode);
1015
+ if (found) return found;
1016
+ }
1017
+ }
1018
+ return null;
1019
+ }
1020
+
1021
+ // 查找节点的父节点(返回父节点本身,不只是code)
1022
+ function findParentNodeInFullTree(nodes, targetCode, parent = null) {
1023
+ for (const node of nodes) {
1024
+ if (node.code === targetCode) {
1025
+ return parent;
1026
+ }
1027
+
1028
+ if (node.children && node.children.length > 0) {
1029
+ const found = findParentNodeInFullTree(node.children, targetCode, node);
1030
+ if (found) return found;
1031
+ }
1032
+ }
1033
+ return null;
919
1034
  }
920
1035
 
921
1036
  function getNextClickNode() {
@@ -937,98 +1052,22 @@ function filterNode(value, data) {
937
1052
  return data.name.toLowerCase().includes(value.toLowerCase())
938
1053
  }
939
1054
 
940
- function buildDeptTreeByField(flatList, opt = {}) {
941
- const {
942
- pathKey = 'path',
943
- separator = '.',
944
- sortKey = 'sort'
945
- } = opt;
946
-
947
- // 检查第一行是否有 sort 字段
948
- const firstItem = flatList.length > 0 ? flatList[0] : null;
949
- const actualSortKey = firstItem && firstItem.hasOwnProperty(sortKey) ? sortKey : pathKey;
950
-
951
- // 按路径排序,确保父节点在前
952
- const sortedList = [...flatList].sort((a, b) => {
953
- const aPath = a[pathKey] || '';
954
- const bPath = b[pathKey] || '';
955
- return aPath.length - bPath.length || aPath.localeCompare(bPath);
956
- });
957
-
958
- const pathMap = new Map(); // path -> node
959
- const root = [];
960
-
961
- for (const item of sortedList) {
962
- const path = item[pathKey];
963
- if (!path) continue;
964
-
965
- const node = {
966
- ...item,
967
- children: [],
968
- isLeaf: true // 默认为叶子节点,后续会更新
969
- };
970
-
971
- // 添加到路径映射
972
- pathMap.set(path, node);
973
-
974
- // 查找父节点
975
- const lastDotIndex = path.lastIndexOf(separator);
976
- if (lastDotIndex > -1) {
977
- const parentPath = path.substring(0, lastDotIndex);
978
- const parentNode = pathMap.get(parentPath);
979
-
980
- if (parentNode) {
981
- parentNode.children.push(node);
982
- parentNode.isLeaf = false; // 有子节点,所以不是叶子
983
- continue;
984
- }
985
- }
986
-
987
- // 没有找到父节点,或者没有父路径,则作为根节点
988
- root.push(node);
989
- }
990
-
991
- // 排序:按 sortKey 或 path 排序
992
- const sortTree = (nodes) => {
993
- nodes.sort((a, b) => {
994
- const aVal = a[actualSortKey] || a[pathKey] || '';
995
- const bVal = b[actualSortKey] || b[pathKey] || '';
996
- return String(aVal).localeCompare(String(bVal));
997
- });
998
-
999
- nodes.forEach(node => {
1000
- if (node.children && node.children.length > 0) {
1001
- sortTree(node.children);
1002
- }
1003
- });
1004
- };
1005
-
1006
- sortTree(root);
1007
-
1008
- const hasAnyLeaf = root.some(n => !n.isLeaf);
1009
- return { tree: root, hasAnyLeaf };
1010
- }
1011
1055
 
1012
1056
  function findSearchFieldValue(searchFields, targetKey, options = {}) {
1013
1057
  const {
1014
- keyFields = ['fieldName1', 'groupName'], // 要查找的key字段列表
1015
- valueField = 'searchValue1', // 要返回值的字段
1058
+ keyFields = ['fieldName1', 'groupName'],
1059
+ valueField = 'searchValue1',
1016
1060
  defaultValue = 2
1017
1061
  } = options;
1018
1062
 
1019
- // 检查数据结构
1020
1063
  if (!searchFields?.searchData?.fields?.length) {
1021
1064
  return defaultValue;
1022
1065
  }
1023
1066
 
1024
- // 遍历fields数组
1025
1067
  for (const field of searchFields.searchData.fields) {
1026
- // 检查所有可能的key字段
1027
1068
  for (const keyField of keyFields) {
1028
1069
  if (field[keyField] === targetKey) {
1029
- // 找到了匹配的key,返回对应的值
1030
1070
  const value = field[valueField];
1031
- // 如果值为undefined、null或空字符串,返回默认值
1032
1071
  return value !== undefined && value !== null && value !== ''
1033
1072
  ? value
1034
1073
  : defaultValue;
@@ -1039,8 +1078,6 @@ function findSearchFieldValue(searchFields, targetKey, options = {}) {
1039
1078
  return defaultValue;
1040
1079
  }
1041
1080
 
1042
-
1043
-
1044
1081
  // 暴露方法
1045
1082
  defineExpose({
1046
1083
  search,
@@ -1099,6 +1136,7 @@ defineExpose({
1099
1136
  .node_content {
1100
1137
  display: flex;
1101
1138
  align-items: center;
1139
+ position: relative;
1102
1140
  }
1103
1141
 
1104
1142
  .node-icon {
@@ -1109,20 +1147,129 @@ defineExpose({
1109
1147
  .box-menu {
1110
1148
  z-index: 1000;
1111
1149
  background-color: #fff;
1112
- padding: 10px;
1150
+ // box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
1151
+ border-radius: 4px;
1152
+ padding: 4px 0;
1113
1153
 
1114
1154
  div {
1115
1155
  cursor: pointer;
1116
1156
  }
1117
1157
  }
1118
1158
 
1159
+ .box-menu.box-menu-icons {
1160
+ display: flex;
1161
+ flex-wrap: nowrap;
1162
+ padding: 8px;
1163
+ gap: 0px;
1164
+ line-height: 15px;
1165
+ background: transparent !important;
1166
+ border: none !important;
1167
+ box-shadow: none !important;
1168
+ }
1169
+
1170
+ .box-menu-icons .opertion {
1171
+ width: 16px;
1172
+ height: 16px;
1173
+ padding: 0 !important;
1174
+ display: flex;
1175
+ align-items: center;
1176
+ justify-content: center;
1177
+ border-radius: 4px;
1178
+ margin: 0 2px !important;
1179
+ }
1180
+
1181
+ .box-menu-icons .opertion:hover {
1182
+ background-color: #f5f7fa;
1183
+ transform: translateY(-1px);
1184
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
1185
+ }
1186
+
1187
+ .box-menu-icons .opertion img {
1188
+ width: 20px;
1189
+ height: 20px;
1190
+ margin: 0;
1191
+ }
1192
+
1193
+ .box-menu-icons .opertion div {
1194
+ display: flex;
1195
+ align-items: center;
1196
+ justify-content: center;
1197
+ width: 100%;
1198
+ height: 100%;
1199
+ }
1200
+
1119
1201
  .box-menu .opertion {
1202
+ display: flex;
1203
+ align-items: center;
1204
+ padding: 4px;
1120
1205
  cursor: pointer;
1121
- line-height: 30px;
1206
+ font-size: 14px;
1207
+ color: #606266;
1208
+ transition: all 0.2s ease;
1209
+ line-height: 20px;
1210
+ margin: 0 4px;
1211
+ border-radius: 6px;
1212
+ position: relative;
1213
+ overflow: hidden;
1122
1214
  }
1123
1215
 
1124
1216
  .box-menu .opertion:hover {
1125
1217
  color: var(--btnHoverRed);
1126
- background-color: var(--el-fill-color-light);
1218
+ background-color: #f5f7fa;
1219
+ transform: translateY(-1px);
1220
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
1221
+ }
1222
+
1223
+ .box-menu .opertion:active {
1224
+ transform: translateY(0);
1225
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
1226
+ }
1227
+
1228
+ .box-menu .opertion:not(:last-child)::after {
1229
+ content: '';
1230
+ position: absolute;
1231
+ bottom: 0;
1232
+ left: 16px;
1233
+ right: 16px;
1234
+ height: 1px;
1235
+ background-color: #f0f0f0;
1236
+ }
1237
+
1238
+ .box-menu .opertion:last-child {
1239
+ margin-bottom: 0;
1240
+ }
1241
+
1242
+ .box-menu .opertion img {
1243
+ width: 16px;
1244
+ height: 16px;
1245
+ margin-right: 8px;
1246
+ vertical-align: middle;
1247
+ }
1248
+
1249
+ .box-menu .opertion div {
1250
+ display: flex;
1251
+ align-items: center;
1252
+ width: 100%;
1253
+ }
1254
+
1255
+ /* 菜单激活状态 */
1256
+ .box-menu .opertion.active {
1257
+ background-color: #ecf5ff;
1258
+ color: var(--centalineBlue);
1259
+ }
1260
+
1261
+ /* 悬停模式下的节点样式 */
1262
+ .node_content:hover {
1263
+ background-color: #f5f7fa;
1264
+ }
1265
+
1266
+ .popper-transparent {
1267
+ background: transparent !important;
1268
+ border: none !important;
1269
+ box-shadow: none !important;
1270
+ }
1271
+
1272
+ .popper-transparent .el-popper__arrow::before {
1273
+ background: transparent !important;
1127
1274
  }
1128
1275
  </style>