Compare commits

..

76 Commits

Author SHA1 Message Date
roymondchen
b564c9e4aa fix(editor): 切换动作类型时清空关联配置字段 2026-07-24 17:17:55 +08:00
roymondchen
b5abf31066 fix(editor): 修复 collectIdle 批次结算与中断时 Promise 挂起问题
引入批次级 Promise 结算,避免快速连续触发或 clearIdleTasks 后 collecting 卡死。
collectIdle 返回 boolean 表示是否完整完成,initService 据此跳过被中断的 stage 更新。
2026-07-24 17:17:55 +08:00
roymondchen
bf8df74864 fix(form): 完善 mountFormInstance 清理逻辑,避免临时表单实例泄漏
补充 mount 全流程异常清理、非正数 timeout 兜底,并支持 AbortSignal 主动中断 debug 弹层。
2026-07-24 16:51:42 +08:00
roymondchen
a37e0487c6 chore: update lockfile v1.8.0-beta.16 2026-07-23 22:12:42 +08:00
roymondchen
329cd947e1 chore: release v1.8.0-beta.16 2026-07-23 22:11:18 +08:00
roymondchen
0e1986c819 feat: 参考建议可选值最多展示个数由 5 调整为 20 2026-07-23 22:07:51 +08:00
roymondchen
d0a22315c0 refactor(form): 抽离表单校验逻辑至 validateForm 并复用于 props 面板
- 将 editor 的 validatePropsForm 逻辑迁移到 @tmagic/form 的 validateForm
- FormPanel 源码保存改为复用 validateForm 静默校验
- 补充 form 校验与 applyExtendState 相关单元测试
2026-07-23 22:02:43 +08:00
roymondchen
fe7815cf3d chore: update lockfile v1.8.0-beta.15 2026-07-23 20:25:53 +08:00
roymondchen
bf9078a458 chore: release v1.8.0-beta.15 2026-07-23 20:24:47 +08:00
roymondchen
3b9d5da0bc feat(form,editor): submitForm/validateForm 静默挂载时注入静默标记,vs-code 字段跳过 monaco 渲染 2026-07-23 20:22:47 +08:00
roymondchen
2e031cc134 feat(design): 优化 FormItem 和 colorPicker 样式 2026-07-23 19:26:54 +08:00
roymondchen
1f389ff9fd feat(form): text 类型校验允许数字值,兼容数据源字段配置 2026-07-23 19:04:04 +08:00
roymondchen
fe9e754b50 feat(editor): 数据源字段选择器校验提示细化并支持 fieldConfig 类型校验 2026-07-23 19:03:28 +08:00
roymondchen
745f449396 feat: 新增 page-structure-change 事件,支持页面增删统一通知 2026-07-23 19:00:12 +08:00
manmanyu
48abab7051 feat(form,design): 一行多个表单默认右靠齐label 2026-07-23 08:44:10 +00:00
roymondchen
11bf037054 fix: 跳过无字段名称配置的类型校验 2026-07-23 16:32:07 +08:00
roymondchen
97e0cac44a fix(core): 为背景图片 URL 添加引号 2026-07-23 15:40:58 +08:00
roymondchen
cde0805292 fix(editor): 使用属性面板中的源码删除样式无效 2026-07-23 15:15:19 +08:00
roymondchen
6919a0a0b1 fix(editor): 右键菜单动态变化后修复菜单位置防止截断 2026-07-23 15:13:26 +08:00
roymondchen
d9d2f8f04c chore: update lockfile v1.8.0-beta.14 2026-07-23 12:13:40 +08:00
roymondchen
3fbce585a4 chore: release v1.8.0-beta.14 2026-07-23 12:12:18 +08:00
roymondchen
09eecd5bed fix(stage): 修复选中组件时 stage 外层滚动 2026-07-23 12:09:13 +08:00
roymondchen
ee5247ca94 feat: playground 支持主题切换 2026-07-22 19:03:05 +08:00
roymondchen
8699dc12af fix: 调整编辑器配置项展示 2026-07-22 18:39:10 +08:00
manmanyu
b13cd3425b feat(design,editor): 新增magic-admin主题样式,优化组件表单配置布局样式 2026-07-22 09:31:26 +00:00
roymondchen
89689bb9fd fix(editor): 保留历史操作来源并补发插入事件 2026-07-22 17:17:42 +08:00
roymondchen
0feada8d1f fix(form): 统一 extendState 合并逻辑 2026-07-22 16:51:07 +08:00
roymondchen
d27710c464 chore: update lockfile v1.8.0-beta.13 2026-07-22 11:35:33 +08:00
roymondchen
3e7980c0f5 chore: release v1.8.0-beta.13 2026-07-22 11:34:14 +08:00
roymondchen
78b34a452b fix(form): 修复隐藏字段校验处理 2026-07-21 21:19:28 +08:00
roymondchen
6f3f321736 feat(editor): 抽离表单视图与对比逻辑 2026-07-21 21:18:29 +08:00
roymondchen
6cdf54a4a6 fix(form): typeMatch 使用 toLine 统一类型转换并处理异步 type/defaultValue
替换局部 normalizeType 为 @tmagic/utils 的 toLine,新增 isPromise 守卫。

在 type 或 defaultValue 为 Promise 时跳过校验并回退示例值,补充对应单元测试。
2026-07-21 16:20:23 +08:00
roymondchen
08ac9038bf chore: update lockfile v1.8.0-beta.12 2026-07-20 20:15:26 +08:00
roymondchen
58b151af64 chore: release v1.8.0-beta.12 2026-07-20 20:11:33 +08:00
roymondchen
e5e0da3785 fix(editor): 修正 StyleSetter 字段 prop 取值逻辑
将 `prop ? `${prop}.${name}` : name` 简化为 `prop || name`,
避免嵌套路径拼接导致的表单校验 prop 错误。
2026-07-20 20:02:42 +08:00
roymondchen
ef66e8a598 fix(form,editor): 完善表单校验与样式属性匹配 2026-07-20 19:09:55 +08:00
roymondchen
932974f0fa style: lint-fix 自动修复格式问题 2026-07-20 11:20:15 +08:00
roymondchen
edb44da255 chore: 清理 vue-demi/composition-api 传递依赖,重构 ActionButton 去重并修复 Popconfirm 兼容 2026-07-20 11:18:41 +08:00
roymondchen
7459224009 chore(deps): 升级开发依赖版本并同步 workspace catalog 与 lockfile 2026-07-16 19:48:07 +08:00
roymondchen
d07e48bb10 feat(form,editor): 新增 validateForm 校验能力并接入源码保存校验联动 2026-07-16 19:37:46 +08:00
roymondchen
4a15da2108 fix(editor): 修复 Monaco 编辑器卸载时容器失效与 Canceled 未处理异常 2026-07-16 19:35:43 +08:00
roymondchen
ce43fface8 refactor(editor): 完善内置 typeMatch 校验规则并规范文件名
- 重命名 typeMatchRules.ts 为 type-match-rules.ts
- 为 event-select、display-conds 等字段补充结构校验规则
- advancedTabConfig 的 code-select 字段增加 hookType 校验
- 修复 update 未携带 invalidInfo 时清除对应节点全部来源错误
- 将 code-select/display-conds 字段 typeMatch 触发时机调整为 change
2026-07-14 20:28:03 +08:00
roymondchen
0e4669261f feat(editor): 新增属性配置表单校验联动能力
- 新增 enablePropsFormValidate 开关
- 校验失败时仍更新节点并记录错误
- editorService 增加 invalidNodeIds 状态及错误读写方法
- 新增 invalid-node-change 事件,错误随历史快照还原
- 组件树节点标红并展示错误图标
- playground 保存前拦截校验错误组件
- 补充 API 与进阶指南文档
2026-07-14 19:51:21 +08:00
roymondchen
1c67b5e77b feat(editor): 注册编辑器字段内置 typeMatch 校验规则并补充文档
- 新增 typeMatchRules.ts,为编辑器自定义字段注册内置 typeMatch 校验规则
- 抽取 event.ts 事件工具函数
- form typeMatch 适配 TDesign 校验器签名,错误消息补充实际值
- rules.md 新增「Editor 字段内置规则」章节,16 个字段文档补充校验说明
2026-07-14 17:15:28 +08:00
roymondchen
bbe73aae64 feat(editor): 扩展 customContentMenu 支持 getTarget 获取右键目标
为数据源与代码块面板的右键菜单传入 getTarget 回调,便于业务在自定义菜单 handler 中读取当前节点 id 与原始数据。
2026-07-14 15:55:19 +08:00
roymondchen
734e6a8ee6 feat(form): 支持 rules.typeMatch 按字段 type 校验值类型与选项匹配
新增可扩展的 typeMatch 校验能力,兼容 async-validator 规则体系,并补充文档与单元测试。
2026-07-13 15:52:56 +08:00
roymondchen
f7d737a0c0 docs: 补充新增修改代码测试覆盖率不低于85%的开发约定
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 14:43:48 +08:00
roymondchen
42d45bed18 fix(form): radio group disabled不生效 2026-07-10 16:40:42 +08:00
roymondchen
de7a4f73ad style(table): 为 sub-actions 类名添加 tmagic-table 命名空间并迁移样式到主题文件 2026-07-07 19:45:38 +08:00
roymondchen
605f07c8fe feat(editor): 支持自定义历史对比/回滚弹窗内表单组件的尺寸 2026-07-06 17:17:39 +08:00
roymondchen
90492ad3ea fix(adapters): tag 非法 type 回退为默认主题
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 16:48:45 +08:00
roymondchen
b183e71f64 feat(table): 抽取 ActionPopconfirm 并优化 sub-actions 展开交互
将 Popconfirm 逻辑抽离为独立组件,ActionButton 内置 tooltip 支持,sub-actions 触发按钮改用箭头图标切换展开状态。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 16:47:16 +08:00
roymondchen
78a1259665 feat(table): 操作列支持 sub-actions 更多菜单并抽取 ActionButton 组件 2026-07-06 15:08:24 +08:00
roymondchen
4a893d35df fix(editor): 为 FloatingBox 拖拽遮罩添加失焦等兜底清理,避免残留导致编辑器无法点击 2026-07-06 11:34:41 +08:00
roymondchen
3c21f45e88 feat(editor): 支持通过 uiService 配置列布局最小宽度
将左侧、中间、右侧列的最小宽度从硬编码常量改为 UiState 配置项,便于业务侧自定义编辑器布局约束。
2026-07-03 19:44:13 +08:00
roymondchen
aa71fe9ddd fix(core): 在 initNode 前校验 config.items 为数组
避免 config.items 非数组时 forEach 抛出运行时错误。
2026-07-03 11:46:09 +08:00
roymondchen
9e8272b521 feat(table): 为操作列新增 Popconfirm 二次确认能力
支持 popconfirm、confirmText、popconfirmWidth 配置,并扩展 Popconfirm 组件的 width 与 teleported 属性。
2026-07-03 11:39:37 +08:00
roymondchen
9aa251ce57 fix(editor): 延迟 FloatingBox 拖拽遮罩到实际位移时显示,修复关闭按钮点击失效 2026-07-02 20:52:17 +08:00
roymondchen
3b9fb714e5 refactor(editor): 解耦 FloatingBox 的 uiService 依赖并改为 props 传入
- FloatingBox 不再强制依赖 uiService
- frameworkWidth 默认回退到视窗宽度
- 新增 initialStyle prop 支持外部设置初始样式
- 各调用方显式传入 frameworkWidth 以保留右边界收敛行为
2026-07-02 20:22:24 +08:00
roymondchen
284be0d276 fix(editor): 修复 FloatingBox 拖动时鼠标进入 iframe 区域事件丢失
- 拖拽/缩放开始时插入全屏透明遮罩盖住 iframe,结束时移除,避免事件被 iframe 吞掉
- 修正拖拽标题时 z-index 竞态导致遮罩被浮窗盖住的问题
- 将 body 内边距从 scss 抽取为 bodyStyle 透传,复用方按需自定义
2026-07-02 20:01:45 +08:00
roymondchen
c57ef89715 chore: update lockfile v1.8.0-beta.11 2026-06-29 21:27:41 +08:00
roymondchen
1ef3ea9eb9 chore: release v1.8.0-beta.11 2026-06-29 21:26:37 +08:00
roymondchen
83f4e52845 feat(editor): support custom width and initial diff mode for history diff dialog
为历史差异/回滚确认弹窗新增可配置宽度(dialogWidth / width 透传至 TMagicDialog),
并支持通过 payload.mode 指定打开时的初始对比模式,不可用时回退自动推断。
2026-06-29 20:23:19 +08:00
roymondchen
4330738b9f feat(form): add debug mode to submitForm
Allow submitForm to render a visible modal for manual validation and submission during debugging.
2026-06-29 15:49:33 +08:00
roymondchen
e59c3d2dcd chore: update lockfile v1.8.0-beta.10 2026-06-26 19:53:58 +08:00
roymondchen
55135fc063 chore: release v1.8.0-beta.10 2026-06-26 19:52:59 +08:00
roymondchen
af197363cf fix(vue-runtime-help): handle remove when root is not initialized
When root is empty but RemoveData carries app config, update root instead of throwing.
2026-06-26 19:51:44 +08:00
roymondchen
7bbb1f24c0 feat(editor): add unified change events 2026-06-26 19:48:38 +08:00
roymondchen
f7afed66aa feat(editor): refine history diff defaults 2026-06-26 17:08:39 +08:00
roymondchen
fd359e493f feat(form): support useFieldTextInError option
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 16:38:40 +08:00
roymondchen
5e661d0958 refactor(editor): 历史对比 services 改为显式传入并可选
CompareForm / HistoryDiffDialog / useHistoryRevert 不再通过 useServices()
默认 inject 获取 services,改为由调用方显式传入并设为可选,避免在
createApp 动态挂载游离弹窗场景下 inject 链不可靠。同步更新调用点、
测试与文档示例签名。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 15:00:08 +08:00
roymondchen
5ea992f5f7 chore: update lockfile v1.8.0-beta.9 2026-06-25 18:17:38 +08:00
roymondchen
4c81f22485 chore: release v1.8.0-beta.9 2026-06-25 18:13:58 +08:00
roymondchen
40acb5b34c feat: 导出 componentListService 和 keybindingService 2026-06-25 17:32:52 +08:00
roymondchen
ce65b18dbb feat(editor): 优化历史记录对比与回滚流程 2026-06-25 16:30:01 +08:00
roymondchen
1c936ff439 chore: update deps 2026-06-24 14:43:11 +08:00
290 changed files with 15108 additions and 3895 deletions

1
.gitignore vendored
View File

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

View File

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

View File

@ -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)

View File

@ -111,6 +111,10 @@ export default defineConfig({
text: '@tmagic/form',
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: '表单对比',
items: [

View File

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

View File

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

View File

@ -48,7 +48,7 @@ DSL 操作方法(`add` / `remove` / `update` 等)默认返回操作结果(
## 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`
@ -71,6 +71,8 @@ DSL 操作方法(`add` / `remove` / `update` 等)默认返回操作结果(
'modifiedNodeIds': 当前页面所有改动过的节点id
'invalidNodeIds': 校验失败的节点错误信息(`Map<Id, NodeInvalidInfo>`),供组件树标红提示与保存拦截读取
'pageLength': 所以页面个数
'pageFragmentLength': 页面片个数
@ -93,7 +95,7 @@ const node = editorService.get("node");
## 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`
- **详情:**
@ -510,6 +512,10 @@ editorService.highlight("text_123");
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
- `{Object}` invalidInfo 启用 [enablePropsFormValidate](./props.md#enablepropsformvalidate) 时,属性面板提交携带的校验错误信息,在写入历史记录之前落库,使历史快照与本次变更对齐
- `{Id}` id 节点 id
- `{'props' | 'style'}` source 错误来源:属性表单 / 样式表单
- `{string}` error 错误文案(可为含 `<br>` 的 HTML为空时表示清除该来源的错误记录
::: details 查看 ChangeRecord 类型定义
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
@ -927,6 +933,89 @@ await editorService.revertPageStepById(historyIds);
重置当前记录的修改过的节点id记录通常用于保存之后
## setInvalidNode
- **参数:**
- `{Id}` id 节点 id
- `{'props' | 'style'}` source 错误来源:属性表单(`props`/ 样式表单(`style`
- `{string}` message 错误文案(可能为包含 `<br>` 的 HTML
- **详情:**
记录(或覆盖)某个节点在指定来源上的校验错误信息,写入 `invalidNodeIds` 状态并发出 [invalid-node-change](#invalid-node-change) 事件。
属性表单与样式表单是两个独立的 `FormPanel` 且均指向同一节点,故以来源为键分别保存,避免某个面板校验通过时误清另一个面板记录的错误。节点视为存在错误当且仅当任一来源存在非空文本。
- **示例:**
```js
import { editorService } from "@tmagic/editor";
// 标记 text_123 的属性表单校验错误
editorService.setInvalidNode("text_123", "props", "标题不能为空");
```
## deleteInvalidNode
- **参数:**
- `{Id}` id 节点 id
- `{'props' | 'style'}` source 可选;指定来源则仅删除该来源错误,不传则删除该节点全部来源的错误
- **详情:**
删除节点的校验错误记录。仅当该来源被清空且另一来源也无错误时,节点整体错误记录才会被移除;随后发出 [invalid-node-change](#invalid-node-change) 事件。
## getInvalidNodeIds
- **返回:**
- {`Map<Id, NodeInvalidInfo>`} 当前存在校验错误的节点错误 Mapkey 为节点 id
- **详情:**
获取当前存在校验错误的节点错误 Map供组件树标红提示与保存拦截读取。
::: details 查看 NodeInvalidInfo 及关联类型定义
<<< @/../packages/editor/src/type.ts#NodeInvalidInfo{ts}
<<< @/../packages/editor/src/type.ts#NodeInvalidSource{ts}
<<< @/../packages/schema/src/index.ts#Id{ts}
:::
- **示例:**
```js
import { editorService } from "@tmagic/editor";
// 保存前检查是否存在校验错误的组件
const invalidNodeIds = editorService.getInvalidNodeIds();
if (invalidNodeIds.size > 0) {
const names = [...invalidNodeIds.keys()].map((id) => {
const node = editorService.getNodeById(id);
return node?.name ? `${node.name}(${id})` : `${id}`;
});
console.warn(`以下组件存在配置校验错误,请修复后再保存:${names.join("、")}`);
}
```
## getInvalidNodeInfo
- **参数:**
- `{Id}` id 节点 id
- **返回:**
- {`NodeInvalidInfo` | undefined} 指定节点的校验错误信息(含 `props` / `style` 来源的错误文案)
- **详情:**
获取指定节点的校验错误信息。
## resetInvalidNodeId
- **详情:**
清空全部校验错误记录(即 `invalidNodeIds` 状态),随后发出 [invalid-node-change](#invalid-node-change) 事件。
## resetState
- **详情:**

View File

@ -1265,7 +1265,7 @@ const guidesOptions = {
## disabledShowSrc
- **详情:**
禁用属性配置面板右下角"显示源码"的按钮
该按钮可以查看和编辑组件的 JSON 配置
@ -1282,6 +1282,45 @@ const guidesOptions = {
</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
- **详情:**
@ -1365,13 +1404,34 @@ const guidesOptions = {
- **详情:**
用于自定义组件树与画布的右键菜单
用于自定义组件树(图层)、画布、数据源、代码块的右键菜单
该函数会在显示右键菜单前被调用,接收默认菜单项作为参数,返回最终显示的菜单项
该函数会在显示右键菜单前被调用,接收默认菜单项与菜单类型作为参数,返回最终显示的菜单项
第二个参数 `type` 用于区分菜单来源:`'layer'`(图层)、`'viewer'`(画布)、`'data-source'`(数据源)、`'code-block'`(代码块)
第三个参数 `getTarget` 仅在**数据源**与**代码块**面板下传入,用于读取当前右键目标(节点 `id` 与原始数据);图层 / 画布不会传入该参数
- **默认值:** `(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>
<script setup>
const customContentMenu = (menus, { node }) => {
// 为特定类型的组件添加自定义菜单
if (node?.type === 'container') {
const customContentMenu = (menus, type, getTarget) => {
// 为数据源右键菜单追加自定义菜单
if (type === 'data-source') {
menus.push({
type: 'button',
text: '清空容器',
text: '查看详情',
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>
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,6 +24,26 @@ type为'select'
</template>
</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="[{

View File

@ -82,6 +82,21 @@ Input输入框的type为'text', 是type的默认值所以可以不配置
</template>
</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="[{
@ -124,6 +139,7 @@ Input输入框的type为'text', 是type的默认值所以可以不配置
| tooltip | 输入时显示内容 | string / `ToolTipConfigType` | — | — |
| trim | 是否去掉首尾空格 | boolean | — | false |
| filter | 过滤值 | string / Function | number | - |
| rules | 表单验证规则 | `Rule[]` | — | — |
| prepend | 前置内容 | string | — | - |
| append | 后置内容 | string / Object | — | - |
| onChange | 值变化时触发的函数 | `OnChangeHandler` | — | - |
@ -140,6 +156,8 @@ Input输入框的type为'text', 是type的默认值所以可以不配置
<<< @/../packages/form-schema/src/base.ts#FormValue{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
View 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}
:::

View File

@ -82,7 +82,7 @@ const {
onPageDiff,
onDataSourceDiff,
onCodeBlockDiff,
} = useHistoryRevert(editorRef.value);
} = useHistoryRevert({}, editorRef.value);
// 回滚:可差异步骤弹出差异确认弹窗、其余步骤弹普通二次确认框;用户点「确定」后回滚第 index 步,
// 命中前置校验或用户取消时不执行,返回 null
@ -102,6 +102,7 @@ onCodeBlockDiff(id, index);
| --- | --- | --- |
| `appContext` | 否 | 父级应用上下文,用于让动态挂载的差异确认弹窗继承全局组件 / 指令 / provide / 插件Element Plus、`@tmagic/form` 字段组件等)。在组件 `setup` 中调用时会自动取当前组件的 `appContext`,无需手动传;仅当在组件 setup 之外调用时才需显式传入(如 `editorApp._context`)。 |
| `extendState` | 否 | 透传给差异确认弹窗的 `extendState`(同 Editor 的 [`extendFormState`](#自定义对比判断)),使对比表单中依赖业务上下文的 `display` / `disabled``filterFunction` 正常工作。 |
| `dialogWidth` | 否 | 内置页面 / 数据源 / 代码块的差异 / 回滚确认弹窗默认宽度(透传给 `TMagicDialog``width`),如 `'1200px'` / `'80%'`。缺省时使用弹窗内置默认宽度(`900px`)。业务自有历史可在 `viewDiff` / `confirmAndRevert` 调用时通过各自入参的 `width` 单独覆盖。 |
> 若只需要无确认、无校验的静默回滚,直接用上面的 `editorService.revertPageStep` 等即可,无需 `useHistoryRevert`

View 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 中的失效节点错误记录,避免残留误报;删除节点时也会同步清理其子树相关的错误记录。

View File

@ -2,7 +2,7 @@
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");
```
也可在安装时传入自定义 `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 的样式文件需要单独引入。

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>

View File

@ -1,5 +1,5 @@
<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">
<slot name="header" class="header"></slot>
</template>
@ -14,6 +14,7 @@
import { computed } from 'vue';
import { getDesignConfig } from './config';
import { isGlobalFlat } from './index';
import type { CardProps } from './types';
defineOptions({
@ -26,5 +27,14 @@ const ui = getDesignConfig('components')?.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>

View File

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

View File

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

View File

@ -1,36 +1,65 @@
<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>
<slot name="label"></slot>
<slot name="label"> </slot>
</template>
<template #default>
<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>
</component>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { computed, inject } from 'vue';
import Alert from './Alert.vue';
import { getDesignConfig } from './config';
import { stripValidateSuggestion } from './formValidateMessage';
import { isGlobalFlat } from './index';
import type { FormItemProps } from './types';
defineOptions({
name: 'TMFormItem',
});
const props = defineProps<FormItemProps>();
const props = defineProps<FormItemProps & { theme?: string }>();
const ui = getDesignConfig('components')?.formItem;
const uiComponent = ui?.component || 'el-form-item';
const adapterType = getDesignConfig('adapterType');
const formInline = inject<boolean>('formInline', false);
const formInRow = inject<boolean>('isInRow', false);
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;
});
/**
* 校验错误文案中修改建议仅用于错误汇总展示
* form-item 行内错误只展示主错误描述不展示修改建议
*/
const resolveErrorText = (error?: string) => stripValidateSuggestion(error);
</script>

View File

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

View 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];

View File

@ -8,6 +8,8 @@ import './theme/index.scss';
export * from './types';
export * from './config';
export * from './theme';
export * from './formValidateMessage';
export { default as TMagicAutocomplete } from './Autocomplete.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 {
install(app: App, options: DesignPluginOptions) {
tMagicMessage =
@ -128,6 +134,8 @@ export default {
useZIndex = options.useZIndex;
}
isGlobalFlat.value = options.flat ?? false;
if (options.adapterType && globalThis.document?.documentElement) {
globalThis.document.documentElement.classList.add(`tmagic-adapter-${options.adapterType}`);
}

View 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 proxycomputed
* `toRefs`
*
* `prop.theme`
*
* @example
* const props = defineProps<{ theme?: string }>();
* const theme = useTheme(props);
* // theme.value -> 'magic-admin' 或 ''
*/
export const useTheme = <T extends { theme?: string }>(prop: T): ComputedRef<string> => {
const injected = inject(M_THEME_KEY, null);
return computed(() => prop.theme || injected?.value || '');
};

View File

@ -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;
}
}
}

View 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;
}

View File

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

View 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;
}
}
}
}
}

View File

@ -24,6 +24,17 @@ export interface BadgeProps {
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 {
type?: string;
size?: FieldSize;
@ -39,6 +50,11 @@ export interface CardProps {
bodyStyle?: Record<string, any>;
shadow?: string;
header?: string;
/**
* / / / / header 线 / header body
* 使TMagicCard prop 使
*/
flat?: boolean;
}
export interface CascaderProps {
@ -97,6 +113,7 @@ export interface ColorPickerProps {
disabled?: boolean;
showAlpha?: boolean;
size?: FieldSize;
flat?: boolean;
}
export interface DatePickerProps {
@ -185,7 +202,9 @@ export interface FormItemProps {
labelWidth?: string | number;
rules?: any;
extra?: string;
extraTips?: string;
labelPosition?: 'top' | 'left' | 'right';
text?: string;
}
export interface InputProps {
@ -234,6 +253,9 @@ export interface PaginationProps {
export interface PopconfirmProps {
title?: string;
width?: string | number;
/** 浮层是否插入到 body默认 true。设为 false 时浮层内联渲染,便于嵌套在 hover 浮层中避免父级因 mouseleave 收起。 */
teleported?: boolean;
placement?:
| 'top'
| 'left'
@ -461,6 +483,11 @@ export interface Components {
props: (props: BadgeProps) => BadgeProps;
};
alert: {
component: DefineComponent<AlertProps, {}, any> | string;
props: (props: AlertProps) => AlertProps;
};
autocomplete: {
component: DefineComponent<AutocompleteProps, {}, any> | string;
props: (props: AutocompleteProps) => AutocompleteProps;
@ -723,6 +750,7 @@ export interface Components {
}
export interface DesignPluginOptions {
flat?: boolean;
adapterType?: string;
message?: TMagicMessage;
messageBox?: TMagicMessageBox;

View File

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

View File

@ -4,6 +4,7 @@
:page-bar-sort-options="pageBarSortOptions"
:page-filter-function="pageFilterFunction"
:hide-sidebar="hideSidebar"
:theme="theme"
>
<template #header>
<slot name="header"></slot>
@ -135,9 +136,10 @@
<script lang="ts" setup>
import { EventEmitter } from 'events';
import { provide } from 'vue';
import { computed, provide, ref } from 'vue';
import type { MApp } from '@tmagic/core';
import { M_THEME_KEY } from '@tmagic/design';
import Framework from './layouts/Framework.vue';
import TMagicNavMenu from './layouts/NavMenu.vue';
@ -158,7 +160,7 @@ import stageOverlayService from './services/stageOverlay';
import storageService from './services/storage';
import uiService from './services/ui';
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 type { TreeNodeData } from './type';
import type { EditorSlots, EventBus, Services, StageOptions } from './type';
@ -227,10 +229,14 @@ const stageOptions: StageOptions = {
stageOverlayService.set('stageOptions', stageOptions);
const propsPanelRef = ref<InstanceType<typeof FormPanel> | null>(null);
provide('services', services);
provide('codeOptions', props.codeOptions);
provide('stageOptions', stageOptions);
/** 是否启用「属性配置表单校验」联动能力,供 PropsPanel / FormPanel 判断校验失败时是否仍更新节点并记录错误 */
provide(ENABLE_PROPS_FORM_VALIDATE, props.enablePropsFormValidate ?? false);
/**
* 把顶层 `extendFormState` 提供给非 PropsPanel 链路上的组件使用例如历史差异对话框 HistoryDiffDialog
* 内部的 CompareForm这样所有依赖业务上下文的表单 filterFunction 都能拿到一致的扩展状态
@ -238,6 +244,20 @@ provide('stageOptions', stageOptions);
*/
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
* markRaw component 形式渲染无法直接通过 props 透传业务方可借此在历史记录
@ -247,10 +267,22 @@ provide('historyListExtraTabs', props.historyListExtraTabs);
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>) => {
propsPanelRef.value = e;
emit('props-panel-mounted', e);
};
const propsPanelUnmountedHandler = () => {
propsPanelRef.value = null;
emit('props-panel-unmounted');
};

View File

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

View File

@ -10,104 +10,45 @@
:is-compare="true"
:disabled="true"
:label-width="labelWidth"
:extend-state="extendState"
:extend-state="mergedExtendState"
:show-diff="showDiff"
:self-diff-field-types="selfDiffFieldTypes"
:size="size"
></MForm>
</div>
</template>
<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 { type CodeBlockContent, type DataSourceSchema, HookType, type MNode } from '@tmagic/core';
import { type FormConfig, type FormState, type FormValue, MForm } from '@tmagic/form';
import { type CodeBlockContent, HookType } from '@tmagic/core';
import { type FormConfig, type FormValue, MForm } from '@tmagic/form';
import { useServices } from '@editor/hooks/use-services';
import type { CompareCategory, CompareFormLoadConfig, Services } from '@editor/type';
import { getCodeBlockFormConfig } from '@editor/utils/code-block';
import { useCompareForm } from '@editor/hooks/use-compare-form';
import type { CompareFormBaseProps } from '@editor/type';
defineOptions({
name: 'MEditorCompareForm',
});
const props = withDefaults(
defineProps<{
/** 当前值(修改后的值) */
value: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
/** 用于对比的旧值(修改前的值) */
lastValue?: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
/**
* 类型说明
* - `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;
}>(),
defineProps<
CompareFormBaseProps & {
/** 用于对比的旧值(修改前的值) */
lastValue?: CompareFormBaseProps['value'];
/** 需要走 self diff 的字段类型(例如 mod-cond。 */
selfDiffFieldTypes?: string[];
}
>(),
{
category: 'node',
labelWidth: '120px',
services: () => useServices(),
},
);
const { propsService, dataSourceService, codeBlockService, editorService } = props.services;
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 { config, currentValues, wrapperStyle, mergedExtendState, loadConfig, formRef, normalizeCodeBlockValue } =
useCompareForm(props);
const lastValuesProcessed = computed<FormValue>(() => {
if (props.category === 'code-block') {
@ -116,18 +57,6 @@ const lastValuesProcessed = computed<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` 字段在历史数据中存在两种"语义为空"的形态
* - 字符串 `''`旧数据 / 用户从未配置过钩子
@ -156,104 +85,6 @@ const showDiff = ({ curValue, lastValue, config }: { curValue: any; lastValue: a
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<{
form: ShallowRef<InstanceType<typeof MForm> | null>;
config: Ref<FormConfig>;

View File

@ -2,8 +2,8 @@
<transition name="fade">
<div
v-show="visible"
class="magic-editor-content-menu"
ref="menu"
:class="['magic-editor-content-menu', themeClass]"
:style="menuStyle"
@mouseenter="mouseenterHandler()"
@contextmenu.prevent
@ -39,7 +39,7 @@
<script lang="ts" setup>
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';
@ -73,6 +73,14 @@ const menuEl = useTemplateRef<HTMLDivElement>('menu');
const buttonRefs = useTemplateRef<InstanceType<typeof ToolButton>[]>('buttons');
const subMenuRef = useTemplateRef<any>('subMenu');
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 zIndex = useZIndex();
const curZIndex = ref<number>(0);
@ -120,20 +128,42 @@ const outsideClickHideHandler = (e: MouseEvent) => {
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;
if (menuHeight + e.clientY > document.body.clientHeight) {
top = document.body.clientHeight - menuHeight;
const menuHeight = menu.clientHeight;
const menuWidth = menu.clientWidth;
let { top, left } = menuPosition.value;
if (top + menuHeight > document.body.clientHeight) {
top = Math.max(0, document.body.clientHeight - menuHeight);
}
if (left + menuWidth > document.body.clientWidth) {
left = Math.max(0, document.body.clientWidth - menuWidth);
}
if (top !== menuPosition.value.top || left !== menuPosition.value.left) {
menuPosition.value = { top, left };
}
};
const setPosition = (e: { clientY: number; clientX: number }) => {
menuPosition.value = {
top,
top: e.clientY,
left: e.clientX,
};
fixPosition();
};
//
const resizeObserver = new ResizeObserver(() => {
fixPosition();
});
const show = (e?: { clientY: number; clientX: number }) => {
visible.value = true;
@ -178,12 +208,18 @@ const mouseenterHandler = () => {
};
onMounted(() => {
if (menuEl.value) {
resizeObserver.observe(menuEl.value);
}
if (props.isSubMenu) return;
globalThis.addEventListener('mousedown', outsideClickHideHandler, true);
});
onBeforeUnmount(() => {
resizeObserver.disconnect();
if (props.isSubMenu) return;
globalThis.removeEventListener('mousedown', outsideClickHideHandler, true);

View File

@ -1,6 +1,12 @@
<template>
<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">
<slot name="title">
<span>{{ title }}</span>
@ -9,7 +15,7 @@
<TMagicButton link size="small" @click="closeHandler"><MIcon :icon="Close"></MIcon></TMagicButton>
</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>
</div>
</div>
@ -17,14 +23,13 @@
</template>
<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 VanillaMoveable from 'moveable';
import { TMagicButton, useZIndex } from '@tmagic/design';
import { TMagicButton, useThemeClass, useZIndex } from '@tmagic/design';
import MIcon from '@editor/components/Icon.vue';
import { useServices } from '@editor/hooks/use-services';
interface Position {
left: number;
@ -39,17 +44,30 @@ const props = withDefaults(
defineProps<{
position?: Position;
title?: string;
bodyStyle?: CSSProperties;
/** 浮窗初始样式,会与内部计算样式合并,外部传入优先 */
initialStyle?: CSSProperties;
/** 用于约束浮窗 left 的容器宽度,传入时按宽度收敛 left避免超出右边界默认取视窗宽度 */
frameworkWidth?: number;
beforeClose?: (_done: (_cancel?: boolean) => void) => void;
}>(),
{
title: '',
position: () => ({ left: 0, top: 0 }),
initialStyle: () => ({}),
frameworkWidth: 0,
},
);
const targetEl = useTemplateRef<HTMLDivElement>('target');
const titleEl = useTemplateRef<HTMLDivElement>('title');
/**
* 主题修饰类来自最近的 `<MEditor>` / `<MForm>` 祖先 provide
* 挂在 `Teleport` 出去的浮动面板根节点上让主题级 CSS 变量在 portal 节点上也能命中
*/
const themeClass = useThemeClass();
const zIndex = useZIndex();
const curZIndex = ref<number>(0);
@ -66,12 +84,11 @@ const bodyHeight = computed(() => {
return 'auto';
});
const { uiService } = useServices();
const frameworkWidth = computed(() => uiService.get('frameworkRect').width || 0);
const style = computed(() => {
let { left } = props.position;
if (width.value) {
left = left + width.value > frameworkWidth.value ? frameworkWidth.value - width.value : left;
const frameworkWidth = props.frameworkWidth || globalThis.window?.innerWidth || 0;
if (width.value && frameworkWidth) {
left = left + width.value > frameworkWidth ? frameworkWidth - width.value : left;
}
return {
@ -79,11 +96,60 @@ const style = computed(() => {
top: `${props.position.top}px`,
width: width.value ? `${width.value}px` : 'auto',
height: height.value ? `${height.value}px` : 'auto',
...props.initialStyle,
};
});
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 = () => {
moveable = new VanillaMoveable(globalThis.document.body, {
className: 'm-editor-floating-box-moveable',
@ -101,6 +167,14 @@ const initMoveable = () => {
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) => {
e.target.style.transform = e.transform;
});
@ -115,6 +189,7 @@ const initMoveable = () => {
};
const destroyMoveable = () => {
hideDragMask();
moveable?.destroy();
moveable = null;
};

View File

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

View File

@ -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>

View File

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

View File

@ -1,5 +1,6 @@
<template>
<MagicCodeEditor
v-if="!silentMode"
:height="config.height"
:type="diffMode ? 'diff' : undefined"
:init-values="diffMode ? (lastValues || {})[name] : model[name]"
@ -17,9 +18,9 @@
</template>
<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';
@ -27,6 +28,13 @@ defineOptions({
name: 'MFieldsVsCode',
});
/**
* 静默模式submitForm/validateForm 隐藏挂载下跳过 monaco 渲染
* 校验由 FormItem 针对 model 中的值完成与编辑器实例无关本组件挂载无值副作用
* save 仅由用户操作/编辑器内容变更触发跳过可省去 monaco worker/model 的无谓实例化
*/
const silentMode = inject(FORM_SILENT_MODE_KEY, false);
const emit = defineEmits<{
change: [value: string | any];
}>();

View File

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

View File

@ -147,6 +147,7 @@ const selectConfig: SelectConfig = {
type: 'select',
name: props.name,
disabled: props.disabled,
text: props.config.text,
options: () => {
if (codeDsl.value) {
return map(codeDsl.value, (value, key) => ({

View File

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

View File

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

View File

@ -8,10 +8,12 @@
</div>
<FloatingBox
:body-style="{ padding: '0 16px' }"
v-model:visible="addDialogVisible"
v-model:width="width"
v-model:height="editorHeight"
:title="fieldTitle"
:framework-width="frameworkWidth"
:position="boxPosition"
>
<template #body>
@ -28,10 +30,12 @@
</FloatingBox>
<FloatingBox
:body-style="{ padding: '0 16px' }"
v-model:visible="addFromJsonDialogVisible"
v-model:width="width"
v-model:height="editorHeight"
title="快速添加数据定义"
:framework-width="frameworkWidth"
:position="boxPosition"
>
<template #body>
@ -358,6 +362,7 @@ const { height: editorHeight } = useEditorContentHeight();
const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null));
const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating);
const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
/**
* DataSourceConfigPanel 注入打开数据源详情后需要直接打开的字段路径字段名数组

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@
@click="selectDirection('Left')"
></div>
<div
class="border-icon"
class="border-icon center"
:class="{ active: direction === '', configured: isConfigured('') }"
@click="selectDirection()"
></div>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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';

View File

@ -1,8 +1,24 @@
<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
d="M912.526651 867.741144 555.540144 510.712681l356.986507-357.000833c11.171434-11.18576 11.171434-29.257348 0-40.443108-11.20111-11.18576-29.272697-11.18576-40.444131 0L515.096013 470.267527 158.096203 113.267716c-11.187807-11.159154-29.258371-11.159154-40.444131 0-11.186783 11.186783-11.186783 29.286 0 40.47176L474.623229 510.712681 117.623419 867.741144c-11.159154 11.172457-11.159154 29.216415 0 40.443108 11.18576 11.17348 29.284977 11.17348 40.47176 0l357.000833-357.027439 356.985484 357.027439c11.171434 11.17348 29.243021 11.17348 40.444131 0C923.698085 896.957559 923.725714 878.913601 912.526651 867.741144z"
fill="#5D5D5D"
></path>
d="M0 0.625C0 0.279822 0.248731 0 0.555556 0H9.44444C9.75127 0 10 0.279822 10 0.625C10 0.970178 9.75127 1.25 9.44444 1.25H0.555556C0.248731 1.25 0 0.970178 0 0.625Z"
fill-opacity="0.3"
/>
<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>
</template>

View File

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

View File

@ -1,8 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
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"
fill="#333333"
></path>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<rect x="3" y="3" width="2" height="10" rx="1" />
<rect x="7" y="3" width="2" height="10" rx="1" />
<rect x="11" y="3" width="2" height="10" rx="1" fill-opacity="0.3" />
</svg>
</template>

View File

@ -1,8 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
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"
fill="#333333"
></path>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<rect x="13" y="3" width="2" height="10" rx="1" transform="rotate(90 13 3)" />
<rect x="13" y="7" width="2" height="10" rx="1" transform="rotate(90 13 7)" />
<rect x="13" y="11" width="2" height="10" rx="1" transform="rotate(90 13 11)" fill-opacity="0.3" />
</svg>
</template>

View File

@ -1,7 +1,8 @@
<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="M896 320H128V0h768v320z m0 320H128v-256h768v256z m-128 192l-256 192-256-192 192-0.032V704h128v128h192z"
></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"
/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 12L8 15L5 12L11 12Z" fill-opacity="0.3" />
</svg>
</template>

View File

@ -1,7 +1,8 @@
<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="M896 704H128v320h768V704z m0-320H128v256h768v-256z m-128-192l-256-192-256 192 192 0.032V320h128V192h192z"
></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"
/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 4L8 1L11 4L5 4Z" fill-opacity="0.3" />
</svg>
</template>

View File

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

View File

@ -1,7 +1,8 @@
<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="M704 128v768h320V128H704zM384 128v768h256V128h-256zM192 256l-192 256 192 256 0.032-192H320v-128H192V256z"
></path>
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 fill-rule="evenodd" clip-rule="evenodd" d="M4 11L1 8L4 5L4 11Z" fill-opacity="0.3" />
</svg>
</template>

View File

@ -1,5 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path d="M480 1024V0h64v1024h-64z m128-64V64h320v896H608zM96 960V64h320v896H96z"></path>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<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>
</template>

View File

@ -1,5 +1,18 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path d="M416 160H96v704h320V160z m384 0H480v704h320V160z m128-160h-64v1024h64V0z"></path>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<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>
</template>

View File

@ -1,5 +1,18 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path d="M608 160h320v704H608V160zM224 160h320v704H224V160zM96 0h64v1024H96V0z"></path>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<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>
</template>

View File

@ -1,7 +1,32 @@
<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="M320 864H192v160H128v-160H0V160h128V0h64v160h128v704z m704 0h-128v160h-64v-160h-128V160h128V0h64v160h128v704z"
></path>
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
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>
</template>

View File

@ -1,5 +1,31 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path d="M64 1024H0V0h64v1024z m384-160H128V160h320v704z m448 0H576V160h320v704z m128 160h-64V0h64v1024z"></path>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<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>
</template>

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