Compare commits

...

78 Commits

Author SHA1 Message Date
roymondchen
614f12adf3 feat(editor): 支持历史记录持久化
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 17:04:39 +08:00
roymondchen
bddc6f343c feat(editor): 支持按历史记录 uuid 回滚 2026-06-05 19:25:50 +08:00
roymondchen
be3a900e6a fix(editor): 修复历史对比属性配置上下文缺失 2026-06-05 17:27:20 +08:00
roymondchen
bc555ebdc0 chore: update lockfile v1.8.0-beta.4 2026-06-04 17:15:03 +08:00
roymondchen
b7d1cea7c1 chore: release v1.8.0-beta.4 2026-06-04 17:13:59 +08:00
roymondchen
3bd0eecb42 fix(editor): 修复合并历史记录信息展示
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 17:10:54 +08:00
roymondchen
cd19dec790 fix(editor): 修复历史对比样式配置显示
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 16:59:08 +08:00
roymondchen
10b70c36bb fix(editor): 禁止缺少变更记录的历史回滚 2026-06-04 16:48:24 +08:00
roymondchen
27b2c2c685 feat(editor): 历史记录支持操作来源 2026-06-04 16:08:52 +08:00
roymondchen
a8a9cf372d chore: update lockfile v1.8.0-beta.3 2026-06-04 14:13:01 +08:00
roymondchen
6253d7ed23 chore: release v1.8.0-beta.3 2026-06-04 14:12:13 +08:00
roymondchen
444d4223a9 feat(stage): 非点击画布选中组件时高亮闪烁选中区域
从图层树、面包屑等外部选中组件时,在画布上对选中区域做一次紫色高亮闪烁,
帮助用户快速定位组件;选中页面不触发。支持通过 editor 的 disabledFlashTip 关闭。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 14:02:57 +08:00
roymondchen
a9e9e65f9c feat(editor): 历史记录列表展示时间并优化回滚差异弹窗
为历史步骤自动写入 timestamp 并按当天/跨天格式化展示;回滚确认弹窗区分标题与说明,关闭时清理确认回调。
2026-06-03 18:09:21 +08:00
roymondchen
42162f2e4a feat(editor): 历史记录差异对比弹窗关闭时派发 close 事件
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 19:56:34 +08:00
roymondchen
7a161cab00 refactor(editor): 历史记录数据源/代码块 tab 复用通用 BucketTab 2026-06-02 19:07:38 +08:00
roymondchen
1cd69b33fe feat(editor): 对比表单支持自定义 loadConfig 加载逻辑
将 CompareCategory 等类型抽取到 type.ts,
新增 CompareFormLoadConfig 支持外部接管表单配置加载,
HistoryDiffDialog 透传 loadConfig 并支持 width 配置及对外导出。
2026-06-02 17:03:27 +08:00
roymondchen
12069e0937 feat(form): submitForm 支持返回 changeRecords
新增 returnChangeRecords 选项,开启后 resolve { values, changeRecords },
便于命令式调用时获取表单变更记录,并同步更新文档与单测。
2026-06-02 16:43:07 +08:00
roymondchen
1b66ab1b88 refactor(editor): 抽取 serializeConfig 工具统一序列化配置
将分散在 CodeLink、CodeEditor 及 playground 中重复的 serialize-javascript
序列化逻辑收敛为 @editor/utils/editor 的 serializeConfig 并对外导出复用。
2026-06-02 16:34:23 +08:00
roymondchen
64d35d5363 fix(form): 对比模式下无 name 字段时不展示差异
避免 name 为空时拿整个 model/lastValues 做对比导致误判

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 16:28:08 +08:00
roymondchen
35fc394199 feat(form): fieldset legend 支持函数动态生成标题 2026-06-02 14:24:09 +08:00
roymondchen
8612311db1 feat(editor): 历史记录面板支持自定义扩展 tab 并开放 Bucket/goto 配置
新增 historyListExtraTabs 配置,可在内置页面/数据源/代码块 tab 后追加业务自定义历史 tab。
导出 HistoryListBucket 供复用,GroupRow 支持配置是否允许跳转,Bucket 支持配置是否展示初始项。
2026-06-01 19:21:36 +08:00
roymondchen
818b41f07f chore: update lockfile v1.8.0-beta.2 2026-05-29 18:56:40 +08:00
roymondchen
9b34124805 chore: release v1.8.0-beta.2 2026-05-29 18:55:38 +08:00
roymondchen
7a61a35664 fix(editor): 显式标注 CompareForm 的 defineExpose 类型以修复 DTS 构建报错
defineExpose 同时暴露 MForm 实例 ref 与递归的 FormConfig ref,导致
vue-tsc 生成声明文件时推断类型过大无法序列化(TS7056)。改为显式标注
暴露类型,使其引用具名别名而非展开完整结构。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 18:53:08 +08:00
roymondchen
025cca365c perf(dep): 依赖收集改为单次遍历批量处理多 target
将 collectItems/removeTargetsDep 改为整棵树只遍历一次、在每个属性上检查所有
target,把结构遍历开销从 ×targets 降到 ×1,收集结果保持一致。

同时修正 dataSourceMethodDeps 字段命名并补充到 MApp schema。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 17:55:13 +08:00
roymondchen
a3333e2b4e feat(editor): 新增 hideSidebar 配置支持隐藏左侧面板
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:49:10 +08:00
roymondchen
cbc4b25072 feat(editor): 字段对比模式逐项展示差异并补充历史记录面板文档
- CodeSelect/CodeSelectCol/EventSelect/DataSource 等复合字段在对比模式下
  按索引对齐前后值,逐项展示新增/删除/修改高亮,并隐藏写操作按钮
- form 容器/列表/表格支持对比模式只读展示
- 新增「历史记录面板」指南文档,完善表单对比文档及 menu props 说明
- 补充相关单元测试

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 15:51:47 +08:00
roymondchen
b02aa75ddc feat(editor): 历史记录面板支持单步回滚(类 git revert)
将目标历史步骤的修改作为一次新操作反向应用,不破坏原有栈结构,
page/dataSource/codeBlock 三类 service 均提供 revert 能力;
面板新增关闭按钮、步骤编号展示与合并组卡片样式优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 14:19:44 +08:00
roymondchen
f0c66427b8 feat: form 新增 showDiff prop 支持自定义对比判断
- form: MForm/Container 新增 showDiff prop,允许调用方自定义

  '是否展示对比内容' 的判断逻辑,并在嵌套 Container 中自动透传;

  不传时沿用默认的 isEqual 行为

- editor: CompareForm 利用该能力处理 code-select 字段中 '' 与

  { hookType: 'code', hookData: [] } 两种语义为空形态被 isEqual 误判为差异的问题

- docs: 补充 form-props.md 中 showDiff 的说明与示例

- test: 补充 Code 字段相关单测
2026-05-28 20:30:05 +08:00
roymondchen
c854dfa8bf feat(editor): vs-code 字段对比模式改用 monaco diff 编辑器
- Container.vue 新增「自接管对比」字段类型白名单(当前含 vs-code),命中时只渲染一次组件并透传 model/lastValues/isCompare,由字段内部展示差异
- Code.vue 在 isCompare 模式下切换到 type='diff',使用 monaco 内置 diff 视图替代两个独立编辑器实例
- CodeEditor.vue 补充对 modifiedValues 的 watch,避免 diff 模式下右侧值停留在初始快照
2026-05-28 20:12:46 +08:00
roymondchen
59f4e0edac feat(editor): 历史记录面板支持差异对比
- 新增 HistoryDiffDialog 历史差异对比弹窗
- 新增 CompareForm 表单对比组件
- 抽取 code-block 工具函数到 utils/code-block.ts
- 历史列表面板支持选择两个版本进行对比
2026-05-28 19:49:03 +08:00
roymondchen
0f8abf7298 fix: 对比模式下关闭 tab-pane 的 lazy,确保差异数能正确统计 2026-05-28 19:32:54 +08:00
roymondchen
62a2ee6693 feat(editor): 历史记录面板支持点击跳转与回到初始状态
- 单步组头部点击跳转到该步骤;合并组头部点击展开/收起,子步行点击跳转到具体步骤
- 列表底部新增「初始」记录项,可一键回到所有修改之前的状态
- editorService/dataSourceService/codeBlockService 新增 goto API;historyService 暴露 cursor 读取器
2026-05-28 18:52:11 +08:00
roymondchen
0446202ba6 feat(editor): 新增历史记录列表面板
- 新增 history-list 模块(面板、Tab、Bucket、GroupRow 与 composables)
- NavMenu 接入历史记录面板入口
- history/editor/codeBlock/dataSource service 配合面板能力调整
- utils/undo-redo 适配新面板
- 扩展 type.ts 相关类型定义
- 新增 history-list-panel.scss 并在 theme.scss 引入
- 补充 history-list 模块完整单元测试
- playground 同步小幅调整
2026-05-28 17:51:52 +08:00
roymondchen
285434ef3e feat(form): 支持自定义 label slot
在 MForm / Container 上新增具名作用域插槽 label,允许使用方自定义表单项标题渲染。
Slot 作用域参数:config、type、text、prop、disabled。
类型 FormLabelSlotProps / FormSlots 提取到 schema.ts 复用。
2026-05-28 16:45:11 +08:00
roymondchen
8dae67769c feat(editor): 数据源与代码块 service 支持 undo/redo
- dataSourceService / codeBlockService 新增 undo / redo / canUndo / canRedo 方法
- undo/redo 内部复用 add / update / remove / setCodeDslByIdSync / deleteCodeDslByIds 写回,
  并强制 doNotPushHistory,借此自动驱动 initService 中的依赖收集链路
  (DepTargetType.DATA_SOURCE / DATA_SOURCE_COND / DATA_SOURCE_METHOD / CODE_BLOCK)
- 更新场景下若 step 带 changeRecords,按 propPath 局部 patch,不冲掉同节点其它无关变更;
  缺省退化为整 schema / 整内容替换
- 补充对应单测与 API 文档
2026-05-28 16:40:49 +08:00
roymondchen
09558fa027 feat(editor): 历史记录接入 changeRecords,undo/redo 按 propPath 局部更新
- 节点 / 数据源 / 代码块的 history step 增加 changeRecords 字段

- editor.update / dataSource.update / codeBlock.setCodeDslById(Sync) 透传 changeRecords 入历史

- applyHistoryOp 的 update 分支:携带 changeRecords 时,按 propPath 从 oldNode/newNode 取值

  构造最小 patch 走 update,不冲掉同节点上其它无关变更;缺省退化为整节点替换

  (覆盖 sort/moveLayer/拖动等纯快照场景)

- editor.update 增加 changeRecordList 形参,多节点场景每个节点单独保留 records;

  use-stage 多选拖动 / 缩放改用 changeRecordList,避免 records 在多节点间共享

- use-code-block-edit.submitCodeBlockHandler 透传 form changeRecords

- 同步更新 editor / dataSource / codeBlock / history service 文档
2026-05-28 16:28:35 +08:00
roymondchen
4c855ba50b feat(editor): 写操作支持 doNotPushHistory 选项以跳过历史记录
- editor/codeBlock/dataSource 的 add/update/delete 等接口新增 doNotPushHistory 选项
- 移除不再使用的 editor-history 工具及其单测
- 修复 layer 节点状态在重建时丢失已有 status 的问题
- 同步更新 service 方法文档,新增 dragto 复现用例
2026-05-28 16:03:29 +08:00
roymondchen
e2c065f90d feat(editor): 代码块与数据源支持按 id 独立的历史记录
- history service 新增 pushCodeBlock/undoCodeBlock/redoCodeBlock
  /canUndoCodeBlock/canRedoCodeBlock 及数据源对称 API
- 按 id 维度各自维护独立 UndoRedo 栈,与页面/节点历史完全解耦
- type 新增 CodeBlockStepValue / DataSourceStepValue 独立类型
- HistoryState 扩展 codeBlockState / dataSourceState 字段
- codeBlockService.setCodeDslByIdSync / deleteCodeDslByIds 自动入历史
- dataSourceService.add / update / remove 自动入历史
- 入栈成功时 emit code-block-history-change / data-source-history-change
- 补充单测共 21 例,更新 history/codeBlock/dataSource 相关文档
2026-05-27 19:50:17 +08:00
roymondchen
a341c7d73e fix(editor): 多选时对多个节点的操作合并入同一条历史记录
- moveToContainer 支持数组形参,多选移动整批只产生一条历史记录

- use-stage 拖动多选元素入容器 / 多选拖动缩放整批合成一次调用

- 右键移动至改走 moveToContainer,避免 remove+add 切成两条历史

- 跳过选中目标节点的分支清理 state.nodes 残留旧引用

- history.push 新增可选 pageId 参数,跨页操作正确落到目标页栈

- pushOpHistory 显式按 step.data.id 入栈,避免跨页操作错配
2026-05-27 19:09:34 +08:00
roymondchen
de94a75803 refactor(editor): 移除 BaseService 废弃的 use/middleware 机制
- 删除已 @deprecated 的 BaseService.use 方法及其 middleware 通道

- 删除 utils/compose.ts 及对应测试(仅服务于 middleware,无其他引用)

- editor.ts 移除 safeOptions/safeParent 兜底,相关方法 options 改用形参默认值

- props.ts fillConfig 的 labelWidth 改为形参默认值,移除 typeof function 兜底

- 同步更新 5 份 service 方法文档,删除 ## use 章节
2026-05-27 18:55:38 +08:00
roymondchen
d01a28ce76 fix(editor): 修复移动到菜单导致节点引用异常的问题 2026-05-27 17:17:43 +08:00
roymondchen
6c40425d8c chore: update lockfile v1.8.0-beta.1 2026-05-27 11:28:31 +08:00
roymondchen
b8b0490260 chore: release v1.8.0-beta.1 2026-05-27 11:27:14 +08:00
roymondchen
2846f9eb2a fix(core): app.emit 在节点配置事件时不应短路 super.emit
去掉 eventHelper.emit 前的 return,避免节点配置 events 后 app.on 注册的监听器被吞掉,并补充回归测试。
2026-05-27 11:20:48 +08:00
roymondchen
62fc818ae1 refactor(form-schema): style-setter 继承 containercommonconfig 2026-05-26 21:10:22 +08:00
roymondchen
ff810d09e4 feat(editor): 数据源字段选择按钮在对比模式与禁用态下禁止切换
- 按钮新增 disabled 绑定 (props.disabled || mForm?.isCompare)

- 抽取 onToggleDataSourceFieldSelectHandler 增加 guard 防御

- 补充对应单元测试
2026-05-26 21:05:01 +08:00
roymondchen
b1193b909e feat(editor): 样式设置器 StyleSetter 支持表单对比模式
- Index.vue 透传 lastValues/isCompare 给各分类子组件,并冒泡 addDiffCount

- pro 下 6 个分类组件接受新 props 并向 MContainer 传递

- Layout/Border 同时将新 props 传递给内部 Box/Border 组件

- components/Border.vue 接受新 props 并冒泡 MContainer 的 addDiffCount

- components/Box.vue 接受 props 以保持接口一致

- 补充单元测试覆盖透传与事件冒泡
2026-05-26 20:59:43 +08:00
roymondchen
540a2716d8 fix(editor): serializeConfig 只去掉对象 key 的引号,避免破坏字符串 value 内的引号 2026-05-26 20:20:51 +08:00
roymondchen
a1fcb191d2 feat(eslint-config): 禁止匿名 default class/function 导出
新增 no-restricted-syntax 规则,禁止匿名形式的
`export default class {}` 与 `export default function () {}`。

匿名 default 导出在 dts 聚合(rolldown / api-extractor /
vue-tsc 等)时会被命名为 `export_default`,导致跨包继承链在
.vue / .tsx 下解析失败,父类成员(如 EventEmitter 的 on/off)
无法被 ts-plugin 推断。

同时重申 base.mjs 中已有的 ForIn / Labeled / With 选择器,
避免在 .ts/.tsx 下被本规则整体覆盖。
2026-05-26 17:09:37 +08:00
roymondchen
b9a6dd5b84 fix(editor): 修复 root 整体替换时图层面板节点状态残留与组件树闪烁问题 2026-05-26 17:06:45 +08:00
roymondchen
08011efd6d refactor(form): 使用 getter 访问 props 字段并补充单元测试
- formState 中与 props 对应的字段改用 getter,避免 props 与 formState 之间的同步中间态
- 完善 extendState 同步段的响应式追踪说明注释
- 新增 Form.extra.spec.ts 覆盖 isCompare 模式与 config 变化场景
2026-05-26 11:51:34 +08:00
roymondchen
fbbd05e291 chore: update lockfile v1.8.0-beta.0 2026-05-22 16:54:18 +08:00
roymondchen
9b65917371 chore: release v1.8.0-beta.0 2026-05-22 16:53:17 +08:00
roymondchen
3d038513e3 feat(editor): 新增 DSL 修改方法的 doNotSwitchPage 选项
在 add / remove / doRemove / sort / paste / alignCenter / moveToContainer
的 options 对象中新增 doNotSwitchPage,与 doNotSelect 合并为同一配置 DslOpOptions,
用于在 DSL 操作(新增 / 删除 / 跨页移动)会引发当前页面切换时跳过该次切换。

- 抽取共用类型 DslOpOptions 到 type.ts 并对外导出
- 新增 editorService.isOnDifferentPage 辅助方法用于跨页判断
- 修复 doUpdate 同步 state.page 时无条件覆盖的问题:只在被更新页就是当前页时才同步引用,避免「更新非当前页」误把编辑器切到该页
- doRemove 中对已删除节点的引用清理与当前页清空逻辑提升为无条件执行,避免 doNotSelect / doNotSwitchPage 跳过后续 select 时 state 持有已删除节点
- 补充对应单元测试与 API 文档

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 16:49:52 +08:00
roymondchen
eb1c5a3ec1 fix(editor): 属性面板 padding 仅作用于最外层表单
为 PropsPanel 顶层 MForm 增加 .m-editor-props-form-panel-form 专属类名,
将原本挂在通用 .tmagic-design-form 上的 padding 与 tab 样式迁移到该类,
避免子组件中嵌套的 .tmagic-design-form 被错误命中。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 15:57:13 +08:00
roymondchen
7ff590b1b6 chore: update lockfile v1.7.14-beta.3 2026-05-21 16:11:51 +08:00
roymondchen
7eeb9b544e chore: release v1.7.14-beta.3 2026-05-21 16:10:48 +08:00
roymondchen
638c3e9f3c feat(form): 新增 submitForm 命令式提交函数
提供脱离组件树以函数方式完成一次表单校验/提交的能力,类似 ElMessage 用法:
传入 config/initValues 等 props 后内部临时挂载 Form 实例,
初始化完成即调用 submitForm,校验通过 resolve 表单值、失败 reject,
最后自动卸载,并支持 appContext 继承、timeout 与 native 透传。

同步补充单元测试、API 文档及侧边栏入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 15:55:28 +08:00
roymondchen
2d31b3812f feat(form): 容器组件新增 extendState 属性
FormBox、FormDialog、FormDrawer 新增 extendState 属性,并透传给内部 MForm,
方便外层注入 $message、$store 等扩展上下文到 formState。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 14:58:43 +08:00
roymondchen
05e512b1fe feat(editor): 新增 DSL 修改方法的 doNotSelect 选项
- add/remove/sort/alignCenter/moveToContainer/paste 新增 doNotSelect 选项,控制操作后是否自动触发选中变化
- doUpdate/doRemove 改为始终同步当前选中列表中的节点引用,避免 state 持有已被替换/已删除的过期节点
- 顺手修复 doUpdate 在 splice(-1) 时误改最后一个选中项的 bug
- 移除 update/doUpdate 的 selectedAfterUpdate 参数(语义已内化),move 不再暴露无意义的 doNotSelect
- 新增 safeOptions / safeParent 辅助函数,兜底插件机制将 dispatch 注入到形参位置的场景

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 17:20:04 +08:00
roymondchen
1e69bc221d refactor(utils): 放宽 isPop/isPage/isPageFragment 入参为仅需 type 字段
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 16:25:42 +08:00
roymondchen
12ce19fb02 fix(form): 修复table-group-list中model属性可能为undefined导致的报错 2026-05-18 20:07:49 +08:00
roymondchen
aa2ee9fd4b fix(form): select 在 model 值变化时补拉 init 选项
配置 config.option 时监听 model 字段变化,若当前 options 缺少对应项则重新 getInitOption,并补充单测覆盖。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 17:49:21 +08:00
roymondchen
f00e84793d chore: update lockfile v1.7.14-beta.2 2026-05-18 13:36:06 +08:00
roymondchen
297e5cebb0 chore: release v1.7.14-beta.2 2026-05-18 13:35:04 +08:00
roymondchen
5ba2019d0b chore: 更新pnpm 2026-05-18 13:17:57 +08:00
roymondchen
c45df6f6ec build: 优化test性能 2026-05-18 12:49:04 +08:00
roymondchen
f1aedc4ce7 fix(editor): 修复 CodeEditor setValue 时滚动位置与折叠等视图状态丢失
使用 saveViewState/restoreViewState 替代 getPosition/setPosition,并放到
nextTick 中执行,避免被 setAutoHeight 的 setScrollTop(0) 覆盖,导致光标
位置变化时编辑器滚动跳回顶部。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 12:09:45 +08:00
roymondchen
873a51fc87 docs: 升级 VitePress 至 v2 alpha,类型引用改为源码片段同步
- 升级 vitepress 到 ^2.0.0-alpha.17
- vite.optimizeDeps.rolldownOptions.transform.define 迁移至 vite.define 以适配 v2 API
- 同步升级 vitest/rolldown/vue/vite 等周边依赖
- 文档中类型链接统一改为 <<< 片段引用源码 region,避免 commit hash 链接失效
- packages/{core,editor,form-schema,schema,stage} 相关类型加 // #region 锚点
- 移除已废弃的 docs/guide/advanced/tmagic-ui.md 及侧栏入口

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 11:47:03 +08:00
roymondchen
d16ab9a805 docs: 重写快速开始与 runtime 指南,与 playground/runtime 源码对齐
快速开始:
- 补充 admin-client / runtime 项目结构说明
- 完善 UI adapter(element-plus / tdesign-vue-next)说明
- 增加 Monaco worker 注入与常见报错处理
- 重写 m-editor 完整示例,对齐 playground 源码

runtime 指南:
- 完善 tmagic.config.ts 与 .tmagic 入口产物说明
- 拆分 playground / page 双入口实现细节
- 新增 vite 多入口构建、跨域方案
- 补充 @tmagic/vue-runtime-help 常用 Hook 表格

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 19:40:49 +08:00
roymondchen
df8790042f feat(editor): 导航菜单支持菜单项溢出收纳,新增 NavMenuColumn 组件
- 抽离每列渲染逻辑为 NavMenuColumn 组件,监听容器宽度
- 容器空间不足时自动隐藏溢出项,并通过更多按钮 Popover 展开
- ToolButton 暴露根元素引用,便于父级测量宽度
- design ButtonProps 新增 bg 属性,用于更多按钮的激活态样式
- 补充 NavMenuColumn / NavMenu / ToolButton 的单元测试

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 19:33:53 +08:00
roymondchen
e64d86660d fix(form): 修复 Select 在 value 为空时仍发起 initUrl 请求的问题
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 19:32:04 +08:00
roymondchen
0efd23d6ab chore: update lockfile v1.7.14-beta.1 2026-05-14 19:38:24 +08:00
roymondchen
f13f94ca2d chore: release v1.7.14-beta.1 2026-05-14 19:37:22 +08:00
roymondchen
54a5570419 feat(form): 支持 TextConfig handler 返回 Promise,buttonClickHandler 改为 async await
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 19:32:19 +08:00
roymondchen
2ad5101471 fix(editor): 修复 StyleSetter 嵌套场景下 propPath 丢失上下文路径的问题
当 prop 与 name 不一致(如 data.items.0.style)时,原实现固定使用 name 会丢失上下文路径,
改为优先使用 prop,回退到 name,确保 changeRecords 携带完整路径。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 19:17:48 +08:00
roymondchen
ab6918f43d test: 完善测试用例 2026-05-14 15:26:22 +08:00
364 changed files with 39775 additions and 3118 deletions

View File

@ -1 +1 @@
npm test
npm run test

View File

@ -1,3 +1,151 @@
# [1.8.0-beta.4](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.3...v1.8.0-beta.4) (2026-06-04)
### Bug Fixes
* **editor:** 修复历史对比样式配置显示 ([cd19dec](https://github.com/Tencent/tmagic-editor/commit/cd19dec7907cac5cff775f1cbde24cb3f384e87b))
* **editor:** 修复合并历史记录信息展示 ([3bd0eec](https://github.com/Tencent/tmagic-editor/commit/3bd0eecb42d06f06f50cc4736ecc31cc07cc1886))
* **editor:** 禁止缺少变更记录的历史回滚 ([10b70c3](https://github.com/Tencent/tmagic-editor/commit/10b70c36bbace6af48bf6fa63f2df0704c6861af))
### Features
* **editor:** 历史记录支持操作来源 ([27b2c2c](https://github.com/Tencent/tmagic-editor/commit/27b2c2c68598264e97a1e1ecc34121829851c85e))
# [1.8.0-beta.3](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.2...v1.8.0-beta.3) (2026-06-04)
### Bug Fixes
* **form:** 对比模式下无 name 字段时不展示差异 ([64d35d5](https://github.com/Tencent/tmagic-editor/commit/64d35d53631698e8d94362765a1621654bd3d1f6))
### Features
* **editor:** 历史记录列表展示时间并优化回滚差异弹窗 ([a9e9e65](https://github.com/Tencent/tmagic-editor/commit/a9e9e65f9c50e47b22de8eab7184cebd87632bc6))
* **editor:** 历史记录差异对比弹窗关闭时派发 close 事件 ([42162f2](https://github.com/Tencent/tmagic-editor/commit/42162f2e4ac651ad78ff2f5291e00639a658a1ae))
* **editor:** 历史记录面板支持自定义扩展 tab 并开放 Bucket/goto 配置 ([8612311](https://github.com/Tencent/tmagic-editor/commit/8612311db12a22adcc30188ae1ead03729fa6a7a))
* **editor:** 对比表单支持自定义 loadConfig 加载逻辑 ([1cd69b3](https://github.com/Tencent/tmagic-editor/commit/1cd69b33fecd75fe8522d9a261e1c03e806ecf69))
* **form:** fieldset legend 支持函数动态生成标题 ([35fc394](https://github.com/Tencent/tmagic-editor/commit/35fc39419902e14e2d5bdf98f99802f05a4b5934))
* **form:** submitForm 支持返回 changeRecords ([12069e0](https://github.com/Tencent/tmagic-editor/commit/12069e0937589cf9b7684e4bd5ed927e15462513))
* **stage:** 非点击画布选中组件时高亮闪烁选中区域 ([444d422](https://github.com/Tencent/tmagic-editor/commit/444d4223a943d763a33b752ffbbfa704591820ca))
# [1.8.0-beta.2](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.1...v1.8.0-beta.2) (2026-05-29)
### Bug Fixes
* **editor:** 修复移动到菜单导致节点引用异常的问题 ([d01a28c](https://github.com/Tencent/tmagic-editor/commit/d01a28ce76203765f333548b30b4ec2954e68d4c))
* **editor:** 多选时对多个节点的操作合并入同一条历史记录 ([a341c7d](https://github.com/Tencent/tmagic-editor/commit/a341c7d73e78f0727c1adffce767b6806d356beb))
* **editor:** 显式标注 CompareForm 的 defineExpose 类型以修复 DTS 构建报错 ([7a61a35](https://github.com/Tencent/tmagic-editor/commit/7a61a356649838531f4f51c45e2e76ab84474107))
* 对比模式下关闭 tab-pane 的 lazy确保差异数能正确统计 ([0f8abf7](https://github.com/Tencent/tmagic-editor/commit/0f8abf729854f5bfc3fbad98153a77e947ead246))
### Features
* **editor:** vs-code 字段对比模式改用 monaco diff 编辑器 ([c854dfa](https://github.com/Tencent/tmagic-editor/commit/c854dfa8bf80bd501534b98c72fa1b2802076cac))
* **editor:** 代码块与数据源支持按 id 独立的历史记录 ([e2c065f](https://github.com/Tencent/tmagic-editor/commit/e2c065f90d12d1234edd3620430262857a014ee9))
* **editor:** 写操作支持 doNotPushHistory 选项以跳过历史记录 ([4c855ba](https://github.com/Tencent/tmagic-editor/commit/4c855ba50b69a2e0ab73f944171c4d5561d5a06a))
* **editor:** 历史记录接入 changeRecordsundo/redo 按 propPath 局部更新 ([09558fa](https://github.com/Tencent/tmagic-editor/commit/09558fa0273af0b7d25b4338a8ea56810b09bb1c))
* **editor:** 历史记录面板支持单步回滚(类 git revert ([b02aa75](https://github.com/Tencent/tmagic-editor/commit/b02aa75ddc2b37a024a8966ddad96cf8d85317bb))
* **editor:** 历史记录面板支持差异对比 ([59f4e0e](https://github.com/Tencent/tmagic-editor/commit/59f4e0edac47e986a83a3f9b7862cf92650b7fee))
* **editor:** 历史记录面板支持点击跳转与回到初始状态 ([62a2ee6](https://github.com/Tencent/tmagic-editor/commit/62a2ee66931caed51f86bf170c3bce96c7e40dea))
* **editor:** 字段对比模式逐项展示差异并补充历史记录面板文档 ([cbc4b25](https://github.com/Tencent/tmagic-editor/commit/cbc4b25072542d98f19707a11b87be0295157216))
* **editor:** 数据源与代码块 service 支持 undo/redo ([8dae677](https://github.com/Tencent/tmagic-editor/commit/8dae67769c32dbf65413d47ac56ca46e65eaeecf))
* **editor:** 新增 hideSidebar 配置支持隐藏左侧面板 ([a3333e2](https://github.com/Tencent/tmagic-editor/commit/a3333e2b4e0f05b2f83c9dc539466ebd31c04250))
* **editor:** 新增历史记录列表面板 ([0446202](https://github.com/Tencent/tmagic-editor/commit/0446202ba6aaf0c99265b367343c7a4d1a8201e9))
* form 新增 showDiff prop 支持自定义对比判断 ([f0c6642](https://github.com/Tencent/tmagic-editor/commit/f0c66427b8e011252110a11c90a109f5f58d3101))
* **form:** 支持自定义 label slot ([285434e](https://github.com/Tencent/tmagic-editor/commit/285434ef3effd94c51d3ed10198842f6e689046a))
### Performance Improvements
* **dep:** 依赖收集改为单次遍历批量处理多 target ([025cca3](https://github.com/Tencent/tmagic-editor/commit/025cca365c87d755abfc047786ac9a75758019f5))
# [1.8.0-beta.1](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.0...v1.8.0-beta.1) (2026-05-27)
### Bug Fixes
* **core:** app.emit 在节点配置事件时不应短路 super.emit ([2846f9e](https://github.com/Tencent/tmagic-editor/commit/2846f9eb2a8655175a024b16eaba22b522e88603))
* **editor:** serializeConfig 只去掉对象 key 的引号,避免破坏字符串 value 内的引号 ([540a271](https://github.com/Tencent/tmagic-editor/commit/540a2716d8e8e7b947ec5aa6352736dff6ee225c))
* **editor:** 修复 root 整体替换时图层面板节点状态残留与组件树闪烁问题 ([b9a6dd5](https://github.com/Tencent/tmagic-editor/commit/b9a6dd5b84d6f043eda94dbc1a07b75aea87e6f2))
### Features
* **editor:** 数据源字段选择按钮在对比模式与禁用态下禁止切换 ([ff810d0](https://github.com/Tencent/tmagic-editor/commit/ff810d09e41163834f0ac9fd2057bd9fb9d53c55))
* **editor:** 样式设置器 StyleSetter 支持表单对比模式 ([b1193b9](https://github.com/Tencent/tmagic-editor/commit/b1193b909e5e15f78783f72eb21959a52128e973))
* **eslint-config:** 禁止匿名 default class/function 导出 ([a1fcb19](https://github.com/Tencent/tmagic-editor/commit/a1fcb191d243b3c7034f31f753757ca4bbd83f5f))
# [1.8.0-beta.0](https://github.com/Tencent/tmagic-editor/compare/v1.7.14-beta.3...v1.8.0-beta.0) (2026-05-22)
### Bug Fixes
* **editor:** 属性面板 padding 仅作用于最外层表单 ([eb1c5a3](https://github.com/Tencent/tmagic-editor/commit/eb1c5a3ec1c5987b50c700dfb9019aad695e042a))
### Features
* **editor:** 新增 DSL 修改方法的 doNotSwitchPage 选项 ([3d03851](https://github.com/Tencent/tmagic-editor/commit/3d038513e3f0d1c303332fd902c1ef83d7dfe860))
## [1.7.14-beta.3](https://github.com/Tencent/tmagic-editor/compare/v1.7.14-beta.2...v1.7.14-beta.3) (2026-05-21)
### Bug Fixes
* **form:** select 在 model 值变化时补拉 init 选项 ([aa2ee9f](https://github.com/Tencent/tmagic-editor/commit/aa2ee9fd4b08a4a2896eead33dfd1d4ba029c501))
* **form:** 修复table-group-list中model属性可能为undefined导致的报错 ([12ce19f](https://github.com/Tencent/tmagic-editor/commit/12ce19fb02af7ac621d220b7e6d0a98859e631de))
### Features
* **editor:** 新增 DSL 修改方法的 doNotSelect 选项 ([05e512b](https://github.com/Tencent/tmagic-editor/commit/05e512b1fe978e26aa3064e7deae9a1aeadcae25))
* **form:** 容器组件新增 extendState 属性 ([2d31b38](https://github.com/Tencent/tmagic-editor/commit/2d31b3812f2195f4afc5f16774e155f00cb0ec20))
* **form:** 新增 submitForm 命令式提交函数 ([638c3e9](https://github.com/Tencent/tmagic-editor/commit/638c3e9f3cb550da2749fd4814c3bec9d518d081))
## [1.7.14-beta.2](https://github.com/Tencent/tmagic-editor/compare/v1.7.14-beta.1...v1.7.14-beta.2) (2026-05-18)
### Bug Fixes
* **editor:** 修复 CodeEditor setValue 时滚动位置与折叠等视图状态丢失 ([f1aedc4](https://github.com/Tencent/tmagic-editor/commit/f1aedc4ce7f93dd07cb4b7b3c1d39e459b504176))
* **form:** 修复 Select 在 value 为空时仍发起 initUrl 请求的问题 ([e64d866](https://github.com/Tencent/tmagic-editor/commit/e64d86660d83769b498de05d221b900a8c9c5b3c))
### Features
* **editor:** 导航菜单支持菜单项溢出收纳,新增 NavMenuColumn 组件 ([df87900](https://github.com/Tencent/tmagic-editor/commit/df8790042fd1309a6599c2db45bae7c61e1a2600))
## [1.7.14-beta.1](https://github.com/Tencent/tmagic-editor/compare/v1.7.14-beta.0...v1.7.14-beta.1) (2026-05-14)
### Bug Fixes
* **editor:** 修复 StyleSetter 嵌套场景下 propPath 丢失上下文路径的问题 ([2ad5101](https://github.com/Tencent/tmagic-editor/commit/2ad51014719632b9b6b141f025ed54c3ad20921d))
### Features
* **form:** 支持 TextConfig handler 返回 PromisebuttonClickHandler 改为 async await ([54a5570](https://github.com/Tencent/tmagic-editor/commit/54a5570419690de6a42704187120e40c622edbf1))
## [1.7.14-beta.0](https://github.com/Tencent/tmagic-editor/compare/v1.7.13-beta.0...v1.7.14-beta.0) (2026-05-11)

View File

@ -103,9 +103,10 @@ export default defineConfig({
link: '/guide/advanced/data-source.md'
},
{
text: '@tmagic/ui',
link: '/guide/advanced/tmagic-ui.md',
text: '历史记录面板',
link: '/guide/advanced/history-list.md',
},
{
text: '@tmagic/form',
link: '/guide/advanced/tmagic-form.md',
@ -253,6 +254,15 @@ export default defineConfig({
},
]
},
{
text: '工具函数',
items: [
{
text: 'submitForm',
link: '/api/form/submit-form'
},
]
},
],
},
{
@ -551,14 +561,8 @@ export default defineConfig({
},
vite: {
optimizeDeps: {
rolldownOptions: {
transform: {
define: {
global: 'globalThis',
},
},
},
define: {
global: 'globalThis',
},
resolve: {
alias:[

View File

@ -1,9 +1,20 @@
# codeBlockService方法
写入历史栈的方法([setCodeDslById](#setcodedslbyid)、[setCodeDslByIdSync](#setcodedslbyidsync)、[deleteCodeDslByIds](#deletecodedslbyids) 等)的 `options` 支持
[historyDescription / historySource](./editorServiceMethods.md#历史记录相关-options),会透传到 `historyService.pushCodeBlock``historyDescription` / `source` 字段。
## setCodeDsl
- **参数:**
- {[CodeBlockDSL](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/schema/src/index.ts#L75)} codeDsl 代码块DSL
- {`CodeBlockDSL`} codeDsl 代码块DSL
::: details 查看 CodeBlockDSL 及关联类型定义
<<< @/../packages/schema/src/index.ts#CodeBlockDSL{ts}
<<< @/../packages/schema/src/index.ts#CodeBlockContent{ts}
<<< @/../packages/schema/src/index.ts#CodeParam{ts}
:::
- **返回:**
- `{Promise<void>}`
@ -15,7 +26,7 @@
## getCodeDsl
- **返回:**
- {[CodeBlockDSL](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/schema/src/index.ts#L75) | null}
- {`CodeBlockDSL` | null}
- **详情:**
@ -27,7 +38,7 @@
- `{string | number}` id 代码块id
- **返回:**
- {[CodeBlockContent](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/schema/src/index.ts#L79) | null}
- {`CodeBlockContent` | null}
- **详情:**
@ -39,7 +50,16 @@
- **参数:**
- `{string | number}` id 代码块id
- {Partial<[CodeBlockContent](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/schema/src/index.ts#L79)>} codeConfig 代码块内容配置信息
- {Partial<`CodeBlockContent`>} codeConfig 代码块内容配置信息
- `{Object}` options 可选配置
- {`ChangeRecord`[]} changeRecords form 端变更记录,用于历史记录的精细化撤销/重做
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
- `{HistoryOpSource}` historySource 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
::: details 查看 ChangeRecord 类型定义
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
:::
- **返回:**
- `{Promise<void>}`
@ -52,8 +72,13 @@
- **参数:**
- `{string | number}` id 代码块id
- {Partial<[CodeBlockContent](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/schema/src/index.ts#L79)>} codeConfig 代码块内容配置信息
- {Partial<`CodeBlockContent`>} codeConfig 代码块内容配置信息
- `{boolean}` force 是否强制写入,默认 `true`;为 `false` 时若同 id 已存在则跳过
- `{Object}` options 可选配置
- {`ChangeRecord`[]} changeRecords form 端变更记录,用于历史记录的精细化撤销/重做
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
- `{HistoryOpSource}` historySource 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
- **返回:**
- `{void}`
@ -62,13 +87,20 @@
同步版本的 [setCodeDslById](#setcodedslbyid),并会触发 `addOrUpdate` 事件
::: tip
写入成功时(`force=false` 且同 id 已存在的跳过场景除外)会自动调用 `historyService.pushCodeBlock`
把本次变更入历史栈,参见 [historyService.pushCodeBlock](./historyServiceMethods.md#pushcodeblock)。
传入的 `changeRecords` 会一同写进 step撤销/重做时调用方可据此按 `propPath` 局部回放。
传入 `doNotPushHistory: true` 可跳过写入历史栈,常用于批量导入、外部同步等非用户操作场景。
:::
## getCodeDslByIds
- **参数:**
- `{string[]}` ids 代码块id数组
- **返回:**
- {[CodeBlockDSL](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/schema/src/index.ts#L75)} 命中的代码块dsl
- {`CodeBlockDSL`} 命中的代码块dsl
- **详情:**
@ -186,6 +218,10 @@
- **参数:**
- `{(string | number)[]}` codeIds 需要删除的代码块id数组
- `{Object}` options 可选配置
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
- `{HistoryOpSource}` historySource 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
- **返回:**
- `{Promise<void>}`
@ -194,6 +230,157 @@
在dsl数据源中删除指定id的代码块每删除一个会触发一次 `remove` 事件
::: tip
对每个实际存在并被删除的代码块,会自动调用 `historyService.pushCodeBlock` 入栈一条
`newContent=null` 的删除记录;不存在的 id 不会入历史。传入 `doNotPushHistory: true` 也可显式跳过写入历史栈。
:::
## setCodeDslByIdAndGetHistoryId
- **参数:** 同 [setCodeDslById](#setcodedslbyid)
- **返回:**
- {`Promise<string | null>`} 本次写入历史记录的 uuid未写入历史`doNotPushHistory: true` 等)时返回 `null`
- **详情:**
与 [setCodeDslById](#setcodedslbyid) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`,可用于精确引用 / 定位该条历史记录。
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
- **示例:**
```js
import { codeBlockService } from "@tmagic/editor";
const historyId = await codeBlockService.setCodeDslByIdAndGetHistoryId("code_1234", {
name: "代码块1",
content: "() => {}",
});
console.log(historyId); // 本次变更对应的历史记录 uuid或 null
```
## setCodeDslByIdSyncAndGetHistoryId
- **参数:** 同 [setCodeDslByIdSync](#setcodedslbyidsync)
- **返回:**
- {`string | null`} 本次写入历史记录的 uuid未写入历史`doNotPushHistory: true`、或 `force=false` 跳过等)时返回 `null`
- **详情:**
与 [setCodeDslByIdSync](#setcodedslbyidsync) 行为完全一致(同步),仅把返回值换成本次写入历史记录的 `uuid`
## deleteCodeDslByIdsAndGetHistoryId
- **参数:** 同 [deleteCodeDslByIds](#deletecodedslbyids)
- **返回:**
- {`Promise<string[]>`} 本次写入的全部历史记录 uuid按删除顺序未写入任何历史时返回空数组 `[]`
- **详情:**
与 [deleteCodeDslByIds](#deletecodedslbyids) 行为完全一致。由于一次可删除多个代码块、会产生多条历史记录,因此返回的是 uuid 数组(每条删除记录一个 uuid不存在的 id 不会入历史,也不会出现在返回数组中。
- **示例:**
```js
import { codeBlockService } from "@tmagic/editor";
const historyIds = await codeBlockService.deleteCodeDslByIdsAndGetHistoryId(["code_1", "code_2"]);
console.log(historyIds); // ['xxxx', 'yyyy'],或 []
```
## revertById
- **参数:**
- `{string}` uuid 目标历史记录的 uuid通常由 [setCodeDslByIdAndGetHistoryId](#setcodedslbyidandgethistoryid) 等方法返回)
- **返回:**
- {`Promise<CodeBlockStepValue | null>`} 反向应用后产生的新 step找不到对应 uuid / 该步未应用时返回 `null`
- **详情:**
通过历史记录 uuid「回滚」某条代码块历史步骤类 git revert 语义),语义同按 `(id, index)` 回滚,
仅无需调用方再传 `codeBlockId``index`:内部会按 uuid 在全部代码块栈中定位对应步骤后再回滚。
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
- **示例:**
```js
import { codeBlockService } from "@tmagic/editor";
const historyId = await codeBlockService.setCodeDslByIdAndGetHistoryId("code_1234", { name: "代码块1" });
if (historyId) {
await codeBlockService.revertById(historyId);
}
```
## undo
- **参数:**
- `{Id}` id 代码块id
- **返回:**
- `{Promise<CodeBlockStepValue | null>}` 撤销的 step栈不存在或已无可撤销时返回 `null`
- **详情:**
撤销指定代码块的最近一次变更。内部根据 [historyService](./historyServiceMethods.md) 取出 step 后,
复用 [setCodeDslByIdSync](#setcodedslbyidsync) / [deleteCodeDslByIds](#deletecodedslbyids) 写回,
并自动带上 `doNotPushHistory: true`,确保不会再次入栈。
写回会触发对应的 `addOrUpdate` / `remove` 事件,编辑器内部据此重新维护
`DepTargetType.CODE_BLOCK` 的 dep target无需调用方额外处理。
对于带有 `changeRecords` 的更新 step会按 `propPath` 局部 patch 当前代码块内容;缺省才退化为整内容替换,
避免冲掉同代码块上的其它无关变更。
- **示例:**
```js
import { codeBlockService } from "@tmagic/editor";
if (codeBlockService.canUndo("code_1234")) {
await codeBlockService.undo("code_1234");
}
```
## redo
- **参数:**
- `{Id}` id 代码块id
- **返回:**
- `{Promise<CodeBlockStepValue | null>}` 重做的 step栈不存在或已无可重做时返回 `null`
- **详情:**
重做指定代码块的下一次变更。其它行为同 [undo](#undo)。
## canUndo
- **参数:**
- `{Id}` id 代码块id
- **返回:**
- `{boolean}`
- **详情:**
当前指定代码块是否可撤销,等价于 `historyService.canUndoCodeBlock(id)`
## canRedo
- **参数:**
- `{Id}` id 代码块id
- **返回:**
- `{boolean}`
- **详情:**
当前指定代码块是否可重做,等价于 `historyService.canRedoCodeBlock(id)`
## setParamsColConfig
- **参数:**
@ -227,9 +414,25 @@
## copyWithRelated
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]} config 组件节点配置
- {`MNode` | `MNode`[]} config 组件节点配置
- `{TargetOptions}` collectorOptions 可选的依赖收集器配置
::: details 查看 MNode 及关联类型定义
<<< @/../packages/schema/src/index.ts#MNode{ts}
<<< @/../packages/schema/src/index.ts#MComponent{ts}
<<< @/../packages/schema/src/index.ts#MContainer{ts}
<<< @/../packages/schema/src/index.ts#MIteratorContainer{ts}
<<< @/../packages/schema/src/index.ts#MPage{ts}
<<< @/../packages/schema/src/index.ts#MApp{ts}
<<< @/../packages/schema/src/index.ts#MPageFragment{ts}
:::
- **返回:**
- `{void}`
@ -264,15 +467,11 @@
销毁 codeBlockService重置状态并移除所有事件监听和插件
## use
使用中间件的方式扩展方法,上述方法中标记有`扩展支持: 是`的方法都支持使用use扩展
## usePlugin
- **详情:**
相对于[use](#use), usePlugin支持更加灵活更加细致的扩展, 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
usePlugin支持灵活细致的扩展, 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
每个支持扩展的方法都支持定制before、after两个hook来干预原有方法的行为before可以用于修改传入参数after可以用于修改返回的值
@ -281,3 +480,4 @@
- **详情:**
删掉当前设置的所有扩展

View File

@ -4,7 +4,13 @@
- **参数:**
- {[ComponentGroup](https://github.com/Tencent/tmagic-editor/blob/5880dfbe15fcead63e9dc7c91900f8c4e7a574d8/packages/editor/src/type.ts#L355)[]} componentGroupList 组件列表配置
- {`ComponentGroup`[]} componentGroupList 组件列表配置
::: details 查看 ComponentGroup 及关联类型定义
<<< @/../packages/editor/src/type.ts#ComponentGroup{ts}
<<< @/../packages/editor/src/type.ts#ComponentItem{ts}
:::
- **返回:**
@ -48,7 +54,7 @@ componentListService.setList([
- **返回:**
- {[ComponentGroup](https://github.com/Tencent/tmagic-editor/blob/5880dfbe15fcead63e9dc7c91900f8c4e7a574d8/packages/editor/src/type.ts#L355)[]} 组件列表配置
- {`ComponentGroup`[]} 组件列表配置
- **详情:**
@ -102,3 +108,4 @@ import { componentListService } from '@tmagic/editor';
componentListService.destroy();
```

View File

@ -59,7 +59,19 @@ dataSourceService.set("editable", false);
- `{string}` type 数据源类型,默认为 'base'
- **返回:**
- {[FormConfig](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L864)} 表单配置
- {`FormConfig`} 表单配置
::: details 查看 FormConfig 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FormConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItemConfig{ts}
<<< @/../packages/form-schema/src/base.ts#ChildConfig{ts}
<<< @/../packages/form-schema/src/base.ts#DynamicTypeConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::
- **详情:**
@ -80,7 +92,7 @@ console.log(config);
- **参数:**
- `{string}` type 数据源类型
- {[FormConfig](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L864)} config 表单配置
- {`FormConfig`} config 表单配置
- **返回:**
- `{void}`
@ -120,7 +132,23 @@ dataSourceService.setFormConfig("http", [
- `{string}` type 数据源类型,默认为 'base'
- **返回:**
- {Partial<[DataSourceSchema](https://github.com/Tencent/tmagic-editor/blob/5880dfbe15fcead63e9dc7c91900f8c4e7a574d8/packages/schema/src/index.ts#L221)>} 数据源默认值
- {Partial<`DataSourceSchema`>} 数据源默认值
::: details 查看 DataSourceSchema 及关联类型定义
<<< @/../packages/schema/src/index.ts#DataSourceSchema{ts}
<<< @/../packages/schema/src/index.ts#DataSchema{ts}
<<< @/../packages/schema/src/index.ts#MockSchema{ts}
<<< @/../packages/schema/src/index.ts#CodeBlockContent{ts}
<<< @/../packages/schema/src/index.ts#CodeParam{ts}
<<< @/../packages/schema/src/index.ts#EventConfig{ts}
<<< @/../packages/schema/src/index.ts#JsEngine{ts}
:::
- **详情:**
@ -141,7 +169,7 @@ console.log(defaultValue);
- **参数:**
- `{string}` type 数据源类型
- {Partial<[DataSourceSchema](https://github.com/Tencent/tmagic-editor/blob/5880dfbe15fcead63e9dc7c91900f8c4e7a574d8/packages/schema/src/index.ts#L221)>} value 数据源默认值
- {Partial<`DataSourceSchema`>} value 数据源默认值
- **返回:**
- `{void}`
@ -170,7 +198,11 @@ dataSourceService.setFormValue("http", {
- `{string}` type 数据源类型,默认为 'base'
- **返回:**
- {[EventOption](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/core/src/events.ts#L26-L29)[]} 事件列表
- {`EventOption`[]} 事件列表
::: details 查看 EventOption 类型定义
<<< @/../packages/core/src/utils.ts#EventOption{ts}
:::
- **详情:**
@ -191,7 +223,7 @@ console.log(events);
- **参数:**
- `{string}` type 数据源类型
- {[EventOption](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/core/src/events.ts#L26-L29)[]} value 事件列表
- {`EventOption`[]} value 事件列表
- **返回:**
- `{void}`
@ -219,7 +251,7 @@ dataSourceService.setFormEvent("http", [
- `{string}` type 数据源类型,默认为 'base'
- **返回:**
- {[EventOption](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/core/src/events.ts#L26-L29)[]} 方法列表
- {`EventOption`[]} 方法列表
- **详情:**
@ -240,7 +272,7 @@ console.log(methods);
- **参数:**
- `{string}` type 数据源类型
- {[EventOption](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/core/src/events.ts#L26-L29)[]} value 方法列表
- {`EventOption`[]} value 方法列表
- **返回:**
- `{void}`
@ -265,15 +297,25 @@ dataSourceService.setFormMethod("http", [
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[DataSourceSchema](https://github.com/Tencent/tmagic-editor/blob/5880dfbe15fcead63e9dc7c91900f8c4e7a574d8/packages/schema/src/index.ts#L221)} config 数据源配置
- {`DataSourceSchema`} config 数据源配置
- `{Object}` options 可选配置
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
- `{HistoryOpSource}` historySource 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
- **返回:**
- {[DataSourceSchema](https://github.com/Tencent/tmagic-editor/blob/5880dfbe15fcead63e9dc7c91900f8c4e7a574d8/packages/schema/src/index.ts#L221)} 添加后的数据源配置
- {`DataSourceSchema`} 添加后的数据源配置
- **详情:**
添加一个数据源如果配置中没有id或id已存在会自动生成新的id
::: tip
添加成功会自动调用 `historyService.pushDataSource` 入栈一条 `oldSchema=null` 的新增记录,
参见 [historyService.pushDataSource](./historyServiceMethods.md#pushdatasource)。
传入 `doNotPushHistory: true` 可跳过写入历史栈,常用于批量导入、外部同步等非用户操作场景。
:::
- **示例:**
```js
@ -294,17 +336,30 @@ console.log(newDs.id); // 自动生成的id
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[DataSourceSchema](https://github.com/Tencent/tmagic-editor/blob/5880dfbe15fcead63e9dc7c91900f8c4e7a574d8/packages/schema/src/index.ts#L221)} config 数据源配置
- {`DataSourceSchema`} config 数据源配置
- `{Object}` options 可选配置
- {[ChangeRecord](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/form/src/schema.ts#L27-L39)[]} changeRecords 变更记录
- {`ChangeRecord`[]} changeRecords 变更记录
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
- `{HistoryOpSource}` historySource 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
::: details 查看 ChangeRecord 类型定义
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
:::
- **返回:**
- {[DataSourceSchema](https://github.com/Tencent/tmagic-editor/blob/5880dfbe15fcead63e9dc7c91900f8c4e7a574d8/packages/schema/src/index.ts#L221)} 更新后的数据源配置
- {`DataSourceSchema`} 更新后的数据源配置
- **详情:**
更新数据源
::: tip
更新成功会自动调用 `historyService.pushDataSource` 入栈一条 `oldSchema` / `newSchema`
均为对应 schema 的更新记录,传入的 `changeRecords` 也会一并写进 step撤销/重做时调用方可据此按
`propPath` 局部回放,缺省才退化为整 schema 替换。传入 `doNotPushHistory: true` 可跳过写入历史栈。
:::
- **示例:**
```js
@ -326,6 +381,10 @@ console.log(updatedDs);
- **参数:**
- `{string}` id 数据源id
- `{Object}` options 可选配置
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
- `{HistoryOpSource}` historySource 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
- **返回:**
- `{void}`
@ -334,6 +393,11 @@ console.log(updatedDs);
删除指定id的数据源
::: tip
对实际存在的数据源会自动调用 `historyService.pushDataSource` 入栈一条 `newSchema=null`
的删除记录;不存在的 id 不会入历史。传入 `doNotPushHistory: true` 也可显式跳过写入历史栈。
:::
- **示例:**
```js
@ -342,6 +406,78 @@ import { dataSourceService } from "@tmagic/editor";
dataSourceService.remove("ds_123");
```
## addAndGetHistoryId
- **参数:** 同 [add](#add)
- **返回:**
- {`string` | null} 本次写入历史记录的 uuid未写入历史`doNotPushHistory: true` 等)时返回 `null`
- **详情:**
与 [add](#add) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`,可用于精确引用 / 定位该条历史记录。
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
- **示例:**
```js
import { dataSourceService } from "@tmagic/editor";
const historyId = dataSourceService.addAndGetHistoryId({
type: "http",
title: "用户信息",
url: "/api/user",
});
console.log(historyId); // 本次新增对应的历史记录 uuid或 null
```
## updateAndGetHistoryId
- **参数:** 同 [update](#update)
- **返回:**
- {`string` | null} 本次写入历史记录的 uuid未写入历史时返回 `null`
- **详情:**
与 [update](#update) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## removeAndGetHistoryId
- **参数:** 同 [remove](#remove)
- **返回:**
- {`string` | null} 本次写入历史记录的 uuid删除的 id 不存在或未写入历史时返回 `null`
- **详情:**
与 [remove](#remove) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## revertById
- **参数:**
- `{string}` uuid 目标历史记录的 uuid通常由 [addAndGetHistoryId](#addandgethistoryid) 等方法返回)
- **返回:**
- {`DataSourceStepValue` | null} 反向应用后产生的新 step找不到对应 uuid / 该步未应用时返回 `null`
- **详情:**
通过历史记录 uuid「回滚」某条数据源历史步骤类 git revert 语义),语义同按 `(id, index)` 回滚,
仅无需调用方再传 `dataSourceId``index`:内部会按 uuid 在全部数据源栈中定位对应步骤后再回滚。
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
- **示例:**
```js
import { dataSourceService } from "@tmagic/editor";
const historyId = dataSourceService.addAndGetHistoryId({ type: "http", title: "用户信息" });
if (historyId) {
dataSourceService.revertById(historyId);
}
```
## createId
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
@ -370,7 +506,7 @@ console.log(id); // 'ds_xxx-xxx-xxx'
- `{string}` id 数据源id
- **返回:**
- {[DataSourceSchema](https://github.com/Tencent/tmagic-editor/blob/5880dfbe15fcead63e9dc7c91900f8c4e7a574d8/packages/schema/src/index.ts#L221) | undefined} 数据源配置
- {`DataSourceSchema` | undefined} 数据源配置
- **详情:**
@ -385,12 +521,94 @@ const ds = dataSourceService.getDataSourceById("ds_123");
console.log(ds);
```
## undo
- **参数:**
- `{Id}` id 数据源id
- **返回:**
- {`DataSourceStepValue` | null} 撤销的 step栈不存在或已无可撤销时返回 `null`
- **详情:**
撤销指定数据源的最近一次变更。内部根据 [historyService](./historyServiceMethods.md) 取出 step 后,
复用 [add](#add) / [update](#update) / [remove](#remove) 写回,并自动带上 `doNotPushHistory: true`
确保不会再次入栈。
写回会触发对应的 `add` / `update` / `remove` 事件,编辑器内部据此重新维护数据源相关的依赖收集
`DepTargetType.DATA_SOURCE` / `DATA_SOURCE_COND` / `DATA_SOURCE_METHOD`),无需调用方额外处理。
对于带有 `changeRecords` 的更新 step会按 `propPath` 局部 patch 当前数据源;缺省才退化为整 schema 替换,
避免冲掉同节点上的其它无关变更。
- **示例:**
```js
import { dataSourceService } from "@tmagic/editor";
if (dataSourceService.canUndo("ds_123")) {
dataSourceService.undo("ds_123");
}
```
## redo
- **参数:**
- `{Id}` id 数据源id
- **返回:**
- {`DataSourceStepValue` | null} 重做的 step栈不存在或已无可重做时返回 `null`
- **详情:**
重做指定数据源的下一次变更。其它行为同 [undo](#undo)。
## canUndo
- **参数:**
- `{Id}` id 数据源id
- **返回:**
- `{boolean}`
- **详情:**
当前指定数据源是否可撤销,等价于 `historyService.canUndoDataSource(id)`
## canRedo
- **参数:**
- `{Id}` id 数据源id
- **返回:**
- `{boolean}`
- **详情:**
当前指定数据源是否可重做,等价于 `historyService.canRedoDataSource(id)`
## copyWithRelated
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]} config 组件节点配置
- {`MNode` | `MNode`[]} config 组件节点配置
- `{TargetOptions}` collectorOptions 可选的收集器配置
::: details 查看 MNode 及关联类型定义
<<< @/../packages/schema/src/index.ts#MNode{ts}
<<< @/../packages/schema/src/index.ts#MComponent{ts}
<<< @/../packages/schema/src/index.ts#MContainer{ts}
<<< @/../packages/schema/src/index.ts#MIteratorContainer{ts}
<<< @/../packages/schema/src/index.ts#MPage{ts}
<<< @/../packages/schema/src/index.ts#MApp{ts}
<<< @/../packages/schema/src/index.ts#MPageFragment{ts}
:::
- **返回:**
- `{void}`
@ -505,3 +723,4 @@ import { dataSourceService } from "@tmagic/editor";
dataSourceService.removeAllPlugins();
```

View File

@ -4,37 +4,71 @@
- **详情:** dsl跟节点发生变化[editorService.set('root', {})](./editorServiceMethods.md#set)后触发
- **事件回调函数:** (value: [MApp](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/schema/src/index.ts?plain=1#L66-L73), preValue?: [MApp](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/schema/src/index.ts?plain=1#L66-L73)) => void
- **事件回调函数:** `(value: MApp, preValue?: MApp) => void`
::: details 查看 MApp 及关联类型定义
<<< @/../packages/schema/src/index.ts#MApp{ts}
<<< @/../packages/schema/src/index.ts#MComponent{ts}
<<< @/../packages/schema/src/index.ts#NodeType{ts}
<<< @/../packages/schema/src/index.ts#MPage{ts}
<<< @/../packages/schema/src/index.ts#MPageFragment{ts}
<<< @/../packages/schema/src/index.ts#CodeBlockDSL{ts}
<<< @/../packages/schema/src/index.ts#DataSourceSchema{ts}
<<< @/../packages/schema/src/index.ts#DataSourceDeps{ts}
:::
## select
- **详情:** 选中组件,[editorService.select()](./editorServiceMethods.md#select)后触发
- **事件回调函数:** (node: [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)) => void
- **事件回调函数:** `(node: MNode) => void`
::: details 查看 MNode 及关联类型定义
<<< @/../packages/schema/src/index.ts#MNode{ts}
<<< @/../packages/schema/src/index.ts#MComponent{ts}
<<< @/../packages/schema/src/index.ts#MContainer{ts}
<<< @/../packages/schema/src/index.ts#MPage{ts}
<<< @/../packages/schema/src/index.ts#MPageFragment{ts}
:::
## add
- **详情:** 添加节点后触发,[editorService.add()](./editorServiceMethods.md#add)后触发
- **事件回调函数:** (node: [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]) => void
- **事件回调函数:** `(node: MNode[]) => void`
## remove
- **详情:** 删除节点后触发,[editorService.remove()](./editorServiceMethods.md#remove)后触发
- **事件回调函数:** (node: [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]) => void
- **事件回调函数:** `(node: MNode[]) => void`
## update
- **详情:** 更新组件后触发,[editorService.update()](./editorServiceMethods.md#update)后触发
- **事件回调函数:** (data: Array<{ newNode: [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210); oldNode: [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210); changeRecords?: [ChangeRecord](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/form/src/schema.ts#L27-L39)[] }>) => void
- **事件回调函数:** `(data: Array<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }>) => void`
::: details 查看 ChangeRecord 类型定义
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
:::
## move-layer
- **详情:** 移动节点层级后触发,[editorService.moveLayer()](./editorServiceMethods.md#movelayer)后触发
- **事件回调函数:** (offset: number | [LayerOffset](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/type.ts)) => void
- **事件回调函数:** `(offset: number | LayerOffset) => void`
其中 `LayerOffset` 枚举值为 `'top'` / `'bottom'`
@ -42,10 +76,14 @@
- **详情:** 拖拽节点到指定容器后触发,[editorService.dragTo()](./editorServiceMethods.md#dragto)后触发
- **事件回调函数:** (data: { targetIndex: number; configs: [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]; targetParent: [MContainer](https://github.com/Tencent/tmagic-editor/blob/c143a5f7670ae61d80c1a2cfcc780cfb5259849d/packages/schema/src/index.ts#L54-L59) }) => void
- **事件回调函数:** `(data: { targetIndex: number; configs: MNode | MNode[]; targetParent: MContainer }) => void`
::: details 查看 MContainer 类型定义
<<< @/../packages/schema/src/index.ts#MContainer{ts}
:::
## history-change
- **详情:** 历史记录改变,[editorService.redo()editorService.undo()](./editorServiceMethods.md#undo)后触发
- **事件回调函数:** (data: [MPage](https://github.com/Tencent/tmagic-editor/blob/c143a5f7670ae61d80c1a2cfcc780cfb5259849d/packages/schema/src/index.ts#L61) | [MPageFragment](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)) => void
- **事件回调函数:** `(data: MPage | MPageFragment) => void`

View File

@ -1,5 +1,48 @@
# editorService方法
## 历史记录相关 options
下列 DSL 操作方法([add](#add)、[remove](#remove)、[update](#update) 等)的 `options` / `data` 参数,以及
[codeBlockService](./codeBlockServiceMethods.md) / [dataSourceService](./dataSourceServiceMethods.md)
`options`,在 `doNotPushHistory` 之外还可传入:
- `{string}` **historyDescription**:入栈时附带的人类可读描述,用于历史面板展示;不影响 undo/redo 行为,缺省时面板会自动生成描述
- `{HistoryOpSource}` **historySource**:操作途径,用于历史面板展示与埋点;不影响 undo/redo 行为,缺省时面板视为「未知」
编辑器内置交互(画布、树面板、配置面板、右键菜单、快捷键等)会自动传入对应的 `historySource`
业务侧程序化调用时建议显式传入(如 `api`),便于历史面板区分来源。
## 历史记录 uuid 与 \*AndGetHistoryId
每条历史记录入栈时都会自动生成一个唯一标识 `uuid`(见 [StepValue](#undo)),可用于精确引用 / 定位某一条历史记录(如埋点、回滚、跨端同步等)。
DSL 操作方法(`add` / `remove` / `update` 等)默认返回操作结果(节点 / 节点集合 / void不会返回 `uuid`。若需要拿到本次写入历史记录的 `uuid`,可改用对应的 `*AndGetHistoryId` 方法:它们与原方法行为完全一致,仅把返回值换成本次写入历史记录的 `uuid``string`)。当本次操作未写入历史(`doNotPushHistory: true`、无实际变更或提前返回)时返回 `null`
| 原方法 | 取 uuid 的方法 | 返回值 |
| --- | --- | --- |
| [add](#add) | [addAndGetHistoryId](#addandgethistoryid) | `Promise<string \| null>` |
| [remove](#remove) | [removeAndGetHistoryId](#removeandgethistoryid) | `Promise<string \| null>` |
| [update](#update) | [updateAndGetHistoryId](#updateandgethistoryid) | `Promise<string \| null>` |
| [moveLayer](#movelayer) | [moveLayerAndGetHistoryId](#movelayerandgethistoryid) | `Promise<string \| null>` |
| [moveToContainer](#movetocontainer) | [moveToContainerAndGetHistoryId](#movetocontainerandgethistoryid) | `Promise<string \| null>` |
| [dragTo](#dragto) | [dragToAndGetHistoryId](#dragtoandgethistoryid) | `Promise<string \| null>` |
[dataSourceService](./dataSourceServiceMethods.md) / [codeBlockService](./codeBlockServiceMethods.md) 也提供了同名约定的 `*AndGetHistoryId` 方法。
拿到 `uuid` 后,可在需要时按 uuid「回滚」对应的历史记录类 git revert 语义,详见[历史记录面板](../../guide/advanced/history-list.md))。相比按 index 回滚uuid 不会随栈内步骤增删而变化,更适合业务侧持有引用后再回滚:
- 页面:[editorService.revertPageStepById(uuid)](#revertpagestepbyid)
- 数据源:[dataSourceService.revertById(uuid)](./dataSourceServiceMethods.md#revertbyid)
- 代码块:[codeBlockService.revertById(uuid)](./codeBlockServiceMethods.md#revertbyid)
::: details 查看 HistoryOpOptions / DslOpOptions / HistoryOpSource 类型定义
<<< @/../packages/editor/src/type.ts#HistoryOpOptions{ts}
<<< @/../packages/editor/src/type.ts#DslOpOptions{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
:::
## get
- **参数:**
@ -78,7 +121,19 @@ editorService.set("node", {
:::
- **返回:**
- {[EditorNodeInfo](https://github.com/Tencent/tmagic-editor/blob/c143a5f7670ae61d80c1a2cfcc780cfb5259849d/packages/editor/src/type.ts#L139-L143)}
- {`EditorNodeInfo`}
::: details 查看 EditorNodeInfo 及关联类型定义
<<< @/../packages/editor/src/type.ts#EditorNodeInfo{ts}
<<< @/../packages/schema/src/index.ts#MNode{ts}
<<< @/../packages/schema/src/index.ts#MContainer{ts}
<<< @/../packages/schema/src/index.ts#MPage{ts}
<<< @/../packages/schema/src/index.ts#MPageFragment{ts}
:::
- **详情:**
@ -103,7 +158,23 @@ console.log(info.page);
- `{boolean}` raw 是否使用toRaw默认为true
- **返回:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)} 组件节点配置
- {`MNode`} 组件节点配置
::: details 查看 MNode 及关联类型定义
<<< @/../packages/schema/src/index.ts#MNode{ts}
<<< @/../packages/schema/src/index.ts#MComponent{ts}
<<< @/../packages/schema/src/index.ts#MContainer{ts}
<<< @/../packages/schema/src/index.ts#MIteratorContainer{ts}
<<< @/../packages/schema/src/index.ts#MPage{ts}
<<< @/../packages/schema/src/index.ts#MApp{ts}
<<< @/../packages/schema/src/index.ts#MPageFragment{ts}
:::
- **详情:**
@ -126,7 +197,7 @@ console.log(node);
- `{boolean}` raw 是否使用toRaw默认为true
- **返回:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)} 指点组件的父节点配置
- {`MNode`} 指点组件的父节点配置
- **详情:**
@ -142,16 +213,43 @@ const parent = editorService.getParentById("text_123");
console.log(parent);
```
## isOnDifferentPage
- **参数:**
- {`MNode`} node 节点配置
- **返回:**
- `{boolean}` true 表示该节点位于非当前页面(即选中该节点将会引起当前页面切换)
- **详情:**
判断给定节点是否位于非当前页面,通常用于配合 `doNotSwitchPage` 选项判断 DSL 操作是否会引起页面切换
- **示例:**
```js
import { editorService } from "@tmagic/editor";
const otherPageNode = editorService.getNodeById("text_456");
if (editorService.isOnDifferentPage(otherPageNode)) {
console.log("该节点在其它页面,操作会触发页面切换");
}
```
## getLayout
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)} parent
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)} node 可选
- {`MNode`} parent
- {`MNode`} node 可选
- **返回:**
- {Promise<[Layout](https://github.com/Tencent/tmagic-editor/blob/c143a5f7670ae61d80c1a2cfcc780cfb5259849d/packages/editor/src/type.ts#L297-L302)>} 当前布局模式
- {Promise<`Layout`>} 当前布局模式
::: details 查看 Layout 类型定义
<<< @/../packages/editor/src/type.ts#Layout{ts}
:::
- **详情:**
@ -179,10 +277,10 @@ editorService.getLayout(parent).then((layout) => {
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {number | string | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)} config 需要选中的节点或节点ID
- {number | string | `MNode`} config 需要选中的节点或节点ID
- **返回:**
- {Promise<[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)>} 当前选中的节点配置
- {Promise<`MNode`>} 当前选中的节点配置
- **详情:**
@ -229,7 +327,7 @@ editorService.get("stage")?.multiSelect(["text_123", "button_123"]);
## selectNextNode
- **返回:**
- {Promise<[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | null>} 选中后的节点配置
- {Promise<`MNode` | null>} 选中后的节点配置
- **详情:**
@ -238,7 +336,7 @@ editorService.get("stage")?.multiSelect(["text_123", "button_123"]);
## selectNextPage
- **返回:**
- {Promise<[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)>} 选中后的页面配置
- {Promise<`MNode`>} 选中后的页面配置
- **详情:**
@ -258,7 +356,7 @@ editorService.get("stage")?.multiSelect(["text_123", "button_123"]);
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {number | string | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)} config 需要高亮的节点或节点ID
- {number | string | `MNode`} config 需要高亮的节点或节点ID
- **返回:**
- `{Promise<void>}`
@ -280,12 +378,12 @@ editorService.highlight("text_123");
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)} node 新组件节点
- {`MNode`} node 新组件节点
- {[MContainer](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L139)} parent 指定的容器节点
- {`MContainer`} parent 指定的容器节点
- **返回:**
- {Promise<[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)>} 新增的组件
- {Promise<`MNode`>} 新增的组件
- **详情:**
@ -296,12 +394,19 @@ editorService.highlight("text_123");
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]} node 新组件节点配置或多个节点集合
- {`MNode` | `MNode`[]} node 新组件节点配置或多个节点集合
- {[MContainer](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L139)} parent 指定的容器组件节点配置,如果不设置,默认为当前选中的组件的父节点
- {`MContainer`} parent 指定的容器组件节点配置,如果不设置,默认为当前选中的组件的父节点
- `{Object}` options 可选配置
- `{boolean}` doNotSelect 添加后是否不更新当前选中节点(默认 false添加后会选中新增的节点
- `{boolean}` doNotSwitchPage 添加后是否不切换当前页面(默认 false新增页面 / 跨页新增时为 true 会跳过会引发页面切换的选中操作)
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
- **返回:**
- {Promise<[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]>} 新增的组件或组件集合
- {Promise<`MNode` | `MNode`[]>} 新增的组件或组件集合
- **详情:**
@ -319,7 +424,10 @@ editorService.highlight("text_123");
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)} node 要删除的节点
- {`MNode`} node 要删除的节点
- `{Object}` options 可选配置
- `{boolean}` doNotSelect 删除后是否不更新当前选中节点(默认 false
- `{boolean}` doNotSwitchPage 删除后是否不切换当前页面(默认 false删除页面 / 页面片段时为 true 会跳过自动切换到首个剩余页面)
- **返回:**
- `{Promise<void>}`
@ -328,12 +436,22 @@ editorService.highlight("text_123");
删除指定的组件或者页面
:::tip
无论是否传入 `doNotSelect` / `doNotSwitchPage`当被删除节点在当前选中列表中时state 都会自动移除该节点的引用当被删除的正好是当前页面时state.page 也会同步清空,避免持有已删除节点
:::
## remove
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[])} node 要删除的节点或节点集合
- {`MNode` | `MNode`[])} node 要删除的节点或节点集合
- `{Object}` options 可选配置
- `{boolean}` doNotSelect 删除后是否不更新当前选中节点(默认 false删除后会选中父节点或首个页面
- `{boolean}` doNotSwitchPage 删除后是否不切换当前页面(默认 false删除页面 / 页面片段时为 true 会跳过自动切换到首个剩余页面)
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
- **返回:**
- `{Promise<void>}`
@ -355,20 +473,27 @@ editorService.highlight("text_123");
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)} config 新的节点
- {`MNode`} config 新的节点
- `{Object}` data 可选配置
- {[ChangeRecord](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form/src/schema.ts#L8)[]} changeRecords 变更记录
- `{boolean}` selectedAfterUpdate 更新后是否将新节点同步到当前选中节点列表
- {`ChangeRecord`[]} changeRecords 变更记录
- **返回:**
- `{Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }>}` 更新前后的节点信息
::: details 查看 ChangeRecord 类型定义
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
:::
- **详情:**
更新节点
:::tip
节点中应该要有id不然不知道要更新哪个节点
当被更新节点正好在当前选中列表中时state 会自动同步到新的节点引用,无需调用方处理
当被更新节点正好是当前页面时state.page 也会同步到新的节点引用;更新非当前页面(不同 ID时不会把编辑器切到该页
:::
## update
@ -376,13 +501,20 @@ editorService.highlight("text_123");
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]} config 新的节点或节点集合
- {`MNode` | `MNode`[]} config 新的节点或节点集合
- `{Object}` data 可选配置
- {[ChangeRecord](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/form/src/schema.ts#L27-L39)[]} changeRecords 变更记录
- `{boolean}` selectedAfterUpdate 更新后是否同步到当前选中节点列表
- {`ChangeRecord`[]} changeRecords 单节点 form 端变更记录(多节点场景下被忽略,使用 `changeRecordList`
- {`ChangeRecord`[][]} changeRecordList 多节点 form 端变更记录列表,按 `config` 数组同序对应每个节点;优先级高于 `changeRecords`
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
::: details 查看 ChangeRecord 类型定义
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
:::
- **返回:**
- {Promise<[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]>} 新的节点或节点集合
- {Promise<`MNode` | `MNode`[]>} 新的节点或节点集合
- **详情:**
@ -396,6 +528,16 @@ editorService.highlight("text_123");
编辑器内部更新组件都是调用update来实现的update除了更新操作外还会记录历史堆还会更新[代码块](../../guide/advanced/code-block.md)关系链。
:::
:::tip
**多节点场景必须使用 `changeRecordList`**:每个节点应保留自己独立的 records不能把多个节点的
records 合并到同一个 `changeRecords` 数组里,否则 `doUpdate` / 依赖收集 / 历史回放都会按错误的
`propPath` 处理。
写入历史时,每个节点的 records 会单独保存到 `updatedItems[i].changeRecords`;撤销/重做时若有
records则仅按 `propPath` 局部更新对应字段,避免整节点替换冲掉同节点上的其它无关变更;缺省
才退化为整节点替换(如内部 `sort` / `moveLayer` / 拖动等纯快照场景)。
:::
## sort
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
@ -403,6 +545,11 @@ editorService.highlight("text_123");
- **参数:**
- `{ string | number }` id1
- `{ string | number }` id2
- `{Object}` options 可选配置
- `{boolean}` doNotSelect 排序后是否不更新当前选中节点(默认 false
- `{boolean}` doNotSwitchPage 排序后是否不切换当前页面(排序只发生在同一父节点内,方法内为空操作;保留以与其它 DSL 操作 API 一致)
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
- **返回:**
- `{Promise<void>}`
@ -418,7 +565,7 @@ editorService.highlight("text_123");
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]} config 需要复制的节点或节点集合
- {`MNode` | `MNode`[]} config 需要复制的节点或节点集合
- **返回:**
- `{void}`
@ -432,7 +579,7 @@ editorService.highlight("text_123");
## copyWithRelated
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]} config 需要复制的节点或节点集合
- {`MNode` | `MNode`[]} config 需要复制的节点或节点集合
- `{TargetOptions}` collectorOptions 可选的依赖收集器配置
- **返回:**
@ -460,10 +607,22 @@ editorService.highlight("text_123");
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[PastePosition](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/type.ts#L152-L163)} position 粘贴的坐标
- {`PastePosition`} position 粘贴的坐标
::: details 查看 PastePosition 类型定义
<<< @/../packages/editor/src/type.ts#PastePosition{ts}
:::
- `{TargetOptions}` collectorOptions 可选的依赖收集器配置
- `{Object}` options 可选配置
- `{boolean}` doNotSelect 粘贴后是否不更新当前选中节点(默认 false
- `{boolean}` doNotSwitchPage 粘贴后是否不切换当前页面(默认 false跨页粘贴时为 true 会跳过页面切换)
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
- **返回:**
- {Promise<[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]>} 添加后的组件节点配置
- {Promise<`MNode` | `MNode`[]>} 添加后的组件节点配置
- **详情:**
@ -476,10 +635,10 @@ editorService.highlight("text_123");
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)} config 需要居中的组件
- {`MNode`} config 需要居中的组件
- **返回:**
- {Promise<[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)>}
- {Promise<`MNode`>}
- **详情:**
@ -494,10 +653,16 @@ editorService.highlight("text_123");
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]} config 需要居中的组件或者组件集合
- {`MNode` | `MNode`[]} config 需要居中的组件或者组件集合
- `{Object}` options 可选配置
- `{boolean}` doNotSelect 居中后是否不更新当前选中节点(默认 false
- `{boolean}` doNotSwitchPage 居中后是否不切换当前页面(居中只更新节点 style方法内为空操作保留以与其它 DSL 操作 API 一致)
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
- **返回:**
- {Promise<[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]>}
- {Promise<`MNode` | `MNode`[]>}
- **详情:**
@ -515,6 +680,10 @@ alignCenter可以支持一次水平居中多个组件alignCenter是通过调
- **参数:**
- `{number | 'top' | 'bottom'}` offset
- `{Object}` options 可选配置
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
- **返回:**
- `{Promise<void>}`
@ -530,11 +699,17 @@ alignCenter可以支持一次水平居中多个组件alignCenter是通过调
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)} config 需要移动的节点
- {`MNode`} config 需要移动的节点
- `{string | number}` targetId 容器ID
- `{Object}` options 可选配置
- `{boolean}` doNotSelect 移动后是否不更新当前选中节点(默认 false
- `{boolean}` doNotSwitchPage 移动后是否不切换当前页面(默认 false目标容器位于其它页面时为 true 会跳过自动选中以避免页面切换)
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
- **返回:**
- Promise<[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | undefined>
- Promise<`MNode` | undefined>
- **详情:**
@ -543,9 +718,13 @@ alignCenter可以支持一次水平居中多个组件alignCenter是通过调
## dragTo
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]} config 需要拖拽的节点或节点集合
- {[MContainer](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L139)} targetParent 目标父容器
- {`MNode` | `MNode`[]} config 需要拖拽的节点或节点集合
- {`MContainer`} targetParent 目标父容器
- `{number}` targetIndex 目标位置索引
- `{Object}` options 可选配置
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
- **返回:**
- `{Promise<void>}`
@ -554,12 +733,131 @@ alignCenter可以支持一次水平居中多个组件alignCenter是通过调
将节点(支持多选)拖拽到目标容器的指定位置,会自动处理跨容器布局切换并记录历史
## addAndGetHistoryId
- **参数:** 同 [add](#add)
- **返回:**
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- **详情:**
与 [add](#add) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`,见[历史记录 uuid 与 \*AndGetHistoryId](#历史记录-uuid-与-andgethistoryid)
- **示例:**
```js
import { editorService } from "@tmagic/editor";
const historyId = await editorService.addAndGetHistoryId(
{ type: "text", text: "hello" },
parent,
{ historySource: "api" },
);
console.log(historyId); // 本次新增对应的历史记录 uuid或 null
```
## removeAndGetHistoryId
- **参数:** 同 [remove](#remove)
- **返回:**
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- **详情:**
与 [remove](#remove) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## updateAndGetHistoryId
- **参数:** 同 [update](#update)
- **返回:**
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- **详情:**
与 [update](#update) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## moveLayerAndGetHistoryId
- **参数:** 同 [moveLayer](#movelayer)
- **返回:**
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- **详情:**
与 [moveLayer](#movelayer) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## moveToContainerAndGetHistoryId
- **参数:** 同 [moveToContainer](#movetocontainer)
- **返回:**
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- **详情:**
与 [moveToContainer](#movetocontainer) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## dragToAndGetHistoryId
- **参数:** 同 [dragTo](#dragto)
- **返回:**
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- **详情:**
与 [dragTo](#dragto) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## revertPageStepById
- **参数:**
- `{string}` uuid 目标历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid)(通常由 `*AndGetHistoryId` 方法返回)
- **返回:**
- {Promise<`StepValue` | null>} 反向应用后产生的新 step找不到对应 uuid / 该步未应用 / 反向失败时返回 `null`
- **详情:**
通过历史记录 uuid「回滚」当前页面的某条历史步骤类 git revert 语义):不移动游标、不丢弃任何步骤,而是把目标 step 的修改**反向应用为一条全新的步骤**压入栈顶。语义与按 index 回滚一致,仅入参从 index 改为 uuid更适合业务侧持有引用后再回滚。
::: tip
`opType: 'update'` 的步骤必须携带 `changeRecords` 才支持回滚(否则只能整节点替换,会冲掉后续无关变更);未应用(已被撤销)的步骤无法回滚。
:::
- **示例:**
```js
import { editorService } from "@tmagic/editor";
// 执行操作时拿到本次历史记录 uuid
const historyId = await editorService.addAndGetHistoryId({ type: "text", text: "hello" });
// 之后任意时机按 uuid 回滚该步骤
if (historyId) {
await editorService.revertPageStepById(historyId);
}
```
## undo
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **返回:**
- {Promise<[StepValue](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/type.ts#L400-L404) | null>}
- {Promise<`StepValue` | null>}
::: details 查看 StepValue 及关联类型定义
<<< @/../packages/editor/src/type.ts#StepValue{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpType{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
<<< @/../packages/schema/src/index.ts#Id{ts}
:::
- **详情:**
@ -570,7 +868,17 @@ alignCenter可以支持一次水平居中多个组件alignCenter是通过调
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **返回:**
- {Promise<[StepValue](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/type.ts#L400-L404) | null>}
- {Promise<`StepValue` | null>}
::: details 查看 StepValue 及关联类型定义
<<< @/../packages/editor/src/type.ts#StepValue{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpType{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
<<< @/../packages/schema/src/index.ts#Id{ts}
:::
- **详情:**
@ -583,6 +891,10 @@ alignCenter可以支持一次水平居中多个组件alignCenter是通过调
- **参数:**
- `{number}` left
- `{number}` top
- `{Object}` options 可选配置
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
- **返回:**
- `{Promise<void>}`
@ -611,45 +923,11 @@ alignCenter可以支持一次水平居中多个组件alignCenter是通过调
移除所有事件监听清空state移除所有插件
## use
使用中间件的方式扩展方法,上述方法中标记有`扩展支持: 是`的方法都支持使用use扩展
- **示例:**
```js
import { editorService, getAddParent } from "@tmagic/editor";
import { ElMessageBox } from "element-plus";
editorService.use({
// 添加是否删除节点确认提示
async remove(node, next) {
await ElMessageBox.confirm("是否删除", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
next();
},
add(node, next) {
// text组件只能添加到container中
const parentNode = getAddParent(node);
if (node.type === "text" && parentNode?.type !== "container") {
return;
}
next();
},
});
```
## usePlugin
- **详情:**
相对于[use](#use), usePlugin支持更加灵活更加细致的扩展, 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
usePlugin支持灵活细致的扩展 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
每个支持扩展的方法都支持定制before、after两个hook来干预原有方法的行为before可以用于修改传入参数after可以用于修改返回的值
@ -673,3 +951,4 @@ editorService.usePlugin({
- **详情:**
删掉当前设置的所有扩展

View File

@ -4,7 +4,9 @@
- **详情:** 编辑器右侧组件属性配置加载完毕后触发
- **事件回调函数:** (instance: InstanceType<typeof [FormPanel](https://github.com/Tencent/tmagic-editor/blob/master/packages/editor/src/layouts/props-panel/FormPanel.vue)>) => void
- **事件回调函数:** `(instance: InstanceType<typeof FormPanel>) => void`
> [`FormPanel.vue`](https://github.com/Tencent/tmagic-editor/blob/master/packages/editor/src/layouts/props-panel/FormPanel.vue) 是属性面板组件实例
## props-panel-unmounted
@ -16,7 +18,25 @@
- **详情:** 当 [modelValue](./props.md#modelvalue-v-model)(DSL) 变化时触发,配合 `v-model` 使用
- **事件回调函数:** (value: [MApp](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/schema/src/index.ts?plain=1#L66-L73) | null) => void
- **事件回调函数:** `(value: MApp | null) => void`
::: details 查看 MApp 及关联类型定义
<<< @/../packages/schema/src/index.ts#MApp{ts}
<<< @/../packages/schema/src/index.ts#MComponent{ts}
<<< @/../packages/schema/src/index.ts#NodeType{ts}
<<< @/../packages/schema/src/index.ts#MPage{ts}
<<< @/../packages/schema/src/index.ts#MPageFragment{ts}
<<< @/../packages/schema/src/index.ts#CodeBlockDSL{ts}
<<< @/../packages/schema/src/index.ts#DataSourceSchema{ts}
<<< @/../packages/schema/src/index.ts#DataSourceDeps{ts}
:::
## props-form-error
@ -38,7 +58,13 @@
默认行为(切换可展开节点的展开/收起状态)会先于该事件执行;可通过 [`beforeLayerNodeDblclick`](./props.md#beforelayernodedblclick) 钩子拦截,返回 `false` 时该事件不会被触发
- **事件回调函数:** (event: MouseEvent, data: [TreeNodeData](https://github.com/Tencent/tmagic-editor/blob/master/packages/editor/src/type.ts)) => void
- **事件回调函数:** `(event: MouseEvent, data: TreeNodeData) => void`
::: details 查看 TreeNodeData 及关联类型定义
<<< @/../packages/editor/src/type.ts#TreeNodeData{ts}
<<< @/../packages/schema/src/index.ts#Id{ts}
:::
- **示例:**

View File

@ -4,7 +4,11 @@
- **参数:**
- {Record<string, [EventOption](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/core/src/events.ts#L26-L29)[]>} events 事件配置对象
- {Record<string, `EventOption`[]>} events 事件配置对象
::: details 查看 EventOption 类型定义
<<< @/../packages/core/src/utils.ts#EventOption{ts}
:::
- **返回:**
@ -35,7 +39,7 @@ eventsService.setEvents({
- **参数:**
- `{string}` type 组件类型
- {[EventOption](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/core/src/events.ts#L26-L29)[]} events 事件列表
- {`EventOption`[]} events 事件列表
- **返回:**
@ -64,7 +68,7 @@ eventsService.setEvent('button', [
- **返回:**
- {[EventOption](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/core/src/events.ts#L26-L29)[]} 事件列表
- {`EventOption`[]} 事件列表
- **详情:**
@ -83,7 +87,7 @@ console.log(events); // [{ label: '点击', value: 'click' }, ...]
- **参数:**
- {Record<string, [EventOption](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/core/src/events.ts#L26-L29)[]>} methods 方法配置对象
- {Record<string, `EventOption`[]>} methods 方法配置对象
- **返回:**
@ -115,7 +119,7 @@ eventsService.setMethods({
- **参数:**
- `{string}` type 组件类型
- {[EventOption](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/core/src/events.ts#L26-L29)[]} methods 方法列表
- {`EventOption`[]} methods 方法列表
- **返回:**
@ -146,7 +150,7 @@ eventsService.setMethod('video', [
- **返回:**
- {[EventOption](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/core/src/events.ts#L26-L29)[]} 方法列表
- {`EventOption`[]} 方法列表
- **详情:**
@ -200,3 +204,4 @@ import { eventsService } from '@tmagic/editor';
eventsService.destroy();
```

View File

@ -4,14 +4,105 @@
- **详情:** 页面切换
- **事件回调函数:** (undoRedo: [UndoRedo](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/utils/undo-redo.ts)) => void
- **事件回调函数:** `(undoRedo: UndoRedo) => void`
::: details 查看 UndoRedo 类定义
<<< @/../packages/editor/src/utils/undo-redo.ts#UndoRedo{ts}
:::
## change
- **详情:** 历史记录发生变化
- **事件回调函数:** (state: [StepValue](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/type.ts#L400-L404) | null) => void
- **事件回调函数:** `(state: StepValue | null) => void`
::: details 查看 StepValue 及关联类型定义
<<< @/../packages/editor/src/type.ts#StepValue{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpType{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
<<< @/../packages/schema/src/index.ts#Id{ts}
<<< @/../packages/schema/src/index.ts#MNode{ts}
:::
:::tip
当游标处于历史栈边界(已经无法继续撤销或重做)时,`UndoRedo.undo()` / `redo()` 返回 `null`,对应 `change` 回调收到的 `state``null`
:::
## code-block-history-change
- **详情:** 代码块历史记录发生变化(`pushCodeBlock` / `undoCodeBlock` / `redoCodeBlock` 成功时触发)
- **事件回调函数:** `(codeBlockId: Id, step: CodeBlockStepValue) => void`
::: details 查看 CodeBlockStepValue 及关联类型定义
<<< @/../packages/editor/src/type.ts#CodeBlockStepValue{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
<<< @/../packages/schema/src/index.ts#CodeBlockContent{ts}
<<< @/../packages/schema/src/index.ts#Id{ts}
:::
:::tip
- 新增触发的 step 中 `oldContent``null`
- 删除触发的 step 中 `newContent``null`
- `undo` / `redo` 返回 `null`(边界状态)时不会触发该事件
:::
## data-source-history-change
- **详情:** 数据源历史记录发生变化(`pushDataSource` / `undoDataSource` / `redoDataSource` 成功时触发)
- **事件回调函数:** `(dataSourceId: Id, step: DataSourceStepValue) => void`
::: details 查看 DataSourceStepValue 及关联类型定义
<<< @/../packages/editor/src/type.ts#DataSourceStepValue{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
<<< @/../packages/schema/src/index.ts#Id{ts}
:::
:::tip
- 新增触发的 step 中 `oldSchema``null`
- 删除触发的 step 中 `newSchema``null`
- `undo` / `redo` 返回 `null`(边界状态)时不会触发该事件
:::
## mark-saved
- **详情:** 调用 `markSaved` / `markPageSaved` / `markCodeBlockSaved` / `markDataSourceSaved` 标记「已保存」记录时触发
- **事件回调函数:** `(payload: { kind: 'all' | 'page' | 'code-block' | 'data-source'; id?: Id }) => void`
::: tip
- `markSaved` 触发时 `kind``all`,无 `id`
- 细粒度方法触发时 `kind` 对应类别,`id` 为目标页面 / 代码块 / 数据源 id
:::
## save-to-indexed-db
- **详情:** `saveToIndexedDB` 把历史记录写入本地 IndexedDB 成功时触发
- **事件回调函数:** `(snapshot: PersistedHistoryState) => void`
::: details 查看 PersistedHistoryState 类型定义
<<< @/../packages/editor/src/type.ts#PersistedHistoryState{ts}
<<< @/../packages/editor/src/utils/undo-redo.ts#SerializedUndoRedo{ts}
:::
## restore-from-indexed-db
- **详情:** `restoreFromIndexedDB` 从本地 IndexedDB 读取并重建历史记录成功时触发(找不到记录时不触发)
- **事件回调函数:** `(snapshot: PersistedHistoryState) => void`
::: details 查看 PersistedHistoryState 类型定义
<<< @/../packages/editor/src/type.ts#PersistedHistoryState{ts}
:::

View File

@ -4,7 +4,7 @@
- **详情:**
重置记录
重置全部历史记录(包括页面节点栈、代码块栈、数据源栈),并重置当前页面 id / canRedo / canUndo
## resetPage
@ -16,12 +16,18 @@
- **详情:**
重置历史记录全部内部状态(清空 pageId、pageSteps、canRedo、canUndo
重置历史记录全部内部状态(清空 pageId、pageSteps、canRedo、canUndo、codeBlockState、dataSourceState
## changePage
- **参数:**
- {[MPage](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L157) | [MPageFragment](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L162)} page
- `{MPage | MPageFragment} page`
::: details 查看 MPage / MPageFragment 类型定义
<<< @/../packages/schema/src/index.ts#MPage{ts}
<<< @/../packages/schema/src/index.ts#MPageFragment{ts}
:::
- **详情:**
@ -30,35 +36,349 @@
## push
- **参数:**
- {[StepValue](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/type.ts#L400-L404)} state
- `{StepValue} state`
::: details 查看 StepValue 及关联类型定义
<<< @/../packages/editor/src/type.ts#StepValue{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpType{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
<<< @/../packages/schema/src/index.ts#Id{ts}
<<< @/../packages/schema/src/index.ts#MNode{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
:::
- **返回:**
- {[StepValue](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/type.ts#L400-L404) | null}
- `{StepValue | null}`
- **详情:**
添加一条历史记录
::: tip
`opType: 'update'` 的每个 `updatedItems[i]` 上可携带 `changeRecords`,用于撤销 / 重做时仅按
`propPath` 局部更新对应字段,避免整节点替换冲掉同节点上的其它无关变更;不带
`changeRecords` 时退化为整节点替换(如 `sort` / `moveLayer` / 拖动等纯快照场景)。
`StepValue` 上的 `historyDescription` / `source` 仅用于历史面板展示与埋点,不影响 undo/redo 行为。
入栈时会为每条记录自动生成唯一标识 `uuid`(调用方未指定时),可用于精确引用 / 定位某一条历史记录。
若需要在执行 DSL 操作后拿到本次写入记录的 `uuid`,可使用 editorService / dataSourceService /
codeBlockService 提供的 `*AndGetHistoryId` 方法,参见
[editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
`pushCodeBlock` / `pushDataSource` 同样会自动写入 `uuid`
:::
## undo
- **返回:**
- {[StepValue](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/editor/src/type.ts#L554-L573) | null}
- `{StepValue | null}`
- **详情:**
撤销当前操作
撤销当前操作。`opType: 'update'` 时,若 `updatedItems[i].changeRecords` 存在,会按
`propPath``oldNode` 取值做局部回滚;否则用 `oldNode` 整节点替换。
## redo
- **返回:**
- {[StepValue](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/editor/src/type.ts#L554-L573) | null}
- `{StepValue | null}`
- **详情:**
恢复到下一步
恢复到下一步。`opType: 'update'` 时,若 `updatedItems[i].changeRecords` 存在,会按
`propPath``newNode` 取值做局部重做;否则用 `newNode` 整节点替换。
## pushCodeBlock
- **参数:**
- `{Id} codeBlockId` 代码块 id
- `{Object} payload`
- `{CodeBlockContent | null} oldContent` 变更前的代码块内容;新增时为 `null`
- `{CodeBlockContent | null} newContent` 变更后的代码块内容;删除时为 `null`
- `{ChangeRecord[]} changeRecords` 可选form 端 propPath/value 变更列表,撤销/重做时若有则按 propPath 局部更新;缺省(或空数组)才退化为整内容替换
- `{string}` historyDescription 可选;人类可读描述,用于历史面板展示;不影响 undo/redo 行为
- `{HistoryOpSource}` source 可选;操作途径,用于历史面板展示与埋点;不影响 undo/redo 行为
::: details 查看 CodeBlockStepValue 及关联类型定义
<<< @/../packages/editor/src/type.ts#CodeBlockStepValue{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
<<< @/../packages/schema/src/index.ts#CodeBlockContent{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
:::
- **返回:**
- `{CodeBlockStepValue | null}` 入栈失败(未传 id时返回 `null`
- **详情:**
推入一条代码块变更记录。与页面 / 节点完全无关,按 `codeBlockId` 维度独立一份 `UndoRedo` 栈,
栈实例存放在 `historyService.state.codeBlockState[codeBlockId]`
入栈成功后会触发 `code-block-history-change` 事件。
::: tip
`codeBlockService.setCodeDslByIdSync``codeBlockService.deleteCodeDslByIds` 内部已经
自动调用本方法,业务代码通常无需手动调用。
:::
## undoCodeBlock
- **参数:**
- `{Id} codeBlockId`
- **返回:**
- `{CodeBlockStepValue | null}` 栈不存在或已无可撤销记录时返回 `null`
- **详情:**
撤销指定代码块的最近一次变更。成功时会触发 `code-block-history-change` 事件。
拿到 step 后由调用方根据 `step.oldContent` 写回 `codeBlockService`(本方法不会自动回放)。
## redoCodeBlock
- **参数:**
- `{Id} codeBlockId`
- **返回:**
- `{CodeBlockStepValue | null}` 栈不存在或已无可重做记录时返回 `null`
- **详情:**
重做指定代码块的下一次变更。成功时会触发 `code-block-history-change` 事件。
## canUndoCodeBlock
- **参数:**
- `{Id} codeBlockId`
- **返回:**
- `{boolean}`
- **详情:**
指定代码块当前是否可撤销。栈不存在时返回 `false`
## canRedoCodeBlock
- **参数:**
- `{Id} codeBlockId`
- **返回:**
- `{boolean}`
- **详情:**
指定代码块当前是否可重做。栈不存在时返回 `false`
## pushDataSource
- **参数:**
- `{Id} dataSourceId` 数据源 id
- `{Object} payload`
- `{DataSourceSchema | null} oldSchema` 变更前的数据源 schema新增时为 `null`
- `{DataSourceSchema | null} newSchema` 变更后的数据源 schema删除时为 `null`
- `{ChangeRecord[]} changeRecords` 可选form 端 propPath/value 变更列表,撤销/重做时若有则按 propPath 局部更新;缺省(或空数组)才退化为整 schema 替换
- `{string}` historyDescription 可选;人类可读描述,用于历史面板展示;不影响 undo/redo 行为
- `{HistoryOpSource}` source 可选;操作途径,用于历史面板展示与埋点;不影响 undo/redo 行为
::: details 查看 DataSourceStepValue 及关联类型定义
<<< @/../packages/editor/src/type.ts#DataSourceStepValue{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
:::
- **返回:**
- `{DataSourceStepValue | null}` 入栈失败(未传 id时返回 `null`
- **详情:**
推入一条数据源变更记录。与页面 / 节点完全无关,按 `dataSourceId` 维度独立一份 `UndoRedo` 栈,
栈实例存放在 `historyService.state.dataSourceState[dataSourceId]`
入栈成功后会触发 `data-source-history-change` 事件。
::: tip
`dataSourceService.add` / `update` / `remove` 内部已经自动调用本方法,业务代码通常无需手动调用。
:::
## undoDataSource
- **参数:**
- `{Id} dataSourceId`
- **返回:**
- `{DataSourceStepValue | null}`
- **详情:**
撤销指定数据源的最近一次变更。成功时会触发 `data-source-history-change` 事件。
拿到 step 后由调用方根据 `step.oldSchema` 写回 `dataSourceService`(本方法不会自动回放)。
## redoDataSource
- **参数:**
- `{Id} dataSourceId`
- **返回:**
- `{DataSourceStepValue | null}`
- **详情:**
重做指定数据源的下一次变更。成功时会触发 `data-source-history-change` 事件。
## canUndoDataSource
- **参数:**
- `{Id} dataSourceId`
- **返回:**
- `{boolean}`
- **详情:**
指定数据源当前是否可撤销。栈不存在时返回 `false`
## canRedoDataSource
- **参数:**
- `{Id} dataSourceId`
- **返回:**
- `{boolean}`
- **详情:**
指定数据源当前是否可重做。栈不存在时返回 `false`
## markSaved
- **详情:**
标记「整份 DSL 已保存」:把页面 / 代码块 / 数据源所有栈当前游标所在的记录都标记为已保存(`saved = true`)。
同一栈内任意时刻最多保留一条已保存记录标记前会清除该栈内全部旧标记某个栈处于「全部已撤销」cursor 为 0时不会留下已保存记录从 IndexedDB 恢复时其游标会回到 0。
通常在 DSL 整体落库(保存到后端 / 本地)成功后调用,配合 [`restoreFromIndexedDB`](#restorefromindexeddb) 把游标恢复到此处。仅保存了其中一类时请改用更细粒度的 `markPageSaved` / `markCodeBlockSaved` / `markDataSourceSaved`
调用后会触发 `mark-saved` 事件(`{ kind: 'all' }`)。
## markPageSaved
- **参数:**
- `{Id} pageId` 可选;缺省为当前活动页
- **详情:**
标记指定页面(缺省当前活动页)历史栈的当前记录为已保存,仅影响该页面自己的栈。触发 `mark-saved` 事件(`{ kind: 'page', id }`)。
## markCodeBlockSaved
- **参数:**
- `{Id} codeBlockId`
- **详情:**
标记指定代码块历史栈的当前记录为已保存,仅影响该代码块自己的栈。触发 `mark-saved` 事件(`{ kind: 'code-block', id }`)。
## markDataSourceSaved
- **参数:**
- `{Id} dataSourceId`
- **详情:**
标记指定数据源历史栈的当前记录为已保存,仅影响该数据源自己的栈。触发 `mark-saved` 事件(`{ kind: 'data-source', id }`)。
## clearPage
- **参数:**
- `{Id} pageId` 可选;缺省为当前活动页
- **详情:**
清空指定页面(缺省当前活动页)的历史记录栈。仅删除撤销/重做记录,不会改动当前 DSL清空后该页将无法再撤销/重做之前的操作。清空当前活动页时会同步刷新 `canUndo` / `canRedo` 并触发 `change` 事件。
## clearCodeBlock
- **参数:**
- `{Id} codeBlockId` 可选;缺省清空全部代码块
- **详情:**
清空代码块历史记录栈:传入 `codeBlockId` 仅清空该代码块,缺省清空全部代码块。仅删除撤销/重做记录,不会改动代码块本身。
## clearDataSource
- **参数:**
- `{Id} dataSourceId` 可选;缺省清空全部数据源
- **详情:**
清空数据源历史记录栈:传入 `dataSourceId` 仅清空该数据源,缺省清空全部数据源。仅删除撤销/重做记录,不会改动数据源本身。
## saveToIndexedDB
- **参数:**
- `{HistoryPersistOptions} options` 可选
::: details 查看 HistoryPersistOptions / PersistedHistoryState 类型定义
<<< @/../packages/editor/src/type.ts#HistoryPersistOptions{ts}
<<< @/../packages/editor/src/type.ts#PersistedHistoryState{ts}
<<< @/../packages/editor/src/utils/undo-redo.ts#SerializedUndoRedo{ts}
:::
- **返回:**
- `{Promise<PersistedHistoryState>}` 写入成功的快照对象
- **详情:**
把当前内存中的全部历史栈(页面 / 代码块 / 数据源)连同各自游标、容量序列化后写入本地 IndexedDB。
- 最终库名为 `${dbName}-${当前 DSL app id}`,按应用隔离;
- `key` 用于在同一 store 下区分不同记录,缺省为 `default`
- 历史记录里可能包含函数(代码块内容 / 节点事件等),内部使用 `serialize-javascript` 序列化为字符串后写入,恢复时再用 `parseDSL` 还原,因此可安全持久化函数 / `Map` 等;
- 不支持 IndexedDB 的环境(如 SSR会 reject。
写入成功后触发 `save-to-indexed-db` 事件。
::: warning
`beforeunload` / `pagehide` 阶段浏览器不会等待异步 IndexedDB 事务提交,单纯依赖卸载时写入可能丢失最近一次编辑。建议在历史变更时(防抖)即调用本方法持久化,确保刷新后能完整恢复。
:::
## restoreFromIndexedDB
- **参数:**
- `{HistoryPersistOptions} options` 可选
- **返回:**
- `{Promise<PersistedHistoryState | null>}` 找不到记录时返回 `null`
- **详情:**
从本地 IndexedDB 读取此前保存的历史快照并重建全部撤销/重做栈。
- 每个栈都会按 `listMaxSize` 裁剪并还原游标;
- 若某个栈存在已保存记录(见 `markSaved`),其游标会被定位到「最近一条已保存记录」之后,使恢复后的状态与落库的 DSL 对齐;
- 会整体覆盖当前内存中的历史状态,并把活动页恢复为快照中的 `pageId`
- 找不到对应记录时返回 `null` 且不改动当前状态;不支持 IndexedDB 的环境会 reject。
成功后触发 `restore-from-indexed-db``change` 事件。
## destroy
- **详情:**
销毁

View File

@ -9,7 +9,25 @@
- **默认值:** `{}`
- **类型:** [MApp](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/schema/src/index.ts?plain=1#L66-L73)[]
- **类型:** `MApp[]`
::: details 查看 MApp 及关联类型定义
<<< @/../packages/schema/src/index.ts#MApp{ts}
<<< @/../packages/schema/src/index.ts#MComponent{ts}
<<< @/../packages/schema/src/index.ts#NodeType{ts}
<<< @/../packages/schema/src/index.ts#MPage{ts}
<<< @/../packages/schema/src/index.ts#MPageFragment{ts}
<<< @/../packages/schema/src/index.ts#CodeBlockDSL{ts}
<<< @/../packages/schema/src/index.ts#DataSourceSchema{ts}
<<< @/../packages/schema/src/index.ts#DataSourceDeps{ts}
:::
- **示例:**
@ -49,7 +67,13 @@ const dsl = ref({
- **默认值:** `[]`
- **类型:** [ComponentGroup](https://github.com/Tencent/tmagic-editor/blob/5880dfbe15fcead63e9dc7c91900f8c4e7a574d8/packages/editor/src/type.ts#L355)
- **类型:** `ComponentGroup[]`
::: details 查看 ComponentGroup 及关联类型定义
<<< @/../packages/editor/src/type.ts#ComponentGroup{ts}
<<< @/../packages/editor/src/type.ts#ComponentItem{ts}
:::
::: tip
icon使用的是[element-plus icon](https://element-plus.org/zh-CN/component/icon.html)
@ -128,7 +152,11 @@ const componentGroupList = ref([
- **默认值:** `[]`
- **类型:** [DatasourceTypeOption](https://github.com/Tencent/tmagic-editor/blob/5880dfbe15fcead63e9dc7c91900f8c4e7a574d8/packages/editor/src/type.ts#L589)
- **类型:** `DatasourceTypeOption[]`
::: details 查看 DatasourceTypeOption 类型定义
<<< @/../packages/editor/src/type.ts#DatasourceTypeOption{ts}
:::
- **示例:**
@ -169,7 +197,21 @@ const datasourceTypeList = ref([
}
```
- **类型:** [SideBarData](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/type.ts#L258-L265)
- **类型:** `SideBarData`
::: details 查看 SideBarData 及关联类型定义
<<< @/../packages/editor/src/type.ts#SideBarData{ts}
<<< @/../packages/editor/src/type.ts#SideItem{ts}
<<< @/../packages/editor/src/type.ts#SideItemKey{ts}
<<< @/../packages/editor/src/type.ts#SideComponent{ts}
<<< @/../packages/editor/src/type.ts#MenuComponent{ts}
<<< @/../packages/editor/src/type.ts#Services{ts}
:::
- **示例:**
@ -218,7 +260,7 @@ icon使用的是[element-plus icon](https://element-plus.org/zh-CN/component/ico
顶部工具栏
系统提供了几个常用功能: `'/' | 'delete' | 'undo' | 'redo' | 'zoom-in' | 'zoom-out' | 'zoom' | 'guides' | 'rule' | 'scale-to-original' | 'scale-to-fit'`
系统提供了几个常用功能: `'/' | 'delete' | 'undo' | 'redo' | 'zoom-in' | 'zoom-out' | 'zoom' | 'guides' | 'rule' | 'scale-to-original' | 'scale-to-fit' | 'history-list'`
'/': 分隔符
@ -242,13 +284,29 @@ icon使用的是[element-plus icon](https://element-plus.org/zh-CN/component/ico
'scale-to-fit': 缩放以适应
'history-list': 历史记录面板(按 页面 / 数据源 / 代码块 三个 tab 展示操作历史,相邻同目标修改自动合并,支持点击跳转、回到初始状态、单步回滚及差异对比,详见[历史记录面板](/guide/advanced/history-list.md)
- **默认值:**
```js
{ left: [], center: [], right: [] }
```
- **类型:** [MenuBarData](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/type.ts#L235-L242)
- **类型:** `MenuBarData`
::: details 查看 MenuBarData 及关联类型定义
<<< @/../packages/editor/src/type.ts#MenuBarData{ts}
<<< @/../packages/editor/src/type.ts#ColumnLayout{ts}
<<< @/../packages/editor/src/type.ts#MenuItem{ts}
<<< @/../packages/editor/src/type.ts#MenuButton{ts}
<<< @/../packages/editor/src/type.ts#MenuComponent{ts}
<<< @/../packages/editor/src/type.ts#Services{ts}
:::
- **示例:**
@ -296,7 +354,15 @@ const menu = ref({
- **默认值:** `[]`
- **类型:** ([MenuButton](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/type.ts#L168-L195) | [MenuComponent](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/type.ts#L197-L210))[]
- **类型:** `(MenuButton | MenuComponent)[]`
::: details 查看 MenuButton / MenuComponent 及关联类型定义
<<< @/../packages/editor/src/type.ts#MenuButton{ts}
<<< @/../packages/editor/src/type.ts#MenuComponent{ts}
<<< @/../packages/editor/src/type.ts#Services{ts}
:::
- **示例:**
@ -330,7 +396,9 @@ const layerContentMenu = ref([
- **默认值:** `[]`
- **类型:** ([MenuButton](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/type.ts#L168-L195) | [MenuComponent](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/type.ts#L197-L210))[]
- **类型:** `(MenuButton | MenuComponent)[]`
> 已在上面 [layerContentMenu](#layercontentmenu) 段落展开过相同类型,参考即可。
- **示例:**
@ -471,7 +539,19 @@ const renderFunction = async (stage) => {
- **默认值:** `{}`
-
- **类型:** Record<string, [FormConfig](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/form/src/schema.ts#L706)>
- **类型:** `Record<string, FormConfig>`
::: details 查看 FormConfig 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FormConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItemConfig{ts}
<<< @/../packages/form-schema/src/base.ts#ChildConfig{ts}
<<< @/../packages/form-schema/src/base.ts#DynamicTypeConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::
- **示例:**
@ -552,7 +632,11 @@ const propsValues = {
- **默认值:** `{}`
- **类型:** Record<string, { events: [EventOption](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/core/src/events.ts#L26-L29)[]; methods: [EventOption](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/core/src/events.ts#L26-L29)[] }>
- **类型:** `Record<string, { events: EventOption[]; methods: EventOption[] }>`
::: details 查看 EventOption 类型定义
<<< @/../packages/core/src/utils.ts#EventOption{ts}
:::
- **示例:**
@ -593,7 +677,23 @@ const eventMethodList = {
- **默认值:** `{}`
- **类型:** Record<string, Partial<[DataSourceSchema](https://github.com/Tencent/tmagic-editor/blob/5880dfbe15fcead63e9dc7c91900f8c4e7a574d8/packages/schema/src/index.ts#L221)>>
- **类型:** `Record<string, Partial<DataSourceSchema>>`
::: details 查看 DataSourceSchema 及关联类型定义
<<< @/../packages/schema/src/index.ts#DataSourceSchema{ts}
<<< @/../packages/schema/src/index.ts#DataSchema{ts}
<<< @/../packages/schema/src/index.ts#MockSchema{ts}
<<< @/../packages/schema/src/index.ts#CodeBlockContent{ts}
<<< @/../packages/schema/src/index.ts#CodeParam{ts}
<<< @/../packages/schema/src/index.ts#EventConfig{ts}
<<< @/../packages/schema/src/index.ts#JsEngine{ts}
:::
- **示例:**
@ -634,7 +734,9 @@ const datasourceValues = {
- **默认值:** `{}`
- **类型:** Record<string, [FormConfig](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/form/src/schema.ts#L706)>
- **类型:** `Record<string, FormConfig>`
> 已在上面 [propsConfigs](#propsconfigs) 段落展开过 `FormConfig` 类型定义,参考即可。
- **示例:**
@ -675,7 +777,11 @@ const datasourceConfigs = {
- **默认值:** `{}`
- **类型:** ((config: [CustomizeMoveableOptionsCallbackConfig](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/stage/src/types.ts#L97-L109)) => MoveableOptions) | [MoveableOptions](https://daybrush.com/moveable/release/latest/doc/)
- **类型:** `((config: CustomizeMoveableOptionsCallbackConfig) => MoveableOptions) | `[`MoveableOptions`](https://daybrush.com/moveable/release/latest/doc/)
::: details 查看 CustomizeMoveableOptionsCallbackConfig 类型定义
<<< @/../packages/stage/src/types.ts#CustomizeMoveableOptionsCallbackConfig{ts}
:::
- **示例:**
@ -1114,6 +1220,28 @@ const guidesOptions = {
</template>
```
## disabledFlashTip
- **详情:**
禁用「非点击画布选中组件时的高亮闪烁提示」。
当组件不是通过点击画布选中(如从组件树、面包屑等外部方式选中)时,编辑器会在画布上对选中区域做一次高亮闪烁,帮助用户快速定位组件在画布中的位置。设置为 `true` 可关闭该提示。
注:选中页面(`magic-ui-page`)时不会触发闪烁。
- **默认值:** `false`
- **类型:** `boolean`
- **示例:**
```html
<template>
<m-editor :disabled-flash-tip="true"></m-editor>
</template>
```
## disabledStageOverlay
- **详情:**
@ -1402,6 +1530,55 @@ const extendFormState = async (state) => {
```
:::
## historyListExtraTabs
- **详情:**
[历史记录面板](/guide/advanced/history-list.md) 的自定义扩展 tab。
业务方可借此在历史记录面板内置的「页面 / 数据源 / 代码块」三个 tab 之后追加自定义模块的历史 tab例如某个自定义模块维护自己的操作历史时可在面板中增加一个独立的 tab 来展示与回滚。
- **默认值:** `[]`
- **类型:** `HistoryListExtraTab[]`
::: details 查看 HistoryListExtraTab 类型定义
<<< @/../packages/editor/src/type.ts#HistoryListExtraTab{ts}
:::
- **示例:**
```html
<template>
<m-editor :menu="menu" :history-list-extra-tabs="historyListExtraTabs"></m-editor>
</template>
<script setup>
import { markRaw } from 'vue';
import MyModuleHistoryTab from './MyModuleHistoryTab.vue';
const historyListExtraTabs = [
{
name: 'my-module',
// label 支持字符串或函数,函数形式便于展示动态数量
label: () => '我的模块',
component: markRaw(MyModuleHistoryTab),
// 传入内容组件的 props
props: { foo: 'bar' },
// 内容组件的事件监听
listeners: {
goto: (cursor) => console.log(cursor),
},
},
];
</script>
```
::: tip
内容组件内部可自行通过 `useServices()` 获取 `historyService` 等服务来读取与回滚自定义模块的历史。
:::
## pageBarSortOptions
- **详情:**
@ -1412,7 +1589,11 @@ const extendFormState = async (state) => {
- **默认值:** `undefined`
- **类型:** [PageBarSortOptions](https://github.com/Tencent/tmagic-editor/blob/master/packages/editor/src/type.ts)
- **类型:** `PageBarSortOptions`
::: details 查看 PageBarSortOptions 类型定义
<<< @/../packages/editor/src/type.ts#PageBarSortOptions{ts}
:::
- **示例:**

View File

@ -47,11 +47,23 @@
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[FormConfig](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L864)} config
- {`FormConfig`} config
- `{string}` labelWidth 表单项 label 宽度,默认 `'80px'`
::: details 查看 FormConfig 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FormConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItemConfig{ts}
<<< @/../packages/form-schema/src/base.ts#ChildConfig{ts}
<<< @/../packages/form-schema/src/base.ts#DynamicTypeConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::
- **返回:**
- {Promise<[FormConfig](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L864)>}
- {Promise<`FormConfig`>}
- **详情:**
@ -60,7 +72,11 @@
## setPropsConfigs
- **参数:**
- {Record<string, [FormConfig](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L864) | [PropsFormConfigFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/editor/src/type.ts#L721)>} configs
- {Record<string, `FormConfig` | `PropsFormConfigFunction`>} configs
::: details 查看 PropsFormConfigFunction 类型定义
<<< @/../packages/editor/src/type.ts#PropsFormConfigFunction{ts}
:::
- **返回:**
- `{void}`
@ -75,7 +91,7 @@
- **参数:**
- `{string}` type 组件类型
- {[FormConfig](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L864)} config 属性表单配置DSL
- {`FormConfig`} config 属性表单配置DSL
- **返回:**
- `{Promise<void>}`
@ -91,10 +107,26 @@
- **参数:**
- `{string}` type 组件类型
- `{Object}` data 可选参数
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210) | null} node 当前节点
- {`MNode` | null} node 当前节点
::: details 查看 MNode 及关联类型定义
<<< @/../packages/schema/src/index.ts#MNode{ts}
<<< @/../packages/schema/src/index.ts#MComponent{ts}
<<< @/../packages/schema/src/index.ts#MContainer{ts}
<<< @/../packages/schema/src/index.ts#MIteratorContainer{ts}
<<< @/../packages/schema/src/index.ts#MPage{ts}
<<< @/../packages/schema/src/index.ts#MApp{ts}
<<< @/../packages/schema/src/index.ts#MPageFragment{ts}
:::
- **返回:**
- {Promise<[FormConfig](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L864)>}
- {Promise<`FormConfig`>}
- **详情:**
@ -103,7 +135,7 @@
## setPropsValues
- **参数:**
- {Record<string, [MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)>} values
- {Record<string, `MNode`>} values
- **返回:**
- `{void}`
@ -116,7 +148,7 @@
- **参数:**
- `{string}` type 组件类型
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)} value 组件初始值
- {`MNode`} value 组件初始值
- **返回:**
- `{Promise<void>}`
@ -134,7 +166,7 @@
- `{Object}` defaultValue 组件默认值,可选
- **返回:**
- {Promise<[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)>} 合并默认配置后的节点对象
- {Promise<`MNode`>} 合并默认配置后的节点对象
- **详情:**
@ -159,11 +191,11 @@
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)} config
- {`MNode`} config
- `{boolean}` force 是否强制设置新ID默认 `true`
- **返回:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)} 处理后的节点
- {`MNode`} 处理后的节点
- **详情:**
@ -186,8 +218,8 @@
## replaceRelateId
- **参数:**
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]} originConfigs 原始组件配置
- {[MNode](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/schema/src/index.ts#L210)[]} targetConfigs 待替换的组件配置
- {`MNode`[]} originConfigs 原始组件配置
- {`MNode`[]} targetConfigs 待替换的组件配置
- `{TargetOptions}` collectorOptions 依赖收集器配置
- **返回:**
@ -222,15 +254,11 @@
销毁propsService
## use
使用中间件的方式扩展方法,上述方法中标记有`扩展支持: 是`的方法都支持使用use扩展
## usePlugin
- **详情:**
相对于[use](#use), usePlugin支持更加灵活更加细致的扩展, 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
usePlugin支持灵活细致的扩展, 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
每个支持扩展的方法都支持定制before、after两个hook来干预原有方法的行为before可以用于修改传入参数after可以用于修改返回的值
@ -239,3 +267,4 @@
- **详情:**
删掉当前设置的所有扩展

View File

@ -22,7 +22,7 @@
- **详情:** 编辑器顶部菜单栏
- **默认:** [NavMenu.vue](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/layouts/NavMenu.vue)
- **默认:** [NavMenu.vue](https://github.com/Tencent/tmagic-editor/blob/master/packages/editor/src/layouts/NavMenu.vue)
- **插槽 Props**
- `editorService`: editorService 实例
@ -64,7 +64,7 @@
- **详情:** 左边栏
- **默认:** [Sidebar.vue](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/layouts/sidebar/Sidebar.vue)
- **默认:** [Sidebar.vue](https://github.com/Tencent/tmagic-editor/blob/master/packages/editor/src/layouts/sidebar/Sidebar.vue)
- **插槽 Props**
- `editorService`: editorService 实例
@ -259,7 +259,7 @@
- **详情:** 编辑器中间区域
- **默认:** [Workspace.vue](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/layouts/workspace/Workspace.vue)
- **默认:** [Workspace.vue](https://github.com/Tencent/tmagic-editor/blob/master/packages/editor/src/layouts/workspace/Workspace.vue)
- **插槽 Props**
- `editorService`: editorService 实例
@ -268,7 +268,7 @@
- **详情:** 画布
- **默认:** [Stage.vue](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/layouts/workspace/Stage.vue)
- **默认:** [Stage.vue](https://github.com/Tencent/tmagic-editor/blob/master/packages/editor/src/layouts/workspace/Stage.vue)
## stage-top
@ -380,7 +380,7 @@
- **详情:** 当前没有页面时,编辑器中间区域
- **默认:** [AddPageBox.vue](https://github.com/Tencent/tmagic-editor/blob/239b5d3efeae916a8cf3e3566d88063ecccc0553/packages/editor/src/layouts/AddPageBox.vue)
- **默认:** [AddPageBox.vue](https://github.com/Tencent/tmagic-editor/blob/master/packages/editor/src/layouts/AddPageBox.vue)
- **插槽 Props**
- `editorService`: editorService 实例

View File

@ -231,28 +231,11 @@ import { storageService } from '@tmagic/editor';
storageService.destroy();
```
## use
使用中间件的方式扩展方法,上述方法中标记有`扩展支持: 是`的方法都支持使用use扩展
- **示例:**
```js
import { storageService } from '@tmagic/editor';
storageService.use({
getItem(key, options, next) {
console.log('获取存储项:', key);
return next();
},
});
```
## usePlugin
- **详情:**
相对于[use](#use), usePlugin支持更加灵活更加细致的扩展, 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
usePlugin支持灵活细致的扩展 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
每个支持扩展的方法都支持定制before、after两个hook来干预原有方法的行为before可以用于修改传入参数after可以用于修改返回的值

View File

@ -179,29 +179,11 @@ import { uiService } from '@tmagic/editor';
uiService.destroy();
```
## use
使用中间件的方式扩展方法,上述方法中标记有`扩展支持: 是`的方法都支持使用use扩展
- **示例:**
```js
import { uiService } from '@tmagic/editor';
uiService.use({
async zoom(value, next) {
console.log('缩放前:', uiService.get('zoom'));
await next();
console.log('缩放后:', uiService.get('zoom'));
},
});
```
## usePlugin
- **详情:**
相对于[use](#use), usePlugin支持更加灵活更加细致的扩展, 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
usePlugin支持灵活细致的扩展 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
每个支持扩展的方法都支持定制before、after两个hook来干预原有方法的行为before可以用于修改传入参数after可以用于修改返回的值

View File

@ -6,7 +6,19 @@
- **默认值:** `[]`
- **类型:** [FormConfig](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L864)
- **类型:** `FormConfig`
::: details 查看 FormConfig 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FormConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItemConfig{ts}
<<< @/../packages/form-schema/src/base.ts#ChildConfig{ts}
<<< @/../packages/form-schema/src/base.ts#DynamicTypeConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::
- **示例:**

View File

@ -18,6 +18,8 @@
- **详情:** 提交表单,先执行校验,校验通过后清空 `changeRecords` 并返回当前表单值
- **相关:** 如果你想脱离组件树以函数方式完成一次表单提交,参见 [`submitForm` 函数](./submit-form.md)
## changeHandler
- **签名:** `(prop: string, value: any, eventData?: ContainerChangeEventData) => void`

View File

@ -6,7 +6,19 @@
- **默认值:** `[]`
- **类型:** [FormConfig](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L864)
- **类型:** `FormConfig`
::: details 查看 FormConfig 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FormConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItemConfig{ts}
<<< @/../packages/form-schema/src/base.ts#ChildConfig{ts}
<<< @/../packages/form-schema/src/base.ts#DynamicTypeConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::
- **示例:**
@ -73,6 +85,44 @@
- **类型:** `boolean`
## showDiff
- **详情:**
自定义“是否展示对比内容”的判断函数(仅在 `isCompare === true` 时生效)。
- 不传:使用默认逻辑 `!isEqual(curValue, lastValue)`(基于 lodash `isEqual`
- 传函数:完全以函数返回值为准,返回 `true` 才展示前后两份对比内容。
该 prop 通过 `formState` 透传到所有层级的 Container 中,调用方只需在 MForm 这一层传一次即可对整棵表单生效。
典型场景:某些字段语义上相等但结构不同(例如 `code-select` 字段中 `''``{ hookType: 'code', hookData: [] }` 应视为相等),调用方在此处显式声明,避免被 `isEqual` 误判为差异。
- **类型:** `(data: { curValue: any; lastValue: any; config: FormItemConfig }) => boolean`
- **示例:**
```html
<template>
<m-form :config="config" :is-compare="true" :last-values="lastValues" :show-diff="showDiff"></m-form>
</template>
<script setup>
import { isEqual } from 'lodash-es';
const showDiff = ({ curValue, lastValue, config }) => {
if (config?.type === 'code-select') {
// 业务侧自定义:双方都是“空形态”视为相等,不展示对比
const isEmpty = (v) =>
v === '' || v === undefined || v === null ||
(typeof v === 'object' && v.hookType === 'code' && Array.isArray(v.hookData) && v.hookData.length === 0);
if (isEmpty(curValue) && isEmpty(lastValue)) return false;
}
return !isEqual(curValue, lastValue);
};
</script>
```
## parentValues
- **详情:** 父级表单值

View File

@ -0,0 +1,218 @@
# submitForm 函数
以命令式方式调用 `MForm` 组件完成一次表单校验/提交,类似 `ElMessage` 的用法。
调用时函数内部会临时挂载一个不可见的 `MForm` 实例,把入参作为 props 透传给它,等待初始化完成后调用其 `submitForm` 方法。校验通过则 `resolve` 表单值,校验失败则 `reject` 错误信息,最后自动卸载实例并清理 DOM。
适用于一些没有合适的容器、但又需要复用 `MForm` 校验逻辑的场景,例如:
- 通过快捷菜单/命令面板触发一次性表单
- 在脚本/服务层完成一次表单值校验后再发请求
- 把 `config` 配置当作"可执行的校验规则"使用
## 签名
```ts
function submitForm(options: SubmitFormOptions): Promise<any>;
```
## 参数
`options``MForm` 组件的 props 基本对齐,额外提供了 `native``returnChangeRecords``appContext``timeout` 等参数。
| 名称 | 类型 | 默认值 | 说明 |
| ---------------------- | ------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------- |
| `config` | `FormConfig` | — | 必填,表单配置 |
| `initValues` | `Record<string, any>` | `{}` | 表单初始值 |
| `lastValues` | `Record<string, any>` | `{}` | 需对比的值(开启对比模式时传入) |
| `isCompare` | `boolean` | `false` | 是否开启对比模式 |
| `parentValues` | `Record<string, any>` | `{}` | 父级 values透传给字段的回调 |
| `labelWidth` | `string` | `'200px'` | label 宽度 |
| `disabled` | `boolean` | `false` | 是否禁用 |
| `height` | `string` | `'auto'` | 表单高度 |
| `stepActive` | `string \| number` | `1` | 步骤表单当前激活步骤 |
| `size` | `'small' \| 'default' \| 'large'` | — | 组件尺寸 |
| `inline` | `boolean` | `false` | 是否行内表单 |
| `labelPosition` | `string` | `'right'` | label 对齐方式 |
| `keyProp` | `string` | `'__key'` | 配置项的唯一 key |
| `popperClass` | `string` | — | 弹层 className |
| `preventSubmitDefault` | `boolean` | — | 是否阻止表单原生 submit |
| `extendState` | `(state: FormState) => Record<string, any> \| Promise<Record<string, any>>` | — | 扩展 `formState` |
| `native` | `boolean` | `false` | 透传给 `Form.submitForm``true` 时返回内部响应式 `values`,否则返回 `cloneDeep(toRaw(values))` |
| `returnChangeRecords` | `boolean` | `false` | `true` 时 resolve 结果为 `{ values, changeRecords }`,携带表单变更记录;否则仅 resolve `values` |
| `appContext` | `AppContext \| null` | `null` | 父级 Vue 应用上下文。需要继承全局组件、指令、provide 等时传入,常通过 `app._context``getCurrentInstance()?.appContext` 获取 |
| `timeout` | `number` | `10000` | 等待表单初始化的最长时间(毫秒)。超时将以错误 reject。设为 `<= 0` 时关闭超时兜底 |
## 返回值
- `校验通过``Promise<any>` resolve 当前表单值(`native` 决定是否克隆);当 `returnChangeRecords``true`resolve `{ values, changeRecords }`
- `校验失败``Promise<any>` reject 一个 `Error``message` 中包含逐条字段错误信息(格式 `${text} -> ${message}`,多条用 `<br>` 分隔)
- `初始化超时``Promise<any>` reject `Error('submitForm timeout after ${timeout}ms: form is not initialized.')`
无论成功或失败,函数都会在最后自动 `unmount` 内部 app 并移除挂载用的 DOM 容器,无需调用方手动清理。
::: tip 关于 changeRecords
`changeRecords` 记录的是表单挂载后发生的字段变更(由各字段的 `change` 事件累积而来)。在 `submitForm` 这种命令式、无用户交互的场景下,通常为空数组;只有在 `extendState` 或字段联动等逻辑中触发了变更时才会有内容。`MForm` 内部的 `submitForm` 在校验通过后会清空变更记录,因此本函数会在调用前先对其做快照再返回。
:::
## 基础用法
```ts
import { submitForm } from '@tmagic/form';
try {
const values = await submitForm({
config: [
{
type: 'text',
name: 'username',
text: '用户名',
rules: [{ required: true, message: '请输入用户名' }],
},
],
initValues: { username: '' },
});
console.log('提交成功', values);
} catch (e) {
console.error('校验失败', e);
}
```
## 同时获取变更记录changeRecords
设置 `returnChangeRecords: true`resolve 的结果会从单纯的 `values` 变为 `{ values, changeRecords }`
```ts
import { submitForm } from '@tmagic/form';
const { values, changeRecords } = await submitForm({
config: [{ type: 'text', name: 'username', text: '用户名' }],
initValues: { username: 'foo' },
returnChangeRecords: true,
});
console.log(values); // { username: 'foo' }
console.log(changeRecords); // ChangeRecord[]
```
## 在组件中继承父级应用上下文
`MForm` 内部使用 `@tmagic/design` 的组件(背后可能是 `element-plus``tdesign`),需要宿主应用先完成相应的 `app.use(...)` 安装。在 `submitForm` 这种脱离常规组件树的命令式调用中,可通过 `appContext` 把父级应用上下文带过去:
```vue
<script setup lang="ts">
import { getCurrentInstance } from 'vue';
import { submitForm } from '@tmagic/form';
const { appContext } = getCurrentInstance()!;
const onClick = async () => {
const values = await submitForm({
config: [{ type: 'text', name: 'text', text: '文本' }],
initValues: { text: 'hello' },
appContext,
});
console.log(values);
};
</script>
```
也可以在初始化 app 时把上下文缓存下来,再在任意位置复用:
```ts
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import MagicForm, { type SubmitFormOptions, submitForm as rawSubmitForm } from '@tmagic/form';
import App from './App.vue';
const app = createApp(App);
app.use(ElementPlus);
app.use(MagicForm);
app.mount('#app');
export const submitForm = (options: Omit<SubmitFormOptions, 'appContext'>) =>
rawSubmitForm({ ...options, appContext: app._context });
```
## 处理校验错误
校验失败时 reject 的 `Error.message` 已经把出错字段拼好,可以直接展示到用户:
```ts
import { tMagicMessage } from '@tmagic/design';
try {
const values = await submitForm({ config, initValues });
await save(values);
} catch (e: any) {
tMagicMessage.error({
dangerouslyUseHTMLString: true,
message: e.message,
});
}
```
## 运行环境
`submitForm` 内部依赖 `document` / `window` 来挂载临时 Vue 实例,因此**只能在浏览器或具备 DOM 环境的运行时中使用**。
| 环境 | 是否可用 | 说明 |
| ----------------------------------------------- | -------- | --------------------------------------------------------------------------------- |
| 浏览器 / Electron 渲染进程 / 浏览器扩展 | ✅ | 直接可用 |
| Vitest / Jest + `happy-dom` / `jsdom` | ✅ | 项目自身的单测就跑在这种环境下 |
| 纯 Node.js / Bun / Deno无 DOM polyfill | ❌ | 模块顶层就会读 `document`,会抛 `document is not defined` |
| Node.js + 手动注入 `happy-dom` / `jsdom` | ⚠️ | 可用,需要在 import `@tmagic/form` **之前**完成全局变量注入;校验行为不一定与浏览器完全一致 |
### 在 Node.js 中使用(需要先准备 DOM
下面是一个在 Node 脚本里调用 `submitForm` 的完整例子,使用 [`happy-dom`](https://github.com/capricorn86/happy-dom) 作为 DOM polyfill
```ts
// scripts/check-form.ts
import { Window } from 'happy-dom';
const window = new Window();
Object.assign(globalThis, {
window,
document: window.document,
navigator: window.navigator,
HTMLElement: window.HTMLElement,
});
// 注意DOM polyfill 必须先注入到 globalThis再用动态 import
// 加载业务模块,否则 @tmagic/design 等模块顶层执行时就会读 document
const { createApp } = await import('vue');
const ElementPlus = (await import('element-plus')).default;
const MagicForm = (await import('@tmagic/form')).default;
const { submitForm } = await import('@tmagic/form');
const parentApp = createApp({ render: () => null });
parentApp.use(ElementPlus);
parentApp.use(MagicForm);
const values = await submitForm({
config: [{ type: 'text', name: 'username', text: '用户名' }],
initValues: { username: 'foo' },
appContext: parentApp._context,
});
console.log(values);
```
::: warning 注意
- DOM polyfill 必须在 **import 业务模块之前** 注入到 `globalThis`,否则模块顶层执行时仍会失败
- 在 `happy-dom` / `jsdom` 中,`element-plus` 的部分 `validate()` 行为不一定能 1:1 复现真实浏览器(例如某些场景下必填规则可能不触发),建议关键校验使用自定义 `validator` 函数确保稳定
- 如果只是想在 Node 端做一次纯校验,更稳妥的做法是直接复用 [`async-validator`](https://github.com/yiminghe/async-validator)element-plus 内部用的就是它),绕开整个 Vue 渲染层
:::
## 类型定义
::: details 查看 `SubmitFormOptions` 类型定义
<<< @/../packages/form/src/submitForm.ts#SubmitFormOptions{ts}
:::
::: details 查看 `SubmitFormResult` 类型定义
<<< @/../packages/form/src/submitForm.ts#SubmitFormResult{ts}
:::

View File

@ -1,7 +1,30 @@
# 表单对比
tmagic-form可以支持两个版本的表单值对比如果有容器嵌套将在tab标签页展示对应tab下存在的差异数便于在复杂嵌套表单场景下直观的看到差异情况
## 使用方法
在初始化表单时,需要传入当前版本的表单值,上一版本的表单值,以及表单配置,具体可参见[Form Playground](https://tencent.github.io/tmagic-editor/playground/index.html#/form)的demo演示
在初始化表单时,开启对比模式 `is-compare`,并传入当前版本的表单值(`init-values`)、上一版本的表单值(`last-values`)以及表单配置,具体可参见[Form Playground](https://tencent.github.io/tmagic-editor/playground/index.html#/form)的demo演示。
```html
<m-form
:config="config"
:is-compare="true"
:init-values="curValues"
:last-values="lastValues"
></m-form>
```
相关属性详见 Form 组件 props
- [`isCompare`](/api/form/form-props.html#iscompare):是否开启对比模式;
- [`lastValues`](/api/form/form-props.html#lastvalues):需对比的上一版本表单值;
- [`showDiff`](/api/form/form-props.html#showdiff):自定义「是否展示对比内容」的判断函数,用于规避语义相等但结构不同导致的误判。
## 对比模式下的字段行为
对比模式下,表单仅做只读展示:增删、复制、排序、导入、编辑等写操作按钮会被隐藏。对于由列表或嵌套子表单组成的复合字段(如 `event-select``code-select``code-select-col`),表单会按索引对齐前后值,逐项展示新增 / 删除 / 修改的高亮差异,而不会渲染出两套独立组件。
## 应用场景
编辑器的[历史记录面板](/guide/advanced/history-list.md)即基于该能力,对历史步骤的前后值做表单形式的差异对比。
## 效果展示
<img src="https://vip.image.video.qpic.cn/vupload/20230301/c626071677661813135.png" alt="表单对比"/>

View File

@ -33,7 +33,29 @@
| ----------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| formTitle | 弹窗标题 | string | — | — |
| codeOptions | 代码编辑器配置项 | object | — | — |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 CodeLinkConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#CodeLinkConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -33,6 +33,28 @@
| ----------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| notEditable | 是否不可编辑代码块disable控制是否可选择 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| notEditable | 是否不可编辑代码块disable控制是否可选择 | boolean / `FilterFunction` | — | false |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 CodeSelectColConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#CodeSelectColConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -30,6 +30,28 @@ CodeSelect 组件支持:
| --------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| className | 自定义类名 | string | — | — |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 CodeSelectConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#CodeSelectConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -47,14 +47,36 @@
| ------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| language | 代码语言 | string | javascript/typescript/json等 | — |
| height | 编辑器高度 | string | — | — |
| parse | 是否解析代码 | boolean | — | false |
| options | 编辑器配置项 | object | — | — |
| autosize | 自动调整大小配置 | object | — | — |
| mFormItemType | 传入代码编辑器的自定义类型 | string | — | — |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 CodeConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#CodeConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::
## autosize Attributes

View File

@ -33,6 +33,28 @@
| ------------ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| parentFields | 父级字段 | string[] | — | — |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 CondOpSelectConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#CondOpSelectConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -55,13 +55,35 @@
| ------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| value | 返回值类型 | string | key/value | — |
| checkStrictly | 是否严格遵守父子节点不互相关联 | boolean / Function | — | — |
| dataSourceFieldType | 允许选择的字段类型 | DataSourceFieldType[] | — | — |
| fieldConfig | 自定义字段配置 | ChildConfig | — | — |
| notEditable | 是否不可编辑数据源disable控制是否可选择 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| notEditable | 是否不可编辑数据源disable控制是否可选择 | boolean / `FilterFunction` | — | false |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 DataSourceFieldSelectConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#DataSourceFieldSelectConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::
## value说明

View File

@ -22,5 +22,27 @@
| -------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 DataSourceFieldsConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#DataSourceFieldsConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -22,5 +22,27 @@
| -------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 DataSourceInputConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#DataSourceInputConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -33,6 +33,28 @@
| ----------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| notEditable | 是否不可编辑数据源disable控制是否可选择 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| notEditable | 是否不可编辑数据源disable控制是否可选择 | boolean / `FilterFunction` | — | false |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 DataSourceMethodSelectConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#DataSourceMethodSelectConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -22,5 +22,27 @@
| -------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 DataSourceMethodsConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#DataSourceMethodsConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -22,5 +22,27 @@
| -------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 DataSourceMocksConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#DataSourceMocksConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -45,11 +45,35 @@
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| placeholder | 输入框占位文本 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| dataSourceType | 数据源类型过滤 | string | base/http等 | — |
| value | 返回值类型 | string | id/value | — |
| notEditable | 是否不可编辑数据源disable控制是否可选择 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| notEditable | 是否不可编辑数据源disable控制是否可选择 | boolean / `FilterFunction` | — | false |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 DataSourceSelect 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#DataSourceSelect{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
<<< @/../packages/form-schema/src/base.ts#Input{ts}
:::
## value说明

View File

@ -33,7 +33,29 @@
| ------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| titlePrefix | 标题前缀 | string | — | — |
| parentFields | 父级字段 | string[] / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | — |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| parentFields | 父级字段 | string[] / `FilterFunction` | — | — |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 DisplayCondsConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#DisplayCondsConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -34,7 +34,7 @@
| ---------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| src | 事件来源 | string | datasource/component | — |
| labelWidth | 标签宽度 | string | — | — |
| eventNameConfig | 事件名称表单配置 | FormItem | — | — |
@ -43,7 +43,29 @@
| compActionConfig | 联动组件动作配置 | FormItem | — | — |
| codeActionConfig | 联动代码配置 | FormItem | — | — |
| dataSourceActionConfig | 联动数据源配置 | FormItem | — | — |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 EventSelectConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#EventSelectConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::
## src说明

View File

@ -35,6 +35,28 @@
| -------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| advanced | 是否支持高级模式(代码编辑) | boolean | — | false |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 KeyValueConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#KeyValueConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -22,8 +22,30 @@
| -------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 PageFragmentSelectConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#PageFragmentSelectConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::
## 使用说明

View File

@ -22,8 +22,30 @@
| -------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 UISelectConfig 配置类型定义
<<< @/../packages/form-schema/src/editor.ts#UISelectConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::
## 使用说明

View File

@ -423,16 +423,40 @@ options 支持传入函数,可根据表单其他字段动态生成选项列表
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| placeholder | 输入框占位文本 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/master/packages/form-schema/src/base.ts) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| multiple | 是否多选 | boolean | — | false |
| emitPath | 在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组,若设置 false则只返回该节点的值 | boolean | — | true |
| checkStrictly | 是否严格的遵守父子节点不互相关联 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/master/packages/form-schema/src/base.ts) | — | false |
| valueSeparator | 合并成字符串时的分隔符 | string / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/master/packages/form-schema/src/base.ts) | — | — |
| checkStrictly | 是否严格的遵守父子节点不互相关联 | boolean / `FilterFunction` | — | false |
| valueSeparator | 合并成字符串时的分隔符 | string / `FilterFunction` | — | — |
| popperClass | 弹出内容的自定义类名 | string | — | — |
| remote | 是否为远程搜索 | boolean | — | false |
| options | 选项数据源 | Array / Function | — | — |
| option | 远程选项配置 | Object | — | — |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/master/packages/form-schema/src/base.ts) | — | — |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | — |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 CascaderConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#CascaderConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
<<< @/../packages/form-schema/src/base.ts#Input{ts}
:::
## options item

View File

@ -154,12 +154,36 @@ options 支持函数形式,可根据表单状态动态生成选项。
|------|------|------|--------|--------|
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/master/packages/form-schema/src/base.ts) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| activeValue | 选中时的值 | string / number | — | truefilter 为 'number' 时默认 1 |
| inactiveValue | 未选中时的值 | string / number | — | falsefilter 为 'number' 时默认 0 |
| useLabel | 是否使用外部 label 显示 | boolean | — | false |
| filter | 值过滤器 | 'number' / Function | — | — |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/master/packages/form-schema/src/base.ts) | — | — |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | — |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 CheckboxConfig / CheckboxGroupConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#CheckboxConfig{ts}
<<< @/../packages/form-schema/src/base.ts#CheckboxGroupConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::
## CheckboxGroup Attributes
@ -167,9 +191,9 @@ options 支持函数形式,可根据表单状态动态生成选项。
|------|------|------|--------|--------|
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/master/packages/form-schema/src/base.ts) | — | false |
| options | 选项列表 | Array / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/master/packages/form-schema/src/base.ts) | — | — |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/master/packages/form-schema/src/base.ts) | — | — |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| options | 选项列表 | Array / `FilterFunction` | — | — |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | — |
## options item

View File

@ -69,9 +69,31 @@
|------|------|------|--------|--------|
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/master/packages/form-schema/src/base.ts) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| defaultValue | 默认颜色值 | string | — | — |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/master/packages/form-schema/src/base.ts) | — | — |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | — |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 ColorPickConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#ColorPickConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::
## 颜色格式说明

View File

@ -99,10 +99,34 @@
| name | 绑定值的字段名 | string | — | — |
| text | 表单标签 | string | — | — |
| placeholder | 输入框占位文本 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| format | 显示在输入框中的格式 | string | 见[日期格式](#日期格式) | YYYY/MM/DD |
| valueFormat | 绑定值的格式。不指定则绑定值为 Date 对象 | string | 见[日期格式](#日期格式) | YYYY/MM/DD |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | — |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | — |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 DateConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#DateConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
<<< @/../packages/form-schema/src/base.ts#Input{ts}
:::
## TypeScript 定义

View File

@ -41,9 +41,31 @@ type为'daterange'
| name | 绑定值(数组形式) | string | — | — |
| names | 绑定值(拆分为两个字段) | string[] | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| dateFormat | 日期格式 | string | — | YYYY/MM/DD |
| timeFormat | 时间格式 | string | — | HH:mm:ss |
| valueFormat | 绑定值的格式 | string | — | YYYY/MM/DD HH:mm:ss |
| defaultTime | 默认时间 | Date[] | — | — |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 DaterangeConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#DaterangeConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -101,8 +101,32 @@
| name | 绑定值的字段名 | string | — | — |
| text | 表单标签 | string | — | — |
| placeholder | 输入框占位文本 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| format | 显示在输入框中的格式 | string | 见[日期格式](#日期格式) | YYYY/MM/DD HH:mm:ss |
| valueFormat | 绑定值的格式 | string | 见[日期格式](#日期格式) | YYYY/MM/DD HH:mm:ss |
| defaultTime | 选择日期后的默认时间值 | Date | — | — |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | — |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | — |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 DateTimeConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#DateTimeConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
<<< @/../packages/form-schema/src/base.ts#Input{ts}
:::

View File

@ -2,16 +2,6 @@
用于显示,不可编辑
## TS 定义
```typescript
interface Display extends FormItem {
type: "display";
}
```
点击查看[FormItem](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L90)的定义
## 基础用法
<demo-block type="form" :config="[{
@ -33,3 +23,12 @@ interface Display extends FormItem {
| ---- | -------- | ------ | ------ | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
## 配置类型
::: details 查看 DisplayConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#DisplayConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -50,3 +50,12 @@
| name | 字段名 | string | — |
| label | 标签名 | string | — |
| defaultValue | 默认值 | any | — |
## 配置类型
::: details 查看 DynamicFieldConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#DynamicFieldConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -2,16 +2,6 @@
改值体现于最终提交的表单中用于例如编辑记录的id这种场景中
## TS 定义
```typescript
interface Hidden extends FormItem {
type: "hidden";
}
```
点击查看[FormItem](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L90)的定义
## 基础用法
<demo-block type="form" :config="[{
@ -30,3 +20,12 @@ interface Hidden extends FormItem {
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
| ---- | ------ | ------ | ------ | ------ |
| name | 绑定值 | string | — | — |
## 配置类型
::: details 查看 HiddenConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#HiddenConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -2,28 +2,6 @@
用于显示,不可编辑
## TS 定义
```typescript
interface Link extends FormItem {
type: "link";
href?: string | typeof LinkHrefFunction;
css?: {
[key: string]: string | number;
};
disabledCss?: {
[key: string]: string | number;
};
formTitle?: string;
formWidth?: number | string;
displayText?: string | typeof LinkDisplayTextFunction;
form: FormConfig | typeof LinkFormFunction;
fullscreen?: boolean;
}
```
点击查看[FormItem](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L90)的定义
## 基础用法
<demo-block type="form" :config="[{
@ -63,3 +41,12 @@ interface Link extends FormItem {
| ---- | -------- | ------ | ------ | ------ |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
## 配置类型
::: details 查看 LinkConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#LinkConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -23,6 +23,28 @@ type为'number-range'
| --------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
| name | 绑定值(数组形式 [min, max] | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| clearable | 是否可清空 | boolean | — | true |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 NumberRangeConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#NumberRangeConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -58,9 +58,31 @@ disabled 属性接受一个 Boolean设置为 true 即可禁用整个组件,
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| placeholder | 输入框占位文本 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| min | 设置计数器允许的最小值 | number | — | -Infinity |
| max | 设置计数器允许的最大值 | number | — | Infinity |
| step | 计数器步长 | number | — | 1 |
| tooltip | 输入框提示信息 | string | — | — |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 NumberConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#NumberConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -2,23 +2,6 @@
在一组备选项中进行单选
## TS 定义
```typescript
interface RadioGroup extends FormItem {
type: "radio-group";
childType?: "default" | "button";
options: {
value: any;
text?: string;
icon?: any;
tooltip?: string;
}[];
}
```
点击查看[FormItem](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L90)的定义
## 基础用法
由于选项默认可见,不宜过多,若选项过多,建议使用 Select 选择器。
@ -68,10 +51,34 @@ interface RadioGroup extends FormItem {
| --------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ------- |
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| childType | 子项展示形式 | string | default / button | default |
| options | 选项 | Array | — | - |
| onChange | 值变化时触发的函数 | [OnChangeHandler ](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FormItem / FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 RadioGroupConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#RadioGroupConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::
## options item

View File

@ -188,16 +188,40 @@ app.use(MagicForm, {
| name | 绑定值 | string | — | — |
| placeholder | 输入框占位文本 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| multiple | 是否多选 | boolean | — | false |
| valueKey | 作为 value 唯一标识的键名,绑定值为对象类型时必填 | string | — | value |
| allowCreate | 是否允许用户创建新条目 | boolean | — | false |
| remote | 是否为远程搜索 | boolean | — | false |
| group | 是否选择分组 | boolean | — | false |
| onChange | 值变化时触发的函数 | [OnChangeHandler ](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L90) | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
| options | 选项 | Array | — | - |
| option | 选项 | Object | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 SelectConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#SelectConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
<<< @/../packages/form-schema/src/base.ts#Input{ts}
:::
## options item
| 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -46,6 +46,20 @@
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
| -------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
| name | 绑定值 | string | — | — |
| disabled | 是否禁用 | boolean / [Function](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L90) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| active-value | switch 打开时的值 | boolean / string / number | — | true |
| inactive-value | switch 关闭时的值 | boolean / string / number | — | false |
::: details 查看 FilterFunction 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
:::
## 配置类型
::: details 查看 SwitchConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#SwitchConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -119,14 +119,40 @@ Input输入框的type为'text', 是type的默认值所以可以不配置
| name | 绑定值 | string | — | — |
| text | 表单标签 | string | — | — |
| placeholder | 输入框占位文本 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| clearable | 是否可清空 | boolean | — | true |
| tooltip | 输入时显示内容 | string / [ToolTipConfigType](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L90) | — | — |
| tooltip | 输入时显示内容 | string / `ToolTipConfigType` | — | — |
| trim | 是否去掉首尾空格 | boolean | — | false |
| filter | 过滤值 | string / Function | number | - |
| prepend | 前置内容 | string | — | - |
| append | 后置内容 | string / Object | — | - |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler / ToolTipConfigType 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
<<< @/../packages/form-schema/src/base.ts#ToolTipConfigType{ts}
:::
## 配置类型
::: details 查看 TextConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#TextConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
<<< @/../packages/form-schema/src/base.ts#Input{ts}
:::
## append Attributes

View File

@ -38,8 +38,30 @@
| name | 绑定值 | string | — | — |
| placeholder | 输入框占位文本 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| placeholder | 输入框占位文本 | string | — | — |
| trim | 是否去掉首尾空格 | boolean | — | false |
| filter | 过滤值 | string / Function | number | - |
| onChange | 值变化时触发的函数 | [OnChangeHandler ](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L90) | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 TextareaConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#TextareaConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -38,4 +38,20 @@
| name | 绑定值 | string | — | — |
| placeholder | 输入框占位文本 | string | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [Function](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L90) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
::: details 查看 FilterFunction 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
:::
## 配置类型
::: details 查看 TimeConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#TimeConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
<<< @/../packages/form-schema/src/base.ts#Input{ts}
:::

View File

@ -41,8 +41,30 @@ type为'timerange'
| name | 绑定值(数组形式) | string | — | — |
| names | 绑定值(拆分为两个字段) | string[] | — | — |
| text | 表单标签 | string | — | — |
| disabled | 是否禁用 | boolean / [FilterFunction](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L195) | — | false |
| disabled | 是否禁用 | boolean / `FilterFunction` | — | false |
| format | 显示格式 | string | — | HH:mm:ss |
| valueFormat | 绑定值的格式 | string | — | HH:mm:ss |
| defaultTime | 默认时间 | Date[] | — | — |
| onChange | 值变化时触发的函数 | [OnChangeHandler](https://github.com/Tencent/tmagic-editor/blob/cce8b63fc3618b5b811aa33c703de21c22be8a6a/packages/form-schema/src/base.ts#L30) | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
::: details 查看 FilterFunction / OnChangeHandler 及关联类型定义
<<< @/../packages/form-schema/src/base.ts#FilterFunction{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandler{ts}
<<< @/../packages/form-schema/src/base.ts#OnChangeHandlerData{ts}
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
<<< @/../packages/form-schema/src/base.ts#FormValue{ts}
:::
## 配置类型
::: details 查看 TimerangeConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#TimerangeConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::

View File

@ -1,5 +1,36 @@
# 布局
## 配置类型
::: details 查看 ContainerCommonConfig / RowConfig / TabConfig / TabPaneConfig / FieldsetConfig / PanelConfig / StepConfig / FlexLayoutConfig / GroupListConfig / TableConfig / TableColumnConfig / TableGroupListCommonConfig 配置类型定义
<<< @/../packages/form-schema/src/base.ts#ContainerCommonConfig{ts}
<<< @/../packages/form-schema/src/base.ts#RowConfig{ts}
<<< @/../packages/form-schema/src/base.ts#TabConfig{ts}
<<< @/../packages/form-schema/src/base.ts#TabPaneConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FieldsetConfig{ts}
<<< @/../packages/form-schema/src/base.ts#PanelConfig{ts}
<<< @/../packages/form-schema/src/base.ts#StepConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FlexLayoutConfig{ts}
<<< @/../packages/form-schema/src/base.ts#GroupListConfig{ts}
<<< @/../packages/form-schema/src/base.ts#TableConfig{ts}
<<< @/../packages/form-schema/src/base.ts#TableColumnConfig{ts}
<<< @/../packages/form-schema/src/base.ts#TableGroupListCommonConfig{ts}
<<< @/../packages/form-schema/src/base.ts#FormItem{ts}
:::
## 基础用法
<demo-block type="form" :config="[{
@ -104,6 +135,18 @@
}]
}]"></demo-block>
`legend` 除了支持字符串,也支持函数,函数返回值作为标题展示,可根据表单数据动态生成:
<demo-block type="form" :config="[{
type: 'fieldset',
labelWidth: '100px',
legend: (mForm, { formValue }) => `当前值:${formValue.text || '空'}`,
items: [{
name: 'text',
text: '配置1',
}]
}]"></demo-block>
### panel
<demo-block type="form" :config="[{

View File

@ -30,7 +30,7 @@ tmagic-editor的联动指这两种情况
当然我们也可以通过上述的参数传入,以及其他函数 API 实现更多灵活的表单联动,具体参考[表单 API](../../form-config/relate)。
## 组件联动
tmagic-editor在 @tmagic/core 中,实现了组件的事件绑定/分发机制。在组件渲染时,每个组件在 @tmagic/ui 中经过基础组件渲染时,会被基础组件注入公共方法的实现。如下对按钮配置了**点击使文本隐藏**的联动事件,那么在对应按钮被点击时,将会触发对应绑定文本的隐藏。
tmagic-editor在 `@tmagic/core` 中,实现了组件的事件绑定/分发机制。在组件渲染时,每个组件在经过 `@tmagic/vue-container`vue 端)或 `@tmagic/react-container`react 端)等基础渲染组件渲染时,会被基础组件注入公共方法的实现。如下对按钮配置了**点击使文本隐藏**的联动事件,那么在对应按钮被点击时,将会触发对应绑定文本的隐藏。
<img src="https://image.video.qpic.cn/oa_88b7d-10_2117738923_1637238863127559">

View File

@ -0,0 +1,134 @@
# 历史记录面板
编辑器内置了一个可视化的「历史记录面板」,用于查看与回溯编辑过程中产生的所有操作。相比顶部菜单栏只能「撤销 / 重做」相邻一步,历史记录面板提供了对整条历史栈的全局视角:可以按页面、数据源、代码块分类浏览,点击任意一步直接跳转,查看每一步的前后差异,甚至像 `git revert` 一样单独回滚某一步而不破坏后续操作。
## 开启面板
历史记录面板以一个内置菜单项 `'history-list'` 的形式提供,将它加入 [`menu`](/api/editor/props.html#menu) 配置即可在顶部工具栏出现一个时钟图标,点击展开面板:
```html
<template>
<m-editor :menu="menu"></m-editor>
</template>
<script setup>
import { ref } from 'vue';
const menu = ref({
left: [],
center: ['delete', 'undo', 'redo', '/', 'history-list'],
right: [],
});
</script>
```
## 面板结构
面板分为三个 tab分别对应三类可被历史记录追踪的对象tab 标题后的数字为各自的分组数量:
| Tab | 内容 | 跳转 API |
| --- | --- | --- |
| 页面 | 当前活动页面的节点操作历史 | `editorService.gotoPageStep(cursor)` |
| 数据源 | 按 `dataSource.id` 分组的数据源变更历史 | `dataSourceService.goto(id, cursor)` |
| 代码块 | 按 `codeBlock.id` 分组的代码块变更历史 | `codeBlockService.goto(id, cursor)` |
### 相邻同目标自动合并
为了避免「连续微调同一个节点 / 数据源 / 代码块」时产生大量碎片化记录,面板会把**相邻的、针对同一目标的连续 `update`** 自动合并成一个分组:
- 页面 tab连续修改同一节点按节点 id 判定)的多步合并为一组,点击组头部可展开查看每一子步;
- 数据源 / 代码块 tab相邻的连续 `update` 按目标 id 合并;`add` / `remove` 始终独立成组(语义上是一次性事件)。
> 合并仅作用于展示与交互,不改变底层 undo/redo 栈的真实结构。
## 交互能力
每个分组 / 步骤支持以下操作:
### 1. 点击跳转
点击任意一条记录编辑器会跳转到「应用至该步完成」的状态。其本质是把对应栈的游标cursor移动到 `step.index + 1`,由 service 层的 undo/redo 链路完成中间步骤的批量正向 / 反向应用。
### 2. 回到初始状态
每个 tab 列表底部提供「回到初始状态」入口,等价于把对应栈游标移到 `0`(所有真实步骤全部撤销)。
### 3. 单步回滚(类 git revert
对于历史中间的某一步,可以单独「回滚」它,而保留它之后的所有操作。该行为不会倒带游标,而是把目标步骤的修改**反向应用为一次全新的操作**并压入栈顶,因此不会破坏既有历史结构:
- 页面:`editorService.revertPageStep(index)`
- 数据源:`dataSourceService.revert(id, index)`
- 代码块:`codeBlockService.revert(id, index)`
如果业务侧在执行操作时已通过 `*AndGetHistoryId` 拿到了该条记录的 [uuid](/api/editor/editorServiceMethods.md#历史记录-uuid-与-andgethistoryid),也可以直接按 uuid 回滚(无需再关心 index / id且 uuid 不会随栈内步骤增删而变化):
- 页面:`editorService.revertPageStepById(uuid)`
- 数据源:`dataSourceService.revertById(uuid)`
- 代码块:`codeBlockService.revertById(uuid)`
### 4. 差异对比
在前后值都存在的 `update` 步骤上提供「查看差异」入口,点击后弹出差异对话框。对话框支持两个维度的切换:
- **对比对象**
- `与修改前对比`:该步骤修改前 vs 修改后(默认,体现这一步带来的变化);
- `与当前对比`:该步骤修改后 vs 编辑器中的最新值(用于确认「这一步之后是否又被改动过」,当前值缺失时禁用)。
- **展示形态**
- `表单对比`:以属性表单形式逐字段对比,可读性更好(基于 [表单对比](/form-config/compare.md) 能力);
- `源码对比`:以 JSON 源码做整体 diff基于 monaco diff 编辑器),可以看到表单未覆盖到的字段。
::: tip
表单对比依赖 `@tmagic/form` 的对比模式(`isCompare` / `lastValues`)。对于 `event-select``code-select``code-select-col` 等由列表或嵌套子表单组成的复合字段,表单会逐项展示新增 / 删除 / 修改的高亮差异,并在对比模式下隐藏「添加 / 删除 / 编辑」等写操作按钮,仅保留只读展示。
:::
## 扩展自定义 tab
内置的三个 tab 之外,业务方可以通过 Editor 的 [`historyListExtraTabs`](/api/editor/props.html#historylistextratabs) 在面板中追加自定义的历史 tab追加在「页面 / 数据源 / 代码块」之后。适用于某个自定义模块维护自己的操作历史,需要在历史记录面板中独立展示与回滚的场景。
```html
<template>
<m-editor :menu="menu" :history-list-extra-tabs="historyListExtraTabs"></m-editor>
</template>
<script setup>
import { markRaw } from 'vue';
import MyModuleHistoryTab from './MyModuleHistoryTab.vue';
const historyListExtraTabs = [
{
name: 'my-module',
// label 支持字符串或函数,函数形式便于展示动态数量
label: () => `我的模块 (${getMyModuleHistory().length})`,
component: markRaw(MyModuleHistoryTab),
props: { foo: 'bar' },
listeners: {
goto: (cursor) => console.log(cursor),
},
},
];
</script>
```
每个扩展 tab 的字段说明:
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `name` | 是 | tab 唯一标识,作为内部 `TMagicTabs``name` |
| `label` | 是 | tab 显示文案,支持字符串或返回字符串的函数(便于展示动态数量) |
| `component` | 是 | tab 内容区渲染的组件 |
| `props` | 否 | 传入内容组件的 props |
| `listeners` | 否 | 内容组件的事件监听 |
> 内容组件内部可自行通过 `useServices()` 拿到 `historyService` 等服务,读取并回滚自定义模块自己维护的历史。
## 自定义对比判断
差异对话框中的「表单对比」最终透传到 `MForm`,你可以通过 Editor 顶层注入的 `extendFormState` 让对比表单拿到完整业务上下文,从而让依赖上下文的 `display` / `disabled``filterFunction` 正常工作。
若某些字段语义上相等但结构不同(例如 `code-select` 字段中 `''``{ hookType: 'code', hookData: [] }` 应视为相等),可借助 `@tmagic/form` 的 [`showDiff`](/api/form/form-props.html#showdiff) 自定义判断函数避免被误判为差异。
## 相关 API
历史面板的数据均来自 `historyService` 暴露的聚合方法,详见 [historyService 方法](/api/editor/historyServiceMethods.md)。

View File

@ -1,5 +1,5 @@
# 页面渲染
tmagic-editor的页面渲染是通过在载入编辑器中保存的 DSL 配置,通过 ui 渲染器渲染页面。在容器布局原理里我们提到过,容器和组件在配置中呈树状结构,所以渲染页面的时候,渲染器会递归配置内容,从而渲染出页面所有组件。
tmagic-editor的页面渲染是通过在载入编辑器中保存的 DSL 配置,通过基础渲染组件vue 下为 `@tmagic/vue-container`react 下为 `@tmagic/react-container`渲染页面。在容器布局原理里我们提到过,容器和组件在配置中呈树状结构,所以渲染页面的时候,渲染器会递归配置内容,从而渲染出页面所有组件。
<img src="https://vfiles.gtimg.cn/vupload/20211009/f4d3031633778551251.png">
@ -25,7 +25,7 @@ export default {
```
## 组件渲染
所有tmagic-editor组件都通过一个tmagic-editor基础组件来渲染。这个基础组件会识别当前渲染组件的类型。如果当前渲染组件是普通组件包括ui中提供的基础组件和业务开发的业务组件),则直接渲染;如果当前渲染组件是容器,则回到[容器渲染](#容器渲染)逻辑中。
所有tmagic-editor组件都通过一个tmagic-editor基础组件来渲染。这个基础组件会识别当前渲染组件的类型。如果当前渲染组件是普通组件包括 `vue-components` / `react-components` 中提供的基础组件和业务开发的业务组件),则直接渲染;如果当前渲染组件是容器,则回到[容器渲染](#容器渲染)逻辑中。
基础组件的具体形式为:
```vue
@ -59,6 +59,6 @@ export default defineComponent({
```
## 渲染器示例
在tmagic-editor的示例项目中我们提供了三个版本的 @tmagic/ui可以参考对应前端框架的渲染器实现。
- [vue 渲染器](https://github.com/Tencent/tmagic-editor/blob/master/vue-components/container/src/Container.vue)
- [react 渲染器](https://github.com/Tencent/tmagic-editor/blob/master/react-components/container/src/Container.tsx)
在tmagic-editor的示例项目中我们针对 vue 和 react 分别提供了基础渲染组件的实现,可以参考对应前端框架的渲染器实现。
- [vue 渲染器`@tmagic/vue-container`](https://github.com/Tencent/tmagic-editor/blob/master/vue-components/container/src/Container.vue)
- [react 渲染器`@tmagic/react-container`](https://github.com/Tencent/tmagic-editor/blob/master/react-components/container/src/Container.tsx)

View File

@ -1,23 +0,0 @@
# @tmagic/ui
在前面[页面渲染](../advanced/page)中提到的 UI 渲染器,就是包含在 @tmagic/ui 中的渲染器组件。
tmagic-editor的设计是希望发布的页面支持多个前端框架即各个业务方可以根据自己熟悉的语言来开发组件、发布页面。也可以通过 [实现一个 runtime](../runtime.html) 的方式,来实现一个自己的 @tmagic/ui
所以tmagic-editor的设计中针对每个前端框架都需要有一个对应的 @tmagic/ui 来承担渲染器职责。同时,也需要一个使用和 @tmagic/ui 相同前端框架的 runtime 用来加载 vue-components 和业务组件,具体 runtime 概念,可以参考[页面发布](../publish)。
我们以项目代码中提供的 vue 版本的 vue-components 作为示例介绍其中包含的内容(参考 `vue-components/` 目录下的源码)。
## 渲染器
在 vue 中,实现渲染器的具体形式参考[页面渲染](../advanced/page)中描述的[容器渲染](../advanced/page.html#容器渲染)和[组件渲染](../advanced/page.html#组件渲染)。
## 基础组件
在 vue-components 中,我们提供了几个基础组件,可以在项目源码中找到对应内容。
- page tmagic-editor的页面基础
- container tmagic-editor的容器渲染器
- Component.vue tmagic-editor的组件渲染器
- button/text 基础组件示例
其中 page/container/Component 是 UI 的基础,是每个框架的 UI 都应该实现的。
button/text 其实就是一个组件开发的示例,具体组件开发相关规范可以参考[组件开发](../component)。

View File

@ -1,6 +1,15 @@
# 快速开始
tmagic-editor的编辑器我们已经封装成一个 npm 包,可以直接安装使用。编辑器是使用 vue3 开发的(仅支持vue3),但使用编辑器的业务(runtime)可以不限框架,可以用 vue2、react 等开发业务组件。
tmagic-editor 的编辑器已经封装成 npm 包,可以直接安装使用。编辑器使用 Vue 3 开发(**仅支持 Vue 3**),但承载真实业务的 runtime 不限框架,可以使用 Vue 2、Vue 3、React 等开发业务组件。
整个项目结构由两部分组成:
- **admin-client**(编辑器 / 管理端):基于 `@tmagic/editor`,加载 runtime iframe、提供拖拽/属性配置/发布等能力。
- **runtime**(运行时):负责解析 DSL 并渲染页面,分为编辑器内嵌的 `playground` 和线上发布使用的 `page` 两个产物。
> 仓库 [`playground/`](https://github.com/Tencent/tmagic-editor/tree/master/playground) 与 [`runtime/vue/`](https://github.com/Tencent/tmagic-editor/tree/master/runtime/vue) 就是一份完整可运行的最小实践,本节内容均与之对齐,可以对照阅读源码。
## 使用脚手架创建(推荐)
::: code-group
@ -11,220 +20,423 @@ $ npm create tmagic@latest
```bash [pnpm]
$ pnpm create tmagic
```
:::
按照提示操作可以创建`6`种项目:
按照交互式提示,可以创建以下 `6` 种项目:
* runtime:运行时DSL渲染
* admin-client:管理端(编辑器)
* components:组件库(组件/插件/数据源)
* component:组件
* data-source:数据源
* plugin:插件
| 类型 | 说明 |
| -------------- | ------------------------------ |
| `runtime` | 运行时DSL 渲染) |
| `admin-client` | 管理端(编辑器) |
| `components` | 组件库(组件 / 插件 / 数据源) |
| `component` | 单个组件 |
| `data-source` | 单个数据源 |
| `plugin` | 单个插件 |
至少需要一个runtime与admin-client后就可以运行起一个最简单的项目了。
后续还需要新增组件、插件、数据源等,可以继续添加后面几种类型的项目。
新增好一个组件/插件/数据源后可以到runtime/tmagic.config.ts中配置到packages中
最少需要一个 `runtime` 加一个 `admin-client`,就能跑起一个完整的可视化搭建流程。后续可以再陆续创建组件、插件、数据源;新建好后到 `runtime/tmagic.config.ts``packages` 中注册即可,参考[组件开发](./component.md) 与[页面发布 § @tmagic/cli](./publish.md#tmagic-cli)。
## 手动安装
node.js >= 18
::: tip 环境要求
可以通过[Vite](https://cn.vitejs.dev/) 或 [Vue CLI](https://cli.vuejs.org/zh/)快速创建项目。
- Node.js `^20.19.0 || >=22.12.0`
- 推荐使用 [Vite](https://cn.vitejs.dev/);如果使用 [Vue CLI](https://cli.vuejs.org/zh/) 需要在 `vue.config.js` 中加上 `transpileDependencies: [/@tmagic/]`
:::
> 使用Vue CLI生成的项目需要在vue.config.js中加上配置transpileDependencies: [/@tmagic/]
### 1. 安装编辑器依赖
`@tmagic/editor` 把内部使用到的 UI 组件抽象到了 `@tmagic/design`,通过 **adapter** 的形式接入具体的 UI 组件库。我们提供了:
- [`@tmagic/element-plus-adapter`](https://github.com/Tencent/tmagic-editor/tree/master/packages/element-plus-adapter):接入 [Element Plus](https://element-plus.org/)
- [`@tmagic/tdesign-vue-next-adapter`](https://github.com/Tencent/tmagic-editor/tree/master/packages/tdesign-vue-next-adapter):接入 [TDesign Vue Next](https://tdesign.tencent.com/vue-next/overview)
任选其一即可,下面以 Element Plus 为例:
```bash
$ npm install @tmagic/editor -S
$ npm install @tmagic/editor @tmagic/core @tmagic/element-plus-adapter element-plus -S
```
由于在实际应用中项目常常会用到例如[element-plus](https://element-plus.org/)、[tdesign-vue-next](https://tdesign.tencent.com/vue-next/overview)等UI组件库。为了能让使用者能够选择不同UI库[@tmagic/editor](https://github.com/Tencent/tmagic-editor/tree/master/packages/editor)将其中使用到的UI组件封装到[@tmagic/design](https://github.com/Tencent/tmagic-editor/tree/master/packages/design)中然后通过不同的adapter来指定使用具体的对应的UI库我们提供了[@tmagic/element-plus-adapter](https://github.com/Tencent/tmagic-editor/tree/master/packages/element-plus-adapter)来支持[element-plus](https://element-plus.org/),所以还需要安装相关的依赖。
```bash
$ npm install @tmagic/element-plus-adapter element-plus -S
```
editor 中还包含了[monaco-editor](https://microsoft.github.io/monaco-editor/)所以还需安装monaco-editor可以参考 monaco-editor 的[配置指引](https://github.com/microsoft/monaco-editor/blob/main/docs/integrate-esm.md)。
`@tmagic/editor` 内部使用了 [monaco-editor](https://microsoft.github.io/monaco-editor/) 作为代码编辑器,需要额外安装并按照官方[配置指引](https://github.com/microsoft/monaco-editor/blob/main/docs/integrate-esm.md)注入 worker
```bash
$ npm install monaco-editor -S
```
## 快速上手
### 2. 引入 @tmagic/editor
## 引入 @tmagic/editor
参考 [`playground/src/main.ts`](https://github.com/Tencent/tmagic-editor/blob/master/playground/src/main.ts),在入口文件中按以下顺序完成 Monaco worker、UI 库样式、editor 样式与 adapter 的注入:
在 main.js 中写入以下内容:
```ts
import { createApp } from "vue";
import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import CssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import HtmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import JsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import TsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
```js
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import editorPlugin from "@tmagic/editor";
import MagicElementPlusAdapter from "@tmagic/element-plus-adapter";
import editorPlugin from '@tmagic/editor';
import MagicElementPlusAdapter from '@tmagic/element-plus-adapter';
import App from "./App.vue";
import App from './App.vue';
import "element-plus/dist/index.css";
import "@tmagic/editor/dist/style.css";
import 'element-plus/dist/index.css';
import '@tmagic/editor/dist/style.css';
// @ts-ignore
globalThis.MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === "json") return new JsonWorker();
if (["css", "scss", "less"].includes(label)) return new CssWorker();
if (["html", "handlebars", "razor"].includes(label))
return new HtmlWorker();
if (["typescript", "javascript"].includes(label)) return new TsWorker();
return new EditorWorker();
},
};
const app = createApp(App);
app.use(ElementPlus, {
locale: zhCn,
});
app.use(editorPlugin, MagicElementPlusAdapter);
app.mount('#app');
createApp(App).use(editorPlugin, MagicElementPlusAdapter).mount("#app");
```
以上代码便完成了 @tmagic/editor 的引入。需要注意的是,样式文件需要单独引入。
::: tip 切换 UI 适配器
playground 通过 `sessionStorage` 来切换 adapter参考实现
可以参考我们提供的[Playground](https://github.com/Tencent/tmagic-editor/blob/master/playground/src/main.ts)示例实现代码
```ts
const adapter =
sessionStorage.getItem("tmagic-playground-ui-adapter") || "element-plus";
const adapterModule =
adapter === "tdesign-vue-next"
? import("@tmagic/tdesign-vue-next-adapter")
: import("@tmagic/element-plus-adapter");
```
## 使用 m-editor 组件
:::
在 App.vue 中写入以下内容:
::: tip 常见报错
```html
1. `Preprocessor dependency "sass" not found.` —— 安装 sass`npm i sass -D`
2. `Uncaught ReferenceError: global is not defined` —— Vite 项目需要在 `vite.config.ts` 中加上:
```ts
// vite 8以下版本
optimizeDeps: {
esbuildOptions: {
define: { global: 'globalThis' },
},
}
```
```ts
// vite 8及以上
define: {
global: 'globalThis',
},
```
:::
### 3. 渲染 m-editor
`App.vue` 中渲染 `<TMagicEditor />`(即 `m-editor` 组件),最少需要传入 `v-model``runtime-url``component-group-list``props-configs``props-values` 五个核心属性:
```vue
<template>
<m-editor
v-model="dsl"
:menu="menu"
:runtime-url="runtimeUrl"
:props-configs="propsConfigs"
:props-values="propsValues"
:component-group-list="componentGroupList"
>
</m-editor>
<div class="editor-app">
<TMagicEditor
v-model="value"
ref="editor"
:menu="menu"
:runtime-url="runtimeUrl"
:props-configs="propsConfigs"
:props-values="propsValues"
:event-method-list="eventMethodList"
:datasource-configs="datasourceConfigs"
:datasource-values="datasourceValues"
:datasource-event-method-list="datasourceEventMethodList"
:component-group-list="componentGroupList"
:default-selected="defaultSelected"
:stage-rect="stageRect"
:auto-scroll-into-view="true"
/>
</div>
</template>
<script>
import { defineComponent, ref } from "vue";
<script lang="ts" setup>
import { ref, shallowRef } from "vue";
import type { MApp } from "@tmagic/core";
import { TMagicEditor } from "@tmagic/editor";
export default defineComponent({
name: "App",
import componentGroupList from "./configs/componentGroupList";
import dsl from "./configs/dsl";
import { useEditorRes } from "./composables/use-editor-res";
setup() {
return {
menu: ref({
left: [
// 顶部左侧菜单按钮
],
center: [
// 顶部中间菜单按钮
],
right: [
// 顶部右侧菜单按钮
],
}),
const editor = shallowRef<InstanceType<typeof TMagicEditor>>();
const value = ref<MApp>(dsl);
const defaultSelected = ref(dsl.items[0].id);
const stageRect = ref({ width: 375, height: 817 });
dsl: ref({
// 初始化页面数据
}),
const { VITE_RUNTIME_PATH } = import.meta.env;
const runtimeUrl = `${VITE_RUNTIME_PATH}/playground/index.html`;
runtimeUrl: "/runtime/vue/playground/index.html",
const {
propsValues,
propsConfigs,
eventMethodList,
datasourceConfigs,
datasourceValues,
datasourceEventMethodList,
} = useEditorRes();
propsConfigs: [
// 组件属性列表
],
propsValues: [
// 组件默认值
],
componentGroupList: ref([
// 组件列表
]),
};
const menu = {
left: [{ type: "text", text: "魔方" }],
center: ["delete", "undo", "redo", "guides", "rule", "zoom"],
right: [
{
type: "button",
text: "保存",
handler: () =>
localStorage.setItem("magicDSL", JSON.stringify(value.value)),
},
});
],
};
</script>
<style lang="scss">
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
#app {
width: 100%;
height: 100%;
display: flex;
}
.m-editor {
flex: 1;
height: 100%;
}
html,
body,
#app {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.editor-app {
width: 100%;
height: 100%;
}
.editor-app .m-editor {
flex: 1;
height: 100%;
}
</style>
```
关于 [@tmagic/editor](https://github.com/Tencent/tmagic-editor/tree/master/packages/editor) 组件,更多的属性配置详情请参考[编辑器 API](../api/editor/props.md)。
完整的菜单/预览/键盘快捷键实现可以参考 [`playground/src/pages/Editor.vue`](https://github.com/Tencent/tmagic-editor/blob/master/playground/src/pages/Editor.vue)。
其中,**有四个需要注意的属性配置项**`runtimeUrl` `propsValues` `propsConfigs` `componentGroupList`。这是能让我们的编辑器正常运行的关键
更多 prop 详见[编辑器 API](../api/editor/props.md),下文重点介绍最关键的 4 个:`runtimeUrl``componentGroupList``propsConfigs/propsValues`、初始 DSL`v-model`
:::tip
如果出现```Preprocessor dependency "sass" not found. Did you install it?```那么需要install sass
## runtimeUrl
```bash
npm install sass -D
```
:::
编辑器中央的模拟器画布是一个 `iframe``runtimeUrl` 就是这个 iframe 加载的地址,里面运行着一份 **playground runtime**,负责响应编辑器中组件的增删改查。
:::tip
如果是使用vite构建工具如果出现 ```Uncaught ReferenceError: global is not defined```那么需要再vite.config.js中添加如下配置
playground 中通过 Vite proxy 把 runtime 服务(默认端口 `8078`)代理到了同一个域:
```js
{
optimizeDeps: {
esbuildOptions: {
define: {
global: 'globalThis',
},
```ts
server: {
port: 8098,
proxy: {
'^/tmagic-editor/playground/runtime': {
target: 'http://127.0.0.1:8078',
changeOrigin: true,
prependPath: false,
},
},
}
```
:::
## runtimeUrl
该配置涉及到 [runtime 概念](runtime.md)tmagic-editor编辑器中心的模拟器画布是一个 iframe这里的 `runtimeUrl` 配置的,就是你提供的 iframe 的 url其中渲染了一个 runtime用来响应编辑器中的组件增删改等操作。
:::tip
可以使用`npm create tmagic` 来快速创建一个runtime项目。
:::
实际项目中可以使用 `npm create tmagic` 快速生成一个 runtime 项目,详见[RUNTIME](./runtime.md)。
## componentGroupList
`componentGroupList` 是指定左侧组件库内容的配置。此处定义了在编辑器组件库中有什么组件。在添加的时候通过组件 `type` 来确定 runtime 中要渲染什么组件。可以参考 [componentGroupList 配置](../api/editor/props.html#componentgrouplist)
`componentGroupList` 决定左侧组件库展示哪些组件分组。每个 item 通过 `type` 与 runtime 中注册的组件类型一一对应,添加到画布时编辑器会基于 `type` 通知 runtime 渲染对应组件。
## propsConfigs/propsValues
```ts
import {
Files,
FolderOpened,
PictureFilled,
SwitchButton,
Tickets,
} from "@element-plus/icons-vue";
import type { ComponentGroup } from "@tmagic/editor";
`propsConfigs` `propsValues``componentGroupList` 中声明的组件是一一对应的,通过 `type` 来识别属于哪个组件,该配置涉及的内容,就是组件的表单配置描述,在[组件开发中](./component.md)会通过 formConfig 配置来声明这份内容。
`configs` 既可以通过 hardcode 方式写上每个组件的表单配置,也可以通过组件打包方式得到对应内容,然后通过异步加载来载入。比如:
```javascript
setup() {
asyncLoadJs(`/runtime/vue/assets/config.js`).then(() => {
propsConfigs.value = window.magicPresetConfigs;
});
asyncLoadJs(`/runtime/vue/assets/value.js`).then(() => {
propsValues.value = window.magicPresetValues;
});
}
export default [
{
title: "示例容器",
items: [
{ icon: FolderOpened, text: "组", type: "container" },
{ icon: FolderOpened, text: "蒙层", type: "overlay" },
{ icon: Files, text: "迭代器容器", type: "iterator-container" },
],
},
{
title: "示例组件",
items: [
{ icon: Tickets, text: "文本", type: "text" },
{ icon: SwitchButton, text: "按钮", type: "button" },
{ icon: PictureFilled, text: "图片", type: "img" },
],
},
// 也可以提供完整 schema 作为「组合」,添加时直接落入完整子树
{
title: "组合",
items: [
{
icon: Tickets,
text: "弹窗",
data: {
type: "overlay",
name: "弹窗",
style: {
position: "fixed",
width: "100%",
height: "100%",
top: 0,
left: 0,
},
items: [
/* ... */
],
},
},
],
},
] as ComponentGroup[];
```
::: tip 如何快速得到一个 configs/values
上述的 runtime 产物中dist 目录中即包含一个 entry 文件夹在你的项目组件初始化之后分别异步加载里面的config/index.umd.js、value/index.umd.js。并如上面代码中赋值给 configs/values 即可。
完整字段参考 [`componentGroupList`](../api/editor/props.md#componentgrouplist)。
## propsConfigs / propsValues
`propsConfigs` `propsValues``componentGroupList` 中声明的组件通过 `type` 一一对应:
- `propsConfigs[type]`:组件**右侧表单**的配置描述(在组件中 `formConfig` 字段提供)。
- `propsValues[type]`:组件被添加到画布时的**初始默认值**(在组件中 `initValue` 字段提供)。
这些内容会通过 `@tmagic/cli` 在 runtime 构建时打包出对应的 UMD 文件编辑器异步加载即可。playground 中的真实做法([`use-editor-res.ts`](https://github.com/Tencent/tmagic-editor/blob/master/playground/src/pages/composables/use-editor-res.ts)
```ts
import { ref } from "vue";
import { asyncLoadJs } from "@tmagic/editor";
const { VITE_ENTRY_PATH } = import.meta.env;
export const useEditorRes = () => {
const propsValues = ref<Record<string, any>>({});
const propsConfigs = ref<Record<string, any>>({});
const eventMethodList = ref<Record<string, any>>({});
const datasourceConfigs = ref<Record<string, any>>({});
const datasourceValues = ref<Record<string, any>>({});
const datasourceEventMethodList = ref<Record<string, any>>({
base: { events: [], methods: [] },
});
asyncLoadJs(`${VITE_ENTRY_PATH}/config/index.umd.cjs`).then(() => {
propsConfigs.value = (globalThis as any).magicPresetConfigs;
});
asyncLoadJs(`${VITE_ENTRY_PATH}/value/index.umd.cjs`).then(() => {
propsValues.value = (globalThis as any).magicPresetValues;
});
asyncLoadJs(`${VITE_ENTRY_PATH}/event/index.umd.cjs`).then(() => {
eventMethodList.value = (globalThis as any).magicPresetEvents;
});
asyncLoadJs(`${VITE_ENTRY_PATH}/ds-config/index.umd.cjs`).then(() => {
datasourceConfigs.value = (globalThis as any).magicPresetDsConfigs;
});
asyncLoadJs(`${VITE_ENTRY_PATH}/ds-value/index.umd.cjs`).then(() => {
datasourceValues.value = (globalThis as any).magicPresetDsValues;
});
return {
propsValues,
propsConfigs,
eventMethodList,
datasourceConfigs,
datasourceValues,
datasourceEventMethodList,
};
};
```
::: tip 怎样得到这些 UMD 文件?
在 runtime 项目中执行 `npm run build:libs`(参考 [`runtime/vue/package.json`](https://github.com/Tencent/tmagic-editor/blob/master/runtime/vue/package.json)),会在 `dist/entry/` 下生成 `config/value/event/ds-config/ds-value` 五个目录的 UMD 文件,全局变量分别为 `magicPresetConfigs` `magicPresetValues` `magicPresetEvents` `magicPresetDsConfigs` `magicPresetDsValues`
:::
## 更多
如果是在调试期,也可以直接 hardcode 一份 `propsConfigs` / `propsValues`,比如:
通过上述步骤,可以快速得到一个初始化的简单编辑器。
```ts
const propsConfigs = ref({
text: [{ name: "text", text: "文案" }],
button: [{ name: "text", text: "按钮文案" }],
});
除了上述内容外文档的其他章节中也会更深入的描述整个tmagic-editor的设计理念和实现细节。同时你也可以查看我们的[项目源码](https://github.com/Tencent/tmagic-editor),从源码提供的 playground 和 runtime 示例来开发和理解tmagic-editor。
const propsValues = ref({
text: { text: "一段文字" },
button: { text: "按钮" },
});
```
## v-modelDSL 初始值
`v-model` 绑定的是整个页面的 [DSL](./advanced/js-schema.md),最简的初始 DSL 长这样:
```ts
import { type MApp, NodeType } from "@tmagic/core";
const dsl: MApp = {
id: "1",
name: "demo",
type: NodeType.ROOT,
items: [
{
type: NodeType.PAGE,
id: "page_1",
name: "index",
layout: "absolute",
style: { position: "relative", width: "100%", height: "100%" },
items: [],
},
],
};
```
完整含数据源、代码块、事件联动的 DSL 示例见 [`playground/src/configs/dsl.ts`](https://github.com/Tencent/tmagic-editor/blob/master/playground/src/configs/dsl.ts)。
::: tip 持久化与历史记录
playground 用 `localStorage` + `serialize-javascript` 做了一个本地持久化方案,并在保存后调用 `editor.editorService.resetModifiedNodeId()` 重置修改状态,可以直接复用。
:::
## 进阶:编辑器服务与插件
`@tmagic/editor` 提供了多组 **service**`editorService` / `propsService` / `historyService` / `uiService` …)和 **插件机制**,可以非侵入式扩展行为。例如 playground 中:
```ts
import { editorService, propsService } from "@tmagic/editor";
editorService.usePlugin({
beforeDoAdd: (config, parent) => {
if (config.type === "overlay") {
// 蒙层始终插入到当前 page 下,并钉到 (0, 0)
config.style = { ...config.style, left: 0, top: 0 };
return [config, editorService.get("page")];
}
return [config, parent];
},
});
propsService.usePlugin({
beforeFillConfig: (config) => [config, "100px"],
});
```
更多扩展能力见[编辑器扩展](./editor-expand.md)与各 service 的 [API 文档](../api/editor/props.md)。
## 下一步
- [基础概念](./conception.md):编辑器 / 模拟器 / runtime / DSL 的关系
- [RUNTIME](./runtime.md):实现并打包一个 runtime
- [组件开发](./component.md):自定义业务组件
- [页面发布](./publish.md):基于 `@tmagic/cli` 的产物结构与发布流程
- [Playground 源码](https://github.com/Tencent/tmagic-editor/tree/master/playground):与本节示例完全对应
通过 `pnpm bootstrap && pnpm pg` 即可在仓库本地启动这份 playground自由调试。

View File

@ -18,7 +18,7 @@ runtime 的概念是理解tmagic-editor项目页运行的重要概念runti
各个 runtime 的作用除了作为不同场景下的渲染环境同时也是不同环境的打包构建载体。tmagic-editor示例代码中的打包就是基于 runtime 进行的。
### 业务相关
由于 runtime 是页面渲染的承载环境,其中会加载 @tmagic/ui 以及各个业务组件,业务发布项目页也是基于 runtime所以在 runtime 中实现业务方的自定义逻辑是最合适的。runtime 可以提供一些全局 API供业务组件调用。我们可以把下面的模拟器中的 runtime 视为一个业务方runtime。
由于 runtime 是页面渲染的承载环境,其中会加载 `@tmagic/vue-container`(或 `@tmagic/react-container`)等基础渲染组件以及各个业务组件,业务发布项目页也是基于 runtime所以在 runtime 中实现业务方的自定义逻辑是最合适的。runtime 可以提供一些全局 API供业务组件调用。我们可以把下面的模拟器中的 runtime 视为一个业务方runtime。
tmagic-editor提供了三个版本的 runtime 示例,可以参考:
- [vue runtime](https://github.com/Tencent/tmagic-editor/blob/master/runtime/vue)

View File

@ -1,29 +1,328 @@
# RUNTIME
本章详细介绍如何深入理解tmagic-editor的打包以及如何根据需求定制修改tmagic-editor的页面打包发布方案。页面发布、打包相关的定制化开发需要使用tmagic-editor的业务方搭建好基于开源tmagic-editor的管理平台、存储服务等配套设施。
本章详细介绍 tmagic-editor 中 runtime 的概念、目录结构与实现方式。所有内容均与开源仓库 [`runtime/vue/`](https://github.com/Tencent/tmagic-editor/tree/master/runtime/vue) 一一对应,可以对照阅读。
## runtime 是什么
runtime是用来解析DSL的执行环境用于渲染 DSL 呈现页面
**runtime 是用来解析 DSL 的执行环境**。编辑器只负责生成 DSL最终把它**渲染成可见页面**的工作交给 runtime
编辑器生成出来的DSL需要通过 runtime 来渲染。
在一份完整的 tmagic-editor 项目中runtime 同时承担两个角色:
## 实现一个 runtime
| 角色 | 入口 | 用途 |
| --- | --- | --- |
| **page** | `runtime/vue/page/` | 线上发布产物,加载 `window.magicDSL` 渲染真实页面 |
| **playground** | `runtime/vue/playground/` | 编辑器中央 iframe 加载的画布,响应增删改并渲染所见即所得 |
:::tip
可以使用`npm create tmagic` 来快速创建一个runtime项目。
两者共用同一份组件、插件、数据源代码,只在入口(`main.ts` / `App.vue`)上有差异。
::: tip
DSL、playground 与 editor 之间的通信原理可以前往[教程](/guide/tutorial/)继续了解。
:::
创建出来的项目会包含page、playground两个目录。
## 创建 runtime 项目
::: tip
推荐用 `npm create tmagic@latest` / `pnpm create tmagic` 快速生成 runtime 模板,按提示选择 `runtime` 即可。
:::
生成的项目结构如下(与 [`runtime/vue/`](https://github.com/Tencent/tmagic-editor/tree/master/runtime/vue) 完全一致):
```bash
.
├── page
├── playground
runtime/vue
├── page/ # 线上 page 入口
│ ├── App.vue
│ ├── index.html
│ ├── main.ts
│ └── utils/
├── playground/ # 编辑器内 iframe 入口
│ ├── App.vue
│ ├── index.html
│ └── main.ts
├── public/
├── scripts/ # build 脚本res / page / playground / all
├── tmagic.config.ts # @tmagic/cli 配置:声明组件、插件、数据源
├── tmagic.config.local.ts# 本地覆盖配置(可选)
└── vite.config.ts # 多入口构建page + playground
```
page用于生产环境
## tmagic.config.ts声明组件 / 插件 / 数据源
playground用于编辑器中
`tmagic.config.ts` 是 [@tmagic/cli](./publish.md#tmagic-cli) 的入口,它会扫描 `packages` 列表,生成 `.tmagic/comp-entry.ts` 等 5 个入口文件runtime 只需要从这些入口里 `import` 即可:
:::tip
想要了解DSL的解析以及runtime与编辑器的通信可以前往[教程](/guide/tutorial/)
```ts
import { defineConfig } from '@tmagic/cli';
export default defineConfig({
componentFileAffix: '.vue',
// 是否使用 vite + 异步组件,详见 page/main.ts 中的 defineAsyncComponent
dynamicImport: true,
npmConfig: {
client: 'pnpm',
keepPackageJsonClean: true,
},
packages: [
{
// key 为组件 type需要与编辑器中 componentGroupList 的 type 对应
button: '@tmagic/vue-button',
container: '@tmagic/vue-container',
img: '@tmagic/vue-img',
'iterator-container': '@tmagic/vue-iterator-container',
overlay: '@tmagic/vue-overlay',
page: '@tmagic/vue-page',
'page-fragment': '@tmagic/vue-page-fragment',
'page-fragment-container': '@tmagic/vue-page-fragment-container',
qrcode: '@tmagic/vue-qrcode',
text: '@tmagic/vue-text',
},
],
});
```
`tmagic.config.local.ts` 用于本地覆盖(不会被提交),常见用法是把线上 npm 包临时替换为本地组件目录调试。
执行 `npm run tmagic`(即 `tmagic entry`runtime 根目录下会生成:
```bash
.tmagic/
├── comp-entry.ts # page 同步组件入口
├── async-comp-entry.ts # page 异步组件入口dynamicImport 时使用)
├── config-entry.ts # 编辑器右侧表单配置
├── value-entry.ts # 组件初始值
├── event-entry.ts # 组件事件 / 方法列表
├── plugin-entry.ts # 插件入口
├── datasource-entry.ts # 同步数据源入口
└── async-datasource-entry.ts # 异步数据源入口
```
> 详细产物说明见[页面发布 § @tmagic/cli](./publish.md#tmagic-cli)。
## playground runtime 实现
playground 是编辑器中央 iframe 加载的画布,最关键的逻辑就是把编辑器派发的 DSL 变更同步到本地 Vue 状态并触发重新渲染。
`@tmagic/vue-runtime-help` 提供的 `useEditorDsl` Hook 已经帮我们实现了与编辑器的通信(`onRuntimeReady` / `updateRootConfig` / `updatePageId` / `add` / `update` / `remove` 等);只需要在入口里:
1. 创建 `TMagicApp` 实例,注册组件、数据源、插件;
2. 通过 `provide('app', app)` 把实例注入子组件;
3. 在 `App.vue` 里使用 `useEditorDsl()` + `useComponent('page')` 渲染页面。
完整的 [`runtime/vue/playground/main.ts`](https://github.com/Tencent/tmagic-editor/blob/master/runtime/vue/playground/main.ts)
```ts
import { createApp } from 'vue';
import TMagicApp, { DataSourceManager, DeepObservedData } from '@tmagic/core';
import App from './App.vue';
import '@tmagic/core/resetcss.css';
DataSourceManager.registerObservedData(DeepObservedData);
Promise.all([
import('../.tmagic/comp-entry'),
import('../.tmagic/plugin-entry'),
import('../.tmagic/datasource-entry'),
]).then(([components, plugins, dataSources]) => {
const vueApp = createApp(App);
const app = new TMagicApp({
ua: window.navigator.userAgent,
platform: 'editor',
});
if (app.env.isWeb) {
app.setDesignWidth(window.document.documentElement.getBoundingClientRect().width);
}
Object.entries(components.default).forEach(([type, component]: [string, any]) => {
app.registerComponent(type, component);
});
Object.entries(dataSources.default).forEach(([type, ds]: [string, any]) => {
DataSourceManager.register(type, ds);
});
Object.values(plugins.default).forEach((plugin: any) => {
vueApp.use(plugin, { app });
});
window.appInstance = app;
vueApp.config.globalProperties.app = app;
vueApp.provide('app', app);
vueApp.mount('#app');
});
```
[`playground/App.vue`](https://github.com/Tencent/tmagic-editor/blob/master/runtime/vue/playground/App.vue) 出乎意料地短:
```vue
<template>
<component v-if="pageConfig" :is="pageComponent" :key="pageConfig.id" :config="pageConfig"></component>
</template>
<script lang="ts" setup>
import { useComponent, useEditorDsl } from '@tmagic/vue-runtime-help';
const { pageConfig } = useEditorDsl();
const pageComponent = useComponent('page');
</script>
```
::: tip 关键点
- `platform: 'editor'` 告知 `@tmagic/core` 进入编辑模式;
- `useEditorDsl()` 内部已经调用 `window.magic?.onRuntimeReady({...})`,把 add/update/remove 等回调挂载到全局,编辑器通过 `iframe.contentWindow.magic` 触发;
- 当 DSL 变化时,`pageConfig` 自动更新;当页面 DOM 渲染完成,`useEditorDsl` 会调用 `magic.onPageElUpdate(...)` 把页面元素同步给编辑器,让选中框能够吸附。
:::
## page runtime 实现(线上发布)
[`runtime/vue/page/main.ts`](https://github.com/Tencent/tmagic-editor/blob/master/runtime/vue/page/main.ts) 与 playground 的差别在于:
1. 不需要响应编辑器消息,直接读取 `window.magicDSL`(或 `localPreview` 模式下从 `localStorage` 读取);
2. 使用 `defineAsyncComponent` + 异步入口,按需加载组件,**减小首屏体积**
3. 数据源走 `registerDataSourceOnDemand`,只注册当前 DSL 用到的;
4. 注入 `request``userRender` 等业务侧 API 给组件复用。
```ts
import { createApp, defineAsyncComponent, resolveDirective, withDirectives } from 'vue';
import TMagicApp, { DataSourceManager, DeepObservedData, getUrlParam, registerDataSourceOnDemand } from '@tmagic/core';
import components from '../.tmagic/async-comp-entry';
import asyncDataSources from '../.tmagic/async-datasource-entry';
import plugins from '../.tmagic/plugin-entry';
import request, { service } from './utils/request';
import AppComponent from './App.vue';
import { getLocalConfig } from './utils';
import '@tmagic/core/resetcss.css';
DataSourceManager.registerObservedData(DeepObservedData);
const vueApp = createApp(AppComponent);
vueApp.use(request);
const dsl = ((getUrlParam('localPreview') ? getLocalConfig() : window.magicDSL) || [])[0] || {};
const app = new TMagicApp({
ua: window.navigator.userAgent,
config: dsl,
request: service,
curPage: getUrlParam('page'),
useMock: Boolean(getUrlParam('useMock')),
});
app.setDesignWidth(app.env.isWeb ? window.document.documentElement.getBoundingClientRect().width : 375);
Object.entries(components).forEach(([type, component]: [string, any]) => {
app.registerComponent(type, defineAsyncComponent(component));
});
Object.values(plugins).forEach((plugin: any) => {
vueApp.use(plugin, { app });
});
registerDataSourceOnDemand(dsl, asyncDataSources).then((dataSources) => {
Object.entries(dataSources).forEach(([type, ds]: [string, any]) => {
DataSourceManager.register(type, ds);
});
vueApp.config.globalProperties.app = app;
vueApp.provide('app', app);
vueApp.mount('#app');
});
```
[`page/App.vue`](https://github.com/Tencent/tmagic-editor/blob/master/runtime/vue/page/App.vue) 用 `useDsl()`(注意不是 `useEditorDsl`
```vue
<template>
<component :is="pageComponent" :config="pageConfig as MPage"></component>
</template>
<script lang="ts" setup>
import type { MPage } from '@tmagic/core';
import { useComponent, useDsl } from '@tmagic/vue-runtime-help';
const { pageConfig, app } = useDsl();
const pageComponent = useComponent('page');
</script>
```
## vite 多入口构建
`runtime/vue` 通过单个 vite 工程构建出两份产物([`vite.config.ts`](https://github.com/Tencent/tmagic-editor/blob/master/runtime/vue/vite.config.ts)
```ts
build: {
rolldownOptions: {
input: {
page: path.resolve(__dirname, './page/index.html'),
playground: path.resolve(__dirname, './playground/index.html'),
},
},
}
```
加上 `package.json` 中提供的 build 脚本:
```json
{
"scripts": {
"tmagic": "tmagic entry",
"dev": "vite --force",
"build": "rimraf ./dist && node scripts/build.mjs --type=all",
"build:libs": "node scripts/build.mjs --type=res",
"build:page": "node scripts/build.mjs --type=page",
"build:playground": "node scripts/build.mjs --type=playground"
}
}
```
最常用的两个:
- `npm run build:libs`:构建 **编辑器侧**用到的 `config / value / event / ds-config / ds-value` 五份 UMD 资源(输出到 `dist/entry/`),编辑器通过 `asyncLoadJs` 异步加载(参考[快速开始 § propsConfigs / propsValues](./index.md#propsconfigs-propsvalues))。
- `npm run build`:同时产出 `playground/index.html``page/index.html``entry/`,可以一份产物覆盖编辑器、预览、线上三种场景。
## @tmagic/vue-runtime-help 常用 Hook
| Hook | 作用 |
| --- | --- |
| `useEditorDsl()` | playground 入口使用,建立与编辑器通信、维护当前页面 `pageConfig` |
| `useDsl()` | page 入口使用,从 `window.magicDSL` 中读取并维护 `pageConfig` |
| `useComponent(type)` | 通过组件 type 解析出已注册的 Vue 组件(找不到时会回退到 `magic-ui-${type}` |
| `useApp()` | 取出注入的 `TMagicApp` 实例 |
| `useComponentStatus()` | 获取组件在编辑器中的展示/禁用状态 |
::: tip
React runtime 的实现思路完全一致,对应包是 [`@tmagic/react-runtime-help`](https://github.com/Tencent/tmagic-editor/tree/master/runtime/react-runtime-help),可以参照本节自行迁移。
:::
## 跨域
playground 是被编辑器以 iframe 形式加载的,开发期需要保证 runtime 服务允许跨域。仓库里的做法是用 Vite 的 proxy 把 runtime 反代到 playground 同域:
```ts
// playground/vite.config.ts
server: {
port: 8098,
proxy: {
'^/tmagic-editor/playground/runtime': {
target: 'http://127.0.0.1:8078',
changeOrigin: true,
prependPath: false,
},
},
}
```
如果编辑器和 runtime 跨域部署,需要在 runtime 服务侧返回 `Access-Control-Allow-Origin`,并保证 iframe 的 `postMessage` 同源策略允许双方通信。
## 进一步阅读
- [基础概念](./conception.md)编辑器、模拟器、runtime 的关系
- [组件开发](./component.md)组件四件套component / form-config / init-value / event
- [页面发布](./publish.md)page.html 注入 DSL 的发布流程
- [教程](./tutorial/index.md):从零实现一份 runtime理解 magic API 与 DSL 解析

View File

@ -1,12 +1,26 @@
# 3.[DSL](../conception.md#dsl) 解析渲染
tmagic 提供了 vue/react 两个版本的解析渲染组件,可以直接使用
tmagic 提供了 vue/react 两个版本的解析渲染组件,可以直接使用。基础渲染组件以 container 为核心,配合 page、button、img、text 等多个独立的 npm 包,分别发布在 `vue-components/``react-components/` 下:
[@tmagic/ui](https://www.npmjs.com/package/@tmagic/ui)
vue 版本:
[@tmagic/ui-react](https://www.npmjs.com/package/@tmagic/ui-react)
- [@tmagic/vue-container](https://www.npmjs.com/package/@tmagic/vue-container)
- [@tmagic/vue-page](https://www.npmjs.com/package/@tmagic/vue-page)
- [@tmagic/vue-button](https://www.npmjs.com/package/@tmagic/vue-button)
- [@tmagic/vue-img](https://www.npmjs.com/package/@tmagic/vue-img)
- [@tmagic/vue-text](https://www.npmjs.com/package/@tmagic/vue-text)
- 其他:`@tmagic/vue-overlay``@tmagic/vue-qrcode``@tmagic/vue-page-fragment``@tmagic/vue-page-fragment-container``@tmagic/vue-iterator-container`
接下来是以vue为基础来讲述如何实现一个[@tmagic/ui](https://www.npmjs.com/package/@tmagic/ui)
react 版本:
- [@tmagic/react-container](https://www.npmjs.com/package/@tmagic/react-container)
- [@tmagic/react-page](https://www.npmjs.com/package/@tmagic/react-page)
- [@tmagic/react-button](https://www.npmjs.com/package/@tmagic/react-button)
- [@tmagic/react-img](https://www.npmjs.com/package/@tmagic/react-img)
- [@tmagic/react-text](https://www.npmjs.com/package/@tmagic/react-text)
- 其他:`@tmagic/react-overlay``@tmagic/react-qrcode``@tmagic/react-page-fragment``@tmagic/react-page-fragment-container``@tmagic/react-iterator-container`
接下来是以 vue 为基础,来讲述如何实现一个类似 [@tmagic/vue-container](https://www.npmjs.com/package/@tmagic/vue-container) 的渲染器
## 准备工作

View File

@ -349,6 +349,43 @@ export default {
lib: 'always',
},
],
/**
* 禁止匿名 default class / default function 导出
* @reason 匿名 default 导出在 dts 聚合rolldown / api-extractor / vue-tsc 时会被命名为
* `export_default`导致跨包继承链在 .vue / .tsx 文件下解析失败
* 父类成员 EventEmitter on/off无法被 ts-plugin 推断出来
* 必须使用具名形式 `export class Foo {}` `export default Foo;`
* `export default class Foo {}`确保类型聚合后保留原标识符
*
* 此处需要重申 base.mjs 中已有的 no-restricted-syntax 选择器
* ForIn / Labeled / With否则在 .ts/.tsx 下会被本规则整体覆盖
*/
'no-restricted-syntax': [
'error',
{
selector: 'ExportDefaultDeclaration > ClassDeclaration[id=null]',
message:
'禁止匿名 default class 导出。请改为具名形式(如 `export default class Foo extends Bar {}`),否则聚合 dts 会丢失类型信息导致跨包继承的成员on/off/emit 等)无法被推断。',
},
{
selector: 'ExportDefaultDeclaration > FunctionDeclaration[id=null]',
message:
'禁止匿名 default function 导出。请改为具名形式(如 `export default function foo() {}`),便于 dts 聚合保留原标识符与跨包类型推断。',
},
{
selector: 'ForInStatement',
message:
'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.',
},
{
selector: 'LabeledStatement',
message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.',
},
{
selector: 'WithStatement',
message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.',
},
],
/**
* 在类型注释周围需要一致的间距
*/

View File

@ -1,9 +1,9 @@
{
"version": "1.7.14-beta.0",
"version": "1.8.0-beta.4",
"name": "tmagic",
"private": true,
"type": "module",
"packageManager": "pnpm@10.32.1",
"packageManager": "pnpm@10.33.4",
"scripts": {
"bootstrap": "pnpm i && pnpm build",
"clean:top": "rimraf */**/dist */**/types */dist coverage dwt* temp packages/cli/lib",
@ -56,6 +56,7 @@
"enquirer": "^2.4.1",
"eslint": "^10.3.0",
"execa": "^9.6.0",
"happy-dom": "^20.9.0",
"highlight.js": "^11.11.1",
"husky": "^9.1.7",
"jsdom": "^27.2.0",
@ -65,18 +66,18 @@
"prettier": "^3.8.3",
"recast": "^0.23.11",
"rimraf": "^3.0.2",
"rolldown": "^1.0.0",
"rolldown-plugin-dts": "^0.25.0",
"rolldown": "^1.0.1",
"rolldown-plugin-dts": "^0.25.1",
"sass-embedded": "^1.99.0",
"semver": "^7.7.3",
"serialize-javascript": "^7.0.0",
"shx": "^0.3.4",
"typescript": "catalog:",
"vite": "catalog:",
"vitepress": "^1.6.4",
"vitest": "^4.1.5",
"vitepress": "^2.0.0-alpha.17",
"vitest": "^4.1.6",
"vue": "catalog:",
"vue-tsc": "^3.2.8"
"vue-tsc": "^3.2.9"
},
"config": {
"commitizen": {

View File

@ -1,5 +1,5 @@
{
"version": "1.7.14-beta.0",
"version": "1.8.0-beta.4",
"name": "@tmagic/cli",
"main": "lib/index.js",
"types": "lib/index.d.ts",

View File

@ -1,18 +1,137 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { describe, expect, test } from 'vitest';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import Core from '../src/Core';
import { ModuleMainFilePath, UserConfig } from '../src/types';
const emptyModuleMap: ModuleMainFilePath = {
componentPackage: {},
componentMap: {},
pluginPakcage: {},
pluginMap: {},
configMap: {},
valueMap: {},
eventMap: {},
datasourcePackage: {},
datasourceMap: {},
dsConfigMap: {},
dsValueMap: {},
dsEventMap: {},
};
/**
* prepareEntryFile writeTemp await
*
*/
const waitForFile = async (filePath: string, timeoutMs = 2000) => {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (fs.existsSync(filePath)) return true;
await new Promise((resolve) => setTimeout(resolve, 20));
}
return false;
};
describe('Core', () => {
test('instance', () => {
const core = new Core({
packages: [],
source: './a',
temp: './b',
});
expect(core).toBeInstanceOf(Core);
let tmpRoot: string;
beforeEach(() => {
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-core-'));
});
afterEach(() => {
fs.rmSync(tmpRoot, { recursive: true, force: true });
vi.restoreAllMocks();
});
test('实例化后基本字段齐备', () => {
const core = new Core({ packages: [], source: './a', temp: './b' });
expect(core).toBeInstanceOf(Core);
expect(typeof core.version).toBe('string');
expect(core.options.source).toBe('./a');
expect(core.moduleMainFilePath.componentMap).toEqual({});
});
test('dir.temp() 解析为 source/temp 的绝对路径', () => {
const core = new Core({ packages: [], source: './a', temp: './b' });
expect(core.dir.temp()).toBe(path.join(process.cwd(), './a/b'));
});
test('writeTemp 会按 temp 目录写入文件', async () => {
const core = new Core({ packages: [], source: tmpRoot, temp: 'tmp-out' });
await core.writeTemp('hello.txt', 'world');
const target = path.join(tmpRoot, 'tmp-out', 'hello.txt');
expect(fs.existsSync(target)).toBe(true);
expect(fs.readFileSync(target, 'utf-8')).toBe('world');
});
test('init 在没有 packages 时使用默认的 resolveAppPackages 结果', async () => {
const core = new Core({ packages: [], source: tmpRoot, temp: 'tmp' });
await core.init();
expect(core.moduleMainFilePath).toMatchObject({
componentPackage: {},
componentMap: {},
datasourcePackage: {},
});
});
test('init 优先使用 onInit 钩子覆写 moduleMainFilePath', async () => {
const onInit = vi.fn().mockResolvedValue({
...emptyModuleMap,
componentMap: { foo: 'bar' },
});
const options: UserConfig = {
packages: [],
source: tmpRoot,
temp: 'tmp',
onInit,
};
const core = new Core(options);
await core.init();
expect(onInit).toHaveBeenCalledWith(core);
expect(core.moduleMainFilePath.componentMap).toEqual({ foo: 'bar' });
});
test('prepare 会写出 entry 文件,并触发 onPrepare 钩子', async () => {
const onPrepare = vi.fn();
const core = new Core({
packages: [],
source: tmpRoot,
temp: 'tmp-entry',
useTs: true,
onPrepare,
});
await core.prepare();
const tempDir = path.join(tmpRoot, 'tmp-entry');
expect(await waitForFile(path.join(tempDir, 'comp-entry.ts'))).toBe(true);
expect(await waitForFile(path.join(tempDir, 'plugin-entry.ts'))).toBe(true);
expect(await waitForFile(path.join(tempDir, 'datasource-entry.ts'))).toBe(true);
expect(onPrepare).toHaveBeenCalledWith(core);
});
test('prepare 在 useTs=false 时同时输出 .js 与 .d.ts', async () => {
const core = new Core({
packages: [],
source: tmpRoot,
temp: 'tmp-js',
useTs: false,
});
await core.prepare();
const tempDir = path.join(tmpRoot, 'tmp-js');
expect(await waitForFile(path.join(tempDir, 'comp-entry.js'))).toBe(true);
expect(await waitForFile(path.join(tempDir, 'comp-entry.d.ts'))).toBe(true);
});
});

View File

@ -0,0 +1,49 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { allowTs, transformTsFileToCodeSync } from '../src/utils/allowTs';
describe('allowTs', () => {
let tmpRoot: string;
let tsFile: string;
beforeEach(() => {
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-allowts-'));
tsFile = path.join(tmpRoot, 'sample.ts');
fs.writeFileSync(
tsFile,
`export const greet = (name: string): string => \`hi \${name}\`;\nexport default greet;\n`,
);
});
afterEach(() => {
fs.rmSync(tmpRoot, { recursive: true, force: true });
delete require.extensions['.ts'];
});
test('transformTsFileToCodeSync 输出 cjs 代码并保留逻辑', () => {
const code = transformTsFileToCodeSync(tsFile);
expect(code).toContain('exports');
expect(code).toContain('greet');
expect(code).toContain('hi');
});
test('allowTs 注册 .ts loader 后require 可以加载 ts 文件', () => {
allowTs();
expect(typeof require.extensions['.ts']).toBe('function');
delete require.cache[tsFile];
// eslint-disable-next-line @typescript-eslint/no-require-imports
const mod = require(tsFile);
const greet = mod.default ?? mod.greet;
expect(greet('world')).toBe('hi world');
});
});

View File

@ -0,0 +1,56 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { cli } from '../src/cli';
describe('cli', () => {
let tmpRoot: string;
let originalArgv: string[];
beforeEach(() => {
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-cli-'));
originalArgv = process.argv;
vi.spyOn(console, 'log').mockImplementation(() => undefined);
});
afterEach(() => {
fs.rmSync(tmpRoot, { recursive: true, force: true });
process.argv = originalArgv;
delete require.extensions['.ts'];
vi.restoreAllMocks();
});
test('调用后注册了 .ts 扩展并能解析 --version 参数', () => {
process.argv = ['node', 'tmagic', '--version'];
expect(() =>
cli({
packages: [],
source: tmpRoot,
temp: 'tmp',
}),
).not.toThrow();
expect(typeof require.extensions['.ts']).toBe('function');
});
test('未指定子命令时不会触发 entry 动作', () => {
process.argv = ['node', 'tmagic'];
expect(() =>
cli({
packages: [],
source: tmpRoot,
temp: 'tmp',
}),
).not.toThrow();
});
});

View File

@ -0,0 +1,112 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { scripts } from '../src/commands';
import Core from '../src/Core';
import { allowTs } from '../src/utils/allowTs';
const writeFile = (file: string, content: string) => {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, content);
};
describe('scripts (entry 命令)', () => {
let tmpRoot: string;
let originalNodeEnv: string | undefined;
beforeEach(() => {
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-cmd-'));
originalNodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;
allowTs();
vi.spyOn(console, 'log').mockImplementation(() => undefined);
});
afterEach(() => {
fs.rmSync(tmpRoot, { recursive: true, force: true });
if (originalNodeEnv === undefined) {
delete process.env.NODE_ENV;
} else {
process.env.NODE_ENV = originalNodeEnv;
}
delete require.extensions['.ts'];
vi.restoreAllMocks();
});
test('未指定 NODE_ENV 时默认设为 development并返回初始化好的 App', async () => {
const entry = scripts({
packages: [],
source: tmpRoot,
temp: 'tmp',
});
const app = await entry();
expect(process.env.NODE_ENV).toBe('development');
expect(app).toBeInstanceOf(Core);
expect(app.options.source).toBe(tmpRoot);
});
test('cleanTemp=true 时会清空 temp 目录', async () => {
const tempDir = path.join(tmpRoot, 'tmp');
writeFile(path.join(tempDir, 'old.txt'), 'should be deleted');
const entry = scripts({
packages: [],
source: tmpRoot,
temp: 'tmp',
cleanTemp: true,
});
await entry();
expect(fs.existsSync(path.join(tempDir, 'old.txt'))).toBe(false);
});
test('能够读取 source 下的 tmagic.config.js 并合并到默认配置中', async () => {
writeFile(path.join(tmpRoot, 'tmagic.config.js'), 'module.exports = { useTs: false, packages: [] };\n');
const entry = scripts({
packages: [],
source: tmpRoot,
temp: 'tmp',
useTs: true,
});
const app = await entry();
expect(app.options.useTs).toBe(false);
});
test('local 配置文件会覆盖普通配置,并且 packages 会被合并', async () => {
writeFile(path.join(tmpRoot, 'tmagic.config.js'), "module.exports = { useTs: false, packages: ['foo'] };\n");
writeFile(path.join(tmpRoot, 'tmagic.config.local.js'), "module.exports = { useTs: true, packages: ['bar'] };\n");
const entry = scripts({
packages: [],
source: tmpRoot,
temp: 'tmp',
});
// packages 中的 'foo' 与 'bar' 都不是真实的 npm 包,
// 由于配置在合并后会触发 resolveAppPackages 解析,这里我们 mock 掉 init
// 以便仅校验配置合并行为。
const initSpy = vi.spyOn(Core.prototype, 'init').mockResolvedValue(undefined);
const prepareSpy = vi.spyOn(Core.prototype, 'prepare').mockResolvedValue(undefined);
const app = await entry();
expect(initSpy).toHaveBeenCalled();
expect(prepareSpy).toHaveBeenCalled();
expect(app.options.useTs).toBe(true);
expect(app.options.packages).toEqual(['foo', 'bar']);
});
});

View File

@ -0,0 +1,138 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import Core from '../src/Core';
import { EntryType } from '../src/types';
import { generateContent, makeCamelCase, prepareEntryFile, prettyCode } from '../src/utils/prepareEntryFile';
/**
* prepareEntryFile writeTemp Promise
*
*/
const waitForContent = async (filePath: string, expected: string, timeoutMs = 2000) => {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf-8');
if (content.includes(expected)) return content;
}
await new Promise((resolve) => setTimeout(resolve, 20));
}
return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '';
};
describe('makeCamelCase', () => {
test('短横线分隔的字符串转为驼峰', () => {
expect(makeCamelCase('foo-bar-baz')).toBe('fooBarBaz');
expect(makeCamelCase('foo')).toBe('foo');
expect(makeCamelCase('a-b-c-d')).toBe('aBCD');
});
test('非字符串返回空字符串', () => {
expect(makeCamelCase(123 as unknown as string)).toBe('');
expect(makeCamelCase(null as unknown as string)).toBe('');
expect(makeCamelCase(undefined as unknown as string)).toBe('');
});
});
describe('prettyCode', () => {
test('转换反斜杠并美化代码', () => {
const out = prettyCode("const x: Record<string, any> = { 'a\\b': 1 };\nexport default x;");
expect(out).toContain("'a/b'");
expect(out).toContain('export default');
});
});
describe('generateContent', () => {
test('使用默认参数生成空对象的入口文件', () => {
const code = generateContent(true, EntryType.COMPONENT);
expect(code).toContain('const components: Record<string, any>');
expect(code).toContain('export default components');
});
test('为组件 / 插件 / 数据源生成 default import', () => {
const code = generateContent(
true,
EntryType.COMPONENT,
{ 'foo-bar': 'foo-bar-pkg' },
{ 'foo-bar': './foo-bar/index' },
);
expect(code).toContain("import fooBar from './foo-bar/index'");
expect(code).toContain("'foo-bar': fooBar");
});
test('config / value / event 类型并且 packagePath 与 packageMap 一致时使用具名导入', () => {
const code = generateContent(true, EntryType.CONFIG, { 'foo-bar': './pkg' }, { 'foo-bar': './pkg' });
expect(code).toContain("import { config as fooBar } from './pkg'");
expect(code).toContain("'foo-bar': fooBar");
});
test('dynamicImport 启用时使用 import() 语法', () => {
const code = generateContent(true, EntryType.COMPONENT, { foo: './foo' }, { foo: './foo/index' }, true);
expect(code).toContain("'foo': () => import('./foo/index')");
});
test('dynamicIgnore 中的 key 不走 dynamicImport', () => {
const code = generateContent(
true,
EntryType.COMPONENT,
{ foo: './foo', bar: './bar' },
{ foo: './foo/index', bar: './bar/index' },
true,
['foo'],
);
expect(code).toContain("import foo from './foo/index'");
expect(code).toContain("'foo': foo");
expect(code).toContain("'bar': () => import('./bar/index')");
});
test('useTs=false 时不会添加类型注解', () => {
const code = generateContent(false, EntryType.COMPONENT, { foo: './foo' }, { foo: './foo' });
expect(code).not.toContain('Record<string, any>');
expect(code).toContain('const components');
});
});
describe('prepareEntryFile', () => {
let tmpRoot: string;
beforeEach(() => {
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-prep-'));
});
afterEach(() => {
fs.rmSync(tmpRoot, { recursive: true, force: true });
vi.restoreAllMocks();
});
test('beforeWriteEntry 钩子可以改写最终写入的内容', async () => {
const beforeWriteEntry = vi.fn(async (map: Record<string, string>) => ({
...map,
'comp-entry': '// custom comp entry\n',
}));
const core = new Core({
packages: [],
source: tmpRoot,
temp: 'tmp',
useTs: true,
hooks: { beforeWriteEntry },
});
await prepareEntryFile(core);
expect(beforeWriteEntry).toHaveBeenCalled();
const compEntry = path.join(tmpRoot, 'tmp', 'comp-entry.ts');
const content = await waitForContent(compEntry, 'custom comp entry');
expect(content).toContain('custom comp entry');
});
});

View File

@ -0,0 +1,173 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import Core from '../src/Core';
import { resolveAppPackages } from '../src/utils/resolveAppPackages';
const writeFile = (filePath: string, content: string) => {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content);
};
describe('resolveAppPackages', () => {
let tmpRoot: string;
beforeEach(() => {
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-resolve-'));
vi.spyOn(console, 'log').mockImplementation(() => undefined);
});
afterEach(() => {
fs.rmSync(tmpRoot, { recursive: true, force: true });
vi.restoreAllMocks();
});
test('packages 为空时返回空的映射结构', () => {
const app = new Core({ packages: [], source: tmpRoot, temp: 'tmp' });
const result = resolveAppPackages(app);
expect(result).toEqual({
componentPackage: {},
componentMap: {},
configMap: {},
eventMap: {},
valueMap: {},
pluginPakcage: {},
pluginMap: {},
datasourcePackage: {},
datasourceMap: {},
dsConfigMap: {},
dsEventMap: {},
dsValueMap: {},
});
});
test('解析普通组件目录', () => {
const pkgDir = path.join(tmpRoot, 'my-comp');
writeFile(
path.join(pkgDir, 'index.js'),
"import Foo from './Foo';\nexport default Foo;\nexport const config = {};\nexport const value = {};\n",
);
writeFile(path.join(pkgDir, 'Foo.vue'), '<template></template>');
const app = new Core({
packages: [{ 'my-comp': pkgDir }],
source: tmpRoot,
temp: 'tmp',
componentFileAffix: '.vue',
});
const result = resolveAppPackages(app);
expect(Object.keys(result.componentPackage)).toContain('my-comp');
expect(result.componentMap['my-comp']).toBeTruthy();
});
test('解析插件 (export default 含 install 的对象)', () => {
const pkgDir = path.join(tmpRoot, 'my-plugin');
writeFile(path.join(pkgDir, 'index.js'), 'export default { install() {} };\n');
const app = new Core({
packages: [{ 'my-plugin': pkgDir }],
source: tmpRoot,
temp: 'tmp',
});
const result = resolveAppPackages(app);
expect(result.pluginPakcage['my-plugin']).toBeTruthy();
expect(result.pluginMap['my-plugin']).toBeTruthy();
});
test('解析数据源 (export default class extends DataSource)', () => {
const pkgDir = path.join(tmpRoot, 'my-ds');
writeFile(path.join(pkgDir, 'index.js'), 'export default class MyDataSource extends DataSource {}\n');
const app = new Core({
packages: [{ 'my-ds': pkgDir }],
source: tmpRoot,
temp: 'tmp',
});
const result = resolveAppPackages(app);
expect(result.datasourcePackage['my-ds']).toBeTruthy();
});
test('解析自定义父类的数据源 (datasoucreSuperClass)', () => {
const pkgDir = path.join(tmpRoot, 'my-custom-ds');
writeFile(path.join(pkgDir, 'index.js'), 'export default class MyDataSource extends MyBaseDS {}\n');
const app = new Core({
packages: [{ 'my-custom-ds': pkgDir }],
source: tmpRoot,
temp: 'tmp',
datasoucreSuperClass: ['MyBaseDS'],
});
const result = resolveAppPackages(app);
expect(result.datasourcePackage['my-custom-ds']).toBeTruthy();
});
test('解析组件包 (export default 是包含多个子组件的对象)', () => {
const pkgDir = path.join(tmpRoot, 'my-pkg');
writeFile(path.join(pkgDir, 'package.json'), JSON.stringify({ name: 'my-pkg', main: 'index.js' }));
writeFile(
path.join(pkgDir, 'index.js'),
"import foo from './foo';\nimport bar from './bar';\nexport default { foo, bar };\n",
);
writeFile(path.join(pkgDir, 'foo/package.json'), JSON.stringify({ name: 'foo', main: 'index.js' }));
writeFile(path.join(pkgDir, 'foo/index.js'), "import FooComp from './FooComp';\nexport default FooComp;\n");
writeFile(path.join(pkgDir, 'foo/FooComp.vue'), '<template></template>');
writeFile(path.join(pkgDir, 'bar/package.json'), JSON.stringify({ name: 'bar', main: 'index.js' }));
writeFile(path.join(pkgDir, 'bar/index.js'), "import BarComp from './BarComp';\nexport default BarComp;\n");
writeFile(path.join(pkgDir, 'bar/BarComp.vue'), '<template></template>');
const app = new Core({
packages: [pkgDir],
source: tmpRoot,
temp: 'tmp',
componentFileAffix: '.vue',
});
const result = resolveAppPackages(app);
expect(result.componentPackage.foo).toBeTruthy();
expect(result.componentPackage.bar).toBeTruthy();
});
test('字符串形式 packages 没有 key 时仅做解析不写入映射', () => {
const pkgDir = path.join(tmpRoot, 'no-key-comp');
writeFile(path.join(pkgDir, 'index.js'), "import Foo from './Foo';\nexport default Foo;\n");
writeFile(path.join(pkgDir, 'Foo.vue'), '<template></template>');
const app = new Core({
packages: [pkgDir],
source: tmpRoot,
temp: 'tmp',
componentFileAffix: '.vue',
});
const result = resolveAppPackages(app);
expect(Object.keys(result.componentPackage)).toHaveLength(0);
});
test('packages 为对象但找不到合法 moduleName 时抛错', () => {
const app = new Core({
packages: [{ foo: '' }],
source: tmpRoot,
temp: 'tmp',
});
expect(() => resolveAppPackages(app)).toThrowError(/packages中包含非法配置/);
});
});

View File

@ -0,0 +1,206 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import {
backupFile,
backupLock,
backupNpmLock,
backupPackageJson,
backupPnpmLock,
backupYarnLock,
isRootPath,
restoreFile,
restoreLock,
restoreNpmLock,
restorePackageJson,
restorePnpmLock,
restoreYarnLock,
} from '../src/utils/backupPackageFile';
import { defineConfig } from '../src/utils/defineUserConfig';
import { hasExportDefault, isPlainObject, loadUserConfig } from '../src/utils/loadUserConfig';
import * as logger from '../src/utils/logger';
describe('logger', () => {
beforeEach(() => {
vi.spyOn(console, 'log').mockImplementation(() => undefined);
});
afterEach(() => {
vi.restoreAllMocks();
});
test('info / error / success / execInfo 都会调用 console.log', () => {
logger.info('a');
logger.error('b');
logger.success('c');
logger.execInfo('d');
expect((console.log as any).mock.calls.length).toBe(4);
});
});
describe('isRootPath', () => {
test('非字符串输入抛错', () => {
expect(() => isRootPath(123 as any)).toThrow(TypeError);
});
test('空字符串与超长字符串返回 false', () => {
expect(isRootPath('')).toBe(false);
expect(isRootPath('x'.repeat(101))).toBe(false);
});
test('Linux 根路径返回 true', () => {
if (process.platform !== 'win32') {
expect(isRootPath('/')).toBe(true);
expect(isRootPath('/foo')).toBe(false);
}
});
test('两侧空白会被裁剪', () => {
if (process.platform !== 'win32') {
expect(isRootPath(' / ')).toBe(true);
}
});
});
describe('backupFile / restoreFile - 根路径短路', () => {
test('isRootPath 为 true 时 backupFile 与 restoreFile 直接返回', () => {
if (process.platform === 'win32') return;
// 不应抛错也不应有副作用
expect(() => backupFile('/', 'package.json')).not.toThrow();
expect(() => restoreFile('/', 'package.json')).not.toThrow();
});
});
describe('backupFile / restoreFile 流程', () => {
let tmpRoot: string;
let nested: string;
beforeEach(() => {
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-bk-'));
nested = path.join(tmpRoot, 'nested');
fs.mkdirSync(nested, { recursive: true });
fs.writeFileSync(path.join(tmpRoot, 'package.json'), '{}');
});
afterEach(() => {
fs.rmSync(tmpRoot, { recursive: true, force: true });
});
test('在嵌套目录中向上递归找到目标后备份', () => {
backupFile(nested, 'package.json');
expect(fs.existsSync(path.join(tmpRoot, 'package.json.bak'))).toBe(true);
});
test('restore 回滚备份', () => {
backupFile(nested, 'package.json');
fs.writeFileSync(path.join(tmpRoot, 'package.json'), '{"changed":true}');
restoreFile(nested, 'package.json');
const restored = JSON.parse(fs.readFileSync(path.join(tmpRoot, 'package.json'), 'utf-8'));
expect(restored).toEqual({});
});
test('便利函数全部能调用', () => {
fs.writeFileSync(path.join(tmpRoot, 'pnpm-lock.yaml'), '');
fs.writeFileSync(path.join(tmpRoot, 'yarn-lock.json'), '');
fs.writeFileSync(path.join(tmpRoot, 'package-lock.json'), '');
backupPnpmLock(tmpRoot);
backupYarnLock(tmpRoot);
backupNpmLock(tmpRoot);
backupPackageJson(tmpRoot);
expect(fs.existsSync(path.join(tmpRoot, 'pnpm-lock.yaml.bak'))).toBe(true);
expect(fs.existsSync(path.join(tmpRoot, 'yarn-lock.json.bak'))).toBe(true);
expect(fs.existsSync(path.join(tmpRoot, 'package-lock.json.bak'))).toBe(true);
expect(fs.existsSync(path.join(tmpRoot, 'package.json.bak'))).toBe(true);
restorePnpmLock(tmpRoot);
restoreYarnLock(tmpRoot);
restoreNpmLock(tmpRoot);
restorePackageJson(tmpRoot);
});
test('backupLock / restoreLock 走对应 npm 类型', () => {
fs.writeFileSync(path.join(tmpRoot, 'pnpm-lock.yaml'), '');
backupLock(tmpRoot, 'pnpm');
expect(fs.existsSync(path.join(tmpRoot, 'pnpm-lock.yaml.bak'))).toBe(true);
restoreLock(tmpRoot, 'pnpm');
fs.writeFileSync(path.join(tmpRoot, 'yarn-lock.json'), '');
backupLock(tmpRoot, 'yarn');
expect(fs.existsSync(path.join(tmpRoot, 'yarn-lock.json.bak'))).toBe(true);
restoreLock(tmpRoot, 'yarn');
fs.writeFileSync(path.join(tmpRoot, 'package-lock.json'), '');
backupLock(tmpRoot, 'npm');
expect(fs.existsSync(path.join(tmpRoot, 'package-lock.json.bak'))).toBe(true);
restoreLock(tmpRoot, 'npm');
backupLock(tmpRoot, 'unknown');
restoreLock(tmpRoot, 'unknown');
});
});
describe('defineConfig', () => {
test('原样返回输入', () => {
const cfg = { source: '.', temp: 'tmp', packages: [] };
expect(defineConfig(cfg as any)).toBe(cfg);
});
});
describe('loadUserConfig 与 isPlainObject / hasExportDefault', () => {
test('isPlainObject', () => {
expect(isPlainObject({})).toBe(true);
expect(isPlainObject({ a: 1 })).toBe(true);
expect(isPlainObject([])).toBe(false);
expect(isPlainObject(null)).toBe(false);
expect(isPlainObject('s')).toBe(false);
});
test('hasExportDefault 仅识别 __esModule + default', () => {
expect(hasExportDefault({ __esModule: true, default: 1 })).toBe(true);
expect(hasExportDefault({ default: 1 })).toBe(false);
expect(hasExportDefault({ __esModule: true })).toBe(false);
expect(hasExportDefault('x')).toBe(false);
});
test('loadUserConfig - 没有 path 时返回 {}', async () => {
expect(await loadUserConfig()).toEqual({});
expect(await loadUserConfig('')).toEqual({});
});
test('loadUserConfig - 不匹配的扩展名返回 {}', async () => {
expect(await loadUserConfig('/path/file.json')).toEqual({});
});
test('loadUserConfig - 加载真实 .js 配置文件 (CommonJS 默认导出)', async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-cfg-'));
const cfg = path.join(tmp, 'cfg.js');
fs.writeFileSync(cfg, "module.exports = { source: '.', temp: 'tmp', packages: [], useTs: true };\n");
try {
const config = await loadUserConfig(cfg);
expect(config).toMatchObject({ useTs: true, packages: [] });
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test('loadUserConfig - 加载 ESM-style __esModule + default 配置', async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-cfg-esm-'));
const cfg = path.join(tmp, 'cfg.js');
fs.writeFileSync(
cfg,
"Object.defineProperty(exports, '__esModule', { value: true });\n" +
"exports.default = { source: '.', temp: 'tmp', packages: [], useTs: false };\n",
);
try {
const config = await loadUserConfig(cfg);
expect(config).toMatchObject({ useTs: false });
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
});

View File

@ -1,5 +1,5 @@
{
"version": "1.7.14-beta.0",
"version": "1.8.0-beta.4",
"name": "@tmagic/core",
"type": "module",
"sideEffects": false,

View File

@ -244,7 +244,7 @@ class App extends EventEmitter {
node.data?.id &&
node.eventKeys.has(`${String(name)}_${node.data.id}`)
) {
return this.eventHelper.emit(node.eventKeys.get(`${String(name)}_${node.data.id}`)!, node, ...otherArgs);
this.eventHelper.emit(node.eventKeys.get(`${String(name)}_${node.data.id}`)!, node, ...otherArgs);
}
return super.emit(name, ...args);
}

View File

@ -134,7 +134,9 @@ export const transformStyle = (style: Record<string, any> | string, jsEngine: Js
export const COMMON_EVENT_PREFIX = 'magic:common:events:';
export const COMMON_METHOD_PREFIX = 'magic:common:actions:';
// #region EventOption
export interface EventOption {
label: string;
value: string;
}
// #endregion EventOption

View File

@ -1,9 +1,10 @@
import { describe, expect, test } from 'vitest';
import { describe, expect, test, vi } from 'vitest';
import { MApp, NodeType } from '@tmagic/schema';
import App from '../src/App';
import TMagicIteratorContainer from '../src/IteratorContainer';
import Node from '../src/Node';
const createAppDsl = (pageLength: number, nodeLength = 0) => {
const dsl: MApp = {
@ -263,3 +264,236 @@ describe('App', () => {
expect(ic2?.nodes.length).toBe(0);
});
});
describe('App 配置/方法/组件注册', () => {
test('platform=editor 时不创建 eventHelper', () => {
const app = new App({ platform: 'editor' });
expect(app.eventHelper).toBeUndefined();
expect(app.platform).toBe('editor');
});
test('disabledFlexible 时不创建 flexible', () => {
const app = new App({ disabledFlexible: true });
expect((app as any).flexible).toBeUndefined();
});
test('设置自定义 iteratorContainerType / pageFragmentContainerType', () => {
const app = new App({
iteratorContainerType: ['my-iter', 'custom-iter'],
pageFragmentContainerType: 'my-frag',
});
expect(app.iteratorContainerType.has('my-iter')).toBe(true);
expect(app.iteratorContainerType.has('custom-iter')).toBe(true);
expect(app.pageFragmentContainerType.has('my-frag')).toBe(true);
});
test('useMock=true 透传到 DataSourceManager', () => {
const app = new App({ useMock: true });
expect(app.useMock).toBe(true);
});
test('registerComponent / resolveComponent / unregisterComponent', () => {
const app = new App({});
const comp = { tag: 'x' };
app.registerComponent('my', comp);
expect(app.resolveComponent('my')).toBe(comp);
app.unregisterComponent('my');
expect(app.resolveComponent('my')).toBeUndefined();
});
test('registerNode 静态方法存入 nodeClassMap', () => {
class Custom extends Node {}
App.registerNode('custom-type', Custom);
expect(App.nodeClassMap.get('custom-type')).toBe(Custom);
});
test('setEnv 接受字符串/Env 实例', () => {
const app = new App({});
app.setEnv();
expect(app.env).toBeDefined();
app.setEnv('Mozilla/5.0');
expect(app.env).toBeDefined();
});
test('getPage / getNode 默认返回当前 page', () => {
const app = new App({
config: {
type: NodeType.ROOT,
id: 'app',
items: [{ type: NodeType.PAGE, id: 'p1', items: [{ id: 'btn', type: 'button' }] }],
},
});
expect(app.getPage()).toBe(app.page);
expect(app.getPage('p1')).toBe(app.page);
expect(app.getPage('not-exist')).toBeUndefined();
expect(app.getNode('btn')?.data.id).toBe('btn');
});
test('setPage 不存在时清空当前 page', () => {
const app = new App({
config: {
type: NodeType.ROOT,
id: 'app',
items: [{ type: NodeType.PAGE, id: 'p1', items: [] }],
},
});
app.setPage('not-exist');
expect(app.page).toBeUndefined();
});
test('runCode 执行代码块', async () => {
const fn = vi.fn();
const app = new App({
config: {
type: NodeType.ROOT,
id: 'app',
items: [{ type: NodeType.PAGE, id: 'p1', items: [] }],
codeBlocks: { c1: { name: 'c1', content: fn, params: [] } },
},
});
await app.runCode('c1', { p: 1 }, []);
expect(fn).toHaveBeenCalled();
});
test('runCode 抛错时进入 errorHandler', async () => {
const errorHandler = vi.fn();
const app = new App({
errorHandler,
config: {
type: NodeType.ROOT,
id: 'app',
items: [{ type: NodeType.PAGE, id: 'p1', items: [] }],
codeBlocks: {
c1: {
name: 'c1',
content: () => {
throw new Error('boom');
},
params: [],
},
},
},
});
await app.runCode('c1', {}, []);
expect(errorHandler).toHaveBeenCalled();
});
test('runDataSourceMethod 调用 schema methods 中的 content', async () => {
const fn = vi.fn().mockResolvedValue('ok');
const app = new App({
config: {
type: NodeType.ROOT,
id: 'app',
items: [{ type: NodeType.PAGE, id: 'p1', items: [] }],
dataSources: [
{
type: 'base',
id: 'ds_1',
fields: [],
methods: [{ name: 'doIt', content: fn, params: [] }],
events: [],
},
],
} as any,
});
await app.runDataSourceMethod('ds_1', 'doIt', { p: 1 }, []);
expect(fn).toHaveBeenCalled();
});
test('runDataSourceMethod 不存在的数据源直接返回', async () => {
const app = new App({
config: {
type: NodeType.ROOT,
id: 'app',
items: [{ type: NodeType.PAGE, id: 'p1', items: [] }],
},
});
await expect(app.runDataSourceMethod('not', 'm', {}, [])).resolves.toBeUndefined();
await expect(app.runDataSourceMethod('', '', {}, [])).resolves.toBeUndefined();
});
test('emit 触发 node 事件时走 eventHelper', () => {
const app = new App({
config: {
type: NodeType.ROOT,
id: 'app',
items: [
{
type: NodeType.PAGE,
id: 'p1',
items: [{ id: 'btn', type: 'button', events: [{ name: 'click', actions: [] }] }],
},
],
} as any,
});
const node = app.getNode('btn')!;
const result = app.emit('click', node, 'arg1');
expect(typeof result).toBe('boolean');
});
// 回归用例:节点配置了 events 时eventHelper 派发不能短路掉 super.emit
// 即 app.on(name, cb) 注册的回调依然要被触发。
test('emit: 节点已绑定 events 时app.on 注册的监听器仍然会被调用', () => {
const app = new App({
config: {
type: NodeType.ROOT,
id: 'app',
items: [
{
type: NodeType.PAGE,
id: 'p1',
items: [{ id: 'btn', type: 'button', events: [{ name: 'click', actions: [] }] }],
},
],
} as any,
});
const node = app.getNode('btn')!;
const cb = vi.fn();
app.on('click', cb);
const result = app.emit('click', node, 'arg1');
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenCalledWith(node, 'arg1');
// EventEmitter.emit 在有 listener 时返回 true
expect(result).toBe(true);
});
test('emit: 未命中节点 eventKeys 时app.on 注册的监听器正常被调用', () => {
const app = new App({
config: {
type: NodeType.ROOT,
id: 'app',
items: [
{
type: NodeType.PAGE,
id: 'p1',
items: [{ id: 'btn', type: 'button' }],
},
],
} as any,
});
const node = app.getNode('btn')!;
const cb = vi.fn();
app.on('click', cb);
app.emit('click', node, 'arg1');
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenCalledWith(node, 'arg1');
});
test('destroy 清理所有资源', () => {
const app = new App({
config: {
type: NodeType.ROOT,
id: 'app',
items: [{ type: NodeType.PAGE, id: 'p1', items: [{ id: 'btn', type: 'button' }] }],
},
});
app.destroy();
expect(app.page).toBeUndefined();
expect(app.dsl).toBeUndefined();
expect(app.components.size).toBe(0);
});
});

View File

@ -0,0 +1,834 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
import { beforeEach, describe, expect, test, vi } from 'vitest';
import {
ActionType,
type MApp,
NODE_DISABLE_CODE_BLOCK_KEY,
NODE_DISABLE_DATA_SOURCE_KEY,
NodeType,
} from '@tmagic/schema';
import { DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX } from '@tmagic/utils';
import App from '../src/App';
import EventHelper from '../src/EventHelper';
import FlowState from '../src/FlowState';
const flushAsync = () => new Promise((r) => setTimeout(r, 0));
const createDsl = (overrides: Partial<MApp> = {}): MApp => ({
type: NodeType.ROOT,
id: 'app',
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
id: 'btn_1',
type: 'button',
},
{
id: 'btn_2',
type: 'button',
},
],
},
],
...overrides,
});
describe('EventHelper 构造与销毁', () => {
test('实例化继承 EventEmitter 并保存 app 引用', () => {
const app = new App({});
const helper = new EventHelper({ app });
expect(helper).toBeInstanceOf(EventHelper);
expect(helper.app).toBe(app);
expect(helper.eventQueue).toEqual([]);
});
test('destroy 清空内部状态与监听器', () => {
const app = new App({ config: createDsl() });
const helper = app.eventHelper!;
const handler = vi.fn();
helper.on('foo', handler);
helper.destroy();
helper.emit('foo');
expect(handler).not.toHaveBeenCalled();
expect((helper as any).nodeEventList.size).toBe(0);
expect((helper as any).dataSourceEventList.size).toBe(0);
});
});
describe('EventHelper - bindNodeEvents / initEvents / removeNodeEvents', () => {
test('忽略没有 name 的事件配置', () => {
const app = new App({ config: createDsl() });
const helper = app.eventHelper!;
const node = app.getNode('btn_1')!;
node.events = [{ name: '', actions: [] } as any];
helper.bindNodeEvents(node);
expect((helper as any).nodeEventList.size).toBe(0);
});
test('为带 name 的事件创建 symbol 并写入 eventKeys', () => {
const app = new App({ config: createDsl() });
const helper = app.eventHelper!;
const node = app.getNode('btn_1')!;
node.events = [{ name: 'click', actions: [] }];
node.eventKeys.clear();
helper.bindNodeEvents(node);
expect(node.eventKeys.has(`click_${node.data.id}`)).toBe(true);
expect((helper as any).nodeEventList.size).toBe(1);
});
test('已存在的 eventKey 会被复用而不是重新创建', () => {
const app = new App({ config: createDsl() });
const helper = app.eventHelper!;
const node = app.getNode('btn_1')!;
const existingSymbol = Symbol('click_btn_1');
node.eventKeys.set(`click_${node.data.id}`, existingSymbol);
node.events = [{ name: 'click', actions: [] }];
helper.bindNodeEvents(node);
expect(node.eventKeys.get(`click_${node.data.id}`)).toBe(existingSymbol);
});
test('${nodeId}.${eventName} 形式将命名空间转换为 ${eventName}_${nodeId}', () => {
const app = new App({ config: createDsl() });
const helper = app.eventHelper!;
const node = app.getNode('btn_1')!;
node.events = [{ name: 'btn_2.click', actions: [] }];
node.eventKeys.clear();
helper.bindNodeEvents(node);
expect(node.eventKeys.has('click_btn_2')).toBe(true);
});
test('initEvents 会为 page 和 pageFragments 内的节点都绑定事件', () => {
const dsl = createDsl({
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
id: 'pf_container',
type: 'page-fragment-container',
pageFragmentId: 'pf_1',
},
{
id: 'btn_in_page',
type: 'button',
events: [{ name: 'click', actions: [] }],
},
],
},
{
type: NodeType.PAGE_FRAGMENT,
id: 'pf_1',
items: [
{
id: 'btn_in_pf',
type: 'button',
events: [{ name: 'click', actions: [] }],
},
],
},
],
} as any);
const app = new App({ config: dsl });
const helper = app.eventHelper!;
expect(app.pageFragments.size).toBe(1);
helper.initEvents();
expect((helper as any).nodeEventList.size).toBeGreaterThanOrEqual(2);
});
test('removeNodeEvents 会移除全部 node 上注册的监听', () => {
const app = new App({
config: createDsl({
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
id: 'btn_1',
type: 'button',
events: [{ name: 'click', actions: [] }],
},
],
},
],
} as any),
});
const helper = app.eventHelper!;
expect((helper as any).nodeEventList.size).toBe(1);
helper.removeNodeEvents();
expect((helper as any).nodeEventList.size).toBe(0);
});
});
describe('EventHelper - 事件队列', () => {
test('addEventToQueue / getEventQueue', () => {
const app = new App({});
const helper = new EventHelper({ app });
helper.addEventToQueue({ toId: 'x', method: 'm', fromCpt: null, args: [1] });
expect(helper.getEventQueue()).toHaveLength(1);
expect(helper.getEventQueue()[0].toId).toBe('x');
});
});
describe('EventHelper - eventHandler / actionHandler 流程', () => {
let beforeHandler: ReturnType<typeof vi.fn>;
let afterHandler: ReturnType<typeof vi.fn>;
beforeEach(() => {
beforeHandler = vi.fn();
afterHandler = vi.fn();
});
test('emit click 时执行 EventConfig.actions 并触发 before/after 钩子', async () => {
const fromInstance = { doIt: vi.fn() };
const toInstance = { doIt: vi.fn() };
const app = new App({
beforeEventHandler: beforeHandler,
afterEventHandler: afterHandler,
config: createDsl({
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
id: 'btn_1',
type: 'button',
events: [
{
name: 'click',
actions: [{ actionType: ActionType.COMP, to: 'btn_2', method: 'doIt' }],
},
],
},
{ id: 'btn_2', type: 'button' },
],
},
],
} as any),
});
const fromNode = app.getNode('btn_1')!;
const toNode = app.getNode('btn_2')!;
fromNode.setInstance(fromInstance);
toNode.setInstance(toInstance);
app.emit('click', fromNode, 'extraArg');
await flushAsync();
expect(beforeHandler).toHaveBeenCalled();
expect(afterHandler).toHaveBeenCalled();
expect(toInstance.doIt).toHaveBeenCalled();
expect(toInstance.doIt.mock.calls[0][1]).toBe('extraArg');
});
test('actions 中如果 flowState.isAbort 为 true 会中断后续 action', async () => {
const action2Spy = vi.fn();
const codeBlocks = {
abortCode: {
name: 'abortCode',
params: [],
content: ({ flowState }: any) => {
flowState.abort();
},
},
shouldNotRun: {
name: 'shouldNotRun',
params: [],
content: action2Spy,
},
};
const app = new App({
config: createDsl({
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
id: 'btn_1',
type: 'button',
events: [
{
name: 'click',
actions: [
{ actionType: ActionType.CODE, codeId: 'abortCode' } as any,
{ actionType: ActionType.CODE, codeId: 'shouldNotRun' } as any,
],
},
],
},
],
},
],
codeBlocks,
} as any),
});
app.emit('click', app.getNode('btn_1')!);
await flushAsync();
await flushAsync();
expect(action2Spy).not.toHaveBeenCalled();
});
test('CODE action 在 NODE_DISABLE_CODE_BLOCK_KEY=true 时跳过', async () => {
const codeFn = vi.fn();
const app = new App({
config: createDsl({
codeBlocks: { c: { name: 'c', params: [], content: codeFn } },
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
id: 'btn_1',
type: 'button',
[NODE_DISABLE_CODE_BLOCK_KEY]: true,
events: [
{
name: 'click',
actions: [{ actionType: ActionType.CODE, codeId: 'c' } as any],
},
],
},
],
},
],
} as any),
});
app.emit('click', app.getNode('btn_1')!);
await flushAsync();
expect(codeFn).not.toHaveBeenCalled();
});
test('DATA_SOURCE action 正常执行时通过 runDataSourceMethod 调用', async () => {
const methodFn = vi.fn().mockResolvedValue('ok');
const app = new App({
config: createDsl({
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [],
events: [],
methods: [{ name: 'fetch', params: [], content: methodFn }],
},
],
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
id: 'btn_1',
type: 'button',
events: [
{
name: 'click',
actions: [
{
actionType: ActionType.DATA_SOURCE,
dataSourceMethod: ['ds_1', 'fetch'],
params: { x: 1 },
} as any,
],
},
],
},
],
},
],
} as any),
});
app.emit('click', app.getNode('btn_1')!);
await flushAsync();
await flushAsync();
expect(methodFn).toHaveBeenCalled();
expect(methodFn.mock.calls[0][0].params).toEqual({ x: 1 });
});
test('DATA_SOURCE action 在 NODE_DISABLE_DATA_SOURCE_KEY=true 时跳过', async () => {
const methodFn = vi.fn();
const app = new App({
config: createDsl({
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [],
events: [],
methods: [{ name: 'fetch', params: [], content: methodFn }],
},
],
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
id: 'btn_1',
type: 'button',
[NODE_DISABLE_DATA_SOURCE_KEY]: true,
events: [
{
name: 'click',
actions: [
{
actionType: ActionType.DATA_SOURCE,
dataSourceMethod: ['ds_1', 'fetch'],
} as any,
],
},
],
},
],
},
],
} as any),
});
app.emit('click', app.getNode('btn_1')!);
await flushAsync();
expect(methodFn).not.toHaveBeenCalled();
});
test('actionHandler 抛错时调用 errorHandler', async () => {
const errorHandler = vi.fn();
const app = new App({
errorHandler,
config: createDsl({
codeBlocks: {
boom: {
name: 'boom',
params: [],
content: () => {
throw new Error('boom');
},
},
},
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
id: 'btn_1',
type: 'button',
events: [
{
name: 'click',
actions: [{ actionType: ActionType.CODE, codeId: 'boom' } as any],
},
],
},
],
},
],
} as any),
});
app.emit('click', app.getNode('btn_1')!);
await flushAsync();
await flushAsync();
expect(errorHandler).toHaveBeenCalled();
});
test('兼容 DeprecatedEventConfig没有 actions 字段时走 compActionHandler', async () => {
const targetInstance = { ping: vi.fn() };
const app = new App({
config: createDsl({
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
id: 'btn_1',
type: 'button',
events: [{ name: 'click', to: 'btn_2', method: 'ping' } as any],
},
{ id: 'btn_2', type: 'button' },
],
},
],
} as any),
});
const fromNode = app.getNode('btn_1')!;
app.getNode('btn_2')!.setInstance(targetInstance);
app.emit('click', fromNode);
await flushAsync();
expect(targetInstance.ping).toHaveBeenCalled();
});
test('compActionHandler 找不到目标节点时进入 eventQueue', async () => {
const app = new App({
config: createDsl({
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
id: 'btn_1',
type: 'button',
events: [
{
name: 'click',
actions: [{ actionType: ActionType.COMP, to: 'not-exist', method: 'foo' }],
},
],
},
],
},
],
} as any),
});
app.emit('click', app.getNode('btn_1')!);
await flushAsync();
expect(app.eventHelper!.getEventQueue()).toHaveLength(1);
expect(app.eventHelper!.getEventQueue()[0].toId).toBe('not-exist');
expect(app.eventHelper!.getEventQueue()[0].method).toBe('foo');
});
test('compActionHandler目标节点没有 instance 时方法入 node.eventQueue', async () => {
const app = new App({
config: createDsl({
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
id: 'btn_1',
type: 'button',
events: [
{
name: 'click',
actions: [{ actionType: ActionType.COMP, to: 'btn_2', method: 'foo' }],
},
],
},
{ id: 'btn_2', type: 'button' },
],
},
],
} as any),
});
const targetNode = app.getNode('btn_2')!;
app.emit('click', app.getNode('btn_1')!);
await flushAsync();
expect((targetNode as any).eventQueue).toHaveLength(1);
expect((targetNode as any).eventQueue[0].method).toBe('foo');
});
test('compActionHandlermethod 是数组时取 [to, method]', async () => {
const targetInstance = { hi: vi.fn() };
const app = new App({
config: createDsl({
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
id: 'btn_1',
type: 'button',
events: [
{
name: 'click',
actions: [{ actionType: ActionType.COMP, method: ['btn_2', 'hi'] } as any],
},
],
},
{ id: 'btn_2', type: 'button' },
],
},
],
} as any),
});
app.getNode('btn_2')!.setInstance(targetInstance);
app.emit('click', app.getNode('btn_1')!);
await flushAsync();
expect(targetInstance.hi).toHaveBeenCalled();
});
test('compActionHandler当前没有 page 时抛错被 errorHandler 捕获(兼容旧配置)', async () => {
const errorHandler = vi.fn();
const app = new App({
errorHandler,
config: createDsl({
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
id: 'btn_1',
type: 'button',
events: [{ name: 'click', to: 'btn_2', method: 'foo' } as any],
},
],
},
],
} as any),
});
const node = app.getNode('btn_1')!;
app.page = undefined;
app.emit('click', node);
await flushAsync();
await flushAsync();
expect(errorHandler).toHaveBeenCalled();
const lastErr = errorHandler.mock.calls[errorHandler.mock.calls.length - 1][0];
expect(lastErr).toBeInstanceOf(Error);
});
test('compActionHandler在 pageFragments 中也能找到目标节点', async () => {
const app = new App({
config: {
type: NodeType.ROOT,
id: 'app',
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{ id: 'pf_container', type: 'page-fragment-container', pageFragmentId: 'pf_1' },
{
id: 'btn_1',
type: 'button',
events: [
{
name: 'click',
actions: [{ actionType: ActionType.COMP, to: 'btn_in_pf', method: 'go' }],
},
],
},
],
},
{
type: NodeType.PAGE_FRAGMENT,
id: 'pf_1',
items: [{ id: 'btn_in_pf', type: 'button' }],
},
],
} as any,
});
const target = app.pageFragments.get('pf_container')!.getNode('btn_in_pf')!;
const inst = { go: vi.fn() };
target.setInstance(inst);
app.emit('click', app.getNode('btn_1')!);
await flushAsync();
expect(inst.go).toHaveBeenCalled();
});
});
describe('EventHelper - bindDataSourceEvents / removeDataSourceEvents', () => {
test('为数据源 schema.events 中自定义事件绑定监听', async () => {
const app = new App({
config: createDsl({
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [],
methods: [],
events: [
{
name: 'change',
actions: [{ actionType: ActionType.CODE, codeId: 'logCode' } as any],
} as any,
],
},
],
codeBlocks: {
logCode: { name: 'logCode', params: [], content: vi.fn() },
},
} as any),
});
await flushAsync();
const helper = app.eventHelper!;
expect(helper).toBeDefined();
expect((helper as any).dataSourceEventList.has('ds_1')).toBe(true);
const dsEvents: Map<string, any> = (helper as any).dataSourceEventList.get('ds_1');
expect(dsEvents.has('change')).toBe(true);
const ds = app.dataSourceManager!.get('ds_1')!;
expect(ds.listenerCount('change')).toBeGreaterThanOrEqual(1);
});
test('数据源字段变更事件 (DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX) 通过 onDataChange 注册', async () => {
const app = new App({
config: createDsl({
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [{ type: 'string', name: 'foo', title: 'foo', defaultValue: '', enable: true }],
methods: [],
events: [
{
name: `${DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX}.foo`,
actions: [],
} as any,
],
},
],
} as any),
});
await flushAsync();
const ds = app.dataSourceManager!.get('ds_1')!;
const onDataChangeSpy = vi.spyOn(ds, 'onDataChange');
app.eventHelper!.bindDataSourceEvents();
expect(onDataChangeSpy).toHaveBeenCalled();
});
test('event.name 为空字符串时跳过绑定', () => {
const app = new App({
config: createDsl({
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [],
methods: [],
events: [{ name: '', actions: [] } as any],
},
],
} as any),
});
const helper = app.eventHelper!;
expect(() => helper.bindDataSourceEvents()).not.toThrow();
});
test('removeDataSourceEvents当 dataSourceEventList 为空时直接返回', () => {
const app = new App({});
const helper = new EventHelper({ app });
expect(() => helper.removeDataSourceEvents([])).not.toThrow();
});
test('removeDataSourceEvents 会同时清理 onDataChange 与普通事件', async () => {
const app = new App({
config: createDsl({
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [{ type: 'string', name: 'foo', title: 'foo', defaultValue: '', enable: true }],
methods: [],
events: [
{ name: 'change', actions: [] } as any,
{ name: `${DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX}.foo`, actions: [] } as any,
],
},
],
} as any),
});
await flushAsync();
const helper = app.eventHelper!;
const ds = app.dataSourceManager!.get('ds_1')!;
const offSpy = vi.spyOn(ds, 'off');
const offDataChangeSpy = vi.spyOn(ds, 'offDataChange');
helper.removeDataSourceEvents([ds]);
expect(offSpy).toHaveBeenCalled();
expect(offDataChangeSpy).toHaveBeenCalled();
expect((helper as any).dataSourceEventList.size).toBe(0);
});
test('数据源触发自定义事件后会调用配置的 action', async () => {
const codeFn = vi.fn();
const app = new App({
config: createDsl({
codeBlocks: { c: { name: 'c', params: [], content: codeFn } },
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [],
methods: [],
events: [
{
name: 'change',
actions: [{ actionType: ActionType.CODE, codeId: 'c' } as any],
} as any,
],
},
],
} as any),
});
await flushAsync();
const ds = app.dataSourceManager!.get('ds_1')!;
ds.setData({ a: 1 });
await flushAsync();
expect(codeFn).toHaveBeenCalled();
});
});
describe('EventHelper - flowState 状态管理', () => {
test('FlowState abort/reset 行为', () => {
const fs = new FlowState();
expect(fs.isAbort).toBe(false);
fs.abort();
expect(fs.isAbort).toBe(true);
fs.reset();
expect(fs.isAbort).toBe(false);
});
});

View File

@ -0,0 +1,57 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import { describe, expect, test, vi } from 'vitest';
import Flexible from '../src/Flexible';
describe('Flexible', () => {
test('实例化默认 designWidth=375 并设置 fontSize', () => {
const f = new Flexible();
expect(f.designWidth).toBe(375);
expect(globalThis.document.body.style.fontSize).toBeDefined();
f.destroy();
});
test('options.designWidth 触发 refreshRem 与 fontSize 写入', () => {
const f = new Flexible({ designWidth: 750 });
expect(f.designWidth).toBe(750);
expect(globalThis.document.documentElement.style.fontSize).toMatch(/px$/);
f.destroy();
});
test('setDesignWidth 更新数值并 refresh', () => {
const f = new Flexible();
f.setDesignWidth(414);
expect(f.designWidth).toBe(414);
f.destroy();
});
test('correctRem 根据计算偏差调整字体', () => {
const f = new Flexible();
const fontSize = 100;
const result = f.correctRem(fontSize);
expect(typeof result).toBe('number');
f.destroy();
});
test('resize 事件 debounce 调用 refreshRem', async () => {
const f = new Flexible();
const spy = vi.spyOn(f, 'refreshRem').mockImplementation(() => undefined);
globalThis.dispatchEvent(new Event('resize'));
await new Promise((r) => setTimeout(r, 350));
expect(spy).toHaveBeenCalled();
spy.mockRestore();
f.destroy();
});
test('pageshow persisted 触发 resize 处理', () => {
const f = new Flexible();
const evt = new Event('pageshow') as any;
evt.persisted = true;
globalThis.dispatchEvent(evt);
f.destroy();
});
});

View File

@ -0,0 +1,126 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import { describe, expect, test, vi } from 'vitest';
import { type MApp, NodeType } from '@tmagic/schema';
import App from '../src/App';
import Node from '../src/Node';
const baseDsl = (): MApp => ({
type: NodeType.ROOT,
id: 'app',
items: [
{
type: NodeType.PAGE,
id: 'p1',
items: [{ id: 'btn', type: 'button' }],
},
],
});
describe('Node 基础', () => {
test('实例化时初始化 events / style 默认值', () => {
const app = new App({ config: baseDsl() });
const node = app.page!.getNode('btn')!;
expect(node).toBeInstanceOf(Node);
expect(node.events).toEqual([]);
expect(node.style).toEqual({});
});
test('setData 更新 events / style 并触发 update-data 事件', () => {
const app = new App({ config: baseDsl() });
const node = app.page!.getNode('btn')!;
const handler = vi.fn();
node.on('update-data', handler);
node.setData({
id: 'btn',
type: 'button',
events: [{ name: 'click', actions: [] }],
style: { color: 'red' },
} as any);
expect(handler).toHaveBeenCalled();
expect(node.events).toHaveLength(1);
expect(node.style.color).toBe('red');
});
test('setInstance 与 setData 同步实例的 config', () => {
const app = new App({ config: baseDsl() });
const node = app.page!.getNode('btn')!;
const instance: any = {};
node.setInstance(instance);
node.setData({ id: 'btn', type: 'button', text: 'changed' } as any);
expect(instance.config?.text).toBe('changed');
});
test('frozen instance 时 setData 不抛错', () => {
const app = new App({ config: baseDsl() });
const node = app.page!.getNode('btn')!;
const frozen = Object.freeze({ __isVue: false });
node.setInstance(frozen);
expect(() => node.setData({ id: 'btn', type: 'button' } as any)).not.toThrow();
});
test('addEventToQueue 入队', () => {
const app = new App({ config: baseDsl() });
const node = app.page!.getNode('btn')!;
node.addEventToQueue({ method: 'm', fromCpt: null, args: [1, 2] });
expect((node as any).eventQueue).toHaveLength(1);
});
test('registerMethod (deprecated) 注入实例方法', () => {
const app = new App({ config: baseDsl() });
const node = app.page!.getNode('btn')!;
node.registerMethod({ doIt: () => 'ok', notFn: 'x' as any });
expect(node.instance.doIt()).toBe('ok');
expect(node.instance.notFn).toBeUndefined();
node.registerMethod(undefined as any);
});
test('runHookCode 函数式回退', async () => {
const app = new App({ config: baseDsl() });
const node = app.page!.getNode('btn')!;
const fn = vi.fn();
(node.data as any).created = fn;
await node.runHookCode('created');
expect(fn).toHaveBeenCalledWith(node);
});
test('runHookCode 数据格式不匹配时不报错', async () => {
const app = new App({ config: baseDsl() });
const node = app.page!.getNode('btn')!;
(node.data as any).onSomething = { hookType: 'other' };
await expect(node.runHookCode('onSomething')).resolves.toBeUndefined();
});
test('destroy 清理状态与监听', () => {
const app = new App({ config: baseDsl() });
const node = app.page!.getNode('btn')!;
const handler = vi.fn();
node.on('test', handler);
node.destroy();
node.emit('test');
expect(handler).not.toHaveBeenCalled();
expect(node.instance).toBeNull();
expect(node.events).toEqual([]);
});
test('created/destroy 生命周期触发 hook', async () => {
const app = new App({ config: baseDsl() });
const codeFn = vi.fn();
app.codeDsl = {
hello: { name: 'hello', content: codeFn, params: [] },
} as any;
const node = app.page!.getNode('btn')!;
(node.data as any).created = {
hookType: 'code',
hookData: [{ codeId: 'hello', params: {} }],
};
node.emit('created', null);
await new Promise((r) => setTimeout(r, 0));
expect(codeFn).toHaveBeenCalled();
});
});

View File

@ -1,5 +1,5 @@
{
"version": "1.7.14-beta.0",
"version": "1.8.0-beta.4",
"name": "@tmagic/data-source",
"type": "module",
"sideEffects": false,

View File

@ -241,7 +241,7 @@ export const registerDataSourceOnDemand = async (
dsl: MApp,
dataSourceModules: Record<string, () => Promise<AsyncDataSourceResolveResult>>,
) => {
const { dataSourceMethodsDeps = {}, dataSourceCondDeps = {}, dataSourceDeps = {}, dataSources = [] } = dsl;
const { dataSourceMethodDeps = {}, dataSourceCondDeps = {}, dataSourceDeps = {}, dataSources = [] } = dsl;
const dsModuleMap: Record<string, () => Promise<AsyncDataSourceResolveResult>> = {};
@ -253,7 +253,7 @@ export const registerDataSourceOnDemand = async (
}
if (!Object.keys(dep).length) {
dep = dataSourceMethodsDeps[ds.id] || {};
dep = dataSourceMethodDeps[ds.id] || {};
}
if (Object.keys(dep).length && dataSourceModules[ds.type]) {

View File

@ -1,8 +1,9 @@
import { describe, expect, test } from 'vitest';
import { describe, expect, test, vi } from 'vitest';
import App from '@tmagic/core';
import { DataSource } from '@data-source/index';
import { DeepObservedData } from '@data-source/observed-data';
describe('DataSource', () => {
test('instance', () => {
@ -111,3 +112,130 @@ describe('DataSource setData', () => {
expect(ds.data.obj.a).toBe('a1');
});
});
describe('DataSource lifecycle / mock', () => {
test('编辑器中使用 mock 数据', () => {
const app = new App({}) as any;
app.platform = 'editor';
const ds = new DataSource({
app,
schema: {
type: 'base',
id: '1',
fields: [{ name: 'name' }],
methods: [],
events: [],
mocks: [{ useInEditor: true, data: { name: 'mock' }, enable: true }],
} as any,
});
expect(ds.data.name).toBe('mock');
});
test('useMock=true 在运行时使用 mock', () => {
const ds = new DataSource({
app: new App({}),
useMock: true,
schema: {
type: 'base',
id: '1',
fields: [{ name: 'name' }],
methods: [],
events: [],
mocks: [{ enable: true, data: { name: 'enabled-mock' } }],
} as any,
});
expect(ds.data.name).toBe('enabled-mock');
});
test('initialData 优先时设置 isInit', () => {
const ds = new DataSource({
app: new App({}),
initialData: { name: 'preset' },
schema: {
type: 'base',
id: '1',
fields: [{ name: 'name' }],
methods: [],
events: [],
},
});
expect(ds.isInit).toBe(true);
expect(ds.data.name).toBe('preset');
});
test('支持自定义 ObservedDataClass', () => {
const ds = new DataSource({
app: new App({}),
ObservedDataClass: DeepObservedData,
schema: {
type: 'base',
id: '1',
fields: [{ name: 'name' }],
methods: [],
events: [],
},
});
const cb = vi.fn();
ds.onDataChange('name', cb);
ds.setData('next', 'name');
expect(cb).toHaveBeenCalled();
ds.offDataChange('name', cb);
});
test('setValue 等价于按 path 的 setData 并发出 change', () => {
const ds = new DataSource({
app: new App({}),
schema: {
type: 'base',
id: '1',
fields: [{ name: 'name' }],
methods: [],
events: [],
},
});
const change = vi.fn();
ds.on('change', change);
ds.setValue('name', 'V');
expect(ds.data.name).toBe('V');
expect(change).toHaveBeenCalledWith({ updateData: 'V', path: 'name' });
});
test('setFields / setMethods / DATA_SOURCE_SET_DATA_METHOD_NAME 自动注入', () => {
const ds = new DataSource({
app: new App({}),
schema: {
type: 'base',
id: '1',
fields: [{ name: 'name' }],
methods: [],
events: [],
},
});
ds.setFields([{ name: 'foo' }] as any);
expect(ds.fields[0].name).toBe('foo');
ds.setMethods([{ name: 'doIt' } as any]);
expect(ds.methods[0].name).toBe('doIt');
(ds as any).setDataFromEvent({ params: { field: ['name'], data: 'X' } });
expect(ds.data.name).toBe('X');
});
test('destroy 清理 fields 与监听', () => {
const ds = new DataSource({
app: new App({}),
schema: {
type: 'base',
id: '1',
fields: [{ name: 'name' }],
methods: [],
events: [],
},
});
const handler = vi.fn();
ds.on('change', handler);
ds.destroy();
expect(ds.fields).toHaveLength(0);
ds.emit('change', {});
expect(handler).not.toHaveBeenCalled();
});
});

View File

@ -1,8 +1,15 @@
import { afterAll, describe, expect, test } from 'vitest';
import { afterAll, afterEach, describe, expect, test, vi } from 'vitest';
import TMagicApp, { NodeType } from '@tmagic/core';
import TMagicApp, {
type MApp,
NODE_CONDS_KEY,
NODE_CONDS_RESULT_KEY,
NODE_DISABLE_DATA_SOURCE_KEY,
NodeType,
} from '@tmagic/core';
import { DataSource, DataSourceManager } from '@data-source/index';
import { SimpleObservedData } from '@data-source/observed-data/SimpleObservedData';
const app = new TMagicApp({
config: {
@ -93,3 +100,628 @@ describe('DataSourceManager', () => {
expect(dsm.get('1')).toBeInstanceOf(DataSource);
});
});
describe('DataSourceManager - 注册 / 等待 / observedData', () => {
test('register 注册新的数据源类', () => {
class Custom extends DataSource {}
DataSourceManager.register('custom-1', Custom as any);
expect(DataSourceManager.getDataSourceClass('custom-1')).toBe(Custom);
DataSourceManager.clearDataSourceClass();
expect(DataSourceManager.getDataSourceClass('custom-1')).toBeUndefined();
});
test('initialData 在构造时被合并到 data', () => {
const dsm = new DataSourceManager({
app: new TMagicApp({}),
initialData: { 1: { name: 'preset' } },
});
expect(dsm.data['1']).toEqual({ name: 'preset' });
expect(dsm.initialData['1']).toEqual({ name: 'preset' });
});
test('useMock 可被读取', () => {
const dsm = new DataSourceManager({ app: new TMagicApp({}), useMock: true });
expect(dsm.useMock).toBe(true);
});
test('registerObservedData 静态方法', () => {
class Fake {}
expect(() => DataSourceManager.registerObservedData(Fake as any)).not.toThrow();
// 用完恢复,避免污染后续用例
DataSourceManager.registerObservedData(SimpleObservedData);
});
});
describe('DataSourceManager - init 生命周期', () => {
afterEach(() => {
DataSourceManager.clearDataSourceClass();
});
const createApp = (jsEngine?: any) =>
new TMagicApp({
// jsEngine 选填,用于走 init 中的 jsEngine 分支
...(jsEngine ? { jsEngine } : {}),
config: {
type: NodeType.ROOT,
id: 'app_init',
items: [],
},
} as any);
test('ds.isInit 为 true 时直接跳过', async () => {
const dsm = new DataSourceManager({ app: createApp() });
const ds = new DataSource({
app: createApp(),
schema: { type: 'base', id: 'ds_skip', fields: [], methods: [], events: [] },
});
ds.isInit = true;
await dsm.init(ds);
// isInit 仍为 true且没有抛错
expect(ds.isInit).toBe(true);
});
test('jsEngine 命中 disabledInitInJsEngine 时跳过 init', async () => {
const app = createApp('nodejs');
const dsm = new DataSourceManager({ app });
const ds = new DataSource({
app,
schema: {
type: 'base',
id: 'ds_disabled',
fields: [],
methods: [],
events: [],
disabledInitInJsEngine: ['nodejs'],
} as any,
});
expect(ds.isInit).toBe(false);
await dsm.init(ds);
expect(ds.isInit).toBe(false);
});
test('methods 中 timing=beforeInit 的 content 会在 ds.init 之前调用', async () => {
const app = createApp();
const dsm = new DataSourceManager({ app });
const beforeContent = vi.fn();
const ds = new DataSource({
app,
schema: {
type: 'base',
id: 'ds_before',
fields: [],
events: [],
methods: [{ name: 'before', content: beforeContent, timing: 'beforeInit', params: [] }],
} as any,
});
await dsm.init(ds);
expect(beforeContent).toHaveBeenCalledTimes(1);
const arg = beforeContent.mock.calls[0][0];
expect(arg.dataSource).toBe(ds);
expect(arg.app).toBe(app);
expect(ds.isInit).toBe(true);
});
test('methods 中 timing=afterInit 的 content 会在 ds.init 之后调用', async () => {
const app = createApp();
const dsm = new DataSourceManager({ app });
const order: string[] = [];
const afterContent = vi.fn(() => {
order.push('after');
});
const ds = new DataSource({
app,
schema: {
type: 'base',
id: 'ds_after',
fields: [],
events: [],
methods: [{ name: 'after', content: afterContent, timing: 'afterInit', params: [] }],
} as any,
});
const origInit = ds.init.bind(ds);
ds.init = async () => {
order.push('init');
await origInit();
};
await dsm.init(ds);
expect(afterContent).toHaveBeenCalledTimes(1);
expect(order).toEqual(['init', 'after']);
});
test('method.content 非函数时 init 提前返回,不会执行 ds.init', async () => {
const app = createApp();
const dsm = new DataSourceManager({ app });
const ds = new DataSource({
app,
schema: {
type: 'base',
id: 'ds_bad_method',
fields: [],
events: [],
methods: [{ name: 'bad', content: 'not-a-function', timing: 'beforeInit', params: [] } as any],
} as any,
});
const initSpy = vi.spyOn(ds, 'init');
await dsm.init(ds);
expect(initSpy).not.toHaveBeenCalled();
expect(ds.isInit).toBe(false);
});
test('afterInit 阶段遇到非函数 content 也会提前返回', async () => {
const app = createApp();
const dsm = new DataSourceManager({ app });
const afterFn = vi.fn();
const ds = new DataSource({
app,
schema: {
type: 'base',
id: 'ds_after_bad',
fields: [],
events: [],
methods: [{ name: 'before', content: () => undefined, timing: 'beforeInit', params: [] } as any],
} as any,
});
// ds.init 执行之后再向 methods 中追加一个 content 非函数的 afterInit 项
const origInit = ds.init.bind(ds);
ds.init = async () => {
await origInit();
ds.setMethods([
{ name: 'bad', content: 'not-a-function', timing: 'afterInit', params: [] } as any,
{ name: 'after', content: afterFn, timing: 'afterInit', params: [] } as any,
]);
};
await dsm.init(ds);
// 第二个循环在第一个非函数 content 处提前返回afterFn 不会被调用
expect(afterFn).not.toHaveBeenCalled();
expect(ds.isInit).toBe(true);
});
test('beforeInit / afterInit 同时存在但 timing 不匹配时安全跳过', async () => {
const app = createApp();
const dsm = new DataSourceManager({ app });
const beforeFn = vi.fn();
const afterFn = vi.fn();
const ds = new DataSource({
app,
schema: {
type: 'base',
id: 'ds_mixed',
fields: [],
events: [],
methods: [
{ name: 'b', content: beforeFn, timing: 'beforeInit', params: [] } as any,
{ name: 'a', content: afterFn, timing: 'afterInit', params: [] } as any,
],
} as any,
});
await dsm.init(ds);
expect(beforeFn).toHaveBeenCalledTimes(1);
expect(afterFn).toHaveBeenCalledTimes(1);
});
});
describe('DataSourceManager - addDataSource 边界', () => {
afterEach(() => {
DataSourceManager.clearDataSourceClass();
});
test('config 为空时直接返回 undefined', () => {
const dsm = new DataSourceManager({ app: new TMagicApp({}) });
expect(dsm.addDataSource(undefined)).toBeUndefined();
});
test('destroy 后 waitInitSchemaList 为空,再次加入未知类型会重建 listMap', () => {
const dsm = new DataSourceManager({ app: new TMagicApp({}) });
dsm.destroy();
const ret = dsm.addDataSource({
id: 'ds_unknown_after_destroy',
type: 'never-registered',
fields: [{ name: 'a', defaultValue: 1 }],
methods: [],
events: [],
} as any);
expect(ret).toBeUndefined();
expect(dsm.data.ds_unknown_after_destroy).toEqual({ a: 1 });
});
test('多次加入同一未知类型会推到等待列表', () => {
const dsm = new DataSourceManager({ app: new TMagicApp({}) });
dsm.addDataSource({
id: 'pending_1',
type: 'pending-shared',
fields: [],
methods: [],
events: [],
} as any);
dsm.addDataSource({
id: 'pending_2',
type: 'pending-shared',
fields: [],
methods: [],
events: [],
} as any);
class SharedDS extends DataSource {}
DataSourceManager.register('pending-shared', SharedDS as any);
expect(dsm.get('pending_1')).toBeInstanceOf(SharedDS);
expect(dsm.get('pending_2')).toBeInstanceOf(SharedDS);
});
});
describe('DataSourceManager - updateSchema 边界', () => {
afterEach(() => {
DataSourceManager.clearDataSourceClass();
});
test('传入的 schema 在 manager 中不存在时直接 return', () => {
const dsm = new DataSourceManager({
app: new TMagicApp({
config: {
type: NodeType.ROOT,
id: 'app_us',
items: [],
dataSources: [{ type: 'base', id: 'real', fields: [{ name: 'a' }], methods: [], events: [] }],
},
}),
});
expect(dsm.get('real')).toBeInstanceOf(DataSource);
dsm.updateSchema([
{ type: 'base', id: 'not_exist', fields: [{ name: 'b' }], methods: [], events: [] },
{ type: 'base', id: 'real', fields: [{ name: 'a' }], methods: [], events: [] },
]);
// real 没有被删除/重建(因为遇到 not_exist 时整个 updateSchema 提前 return
expect(dsm.get('real')).toBeInstanceOf(DataSource);
});
test('updateSchema 中新 type 未注册时不会调用 init', () => {
const dsm = new DataSourceManager({
app: new TMagicApp({
config: {
type: NodeType.ROOT,
id: 'app_us2',
items: [],
dataSources: [{ type: 'base', id: 'X', fields: [], methods: [], events: [] }],
},
}),
});
expect(dsm.get('X')).toBeInstanceOf(DataSource);
dsm.updateSchema([{ type: 'never-registered', id: 'X', fields: [], methods: [], events: [] } as any]);
expect(dsm.get('X')).toBeUndefined();
});
});
describe('DataSourceManager - compiledNode 边界', () => {
afterEach(() => {
DataSourceManager.clearDataSourceClass();
});
const createManager = () =>
new DataSourceManager({
app: new TMagicApp({
config: {
type: NodeType.ROOT,
id: 'app_cn',
items: [],
dataSources: [
{
type: 'base',
id: 'ds_cn',
fields: [{ name: 'val', defaultValue: 'V' }],
methods: [],
events: [],
},
],
dataSourceDeps: {
ds_cn: {
text_a: { name: 'text', keys: ['text'] },
},
} as any,
},
}),
});
test('节点带 NODE_DISABLE_DATA_SOURCE_KEY 时直接返回原节点', () => {
const dsm = createManager();
const node: any = {
id: 'text_a',
type: 'text',
text: 'hello ${ds_cn.val}',
[NODE_DISABLE_DATA_SOURCE_KEY]: true,
};
expect(dsm.compiledNode(node)).toBe(node);
});
test('deep=true 时数组 items 会递归编译', () => {
const dsm = createManager();
const node: any = {
id: 'wrap',
type: 'container',
items: [{ id: 'text_a', type: 'text', text: 'hi ${ds_cn.val}' }],
};
const compiled: any = dsm.compiledNode(node, undefined, true);
expect(compiled.items[0].text).toBe('hi V');
});
test('deep=false 时 items 保持原样', () => {
const dsm = createManager();
const items = [{ id: 'text_a', type: 'text', text: 'hi ${ds_cn.val}' }];
const node: any = { id: 'wrap', type: 'container', items };
const compiled: any = dsm.compiledNode(node);
expect(compiled.items).toBe(items);
});
test('节点 condResult=false 时跳过模板编译', () => {
const dsm = createManager();
const node: any = {
id: 'text_a',
type: 'text',
text: 'hi ${ds_cn.val}',
condResult: false,
};
const compiled: any = dsm.compiledNode(node);
expect(compiled.text).toBe('hi ${ds_cn.val}');
});
test('condResult=undefined 且 NODE_CONDS_RESULT_KEY=true 时也跳过模板编译', () => {
const dsm = createManager();
const node: any = {
id: 'text_a',
type: 'text',
text: 'hi ${ds_cn.val}',
[NODE_CONDS_RESULT_KEY]: true,
};
const compiled: any = dsm.compiledNode(node);
expect(compiled.text).toBe('hi ${ds_cn.val}');
});
test('dsl.dataSourceDeps 缺失时使用空依赖对象', () => {
const app = new TMagicApp({
config: {
type: NodeType.ROOT,
id: 'app_no_deps',
items: [],
dataSources: [
{ type: 'base', id: 'ds_nd', fields: [{ name: 'v', defaultValue: 'V' }], methods: [], events: [] },
],
},
});
expect(app.dsl?.dataSourceDeps).toBeUndefined();
const dsm = new DataSourceManager({ app });
const node: any = { id: 'p', type: 'text', text: 'hi' };
const compiled = dsm.compiledNode(node) as any;
expect(compiled.text).toBe('hi');
});
});
describe('DataSourceManager - compliedConds 边界', () => {
afterEach(() => {
DataSourceManager.clearDataSourceClass();
});
test('NODE_DISABLE_DATA_SOURCE_KEY=true 时直接返回 true', () => {
const dsm = new DataSourceManager({ app: new TMagicApp({}) });
expect(
dsm.compliedConds({
[NODE_DISABLE_DATA_SOURCE_KEY]: true,
[NODE_CONDS_KEY]: [{ cond: [{ field: ['ds_1', 'a'], op: '=', value: 1 }] }] as any,
}),
).toBe(true);
});
test('NODE_CONDS_RESULT_KEY 为真时会对条件结果取反', () => {
const dsm = new DataSourceManager({ app: new TMagicApp({}) });
dsm.data.ds_x = { a: 1 };
// 条件成立 -> compliedConditions 返回 true再取反应为 false
expect(
dsm.compliedConds({
[NODE_CONDS_KEY]: [{ cond: [{ field: ['ds_x', 'a'], op: '=', value: 1 }] }] as any,
[NODE_CONDS_RESULT_KEY]: true,
}),
).toBe(false);
});
});
describe('DataSourceManager - 迭代器相关方法', () => {
afterEach(() => {
DataSourceManager.clearDataSourceClass();
});
const createManager = () =>
new DataSourceManager({
app: new TMagicApp({
config: {
type: NodeType.ROOT,
id: 'app_iter',
items: [],
dataSources: [
{
type: 'base',
id: 'ds_iter',
fields: [
{
name: 'list',
type: 'array',
fields: [{ name: 'label' }],
defaultValue: [{ label: 'A' }],
},
],
methods: [],
events: [],
},
],
},
}),
});
test('compliedIteratorItemConds: dataSourceField 指向未知数据源时返回 true', () => {
const dsm = createManager();
const result = dsm.compliedIteratorItemConds(
{ label: 'x' },
{ [NODE_CONDS_KEY]: [{ cond: [{ field: ['ds_iter', 'list', 'label'], op: '=', value: 'x' }] }] } as any,
['no_such_ds', 'list'],
);
expect(result).toBe(true);
});
test('compliedIteratorItemConds: 使用迭代上下文计算条件', () => {
const dsm = createManager();
const node: any = {
[NODE_CONDS_KEY]: [{ cond: [{ field: ['ds_iter', 'list', 'label'], op: '=', value: 'B' }] }],
};
expect(dsm.compliedIteratorItemConds({ label: 'B' }, node, ['ds_iter', 'list'])).toBe(true);
expect(dsm.compliedIteratorItemConds({ label: 'A' }, node, ['ds_iter', 'list'])).toBe(false);
});
test('compliedIteratorItems: 未知数据源时原样返回 nodes', () => {
const dsm = createManager();
const nodes: any = [{ id: 'iter_1', type: 'text', text: '${ds_iter.list.label}' }];
expect(dsm.compliedIteratorItems({ label: 'B' }, nodes, ['no_such_ds'])).toBe(nodes);
});
test('compliedIteratorItems: 无 deps / condDeps 时原样返回 nodes', () => {
const dsm = createManager();
const nodes: any = [{ id: 'plain', type: 'text', text: 'plain' }];
expect(dsm.compliedIteratorItems({ label: 'B' }, nodes, ['ds_iter', 'list'])).toBe(nodes);
});
test('compliedIteratorItems: 命中 deps 时按迭代上下文进行编译', () => {
const dsm = createManager();
const nodes: any = [{ id: 'iter_text', type: 'text', text: 'hello ${ds_iter.list.label}' }];
const compiled = dsm.compliedIteratorItems({ label: 'B' }, nodes, ['ds_iter', 'list']);
expect(compiled[0]).not.toBe(nodes[0]);
expect((compiled[0] as any).text).toBe('hello B');
});
});
describe('DataSourceManager - onDataChange / offDataChange', () => {
afterEach(() => {
DataSourceManager.clearDataSourceClass();
});
test('onDataChange / offDataChange 转发到对应数据源', () => {
const dsm = new DataSourceManager({
app: new TMagicApp({
config: {
type: NodeType.ROOT,
id: 'app_odc',
items: [],
dataSources: [{ type: 'base', id: 'ds_odc', fields: [{ name: 'name' }], methods: [], events: [] }],
},
}),
});
const callback = vi.fn();
dsm.onDataChange('ds_odc', 'name', callback);
const ds = dsm.get('ds_odc')!;
ds.setData('A', 'name');
expect(callback).toHaveBeenCalledTimes(1);
dsm.offDataChange('ds_odc', 'name', callback);
ds.setData('B', 'name');
expect(callback).toHaveBeenCalledTimes(1);
});
test('数据源不存在时 onDataChange / offDataChange 安全返回 undefined', () => {
const dsm = new DataSourceManager({ app: new TMagicApp({}) });
const callback = vi.fn();
expect(dsm.onDataChange('no_id', 'a', callback)).toBeUndefined();
expect(dsm.offDataChange('no_id', 'a', callback)).toBeUndefined();
});
});
describe('DataSourceManager - callDsInit 异常 / 兼容分支', () => {
afterEach(() => {
DataSourceManager.clearDataSourceClass();
vi.restoreAllMocks();
});
const buildConfig = (id: string): MApp => ({
type: NodeType.ROOT,
id,
items: [],
dataSources: [
{ type: 'base', id: 'ds_ok', fields: [{ name: 'a', defaultValue: 1 }], methods: [], events: [] },
{ type: 'base', id: 'ds_err', fields: [{ name: 'b', defaultValue: 2 }], methods: [], events: [] },
],
});
test('init 完成但 this.data[dsId] 为空时走 delete 分支', async () => {
const app = new TMagicApp({ config: buildConfig('app_empty_data') });
const dsm = new DataSourceManager({ app });
// 在 Promise.allSettled 的 .then() 微任务执行之前把 data 清空
dsm.data = {} as any;
const [data, errors] = await new Promise<any[]>((resolve) => {
dsm.once('init', (...args: any[]) => resolve(args));
});
// 由于 this.data[dsId] 为空data 中也不会包含对应 dsId
expect(data.ds_ok).toBeUndefined();
expect(data.ds_err).toBeUndefined();
expect(Object.keys(errors)).toHaveLength(0);
});
test('init 抛错时通过 Promise.allSettled 的 rejected 分支收集 errors', async () => {
const initSpy = vi.spyOn(DataSource.prototype, 'init').mockImplementation(async function (this: DataSource) {
if (this.id === 'ds_err') {
throw new Error('boom');
}
// ok 路径
(this as any).isInit = true;
});
const app = new TMagicApp({ config: buildConfig('app_err') });
const dsm = new DataSourceManager({ app });
const [data, errors] = await new Promise<any[]>((resolve) => {
dsm.once('init', (...args: any[]) => resolve(args));
});
expect(data.ds_ok).toEqual({ a: 1 });
expect(data.ds_err).toBeUndefined();
expect(errors.ds_err).toBeInstanceOf(Error);
expect(errors.ds_err.message).toBe('boom');
initSpy.mockRestore();
});
test('Promise.allSettled 不可用时走 Promise.all 兼容分支并发出 init 事件', async () => {
const original = Promise.allSettled;
(Promise as any).allSettled = undefined;
try {
const app = new TMagicApp({ config: buildConfig('app_compat') });
const dsm = new DataSourceManager({ app });
await new Promise<void>((resolve) => {
dsm.once('init', () => resolve());
});
expect(dsm.data.ds_ok).toEqual({ a: 1 });
expect(dsm.data.ds_err).toEqual({ b: 2 });
} finally {
(Promise as any).allSettled = original;
}
});
test('Promise.allSettled 不可用且 init 抛错时进入 catch 分支', async () => {
const original = Promise.allSettled;
(Promise as any).allSettled = undefined;
const initSpy = vi.spyOn(DataSource.prototype, 'init').mockRejectedValue(new Error('compat-boom'));
try {
const app = new TMagicApp({ config: buildConfig('app_compat_err') });
const dsm = new DataSourceManager({ app });
// 在兼容路径下catch 分支也会发 init 事件
const data = await new Promise<any>((resolve) => {
dsm.once('init', (...args: any[]) => resolve(args[0]));
});
expect(data).toBeDefined();
} finally {
(Promise as any).allSettled = original;
initSpy.mockRestore();
}
});
});

View File

@ -0,0 +1,237 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
*/
import { describe, expect, test, vi } from 'vitest';
import App from '@tmagic/core';
import { HttpDataSource } from '@data-source/data-sources';
const createSchema = (overrides: Partial<any> = {}) => ({
type: 'http',
id: 'http_1',
fields: [{ name: 'name' }],
methods: [],
events: [],
options: {
url: 'https://example.com/api',
method: 'GET',
params: {},
data: {},
headers: {},
},
...overrides,
});
describe('HttpDataSource 基础', () => {
test('实例化时记录 httpOptions / type', () => {
const ds = new HttpDataSource({
schema: createSchema() as any,
app: new App({}),
});
expect(ds).toBeInstanceOf(HttpDataSource);
expect(ds.type).toBe('http');
expect(ds.httpOptions.url).toBe('https://example.com/api');
});
test('优先使用自定义 request', async () => {
const request = vi.fn().mockResolvedValue({ name: 'from-request' });
const ds = new HttpDataSource({
schema: createSchema() as any,
app: new App({}),
request,
});
await ds.request();
expect(request).toHaveBeenCalled();
expect(ds.data.name).toBe('from-request');
expect(ds.error).toBeUndefined();
});
test('autoFetch=true 在 init 时主动请求', async () => {
const request = vi.fn().mockResolvedValue({ name: 'auto' });
const ds = new HttpDataSource({
schema: createSchema({ autoFetch: true }) as any,
app: new App({}),
request,
});
await ds.init();
expect(request).toHaveBeenCalledTimes(1);
expect(ds.isInit).toBe(true);
});
test('beforeRequest / afterResponse 钩子被调用', async () => {
const beforeRequest = vi.fn(async (opt: any) => ({ ...opt, params: { extra: 1 } }));
const afterResponse = vi.fn(async (res: any) => ({ ...res, name: 'after' }));
const request = vi.fn().mockResolvedValue({ name: 'origin' });
const ds = new HttpDataSource({
schema: createSchema({ beforeRequest, afterResponse }) as any,
app: new App({}),
request,
});
await ds.request();
expect(beforeRequest).toHaveBeenCalled();
expect(afterResponse).toHaveBeenCalled();
expect(ds.data.name).toBe('after');
});
test('responseOptions.dataPath 截取响应字段', async () => {
const request = vi.fn().mockResolvedValue({ data: { name: 'inner' } });
const ds = new HttpDataSource({
schema: createSchema({ responseOptions: { dataPath: 'data' } }) as any,
app: new App({}),
request,
});
await ds.request();
expect(ds.data.name).toBe('inner');
});
test('请求失败时填充 error 并触发 error 事件', async () => {
const request = vi.fn().mockRejectedValue(new Error('boom'));
const ds = new HttpDataSource({
schema: createSchema() as any,
app: new App({}),
request,
});
const errorHandler = vi.fn();
ds.on('error', errorHandler);
await ds.request();
expect(ds.isLoading).toBe(false);
expect(ds.error?.msg).toBe('boom');
expect(errorHandler).toHaveBeenCalled();
});
test('GET / POST 包装方法', async () => {
const request = vi.fn().mockResolvedValue({ name: 'ok' });
const ds = new HttpDataSource({
schema: createSchema() as any,
app: new App({}),
request,
});
await ds.get({ url: 'https://x.com/g' });
expect(request.mock.calls[0][0].method).toBe('GET');
await ds.post({ url: 'https://x.com/p' });
expect(request.mock.calls[1][0].method).toBe('POST');
});
test('options 中 url/params 等可以是函数', async () => {
const request = vi.fn().mockResolvedValue({});
const ds = new HttpDataSource({
schema: createSchema({
options: {
url: ({ dataSource }: any) => `https://x/${dataSource.id}`,
params: () => ({ p: 1 }),
data: () => ({ d: 1 }),
headers: () => ({ 'X-Custom': '1' }),
},
}) as any,
app: new App({}),
request,
});
await ds.request();
const opt = request.mock.calls[0][0];
expect(opt.url).toBe('https://x/http_1');
expect(opt.params).toEqual({ p: 1 });
expect(opt.data).toEqual({ d: 1 });
expect(opt.headers).toEqual({ 'X-Custom': '1' });
});
test('编辑器中使用 mockData 而非真实请求', async () => {
const request = vi.fn();
const app = new App({}) as any;
app.platform = 'editor';
const ds = new HttpDataSource({
schema: createSchema({
mocks: [{ useInEditor: true, data: { name: 'mock-name' } }],
}) as any,
app,
request,
});
await ds.request();
expect(request).not.toHaveBeenCalled();
expect(ds.data.name).toBe('mock-name');
});
test('beforeRequest/afterRequest method 被注册', async () => {
const before = vi.fn();
const after = vi.fn();
const request = vi.fn().mockResolvedValue({});
const ds = new HttpDataSource({
schema: createSchema({
methods: [
{ name: 'b', timing: 'beforeRequest', content: before, params: [] },
{ name: 'a', timing: 'afterRequest', content: after, params: [] },
{ name: 'noop', content: 'not-a-function' as any, params: [] },
],
}) as any,
app: new App({}),
request,
});
await ds.request();
expect(before).toHaveBeenCalled();
expect(after).toHaveBeenCalled();
});
});
describe('webRequest 默认实现', () => {
test('未传自定义 request 时使用 fetch非 GET 携带 body', async () => {
const fetchMock = vi.fn().mockResolvedValue({
json: async () => ({ name: 'fetched' }),
});
const original = globalThis.fetch;
(globalThis as any).fetch = fetchMock;
try {
const ds = new HttpDataSource({
schema: createSchema({
options: {
url: 'https://x.com/api',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: { foo: 'bar' },
params: { q: 'v' },
},
}) as any,
app: new App({}),
});
await ds.request();
expect(fetchMock).toHaveBeenCalledTimes(1);
const [url, init] = fetchMock.mock.calls[0];
expect(url).toContain('q=v');
expect(init.method).toBe('POST');
expect(init.body).toContain('foo');
expect(ds.data.name).toBe('fetched');
} finally {
(globalThis as any).fetch = original;
}
});
test('Content-Type 为 form-urlencoded 时 body 用 url 编码', async () => {
const fetchMock = vi.fn().mockResolvedValue({ json: async () => ({}) });
const original = globalThis.fetch;
(globalThis as any).fetch = fetchMock;
try {
const ds = new HttpDataSource({
schema: createSchema({
options: {
url: 'https://x.com/api',
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: { a: 1, b: { x: 1 }, c: undefined },
},
}) as any,
app: new App({}),
});
await ds.request();
const [, init] = fetchMock.mock.calls[0];
expect(init.body).toContain('a=1');
expect(init.body).toContain('b=');
expect(init.body).not.toContain('c=');
} finally {
(globalThis as any).fetch = original;
}
});
});

View File

@ -0,0 +1,87 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import { describe, expect, test, vi } from 'vitest';
import { DeepObservedData, SimpleObservedData } from '@data-source/observed-data';
describe('SimpleObservedData', () => {
test('update / getData 全量与按路径', () => {
const od = new SimpleObservedData({ a: 1, b: { c: 2 } });
expect(od.getData('')).toEqual({ a: 1, b: { c: 2 } });
expect(od.getData('b.c')).toBe(2);
od.update({ a: 9 });
expect(od.data.a).toBe(9);
od.update(99, 'a');
expect(od.data.a).toBe(99);
});
test('on / off 监听变更, immediate 立即触发一次', () => {
const od = new SimpleObservedData({ a: 1 });
const cb = vi.fn();
od.on('a', cb, { immediate: true });
expect(cb).toHaveBeenCalledTimes(1);
od.update(2, 'a');
expect(cb).toHaveBeenCalledTimes(2);
od.off('a', cb);
od.update(3, 'a');
expect(cb).toHaveBeenCalledTimes(2);
});
test('全量更新触发空 path 监听器', () => {
const od = new SimpleObservedData({ a: 1 });
const cb = vi.fn();
od.on('', cb);
od.update({ a: 2 });
expect(cb).toHaveBeenCalled();
});
test('destroy 不抛错', () => {
const od = new SimpleObservedData({});
expect(() => od.destroy()).not.toThrow();
});
});
describe('DeepObservedData', () => {
test('on/update/off/getData 完整链路', () => {
const od = new DeepObservedData({ a: 1, list: [{ name: 'x' }] });
const cb = vi.fn();
od.on('a', cb);
od.update(2, 'a');
expect(cb).toHaveBeenCalled();
expect(od.getData('a')).toBe(2);
od.off('a', cb);
cb.mockClear();
od.update(3, 'a');
expect(cb).not.toHaveBeenCalled();
});
test('immediate 选项立刻触发一次回调', () => {
const od = new DeepObservedData({ a: 1 });
const cb = vi.fn();
od.on('a', cb, { immediate: true });
expect(cb).toHaveBeenCalled();
});
test('off 不存在的 callback 不抛错', () => {
const od = new DeepObservedData({ a: 1 });
expect(() => od.off('a', () => undefined)).not.toThrow();
expect(() => od.off('not-exist', () => undefined)).not.toThrow();
});
test('destroy 解除所有监听', () => {
const od = new DeepObservedData({ a: 1 });
const cb = vi.fn();
od.on('a', cb);
od.destroy();
od.update(2, 'a');
expect(cb).not.toHaveBeenCalled();
});
});

View File

@ -1,10 +1,10 @@
import { describe, expect, test } from 'vitest';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import TMagicApp, { type MApp, NodeType } from '@tmagic/core';
import { createDataSourceManager, DataSourceManager } from '@data-source/index';
import { createDataSourceManager, DataSource, DataSourceManager } from '@data-source/index';
const dsl: MApp = {
const createDsl = (): MApp => ({
type: NodeType.ROOT,
id: 'app_1',
items: [
@ -41,13 +41,803 @@ const dsl: MApp = {
events: [],
},
],
};
});
describe('createDataSourceManager', () => {
afterEach(() => {
DataSourceManager.clearDataSourceClass();
});
describe('createDataSourceManager - 基础', () => {
test('instance', () => {
const manager = createDataSourceManager(new TMagicApp({ config: dsl }));
const manager = createDataSourceManager(new TMagicApp({ config: createDsl() }));
expect(manager).toBeInstanceOf(DataSourceManager);
});
DataSourceManager.clearDataSourceClass();
test('dsl 中没有 dataSources 时返回 undefined', () => {
const app = new TMagicApp({
config: {
type: NodeType.ROOT,
id: 'app_no_ds',
items: [],
},
});
const manager = createDataSourceManager(app);
expect(manager).toBeUndefined();
});
test('app 没有 dsl 时返回 undefined', () => {
const app = new TMagicApp({});
const manager = createDataSourceManager(app);
expect(manager).toBeUndefined();
});
test('useMock 透传到 DataSourceManager', () => {
const manager = createDataSourceManager(new TMagicApp({ config: createDsl() }), true);
expect(manager?.useMock).toBe(true);
});
test('initialData 透传到 DataSourceManager', () => {
const manager = createDataSourceManager(new TMagicApp({ config: createDsl() }), false, {
ds_bebcb2d5: { text: 'preset' },
});
expect(manager?.initialData.ds_bebcb2d5).toEqual({ text: 'preset' });
expect(manager?.data.ds_bebcb2d5.text).toBe('preset');
});
});
describe('createDataSourceManager - 初始化阶段编译', () => {
test('platform!=editor && 存在 dataSourceCondDeps 时按节点写入 condResult', () => {
const dsl: MApp = {
type: NodeType.ROOT,
id: 'app_cond',
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
type: 'text',
id: 'cond_node',
text: 'hello',
displayConds: [{ cond: [{ field: ['ds_1', 'a'], op: '=', value: 1 }] }],
} as any,
],
},
],
dataSourceCondDeps: {
ds_1: {
cond_node: { name: '文本', keys: ['displayConds'] },
},
},
dataSourceDeps: {},
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [{ name: 'a', defaultValue: 1 }],
methods: [],
events: [],
},
],
};
const app = new TMagicApp({ config: dsl, platform: 'mobile' });
createDataSourceManager(app);
const node: any = (app.dsl?.items[0] as any).items[0];
expect(node.condResult).toBe(true);
});
test('platform=editor 时初始化不写入 condResult', () => {
const dsl: MApp = {
type: NodeType.ROOT,
id: 'app_cond_editor',
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
type: 'text',
id: 'cond_node',
text: 'hello',
displayConds: [{ cond: [{ field: ['ds_1', 'a'], op: '=', value: 1 }] }],
} as any,
],
},
],
dataSourceCondDeps: {
ds_1: {
cond_node: { name: '文本', keys: ['displayConds'] },
},
},
dataSourceDeps: {},
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [{ name: 'a', defaultValue: 1 }],
methods: [],
events: [],
},
],
};
const app = new TMagicApp({ config: dsl, platform: 'editor' });
createDataSourceManager(app);
const node: any = (app.dsl?.items[0] as any).items[0];
expect(node.condResult).toBeUndefined();
});
test('存在 dataSourceDeps 时初始化即编译节点字段(模板)', () => {
const dsl: MApp = {
type: NodeType.ROOT,
id: 'app_dep',
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
type: 'text',
id: 'dep_node',
text: 'hello ${ds_1.name}',
} as any,
],
},
],
dataSourceDeps: {
ds_1: {
dep_node: { name: '文本', keys: ['text'] },
},
},
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [{ name: 'name', defaultValue: 'world' }],
methods: [],
events: [],
},
],
};
const app = new TMagicApp({ config: dsl, platform: 'mobile' });
createDataSourceManager(app);
const node: any = (app.dsl?.items[0] as any).items[0];
expect(node.text).toBe('hello world');
});
});
describe('createDataSourceManager - jsEngine=nodejs', () => {
test('nodejs 环境下不监听 change触发 setData 不会走 update-data', () => {
const app = new TMagicApp({ config: createDsl(), jsEngine: 'nodejs' });
const manager = createDataSourceManager(app);
expect(manager).toBeInstanceOf(DataSourceManager);
expect(manager?.listenerCount('change')).toBe(0);
const updateSpy = vi.fn();
manager?.on('update-data', updateSpy);
const ds = manager?.get('ds_bebcb2d5');
ds?.setData({ text: 'changed' });
expect(updateSpy).not.toHaveBeenCalled();
});
});
describe('createDataSourceManager - change 事件', () => {
let app: TMagicApp;
let manager: DataSourceManager | undefined;
beforeEach(() => {
const dsl: MApp = {
type: NodeType.ROOT,
id: 'app_change',
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
type: 'text',
id: 'text_1',
text: 'origin ${ds_1.name}',
} as any,
],
},
],
dataSourceDeps: {
ds_1: {
text_1: { name: '文本', keys: ['text'] },
},
},
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [{ name: 'name', defaultValue: 'world' }],
methods: [],
events: [],
},
],
};
app = new TMagicApp({ config: dsl, platform: 'mobile' });
manager = createDataSourceManager(app);
});
test('change 事件触发后会发出 update-data并携带新节点 / sourceId / pageId', () => {
const update = vi.fn();
manager?.on('update-data', update);
const ds = manager?.get('ds_1');
ds?.setData({ name: 'new' });
expect(update).toHaveBeenCalledTimes(1);
const [newNodes, sourceId, , pageId] = update.mock.calls[0];
expect(sourceId).toBe('ds_1');
expect(pageId).toBe('page_1');
expect(newNodes[0].id).toBe('text_1');
expect(newNodes[0].text).toBe('origin new');
});
test('change 事件会调用 page.setData 并触发节点 setData', () => {
const node = app.getNode('text_1');
const setDataSpy = vi.spyOn(node!, 'setData');
const ds = manager?.get('ds_1');
ds?.setData({ name: 'second' });
expect(setDataSpy).toHaveBeenCalled();
const calledArg = setDataSpy.mock.calls[0][0] as any;
expect(calledArg.text).toBe('origin second');
});
test('依赖中的节点不存在时不会发出 update-data', () => {
const update = vi.fn();
manager?.on('update-data', update);
if (app.dsl?.dataSourceDeps) {
app.dsl.dataSourceDeps = {};
}
const ds = manager?.get('ds_1');
ds?.setData({ name: 'noop' });
expect(update).not.toHaveBeenCalled();
});
test('page 自身被命中时调用 app.page.setData', () => {
// 把 page 自己加入到依赖中
if (app.dsl?.dataSourceDeps) {
app.dsl.dataSourceDeps.ds_1 = {
page_1: { name: 'page', keys: ['style'] },
} as any;
}
const pageSetData = vi.spyOn(app.page!, 'setData');
const ds = manager?.get('ds_1');
ds?.setData({ name: 'X' });
expect(pageSetData).toHaveBeenCalled();
const arg: any = pageSetData.mock.calls[0][0];
expect(arg.id).toBe('page_1');
});
test('page 没有 instance 时通过 replaceChildNode 写回 page.data', () => {
const ds = manager?.get('ds_1');
expect(app.page?.instance).toBeFalsy();
ds?.setData({ name: 'replaced' });
const replacedText = (app.page?.data as any).items[0].text;
expect(replacedText).toBe('origin replaced');
});
});
describe('createDataSourceManager - editor 平台', () => {
test('editor 平台会遍历所有页面,而非仅当前页', () => {
const dsl: MApp = {
type: NodeType.ROOT,
id: 'app_editor',
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [{ type: 'text', id: 'text_a', text: 'a ${ds_1.name}' } as any],
},
{
type: NodeType.PAGE,
id: 'page_2',
items: [{ type: 'text', id: 'text_b', text: 'b ${ds_1.name}' } as any],
},
],
dataSourceDeps: {
ds_1: {
text_a: { name: '文本', keys: ['text'] },
text_b: { name: '文本', keys: ['text'] },
},
},
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [{ name: 'name', defaultValue: 'init' }],
methods: [],
events: [],
},
],
};
const app = new TMagicApp({ config: dsl, platform: 'editor' });
const manager = createDataSourceManager(app);
const update = vi.fn();
manager?.on('update-data', update);
const ds = manager?.get('ds_1');
ds?.setData({ name: 'V' });
expect(update).toHaveBeenCalledTimes(2);
const pageIds = update.mock.calls.map((c) => c[3]);
expect(pageIds).toContain('page_1');
expect(pageIds).toContain('page_2');
});
test('非 editor 平台只处理当前页', () => {
const dsl: MApp = {
type: NodeType.ROOT,
id: 'app_runtime',
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [{ type: 'text', id: 'text_a', text: 'a ${ds_1.name}' } as any],
},
{
type: NodeType.PAGE,
id: 'page_2',
items: [{ type: 'text', id: 'text_b', text: 'b ${ds_1.name}' } as any],
},
],
dataSourceDeps: {
ds_1: {
text_a: { name: '文本', keys: ['text'] },
text_b: { name: '文本', keys: ['text'] },
},
},
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [{ name: 'name', defaultValue: 'init' }],
methods: [],
events: [],
},
],
};
const app = new TMagicApp({ config: dsl, platform: 'mobile', curPage: 'page_1' });
const manager = createDataSourceManager(app);
const update = vi.fn();
manager?.on('update-data', update);
const ds = manager?.get('ds_1');
ds?.setData({ name: 'V' });
expect(update).toHaveBeenCalledTimes(1);
expect(update.mock.calls[0][3]).toBe('page_1');
});
test('非 editor 平台命中 isPageFragment 分支也会被处理', () => {
const dsl: MApp = {
type: NodeType.ROOT,
id: 'app_pf',
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [{ type: 'text', id: 'text_a', text: 'a' } as any],
},
{
type: NodeType.PAGE_FRAGMENT,
id: 'pf_1',
items: [{ type: 'text', id: 'text_b', text: 'b ${ds_1.name}' } as any],
} as any,
],
dataSourceDeps: {
ds_1: {
text_b: { name: '文本', keys: ['text'] },
},
},
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [{ name: 'name', defaultValue: 'init' }],
methods: [],
events: [],
},
],
};
const app = new TMagicApp({ config: dsl, platform: 'mobile', curPage: 'page_1' });
const manager = createDataSourceManager(app);
const update = vi.fn();
manager?.on('update-data', update);
const ds = manager?.get('ds_1');
ds?.setData({ name: 'V' });
expect(update).toHaveBeenCalledTimes(1);
expect(update.mock.calls[0][3]).toBe('pf_1');
});
});
describe('createDataSourceManager - pageFragments 同步', () => {
test('当 newNode 为 pageFragment 自身时,调用 pageFragment.setData', () => {
const dsl: MApp = {
type: NodeType.ROOT,
id: 'app_pf_self',
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
type: 'page-fragment-container',
id: 'pfc_1',
pageFragmentId: 'pf_1',
items: [],
} as any,
],
},
{
type: NodeType.PAGE_FRAGMENT,
id: 'pf_1',
items: [{ type: 'text', id: 'pf_text', text: 'pf ${ds_1.name}' } as any],
extra: '${ds_1.name}',
} as any,
],
dataSourceDeps: {
ds_1: {
pf_1: { name: 'pf', keys: ['extra'] },
},
},
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [{ name: 'name', defaultValue: 'init' }],
methods: [],
events: [],
},
],
};
const app = new TMagicApp({ config: dsl, platform: 'editor', curPage: 'page_1' });
const manager = createDataSourceManager(app);
expect(app.pageFragments.size).toBeGreaterThan(0);
const pageFragment = app.pageFragments.get('pfc_1')!;
const pfSetData = vi.spyOn(pageFragment, 'setData');
const ds = manager?.get('ds_1');
ds?.setData({ name: 'X' });
expect(pfSetData).toHaveBeenCalled();
const arg: any = pfSetData.mock.calls[0][0];
expect(arg.id).toBe('pf_1');
});
test('当 newNode 是 pageFragment 内子节点时pageFragment 内同步并 replaceChildNode', () => {
const dsl: MApp = {
type: NodeType.ROOT,
id: 'app_pf_child',
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
type: 'page-fragment-container',
id: 'pfc_1',
pageFragmentId: 'pf_1',
items: [],
} as any,
],
},
{
type: NodeType.PAGE_FRAGMENT,
id: 'pf_1',
items: [{ type: 'text', id: 'pf_text', text: 'pf ${ds_1.name}' } as any],
} as any,
],
dataSourceDeps: {
ds_1: {
pf_text: { name: 'pf_text', keys: ['text'] },
},
},
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [{ name: 'name', defaultValue: 'init' }],
methods: [],
events: [],
},
],
};
const app = new TMagicApp({ config: dsl, platform: 'editor', curPage: 'page_1' });
const manager = createDataSourceManager(app);
const pageFragment = app.pageFragments.get('pfc_1')!;
const innerNode = pageFragment.getNode('pf_text', { strict: true })!;
const innerSetData = vi.spyOn(innerNode, 'setData');
const ds = manager?.get('ds_1');
ds?.setData({ name: 'Y' });
expect(innerSetData).toHaveBeenCalled();
const arg: any = innerSetData.mock.calls[0][0];
expect(arg.text).toBe('pf Y');
expect((pageFragment.data as any).items[0].text).toBe('pf Y');
});
});
describe('createDataSourceManager - app.page 不存在', () => {
test('app.page 缺失时跳过 page.setData / 节点 setData但仍发出 update-data', () => {
const dsl: MApp = {
type: NodeType.ROOT,
id: 'app_no_page',
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [{ type: 'text', id: 'text_a', text: 'a ${ds_1.name}' } as any],
},
],
dataSourceDeps: {
ds_1: {
text_a: { name: '文本', keys: ['text'] },
},
},
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [{ name: 'name', defaultValue: 'init' }],
methods: [],
events: [],
},
],
};
// curPage 指向不存在的页setPage 会调用 deletePage 让 app.page = undefined
const app = new TMagicApp({ config: dsl, platform: 'editor', curPage: 'not_exist' });
expect(app.page).toBeUndefined();
const manager = createDataSourceManager(app);
const update = vi.fn();
manager?.on('update-data', update);
const ds = manager?.get('ds_1');
expect(() => ds?.setData({ name: 'V' })).not.toThrow();
expect(update).toHaveBeenCalledTimes(1);
expect(update.mock.calls[0][3]).toBe('page_1');
});
});
describe('createDataSourceManager - pageFragment 与被遍历 page 同 id', () => {
test('editor 平台遍历到 pageFragment 自身页时进入 pageFragment.data.id === page.id 分支', () => {
const dsl: MApp = {
type: NodeType.ROOT,
id: 'app_pf_iter',
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
type: 'page-fragment-container',
id: 'pfc_1',
pageFragmentId: 'pf_1',
items: [],
} as any,
],
},
{
type: NodeType.PAGE_FRAGMENT,
id: 'pf_1',
items: [{ type: 'text', id: 'pf_text', text: 'pf ${ds_1.name}' } as any],
} as any,
],
dataSourceDeps: {
ds_1: {
pf_text: { name: 'pf_text', keys: ['text'] },
},
},
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [{ name: 'name', defaultValue: 'init' }],
methods: [],
events: [],
},
],
};
const app = new TMagicApp({ config: dsl, platform: 'editor', curPage: 'page_1' });
const manager = createDataSourceManager(app);
const pageFragment = app.pageFragments.get('pfc_1')!;
const innerNode = pageFragment.getNode('pf_text', { strict: true })!;
const innerSetData = vi.spyOn(innerNode, 'setData');
const ds = manager?.get('ds_1');
ds?.setData({ name: 'Z' });
expect(innerSetData).toHaveBeenCalled();
const arg: any = innerSetData.mock.calls[0][0];
expect(arg.text).toBe('pf Z');
expect((pageFragment.data as any).items[0].text).toBe('pf Z');
});
});
describe('createDataSourceManager - pageFragment 边界分支', () => {
const buildDsl = (): MApp => ({
type: NodeType.ROOT,
id: 'app_pf_edge',
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{
type: 'page-fragment-container',
id: 'pfc_1',
pageFragmentId: 'pf_1',
items: [],
} as any,
],
},
{
type: NodeType.PAGE_FRAGMENT,
id: 'pf_1',
items: [{ type: 'text', id: 'pf_text', text: 'pf ${ds_1.name}' } as any],
} as any,
],
dataSourceDeps: {
ds_1: {
pf_text: { name: 'pf_text', keys: ['text'] },
},
},
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [{ name: 'name', defaultValue: 'init' }],
methods: [],
events: [],
},
],
});
test('pageFragment.getNode 返回 undefined 时安全跳过 setData', () => {
const app = new TMagicApp({ config: buildDsl(), platform: 'editor', curPage: 'page_1' });
const manager = createDataSourceManager(app);
const pageFragment = app.pageFragments.get('pfc_1')!;
// 模拟 pageFragment 内对应节点已被移除的边界
pageFragment.nodes.delete('pf_text');
const ds = manager?.get('ds_1');
expect(() => ds?.setData({ name: 'A' })).not.toThrow();
});
test('pageFragment 与当前遍历的 page、newNode 都无关时不会进入 pageFragment 同步分支', () => {
const dsl: MApp = {
type: NodeType.ROOT,
id: 'app_pf_unrelated',
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{ type: 'text', id: 'plain_text', text: 'a ${ds_1.name}' } as any,
{
type: 'page-fragment-container',
id: 'pfc_1',
pageFragmentId: 'pf_1',
items: [],
} as any,
],
},
{
type: NodeType.PAGE_FRAGMENT,
id: 'pf_1',
items: [{ type: 'text', id: 'pf_text', text: 'pf' } as any],
} as any,
],
dataSourceDeps: {
ds_1: {
plain_text: { name: 'plain_text', keys: ['text'] },
},
},
dataSources: [
{
id: 'ds_1',
type: 'base',
fields: [{ name: 'name', defaultValue: 'init' }],
methods: [],
events: [],
},
],
};
const app = new TMagicApp({ config: dsl, platform: 'mobile', curPage: 'page_1' });
const manager = createDataSourceManager(app);
const pageFragment = app.pageFragments.get('pfc_1')!;
const pfSetData = vi.spyOn(pageFragment, 'setData');
const ds = manager?.get('ds_1');
ds?.setData({ name: 'C' });
// pageFragment 与本次更新无关,不会被同步
expect(pfSetData).not.toHaveBeenCalled();
});
test('pageFragment.instance 为真时跳过 replaceChildNode', () => {
const app = new TMagicApp({ config: buildDsl(), platform: 'editor', curPage: 'page_1' });
const manager = createDataSourceManager(app);
const pageFragment = app.pageFragments.get('pfc_1')!;
pageFragment.setInstance({ __isVue: true });
const before = (pageFragment.data as any).items[0].text;
const ds = manager?.get('ds_1');
ds?.setData({ name: 'B' });
// 因为 instance 存在pageFragment.data 不会被 replaceChildNode 改写
expect((pageFragment.data as any).items[0].text).toBe(before);
});
});
describe('createDataSourceManager - 自定义数据源类型尚未注册', () => {
test('未知类型在初始化时不抛错,仅写入默认数据', () => {
const dsl: MApp = {
type: NodeType.ROOT,
id: 'app_pending',
items: [],
dataSources: [
{
id: 'ds_unknown',
type: 'custom-not-registered',
fields: [{ name: 'name', defaultValue: 'd' }],
methods: [],
events: [],
} as any,
],
};
const app = new TMagicApp({ config: dsl });
const manager = createDataSourceManager(app);
expect(manager).toBeInstanceOf(DataSourceManager);
expect(manager?.data.ds_unknown).toEqual({ name: 'd' });
expect(manager?.get('ds_unknown')).toBeUndefined();
});
test('在未注册期间通过 register 触发延迟初始化', () => {
const dsl: MApp = {
type: NodeType.ROOT,
id: 'app_lazy',
items: [],
dataSources: [
{
id: 'ds_lazy',
type: 'lazy-type',
fields: [{ name: 'name' }],
methods: [],
events: [],
} as any,
],
};
const app = new TMagicApp({ config: dsl });
const manager = createDataSourceManager(app);
expect(manager?.get('ds_lazy')).toBeUndefined();
class LazyDataSource extends DataSource {}
DataSourceManager.register('lazy-type', LazyDataSource as any);
expect(manager?.get('ds_lazy')).toBeInstanceOf(LazyDataSource);
});
});

View File

@ -0,0 +1,57 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import { describe, expect, test } from 'vitest';
import { getDeps } from '@data-source/depsCache';
describe('getDeps', () => {
test('从节点收集普通字段依赖', () => {
const ds: any = {
id: 'ds_1',
fields: [{ name: 'name', type: 'string' }],
};
const nodes: any[] = [
{
id: 'page_1',
type: 'page',
items: [
{
id: 'btn_1',
type: 'text',
text: '${ds_1.name}',
},
],
},
];
const result = getDeps(ds, nodes, false);
expect(result.deps).toBeDefined();
expect(result.condDeps).toBeDefined();
});
test('inEditor=true 时缓存键包含所有 traverse 节点', () => {
const ds: any = {
id: 'ds_2',
fields: [{ name: 'name' }],
};
const nodes: any[] = [
{
id: 'page_1',
type: 'page',
items: [{ id: 'btn_1', type: 'text', text: '${ds_2.name}' }],
},
];
const result = getDeps(ds, nodes, true);
expect(result.deps).toBeDefined();
});
test('cache 命中时返回同一对象', () => {
const ds: any = { id: 'ds_3', fields: [{ name: 'n' }] };
const nodes: any[] = [{ id: 'p', type: 'page', items: [] }];
const r1 = getDeps(ds, nodes, false);
const r2 = getDeps(ds, nodes, false);
expect(r1).toBe(r2);
});
});

View File

@ -1,8 +1,18 @@
import { describe, expect, test } from 'vitest';
import { describe, expect, test, vi } from 'vitest';
import { dataSourceTemplateRegExp } from '@tmagic/core';
import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, dataSourceTemplateRegExp, NodeType } from '@tmagic/core';
import { compiledCondition, createIteratorContentData, template } from '@data-source/utils';
import {
compiledCondition,
compiledNodeField,
compliedConditions,
compliedDataSourceField,
compliedIteratorItem,
createIteratorContentData,
registerDataSourceOnDemand,
template,
updateNode,
} from '@data-source/utils';
describe('compiledCondition', () => {
test('=,true', () => {
@ -184,3 +194,207 @@ describe('createIteratorContentData', () => {
expect(ctxData.ds.a.c.a).toBe(1);
});
});
describe('compliedConditions', () => {
test('未配置 conditions 时直接返回 true', () => {
expect(compliedConditions({}, {})).toBe(true);
expect(compliedConditions({ ['displayConds' as any]: [] } as any, {})).toBe(true);
});
test('任一 cond 通过即返回 true', () => {
const node: any = {
displayConds: [
{ cond: [{ field: ['ds_1', 'a'], op: '=', value: 2 }] },
{ cond: [{ field: ['ds_1', 'a'], op: '=', value: 1 }] },
],
};
expect(compliedConditions(node, { ds_1: { a: 1 } })).toBe(true);
});
test('全部不通过则返回 false', () => {
const node: any = {
displayConds: [{ cond: [{ field: ['ds_1', 'a'], op: '=', value: 2 }] }],
};
expect(compliedConditions(node, { ds_1: { a: 1 } })).toBe(false);
});
test('cond 为空被跳过', () => {
const node: any = { displayConds: [{ cond: undefined }] };
expect(compliedConditions(node, {})).toBe(false);
});
});
describe('compiledCondition 边界', () => {
test('数据源不存在时直接 break 视为通过', () => {
expect(compiledCondition([{ field: ['unknown', 'a'], op: '=', value: 1 }], {})).toBe(true);
});
test('field 取值异常(如类型错)时 console.warn 不阻断', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
const result = compiledCondition([{ field: ['ds', 'a', 'b', 'c'], op: '=', value: 1 }], { ds: { a: 'string' } });
expect(result).toBe(true);
warn.mockRestore();
});
});
describe('updateNode', () => {
test('页面节点直接替换 dsl.items', () => {
const dsl: any = {
type: NodeType.ROOT,
id: 'app',
items: [{ id: 'p1', type: NodeType.PAGE, items: [{ id: 'btn' }] }],
};
updateNode({ id: 'p1', type: NodeType.PAGE, items: [{ id: 'btn2' }] } as any, dsl);
expect(dsl.items[0].items[0].id).toBe('btn2');
});
test('非页面节点走 replaceChildNode', () => {
const dsl: any = {
type: NodeType.ROOT,
id: 'app',
items: [
{
id: 'p1',
type: NodeType.PAGE,
items: [{ id: 'btn', type: 'button', text: 'old' }],
},
],
};
updateNode({ id: 'btn', type: 'button', text: 'new' } as any, dsl);
expect(dsl.items[0].items[0].text).toBe('new');
});
});
describe('compliedDataSourceField', () => {
test('不带前缀直接返回原值', () => {
expect(compliedDataSourceField(['no-prefix-id', 'name'], { id: { name: 'x' } })).toEqual(['no-prefix-id', 'name']);
});
test('数据源不存在时返回原值', () => {
expect(compliedDataSourceField([`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}id`, 'name'], {})).toEqual([
`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}id`,
'name',
]);
});
test('正常解析数据源字段', () => {
const value = compliedDataSourceField([`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}id`, 'name'], {
id: { name: 'x' },
});
expect(value).toBe('x');
});
test('字段路径不存在时返回原值', () => {
expect(
compliedDataSourceField([`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}id`, 'name', 'sub'], { id: { name: 'x' } }),
).toEqual([`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}id`, 'name', 'sub']);
});
});
describe('compiledNodeField', () => {
const data = { id: { name: 'world' } };
test('字符串模板', () => {
expect(compiledNodeField('hello ${id.name}', data)).toBe('hello world');
});
test('isBindDataSource 直接取整个数据源', () => {
expect(compiledNodeField({ isBindDataSource: true, dataSourceId: 'id' }, data)).toEqual({ name: 'world' });
});
test('isBindDataSourceField 走模板', () => {
expect(compiledNodeField({ isBindDataSourceField: true, dataSourceId: 'id', template: 'hi ${name}' }, data)).toBe(
'hi world',
);
});
test('数组形式走 compliedDataSourceField', () => {
expect(compiledNodeField([`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}id`, 'name'], data)).toBe('world');
});
test('未匹配格式直接返回原值', () => {
expect(compiledNodeField(123 as any, data)).toBe(123);
});
});
describe('compliedIteratorItem', () => {
test('递归 compile items 并应用条件', () => {
const item: any = {
id: 'parent',
items: [{ id: 'child', text: 'origin' }],
};
const ctxData = { ds: { name: 'V' } };
const result = compliedIteratorItem({
compile: (v: any) => `compiled-${v}`,
dsId: 'ds',
item,
deps: { child: { name: 'c', keys: ['text'] } },
condDeps: {},
inEditor: false,
ctxData,
});
expect(result.items[0].text).toBe('compiled-origin');
expect(result.id).toBe('parent');
});
test('items 不是数组时保留原值', () => {
const result = compliedIteratorItem({
compile: (v: any) => v,
dsId: 'ds',
item: { id: 'p', items: 'not-array' as any } as any,
deps: {},
condDeps: {},
inEditor: true,
ctxData: {},
});
expect(result.items).toBe('not-array');
});
test('条件依赖在非编辑器中会写入 condResult', () => {
const result = compliedIteratorItem({
compile: (v: any) => v,
dsId: 'ds',
item: {
id: 'p',
displayConds: [{ cond: [{ field: ['ds', 'a'], op: '=', value: 1 }] }],
} as any,
deps: {},
condDeps: { p: { name: 'p', keys: ['displayConds'] } },
inEditor: false,
ctxData: { ds: { a: 1 } },
});
expect(result.condResult).toBe(true);
});
});
describe('registerDataSourceOnDemand', () => {
test('按依赖按需返回模块', async () => {
const dsl: any = {
dataSources: [
{ id: 'a', type: 'http' },
{ id: 'b', type: 'mock' },
{ id: 'c', type: 'http' },
],
dataSourceDeps: { a: { node1: { name: 'n', keys: ['x'] } } },
dataSourceCondDeps: { c: { node2: { name: 'n', keys: ['y'] } } },
dataSourceMethodDeps: {},
};
const httpModule = { default: class HttpDS {} };
const mockModule = { default: class MockDS {} };
const modules = await registerDataSourceOnDemand(dsl, {
http: () => Promise.resolve(httpModule as any),
mock: () => Promise.resolve(mockModule as any),
});
expect(modules.http).toBe(httpModule.default);
expect(modules.mock).toBeUndefined();
});
test('找不到对应模块时跳过', async () => {
const dsl: any = {
dataSources: [{ id: 'a', type: 'unknown' }],
dataSourceDeps: { a: { node: { name: 'n', keys: ['x'] } } },
};
const modules = await registerDataSourceOnDemand(dsl, {});
expect(Object.keys(modules)).toHaveLength(0);
});
});

View File

@ -1,5 +1,5 @@
{
"version": "1.7.14-beta.0",
"version": "1.8.0-beta.4",
"name": "@tmagic/dep",
"type": "module",
"sideEffects": false,

View File

@ -127,10 +127,38 @@ export default class Watcher {
deep = false,
type?: DepTargetType | string,
) {
this.collectByCallback(nodes, type, ({ node, target }) => {
this.removeTargetDep(target, node);
this.collectItem(node, target, depExtendedData, deep);
});
const targets = this.getCollectableTargets(type);
if (!targets.length) {
return;
}
// 整棵树只遍历一次、在每个属性上检查所有 target把结构遍历开销从 ×targets 降到 ×1详见 collectItems
for (const node of nodes) {
this.removeTargetsDep(targets, node);
this.collectItems(node, targets, depExtendedData, deep);
}
}
/**
* target collectByCallback
*
* editor dep service / worker public
* @param type
*/
public getCollectableTargets(type?: DepTargetType | string): Target[] {
const targets: Target[] = [];
traverseTarget(
this.targetsList,
(target) => {
if (!type && !target.isCollectByDefault) {
return;
}
targets.push(target);
},
type,
);
return targets;
}
public collectByCallback(
@ -195,53 +223,11 @@ export default class Watcher {
this.clear(nodes, type);
}
/**
* target collectItems(node, [target], ...)
*/
public collectItem(node: TargetNode, target: Target, depExtendedData: DepExtendedData = {}, deep = false) {
if (node[NODE_DISABLE_DATA_SOURCE_KEY] && DATA_SOURCE_TARGET_TYPES.has(target.type)) {
return;
}
if (node[NODE_DISABLE_CODE_BLOCK_KEY] && target.type === DepTargetType.CODE_BLOCK) {
return;
}
const collectTarget = (config: Record<string | number, any>, prop = '') => {
const doCollect = (key: string, value: any) => {
const keyIsItems = key === this.childrenProp;
const fullKey = prop ? `${prop}.${key}` : key;
if (target.isTarget(fullKey, value)) {
target.updateDep({
id: node[this.idProp],
name: `${node[this.nameProp] || node[this.idProp]}`,
data: depExtendedData,
key: fullKey,
});
} else if (!keyIsItems && Array.isArray(value)) {
for (let i = 0, l = value.length; i < l; i++) {
const item = value[i];
if (isObject(item)) {
collectTarget(item, `${fullKey}[${i}]`);
}
}
} else if (isObject(value)) {
collectTarget(value, fullKey);
}
if (keyIsItems && deep && Array.isArray(value)) {
for (const child of value) {
this.collectItem(child, target, depExtendedData, deep);
}
}
};
for (const [key, value] of Object.entries(config)) {
if (typeof value === 'undefined' || value === '') continue;
doCollect(key, value);
}
};
collectTarget(node);
this.collectItems(node, [target], depExtendedData, deep);
}
public removeTargetDep(target: Target, node: TargetNode, key?: string | number) {
@ -252,4 +238,117 @@ export default class Watcher {
}
}
}
/**
* removeTargetDep target
* ×targets ×1
*
* editor dep service public
*/
public removeTargetsDep(targets: Target[], node: TargetNode, key?: string | number) {
const id = node[this.idProp];
for (const target of targets) {
target.removeDep(id, key);
}
if (typeof key === 'undefined' && Array.isArray(node[this.childrenProp]) && node[this.childrenProp].length) {
for (const item of node[this.childrenProp] as TargetNode[]) {
this.removeTargetsDep(targets, item, key);
}
}
}
/**
* collectItem target
*
* target O(targets × ) +
* Object.entries / / fullKey targets
* target ×targets ×1isTarget
*
* editor dep service / worker public
*/
public collectItems(node: TargetNode, targets: Target[], depExtendedData: DepExtendedData = {}, deep = false) {
// 对应 collectItem 开头的 NODE_DISABLE_* 判断:被禁用的 target 在该节点及其子树都不收集
const activeTargets = this.filterTargetsByNode(node, targets);
if (!activeTargets.length) {
return;
}
this.collectTargetForTargets(node, node, '', activeTargets, depExtendedData, deep);
}
private filterTargetsByNode(node: TargetNode, targets: Target[]): Target[] {
const disableDataSource = Boolean(node[NODE_DISABLE_DATA_SOURCE_KEY]);
const disableCodeBlock = Boolean(node[NODE_DISABLE_CODE_BLOCK_KEY]);
if (!disableDataSource && !disableCodeBlock) {
return targets;
}
return targets.filter((target) => {
if (disableDataSource && DATA_SOURCE_TARGET_TYPES.has(target.type)) {
return false;
}
if (disableCodeBlock && target.type === DepTargetType.CODE_BLOCK) {
return false;
}
return true;
});
}
private collectTargetForTargets(
node: TargetNode,
config: Record<string | number, any>,
prop: string,
targets: Target[],
depExtendedData: DepExtendedData,
deep: boolean,
) {
const id = node[this.idProp];
const name = `${node[this.nameProp] || node[this.idProp]}`;
for (const [key, value] of Object.entries(config)) {
if (typeof value === 'undefined' || value === '') continue;
const keyIsItems = key === this.childrenProp;
const fullKey = prop ? `${prop}.${key}` : key;
// 在该属性上检查所有 target命中的更新依赖未命中的留待递归到更深层
let notMatched: Target[] | null = null;
for (let i = 0, l = targets.length; i < l; i++) {
const target = targets[i];
if (target.isTarget(fullKey, value, config)) {
target.updateDep({
id,
name,
data: depExtendedData,
key: fullKey,
});
} else {
(notMatched || (notMatched = [])).push(target);
}
}
// 对应原 doCollect 的 else-if 分支:仅未命中的 target 才继续往 value 内部递归
if (notMatched) {
if (!keyIsItems && Array.isArray(value)) {
for (let i = 0, l = value.length; i < l; i++) {
const item = value[i];
if (isObject(item)) {
this.collectTargetForTargets(node, item, `${fullKey}[${i}]`, notMatched, depExtendedData, deep);
}
}
} else if (isObject(value)) {
this.collectTargetForTargets(node, value, fullKey, notMatched, depExtendedData, deep);
}
}
// 对应原 doCollect 末尾的无条件子节点递归
if (keyIsItems && deep && Array.isArray(value)) {
for (const child of value) {
this.collectItems(child, targets, depExtendedData, deep);
}
}
}
}
}

View File

@ -15,7 +15,7 @@ export enum DepTargetType {
DATA_SOURCE_COND = 'data-source-cond',
}
export type IsTarget = (key: string | number, value: any) => boolean;
export type IsTarget = (key: string | number, value: any, data?: Record<string, any>) => boolean;
export interface TargetOptions {
isTarget: IsTarget;

View File

@ -25,4 +25,71 @@ describe('Target', () => {
expect(defaultTarget.type).toBe('default');
expect(target.type).toBe('target');
});
test('initialDeps / name / isCollectByDefault 默认值', () => {
const t = new Target({
isTarget: () => true,
id: 't1',
name: 'first',
initialDeps: { node_1: { name: 'n', keys: ['k1'] } },
});
expect(t.name).toBe('first');
expect(t.deps.node_1.keys).toEqual(['k1']);
expect(t.isCollectByDefault).toBe(true);
const t2 = new Target({
isTarget: () => true,
id: 't2',
isCollectByDefault: false,
});
expect(t2.isCollectByDefault).toBe(false);
});
test('updateDep 累加 keys 并保留 name/data', () => {
const t = new Target({ isTarget: () => true, id: 't' });
t.updateDep({ id: 'n1', name: 'n1-name', key: 'key1', data: { foo: 1 } });
expect(t.deps.n1.name).toBe('n1-name');
expect(t.deps.n1.keys).toEqual(['key1']);
expect((t.deps.n1 as any).data).toEqual({ foo: 1 });
t.updateDep({ id: 'n1', name: 'n1-name', key: 'key2', data: { foo: 2 } });
expect(t.deps.n1.keys).toEqual(['key1', 'key2']);
t.updateDep({ id: 'n1', name: 'n1-name', key: 'key1', data: { foo: 3 } });
expect(t.deps.n1.keys).toEqual(['key1', 'key2']);
});
test('removeDep 全删 / 删指定 id / 按 key 删', () => {
const t = new Target({ isTarget: () => true, id: 't' });
t.updateDep({ id: 'n1', name: 'n', key: 'k1', data: {} });
t.updateDep({ id: 'n1', name: 'n', key: 'k2', data: {} });
t.updateDep({ id: 'n2', name: 'n', key: 'k1', data: {} });
t.removeDep('n1', 'k1');
expect(t.deps.n1.keys).toEqual(['k2']);
t.removeDep('n1', 'k2');
expect(t.deps.n1).toBeUndefined();
t.removeDep('n2');
expect(t.deps.n2).toBeUndefined();
t.updateDep({ id: 'a', name: 'a', key: 'k', data: {} });
t.updateDep({ id: 'b', name: 'b', key: 'k', data: {} });
t.removeDep();
expect(Object.keys(t.deps)).toHaveLength(0);
t.removeDep('not-exist');
});
test('hasDep / destroy', () => {
const t = new Target({ isTarget: () => true, id: 't' });
t.updateDep({ id: 'n1', name: 'n', key: 'k', data: {} });
expect(t.hasDep('n1', 'k')).toBe(true);
expect(t.hasDep('n1', 'other')).toBe(false);
expect(t.hasDep('not-exist', 'k')).toBe(false);
t.destroy();
expect(t.deps).toEqual({});
});
});

View File

@ -1,8 +1,10 @@
import { describe, expect, test } from 'vitest';
import { DataSchema } from '@tmagic/schema';
import { DataSchema, NODE_CONDS_KEY } from '@tmagic/schema';
import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX } from '@tmagic/utils';
import Target from '../src/Target';
import { DepTargetType } from '../src/types';
import * as utils from '../src/utils';
describe('utils', () => {
@ -193,4 +195,94 @@ describe('utils', () => {
}),
).toBeTruthy();
});
test('isDataSourceTarget', () => {
const ds = { id: 'ds_1', fields: [{ name: 'name', type: 'string' }] as DataSchema[] };
expect(utils.isDataSourceTarget(ds, 'k', null)).toBe(false);
expect(utils.isDataSourceTarget(ds, 'k', 123)).toBe(false);
expect(utils.isDataSourceTarget(ds, `${NODE_CONDS_KEY}_x`, '${ds_1.name}')).toBe(false);
expect(utils.isDataSourceTarget(ds, 'text', '${ds_1.name}')).toBe(true);
expect(utils.isDataSourceTarget(ds, 'text', '${other.name}')).toBe(false);
expect(utils.isDataSourceTarget(ds, 'text', { isBindDataSource: true, dataSourceId: 'ds_1' })).toBe(true);
expect(utils.isDataSourceTarget(ds, 'text', { isBindDataSource: true, dataSourceId: 'other' })).toBe(false);
expect(
utils.isDataSourceTarget(ds, 'text', {
isBindDataSourceField: true,
dataSourceId: 'ds_1',
template: 'foo${name}',
}),
).toBe(true);
expect(utils.isDataSourceTarget(ds, 'text', [`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}ds_1`, 'name'])).toBe(true);
expect(
utils.isDataSourceTarget(
{ id: 'ds_1', fields: [{ name: 'arr', type: 'array', fields: [{ name: 'a' }] }] as DataSchema[] },
'text',
[`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}ds_1`, 'arr', 'a'],
true,
),
).toBe(true);
});
test('isDataSourceCondTarget', () => {
const ds = { id: 'ds_1', fields: [{ name: 'name' }] as DataSchema[] };
expect(utils.isDataSourceCondTarget(ds, 'k', 'not-array')).toBe(false);
expect(utils.isDataSourceCondTarget(ds, 'k', null as any)).toBe(false);
expect(utils.isDataSourceCondTarget(ds, `${NODE_CONDS_KEY}_x`, ['ds_1', 'name'])).toBe(true);
expect(utils.isDataSourceCondTarget(ds, 'k', ['ds_1', 'name'])).toBe(false);
expect(utils.isDataSourceCondTarget(ds, `${NODE_CONDS_KEY}_x`, ['other', 'name'])).toBe(false);
expect(utils.isDataSourceCondTarget(ds, `${NODE_CONDS_KEY}_x`, ['ds_1', 'unknown'])).toBe(false);
});
test('createDataSourceTarget / Cond / Method', () => {
const ds = { id: 'ds_1', fields: [{ name: 'name' }] as DataSchema[] };
const t1 = utils.createDataSourceTarget(ds);
expect(t1.type).toBe(DepTargetType.DATA_SOURCE);
expect(t1.isTarget('text', '${ds_1.name}')).toBe(true);
const t2 = utils.createDataSourceCondTarget(ds);
expect(t2.type).toBe(DepTargetType.DATA_SOURCE_COND);
expect(t2.isTarget(`${NODE_CONDS_KEY}_x`, ['ds_1', 'name'])).toBe(true);
const t3 = utils.createDataSourceMethodTarget({
id: 'ds_1',
methods: [{ name: 'load', content: () => undefined, params: [] } as any],
fields: [{ name: 'name' }] as DataSchema[],
});
expect(t3.type).toBe(DepTargetType.DATA_SOURCE_METHOD);
expect(t3.isTarget('k', ['ds_1', 'load'])).toBe(true);
expect(t3.isTarget('k', ['ds_1', 'name'])).toBe(false);
expect(t3.isTarget('k', ['other', 'load'])).toBe(false);
expect(t3.isTarget('k', 'not-array')).toBe(false);
expect(t3.isTarget('k', ['ds_1', ''])).toBe(false);
expect(t3.isTarget('k', ['ds_1', 'unknown'])).toBe(true);
});
test('traverseTarget 遍历所有 / 指定 type', () => {
const t1 = new Target({ id: '1', isTarget: () => true, type: 'a' });
const t2 = new Target({ id: '2', isTarget: () => true, type: 'b' });
const list = {
a: { 1: t1 },
b: { 2: t2 },
};
const visited: string[] = [];
utils.traverseTarget(list, (t) => visited.push(`${t.type}:${t.id}`));
expect(visited).toEqual(expect.arrayContaining(['a:1', 'b:2']));
const visitedA: string[] = [];
utils.traverseTarget(list, (t) => visitedA.push(`${t.type}:${t.id}`), 'a');
expect(visitedA).toEqual(['a:1']);
const visitedX: string[] = [];
utils.traverseTarget(list, (t) => visitedX.push(`${t.type}:${t.id}`), 'not-exist');
expect(visitedX).toEqual([]);
});
});

View File

@ -1,5 +1,5 @@
{
"version": "1.7.14-beta.0",
"version": "1.8.0-beta.4",
"name": "@tmagic/design",
"type": "module",
"sideEffects": [

View File

@ -32,6 +32,7 @@ export interface ButtonProps {
circle?: boolean;
icon?: any;
variant?: string;
bg?: boolean;
}
export interface CardProps {

Some files were not shown because too many files have changed in this diff Show More