Compare commits

..

No commits in common. "master" and "v1.7.7" have entirely different histories.

141 changed files with 5501 additions and 7416 deletions

View File

@ -41,9 +41,6 @@ jobs:
- name: move to dist
run: mv docs/.vitepress/dist/* dist/docs && mv playground/dist/* dist/playground
- name: Bypass Jekyll on GitHub Pages
run: touch dist/.nojekyll
- name: Deploy to GitHub Pages
uses: crazy-max/ghaction-github-pages@v2
with:

View File

@ -1,56 +0,0 @@
# AGENTS.md — TMagic编辑器
> 魔方平台可视化编辑器核心,提供拖拽式活动页面编辑能力。
> 负责人roymondchen | 创建2026-04-03
## 项目概述
TMagic Editor 是魔方平台的可视化编辑器核心库,提供拖拽式组件编辑、配置面板、预览发布等能力。支持 Vue 和 React 双框架 Runtime采用 pnpm monorepo 管理多个核心包。开源项目,同时支持内部业务定制。
**技术栈:** Vue 3, Element Plus, TypeScript, Vite, vitest, VitePress
**主仓库:** `https://git.woa.com/vft-magic/tmagic-editor.git`
**开源仓库:** `https://github.com/Tencent/tmagic-editor.git`
## 架构地图
关键目录:
- `packages/` — 核心编辑器包202 *.vue, 194 *.ts
- `runtime/` — Vue/React Runtime 实现
- `vue-components/` — Vue 组件封装
- `react-components/` — React 组件封装
- `playground/` — 演示 playground
- `docs/` — VitePress 文档100 *.md
- `scripts/` — 构建和发布脚本
- `eslint-config/` — 共享 ESLint 配置
## 开发约定
**分支策略:** dev=dev, test/prod=master
**提交规范:** commitlint + husky`type: 描述`
**禁止事项:**
- 禁止在核心包中引入腾讯内部专有依赖(开源项目)
- 禁止直接修改 CHANGELOG.md应通过 `pnpm changelog` 生成
## 常用命令
pnpm bootstrap # 安装依赖并构建
pnpm pg # 启动 Vue playground
pnpm pg:react # 启动 React playground
pnpm build # 完整构建DTS + 包)
pnpm test # 运行测试
pnpm lint-fix # ESLint 修复
pnpm docs:dev # 启动文档开发
pnpm release # 发版
## 当前状态
**当前里程碑:** {待人工填写}
## 深入阅读
| 文档 | 说明 |
|------|------|
| docs/ | VitePress 文档站 |
| CONTRIBUTING.md | 贡献指南 |
| CHANGELOG.md | 变更日志 |

File diff suppressed because it is too large Load Diff

537
LICENSE
View File

@ -74,6 +74,15 @@ Open Source Software Licensed under the Apache License Version 2.0:
1. typescript
Copyright (c) Microsoft Corporation. All rights reserved.
2. log4js
Copyright 2015 Gareth Jones (with contributions from many other people)
3. reflect-metadata
Copyright (c) Microsoft Corporation. All rights reserved.
4. xlsx
Copyright (C) 2013-present SheetJS
Terms of the Apache License Version 2.0:
--------------------------------------------------------------------
@ -346,8 +355,8 @@ Open Source Software Licensed under the BSD 2-Clause License:
1. @typescript-eslint/parser
Copyright JS Foundation and other contributors, https://js.foundation
2. terser
Copyright 2012-2018 (c) Mihai Bazon <mihai.bazon@gmail.com>
2. uglify-js
Copyright 2012-2019 (c) Mihai Bazon <mihai.bazon@gmail.com>
Terms of the BSD 2-Clause License:
@ -381,8 +390,8 @@ Open Source Software Licensed under the BSD 3-Clause License:
Copyright 2014 Yahoo! Inc.
All rights reserved.
2. highlight.js
Copyright (c) 2006, Ivan Sagalaev.
2. serialize-javascript
Copyright 2014 Yahoo! Inc.
All rights reserved.
@ -402,14 +411,11 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
Open Source Software Licensed under the ISC License:
--------------------------------------------------------------------
1. c8
Copyright (c) 2017, Contributors
1. raiz
Copyright raiz original authour and authors
2. picocolors
Copyright (c) 2021 Alexey Raspopov, Kostiantyn Denysov, Anton Verinov
3. semver
Copyright (c) Isaac Z. Schlueter and Contributors
2. axios-jsonp
Copyright (c) Adonis
Terms of the ISC License:
@ -422,239 +428,232 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE
Open Source Software Licensed under the MIT License:
--------------------------------------------------------------------
1. @commitlint/cli
Copyright (c) 2016 - present Mario Nebl
2. @commitlint/config-conventional
Copyright (c) 2016 - present Mario Nebl
3. @element-plus/icons-vue
Copyright (c) element-plus contributors
4. @eslint/js
Copyright OpenJS Foundation and other contributors
5. @popperjs/core
Copyright (c) 2019 Federico Zivolo
6. @scena/guides
Copyright (c) 2019 Daybrush
7. @stylistic/eslint-plugin
Copyright (c) Anthony Fu
8. @types/events
Copyright (c) Microsoft Corporation.
9. @types/fs-extra
Copyright (c) Microsoft Corporation.
10. @types/lodash-es
Copyright (c) Microsoft Corporation.
11. @types/node
Copyright (c) Microsoft Corporation.
12. @types/qrcode
Copyright (c) Microsoft Corporation.
13. @types/react
Copyright (c) Microsoft Corporation.
14. @types/react-dom
Copyright (c) Microsoft Corporation.
15. @types/serialize-javascript
Copyright (c) Microsoft Corporation.
16. @types/sortablejs
Copyright (c) Microsoft Corporation.
17. @typescript-eslint/eslint-plugin
Copyright (c) 2019 TypeScript ESLint and other contributors
18. @vitejs/plugin-legacy
Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors
19. @vitejs/plugin-react-refresh
Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors
20. @vitejs/plugin-vue
Copyright (c) 2019-present, Yuxi (Evan) You and contributors
21. @vitejs/plugin-vue-jsx
Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors
22. @vitest/coverage-v8
Copyright (c) 2021-present, Anthony Fu and Vitest contributors
23. @vue/compiler-sfc
Copyright (c) 2018-present, Yuxi (Evan) You
24. @vue/test-utils
Copyright (c) 2021-present vuejs
25. axios
Copyright (c) 2014-present Matt Zabriskie
26. buffer
Copyright (c) Feross Aboukhadijeh, and other contributors
27. cac
Copyright (c) egoist <0x142857@gmail.com>
28. chokidar
Copyright (c) Paul Miller (https://paulmillr.com)
29. commitizen
Copyright (c) 2015 Jim Cummins
30. conventional-changelog-cli
Copyright (c) Steve Mao
31. cosmiconfig
Copyright (c) 2015 David Clark
32. cz-conventional-changelog
Copyright (c) 2015-2018 Commitizen Contributors
33. dayjs
Copyright (c) 2018-present, iamkun
34. deep-object-diff
Copyright (c) 2017 Matt Phillips
35. deep-state-observer
Copyright (c) neuronet.io
36. element-plus
Copyright element-plus original authour and authors
37. emmet-monaco-es
Copyright (c) Troy
38. enquirer
Copyright (c) 2016-present, Jon Schlinkert
39. esbuild
Copyright (c) 2020 Evan Wallace
40. eslint
Copyright JS Foundation and other contributors, https://js.foundation
41. eslint-config-prettier
Copyright (c) 2017 Simon Lydell
42. eslint-plugin-import
Copyright (c) 2015 Ben Mosher
43. eslint-plugin-prettier
Copyright © 2017 Andres Suarez and Teddy Katz
44. eslint-plugin-simple-import-sort
Copyright (c) 2018, 2019, 2020 Simon Lydell
45. eslint-plugin-vue
Copyright (c) 2017 Toru Nagashima
46. events
1. events
Copyright Joyent, Inc. and other Node contributors.
47. execa
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com>
2. vite-plugin-dts
Copyright (c) 2021-present qmhc
48. fs-extra
Copyright (c) 2011-2017 JP Richardson
3. color
Copyright (c) 2012 Heather Arthur
49. gesto
Copyright (c) 2019 Daybrush
4. element-plus
Copyright element-plus original authour and authors
50. globals
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com>
5. @types/node
Copyright (c) Microsoft TypeScript, DefinitelyTyped, Alberto Schiabel, Alvis HT Tang, Andrew Makarov, Benjamin Toueg, Chigozirim C., David Junger, Deividas Bakanas, Eugene Y. Q. Shen, Hannes Magnusson, Hoàng Văn Khải, Huw, Kelvin Jin, Klaus Meinhardt, Lishude, Mariusz Wiktorczyk, Mohsen Azimi, Nicolas Even, Nikita Galkin, Parambir Singh, Sebastian Silbermann, Simon Schick, Thomas den Hollander, Wilco Bakker, wwwy3y3, Zane Hannan AU, Samuel Ainsworth, Kyle Uehlein, Thanik Bhongbhibhat, Marcin Kopacz, Trivikram Kamat, Junxiao Shi, Ilia Baryshnikov, and ExE Boss.
51. husky
Copyright (c) 2021 typicode
52. jsdom
Copyright (c) 2010 Elijah Insua
53. keycon
Copyright (c) 2019 Daybrush
54. lint-staged
Copyright (c) 2016 Andrey Okonetchnikov
55. merge-options
Copyright (c) Michael Mayer
56. minimist
Copyright (c) James Halliday
57. monaco-editor
Copyright (c) 2016 - present Microsoft Corporation
58. moveable
Copyright (c) 2019 Daybrush
59. moveable-helper
Copyright (c) 2019 Daybrush
60. prettier
Copyright © James Long and contributors
61. qrcode
Copyright (c) Ryan Day
62. react
Copyright (c) Facebook, Inc. and its affiliates.
63. react-dom
Copyright (c) Facebook, Inc. and its affiliates.
64. recast
Copyright (c) 2012 Ben Newman <bn@cs.stanford.edu>
65. rolldown
Copyright (c) 2023-present Rolldown contributors
66. rolldown-plugin-dts
Copyright (c) Kevin Deng
67. sass-embedded
Copyright (c) 2019 Google Inc.
68. scenejs
Copyright (c) 2019 Daybrush
69. shx
Copyright (c) ShellJS contributors
70. sortablejs
Copyright (c) 2019 All contributors to Sortable
71. tdesign-vue-next
Copyright (c) Tencent
72. typescript-eslint
Copyright (c) 2019 TypeScript ESLint and other contributors
73. vite-plugin-commonjs
Copyright (c) vite-plugin contributors
74. vitepress
Copyright (c) 2019-present, Yuxi (Evan) You
75. vitest
Copyright (c) 2021-present, Anthony Fu and Vitest contributors
76. vue
6. vue
Copyright (c) 2018-present, Yuxi (Evan) You
77. vue-router
7. @vitejs/plugin-vue
Copyright (c) 2019-present, Yuxi (Evan) You and contributors
8. @vitejs/plugin-vue-jsx
Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors
9. @vue/compiler-sfc
Copyright (c) 2018-present, Yuxi (Evan) You
10. @vue/test-utils
Copyright (c) 2021-present vuejs
11. sass
Copyright (c) 2016, Google Inc.
12. vue-tsc
Copyright (c) 2021-present Johnson Chu
13. moment
Copyright (c) JS Foundation and other contributors
14. sortablejs
Copyright (c) 2019 All contributors to Sortable
15. @scena/guides
Copyright (c) 2019 Daybrush
16. moveable
Copyright (c) 2019 Daybrush
17. delegate
Copyright (c) Zeno Rocha
18. tiny-emitter
Copyright (c) 2017 Scott Corgan
19. @testing-library/vue
Copyright (c) 2018 Daniel Cook
Copyright (c) 2017 Kent C. Dodds
20. react
Copyright (c) Facebook, Inc. and its affiliates.
21. react-dom
Copyright (c) Facebook, Inc. and its affiliates.
22. vue
Copyright (c) 2013-present, Yuxi (Evan) You
23. @vue/composition-api
Copyright (c) 2019-present, liximomo(X.L)
24. vite-plugin-vue2
Copyright © underfin
25. vue-template-compiler
Copyright (c)-present, Yuxi (Evan) You
26. rollup-plugin-external-globals
Copyright (c) 2018 eight
27. recast
Copyright (c) 2012 Ben Newman <bn@cs.stanford.edu>
28. @babel/preset-env
Copyright (c) 2014-present Sebastian McKenzie and other contributors
29. @vitejs/plugin-react-refresh
Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors
30. @commitlint/cli
Copyright (c) 2016 - present Mario Nebl
31. @commitlint/config-conventional
Copyright (c) 2016 - present Mario Nebl
32. @typescript-eslint/eslint-plugin
Copyright (c) 2019 TypeScript ESLint and other contributors
33. @vue/cli-plugin-babel
Copyright (c) 2017-present, Yuxi (Evan) You
34. @vue/cli-plugin-unit-jest
Copyright (c) 2017-present, Yuxi (Evan) You
35. babel-eslint
Copyright (c) 2014-2016 Sebastian McKenzie <sebmck@gmail.com>
36. cz-conventional-changelog
Copyright (c) 2015-2018 Commitizen Contributors
37. eslint
Copyright JS Foundation and other contributors, https://js.foundation
38. eslint-plugin-import
Copyright (c) 2015 Ben Mosher
39. eslint-plugin-prettier
Copyright © 2017 Andres Suarez and Teddy Katz
40. eslint-plugin-simple-import-sort
Copyright (c) 2018, 2019, 2020 Simon Lydell
41. eslint-plugin-vue
Copyright (c) 2017 Toru Nagashima
42. husky
Copyright (c) 2021 typicode
43. lerna
Copyright (c) 2015-present Lerna Contributors
44. lint-staged
Copyright (c) 2016 Andrey Okonetchnikov
45. prettier
Copyright © James Long and contributors
46. vue-jest
Copyright (c) 2017 Edd Yerburgh
47. axios
Copyright (c) 2014-present Matt Zabriskie
48. core-js
Copyright (c) 2014-2021 Denis Pushkarev
49. js-cookie
Copyright (c) 2018 Copyright 2018 Klaus Hartl, Fagner Brack, GitHub Contributors
50. vue-router
Copyright (c) 2020 Eduardo San Martin Morote
78. vue-tsc
Copyright (c) 2021-present Johnson Chu
51. koa
Copyright (c) 2019 Koa contributors
52. koa-bodyparser
Copyright (c) 2014 dead_horse
53. koa-router
Copyright (c) 2015 Alexander C. Mingoia
54. koa-send
Copyright (c) 2020 Koa contributors
55. module-alias
Copyright (c) 2018, Nick Gavrilov
56. mysql2
Copyright (c) 2016 Andrey Sidorov (sidorares@yandex.ru) and contributors
57. sequelize
Copyright (c) 2014-present Sequelize contributors
58. sequelize-typescript
Copyright (c) 2017 Robin Buschmann
59. lodash
Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
60. jest
Copyright (c) Facebook, Inc. and its affiliates.
61. fs-extra
Copyright (c) 2011-2017 JP Richardson
62. moment-timezone
Copyright (c) JS Foundation and other contributors
63. nodemon
Copyright (C) Remy Sharp
64. ts-node
Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)
65. tsconfig-paths
Copyright (c) 2016 Jonas Kello
66. prettier
Copyright © James Long and contributors
67. @babel/core
Copyright (c) 2014-present Sebastian McKenzie and other contributors
68. @babel/preset-typescript
Copyright (c) 2014-present Sebastian McKenzie and other contributors
69. @types/fs-extra
Copyright (c) Microsoft Corporation.
70. @types/jest
Copyright (c) Microsoft Corporation.
71. @types/koa
Copyright (C) DavidCai1993, jKey Lu, Brice Bernard, harryparkdotio, Wooram Jun, Christian Vaagland Tellnes, Piotr Kuczynski, and vnoder.
72. @types/koa-bodyparser
Copyright (C) Jerry Chin, Anup Kishore, Hiroshi Ioka, Alexi Maschas, and Pirasis Leelatanon.
73. zepto
Copyright (c) 2010-2016 Thomas Fuchs
http://zeptojs.com/
74. monaco-editor
Copyright (c) 2016 - present Microsoft Corporation
75. @types/koa-router
Copyright (C) Jerry Chin, Pavel Ivanov, JounQin, Romain Faust, Guillaume Mayer, Andrea Gueugnaut, and Yves Kaufmann.
Terms of the MIT License:
@ -5431,73 +5430,3 @@ Repository: github:eemeli/yaml
> TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
> THIS SOFTWARE.
Open Source Software Licensed under the Zero-Clause BSD License (0BSD):
--------------------------------------------------------------------
1. tslib
Copyright (c) Microsoft Corp.
Terms of the Zero-Clause BSD License (0BSD):
--------------------------------------------------------------------
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
Open Source Software Licensed under the Blue Oak Model License 1.0.0:
--------------------------------------------------------------------
1. rimraf
Copyright (c) Isaac Z. Schlueter and Contributors
Terms of the Blue Oak Model License 1.0.0:
--------------------------------------------------------------------
Blue Oak Model License
Version 1.0.0
Purpose
This license gives everyone as much permission to work with this software as possible, while protecting contributors from liability.
Acceptance
In order to receive this license, you must agree to its rules. The rules of this license are both obligations under that agreement and conditions to your license. You must not do anything with this software that triggers a rule that you cannot or will not follow.
Copyright
Each contributor licenses you to do everything with this software that would otherwise infringe that contributor's copyright in it.
Notices
You must ensure that everyone who gets a copy of any part of this software from you, with or without changes, also gets the text of this license or a link to https://blueoakcouncil.org/license/1.0.0.
Excuse
If anyone notifies you in writing that you have not complied with Notices, you can keep your license by taking all practical steps to comply within 30 days after the notice. If you do not do so, your license ends immediately.
Patent
Each contributor licenses you to do everything with this software that would otherwise infringe any patent claims they can license or become able to license.
Reliability
No contributor can revoke this license.
No Liability
As far as the law allows, this software comes as is, without any warranty or condition, and no contributor will be liable to anyone for any damages related to this software or this license, under any kind of legal claim.
Open Source Software Licensed under the (MIT OR CC0-1.0) License:
--------------------------------------------------------------------
1. type-fest
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
A copy of the MIT License is included in this file.

View File

@ -16,9 +16,9 @@ https://tencent.github.io/tmagic-editor/playground/index.html
## 环境准备
node.js ^20.19.0 || >=22.12.0
node.js >= 18
pnpm >= 10
pnpm >= 9
先安装 pnpm

View File

@ -552,11 +552,9 @@ export default defineConfig({
vite: {
optimizeDeps: {
rolldownOptions: {
transform: {
define: {
global: 'globalThis',
},
esbuildOptions: {
define: {
global: 'globalThis',
},
},
},

View File

@ -189,8 +189,6 @@
import hljs from 'highlight.js';
import serialize from 'serialize-javascript';
import { MForm } from '@tmagic/form';
export function stripScript(content) {
const result = content.match(/<(script)>([\s\S]+)<\/\1>/);
return result && result[2] ? result[2].trim() : '';
@ -212,10 +210,6 @@ export function stripTemplate(content) {
export default {
props: ['type', 'config'],
components: {
MForm,
},
data() {
return {
codepen: {

View File

@ -1,6 +1,6 @@
{
"name": "@tmagic/eslint-config",
"version": "0.1.0",
"version": "0.0.3",
"main": "index.mjs",
"type": "module",
"repository": {
@ -9,21 +9,20 @@
"url": "https://github.com/Tencent/tmagic-editor.git"
},
"dependencies": {
"@eslint/js": "^10.0.1",
"@typescript-eslint/parser": "^8.58.0",
"@typescript-eslint/eslint-plugin": "^8.58.0",
"@stylistic/eslint-plugin": "^5.10.0",
"@eslint/js": "^9.34.0",
"@typescript-eslint/parser": "^8.41.0",
"@typescript-eslint/eslint-plugin": "^8.41.0 ",
"@stylistic/eslint-plugin": "^5.2.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-simple-import-sort": "^13.0.0",
"eslint-plugin-vue": "^10.8.0",
"vue-eslint-parser": "^10.4.0",
"eslint-plugin-prettier": "^5.5.5",
"globals": "^17.4.0",
"typescript-eslint": "^8.58.0"
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-vue": "^10.4.0",
"eslint-plugin-prettier": "^5.5.4 ",
"globals": "^16.3.0",
"typescript-eslint": "^8.41.0"
},
"peerDependencies": {
"eslint": ">=10.0.0",
"prettier": ">=3.8.0"
"eslint": ">=9.24.0",
"prettier": ">=3.5.3"
}
}

View File

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

View File

@ -1,9 +1,9 @@
{
"version": "1.7.11",
"version": "1.7.7",
"name": "tmagic",
"private": true,
"type": "module",
"packageManager": "pnpm@10.32.1",
"packageManager": "pnpm@10.18.2",
"scripts": {
"bootstrap": "pnpm i && pnpm build",
"clean:top": "rimraf */**/dist */**/types */dist coverage dwt* temp packages/cli/lib",
@ -16,8 +16,8 @@
"playground:react": "pnpm --filter \"runtime-react\" build:libs && pnpm --filter \"runtime-react\" --filter \"tmagic-playground\" dev:react",
"pg:react": "pnpm playground:react",
"build": "pnpm build:dts && node scripts/build.mjs",
"build:dts": "pnpm --filter \"@tmagic/cli\" build && tsc -p tsconfig.build-browser.json && vue-tsc --declaration --emitDeclarationOnly --project tsconfig.build-vue.json && rolldown -c rolldown.dts.config.mjs && rimraf temp",
"check:type": "node scripts/check-type.mjs",
"build:dts": "pnpm --filter \"@tmagic/cli\" build && tsc -p tsconfig.build-browser.json && vue-tsc --declaration --emitDeclarationOnly --project tsconfig.build-vue.json && rollup -c rollup.dts.config.js && rimraf temp",
"check:type": "tsc --incremental --noEmit -p tsconfig.check.json && vue-tsc --noEmit -p tsconfig.check-vue.json",
"build:playground": "pnpm --filter \"runtime-vue\" build && pnpm --filter \"tmagic-playground\" build",
"docs:dev": "vitepress dev docs",
"docs:serve": "vitepress serve docs",
@ -42,10 +42,11 @@
"devDependencies": {
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@rollup/plugin-alias": "^6.0.0",
"@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.2",
"@vitest/coverage-v8": "^4.0.12",
"@vue/compiler-sfc": "catalog:",
"c8": "^10.1.3",
"commitizen": "^4.3.1",
@ -54,7 +55,7 @@
"cz-conventional-changelog": "^3.3.0",
"element-plus": "^2.11.8",
"enquirer": "^2.4.1",
"eslint": "^10.2.1",
"eslint": "^9.39.1",
"execa": "^9.6.0",
"highlight.js": "^11.11.1",
"husky": "^9.1.7",
@ -62,21 +63,21 @@
"lint-staged": "^16.2.7",
"minimist": "^1.2.8",
"picocolors": "^1.1.1",
"prettier": "^3.8.3",
"prettier": "^3.6.2",
"recast": "^0.23.11",
"rimraf": "^3.0.2",
"rolldown": "^1.0.0-rc.17",
"rolldown-plugin-dts": "^0.23.2",
"sass-embedded": "^1.99.0",
"rollup": "4.44.1",
"rollup-plugin-dts": "^6.2.3",
"sass-embedded": "^1.93.3",
"semver": "^7.7.3",
"serialize-javascript": "^7.0.0",
"shx": "^0.3.4",
"typescript": "catalog:",
"vite": "catalog:",
"vitepress": "^1.6.4",
"vitest": "^4.1.0",
"vitest": "^4.0.12",
"vue": "catalog:",
"vue-tsc": "^3.2.7"
"vue-tsc": "^3.1.4"
},
"config": {
"commitizen": {

View File

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

View File

@ -2,8 +2,8 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"moduleResolution": "node16",
"module": "node16",
"moduleResolution": "Node",
"module": "CommonJS",
"rootDir": "./src",
"outDir": "./lib",
"declaration": true,

View File

@ -1,15 +1,14 @@
{
"version": "1.7.11",
"version": "1.7.7",
"name": "@tmagic/core",
"type": "module",
"sideEffects": false,
"main": "dist/tmagic-core.umd.cjs",
"module": "dist/es/index.js",
"module": "dist/tmagic-core.js",
"types": "types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./dist/es/index.js",
"import": "./dist/tmagic-core.js",
"require": "./dist/tmagic-core.umd.cjs"
},
"./resetcss.css": {

View File

@ -293,11 +293,9 @@ class App extends EventEmitter {
const method = methods.find((item) => item.name === methodName);
if (method && typeof method.content === 'function') {
return await method.content({ app: this, params, dataSource, eventParams: args, flowState, node });
}
if (typeof dataSource[methodName] === 'function') {
return await dataSource[methodName]({ params });
await method.content({ app: this, params, dataSource, eventParams: args, flowState, node });
} else if (typeof dataSource[methodName] === 'function') {
await dataSource[methodName]();
}
} catch (e: any) {
if (this.errorHandler) {

View File

@ -16,8 +16,6 @@
* limitations under the License.
*/
import { cloneDeep } from 'lodash-es';
import type { Id, MComponent, MContainer, MPage, MPageFragment } from '@tmagic/schema';
import App from './App';
@ -72,7 +70,7 @@ class Page extends Node {
this.app.pageFragments.set(
config.id,
new Page({
config: cloneDeep(pageFragment),
config: pageFragment,
app: this.app,
}),
);

View File

@ -1,15 +1,14 @@
{
"version": "1.7.11",
"version": "1.7.7",
"name": "@tmagic/data-source",
"type": "module",
"sideEffects": false,
"main": "dist/tmagic-data-source.umd.cjs",
"module": "dist/es/index.js",
"module": "dist/tmagic-data-source.js",
"types": "types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./dist/es/index.js",
"import": "./dist/tmagic-data-source.js",
"require": "./dist/tmagic-data-source.umd.cjs"
},
"./*": "./*"

View File

@ -15,7 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { cloneDeep, union } from 'lodash-es';
import { union } from 'lodash-es';
import type { default as TMagicApp } from '@tmagic/core';
import { getDepNodeIds, getNodes, isPage, isPageFragment, replaceChildNode } from '@tmagic/core';
@ -82,11 +82,11 @@ export const createDataSourceManager = (app: TMagicApp, useMock?: boolean, initi
for (const [, pageFragment] of app.pageFragments) {
if (pageFragment.data.id === newNode.id) {
pageFragment.setData(cloneDeep(newNode));
pageFragment.setData(newNode);
} else if (pageFragment.data.id === page.id) {
pageFragment.getNode(newNode.id, { strict: true })?.setData(cloneDeep(newNode));
pageFragment.getNode(newNode.id, { strict: true })?.setData(newNode);
if (!pageFragment.instance) {
replaceChildNode(cloneDeep(newNode), [pageFragment.data]);
replaceChildNode(newNode, [pageFragment.data]);
}
}
}

View File

@ -20,7 +20,7 @@ import EventEmitter from 'events';
import { cloneDeep } from 'lodash-es';
import type { CodeBlockContent, DataSchema, DataSourceSchema, default as TMagicApp } from '@tmagic/core';
import { DATA_SOURCE_SET_DATA_METHOD_NAME, getDefaultValueFromFields } from '@tmagic/core';
import { getDefaultValueFromFields } from '@tmagic/core';
import { ObservedData } from '@data-source/observed-data/ObservedData';
import { SimpleObservedData } from '@data-source/observed-data/SimpleObservedData';
@ -51,7 +51,6 @@ export default class DataSource<T extends DataSourceSchema = DataSourceSchema> e
super();
this.#id = options.schema.id;
this.#type = options.schema.type;
this.#schema = options.schema;
this.app = options.app;
@ -59,11 +58,6 @@ export default class DataSource<T extends DataSourceSchema = DataSourceSchema> e
this.setFields(options.schema.fields);
this.setMethods(options.schema.methods || []);
// @ts-ignore
this[DATA_SOURCE_SET_DATA_METHOD_NAME] = ({ params }: { params: { field?: string[]; data: any } }) => {
this.setData(params.data, params.field?.join('.'));
};
let data = options.initialData;
// eslint-disable-next-line @typescript-eslint/naming-convention
const ObservedDataClass = options.ObservedDataClass || SimpleObservedData;

View File

@ -79,9 +79,9 @@ export default class HttpDataSource extends DataSource<HttpDataSourceSchema> {
/** 请求函数 */
#fetch?: RequestFunction;
/** 请求前需要执行的函数队列 */
#beforeRequest: (Function | ((...args: any[]) => any))[] = [];
#beforeRequest: ((...args: any[]) => any)[] = [];
/** 请求后需要执行的函数队列 */
#afterRequest: (Function | ((...args: any[]) => any))[] = [];
#afterRequest: ((...args: any[]) => any)[] = [];
#type = 'http';

View File

@ -1,15 +1,14 @@
{
"version": "1.7.11",
"version": "1.7.7",
"name": "@tmagic/dep",
"type": "module",
"sideEffects": false,
"main": "dist/tmagic-dep.umd.cjs",
"module": "dist/es/index.js",
"module": "dist/tmagic-dep.js",
"types": "types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./dist/es/index.js",
"import": "./dist/tmagic-dep.js",
"require": "./dist/tmagic-dep.umd.cjs"
},
"./*": "./*"

View File

@ -71,7 +71,7 @@ export default class Target {
this.deps[id] = dep;
if (!dep.keys.includes(key)) {
if (dep.keys.indexOf(key) === -1) {
dep.keys.push(key);
}
}
@ -115,7 +115,7 @@ export default class Target {
public hasDep(id: string | number, key: string | number) {
const dep = this.deps[id];
return dep?.keys.includes(key) ?? false;
return Boolean(dep?.keys.find((d) => d === key));
}
public destroy() {

View File

@ -5,12 +5,6 @@ import type Target from './Target';
import { type DepExtendedData, DepTargetType, type TargetList, TargetNode } from './types';
import { traverseTarget } from './utils';
const DATA_SOURCE_TARGET_TYPES: Set<string> = new Set([
DepTargetType.DATA_SOURCE,
DepTargetType.DATA_SOURCE_COND,
DepTargetType.DATA_SOURCE_METHOD,
]);
export default class Watcher {
private targetsList: TargetList = {};
private childrenProp = 'items';
@ -165,7 +159,7 @@ export default class Watcher {
};
}
const clearedItemsNodeIds = new Set<string | number>();
const clearedItemsNodeIds: (string | number)[] = [];
traverseTarget(targetsList, (target) => {
if (nodes) {
for (const node of nodes) {
@ -174,9 +168,9 @@ export default class Watcher {
if (
Array.isArray(node[this.childrenProp]) &&
node[this.childrenProp].length &&
!clearedItemsNodeIds.has(node[this.idProp])
!clearedItemsNodeIds.includes(node[this.idProp])
) {
clearedItemsNodeIds.add(node[this.idProp]);
clearedItemsNodeIds.push(node[this.idProp]);
this.clear(node[this.childrenProp]);
}
}
@ -196,7 +190,12 @@ export default class Watcher {
}
public collectItem(node: TargetNode, target: Target, depExtendedData: DepExtendedData = {}, deep = false) {
if (node[NODE_DISABLE_DATA_SOURCE_KEY] && DATA_SOURCE_TARGET_TYPES.has(target.type)) {
const dataSourceTargetTypes: string[] = [
DepTargetType.DATA_SOURCE,
DepTargetType.DATA_SOURCE_COND,
DepTargetType.DATA_SOURCE_METHOD,
];
if (node[NODE_DISABLE_DATA_SOURCE_KEY] && dataSourceTargetTypes.includes(target.type)) {
return;
}

View File

@ -18,8 +18,6 @@ import {
import Target from './Target';
import { DepTargetType, type TargetList } from './types';
const INTEGER_REGEXP = /^\d+$/;
export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent, initialDeps: DepData = {}) =>
new Target({
type: DepTargetType.CODE_BLOCK,
@ -32,7 +30,8 @@ export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent, initi
}
if (value?.hookType === HookType.CODE && Array.isArray(value.hookData)) {
return value.hookData.some((item: HookData) => item.codeId === id);
const index = value.hookData.findIndex((item: HookData) => item.codeId === id);
return Boolean(index > -1);
}
return false;
@ -55,7 +54,12 @@ export const isIncludeArrayField = (keys: string[], fields: DataSchema[]) => {
f = field?.fields || [];
// 字段类型为数组并且后面没有数字索引
return field?.type === 'array' && index < keys.length - 1 && !INTEGER_REGEXP.test(keys[index + 1]);
return (
field?.type === 'array' &&
// 不是整数
/^(?!\d+$).*$/.test(`${keys[index + 1]}`) &&
index < keys.length - 1
);
});
};
@ -74,25 +78,33 @@ export const isDataSourceTemplate = (value: any, ds: Pick<DataSourceSchema, 'id'
return false;
}
for (const tpl of templates) {
const arrayFieldTemplates = [];
const fieldTemplates = [];
templates.forEach((tpl) => {
// 将${dsId.xxxx} 转成 dsId.xxxx
const expression = tpl.substring(2, tpl.length - 1);
const keys = getKeysArray(expression);
const dsId = keys.shift();
if (!dsId || dsId !== ds.id) {
continue;
return;
}
// ${dsId.array} ${dsId.array[0]} ${dsId.array[0].a} 这种是依赖
// ${dsId.array.a} 这种不是依赖,这种需要再迭代器容器中的组件才能使用,依赖由迭代器处理
const includesArray = isIncludeArrayField(keys, ds.fields);
if (hasArray === includesArray) {
return true;
if (isIncludeArrayField(keys, ds.fields)) {
arrayFieldTemplates.push(tpl);
} else {
fieldTemplates.push(tpl);
}
});
if (hasArray) {
return arrayFieldTemplates.length > 0;
}
return false;
return fieldTemplates.length > 0;
};
/**
@ -172,12 +184,7 @@ export const isDataSourceTarget = (
value: any,
hasArray = false,
) => {
if (!value) {
return false;
}
const valueType = typeof value;
if (valueType !== 'string' && valueType !== 'object') {
if (!value || !['string', 'object'].includes(typeof value)) {
return false;
}
@ -186,13 +193,13 @@ export const isDataSourceTarget = (
}
// 或者在模板在使用数据源
if (valueType === 'string') {
if (typeof value === 'string') {
return isDataSourceTemplate(value, ds, hasArray);
}
// 关联数据源对象,如:{ isBindDataSource: true, dataSourceId: 'xxx'}
// 使用data-source-select value: 'value' 可以配置出来
if (isObject(value) && value.isBindDataSource && value.dataSourceId === ds.id) {
if (isObject(value) && value?.isBindDataSource && value.dataSourceId && value.dataSourceId === ds.id) {
return true;
}
@ -203,7 +210,10 @@ export const isDataSourceTarget = (
if (isUseDataSourceField(value, ds.id)) {
const [, ...keys] = value;
const includeArray = isIncludeArrayField(keys, ds.fields);
return hasArray ? includeArray : !includeArray;
if (hasArray) {
return includeArray;
}
return !includeArray;
}
return false;
@ -225,9 +235,12 @@ export const isDataSourceCondTarget = (
return false;
}
if (ds.fields?.some((field) => field.name === keys[0])) {
if (ds.fields?.find((field) => field.name === keys[0])) {
const includeArray = isIncludeArrayField(keys, ds.fields);
return hasArray ? includeArray : !includeArray;
if (hasArray) {
return includeArray;
}
return !includeArray;
}
return false;
@ -269,12 +282,12 @@ export const createDataSourceMethodTarget = (
return false;
}
if (ds.methods?.some((method) => method.name === methodName)) {
if (ds.methods?.find((method) => method.name === methodName)) {
return true;
}
// 配置的方法名称可能会是数据源类中定义的并不存在于methods中所以这里判断如果methodName如果是字段名称就表示配置的不是方法
if (ds.fields?.some((field) => field.name === methodName)) {
if (ds.fields?.find((field) => field.name === methodName)) {
return false;
}
@ -287,18 +300,11 @@ export const traverseTarget = (
cb: (target: Target) => void,
type?: DepTargetType | string,
) => {
if (type) {
const targets = targetsList[type];
if (targets) {
for (const target of Object.values(targets)) {
cb(target);
}
}
return;
}
for (const targets of Object.values(targetsList)) {
for (const target of Object.values(targets)) {
if (type && target.type !== type) {
continue;
}
cb(target);
}
}

View File

@ -1,19 +1,18 @@
{
"version": "1.7.11",
"version": "1.7.7",
"name": "@tmagic/design",
"type": "module",
"sideEffects": [
"dist/style.css",
"dist/es/style.css",
"src/theme/*"
],
"main": "dist/tmagic-design.umd.cjs",
"module": "dist/es/index.js",
"module": "dist/tmagic-design.js",
"types": "types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./dist/es/index.js",
"import": "./dist/tmagic-design.js",
"require": "./dist/tmagic-design.umd.cjs"
},
"./*": "./*"

View File

@ -342,7 +342,7 @@ export interface TableColumnOptions<T = any> {
prop?: string;
align?: string;
headerAlign?: string;
sortable?: boolean | string;
sortable?: boolean;
sortOrders?: Array<'ascending' | 'descending'>;
selectable?: (row: T, index: number) => boolean;
};

View File

@ -1,20 +1,19 @@
{
"version": "1.7.11",
"version": "1.7.7",
"name": "@tmagic/editor",
"type": "module",
"sideEffects": [
"dist/style.css",
"dist/es/style.css",
"src/theme/*"
],
"main": "dist/tmagic-editor.umd.cjs",
"module": "dist/es/index.js",
"module": "dist/tmagic-editor.js",
"style": "dist/style.css",
"types": "types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./dist/es/index.js",
"import": "./dist/tmagic-editor.js",
"require": "./dist/tmagic-editor.umd.cjs"
},
"./dist/style.css": {
@ -69,21 +68,18 @@
"@types/lodash-es": "^4.17.4",
"@types/serialize-javascript": "^5.0.4",
"@types/sortablejs": "^1.15.9",
"@vue/test-utils": "^2.4.6"
"@vue/test-utils": "^2.4.6",
"type-fest": "^5.2.0"
},
"peerDependencies": {
"@tmagic/core": "workspace:*",
"monaco-editor": "^0.55.1 ",
"type-fest": "^5.2.0",
"monaco-editor": "^0.48.0",
"typescript": "catalog:",
"vue": "catalog:"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
},
"type-fest": {
"optional": true
}
}
}

View File

@ -207,7 +207,6 @@ const stageOptions: StageOptions = {
renderType: props.renderType,
guidesOptions: props.guidesOptions,
disabledMultiSelect: props.disabledMultiSelect,
beforeDblclick: props.beforeDblclick,
};
stageOverlayService.set('stageOptions', stageOptions);

View File

@ -65,9 +65,8 @@ import type { CodeBlockContent } from '@tmagic/core';
import { TMagicButton, TMagicDialog, tMagicMessage, tMagicMessageBox, TMagicTag } from '@tmagic/design';
import {
type ContainerChangeEventData,
defineFormConfig,
defineFormItem,
type FormConfig,
type FormState,
MFormBox,
type TableColumnConfig,
} from '@tmagic/form';
@ -88,7 +87,7 @@ const width = defineModel<number>('width', { default: 670 });
const boxVisible = defineModel<boolean>('visible', { default: false });
const props = defineProps<{
content: Omit<CodeBlockContent, 'content'> & { content: string };
content: CodeBlockContent;
disabled?: boolean;
isDataSource?: boolean;
dataSourceType?: string;
@ -119,7 +118,7 @@ const diffChange = () => {
difVisible.value = false;
};
const defaultParamColConfig = defineFormItem<TableColumnConfig>({
const defaultParamColConfig: TableColumnConfig = {
type: 'row',
label: '参数类型',
items: [
@ -147,79 +146,76 @@ const defaultParamColConfig = defineFormItem<TableColumnConfig>({
],
},
],
});
};
const functionConfig = computed(
() =>
defineFormConfig([
const functionConfig = computed<FormConfig>(() => [
{
text: '名称',
name: 'name',
rules: [{ required: true, message: '请输入名称', trigger: 'blur' }],
},
{
text: '描述',
name: 'desc',
},
{
text: '执行时机',
name: 'timing',
type: 'select',
options: () => {
const options = [
{ text: '初始化前', value: 'beforeInit' },
{ text: '初始化后', value: 'afterInit' },
];
if (props.dataSourceType !== 'base') {
options.push({ text: '请求前', value: 'beforeRequest' });
options.push({ text: '请求后', value: 'afterRequest' });
}
return options;
},
display: () => props.isDataSource,
},
{
type: 'table',
border: true,
text: '参数',
enableFullscreen: false,
enableToggleMode: false,
name: 'params',
dropSort: false,
items: [
{
text: '名称',
type: 'text',
label: '参数名',
name: 'name',
rules: [{ required: true, message: '请输入名称', trigger: 'blur' }],
},
{
text: '描述',
name: 'desc',
type: 'text',
label: '描述',
name: 'extra',
},
{
text: '执行时机',
name: 'timing',
type: 'select',
options: () => {
const options = [
{ text: '初始化前', value: 'beforeInit' },
{ text: '初始化后', value: 'afterInit' },
];
if (props.dataSourceType !== 'base') {
options.push({ text: '请求前', value: 'beforeRequest' });
options.push({ text: '请求后', value: 'afterRequest' });
}
return options;
},
display: () => props.isDataSource,
},
{
type: 'table',
border: true,
text: '参数',
enableFullscreen: false,
enableToggleMode: false,
name: 'params',
dropSort: false,
items: [
{
type: 'text',
label: '参数名',
name: 'name',
},
{
type: 'text',
label: '描述',
name: 'extra',
},
codeBlockService.getParamsColConfig() || defaultParamColConfig,
],
},
{
name: 'content',
type: 'vs-code',
options: inject('codeOptions', {}),
autosize: { minRows: 10, maxRows: 30 },
onChange: (_formState, code: string) => {
try {
// js
getEditorConfig('parseDSL')(code);
codeBlockService.getParamsColConfig() || defaultParamColConfig,
],
},
{
name: 'content',
type: 'vs-code',
options: inject('codeOptions', {}),
autosize: { minRows: 10, maxRows: 30 },
onChange: (formState: FormState | undefined, code: string) => {
try {
// js
getEditorConfig('parseDSL')(code);
return code;
} catch (error: any) {
tMagicMessage.error(error.message);
return code;
} catch (error: any) {
tMagicMessage.error(error.message);
throw error;
}
},
},
]) as FormConfig,
);
throw error;
}
},
},
]);
const parseContent = (content: any) => {
if (typeof content === 'string') {

View File

@ -13,7 +13,7 @@
<script lang="ts" setup>
import { computed, useTemplateRef } from 'vue';
import { type ContainerChangeEventData, type FormItemConfig, type FormValue, MForm } from '@tmagic/form';
import { type ContainerChangeEventData, type FormConfig, type FormValue, MForm } from '@tmagic/form';
import type { CodeParamStatement } from '@editor/type';
import { error } from '@editor/utils';
@ -34,7 +34,7 @@ const emit = defineEmits(['change']);
const formRef = useTemplateRef<InstanceType<typeof MForm>>('form');
const getFormConfig = (items: FormItemConfig[] = []) => [
const getFormConfig = (items: FormConfig = []) => [
{
type: 'fieldset',
items,
@ -46,29 +46,13 @@ const getFormConfig = (items: FormItemConfig[] = []) => [
const codeParamsConfig = computed(() =>
getFormConfig(
props.paramsConfig.map(({ name, text, extra, ...config }) => {
let { type } = config;
if (typeof type === 'function') {
type = type(undefined, {
model: props.model[props.name],
});
}
if (type && ['data-source-field-select', 'vs-code'].includes(type)) {
return {
...config,
name,
text,
extra,
};
}
return {
type: 'data-source-field-select' as const,
name,
text,
extra,
fieldConfig: config,
};
}) as FormItemConfig[],
props.paramsConfig.map(({ name, text, extra, ...config }) => ({
type: 'data-source-field-select',
name,
text,
extra,
fieldConfig: config,
})),
),
);

View File

@ -98,8 +98,6 @@ export interface EditorProps {
isContainer?: (el: HTMLElement) => boolean | Promise<boolean>;
/** 用于自定义组件树与画布的右键菜单 */
customContentMenu?: CustomContentMenuFunction;
/** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/** 页面顺序拖拽配置参数 */
pageBarSortOptions?: PageBarSortOptions;

View File

@ -1,12 +1,12 @@
<template>
<MLink :config="formConfig" :model="modelValue" name="form" @change="changeHandler"></MLink>
<m-fields-link :config="formConfig" :model="modelValue" name="form" @change="changeHandler"></m-fields-link>
</template>
<script lang="ts" setup>
import { computed, reactive, watch } from 'vue';
import serialize from 'serialize-javascript';
import type { CodeLinkConfig, FieldProps, MLink } from '@tmagic/form';
import type { CodeLinkConfig, FieldProps } from '@tmagic/form';
import { getEditorConfig } from '@editor/utils/config';

View File

@ -50,7 +50,6 @@ import {
createValues,
type FieldProps,
filterFunction,
type FormItemConfig,
type FormState,
MSelect,
type SelectConfig,
@ -116,7 +115,7 @@ watch(
const selectConfig: SelectConfig = {
type: 'select',
name: props.name,
disabled: props.disabled,
disable: props.disabled,
options: () => {
if (codeDsl.value) {
return map(codeDsl.value, (value, key) => ({
@ -142,9 +141,7 @@ const onCodeIdChangeHandler = (value: any) => {
changeRecords.push({
propPath: props.prop.replace(`${props.name}`, 'params'),
value: paramsConfig.value.length
? createValues(mForm, paramsConfig.value as unknown as FormItemConfig[], {}, props.model.params)
: {},
value: paramsConfig.value.length ? createValues(mForm, paramsConfig.value, {}, props.model.params) : {},
});
emit('change', value, {

View File

@ -1,21 +1,6 @@
<template>
<div class="m-editor-data-source-field-select">
<template v-if="dataSourceId">
<TMagicCascader
:model-value="selectFieldsId"
clearable
filterable
:size="size"
:disabled="disabled"
:options="fieldsOptions"
:props="{
checkStrictly,
}"
@change="fieldChangeHandler"
></TMagicCascader>
</template>
<template v-else-if="checkStrictly">
<template v-if="checkStrictly">
<TMagicSelect
:model-value="selectDataSourceId"
clearable
@ -107,8 +92,6 @@ const props = defineProps<{
dataSourceFieldType?: DataSourceFieldType[];
/** 是否可以编辑数据源disable表示的是是否可以选择数据源 */
notEditable?: boolean | FilterFunction;
/** 指定数据源ID限定只能选择该数据源的字段 */
dataSourceId?: string;
}>();
const emit = defineEmits<{
@ -123,12 +106,7 @@ const { dataSourceService, uiService } = useServices();
const mForm = inject<FormState | undefined>('mForm');
const eventBus = inject<EventBus>('eventBus');
const allDataSources = computed(() => dataSourceService.get('dataSources') || []);
const dataSources = computed(() => {
if (!props.dataSourceId) return allDataSources.value;
return allDataSources.value.filter((ds) => ds.id === props.dataSourceId);
});
const dataSources = computed(() => dataSourceService.get('dataSources') || []);
const valueIsKey = computed(() => props.value === 'key');
const notEditable = computed(() => filterFunction(mForm, props.notEditable, props));
@ -147,13 +125,7 @@ const selectFieldsId = ref<string[]>([]);
watch(
modelValue,
(value) => {
if (props.dataSourceId) {
const dsIdValue = valueIsKey.value
? props.dataSourceId
: `${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}${props.dataSourceId}`;
selectDataSourceId.value = dsIdValue;
selectFieldsId.value = Array.isArray(value) ? value : [];
} else if (Array.isArray(value) && value.length) {
if (Array.isArray(value)) {
const [dsId, ...fields] = value;
selectDataSourceId.value = dsId;
selectFieldsId.value = fields;
@ -168,7 +140,7 @@ watch(
);
const fieldsOptions = computed(() => {
const ds = allDataSources.value.find((ds) => ds.id === removeDataSourceFieldPrefix(selectDataSourceId.value));
const ds = dataSources.value.find((ds) => ds.id === removeDataSourceFieldPrefix(selectDataSourceId.value));
if (!ds) return [];
@ -191,13 +163,8 @@ const dsChangeHandler = (v: string) => {
};
const fieldChangeHandler = (v: string[] = []) => {
if (props.dataSourceId) {
modelValue.value = v;
emit('change', v);
} else {
modelValue.value = [selectDataSourceId.value, ...v];
emit('change', modelValue.value);
}
modelValue.value = [selectDataSourceId.value, ...v];
emit('change', modelValue.value);
};
const onChangeHandler = (v: string[] = []) => {

View File

@ -8,7 +8,6 @@
:value="config.value"
:checkStrictly="checkStrictly"
:dataSourceFieldType="config.dataSourceFieldType"
:dataSourceId="config.dataSourceId"
@change="onChangeHandler"
></FieldSelect>
@ -48,13 +47,7 @@ import { Coin } from '@element-plus/icons-vue';
import { DataSchema } from '@tmagic/core';
import { TMagicButton, tMagicMessage, TMagicTooltip } from '@tmagic/design';
import {
type ContainerChangeEventData,
type DataSourceFieldSelectConfig,
type FieldProps,
type FormState,
getFormField,
} from '@tmagic/form';
import type { ContainerChangeEventData, DataSourceFieldSelectConfig, FieldProps, FormState } from '@tmagic/form';
import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, removeDataSourceFieldPrefix } from '@tmagic/utils';
import MIcon from '@editor/components/Icon.vue';
@ -99,9 +92,7 @@ const dataSources = computed(() => dataSourceService.get('dataSources') || []);
const disabledDataSource = computed(() => propsService.getDisabledDataSource());
const type = computed((): string => {
if (!props.config.fieldConfig) return '';
let type = 'type' in props.config.fieldConfig ? props.config.fieldConfig.type : '';
let type = props.config.fieldConfig?.type;
if (typeof type === 'function') {
type = type(mForm, {
model: props.model,
@ -109,18 +100,11 @@ const type = computed((): string => {
}
if (type === 'form') return '';
if (type === 'container') return '';
return (
type?.replace(/([A-Z])/g, '-$1').toLowerCase() ||
(props.config.fieldConfig && 'items' in props.config.fieldConfig ? '' : 'text')
);
return type?.replace(/([A-Z])/g, '-$1').toLowerCase() || (props.config.items ? '' : 'text');
});
const tagName = computed(() => {
const component =
getFormField(type.value || 'container') ||
resolveComponent(
`m-${props.config.fieldConfig && 'items' in props.config.fieldConfig ? 'form' : 'fields'}-${type.value}`,
);
const component = resolveComponent(`m-${props.config.items ? 'form' : 'fields'}-${type.value}`);
if (typeof component !== 'string') return component;
return 'm-fields-text';
});

View File

@ -52,15 +52,12 @@ import { inject, Ref, ref } from 'vue';
import type { DataSchema } from '@tmagic/core';
import { TMagicButton, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
import {
type CodeConfig,
type ContainerChangeEventData,
type DataSourceFieldsConfig,
type FieldProps,
type FormConfig,
type FormState,
MFormBox,
type NumberConfig,
type TextConfig,
} from '@tmagic/form';
import { type ColumnConfig, MagicTable } from '@tmagic/table';
import { getDefaultValueFromFields } from '@tmagic/utils';
@ -250,7 +247,7 @@ const dataSourceFieldsConfig: FormConfig = [
{ text: 'true', value: true },
{ text: 'false', value: false },
],
} as unknown as CodeConfig | NumberConfig | TextConfig,
},
{
name: 'enable',
text: '是否可用',

View File

@ -48,18 +48,15 @@ import {
type DataSourceMethodSelectConfig,
type FieldProps,
filterFunction,
type FormItemConfig,
type FormState,
MCascader,
} from '@tmagic/form';
import { DATA_SOURCE_SET_DATA_METHOD_NAME } from '@tmagic/utils';
import CodeParams from '@editor/components/CodeParams.vue';
import MIcon from '@editor/components/Icon.vue';
import { useServices } from '@editor/hooks/use-services';
import type { CodeParamStatement, EventBus } from '@editor/type';
import { SideItemKey } from '@editor/type';
import { getFieldType } from '@editor/utils';
defineOptions({
name: 'MFieldsDataSourceMethodSelect',
@ -94,43 +91,6 @@ const isCustomMethod = computed(() => {
const getParamItemsConfig = ([dataSourceId, methodName]: [Id, string] = ['', '']): CodeParamStatement[] => {
if (!dataSourceId) return [];
if (methodName === DATA_SOURCE_SET_DATA_METHOD_NAME) {
return [
{
name: 'field',
text: '字段',
type: 'data-source-field-select',
dataSourceId,
checkStrictly: true,
},
{
name: 'data',
text: '数据',
type: (_formState, { model }) => {
const fieldType = getFieldType(dataSourceService.getDataSourceById(`${dataSourceId}`), model.field);
let type = 'vs-code';
if (fieldType === 'number') {
type = 'number';
} else if (fieldType === 'string') {
type = 'text';
} else if (fieldType === 'boolean') {
type = 'switch';
}
return type;
},
language: 'javascript',
options: inject('codeOptions', {}),
autosize: {
minRows: 1,
maxRows: 10,
},
},
];
}
const paramStatements = dataSources.value
?.find((item) => item.id === dataSourceId)
?.methods?.find((item) => item.name === methodName)?.params;
@ -147,21 +107,19 @@ const paramsConfig = ref<CodeParamStatement[]>(getParamItemsConfig(props.model[p
const methodsOptions = computed(
() =>
dataSources.value?.map((ds) => ({
label: ds.title || ds.id,
value: ds.id,
children: [
{
label: '设置数据',
value: DATA_SOURCE_SET_DATA_METHOD_NAME,
},
...(dataSourceService?.getFormMethod(ds.type) || []),
...(ds.methods || []).map((method) => ({
label: method.name,
value: method.name,
})),
],
})) || [],
dataSources.value
?.filter((ds) => ds.methods?.length || dataSourceService.getFormMethod(ds.type).length)
?.map((ds) => ({
label: ds.title || ds.id,
value: ds.id,
children: [
...(dataSourceService?.getFormMethod(ds.type) || []),
...(ds.methods || []).map((method) => ({
label: method.name,
value: method.name,
})),
],
})) || [],
);
const cascaderConfig = computed<CascaderConfig>(() => ({
@ -184,9 +142,7 @@ const onChangeHandler = (value: any) => {
changeRecords.push({
propPath: props.prop.replace(`${props.name}`, 'params'),
value: paramsConfig.value.length
? createValues(mForm, paramsConfig.value as unknown as FormItemConfig[], {}, props.model.params)
: {},
value: paramsConfig.value.length ? createValues(mForm, paramsConfig.value, {}, props.model.params) : {},
});
emit('change', value, {

View File

@ -42,7 +42,7 @@ const props = withDefaults(defineProps<FieldProps<DataSourceMethodsConfig>>(), {
const emit = defineEmits(['change']);
const codeConfig = ref<Omit<CodeBlockContent, 'content'> & { content: string }>();
const codeConfig = ref<CodeBlockContent>();
const codeBlockEditorRef = useTemplateRef<InstanceType<typeof CodeBlockEditor>>('codeBlockEditor');
let editIndex = -1;
@ -72,14 +72,10 @@ const methodColumns: ColumnConfig[] = [
{
text: '编辑',
handler: (method: CodeBlockContent, index: number) => {
let codeContent: string = '({ params, dataSource, app }) => {\n // place your code here\n}';
let codeContent = method.content || '({ params, dataSource, app }) => {\n // place your code here\n}';
if (method.content) {
if (typeof method.content !== 'string') {
codeContent = method.content.toString();
} else {
codeContent = method.content;
}
if (typeof codeContent !== 'string') {
codeContent = codeContent.toString();
}
codeConfig.value = {

View File

@ -67,7 +67,6 @@ const config = computed<GroupListConfig>(() => ({
name: props.name,
titlePrefix: props.config.titlePrefix,
expandAll: true,
enableToggleMode: false,
items: [
{
type: 'table',

View File

@ -1,6 +1,6 @@
<template>
<div class="m-fields-event-select">
<MTable
<m-form-table
v-if="isOldVersion"
name="events"
:size="size"
@ -8,7 +8,7 @@
:model="model"
:config="tableConfig"
@change="onChangeHandler"
></MTable>
></m-form-table>
<div v-else class="fullWidth">
<TMagicButton class="create-button" type="primary" :size="size" :disabled="disabled" @click="addEvent()"
@ -41,7 +41,7 @@
:icon="Delete"
:disabled="disabled"
:size="size"
@click="removeEvent(Number(index))"
@click="removeEvent(index)"
></TMagicButton>
</template>
</MPanel>
@ -59,18 +59,17 @@ import { ActionType } from '@tmagic/core';
import { TMagicButton } from '@tmagic/design';
import type {
CascaderOption,
ChildConfig,
CodeSelectColConfig,
ContainerChangeEventData,
DataSourceMethodSelectConfig,
DynamicTypeConfig,
EventSelectConfig,
FieldProps,
FormState,
OnChangeHandlerData,
PanelConfig,
TableConfig,
UISelectConfig,
} from '@tmagic/form';
import { defineFormItem, MContainer as MFormContainer, MPanel, MTable } from '@tmagic/form';
import { MContainer as MFormContainer, MPanel } from '@tmagic/form';
import { DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX, traverseNode } from '@tmagic/utils';
import { useServices } from '@editor/hooks/use-services';
@ -90,10 +89,10 @@ const { editorService, dataSourceService, eventsService, codeBlockService, props
//
const eventNameConfig = computed(() => {
const defaultEventNameConfig = {
const defaultEventNameConfig: ChildConfig = {
name: 'name',
text: '事件',
type: (mForm: FormState | undefined, { formValue }: any) => {
type: (mForm, { formValue }: any) => {
if (
props.config.src !== 'component' ||
(formValue.type === 'page-fragment-container' && formValue.pageFragmentId)
@ -213,12 +212,12 @@ const actionTypeConfig = computed(() => {
//
const targetCompConfig = computed(() => {
const defaultTargetCompConfig: UISelectConfig = {
const defaultTargetCompConfig = {
name: 'to',
text: '联动组件',
type: 'ui-select',
display: (_mForm, { model }) => model.actionType === ActionType.COMP,
onChange: (_MForm, _v, { setModel }) => {
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.actionType === ActionType.COMP,
onChange: (MForm: FormState, v: string, { setModel }: OnChangeHandlerData) => {
setModel('method', '');
},
};
@ -227,10 +226,10 @@ const targetCompConfig = computed(() => {
//
const compActionConfig = computed(() => {
const defaultCompActionConfig: DynamicTypeConfig = {
const defaultCompActionConfig: ChildConfig = {
name: 'method',
text: '动作',
type: (mForm: FormState | undefined, { model }: any) => {
type: (mForm, { model }: any) => {
const to = editorService.getNodeById(model.to);
if (to && to.type === 'page-fragment-container' && to.pageFragmentId) {
@ -240,7 +239,7 @@ const compActionConfig = computed(() => {
return 'select';
},
checkStrictly: () => props.config.src !== 'component',
display: (mForm: FormState | undefined, { model }: any) => model.actionType === ActionType.COMP,
display: (mForm, { model }: any) => model.actionType === ActionType.COMP,
options: (mForm: FormState, { model }: any) => {
const node = editorService.getNodeById(model.to);
if (!node?.type) return [];
@ -305,68 +304,62 @@ const dataSourceActionConfig = computed(() => {
});
//
const tableConfig = computed(
() =>
defineFormItem({
type: 'table',
name: 'events',
items: [
{
name: 'name',
label: '事件名',
type: eventNameConfig.value.type,
options: (mForm: FormState, { formValue }: any) =>
eventsService.getEvent(formValue.type).map((option: any) => ({
text: option.label,
value: option.value,
})),
},
{
name: 'to',
label: '联动组件',
type: 'ui-select',
},
{
name: 'method',
label: '动作',
type: compActionConfig.value.type,
options: (mForm: FormState, { model }: any) => {
const node = editorService.getNodeById(model.to);
if (!node?.type) return [];
const tableConfig = computed(() => ({
type: 'table',
name: 'events',
items: [
{
name: 'name',
label: '事件名',
type: eventNameConfig.value.type,
options: (mForm: FormState, { formValue }: any) =>
eventsService.getEvent(formValue.type).map((option: any) => ({
text: option.label,
value: option.value,
})),
},
{
name: 'to',
label: '联动组件',
type: 'ui-select',
},
{
name: 'method',
label: '动作',
type: compActionConfig.value.type,
options: (mForm: FormState, { model }: any) => {
const node = editorService.getNodeById(model.to);
if (!node?.type) return [];
return eventsService.getMethod(node.type, model.to).map((option: any) => ({
text: option.label,
value: option.value,
}));
},
},
],
}) as TableConfig,
);
return eventsService.getMethod(node.type, model.to).map((option: any) => ({
text: option.label,
value: option.value,
}));
},
},
],
}));
//
const actionsConfig = computed(
() =>
defineFormItem({
type: 'panel',
const actionsConfig = computed<PanelConfig>(() => ({
type: 'panel',
items: [
{
type: 'group-list',
name: 'actions',
expandAll: true,
enableToggleMode: false,
titlePrefix: '动作',
items: [
{
type: 'group-list',
name: 'actions',
expandAll: true,
enableToggleMode: false,
titlePrefix: '动作',
items: [
actionTypeConfig.value,
targetCompConfig.value,
compActionConfig.value,
codeActionConfig.value,
dataSourceActionConfig.value,
],
},
actionTypeConfig.value,
targetCompConfig.value,
compActionConfig.value,
codeActionConfig.value,
dataSourceActionConfig.value,
],
}) as PanelConfig,
);
},
],
}));
//
const isOldVersion = computed(() => {

View File

@ -26,7 +26,7 @@ import type { StyleSchema } from '@tmagic/schema';
import MIcon from '@editor/components/Icon.vue';
import { Background, Border, Font, Layout, Position, Transform } from './pro/';
import { Background, Border, Font, Layout, Position } from './pro/';
defineOptions({
name: 'MFieldsStyleSetter',
@ -60,10 +60,6 @@ const list = [
title: '边框与圆角',
component: Border,
},
{
title: '变形',
component: Transform,
},
];
const collapseValue = shallowRef(

View File

@ -28,10 +28,10 @@
<script lang="ts" setup>
import { TMagicButton, TMagicInput } from '@tmagic/design';
import type { FieldProps, StyleSetterConfig } from '@tmagic/form';
import type { FieldProps, FormItem } from '@tmagic/form';
const emit = defineEmits(['change']);
defineProps<FieldProps<StyleSetterConfig>>();
defineProps<FieldProps<{ type: 'style-setter' } & FormItem>>();
const horizontalList = [
{

View File

@ -39,48 +39,46 @@
import { computed, ref } from 'vue';
import type { ContainerChangeEventData, FormValue } from '@tmagic/form';
import { defineFormItem, MContainer } from '@tmagic/form';
import { MContainer } from '@tmagic/form';
import type { StyleSchema } from '@tmagic/schema';
const direction = ref('');
const config = computed(() =>
defineFormItem({
items: [
{
name: `border${direction.value}Width`,
text: '边框宽度',
labelWidth: '68px',
type: 'data-source-field-select',
fieldConfig: {
type: 'text',
},
const config = computed(() => ({
items: [
{
name: `border${direction.value}Width`,
text: '边框宽度',
labelWidth: '68px',
type: 'data-source-field-select',
fieldConfig: {
type: 'text',
},
{
name: `border${direction.value}Color`,
text: '边框颜色',
labelWidth: '68px',
type: 'data-source-field-select',
fieldConfig: {
type: 'colorPicker',
},
},
{
name: `border${direction.value}Color`,
text: '边框颜色',
labelWidth: '68px',
type: 'data-source-field-select',
fieldConfig: {
type: 'colorPicker',
},
{
name: `border${direction.value}Style`,
text: '边框样式',
labelWidth: '68px',
type: 'data-source-field-select',
fieldConfig: {
type: 'select',
options: ['solid', 'dashed', 'dotted'].map((item) => ({
value: item,
text: item,
})),
},
},
{
name: `border${direction.value}Style`,
text: '边框样式',
labelWidth: '68px',
type: 'data-source-field-select',
fieldConfig: {
type: 'select',
options: ['solid', 'dashed', 'dotted'].map((item) => ({
value: item,
text: item,
})),
},
],
}),
);
},
],
}));
const selectDirection = (d?: string) => (direction.value = d || '');

View File

@ -5,7 +5,7 @@
<script lang="ts" setup>
import { markRaw } from 'vue';
import { type ContainerChangeEventData, defineFormItem, MContainer } from '@tmagic/form';
import { ContainerChangeEventData, MContainer } from '@tmagic/form';
import type { StyleSchema } from '@tmagic/schema';
import BackgroundPosition from '../components/BackgroundPosition.vue';
@ -21,7 +21,7 @@ const emit = defineEmits<{
change: [v: StyleSchema, eventData: ContainerChangeEventData];
}>();
const config = defineFormItem({
const config = {
items: [
{
name: 'backgroundColor',
@ -39,7 +39,7 @@ const config = defineFormItem({
type: 'data-source-field-select',
fieldConfig: {
type: 'img-upload',
} as any,
},
},
{
name: 'backgroundSize',
@ -74,7 +74,7 @@ const config = defineFormItem({
labelWidth: '68px',
},
],
});
};
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
emit('change', value, eventData);

View File

@ -4,7 +4,7 @@
</template>
<script lang="ts" setup>
import { type ContainerChangeEventData, defineFormItem, MContainer } from '@tmagic/form';
import { type ContainerChangeEventData, MContainer } from '@tmagic/form';
import type { StyleSchema } from '@tmagic/schema';
import Border from '../components/Border.vue';
@ -19,7 +19,7 @@ const emit = defineEmits<{
change: [v: StyleSchema, eventData: ContainerChangeEventData];
}>();
const config = defineFormItem({
const config = {
items: [
{
labelWidth: '68px',
@ -31,7 +31,7 @@ const config = defineFormItem({
},
},
],
});
};
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
emit('change', value, eventData);

View File

@ -5,7 +5,7 @@
<script lang="ts" setup>
import { markRaw } from 'vue';
import { type ContainerChangeEventData, defineFormItem, MContainer } from '@tmagic/form';
import { ContainerChangeEventData, MContainer } from '@tmagic/form';
import type { StyleSchema } from '@tmagic/schema';
import { AlignCenter, AlignLeft, AlignRight } from '../icons/text-align';
@ -20,7 +20,7 @@ const emit = defineEmits<{
change: [v: StyleSchema, eventData: ContainerChangeEventData];
}>();
const config = defineFormItem({
const config = {
items: [
{
type: 'row',
@ -86,7 +86,7 @@ const config = defineFormItem({
],
},
],
});
};
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
emit('change', value, eventData);

View File

@ -12,8 +12,8 @@
<script lang="ts" setup>
import { markRaw } from 'vue';
import type { ChildConfig, ContainerChangeEventData } from '@tmagic/form';
import { defineFormItem, MContainer } from '@tmagic/form';
import type { ContainerChangeEventData, FormState } from '@tmagic/form';
import { MContainer } from '@tmagic/form';
import type { StyleSchema } from '@tmagic/schema';
import Box from '../components/Box.vue';
@ -42,7 +42,7 @@ const emit = defineEmits<{
change: [v: string | StyleSchema, eventData: ContainerChangeEventData];
}>();
const config = defineFormItem({
const config = {
items: [
{
name: 'display',
@ -74,7 +74,7 @@ const config = defineFormItem({
tooltip: '垂直方向 起点在下沿 column-reverse',
},
],
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.display === 'flex',
},
{
name: 'justifyContent',
@ -89,7 +89,7 @@ const config = defineFormItem({
{ value: 'space-between', icon: markRaw(JustifyContentSpaceBetween), tooltip: '两端对齐 space-between' },
{ value: 'space-around', icon: markRaw(JustifyContentSpaceAround), tooltip: '横向平分 space-around' },
],
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.display === 'flex',
},
{
name: 'alignItems',
@ -104,7 +104,7 @@ const config = defineFormItem({
{ value: 'space-between', icon: markRaw(JustifyContentSpaceBetween), tooltip: '两端对齐 space-between' },
{ value: 'space-around', icon: markRaw(JustifyContentSpaceAround), tooltip: '横向平分 space-around' },
],
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.display === 'flex',
},
{
name: 'flexWrap',
@ -117,7 +117,7 @@ const config = defineFormItem({
{ value: 'wrap', text: '正换行', tooltip: '第一行在上方 wrap' },
{ value: 'wrap-reverse', text: '逆换行', tooltip: '第一行在下方 wrap-reverse' },
],
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.display === 'flex',
},
{
type: 'row',
@ -180,7 +180,7 @@ const config = defineFormItem({
],
},
],
}) as ChildConfig;
};
const change = (value: string | StyleSchema, eventData: ContainerChangeEventData) => {
emit('change', value, eventData);

View File

@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
import { type ContainerChangeEventData, defineFormItem, MContainer } from '@tmagic/form';
import { ContainerChangeEventData, MContainer } from '@tmagic/form';
import type { StyleSchema } from '@tmagic/schema';
const props = defineProps<{
@ -24,7 +24,7 @@ const positionText: Record<string, string> = {
sticky: '粘性定位',
};
const config = defineFormItem({
const config = {
items: [
{
name: 'position',
@ -95,7 +95,7 @@ const config = defineFormItem({
},
},
],
});
};
const change = (value: string | StyleSchema, eventData: ContainerChangeEventData) => {
emit('change', value, eventData);

View File

@ -1,54 +0,0 @@
<template>
<MContainer :config="config" :model="values" :size="size" :disabled="disabled" @change="change"></MContainer>
</template>
<script lang="ts" setup>
import { type ContainerChangeEventData, defineFormItem, MContainer } from '@tmagic/form';
import type { StyleSchema } from '@tmagic/schema';
defineProps<{
values: Partial<StyleSchema>;
disabled?: boolean;
size?: 'large' | 'default' | 'small';
}>();
const emit = defineEmits<{
change: [v: StyleSchema, eventData: ContainerChangeEventData];
}>();
const config = defineFormItem({
items: [
{
name: 'transform',
items: [
{
name: 'rotate',
text: '旋转角度',
labelWidth: '68px',
type: 'data-source-field-select',
checkStrictly: false,
dataSourceFieldType: ['string', 'number'],
fieldConfig: {
type: 'text',
},
},
{
name: 'scale',
text: '缩放',
labelWidth: '68px',
type: 'data-source-field-select',
checkStrictly: false,
dataSourceFieldType: ['string', 'number'],
fieldConfig: {
type: 'text',
},
},
],
},
],
});
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
emit('change', value, eventData);
};
</script>

View File

@ -3,4 +3,3 @@ export { default as Font } from './Font.vue';
export { default as Layout } from './Layout.vue';
export { default as Position } from './Position.vue';
export { default as Border } from './Border.vue';
export { default as Transform } from './Transform.vue';

View File

@ -8,7 +8,7 @@ import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
import type { Services } from '@editor/type';
export const useCodeBlockEdit = (codeBlockService: Services['codeBlockService']) => {
const codeConfig = ref<Omit<CodeBlockContent, 'content'> & { content: string }>();
const codeConfig = ref<CodeBlockContent>();
const codeId = ref<string>();
const codeBlockEditorRef = useTemplateRef<InstanceType<typeof CodeBlockEditor>>('codeBlockEditor');
@ -36,14 +36,10 @@ export const useCodeBlockEdit = (codeBlockService: Services['codeBlockService'])
return;
}
let codeContent = '';
let codeContent = codeBlock.content;
if (codeBlock.content) {
if (typeof codeBlock.content !== 'string') {
codeContent = codeBlock.content.toString();
} else {
codeContent = codeBlock.content;
}
if (typeof codeContent !== 'string') {
codeContent = codeContent.toString();
}
codeConfig.value = {

View File

@ -59,12 +59,10 @@ export const useStage = (stageOptions: StageOptions) => {
},
);
const hGuidesCache = getGuideLineFromCache(getGuideLineKey(H_GUIDE_LINE_STORAGE_KEY));
const vGuidesCache = getGuideLineFromCache(getGuideLineKey(V_GUIDE_LINE_STORAGE_KEY));
stage.mask?.setGuides([hGuidesCache, vGuidesCache]);
uiService.set('hasGuides', hGuidesCache.length > 0 || vGuidesCache.length > 0);
stage.mask?.setGuides([
getGuideLineFromCache(getGuideLineKey(H_GUIDE_LINE_STORAGE_KEY)),
getGuideLineFromCache(getGuideLineKey(V_GUIDE_LINE_STORAGE_KEY)),
]);
stage.on('page-el-update', () => {
editorService.set('stageLoading', false);
@ -126,11 +124,6 @@ export const useStage = (stageOptions: StageOptions) => {
stage.on('change-guides', (e) => {
uiService.set('showGuides', true);
uiService.set(
'hasGuides',
(stage.mask?.horizontalGuidelines.length ?? 0) > 0 || (stage.mask?.verticalGuidelines.length ?? 0) > 0,
);
if (!root.value || !page.value) return;
const storageKey = getGuideLineKey(

View File

@ -15,6 +15,38 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { App } from 'vue';
import type { DesignPluginOptions } from '@tmagic/design';
import designPlugin from '@tmagic/design';
import type { FormInstallOptions } from '@tmagic/form';
import formPlugin from '@tmagic/form';
import tablePlugin from '@tmagic/table';
import Code from './fields/Code.vue';
import CodeLink from './fields/CodeLink.vue';
import CodeSelect from './fields/CodeSelect.vue';
import CodeSelectCol from './fields/CodeSelectCol.vue';
import CondOpSelect from './fields/CondOpSelect.vue';
import DataSourceFields from './fields/DataSourceFields.vue';
import DataSourceFieldSelect from './fields/DataSourceFieldSelect/Index.vue';
import DataSourceInput from './fields/DataSourceInput.vue';
import DataSourceMethods from './fields/DataSourceMethods.vue';
import DataSourceMethodSelect from './fields/DataSourceMethodSelect.vue';
import DataSourceMocks from './fields/DataSourceMocks.vue';
import DataSourceSelect from './fields/DataSourceSelect.vue';
import DisplayConds from './fields/DisplayConds.vue';
import EventSelect from './fields/EventSelect.vue';
import KeyValue from './fields/KeyValue.vue';
import PageFragmentSelect from './fields/PageFragmentSelect.vue';
import StyleSetter from './fields/StyleSetter/Index.vue';
import uiSelect from './fields/UISelect.vue';
import CodeEditor from './layouts/CodeEditor.vue';
import { setEditorConfig } from './utils/config';
import Editor from './Editor.vue';
import type { EditorInstallOptions } from './type';
import './theme/index.scss';
export * from '@tmagic/form';
export { default as formPlugin } from '@tmagic/form';
@ -78,4 +110,44 @@ export { default as DisplayConds } from './fields/DisplayConds.vue';
export { default as CondOpSelect } from './fields/CondOpSelect.vue';
export { default as StyleSetter } from './fields/StyleSetter/Index.vue';
export { default } from './plugin';
const defaultInstallOpt: EditorInstallOptions = {
// eslint-disable-next-line no-eval
parseDSL: (dsl: string) => eval(dsl),
customCreateMonacoEditor: (monaco, codeEditorEl, options) => monaco.editor.create(codeEditorEl, options),
customCreateMonacoDiffEditor: (monaco, codeEditorEl, options) =>
monaco.editor.createDiffEditor(codeEditorEl, options),
};
export default {
install: (app: App, opt?: Partial<EditorInstallOptions | DesignPluginOptions | FormInstallOptions>): void => {
const option = Object.assign(defaultInstallOpt, opt || {});
app.use(designPlugin, opt || {});
app.use(formPlugin, opt || {});
app.use(tablePlugin);
app.config.globalProperties.$TMAGIC_EDITOR = option;
setEditorConfig(option);
app.component(`${Editor.name || 'MEditor'}`, Editor);
app.component('magic-code-editor', CodeEditor);
app.component('m-fields-ui-select', uiSelect);
app.component('m-fields-code-link', CodeLink);
app.component('m-fields-vs-code', Code);
app.component('m-fields-code-select', CodeSelect);
app.component('m-fields-code-select-col', CodeSelectCol);
app.component('m-fields-event-select', EventSelect);
app.component('m-fields-data-source-fields', DataSourceFields);
app.component('m-fields-data-source-mocks', DataSourceMocks);
app.component('m-fields-key-value', KeyValue);
app.component('m-fields-data-source-input', DataSourceInput);
app.component('m-fields-data-source-select', DataSourceSelect);
app.component('m-fields-data-source-methods', DataSourceMethods);
app.component('m-fields-data-source-method-select', DataSourceMethodSelect);
app.component('m-fields-data-source-field-select', DataSourceFieldSelect);
app.component('m-fields-page-fragment-select', PageFragmentSelect);
app.component('m-fields-display-conds', DisplayConds);
app.component('m-fields-cond-op-select', CondOpSelect);
app.component('m-form-style-setter', StyleSetter);
},
};

View File

@ -233,7 +233,7 @@ export const initServiceEvents = (
((event: 'update:modelValue', value: MApp | null) => void),
{ editorService, codeBlockService, dataSourceService, depService }: Services,
) => {
let getTMagicAppPromise: Promise<TMagicCore | undefined> | null = null;
let getTMagicAppPrimise: Promise<TMagicCore | undefined> | null = null;
const getTMagicApp = async (): Promise<TMagicCore | undefined> => {
const stage = await getStage();
@ -246,11 +246,11 @@ export const initServiceEvents = (
return renderer.runtime.getApp?.();
}
if (getTMagicAppPromise) {
return getTMagicAppPromise;
if (getTMagicAppPrimise) {
return getTMagicAppPrimise;
}
getTMagicAppPromise = new Promise<TMagicCore | undefined>((resolve) => {
getTMagicAppPrimise = new Promise<TMagicCore | undefined>((resolve) => {
// 设置 10s 超时
const timeout = globalThis.setTimeout(() => {
resolve(void 0);
@ -264,7 +264,7 @@ export const initServiceEvents = (
});
});
return getTMagicAppPromise;
return getTMagicAppPrimise;
};
const updateStageNodes = (nodes: MComponent[]) => {
@ -560,7 +560,7 @@ export const initServiceEvents = (
depService.clear(nodes);
};
// 历史记录变化时,需要重新收集依赖
// 由于历史记录变化是更新整个page所以历史记录变化时,需要重新收集依赖
const historyChangeHandler = (page: MPage | MPageFragment) => {
collectIdle([page], true).then(() => {
updateStageNode(page);

View File

@ -37,7 +37,6 @@ const columnWidth = computed(() => uiService.get('columnWidth'));
const keys = Object.values(ColumnLayout);
const showGuides = computed((): boolean => uiService.get('showGuides'));
const hasGuides = computed((): boolean => uiService.get('hasGuides'));
const showRule = computed((): boolean => uiService.get('showRule'));
const zoom = computed((): number => uiService.get('zoom'));
@ -144,7 +143,6 @@ const getConfig = (item: MenuItem): (MenuButton | MenuComponent)[] => {
});
break;
case 'guides':
if (!hasGuides.value) break;
config.push({
type: 'button',
className: 'guides',

View File

@ -80,7 +80,7 @@ const props = withDefaults(
let stage: StageCore | null = null;
let runtime: Runtime | null = null;
const { editorService, uiService, keybindingService, stageOverlayService } = useServices();
const { editorService, uiService, keybindingService } = useServices();
const stageLoading = computed(() => editorService.get('stageLoading'));
@ -97,60 +97,6 @@ const page = computed(() => editorService.get('page'));
const zoom = computed(() => uiService.get('zoom'));
const node = computed(() => editorService.get('node'));
/**
* 判断元素是否被非页面级的滚动容器裁剪未完整显示
*
* 从元素向上遍历祖先节点跳过页面/页面片容器
* 检查是否存在设置了 overflow 的滚动容器将该元素裁剪
* 只有元素未被完整显示时才需要打开 overlay 以展示完整内容
*/
const isClippedByScrollContainer = (el: HTMLElement): boolean => {
const win = el.ownerDocument.defaultView;
if (!win) return false;
// id
const root = editorService.get('root');
const pageIds = new Set(root?.items?.map((item) => `${item.id}`) ?? []);
// el
const elId = getIdFromEl()(el);
if (elId && pageIds.has(elId)) return false;
let parent = el.parentElement;
while (parent && parent !== el.ownerDocument.documentElement) {
const parentId = getIdFromEl()(parent);
//
if (parentId && pageIds.has(parentId)) {
return false;
}
const { overflowX, overflowY } = win.getComputedStyle(parent);
if (
['auto', 'scroll', 'hidden'].includes(overflowX) ||
['auto', 'scroll', 'hidden'].includes(overflowY) ||
parent.scrollWidth > parent.clientWidth ||
parent.scrollHeight > parent.clientHeight
) {
//
const elRect = el.getBoundingClientRect();
const containerRect = parent.getBoundingClientRect();
if (
elRect.top < containerRect.top ||
elRect.left < containerRect.left ||
elRect.bottom > containerRect.bottom ||
elRect.right > containerRect.right
) {
return true;
}
}
parent = parent.parentElement;
}
return false;
};
watchEffect(() => {
if (stage || !page.value) return;
@ -163,40 +109,6 @@ watchEffect(() => {
stageWrapRef.value?.container?.focus();
});
stage.on('dblclick', async (event: MouseEvent) => {
if (props.stageOptions.beforeDblclick) {
const result = await props.stageOptions.beforeDblclick(event);
if (result === false) return;
}
const el = (await stage?.actionManager?.getElementFromPoint(event)) || null;
if (!el) return;
const id = getIdFromEl()(el);
if (id) {
const node = editorService.getNodeById(id);
if (node?.type === 'page-fragment-container' && node.pageFragmentId) {
await editorService.select(node.pageFragmentId);
return;
}
}
if (!props.disabledStageOverlay && isClippedByScrollContainer(el)) {
stageOverlayService.openOverlay(el);
return;
}
const nextEl = (await stage?.actionManager?.getNextElementFromPoint(event)) || null;
if (nextEl) {
const nextId = getIdFromEl()(nextEl);
if (nextId) {
await editorService.select(nextId);
editorService.get('stage')?.select(nextId);
}
}
});
editorService.set('stage', markRaw(stage));
stage.mount(stageContainerEl.value);

View File

@ -46,7 +46,12 @@ const style = computed(() => ({
}));
watch(stage, (stage) => {
if (!stage) {
if (stage) {
stage.on('dblclick', async (event: MouseEvent) => {
const el = (await stage.actionManager?.getElementFromPoint(event)) || null;
stageOverlayService.openOverlay(el);
});
} else {
stageOverlayService.closeOverlay();
}
});

View File

@ -1,92 +0,0 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { App } from 'vue';
import type { DesignPluginOptions } from '@tmagic/design';
import designPlugin from '@tmagic/design';
import type { FormInstallOptions } from '@tmagic/form';
import formPlugin from '@tmagic/form';
import tablePlugin from '@tmagic/table';
import Code from './fields/Code.vue';
import CodeLink from './fields/CodeLink.vue';
import CodeSelect from './fields/CodeSelect.vue';
import CodeSelectCol from './fields/CodeSelectCol.vue';
import CondOpSelect from './fields/CondOpSelect.vue';
import DataSourceFields from './fields/DataSourceFields.vue';
import DataSourceFieldSelect from './fields/DataSourceFieldSelect/Index.vue';
import DataSourceInput from './fields/DataSourceInput.vue';
import DataSourceMethods from './fields/DataSourceMethods.vue';
import DataSourceMethodSelect from './fields/DataSourceMethodSelect.vue';
import DataSourceMocks from './fields/DataSourceMocks.vue';
import DataSourceSelect from './fields/DataSourceSelect.vue';
import DisplayConds from './fields/DisplayConds.vue';
import EventSelect from './fields/EventSelect.vue';
import KeyValue from './fields/KeyValue.vue';
import PageFragmentSelect from './fields/PageFragmentSelect.vue';
import StyleSetter from './fields/StyleSetter/Index.vue';
import uiSelect from './fields/UISelect.vue';
import CodeEditor from './layouts/CodeEditor.vue';
import { setEditorConfig } from './utils/config';
import Editor from './Editor.vue';
import type { EditorInstallOptions } from './type';
import './theme/index.scss';
const defaultInstallOpt: EditorInstallOptions = {
// eslint-disable-next-line no-eval
parseDSL: (dsl: string) => eval(dsl),
customCreateMonacoEditor: (monaco, codeEditorEl, options) => monaco.editor.create(codeEditorEl, options),
customCreateMonacoDiffEditor: (monaco, codeEditorEl, options) =>
monaco.editor.createDiffEditor(codeEditorEl, options),
};
export default {
install: (app: App, opt?: Partial<EditorInstallOptions | DesignPluginOptions | FormInstallOptions>): void => {
const option = Object.assign(defaultInstallOpt, opt || {});
app.use(designPlugin, opt || {});
app.use(formPlugin, opt || {});
app.use(tablePlugin);
app.config.globalProperties.$TMAGIC_EDITOR = option;
setEditorConfig(option);
app.component(`${Editor.name || 'MEditor'}`, Editor);
app.component('magic-code-editor', CodeEditor);
app.component('m-fields-ui-select', uiSelect);
app.component('m-fields-code-link', CodeLink);
app.component('m-fields-vs-code', Code);
app.component('m-fields-code-select', CodeSelect);
app.component('m-fields-code-select-col', CodeSelectCol);
app.component('m-fields-event-select', EventSelect);
app.component('m-fields-data-source-fields', DataSourceFields);
app.component('m-fields-data-source-mocks', DataSourceMocks);
app.component('m-fields-key-value', KeyValue);
app.component('m-fields-data-source-input', DataSourceInput);
app.component('m-fields-data-source-select', DataSourceSelect);
app.component('m-fields-data-source-methods', DataSourceMethods);
app.component('m-fields-data-source-method-select', DataSourceMethodSelect);
app.component('m-fields-data-source-field-select', DataSourceFieldSelect);
app.component('m-fields-page-fragment-select', PageFragmentSelect);
app.component('m-fields-display-conds', DisplayConds);
app.component('m-fields-cond-op-select', CondOpSelect);
app.component('m-form-style-setter', StyleSetter);
},
};

View File

@ -17,13 +17,23 @@
*/
import { reactive, toRaw } from 'vue';
import { cloneDeep, isObject, mergeWith, uniq } from 'lodash-es';
import { cloneDeep, get, isObject, mergeWith, uniq } from 'lodash-es';
import type { Writable } from 'type-fest';
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
import { NodeType } from '@tmagic/core';
import { NodeType, Target, Watcher } from '@tmagic/core';
import type { ChangeRecord } from '@tmagic/form';
import { isFixed } from '@tmagic/stage';
import { getNodeInfo, getNodePath, isPage, isPageFragment } from '@tmagic/utils';
import {
calcValueByFontsize,
getElById,
getNodeInfo,
getNodePath,
isNumber,
isPage,
isPageFragment,
isPop,
} from '@tmagic/utils';
import BaseService from '@editor/services//BaseService';
import propsService from '@editor/services//props';
@ -32,39 +42,69 @@ import storageService, { Protocol } from '@editor/services/storage';
import type {
AddMNode,
AsyncHookPlugin,
AsyncMethodName,
EditorEvents,
EditorNodeInfo,
HistoryOpType,
PastePosition,
StepValue,
StoreState,
StoreStateKey,
} from '@editor/type';
import { canUsePluginMethods, LayerOffset, Layout } from '@editor/type';
import { LayerOffset, Layout } from '@editor/type';
import {
calcAlignCenterStyle,
calcLayerTargetIndex,
calcMoveStyle,
classifyDragSources,
collectRelatedNodes,
change2Fixed,
COPY_STORAGE_KEY,
editorNodeMergeCustomizer,
Fixed2Other,
fixNodePosition,
getInitPositionStyle,
getNodeIndex,
getPageFragmentList,
getPageList,
moveItemsInContainer,
resolveSelectedNode,
setChildrenLayout,
setLayout,
toggleFixedPosition,
} from '@editor/utils/editor';
import type { HistoryOpContext } from '@editor/utils/editor-history';
import { applyHistoryAddOp, applyHistoryRemoveOp, applyHistoryUpdateOp } from '@editor/utils/editor-history';
import { beforePaste, getAddParent } from '@editor/utils/operator';
export interface EditorEvents {
'root-change': [value: StoreState['root'], preValue?: StoreState['root']];
select: [node: MNode | null];
add: [nodes: MNode[]];
remove: [nodes: MNode[]];
update: [nodes: { newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }[]];
'move-layer': [offset: number | LayerOffset];
'drag-to': [data: { targetIndex: number; configs: MNode | MNode[]; targetParent: MContainer }];
'history-change': [data: MPage | MPageFragment];
}
const canUsePluginMethods = {
async: [
'getLayout',
'highlight',
'select',
'multiSelect',
'doAdd',
'add',
'doRemove',
'remove',
'doUpdate',
'update',
'sort',
'copy',
'paste',
'doPaste',
'doAlignCenter',
'alignCenter',
'moveLayer',
'moveToContainer',
'dragTo',
'undo',
'redo',
'move',
] as const,
sync: [],
};
type AsyncMethodName = Writable<(typeof canUsePluginMethods)['async']>;
class Editor extends BaseService {
public state: StoreState = reactive({
root: null,
@ -81,7 +121,6 @@ class Editor extends BaseService {
disabledMultiSelect: false,
});
private isHistoryStateChange = false;
private selectionBeforeOp: Id[] | null = null;
constructor() {
super(
@ -351,8 +390,6 @@ class Editor extends BaseService {
* @returns
*/
public async add(addNode: AddMNode | MNode[], parent?: MContainer | null): Promise<MNode | MNode[]> {
this.captureSelectionBeforeOp();
const stage = this.get('stage');
// 新增多个组件只存在于粘贴多个组件,粘贴的是一个完整的config,所以不再需要getPropsValue
@ -398,21 +435,7 @@ class Editor extends BaseService {
}
if (!(isPage(newNodes[0]) || isPageFragment(newNodes[0]))) {
const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
this.pushOpHistory(
'add',
{
nodes: newNodes.map((n) => cloneDeep(toRaw(n))),
parentId: (this.getParentById(newNodes[0].id, false) ?? this.get('root'))!.id,
indexMap: Object.fromEntries(
newNodes.map((n) => {
const p = this.getParentById(n.id, false) as MContainer;
return [n.id, p ? getNodeIndex(n.id, p) : -1];
}),
),
},
{ name: pageForOp?.name || '', id: pageForOp!.id },
);
this.pushHistoryState();
}
this.emit('add', newNodes);
@ -475,33 +498,13 @@ class Editor extends BaseService {
* @param {Object} node
*/
public async remove(nodeOrNodeList: MNode | MNode[]): Promise<void> {
this.captureSelectionBeforeOp();
const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
const removedItems: { node: MNode; parentId: Id; index: number }[] = [];
let pageForOp: { name: string; id: Id } | null = null;
if (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) {
for (const n of nodes) {
const { parent, node: curNode, page } = this.getNodeInfo(n.id, false);
if (parent && curNode) {
if (!pageForOp && page) {
pageForOp = { name: page.name || '', id: page.id };
}
const idx = getNodeIndex(curNode.id, parent);
removedItems.push({
node: cloneDeep(toRaw(curNode)),
parentId: parent.id,
index: typeof idx === 'number' ? idx : -1,
});
}
}
}
await Promise.all(nodes.map((node) => this.doRemove(node)));
if (removedItems.length > 0 && pageForOp) {
this.pushOpHistory('remove', { removedItems }, pageForOp);
if (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) {
// 更新历史记录
this.pushHistoryState();
}
this.emit('remove', nodes);
@ -522,9 +525,21 @@ class Editor extends BaseService {
const node = toRaw(info.node);
let newConfig = await toggleFixedPosition(toRaw(config), node, root, this.getLayout);
let newConfig = await this.toggleFixedPosition(toRaw(config), node, root);
newConfig = mergeWith(cloneDeep(node), newConfig, editorNodeMergeCustomizer);
newConfig = mergeWith(cloneDeep(node), newConfig, (objValue, srcValue, key, object: any, source: any) => {
if (typeof srcValue === 'undefined' && Object.hasOwn(source, key)) {
return '';
}
if (isObject(srcValue) && Array.isArray(objValue)) {
// 原来的配置是数组,新的配置是对象,则直接使用新的值
return srcValue;
}
if (Array.isArray(srcValue)) {
return srcValue;
}
});
if (!newConfig.type) throw new Error('配置缺少type值');
@ -582,28 +597,12 @@ class Editor extends BaseService {
config: MNode | MNode[],
data: { changeRecords?: ChangeRecord[] } = {},
): Promise<MNode | MNode[]> {
this.captureSelectionBeforeOp();
const nodes = Array.isArray(config) ? config : [config];
const updateData = await Promise.all(nodes.map((node) => this.doUpdate(node, data)));
if (updateData[0].oldNode?.type !== NodeType.ROOT) {
const curNodes = this.get('nodes');
if (!this.isHistoryStateChange && curNodes.length) {
const pageForOp = this.getNodeInfo(nodes[0].id, false).page;
this.pushOpHistory(
'update',
{
updatedItems: updateData.map((d) => ({
oldNode: cloneDeep(d.oldNode),
newNode: cloneDeep(toRaw(d.newNode)),
})),
},
{ name: pageForOp?.name || '', id: pageForOp!.id },
);
}
this.isHistoryStateChange = false;
this.pushHistoryState();
}
this.emit('update', updateData);
@ -617,8 +616,6 @@ class Editor extends BaseService {
* @returns void
*/
public async sort(id1: Id, id2: Id): Promise<void> {
this.captureSelectionBeforeOp();
const root = this.get('root');
if (!root) throw new Error('root为空');
@ -643,6 +640,9 @@ class Editor extends BaseService {
parentId: parent.id,
root: cloneDeep(root),
});
this.addModifiedNodeId(parent.id);
this.pushHistoryState();
}
/**
@ -664,8 +664,31 @@ class Editor extends BaseService {
public copyWithRelated(config: MNode | MNode[], collectorOptions?: TargetOptions): void {
const copyNodes: MNode[] = Array.isArray(config) ? config : [config];
// 初始化复制组件相关的依赖收集器
if (collectorOptions && typeof collectorOptions.isTarget === 'function') {
collectRelatedNodes(copyNodes, collectorOptions, (id) => this.getNodeById(id));
const customTarget = new Target({
...collectorOptions,
});
const coperWatcher = new Watcher();
coperWatcher.addTarget(customTarget);
coperWatcher.collect(copyNodes, {}, true, collectorOptions.type);
Object.keys(customTarget.deps).forEach((nodeId: Id) => {
const node = this.getNodeById(nodeId);
if (!node) return;
customTarget!.deps[nodeId].keys.forEach((key) => {
const relateNodeId = get(node, key);
const isExist = copyNodes.find((node) => node.id === relateNodeId);
if (!isExist) {
const relateNode = this.getNodeById(relateNodeId);
if (relateNode) {
copyNodes.push(relateNode);
}
}
});
});
}
storageService.setItem(COPY_STORAGE_KEY, copyNodes, {
@ -710,16 +733,32 @@ class Editor extends BaseService {
public async doAlignCenter(config: MNode): Promise<MNode> {
const parent = this.getParentById(config.id);
if (!parent) throw new Error('找不到父节点');
const node = cloneDeep(toRaw(config));
const layout = await this.getLayout(parent, node);
const doc = this.get('stage')?.renderer?.contentWindow?.document;
const newStyle = calcAlignCenterStyle(node, parent, layout, doc);
if (layout === Layout.RELATIVE) {
return config;
}
if (!newStyle) return config;
if (!node.style) return config;
const stage = this.get('stage');
const doc = stage?.renderer?.contentWindow?.document;
if (doc) {
const el = getElById()(doc, node.id);
const parentEl = layout === Layout.FIXED ? doc.body : el?.offsetParent;
if (parentEl && el) {
node.style.left = calcValueByFontsize(doc, (parentEl.clientWidth - el.clientWidth) / 2);
node.style.right = '';
}
} else if (parent.style && isNumber(parent.style?.width) && isNumber(node.style?.width)) {
node.style.left = (parent.style.width - node.style.width) / 2;
node.style.right = '';
}
node.style = newStyle;
return node;
}
@ -750,8 +789,6 @@ class Editor extends BaseService {
* @param offset
*/
public async moveLayer(offset: number | LayerOffset): Promise<void> {
this.captureSelectionBeforeOp();
const root = this.get('root');
if (!root) throw new Error('root为空');
@ -764,16 +801,22 @@ class Editor extends BaseService {
const brothers: MNode[] = parent.items || [];
const index = brothers.findIndex((item) => `${item.id}` === `${node?.id}`);
// 流式布局与绝对定位布局操作的相反的
const layout = await this.getLayout(parent, node);
const isRelative = layout === Layout.RELATIVE;
const offsetIndex = calcLayerTargetIndex(index, offset, brothers.length, isRelative);
let offsetIndex: number;
if (offset === LayerOffset.TOP) {
offsetIndex = isRelative ? 0 : brothers.length;
} else if (offset === LayerOffset.BOTTOM) {
offsetIndex = isRelative ? brothers.length : 0;
} else {
offsetIndex = index + (isRelative ? -offset : offset);
}
if ((offsetIndex > 0 && offsetIndex > brothers.length) || offsetIndex < 0) {
return;
}
const oldParent = cloneDeep(toRaw(parent));
brothers.splice(index, 1);
brothers.splice(offsetIndex, 0, node);
@ -786,14 +829,7 @@ class Editor extends BaseService {
});
this.addModifiedNodeId(parent.id);
const pageForOp = this.getNodeInfo(node.id, false).page;
this.pushOpHistory(
'update',
{
updatedItems: [{ oldNode: oldParent, newNode: cloneDeep(toRaw(parent)) }],
},
{ name: pageForOp?.name || '', id: pageForOp!.id },
);
this.pushHistoryState();
this.emit('move-layer', offset);
}
@ -804,17 +840,12 @@ class Editor extends BaseService {
* @param targetId ID
*/
public async moveToContainer(config: MNode, targetId: Id): Promise<MNode | undefined> {
this.captureSelectionBeforeOp();
const root = this.get('root');
const { node, parent, page: pageForOp } = this.getNodeInfo(config.id, false);
const { node, parent } = this.getNodeInfo(config.id, false);
const target = this.getNodeById(targetId, false) as MContainer;
const stage = this.get('stage');
if (root && node && parent && stage) {
const oldSourceParent = cloneDeep(toRaw(parent));
const oldTarget = cloneDeep(toRaw(target));
const index = getNodeIndex(node.id, parent);
parent.items?.splice(index, 1);
@ -845,60 +876,62 @@ class Editor extends BaseService {
this.addModifiedNodeId(target.id);
this.addModifiedNodeId(parent.id);
this.pushOpHistory(
'update',
{
updatedItems: [
{ oldNode: oldSourceParent, newNode: cloneDeep(toRaw(parent)) },
{ oldNode: oldTarget, newNode: cloneDeep(toRaw(target)) },
],
},
{ name: pageForOp?.name || '', id: pageForOp!.id },
);
this.pushHistoryState();
return newConfig;
}
}
public async dragTo(config: MNode | MNode[], targetParent: MContainer, targetIndex: number) {
this.captureSelectionBeforeOp();
if (!targetParent || !Array.isArray(targetParent.items)) return;
const configs = Array.isArray(config) ? config : [config];
const beforeSnapshots = new Map<string, MNode>();
for (const cfg of configs) {
const { parent } = this.getNodeInfo(cfg.id, false);
if (parent && !beforeSnapshots.has(`${parent.id}`)) {
beforeSnapshots.set(`${parent.id}`, cloneDeep(toRaw(parent)));
}
}
if (!beforeSnapshots.has(`${targetParent.id}`)) {
beforeSnapshots.set(`${targetParent.id}`, cloneDeep(toRaw(targetParent)));
}
const sourceIndicesInTargetParent: number[] = [];
const sourceOutTargetParent: MNode[] = [];
const newLayout = await this.getLayout(targetParent);
const { sameParentIndices, crossParentConfigs, aborted } = classifyDragSources(configs, targetParent, (id, raw) =>
this.getNodeInfo(id, raw),
);
if (aborted) return;
for (const { config: crossConfig, parent } of crossParentConfigs) {
const layout = await this.getLayout(parent);
if (newLayout !== layout) {
setLayout(crossConfig, newLayout);
// eslint-disable-next-line no-restricted-syntax
forConfigs: for (const config of configs) {
const { parent, node: curNode } = this.getNodeInfo(config.id, false);
if (!parent || !curNode) {
continue;
}
const path = getNodePath(curNode.id, parent.items);
for (const node of path) {
if (targetParent.id === node.id) {
continue forConfigs;
}
}
const index = getNodeIndex(curNode.id, parent);
if (parent.id === targetParent.id) {
if (typeof index !== 'number' || index === -1) {
return;
}
sourceIndicesInTargetParent.push(index);
} else {
const layout = await this.getLayout(parent);
if (newLayout !== layout) {
setLayout(config, newLayout);
}
parent.items?.splice(index, 1);
sourceOutTargetParent.push(config);
this.addModifiedNodeId(parent.id);
}
const index = getNodeIndex(crossConfig.id, parent);
parent.items?.splice(index, 1);
this.addModifiedNodeId(parent.id);
}
moveItemsInContainer(sameParentIndices, targetParent, targetIndex);
moveItemsInContainer(sourceIndicesInTargetParent, targetParent, targetIndex);
crossParentConfigs.forEach(({ config: crossConfig }, index) => {
targetParent.items?.splice(targetIndex + index, 0, crossConfig);
this.addModifiedNodeId(crossConfig.id);
sourceOutTargetParent.forEach((config, index) => {
targetParent.items?.splice(targetIndex + index, 0, config);
this.addModifiedNodeId(config.id);
});
const page = this.get('page');
@ -913,40 +946,28 @@ class Editor extends BaseService {
});
}
const updatedItems: { oldNode: MNode; newNode: MNode }[] = [];
for (const oldNode of beforeSnapshots.values()) {
const newNode = this.getNodeById(oldNode.id, false);
if (newNode) {
updatedItems.push({ oldNode, newNode: cloneDeep(toRaw(newNode)) });
}
}
const pageForOp = this.getNodeInfo(configs[0].id, false).page;
this.pushOpHistory('update', { updatedItems }, { name: pageForOp?.name || '', id: pageForOp!.id });
this.pushHistoryState();
this.emit('drag-to', { targetIndex, configs, targetParent });
}
/**
*
* @returns
* @returns
*/
public async undo(): Promise<StepValue | null> {
const value = historyService.undo();
if (value) {
await this.applyHistoryOp(value, true);
}
await this.changeHistoryState(value);
return value;
}
/**
*
* @returns
* @returns
*/
public async redo(): Promise<StepValue | null> {
const value = historyService.redo();
if (value) {
await this.applyHistoryOp(value, false);
}
await this.changeHistoryState(value);
return value;
}
@ -954,10 +975,47 @@ class Editor extends BaseService {
const node = toRaw(this.get('node'));
if (!node || isPage(node)) return;
const newStyle = calcMoveStyle(node.style || {}, left, top);
if (!newStyle) return;
const { style, id, type } = node;
if (!style || !['absolute', 'fixed'].includes(style.position)) return;
await this.update({ id: node.id, type: node.type, style: newStyle });
const update = (style: { [key: string]: any }) =>
this.update({
id,
type,
style,
});
if (top) {
if (isNumber(style.top)) {
update({
...style,
top: Number(style.top) + Number(top),
bottom: '',
});
} else if (isNumber(style.bottom)) {
update({
...style,
bottom: Number(style.bottom) - Number(top),
top: '',
});
}
}
if (left) {
if (isNumber(style.left)) {
update({
...style,
left: Number(style.left) + Number(left),
right: '',
});
} else if (isNumber(style.right)) {
update({
...style,
right: Number(style.right) - Number(left),
left: '',
});
}
}
}
public resetState() {
@ -1010,89 +1068,70 @@ class Editor extends BaseService {
}
}
private captureSelectionBeforeOp() {
if (this.isHistoryStateChange || this.selectionBeforeOp) return;
this.selectionBeforeOp = this.get('nodes').map((n) => n.id);
}
private pushOpHistory(opType: HistoryOpType, extra: Partial<StepValue>, pageData: { name: string; id: Id }) {
if (this.isHistoryStateChange) {
this.selectionBeforeOp = null;
return;
private pushHistoryState() {
const curNode = cloneDeep(toRaw(this.get('node')));
const page = this.get('page');
if (!this.isHistoryStateChange && curNode && page) {
historyService.push({
data: cloneDeep(toRaw(page)),
modifiedNodeIds: this.get('modifiedNodeIds'),
nodeId: curNode.id,
});
}
const step: StepValue = {
data: pageData,
opType,
selectedBefore: this.selectionBeforeOp ?? [],
selectedAfter: this.get('nodes').map((n) => n.id),
modifiedNodeIds: new Map(this.get('modifiedNodeIds')),
...extra,
};
historyService.push(step);
this.selectionBeforeOp = null;
this.isHistoryStateChange = false;
}
/**
* /
* @param step
* @param reverse true = false =
*/
private async applyHistoryOp(step: StepValue, reverse: boolean) {
private async changeHistoryState(value: StepValue | null) {
if (!value) return;
this.isHistoryStateChange = true;
await this.update(value.data);
this.set('modifiedNodeIds', value.modifiedNodeIds);
setTimeout(() => {
if (!value.nodeId) return;
this.select(value.nodeId).then(() => {
this.get('stage')?.select(value.nodeId);
});
}, 0);
this.emit('history-change', value.data);
}
const root = this.get('root');
const stage = this.get('stage');
if (!root) return;
private async toggleFixedPosition(dist: MNode, src: MNode, root: MApp) {
const newConfig = cloneDeep(dist);
const ctx: HistoryOpContext = {
root,
stage,
getNodeById: (id, raw) => this.getNodeById(id, raw),
getNodeInfo: (id, raw) => this.getNodeInfo(id, raw),
setRoot: (r) => this.set('root', r),
setPage: (p) => this.set('page', p),
getPage: () => this.get('page'),
};
switch (step.opType) {
case 'add':
await applyHistoryAddOp(step, reverse, ctx);
break;
case 'remove':
await applyHistoryRemoveOp(step, reverse, ctx);
break;
case 'update':
await applyHistoryUpdateOp(step, reverse, ctx);
break;
if (!isPop(src) && newConfig.style?.position) {
if (isFixed(newConfig.style) && !isFixed(src.style || {})) {
newConfig.style = change2Fixed(newConfig, root);
} else if (!isFixed(newConfig.style) && isFixed(src.style || {})) {
newConfig.style = await Fixed2Other(newConfig, root, this.getLayout);
}
}
this.set('modifiedNodeIds', step.modifiedNodeIds);
const page = toRaw(this.get('page'));
if (page) {
const selectIds = reverse ? step.selectedBefore : step.selectedAfter;
setTimeout(() => {
if (!selectIds.length) return;
if (selectIds.length > 1) {
this.multiSelect(selectIds);
stage?.multiSelect(selectIds);
} else {
this.select(selectIds[0])
.then(() => stage?.select(selectIds[0]))
.catch(() => {});
}
}, 0);
this.emit('history-change', page as MPage | MPageFragment);
}
this.isHistoryStateChange = false;
return newConfig;
}
private selectedConfigExceptionHandler(config: MNode | Id): EditorNodeInfo {
return resolveSelectedNode(config, (id) => this.getNodeInfo(id), this.state.root?.id);
let id: Id;
if (typeof config === 'string' || typeof config === 'number') {
id = config;
} else {
id = config.id;
}
if (!id) {
throw new Error('没有ID无法选中');
}
const { node, parent, page } = this.getNodeInfo(id);
if (!node) throw new Error('获取不到组件信息');
if (node.id === this.state.root?.id) {
throw new Error('不能选根节点');
}
return {
node,
parent,
page,
};
}
}

View File

@ -56,7 +56,15 @@ class History extends BaseService {
this.state.pageId = page.id;
if (!this.state.pageSteps[this.state.pageId]) {
this.state.pageSteps[this.state.pageId] = new UndoRedo<StepValue>();
const undoRedo = new UndoRedo<StepValue>();
undoRedo.pushElement({
data: page,
modifiedNodeIds: new Map(),
nodeId: page.id,
});
this.state.pageSteps[this.state.pageId] = undoRedo;
}
this.setCanUndoRedo();

View File

@ -102,11 +102,9 @@ class Props extends BaseService {
}
public async setPropsConfig(type: string, config: FormConfig | PropsFormConfigFunction) {
let c: FormConfig;
let c = config;
if (typeof config === 'function') {
c = config({ editorService });
} else {
c = config;
}
this.state.propsConfigMap[toLine(type)] = await this.fillConfig(Array.isArray(c) ? c : [c]);

View File

@ -55,7 +55,6 @@ const state = shallowReactive<UiState>({
DEFAULT_RIGHT_COLUMN_WIDTH,
},
showGuides: true,
hasGuides: false,
showRule: true,
propsPanelSize: 'small',
showAddPageButton: true,

View File

@ -19,11 +19,11 @@
import type { Component } from 'vue';
import type EventEmitter from 'events';
import type * as Monaco from 'monaco-editor';
import type { default as Sortable, Options, SortableEvent } from 'sortablejs';
import type { PascalCasedProperties, Writable } from 'type-fest';
import Sortable, { type Options, type SortableEvent } from 'sortablejs';
import type { PascalCasedProperties } from 'type-fest';
import type { CodeBlockContent, CodeBlockDSL, Id, MApp, MContainer, MNode, MPage, MPageFragment } from '@tmagic/core';
import type { ChangeRecord, FormConfig, TableColumnConfig, TypeFunction } from '@tmagic/form';
import type { FormConfig, TableColumnConfig } from '@tmagic/form';
import type StageCore from '@tmagic/stage';
import type {
ContainerHighlightType,
@ -164,8 +164,6 @@ export interface StageOptions {
disabledMultiSelect?: boolean;
disabledRule?: boolean;
zoom?: number;
/** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
}
export interface StoreState {
@ -253,8 +251,6 @@ export interface UiState {
columnWidth: GetColumnWidth;
/** 是否显示画布参考线true: 显示false: 不显示默认为true */
showGuides: boolean;
/** 画布上是否存在参考线 */
hasGuides: boolean;
/** 是否显示标尺true: 显示false: 不显示默认为true */
showRule: boolean;
/** 用于控制该属性配置表单内组件的尺寸 */
@ -545,31 +541,14 @@ export interface CodeParamStatement {
/** 参数名称 */
name: string;
/** 参数类型 */
type?: string | TypeFunction<string>;
type?: string;
[key: string]: any;
}
export type HistoryOpType = 'add' | 'remove' | 'update';
export interface StepValue {
/** 页面信息 */
data: { name: string; id: Id };
opType: HistoryOpType;
/** 操作前选中的节点 ID用于撤销后恢复选择状态 */
selectedBefore: Id[];
/** 操作后选中的节点 ID用于重做后恢复选择状态 */
selectedAfter: Id[];
data: MPage | MPageFragment;
modifiedNodeIds: Map<Id, Id>;
/** opType 'add': 新增的节点 */
nodes?: MNode[];
/** opType 'add': 父节点 ID */
parentId?: Id;
/** opType 'add': 每个新增节点在父节点 items 中的索引 */
indexMap?: Record<string, number>;
/** opType 'remove': 被删除的节点及其位置信息 */
removedItems?: { node: MNode; parentId: Id; index: number }[];
/** opType 'update': 变更前后的节点快照 */
updatedItems?: { oldNode: MNode; newNode: MNode }[];
nodeId: Id;
}
export interface HistoryState {
@ -733,44 +712,3 @@ export type CustomContentMenuFunction = (
menus: (MenuButton | MenuComponent)[],
type: 'layer' | 'data-source' | 'viewer' | 'code-block',
) => (MenuButton | MenuComponent)[];
export interface EditorEvents {
'root-change': [value: StoreState['root'], preValue?: StoreState['root']];
select: [node: MNode | null];
add: [nodes: MNode[]];
remove: [nodes: MNode[]];
update: [nodes: { newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }[]];
'move-layer': [offset: number | LayerOffset];
'drag-to': [data: { targetIndex: number; configs: MNode | MNode[]; targetParent: MContainer }];
'history-change': [data: MPage | MPageFragment];
}
export const canUsePluginMethods = {
async: [
'getLayout',
'highlight',
'select',
'multiSelect',
'doAdd',
'add',
'doRemove',
'remove',
'doUpdate',
'update',
'sort',
'copy',
'paste',
'doPaste',
'doAlignCenter',
'alignCenter',
'moveLayer',
'moveToContainer',
'dragTo',
'undo',
'redo',
'move',
] as const,
sync: [],
};
export type AsyncMethodName = Writable<(typeof canUsePluginMethods)['async']>;

View File

@ -1,6 +1,6 @@
import { defineFormConfig, type FormConfig } from '@tmagic/form';
import { defineFormConfig } from '@tmagic/form';
export default (): FormConfig =>
export default () =>
defineFormConfig([
{
name: 'id',

View File

@ -1,81 +1,83 @@
import type { DataSchema, DataSourceFieldType, DataSourceSchema } from '@tmagic/core';
import { type CascaderOption, defineFormItem, type FormConfig } from '@tmagic/form';
import { DataSchema, DataSourceFieldType, DataSourceSchema } from '@tmagic/core';
import { CascaderOption, FormConfig, FormState } from '@tmagic/form';
import { dataSourceTemplateRegExp, getKeysArray, isNumber } from '@tmagic/utils';
import BaseFormConfig from './formConfigs/base';
import HttpFormConfig from './formConfigs/http';
const dataSourceFormConfig = defineFormItem({
type: 'tab',
items: [
{
title: '数据定义',
items: [
{
name: 'fields',
type: 'data-source-fields',
defaultValue: () => [],
},
],
},
{
title: '方法定义',
items: [
{
name: 'methods',
type: 'data-source-methods',
defaultValue: () => [],
},
],
},
{
title: '事件配置',
items: [
{
name: 'events',
src: 'datasource',
type: 'event-select',
},
],
},
{
title: 'mock数据',
items: [
{
name: 'mocks',
type: 'data-source-mocks',
defaultValue: () => [],
},
],
},
{
title: '请求参数裁剪',
display: (_formState, { model }) => model.type === 'http',
items: [
{
name: 'beforeRequest',
type: 'vs-code',
parse: true,
autosize: { minRows: 10, maxRows: 30 },
},
],
},
{
title: '响应数据裁剪',
display: (_formStat, { model }) => model.type === 'http',
items: [
{
name: 'afterResponse',
type: 'vs-code',
parse: true,
autosize: { minRows: 10, maxRows: 30 },
},
],
},
],
});
const fillConfig = (config: FormConfig): FormConfig => [...BaseFormConfig(), ...config, dataSourceFormConfig];
const fillConfig = (config: FormConfig): FormConfig => [
...BaseFormConfig(),
...config,
{
type: 'tab',
items: [
{
title: '数据定义',
items: [
{
name: 'fields',
type: 'data-source-fields',
defaultValue: () => [],
},
],
},
{
title: '方法定义',
items: [
{
name: 'methods',
type: 'data-source-methods',
defaultValue: () => [],
},
],
},
{
title: '事件配置',
items: [
{
name: 'events',
src: 'datasource',
type: 'event-select',
},
],
},
{
title: 'mock数据',
items: [
{
name: 'mocks',
type: 'data-source-mocks',
defaultValue: () => [],
},
],
},
{
title: '请求参数裁剪',
display: (_formState: FormState, { model }: any) => model.type === 'http',
items: [
{
name: 'beforeRequest',
type: 'vs-code',
parse: true,
autosize: { minRows: 10, maxRows: 30 },
},
],
},
{
title: '响应数据裁剪',
display: (_formState: FormState, { model }: any) => model.type === 'http',
items: [
{
name: 'afterResponse',
type: 'vs-code',
parse: true,
autosize: { minRows: 10, maxRows: 30 },
},
],
},
],
},
];
export const getFormConfig = (type: string, configs: Record<string, FormConfig>): FormConfig => {
switch (type) {

View File

@ -1,138 +0,0 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { toRaw } from 'vue';
import { cloneDeep } from 'lodash-es';
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment } from '@tmagic/core';
import { NodeType } from '@tmagic/core';
import type StageCore from '@tmagic/stage';
import { isPage, isPageFragment } from '@tmagic/utils';
import type { EditorNodeInfo, StepValue } from '@editor/type';
import { getNodeIndex } from '@editor/utils/editor';
export interface HistoryOpContext {
root: MApp;
stage: StageCore | null;
getNodeById(id: Id, raw?: boolean): MNode | null;
getNodeInfo(id: Id, raw?: boolean): EditorNodeInfo;
setRoot(root: MApp): void;
setPage(page: MPage | MPageFragment): void;
getPage(): MPage | MPageFragment | null;
}
/**
* add
* reverse=true
* reverse=false
*/
export async function applyHistoryAddOp(step: StepValue, reverse: boolean, ctx: HistoryOpContext): Promise<void> {
const { root, stage } = ctx;
if (reverse) {
for (const node of step.nodes ?? []) {
const parent = ctx.getNodeById(step.parentId!, false) as MContainer;
if (!parent?.items) continue;
const idx = getNodeIndex(node.id, parent);
if (typeof idx === 'number' && idx !== -1) {
parent.items.splice(idx, 1);
}
await stage?.remove({ id: node.id, parentId: parent.id, root: cloneDeep(root) });
}
} else {
const parent = ctx.getNodeById(step.parentId!, false) as MContainer;
if (parent?.items) {
for (const node of step.nodes ?? []) {
const idx = step.indexMap?.[node.id] ?? parent.items.length;
parent.items.splice(idx, 0, cloneDeep(node));
await stage?.add({
config: cloneDeep(node),
parent: cloneDeep(parent),
parentId: parent.id,
root: cloneDeep(root),
});
}
}
}
}
/**
* remove
* reverse=true
* reverse=false
*/
export async function applyHistoryRemoveOp(step: StepValue, reverse: boolean, ctx: HistoryOpContext): Promise<void> {
const { root, stage } = ctx;
if (reverse) {
const sorted = [...(step.removedItems ?? [])].sort((a, b) => a.index - b.index);
for (const { node, parentId, index } of sorted) {
const parent = ctx.getNodeById(parentId, false) as MContainer;
if (!parent?.items) continue;
parent.items.splice(index, 0, cloneDeep(node));
await stage?.add({ config: cloneDeep(node), parent: cloneDeep(parent), parentId, root: cloneDeep(root) });
}
} else {
for (const { node, parentId } of step.removedItems ?? []) {
const parent = ctx.getNodeById(parentId, false) as MContainer;
if (!parent?.items) continue;
const idx = getNodeIndex(node.id, parent);
if (typeof idx === 'number' && idx !== -1) {
parent.items.splice(idx, 1);
}
await stage?.remove({ id: node.id, parentId, root: cloneDeep(root) });
}
}
}
/**
* update
* reverse=true oldNode
* reverse=false newNode
*/
export async function applyHistoryUpdateOp(step: StepValue, reverse: boolean, ctx: HistoryOpContext): Promise<void> {
const { root, stage } = ctx;
const items = step.updatedItems ?? [];
for (const { oldNode, newNode } of items) {
const config = reverse ? oldNode : newNode;
if (config.type === NodeType.ROOT) {
ctx.setRoot(cloneDeep(config) as MApp);
continue;
}
const info = ctx.getNodeInfo(config.id, false);
if (!info.parent) continue;
const idx = getNodeIndex(config.id, info.parent);
if (typeof idx !== 'number' || idx === -1) continue;
info.parent.items![idx] = cloneDeep(config);
if (isPage(config) || isPageFragment(config)) {
ctx.setPage(config as MPage | MPageFragment);
}
}
const curPage = ctx.getPage();
if (stage && curPage) {
await stage.update({
config: cloneDeep(toRaw(curPage)),
parentId: root.id,
root: cloneDeep(toRaw(root)),
});
}
}

View File

@ -17,13 +17,12 @@
*/
import { detailedDiff } from 'deep-object-diff';
import { cloneDeep, get, isObject } from 'lodash-es';
import { isObject } from 'lodash-es';
import serialize from 'serialize-javascript';
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
import { NODE_CONDS_KEY, NodeType, Target, Watcher } from '@tmagic/core';
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment } from '@tmagic/core';
import { NODE_CONDS_KEY, NodeType } from '@tmagic/core';
import type StageCore from '@tmagic/stage';
import { isFixed } from '@tmagic/stage';
import {
calcValueByFontsize,
getElById,
@ -35,8 +34,7 @@ import {
isValueIncludeDataSource,
} from '@tmagic/utils';
import type { EditorNodeInfo } from '@editor/type';
import { LayerOffset, Layout } from '@editor/type';
import { Layout } from '@editor/type';
export const COPY_STORAGE_KEY = '$MagicEditorCopyData';
export const COPY_CODE_STORAGE_KEY = '$MagicEditorCopyCode';
@ -438,246 +436,3 @@ export const buildChangeRecords = (value: any, basePath: string) => {
return changeRecords;
};
/**
* ID解析出选中节点信息
* @param config ID
* @param getNodeInfoFn
* @param rootId ID
* @returns nodeparentpage
*/
export const resolveSelectedNode = (
config: MNode | Id,
getNodeInfoFn: (id: Id) => EditorNodeInfo,
rootId?: Id,
): EditorNodeInfo => {
const id: Id = typeof config === 'string' || typeof config === 'number' ? config : config.id;
if (!id) {
throw new Error('没有ID无法选中');
}
const { node, parent, page } = getNodeInfoFn(id);
if (!node) throw new Error('获取不到组件信息');
if (node.id === rootId) throw new Error('不能选根节点');
return { node, parent, page };
};
/**
* fixed
* fixed fixed
* @param dist
* @param src
* @param root
* @param getLayoutFn
* @returns
*/
export const toggleFixedPosition = async (
dist: MNode,
src: MNode,
root: MApp,
getLayoutFn: (parent: MNode, node?: MNode | null) => Promise<Layout>,
): Promise<MNode> => {
const newConfig = cloneDeep(dist);
if (!isPop(src) && newConfig.style?.position) {
if (isFixed(newConfig.style) && !isFixed(src.style || {})) {
newConfig.style = change2Fixed(newConfig, root);
} else if (!isFixed(newConfig.style) && isFixed(src.style || {})) {
newConfig.style = await Fixed2Other(newConfig, root, getLayoutFn);
}
}
return newConfig;
};
/**
*
* absolute fixed top/left bottom/right
* @param style
* @param left
* @param top
* @returns null
*/
export const calcMoveStyle = (style: Record<string, any>, left: number, top: number): Record<string, any> | null => {
if (!style || !['absolute', 'fixed'].includes(style.position)) return null;
const newStyle: Record<string, any> = { ...style };
if (top) {
if (isNumber(style.top)) {
newStyle.top = Number(style.top) + Number(top);
newStyle.bottom = '';
} else if (isNumber(style.bottom)) {
newStyle.bottom = Number(style.bottom) - Number(top);
newStyle.top = '';
}
}
if (left) {
if (isNumber(style.left)) {
newStyle.left = Number(style.left) + Number(left);
newStyle.right = '';
} else if (isNumber(style.right)) {
newStyle.right = Number(style.right) - Number(left);
newStyle.left = '';
}
}
return newStyle;
};
/**
*
* relative DOM 退 width
* @param node
* @param parent
* @param layout
* @param doc document DOM
* @returns null
*/
export const calcAlignCenterStyle = (
node: MNode,
parent: MContainer,
layout: Layout,
doc?: Document,
): Record<string, any> | null => {
if (layout === Layout.RELATIVE || !node.style) return null;
const style = { ...node.style };
if (doc) {
const el = getElById()(doc, node.id);
const parentEl = layout === Layout.FIXED ? doc.body : el?.offsetParent;
if (parentEl && el) {
style.left = calcValueByFontsize(doc, (parentEl.clientWidth - el.clientWidth) / 2);
style.right = '';
}
} else if (parent.style && isNumber(parent.style?.width) && isNumber(node.style?.width)) {
style.left = (parent.style.width - node.style.width) / 2;
style.right = '';
}
return style;
};
/**
*
* "上移""上移"
* @param currentIndex
* @param offset LayerOffset.TOP / LayerOffset.BOTTOM
* @param brothersLength
* @param isRelative
* @returns
*/
export const calcLayerTargetIndex = (
currentIndex: number,
offset: number | LayerOffset,
brothersLength: number,
isRelative: boolean,
): number => {
if (offset === LayerOffset.TOP) {
return isRelative ? 0 : brothersLength;
}
if (offset === LayerOffset.BOTTOM) {
return isRelative ? brothersLength : 0;
}
return currentIndex + (isRelative ? -(offset as number) : (offset as number));
};
/**
* mergeWith
* - undefined source key
* - 使
* -
*/
export const editorNodeMergeCustomizer = (objValue: any, srcValue: any, key: string, _object: any, source: any) => {
if (typeof srcValue === 'undefined' && Object.hasOwn(source, key)) {
return '';
}
if (isObject(srcValue) && Array.isArray(objValue)) {
return srcValue;
}
if (Array.isArray(srcValue)) {
return srcValue;
}
};
/**
* copyNodes
* @param copyNodes
* @param collectorOptions
* @param getNodeById ID
*/
export const collectRelatedNodes = (
copyNodes: MNode[],
collectorOptions: TargetOptions,
getNodeById: (id: Id) => MNode | null,
): void => {
const customTarget = new Target({ ...collectorOptions });
const coperWatcher = new Watcher();
coperWatcher.addTarget(customTarget);
coperWatcher.collect(copyNodes, {}, true, collectorOptions.type);
Object.keys(customTarget.deps).forEach((nodeId: Id) => {
const node = getNodeById(nodeId);
if (!node) return;
customTarget.deps[nodeId].keys.forEach((key) => {
const relateNodeId = get(node, key);
const isExist = copyNodes.find((n) => n.id === relateNodeId);
if (!isExist) {
const relateNode = getNodeById(relateNodeId);
if (relateNode) {
copyNodes.push(relateNode);
}
}
});
});
};
export interface DragClassification {
sameParentIndices: number[];
crossParentConfigs: { config: MNode; parent: MContainer }[];
/** 当同父容器节点索引异常时置为 true调用方应中止拖拽操作 */
aborted: boolean;
}
/**
* vs
* @param configs
* @param targetParent
* @param getNodeInfo
* @returns
*/
export const classifyDragSources = (
configs: MNode[],
targetParent: MContainer,
getNodeInfo: (id: Id, raw?: boolean) => EditorNodeInfo,
): DragClassification => {
const sameParentIndices: number[] = [];
const crossParentConfigs: { config: MNode; parent: MContainer }[] = [];
for (const config of configs) {
const { parent, node: curNode } = getNodeInfo(config.id, false);
if (!parent || !curNode) continue;
const path = getNodePath(curNode.id, parent.items);
if (path.some((node) => `${targetParent.id}` === `${node.id}`)) continue;
const index = getNodeIndex(curNode.id, parent);
if (`${parent.id}` === `${targetParent.id}`) {
if (typeof index !== 'number' || index === -1) {
return { sameParentIndices, crossParentConfigs, aborted: true };
}
sameParentIndices.push(index);
} else {
crossParentConfigs.push({ config, parent });
}
}
return { sameParentIndices, crossParentConfigs, aborted: false };
};

View File

@ -27,4 +27,3 @@ export * from './scroll-viewer';
export * from './tree';
export * from './undo-redo';
export * from './const';
export { default as loadMonaco } from './monaco-editor';

View File

@ -24,7 +24,7 @@ import {
NODE_DISABLE_DATA_SOURCE_KEY,
} from '@tmagic/core';
import { tMagicMessage } from '@tmagic/design';
import type { ChildConfig, DisplayCondsConfig, FormConfig, TabConfig, TabPaneConfig } from '@tmagic/form';
import type { FormConfig, FormState, TabConfig, TabPaneConfig } from '@tmagic/form';
export const arrayOptions = [
{ text: '包含', value: 'include' },
@ -106,12 +106,7 @@ export const styleTabConfig: TabPaneConfig = {
'borderWidth',
'borderStyle',
'borderColor',
'opacity',
],
} as unknown as ChildConfig,
{
name: 'transform',
defaultValue: () => ({}),
},
],
},
@ -173,9 +168,9 @@ export const advancedTabConfig: TabPaneConfig = {
],
};
export const displayTabConfig: TabPaneConfig<DisplayCondsConfig> = {
export const displayTabConfig: TabPaneConfig = {
title: '显示条件',
display: (_state, { model }) => model.type !== 'page',
display: (_state: FormState, { model }: any) => model.type !== 'page',
items: [
{
name: NODE_CONDS_RESULT_KEY,
@ -214,7 +209,7 @@ export const fillConfig = (
const propsConfig: FormConfig = [];
// 组件类型,必须要有
if (!config.find((item) => 'name' in item && item.name === 'type')) {
if (!config.find((item) => item.name === 'type')) {
propsConfig.push({
text: 'type',
name: 'type',
@ -222,7 +217,7 @@ export const fillConfig = (
});
}
if (!config.find((item) => 'name' in item && item.name === 'id')) {
if (!config.find((item) => item.name === 'id')) {
// 组件id必须要有
propsConfig.push({
name: 'id',
@ -246,16 +241,14 @@ export const fillConfig = (
});
}
if (!config.find((item) => 'name' in item && item.name === 'name')) {
if (!config.find((item) => item.name === 'name')) {
propsConfig.push({
name: 'name',
text: '组件名称',
});
}
const noCodeAdvancedTabItems = advancedTabConfig.items.filter(
(item) => 'type' in item && item.type !== 'code-select',
);
const noCodeAdvancedTabItems = advancedTabConfig.items.filter((item) => item.type !== 'code-select');
if (noCodeAdvancedTabItems.length > 0 && disabledCodeBlock) {
advancedTabConfig.items = noCodeAdvancedTabItems;

View File

@ -23,7 +23,7 @@ export class UndoRedo<T = any> {
private listCursor: number;
private listMaxSize: number;
constructor(listMaxSize = 100) {
constructor(listMaxSize = 20) {
const minListMaxSize = 2;
this.elementList = [];
this.listCursor = 0;
@ -42,30 +42,29 @@ export class UndoRedo<T = any> {
}
public canUndo(): boolean {
return this.listCursor > 0;
return this.listCursor > 1;
}
/** 返回被撤销的操作 */
// 返回undo后的当前元素
public undo(): T | null {
if (!this.canUndo()) {
return null;
}
this.listCursor -= 1;
return cloneDeep(this.elementList[this.listCursor]);
return this.getCurrentElement();
}
public canRedo() {
return this.elementList.length > this.listCursor;
}
/** 返回被重做的操作 */
// 返回redo后的当前元素
public redo(): T | null {
if (!this.canRedo()) {
return null;
}
const element = cloneDeep(this.elementList[this.listCursor]);
this.listCursor += 1;
return element;
return this.getCurrentElement();
}
public getCurrentElement(): T | null {

View File

@ -30,9 +30,6 @@ import { COPY_STORAGE_KEY, setEditorConfig } from '@editor/utils';
setEditorConfig({
// eslint-disable-next-line no-eval
parseDSL: (dsl: string) => eval(dsl),
customCreateMonacoEditor: (monaco, codeEditorEl, options) => monaco.editor.create(codeEditorEl, options),
customCreateMonacoDiffEditor: (monaco, codeEditorEl, options) =>
monaco.editor.createDiffEditor(codeEditorEl, options),
});
// mock window.localStage

View File

@ -1,245 +0,0 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, expect, test, vi } from 'vitest';
import type { MApp, MContainer, MNode } from '@tmagic/core';
import { NodeType } from '@tmagic/core';
import type { StepValue } from '@editor/type';
import type { HistoryOpContext } from '@editor/utils/editor-history';
import { applyHistoryAddOp, applyHistoryRemoveOp, applyHistoryUpdateOp } from '@editor/utils/editor-history';
const makePage = (): MContainer => ({
id: 'page_1',
type: NodeType.PAGE,
items: [
{ id: 'n1', type: 'text' },
{ id: 'n2', type: 'button' },
],
});
const makeRoot = (page: MContainer): MApp => ({
id: 'app_1',
type: NodeType.ROOT,
items: [page],
});
const makeCtx = (root: MApp): HistoryOpContext => {
const page = root.items[0] as MContainer;
return {
root,
stage: {
add: vi.fn(),
remove: vi.fn(),
update: vi.fn(),
} as any,
getNodeById: (id: any) => {
if (`${id}` === `${root.id}`) return root as unknown as MNode;
if (`${id}` === `${page.id}`) return page as unknown as MNode;
return page.items.find((n) => `${n.id}` === `${id}`) ?? null;
},
getNodeInfo: (id: any) => {
if (`${id}` === `${page.id}`) {
return { node: page as unknown as MNode, parent: root as unknown as MContainer, page: page as any };
}
const node = page.items.find((n) => `${n.id}` === `${id}`);
return { node: node ?? null, parent: node ? page : null, page: page as any };
},
setRoot: vi.fn(),
setPage: vi.fn(),
getPage: () => page as any,
};
};
describe('applyHistoryAddOp', () => {
test('撤销 add从父节点移除已添加的节点', async () => {
const page = makePage();
const root = makeRoot(page);
const ctx = makeCtx(root);
const step: StepValue = {
opType: 'add',
selectedBefore: [],
selectedAfter: ['n1'],
modifiedNodeIds: new Map(),
nodes: [{ id: 'n1', type: 'text' }],
parentId: 'page_1',
};
expect(page.items).toHaveLength(2);
await applyHistoryAddOp(step, true, ctx);
expect(page.items).toHaveLength(1);
expect(page.items[0].id).toBe('n2');
expect(ctx.stage!.remove).toHaveBeenCalled();
});
test('重做 add重新添加节点到父节点', async () => {
const page: MContainer = { id: 'page_1', type: NodeType.PAGE, items: [] };
const root = makeRoot(page);
const ctx = makeCtx(root);
const step: StepValue = {
opType: 'add',
selectedBefore: [],
selectedAfter: ['new1'],
modifiedNodeIds: new Map(),
nodes: [{ id: 'new1', type: 'text' }],
parentId: 'page_1',
indexMap: { new1: 0 },
};
await applyHistoryAddOp(step, false, ctx);
expect(page.items).toHaveLength(1);
expect(page.items[0].id).toBe('new1');
expect(ctx.stage!.add).toHaveBeenCalled();
});
});
describe('applyHistoryRemoveOp', () => {
test('撤销 remove将已删除节点按原位置重新插入', async () => {
const page: MContainer = { id: 'page_1', type: NodeType.PAGE, items: [{ id: 'n2', type: 'button' }] };
const root = makeRoot(page);
const ctx = makeCtx(root);
const step: StepValue = {
opType: 'remove',
selectedBefore: ['n1'],
selectedAfter: [],
modifiedNodeIds: new Map(),
removedItems: [{ node: { id: 'n1', type: 'text' }, parentId: 'page_1', index: 0 }],
};
await applyHistoryRemoveOp(step, true, ctx);
expect(page.items).toHaveLength(2);
expect(page.items[0].id).toBe('n1');
expect(ctx.stage!.add).toHaveBeenCalled();
});
test('重做 remove再次删除节点', async () => {
const page = makePage();
const root = makeRoot(page);
const ctx = makeCtx(root);
const step: StepValue = {
opType: 'remove',
selectedBefore: [],
selectedAfter: [],
modifiedNodeIds: new Map(),
removedItems: [{ node: { id: 'n1', type: 'text' }, parentId: 'page_1', index: 0 }],
};
expect(page.items).toHaveLength(2);
await applyHistoryRemoveOp(step, false, ctx);
expect(page.items).toHaveLength(1);
expect(page.items[0].id).toBe('n2');
expect(ctx.stage!.remove).toHaveBeenCalled();
});
});
describe('applyHistoryUpdateOp', () => {
test('撤销 update将节点恢复为 oldNode', async () => {
const page = makePage();
const root = makeRoot(page);
const ctx = makeCtx(root);
const step: StepValue = {
opType: 'update',
selectedBefore: [],
selectedAfter: [],
modifiedNodeIds: new Map(),
updatedItems: [
{
oldNode: { id: 'n1', type: 'text', text: 'before' },
newNode: { id: 'n1', type: 'text', text: 'after' },
},
],
};
await applyHistoryUpdateOp(step, true, ctx);
expect(page.items[0].text).toBe('before');
expect(ctx.stage!.update).toHaveBeenCalled();
});
test('重做 update将节点更新为 newNode', async () => {
const page = makePage();
const root = makeRoot(page);
const ctx = makeCtx(root);
const step: StepValue = {
opType: 'update',
selectedBefore: [],
selectedAfter: [],
modifiedNodeIds: new Map(),
updatedItems: [
{
oldNode: { id: 'n1', type: 'text', text: 'before' },
newNode: { id: 'n1', type: 'text', text: 'after' },
},
],
};
await applyHistoryUpdateOp(step, false, ctx);
expect(page.items[0].text).toBe('after');
});
test('update ROOT 类型调用 setRoot', async () => {
const page = makePage();
const root = makeRoot(page);
const ctx = makeCtx(root);
const step: StepValue = {
opType: 'update',
selectedBefore: [],
selectedAfter: [],
modifiedNodeIds: new Map(),
updatedItems: [
{
oldNode: { id: 'app_1', type: NodeType.ROOT, items: [] } as any,
newNode: { id: 'app_1', type: NodeType.ROOT, items: [page] } as any,
},
],
};
await applyHistoryUpdateOp(step, true, ctx);
expect(ctx.setRoot).toHaveBeenCalled();
});
test('update 页面节点调用 setPage', async () => {
const page = makePage();
const root = makeRoot(page);
const ctx = makeCtx(root);
const updatedPage = { ...page, name: 'renamed' };
const step: StepValue = {
opType: 'update',
selectedBefore: [],
selectedAfter: [],
modifiedNodeIds: new Map(),
updatedItems: [
{
oldNode: page as any,
newNode: updatedPage as any,
},
],
};
await applyHistoryUpdateOp(step, false, ctx);
expect(ctx.setPage).toHaveBeenCalled();
});
});

View File

@ -17,11 +17,8 @@
*/
import { describe, expect, test } from 'vitest';
import type { MApp, MContainer, MNode } from '@tmagic/core';
import { NodeType } from '@tmagic/core';
import type { EditorNodeInfo } from '@editor/type';
import { LayerOffset, Layout } from '@editor/type';
import * as editor from '@editor/utils/editor';
describe('util form', () => {
@ -308,452 +305,3 @@ describe('buildChangeRecords', () => {
]);
});
});
// ===== 以下为新提取的工具函数测试 =====
const mockRoot: MApp = {
id: 'app_1',
type: NodeType.ROOT,
items: [
{
id: 'page_1',
type: NodeType.PAGE,
name: 'index',
style: { position: 'relative', width: 375 },
items: [
{
id: 'node_1',
type: 'text',
style: { position: 'absolute', top: 10, left: 20, width: 100 },
},
{
id: 'node_2',
type: 'button',
style: { position: 'absolute', bottom: 50, right: 30 },
},
{
id: 'node_3',
type: 'image',
style: { position: 'relative', top: 0, left: 0 },
},
],
},
],
};
const mockGetNodeInfo = (id: string | number): EditorNodeInfo => {
const page = mockRoot.items[0];
if (`${id}` === `${mockRoot.id}`) {
return { node: mockRoot as unknown as MNode, parent: null, page: null };
}
if (`${id}` === `${page.id}`) {
return { node: page, parent: mockRoot as unknown as MContainer, page: page as any };
}
const items = (page as MContainer).items || [];
const node = items.find((n: MNode) => `${n.id}` === `${id}`);
if (node) {
return { node, parent: page as MContainer, page: page as any };
}
return { node: null, parent: null, page: null };
};
describe('resolveSelectedNode', () => {
test('传入数字ID正常返回节点信息', () => {
const result = editor.resolveSelectedNode('node_1', mockGetNodeInfo, mockRoot.id);
expect(result.node?.id).toBe('node_1');
expect(result.parent?.id).toBe('page_1');
expect(result.page?.id).toBe('page_1');
});
test('传入节点配置对象,正常返回节点信息', () => {
const config: MNode = { id: 'node_2', type: 'button' };
const result = editor.resolveSelectedNode(config, mockGetNodeInfo, mockRoot.id);
expect(result.node?.id).toBe('node_2');
});
test('传入页面ID正常返回页面信息', () => {
const result = editor.resolveSelectedNode('page_1', mockGetNodeInfo, mockRoot.id);
expect(result.node?.id).toBe('page_1');
});
test('传入空ID抛出错误', () => {
expect(() => editor.resolveSelectedNode({ id: '', type: 'text' }, mockGetNodeInfo)).toThrow('没有ID无法选中');
});
test('传入不存在的ID抛出错误', () => {
expect(() => editor.resolveSelectedNode('not_exist', mockGetNodeInfo)).toThrow('获取不到组件信息');
});
test('传入根节点ID抛出错误', () => {
expect(() => editor.resolveSelectedNode('app_1', mockGetNodeInfo, mockRoot.id)).toThrow('不能选根节点');
});
test('不传rootId时不校验根节点', () => {
const result = editor.resolveSelectedNode('app_1', mockGetNodeInfo);
expect(result.node?.id).toBe('app_1');
});
});
describe('toggleFixedPosition', () => {
const getLayoutFn = async () => Layout.ABSOLUTE;
test('非fixed变为fixed调用change2Fixed', async () => {
const src: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 10, left: 20 } };
const dist: MNode = { id: 'node_1', type: 'text', style: { position: 'fixed', top: 10, left: 20 } };
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
expect(result.style?.position).toBe('fixed');
expect(result).not.toBe(dist);
});
test('fixed变为非fixed调用Fixed2Other', async () => {
const src: MNode = { id: 'node_1', type: 'text', style: { position: 'fixed', top: 10, left: 20 } };
const dist: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 10, left: 20 } };
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
expect(result.style?.position).toBe('absolute');
});
test('定位未变化,不修改样式', async () => {
const src: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 10, left: 20 } };
const dist: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 30, left: 40 } };
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
expect(result.style?.top).toBe(30);
expect(result.style?.left).toBe(40);
});
test('pop类型节点不做处理', async () => {
const src: MNode = {
id: 'node_1',
type: 'pop',
style: { position: 'absolute', top: 10, left: 20 },
name: 'pop',
};
const dist: MNode = { id: 'node_1', type: 'pop', style: { position: 'fixed', top: 10, left: 20 }, name: 'pop' };
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
expect(result.style?.position).toBe('fixed');
});
test('目标节点无position属性不做处理', async () => {
const src: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute' } };
const dist: MNode = { id: 'node_1', type: 'text', style: { width: 100 } };
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
expect(result.style?.position).toBeUndefined();
});
test('返回深拷贝,不修改原对象', async () => {
const src: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 10 } };
const dist: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 20 } };
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
expect(result).not.toBe(dist);
expect(dist.style?.top).toBe(20);
});
});
describe('calcMoveStyle', () => {
test('absolute定位向下移动', () => {
const style = { position: 'absolute', top: 10, left: 20 };
const result = editor.calcMoveStyle(style, 0, 5);
expect(result).toEqual({ position: 'absolute', top: 15, left: 20, bottom: '' });
});
test('absolute定位向右移动', () => {
const style = { position: 'absolute', top: 10, left: 20 };
const result = editor.calcMoveStyle(style, 5, 0);
expect(result).toEqual({ position: 'absolute', top: 10, left: 25, right: '' });
});
test('absolute定位同时向下和向右移动', () => {
const style = { position: 'absolute', top: 10, left: 20 };
const result = editor.calcMoveStyle(style, 3, 7);
expect(result).toEqual({ position: 'absolute', top: 17, left: 23, bottom: '', right: '' });
});
test('fixed定位正常移动', () => {
const style = { position: 'fixed', top: 100, left: 200 };
const result = editor.calcMoveStyle(style, -10, -20);
expect(result).toEqual({ position: 'fixed', top: 80, left: 190, bottom: '', right: '' });
});
test('使用bottom定位时向下移动减小bottom', () => {
const style = { position: 'absolute', bottom: 50, left: 20 };
const result = editor.calcMoveStyle(style, 0, 10);
expect(result?.bottom).toBe(40);
expect(result?.top).toBe('');
});
test('使用right定位时向右移动减小right', () => {
const style = { position: 'absolute', top: 10, right: 30 };
const result = editor.calcMoveStyle(style, 10, 0);
expect(result?.right).toBe(20);
expect(result?.left).toBe('');
});
test('relative定位返回null', () => {
const style = { position: 'relative', top: 0, left: 0 };
const result = editor.calcMoveStyle(style, 10, 10);
expect(result).toBeNull();
});
test('无position属性返回null', () => {
const style = { width: 100 };
const result = editor.calcMoveStyle(style, 10, 10);
expect(result).toBeNull();
});
test('空样式对象返回null', () => {
const result = editor.calcMoveStyle({}, 10, 10);
expect(result).toBeNull();
});
test('偏移量为0不修改样式', () => {
const style = { position: 'absolute', top: 10, left: 20 };
const result = editor.calcMoveStyle(style, 0, 0);
expect(result).toEqual({ position: 'absolute', top: 10, left: 20 });
});
test('不修改原对象', () => {
const style = { position: 'absolute', top: 10, left: 20 };
editor.calcMoveStyle(style, 5, 5);
expect(style.top).toBe(10);
expect(style.left).toBe(20);
});
});
describe('calcAlignCenterStyle', () => {
test('absolute布局通过配置中的width计算居中', () => {
const node: MNode = { id: 'n1', type: 'text', style: { width: 100, left: 0 } };
const parent = { id: 'p1', type: NodeType.PAGE, style: { width: 375 }, items: [] } as unknown as MContainer;
const result = editor.calcAlignCenterStyle(node, parent, Layout.ABSOLUTE);
expect(result?.left).toBe(137.5);
expect(result?.right).toBe('');
});
test('relative布局返回null', () => {
const node: MNode = { id: 'n1', type: 'text', style: { width: 100 } };
const parent = { id: 'p1', type: NodeType.PAGE, style: { width: 375 }, items: [] } as unknown as MContainer;
const result = editor.calcAlignCenterStyle(node, parent, Layout.RELATIVE);
expect(result).toBeNull();
});
test('节点无style返回null', () => {
const node: MNode = { id: 'n1', type: 'text' };
const parent = { id: 'p1', type: NodeType.PAGE, style: { width: 375 }, items: [] } as unknown as MContainer;
const result = editor.calcAlignCenterStyle(node, parent, Layout.ABSOLUTE);
expect(result).toBeNull();
});
test('父节点无style不修改', () => {
const node: MNode = { id: 'n1', type: 'text', style: { width: 100, left: 10 } };
const parent = { id: 'p1', type: NodeType.PAGE, items: [] } as unknown as MContainer;
const result = editor.calcAlignCenterStyle(node, parent, Layout.ABSOLUTE);
expect(result?.left).toBe(10);
});
test('父节点width非数字不修改left', () => {
const node: MNode = { id: 'n1', type: 'text', style: { width: 100, left: 10 } };
const parent = {
id: 'p1',
type: NodeType.PAGE,
style: { width: '100%' },
items: [],
} as unknown as MContainer;
const result = editor.calcAlignCenterStyle(node, parent, Layout.ABSOLUTE);
expect(result?.left).toBe(10);
});
test('不修改原节点style', () => {
const node: MNode = { id: 'n1', type: 'text', style: { width: 100, left: 0 } };
const parent = { id: 'p1', type: NodeType.PAGE, style: { width: 375 }, items: [] } as unknown as MContainer;
editor.calcAlignCenterStyle(node, parent, Layout.ABSOLUTE);
expect(node.style?.left).toBe(0);
});
});
describe('calcLayerTargetIndex', () => {
test('绝对定位向上移动1层', () => {
const result = editor.calcLayerTargetIndex(2, 1, 5, false);
expect(result).toBe(3);
});
test('绝对定位向下移动1层', () => {
const result = editor.calcLayerTargetIndex(2, -1, 5, false);
expect(result).toBe(1);
});
test('流式布局向上移动1层索引减小', () => {
const result = editor.calcLayerTargetIndex(2, 1, 5, true);
expect(result).toBe(1);
});
test('流式布局向下移动1层索引增大', () => {
const result = editor.calcLayerTargetIndex(2, -1, 5, true);
expect(result).toBe(3);
});
test('绝对定位,置顶', () => {
const result = editor.calcLayerTargetIndex(2, LayerOffset.TOP, 5, false);
expect(result).toBe(5);
});
test('绝对定位,置底', () => {
const result = editor.calcLayerTargetIndex(2, LayerOffset.BOTTOM, 5, false);
expect(result).toBe(0);
});
test('流式布局,置顶(索引最小)', () => {
const result = editor.calcLayerTargetIndex(3, LayerOffset.TOP, 5, true);
expect(result).toBe(0);
});
test('流式布局,置底(索引最大)', () => {
const result = editor.calcLayerTargetIndex(1, LayerOffset.BOTTOM, 5, true);
expect(result).toBe(5);
});
test('偏移量为0索引不变', () => {
const result = editor.calcLayerTargetIndex(2, 0, 5, false);
expect(result).toBe(2);
});
});
describe('editorNodeMergeCustomizer', () => {
test('undefined 且 source 拥有该 key 时返回空字符串', () => {
const source = { name: undefined };
const result = editor.editorNodeMergeCustomizer('old', undefined, 'name', {}, source);
expect(result).toBe('');
});
test('source 不拥有该 key 时返回 undefined使用默认合并', () => {
const result = editor.editorNodeMergeCustomizer('old', undefined, 'name', {}, {});
expect(result).toBeUndefined();
});
test('原来是数组,新值是对象,使用新值', () => {
const srcValue = { a: 1 };
const result = editor.editorNodeMergeCustomizer([1, 2], srcValue, 'key', {}, {});
expect(result).toBe(srcValue);
});
test('新值是数组,直接替换', () => {
const srcValue = [3, 4];
const result = editor.editorNodeMergeCustomizer([1, 2], srcValue, 'key', {}, {});
expect(result).toBe(srcValue);
});
test('都是普通值,返回 undefined使用默认合并', () => {
const result = editor.editorNodeMergeCustomizer('old', 'new', 'key', {}, {});
expect(result).toBeUndefined();
});
});
describe('classifyDragSources', () => {
const makeTree = (): { root: MApp; getNodeInfo: (id: any, raw?: boolean) => EditorNodeInfo } => {
const child1: MNode = { id: 'c1', type: 'text' };
const child2: MNode = { id: 'c2', type: 'text' };
const child3: MNode = { id: 'c3', type: 'text' };
const container1: MContainer = {
id: 'cont1',
type: NodeType.CONTAINER,
items: [child1, child2],
};
const container2: MContainer = {
id: 'cont2',
type: NodeType.CONTAINER,
items: [child3],
};
const page: any = {
id: 'page_1',
type: NodeType.PAGE,
items: [container1, container2],
};
const root: MApp = { id: 'app', type: NodeType.ROOT, items: [page] };
const getNodeInfo = (id: any): EditorNodeInfo => {
if (`${id}` === 'c1' || `${id}` === 'c2') {
return {
node: container1.items.find((n) => `${n.id}` === `${id}`) ?? null,
parent: container1,
page,
};
}
if (`${id}` === 'c3') {
return { node: child3, parent: container2, page };
}
if (`${id}` === 'cont1') {
return { node: container1, parent: page, page };
}
if (`${id}` === 'cont2') {
return { node: container2, parent: page, page };
}
return { node: null, parent: null, page: null };
};
return { root, getNodeInfo };
};
test('同父容器内拖拽,返回 sameParentIndices', () => {
const { getNodeInfo } = makeTree();
const targetParent = getNodeInfo('cont1').node as MContainer;
const result = editor.classifyDragSources([{ id: 'c1', type: 'text' }], targetParent, getNodeInfo);
expect(result.aborted).toBe(false);
expect(result.sameParentIndices).toEqual([0]);
expect(result.crossParentConfigs).toHaveLength(0);
});
test('跨容器拖拽,返回 crossParentConfigs', () => {
const { getNodeInfo } = makeTree();
const targetParent = getNodeInfo('cont1').node as MContainer;
const result = editor.classifyDragSources([{ id: 'c3', type: 'text' }], targetParent, getNodeInfo);
expect(result.aborted).toBe(false);
expect(result.sameParentIndices).toHaveLength(0);
expect(result.crossParentConfigs).toHaveLength(1);
expect(result.crossParentConfigs[0].config.id).toBe('c3');
});
test('混合拖拽:同容器+跨容器', () => {
const { getNodeInfo } = makeTree();
const targetParent = getNodeInfo('cont1').node as MContainer;
const result = editor.classifyDragSources(
[
{ id: 'c1', type: 'text' },
{ id: 'c3', type: 'text' },
],
targetParent,
getNodeInfo,
);
expect(result.aborted).toBe(false);
expect(result.sameParentIndices).toEqual([0]);
expect(result.crossParentConfigs).toHaveLength(1);
});
test('节点不存在时跳过', () => {
const { getNodeInfo } = makeTree();
const targetParent = getNodeInfo('cont1').node as MContainer;
const result = editor.classifyDragSources([{ id: 'nonexistent', type: 'text' }], targetParent, getNodeInfo);
expect(result.aborted).toBe(false);
expect(result.sameParentIndices).toHaveLength(0);
expect(result.crossParentConfigs).toHaveLength(0);
});
test('目标容器在节点路径上时跳过(防止循环嵌套)', () => {
const { getNodeInfo } = makeTree();
const targetParent = getNodeInfo('cont1').node as MContainer;
const result = editor.classifyDragSources([{ id: 'c1', type: 'text' }], targetParent, (id: any) => {
if (`${id}` === 'c1') {
return {
node: { id: 'c1', type: 'text' },
parent: targetParent,
page: { id: 'page_1', type: NodeType.PAGE, items: [] } as any,
};
}
return { node: null, parent: null, page: null };
});
expect(result.sameParentIndices).toEqual([0]);
expect(result.crossParentConfigs).toHaveLength(0);
});
});

View File

@ -21,66 +21,60 @@ import { UndoRedo } from '@editor/utils/undo-redo';
describe('undo', () => {
let undoRedo: UndoRedo;
const element = { a: 1 };
beforeEach(() => {
undoRedo = new UndoRedo();
undoRedo.pushElement(element);
});
test('can not undo: empty list', () => {
test('can no undo: empty list', () => {
expect(undoRedo.canUndo()).toBe(false);
expect(undoRedo.undo()).toEqual(null);
});
test('can undo after one push', () => {
undoRedo.pushElement({ a: 1 });
expect(undoRedo.canUndo()).toBe(true);
expect(undoRedo.undo()).toEqual({ a: 1 });
expect(undoRedo.canUndo()).toBe(false);
});
test('can undo returns the operation being undone', () => {
undoRedo.pushElement({ a: 1 });
test('can undo', () => {
undoRedo.pushElement({ a: 2 });
expect(undoRedo.canUndo()).toBe(true);
expect(undoRedo.undo()).toEqual({ a: 2 });
expect(undoRedo.canUndo()).toBe(true);
expect(undoRedo.undo()).toEqual({ a: 1 });
expect(undoRedo.canUndo()).toBe(false);
expect(undoRedo.undo()).toEqual(element);
});
});
describe('redo', () => {
let undoRedo: UndoRedo;
const element = { a: 1 };
beforeEach(() => {
undoRedo = new UndoRedo();
undoRedo.pushElement(element);
});
test('can not redo: empty list', () => {
test('can no redo: empty list', () => {
expect(undoRedo.canRedo()).toBe(false);
expect(undoRedo.redo()).toBe(null);
});
test('can not redo: no undo', () => {
test('can no redo: no undo', () => {
for (let i = 0; i < 5; i++) {
undoRedo.pushElement({ a: i });
undoRedo.pushElement(element);
expect(undoRedo.canRedo()).toBe(false);
expect(undoRedo.redo()).toBe(null);
}
});
test('can not redo: undo and push', () => {
undoRedo.pushElement({ a: 1 });
undoRedo.pushElement({ a: 2 });
test('can no redo: undo and push', () => {
undoRedo.pushElement(element);
undoRedo.undo();
undoRedo.pushElement({ a: 3 });
undoRedo.pushElement(element);
expect(undoRedo.canRedo()).toBe(false);
expect(undoRedo.redo()).toEqual(null);
});
test('can not redo: redo end', () => {
undoRedo.pushElement({ a: 1 });
undoRedo.pushElement({ a: 2 });
test('can no redo: redo end', () => {
const element1 = { a: 1 };
const element2 = { a: 2 };
undoRedo.pushElement(element1);
undoRedo.pushElement(element2);
undoRedo.undo();
undoRedo.undo();
undoRedo.redo();
@ -91,20 +85,23 @@ describe('redo', () => {
});
test('can redo', () => {
undoRedo.pushElement({ a: 1 });
undoRedo.pushElement({ a: 2 });
const element1 = { a: 1 };
const element2 = { a: 2 };
undoRedo.pushElement(element1);
undoRedo.pushElement(element2);
undoRedo.undo();
undoRedo.undo();
expect(undoRedo.canRedo()).toBe(true);
expect(undoRedo.redo()).toEqual({ a: 1 });
expect(undoRedo.redo()).toEqual(element1);
expect(undoRedo.canRedo()).toBe(true);
expect(undoRedo.redo()).toEqual({ a: 2 });
expect(undoRedo.redo()).toEqual(element2);
});
});
describe('get current element', () => {
let undoRedo: UndoRedo;
const element = { a: 1 };
beforeEach(() => {
undoRedo = new UndoRedo();
@ -115,38 +112,44 @@ describe('get current element', () => {
});
test('has element', () => {
undoRedo.pushElement({ a: 1 });
expect(undoRedo.getCurrentElement()).toEqual({ a: 1 });
undoRedo.pushElement(element);
expect(undoRedo.getCurrentElement()).toEqual(element);
});
});
describe('list max size', () => {
let undoRedo: UndoRedo;
const listMaxSize = 100;
const element = { a: 1 };
beforeEach(() => {
undoRedo = new UndoRedo(listMaxSize);
undoRedo.pushElement(element);
});
test('reach max size', () => {
for (let i = 0; i <= listMaxSize; i++) {
for (let i = 0; i < listMaxSize; i++) {
undoRedo.pushElement({ a: i });
}
undoRedo.pushElement({ a: listMaxSize }); // 这个元素使得list达到maxSize触发数据删除
expect(undoRedo.getCurrentElement()).toEqual({ a: listMaxSize });
expect(undoRedo.canRedo()).toBe(false);
expect(undoRedo.canUndo()).toBe(true);
});
test('reach max size, then undo all', () => {
for (let i = 0; i <= listMaxSize; i++) {
test('reach max size, then undo', () => {
for (let i = 0; i < listMaxSize + 1; i++) {
undoRedo.pushElement({ a: i });
}
for (let i = 0; i < listMaxSize; i++) {
for (let i = 0; i < listMaxSize - 1; i++) {
undoRedo.undo();
}
const ele = undoRedo.getCurrentElement();
undoRedo.undo();
expect(ele?.a).toBe(1); // 经过超过maxSize被删元素之后原本a值为0的第一个元素已经被删除现在第一个元素a值为1
expect(undoRedo.canUndo()).toBe(false);
expect(undoRedo.getCurrentElement()).toEqual(null);
expect(undoRedo.getCurrentElement()).toEqual(element);
});
});

View File

@ -1,15 +1,14 @@
{
"version": "1.7.11",
"version": "1.7.7",
"name": "@tmagic/element-plus-adapter",
"type": "module",
"sideEffects": false,
"main": "dist/tmagic-element-plus-adapter.umd.cjs",
"module": "dist/es/index.js",
"module": "dist/tmagic-element-plus-adapter.js",
"types": "types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./dist/es/index.js",
"import": "./dist/tmagic-element-plus-adapter.js",
"require": "./dist/tmagic-element-plus-adapter.umd.cjs"
},
"./*": "./*"

View File

@ -1,15 +1,14 @@
{
"version": "1.7.11",
"version": "1.7.7",
"name": "@tmagic/form-schema",
"type": "module",
"sideEffects": false,
"main": "dist/tmagic-form-schema.umd.cjs",
"module": "dist/es/index.js",
"module": "dist/tmagic-form-schema.js",
"types": "types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./dist/es/index.js",
"import": "./dist/tmagic-form-schema.js",
"require": "./dist/tmagic-form-schema.umd.cjs"
},
"./*": "./*"

View File

@ -91,10 +91,12 @@ export interface FormItem {
/** vnode的key值默认是遍历数组时的index */
__key?: string | number;
/** 表单域标签的的宽度,例如 '50px'。支持 auto。 */
labelWidth?: string | number;
labelWidth?: string;
/** label 标签的title属性 */
labelTitle?: string;
className?: string;
/** 表单组件类型 */
type?: string | TypeFunction;
/** 字段名 */
name?: string | number;
/** 额外的提示信息,和 help 类似,当提示文案同时出现时,可以使用这个。 */
@ -127,16 +129,11 @@ export interface FormItem {
expand?: boolean;
style?: Record<string, any>;
fieldStyle?: Record<string, any>;
labelPosition?: 'top' | 'left' | 'right';
}
export interface DynamicTypeConfig extends FormItem {
type: TypeFunction;
[key: string]: any;
}
export interface ContainerCommonConfig<T = never> extends FormItem {
items: FormConfig<T>;
export interface ContainerCommonConfig {
items: FormConfig;
onInitValue?: (
mForm: FormState | undefined,
data: {
@ -185,12 +182,12 @@ export interface Input {
placeholder?: string;
}
export type TypeFunction<T extends string = string> = (
export type TypeFunction = (
mForm: FormState | undefined,
data: {
model: FormValue;
},
) => T;
) => string;
export type FilterFunction<T = boolean> = (
mForm: FormState | undefined,
@ -211,7 +208,6 @@ export type FilterFunction<T = boolean> = (
*/
export interface SelectConfigOption {
/** 选项的标签 */
label?: string | SelectOptionTextFunction;
text: string | SelectOptionTextFunction;
/** 选项的值 */
value: any | SelectOptionValueFunction;
@ -348,15 +344,13 @@ export interface HtmlField extends FormItem {
export interface DisplayConfig extends FormItem {
type: 'display';
initValue?: string | number | boolean;
displayText?: FilterFunction<string> | string;
displayText: FilterFunction<string> | string;
}
/** 文本输入框 */
export interface TextConfig extends FormItem, Input {
type?: 'text';
tooltip?: string;
/** 是否可清空 */
clearable?: boolean;
prepend?: string;
/** 后置元素,一般为标签或按钮 */
append?:
@ -442,17 +436,6 @@ export interface TimeConfig extends FormItem, Input {
valueFormat?: 'HH:mm:ss' | string;
}
/**
*
*/
export interface TimerangeConfig extends FormItem {
type: 'timerange';
names?: string[];
defaultTime?: Date[];
format?: 'HH:mm:ss' | string;
valueFormat?: 'HH:mm:ss' | string;
}
/**
*
*/
@ -476,11 +459,11 @@ export interface SwitchConfig extends FormItem {
*
*/
export interface RadioGroupConfig extends FormItem {
type: 'radio-group' | 'radioGroup';
type: 'radio-group';
childType?: 'default' | 'button';
options: {
value: string | number | boolean;
text?: string;
text: string;
icon?: any;
tooltip?: string;
}[];
@ -503,7 +486,7 @@ export interface CheckboxGroupOption {
*
*/
export interface CheckboxGroupConfig extends FormItem {
type: 'checkbox-group' | 'checkboxGroup';
type: 'checkbox-group';
options: CheckboxGroupOption[] | FilterFunction<CheckboxGroupOption[]>;
}
@ -550,7 +533,7 @@ export interface SelectConfig extends FormItem, Input {
/**
*
*/
export interface LinkConfig<T = never> extends FormItem {
export interface LinkConfig extends FormItem {
type: 'link';
href?: string | ((model: Record<string, any>) => string);
css?: {
@ -570,7 +553,7 @@ export interface LinkConfig<T = never> extends FormItem {
) => string)
| string;
form:
| FormConfig<T>
| FormConfig
| ((
mForm: FormState | undefined,
data: {
@ -578,7 +561,7 @@ export interface LinkConfig<T = never> extends FormItem {
values?: Readonly<FormValue> | null;
formValue?: FormValue;
},
) => FormConfig<T>);
) => FormConfig);
fullscreen?: boolean;
}
@ -619,7 +602,7 @@ export interface CascaderConfig extends FormItem, Input {
}
export interface DynamicFieldConfig extends FormItem {
type: 'dynamic-field' | 'dynamicField';
type: 'dynamic-field';
returnFields: (
config: DynamicFieldConfig,
model: Record<any, any>,
@ -635,38 +618,32 @@ export interface DynamicFieldConfig extends FormItem {
/**
*
*/
export interface RowConfig<T = never> extends FormItem {
export interface RowConfig extends FormItem {
type: 'row';
span: number;
items: ({ span?: number } & (ChildConfig<T> | EditorChildConfig | T))[];
items: ({ span?: number } & (ChildConfig | EditorChildConfig))[];
}
/**
*
*/
export interface TabPaneConfig<T = never> {
export interface TabPaneConfig {
status?: string;
/** 标签页名称,用于关联 model 中的数据 */
name?: string | number;
title: string;
lazy?: boolean;
labelWidth?: string;
items: FormConfig<T>;
display?: boolean | 'expand' | FilterFunction<boolean | 'expand'>;
items: FormConfig;
onTabClick?: (mForm: FormState | undefined, tab: any, data: any) => void;
[key: string]: any;
}
export interface TabConfig<T = never> extends FormItem, ContainerCommonConfig<T> {
export interface TabConfig extends FormItem, ContainerCommonConfig {
type: 'tab' | 'dynamic-tab';
tabType?: string;
editable?: boolean;
dynamic?: boolean;
tabPosition?: 'top' | 'right' | 'bottom' | 'left';
/** 当前激活的标签页,可以是固定值或动态函数 */
active?:
| string
| ((mForm: FormState | undefined, data: { model: FormValue; formValue?: FormValue; prop: string }) => string);
items: TabPaneConfig<T>[];
items: TabPaneConfig[];
onChange?: (mForm: FormState | undefined, data: any) => void;
onTabAdd?: (mForm: FormState | undefined, data: any) => void;
onTabRemove?: (mForm: FormState | undefined, tabName: string, data: any) => void;
@ -677,7 +654,7 @@ export interface TabConfig<T = never> extends FormItem, ContainerCommonConfig<T>
/**
*
*/
export interface FieldsetConfig<T = never> extends FormItem, ContainerCommonConfig<T> {
export interface FieldsetConfig extends FormItem, ContainerCommonConfig {
type: 'fieldset';
checkbox?:
| boolean
@ -694,7 +671,7 @@ export interface FieldsetConfig<T = never> extends FormItem, ContainerCommonConf
/**
*
*/
export interface PanelConfig<T = never> extends FormItem, ContainerCommonConfig<T> {
export interface PanelConfig extends FormItem, ContainerCommonConfig {
type: 'panel';
expand?: boolean;
title?: string;
@ -706,10 +683,7 @@ export interface TableColumnConfig extends FormItem {
label: string;
width?: string | number;
sortable?: boolean;
items?: FormConfig;
itemsFunction?: (row: any) => FormConfig;
titleTip?: FilterFunction<string>;
type?: string;
[key: string]: any;
}
/**
@ -741,11 +715,10 @@ export interface TableConfig extends FormItem {
importable?: (mForm: FormState | undefined, data: any) => boolean | 'undefined' | boolean;
/** 是否显示checkbox */
selection?: (mForm: FormState | undefined, data: any) => boolean | boolean | 'single';
/** 新增的默认行,可以是函数动态生成或静态对象 */
defaultAdd?: ((mForm: FormState | undefined, data: any) => any) | Record<string, any>;
/** 新增的默认行 */
defaultAdd?: (mForm: FormState | undefined, data: any) => any;
copyHandler?: (mForm: FormState | undefined, data: any) => any;
onSelect?: (mForm: FormState | undefined, data: any) => any;
/** @deprecated 请使用 defaultSort */
defautSort?: SortProp;
defaultSort?: SortProp;
/** 是否支持拖拽排序 */
@ -761,22 +734,20 @@ export interface TableConfig extends FormItem {
titleTip?: FilterFunction<string>;
rowKey?: string;
/** table 新增行时前置回调 */
beforeAddRow?: (mForm: FormState | undefined, data: any) => boolean | Promise<boolean>;
beforeAddRow?: (mForm: FormState | undefined, data: any) => boolean;
addButtonConfig?: {
props?: Record<string, any>;
text?: string;
};
sort?: boolean;
sortKey?: string;
}
export interface GroupListConfig<T = never> extends FormItem {
export interface GroupListConfig extends FormItem {
type: 'table' | 'groupList' | 'group-list';
span?: number;
enableToggleMode?: boolean;
items: FormConfig<T>;
groupItems?: FormConfig<T>;
tableItems?: FormConfig<T>;
items: FormConfig;
groupItems?: FormConfig;
tableItems?: FormConfig;
titleKey?: string;
titlePrefix?: string;
title?: string | FilterFunction<string>;
@ -789,8 +760,7 @@ export interface GroupListConfig<T = never> extends FormItem {
*/
defaultExpandQuantity?: number;
addable?: (mForm: FormState | undefined, data: any) => boolean | 'undefined' | boolean;
/** 新增的默认值,可以是函数动态生成或静态对象 */
defaultAdd?: ((mForm: FormState | undefined, data: any) => any) | Record<string, any>;
defaultAdd?: (mForm: FormState | undefined, data: any) => any;
delete?: (model: any, index: number | string | symbol, values: any) => boolean | boolean;
copyable?: FilterFunction<boolean>;
movable?: (
@ -804,53 +774,45 @@ export interface GroupListConfig<T = never> extends FormItem {
props?: Record<string, any>;
text?: string;
};
/** 最大行数 */
max?: number;
beforeAddRow?: (mForm: FormState | undefined, data: any) => boolean;
[key: string]: any;
}
interface StepItemConfig<T = never> extends FormItem, ContainerCommonConfig<T> {
interface StepItemConfig extends FormItem, ContainerCommonConfig {
title: string;
}
export interface StepConfig<T = never> extends FormItem {
export interface StepConfig extends FormItem {
type: 'step';
/** 每个 step 的间距,不填写将自适应间距。支持百分比。 */
space?: string | number;
items: StepItemConfig<T>[];
items: StepItemConfig[];
}
export interface ComponentConfig extends FormItem {
type: 'component';
id: string;
extend?: any;
display?: any;
component?: any;
extend: any;
display: any;
}
export interface FlexLayoutConfig<T = never> extends FormItem, ContainerCommonConfig<T> {
export interface FlexLayoutConfig extends FormItem, ContainerCommonConfig {
type: 'flex-layout';
/** flex 子项间距,默认 '16px' */
gap?: string;
}
export type ChildConfig<T = never> =
| ContainerCommonConfig<T>
| TabConfig<T>
| RowConfig<T>
| FieldsetConfig<T>
| PanelConfig<T>
export type ChildConfig =
| FormItem
| TabConfig
| RowConfig
| FieldsetConfig
| PanelConfig
| TableConfig
| GroupListConfig<T>
| StepConfig<T>
| GroupListConfig
| StepConfig
| DisplayConfig
| TextConfig
| NumberConfig
| NumberRangeConfig
| HiddenConfig
| LinkConfig<T>
| LinkConfig
| DaterangeConfig
| TimerangeConfig
| SelectConfig
| CascaderConfig
| HtmlField
@ -861,12 +823,8 @@ export type ChildConfig<T = never> =
| CheckboxConfig
| SwitchConfig
| RadioGroupConfig
| CheckboxGroupConfig
| TextareaConfig
| DynamicFieldConfig
| ComponentConfig
| FlexLayoutConfig<T>;
| ComponentConfig;
export type FormItemConfig<T = never> = ChildConfig<T> | DynamicTypeConfig | EditorChildConfig<T> | T;
export type FormConfig<T = never> = FormItemConfig<T>[];
export type FormConfig = (ChildConfig | EditorChildConfig)[];

View File

@ -1,8 +1,8 @@
import type { DataSourceFieldType, DataSourceSchema } from '@tmagic/schema';
import type { FilterFunction, FormItem, FormItemConfig, FormState, Input } from './base';
import type { ChildConfig, FilterFunction, FormItem, FormState, Input } from './base';
export interface DataSourceFieldSelectConfig<T = never> extends FormItem {
export interface DataSourceFieldSelectConfig extends FormItem {
type: 'data-source-field-select';
/**
* data
@ -26,15 +26,13 @@ export interface DataSourceFieldSelectConfig<T = never> extends FormItem {
},
) => boolean);
dataSourceFieldType?: DataSourceFieldType[];
fieldConfig?: FormItemConfig<T>;
fieldConfig?: ChildConfig;
/** 是否可以编辑数据源disable表示的是是否可以选择数据源 */
notEditable?: boolean | FilterFunction;
dataSourceId?: string;
}
export interface CodeConfig extends FormItem {
type: 'vs-code';
type: 'code';
language?: string;
options?: {
[key: string]: any;
@ -106,7 +104,6 @@ export interface DataSourceSelect extends FormItem, Input {
}
export interface DisplayCondsConfig extends FormItem {
type: 'display-conds';
titlePrefix?: string;
parentFields?: string[] | FilterFunction<string[]>;
}
@ -143,12 +140,8 @@ export interface UISelectConfig extends FormItem {
type: 'ui-select';
}
export interface StyleSetterConfig extends FormItem {
type: 'style-setter';
}
export type EditorChildConfig<T = never> =
| DataSourceFieldSelectConfig<T>
export type EditorChildConfig =
| DataSourceFieldSelectConfig
| CodeConfig
| CodeLinkConfig
| CodeSelectConfig
@ -164,5 +157,4 @@ export type EditorChildConfig<T = never> =
| EventSelectConfig
| KeyValueConfig
| PageFragmentSelectConfig
| UISelectConfig
| StyleSetterConfig;
| UISelectConfig;

View File

@ -1,8 +1,6 @@
import type { FormConfig, FormItemConfig } from './base';
import type { FormConfig } from './base';
export * from './base';
export * from './editor';
export const defineFormConfig = <T = never>(config: FormConfig<T>): FormConfig<T> => config;
export const defineFormItem = <T = never>(config: FormItemConfig<T>): FormItemConfig<T> => config;
export const defineFormConfig = <T = FormConfig>(config: T): T => config;

View File

@ -1,20 +1,19 @@
{
"version": "1.7.11",
"version": "1.7.7",
"name": "@tmagic/form",
"type": "module",
"sideEffects": [
"dist/style.css",
"dist/es/style.css",
"src/theme/*"
],
"main": "dist/tmagic-form.umd.cjs",
"module": "dist/es/index.js",
"module": "dist/tmagic-form.js",
"style": "dist/style.css",
"types": "types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./dist/es/index.js",
"import": "./dist/tmagic-form.js",
"require": "./dist/tmagic-form.umd.cjs"
},
"./dist/style.css": {

View File

@ -218,13 +218,13 @@ const getTextByName = (name: string, config: FormConfig = props.config): string
return typeof item.text === 'string' ? item.text : undefined;
}
if ('items' in item && Array.isArray(item.items)) {
if (item.items && Array.isArray(item.items)) {
const result = findInConfig(item.items, remainingParts);
if (result !== undefined) return result;
}
}
if ('items' in item && Array.isArray(item.items)) {
if (item.items && Array.isArray(item.items)) {
const result = findInConfig(item.items, parts);
if (result !== undefined) return result;
}

View File

@ -117,25 +117,19 @@ const stepActive = ref(1);
const bodyHeight = ref(`${document.body.clientHeight - 194}px`);
const stepCount = computed(() => {
if (!Array.isArray(props.config)) {
return 0;
}
for (const item of props.config) {
if ('type' in item && item.type === 'step') {
return (item as StepConfig).items.length;
const { length } = props.config;
for (let index = 0; index < length; index++) {
if (props.config[index].type === 'step') {
return (props.config[index] as StepConfig).items.length;
}
}
return 0;
});
const hasStep = computed(() => {
if (!Array.isArray(props.config)) {
return false;
}
for (const item of props.config) {
if ('type' in item && item.type === 'step') {
const { length } = props.config;
for (let index = 0; index < length; index++) {
if (props.config[index].type === 'step') {
return true;
}
}

View File

@ -1,5 +1,5 @@
<template>
<TMagicCol v-show="display && type !== 'hidden'" :span="span">
<TMagicCol v-show="display && config.type !== 'hidden'" :span="span">
<Container
:model="model"
:lastValues="lastValues"
@ -21,7 +21,7 @@ import { computed, inject } from 'vue';
import { TMagicCol } from '@tmagic/design';
import type { ContainerChangeEventData, FormItemConfig, FormState } from '../schema';
import type { ChildConfig, ContainerChangeEventData, FormState } from '../schema';
import { display as displayFunction } from '../utils/form';
import Container from './Container.vue';
@ -34,8 +34,8 @@ const props = defineProps<{
model: any;
lastValues?: any;
isCompare?: boolean;
config: FormItemConfig;
labelWidth?: string | number;
config: ChildConfig;
labelWidth?: string;
expandMore?: boolean;
span?: number;
size?: string;
@ -52,6 +52,4 @@ const mForm = inject<FormState | undefined>('mForm');
const display = computed(() => displayFunction(mForm, props.config.display, props));
const changeHandler = (v: any, eventData: ContainerChangeEventData) => emit('change', v, eventData);
const onAddDiffCount = () => emit('addDiffCount');
const type = computed(() => (props.config as any).type);
</script>

View File

@ -1,11 +1,11 @@
<template>
<div
:data-tmagic-id="(config as Record<string, any>).id"
:data-tmagic-id="config.id"
:data-tmagic-form-item-prop="itemProp"
:class="`m-form-container m-container-${type || ''} ${config.className || ''}${config.tip ? ' has-tip' : ''}`"
:style="config.style"
>
<MHidden v-if="type === 'hidden'" :name="`${name}`" :prop="itemProp" :model="model"></MHidden>
<m-fields-hidden v-if="type === 'hidden'" v-bind="fieldsProps" :model="model"></m-fields-hidden>
<component
v-else-if="items && !text && type && display"
@ -28,7 +28,7 @@
<FormLabel
:tip="config.tip"
:type="type"
:use-label="(config as CheckboxConfig).useLabel"
:use-label="config.useLabel"
:label-title="config.labelTitle"
:text="text"
></FormLabel>
@ -61,7 +61,7 @@
></component>
</TMagicFormItem>
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel" placement="top">
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !config.useLabel" placement="top">
<TMagicIcon style="line-height: 40px; margin-left: 5px"><warning-filled /></TMagicIcon>
<template #content>
<div v-html="config.tip"></div>
@ -80,7 +80,7 @@
<FormLabel
:tip="config.tip"
:type="type"
:use-label="(config as CheckboxConfig).useLabel"
:use-label="config.useLabel"
:label-title="config.labelTitle"
:text="text"
></FormLabel>
@ -95,7 +95,7 @@
<component v-else v-bind="fieldsProps" :is="tagName" :model="lastValues" @change="onChangeHandler"></component>
</TMagicFormItem>
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel" placement="top">
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !config.useLabel" placement="top">
<TMagicIcon style="line-height: 40px; margin-left: 5px"><warning-filled /></TMagicIcon>
<template #content>
<div v-html="config.tip"></div>
@ -112,7 +112,7 @@
<FormLabel
:tip="config.tip"
:type="type"
:use-label="(config as CheckboxConfig).useLabel"
:use-label="config.useLabel"
:label-title="config.labelTitle"
:text="text"
></FormLabel>
@ -127,7 +127,7 @@
<component v-else v-bind="fieldsProps" :is="tagName" :model="model" @change="onChangeHandler"></component>
</TMagicFormItem>
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel" placement="top">
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !config.useLabel" placement="top">
<TMagicIcon style="line-height: 40px; margin-left: 5px"><warning-filled /></TMagicIcon>
<template #content>
<div v-html="config.tip"></div>
@ -172,18 +172,14 @@ import { isEqual } from 'lodash-es';
import { TMagicButton, TMagicFormItem, TMagicIcon, TMagicTooltip } from '@tmagic/design';
import { getValueByKeyPath } from '@tmagic/utils';
import MHidden from '../fields/Hidden.vue';
import type {
CheckboxConfig,
ComponentConfig,
ChildConfig,
ContainerChangeEventData,
ContainerCommonConfig,
FormItemConfig,
FormState,
FormValue,
ToolTipConfigType,
} from '../schema';
import { getField } from '../utils/config';
import { createObjectProp, display as displayFunction, filterFunction, getRules } from '../utils/form';
import FormLabel from './FormLabel.vue';
@ -198,10 +194,10 @@ const props = withDefaults(
model: FormValue;
/** 需对比的值(开启对比模式时传入) */
lastValues?: FormValue;
config: FormItemConfig;
config: ChildConfig;
prop?: string;
disabled?: boolean;
labelWidth?: string | number;
labelWidth?: string;
expandMore?: boolean;
stepActive?: string | number;
size?: string;
@ -252,20 +248,11 @@ const itemProp = computed(() => {
return `${n}`;
});
const type = computed((): string => {
let type = 'type' in props.config ? props.config.type : '';
type = type && filterFunction<string>(mForm, type, props);
if (type === 'form') return '';
if (type === 'container') return '';
return type?.replace(/([A-Z])/g, '-$1').toLowerCase() || (items.value ? '' : 'text');
});
const tagName = computed(() => {
if (type.value === 'component' && (props.config as ComponentConfig).component) {
return (props.config as ComponentConfig).component;
if (type.value === 'component' && props.config.component) {
return props.config.component;
}
return getField(type.value || 'container') || `m-${items.value ? 'form' : 'fields'}-${type.value}`;
return `m-${items.value ? 'form' : 'fields'}-${type.value}`;
});
const disabled = computed(() => props.disabled || filterFunction(mForm, props.config.disabled, props));
@ -289,6 +276,14 @@ const tooltip = computed(() => {
const rule = computed(() => getRules(mForm, props.config.rules, props));
const type = computed((): string => {
let { type } = props.config;
type = type && filterFunction<string>(mForm, type, props);
if (type === 'form') return '';
if (type === 'container') return '';
return type?.replace(/([A-Z])/g, '-$1').toLowerCase() || (items.value ? '' : 'text');
});
const display = computed((): boolean => {
const value = displayFunction(mForm, props.config.display, props);
@ -304,7 +299,7 @@ const fieldsProps = computed(() => ({
name: name.value,
disabled: disabled.value,
prop: itemProp.value,
key: (props.config as Record<string, any>)[mForm?.keyProps],
key: props.config[mForm?.keyProps],
style: props.config.fieldStyle,
}));

View File

@ -10,7 +10,7 @@
v-for="(item, index) in model[name]"
:key="index"
:model="item"
:lastValues="getLastValues(lastValues?.[name], Number(index))"
:lastValues="getLastValues(lastValues[name], Number(index))"
:is-compare="isCompare"
:config="config"
:prop="prop"
@ -27,19 +27,30 @@
></MFieldsGroupListItem>
<div class="m-fields-group-list-footer">
<slot name="toggle-button"></slot>
<TMagicButton v-if="config.enableToggleMode" :icon="Grid" size="small" @click="toggleMode"
>切换为表格</TMagicButton
>
<div style="display: flex; justify-content: flex-end; flex: 1">
<slot name="add-button" :trigger="addHandler"></slot>
<TMagicButton
v-if="addable"
:size="config.enableToggleMode ? 'small' : 'default'"
:icon="Plus"
v-bind="config.addButtonConfig?.props || { type: 'primary' }"
:disabled="disabled"
@click="addHandler"
>{{ config.addButtonConfig?.text || '新增' }}</TMagicButton
>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue';
import { computed, inject } from 'vue';
import { Grid, Plus } from '@element-plus/icons-vue';
import { cloneDeep } from 'lodash-es';
import { tMagicMessage } from '@tmagic/design';
import { TMagicButton } from '@tmagic/design';
import type { ContainerChangeEventData, FormState, GroupListConfig } from '../schema';
import { initValue } from '../utils/form';
@ -60,7 +71,6 @@ const props = defineProps<{
prop?: string;
size?: string;
disabled?: boolean;
showIndex?: boolean;
}>();
const emit = defineEmits<{
@ -70,6 +80,21 @@ const emit = defineEmits<{
const mForm = inject<FormState | undefined>('mForm');
const addable = computed(() => {
if (!props.name) return false;
if (typeof props.config.addable === 'function') {
return props.config.addable(mForm, {
model: props.model[props.name],
formValue: mForm?.values,
prop: props.prop,
config: props.config,
});
}
return typeof props.config.addable === 'undefined' ? true : props.config.addable;
});
const changeHandler = (v: any, eventData: ContainerChangeEventData) => {
emit('change', props.model, eventData);
};
@ -77,20 +102,6 @@ const changeHandler = (v: any, eventData: ContainerChangeEventData) => {
const addHandler = async () => {
if (!props.name) return false;
if (props.config.max && props.model[props.name].length >= props.config.max) {
tMagicMessage.error(`最多新增配置不能超过${props.config.max}`);
return;
}
if (typeof props.config.beforeAddRow === 'function') {
const beforeCheckRes = await props.config.beforeAddRow(mForm, {
model: props.model[props.name],
formValue: mForm?.values,
prop: props.prop,
});
if (!beforeCheckRes) return;
}
let initValues = {};
if (typeof props.config.defaultAdd === 'function') {
@ -142,6 +153,17 @@ const swapHandler = (idx1: number, idx2: number) => {
emit('change', props.model[props.name]);
};
const toggleMode = () => {
props.config.type = 'table';
props.config.groupItems = props.config.items;
props.config.items = (props.config.tableItems ||
props.config.items.map((item: any) => ({
...item,
label: item.label || item.text,
text: null,
}))) as any;
};
const onAddDiffCount = () => emit('addDiffCount');
const getLastValues = (item: any, index: number) => item?.[index] || {};

View File

@ -144,9 +144,7 @@ const rowConfig = computed(() => ({
span: props.config.span || 24,
items: props.config.items,
labelWidth: props.config.labelWidth,
[mForm?.keyProp || '__key']: `${(props.config as Record<string, any>)[mForm?.keyProp || '__key']}${String(
props.index,
)}`,
[mForm?.keyProp || '__key']: `${props.config[mForm?.keyProp || '__key']}${String(props.index)}`,
}));
const title = computed(() => {

View File

@ -1,181 +0,0 @@
<template>
<component
:is="displayMode === 'table' ? MFormTable : MFormGroupList"
v-bind="$attrs"
:model="model"
:name="`${name}`"
:config="currentConfig"
:disabled="disabled"
:size="size"
:is-compare="isCompare"
:last-values="lastValues"
:prop="prop"
:label-width="labelWidth"
:show-index="showIndex"
:sort-key="sortKey"
:sort="sort"
@change="onChange"
@select="onSelect"
@addDiffCount="onAddDiffCount"
>
<template #toggle-button>
<TMagicButton
v-if="config.enableToggleMode || enableToggleMode"
:icon="Grid"
size="small"
@click="toggleDisplayMode"
>
{{ displayMode === 'table' ? '展开配置' : '切换为表格' }}
</TMagicButton>
</template>
<template #add-button="{ trigger }">
<TMagicButton
v-if="addable"
:class="displayMode === 'table' ? 'm-form-table-add-button' : ''"
:size="addButtonSize"
:plain="displayMode === 'table'"
:icon="Plus"
:disabled="disabled"
v-bind="currentConfig.addButtonConfig?.props || { type: 'primary' }"
@click="trigger"
>
{{ currentConfig.addButtonConfig?.text || (displayMode === 'table' ? '新增一行' : '新增') }}
</TMagicButton>
</template>
</component>
</template>
<script setup lang="ts">
import { computed, inject, ref } from 'vue';
import { Grid, Plus } from '@element-plus/icons-vue';
import { TMagicButton } from '@tmagic/design';
import type { FormState, GroupListConfig, TableConfig } from '@tmagic/form-schema';
import type { ContainerChangeEventData } from '../schema';
import MFormTable from '../table/Table.vue';
import MFormGroupList from './GroupList.vue';
defineOptions({
name: 'MFormTableGroupList',
inheritAttrs: false,
});
const props = defineProps<{
model: any;
lastValues?: any;
isCompare?: boolean;
config: TableConfig | GroupListConfig;
name: string;
prop?: string;
labelWidth?: string;
disabled?: boolean;
size?: string;
enableToggleMode?: true;
showIndex?: boolean;
sortKey?: string;
sort?: boolean;
}>();
const emit = defineEmits(['change', 'select', 'addDiffCount']);
const mForm = inject<FormState | undefined>('mForm');
const addable = computed(() => {
const modelName = props.name || props.config.name || '';
if (!modelName) return false;
if (!props.model[modelName].length) {
return true;
}
if (typeof props.config.addable === 'function') {
return props.config.addable(mForm, {
model: props.model[modelName],
formValue: mForm?.values,
prop: props.prop,
config: props.config,
});
}
return typeof props.config.addable === 'undefined' ? true : props.config.addable;
});
const isGroupListType = (type: string | undefined) => type === 'groupList' || type === 'group-list';
const displayMode = ref<'table' | 'groupList'>(isGroupListType(props.config.type) ? 'groupList' : 'table');
const calcLabelWidth = (label: string) => {
if (!label) return '0px';
const zhLength = label.match(/[^\x00-\xff]/g)?.length || 0;
const chLength = label.length - zhLength;
return `${Math.max(chLength * 8 + zhLength * 20, 80)}px`;
};
// config table table
// groupList table config
const tableConfig = computed<TableConfig>(() => {
if (!isGroupListType(props.config.type)) {
return props.config as TableConfig;
}
const source = props.config as GroupListConfig;
return {
...props.config,
type: 'table',
groupItems: source.items,
items:
source.tableItems ||
(source.items as any[]).map((item: any) => ({
...item,
label: item.label || item.text,
text: null,
})),
} as any as TableConfig;
});
// groupList config
const groupListConfig = computed<GroupListConfig>(() => {
if (isGroupListType(props.config.type)) {
return props.config as GroupListConfig;
}
const source = props.config as TableConfig;
return {
...props.config,
type: 'groupList',
tableItems: source.items,
items:
source.groupItems ||
(source.items as any[]).map((item: any) => {
const text = item.text || item.label;
return {
...item,
text,
labelWidth: calcLabelWidth(text),
span: item.span || 12,
};
}),
} as any as GroupListConfig;
});
// displayMode `<component :is>` any
const currentConfig = computed<any>(() => (displayMode.value === 'table' ? tableConfig.value : groupListConfig.value));
// Table/GroupList
const addButtonSize = computed(() => {
if (displayMode.value === 'table') return 'small';
return props.config.enableToggleMode !== false ? 'small' : 'default';
});
const toggleDisplayMode = () => {
displayMode.value = displayMode.value === 'table' ? 'groupList' : 'table';
};
const onChange = (v: any, eventData?: ContainerChangeEventData) => emit('change', v, eventData);
const onSelect = (...args: any[]) => emit('select', ...args);
const onAddDiffCount = () => emit('addDiffCount');
</script>

View File

@ -3,18 +3,14 @@
</template>
<script setup lang="ts">
import type { FormValue } from '../schema';
import type { FieldProps, HiddenConfig } from '../schema';
import { useAddField } from '../utils/useAddField';
defineOptions({
name: 'MFormHidden',
});
const props = defineProps<{
model: FormValue;
name: string;
prop: string;
}>();
const props = defineProps<FieldProps<HiddenConfig>>();
useAddField(props.prop);
</script>

View File

@ -10,7 +10,7 @@
<TMagicTooltip :disabled="!Boolean(option.tooltip)" placement="top-start" :content="option.tooltip">
<div>
<TMagicIcon v-if="option.icon" :size="iconSize"><component :is="option.icon"></component></TMagicIcon>
<span v-if="option.text">{{ option.text }}</span>
<span>{{ option.text }}</span>
</div>
</TMagicTooltip>
</component>

View File

@ -21,7 +21,7 @@ import dayjs from 'dayjs';
import { TMagicTimePicker } from '@tmagic/design';
import type { ChangeRecord, FieldProps, TimerangeConfig } from '../schema';
import type { ChangeRecord, DaterangeConfig, FieldProps } from '../schema';
import { datetimeFormatter } from '../utils/form';
import { useAddField } from '../utils/useAddField';
@ -29,7 +29,7 @@ defineOptions({
name: 'MFormTimeRange',
});
const props = defineProps<FieldProps<TimerangeConfig>>();
const props = defineProps<FieldProps<DaterangeConfig>>();
const emit = defineEmits(['change']);

View File

@ -16,8 +16,44 @@
* limitations under the License.
*/
import type { App } from 'vue';
import Container from './containers/Container.vue';
import Fieldset from './containers/Fieldset.vue';
import FlexLayout from './containers/FlexLayout.vue';
import GroupList from './containers/GroupList.vue';
import Panel from './containers/Panel.vue';
import Row from './containers/Row.vue';
import MStep from './containers/Step.vue';
import Tabs from './containers/Tabs.vue';
import Cascader from './fields/Cascader.vue';
import Checkbox from './fields/Checkbox.vue';
import CheckboxGroup from './fields/CheckboxGroup.vue';
import ColorPicker from './fields/ColorPicker.vue';
import Date from './fields/Date.vue';
import Daterange from './fields/Daterange.vue';
import DateTime from './fields/DateTime.vue';
import Display from './fields/Display.vue';
import DynamicField from './fields/DynamicField.vue';
import Hidden from './fields/Hidden.vue';
import Link from './fields/Link.vue';
import Number from './fields/Number.vue';
import NumberRange from './fields/NumberRange.vue';
import RadioGroup from './fields/RadioGroup.vue';
import Select from './fields/Select.vue';
import Switch from './fields/Switch.vue';
import Text from './fields/Text.vue';
import Textarea from './fields/Textarea.vue';
import Time from './fields/Time.vue';
import Timerange from './fields/Timerange.vue';
import Table from './table/Table.vue';
import { setConfig } from './utils/config';
import Form from './Form.vue';
import FormDialog from './FormDialog.vue';
import type { FormConfig } from './schema';
import './theme/index.scss';
export * from './schema';
export * from './utils/form';
export * from './utils/useAddField';
@ -32,9 +68,8 @@ export { default as MFlexLayout } from './containers/FlexLayout.vue';
export { default as MPanel } from './containers/Panel.vue';
export { default as MRow } from './containers/Row.vue';
export { default as MTabs } from './containers/Tabs.vue';
export { default as MTable } from './containers/TableGroupList.vue';
export { default as MGroupList } from './containers/TableGroupList.vue';
export { default as MTableGroupList } from './containers/TableGroupList.vue';
export { default as MTable } from './table/Table.vue';
export { default as MGroupList } from './containers/GroupList.vue';
export { default as MText } from './fields/Text.vue';
export { default as MNumber } from './fields/Number.vue';
export { default as MNumberRange } from './fields/NumberRange.vue';
@ -56,14 +91,52 @@ export { default as MSelect } from './fields/Select.vue';
export { default as MCascader } from './fields/Cascader.vue';
export { default as MDynamicField } from './fields/DynamicField.vue';
export {
deleteField as deleteFormField,
getField as getFormField,
registerField as registerFormField,
} from './utils/config';
export type { FormInstallOptions } from './plugin';
export const createForm = <T extends [] = []>(config: FormConfig | T) => config;
export { default } from './plugin';
export interface FormInstallOptions {
[key: string]: any;
}
const defaultInstallOpt: FormInstallOptions = {};
export default {
install(app: App, opt: FormInstallOptions = {}) {
const option = Object.assign(defaultInstallOpt, opt);
app.config.globalProperties.$MAGIC_FORM = option;
setConfig(option);
app.component('m-form', Form);
app.component('m-form-dialog', FormDialog);
app.component('m-form-container', Container);
app.component('m-form-fieldset', Fieldset);
app.component('m-form-group-list', GroupList);
app.component('m-form-panel', Panel);
app.component('m-form-row', Row);
app.component('m-form-step', MStep);
app.component('m-form-table', Table);
app.component('m-form-tab', Tabs);
app.component('m-form-flex-layout', FlexLayout);
app.component('m-fields-text', Text);
app.component('m-fields-img-upload', Text);
app.component('m-fields-number', Number);
app.component('m-fields-number-range', NumberRange);
app.component('m-fields-textarea', Textarea);
app.component('m-fields-hidden', Hidden);
app.component('m-fields-date', Date);
app.component('m-fields-datetime', DateTime);
app.component('m-fields-daterange', Daterange);
app.component('m-fields-timerange', Timerange);
app.component('m-fields-time', Time);
app.component('m-fields-checkbox', Checkbox);
app.component('m-fields-switch', Switch);
app.component('m-fields-color-picker', ColorPicker);
app.component('m-fields-checkbox-group', CheckboxGroup);
app.component('m-fields-radio-group', RadioGroup);
app.component('m-fields-display', Display);
app.component('m-fields-link', Link);
app.component('m-fields-select', Select);
app.component('m-fields-cascader', Cascader);
app.component('m-fields-dynamic-field', DynamicField);
},
};

View File

@ -1,101 +0,0 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type App } from 'vue';
import Container from './containers/Container.vue';
import Fieldset from './containers/Fieldset.vue';
import FlexLayout from './containers/FlexLayout.vue';
import Panel from './containers/Panel.vue';
import Row from './containers/Row.vue';
import MStep from './containers/Step.vue';
import TableGroupList from './containers/TableGroupList.vue';
import Tabs from './containers/Tabs.vue';
import Cascader from './fields/Cascader.vue';
import Checkbox from './fields/Checkbox.vue';
import CheckboxGroup from './fields/CheckboxGroup.vue';
import ColorPicker from './fields/ColorPicker.vue';
import Date from './fields/Date.vue';
import Daterange from './fields/Daterange.vue';
import DateTime from './fields/DateTime.vue';
import Display from './fields/Display.vue';
import DynamicField from './fields/DynamicField.vue';
import Hidden from './fields/Hidden.vue';
import Link from './fields/Link.vue';
import Number from './fields/Number.vue';
import NumberRange from './fields/NumberRange.vue';
import RadioGroup from './fields/RadioGroup.vue';
import Select from './fields/Select.vue';
import Switch from './fields/Switch.vue';
import Text from './fields/Text.vue';
import Textarea from './fields/Textarea.vue';
import Time from './fields/Time.vue';
import Timerange from './fields/Timerange.vue';
import { setConfig } from './utils/config';
import Form from './Form.vue';
import FormDialog from './FormDialog.vue';
import './theme/index.scss';
export interface FormInstallOptions {
[key: string]: any;
}
const defaultInstallOpt: FormInstallOptions = {};
export default {
install(app: App, opt: FormInstallOptions = {}) {
const option = Object.assign(defaultInstallOpt, opt);
app.config.globalProperties.$MAGIC_FORM = option;
setConfig(option);
app.component('m-form', Form);
app.component('m-form-dialog', FormDialog);
app.component('m-form-container', Container);
app.component('m-form-fieldset', Fieldset);
app.component('m-form-group-list', TableGroupList);
app.component('m-form-panel', Panel);
app.component('m-form-row', Row);
app.component('m-form-step', MStep);
app.component('m-form-table', TableGroupList);
app.component('m-form-tab', Tabs);
app.component('m-form-flex-layout', FlexLayout);
app.component('m-fields-text', Text);
app.component('m-fields-img-upload', Text);
app.component('m-fields-number', Number);
app.component('m-fields-number-range', NumberRange);
app.component('m-fields-textarea', Textarea);
app.component('m-fields-hidden', Hidden);
app.component('m-fields-date', Date);
app.component('m-fields-datetime', DateTime);
app.component('m-fields-daterange', Daterange);
app.component('m-fields-timerange', Timerange);
app.component('m-fields-time', Time);
app.component('m-fields-checkbox', Checkbox);
app.component('m-fields-switch', Switch);
app.component('m-fields-color-picker', ColorPicker);
app.component('m-fields-checkbox-group', CheckboxGroup);
app.component('m-fields-radio-group', RadioGroup);
app.component('m-fields-display', Display);
app.component('m-fields-link', Link);
app.component('m-fields-select', Select);
app.component('m-fields-cascader', Cascader);
app.component('m-fields-dynamic-field', DynamicField);
},
};

View File

@ -4,7 +4,7 @@
v-bind="$attrs"
class="m-fields-table-wrap"
:class="{ fixed: isFullscreen }"
:style="isFullscreen ? `z-index: ${fullscreenZIndex}` : ''"
:style="isFullscreen ? `z-index: ${nextZIndex()}` : ''"
>
<div class="m-fields-table" :class="{ 'm-fields-table-item-extra': config.itemExtra }">
<span v-if="config.extra" style="color: rgba(0, 0, 0, 0.45)" v-html="config.extra"></span>
@ -34,7 +34,13 @@
<div style="display: flex; justify-content: space-between; margin: 10px 0">
<div style="display: flex">
<slot name="toggle-button" v-if="!isFullscreen"></slot>
<TMagicButton
:icon="Grid"
size="small"
@click="toggleMode"
v-if="enableToggleMode && config.enableToggleMode !== false && !isFullscreen"
>展开配置</TMagicButton
>
<TMagicButton
:icon="FullScreen"
size="small"
@ -58,7 +64,17 @@
>清空</TMagicButton
>
</div>
<slot name="add-button" :trigger="newHandler"></slot>
<TMagicButton
v-if="addable"
class="m-form-table-add-button"
size="small"
plain
:icon="Plus"
v-bind="config.addButtonConfig?.props || { type: 'primary' }"
:disabled="disabled"
@click="newHandler()"
>{{ config.addButtonConfig?.text || '新增一行' }}</TMagicButton
>
</div>
<div class="bottom" style="text-align: right" v-if="config.pagination">
@ -80,8 +96,8 @@
</template>
<script setup lang="ts">
import { computed, ref, useTemplateRef, watch } from 'vue';
import { FullScreen } from '@element-plus/icons-vue';
import { computed, ref, useTemplateRef } from 'vue';
import { FullScreen, Grid, Plus } from '@element-plus/icons-vue';
import { TMagicButton, TMagicPagination, TMagicTable, TMagicTooltip, TMagicUpload, useZIndex } from '@tmagic/design';
@ -104,6 +120,7 @@ defineOptions({
const props = withDefaults(defineProps<TableProps>(), {
prop: '',
sortKey: '',
enableToggleMode: true,
showIndex: true,
lastValues: () => ({}),
isCompare: false,
@ -121,24 +138,42 @@ const { pageSize, currentPage, paginationData, handleSizeChange, handleCurrentCh
const { nextZIndex } = useZIndex();
const updateKey = ref(1);
const fullscreenZIndex = ref(nextZIndex());
const { newHandler } = useAdd(props, emit);
const { addable, newHandler } = useAdd(props, emit);
const { columns } = useTableColumns(props, emit, currentPage, pageSize, modelName);
useSortable(props, emit, tMagicTableRef, modelName, updateKey);
const { isFullscreen, toggleFullscreen } = useFullscreen();
watch(isFullscreen, (value) => {
if (value) {
fullscreenZIndex.value = nextZIndex();
}
});
const { importable, excelHandler, clearHandler } = useImport(props, emit, newHandler);
const { selectHandle, toggleRowSelection } = useSelection(props, emit, tMagicTableRef);
const data = computed(() => (props.config.pagination ? paginationData.value : props.model[modelName.value]));
const toggleMode = () => {
const calcLabelWidth = (label: string) => {
if (!label) return '0px';
const zhLength = label.match(/[^\x00-\xff]/g)?.length || 0;
const chLength = label.length - zhLength;
return `${Math.max(chLength * 8 + zhLength * 20, 80)}px`;
};
// groupList
props.config.type = 'groupList';
props.config.enableToggleMode = true;
props.config.tableItems = props.config.items;
props.config.items =
props.config.groupItems ||
props.config.items.map((item: any) => {
const text = item.text || item.label;
const labelWidth = calcLabelWidth(text);
return {
...item,
text,
labelWidth,
span: item.span || 12,
};
});
};
const sortChangeHandler = (sortOptions: SortProp) => {
const modelName = props.name || props.config.name || '';
sortChange(props.model[modelName], sortOptions);

View File

@ -13,5 +13,6 @@ export interface TableProps {
sortKey?: string;
text?: string;
size?: string;
enableToggleMode?: boolean;
showIndex?: boolean;
}

View File

@ -1,7 +1,7 @@
import { inject } from 'vue';
import { computed, inject } from 'vue';
import { tMagicMessage } from '@tmagic/design';
import type { FormConfig, FormState } from '@tmagic/form-schema';
import type { FormState } from '@tmagic/form-schema';
import { initValue } from '../utils/form';
@ -13,6 +13,21 @@ export const useAdd = (
) => {
const mForm = inject<FormState | undefined>('mForm');
const addable = computed(() => {
const modelName = props.name || props.config.name || '';
if (!props.model[modelName].length) {
return true;
}
if (typeof props.config.addable === 'function') {
return props.config.addable(mForm, {
model: props.model[modelName],
formValue: mForm?.values,
prop: props.prop,
});
}
return typeof props.config.addable === 'undefined' ? true : props.config.addable;
});
const newHandler = async (row?: any) => {
const modelName = props.name || props.config.name || '';
@ -22,7 +37,7 @@ export const useAdd = (
}
if (typeof props.config.beforeAddRow === 'function') {
const beforeCheckRes = await props.config.beforeAddRow(mForm, {
const beforeCheckRes = props.config.beforeAddRow(mForm, {
model: props.model[modelName],
formValue: mForm?.values,
prop: props.prop,
@ -71,7 +86,7 @@ export const useAdd = (
}
inputs = await initValue(mForm, {
config: columns as FormConfig,
config: columns,
initValues: inputs,
});
}
@ -91,6 +106,7 @@ export const useAdd = (
};
return {
addable,
newHandler,
};
};

View File

@ -1,5 +1,5 @@
import { inject, nextTick, type Ref, type ShallowRef, watchEffect } from 'vue';
import type { default as SortableType, SortableEvent } from 'sortablejs';
import Sortable, { type SortableEvent } from 'sortablejs';
import { type TMagicTable } from '@tmagic/design';
import type { FormState } from '@tmagic/form-schema';
@ -8,9 +8,6 @@ import { sortArray } from '../utils/form';
import type { TableProps } from './type';
let SortablePromise: Promise<typeof SortableType> | undefined;
const loadSortable = () => (SortablePromise ??= import('sortablejs').then((m) => m.default));
export const useSortable = (
props: TableProps,
emit: (event: 'select' | 'change' | 'addDiffCount', ...args: any[]) => void,
@ -20,16 +17,15 @@ export const useSortable = (
) => {
const mForm = inject<FormState | undefined>('mForm');
let sortable: SortableType | undefined;
const rowDrop = async () => {
let sortable: Sortable | undefined;
const rowDrop = () => {
sortable?.destroy();
const tableEl = tMagicTableRef.value?.getEl();
const tBodyEl = tableEl?.querySelector('.el-table__body > tbody') || tableEl?.querySelector('.t-table__body');
if (!tBodyEl) {
return;
}
sortable = (await loadSortable()).create(tBodyEl, {
sortable = Sortable.create(tBodyEl, {
draggable: '.tmagic-design-table-row',
filter: 'input', // 表单组件选字操作和触发拖拽会冲突,优先保证选字操作
preventOnFilter: false, // 允许选字

View File

@ -3,7 +3,7 @@ import { WarningFilled } from '@element-plus/icons-vue';
import { cloneDeep } from 'lodash-es';
import { type TableColumnOptions, TMagicIcon, TMagicTooltip } from '@tmagic/design';
import type { FormItemConfig, FormState, TableColumnConfig } from '@tmagic/form-schema';
import type { FormState, TableColumnConfig } from '@tmagic/form-schema';
import Container from '../containers/Container.vue';
import type { ContainerChangeEventData } from '../schema';
@ -68,7 +68,7 @@ export const useTableColumns = (
return `${props.prop}${props.prop ? '.' : ''}${index + 1 + currentPage.value * pageSize.value - 1}`;
};
const makeConfig = (config: TableColumnConfig, row: any): TableColumnConfig => {
const makeConfig = (config: TableColumnConfig, row: any) => {
const newConfig = cloneDeep(config);
if (typeof config.itemsFunction === 'function') {
newConfig.items = config.itemsFunction(row);
@ -199,7 +199,7 @@ export const useTableColumns = (
disabled: props.disabled,
prop: getProp($index),
rules: column.rules,
config: makeConfig(column, row) as FormItemConfig,
config: makeConfig(column, row),
model: row,
lastValues: lastData.value[$index],
isCompare: props.isCompare,

View File

@ -16,8 +16,6 @@
* limitations under the License.
*/
import type { Component } from 'vue';
let $MAGIC_FORM = {} as any;
const setConfig = (option: any): void => {
@ -26,17 +24,4 @@ const setConfig = (option: any): void => {
const getConfig = <T = unknown>(key: string): T => $MAGIC_FORM[key];
const fieldRegistry = new Map<string, Component>();
const registerField = (tagName: string, component: Component): void => {
if (fieldRegistry.has(tagName)) {
return;
}
fieldRegistry.set(tagName, component);
};
const getField = (tagName: string): Component | undefined => fieldRegistry.get(tagName);
const deleteField = (tagName: string): boolean => fieldRegistry.delete(tagName);
export { deleteField, getConfig, getField, registerField, setConfig };
export { getConfig, setConfig };

View File

@ -23,7 +23,7 @@ import { cloneDeep } from 'lodash-es';
import { getValueByKeyPath } from '@tmagic/utils';
import type {
import {
ChildConfig,
ContainerCommonConfig,
DaterangeConfig,
@ -34,7 +34,6 @@ import type {
HtmlField,
Rule,
SortProp,
TableConfig,
TabPaneConfig,
TypeFunction,
} from '../schema';
@ -119,8 +118,7 @@ const initValueItem = function (
) {
const { items } = item as ContainerCommonConfig;
const { names } = item as DaterangeConfig;
const type = 'type' in item ? item.type : '';
const { name } = item;
const { type, name } = item as ChildConfig;
if (isTableSelect(type) && name) {
value[name] = initValue[name] ?? '';
@ -150,15 +148,14 @@ const initValueItem = function (
setValue(mForm, value, initValue, item);
if (type === 'table') {
const tableConfig = item as TableConfig;
if (tableConfig.defautSort) {
sortChange(value[name], tableConfig.defautSort);
} else if (tableConfig.defaultSort) {
sortChange(value[name], tableConfig.defaultSort);
if (item.defautSort) {
sortChange(value[name], item.defautSort);
} else if (item.defaultSort) {
sortChange(value[name], item.defaultSort);
}
if (tableConfig.sort && tableConfig.sortKey) {
value[name].sort((a: any, b: any) => b[tableConfig.sortKey!] - a[tableConfig.sortKey!]);
if (item.sort && item.sortKey) {
value[name].sort((a: any, b: any) => b[item.sortKey] - a[item.sortKey]);
}
}
@ -172,8 +169,8 @@ export const createValues = function (
value: FormValue = {},
) {
if (Array.isArray(config)) {
config.forEach((item) => {
initValueItem(mForm, item as ChildConfig | TabPaneConfig, initValue, value);
config.forEach((item: ChildConfig | TabPaneConfig) => {
initValueItem(mForm, item, initValue, value);
});
}

View File

@ -1,15 +1,14 @@
{
"version": "1.7.11",
"version": "1.7.7",
"name": "@tmagic/schema",
"type": "module",
"sideEffects": false,
"main": "dist/tmagic-schema.umd.cjs",
"module": "dist/es/index.js",
"module": "dist/tmagic-schema.js",
"types": "types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./dist/es/index.js",
"import": "./dist/tmagic-schema.js",
"require": "./dist/tmagic-schema.umd.cjs"
},
"./*": "./*"

View File

@ -186,7 +186,7 @@ export interface CodeBlockContent {
/** 代码块名称 */
name: string;
/** 代码块内容 */
content: ((...args: any[]) => any) | Function;
content: ((...args: any[]) => any) | string;
/** 参数定义 */
params: CodeParam[] | [];
/** 注释 */

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