@tmagic/editor 1.8.0-manmanyu.8 → 1.8.0

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 (281) hide show
  1. package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +22 -13
  2. package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +10 -5
  3. package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +7 -6
  4. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +21 -119
  5. package/dist/es/components/ContentMenu.vue_vue_type_script_setup_true_lang.js +21 -5
  6. package/dist/es/components/FloatingBox.vue_vue_type_script_setup_true_lang.js +62 -16
  7. package/dist/es/components/Icon.vue_vue_type_script_setup_true_lang.js +1 -1
  8. package/dist/es/components/Resizer.vue_vue_type_script_setup_true_lang.js +3 -2
  9. package/dist/es/components/ScrollBar.js +0 -1
  10. package/dist/es/components/ScrollBar.vue_vue_type_script_setup_true_lang.js +5 -4
  11. package/dist/es/components/ScrollViewer.vue_vue_type_script_setup_true_lang.js +1 -1
  12. package/dist/es/components/SearchInput.vue_vue_type_script_setup_true_lang.js +1 -1
  13. package/dist/es/components/SplitView.vue_vue_type_script_setup_true_lang.js +1 -1
  14. package/dist/es/components/ToolButton.vue_vue_type_script_setup_true_lang.js +1 -1
  15. package/dist/es/components/Tree.vue_vue_type_script_setup_true_lang.js +1 -1
  16. package/dist/es/components/TreeNode.vue_vue_type_script_setup_true_lang.js +1 -1
  17. package/dist/es/components/ViewForm.js +5 -0
  18. package/dist/es/components/ViewForm.vue_vue_type_script_setup_true_lang.js +56 -0
  19. package/dist/es/editorProps.js +7 -1
  20. package/dist/es/fields/Code.vue_vue_type_script_setup_true_lang.js +1 -1
  21. package/dist/es/fields/CodeLink.vue_vue_type_script_setup_true_lang.js +1 -1
  22. package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +30 -113
  23. package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +2 -2
  24. package/dist/es/fields/CondOpSelect.vue_vue_type_script_setup_true_lang.js +7 -19
  25. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +7 -6
  26. package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +4 -2
  27. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +12 -4
  28. package/dist/es/fields/DataSourceInput.vue_vue_type_script_setup_true_lang.js +9 -20
  29. package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +2 -2
  30. package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +5 -3
  31. package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +9 -4
  32. package/dist/es/fields/DataSourceSelect.vue_vue_type_script_setup_true_lang.js +2 -2
  33. package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +10 -110
  34. package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +52 -363
  35. package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +1 -1
  36. package/dist/es/fields/PageFragmentSelect.vue_vue_type_script_setup_true_lang.js +1 -1
  37. package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +3 -6
  38. package/dist/es/fields/StyleSetter/components/BackgroundPosition.vue_vue_type_script_setup_true_lang.js +1 -1
  39. package/dist/es/fields/StyleSetter/components/Border.vue_vue_type_script_setup_true_lang.js +4 -35
  40. package/dist/es/fields/StyleSetter/components/Box.vue_vue_type_script_setup_true_lang.js +3 -3
  41. package/dist/es/fields/StyleSetter/configs.js +663 -0
  42. package/dist/es/fields/StyleSetter/icons/align-items/Center.js +1 -1
  43. package/dist/es/fields/StyleSetter/icons/align-items/FlexEnd.js +1 -1
  44. package/dist/es/fields/StyleSetter/icons/align-items/FlexStart.js +1 -1
  45. package/dist/es/fields/StyleSetter/icons/align-items/SpaceAround.js +1 -1
  46. package/dist/es/fields/StyleSetter/icons/align-items/SpaceBetween.js +1 -1
  47. package/dist/es/fields/StyleSetter/icons/background-repeat/NoRepeat.js +1 -1
  48. package/dist/es/fields/StyleSetter/icons/background-repeat/Repeat.js +1 -1
  49. package/dist/es/fields/StyleSetter/icons/background-repeat/RepeatX.js +1 -1
  50. package/dist/es/fields/StyleSetter/icons/background-repeat/RepeatY.js +1 -1
  51. package/dist/es/fields/StyleSetter/icons/display/Block.js +1 -1
  52. package/dist/es/fields/StyleSetter/icons/display/Flex.js +1 -1
  53. package/dist/es/fields/StyleSetter/icons/display/Inline.js +1 -1
  54. package/dist/es/fields/StyleSetter/icons/display/InlineBlock.js +1 -1
  55. package/dist/es/fields/StyleSetter/icons/display/None.js +1 -1
  56. package/dist/es/fields/StyleSetter/icons/flex-direction/Column.js +1 -1
  57. package/dist/es/fields/StyleSetter/icons/flex-direction/ColumnReverse.js +1 -1
  58. package/dist/es/fields/StyleSetter/icons/flex-direction/Row.js +1 -1
  59. package/dist/es/fields/StyleSetter/icons/flex-direction/RowReverse.js +1 -1
  60. package/dist/es/fields/StyleSetter/icons/justify-content/Center.js +1 -1
  61. package/dist/es/fields/StyleSetter/icons/justify-content/FlexEnd.js +1 -1
  62. package/dist/es/fields/StyleSetter/icons/justify-content/FlexStart.js +1 -1
  63. package/dist/es/fields/StyleSetter/icons/justify-content/SpaceAround.js +1 -1
  64. package/dist/es/fields/StyleSetter/icons/justify-content/SpaceBetween.js +1 -1
  65. package/dist/es/fields/StyleSetter/icons/text-align/Center.js +1 -1
  66. package/dist/es/fields/StyleSetter/icons/text-align/Left.js +1 -1
  67. package/dist/es/fields/StyleSetter/icons/text-align/Right.js +1 -1
  68. package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +29 -106
  69. package/dist/es/fields/StyleSetter/pro/Border.vue_vue_type_script_setup_true_lang.js +8 -11
  70. package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +30 -89
  71. package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +29 -297
  72. package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +29 -82
  73. package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +8 -24
  74. package/dist/es/fields/UISelect.js +0 -1
  75. package/dist/es/fields/UISelect.vue_vue_type_script_setup_true_lang.js +1 -1
  76. package/dist/es/fields/configs/codeSelect.js +98 -0
  77. package/dist/es/fields/configs/displayConds.js +158 -0
  78. package/dist/es/fields/configs/eventSelect.js +225 -0
  79. package/dist/es/fields/configs/stickyAddButton.js +21 -0
  80. package/dist/es/fields/headless-validation.js +139 -0
  81. package/dist/es/headless.js +2 -0
  82. package/dist/es/hooks/use-code-block-edit.js +4 -2
  83. package/dist/es/hooks/use-compare-form.js +101 -0
  84. package/dist/es/hooks/use-form-context.js +29 -0
  85. package/dist/es/hooks/use-stage.js +1 -0
  86. package/dist/es/icons/CenterIcon.vue_vue_type_script_setup_true_lang.js +1 -1
  87. package/dist/es/icons/CodeIcon.vue_vue_type_script_setup_true_lang.js +1 -1
  88. package/dist/es/icons/DatasourceIcon.js +1 -1
  89. package/dist/es/icons/FolderMinusIcon.vue_vue_type_script_setup_true_lang.js +1 -1
  90. package/dist/es/index.js +17 -9
  91. package/dist/es/initService.js +52 -16
  92. package/dist/es/layouts/AddPageBox.vue_vue_type_script_setup_true_lang.js +1 -1
  93. package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +26 -10
  94. package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +11 -8
  95. package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +3 -3
  96. package/dist/es/layouts/NavMenuColumn.vue_vue_type_script_setup_true_lang.js +1 -1
  97. package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +1 -1
  98. package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +8 -5
  99. package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +1 -1
  100. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +26 -10
  101. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +36 -28
  102. package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +1 -1
  103. package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +1 -1
  104. package/dist/es/layouts/history-list/composables.js +1 -1
  105. package/dist/es/layouts/history-list/useHistoryList.js +4 -4
  106. package/dist/es/layouts/history-list/useHistoryRevert.js +38 -33
  107. package/dist/es/layouts/page-bar/AddButton.vue_vue_type_script_setup_true_lang.js +1 -1
  108. package/dist/es/layouts/page-bar/PageBar.vue_vue_type_script_setup_true_lang.js +1 -1
  109. package/dist/es/layouts/page-bar/PageBarScrollContainer.vue_vue_type_script_setup_true_lang.js +1 -1
  110. package/dist/es/layouts/page-bar/PageList.vue_vue_type_script_setup_true_lang.js +1 -1
  111. package/dist/es/layouts/page-bar/Search.vue_vue_type_script_setup_true_lang.js +2 -2
  112. package/dist/es/layouts/props-panel/FormPanel.vue_vue_type_script_setup_true_lang.js +50 -20
  113. package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +53 -35
  114. package/dist/es/layouts/props-panel/use-style-panel.js +3 -2
  115. package/dist/es/layouts/sidebar/ComponentListPanel.vue_vue_type_script_setup_true_lang.js +3 -3
  116. package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +21 -36
  117. package/dist/es/layouts/sidebar/code-block/CodeBlockList.vue_vue_type_script_setup_true_lang.js +1 -1
  118. package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +4 -4
  119. package/dist/es/layouts/sidebar/code-block/useContentMenu.js +19 -4
  120. package/dist/es/layouts/sidebar/data-source/DataSourceAddButton.vue_vue_type_script_setup_true_lang.js +1 -1
  121. package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +9 -4
  122. package/dist/es/layouts/sidebar/data-source/DataSourceList.vue_vue_type_script_setup_true_lang.js +1 -1
  123. package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +4 -4
  124. package/dist/es/layouts/sidebar/data-source/useContentMenu.js +19 -4
  125. package/dist/es/layouts/sidebar/layer/LayerMenu.vue_vue_type_script_setup_true_lang.js +3 -3
  126. package/dist/es/layouts/sidebar/layer/LayerNodeContent.js +5 -0
  127. package/dist/es/layouts/sidebar/layer/LayerNodeContent.vue_vue_type_script_setup_true_lang.js +46 -0
  128. package/dist/es/layouts/sidebar/layer/LayerNodeTool.vue_vue_type_script_setup_true_lang.js +1 -1
  129. package/dist/es/layouts/sidebar/layer/LayerPanel.vue_vue_type_script_setup_true_lang.js +6 -4
  130. package/dist/es/layouts/sidebar/layer/use-click.js +3 -3
  131. package/dist/es/layouts/sidebar/layer/use-drag.js +11 -10
  132. package/dist/es/layouts/sidebar/layer/use-node-status.js +2 -2
  133. package/dist/es/layouts/workspace/Breadcrumb.vue_vue_type_script_setup_true_lang.js +1 -1
  134. package/dist/es/layouts/workspace/Workspace.vue_vue_type_script_setup_true_lang.js +1 -1
  135. package/dist/es/layouts/workspace/viewer/NodeListMenu.vue_vue_type_script_setup_true_lang.js +9 -3
  136. package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +55 -9
  137. package/dist/es/layouts/workspace/viewer/StageOverlay.vue_vue_type_script_setup_true_lang.js +1 -1
  138. package/dist/es/layouts/workspace/viewer/ViewerMenu.vue_vue_type_script_setup_true_lang.js +8 -8
  139. package/dist/es/plugin.js +35 -23
  140. package/dist/es/services/codeBlock.js +40 -31
  141. package/dist/es/services/dataSource.js +46 -34
  142. package/dist/es/services/dep.js +162 -41
  143. package/dist/es/services/editor.js +373 -137
  144. package/dist/es/services/events.js +4 -4
  145. package/dist/es/services/history.js +275 -392
  146. package/dist/es/services/keybinding.js +3 -3
  147. package/dist/es/services/props.js +2 -1
  148. package/dist/es/services/storage.js +2 -2
  149. package/dist/es/services/ui.js +7 -2
  150. package/dist/es/style.css +277 -114
  151. package/dist/es/utils/code-block.js +14 -7
  152. package/dist/es/utils/config.js +4 -1
  153. package/dist/es/utils/const.js +2 -2
  154. package/dist/es/utils/content-menu.js +2 -2
  155. package/dist/es/utils/data-source/index.js +34 -12
  156. package/dist/es/utils/dep/collect-worker-client.js +154 -0
  157. package/dist/es/utils/dep/idle-task.js +70 -22
  158. package/dist/es/utils/dep/worker.js +1 -1
  159. package/dist/es/utils/editor.js +4 -2
  160. package/dist/es/utils/event.js +135 -0
  161. package/dist/es/utils/history.js +35 -47
  162. package/dist/es/utils/monaco-editor.js +22 -6
  163. package/dist/es/utils/operator.js +2 -2
  164. package/dist/es/utils/props.js +50 -106
  165. package/dist/es/utils/type-match-rules.js +356 -0
  166. package/dist/style.css +277 -114
  167. package/dist/themes/magic-admin.css +978 -175
  168. package/dist/tmagic-editor-headless.umd.cjs +10880 -0
  169. package/dist/tmagic-editor.umd.cjs +13047 -11363
  170. package/package.json +18 -13
  171. package/src/Editor.vue +23 -6
  172. package/src/components/CodeBlockEditor.vue +3 -0
  173. package/src/components/CodeParams.vue +10 -4
  174. package/src/components/CompareForm.vue +20 -190
  175. package/src/components/ContentMenu.vue +34 -6
  176. package/src/components/FloatingBox.vue +71 -7
  177. package/src/components/ScrollBar.vue +0 -67
  178. package/src/components/ViewForm.vue +54 -0
  179. package/src/editorProps.ts +23 -10
  180. package/src/fields/Code.vue +1 -1
  181. package/src/fields/CodeSelect.vue +20 -113
  182. package/src/fields/CodeSelectCol.vue +1 -1
  183. package/src/fields/CondOpSelect.vue +5 -26
  184. package/src/fields/DataSourceFieldSelect/FieldSelect.vue +2 -2
  185. package/src/fields/DataSourceFieldSelect/Index.vue +4 -1
  186. package/src/fields/DataSourceFields.vue +5 -1
  187. package/src/fields/DataSourceInput.vue +10 -28
  188. package/src/fields/DataSourceMethodSelect.vue +1 -1
  189. package/src/fields/DataSourceMethods.vue +6 -2
  190. package/src/fields/DataSourceMocks.vue +3 -0
  191. package/src/fields/DataSourceSelect.vue +1 -1
  192. package/src/fields/DisplayConds.vue +17 -139
  193. package/src/fields/EventSelect.vue +29 -416
  194. package/src/fields/KeyValue.vue +1 -1
  195. package/src/fields/StyleSetter/Index.vue +2 -8
  196. package/src/fields/StyleSetter/components/Border.vue +4 -38
  197. package/src/fields/StyleSetter/components/Box.vue +1 -1
  198. package/src/fields/StyleSetter/components/Position.vue +1 -1
  199. package/src/fields/StyleSetter/configs.ts +690 -0
  200. package/src/fields/StyleSetter/pro/Background.vue +9 -64
  201. package/src/fields/StyleSetter/pro/Border.vue +5 -16
  202. package/src/fields/StyleSetter/pro/Font.vue +11 -71
  203. package/src/fields/StyleSetter/pro/Layout.vue +10 -230
  204. package/src/fields/StyleSetter/pro/Position.vue +11 -82
  205. package/src/fields/StyleSetter/pro/Transform.vue +6 -34
  206. package/src/fields/UISelect.vue +0 -12
  207. package/src/fields/configs/codeSelect.ts +127 -0
  208. package/src/fields/configs/displayConds.ts +184 -0
  209. package/src/fields/configs/eventSelect.ts +309 -0
  210. package/src/fields/configs/stickyAddButton.ts +32 -0
  211. package/src/fields/headless-validation.ts +177 -0
  212. package/src/headless.ts +33 -0
  213. package/src/hooks/index.ts +1 -0
  214. package/src/hooks/use-compare-form.ts +166 -0
  215. package/src/hooks/use-form-context.ts +56 -0
  216. package/src/hooks/use-stage.ts +1 -0
  217. package/src/index.ts +9 -0
  218. package/src/initService.ts +73 -18
  219. package/src/layouts/CodeEditor.vue +32 -6
  220. package/src/layouts/Framework.vue +7 -6
  221. package/src/layouts/history-list/BucketTab.vue +2 -2
  222. package/src/layouts/history-list/HistoryDiffDialog.vue +33 -15
  223. package/src/layouts/history-list/HistoryListPanel.vue +16 -18
  224. package/src/layouts/history-list/useHistoryRevert.ts +57 -41
  225. package/src/layouts/props-panel/FormPanel.vue +75 -20
  226. package/src/layouts/props-panel/PropsPanel.vue +81 -30
  227. package/src/layouts/props-panel/use-style-panel.ts +4 -3
  228. package/src/layouts/sidebar/ComponentListPanel.vue +6 -2
  229. package/src/layouts/sidebar/Sidebar.vue +5 -3
  230. package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +5 -2
  231. package/src/layouts/sidebar/code-block/useContentMenu.ts +14 -2
  232. package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +3 -0
  233. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +3 -3
  234. package/src/layouts/sidebar/data-source/useContentMenu.ts +14 -2
  235. package/src/layouts/sidebar/layer/LayerMenu.vue +2 -2
  236. package/src/layouts/sidebar/layer/LayerNodeContent.vue +57 -0
  237. package/src/layouts/sidebar/layer/LayerPanel.vue +5 -2
  238. package/src/layouts/sidebar/layer/use-click.ts +3 -3
  239. package/src/layouts/sidebar/layer/use-node-status.ts +2 -2
  240. package/src/layouts/workspace/viewer/NodeListMenu.vue +4 -1
  241. package/src/layouts/workspace/viewer/Stage.vue +80 -10
  242. package/src/layouts/workspace/viewer/ViewerMenu.vue +7 -7
  243. package/src/plugin.ts +35 -25
  244. package/src/services/codeBlock.ts +4 -0
  245. package/src/services/dataSource.ts +3 -0
  246. package/src/services/dep.ts +292 -47
  247. package/src/services/editor.ts +383 -120
  248. package/src/services/events.ts +8 -7
  249. package/src/services/history.ts +22 -1
  250. package/src/services/keybinding.ts +3 -3
  251. package/src/services/ui.ts +26 -5
  252. package/src/theme/code-block.scss +0 -37
  253. package/src/theme/display-conds.scss +4 -0
  254. package/src/theme/event.scss +25 -33
  255. package/src/theme/floating-box.scss +9 -1
  256. package/src/theme/framework.scss +4 -1
  257. package/src/theme/layer-node-content.scss +14 -0
  258. package/src/theme/nav-menu.scss +5 -1
  259. package/src/theme/props-panel.scss +17 -3
  260. package/src/theme/scroll-bar.scss +64 -0
  261. package/src/theme/theme.scss +3 -0
  262. package/src/theme/themes/magic-admin/index.scss +109 -9
  263. package/src/theme/ui-select.scss +9 -0
  264. package/src/type.ts +216 -6
  265. package/src/utils/code-block.ts +1 -0
  266. package/src/utils/config.ts +9 -1
  267. package/src/utils/const.ts +3 -3
  268. package/src/utils/content-menu.ts +2 -2
  269. package/src/utils/data-source/index.ts +31 -12
  270. package/src/utils/dep/collect-worker-client.ts +215 -0
  271. package/src/utils/dep/idle-task.ts +104 -35
  272. package/src/utils/dep/worker.ts +113 -9
  273. package/src/utils/event.ts +237 -0
  274. package/src/utils/index.ts +2 -0
  275. package/src/utils/monaco-editor.ts +27 -0
  276. package/src/utils/operator.ts +2 -2
  277. package/src/utils/props.ts +61 -122
  278. package/src/utils/type-match-rules.ts +697 -0
  279. package/types/headless.d.ts +17 -0
  280. package/types/index.d.ts +1494 -978
  281. package/src/fields/StyleSetter/icons/align-items/index copy.ts +0 -5
@@ -16,7 +16,7 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import { reactive, toRaw } from 'vue';
19
+ import { nextTick, reactive, toRaw } from 'vue';
20
20
  import { cloneDeep, isEmpty, isEqual, isObject, mergeWith, uniq } from 'lodash-es';
21
21
 
22
22
  import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
@@ -30,7 +30,9 @@ import {
30
30
  guid,
31
31
  isPage,
32
32
  isPageFragment,
33
+ isPageOrFragment,
33
34
  setValueByKeyPath,
35
+ traverseNode,
34
36
  } from '@tmagic/utils';
35
37
 
36
38
  import BaseService from '@editor/services//BaseService';
@@ -47,11 +49,14 @@ import type {
47
49
  EditorNodeInfo,
48
50
  HistoryOpSource,
49
51
  HistoryOpType,
52
+ NodeInvalidInfo,
53
+ NodeInvalidSource,
50
54
  PastePosition,
51
55
  StepDiffItem,
52
56
  StepValue,
53
57
  StoreState,
54
58
  StoreStateKey,
59
+ UpdateOptions,
55
60
  } from '@editor/type';
56
61
  import { canUsePluginMethods, LayerOffset, Layout } from '@editor/type';
57
62
  import {
@@ -79,6 +84,10 @@ import { beforePaste, getAddParent } from '@editor/utils/operator';
79
84
 
80
85
  type MoveItem = { node: MNode; parent: MContainer; pageForOp: { name: string; id: Id } | null };
81
86
 
87
+ /** 历史插回时把记录的下标收敛到 [0, length],越界(含未记录)一律追加到末尾 */
88
+ const clampIndex = (index: number | undefined, length: number): number =>
89
+ typeof index === 'number' && index >= 0 && index <= length ? index : length;
90
+
82
91
  /**
83
92
  * 把「变更前后节点快照」列表归一成 update 类型的 {@link StepDiffItem} 列表,供 {@link StepValue.diff} 使用。
84
93
  * `changeRecords` 来自 form 端的 propPath/value 列表,撤销/重做时只对这些 propPath 做局部更新;
@@ -104,18 +113,29 @@ class Editor extends BaseService {
104
113
  stageLoading: true,
105
114
  highlightNode: null,
106
115
  modifiedNodeIds: new Map(),
116
+ invalidNodeIds: new Map(),
107
117
  pageLength: 0,
108
118
  pageFragmentLength: 0,
109
119
  disabledMultiSelect: false,
110
120
  alwaysMultiSelect: false,
111
121
  });
112
122
  private selectionBeforeOp: Id[] | null = null;
123
+ /**
124
+ * 操作前的节点校验错误快照,与 selectionBeforeOp 同时在 captureSelectionBeforeOp 中捕获,
125
+ * 供 pushOpHistory 写入 step.extra.invalidNodeIdsBefore,用于撤销时还原到「操作前」的错误状态。
126
+ */
127
+ private invalidNodeIdsBeforeOp: Map<Id, NodeInvalidInfo> | null = null;
113
128
  /**
114
129
  * 最近一次 pushOpHistory 写入的历史记录 uuid。
115
130
  * 供 *AndGetHistoryId 系列方法在调用普通操作后取回本次产生的历史记录 id;
116
131
  * 普通操作不会读取它,调用前由 *AndGetHistoryId 重置为 null。
117
132
  */
118
133
  private lastPushedHistoryId: string | null = null;
134
+ /**
135
+ * 上一次 doAdd 的插入记忆:nodeId 为插入的节点,selectedId 为当时的选中节点。
136
+ * 仅在选中节点未变化时复用(连续 add / doNotSelect / 批量粘贴),选中变化后自动失效,无需手动清理。
137
+ */
138
+ private lastAdded: { selectedId: Id; nodeId: Id } | null = null;
119
139
 
120
140
  constructor() {
121
141
  super(
@@ -127,7 +147,7 @@ class Editor extends BaseService {
127
147
 
128
148
  /**
129
149
  * 设置当前指点节点配置
130
- * @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength
150
+ * @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength
131
151
  * @param value MNode
132
152
  * @param options.historySource 设置 root 时,本次变更写入历史记录的「操作来源」(仅 name === 'root' 时生效)
133
153
  */
@@ -180,12 +200,15 @@ class Editor extends BaseService {
180
200
  }
181
201
 
182
202
  this.emit('root-change', value as StoreState['root'], preValue as StoreState['root'], options);
203
+
204
+ // 整体替换 DSL 后,清理不再存在于新 DSL 中的失效节点错误记录,避免残留误报。
205
+ this.pruneInvalidNodeIds();
183
206
  }
184
207
  }
185
208
 
186
209
  /**
187
210
  * 获取当前指点节点配置
188
- * @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength'
211
+ * @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength'
189
212
  * @returns MNode
190
213
  */
191
214
  public get<K extends StoreStateKey>(name: K): StoreState[K] {
@@ -260,7 +283,7 @@ class Editor extends BaseService {
260
283
  public isOnDifferentPage(node: MNode): boolean {
261
284
  const currentPageId = this.get('page')?.id;
262
285
  if (currentPageId === undefined || currentPageId === null) return false;
263
- if (isPage(node) || isPageFragment(node)) {
286
+ if (isPageOrFragment(node)) {
264
287
  return `${node.id}` !== `${currentPageId}`;
265
288
  }
266
289
  const nodePage = this.getNodeInfo(node.id, false).page;
@@ -391,7 +414,7 @@ class Editor extends BaseService {
391
414
  this.set('highlightNode', null);
392
415
  }
393
416
 
394
- public async doAdd(node: MNode, parent: MContainer): Promise<MNode> {
417
+ public async doAdd(node: MNode, parent: MContainer, _options: DslOpOptions = {}): Promise<MNode> {
395
418
  const root = this.get('root');
396
419
 
397
420
  if (!root) throw new Error('root为空');
@@ -401,17 +424,20 @@ class Editor extends BaseService {
401
424
 
402
425
  if (!curNode) throw new Error('当前选中节点为空');
403
426
 
404
- if ((parent.type === NodeType.ROOT || curNode?.type === NodeType.ROOT) && !(isPage(node) || isPageFragment(node))) {
427
+ if ((parent.type === NodeType.ROOT || curNode?.type === NodeType.ROOT) && !isPageOrFragment(node)) {
405
428
  throw new Error('app下不能添加组件');
406
429
  }
407
430
 
408
- if (parent.id !== curNode.id && !(isPage(node) || isPageFragment(node))) {
409
- const index = parent.items.indexOf(curNode);
410
- parent.items?.splice(index + 1, 0, node);
411
- } else {
412
- // 新增节点添加到配置中
413
- parent.items?.push(node);
414
- }
431
+ // 连续 add 时接在上一个新增节点之后,否则接在当前选中节点之后;
432
+ // 锚点节点可能已不在 items 中(被删 / DSL 整体替换),回退到当前选中节点,都不在则追加到末尾
433
+ const anchorId = this.lastAdded?.selectedId === curNode.id ? this.lastAdded.nodeId : curNode.id;
434
+ const anchorIndex = isPageOrFragment(node)
435
+ ? -1
436
+ : Math.max(getNodeIndex(anchorId, parent), getNodeIndex(curNode.id, parent));
437
+ const insertIndex = anchorIndex < 0 ? parent.items.length : anchorIndex + 1;
438
+
439
+ parent.items.splice(insertIndex, 0, node);
440
+ this.lastAdded = { selectedId: curNode.id, nodeId: node.id };
415
441
 
416
442
  const layout = await this.getLayout(toRaw(parent), node as MNode);
417
443
  node.style = getInitPositionStyle(node.style, layout);
@@ -421,6 +447,7 @@ class Editor extends BaseService {
421
447
  parent: cloneDeep(parent),
422
448
  parentId: parent.id,
423
449
  root: cloneDeep(root),
450
+ index: insertIndex,
424
451
  });
425
452
 
426
453
  const newStyle = fixNodePosition(node, parent, stage);
@@ -439,24 +466,23 @@ class Editor extends BaseService {
439
466
  * 向指点容器添加组件节点
440
467
  * @param addConfig 将要添加的组件节点配置
441
468
  * @param parent 要添加到的容器组件节点配置,如果不设置,默认为当前选中的组件的父节点
442
- * @param options 可选配置
443
- * @param options.doNotSelect 添加后是否不更新当前选中节点(默认 false,添加后会选中新增的节点)
444
- * @param options.doNotSwitchPage 添加后是否不切换当前页面(默认 false;新增页面 / 跨页新增时为 true 会跳过会引发页面切换的选中操作)
445
- * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
469
+ * @param options 可选配置,见 {@link DslOpOptions}
446
470
  * @returns 添加后的节点
447
471
  */
448
472
  public async add(
449
473
  addNode: AddMNode | MNode[],
450
474
  parent?: MContainer | null,
451
- {
475
+ options: DslOpOptions = {},
476
+ ): Promise<MNode | MNode[]> {
477
+ this.captureSelectionBeforeOp();
478
+
479
+ const {
452
480
  doNotSelect = false,
453
481
  doNotSwitchPage = false,
454
482
  doNotPushHistory = false,
455
483
  historyDescription,
456
484
  historySource,
457
- }: DslOpOptions = {},
458
- ): Promise<MNode | MNode[]> {
459
- this.captureSelectionBeforeOp();
485
+ } = options;
460
486
 
461
487
  const stage = this.get('stage');
462
488
 
@@ -472,17 +498,15 @@ class Editor extends BaseService {
472
498
  addNodes.push(...addNode);
473
499
  }
474
500
 
475
- const newNodes = await Promise.all(
476
- addNodes.map((node) => {
477
- const root = this.get('root');
478
- if ((isPage(node) || isPageFragment(node)) && root) {
479
- return this.doAdd(node, root);
480
- }
481
- const parentNode = parent ?? getAddParent(node);
482
- if (!parentNode) throw new Error('未找到父元素');
483
- return this.doAdd(node, parentNode);
484
- }),
485
- );
501
+ // 必须串行:每个节点的插入位置依赖上一个已插入的节点,并行时插入顺序会受
502
+ // doAdd 前置耗时(如异步 beforeDoAdd 插件钩子)影响而错乱
503
+ const newNodes: MNode[] = [];
504
+ for (const node of addNodes) {
505
+ const root = this.get('root');
506
+ const parentNode = isPageOrFragment(node) && root ? root : (parent ?? getAddParent(node));
507
+ if (!parentNode) throw new Error('未找到父元素');
508
+ newNodes.push(await this.doAdd(node, parentNode, options));
509
+ }
486
510
 
487
511
  if (newNodes.length > 1) {
488
512
  // 多选时只要任一新增节点位于非当前页面,触发的 multiSelect 就会引起页面切换
@@ -511,7 +535,7 @@ class Editor extends BaseService {
511
535
  }
512
536
  }
513
537
 
514
- if (!(isPage(newNodes[0]) || isPageFragment(newNodes[0]))) {
538
+ if (!isPageOrFragment(newNodes[0])) {
515
539
  const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
516
540
  if (!doNotPushHistory) {
517
541
  const parentId = (this.getParentById(newNodes[0].id, false) ?? this.get('root'))!.id;
@@ -535,6 +559,18 @@ class Editor extends BaseService {
535
559
  }
536
560
 
537
561
  this.emit('add', newNodes);
562
+ this.emit('change', {
563
+ type: 'add',
564
+ data: newNodes.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
565
+ historySource,
566
+ doNotPushHistory,
567
+ });
568
+
569
+ // 页面 / 页面片新增不入历史栈(见上方 isPageOrFragment 分支),这里合并补发一次结构变更通知
570
+ const addedPages = newNodes.filter((node) => isPageOrFragment(node)) as (MPage | MPageFragment)[];
571
+ if (addedPages.length) {
572
+ historyService.notifyPageStructureChange({ add: addedPages, remove: [] });
573
+ }
538
574
 
539
575
  return Array.isArray(addNode) ? newNodes : newNodes[0];
540
576
  }
@@ -554,12 +590,14 @@ class Editor extends BaseService {
554
590
 
555
591
  if (typeof index !== 'number' || index === -1) throw new Error('找不要删除的节点');
556
592
 
557
- parent.items?.splice(index, 1);
558
593
  const stage = this.get('stage');
559
- stage?.remove({ id: node.id, parentId: parent.id, root: cloneDeep(root) });
594
+ const currentPage = this.get('page');
595
+ const isDeletingCurrentPage = isPageOrFragment(node) && !!currentPage && `${currentPage.id}` === `${node.id}`;
596
+
597
+ parent.items?.splice(index, 1);
560
598
 
561
599
  // 始终清理已删除节点在 state 中的残留引用:
562
- // - 即使后续会调用 selectDefault / select(parent) 覆盖,跳过这些调用(doNotSelect / doNotSwitchPage)时也不能让 state 持有已删除节点
600
+ // - 即使后续会调用 select 覆盖,跳过这些调用(doNotSelect / doNotSwitchPage)时也不能让 state 持有已删除节点
563
601
  const selectedNodes = this.get('nodes');
564
602
  const removedSelectedIndex = selectedNodes.findIndex((n: MNode) => `${n.id}` === `${node.id}`);
565
603
  if (removedSelectedIndex !== -1) {
@@ -567,38 +605,44 @@ class Editor extends BaseService {
567
605
  nextSelected.splice(removedSelectedIndex, 1);
568
606
  this.set('nodes', nextSelected);
569
607
  }
570
- if (isPage(node) || isPageFragment(node)) {
571
- const currentPage = this.get('page');
572
- if (currentPage && `${currentPage.id}` === `${node.id}`) {
573
- this.set('page', null);
574
- }
575
- }
576
-
577
- const selectDefault = async (pages: MNode[]) => {
578
- if (pages[0]) {
579
- await this.select(pages[0]);
580
- stage?.select(pages[0].id);
581
- } else {
582
- this.selectRoot();
583
- }
584
- };
585
608
 
609
+ const removeData = { id: node.id, parentId: parent.id, root: cloneDeep(root) };
586
610
  const rootItems = root.items || [];
587
-
588
- if (isPage(node)) {
589
- this.state.pageLength -= 1;
590
-
591
- // 删除页面后默认会切到首个剩余页面(selectDefault),doNotSwitchPage 时跳过这次自动切换
592
- if (!doNotSelect && !doNotSwitchPage) {
593
- await selectDefault(rootItems);
611
+ // 删非当前页时画布不应该变动,所以只有删的是当前页才重新选中
612
+ const shouldReselect = isDeletingCurrentPage && !doNotSelect && !doNotSwitchPage;
613
+ // 还有剩余页面才能切过去,否则退回选中 root
614
+ const shouldSwitchPage = shouldReselect && rootItems.length > 0;
615
+
616
+ if (isPageOrFragment(node)) {
617
+ if (isPage(node)) {
618
+ this.state.pageLength -= 1;
619
+ } else {
620
+ this.state.pageFragmentLength -= 1;
594
621
  }
595
- } else if (isPageFragment(node)) {
596
- this.state.pageFragmentLength -= 1;
597
622
 
598
- if (!doNotSelect && !doNotSwitchPage) {
599
- await selectDefault(rootItems);
623
+ if (shouldSwitchPage) {
624
+ // runtime 收到页面删除时会销毁当前渲染的 page 实例,必须先把画布切到剩余页面再通知删除,
625
+ // 否则画布会被清空,且要等下一次 updatePageId 才能恢复。
626
+ // nextTick 用于等 Stage 的 page watch 把新页面 id 同步给 runtime。
627
+ await this.select(rootItems[0]);
628
+ stage?.select(rootItems[0].id);
629
+ await nextTick();
630
+ stage?.remove(removeData);
631
+ } else {
632
+ // page 置空会让 Workspace 卸载 Stage 并销毁 renderer,删除通知必须在卸载前发出,
633
+ // 且不能 await:runtime 未 ready 时 getRuntime 的监听会随 destroy 一起被移除,永远不会 resolve
634
+ stage?.remove(removeData);
635
+
636
+ if (shouldReselect) {
637
+ // 页面已全部删完
638
+ this.selectRoot();
639
+ } else if (isDeletingCurrentPage) {
640
+ this.set('page', null);
641
+ }
600
642
  }
601
643
  } else {
644
+ stage?.remove(removeData);
645
+
602
646
  if (!doNotSelect) {
603
647
  await this.select(parent);
604
648
  stage?.select(parent.id);
@@ -621,23 +665,18 @@ class Editor extends BaseService {
621
665
  * @param options.doNotSwitchPage 删除后是否不切换当前页面(默认 false;删除页面 / 页面片段时为 true 会跳过自动切换到首个剩余页面)
622
666
  * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
623
667
  */
624
- public async remove(
625
- nodeOrNodeList: MNode | MNode[],
626
- {
627
- doNotSelect = false,
628
- doNotSwitchPage = false,
629
- doNotPushHistory = false,
630
- historyDescription,
631
- historySource,
632
- }: DslOpOptions = {},
633
- ): Promise<void> {
668
+ public async remove(nodeOrNodeList: MNode | MNode[], options: DslOpOptions = {}): Promise<void> {
669
+ const { doNotPushHistory = false, historyDescription, historySource } = options;
634
670
  this.captureSelectionBeforeOp();
635
671
 
636
672
  const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
637
673
 
674
+ // 删除后节点已从树中移除,无法再反查所属 page,这里在删除前先逐个捕获用于 `change` 事件
675
+ const changeItems = nodes.map((node) => ({ node, page: this.getPageOfNode(node.id) }));
676
+
638
677
  const removedItems: StepDiffItem<MNode>[] = [];
639
678
  let pageForOp: { name: string; id: Id } | null = null;
640
- if (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) {
679
+ if (!isPageOrFragment(nodes[0])) {
641
680
  for (const n of nodes) {
642
681
  const { parent, node: curNode, page } = this.getNodeInfo(n.id, false);
643
682
  if (parent && curNode) {
@@ -654,7 +693,10 @@ class Editor extends BaseService {
654
693
  }
655
694
  }
656
695
 
657
- await Promise.all(nodes.map((node) => this.doRemove(node, { doNotSelect, doNotSwitchPage })));
696
+ await Promise.all(nodes.map((node) => this.doRemove(node, options)));
697
+
698
+ // 删除节点时同步清理其(含子树)的校验错误记录;置于 pushOpHistory 之前,使历史快照与本次删除对齐。
699
+ this.removeInvalidNodesBySubtree(nodes);
658
700
 
659
701
  if (removedItems.length > 0 && pageForOp) {
660
702
  if (!doNotPushHistory) {
@@ -670,12 +712,20 @@ class Editor extends BaseService {
670
712
  }
671
713
 
672
714
  this.emit('remove', nodes);
715
+ this.emit('change', { type: 'remove', data: changeItems, historySource, doNotPushHistory });
716
+
717
+ // 页面 / 页面片删除不入历史栈(见上方 isPageOrFragment 分支),这里合并补发一次结构变更通知
718
+ const removedPages = nodes.filter((node) => isPageOrFragment(node)) as (MPage | MPageFragment)[];
719
+ if (removedPages.length) {
720
+ historyService.notifyPageStructureChange({ add: [], remove: removedPages });
721
+ }
673
722
  }
674
723
 
675
724
  public async doUpdate(
676
725
  config: MNode,
677
- { changeRecords = [], historySource }: { changeRecords?: ChangeRecord[]; historySource?: HistoryOpSource } = {},
726
+ data: UpdateOptions = {},
678
727
  ): Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }> {
728
+ const { changeRecords = [], historySource, replace = false } = data;
679
729
  const root = this.get('root');
680
730
  if (!root) throw new Error('root为空');
681
731
 
@@ -687,9 +737,14 @@ class Editor extends BaseService {
687
737
 
688
738
  const node = toRaw(info.node);
689
739
 
690
- let newConfig = await toggleFixedPosition(toRaw(config), node, info.path, this.getLayout);
740
+ // replace=true 时跳过 toggleFixedPosition / mergeWith / setChildrenLayout,直接用传入配置整节点替换
741
+ let newConfig = replace
742
+ ? cloneDeep(toRaw(config))
743
+ : await toggleFixedPosition(toRaw(config), node, info.path, this.getLayout);
691
744
 
692
- newConfig = mergeWith(cloneDeep(node), newConfig, editorNodeMergeCustomizer);
745
+ if (!replace) {
746
+ newConfig = mergeWith(cloneDeep(node), newConfig, editorNodeMergeCustomizer);
747
+ }
693
748
 
694
749
  if (!newConfig.type) throw new Error('配置缺少type值');
695
750
 
@@ -710,10 +765,12 @@ class Editor extends BaseService {
710
765
 
711
766
  if (!parentNodeItems || typeof index === 'undefined' || index === -1) throw new Error('更新的节点未找到');
712
767
 
713
- const newLayout = await this.getLayout(newConfig);
714
- const layout = await this.getLayout(node);
715
- if (Array.isArray(newConfig.items) && newLayout !== layout) {
716
- newConfig = setChildrenLayout(newConfig as MContainer, newLayout);
768
+ if (!replace) {
769
+ const newLayout = await this.getLayout(newConfig);
770
+ const layout = await this.getLayout(node);
771
+ if (Array.isArray(newConfig.items) && newLayout !== layout) {
772
+ newConfig = setChildrenLayout(newConfig as MContainer, newLayout);
773
+ }
717
774
  }
718
775
 
719
776
  parentNodeItems[index] = newConfig;
@@ -727,7 +784,7 @@ class Editor extends BaseService {
727
784
  }
728
785
 
729
786
  // 只有被更新节点正好是当前选中页面时才同步 state.page,避免「更新非当前页」误将编辑器切到该页
730
- if (isPage(newConfig) || isPageFragment(newConfig)) {
787
+ if (isPageOrFragment(newConfig)) {
731
788
  const currentPage = this.get('page');
732
789
  if (currentPage && `${currentPage.id}` === `${newConfig.id}`) {
733
790
  this.set('page', newConfig as MPage | MPageFragment);
@@ -747,26 +804,20 @@ class Editor extends BaseService {
747
804
  * 更新节点
748
805
  * update后会触发依赖收集,收集完后会掉stage.update方法
749
806
  * @param config 新的节点配置,配置中需要有id信息
750
- * @param data 额外数据
751
- * @param data.changeRecords 单节点 form 端变更记录(多节点场景下被忽略,使用 changeRecordList)
752
- * @param data.changeRecordList 多节点 form 端变更记录列表,按 config 数组同序对应每个节点;优先级高于 changeRecords
753
- * @param data.doNotPushHistory 是否不写入历史记录(默认 false)
754
- * @param data.historyDescription 入栈时附带的人类可读描述,用于历史面板展示(不影响 undo/redo 行为)
807
+ * @param data 额外数据,见 {@link UpdateOptions}
755
808
  * @returns 更新后的节点配置
756
809
  */
757
- public async update(
758
- config: MNode | MNode[],
759
- data: {
760
- changeRecords?: ChangeRecord[];
761
- changeRecordList?: ChangeRecord[][];
762
- doNotPushHistory?: boolean;
763
- historyDescription?: string;
764
- historySource?: HistoryOpSource;
765
- } = {},
766
- ): Promise<MNode | MNode[]> {
810
+ public async update(config: MNode | MNode[], data: UpdateOptions = {}): Promise<MNode | MNode[]> {
767
811
  this.captureSelectionBeforeOp();
768
812
 
769
- const { doNotPushHistory = false, changeRecordList, changeRecords, historyDescription, historySource } = data;
813
+ const {
814
+ doNotPushHistory = false,
815
+ changeRecordList,
816
+ changeRecords,
817
+ historyDescription,
818
+ historySource,
819
+ invalidInfo,
820
+ } = data;
770
821
 
771
822
  const nodes = Array.isArray(config) ? config : [config];
772
823
 
@@ -775,10 +826,13 @@ class Editor extends BaseService {
775
826
  const updateData = await Promise.all(
776
827
  nodes.map((node, index) => {
777
828
  const recordsForNode = changeRecordList ? (changeRecordList[index] ?? []) : (changeRecords ?? []);
778
- return this.doUpdate(node, { changeRecords: recordsForNode, historySource });
829
+ return this.doUpdate(node, { ...data, changeRecords: recordsForNode });
779
830
  }),
780
831
  );
781
832
 
833
+ // 校验错误信息在 pushOpHistory 之前落库,保证历史快照包含本次变更对应的错误状态。
834
+ this.applyInvalidInfo(config, invalidInfo);
835
+
782
836
  if (updateData[0].oldNode?.type !== NodeType.ROOT) {
783
837
  const curNodes = this.get('nodes');
784
838
  if (curNodes.length) {
@@ -805,6 +859,12 @@ class Editor extends BaseService {
805
859
  }
806
860
 
807
861
  this.emit('update', updateData);
862
+ this.emit('change', {
863
+ type: 'update',
864
+ data: updateData.map((node) => ({ node, page: this.getPageOfNode(node.newNode.id) })),
865
+ historySource,
866
+ doNotPushHistory,
867
+ });
808
868
  return Array.isArray(config) ? updateData.map((item) => item.newNode) : updateData[0].newNode;
809
869
  }
810
870
 
@@ -1046,6 +1106,13 @@ class Editor extends BaseService {
1046
1106
  }
1047
1107
 
1048
1108
  this.emit('move-layer', offset);
1109
+ this.emit('change', {
1110
+ type: 'move-layer',
1111
+ data: [{ node, page: this.getPageOfNode(node.id) }],
1112
+ offset,
1113
+ historySource,
1114
+ doNotPushHistory,
1115
+ });
1049
1116
  }
1050
1117
 
1051
1118
  /**
@@ -1074,7 +1141,7 @@ class Editor extends BaseService {
1074
1141
  }: DslOpOptions = {},
1075
1142
  ): Promise<MNode | MNode[]> {
1076
1143
  const isBatch = Array.isArray(config);
1077
- const configs = (isBatch ? config : [config]).filter((item) => !(isPage(item) || isPageFragment(item)));
1144
+ const configs = (isBatch ? config : [config]).filter((item) => !isPageOrFragment(item));
1078
1145
 
1079
1146
  if (configs.length === 0) {
1080
1147
  throw new Error('没有可移动的节点');
@@ -1225,6 +1292,14 @@ class Editor extends BaseService {
1225
1292
  }
1226
1293
 
1227
1294
  this.emit('drag-to', { targetIndex, configs, targetParent });
1295
+ this.emit('change', {
1296
+ type: 'drag-to',
1297
+ data: configs.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
1298
+ targetIndex,
1299
+ targetParent,
1300
+ historySource,
1301
+ doNotPushHistory,
1302
+ });
1228
1303
  }
1229
1304
 
1230
1305
  // #region AndGetHistoryId
@@ -1260,13 +1335,7 @@ class Editor extends BaseService {
1260
1335
  /** 等价于 {@link update},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
1261
1336
  public async updateAndGetHistoryId(
1262
1337
  config: MNode | MNode[],
1263
- data: {
1264
- changeRecords?: ChangeRecord[];
1265
- changeRecordList?: ChangeRecord[][];
1266
- doNotPushHistory?: boolean;
1267
- historyDescription?: string;
1268
- historySource?: HistoryOpSource;
1269
- } = {},
1338
+ data: UpdateOptions = {},
1270
1339
  ): Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> {
1271
1340
  this.lastPushedHistoryId = null;
1272
1341
  const result = await this.update(config, data);
@@ -1519,6 +1588,7 @@ class Editor extends BaseService {
1519
1588
  this.set('stage', null);
1520
1589
  this.set('highlightNode', null);
1521
1590
  this.set('modifiedNodeIds', new Map());
1591
+ this.set('invalidNodeIds', new Map());
1522
1592
  this.set('pageLength', 0);
1523
1593
  }
1524
1594
 
@@ -1532,6 +1602,62 @@ class Editor extends BaseService {
1532
1602
  this.get('modifiedNodeIds').clear();
1533
1603
  }
1534
1604
 
1605
+ /**
1606
+ * 记录(或覆盖)某个节点在指定来源(属性表单 / 样式表单)上的校验错误信息。
1607
+ * @param id 节点 id
1608
+ * @param source 错误来源:'props'(属性表单)| 'style'(样式表单)
1609
+ * @param message 错误文案(可能是包含 <br> 的 HTML)
1610
+ */
1611
+ public setInvalidNode(id: Id, source: NodeInvalidSource, message: string) {
1612
+ const map = this.get('invalidNodeIds');
1613
+ const info: NodeInvalidInfo = { ...(map.get(id) || {}) };
1614
+ info[source] = message;
1615
+ map.set(id, info);
1616
+ this.emit('invalid-node-change', map);
1617
+ }
1618
+
1619
+ /**
1620
+ * 删除节点的校验错误记录。
1621
+ * @param id 节点 id
1622
+ * @param source 指定来源则仅删除该来源;不传则删除该节点全部来源的错误
1623
+ */
1624
+ public deleteInvalidNode(id: Id, source?: NodeInvalidSource) {
1625
+ const map = this.get('invalidNodeIds');
1626
+ if (!map.has(id)) return;
1627
+
1628
+ if (!source) {
1629
+ map.delete(id);
1630
+ } else {
1631
+ const info: NodeInvalidInfo = { ...(map.get(id) || {}) };
1632
+ delete info[source];
1633
+ if (info.props || info.style) {
1634
+ map.set(id, info);
1635
+ } else {
1636
+ map.delete(id);
1637
+ }
1638
+ }
1639
+
1640
+ this.emit('invalid-node-change', map);
1641
+ }
1642
+
1643
+ /** 获取当前存在校验错误的节点错误 Map(key 为节点 id) */
1644
+ public getInvalidNodeIds(): Map<Id, NodeInvalidInfo> {
1645
+ return this.get('invalidNodeIds');
1646
+ }
1647
+
1648
+ /** 获取指定节点的校验错误信息 */
1649
+ public getInvalidNodeInfo(id: Id): NodeInvalidInfo | undefined {
1650
+ return this.get('invalidNodeIds').get(id);
1651
+ }
1652
+
1653
+ /** 清空全部校验错误记录 */
1654
+ public resetInvalidNodeId() {
1655
+ const map = this.get('invalidNodeIds');
1656
+ if (map.size === 0) return;
1657
+ map.clear();
1658
+ this.emit('invalid-node-change', map);
1659
+ }
1660
+
1535
1661
  public usePlugin(options: AsyncHookPlugin<AsyncMethodName, Editor>): void {
1536
1662
  super.usePlugin(options);
1537
1663
  }
@@ -1554,13 +1680,92 @@ class Editor extends BaseService {
1554
1680
  return super.emit(eventName, ...args);
1555
1681
  }
1556
1682
 
1683
+ /**
1684
+ * 应用一次属性面板提交携带的校验错误信息(在写入历史记录之前调用,使历史快照与本次变更对齐)。
1685
+ * - invalidInfo 为空(调用方未携带):认为本次更新对应的节点已无校验错误,清除其全部来源的错误;
1686
+ * - error 非空则记录错误,为空则清除对应来源的错误。
1687
+ */
1688
+ private applyInvalidInfo(
1689
+ config: MNode | MNode[],
1690
+ invalidInfo?: { id: Id; source: NodeInvalidSource; error?: string },
1691
+ ) {
1692
+ if (!invalidInfo) {
1693
+ // 调用方未携带 invalidInfo:本次更新对应节点不应再保留校验错误,清除其全部来源。
1694
+ const ids = (Array.isArray(config) ? config : [config])
1695
+ .map((node) => node.id)
1696
+ .filter((id): id is Id => id !== undefined);
1697
+ ids.forEach((id) => this.deleteInvalidNode(id));
1698
+ return;
1699
+ }
1700
+ const { id, source, error } = invalidInfo;
1701
+ if (error) {
1702
+ this.setInvalidNode(id, source, error);
1703
+ } else {
1704
+ this.deleteInvalidNode(id, source);
1705
+ }
1706
+ }
1707
+
1708
+ /** 删除被移除节点(含其子树)的校验错误记录,避免残留误报 */
1709
+ private removeInvalidNodesBySubtree(nodes: MNode[]) {
1710
+ const map = this.get('invalidNodeIds');
1711
+ if (map.size === 0) return;
1712
+
1713
+ let changed = false;
1714
+ nodes.forEach((node) => {
1715
+ traverseNode(node, (n) => {
1716
+ if (n.id !== undefined && map.delete(n.id)) {
1717
+ changed = true;
1718
+ }
1719
+ });
1720
+ });
1721
+
1722
+ if (changed) {
1723
+ this.emit('invalid-node-change', map);
1724
+ }
1725
+ }
1726
+
1727
+ /** 清理不在当前 DSL 中的失效节点错误记录(用于整体替换 root 后) */
1728
+ private pruneInvalidNodeIds() {
1729
+ const map = this.get('invalidNodeIds');
1730
+ if (map.size === 0) return;
1731
+
1732
+ let changed = false;
1733
+ for (const id of [...map.keys()]) {
1734
+ if (!this.getNodeById(id, false)) {
1735
+ map.delete(id);
1736
+ changed = true;
1737
+ }
1738
+ }
1739
+
1740
+ if (changed) {
1741
+ this.emit('invalid-node-change', map);
1742
+ }
1743
+ }
1744
+
1557
1745
  private addModifiedNodeId(id: Id) {
1558
1746
  this.get('modifiedNodeIds').set(id, id);
1559
1747
  }
1560
1748
 
1749
+ /**
1750
+ * 获取指定节点所属的页面 / 页面片:
1751
+ * - 普通节点返回其所在的 page;
1752
+ * - 节点本身就是 page / pageFragment 时返回它自己;
1753
+ * - 找不到时返回 null。
1754
+ * 供 `change` 事件携带「变更节点对应的 page」(而非编辑器当前选中页)。
1755
+ */
1756
+ private getPageOfNode(id: Id): MPage | MPageFragment | null {
1757
+ const { node, page } = this.getNodeInfo(id, false);
1758
+ if (page) return page;
1759
+ if (node && isPageOrFragment(node)) return node as MPage | MPageFragment;
1760
+ return null;
1761
+ }
1762
+
1561
1763
  private captureSelectionBeforeOp() {
1562
1764
  if (this.selectionBeforeOp) return;
1563
1765
  this.selectionBeforeOp = this.get('nodes').map((n) => n.id);
1766
+ // 与选区快照同步捕获「操作前」的校验错误状态;因 applyInvalidInfo 会在 pushOpHistory 前修改 invalidNodeIds,
1767
+ // 必须在此(操作最开始)留存操作前快照,供 undo 还原。
1768
+ this.invalidNodeIdsBeforeOp = new Map(this.get('invalidNodeIds'));
1564
1769
  }
1565
1770
 
1566
1771
  /**
@@ -1579,6 +1784,10 @@ class Editor extends BaseService {
1579
1784
  const nextMap = new Map(nextPages.map((p) => [`${p.id}`, p]));
1580
1785
  const indexInItems = (root: MApp, id: Id) => (root.items ?? []).findIndex((item) => `${item.id}` === `${id}`);
1581
1786
 
1787
+ // 收集本次整体替换中增删的页面,循环结束后合并为一次结构变更通知(避免逐页派发多个事件)
1788
+ const addedPages: (MPage | MPageFragment)[] = [];
1789
+ const removedPages: (MPage | MPageFragment)[] = [];
1790
+
1582
1791
  nextPages.forEach((nextPage) => {
1583
1792
  const prevPage = prevMap.get(`${nextPage.id}`);
1584
1793
  if (!prevPage) {
@@ -1588,6 +1797,7 @@ class Editor extends BaseService {
1588
1797
  { newSchema: cloneDeep(toRaw(nextPage)), parentId: nextRoot.id, index: indexInItems(nextRoot, nextPage.id) },
1589
1798
  source,
1590
1799
  );
1800
+ addedPages.push(nextPage);
1591
1801
  } else if (!isEqual(toRaw(prevPage), toRaw(nextPage))) {
1592
1802
  this.pushPageDiffStep(
1593
1803
  'update',
@@ -1606,8 +1816,13 @@ class Editor extends BaseService {
1606
1816
  { oldSchema: cloneDeep(toRaw(prevPage)), parentId: preRoot.id, index: indexInItems(preRoot, prevPage.id) },
1607
1817
  source,
1608
1818
  );
1819
+ removedPages.push(prevPage);
1609
1820
  }
1610
1821
  });
1822
+
1823
+ if (addedPages.length || removedPages.length) {
1824
+ historyService.notifyPageStructureChange({ add: addedPages, remove: removedPages });
1825
+ }
1611
1826
  }
1612
1827
 
1613
1828
  /**
@@ -1667,6 +1882,9 @@ class Editor extends BaseService {
1667
1882
  selectedBefore: this.selectionBeforeOp ?? [],
1668
1883
  selectedAfter: this.get('nodes').map((n) => n.id),
1669
1884
  modifiedNodeIds: new Map(this.get('modifiedNodeIds')),
1885
+ // 方向性双快照:undo 还原「操作前」错误状态,redo 还原「操作后」错误状态。
1886
+ invalidNodeIdsBefore: new Map(this.invalidNodeIdsBeforeOp ?? this.get('invalidNodeIds')),
1887
+ invalidNodeIdsAfter: new Map(this.get('invalidNodeIds')),
1670
1888
  },
1671
1889
  diff,
1672
1890
  };
@@ -1679,16 +1897,19 @@ class Editor extends BaseService {
1679
1897
  const historyId = pushed ? step.uuid : null;
1680
1898
  this.lastPushedHistoryId = historyId;
1681
1899
  this.selectionBeforeOp = null;
1900
+ this.invalidNodeIdsBeforeOp = null;
1682
1901
  return historyId;
1683
1902
  }
1684
1903
 
1685
1904
  /**
1686
1905
  * 应用历史操作(撤销 / 重做)
1687
1906
  *
1688
- * 所有 DSL 修改都走 `editor.add / remove / update`,并通过 `doNotPushHistory` 阻止再次入栈、
1689
- * `doNotSelect / doNotSwitchPage` 让选区由方法末尾的统一逻辑兜底。
1907
+ * 删除 / 更新类修改走 `editor.remove / update`,并通过 `doNotPushHistory` 阻止再次入栈、
1908
+ * `doNotSelect / doNotSwitchPage` 让选区由方法末尾的统一逻辑兜底;
1909
+ * 「重新插回节点」(撤销 remove / 重做 add)需按 step 记录的 parentId / index 精确还原,
1910
+ * 不走 this.add,由手工 splice + stage.add 完成后补发等价的 add / change 事件(见 emitHistoryInsertEvents)。
1690
1911
  *
1691
- * 注意:这些公开方法会发出 add / remove / update 事件,业务侧若需要区分"用户操作"与"撤销重做触发",
1912
+ * 注意:这些路径都会发出 add / remove / update / change 事件,业务侧若需要区分"用户操作"与"撤销重做触发",
1692
1913
  * 请监听 `history-change` 事件配合判断。
1693
1914
  *
1694
1915
  * @param step 操作记录
@@ -1701,7 +1922,14 @@ class Editor extends BaseService {
1701
1922
  const stage = this.get('stage');
1702
1923
  if (!root) return;
1703
1924
 
1704
- const commonOpts = { doNotSelect: true, doNotSwitchPage: true, doNotPushHistory: true } as const;
1925
+ // 撤销/重做内部复用 add/remove/update,透传被应用 step 上记录的 source,
1926
+ // 使其触发的 change 事件同样携带「历史来源」
1927
+ const commonOpts = {
1928
+ doNotSelect: true,
1929
+ doNotSwitchPage: true,
1930
+ doNotPushHistory: true,
1931
+ historySource: step.source,
1932
+ } as const;
1705
1933
 
1706
1934
  switch (step.opType) {
1707
1935
  case 'add': {
@@ -1717,46 +1945,56 @@ class Editor extends BaseService {
1717
1945
  }
1718
1946
  } else {
1719
1947
  // 重做 add:按记录的 parentId / index 把节点重新插回父容器。
1720
- // 按目标 index 升序逐个插入,先小后大避免索引漂移
1948
+ // 按目标 index 升序逐个插入,先小后大避免索引漂移。
1949
+ // 不走 this.add:doAdd 依赖当前选中节点(无选区时直接抛错),且只能插到选中节点之后 / 末尾,
1950
+ // 无法按 step 记录的 parentId / index 精确还原,还会重算 style 破坏已记录的终态位置。
1721
1951
  const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
1952
+ const addedNodes: MNode[] = [];
1722
1953
  for (const { newSchema, parentId, index } of sorted) {
1723
1954
  if (!newSchema || parentId === undefined) continue;
1724
1955
  const parent = this.getNodeById(parentId, false) as MContainer | null;
1725
1956
  if (parent?.items) {
1726
- if (typeof index === 'number' && index >= 0 && index < parent.items.length) {
1727
- parent.items.splice(index, 0, cloneDeep(newSchema));
1728
- } else {
1729
- parent.items.push(cloneDeep(newSchema));
1730
- }
1957
+ const addedNode = cloneDeep(newSchema);
1958
+ const insertIndex = clampIndex(index, parent.items.length);
1959
+ parent.items.splice(insertIndex, 0, addedNode);
1960
+ addedNodes.push(addedNode);
1731
1961
  await stage?.add({
1732
1962
  config: cloneDeep(newSchema),
1733
1963
  parent: cloneDeep(parent),
1734
1964
  parentId: parent.id,
1735
1965
  root: cloneDeep(root),
1966
+ index: insertIndex,
1736
1967
  });
1737
1968
  }
1738
1969
  }
1970
+ this.emitHistoryInsertEvents(addedNodes, step.source);
1739
1971
  }
1740
1972
  break;
1741
1973
  }
1742
1974
  case 'remove': {
1743
1975
  const items = step.diff ?? [];
1744
1976
  if (reverse) {
1745
- // 撤销 remove:按原 index 升序逐个插回(先小后大避免索引漂移)
1977
+ // 撤销 remove:按原 index 升序逐个插回(先小后大避免索引漂移);不走 this.add 的原因同上
1746
1978
  const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
1979
+ const addedNodes: MNode[] = [];
1747
1980
  for (const { oldSchema, parentId, index } of sorted) {
1748
1981
  if (!oldSchema || parentId === undefined) continue;
1749
1982
  const parent = this.getNodeById(parentId, false) as MContainer | null;
1750
1983
  if (parent?.items) {
1751
- parent.items.splice(index ?? parent.items.length, 0, cloneDeep(oldSchema));
1984
+ const addedNode = cloneDeep(oldSchema);
1985
+ const insertIndex = clampIndex(index, parent.items.length);
1986
+ parent.items.splice(insertIndex, 0, addedNode);
1987
+ addedNodes.push(addedNode);
1752
1988
  await stage?.add({
1753
1989
  config: cloneDeep(oldSchema),
1754
1990
  parent: cloneDeep(parent),
1755
1991
  parentId,
1756
1992
  root: cloneDeep(root),
1993
+ index: insertIndex,
1757
1994
  });
1758
1995
  }
1759
1996
  }
1997
+ this.emitHistoryInsertEvents(addedNodes, step.source);
1760
1998
  } else {
1761
1999
  // 重做 remove:再删一次
1762
2000
  for (const { oldSchema } of items) {
@@ -1796,7 +2034,7 @@ class Editor extends BaseService {
1796
2034
  return cloneDeep(reverse ? oldNode : newNode);
1797
2035
  });
1798
2036
  if (configs.length) {
1799
- await this.update(configs, { doNotPushHistory: true });
2037
+ await this.update(configs, { doNotPushHistory: true, historySource: step.source });
1800
2038
  }
1801
2039
  break;
1802
2040
  }
@@ -1804,6 +2042,15 @@ class Editor extends BaseService {
1804
2042
 
1805
2043
  this.set('modifiedNodeIds', step.extra?.modifiedNodeIds ?? new Map());
1806
2044
 
2045
+ // 还原校验错误标记:因 undo/redo 复用同一 step,需按方向取「操作前 / 操作后」快照——
2046
+ // undo(reverse=true) 还原到操作前的错误状态(撤销一个「校验失败」的改动后错误消失),
2047
+ // redo(reverse=false) 还原到操作后的错误状态(重做后错误恢复)。
2048
+ // 浅拷贝一份以隔离历史快照,避免后续 set/deleteInvalidNode 反向污染 step.extra。
2049
+ const invalidToRestore =
2050
+ (reverse ? step.extra?.invalidNodeIdsBefore : step.extra?.invalidNodeIdsAfter) ?? new Map();
2051
+ this.set('invalidNodeIds', new Map(invalidToRestore));
2052
+ this.emit('invalid-node-change', this.get('invalidNodeIds'));
2053
+
1807
2054
  const page = toRaw(this.get('page'));
1808
2055
  if (page) {
1809
2056
  const selectIds = (reverse ? step.extra?.selectedBefore : step.extra?.selectedAfter) ?? [];
@@ -1826,6 +2073,22 @@ class Editor extends BaseService {
1826
2073
  private selectedConfigExceptionHandler(config: MNode | Id): EditorNodeInfo {
1827
2074
  return resolveSelectedNode(config, (id) => this.getNodeInfo(id), this.state.root?.id);
1828
2075
  }
2076
+
2077
+ /**
2078
+ * 撤销 remove / 重做 add 通过手工 splice 按 step 记录的 parentId / index 精确还原节点(不走 this.add,原因见调用处注释),
2079
+ * 这里补齐与 this.add 等价的 add / change 事件,保证撤销/重做路径的事件行为与正向操作一致。
2080
+ */
2081
+ private emitHistoryInsertEvents(nodes: MNode[], source?: HistoryOpSource) {
2082
+ if (!nodes.length) return;
2083
+ this.emit('add', nodes);
2084
+ this.emit('change', {
2085
+ type: 'add',
2086
+ data: nodes.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
2087
+ historySource: source,
2088
+ // 撤销/重做本身不再写入历史记录
2089
+ doNotPushHistory: true,
2090
+ });
2091
+ }
1829
2092
  }
1830
2093
 
1831
2094
  export type EditorService = Editor;