Compare commits

..

No commits in common. "master" and "v1.8.0-beta.5" have entirely different histories.

331 changed files with 6267 additions and 21175 deletions

1
.gitignore vendored
View File

@ -20,7 +20,6 @@ pnpm-debug.log*
# Editor directories and files # Editor directories and files
.idea .idea
.vscode .vscode
.codebuddy
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj

View File

@ -14,13 +14,12 @@ TMagic Editor 是魔方平台的可视化编辑器核心库,提供拖拽式组
## 架构地图 ## 架构地图
关键目录: 关键目录:
- `packages/` — 核心编辑器包202 *.vue, 194 *.ts
- `packages/` — 核心编辑器包
- `runtime/` — Vue/React Runtime 实现 - `runtime/` — Vue/React Runtime 实现
- `vue-components/` — Vue 组件封装 - `vue-components/` — Vue 组件封装
- `react-components/` — React 组件封装 - `react-components/` — React 组件封装
- `playground/` — 演示 playground - `playground/` — 演示 playground
- `docs/` — VitePress 文档 - `docs/` — VitePress 文档100 *.md
- `scripts/` — 构建和发布脚本 - `scripts/` — 构建和发布脚本
- `eslint-config/` — 共享 ESLint 配置 - `eslint-config/` — 共享 ESLint 配置
@ -28,13 +27,10 @@ TMagic Editor 是魔方平台的可视化编辑器核心库,提供拖拽式组
**分支策略:** dev=dev, test/prod=master **分支策略:** dev=dev, test/prod=master
**提交规范:** commitlint + husky`type: 描述` **提交规范:** commitlint + husky`type: 描述`
**测试覆盖率:** 新增或修改的代码必须补充单元测试,覆盖率不低于 85%
**禁止事项:** **禁止事项:**
- 禁止在核心包中引入腾讯内部专有依赖(开源项目) - 禁止在核心包中引入腾讯内部专有依赖(开源项目)
- 禁止直接修改 CHANGELOG.md应通过 `pnpm changelog` 生成 - 禁止直接修改 CHANGELOG.md应通过 `pnpm changelog` 生成
- 禁止提交未达 85% 测试覆盖率的新增/修改代码
## 常用命令 ## 常用命令
@ -54,7 +50,7 @@ TMagic Editor 是魔方平台的可视化编辑器核心库,提供拖拽式组
## 深入阅读 ## 深入阅读
| 文档 | 说明 | | 文档 | 说明 |
| --------------- | ---------------- | |------|------|
| docs/ | VitePress 文档站 | | docs/ | VitePress 文档站 |
| CONTRIBUTING.md | 贡献指南 | | CONTRIBUTING.md | 贡献指南 |
| CHANGELOG.md | 变更日志 | | CHANGELOG.md | 变更日志 |

View File

@ -1,158 +1,3 @@
# [1.8.0-beta.14](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.13...v1.8.0-beta.14) (2026-07-23)
### Bug Fixes
* **editor:** 保留历史操作来源并补发插入事件 ([89689bb](https://github.com/Tencent/tmagic-editor/commit/89689bb9fd2d2d3edcc027b87932cbdd3a5755ad))
* **form:** 统一 extendState 合并逻辑 ([0feada8](https://github.com/Tencent/tmagic-editor/commit/0feada8d1fa4057444da746040b3824df09eef07))
* **stage:** 修复选中组件时 stage 外层滚动 ([09eecd5](https://github.com/Tencent/tmagic-editor/commit/09eecd5bedd49e5a002fa5ef122eaa1579dc895b))
* 调整编辑器配置项展示 ([8699dc1](https://github.com/Tencent/tmagic-editor/commit/8699dc12af9d52b1c460f61f3d3f48e4cf3fc5f6))
### Features
* **design,editor:** 新增magic-admin主题样式,优化组件表单配置布局样式 ([b13cd34](https://github.com/Tencent/tmagic-editor/commit/b13cd3425b16eaeac33f897bf51e80f7984db782))
* playground 支持主题切换 ([ee5247c](https://github.com/Tencent/tmagic-editor/commit/ee5247ca9436c385a5b634d161726006912fe77c))
# [1.8.0-beta.13](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.11...v1.8.0-beta.13) (2026-07-22)
### Bug Fixes
* **adapters:** tag 非法 type 回退为默认主题 ([90492ad](https://github.com/Tencent/tmagic-editor/commit/90492ad3eac82f052e99e7a286772a3feac288f7))
* **core:** 在 initNode 前校验 config.items 为数组 ([aa71fe9](https://github.com/Tencent/tmagic-editor/commit/aa71fe9ddd011e98b40af04e898bdf79263e0e89))
* **editor:** 为 FloatingBox 拖拽遮罩添加失焦等兜底清理,避免残留导致编辑器无法点击 ([4a893d3](https://github.com/Tencent/tmagic-editor/commit/4a893d35df354cfedce8e57b0deaaa0c535f8fba))
* **editor:** 修复 FloatingBox 拖动时鼠标进入 iframe 区域事件丢失 ([284be0d](https://github.com/Tencent/tmagic-editor/commit/284be0d276d90fe58cdcd94372dbacb476fd919c))
* **editor:** 修复 Monaco 编辑器卸载时容器失效与 Canceled 未处理异常 ([4a15da2](https://github.com/Tencent/tmagic-editor/commit/4a15da21083a30d2590a03fe68c488c71ec44eb2))
* **editor:** 修正 StyleSetter 字段 prop 取值逻辑 ([e5e0da3](https://github.com/Tencent/tmagic-editor/commit/e5e0da37852c78728e6506980dc2de6928dd571c))
* **editor:** 延迟 FloatingBox 拖拽遮罩到实际位移时显示,修复关闭按钮点击失效 ([9aa251c](https://github.com/Tencent/tmagic-editor/commit/9aa251ce57e36120a1f6c3b4835ba3bcd604448d))
* **form,editor:** 完善表单校验与样式属性匹配 ([ef66e8a](https://github.com/Tencent/tmagic-editor/commit/ef66e8a59863f9ffeb7292adcb314e5620c238ef))
* **form:** radio group disabled不生效 ([42d45be](https://github.com/Tencent/tmagic-editor/commit/42d45bed187393ae02a1fef1df5d46b114f198a8))
* **form:** typeMatch 使用 toLine 统一类型转换并处理异步 type/defaultValue ([6cdf54a](https://github.com/Tencent/tmagic-editor/commit/6cdf54a4a663dd6531c70a069220b6d9a63ca7c0))
* **form:** 修复隐藏字段校验处理 ([78b34a4](https://github.com/Tencent/tmagic-editor/commit/78b34a452b1ac263b652ace81ef351f56fae7ff9))
### Features
* **editor:** 扩展 customContentMenu 支持 getTarget 获取右键目标 ([bbe73aa](https://github.com/Tencent/tmagic-editor/commit/bbe73aae6437cabc8cfada24367c3d969687faa9))
* **editor:** 抽离表单视图与对比逻辑 ([6f3f321](https://github.com/Tencent/tmagic-editor/commit/6f3f321736a40b242e7b556b0bea456484f161ea))
* **editor:** 支持自定义历史对比/回滚弹窗内表单组件的尺寸 ([605f07c](https://github.com/Tencent/tmagic-editor/commit/605f07c8fe24fd67de22f5cb9adfdb151d3b8772))
* **editor:** 支持通过 uiService 配置列布局最小宽度 ([3c21f45](https://github.com/Tencent/tmagic-editor/commit/3c21f45e88dfe7c61552c704bd9cbd09215017fe))
* **editor:** 新增属性配置表单校验联动能力 ([0e46692](https://github.com/Tencent/tmagic-editor/commit/0e4669261fe50acaa8f72203dbc342e6eff324e6))
* **editor:** 注册编辑器字段内置 typeMatch 校验规则并补充文档 ([1c67b5e](https://github.com/Tencent/tmagic-editor/commit/1c67b5e77b726ded63b12d761faff37e97563ff2))
* **form,editor:** 新增 validateForm 校验能力并接入源码保存校验联动 ([d07e48b](https://github.com/Tencent/tmagic-editor/commit/d07e48bb101a3833b57b97001af3f053e2392c7a))
* **form:** 支持 rules.typeMatch 按字段 type 校验值类型与选项匹配 ([734e6a8](https://github.com/Tencent/tmagic-editor/commit/734e6a8ee6f9a948273c25461556eb4f54adc1ee))
* **table:** 为操作列新增 Popconfirm 二次确认能力 ([9e8272b](https://github.com/Tencent/tmagic-editor/commit/9e8272b521784265f18007e11f94d2ae5ee3faba))
* **table:** 抽取 ActionPopconfirm 并优化 sub-actions 展开交互 ([b183e71](https://github.com/Tencent/tmagic-editor/commit/b183e71f642fba84c82b0e8e63bcb03e0d27e610))
* **table:** 操作列支持 sub-actions 更多菜单并抽取 ActionButton 组件 ([78a1259](https://github.com/Tencent/tmagic-editor/commit/78a1259665c9d7057f3cf0066beef2604949345b))
# [1.8.0-beta.12](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.11...v1.8.0-beta.12) (2026-07-20)
### Bug Fixes
* **adapters:** tag 非法 type 回退为默认主题 ([90492ad](https://github.com/Tencent/tmagic-editor/commit/90492ad3eac82f052e99e7a286772a3feac288f7))
* **core:** 在 initNode 前校验 config.items 为数组 ([aa71fe9](https://github.com/Tencent/tmagic-editor/commit/aa71fe9ddd011e98b40af04e898bdf79263e0e89))
* **editor:** 为 FloatingBox 拖拽遮罩添加失焦等兜底清理,避免残留导致编辑器无法点击 ([4a893d3](https://github.com/Tencent/tmagic-editor/commit/4a893d35df354cfedce8e57b0deaaa0c535f8fba))
* **editor:** 修复 FloatingBox 拖动时鼠标进入 iframe 区域事件丢失 ([284be0d](https://github.com/Tencent/tmagic-editor/commit/284be0d276d90fe58cdcd94372dbacb476fd919c))
* **editor:** 修复 Monaco 编辑器卸载时容器失效与 Canceled 未处理异常 ([4a15da2](https://github.com/Tencent/tmagic-editor/commit/4a15da21083a30d2590a03fe68c488c71ec44eb2))
* **editor:** 修正 StyleSetter 字段 prop 取值逻辑 ([e5e0da3](https://github.com/Tencent/tmagic-editor/commit/e5e0da37852c78728e6506980dc2de6928dd571c))
* **editor:** 延迟 FloatingBox 拖拽遮罩到实际位移时显示,修复关闭按钮点击失效 ([9aa251c](https://github.com/Tencent/tmagic-editor/commit/9aa251ce57e36120a1f6c3b4835ba3bcd604448d))
* **form,editor:** 完善表单校验与样式属性匹配 ([ef66e8a](https://github.com/Tencent/tmagic-editor/commit/ef66e8a59863f9ffeb7292adcb314e5620c238ef))
* **form:** radio group disabled不生效 ([42d45be](https://github.com/Tencent/tmagic-editor/commit/42d45bed187393ae02a1fef1df5d46b114f198a8))
### Features
* **editor:** 扩展 customContentMenu 支持 getTarget 获取右键目标 ([bbe73aa](https://github.com/Tencent/tmagic-editor/commit/bbe73aae6437cabc8cfada24367c3d969687faa9))
* **editor:** 支持自定义历史对比/回滚弹窗内表单组件的尺寸 ([605f07c](https://github.com/Tencent/tmagic-editor/commit/605f07c8fe24fd67de22f5cb9adfdb151d3b8772))
* **editor:** 支持通过 uiService 配置列布局最小宽度 ([3c21f45](https://github.com/Tencent/tmagic-editor/commit/3c21f45e88dfe7c61552c704bd9cbd09215017fe))
* **editor:** 新增属性配置表单校验联动能力 ([0e46692](https://github.com/Tencent/tmagic-editor/commit/0e4669261fe50acaa8f72203dbc342e6eff324e6))
* **editor:** 注册编辑器字段内置 typeMatch 校验规则并补充文档 ([1c67b5e](https://github.com/Tencent/tmagic-editor/commit/1c67b5e77b726ded63b12d761faff37e97563ff2))
* **form,editor:** 新增 validateForm 校验能力并接入源码保存校验联动 ([d07e48b](https://github.com/Tencent/tmagic-editor/commit/d07e48bb101a3833b57b97001af3f053e2392c7a))
* **form:** 支持 rules.typeMatch 按字段 type 校验值类型与选项匹配 ([734e6a8](https://github.com/Tencent/tmagic-editor/commit/734e6a8ee6f9a948273c25461556eb4f54adc1ee))
* **table:** 为操作列新增 Popconfirm 二次确认能力 ([9e8272b](https://github.com/Tencent/tmagic-editor/commit/9e8272b521784265f18007e11f94d2ae5ee3faba))
* **table:** 抽取 ActionPopconfirm 并优化 sub-actions 展开交互 ([b183e71](https://github.com/Tencent/tmagic-editor/commit/b183e71f642fba84c82b0e8e63bcb03e0d27e610))
* **table:** 操作列支持 sub-actions 更多菜单并抽取 ActionButton 组件 ([78a1259](https://github.com/Tencent/tmagic-editor/commit/78a1259665c9d7057f3cf0066beef2604949345b))
# [1.8.0-beta.11](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.10...v1.8.0-beta.11) (2026-06-29)
### Features
* **editor:** support custom width and initial diff mode for history diff dialog ([83f4e52](https://github.com/Tencent/tmagic-editor/commit/83f4e528450be3971eea25b59dab126e04ac4df4))
* **form:** add debug mode to submitForm ([4330738](https://github.com/Tencent/tmagic-editor/commit/4330738b9f3d91c728826f5a74129cf1febbcf48))
# [1.8.0-beta.10](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.9...v1.8.0-beta.10) (2026-06-26)
### Bug Fixes
* **vue-runtime-help:** handle remove when root is not initialized ([af19736](https://github.com/Tencent/tmagic-editor/commit/af197363cfaa5c555758c19354a0b3a5031b973e))
### Features
* **editor:** add unified change events ([7bbb1f2](https://github.com/Tencent/tmagic-editor/commit/7bbb1f24c0f7180f301f573de1b916bfd0a35152))
* **editor:** refine history diff defaults ([f7afed6](https://github.com/Tencent/tmagic-editor/commit/f7afed66aaa8f57cdde02f0e25cca1dab825e7c0))
* **form:** support useFieldTextInError option ([fd359e4](https://github.com/Tencent/tmagic-editor/commit/fd359e493fdf205126957799a4a228f31267f3c2))
# [1.8.0-beta.9](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.8...v1.8.0-beta.9) (2026-06-25)
### Features
* **editor:** 优化历史记录对比与回滚流程 ([ce65b18](https://github.com/Tencent/tmagic-editor/commit/ce65b18dbb2976db484ab35a98479858b4c426e4))
* 导出 componentListService 和 keybindingService ([40acb5b](https://github.com/Tencent/tmagic-editor/commit/40acb5b34cb3b492bc5234c75bde4fbf8e787be2))
# [1.8.0-beta.8](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.7...v1.8.0-beta.8) (2026-06-23)
### Bug Fixes
* **playground:** 调整初始化时序避免历史恢复被覆盖 ([bfdaf2b](https://github.com/Tencent/tmagic-editor/commit/bfdaf2b244dfe7387cac5b399a84ccd614201e73))
### Features
* **editor:** 历史记录支持展示操作人 ([1ade61d](https://github.com/Tencent/tmagic-editor/commit/1ade61d62e61c86fcff2033a3dd57c533ce4241c))
# [1.8.0-beta.7](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.6...v1.8.0-beta.7) (2026-06-17)
### Bug Fixes
* **editor:** sidebar 侧栏项 key 与 SideItemKey 常量对齐 ([8933c46](https://github.com/Tencent/tmagic-editor/commit/8933c46b73525aa2aeca81aeea88610539a218d9))
* **editor:** 修复历史记录面板分组与展示逻辑 ([bbf79fd](https://github.com/Tencent/tmagic-editor/commit/bbf79fd6df505351bc28d63bea1a5c0cd16a5984))
* **editor:** 恢复新增组件在当前视窗中垂直居中 ([24b9b34](https://github.com/Tencent/tmagic-editor/commit/24b9b34f6569ed671edaf4e9f14622597688d72c))
* **editor:** 数据源配置面板默认激活「数据定义」tab ([95769d6](https://github.com/Tencent/tmagic-editor/commit/95769d6ae05d6171366ad5995f6e8ce79491d5cd))
# [1.8.0-beta.6](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.5...v1.8.0-beta.6) (2026-06-15)
### Bug Fixes
* **editor:** 仅在使用 left/top 定位时修正节点位置 ([27fac02](https://github.com/Tencent/tmagic-editor/commit/27fac02e994b69417622d009cb69037d99eef10a))
* **editor:** 完善 fixed 与 absolute 定位切换逻辑 ([1298104](https://github.com/Tencent/tmagic-editor/commit/1298104732de1074ecc1398777459dd2fcbb74f7))
* **playground:** 修复历史恢复后加载 DSL 时缺少页面基线记录 ([4a82891](https://github.com/Tencent/tmagic-editor/commit/4a8289137b539a6597d1b978418bb9901392e5c3))
### Features
* **editor:** 增强 *AndGetHistoryId 与 revertById 的 historyIds 能力 ([6960bd5](https://github.com/Tencent/tmagic-editor/commit/6960bd50e16fe612e486eb78af7630642f5e6c8f))
* **editor:** 数据源与代码块历史记录不再合并相邻操作 ([89cef4e](https://github.com/Tencent/tmagic-editor/commit/89cef4e9a9b1ffb11ab72c33e8b283bf59377f47))
* **editor:** 查看/编辑时自动切换 sidebar 并定位到对应配置 ([ffcc734](https://github.com/Tencent/tmagic-editor/commit/ffcc7341021da525263819cb6559cf20c2afea6a))
* **editor:** 边框方向有配置时显示高亮状态 ([b35132e](https://github.com/Tencent/tmagic-editor/commit/b35132e93eba6136990ba82912e53887c17c0cc1))
# [1.8.0-beta.5](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.4...v1.8.0-beta.5) (2026-06-11) # [1.8.0-beta.5](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.4...v1.8.0-beta.5) (2026-06-11)

View File

@ -111,10 +111,6 @@ export default defineConfig({
text: '@tmagic/form', text: '@tmagic/form',
link: '/guide/advanced/tmagic-form.md', link: '/guide/advanced/tmagic-form.md',
}, },
{
text: '属性配置表单校验联动',
link: '/guide/advanced/prop-form-validate.md',
},
], ],
}, },
{ {
@ -492,15 +488,6 @@ export default defineConfig({
} }
] ]
}, },
{
text: '表单校验',
items: [
{
text: '表单校验',
link: '/form-config/rules.md',
}
]
},
{ {
text: '表单对比', text: '表单对比',
items: [ items: [

View File

@ -162,7 +162,7 @@
} }
&:hover { &:hover {
color: var(--el-color-primary,#409eff);; color: #409eff;
background-color: #f9fafc; background-color: #f9fafc;
} }

View File

@ -1,7 +1,7 @@
# codeBlockService方法 # codeBlockService方法
写入历史栈的方法([setCodeDslById](#setcodedslbyid)、[setCodeDslByIdSync](#setcodedslbyidsync)、[deleteCodeDslByIds](#deletecodedslbyids) 等)的 `options` 支持 写入历史栈的方法([setCodeDslById](#setcodedslbyid)、[setCodeDslByIdSync](#setcodedslbyidsync)、[deleteCodeDslByIds](#deletecodedslbyids) 等)的 `options` 支持
[historyDescription / historySource](./editorServiceMethods.md#历史记录相关-options),会透传到 `historyService.push('codeBlock', step, id)` 入栈记录`historyDescription` / `source` 字段。 [historyDescription / historySource](./editorServiceMethods.md#历史记录相关-options),会透传到 `historyService.pushCodeBlock` `historyDescription` / `source` 字段。
## setCodeDsl ## setCodeDsl
@ -88,8 +88,8 @@
同步版本的 [setCodeDslById](#setcodedslbyid),并会触发 `addOrUpdate` 事件 同步版本的 [setCodeDslById](#setcodedslbyid),并会触发 `addOrUpdate` 事件
::: tip ::: tip
写入成功时(`force=false` 且同 id 已存在的跳过场景除外)会自动调用 `historyService.push('codeBlock', step, id)` 写入成功时(`force=false` 且同 id 已存在的跳过场景除外)会自动调用 `historyService.pushCodeBlock`
把本次变更入历史栈,参见 [historyService.push](./historyServiceMethods.md#push)。 把本次变更入历史栈,参见 [historyService.pushCodeBlock](./historyServiceMethods.md#pushcodeblock)。
传入的 `changeRecords` 会一同写进 step撤销/重做时调用方可据此按 `propPath` 局部回放。 传入的 `changeRecords` 会一同写进 step撤销/重做时调用方可据此按 `propPath` 局部回放。
传入 `doNotPushHistory: true` 可跳过写入历史栈,常用于批量导入、外部同步等非用户操作场景。 传入 `doNotPushHistory: true` 可跳过写入历史栈,常用于批量导入、外部同步等非用户操作场景。
::: :::
@ -231,8 +231,8 @@
在dsl数据源中删除指定id的代码块每删除一个会触发一次 `remove` 事件 在dsl数据源中删除指定id的代码块每删除一个会触发一次 `remove` 事件
::: tip ::: tip
对每个实际存在并被删除的代码块,会自动调用 `historyService.push('codeBlock', step, id)` 入栈一条 对每个实际存在并被删除的代码块,会自动调用 `historyService.pushCodeBlock` 入栈一条
`newSchema=null` 的删除记录;不存在的 id 不会入历史。传入 `doNotPushHistory: true` 也可显式跳过写入历史栈。 `newContent=null` 的删除记录;不存在的 id 不会入历史。传入 `doNotPushHistory: true` 也可显式跳过写入历史栈。
::: :::
## setCodeDslByIdAndGetHistoryId ## setCodeDslByIdAndGetHistoryId
@ -240,12 +240,11 @@
- **参数:** 同 [setCodeDslById](#setcodedslbyid) - **参数:** 同 [setCodeDslById](#setcodedslbyid)
- **返回:** - **返回:**
- {`Promise<`[`DslOpWithHistoryIdsResult<void>`](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)`>`} - {`Promise<string | null>`} 本次写入历史记录的 uuid未写入历史`doNotPushHistory: true` 等)时返回 `null`
- `historyIds`:本次写入历史记录的 uuid 列表;未写入历史(`doNotPushHistory: true` 等)时为 `[]`
- **详情:** - **详情:**
与 [setCodeDslById](#setcodedslbyid) 行为完全一致,并在返回值中额外提供 `historyIds`,可用于精确引用 / 定位该条历史记录。 与 [setCodeDslById](#setcodedslbyid) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`,可用于精确引用 / 定位该条历史记录。
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。 参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
- **示例:** - **示例:**
@ -253,11 +252,11 @@
```js ```js
import { codeBlockService } from "@tmagic/editor"; import { codeBlockService } from "@tmagic/editor";
const { historyIds } = await codeBlockService.setCodeDslByIdAndGetHistoryId("code_1234", { const historyId = await codeBlockService.setCodeDslByIdAndGetHistoryId("code_1234", {
name: "代码块1", name: "代码块1",
content: "() => {}", content: "() => {}",
}); });
console.log(historyIds); // 本次变更对应的历史记录 uuid 列表,或 [] console.log(historyId); // 本次变更对应的历史记录 uuid或 null
``` ```
## setCodeDslByIdSyncAndGetHistoryId ## setCodeDslByIdSyncAndGetHistoryId
@ -265,46 +264,44 @@ console.log(historyIds); // 本次变更对应的历史记录 uuid 列表,或
- **参数:** 同 [setCodeDslByIdSync](#setcodedslbyidsync) - **参数:** 同 [setCodeDslByIdSync](#setcodedslbyidsync)
- **返回:** - **返回:**
- {[`DslOpWithHistoryIdsResult<void>`](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)} - {`string | null`} 本次写入历史记录的 uuid未写入历史`doNotPushHistory: true`、或 `force=false` 跳过等)时返回 `null`
- `historyIds`:本次写入历史记录的 uuid 列表;未写入历史(`doNotPushHistory: true`、或 `force=false` 跳过等)时为 `[]`
- **详情:** - **详情:**
与 [setCodeDslByIdSync](#setcodedslbyidsync) 行为完全一致(同步),并在返回值中额外提供 `historyIds` 与 [setCodeDslByIdSync](#setcodedslbyidsync) 行为完全一致(同步),仅把返回值换成本次写入历史记录的 `uuid`
## deleteCodeDslByIdsAndGetHistoryId ## deleteCodeDslByIdsAndGetHistoryId
- **参数:** 同 [deleteCodeDslByIds](#deletecodedslbyids) - **参数:** 同 [deleteCodeDslByIds](#deletecodedslbyids)
- **返回:** - **返回:**
- {`Promise<`[`DslOpWithHistoryIdsResult<void>`](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)`>`} - {`Promise<string[]>`} 本次写入的全部历史记录 uuid按删除顺序未写入任何历史时返回空数组 `[]`
- `historyIds`:本次写入的全部历史记录 uuid按删除顺序未写入任何历史时为 `[]`
- **详情:** - **详情:**
与 [deleteCodeDslByIds](#deletecodedslbyids) 行为完全一致。由于一次可删除多个代码块、会产生多条历史记录,因此使用 `historyIds` 数组(每条删除记录一个 uuid不存在的 id 不会入历史,也不会出现在 `historyIds` 中。 与 [deleteCodeDslByIds](#deletecodedslbyids) 行为完全一致。由于一次可删除多个代码块、会产生多条历史记录,因此返回的是 uuid 数组(每条删除记录一个 uuid不存在的 id 不会入历史,也不会出现在返回数组中。
- **示例:** - **示例:**
```js ```js
import { codeBlockService } from "@tmagic/editor"; import { codeBlockService } from "@tmagic/editor";
const { historyIds } = await codeBlockService.deleteCodeDslByIdsAndGetHistoryId(["code_1", "code_2"]); const historyIds = await codeBlockService.deleteCodeDslByIdsAndGetHistoryId(["code_1", "code_2"]);
console.log(historyIds); // ['xxxx', 'yyyy'],或 [] console.log(historyIds); // ['xxxx', 'yyyy'],或 []
``` ```
## revertById ## revertById
- **参数:** - **参数:**
- `{string[]}` uuids 目标历史记录的 uuid 列表(通常由 [setCodeDslByIdAndGetHistoryId](#setcodedslbyidandgethistoryid) 等方法返回`historyIds` - `{string}` uuid 目标历史记录的 uuid通常由 [setCodeDslByIdAndGetHistoryId](#setcodedslbyidandgethistoryid) 等方法返回)
- **返回:** - **返回:**
- {`Promise<(`CodeBlockStepValue` | null)[]>`} 与入参同序的回滚结果列表,某项失败时为 `null` - {`Promise<CodeBlockStepValue | null>`} 反向应用后产生的新 step找不到对应 uuid / 该步未应用时返回 `null`
- **详情:** - **详情:**
通过历史记录 uuid「回滚」代码块历史步骤类 git revert 语义),语义同按 `(id, index)` 回滚, 通过历史记录 uuid「回滚」某条代码块历史步骤(类 git revert 语义),语义同按 `(id, index)` 回滚,
仅无需调用方再传 `codeBlockId``index`。**按数组顺序依次回滚** 仅无需调用方再传 `codeBlockId``index`:内部会按 uuid 在全部代码块栈中定位对应步骤后再回滚
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。 参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
- **示例:** - **示例:**
@ -312,9 +309,9 @@ console.log(historyIds); // ['xxxx', 'yyyy'],或 []
```js ```js
import { codeBlockService } from "@tmagic/editor"; import { codeBlockService } from "@tmagic/editor";
const { historyIds } = await codeBlockService.setCodeDslByIdAndGetHistoryId("code_1234", { name: "代码块1" }); const historyId = await codeBlockService.setCodeDslByIdAndGetHistoryId("code_1234", { name: "代码块1" });
if (historyIds.length) { if (historyId) {
await codeBlockService.revertById(historyIds); await codeBlockService.revertById(historyId);
} }
``` ```
@ -370,7 +367,7 @@ if (codeBlockService.canUndo("code_1234")) {
- **详情:** - **详情:**
当前指定代码块是否可撤销,等价于 `historyService.canUndo('codeBlock', id)`。 当前指定代码块是否可撤销,等价于 `historyService.canUndoCodeBlock(id)`。
## canRedo ## canRedo
@ -382,7 +379,7 @@ if (codeBlockService.canUndo("code_1234")) {
- **详情:** - **详情:**
当前指定代码块是否可重做,等价于 `historyService.canRedo('codeBlock', id)`。 当前指定代码块是否可重做,等价于 `historyService.canRedoCodeBlock(id)`。
## setParamsColConfig ## setParamsColConfig

View File

@ -311,8 +311,8 @@ dataSourceService.setFormMethod("http", [
添加一个数据源如果配置中没有id或id已存在会自动生成新的id 添加一个数据源如果配置中没有id或id已存在会自动生成新的id
::: tip ::: tip
添加成功会自动调用 `historyService.push('dataSource', step, id)` 入栈一条 `oldSchema=null` 的新增记录, 添加成功会自动调用 `historyService.pushDataSource` 入栈一条 `oldSchema=null` 的新增记录,
参见 [historyService.push](./historyServiceMethods.md#push)。 参见 [historyService.pushDataSource](./historyServiceMethods.md#pushdatasource)。
传入 `doNotPushHistory: true` 可跳过写入历史栈,常用于批量导入、外部同步等非用户操作场景。 传入 `doNotPushHistory: true` 可跳过写入历史栈,常用于批量导入、外部同步等非用户操作场景。
::: :::
@ -355,7 +355,7 @@ console.log(newDs.id); // 自动生成的id
更新数据源 更新数据源
::: tip ::: tip
更新成功会自动调用 `historyService.push('dataSource', step, id)` 入栈一条 `oldSchema` / `newSchema` 更新成功会自动调用 `historyService.pushDataSource` 入栈一条 `oldSchema` / `newSchema`
均为对应 schema 的更新记录,传入的 `changeRecords` 也会一并写进 step撤销/重做时调用方可据此按 均为对应 schema 的更新记录,传入的 `changeRecords` 也会一并写进 step撤销/重做时调用方可据此按
`propPath` 局部回放,缺省才退化为整 schema 替换。传入 `doNotPushHistory: true` 可跳过写入历史栈。 `propPath` 局部回放,缺省才退化为整 schema 替换。传入 `doNotPushHistory: true` 可跳过写入历史栈。
::: :::
@ -394,7 +394,7 @@ console.log(updatedDs);
删除指定id的数据源 删除指定id的数据源
::: tip ::: tip
对实际存在的数据源会自动调用 `historyService.push('dataSource', step, id)` 入栈一条 `newSchema=null` 对实际存在的数据源会自动调用 `historyService.pushDataSource` 入栈一条 `newSchema=null`
的删除记录;不存在的 id 不会入历史。传入 `doNotPushHistory: true` 也可显式跳过写入历史栈。 的删除记录;不存在的 id 不会入历史。传入 `doNotPushHistory: true` 也可显式跳过写入历史栈。
::: :::
@ -411,13 +411,11 @@ dataSourceService.remove("ds_123");
- **参数:** 同 [add](#add) - **参数:** 同 [add](#add)
- **返回:** - **返回:**
- {[`DslOpWithHistoryIdsResult<DataSourceSchema>`](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)} - {`string` | null} 本次写入历史记录的 uuid未写入历史`doNotPushHistory: true` 等)时返回 `null`
- `result`:同 [add](#add) 的返回值(新增的数据源配置)
- `historyIds`:本次写入历史记录的 uuid 列表;未写入历史(`doNotPushHistory: true` 等)时为 `[]`
- **详情:** - **详情:**
与 [add](#add) 行为完全一致,并在返回值中额外提供 `historyIds`,可用于精确引用 / 定位该条历史记录。 与 [add](#add) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`,可用于精确引用 / 定位该条历史记录。
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。 参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
- **示例:** - **示例:**
@ -425,13 +423,12 @@ dataSourceService.remove("ds_123");
```js ```js
import { dataSourceService } from "@tmagic/editor"; import { dataSourceService } from "@tmagic/editor";
const { result, historyIds } = dataSourceService.addAndGetHistoryId({ const historyId = dataSourceService.addAndGetHistoryId({
type: "http", type: "http",
title: "用户信息", title: "用户信息",
url: "/api/user", url: "/api/user",
}); });
console.log(result); // 新增的数据源配置 console.log(historyId); // 本次新增对应的历史记录 uuid或 null
console.log(historyIds); // 本次新增对应的历史记录 uuid 列表,或 []
``` ```
## updateAndGetHistoryId ## updateAndGetHistoryId
@ -439,38 +436,35 @@ console.log(historyIds); // 本次新增对应的历史记录 uuid 列表,或
- **参数:** 同 [update](#update) - **参数:** 同 [update](#update)
- **返回:** - **返回:**
- {[`DslOpWithHistoryIdsResult<DataSourceSchema>`](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)} - {`string` | null} 本次写入历史记录的 uuid未写入历史时返回 `null`
- `result`:同 [update](#update) 的返回值(更新后的数据源配置)
- `historyIds`:本次写入历史记录的 uuid 列表;未写入历史时为 `[]`
- **详情:** - **详情:**
与 [update](#update) 行为完全一致,并在返回值中额外提供 `historyIds` 与 [update](#update) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## removeAndGetHistoryId ## removeAndGetHistoryId
- **参数:** 同 [remove](#remove) - **参数:** 同 [remove](#remove)
- **返回:** - **返回:**
- {[`DslOpWithHistoryIdsResult<void>`](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)} - {`string` | null} 本次写入历史记录的 uuid删除的 id 不存在或未写入历史时返回 `null`
- `historyIds`:本次写入历史记录的 uuid 列表;删除的 id 不存在或未写入历史时为 `[]`
- **详情:** - **详情:**
与 [remove](#remove) 行为完全一致,并在返回值中额外提供 `historyIds` 与 [remove](#remove) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## revertById ## revertById
- **参数:** - **参数:**
- `{string[]}` uuids 目标历史记录的 uuid 列表(通常由 [addAndGetHistoryId](#addandgethistoryid) 等方法返回`historyIds` - `{string}` uuid 目标历史记录的 uuid通常由 [addAndGetHistoryId](#addandgethistoryid) 等方法返回)
- **返回:** - **返回:**
- {(`DataSourceStepValue` | null)[]} 与入参同序的回滚结果列表,某项失败时为 `null` - {`DataSourceStepValue` | null} 反向应用后产生的新 step找不到对应 uuid / 该步未应用时返回 `null`
- **详情:** - **详情:**
通过历史记录 uuid「回滚」数据源历史步骤类 git revert 语义),语义同按 `(id, index)` 回滚, 通过历史记录 uuid「回滚」某条数据源历史步骤(类 git revert 语义),语义同按 `(id, index)` 回滚,
仅无需调用方再传 `dataSourceId``index`。**按数组顺序依次回滚** 仅无需调用方再传 `dataSourceId``index`:内部会按 uuid 在全部数据源栈中定位对应步骤后再回滚
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。 参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
- **示例:** - **示例:**
@ -478,9 +472,9 @@ console.log(historyIds); // 本次新增对应的历史记录 uuid 列表,或
```js ```js
import { dataSourceService } from "@tmagic/editor"; import { dataSourceService } from "@tmagic/editor";
const { historyIds } = dataSourceService.addAndGetHistoryId({ type: "http", title: "用户信息" }); const historyId = dataSourceService.addAndGetHistoryId({ type: "http", title: "用户信息" });
if (historyIds.length) { if (historyId) {
dataSourceService.revertById(historyIds); dataSourceService.revertById(historyId);
} }
``` ```
@ -579,7 +573,7 @@ if (dataSourceService.canUndo("ds_123")) {
- **详情:** - **详情:**
当前指定数据源是否可撤销,等价于 `historyService.canUndo('dataSource', id)`。 当前指定数据源是否可撤销,等价于 `historyService.canUndoDataSource(id)`。
## canRedo ## canRedo
@ -591,7 +585,7 @@ if (dataSourceService.canUndo("ds_123")) {
- **详情:** - **详情:**
当前指定数据源是否可重做,等价于 `historyService.canRedo('dataSource', id)`。 当前指定数据源是否可重做,等价于 `historyService.canRedoDataSource(id)`。
## copyWithRelated ## copyWithRelated

View File

@ -87,35 +87,3 @@
- **详情:** 历史记录改变,[editorService.redo()editorService.undo()](./editorServiceMethods.md#undo)后触发 - **详情:** 历史记录改变,[editorService.redo()editorService.undo()](./editorServiceMethods.md#undo)后触发
- **事件回调函数:** `(data: MPage | MPageFragment) => void` - **事件回调函数:** `(data: MPage | MPageFragment) => void`
## change
- **详情:** DSL 发生变更后统一触发,免去分别监听 `add` / `remove` / `update` / `move-layer` / `drag-to`。在 [editorService.add()](./editorServiceMethods.md#add)、[editorService.remove()](./editorServiceMethods.md#remove)、[editorService.update()](./editorServiceMethods.md#update)、[editorService.moveLayer()](./editorServiceMethods.md#movelayer)、[editorService.dragTo()](./editorServiceMethods.md#dragto) 后触发。
回调参数 `event` 通过 `type` 区分操作类型,并携带本次变更的节点列表 `data`,每项包含变更的 `node` 及其所属的 `page`(可能为 `null`)。`move-layer` 额外携带层级偏移 `offset``drag-to` 额外携带目标位置 `targetIndex` / `targetParent`
::: warning 注意
撤销 / 重做(`undo` / `redo`)内部同样会经由 `add` / `remove` / `update` 触发本事件;如需区分「用户操作」与「撤销重做」,请配合 [history-change](#history-change) 事件判断。
:::
- **事件回调函数:** `(event: EditorChangeEvent) => void`
::: details 查看 EditorChangeEvent 类型定义
<<< @/../packages/editor/src/type.ts#EditorChangeEvent{ts}
:::
## invalid-node-change
- **详情:** 节点校验错误状态发生变化时触发(记录 / 清除错误、删除节点、整体替换 root、撤销重做还原错误标记等场景均会触发在 [editorService.setInvalidNode()](./editorServiceMethods.md#setinvalidnode)、[editorService.deleteInvalidNode()](./editorServiceMethods.md#deleteinvalidnode)、[editorService.resetInvalidNodeId()](./editorServiceMethods.md#resetinvalidnodeid) 及 DSL 操作 / 撤销重做内部调用后触发。
携带当前完整的错误 Map供非响应式消费方如自定义工具栏订阅实现组件树标红、保存拦截等。需响应式读取如组件树节点内容请直接读取 `editorService.get('invalidNodeIds')`
- **事件回调函数:** `(invalidNodeIds: Map<Id, NodeInvalidInfo>) => void`
::: details 查看 NodeInvalidInfo 及关联类型定义
<<< @/../packages/editor/src/type.ts#NodeInvalidInfo{ts}
<<< @/../packages/editor/src/type.ts#NodeInvalidSource{ts}
<<< @/../packages/schema/src/index.ts#Id{ts}
:::

View File

@ -16,39 +16,37 @@
每条历史记录入栈时都会自动生成一个唯一标识 `uuid`(见 [StepValue](#undo)),可用于精确引用 / 定位某一条历史记录(如埋点、回滚、跨端同步等)。 每条历史记录入栈时都会自动生成一个唯一标识 `uuid`(见 [StepValue](#undo)),可用于精确引用 / 定位某一条历史记录(如埋点、回滚、跨端同步等)。
DSL 操作方法(`add` / `remove` / `update` 等)默认返回操作结果(节点 / 节点集合 / void不会返回 `uuid`。若需要同时拿到原操作结果与本次写入历史记录的 `uuid`,可改用对应的 `*AndGetHistoryId` 方法:它们与原方法行为完全一致,返回值类型为 [`DslOpWithHistoryIdsResult<T>`](#历史记录-uuid-与-andgethistoryid)`result` 为原方法返回值,`historyIds` 为本次写入的 uuid 列表)。当本次操作未写入历史(`doNotPushHistory: true`、无实际变更或提前返回)时 `historyIds``[]`;单次操作通常返回含一个 uuid 的数组 DSL 操作方法(`add` / `remove` / `update` 等)默认返回操作结果(节点 / 节点集合 / void不会返回 `uuid`。若需要拿到本次写入历史记录的 `uuid`,可改用对应的 `*AndGetHistoryId` 方法:它们与原方法行为完全一致,仅把返回值换成本次写入历史记录的 `uuid``string`)。当本次操作未写入历史(`doNotPushHistory: true`、无实际变更或提前返回)时返回 `null`
| 原方法 | 取 uuid 的方法 | 返回值 | | 原方法 | 取 uuid 的方法 | 返回值 |
| --- | --- | --- | | --- | --- | --- |
| [add](#add) | [addAndGetHistoryId](#addandgethistoryid) | `Promise<DslOpWithHistoryIdsResult<MNode \| MNode[]>>` | | [add](#add) | [addAndGetHistoryId](#addandgethistoryid) | `Promise<string \| null>` |
| [remove](#remove) | [removeAndGetHistoryId](#removeandgethistoryid) | `Promise<DslOpWithHistoryIdsResult<void>>` | | [remove](#remove) | [removeAndGetHistoryId](#removeandgethistoryid) | `Promise<string \| null>` |
| [update](#update) | [updateAndGetHistoryId](#updateandgethistoryid) | `Promise<DslOpWithHistoryIdsResult<MNode \| MNode[]>>` | | [update](#update) | [updateAndGetHistoryId](#updateandgethistoryid) | `Promise<string \| null>` |
| [moveLayer](#movelayer) | [moveLayerAndGetHistoryId](#movelayerandgethistoryid) | `Promise<DslOpWithHistoryIdsResult<void>>` | | [moveLayer](#movelayer) | [moveLayerAndGetHistoryId](#movelayerandgethistoryid) | `Promise<string \| null>` |
| [moveToContainer](#movetocontainer) | [moveToContainerAndGetHistoryId](#movetocontainerandgethistoryid) | `Promise<DslOpWithHistoryIdsResult<MNode \| MNode[]>>` | | [moveToContainer](#movetocontainer) | [moveToContainerAndGetHistoryId](#movetocontainerandgethistoryid) | `Promise<string \| null>` |
| [dragTo](#dragto) | [dragToAndGetHistoryId](#dragtoandgethistoryid) | `Promise<DslOpWithHistoryIdsResult<void>>` | | [dragTo](#dragto) | [dragToAndGetHistoryId](#dragtoandgethistoryid) | `Promise<string \| null>` |
[dataSourceService](./dataSourceServiceMethods.md) / [codeBlockService](./codeBlockServiceMethods.md) 也提供了同名约定的 `*AndGetHistoryId` 方法,返回值约定相同(`result` + `historyIds` [dataSourceService](./dataSourceServiceMethods.md) / [codeBlockService](./codeBlockServiceMethods.md) 也提供了同名约定的 `*AndGetHistoryId` 方法。
拿到 `uuid` 后,可在需要时按 uuid「回滚」对应的历史记录类 git revert 语义,详见[历史记录面板](../../guide/advanced/history-list.md))。相比按 index 回滚uuid 不会随栈内步骤增删而变化,更适合业务侧持有引用后再回滚: 拿到 `uuid` 后,可在需要时按 uuid「回滚」对应的历史记录类 git revert 语义,详见[历史记录面板](../../guide/advanced/history-list.md))。相比按 index 回滚uuid 不会随栈内步骤增删而变化,更适合业务侧持有引用后再回滚:
- 页面:[editorService.revertPageStepById(uuids)](#revertpagestepbyid) - 页面:[editorService.revertPageStepById(uuid)](#revertpagestepbyid)
- 数据源:[dataSourceService.revertById(uuids)](./dataSourceServiceMethods.md#revertbyid) - 数据源:[dataSourceService.revertById(uuid)](./dataSourceServiceMethods.md#revertbyid)
- 代码块:[codeBlockService.revertById(uuids)](./codeBlockServiceMethods.md#revertbyid) - 代码块:[codeBlockService.revertById(uuid)](./codeBlockServiceMethods.md#revertbyid)
::: details 查看 HistoryOpOptions / DslOpOptions / DslOpWithHistoryIdsResult / HistoryOpSource 类型定义 ::: details 查看 HistoryOpOptions / DslOpOptions / HistoryOpSource 类型定义
<<< @/../packages/editor/src/type.ts#HistoryOpOptions{ts} <<< @/../packages/editor/src/type.ts#HistoryOpOptions{ts}
<<< @/../packages/editor/src/type.ts#DslOpOptions{ts} <<< @/../packages/editor/src/type.ts#DslOpOptions{ts}
<<< @/../packages/editor/src/type.ts#DslOpWithHistoryIdsResult{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts} <<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
::: :::
## get ## get
- **参数:** - **参数:**
- `{'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength' | 'stage' | 'stageLoading' | 'disabledMultiSelect' | 'alwaysMultiSelect'} name` - `{'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength' | 'stage' | 'stageLoading' | 'disabledMultiSelect' | 'alwaysMultiSelect'} name`
- **返回:** - **返回:**
- `{any} value` - `{any} value`
@ -71,8 +69,6 @@ DSL 操作方法(`add` / `remove` / `update` 等)默认返回操作结果(
'modifiedNodeIds': 当前页面所有改动过的节点id 'modifiedNodeIds': 当前页面所有改动过的节点id
'invalidNodeIds': 校验失败的节点错误信息(`Map<Id, NodeInvalidInfo>`),供组件树标红提示与保存拦截读取
'pageLength': 所以页面个数 'pageLength': 所以页面个数
'pageFragmentLength': 页面片个数 'pageFragmentLength': 页面片个数
@ -95,7 +91,7 @@ const node = editorService.get("node");
## set ## set
- `{'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength' | 'stage' | 'stageLoading' | 'disabledMultiSelect' | 'alwaysMultiSelect'} name` - `{'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength' | 'stage' | 'stageLoading' | 'disabledMultiSelect' | 'alwaysMultiSelect'} name`
- `{any} value` - `{any} value`
- **详情:** - **详情:**
@ -512,10 +508,6 @@ editorService.highlight("text_123");
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false - `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options) - `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options) - `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
- `{Object}` invalidInfo 启用 [enablePropsFormValidate](./props.md#enablepropsformvalidate) 时,属性面板提交携带的校验错误信息,在写入历史记录之前落库,使历史快照与本次变更对齐
- `{Id}` id 节点 id
- `{'props' | 'style'}` source 错误来源:属性表单 / 样式表单
- `{string}` error 错误文案(可为含 `<br>` 的 HTML为空时表示清除该来源的错误记录
::: details 查看 ChangeRecord 类型定义 ::: details 查看 ChangeRecord 类型定义
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts} <<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
@ -746,26 +738,23 @@ alignCenter可以支持一次水平居中多个组件alignCenter是通过调
- **参数:** 同 [add](#add) - **参数:** 同 [add](#add)
- **返回:** - **返回:**
- {Promise<[`DslOpWithHistoryIdsResult<MNode | MNode[]>`](#历史记录-uuid-与-andgethistoryid)>} - {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- `result`:同 [add](#add) 的返回值(新增节点 / 节点数组)
- `historyIds`:本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid) 列表;未写入历史时为 `[]`
- **详情:** - **详情:**
与 [add](#add) 行为完全一致,并在返回值中额外提供 `historyIds`,见[历史记录 uuid 与 \*AndGetHistoryId](#历史记录-uuid-与-andgethistoryid) 与 [add](#add) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`,见[历史记录 uuid 与 \*AndGetHistoryId](#历史记录-uuid-与-andgethistoryid)
- **示例:** - **示例:**
```js ```js
import { editorService } from "@tmagic/editor"; import { editorService } from "@tmagic/editor";
const { result, historyIds } = await editorService.addAndGetHistoryId( const historyId = await editorService.addAndGetHistoryId(
{ type: "text", text: "hello" }, { type: "text", text: "hello" },
parent, parent,
{ historySource: "api" }, { historySource: "api" },
); );
console.log(result); // 新增节点 console.log(historyId); // 本次新增对应的历史记录 uuid或 null
console.log(historyIds); // 本次新增对应的历史记录 uuid 列表,或 []
``` ```
## removeAndGetHistoryId ## removeAndGetHistoryId
@ -773,74 +762,67 @@ console.log(historyIds); // 本次新增对应的历史记录 uuid 列表,或
- **参数:** 同 [remove](#remove) - **参数:** 同 [remove](#remove)
- **返回:** - **返回:**
- {Promise<[`DslOpWithHistoryIdsResult<void>`](#历史记录-uuid-与-andgethistoryid)>} - {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- `historyIds`:本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid) 列表;未写入历史时为 `[]`
- **详情:** - **详情:**
与 [remove](#remove) 行为完全一致,并在返回值中额外提供 `historyIds` 与 [remove](#remove) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## updateAndGetHistoryId ## updateAndGetHistoryId
- **参数:** 同 [update](#update) - **参数:** 同 [update](#update)
- **返回:** - **返回:**
- {Promise<[`DslOpWithHistoryIdsResult<MNode | MNode[]>`](#历史记录-uuid-与-andgethistoryid)>} - {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- `result`:同 [update](#update) 的返回值(更新后的节点 / 节点数组)
- `historyIds`:本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid) 列表;未写入历史时为 `[]`
- **详情:** - **详情:**
与 [update](#update) 行为完全一致,并在返回值中额外提供 `historyIds` 与 [update](#update) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## moveLayerAndGetHistoryId ## moveLayerAndGetHistoryId
- **参数:** 同 [moveLayer](#movelayer) - **参数:** 同 [moveLayer](#movelayer)
- **返回:** - **返回:**
- {Promise<[`DslOpWithHistoryIdsResult<void>`](#历史记录-uuid-与-andgethistoryid)>} - {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- `historyIds`:本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid) 列表;未写入历史时为 `[]`
- **详情:** - **详情:**
与 [moveLayer](#movelayer) 行为完全一致,并在返回值中额外提供 `historyIds` 与 [moveLayer](#movelayer) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## moveToContainerAndGetHistoryId ## moveToContainerAndGetHistoryId
- **参数:** 同 [moveToContainer](#movetocontainer) - **参数:** 同 [moveToContainer](#movetocontainer)
- **返回:** - **返回:**
- {Promise<[`DslOpWithHistoryIdsResult<MNode | MNode[]>`](#历史记录-uuid-与-andgethistoryid)>} - {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- `result`:同 [moveToContainer](#movetocontainer) 的返回值(移动后的节点 / 节点数组)
- `historyIds`:本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid) 列表;未写入历史时为 `[]`
- **详情:** - **详情:**
与 [moveToContainer](#movetocontainer) 行为完全一致,并在返回值中额外提供 `historyIds` 与 [moveToContainer](#movetocontainer) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## dragToAndGetHistoryId ## dragToAndGetHistoryId
- **参数:** 同 [dragTo](#dragto) - **参数:** 同 [dragTo](#dragto)
- **返回:** - **返回:**
- {Promise<[`DslOpWithHistoryIdsResult<void>`](#历史记录-uuid-与-andgethistoryid)>} - {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- `historyIds`:本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid) 列表;未写入历史时为 `[]`
- **详情:** - **详情:**
与 [dragTo](#dragto) 行为完全一致,并在返回值中额外提供 `historyIds` 与 [dragTo](#dragto) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## revertPageStepById ## revertPageStepById
- **参数:** - **参数:**
- `{string[]}` uuids 目标历史记录的 uuid 列表(通常由 `*AndGetHistoryId` 方法返回`historyIds` - `{string}` uuid 目标历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid)(通常由 `*AndGetHistoryId` 方法返回)
- **返回:** - **返回:**
- {Promise<(`StepValue` | null)[]>} 与入参同序的回滚结果列表,某项失败时为 `null` - {Promise<`StepValue` | null>} 反向应用后产生的新 step找不到对应 uuid / 该步未应用 / 反向失败时返回 `null`
- **详情:** - **详情:**
通过历史记录 uuid「回滚」当前页面的历史步骤类 git revert 语义):不移动游标、不丢弃任何步骤,而是把目标 step 的修改**反向应用为一条全新的步骤**压入栈顶。**按数组顺序依次回滚** 通过历史记录 uuid「回滚」当前页面的某条历史步骤(类 git revert 语义):不移动游标、不丢弃任何步骤,而是把目标 step 的修改**反向应用为一条全新的步骤**压入栈顶。语义与按 index 回滚一致,仅入参从 index 改为 uuid更适合业务侧持有引用后再回滚
::: tip ::: tip
`opType: 'update'` 的步骤必须携带 `changeRecords` 才支持回滚(否则只能整节点替换,会冲掉后续无关变更);未应用(已被撤销)的步骤无法回滚。 `opType: 'update'` 的步骤必须携带 `changeRecords` 才支持回滚(否则只能整节点替换,会冲掉后续无关变更);未应用(已被撤销)的步骤无法回滚。
@ -852,10 +834,12 @@ console.log(historyIds); // 本次新增对应的历史记录 uuid 列表,或
import { editorService } from "@tmagic/editor"; import { editorService } from "@tmagic/editor";
// 执行操作时拿到本次历史记录 uuid // 执行操作时拿到本次历史记录 uuid
const { historyIds } = await editorService.addAndGetHistoryId({ type: "text", text: "hello" }); const historyId = await editorService.addAndGetHistoryId({ type: "text", text: "hello" });
// 之后任意时机按 uuid 回滚(支持单个或整批 historyIds // 之后任意时机按 uuid 回滚该步骤
await editorService.revertPageStepById(historyIds); if (historyId) {
await editorService.revertPageStepById(historyId);
}
``` ```
## undo ## undo
@ -868,10 +852,6 @@ await editorService.revertPageStepById(historyIds);
::: details 查看 StepValue 及关联类型定义 ::: details 查看 StepValue 及关联类型定义
<<< @/../packages/editor/src/type.ts#StepValue{ts} <<< @/../packages/editor/src/type.ts#StepValue{ts}
<<< @/../packages/editor/src/type.ts#BaseStepValue{ts}
<<< @/../packages/editor/src/type.ts#StepExtra{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpType{ts} <<< @/../packages/editor/src/type.ts#HistoryOpType{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts} <<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
@ -893,10 +873,6 @@ await editorService.revertPageStepById(historyIds);
::: details 查看 StepValue 及关联类型定义 ::: details 查看 StepValue 及关联类型定义
<<< @/../packages/editor/src/type.ts#StepValue{ts} <<< @/../packages/editor/src/type.ts#StepValue{ts}
<<< @/../packages/editor/src/type.ts#BaseStepValue{ts}
<<< @/../packages/editor/src/type.ts#StepExtra{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpType{ts} <<< @/../packages/editor/src/type.ts#HistoryOpType{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts} <<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
@ -933,89 +909,6 @@ await editorService.revertPageStepById(historyIds);
重置当前记录的修改过的节点id记录通常用于保存之后 重置当前记录的修改过的节点id记录通常用于保存之后
## setInvalidNode
- **参数:**
- `{Id}` id 节点 id
- `{'props' | 'style'}` source 错误来源:属性表单(`props`/ 样式表单(`style`
- `{string}` message 错误文案(可能为包含 `<br>` 的 HTML
- **详情:**
记录(或覆盖)某个节点在指定来源上的校验错误信息,写入 `invalidNodeIds` 状态并发出 [invalid-node-change](#invalid-node-change) 事件。
属性表单与样式表单是两个独立的 `FormPanel` 且均指向同一节点,故以来源为键分别保存,避免某个面板校验通过时误清另一个面板记录的错误。节点视为存在错误当且仅当任一来源存在非空文本。
- **示例:**
```js
import { editorService } from "@tmagic/editor";
// 标记 text_123 的属性表单校验错误
editorService.setInvalidNode("text_123", "props", "标题不能为空");
```
## deleteInvalidNode
- **参数:**
- `{Id}` id 节点 id
- `{'props' | 'style'}` source 可选;指定来源则仅删除该来源错误,不传则删除该节点全部来源的错误
- **详情:**
删除节点的校验错误记录。仅当该来源被清空且另一来源也无错误时,节点整体错误记录才会被移除;随后发出 [invalid-node-change](#invalid-node-change) 事件。
## getInvalidNodeIds
- **返回:**
- {`Map<Id, NodeInvalidInfo>`} 当前存在校验错误的节点错误 Mapkey 为节点 id
- **详情:**
获取当前存在校验错误的节点错误 Map供组件树标红提示与保存拦截读取。
::: details 查看 NodeInvalidInfo 及关联类型定义
<<< @/../packages/editor/src/type.ts#NodeInvalidInfo{ts}
<<< @/../packages/editor/src/type.ts#NodeInvalidSource{ts}
<<< @/../packages/schema/src/index.ts#Id{ts}
:::
- **示例:**
```js
import { editorService } from "@tmagic/editor";
// 保存前检查是否存在校验错误的组件
const invalidNodeIds = editorService.getInvalidNodeIds();
if (invalidNodeIds.size > 0) {
const names = [...invalidNodeIds.keys()].map((id) => {
const node = editorService.getNodeById(id);
return node?.name ? `${node.name}(${id})` : `${id}`;
});
console.warn(`以下组件存在配置校验错误,请修复后再保存:${names.join("、")}`);
}
```
## getInvalidNodeInfo
- **参数:**
- `{Id}` id 节点 id
- **返回:**
- {`NodeInvalidInfo` | undefined} 指定节点的校验错误信息(含 `props` / `style` 来源的错误文案)
- **详情:**
获取指定节点的校验错误信息。
## resetInvalidNodeId
- **详情:**
清空全部校验错误记录(即 `invalidNodeIds` 状态),随后发出 [invalid-node-change](#invalid-node-change) 事件。
## resetState ## resetState
- **详情:** - **详情:**

View File

@ -1,18 +1,24 @@
# historyService事件 # historyService事件
## page-change
- **详情:** 页面切换
- **事件回调函数:** `(undoRedo: UndoRedo) => void`
::: details 查看 UndoRedo 类定义
<<< @/../packages/editor/src/utils/undo-redo.ts#UndoRedo{ts}
:::
## change ## change
- **详情:** 页面历史记录发生变化(`page` 类型 `push` / `undo` / `redo` 成功时触发;与 `code-block-history-change` / `data-source-history-change` 同构) - **详情:** 历史记录发生变化
- **事件回调函数:** `(pageId: Id, step: StepValue) => void` - **事件回调函数:** `(state: StepValue | null) => void`
::: details 查看 StepValue 及关联类型定义 ::: details 查看 StepValue 及关联类型定义
<<< @/../packages/editor/src/type.ts#StepValue{ts} <<< @/../packages/editor/src/type.ts#StepValue{ts}
<<< @/../packages/editor/src/type.ts#BaseStepValue{ts}
<<< @/../packages/editor/src/type.ts#StepExtra{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpType{ts} <<< @/../packages/editor/src/type.ts#HistoryOpType{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts} <<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
@ -23,36 +29,18 @@
::: :::
:::tip :::tip
回调签名已与其它历史类型统一为 `(id, step)`。当游标处于历史栈边界(已无法继续撤销 / 重做)时 `undo` / `redo` 返回 `null`,此时不会触发该事件。 当游标处于历史栈边界(已经无法继续撤销或重做)时,`UndoRedo.undo()` / `redo()` 返回 `null`,对应 `change` 回调收到的 `state``null`
:::
## marker-change
- **详情:** 通过 [`setMarker`](./historyServiceMethods.md#setmarker) 为某个历史栈种入 `initial` 基线时触发(适用于所有类型)
- **事件回调函数:** `(id: Id, marker: StepValue, stepType: HistoryStepType) => void`
## clear
- **详情:** 调用 [`clear`](./historyServiceMethods.md#clear) 清空历史栈时触发(适用于所有类型)
- **事件回调函数:** `(id: Id | undefined, stepType: HistoryStepType) => void`
:::tip
`id` 缺省(清空 `stepType` 下全部栈)时回调的 `id``undefined`
::: :::
## code-block-history-change ## code-block-history-change
- **详情:** 代码块历史记录发生变化(`push('codeBlock', step, codeBlockId)` / `undo('codeBlock', id)` / `redo('codeBlock', id)` 成功时触发) - **详情:** 代码块历史记录发生变化(`pushCodeBlock` / `undoCodeBlock` / `redoCodeBlock` 成功时触发)
- **事件回调函数:** `(codeBlockId: Id, step: CodeBlockStepValue) => void` - **事件回调函数:** `(codeBlockId: Id, step: CodeBlockStepValue) => void`
::: details 查看 CodeBlockStepValue 及关联类型定义 ::: details 查看 CodeBlockStepValue 及关联类型定义
<<< @/../packages/editor/src/type.ts#CodeBlockStepValue{ts} <<< @/../packages/editor/src/type.ts#CodeBlockStepValue{ts}
<<< @/../packages/editor/src/type.ts#BaseStepValue{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts} <<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
<<< @/../packages/schema/src/index.ts#CodeBlockContent{ts} <<< @/../packages/schema/src/index.ts#CodeBlockContent{ts}
@ -61,42 +49,40 @@
::: :::
:::tip :::tip
- 新增触发的 step 其 diff 项 `oldSchema` 为 `null` - 新增触发的 step `oldContent` 为 `null`
- 删除触发的 step 其 diff 项 `newSchema` 为 `null` - 删除触发的 step `newContent` 为 `null`
- `undo` / `redo` 返回 `null`(边界状态)时不会触发该事件 - `undo` / `redo` 返回 `null`(边界状态)时不会触发该事件
::: :::
## data-source-history-change ## data-source-history-change
- **详情:** 数据源历史记录发生变化(`push('dataSource', step, dataSourceId)` / `undo('dataSource', id)` / `redo('dataSource', id)` 成功时触发) - **详情:** 数据源历史记录发生变化(`pushDataSource` / `undoDataSource` / `redoDataSource` 成功时触发)
- **事件回调函数:** `(dataSourceId: Id, step: DataSourceStepValue) => void` - **事件回调函数:** `(dataSourceId: Id, step: DataSourceStepValue) => void`
::: details 查看 DataSourceStepValue 及关联类型定义 ::: details 查看 DataSourceStepValue 及关联类型定义
<<< @/../packages/editor/src/type.ts#DataSourceStepValue{ts} <<< @/../packages/editor/src/type.ts#DataSourceStepValue{ts}
<<< @/../packages/editor/src/type.ts#BaseStepValue{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts} <<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
<<< @/../packages/schema/src/index.ts#Id{ts} <<< @/../packages/schema/src/index.ts#Id{ts}
::: :::
:::tip :::tip
- 新增触发的 step 其 diff 项 `oldSchema``null` - 新增触发的 step `oldSchema``null`
- 删除触发的 step 其 diff 项 `newSchema``null` - 删除触发的 step `newSchema``null`
- `undo` / `redo` 返回 `null`(边界状态)时不会触发该事件 - `undo` / `redo` 返回 `null`(边界状态)时不会触发该事件
::: :::
## mark-saved ## mark-saved
- **详情:** 调用 [`markSaved`](./historyServiceMethods.md#marksaved) 标记「已保存」记录时触发 - **详情:** 调用 `markSaved` / `markPageSaved` / `markCodeBlockSaved` / `markDataSourceSaved` 标记「已保存」记录时触发
- **事件回调函数:** `(payload: { kind: 'all' | HistoryStepType; id?: Id }) => void` - **事件回调函数:** `(payload: { kind: 'all' | 'page' | 'code-block' | 'data-source'; id?: Id }) => void`
::: tip ::: tip
- `markSaved(stepType)`(缺省 id触发时 `kind``all`,无 `id`(此时 `stepType` 不生效) - `markSaved` 触发时 `kind``all`,无 `id`
- `markSaved(stepType, id)` 触发时 `kind` 为对应的 `stepType``page` / `codeBlock` / `dataSource` / 扩展),`id` 为目标栈 id - 细粒度方法触发时 `kind` 对应类别,`id` 为目标页面 / 代码块 / 数据源 id
::: :::
## save-to-indexed-db ## save-to-indexed-db

View File

@ -4,71 +4,43 @@
- **详情:** - **详情:**
重置全部历史记录:清空 `state.steps` 下的页面 / 代码块 / 数据源 / 扩展类型全部栈(保留已注册的扩展类型键)。 重置全部历史记录(包括页面节点栈、代码块栈、数据源栈),并重置当前页面 id / canRedo / canUndo
## resetPage
- **详情:**
重置当前页面的历史记录状态清空当前页面id重置 canRedo/canUndo
## resetState ## resetState
- **详情:** - **详情:**
同 [`reset`](#reset),清空 `state.steps` 下全部栈。 重置历史记录全部内部状态(清空 pageId、pageSteps、canRedo、canUndo、codeBlockState、dataSourceState
::: tip ## changePage
历史服务不再维护「当前活动页」状态(已移除 `state.pageId` / `state.canUndo` / `state.canRedo`)。
活动页由 `editorService` 维护,撤销 / 重做 / 读取页面历史时请显式传入 pageId。 - **参数:**
是否可撤销 / 重做请改用 [`canUndo`](#canundo) / [`canRedo`](#canredo)。 - `{MPage | MPageFragment} page`
::: details 查看 MPage / MPageFragment 类型定义
<<< @/../packages/schema/src/index.ts#MPage{ts}
<<< @/../packages/schema/src/index.ts#MPageFragment{ts}
::: :::
## registerStepType
- **参数:**
- `{string} stepType` 自定义历史类型标识(勿与内置 `page` / `codeBlock` / `dataSource` 重名)
- `{Object} options` 可选
- `{string} event` push / undo / redo 后派发的事件名;缺省为 `${stepType}-history-change`
- `{string} name` 历史面板中的展示名称tab / 分组标题等);缺省回退到 stepType 本身
- **详情:** - **详情:**
注册一个扩展历史类型,使其可与内置 `page` / `codeBlock` / `dataSource` 一样走统一的 按页面切换历史堆栈
[`push`](#push) / [`undo`](#undo) / [`redo`](#redo)(按 id 分栈、独立 undo/redo
注册后该类型的栈存放在 `historyService.state.steps[stepType]`,展示名称存放在 `historyService.state.stepNames[stepType]`
## getStepName
- **参数:**
- `{HistoryStepType} stepType` 历史类型
- **返回:**
- `{string}` 该类型的展示名称(用于历史面板 tab / 分组标题等);未登记时回退到 stepType 本身
- **详情:**
读取指定历史类型的展示名称。内置 `page` / `codeBlock` / `dataSource` 默认分别为「页面 / 代码块 / 数据源」。
## setStepName
- **参数:**
- `{HistoryStepType} stepType` 历史类型
- `{string} name` 展示名称
- **详情:**
设置指定历史类型的展示名称(写入 `historyService.state.stepNames`,历史面板会响应式刷新)。
内置 `page` / `codeBlock` / `dataSource` 也可在此覆盖默认中文名。
## push ## push
- **参数:** - **参数:**
- `{HistoryStepType} stepType` 历史类型,内置 `'page'` / `'codeBlock'` / `'dataSource'`,并支持通过 [`registerStepType`](#registersteptype) 扩展 - `{StepValue} state`
- `{StepValue | BaseStepValue} step` 已构造好的历史记录(缺省自动补全 `uuid` / `timestamp`
- `{Id} id` 必填;目标栈 id`page` 为 pageId其余类型为对应资源 id
::: details 查看 StepValue 及关联类型定义 ::: details 查看 StepValue 及关联类型定义
<<< @/../packages/editor/src/type.ts#StepValue{ts} <<< @/../packages/editor/src/type.ts#StepValue{ts}
<<< @/../packages/editor/src/type.ts#BaseStepValue{ts}
<<< @/../packages/editor/src/type.ts#StepExtra{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpType{ts} <<< @/../packages/editor/src/type.ts#HistoryOpType{ts}
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts} <<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
@ -81,137 +53,278 @@
::: :::
- **返回:** - **返回:**
- `{StepValue | BaseStepValue | null}` 入栈失败(未传 / 无效 id时返回 `null` - `{StepValue | null}`
- **详情:** - **详情:**
添加一条历史记录。统一入口,所有类型(`page` / `codeBlock` / `dataSource` / 扩展)行为完全一致:按 `stepType` 选择目标栈类型、按 `id`(必填)选择具体栈,按需建栈后入栈,并派发对应的历史变更事件(`page``change`,其余如 `code-block-history-change` / `data-source-history-change`),回调签名统一为 `(id, step)` 添加一条历史记录
跨页 / 跨资源操作(如把节点搬到其它页)必须显式传入目标 id。`codeBlock` / `dataSource` 的 step 通常由 `createStackStep` 等工具按 `oldValue` / `newValue` 构造后传入。
::: tip ::: tip
`opType: 'update'` 的每个 diff 项上可携带 `changeRecords`,用于撤销 / 重做时仅按 `opType: 'update'` 的每个 `updatedItems[i]` 上可携带 `changeRecords`,用于撤销 / 重做时仅按
`propPath` 局部更新对应字段,避免整节点替换冲掉同节点上的其它无关变更;不带 `propPath` 局部更新对应字段,避免整节点替换冲掉同节点上的其它无关变更;不带
`changeRecords` 时退化为整节点替换(如 `sort` / `moveLayer` / 拖动等纯快照场景)。 `changeRecords` 时退化为整节点替换(如 `sort` / `moveLayer` / 拖动等纯快照场景)。
`step` 上的 `historyDescription` / `source` 仅用于历史面板展示与埋点,不影响 undo/redo 行为。 `StepValue` 上的 `historyDescription` / `source` 仅用于历史面板展示与埋点,不影响 undo/redo 行为。
入栈时会为每条记录自动生成唯一标识 `uuid`(调用方未指定时),可用于精确引用 / 定位某一条历史记录。 入栈时会为每条记录自动生成唯一标识 `uuid`(调用方未指定时),可用于精确引用 / 定位某一条历史记录。
若需要在执行 DSL 操作后拿到本次写入记录的 `uuid`,可使用 editorService / dataSourceService / 若需要在执行 DSL 操作后拿到本次写入记录的 `uuid`,可使用 editorService / dataSourceService /
codeBlockService 提供的 `*AndGetHistoryId` 方法,参见 codeBlockService 提供的 `*AndGetHistoryId` 方法,参见
[editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
`pushCodeBlock` / `pushDataSource` 同样会自动写入 `uuid`
::: :::
## undo ## undo
- **参数:**
- `{HistoryStepType} stepType` 历史类型
- `{Id} id` 必填;目标栈 id`page` 为 pageId其余类型为对应资源 id
- **返回:** - **返回:**
- `{StepValue | BaseStepValue | null}` - `{StepValue | null}`
- **详情:** - **详情:**
撤销指定历史栈的最近一次变更。所有类型行为一致:按 `stepType` + `id` 定位栈,不会越过 index 0 的 initial 基线(所有类型同等适用,见 [`setMarker`](#setmarker)),仅在确有可撤销 step 时派发对应的历史变更事件(`page``change`,回调签名 `(id, step)`)。 撤销当前操作。`opType: 'update'` 时,若 `updatedItems[i].changeRecords` 存在,会按
`propPath``oldNode` 取值做局部回滚;否则用 `oldNode` 整节点替换。
`page` 类型 `opType: 'update'` 时,若 diff 项的 `changeRecords` 存在,会按 `propPath``oldSchema` 取值做局部回滚;否则用 `oldSchema` 整节点替换。`codeBlock` / `dataSource` 拿到 step 后由调用方写回对应 service本方法不会自动回放
## redo ## redo
- **参数:**
- `{HistoryStepType} stepType` 历史类型
- `{Id} id` 必填;目标栈 id`page` 为 pageId其余类型为对应资源 id
- **返回:** - **返回:**
- `{StepValue | BaseStepValue | null}` - `{StepValue | null}`
- **详情:** - **详情:**
恢复指定历史栈到下一步,语义与 [`undo`](#undo) 对称。`page` 类型 `opType: 'update'` 时,若 diff 项的 `changeRecords` 存在,会按 `propPath``newSchema` 取值做局部重做;否则用 `newSchema` 整节点替换。 恢复到下一步。`opType: 'update'` 时,若 `updatedItems[i].changeRecords` 存在,会按
`propPath``newNode` 取值做局部重做;否则用 `newNode` 整节点替换。
## canUndo ## pushCodeBlock
- **参数:** - **参数:**
- `{HistoryStepType} stepType` 历史类型 - `{Id} codeBlockId` 代码块 id
- `{Id} id` 可选;目标栈 id缺省 / 无效时返回 `false` - `{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}` - `{boolean}`
- **详情:** - **详情:**
指定历史栈当前是否可撤销(游标高于 index 0 的 initial 基线底线)。适用于所有类型(`page` / `codeBlock` / `dataSource` / 扩展)。 指定代码块当前是否可撤销。栈不存在时返回 `false`
## canRedo ## canRedoCodeBlock
- **参数:** - **参数:**
- `{HistoryStepType} stepType` 历史类型 - `{Id} codeBlockId`
- `{Id} id` 可选;目标栈 id缺省 / 无效时返回 `false`
- **返回:** - **返回:**
- `{boolean}` - `{boolean}`
- **详情:** - **详情:**
指定历史栈当前是否可重做。适用于所有类型(`page` / `codeBlock` / `dataSource` / 扩展)。 指定代码块当前是否可重做。栈不存在时返回 `false`
## setMarker ## pushDataSource
- **参数:** - **参数:**
- `{HistoryStepType} stepType` 历史类型 - `{Id} dataSourceId` 数据源 id
- `{Id} id` 目标栈 id`page` 为 pageId其余类型为对应资源 id - `{Object} payload`
- `{Object} options` 可选:`name` / `description` / `source`,用于基线的展示信息 - `{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}
:::
- **返回:** - **返回:**
- `{StepValue | null}` 已存在基线时返回原基线;栈非空(无基线)或 id 无效时返回 `null` - `{DataSourceStepValue | null}` 入栈失败(未传 id时返回 `null`
- **详情:** - **详情:**
为指定历史栈种入一条 `opType: 'initial'` 的「初始基线」记录,作为该栈 index 0 的固定底线:它是真实入栈并随栈持久化的 step但被钉为撤销 / 回滚的下限,`undo` / `goto` / `revert` 都不会越过它。所有类型(含扩展类型)均可设置基线,仅当目标栈为空时种入。 推入一条数据源变更记录。与页面 / 节点完全无关,按 `dataSourceId` 维度独立一份 `UndoRedo` 栈,
栈实例存放在 `historyService.state.dataSourceState[dataSourceId]`
## getMarker 入栈成功后会触发 `data-source-history-change` 事件。
::: tip
`dataSourceService.add` / `update` / `remove` 内部已经自动调用本方法,业务代码通常无需手动调用。
:::
## undoDataSource
- **参数:** - **参数:**
- `{HistoryStepType} stepType` 历史类型 - `{Id} dataSourceId`
- `{Id} id` 可选;目标栈 id缺省 / 无效时返回 `undefined`
- **返回:** - **返回:**
- `{StepValue | undefined}` - `{DataSourceStepValue | null}`
- **详情:** - **详情:**
读取指定历史栈的初始基线 step栈 index 0 且 `opType: 'initial'`),不存在时返回 `undefined` 撤销指定数据源的最近一次变更。成功时会触发 `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 ## markSaved
- **详情:**
标记「整份 DSL 已保存」:把页面 / 代码块 / 数据源所有栈当前游标所在的记录都标记为已保存(`saved = true`)。
同一栈内任意时刻最多保留一条已保存记录标记前会清除该栈内全部旧标记某个栈处于「全部已撤销」cursor 为 0时不会留下已保存记录从 IndexedDB 恢复时其游标会回到 0。
通常在 DSL 整体落库(保存到后端 / 本地)成功后调用,配合 [`restoreFromIndexedDB`](#restorefromindexeddb) 把游标恢复到此处。仅保存了其中一类时请改用更细粒度的 `markPageSaved` / `markCodeBlockSaved` / `markDataSourceSaved`
调用后会触发 `mark-saved` 事件(`{ kind: 'all' }`)。
## markPageSaved
- **参数:** - **参数:**
- `{HistoryStepType} stepType` 历史类型,内置另有 `'codeBlock'` / `'dataSource'`,并支持扩展(仅在传入 `id` 时生效) - `{Id} pageId` 可选;缺省为当前活动页
- `{Id} id` 可选;目标栈 id。缺省表示标记全部类型、全部栈
- **详情:** - **详情:**
标记历史记录为「已保存」(把对应栈当前游标所在的记录标记为 `saved = true`)。统一入口: 标记指定页面(缺省当前活动页)历史栈的当前记录为已保存,仅影响该页面自己的栈。触发 `mark-saved` 事件(`{ kind: 'page', id }`)。
- **缺省 `id`**:标记「整份 DSL 已保存」——把所有类型、所有栈当前游标所在的记录都标记为已保存(此时 `stepType` 不生效),触发 `mark-saved` 事件且 `{ kind: 'all' }`。通常在 DSL 整体落库成功后调用。 ## markCodeBlockSaved
- **传入 `id`**:仅标记 `stepType` 下该 id 对应的栈,触发 `mark-saved` 事件且 `{ kind: stepType, id }`(如 `{ kind: 'page', id }` / `{ kind: 'codeBlock', id }` / `{ kind: 'dataSource', id }`)。
同一栈内任意时刻最多保留一条已保存记录标记前会清除该栈内全部旧标记某个栈处于「全部已撤销」cursor 为 0时不会留下已保存记录从 IndexedDB 恢复时其游标会回到 0。配合 [`restoreFromIndexedDB`](#restorefromindexeddb) 把游标恢复到此处。
## clear
- **参数:** - **参数:**
- `{HistoryStepType} stepType` 历史类型,内置另有 `'codeBlock'` / `'dataSource'`,并支持扩展 - `{Id} codeBlockId`
- `{Id} id` 可选;目标栈 id。缺省表示清空 `stepType` 下的全部栈
- **详情:** - **详情:**
清空历史记录栈。统一入口所有类型page / codeBlock / dataSource / 扩展)行为一致: 标记指定代码块历史栈的当前记录为已保存,仅影响该代码块自己的栈。触发 `mark-saved` 事件(`{ kind: 'code-block', id }`)。
- **传入 `id`**:仅清空 `stepType` 下该 id 对应的栈; ## markDataSourceSaved
- **缺省 `id`**:清空 `stepType` 下的全部栈。
仅删除撤销/重做记录,不会改动 DSL / 代码块 / 数据源本身。清空时会**保留各栈原有的 initial 基线**(文案 / 来源,见 [`setMarker`](#setmarker)),无基线时清空成空栈。清空后触发 `clear` 事件(签名 `(id, stepType)`)。 - **参数:**
- `{Id} dataSourceId`
- **详情:**
标记指定数据源历史栈的当前记录为已保存,仅影响该数据源自己的栈。触发 `mark-saved` 事件(`{ kind: 'data-source', id }`)。
## clearPage
- **参数:**
- `{Id} pageId` 可选;缺省为当前活动页
- **详情:**
清空指定页面(缺省当前活动页)的历史记录栈。仅删除撤销/重做记录,不会改动当前 DSL清空后该页将无法再撤销/重做之前的操作。清空当前活动页时会同步刷新 `canUndo` / `canRedo` 并触发 `change` 事件。
## clearCodeBlock
- **参数:**
- `{Id} codeBlockId` 可选;缺省清空全部代码块
- **详情:**
清空代码块历史记录栈:传入 `codeBlockId` 仅清空该代码块,缺省清空全部代码块。仅删除撤销/重做记录,不会改动代码块本身。
## clearDataSource
- **参数:**
- `{Id} dataSourceId` 可选;缺省清空全部数据源
- **详情:**
清空数据源历史记录栈:传入 `dataSourceId` 仅清空该数据源,缺省清空全部数据源。仅删除撤销/重做记录,不会改动数据源本身。
## saveToIndexedDB ## saveToIndexedDB
@ -231,7 +344,7 @@
- **详情:** - **详情:**
把当前内存中的全部历史栈(页面 / 代码块 / 数据源 / 扩展类型)连同各自游标、容量序列化后写入本地 IndexedDB。 把当前内存中的全部历史栈(页面 / 代码块 / 数据源)连同各自游标、容量序列化后写入本地 IndexedDB。
- 最终库名为 `${dbName}-${当前 DSL app id}`,按应用隔离; - 最终库名为 `${dbName}-${当前 DSL app id}`,按应用隔离;
- `key` 用于在同一 store 下区分不同记录,缺省为 `default` - `key` 用于在同一 store 下区分不同记录,缺省为 `default`
@ -258,32 +371,14 @@
- 每个栈都会按 `listMaxSize` 裁剪并还原游标; - 每个栈都会按 `listMaxSize` 裁剪并还原游标;
- 若某个栈存在已保存记录(见 `markSaved`),其游标会被定位到「最近一条已保存记录」之后,使恢复后的状态与落库的 DSL 对齐; - 若某个栈存在已保存记录(见 `markSaved`),其游标会被定位到「最近一条已保存记录」之后,使恢复后的状态与落库的 DSL 对齐;
- 会整体覆盖当前内存中的历史状态(活动页由 `editorService` 维护,不在此恢复) - 会整体覆盖当前内存中的历史状态,并把活动页恢复为快照中的 `pageId`
- 找不到对应记录时返回 `null` 且不改动当前状态;不支持 IndexedDB 的环境会 reject。 - 找不到对应记录时返回 `null` 且不改动当前状态;不支持 IndexedDB 的环境会 reject。
成功后触发 `restore-from-indexed-db` 事件。 成功后触发 `restore-from-indexed-db``change` 事件。
## findStepLocationByUuid
- **参数:**
- `{HistoryStepType} stepType` 历史类型
- `{string} uuid` 目标历史记录的 uuid
- `{Id} id` 可选;目标栈 id
- **返回:**
- `{ { id: Id; index: number } | null }` 找到时返回所属栈 id 与步骤索引;找不到时返回 `null`
- **详情:**
按历史记录 uuid 在指定历史类型的栈中查找其所属 id 与索引,统一入口。
- **传入 `id`**:仅在该 id 对应的单个栈中查找(如页面历史按活动页查看,传入 pageId
- **缺省 `id`**:遍历该类型下全部栈查找(代码块 / 数据源等按全部资源分桶的场景)。
供「按 uuid 回滚」等需要把 uuid 映射回 `(id, index)` 的场景使用,如 [editorService.revertPageStepById](./editorServiceMethods.md#revertpagestepbyid) / [codeBlockService.revertById](./codeBlockServiceMethods.md#revertbyid) / [dataSourceService.revertById](./dataSourceServiceMethods.md#revertbyid) 内部均通过本方法定位步骤。
## destroy ## destroy
- **详情:** - **详情:**
销毁 销毁

View File

@ -1282,45 +1282,6 @@ const guidesOptions = {
</template> </template>
``` ```
## enablePropsFormValidate
- **详情:**
是否启用「属性配置表单校验」联动能力。
开启后(默认 `false` 关闭),当属性面板(属性表单 / 样式表单)校验失败时,编辑器会**仍按当前表单值更新节点**,并把错误信息集中记录到 `editorService``invalidNodeIds` 状态),用于:
- 组件树(图层)中对出错节点标红并显示错误图标,鼠标悬停可查看错误文案;
- 保存前拦截:业务可通过 `editorService.getInvalidNodeIds()` 读取错误节点,存在校验错误时阻止保存(参考 [playground 菜单保存拦截](../../guide/advanced/prop-form-validate.md#保存拦截))。
关闭时保持原行为:属性 / 样式表单校验失败则丢弃本次改动,不写入节点。
:::tip
校验错误以「来源」为维度分别记录 —— 属性表单来源记为 `props`,样式表单来源记为 `style`;两者指向同一节点,互不覆盖。节点只要任一来源存在错误即视为出错。
错误信息会随 DSL 操作写入历史记录快照,因此「撤销 / 重做」能正确还原校验错误状态(撤销一个「校验失败」的改动后错误消失,重做后错误恢复)。
:::
- **默认值:** `false`
- **类型:** `boolean`
- **示例:**
```html
<template>
<!-- 开启属性配置表单校验联动能力 -->
<m-editor :enable-props-form-validate="true"></m-editor>
</template>
```
- **相关 API**
- `editorService` 错误状态与方法:`get('invalidNodeIds')` / [setInvalidNode](#setinvalidnode) / [deleteInvalidNode](#deleteinvalidnode) / [getInvalidNodeIds](#getinvalidnodeids) / [getInvalidNodeInfo](#getinvalidnodeinfo) / [resetInvalidNodeId](#resetinvalidnodeid)
- 错误状态变化事件:[invalid-node-change](#invalid-node-change)
- 进阶用法见[属性配置表单校验联动](../../guide/advanced/prop-form-validate.md)
## disabledDataSource ## disabledDataSource
- **详情:** - **详情:**
@ -1404,34 +1365,13 @@ const guidesOptions = {
- **详情:** - **详情:**
用于自定义组件树(图层)、画布、数据源、代码块的右键菜单 用于自定义组件树与画布的右键菜单
该函数会在显示右键菜单前被调用,接收默认菜单项与菜单类型作为参数,返回最终显示的菜单项 该函数会在显示右键菜单前被调用,接收默认菜单项作为参数,返回最终显示的菜单项
第二个参数 `type` 用于区分菜单来源:`'layer'`(图层)、`'viewer'`(画布)、`'data-source'`(数据源)、`'code-block'`(代码块)
第三个参数 `getTarget` 仅在**数据源**与**代码块**面板下传入,用于读取当前右键目标(节点 `id` 与原始数据);图层 / 画布不会传入该参数
- **默认值:** `(menus) => menus` - **默认值:** `(menus) => menus`
- **类型:** - **类型:** `(menus: (MenuButton | MenuComponent)[], data: { node?: MNode; page?: MPage; parent?: MContainer; stage?: StageCore }) => (MenuButton | MenuComponent)[]`
```ts
interface ContentMenuTarget {
/** 目标 ID */
id: string;
/** 原始节点数据(树节点等) */
data?: TreeNodeData;
}
type ContentMenuType = 'layer' | 'data-source' | 'viewer' | 'code-block';
type CustomContentMenuFunction = (
menus: (MenuButton | MenuComponent)[],
type: ContentMenuType,
getTarget?: () => ContentMenuTarget | null,
) => (MenuButton | MenuComponent)[];
```
- **示例:** - **示例:**
@ -1441,24 +1381,20 @@ type CustomContentMenuFunction = (
</template> </template>
<script setup> <script setup>
const customContentMenu = (menus, type, getTarget) => { const customContentMenu = (menus, { node }) => {
// 为数据源右键菜单追加自定义菜单 // 为特定类型的组件添加自定义菜单
if (type === 'data-source') { if (node?.type === 'container') {
menus.push({ menus.push({
type: 'button', type: 'button',
text: '查看详情', text: '清空容器',
handler: () => { handler: () => {
// getTarget 仅在数据源 / 代码块面板下传入 // 清空容器的逻辑
const target = getTarget?.();
if (target) {
console.log(target.id, target.data);
}
}, },
}); });
} }
// 可以过滤掉某些菜单项 // 可以过滤掉某些菜单项
return menus.filter((menu) => menu.text !== '删除'); return menus.filter(menu => menu.text !== '删除');
}; };
</script> </script>
``` ```

View File

@ -150,14 +150,6 @@
- **类型:** `boolean` - **类型:** `boolean`
## useFieldTextInError
- **详情:** 透传给内部 `Form`,控制表单校验失败时错误提示前缀是否使用字段的 `text` 文案。`false` 时直接使用字段 `name`
- **默认值:** `true`
- **类型:** `boolean`
## closeOnClickModal ## closeOnClickModal
- **详情:** 是否可以通过点击 modal 关闭 Dialog - **详情:** 是否可以通过点击 modal 关闭 Dialog

View File

@ -40,8 +40,6 @@
- **详情:** 通过 `name` 从表单 `config` 中查找对应表单项的 `text` - **详情:** 通过 `name` 从表单 `config` 中查找对应表单项的 `text`
- **相关:** 表单校验失败时是否使用该方法生成错误提示前缀,由 [`useFieldTextInError`](./form-props.md#usefieldtextinerror) prop 控制(默认 `true`
## values ## values
- **类型:** `Ref<FormValue>` - **类型:** `Ref<FormValue>`

View File

@ -206,19 +206,6 @@
- **类型:** `boolean` - **类型:** `boolean`
## useFieldTextInError
- **详情:**
表单校验失败时,错误提示前缀是否使用字段的 `text` 文案(通过 `getTextByName``config` 中查找)。
- `true`(默认):错误提示形如 `字段文案 -> 错误信息`,找不到 `text` 时回退为字段 `name`
- `false`:跳过查找,直接使用字段 `name` 作为错误提示前缀(形如 `字段name -> 错误信息`)。
- **默认值:** `true`
- **类型:** `boolean`
## extendState ## extendState
- **详情:** 扩展 formState 的钩子函数,返回的对象会被合并到 formState 上 - **详情:** 扩展 formState 的钩子函数,返回的对象会被合并到 formState 上

View File

@ -37,7 +37,6 @@ function submitForm(options: SubmitFormOptions): Promise<any>;
| `keyProp` | `string` | `'__key'` | 配置项的唯一 key | | `keyProp` | `string` | `'__key'` | 配置项的唯一 key |
| `popperClass` | `string` | — | 弹层 className | | `popperClass` | `string` | — | 弹层 className |
| `preventSubmitDefault` | `boolean` | — | 是否阻止表单原生 submit | | `preventSubmitDefault` | `boolean` | — | 是否阻止表单原生 submit |
| `useFieldTextInError` | `boolean` | `true` | 校验失败时错误提示前缀是否使用字段的 `text` 文案;`false` 时直接使用字段 `name` |
| `extendState` | `(state: FormState) => Record<string, any> \| Promise<Record<string, any>>` | — | 扩展 `formState` | | `extendState` | `(state: FormState) => Record<string, any> \| Promise<Record<string, any>>` | — | 扩展 `formState` |
| `native` | `boolean` | `false` | 透传给 `Form.submitForm``true` 时返回内部响应式 `values`,否则返回 `cloneDeep(toRaw(values))` | | `native` | `boolean` | `false` | 透传给 `Form.submitForm``true` 时返回内部响应式 `values`,否则返回 `cloneDeep(toRaw(values))` |
| `returnChangeRecords` | `boolean` | `false` | `true` 时 resolve 结果为 `{ values, changeRecords }`,携带表单变更记录;否则仅 resolve `values` | | `returnChangeRecords` | `boolean` | `false` | `true` 时 resolve 结果为 `{ values, changeRecords }`,携带表单变更记录;否则仅 resolve `values` |
@ -68,10 +67,7 @@ try {
type: 'text', type: 'text',
name: 'username', name: 'username',
text: '用户名', text: '用户名',
rules: [ rules: [{ required: true, message: '请输入用户名' }],
{ required: true, message: '请输入用户名' },
{ typeMatch: true, message: '用户名类型不合法' },
],
}, },
], ],
initValues: { username: '' }, initValues: { username: '' },

View File

@ -27,10 +27,6 @@
} }
``` ```
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:要求 `string`,有代码块 DSL 时须为已有 codeId。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -24,10 +24,6 @@ CodeSelect 组件支持:
- 选择数据源方法 - 选择数据源方法
- 配置代码块参数 - 配置代码块参数
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:做 `{ hookType: 'code', hookData }` 的浅层结构校验,`codeId` / 数据源方法存在性由内部单元格各自校验。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -41,10 +41,6 @@
} }
``` ```
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:默认要求 `string``parse: true` 时跳过校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -27,10 +27,6 @@
} }
``` ```
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为已知算子,能解析字段类型时按类型收窄。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -49,10 +49,6 @@
} }
``` ```
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数据源路径 `string[]`,有 `fieldConfig` 且非路径值时跳过。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -16,10 +16,6 @@
} }
``` ```
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数组并做 `name`/`type` 等浅层结构校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -16,10 +16,6 @@
} }
``` ```
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:要求 `string``${...}` 绑定须指向已有数据源 / 字段。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -27,10 +27,6 @@
} }
``` ```
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为 `[dsId, methodName]`,方法须在该数据源可选方法集中。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -16,10 +16,6 @@
} }
``` ```
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数组并做 `content`/`params` 等浅层结构校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -16,10 +16,6 @@
} }
``` ```
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数组并做 `title`/`enable`/`data` 等浅层结构校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -38,10 +38,6 @@
} }
``` ```
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:`value: 'id'` 时须为已有 ds id否则为含 `isBindDataSource` + `dataSourceId` 的对象。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -27,10 +27,6 @@
} }
``` ```
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数组并做 `cond[].field`/`op` 浅层结构校验,算子合法性 / 字段路径存在性由内部单元格各自校验。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -28,10 +28,6 @@
} }
``` ```
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数组并做兼容新旧格式的浅层结构校验,事件名 / 联动动作是否属于可选项由内部单元格各自校验。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -29,10 +29,6 @@
} }
``` ```
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为普通对象,`advanced` 且值为 `function` 时放行。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -16,10 +16,6 @@
} }
``` ```
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为 `string``number`,有节点树时须为已有页面片 id。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -16,10 +16,6 @@
} }
``` ```
## 值校验
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为 `string``number`,有节点树时须为已有组件 id。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
## Attributes ## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |

View File

@ -24,26 +24,6 @@ type为'select'
</template> </template>
</demo-block> </demo-block>
## 选项值校验
可在 `rules` 中开启 `typeMatch`,校验当前值是否落在 `options` 中(`multiple` 时校验数组元素)。详见[表单校验](/form-config/rules.md)。
```ts
{
type: 'select',
name: 'status',
text: '状态',
options: [
{ text: '启用', value: 1 },
{ text: '禁用', value: 0 },
],
rules: [
{ required: true, message: '请选择状态' },
{ typeMatch: true, message: '状态值不合法' },
],
}
```
## 有禁用选项 ## 有禁用选项
<demo-block type="form" :config="[{ <demo-block type="form" :config="[{

View File

@ -82,21 +82,6 @@ Input输入框的type为'text', 是type的默认值所以可以不配置
</template> </template>
</demo-block> </demo-block>
## 表单校验
可通过 `rules` 配置校验规则。其中 `typeMatch: true` 会按字段 type以及 `filter` / `valueFormat` 等)校验值是否合法,详见[表单校验](/form-config/rules.md)。
```ts
{
name: 'text',
text: '输入框',
rules: [
{ required: true, message: '请输入' },
{ typeMatch: true, message: '值类型不合法' },
],
}
```
## 去掉首尾空格 ## 去掉首尾空格
<demo-block type="form" :config="[{ <demo-block type="form" :config="[{
@ -139,7 +124,6 @@ Input输入框的type为'text', 是type的默认值所以可以不配置
| tooltip | 输入时显示内容 | string / `ToolTipConfigType` | — | — | | tooltip | 输入时显示内容 | string / `ToolTipConfigType` | — | — |
| trim | 是否去掉首尾空格 | boolean | — | false | | trim | 是否去掉首尾空格 | boolean | — | false |
| filter | 过滤值 | string / Function | number | - | | filter | 过滤值 | string / Function | number | - |
| rules | 表单验证规则 | `Rule[]` | — | — |
| prepend | 前置内容 | string | — | - | | prepend | 前置内容 | string | — | - |
| append | 后置内容 | string / Object | — | - | | append | 后置内容 | string / Object | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - | | onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
@ -156,8 +140,6 @@ Input输入框的type为'text', 是type的默认值所以可以不配置
<<< @/../packages/form-schema/src/base.ts#FormValue{ts} <<< @/../packages/form-schema/src/base.ts#FormValue{ts}
<<< @/../packages/form-schema/src/base.ts#ToolTipConfigType{ts} <<< @/../packages/form-schema/src/base.ts#ToolTipConfigType{ts}
<<< @/../packages/form-schema/src/base.ts#Rule{ts}
::: :::

View File

@ -1,206 +0,0 @@
# 表单校验
`@tmagic/form` 字段可通过 `rules` 配置校验规则,兼容 [async-validator](https://github.com/yiminghe/async-validator)Element Plus 内部使用)的常见能力,并额外支持按字段 `type` 做值类型 / 选项匹配校验(`typeMatch`)。
## 基础用法
```ts
{
name: 'title',
text: '名称',
rules: [
{ required: true, message: '请输入名称' },
{ typeMatch: true, message: '名称类型不合法' },
],
}
```
`rules` 类型见下方 [`Rule`](#类型定义)。未配置 `typeMatch` 时,现有表单行为不变。
## typeMatch
开启 `typeMatch: true` 后,表单会按字段 `config.type`(及关联配置)校验当前值是否合法。空值(`undefined` / `null` / `''`,多选类空数组 `[]`)直接通过,必填请继续使用 `required`
### 内置映射
| 字段 type | 期望值 |
| --- | --- |
| `text` / `textarea` / `color-picker` / `html` / 默认无 type | `string`;若 `filter: 'number'` 则为 `number``filter` 为自定义函数时跳过内置类型校验 |
| `display` / `hidden` | 不校验 |
| `number` | `number`(非 NaN |
| `number-range` | 长度为 2 的数字数组 |
| `date` / `datetime` / `time` | 按 `valueFormat` 校验;`x` / `timestamp` 期望 `number`,其余按 [Day.js format](https://day.js.org/docs/en/display/format) 严格解析字符串 |
| `daterange` / `timerange` | 无 `names` 时为长度为 2 的数组,元素按 `valueFormat` 校验;有 `names` 时跳过 |
| `switch` / `checkbox` | 值必须是解析后的 `activeValue` / `inactiveValue` 之一(显式配置优先;未配置且 `filter === 'number'` 时为 `1`/`0`;否则为 `true`/`false` |
| `select` | 单选:值 ∈ options`multiple`:数组且每项 ∈ options`allowCreate` / `remote`:只校验基础形态,不做 options 枚举 |
| `radio-group` / `radioGroup` | 值 ∈ options |
| `checkbox-group` / `checkboxGroup` | 数组且每项 ∈ options |
| `cascader` | 有 `valueSeparator` 时可为 `string``array`;默认 `emitPath` 为路径数组;`emitPath: false` 为叶子值;`multiple` 为数组;静态 options 校验路径/叶子;`remote` 只做形态校验 |
| `table` / `group-list` / `groupList` | `array` |
| 容器类(`row` / `tab` / `fieldset` / `panel` / `step` / `flex-layout` / `link` / `component` / `dynamic-field` 等) | 不校验 |
日期类默认 `valueFormat` 与字段组件一致:
- `date``YYYY/MM/DD`
- `datetime` / `daterange``YYYY/MM/DD HH:mm:ss`
- `time` / `timerange``HH:mm:ss`
### 与自定义 validator 共存
同一条 rule 同时配置了 `typeMatch``validator` 时,会先做类型匹配校验,通过后再执行自定义 `validator`
```ts
{
name: 'age',
type: 'number',
rules: [
{
typeMatch: true,
message: '年龄必须是数字',
validator: ({ value, callback }) => {
if (value < 0) {
callback(new Error('年龄不能小于 0'));
return;
}
callback();
},
},
],
}
```
## 扩展自定义 type 规则
业务可覆盖内置规则,或为自定义字段 type 注册校验。自定义规则优先于内置规则。
### 运行时注册
```ts
import {
registerTypeMatchRule,
registerTypeMatchRules,
deleteTypeMatchRule,
clearTypeMatchRules,
} from '@tmagic/form';
// 覆盖内置 text
registerTypeMatchRule('text', (value, { message }) => {
if (typeof value !== 'string') {
return message || '值类型应为字符串';
}
});
// 扩展业务字段
registerTypeMatchRule('vs-code', (value, { message }) => {
if (typeof value !== 'string') {
return message || '代码字段应为字符串';
}
});
// 批量注册
registerTypeMatchRules({
foo: (value) => (Array.isArray(value) ? undefined : '应为数组'),
});
// 删除 / 清空
deleteTypeMatchRule('foo');
clearTypeMatchRules();
```
自定义校验器签名:`(value, context) => string | undefined`。返回错误文案表示失败,返回 `undefined` 表示通过。`context` 包含 `fieldType``mForm``props``message`
### 安装时注册
```ts
import MagicForm from '@tmagic/form';
app.use(MagicForm, {
typeMatchRules: {
'my-field': (value, { message }) => {
if (typeof value !== 'string') {
return message || 'my-field 应为字符串';
}
},
},
});
```
### Editor 字段内置规则
安装 `@tmagic/editor` 时会自动 `registerTypeMatchRules` 注册编辑器自定义字段规则。服务数据(数据源 / 代码块 / 节点树)未就绪时,只做基础形态校验,不做枚举或存在性失败。
| 字段 type | 期望值 |
| --- | --- |
| `key-value` / `style-setter` | 普通对象;`key-value` + `advanced` 且值为 `function` 时放行 |
| `cond-op-select` | 已知算子;能解析字段类型时按类型收窄 |
| `code-select-col` | `string`;有代码块 DSL 时须为已有 codeId |
| `page-fragment-select` / `ui-select` | `string \| number`;有节点树时须为已有页面片 / 组件 id |
| `data-source-input` | `string``${...}` 绑定须指向已有数据源/字段 |
| `data-source-method-select` | `[dsId, methodName]`,方法须在该数据源可选方法集中 |
| `data-source-field-select` | 数据源路径 `string[]`;有 `fieldConfig` 且非路径值时跳过 |
| `data-source-select` | `value: 'id'` 为已有 ds id否则为含 `isBindDataSource` + `dataSourceId` 的对象 |
| `code-select` | `{ hookType: 'code', hookData }` 的浅层结构校验(`codeId` 存在性 / 数据源方法存在性由内部 `code-select-col``data-source-method-select` 单元格各自校验,只标红出错单元格) |
| `data-source-fields` / `data-source-mocks` / `data-source-methods` | 数组 + 浅层结构(`name`/`type``title`/`enable`/`data``content`/`params` 等) |
| `event-select` | 数组;兼容新旧格式的浅层结构校验(`name` / 联动组件 `method` 是否 ∈ 可选项由字段内 `eventNameConfig.rules``compActionConfig.rules` 单独校验,只标红对应 select |
| `display-conds` | 数组 + `cond[].field`/`op` 浅层结构校验(`op` 是否为已知算子、字段路径是否存在由内部 `cond-op-select``field` 单元格各自校验,只标红出错单元格) |
> 容器类字段(`event-select` / `code-select` / `display-conds`)遵循同一约定:容器级 typeMatch 只做结构校验,「枚举 / 存在性」下沉到内部单元格各自的 typeMatch/rules避免单个子项非法导致整块表单标红。
业务仍可用 `registerTypeMatchRule` 覆盖上述任一 type。
## 示例
### select 选项匹配
```ts
{
name: 'status',
type: 'select',
options: [
{ text: '启用', value: 1 },
{ text: '禁用', value: 0 },
],
rules: [
{ required: true, message: '请选择状态' },
{ typeMatch: true, message: '状态值不合法' },
],
}
```
### date 按 valueFormat 校验
```ts
{
name: 'birthday',
type: 'date',
valueFormat: 'YYYY-MM-DD',
rules: [{ typeMatch: true, message: '日期格式不正确' }],
}
```
### text + filter: number
```ts
{
name: 'width',
type: 'text',
filter: 'number',
rules: [{ typeMatch: true, message: '宽度应为数字' }],
}
```
## 类型定义
::: details 查看 Rule 类型定义
<<< @/../packages/form-schema/src/base.ts#Rule{ts}
:::
::: details 查看 TypeMatchValidator / TypeMatchValidateContext 类型定义
<<< @/../packages/form/src/utils/typeMatch.ts#TypeMatchValidator{ts}
<<< @/../packages/form/src/utils/typeMatch.ts#TypeMatchValidateContext{ts}
:::
::: details 查看 FormInstallOptions 类型定义
<<< @/../packages/form/src/plugin.ts#FormInstallOptions{ts}
:::

View File

@ -61,50 +61,11 @@ const menu = ref({
- 数据源:`dataSourceService.revert(id, index)` - 数据源:`dataSourceService.revert(id, index)`
- 代码块:`codeBlockService.revert(id, index)` - 代码块:`codeBlockService.revert(id, index)`
如果业务侧在执行操作时已通过 `*AndGetHistoryId` 拿到了该条记录的 uuid[uuid 与 *AndGetHistoryId](/api/editor/editorServiceMethods.md#历史记录-uuid-与-andgethistoryid) 返回的 `historyIds`,也可以直接按 uuid 回滚(无需再关心 index / id且 uuid 不会随栈内步骤增删而变化): 如果业务侧在执行操作时已通过 `*AndGetHistoryId` 拿到了该条记录的 [uuid](/api/editor/editorServiceMethods.md#历史记录-uuid-与-andgethistoryid),也可以直接按 uuid 回滚(无需再关心 index / id且 uuid 不会随栈内步骤增删而变化):
- 页面:`editorService.revertPageStepById(uuids)` - 页面:`editorService.revertPageStepById(uuid)`
- 数据源:`dataSourceService.revertById(uuids)` - 数据源:`dataSourceService.revertById(uuid)`
- 代码块:`codeBlockService.revertById(uuids)` - 代码块:`codeBlockService.revertById(uuid)`
#### 复用面板的「交互式回滚 / 查看差异」流程
上面的 `revert*` 方法是**静默**的:它们直接执行反向应用,不做任何校验与二次确认。如果业务方想在自己的入口(自定义按钮、右键菜单等)里复用历史面板那一套**完整交互流程**——即「目标数据已删除的前置校验 + 失败提示」「可差异步骤弹差异确认弹窗 / 其余步骤弹普通二次确认框」「用户确认后才回滚」,以及「查看差异」弹窗——可以直接 import `useHistoryRevert`
```ts
import { useHistoryRevert } from '@tmagic/editor';
// editorRef 为 <m-editor> 组件实例,其 expose 出的即各 service 集合Services
const {
onPageRevert,
onDataSourceRevert,
onCodeBlockRevert,
onPageDiff,
onDataSourceDiff,
onCodeBlockDiff,
} = useHistoryRevert({}, editorRef.value);
// 回滚:可差异步骤弹出差异确认弹窗、其余步骤弹普通二次确认框;用户点「确定」后回滚第 index 步,
// 命中前置校验或用户取消时不执行,返回 null
await onPageRevert(index);
await onDataSourceRevert(id, index);
await onCodeBlockRevert(id, index);
// 查看差异:可差异步骤弹出只读差异弹窗展示前后值差异,无可对比内容时不弹窗
onPageDiff(index);
onDataSourceDiff(id, index);
onCodeBlockDiff(id, index);
```
回滚确认弹窗与查看差异弹窗均由 `useHistoryRevert` 内部**按需动态挂载** `HistoryDiffDialog` 实现,业务方无需自行挂载任何弹窗组件。第二个参数 `options` 可选:
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `appContext` | 否 | 父级应用上下文,用于让动态挂载的差异确认弹窗继承全局组件 / 指令 / provide / 插件Element Plus、`@tmagic/form` 字段组件等)。在组件 `setup` 中调用时会自动取当前组件的 `appContext`,无需手动传;仅当在组件 setup 之外调用时才需显式传入(如 `editorApp._context`)。 |
| `extendState` | 否 | 透传给差异确认弹窗的 `extendState`(同 Editor 的 [`extendFormState`](#自定义对比判断)),使对比表单中依赖业务上下文的 `display` / `disabled``filterFunction` 正常工作。 |
| `dialogWidth` | 否 | 内置页面 / 数据源 / 代码块的差异 / 回滚确认弹窗默认宽度(透传给 `TMagicDialog``width`),如 `'1200px'` / `'80%'`。缺省时使用弹窗内置默认宽度(`900px`)。业务自有历史可在 `viewDiff` / `confirmAndRevert` 调用时通过各自入参的 `width` 单独覆盖。 |
> 若只需要无确认、无校验的静默回滚,直接用上面的 `editorService.revertPageStep` 等即可,无需 `useHistoryRevert`
### 4. 差异对比 ### 4. 差异对比

View File

@ -1,121 +0,0 @@
# 属性配置表单校验联动
编辑器在属性面板(属性表单 / 样式表单)中已支持 [表单校验](../../form-config/rules.md)。默认情况下,表单校验失败时本次改动会被丢弃,不会写入节点。
「属性配置表单校验联动」能力(`enablePropsFormValidate`)允许在**校验失败时仍按当前表单值更新节点**,并把错误信息集中记录到 `editorService`,从而实现:
- 组件树(图层)中对出错节点**标红并显示错误图标**,悬停可查看错误文案;
- 业务在**保存前拦截**存在校验错误的组件(见 [保存拦截](#保存拦截)
- 校验错误随 DSL 操作写入历史快照,**撤销 / 重做能正确还原**错误状态。
> 该能力默认关闭,需通过 `<m-editor :enable-props-form-validate="true">` 显式开启。
## 工作原理
### 错误来源维度
属性面板分「属性」与「样式」两个独立的 `FormPanel`,它们指向同一节点。为避免某个面板校验通过时误清另一个面板记录的错误,错误信息**以来源(`source`)为键分别保存**
- `'props'`:属性表单校验错误
- `'style'`:样式表单校验错误
节点只要**任一来源存在非空错误文案**即视为出错。
内部类型定义见 `NodeInvalidInfo`
```ts
interface NodeInvalidInfo {
/** 属性表单校验错误文案(可能为包含 <br> 的 HTML */
props?: string;
/** 样式表单校验错误文案(可能为包含 <br> 的 HTML */
style?: string;
}
```
### 数据流
1. 表单校验失败时(开启了 `enablePropsFormValidate``FormPanel` 仍以当前表单值提交,并把错误随提交事件一并抛出;
2. `PropsPanel` 在调用 `editorService.update()` 时,把 `invalidInfo: { id, source, error }` 一并传入;
3. `editorService` 在**写入历史记录之前**落库错误标记(`applyInvalidInfo`),使历史快照与本次变更对齐;
4. 错误被记录到 `invalidNodeIds` 状态,并触发 `invalid-node-change` 事件;
5. 组件树节点通过读取 `invalidNodeIds` 响应式地展示标红与错误图标。
校验成功时(或源码编辑器保存时)不携带 `invalidInfo`,保持已有错误状态不变(即不会清除错误)——只有对应来源的表单再次校验通过才会清除该来源的错误。
## 快速开始
```html
<template>
<m-editor :enable-props-form-validate="true"></m-editor>
</template>
```
开启后无需其它配置,属性 / 样式表单校验失败即会自动记录错误并标红组件树节点。
## 读取与清除错误
业务侧可通过 `editorService` 读取或清除错误标记:
```js
import { editorService } from "@tmagic/editor";
// 读取全部错误节点Map<Id, NodeInvalidInfo>
const invalidNodeIds = editorService.getInvalidNodeIds();
// 读取单个节点的错误
const info = editorService.getInvalidNodeInfo("text_123");
// 手动记录 / 清除错误(一般无需手动调用,属性面板已自动维护)
editorService.setInvalidNode("text_123", "props", "标题不能为空");
editorService.deleteInvalidNode("text_123", "props"); // 仅清除属性表单错误
editorService.resetInvalidNodeId(); // 清空全部错误
```
订阅错误状态变化(非响应式消费方,如自定义工具栏):
```js
editorService.on("invalid-node-change", (invalidNodeIds) => {
console.log("校验错误节点数:", invalidNodeIds.size);
});
```
> 响应式读取(如自定义组件树节点内容)请直接读取 `editorService.get('invalidNodeIds')`,其变化会触发 Vue 响应式更新。
## 保存拦截
开启校验联动后,业务可读取 `invalidNodeIds`,在存在校验错误时阻止保存并提示:
```js
import { editorService } from "@tmagic/editor";
import { tMagicMessage } from "@tmagic/design";
const checkInvalidNodes = (services) => {
const invalidNodeIds = services?.editorService.getInvalidNodeIds?.();
if (!invalidNodeIds || invalidNodeIds.size === 0) {
return true;
}
const names = [...invalidNodeIds.keys()].map((id) => {
const node = services?.editorService.getNodeById(id);
return node?.name ? `${node.name}(${id})` : `${id}`;
});
tMagicMessage.error(`以下组件存在配置校验错误,请修复后再保存:${names.join("、")}`);
return false;
};
// 在菜单保存按钮 / 预览保存的 handler 中:
if (!checkInvalidNodes(services)) return;
save();
```
playground 已内置该拦截逻辑,参见 `playground/src/pages/composables/use-editor-menu.ts`
## 与历史记录的关系
错误标记会随 DSL 操作(`add` / `update` 等)一并写入历史快照。编辑器内部在「操作前」与「操作后」分别留存错误状态快照:
- **撤销undo**:还原到操作前的错误状态(撤销一个「校验失败」的改动后错误消失);
- **重做redo**:还原到操作后的错误状态(重做后错误恢复)。
整体替换 `root`(如加载新 DSL后会自动清理不存在于新 DSL 中的失效节点错误记录,避免残留误报;删除节点时也会同步清理其子树相关的错误记录。

View File

@ -2,7 +2,7 @@
tmagic-editor的表单配置核心就是使用了 @tmagic/form 来作为渲染器。@tmagic/form 是一个 npm 包,可以安装它,在你想使用的地方单独使用。 tmagic-editor的表单配置核心就是使用了 @tmagic/form 来作为渲染器。@tmagic/form 是一个 npm 包,可以安装它,在你想使用的地方单独使用。
@tmagic/form 接受一个表单配置,详细配置可参考[表单 api](../../api/form/form-props.md)。字段校验(含 `typeMatch` 类型匹配)见[表单校验](../../form-config/rules.md)。 @tmagic/form 接受一个表单配置,详细配置可参考[表单 api](../../api/form/form-props.md)。
## 安装 ## 安装
@ -45,20 +45,6 @@ app.use(MagicForm);
app.mount("#app"); app.mount("#app");
``` ```
也可在安装时传入自定义 `typeMatch` 规则,详见[表单校验 - 扩展自定义 type 规则](../../form-config/rules.md#扩展自定义-type-规则)
```javascript
app.use(MagicForm, {
typeMatchRules: {
'my-field': (value, { message }) => {
if (typeof value !== 'string') {
return message || 'my-field 应为字符串';
}
},
},
});
```
以上代码便完成了 @tmagic/form 的引入。需要注意的是Element Plus 的样式文件需要单独引入。 以上代码便完成了 @tmagic/form 的引入。需要注意的是Element Plus 的样式文件需要单独引入。

View File

@ -16,7 +16,7 @@ export default defineConfig([
'*/**/public/**/*', '*/**/public/**/*',
'*/**/types/**/*', '*/**/types/**/*',
'*/**/*.config.ts', '*/**/*.config.ts',
'./temp/**/*', './tepm/**/*',
'vite-env.d.ts', 'vite-env.d.ts',
]), ]),
...eslintConfig(path.join(path.dirname(fileURLToPath(import.meta.url)), 'tsconfig.json')), ...eslintConfig(path.join(path.dirname(fileURLToPath(import.meta.url)), 'tsconfig.json')),

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.5",
"name": "tmagic", "name": "tmagic",
"private": true, "private": true,
"type": "module", "type": "module",
@ -40,44 +40,44 @@
"url": "https://github.com/Tencent/tmagic-editor.git" "url": "https://github.com/Tencent/tmagic-editor.git"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^20.5.3", "@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.5.3", "@commitlint/config-conventional": "^20.0.0",
"@tmagic/eslint-config": "workspace:*", "@tmagic/eslint-config": "workspace:*",
"@types/node": "24.0.10", "@types/node": "24.0.10",
"@vitejs/plugin-vue": "^6.0.8", "@vitejs/plugin-vue": "^6.0.6",
"@vitest/coverage-v8": "^4.1.10", "@vitest/coverage-v8": "^4.1.5",
"@vue/compiler-sfc": "catalog:", "@vue/compiler-sfc": "catalog:",
"c8": "^10.1.3", "c8": "^10.1.3",
"commitizen": "^4.3.2", "commitizen": "^4.3.1",
"conventional-changelog-cli": "^5.0.0", "conventional-changelog-cli": "^5.0.0",
"cosmiconfig": "^9.0.2", "cosmiconfig": "^ 9.0.0 ",
"cz-conventional-changelog": "^3.3.0", "cz-conventional-changelog": "^3.3.0",
"element-plus": "^2.14.3", "element-plus": "^2.11.8",
"enquirer": "^2.4.1", "enquirer": "^2.4.1",
"eslint": "^10.7.0", "eslint": "^10.3.0",
"execa": "^9.6.1", "execa": "^9.6.0",
"happy-dom": "^20.10.6", "happy-dom": "^20.9.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"husky": "^9.1.7", "husky": "^9.1.7",
"jsdom": "^27.4.0", "jsdom": "^27.2.0",
"lint-staged": "^16.4.0", "lint-staged": "^16.2.7",
"minimist": "^1.2.8", "minimist": "^1.2.8",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"prettier": "^3.9.5", "prettier": "^3.8.3",
"recast": "^0.23.12", "recast": "^0.23.11",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rolldown": "^1.2.0", "rolldown": "^1.0.1",
"rolldown-plugin-dts": "^0.26.0", "rolldown-plugin-dts": "^0.25.1",
"sass-embedded": "^1.100.0", "sass-embedded": "^1.99.0",
"semver": "^7.8.5", "semver": "^7.7.3",
"serialize-javascript": "^7.0.7", "serialize-javascript": "^7.0.0",
"shx": "^0.3.4", "shx": "^0.3.4",
"typescript": "catalog:", "typescript": "catalog:",
"vite": "catalog:", "vite": "catalog:",
"vitepress": "2.0.0-alpha.18", "vitepress": "^2.0.0-alpha.17",
"vitest": "^4.1.10", "vitest": "^4.1.6",
"vue": "catalog:", "vue": "catalog:",
"vue-tsc": "^3.3.7" "vue-tsc": "^3.2.9"
}, },
"config": { "config": {
"commitizen": { "commitizen": {

View File

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

View File

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

View File

@ -76,12 +76,10 @@ class IteratorContainer extends Node {
} }
} }
if (Array.isArray(config.items)) {
config.items?.forEach((element: MNode) => { config.items?.forEach((element: MNode) => {
this.initNode(element, node, map); this.initNode(element, node, map);
}); });
} }
}
public setNodes(nodes: MNode[], index: number) { public setNodes(nodes: MNode[], index: number) {
const map = this.nodes[index] || new Map(); const map = this.nodes[index] || new Map();

View File

@ -79,12 +79,10 @@ class Page extends Node {
} }
} }
if (Array.isArray(config.items)) {
config.items?.forEach((element: MComponent | MContainer) => { config.items?.forEach((element: MComponent | MContainer) => {
this.initNode(element, node); this.initNode(element, node);
}); });
} }
}
public getNode<T extends TMagicNode = TMagicNode>( public getNode<T extends TMagicNode = TMagicNode>(
id: Id, id: Id,

View File

@ -51,7 +51,7 @@ export const style2Obj = (style: string) => {
export const fillBackgroundImage = (value: string) => { export const fillBackgroundImage = (value: string) => {
if (value && !/^url/.test(value) && !/^linear-gradient/.test(value)) { if (value && !/^url/.test(value) && !/^linear-gradient/.test(value)) {
return `url("${value}")`; return `url(${value})`;
} }
return value; return value;
}; };

View File

@ -50,7 +50,7 @@ describe('style2Obj', () => {
describe('fillBackgroundImage', () => { describe('fillBackgroundImage', () => {
test('裸路径会包裹 url()', () => { test('裸路径会包裹 url()', () => {
expect(fillBackgroundImage('a.png')).toBe('url("a.png")'); expect(fillBackgroundImage('a.png')).toBe('url(a.png)');
}); });
test('已经是 url() 不重复包裹', () => { test('已经是 url() 不重复包裹', () => {
@ -127,7 +127,7 @@ describe('transformStyle', () => {
test('backgroundImage: browser 下补全 url()', () => { test('backgroundImage: browser 下补全 url()', () => {
expect(transformStyle({ backgroundImage: 'a.png' }, 'browser')).toEqual({ expect(transformStyle({ backgroundImage: 'a.png' }, 'browser')).toEqual({
backgroundImage: 'url("a.png")', backgroundImage: 'url(a.png)',
}); });
}); });

View File

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

View File

@ -38,7 +38,8 @@ export interface HttpDataSourceSchema extends DataSourceSchema {
}; };
autoFetch?: boolean; autoFetch?: boolean;
beforeRequest: beforeRequest:
string | ((options: HttpOptions, content: { app: TMagicApp; dataSource: HttpDataSource }) => HttpOptions); | string
| ((options: HttpOptions, content: { app: TMagicApp; dataSource: HttpDataSource }) => HttpOptions);
afterResponse: afterResponse:
| string | string
| ((response: any, content: { app: TMagicApp; dataSource: HttpDataSource; options: Partial<HttpOptions> }) => any); | ((response: any, content: { app: TMagicApp; dataSource: HttpDataSource; options: Partial<HttpOptions> }) => any);

View File

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

View File

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

View File

@ -1,23 +0,0 @@
<template>
<component class="tmagic-design-alert" :is="uiComponent" v-bind="uiProps">
<slot></slot>
</component>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { getDesignConfig } from './config';
import type { AlertProps } from './types';
defineOptions({
name: 'TMAlert',
});
const props = defineProps<AlertProps>();
const ui = getDesignConfig('components')?.alert;
const uiComponent = ui?.component || 'el-alert';
const uiProps = computed<AlertProps>(() => ui?.props(props) || props);
</script>

View File

@ -1,5 +1,5 @@
<template> <template>
<component :is="uiComponent" :class="['tmagic-design-card', { 'tmagic-design-card--flat': isFlat }]" v-bind="uiProps"> <component class="tmagic-design-card" :is="uiComponent" v-bind="uiProps">
<template #header v-if="$slots.header"> <template #header v-if="$slots.header">
<slot name="header" class="header"></slot> <slot name="header" class="header"></slot>
</template> </template>
@ -14,7 +14,6 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { getDesignConfig } from './config'; import { getDesignConfig } from './config';
import { isGlobalFlat } from './index';
import type { CardProps } from './types'; import type { CardProps } from './types';
defineOptions({ defineOptions({
@ -27,14 +26,5 @@ const ui = getDesignConfig('components')?.card;
const uiComponent = ui?.component || 'el-card'; const uiComponent = ui?.component || 'el-card';
// `<MEditor>` / `<MForm>` theme `magic-admin` const uiProps = computed<CardProps>(() => ui?.props(props) || props);
// `flat=true` `flat`
const isFlat = computed(() => !!props.flat || isGlobalFlat.value);
// `flat` UI el-card / t-card props
// / DOM
const uiProps = computed<CardProps>(() => {
const { flat: _flat, ...rest } = props;
return ui?.props(rest as CardProps) || (rest as CardProps);
});
</script> </script>

View File

@ -7,39 +7,18 @@
@update:modelValue="updateModelValue" @update:modelValue="updateModelValue"
> >
</component> </component>
<MInput
v-if="isFlat && !isLargeStageContainer"
@update:modelValue="updateModelValue"
@change="changeHandler"
v-bind="uiProps"
class="tmagic-design-color-picker-input"
clearable
></MInput>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, type ComputedRef, inject } from 'vue'; import { computed } from 'vue';
import { getDesignConfig } from './config'; import { getDesignConfig } from './config';
import { isGlobalFlat } from './index';
import MInput from './Input.vue';
import type { ColorPickerProps } from './types'; import type { ColorPickerProps } from './types';
defineOptions({ defineOptions({
name: 'TMColorPicker', name: 'TMColorPicker',
}); });
const isFlat = computed(() => !!props.flat || isGlobalFlat.value);
// `<MEditor>` computed provide props.isLargeStageContainer
// `ComputedRef<boolean>` `<script setup>`
// `!isLargeStageContainer` computed
// `false` `ComputedRef`
const isLargeStageContainer = inject<ComputedRef<boolean>>(
'isLargeStageContainer',
computed(() => false),
);
const props = withDefaults(defineProps<ColorPickerProps>(), { const props = withDefaults(defineProps<ColorPickerProps>(), {
showAlpha: false, showAlpha: false,
disabled: false, disabled: false,

View File

@ -35,16 +35,6 @@ defineExpose({
return form.value?.validate(); return form.value?.validate();
}, },
clearValidate(props?: string | string[]) {
if (typeof form.value?.clearValidate === 'function') {
return form.value?.clearValidate(props);
}
// tdesign 使 clearValidateelement-plus clearValidate
if (typeof form.value?.clearValidateState === 'function') {
return form.value?.clearValidateState();
}
},
resetFields() { resetFields() {
if (typeof form.value?.resetFields === 'function') { if (typeof form.value?.resetFields === 'function') {
return form.value?.resetFields(); return form.value?.resetFields();

View File

@ -1,65 +1,36 @@
<template> <template>
<component <component class="tmagic-design-form-item" :is="uiComponent" v-bind="uiProps">
class="tmagic-design-form-item"
:class="{ 'has-extra-tips': adapterType === 'element-plus' && extraTips }"
:is="uiComponent"
v-bind="uiProps"
>
<template #label> <template #label>
<slot name="label"></slot> <slot name="label"></slot>
</template> </template>
<template #default> <template #default>
<slot></slot> <slot></slot>
<alert <div v-if="adapterType === 'element-plus' && extra" v-html="extra" class="m-form-tip"></div>
v-if="adapterType === 'element-plus' && extraTips"
:title="extraTips"
type="warning"
show-icon
:closable="false"
></alert>
<div v-else-if="adapterType === 'element-plus' && extra" v-html="extra" class="m-form-tip"></div>
</template>
<template v-if="adapterType === 'element-plus'" #error="{ error }">
<div class="el-form-item__error">{{ resolveErrorText(error) }}</div>
</template> </template>
</component> </component>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject } from 'vue'; import { computed } from 'vue';
import Alert from './Alert.vue';
import { getDesignConfig } from './config'; import { getDesignConfig } from './config';
import { stripValidateSuggestion } from './formValidateMessage';
import { isGlobalFlat } from './index';
import type { FormItemProps } from './types'; import type { FormItemProps } from './types';
defineOptions({ defineOptions({
name: 'TMFormItem', name: 'TMFormItem',
}); });
const props = defineProps<FormItemProps & { theme?: string }>(); const props = defineProps<FormItemProps>();
const ui = getDesignConfig('components')?.formItem; const ui = getDesignConfig('components')?.formItem;
const uiComponent = ui?.component || 'el-form-item'; const uiComponent = ui?.component || 'el-form-item';
const adapterType = getDesignConfig('adapterType'); const adapterType = getDesignConfig('adapterType');
const formInline = inject<boolean>('formInline');
const formInRow = inject<boolean>('isInRow');
const uiProps = computed<FormItemProps>(() => { const uiProps = computed<FormItemProps>(() => {
const { extra, extraTips, ...rest } = ui?.props(props) || props; const { extra, ...rest } = ui?.props(props) || props;
if (isGlobalFlat.value && rest.labelPosition === undefined) {
return { ...rest, labelPosition: formInline || formInRow ? 'right' : 'left' };
}
return rest; return rest;
}); });
/**
* 校验错误文案中修改建议仅用于错误汇总展示
* form-item 行内错误只展示主错误描述不展示修改建议
*/
const resolveErrorText = (error?: string) => stripValidateSuggestion(error);
</script> </script>

View File

@ -4,9 +4,10 @@
<div <div
v-if="popoverVisible || !destroyOnClose" v-if="popoverVisible || !destroyOnClose"
v-show="popoverVisible" v-show="popoverVisible"
class="tmagic-design-popper"
ref="popperElementRef" ref="popperElementRef"
:tabindex="tabindex" :tabindex="tabindex"
:class="['tmagic-design-popper', popperClass, themeClass]" :class="popperClass"
:style="style" :style="style"
@mouseenter.once="popperMouseenterHandler" @mouseenter.once="popperMouseenterHandler"
> >
@ -21,7 +22,7 @@ import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref
import type { Instance } from '@popperjs/core'; import type { Instance } from '@popperjs/core';
import { createPopper } from '@popperjs/core'; import { createPopper } from '@popperjs/core';
import { useThemeClass, useZIndex } from './index'; import { useZIndex } from './index';
import type { PopoverProps } from './types'; import type { PopoverProps } from './types';
defineSlots<{ defineSlots<{
@ -45,12 +46,6 @@ const props = withDefaults(defineProps<PopoverProps>(), {
closeOnClickOutside: true, closeOnClickOutside: true,
}); });
/**
* 主题修饰类来自最近的 `<MEditor>` / `<MForm>` 祖先 provide
* 挂在 `Teleport` 出去的 popper 根节点上让主题级 CSS 变量`--el-color-primary`
* portal 节点上也能命中详见 `@tmagic/design/theme.ts`
*/
const themeClass = useThemeClass();
const emit = defineEmits<{ const emit = defineEmits<{
/** 受控模式(传入了 visible下点击外部收起时触发便于配合 v-model:visible。 */ /** 受控模式(传入了 visible下点击外部收起时触发便于配合 v-model:visible。 */
'update:visible': [_visible: boolean]; 'update:visible': [_visible: boolean];

View File

@ -1,47 +0,0 @@
/*
* 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
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
*
*
* typeMatch
* `主错误描述${VALIDATE_SUGGESTION_SEPARATOR}修改建议`
* - form-item tooltip
* -
*
* /
* / helper
*/
export const VALIDATE_SUGGESTION_SEPARATOR = '\n\n';
/**
*
*
* @param message
* @param suggestion
*/
export const appendValidateSuggestion = (message: string, suggestion?: string): string =>
suggestion ? `${message}${VALIDATE_SUGGESTION_SEPARATOR}${suggestion}` : message;
/**
*
*
* @param text `主错误描述\n\n修改建议`
*/
export const stripValidateSuggestion = (text?: string): string =>
String(text ?? '').split(VALIDATE_SUGGESTION_SEPARATOR)[0];

View File

@ -8,8 +8,6 @@ import './theme/index.scss';
export * from './types'; export * from './types';
export * from './config'; export * from './config';
export * from './theme';
export * from './formValidateMessage';
export { default as TMagicAutocomplete } from './Autocomplete.vue'; export { default as TMagicAutocomplete } from './Autocomplete.vue';
export { default as TMagicBadge } from './Badge.vue'; export { default as TMagicBadge } from './Badge.vue';
@ -95,10 +93,6 @@ export let useZIndex = (zIndexOverrides?: Ref<number>) => {
}; };
}; };
// 与 form/utils/config、editor/utils/config 里的同名变量保持一致:用 ref 持有 flat 全局开关,
// const 引用本身不可变,规避 `no-mutable-exports` / `naming-convention` 对 `let` 模块变量的限制。
export const isGlobalFlat = ref(false);
export default { export default {
install(app: App, options: DesignPluginOptions) { install(app: App, options: DesignPluginOptions) {
tMagicMessage = tMagicMessage =
@ -134,8 +128,6 @@ export default {
useZIndex = options.useZIndex; useZIndex = options.useZIndex;
} }
isGlobalFlat.value = options.flat ?? false;
if (options.adapterType && globalThis.document?.documentElement) { if (options.adapterType && globalThis.document?.documentElement) {
globalThis.document.documentElement.classList.add(`tmagic-adapter-${options.adapterType}`); globalThis.document.documentElement.classList.add(`tmagic-adapter-${options.adapterType}`);
} }

View File

@ -1,59 +0,0 @@
/*
* 穿 portal
*
* `<MEditor>` / `<MForm>` provide `theme` `<Teleport>`
* `useThemeClass()` inject
* CSS `--el-color-primary` body portal
*
* portal `m-editor` `m-form`使
* `m-theme--<theme>` CSS
* editor / form / portal editor / form
* `.m-editor.m-editor--<theme>` / `.m-form.m-form--<theme>`
*
*/
import type { ComputedRef, InjectionKey, Ref } from 'vue';
import { computed, inject } from 'vue';
/** 祖先 `<MEditor>` / `<MForm>` theme props.theme MForm
* theme `<MEditor>` Form.vue effectiveTheme */
export const M_THEME_KEY: InjectionKey<Ref<string>> = Symbol('mTheme');
/**
* `<MEditor>` / `<MForm>` provide class
* `"m-theme--magic-admin"`
*
* -
* - `m-editor` / `m-form` portal
* editor / form DOM
*
* 使 `<Teleport>` `TMagicPopover` / `FloatingBox` /
* `ContentMenu` CSS
*/
export const useThemeClass = (): ComputedRef<string> => {
const theme = inject(M_THEME_KEY, null);
return computed(() => {
const t = theme?.value;
return t ? `m-theme--${t}` : '';
});
};
/**
* theme `prop.theme` 退
* `<MEditor>` / `<MForm>` provide
*
* `prop` `theme?: string` `defineProps()`
* reactive proxycomputed
* `toRefs`
*
* `prop.theme`
*
* @example
* const props = defineProps<{ theme?: string }>();
* const theme = useTheme(props);
* // theme.value -> 'magic-admin' 或 ''
*/
export const useTheme = <T extends { theme?: string }>(prop: T): ComputedRef<string> => {
const injected = inject(M_THEME_KEY, null);
return computed(() => prop.theme || injected?.value || '');
};

View File

@ -1,67 +0,0 @@
// `TMagicCard` 铺平模式去掉卡片自身的视觉表达背景 / 阴影 / 边框 / 圆角 /
// header 分隔线 / header & body 内边距让卡片在视觉上与外层容器融为一体
//
// 不动 body-style不动折叠相关 prop 这些上层用法仍可正常工作
// `is-always-shadow` el-card `shadow="always"` 时附加的类需要单独再压一次
.tmagic-design-card.tmagic-design-card--flat {
background: #fff;
box-shadow: none !important;
border-top: 0;
border-left: 0;
border-right: 0;
border-radius: 0;
border-bottom: 0;
&.m-fields-group-list-item {
margin-bottom: 0;
}
&.is-always-shadow {
box-shadow: none;
}
.el-card__header {
border-bottom: none;
padding: 16px;
&:hover {
background: transparent;
box-shadow: none;
cursor: default;
}
}
.el-card__body {
padding: 0 16px 0 16px;
// 这里每个form-item应该要有一个16px间距但是除了tbody中的form-item
.tmagic-design-form-item:not(tbody .tmagic-design-form-item) {
margin-bottom: 16px;
}
> .m-container-row {
overflow-x: hidden;
}
}
.tmagic-design-card--flat {
// .el-card__body {
// padding: 0;
// }
// .el-card__header {
// padding: 0 16px;
// }
}
}
.m-split-title-card-container {
> .tmagic-design-card.tmagic-design-card--flat {
background-color: transparent;
> .el-card__header {
margin-bottom: 10px;
padding: 0;
font-size: 16px;
font-weight: 500;
line-height: 24px;
}
> .el-card__body {
background-color: #fff;
padding-top: 16px;
}
}
}

View File

@ -1,4 +0,0 @@
.tmagic-design-color-picker-input {
margin-left: 8px;
width: calc(var(--el-input-width, 100%) - 40px);
}

View File

@ -1,3 +1 @@
@use "./popover.scss"; @use "./popover.scss";
@use "./card.scss";
@use "./colorPicker.scss";

View File

@ -1,38 +0,0 @@
@use "../../index.scss";
.m-form.m-form--magic-admin {
.el-collapse-item__header {
background-color: transparent;
padding: 0;
border: 0;
padding-left: 0;
font-size: 16px;
font-weight: 500;
line-height: 24px;
margin-top: 8px;
margin-bottom: 8px;
}
}
.tmagic-design-form-item {
&.has-extra-tips {
> .el-form-item__content {
gap: 10px;
> .tmagic-design-alert {
border-radius: 2px;
width: fit-content;
padding: 0 8px;
color: var(--el-color-primary, #0056ea);
background-color: var(--el-color-primary-light-9, rgb(236, 245, 255));
.el-icon.el-alert__icon {
fill: var(--el-color-primary, #0056ea);
color: var(--el-color-primary, #0056ea);
}
.el-alert__title {
font-size: 14px;
line-height: 22px;
}
}
}
}
}

View File

@ -24,17 +24,6 @@ export interface BadgeProps {
hidden?: boolean; hidden?: boolean;
} }
export interface AlertProps {
title?: string;
/** ElAlert 实际支持的 type与 ButtonProps.type 不同,没有 `primary` / `danger` */
type?: 'success' | 'warning' | 'info' | 'error' | 'primary';
description?: string;
closable?: boolean;
center?: boolean;
closeText?: string;
showIcon?: boolean;
effect?: 'light' | 'dark';
}
export interface ButtonProps { export interface ButtonProps {
type?: string; type?: string;
size?: FieldSize; size?: FieldSize;
@ -50,11 +39,6 @@ export interface CardProps {
bodyStyle?: Record<string, any>; bodyStyle?: Record<string, any>;
shadow?: string; shadow?: string;
header?: string; header?: string;
/**
* / / / / header 线 / header body
* 使TMagicCard prop 使
*/
flat?: boolean;
} }
export interface CascaderProps { export interface CascaderProps {
@ -113,7 +97,6 @@ export interface ColorPickerProps {
disabled?: boolean; disabled?: boolean;
showAlpha?: boolean; showAlpha?: boolean;
size?: FieldSize; size?: FieldSize;
flat?: boolean;
} }
export interface DatePickerProps { export interface DatePickerProps {
@ -202,9 +185,7 @@ export interface FormItemProps {
labelWidth?: string | number; labelWidth?: string | number;
rules?: any; rules?: any;
extra?: string; extra?: string;
extraTips?: string;
labelPosition?: 'top' | 'left' | 'right'; labelPosition?: 'top' | 'left' | 'right';
text?: string;
} }
export interface InputProps { export interface InputProps {
@ -253,9 +234,6 @@ export interface PaginationProps {
export interface PopconfirmProps { export interface PopconfirmProps {
title?: string; title?: string;
width?: string | number;
/** 浮层是否插入到 body默认 true。设为 false 时浮层内联渲染,便于嵌套在 hover 浮层中避免父级因 mouseleave 收起。 */
teleported?: boolean;
placement?: placement?:
| 'top' | 'top'
| 'left' | 'left'
@ -483,11 +461,6 @@ export interface Components {
props: (props: BadgeProps) => BadgeProps; props: (props: BadgeProps) => BadgeProps;
}; };
alert: {
component: DefineComponent<AlertProps, {}, any> | string;
props: (props: AlertProps) => AlertProps;
};
autocomplete: { autocomplete: {
component: DefineComponent<AutocompleteProps, {}, any> | string; component: DefineComponent<AutocompleteProps, {}, any> | string;
props: (props: AutocompleteProps) => AutocompleteProps; props: (props: AutocompleteProps) => AutocompleteProps;
@ -750,7 +723,6 @@ export interface Components {
} }
export interface DesignPluginOptions { export interface DesignPluginOptions {
flat?: boolean;
adapterType?: string; adapterType?: string;
message?: TMagicMessage; message?: TMagicMessage;
messageBox?: TMagicMessageBox; messageBox?: TMagicMessageBox;

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.5",
"name": "@tmagic/editor", "name": "@tmagic/editor",
"type": "module", "type": "module",
"sideEffects": [ "sideEffects": [

View File

@ -4,7 +4,6 @@
:page-bar-sort-options="pageBarSortOptions" :page-bar-sort-options="pageBarSortOptions"
:page-filter-function="pageFilterFunction" :page-filter-function="pageFilterFunction"
:hide-sidebar="hideSidebar" :hide-sidebar="hideSidebar"
:theme="theme"
> >
<template #header> <template #header>
<slot name="header"></slot> <slot name="header"></slot>
@ -136,10 +135,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { computed, provide, ref } from 'vue'; import { provide } from 'vue';
import type { MApp } from '@tmagic/core'; import type { MApp } from '@tmagic/core';
import { M_THEME_KEY } from '@tmagic/design';
import Framework from './layouts/Framework.vue'; import Framework from './layouts/Framework.vue';
import TMagicNavMenu from './layouts/NavMenu.vue'; import TMagicNavMenu from './layouts/NavMenu.vue';
@ -160,7 +158,7 @@ import stageOverlayService from './services/stageOverlay';
import storageService from './services/storage'; import storageService from './services/storage';
import uiService from './services/ui'; import uiService from './services/ui';
import keybindingConfig from './utils/keybinding-config'; import keybindingConfig from './utils/keybinding-config';
import { defaultEditorProps, EditorProps, ENABLE_PROPS_FORM_VALIDATE } from './editorProps'; import { defaultEditorProps, EditorProps } from './editorProps';
import { initServiceEvents, initServiceState } from './initService'; import { initServiceEvents, initServiceState } from './initService';
import type { TreeNodeData } from './type'; import type { TreeNodeData } from './type';
import type { EditorSlots, EventBus, Services, StageOptions } from './type'; import type { EditorSlots, EventBus, Services, StageOptions } from './type';
@ -229,14 +227,10 @@ const stageOptions: StageOptions = {
stageOverlayService.set('stageOptions', stageOptions); stageOverlayService.set('stageOptions', stageOptions);
const propsPanelRef = ref<InstanceType<typeof FormPanel> | null>(null);
provide('services', services); provide('services', services);
provide('codeOptions', props.codeOptions); provide('codeOptions', props.codeOptions);
provide('stageOptions', stageOptions); provide('stageOptions', stageOptions);
/** 是否启用「属性配置表单校验」联动能力,供 PropsPanel / FormPanel 判断校验失败时是否仍更新节点并记录错误 */
provide(ENABLE_PROPS_FORM_VALIDATE, props.enablePropsFormValidate ?? false);
/** /**
* 把顶层 `extendFormState` 提供给非 PropsPanel 链路上的组件使用例如历史差异对话框 HistoryDiffDialog * 把顶层 `extendFormState` 提供给非 PropsPanel 链路上的组件使用例如历史差异对话框 HistoryDiffDialog
* 内部的 CompareForm这样所有依赖业务上下文的表单 filterFunction 都能拿到一致的扩展状态 * 内部的 CompareForm这样所有依赖业务上下文的表单 filterFunction 都能拿到一致的扩展状态
@ -244,20 +238,6 @@ provide(ENABLE_PROPS_FORM_VALIDATE, props.enablePropsFormValidate ?? false);
*/ */
provide('extendFormState', props.extendFormState); provide('extendFormState', props.extendFormState);
// computed provide provide
// props.isLargeStageContainer @tmagic/design/ColorPicker
// `inject<ComputedRef<boolean>>('isLargeStageContainer', computed(() => false))`
// `.value`
provide(
'isLargeStageContainer',
computed(() => props.isLargeStageContainer),
);
/**
* 提供 PropsPanel 主属性表单的 formState getter供历史差异弹窗复用
* CompareForm PropsPanel filterFunction 上下文保持一致
*/
provide('getPropsPanelFormState', () => propsPanelRef.value?.configForm?.formState);
/** /**
* 把历史记录面板的自定义扩展 tab 提供给深层的 HistoryListPanel它挂在 NavMenu * 把历史记录面板的自定义扩展 tab 提供给深层的 HistoryListPanel它挂在 NavMenu
* markRaw component 形式渲染无法直接通过 props 透传业务方可借此在历史记录 * markRaw component 形式渲染无法直接通过 props 透传业务方可借此在历史记录
@ -267,22 +247,10 @@ provide('historyListExtraTabs', props.historyListExtraTabs);
provide<EventBus>('eventBus', new EventEmitter()); provide<EventBus>('eventBus', new EventEmitter());
/**
* 把当前主题以响应式 ref 形式 provide 给后代供包含 `Teleport` 的组件
* `TMagicPopover` / `FloatingBox` / `ContentMenu`在传送目标上挂 `m-theme--<theme>`
* 让主题级 CSS 变量在 portal 节点上也能命中详见 `@tmagic/design/theme.ts`
*/
provide(
M_THEME_KEY,
computed(() => props.theme ?? ''),
);
const propsPanelMountedHandler = (e: InstanceType<typeof FormPanel>) => { const propsPanelMountedHandler = (e: InstanceType<typeof FormPanel>) => {
propsPanelRef.value = e;
emit('props-panel-mounted', e); emit('props-panel-mounted', e);
}; };
const propsPanelUnmountedHandler = () => { const propsPanelUnmountedHandler = () => {
propsPanelRef.value = null;
emit('props-panel-unmounted'); emit('props-panel-unmounted');
}; };

View File

@ -4,9 +4,7 @@
v-model:visible="boxVisible" v-model:visible="boxVisible"
v-model:width="width" v-model:width="width"
v-model:height="codeBlockEditorHeight" v-model:height="codeBlockEditorHeight"
:body-style="{ padding: '0 16px' }"
:title="content.name ? `${disabled ? '查看' : '编辑'}${content.name}` : '新增代码'" :title="content.name ? `${disabled ? '查看' : '编辑'}${content.name}` : '新增代码'"
:framework-width="frameworkWidth"
:position="boxPosition" :position="boxPosition"
:before-close="beforeClose" :before-close="beforeClose"
> >
@ -208,7 +206,6 @@ const closedHandler = () => {
const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null)); const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null));
const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating); const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating);
const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
watch(boxVisible, (visible) => { watch(boxVisible, (visible) => {
nextTick(() => { nextTick(() => {

View File

@ -10,45 +10,100 @@
:is-compare="true" :is-compare="true"
:disabled="true" :disabled="true"
:label-width="labelWidth" :label-width="labelWidth"
:extend-state="mergedExtendState" :extend-state="extendState"
:show-diff="showDiff" :show-diff="showDiff"
:self-diff-field-types="selfDiffFieldTypes"
:size="size"
></MForm> ></MForm>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, type Ref, type ShallowRef } from 'vue'; import { computed, inject, type Ref, ref, type ShallowRef, useTemplateRef, watch, watchEffect } from 'vue';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { type CodeBlockContent, HookType } from '@tmagic/core'; import { type CodeBlockContent, type DataSourceSchema, HookType, type MNode } from '@tmagic/core';
import { type FormConfig, type FormValue, MForm } from '@tmagic/form'; import { type FormConfig, type FormState, type FormValue, MForm } from '@tmagic/form';
import { useCompareForm } from '@editor/hooks/use-compare-form'; import { useServices } from '@editor/hooks/use-services';
import type { CompareFormBaseProps } from '@editor/type'; import type { CompareCategory, CompareFormLoadConfig } from '@editor/type';
import { getCodeBlockFormConfig } from '@editor/utils/code-block';
defineOptions({ defineOptions({
name: 'MEditorCompareForm', name: 'MEditorCompareForm',
}); });
const props = withDefaults( const props = withDefaults(
defineProps< defineProps<{
CompareFormBaseProps & { /** 当前值(修改后的值) */
value: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
/** 用于对比的旧值(修改前的值) */ /** 用于对比的旧值(修改前的值) */
lastValue?: CompareFormBaseProps['value']; lastValue?: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
/** 需要走 self diff 的字段类型(例如 mod-cond。 */ /**
selfDiffFieldTypes?: string[]; * 类型说明
} * - `category` `node` `type` 为节点组件的类型例如 'text''button''page''container'
>(), * - `category` `data-source` `type` 为数据源类型例如 'base''http'
* - `category` `code-block` `type` 可不传
*/
type?: string;
/** 表单配置类别,决定从哪里取 FormConfig */
category?: CompareCategory;
/** 数据源代码块场景下的数据源类型base/http用于代码块表单中"执行时机"展示 */
dataSourceType?: string;
labelWidth?: string;
/**
* 外层容器高度设置后表单内容超出时会在 CompareForm 内部出现滚动条
* 避免 dialog / 面板使用方需要自行处理滚动可传任意 CSS 长度例如 `60vh` / `400px` / `100%`
*/
height?: string;
/**
* 用户自定义注入到 MForm.formState 的扩展字段 Editor 顶层的 `extendFormState`
* PropsPanel `extend-state` 语义一致表单 item `display` / `disabled`
* filterFunction 经常依赖这里注入的字段 stage自定义业务上下文等
* 因此在差异对比场景下也需要透传避免出现 `formState.xxx is undefined` 的运行时错误
*/
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/**
* 自定义 FormConfig 加载逻辑传入后将接管内置的按 `category`(node/data-source/code-block)
* 取配置逻辑调用方可根据业务自行返回或异步返回表单配置可通过
* `ctx.defaultLoadConfig()` 复用默认结果再做二次加工返回的 config 直接用于对比展示
*/
loadConfig?: CompareFormLoadConfig;
}>(),
{ {
category: 'node', category: 'node',
labelWidth: '120px', labelWidth: '120px',
}, },
); );
const { config, currentValues, wrapperStyle, mergedExtendState, loadConfig, formRef, normalizeCodeBlockValue } = const { propsService, dataSourceService, codeBlockService, editorService } = useServices();
useCompareForm(props); const services = useServices();
const config = ref<FormConfig>([]);
/** vs-code 编辑器的 monaco 配置项,沿用 Editor 顶层 provide('codeOptions', ...) 的注入。 */
const codeOptions = inject<Record<string, any>>('codeOptions', {});
/** 将代码块的 content 字段统一成字符串,便于在表单/对比中展示 */
const normalizeCodeBlockValue = (
v: Partial<CodeBlockContent> | Record<string, any> | undefined,
): Record<string, any> => {
if (!v) return {};
const next: Record<string, any> = { ...v };
if (next.content && typeof next.content !== 'string') {
try {
next.content = next.content.toString();
} catch {
next.content = '';
}
}
return next;
};
const currentValues = computed<FormValue>(() => {
if (props.category === 'code-block') {
return normalizeCodeBlockValue(props.value as Partial<CodeBlockContent>);
}
return (props.value || {}) as FormValue;
});
const lastValuesProcessed = computed<FormValue>(() => { const lastValuesProcessed = computed<FormValue>(() => {
if (props.category === 'code-block') { if (props.category === 'code-block') {
@ -57,6 +112,18 @@ const lastValuesProcessed = computed<FormValue>(() => {
return (props.lastValue || {}) as FormValue; return (props.lastValue || {}) as FormValue;
}); });
/**
* 外层包裹层的样式当传入 `height` 时启用固定高度 + 内部滚动
* 这样滚动条会出现在 CompareForm 内部避免父容器 Dialog自身也产生滚动
*/
const wrapperStyle = computed(() => {
if (!props.height) return undefined;
return {
height: props.height,
overflow: 'auto',
} as Record<string, string>;
});
/** /**
* `code-select` 字段在历史数据中存在两种"语义为空"的形态 * `code-select` 字段在历史数据中存在两种"语义为空"的形态
* - 字符串 `''`旧数据 / 用户从未配置过钩子 * - 字符串 `''`旧数据 / 用户从未配置过钩子
@ -85,6 +152,100 @@ const showDiff = ({ curValue, lastValue, config }: { curValue: any; lastValue: a
return !isEqual(curValue, lastValue); return !isEqual(curValue, lastValue);
}; };
const removeStyleDisplayConfig = (formConfig: FormConfig): FormConfig =>
formConfig.map((item) => {
if (!('type' in item)) return item;
if (item?.type !== 'tab' || !Array.isArray(item.items)) return item;
return {
...item,
items: item.items.map((tabPane) => {
if (tabPane?.title !== '样式' || !Array.isArray(tabPane.items)) return tabPane;
return {
...tabPane,
display: true,
};
}),
};
});
/**
* 内置的默认 FormConfig 加载逻辑 `category` 从对应 service / 工具取配置
* 作为 ctx.defaultLoadConfig 透传给自定义 `loadConfig`方便复用与二次加工
*/
const defaultLoadConfig = async (): Promise<FormConfig> => {
switch (props.category) {
case 'node': {
if (!props.type) {
return [];
}
return removeStyleDisplayConfig(
await propsService.getPropsConfig(props.type, { node: props.value as unknown as MNode }),
);
}
case 'data-source': {
return dataSourceService.getFormConfig(props.type || 'base');
}
case 'code-block': {
return getCodeBlockFormConfig({
paramColConfig: codeBlockService.getParamsColConfig(),
// dataSourceType "" props.dataSourceType
// step
isDataSource: () => Boolean(props.dataSourceType),
dataSourceType: () => props.dataSourceType,
codeOptions,
// /
editable: false,
});
}
default:
return [];
}
};
const loadConfig = async () => {
if (props.loadConfig) {
config.value = await props.loadConfig({
category: props.category,
type: props.type,
dataSourceType: props.dataSourceType,
defaultLoadConfig,
});
return;
}
config.value = await defaultLoadConfig();
};
watch(
[() => props.category, () => props.type, () => props.dataSourceType, () => props.loadConfig],
() => {
loadConfig();
},
{ immediate: true },
);
const formRef = useTemplateRef<InstanceType<typeof MForm>>('form');
/**
* services / stage 注入 MForm formState避免 propsService 注入的表单配置中
* 形如 `display: ({ services }) => services.uiService.get(...)` filterFunction
* 在执行时拿不到 `formState.services` 而报错
*
* props-panel/FormPanel.vue 中的注入方式保持一致
* - services整个 useServices() 返回的服务集合
* - stage当前 editorService.get('stage') 的最新值
*/
const stage = computed(() => editorService.get('stage'));
watchEffect(() => {
if (formRef.value) {
formRef.value.formState.stage = stage.value;
formRef.value.formState.services = services;
}
});
defineExpose<{ defineExpose<{
form: ShallowRef<InstanceType<typeof MForm> | null>; form: ShallowRef<InstanceType<typeof MForm> | null>;
config: Ref<FormConfig>; config: Ref<FormConfig>;

View File

@ -2,8 +2,8 @@
<transition name="fade"> <transition name="fade">
<div <div
v-show="visible" v-show="visible"
class="magic-editor-content-menu"
ref="menu" ref="menu"
:class="['magic-editor-content-menu', themeClass]"
:style="menuStyle" :style="menuStyle"
@mouseenter="mouseenterHandler()" @mouseenter="mouseenterHandler()"
@contextmenu.prevent @contextmenu.prevent
@ -39,7 +39,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, nextTick, onBeforeUnmount, onMounted, type Ref, ref, useTemplateRef } from 'vue'; import { computed, nextTick, onBeforeUnmount, onMounted, type Ref, ref, useTemplateRef } from 'vue';
import { useThemeClass, useZIndex } from '@tmagic/design'; import { useZIndex } from '@tmagic/design';
import { MenuButton, MenuComponent } from '@editor/type'; import { MenuButton, MenuComponent } from '@editor/type';
@ -73,14 +73,6 @@ const menuEl = useTemplateRef<HTMLDivElement>('menu');
const buttonRefs = useTemplateRef<InstanceType<typeof ToolButton>[]>('buttons'); const buttonRefs = useTemplateRef<InstanceType<typeof ToolButton>[]>('buttons');
const subMenuRef = useTemplateRef<any>('subMenu'); const subMenuRef = useTemplateRef<any>('subMenu');
const visible = ref(false); const visible = ref(false);
/**
* 主题修饰类来自最近的 `<MEditor>` / `<MForm>` 祖先 provide
* - 顶层 `ContentMenu` 渲染在编辑器子树中加上去是冗余但无害
* - 子菜单递归 `<content-menu :is-sub-menu="true">` `<teleport to="body">` 送到 body
* 不在编辑器子树中必须显式挂上主题类主题级 CSS 变量才能命中
*/
const themeClass = useThemeClass();
const subMenuData: Ref<(MenuButton | MenuComponent)[]> = ref<(MenuButton | MenuComponent)[]>([]); const subMenuData: Ref<(MenuButton | MenuComponent)[]> = ref<(MenuButton | MenuComponent)[]>([]);
const zIndex = useZIndex(); const zIndex = useZIndex();
const curZIndex = ref<number>(0); const curZIndex = ref<number>(0);
@ -128,42 +120,20 @@ const outsideClickHideHandler = (e: MouseEvent) => {
hide(); hide();
}; };
//
const fixPosition = () => {
const menu = menuEl.value;
if (!menu || !visible.value) return;
const menuHeight = menu.clientHeight;
const menuWidth = menu.clientWidth;
let { top, left } = menuPosition.value;
if (top + menuHeight > document.body.clientHeight) {
top = Math.max(0, document.body.clientHeight - menuHeight);
}
if (left + menuWidth > document.body.clientWidth) {
left = Math.max(0, document.body.clientWidth - menuWidth);
}
if (top !== menuPosition.value.top || left !== menuPosition.value.left) {
menuPosition.value = { top, left };
}
};
const setPosition = (e: { clientY: number; clientX: number }) => { const setPosition = (e: { clientY: number; clientX: number }) => {
const menuHeight = menuEl.value?.clientHeight || 0;
let top = e.clientY;
if (menuHeight + e.clientY > document.body.clientHeight) {
top = document.body.clientHeight - menuHeight;
}
menuPosition.value = { menuPosition.value = {
top: e.clientY, top,
left: e.clientX, left: e.clientX,
}; };
fixPosition();
}; };
//
const resizeObserver = new ResizeObserver(() => {
fixPosition();
});
const show = (e?: { clientY: number; clientX: number }) => { const show = (e?: { clientY: number; clientX: number }) => {
visible.value = true; visible.value = true;
@ -208,18 +178,12 @@ const mouseenterHandler = () => {
}; };
onMounted(() => { onMounted(() => {
if (menuEl.value) {
resizeObserver.observe(menuEl.value);
}
if (props.isSubMenu) return; if (props.isSubMenu) return;
globalThis.addEventListener('mousedown', outsideClickHideHandler, true); globalThis.addEventListener('mousedown', outsideClickHideHandler, true);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
resizeObserver.disconnect();
if (props.isSubMenu) return; if (props.isSubMenu) return;
globalThis.removeEventListener('mousedown', outsideClickHideHandler, true); globalThis.removeEventListener('mousedown', outsideClickHideHandler, true);

View File

@ -1,12 +1,6 @@
<template> <template>
<Teleport to="body" v-if="visible"> <Teleport to="body" v-if="visible">
<div <div ref="target" class="m-editor-float-box" :style="{ ...style, zIndex: curZIndex }" @mousedown="nextZIndex">
ref="target"
:class="['m-editor-float-box', themeClass]"
v-bind="$attrs"
:style="{ ...style, zIndex: curZIndex }"
@mousedown="nextZIndex"
>
<div ref="title" class="m-editor-float-box-title"> <div ref="title" class="m-editor-float-box-title">
<slot name="title"> <slot name="title">
<span>{{ title }}</span> <span>{{ title }}</span>
@ -15,7 +9,7 @@
<TMagicButton link size="small" @click="closeHandler"><MIcon :icon="Close"></MIcon></TMagicButton> <TMagicButton link size="small" @click="closeHandler"><MIcon :icon="Close"></MIcon></TMagicButton>
</div> </div>
</div> </div>
<div class="m-editor-float-box-body" :style="{ height: `${bodyHeight}px`, ...bodyStyle }"> <div class="m-editor-float-box-body" :style="{ height: `${bodyHeight}px` }">
<slot name="body"></slot> <slot name="body"></slot>
</div> </div>
</div> </div>
@ -23,13 +17,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, type CSSProperties, nextTick, onBeforeUnmount, provide, ref, useTemplateRef, watch } from 'vue'; import { computed, nextTick, onBeforeUnmount, provide, ref, useTemplateRef, watch } from 'vue';
import { Close } from '@element-plus/icons-vue'; import { Close } from '@element-plus/icons-vue';
import VanillaMoveable from 'moveable'; import VanillaMoveable from 'moveable';
import { TMagicButton, useThemeClass, useZIndex } from '@tmagic/design'; import { TMagicButton, useZIndex } from '@tmagic/design';
import MIcon from '@editor/components/Icon.vue'; import MIcon from '@editor/components/Icon.vue';
import { useServices } from '@editor/hooks/use-services';
interface Position { interface Position {
left: number; left: number;
@ -44,30 +39,17 @@ const props = withDefaults(
defineProps<{ defineProps<{
position?: Position; position?: Position;
title?: string; title?: string;
bodyStyle?: CSSProperties;
/** 浮窗初始样式,会与内部计算样式合并,外部传入优先 */
initialStyle?: CSSProperties;
/** 用于约束浮窗 left 的容器宽度,传入时按宽度收敛 left避免超出右边界默认取视窗宽度 */
frameworkWidth?: number;
beforeClose?: (_done: (_cancel?: boolean) => void) => void; beforeClose?: (_done: (_cancel?: boolean) => void) => void;
}>(), }>(),
{ {
title: '', title: '',
position: () => ({ left: 0, top: 0 }), position: () => ({ left: 0, top: 0 }),
initialStyle: () => ({}),
frameworkWidth: 0,
}, },
); );
const targetEl = useTemplateRef<HTMLDivElement>('target'); const targetEl = useTemplateRef<HTMLDivElement>('target');
const titleEl = useTemplateRef<HTMLDivElement>('title'); const titleEl = useTemplateRef<HTMLDivElement>('title');
/**
* 主题修饰类来自最近的 `<MEditor>` / `<MForm>` 祖先 provide
* 挂在 `Teleport` 出去的浮动面板根节点上让主题级 CSS 变量在 portal 节点上也能命中
*/
const themeClass = useThemeClass();
const zIndex = useZIndex(); const zIndex = useZIndex();
const curZIndex = ref<number>(0); const curZIndex = ref<number>(0);
@ -84,11 +66,12 @@ const bodyHeight = computed(() => {
return 'auto'; return 'auto';
}); });
const { uiService } = useServices();
const frameworkWidth = computed(() => uiService.get('frameworkRect').width || 0);
const style = computed(() => { const style = computed(() => {
let { left } = props.position; let { left } = props.position;
const frameworkWidth = props.frameworkWidth || globalThis.window?.innerWidth || 0; if (width.value) {
if (width.value && frameworkWidth) { left = left + width.value > frameworkWidth.value ? frameworkWidth.value - width.value : left;
left = left + width.value > frameworkWidth ? frameworkWidth - width.value : left;
} }
return { return {
@ -96,60 +79,11 @@ const style = computed(() => {
top: `${props.position.top}px`, top: `${props.position.top}px`,
width: width.value ? `${width.value}px` : 'auto', width: width.value ? `${width.value}px` : 'auto',
height: height.value ? `${height.value}px` : 'auto', height: height.value ? `${height.value}px` : 'auto',
...props.initialStyle,
}; };
}); });
let moveable: VanillaMoveable | null = null; let moveable: VanillaMoveable | null = null;
// / iframe iframe iframe
let dragMask: HTMLDivElement | null = null;
let dragMaskVisible = false;
// moveable dragEnd/resizeEnd
// alt-tab
// pointerup/blur/visibilitychange
const bindDragMaskSafety = () => {
globalThis.window?.addEventListener('pointerup', hideDragMask, true);
globalThis.window?.addEventListener('blur', hideDragMask);
globalThis.document?.addEventListener('visibilitychange', hideDragMask);
};
const unbindDragMaskSafety = () => {
globalThis.window?.removeEventListener('pointerup', hideDragMask, true);
globalThis.window?.removeEventListener('blur', hideDragMask);
globalThis.document?.removeEventListener('visibilitychange', hideDragMask);
};
const showDragMask = () => {
if (dragMaskVisible) {
return;
}
if (!dragMask) {
dragMask = globalThis.document.createElement('div');
dragMask.className = 'm-editor-float-box-drag-mask';
}
globalThis.document.body.appendChild(dragMask);
dragMaskVisible = true;
bindDragMaskSafety();
// root @mousedown="nextZIndex" z-index
// iframe nextTick z-index
const setMaskZIndex = () => {
if (dragMask) {
dragMask.style.zIndex = `${curZIndex.value + 1}`;
}
};
setMaskZIndex();
nextTick(setMaskZIndex);
};
const hideDragMask = () => {
unbindDragMaskSafety();
dragMask?.parentNode?.removeChild(dragMask);
dragMaskVisible = false;
};
const initMoveable = () => { const initMoveable = () => {
moveable = new VanillaMoveable(globalThis.document.body, { moveable = new VanillaMoveable(globalThis.document.body, {
className: 'm-editor-floating-box-moveable', className: 'm-editor-floating-box-moveable',
@ -167,14 +101,6 @@ const initMoveable = () => {
bounds: { left: 0, top: 0, right: 0, bottom: 0, position: 'css' }, bounds: { left: 0, top: 0, right: 0, bottom: 0, position: 'css' },
}); });
// /moveable dragStart/resizeStart mousedown
// mouseup click
// drag/resize
moveable.on('drag', showDragMask);
moveable.on('resize', showDragMask);
moveable.on('dragEnd', hideDragMask);
moveable.on('resizeEnd', hideDragMask);
moveable.on('drag', (e) => { moveable.on('drag', (e) => {
e.target.style.transform = e.transform; e.target.style.transform = e.transform;
}); });
@ -189,7 +115,6 @@ const initMoveable = () => {
}; };
const destroyMoveable = () => { const destroyMoveable = () => {
hideDragMask();
moveable?.destroy(); moveable?.destroy();
moveable = null; moveable = null;
}; };

View File

@ -87,3 +87,70 @@ const scrollBy = (delta: number) => {
} }
}; };
</script> </script>
<style lang="scss">
.m-editor-scroll-bar {
position: absolute;
background-color: transparent;
opacity: 0.3;
transition:
background-color 0.2s linear,
opacity 0.2s linear;
.m-editor-scroll-bar-thumb {
background-color: #aaa;
border-radius: 6px;
position: absolute;
}
&.horizontal {
width: 100%;
height: 15px;
bottom: 0;
.m-editor-scroll-bar-thumb {
height: 6px;
transition:
background-color 0.2s linear,
height 0.2s ease-in-out;
bottom: 2px;
}
}
&.vertical {
height: 100%;
width: 15px;
right: 5px;
.m-editor-scroll-bar-thumb {
width: 6px;
transition:
background-color 0.2s linear,
width 0.2s ease-in-out;
right: 2px;
}
}
&:hover,
&:focus {
background-color: #eee;
opacity: 0.9;
.m-editor-scroll-bar-thumb {
background-color: #999;
}
&.horizontal {
.m-editor-scroll-bar-thumb {
height: 11px;
}
}
&.vertical {
.m-editor-scroll-bar-thumb {
width: 11px;
}
}
}
}
</style>

View File

@ -1,55 +0,0 @@
<template>
<div class="m-editor-view-form-wrapper" :style="wrapperStyle">
<MForm
v-if="config.length"
ref="form"
class="m-editor-view-form"
:config="config"
:init-values="currentValues"
:disabled="disabled"
:label-width="labelWidth"
:extend-state="mergedExtendState"
:size="size"
></MForm>
</div>
</template>
<script lang="ts" setup>
import { type Ref, type ShallowRef } from 'vue';
import { type FormConfig, MForm } from '@tmagic/form';
import { useCompareForm } from '@editor/hooks/use-compare-form';
import type { CompareFormBaseProps } from '@editor/type';
defineOptions({
name: 'MEditorViewForm',
});
const props = withDefaults(
defineProps<
CompareFormBaseProps & {
/** 是否禁用表单(默认只读展示)。 */
disabled?: boolean;
}
>(),
{
category: 'node',
labelWidth: '120px',
disabled: true,
// extendState useCompareForm props.extendState ?? ...
},
);
const { config, currentValues, wrapperStyle, mergedExtendState, loadConfig, formRef } = useCompareForm(props);
defineExpose<{
form: ShallowRef<InstanceType<typeof MForm> | null>;
config: Ref<FormConfig>;
reload: () => Promise<void>;
}>({
form: formRef,
config,
reload: loadConfig,
});
</script>

View File

@ -1,5 +1,3 @@
import type { InjectionKey } from 'vue';
import type { DataSourceSchema, EventOption, Id, MApp, MNode, MPage, MPageFragment } from '@tmagic/core'; import type { DataSourceSchema, EventOption, Id, MApp, MNode, MPage, MPageFragment } from '@tmagic/core';
import type { FormConfig, FormState } from '@tmagic/form'; import type { FormConfig, FormState } from '@tmagic/form';
import StageCore, { import StageCore, {
@ -28,17 +26,7 @@ import type {
TreeNodeData, TreeNodeData,
} from './type'; } from './type';
/**
* provide/inject
* 使 Symbol PropsPanel / FormPanel
*/
export const ENABLE_PROPS_FORM_VALIDATE: InjectionKey<boolean> = Symbol('enablePropsFormValidate');
export interface EditorProps { export interface EditorProps {
/** 是否是大屏模拟器容器大屏容器时左侧属性面板会扩展为两列正常3列颜色表单不扩展两列等 */
isLargeStageContainer?: boolean;
theme?: string;
/** 页面初始值 */ /** 页面初始值 */
modelValue?: MApp; modelValue?: MApp;
/** 左侧面板中的组件类型列表 */ /** 左侧面板中的组件类型列表 */
@ -103,11 +91,6 @@ export interface EditorProps {
disabledFlashTip?: boolean; disabledFlashTip?: boolean;
/** 禁用双击在浮层中单独编辑选中组件 */ /** 禁用双击在浮层中单独编辑选中组件 */
disabledStageOverlay?: boolean; disabledStageOverlay?: boolean;
/**
* /
* editorService false
*/
enablePropsFormValidate?: boolean;
/** 禁用属性配置面板右下角显示源码的按钮 */ /** 禁用属性配置面板右下角显示源码的按钮 */
disabledShowSrc?: boolean; disabledShowSrc?: boolean;
/** 禁用数据源 */ /** 禁用数据源 */

View File

@ -1,10 +1,9 @@
<template> <template>
<div class="m-fields-code-select" :class="config.className"> <div class="m-fields-code-select" :class="config.className">
<TMagicCard :flat="config.flat"> <TMagicCard>
<MContainer <MContainer
:config="codeConfig" :config="codeConfig"
:size="size" :size="size"
class="code-select-content"
:prop="prop" :prop="prop"
:disabled="disabled" :disabled="disabled"
:is-compare="isCompareMode" :is-compare="isCompareMode"
@ -13,20 +12,16 @@
@change="changeHandler" @change="changeHandler"
> >
</MContainer> </MContainer>
<TMagicButton class="create-button fullWidth" :icon="Plus" :size="size" :disabled="disabled" @click="newHandler()"
>添加{{ config.text }}</TMagicButton
>
</TMagicCard> </TMagicCard>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, watch } from 'vue'; import { computed, watch } from 'vue';
import { Plus } from '@element-plus/icons-vue';
import { isEmpty } from 'lodash-es'; import { isEmpty } from 'lodash-es';
import { HookCodeType, HookType } from '@tmagic/core'; import { HookCodeType, HookType } from '@tmagic/core';
import { TMagicButton, TMagicCard } from '@tmagic/design'; import { TMagicCard } from '@tmagic/design';
import type { CodeSelectConfig, ContainerChangeEventData, FieldProps, GroupListConfig } from '@tmagic/form'; import type { CodeSelectConfig, ContainerChangeEventData, FieldProps, GroupListConfig } from '@tmagic/form';
import { MContainer } from '@tmagic/form'; import { MContainer } from '@tmagic/form';
@ -58,23 +53,12 @@ const props = withDefaults(defineProps<FieldProps<CodeSelectConfig>>(), {});
* 仅当存在历史值时才启用对比避免 lastValues 缺失时退化为全部新增的空对比 * 仅当存在历史值时才启用对比避免 lastValues 缺失时退化为全部新增的空对比
*/ */
const isCompareMode = computed(() => Boolean(props.isCompare && props.lastValues)); const isCompareMode = computed(() => Boolean(props.isCompare && props.lastValues));
const newHandler = () => {
const defaultCode = {
codeType: HookCodeType.CODE,
codeId: '',
};
const name = props.config.name || '';
const hookData = props.model[name]?.hookData || [];
emit('change', defaultCode, {
modifyKey: `hookData.${hookData.length}`,
});
};
const codeConfig = computed<GroupListConfig>(() => ({ const codeConfig = computed<GroupListConfig>(() => ({
type: 'group-list', type: 'group-list',
name: 'hookData', name: 'hookData',
enableToggleMode: false, enableToggleMode: false,
expandAll: true, expandAll: true,
addable: () => false,
title: (mForm, { model, index }: any) => { title: (mForm, { model, index }: any) => {
if (model.codeType === HookCodeType.DATA_SOURCE_METHOD) { if (model.codeType === HookCodeType.DATA_SOURCE_METHOD) {
if (Array.isArray(model.codeId)) { if (Array.isArray(model.codeId)) {
@ -97,46 +81,48 @@ const codeConfig = computed<GroupListConfig>(() => ({
return model.codeId || index; return model.codeId || index;
}, },
titlePrefix: props.config.name === undefined ? undefined : String(props.config.name),
items: [ items: [
{ {
text: '代码类型', type: 'row',
items: [
{
type: 'select', type: 'select',
name: 'codeType', name: 'codeType',
labelPosition: 'right', span: 6,
rules: [{ typeMatch: true, trigger: 'change' }],
options: [ options: [
{ value: HookCodeType.CODE, text: '代码块' }, { value: HookCodeType.CODE, text: '代码块' },
{ value: HookCodeType.DATA_SOURCE_METHOD, text: '数据源方法' }, { value: HookCodeType.DATA_SOURCE_METHOD, text: '数据源方法' },
], ],
defaultValue: HookCodeType.CODE, defaultValue: 'code',
onChange: (_mForm, v: HookCodeType, { setModel }) => { onChange: (_mForm, v: HookCodeType, { setModel }) => {
if (v === HookCodeType.DATA_SOURCE_METHOD) { if (v === HookCodeType.DATA_SOURCE_METHOD) {
setModel('codeId', []); setModel('codeId', []);
} else { } else {
setModel('codeId', ''); setModel('codeId', '');
} }
return v; return v;
}, },
}, },
{ {
type: 'code-select-col', type: 'code-select-col',
name: 'codeId', name: 'codeId',
text: '代码块', span: 18,
rules: [{ typeMatch: true, trigger: 'change' }], labelWidth: 0,
display: (_mForm, { model }) => model.codeType !== HookCodeType.DATA_SOURCE_METHOD, display: (_mForm, { model }) => model.codeType !== HookCodeType.DATA_SOURCE_METHOD,
notEditable: () => !codeBlockService.getEditStatus(), notEditable: () => !codeBlockService.getEditStatus(),
}, },
{ {
type: 'data-source-method-select', type: 'data-source-method-select',
name: 'codeId', name: 'codeId',
text: '数据源字段', span: 18,
rules: [{ typeMatch: true, trigger: 'change' }], labelWidth: 0,
display: (_mForm, { model }) => model.codeType === HookCodeType.DATA_SOURCE_METHOD, display: (_mForm, { model }) => model.codeType === HookCodeType.DATA_SOURCE_METHOD,
notEditable: () => !dataSourceService.get('editable'), notEditable: () => !dataSourceService.get('editable'),
}, },
], ],
},
],
})); }));
watch( watch(

View File

@ -107,12 +107,10 @@ const isCompareMode = computed(() => Boolean(props.isCompare && props.lastValues
const notEditable = computed(() => filterFunction(mForm, props.config.notEditable, props)); const notEditable = computed(() => filterFunction(mForm, props.config.notEditable, props));
const codeBlockSidePanel = computed(() => const hasCodeBlockSidePanel = computed(() =>
(uiService.get('sideBarItems') || []).find((item) => item.$key === SideItemKey.CODE_BLOCK), (uiService.get('sideBarItems') || []).find((item) => item.$key === SideItemKey.CODE_BLOCK),
); );
const hasCodeBlockSidePanel = computed(() => codeBlockSidePanel.value);
/** /**
* 根据代码块id获取代码块参数配置 * 根据代码块id获取代码块参数配置
* @param codeId 代码块ID * @param codeId 代码块ID
@ -147,7 +145,6 @@ const selectConfig: SelectConfig = {
type: 'select', type: 'select',
name: props.name, name: props.name,
disabled: props.disabled, disabled: props.disabled,
text: props.config.text,
options: () => { options: () => {
if (codeDsl.value) { if (codeDsl.value) {
return map(codeDsl.value, (value, key) => ({ return map(codeDsl.value, (value, key) => ({
@ -194,10 +191,6 @@ const onParamsChangeHandler = (value: any, eventData: ContainerChangeEventData)
}; };
const editCode = (id: string) => { const editCode = (id: string) => {
const sideBarItem = codeBlockSidePanel.value;
if (sideBarItem) {
uiService.set('sideBarActiveTabName', sideBarItem.text || sideBarItem.$key || SideItemKey.CODE_BLOCK);
}
eventBus?.emit('edit-code', id); eventBus?.emit('edit-code', id);
}; };
</script> </script>

View File

@ -33,7 +33,7 @@ import { getDesignConfig, TMagicSelect } from '@tmagic/design';
import type { CondOpSelectConfig, FieldProps } from '@tmagic/form'; import type { CondOpSelectConfig, FieldProps } from '@tmagic/form';
import { useServices } from '@editor/hooks/use-services'; import { useServices } from '@editor/hooks/use-services';
import { getCondOpOptionsByFieldType, getFieldType } from '@editor/utils'; import { arrayOptions, eqOptions, getFieldType, numberOptions } from '@editor/utils';
defineOptions({ defineOptions({
name: 'MFieldsCondOpSelect', name: 'MFieldsCondOpSelect',
@ -51,9 +51,31 @@ const optionComponent = getDesignConfig('components')?.option;
const options = computed(() => { const options = computed(() => {
const [id, ...fieldNames] = [...(props.config.parentFields || []), ...props.model.field]; const [id, ...fieldNames] = [...(props.config.parentFields || []), ...props.model.field];
const ds = dataSourceService.getDataSourceById(id); const ds = dataSourceService.getDataSourceById(id);
const type = getFieldType(ds, fieldNames); const type = getFieldType(ds, fieldNames);
return getCondOpOptionsByFieldType(type);
if (type === 'array') {
return arrayOptions;
}
if (type === 'boolean' || type === 'null') {
return [
{ text: '是', value: 'is' },
{ text: '不是', value: 'not' },
];
}
if (type === 'number') {
return [...eqOptions, ...numberOptions];
}
if (type === 'string') {
return [...arrayOptions, ...eqOptions];
}
return [...arrayOptions, ...eqOptions, ...numberOptions];
}); });
const fieldChangeHandler = (v: string) => { const fieldChangeHandler = (v: string) => {

View File

@ -211,25 +211,11 @@ const onChangeHandler = (v: string[] = []) => {
emit('change', v); emit('change', v);
}; };
const dataSourceSidePanel = computed(() => const hasDataSourceSidePanel = computed(() =>
uiService.get('sideBarItems').find((item) => item.$key === SideItemKey.DATA_SOURCE), uiService.get('sideBarItems').find((item) => item.$key === SideItemKey.DATA_SOURCE),
); );
const hasDataSourceSidePanel = computed(() => dataSourceSidePanel.value);
const editHandler = (id: string) => { const editHandler = (id: string) => {
const sideBarItem = dataSourceSidePanel.value; eventBus?.emit('edit-data-source', removeDataSourceFieldPrefix(id));
if (sideBarItem) {
uiService.set('sideBarActiveTabName', sideBarItem.text || sideBarItem.$key || SideItemKey.DATA_SOURCE);
}
const dataSourceId = removeDataSourceFieldPrefix(id);
const fieldPath = selectFieldsId.value;
if (fieldPath.length) {
eventBus?.emit('edit-data-source-field', dataSourceId, [...fieldPath]);
} else {
eventBus?.emit('edit-data-source', dataSourceId);
}
}; };
</script> </script>

View File

@ -1,12 +1,7 @@
<template> <template>
<div <div class="m-fields-data-source-field-select">
class="m-fields-data-source-field-select"
:class="{
[`data-source-field-${type}`]: !isSelectValid,
}"
>
<FieldSelect <FieldSelect
v-if="isSelectValid" v-if="!disabledDataSource && (showDataSourceFieldSelect || !config.fieldConfig)"
:model-value="model[name]" :model-value="model[name]"
:disabled="disabled" :disabled="disabled"
:size="size" :size="size"
@ -42,14 +37,15 @@
:size="size" :size="size"
:disabled="disabled" :disabled="disabled"
@click="onToggleDataSourceFieldSelectHandler" @click="onToggleDataSourceFieldSelectHandler"
><MIcon :icon="dataSourceIcon"></MIcon> ><MIcon :icon="Coin"></MIcon
</TMagicButton> ></TMagicButton>
</TMagicTooltip> </TMagicTooltip>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, ref, resolveComponent, watch } from 'vue'; import { computed, inject, ref, resolveComponent, watch } from 'vue';
import { Coin } from '@element-plus/icons-vue';
import { DataSchema } from '@tmagic/core'; import { DataSchema } from '@tmagic/core';
import { TMagicButton, tMagicMessage, TMagicTooltip } from '@tmagic/design'; import { TMagicButton, tMagicMessage, TMagicTooltip } from '@tmagic/design';
@ -65,8 +61,6 @@ import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, removeDataSourceFieldPrefix } f
import MIcon from '@editor/components/Icon.vue'; import MIcon from '@editor/components/Icon.vue';
import { useServices } from '@editor/hooks/use-services'; import { useServices } from '@editor/hooks/use-services';
import dataSourceIcon from '../../icons/DatasourceIcon.vue';
import FieldSelect from './FieldSelect.vue'; import FieldSelect from './FieldSelect.vue';
defineOptions({ defineOptions({
@ -81,9 +75,6 @@ const props = withDefaults(defineProps<FieldProps<DataSourceFieldSelectConfig>>(
const showDataSourceFieldSelect = ref(false); const showDataSourceFieldSelect = ref(false);
const isSelectValid = computed(
() => !disabledDataSource.value && (showDataSourceFieldSelect.value || !props.config.fieldConfig),
);
watch( watch(
() => props.model[props.name], () => props.model[props.name],
(value) => { (value) => {

View File

@ -8,12 +8,10 @@
</div> </div>
<FloatingBox <FloatingBox
:body-style="{ padding: '0 16px' }"
v-model:visible="addDialogVisible" v-model:visible="addDialogVisible"
v-model:width="width" v-model:width="width"
v-model:height="editorHeight" v-model:height="editorHeight"
:title="fieldTitle" :title="fieldTitle"
:framework-width="frameworkWidth"
:position="boxPosition" :position="boxPosition"
> >
<template #body> <template #body>
@ -30,12 +28,10 @@
</FloatingBox> </FloatingBox>
<FloatingBox <FloatingBox
:body-style="{ padding: '0 16px' }"
v-model:visible="addFromJsonDialogVisible" v-model:visible="addFromJsonDialogVisible"
v-model:width="width" v-model:width="width"
v-model:height="editorHeight" v-model:height="editorHeight"
title="快速添加数据定义" title="快速添加数据定义"
:framework-width="frameworkWidth"
:position="boxPosition" :position="boxPosition"
> >
<template #body> <template #body>
@ -51,7 +47,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, type ComputedRef, inject, onMounted, provide, Ref, ref } from 'vue'; import { computed, inject, Ref, ref } from 'vue';
import type { DataSchema } from '@tmagic/core'; import type { DataSchema } from '@tmagic/core';
import { TMagicButton, tMagicMessage, tMagicMessageBox } from '@tmagic/design'; import { TMagicButton, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
@ -105,16 +101,6 @@ const newHandler = () => {
addDialogVisible.value = true; addDialogVisible.value = true;
}; };
const editField = (row: Record<string, any>, index: number) => {
fieldValues.value = {
...row,
index,
};
fieldTitle.value = `编辑${row.title}`;
calcBoxPosition();
addDialogVisible.value = true;
};
const fieldChange = ({ index, ...value }: Record<string, any>, data: ContainerChangeEventData) => { const fieldChange = ({ index, ...value }: Record<string, any>, data: ContainerChangeEventData) => {
addDialogVisible.value = false; addDialogVisible.value = false;
@ -172,7 +158,13 @@ const fieldColumns: ColumnConfig[] = [
{ {
text: '编辑', text: '编辑',
handler: (row: Record<string, any>, index: number) => { handler: (row: Record<string, any>, index: number) => {
editField(row, index); fieldValues.value = {
...row,
index,
};
fieldTitle.value = `编辑${row.title}`;
calcBoxPosition();
addDialogVisible.value = true;
}, },
}, },
{ {
@ -362,30 +354,4 @@ const { height: editorHeight } = useEditorContentHeight();
const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null)); const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null));
const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating); const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating);
const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
/**
* DataSourceConfigPanel 注入打开数据源详情后需要直接打开的字段路径字段名数组
* 当前层消费 path[0]并把剩余路径下发给嵌套字段实现逐层打开
*/
const editingFieldPath = inject<ComputedRef<string[]>>(
'editingDataSourceFieldPath',
computed(() => []),
);
provide(
'editingDataSourceFieldPath',
computed(() => editingFieldPath.value.slice(1)),
);
onMounted(() => {
const path = editingFieldPath.value;
if (!path.length) return;
const fields: Record<string, any>[] = props.model[props.name] || [];
const index = fields.findIndex((field) => field.name === path[0]);
if (index === -1) return;
editField(fields[index], index);
});
</script> </script>

View File

@ -30,10 +30,10 @@
<template #suffix> <template #suffix>
<Icon :icon="Coin" /> <Icon :icon="Coin" />
</template> </template>
<template #default="slotProps"> <template #default="{ item }">
<div style="display: flex; flex-direction: column; line-height: 1.2em"> <div style="display: flex; flex-direction: column; line-height: 1.2em">
<div>{{ slotProps?.item?.text || '' }}</div> <div>{{ item.text }}</div>
<span style="font-size: 10px; color: rgba(0, 0, 0, 0.6)">{{ slotProps?.item?.value || '' }}</span> <span style="font-size: 10px; color: rgba(0, 0, 0, 0.6)">{{ item.value }}</span>
</div> </div>
</template> </template>
</component> </component>
@ -79,11 +79,11 @@ import { Coin } from '@element-plus/icons-vue';
import type { DataSchema, DataSourceSchema } from '@tmagic/core'; import type { DataSchema, DataSourceSchema } from '@tmagic/core';
import { getDesignConfig, TMagicAutocomplete, TMagicInput, TMagicTag } from '@tmagic/design'; import { getDesignConfig, TMagicAutocomplete, TMagicInput, TMagicTag } from '@tmagic/design';
import type { DataSourceInputConfig, FieldProps } from '@tmagic/form'; import type { DataSourceInputConfig, FieldProps } from '@tmagic/form';
import { getKeysArray } from '@tmagic/utils'; import { getKeysArray, isNumber } from '@tmagic/utils';
import Icon from '@editor/components/Icon.vue'; import Icon from '@editor/components/Icon.vue';
import { useServices } from '@editor/hooks/use-services'; import { useServices } from '@editor/hooks/use-services';
import { getDisplayField, resolveFieldByPath } from '@editor/utils/data-source'; import { getDisplayField } from '@editor/utils/data-source';
defineOptions({ defineOptions({
name: 'MFieldsDataSourceInput', name: 'MFieldsDataSourceInput',
@ -251,16 +251,34 @@ const fieldQuerySearch = (
return; return;
} }
const { fields } = resolveFieldByPath(ds.fields, keys, { skipNumberIndices: true }); let fields = ds.fields || [];
//
let key = keys.shift();
while (key) {
if (isNumber(key)) {
key = keys.shift();
continue;
}
for (const field of fields) {
if (field.name === key) {
fields = field.fields || [];
key = keys.shift();
break;
}
}
}
if (curCharIsDot(dotIndex)) { if (curCharIsDot(dotIndex)) {
// . // .
result = fields; result = fields || [];
} else if (dotIndex > -1) { } else if (dotIndex > -1) {
const queryName = queryString.substring(dotIndex + 1).toLowerCase(); const queryName = queryString.substring(dotIndex + 1).toLowerCase();
result = fields.filter( result =
fields.filter(
(field) => field.name?.toLowerCase().includes(queryName) || field.title?.toLowerCase().includes(queryName), (field) => field.name?.toLowerCase().includes(queryName) || field.title?.toLowerCase().includes(queryName),
); ) || [];
} }
cb( cb(

View File

@ -13,7 +13,7 @@
></MCascader> ></MCascader>
<TMagicTooltip <TMagicTooltip
v-if="model[name] && isCustomMethod && dataSourceSidePanel && !isCompare" v-if="model[name] && isCustomMethod && hasDataSourceSidePanel && !isCompare"
:content="notEditable ? '查看' : '编辑'" :content="notEditable ? '查看' : '编辑'"
> >
<TMagicButton class="m-fields-select-action-button" :size="size" @click="editCodeHandler"> <TMagicButton class="m-fields-select-action-button" :size="size" @click="editCodeHandler">
@ -75,7 +75,7 @@ const props = withDefaults(defineProps<FieldProps<DataSourceMethodSelectConfig>>
disabled: false, disabled: false,
}); });
const dataSourceSidePanel = computed(() => const hasDataSourceSidePanel = computed(() =>
(uiService.get('sideBarItems') || []).find((item) => item.$key === SideItemKey.DATA_SOURCE), (uiService.get('sideBarItems') || []).find((item) => item.$key === SideItemKey.DATA_SOURCE),
); );
@ -208,17 +208,12 @@ const onParamsChangeHandler = (value: any, eventData: ContainerChangeEventData)
}; };
const editCodeHandler = () => { const editCodeHandler = () => {
const [id, methodName] = props.model[props.name]; const [id] = props.model[props.name];
const dataSource = dataSourceService.getDataSourceById(id); const dataSource = dataSourceService.getDataSourceById(id);
if (!dataSource) return; if (!dataSource) return;
const sideBarItem = dataSourceSidePanel.value; eventBus?.emit('edit-data-source', id);
if (sideBarItem) {
uiService.set('sideBarActiveTabName', sideBarItem.text || sideBarItem.$key || SideItemKey.DATA_SOURCE);
}
eventBus?.emit('edit-data-source-method', id, methodName);
}; };
</script> </script>

View File

@ -21,7 +21,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, type ComputedRef, inject, nextTick, onMounted, ref, useTemplateRef } from 'vue'; import { computed, inject, nextTick, ref, useTemplateRef } from 'vue';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import type { CodeBlockContent } from '@tmagic/core'; import type { CodeBlockContent } from '@tmagic/core';
@ -52,29 +52,6 @@ const codeBlockEditorRef = useTemplateRef<InstanceType<typeof CodeBlockEditor>>(
let editIndex = -1; let editIndex = -1;
const editMethod = (method: CodeBlockContent, index: number) => {
let codeContent: string = '({ params, dataSource, app }) => {\n // place your code here\n}';
if (method.content) {
if (typeof method.content !== 'string') {
codeContent = method.content.toString();
} else {
codeContent = method.content;
}
}
codeConfig.value = {
...cloneDeep(method),
content: codeContent,
};
editIndex = index;
nextTick(() => {
codeBlockEditorRef.value?.show();
});
};
const methodColumns: ColumnConfig[] = [ const methodColumns: ColumnConfig[] = [
{ {
label: '名称', label: '名称',
@ -100,7 +77,26 @@ const methodColumns: ColumnConfig[] = [
{ {
text: '编辑', text: '编辑',
handler: (method: CodeBlockContent, index: number) => { handler: (method: CodeBlockContent, index: number) => {
editMethod(method, index); let codeContent: string = '({ params, dataSource, app }) => {\n // place your code here\n}';
if (method.content) {
if (typeof method.content !== 'string') {
codeContent = method.content.toString();
} else {
codeContent = method.content;
}
}
codeConfig.value = {
...cloneDeep(method),
content: codeContent,
};
editIndex = index;
nextTick(() => {
codeBlockEditorRef.value?.show();
});
}, },
}, },
{ {
@ -162,21 +158,4 @@ const submitCodeHandler = (value: CodeBlockContent, data: ContainerChangeEventDa
codeBlockEditorRef.value?.hide(); codeBlockEditorRef.value?.hide();
}; };
/** 由 DataSourceConfigPanel 注入:打开数据源详情后需要直接打开的方法名 */
const editingMethodName = inject<ComputedRef<string | undefined>>(
'editingDataSourceMethodName',
computed(() => ''),
);
onMounted(() => {
const methodName = editingMethodName.value;
if (!methodName) return;
const methods: CodeBlockContent[] = props.model[props.name] || [];
const index = methods.findIndex((method) => method.name === methodName);
if (index === -1) return;
editMethod(methods[index], index);
});
</script> </script>

View File

@ -7,12 +7,10 @@
</div> </div>
<FloatingBox <FloatingBox
:body-style="{ padding: '0 16px' }"
v-model:visible="addDialogVisible" v-model:visible="addDialogVisible"
v-model:width="width" v-model:width="width"
v-model:height="editorHeight" v-model:height="editorHeight"
:title="drawerTitle" :title="drawerTitle"
:framework-width="frameworkWidth"
:position="boxPosition" :position="boxPosition"
> >
<template #body> <template #body>
@ -256,5 +254,4 @@ const addDialogVisible = defineModel<boolean>('visible', { default: false });
const { height: editorHeight } = useEditorContentHeight(); const { height: editorHeight } = useEditorContentHeight();
const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null)); const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null));
const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating); const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating);
const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
</script> </script>

View File

@ -68,15 +68,12 @@ const config = computed<GroupListConfig>(() => ({
titlePrefix: props.config.titlePrefix, titlePrefix: props.config.titlePrefix,
expandAll: true, expandAll: true,
enableToggleMode: false, enableToggleMode: false,
flat: props.config.flat,
items: [ items: [
{ {
type: 'table', type: 'table',
name: 'cond', name: 'cond',
operateColWidth: props.config.operateColWidth, operateColWidth: 80,
enableToggleMode: false, enableToggleMode: false,
fixed: props.config.fixed,
flat: props.config.flat,
items: [ items: [
parentFields.value.length parentFields.value.length
? { ? {
@ -101,11 +98,6 @@ const config = computed<GroupListConfig>(() => ({
label: '字段', label: '字段',
checkStrictly: false, checkStrictly: false,
onChange: fieldOnChange, onChange: fieldOnChange,
defaultValue: () => [],
rules: [
{ required: true, trigger: 'blur', message: '请选择字段' },
{ typeMatch: true, trigger: 'change' },
],
} }
: { : {
type: 'data-source-field-select', type: 'data-source-field-select',
@ -115,11 +107,6 @@ const config = computed<GroupListConfig>(() => ({
checkStrictly: false, checkStrictly: false,
dataSourceFieldType: ['string', 'number', 'boolean', 'any'], dataSourceFieldType: ['string', 'number', 'boolean', 'any'],
onChange: fieldOnChange, onChange: fieldOnChange,
defaultValue: () => [],
rules: [
{ required: true, trigger: 'blur', message: '请选择字段' },
{ typeMatch: true, trigger: 'change' },
],
}, },
{ {
type: 'cond-op-select', type: 'cond-op-select',
@ -127,10 +114,6 @@ const config = computed<GroupListConfig>(() => ({
label: '条件', label: '条件',
width: 140, width: 140,
name: 'op', name: 'op',
rules: [
{ required: true, trigger: 'blur', message: '请选择条件' },
{ typeMatch: true, trigger: 'change' },
],
}, },
{ {
label: '值', label: '值',

View File

@ -12,21 +12,16 @@
@change="onChangeHandler" @change="onChangeHandler"
></MTable> ></MTable>
<div v-else class="fullWidth event-select-container"> <div v-else class="fullWidth">
<div class="event-select-header">
<div class="event-select-title">事件配置</div>
<TMagicButton <TMagicButton
v-if="!isCompareMode && displayList.length > 0" v-if="!isCompareMode"
class="create-button" class="create-button"
text
type="primary" type="primary"
:icon="Plus"
:size="size" :size="size"
:disabled="disabled" :disabled="disabled"
@click="addEvent()" @click="addEvent()"
>添加事件</TMagicButton >添加事件</TMagicButton
> >
</div>
<MPanel <MPanel
v-for="entry in displayList" v-for="entry in displayList"
:key="entry.index" :key="entry.index"
@ -37,13 +32,10 @@
:model="entry.cardItem" :model="entry.cardItem"
:last-values="entry.lastCardItem" :last-values="entry.lastCardItem"
:is-compare="isCompareMode" :is-compare="isCompareMode"
:hide-expand="true"
:label-width="config.labelWidth || '100px'" :label-width="config.labelWidth || '100px'"
@change="onChangeHandler" @change="onChangeHandler"
> >
<template #header> <template #header>
<div class="event-item-header">
<div class="event-item-title">事件{{ Number(entry.index) + 1 }}</div>
<MFormContainer <MFormContainer
class="fullWidth" class="fullWidth"
:config="eventNameConfig" :config="eventNameConfig"
@ -56,26 +48,16 @@
@change="eventNameChangeHandler" @change="eventNameChangeHandler"
></MFormContainer> ></MFormContainer>
<TMagicButton <TMagicButton
class="event-item-delete-button"
v-if="!isCompareMode" v-if="!isCompareMode"
style="color: #f56c6c"
link link
:icon="Delete" :icon="Delete"
:disabled="disabled" :disabled="disabled"
:size="size" :size="size"
@click="removeEvent(Number(entry.index))" @click="removeEvent(Number(entry.index))"
></TMagicButton> ></TMagicButton>
</div>
</template> </template>
</MPanel> </MPanel>
<TMagicButton
v-if="!isCompareMode"
class="create-button fullWidth"
:icon="Plus"
:disabled="disabled"
@click="addEvent()"
>添加事件</TMagicButton
>
</div> </div>
</div> </div>
</template> </template>
@ -83,12 +65,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { Delete } from '@element-plus/icons-vue'; import { Delete } from '@element-plus/icons-vue';
import { Plus } from '@element-plus/icons-vue';
import { has } from 'lodash-es'; import { has } from 'lodash-es';
import type { EventOption, MComponent, MContainer } from '@tmagic/core';
import { ActionType } from '@tmagic/core'; import { ActionType } from '@tmagic/core';
import { TMagicButton } from '@tmagic/design'; import { TMagicButton } from '@tmagic/design';
import type { import type {
CascaderOption,
CodeSelectColConfig, CodeSelectColConfig,
ContainerChangeEventData, ContainerChangeEventData,
DataSourceMethodSelectConfig, DataSourceMethodSelectConfig,
@ -101,15 +84,10 @@ import type {
UISelectConfig, UISelectConfig,
} from '@tmagic/form'; } from '@tmagic/form';
import { defineFormItem, MContainer as MFormContainer, MPanel, MTable } from '@tmagic/form'; import { defineFormItem, MContainer as MFormContainer, MPanel, MTable } from '@tmagic/form';
import { DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX, traverseNode } from '@tmagic/utils';
import { useServices } from '@editor/hooks/use-services'; import { useServices } from '@editor/hooks/use-services';
import { import { getCascaderOptionsFromFields } from '@editor/utils';
getCompActionAllowedValues,
getCompActionOptions,
getEventNameAllowedValues,
getEventNameOptions,
normalizeCompActionValue,
} from '@editor/utils';
defineOptions({ defineOptions({
name: 'MFieldsEventSelect', name: 'MFieldsEventSelect',
@ -127,7 +105,7 @@ const { editorService, dataSourceService, eventsService, codeBlockService, props
const eventNameConfig = computed(() => { const eventNameConfig = computed(() => {
const defaultEventNameConfig = { const defaultEventNameConfig = {
name: 'name', name: 'name',
text: '事件类型', text: '事件',
type: (mForm: FormState | undefined, { formValue }: any) => { type: (mForm: FormState | undefined, { formValue }: any) => {
if ( if (
props.config.src !== 'component' || props.config.src !== 'component' ||
@ -137,27 +115,79 @@ const eventNameConfig = computed(() => {
} }
return 'select'; return 'select';
}, },
labelWidth: '70px', labelWidth: '40px',
checkStrictly: () => props.config.src !== 'component', checkStrictly: () => props.config.src !== 'component',
valueSeparator: '.', valueSeparator: '.',
options: (_mForm: FormState, { formValue }: any) => getEventNameOptions(props.config.src, formValue), options: (mForm: FormState, { formValue }: any) => {
rules: [ let events: EventOption[] | CascaderOption[] = [];
if (props.config.src === 'component') {
events = eventsService.getEvent(formValue.type);
if (formValue.type === 'page-fragment-container' && formValue.pageFragmentId) {
const pageFragment = editorService.get('root')?.items?.find((page) => page.id === formValue.pageFragmentId);
if (pageFragment) {
events = [
{ {
validator: ({ value, callback }: any, { formValue }: any) => { label: pageFragment.name || '页面片容器',
const allowedNames = getEventNameAllowedValues(props.config as any, formValue); value: pageFragment.id,
if (allowedNames && allowedNames.size > 0 && value && !allowedNames.has(value)) { children: events,
return callback(`事件名(${value})不存在`); },
];
pageFragment.items.forEach((node) => {
traverseNode<MComponent | MContainer>(node, (node) => {
const nodeEvents = (node.type && eventsService.getEvent(node.type)) || [];
events.push({
label: `${node.name}_${node.id}`,
value: `${node.id}`,
children: nodeEvents,
});
});
});
return events;
} }
callback(); }
return events.map((option) => ({
text: option.label,
value: option.value,
}));
}
if (props.config.src === 'datasource') {
//
events = dataSourceService.getFormEvent(formValue.type);
//
const dataSource = dataSourceService.getDataSourceById(formValue.id);
const fields = dataSource?.fields || [];
if (fields.length > 0) {
return [
...events,
{
label: '数据变化',
value: DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX,
children: getCascaderOptionsFromFields(fields),
}, },
trigger: 'blur', ];
}
return events;
}
}, },
],
}; };
return { ...defaultEventNameConfig, ...props.config.eventNameConfig }; return { ...defaultEventNameConfig, ...props.config.eventNameConfig };
}); });
const actionTypeOptions = computed(() => { //
const actionTypeConfig = computed(() => {
const defaultActionTypeConfig = {
name: 'actionType',
text: '联动类型',
type: 'select',
defaultValue: ActionType.COMP,
options: () => {
const o: { const o: {
text: string; text: string;
label: string; label: string;
@ -189,27 +219,7 @@ const actionTypeOptions = computed(() => {
} }
return o; return o;
});
//
const actionTypeConfig = computed(() => {
const defaultActionTypeConfig = {
name: 'actionType',
text: '联动类型',
type: 'select',
labelPosition: 'left',
defaultValue: ActionType.COMP,
options: actionTypeOptions.value,
rules: [
{
required: true,
message: '联动类型不能为空',
}, },
{
typeMatch: true,
trigger: 'blur',
},
],
}; };
return { ...defaultActionTypeConfig, ...props.config.actionTypeConfig }; return { ...defaultActionTypeConfig, ...props.config.actionTypeConfig };
}); });
@ -220,17 +230,10 @@ const targetCompConfig = computed(() => {
name: 'to', name: 'to',
text: '联动组件', text: '联动组件',
type: 'ui-select', type: 'ui-select',
labelPosition: 'left',
display: (_mForm, { model }) => model.actionType === ActionType.COMP, display: (_mForm, { model }) => model.actionType === ActionType.COMP,
onChange: (_MForm, _v, { setModel }) => { onChange: (_MForm, _v, { setModel }) => {
setModel('method', ''); setModel('method', '');
}, },
rules: [
{
typeMatch: true,
trigger: 'blur',
},
],
}; };
return { ...defaultTargetCompConfig, ...props.config.targetCompConfig }; return { ...defaultTargetCompConfig, ...props.config.targetCompConfig };
}); });
@ -240,7 +243,6 @@ const compActionConfig = computed(() => {
const defaultCompActionConfig: DynamicTypeConfig = { const defaultCompActionConfig: DynamicTypeConfig = {
name: 'method', name: 'method',
text: '动作', text: '动作',
labelPosition: 'left',
type: (mForm: FormState | undefined, { model }: any) => { type: (mForm: FormState | undefined, { model }: any) => {
const to = editorService.getNodeById(model.to); const to = editorService.getNodeById(model.to);
@ -252,20 +254,41 @@ const compActionConfig = computed(() => {
}, },
checkStrictly: () => props.config.src !== 'component', checkStrictly: () => props.config.src !== 'component',
display: (mForm: FormState | undefined, { model }: any) => model.actionType === ActionType.COMP, display: (mForm: FormState | undefined, { model }: any) => model.actionType === ActionType.COMP,
options: (_mForm: FormState, { model }: any) => getCompActionOptions(model.to), options: (mForm: FormState, { model }: any) => {
rules: [ const node = editorService.getNodeById(model.to);
{ if (!node?.type) return [];
trigger: 'blur',
validator: ({ value, callback }: any, { model }: any) => { let methods: EventOption[] | CascaderOption[] = [];
const allowedMethods = getCompActionAllowedValues(props.config as any, model);
const normalized = normalizeCompActionValue(value); methods = eventsService.getMethod(node.type, model.to);
if (allowedMethods && allowedMethods.size > 0 && normalized && !allowedMethods.has(normalized)) {
return callback(`动作名(${normalized})不存在`); if (node.type === 'page-fragment-container' && node.pageFragmentId) {
const pageFragment = editorService.get('root')?.items?.find((page) => page.id === node.pageFragmentId);
if (pageFragment) {
methods = [];
pageFragment.items.forEach((node: MComponent | MContainer) => {
traverseNode<MComponent | MContainer>(node, (node) => {
const nodeMethods = (node.type && eventsService.getMethod(node.type, node.id)) || [];
if (nodeMethods.length) {
methods.push({
label: `${node.name}_${node.id}`,
value: `${node.id}`,
children: nodeMethods,
});
} }
callback(); });
});
return methods;
}
}
return methods.map((method) => ({
text: method.label,
value: method.value,
}));
}, },
},
],
}; };
return { ...defaultCompActionConfig, ...props.config.compActionConfig }; return { ...defaultCompActionConfig, ...props.config.compActionConfig };
}); });
@ -339,7 +362,6 @@ const actionsConfig = computed(
() => () =>
defineFormItem({ defineFormItem({
type: 'panel', type: 'panel',
labelPosition: 'left',
items: [ items: [
{ {
type: 'group-list', type: 'group-list',
@ -347,7 +369,6 @@ const actionsConfig = computed(
expandAll: true, expandAll: true,
enableToggleMode: false, enableToggleMode: false,
titlePrefix: '动作', titlePrefix: '动作',
labelPosition: 'left',
items: [ items: [
actionTypeConfig.value, actionTypeConfig.value,
targetCompConfig.value, targetCompConfig.value,

View File

@ -2,12 +2,11 @@
<TMagicCollapse class="m-fields-style-setter" v-model="collapseValue"> <TMagicCollapse class="m-fields-style-setter" v-model="collapseValue">
<template v-for="(item, index) in list" :key="index"> <template v-for="(item, index) in list" :key="index">
<TMagicCollapseItem :name="`${index}`"> <TMagicCollapseItem :name="`${index}`">
<template #title>{{ item.title }}</template> <template #title><MIcon :icon="Grid"></MIcon>{{ item.title }}</template>
<component <component
v-if="item.component" v-if="item.component"
:is="item.component" :is="item.component"
:values="model[name]" :values="model[name]"
:prop="prop || name"
:last-values="lastValues?.[name]" :last-values="lastValues?.[name]"
:is-compare="isCompare" :is-compare="isCompare"
:size="size" :size="size"
@ -22,18 +21,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { shallowRef } from 'vue'; import { shallowRef } from 'vue';
import { Grid } from '@element-plus/icons-vue';
import { TMagicCollapse, TMagicCollapseItem } from '@tmagic/design'; import { TMagicCollapse, TMagicCollapseItem } from '@tmagic/design';
import type { ContainerChangeEventData, FieldProps } from '@tmagic/form'; import type { ContainerChangeEventData, FieldProps } from '@tmagic/form';
import type { StyleSchema } from '@tmagic/schema'; import type { StyleSchema } from '@tmagic/schema';
import MIcon from '@editor/components/Icon.vue';
import { Background, Border, Font, Layout, Position, Transform } from './pro/'; import { Background, Border, Font, Layout, Position, Transform } from './pro/';
defineOptions({ defineOptions({
name: 'MFieldsStyleSetter', name: 'MFieldsStyleSetter',
}); });
defineProps<FieldProps<StyleSchema>>(); const props = defineProps<FieldProps<StyleSchema>>();
const emit = defineEmits<{ const emit = defineEmits<{
change: [v: any, eventData: ContainerChangeEventData]; change: [v: any, eventData: ContainerChangeEventData];
@ -75,6 +77,13 @@ const collapseValue = shallowRef(
); );
const change = (v: any, eventData: ContainerChangeEventData) => { const change = (v: any, eventData: ContainerChangeEventData) => {
eventData.changeRecords?.forEach((record) => {
if (props.prop) {
record.propPath = `${props.prop}.${record.propPath}`;
} else if (props.name) {
record.propPath = `${props.name}.${record.propPath}`;
}
});
emit('change', v, eventData); emit('change', v, eventData);
}; };

View File

@ -4,31 +4,27 @@
<div class="border-icon-container-row"> <div class="border-icon-container-row">
<div <div
class="border-icon border-icon-top" class="border-icon border-icon-top"
:class="{ active: direction === 'Top', configured: isConfigured('Top') }" :class="{ active: direction === 'Top' }"
@click="selectDirection('Top')" @click="selectDirection('Top')"
></div> ></div>
</div> </div>
<div class="border-icon-container-row"> <div class="border-icon-container-row">
<div <div
class="border-icon border-icon-left" class="border-icon border-icon-left"
:class="{ active: direction === 'Left', configured: isConfigured('Left') }" :class="{ active: direction === 'Left' }"
@click="selectDirection('Left')" @click="selectDirection('Left')"
></div> ></div>
<div <div class="border-icon" :class="{ active: direction === '' }" @click="selectDirection()"></div>
class="border-icon center"
:class="{ active: direction === '', configured: isConfigured('') }"
@click="selectDirection()"
></div>
<div <div
class="border-icon border-icon-right" class="border-icon border-icon-right"
:class="{ active: direction === 'Right', configured: isConfigured('Right') }" :class="{ active: direction === 'Right' }"
@click="selectDirection('Right')" @click="selectDirection('Right')"
></div> ></div>
</div> </div>
<div class="border-icon-container-row"> <div class="border-icon-container-row">
<div <div
class="border-icon border-icon-bottom" class="border-icon border-icon-bottom"
:class="{ active: direction === 'Bottom', configured: isConfigured('Bottom') }" :class="{ active: direction === 'Bottom' }"
@click="selectDirection('Bottom')" @click="selectDirection('Bottom')"
></div> ></div>
</div> </div>
@ -102,7 +98,7 @@ const emit = defineEmits<{
addDiffCount: []; addDiffCount: [];
}>(); }>();
const props = withDefaults( withDefaults(
defineProps<{ defineProps<{
model: FormValue; model: FormValue;
lastValues?: FormValue; lastValues?: FormValue;
@ -113,11 +109,6 @@ const props = withDefaults(
{}, {},
); );
const hasValue = (value: unknown) => value !== undefined && value !== null && value !== '';
const isConfigured = (d: string) =>
['Width', 'Color', 'Style'].some((key) => hasValue(props.model?.[`border${d}${key}`]));
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => { const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
eventData.changeRecords?.forEach((record) => { eventData.changeRecords?.forEach((record) => {
emit('change', record.value, { emit('change', record.value, {

View File

@ -1,19 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path
d="M12 4C12.5523 4 13 4.44772 13 5V11C13 11.5523 12.5523 12 12 12H4C3.44772 12 3 11.5523 3 11V5C3 4.44772 3.44772 4 4 4H12ZM4.5 5C4.22386 5 4 5.22386 4 5.5V10.5C4 10.7761 4.22386 11 4.5 11H11.5C11.7761 11 12 10.7761 12 10.5V5.5C12 5.22386 11.7761 5 11.5 5H4.5Z"
/>
<rect
x="13.75"
y="7.75"
width="0.5"
height="11.5"
rx="0.25"
transform="rotate(90 13.75 7.75)"
stroke="black"
stroke-opacity="0.3"
style="stroke: black; stroke-opacity: 0.3"
stroke-width="0.5"
/>
</svg>
</template>

View File

@ -1,19 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path
d="M12 3C12.5523 3 13 3.44772 13 4V10C13 10.5523 12.5523 11 12 11H4C3.44772 11 3 10.5523 3 10V4C3 3.44772 3.44772 3 4 3H12ZM4.5 4C4.22386 4 4 4.22386 4 4.5V9.5C4 9.77614 4.22386 10 4.5 10H11.5C11.7761 10 12 9.77614 12 9.5V4.5C12 4.22386 11.7761 4 11.5 4H4.5Z"
/>
<rect
x="13.75"
y="12.25"
width="0.5"
height="11.5"
rx="0.25"
transform="rotate(90 13.75 12.25)"
stroke="black"
stroke-opacity="0.3"
style="stroke: black; stroke-opacity: 0.3"
stroke-width="0.5"
/>
</svg>
</template>

View File

@ -1,19 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path
d="M12 5C12.5523 5 13 5.44772 13 6V12C13 12.5523 12.5523 13 12 13H4C3.44772 13 3 12.5523 3 12V6C3 5.44772 3.44772 5 4 5H12ZM4.5 6C4.22386 6 4 6.22386 4 6.5V11.5C4 11.7761 4.22386 12 4.5 12H11.5C11.7761 12 12 11.7761 12 11.5V6.5C12 6.22386 11.7761 6 11.5 6H4.5Z"
/>
<rect
x="13.75"
y="3.25"
width="0.5"
height="11.5"
rx="0.25"
transform="rotate(90 13.75 3.25)"
stroke="white"
stroke-opacity="0.3"
style="stroke: white; stroke-opacity: 0.3"
stroke-width="0.5"
/>
</svg>
</template>

View File

@ -1,34 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path
d="M12 8C12.5523 8 13 8.44772 13 9V12C13 12.5523 12.5523 13 12 13H4C3.44772 13 3 12.5523 3 12V9C3 8.44772 3.44772 8 4 8H12ZM4.5 9C4.22386 9 4 9.22386 4 9.5V11.5C4 11.7761 4.22386 12 4.5 12H11.5C11.7761 12 12 11.7761 12 11.5V9.5C12 9.22386 11.7761 9 11.5 9H4.5Z"
/>
<path
d="M12 2C12.5523 2 13 2.44772 13 3V6C13 6.55228 12.5523 7 12 7H4C3.44772 7 3 6.55228 3 6V3C3 2.44772 3.44772 2 4 2H12ZM4.5 3C4.22386 3 4 3.22386 4 3.5V5.5C4 5.77614 4.22386 6 4.5 6H11.5C11.7761 6 12 5.77614 12 5.5V3.5C12 3.22386 11.7761 3 11.5 3H4.5Z"
/>
<rect
x="13.75"
y="10.25"
width="0.5"
height="11.5"
rx="0.25"
transform="rotate(90 13.75 10.25)"
stroke="black"
stroke-opacity="0.3"
style="stroke: black; stroke-opacity: 0.3"
stroke-width="0.5"
/>
<rect
x="13.75"
y="4.25"
width="0.5"
height="11.5"
rx="0.25"
transform="rotate(90 13.75 4.25)"
stroke="black"
stroke-opacity="0.3"
style="stroke: black; stroke-opacity: 0.3"
stroke-width="0.5"
/>
</svg>
</template>

View File

@ -1,34 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path
d="M12 9C12.5523 9 13 9.44772 13 10V12C13 12.5523 12.5523 13 12 13H4C3.44772 13 3 12.5523 3 12V10C3 9.44772 3.44772 9 4 9H12ZM4.5 10C4.22386 10 4 10.2239 4 10.5V11.5C4 11.7761 4.22386 12 4.5 12H11.5C11.7761 12 12 11.7761 12 11.5V10.5C12 10.2239 11.7761 10 11.5 10H4.5Z"
/>
<path
d="M12 3C12.5523 3 13 3.44772 13 4V6C13 6.55228 12.5523 7 12 7H4C3.44772 7 3 6.55228 3 6V4C3 3.44772 3.44772 3 4 3H12ZM4.5 4C4.22386 4 4 4.22386 4 4.5V5.5C4 5.77614 4.22386 6 4.5 6H11.5C11.7761 6 12 5.77614 12 5.5V4.5C12 4.22386 11.7761 4 11.5 4H4.5Z"
/>
<rect
x="13.75"
y="14.25"
width="0.5"
height="11.5"
rx="0.25"
transform="rotate(90 13.75 14.25)"
stroke="black"
stroke-opacity="0.3"
style="stroke: black; stroke-opacity: 0.3"
stroke-width="0.5"
/>
<rect
x="13.75"
y="1.25"
width="0.5"
height="11.5"
rx="0.25"
transform="rotate(90 13.75 1.25)"
stroke="black"
stroke-opacity="0.3"
style="stroke: black; stroke-opacity: 0.3"
stroke-width="0.5"
/>
</svg>
</template>

View File

@ -1,5 +0,0 @@
export { default as AlignItemsCenter } from './Center.vue';
export { default as AlignItemsFlexEnd } from './FlexEnd.vue';
export { default as AlignItemsFlexStart } from './FlexStart.vue';
export { default as AlignItemsSpaceAround } from './SpaceAround.vue';
export { default as AlignItemsSpaceBetween } from './SpaceBetween.vue';

View File

@ -1,24 +1,8 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path <path
d="M0 0.625C0 0.279822 0.248731 0 0.555556 0H9.44444C9.75127 0 10 0.279822 10 0.625C10 0.970178 9.75127 1.25 9.44444 1.25H0.555556C0.248731 1.25 0 0.970178 0 0.625Z" d="M912.526651 867.741144 555.540144 510.712681l356.986507-357.000833c11.171434-11.18576 11.171434-29.257348 0-40.443108-11.20111-11.18576-29.272697-11.18576-40.444131 0L515.096013 470.267527 158.096203 113.267716c-11.187807-11.159154-29.258371-11.159154-40.444131 0-11.186783 11.186783-11.186783 29.286 0 40.47176L474.623229 510.712681 117.623419 867.741144c-11.159154 11.172457-11.159154 29.216415 0 40.443108 11.18576 11.17348 29.284977 11.17348 40.47176 0l357.000833-357.027439 356.985484 357.027439c11.171434 11.17348 29.243021 11.17348 40.444131 0C923.698085 896.957559 923.725714 878.913601 912.526651 867.741144z"
fill-opacity="0.3" fill="#5D5D5D"
/> ></path>
<path
d="M0 9.375C0 9.02982 0.248731 8.75 0.555556 8.75H9.44444C9.75127 8.75 10 9.02982 10 9.375C10 9.72018 9.75127 10 9.44444 10H0.555556C0.248731 10 0 9.72018 0 9.375Z"
fill-opacity="0.3"
/>
<path
d="M0.555556 4.375C0.248731 4.375 0 4.65482 0 5C0 5.34518 0.248731 5.625 0.555556 5.625H2.87307L3.42868 4.99994L2.87318 4.375H0.555556Z"
fill-opacity="0.3"
/>
<path
d="M9.44444 5.625H7.12699L6.57138 4.99994L7.12688 4.375H9.44444C9.75127 4.375 10 4.65482 10 5C10 5.34518 9.75127 5.625 9.44444 5.625Z"
fill-opacity="0.3"
/>
<path
transform="translate(0 2.6072)"
d="M3.30542 0.183058C3.52237 -0.0610194 3.87413 -0.0610194 4.09109 0.183058C4.30805 0.427136 4.30805 0.822864 4.09109 1.06694L2.91256 2.39279L4.0911 3.71865C4.30806 3.96273 4.30806 4.35846 4.0911 4.60253C3.87414 4.84661 3.52239 4.84661 3.30543 4.60253L2.12688 3.27667L0.948393 4.60248C0.731435 4.84655 0.379676 4.84655 0.162719 4.60248C-0.0542395 4.3584 -0.0542395 3.96267 0.162719 3.71859L1.34121 2.39279L0.162731 1.067C-0.0542269 0.822922 -0.0542268 0.427194 0.162731 0.183116C0.379689 -0.0609612 0.731447 -0.0609612 0.948405 0.183116L2.12688 1.50891L3.30542 0.183058Z"
/>
</svg> </svg>
</template> </template>

View File

@ -1,13 +1,31 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<rect x="13" y="3" width="2" height="2" rx="0.5" transform="rotate(90 13 3)" fill-opacity="0.3" /> <path
<rect x="13" y="7" width="2" height="2" rx="0.5" transform="rotate(90 13 7)" fill-opacity="0.3" /> d="M884.736 102.4l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456C696.32 311.296 712.704 327.68 733.184 327.68l147.456 0c20.48 0 36.864-16.384 36.864-36.864L917.504 139.264C921.6 118.784 905.216 102.4 884.736 102.4zM884.736 290.816l-147.456 0L737.28 139.264l147.456 0L884.736 290.816z"
<rect x="13" y="11" width="2" height="2" rx="0.5" transform="rotate(90 13 11)" fill-opacity="0.3" /> ></path>
<rect x="9" y="3" width="2" height="2" rx="0.5" transform="rotate(90 9 3)" /> <path
<rect x="9" y="7" width="2" height="2" rx="0.5" transform="rotate(90 9 7)" /> d="M884.736 696.32l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C921.6 712.704 905.216 696.32 884.736 696.32zM884.736 884.736l-147.456 0 0-147.456 147.456 0L884.736 884.736z"
<rect x="9" y="11" width="2" height="2" rx="0.5" transform="rotate(90 9 11)" fill-opacity="0.3" /> ></path>
<rect x="5" y="3" width="2" height="2" rx="0.5" transform="rotate(90 5 3)" /> <path
<rect x="5" y="7" width="2" height="2" rx="0.5" transform="rotate(90 5 7)" /> d="M884.736 401.408l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C921.6 417.792 905.216 401.408 884.736 401.408zM884.736 585.728l-147.456 0 0-147.456 147.456 0L884.736 585.728z"
<rect x="5" y="11" width="2" height="2" rx="0.5" transform="rotate(90 5 11)" fill-opacity="0.3" /> ></path>
<path
d="M585.728 401.408l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C622.592 417.792 606.208 401.408 585.728 401.408zM585.728 585.728l-147.456 0 0-147.456 147.456 0L585.728 585.728z"
></path>
<path
d="M585.728 102.4l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864L622.592 139.264C622.592 118.784 606.208 102.4 585.728 102.4zM585.728 290.816l-147.456 0L438.272 139.264l147.456 0L585.728 290.816z"
></path>
<path
d="M585.728 696.32l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C622.592 712.704 606.208 696.32 585.728 696.32zM585.728 884.736l-147.456 0 0-147.456 147.456 0L585.728 884.736z"
></path>
<path
d="M290.816 696.32 139.264 696.32c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C327.68 712.704 311.296 696.32 290.816 696.32zM290.816 884.736 139.264 884.736l0-147.456 147.456 0L286.72 884.736z"
></path>
<path
d="M290.816 401.408 139.264 401.408c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C327.68 417.792 311.296 401.408 290.816 401.408zM290.816 585.728 139.264 585.728l0-147.456 147.456 0L286.72 585.728z"
></path>
<path
d="M290.816 102.4 139.264 102.4c-20.48 0-36.864 16.384-36.864 36.864l0 147.456C102.4 311.296 118.784 327.68 139.264 327.68l147.456 0C311.296 327.68 327.68 311.296 327.68 290.816L327.68 139.264C327.68 118.784 311.296 102.4 290.816 102.4zM290.816 290.816 139.264 290.816 139.264 139.264l147.456 0L286.72 290.816z"
></path>
</svg> </svg>
</template> </template>

View File

@ -1,7 +1,8 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<rect x="3" y="3" width="2" height="10" rx="1" /> <path
<rect x="7" y="3" width="2" height="10" rx="1" /> d="M796.444444 170.666667h-227.555555v682.666666h227.555555V170.666667z m-56.888888 625.777777h-113.777778V227.555556h113.777778v568.888888zM455.111111 170.666667H227.555556v682.666666h227.555555V170.666667zM398.222222 796.444444H284.444444V227.555556h113.777778v568.888888zM910.222222 56.888889h56.888889v910.222222h-56.888889zM56.888889 56.888889h56.888889v910.222222H56.888889z"
<rect x="11" y="3" width="2" height="10" rx="1" fill-opacity="0.3" /> fill="#333333"
></path>
</svg> </svg>
</template> </template>

View File

@ -1,7 +1,8 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<rect x="13" y="3" width="2" height="10" rx="1" transform="rotate(90 13 3)" /> <path
<rect x="13" y="7" width="2" height="10" rx="1" transform="rotate(90 13 7)" /> d="M170.666667 227.555556v227.555555h682.666666V227.555556H170.666667z m625.777777 56.888888v113.777778H227.555556V284.444444h568.888888zM170.666667 568.888889v227.555555h682.666666v-227.555555H170.666667z m625.777777 56.888889v113.777778H227.555556v-113.777778h568.888888zM56.888889 56.888889h910.222222v56.888889H56.888889zM56.888889 910.222222h910.222222v56.888889H56.888889z"
<rect x="13" y="11" width="2" height="10" rx="1" transform="rotate(90 13 11)" fill-opacity="0.3" /> fill="#333333"
></path>
</svg> </svg>
</template> </template>

View File

@ -1,8 +1,7 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path <path
d="M12 3C12.5523 3 13 3.44772 13 4V10C13 10.5523 12.5523 11 12 11H4C3.44772 11 3 10.5523 3 10V4C3 3.44772 3.44772 3 4 3H12ZM4.5 4C4.22386 4 4 4.22386 4 4.5V9.5C4 9.77614 4.22386 10 4.5 10H11.5C11.7761 10 12 9.77614 12 9.5V4.5C12 4.22386 11.7761 4 11.5 4H4.5Z" d="M896 320H128V0h768v320z m0 320H128v-256h768v256z m-128 192l-256 192-256-192 192-0.032V704h128v128h192z"
/> ></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 12L8 15L5 12L11 12Z" fill-opacity="0.3" />
</svg> </svg>
</template> </template>

View File

@ -1,8 +1,7 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path <path
d="M12 5C12.5523 5 13 5.44772 13 6V12C13 12.5523 12.5523 13 12 13H4C3.44772 13 3 12.5523 3 12V6C3 5.44772 3.44772 5 4 5H12ZM4.5 6C4.22386 6 4 6.22386 4 6.5V11.5C4 11.7761 4.22386 12 4.5 12H11.5C11.7761 12 12 11.7761 12 11.5V6.5C12 6.22386 11.7761 6 11.5 6H4.5Z" d="M896 704H128v320h768V704z m0-320H128v256h768v-256z m-128-192l-256-192-256 192 192 0.032V320h128V192h192z"
/> ></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 4L8 1L11 4L5 4Z" fill-opacity="0.3" />
</svg> </svg>
</template> </template>

View File

@ -1,8 +1,7 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path <path
d="M10 4C10.5523 4 11 4.44772 11 5V11C11 11.5523 10.5523 12 10 12H2C1.44772 12 1 11.5523 1 11V5C1 4.44772 1.44772 4 2 4H10ZM2.5 5C2.22386 5 2 5.22386 2 5.5V10.5C2 10.7761 2.22386 11 2.5 11H9.5C9.77614 11 10 10.7761 10 10.5V5.5C10 5.22386 9.77614 5 9.5 5H2.5Z" d="M320 128v768H0V128h320z m320 0v768h-256V128h256z m192 128l192 256-192 256-0.032-192H704v-128h128V256z"
/> ></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 5L15 8L12 11L12 5Z" fill-opacity="0.3" />
</svg> </svg>
</template> </template>

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