@handaotech-design/bom 0.0.49 → 0.0.50

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,6 +1,5 @@
1
1
  <script lang="ts" setup>
2
2
  import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
3
- import type { TreeProps } from 'ant-design-vue'
4
3
  import { Dropdown, Tree } from 'ant-design-vue'
5
4
  import { watchDebounced } from '@vueuse/shared'
6
5
  import * as _ from 'lodash-es'
@@ -26,45 +25,62 @@ type TreeNodeWithMeta = DataNode & {
26
25
  }
27
26
 
28
27
  interface Props {
29
- treeData: TreeProps['treeData']
28
+ bomData: BomNode[]
30
29
  config: BomTreeConfig
31
30
  maintainable?: boolean
32
31
  shouldSelect?: (node: DataNode) => Promise<boolean>
33
- keyNodeMap: Map<string, any>
34
- firstSelectableKey?: string
35
32
  loadData?: (node: BomNode, eventNode?: EventDataNode) => Promise<DataNode[]>
36
33
  }
37
34
 
38
- const localTreeData = ref<DataNode[]>(props.treeData || [])
35
+ const treeData = ref<DataNode[]>([])
36
+ const firstSelectableKey = ref<string>()
37
+ const keyToNodeMap = ref<Map<string, TreeNodeWithMeta>>(new Map())
39
38
  const expandedKeys = ref<(string | number)[]>([])
40
39
  const searchValue = ref<string>('')
41
40
  const autoExpandParent = ref<boolean>(true)
42
41
  const treeContainerHeight = ref<number>(0)
43
- const treeContainerId = 'treeContainer'
42
+ const treeContainerId = 'bomCustomTreeContainer'
44
43
  const treeContainerRef = ref<HTMLElement | null>(null)
45
44
  const treeRef = ref<any>(null)
46
- let keyToNodeMap = new Map<string, TreeNodeWithMeta>()
47
45
  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())
46
+
47
+ const ROOT_BATCH_SIZE = 200
48
+ const currentRootSize = ref<number>(ROOT_BATCH_SIZE)
49
+ const filteredData = ref<DataNode[]>([])
51
50
  const displayTreeData = computed(() => {
52
- if (!localTreeData.value) {
53
- return []
54
- }
55
- return localTreeData.value.filter(node => visibleRootKeys.value.has(node.key as string))
51
+ return (filteredData.value ?? []).slice(0, currentRootSize.value)
56
52
  })
53
+
57
54
  const isPreservingTreeState = ref<boolean>(false)
58
- const isAutoLoadingRoots = ref(false)
59
- let isProgrammaticScroll = false
60
- const searchMatches = ref<string[]>([])
61
- const currentMatchIndex = ref(0)
62
55
  const initTreeState = () => {
63
56
  searchValue.value = ''
64
57
  selectedKeys.value = []
65
58
  expandedKeys.value = []
66
- searchMatches.value = []
67
- currentMatchIndex.value = 0
59
+ }
60
+
61
+ const filterTreeData = (data: DataNode[], filteredKeys: string[]) => {
62
+ const getNodes = (result: any, node: any) => {
63
+ if (_.includes(filteredKeys, node.key)) {
64
+ result.push({ ...node })
65
+ return result
66
+ }
67
+ if (Array.isArray(node.children)) {
68
+ const children = node.children.reduce(getNodes, [])
69
+ if (!_.isEmpty(children)) {
70
+ result.push({ ...node, children })
71
+ }
72
+ }
73
+ return result
74
+ }
75
+ return (data || []).reduce(getNodes, [])
76
+ }
77
+
78
+ const resetSearchStatus = () => {
79
+ currentRootSize.value = ROOT_BATCH_SIZE
80
+ }
81
+
82
+ const resetFilterData = () => {
83
+ filteredData.value = treeData.value ?? []
68
84
  }
69
85
 
70
86
  const onExpand = (keys: Key[]) => {
@@ -81,89 +97,44 @@ const updateTreeContainerHeight = () => {
81
97
 
82
98
  // 获取直接父节点的 key
83
99
  const getParentKeys = (key: string): string[] | undefined => {
84
- const treeNode = keyToNodeMap.get(key)
100
+ const treeNode = keyToNodeMap.value.get(key)
85
101
  return treeNode?.parentKeys || []
86
102
  }
87
103
 
88
- const ensureRootVisibleByKey = (key?: string) => {
89
- if (!key) {
90
- return
91
- }
92
- const parentKeys = getParentKeys(key)
93
- const rootKey = parentKeys?.[0] ?? key
94
- if (!rootKey || visibleRootKeys.value.has(rootKey)) {
95
- return
96
- }
97
- const next = new Set(visibleRootKeys.value)
98
- next.add(rootKey)
99
- visibleRootKeys.value = next
100
- }
101
-
102
- async function locateNodeByKey(targetKey: string) {
103
- if (!targetKey) {
104
+ const selectFirstSelectableNode = (preserveExpanded = false) => {
105
+ if (!firstSelectableKey.value) {
104
106
  return
105
107
  }
106
- isProgrammaticScroll = true
107
- ensureRootVisibleByKey(targetKey)
108
- const parentKeys = getParentKeys(targetKey) || []
109
- expandedKeys.value = Array.from(new Set([
110
- ...expandedKeys.value,
111
- ...parentKeys,
112
- ]))
113
- await nextTick()
114
- await nextTick()
115
- treeRef.value?.scrollTo?.({
116
- key: targetKey,
117
- align: 'top',
118
- })
119
- setTimeout(() => {
120
- focusNode(targetKey)
121
- }, 0) // 延时足够滚动完成
122
- requestAnimationFrame(() => {
123
- setTimeout(() => {
124
- isProgrammaticScroll = false
125
- }, 500) // 延时足够滚动完成
108
+ nextTick(() => {
109
+ const parentKeys = getParentKeys(firstSelectableKey.value!) as string[]
110
+ expandedKeys.value = preserveExpanded
111
+ ? Array.from(new Set([...expandedKeys.value, ...parentKeys])) // 取并集
112
+ : parentKeys
113
+ selectedKeys.value = [firstSelectableKey.value!]
126
114
  })
127
115
  }
128
116
 
129
- const scrollToMatch = async (index: number) => {
130
- if (!searchMatches.value.length) {
117
+ const canLoadMoreRoot = computed(() => currentRootSize.value < filteredData.value.length)
118
+ const loadMoreRoot = () => {
119
+ if (!canLoadMoreRoot.value) {
131
120
  return
132
121
  }
133
- const total = searchMatches.value.length
134
- const normalized = ((index % total) + total) % total
135
- currentMatchIndex.value = normalized
136
- const targetKey = searchMatches.value[normalized]
137
- await locateNodeByKey(targetKey)
122
+ currentRootSize.value = currentRootSize.value + ROOT_BATCH_SIZE
138
123
  }
139
124
 
140
- const focusPreviousMatch = () => {
141
- if (!searchMatches.value.length) {
125
+ const maybeAutoLoadMoreRoot = () => {
126
+ const container = treeContainerRef.value
127
+ if (!container) {
142
128
  return
143
129
  }
144
- scrollToMatch(currentMatchIndex.value - 1)
145
- }
146
-
147
- const focusNextMatch = () => {
148
- if (!searchMatches.value.length) {
130
+ const nearBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 80
131
+ if (!nearBottom) {
149
132
  return
150
133
  }
151
- scrollToMatch(currentMatchIndex.value + 1)
134
+ loadMoreRoot()
152
135
  }
153
136
 
154
- const selectFirstSelectableNode = (preserveExpanded = false) => {
155
- if (!props.firstSelectableKey) {
156
- return
157
- }
158
- nextTick(() => {
159
- ensureRootVisibleByKey(props.firstSelectableKey)
160
- const parentKeys = getParentKeys(props.firstSelectableKey!) as string[]
161
- expandedKeys.value = preserveExpanded
162
- ? Array.from(new Set([...expandedKeys.value, ...parentKeys])) // 取并集
163
- : parentKeys
164
- selectedKeys.value = [props.firstSelectableKey!]
165
- })
166
- }
137
+ const handleTreeScroll: () => void = _.throttle(maybeAutoLoadMoreRoot, 300)
167
138
 
168
139
  type Key = string | number
169
140
  const onSelected = async (keys: Key[], { node }: { node: EventDataNode }) => {
@@ -172,7 +143,6 @@ const onSelected = async (keys: Key[], { node }: { node: EventDataNode }) => {
172
143
  }
173
144
  const key = node?.dataRef?.key
174
145
  if (key) {
175
- ensureRootVisibleByKey(key as string)
176
146
  selectedKeys.value = [key as string]
177
147
  }
178
148
  else {
@@ -191,16 +161,31 @@ onBeforeUnmount(() => {
191
161
  window.removeEventListener('resize', updateTreeContainerHeight)
192
162
  })
193
163
 
164
+ const scrollTo = async (targetKey: Key | undefined) => {
165
+ if (!targetKey) {
166
+ return
167
+ }
168
+ await nextTick()
169
+ treeRef.value?.scrollTo?.({
170
+ key: targetKey,
171
+ align: 'top',
172
+ })
173
+ }
174
+
194
175
  watch(
195
- () => localTreeData.value,
176
+ () => props.bomData,
196
177
  async () => {
197
- keyToNodeMap = props.keyNodeMap
198
- const visibleSet = new Set<string>()
199
- ;(localTreeData.value || []).slice(0, MAX_ROOT).forEach((node) => {
200
- visibleSet.add(node.key as string)
201
- })
202
- visibleRootKeys.value = visibleSet
178
+ const {
179
+ treeData: _treeDta,
180
+ nodeMap,
181
+ firstSelectableNodeKey: _firstSelectableNodeKey,
182
+ } = convertBomDataToTree(props.bomData!, props.config.nodeConfig.rule)
183
+ treeData.value = _treeDta
184
+ keyToNodeMap.value = nodeMap
185
+ firstSelectableKey.value = _firstSelectableNodeKey
203
186
  initTreeState()
187
+ resetFilterData()
188
+ resetSearchStatus()
204
189
  if (!isPreservingTreeState.value) {
205
190
  selectFirstSelectableNode(true)
206
191
  }
@@ -213,39 +198,36 @@ watch(
213
198
  watchDebounced(
214
199
  searchValue, async (value: string) => {
215
200
  let expanded: any[] = []
201
+ const matchedKeys: string[] = []
216
202
  if (_.isEmpty(value)) {
217
- searchMatches.value = []
218
- currentMatchIndex.value = 0
219
203
  expanded = getParentKeys(selectedKeys.value[0]) || []
204
+ resetFilterData()
205
+ resetSearchStatus()
206
+ await scrollTo(treeData.value?.[0]?.key)
220
207
  }
221
208
  else {
222
209
  const matchedParents: string[][] = []
223
- const matches: string[] = []
224
- for (const node of keyToNodeMap.values()) {
210
+ for (const node of keyToNodeMap.value.values()) {
225
211
  if (hasSearchMatch((node as any).title)) {
226
212
  const key = node.key as string
227
- matches.push(key)
228
- ensureRootVisibleByKey(key)
213
+ matchedKeys.push(key)
229
214
  matchedParents.push(getParentKeys(key) || [])
230
215
  }
231
216
  }
232
- searchMatches.value = matches
233
- currentMatchIndex.value = matches.length ? 0 : 0
234
217
  expanded = matchedParents.flat()
218
+ filteredData.value = _.isEmpty(matchedKeys) ? [] : filterTreeData(treeData.value as any, matchedKeys)
219
+ resetSearchStatus()
220
+ await scrollTo(filteredData.value?.[0]?.key)
235
221
  }
236
- expandedKeys.value = expanded
237
222
  await nextTick()
238
223
  expandedKeys.value = _.uniq(expanded.flat(1)) as any as string[]
239
- if (!_.isEmpty(value) && searchMatches.value.length) {
240
- await scrollToMatch(currentMatchIndex.value)
241
- }
242
224
  }, { debounce: 500 })
243
225
 
244
226
  watch(
245
227
  () => selectedKeys.value,
246
228
  () => {
247
229
  if (selectedKeys.value && selectedKeys.value.length) {
248
- return emit('select', keyToNodeMap.get(selectedKeys.value[0]))
230
+ return emit('select', keyToNodeMap.value.get(selectedKeys.value[0]))
249
231
  }
250
232
  },
251
233
  )
@@ -274,10 +256,9 @@ async function updateTreeWithPreservedState(updateDataFn: () => Promise<void>) {
274
256
  isPreservingTreeState.value = true
275
257
  await updateDataFn()
276
258
  await nextTick(() => {
277
- expandedKeys.value = prevExpanded.filter(key => keyToNodeMap.has(key as string))
259
+ expandedKeys.value = prevExpanded.filter(key => keyToNodeMap.value.has(key as string))
278
260
 
279
- if (prevSelected && keyToNodeMap.has(prevSelected)) {
280
- ensureRootVisibleByKey(prevSelected)
261
+ if (prevSelected && keyToNodeMap.value.has(prevSelected)) {
281
262
  selectedKeys.value = [prevSelected]
282
263
  }
283
264
  else {
@@ -301,43 +282,6 @@ defineExpose({
301
282
  getDepth,
302
283
  })
303
284
 
304
- const canLoadMoreRoot = computed(() => visibleRootKeys.value.size < fullRootKeys.value.length)
305
- const loadMoreRoot = () => {
306
- if (!fullRootKeys.value.length) {
307
- return
308
- }
309
- const currentSize = visibleRootKeys.value.size
310
- if (currentSize >= fullRootKeys.value.length) {
311
- return
312
- }
313
- const next = new Set(visibleRootKeys.value)
314
- fullRootKeys.value.slice(currentSize, currentSize + MAX_ROOT).forEach((key) => {
315
- next.add(key)
316
- })
317
- visibleRootKeys.value = next
318
- }
319
-
320
- const maybeAutoLoadMoreRoot = () => {
321
- if (isAutoLoadingRoots.value || !canLoadMoreRoot.value || isProgrammaticScroll) {
322
- return
323
- }
324
- const container = treeContainerRef.value
325
- if (!container) {
326
- return
327
- }
328
- const nearBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 80
329
- if (!nearBottom) {
330
- return
331
- }
332
- isAutoLoadingRoots.value = true
333
- loadMoreRoot()
334
- requestAnimationFrame(() => {
335
- isAutoLoadingRoots.value = false
336
- })
337
- }
338
-
339
- const handleTreeScroll = _.throttle(maybeAutoLoadMoreRoot, 300)
340
-
341
285
  async function _loadData(node: EventDataNode) {
342
286
  if (!props.loadData) {
343
287
  return
@@ -352,7 +296,7 @@ async function _loadData(node: EventDataNode) {
352
296
  children.forEach(item => item.key = node.key + (item.key ?? item.id) as string)
353
297
  const { treeData, nodeMap } = convertBomDataToTree(children as any, props.config.nodeConfig.rule) as any
354
298
  nodeMap.entries().forEach(([key, value]: any[]) => {
355
- keyToNodeMap.set(key as string, {
299
+ keyToNodeMap.value.set(key as string, {
356
300
  ...value,
357
301
  parentKeys: [...(getParentKeys(node.key as string) || []), node.key as string],
358
302
  })
@@ -364,36 +308,6 @@ async function _loadData(node: EventDataNode) {
364
308
  node.isLeaf = true
365
309
  }
366
310
  }
367
-
368
- function focusNode(key: string) {
369
- // 先清理页面上已有的焦点动画
370
- document.querySelectorAll<HTMLElement>('.tree-node-tittle.focus-flash').forEach((el) => {
371
- el.classList.remove('focus-flash')
372
- })
373
- // 1. 用 antd Tree 自带滚动
374
- treeRef.value?.scrollTo({ key, align: 'center' })
375
-
376
- // 2. 下一帧再找 DOM,避免滚动未完成
377
- requestAnimationFrame(() => {
378
- const el = document.querySelector(
379
- `.tree-node-tittle[data-key="${key}"]`,
380
- ) as HTMLElement | null
381
-
382
- if (!el) {
383
- return
384
- }
385
-
386
- // 3. 触发一次可感知的焦点动画
387
- el.classList.remove('focus-flash')
388
- // eslint-disable-next-line no-void
389
- void el.offsetWidth
390
- el.classList.add('focus-flash')
391
-
392
- setTimeout(() => {
393
- el.classList.remove('focus-flash')
394
- }, 2400)
395
- })
396
- }
397
311
  </script>
398
312
 
399
313
  <template>
@@ -407,27 +321,6 @@ function focusNode(key: string) {
407
321
  <template #prefix>
408
322
  <div class="i-icon-search w-16px h-16px" />
409
323
  </template>
410
- <template #suffix>
411
- <div v-if="searchValue" class="match-hint mr-8px font-size-12px">
412
- {{ searchMatches.length ? currentMatchIndex + 1 : 0 }} / {{ searchMatches.length }}
413
- </div>
414
- <div v-if="searchValue" class="match-nav" :class="{ 'is-disabled': !searchMatches.length }">
415
- <div
416
- class="match-nav-btn mr-4px"
417
- :class="{ disabled: !searchMatches.length }"
418
- @click.stop.prevent="focusPreviousMatch"
419
- >
420
-
421
- </div>
422
- <div
423
- class="match-nav-btn"
424
- :class="{ disabled: !searchMatches.length }"
425
- @click.stop.prevent="focusNextMatch"
426
- >
427
-
428
- </div>
429
- </div>
430
- </template>
431
324
  </HdGrayInput>
432
325
  </div>
433
326
  <div
@@ -4,7 +4,6 @@ import * as _ from 'lodash-es'
4
4
  import type { DataNode } from 'ant-design-vue/es/vc-tree-select/interface'
5
5
  import type { EventDataNode } from 'ant-design-vue/es/tree'
6
6
  import type { BomNode, BomTreeConfig, Optional, WorkBenchLayoutConfig } from '../../models'
7
- import { convertBomDataToTree } from '../../utils'
8
7
  import HdLeftRight from '../left-right/index'
9
8
  import HdBomTree from '../bom-tree/index'
10
9
 
@@ -24,9 +23,6 @@ interface Props {
24
23
  treeShouldSelect?: (node: DataNode) => Promise<boolean>
25
24
  loadData?: (node: BomNode, eventNode?: EventDataNode) => Promise<BomNode[]>
26
25
  }
27
- const bomDataForTree = ref<BomNode[]>()
28
- const firstSelectableKey = ref<string | undefined>()
29
- const keyNodeMap = ref<Map<string, BomNode>>(new Map())
30
26
  const selectedNode = ref<BomNode>()
31
27
 
32
28
  async function onSelected(data: BomNode) {
@@ -38,13 +34,6 @@ watch(
38
34
  () => {
39
35
  if (_.isEmpty(props.bomData)) {
40
36
  selectedNode.value = undefined
41
- bomDataForTree.value = undefined
42
- }
43
- else {
44
- const { treeData, nodeMap, firstSelectableNodeKey } = convertBomDataToTree(props.bomData!, props.treeConfig.nodeConfig.rule)
45
- bomDataForTree.value = treeData
46
- keyNodeMap.value = nodeMap
47
- firstSelectableKey.value = firstSelectableNodeKey
48
37
  }
49
38
  },
50
39
  { immediate: true },
@@ -57,7 +46,7 @@ defineExpose({
57
46
 
58
47
  <template>
59
48
  <HdLeftRight
60
- v-if="bomDataForTree"
49
+ v-if="props.bomData"
61
50
  :module-key="props.moduleKey"
62
51
  :max-left-width="props?.layoutConfig?.maxLeftWidth"
63
52
  >
@@ -67,12 +56,10 @@ defineExpose({
67
56
  <div class="bom-tree-container">
68
57
  <HdBomTree
69
58
  ref="treeRef"
70
- :tree-data="bomDataForTree"
59
+ :bom-data="props.bomData ?? []"
71
60
  :config="props.treeConfig"
72
61
  :maintainable="props.maintainable"
73
62
  :should-select="props.treeShouldSelect"
74
- :first-selectable-key="firstSelectableKey"
75
- :key-node-map="keyNodeMap"
76
63
  :load-data="props.loadData"
77
64
  @select="(data: BomNode) => onSelected(data)"
78
65
  >
@@ -3,7 +3,7 @@ import type { BomNode, BomTreeConfig } from '../models';
3
3
  export declare class BomSDK {
4
4
  httpClient: HttpClient;
5
5
  constructor(client: HttpClient);
6
- lazyLoadAll(): Promise<unknown>;
6
+ lazyLoadAllPpboms(): Promise<unknown>;
7
7
  getBusinessConfig(configCode: string): Promise<unknown>;
8
8
  getBusinessId(bomNodeId: string): Promise<unknown>;
9
9
  loadChildren(bomNode: BomNode, treeConfig: BomTreeConfig): Promise<BomNode[]>;
@@ -5,7 +5,7 @@ export class BomSDK {
5
5
  constructor(client) {
6
6
  this.httpClient = client;
7
7
  }
8
- async lazyLoadAll() {
8
+ async lazyLoadAllPpboms() {
9
9
  return this.httpClient.post("/ppboms/query/lazy-all", {
10
10
  page: 1,
11
11
  pageSize: Number.MAX_SAFE_INTEGER