@veltra/desktop 1.0.13 → 1.0.15

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 (166) 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/style.js +2 -1
  7. package/dist/components/action/style2.css +35 -7
  8. package/dist/components/batch-edit/batch-edit-form.js +106 -42
  9. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  10. package/dist/components/batch-edit/batch-edit-list.js +84 -47
  11. package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
  12. package/dist/components/batch-edit/batch-edit.js +42 -10
  13. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  14. package/dist/components/batch-edit/style.js +2 -4
  15. package/dist/components/batch-edit/style2.css +202 -30
  16. package/dist/components/batch-edit/use-edit.js +45 -9
  17. package/dist/components/batch-edit/use-edit.js.map +1 -1
  18. package/dist/components/cascade/cascade.js +3 -1
  19. package/dist/components/cascade/cascade.js.map +1 -1
  20. package/dist/components/code-editor/code-editor.js +145 -49
  21. package/dist/components/code-editor/code-editor.js.map +1 -1
  22. package/dist/components/code-editor/code-editor.vue.d.ts +2 -0
  23. package/dist/components/code-editor/style2.css +87 -19
  24. package/dist/components/dialog/dialog.js +6 -2
  25. package/dist/components/dialog/dialog.js.map +1 -1
  26. package/dist/components/dialog/style2.css +2 -3
  27. package/dist/components/expression-editor/components/path-preview.js +27 -0
  28. package/dist/components/expression-editor/components/path-preview.js.map +1 -0
  29. package/dist/components/expression-editor/components/variable-picker.js +200 -153
  30. package/dist/components/expression-editor/components/variable-picker.js.map +1 -1
  31. package/dist/components/expression-editor/core/chip.js +54 -0
  32. package/dist/components/expression-editor/core/chip.js.map +1 -0
  33. package/dist/components/expression-editor/core/editor.js +425 -0
  34. package/dist/components/expression-editor/core/editor.js.map +1 -0
  35. package/dist/components/expression-editor/core/mention.js +91 -0
  36. package/dist/components/expression-editor/core/mention.js.map +1 -0
  37. package/dist/components/expression-editor/core/model.js +64 -0
  38. package/dist/components/expression-editor/core/model.js.map +1 -0
  39. package/dist/components/expression-editor/di.js +4 -10
  40. package/dist/components/expression-editor/di.js.map +1 -1
  41. package/dist/components/expression-editor/expression-editor.js +168 -114
  42. package/dist/components/expression-editor/expression-editor.js.map +1 -1
  43. package/dist/components/expression-editor/expression-editor.vue.d.ts +2 -1
  44. package/dist/components/expression-editor/style.js +0 -2
  45. package/dist/components/expression-editor/style2.css +145 -218
  46. package/dist/components/index.js +54 -54
  47. package/dist/components/layout/layout.js +12 -11
  48. package/dist/components/layout/layout.js.map +1 -1
  49. package/dist/components/multi-select/multi-select.vue.d.ts +1 -1
  50. package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +1 -1
  51. package/dist/components/number-input/number-input.js +4 -4
  52. package/dist/components/number-input/number-input.js.map +1 -1
  53. package/dist/components/pop-confirm/pop-confirm.vue.d.ts +1 -1
  54. package/dist/components/rich-text-editor/rich-text-editor.js +1 -1
  55. package/dist/components/rich-text-editor/toolbar.js +2 -2
  56. package/dist/components/steps/steps.vue.d.ts +1 -1
  57. package/dist/components/table/use-table.js +16 -0
  58. package/dist/components/table/use-table.js.map +1 -1
  59. package/dist/components/tabs/tabs-horizontal.vue.d.ts +3 -3
  60. package/dist/components/tabs/tabs-vertical.vue.d.ts +2 -2
  61. package/dist/components/tabs/tabs.vue.d.ts +3 -3
  62. package/dist/components/tip/tip.js +4 -5
  63. package/dist/components/tip/tip.js.map +1 -1
  64. package/dist/components/tip/tip.vue.d.ts +1 -1
  65. package/dist/components/tree/style2.css +18 -12
  66. package/dist/components/tree/tree-node.js +7 -4
  67. package/dist/components/tree/tree-node.js.map +1 -1
  68. package/dist/components/tree-select/tree-select.vue.d.ts +1 -1
  69. package/dist/index.d.ts +3 -3
  70. package/dist/index.js +83 -83
  71. package/dist/style.js +3 -3
  72. package/dist/types/action.d.ts +38 -4
  73. package/dist/types/cascade.d.ts +2 -3
  74. package/dist/types/code-editor.d.ts +9 -2
  75. package/dist/types/expression-editor.d.ts +9 -1
  76. package/dist/types/index.d.ts +2 -2
  77. package/package.json +11 -12
  78. package/src/components/action/action-group.vue +126 -56
  79. package/src/components/action/action.vue +35 -12
  80. package/src/components/action/style.scss +46 -10
  81. package/src/components/action/style.ts +2 -1
  82. package/src/components/batch-edit/batch-edit-form.vue +121 -45
  83. package/src/components/batch-edit/batch-edit-list.vue +56 -39
  84. package/src/components/batch-edit/batch-edit.vue +57 -6
  85. package/src/components/batch-edit/style.scss +251 -37
  86. package/src/components/batch-edit/style.ts +5 -8
  87. package/src/components/batch-edit/use-edit.ts +58 -11
  88. package/src/components/cascade/cascade.vue +3 -1
  89. package/src/components/code-editor/code-editor.vue +198 -63
  90. package/src/components/code-editor/style.scss +110 -12
  91. package/src/components/dialog/dialog.vue +6 -2
  92. package/src/components/dialog/style.scss +4 -4
  93. package/src/components/expression-editor/__test__/chip.test.ts +86 -0
  94. package/src/components/expression-editor/__test__/editor.test.ts +47 -0
  95. package/src/components/expression-editor/__test__/mention.test.ts +122 -0
  96. package/src/components/expression-editor/__test__/model.test.ts +127 -0
  97. package/src/components/expression-editor/components/path-preview.vue +32 -0
  98. package/src/components/expression-editor/components/variable-picker.vue +225 -219
  99. package/src/components/expression-editor/core/chip.ts +101 -0
  100. package/src/components/expression-editor/core/editor.ts +514 -0
  101. package/src/components/expression-editor/core/mention.ts +121 -0
  102. package/src/components/expression-editor/core/model.ts +109 -0
  103. package/src/components/expression-editor/di.ts +4 -32
  104. package/src/components/expression-editor/expression-editor.vue +184 -151
  105. package/src/components/expression-editor/style.scss +148 -219
  106. package/src/components/expression-editor/style.ts +0 -2
  107. package/src/components/layout/layout.vue +17 -19
  108. package/src/components/number-input/number-input.vue +4 -4
  109. package/src/components/table/use-table.ts +16 -0
  110. package/src/components/tip/tip.vue +4 -3
  111. package/src/components/tree/style.scss +30 -33
  112. package/src/components/tree/tree-node.vue +4 -2
  113. package/src/types/action.ts +42 -4
  114. package/src/types/cascade.ts +4 -3
  115. package/src/types/code-editor.ts +9 -2
  116. package/src/types/expression-editor.ts +9 -0
  117. package/dist/components/action/action-group.vue_vue_type_script_setup_true_lang.js +0 -65
  118. package/dist/components/action/action-group.vue_vue_type_script_setup_true_lang.js.map +0 -1
  119. package/dist/components/expression-editor/constants.js +0 -9
  120. package/dist/components/expression-editor/constants.js.map +0 -1
  121. package/dist/components/expression-editor/internal/editor-runtime/create-runtime.js +0 -47
  122. package/dist/components/expression-editor/internal/editor-runtime/create-runtime.js.map +0 -1
  123. package/dist/components/expression-editor/internal/editor-runtime/model-sync.js +0 -49
  124. package/dist/components/expression-editor/internal/editor-runtime/model-sync.js.map +0 -1
  125. package/dist/components/expression-editor/internal/features/commands/register-command-packs.js +0 -215
  126. package/dist/components/expression-editor/internal/features/commands/register-command-packs.js.map +0 -1
  127. package/dist/components/expression-editor/internal/features/drag-drop/drag-drop-service.js +0 -33
  128. package/dist/components/expression-editor/internal/features/drag-drop/drag-drop-service.js.map +0 -1
  129. package/dist/components/expression-editor/internal/features/insertion/insertion-service.js +0 -33
  130. package/dist/components/expression-editor/internal/features/insertion/insertion-service.js.map +0 -1
  131. package/dist/components/expression-editor/nodes/variable-node.js +0 -94
  132. package/dist/components/expression-editor/nodes/variable-node.js.map +0 -1
  133. package/dist/components/expression-editor/parser.js +0 -32
  134. package/dist/components/expression-editor/parser.js.map +0 -1
  135. package/dist/components/expression-editor/plain-text.js +0 -19
  136. package/dist/components/expression-editor/plain-text.js.map +0 -1
  137. package/dist/components/expression-editor/use-context.js +0 -69
  138. package/dist/components/expression-editor/use-context.js.map +0 -1
  139. package/dist/components/expression-editor/use-decorators.js +0 -27
  140. package/dist/components/expression-editor/use-decorators.js.map +0 -1
  141. package/dist/components/expression-editor/use-expression-drag-drop.js +0 -275
  142. package/dist/components/expression-editor/use-expression-drag-drop.js.map +0 -1
  143. package/src/components/batch-edit/batch-edit-new.vue +0 -168
  144. package/src/components/batch-edit/style-new.scss +0 -35
  145. package/src/components/batch-edit/use-edit-new.ts +0 -68
  146. package/src/components/batch-edit/use-tip.ts +0 -47
  147. package/src/components/expression-editor/__test__/architecture-boundaries.test.ts +0 -103
  148. package/src/components/expression-editor/__test__/architecture-compatibility.test.ts +0 -52
  149. package/src/components/expression-editor/__test__/drag-drop.test.ts +0 -171
  150. package/src/components/expression-editor/__test__/parser.test.ts +0 -62
  151. package/src/components/expression-editor/constants.ts +0 -6
  152. package/src/components/expression-editor/internal/contracts/editor-runtime.ts +0 -11
  153. package/src/components/expression-editor/internal/editor-runtime/create-runtime.ts +0 -64
  154. package/src/components/expression-editor/internal/editor-runtime/index.ts +0 -1
  155. package/src/components/expression-editor/internal/editor-runtime/model-sync.ts +0 -68
  156. package/src/components/expression-editor/internal/features/commands/register-command-packs.ts +0 -517
  157. package/src/components/expression-editor/internal/features/drag-drop/drag-drop-service.ts +0 -51
  158. package/src/components/expression-editor/internal/features/insertion/insertion-service.ts +0 -56
  159. package/src/components/expression-editor/nodes/variable-node.tsx +0 -118
  160. package/src/components/expression-editor/parser.ts +0 -46
  161. package/src/components/expression-editor/plain-text.ts +0 -25
  162. package/src/components/expression-editor/plan.md +0 -22
  163. package/src/components/expression-editor/use-context.ts +0 -134
  164. package/src/components/expression-editor/use-decorators.tsx +0 -29
  165. package/src/components/expression-editor/use-editor.ts +0 -12
  166. package/src/components/expression-editor/use-expression-drag-drop.ts +0 -396
@@ -50,6 +50,8 @@ export interface EditReturned {
50
50
  handleSave: () => Promise<void>
51
51
  handleClose: () => void
52
52
  handleCreate: () => void
53
+ handleEdit: (row: TableRow) => void
54
+ handleCopy: (row: TableRow) => void
53
55
  handleDelete: (row: TableRow) => Promise<void>
54
56
  handleInsertToNext: (row: TableRow) => void
55
57
  handleInsertToPrev: (row: TableRow) => void
@@ -79,8 +81,17 @@ export function useEdit(options: Options): EditReturned {
79
81
  }
80
82
  )
81
83
 
82
- /** 是否是用户输入产生的值变更 */
83
- let changedByUserInput = true
84
+ /**
85
+ * 「打开行 / 切换 type」时记录的基线快照
86
+ * @description
87
+ * 用于 changeCb 内部比对 — 若新值与基线相等(深比较)视为无改动,避免某些组件在 mount
88
+ * 阶段把规范化后的值再写回模型时被误判为「用户已修改」
89
+ */
90
+ let baselineSnapshot: Record<string, any> | undefined
91
+
92
+ function snapshotModelData() {
93
+ return safeRun(() => JSON.parse(JSON.stringify(props.model?.data ?? {})), {})
94
+ }
84
95
 
85
96
  watch(
86
97
  () => state.row,
@@ -89,22 +100,40 @@ export function useEdit(options: Options): EditReturned {
89
100
  if (row) {
90
101
  state.type = 'update'
91
102
  state.visible = true
92
- changedByUserInput = false
93
103
  props.model?.setData(row.data)
94
- nextTick(() => {
95
- changedByUserInput = true
96
- })
97
104
  } else {
98
105
  state.visible = false
99
106
  }
100
- }
107
+ baselineSnapshot = snapshotModelData()
108
+ state.dataUpdated = false
109
+ },
110
+ // sync 保证 setData 完成后立刻取快照,避免 mount 期间组件回写值时丢基线
111
+ { flush: 'sync' }
101
112
  )
102
113
 
103
- const changeCb = (_field: string, _val: any) => {
104
- // 检测到为用户变更时,为数据添加一个已更改状态从而显示修改按钮
105
- if (changedByUserInput) {
106
- state.dataUpdated = true
114
+ const changeCb = (field: string, val: any) => {
115
+ // 与基线比较 — 同值(如组件 mount 时把当前值再写一次)不算"用户修改"
116
+ if (baselineSnapshot) {
117
+ const baseVal = o(baselineSnapshot).get(field)
118
+ if (isSameValue(baseVal, val)) {
119
+ return
120
+ }
121
+ }
122
+ state.dataUpdated = true
123
+ }
124
+
125
+ function isSameValue(a: any, b: any) {
126
+ if (a === b) return true
127
+ if (a == null && b == null) return true
128
+ if (typeof a !== typeof b) return false
129
+ if (typeof a === 'object') {
130
+ try {
131
+ return JSON.stringify(a) === JSON.stringify(b)
132
+ } catch {
133
+ return false
134
+ }
107
135
  }
136
+ return false
108
137
  }
109
138
 
110
139
  watch(
@@ -216,6 +245,22 @@ export function useEdit(options: Options): EditReturned {
216
245
  })
217
246
  }
218
247
 
248
+ /** 编辑指定行(等同于点击行) */
249
+ function handleEdit(row: TableRow) {
250
+ state.row = row
251
+ state.depth = row.depth
252
+ }
253
+
254
+ /** 复制指定行:在其下方以 create 模式打开表单,并预填副本数据 */
255
+ function handleCopy(row: TableRow) {
256
+ runCreate(() => {
257
+ state.depth = row.depth
258
+ insertIndexes.value = [...row.indexes.slice(0, -1), row.index + 1]
259
+ const cloned = safeRun(() => JSON.parse(JSON.stringify(row.data ?? {})), {})
260
+ props.model?.setData(cloned)
261
+ })
262
+ }
263
+
219
264
  function runWithLoading<Arg extends any[]>(fn: (...args: Arg) => Promise<void> | void) {
220
265
  return async (...args: Arg) => {
221
266
  state.loading = true
@@ -321,6 +366,8 @@ export function useEdit(options: Options): EditReturned {
321
366
  handleInsertToNext,
322
367
  handleInsertToPrev,
323
368
  handleCreate,
369
+ handleEdit,
370
+ handleCopy,
324
371
  handleClose,
325
372
  handleSave
326
373
  }
@@ -255,7 +255,9 @@ const qs = shallowRef<string>('')
255
255
  watch([qs, forest], ([qs, forest]) => {
256
256
  const { filterable } = props
257
257
  if (!filterable || !qs) {
258
- forest.dfs((node) => (node.visible = true))
258
+ forest.dfs((node) => {
259
+ node.visible = true
260
+ })
259
261
  getPanelItemList(forest.roots)
260
262
  return
261
263
  }
@@ -1,24 +1,35 @@
1
1
  <template>
2
- <u-scroll :class="className" @keyup.enter.stop>
2
+ <u-scroll :class="className" :style="rootStyle" @keyup.enter.stop>
3
3
  <div ref="container"></div>
4
4
  </u-scroll>
5
5
  </template>
6
6
 
7
7
  <script lang="ts" setup>
8
- import { EditorState } from '@codemirror/state'
8
+ import { Compartment, EditorState, type Extension } from '@codemirror/state'
9
9
  import { oneDark } from '@codemirror/theme-one-dark'
10
- import { tooltips } from '@codemirror/view'
10
+ import { ViewPlugin, tooltips, type ViewUpdate } from '@codemirror/view'
11
11
  import { useFormComponent, useFormFallbackProps } from '@veltra/compositions'
12
12
  import { bem, zIndex } from '@veltra/utils'
13
13
  import { EditorView, basicSetup } from 'codemirror'
14
- import { computed, onBeforeUnmount, shallowRef, useTemplateRef, watch } from 'vue'
14
+ import {
15
+ computed,
16
+ onBeforeUnmount,
17
+ shallowRef,
18
+ useTemplateRef,
19
+ watch,
20
+ type CSSProperties
21
+ } from 'vue'
15
22
 
16
23
  import type { CodeEditorLang, CodeEditorProps } from '../../types'
17
24
  import { UScroll } from '../scroll'
18
25
 
26
+ defineOptions({ name: 'CodeEditor' })
27
+
19
28
  const props = withDefaults(defineProps<CodeEditorProps>(), {
20
29
  disabled: undefined,
21
- readonly: undefined
30
+ readonly: undefined,
31
+ dark: false,
32
+ defaultLines: 8
22
33
  })
23
34
 
24
35
  const model = defineModel<string>()
@@ -27,101 +38,225 @@ const cls = bem('code-editor')
27
38
 
28
39
  const { formProps } = useFormComponent()
29
40
 
30
- const { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props])
31
-
32
- const className = computed<string[]>(() => {
33
- return [
34
- cls.b,
35
- cls.m(size.value),
36
- bem.is('disabled', disabled.value),
37
- bem.is('readonly', readonly.value)
38
- ]
41
+ const { disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {
42
+ disabled: false,
43
+ readonly: false
39
44
  })
40
45
 
41
- const containerRef = useTemplateRef('container')
46
+ const className = computed<string[]>(() => [
47
+ cls.b,
48
+ bem.is('disabled', disabled.value),
49
+ bem.is('readonly', readonly.value),
50
+ bem.is('dark', props.dark)
51
+ ])
42
52
 
53
+ const rootStyle = computed<CSSProperties>(() => ({
54
+ '--u-code-editor-default-lines': Math.max(1, props.defaultLines)
55
+ }))
56
+
57
+ const containerRef = useTemplateRef('container')
43
58
  const editor = shallowRef<EditorView | null>(null)
44
59
 
45
- const langLoaders: Record<CodeEditorLang, () => Promise<any>> = {
60
+ const langLoaders: Record<CodeEditorLang, () => Promise<Extension>> = {
46
61
  js: () => import('@codemirror/lang-javascript').then((m) => m.javascript({ typescript: true })),
47
62
  sql: () => import('@codemirror/lang-sql').then((m) => m.sql()),
48
63
  java: () => import('@codemirror/lang-java').then((m) => m.java()),
49
64
  json: () => import('@codemirror/lang-json').then((m) => m.json())
50
65
  }
51
66
 
52
- async function renderEditor() {
53
- if (!containerRef.value) return
54
- editor.value?.destroy()
67
+ const themeCompartment = new Compartment()
68
+ const editableCompartment = new Compartment()
69
+ const readOnlyCompartment = new Compartment()
70
+ const langCompartment = new Compartment()
55
71
 
56
- const { language } = props
72
+ let phantomGutter: HTMLElement | null = null
73
+ let phantomContainer: HTMLElement | null = null
74
+ let lastPhantomKey = ''
57
75
 
58
- const extensions = [
59
- basicSetup,
60
- oneDark,
61
- tooltips({
62
- parent: document.body
63
- }),
64
- EditorView.theme({
65
- '.cm-tooltip': {
66
- zIndex: zIndex()
67
- }
68
- })
69
- ]
76
+ function resetPhantomCache() {
77
+ phantomGutter = null
78
+ phantomContainer = null
79
+ lastPhantomKey = ''
80
+ }
81
+
82
+ /**
83
+ * 在 .cm-lineNumbers 末尾追加占位行号 DOM,使可视行号数量补齐到 defaultLines。
84
+ * CodeMirror 的 lineNumbers() 仅为真实文档行渲染行号,超出部分由本逻辑补全。
85
+ */
86
+ function syncPhantomLineNumbers() {
87
+ const view = editor.value
88
+ if (!view) return
70
89
 
71
- if (disabled.value) {
72
- extensions.push(EditorView.editable.of(false))
90
+ if (!phantomGutter || !phantomGutter.isConnected) {
91
+ phantomGutter = view.dom.querySelector<HTMLElement>('.cm-gutter.cm-lineNumbers')
73
92
  }
74
- if (readonly.value) {
75
- extensions.push(EditorState.readOnly.of(true))
93
+ if (!phantomGutter) return
94
+
95
+ if (!phantomContainer) {
96
+ phantomContainer = document.createElement('div')
97
+ phantomContainer.className = 'cm-phantom-lines'
98
+ }
99
+ if (
100
+ phantomContainer.parentElement !== phantomGutter ||
101
+ phantomContainer !== phantomGutter.lastElementChild
102
+ ) {
103
+ phantomGutter.appendChild(phantomContainer)
76
104
  }
77
- if (language) {
78
- const lang = await langLoaders[language]()
79
105
 
80
- extensions.push(lang)
106
+ const realLines = view.state.doc.lines
107
+ const target = Math.max(0, Math.max(1, props.defaultLines) - realLines)
108
+ const lineHeight = view.defaultLineHeight
109
+
110
+ const key = `${realLines}|${target}|${lineHeight}`
111
+ if (key === lastPhantomKey) return
112
+ lastPhantomKey = key
113
+
114
+ while (phantomContainer.children.length > target) {
115
+ phantomContainer.lastElementChild!.remove()
116
+ }
117
+ while (phantomContainer.children.length < target) {
118
+ const el = document.createElement('div')
119
+ el.className = 'cm-gutterElement cm-gutterElement-phantom'
120
+ phantomContainer.appendChild(el)
81
121
  }
122
+ for (let i = 0; i < target; i++) {
123
+ const el = phantomContainer.children[i] as HTMLElement
124
+ const num = String(realLines + i + 1)
125
+ if (el.textContent !== num) el.textContent = num
126
+ if (lineHeight && el.style.height !== `${lineHeight}px`) {
127
+ el.style.height = `${lineHeight}px`
128
+ }
129
+ }
130
+ }
82
131
 
83
- editor.value = new EditorView({
84
- doc: model.value,
85
- extensions,
86
- parent: containerRef.value,
132
+ const phantomLineNumbersPlugin = ViewPlugin.fromClass(
133
+ class {
134
+ update(update: ViewUpdate) {
135
+ if (update.docChanged || update.geometryChanged) {
136
+ syncPhantomLineNumbers()
137
+ }
138
+ }
139
+ }
140
+ )
141
+
142
+ function buildExtensions(): Extension[] {
143
+ return [
144
+ basicSetup,
145
+ tooltips({ parent: document.body }),
146
+ EditorView.theme({
147
+ '.cm-tooltip': { zIndex: zIndex() }
148
+ }),
149
+ themeCompartment.of(props.dark ? oneDark : []),
150
+ editableCompartment.of(EditorView.editable.of(!disabled.value)),
151
+ readOnlyCompartment.of(EditorState.readOnly.of(readonly.value)),
152
+ langCompartment.of([]),
153
+ phantomLineNumbersPlugin
154
+ ]
155
+ }
87
156
 
157
+ let editorToken = 0
158
+ let langToken = 0
159
+
160
+ async function applyLanguage() {
161
+ const view = editor.value
162
+ if (!view) return
163
+ const myToken = ++langToken
164
+ const { language } = props
165
+ const ext = language ? await langLoaders[language]() : []
166
+ if (myToken !== langToken || editor.value !== view) return
167
+ view.dispatch({ effects: langCompartment.reconfigure(ext) })
168
+ }
169
+
170
+ function destroyEditor() {
171
+ ++editorToken
172
+ ++langToken
173
+ editor.value?.destroy()
174
+ editor.value = null
175
+ resetPhantomCache()
176
+ }
177
+
178
+ function createEditor() {
179
+ destroyEditor()
180
+ const myToken = ++editorToken
181
+ const parent = containerRef.value
182
+ if (!parent) return
183
+
184
+ const view = new EditorView({
185
+ doc: model.value,
186
+ extensions: buildExtensions(),
187
+ parent,
88
188
  dispatch(tr) {
89
- editor.value?.update([tr])
90
- if (tr.changes.empty || !tr.docChanged) {
91
- return
189
+ view.update([tr])
190
+ if (tr.docChanged) {
191
+ model.value = tr.state.doc.toString()
92
192
  }
93
- model.value = tr.state.doc.toString()
94
193
  }
95
194
  })
195
+
196
+ if (myToken !== editorToken) {
197
+ view.destroy()
198
+ return
199
+ }
200
+
201
+ editor.value = view
202
+ syncPhantomLineNumbers()
203
+ applyLanguage()
96
204
  }
97
205
 
98
- watch([containerRef, () => props.language, disabled, readonly], renderEditor)
206
+ watch(
207
+ containerRef,
208
+ (el) => {
209
+ if (el) createEditor()
210
+ else destroyEditor()
211
+ },
212
+ { immediate: true, flush: 'post' }
213
+ )
214
+
215
+ watch(() => props.language, applyLanguage)
216
+
217
+ watch(
218
+ () => props.dark,
219
+ (dark) => {
220
+ editor.value?.dispatch({
221
+ effects: themeCompartment.reconfigure(dark ? oneDark : [])
222
+ })
223
+ }
224
+ )
225
+
226
+ watch(disabled, (v) => {
227
+ editor.value?.dispatch({
228
+ effects: editableCompartment.reconfigure(EditorView.editable.of(!v))
229
+ })
230
+ })
231
+
232
+ watch(readonly, (v) => {
233
+ editor.value?.dispatch({
234
+ effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(v))
235
+ })
236
+ })
237
+
238
+ watch(() => props.defaultLines, syncPhantomLineNumbers)
99
239
 
100
240
  watch(
101
241
  [() => props.modelValue, editor],
102
242
  ([v, e]) => {
103
243
  if (!e) return
244
+ if (e.composing) return
245
+ const next = v ?? ''
246
+ if (e.state.doc.toString() === next) return
104
247
 
105
- if (e.composing || e.state.doc.toJSON().join('\n') === v) {
106
- return
107
- }
108
- const isSelectionOutOfRange = !e?.state.selection.ranges.every(
109
- (range) => range.anchor < (v?.length ?? 0) && range.head < (v?.length ?? 0)
248
+ const valueLength = next.length
249
+ const inRange = e.state.selection.ranges.every(
250
+ (range) => range.anchor <= valueLength && range.head <= valueLength
110
251
  )
111
- e?.dispatch({
112
- changes: {
113
- from: 0,
114
- to: e?.state.doc.length,
115
- insert: v
116
- },
117
- selection: isSelectionOutOfRange ? { anchor: 0, head: 0 } : e?.state.selection,
252
+ e.dispatch({
253
+ changes: { from: 0, to: e.state.doc.length, insert: next },
254
+ selection: inRange ? e.state.selection : { anchor: 0, head: 0 },
118
255
  scrollIntoView: true
119
256
  })
120
257
  },
121
258
  { immediate: true }
122
259
  )
123
260
 
124
- onBeforeUnmount(() => {
125
- editor.value?.destroy()
126
- })
261
+ onBeforeUnmount(destroyEditor)
127
262
  </script>
@@ -2,32 +2,130 @@
2
2
  @use 'pkg:@veltra/styles/functions' as fn;
3
3
  @use 'pkg:@veltra/styles/vars';
4
4
 
5
- // 方便拼接
6
5
  $root-name: code-editor;
7
6
 
8
7
  @include m.b($root-name) {
9
- max-height: 300px;
8
+ --u-code-editor-default-lines: 8;
9
+ --u-code-editor-line-height: 1.6;
10
+ --u-code-editor-font-size: 13px;
11
+ --u-code-editor-padding-block: 6px;
12
+ --u-code-editor-bg: #{fn.use-var(bg, color, top)};
13
+ --u-code-editor-fg: #{fn.use-var(text-color, main)};
14
+ --u-code-editor-border: #{fn.use-var(border, color)};
15
+ --u-code-editor-gutter-fg: #{fn.use-var(text-color, placeholder)};
16
+ --u-code-editor-active-bg: rgba(30, 136, 229, 0.06);
17
+ --u-code-editor-selection-bg: rgba(30, 136, 229, 0.18);
18
+
19
+ position: relative;
10
20
  width: 100%;
11
- box-shadow: inset 0 0 1px 1px fn.use-var(border-color);
12
- transition: box-shadow 0.25s ease;
21
+ max-height: 480px;
22
+ border-radius: fn.use-var(radius, default);
23
+ background-color: var(--u-code-editor-bg);
24
+ box-shadow: inset 0 0 0 1px var(--u-code-editor-border);
25
+ overflow: hidden;
26
+ transition: box-shadow 0.2s ease;
13
27
 
14
- &:hover {
15
- box-shadow: inset 0 0 1px 1px fn.use-var(color, primary);
28
+ &:hover,
29
+ &:focus-within {
30
+ box-shadow: inset 0 0 0 1px fn.use-var(color, primary);
16
31
  }
17
32
 
18
- @include m.size using ($size) {
19
- min-height: fn.use-var(form-component-height, $size);
20
- border-radius: fn.use-var(radius, $size);
21
- padding: 2px calc(fn.use-var(gap, $size) / 2);
33
+ .cm-editor {
34
+ background: transparent;
35
+ color: var(--u-code-editor-fg);
36
+ font-size: var(--u-code-editor-font-size);
37
+ line-height: var(--u-code-editor-line-height);
38
+ font-family: ui-monospace, 'JetBrains Mono', 'Fira Code', 'SFMono-Regular', Menlo, Consolas,
39
+ 'Liberation Mono', monospace;
40
+ outline: none;
41
+
42
+ &.cm-focused {
43
+ outline: none;
44
+ }
45
+
46
+ .cm-scroller {
47
+ font-family: inherit;
48
+ line-height: inherit;
49
+ overflow: visible;
50
+ }
51
+
52
+ .cm-content {
53
+ padding: var(--u-code-editor-padding-block) 0;
54
+ min-height: calc(
55
+ var(--u-code-editor-default-lines) * var(--u-code-editor-line-height) *
56
+ var(--u-code-editor-font-size) + var(--u-code-editor-padding-block) * 2
57
+ );
58
+ caret-color: fn.use-var(color, primary);
59
+ }
60
+
61
+ .cm-line {
62
+ padding: 0 8px;
63
+ }
64
+
65
+ .cm-gutters {
66
+ background-color: transparent;
67
+ color: var(--u-code-editor-gutter-fg);
68
+ border-right: 1px solid var(--u-code-editor-border);
69
+ user-select: none;
70
+ }
71
+
72
+ .cm-gutterElement-phantom {
73
+ color: var(--u-code-editor-gutter-fg);
74
+ opacity: 0.45;
75
+ padding: 0 3px 0 5px;
76
+ text-align: right;
77
+ white-space: nowrap;
78
+ min-width: 1ch;
79
+ box-sizing: border-box;
80
+ pointer-events: none;
81
+ }
82
+
83
+ .cm-activeLineGutter,
84
+ .cm-activeLine {
85
+ background-color: var(--u-code-editor-active-bg);
86
+ }
87
+
88
+ .cm-selectionBackground,
89
+ ::selection {
90
+ background-color: var(--u-code-editor-selection-bg);
91
+ }
92
+
93
+ .cm-cursor {
94
+ border-left-color: fn.use-var(color, primary);
95
+ }
96
+ }
97
+
98
+ @include m.is(dark) {
99
+ --u-code-editor-bg: #282c34;
100
+ --u-code-editor-fg: #abb2bf;
101
+ --u-code-editor-border: rgba(255, 255, 255, 0.08);
102
+ --u-code-editor-gutter-fg: #5c6370;
103
+ --u-code-editor-active-bg: rgba(255, 255, 255, 0.04);
104
+ --u-code-editor-selection-bg: rgba(99, 162, 255, 0.25);
22
105
  }
23
106
 
24
107
  @include m.is(disabled) {
25
108
  background-color: fn.use-var(color, disabled);
26
- box-shadow: inset 0 0 1px 1px fn.use-var(border, color);
109
+ box-shadow: inset 0 0 0 1px var(--u-code-editor-border);
27
110
  cursor: not-allowed;
111
+
112
+ &:hover,
113
+ &:focus-within {
114
+ box-shadow: inset 0 0 0 1px var(--u-code-editor-border);
115
+ }
116
+
117
+ .cm-editor,
118
+ .cm-content {
119
+ cursor: not-allowed;
120
+ }
28
121
  }
29
122
 
30
123
  @include m.is(readonly) {
31
- box-shadow: none;
124
+ box-shadow: inset 0 0 0 1px var(--u-code-editor-border);
125
+
126
+ &:hover,
127
+ &:focus-within {
128
+ box-shadow: inset 0 0 0 1px var(--u-code-editor-border);
129
+ }
32
130
  }
33
131
  }
@@ -193,12 +193,16 @@ const translated = {
193
193
  y: 0
194
194
  }
195
195
 
196
- /** 更新弹框位置 */
196
+ /**
197
+ * 更新弹框位置
198
+ * 使用独立 `translate` 而非整体 `transform`, 与 spring 动画的 `scale` 解耦,
199
+ * 避免动画期间整体 transform 被关键帧覆盖、动画结束后又"啪"地跳回内联位置
200
+ */
197
201
  const updateDialogTransform = (x: number, y: number) => {
198
202
  const dom = dialogRef.value
199
203
  if (!dom) return
200
204
  setStyles(dom, {
201
- transform: `scale3d(1, 1, 1) translate3d(${x}px, ${y}px, 0)`
205
+ translate: `${x}px ${y}px`
202
206
  })
203
207
  }
204
208
 
@@ -23,7 +23,6 @@ $root-name: dialog;
23
23
  @include m.b($root-name) {
24
24
  pointer-events: auto;
25
25
  border-radius: fn.use-var(radius, large);
26
- will-change: transform, opacity;
27
26
  display: flex;
28
27
  flex-direction: column;
29
28
  max-height: 90vh;
@@ -119,12 +118,13 @@ $root-name: dialog;
119
118
  }
120
119
  }
121
120
 
122
- // // 弹框最大化动画进入后的类
121
+ // 弹框最大化动画进入后的类
123
122
  .#{$root-name}-maximize-enter-to {
124
123
  width: 100vw !important;
125
124
  height: 100vh !important;
126
125
  max-height: 100vh;
127
- transform: translate3d(0, 0, 0) scale3d(1, 1, 1) !important;
126
+ // 重置拖拽偏移; 不动 scale 以免与 spring leave 的 scale: 0 冲突
127
+ translate: 0 0 !important;
128
128
  @include m.bem($root-name, header) {
129
129
  &:active {
130
130
  cursor: default;
@@ -135,7 +135,7 @@ $root-name: dialog;
135
135
  // 弹框最大化动画激活时的类
136
136
  .#{$root-name}-maximize-enter-active,
137
137
  .#{$root-name}-maximize-leave-active {
138
- transition-property: width, height, transform;
138
+ transition-property: width, height, translate;
139
139
  transition-duration: 0.25s;
140
140
  }
141
141