mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-25 07:28:23 +00:00
Compare commits
76 Commits
v1.8.0-bet
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b564c9e4aa | ||
|
|
b5abf31066 | ||
|
|
bf8df74864 | ||
|
|
a37e0487c6 | ||
|
|
329cd947e1 | ||
|
|
0e1986c819 | ||
|
|
d0a22315c0 | ||
|
|
fe7815cf3d | ||
|
|
bf9078a458 | ||
|
|
3b9d5da0bc | ||
|
|
2e031cc134 | ||
|
|
1f389ff9fd | ||
|
|
fe9e754b50 | ||
|
|
745f449396 | ||
|
|
48abab7051 | ||
|
|
11bf037054 | ||
|
|
97e0cac44a | ||
|
|
cde0805292 | ||
|
|
6919a0a0b1 | ||
|
|
d9d2f8f04c | ||
|
|
3fbce585a4 | ||
|
|
09eecd5bed | ||
|
|
ee5247ca94 | ||
|
|
8699dc12af | ||
|
|
b13cd3425b | ||
|
|
89689bb9fd | ||
|
|
0feada8d1f | ||
|
|
d27710c464 | ||
|
|
3e7980c0f5 | ||
|
|
78b34a452b | ||
|
|
6f3f321736 | ||
|
|
6cdf54a4a6 | ||
|
|
08ac9038bf | ||
|
|
58b151af64 | ||
|
|
e5e0da3785 | ||
|
|
ef66e8a598 | ||
|
|
932974f0fa | ||
|
|
edb44da255 | ||
|
|
7459224009 | ||
|
|
d07e48bb10 | ||
|
|
4a15da2108 | ||
|
|
ce43fface8 | ||
|
|
0e4669261f | ||
|
|
1c67b5e77b | ||
|
|
bbe73aae64 | ||
|
|
734e6a8ee6 | ||
|
|
f7d737a0c0 | ||
|
|
42d45bed18 | ||
|
|
de7a4f73ad | ||
|
|
605f07c8fe | ||
|
|
90492ad3ea | ||
|
|
b183e71f64 | ||
|
|
78a1259665 | ||
|
|
4a893d35df | ||
|
|
3c21f45e88 | ||
|
|
aa71fe9ddd | ||
|
|
9e8272b521 | ||
|
|
9aa251ce57 | ||
|
|
3b9fb714e5 | ||
|
|
284be0d276 | ||
|
|
c57ef89715 | ||
|
|
1ef3ea9eb9 | ||
|
|
83f4e52845 | ||
|
|
4330738b9f | ||
|
|
e59c3d2dcd | ||
|
|
55135fc063 | ||
|
|
af197363cf | ||
|
|
7bbb1f24c0 | ||
|
|
f7afed66aa | ||
|
|
fd359e493f | ||
|
|
5e661d0958 | ||
|
|
5ea992f5f7 | ||
|
|
4c81f22485 | ||
|
|
40acb5b34c | ||
|
|
ce65b18dbb | ||
|
|
1c936ff439 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -20,6 +20,7 @@ pnpm-debug.log*
|
|||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
.codebuddy
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
|
|||||||
10
AGENTS.md
10
AGENTS.md
@ -14,12 +14,13 @@ 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 文档(100 *.md)
|
- `docs/` — VitePress 文档
|
||||||
- `scripts/` — 构建和发布脚本
|
- `scripts/` — 构建和发布脚本
|
||||||
- `eslint-config/` — 共享 ESLint 配置
|
- `eslint-config/` — 共享 ESLint 配置
|
||||||
|
|
||||||
@ -27,10 +28,13 @@ 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% 测试覆盖率的新增/修改代码
|
||||||
|
|
||||||
## 常用命令
|
## 常用命令
|
||||||
|
|
||||||
@ -50,7 +54,7 @@ TMagic Editor 是魔方平台的可视化编辑器核心库,提供拖拽式组
|
|||||||
## 深入阅读
|
## 深入阅读
|
||||||
|
|
||||||
| 文档 | 说明 |
|
| 文档 | 说明 |
|
||||||
|------|------|
|
| --------------- | ---------------- |
|
||||||
| docs/ | VitePress 文档站 |
|
| docs/ | VitePress 文档站 |
|
||||||
| CONTRIBUTING.md | 贡献指南 |
|
| CONTRIBUTING.md | 贡献指南 |
|
||||||
| CHANGELOG.md | 变更日志 |
|
| CHANGELOG.md | 变更日志 |
|
||||||
|
|||||||
135
CHANGELOG.md
135
CHANGELOG.md
@ -1,3 +1,138 @@
|
|||||||
|
# [1.8.0-beta.16](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.15...v1.8.0-beta.16) (2026-07-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 参考建议可选值最多展示个数由 5 调整为 20 ([0e1986c](https://github.com/Tencent/tmagic-editor/commit/0e1986c81998e50461c294c1a27a8139e8ae9a13))
|
||||||
|
# [1.8.0-beta.15](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.14...v1.8.0-beta.15) (2026-07-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **core:** 为背景图片 URL 添加引号 ([97e0cac](https://github.com/Tencent/tmagic-editor/commit/97e0cac44a2d6ceafaf95fbc4ae716bfe5d47f70))
|
||||||
|
* **editor:** 使用属性面板中的源码删除样式无效 ([cde0805](https://github.com/Tencent/tmagic-editor/commit/cde08052924ddef1c51faeb68ce15d3195827adb))
|
||||||
|
* **editor:** 右键菜单动态变化后修复菜单位置防止截断 ([6919a0a](https://github.com/Tencent/tmagic-editor/commit/6919a0a0b19efff92821fc8d52b61b2f7382a817))
|
||||||
|
* 跳过无字段名称配置的类型校验 ([11bf037](https://github.com/Tencent/tmagic-editor/commit/11bf037054774937132743a40006364e49ee8fd5))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **design:** 优化 FormItem 和 colorPicker 样式 ([2e031cc](https://github.com/Tencent/tmagic-editor/commit/2e031cc1346fb6ea739f0d45a89213b6577dc7ee))
|
||||||
|
* **editor:** 数据源字段选择器校验提示细化并支持 fieldConfig 类型校验 ([fe9e754](https://github.com/Tencent/tmagic-editor/commit/fe9e754b50f14e6034170b2284553ec1c4536150))
|
||||||
|
* **form,design:** 一行多个表单默认右靠齐label ([48abab7](https://github.com/Tencent/tmagic-editor/commit/48abab7051b93b7ed336172c21ab3b908733f2e7))
|
||||||
|
* **form,editor:** submitForm/validateForm 静默挂载时注入静默标记,vs-code 字段跳过 monaco 渲染 ([3b9d5da](https://github.com/Tencent/tmagic-editor/commit/3b9d5da0bc9f323e944c0e2adb4fc88642ce1915))
|
||||||
|
* **form:** text 类型校验允许数字值,兼容数据源字段配置 ([1f389ff](https://github.com/Tencent/tmagic-editor/commit/1f389ff9fd77fb93ad749f3fdbbc8aa0fe8725ac))
|
||||||
|
* 新增 page-structure-change 事件,支持页面增删统一通知 ([745f449](https://github.com/Tencent/tmagic-editor/commit/745f44939605528e8e2ccd0639478db98293a441))
|
||||||
|
# [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)
|
# [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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -111,6 +111,10 @@ 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',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -488,6 +492,15 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: '表单校验',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
text: '表单校验',
|
||||||
|
link: '/form-config/rules.md',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: '表单对比',
|
text: '表单对比',
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@ -162,7 +162,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #409eff;
|
color: var(--el-color-primary,#409eff);;
|
||||||
background-color: #f9fafc;
|
background-color: #f9fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -87,3 +87,35 @@
|
|||||||
- **详情:** 历史记录改变,[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}
|
||||||
|
:::
|
||||||
|
|||||||
@ -48,7 +48,7 @@ DSL 操作方法(`add` / `remove` / `update` 等)默认返回操作结果(
|
|||||||
## get
|
## get
|
||||||
|
|
||||||
- **参数:**
|
- **参数:**
|
||||||
- `{'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength' | 'stage' | 'stageLoading' | 'disabledMultiSelect' | 'alwaysMultiSelect'} name`
|
- `{'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength' | 'stage' | 'stageLoading' | 'disabledMultiSelect' | 'alwaysMultiSelect'} name`
|
||||||
|
|
||||||
- **返回:**
|
- **返回:**
|
||||||
- `{any} value`
|
- `{any} value`
|
||||||
@ -71,6 +71,8 @@ DSL 操作方法(`add` / `remove` / `update` 等)默认返回操作结果(
|
|||||||
|
|
||||||
'modifiedNodeIds': 当前页面所有改动过的节点id
|
'modifiedNodeIds': 当前页面所有改动过的节点id
|
||||||
|
|
||||||
|
'invalidNodeIds': 校验失败的节点错误信息(`Map<Id, NodeInvalidInfo>`),供组件树标红提示与保存拦截读取
|
||||||
|
|
||||||
'pageLength': 所以页面个数
|
'pageLength': 所以页面个数
|
||||||
|
|
||||||
'pageFragmentLength': 页面片个数
|
'pageFragmentLength': 页面片个数
|
||||||
@ -93,7 +95,7 @@ const node = editorService.get("node");
|
|||||||
|
|
||||||
## set
|
## set
|
||||||
|
|
||||||
- `{'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength' | 'stage' | 'stageLoading' | 'disabledMultiSelect' | 'alwaysMultiSelect'} name`
|
- `{'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength' | 'stage' | 'stageLoading' | 'disabledMultiSelect' | 'alwaysMultiSelect'} name`
|
||||||
- `{any} value`
|
- `{any} value`
|
||||||
|
|
||||||
- **详情:**
|
- **详情:**
|
||||||
@ -510,6 +512,10 @@ 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}
|
||||||
@ -927,6 +933,89 @@ 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>`} 当前存在校验错误的节点错误 Map(key 为节点 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
|
||||||
|
|
||||||
- **详情:**
|
- **详情:**
|
||||||
|
|||||||
@ -1282,6 +1282,45 @@ 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
|
||||||
|
|
||||||
- **详情:**
|
- **详情:**
|
||||||
@ -1365,13 +1404,34 @@ 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)[];
|
||||||
|
```
|
||||||
|
|
||||||
- **示例:**
|
- **示例:**
|
||||||
|
|
||||||
@ -1381,20 +1441,24 @@ const guidesOptions = {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const customContentMenu = (menus, { node }) => {
|
const customContentMenu = (menus, type, getTarget) => {
|
||||||
// 为特定类型的组件添加自定义菜单
|
// 为数据源右键菜单追加自定义菜单
|
||||||
if (node?.type === 'container') {
|
if (type === 'data-source') {
|
||||||
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>
|
||||||
```
|
```
|
||||||
|
|||||||
@ -150,6 +150,14 @@
|
|||||||
|
|
||||||
- **类型:** `boolean`
|
- **类型:** `boolean`
|
||||||
|
|
||||||
|
## useFieldTextInError
|
||||||
|
|
||||||
|
- **详情:** 透传给内部 `Form`,控制表单校验失败时错误提示前缀是否使用字段的 `text` 文案。`false` 时直接使用字段 `name`
|
||||||
|
|
||||||
|
- **默认值:** `true`
|
||||||
|
|
||||||
|
- **类型:** `boolean`
|
||||||
|
|
||||||
## closeOnClickModal
|
## closeOnClickModal
|
||||||
|
|
||||||
- **详情:** 是否可以通过点击 modal 关闭 Dialog
|
- **详情:** 是否可以通过点击 modal 关闭 Dialog
|
||||||
|
|||||||
@ -40,6 +40,8 @@
|
|||||||
|
|
||||||
- **详情:** 通过 `name` 从表单 `config` 中查找对应表单项的 `text`
|
- **详情:** 通过 `name` 从表单 `config` 中查找对应表单项的 `text`
|
||||||
|
|
||||||
|
- **相关:** 表单校验失败时是否使用该方法生成错误提示前缀,由 [`useFieldTextInError`](./form-props.md#usefieldtextinerror) prop 控制(默认 `true`)
|
||||||
|
|
||||||
## values
|
## values
|
||||||
|
|
||||||
- **类型:** `Ref<FormValue>`
|
- **类型:** `Ref<FormValue>`
|
||||||
|
|||||||
@ -206,6 +206,19 @@
|
|||||||
|
|
||||||
- **类型:** `boolean`
|
- **类型:** `boolean`
|
||||||
|
|
||||||
|
## useFieldTextInError
|
||||||
|
|
||||||
|
- **详情:**
|
||||||
|
|
||||||
|
表单校验失败时,错误提示前缀是否使用字段的 `text` 文案(通过 `getTextByName` 从 `config` 中查找)。
|
||||||
|
|
||||||
|
- `true`(默认):错误提示形如 `字段文案 -> 错误信息`,找不到 `text` 时回退为字段 `name`;
|
||||||
|
- `false`:跳过查找,直接使用字段 `name` 作为错误提示前缀(形如 `字段name -> 错误信息`)。
|
||||||
|
|
||||||
|
- **默认值:** `true`
|
||||||
|
|
||||||
|
- **类型:** `boolean`
|
||||||
|
|
||||||
## extendState
|
## extendState
|
||||||
|
|
||||||
- **详情:** 扩展 formState 的钩子函数,返回的对象会被合并到 formState 上
|
- **详情:** 扩展 formState 的钩子函数,返回的对象会被合并到 formState 上
|
||||||
|
|||||||
@ -37,6 +37,7 @@ 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` |
|
||||||
@ -67,7 +68,10 @@ try {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'username',
|
name: 'username',
|
||||||
text: '用户名',
|
text: '用户名',
|
||||||
rules: [{ required: true, message: '请输入用户名' }],
|
rules: [
|
||||||
|
{ required: true, message: '请输入用户名' },
|
||||||
|
{ typeMatch: true, message: '用户名类型不合法' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
initValues: { username: '' },
|
initValues: { username: '' },
|
||||||
|
|||||||
@ -27,6 +27,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:要求 `string`,有代码块 DSL 时须为已有 codeId。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -24,6 +24,10 @@ CodeSelect 组件支持:
|
|||||||
- 选择数据源方法
|
- 选择数据源方法
|
||||||
- 配置代码块参数
|
- 配置代码块参数
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:做 `{ hookType: 'code', hookData }` 的浅层结构校验,`codeId` / 数据源方法存在性由内部单元格各自校验。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -41,6 +41,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:默认要求 `string`,`parse: true` 时跳过校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -27,6 +27,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为已知算子,能解析字段类型时按类型收窄。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -49,6 +49,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数据源路径 `string[]`,有 `fieldConfig` 且非路径值时跳过。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -16,6 +16,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数组并做 `name`/`type` 等浅层结构校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -16,6 +16,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:要求 `string`,`${...}` 绑定须指向已有数据源 / 字段。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -27,6 +27,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为 `[dsId, methodName]`,方法须在该数据源可选方法集中。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -16,6 +16,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数组并做 `content`/`params` 等浅层结构校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -16,6 +16,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数组并做 `title`/`enable`/`data` 等浅层结构校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -38,6 +38,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:`value: 'id'` 时须为已有 ds id,否则为含 `isBindDataSource` + `dataSourceId` 的对象。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -27,6 +27,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数组并做 `cond[].field`/`op` 浅层结构校验,算子合法性 / 字段路径存在性由内部单元格各自校验。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -28,6 +28,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数组并做兼容新旧格式的浅层结构校验,事件名 / 联动动作是否属于可选项由内部单元格各自校验。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -29,6 +29,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为普通对象,`advanced` 且值为 `function` 时放行。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -16,6 +16,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为 `string` 或 `number`,有节点树时须为已有页面片 id。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -16,6 +16,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 值校验
|
||||||
|
|
||||||
|
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为 `string` 或 `number`,有节点树时须为已有组件 id。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|
|||||||
@ -24,6 +24,26 @@ 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="[{
|
||||||
|
|||||||
@ -82,6 +82,21 @@ 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="[{
|
||||||
@ -124,6 +139,7 @@ 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` | — | - |
|
||||||
@ -140,6 +156,8 @@ 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}
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
206
docs/form-config/rules.md
Normal file
206
docs/form-config/rules.md
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
# 表单校验
|
||||||
|
|
||||||
|
`@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}
|
||||||
|
:::
|
||||||
@ -82,7 +82,7 @@ const {
|
|||||||
onPageDiff,
|
onPageDiff,
|
||||||
onDataSourceDiff,
|
onDataSourceDiff,
|
||||||
onCodeBlockDiff,
|
onCodeBlockDiff,
|
||||||
} = useHistoryRevert(editorRef.value);
|
} = useHistoryRevert({}, editorRef.value);
|
||||||
|
|
||||||
// 回滚:可差异步骤弹出差异确认弹窗、其余步骤弹普通二次确认框;用户点「确定」后回滚第 index 步,
|
// 回滚:可差异步骤弹出差异确认弹窗、其余步骤弹普通二次确认框;用户点「确定」后回滚第 index 步,
|
||||||
// 命中前置校验或用户取消时不执行,返回 null
|
// 命中前置校验或用户取消时不执行,返回 null
|
||||||
@ -102,6 +102,7 @@ onCodeBlockDiff(id, index);
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `appContext` | 否 | 父级应用上下文,用于让动态挂载的差异确认弹窗继承全局组件 / 指令 / provide / 插件(Element Plus、`@tmagic/form` 字段组件等)。在组件 `setup` 中调用时会自动取当前组件的 `appContext`,无需手动传;仅当在组件 setup 之外调用时才需显式传入(如 `editorApp._context`)。 |
|
| `appContext` | 否 | 父级应用上下文,用于让动态挂载的差异确认弹窗继承全局组件 / 指令 / provide / 插件(Element Plus、`@tmagic/form` 字段组件等)。在组件 `setup` 中调用时会自动取当前组件的 `appContext`,无需手动传;仅当在组件 setup 之外调用时才需显式传入(如 `editorApp._context`)。 |
|
||||||
| `extendState` | 否 | 透传给差异确认弹窗的 `extendState`(同 Editor 的 [`extendFormState`](#自定义对比判断)),使对比表单中依赖业务上下文的 `display` / `disabled` 等 `filterFunction` 正常工作。 |
|
| `extendState` | 否 | 透传给差异确认弹窗的 `extendState`(同 Editor 的 [`extendFormState`](#自定义对比判断)),使对比表单中依赖业务上下文的 `display` / `disabled` 等 `filterFunction` 正常工作。 |
|
||||||
|
| `dialogWidth` | 否 | 内置页面 / 数据源 / 代码块的差异 / 回滚确认弹窗默认宽度(透传给 `TMagicDialog` 的 `width`),如 `'1200px'` / `'80%'`。缺省时使用弹窗内置默认宽度(`900px`)。业务自有历史可在 `viewDiff` / `confirmAndRevert` 调用时通过各自入参的 `width` 单独覆盖。 |
|
||||||
|
|
||||||
> 若只需要无确认、无校验的静默回滚,直接用上面的 `editorService.revertPageStep` 等即可,无需 `useHistoryRevert`。
|
> 若只需要无确认、无校验的静默回滚,直接用上面的 `editorService.revertPageStep` 等即可,无需 `useHistoryRevert`。
|
||||||
|
|
||||||
|
|||||||
121
docs/guide/advanced/prop-form-validate.md
Normal file
121
docs/guide/advanced/prop-form-validate.md
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
# 属性配置表单校验联动
|
||||||
|
|
||||||
|
编辑器在属性面板(属性表单 / 样式表单)中已支持 [表单校验](../../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 中的失效节点错误记录,避免残留误报;删除节点时也会同步清理其子树相关的错误记录。
|
||||||
@ -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)。
|
@tmagic/form 接受一个表单配置,详细配置可参考[表单 api](../../api/form/form-props.md)。字段校验(含 `typeMatch` 类型匹配)见[表单校验](../../form-config/rules.md)。
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
@ -45,6 +45,20 @@ 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 的样式文件需要单独引入。
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export default defineConfig([
|
|||||||
'*/**/public/**/*',
|
'*/**/public/**/*',
|
||||||
'*/**/types/**/*',
|
'*/**/types/**/*',
|
||||||
'*/**/*.config.ts',
|
'*/**/*.config.ts',
|
||||||
'./tepm/**/*',
|
'./temp/**/*',
|
||||||
'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')),
|
||||||
|
|||||||
46
package.json
46
package.json
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.8.0-beta.8",
|
"version": "1.8.0-beta.16",
|
||||||
"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.1.0",
|
"@commitlint/cli": "^20.5.3",
|
||||||
"@commitlint/config-conventional": "^20.0.0",
|
"@commitlint/config-conventional": "^20.5.3",
|
||||||
"@tmagic/eslint-config": "workspace:*",
|
"@tmagic/eslint-config": "workspace:*",
|
||||||
"@types/node": "24.0.10",
|
"@types/node": "24.0.10",
|
||||||
"@vitejs/plugin-vue": "^6.0.6",
|
"@vitejs/plugin-vue": "^6.0.8",
|
||||||
"@vitest/coverage-v8": "^4.1.5",
|
"@vitest/coverage-v8": "^4.1.10",
|
||||||
"@vue/compiler-sfc": "catalog:",
|
"@vue/compiler-sfc": "catalog:",
|
||||||
"c8": "^10.1.3",
|
"c8": "^10.1.3",
|
||||||
"commitizen": "^4.3.1",
|
"commitizen": "^4.3.2",
|
||||||
"conventional-changelog-cli": "^5.0.0",
|
"conventional-changelog-cli": "^5.0.0",
|
||||||
"cosmiconfig": "^ 9.0.0 ",
|
"cosmiconfig": "^9.0.2",
|
||||||
"cz-conventional-changelog": "^3.3.0",
|
"cz-conventional-changelog": "^3.3.0",
|
||||||
"element-plus": "^2.11.8",
|
"element-plus": "^2.14.3",
|
||||||
"enquirer": "^2.4.1",
|
"enquirer": "^2.4.1",
|
||||||
"eslint": "^10.3.0",
|
"eslint": "^10.7.0",
|
||||||
"execa": "^9.6.0",
|
"execa": "^9.6.1",
|
||||||
"happy-dom": "^20.9.0",
|
"happy-dom": "^20.10.6",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^27.2.0",
|
"jsdom": "^27.4.0",
|
||||||
"lint-staged": "^16.2.7",
|
"lint-staged": "^16.4.0",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.9.5",
|
||||||
"recast": "^0.23.11",
|
"recast": "^0.23.12",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rolldown": "^1.0.1",
|
"rolldown": "^1.2.0",
|
||||||
"rolldown-plugin-dts": "^0.25.1",
|
"rolldown-plugin-dts": "^0.26.0",
|
||||||
"sass-embedded": "^1.99.0",
|
"sass-embedded": "^1.100.0",
|
||||||
"semver": "^7.7.3",
|
"semver": "^7.8.5",
|
||||||
"serialize-javascript": "^7.0.0",
|
"serialize-javascript": "^7.0.7",
|
||||||
"shx": "^0.3.4",
|
"shx": "^0.3.4",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"vitepress": "^2.0.0-alpha.17",
|
"vitepress": "2.0.0-alpha.18",
|
||||||
"vitest": "^4.1.6",
|
"vitest": "^4.1.10",
|
||||||
"vue": "catalog:",
|
"vue": "catalog:",
|
||||||
"vue-tsc": "^3.2.9"
|
"vue-tsc": "^3.3.7"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"commitizen": {
|
"commitizen": {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.8.0-beta.8",
|
"version": "1.8.0-beta.16",
|
||||||
"name": "@tmagic/cli",
|
"name": "@tmagic/cli",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"types": "lib/index.d.ts",
|
"types": "lib/index.d.ts",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.8.0-beta.8",
|
"version": "1.8.0-beta.16",
|
||||||
"name": "@tmagic/core",
|
"name": "@tmagic/core",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -76,10 +76,12 @@ 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();
|
||||||
|
|||||||
@ -79,10 +79,12 @@ 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,
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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")',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.8.0-beta.8",
|
"version": "1.8.0-beta.16",
|
||||||
"name": "@tmagic/data-source",
|
"name": "@tmagic/data-source",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -38,8 +38,7 @@ export interface HttpDataSourceSchema extends DataSourceSchema {
|
|||||||
};
|
};
|
||||||
autoFetch?: boolean;
|
autoFetch?: boolean;
|
||||||
beforeRequest:
|
beforeRequest:
|
||||||
| string
|
string | ((options: HttpOptions, content: { app: TMagicApp; dataSource: HttpDataSource }) => HttpOptions);
|
||||||
| ((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);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.8.0-beta.8",
|
"version": "1.8.0-beta.16",
|
||||||
"name": "@tmagic/dep",
|
"name": "@tmagic/dep",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.8.0-beta.8",
|
"version": "1.8.0-beta.16",
|
||||||
"name": "@tmagic/design",
|
"name": "@tmagic/design",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": [
|
"sideEffects": [
|
||||||
|
|||||||
23
packages/design/src/Alert.vue
Normal file
23
packages/design/src/Alert.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<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>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<component class="tmagic-design-card" :is="uiComponent" v-bind="uiProps">
|
<component :is="uiComponent" :class="['tmagic-design-card', { 'tmagic-design-card--flat': isFlat }]" 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,6 +14,7 @@
|
|||||||
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({
|
||||||
@ -26,5 +27,14 @@ const ui = getDesignConfig('components')?.card;
|
|||||||
|
|
||||||
const uiComponent = ui?.component || 'el-card';
|
const uiComponent = ui?.component || 'el-card';
|
||||||
|
|
||||||
const uiProps = computed<CardProps>(() => ui?.props(props) || props);
|
// 当祖先 `<MEditor>` / `<MForm>` 的 theme 是 `magic-admin` 时,整套设计语言默认走「无卡片感」,
|
||||||
|
// 因此这里自动等价于 `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>
|
||||||
|
|||||||
@ -7,18 +7,39 @@
|
|||||||
@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 } from 'vue';
|
import { computed, type ComputedRef, inject } 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,
|
||||||
|
|||||||
@ -35,6 +35,16 @@ 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 使用 clearValidate,element-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();
|
||||||
|
|||||||
@ -1,36 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<component class="tmagic-design-form-item" :is="uiComponent" v-bind="uiProps">
|
<component
|
||||||
|
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>
|
||||||
<div v-if="adapterType === 'element-plus' && extra" v-html="extra" class="m-form-tip"></div>
|
<alert
|
||||||
|
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 } from 'vue';
|
import { computed, inject } 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>();
|
const props = defineProps<FormItemProps & { theme?: string }>();
|
||||||
|
|
||||||
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', false);
|
||||||
|
const formInRow = inject<boolean>('isInRow', false);
|
||||||
const uiProps = computed<FormItemProps>(() => {
|
const uiProps = computed<FormItemProps>(() => {
|
||||||
const { extra, ...rest } = ui?.props(props) || props;
|
const { extra, extraTips, ...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>
|
||||||
|
|||||||
@ -4,10 +4,9 @@
|
|||||||
<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="popperClass"
|
:class="['tmagic-design-popper', popperClass, themeClass]"
|
||||||
:style="style"
|
:style="style"
|
||||||
@mouseenter.once="popperMouseenterHandler"
|
@mouseenter.once="popperMouseenterHandler"
|
||||||
>
|
>
|
||||||
@ -22,7 +21,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 { useZIndex } from './index';
|
import { useThemeClass, useZIndex } from './index';
|
||||||
import type { PopoverProps } from './types';
|
import type { PopoverProps } from './types';
|
||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
@ -46,6 +45,12 @@ 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];
|
||||||
|
|||||||
47
packages/design/src/formValidateMessage.ts
Normal file
47
packages/design/src/formValidateMessage.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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];
|
||||||
@ -8,6 +8,8 @@ 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';
|
||||||
@ -93,6 +95,10 @@ 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 =
|
||||||
@ -128,6 +134,8 @@ 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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
59
packages/design/src/theme.ts
Normal file
59
packages/design/src/theme.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* 主题作用域穿透到 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 proxy,computed 可正确跟踪外部更新);不要传
|
||||||
|
* `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 || '');
|
||||||
|
};
|
||||||
67
packages/design/src/theme/card.scss
Normal file
67
packages/design/src/theme/card.scss
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// `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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
packages/design/src/theme/colorPicker.scss
Normal file
8
packages/design/src/theme/colorPicker.scss
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.tmagic-design-color-picker-input {
|
||||||
|
margin-left: 8px;
|
||||||
|
width: calc(var(--el-input-width, 100%) - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmagic-design-color-picker {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
@ -1 +1,3 @@
|
|||||||
@use "./popover.scss";
|
@use "./popover.scss";
|
||||||
|
@use "./card.scss";
|
||||||
|
@use "./colorPicker.scss";
|
||||||
|
|||||||
38
packages/design/src/theme/themes/magic-admin/index.scss
Normal file
38
packages/design/src/theme/themes/magic-admin/index.scss
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,6 +24,17 @@ 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;
|
||||||
@ -39,6 +50,11 @@ 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 {
|
||||||
@ -97,6 +113,7 @@ export interface ColorPickerProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
showAlpha?: boolean;
|
showAlpha?: boolean;
|
||||||
size?: FieldSize;
|
size?: FieldSize;
|
||||||
|
flat?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatePickerProps {
|
export interface DatePickerProps {
|
||||||
@ -185,7 +202,9 @@ 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 {
|
||||||
@ -234,6 +253,9 @@ 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'
|
||||||
@ -461,6 +483,11 @@ 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;
|
||||||
@ -723,6 +750,7 @@ export interface Components {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DesignPluginOptions {
|
export interface DesignPluginOptions {
|
||||||
|
flat?: boolean;
|
||||||
adapterType?: string;
|
adapterType?: string;
|
||||||
message?: TMagicMessage;
|
message?: TMagicMessage;
|
||||||
messageBox?: TMagicMessageBox;
|
messageBox?: TMagicMessageBox;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.8.0-beta.8",
|
"version": "1.8.0-beta.16",
|
||||||
"name": "@tmagic/editor",
|
"name": "@tmagic/editor",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": [
|
"sideEffects": [
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
: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>
|
||||||
@ -135,9 +136,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
import { provide } from 'vue';
|
import { computed, provide, ref } 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';
|
||||||
@ -158,7 +160,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 } from './editorProps';
|
import { defaultEditorProps, EditorProps, ENABLE_PROPS_FORM_VALIDATE } 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';
|
||||||
@ -227,10 +229,14 @@ 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 都能拿到一致的扩展状态,
|
||||||
@ -238,6 +244,20 @@ provide('stageOptions', stageOptions);
|
|||||||
*/
|
*/
|
||||||
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 透传)。业务方可借此在历史记录
|
||||||
@ -247,10 +267,22 @@ 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');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,9 @@
|
|||||||
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"
|
||||||
>
|
>
|
||||||
@ -206,6 +208,7 @@ 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(() => {
|
||||||
|
|||||||
@ -10,104 +10,45 @@
|
|||||||
:is-compare="true"
|
:is-compare="true"
|
||||||
:disabled="true"
|
:disabled="true"
|
||||||
:label-width="labelWidth"
|
:label-width="labelWidth"
|
||||||
:extend-state="extendState"
|
:extend-state="mergedExtendState"
|
||||||
: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, inject, provide, type Ref, ref, type ShallowRef, useTemplateRef, watch, watchEffect } from 'vue';
|
import { computed, type Ref, type ShallowRef } from 'vue';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
|
||||||
import { type CodeBlockContent, type DataSourceSchema, HookType, type MNode } from '@tmagic/core';
|
import { type CodeBlockContent, HookType } from '@tmagic/core';
|
||||||
import { type FormConfig, type FormState, type FormValue, MForm } from '@tmagic/form';
|
import { type FormConfig, type FormValue, MForm } from '@tmagic/form';
|
||||||
|
|
||||||
import { useServices } from '@editor/hooks/use-services';
|
import { useCompareForm } from '@editor/hooks/use-compare-form';
|
||||||
import type { CompareCategory, CompareFormLoadConfig, Services } from '@editor/type';
|
import type { CompareFormBaseProps } 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?: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
|
lastValue?: CompareFormBaseProps['value'];
|
||||||
/**
|
/** 需要走 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;
|
|
||||||
/** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
|
|
||||||
services: Services;
|
|
||||||
}>(),
|
|
||||||
{
|
{
|
||||||
category: 'node',
|
category: 'node',
|
||||||
labelWidth: '120px',
|
labelWidth: '120px',
|
||||||
services: () => useServices(),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { propsService, dataSourceService, codeBlockService, editorService } = props.services;
|
const { config, currentValues, wrapperStyle, mergedExtendState, loadConfig, formRef, normalizeCodeBlockValue } =
|
||||||
|
useCompareForm(props);
|
||||||
provide('services', props.services);
|
|
||||||
|
|
||||||
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') {
|
||||||
@ -116,18 +57,6 @@ 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` 字段在历史数据中存在两种"语义为空"的形态:
|
||||||
* - 字符串 `''`(旧数据 / 用户从未配置过钩子);
|
* - 字符串 `''`(旧数据 / 用户从未配置过钩子);
|
||||||
@ -156,104 +85,6 @@ 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': {
|
|
||||||
const config = dataSourceService.getFormConfig(props.type || 'base');
|
|
||||||
// 数据源表单外层 tab 的「数据定义」项 status 为 'fields',tab-pane name 随之为 'fields'。
|
|
||||||
// 未显式设置 active 时,Tabs 默认取 '0',与 'fields' 不匹配会导致打开弹窗时无默认激活项,
|
|
||||||
// 这里与 DataSourceConfigPanel 保持一致,默认激活「数据定义」tab。
|
|
||||||
return config.map((item) => ('type' in item && item.type === 'tab' ? { ...item, active: 'fields' } : item));
|
|
||||||
}
|
|
||||||
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 = props.services;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose<{
|
defineExpose<{
|
||||||
form: ShallowRef<InstanceType<typeof MForm> | null>;
|
form: ShallowRef<InstanceType<typeof MForm> | null>;
|
||||||
config: Ref<FormConfig>;
|
config: Ref<FormConfig>;
|
||||||
|
|||||||
@ -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 { useZIndex } from '@tmagic/design';
|
import { useThemeClass, useZIndex } from '@tmagic/design';
|
||||||
|
|
||||||
import { MenuButton, MenuComponent } from '@editor/type';
|
import { MenuButton, MenuComponent } from '@editor/type';
|
||||||
|
|
||||||
@ -73,6 +73,14 @@ 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);
|
||||||
@ -120,20 +128,42 @@ const outsideClickHideHandler = (e: MouseEvent) => {
|
|||||||
hide();
|
hide();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setPosition = (e: { clientY: number; clientX: number }) => {
|
// 根据菜单实际尺寸修正位置,避免超出可视范围
|
||||||
const menuHeight = menuEl.value?.clientHeight || 0;
|
const fixPosition = () => {
|
||||||
|
const menu = menuEl.value;
|
||||||
|
if (!menu || !visible.value) return;
|
||||||
|
|
||||||
let top = e.clientY;
|
const menuHeight = menu.clientHeight;
|
||||||
if (menuHeight + e.clientY > document.body.clientHeight) {
|
const menuWidth = menu.clientWidth;
|
||||||
top = document.body.clientHeight - menuHeight;
|
|
||||||
|
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 }) => {
|
||||||
menuPosition.value = {
|
menuPosition.value = {
|
||||||
top,
|
top: e.clientY,
|
||||||
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;
|
||||||
|
|
||||||
@ -178,12 +208,18 @@ 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);
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body" v-if="visible">
|
<Teleport to="body" v-if="visible">
|
||||||
<div ref="target" class="m-editor-float-box" :style="{ ...style, zIndex: curZIndex }" @mousedown="nextZIndex">
|
<div
|
||||||
|
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>
|
||||||
@ -9,7 +15,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` }">
|
<div class="m-editor-float-box-body" :style="{ height: `${bodyHeight}px`, ...bodyStyle }">
|
||||||
<slot name="body"></slot>
|
<slot name="body"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -17,14 +23,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onBeforeUnmount, provide, ref, useTemplateRef, watch } from 'vue';
|
import { computed, type CSSProperties, 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, useZIndex } from '@tmagic/design';
|
import { TMagicButton, useThemeClass, 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;
|
||||||
@ -39,17 +44,30 @@ 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);
|
||||||
|
|
||||||
@ -66,12 +84,11 @@ 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;
|
||||||
if (width.value) {
|
const frameworkWidth = props.frameworkWidth || globalThis.window?.innerWidth || 0;
|
||||||
left = left + width.value > frameworkWidth.value ? frameworkWidth.value - width.value : left;
|
if (width.value && frameworkWidth) {
|
||||||
|
left = left + width.value > frameworkWidth ? frameworkWidth - width.value : left;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -79,11 +96,60 @@ 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',
|
||||||
@ -101,6 +167,14 @@ 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;
|
||||||
});
|
});
|
||||||
@ -115,6 +189,7 @@ const initMoveable = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const destroyMoveable = () => {
|
const destroyMoveable = () => {
|
||||||
|
hideDragMask();
|
||||||
moveable?.destroy();
|
moveable?.destroy();
|
||||||
moveable = null;
|
moveable = null;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -87,70 +87,3 @@ 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>
|
|
||||||
|
|||||||
55
packages/editor/src/components/ViewForm.vue
Normal file
55
packages/editor/src/components/ViewForm.vue
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<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>
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
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, {
|
||||||
@ -26,7 +28,17 @@ 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;
|
||||||
/** 左侧面板中的组件类型列表 */
|
/** 左侧面板中的组件类型列表 */
|
||||||
@ -91,6 +103,11 @@ export interface EditorProps {
|
|||||||
disabledFlashTip?: boolean;
|
disabledFlashTip?: boolean;
|
||||||
/** 禁用双击在浮层中单独编辑选中组件 */
|
/** 禁用双击在浮层中单独编辑选中组件 */
|
||||||
disabledStageOverlay?: boolean;
|
disabledStageOverlay?: boolean;
|
||||||
|
/**
|
||||||
|
* 是否启用「属性配置表单校验」联动能力:开启后属性/样式表单校验失败时仍更新节点,
|
||||||
|
* 并把错误信息集中记录到 editorService,用于组件树标红提示与保存拦截;默认 false(关闭)。
|
||||||
|
*/
|
||||||
|
enablePropsFormValidate?: boolean;
|
||||||
/** 禁用属性配置面板右下角显示源码的按钮 */
|
/** 禁用属性配置面板右下角显示源码的按钮 */
|
||||||
disabledShowSrc?: boolean;
|
disabledShowSrc?: boolean;
|
||||||
/** 禁用数据源 */
|
/** 禁用数据源 */
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<MagicCodeEditor
|
<MagicCodeEditor
|
||||||
|
v-if="!silentMode"
|
||||||
:height="config.height"
|
:height="config.height"
|
||||||
:type="diffMode ? 'diff' : undefined"
|
:type="diffMode ? 'diff' : undefined"
|
||||||
:init-values="diffMode ? (lastValues || {})[name] : model[name]"
|
:init-values="diffMode ? (lastValues || {})[name] : model[name]"
|
||||||
@ -17,9 +18,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed, inject } from 'vue';
|
||||||
|
|
||||||
import type { CodeConfig, FieldProps } from '@tmagic/form';
|
import { type CodeConfig, type FieldProps, FORM_SILENT_MODE_KEY } from '@tmagic/form';
|
||||||
|
|
||||||
import MagicCodeEditor from '@editor/layouts/CodeEditor.vue';
|
import MagicCodeEditor from '@editor/layouts/CodeEditor.vue';
|
||||||
|
|
||||||
@ -27,6 +28,13 @@ defineOptions({
|
|||||||
name: 'MFieldsVsCode',
|
name: 'MFieldsVsCode',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 静默模式(submitForm/validateForm 隐藏挂载)下跳过 monaco 渲染:
|
||||||
|
* 校验由 FormItem 针对 model 中的值完成,与编辑器实例无关;本组件挂载无值副作用
|
||||||
|
* (save 仅由用户操作/编辑器内容变更触发),跳过可省去 monaco worker/model 的无谓实例化。
|
||||||
|
*/
|
||||||
|
const silentMode = inject(FORM_SILENT_MODE_KEY, false);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
change: [value: string | any];
|
change: [value: string | any];
|
||||||
}>();
|
}>();
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="m-fields-code-select" :class="config.className">
|
<div class="m-fields-code-select" :class="config.className">
|
||||||
<TMagicCard>
|
<TMagicCard :flat="config.flat">
|
||||||
<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"
|
||||||
@ -12,16 +13,20 @@
|
|||||||
@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 { TMagicCard } from '@tmagic/design';
|
import { TMagicButton, 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';
|
||||||
|
|
||||||
@ -53,12 +58,23 @@ 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)) {
|
||||||
@ -81,48 +97,46 @@ const codeConfig = computed<GroupListConfig>(() => ({
|
|||||||
|
|
||||||
return model.codeId || index;
|
return model.codeId || index;
|
||||||
},
|
},
|
||||||
|
titlePrefix: props.config.name === undefined ? undefined : String(props.config.name),
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
type: 'row',
|
text: '代码类型',
|
||||||
items: [
|
|
||||||
{
|
|
||||||
type: 'select',
|
type: 'select',
|
||||||
name: 'codeType',
|
name: 'codeType',
|
||||||
span: 6,
|
labelPosition: 'right',
|
||||||
|
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: 'code',
|
defaultValue: HookCodeType.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',
|
||||||
span: 18,
|
text: '代码块',
|
||||||
labelWidth: 0,
|
rules: [{ typeMatch: true, trigger: 'change' }],
|
||||||
|
|
||||||
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',
|
||||||
span: 18,
|
text: '数据源字段',
|
||||||
labelWidth: 0,
|
rules: [{ typeMatch: true, trigger: 'change' }],
|
||||||
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(
|
||||||
|
|||||||
@ -147,6 +147,7 @@ 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) => ({
|
||||||
|
|||||||
@ -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 { arrayOptions, eqOptions, getFieldType, numberOptions } from '@editor/utils';
|
import { getCondOpOptionsByFieldType, getFieldType } from '@editor/utils';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MFieldsCondOpSelect',
|
name: 'MFieldsCondOpSelect',
|
||||||
@ -51,31 +51,9 @@ 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) => {
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="m-fields-data-source-field-select">
|
<div
|
||||||
|
class="m-fields-data-source-field-select"
|
||||||
|
:class="{
|
||||||
|
[`data-source-field-${type}`]: !isSelectValid,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<FieldSelect
|
<FieldSelect
|
||||||
v-if="!disabledDataSource && (showDataSourceFieldSelect || !config.fieldConfig)"
|
v-if="isSelectValid"
|
||||||
:model-value="model[name]"
|
:model-value="model[name]"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:size="size"
|
:size="size"
|
||||||
@ -37,15 +42,14 @@
|
|||||||
:size="size"
|
:size="size"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@click="onToggleDataSourceFieldSelectHandler"
|
@click="onToggleDataSourceFieldSelectHandler"
|
||||||
><MIcon :icon="Coin"></MIcon
|
><MIcon :icon="dataSourceIcon"></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';
|
||||||
@ -61,6 +65,8 @@ 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({
|
||||||
@ -75,6 +81,9 @@ 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) => {
|
||||||
|
|||||||
@ -8,10 +8,12 @@
|
|||||||
</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>
|
||||||
@ -28,10 +30,12 @@
|
|||||||
</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>
|
||||||
@ -358,6 +362,7 @@ 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 注入:打开数据源详情后需要直接打开的字段路径(字段名数组)。
|
* 由 DataSourceConfigPanel 注入:打开数据源详情后需要直接打开的字段路径(字段名数组)。
|
||||||
|
|||||||
@ -30,10 +30,10 @@
|
|||||||
<template #suffix>
|
<template #suffix>
|
||||||
<Icon :icon="Coin" />
|
<Icon :icon="Coin" />
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ item }">
|
<template #default="slotProps">
|
||||||
<div style="display: flex; flex-direction: column; line-height: 1.2em">
|
<div style="display: flex; flex-direction: column; line-height: 1.2em">
|
||||||
<div>{{ item.text }}</div>
|
<div>{{ slotProps?.item?.text || '' }}</div>
|
||||||
<span style="font-size: 10px; color: rgba(0, 0, 0, 0.6)">{{ item.value }}</span>
|
<span style="font-size: 10px; color: rgba(0, 0, 0, 0.6)">{{ slotProps?.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, isNumber } from '@tmagic/utils';
|
import { getKeysArray } 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 } from '@editor/utils/data-source';
|
import { getDisplayField, resolveFieldByPath } from '@editor/utils/data-source';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MFieldsDataSourceInput',
|
name: 'MFieldsDataSourceInput',
|
||||||
@ -251,34 +251,16 @@ const fieldQuerySearch = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let fields = ds.fields || [];
|
const { fields } = resolveFieldByPath(ds.fields, keys, { skipNumberIndices: true });
|
||||||
|
|
||||||
// 后面这些是字段
|
|
||||||
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 =
|
result = fields.filter(
|
||||||
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(
|
||||||
|
|||||||
@ -7,10 +7,12 @@
|
|||||||
</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>
|
||||||
@ -254,4 +256,5 @@ 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>
|
||||||
|
|||||||
@ -68,12 +68,15 @@ 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: 80,
|
operateColWidth: props.config.operateColWidth,
|
||||||
enableToggleMode: false,
|
enableToggleMode: false,
|
||||||
|
fixed: props.config.fixed,
|
||||||
|
flat: props.config.flat,
|
||||||
items: [
|
items: [
|
||||||
parentFields.value.length
|
parentFields.value.length
|
||||||
? {
|
? {
|
||||||
@ -98,6 +101,11 @@ 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',
|
||||||
@ -107,6 +115,11 @@ 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',
|
||||||
@ -114,6 +127,10 @@ 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: '值',
|
||||||
|
|||||||
@ -12,16 +12,21 @@
|
|||||||
@change="onChangeHandler"
|
@change="onChangeHandler"
|
||||||
></MTable>
|
></MTable>
|
||||||
|
|
||||||
<div v-else class="fullWidth">
|
<div v-else class="fullWidth event-select-container">
|
||||||
|
<div class="event-select-header">
|
||||||
|
<div class="event-select-title">事件配置</div>
|
||||||
<TMagicButton
|
<TMagicButton
|
||||||
v-if="!isCompareMode"
|
v-if="!isCompareMode && displayList.length > 0"
|
||||||
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"
|
||||||
@ -32,10 +37,13 @@
|
|||||||
: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"
|
||||||
@ -48,16 +56,26 @@
|
|||||||
@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>
|
||||||
@ -65,13 +83,12 @@
|
|||||||
<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,
|
||||||
@ -84,10 +101,15 @@ 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 { getCascaderOptionsFromFields } from '@editor/utils';
|
import {
|
||||||
|
getCompActionAllowedValues,
|
||||||
|
getCompActionOptions,
|
||||||
|
getEventNameAllowedValues,
|
||||||
|
getEventNameOptions,
|
||||||
|
normalizeCompActionValue,
|
||||||
|
} from '@editor/utils';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MFieldsEventSelect',
|
name: 'MFieldsEventSelect',
|
||||||
@ -105,7 +127,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' ||
|
||||||
@ -115,79 +137,27 @@ const eventNameConfig = computed(() => {
|
|||||||
}
|
}
|
||||||
return 'select';
|
return 'select';
|
||||||
},
|
},
|
||||||
labelWidth: '40px',
|
labelWidth: '70px',
|
||||||
checkStrictly: () => props.config.src !== 'component',
|
checkStrictly: () => props.config.src !== 'component',
|
||||||
valueSeparator: '.',
|
valueSeparator: '.',
|
||||||
options: (mForm: FormState, { formValue }: any) => {
|
options: (_mForm: FormState, { formValue }: any) => getEventNameOptions(props.config.src, formValue),
|
||||||
let events: EventOption[] | CascaderOption[] = [];
|
rules: [
|
||||||
|
|
||||||
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 = [
|
|
||||||
{
|
{
|
||||||
label: pageFragment.name || '页面片容器',
|
validator: ({ value, callback }: any, { formValue }: any) => {
|
||||||
value: pageFragment.id,
|
const allowedNames = getEventNameAllowedValues(props.config as any, formValue);
|
||||||
children: events,
|
if (allowedNames && allowedNames.size > 0 && value && !allowedNames.has(value)) {
|
||||||
|
return callback(`事件名(${value})不存在`);
|
||||||
|
}
|
||||||
|
callback();
|
||||||
},
|
},
|
||||||
];
|
trigger: 'blur',
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
@ -219,6 +189,32 @@ const actionTypeConfig = 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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onChange: (_mForm: FormState, _v: string, { setModel }: any) => {
|
||||||
|
setModel('to', '');
|
||||||
|
setModel('method', '');
|
||||||
|
setModel('codeId', '');
|
||||||
|
setModel('dataSourceMethod', []);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return { ...defaultActionTypeConfig, ...props.config.actionTypeConfig };
|
return { ...defaultActionTypeConfig, ...props.config.actionTypeConfig };
|
||||||
@ -230,10 +226,17 @@ 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 };
|
||||||
});
|
});
|
||||||
@ -243,6 +246,7 @@ 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);
|
||||||
|
|
||||||
@ -254,41 +258,20 @@ 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) => {
|
options: (_mForm: FormState, { model }: any) => getCompActionOptions(model.to),
|
||||||
const node = editorService.getNodeById(model.to);
|
rules: [
|
||||||
if (!node?.type) return [];
|
{
|
||||||
|
trigger: 'blur',
|
||||||
let methods: EventOption[] | CascaderOption[] = [];
|
validator: ({ value, callback }: any, { model }: any) => {
|
||||||
|
const allowedMethods = getCompActionAllowedValues(props.config as any, model);
|
||||||
methods = eventsService.getMethod(node.type, model.to);
|
const normalized = normalizeCompActionValue(value);
|
||||||
|
if (allowedMethods && allowedMethods.size > 0 && normalized && !allowedMethods.has(normalized)) {
|
||||||
if (node.type === 'page-fragment-container' && node.pageFragmentId) {
|
return callback(`动作名(${normalized})不存在`);
|
||||||
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 };
|
||||||
});
|
});
|
||||||
@ -362,6 +345,7 @@ const actionsConfig = computed(
|
|||||||
() =>
|
() =>
|
||||||
defineFormItem({
|
defineFormItem({
|
||||||
type: 'panel',
|
type: 'panel',
|
||||||
|
labelPosition: 'left',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
type: 'group-list',
|
type: 'group-list',
|
||||||
@ -369,6 +353,7 @@ 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,
|
||||||
|
|||||||
@ -2,11 +2,12 @@
|
|||||||
<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><MIcon :icon="Grid"></MIcon>{{ item.title }}</template>
|
<template #title>{{ 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"
|
||||||
@ -21,21 +22,18 @@
|
|||||||
|
|
||||||
<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',
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps<FieldProps<StyleSchema>>();
|
defineProps<FieldProps<StyleSchema>>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
change: [v: any, eventData: ContainerChangeEventData];
|
change: [v: any, eventData: ContainerChangeEventData];
|
||||||
@ -77,13 +75,6 @@ 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
@click="selectDirection('Left')"
|
@click="selectDirection('Left')"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
class="border-icon"
|
class="border-icon center"
|
||||||
:class="{ active: direction === '', configured: isConfigured('') }"
|
:class="{ active: direction === '', configured: isConfigured('') }"
|
||||||
@click="selectDirection()"
|
@click="selectDirection()"
|
||||||
></div>
|
></div>
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
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';
|
||||||
@ -1,8 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
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"
|
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"
|
||||||
fill="#5D5D5D"
|
fill-opacity="0.3"
|
||||||
></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>
|
||||||
|
|||||||
@ -1,31 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path
|
<rect x="13" y="3" width="2" height="2" rx="0.5" transform="rotate(90 13 3)" 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="7" width="2" height="2" rx="0.5" transform="rotate(90 13 7)" fill-opacity="0.3" />
|
||||||
></path>
|
<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)" />
|
||||||
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="7" width="2" height="2" rx="0.5" transform="rotate(90 9 7)" />
|
||||||
></path>
|
<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)" />
|
||||||
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="7" width="2" height="2" rx="0.5" transform="rotate(90 5 7)" />
|
||||||
></path>
|
<rect x="5" y="11" width="2" height="2" rx="0.5" transform="rotate(90 5 11)" fill-opacity="0.3" />
|
||||||
<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>
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path
|
<rect x="3" 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="7" y="3" width="2" height="10" rx="1" />
|
||||||
fill="#333333"
|
<rect x="11" y="3" width="2" height="10" rx="1" fill-opacity="0.3" />
|
||||||
></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path
|
<rect x="13" y="3" width="2" height="10" rx="1" transform="rotate(90 13 3)" />
|
||||||
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="7" width="2" height="10" rx="1" transform="rotate(90 13 7)" />
|
||||||
fill="#333333"
|
<rect x="13" y="11" width="2" height="10" rx="1" transform="rotate(90 13 11)" fill-opacity="0.3" />
|
||||||
></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
d="M896 320H128V0h768v320z m0 320H128v-256h768v256z m-128 192l-256 192-256-192 192-0.032V704h128v128h192z"
|
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"
|
||||||
></path>
|
/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 12L8 15L5 12L11 12Z" fill-opacity="0.3" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
d="M896 704H128v320h768V704z m0-320H128v256h768v-256z m-128-192l-256-192-256 192 192 0.032V320h128V192h192z"
|
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"
|
||||||
></path>
|
/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 4L8 1L11 4L5 4Z" fill-opacity="0.3" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
d="M320 128v768H0V128h320z m320 0v768h-256V128h256z m192 128l192 256-192 256-0.032-192H704v-128h128V256z"
|
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"
|
||||||
></path>
|
/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 5L15 8L12 11L12 5Z" fill-opacity="0.3" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
d="M704 128v768h320V128H704zM384 128v768h256V128h-256zM192 256l-192 256 192 256 0.032-192H320v-128H192V256z"
|
d="M14 4C14.5523 4 15 4.44772 15 5V11C15 11.5523 14.5523 12 14 12H6C5.44772 12 5 11.5523 5 11V5C5 4.44772 5.44772 4 6 4H14ZM6.5 5C6.22386 5 6 5.22386 6 5.5V10.5C6 10.7761 6.22386 11 6.5 11H13.5C13.7761 11 14 10.7761 14 10.5V5.5C14 5.22386 13.7761 5 13.5 5H6.5Z"
|
||||||
></path>
|
/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 11L1 8L4 5L4 11Z" fill-opacity="0.3" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path d="M480 1024V0h64v1024h-64z m128-64V64h320v896H608zM96 960V64h320v896H96z"></path>
|
<path
|
||||||
|
d="M13 4C13.5523 4 14 4.44772 14 5V11C14 11.5523 13.5523 12 13 12H3C2.44772 12 2 11.5523 2 11V5C2 4.44772 2.44772 4 3 4H13ZM3.59082 5C3.31472 5.00005 3.09082 5.22389 3.09082 5.5V10.5C3.09082 10.7761 3.31472 11 3.59082 11H12.4092C12.6853 11 12.9092 10.7761 12.9092 10.5V5.5C12.9092 5.22389 12.6853 5.00005 12.4092 5H3.59082Z"
|
||||||
|
/>
|
||||||
|
<rect x="7.5" y="2" width="1" height="12" rx="0.5" fill-opacity="0.3" style="fill: black; fill-opacity: 0.3" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,5 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path d="M416 160H96v704h320V160z m384 0H480v704h320V160z m128-160h-64v1024h64V0z"></path>
|
<path
|
||||||
|
d="M11 4C11.5523 4 12 4.44772 12 5V11C12 11.5523 11.5523 12 11 12H2C1.44772 12 1 11.5523 1 11V5C1 4.44772 1.44772 4 2 4H11ZM2.5 5C2.22386 5 2 5.22386 2 5.5V10.5C2 10.7761 2.22386 11 2.5 11H10.5C10.7761 11 11 10.7761 11 10.5V5.5C11 5.22386 10.7761 5 10.5 5H2.5Z"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="14.25"
|
||||||
|
y="2.25"
|
||||||
|
width="0.5"
|
||||||
|
height="11.5"
|
||||||
|
rx="0.25"
|
||||||
|
stroke="black"
|
||||||
|
stroke-opacity="0.3"
|
||||||
|
style="stroke: black; stroke-opacity: 0.3"
|
||||||
|
stroke-width="0.5"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,5 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path d="M608 160h320v704H608V160zM224 160h320v704H224V160zM96 0h64v1024H96V0z"></path>
|
<path
|
||||||
|
d="M14 4C14.5523 4 15 4.44772 15 5V11C15 11.5523 14.5523 12 14 12H5C4.44772 12 4 11.5523 4 11V5C4 4.44772 4.44772 4 5 4H14ZM5.5 5C5.22386 5 5 5.22386 5 5.5V10.5C5 10.7761 5.22386 11 5.5 11H13.5C13.7761 11 14 10.7761 14 10.5V5.5C14 5.22386 13.7761 5 13.5 5H5.5Z"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="1.25"
|
||||||
|
y="2.25"
|
||||||
|
width="0.5"
|
||||||
|
height="11.5"
|
||||||
|
rx="0.25"
|
||||||
|
stroke="white"
|
||||||
|
stroke-opacity="0.3"
|
||||||
|
style="stroke: white; stroke-opacity: 0.3"
|
||||||
|
stroke-width="0.5"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,7 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
d="M320 864H192v160H128v-160H0V160h128V0h64v160h128v704z m704 0h-128v160h-64v-160h-128V160h128V0h64v160h128v704z"
|
d="M6 4C6.55228 4 7 4.44772 7 5V11C7 11.5523 6.55228 12 6 12H3C2.44772 12 2 11.5523 2 11V5C2 4.44772 2.44772 4 3 4H6ZM3.5 5C3.22386 5 3 5.22386 3 5.5V10.5C3 10.7761 3.22386 11 3.5 11H5.5C5.77614 11 6 10.7761 6 10.5V5.5C6 5.22386 5.77614 5 5.5 5H3.5Z"
|
||||||
></path>
|
/>
|
||||||
|
<path
|
||||||
|
d="M13 4C13.5523 4 14 4.44772 14 5V11C14 11.5523 13.5523 12 13 12H10C9.44772 12 9 11.5523 9 11V5C9 4.44772 9.44772 4 10 4H13ZM10.5 5C10.2239 5 10 5.22386 10 5.5V10.5C10 10.7761 10.2239 11 10.5 11H12.5C12.7761 11 13 10.7761 13 10.5V5.5C13 5.22386 12.7761 5 12.5 5H10.5Z"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="11.25"
|
||||||
|
y="2.25"
|
||||||
|
width="0.5"
|
||||||
|
height="11.5"
|
||||||
|
rx="0.25"
|
||||||
|
stroke="black"
|
||||||
|
stroke-opacity="0.3"
|
||||||
|
style="stroke: black; stroke-opacity: 0.3"
|
||||||
|
stroke-width="0.5"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="4.25"
|
||||||
|
y="2.25"
|
||||||
|
width="0.5"
|
||||||
|
height="11.5"
|
||||||
|
rx="0.25"
|
||||||
|
stroke="black"
|
||||||
|
stroke-opacity="0.3"
|
||||||
|
style="stroke: black; stroke-opacity: 0.3"
|
||||||
|
stroke-width="0.5"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,5 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path d="M64 1024H0V0h64v1024z m384-160H128V160h320v704z m448 0H576V160h320v704z m128 160h-64V0h64v1024z"></path>
|
<path
|
||||||
|
d="M6 4C6.55228 4 7 4.44772 7 5V11C7 11.5523 6.55228 12 6 12H4C3.44772 12 3 11.5523 3 11V5C3 4.44772 3.44772 4 4 4H6ZM4.5 5C4.22386 5 4 5.22386 4 5.5V10.5C4 10.7761 4.22386 11 4.5 11H5.5C5.77614 11 6 10.7761 6 10.5V5.5C6 5.22386 5.77614 5 5.5 5H4.5Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12 4C12.5523 4 13 4.44772 13 5V11C13 11.5523 12.5523 12 12 12H10C9.44772 12 9 11.5523 9 11V5C9 4.44772 9.44772 4 10 4H12ZM10.5 5C10.2239 5 10 5.22386 10 5.5V10.5C10 10.7761 10.2239 11 10.5 11H11.5C11.7761 11 12 10.7761 12 10.5V5.5C12 5.22386 11.7761 5 11.5 5H10.5Z"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="14.25"
|
||||||
|
y="2.25"
|
||||||
|
width="0.5"
|
||||||
|
height="11.5"
|
||||||
|
rx="0.25"
|
||||||
|
stroke="black"
|
||||||
|
stroke-opacity="0.3"
|
||||||
|
stroke-width="0.5"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="1.25"
|
||||||
|
y="2.25"
|
||||||
|
width="0.5"
|
||||||
|
height="11.5"
|
||||||
|
rx="0.25"
|
||||||
|
stroke="black"
|
||||||
|
stroke-opacity="0.3"
|
||||||
|
style="stroke: black; stroke-opacity: 0.3"
|
||||||
|
stroke-width="0.5"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user