mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-04-23 02:08:53 +00:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3875ccde33 | ||
|
|
62a488ac66 | ||
|
|
27bb886054 | ||
|
|
fa09ab0b30 | ||
|
|
31f4d2b4e2 | ||
|
|
b2888962df | ||
|
|
b3f4e42716 | ||
|
|
6e07d5762b | ||
|
|
cfd5998242 | ||
|
|
26dc70d70c | ||
|
|
99c8274a1e | ||
|
|
334569e2d7 | ||
|
|
f583c7daec | ||
|
|
172a7a1c92 | ||
|
|
73c676931f | ||
|
|
df2d635682 | ||
|
|
0c2f2fd2b5 | ||
|
|
a7274198bf | ||
|
|
6f2e8d8d74 | ||
|
|
637a5bb69a | ||
|
|
42e7ac1b2e | ||
|
|
a3cdad9d91 | ||
|
|
711af79d72 | ||
|
|
728fbc035c | ||
|
|
01795455e9 | ||
|
|
9b56223359 | ||
|
|
984cea7ca3 | ||
|
|
9921ed8a2d | ||
|
|
e36d8d7cf8 | ||
|
|
35aa81514b | ||
|
|
450376872e | ||
|
|
e8714c96c9 | ||
|
|
feefd3779e | ||
|
|
1ae023db8c | ||
|
|
55eb546ad6 | ||
|
|
1664559d8f | ||
|
|
a34d0cdccc | ||
|
|
210ac436fc | ||
|
|
64c8ed15ab | ||
|
|
bada79e519 | ||
|
|
06a6068c47 | ||
|
|
58281a345b | ||
|
|
0bbafa153d | ||
|
|
f6bd647958 | ||
|
|
c79034befc | ||
|
|
88e6c7d377 | ||
|
|
92bd5cf942 | ||
|
|
5ae667b7ee | ||
|
|
18bfbefaf2 | ||
|
|
1b9492165c | ||
|
|
61f00a0fb7 | ||
|
|
6d91a7a844 | ||
|
|
3e4d49dd45 |
3
.github/workflows/pages.yml
vendored
3
.github/workflows/pages.yml
vendored
@ -41,6 +41,9 @@ 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:
|
||||
|
||||
56
AGENTS.md
Normal file
56
AGENTS.md
Normal file
@ -0,0 +1,56 @@
|
||||
# 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 | 变更日志 |
|
||||
3240
CHANGELOG.md
3240
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
477
LICENSE
477
LICENSE
@ -74,15 +74,6 @@ 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:
|
||||
--------------------------------------------------------------------
|
||||
@ -355,8 +346,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. uglify-js
|
||||
Copyright 2012-2019 (c) Mihai Bazon <mihai.bazon@gmail.com>
|
||||
2. terser
|
||||
Copyright 2012-2018 (c) Mihai Bazon <mihai.bazon@gmail.com>
|
||||
|
||||
|
||||
Terms of the BSD 2-Clause License:
|
||||
@ -390,8 +381,8 @@ Open Source Software Licensed under the BSD 3-Clause License:
|
||||
Copyright 2014 Yahoo! Inc.
|
||||
All rights reserved.
|
||||
|
||||
2. serialize-javascript
|
||||
Copyright 2014 Yahoo! Inc.
|
||||
2. highlight.js
|
||||
Copyright (c) 2006, Ivan Sagalaev.
|
||||
All rights reserved.
|
||||
|
||||
|
||||
@ -411,11 +402,14 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
|
||||
Open Source Software Licensed under the ISC License:
|
||||
--------------------------------------------------------------------
|
||||
1. raiz
|
||||
Copyright raiz original authour and authors
|
||||
1. c8
|
||||
Copyright (c) 2017, Contributors
|
||||
|
||||
2. axios-jsonp
|
||||
Copyright (c) Adonis
|
||||
2. picocolors
|
||||
Copyright (c) 2021 Alexey Raspopov, Kostiantyn Denysov, Anton Verinov
|
||||
|
||||
3. semver
|
||||
Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
|
||||
|
||||
Terms of the ISC License:
|
||||
@ -428,232 +422,239 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE
|
||||
|
||||
Open Source Software Licensed under the MIT License:
|
||||
--------------------------------------------------------------------
|
||||
1. events
|
||||
Copyright Joyent, Inc. and other Node contributors.
|
||||
|
||||
2. vite-plugin-dts
|
||||
Copyright (c) 2021-present qmhc
|
||||
|
||||
3. color
|
||||
Copyright (c) 2012 Heather Arthur
|
||||
|
||||
4. element-plus
|
||||
Copyright element-plus original authour and authors
|
||||
|
||||
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.
|
||||
|
||||
6. vue
|
||||
Copyright (c) 2018-present, Yuxi (Evan) You
|
||||
|
||||
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
|
||||
1. @commitlint/cli
|
||||
Copyright (c) 2016 - present Mario Nebl
|
||||
|
||||
31. @commitlint/config-conventional
|
||||
2. @commitlint/config-conventional
|
||||
Copyright (c) 2016 - present Mario Nebl
|
||||
|
||||
32. @typescript-eslint/eslint-plugin
|
||||
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
|
||||
|
||||
33. @vue/cli-plugin-babel
|
||||
Copyright (c) 2017-present, Yuxi (Evan) You
|
||||
18. @vitejs/plugin-legacy
|
||||
Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors
|
||||
|
||||
34. @vue/cli-plugin-unit-jest
|
||||
Copyright (c) 2017-present, Yuxi (Evan) You
|
||||
19. @vitejs/plugin-react-refresh
|
||||
Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors
|
||||
|
||||
35. babel-eslint
|
||||
Copyright (c) 2014-2016 Sebastian McKenzie <sebmck@gmail.com>
|
||||
20. @vitejs/plugin-vue
|
||||
Copyright (c) 2019-present, Yuxi (Evan) You and contributors
|
||||
|
||||
36. cz-conventional-changelog
|
||||
Copyright (c) 2015-2018 Commitizen Contributors
|
||||
21. @vitejs/plugin-vue-jsx
|
||||
Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors
|
||||
|
||||
37. eslint
|
||||
Copyright JS Foundation and other contributors, https://js.foundation
|
||||
22. @vitest/coverage-v8
|
||||
Copyright (c) 2021-present, Anthony Fu and Vitest contributors
|
||||
|
||||
38. eslint-plugin-import
|
||||
Copyright (c) 2015 Ben Mosher
|
||||
23. @vue/compiler-sfc
|
||||
Copyright (c) 2018-present, Yuxi (Evan) You
|
||||
|
||||
39. eslint-plugin-prettier
|
||||
Copyright © 2017 Andres Suarez and Teddy Katz
|
||||
24. @vue/test-utils
|
||||
Copyright (c) 2021-present vuejs
|
||||
|
||||
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
|
||||
25. axios
|
||||
Copyright (c) 2014-present Matt Zabriskie
|
||||
|
||||
48. core-js
|
||||
Copyright (c) 2014-2021 Denis Pushkarev
|
||||
26. buffer
|
||||
Copyright (c) Feross Aboukhadijeh, and other contributors
|
||||
|
||||
49. js-cookie
|
||||
Copyright (c) 2018 Copyright 2018 Klaus Hartl, Fagner Brack, GitHub Contributors
|
||||
27. cac
|
||||
Copyright (c) egoist <0x142857@gmail.com>
|
||||
|
||||
50. vue-router
|
||||
Copyright (c) 2020 Eduardo San Martin Morote
|
||||
28. chokidar
|
||||
Copyright (c) Paul Miller (https://paulmillr.com)
|
||||
|
||||
51. koa
|
||||
Copyright (c) 2019 Koa contributors
|
||||
29. commitizen
|
||||
Copyright (c) 2015 Jim Cummins
|
||||
|
||||
52. koa-bodyparser
|
||||
Copyright (c) 2014 dead_horse
|
||||
30. conventional-changelog-cli
|
||||
Copyright (c) Steve Mao
|
||||
|
||||
53. koa-router
|
||||
Copyright (c) 2015 Alexander C. Mingoia
|
||||
31. cosmiconfig
|
||||
Copyright (c) 2015 David Clark
|
||||
|
||||
54. koa-send
|
||||
Copyright (c) 2020 Koa contributors
|
||||
32. cz-conventional-changelog
|
||||
Copyright (c) 2015-2018 Commitizen Contributors
|
||||
|
||||
55. module-alias
|
||||
Copyright (c) 2018, Nick Gavrilov
|
||||
33. dayjs
|
||||
Copyright (c) 2018-present, iamkun
|
||||
|
||||
56. mysql2
|
||||
Copyright (c) 2016 Andrey Sidorov (sidorares@yandex.ru) and contributors
|
||||
34. deep-object-diff
|
||||
Copyright (c) 2017 Matt Phillips
|
||||
|
||||
57. sequelize
|
||||
Copyright (c) 2014-present Sequelize contributors
|
||||
35. deep-state-observer
|
||||
Copyright (c) neuronet.io
|
||||
|
||||
58. sequelize-typescript
|
||||
Copyright (c) 2017 Robin Buschmann
|
||||
36. element-plus
|
||||
Copyright element-plus original authour and authors
|
||||
|
||||
59. lodash
|
||||
Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
|
||||
37. emmet-monaco-es
|
||||
Copyright (c) Troy
|
||||
|
||||
60. jest
|
||||
Copyright (c) Facebook, Inc. and its affiliates.
|
||||
38. enquirer
|
||||
Copyright (c) 2016-present, Jon Schlinkert
|
||||
|
||||
61. fs-extra
|
||||
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
|
||||
Copyright Joyent, Inc. and other Node contributors.
|
||||
|
||||
47. execa
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com>
|
||||
|
||||
48. fs-extra
|
||||
Copyright (c) 2011-2017 JP Richardson
|
||||
|
||||
62. moment-timezone
|
||||
Copyright (c) JS Foundation and other contributors
|
||||
49. gesto
|
||||
Copyright (c) 2019 Daybrush
|
||||
|
||||
63. nodemon
|
||||
Copyright (C) Remy Sharp
|
||||
50. globals
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com>
|
||||
|
||||
64. ts-node
|
||||
Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)
|
||||
51. husky
|
||||
Copyright (c) 2021 typicode
|
||||
|
||||
65. tsconfig-paths
|
||||
Copyright (c) 2016 Jonas Kello
|
||||
52. jsdom
|
||||
Copyright (c) 2010 Elijah Insua
|
||||
|
||||
66. prettier
|
||||
Copyright © James Long and contributors
|
||||
53. keycon
|
||||
Copyright (c) 2019 Daybrush
|
||||
|
||||
67. @babel/core
|
||||
Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
||||
54. lint-staged
|
||||
Copyright (c) 2016 Andrey Okonetchnikov
|
||||
|
||||
68. @babel/preset-typescript
|
||||
Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
||||
55. merge-options
|
||||
Copyright (c) Michael Mayer
|
||||
|
||||
69. @types/fs-extra
|
||||
Copyright (c) Microsoft Corporation.
|
||||
56. minimist
|
||||
Copyright (c) James Halliday
|
||||
|
||||
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
|
||||
57. 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.
|
||||
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
|
||||
Copyright (c) 2018-present, Yuxi (Evan) You
|
||||
|
||||
77. vue-router
|
||||
Copyright (c) 2020 Eduardo San Martin Morote
|
||||
|
||||
78. vue-tsc
|
||||
Copyright (c) 2021-present Johnson Chu
|
||||
|
||||
|
||||
Terms of the MIT License:
|
||||
@ -5430,3 +5431,73 @@ 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.
|
||||
|
||||
|
||||
@ -552,9 +552,11 @@ export default defineConfig({
|
||||
|
||||
vite: {
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
define: {
|
||||
global: 'globalThis',
|
||||
rolldownOptions: {
|
||||
transform: {
|
||||
define: {
|
||||
global: 'globalThis',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -189,6 +189,8 @@
|
||||
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() : '';
|
||||
@ -210,6 +212,10 @@ export function stripTemplate(content) {
|
||||
export default {
|
||||
props: ['type', 'config'],
|
||||
|
||||
components: {
|
||||
MForm,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
codepen: {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tmagic/eslint-config",
|
||||
"version": "0.0.3",
|
||||
"version": "0.1.0",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
@ -9,20 +9,21 @@
|
||||
"url": "https://github.com/Tencent/tmagic-editor.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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/js": "^10.0.1",
|
||||
"@typescript-eslint/parser": "^8.58.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.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"
|
||||
"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"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=9.24.0",
|
||||
"prettier": ">=3.5.3"
|
||||
"eslint": ">=10.0.0",
|
||||
"prettier": ">=3.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ export default defineConfig([
|
||||
'*/**/public/**/*',
|
||||
'*/**/types/**/*',
|
||||
'*/**/*.config.ts',
|
||||
'./tepm/**/*',
|
||||
'vite-env.d.ts',
|
||||
]),
|
||||
...eslintConfig(path.join(path.dirname(fileURLToPath(import.meta.url)), 'tsconfig.json')),
|
||||
|
||||
25
package.json
25
package.json
@ -1,9 +1,9 @@
|
||||
{
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.10",
|
||||
"name": "tmagic",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.18.2",
|
||||
"packageManager": "pnpm@10.32.1",
|
||||
"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 && 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: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:playground": "pnpm --filter \"runtime-vue\" build && pnpm --filter \"tmagic-playground\" build",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:serve": "vitepress serve docs",
|
||||
@ -42,11 +42,10 @@
|
||||
"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.2",
|
||||
"@vitest/coverage-v8": "^4.0.12",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"@vitest/coverage-v8": "^4.1.0",
|
||||
"@vue/compiler-sfc": "catalog:",
|
||||
"c8": "^10.1.3",
|
||||
"commitizen": "^4.3.1",
|
||||
@ -55,7 +54,7 @@
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"element-plus": "^2.11.8",
|
||||
"enquirer": "^2.4.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint": "^10.0.3",
|
||||
"execa": "^9.6.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"husky": "^9.1.7",
|
||||
@ -63,11 +62,11 @@
|
||||
"lint-staged": "^16.2.7",
|
||||
"minimist": "^1.2.8",
|
||||
"picocolors": "^1.1.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier": "^3.8.1",
|
||||
"recast": "^0.23.11",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "4.44.1",
|
||||
"rollup-plugin-dts": "^6.2.3",
|
||||
"rolldown": "^1.0.0-rc.9",
|
||||
"rolldown-plugin-dts": "^0.22.5",
|
||||
"sass-embedded": "^1.93.3",
|
||||
"semver": "^7.7.3",
|
||||
"serialize-javascript": "^7.0.0",
|
||||
@ -75,9 +74,9 @@
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vitepress": "^1.6.4",
|
||||
"vitest": "^4.0.12",
|
||||
"vitest": "^4.1.0",
|
||||
"vue": "catalog:",
|
||||
"vue-tsc": "^3.1.4"
|
||||
"vue-tsc": "^3.2.6"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/cli",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"moduleResolution": "Node",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node16",
|
||||
"module": "node16",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declaration": true,
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
{
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/core",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"main": "dist/tmagic-core.umd.cjs",
|
||||
"module": "dist/tmagic-core.js",
|
||||
"module": "dist/es/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./dist/tmagic-core.js",
|
||||
"import": "./dist/es/index.js",
|
||||
"require": "./dist/tmagic-core.umd.cjs"
|
||||
},
|
||||
"./resetcss.css": {
|
||||
|
||||
@ -293,9 +293,11 @@ class App extends EventEmitter {
|
||||
|
||||
const method = methods.find((item) => item.name === methodName);
|
||||
if (method && typeof method.content === 'function') {
|
||||
await method.content({ app: this, params, dataSource, eventParams: args, flowState, node });
|
||||
} else if (typeof dataSource[methodName] === 'function') {
|
||||
await dataSource[methodName]();
|
||||
return await method.content({ app: this, params, dataSource, eventParams: args, flowState, node });
|
||||
}
|
||||
|
||||
if (typeof dataSource[methodName] === 'function') {
|
||||
return await dataSource[methodName]({ params });
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (this.errorHandler) {
|
||||
|
||||
@ -16,6 +16,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import type { Id, MComponent, MContainer, MPage, MPageFragment } from '@tmagic/schema';
|
||||
|
||||
import App from './App';
|
||||
@ -70,7 +72,7 @@ class Page extends Node {
|
||||
this.app.pageFragments.set(
|
||||
config.id,
|
||||
new Page({
|
||||
config: pageFragment,
|
||||
config: cloneDeep(pageFragment),
|
||||
app: this.app,
|
||||
}),
|
||||
);
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
{
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/data-source",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"main": "dist/tmagic-data-source.umd.cjs",
|
||||
"module": "dist/tmagic-data-source.js",
|
||||
"module": "dist/es/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./dist/tmagic-data-source.js",
|
||||
"import": "./dist/es/index.js",
|
||||
"require": "./dist/tmagic-data-source.umd.cjs"
|
||||
},
|
||||
"./*": "./*"
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { union } from 'lodash-es';
|
||||
import { cloneDeep, 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(newNode);
|
||||
pageFragment.setData(cloneDeep(newNode));
|
||||
} else if (pageFragment.data.id === page.id) {
|
||||
pageFragment.getNode(newNode.id, { strict: true })?.setData(newNode);
|
||||
pageFragment.getNode(newNode.id, { strict: true })?.setData(cloneDeep(newNode));
|
||||
if (!pageFragment.instance) {
|
||||
replaceChildNode(newNode, [pageFragment.data]);
|
||||
replaceChildNode(cloneDeep(newNode), [pageFragment.data]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 { getDefaultValueFromFields } from '@tmagic/core';
|
||||
import { DATA_SOURCE_SET_DATA_METHOD_NAME, getDefaultValueFromFields } from '@tmagic/core';
|
||||
|
||||
import { ObservedData } from '@data-source/observed-data/ObservedData';
|
||||
import { SimpleObservedData } from '@data-source/observed-data/SimpleObservedData';
|
||||
@ -51,6 +51,7 @@ 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;
|
||||
@ -58,6 +59,11 @@ 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;
|
||||
|
||||
@ -79,9 +79,9 @@ export default class HttpDataSource extends DataSource<HttpDataSourceSchema> {
|
||||
/** 请求函数 */
|
||||
#fetch?: RequestFunction;
|
||||
/** 请求前需要执行的函数队列 */
|
||||
#beforeRequest: ((...args: any[]) => any)[] = [];
|
||||
#beforeRequest: (Function | ((...args: any[]) => any))[] = [];
|
||||
/** 请求后需要执行的函数队列 */
|
||||
#afterRequest: ((...args: any[]) => any)[] = [];
|
||||
#afterRequest: (Function | ((...args: any[]) => any))[] = [];
|
||||
|
||||
#type = 'http';
|
||||
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
{
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/dep",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"main": "dist/tmagic-dep.umd.cjs",
|
||||
"module": "dist/tmagic-dep.js",
|
||||
"module": "dist/es/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./dist/tmagic-dep.js",
|
||||
"import": "./dist/es/index.js",
|
||||
"require": "./dist/tmagic-dep.umd.cjs"
|
||||
},
|
||||
"./*": "./*"
|
||||
|
||||
@ -71,7 +71,7 @@ export default class Target {
|
||||
|
||||
this.deps[id] = dep;
|
||||
|
||||
if (dep.keys.indexOf(key) === -1) {
|
||||
if (!dep.keys.includes(key)) {
|
||||
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 Boolean(dep?.keys.find((d) => d === key));
|
||||
return dep?.keys.includes(key) ?? false;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
|
||||
@ -5,6 +5,12 @@ 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';
|
||||
@ -159,7 +165,7 @@ export default class Watcher {
|
||||
};
|
||||
}
|
||||
|
||||
const clearedItemsNodeIds: (string | number)[] = [];
|
||||
const clearedItemsNodeIds = new Set<string | number>();
|
||||
traverseTarget(targetsList, (target) => {
|
||||
if (nodes) {
|
||||
for (const node of nodes) {
|
||||
@ -168,9 +174,9 @@ export default class Watcher {
|
||||
if (
|
||||
Array.isArray(node[this.childrenProp]) &&
|
||||
node[this.childrenProp].length &&
|
||||
!clearedItemsNodeIds.includes(node[this.idProp])
|
||||
!clearedItemsNodeIds.has(node[this.idProp])
|
||||
) {
|
||||
clearedItemsNodeIds.push(node[this.idProp]);
|
||||
clearedItemsNodeIds.add(node[this.idProp]);
|
||||
this.clear(node[this.childrenProp]);
|
||||
}
|
||||
}
|
||||
@ -190,12 +196,7 @@ export default class Watcher {
|
||||
}
|
||||
|
||||
public collectItem(node: TargetNode, target: Target, depExtendedData: DepExtendedData = {}, deep = false) {
|
||||
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)) {
|
||||
if (node[NODE_DISABLE_DATA_SOURCE_KEY] && DATA_SOURCE_TARGET_TYPES.has(target.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,8 @@ 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,
|
||||
@ -30,8 +32,7 @@ export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent, initi
|
||||
}
|
||||
|
||||
if (value?.hookType === HookType.CODE && Array.isArray(value.hookData)) {
|
||||
const index = value.hookData.findIndex((item: HookData) => item.codeId === id);
|
||||
return Boolean(index > -1);
|
||||
return value.hookData.some((item: HookData) => item.codeId === id);
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -54,12 +55,7 @@ export const isIncludeArrayField = (keys: string[], fields: DataSchema[]) => {
|
||||
f = field?.fields || [];
|
||||
|
||||
// 字段类型为数组并且后面没有数字索引
|
||||
return (
|
||||
field?.type === 'array' &&
|
||||
// 不是整数
|
||||
/^(?!\d+$).*$/.test(`${keys[index + 1]}`) &&
|
||||
index < keys.length - 1
|
||||
);
|
||||
return field?.type === 'array' && index < keys.length - 1 && !INTEGER_REGEXP.test(keys[index + 1]);
|
||||
});
|
||||
};
|
||||
|
||||
@ -78,33 +74,25 @@ export const isDataSourceTemplate = (value: any, ds: Pick<DataSourceSchema, 'id'
|
||||
return false;
|
||||
}
|
||||
|
||||
const arrayFieldTemplates = [];
|
||||
const fieldTemplates = [];
|
||||
|
||||
templates.forEach((tpl) => {
|
||||
for (const tpl of templates) {
|
||||
// 将${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) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ${dsId.array} ${dsId.array[0]} ${dsId.array[0].a} 这种是依赖
|
||||
// ${dsId.array.a} 这种不是依赖,这种需要再迭代器容器中的组件才能使用,依赖由迭代器处理
|
||||
if (isIncludeArrayField(keys, ds.fields)) {
|
||||
arrayFieldTemplates.push(tpl);
|
||||
} else {
|
||||
fieldTemplates.push(tpl);
|
||||
const includesArray = isIncludeArrayField(keys, ds.fields);
|
||||
if (hasArray === includesArray) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasArray) {
|
||||
return arrayFieldTemplates.length > 0;
|
||||
}
|
||||
|
||||
return fieldTemplates.length > 0;
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -184,7 +172,12 @@ export const isDataSourceTarget = (
|
||||
value: any,
|
||||
hasArray = false,
|
||||
) => {
|
||||
if (!value || !['string', 'object'].includes(typeof value)) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const valueType = typeof value;
|
||||
if (valueType !== 'string' && valueType !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -193,13 +186,13 @@ export const isDataSourceTarget = (
|
||||
}
|
||||
|
||||
// 或者在模板在使用数据源
|
||||
if (typeof value === 'string') {
|
||||
if (valueType === 'string') {
|
||||
return isDataSourceTemplate(value, ds, hasArray);
|
||||
}
|
||||
|
||||
// 关联数据源对象,如:{ isBindDataSource: true, dataSourceId: 'xxx'}
|
||||
// 使用data-source-select value: 'value' 可以配置出来
|
||||
if (isObject(value) && value?.isBindDataSource && value.dataSourceId && value.dataSourceId === ds.id) {
|
||||
if (isObject(value) && value.isBindDataSource && value.dataSourceId === ds.id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -210,10 +203,7 @@ export const isDataSourceTarget = (
|
||||
if (isUseDataSourceField(value, ds.id)) {
|
||||
const [, ...keys] = value;
|
||||
const includeArray = isIncludeArrayField(keys, ds.fields);
|
||||
if (hasArray) {
|
||||
return includeArray;
|
||||
}
|
||||
return !includeArray;
|
||||
return hasArray ? includeArray : !includeArray;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -235,12 +225,9 @@ export const isDataSourceCondTarget = (
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ds.fields?.find((field) => field.name === keys[0])) {
|
||||
if (ds.fields?.some((field) => field.name === keys[0])) {
|
||||
const includeArray = isIncludeArrayField(keys, ds.fields);
|
||||
if (hasArray) {
|
||||
return includeArray;
|
||||
}
|
||||
return !includeArray;
|
||||
return hasArray ? includeArray : !includeArray;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -282,12 +269,12 @@ export const createDataSourceMethodTarget = (
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ds.methods?.find((method) => method.name === methodName)) {
|
||||
if (ds.methods?.some((method) => method.name === methodName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 配置的方法名称可能会是数据源类中定义的,并不存在于methods中,所以这里判断如果methodName如果是字段名称,就表示配置的不是方法
|
||||
if (ds.fields?.find((field) => field.name === methodName)) {
|
||||
if (ds.fields?.some((field) => field.name === methodName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -300,11 +287,18 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
{
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/design",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
"dist/style.css",
|
||||
"dist/es/style.css",
|
||||
"src/theme/*"
|
||||
],
|
||||
"main": "dist/tmagic-design.umd.cjs",
|
||||
"module": "dist/tmagic-design.js",
|
||||
"module": "dist/es/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./dist/tmagic-design.js",
|
||||
"import": "./dist/es/index.js",
|
||||
"require": "./dist/tmagic-design.umd.cjs"
|
||||
},
|
||||
"./*": "./*"
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
{
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/editor",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
"dist/style.css",
|
||||
"dist/es/style.css",
|
||||
"src/theme/*"
|
||||
],
|
||||
"main": "dist/tmagic-editor.umd.cjs",
|
||||
"module": "dist/tmagic-editor.js",
|
||||
"module": "dist/es/index.js",
|
||||
"style": "dist/style.css",
|
||||
"types": "types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./dist/tmagic-editor.js",
|
||||
"import": "./dist/es/index.js",
|
||||
"require": "./dist/tmagic-editor.umd.cjs"
|
||||
},
|
||||
"./dist/style.css": {
|
||||
@ -68,18 +69,21 @@
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
"@types/serialize-javascript": "^5.0.4",
|
||||
"@types/sortablejs": "^1.15.9",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"type-fest": "^5.2.0"
|
||||
"@vue/test-utils": "^2.4.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tmagic/core": "workspace:*",
|
||||
"monaco-editor": "^0.48.0",
|
||||
"monaco-editor": "^0.55.1 ",
|
||||
"type-fest": "^5.2.0",
|
||||
"typescript": "catalog:",
|
||||
"vue": "catalog:"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
},
|
||||
"type-fest": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,6 +207,7 @@ const stageOptions: StageOptions = {
|
||||
renderType: props.renderType,
|
||||
guidesOptions: props.guidesOptions,
|
||||
disabledMultiSelect: props.disabledMultiSelect,
|
||||
beforeDblclick: props.beforeDblclick,
|
||||
};
|
||||
|
||||
stageOverlayService.set('stageOptions', stageOptions);
|
||||
|
||||
@ -65,8 +65,9 @@ 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';
|
||||
@ -87,7 +88,7 @@ const width = defineModel<number>('width', { default: 670 });
|
||||
const boxVisible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const props = defineProps<{
|
||||
content: CodeBlockContent;
|
||||
content: Omit<CodeBlockContent, 'content'> & { content: string };
|
||||
disabled?: boolean;
|
||||
isDataSource?: boolean;
|
||||
dataSourceType?: string;
|
||||
@ -118,7 +119,7 @@ const diffChange = () => {
|
||||
difVisible.value = false;
|
||||
};
|
||||
|
||||
const defaultParamColConfig: TableColumnConfig = {
|
||||
const defaultParamColConfig = defineFormItem<TableColumnConfig>({
|
||||
type: 'row',
|
||||
label: '参数类型',
|
||||
items: [
|
||||
@ -146,76 +147,79 @@ const defaultParamColConfig: TableColumnConfig = {
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
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: [
|
||||
const functionConfig = computed(
|
||||
() =>
|
||||
defineFormConfig([
|
||||
{
|
||||
type: 'text',
|
||||
label: '参数名',
|
||||
text: '名称',
|
||||
name: 'name',
|
||||
rules: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
label: '描述',
|
||||
name: 'extra',
|
||||
text: '描述',
|
||||
name: 'desc',
|
||||
},
|
||||
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);
|
||||
{
|
||||
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);
|
||||
|
||||
return code;
|
||||
} catch (error: any) {
|
||||
tMagicMessage.error(error.message);
|
||||
return code;
|
||||
} catch (error: any) {
|
||||
tMagicMessage.error(error.message);
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
]) as FormConfig,
|
||||
);
|
||||
|
||||
const parseContent = (content: any) => {
|
||||
if (typeof content === 'string') {
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
|
||||
import { type ContainerChangeEventData, type FormConfig, type FormValue, MForm } from '@tmagic/form';
|
||||
import { type ContainerChangeEventData, type FormItemConfig, 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: FormConfig = []) => [
|
||||
const getFormConfig = (items: FormItemConfig[] = []) => [
|
||||
{
|
||||
type: 'fieldset',
|
||||
items,
|
||||
@ -46,13 +46,29 @@ const getFormConfig = (items: FormConfig = []) => [
|
||||
|
||||
const codeParamsConfig = computed(() =>
|
||||
getFormConfig(
|
||||
props.paramsConfig.map(({ name, text, extra, ...config }) => ({
|
||||
type: 'data-source-field-select',
|
||||
name,
|
||||
text,
|
||||
extra,
|
||||
fieldConfig: config,
|
||||
})),
|
||||
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[],
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@ -98,6 +98,8 @@ 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;
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<m-fields-link :config="formConfig" :model="modelValue" name="form" @change="changeHandler"></m-fields-link>
|
||||
<MLink :config="formConfig" :model="modelValue" name="form" @change="changeHandler"></MLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import serialize from 'serialize-javascript';
|
||||
|
||||
import type { CodeLinkConfig, FieldProps } from '@tmagic/form';
|
||||
import type { CodeLinkConfig, FieldProps, MLink } from '@tmagic/form';
|
||||
|
||||
import { getEditorConfig } from '@editor/utils/config';
|
||||
|
||||
|
||||
@ -50,6 +50,7 @@ import {
|
||||
createValues,
|
||||
type FieldProps,
|
||||
filterFunction,
|
||||
type FormItemConfig,
|
||||
type FormState,
|
||||
MSelect,
|
||||
type SelectConfig,
|
||||
@ -115,7 +116,7 @@ watch(
|
||||
const selectConfig: SelectConfig = {
|
||||
type: 'select',
|
||||
name: props.name,
|
||||
disable: props.disabled,
|
||||
disabled: props.disabled,
|
||||
options: () => {
|
||||
if (codeDsl.value) {
|
||||
return map(codeDsl.value, (value, key) => ({
|
||||
@ -141,7 +142,9 @@ const onCodeIdChangeHandler = (value: any) => {
|
||||
|
||||
changeRecords.push({
|
||||
propPath: props.prop.replace(`${props.name}`, 'params'),
|
||||
value: paramsConfig.value.length ? createValues(mForm, paramsConfig.value, {}, props.model.params) : {},
|
||||
value: paramsConfig.value.length
|
||||
? createValues(mForm, paramsConfig.value as unknown as FormItemConfig[], {}, props.model.params)
|
||||
: {},
|
||||
});
|
||||
|
||||
emit('change', value, {
|
||||
|
||||
@ -1,6 +1,21 @@
|
||||
<template>
|
||||
<div class="m-editor-data-source-field-select">
|
||||
<template v-if="checkStrictly">
|
||||
<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">
|
||||
<TMagicSelect
|
||||
:model-value="selectDataSourceId"
|
||||
clearable
|
||||
@ -92,6 +107,8 @@ const props = defineProps<{
|
||||
dataSourceFieldType?: DataSourceFieldType[];
|
||||
/** 是否可以编辑数据源,disable表示的是是否可以选择数据源 */
|
||||
notEditable?: boolean | FilterFunction;
|
||||
/** 指定数据源ID,限定只能选择该数据源的字段 */
|
||||
dataSourceId?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -106,7 +123,12 @@ const { dataSourceService, uiService } = useServices();
|
||||
const mForm = inject<FormState | undefined>('mForm');
|
||||
const eventBus = inject<EventBus>('eventBus');
|
||||
|
||||
const dataSources = computed(() => dataSourceService.get('dataSources') || []);
|
||||
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 valueIsKey = computed(() => props.value === 'key');
|
||||
const notEditable = computed(() => filterFunction(mForm, props.notEditable, props));
|
||||
@ -125,7 +147,13 @@ const selectFieldsId = ref<string[]>([]);
|
||||
watch(
|
||||
modelValue,
|
||||
(value) => {
|
||||
if (Array.isArray(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) {
|
||||
const [dsId, ...fields] = value;
|
||||
selectDataSourceId.value = dsId;
|
||||
selectFieldsId.value = fields;
|
||||
@ -140,7 +168,7 @@ watch(
|
||||
);
|
||||
|
||||
const fieldsOptions = computed(() => {
|
||||
const ds = dataSources.value.find((ds) => ds.id === removeDataSourceFieldPrefix(selectDataSourceId.value));
|
||||
const ds = allDataSources.value.find((ds) => ds.id === removeDataSourceFieldPrefix(selectDataSourceId.value));
|
||||
|
||||
if (!ds) return [];
|
||||
|
||||
@ -163,8 +191,13 @@ const dsChangeHandler = (v: string) => {
|
||||
};
|
||||
|
||||
const fieldChangeHandler = (v: string[] = []) => {
|
||||
modelValue.value = [selectDataSourceId.value, ...v];
|
||||
emit('change', modelValue.value);
|
||||
if (props.dataSourceId) {
|
||||
modelValue.value = v;
|
||||
emit('change', v);
|
||||
} else {
|
||||
modelValue.value = [selectDataSourceId.value, ...v];
|
||||
emit('change', modelValue.value);
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeHandler = (v: string[] = []) => {
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
:value="config.value"
|
||||
:checkStrictly="checkStrictly"
|
||||
:dataSourceFieldType="config.dataSourceFieldType"
|
||||
:dataSourceId="config.dataSourceId"
|
||||
@change="onChangeHandler"
|
||||
></FieldSelect>
|
||||
|
||||
@ -47,7 +48,13 @@ import { Coin } from '@element-plus/icons-vue';
|
||||
|
||||
import { DataSchema } from '@tmagic/core';
|
||||
import { TMagicButton, tMagicMessage, TMagicTooltip } from '@tmagic/design';
|
||||
import type { ContainerChangeEventData, DataSourceFieldSelectConfig, FieldProps, FormState } from '@tmagic/form';
|
||||
import {
|
||||
type ContainerChangeEventData,
|
||||
type DataSourceFieldSelectConfig,
|
||||
type FieldProps,
|
||||
type FormState,
|
||||
getFormField,
|
||||
} from '@tmagic/form';
|
||||
import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, removeDataSourceFieldPrefix } from '@tmagic/utils';
|
||||
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
@ -92,7 +99,9 @@ const dataSources = computed(() => dataSourceService.get('dataSources') || []);
|
||||
const disabledDataSource = computed(() => propsService.getDisabledDataSource());
|
||||
|
||||
const type = computed((): string => {
|
||||
let type = props.config.fieldConfig?.type;
|
||||
if (!props.config.fieldConfig) return '';
|
||||
|
||||
let type = 'type' in props.config.fieldConfig ? props.config.fieldConfig.type : '';
|
||||
if (typeof type === 'function') {
|
||||
type = type(mForm, {
|
||||
model: props.model,
|
||||
@ -100,11 +109,18 @@ const type = computed((): string => {
|
||||
}
|
||||
if (type === 'form') return '';
|
||||
if (type === 'container') return '';
|
||||
return type?.replace(/([A-Z])/g, '-$1').toLowerCase() || (props.config.items ? '' : 'text');
|
||||
return (
|
||||
type?.replace(/([A-Z])/g, '-$1').toLowerCase() ||
|
||||
(props.config.fieldConfig && 'items' in props.config.fieldConfig ? '' : 'text')
|
||||
);
|
||||
});
|
||||
|
||||
const tagName = computed(() => {
|
||||
const component = resolveComponent(`m-${props.config.items ? 'form' : 'fields'}-${type.value}`);
|
||||
const component =
|
||||
getFormField(type.value || 'container') ||
|
||||
resolveComponent(
|
||||
`m-${props.config.fieldConfig && 'items' in props.config.fieldConfig ? 'form' : 'fields'}-${type.value}`,
|
||||
);
|
||||
if (typeof component !== 'string') return component;
|
||||
return 'm-fields-text';
|
||||
});
|
||||
|
||||
@ -52,12 +52,15 @@ 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';
|
||||
@ -247,7 +250,7 @@ const dataSourceFieldsConfig: FormConfig = [
|
||||
{ text: 'true', value: true },
|
||||
{ text: 'false', value: false },
|
||||
],
|
||||
},
|
||||
} as unknown as CodeConfig | NumberConfig | TextConfig,
|
||||
{
|
||||
name: 'enable',
|
||||
text: '是否可用',
|
||||
|
||||
@ -48,15 +48,18 @@ 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',
|
||||
@ -91,6 +94,43 @@ 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;
|
||||
@ -107,19 +147,21 @@ const paramsConfig = ref<CodeParamStatement[]>(getParamItemsConfig(props.model[p
|
||||
|
||||
const methodsOptions = computed(
|
||||
() =>
|
||||
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,
|
||||
})),
|
||||
],
|
||||
})) || [],
|
||||
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,
|
||||
})),
|
||||
],
|
||||
})) || [],
|
||||
);
|
||||
|
||||
const cascaderConfig = computed<CascaderConfig>(() => ({
|
||||
@ -142,7 +184,9 @@ const onChangeHandler = (value: any) => {
|
||||
|
||||
changeRecords.push({
|
||||
propPath: props.prop.replace(`${props.name}`, 'params'),
|
||||
value: paramsConfig.value.length ? createValues(mForm, paramsConfig.value, {}, props.model.params) : {},
|
||||
value: paramsConfig.value.length
|
||||
? createValues(mForm, paramsConfig.value as unknown as FormItemConfig[], {}, props.model.params)
|
||||
: {},
|
||||
});
|
||||
|
||||
emit('change', value, {
|
||||
|
||||
@ -42,7 +42,7 @@ const props = withDefaults(defineProps<FieldProps<DataSourceMethodsConfig>>(), {
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const codeConfig = ref<CodeBlockContent>();
|
||||
const codeConfig = ref<Omit<CodeBlockContent, 'content'> & { content: string }>();
|
||||
const codeBlockEditorRef = useTemplateRef<InstanceType<typeof CodeBlockEditor>>('codeBlockEditor');
|
||||
|
||||
let editIndex = -1;
|
||||
@ -72,10 +72,14 @@ const methodColumns: ColumnConfig[] = [
|
||||
{
|
||||
text: '编辑',
|
||||
handler: (method: CodeBlockContent, index: number) => {
|
||||
let codeContent = method.content || '({ params, dataSource, app }) => {\n // place your code here\n}';
|
||||
let codeContent: string = '({ params, dataSource, app }) => {\n // place your code here\n}';
|
||||
|
||||
if (typeof codeContent !== 'string') {
|
||||
codeContent = codeContent.toString();
|
||||
if (method.content) {
|
||||
if (typeof method.content !== 'string') {
|
||||
codeContent = method.content.toString();
|
||||
} else {
|
||||
codeContent = method.content;
|
||||
}
|
||||
}
|
||||
|
||||
codeConfig.value = {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="m-fields-event-select">
|
||||
<m-form-table
|
||||
<MTable
|
||||
v-if="isOldVersion"
|
||||
name="events"
|
||||
:size="size"
|
||||
@ -8,7 +8,7 @@
|
||||
:model="model"
|
||||
:config="tableConfig"
|
||||
@change="onChangeHandler"
|
||||
></m-form-table>
|
||||
></MTable>
|
||||
|
||||
<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(index)"
|
||||
@click="removeEvent(Number(index))"
|
||||
></TMagicButton>
|
||||
</template>
|
||||
</MPanel>
|
||||
@ -59,17 +59,18 @@ 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 { MContainer as MFormContainer, MPanel } from '@tmagic/form';
|
||||
import { defineFormItem, MContainer as MFormContainer, MPanel, MTable } from '@tmagic/form';
|
||||
import { DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX, traverseNode } from '@tmagic/utils';
|
||||
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
@ -89,10 +90,10 @@ const { editorService, dataSourceService, eventsService, codeBlockService, props
|
||||
|
||||
// 事件名称下拉框表单配置
|
||||
const eventNameConfig = computed(() => {
|
||||
const defaultEventNameConfig: ChildConfig = {
|
||||
const defaultEventNameConfig = {
|
||||
name: 'name',
|
||||
text: '事件',
|
||||
type: (mForm, { formValue }: any) => {
|
||||
type: (mForm: FormState | undefined, { formValue }: any) => {
|
||||
if (
|
||||
props.config.src !== 'component' ||
|
||||
(formValue.type === 'page-fragment-container' && formValue.pageFragmentId)
|
||||
@ -212,12 +213,12 @@ const actionTypeConfig = computed(() => {
|
||||
|
||||
// 联动组件配置
|
||||
const targetCompConfig = computed(() => {
|
||||
const defaultTargetCompConfig = {
|
||||
const defaultTargetCompConfig: UISelectConfig = {
|
||||
name: 'to',
|
||||
text: '联动组件',
|
||||
type: 'ui-select',
|
||||
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.actionType === ActionType.COMP,
|
||||
onChange: (MForm: FormState, v: string, { setModel }: OnChangeHandlerData) => {
|
||||
display: (_mForm, { model }) => model.actionType === ActionType.COMP,
|
||||
onChange: (_MForm, _v, { setModel }) => {
|
||||
setModel('method', '');
|
||||
},
|
||||
};
|
||||
@ -226,10 +227,10 @@ const targetCompConfig = computed(() => {
|
||||
|
||||
// 联动组件动作配置
|
||||
const compActionConfig = computed(() => {
|
||||
const defaultCompActionConfig: ChildConfig = {
|
||||
const defaultCompActionConfig: DynamicTypeConfig = {
|
||||
name: 'method',
|
||||
text: '动作',
|
||||
type: (mForm, { model }: any) => {
|
||||
type: (mForm: FormState | undefined, { model }: any) => {
|
||||
const to = editorService.getNodeById(model.to);
|
||||
|
||||
if (to && to.type === 'page-fragment-container' && to.pageFragmentId) {
|
||||
@ -239,7 +240,7 @@ const compActionConfig = computed(() => {
|
||||
return 'select';
|
||||
},
|
||||
checkStrictly: () => props.config.src !== 'component',
|
||||
display: (mForm, { model }: any) => model.actionType === ActionType.COMP,
|
||||
display: (mForm: FormState | undefined, { model }: any) => model.actionType === ActionType.COMP,
|
||||
options: (mForm: FormState, { model }: any) => {
|
||||
const node = editorService.getNodeById(model.to);
|
||||
if (!node?.type) return [];
|
||||
@ -304,62 +305,68 @@ const dataSourceActionConfig = computed(() => {
|
||||
});
|
||||
|
||||
// 兼容旧的数据格式
|
||||
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 [];
|
||||
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 [];
|
||||
|
||||
return eventsService.getMethod(node.type, model.to).map((option: any) => ({
|
||||
text: option.label,
|
||||
value: option.value,
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
return eventsService.getMethod(node.type, model.to).map((option: any) => ({
|
||||
text: option.label,
|
||||
value: option.value,
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
}) as TableConfig,
|
||||
);
|
||||
|
||||
// 组件动作组表单配置
|
||||
const actionsConfig = computed<PanelConfig>(() => ({
|
||||
type: 'panel',
|
||||
items: [
|
||||
{
|
||||
type: 'group-list',
|
||||
name: 'actions',
|
||||
expandAll: true,
|
||||
enableToggleMode: false,
|
||||
titlePrefix: '动作',
|
||||
const actionsConfig = computed(
|
||||
() =>
|
||||
defineFormItem({
|
||||
type: 'panel',
|
||||
items: [
|
||||
actionTypeConfig.value,
|
||||
targetCompConfig.value,
|
||||
compActionConfig.value,
|
||||
codeActionConfig.value,
|
||||
dataSourceActionConfig.value,
|
||||
{
|
||||
type: 'group-list',
|
||||
name: 'actions',
|
||||
expandAll: true,
|
||||
enableToggleMode: false,
|
||||
titlePrefix: '动作',
|
||||
items: [
|
||||
actionTypeConfig.value,
|
||||
targetCompConfig.value,
|
||||
compActionConfig.value,
|
||||
codeActionConfig.value,
|
||||
dataSourceActionConfig.value,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
}) as PanelConfig,
|
||||
);
|
||||
|
||||
// 是否为旧的数据格式
|
||||
const isOldVersion = computed(() => {
|
||||
|
||||
@ -26,7 +26,7 @@ import type { StyleSchema } from '@tmagic/schema';
|
||||
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
|
||||
import { Background, Border, Font, Layout, Position } from './pro/';
|
||||
import { Background, Border, Font, Layout, Position, Transform } from './pro/';
|
||||
|
||||
defineOptions({
|
||||
name: 'MFieldsStyleSetter',
|
||||
@ -60,6 +60,10 @@ const list = [
|
||||
title: '边框与圆角',
|
||||
component: Border,
|
||||
},
|
||||
{
|
||||
title: '变形',
|
||||
component: Transform,
|
||||
},
|
||||
];
|
||||
|
||||
const collapseValue = shallowRef(
|
||||
|
||||
@ -28,10 +28,10 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TMagicButton, TMagicInput } from '@tmagic/design';
|
||||
import type { FieldProps, FormItem } from '@tmagic/form';
|
||||
import type { FieldProps, StyleSetterConfig } from '@tmagic/form';
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
defineProps<FieldProps<{ type: 'style-setter' } & FormItem>>();
|
||||
defineProps<FieldProps<StyleSetterConfig>>();
|
||||
|
||||
const horizontalList = [
|
||||
{
|
||||
|
||||
@ -39,46 +39,48 @@
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import type { ContainerChangeEventData, FormValue } from '@tmagic/form';
|
||||
import { MContainer } from '@tmagic/form';
|
||||
import { defineFormItem, MContainer } from '@tmagic/form';
|
||||
import type { StyleSchema } from '@tmagic/schema';
|
||||
|
||||
const direction = ref('');
|
||||
|
||||
const config = computed(() => ({
|
||||
items: [
|
||||
{
|
||||
name: `border${direction.value}Width`,
|
||||
text: '边框宽度',
|
||||
labelWidth: '68px',
|
||||
type: 'data-source-field-select',
|
||||
fieldConfig: {
|
||||
type: 'text',
|
||||
const config = computed(() =>
|
||||
defineFormItem({
|
||||
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 || '');
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
import { ContainerChangeEventData, MContainer } from '@tmagic/form';
|
||||
import { type ContainerChangeEventData, defineFormItem, 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 = {
|
||||
const config = defineFormItem({
|
||||
items: [
|
||||
{
|
||||
name: 'backgroundColor',
|
||||
@ -39,7 +39,7 @@ const config = {
|
||||
type: 'data-source-field-select',
|
||||
fieldConfig: {
|
||||
type: 'img-upload',
|
||||
},
|
||||
} as any,
|
||||
},
|
||||
{
|
||||
name: 'backgroundSize',
|
||||
@ -74,7 +74,7 @@ const config = {
|
||||
labelWidth: '68px',
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
|
||||
emit('change', value, eventData);
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ContainerChangeEventData, MContainer } from '@tmagic/form';
|
||||
import { type ContainerChangeEventData, defineFormItem, 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 = {
|
||||
const config = defineFormItem({
|
||||
items: [
|
||||
{
|
||||
labelWidth: '68px',
|
||||
@ -31,7 +31,7 @@ const config = {
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
|
||||
emit('change', value, eventData);
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
import { ContainerChangeEventData, MContainer } from '@tmagic/form';
|
||||
import { type ContainerChangeEventData, defineFormItem, 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 = {
|
||||
const config = defineFormItem({
|
||||
items: [
|
||||
{
|
||||
type: 'row',
|
||||
@ -86,7 +86,7 @@ const config = {
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
|
||||
emit('change', value, eventData);
|
||||
|
||||
@ -12,8 +12,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
import type { ContainerChangeEventData, FormState } from '@tmagic/form';
|
||||
import { MContainer } from '@tmagic/form';
|
||||
import type { ChildConfig, ContainerChangeEventData } from '@tmagic/form';
|
||||
import { defineFormItem, 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 = {
|
||||
const config = defineFormItem({
|
||||
items: [
|
||||
{
|
||||
name: 'display',
|
||||
@ -74,7 +74,7 @@ const config = {
|
||||
tooltip: '垂直方向 起点在下沿 column-reverse',
|
||||
},
|
||||
],
|
||||
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
},
|
||||
{
|
||||
name: 'justifyContent',
|
||||
@ -89,7 +89,7 @@ const config = {
|
||||
{ value: 'space-between', icon: markRaw(JustifyContentSpaceBetween), tooltip: '两端对齐 space-between' },
|
||||
{ value: 'space-around', icon: markRaw(JustifyContentSpaceAround), tooltip: '横向平分 space-around' },
|
||||
],
|
||||
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
},
|
||||
{
|
||||
name: 'alignItems',
|
||||
@ -104,7 +104,7 @@ const config = {
|
||||
{ value: 'space-between', icon: markRaw(JustifyContentSpaceBetween), tooltip: '两端对齐 space-between' },
|
||||
{ value: 'space-around', icon: markRaw(JustifyContentSpaceAround), tooltip: '横向平分 space-around' },
|
||||
],
|
||||
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
},
|
||||
{
|
||||
name: 'flexWrap',
|
||||
@ -117,7 +117,7 @@ const config = {
|
||||
{ value: 'wrap', text: '正换行', tooltip: '第一行在上方 wrap' },
|
||||
{ value: 'wrap-reverse', text: '逆换行', tooltip: '第一行在下方 wrap-reverse' },
|
||||
],
|
||||
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
@ -180,7 +180,7 @@ const config = {
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}) as ChildConfig;
|
||||
|
||||
const change = (value: string | StyleSchema, eventData: ContainerChangeEventData) => {
|
||||
emit('change', value, eventData);
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ContainerChangeEventData, MContainer } from '@tmagic/form';
|
||||
import { type ContainerChangeEventData, defineFormItem, 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 = {
|
||||
const config = defineFormItem({
|
||||
items: [
|
||||
{
|
||||
name: 'position',
|
||||
@ -95,7 +95,7 @@ const config = {
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const change = (value: string | StyleSchema, eventData: ContainerChangeEventData) => {
|
||||
emit('change', value, eventData);
|
||||
|
||||
54
packages/editor/src/fields/StyleSetter/pro/Transform.vue
Normal file
54
packages/editor/src/fields/StyleSetter/pro/Transform.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<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>
|
||||
@ -3,3 +3,4 @@ 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';
|
||||
|
||||
@ -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<CodeBlockContent>();
|
||||
const codeConfig = ref<Omit<CodeBlockContent, 'content'> & { content: string }>();
|
||||
const codeId = ref<string>();
|
||||
const codeBlockEditorRef = useTemplateRef<InstanceType<typeof CodeBlockEditor>>('codeBlockEditor');
|
||||
|
||||
@ -36,10 +36,14 @@ export const useCodeBlockEdit = (codeBlockService: Services['codeBlockService'])
|
||||
return;
|
||||
}
|
||||
|
||||
let codeContent = codeBlock.content;
|
||||
let codeContent = '';
|
||||
|
||||
if (typeof codeContent !== 'string') {
|
||||
codeContent = codeContent.toString();
|
||||
if (codeBlock.content) {
|
||||
if (typeof codeBlock.content !== 'string') {
|
||||
codeContent = codeBlock.content.toString();
|
||||
} else {
|
||||
codeContent = codeBlock.content;
|
||||
}
|
||||
}
|
||||
|
||||
codeConfig.value = {
|
||||
|
||||
@ -15,38 +15,6 @@
|
||||
* 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';
|
||||
@ -110,44 +78,4 @@ 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';
|
||||
|
||||
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);
|
||||
},
|
||||
};
|
||||
export { default } from './plugin';
|
||||
|
||||
@ -233,7 +233,7 @@ export const initServiceEvents = (
|
||||
((event: 'update:modelValue', value: MApp | null) => void),
|
||||
{ editorService, codeBlockService, dataSourceService, depService }: Services,
|
||||
) => {
|
||||
let getTMagicAppPrimise: Promise<TMagicCore | undefined> | null = null;
|
||||
let getTMagicAppPromise: 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 (getTMagicAppPrimise) {
|
||||
return getTMagicAppPrimise;
|
||||
if (getTMagicAppPromise) {
|
||||
return getTMagicAppPromise;
|
||||
}
|
||||
|
||||
getTMagicAppPrimise = new Promise<TMagicCore | undefined>((resolve) => {
|
||||
getTMagicAppPromise = new Promise<TMagicCore | undefined>((resolve) => {
|
||||
// 设置 10s 超时
|
||||
const timeout = globalThis.setTimeout(() => {
|
||||
resolve(void 0);
|
||||
@ -264,7 +264,7 @@ export const initServiceEvents = (
|
||||
});
|
||||
});
|
||||
|
||||
return getTMagicAppPrimise;
|
||||
return getTMagicAppPromise;
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
@ -80,7 +80,7 @@ const props = withDefaults(
|
||||
let stage: StageCore | null = null;
|
||||
let runtime: Runtime | null = null;
|
||||
|
||||
const { editorService, uiService, keybindingService } = useServices();
|
||||
const { editorService, uiService, keybindingService, stageOverlayService } = useServices();
|
||||
|
||||
const stageLoading = computed(() => editorService.get('stageLoading'));
|
||||
|
||||
@ -97,6 +97,60 @@ 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;
|
||||
|
||||
@ -109,6 +163,40 @@ 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);
|
||||
|
||||
@ -46,12 +46,7 @@ const style = computed(() => ({
|
||||
}));
|
||||
|
||||
watch(stage, (stage) => {
|
||||
if (stage) {
|
||||
stage.on('dblclick', async (event: MouseEvent) => {
|
||||
const el = (await stage.actionManager?.getElementFromPoint(event)) || null;
|
||||
stageOverlayService.openOverlay(el);
|
||||
});
|
||||
} else {
|
||||
if (!stage) {
|
||||
stageOverlayService.closeOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
92
packages/editor/src/plugin.ts
Normal file
92
packages/editor/src/plugin.ts
Normal file
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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);
|
||||
},
|
||||
};
|
||||
@ -17,23 +17,13 @@
|
||||
*/
|
||||
|
||||
import { reactive, toRaw } from 'vue';
|
||||
import { cloneDeep, get, isObject, mergeWith, uniq } from 'lodash-es';
|
||||
import type { Writable } from 'type-fest';
|
||||
import { cloneDeep, isObject, mergeWith, uniq } from 'lodash-es';
|
||||
|
||||
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
|
||||
import { NodeType, Target, Watcher } from '@tmagic/core';
|
||||
import { NodeType } from '@tmagic/core';
|
||||
import type { ChangeRecord } from '@tmagic/form';
|
||||
import { isFixed } from '@tmagic/stage';
|
||||
import {
|
||||
calcValueByFontsize,
|
||||
getElById,
|
||||
getNodeInfo,
|
||||
getNodePath,
|
||||
isNumber,
|
||||
isPage,
|
||||
isPageFragment,
|
||||
isPop,
|
||||
} from '@tmagic/utils';
|
||||
import { getNodeInfo, getNodePath, isPage, isPageFragment } from '@tmagic/utils';
|
||||
|
||||
import BaseService from '@editor/services//BaseService';
|
||||
import propsService from '@editor/services//props';
|
||||
@ -42,69 +32,39 @@ import storageService, { Protocol } from '@editor/services/storage';
|
||||
import type {
|
||||
AddMNode,
|
||||
AsyncHookPlugin,
|
||||
AsyncMethodName,
|
||||
EditorEvents,
|
||||
EditorNodeInfo,
|
||||
HistoryOpType,
|
||||
PastePosition,
|
||||
StepValue,
|
||||
StoreState,
|
||||
StoreStateKey,
|
||||
} from '@editor/type';
|
||||
import { LayerOffset, Layout } from '@editor/type';
|
||||
import { canUsePluginMethods, LayerOffset, Layout } from '@editor/type';
|
||||
import {
|
||||
change2Fixed,
|
||||
calcAlignCenterStyle,
|
||||
calcLayerTargetIndex,
|
||||
calcMoveStyle,
|
||||
classifyDragSources,
|
||||
collectRelatedNodes,
|
||||
COPY_STORAGE_KEY,
|
||||
Fixed2Other,
|
||||
editorNodeMergeCustomizer,
|
||||
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,
|
||||
@ -121,6 +81,7 @@ class Editor extends BaseService {
|
||||
disabledMultiSelect: false,
|
||||
});
|
||||
private isHistoryStateChange = false;
|
||||
private selectionBeforeOp: Id[] | null = null;
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
@ -390,6 +351,8 @@ 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
|
||||
@ -435,7 +398,21 @@ class Editor extends BaseService {
|
||||
}
|
||||
|
||||
if (!(isPage(newNodes[0]) || isPageFragment(newNodes[0]))) {
|
||||
this.pushHistoryState();
|
||||
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.emit('add', newNodes);
|
||||
@ -498,13 +475,33 @@ 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 (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) {
|
||||
// 更新历史记录
|
||||
this.pushHistoryState();
|
||||
if (removedItems.length > 0 && pageForOp) {
|
||||
this.pushOpHistory('remove', { removedItems }, pageForOp);
|
||||
}
|
||||
|
||||
this.emit('remove', nodes);
|
||||
@ -525,21 +522,9 @@ class Editor extends BaseService {
|
||||
|
||||
const node = toRaw(info.node);
|
||||
|
||||
let newConfig = await this.toggleFixedPosition(toRaw(config), node, root);
|
||||
let newConfig = await toggleFixedPosition(toRaw(config), node, root, this.getLayout);
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
newConfig = mergeWith(cloneDeep(node), newConfig, editorNodeMergeCustomizer);
|
||||
|
||||
if (!newConfig.type) throw new Error('配置缺少type值');
|
||||
|
||||
@ -597,12 +582,28 @@ 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) {
|
||||
this.pushHistoryState();
|
||||
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.emit('update', updateData);
|
||||
@ -616,6 +617,8 @@ 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为空');
|
||||
|
||||
@ -640,9 +643,6 @@ class Editor extends BaseService {
|
||||
parentId: parent.id,
|
||||
root: cloneDeep(root),
|
||||
});
|
||||
|
||||
this.addModifiedNodeId(parent.id);
|
||||
this.pushHistoryState();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -664,31 +664,8 @@ 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') {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
collectRelatedNodes(copyNodes, collectorOptions, (id) => this.getNodeById(id));
|
||||
}
|
||||
|
||||
storageService.setItem(COPY_STORAGE_KEY, copyNodes, {
|
||||
@ -733,32 +710,16 @@ 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);
|
||||
if (layout === Layout.RELATIVE) {
|
||||
return config;
|
||||
}
|
||||
const doc = this.get('stage')?.renderer?.contentWindow?.document;
|
||||
const newStyle = calcAlignCenterStyle(node, parent, layout, doc);
|
||||
|
||||
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 = '';
|
||||
}
|
||||
if (!newStyle) return config;
|
||||
|
||||
node.style = newStyle;
|
||||
return node;
|
||||
}
|
||||
|
||||
@ -789,6 +750,8 @@ 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为空');
|
||||
|
||||
@ -801,22 +764,16 @@ 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;
|
||||
|
||||
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);
|
||||
}
|
||||
const offsetIndex = calcLayerTargetIndex(index, offset, brothers.length, isRelative);
|
||||
|
||||
if ((offsetIndex > 0 && offsetIndex > brothers.length) || offsetIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldParent = cloneDeep(toRaw(parent));
|
||||
|
||||
brothers.splice(index, 1);
|
||||
brothers.splice(offsetIndex, 0, node);
|
||||
|
||||
@ -829,7 +786,14 @@ class Editor extends BaseService {
|
||||
});
|
||||
|
||||
this.addModifiedNodeId(parent.id);
|
||||
this.pushHistoryState();
|
||||
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.emit('move-layer', offset);
|
||||
}
|
||||
@ -840,12 +804,17 @@ 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 } = this.getNodeInfo(config.id, false);
|
||||
const { node, parent, page: pageForOp } = 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);
|
||||
|
||||
@ -876,62 +845,60 @@ class Editor extends BaseService {
|
||||
|
||||
this.addModifiedNodeId(target.id);
|
||||
this.addModifiedNodeId(parent.id);
|
||||
this.pushHistoryState();
|
||||
this.pushOpHistory(
|
||||
'update',
|
||||
{
|
||||
updatedItems: [
|
||||
{ oldNode: oldSourceParent, newNode: cloneDeep(toRaw(parent)) },
|
||||
{ oldNode: oldTarget, newNode: cloneDeep(toRaw(target)) },
|
||||
],
|
||||
},
|
||||
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
||||
);
|
||||
|
||||
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 sourceIndicesInTargetParent: number[] = [];
|
||||
const sourceOutTargetParent: MNode[] = [];
|
||||
|
||||
const newLayout = await this.getLayout(targetParent);
|
||||
|
||||
// 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 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)));
|
||||
}
|
||||
|
||||
moveItemsInContainer(sourceIndicesInTargetParent, targetParent, targetIndex);
|
||||
const newLayout = await this.getLayout(targetParent);
|
||||
const { sameParentIndices, crossParentConfigs, aborted } = classifyDragSources(configs, targetParent, (id, raw) =>
|
||||
this.getNodeInfo(id, raw),
|
||||
);
|
||||
if (aborted) return;
|
||||
|
||||
sourceOutTargetParent.forEach((config, index) => {
|
||||
targetParent.items?.splice(targetIndex + index, 0, config);
|
||||
this.addModifiedNodeId(config.id);
|
||||
for (const { config: crossConfig, parent } of crossParentConfigs) {
|
||||
const layout = await this.getLayout(parent);
|
||||
if (newLayout !== layout) {
|
||||
setLayout(crossConfig, newLayout);
|
||||
}
|
||||
const index = getNodeIndex(crossConfig.id, parent);
|
||||
parent.items?.splice(index, 1);
|
||||
this.addModifiedNodeId(parent.id);
|
||||
}
|
||||
|
||||
moveItemsInContainer(sameParentIndices, targetParent, targetIndex);
|
||||
|
||||
crossParentConfigs.forEach(({ config: crossConfig }, index) => {
|
||||
targetParent.items?.splice(targetIndex + index, 0, crossConfig);
|
||||
this.addModifiedNodeId(crossConfig.id);
|
||||
});
|
||||
|
||||
const page = this.get('page');
|
||||
@ -946,28 +913,40 @@ class Editor extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
this.pushHistoryState();
|
||||
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.emit('drag-to', { targetIndex, configs, targetParent });
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销当前操作
|
||||
* @returns 上一次数据
|
||||
* @returns 被撤销的操作
|
||||
*/
|
||||
public async undo(): Promise<StepValue | null> {
|
||||
const value = historyService.undo();
|
||||
await this.changeHistoryState(value);
|
||||
if (value) {
|
||||
await this.applyHistoryOp(value, true);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复到下一步
|
||||
* @returns 下一步数据
|
||||
* @returns 被恢复的操作
|
||||
*/
|
||||
public async redo(): Promise<StepValue | null> {
|
||||
const value = historyService.redo();
|
||||
await this.changeHistoryState(value);
|
||||
if (value) {
|
||||
await this.applyHistoryOp(value, false);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@ -975,47 +954,10 @@ class Editor extends BaseService {
|
||||
const node = toRaw(this.get('node'));
|
||||
if (!node || isPage(node)) return;
|
||||
|
||||
const { style, id, type } = node;
|
||||
if (!style || !['absolute', 'fixed'].includes(style.position)) return;
|
||||
const newStyle = calcMoveStyle(node.style || {}, left, top);
|
||||
if (!newStyle) return;
|
||||
|
||||
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: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
await this.update({ id: node.id, type: node.type, style: newStyle });
|
||||
}
|
||||
|
||||
public resetState() {
|
||||
@ -1068,70 +1010,89 @@ class Editor extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private async changeHistoryState(value: StepValue | null) {
|
||||
if (!value) return;
|
||||
|
||||
/**
|
||||
* 应用历史操作(撤销 / 重做)
|
||||
* @param step 操作记录
|
||||
* @param reverse true = 撤销,false = 重做
|
||||
*/
|
||||
private async applyHistoryOp(step: StepValue, reverse: boolean) {
|
||||
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);
|
||||
}
|
||||
|
||||
private async toggleFixedPosition(dist: MNode, src: MNode, root: MApp) {
|
||||
const newConfig = cloneDeep(dist);
|
||||
const root = this.get('root');
|
||||
const stage = this.get('stage');
|
||||
if (!root) return;
|
||||
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
return newConfig;
|
||||
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;
|
||||
}
|
||||
|
||||
private selectedConfigExceptionHandler(config: MNode | Id): EditorNodeInfo {
|
||||
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,
|
||||
};
|
||||
return resolveSelectedNode(config, (id) => this.getNodeInfo(id), this.state.root?.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -56,15 +56,7 @@ class History extends BaseService {
|
||||
this.state.pageId = page.id;
|
||||
|
||||
if (!this.state.pageSteps[this.state.pageId]) {
|
||||
const undoRedo = new UndoRedo<StepValue>();
|
||||
|
||||
undoRedo.pushElement({
|
||||
data: page,
|
||||
modifiedNodeIds: new Map(),
|
||||
nodeId: page.id,
|
||||
});
|
||||
|
||||
this.state.pageSteps[this.state.pageId] = undoRedo;
|
||||
this.state.pageSteps[this.state.pageId] = new UndoRedo<StepValue>();
|
||||
}
|
||||
|
||||
this.setCanUndoRedo();
|
||||
|
||||
@ -102,9 +102,11 @@ class Props extends BaseService {
|
||||
}
|
||||
|
||||
public async setPropsConfig(type: string, config: FormConfig | PropsFormConfigFunction) {
|
||||
let c = config;
|
||||
let c: FormConfig;
|
||||
if (typeof config === 'function') {
|
||||
c = config({ editorService });
|
||||
} else {
|
||||
c = config;
|
||||
}
|
||||
|
||||
this.state.propsConfigMap[toLine(type)] = await this.fillConfig(Array.isArray(c) ? c : [c]);
|
||||
|
||||
@ -19,11 +19,11 @@
|
||||
import type { Component } from 'vue';
|
||||
import type EventEmitter from 'events';
|
||||
import type * as Monaco from 'monaco-editor';
|
||||
import Sortable, { type Options, type SortableEvent } from 'sortablejs';
|
||||
import type { PascalCasedProperties } from 'type-fest';
|
||||
import type { default as Sortable, Options, SortableEvent } from 'sortablejs';
|
||||
import type { PascalCasedProperties, Writable } from 'type-fest';
|
||||
|
||||
import type { CodeBlockContent, CodeBlockDSL, Id, MApp, MContainer, MNode, MPage, MPageFragment } from '@tmagic/core';
|
||||
import type { FormConfig, TableColumnConfig } from '@tmagic/form';
|
||||
import type { ChangeRecord, FormConfig, TableColumnConfig, TypeFunction } from '@tmagic/form';
|
||||
import type StageCore from '@tmagic/stage';
|
||||
import type {
|
||||
ContainerHighlightType,
|
||||
@ -164,6 +164,8 @@ export interface StageOptions {
|
||||
disabledMultiSelect?: boolean;
|
||||
disabledRule?: boolean;
|
||||
zoom?: number;
|
||||
/** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
|
||||
beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
|
||||
}
|
||||
|
||||
export interface StoreState {
|
||||
@ -541,14 +543,31 @@ export interface CodeParamStatement {
|
||||
/** 参数名称 */
|
||||
name: string;
|
||||
/** 参数类型 */
|
||||
type?: string;
|
||||
type?: string | TypeFunction<string>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type HistoryOpType = 'add' | 'remove' | 'update';
|
||||
|
||||
export interface StepValue {
|
||||
data: MPage | MPageFragment;
|
||||
/** 页面信息 */
|
||||
data: { name: string; id: Id };
|
||||
opType: HistoryOpType;
|
||||
/** 操作前选中的节点 ID,用于撤销后恢复选择状态 */
|
||||
selectedBefore: Id[];
|
||||
/** 操作后选中的节点 ID,用于重做后恢复选择状态 */
|
||||
selectedAfter: Id[];
|
||||
modifiedNodeIds: Map<Id, Id>;
|
||||
nodeId: 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 }[];
|
||||
}
|
||||
|
||||
export interface HistoryState {
|
||||
@ -712,3 +731,44 @@ 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']>;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { defineFormConfig } from '@tmagic/form';
|
||||
import { defineFormConfig, type FormConfig } from '@tmagic/form';
|
||||
|
||||
export default () =>
|
||||
export default (): FormConfig =>
|
||||
defineFormConfig([
|
||||
{
|
||||
name: 'id',
|
||||
|
||||
@ -1,83 +1,81 @@
|
||||
import { DataSchema, DataSourceFieldType, DataSourceSchema } from '@tmagic/core';
|
||||
import { CascaderOption, FormConfig, FormState } from '@tmagic/form';
|
||||
import type { DataSchema, DataSourceFieldType, DataSourceSchema } from '@tmagic/core';
|
||||
import { type CascaderOption, defineFormItem, type FormConfig } from '@tmagic/form';
|
||||
import { dataSourceTemplateRegExp, getKeysArray, isNumber } from '@tmagic/utils';
|
||||
|
||||
import BaseFormConfig from './formConfigs/base';
|
||||
import HttpFormConfig from './formConfigs/http';
|
||||
|
||||
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 },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
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];
|
||||
|
||||
export const getFormConfig = (type: string, configs: Record<string, FormConfig>): FormConfig => {
|
||||
switch (type) {
|
||||
|
||||
138
packages/editor/src/utils/editor-history.ts
Normal file
138
packages/editor/src/utils/editor-history.ts
Normal file
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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)),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -17,12 +17,13 @@
|
||||
*/
|
||||
|
||||
import { detailedDiff } from 'deep-object-diff';
|
||||
import { isObject } from 'lodash-es';
|
||||
import { cloneDeep, get, isObject } from 'lodash-es';
|
||||
import serialize from 'serialize-javascript';
|
||||
|
||||
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment } from '@tmagic/core';
|
||||
import { NODE_CONDS_KEY, NodeType } from '@tmagic/core';
|
||||
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
|
||||
import { NODE_CONDS_KEY, NodeType, Target, Watcher } from '@tmagic/core';
|
||||
import type StageCore from '@tmagic/stage';
|
||||
import { isFixed } from '@tmagic/stage';
|
||||
import {
|
||||
calcValueByFontsize,
|
||||
getElById,
|
||||
@ -34,7 +35,8 @@ import {
|
||||
isValueIncludeDataSource,
|
||||
} from '@tmagic/utils';
|
||||
|
||||
import { Layout } from '@editor/type';
|
||||
import type { EditorNodeInfo } from '@editor/type';
|
||||
import { LayerOffset, Layout } from '@editor/type';
|
||||
|
||||
export const COPY_STORAGE_KEY = '$MagicEditorCopyData';
|
||||
export const COPY_CODE_STORAGE_KEY = '$MagicEditorCopyCode';
|
||||
@ -436,3 +438,246 @@ export const buildChangeRecords = (value: any, basePath: string) => {
|
||||
|
||||
return changeRecords;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据节点配置或ID解析出选中节点信息,并进行合法性校验
|
||||
* @param config 节点配置或节点ID
|
||||
* @param getNodeInfoFn 获取节点信息的回调函数
|
||||
* @param rootId 根节点ID,用于排除根节点被选中
|
||||
* @returns 选中节点的完整信息(node、parent、page)
|
||||
*/
|
||||
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 };
|
||||
};
|
||||
|
||||
@ -27,3 +27,4 @@ export * from './scroll-viewer';
|
||||
export * from './tree';
|
||||
export * from './undo-redo';
|
||||
export * from './const';
|
||||
export { default as loadMonaco } from './monaco-editor';
|
||||
|
||||
@ -24,7 +24,7 @@ import {
|
||||
NODE_DISABLE_DATA_SOURCE_KEY,
|
||||
} from '@tmagic/core';
|
||||
import { tMagicMessage } from '@tmagic/design';
|
||||
import type { FormConfig, FormState, TabConfig, TabPaneConfig } from '@tmagic/form';
|
||||
import type { ChildConfig, DisplayCondsConfig, FormConfig, TabConfig, TabPaneConfig } from '@tmagic/form';
|
||||
|
||||
export const arrayOptions = [
|
||||
{ text: '包含', value: 'include' },
|
||||
@ -107,6 +107,10 @@ export const styleTabConfig: TabPaneConfig = {
|
||||
'borderStyle',
|
||||
'borderColor',
|
||||
],
|
||||
} as unknown as ChildConfig,
|
||||
{
|
||||
name: 'transform',
|
||||
defaultValue: () => ({}),
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -168,9 +172,9 @@ export const advancedTabConfig: TabPaneConfig = {
|
||||
],
|
||||
};
|
||||
|
||||
export const displayTabConfig: TabPaneConfig = {
|
||||
export const displayTabConfig: TabPaneConfig<DisplayCondsConfig> = {
|
||||
title: '显示条件',
|
||||
display: (_state: FormState, { model }: any) => model.type !== 'page',
|
||||
display: (_state, { model }) => model.type !== 'page',
|
||||
items: [
|
||||
{
|
||||
name: NODE_CONDS_RESULT_KEY,
|
||||
@ -209,7 +213,7 @@ export const fillConfig = (
|
||||
const propsConfig: FormConfig = [];
|
||||
|
||||
// 组件类型,必须要有
|
||||
if (!config.find((item) => item.name === 'type')) {
|
||||
if (!config.find((item) => 'name' in item && item.name === 'type')) {
|
||||
propsConfig.push({
|
||||
text: 'type',
|
||||
name: 'type',
|
||||
@ -217,7 +221,7 @@ export const fillConfig = (
|
||||
});
|
||||
}
|
||||
|
||||
if (!config.find((item) => item.name === 'id')) {
|
||||
if (!config.find((item) => 'name' in item && item.name === 'id')) {
|
||||
// 组件id,必须要有
|
||||
propsConfig.push({
|
||||
name: 'id',
|
||||
@ -241,14 +245,16 @@ export const fillConfig = (
|
||||
});
|
||||
}
|
||||
|
||||
if (!config.find((item) => item.name === 'name')) {
|
||||
if (!config.find((item) => 'name' in item && item.name === 'name')) {
|
||||
propsConfig.push({
|
||||
name: 'name',
|
||||
text: '组件名称',
|
||||
});
|
||||
}
|
||||
|
||||
const noCodeAdvancedTabItems = advancedTabConfig.items.filter((item) => item.type !== 'code-select');
|
||||
const noCodeAdvancedTabItems = advancedTabConfig.items.filter(
|
||||
(item) => 'type' in item && item.type !== 'code-select',
|
||||
);
|
||||
|
||||
if (noCodeAdvancedTabItems.length > 0 && disabledCodeBlock) {
|
||||
advancedTabConfig.items = noCodeAdvancedTabItems;
|
||||
|
||||
@ -23,7 +23,7 @@ export class UndoRedo<T = any> {
|
||||
private listCursor: number;
|
||||
private listMaxSize: number;
|
||||
|
||||
constructor(listMaxSize = 20) {
|
||||
constructor(listMaxSize = 100) {
|
||||
const minListMaxSize = 2;
|
||||
this.elementList = [];
|
||||
this.listCursor = 0;
|
||||
@ -42,29 +42,30 @@ export class UndoRedo<T = any> {
|
||||
}
|
||||
|
||||
public canUndo(): boolean {
|
||||
return this.listCursor > 1;
|
||||
return this.listCursor > 0;
|
||||
}
|
||||
|
||||
// 返回undo后的当前元素
|
||||
/** 返回被撤销的操作 */
|
||||
public undo(): T | null {
|
||||
if (!this.canUndo()) {
|
||||
return null;
|
||||
}
|
||||
this.listCursor -= 1;
|
||||
return this.getCurrentElement();
|
||||
return cloneDeep(this.elementList[this.listCursor]);
|
||||
}
|
||||
|
||||
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 this.getCurrentElement();
|
||||
return element;
|
||||
}
|
||||
|
||||
public getCurrentElement(): T | null {
|
||||
|
||||
@ -30,6 +30,9 @@ 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
|
||||
|
||||
245
packages/editor/tests/unit/utils/editor-history.spec.ts
Normal file
245
packages/editor/tests/unit/utils/editor-history.spec.ts
Normal file
@ -0,0 +1,245 @@
|
||||
/*
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
@ -17,8 +17,11 @@
|
||||
*/
|
||||
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', () => {
|
||||
@ -305,3 +308,452 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -21,60 +21,66 @@ 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 no undo: empty list', () => {
|
||||
test('can not undo: empty list', () => {
|
||||
expect(undoRedo.canUndo()).toBe(false);
|
||||
expect(undoRedo.undo()).toEqual(null);
|
||||
});
|
||||
|
||||
test('can undo', () => {
|
||||
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 });
|
||||
undoRedo.pushElement({ a: 2 });
|
||||
expect(undoRedo.canUndo()).toBe(true);
|
||||
expect(undoRedo.undo()).toEqual(element);
|
||||
expect(undoRedo.undo()).toEqual({ a: 2 });
|
||||
expect(undoRedo.canUndo()).toBe(true);
|
||||
expect(undoRedo.undo()).toEqual({ a: 1 });
|
||||
expect(undoRedo.canUndo()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redo', () => {
|
||||
let undoRedo: UndoRedo;
|
||||
const element = { a: 1 };
|
||||
|
||||
beforeEach(() => {
|
||||
undoRedo = new UndoRedo();
|
||||
undoRedo.pushElement(element);
|
||||
});
|
||||
|
||||
test('can no redo: empty list', () => {
|
||||
test('can not redo: empty list', () => {
|
||||
expect(undoRedo.canRedo()).toBe(false);
|
||||
expect(undoRedo.redo()).toBe(null);
|
||||
});
|
||||
|
||||
test('can no redo: no undo', () => {
|
||||
test('can not redo: no undo', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
undoRedo.pushElement(element);
|
||||
undoRedo.pushElement({ a: i });
|
||||
expect(undoRedo.canRedo()).toBe(false);
|
||||
expect(undoRedo.redo()).toBe(null);
|
||||
}
|
||||
});
|
||||
|
||||
test('can no redo: undo and push', () => {
|
||||
undoRedo.pushElement(element);
|
||||
test('can not redo: undo and push', () => {
|
||||
undoRedo.pushElement({ a: 1 });
|
||||
undoRedo.pushElement({ a: 2 });
|
||||
undoRedo.undo();
|
||||
undoRedo.pushElement(element);
|
||||
undoRedo.pushElement({ a: 3 });
|
||||
expect(undoRedo.canRedo()).toBe(false);
|
||||
expect(undoRedo.redo()).toEqual(null);
|
||||
});
|
||||
|
||||
test('can no redo: redo end', () => {
|
||||
const element1 = { a: 1 };
|
||||
const element2 = { a: 2 };
|
||||
undoRedo.pushElement(element1);
|
||||
undoRedo.pushElement(element2);
|
||||
test('can not redo: redo end', () => {
|
||||
undoRedo.pushElement({ a: 1 });
|
||||
undoRedo.pushElement({ a: 2 });
|
||||
undoRedo.undo();
|
||||
undoRedo.undo();
|
||||
undoRedo.redo();
|
||||
@ -85,23 +91,20 @@ describe('redo', () => {
|
||||
});
|
||||
|
||||
test('can redo', () => {
|
||||
const element1 = { a: 1 };
|
||||
const element2 = { a: 2 };
|
||||
undoRedo.pushElement(element1);
|
||||
undoRedo.pushElement(element2);
|
||||
undoRedo.pushElement({ a: 1 });
|
||||
undoRedo.pushElement({ a: 2 });
|
||||
undoRedo.undo();
|
||||
undoRedo.undo();
|
||||
|
||||
expect(undoRedo.canRedo()).toBe(true);
|
||||
expect(undoRedo.redo()).toEqual(element1);
|
||||
expect(undoRedo.redo()).toEqual({ a: 1 });
|
||||
expect(undoRedo.canRedo()).toBe(true);
|
||||
expect(undoRedo.redo()).toEqual(element2);
|
||||
expect(undoRedo.redo()).toEqual({ a: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('get current element', () => {
|
||||
let undoRedo: UndoRedo;
|
||||
const element = { a: 1 };
|
||||
|
||||
beforeEach(() => {
|
||||
undoRedo = new UndoRedo();
|
||||
@ -112,44 +115,38 @@ describe('get current element', () => {
|
||||
});
|
||||
|
||||
test('has element', () => {
|
||||
undoRedo.pushElement(element);
|
||||
expect(undoRedo.getCurrentElement()).toEqual(element);
|
||||
undoRedo.pushElement({ a: 1 });
|
||||
expect(undoRedo.getCurrentElement()).toEqual({ a: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
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', () => {
|
||||
for (let i = 0; i < listMaxSize + 1; i++) {
|
||||
test('reach max size, then undo all', () => {
|
||||
for (let i = 0; i <= listMaxSize; i++) {
|
||||
undoRedo.pushElement({ a: i });
|
||||
}
|
||||
for (let i = 0; i < listMaxSize - 1; i++) {
|
||||
for (let i = 0; i < listMaxSize; 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(element);
|
||||
expect(undoRedo.getCurrentElement()).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
{
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/element-plus-adapter",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"main": "dist/tmagic-element-plus-adapter.umd.cjs",
|
||||
"module": "dist/tmagic-element-plus-adapter.js",
|
||||
"module": "dist/es/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./dist/tmagic-element-plus-adapter.js",
|
||||
"import": "./dist/es/index.js",
|
||||
"require": "./dist/tmagic-element-plus-adapter.umd.cjs"
|
||||
},
|
||||
"./*": "./*"
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
{
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/form-schema",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"main": "dist/tmagic-form-schema.umd.cjs",
|
||||
"module": "dist/tmagic-form-schema.js",
|
||||
"module": "dist/es/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./dist/tmagic-form-schema.js",
|
||||
"import": "./dist/es/index.js",
|
||||
"require": "./dist/tmagic-form-schema.umd.cjs"
|
||||
},
|
||||
"./*": "./*"
|
||||
|
||||
@ -91,12 +91,10 @@ export interface FormItem {
|
||||
/** vnode的key值,默认是遍历数组时的index */
|
||||
__key?: string | number;
|
||||
/** 表单域标签的的宽度,例如 '50px'。支持 auto。 */
|
||||
labelWidth?: string;
|
||||
labelWidth?: string | number;
|
||||
/** label 标签的title属性 */
|
||||
labelTitle?: string;
|
||||
className?: string;
|
||||
/** 表单组件类型 */
|
||||
type?: string | TypeFunction;
|
||||
/** 字段名 */
|
||||
name?: string | number;
|
||||
/** 额外的提示信息,和 help 类似,当提示文案同时出现时,可以使用这个。 */
|
||||
@ -129,11 +127,16 @@ 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 {
|
||||
items: FormConfig;
|
||||
export interface ContainerCommonConfig<T = never> extends FormItem {
|
||||
items: FormConfig<T>;
|
||||
onInitValue?: (
|
||||
mForm: FormState | undefined,
|
||||
data: {
|
||||
@ -182,12 +185,12 @@ export interface Input {
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export type TypeFunction = (
|
||||
export type TypeFunction<T extends string = string> = (
|
||||
mForm: FormState | undefined,
|
||||
data: {
|
||||
model: FormValue;
|
||||
},
|
||||
) => string;
|
||||
) => T;
|
||||
|
||||
export type FilterFunction<T = boolean> = (
|
||||
mForm: FormState | undefined,
|
||||
@ -208,6 +211,7 @@ export type FilterFunction<T = boolean> = (
|
||||
*/
|
||||
export interface SelectConfigOption {
|
||||
/** 选项的标签 */
|
||||
label?: string | SelectOptionTextFunction;
|
||||
text: string | SelectOptionTextFunction;
|
||||
/** 选项的值 */
|
||||
value: any | SelectOptionValueFunction;
|
||||
@ -344,13 +348,15 @@ 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?:
|
||||
@ -436,6 +442,17 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个多选框
|
||||
*/
|
||||
@ -459,11 +476,11 @@ export interface SwitchConfig extends FormItem {
|
||||
* 单选框
|
||||
*/
|
||||
export interface RadioGroupConfig extends FormItem {
|
||||
type: 'radio-group';
|
||||
type: 'radio-group' | 'radioGroup';
|
||||
childType?: 'default' | 'button';
|
||||
options: {
|
||||
value: string | number | boolean;
|
||||
text: string;
|
||||
text?: string;
|
||||
icon?: any;
|
||||
tooltip?: string;
|
||||
}[];
|
||||
@ -486,7 +503,7 @@ export interface CheckboxGroupOption {
|
||||
* 多选框组
|
||||
*/
|
||||
export interface CheckboxGroupConfig extends FormItem {
|
||||
type: 'checkbox-group';
|
||||
type: 'checkbox-group' | 'checkboxGroup';
|
||||
options: CheckboxGroupOption[] | FilterFunction<CheckboxGroupOption[]>;
|
||||
}
|
||||
|
||||
@ -533,7 +550,7 @@ export interface SelectConfig extends FormItem, Input {
|
||||
/**
|
||||
* 链接
|
||||
*/
|
||||
export interface LinkConfig extends FormItem {
|
||||
export interface LinkConfig<T = never> extends FormItem {
|
||||
type: 'link';
|
||||
href?: string | ((model: Record<string, any>) => string);
|
||||
css?: {
|
||||
@ -553,7 +570,7 @@ export interface LinkConfig extends FormItem {
|
||||
) => string)
|
||||
| string;
|
||||
form:
|
||||
| FormConfig
|
||||
| FormConfig<T>
|
||||
| ((
|
||||
mForm: FormState | undefined,
|
||||
data: {
|
||||
@ -561,7 +578,7 @@ export interface LinkConfig extends FormItem {
|
||||
values?: Readonly<FormValue> | null;
|
||||
formValue?: FormValue;
|
||||
},
|
||||
) => FormConfig);
|
||||
) => FormConfig<T>);
|
||||
fullscreen?: boolean;
|
||||
}
|
||||
|
||||
@ -602,7 +619,7 @@ export interface CascaderConfig extends FormItem, Input {
|
||||
}
|
||||
|
||||
export interface DynamicFieldConfig extends FormItem {
|
||||
type: 'dynamic-field';
|
||||
type: 'dynamic-field' | 'dynamicField';
|
||||
returnFields: (
|
||||
config: DynamicFieldConfig,
|
||||
model: Record<any, any>,
|
||||
@ -618,32 +635,38 @@ export interface DynamicFieldConfig extends FormItem {
|
||||
/**
|
||||
* 分组容器
|
||||
*/
|
||||
export interface RowConfig extends FormItem {
|
||||
export interface RowConfig<T = never> extends FormItem {
|
||||
type: 'row';
|
||||
span: number;
|
||||
items: ({ span?: number } & (ChildConfig | EditorChildConfig))[];
|
||||
items: ({ span?: number } & (ChildConfig<T> | EditorChildConfig | T))[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签页容器
|
||||
*/
|
||||
export interface TabPaneConfig {
|
||||
export interface TabPaneConfig<T = never> {
|
||||
status?: string;
|
||||
/** 标签页名称,用于关联 model 中的数据 */
|
||||
name?: string | number;
|
||||
title: string;
|
||||
lazy?: boolean;
|
||||
labelWidth?: string;
|
||||
items: FormConfig;
|
||||
items: FormConfig<T>;
|
||||
display?: boolean | 'expand' | FilterFunction<boolean | 'expand'>;
|
||||
onTabClick?: (mForm: FormState | undefined, tab: any, data: any) => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface TabConfig extends FormItem, ContainerCommonConfig {
|
||||
export interface TabConfig<T = never> extends FormItem, ContainerCommonConfig<T> {
|
||||
type: 'tab' | 'dynamic-tab';
|
||||
tabType?: string;
|
||||
editable?: boolean;
|
||||
dynamic?: boolean;
|
||||
tabPosition?: 'top' | 'right' | 'bottom' | 'left';
|
||||
items: TabPaneConfig[];
|
||||
/** 当前激活的标签页,可以是固定值或动态函数 */
|
||||
active?:
|
||||
| string
|
||||
| ((mForm: FormState | undefined, data: { model: FormValue; formValue?: FormValue; prop: string }) => string);
|
||||
items: TabPaneConfig<T>[];
|
||||
onChange?: (mForm: FormState | undefined, data: any) => void;
|
||||
onTabAdd?: (mForm: FormState | undefined, data: any) => void;
|
||||
onTabRemove?: (mForm: FormState | undefined, tabName: string, data: any) => void;
|
||||
@ -654,7 +677,7 @@ export interface TabConfig extends FormItem, ContainerCommonConfig {
|
||||
/**
|
||||
* 分组
|
||||
*/
|
||||
export interface FieldsetConfig extends FormItem, ContainerCommonConfig {
|
||||
export interface FieldsetConfig<T = never> extends FormItem, ContainerCommonConfig<T> {
|
||||
type: 'fieldset';
|
||||
checkbox?:
|
||||
| boolean
|
||||
@ -671,7 +694,7 @@ export interface FieldsetConfig extends FormItem, ContainerCommonConfig {
|
||||
/**
|
||||
* 面板容器
|
||||
*/
|
||||
export interface PanelConfig extends FormItem, ContainerCommonConfig {
|
||||
export interface PanelConfig<T = never> extends FormItem, ContainerCommonConfig<T> {
|
||||
type: 'panel';
|
||||
expand?: boolean;
|
||||
title?: string;
|
||||
@ -683,7 +706,10 @@ export interface TableColumnConfig extends FormItem {
|
||||
label: string;
|
||||
width?: string | number;
|
||||
sortable?: boolean;
|
||||
[key: string]: any;
|
||||
items?: FormConfig;
|
||||
itemsFunction?: (row: any) => FormConfig;
|
||||
titleTip?: FilterFunction<string>;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -715,10 +741,11 @@ 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;
|
||||
/** 新增的默认行,可以是函数动态生成或静态对象 */
|
||||
defaultAdd?: ((mForm: FormState | undefined, data: any) => any) | Record<string, any>;
|
||||
copyHandler?: (mForm: FormState | undefined, data: any) => any;
|
||||
onSelect?: (mForm: FormState | undefined, data: any) => any;
|
||||
/** @deprecated 请使用 defaultSort */
|
||||
defautSort?: SortProp;
|
||||
defaultSort?: SortProp;
|
||||
/** 是否支持拖拽排序 */
|
||||
@ -739,15 +766,17 @@ export interface TableConfig extends FormItem {
|
||||
props?: Record<string, any>;
|
||||
text?: string;
|
||||
};
|
||||
sort?: boolean;
|
||||
sortKey?: string;
|
||||
}
|
||||
|
||||
export interface GroupListConfig extends FormItem {
|
||||
export interface GroupListConfig<T = never> extends FormItem {
|
||||
type: 'table' | 'groupList' | 'group-list';
|
||||
span?: number;
|
||||
enableToggleMode?: boolean;
|
||||
items: FormConfig;
|
||||
groupItems?: FormConfig;
|
||||
tableItems?: FormConfig;
|
||||
items: FormConfig<T>;
|
||||
groupItems?: FormConfig<T>;
|
||||
tableItems?: FormConfig<T>;
|
||||
titleKey?: string;
|
||||
titlePrefix?: string;
|
||||
title?: string | FilterFunction<string>;
|
||||
@ -760,7 +789,8 @@ export interface GroupListConfig extends FormItem {
|
||||
*/
|
||||
defaultExpandQuantity?: number;
|
||||
addable?: (mForm: FormState | undefined, data: any) => boolean | 'undefined' | boolean;
|
||||
defaultAdd?: (mForm: FormState | undefined, data: any) => any;
|
||||
/** 新增的默认值,可以是函数动态生成或静态对象 */
|
||||
defaultAdd?: ((mForm: FormState | undefined, data: any) => any) | Record<string, any>;
|
||||
delete?: (model: any, index: number | string | symbol, values: any) => boolean | boolean;
|
||||
copyable?: FilterFunction<boolean>;
|
||||
movable?: (
|
||||
@ -774,45 +804,50 @@ export interface GroupListConfig extends FormItem {
|
||||
props?: Record<string, any>;
|
||||
text?: string;
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface StepItemConfig extends FormItem, ContainerCommonConfig {
|
||||
interface StepItemConfig<T = never> extends FormItem, ContainerCommonConfig<T> {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface StepConfig extends FormItem {
|
||||
export interface StepConfig<T = never> extends FormItem {
|
||||
type: 'step';
|
||||
/** 每个 step 的间距,不填写将自适应间距。支持百分比。 */
|
||||
space?: string | number;
|
||||
items: StepItemConfig[];
|
||||
items: StepItemConfig<T>[];
|
||||
}
|
||||
|
||||
export interface ComponentConfig extends FormItem {
|
||||
type: 'component';
|
||||
id: string;
|
||||
extend: any;
|
||||
display: any;
|
||||
extend?: any;
|
||||
display?: any;
|
||||
component?: any;
|
||||
}
|
||||
|
||||
export interface FlexLayoutConfig extends FormItem, ContainerCommonConfig {
|
||||
export interface FlexLayoutConfig<T = never> extends FormItem, ContainerCommonConfig<T> {
|
||||
type: 'flex-layout';
|
||||
/** flex 子项间距,默认 '16px' */
|
||||
gap?: string;
|
||||
}
|
||||
|
||||
export type ChildConfig =
|
||||
| FormItem
|
||||
| TabConfig
|
||||
| RowConfig
|
||||
| FieldsetConfig
|
||||
| PanelConfig
|
||||
export type ChildConfig<T = never> =
|
||||
| ContainerCommonConfig<T>
|
||||
| TabConfig<T>
|
||||
| RowConfig<T>
|
||||
| FieldsetConfig<T>
|
||||
| PanelConfig<T>
|
||||
| TableConfig
|
||||
| GroupListConfig
|
||||
| StepConfig
|
||||
| GroupListConfig<T>
|
||||
| StepConfig<T>
|
||||
| DisplayConfig
|
||||
| TextConfig
|
||||
| NumberConfig
|
||||
| NumberRangeConfig
|
||||
| HiddenConfig
|
||||
| LinkConfig
|
||||
| LinkConfig<T>
|
||||
| DaterangeConfig
|
||||
| TimerangeConfig
|
||||
| SelectConfig
|
||||
| CascaderConfig
|
||||
| HtmlField
|
||||
@ -823,8 +858,12 @@ export type ChildConfig =
|
||||
| CheckboxConfig
|
||||
| SwitchConfig
|
||||
| RadioGroupConfig
|
||||
| CheckboxGroupConfig
|
||||
| TextareaConfig
|
||||
| DynamicFieldConfig
|
||||
| ComponentConfig;
|
||||
| ComponentConfig
|
||||
| FlexLayoutConfig<T>;
|
||||
|
||||
export type FormConfig = (ChildConfig | EditorChildConfig)[];
|
||||
export type FormItemConfig<T = never> = ChildConfig<T> | DynamicTypeConfig | EditorChildConfig<T> | T;
|
||||
|
||||
export type FormConfig<T = never> = FormItemConfig<T>[];
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { DataSourceFieldType, DataSourceSchema } from '@tmagic/schema';
|
||||
|
||||
import type { ChildConfig, FilterFunction, FormItem, FormState, Input } from './base';
|
||||
import type { FilterFunction, FormItem, FormItemConfig, FormState, Input } from './base';
|
||||
|
||||
export interface DataSourceFieldSelectConfig extends FormItem {
|
||||
export interface DataSourceFieldSelectConfig<T = never> extends FormItem {
|
||||
type: 'data-source-field-select';
|
||||
/**
|
||||
* 是否要编译成数据源的data。
|
||||
@ -26,13 +26,15 @@ export interface DataSourceFieldSelectConfig extends FormItem {
|
||||
},
|
||||
) => boolean);
|
||||
dataSourceFieldType?: DataSourceFieldType[];
|
||||
fieldConfig?: ChildConfig;
|
||||
fieldConfig?: FormItemConfig<T>;
|
||||
/** 是否可以编辑数据源,disable表示的是是否可以选择数据源 */
|
||||
notEditable?: boolean | FilterFunction;
|
||||
|
||||
dataSourceId?: string;
|
||||
}
|
||||
|
||||
export interface CodeConfig extends FormItem {
|
||||
type: 'code';
|
||||
type: 'vs-code';
|
||||
language?: string;
|
||||
options?: {
|
||||
[key: string]: any;
|
||||
@ -104,6 +106,7 @@ export interface DataSourceSelect extends FormItem, Input {
|
||||
}
|
||||
|
||||
export interface DisplayCondsConfig extends FormItem {
|
||||
type: 'display-conds';
|
||||
titlePrefix?: string;
|
||||
parentFields?: string[] | FilterFunction<string[]>;
|
||||
}
|
||||
@ -140,8 +143,12 @@ export interface UISelectConfig extends FormItem {
|
||||
type: 'ui-select';
|
||||
}
|
||||
|
||||
export type EditorChildConfig =
|
||||
| DataSourceFieldSelectConfig
|
||||
export interface StyleSetterConfig extends FormItem {
|
||||
type: 'style-setter';
|
||||
}
|
||||
|
||||
export type EditorChildConfig<T = never> =
|
||||
| DataSourceFieldSelectConfig<T>
|
||||
| CodeConfig
|
||||
| CodeLinkConfig
|
||||
| CodeSelectConfig
|
||||
@ -157,4 +164,5 @@ export type EditorChildConfig =
|
||||
| EventSelectConfig
|
||||
| KeyValueConfig
|
||||
| PageFragmentSelectConfig
|
||||
| UISelectConfig;
|
||||
| UISelectConfig
|
||||
| StyleSetterConfig;
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { FormConfig } from './base';
|
||||
import type { FormConfig, FormItemConfig } from './base';
|
||||
|
||||
export * from './base';
|
||||
export * from './editor';
|
||||
|
||||
export const defineFormConfig = <T = FormConfig>(config: T): T => config;
|
||||
export const defineFormConfig = <T = never>(config: FormConfig<T>): FormConfig<T> => config;
|
||||
|
||||
export const defineFormItem = <T = never>(config: FormItemConfig<T>): FormItemConfig<T> => config;
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
{
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/form",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
"dist/style.css",
|
||||
"dist/es/style.css",
|
||||
"src/theme/*"
|
||||
],
|
||||
"main": "dist/tmagic-form.umd.cjs",
|
||||
"module": "dist/tmagic-form.js",
|
||||
"module": "dist/es/index.js",
|
||||
"style": "dist/style.css",
|
||||
"types": "types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./dist/tmagic-form.js",
|
||||
"import": "./dist/es/index.js",
|
||||
"require": "./dist/tmagic-form.umd.cjs"
|
||||
},
|
||||
"./dist/style.css": {
|
||||
|
||||
@ -218,13 +218,13 @@ const getTextByName = (name: string, config: FormConfig = props.config): string
|
||||
return typeof item.text === 'string' ? item.text : undefined;
|
||||
}
|
||||
|
||||
if (item.items && Array.isArray(item.items)) {
|
||||
if ('items' in item && Array.isArray(item.items)) {
|
||||
const result = findInConfig(item.items, remainingParts);
|
||||
if (result !== undefined) return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.items && Array.isArray(item.items)) {
|
||||
if ('items' in item && Array.isArray(item.items)) {
|
||||
const result = findInConfig(item.items, parts);
|
||||
if (result !== undefined) return result;
|
||||
}
|
||||
|
||||
@ -117,19 +117,25 @@ const stepActive = ref(1);
|
||||
const bodyHeight = ref(`${document.body.clientHeight - 194}px`);
|
||||
|
||||
const stepCount = computed(() => {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const hasStep = computed(() => {
|
||||
const { length } = props.config;
|
||||
for (let index = 0; index < length; index++) {
|
||||
if (props.config[index].type === 'step') {
|
||||
if (!Array.isArray(props.config)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const item of props.config) {
|
||||
if ('type' in item && item.type === 'step') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TMagicCol v-show="display && config.type !== 'hidden'" :span="span">
|
||||
<TMagicCol v-show="display && 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 { ChildConfig, ContainerChangeEventData, FormState } from '../schema';
|
||||
import type { ContainerChangeEventData, FormItemConfig, 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: ChildConfig;
|
||||
labelWidth?: string;
|
||||
config: FormItemConfig;
|
||||
labelWidth?: string | number;
|
||||
expandMore?: boolean;
|
||||
span?: number;
|
||||
size?: string;
|
||||
@ -52,4 +52,6 @@ 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>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div
|
||||
:data-tmagic-id="config.id"
|
||||
:data-tmagic-id="(config as Record<string, any>).id"
|
||||
:data-tmagic-form-item-prop="itemProp"
|
||||
:class="`m-form-container m-container-${type || ''} ${config.className || ''}${config.tip ? ' has-tip' : ''}`"
|
||||
:style="config.style"
|
||||
>
|
||||
<m-fields-hidden v-if="type === 'hidden'" v-bind="fieldsProps" :model="model"></m-fields-hidden>
|
||||
<MHidden v-if="type === 'hidden'" :name="`${name}`" :prop="itemProp" :model="model"></MHidden>
|
||||
|
||||
<component
|
||||
v-else-if="items && !text && type && display"
|
||||
@ -28,7 +28,7 @@
|
||||
<FormLabel
|
||||
:tip="config.tip"
|
||||
:type="type"
|
||||
:use-label="config.useLabel"
|
||||
:use-label="(config as CheckboxConfig).useLabel"
|
||||
:label-title="config.labelTitle"
|
||||
:text="text"
|
||||
></FormLabel>
|
||||
@ -61,7 +61,7 @@
|
||||
></component>
|
||||
</TMagicFormItem>
|
||||
|
||||
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !config.useLabel" placement="top">
|
||||
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).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.useLabel"
|
||||
:use-label="(config as CheckboxConfig).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.useLabel" placement="top">
|
||||
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).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.useLabel"
|
||||
:use-label="(config as CheckboxConfig).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.useLabel" placement="top">
|
||||
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel" placement="top">
|
||||
<TMagicIcon style="line-height: 40px; margin-left: 5px"><warning-filled /></TMagicIcon>
|
||||
<template #content>
|
||||
<div v-html="config.tip"></div>
|
||||
@ -172,14 +172,18 @@ 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 {
|
||||
ChildConfig,
|
||||
CheckboxConfig,
|
||||
ComponentConfig,
|
||||
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';
|
||||
@ -194,10 +198,10 @@ const props = withDefaults(
|
||||
model: FormValue;
|
||||
/** 需对比的值(开启对比模式时传入) */
|
||||
lastValues?: FormValue;
|
||||
config: ChildConfig;
|
||||
config: FormItemConfig;
|
||||
prop?: string;
|
||||
disabled?: boolean;
|
||||
labelWidth?: string;
|
||||
labelWidth?: string | number;
|
||||
expandMore?: boolean;
|
||||
stepActive?: string | number;
|
||||
size?: string;
|
||||
@ -248,11 +252,20 @@ 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.component) {
|
||||
return props.config.component;
|
||||
if (type.value === 'component' && (props.config as ComponentConfig).component) {
|
||||
return (props.config as ComponentConfig).component;
|
||||
}
|
||||
return `m-${items.value ? 'form' : 'fields'}-${type.value}`;
|
||||
|
||||
return getField(type.value || 'container') || `m-${items.value ? 'form' : 'fields'}-${type.value}`;
|
||||
});
|
||||
|
||||
const disabled = computed(() => props.disabled || filterFunction(mForm, props.config.disabled, props));
|
||||
@ -276,14 +289,6 @@ 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);
|
||||
|
||||
@ -299,7 +304,7 @@ const fieldsProps = computed(() => ({
|
||||
name: name.value,
|
||||
disabled: disabled.value,
|
||||
prop: itemProp.value,
|
||||
key: props.config[mForm?.keyProps],
|
||||
key: (props.config as Record<string, any>)[mForm?.keyProps],
|
||||
style: props.config.fieldStyle,
|
||||
}));
|
||||
|
||||
|
||||
@ -144,7 +144,9 @@ const rowConfig = computed(() => ({
|
||||
span: props.config.span || 24,
|
||||
items: props.config.items,
|
||||
labelWidth: props.config.labelWidth,
|
||||
[mForm?.keyProp || '__key']: `${props.config[mForm?.keyProp || '__key']}${String(props.index)}`,
|
||||
[mForm?.keyProp || '__key']: `${(props.config as Record<string, any>)[mForm?.keyProp || '__key']}${String(
|
||||
props.index,
|
||||
)}`,
|
||||
}));
|
||||
|
||||
const title = computed(() => {
|
||||
|
||||
@ -3,14 +3,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FieldProps, HiddenConfig } from '../schema';
|
||||
import type { FormValue } from '../schema';
|
||||
import { useAddField } from '../utils/useAddField';
|
||||
|
||||
defineOptions({
|
||||
name: 'MFormHidden',
|
||||
});
|
||||
|
||||
const props = defineProps<FieldProps<HiddenConfig>>();
|
||||
const props = defineProps<{
|
||||
model: FormValue;
|
||||
name: string;
|
||||
prop: string;
|
||||
}>();
|
||||
|
||||
useAddField(props.prop);
|
||||
</script>
|
||||
|
||||
@ -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>{{ option.text }}</span>
|
||||
<span v-if="option.text">{{ option.text }}</span>
|
||||
</div>
|
||||
</TMagicTooltip>
|
||||
</component>
|
||||
|
||||
@ -21,7 +21,7 @@ import dayjs from 'dayjs';
|
||||
|
||||
import { TMagicTimePicker } from '@tmagic/design';
|
||||
|
||||
import type { ChangeRecord, DaterangeConfig, FieldProps } from '../schema';
|
||||
import type { ChangeRecord, FieldProps, TimerangeConfig } from '../schema';
|
||||
import { datetimeFormatter } from '../utils/form';
|
||||
import { useAddField } from '../utils/useAddField';
|
||||
|
||||
@ -29,7 +29,7 @@ defineOptions({
|
||||
name: 'MFormTimeRange',
|
||||
});
|
||||
|
||||
const props = defineProps<FieldProps<DaterangeConfig>>();
|
||||
const props = defineProps<FieldProps<TimerangeConfig>>();
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
|
||||
@ -16,44 +16,8 @@
|
||||
* 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';
|
||||
@ -91,52 +55,14 @@ 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 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);
|
||||
},
|
||||
};
|
||||
export { default } from './plugin';
|
||||
|
||||
102
packages/form/src/plugin.ts
Normal file
102
packages/form/src/plugin.ts
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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 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 './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', 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);
|
||||
},
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
import { computed, inject } from 'vue';
|
||||
|
||||
import { tMagicMessage } from '@tmagic/design';
|
||||
import type { FormState } from '@tmagic/form-schema';
|
||||
import type { FormConfig, FormState } from '@tmagic/form-schema';
|
||||
|
||||
import { initValue } from '../utils/form';
|
||||
|
||||
@ -86,7 +86,7 @@ export const useAdd = (
|
||||
}
|
||||
|
||||
inputs = await initValue(mForm, {
|
||||
config: columns,
|
||||
config: columns as FormConfig,
|
||||
initValues: inputs,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { inject, nextTick, type Ref, type ShallowRef, watchEffect } from 'vue';
|
||||
import Sortable, { type SortableEvent } from 'sortablejs';
|
||||
import type { default as SortableType, SortableEvent } from 'sortablejs';
|
||||
|
||||
import { type TMagicTable } from '@tmagic/design';
|
||||
import type { FormState } from '@tmagic/form-schema';
|
||||
@ -8,6 +8,9 @@ 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,
|
||||
@ -17,15 +20,16 @@ export const useSortable = (
|
||||
) => {
|
||||
const mForm = inject<FormState | undefined>('mForm');
|
||||
|
||||
let sortable: Sortable | undefined;
|
||||
const rowDrop = () => {
|
||||
let sortable: SortableType | undefined;
|
||||
const rowDrop = async () => {
|
||||
sortable?.destroy();
|
||||
const tableEl = tMagicTableRef.value?.getEl();
|
||||
const tBodyEl = tableEl?.querySelector('.el-table__body > tbody') || tableEl?.querySelector('.t-table__body');
|
||||
if (!tBodyEl) {
|
||||
return;
|
||||
}
|
||||
sortable = Sortable.create(tBodyEl, {
|
||||
|
||||
sortable = (await loadSortable()).create(tBodyEl, {
|
||||
draggable: '.tmagic-design-table-row',
|
||||
filter: 'input', // 表单组件选字操作和触发拖拽会冲突,优先保证选字操作
|
||||
preventOnFilter: false, // 允许选字
|
||||
|
||||
@ -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 { FormState, TableColumnConfig } from '@tmagic/form-schema';
|
||||
import type { FormItemConfig, 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) => {
|
||||
const makeConfig = (config: TableColumnConfig, row: any): TableColumnConfig => {
|
||||
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),
|
||||
config: makeConfig(column, row) as FormItemConfig,
|
||||
model: row,
|
||||
lastValues: lastData.value[$index],
|
||||
isCompare: props.isCompare,
|
||||
|
||||
@ -16,6 +16,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Component } from 'vue';
|
||||
|
||||
let $MAGIC_FORM = {} as any;
|
||||
|
||||
const setConfig = (option: any): void => {
|
||||
@ -24,4 +26,17 @@ const setConfig = (option: any): void => {
|
||||
|
||||
const getConfig = <T = unknown>(key: string): T => $MAGIC_FORM[key];
|
||||
|
||||
export { getConfig, setConfig };
|
||||
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 };
|
||||
|
||||
@ -23,7 +23,7 @@ import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { getValueByKeyPath } from '@tmagic/utils';
|
||||
|
||||
import {
|
||||
import type {
|
||||
ChildConfig,
|
||||
ContainerCommonConfig,
|
||||
DaterangeConfig,
|
||||
@ -34,6 +34,7 @@ import {
|
||||
HtmlField,
|
||||
Rule,
|
||||
SortProp,
|
||||
TableConfig,
|
||||
TabPaneConfig,
|
||||
TypeFunction,
|
||||
} from '../schema';
|
||||
@ -118,7 +119,8 @@ const initValueItem = function (
|
||||
) {
|
||||
const { items } = item as ContainerCommonConfig;
|
||||
const { names } = item as DaterangeConfig;
|
||||
const { type, name } = item as ChildConfig;
|
||||
const type = 'type' in item ? item.type : '';
|
||||
const { name } = item;
|
||||
|
||||
if (isTableSelect(type) && name) {
|
||||
value[name] = initValue[name] ?? '';
|
||||
@ -148,14 +150,15 @@ const initValueItem = function (
|
||||
setValue(mForm, value, initValue, item);
|
||||
|
||||
if (type === 'table') {
|
||||
if (item.defautSort) {
|
||||
sortChange(value[name], item.defautSort);
|
||||
} else if (item.defaultSort) {
|
||||
sortChange(value[name], item.defaultSort);
|
||||
const tableConfig = item as TableConfig;
|
||||
if (tableConfig.defautSort) {
|
||||
sortChange(value[name], tableConfig.defautSort);
|
||||
} else if (tableConfig.defaultSort) {
|
||||
sortChange(value[name], tableConfig.defaultSort);
|
||||
}
|
||||
|
||||
if (item.sort && item.sortKey) {
|
||||
value[name].sort((a: any, b: any) => b[item.sortKey] - a[item.sortKey]);
|
||||
if (tableConfig.sort && tableConfig.sortKey) {
|
||||
value[name].sort((a: any, b: any) => b[tableConfig.sortKey!] - a[tableConfig.sortKey!]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,8 +172,8 @@ export const createValues = function (
|
||||
value: FormValue = {},
|
||||
) {
|
||||
if (Array.isArray(config)) {
|
||||
config.forEach((item: ChildConfig | TabPaneConfig) => {
|
||||
initValueItem(mForm, item, initValue, value);
|
||||
config.forEach((item) => {
|
||||
initValueItem(mForm, item as ChildConfig | TabPaneConfig, initValue, value);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
{
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/schema",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"main": "dist/tmagic-schema.umd.cjs",
|
||||
"module": "dist/tmagic-schema.js",
|
||||
"module": "dist/es/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./dist/tmagic-schema.js",
|
||||
"import": "./dist/es/index.js",
|
||||
"require": "./dist/tmagic-schema.umd.cjs"
|
||||
},
|
||||
"./*": "./*"
|
||||
|
||||
@ -186,7 +186,7 @@ export interface CodeBlockContent {
|
||||
/** 代码块名称 */
|
||||
name: string;
|
||||
/** 代码块内容 */
|
||||
content: ((...args: any[]) => any) | string;
|
||||
content: ((...args: any[]) => any) | Function;
|
||||
/** 参数定义 */
|
||||
params: CodeParam[] | [];
|
||||
/** 注释 */
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
{
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/stage",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"main": "dist/tmagic-stage.umd.cjs",
|
||||
"module": "dist/tmagic-stage.js",
|
||||
"module": "dist/es/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./dist/tmagic-stage.js",
|
||||
"import": "./dist/es/index.js",
|
||||
"require": "./dist/tmagic-stage.umd.cjs"
|
||||
},
|
||||
"./*": "./*"
|
||||
@ -30,6 +31,7 @@
|
||||
"dependencies": {
|
||||
"@scena/guides": "^0.29.2",
|
||||
"events": "^3.3.0",
|
||||
"@zumer/snapdom": "^2.8.0",
|
||||
"keycon": "^1.4.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"moveable": "^0.53.0",
|
||||
|
||||
@ -236,6 +236,30 @@ export default class ActionManager extends EventEmitter {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取鼠标下方第二个可选中元素(跳过最上层),用于双击穿透选中下方元素
|
||||
* @param event 鼠标事件
|
||||
* @returns 鼠标下方第二个可选中元素,不存在时返回 null
|
||||
*/
|
||||
public async getNextElementFromPoint(event: MouseEvent): Promise<HTMLElement | null> {
|
||||
const els = this.getElementsFromPoint(event as Point);
|
||||
|
||||
let stopped = false;
|
||||
const stop = () => (stopped = true);
|
||||
let skippedFirst = false;
|
||||
for (const el of els) {
|
||||
if (!getIdFromEl()(el)?.startsWith(GHOST_EL_ID_PREFIX) && (await this.isElCanSelect(el, event, stop))) {
|
||||
if (stopped) break;
|
||||
if (!skippedFirst) {
|
||||
skippedFirst = true;
|
||||
continue;
|
||||
}
|
||||
return el;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断一个元素能否在当前场景被选中
|
||||
* @param el 被判断的元素
|
||||
|
||||
@ -124,6 +124,7 @@ export default class Rule extends EventEmitter {
|
||||
this.hGuides?.off('changeGuides', this.hGuidesChangeGuidesHandler);
|
||||
this.vGuides?.off('changeGuides', this.vGuidesChangeGuidesHandler);
|
||||
this.containerResizeObserver?.disconnect();
|
||||
this.container = undefined;
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
@ -137,7 +138,6 @@ export default class Rule extends EventEmitter {
|
||||
|
||||
this.hGuides = undefined;
|
||||
this.vGuides = undefined;
|
||||
this.container = undefined;
|
||||
}
|
||||
|
||||
private getGuidesStyle = (type: GuidesType) => ({
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { SnapdomOptions } from '@zumer/snapdom';
|
||||
import type { MoveableOptions, OnDragStart } from 'moveable';
|
||||
|
||||
import type { Id } from '@tmagic/core';
|
||||
@ -88,7 +89,38 @@ export default class StageCore extends EventEmitter {
|
||||
* @param id 选中的id
|
||||
*/
|
||||
public async select(id: Id, event?: MouseEvent): Promise<void> {
|
||||
const el = this.renderer?.getTargetElement(id) || null;
|
||||
if (!this.renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let el = this.renderer.getTargetElement(id) || null;
|
||||
|
||||
if (!el) {
|
||||
el = await new Promise<HTMLElement | null>((resolve) => {
|
||||
const observer = new MutationObserver(() => {
|
||||
const target = this.renderer?.getTargetElement(id);
|
||||
if (target) {
|
||||
observer.disconnect();
|
||||
clearTimeout(timer);
|
||||
resolve(target);
|
||||
}
|
||||
});
|
||||
|
||||
const body = this.renderer?.getDocument()?.body;
|
||||
if (!body) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
observer.observe(body, { childList: true, subtree: true });
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
observer.disconnect();
|
||||
resolve(this.renderer?.getTargetElement(id) || null);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
if (el === this.actionManager?.getSelectedEl()) return;
|
||||
|
||||
await this.renderer?.select([id]);
|
||||
@ -241,6 +273,21 @@ export default class StageCore extends EventEmitter {
|
||||
this.renderer?.reloadIframe(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定id的dom元素生成为图片
|
||||
*/
|
||||
public async getElementImage(
|
||||
id: Id,
|
||||
type: 'download' | 'raw' | 'svg' | 'canvas' | 'png' | 'jpeg' | 'webp' | 'blob' = 'png',
|
||||
options: SnapdomOptions = {},
|
||||
) {
|
||||
if (!this.renderer) {
|
||||
throw new Error('Renderer is not initialized');
|
||||
}
|
||||
|
||||
return this.renderer.getElementImage(id, type, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁实例
|
||||
*/
|
||||
|
||||
@ -18,6 +18,8 @@
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { snapdom, SnapdomOptions } from '@zumer/snapdom';
|
||||
|
||||
import type { Id } from '@tmagic/core';
|
||||
import { getElById, getHost, guid, injectStyle, isSameDomain } from '@tmagic/core';
|
||||
|
||||
@ -162,6 +164,35 @@ export default class StageRender extends EventEmitter {
|
||||
return getElById()(this.getDocument(), id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定id的dom元素生成为图片
|
||||
* @param id 目标元素的id
|
||||
* @param options 图片生成选项
|
||||
* @returns data URL 格式的图片数据
|
||||
*/
|
||||
public async getElementImage(
|
||||
id: Id,
|
||||
type: 'download' | 'raw' | 'svg' | 'canvas' | 'png' | 'jpeg' | 'webp' | 'blob' = 'png',
|
||||
options: SnapdomOptions = {},
|
||||
) {
|
||||
const el = this.getTargetElement(id);
|
||||
if (!el) {
|
||||
throw new Error(`Element with id "${id}" not found`);
|
||||
}
|
||||
|
||||
el.scrollIntoView();
|
||||
|
||||
const toFunc = `to${type.charAt(0).toUpperCase() + type.slice(1)}`;
|
||||
|
||||
const result = await snapdom(el, options);
|
||||
|
||||
if (toFunc in result) {
|
||||
return result[toFunc]();
|
||||
}
|
||||
|
||||
throw new Error(`Invalid type: ${type}`);
|
||||
}
|
||||
|
||||
public postTmagicRuntimeReady() {
|
||||
this.contentWindow = this.iframe?.contentWindow as RuntimeWindow;
|
||||
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
{
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/table",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
"dist/style.css",
|
||||
"dist/es/style.css",
|
||||
"src/theme/*"
|
||||
],
|
||||
"main": "dist/tmagic-table.umd.cjs",
|
||||
"module": "dist/tmagic-table.js",
|
||||
"module": "dist/es/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./dist/tmagic-table.js",
|
||||
"import": "./dist/es/index.js",
|
||||
"require": "./dist/tmagic-table.umd.cjs"
|
||||
},
|
||||
"./dist/style.css": {
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<MForm
|
||||
v-else-if="(config.type || config.editInlineFormConfig) && editState[index]"
|
||||
label-width="0"
|
||||
:config="config.editInlineFormConfig ?? [config]"
|
||||
:config="config.editInlineFormConfig ?? [config as FormItemConfig]"
|
||||
:init-values="editState[index]"
|
||||
@change="formChangeHandler"
|
||||
></MForm>
|
||||
@ -46,7 +46,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { TMagicButton, TMagicTag, TMagicTooltip } from '@tmagic/design';
|
||||
import { type ContainerChangeEventData, MForm } from '@tmagic/form';
|
||||
import type { FormValue } from '@tmagic/form-schema';
|
||||
import type { FormItemConfig, FormValue } from '@tmagic/form-schema';
|
||||
import { setValueByKeyPath } from '@tmagic/utils';
|
||||
|
||||
import { ColumnConfig } from './schema';
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
{
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/tdesign-vue-next-adapter",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"main": "dist/tmagic-tdesign-vue-next-adapter.umd.cjs",
|
||||
"module": "dist/tmagic-tdesign-vue-next-adapter.js",
|
||||
"module": "dist/es/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./dist/tmagic-tdesign-vue-next-adapter.js",
|
||||
"import": "./dist/es/index.js",
|
||||
"require": "./dist/tmagic-tdesign-vue-next-adapter.umd.cjs"
|
||||
},
|
||||
"./*": "./*"
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
{
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/utils",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"main": "dist/tmagic-utils.umd.cjs",
|
||||
"module": "dist/tmagic-utils.js",
|
||||
"module": "dist/es/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./dist/tmagic-utils.js",
|
||||
"import": "./dist/es/index.js",
|
||||
"require": "./dist/tmagic-utils.umd.cjs"
|
||||
}
|
||||
},
|
||||
|
||||
5
packages/utils/src/const.ts
Normal file
5
packages/utils/src/const.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX = 'ds-field::';
|
||||
|
||||
export const DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX = 'ds-field-changed';
|
||||
|
||||
export const DATA_SOURCE_SET_DATA_METHOD_NAME = 'setDataFromEvent';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user