@tmagic/editor 1.8.0-beta.10 → 1.8.0-beta.12

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 (111) hide show
  1. package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +7 -4
  2. package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +4 -0
  3. package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +2 -1
  4. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +5 -2
  5. package/dist/es/components/FloatingBox.vue_vue_type_script_setup_true_lang.js +60 -14
  6. package/dist/es/components/ScrollBar.js +0 -1
  7. package/dist/es/editorProps.js +6 -1
  8. package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +15 -3
  9. package/dist/es/fields/CondOpSelect.vue_vue_type_script_setup_true_lang.js +2 -17
  10. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +2 -1
  11. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +7 -0
  12. package/dist/es/fields/DataSourceInput.vue_vue_type_script_setup_true_lang.js +5 -17
  13. package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +4 -0
  14. package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +29 -3
  15. package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +50 -88
  16. package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +2 -5
  17. package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +48 -23
  18. package/dist/es/fields/StyleSetter/pro/Border.vue_vue_type_script_setup_true_lang.js +6 -3
  19. package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +28 -22
  20. package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +28 -22
  21. package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +28 -22
  22. package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +6 -3
  23. package/dist/es/fields/UISelect.js +0 -1
  24. package/dist/es/index.js +9 -6
  25. package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +21 -6
  26. package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +9 -6
  27. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +14 -4
  28. package/dist/es/layouts/history-list/composables.js +1 -1
  29. package/dist/es/layouts/history-list/useHistoryRevert.js +17 -6
  30. package/dist/es/layouts/props-panel/FormPanel.vue_vue_type_script_setup_true_lang.js +44 -5
  31. package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +15 -8
  32. package/dist/es/layouts/props-panel/use-style-panel.js +3 -2
  33. package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +15 -31
  34. package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
  35. package/dist/es/layouts/sidebar/code-block/useContentMenu.js +18 -3
  36. package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +4 -0
  37. package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
  38. package/dist/es/layouts/sidebar/data-source/useContentMenu.js +18 -3
  39. package/dist/es/layouts/sidebar/layer/LayerNodeContent.js +5 -0
  40. package/dist/es/layouts/sidebar/layer/LayerNodeContent.vue_vue_type_script_setup_true_lang.js +46 -0
  41. package/dist/es/layouts/sidebar/layer/LayerPanel.vue_vue_type_script_setup_true_lang.js +2 -1
  42. package/dist/es/layouts/workspace/viewer/NodeListMenu.vue_vue_type_script_setup_true_lang.js +8 -2
  43. package/dist/es/plugin.js +3 -1
  44. package/dist/es/services/editor.js +104 -5
  45. package/dist/es/services/props.js +2 -1
  46. package/dist/es/services/storage.js +1 -1
  47. package/dist/es/services/ui.js +3 -0
  48. package/dist/es/style.css +104 -55
  49. package/dist/es/utils/data-source/index.js +30 -11
  50. package/dist/es/utils/event.js +123 -0
  51. package/dist/es/utils/monaco-editor.js +22 -6
  52. package/dist/es/utils/props.js +94 -8
  53. package/dist/es/utils/type-match-rules.js +360 -0
  54. package/dist/style.css +104 -55
  55. package/dist/tmagic-editor.umd.cjs +16923 -15771
  56. package/package.json +8 -8
  57. package/src/Editor.vue +3 -1
  58. package/src/components/CodeBlockEditor.vue +3 -0
  59. package/src/components/CompareForm.vue +7 -0
  60. package/src/components/FloatingBox.vue +77 -8
  61. package/src/components/ScrollBar.vue +0 -67
  62. package/src/editorProps.ts +13 -0
  63. package/src/fields/CodeSelect.vue +4 -1
  64. package/src/fields/CondOpSelect.vue +2 -24
  65. package/src/fields/DataSourceFields.vue +5 -0
  66. package/src/fields/DataSourceInput.vue +7 -25
  67. package/src/fields/DataSourceMocks.vue +3 -0
  68. package/src/fields/DisplayConds.vue +14 -0
  69. package/src/fields/EventSelect.vue +84 -129
  70. package/src/fields/StyleSetter/Index.vue +2 -8
  71. package/src/fields/StyleSetter/pro/Background.vue +94 -55
  72. package/src/fields/StyleSetter/pro/Border.vue +9 -11
  73. package/src/fields/StyleSetter/pro/Font.vue +66 -64
  74. package/src/fields/StyleSetter/pro/Layout.vue +140 -138
  75. package/src/fields/StyleSetter/pro/Position.vue +69 -67
  76. package/src/fields/StyleSetter/pro/Transform.vue +23 -25
  77. package/src/fields/UISelect.vue +0 -12
  78. package/src/index.ts +1 -0
  79. package/src/layouts/CodeEditor.vue +32 -6
  80. package/src/layouts/Framework.vue +7 -6
  81. package/src/layouts/history-list/HistoryDiffDialog.vue +23 -5
  82. package/src/layouts/history-list/useHistoryRevert.ts +25 -5
  83. package/src/layouts/props-panel/FormPanel.vue +66 -5
  84. package/src/layouts/props-panel/PropsPanel.vue +20 -6
  85. package/src/layouts/props-panel/use-style-panel.ts +4 -3
  86. package/src/layouts/sidebar/Sidebar.vue +2 -0
  87. package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +4 -1
  88. package/src/layouts/sidebar/code-block/useContentMenu.ts +13 -1
  89. package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +3 -0
  90. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +2 -2
  91. package/src/layouts/sidebar/data-source/useContentMenu.ts +13 -1
  92. package/src/layouts/sidebar/layer/LayerNodeContent.vue +57 -0
  93. package/src/layouts/sidebar/layer/LayerPanel.vue +4 -1
  94. package/src/layouts/workspace/viewer/NodeListMenu.vue +4 -1
  95. package/src/plugin.ts +3 -1
  96. package/src/services/editor.ts +168 -3
  97. package/src/services/ui.ts +6 -0
  98. package/src/theme/event.scss +8 -0
  99. package/src/theme/floating-box.scss +9 -1
  100. package/src/theme/layer-node-content.scss +14 -0
  101. package/src/theme/scroll-bar.scss +64 -0
  102. package/src/theme/theme.scss +3 -0
  103. package/src/theme/ui-select.scss +9 -0
  104. package/src/type.ts +72 -2
  105. package/src/utils/data-source/index.ts +30 -12
  106. package/src/utils/event.ts +224 -0
  107. package/src/utils/index.ts +2 -0
  108. package/src/utils/monaco-editor.ts +27 -0
  109. package/src/utils/props.ts +160 -2
  110. package/src/utils/type-match-rules.ts +678 -0
  111. package/types/index.d.ts +391 -85
package/src/plugin.ts CHANGED
@@ -21,7 +21,7 @@ import type { App } from 'vue';
21
21
  import type { DesignPluginOptions } from '@tmagic/design';
22
22
  import designPlugin from '@tmagic/design';
23
23
  import type { FormInstallOptions } from '@tmagic/form';
24
- import formPlugin from '@tmagic/form';
24
+ import formPlugin, { registerTypeMatchRules } from '@tmagic/form';
25
25
  import tablePlugin from '@tmagic/table';
26
26
 
27
27
  import Code from './fields/Code.vue';
@@ -44,6 +44,7 @@ import StyleSetter from './fields/StyleSetter/Index.vue';
44
44
  import uiSelect from './fields/UISelect.vue';
45
45
  import CodeEditor from './layouts/CodeEditor.vue';
46
46
  import { setEditorConfig } from './utils/config';
47
+ import { editorTypeMatchRules } from './utils/type-match-rules';
47
48
  import Editor from './Editor.vue';
48
49
  import type { EditorInstallOptions } from './type';
49
50
 
@@ -64,6 +65,7 @@ export default {
64
65
  app.use(designPlugin, opt || {});
65
66
  app.use(formPlugin, opt || {});
66
67
  app.use(tablePlugin);
68
+ registerTypeMatchRules(editorTypeMatchRules);
67
69
 
68
70
  app.config.globalProperties.$TMAGIC_EDITOR = option;
69
71
  setEditorConfig(option);
@@ -31,6 +31,7 @@ import {
31
31
  isPage,
32
32
  isPageFragment,
33
33
  setValueByKeyPath,
34
+ traverseNode,
34
35
  } from '@tmagic/utils';
35
36
 
36
37
  import BaseService from '@editor/services//BaseService';
@@ -47,6 +48,8 @@ import type {
47
48
  EditorNodeInfo,
48
49
  HistoryOpSource,
49
50
  HistoryOpType,
51
+ NodeInvalidInfo,
52
+ NodeInvalidSource,
50
53
  PastePosition,
51
54
  StepDiffItem,
52
55
  StepValue,
@@ -104,12 +107,18 @@ class Editor extends BaseService {
104
107
  stageLoading: true,
105
108
  highlightNode: null,
106
109
  modifiedNodeIds: new Map(),
110
+ invalidNodeIds: new Map(),
107
111
  pageLength: 0,
108
112
  pageFragmentLength: 0,
109
113
  disabledMultiSelect: false,
110
114
  alwaysMultiSelect: false,
111
115
  });
112
116
  private selectionBeforeOp: Id[] | null = null;
117
+ /**
118
+ * 操作前的节点校验错误快照,与 selectionBeforeOp 同时在 captureSelectionBeforeOp 中捕获,
119
+ * 供 pushOpHistory 写入 step.extra.invalidNodeIdsBefore,用于撤销时还原到「操作前」的错误状态。
120
+ */
121
+ private invalidNodeIdsBeforeOp: Map<Id, NodeInvalidInfo> | null = null;
113
122
  /**
114
123
  * 最近一次 pushOpHistory 写入的历史记录 uuid。
115
124
  * 供 *AndGetHistoryId 系列方法在调用普通操作后取回本次产生的历史记录 id;
@@ -127,7 +136,7 @@ class Editor extends BaseService {
127
136
 
128
137
  /**
129
138
  * 设置当前指点节点配置
130
- * @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength
139
+ * @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength
131
140
  * @param value MNode
132
141
  * @param options.historySource 设置 root 时,本次变更写入历史记录的「操作来源」(仅 name === 'root' 时生效)
133
142
  */
@@ -180,12 +189,15 @@ class Editor extends BaseService {
180
189
  }
181
190
 
182
191
  this.emit('root-change', value as StoreState['root'], preValue as StoreState['root'], options);
192
+
193
+ // 整体替换 DSL 后,清理不再存在于新 DSL 中的失效节点错误记录,避免残留误报。
194
+ this.pruneInvalidNodeIds();
183
195
  }
184
196
  }
185
197
 
186
198
  /**
187
199
  * 获取当前指点节点配置
188
- * @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength'
200
+ * @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength'
189
201
  * @returns MNode
190
202
  */
191
203
  public get<K extends StoreStateKey>(name: K): StoreState[K] {
@@ -663,6 +675,9 @@ class Editor extends BaseService {
663
675
 
664
676
  await Promise.all(nodes.map((node) => this.doRemove(node, { doNotSelect, doNotSwitchPage })));
665
677
 
678
+ // 删除节点时同步清理其(含子树)的校验错误记录;置于 pushOpHistory 之前,使历史快照与本次删除对齐。
679
+ this.removeInvalidNodesBySubtree(nodes);
680
+
666
681
  if (removedItems.length > 0 && pageForOp) {
667
682
  if (!doNotPushHistory) {
668
683
  this.pushOpHistory('remove', {
@@ -770,11 +785,23 @@ class Editor extends BaseService {
770
785
  doNotPushHistory?: boolean;
771
786
  historyDescription?: string;
772
787
  historySource?: HistoryOpSource;
788
+ /**
789
+ * 属性面板提交时携带的校验错误信息,在写入历史记录之前落库,
790
+ * 使历史快照与本次变更对齐,从而 undo/redo 能正确还原错误标记。
791
+ */
792
+ invalidInfo?: { id: Id; source: NodeInvalidSource; error?: string };
773
793
  } = {},
774
794
  ): Promise<MNode | MNode[]> {
775
795
  this.captureSelectionBeforeOp();
776
796
 
777
- const { doNotPushHistory = false, changeRecordList, changeRecords, historyDescription, historySource } = data;
797
+ const {
798
+ doNotPushHistory = false,
799
+ changeRecordList,
800
+ changeRecords,
801
+ historyDescription,
802
+ historySource,
803
+ invalidInfo,
804
+ } = data;
778
805
 
779
806
  const nodes = Array.isArray(config) ? config : [config];
780
807
 
@@ -787,6 +814,9 @@ class Editor extends BaseService {
787
814
  }),
788
815
  );
789
816
 
817
+ // 校验错误信息在 pushOpHistory 之前落库,保证历史快照包含本次变更对应的错误状态。
818
+ this.applyInvalidInfo(config, invalidInfo);
819
+
790
820
  if (updateData[0].oldNode?.type !== NodeType.ROOT) {
791
821
  const curNodes = this.get('nodes');
792
822
  if (curNodes.length) {
@@ -1542,6 +1572,7 @@ class Editor extends BaseService {
1542
1572
  this.set('stage', null);
1543
1573
  this.set('highlightNode', null);
1544
1574
  this.set('modifiedNodeIds', new Map());
1575
+ this.set('invalidNodeIds', new Map());
1545
1576
  this.set('pageLength', 0);
1546
1577
  }
1547
1578
 
@@ -1555,6 +1586,62 @@ class Editor extends BaseService {
1555
1586
  this.get('modifiedNodeIds').clear();
1556
1587
  }
1557
1588
 
1589
+ /**
1590
+ * 记录(或覆盖)某个节点在指定来源(属性表单 / 样式表单)上的校验错误信息。
1591
+ * @param id 节点 id
1592
+ * @param source 错误来源:'props'(属性表单)| 'style'(样式表单)
1593
+ * @param message 错误文案(可能是包含 <br> 的 HTML)
1594
+ */
1595
+ public setInvalidNode(id: Id, source: NodeInvalidSource, message: string) {
1596
+ const map = this.get('invalidNodeIds');
1597
+ const info: NodeInvalidInfo = { ...(map.get(id) || {}) };
1598
+ info[source] = message;
1599
+ map.set(id, info);
1600
+ this.emit('invalid-node-change', map);
1601
+ }
1602
+
1603
+ /**
1604
+ * 删除节点的校验错误记录。
1605
+ * @param id 节点 id
1606
+ * @param source 指定来源则仅删除该来源;不传则删除该节点全部来源的错误
1607
+ */
1608
+ public deleteInvalidNode(id: Id, source?: NodeInvalidSource) {
1609
+ const map = this.get('invalidNodeIds');
1610
+ if (!map.has(id)) return;
1611
+
1612
+ if (!source) {
1613
+ map.delete(id);
1614
+ } else {
1615
+ const info: NodeInvalidInfo = { ...(map.get(id) || {}) };
1616
+ delete info[source];
1617
+ if (info.props || info.style) {
1618
+ map.set(id, info);
1619
+ } else {
1620
+ map.delete(id);
1621
+ }
1622
+ }
1623
+
1624
+ this.emit('invalid-node-change', map);
1625
+ }
1626
+
1627
+ /** 获取当前存在校验错误的节点错误 Map(key 为节点 id) */
1628
+ public getInvalidNodeIds(): Map<Id, NodeInvalidInfo> {
1629
+ return this.get('invalidNodeIds');
1630
+ }
1631
+
1632
+ /** 获取指定节点的校验错误信息 */
1633
+ public getInvalidNodeInfo(id: Id): NodeInvalidInfo | undefined {
1634
+ return this.get('invalidNodeIds').get(id);
1635
+ }
1636
+
1637
+ /** 清空全部校验错误记录 */
1638
+ public resetInvalidNodeId() {
1639
+ const map = this.get('invalidNodeIds');
1640
+ if (map.size === 0) return;
1641
+ map.clear();
1642
+ this.emit('invalid-node-change', map);
1643
+ }
1644
+
1558
1645
  public usePlugin(options: AsyncHookPlugin<AsyncMethodName, Editor>): void {
1559
1646
  super.usePlugin(options);
1560
1647
  }
@@ -1577,6 +1664,68 @@ class Editor extends BaseService {
1577
1664
  return super.emit(eventName, ...args);
1578
1665
  }
1579
1666
 
1667
+ /**
1668
+ * 应用一次属性面板提交携带的校验错误信息(在写入历史记录之前调用,使历史快照与本次变更对齐)。
1669
+ * - invalidInfo 为空(调用方未携带):认为本次更新对应的节点已无校验错误,清除其全部来源的错误;
1670
+ * - error 非空则记录错误,为空则清除对应来源的错误。
1671
+ */
1672
+ private applyInvalidInfo(
1673
+ config: MNode | MNode[],
1674
+ invalidInfo?: { id: Id; source: NodeInvalidSource; error?: string },
1675
+ ) {
1676
+ if (!invalidInfo) {
1677
+ // 调用方未携带 invalidInfo:本次更新对应节点不应再保留校验错误,清除其全部来源。
1678
+ const ids = (Array.isArray(config) ? config : [config])
1679
+ .map((node) => node.id)
1680
+ .filter((id): id is Id => id !== undefined);
1681
+ ids.forEach((id) => this.deleteInvalidNode(id));
1682
+ return;
1683
+ }
1684
+ const { id, source, error } = invalidInfo;
1685
+ if (error) {
1686
+ this.setInvalidNode(id, source, error);
1687
+ } else {
1688
+ this.deleteInvalidNode(id, source);
1689
+ }
1690
+ }
1691
+
1692
+ /** 删除被移除节点(含其子树)的校验错误记录,避免残留误报 */
1693
+ private removeInvalidNodesBySubtree(nodes: MNode[]) {
1694
+ const map = this.get('invalidNodeIds');
1695
+ if (map.size === 0) return;
1696
+
1697
+ let changed = false;
1698
+ nodes.forEach((node) => {
1699
+ traverseNode(node, (n) => {
1700
+ if (n.id !== undefined && map.delete(n.id)) {
1701
+ changed = true;
1702
+ }
1703
+ });
1704
+ });
1705
+
1706
+ if (changed) {
1707
+ this.emit('invalid-node-change', map);
1708
+ }
1709
+ }
1710
+
1711
+ /** 清理不在当前 DSL 中的失效节点错误记录(用于整体替换 root 后) */
1712
+ private pruneInvalidNodeIds() {
1713
+ const map = this.get('invalidNodeIds');
1714
+ if (map.size === 0) return;
1715
+
1716
+ let changed = false;
1717
+ for (const id of [...map.keys()]) {
1718
+ if (!this.getNodeById(id, false)) {
1719
+ map.delete(id);
1720
+ changed = true;
1721
+ }
1722
+ }
1723
+
1724
+ if (changed) {
1725
+ this.emit('invalid-node-change', map);
1726
+ }
1727
+ }
1728
+
1580
1729
  private addModifiedNodeId(id: Id) {
1581
1730
  this.get('modifiedNodeIds').set(id, id);
1582
1731
  }
@@ -1598,6 +1747,9 @@ class Editor extends BaseService {
1598
1747
  private captureSelectionBeforeOp() {
1599
1748
  if (this.selectionBeforeOp) return;
1600
1749
  this.selectionBeforeOp = this.get('nodes').map((n) => n.id);
1750
+ // 与选区快照同步捕获「操作前」的校验错误状态;因 applyInvalidInfo 会在 pushOpHistory 前修改 invalidNodeIds,
1751
+ // 必须在此(操作最开始)留存操作前快照,供 undo 还原。
1752
+ this.invalidNodeIdsBeforeOp = new Map(this.get('invalidNodeIds'));
1601
1753
  }
1602
1754
 
1603
1755
  /**
@@ -1704,6 +1856,9 @@ class Editor extends BaseService {
1704
1856
  selectedBefore: this.selectionBeforeOp ?? [],
1705
1857
  selectedAfter: this.get('nodes').map((n) => n.id),
1706
1858
  modifiedNodeIds: new Map(this.get('modifiedNodeIds')),
1859
+ // 方向性双快照:undo 还原「操作前」错误状态,redo 还原「操作后」错误状态。
1860
+ invalidNodeIdsBefore: new Map(this.invalidNodeIdsBeforeOp ?? this.get('invalidNodeIds')),
1861
+ invalidNodeIdsAfter: new Map(this.get('invalidNodeIds')),
1707
1862
  },
1708
1863
  diff,
1709
1864
  };
@@ -1716,6 +1871,7 @@ class Editor extends BaseService {
1716
1871
  const historyId = pushed ? step.uuid : null;
1717
1872
  this.lastPushedHistoryId = historyId;
1718
1873
  this.selectionBeforeOp = null;
1874
+ this.invalidNodeIdsBeforeOp = null;
1719
1875
  return historyId;
1720
1876
  }
1721
1877
 
@@ -1841,6 +1997,15 @@ class Editor extends BaseService {
1841
1997
 
1842
1998
  this.set('modifiedNodeIds', step.extra?.modifiedNodeIds ?? new Map());
1843
1999
 
2000
+ // 还原校验错误标记:因 undo/redo 复用同一 step,需按方向取「操作前 / 操作后」快照——
2001
+ // undo(reverse=true) 还原到操作前的错误状态(撤销一个「校验失败」的改动后错误消失),
2002
+ // redo(reverse=false) 还原到操作后的错误状态(重做后错误恢复)。
2003
+ // 浅拷贝一份以隔离历史快照,避免后续 set/deleteInvalidNode 反向污染 step.extra。
2004
+ const invalidToRestore =
2005
+ (reverse ? step.extra?.invalidNodeIdsBefore : step.extra?.invalidNodeIdsAfter) ?? new Map();
2006
+ this.set('invalidNodeIds', new Map(invalidToRestore));
2007
+ this.emit('invalid-node-change', this.get('invalidNodeIds'));
2008
+
1844
2009
  const page = toRaw(this.get('page'));
1845
2010
  if (page) {
1846
2011
  const selectIds = (reverse ? step.extra?.selectedBefore : step.extra?.selectedAfter) ?? [];
@@ -27,6 +27,9 @@ import {
27
27
  DEFAULT_LEFT_COLUMN_WIDTH,
28
28
  DEFAULT_RIGHT_COLUMN_WIDTH,
29
29
  LEFT_COLUMN_WIDTH_STORAGE_KEY,
30
+ MIN_CENTER_COLUMN_WIDTH,
31
+ MIN_LEFT_COLUMN_WIDTH,
32
+ MIN_RIGHT_COLUMN_WIDTH,
30
33
  RIGHT_COLUMN_WIDTH_STORAGE_KEY,
31
34
  } from '@editor/utils/const';
32
35
 
@@ -54,6 +57,9 @@ const state = shallowReactive<UiState>({
54
57
  storageService.getItem(RIGHT_COLUMN_WIDTH_STORAGE_KEY, { protocol: Protocol.NUMBER }) ||
55
58
  DEFAULT_RIGHT_COLUMN_WIDTH,
56
59
  },
60
+ minLeftColumnWidth: MIN_LEFT_COLUMN_WIDTH,
61
+ minCenterColumnWidth: MIN_CENTER_COLUMN_WIDTH,
62
+ minRightColumnWidth: MIN_RIGHT_COLUMN_WIDTH,
57
63
  showGuides: true,
58
64
  hasGuides: false,
59
65
  showRule: true,
@@ -35,3 +35,11 @@
35
35
  margin-right: 5px;
36
36
  }
37
37
  }
38
+
39
+ .el-form-item {
40
+ .el-form-item {
41
+ &.is-error {
42
+ margin-bottom: 18px;
43
+ }
44
+ }
45
+ }
@@ -25,10 +25,18 @@
25
25
  .m-editor-float-box-body {
26
26
  overflow: auto;
27
27
  flex: 1;
28
- padding: 0 16px;
29
28
  }
30
29
  }
31
30
 
32
31
  .m-editor-floating-box-moveable {
33
32
  opacity: 0;
34
33
  }
34
+
35
+ .m-editor-float-box-drag-mask {
36
+ position: fixed;
37
+ top: 0;
38
+ left: 0;
39
+ width: 100%;
40
+ height: 100%;
41
+ background-color: transparent;
42
+ }
@@ -0,0 +1,14 @@
1
+ .m-editor-layer-node-content {
2
+ display: inline-flex;
3
+ align-items: center;
4
+
5
+ &.is-invalid .m-editor-layer-node-label {
6
+ color: var(--el-color-danger, #f56c6c);
7
+ }
8
+
9
+ .m-editor-layer-node-error-icon {
10
+ margin-left: 4px;
11
+ color: var(--el-color-danger, #f56c6c);
12
+ cursor: help;
13
+ }
14
+ }
@@ -0,0 +1,64 @@
1
+ .m-editor-scroll-bar {
2
+ position: absolute;
3
+ background-color: transparent;
4
+ opacity: 0.3;
5
+ transition:
6
+ background-color 0.2s linear,
7
+ opacity 0.2s linear;
8
+
9
+ .m-editor-scroll-bar-thumb {
10
+ background-color: #aaa;
11
+ border-radius: 6px;
12
+ position: absolute;
13
+ }
14
+
15
+ &.horizontal {
16
+ width: 100%;
17
+ height: 15px;
18
+ bottom: 0;
19
+
20
+ .m-editor-scroll-bar-thumb {
21
+ height: 6px;
22
+ transition:
23
+ background-color 0.2s linear,
24
+ height 0.2s ease-in-out;
25
+ bottom: 2px;
26
+ }
27
+ }
28
+
29
+ &.vertical {
30
+ height: 100%;
31
+ width: 15px;
32
+ right: 5px;
33
+
34
+ .m-editor-scroll-bar-thumb {
35
+ width: 6px;
36
+ transition:
37
+ background-color 0.2s linear,
38
+ width 0.2s ease-in-out;
39
+ right: 2px;
40
+ }
41
+ }
42
+
43
+ &:hover,
44
+ &:focus {
45
+ background-color: #eee;
46
+ opacity: 0.9;
47
+
48
+ .m-editor-scroll-bar-thumb {
49
+ background-color: #999;
50
+ }
51
+
52
+ &.horizontal {
53
+ .m-editor-scroll-bar-thumb {
54
+ height: 11px;
55
+ }
56
+ }
57
+
58
+ &.vertical {
59
+ .m-editor-scroll-bar-thumb {
60
+ width: 11px;
61
+ }
62
+ }
63
+ }
64
+ }
@@ -27,4 +27,7 @@
27
27
  @use "./page-fragment-select.scss";
28
28
  @use "./data-source-field.scss";
29
29
  @use "./data-source-field-select.scss";
30
+ @use "./scroll-bar.scss";
31
+ @use "./ui-select.scss";
32
+ @use "./layer-node-content.scss";
30
33
  @use "./style-setter/index.scss";
@@ -0,0 +1,9 @@
1
+ .m-fields-ui-select {
2
+ cursor: pointer;
3
+ i {
4
+ margin-right: 3px;
5
+ }
6
+ span {
7
+ color: #2882e0;
8
+ }
9
+ }
package/src/type.ts CHANGED
@@ -33,6 +33,7 @@ import type {
33
33
  MPage,
34
34
  MPageFragment,
35
35
  } from '@tmagic/core';
36
+ import type { FieldSize } from '@tmagic/design';
36
37
  import type { ChangeRecord, FormConfig, FormState, TableColumnConfig, TypeFunction } from '@tmagic/form';
37
38
  import type StageCore from '@tmagic/stage';
38
39
  import type {
@@ -76,7 +77,7 @@ export interface FrameworkSlots {
76
77
  empty(props: {}): any;
77
78
  workspace(props: {}): any;
78
79
  'props-panel'(props: {}): any;
79
- 'footer'(props: {}): any;
80
+ footer(props: {}): any;
80
81
  'page-bar'(props: {}): any;
81
82
  'page-bar-add-button'(props: {}): any;
82
83
  'page-bar-title'(props: { page: MPage | MPageFragment }): any;
@@ -209,6 +210,22 @@ export interface StageOptions {
209
210
  beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
210
211
  }
211
212
 
213
+ /**
214
+ * 节点校验错误信息,按来源(属性表单 / 样式表单)分别保存错误文案。
215
+ * 属性表单与样式表单是两个独立的 FormPanel,均指向同一节点,故以来源为键,
216
+ * 避免某个面板校验通过时误清另一个面板记录的错误。
217
+ * 节点视为存在错误当且仅当任一来源存在非空文本。
218
+ */
219
+ export interface NodeInvalidInfo {
220
+ /** 属性表单校验错误文案(可能为包含 <br> 的 HTML) */
221
+ props?: string;
222
+ /** 样式表单校验错误文案(可能为包含 <br> 的 HTML) */
223
+ style?: string;
224
+ }
225
+
226
+ /** 节点校验错误来源 */
227
+ export type NodeInvalidSource = keyof NodeInvalidInfo;
228
+
212
229
  export interface StoreState {
213
230
  root: MApp | null;
214
231
  page: MPage | MPageFragment | null;
@@ -219,6 +236,8 @@ export interface StoreState {
219
236
  stage: StageCore | null;
220
237
  stageLoading: boolean;
221
238
  modifiedNodeIds: Map<Id, Id>;
239
+ /** 校验失败的节点错误信息,按节点 id 存储,供组件树标记与保存拦截读取 */
240
+ invalidNodeIds: Map<Id, NodeInvalidInfo>;
222
241
  pageLength: number;
223
242
  pageFragmentLength: number;
224
243
  disabledMultiSelect: boolean;
@@ -296,6 +315,12 @@ export interface UiState {
296
315
  stageRect: StageRect;
297
316
  /** 编辑器列布局每一列的宽度,分为左中右三列 */
298
317
  columnWidth: GetColumnWidth;
318
+ /** 编辑器列布局左侧列最小宽度 */
319
+ minLeftColumnWidth: number;
320
+ /** 编辑器列布局中间列最小宽度 */
321
+ minCenterColumnWidth: number;
322
+ /** 编辑器列布局右侧列最小宽度 */
323
+ minRightColumnWidth: number;
299
324
  /** 是否显示画布参考线,true: 显示,false: 不显示,默认为true */
300
325
  showGuides: boolean;
301
326
  /** 画布上是否存在参考线 */
@@ -844,6 +869,10 @@ export interface StepExtra {
844
869
  selectedAfter?: Id[];
845
870
  /** 本次操作涉及的节点 id 集合(page 类型) */
846
871
  modifiedNodeIds?: Map<Id, Id>;
872
+ /** 操作前的节点校验错误快照,撤销后还原(使撤销一个「校验失败」的改动后错误消失) */
873
+ invalidNodeIdsBefore?: Map<Id, NodeInvalidInfo>;
874
+ /** 操作后的节点校验错误快照,重做后还原(使重做后错误恢复) */
875
+ invalidNodeIdsAfter?: Map<Id, NodeInvalidInfo>;
847
876
  [key: string]: any;
848
877
  }
849
878
  // #endregion StepExtra
@@ -1200,9 +1229,24 @@ export interface PageBarSortOptions extends PartSortableOptions {
1200
1229
  }
1201
1230
  // #endregion PageBarSortOptions
1202
1231
 
1232
+ /**
1233
+ * 右键菜单当前目标(侧栏树节点等)。
1234
+ * 数据源 / 代码块面板通过 `customContentMenu` 的 `getTarget` 暴露,业务在 handler 内自行读取。
1235
+ */
1236
+ export interface ContentMenuTarget {
1237
+ /** 目标 ID */
1238
+ id: string;
1239
+ /** 原始节点数据(树节点等) */
1240
+ data?: TreeNodeData;
1241
+ }
1242
+
1243
+ export type ContentMenuType = 'layer' | 'data-source' | 'viewer' | 'code-block';
1244
+
1203
1245
  export type CustomContentMenuFunction = (
1204
1246
  menus: (MenuButton | MenuComponent)[],
1205
- type: 'layer' | 'data-source' | 'viewer' | 'code-block',
1247
+ type: ContentMenuType,
1248
+ /** 读取当前右键目标;数据源 / 代码块面板会传入,图层 / 画布一般不需要 */
1249
+ getTarget?: () => ContentMenuTarget | null,
1206
1250
  ) => (MenuButton | MenuComponent)[];
1207
1251
 
1208
1252
  export interface EditorEvents {
@@ -1225,6 +1269,8 @@ export interface EditorEvents {
1225
1269
  * add / remove / update 触发本事件;如需区分「用户操作」与「撤销重做」请配合 `history-change`。
1226
1270
  */
1227
1271
  change: [event: EditorChangeEvent];
1272
+ /** 节点校验错误状态发生变化时触发,携带当前完整的错误 Map(供非响应式消费方订阅) */
1273
+ 'invalid-node-change': [invalidNodeIds: Map<Id, NodeInvalidInfo>];
1228
1274
  }
1229
1275
 
1230
1276
  // #region EditorChangeEvent
@@ -1352,6 +1398,14 @@ export interface DiffDialogPayload {
1352
1398
  targetLabel?: string;
1353
1399
  /** 用于标题展示的目标 id */
1354
1400
  id?: string | number;
1401
+ /**
1402
+ * 指定打开时的初始对比模式:
1403
+ * - before:该步骤修改前 vs 修改后
1404
+ * - current:该步骤修改后 vs 当前最新值
1405
+ * 不传时按是否存在「修改后的值」/「当前值」自动推断。
1406
+ * 若指定的模式当前不可用(对应数据缺失),将回退到自动推断结果。
1407
+ */
1408
+ mode?: 'before' | 'current';
1355
1409
  }
1356
1410
 
1357
1411
  /**
@@ -1408,6 +1462,12 @@ export interface UseHistoryRevertOptions {
1408
1462
  * 以保证两处 filterFunction 读取到一致的运行态上下文。
1409
1463
  */
1410
1464
  getPropsPanelFormState?: () => FormState | undefined;
1465
+ /**
1466
+ * 内置页面 / 数据源 / 代码块的差异 / 回滚确认弹窗默认宽度(透传给 TMagicDialog 的 `width`),
1467
+ * 如 `'1200px'` / `'80%'`。缺省时使用弹窗内置默认宽度(900px)。
1468
+ * 业务自有历史(`viewDiff` / `confirmAndRevert`)可在调用时通过各自入参的 `width` 单独覆盖。
1469
+ */
1470
+ dialogWidth?: string;
1411
1471
  }
1412
1472
 
1413
1473
  /**
@@ -1428,6 +1488,16 @@ export interface CustomDiffFormOptions {
1428
1488
  * 对比弹窗会用它覆盖 CompareForm 中同名扩展字段,避免上下文不一致。
1429
1489
  */
1430
1490
  compareFormState?: FormState;
1491
+ /**
1492
+ * 差异 / 确认回滚弹窗宽度(透传给 HistoryDiffDialog 的 TMagicDialog `width`),
1493
+ * 如 `'1200px'` / `'80%'`。缺省时使用弹窗内置默认宽度(900px)。
1494
+ */
1495
+ width?: string;
1496
+ /**
1497
+ * 差异 / 确认回滚弹窗内 form 表单的尺寸(透传给 CompareForm 的 `size`),
1498
+ * 可选 `'large' | 'default' | 'small'`,缺省时使用表单内置默认尺寸。
1499
+ */
1500
+ size?: FieldSize;
1431
1501
  }
1432
1502
 
1433
1503
  /**
@@ -235,19 +235,37 @@ export const getCascaderOptionsFromFields = (
235
235
  return result;
236
236
  };
237
237
 
238
- export const getFieldType = (ds: DataSourceSchema | undefined, fieldNames: string[]) => {
239
- let fields = ds?.fields;
240
- let type = '';
241
-
242
- for (const fieldName of fieldNames) {
243
- if (!fields?.length) return '';
244
-
245
- const field = fields.find((f) => f.name === fieldName);
246
- if (!field) return '';
238
+ /**
239
+ * 按字段名路径下钻 DataSchema。
240
+ * @param skipNumberIndices 为 true 时跳过数字段(模板路径中的数组下标,如 arr[0].x)
241
+ */
242
+ export const resolveFieldByPath = (
243
+ fields: DataSchema[] | undefined,
244
+ fieldNames: string[],
245
+ options: { skipNumberIndices?: boolean } = {},
246
+ ): { ok: boolean; field?: DataSchema; fields: DataSchema[] } => {
247
+ let currentFields = fields || [];
248
+ let field: DataSchema | undefined;
247
249
 
248
- type = field.type || '';
249
- fields = field.fields;
250
+ for (const name of fieldNames) {
251
+ if (options.skipNumberIndices && isNumber(name)) {
252
+ continue;
253
+ }
254
+ if (!currentFields.length) {
255
+ return { ok: false, fields: currentFields };
256
+ }
257
+ field = currentFields.find((item) => item.name === name);
258
+ if (!field) {
259
+ return { ok: false, fields: currentFields };
260
+ }
261
+ currentFields = field.fields || [];
250
262
  }
251
263
 
252
- return type;
264
+ return { field, ok: true, fields: currentFields };
265
+ };
266
+
267
+ export const getFieldType = (ds: DataSourceSchema | undefined, fieldNames: string[]) => {
268
+ const { ok, field } = resolveFieldByPath(ds?.fields, fieldNames);
269
+ if (!ok) return '';
270
+ return field?.type || '';
253
271
  };