@handaotech-design/bom 0.0.39 → 0.0.41

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.
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import { nextTick, onBeforeUnmount, onMounted, ref, toRefs, watch } from 'vue'
2
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, toRefs, watch } from 'vue'
3
3
  import type { TreeProps } from 'ant-design-vue'
4
4
  import { Dropdown, Tree } from 'ant-design-vue'
5
5
  import { watchDebounced } from '@vueuse/shared'
@@ -8,7 +8,8 @@ import type { DataNode } from 'ant-design-vue/es/vc-tree-select/interface'
8
8
  import type { EventDataNode } from 'ant-design-vue/es/tree'
9
9
  import { MoreOutlined } from '@ant-design/icons-vue'
10
10
  import HdGrayInput from '../gray-input'
11
- import type { BomTreeConfig } from '../../models'
11
+ import type { BomNode, BomTreeConfig } from '../../models'
12
+ import { convertBomDataToTree } from '../../utils'
12
13
 
13
14
  const props = withDefaults(defineProps<Props>(), {
14
15
  maintainable: false,
@@ -29,22 +30,40 @@ interface Props {
29
30
  config: BomTreeConfig
30
31
  maintainable?: boolean
31
32
  shouldSelect?: (node: DataNode) => Promise<boolean>
33
+ keyNodeMap: Map<string, any>
34
+ firstSelectableKey?: string
35
+ loadData?: (node: BomNode, eventNode?: EventDataNode) => Promise<DataNode[]>
32
36
  }
33
37
 
38
+ const localTreeData = ref<DataNode[]>(props.treeData || [])
34
39
  const expandedKeys = ref<(string | number)[]>([])
35
40
  const searchValue = ref<string>('')
36
41
  const autoExpandParent = ref<boolean>(true)
37
- const { treeData: _treeData } = toRefs(props)
38
- const filteredTreeData = ref<TreeProps['treeData']>([])
39
42
  const treeContainerHeight = ref<number>(0)
40
43
  const treeContainerId = 'treeContainer'
44
+ const treeContainerRef = ref<HTMLElement | null>(null)
45
+ const treeRef = ref<any>(null)
41
46
  let keyToNodeMap = new Map<string, TreeNodeWithMeta>()
42
47
  const selectedKeys = ref<string[]>([])
48
+ const MAX_ROOT = 200
49
+ const fullRootKeys = computed(() => (localTreeData.value || []).map(node => node.key as string))
50
+ const visibleRootKeys = ref<Set<string>>(new Set())
51
+ const displayTreeData = computed(() => {
52
+ if (!localTreeData.value) {
53
+ return []
54
+ }
55
+ return localTreeData.value.filter(node => visibleRootKeys.value.has(node.key as string))
56
+ })
43
57
  const isPreservingTreeState = ref<boolean>(false)
58
+ const isAutoLoadingRoots = ref(false)
59
+ const searchMatches = ref<string[]>([])
60
+ const currentMatchIndex = ref(0)
44
61
  const initTreeState = () => {
45
62
  searchValue.value = ''
46
63
  selectedKeys.value = []
47
64
  expandedKeys.value = []
65
+ searchMatches.value = []
66
+ currentMatchIndex.value = 0
48
67
  }
49
68
 
50
69
  const onExpand = (keys: Key[]) => {
@@ -53,66 +72,87 @@ const onExpand = (keys: Key[]) => {
53
72
  }
54
73
 
55
74
  const updateTreeContainerHeight = () => {
56
- const treeContainer = document.getElementById(treeContainerId)
75
+ const treeContainer = treeContainerRef.value || document.getElementById(treeContainerId)
57
76
  if (treeContainer) {
58
77
  treeContainerHeight.value = treeContainer.clientHeight
59
78
  }
60
79
  }
61
80
 
62
- const buildKeyToNodeMap = (treeData: TreeProps['treeData']) => {
63
- const keyMap = new Map<string, TreeNodeWithMeta>()
64
- const traverse = (nodes: TreeProps['treeData'] = [], parentKeys: string[] = []) => {
65
- for (const node of nodes) {
66
- if (parentKeys) {
67
- keyMap.set(node.key as string, {
68
- ...node,
69
- parentKeys: parentKeys || [],
70
- } as any)
71
- }
72
- if (!_.isEmpty(node.children)) {
73
- traverse(node.children, [...parentKeys, node.key as string])
74
- }
75
- }
81
+ // 获取直接父节点的 key
82
+ const getParentKeys = (key: string): string[] | undefined => {
83
+ const treeNode = keyToNodeMap.get(key)
84
+ return treeNode?.parentKeys || []
85
+ }
86
+
87
+ const ensureRootVisibleByKey = (key?: string) => {
88
+ if (!key) {
89
+ return
76
90
  }
77
- traverse(treeData)
78
- return keyMap
91
+ const parentKeys = getParentKeys(key)
92
+ const rootKey = parentKeys?.[0] ?? key
93
+ if (!rootKey || visibleRootKeys.value.has(rootKey)) {
94
+ return
95
+ }
96
+ const next = new Set(visibleRootKeys.value)
97
+ next.add(rootKey)
98
+ visibleRootKeys.value = next
79
99
  }
80
100
 
81
- const filterTreeData = (data: TreeProps['treeData'], filteredKeys: string[]) => {
82
- const getNodes = (result: any, node: any) => {
83
- if (_.includes(filteredKeys, node.key)) {
84
- result.push({ ...node })
85
- return result
86
- }
87
- if (Array.isArray(node.children)) {
88
- const children = node.children.reduce(getNodes, [])
89
- if (!_.isEmpty(children)) {
90
- result.push({ ...node, children })
91
- }
92
- }
93
- return result
101
+ async function locateNodeByKey(targetKey: string) {
102
+ if (!targetKey) {
103
+ return
94
104
  }
95
- return (data || []).reduce(getNodes, [])
105
+ ensureRootVisibleByKey(targetKey)
106
+ const parentKeys = getParentKeys(targetKey) || []
107
+ expandedKeys.value = Array.from(new Set([
108
+ ...expandedKeys.value,
109
+ ...parentKeys,
110
+ ]))
111
+ await nextTick()
112
+ await nextTick()
113
+ treeRef.value?.scrollTo?.({
114
+ key: targetKey,
115
+ align: 'top',
116
+ })
117
+ focusNode(targetKey)
96
118
  }
97
119
 
98
- // 获取直接父节点的 key
99
- const getParentKeys = (key: string): string[] | undefined => {
100
- const treeNode = keyToNodeMap.get(key)
101
- return treeNode?.parentKeys || []
120
+ const scrollToMatch = async (index: number) => {
121
+ if (!searchMatches.value.length) {
122
+ return
123
+ }
124
+ const total = searchMatches.value.length
125
+ const normalized = ((index % total) + total) % total
126
+ currentMatchIndex.value = normalized
127
+ const targetKey = searchMatches.value[normalized]
128
+ await locateNodeByKey(targetKey)
102
129
  }
103
130
 
104
- const selectFirstSelectableNode = (preserveExpanded = false) => {
105
- const firstNode: any = _.find(Array.from(keyToNodeMap.values()), node => (node as any)?.selectable)
106
- if (!firstNode) {
131
+ const focusPreviousMatch = () => {
132
+ if (!searchMatches.value.length) {
133
+ return
134
+ }
135
+ scrollToMatch(currentMatchIndex.value - 1)
136
+ }
137
+
138
+ const focusNextMatch = () => {
139
+ if (!searchMatches.value.length) {
107
140
  return
108
141
  }
142
+ scrollToMatch(currentMatchIndex.value + 1)
143
+ }
109
144
 
145
+ const selectFirstSelectableNode = (preserveExpanded = false) => {
146
+ if (!props.firstSelectableKey) {
147
+ return
148
+ }
110
149
  nextTick(() => {
111
- const parentKeys = getParentKeys(firstNode.key as string) as string[]
150
+ ensureRootVisibleByKey(props.firstSelectableKey)
151
+ const parentKeys = getParentKeys(props.firstSelectableKey!) as string[]
112
152
  expandedKeys.value = preserveExpanded
113
153
  ? Array.from(new Set([...expandedKeys.value, ...parentKeys])) // 取并集
114
154
  : parentKeys
115
- selectedKeys.value = [firstNode.key]
155
+ selectedKeys.value = [props.firstSelectableKey!]
116
156
  })
117
157
  }
118
158
 
@@ -123,6 +163,7 @@ const onSelected = async (keys: Key[], { node }: { node: EventDataNode }) => {
123
163
  }
124
164
  const key = node?.dataRef?.key
125
165
  if (key) {
166
+ ensureRootVisibleByKey(key as string)
126
167
  selectedKeys.value = [key as string]
127
168
  }
128
169
  else {
@@ -142,17 +183,18 @@ onBeforeUnmount(() => {
142
183
  })
143
184
 
144
185
  watch(
145
- () => _treeData.value,
186
+ () => localTreeData.value,
146
187
  async () => {
147
- filteredTreeData.value = _treeData.value
148
- keyToNodeMap = buildKeyToNodeMap(_treeData.value ?? [])
188
+ keyToNodeMap = props.keyNodeMap
189
+ const visibleSet = new Set<string>()
190
+ ;(localTreeData.value || []).slice(0, MAX_ROOT).forEach((node) => {
191
+ visibleSet.add(node.key as string)
192
+ })
193
+ visibleRootKeys.value = visibleSet
149
194
  initTreeState()
150
195
  if (!isPreservingTreeState.value) {
151
196
  selectFirstSelectableNode(true)
152
197
  }
153
- await nextTick(() => {
154
- updateTreeContainerHeight()
155
- })
156
198
  },
157
199
  {
158
200
  immediate: true,
@@ -160,30 +202,34 @@ watch(
160
202
  )
161
203
 
162
204
  watchDebounced(
163
- searchValue, (value: string) => {
164
- let expanded: any[]
205
+ searchValue, async (value: string) => {
206
+ let expanded: any[] = []
165
207
  if (_.isEmpty(value)) {
208
+ searchMatches.value = []
209
+ currentMatchIndex.value = 0
166
210
  expanded = getParentKeys(selectedKeys.value[0]) || []
167
- filteredTreeData.value = _treeData.value
168
211
  }
169
212
  else {
170
- const filteredNodes: TreeProps['treeData'] = []
171
- expanded = (Array.from(keyToNodeMap.values()))
172
- .map((item: any) => {
173
- if (hasSearchMatch(item.title)) {
174
- filteredNodes.push(item)
175
- return getParentKeys(item.key)
176
- }
177
- return null
178
- })
179
- .filter((item, i, self) => item && self.indexOf(item) === i)
180
- const filteredKeys = filteredNodes.map(item => item.key)
181
- filteredTreeData.value = _.isEmpty(filteredKeys) ? [] : filterTreeData(_treeData.value, filteredKeys as unknown as string[])
213
+ const matchedParents: string[][] = []
214
+ const matches: string[] = []
215
+ for (const node of keyToNodeMap.values()) {
216
+ if (hasSearchMatch((node as any).title)) {
217
+ const key = node.key as string
218
+ matches.push(key)
219
+ ensureRootVisibleByKey(key)
220
+ matchedParents.push(getParentKeys(key) || [])
221
+ }
222
+ }
223
+ searchMatches.value = matches
224
+ currentMatchIndex.value = matches.length ? 0 : 0
225
+ expanded = matchedParents.flat()
182
226
  }
183
227
  expandedKeys.value = expanded
184
- nextTick(() => {
185
- expandedKeys.value = _.uniq(expanded.flat(1)) as any as string[]
186
- })
228
+ await nextTick()
229
+ expandedKeys.value = _.uniq(expanded.flat(1)) as any as string[]
230
+ if (!_.isEmpty(value) && searchMatches.value.length) {
231
+ await scrollToMatch(currentMatchIndex.value)
232
+ }
187
233
  }, { debounce: 500 })
188
234
 
189
235
  watch(
@@ -222,6 +268,7 @@ async function updateTreeWithPreservedState(updateDataFn: () => Promise<void>) {
222
268
  expandedKeys.value = prevExpanded.filter(key => keyToNodeMap.has(key as string))
223
269
 
224
270
  if (prevSelected && keyToNodeMap.has(prevSelected)) {
271
+ ensureRootVisibleByKey(prevSelected)
225
272
  selectedKeys.value = [prevSelected]
226
273
  }
227
274
  else {
@@ -244,35 +291,159 @@ defineExpose({
244
291
  getParentKeys,
245
292
  getDepth,
246
293
  })
294
+
295
+ const canLoadMoreRoot = computed(() => visibleRootKeys.value.size < fullRootKeys.value.length)
296
+ const loadMoreRoot = () => {
297
+ if (!fullRootKeys.value.length) {
298
+ return
299
+ }
300
+ const currentSize = visibleRootKeys.value.size
301
+ if (currentSize >= fullRootKeys.value.length) {
302
+ return
303
+ }
304
+ const next = new Set(visibleRootKeys.value)
305
+ fullRootKeys.value.slice(currentSize, currentSize + MAX_ROOT).forEach((key) => {
306
+ next.add(key)
307
+ })
308
+ visibleRootKeys.value = next
309
+ }
310
+
311
+ const maybeAutoLoadMoreRoot = () => {
312
+ if (isAutoLoadingRoots.value || !canLoadMoreRoot.value) {
313
+ return
314
+ }
315
+ const container = treeContainerRef.value
316
+ if (!container) {
317
+ return
318
+ }
319
+ const nearBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 80
320
+ if (!nearBottom) {
321
+ return
322
+ }
323
+ isAutoLoadingRoots.value = true
324
+ loadMoreRoot()
325
+ requestAnimationFrame(() => {
326
+ isAutoLoadingRoots.value = false
327
+ })
328
+ }
329
+
330
+ const handleTreeScroll = _.throttle(maybeAutoLoadMoreRoot, 300)
331
+
332
+ async function _loadData(node: EventDataNode) {
333
+ if (!props.loadData) {
334
+ return
335
+ }
336
+ if (node.children && node.children.length) {
337
+ return
338
+ }
339
+
340
+ const children: DataNode[] = await props.loadData(node.dataRef as BomNode, node)
341
+
342
+ if (children.length) {
343
+ children.forEach(item => item.key = node.key + (item.key ?? item.id) as string)
344
+ const { treeData, nodeMap } = convertBomDataToTree(children as any, props.config.nodeConfig.rule) as any
345
+ nodeMap.entries().forEach(([key, value]: any[]) => {
346
+ keyToNodeMap.set(key as string, {
347
+ ...value,
348
+ parentKeys: [...(getParentKeys(node.key as string) || []), node.key as string],
349
+ })
350
+ })
351
+
352
+ node.dataRef!.children = treeData
353
+ }
354
+ else {
355
+ node.isLeaf = true
356
+ }
357
+ }
358
+
359
+ function focusNode(key: string) {
360
+ // 1. 用 antd Tree 自带滚动
361
+ treeRef.value?.scrollTo({ key, align: 'center' })
362
+
363
+ // 2. 下一帧再找 DOM,避免滚动未完成
364
+ requestAnimationFrame(() => {
365
+ const el = document.querySelector(
366
+ `.tree-node-tittle[data-key="${key}"]`,
367
+ ) as HTMLElement | null
368
+
369
+ if (!el) {
370
+ return
371
+ }
372
+
373
+ // 3. 触发一次可感知的焦点动画
374
+ el.classList.remove('focus-flash')
375
+ void el.offsetWidth
376
+ el.classList.add('focus-flash')
377
+
378
+ setTimeout(() => {
379
+ el.classList.remove('focus-flash')
380
+ }, 2400)
381
+ })
382
+ }
247
383
  </script>
248
384
 
249
385
  <template>
250
386
  <div class="flex flex-col h-100%">
251
387
  <div class="search-box">
252
388
  <HdGrayInput
253
- v-model:value="searchValue"
389
+ v-model:value.trim="searchValue"
254
390
  :placeholder="props.config?.filter?.placeholder || '输入关键词进行筛选(如名称/编号)'"
255
391
  class="search-input"
256
392
  >
257
393
  <template #prefix>
258
394
  <div class="i-icon-search w-16px h-16px" />
259
395
  </template>
396
+ <template #suffix>
397
+ <div v-if="searchValue" class="match-hint mr-8px font-size-12px">
398
+ {{ searchMatches.length ? currentMatchIndex + 1 : 0 }} / {{ searchMatches.length }}
399
+ </div>
400
+ <div v-if="searchValue" class="match-nav" :class="{ 'is-disabled': !searchMatches.length }">
401
+ <a-button
402
+ class="match-nav-btn mr-4px"
403
+ shape="circle"
404
+ size="small"
405
+ :disabled="!searchMatches.length"
406
+ @click.stop.prevent="focusPreviousMatch"
407
+ >
408
+
409
+ </a-button>
410
+ <a-button
411
+ class="match-nav-btn"
412
+ shape="circle"
413
+ size="small"
414
+ :disabled="!searchMatches.length"
415
+ @click.stop.prevent="focusNextMatch"
416
+ >
417
+
418
+ </a-button>
419
+ </div>
420
+ </template>
260
421
  </HdGrayInput>
261
422
  </div>
262
- <div :id="treeContainerId" class="flex-grow tree-wrapper mt-12px overflow-y-hidden">
263
- <div v-if="!_.isEmpty(filteredTreeData)">
423
+ <div
424
+ :id="treeContainerId"
425
+ ref="treeContainerRef"
426
+ class="flex-grow tree-wrapper mt-12px"
427
+ >
428
+ <div v-if="!_.isEmpty(displayTreeData)">
264
429
  <a-tree
430
+ ref="treeRef"
265
431
  :height="treeContainerHeight"
432
+ :tree-data="displayTreeData"
266
433
  :expanded-keys="expandedKeys"
267
434
  :auto-expand-parent="autoExpandParent"
268
- :tree-data="filteredTreeData"
269
435
  :block-node="true"
270
436
  :selected-keys="selectedKeys"
437
+ :load-data="_loadData"
438
+ @scroll="handleTreeScroll"
271
439
  @expand="onExpand"
272
440
  @select="onSelected"
273
441
  >
274
442
  <template #title="{ title, dataRef }">
275
- <div :class="`tree-node-tittle flex items-center ${dataRef.selectable ? 'selectable' : 'not-selectable'}`">
443
+ <div
444
+ :class="`tree-node-tittle flex items-center ${dataRef.selectable ? 'selectable' : 'not-selectable'}`"
445
+ :data-key="dataRef.key"
446
+ >
276
447
  <div
277
448
  v-if="!!dataRef.icon"
278
449
  class="icon w-20px h-20px mr-4px min-w-20px"
@@ -308,7 +479,7 @@ defineExpose({
308
479
  </template>
309
480
  </a-tree>
310
481
  </div>
311
- <div v-show="_.isEmpty(filteredTreeData)" class="w-100% h-100% flex justify-center items-center">
482
+ <div v-show="_.isEmpty(displayTreeData)" class="w-100% h-100% flex justify-center items-center">
312
483
  <span>暂无数据</span>
313
484
  </div>
314
485
  </div>
@@ -316,14 +487,22 @@ defineExpose({
316
487
  </template>
317
488
 
318
489
  <style scoped>
490
+ @charset "UTF-8";
319
491
  .tree-wrapper {
320
492
  background-color: #fff;
493
+ min-height: 0;
321
494
  }
322
495
 
323
496
  .search-box {
324
497
  padding: 0 16px;
325
498
  }
326
499
 
500
+ :deep(.match-nav) .ant-btn:hover, :deep(.match-nav) .ant-btn:focus {
501
+ color: #1E3B9D;
502
+ border-color: #1E3B9D;
503
+ background: #fff;
504
+ }
505
+
327
506
  :deep(.ant-tree-list) .ant-tree-treenode-motion {
328
507
  width: 100%;
329
508
  }
@@ -417,4 +596,34 @@ defineExpose({
417
596
  white-space: nowrap;
418
597
  vertical-align: middle;
419
598
  }
599
+
600
+ .tree-node-tittle {
601
+ position: relative;
602
+ }
603
+
604
+ /* 焦点提示:整行描边框 */
605
+ .tree-node-tittle.focus-flash::after {
606
+ content: "";
607
+ position: absolute;
608
+ inset: -4px -8px;
609
+ border-radius: 8px;
610
+ pointer-events: none;
611
+ box-shadow: 0 0 0 1px rgba(30, 59, 157, 0.25), 0 0 8px rgba(30, 59, 157, 0.25);
612
+ animation: focusSoftGlow 1.6s ease-out 2;
613
+ }
614
+
615
+ @keyframes focusSoftGlow {
616
+ 0% {
617
+ opacity: 0;
618
+ transform: scale(0.98);
619
+ }
620
+ 30% {
621
+ opacity: 1;
622
+ transform: scale(1);
623
+ }
624
+ 100% {
625
+ opacity: 0;
626
+ transform: scale(1.01);
627
+ }
628
+ }
420
629
  </style>
@@ -0,0 +1,3 @@
1
+ export * from './index.vue';
2
+ export declare const HdBomTree: any;
3
+ export default HdBomTree;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ var _exportNames = {
7
+ HdBomTree: true
8
+ };
9
+ module.exports = exports.HdBomTree = void 0;
10
+ var _antDesignVue = require("ant-design-vue");
11
+ var _iconsVue = require("@ant-design/icons-vue");
12
+ var _install = require("../../utils/install");
13
+ var _index = _interopRequireWildcard(require("./index.vue"));
14
+ Object.keys(_index).forEach(function (key) {
15
+ if (key === "default" || key === "__esModule") return;
16
+ if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
17
+ if (key in exports && exports[key] === _index[key]) return;
18
+ Object.defineProperty(exports, key, {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _index[key];
22
+ }
23
+ });
24
+ });
25
+ function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
26
+ function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
27
+ const antdComponents = Object.fromEntries([_antDesignVue.Input, _antDesignVue.Tree, _antDesignVue.Dropdown, _iconsVue.MoreOutlined].map(comp => [comp.name, comp]));
28
+ const HdBomTree = exports.HdBomTree = (0, _install.withInstall)(_index.default, antdComponents);
29
+ module.exports = HdBomTree;