@veltra/desktop 1.0.13 → 1.0.14

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 (180) hide show
  1. package/dist/components/action/action-group.js +110 -3
  2. package/dist/components/action/action-group.js.map +1 -1
  3. package/dist/components/action/action-group.vue.d.ts +16 -3
  4. package/dist/components/action/action.js +26 -13
  5. package/dist/components/action/action.js.map +1 -1
  6. package/dist/components/action/action.vue.d.ts +2 -2
  7. package/dist/components/action/style.js +2 -1
  8. package/dist/components/action/style2.css +35 -7
  9. package/dist/components/auto-complete/auto-complete.vue.d.ts +2 -2
  10. package/dist/components/batch-edit/batch-edit-form.js +106 -42
  11. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  12. package/dist/components/batch-edit/batch-edit-list.js +84 -47
  13. package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
  14. package/dist/components/batch-edit/batch-edit.js +42 -10
  15. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  16. package/dist/components/batch-edit/style.js +2 -4
  17. package/dist/components/batch-edit/style2.css +202 -30
  18. package/dist/components/batch-edit/use-edit.js +45 -9
  19. package/dist/components/batch-edit/use-edit.js.map +1 -1
  20. package/dist/components/cascade/cascade.js +3 -1
  21. package/dist/components/cascade/cascade.js.map +1 -1
  22. package/dist/components/cascade/cascade.vue.d.ts +2 -2
  23. package/dist/components/checkbox/checkbox-button.vue.d.ts +1 -1
  24. package/dist/components/checkbox-group/checkbox-group.vue.d.ts +2 -2
  25. package/dist/components/code-editor/code-editor.js +145 -49
  26. package/dist/components/code-editor/code-editor.js.map +1 -1
  27. package/dist/components/code-editor/code-editor.vue.d.ts +2 -0
  28. package/dist/components/code-editor/style2.css +87 -19
  29. package/dist/components/date-panel/date-panel.vue.d.ts +1 -1
  30. package/dist/components/date-picker/date-picker.vue.d.ts +2 -2
  31. package/dist/components/date-range-picker/date-range-picker.vue.d.ts +2 -2
  32. package/dist/components/dialog/dialog.js +6 -2
  33. package/dist/components/dialog/dialog.js.map +1 -1
  34. package/dist/components/dialog/style2.css +2 -3
  35. package/dist/components/expression-editor/components/path-preview.js +27 -0
  36. package/dist/components/expression-editor/components/path-preview.js.map +1 -0
  37. package/dist/components/expression-editor/components/variable-picker.js +199 -153
  38. package/dist/components/expression-editor/components/variable-picker.js.map +1 -1
  39. package/dist/components/expression-editor/core/chip.js +54 -0
  40. package/dist/components/expression-editor/core/chip.js.map +1 -0
  41. package/dist/components/expression-editor/core/editor.js +424 -0
  42. package/dist/components/expression-editor/core/editor.js.map +1 -0
  43. package/dist/components/expression-editor/core/mention.js +91 -0
  44. package/dist/components/expression-editor/core/mention.js.map +1 -0
  45. package/dist/components/expression-editor/core/model.js +64 -0
  46. package/dist/components/expression-editor/core/model.js.map +1 -0
  47. package/dist/components/expression-editor/di.js +4 -10
  48. package/dist/components/expression-editor/di.js.map +1 -1
  49. package/dist/components/expression-editor/expression-editor.js +168 -114
  50. package/dist/components/expression-editor/expression-editor.js.map +1 -1
  51. package/dist/components/expression-editor/expression-editor.vue.d.ts +2 -1
  52. package/dist/components/expression-editor/style.js +0 -2
  53. package/dist/components/expression-editor/style2.css +145 -219
  54. package/dist/components/index.js +54 -54
  55. package/dist/components/input/input.vue.d.ts +2 -2
  56. package/dist/components/layout/layout.js +12 -11
  57. package/dist/components/layout/layout.js.map +1 -1
  58. package/dist/components/multi-select/multi-select.vue.d.ts +3 -3
  59. package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +3 -3
  60. package/dist/components/notification/notification.vue.d.ts +1 -1
  61. package/dist/components/number/number.vue.d.ts +1 -1
  62. package/dist/components/number-input/number-input.js +4 -4
  63. package/dist/components/number-input/number-input.js.map +1 -1
  64. package/dist/components/number-input/number-input.vue.d.ts +2 -2
  65. package/dist/components/number-range-input/number-range-input.vue.d.ts +1 -1
  66. package/dist/components/password-input/password-input.vue.d.ts +1 -1
  67. package/dist/components/pop-confirm/pop-confirm.vue.d.ts +1 -1
  68. package/dist/components/radio-group/radio-group.vue.d.ts +2 -2
  69. package/dist/components/rich-text-editor/rich-text-editor.js +1 -1
  70. package/dist/components/rich-text-editor/rich-text-editor.vue.d.ts +1 -1
  71. package/dist/components/rich-text-editor/toolbar.js +2 -2
  72. package/dist/components/select/select.vue.d.ts +2 -2
  73. package/dist/components/steps/steps.vue.d.ts +1 -1
  74. package/dist/components/table/use-table.js +16 -0
  75. package/dist/components/table/use-table.js.map +1 -1
  76. package/dist/components/tabs/tabs-horizontal.vue.d.ts +1 -1
  77. package/dist/components/tabs/tabs-vertical.vue.d.ts +1 -1
  78. package/dist/components/tabs/tabs.vue.d.ts +1 -1
  79. package/dist/components/textarea/textarea.vue.d.ts +2 -2
  80. package/dist/components/tip/tip.vue.d.ts +1 -1
  81. package/dist/components/tree/style2.css +18 -12
  82. package/dist/components/tree/tree-node.js +7 -4
  83. package/dist/components/tree/tree-node.js.map +1 -1
  84. package/dist/components/tree-select/tree-select.vue.d.ts +3 -3
  85. package/dist/index.d.ts +3 -3
  86. package/dist/index.js +83 -83
  87. package/dist/style.js +3 -3
  88. package/dist/types/action.d.ts +38 -4
  89. package/dist/types/cascade.d.ts +2 -3
  90. package/dist/types/code-editor.d.ts +9 -2
  91. package/dist/types/expression-editor.d.ts +9 -1
  92. package/dist/types/index.d.ts +2 -2
  93. package/package.json +11 -12
  94. package/src/components/action/action-group.vue +126 -56
  95. package/src/components/action/action.vue +35 -12
  96. package/src/components/action/style.scss +46 -10
  97. package/src/components/action/style.ts +2 -1
  98. package/src/components/batch-edit/batch-edit-form.vue +121 -45
  99. package/src/components/batch-edit/batch-edit-list.vue +56 -39
  100. package/src/components/batch-edit/batch-edit.vue +57 -6
  101. package/src/components/batch-edit/style.scss +251 -37
  102. package/src/components/batch-edit/style.ts +5 -8
  103. package/src/components/batch-edit/use-edit.ts +58 -11
  104. package/src/components/cascade/cascade.vue +3 -1
  105. package/src/components/code-editor/code-editor.vue +198 -63
  106. package/src/components/code-editor/style.scss +110 -12
  107. package/src/components/dialog/dialog.vue +6 -2
  108. package/src/components/dialog/style.scss +4 -4
  109. package/src/components/expression-editor/__test__/chip.test.ts +86 -0
  110. package/src/components/expression-editor/__test__/mention.test.ts +122 -0
  111. package/src/components/expression-editor/__test__/model.test.ts +127 -0
  112. package/src/components/expression-editor/components/path-preview.vue +32 -0
  113. package/src/components/expression-editor/components/variable-picker.vue +224 -219
  114. package/src/components/expression-editor/core/chip.ts +101 -0
  115. package/src/components/expression-editor/core/editor.ts +513 -0
  116. package/src/components/expression-editor/core/mention.ts +121 -0
  117. package/src/components/expression-editor/core/model.ts +109 -0
  118. package/src/components/expression-editor/di.ts +4 -32
  119. package/src/components/expression-editor/expression-editor.vue +184 -151
  120. package/src/components/expression-editor/style.scss +148 -220
  121. package/src/components/expression-editor/style.ts +0 -2
  122. package/src/components/layout/layout.vue +17 -19
  123. package/src/components/number-input/number-input.vue +4 -4
  124. package/src/components/table/use-table.ts +16 -0
  125. package/src/components/tree/style.scss +30 -33
  126. package/src/components/tree/tree-node.vue +4 -2
  127. package/src/types/action.ts +42 -4
  128. package/src/types/cascade.ts +4 -3
  129. package/src/types/code-editor.ts +9 -2
  130. package/src/types/expression-editor.ts +9 -0
  131. package/dist/components/action/action-group.vue_vue_type_script_setup_true_lang.js +0 -65
  132. package/dist/components/action/action-group.vue_vue_type_script_setup_true_lang.js.map +0 -1
  133. package/dist/components/expression-editor/constants.js +0 -9
  134. package/dist/components/expression-editor/constants.js.map +0 -1
  135. package/dist/components/expression-editor/internal/editor-runtime/create-runtime.js +0 -47
  136. package/dist/components/expression-editor/internal/editor-runtime/create-runtime.js.map +0 -1
  137. package/dist/components/expression-editor/internal/editor-runtime/model-sync.js +0 -49
  138. package/dist/components/expression-editor/internal/editor-runtime/model-sync.js.map +0 -1
  139. package/dist/components/expression-editor/internal/features/commands/register-command-packs.js +0 -215
  140. package/dist/components/expression-editor/internal/features/commands/register-command-packs.js.map +0 -1
  141. package/dist/components/expression-editor/internal/features/drag-drop/drag-drop-service.js +0 -33
  142. package/dist/components/expression-editor/internal/features/drag-drop/drag-drop-service.js.map +0 -1
  143. package/dist/components/expression-editor/internal/features/insertion/insertion-service.js +0 -33
  144. package/dist/components/expression-editor/internal/features/insertion/insertion-service.js.map +0 -1
  145. package/dist/components/expression-editor/nodes/variable-node.js +0 -94
  146. package/dist/components/expression-editor/nodes/variable-node.js.map +0 -1
  147. package/dist/components/expression-editor/parser.js +0 -32
  148. package/dist/components/expression-editor/parser.js.map +0 -1
  149. package/dist/components/expression-editor/plain-text.js +0 -19
  150. package/dist/components/expression-editor/plain-text.js.map +0 -1
  151. package/dist/components/expression-editor/use-context.js +0 -69
  152. package/dist/components/expression-editor/use-context.js.map +0 -1
  153. package/dist/components/expression-editor/use-decorators.js +0 -27
  154. package/dist/components/expression-editor/use-decorators.js.map +0 -1
  155. package/dist/components/expression-editor/use-expression-drag-drop.js +0 -275
  156. package/dist/components/expression-editor/use-expression-drag-drop.js.map +0 -1
  157. package/src/components/batch-edit/batch-edit-new.vue +0 -168
  158. package/src/components/batch-edit/style-new.scss +0 -35
  159. package/src/components/batch-edit/use-edit-new.ts +0 -68
  160. package/src/components/batch-edit/use-tip.ts +0 -47
  161. package/src/components/expression-editor/__test__/architecture-boundaries.test.ts +0 -103
  162. package/src/components/expression-editor/__test__/architecture-compatibility.test.ts +0 -52
  163. package/src/components/expression-editor/__test__/drag-drop.test.ts +0 -171
  164. package/src/components/expression-editor/__test__/parser.test.ts +0 -62
  165. package/src/components/expression-editor/constants.ts +0 -6
  166. package/src/components/expression-editor/internal/contracts/editor-runtime.ts +0 -11
  167. package/src/components/expression-editor/internal/editor-runtime/create-runtime.ts +0 -64
  168. package/src/components/expression-editor/internal/editor-runtime/index.ts +0 -1
  169. package/src/components/expression-editor/internal/editor-runtime/model-sync.ts +0 -68
  170. package/src/components/expression-editor/internal/features/commands/register-command-packs.ts +0 -517
  171. package/src/components/expression-editor/internal/features/drag-drop/drag-drop-service.ts +0 -51
  172. package/src/components/expression-editor/internal/features/insertion/insertion-service.ts +0 -56
  173. package/src/components/expression-editor/nodes/variable-node.tsx +0 -118
  174. package/src/components/expression-editor/parser.ts +0 -46
  175. package/src/components/expression-editor/plain-text.ts +0 -25
  176. package/src/components/expression-editor/plan.md +0 -22
  177. package/src/components/expression-editor/use-context.ts +0 -134
  178. package/src/components/expression-editor/use-decorators.tsx +0 -29
  179. package/src/components/expression-editor/use-editor.ts +0 -12
  180. package/src/components/expression-editor/use-expression-drag-drop.ts +0 -396
@@ -5,297 +5,302 @@
5
5
  direction="bottom"
6
6
  :trigger-dom="triggerDom"
7
7
  :visible="visible"
8
- @update:visible="updateVisible($event)"
8
+ @update:visible="handleVisibleChange($event)"
9
9
  >
10
10
  <template #content>
11
- <!-- 搜索框 -->
12
- <div v-if="filterable" :class="cls.e('filter')">
13
- <u-input
14
- placeholder="输入关键字进行过滤"
15
- v-model="searchQuery"
16
- :size="'small'"
17
- clearable
18
- @input="handleSearch"
19
- >
20
- <template #suffix>
21
- <u-icon><Search /></u-icon>
22
- </template>
23
- </u-input>
24
- </div>
25
-
26
- <!-- 面包屑导航 -->
27
- <div v-if="breadcrumbs.length > 1 && !searchQuery" :class="cls.e('breadcrumbs')">
28
- <span v-for="(crumb, index) in breadcrumbs" :key="index" :class="cls.e('breadcrumb-item')">
29
- <span
30
- :class="[
31
- cls.e('breadcrumb-link'),
32
- { [cls.e('breadcrumb-link--active')]: index === breadcrumbs.length - 1 }
33
- ]"
34
- @click="navigateToBreadcrumb(index)"
35
- >
36
- {{ crumb.label }}
11
+ <div :class="cls.e('picker')" @mousedown.prevent>
12
+ <!-- 面包屑:仅逐级模式且非根级显示 -->
13
+ <div v-if="!isFlat && navPath.length > 0" :class="cls.e('breadcrumbs')">
14
+ <span :class="cls.e('breadcrumb-item')">
15
+ <span :class="cls.e('breadcrumb-link')" @click="navigateTo(0)">全部变量</span>
16
+ <u-icon :class="cls.e('breadcrumb-separator')">
17
+ <ArrowRight />
18
+ </u-icon>
37
19
  </span>
38
- <u-icon v-if="index < breadcrumbs.length - 1" :class="cls.e('breadcrumb-separator')">
39
- <ArrowRight />
40
- </u-icon>
41
- </span>
42
- </div>
43
-
44
- <!-- 单面板 -->
45
- <div :class="cls.e('panel')" v-if="currentList.length">
46
- <u-scroll tag="ul" :class="cls.e('panel-list')" :content-class="cls.e('panel-content')">
47
- <li
48
- v-for="(item, idx) in currentList"
49
- :key="item.value ?? idx"
50
- :class="getItemCls(item, idx)"
51
- @click="handleItemClick(item)"
52
- @mouseenter="activeIndex = idx"
20
+ <span
21
+ v-for="(crumb, i) in navPath"
22
+ :key="`${i}-${crumb.value}`"
23
+ :class="cls.e('breadcrumb-item')"
53
24
  >
54
- <span :class="cls.e('item-label')">
55
- {{ item.label }}
56
- </span>
57
- <u-icon v-if="item.children?.length" :class="cls.e('item-expand')">
25
+ <span
26
+ :class="[
27
+ cls.e('breadcrumb-link'),
28
+ { [cls.em('breadcrumb-link', 'active')]: i === navPath.length - 1 }
29
+ ]"
30
+ @click="navigateTo(i + 1)"
31
+ >{{ crumb.label }}</span
32
+ >
33
+ <u-icon v-if="i < navPath.length - 1" :class="cls.e('breadcrumb-separator')">
58
34
  <ArrowRight />
59
35
  </u-icon>
60
- </li>
61
- </u-scroll>
62
- </div>
63
-
64
- <!-- 空状态 -->
65
- <div v-else :class="cls.e('empty')">
66
- <UEmpty description="暂无可用变量" />
36
+ </span>
37
+ </div>
38
+
39
+ <div :class="cls.e('picker-body')">
40
+ <div :class="cls.e('panel')" v-if="currentList.length > 0">
41
+ <u-scroll tag="ul" :class="cls.e('panel-list')" :content-class="cls.e('panel-content')">
42
+ <li
43
+ v-for="(item, idx) in currentList"
44
+ :key="`${idx}-${item.value}`"
45
+ :data-idx="idx"
46
+ :class="getItemCls(item, idx)"
47
+ @click="handleItemClick(item, idx)"
48
+ @mouseenter="activeIndex = idx"
49
+ >
50
+ <span :class="cls.e('item-label')">{{ item.label }}</span>
51
+ <u-icon v-if="!isFlat && hasChildren(item)" :class="cls.e('item-expand')">
52
+ <ArrowRight />
53
+ </u-icon>
54
+ </li>
55
+ </u-scroll>
56
+ </div>
57
+ <div v-else :class="cls.e('empty')">
58
+ <UEmpty description="暂无可用变量" />
59
+ </div>
60
+
61
+ <PathPreview v-if="isFlat && activePath.length > 0" :path="activePath" />
62
+ </div>
67
63
  </div>
68
64
  </template>
69
65
  </u-tip>
70
66
  </template>
71
67
 
72
68
  <script lang="ts" setup>
73
- import { ArrowRight, Search } from '@veltra/icons/normal'
69
+ import { ArrowRight } from '@veltra/icons/normal'
74
70
  import { bem } from '@veltra/utils'
75
- import { inject, ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
71
+ import { computed, nextTick, ref, watch } from 'vue'
76
72
 
77
73
  import type { VariableItem } from '../../../types'
78
74
  import { UEmpty } from '../../empty'
79
75
  import { UIcon } from '../../icon'
80
- import { UInput } from '../../input'
81
76
  import { UScroll } from '../../scroll'
82
77
  import { UTip } from '../../tip'
83
- import { ExpressionEditorDIKey } from '../di'
78
+ import PathPreview from './path-preview.vue'
84
79
 
85
80
  defineOptions({
86
81
  name: 'VariablePicker'
87
82
  })
88
83
 
84
+ interface FlatItem extends VariableItem {
85
+ /** 仅扁平模式使用:从根到当前节点的路径 */
86
+ __path: VariableItem[]
87
+ }
88
+
89
89
  const props = defineProps<{
90
90
  visible: boolean
91
91
  triggerDom?: HTMLElement
92
- filterable?: boolean
93
- registerPickerKeyHandler?: (handler: ((e: KeyboardEvent) => void) | null) => void
92
+ /** 顶层变量树 */
93
+ variables?: VariableItem[]
94
+ /** 来自编辑器 mention 状态的过滤字符串。空 = 逐级模式;非空 = 扁平模式。 */
95
+ filter: string
96
+ /** 是否允许选中分支节点 */
97
+ selectableLevels: 'leaf' | 'any'
94
98
  }>()
95
99
 
96
100
  const emit = defineEmits<{
97
- (e: 'select', variable: VariableItem): void
101
+ (e: 'select', item: VariableItem): void
98
102
  (e: 'update:visible', visible: boolean): void
103
+ /** 用户主动取消(按 Esc)。编辑器据此把 mention 状态置为 dismissed。 */
104
+ (e: 'dismiss'): void
99
105
  }>()
100
106
 
101
- const { cls, editorProps, variableMap } = inject(ExpressionEditorDIKey)!
102
-
103
- const searchQuery = ref('')
104
- const activeIndex = ref<number>(-1)
105
- const navigationPath = ref<VariableItem[]>([]) // 当前导航路径
106
-
107
- // 扁平化变量树,用于搜索
108
- function flattenVariables(
109
- items: VariableItem[] | undefined,
110
- parentPath: VariableItem[] = []
111
- ): VariableItem[] {
112
- if (!items) return []
113
- const result: VariableItem[] = []
114
-
115
- for (const item of items) {
116
- const currentPath = [...parentPath, item]
117
- // 如果是叶子节点,添加到结果中
118
- if (!item.children || item.children.length === 0) {
119
- result.push({
120
- ...item,
121
- label: currentPath.map((p) => p.label).join(' / ')
122
- })
123
- } else {
124
- // 递归处理子节点
125
- result.push(...flattenVariables(item.children, currentPath))
126
- }
127
- }
107
+ const cls = bem('expression-editor')
108
+
109
+ const navPath = ref<VariableItem[]>([])
110
+ const activeIndex = ref(0)
111
+
112
+ const isFlat = computed(() => props.filter !== '')
128
113
 
129
- return result
114
+ function hasChildren(item: VariableItem): boolean {
115
+ return !!item.children && item.children.length > 0
130
116
  }
131
117
 
132
- // 过滤后的变量列表
133
- const filteredVariables = computed(() => {
134
- const { variables } = editorProps
135
- if (!variables || variables.length === 0) return []
118
+ function isLeaf(item: VariableItem): boolean {
119
+ return !hasChildren(item)
120
+ }
136
121
 
137
- if (!searchQuery.value.trim()) {
138
- return variables
122
+ /** 当前层级展示的列表(来源依模式而定) */
123
+ const currentList = computed<VariableItem[]>(() => {
124
+ const variables = props.variables ?? []
125
+ if (isFlat.value) return flatList.value
126
+ let level: VariableItem[] = variables
127
+ for (const node of navPath.value) {
128
+ const found = level.find((it) => it.value === node.value)
129
+ if (!found || !found.children) break
130
+ level = found.children
139
131
  }
140
-
141
- // 搜索模式:扁平化并过滤
142
- const flat = flattenVariables(variables)
143
- const query = searchQuery.value.toLowerCase()
144
- return flat.filter((item) => item.label.toLowerCase().includes(query))
132
+ return level
145
133
  })
146
134
 
147
- // 当前显示的列表(单面板)
148
- const currentList = computed(() => {
149
- if (searchQuery.value.trim()) {
150
- // 搜索模式:显示过滤后的扁平列表
151
- return filteredVariables.value
152
- } else {
153
- // 正常模式:根据导航路径显示当前层级
154
- let current: VariableItem[] = filteredVariables.value
155
- for (const pathItem of navigationPath.value) {
156
- const found = current.find((item) => item.value === pathItem.value)
157
- if (found?.children) {
158
- current = found.children
135
+ /** 扁平模式下:根据 selectableLevels 拍平 + 过滤 */
136
+ const flatList = computed<FlatItem[]>(() => {
137
+ const variables = props.variables ?? []
138
+ const out: FlatItem[] = []
139
+ const includeBranches = props.selectableLevels === 'any'
140
+ function walk(items: VariableItem[], path: VariableItem[]) {
141
+ for (const it of items) {
142
+ const nextPath = [...path, it]
143
+ if (isLeaf(it)) {
144
+ out.push({ ...it, __path: nextPath })
159
145
  } else {
160
- break
146
+ if (includeBranches) out.push({ ...it, __path: nextPath })
147
+ walk(it.children!, nextPath)
161
148
  }
162
149
  }
163
- return current
164
150
  }
151
+ walk(variables, [])
152
+ const q = props.filter.toLowerCase()
153
+ return out.filter((it) => it.label.toLowerCase().includes(q))
165
154
  })
166
155
 
167
- // 面包屑导航
168
- const breadcrumbs = computed(() => {
169
- return [{ label: '全部变量', value: '' }, ...navigationPath.value]
156
+ /** 当前 active 项;扁平模式下用于驱动路径预览 */
157
+ const activePath = computed<VariableItem[]>(() => {
158
+ if (!isFlat.value) return []
159
+ const item = flatList.value[activeIndex.value] as FlatItem | undefined
160
+ return item ? item.__path : []
170
161
  })
171
162
 
172
- function getItemCls(item: VariableItem, index: number): string[] {
173
- const isActive = activeIndex.value === index
174
- const isLeaf = !item.children || item.children.length === 0
163
+ function getItemCls(_item: VariableItem, idx: number) {
164
+ return [cls.e('item'), bem.is('active', idx === activeIndex.value)]
165
+ }
166
+
167
+ function navigateTo(i: number) {
168
+ // 0 = 全部变量;i 之后表示进入到 navPath[i-1]
169
+ navPath.value = navPath.value.slice(0, i)
170
+ activeIndex.value = 0
171
+ }
175
172
 
176
- return [cls.e('item'), bem.is('active', isActive), bem.is('leaf', isLeaf)]
173
+ function handleItemClick(item: VariableItem, idx: number) {
174
+ activeIndex.value = idx
175
+ selectActive()
177
176
  }
178
177
 
179
- function handleItemClick(item: VariableItem) {
180
- // 如果有子节点,导航到下一级
181
- if (item.children && item.children.length > 0) {
182
- navigationPath.value.push(item)
183
- activeIndex.value = -1
178
+ function selectActive() {
179
+ const item = currentList.value[activeIndex.value]
180
+ if (!item) return
181
+
182
+ if (isFlat.value) {
183
+ // 扁平模式始终是「选中并替换」
184
+ emit('select', flattenSourceFor(item))
184
185
  return
185
186
  }
186
187
 
187
- // 叶子节点:选择并关闭
188
- const fullItem = variableMap.value.get(item.value)
189
- if (fullItem) {
190
- emit('select', fullItem)
191
- } else {
192
- emit('select', item)
188
+ // 逐级模式
189
+ if (hasChildren(item)) {
190
+ if (props.selectableLevels === 'any') {
191
+ // Enter 在分支项上:选中分支本身
192
+ emit('select', item)
193
+ return
194
+ }
195
+ // 默认 'leaf':进入下一级
196
+ navPath.value = [...navPath.value, item]
197
+ activeIndex.value = 0
198
+ return
193
199
  }
194
- updateVisible(false)
200
+ emit('select', item)
195
201
  }
196
202
 
197
- // 面包屑导航
198
- function navigateToBreadcrumb(index: number) {
199
- if (index === 0) {
200
- // 点击"全部变量",返回根级
201
- navigationPath.value = []
202
- } else {
203
- // 导航到指定层级
204
- navigationPath.value = navigationPath.value.slice(0, index)
205
- }
206
- activeIndex.value = -1
203
+ function flattenSourceFor(it: VariableItem): VariableItem {
204
+ const flat = it as FlatItem
205
+ // 还原为「不带 __path 的纯 VariableItem」,避免泄漏内部字段
206
+ const out: VariableItem = { value: flat.value, label: flat.label }
207
+ if (flat.type) out.type = flat.type
208
+ if (flat.children) out.children = flat.children
209
+ return out
207
210
  }
208
211
 
209
- function handleSearch() {
210
- // 搜索时重置导航路径
211
- navigationPath.value = []
212
- activeIndex.value = -1
212
+ function moveActive(delta: -1 | 1) {
213
+ const max = currentList.value.length - 1
214
+ if (max < 0) return
215
+ let next = activeIndex.value + delta
216
+ if (next < 0) next = 0
217
+ if (next > max) next = max
218
+ activeIndex.value = next
219
+ scrollActiveIntoView()
213
220
  }
214
221
 
215
- function updateVisible(visible: boolean) {
216
- emit('update:visible', visible)
217
- if (!visible) {
218
- // 关闭时重置状态
219
- navigationPath.value = []
220
- searchQuery.value = ''
221
- activeIndex.value = -1
222
- }
222
+ function scrollActiveIntoView() {
223
+ void nextTick(() => {
224
+ const list = document.querySelector(`.${cls.e('panel-list')}`)
225
+ if (!list) return
226
+ const item = list.querySelector(`[data-idx="${activeIndex.value}"]`)
227
+ if (item && 'scrollIntoView' in item) {
228
+ ;(item as HTMLElement).scrollIntoView({ block: 'nearest' })
229
+ }
230
+ })
223
231
  }
224
232
 
225
- // 处理键盘事件
226
- function handleKeydown(e: KeyboardEvent) {
227
- if (!props.visible) return
233
+ function handleVisibleChange(v: boolean) {
234
+ emit('update:visible', v)
235
+ if (!v) reset()
236
+ }
228
237
 
229
- if (e.key === 'Escape') {
230
- e.preventDefault()
231
- updateVisible(false)
232
- return
233
- }
238
+ function reset() {
239
+ navPath.value = []
240
+ activeIndex.value = 0
241
+ }
234
242
 
235
- if (currentList.value.length === 0) return
243
+ watch(
244
+ () => props.visible,
245
+ (v) => {
246
+ if (!v) reset()
247
+ else activeIndex.value = 0
248
+ }
249
+ )
236
250
 
237
- // 初始化 activeIndex
238
- if (activeIndex.value === -1 && currentList.value.length > 0) {
251
+ watch(
252
+ () => props.filter,
253
+ () => {
254
+ // 过滤词变化:重置 active;逐级 → 扁平时清空 navPath
239
255
  activeIndex.value = 0
256
+ if (isFlat.value) navPath.value = []
240
257
  }
258
+ )
241
259
 
242
- const currentItem = currentList.value[activeIndex.value]
243
-
244
- if (e.key === 'ArrowDown') {
245
- e.preventDefault()
246
- activeIndex.value = Math.min(currentList.value.length - 1, activeIndex.value + 1)
247
- } else if (e.key === 'ArrowUp') {
248
- e.preventDefault()
249
- activeIndex.value = Math.max(0, activeIndex.value - 1)
250
- } else if (e.key === ' ' || e.key === 'Spacebar') {
251
- // 空格键:导航到下一级(仅对有子项的项有效)
252
- e.preventDefault()
253
- if (currentItem?.children && currentItem.children.length > 0) {
254
- handleItemClick(currentItem)
255
- }
256
- } else if (e.key === 'ArrowRight') {
257
- // 右箭头:导航到下一级(仅对有子项的项有效)
258
- e.preventDefault()
259
- if (currentItem?.children && currentItem.children.length > 0) {
260
- handleItemClick(currentItem)
261
- }
262
- } else if (e.key === 'ArrowLeft' || e.key === 'Backspace') {
263
- // 左箭头/退格:返回上一级
264
- e.preventDefault()
265
- if (navigationPath.value.length > 0) {
266
- navigationPath.value.pop()
267
- activeIndex.value = 0
268
- }
269
- } else if (e.key === 'Enter') {
270
- // 回车键:确定选择(仅对最末级项有效)
271
- e.preventDefault()
272
- if (currentItem) {
273
- if (!currentItem.children || currentItem.children.length === 0) {
274
- // 只有叶子节点才能选择
275
- handleItemClick(currentItem)
260
+ /**
261
+ * 由父级 forward 进来的键盘事件。
262
+ * 返回 true 表示已处理(外部应 preventDefault)。
263
+ */
264
+ function handleKeydown(e: KeyboardEvent): boolean {
265
+ if (!props.visible) return false
266
+
267
+ switch (e.key) {
268
+ case 'Escape':
269
+ emit('dismiss')
270
+ emit('update:visible', false)
271
+ reset()
272
+ return true
273
+ case 'ArrowDown':
274
+ moveActive(1)
275
+ return true
276
+ case 'ArrowUp':
277
+ moveActive(-1)
278
+ return true
279
+ case 'ArrowLeft':
280
+ // 仅逐级模式:返回上一级;扁平模式下不拦截,让光标正常移动并触发 mention dismiss
281
+ if (!isFlat.value && navPath.value.length > 0) {
282
+ navPath.value = navPath.value.slice(0, -1)
283
+ activeIndex.value = 0
284
+ return true
276
285
  }
286
+ return false
287
+ case 'ArrowRight': {
288
+ if (isFlat.value) return false
289
+ const item = currentList.value[activeIndex.value]
290
+ if (item && hasChildren(item)) {
291
+ navPath.value = [...navPath.value, item]
292
+ activeIndex.value = 0
293
+ return true
294
+ }
295
+ return false
277
296
  }
297
+ case 'Enter':
298
+ selectActive()
299
+ return true
300
+ default:
301
+ return false
278
302
  }
279
303
  }
280
304
 
281
- // 监听键盘事件
282
- watch(
283
- () => props.visible,
284
- (v) => {
285
- if (v) {
286
- document.addEventListener('keydown', handleKeydown)
287
- } else {
288
- document.removeEventListener('keydown', handleKeydown)
289
- }
290
- }
291
- )
292
-
293
- onMounted(() => {
294
- props.registerPickerKeyHandler?.(handleKeydown)
295
- })
296
-
297
- onBeforeUnmount(() => {
298
- document.removeEventListener('keydown', handleKeydown)
299
- props.registerPickerKeyHandler?.(null)
300
- })
305
+ defineExpose({ handleKeydown })
301
306
  </script>
@@ -0,0 +1,101 @@
1
+ import type { BEM } from '@veltra/utils'
2
+
3
+ import type { Segment } from './model'
4
+
5
+ export interface ChipDeps {
6
+ cls: BEM<'expression-editor'>
7
+ }
8
+
9
+ export interface ChipFactoryOptions extends ChipDeps {
10
+ /**
11
+ * 当用户点击 chip 主体(不是 ×)时回调,由调用方决定如何打开重选面板。
12
+ * 收到的 chipEl 当前在 DOM 中的位置即为重选锚点。
13
+ */
14
+ onReselect: (chipEl: HTMLElement) => void
15
+ /** 当用户点击 × 时回调,调用方应将该 chip 从模型 / DOM 中移除。 */
16
+ onRemove: (chipEl: HTMLElement) => void
17
+ }
18
+
19
+ const CHIP_DATA_SEG = 'var'
20
+ const CHIP_VALUE_ATTR = 'data-value'
21
+ const CHIP_TYPE_ATTR = 'data-var-type'
22
+ const CHIP_CLOSE_ATTR = 'data-chip-close'
23
+
24
+ /** 判断一个 DOM 节点是否为 chip 元素(var 段渲染体) */
25
+ export function isChipElement(node: Node | null): node is HTMLElement {
26
+ return (
27
+ !!node &&
28
+ node.nodeType === 1 &&
29
+ (node as HTMLElement).getAttribute('data-seg') === CHIP_DATA_SEG
30
+ )
31
+ }
32
+
33
+ /** 判断点击目标是否落在 × 删除区。 */
34
+ function isCloseTarget(target: EventTarget | null): boolean {
35
+ if (!(target instanceof Element)) return false
36
+ return !!target.closest(`[${CHIP_CLOSE_ATTR}]`)
37
+ }
38
+
39
+ /** 从给定 segment 创建一个 chip 元素(var 段渲染体)。 */
40
+ export function createChip(
41
+ segment: Extract<Segment, { kind: 'var' }>,
42
+ opts: ChipFactoryOptions
43
+ ): HTMLElement {
44
+ const { cls } = opts
45
+
46
+ const chip = document.createElement('span')
47
+ chip.setAttribute('data-seg', CHIP_DATA_SEG)
48
+ chip.setAttribute(CHIP_VALUE_ATTR, segment.value)
49
+ if (segment.type) chip.setAttribute(CHIP_TYPE_ATTR, segment.type)
50
+ chip.setAttribute('contenteditable', 'false')
51
+ chip.className = cls.e('chip')
52
+ chip.title = segment.label
53
+
54
+ const label = document.createElement('span')
55
+ label.className = cls.e('chip-label')
56
+ label.textContent = segment.type ? `${segment.label} (${segment.type})` : segment.label
57
+
58
+ const close = document.createElement('span')
59
+ close.className = cls.e('chip-close')
60
+ close.setAttribute(CHIP_CLOSE_ATTR, 'true')
61
+ close.setAttribute('aria-label', '删除')
62
+ close.setAttribute('role', 'button')
63
+ close.textContent = '×'
64
+
65
+ chip.appendChild(label)
66
+ chip.appendChild(close)
67
+
68
+ chip.addEventListener('mousedown', (e) => {
69
+ e.preventDefault()
70
+ if (isCloseTarget(e.target)) {
71
+ opts.onRemove(chip)
72
+ } else {
73
+ opts.onReselect(chip)
74
+ }
75
+ })
76
+
77
+ return chip
78
+ }
79
+
80
+ /** 读取 chip 元素绑定的变量 value。 */
81
+ export function readChipValue(el: HTMLElement): string {
82
+ return el.getAttribute(CHIP_VALUE_ATTR) ?? ''
83
+ }
84
+
85
+ /** 读取 chip 元素绑定的可选 type。 */
86
+ export function readChipType(el: HTMLElement): string | undefined {
87
+ return el.getAttribute(CHIP_TYPE_ATTR) ?? undefined
88
+ }
89
+
90
+ /** 在 chip 上设置 / 取消选中(焦点)样式。 */
91
+ export function setChipFocused(el: HTMLElement, focused: boolean): void {
92
+ el.classList.toggle('is-focused', focused)
93
+ }
94
+
95
+ /** 测试支持:暴露用于 DOM 选择器的属性常量。 */
96
+ export const ChipAttrs = {
97
+ seg: 'data-seg',
98
+ value: CHIP_VALUE_ATTR,
99
+ type: CHIP_TYPE_ATTR,
100
+ close: CHIP_CLOSE_ATTR
101
+ } as const