├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── docs ├── .vitepress │ ├── config.mts │ └── theme │ │ └── index.ts ├── about.md ├── guide │ ├── add-component.md │ ├── case │ │ ├── calculation.md │ │ ├── http.md │ │ ├── linkage.md │ │ └── rules.md │ ├── form-parser.md │ ├── introduction.md │ └── render.md ├── index.md ├── package.json ├── public │ ├── add-component.png │ ├── add-config.png │ ├── add-hook.png │ ├── alipay.png │ ├── favicon.ico │ ├── home.png │ ├── http-case.png │ ├── logo.png │ ├── qj-cal.jpg │ ├── qj.png │ ├── rules.png │ ├── wx.jpg │ └── wxpay.png └── update-log.md ├── package.json ├── packages └── lowform-design │ ├── .eslintrc-auto-import.json │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .prettierrc.json │ ├── env.d.ts │ ├── index.html │ ├── package.json │ ├── public │ ├── alipay.png │ ├── favicon.ico │ ├── flowable.jpg │ ├── form.png │ ├── preview.png │ ├── qq_qun.jpg │ ├── wx.jpg │ └── wxpay.png │ ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── axios │ │ └── index.ts │ ├── components │ │ ├── CodemirrorEditor │ │ │ └── index.vue │ │ └── FormParser │ │ │ ├── context.ts │ │ │ ├── hooks │ │ │ ├── useButton.tsx │ │ │ ├── useCheckbox.tsx │ │ │ ├── useDivider.tsx │ │ │ ├── useRadio.tsx │ │ │ ├── useSelect.tsx │ │ │ ├── useTransfer.tsx │ │ │ └── useUpload.tsx │ │ │ └── index.vue │ ├── main.ts │ ├── mock │ │ ├── index.ts │ │ └── news.ts │ ├── mockProdServer.ts │ ├── router │ │ └── index.ts │ ├── stores │ │ ├── global.ts │ │ └── index.ts │ ├── styles │ │ ├── base.scss │ │ └── index.scss │ ├── typings │ │ ├── auto-imports.d.ts │ │ ├── components.d.ts │ │ └── index.d.ts │ └── views │ │ ├── example │ │ └── index.vue │ │ └── form-design │ │ ├── CodeDrawer.vue │ │ ├── editor-body │ │ ├── index.scss │ │ └── index.tsx │ │ ├── index.vue │ │ ├── left-sidebar │ │ ├── AssemblyPanel.vue │ │ ├── CasePanel.vue │ │ ├── OutlinePanel.vue │ │ ├── SourcePanel.vue │ │ ├── case │ │ │ ├── case1.ts │ │ │ ├── case2.ts │ │ │ ├── case3.ts │ │ │ ├── formula.ts │ │ │ ├── news.ts │ │ │ ├── submit.ts │ │ │ └── validate.ts │ │ ├── fieldConfig.ts │ │ └── index.vue │ │ ├── right-panel │ │ ├── event │ │ │ ├── CodeEditorDialog.vue │ │ │ └── index.vue │ │ ├── index.vue │ │ └── props │ │ │ ├── Alert.vue │ │ │ ├── Button.vue │ │ │ ├── Card.vue │ │ │ ├── Checkbox.vue │ │ │ ├── Collapse.vue │ │ │ ├── Color.vue │ │ │ ├── DataOptions.vue │ │ │ ├── Date.vue │ │ │ ├── DateRange.vue │ │ │ ├── Divider.vue │ │ │ ├── Form.vue │ │ │ ├── Input.vue │ │ │ ├── Number.vue │ │ │ ├── Radio.vue │ │ │ ├── Rate.vue │ │ │ ├── Row.vue │ │ │ ├── Select.vue │ │ │ ├── Slider.vue │ │ │ ├── Style.vue │ │ │ ├── Switch.vue │ │ │ ├── Tab.vue │ │ │ ├── Textarea.vue │ │ │ ├── Time.vue │ │ │ ├── TimeRange.vue │ │ │ ├── Transfer.vue │ │ │ ├── Upload.vue │ │ │ └── index.vue │ │ └── top-area │ │ └── index.vue │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── uno.config.ts │ └── vite.config.ts ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: pages 15 | cancel-in-progress: false 16 | 17 | 18 | jobs: 19 | build_and_deploy: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | 27 | - uses: pnpm/action-setup@v3 28 | with: 29 | version: 9 30 | 31 | - name: Setup Node 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: 20 35 | cache: pnpm 36 | 37 | - name: Setup Pages 38 | uses: actions/configure-pages@v4 39 | 40 | - name: Install dependencies 41 | run: pnpm install 42 | 43 | - name: Build with LowForm 44 | run: | 45 | cd ./packages/lowform-design 46 | pnpm build-only 47 | 48 | - name: Build with docs 49 | run: | 50 | cd ./docs 51 | pnpm run docs:build 52 | 53 | - name: Upload artifact 54 | uses: actions/upload-pages-artifact@v3 55 | with: 56 | path: docs/.vitepress/dist 57 | 58 | - name: Deploy to GitHub Pages 59 | id: deployment 60 | uses: actions/deploy-pages@v4 61 | 62 | - name: Submit to external warehouse 63 | uses: peaceiris/actions-gh-pages@v4 64 | with: 65 | personal_token: ${{ secrets.PERSONAL_TOKEN }} 66 | external_repository: tsai996/lowform-pages 67 | publish_branch: main 68 | publish_dir: ./packages/lowform-design/dist 69 | 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | docs/.vitepress/dist 17 | docs/.vitepress/cache 18 | 19 | /cypress/videos/ 20 | /cypress/screenshots/ 21 | 22 | # Editor directories and files 23 | .vscode/* 24 | !.vscode/extensions.json 25 | .idea 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | 32 | *.tsbuildinfo 33 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 |

lowform

5 |

低代码表单设计器

6 |
7 | 8 | ## 介绍 9 | 10 | lowform是一个基于`Vue3`,`Vite`,`TypeScript`,`Element-Plus`等技术栈开发的,适用于低代码或无代码开发平台的表单设计器。 11 | 让普通人也能通过简单配置快速搭建表单。
12 | 13 | - [文档](https://tsai996.github.io/lowform/) 14 | - [在线预览](https://tsai996.github.io/lowform-pages/) 15 | - [成品案例](https://demo.lowflow.vip/) 16 | 17 | #### 示例图 18 | 19 |

20 | 表单设计 21 | 表单预览 22 |

23 | 24 | #### 项目源码 25 | 26 | | | 源码地址 | 27 | |--------|-----------------------------------------| 28 | | github | https://github.com/tsai996/lowform | 29 | | 码云 | https://gitee.com/cai_xiao_feng/lowform | 30 | 31 | 32 | ## 特性 33 | 34 | | 组件 | 状态 | 35 | |-------|----| 36 | | 输入框 | ✅ | 37 | | 多行输入框 | ✅ | 38 | | 数值输入框 | ✅ | 39 | | 下拉框 | ✅ | 40 | | 单选框 | ✅ | 41 | | 多选框 | ✅ | 42 | | 开关 | ✅ | 43 | | 日期选择 | ✅ | 44 | | 日期范围 | ✅ | 45 | | 时间选择 | ✅ | 46 | | 时间范围 | ✅ | 47 | | 评分 | ✅ | 48 | | 滑块 | ✅ | 49 | | 文件上传 | ✅ | 50 | | 取色器 | ✅ | 51 | | 穿梭框 | ✅ | 52 | | 标签页 | ✅ | 53 | | 折叠面板 | ✅ | 54 | | 分栏布局 | ✅ | 55 | | 卡片 | ✅ | 56 | | 按钮 | ✅ | 57 | | 提示 | ✅ | 58 | | 分割线 | ✅ | 59 | 60 | ## 添加微信好友拉入群聊(备注:加群) 61 |

62 | 微信 63 | QQ群 64 |

65 | 66 | ## 赞助 67 | 68 | 开源不易如果该项目对您有帮助,您可以请我喝杯奶茶。 69 |

70 | 微信 71 | 支付宝 72 |

73 | 74 | ## 推荐 75 | 76 | 大家在使用本项目时,推荐结合贺波老师的书 77 | [《深入Flowable流程引擎:核心原理与高阶实战》](https://item.jd.com/14804836.html)学习。这本书得到了Flowable创始人Tijs Rademakers亲笔作序推荐,对系统学习和深入掌握Flowable的用法非常有帮助。 78 | ![flowable.jpg](packages/lowform-design/public/flowable.jpg) 79 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vitepress' 2 | import ElementPlus from "element-plus" 3 | 4 | // https://vitepress.dev/reference/site-config 5 | export default defineConfig({ 6 | title: "LowForm文档", 7 | description: "低代码表单设计器", 8 | lang: 'zh-CN', 9 | base: "/lowform", 10 | head: [['link', { rel: 'icon', href: '/lowform/favicon.ico' }]], 11 | themeConfig: { 12 | logo: "/logo.png", 13 | // https://vitepress.dev/reference/default-theme-config 14 | nav: [ 15 | {text: "文档", link: "/guide/introduction"}, 16 | {text: '更新日志', link: '/update-log', activeMatch: "/update-log"}, 17 | {text: '在线预览', link: 'https://tsai996.github.io/lowform-pages'}, 18 | {text: '关于', link: '/about', activeMatch: "/about"}, 19 | ], 20 | footer: { 21 | copyright: `本文档内容版权属于 LowForm 作者,保留所有权利 。赣ICP备2022011445号-2`, 22 | }, 23 | sidebar: { 24 | "/guide/": [ 25 | { 26 | text: '介绍', 27 | items: [ 28 | {text: "简介", link: "/guide/introduction"}, 29 | {text: "扩展组件", link: "/guide/add-component"}, 30 | {text: "表单解析器", link: "/guide/form-parser"}, 31 | ] 32 | }, 33 | { 34 | text: '案例', 35 | items: [ 36 | {text: "http请求", link: "/guide/case/http"}, 37 | {text: "组件联动", link: "/guide/case/linkage"}, 38 | {text: "规则校验", link: "/guide/case/rules"}, 39 | {text: "动态计算", link: "/guide/case/calculation"}, 40 | ] 41 | } 42 | ] 43 | }, 44 | 45 | socialLinks: [ 46 | {icon: 'github', link: 'https://gitee.com/cai_xiao_feng/lowform'} 47 | ], 48 | search: { 49 | provider: 'local' 50 | } 51 | }, 52 | vite: { 53 | css: { 54 | preprocessorOptions: { 55 | scss: { api: 'modern-compiler' } 56 | } 57 | } 58 | } 59 | }) 60 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import Theme from 'vitepress/theme'; 2 | import ElementPlus from 'element-plus'; 3 | import zhCn from 'element-plus/es/locale/lang/zh-cn' 4 | import 'element-plus/dist/index.css'; 5 | import * as Icons from '@element-plus/icons-vue' 6 | export default { 7 | ...Theme, 8 | enhanceApp({ app }) { 9 | app.use(ElementPlus, { 10 | locale: zhCn, 11 | }); 12 | for (const [key, component] of Object.entries(Icons)) { 13 | app.component(key, component) 14 | } 15 | }, 16 | 17 | } -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | # 作者 & 赞助 2 | ## 作者 3 | 姓名:蔡晓锋 4 | 5 | 邮箱:1941338475@qq.com 6 | 7 | 职位:全栈开发工程师 8 | 9 | 现居:浙江省/宁波市 10 | 11 | 开源项目: 12 | * 流程设计器: [lowflow-design](https://gitee.com/cai_xiao_feng/lowflow-design) 13 | * 表单设计器: [lowform-design](https://gitee.com/cai_xiao_feng/lowform-design) 14 | 15 | 合作意向: 16 | 17 | ## 赞助 18 | 19 | 开源不易如果该项目对您有帮助,您可以请我喝杯奶茶。 20 |

21 | 微信 22 | 支付宝 23 |

-------------------------------------------------------------------------------- /docs/guide/add-component.md: -------------------------------------------------------------------------------- 1 | # 扩展组件 2 | 在表单渲染引擎中,扩展自定义组件是一个常见的需求。 3 | 通过扩展组件,可以满足特定的业务需求或实现个性化的功能。 4 | 以下是扩展组件的完整流程,包括添加组件、注册组件以及注册 Hook 的详细说明。 5 | ## 1. 添加组件 6 | 在 `fieldConfig.ts `文件中添加组件。 7 | ### 1.1 示例 8 | 假设需要添加一个名为 `CustomSelect` 的自定义组件,可以在 `fieldConfig.ts` 文件中进行如下配置: 9 | ```typescript 10 | import type { FormField } from '@xfc/vue3-form-render' 11 | 12 | export const formItems: FormField[] = [ 13 | { 14 | id: 'custom_select', // 组件唯一标识 15 | name: 'CustomSelect', // 组件名称 16 | icon: 'ph:textbox', // 图标 17 | type: 'formItem', // 组件类型 18 | label: '自定义输入框', // 组件标签 19 | value: null, // 默认值 20 | props: { 21 | placeholder: '请输入内容' // 组件属性 22 | }, 23 | ... 24 | }, 25 | // 其他组件配置... 26 | ]; 27 | 28 | export default fieldConfig; 29 | ``` 30 | ## 2. 注册组件 31 | 将自定义组件注册到 `useFormRender` 中,以便表单解析引擎能够正确解析和渲染组件。 32 | ### 2.1 示例:注册自定义组件 33 | 在 `useFormRender` 中注册 `CustomSelect` 组件: 34 | ```typescript 35 | import CustomSelect from './components/CustomSelect'; // 导入自定义组件 36 | 37 | const { Render, FormRender, addComponent, removeComponent, components, hooks } = useFormRender({ 38 | injects: { 39 | http: http 40 | } 41 | }) 42 | export { Render, FormRender, addComponent, removeComponent, components, hooks } 43 | addComponent('CustomSelect', CustomSelect) 44 | ... 45 | ``` 46 | ## 3. 注册 Hook 47 | 通过注册 Hook,可以在组件渲染前修改其属性,实现个性化配置。 48 | ### 3.1 示例:注册 Hook 49 | 假设需要在 `CustomSelect` 组件渲染前动态设置其 `placeholder` 属性,可以注册一个 Hook: 50 | ```tsx 51 | import type { FormField } from '@xfc/vue3-form-render' 52 | 53 | export const useSelect = function (field: FormField) { 54 | const slots = { 55 | default: () => { 56 | return field.props.options?.map((item: Recordable) => { 57 | return 58 | }) 59 | } 60 | } 61 | if (!field.slots) { 62 | field.slots = {} 63 | } 64 | Object.assign(field.slots, slots) 65 | } 66 | addComponent('CustomSelect', CustomSelect, useSelect) 67 | ``` 68 | ## 4. 完整流程 69 | 1. 添加组件配置:在 `fieldConfig.ts` 中定义组件的属性和行为。 70 | 2. 注册组件:在 `useFormRender` 中注册组件,使其能够被表单解析引擎识别。 71 | 3. 注册 Hook:通过 Hook 动态修改组件属性,实现个性化需求。 -------------------------------------------------------------------------------- /docs/guide/case/calculation.md: -------------------------------------------------------------------------------- 1 | # 动态计算 2 | 动态计算是指组件B的值由其他组件的值通过计算得到。 3 | 这种机制常用于表单中需要根据用户输入动态更新某些字段的场景。以下是两个典型示例: 4 | ## 1. 示例1:订单总价格 5 | 订单总价格由单价乘以数量得到。 6 | 实现逻辑 7 | * 监听**单价**和**数量**字段的值变化。 8 | * 当任一字段的值发生变化时,重新计算总价格并更新到**总价格**字段。 9 | 10 | 代码示例: 11 | ```javascript 12 | const { gformData } = $inject; 13 | const unitPrice = formData.unitPrice || 0; 14 | const quantity = formData.quantity || 0; 15 | formData.totalPrice = unitPrice * quantity; 16 | ``` 17 | ## 2. 示例2:请假时长 18 | 以下是一个完整的请假时长动态计算示例: 19 | 20 | 实现逻辑 21 | * 监听**请假时间**字段的值变化。 22 | * 当**请假时间**的值发生变化时,计算时间差并转换为天数,更新到**请假时长**字段。 23 | 24 | 代码示例: 25 | ```javascript 26 | const {getField,formData} = $inject 27 | const value = event[0] 28 | const timestamp = new Date(value[1]).getTime() - new Date(value[0]).getTime() 29 | formData.field_ykkfx = timestamp /1000/60/60/24 30 | ``` 31 | ## 3. 完整示例 32 | 请前往 [在线预览](https://tsai996.github.io/lowform-pages/) ,案例/组件联动/请假,查看完整案例。 33 | ![qj-cal.jpg](../../public/qj-cal.jpg) -------------------------------------------------------------------------------- /docs/guide/case/http.md: -------------------------------------------------------------------------------- 1 | # http请求 2 | 组件的数据源通常需要通过 HTTP 请求从后端接口获取。 3 | 以下是关于如何在组件中通过 HTTP 请求获取数据并动态更新组件属性的详细说明和示例。 4 | 5 | ## 1. 实现步骤 6 | ### 1.1 注入 HTTP 请求对象 7 | 首先,需要将 HTTP 请求对象注入到表单解析器中。具体方法请参考:[注入方法/属性](../form-parser#_6-注入方法-属性)。 8 | ### 1.2 在 **onVnodeMounted** 事件中编写请求代码 9 | 在组件的 `onVnodeMounted` 生命周期事件中,通过 `$inject` 获取 HTTP 对象,并调用其 `request` 方法发送请求。 10 | 将响应结果赋值给组件的 `field.props.options` 属性。 11 | ## 2. 代码示例 12 | 以下是一个完整的示例,展示如何通过 HTTP 请求获取数据并更新组件的选项列表: 13 | ```javascript 14 | const http = $inject.http 15 | http.request({ 16 | url: '/api/news/list' 17 | }).then(res=>{ 18 | if(res.success){ 19 | field.props.options = res.data.map(e=>{ 20 | return { 21 | label: e.title, 22 | value: e.id 23 | } 24 | }) 25 | } 26 | }).catch(err => { 27 | console.error('请求失败:', err); 28 | }); 29 | ``` 30 | ## 3. 示例说明 31 | ### 3.1 请求地址 32 | * `url`:后端接口地址,例如 `/api/news/list`。 33 | ### 3.2 响应数据处理 34 | * `res.data`:假设后端返回的数据格式为数组,每个元素包含 `id` 和 `title` 字段。 35 | * `field.props.options`:将响应数据映射为组件需要的格式,`label` 用于显示文本,`value` 用于选项值。 36 | ### 3.3 组件更新 37 | * 当请求成功后,`field.props.options` 会被更新,组件将根据新的选项列表重新渲染。 38 | 39 | ## 4. 完整示例: 40 | 请前往 [在线预览](https://tsai996.github.io/lowform-pages/) ,案例/组件联动/动态数据联动,查看完整案例。 41 | ![http-case.png](../../public/http-case.png) 42 | -------------------------------------------------------------------------------- /docs/guide/case/linkage.md: -------------------------------------------------------------------------------- 1 | # 组件联动 2 | 组件联动是指当一个组件(组件A)触发某个事件时,另一个组件(组件B)的状态会根据特定条件发生改变。以下是一个请假案例的示例: 3 | ## 1. 示例场景 4 | 当**请假类型**为**事假**时,**请假原因**字段为必填;否则,**请假原因**字段为非必填。 5 | ## 2. 代码实现 6 | 通过监听组件A(请假类型)的值变化,动态设置组件B(请假原因)的必填状态。以下为代码示例: 7 | ```javascript 8 | const {getField} = $inject 9 | const yy = getField('field_h75fl') 10 | const value = event[0] 11 | if(value == 2){ 12 | yy.required = true 13 | }else{ 14 | yy.required = false 15 | } 16 | ``` 17 | ### 代码说明 18 | 1. `getField`:用于获取表单中指定字段的组件对象。 19 | 2. `event[0]`:表示组件A(请假类型)的当前值。 20 | 3. **条件判断**:如果组件A的值为 `2`(代表“事假”),则将组件B(请假原因)的 `required` 属性设置为 `true`,否则设置为 `false`。 21 | 22 | ## 3. 完整案例 23 | 请前往 [在线预览](https://tsai996.github.io/lowform-pages/) ,案例/组件联动/请假,查看完整案例。 24 | ![qj.png](../../public/qj.png) -------------------------------------------------------------------------------- /docs/guide/case/rules.md: -------------------------------------------------------------------------------- 1 | # 规则校验 2 | 规则校验用于确保用户输入的内容符合特定格式或条件。通过在 `FormField` 对象的 `rules` 属性中配置校验规则,可以实现对输入内容的动态校验。 3 | 以下是关于规则校验的详细说明和示例: 4 | ## 1. 规则校验配置 5 | 在 `FormField` 对象中,`rules` 属性用于定义校验规则。每个规则通常包含以下字段: 6 | * `pattern`:正则表达式,用于匹配输入内容。 7 | * `message`:校验失败时显示的错误提示信息。 8 | ## 2. 示例:手机号格式校验 9 | 以下是一个手机号格式校验的配置示例: 10 | ```json 11 | { 12 | "id": "field_tx34r", 13 | "name": "Input", 14 | "type": "formItem", 15 | "label": "手机", 16 | "value": null, 17 | "rules": [ 18 | { 19 | "pattern": "^(0|86|17951)?(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57])[0-9]{8}$", 20 | "message": "手机" 21 | } 22 | ], 23 | ... 24 | } 25 | ``` 26 | ### 3. 常见校验规则示例 27 | 以下是一些常见的输入格式校验规则示例: 28 | #### 邮箱格式校验 29 | ```json 30 | { 31 | "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", 32 | "message": "请输入有效的邮箱地址" 33 | } 34 | ``` 35 | #### URL地址校验 36 | ```json 37 | { 38 | "pattern": "^(https?:\\/\\/)?([\\da-z.-]+)\\.([a-z.]{2,6})([\\/\\w .-]*)*\\/?$", 39 | "message": "请输入有效的URL地址" 40 | } 41 | ``` 42 | #### 身份证号码校验 43 | ```json 44 | { 45 | "pattern": "^\\d{17}[\\dXx]$", 46 | "message": "请输入有效的身份证号码" 47 | } 48 | ``` 49 | ## 3. 完整示例 50 | 完整例子请查看: 51 | 52 | 选中组件在右侧属性面板的 **格式校验** 选择对应的校验正则。 53 | ![rules.png](../../public/rules.png) 54 | -------------------------------------------------------------------------------- /docs/guide/form-parser.md: -------------------------------------------------------------------------------- 1 | # 表单解析器 2 | ## 1. 介绍 3 | 表单解析器是一个可以将json格式解析渲染为表单的组件,支持自定义渲染器,注册组件,自定义组件hook,属性注入等。 4 | 5 | ## 2. 安装 6 | ```shell 7 | npm install @xfc/vue3-form-render 8 | ``` 9 | ## 3. 注册组件 10 | 通过 `addComponent` 方法注册组件,组件注册后可以在json中通过 `name` 属性引用。 11 | ```typescript 12 | import { useSelect } from './hooks/useSelect' 13 | import { useFormRender } from '@xfc/vue3-form-render' 14 | import http from '@/axios' 15 | 16 | const { Render, FormRender, addComponent, removeComponent, components, hooks } = useFormRender({ 17 | injects: { 18 | http: http 19 | } 20 | }) 21 | export { Render, FormRender, addComponent, removeComponent, components, hooks } 22 | addComponent('Form', ElForm) 23 | addComponent('Input', ElInput) 24 | addComponent('Textarea', ElInput) 25 | addComponent('Number', ElInputNumber) 26 | addComponent('Select', ElSelect, useSelect) 27 | ... 28 | ``` 29 | ## 4. 注册Hook 30 | 通过 `addComponent` 添加组件时附带添加上该组件的Hook,Hook可以在组件渲染器修改组件属性,事件等。 31 | ```tsx 32 | import type { FormField } from '@xfc/vue3-form-render' 33 | 34 | export const useSelect = function (field: FormField) { 35 | const slots = { 36 | default: () => { 37 | return field.props.options?.map((item: Recordable) => { 38 | return 39 | }) 40 | } 41 | } 42 | if (!field.slots) { 43 | field.slots = {} 44 | } 45 | Object.assign(field.slots, slots) 46 | } 47 | addComponent('Select', ElSelect, useSelect) 48 | ``` 49 | 50 | ## 5. 渲染器 51 | 默认渲染器分为2种分别为: 52 | * **formItem:** 表单项渲染器 53 | * **container:** 容器渲染器 54 | 55 | 自定义渲染器: 56 | 57 | 渲染器命名优先级:id > name > type,比如如果存在id为 `custom` 的FormField对象,则会优先使用**custom**渲染器。 58 | ```typescript 59 | const { Render, FormRender, addComponent, removeComponent, components, hooks } = useFormRender({ 60 | injects: { 61 | http: http 62 | }, 63 | renders:{ 64 | custom: field => { 65 | return h(Render, {field}) 66 | } 67 | } 68 | }) 69 | ``` 70 | 71 | ## 6. 注入方法/属性 72 | 注入后的方法属性可以的组件事件中调用,比 `customFun` 为例。 73 | ```typescript 74 | const { Render, FormRender, addComponent, removeComponent, components, hooks } = useFormRender({ 75 | injects: { 76 | http: http, 77 | customFun: ()=>{console.log('测试')} 78 | } 79 | }) 80 | ``` 81 | 在 `FormField` 对象的on事件中调用通过 `$inject` 调用注入的方法。 82 | ```typescript 83 | const formConf = ref({ 84 | id: 'formConf', 85 | name: 'Form', 86 | icon: 'ep:document', 87 | type: 'container', 88 | label: '表单', 89 | value: undefined, 90 | readonly: false, 91 | required: undefined, 92 | hidden: false, 93 | props: {...}, 94 | on: { 95 | onMounted: '$inject.customFun()', 96 | onUnmounted: '', 97 | onValidate: '' 98 | }, 99 | children: [] 100 | }) 101 | ``` 102 | ## 7. 插槽扩展 103 | 插槽分为3类分别为: 104 | * 父容器插槽: 105 | 106 | | 插槽名 | 参数 | 说明 | 107 | |---------|:-----------------------:|:------------:| 108 | | prepend | field | 顶部追加渲染组件 | 109 | | default | child、field | 默认插槽,覆盖默认的子项 | 110 | | append | field | 底部追加渲染组件 | 111 | 112 | * 组件插槽: 113 | 114 | 组件插槽分为3类优先级分别为:id > name > type 115 | 116 | | 插槽名 | 参数 | 说明 | 117 | |----------------|:-----:|:------:| 118 | | FormField 内(id、name、type)值 | node、field | 覆盖渲染组件 | 119 | 120 | * 只读插槽: 121 | 122 | 如果对默认只读显示或某些组件的只读显示不满意可以使用该插槽替换默认的只读显示。 123 | 124 | | 插槽名 | 参数 | 说明 | 125 | |------------------------|:-----:|:-----------:| 126 | | readonly | value、field | 覆盖所有只读显示 | 127 | | readonly-`${field.name}` | value、field | 覆盖指定组件的只读显示 | -------------------------------------------------------------------------------- /docs/guide/introduction.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | :::tip LowForm 4 | LowForm 是一款功能强大的低代码表单设计器,支持通过拖拽组件快速构建表单页面。它能够以极少的代码实现复杂的表单逻辑,并通过 hook 方式将数据解析为不同的 UI 组件,极大地提升了开发效率和灵活性。 5 | ::: 6 | ![home.png](../public/home.png) 7 | ## 1. 组件库 8 | 组件库提供了两种便捷的字段添加方式: 9 | * **点击组件:** 点击组件自动添加到画板中,会自动选中该组件。 10 | * **拖拽组件:** 将组件拖拽到画板中,会自动选中该组件。 11 | ## 2. 表单画板 12 | 表单画板是用户设计和布局表单的核心区域。用户可以通过组件库添加的组件在画板中进行自由排列和组合,实时预览表单的布局效果。画板支持以下功能: 13 | * **组件调整位置:** 用户可以通过拖拽组件来调整其在画板中的位置。 14 | * **组件属性配置:** 选中画板中的组件后,右侧属性面板会显示该组件的详细配置选项,用户可以根据需求进行修改。 15 | * **组件复制/删除:** 点击选中组件右下角的操作按钮,即可快速复制或删除组件。 16 | 17 | 通过直观的拖拽操作和实时预览,LowForm 极大地简化了表单设计的流程,帮助用户快速构建出符合需求的表单页面。 18 | ## 3. 属性面板 19 | 属性面板是用户配置组件属性的核心区域。当用户选中画板中的组件时,属性面板会动态显示该组件的可配置选项,包括但不限于: 20 | * **常规:** 如字段名、标签、操作(必填、只读、隐藏)等。 21 | * **属性:** 默认值以及组件的其他属性。 22 | * **事件:** 配置组件的交互行为,如点击事件、输入变化事件等,支持自定义逻辑绑定。 23 | 24 | 属性面板的设计直观易用,支持实时预览修改效果,帮助用户快速调整组件的显示效果和交互行为,确保表单满足业务需求。 25 | ## 4. 工具栏 26 | 工具栏包含多个常用操作,提升表单设计效率: 27 | * **撤回/恢复:** 撤回或恢复历史操作记录,轻松修改设计。 28 | * **电脑/手机切换:** 切换不同平台(电脑/手机)展示效果,确保跨平台兼容。 29 | * **导出/导入:** 支持导入已有表单数据或导出已完成设计的表单。 30 | * **生成代码:** 一键生成 Vue 代码并下载,快速与项目对接。 31 | * **清空/预览:** 清空画板内容或点击预览,查看最终渲染效果。 32 | 33 | 通过这些功能,LowForm 让表单设计、导出和集成变得更加高效、直观。 -------------------------------------------------------------------------------- /docs/guide/render.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | Render 是一个用于渲染表单组件的组件,通过传入 field 属性,可以直接渲染对应的组件。 3 | 35 | ## 1. 接口格式 36 | ```typescript 37 | export interface Field { 38 | // 组件名 39 | name: string 40 | // 属性 41 | props: Props 42 | // 插槽 43 | slots?: {[name: string]: unknown} 44 | // 事件 45 | on?: Recordable, field: Field) => void) | undefined> 46 | } 47 | ``` 48 | ## 2. 渲染例子 49 | 以渲染el-input组件为例子,传入field属性后直接当作el-input组件使用。 50 | ::: details 点我查看代码 51 | ```vue lines:line-numbers {1} 52 | 82 | 83 | 88 | 89 | 90 | ``` 91 | ::: -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "LowForm" 7 | text: "低代码表单设计器" 8 | tagline: 强大、可视化、精美、高扩展 9 | image: 10 | src: /logo.png 11 | alt: LowForm 12 | width: 140 13 | actions: 14 | - theme: brand 15 | text: 开始 16 | link: /guide/introduction 17 | - theme: alt 18 | text: 开源地址 19 | link: https://gitee.com/cai_xiao_feng/lowform-design 20 | 21 | features: 22 | - icon: 💡 23 | title: 最新技术 24 | details: 使用最新的技术栈,Vue3、Vite、TypeScript、Element-Plus等 25 | - icon: ⚡️ 26 | title: 优异性能 27 | details: 通过Vite构建,开发环境秒级热更新,生产环境秒开 28 | - icon: 🛠️ 29 | title: 高效代码封装 30 | details: 通过hooks方式将数据解析为不同的UI组件,极大地提升了开发效率和灵活性 31 | - icon: 🛡️ 32 | title: 组件丰富 33 | details: 提供了多种常用的表单组件,如输入框、下拉框、日期选择等 34 | - icon: 🎨 35 | title: 精美UI 36 | details: 通过Element-Plus提供的组件,实现了精美的UI设计 37 | - icon: 📦 38 | title: 开箱即用 39 | details: 无需配置,即可快速搭建表单页面 40 | --- 41 | 42 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "vitepress": "^1.5.0" 4 | }, 5 | "scripts": { 6 | "docs:dev": "vitepress dev", 7 | "docs:build": "vitepress build", 8 | "docs:preview": "vitepress preview" 9 | }, 10 | "dependencies": { 11 | "@element-plus/icons-vue": "^2.3.1", 12 | "element-plus": "^2.9.0" 13 | } 14 | } -------------------------------------------------------------------------------- /docs/public/add-component.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/docs/public/add-component.png -------------------------------------------------------------------------------- /docs/public/add-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/docs/public/add-config.png -------------------------------------------------------------------------------- /docs/public/add-hook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/docs/public/add-hook.png -------------------------------------------------------------------------------- /docs/public/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/docs/public/alipay.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/docs/public/home.png -------------------------------------------------------------------------------- /docs/public/http-case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/docs/public/http-case.png -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/docs/public/logo.png -------------------------------------------------------------------------------- /docs/public/qj-cal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/docs/public/qj-cal.jpg -------------------------------------------------------------------------------- /docs/public/qj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/docs/public/qj.png -------------------------------------------------------------------------------- /docs/public/rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/docs/public/rules.png -------------------------------------------------------------------------------- /docs/public/wx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/docs/public/wx.jpg -------------------------------------------------------------------------------- /docs/public/wxpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/docs/public/wxpay.png -------------------------------------------------------------------------------- /docs/update-log.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | ## V1.0.2 (2025-01-05) 4 | * 新增:注入http请求 5 | * 新增:添加http请求案例 6 | * 新增:显示/隐藏表单项标签 7 | * 新增:分割线组件 8 | * 修复:修复事件案例注入方法名错误 9 | 10 | ## V1.0.1 (2024-12-27) 11 | * 新增:案例面板提供各种案例 12 | * 新增:添加按钮组件 13 | * 优化:更新@xfc/vue3-form-render依赖为V1.1.4 14 | * 优化:按钮主题添加默认主题 15 | * 优化:事件代码添加徽章 16 | 17 | ## V1.0.0 (2024-12-22) 18 | * 新增:事件代码、注入event、ref、field、$inject 19 | * 新增:添加按钮/提示组件 20 | * 优化:更新@xfc/vue3-form-render依赖为V1.1.3 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lowform", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /packages/lowform-design/.eslintrc-auto-import.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "Component": true, 4 | "ComponentPublicInstance": true, 5 | "ComputedRef": true, 6 | "EffectScope": true, 7 | "ExtractDefaultPropTypes": true, 8 | "ExtractPropTypes": true, 9 | "ExtractPublicPropTypes": true, 10 | "InjectionKey": true, 11 | "PropType": true, 12 | "Ref": true, 13 | "VNode": true, 14 | "WritableComputedRef": true, 15 | "computed": true, 16 | "createApp": true, 17 | "customRef": true, 18 | "defineAsyncComponent": true, 19 | "defineComponent": true, 20 | "effectScope": true, 21 | "getCurrentInstance": true, 22 | "getCurrentScope": true, 23 | "h": true, 24 | "inject": true, 25 | "isProxy": true, 26 | "isReactive": true, 27 | "isReadonly": true, 28 | "isRef": true, 29 | "markRaw": true, 30 | "nextTick": true, 31 | "onActivated": true, 32 | "onBeforeMount": true, 33 | "onBeforeRouteLeave": true, 34 | "onBeforeRouteUpdate": true, 35 | "onBeforeUnmount": true, 36 | "onBeforeUpdate": true, 37 | "onDeactivated": true, 38 | "onErrorCaptured": true, 39 | "onMounted": true, 40 | "onRenderTracked": true, 41 | "onRenderTriggered": true, 42 | "onScopeDispose": true, 43 | "onServerPrefetch": true, 44 | "onUnmounted": true, 45 | "onUpdated": true, 46 | "onWatcherCleanup": true, 47 | "provide": true, 48 | "reactive": true, 49 | "readonly": true, 50 | "ref": true, 51 | "resolveComponent": true, 52 | "shallowReactive": true, 53 | "shallowReadonly": true, 54 | "shallowRef": true, 55 | "toRaw": true, 56 | "toRef": true, 57 | "toRefs": true, 58 | "toValue": true, 59 | "triggerRef": true, 60 | "unref": true, 61 | "useAttrs": true, 62 | "useCssModule": true, 63 | "useCssVars": true, 64 | "useId": true, 65 | "useLink": true, 66 | "useModel": true, 67 | "useRoute": true, 68 | "useRouter": true, 69 | "useSlots": true, 70 | "useTemplateRef": true, 71 | "watch": true, 72 | "watchEffect": true, 73 | "watchPostEffect": true, 74 | "watchSyncEffect": true, 75 | "ElMessageBox": true, 76 | "ElMessage": true, 77 | "ElInput": true, 78 | "ElInputNumber": true, 79 | "ElSelect": true, 80 | "ElRadioGroup": true, 81 | "ElCheckboxGroup": true, 82 | "ElDatePicker": true, 83 | "ElTimePicker": true, 84 | "ElSwitch": true, 85 | "ElSlider": true, 86 | "ElRate": true, 87 | "ElUpload": true, 88 | "ElTabs": true, 89 | "ElTabPane": true, 90 | "ElRow": true, 91 | "ElCol": true, 92 | "ElColorPicker": true, 93 | "ElTransfer": true, 94 | "ElCollapse": true, 95 | "ElCollapseItem": true, 96 | "ElCard": true, 97 | "ElForm": true, 98 | "DirectiveBinding": true, 99 | "MaybeRef": true, 100 | "MaybeRefOrGetter": true, 101 | "ElFormItem": true, 102 | "ElAlert": true, 103 | "ElButton": true, 104 | "ElDivider": true 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /packages/lowform-design/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier/skip-formatting' 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 'latest' 14 | }, 15 | rules: { 16 | 'no-var': 'error', 17 | 'prefer-const': 'off', 18 | 'eqeqeq': 'warn', 19 | '@typescript-eslint/no-unused-vars': ['warn', { 'argsIgnorePattern': '^_' }], 20 | 'vue/no-mutating-props': ['error', { 21 | 'shallowOnly': true 22 | }], 23 | 'vue/multi-word-component-names': 'off' 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/lowform-design/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | *.tsbuildinfo 31 | -------------------------------------------------------------------------------- /packages/lowform-design/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /packages/lowform-design/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/lowform-design/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | lowform-design 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/lowform-design/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lowform-design", 3 | "version": "1.0.2", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "run-p type-check \"build-only {@}\" --", 9 | "preview": "vite preview", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --build --force", 12 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 13 | "format": "prettier --write src/" 14 | }, 15 | "dependencies": { 16 | "@codemirror/autocomplete": "^6.18.3", 17 | "@codemirror/lang-html": "^6.4.9", 18 | "@codemirror/lang-javascript": "^6.2.2", 19 | "@codemirror/lang-json": "^6.0.1", 20 | "@codemirror/theme-one-dark": "^6.1.2", 21 | "@element-plus/icons-vue": "^2.3.1", 22 | "@vueuse/core": "^11.0.3", 23 | "@xfc/vue3-draggable": "^1.0.5", 24 | "@xfc/vue3-form-render": "^1.2.7", 25 | "axios": "^1.7.9", 26 | "codemirror": "^6.0.1", 27 | "element-plus": "^2.9.0", 28 | "file-saver": "^2.0.5", 29 | "lodash-es": "^4.17.21", 30 | "pinia": "^2.1.7", 31 | "vue": "^3.5.13", 32 | "vue-codemirror": "^6.1.1", 33 | "vue-router": "^4.3.3" 34 | }, 35 | "devDependencies": { 36 | "@iconify/vue": "^4.1.2", 37 | "@rushstack/eslint-patch": "^1.8.0", 38 | "@tsconfig/node20": "^20.1.4", 39 | "@types/file-saver": "^2.0.7", 40 | "@types/lodash-es": "^4.17.12", 41 | "@types/node": "^20.14.5", 42 | "@vitejs/plugin-vue": "^5.0.5", 43 | "@vitejs/plugin-vue-jsx": "^4.0.0", 44 | "@vue/eslint-config-prettier": "^9.0.0", 45 | "@vue/eslint-config-typescript": "^13.0.0", 46 | "@vue/tsconfig": "^0.5.1", 47 | "eslint": "^8.57.0", 48 | "eslint-plugin-vue": "^9.23.0", 49 | "mockjs": "^1.1.0", 50 | "npm-run-all2": "^6.2.0", 51 | "pinia-plugin-persistedstate": "^4.0.1", 52 | "prettier": "^3.2.5", 53 | "sass": "^1.82.0", 54 | "typescript": "~5.4.0", 55 | "unocss": "^0.62.3", 56 | "unplugin-auto-import": "^0.18.3", 57 | "unplugin-vue-components": "^0.27.4", 58 | "vite": "^5.3.1", 59 | "vite-plugin-mock": "^2.9.6", 60 | "vite-plugin-vue-devtools": "^7.3.1", 61 | "vite-plugin-vue-setup-extend": "^0.4.0", 62 | "vue-tsc": "^2.0.21" 63 | }, 64 | "packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e" 65 | } 66 | -------------------------------------------------------------------------------- /packages/lowform-design/public/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/packages/lowform-design/public/alipay.png -------------------------------------------------------------------------------- /packages/lowform-design/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/packages/lowform-design/public/favicon.ico -------------------------------------------------------------------------------- /packages/lowform-design/public/flowable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/packages/lowform-design/public/flowable.jpg -------------------------------------------------------------------------------- /packages/lowform-design/public/form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/packages/lowform-design/public/form.png -------------------------------------------------------------------------------- /packages/lowform-design/public/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/packages/lowform-design/public/preview.png -------------------------------------------------------------------------------- /packages/lowform-design/public/qq_qun.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/packages/lowform-design/public/qq_qun.jpg -------------------------------------------------------------------------------- /packages/lowform-design/public/wx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/packages/lowform-design/public/wx.jpg -------------------------------------------------------------------------------- /packages/lowform-design/public/wxpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/packages/lowform-design/public/wxpay.png -------------------------------------------------------------------------------- /packages/lowform-design/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /packages/lowform-design/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsai996/lowform/4a9e14c64dcd124ddaa3d98ef8779bb475954faf/packages/lowform-design/src/assets/logo.png -------------------------------------------------------------------------------- /packages/lowform-design/src/axios/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AxiosError, 3 | AxiosInstance, 4 | AxiosRequestConfig, 5 | AxiosResponse, 6 | InternalAxiosRequestConfig 7 | } from 'axios' 8 | import axios from 'axios' 9 | 10 | export interface ResultData { 11 | code: number 12 | success: boolean 13 | message: string 14 | data: T 15 | } 16 | 17 | class RequestHttp { 18 | service: AxiosInstance 19 | 20 | public constructor(config: AxiosRequestConfig) { 21 | this.service = axios.create(config) 22 | this.service.interceptors.request.use( 23 | (config: InternalAxiosRequestConfig) => { 24 | config.headers['Authorization'] = 'Bearer token' 25 | return config 26 | }, 27 | (error: AxiosError) => { 28 | console.log(error) 29 | } 30 | ) 31 | this.service.interceptors.response.use((response: AxiosResponse) => { 32 | const { data } = response 33 | return data 34 | }) 35 | } 36 | 37 | request(config: AxiosRequestConfig): Promise> { 38 | return this.service.request(config) 39 | } 40 | } 41 | 42 | export default new RequestHttp({ 43 | baseURL: '/', 44 | timeout: 10000 45 | }) 46 | -------------------------------------------------------------------------------- /packages/lowform-design/src/components/CodemirrorEditor/index.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /packages/lowform-design/src/components/FormParser/context.ts: -------------------------------------------------------------------------------- 1 | import { useSelect } from './hooks/useSelect' 2 | import { useRadio } from './hooks/useRadio' 3 | import { useCheckbox } from './hooks/useCheckbox' 4 | import { useUpload } from './hooks/useUpload' 5 | import { useTransfer } from './hooks/useTransfer' 6 | import { useButton } from './hooks/useButton' 7 | import { useDivider } from './hooks/useDivider' 8 | import { useFormRender } from '@xfc/vue3-form-render' 9 | import http from '@/axios' 10 | 11 | const { Render, FormRender, addComponent, removeComponent, components, hooks } = useFormRender({ 12 | injects: { 13 | http: http 14 | } 15 | }) 16 | export { Render, FormRender, addComponent, removeComponent, components, hooks } 17 | addComponent('Form', ElForm) 18 | addComponent('Input', ElInput) 19 | addComponent('Textarea', ElInput) 20 | addComponent('Number', ElInputNumber) 21 | addComponent('Select', ElSelect, useSelect) 22 | addComponent('Radio', ElRadioGroup, useRadio) 23 | addComponent('Checkbox', ElCheckboxGroup, useCheckbox) 24 | addComponent('Date', ElDatePicker) 25 | addComponent('DateRange', ElDatePicker) 26 | addComponent('Time', ElTimePicker) 27 | addComponent('TimeRange', ElTimePicker) 28 | addComponent('Switch', ElSwitch) 29 | addComponent('Slider', ElSlider) 30 | addComponent('Rate', ElRate) 31 | addComponent('Upload', ElUpload, useUpload) 32 | addComponent('Tab', ElTabs) 33 | addComponent('TabPane', ElTabPane) 34 | addComponent('Row', ElRow) 35 | addComponent('Col', ElCol) 36 | addComponent('Collapse', ElCollapse) 37 | addComponent('CollapseItem', ElCollapseItem) 38 | addComponent('Card', ElCard) 39 | addComponent('Color', ElColorPicker) 40 | addComponent('Transfer', ElTransfer, useTransfer) 41 | addComponent('Button', ElButton, useButton) 42 | addComponent('Alert', ElAlert) 43 | addComponent('Divider', ElDivider, useDivider) 44 | -------------------------------------------------------------------------------- /packages/lowform-design/src/components/FormParser/hooks/useButton.tsx: -------------------------------------------------------------------------------- 1 | import type { FormField } from '@xfc/vue3-form-render' 2 | 3 | export const useButton = function (field: FormField) { 4 | const slots = { 5 | default: () => field.label 6 | } 7 | if (!field.slots) { 8 | field.slots = {} 9 | } 10 | Object.assign(field.slots, slots) 11 | } 12 | -------------------------------------------------------------------------------- /packages/lowform-design/src/components/FormParser/hooks/useCheckbox.tsx: -------------------------------------------------------------------------------- 1 | import type { FormField } from '@xfc/vue3-form-render' 2 | 3 | export const useCheckbox = function (field: FormField) { 4 | const slots = { 5 | default: () => { 6 | return field.props.options?.map((item: Recordable) => { 7 | return ( 8 | 9 | {item.label} 10 | 11 | ) 12 | }) 13 | } 14 | } 15 | if (!field.slots) { 16 | field.slots = {} 17 | } 18 | Object.assign(field.slots, slots) 19 | } 20 | -------------------------------------------------------------------------------- /packages/lowform-design/src/components/FormParser/hooks/useDivider.tsx: -------------------------------------------------------------------------------- 1 | import type { FormField } from '@xfc/vue3-form-render' 2 | 3 | export const useDivider = function (field: FormField) { 4 | const slots = { 5 | default: () => field.props.content 6 | } 7 | if (!field.slots) { 8 | field.slots = {} 9 | } 10 | Object.assign(field.slots, slots) 11 | } 12 | -------------------------------------------------------------------------------- /packages/lowform-design/src/components/FormParser/hooks/useRadio.tsx: -------------------------------------------------------------------------------- 1 | import type { FormField } from '@xfc/vue3-form-render' 2 | 3 | export const useRadio = function (field: FormField) { 4 | const slots = { 5 | default: () => { 6 | return field.props.options?.map((item: Recordable) => { 7 | return ( 8 | 9 | {item.label} 10 | 11 | ) 12 | }) 13 | } 14 | } 15 | if (!field.slots) { 16 | field.slots = {} 17 | } 18 | Object.assign(field.slots, slots) 19 | } 20 | -------------------------------------------------------------------------------- /packages/lowform-design/src/components/FormParser/hooks/useSelect.tsx: -------------------------------------------------------------------------------- 1 | import type { FormField } from '@xfc/vue3-form-render' 2 | 3 | export const useSelect = function (field: FormField) { 4 | const slots = { 5 | default: () => { 6 | return field.props.options?.map((item: Recordable) => { 7 | return 8 | }) 9 | } 10 | } 11 | if (!field.slots) { 12 | field.slots = {} 13 | } 14 | Object.assign(field.slots, slots) 15 | } 16 | -------------------------------------------------------------------------------- /packages/lowform-design/src/components/FormParser/hooks/useTransfer.tsx: -------------------------------------------------------------------------------- 1 | import type { FormField } from '@xfc/vue3-form-render' 2 | 3 | export const useTransfer = function (field: FormField) { 4 | field.props.props = { 5 | key: 'value', 6 | label: 'label', 7 | disabled: 'disabled' 8 | } 9 | field.props.data = field.props.options 10 | delete field.props.options 11 | } 12 | -------------------------------------------------------------------------------- /packages/lowform-design/src/components/FormParser/hooks/useUpload.tsx: -------------------------------------------------------------------------------- 1 | import type { FormField } from '@xfc/vue3-form-render' 2 | 3 | export const useUpload = function (field: FormField) { 4 | const slots = { 5 | default: () => { 6 | return ( 7 | 8 | 选择文件 9 | 10 | ) 11 | }, 12 | tip: () => { 13 | const typeDesc = field.props.accept ? ` 的${field.props.accept}文件` : null 14 | return ( 15 |
16 | 单个文件只能上传不超过 {field.props.size}MB {typeDesc} 17 |
18 | ) 19 | } 20 | } 21 | if (!field.slots) { 22 | field.slots = {} 23 | } 24 | Object.assign(field.slots, slots) 25 | } 26 | -------------------------------------------------------------------------------- /packages/lowform-design/src/components/FormParser/index.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /packages/lowform-design/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.vue' 2 | import 'element-plus/theme-chalk/dark/css-vars.css' 3 | import 'uno.css' 4 | import '@/styles/index.scss' 5 | 6 | import { createApp } from 'vue' 7 | import router from '@/router' 8 | import pinia from '@/stores' 9 | import { Icon } from '@iconify/vue' 10 | import * as Icons from '@element-plus/icons-vue' 11 | 12 | const app = createApp(App) 13 | for (const [key, component] of Object.entries(Icons)) { 14 | app.component(key, component) 15 | } 16 | app.use(pinia).use(router).component('Iconify', Icon).mount('#app') 17 | -------------------------------------------------------------------------------- /packages/lowform-design/src/mock/index.ts: -------------------------------------------------------------------------------- 1 | import News from './news' 2 | import type { MockMethod } from 'vite-plugin-mock' 3 | 4 | const mockModules: MockMethod[] = [...News] 5 | export default mockModules 6 | -------------------------------------------------------------------------------- /packages/lowform-design/src/mock/news.ts: -------------------------------------------------------------------------------- 1 | import type { MockMethod } from 'vite-plugin-mock' 2 | 3 | const newsOptions = [ 4 | { 5 | id: '1', 6 | title: '未来科技展在虚构城市开幕,展示虚拟现实新突破', 7 | content: ` 8 | 近日,未来科技展在虚构城市“星辰市”盛大开幕,吸引了全球科技爱好者的关注。本次展览以“虚拟与现实”为主题,展示了最新的虚拟现实(VR)和增强现实(AR)技术。 9 | 展览中,一款名为“幻境之门”的VR设备备受瞩目,它能够为用户提供沉浸式的虚拟世界体验。此外,多家科技公司还展示了AR技术在教育、医疗等领域的创新应用。 10 | 主办方表示,未来科技展旨在推动科技创新,激发年轻人对科技的兴趣和创造力。 11 | ` 12 | }, 13 | { 14 | id: '2', 15 | title: '虚构国家推出全球首条无人驾驶空中巴士线路', 16 | content: ` 17 | 虚构国家“蓝天国”近日宣布,全球首条无人驾驶空中巴士线路正式开通。这条线路连接了首都“云之城”和科技中心“未来谷”,全程约50公里。 18 | 空中巴士采用最新的自动驾驶技术,能够在高空中安全运行。乘客只需通过手机应用预订座位,即可享受便捷的空中出行服务。 19 | 蓝天国交通部门表示,未来将逐步扩大空中巴士网络,推动绿色出行和智慧交通的发展。 20 | ` 21 | }, 22 | { 23 | id: '3', 24 | title: '虚构游戏公司发布全新元宇宙平台', 25 | content: ` 26 | 虚构游戏公司“幻梦科技”近日发布了全新的元宇宙平台“幻梦宇宙”。该平台允许用户创建虚拟角色、购买虚拟土地,并与其他用户互动。 27 | 幻梦宇宙还支持虚拟货币交易,用户可以通过完成任务或出售虚拟物品赚取收益。公司表示,未来将引入更多社交和商业功能,打造一个完整的虚拟生态系统。 28 | 目前,幻梦宇宙已吸引了数百万用户注册,成为全球最热门的元宇宙平台之一。 29 | ` 30 | }, 31 | { 32 | id: '4', 33 | title: '虚构电影节在梦幻岛开幕,展示全球独立电影佳作', 34 | content: ` 35 | 近日,虚构电影节“梦幻岛国际电影节”在风景如画的梦幻岛开幕。本次电影节以“独立与创新”为主题,展映了来自全球的50多部独立电影佳作。 36 | 电影节期间,还举办了多场导演论坛和观众互动活动,探讨电影艺术的未来发展方向。主办方表示,希望通过电影节推动独立电影的发展,为年轻导演提供展示才华的平台。 37 | 梦幻岛国际电影节已成为全球电影爱好者关注的焦点,吸引了众多媒体和影迷前来参与。 38 | ` 39 | }, 40 | { 41 | id: '5', 42 | title: '虚构大学推出全球首个AI辅助教学系统', 43 | content: ` 44 | 虚构大学“星辰大学”近日推出了全球首个AI辅助教学系统“智慧导师”。该系统能够根据学生的学习进度和兴趣,提供个性化的学习建议和资源。 45 | 智慧导师还支持实时互动,学生可以通过语音或文字与AI导师交流,解决学习中的疑难问题。星辰大学表示,未来将进一步完善系统功能,推动教育模式的创新。 46 | 目前,智慧导师已在星辰大学的多个课程中试点应用,受到了师生的广泛好评。 47 | ` 48 | }, 49 | { 50 | id: '6', 51 | title: '虚构环保组织发起全球植树行动', 52 | content: ` 53 | 虚构环保组织“绿色未来”近日发起了全球植树行动,计划在未来五年内种植10亿棵树。该行动旨在应对气候变化,保护地球生态环境。 54 | 绿色未来表示,植树行动将覆盖全球多个国家和地区,重点恢复退化的森林和生态系统。公众可以通过官方网站或手机应用参与行动,捐赠树苗或亲自参与植树活动。 55 | 目前,已有数十万志愿者加入行动,共同为地球的绿色未来贡献力量。 56 | ` 57 | }, 58 | { 59 | id: '7', 60 | title: '虚构音乐节在星光城举办,吸引全球音乐爱好者', 61 | content: ` 62 | 近日,虚构音乐节“星光音乐节”在星光城盛大开幕,吸引了全球音乐爱好者的关注。本次音乐节以“音乐与梦想”为主题,邀请了多位国际知名音乐人和乐队参与演出。 63 | 音乐节期间,还举办了多场音乐工作坊和互动活动,观众可以近距离感受音乐的魅力。主办方表示,希望通过音乐节推动文化交流,为年轻音乐人提供展示才华的平台。 64 | 星光音乐节已成为全球音乐界的盛事,吸引了数十万观众前来参与。 65 | ` 66 | }, 67 | { 68 | id: '8', 69 | title: '虚构公司推出全球首款可折叠智能手机', 70 | content: ` 71 | 虚构科技公司“未来科技”近日发布了全球首款可折叠智能手机“FlexPhone”。该手机采用柔性屏幕技术,可以在折叠和展开之间自由切换。 72 | FlexPhone还配备了最新的处理器和摄像头,支持5G网络和AI功能。未来科技表示,这款手机将重新定义移动设备的未来,为用户带来全新的使用体验。 73 | 目前,FlexPhone已在全球多个国家和地区上市,受到了消费者的热烈追捧。 74 | ` 75 | }, 76 | { 77 | id: '9', 78 | title: '虚构城市推出全球首个智能垃圾分类系统', 79 | content: ` 80 | 虚构城市“绿光城”近日推出了全球首个智能垃圾分类系统“绿光分类”。该系统通过AI技术自动识别垃圾类型,并将其分类投放到相应的垃圾桶中。 81 | 绿光分类还支持手机应用,居民可以通过应用查询垃圾分类信息,并获得环保积分奖励。绿光城政府表示,未来将逐步推广该系统,推动城市的可持续发展。 82 | 目前,绿光分类已在绿光城的多个社区试点应用,受到了居民的广泛好评。 83 | ` 84 | }, 85 | { 86 | id: '10', 87 | title: '虚构艺术展在幻影博物馆开幕,展示数字艺术新潮流', 88 | content: ` 89 | 近日,虚构艺术展“数字幻影”在幻影博物馆开幕,展示了数字艺术的最新潮流。本次展览以“科技与艺术”为主题,展出了多位艺术家的数字绘画、互动装置和虚拟现实作品。 90 | 展览中,一款名为“光影之舞”的互动装置备受瞩目,观众可以通过手势控制光影的变化,创造出独特的艺术效果。主办方表示,希望通过展览推动数字艺术的发展,为观众带来全新的艺术体验。 91 | 目前,数字幻影展览已吸引了数万名观众前来参观。 92 | ` 93 | }, 94 | { 95 | id: '11', 96 | title: '虚构国家推出全球首个量子计算云平台', 97 | content: ` 98 | 虚构国家“科技联邦”近日推出了全球首个量子计算云平台“量子云”。该平台允许用户通过互联网访问量子计算机,进行复杂的科学计算和数据分析。 99 | 量子云还提供了丰富的开发工具和教程,帮助用户快速掌握量子计算技术。科技联邦表示,未来将逐步扩大平台规模,推动量子计算的普及和应用。 100 | 目前,量子云已在全球多个科研机构和企业中试点应用,受到了广泛关注。 101 | ` 102 | }, 103 | { 104 | id: '12', 105 | title: '虚构运动品牌发布全球首款智能运动鞋', 106 | content: ` 107 | 虚构运动品牌“极速动力”近日发布了全球首款智能运动鞋“极速X”。这款运动鞋内置传感器和AI芯片,可以实时监测用户的运动数据,并提供个性化的运动建议。 108 | 极速X还支持无线充电和防水功能,适合各种户外运动场景。极速动力表示,未来将推出更多智能运动装备,推动运动科技的发展。 109 | 目前,极速X已在全球多个国家和地区上市,受到了运动爱好者的热烈追捧。 110 | ` 111 | }, 112 | { 113 | id: '13', 114 | title: '虚构电影节在梦幻岛开幕,展示全球独立电影佳作', 115 | content: ` 116 | 近日,虚构电影节“梦幻岛国际电影节”在风景如画的梦幻岛开幕。本次电影节以“独立与创新”为主题,展映了来自全球的50多部独立电影佳作。 117 | 电影节期间,还举办了多场导演论坛和观众互动活动,探讨电影艺术的未来发展方向。主办方表示,希望通过电影节推动独立电影的发展,为年轻导演提供展示才华的平台。 118 | 梦幻岛国际电影节已成为全球电影爱好者关注的焦点,吸引了众多媒体和影迷前来参与。 119 | ` 120 | }, 121 | { 122 | id: '14', 123 | title: '虚构公司推出全球首款AI辅助医疗诊断系统', 124 | content: ` 125 | 虚构医疗科技公司“智慧医疗”近日推出了全球首款AI辅助医疗诊断系统“智慧医生”。该系统能够通过分析患者的病历和影像数据,提供精准的诊断建议。 126 | 智慧医生还支持远程会诊,医生可以通过系统与专家团队实时交流,提高诊断效率。智慧医疗表示,未来将进一步完善系统功能,推动医疗行业的智能化发展。 127 | 目前,智慧医生已在多家医院试点应用,受到了医生和患者的好评。 128 | ` 129 | }, 130 | { 131 | id: '15', 132 | title: '虚构城市推出全球首个无人配送物流网络', 133 | content: ` 134 | 虚构城市“未来之城”近日推出了全球首个无人配送物流网络“未来快递”。该网络采用无人机和无人车进行配送,能够快速将包裹送达用户手中。 135 | 未来快递还支持实时追踪功能,用户可以通过手机应用查看包裹的配送进度。未来之城政府表示,未来将逐步扩大网络覆盖范围,推动物流行业的智能化发展。 136 | 目前,未来快递已在未来之城的多个区域试点应用,受到了居民的广泛好评。 137 | ` 138 | } 139 | ] 140 | 141 | const News = [ 142 | { 143 | url: '/api/news/list', 144 | method: 'get', 145 | response: () => { 146 | return { 147 | code: 200, 148 | success: true, 149 | message: '操作成功', 150 | data: newsOptions 151 | } 152 | } 153 | }, 154 | { 155 | url: '/api/news/info', 156 | method: 'get', 157 | response: (req: any) => { 158 | const id = req.query.id 159 | return { 160 | code: 200, 161 | success: true, 162 | message: '操作成功', 163 | data: newsOptions.find((item) => item.id === id) 164 | } 165 | } 166 | } 167 | ] as MockMethod[] 168 | 169 | export default News 170 | -------------------------------------------------------------------------------- /packages/lowform-design/src/mockProdServer.ts: -------------------------------------------------------------------------------- 1 | import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer' 2 | import mock from './mock' 3 | 4 | export function setupProdMockServer() { 5 | createProdMockServer(mock) 6 | } 7 | -------------------------------------------------------------------------------- /packages/lowform-design/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import FormDesign from '@/views/example/index.vue' 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | path: '/', 9 | name: 'FormDesign', 10 | component: FormDesign 11 | } 12 | ] 13 | }) 14 | 15 | export default router 16 | -------------------------------------------------------------------------------- /packages/lowform-design/src/stores/global.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | interface GlobalState { 4 | isDark: boolean 5 | } 6 | 7 | export const useGlobalStore = defineStore({ 8 | id: 'app-global', 9 | state: (): GlobalState => ({ 10 | isDark: false 11 | }), 12 | actions: { 13 | switchMode() { 14 | if (this.isDark) { 15 | document.documentElement.classList.add('dark') 16 | } else { 17 | document.documentElement.classList.remove('dark') 18 | } 19 | } 20 | }, 21 | persist: true 22 | }) 23 | -------------------------------------------------------------------------------- /packages/lowform-design/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' 4 | 5 | const pinia = createPinia() 6 | pinia.use(piniaPluginPersistedstate) 7 | export default pinia 8 | -------------------------------------------------------------------------------- /packages/lowform-design/src/styles/base.scss: -------------------------------------------------------------------------------- 1 | a, 2 | .green { 3 | text-decoration: none; 4 | color: hsla(160, 100%, 37%, 1); 5 | transition: 0.4s; 6 | padding: 3px; 7 | } 8 | 9 | @media (hover: hover) { 10 | a:hover { 11 | background-color: hsla(160, 100%, 37%, 0.2); 12 | } 13 | } 14 | 15 | .el-drawer { 16 | .el-drawer__header { 17 | margin-bottom: 0; 18 | padding: calc(var(--el-drawer-padding-primary) - 5px) var(--el-drawer-padding-primary) 19 | calc(var(--el-drawer-padding-primary) - 6px); 20 | border-bottom: 1px var(--el-border-style) var(--el-border-color); 21 | } 22 | 23 | .el-drawer__footer { 24 | border-top: var(--el-border); 25 | padding: calc(var(--el-drawer-padding-primary) - 5px); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/lowform-design/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @use 'base.scss'; 2 | 3 | :root { 4 | } 5 | 6 | @media (prefers-color-scheme: dark) { 7 | :root { 8 | } 9 | } 10 | 11 | *, 12 | *::before, 13 | *::after { 14 | box-sizing: border-box; 15 | margin: 0; 16 | font-weight: normal; 17 | } 18 | 19 | body { 20 | min-height: 100vh; 21 | color: var(--el-text-color-primary); 22 | background: var(--el-bg-color-page); 23 | transition: 24 | color 0.5s, 25 | background-color 0.5s; 26 | line-height: 1.6; 27 | font-family: 28 | Inter, 29 | -apple-system, 30 | BlinkMacSystemFont, 31 | 'Segoe UI', 32 | Roboto, 33 | Oxygen, 34 | Ubuntu, 35 | Cantarell, 36 | 'Fira Sans', 37 | 'Droid Sans', 38 | 'Helvetica Neue', 39 | sans-serif; 40 | font-size: 15px; 41 | text-rendering: optimizeLegibility; 42 | -webkit-font-smoothing: antialiased; 43 | -moz-osx-font-smoothing: grayscale; 44 | } 45 | 46 | a { 47 | color: var(--el-color-primary); 48 | } 49 | 50 | html, 51 | body, 52 | #app { 53 | width: 100%; 54 | height: 100%; 55 | padding: 0; 56 | margin: 0; 57 | } 58 | 59 | ::-webkit-scrollbar { 60 | // width: 12px; 61 | // height: 12px; 62 | } 63 | 64 | ::-webkit-scrollbar-corner { 65 | background: transparent; 66 | } 67 | 68 | ::-webkit-scrollbar-thumb { 69 | border: 2px solid transparent; 70 | border-radius: 6px; 71 | background-color: var(--el-scrollbar-color); 72 | background-clip: padding-box; 73 | 74 | &:hover { 75 | background-color: var(--el-scrollbar-hover-color); 76 | } 77 | } 78 | 79 | ::-webkit-scrollbar-track { 80 | background-color: transparent; 81 | } 82 | -------------------------------------------------------------------------------- /packages/lowform-design/src/typings/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | // biome-ignore lint: disable 7 | export {} 8 | declare global { 9 | const EffectScope: typeof import('vue')['EffectScope'] 10 | const ElAlert: typeof import('element-plus/es')['ElAlert'] 11 | const ElButton: typeof import('element-plus/es')['ElButton'] 12 | const ElCard: typeof import('element-plus/es')['ElCard'] 13 | const ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] 14 | const ElCol: typeof import('element-plus/es')['ElCol'] 15 | const ElCollapse: typeof import('element-plus/es')['ElCollapse'] 16 | const ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] 17 | const ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] 18 | const ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] 19 | const ElDivider: typeof import('element-plus/es')['ElDivider'] 20 | const ElForm: typeof import('element-plus/es')['ElForm'] 21 | const ElInput: typeof import('element-plus/es')['ElInput'] 22 | const ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] 23 | const ElMessage: typeof import('element-plus/es')['ElMessage'] 24 | const ElMessageBox: typeof import('element-plus/es')['ElMessageBox'] 25 | const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] 26 | const ElRate: typeof import('element-plus/es')['ElRate'] 27 | const ElRow: typeof import('element-plus/es')['ElRow'] 28 | const ElSelect: typeof import('element-plus/es')['ElSelect'] 29 | const ElSlider: typeof import('element-plus/es')['ElSlider'] 30 | const ElSwitch: typeof import('element-plus/es')['ElSwitch'] 31 | const ElTabPane: typeof import('element-plus/es')['ElTabPane'] 32 | const ElTabs: typeof import('element-plus/es')['ElTabs'] 33 | const ElTimePicker: typeof import('element-plus/es')['ElTimePicker'] 34 | const ElTransfer: typeof import('element-plus/es')['ElTransfer'] 35 | const ElUpload: typeof import('element-plus/es')['ElUpload'] 36 | const computed: typeof import('vue')['computed'] 37 | const createApp: typeof import('vue')['createApp'] 38 | const customRef: typeof import('vue')['customRef'] 39 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 40 | const defineComponent: typeof import('vue')['defineComponent'] 41 | const effectScope: typeof import('vue')['effectScope'] 42 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 43 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 44 | const h: typeof import('vue')['h'] 45 | const inject: typeof import('vue')['inject'] 46 | const isProxy: typeof import('vue')['isProxy'] 47 | const isReactive: typeof import('vue')['isReactive'] 48 | const isReadonly: typeof import('vue')['isReadonly'] 49 | const isRef: typeof import('vue')['isRef'] 50 | const markRaw: typeof import('vue')['markRaw'] 51 | const nextTick: typeof import('vue')['nextTick'] 52 | const onActivated: typeof import('vue')['onActivated'] 53 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 54 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 55 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 56 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 57 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 58 | const onDeactivated: typeof import('vue')['onDeactivated'] 59 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 60 | const onMounted: typeof import('vue')['onMounted'] 61 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 62 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 63 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 64 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 65 | const onUnmounted: typeof import('vue')['onUnmounted'] 66 | const onUpdated: typeof import('vue')['onUpdated'] 67 | const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] 68 | const provide: typeof import('vue')['provide'] 69 | const reactive: typeof import('vue')['reactive'] 70 | const readonly: typeof import('vue')['readonly'] 71 | const ref: typeof import('vue')['ref'] 72 | const resolveComponent: typeof import('vue')['resolveComponent'] 73 | const shallowReactive: typeof import('vue')['shallowReactive'] 74 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 75 | const shallowRef: typeof import('vue')['shallowRef'] 76 | const toRaw: typeof import('vue')['toRaw'] 77 | const toRef: typeof import('vue')['toRef'] 78 | const toRefs: typeof import('vue')['toRefs'] 79 | const toValue: typeof import('vue')['toValue'] 80 | const triggerRef: typeof import('vue')['triggerRef'] 81 | const unref: typeof import('vue')['unref'] 82 | const useAttrs: typeof import('vue')['useAttrs'] 83 | const useCssModule: typeof import('vue')['useCssModule'] 84 | const useCssVars: typeof import('vue')['useCssVars'] 85 | const useId: typeof import('vue')['useId'] 86 | const useLink: typeof import('vue-router')['useLink'] 87 | const useModel: typeof import('vue')['useModel'] 88 | const useRoute: typeof import('vue-router')['useRoute'] 89 | const useRouter: typeof import('vue-router')['useRouter'] 90 | const useSlots: typeof import('vue')['useSlots'] 91 | const useTemplateRef: typeof import('vue')['useTemplateRef'] 92 | const watch: typeof import('vue')['watch'] 93 | const watchEffect: typeof import('vue')['watchEffect'] 94 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 95 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 96 | } 97 | // for type re-export 98 | declare global { 99 | // @ts-ignore 100 | export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' 101 | import('vue') 102 | } 103 | -------------------------------------------------------------------------------- /packages/lowform-design/src/typings/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | CodemirrorEditor: typeof import('./../components/CodemirrorEditor/index.vue')['default'] 11 | ElAlert: typeof import('element-plus/es')['ElAlert'] 12 | ElAside: typeof import('element-plus/es')['ElAside'] 13 | ElBadge: typeof import('element-plus/es')['ElBadge'] 14 | ElButton: typeof import('element-plus/es')['ElButton'] 15 | ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] 16 | ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] 17 | ElCollapse: typeof import('element-plus/es')['ElCollapse'] 18 | ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] 19 | ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] 20 | ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] 21 | ElContainer: typeof import('element-plus/es')['ElContainer'] 22 | ElDialog: typeof import('element-plus/es')['ElDialog'] 23 | ElDrawer: typeof import('element-plus/es')['ElDrawer'] 24 | ElForm: typeof import('element-plus/es')['ElForm'] 25 | ElFormItem: typeof import('element-plus/es')['ElFormItem'] 26 | ElHeader: typeof import('element-plus/es')['ElHeader'] 27 | ElIcon: typeof import('element-plus/es')['ElIcon'] 28 | ElInput: typeof import('element-plus/es')['ElInput'] 29 | ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] 30 | ElMain: typeof import('element-plus/es')['ElMain'] 31 | ElOption: typeof import('element-plus/es')['ElOption'] 32 | ElRadio: typeof import('element-plus/es')['ElRadio'] 33 | ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] 34 | ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] 35 | ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] 36 | ElSegmented: typeof import('element-plus/es')['ElSegmented'] 37 | ElSelect: typeof import('element-plus/es')['ElSelect'] 38 | ElSlider: typeof import('element-plus/es')['ElSlider'] 39 | ElSpace: typeof import('element-plus/es')['ElSpace'] 40 | ElSwitch: typeof import('element-plus/es')['ElSwitch'] 41 | ElTabPane: typeof import('element-plus/es')['ElTabPane'] 42 | ElTabs: typeof import('element-plus/es')['ElTabs'] 43 | ElText: typeof import('element-plus/es')['ElText'] 44 | ElTooltip: typeof import('element-plus/es')['ElTooltip'] 45 | ElTree: typeof import('element-plus/es')['ElTree'] 46 | FormParser: typeof import('./../components/FormParser/index.vue')['default'] 47 | RouterLink: typeof import('vue-router')['RouterLink'] 48 | RouterView: typeof import('vue-router')['RouterView'] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/lowform-design/src/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | type Recordable = Record 2 | type Nullable = T | null 3 | type ElRef = Nullable 4 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/example/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/CodeDrawer.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 70 | 71 | 76 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/editor-body/index.scss: -------------------------------------------------------------------------------- 1 | .drawing-board { 2 | height: 100%; 3 | width: 100%; 4 | overflow-y: auto; 5 | overflow-x: hidden; 6 | display: flex; 7 | flex-direction: column; 8 | gap: 5px; 9 | 10 | &:empty { 11 | &::after { 12 | content: '从左侧拖入或点选组件进行表单设计'; 13 | position: absolute; 14 | top: 50%; 15 | left: 50%; 16 | transform: translate(-50%, -50%); 17 | width: 100%; 18 | text-align: center; 19 | color: var(--el-text-color-placeholder); 20 | font-size: 18px; 21 | } 22 | } 23 | } 24 | 25 | .editor-form { 26 | height: 100%; 27 | border: 1px dashed var(--el-border-color); 28 | 29 | &.is-active { 30 | border: 1px solid var(--el-color-primary); 31 | } 32 | 33 | .drawing-item { 34 | position: relative; 35 | width: 100%; 36 | border: 1px dashed var(--el-border-color); 37 | 38 | &:hover, 39 | &.active { 40 | background-color: var(--el-color-primary-light-9); 41 | } 42 | 43 | .el-form-item { 44 | &:before { 45 | content: ''; 46 | position: absolute; 47 | z-index: 6; 48 | top: 0; 49 | right: 0; 50 | width: 100%; 51 | height: 100%; 52 | background: transparent; 53 | } 54 | } 55 | 56 | & > .pointer-move { 57 | position: absolute; 58 | z-index: 10; 59 | left: 0; 60 | top: 0; 61 | width: 22px; 62 | height: 22px; 63 | opacity: 0.6; 64 | cursor: move; 65 | display: flex; 66 | align-items: center; 67 | justify-content: center; 68 | border-bottom-right-radius: 6px; 69 | background-color: var(--el-color-primary); 70 | 71 | &:hover { 72 | opacity: 1; 73 | } 74 | 75 | i { 76 | color: var(--el-color-primary-light-9); 77 | } 78 | } 79 | 80 | & > .pointer-wrapper { 81 | position: absolute; 82 | z-index: 99; 83 | right: 0; 84 | bottom: 0; 85 | line-height: 22px; 86 | display: flex; 87 | align-items: center; 88 | border-top-left-radius: 6px; 89 | background-color: var(--el-color-primary); 90 | 91 | div { 92 | width: 20px; 93 | height: 20px; 94 | display: inline-block; 95 | text-align: center; 96 | margin-left: 2px; 97 | cursor: pointer; 98 | 99 | i { 100 | color: var(--el-color-primary-light-9); 101 | } 102 | } 103 | } 104 | 105 | &.active { 106 | border: 1px solid var(--el-color-primary); 107 | } 108 | &.is_hidden { 109 | background: var(--el-color-danger-light-9); 110 | opacity: 0.7; 111 | } 112 | 113 | .field-wrapper { 114 | padding: 5px 10px 16px; 115 | margin-bottom: 0; 116 | } 117 | } 118 | } 119 | 120 | .widget-list { 121 | width: auto; 122 | min-height: 65px; 123 | overflow: hidden; 124 | background: var(--el-bg-color); 125 | border: 2px solid var(--el-border-color); 126 | padding: 3px; 127 | position: relative; 128 | z-index: 7; 129 | 130 | &:empty { 131 | &::after { 132 | content: '将字段拖放到此区域'; 133 | position: absolute; 134 | top: 50%; 135 | left: 50%; 136 | transform: translate(-50%, -50%); 137 | color: var(--el-text-color-secondary); 138 | font-size: 14px; 139 | letter-spacing: 2px; 140 | text-align: center; 141 | z-index: 1; 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/editor-body/index.tsx: -------------------------------------------------------------------------------- 1 | import type { PropType, Ref, VNode } from 'vue' 2 | import Draggable from '@xfc/vue3-draggable' 3 | import { CopyDocument, Delete, Rank } from '@element-plus/icons-vue' 4 | import './index.scss' 5 | import { cloneDeep } from 'lodash-es' 6 | import { type FormField } from '@xfc/vue3-form-render' 7 | import { Render } from '@/components/FormParser/context' 8 | 9 | export default defineComponent({ 10 | props: { 11 | field: { 12 | type: Object as PropType, 13 | required: true 14 | } 15 | }, 16 | setup(props) { 17 | interface FieldContext { 18 | field: FormField 19 | index: number 20 | list: FormField[] 21 | parent: FormField 22 | } 23 | 24 | const { activeData } = inject<{ 25 | activeData: Ref 26 | }>('formDesign')! 27 | const renders: Recordable<(context: FieldContext) => VNode> = { 28 | Form: (context: FieldContext): VNode => { 29 | const { field } = context 30 | const active = activeData.value?.id === field.id 31 | const className = ['editor-form', { 'is-active': active }] 32 | const child = field.children?.map((item, index) => 33 | renderItem({ field: item, index, list: field.children ?? [], parent: field }) 34 | ) 35 | return ( 36 | { 43 | activeData.value = field 44 | event.stopPropagation() 45 | }} 46 | > 47 | (field.children = val)} 50 | options={{ 51 | group: 'componentsGroup', 52 | handle: '.pointer-move', 53 | animation: 340 54 | }} 55 | onAdd={(element) => (activeData.value = element)} 56 | class="drawing-board p5px" 57 | > 58 | {child} 59 | 60 | 61 | ) 62 | }, 63 | formItem: (context: FieldContext): VNode => { 64 | const { field } = context 65 | const active = activeData.value?.id === field.id 66 | const className = ['drawing-item', { active: active }, { is_hidden: field.hidden }] 67 | const child = renderChildren(field) 68 | return ( 69 |
{ 72 | activeData.value = field 73 | event.stopPropagation() 74 | }} 75 | > 76 | 82 | { 87 | if (field.readonly) return 88 | field.value = val 89 | }} 90 | > 91 | {child} 92 | 93 | 94 | {active ? renderTool(context) : null} 95 |
96 | ) 97 | }, 98 | container: (context: FieldContext): VNode => { 99 | const { field } = context 100 | const active = activeData.value?.id === field.id 101 | const className = ['drawing-item', { active: active }, { is_hidden: field.hidden }] 102 | const containers: string[] = ['Tab', 'Collapse', 'Row'] 103 | const child = containers.includes(field.name) ? renderInner(field) : renderChildren(field) 104 | return ( 105 |
{ 108 | activeData.value = field 109 | event.stopPropagation() 110 | }} 111 | > 112 |
113 | { 118 | field.value = val 119 | }} 120 | > 121 | {child} 122 | 123 |
124 | {active ? renderTool(context) : null} 125 |
126 | ) 127 | } 128 | } 129 | const tools: Recordable<(context: FieldContext) => VNode> = { 130 | move: (_) => { 131 | return ( 132 |
133 | 134 | 135 | 136 |
137 | ) 138 | }, 139 | copy: (context) => { 140 | const { field, index, list } = context 141 | return ( 142 |
{ 144 | const clone = cloneDeep(field) 145 | const generateId = (clone: FormField) => { 146 | clone.id = `field_${Math.random().toString(36).substring(2, 7)}` 147 | clone.children?.forEach((el) => generateId(el)) 148 | } 149 | generateId(clone) 150 | list.splice(index + 1, 0, clone) 151 | activeData.value = clone 152 | event.stopPropagation() 153 | }} 154 | > 155 | 156 | 157 | 158 |
159 | ) 160 | }, 161 | del: (context) => { 162 | const { index, list, parent } = context 163 | return ( 164 |
{ 166 | list.splice(index, 1) 167 | const len = list.length 168 | if (len > 0) { 169 | if (index < len) { 170 | activeData.value = list[index] 171 | } else { 172 | activeData.value = list[index - 1] 173 | } 174 | } else { 175 | activeData.value = parent 176 | } 177 | event.stopPropagation() 178 | }} 179 | > 180 | 181 | 182 | 183 |
184 | ) 185 | } 186 | } 187 | const renderTool = (context: FieldContext) => { 188 | return [ 189 | tools.move(context), 190 |
191 | {tools.copy(context)} 192 | {tools.del(context)} 193 |
194 | ] 195 | } 196 | const renderItem = (context: FieldContext): VNode => { 197 | const { field } = context 198 | const render = renders[field.name] || renders[field.type] 199 | if (render) { 200 | return render(context) 201 | } 202 | return {`No rendering found for ${field.id}`} 203 | } 204 | const renderChildren = (field: FormField): VNode | undefined => { 205 | if (field.children) { 206 | const items = field.children.map((item, index) => 207 | renderItem({ field: item, index, list: field.children ?? [], parent: field }) 208 | ) 209 | return ( 210 | (field.children = val)} 213 | options={{ 214 | group: 'componentsGroup', 215 | handle: '.pointer-move', 216 | animation: 340 217 | }} 218 | onAdd={(element) => (activeData.value = element)} 219 | class="widget-list drawing-board" 220 | > 221 | {items} 222 | 223 | ) 224 | } 225 | } 226 | const renderInner = (field: FormField) => { 227 | return field.children?.map((el: FormField) => { 228 | return ( 229 | (el.value = val)} 234 | > 235 | {{ 236 | default: () => renderChildren(el) 237 | }} 238 | 239 | ) 240 | }) 241 | } 242 | return () => { 243 | const { field } = props 244 | return ( 245 |
246 | {renderItem({ 247 | field, 248 | index: 0, 249 | list: [], 250 | parent: field 251 | })} 252 |
253 | ) 254 | } 255 | } 256 | }) 257 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/index.vue: -------------------------------------------------------------------------------- 1 | 145 | 146 | 264 | 265 | 334 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/left-sidebar/AssemblyPanel.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 108 | 109 | 160 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/left-sidebar/CasePanel.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 120 | 121 | 151 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/left-sidebar/OutlinePanel.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 52 | 53 | 70 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/left-sidebar/SourcePanel.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 33 | 34 | 39 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/left-sidebar/case/case2.ts: -------------------------------------------------------------------------------- 1 | import type { FormField } from '@xfc/vue3-form-render' 2 | 3 | const case2: FormField = { 4 | id: 'formConf', 5 | name: 'Form', 6 | icon: 'ep:document', 7 | type: 'container', 8 | label: '表单', 9 | readonly: false, 10 | hidden: false, 11 | props: { 12 | scrollToError: true, 13 | labelPosition: 'top', 14 | labelWidth: 100, 15 | size: 'default', 16 | disabled: false 17 | }, 18 | on: { 19 | onMounted: '', 20 | onUnmounted: '', 21 | onValidate: '' 22 | }, 23 | children: [ 24 | { 25 | id: 'field_nmuut', 26 | name: 'Input', 27 | icon: 'ph:textbox', 28 | type: 'formItem', 29 | label: '组织名称', 30 | value: null, 31 | readonly: false, 32 | required: true, 33 | hidden: false, 34 | rules: [], 35 | props: { 36 | maxlength: 99, 37 | showWordLimit: true, 38 | clearable: true, 39 | placeholder: '请输入组织名称' 40 | }, 41 | on: { 42 | onVnodeMounted: '', 43 | onVnodeUpdated: '', 44 | onBlur: '', 45 | onFocus: '', 46 | onChange: '', 47 | onInput: '', 48 | onClear: '' 49 | } 50 | }, 51 | { 52 | id: 'field_zrr91', 53 | name: 'Number', 54 | icon: 'fluent:keyboard-123-20-regular', 55 | type: 'formItem', 56 | label: '排序', 57 | value: 99, 58 | readonly: false, 59 | required: true, 60 | hidden: false, 61 | props: { 62 | min: 0, 63 | max: 999, 64 | step: 1, 65 | placeholder: '排序', 66 | style: { 67 | width: '100%' 68 | } 69 | }, 70 | on: { 71 | onVnodeMounted: '', 72 | onVnodeUpdated: '', 73 | onBlur: '', 74 | onFocus: '', 75 | onChange: '' 76 | } 77 | }, 78 | { 79 | id: 'field_a1g8c', 80 | name: 'Select', 81 | icon: 'tabler:select', 82 | type: 'formItem', 83 | label: '上级组织', 84 | value: null, 85 | readonly: false, 86 | required: true, 87 | hidden: false, 88 | props: { 89 | filterable: true, 90 | multiple: false, 91 | placeholder: '请选择上级组织', 92 | dataType: 'static', 93 | options: [ 94 | { 95 | label: '杭州分公司', 96 | value: 1, 97 | type: 'number', 98 | disabled: false 99 | }, 100 | { 101 | label: '宁波分公司', 102 | value: 2, 103 | type: 'number', 104 | disabled: false 105 | }, 106 | { 107 | label: '金华分公司', 108 | value: 3, 109 | type: 'number', 110 | disabled: false 111 | } 112 | ] 113 | }, 114 | on: { 115 | onVnodeMounted: '', 116 | onVnodeUpdated: '', 117 | onChange: '', 118 | onVisibleChange: '', 119 | onRemoveTag: '', 120 | onClear: '', 121 | onBlur: '', 122 | onFocus: '' 123 | } 124 | }, 125 | { 126 | id: 'field_jdnz1', 127 | name: 'Select', 128 | icon: 'tabler:select', 129 | type: 'formItem', 130 | label: '负责人', 131 | value: null, 132 | readonly: false, 133 | required: false, 134 | hidden: false, 135 | props: { 136 | filterable: true, 137 | multiple: false, 138 | placeholder: '请选择负责人', 139 | dataType: 'static', 140 | options: [ 141 | { 142 | label: '张三', 143 | value: 1, 144 | type: 'number', 145 | disabled: false 146 | }, 147 | { 148 | label: '李四', 149 | value: 2, 150 | type: 'number', 151 | disabled: false 152 | }, 153 | { 154 | label: '王五', 155 | value: 3, 156 | type: 'number', 157 | disabled: false 158 | }, 159 | { 160 | label: '毛六', 161 | value: 4, 162 | type: 'number', 163 | disabled: false 164 | } 165 | ] 166 | }, 167 | on: { 168 | onVnodeMounted: '', 169 | onVnodeUpdated: '', 170 | onChange: '', 171 | onVisibleChange: '', 172 | onRemoveTag: '', 173 | onClear: '', 174 | onBlur: '', 175 | onFocus: '' 176 | } 177 | } 178 | ] 179 | } 180 | export default case2 181 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/left-sidebar/case/formula.ts: -------------------------------------------------------------------------------- 1 | import type { FormField } from '@xfc/vue3-form-render' 2 | 3 | const formula: FormField = { 4 | id: 'formConf', 5 | name: 'Form', 6 | icon: 'ep:document', 7 | type: 'container', 8 | label: '表单', 9 | readonly: false, 10 | hidden: false, 11 | props: { 12 | scrollToError: true, 13 | labelPosition: 'top', 14 | labelWidth: 100, 15 | size: 'default', 16 | disabled: false 17 | }, 18 | on: { 19 | onMounted: '', 20 | onUnmounted: '', 21 | onValidate: '' 22 | }, 23 | children: [ 24 | { 25 | id: 'field_lob18', 26 | name: 'Radio', 27 | icon: 'gg:radio-checked', 28 | type: 'formItem', 29 | label: '请假类型', 30 | value: 1, 31 | readonly: false, 32 | required: true, 33 | hidden: false, 34 | props: { 35 | dataType: 'static', 36 | options: [ 37 | { 38 | label: '病假', 39 | value: 1, 40 | type: 'number', 41 | disabled: false 42 | }, 43 | { 44 | label: '事假', 45 | value: 2, 46 | type: 'number', 47 | disabled: false 48 | }, 49 | { 50 | label: '年假', 51 | value: 3, 52 | type: 'number', 53 | disabled: false 54 | }, 55 | { 56 | label: '丧假', 57 | value: 4, 58 | type: 'number', 59 | disabled: false 60 | } 61 | ] 62 | }, 63 | on: { 64 | onVnodeMounted: '', 65 | onVnodeUpdated: '', 66 | onChange: 67 | "const {getField} = $inject\nconst yy = getField('field_h75fl')\nconst value = event[0]\nif(value == 2){\n yy.required = true\n}else{\n yy.required = false\n}" 68 | } 69 | }, 70 | { 71 | id: 'field_6720g', 72 | name: 'DateRange', 73 | icon: 'heroicons:calendar-date-range-20-solid', 74 | type: 'formItem', 75 | label: '请假日期', 76 | value: null, 77 | readonly: false, 78 | required: true, 79 | hidden: false, 80 | props: { 81 | type: 'datetimerange', 82 | startPlaceholder: '开始日期', 83 | endPlaceholder: '结束日期', 84 | valueFormat: 'YYYY-MM-DD HH:mm', 85 | format: 'YYYY-MM-DD HH:mm', 86 | key: 1735279561451 87 | }, 88 | on: { 89 | onVnodeMounted: '', 90 | onVnodeUpdated: '', 91 | onChange: 92 | 'const {getField,formData} = $inject\nconst value = event[0]\nconst timestamp = new Date(value[1]).getTime() - new Date(value[0]).getTime()\nformData.field_ykkfx = timestamp /1000/60/60/24', 93 | onBlur: '', 94 | onFocus: '', 95 | onClear: '', 96 | onCalendarChange: '', 97 | onPanelChange: '', 98 | onVisibleChange: '' 99 | } 100 | }, 101 | { 102 | id: 'field_ykkfx', 103 | name: 'Number', 104 | icon: 'fluent:keyboard-123-20-regular', 105 | type: 'formItem', 106 | label: '时长', 107 | value: 0, 108 | readonly: false, 109 | required: true, 110 | hidden: false, 111 | props: { 112 | min: 0, 113 | max: 999, 114 | step: 1, 115 | precision: 1, 116 | placeholder: '时长', 117 | style: { 118 | width: '100%' 119 | }, 120 | disabled: false 121 | }, 122 | on: { 123 | onVnodeMounted: '', 124 | onVnodeUpdated: '', 125 | onBlur: '', 126 | onFocus: '', 127 | onChange: '' 128 | } 129 | }, 130 | { 131 | id: 'field_h75fl', 132 | name: 'Textarea', 133 | icon: 'bi:textarea-resize', 134 | type: 'formItem', 135 | label: '请假原因', 136 | value: null, 137 | readonly: false, 138 | required: false, 139 | hidden: false, 140 | rules: [], 141 | props: { 142 | type: 'textarea', 143 | placeholder: '请输入请假原因', 144 | maxlength: 500, 145 | showWordLimit: true, 146 | autosize: { 147 | minRows: 3, 148 | maxRows: 3 149 | } 150 | }, 151 | on: { 152 | onVnodeMounted: '', 153 | onVnodeUpdated: '', 154 | onBlur: '', 155 | onFocus: '', 156 | onChange: '', 157 | onInput: '', 158 | onClear: '' 159 | } 160 | } 161 | ] 162 | } 163 | export default formula 164 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/left-sidebar/case/news.ts: -------------------------------------------------------------------------------- 1 | import type { FormField } from '@xfc/vue3-form-render' 2 | 3 | const news: FormField = { 4 | id: 'formConf', 5 | name: 'Form', 6 | icon: 'ep:document', 7 | type: 'container', 8 | label: '表单', 9 | readonly: false, 10 | hidden: false, 11 | props: { 12 | scrollToError: true, 13 | labelPosition: 'top', 14 | labelWidth: 100, 15 | size: 'default', 16 | disabled: false 17 | }, 18 | on: { 19 | onMounted: '', 20 | onUnmounted: '', 21 | onValidate: '' 22 | }, 23 | children: [ 24 | { 25 | id: 'field_i0ip7', 26 | name: 'Select', 27 | icon: 'tabler:select', 28 | type: 'formItem', 29 | label: '新闻', 30 | value: null, 31 | readonly: false, 32 | required: true, 33 | hidden: false, 34 | hideLabel: false, 35 | props: { 36 | filterable: true, 37 | multiple: false, 38 | placeholder: '请选择新闻', 39 | dataType: 'static', 40 | options: [ 41 | { 42 | label: '选项1', 43 | value: 1, 44 | type: 'number', 45 | disabled: false 46 | }, 47 | { 48 | label: '选项2', 49 | value: 2, 50 | type: 'number', 51 | disabled: false 52 | } 53 | ] 54 | }, 55 | on: { 56 | onVnodeMounted: 57 | "const http = $inject.http\nhttp.request({\n url: '/api/news/list'\n}).then(res=>{\n if(res.success){\n field.props.options = res.data.map(e=>{\n return {\n label: e.title,\n value: e.id\n }\n })\n }\n})", 58 | onVnodeUpdated: '', 59 | onChange: 60 | "const val = event[0]\nif(val){\n const http = $inject.http\n http.request({\n url: '/api/news/info',\n params: {\n id: val\n }\n }).then(res=>{\n if(res.success){\n const target = $inject.getField('field_g72it')\n target.props.description = res.data.content\n }\n })\n \n\n\n \n}", 61 | onVisibleChange: '', 62 | onRemoveTag: '', 63 | onClear: '', 64 | onBlur: '', 65 | onFocus: '' 66 | } 67 | }, 68 | { 69 | id: 'field_g72it', 70 | name: 'Alert', 71 | icon: 'mdi:alert-outline', 72 | type: 'container', 73 | label: '提示', 74 | value: null, 75 | hidden: false, 76 | props: { 77 | type: 'info', 78 | title: '新闻内容', 79 | description: '', 80 | effect: 'light', 81 | showIcon: true, 82 | closable: false, 83 | center: false 84 | }, 85 | on: { 86 | onVnodeMounted: '', 87 | onVnodeUpdated: '', 88 | onClose: '' 89 | } 90 | } 91 | ] 92 | } 93 | export default news 94 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/left-sidebar/case/submit.ts: -------------------------------------------------------------------------------- 1 | import type { FormField } from '@xfc/vue3-form-render' 2 | 3 | const submit: FormField = { 4 | id: 'formConf', 5 | name: 'Form', 6 | icon: 'ep:document', 7 | type: 'container', 8 | label: '表单', 9 | readonly: false, 10 | hidden: false, 11 | props: { 12 | scrollToError: true, 13 | labelPosition: 'top', 14 | labelWidth: 100, 15 | size: 'default', 16 | disabled: false 17 | }, 18 | on: { 19 | onMounted: '', 20 | onUnmounted: '', 21 | onValidate: '' 22 | }, 23 | children: [ 24 | { 25 | id: 'field_7yebg', 26 | name: 'Input', 27 | icon: 'ph:textbox', 28 | type: 'formItem', 29 | label: '名称', 30 | value: null, 31 | readonly: false, 32 | required: true, 33 | hidden: false, 34 | rules: [], 35 | props: { 36 | maxlength: 150, 37 | showWordLimit: true, 38 | clearable: true, 39 | placeholder: '请输入名称' 40 | }, 41 | on: { 42 | onVnodeMounted: '', 43 | onVnodeUpdated: '', 44 | onBlur: '', 45 | onFocus: '', 46 | onChange: '', 47 | onInput: '', 48 | onClear: '' 49 | } 50 | }, 51 | { 52 | id: 'field_liq1m', 53 | name: 'Radio', 54 | icon: 'gg:radio-checked', 55 | type: 'formItem', 56 | label: '状态', 57 | value: 1, 58 | readonly: false, 59 | required: true, 60 | hidden: false, 61 | props: { 62 | dataType: 'static', 63 | options: [ 64 | { 65 | label: '启用', 66 | value: 1, 67 | type: 'number', 68 | disabled: false 69 | }, 70 | { 71 | label: '禁用', 72 | value: 2, 73 | type: 'number', 74 | disabled: false 75 | } 76 | ] 77 | }, 78 | on: { 79 | onVnodeMounted: '', 80 | onVnodeUpdated: '', 81 | onChange: '' 82 | } 83 | }, 84 | { 85 | id: 'field_k5q4n', 86 | name: 'Textarea', 87 | icon: 'bi:textarea-resize', 88 | type: 'formItem', 89 | label: '备注', 90 | value: null, 91 | readonly: false, 92 | required: true, 93 | hidden: false, 94 | rules: [], 95 | props: { 96 | type: 'textarea', 97 | placeholder: '请输入备注', 98 | maxlength: 500, 99 | showWordLimit: true, 100 | autosize: { 101 | minRows: 3, 102 | maxRows: 3 103 | } 104 | }, 105 | on: { 106 | onVnodeMounted: '', 107 | onVnodeUpdated: '', 108 | onBlur: '', 109 | onFocus: '', 110 | onChange: '', 111 | onInput: '', 112 | onClear: '' 113 | } 114 | }, 115 | { 116 | id: 'field_jt0eq', 117 | name: 'Button', 118 | icon: 'mdi:button-pointer', 119 | type: 'container', 120 | label: '提交', 121 | value: null, 122 | readonly: false, 123 | hidden: false, 124 | props: { 125 | type: 'primary', 126 | plain: false, 127 | text: false, 128 | link: false, 129 | round: false, 130 | circle: false, 131 | disabled: false, 132 | loading: false 133 | }, 134 | on: { 135 | onVnodeMounted: '', 136 | onVnodeUpdated: '', 137 | onClick: 138 | "const formRef = $inject.rootRef\nconst formData = $inject.formData\nfield.props.loading = true\nformRef.value?.validate((valid)=>{\n if(valid){\n console.log(formData)\n setTimeout(() => {\n field.props.loading = false\n alert('提交成功')\n },500)\n }else{\n field.props.loading = false\n }\n})" 139 | } 140 | }, 141 | { 142 | id: 'field_957d9', 143 | name: 'Button', 144 | icon: 'mdi:button-pointer', 145 | type: 'container', 146 | label: '重置', 147 | value: null, 148 | readonly: false, 149 | hidden: false, 150 | props: { 151 | type: 'info', 152 | plain: false, 153 | text: false, 154 | link: false, 155 | round: false, 156 | circle: false, 157 | disabled: false 158 | }, 159 | on: { 160 | onVnodeMounted: '', 161 | onVnodeUpdated: '', 162 | onClick: 'const formRef = $inject.rootRef\nformRef.value?.resetFields()' 163 | } 164 | } 165 | ] 166 | } 167 | export default submit 168 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/left-sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 106 | 107 | 176 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/event/CodeEditorDialog.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 55 | 56 | 61 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/event/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | 30 | 42 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 21 | 22 | 27 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Alert.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Button.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Card.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Collapse.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 82 | 83 | 112 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Color.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 61 | 62 | 79 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/DataOptions.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 83 | 84 | 116 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Date.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/DateRange.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Divider.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Form.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Input.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Number.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Radio.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Rate.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Row.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 76 | 77 | 106 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Select.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Slider.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Style.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Switch.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Tab.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 100 | 101 | 130 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Textarea.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Time.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/TimeRange.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Transfer.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/Upload.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/right-panel/props/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 103 | 104 | 120 | -------------------------------------------------------------------------------- /packages/lowform-design/src/views/form-design/top-area/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 42 | -------------------------------------------------------------------------------- /packages/lowform-design/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 8 | 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": ["./src/*"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/lowform-design/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/lowform-design/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "noEmit": true, 13 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 14 | 15 | "module": "ESNext", 16 | "moduleResolution": "Bundler", 17 | "types": ["node"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/lowform-design/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, presetAttributify, presetUno, transformerDirectives, transformerVariantGroup } from 'unocss' 2 | 3 | export default defineConfig({ 4 | safelist: [ 5 | 'color-error', 6 | 'color-primary' 7 | ], 8 | theme: { 9 | colors: { 10 | primary: 'var(--el-color-primary)', 11 | primary_dark: 'var(--el-color-primary-light-5)', 12 | info: 'var(--el-color-info)', 13 | success: 'var(--el-color-success)', 14 | warning: 'var(--el-color-warning)', 15 | error: 'var(--el-color-error)', 16 | fill: 'var(--el-fill-color-light)', 17 | text: 'var(--el-text-color-primary)', 18 | card: 'var(--el-bg-color-overlay)', 19 | background: 'var(--el-bg-color)', 20 | borderColor: 'var(--el-border-color-lighter)' 21 | } 22 | }, 23 | presets: [ 24 | presetUno({ dark: 'class' }), 25 | presetAttributify() 26 | ], 27 | // 自定义样式规则 28 | rules: [], 29 | // 自定义组合样式 30 | shortcuts: { 31 | bgc: 'flex red', 32 | 'flex-col-center': 'flex-center flex-col', 33 | 'flex-col': 'flex! flex-col', 34 | 'flex-items-center': 'flex! items-center', 35 | 'flex-center': 'flex-items-center justify-center', 36 | 'flex-between': 'flex-items-center justify-between', 37 | 'flex-space': 'flex-items-center flex-justify-between', 38 | 'wh-full': 'w-full h-full' 39 | }, 40 | transformers: [ 41 | transformerDirectives(), 42 | transformerVariantGroup() 43 | ] 44 | }) 45 | -------------------------------------------------------------------------------- /packages/lowform-design/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueJsx from '@vitejs/plugin-vue-jsx' 6 | import vueDevTools from 'vite-plugin-vue-devtools' 7 | import vueSetupExtend from 'vite-plugin-vue-setup-extend' 8 | import Components from 'unplugin-vue-components/vite' 9 | import AutoImport from 'unplugin-auto-import/vite' 10 | import {viteMockServe} from "vite-plugin-mock"; 11 | import Unocss from 'unocss/vite' 12 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 13 | 14 | // https://vitejs.dev/config/ 15 | export default defineConfig({ 16 | base: '/lowform-pages', 17 | plugins: [ 18 | vue(), 19 | vueJsx(), 20 | vueDevTools(), 21 | vueSetupExtend(), 22 | viteMockServe({ 23 | mockPath: './src/mock', 24 | localEnabled: true, 25 | prodEnabled: true, 26 | injectCode: ` import { setupProdMockServer } from './mockProdServer'; setupProdMockServer(); `, 27 | }), 28 | Unocss({ 29 | configFile: './uno.config.ts' 30 | }), 31 | Components({ 32 | extensions: ['vue', 'tsx', 'md'], 33 | globs: ['src/components/*/*.vue', 'src/components/*/*.tsx'], 34 | include: [/\.vue$/, /\.vue\?vue/, /\.md$/, /\.[tj]sx?$/], 35 | resolvers: [ 36 | ElementPlusResolver({ 37 | importStyle: 'sass' 38 | }) 39 | ], 40 | dts: 'src/typings/components.d.ts' 41 | }), 42 | AutoImport({ 43 | imports: ['vue', 'vue-router'], 44 | resolvers: [ElementPlusResolver()], 45 | dts: 'src/typings/auto-imports.d.ts', 46 | eslintrc: { 47 | enabled: true, 48 | filepath: './.eslintrc-auto-import.json' 49 | } 50 | }), 51 | ], 52 | resolve: { 53 | alias: { 54 | '@': fileURLToPath(new URL('./src', import.meta.url)) 55 | } 56 | }, 57 | css: { 58 | preprocessorOptions: { 59 | scss: { api: 'modern-compiler' }, 60 | } 61 | }, 62 | build: { 63 | rollupOptions: { 64 | output: { 65 | chunkFileNames: 'assets/js/[name]-[hash].js', 66 | entryFileNames: 'assets/js/[name]-[hash].js', 67 | assetFileNames: 'assets/[ext]/[name]-[hash].[ext]', 68 | manualChunks: { 69 | vue: ['vue'], 70 | 'element-plus-icons': ['@element-plus/icons-vue'], 71 | 'vue-router': ['vue-router'], 72 | 'element-plus': ['element-plus'] 73 | }, 74 | sanitizeFileName(name) { 75 | const match = /^[a-z]:/i.exec(name) 76 | const driveLetter = match ? match[0] : '' 77 | return ( 78 | driveLetter + 79 | name 80 | .substring(driveLetter.length) 81 | // eslint-disable-next-line no-control-regex 82 | .replace(/[\x00-\x1F\x7F<>*#"{}|^[\]`;?:&=+$,]/g, '') 83 | ) 84 | } 85 | } 86 | } 87 | } 88 | }) 89 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'components/**' 3 | - 'packages/**' 4 | - 'examples/**' 5 | - 'shared/**' 6 | - 'docs' --------------------------------------------------------------------------------