@veltra/desktop 1.8.2 → 1.8.4

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 (207) hide show
  1. package/dist/components/action/action-group.js.map +1 -1
  2. package/dist/components/action/action-group.vue_vue_type_script_setup_true_lang.js.map +1 -1
  3. package/dist/components/action/action.js.map +1 -1
  4. package/dist/components/auto-complete/auto-complete.js.map +1 -1
  5. package/dist/components/badge/badge.js.map +1 -1
  6. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  7. package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
  8. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  9. package/dist/components/breadcrumb/breadcrumb.js.map +1 -1
  10. package/dist/components/button/button-group.js.map +1 -1
  11. package/dist/components/button/button.js.map +1 -1
  12. package/dist/components/calendar/calendar.js.map +1 -1
  13. package/dist/components/card/card-action.js.map +1 -1
  14. package/dist/components/card/card-content.js.map +1 -1
  15. package/dist/components/card/card-cover.js.map +1 -1
  16. package/dist/components/card/card-header.js.map +1 -1
  17. package/dist/components/card/card.js.map +1 -1
  18. package/dist/components/cascade/cascade-panel-item.js.map +1 -1
  19. package/dist/components/cascade/cascade.js +1 -0
  20. package/dist/components/cascade/cascade.js.map +1 -1
  21. package/dist/components/cascade/use-select.js +6 -2
  22. package/dist/components/cascade/use-select.js.map +1 -1
  23. package/dist/components/check-tag/check-tag.js.map +1 -1
  24. package/dist/components/checkbox/checkbox-button.js.map +1 -1
  25. package/dist/components/checkbox/checkbox.js.map +1 -1
  26. package/dist/components/checkbox-group/checkbox-group.js.map +1 -1
  27. package/dist/components/code-editor/code-editor.js.map +1 -1
  28. package/dist/components/collapse/collapse-item.js.map +1 -1
  29. package/dist/components/collapse/collapse.js.map +1 -1
  30. package/dist/components/condition-editor/components/condition-group.js.map +1 -1
  31. package/dist/components/condition-editor/components/condition-row.js.map +1 -1
  32. package/dist/components/condition-editor/condition-editor.js.map +1 -1
  33. package/dist/components/contextmenu/contextmenu-item.js.map +1 -1
  34. package/dist/components/contextmenu/contextmenu-panel.js.map +1 -1
  35. package/dist/components/contextmenu/contextmenu-sub.js.map +1 -1
  36. package/dist/components/contextmenu/contextmenu.js.map +1 -1
  37. package/dist/components/date-panel/date-panel.js.map +1 -1
  38. package/dist/components/date-panel/headers/day.js.map +1 -1
  39. package/dist/components/date-panel/headers/month.js.map +1 -1
  40. package/dist/components/date-panel/headers/year.js.map +1 -1
  41. package/dist/components/date-panel/panels/day.js.map +1 -1
  42. package/dist/components/date-panel/panels/month.js.map +1 -1
  43. package/dist/components/date-panel/panels/year.js.map +1 -1
  44. package/dist/components/date-picker/date-picker.js.map +1 -1
  45. package/dist/components/date-range-picker/date-range-picker.js.map +1 -1
  46. package/dist/components/dialog/dialog.js.map +1 -1
  47. package/dist/components/drawer/drawer.js.map +1 -1
  48. package/dist/components/dropdown/dropdown.js.map +1 -1
  49. package/dist/components/dual-nav/dual-nav-app.js.map +1 -1
  50. package/dist/components/dual-nav/dual-nav.js.map +1 -1
  51. package/dist/components/empty/empty.js.map +1 -1
  52. package/dist/components/expression-editor/components/path-preview.js.map +1 -1
  53. package/dist/components/expression-editor/components/variable-picker.js.map +1 -1
  54. package/dist/components/expression-editor/expression-editor.js.map +1 -1
  55. package/dist/components/file-picker/file-picker.js.map +1 -1
  56. package/dist/components/file-viewer/file-viewer.js.map +1 -1
  57. package/dist/components/file-viewer/previewers/docx-previewer.js.map +1 -1
  58. package/dist/components/file-viewer/previewers/image-previewer.js.map +1 -1
  59. package/dist/components/file-viewer/previewers/ofd-previewer.js.map +1 -1
  60. package/dist/components/file-viewer/previewers/pdf-previewer.js.map +1 -1
  61. package/dist/components/file-viewer/previewers/pdf-viewport-scroll.js.map +1 -1
  62. package/dist/components/file-viewer/previewers/pdf-zoom-bridge.js.map +1 -1
  63. package/dist/components/file-viewer/previewers/sheet-previewer.js.map +1 -1
  64. package/dist/components/file-viewer/previewers/text-previewer.js.map +1 -1
  65. package/dist/components/file-viewer/previewers/video-previewer.js.map +1 -1
  66. package/dist/components/float-button/float-button.js.map +1 -1
  67. package/dist/components/form/form.js.map +1 -1
  68. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
  69. package/dist/components/form-item/form-item.js.map +1 -1
  70. package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js.map +1 -1
  71. package/dist/components/gantt-chart/gantt-chart.js.map +1 -1
  72. package/dist/components/grid/grid-item.js.map +1 -1
  73. package/dist/components/grid/grid.js.map +1 -1
  74. package/dist/components/grid-input/grid-input.js.map +1 -1
  75. package/dist/components/group-input/group-input.js.map +1 -1
  76. package/dist/components/group-nav/group-nav-item.js.map +1 -1
  77. package/dist/components/group-nav/group-nav.js.map +1 -1
  78. package/dist/components/icon/icon.js.map +1 -1
  79. package/dist/components/image-cropper/image-cropper.js.map +1 -1
  80. package/dist/components/input/input.js.map +1 -1
  81. package/dist/components/input/input.vue_vue_type_script_setup_true_lang.js.map +1 -1
  82. package/dist/components/kanban/kanban-column.js.map +1 -1
  83. package/dist/components/kanban/kanban.js.map +1 -1
  84. package/dist/components/kbd/kbd.js.map +1 -1
  85. package/dist/components/layout/layout-resizer.js.map +1 -1
  86. package/dist/components/layout/layout.js.map +1 -1
  87. package/dist/components/list/list-item.js.map +1 -1
  88. package/dist/components/list/list.js.map +1 -1
  89. package/dist/components/loading/loading.js.map +1 -1
  90. package/dist/components/message/message-box.js.map +1 -1
  91. package/dist/components/message/message.js.map +1 -1
  92. package/dist/components/message-confirm/message-confirm-box.js.map +1 -1
  93. package/dist/components/message-confirm/message-confirm.js.map +1 -1
  94. package/dist/components/multi-select/multi-select-option.js.map +1 -1
  95. package/dist/components/multi-select/multi-select.js.map +1 -1
  96. package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
  97. package/dist/components/nav/nav-icon.js.map +1 -1
  98. package/dist/components/nav/nav-item-collapsed.js.map +1 -1
  99. package/dist/components/nav/nav-item.js.map +1 -1
  100. package/dist/components/nav/nav-sub-collapsed.js.map +1 -1
  101. package/dist/components/nav/nav-sub.js.map +1 -1
  102. package/dist/components/nav/nav.js.map +1 -1
  103. package/dist/components/notification/notification-box.js.map +1 -1
  104. package/dist/components/notification/notification.js.map +1 -1
  105. package/dist/components/number/number.js.map +1 -1
  106. package/dist/components/number-input/number-input.js.map +1 -1
  107. package/dist/components/number-range-input/number-range-input.js.map +1 -1
  108. package/dist/components/paginator/paginator.js.map +1 -1
  109. package/dist/components/palette/palette-alpha.js.map +1 -1
  110. package/dist/components/palette/palette-color-switch.js.map +1 -1
  111. package/dist/components/palette/palette-hue.js.map +1 -1
  112. package/dist/components/palette/palette-sv.js.map +1 -1
  113. package/dist/components/palette/palette.js.map +1 -1
  114. package/dist/components/password-input/password-input.js.map +1 -1
  115. package/dist/components/pop-confirm/pop-confirm.js.map +1 -1
  116. package/dist/components/progress/progress.js.map +1 -1
  117. package/dist/components/progress-nodes/progress-nodes.js.map +1 -1
  118. package/dist/components/radio/radio.js.map +1 -1
  119. package/dist/components/radio-group/radio-group.js.map +1 -1
  120. package/dist/components/rich-text-editor/image-node.js +53 -14
  121. package/dist/components/rich-text-editor/image-node.js.map +1 -1
  122. package/dist/components/rich-text-editor/rich-text-editor.js +9 -3
  123. package/dist/components/rich-text-editor/rich-text-editor.js.map +1 -1
  124. package/dist/components/rich-text-editor/style2.css +31 -0
  125. package/dist/components/rich-text-editor/toolbar.js.map +1 -1
  126. package/dist/components/rich-text-editor/use-image-resize.js +118 -0
  127. package/dist/components/rich-text-editor/use-image-resize.js.map +1 -0
  128. package/dist/components/scroll/scroll-bar.js.map +1 -1
  129. package/dist/components/scroll/scroll.js.map +1 -1
  130. package/dist/components/segment/segment.js.map +1 -1
  131. package/dist/components/select/select.js +9 -5
  132. package/dist/components/select/select.js.map +1 -1
  133. package/dist/components/slider/slider-thumb.js.map +1 -1
  134. package/dist/components/slider/slider.js.map +1 -1
  135. package/dist/components/steps/steps.js.map +1 -1
  136. package/dist/components/switch/switch.js.map +1 -1
  137. package/dist/components/table/table-body.js.map +1 -1
  138. package/dist/components/table/table-cell.js.map +1 -1
  139. package/dist/components/table/table-foot.js.map +1 -1
  140. package/dist/components/table/table-head.js.map +1 -1
  141. package/dist/components/table/table-row.js.map +1 -1
  142. package/dist/components/table/table.js.map +1 -1
  143. package/dist/components/table-editor/table-editor.js.map +1 -1
  144. package/dist/components/tabs/tabs-horizontal.js.map +1 -1
  145. package/dist/components/tabs/tabs-vertical.js.map +1 -1
  146. package/dist/components/tabs/tabs.js.map +1 -1
  147. package/dist/components/tag/tag.js.map +1 -1
  148. package/dist/components/text/text.js.map +1 -1
  149. package/dist/components/textarea/textarea.js.map +1 -1
  150. package/dist/components/theme/theme.js.map +1 -1
  151. package/dist/components/tip/tip.js.map +1 -1
  152. package/dist/components/tree/tree-node.js.map +1 -1
  153. package/dist/components/tree/tree.js.map +1 -1
  154. package/dist/components/tree-select/tree-select.js +19 -10
  155. package/dist/components/tree-select/tree-select.js.map +1 -1
  156. package/dist/components/watermark/watermark.js.map +1 -1
  157. package/dist/types/cascade.d.ts +5 -0
  158. package/dist/types/select.d.ts +8 -2
  159. package/dist/types/tree-select.d.ts +8 -2
  160. package/package.json +18 -18
  161. package/src/components/batch-edit/__test__/batch-edit.test.ts +1 -1
  162. package/src/components/button/__test__/button.test.ts +1 -1
  163. package/src/components/cascade/__test__/cascade.test.ts +26 -1
  164. package/src/components/cascade/use-select.ts +5 -5
  165. package/src/components/collapse/__test__/collapse.test.ts +1 -1
  166. package/src/components/condition-editor/__test__/condition-editor.test.ts +1 -1
  167. package/src/components/contextmenu/__test__/contextmenu.test.ts +1 -1
  168. package/src/components/date-picker/__test__/date-picker.test.ts +1 -1
  169. package/src/components/date-range-picker/__test__/date-range-picker.test.ts +1 -1
  170. package/src/components/drawer/__test__/drawer.test.ts +1 -1
  171. package/src/components/expression-editor/__test__/chip.test.ts +1 -1
  172. package/src/components/expression-editor/__test__/editor.test.ts +1 -1
  173. package/src/components/expression-editor/__test__/expression-editor.test.ts +1 -1
  174. package/src/components/expression-editor/__test__/mention.test.ts +1 -1
  175. package/src/components/expression-editor/__test__/model.test.ts +1 -1
  176. package/src/components/file-viewer/__test__/helper.test.ts +1 -1
  177. package/src/components/form/__test__/form.test.ts +1 -1
  178. package/src/components/form/__test__/is-field-modified.test.ts +1 -1
  179. package/src/components/form-item/__test__/form-item.test.ts +1 -1
  180. package/src/components/image-cropper/__test__/draw-crop.test.ts +1 -1
  181. package/src/components/image-cropper/__test__/image-cropper.test.ts +1 -1
  182. package/src/components/image-cropper/__test__/use-transform.test.ts +1 -1
  183. package/src/components/kanban/__test__/kanban.test.ts +1 -1
  184. package/src/components/message-confirm/__test__/message-confirm.test.ts +1 -1
  185. package/src/components/multi-tree-select/__test__/multi-tree-select.test.ts +1 -1
  186. package/src/components/number-input/__test__/number-input.test.ts +1 -1
  187. package/src/components/number-range-input/__test__/number-range-input.test.ts +1 -1
  188. package/src/components/palette/__test__/color-transform.test.ts +1 -1
  189. package/src/components/palette/__test__/palette.test.ts +1 -1
  190. package/src/components/password-input/__test__/password-input.test.ts +1 -1
  191. package/src/components/radio-group/__test__/radio-group.test.ts +1 -1
  192. package/src/components/rich-text-editor/__test__/image-node.test.ts +140 -0
  193. package/src/components/rich-text-editor/image-node.ts +84 -15
  194. package/src/components/rich-text-editor/rich-text-editor.vue +15 -4
  195. package/src/components/rich-text-editor/style.scss +35 -0
  196. package/src/components/rich-text-editor/use-image-resize.ts +168 -0
  197. package/src/components/segment/__test__/segment.test.ts +1 -1
  198. package/src/components/select/__test__/select.test.ts +68 -1
  199. package/src/components/select/select.vue +15 -5
  200. package/src/components/table/__test__/col-width.test.ts +1 -1
  201. package/src/components/table/__test__/column-style.test.ts +1 -1
  202. package/src/components/table-editor/__test__/table-editor.test.ts +1 -1
  203. package/src/components/tree-select/__test__/tree-select.test.ts +50 -1
  204. package/src/components/tree-select/tree-select.vue +28 -11
  205. package/src/types/cascade.ts +5 -0
  206. package/src/types/select.ts +8 -2
  207. package/src/types/tree-select.ts +9 -2
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { describe, expect, it } from 'vite-plus/test'
2
2
 
3
3
  import { FILE_VIEWER_KIND_LABEL, inferKind } from '../helper'
4
4
 
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { describe, expect, it } from 'vite-plus/test'
2
2
  import { createApp, defineComponent, h, nextTick, reactive, ref } from 'vue'
3
3
 
4
4
  import { UInput } from '../../input'
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { describe, expect, it } from 'vite-plus/test'
2
2
 
3
3
  import { isFieldModified } from '../is-field-modified'
4
4
 
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { describe, expect, it } from 'vite-plus/test'
2
2
  import { createApp, h, nextTick, reactive, ref } from 'vue'
3
3
 
4
4
  import { UForm } from '../../form'
@@ -1,4 +1,4 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest'
1
+ import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
2
2
 
3
3
  import { cropToResult, drawCropToCanvas } from '../draw-crop'
4
4
 
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
2
2
  import { createApp, h, nextTick, ref, shallowRef } from 'vue'
3
3
 
4
4
  import type { ImageCropperChangePayload, ImageCropperExposed } from '../../../types'
@@ -1,4 +1,4 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest'
1
+ import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
2
2
  import { createApp, shallowRef } from 'vue'
3
3
 
4
4
  import { useTransform } from '../use-transform'
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { describe, expect, it } from 'vite-plus/test'
2
2
  import { createApp, h, nextTick, ref } from 'vue'
3
3
 
4
4
  import type { KanbanColumnItem } from '../../types'
@@ -1,5 +1,5 @@
1
1
  import { zIndex } from '@veltra/utils'
2
- import { afterEach, describe, expect, it } from 'vitest'
2
+ import { afterEach, describe, expect, it } from 'vite-plus/test'
3
3
  import { nextTick } from 'vue'
4
4
 
5
5
  import { messageConfirm } from '../message-confirm'
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { describe, expect, it } from 'vite-plus/test'
2
2
  import { createApp, h, nextTick, ref, type Ref } from 'vue'
3
3
 
4
4
  import UMultiTreeSelect from '../multi-tree-select.vue'
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { describe, expect, it } from 'vite-plus/test'
2
2
  import { createApp, h, nextTick, ref } from 'vue'
3
3
 
4
4
  import UNumberInput from '../number-input.vue'
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { describe, expect, it } from 'vite-plus/test'
2
2
  import { createApp, h, nextTick, ref } from 'vue'
3
3
 
4
4
  import type { NumberRangeTuple } from '../../../types'
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { describe, expect, it } from 'vite-plus/test'
2
2
 
3
3
  import { HEX2RGBA } from '../color-transform'
4
4
 
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
2
2
  import { createApp, h, nextTick, ref } from 'vue'
3
3
 
4
4
  import UPalette from '../palette.vue'
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { describe, expect, it } from 'vite-plus/test'
2
2
  import { createApp, h, nextTick, ref } from 'vue'
3
3
 
4
4
  import UPasswordInput from '../password-input.vue'
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { describe, expect, it } from 'vite-plus/test'
2
2
  import { createApp, h, nextTick, ref } from 'vue'
3
3
 
4
4
  import URadioGroup from '../radio-group.vue'
@@ -0,0 +1,140 @@
1
+ import { createEditor, type LexicalEditor } from 'lexical'
2
+ import { afterAll, beforeAll, describe, expect, it } from 'vite-plus/test'
3
+
4
+ import { $createImageNode, $isImageNode, ImageNode } from '../image-node'
5
+
6
+ let editor: LexicalEditor
7
+ let container: HTMLDivElement
8
+
9
+ beforeAll(() => {
10
+ container = document.createElement('div')
11
+ document.body.appendChild(container)
12
+ editor = createEditor({
13
+ namespace: 'test',
14
+ nodes: [ImageNode],
15
+ onError: (error) => {
16
+ throw error
17
+ }
18
+ })
19
+ editor.setRootElement(container)
20
+ })
21
+
22
+ afterAll(() => {
23
+ editor.setRootElement(null)
24
+ container.remove()
25
+ })
26
+
27
+ function createImage(options: { src?: string; width?: number; height?: number } = {}) {
28
+ return $createImageNode({
29
+ src: options.src ?? 'https://cdn.example.com/a.png',
30
+ alt: '示例图',
31
+ width: options.width,
32
+ height: options.height
33
+ })
34
+ }
35
+
36
+ /** 节点构造与写方法要求可写上下文(Lexical 0.50 在 read 回调内禁止 new 节点) */
37
+ function inUpdate<T>(fn: () => T): T {
38
+ let result!: T
39
+ editor.update(() => {
40
+ result = fn()
41
+ })
42
+ return result
43
+ }
44
+
45
+ describe('ImageNode', () => {
46
+ it('createDOM 返回 wrapper + img + 缩放手柄,未设置尺寸时无内联宽高', () => {
47
+ const { wrapper, key } = inUpdate(() => {
48
+ const node = createImage()
49
+ return { wrapper: node.createDOM(), key: node.getKey() }
50
+ })
51
+
52
+ expect(wrapper.className).toBe('u-rte-image-wrap')
53
+ expect(wrapper.getAttribute('data-rte-image-key')).toBe(key)
54
+
55
+ const img = wrapper.querySelector('img')
56
+ expect(img?.className).toBe('u-rte-image')
57
+ expect(img?.getAttribute('src')).toBe('https://cdn.example.com/a.png')
58
+ expect(img?.getAttribute('alt')).toBe('示例图')
59
+ expect(img?.style.width).toBe('')
60
+ expect(img?.style.height).toBe('')
61
+
62
+ expect(wrapper.querySelector('.u-rte-image-resizer')).not.toBeNull()
63
+ })
64
+
65
+ it('createDOM 应用节点上的显示尺寸', () => {
66
+ const img = inUpdate(() =>
67
+ createImage({ width: 320, height: 180 }).createDOM().querySelector('img')!
68
+ )
69
+
70
+ expect(img.style.width).toBe('320px')
71
+ expect(img.style.height).toBe('180px')
72
+ })
73
+
74
+ it('updateDOM 在尺寸变化时同步 img 内联样式', () => {
75
+ const { img } = inUpdate(() => {
76
+ const node = createImage({ width: 320, height: 180 })
77
+ const wrapper = node.createDOM()
78
+ const resized = createImage({ width: 480, height: 270 })
79
+ resized.updateDOM(node, wrapper)
80
+ return { img: wrapper.querySelector('img')! }
81
+ })
82
+
83
+ expect(img.style.width).toBe('480px')
84
+ expect(img.style.height).toBe('270px')
85
+ })
86
+
87
+ it('exportDOM 仅在设置尺寸时输出 width / height 属性', () => {
88
+ const { sizedEl, unsizedEl } = inUpdate(() => ({
89
+ sizedEl: createImage({ width: 480, height: 270 }).exportDOM().element as HTMLImageElement,
90
+ unsizedEl: createImage().exportDOM().element as HTMLImageElement
91
+ }))
92
+
93
+ expect(sizedEl.getAttribute('width')).toBe('480')
94
+ expect(sizedEl.getAttribute('height')).toBe('270')
95
+ expect(sizedEl.className).toBe('u-rte-image')
96
+
97
+ expect(unsizedEl.hasAttribute('width')).toBe(false)
98
+ expect(unsizedEl.hasAttribute('height')).toBe(false)
99
+ })
100
+
101
+ it('importDOM 读取 img 的 width / height 属性,缺失时为 0', () => {
102
+ const { sized, unsized } = inUpdate(() => {
103
+ const conversion = ImageNode.importDOM()!.img().conversion
104
+ const dom = document.createElement('img')
105
+ dom.setAttribute('src', 'https://cdn.example.com/a.png')
106
+ dom.setAttribute('width', '640')
107
+ dom.setAttribute('height', '360')
108
+
109
+ const noSizeDom = document.createElement('img')
110
+ noSizeDom.setAttribute('src', 'https://cdn.example.com/b.png')
111
+
112
+ return { sized: conversion(dom).node, unsized: conversion(noSizeDom).node }
113
+ })
114
+
115
+ expect($isImageNode(sized)).toBe(true)
116
+ expect(sized!.exportJSON()).toMatchObject({
117
+ src: 'https://cdn.example.com/a.png',
118
+ width: 640,
119
+ height: 360
120
+ })
121
+ expect(unsized!.exportJSON()).toMatchObject({ width: 0, height: 0 })
122
+ })
123
+
124
+ it('exportJSON / importJSON roundtrip 保留尺寸', () => {
125
+ const json = inUpdate(() => createImage({ width: 480, height: 270 }).exportJSON())
126
+
127
+ const restored = inUpdate(() => ImageNode.importJSON(json).exportJSON())
128
+ expect(restored).toEqual(json)
129
+ })
130
+
131
+ it('editor update 中 setWidthAndHeight 写入尺寸', () => {
132
+ const json = inUpdate(() => {
133
+ const node = createImage()
134
+ node.setWidthAndHeight(200, 100)
135
+ return node.exportJSON()
136
+ })
137
+
138
+ expect(json).toMatchObject({ width: 200, height: 100 })
139
+ })
140
+ })
@@ -18,44 +18,68 @@ function nextImageId(): string {
18
18
  }
19
19
 
20
20
  export interface SerializedImageNode extends Spread<
21
- { src: string; alt: string; id: string },
21
+ { src: string; alt: string; id: string; width: number; height: number },
22
22
  SerializedLexicalNode
23
23
  > {}
24
24
 
25
25
  /**
26
26
  * 图片节点:插入时 src 为本地 objectURL、__file 持有原始文件,提交前由宿主调用
27
27
  * uploadImages 换成服务器地址。__file 不参与序列化,仅会话内有效。
28
+ * width / height 为显示尺寸(px),0 表示未设置、按图片自然尺寸(受 max-width 约束)。
28
29
  * 本仓库为手写 Vue 绑定(无 React 适配层),不走 decorate 渲染管道,
29
- * createDOM 直接返回 <img> 元素。
30
+ * createDOM 返回 wrapper:`<span><img><span 手柄/></span>`,选中与拖拽缩放
31
+ * 由组件层(use-image-resize)基于 data-rte-image-key 驱动。
30
32
  */
31
33
  export class ImageNode extends DecoratorNode<void> {
32
34
  __src: string
33
35
  __alt: string
34
36
  __id: string
35
37
  __file: File | null
38
+ __width: number
39
+ __height: number
36
40
 
37
41
  static override getType(): string {
38
42
  return 'u-image'
39
43
  }
40
44
 
41
45
  static override clone(node: ImageNode): ImageNode {
42
- return new ImageNode(node.__src, node.__alt, node.__id, node.__file, node.__key)
46
+ return new ImageNode(
47
+ node.__src,
48
+ node.__alt,
49
+ node.__id,
50
+ node.__file,
51
+ node.__width,
52
+ node.__height,
53
+ node.__key
54
+ )
43
55
  }
44
56
 
45
57
  static override importJSON(serializedNode: SerializedImageNode): ImageNode {
46
58
  return $createImageNode({
47
59
  src: serializedNode.src,
48
60
  alt: serializedNode.alt,
49
- id: serializedNode.id
61
+ id: serializedNode.id,
62
+ width: serializedNode.width,
63
+ height: serializedNode.height
50
64
  })
51
65
  }
52
66
 
53
- constructor(src: string, alt = '', id = nextImageId(), file: File | null = null, key?: NodeKey) {
67
+ constructor(
68
+ src: string,
69
+ alt = '',
70
+ id = nextImageId(),
71
+ file: File | null = null,
72
+ width = 0,
73
+ height = 0,
74
+ key?: NodeKey
75
+ ) {
54
76
  super(key)
55
77
  this.__src = src
56
78
  this.__alt = alt
57
79
  this.__id = id
58
80
  this.__file = file
81
+ this.__width = width
82
+ this.__height = height
59
83
  }
60
84
 
61
85
  static override importDOM(): DOMConversionMap | null {
@@ -68,26 +92,58 @@ export class ImageNode extends DecoratorNode<void> {
68
92
  element.setAttribute('class', 'u-rte-image')
69
93
  element.setAttribute('alt', this.__alt)
70
94
  element.setAttribute('data-rte-image-id', this.__id)
95
+ if (this.__width) element.setAttribute('width', String(this.__width))
96
+ if (this.__height) element.setAttribute('height', String(this.__height))
71
97
  return { element }
72
98
  }
73
99
 
74
100
  override exportJSON(): SerializedImageNode {
75
- return { type: 'u-image', version: 1, src: this.__src, alt: this.__alt, id: this.__id }
101
+ return {
102
+ type: 'u-image',
103
+ version: 1,
104
+ src: this.__src,
105
+ alt: this.__alt,
106
+ id: this.__id,
107
+ width: this.__width,
108
+ height: this.__height
109
+ }
76
110
  }
77
111
 
78
112
  override createDOM(): HTMLElement {
79
- const element = document.createElement('img')
80
- element.className = 'u-rte-image'
81
- element.src = this.__src
82
- element.alt = this.__alt
83
- element.draggable = false
84
- return element
113
+ const wrapper = document.createElement('span')
114
+ wrapper.className = 'u-rte-image-wrap'
115
+ wrapper.contentEditable = 'false'
116
+ wrapper.setAttribute('data-rte-image-key', this.__key)
117
+
118
+ const img = document.createElement('img')
119
+ img.className = 'u-rte-image'
120
+ img.src = this.__src
121
+ img.alt = this.__alt
122
+ img.draggable = false
123
+ if (this.__width) img.style.width = `${this.__width}px`
124
+ if (this.__height) img.style.height = `${this.__height}px`
125
+
126
+ const handle = document.createElement('span')
127
+ handle.className = 'u-rte-image-resizer'
128
+ handle.setAttribute('aria-hidden', 'true')
129
+
130
+ wrapper.append(img, handle)
131
+ return wrapper
85
132
  }
86
133
 
87
134
  override updateDOM(prevNode: ImageNode, element: HTMLElement): boolean {
88
- const img = element as HTMLImageElement
135
+ const img = element.querySelector('img')
136
+ if (!img) return false
89
137
  if (prevNode.__src !== this.__src) img.src = this.__src
90
138
  if (prevNode.__alt !== this.__alt) img.alt = this.__alt
139
+ if (prevNode.__width !== this.__width) {
140
+ if (this.__width) img.style.width = `${this.__width}px`
141
+ else img.style.removeProperty('width')
142
+ }
143
+ if (prevNode.__height !== this.__height) {
144
+ if (this.__height) img.style.height = `${this.__height}px`
145
+ else img.style.removeProperty('height')
146
+ }
91
147
  return false
92
148
  }
93
149
 
@@ -110,6 +166,13 @@ export class ImageNode extends DecoratorNode<void> {
110
166
  self.__src = src
111
167
  self.__file = null
112
168
  }
169
+
170
+ /** 写入显示尺寸(拖拽缩放结束时调用),px,0 表示恢复自然尺寸 */
171
+ setWidthAndHeight(width: number, height: number): void {
172
+ const self = this.getWritable()
173
+ self.__width = width
174
+ self.__height = height
175
+ }
113
176
  }
114
177
 
115
178
  function convertImageElement(domNode: HTMLElement): DOMConversionOutput {
@@ -119,7 +182,9 @@ function convertImageElement(domNode: HTMLElement): DOMConversionOutput {
119
182
  node: $createImageNode({
120
183
  src,
121
184
  alt: domNode.getAttribute('alt') ?? '',
122
- id: domNode.getAttribute('data-rte-image-id') ?? undefined
185
+ id: domNode.getAttribute('data-rte-image-id') ?? undefined,
186
+ width: parseInt(domNode.getAttribute('width') ?? '') || 0,
187
+ height: parseInt(domNode.getAttribute('height') ?? '') || 0
123
188
  })
124
189
  }
125
190
  }
@@ -129,12 +194,16 @@ export function $createImageNode(options: {
129
194
  alt?: string
130
195
  id?: string
131
196
  file?: File | null
197
+ width?: number
198
+ height?: number
132
199
  }): ImageNode {
133
200
  return new ImageNode(
134
201
  options.src,
135
202
  options.alt ?? '',
136
203
  options.id ?? nextImageId(),
137
- options.file ?? null
204
+ options.file ?? null,
205
+ options.width ?? 0,
206
+ options.height ?? 0
138
207
  )
139
208
  }
140
209
 
@@ -88,6 +88,8 @@ import {
88
88
  $getRoot,
89
89
  $createParagraphNode,
90
90
  $nodesOfType,
91
+ $getSelection,
92
+ $isNodeSelection,
91
93
  $setSelection,
92
94
  COMMAND_PRIORITY_HIGH,
93
95
  DROP_COMMAND,
@@ -110,6 +112,7 @@ import {
110
112
  revokeImageObjectUrls
111
113
  } from './image-node'
112
114
  import Toolbar from './toolbar.vue'
115
+ import { useImageResize } from './use-image-resize'
113
116
 
114
117
  const props = withDefaults(defineProps<RichTextEditorProps>(), {
115
118
  disabled: undefined,
@@ -151,6 +154,11 @@ const showPlaceholder = ref(true)
151
154
  let isComposing = false
152
155
  let cleanupFns: (() => void)[] = []
153
156
 
157
+ const acceptsImage = () => props.image && !isDisabled.value && !isReadonly.value
158
+
159
+ // 图片选中 / 拖拽缩放交互
160
+ useImageResize(editor, acceptsImage)
161
+
154
162
  function initEditor() {
155
163
  if (!editorContainer.value) return
156
164
 
@@ -172,8 +180,6 @@ function initEditor() {
172
180
  cleanupFns.push(registerHistory(editorInstance, createEmptyHistoryState(), 300))
173
181
 
174
182
  // Image input: paste & drop
175
- const acceptsImage = () => props.image && !isDisabled.value && !isReadonly.value
176
-
177
183
  cleanupFns.push(
178
184
  editorInstance.registerCommand(
179
185
  PASTE_COMMAND,
@@ -353,8 +359,13 @@ watch(model, (newVal) => {
353
359
 
354
360
  // Watch disabled/readonly changes
355
361
  watch([isDisabled, isReadonly], () => {
356
- if (editor.value) {
357
- editor.value.setEditable(!isDisabled.value && !isReadonly.value)
362
+ if (!editor.value) return
363
+ editor.value.setEditable(!isDisabled.value && !isReadonly.value)
364
+ if (isDisabled.value || isReadonly.value) {
365
+ // 切出可编辑态时取消可能残留的图片选中
366
+ editor.value.update(() => {
367
+ if ($isNodeSelection($getSelection())) $setSelection(null)
368
+ })
358
369
  }
359
370
  })
360
371
 
@@ -388,3 +388,38 @@ $root-name: rich-text-editor;
388
388
  height: auto;
389
389
  vertical-align: bottom;
390
390
  }
391
+
392
+ // 编辑器内图片 wrapper:承载选中态描边与缩放手柄(宿主直接渲染导出 HTML 时无此结构)
393
+ .u-rte-image-wrap {
394
+ position: relative;
395
+ display: inline-block;
396
+ max-width: 100%;
397
+ vertical-align: bottom;
398
+ line-height: 0;
399
+
400
+ .u-rte-image {
401
+ display: block;
402
+ }
403
+
404
+ &.is-selected .u-rte-image {
405
+ outline: 2px solid fn.use-var(color, primary);
406
+ border-radius: 2px;
407
+ }
408
+ }
409
+
410
+ .u-rte-image-resizer {
411
+ position: absolute;
412
+ right: -5px;
413
+ bottom: -5px;
414
+ display: none;
415
+ width: 10px;
416
+ height: 10px;
417
+ border-radius: 50%;
418
+ background-color: fn.use-var(bg-color, top);
419
+ border: 2px solid fn.use-var(color, primary);
420
+ cursor: nwse-resize;
421
+
422
+ .u-rte-image-wrap.is-selected & {
423
+ display: block;
424
+ }
425
+ }