@veltra/desktop 1.2.4 → 1.2.5

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.
Files changed (135) hide show
  1. package/dist/components/action/action-group.vue.d.ts +2 -2
  2. package/dist/components/action/action.vue.d.ts +2 -2
  3. package/dist/components/auto-complete/auto-complete.js +25 -13
  4. package/dist/components/auto-complete/auto-complete.js.map +1 -1
  5. package/dist/components/auto-complete/auto-complete.vue.d.ts +3 -2
  6. package/dist/components/batch-edit/batch-edit-form.js +33 -26
  7. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  8. package/dist/components/batch-edit/batch-edit-list.js +4 -1
  9. package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
  10. package/dist/components/batch-edit/batch-edit.js +29 -50
  11. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  12. package/dist/components/batch-edit/batch-edit.vue.d.ts +3 -3
  13. package/dist/components/batch-edit/di.js.map +1 -1
  14. package/dist/components/batch-edit/style2.css +3 -0
  15. package/dist/components/batch-edit/use-edit.js +39 -71
  16. package/dist/components/batch-edit/use-edit.js.map +1 -1
  17. package/dist/components/batch-edit/use-features.js +38 -0
  18. package/dist/components/batch-edit/use-features.js.map +1 -0
  19. package/dist/components/cascade/cascade.vue.d.ts +1 -1
  20. package/dist/components/checkbox/checkbox-button.vue.d.ts +1 -1
  21. package/dist/components/contextmenu/style2.css +7 -7
  22. package/dist/components/date-panel/date-panel.vue.d.ts +1 -1
  23. package/dist/components/date-picker/date-picker.vue.d.ts +1 -1
  24. package/dist/components/date-range-picker/date-range-picker.vue.d.ts +1 -1
  25. package/dist/components/dual-nav/dual-nav-app.js +49 -0
  26. package/dist/components/dual-nav/dual-nav-app.js.map +1 -0
  27. package/dist/components/dual-nav/dual-nav.js +125 -0
  28. package/dist/components/dual-nav/dual-nav.js.map +1 -0
  29. package/dist/components/dual-nav/dual-nav.vue.d.ts +13 -0
  30. package/dist/components/dual-nav/helper.js +21 -0
  31. package/dist/components/dual-nav/helper.js.map +1 -0
  32. package/dist/components/dual-nav/index.d.ts +1 -0
  33. package/dist/components/dual-nav/style.js +4 -0
  34. package/dist/components/dual-nav/style2.css +145 -0
  35. package/dist/components/empty/empty.vue.d.ts +1 -1
  36. package/dist/components/index.d.ts +23 -22
  37. package/dist/components/index.js +28 -26
  38. package/dist/components/index.js.map +1 -1
  39. package/dist/components/nav/di.js +7 -0
  40. package/dist/components/nav/di.js.map +1 -0
  41. package/dist/components/nav/helper.js +18 -0
  42. package/dist/components/nav/helper.js.map +1 -0
  43. package/dist/components/nav/index.d.ts +3 -0
  44. package/dist/components/{menu/menu-icon.js → nav/nav-icon.js} +4 -4
  45. package/dist/components/nav/nav-icon.js.map +1 -0
  46. package/dist/components/{menu/menu-item-collapsed.js → nav/nav-item-collapsed.js} +23 -23
  47. package/dist/components/nav/nav-item-collapsed.js.map +1 -0
  48. package/dist/components/{menu/menu-item.js → nav/nav-item.js} +13 -13
  49. package/dist/components/nav/nav-item.js.map +1 -0
  50. package/dist/components/{menu/menu-item.vue.d.ts → nav/nav-item.vue.d.ts} +4 -4
  51. package/dist/components/{menu/menu-sub-collapsed.js → nav/nav-sub-collapsed.js} +19 -19
  52. package/dist/components/nav/nav-sub-collapsed.js.map +1 -0
  53. package/dist/components/{menu/menu-sub.js → nav/nav-sub.js} +24 -24
  54. package/dist/components/nav/nav-sub.js.map +1 -0
  55. package/dist/components/{menu/menu-sub.vue.d.ts → nav/nav-sub.vue.d.ts} +4 -4
  56. package/dist/components/{menu/menu.js → nav/nav.js} +45 -36
  57. package/dist/components/nav/nav.js.map +1 -0
  58. package/dist/components/nav/nav.vue.d.ts +18 -0
  59. package/dist/components/nav/style.d.ts +1 -0
  60. package/dist/components/{menu → nav}/style2.css +105 -105
  61. package/dist/components/{menu/use-menu-item.js → nav/use-nav-item.js} +11 -11
  62. package/dist/components/nav/use-nav-item.js.map +1 -0
  63. package/dist/components/{menu/walk-menu-path.js → nav/walk-nav-path.js} +5 -5
  64. package/dist/components/nav/walk-nav-path.js.map +1 -0
  65. package/dist/components/number/number.vue.d.ts +1 -1
  66. package/dist/components/select/style2.css +4 -4
  67. package/dist/index.d.ts +63 -61
  68. package/dist/index.js +56 -55
  69. package/dist/style.js +2 -1
  70. package/dist/types/auto-complete.d.ts +6 -1
  71. package/dist/types/batch-edit.d.ts +15 -2
  72. package/dist/types/dual-nav.d.ts +27 -0
  73. package/dist/types/index.d.ts +4 -3
  74. package/dist/types/nav.d.ts +48 -0
  75. package/package.json +6 -6
  76. package/src/components/auto-complete/auto-complete.vue +24 -13
  77. package/src/components/batch-edit/batch-edit-form.vue +29 -18
  78. package/src/components/batch-edit/batch-edit-list.vue +6 -1
  79. package/src/components/batch-edit/batch-edit.vue +36 -73
  80. package/src/components/batch-edit/di.ts +3 -1
  81. package/src/components/batch-edit/style.scss +5 -0
  82. package/src/components/batch-edit/use-edit.ts +63 -116
  83. package/src/components/batch-edit/use-features.ts +58 -0
  84. package/src/components/contextmenu/style.scss +5 -5
  85. package/src/components/dual-nav/dual-nav-app.vue +33 -0
  86. package/src/components/dual-nav/dual-nav.vue +150 -0
  87. package/src/components/dual-nav/helper.ts +25 -0
  88. package/src/components/dual-nav/index.ts +1 -0
  89. package/src/components/dual-nav/style.scss +153 -0
  90. package/src/components/dual-nav/style.ts +4 -0
  91. package/src/components/index.ts +3 -1
  92. package/src/components/nav/di.ts +17 -0
  93. package/src/components/nav/helper.ts +22 -0
  94. package/src/components/nav/index.ts +3 -0
  95. package/src/components/{menu/menu-icon.vue → nav/nav-icon.vue} +1 -1
  96. package/src/components/nav/nav-item-collapsed.vue +66 -0
  97. package/src/components/nav/nav-item.vue +36 -0
  98. package/src/components/{menu/menu-sub-collapsed.vue → nav/nav-sub-collapsed.vue} +15 -15
  99. package/src/components/{menu/menu-sub.vue → nav/nav-sub.vue} +19 -19
  100. package/src/components/{menu/menu.vue → nav/nav.vue} +42 -42
  101. package/src/components/{menu → nav}/style.scss +16 -14
  102. package/src/components/{menu/use-menu-item.ts → nav/use-nav-item.ts} +14 -14
  103. package/src/components/{menu/walk-menu-path.ts → nav/walk-nav-path.ts} +2 -2
  104. package/src/components/select/style.scss +4 -4
  105. package/src/style.ts +2 -1
  106. package/src/types/auto-complete.ts +6 -1
  107. package/src/types/batch-edit.ts +18 -1
  108. package/src/types/dual-nav.ts +28 -0
  109. package/src/types/index.ts +2 -1
  110. package/src/types/nav.ts +49 -0
  111. package/dist/components/menu/di.js +0 -7
  112. package/dist/components/menu/di.js.map +0 -1
  113. package/dist/components/menu/helper.js +0 -8
  114. package/dist/components/menu/helper.js.map +0 -1
  115. package/dist/components/menu/index.d.ts +0 -3
  116. package/dist/components/menu/menu-icon.js.map +0 -1
  117. package/dist/components/menu/menu-item-collapsed.js.map +0 -1
  118. package/dist/components/menu/menu-item.js.map +0 -1
  119. package/dist/components/menu/menu-sub-collapsed.js.map +0 -1
  120. package/dist/components/menu/menu-sub.js.map +0 -1
  121. package/dist/components/menu/menu.js.map +0 -1
  122. package/dist/components/menu/menu.vue.d.ts +0 -14
  123. package/dist/components/menu/use-menu-item.js.map +0 -1
  124. package/dist/components/menu/walk-menu-path.js.map +0 -1
  125. package/dist/types/menu.d.ts +0 -40
  126. package/src/components/menu/di.ts +0 -17
  127. package/src/components/menu/helper.ts +0 -3
  128. package/src/components/menu/index.ts +0 -3
  129. package/src/components/menu/menu-item-collapsed.vue +0 -66
  130. package/src/components/menu/menu-item.vue +0 -36
  131. package/src/components/menu/menu-node.ts +0 -6
  132. package/src/types/menu.ts +0 -41
  133. /package/dist/components/{menu → dual-nav}/style.d.ts +0 -0
  134. /package/dist/components/{menu → nav}/style.js +0 -0
  135. /package/src/components/{menu → nav}/style.ts +0 -0
@@ -1,7 +1,7 @@
1
1
  import { last, o, safeRun } from '@cat-kit/core'
2
2
  import { computed, nextTick, shallowReactive, shallowRef, watch, type ShallowRef } from 'vue'
3
3
 
4
- import type { TableRow, BatchEditEmits, BatchEditProps, TableExposed } from '../../types'
4
+ import type { TableRow, BatchEditEmits, BatchEditProps } from '../../types'
5
5
  import type { FormExposed } from '../../types/form'
6
6
 
7
7
  /** 按索引路径在原始数据树中定位节点(等价于历史 Forest.visit 路径访问) */
@@ -29,7 +29,6 @@ function visitDataByIndexPath<T extends Record<string, unknown>>(
29
29
  interface Options {
30
30
  props: BatchEditProps
31
31
  emit: BatchEditEmits
32
- tableRef: ShallowRef<TableExposed | undefined>
33
32
  formRef: ShallowRef<FormExposed | undefined>
34
33
  }
35
34
 
@@ -42,8 +41,6 @@ export interface BatchEditStates {
42
41
  row?: TableRow
43
42
  /** 父级,只在添加子级时会有值 */
44
43
  parentRow?: TableRow
45
- /** 数据是否已更新 */
46
- dataUpdated: boolean
47
44
  }
48
45
 
49
46
  export interface EditReturned {
@@ -51,13 +48,13 @@ export interface EditReturned {
51
48
  insertIndexes: ShallowRef<number[]>
52
49
  handleSave: () => Promise<void>
53
50
  handleClose: () => void
54
- handleCreate: () => void
51
+ handleCreate: () => Promise<void>
55
52
  handleEdit: (row: TableRow) => void
56
- handleCopy: (row: TableRow) => void
53
+ handleCopy: (row: TableRow) => Promise<void>
57
54
  handleDelete: (row: TableRow) => Promise<void>
58
- handleInsertToNext: (row: TableRow) => void
59
- handleInsertToPrev: (row: TableRow) => void
60
- handleInsertChild: (row: TableRow) => void
55
+ handleInsertToNext: (row: TableRow) => Promise<void>
56
+ handleInsertToPrev: (row: TableRow) => Promise<void>
57
+ handleInsertChild: (row: TableRow) => Promise<void>
61
58
  }
62
59
 
63
60
  function snapshotData(data?: Record<string, any>) {
@@ -72,101 +69,49 @@ function replaceModelData(model: Record<string, any>, data: Record<string, any>)
72
69
  }
73
70
 
74
71
  export function useEdit(options: Options): EditReturned {
75
- const { props, emit, tableRef, formRef } = options
72
+ const { props, emit, formRef } = options
76
73
 
77
74
  /** 组件状态 */
78
75
  const state = shallowReactive<BatchEditStates>({
79
76
  depth: undefined,
80
77
  visible: false,
81
78
  type: 'create',
82
- loading: false,
83
- dataUpdated: false
79
+ loading: false
84
80
  })
85
81
 
86
82
  const insertIndexes = shallowRef<number[]>([])
87
83
 
88
- watch(
89
- () => state.visible,
90
- (v) => {
91
- if (!v) {
92
- state.dataUpdated = false
93
- }
94
- }
95
- )
96
-
97
- /**
98
- * 「打开行 / 切换 type」时记录的基线快照
99
- * @description
100
- * 用于 changeCb 内部比对 — 若新值与基线相等(深比较)视为无改动,避免某些组件在 mount
101
- * 阶段把规范化后的值再写回模型时被误判为「用户已修改」
102
- */
103
- let baselineSnapshot: Record<string, any> | undefined
104
- let emptySnapshot: Record<string, any> | undefined
105
-
106
84
  function snapshotModelData() {
107
85
  return snapshotData(props.model)
108
86
  }
109
87
 
110
- function resetFormData(data?: Record<string, any>) {
111
- if (!props.model) return
112
- replaceModelData(props.model, snapshotData(data ?? emptySnapshot))
113
- formRef.value?.clearValidate()
114
- }
115
-
116
- function setFormData(formData: Record<string, any>) {
88
+ function setFormData(formData?: Record<string, any>) {
117
89
  if (!props.model) return
118
90
  replaceModelData(props.model, snapshotData(formData))
119
91
  }
120
92
 
121
- watch(
122
- () => props.model,
123
- (model) => {
124
- emptySnapshot = snapshotData(model)
125
- },
126
- { immediate: true }
127
- )
93
+ const isQuickMode = () => props.mode === 'quick'
94
+
95
+ async function runBeforeCreate(data: Record<string, any>) {
96
+ await props.beforeCreate?.(data, state.parentRow?.data)
97
+ }
128
98
 
129
99
  watch(
130
100
  () => state.row,
131
101
  (row) => {
132
- resetFormData()
133
102
  if (row) {
134
103
  state.type = 'update'
135
104
  state.visible = true
136
- setFormData(row.data)
105
+ if (!isQuickMode()) {
106
+ setFormData(row.data)
107
+ }
137
108
  } else {
138
109
  state.visible = false
139
110
  }
140
- baselineSnapshot = snapshotModelData()
141
- state.dataUpdated = false
142
111
  },
143
- // sync 保证 setData 完成后立刻取快照,避免 mount 期间组件回写值时丢基线
144
112
  { flush: 'sync' }
145
113
  )
146
114
 
147
- watch(
148
- () => props.model,
149
- () => {
150
- if (!baselineSnapshot || !state.visible) return
151
- state.dataUpdated = !isSameValue(baselineSnapshot, snapshotModelData())
152
- },
153
- { deep: true }
154
- )
155
-
156
- function isSameValue(a: any, b: any) {
157
- if (a === b) return true
158
- if (a == null && b == null) return true
159
- if (typeof a !== typeof b) return false
160
- if (typeof a === 'object') {
161
- try {
162
- return JSON.stringify(a) === JSON.stringify(b)
163
- } catch {
164
- return false
165
- }
166
- }
167
- return false
168
- }
169
-
170
115
  const childrenKey = computed(() => {
171
116
  return typeof props.tree === 'string' ? props.tree : 'children'
172
117
  })
@@ -204,59 +149,53 @@ export function useEdit(options: Options): EditReturned {
204
149
  return snapshotModelData()
205
150
  }
206
151
 
207
- async function runCreate(cb: () => void) {
152
+ async function runCreate(cb: () => void, initialData?: Record<string, any>) {
208
153
  state.parentRow = undefined
209
154
  if (state.row) {
210
155
  state.row.isCurrent = false
211
156
  }
212
157
  state.row = undefined
213
158
  state.type = 'create'
214
- resetFormData()
215
159
  cb()
216
160
 
217
- let item: Record<string, any> | undefined = undefined
218
-
219
161
  await nextTick()
220
162
 
221
- state.visible = true
222
- baselineSnapshot = snapshotModelData()
223
- state.dataUpdated = false
163
+ setFormData(initialData)
224
164
 
225
- if (item) {
226
- const row = tableRef.value?.getRowByData(item)
227
- if (row) {
228
- state.row = row
229
- }
165
+ if (isQuickMode() && props.model) {
166
+ await runBeforeCreate(props.model)
230
167
  }
168
+
169
+ state.visible = true
231
170
  }
232
171
 
233
172
  /**
234
173
  * 点击新增按钮
235
174
  */
236
- function handleCreate() {
175
+ async function handleCreate() {
237
176
  const { data } = props
238
- runCreate(() => {
177
+ await runCreate(() => {
239
178
  state.depth = 1
240
179
  insertIndexes.value = [data?.length ?? 0]
241
180
  })
242
181
  }
243
182
 
244
- function handleInsertToPrev(row: TableRow) {
245
- runCreate(() => {
183
+ async function handleInsertToPrev(row: TableRow) {
184
+ await runCreate(() => {
246
185
  state.depth = row.depth
247
186
  insertIndexes.value = [...row.indexes]
248
187
  })
249
188
  }
250
189
 
251
- function handleInsertToNext(row: TableRow) {
252
- runCreate(() => {
190
+ async function handleInsertToNext(row: TableRow) {
191
+ await runCreate(() => {
253
192
  state.depth = row.depth
254
193
  insertIndexes.value = [...row.indexes.slice(0, -1), row.index + 1]
255
194
  })
256
195
  }
257
196
 
258
- function handleInsertChild(row: TableRow) {
259
- runCreate(() => {
197
+ async function handleInsertChild(row: TableRow) {
198
+ await runCreate(() => {
260
199
  state.parentRow = row
261
200
  state.depth = row.depth + 1
262
201
  row.expanded = true
@@ -267,19 +206,17 @@ export function useEdit(options: Options): EditReturned {
267
206
 
268
207
  /** 编辑指定行(等同于点击行) */
269
208
  function handleEdit(row: TableRow) {
209
+ state.type = 'update'
270
210
  state.row = row
271
211
  state.depth = row.depth
272
212
  }
273
213
 
274
214
  /** 复制指定行:在其下方以 create 模式打开表单,并预填副本数据 */
275
- function handleCopy(row: TableRow) {
276
- runCreate(() => {
215
+ async function handleCopy(row: TableRow) {
216
+ await runCreate(() => {
277
217
  state.depth = row.depth
278
218
  insertIndexes.value = [...row.indexes.slice(0, -1), row.index + 1]
279
- setFormData(row.data)
280
- baselineSnapshot = snapshotModelData()
281
- state.dataUpdated = false
282
- })
219
+ }, row.data)
283
220
  }
284
221
 
285
222
  function runWithLoading<Arg extends any[]>(fn: (...args: Arg) => Promise<void> | void) {
@@ -306,36 +243,47 @@ export function useEdit(options: Options): EditReturned {
306
243
 
307
244
  if (!model) return
308
245
 
246
+ if (isQuickMode() && state.type === 'update') return
247
+
309
248
  formRef.value?.clearValidate()
310
249
  const valid = await formRef.value?.validate()
311
250
 
312
251
  if (!valid) return
313
252
 
314
- let item = getInsertData()
315
- if (saveMethod) {
316
- const result = await saveMethod(item, state.type, state.parentRow)
317
- if (result) {
318
- item = result
319
- }
253
+ // quick + create:仅校验 + insert
254
+ if (isQuickMode() && state.type === 'create') {
255
+ insert(getInsertData())
256
+ return
320
257
  }
321
258
 
322
- state.dataUpdated = false
259
+ let item = getInsertData()
323
260
 
324
- // 新增
261
+ // normal + create:beforeCreate → saveMethod → insert
325
262
  if (state.type === 'create') {
263
+ await runBeforeCreate(item)
264
+ if (saveMethod) {
265
+ const result = await saveMethod(item, state.type, state.parentRow?.data)
266
+ if (result) {
267
+ item = result
268
+ }
269
+ }
326
270
  insert(item)
327
-
328
- resetFormData()
329
271
  return
330
272
  }
331
- // 更新
332
- if (state.type === 'update') {
333
- const { row } = state
334
- row &&
335
- Object.keys(item).forEach((key) => {
336
- o(row.data).set(key, o(item).get(key))
337
- })
273
+
274
+ // normal + update
275
+ if (saveMethod) {
276
+ const result = await saveMethod(item, state.type, state.parentRow?.data)
277
+ if (result) {
278
+ item = result
279
+ }
338
280
  }
281
+
282
+ const { row } = state
283
+ row &&
284
+ Object.keys(item).forEach((key) => {
285
+ o(row.data).set(key, o(item).get(key))
286
+ })
339
287
  })
340
288
 
341
289
  const handleDelete = runWithLoading(async (row: TableRow) => {
@@ -376,7 +324,6 @@ export function useEdit(options: Options): EditReturned {
376
324
  state.row = undefined
377
325
  state.parentRow = undefined
378
326
  state.depth = undefined
379
- resetFormData()
380
327
  insertIndexes.value = []
381
328
  }
382
329
 
@@ -0,0 +1,58 @@
1
+ import { computed } from 'vue'
2
+
3
+ import type { BatchEditFeature, BatchEditProps, TableRow } from '../../types'
4
+
5
+ interface Options {
6
+ props: BatchEditProps
7
+ }
8
+ export function useFeatures(options: Options) {
9
+ const { props } = options
10
+ const staticFeatures = computed(() => {
11
+ const { features } = props
12
+
13
+ if (Array.isArray(features)) {
14
+ return new Set(features)
15
+ }
16
+
17
+ const defaultFeatures = new Set<BatchEditFeature>(['create', 'delete', 'update', 'createChild'])
18
+
19
+ if (!features) {
20
+ return defaultFeatures
21
+ }
22
+
23
+ // 函数与 false 的特性视为关闭;显式 true 视为开启(即便不在默认集合内)
24
+ Object.entries(features).forEach(([key, value]) => {
25
+ const k = key as BatchEditFeature
26
+ if (typeof value === 'function' || value === false) {
27
+ defaultFeatures.delete(k)
28
+ } else if (value === true) {
29
+ defaultFeatures.add(k)
30
+ }
31
+ })
32
+ return defaultFeatures
33
+ })
34
+
35
+ const dynamicFeatures = computed<
36
+ Record<BatchEditFeature, ((row?: TableRow) => boolean) | undefined>
37
+ >(() => {
38
+ const { features } = props
39
+ if (!Array.isArray(features) && typeof features === 'object') {
40
+ const ret = Object.entries(features)
41
+ .filter(([_, value]) => {
42
+ return typeof value === 'function'
43
+ })
44
+ .reduce(
45
+ (acc, [key, value]) => {
46
+ acc[key] = value
47
+ return acc
48
+ },
49
+ {} as Record<BatchEditFeature, (row?: TableRow) => boolean>
50
+ )
51
+ return ret
52
+ }
53
+
54
+ return {} as Record<BatchEditFeature, ((row?: TableRow) => boolean) | undefined>
55
+ })
56
+
57
+ return { staticFeatures, dynamicFeatures }
58
+ }
@@ -78,15 +78,15 @@ $arrow-size: 12px;
78
78
 
79
79
  #{$b}__item:not(.is-disabled):not(.is-loading) > & {
80
80
  &:hover {
81
- background-color: var(--u-menu-hover-bg);
82
- color: var(--u-menu-hover-color);
81
+ background-color: var(--u-nav-hover-bg);
82
+ color: var(--u-nav-hover-color);
83
83
  }
84
84
  }
85
85
  }
86
86
 
87
87
  #{$b}__item.is-open:not(.is-disabled) > #{$b}__item-content {
88
- background-color: var(--u-menu-hover-bg);
89
- color: var(--u-menu-hover-color);
88
+ background-color: var(--u-nav-hover-bg);
89
+ color: var(--u-nav-hover-color);
90
90
  }
91
91
 
92
92
  @include m.e(icon-place) {
@@ -128,7 +128,7 @@ $arrow-size: 12px;
128
128
 
129
129
  @include m.e(item-content) {
130
130
  padding: 0 fn.use-var(gap, $size);
131
- min-height: calc(var(--u-menu-height-#{$size}) - 8px);
131
+ min-height: calc(var(--u-nav-height-#{$size}) - 8px);
132
132
  border-radius: calc(fn.use-var(radius, $size) + 1px);
133
133
  }
134
134
  }
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <u-tip direction="right" hide-arrow>
3
+ <button
4
+ type="button"
5
+ :class="[cls.e('app'), bem.is('active', active), bem.is('disabled', app.disabled ?? false)]"
6
+ :disabled="app.disabled"
7
+ @click="emit('click')"
8
+ >
9
+ <UNavIcon v-if="app.icon" :icon="app.icon" :class="cls.e('app-icon')" />
10
+ <span v-else :class="cls.e('app-fallback')">{{ app.title[0] }}</span>
11
+ </button>
12
+
13
+ <template #content>
14
+ {{ app.title }}
15
+ </template>
16
+ </u-tip>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { bem } from '@veltra/utils'
21
+
22
+ import type { NavItem } from '../../types'
23
+ import UNavIcon from '../nav/nav-icon.vue'
24
+ import { UTip } from '../tip'
25
+
26
+ defineOptions({ name: 'DualNavApp' })
27
+
28
+ const cls = bem('dual-nav')
29
+
30
+ defineProps<{ app: NavItem; active: boolean }>()
31
+
32
+ const emit = defineEmits<{ click: [] }>()
33
+ </script>
@@ -0,0 +1,150 @@
1
+ <template>
2
+ <div :class="cls.b">
3
+ <div :class="cls.e('rail')">
4
+ <DualNavApp
5
+ v-for="app in menus"
6
+ :key="app.path"
7
+ :app="app"
8
+ :active="isAppActive(app)"
9
+ @click="handleAppClick(app)"
10
+ />
11
+ </div>
12
+
13
+ <div :class="cls.e('divider')" aria-hidden="true" />
14
+
15
+ <div :class="cls.e('panel')">
16
+ <template v-if="panelApp">
17
+ <div :class="cls.e('panel-header')">
18
+ <div :class="cls.e('panel-header-main')">
19
+ <h3 :class="cls.e('panel-title')">{{ panelApp.title }}</h3>
20
+ <p v-if="panelApp.description" :class="cls.e('description')">
21
+ {{ panelApp.description }}
22
+ </p>
23
+ </div>
24
+
25
+ <u-tip
26
+ v-if="hasExpandableMenus"
27
+ :content="allExpanded ? '折叠全部' : '展开全部'"
28
+ hide-arrow
29
+ >
30
+ <button
31
+ type="button"
32
+ :class="cls.e('toggle-expand')"
33
+ :aria-label="allExpanded ? '折叠全部' : '展开全部'"
34
+ @click="handleToggleExpandAll"
35
+ >
36
+ <u-icon>
37
+ <FolderOpened v-if="!allExpanded" />
38
+ <Folder v-else />
39
+ </u-icon>
40
+ </button>
41
+ </u-tip>
42
+ </div>
43
+
44
+ <div :class="cls.e('panel-body')">
45
+ <UNav
46
+ ref="navRef"
47
+ :menus="panelMenus"
48
+ :current-path="currentPath"
49
+ :collapsed="false"
50
+ @item-click="emit('item-click', $event)"
51
+ />
52
+ </div>
53
+ </template>
54
+ </div>
55
+ </div>
56
+ </template>
57
+
58
+ <script lang="ts" setup>
59
+ import { Folder, FolderOpened } from '@veltra/icons/normal'
60
+ import { bem } from '@veltra/utils'
61
+ import { computed, ref, useTemplateRef, watch } from 'vue'
62
+
63
+ import type { DualNavEmits, DualNavProps, DualNavRootItem } from '../../types/dual-nav'
64
+ import type { NavExposed, NavItem } from '../../types/nav'
65
+ import { UIcon } from '../icon'
66
+ import { UNav } from '../nav'
67
+ import { collectNavBranchPaths } from '../nav/helper'
68
+ import { UTip } from '../tip'
69
+ import DualNavApp from './dual-nav-app.vue'
70
+ import { findRootApp } from './helper'
71
+
72
+ defineOptions({ name: 'DualNav' })
73
+
74
+ const props = defineProps<DualNavProps>()
75
+ const emit = defineEmits<DualNavEmits>()
76
+
77
+ const cls = bem('dual-nav')
78
+ const navRef = useTemplateRef<NavExposed>('navRef')
79
+
80
+ /** 右栏当前展示的应用(点击左轨或 currentPath 同步) */
81
+ const panelAppPath = ref<string>()
82
+ const allExpanded = ref(false)
83
+
84
+ function syncPanelAppFromCurrentPath(currentPath?: string) {
85
+ const rootApp = findRootApp(props.menus, currentPath)
86
+ if (rootApp) {
87
+ panelAppPath.value = rootApp.path
88
+ return
89
+ }
90
+
91
+ if (!panelAppPath.value && props.menus?.length) {
92
+ panelAppPath.value = props.menus[0]!.path
93
+ }
94
+ }
95
+
96
+ watch(
97
+ () => [props.currentPath, props.menus] as const,
98
+ ([currentPath]) => {
99
+ syncPanelAppFromCurrentPath(currentPath)
100
+ },
101
+ { immediate: true }
102
+ )
103
+
104
+ const panelApp = computed<DualNavRootItem | undefined>(() => {
105
+ if (!panelAppPath.value || !props.menus?.length) return undefined
106
+ return props.menus.find((app) => app.path === panelAppPath.value)
107
+ })
108
+
109
+ /** 右栏菜单:有子级时展示子级,单层时展示根项自身 */
110
+ const panelMenus = computed<NavItem[] | undefined>(() => {
111
+ const app = panelApp.value
112
+ if (!app) return undefined
113
+ if (app.children?.length) return app.children
114
+ return [app]
115
+ })
116
+
117
+ const hasExpandableMenus = computed(() => {
118
+ return collectNavBranchPaths(panelMenus.value).length > 0
119
+ })
120
+
121
+ watch(panelAppPath, () => {
122
+ allExpanded.value = false
123
+ })
124
+
125
+ /** 左轨高亮:仅当 currentPath 落在该应用下时激活 */
126
+ function isAppActive(app: NavItem): boolean {
127
+ return findRootApp(props.menus, props.currentPath)?.path === app.path
128
+ }
129
+
130
+ function handleAppClick(app: NavItem) {
131
+ if (app.disabled) return
132
+
133
+ panelAppPath.value = app.path
134
+
135
+ if (!app.children?.length) {
136
+ emit('item-click', app)
137
+ }
138
+ }
139
+
140
+ function handleToggleExpandAll() {
141
+ if (allExpanded.value) {
142
+ navRef.value?.collapseAll()
143
+ allExpanded.value = false
144
+ return
145
+ }
146
+
147
+ navRef.value?.expandAll()
148
+ allExpanded.value = true
149
+ }
150
+ </script>
@@ -0,0 +1,25 @@
1
+ import type { DualNavRootItem } from '../../types'
2
+ import { walkNavWithPath } from '../nav/walk-nav-path'
3
+
4
+ /** 根据当前路径反查所属顶层应用 */
5
+ export function findRootApp(
6
+ menus: DualNavRootItem[] | undefined,
7
+ currentPath?: string
8
+ ): DualNavRootItem | undefined {
9
+ if (!currentPath || !menus?.length) return undefined
10
+
11
+ for (const app of menus) {
12
+ if (app.path === currentPath) return app
13
+
14
+ let matched = false
15
+ walkNavWithPath(app, (node) => {
16
+ if (node.path === currentPath) {
17
+ matched = true
18
+ return false
19
+ }
20
+ })
21
+ if (matched) return app
22
+ }
23
+
24
+ return undefined
25
+ }
@@ -0,0 +1 @@
1
+ export { default as UDualNav } from './dual-nav.vue'