项目初始化

This commit is contained in:
limengnan 2025-05-21 17:26:44 +08:00
commit 36fbdc5ed2
275 changed files with 63666 additions and 0 deletions

32
.cz-config.js Normal file
View File

@ -0,0 +1,32 @@
module.exports = {
types: [
{value: 'feat', name: 'feat: 新功能'},
{value: 'fix', name: 'fix: 修复'},
{value: 'docs', name: 'docs: 文档变更'},
{value: 'style', name: 'style: 代码格式(不影响代码运行的变动)'},
{value: 'cli', name: 'cli: 脚手架优化(不影响代码运行的变动)'},
{value: 'refactor', name: 'refactor: 重构(既不是增加feature也不是修复bug)'},
{value: 'perf', name: 'perf: 性能优化'},
{value: 'test', name: 'test: 增加测试'},
{value: 'chore', name: 'chore: 构建过程或辅助工具的变动'},
{value: 'revert', name: 'revert: 回退'},
{value: 'build', name: 'build: 打包'}
],
// override the messages, defaults are as follows
messages: {
type: '请选择提交类型:',
scope: '请输入文件修改范围(可选):',
// used if allowCustomScopes is true
customScope: '请输入修改范围(可选):',
subject: '请简要描述提交(必填):',
body: '请输入详细描述(可选,待优化去除,跳过即可):',
// breaking: 'List any BREAKING CHANGES (optional):\n',
footer: '请输入要关闭的issue(待优化去除,跳过即可):',
confirmCommit: '确认使用以上信息提交?(y/n/e/h)'
},
allowCustomScopes: true,
// allowBreakingChanges: ['feat', 'fix'],
skipQuestions: ['body', 'footer'],
// limit subject length, commitlint默认是72
subjectLimit: 72
}

6
.eslintignore Normal file
View File

@ -0,0 +1,6 @@
dist
node_modules
**/dist
**/node_modules
**/examples
**/*.d.ts

30
.eslintrc.js Normal file
View File

@ -0,0 +1,30 @@
module.exports = {
'env': {
'browser': true,
'node': true,
'es6': true,
'jest': true,
'commonjs': true
},
'rules': {
'indent': [
'error',
4
],
'quotes': [
'error',
'single'
],
'block-spacing': 'error',
'no-unused-vars': 'warn',
'no-irregular-whitespace': 'warn',
'no-useless-escape': 'warn',
'no-empty': 'warn',
'object-curly-spacing': 'error',
'no-console': 'warn',
'vue/valid-v-model': 'warn',
'vue/no-template-key': 'warn',
'vue/valid-v-for': 'warn',
'vue/require-v-for-key': 'warn',
}
};

67
.gitignore vendored Normal file
View File

@ -0,0 +1,67 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
stats.html
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
.idea
# custom
dist
/locale

187
LICENSE Normal file
View File

@ -0,0 +1,187 @@
FormCreate设计器商业版 软件使用协议
西安锦强未来科技有限公司版权所有 (c) 2024-至今
使用FormCreate设计器商业版必须遵守以下协议
1. 被授权者在软件程序使用过程中应遵守中国现行法律法规,我们不对被授权者的经营行为负任何法律责任。
2. 免责声明:任何情况下根据相关法律,我们不对被授权者因使用本软件产生的数据损坏或丢失、软硬件故障和违法犯罪等问题承担任何责任。
3. 本授权仅限于被授权主体(个人、企业或组织)使用,未经授权不得使用、修改或移除版权信息。
4. 授权者务必尊重知识产权,严格保证不恶意传播产品源码、不得直接对授权的产品本身进行二次转售或倒卖、不得对授权的产品进行简单包装后声称为自己的产品等。否则我们有权利收回产品授权,并根据事态轻重追究相应法律责任。
5. 我们有义务为被授权者提供有效期内的产品下载、更新和维护,一旦过期,授权者无法享有相应权限。终身授权则不受限制。
6. 禁止进行反编译、逆向工程、破解或篡改本软件的授权机制。
7. 本协议及本协议任何条款内容的最终解释权及修改权归西安锦强未来科技有限公司所有。
本软件使用到的开源软件协议
Vue
The MIT License (MIT)
Copyright (c) 2018-present, Yuxi (Evan) You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@form-create/element-ui
MIT License
Copyright (c) 2022 xaboy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Element-plus
MIT License
Copyright (c) 2020 Element Plus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
vuedraggable
The MIT License (MIT)
Copyright (c) 2016-2019 David Desmaisons
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
wangeditor
MIT License
Copyright (c) 2015-present wangeditor-team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
codemirror
MIT License
Copyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Highlight.js
BSD 3-Clause License
Copyright (c) 2006, Ivan Sagalaev.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

102
README.md Normal file
View File

@ -0,0 +1,102 @@
<p align="center">
<a href="https://www.form-create.com">
<img width="300" alt="FormCreate" src="https://static.form-create.com/file/img/info-logo2.png">
</a>
</p>
<p align="center">
<a href="https://www.form-create.com/" target="_blank">官网</a>
<span>&nbsp;|&nbsp;</span>
<a href="https://pro.form-create.com/doc/" target="_blank">帮助文档</a>
<span>&nbsp;|&nbsp;</span>
<a href="https://pro.form-create.com/view/" target="_blank">可视化表单设计器</a>
</p>
**FcDesigner Pro版是一款基于Vue的低代码可视化表单设计器工具通过数据驱动表单渲染。可以通过拖拽的方式快速创建表单提高开发者对表单的开发效率节省开发者的时间。目前在OA系统、ERP系统、电商系统、流程管理等系统中已稳定应用。**
-----
本项目采用 Vue3.0 和 ElementPlus 进行页面构建,内置多语言解决方案,支持二次扩展开发,支持自定义组件扩展。
## 特点
- 多语言配置
> 轻松在设计器中为表单各元素添加多语种文本,一键切换语言体系,打破语言隔阂,拓展全球业务版图,使您在应对全球化的过程中无忧无虑,轻松自如。
- 绑定事件
> 产品提供了可配置的组件和表单事件功能,为您处理各种动态交互提供了便利。无论用户需求何种复杂度与多样性,我们都能迎刃而解,提供满足使用者需求的解决方案。
- 丰富的组件
> 产品内置了50+种常用组件,广泛覆盖多种场景需求,以满足不同的用户需求。更为重要的是,我们支持灵活扩展自定义组件,以满足您独特、个人化的需求,为您提供更丰富的使用体验。
- 灵活的布局
> 产品提供了多种复杂表单布局方式,包括栅格、弹性盒子、表格等,这些功能让复杂的表单布局变得趋于简洁明了。此举不仅拓宽用户选择范围,更是为用户提供了贴心可靠的使用体验。
- 阅读模式
> 致力于实现表单编辑与数据查看模式的无缝切换,高效地提升代码复用性。这种改进将大大提高生产效率,同时也能让用户在任何情况下都能享受到流畅的使用体验。
- 公式计算
> 内置了52种常用的函数计算公式这不仅可以大幅度提高数据分析效率而且也能够灵活满足您在实际业务中的特定计算需求从而保证数据的准确有效性。
- 数据联动
> 提供更灵活的条件设置和组件值联动功能。用户可为组件设置条件,条件满足时触发联动显示,例如动态展示其他组件的值,实现组件间值的实时同步。
- 可视化
> 产品以可视化操作为主导,使您可以轻而易举地完成表单页面的编辑。通过直观的图形界面,无需深入繁琐复杂的代码便可完成操作,大大降低了使用门槛,让编辑工作变得更轻松、更高效。
- 行内布局
> 行内布局功能打破传统表单组件单一纵向堆叠模式,通过简洁直观的操作界面,允许开发者自由拖拽组件,使其能够在同一行内 “并肩齐驱”。
- 5 种主题色切换
> 预设5种主题色蓝色、绿色、橙色、紫色、粉色主题用户可以一键切换表单主题风格多样化的主题选择不仅美观、易用同时也可以满足不同场景和品牌风格需求。
## 编译文件
```
├─dist 编译文件目录
│ ├─index.[es|umd].js 完整包,包含PC端设计器+移动端自适应预览,需要安装vant和@form-create/vant
│ ├─pc/index.[es|umd].js PC端设计器,不包含移动端自适应预览,无需安装vant和@form-create/vant
│ ├─render/vant/form-create.[es|umd].js 移动端runtime环境的渲染器, 无需导入设计器
│ ├─render/element-plus/form-create.[es|umd].js PC端runtime环境的渲染器, 无需导入设计器
```
## 命令说明
运行开发环境
```
npm run dev
```
完整打包设计器
```
npm run build
```
打包多语言文件
```
npm run build:locale
```
打包PC端渲染器
```
npm run build:elm
```
打包移动端渲染器
```
npm run build:mobile
```
打包预览页面,输出 index.html
```
npm run build:preview
```
## 版权声明
FcDesigner Pro是由西安锦强未来科技有限公司的FormCreate团队负责的更新与维护若需在您的项目中应用需购买我们[授权](https://www.form-create.com/price.html)。
## 联系
![http://static.form-create.com/file/img/support.jpg](http://static.form-create.com/file/img/support.jpg)

4
babel.config.js Normal file
View File

@ -0,0 +1,4 @@
module.exports = {
'presets': [['@vue/cli-plugin-babel/preset', {'useBuiltIns': false}]],
'plugins': ['@vue/babel-plugin-jsx']
}

1134
examples/App.vue Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,187 @@
<script>
import {defineComponent} from 'vue'
export default defineComponent({
name: "AiTool",
emits: ['chat'],
data() {
return {
aiIndex: 0,
aiPlaceholders: ['追加一个用户信息表单', '生成一个商品表单,并且增加品牌信息', '当单选框选中"选项1"时显示输入框组件', '输入框必填且长度必须大于13否则提示错误信息', '修改输入框名称为商品名称'],
aiPlaceholder: '请告诉我您希望生成的表单内容,例如:追加一个用户信息表单',
showAi: true,
message: '',
aiLoading: false,
}
},
methods: {
toChat() {
this.$emit('chat');
},
},
mounted() {
this.key = setInterval(() => {
this.aiPlaceholder = '请告诉我您希望生成的表单内容,例如:' + this.aiPlaceholders[(++this.aiIndex) % this.aiPlaceholders.length];
}, 6000);
},
unmounted() {
clearInterval(this.key);
}
})
</script>
<template>
<div>
<div class="ai-tool" v-if="showAi">
<div class="ai-tool-con">
<el-input :placeholder="aiPlaceholder" readonly @click="toChat">
<template #suffix>
<i class="fc-icon icon-send" @click="toChat"
:class="{disabled:!message || !message.trim()}"></i>
</template>
</el-input>
<svg @click="showAi = false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path fill="currentColor"
d="M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"></path>
</svg>
</div>
</div>
<div class="ai-tool-btn" @click="showAi = true" v-else>
<svg class="fc-icon" viewBox="0 0 1331 1024" width="20" height="20">
<path
d="M653.067264 89.6a371.3536 371.3536 0 0 0-362.5984 291.328l-4.7616 21.6576-21.1456 6.7072A268.8 268.8 0 0 0 345.867264 934.4h614.4a268.8 268.8 0 0 0 81.3056-525.1072l-21.1456-6.656-4.7616-21.7088a371.3536 371.3536 0 0 0-362.5984-291.328zM220.632064 343.3984a448.1536 448.1536 0 0 1 864.8704 0A345.7024 345.7024 0 0 1 960.267264 1011.2h-614.4A345.6 345.6 0 0 1 220.632064 343.3984z"
fill="#FFFFFF"></path>
<path
d="M909.067264 805.1712v10.9568h-170.24v-10.9056h14.0288c6.9632 0 13.1072-0.512 18.3296-1.3312a24.8832 24.8832 0 0 0 13.568-7.4752 41.0624 41.0624 0 0 0 7.8848-17.92c2.048-8.192 3.072-19.0976 3.072-32.768V488.7552a231.936 231.936 0 0 0-0.8704-22.3232 61.952 61.952 0 0 0-2.2016-14.848 17.7664 17.7664 0 0 0-4.352-8.7552 33.1776 33.1776 0 0 0-8.2944-6.6048 56.9856 56.9856 0 0 0-27.136-6.9632h-14.0288V418.304h170.24v10.9568h-14.0288c-6.9632 0-13.2608 0.5632-18.7904 1.7408a23.552 23.552 0 0 0-13.1584 7.8848 41.1648 41.1648 0 0 0-8.2944 17.92 153.6512 153.6512 0 0 0-2.6112 31.9488v256.8704c0 9.3184 0.1536 16.896 0.4096 22.7328 0.3072 5.888 1.024 10.7008 2.2016 14.4896a24.832 24.832 0 0 0 4.8128 9.1648 52.8384 52.8384 0 0 0 35.4304 13.1072h14.0288z"
fill="#FFFFFF"></path>
<path
d="M726.027264 405.504H921.867264v36.608h-26.8288c-6.2464 0-11.6224 0.512-16.0768 1.4336a10.8032 10.8032 0 0 0-6.144 3.6864l-0.3072 0.3584a29.2352 29.2352 0 0 0-5.2736 12.1856 142.592 142.592 0 0 0-2.2528 29.0304v256.8704c0 9.216 0.1536 16.5888 0.4096 22.1184a47.9744 47.9744 0 0 0 1.6384 11.3152 14.2848 14.2848 0 0 0 1.792 3.9936 40.0896 40.0896 0 0 0 26.2144 9.3184H921.867264v36.5056h-195.84v-36.5568h26.8288c6.3488 0 11.6224-0.3584 15.9744-1.024a12.0832 12.0832 0 0 0 6.4-3.4304 29.7472 29.7472 0 0 0 4.9152-12.1856l0.1024-0.3584c1.6896-6.656 2.6624-16.4864 2.6624-29.696V488.8064c0-8.8064-0.256-15.8208-0.8192-21.0432l-0.0512-0.6656a50.3808 50.3808 0 0 0-1.6384-11.7248l-0.1024-0.4608-0.1024-0.4096c-0.4608-1.8944-1.024-2.6112-1.024-2.6112l-0.256-0.3072-0.256-0.3072a20.1728 20.1728 0 0 0-4.9152-3.7888 44.0832 44.0832 0 0 0-20.8896-5.376h-26.8288V405.504z m69.632 397.824c4.6592-6.0416 7.6288-13.6192 9.472-21.9136 2.3552-9.5744 3.3792-21.6064 3.3792-35.7376V488.8064c0-9.1136-0.256-16.896-0.9216-23.2448a75.1616 75.1616 0 0 0-2.56-17.3056 30.3616 30.3616 0 0 0-10.3936-17.1008h58.2656c-5.12 5.9904-8.3456 13.9776-10.496 22.5792l-0.1024 0.3584c-2.048 9.216-2.9184 20.8896-2.9184 34.7136v256.8704c0 9.4208 0.1536 17.2544 0.4608 23.3984 0.3072 6.4512 1.1264 12.3904 2.7136 17.5616 1.536 5.0688 3.8912 9.8304 7.424 13.824l0.512 0.6656 0.6656 0.5632 1.9968 1.6384h-57.4976z"
fill="#FFFFFF"></path>
<path
d="M564.696064 683.1104H410.584064l-27.136 62.5664c-6.4 15.4624-9.6256 26.9824-9.6256 34.56 0 3.2256 0.4608 6.144 1.3312 8.7552 1.1264 2.6112 3.328 5.12 6.5536 7.4752 3.4816 2.048 8.192 3.9424 13.9776 5.632 6.144 1.536 14.1824 2.56 24.064 3.072v10.9568H294.667264v-10.9056c9.9328-1.4848 17.92-3.6864 24.064-6.6048a48.64 48.64 0 0 0 15.7696-11.776 81.408 81.408 0 0 0 11.776-20.1728c3.84-8.192 8.3456-18.2272 13.6192-30.208L499.928064 409.6h10.0352l138.2912 330.3936c5.5296 13.7216 10.6496 24.7808 15.36 33.28 4.9152 8.192 9.8816 14.5408 14.848 19.2512 5.2224 4.608 10.752 7.8848 16.5888 9.6256 6.144 1.7408 13.4656 2.7648 21.9136 3.072v10.9056H559.883264v-10.9056c15.7184-0.6144 26.368-3.072 31.8976-7.4752a20.7872 20.7872 0 0 0 8.7552-16.64c-0.256-8.448-4.1984-22.1696-11.776-41.1136l-24.064-56.8832z m-8.3456-21.9136L488.971264 500.6848l-69.12 160.5632h136.4992z"
fill="#FFFFFF"></path>
<path
d="M491.480064 396.8h26.9824l141.6704 338.432c5.376 13.312 10.24 23.808 14.5408 31.5904 4.4544 7.3728 8.6528 12.6464 12.3904 16.2304 4.0448 3.5328 7.8848 5.632 11.6224 6.8096 4.864 1.3824 11.008 2.304 18.7392 2.56l12.288 0.4096v36.096H547.083264v-36.0448l12.288-0.512a94.208 94.208 0 0 0 17.408-1.9456 19.4048 19.4048 0 0 0 7.0656-2.7136c3.2256-2.56 3.84-4.608 3.8912-6.4-0.256-6.0416-3.3792-17.7664-10.8544-36.4544l-20.6848-48.9472H419.032064l-23.7568 54.784c-6.3488 15.2576-8.6016 24.7296-8.6016 29.5424 0 1.5872 0.1536 2.8672 0.4096 3.8912a9.6256 9.6256 0 0 0 1.6896 1.536c2.2528 1.28 5.632 2.6624 10.2912 4.096 5.12 1.1776 12.1856 2.0992 21.504 2.6624l12.032 0.7168v35.84H281.867264v-34.816l10.9568-1.6384c9.216-1.3312 15.872-3.2768 20.4288-5.4272a35.9424 35.9424 0 0 0 11.52-8.6016c3.328-4.096 6.656-9.5744 9.728-16.896l0.2048-0.3072c3.7376-8.0384 8.192-18.0224 13.4144-29.9008l143.36-334.592z m13.3632 33.7408l-68.3008 159.488L489.124864 467.968l86.528 206.1312h-0.9216l25.9072 61.184c7.5776 18.944 12.288 34.5088 12.6976 45.4656v0.4096a32.256 32.256 0 0 1-8.8064 22.2208h66.8672a83.3536 83.3536 0 0 1-1.4336-1.2288l-0.3072-0.256a104.704 104.704 0 0 1-17.0496-21.9648l-0.256-0.4608a327.1168 327.1168 0 0 1-15.9232-34.56L504.843264 430.592zM400.651264 673.9968l-5.2224 12.1344 5.2224-12.0832z m-31.744 73.8304c-4.096 9.2672-7.68 17.3056-10.8544 24.064a93.9008 93.9008 0 0 1-13.6704 23.1424l-0.256 0.256c-2.6112 2.9696-5.4784 5.632-8.6016 8.0384h34.6112a29.0304 29.0304 0 0 1-6.6048-9.1136L363.275264 793.6l-0.2048-0.6144a40.3456 40.3456 0 0 1-1.9456-12.8c0-8.704 2.9696-19.7632 7.7824-32.4096z m70.4-99.3792h97.8432l-48.2816-115.0976-49.5616 115.0976z"
fill="#FFFFFF"></path>
</svg>
</div>
</div>
</template>
<style>
.ai-tool {
position: fixed;
right: 0;
left: 0;
bottom: 120px;
display: flex;
align-items: center;
flex-direction: column;
z-index: 2021;
}
.ai-tool-con {
position: absolute;
width: 100%;
max-width: 600px;
}
.ai-tool-con::before {
content: "";
background: linear-gradient(90deg, #FF0000, #FF8400, #FF8400, #0066FF, #0066FF, #FF8400, #FF8400, #FF0000);
background-size: 400%;
border-radius: 24px;
bottom: -2px;
left: -2px;
position: absolute;
right: -2px;
top: -2px;
z-index: -1;
}
.ai-tool-con:has(.is-focus)::before {
animation: move-ab150fae 20s infinite ease;
filter: blur(5px);
}
.ai-tool-con > svg {
height: 14px;
width: 14px;
background: #ccc;
border-radius: 15px;
padding: 4px;
position: absolute;
right: -6px;
top: -10px;
cursor: pointer;
z-index: 2001;
}
.ai-tool .el-input {
width: 100%;
border-radius: 15px;
cursor: pointer;
}
.ai-tool input {
font-size: 13px;
}
.ai-tool .el-loading-spinner {
padding-top: 10px;
}
.ai-tool .el-input__wrapper {
width: 100%;
box-shadow: none;
border-radius: 24px;
height: 40px;
}
.ai-tool .el-input__wrapper.is-focus {
box-shadow: none;
}
.ai-tool .el-input .fc-icon {
display: flex;
align-items: center;
justify-content: center;
border-radius: 15px;
position: absolute;
right: 12px;
bottom: 12px;
cursor: pointer;
width: 20px;
height: 20px;
background: var(--fc-style-color-1);
color: #fff;
text-align: center;
font-size: 14px;
}
.ai-tool .el-input .fc-icon.disabled {
background: var(--fc-text-color-3);
}
.ai-tool .el-loading-mask {
border-radius: 24px;
}
.ai-tool-btn {
position: fixed;
right: 0px;
bottom: 100px;
color: #FFF;
background-color: var(--fc-style-color-1);
cursor: pointer;
padding: 4px;
border-radius: 5px 0px 0px 4px;
}
@keyframes move-ab150fae {
100% {
background-position: -400% 0;
}
}
</style>

View File

@ -0,0 +1,273 @@
<script>
import {defineComponent} from 'vue'
import makeRule from '../rule'
import StructEditor from "../../src/components/StructEditor.vue";
export default defineComponent({
name: "MakeDragRule",
components: {StructEditor},
data() {
return {
visible: false,
formData: {},
rule: makeRule(),
value: {},
preview: false,
uni: 0,
options: {
"form": {
"inline": false,
"hideRequiredAsterisk": true,
"labelPosition": "right",
"size": "default",
"labelWidth": "125px"
},
"resetBtn": {
"show": false,
},
"submitBtn": {
"show": false,
},
"wrap": {
"style": {
"marginBottom": "8px"
}
},
"ignoreHiddenFields": true,
}
}
},
watch: {
preview(val) {
if (val) {
this.value = this.makeValue();
}
}
},
methods: {
makeValue() {
const dragRule = {
menu: 'aide',
icon: 'icon-tag',
name: this.formData.name,
label: this.formData.label,
}
if (this.formData.validate) {
dragRule.validate = this.formData.validate;
}
if (this.formData.event) {
dragRule.event = this.formData.event.map(item => item.name);
}
const rule = {
type: this.formData.name,
}
if (this.formData.type !== '1') {
rule.title = this.formData.label;
rule.field = this.formData.name;
rule.$required = false;
dragRule.menu = 'main';
dragRule.icon = 'icon-input';
}
rule.props = {};
if (this.formData.options && this.formData.options.length) {
rule.options = this.formData.options;
}
dragRule.rule = (new Function('return function () { return' + JSON.stringify(rule) + '}'))();
const props = [];
(this.formData.props || []).forEach((item, index) => {
const prop = {
type: item.props_type,
field: item.field,
title: item.name
}
if (props.type === 'select') {
prop.options = props.options || []
}
props.push(prop);
})
dragRule.props = (new Function('return function () { return' + JSON.stringify(props) + '}'))();
return dragRule;
},
demo1() {
this.uni++;
this.rule = makeRule();
this.formData = {
"type": "2",
"name": "input",
"label": "输入框",
"validate": [
"string"
],
"event": [
{
"name": "change"
},
{
"name": "blur"
}
],
"props": [
{
"name": "类型",
"field": "type",
"props_type": "select",
"options": [
{
"label": "输入框",
"value": "text"
},
{
"label": "多行输入框",
"value": "textarea"
}
]
},
{
"name": "禁用",
"field": "disabled",
"props_type": "switch",
},
{
"name": "占位显示",
"field": "placeholder",
"props_type": "input",
}
]
}
},
demo2() {
this.uni++;
this.rule = makeRule();
this.formData = {
"type": "3",
"name": "checkbox",
"label": "多选框",
"options": [
{
"label": "选项1",
"value": "1"
},
{
"label": "选项2",
"value": "2"
},
{
"label": "选项3",
"value": "3"
}
],
"validate": [
"array"
],
"event": [
{
"name": "change"
}
],
"props": [
{
"field": "disabled",
"name": "是否禁用",
"props_type": "switch"
},
{
"field": "type",
"name": "按钮类型",
"props_type": "select",
"options": [
{
"label": "默认",
"value": "default"
},
{
"label": "按钮",
"value": "button"
}
]
}
]
}
},
demo3() {
this.uni++;
this.rule = makeRule();
this.formData = {
"type": "1",
"name": "button",
"label": "按钮",
"event": [
{
"name": "click"
}
],
"props": [
{
"field": "disabled",
"name": "是否禁用",
"props_type": "switch"
},
{
"field": "formCreateChild",
"name": "内容",
"props_type": "input",
}
]
}
}
}
})
</script>
<template>
<div class="_fd-make-drag-rule">
<span @click="visible = true">生成拖拽规则</span>
<el-dialog class="_fd-make-drag-pop" v-model="visible">
<template #header>
生成拖拽规则 <span style="font-size: 12px;color:var(--fc-text-color-2);">可以生成任意 Vue 组件或 UI 组件的拖拽规则</span>
</template>
<template v-if="!preview">
<formCreate :key="uni" :rule="rule" v-model="formData" :option="options"></formCreate>
</template>
<template v-else>
<StructEditor ref="editor" v-model="value"></StructEditor>
</template>
<template #footer>
<div>
<template v-if="!preview">
<el-button @click="demo1">示例1</el-button>
<el-button @click="demo2">示例2</el-button>
<el-button @click="demo3">示例3</el-button>
</template>
</div>
<el-button v-if="!preview" @click="preview = true" type="primary">下一步</el-button>
<el-button v-else @click="preview = false" type="primary">上一步</el-button>
</template>
</el-dialog>
</div>
</template>
<style>
._fd-make-drag-rule > span {
font-size: 14px;
color: var(--el-text-color-regular);
}
._fd-make-drag-pop .el-dialog__body {
height: 500px;
overflow: auto;
}
._fd-make-drag-pop .el-dialog__footer {
display: flex;
justify-content: space-between;
}
._fd-make-drag-pop .el-dialog__footer > div {
display: flex;
}
._fd-make-drag-pop ._fd-struct-editor, ._fd-make-drag-pop .CodeMirror {
height: 100%;
}
</style>

154
examples/forms.js Normal file

File diff suppressed because one or more lines are too long

18
examples/index.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Element Plus版本低代码设计器FcDesigner Pro在线演示 | FormCreate</title>
<link rel="icon" href="/logo.png">
<meta name="keywords" content="vue,FormCreate,form-create-designer,fc-designer,开源,低代码,低代码表单,表单设计器,element-plus,element-ui">
<meta name="description" content="FcDesigner Pro版是一款基于Vue3.0的低代码可视化表单设计器工具通过数据驱动表单渲染。可以通过拖拽的方式快速创建表单提高开发者对表单的开发效率节省开发者的时间。目前在OA系统、ERP系统、电商系统、流程管理等系统中已稳定应用。">
</head>
<body>
<div id="app">
</div>
<script
type="module"
src="./main.js"
></script>
</body>
</html>

54
examples/main.js Normal file
View File

@ -0,0 +1,54 @@
import {createApp} from 'vue';
import ELEMENT from 'element-plus';
import vant from 'vant';
import 'element-plus/dist/index.css';
import 'vant/lib/index.css';
import formCreate from '@form-create/element-ui';
import App from './App.vue';
import FcDesigner from '../src/index';
import 'element-plus/theme-chalk/dark/css-vars.css'
// import install from "@form-create/element-ui/auto-import";
// import 'element-plus/es/components/message/style/css';
const app = createApp(App);
// formCreate.use(install);
app.use(ELEMENT);
app.use(vant);
app.use(formCreate);
app.use(FcDesigner);
FcDesigner.setFormula([
{
menu: 'math',
name: 'test',
info: '扩展自定义计算函数示例',
example: 'test(val) == !!val',
handle(val) {
return !!val
}
}
])
FcDesigner.setBehavior([
{
menu: 'other',
name: 'test',
label: '扩展自定义行为',
info: '扩展自定义行为示例',
rule() {
return [
{
type: 'input',
field: 'custom',
title: '自定义配置'
}
]
},
handle(config) {
console.log(config)
}
}
])
app.mount('#app')

342
examples/rule.js Normal file
View File

@ -0,0 +1,342 @@
export default function makeRule() {
return [
{
"type": "radio",
"field": "type",
"title": "组件类型",
"effect": {
"fetch": ""
},
"$required": false,
"props": {
"type": "button"
},
"options": [
{
"label": "表单组件",
"value": "2"
},
{
"label": "选项类表单组件",
"value": "3"
},
{
"label": "辅助组件",
"value": "1"
}
],
"_fc_id": "id_F89qm5ulpnp9adc",
"name": "ref_Fsdxm5ulpnp9aec",
"_fc_drag_tag": "radio",
"display": true,
"hidden": false
},
{
"type": "input",
"field": "name",
"title": "组件名称",
"$required": false,
"_fc_id": "id_F1njm5ultg7fajc",
"name": "ref_Fgcnm5ultg7fakc",
"_fc_drag_tag": "input",
"display": true,
"hidden": false
},
{
"type": "input",
"field": "label",
"title": "组件别名",
"$required": false,
"_fc_id": "id_F1nxm5uluzpfamc",
"name": "ref_Ftxnm5uluzpfanc",
"_fc_drag_tag": "input",
"display": true,
"hidden": false
},
{
"type": "tableForm",
"field": "options",
"title": "选择项",
"props": {
"columns": [
{
"label": "名称",
"required": false,
"style": {
"width": "auto"
},
"rule": [
{
"type": "input",
"field": "label",
"title": "输入框",
"$required": false,
"_fc_id": "id_F0xkm5umkj3abyc",
"name": "ref_Fu3am5umbd2obsc",
"_fc_drag_tag": "input",
"display": true,
"hidden": false
}
]
},
{
"label": "值",
"required": false,
"style": {
"width": "auto"
},
"rule": [
{
"type": "input",
"field": "value",
"title": "值",
"$required": false,
"_fc_id": "id_Fg4xm5unpihhcec",
"name": "ref_Fhrsm5unpihhcgc",
"_fc_drag_tag": "input",
"display": true,
"hidden": false
}
]
}
]
},
"_fc_id": "id_F1n2m5umkj3abzc",
"name": "ref_Ff4dm5umkj3abxc",
"_fc_drag_tag": "tableForm",
"display": true,
"hidden": false,
"computed": {
"hidden": {
"mode": "AND",
"group": [
{
"field": "type",
"condition": "==",
"value": "3"
}
],
"invert": true
}
}
},
{
"type": "checkbox",
"field": "validate",
"title": "Value的数据类型",
"effect": {
"fetch": ""
},
"$required": false,
"props": {
"type": "button"
},
"options": [
{
"label": "字符串",
"value": "string"
},
{
"label": "数字",
"value": "number"
},
{
"label": "数组",
"value": "array"
}
],
"_fc_id": "id_Fs4dm5ulyrpmazc",
"name": "ref_F7q3m5ulyrpmb0c",
"_fc_drag_tag": "checkbox",
"display": true,
"hidden": false,
"computed": {
"hidden": {
"mode": "OR",
"group": [
{
"field": "type",
"condition": "!=",
"value": "1"
}
],
"invert": true
}
}
},
{
"type": "tableForm",
"field": "event",
"title": "事件",
"props": {
"columns": [
{
"label": "事件名称",
"required": false,
"style": {
"width": "auto"
},
"rule": [
{
"type": "input",
"field": "name",
"title": "事件",
"$required": false,
"_fc_id": "id_Ffl9m5ulxopqawc",
"name": "ref_Fn0om5ulxopqaxc",
"_fc_drag_tag": "input",
"display": true,
"hidden": false
}
]
}
]
},
"_fc_id": "id_Fwcmm5ulxatsasc",
"name": "ref_Fwzrm5ulxatsatc",
"_fc_drag_tag": "tableForm",
"display": true,
"hidden": false
},
{
"type": "group",
"field": "props",
"title": "配置项",
"$required": false,
"props": {
"expand": 1,
"rule": [
{
"type": "input",
"field": "field",
"title": "字段名",
"$required": false,
"_fc_id": "id_Fl1ym5um4ygwb6c",
"name": "ref_F28mm5um4ygwb7c",
"_fc_drag_tag": "input",
"display": true,
"hidden": false
},
{
"type": "input",
"field": "name",
"title": "配置名称",
"$required": false,
"_fc_id": "id_Fl1ym5um4ygwb6c",
"name": "ref_F28mm5um4ygwb7c",
"_fc_drag_tag": "input",
"display": true,
"hidden": false
},
{
"type": "radio",
"field": "props_type",
"title": "配置类型",
"effect": {
"fetch": ""
},
"$required": false,
"props": {
"type": "button"
},
"options": [
{
"label": "输入框",
"value": "input"
},
{
"label": "数字",
"value": "inputNumber"
},
{
"label": "布尔",
"value": "switch"
},
{
"label": "选项",
"value": "select"
}
],
"_fc_id": "id_Fl8zm5um9v76bkc",
"name": "ref_Fnaim5um9v76blc",
"_fc_drag_tag": "radio",
"display": true,
"hidden": false
},
{
"type": "tableForm",
"field": "options",
"title": "选择项",
"props": {
"columns": [
{
"label": "名称",
"required": false,
"style": {
"width": "auto"
},
"rule": [
{
"type": "input",
"field": "label",
"title": "名称",
"$required": false,
"_fc_id": "id_Fffzm5umbd2obrc",
"name": "ref_Fu3am5umbd2obsc",
"_fc_drag_tag": "input",
"display": true,
"hidden": false
}
]
},
{
"label": "值",
"required": false,
"style": {
"width": "auto"
},
"rule": [
{
"type": "input",
"field": "value",
"title": "值",
"$required": false,
"_fc_id": "id_Fbzcm5umprcqc2c",
"name": "ref_Fzc0m5umprcqc4c",
"_fc_drag_tag": "input",
"display": true,
"hidden": false
}
]
}
]
},
"_fc_id": "id_Fx1am5umaxcibnc",
"name": "ref_F5c0m5umaxciboc",
"_fc_drag_tag": "tableForm",
"display": true,
"hidden": false,
"computed": {
"hidden": {
"mode": "AND",
"group": [
{
"field": "props_type",
"condition": "==",
"value": "select"
}
],
"invert": true
}
}
}
]
},
"_fc_id": "id_Fg4ym5um923gbec",
"name": "ref_Fzo9m5um923gbfc",
"_fc_drag_tag": "group",
"display": true,
"hidden": false
}
];
}

14
gulpfile.js Normal file
View File

@ -0,0 +1,14 @@
const gulp = require('gulp');
const execa = require('execa');
const fs = require('fs');
gulp.task('default', async function (cb) {
await execa('node_modules/.bin/rimraf', ['locale']);
fs.readdirSync('src/locale').forEach(async function (file) {
const res = /^(.*)\.js$/.exec(file);
if (res) {
await execa('./node_modules/.bin/vite', ['build', '--config', './vite.config.locale.js', '-m', res[1]]);
}
cb();
});
});

24
index.html Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>Element Plus版本低代码设计器FcDesigner Pro在线演示 | FormCreate</title>
<link rel="icon" href="/logo.png">
<meta name="keywords" content="vue,FormCreate,form-create-designer,fc-designer,开源,移动端,低代码,低代码表单,表单设计器,element-plus,element-ui">
<meta name="description" content="FcDesigner Pro是一款基于Vue3.0的低代码可视化表单设计器工具通过数据驱动表单渲染。可以通过拖拽的方式快速创建表单提高开发者对表单的开发效率节省开发者的时间。目前在OA系统、ERP系统、电商系统、流程管理等系统中已稳定应用。">
</head>
<body>
<div id="app">
</div>
<script type="module" src="./examples/main.js"></script>
<script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?931b182e4333e09676463bcc8248f71e";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
</body>
</html>

133
package.json Normal file
View File

@ -0,0 +1,133 @@
{
"name": "fc-designer-pro",
"version": "5.7.0",
"description": "FormCreate设计器商业版",
"unpkg": "./dist/index.umd.js",
"jsdelivr": "./dist/index.umd.js",
"typings": "./types/index.d.ts",
"main": "./dist/index.umd.js",
"module": "./dist/index.es.js",
"exports": {
".": {
"import": "./dist/index.es.js",
"types": "./types/index.d.ts",
"require": "./dist/index.umd.js"
},
"./*": "./*"
},
"scripts": {
"clean": "rimraf dist/",
"dev": "vite --config vite.dev.config.js",
"rollup": "rollup -c ./rollup.config.ts",
"build": "vite build --config ./vite.config.build.js && cross-env ONLY_PC=true vite build --config ./vite.config.pc.js && vite build --config ./vite.config.mobile.js && vite build --config ./vite.config.elm.js",
"build:mobile": "vite build --config ./vite.config.mobile.js",
"build:elm": "vite build --config ./vite.config.elm.js",
"build:locale": "gulp -f gulpfile.js",
"build:preview": "vite build --config ./vite.config.preview.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/xaboy/form-create-designer.git"
},
"keywords": [
"表单设计器",
"@form-create",
"form-builder",
"form-designer",
"draggable",
"form",
"components",
"vue3",
"element-ui",
"json-form",
"dynamic-form"
],
"files": [
"README.md",
"package.json",
"LICENSE",
"types",
"dist",
"locale"
],
"license": "仅限于被授权主体(个人、企业或组织)使用,未经授权不得使用、修改或移除版权信息",
"author": "FormCreate Team",
"homepage": "http://form-create.com",
"publishConfig": {
"access": "public"
},
"private": true,
"devDependencies": {
"@element-plus/icons-vue": "^0.2.6",
"@sixian/css-url": "^1.0.3",
"@types/chalk": "^2.2.0",
"@types/shelljs": "^0.8.9",
"@vitejs/plugin-vue": "^3.1.2",
"@vitejs/plugin-vue-jsx": "^2.0.1",
"@vue/babel-plugin-jsx": "^1.0.7",
"@vue/cli-plugin-babel": "^4.5.13",
"@vue/cli-service": "^4.5.3",
"@vue/compiler-sfc": "^3.0.11",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.4.1",
"chalk": "^4.1.2",
"codemirror": "^6.65.7",
"commander": "^6.0.0",
"cross-env": "^7.0.2",
"css-loader": "^4.2.1",
"cssnano": "^5.1.13",
"cssnano-preset-advanced": "^5.3.8",
"element-plus": "^2.9.8",
"eslint": "^7.7.0",
"eslint-plugin-vue": "^7.2.2",
"esno": "^0.9.1",
"execa": "^5.1.1",
"fast-glob": "^3.2.7",
"figlet": "^1.5.0",
"fs-extra": "^10.0.0",
"gulp": "^4.0.2",
"html-webpack-plugin": "^4.3.0",
"humps": "^2.0.1",
"husky": "^4.2.5",
"javascript-obfuscator": "^4.1.0",
"jsonlint-mod": "^1.7.6",
"lint-staged": "^10.2.11",
"npm-run-all": "^4.1.5",
"ora": "^5.0.0",
"postcss": "^8.4.17",
"rimraf": "^3.0.2",
"rollup-plugin-visualizer": "^5.8.2",
"shelljs": "^0.8.4",
"stringify-author": "^0.1.3",
"tslib": "^2.3.1",
"typescript": "^4.4.3",
"unplugin-preprocessor-directives": "^1.0.3",
"vant": "^4",
"vite": "^3.2.11",
"vite-plugin-banner": "^0.5.0",
"vite-plugin-css-injected-by-js": "^2.1.0",
"vite-plugin-javascript-obfuscator": "^3.1.0",
"vue": "^3.1.5",
"vue-loader": "^15.9.3",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11"
},
"dependencies": {
"@form-create/component-elm-select": "^3.1",
"@form-create/component-elm-tree": "^3.1",
"@form-create/component-elm-upload": "^3.1",
"@form-create/component-wangeditor": "^3.1",
"@form-create/element-ui": "^3.2.8",
"@form-create/utils": "^3.1.23",
"@form-create/vant": "^3.2.8",
"axios": "^1.9.0",
"dayjs": "^1.11.13",
"js-beautify": "^1.15.1",
"jsbarcode": "^3.11.6",
"marked": "^15.0.9",
"qr-code-styling": "^1.9.1",
"signature_pad": "^5.0.4",
"snowflake-id": "^1.1.0",
"vuedraggable": "4.1.0"
}
}

15286
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.75 3C0.75 2.58579 1.08579 2.25 1.5 2.25H7.78647C8.07055 2.25 8.33025 2.4105 8.45729 2.66459L9 3.75H16.5C16.9142 3.75 17.25 4.08579 17.25 4.5V15C17.25 15.4142 16.9142 15.75 16.5 15.75H1.5C1.08579 15.75 0.75 15.4142 0.75 15V3Z" fill="#FFA53D"/>
<path d="M0.75 4.5C0.75 4.08579 1.08579 3.75 1.5 3.75H16.5C16.9142 3.75 17.25 4.08579 17.25 4.5V15C17.25 15.4142 16.9142 15.75 16.5 15.75H1.5C1.08579 15.75 0.75 15.4142 0.75 15V4.5Z" fill="#FFC60A"/>
</svg>

After

Width:  |  Height:  |  Size: 558 B

View File

@ -0,0 +1,11 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_181_24877)">
<path d="M0 4C0 1.79086 2.13127 0 4.76033 0H19.2397C21.8687 0 24 1.79086 24 4V20C24 22.2091 21.8687 24 19.2397 24H4.76033C2.13127 24 0 22.2091 0 20V4Z" fill="#16c0ff"/>
<path d="M4.66669 9.09589C4.66669 8.83447 4.81949 8.59716 5.05749 8.48898L11.7242 5.45868C11.8994 5.37901 12.1006 5.37901 12.2759 5.45868L18.9426 8.48898C19.1806 8.59716 19.3334 8.83447 19.3334 9.0959V15.5879C19.3334 15.8404 19.1907 16.0713 18.9648 16.1842L12.2982 19.5175C12.1105 19.6114 11.8896 19.6114 11.7019 19.5175L5.03521 16.1842C4.80936 16.0713 4.66669 15.8404 4.66669 15.5879V9.09589ZM16.8119 8.98512L12 6.7979L7.16215 8.99693L11.9733 11.0694L16.8119 8.98512ZM12.6667 12.2225V17.8426L18 15.1759V9.9251L12.6667 12.2225ZM6.00002 9.9481V15.1759L11.3334 17.8426V12.2455L6.00002 9.9481Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_181_24877">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

162
src/components/City.vue Normal file
View File

@ -0,0 +1,162 @@
<template>
<div class="_fc-city">
<el-select :disabled="disabled" :clearable="clearable" :modelValue="value.p"
@update:modelValue="changeProvince" @change="onInput">
<template v-for="item in province">
<el-option :label="item.n" :value="item.n"></el-option>
</template>
</el-select>
<el-select :disabled="disabled" :clearable="clearable" v-if="level > 1 && city.length" :modelValue="value.c"
@update:modelValue="changeCity" @change="onInput">
<template v-for="item in city">
<el-option :label="item.n" :value="item.n"></el-option>
</template>
</el-select>
<el-select :disabled="disabled" :clearable="clearable" v-if="level > 2 && area.length" v-model="value.a"
@change="onInput">
<template v-for="item in area">
<el-option :label="item.n" :value="item.n"></el-option>
</template>
</el-select>
</div>
</template>
<script>
import {defineComponent, markRaw} from 'vue';
export default defineComponent({
name: 'FcCity',
props: {
modelValue: Array,
clearable: Boolean,
disabled: Boolean,
filter: Function,
level: {
type: Number,
default: 3
},
api: String,
},
emits: ['update:modelValue', 'change'],
data() {
return {
value: {
p: '',
c: '',
a: ''
},
oldValue: '',
province: [],
}
},
watch: {
modelValue: {
handler(val) {
if (JSON.stringify(val) !== this.oldValue) {
this.updateValue();
}
},
deep: true
}
},
computed: {
city() {
if (this.value.p) {
for (let i = 0; i < this.province.length; i++) {
if (this.province[i].n === this.value.p) {
return this.province[i].d;
}
}
}
return [];
},
area() {
if (this.value.c) {
for (let i = 0; i < this.city.length; i++) {
if (this.city[i].n === this.value.c) {
return this.city[i]?.d || [];
}
}
}
return [];
}
},
methods: {
updateValue() {
const str = JSON.stringify(this.modelValue);
if (str !== JSON.stringify(this.oldValue)) {
this.value = {
p: this.modelValue?.[0] || '',
c: this.modelValue?.[1] || '',
a: this.modelValue?.[2] || '',
}
}
this.oldValue = str;
},
changeProvince(val) {
this.value.p = val;
this.value.c = '';
this.value.a = '';
},
changeCity(val) {
this.value.c = val;
this.value.a = '';
},
onInput() {
let value = [];
if (this.value.p) {
value = [this.value.p, this.value.c, this.value.a].filter(item => !!item);
if (this.level < 3 && value.length !== this.level) {
return;
}
if (this.level > 2 && (value.length < 2 || value.length === 2 && this.area.length)) {
return;
}
}
this.oldValue = JSON.stringify(value);
this.$emit('update:modelValue', value);
this.$emit('change', value);
},
loadData(uri) {
return fetch(uri).then((res) => {
return res.json();
}).then((res) => {
this.province = markRaw(this.filter ? this.filter(res) || [] : res);
});
},
},
created() {
if (this.api) {
this.loadData(this.api);
} else {
this.loadData('https://unpkg.com/@province-city-china/level/level.min.json').catch(() => {
this.loadData('https://cdn.jsdelivr.net/npm/@province-city-china/level/level.min.json').catch(() => {
this.loadData('https://npm.onmicrosoft.cn/@province-city-china/level/level.min.json');
});
})
}
},
mounted() {
this.updateValue();
}
});
</script>
<style>
._fc-city .el-select {
width: 150px;
}
.form-create-m ._fc-city {
width: 100%;
}
.form-create-m ._fc-city .el-select {
width: 100%;
}
.form-create ._fc-city .el-select + .el-select {
margin-left: 12px;
}
</style>

View File

@ -0,0 +1,53 @@
<script>
import {defineComponent, h} from 'vue';
import draggable from 'vuedraggable/src/vuedraggable';
export default defineComponent({
name: 'DragBox',
props: ['rule', 'tag', 'formCreateInject', 'list'],
render(ctx) {
const attrs = {...ctx.$props.rule.props, ...ctx.$attrs};
let _class = '_fd-' + ctx.$props.tag + '-drag _fd-drag-box';
if (!Object.keys(ctx.$slots).length) {
_class += ' drag-holder';
}
attrs.class = _class;
attrs.modelValue = ctx.$props.list || [...ctx.$props.formCreateInject.children];
const keys = {};
if (ctx.$slots.default) {
const children = ctx.$slots.default();
children.forEach(v => {
if (v.key) {
keys[v.key] = v;
}
})
}
return h(draggable, attrs, {
item: ({element, index}) => {
let inline = '';
if(element?._menu?.inline || element?._config?.inline){
inline = ' is-inline'
}
const key = element?.__fc__?.key;
if (key) {
let vnode = keys['_' + element.slot];
if (vnode) {
vnode.children.forEach(v => {
if (v.key === key + 'fc') {
vnode = v
}
});
} else {
vnode = keys[key + 'fc'];
}
if (vnode) {
return h('div', {class: '_fc-' + ctx.$props.tag + '-item _fd-drag-item' + inline, key}, vnode);
}
}
return h('div', {class: '_fc-' + ctx.$props.tag + '-item _fd-drag-item', key: index}, null);
}
});
}
});
</script>

280
src/components/DragTool.vue Normal file
View File

@ -0,0 +1,280 @@
<template>
<div class="_fd-drag-tool" @click.stop="active"
:class="{active: fcx.active === id, 'is-inside': inside, 'is-inline': inline}">
<div class="_fd-drag-mask" v-if="mask"></div>
<div class="_fd-drag-hidden" v-if="hidden">
<i class="fc-icon icon-eye-close"></i> {{ t('props.hide') }}
</div>
<div class="_fd-drag-l" v-if="!hiddenBtn" @click.stop>
<div class="_fd-drag-btn" v-if="dragBtn !== false" v-show="fcx.active === id" style="cursor: move;">
<i class="fc-icon icon-move"></i>
</div>
</div>
<div class="_fd-drag-r" v-if="btns !== false && !hiddenMenu">
<slot name="handle">
<div class="_fd-drag-btn" v-if="actions && actions.length > 0" @click.stop>
<el-dropdown trigger="click" @command="command">
<i class="fc-icon icon-setting"></i>
<template #dropdown>
<el-dropdown-menu>
<template v-for="(label, idx) in actions">
<el-dropdown-item :command="idx">
{{ t(label) || label }}
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="_fd-drag-btn" @click.stop v-if="isCreate && (btns === true || btns.indexOf('create') > -1)"
@click="$emit('create')">
<i class="fc-icon icon-add"></i>
</div>
<div class="_fd-drag-btn" @click.stop v-if="!only && (btns === true || btns.indexOf('copy') > -1)"
@click="$emit('copy')">
<i class="fc-icon icon-copy"></i>
</div>
<div class="_fd-drag-btn" @click.stop v-if="children && (btns === true || btns.indexOf('addChild') > -1)"
@click="$emit('addChild')">
<i class="fc-icon icon-add-child"></i>
</div>
<div class="_fd-drag-btn _fd-drag-danger" @click.stop v-if="btns === true || btns.indexOf('delete') > -1"
@click="$emit('delete')">
<i class="fc-icon icon-delete"></i>
</div>
</slot>
</div>
<slot name="default"></slot>
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'DragTool',
emits: ['create', 'copy', 'addChild', 'delete', 'active', 'action', 'fc.el'],
props: {
dragBtn: Boolean,
children: String,
inside: Boolean,
inline: Boolean,
hidden: Boolean,
mask: Boolean,
actions: Array,
handleBtn: [Boolean, Array],
formCreateInject: Object,
unique: String,
only: Boolean
},
inject: {
fcx: {
default: null
},
designer: {
default: null
},
dragTool: {
default: null
},
},
provide() {
return {
dragTool: this
}
},
computed: {
isCreate() {
return this.dragTool ? !!this.dragTool.children : false;
},
btns() {
if (Array.isArray(this.handleBtn)) {
return this.handleBtn.length ? this.handleBtn : false;
}
return this.handleBtn !== false;
},
id() {
return this.unique || this.formCreateInject.id;
},
hiddenMenu() {
return this.designer.setupState.hiddenDragMenu;
},
t() {
return this.designer.setupState.t;
},
hiddenBtn() {
return this.designer.setupState.hiddenDragBtn;
},
},
methods: {
command(idx) {
this.$emit('action', idx);
},
active() {
if (this.fcx.active === this.id) return;
this.fcx.active = this.id;
this.$emit('active');
}
},
mounted() {
this.$emit('fc.el', this);
},
});
</script>
<style>
._fd-drag-tool {
position: relative;
display: block;
min-height: 20px;
min-width: 0;
box-sizing: border-box;
padding: 2px;
outline: 1px dashed var(--fc-tool-border-color);
overflow: hidden;
word-wrap: break-word;
word-break: break-all;
z-index: 0;
}
._fd-drag-tool ._fd-drag-tool {
margin: 2px;
max-width: calc(100% - 4px);
max-height: calc(100% - 7px);
}
._fd-drag-tool.is-inline {
display: inline-block;
}
._fd-drag-tool.is-inside {
width: inherit;
height: inherit;
}
._fd-drag-tool:hover {
outline-color: var(--fc-style-color-1);
outline-style: solid;
z-index: 1;
}
._fd-drag-tool:has(._fd-drag-tool:hover) {
outline-style: dashed;
}
._fd-drag-tool:not(.active):hover > div > ._fd-drag-btn {
display: flex !important;
opacity: 0.7;
}
._fd-drag-tool:has(._fd-drag-tool:not(.active):hover, ._fd-drag-tool.active:hover) > div > ._fd-drag-btn {
display: none !important;
}
._fd-drag-tool:has(._fd-drag-tool) {
padding: 2px;
}
._fd-drag-tool + ._fd-drag-tool {
margin-top: 5px;
}
._fd-drag-tool.active {
outline: 2px solid var(--fc-style-color-1) !important;
z-index: 2;
min-width: 80px;
min-height: 36px;
}
._fd-drag-tool.active > div > ._fd-drag-btn {
display: flex;
}
._fd-drag-tool._fd-drop-hover ._fd-drag-box {
padding-top: 15px !important;
padding-bottom: 15px !important;
}
/*._fd-drag-tool._fd-drop-hover ._fd-drag-box ._fd-drag-item {
padding-top: 5px !important;
padding-bottom: 5px !important;
}*/
._fd-drag-tool._fd-drop-hover:hover {
outline: 1px dashed var(--fc-tool-border-color);
}
._fd-drag-tool ._fd-drag-btn {
display: none;
}
._fd-drag-r {
position: absolute;
right: 0;
top: calc(100% - 20px);
padding: 0 2px 2px 0;
z-index: 1904;
}
._fd-drag-l {
position: absolute;
top: 0;
left: 0;
z-index: 1904
}
._fd-drag-btn {
height: 18px;
width: 18px;
color: #fff;
background-color: var(--fc-style-color-1);
line-height: 20px;
padding-bottom: 1px;
float: left;
cursor: pointer;
align-items: center;
justify-content: center;
}
._fd-drag-btn .el-dropdown {
color: #fff;
}
._fd-drag-btn + ._fd-drag-btn {
margin-left: 2px;
}
._fd-drag-danger {
background-color: var(--fc-style-color-3);
}
._fd-drag-btn i {
font-size: 14px;
}
._fd-drag-mask, ._fd-drag-hidden {
z-index: 1900;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;;
}
._fd-drag-hidden {
display: flex;
align-items: center;
justify-content: center;
background: rgba(51, 51, 51, .7);
color: #FFFFFF;
font-size: 14px;
}
._fd-drag-tool:hover ._fd-drag-hidden, ._fd-drag-tool.active ._fd-drag-hidden, ._fd-drag-tool:has(._fd-drag-tool.active) ._fd-drag-hidden {
display: none;
}
._fd-drag-hidden .fc-icon {
margin-right: 5px;
}
</style>

View File

@ -0,0 +1,961 @@
<template>
<div class="_fd-event">
<el-badge :value="eventNum" type="warning" :hidden="eventNum < 1">
<el-button class="_fd-plain-button" plain size="small" @click="visible=true">{{
t('event.title')
}}
</el-button>
</el-badge>
<el-dialog class="_fd-event-dialog _fd-config-dialog" :title="t('event.title')" v-model="visible"
destroy-on-close
:close-on-click-modal="false"
append-to-body
width="1080px">
<el-container class="_fd-event-con" style="height: 600px">
<el-aside style="width:300px;">
<el-container class="_fd-event-l">
<el-header class="_fd-event-head" height="40px">
<el-dropdown popper-class="_fd-event-dropdown" trigger="click" size="default"
:placement="'bottom-start'">
<el-button link type="primary" size="default">
{{ t('event.create') }}<i class="fc-icon icon-down" style="font-size: 14px;"></i>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="name in eventName" :key="name" @click="add(name)"
:disabled="useEventKeys.indexOf(name) > -1">
<div class="_fd-event-item">
<span>{{ name }}</span>
<span class="_fd-label" v-if="eventInfo[name]">
{{ eventInfo[name] }}
</span>
</div>
</el-dropdown-item>
<template v-for="(hook, idx) in hookList">
<el-dropdown-item :divided="eventName.length > 0 && !idx"
@click="add(hook)"
:disabled="useEventKeys.indexOf(hook) > -1">
<div class="_fd-event-item">
<div> {{ hook }}</div>
<span class="_fd-label">
{{ eventInfo[hook] }}
</span>
</div>
</el-dropdown-item>
</template>
<el-dropdown-item :divided="eventName.length > 0 || hook" @click="cusEvent">
<div>{{ t('props.custom') }}</div>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-header>
<el-main>
<div class="_fd-menu">
<template v-for="(item, idx) in event" :key="item.id">
<div class="_fd-menu-item" :class="{'is-active': item.id === defActive }">
<div class="_fd-event-title"
@click.stop="edit(idx)">
<div class="_fd-event-method">
<span>function<span>{{
item.name
}}</span></span>
<span class="_fd-label"
v-if="eventInfo[item.name]">{{ eventInfo[item.name] }}</span>
</div>
<el-tooltip
effect="dark"
:content="t('behavior.add')"
placement="top"
:hide-after="0"
v-if="item.name !== 'hook_load'"
>
<i class="fc-icon icon-task-add"
@click.stop="addBehavior(idx)"></i>
</el-tooltip>
<i class="fc-icon icon-delete-circle"
@click.stop="rm(idx)"></i>
</div>
<div class="_fd-event-behaviors">
<fcDraggable :group="{name:'behavior', put:false}" :sort="true"
handle=".icon-drag" direction="vertical" :animation="0"
itemKey="_fc_id"
:list="item.behaviors">
<template #item="{element,index}">
<div class="_fd-event-behavior"
:class="{'is-active': element.id === defActive }"
@click.stop="editBehavior(idx,index)">
<div class="_fd-event-behavior-label">
<div>
<i class="fc-icon icon-drag"></i>
<span>{{
t('behavior.' + element.method + '.name')
}}</span>
</div>
<i class="fc-icon icon-delete-circle"
@click.stop="rmBehavior(idx, index)"></i>
</div>
<div class="_fd-event-behavior-info">
{{
t('behavior.' + element.method + '.info') || t('behavior.' + element.method + '.name')
}}
</div>
</div>
</template>
</fcDraggable>
</div>
</div>
</template>
<div class="_fd-menu-item" v-if="cus" style="padding-left: 10px;">
<div class="_fd-event-title">
<el-input type="text" v-model="cusValue" size="default"
@keydown.enter="addCus"
:placeholder="t('event.placeholder')">
</el-input>
<div>
<i class="fc-icon icon-add" @click.stop="addCus"></i>
<i class="fc-icon icon-delete" @click.stop="closeCus"></i>
</div>
</div>
</div>
</div>
</el-main>
</el-container>
</el-aside>
<el-main>
<el-container class="_fd-event-r">
<el-header class="_fd-event-head" height="40px" v-if="activeData || activeBehavior">
<el-button size="small" @click="close">{{ t('props.cancel') }}</el-button>
<el-button size="small" type="primary" @click="save">{{
t('props.save')
}}
</el-button>
</el-header>
<el-main v-if="activeData">
<el-tabs v-model="eventType" class="_fc-tabs" :key="activeData.key">
<el-tab-pane :label="t('props.custom')" name="fn" lazy>
<FnEditor ref="fn" v-model="eventStr" body :name="activeData.name"
:args="fnArgs"
style="height: 519px;"/>
</el-tab-pane>
<el-tab-pane :label="t('form.globalEvent')" name="event">
<div class="_fd-event-select">
<el-select v-model="eventKey" clearable filterable
style="width: 240px;margin-left: 15px;">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<span class="_fc-manage-text" @click="openConfig"><i
class="fc-icon icon-setting"/></span>
</div>
</el-tab-pane>
</el-tabs>
</el-main>
<el-main v-if="activeBehavior" class="is-behavior">
<el-aside width="220px" class="_fd-event-behavior-list">
<div class="_fd-event-behavior-title">
{{ t('behavior.props.execute') }}
</div>
<el-menu :defaultActive="activeBehavior.method" @select="handleSelect">
<template v-for="item in behaviorMenu">
<el-sub-menu :index="item.label">
<template #title>
<span>{{ t('props.' + item.label) }}</span>
</template>
<template v-for="data in item.children" :key="data.value">
<el-menu-item :index="data.value">{{
t('behavior.' + data.label + '.name')
}}
</el-menu-item>
</template>
</el-sub-menu>
</template>
</el-menu>
</el-aside>
<el-main class="_fd-event-behavior-con">
<div class="_fd-event-behavior-title">
{{ t('behavior.props.info') }}
<div>{{
t('behavior.' + activeBehavior.method + '.info') || t('behavior.' + activeBehavior.method + '.name')
}}
</div>
</div>
<div class="_fd-event-behavior-title" v-if="form.rule && form.rule.length">
{{ t('designer.rule') }}
</div>
<DragForm v-if="form.rule && form.rule.length"
:rule="form.rule" :option="form.options"
v-model="form.formData" v-model:api="form.api">
<template #title="scope">
<template v-if="scope.rule.warning">
<Warning :tooltip="scope.rule.warning">
{{ scope.rule.title }}
</Warning>
</template>
<template v-else>
{{ scope.rule.title }}
</template>
</template>
</DragForm>
<div class="_fd-event-behavior-title">
{{ t('designer.advanced') }}
</div>
<el-form size="small" labelWidth="auto">
<el-form-item :label="t('behavior.props.ignoreError')">
<el-radio-group v-model="activeBehavior.ignoreError">
<el-radio-button :value="true">{{
t('behavior.props.continue')
}}
</el-radio-button>
<el-radio-button :value="false">{{
t('behavior.props.stop')
}}
</el-radio-button>
</el-radio-group>
<div class="_fd-form-item-warning">{{ t('warning.behaviorIgnoreError') }}</div>
</el-form-item>
<el-form-item :label="t('behavior.props.expression')">
<ComputedConfig v-model="activeBehavior.expression"
:title="t('behavior.props.setFormula')"
:invertLabel="t('behavior.props.break')"
:validLabel="t('behavior.props.continue')"></ComputedConfig>
<div class="_fd-form-item-warning">{{ t('warning.behaviorExpression') }}</div>
</el-form-item>
<el-form-item :label="t('behavior.props.stopPropagation')">
<ComputedConfig v-model="activeBehavior.stopPropagation"
:title="t('behavior.props.setFormula')"
:invertLabel="t('behavior.props.continue')"
:validLabel="t('behavior.props.stop')"></ComputedConfig>
<div class="_fd-form-item-warning">{{ t('warning.behaviorStopPropagation') }}</div>
</el-form-item>
</el-form>
</el-main>
</el-main>
</el-container>
</el-main>
</el-container>
<template #footer>
<div>
<el-button size="default" @click="visible=false">{{ t('props.cancel') }}</el-button>
<el-button type="primary" size="default" @click="submit">{{
t('props.ok')
}}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import {deepCopy} from '@form-create/utils/lib/deepextend';
import is from '@form-create/utils/lib/type';
import {defineComponent} from 'vue';
import FnEditor from './FnEditor.vue';
import {getInjectArg} from '../utils';
import {behaviorRules, behaviorTree} from '../utils/behavior';
import ComputedConfig from './computed/ComputedConfig.vue';
import {designerForm} from '../utils/form';
import fcDraggable from 'vuedraggable/src/vuedraggable';
import Warning from './Warning.vue';
const $T = '$FNX:';
const isFNX = v => {
return is.String(v) && v.indexOf($T) === 0;
};
export default defineComponent({
name: 'EventConfig',
emits: ['update:modelValue'],
props: {
modelValue: [Object, undefined, null],
componentName: '',
hook: {
type: Boolean,
default: true,
},
eventName: {
type: Array,
default: () => []
}
},
inject: ['designer'],
components: {
Warning,
ComputedConfig,
FnEditor,
fcDraggable,
DragForm: designerForm.$form(),
},
data() {
return {
visible: false,
activeData: null,
activeBehavior: null,
val: null,
defActive: 'no',
hookList: ['hook_load', 'hook_mounted', 'hook_deleted', 'hook_watch', 'hook_value', 'hook_hidden'],
event: [],
cus: false,
cusValue: '',
eventType: 'fn',
eventKey: '',
eventStr: '',
eventNum: 0,
id: 0,
form: {
rule: [],
options: {
form: {
labelPosition: 'right',
size: 'small',
labelWidth: 'auto',
},
appendValue: false,
submitBtn: false,
},
api: {},
formData: {}
}
};
},
computed: {
useEventKeys() {
const events = {};
this.event.forEach(item => {
events[item.name] = true;
});
return Object.keys(events);
},
behaviorMenu() {
const tree = [];
behaviorTree.forEach(item => {
tree.push({
label: item.key,
children: item.children.map(k => {
return {
label: k,
value: k
}
})
})
});
return tree;
},
t() {
return this.designer.setupState.t;
},
activeRule() {
return this.designer.setupState.activeRule;
},
eventInfo() {
const info = {};
this.eventName.forEach(v => {
info[v] = this.t('com.' + this.componentName + '.event.' + v) || this.t('eventInfo.' + v) || '';
})
this.hookList.forEach(v => {
info[v] = this.t('eventInfo.' + v) || '';
})
return info;
},
globalEvent() {
return this.designer.setupState.formOptions.globalEvent || {};
},
options() {
return Object.keys(this.globalEvent).map(k => {
return {
label: this.globalEvent[k].label,
value: '$GLOBAL:' + k
}
})
},
fnArgs() {
return [getInjectArg(this.t)];
}
},
watch: {
visible(v) {
if (!v) {
this.destroy();
this.closeCus();
} else {
this.init();
}
},
},
methods: {
openConfig() {
this.designer.setupState.openGlobalEventDialog();
},
addCus() {
const val = this.cusValue && this.cusValue.trim();
if (val) {
this.closeCus();
this.add(val);
}
},
closeCus() {
this.cus = false;
this.cusValue = '';
},
cusEvent() {
this.cus = true;
},
loadFnStr(v) {
if (isFNX(v)) {
return v.replace($T, '');
} else if (is.Function(v)) {
const json = v.__json || '';
if (!json) {
return '' + v;
} else if (isFNX(json)) {
return json.replace($T, '');
} else {
return json;
}
} else if (v && v.indexOf('$GLOBAL:') === 0) {
return v;
}
},
parseBehavior(behavior) {
behavior.id = this.id++;
if (behavior.method === 'callback') {
const fn = this.loadFnStr(behavior.callback);
if (fn) {
behavior.callback = fn;
}
}
return behavior;
},
init() {
const behaviors = this.activeRule ? deepCopy(this.activeRule.$behavior || {}) : {};
const hooks = this.activeRule ? {...this.activeRule.hook || {}} : {};
const ons = {...deepCopy(this.modelValue || {})};
Object.keys(hooks).forEach(k => {
ons['hook_' + k] = hooks[k];
})
const event = [];
Object.keys(ons).forEach(k => {
const val = Array.isArray(ons[k]) ? ons[k] : [ons[k]];
val.forEach(v => {
const item = {
name: k,
id: this.id++,
};
const fn = this.loadFnStr(v);
if (fn) {
item.handle = fn;
}
item.behaviors = (behaviors[k] || []).map(this.parseBehavior);
delete behaviors[k];
event.push(item);
});
});
Object.keys(behaviors).forEach(k => {
event.push({
name: k,
id: this.id++,
handle: '',
behaviors: (behaviors[k] || []).map(this.parseBehavior)
});
});
this.event = event;
this.eventNum = event.length;
},
getValue() {
const on = {};
const behaviors = {};
const hooks = {};
let num = 0;
this.event.forEach(item => {
let flag = false;
if (item.handle) {
flag = true;
let list = on;
const handle = item.handle.indexOf('$GLOBAL:') !== 0 ? ($T + item.handle) : item.handle;
if (item.name.indexOf('hook_') > -1) {
hooks[item.name.replace('hook_', '')] = handle;
} else {
if (!list[item.name]) {
list[item.name] = [];
}
list[item.name].push(handle);
}
}
if (item.behaviors && item.behaviors.length) {
flag = true;
behaviors[item.name] = item.behaviors.map(behavior => {
delete behavior.id;
return behavior;
});
}
if (flag) {
num++;
}
});
Object.keys(on).forEach(k => {
on[k] = on[k].length === 1 ? on[k][0] : on[k];
});
return {on, behaviors, hooks, num};
},
add(name) {
this.event.push({
name,
id: this.id++,
behaviors: [],
})
if (!this.activeData) {
this.edit(this.event.length - 1);
}
},
edit(idx) {
if (this.defActive === this.event[idx].id) {
return;
}
this.destroy();
this.activeData = this.event[idx];
this.val = this.activeData.handle || '';
this.eventType = this.val.indexOf('$GLOBAL:') === 0 ? 'event' : 'fn';
if (this.eventType === 'event') {
this.eventKey = this.val;
this.eventStr = '';
} else {
this.eventStr = this.val;
this.eventKey = '';
}
this.defActive = this.activeData.id;
},
rm(idx) {
this.event.splice(idx, 1);
if ((this.activeData && this.defActive === this.activeData.id) || (this.activeBehavior && idx === this.activeBehavior.pid)) {
this.destroy();
}
},
save() {
return new Promise((resolve) => {
if (this.activeData) {
let str = this.eventKey;
if (this.eventType !== 'event') {
if (!this.$refs.fn.save()) {
return false;
}
str = this.eventStr;
}
this.activeData.handle = str;
this.destroy();
resolve();
} else if (this.activeBehavior) {
const update = (config) => {
this.activeBehavior.config = {...config || {}};
const behavior = {...this.activeBehavior};
const pid = behavior.pid;
if (!Object.keys(behavior.config).length) {
delete behavior.config;
}
delete behavior.pid;
this.event[pid].behaviors.forEach((item, idx) => {
if (item.id === behavior.id) {
this.event[pid].behaviors[idx] = behavior;
}
});
};
if (this.form.rule && this.form.rule.length) {
this.form.api.validate().then(() => {
update(this.form.formData);
this.destroy();
resolve();
}).catch(() => {
});
} else {
update();
this.destroy();
resolve();
}
} else {
resolve();
}
});
},
addBehavior(idx) {
this.event[idx].behaviors.push({
method: 'openModel',
id: this.id++,
ignoreError: false,
stopPropagation: '',
expression: '',
})
if (!this.activeData && !this.activeBehavior) {
this.editBehavior(idx, this.event[idx].behaviors.length - 1);
}
},
editBehavior(pid, idx) {
this.destroy();
this.activeBehavior = deepCopy(this.event[pid].behaviors[idx]);
this.activeBehavior.pid = pid;
this.defActive = this.activeBehavior.id;
this.updateBehaviorForm();
},
updateBehaviorForm() {
let rule = behaviorRules[this.activeBehavior.method];
if (is.Function(rule)) {
rule = rule(this.designer.setupState);
}
const loadT = (item) => {
if (item.field && !item.title) {
item.title = this.t('behavior.' + this.activeBehavior.method + '.props.' + item.field) || this.t('behavior.props.' + item.field) || this.t('props.' + item.field);
item.warning = this.t('behavior.' + this.activeBehavior.method + '.warning.' + item.field);
}
}
if (rule) {
this.form.rule = rule.map(item => {
loadT(item);
if (item.control) {
item.control.forEach(control => {
control.rule && control.rule.forEach(item => {
loadT(item);
});
});
}
return item;
});
this.$nextTick(() => {
this.form.api.setValue(this.activeBehavior.config || {});
});
} else {
this.clearBehaviorForm();
}
},
clearBehaviorForm() {
this.form.rule = [];
this.form.formData = {};
},
rmBehavior(pid, idx) {
this.event[pid].behaviors.splice(idx, 1);
if (this.activeBehavior && this.defActive === this.activeBehavior.id) {
this.destroy();
}
},
handleSelect(behavior) {
if (this.activeBehavior.method === behavior) {
return;
}
this.activeBehavior.method = behavior;
this.updateBehaviorForm();
},
destroy() {
this.activeBehavior = null;
this.activeData = null;
this.val = null;
this.defActive = null;
this.clearBehaviorForm();
},
close() {
this.destroy();
},
submit() {
this.save().then(() => {
const {on, behaviors, num, hooks} = this.getValue();
this.$emit('update:modelValue', on);
this.activeRule.$behavior = behaviors;
this.activeRule.hook = hooks;
this.visible = false;
this.eventNum = num;
});
},
},
beforeCreate() {
window.$inject = {
$f: {},
rule: [],
self: {},
option: {},
inject: {},
args: [],
};
},
created() {
this.init();
}
});
</script>
<style>
._fd-event .el-button {
font-weight: 400;
width: 100%;
}
._fd-event .el-badge {
width: 100%;
}
._fd-menu {
display: flex;
flex-direction: column;
box-sizing: border-box;
width: 100%;
}
._fd-menu-item {
padding: 0 15px;
border: 1px solid transparent;
border-bottom: 1px dashed var(--fc-line-color-3);
}
._fd-menu-item.is-active {
border: 1px solid var(--fc-style-color-1);
}
._fd-menu-item.is-active ._fd-event-title i {
color: var(--fc-style-color-1);
}
._fd-event-dialog .el-tabs__header {
margin: 0;
}
._fd-event-select {
display: flex;
align-items: center;
margin-left: 15px;
margin-top: 15px;
}
._fd-event-select .el-select {
width: 240px;
}
._fd-event-con .el-main {
padding: 0;
}
._fd-event-l, ._fd-event-r {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
border: 1px solid var(--fc-line-color-3);
}
._fd-event-dropdown .el-dropdown-menu {
max-height: 500px;
overflow: auto;
}
._fd-event-head {
display: flex;
padding: 5px 15px;
border-bottom: 1px solid var(--fc-line-color-3);
background: var(--fc-bg-color-3);
align-items: center;
}
._fd-event-head .el-button.is-link {
color: var(--fc-style-color-1);
}
._fd-event-r {
border-left: 0 none;
}
._fd-event-r ._fd-event-head {
justify-content: flex-end;
}
._fd-event-l > .el-main, ._fd-event-r > .el-main {
display: flex;
flex-direction: row;
flex: 1;
flex-basis: auto;
box-sizing: border-box;
min-width: 0;
width: 100%;
}
._fd-event-r > .el-main {
flex-direction: column;
}
._fd-event-r > .el-main.is-behavior {
flex-direction: unset;
}
._fd-event-item {
display: flex;
flex-direction: column;
justify-content: center;
max-width: 250px;
font-size: 14px;
overflow: hidden;
white-space: pre-wrap;
}
._fd-event-item ._fd-label {
font-size: 12px;
color: var(--fc-text-color-3);
}
._fd-event-l .el-menu-item.is-active ._fd-event-title i {
color: var(--fc-style-color-1);
}
._fd-event-method {
display: flex;
flex-direction: column;
justify-content: center;
width: 225px;
font-size: 14px;
font-family: monospace;
color: #9D238C;
overflow: hidden;
white-space: pre-wrap;
}
._fd-event-method ._fd-label {
margin-top: 4px;
color: var(--fc-text-color-3);
font-size: 12px;
}
._fd-event-method > span:first-child, ._fd-fn-list-method > span:first-child {
color: #9D238C;
}
._fd-event-method > span:first-child > span, ._fd-fn-list-method > span:first-child > span {
color: var(--fc-text-color-1);
margin-left: 10px;
}
._fd-event-title {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 12px 0;
cursor: pointer;
}
._fd-event-title .fc-icon {
margin-right: 6px;
font-size: 18px;
color: var(--fc-text-color-2);
}
._fd-event-title .el-input {
width: 200px;
}
._fd-event-title .el-input__wrapper {
box-shadow: none;
}
._fd-event-con .CodeMirror {
height: 100%;
width: 100%;
}
._fd-event-con .CodeMirror-wrap pre.CodeMirror-line {
padding-left: 20px;
}
._fd-event-behaviors {
width: 100%;
}
._fd-event-behavior {
width: 100%;
background: var(--fc-bg-color-3);
border-radius: 5px 5px 5px 5px;
padding: 12px;
box-sizing: border-box;
margin-bottom: 12px;
cursor: pointer;
font-size: 12px;
}
._fd-event-behavior.is-active {
background: var(--fc-style-color-1);
}
._fd-event-behavior.is-active ._fd-event-behavior-info, ._fd-event-behavior.is-active ._fd-event-behavior-label {
color: #FFFFFF;
}
._fd-event-behavior-label {
display: flex;
justify-content: space-between;
color: var(--fc-text-color-2);
}
._fd-event-behavior-label > div {
display: flex;
}
._fd-event-behavior-info {
color: var(--fc-text-color-3);
margin-left: 16px;
}
._fd-event-behavior-list {
height: 100%;
padding: 15px;
border-right: 1px solid var(--fc-line-color-3);
}
._fd-event-behavior-list .el-sub-menu__title, ._fd-event-behavior-list .el-menu-item {
height: 30px;
}
._fd-event-behavior-list .el-menu {
border-right: 0 none;
}
._fd-event-behavior-list .el-menu-item.is-active {
background: var(--fc-style-color-1);
color: #FFFFFF;
}
._fd-event-behavior-list .el-menu-item, ._fd-event-behavior-list .el-sub-menu__title {
border-radius: 6px !important;
margin-bottom: 4px;
}
._fd-event-con ._fd-event-behavior-con {
padding: 15px;
}
._fd-event-con .form-create .form-create .el-form-item {
margin-bottom: 18px;
}
._fd-event-con .el-form ._fd-form-item-warning {
font-weight: 400;
font-size: 12px;
color: var(--fc-text-color-3);
line-height: 17px;
margin-top: 6px;
}
._fd-event-behavior-title {
font-size: 13px;
font-weight: 500;
color: var(--fc-text-color-1);
margin-bottom: 12px;
}
._fd-event-behavior-title > div {
font-size: 12px;
font-weight: initial;
color: var(--fc-text-color-3);
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,302 @@
<template>
<div class="_fd-fetch-config">
<el-badge type="warning" is-dot :hidden="!configured">
<el-button class="_fd-plain-button" plain @click="visible=true" size="small">{{ t('struct.title') }}</el-button>
</el-badge>
<el-dialog class="_fd-fetch-dialog _fd-config-dialog" v-model="visible" destroy-on-close
:close-on-click-modal="false"
append-to-body
width="980px">
<template #header>
{{ t('fetch.optionsType.fetch') }}
<Warning :tooltip="t('warning.fetch')"></Warning>
</template>
<el-container class="_fd-fetch-con" style="height: 450px;">
<el-tabs model-value="first" class="_fc-tabs" style="width: 100%">
<el-tab-pane :label="t('fetch.config')" name="first" style="padding-right: 15px;">
<div class="_fd-fetch-info">
{{ t('fetch.info') }}
</div>
<DragForm v-model:api="form.api" v-model="form.formData" :rule="form.rule"
:option="form.options">
<template #title="scope">
<template v-if="scope.rule.warning">
<Warning :tooltip="scope.rule.warning">
{{ scope.rule.title }}
</Warning>
</template>
<template v-else>
{{scope.rule.title}}
</template>
</template>
</DragForm>
</el-tab-pane>
<el-tab-pane lazy :label="t('fetch.beforeFetch')" name="second">
<template #label>
{{ t('fetch.beforeFetch') }}
<Warning :tooltip="t('warning.beforeFetch')"></Warning>
</template>
<FnEditor style="height: 100%;" v-model="form.beforeFetch" name="beforeFetch"
:args="['config', 'data']"
ref="beforeFetch"></FnEditor>
</el-tab-pane>
<el-tab-pane lazy name="third">
<template #label>
{{ t('fetch.parse') }}
<Warning :tooltip="t('warning.fetchParse')"></Warning>
</template>
<FnEditor style="height: 100%;" v-model="form.parse" name="parse"
:args="[{name:'res', info: t('fetch.response')}, 'rule', 'api']"
ref="parse"></FnEditor>
</el-tab-pane>
<el-tab-pane lazy :label="t('fetch.onError')" name="fourth">
<FnEditor style="height: 100%;" v-model="form.onError" name="onError"
:args="['e']"
ref="error"></FnEditor>
</el-tab-pane>
</el-tabs>
</el-container>
<template #footer>
<div>
<el-button size="default" @click="visible=false">{{ t('props.cancel') }}</el-button>
<el-button type="primary" size="default" @click="save">{{
t('props.ok')
}}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import {deepCopy} from '@form-create/utils/lib/deepextend';
import FnEditor from './FnEditor.vue';
import StructEditor from './StructEditor.vue';
import {defineComponent} from 'vue';
import {designerForm} from '../utils/form';
import errorMessage from '../utils/message';
import is from '@form-create/utils/lib/type';
import Warning from './Warning.vue';
const makeRule = (t) => {
return [
{
type: 'input',
field: 'action',
title: t('fetch.action'),
value: '',
validate: [{required: true, message: t('fetch.actionRequired'), trigger: 'blur'}],
inject: true,
on: {
blur({self}, e) {
self._start = e.target.selectionStart;
}
},
children: [
{
type: 'VariableConfig',
slot: 'suffix',
props: {
popover: true,
},
inject: true,
on: {
confirm({api}, val) {
const rule = api.getRule('action');
rule.value = rule.value.substring(0, rule._start) + val + rule.value.substring(rule._start);
},
}
}
]
},
{
type: 'radio',
field: 'method',
title: t('fetch.method'),
value: 'GET',
options: [
{label: 'GET', value: 'GET'},
{label: 'POST', value: 'POST'},
],
$required: true,
},
{
type: 'radio',
field: 'dataType',
title: t('fetch.dataType'),
warning: t('warning.fetchDataType'),
value: 'json',
options: [
{label: 'JSON', value: 'json'},
{label: 'FormData', value: 'formData'},
],
$required: true,
},
{
type: 'FetchTable',
field: 'headers',
title: t('fetch.headers'),
value: {},
},
{
type: 'FetchTable',
field: 'query',
title: t('fetch.query'),
warning: t('warning.fetchQuery'),
value: {},
},
{
type: 'FetchTable',
field: 'data',
title: t('fetch.data'),
warning: t('warning.fetchData'),
value: {},
}];
}
export default defineComponent({
name: 'FetchConfig',
emits: ['update:modelValue'],
props: {
modelValue: [Object, String],
to: String,
},
components: {
Warning,
DragForm: designerForm.$form(),
FnEditor,
StructEditor
},
inject: ['designer'],
data() {
return {
visible: false,
value: deepCopy(this.modelValue || {}),
form: {
api: {},
formData: {},
rule: [],
options: {
form: {
labelWidth: '90px',
size: 'default'
},
submitBtn: false,
resetBtn: false,
}
}
};
},
computed: {
t() {
return this.designer.setupState.t;
},
configured() {
return !is.empty(this.modelValue);
},
},
watch: {
visible(v) {
if (v) {
this.value = deepCopy(this.modelValue || {});
this.active();
}
},
},
methods: {
open() {
this.visible = true;
},
active() {
const formData = this.value;
this.form.rule = formData.type === 'static' ? [] : makeRule(this.t);
this.form.formData = {...formData};
this.form.label = formData.label;
this.form.type = formData.type;
this.form.data = formData.data;
this.form.dataType = formData.dataType;
this.form.parse = formData.parse || '';
this.form.beforeFetch = formData.beforeFetch || '';
this.form.onError = formData.onError || '';
},
save() {
this.form.api.validate().then(() => {
const formData = {...this.form.formData};
if ((this.$refs.parse && !this.$refs.parse.save()) || (this.$refs.beforeFetch && !this.$refs.beforeFetch.save()) || (this.$refs.error && !this.$refs.error.save())) {
return;
}
formData.parse = designerForm.parseFn(this.form.parse);
formData.beforeFetch = designerForm.parseFn(this.form.beforeFetch);
formData.onError = this.form.onError;
formData.label = this.form.label;
formData.type = this.form.type;
formData.to = this.to || 'options';
this.$emit('update:modelValue', formData);
this.visible = false;
}).catch(err => {
console.error(err);
errorMessage(err[Object.keys(err)[0]][0].message);
});
},
},
created() {
this.active();
}
});
</script>
<style>
._fd-fetch-config, ._fd-fetch-config .el-badge {
width: 100%;
}
._fd-fetch-config .el-button {
font-weight: 400;
width: 100%;
}
._fd-fetch-dialog .el-tabs__header {
margin-bottom: 0;
}
._fd-fetch-dialog .form-create {
margin-top: 15px;
}
._fd-fetch-dialog ._fc-tabs {
display: flex;
}
._fd-fetch-dialog ._fc-tabs .el-tabs__content {
display: flex;
flex: 1;
overflow: auto;
}
._fd-fetch-info {
display: flex;
font-size: 12px;
position: relative;
margin-top: 8px;
margin-left: 15px;
padding: 8px 13px;
line-height: 18px;
background: rgba(170, 170, 170, 0.1);
border-radius: 6px;
color: var(--fc-text-color-2);
}
._fd-fetch-con .el-main {
padding: 0;
}
._fd-fetch-con .CodeMirror {
height: 100%;
width: 100%;
}
._fd-fetch-con .CodeMirror-wrap pre.CodeMirror-line {
padding-left: 20px;
}
</style>

View File

@ -0,0 +1,169 @@
<template>
<div class="_fd-fetch-table">
<el-container class="_fd-fetch-table-con" v-if="value.length > 0">
<el-header>
<div style="width: 40%">{{ t('props.key') }}</div>
<div>{{ t('props.value') }}</div>
</el-header>
<el-main>
<template v-for="(item, idx) in value" :key="idx">
<div class="_fd-fetch-table-row">
<div class="_fd-fetch-table-key">
<el-input v-model="item.key" @blur="(e)=>onBlur(item,e)">
<template #suffix>
<VariableConfig popover
@confirm="(val) => onConfirm(item, 'key', val)"></VariableConfig>
</template>
</el-input>
</div>
<el-input v-model="item.value" @blur="(e)=>onBlur(item,e)">
<template #suffix>
<VariableConfig popover
@confirm="(val) => onConfirm(item, 'value', val)"></VariableConfig>
</template>
</el-input>
<i class="fc-icon icon-delete-circle" @click="rm(idx)"></i>
</div>
</template>
</el-main>
</el-container>
<el-button link type="primary" @click="add">
<i class="fc-icon icon-add"></i> {{ t('tableOptions.add') }}
</el-button>
</div>
</template>
<script>
import {defineComponent} from 'vue';
import VariableConfig from './computed/VariableConfig.vue';
export default defineComponent({
name: 'FetchTable',
components: {VariableConfig},
inject: ['designer'],
emits: ['update:modelValue'],
props: {
modelValue: Object,
},
computed: {
t() {
return this.designer.setupState.t;
},
},
data() {
return {
value: [],
active: null,
start: null,
}
},
methods: {
onConfirm(item, key, val) {
if (item === this.active) {
item[key] = (item[key] || '').substring(0, this.start) + val + (item[key] || '').substring(this.start);
} else {
item[key] += val;
this.active = null;
this.start = null;
}
this.submit();
},
onBlur(item, e) {
this.active = item;
this.start = e.target.selectionStart;
this.submit();
},
submit() {
const value = {};
this.value.forEach(item => {
if (item.key && item.value) {
value[item.key] = item.value;
}
});
this.$emit('update:modelValue', value);
},
add() {
this.value.push({});
},
rm(idx) {
this.value.splice(idx, 1);
this.submit();
}
},
created() {
const value = [];
Object.keys(this.modelValue || {}).forEach(k => {
value.push({
key: k,
value: this.modelValue[k]
})
});
this.value = value;
}
});
</script>
<style>
._fd-fetch-table {
width: 100%;
}
._fd-fetch-table .el-button > span {
font-size: 12px;
font-weight: 400;
}
._fd-fetch-table-con {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
border: 1px solid var(--fc-line-color-3);
border-bottom: 0 none;
}
._fd-fetch-table-con .el-header {
display: flex;
align-items: center;
height: 30px;
padding-left: 12px;
background: var(--fc-bg-color-3);
color: var(--fc-text-color-1);
}
._fd-fetch-table-row {
display: flex;
align-items: center;
min-height: 34px;
padding: 0 10px 4px;
border-bottom: 1px solid var(--fc-line-color-3);;
}
._fd-fetch-table-row > .fc-icon {
width: 24px;
height: 24px;
margin-left: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 4px;
font-size: 18px;
color: var(--fc-text-color-2);
cursor: pointer;
}
._fd-fetch-table-row .el-input {
display: flex;
flex: 1;
margin-top: 4px;
font-size: 13px;
}
._fd-fetch-table-key {
margin-right: 15px;
width: calc(40% - 20px);
}
</style>

View File

@ -0,0 +1,310 @@
<template>
<div class="_fd-field-input">
<i class="fc-icon icon-group" @click.stop="copy"></i>
<el-input
v-if="!fieldList.length"
v-model="value"
:readonly="fieldReadonly || disabled"
:disabled="fieldReadonly || disabled"
@focus="onFocus"
@blur="onInput"
>
<template #append v-if="!fieldReadonly">
<i class="fc-icon icon-auto" @click="makeField"></i>
</template>
</el-input>
<el-tree-select
v-else
v-model="value"
:readonly="fieldReadonly || disabled"
:disabled="disabled"
:allow-create="!fieldReadonly"
:filterable="true"
:default-first-option="!fieldReadonly"
:indent="10"
:checkStrictly="isSubform && relationField !== true"
popper-class="_fd-field-popper"
@focus="onFocus"
@change="onInput"
@current-change="currentChange"
:data="fieldList"
/>
</div>
</template>
<script>
import {defineComponent, nextTick, onUnmounted} from 'vue';
import uniqueId from '@form-create/utils/lib/unique';
import {copyTextToClipboard, escapeRegExp} from '../utils';
import errorMessage from '../utils/message';
import is from '@form-create/utils/lib/type';
import {deepCopy} from '@form-create/utils/lib/deepextend';
export default defineComponent({
name: 'FieldInput',
inject: ['designer'],
emits: ['update:modelValue'],
props: {
modelValue: String,
disabled: Boolean,
},
computed: {
fieldList() {
if (this.key) {
return this.getFieldList();
}
},
subformFieldList() {
const fieldList = this.designer.setupState.fieldList || [];
const _fieldList = this.fieldLeafSelectable ? fieldList : this.removeLeafNodes(deepCopy(fieldList));
if (_fieldList.length) {
return _fieldList
}
return fieldList;
},
fieldReadonly() {
return this.designer.setupState.fieldReadonly;
},
isSubform() {
return this.activeRule && this.activeRule._menu.subForm;
},
activeRule() {
return this.designer.setupState.activeRule;
},
relationField() {
return this.designer.props.config.relationField;
},
fieldLeafSelectable() {
return this.designer.props.config.fieldLeafSelectable !== false;
},
t() {
return this.designer.setupState.t;
}
},
data() {
return {
value: this.modelValue || '',
oldValue: '',
key: 1,
activeNode: null,
}
},
watch: {
modelValue(n) {
this.value = n;
}
},
methods: {
getFieldList() {
let fieldList = this.designer.setupState.fieldList || [];
if (this.relationField === false) {
return fieldList;
}
if (this.isSubform) {
fieldList = this.subformFieldList
} else {
const rule = this.activeRule;
let ctx = rule && rule.__fc__ && rule.__fc__.parent;
while (ctx) {
if (ctx.rule._menu && ['array', 'object'].indexOf(ctx.rule._menu.subForm) > -1) {
const _fieldList = this.findChildrenById(fieldList, ctx.rule.field) || fieldList;
if (_fieldList.length) {
fieldList = _fieldList
}
break;
} else {
ctx = ctx.parent;
}
}
}
return fieldList;
},
removeLeafNodes(tree) {
if (!Array.isArray(tree) || tree.length === 0) {
return tree;
}
function recurse(nodes) {
return nodes.filter(node => {
if (node.children && node.children.length > 0) {
node.children = recurse(node.children);
return true;
}
return false;
});
}
return recurse(tree);
},
findChildrenById(tree, id) {
if (!Array.isArray(tree)) {
return null;
}
for (const node of tree) {
if (node.value === id) {
return node.children || [];
}
if (node.children) {
const result = this.findChildrenById(node.children, id);
if (result !== null) {
return result;
}
}
}
return null;
},
copy() {
copyTextToClipboard(this.modelValue);
},
getSubChildren() {
let subChildren = this.designer.setupState.getSubFormChildren(this.activeRule) || [];
subChildren = is.trueArray(subChildren) ? subChildren : this.designer.setupState.children;
return subChildren;
},
getSubFieldChildren() {
const subChildren = this.getSubChildren();
const list = [];
const getRule = (children) => {
children && children.forEach(rule => {
if (rule && rule._fc_drag_tag && rule.field) {
list.push({...rule, children: []});
} else if (rule && rule.children) {
getRule(rule.children);
}
});
return list;
}
return getRule(subChildren);
},
checkValue() {
const oldField = this.oldValue;
const temp = escapeRegExp(oldField);
let field = (this.value || '').replace(/[\s\ ]/g, '');
if (!field) {
errorMessage(this.t('computed.fieldEmpty'));
return oldField;
} else if (!/^[a-zA-Z]/.test(field)) {
errorMessage(this.t('computed.fieldChar'));
return oldField;
} else if (oldField !== field) {
const flag = field.indexOf('.') > -1;
if (flag) {
field = field.replaceAll('.', '_');
}
if (this.getSubFieldChildren().filter(v => v.field === field).length > 0) {
errorMessage(this.t('computed.fieldExist', {label: field}));
return oldField;
}
// else if (temp) {
// const regex = /"_computed"\s*:\s*(\{\s*(?:"[^"]*"\s*:\s*"(?:\\"|[^"])*"(?:,\s*)?)*\})/g;
// const subChildren = this.getSubChildren();
// const json = JSON.stringify(subChildren).replace(JSON.stringify(this.activeRule), '');
// let match;
// while ((match = regex.exec(json)) !== null) {
// const obj = JSON.parse(match[1]);
// let _exec = false;
// Object.keys(obj).forEach(k => {
// if (!_exec) {
// const fieldRag = new RegExp(`(${temp})(?![a-zA-Z0-9_$])`, 'g');
// _exec = !!obj[k].match(fieldRag);
// }
// });
// if (_exec) {
// errorMessage(this.t('computed.fieldUsed', {label: oldField}));
// return oldField;
// }
// }
// }
if (flag) {
return field;
}
}
this.oldValue = '';
return field;
},
onFocus() {
this.oldValue = this.value;
},
makeField() {
this.oldValue = this.value;
this.value = uniqueId();
this.onInput();
},
updateRule(node) {
const update = {...node.update || {}};
if (!update.title) {
update.title = node.label;
}
this.designer.setupState.mergeRule(this.activeRule, update);
this.designer.setupState.updateRuleFormData();
},
onInput() {
if (this.value !== this.modelValue) {
this.value = this.checkValue();
if (this.value !== this.modelValue) {
const node = this.activeNode;
this.activeNode = null;
this.oldValue = this.value;
this.$emit('update:modelValue', this.value);
if (node) {
this.updateRule(node);
}
}
}
},
currentChange(node) {
this.activeNode = node;
},
},
mounted() {
const updateKey = () => {
nextTick(() => {
++this.key;
});
}
this.designer.setupState.bus.$on('dragEnd', updateKey);
onUnmounted(() => {
this.designer.setupState.bus.$off('dragEnd', updateKey);
})
}
});
</script>
<style>
._fd-field-input {
width: 100%;
position: relative;
}
._fd-field-input > .fc-icon {
position: absolute;
right: 28px;
top: 1px;
z-index: 3;
color: #a8abb2;
cursor: pointer;
width: 24px;
height: 24px;
text-align: center;
}
._fd-field-input .el-input-group__append {
width: 25px;
padding: 0;
margin: 0;
color: var(--fc-text-color-3);
cursor: pointer;
}
._fd-field-popper .el-tree-node__content {
padding: 2px 0;
color: #333;
}
._fd-field-popper .el-select-dropdown__list > .el-select-dropdown__item {
padding-left: 15px;
border-bottom: 1px solid var(--fc-line-color-3);
box-sizing: border-box;
height: 26px;
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<el-tree
ref="treeRef"
class="_fc-field-tree"
:data="field"
default-expand-all
:expand-on-click-node="false"
:indent="10"
@nodeClick="nodeClick"
>
<template #default="{ node, data }">
<template v-if="data.rule || data.item">
<fcDraggable :group="{name:'default', pull:'clone', put:false}" :sort="false"
:list="[{...data, _field: true}]" itemKey="label" class="_fc-field-drag">
<template #item>
<div class="_fc-field-node">
<div class="_fc-field-node-label">
<i class="fc-icon" :class="data.icon || 'icon-input'" v-if="node.isLeaf"></i>
<i class="fc-icon icon-folder" v-else></i>
<span>{{ data.label }}</span>
</div>
</div>
</template>
</fcDraggable>
</template>
<template v-else>
<div class="_fc-field-node">
<div class="_fc-field-node-label">
<i class="fc-icon" :class="data.icon || 'icon-input'" v-if="node.isLeaf"></i>
<i class="fc-icon icon-folder" v-else></i>
<span>{{ data.label }}</span>
</div>
</div>
</template>
</template>
</el-tree>
</template>
<script>
import {defineComponent} from 'vue';
import fcDraggable from 'vuedraggable/src/vuedraggable';
export default defineComponent({
name: 'FieldList',
inject: ['designer'],
props: {
field: Array
},
components: {
fcDraggable
},
methods: {
nodeClick(node) {
if (node.rule || node.item) {
const item = {...node};
this.designer.setupState.clickField(item);
}
}
}
});
</script>
<style>
._fc-field-tree .el-tree-node__content {
display: flex;
flex: 1;
color: var(--fc-text-color-1);
}
._fc-field-tree .el-tree-node__content:hover {
background-color: var(--fc-style-bg-color-1);
color: var(--fc-style-color-1);
}
._fc-field-tree ._fc-field-drag {
display: flex;
flex: 1;
flex-direction: column;
}
._fc-field-tree .fc-icon {
margin-right: 5px;
font-size: 18px;
}
._fc-field-tree .icon-folder {
color: var(--fc-style-color-1);
}
._fc-field-node-label {
display: flex;
align-items: center;
user-select: none;
}
</style>

310
src/components/FnConfig.vue Normal file
View File

@ -0,0 +1,310 @@
<template>
<div class="_fd-fn-list">
<el-badge :value="eventNum" type="warning" :hidden="eventNum < 1">
<el-button class="_fd-plain-button" plain @click="visible=true" size="small">{{ t('event.title') }}</el-button>
</el-badge>
<el-dialog class="_fd-fn-list-dialog _fd-config-dialog" :title="t('event.title')" v-model="visible" destroy-on-close
:close-on-click-modal="false"
append-to-body
width="980px">
<el-container class="_fd-fn-list-con" style="height: 600px">
<el-aside style="width:300px;">
<el-container class="_fd-fn-list-l">
<el-header class="_fd-fn-list-head" height="40px">
<el-text type="primary" size="default">
{{ t('event.list') }}
</el-text>
</el-header>
<el-main>
<el-menu
:default-active="defActive"
v-model="activeData">
<template v-for="(item, name) in event" :key="name">
<el-menu-item :index="name">
<div class="_fd-fn-list-method" @click.stop="edit(item)">
<span>function<span>{{ name }}</span></span>
<span class="_fd-label" v-if="eventInfo[name]">{{ eventInfo[name] }}</span>
<span class="_fd-dot" v-if="item.fn"></span>
</div>
</el-menu-item>
</template>
</el-menu>
</el-main>
</el-container>
</el-aside>
<el-main>
<el-container class="_fd-fn-list-r">
<el-header class="_fd-fn-list-head" height="40px" v-if="activeData">
<el-button size="small" @click="close">{{ t('props.cancel') }}</el-button>
<el-button size="small" type="primary" @click="save">{{
t('props.save')
}}
</el-button>
</el-header>
<el-main v-if="activeData">
<FnEditor ref="fn" v-model="eventStr" :name="activeData.item.name"
:args="activeData.item.args"/>
</el-main>
</el-container>
</el-main>
</el-container>
<template #footer>
<div>
<el-button size="default" @click="visible=false">{{ t('props.cancel') }}</el-button>
<el-button type="primary" size="default" @click="submit">{{
t('props.ok')
}}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import unique from '@form-create/utils/lib/unique';
import deepExtend from '@form-create/utils/lib/deepextend';
import {defineComponent} from 'vue';
import FnEditor from './FnEditor.vue';
const PREFIX = '[[FORM-CREATE-PREFIX-';
const SUFFIX = '-FORM-CREATE-SUFFIX]]';
export default defineComponent({
name: 'FnConfig',
emits: ['update:modelValue'],
props: {
modelValue: [Object, undefined, null],
eventConfig: {
type: Array,
default: () => []
},
},
inject: ['designer'],
components: {
FnEditor,
},
data() {
return {
visible: false,
activeData: null,
defActive: 'no',
event: {},
cus: false,
eventStr: '',
};
},
computed: {
eventInfo() {
const info = {};
this.eventConfig.forEach(v => {
info[v.name] = v.info;
});
return info;
},
t() {
return this.designer.setupState.t;
},
eventNum() {
let num = 0;
Object.keys(this.modelValue || {}).forEach(k => {
if (this.modelValue[k]) {
num++;
}
});
return num;
},
},
watch: {
visible(v) {
this.event = v ? this.loadFN(deepExtend({}, this.modelValue || {})) : {};
if (!v) {
this.destroy();
}
},
},
methods: {
getArgs(item) {
return item.args.join(', ');
},
loadFN(e) {
const val = {};
this.eventConfig.forEach(item => {
const k = item.name;
const fn = e[k] || '';
val[k] = {
item, fn
}
});
return val;
},
parseFN(e) {
const on = {};
Object.keys(e).forEach(k => {
if (e[k].fn) {
on[k] = e[k].fn;
}
});
return on;
},
edit(data) {
data.key = unique();
this.activeData = data;
this.eventStr = data.fn || (PREFIX + `function ${data.item.name}(${this.getArgs(data.item)}){}` + SUFFIX);
this.defActive = data.item.name;
},
save() {
if (this.$refs.fn.save()) {
this.activeData.fn = this.eventStr;
this.destroy();
return true;
}
return false;
},
destroy() {
this.activeData = null;
this.defActive = 'no';
},
close() {
this.destroy();
},
submit() {
if (this.activeData && !this.save()) {
return;
}
this.$emit('update:modelValue', this.parseFN(this.event));
this.visible = false;
this.destroy();
},
}
});
</script>
<style>
._fd-fn-list, ._fd-fn-list .el-badge {
width: 100%;
}
._fd-fn-list .el-button {
font-weight: 400;
width: 100%;
}
._fd-fn-list-con .el-main {
padding: 0;
}
._fd-fn-list-l, ._fd-fn-list-r {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
border: 1px solid var(--fc-line-color-3);
}
._fd-fn-list-head {
display: flex;
padding: 5px 15px;
border-bottom: 1px solid var(--fc-line-color-3);
background: var(--fc-bg-color-3);
align-items: center;
}
._fd-fn-list-head .el-button.is-link {
color: var(--fc-style-color-1);
}
._fd-fn-list-r {
border-left: 0 none;
}
._fd-fn-list-r ._fd-fn-list-head {
justify-content: flex-end;
}
._fd-fn-list-l > .el-main, ._fd-fn-list-r > .el-main {
display: flex;
flex-direction: row;
flex: 1;
flex-basis: auto;
box-sizing: border-box;
min-width: 0;
width: 100%;
}
._fd-fn-list-r > .el-main {
flex-direction: column;
}
._fd-fn-list-l .el-menu {
padding: 0 10px 5px;
border-right: 0 none;
width: 100%;
border-top: 0 none;
overflow: auto;
}
._fd-fn-list-l .el-menu-item.is-active {
background: var(--fc-bg-color-3);
color: var(--fc-text-color-2);
}
._fd-fn-list-l .el-menu-item {
height: auto;
line-height: 1em;
border: 1px solid var(--fc-line-color-3);
border-radius: 5px;
padding: 0;
margin-top: 5px;
}
._fd-fn-list-method {
display: flex;
flex-direction: column;
justify-content: center;
padding: 10px 20px 10px 0;
font-size: 14px;
line-height: 1em;
font-family: monospace;
width: 100%;
overflow: hidden;
white-space: pre-wrap;
position: relative;
}
._fd-fn-list-method ._fd-label {
margin-top: 4px;
color: var(--fc-text-color-3);
font-size: 12px;
}
._fd-fn-list-method ._fd-dot {
position: absolute;
top: 50%;
margin-top: -3px;
right: 16px;
display: block;
width: 6px;
height: 6px;
background: #00C050;
border-radius: 15px;
}
._fd-fn-list-method-info > span:first-child, ._fd-fn-list-method > span:first-child {
color: #9D238C;
}
._fd-fn-list-method-info > span:first-child > span, ._fd-fn-list-method > span:first-child > span {
color: var(--fc-text-color-1);
margin-left: 10px;
}
._fd-fn-list-con .CodeMirror {
height: 100%;
width: 100%;
}
._fd-fn-list-con .CodeMirror-wrap pre.CodeMirror-line {
padding-left: 20px;
}
</style>

252
src/components/FnEditor.vue Normal file
View File

@ -0,0 +1,252 @@
<template>
<div class="_fd-fn">
<div class="_fd-fn-tip">
<div class="_fd-fn-ind"></div>
<div class="cm-keyword"><span>function {{ name }}(<template
v-for="(item, idx) in argList">{{ idx > 0 ? ', ' : '' }}<template v-if="item.type === 'string'">
<span>{{ item.name }}</span>
</template><template v-else><el-popover placement="top-start" :width="400" :hide-after="0" trigger="click"
:title="item.name"
:content="item.info || ''"
><template #reference><span class="_fd-fn-arg">{{ item.name }}<i
class="fc-icon icon-question"></i></span></template>
<template v-if="item.columns">
<el-table :data="item.columns" border>
<el-table-column width="120" property="label" :label="t('props.field')"/>
<el-table-column property="info" :label="t('event.info')"/>
<el-table-column width="80" property="type" :label="t('event.type')"/>
</el-table>
</template>
</el-popover>
</template>
</template>) {</span></div>
</div>
<div ref="editor" class="_fd-fn-editor"></div>
<div class="_fd-fn-tip">
<div class="_fd-fn-ind"></div>
<div class="cm-keyword">}</div>
</div>
<el-button v-if="visible && button" type="primary" size="small" @click="save">{{ t('props.save') }}</el-button>
</div>
</template>
<script>
import 'codemirror/lib/codemirror.css';
import 'codemirror/addon/hint/show-hint.css';
import CodeMirror from 'codemirror/lib/codemirror';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/hint/javascript-hint';
import {defineComponent, markRaw} from 'vue';
import {addAutoKeyMap, toJSON} from '../utils';
import errorMessage from '../utils/message';
const PREFIX = '[[FORM-CREATE-PREFIX-';
const SUFFIX = '-FORM-CREATE-SUFFIX]]';
export default defineComponent({
name: 'FnEditor',
emits: ['update:modelValue', 'change'],
props: {
modelValue: [String, Function],
name: String,
args: Array,
body: Boolean,
button: Boolean,
fnx: Boolean,
},
inject: ['designer'],
data() {
return {
editor: null,
fn: '',
visible: false,
value: '',
};
},
watch: {
modelValue(n) {
if (n != this.value && (!n || !n.__json || (n.__json && n.__json != this.value))) {
this.editor && this.editor.setValue(this.tidyValue());
}
},
},
computed: {
t() {
return this.designer.setupState.t;
},
argStr() {
return (this.args || []).map(arg => {
if (typeof arg === 'string') {
return arg;
}
return arg.name;
}).join(', ');
},
argList() {
return this.args.map(arg => {
if (typeof arg === 'string') {
return {
name: arg,
type: 'string'
}
}
return arg;
});
},
},
mounted() {
this.$nextTick(() => {
this.load();
});
},
methods: {
save() {
const str = this.editor.getValue() || '';
if (str.trim() === '') {
this.fn = '';
} else {
let fn;
try {
fn = (new Function('return function (' + this.argStr + '){\n' + str + '\n}'))();
} catch (e) {
console.error(e);
errorMessage(this.t('struct.errorMsg'));
return false;
}
if (this.body) {
this.fn = (this.fnx ? '$FNX:' : '') + str;
} else {
this.fn = PREFIX + fn + SUFFIX;
}
}
this.submit();
return true;
},
submit() {
this.$emit('update:modelValue', this.fn);
this.$emit('change', this.fn);
this.value = this.fn;
this.visible = false;
},
trimString(input) {
const firstIndex = input.indexOf('{');
const lastIndex = input.lastIndexOf('}');
if (firstIndex === -1 || lastIndex === -1 || firstIndex >= lastIndex) {
return input;
}
return input.slice(firstIndex + 1, lastIndex).replace(/^\n+|\n+$/g, '');
},
tidyValue() {
let value = this.modelValue || '';
if (value.__json) {
value = value.__json;
}
if (this.fnx && typeof value === 'string' && value.indexOf('$FNX:') === 0) {
value = value.slice(5);
}
if (typeof value === 'function') {
value = this.trimString(toJSON(value)).trim();
} else if (!this.body) {
value = this.trimString(value).trim();
}
this.value = value;
return value;
},
load() {
this.$nextTick(() => {
let value = this.tidyValue();
this.editor = markRaw(CodeMirror(this.$refs.editor, {
lineNumbers: true,
mode: {name: 'javascript', globalVars: true},
extraKeys: {'Ctrl-Space': 'autocomplete'},
line: true,
tabSize: 2,
lineWrapping: true,
value,
}));
this.editor.on('inputRead', (cm, event) => {
if (event.keyCode === 32 && event.ctrlKey) { // Ctrl + Space
CodeMirror.showHint(cm, CodeMirror.hint.javascript); //
}
});
this.editor.on('change', () => {
this.visible = true;
});
addAutoKeyMap(this.editor);
});
},
}
});
</script>
<style>
._fd-fn {
display: flex;
flex-direction: column;
position: relative;
width: 100%;
height: 100%;
}
._fd-fn .el-button {
position: absolute;
bottom: 3px;
right: 5px;
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
}
._fd-fn-editor {
display: flex;
flex: 1;
width: 100%;
overflow: auto;
}
._fd-fn-editor .CodeMirror {
height: 100%;
width: 100%;
}
._fd-fn-tip {
color: var(--fc-text-color-1);
font-family: monospace;
direction: ltr;
background: var(--fc-bg-color-2);
}
._fd-fn-tip .cm-keyword {
color: #708;
line-height: 24px;
white-space: nowrap;
overflow-x: auto;
}
._fd-fn-tip .cm-keyword::-webkit-scrollbar {
width: 0;
height: 0;
background-color: transparent;
}
._fd-fn-ind {
background-color: var(--fc-bg-color-3);
width: 29px;
height: 24px;
display: inline-block;
margin-right: 4px;
border-right: 1px solid var(--fc-line-color-2);
float: left;
}
._fd-fn-arg {
text-decoration: underline;
cursor: pointer;
}
._fd-fn-arg i {
font-size: 12px;
color: var(--fc-style-color-1);
}
</style>

101
src/components/FnInput.vue Normal file
View File

@ -0,0 +1,101 @@
<template>
<div class="_fd-fn-input">
<el-badge type="warning" is-dot :hidden="!configured">
<el-button class="_fd-plain-button" plain @click="visible=true" size="small">
<slot>
{{t('event.action')}}
</slot>
</el-button>
</el-badge>
<el-dialog class="_fd-fn-input-dialog _fd-config-dialog" :title="title || t('struct.title')" v-model="visible"
destroy-on-close
:close-on-click-modal="false"
append-to-body width="800px">
<FnEditor ref="editor" v-model="value" :name="name" :args="args" :body="body" :fnx="fnx"></FnEditor>
<template #footer>
<div>
<el-button @click="visible = false" size="default">{{ t('props.cancel') }}</el-button>
<el-button type="primary" @click="onOk" size="default">{{ t('props.ok') }}</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/javascript/javascript';
import {defineComponent} from 'vue';
import FnEditor from './FnEditor.vue';
export default defineComponent({
name: 'FnInput',
components: {FnEditor},
emits: ['update:modelValue', 'change'],
props: {
modelValue: [String, Function],
name: String,
args: Array,
title: String,
body: Boolean,
fnx: Boolean,
defaultValue: {
require: false
},
validate: Function,
},
inject: ['designer'],
computed: {
t() {
return this.designer.setupState.t;
},
configured() {
return !!this.modelValue;
},
},
data() {
return {
visible: false,
value: this.modelValue
};
},
watch: {
modelValue(n){
this.value = n;
}
},
methods: {
onOk() {
if(this.$refs.editor.save()) {
this.$emit('update:modelValue', this.value);
this.$emit('change', this.value);
this.visible = false;
}
},
}
});
</script>
<style>
._fd-fn-input {
width: 100%;
}
._fd-fn-input .el-badge {
width: 100%;
}
._fd-fn-input .el-button {
font-weight: 400;
width: 100%;
}
._fd-fn-input-dialog .CodeMirror-lint-tooltip {
z-index: 2021 !important;
}
._fd-fn-input-dialog .el-dialog__body {
padding: 0px;
height: 500px;
}
</style>

151
src/components/FormList.vue Normal file
View File

@ -0,0 +1,151 @@
<template>
<el-tree
ref="treeRef"
class="_fc-form-tree"
:data="list"
:indent="5"
>
<template #default="{ node, data }">
<div class="_fc-form-node">
<div class="_fc-form-node-label">
<i class="fc-icon icon-form" v-if="node.isLeaf"></i>
<i class="fc-icon icon-folder" v-else></i>
<div>
<div>{{ data.label }}</div>
<span class="_fc-form-node-info" v-if="data.info">{{ data.info }}</span>
</div>
</div>
<i class="fc-icon icon-edit" v-if="node.isLeaf && !data.disabled" @click.stop="nodeClick(data)"></i>
</div>
</template>
</el-tree>
</template>
<script>
import {defineComponent} from 'vue';
import {ElLoading} from 'element-plus';
export default defineComponent({
name: 'FormList',
inject: ['designer'],
props: {
list: Array
},
data() {
return {
loading: false,
timerId: undefined,
load: undefined,
};
},
methods: {
loadConfig(config, data) {
this.designer.setupState.openInputData(false);
this.designer.setupState.pageData = [];
this.designer.setupState.setOptions(config.options);
this.designer.setupState.setRule(config.rule);
this.designer.emit('switchForm', data);
},
starLoad() {
if (!this.loading) {
this.load = ElLoading.service({
target: document.getElementsByClassName('_fc-m-drag')[0],
lock: true,
});
this.loading = true;
this.timerId = setTimeout(() => {
this.endLoad();
}, 5000);
}
},
endLoad() {
this.loading = false;
this.load && this.load.close();
this.timerId && clearTimeout(this.timerId);
this.load = undefined;
this.timerId = undefined;
},
nodeClick(data) {
if (this.loading) {
return;
}
let config = {
rule: data.rule || [],
options: data.options || {}
}
if (data.load) {
const value = data.load(data);
if (value && value.then) {
this.starLoad();
value.then(res => {
if (res.rule) {
config.rule = res.rule;
}
if (res.options) {
config.options = res.options;
}
this.loadConfig(config, data);
this.endLoad();
}).catch(e => {
this.endLoad();
});
return;
} else if (value) {
if (value.rule) {
config.rule = value.rule;
}
if (value.options) {
config.options = value.options;
}
}
}
this.loadConfig(config, data);
}
}
});
</script>
<style>
._fc-l ._fc-form-tree .el-tree-node__content {
display: flex;
flex: 1;
color: var(--fc-text-color-1);
height: auto;
padding: 5px 0;
}
._fc-form-tree .el-tree-node__content:hover {
background-color: var(--fc-style-bg-color-1);
color: var(--fc-style-color-1);
}
._fc-form-tree .fc-icon {
margin-right: 5px;
font-size: 18px;
color: var(--fc-style-color-1);
}
._fc-form-tree .icon-folder {
color: #FFBA00;
}
._fc-form-node {
width: 100%;
display: flex;
justify-content: space-between;
flex-direction: row;
padding-right: 12px;
}
._fc-form-node-label {
display: flex;
align-items: flex-start;
white-space: normal;
user-select: none;
}
._fc-form-node-info {
font-size: 12px;
color: var(--fc-text-color-2);
}
</style>

View File

@ -0,0 +1,383 @@
<template>
<div class="_fd-gcc">
<el-badge :value="eventNum" type="warning" :hidden="eventNum < 1">
<el-button class="_fd-plain-button" plain @click="open" size="small">{{ t('class.title') }}</el-button>
</el-badge>
<el-dialog class="_fd-gcc-dialog _fd-config-dialog" v-model="visible" destroy-on-close
:close-on-click-modal="false"
append-to-body
width="700px">
<template #header>
{{ t('form.globalClass') }}
<Warning :tooltip="t('warning.globalClass')"></Warning>
</template>
<el-container class="_fd-gcc-con" style="height: 600px">
<el-aside style="width:255px;">
<el-container class="_fd-gcc-l">
<el-header class="_fd-gcc-head" height="40px">
<el-button link type="primary" size="default" @click="cusEvent">
{{ t('class.create') }}
</el-button>
</el-header>
<el-main>
<el-menu>
<el-menu-item :class="{'is-active':activeStyle, '_fd-gcc-default': true}">
<div class="_fd-gcc-title" @click.stop="changeStyle">
<div class="_fd-gcc-method">
<span class="_fd-label">{{ t('form.globalClass') }}</span>
</div>
</div>
</el-menu-item>
<template v-for="(item, key) in value">
<el-menu-item :class="{'is-active':key === activeIdx}">
<div class="_fd-gcc-title" @click.stop="active(key)">
<div class="_fd-gcc-method">
<span>.{{ key }}</span>
<span class="_fd-label" v-if="item.label">{{ item.label }}</span>
</div>
<i class="fc-icon icon-delete" v-if="item.deletable !== false" @click.stop="rm(key)"></i>
</div>
</el-menu-item>
</template>
<el-menu-item v-if="cus" style="padding-left: 10px;">
<div class="_fd-gcc-title" @click.stop>
<el-input type="text" v-model="cusValue" size="default"
@keydown.enter="addCus"
:placeholder="t('class.placeholder')">
</el-input>
<div>
<i class="fc-icon icon-add" @click.stop="addCus"></i>
<i class="fc-icon icon-delete" @click.stop="closeCus"></i>
</div>
</div>
</el-menu-item>
</el-menu>
</el-main>
</el-container>
</el-aside>
<el-main>
<el-container class="_fd-gcc-r">
<el-header class="_fd-gcc-head" height="40px" v-if="activeIdx || activeStyle">
<el-button size="small" @click="close">{{ t('props.cancel') }}</el-button>
<el-button size="small" type="primary" @click="save">{{
t('props.save')
}}
</el-button>
</el-header>
<el-main v-if="activeIdx || activeStyle" :key="activeIdx"
:class="activeStyle ? '_fd-gcc-style' : ''">
<template v-if="activeStyle">
<StyleEditor ref="editor" v-model="content"></StyleEditor>
</template>
<template v-else>
<el-form size="small">
<StyleConfig v-model="handle"/>
</el-form>
</template>
</el-main>
</el-container>
</el-main>
</el-container>
<template #footer>
<div>
<el-button size="default" @click="visible=false">{{ t('props.cancel') }}</el-button>
<el-button type="primary" size="default" @click="submit">{{
t('props.ok')
}}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import unique from '@form-create/utils/lib/unique';
import {deepCopy} from '@form-create/utils/lib/deepextend';
import {defineComponent, markRaw} from 'vue';
import {getInjectArg} from '../utils';
import StyleConfig from './style/StyleConfig.vue';
import toLine from '@form-create/utils/lib/toline';
import StyleEditor from './style/StyleEditor.vue';
import Warning from './Warning.vue';
export default defineComponent({
name: 'GlobalClassConfig',
emits: ['update:modelValue'],
props: {
modelValue: Object,
},
components: {
Warning,
StyleEditor,
StyleConfig,
},
inject: ['designer'],
data() {
return {
visible: false,
activeIdx: '',
value: {},
cus: false,
cusValue: '',
handle: '',
styleEl: null,
content: '',
activeStyle: false,
};
},
computed: {
t() {
return this.designer.setupState.t;
},
eventNum() {
return Object.keys(this.modelValue || {}).length;
},
fnArgs() {
return [getInjectArg(this.t)];
}
},
watch: {
visible(v) {
if (v) {
this.activeIdx = '';
this.value = deepCopy(this.modelValue || {});
}
},
modelValue() {
this.updateGlobalStyle();
},
},
methods: {
open() {
this.visible = true;
},
changeStyle() {
this.content = this.designer.setupState.formOptions.style || '#_demo1{\n display:flex;\n}\n\n._demo2{\n display:flex;\n}';
this.activeStyle = true;
this.activeIdx = '';
},
active(idx) {
this.activeStyle = false;
if (this.activeIdx !== idx) {
this.handle = this.value[idx].style || '';
this.activeIdx = idx;
}
},
addCus() {
const label = this.cusValue && this.cusValue.trim();
if (label) {
const key = 'cls_' + unique();
this.value[key] = {
label,
style: {},
};
this.active(key);
this.closeCus();
}
},
closeCus() {
this.cus = false;
this.cusValue = '';
},
cusEvent() {
this.cus = true;
},
save() {
if (this.activeStyle) {
this.$refs.editor.save();
this.designer.setupState.formOptions.style = this.content;
this.activeStyle = false;
} else {
this.value[this.activeIdx].style = this.handle;
this.activeIdx = '';
}
},
rm(key) {
delete this.value[key];
if (key === this.activeIdx) {
this.activeIdx = '';
}
},
close() {
this.activeIdx = '';
},
submit() {
if (this.activeIdx || this.activeStyle) {
this.save();
}
this.$emit('update:modelValue', {...this.value});
this.visible = false;
},
updateGlobalStyle() {
let content = '';
const globalClass = this.modelValue || {};
Object.keys(globalClass).forEach(k => {
let subCss = '';
globalClass[k].style && Object.keys(globalClass[k].style).forEach(key => {
subCss += toLine(key) + ':' + globalClass[k].style[key] + ';';
});
if (globalClass[k].content) {
subCss += globalClass[k].content + ';';
}
if (subCss) {
content += `.${k}{${subCss}}`;
}
});
if (content) {
this.styleEl.innerHTML = content;
}
},
},
created() {
this.styleEl = markRaw(document.createElement('style'));
this.styleEl.type = 'text/css';
document.head.appendChild(this.styleEl);
this.updateGlobalStyle();
},
unmounted() {
document.head.removeChild(this.styleEl);
}
});
</script>
<style>
._fd-gcc, ._fd-gcc .el-badge {
width: 100%;
}
._fd-gcc .el-button {
font-weight: 400;
width: 100%;
}
._fd-gcc-con .el-main {
padding: 0;
}
._fd-gcc-l, ._fd-gcc-r {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
border: 1px solid var(--fc-line-color-3);
}
._fd-gcc-head {
display: flex;
padding: 5px 15px;
border-bottom: 1px solid var(--fc-line-color-3);
background: var(--fc-bg-color-3);
align-items: center;
}
._fd-gcc-head .el-button.is-link {
color: var(--fc-style-color-1);
}
._fd-gcc-r {
border-left: 0 none;
}
._fd-gcc-r ._fd-gcc-head {
justify-content: flex-end;
}
._fd-gcc-l > .el-main, ._fd-gcc-r > .el-main {
display: flex;
flex-direction: row;
flex: 1;
flex-basis: auto;
box-sizing: border-box;
min-width: 0;
width: 100%;
}
._fd-gcc-r > .el-main {
padding: 20px;
flex-direction: column;
}
._fd-gcc-r > .el-main._fd-gcc-style {
padding: 0;
}
._fd-gcc-r .el-form-item {
margin-bottom: 10px !important;
}
._fd-gcc-l .el-menu {
padding: 0 10px 5px;
border-right: 0 none;
width: 100%;
border-top: 0 none;
overflow: auto;
}
._fd-gcc-l .el-menu-item.is-active {
background: var(--fc-bg-color-3);
color: var(--fc-style-color-1);
}
._fd-gcc-l .el-menu-item {
height: auto;
line-height: 1em;
border: 1px solid var(--fc-line-color-3);
border-radius: 5px;
padding: 0;
margin-top: 5px;
}
._fd-gcc-default.is-active ._fd-label {
color: var(--fc-style-color-1);
margin-top: 0;
}
._fd-gcc-method {
display: flex;
flex-direction: column;
justify-content: center;
width: 175px;
font-size: 14px;
font-family: monospace;
overflow: hidden;
white-space: pre-wrap;
color: #923B76;
}
._fd-gcc-method ._fd-label {
color: var(--fc-text-color-3);
font-size: 12px;
}
._fd-gcc-method span + ._fd-label {
margin-top: 4px;
}
._fd-gcc-title {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 10px 0;
}
._fd-gcc-title .el-input {
width: 160px;
}
._fd-gcc-title .fc-icon {
margin-right: 6px;
font-size: 18px;
color: var(--fc-text-color-2);
}
._fd-gcc-title .el-input__wrapper {
box-shadow: none;
}
._fd-gcc-con .el-menu-item.is-active i {
color: var(--fc-text-color-2);
}
</style>

View File

@ -0,0 +1,93 @@
<template>
<div class="_fd-gcs">
<el-select v-model="value"
multiple
filterable
allow-create
default-first-option
:reserve-keyword="false"
clearable @change="input">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<div class="_fd-gcs-handle">
<div class="_fc-manage-text" @click="openConfig"><i
class="fc-icon icon-setting"/></div>
</div>
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'GlobalClassSelect',
emits: ['update:modelValue'],
props: {
modelValue: [Array, String],
to: String,
},
inject: ['designer'],
computed: {
t() {
return this.designer.setupState.t;
},
options() {
return Object.keys((this.designer.setupState.formOptions.globalClass || {})).map(k => {
return {label: this.designer.setupState.formOptions.globalClass[k].label, value: k}
})
},
},
watch: {
modelValue() {
this.tidyValue();
},
},
data() {
return {
value: [],
};
},
methods: {
tidyValue() {
this.value = Array.isArray(this.modelValue) ? this.modelValue : (this.modelValue || '').split(' ').filter(v => !!v);
},
openConfig() {
this.designer.setupState.openGlobalClassDialog();
},
input() {
this.$emit('update:modelValue', this.value.join(' '));
},
},
created() {
this.tidyValue();
}
});
</script>
<style>
._fd-gcs {
display: flex;
align-items: center;
width: 100%;
}
._fd-gcs .el-select {
width: 190px;
}
._fd-gcs-handle {
display: inline-flex;
height: 14px;
line-height: 14px;
}
._fd-gcs-handle ._fc-manage-text {
border-left: 1px solid var(--fc-line-color-3);
padding-left: 4px;
}
</style>

View File

@ -0,0 +1,325 @@
<template>
<div class="_fd-gec">
<el-badge :value="eventNum" type="warning" :hidden="eventNum < 1">
<el-button class="_fd-plain-button" plain @click="open" size="small">{{ t('event.title') }}</el-button>
</el-badge>
<el-dialog class="_fd-gec-dialog _fd-config-dialog" v-model="visible" destroy-on-close
:close-on-click-modal="false"
append-to-body
width="980px">
<template #header>
{{ t('form.globalEvent') }}<Warning :tooltip="t('warning.globalEvent')"></Warning>
</template>
<el-container class="_fd-gec-con" style="height: 600px">
<el-aside style="width:300px;">
<el-container class="_fd-gec-l">
<el-header class="_fd-gec-head" height="40px">
<el-button link type="primary" size="default" @click="cusEvent">
{{ t('event.create') }}
</el-button>
</el-header>
<el-main>
<el-menu>
<template v-for="(item, key) in event">
<el-menu-item :class="{'is-active':key === activeIdx}">
<div class="_fd-gec-title" @click.stop="active(key)">
<div class="_fd-gec-method">
<span>{{ key }}</span>
<span class="_fd-label" v-if="item.label">{{ item.label }}</span>
</div>
<i class="fc-icon icon-delete" v-if="item.deletable !== false" @click.stop="rm(key)"></i>
</div>
</el-menu-item>
</template>
<el-menu-item v-if="cus" style="padding-left: 10px;">
<div class="_fd-gec-title" @click.stop>
<el-input type="text" v-model="cusValue" size="default"
@keydown.enter="addCus"
:placeholder="t('event.placeholder')">
</el-input>
<div>
<i class="fc-icon icon-add" @click.stop="addCus"></i>
<i class="fc-icon icon-delete" @click.stop="closeCus"></i>
</div>
</div>
</el-menu-item>
</el-menu>
</el-main>
</el-container>
</el-aside>
<el-main>
<el-container class="_fd-gec-r">
<el-header class="_fd-gec-head" height="40px" v-if="activeIdx">
<el-button size="small" @click="close">{{ t('props.cancel') }}</el-button>
<el-button size="small" type="primary" @click="save">{{
t('props.save')
}}
</el-button>
</el-header>
<el-main v-if="activeIdx" :key="activeIdx">
<FnEditor v-model="handle" name="handle" :args="fnArgs"
ref="data"></FnEditor>
</el-main>
</el-container>
</el-main>
</el-container>
<template #footer>
<div>
<el-button size="default" @click="visible=false">{{ t('props.cancel') }}</el-button>
<el-button type="primary" size="default" @click="submit">{{
t('props.ok')
}}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import unique from '@form-create/utils/lib/unique';
import {deepCopy} from '@form-create/utils/lib/deepextend';
import FnEditor from './FnEditor.vue';
import {defineComponent} from 'vue';
import {getInjectArg} from '../utils';
import Warning from './Warning.vue';
export default defineComponent({
name: 'GlobalEventConfig',
emits: ['update:modelValue'],
props: {
modelValue: Object,
eventName: Array,
},
components: {
Warning,
FnEditor,
},
inject: ['designer'],
data() {
return {
visible: false,
activeIdx: '',
event: {},
cus: false,
cusValue: '',
handle: '',
};
},
computed: {
t() {
return this.designer.setupState.t;
},
eventNum() {
return Object.keys(this.modelValue || {}).length;
},
fnArgs() {
return [getInjectArg(this.t)];
}
},
watch: {
visible(v) {
if (v) {
this.activeIdx = '';
this.event = deepCopy(this.modelValue || {});
}
},
},
methods: {
open() {
this.visible = true;
},
active(idx) {
if (this.activeIdx !== idx) {
this.handle = this.event[idx].handle || '';
this.activeIdx = idx;
}
},
addCus() {
const label = this.cusValue && this.cusValue.trim();
if (label) {
const key = 'event_' + unique();
this.event[key] = {
label,
handle: '',
};
this.active(key);
this.closeCus();
}
},
closeCus() {
this.cus = false;
this.cusValue = '';
},
cusEvent() {
this.cus = true;
},
save() {
if (!this.$refs.data.save()) {
return false;
}
this.event[this.activeIdx].handle = this.handle;
this.activeIdx = '';
return true;
},
rm(key) {
delete this.event[key];
if (key === this.activeIdx) {
this.activeIdx = '';
}
},
close() {
this.activeIdx = '';
},
submit() {
if (this.activeIdx && !this.save()) {
return;
}
this.$emit('update:modelValue', {...this.event});
this.visible = false;
},
},
beforeCreate() {
window.$inject = {
$f: {},
rule: [],
self: {},
option: {},
inject: {},
args: [],
};
}
});
</script>
<style>
._fd-gec, ._fd-gec .el-badge {
width: 100%;
}
._fd-gec .el-button {
font-weight: 400;
width: 100%;
}
._fd-gec-con .el-main {
padding: 0;
}
._fd-gec-l, ._fd-gec-r {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
border: 1px solid var(--fc-line-color-3);
}
._fd-gec-head {
display: flex;
padding: 5px 15px;
border-bottom: 1px solid var(--fc-line-color-3);
background: var(--fc-bg-color-3);
align-items: center;
}
._fd-gec-head .el-button.is-link {
color: var(--fc-style-color-1);
}
._fd-gec-r {
border-left: 0 none;
}
._fd-gec-r ._fd-gec-head {
justify-content: flex-end;
}
._fd-gec-l > .el-main, ._fd-gec-r > .el-main {
display: flex;
flex-direction: row;
flex: 1;
flex-basis: auto;
box-sizing: border-box;
min-width: 0;
width: 100%;
}
._fd-gec-r > .el-main {
flex-direction: column;
}
._fd-gec-l .el-menu {
padding: 0 10px 5px;
border-right: 0 none;
width: 100%;
border-top: 0 none;
overflow: auto;
}
._fd-gec-l .el-menu-item.is-active {
background: var(--fc-bg-color-3);
color: var(--fc-text-color-2);
}
._fd-gec-l .el-menu-item {
height: auto;
line-height: 1em;
border: 1px solid var(--fc-line-color-3);
border-radius: 5px;
padding: 0;
margin-top: 5px;
}
._fd-gec-method {
display: flex;
flex-direction: column;
justify-content: center;
width: 225px;
font-size: 14px;
font-family: monospace;
color: #9D238C;
overflow: hidden;
white-space: pre-wrap;
}
._fd-gec-method ._fd-label {
margin-top: 4px;
color: var(--fc-text-color-3);
font-size: 12px;
}
._fd-gec-title {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 10px 0;
}
._fd-gec-title .el-input {
width: 200px;
}
._fd-gec-title .fc-icon {
margin-right: 6px;
font-size: 18px;
color: var(--fc-text-color-2);
}
._fd-gec-title .el-input__wrapper {
box-shadow: none;
}
._fd-gec-con .el-menu-item.is-active i {
color: var(--fc-text-color-2);
}
._fd-gec-con .CodeMirror {
height: 100%;
width: 100%;
}
._fd-gec-con .CodeMirror-wrap pre.CodeMirror-line {
padding-left: 20px;
}
</style>

View File

@ -0,0 +1,528 @@
<template>
<div class="_fd-gfc">
<el-badge :value="dataNum" type="warning" :hidden="dataNum < 1">
<el-button class="_fd-plain-button" plain @click="open" size="small">{{ t('fetch.title') }}</el-button>
</el-badge>
<el-dialog class="_fd-gfc-dialog _fd-config-dialog" v-model="visible" destroy-on-close
:close-on-click-modal="false"
append-to-body
width="980px">
<template #header>
{{ t('form.globalFetch') }}
<Warning :tooltip="t('warning.globalFetch')"></Warning>
</template>
<el-container class="_fd-gfc-con" style="height: 600px">
<el-aside style="width:300px;">
<el-container class="_fd-gfc-l">
<el-header class="_fd-gfc-head" height="40px">
<el-dropdown trigger="click" size="default">
<el-button link type="primary" size="default">
{{ t('fetch.create') }}<i class="fc-icon icon-down" style="font-size: 14px;"></i>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="(label, key) in types" :key="key"
@click="cusEvent(key)">
<div>{{ label }}</div>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-header>
<el-main>
<el-menu>
<template v-for="(item, key) in list">
<el-menu-item :class="{'is-active':key === activeIdx}">
<div class="_fd-gfc-title" @click.stop="active(key)">
<div class="_fd-gfc-method">
<span>{{ key }}</span>
<span class="_fd-label" v-if="item.label">{{ item.label }}</span>
</div>
<i class="fc-icon icon-delete" v-if="item.deletable !== false" @click.stop="rm(key)"></i>
</div>
</el-menu-item>
</template>
<el-menu-item v-if="cus" style="padding-left: 10px;">
<div class="_fd-gfc-title" @click.stop>
<el-input type="text" v-model="cusValue" size="default"
@keydown.enter="addCus"
:placeholder="t('fetch.placeholder')">
</el-input>
<div>
<i class="fc-icon icon-add" @click.stop="addCus"></i>
<i class="fc-icon icon-delete" @click.stop="closeCus"></i>
</div>
</div>
</el-menu-item>
</el-menu>
</el-main>
</el-container>
</el-aside>
<el-main>
<el-container class="_fd-gfc-r">
<el-header class="_fd-gfc-head" height="40px" v-if="activeIdx">
<el-button size="small" @click="close">{{ t('props.cancel') }}</el-button>
<el-button size="small" type="primary" @click="save">{{
t('props.save')
}}
</el-button>
</el-header>
<el-main v-if="activeIdx" :key="activeIdx">
<template v-if="list[activeIdx].type === 'fetch'">
<el-tabs model-value="first" class="_fc-tabs" style="width: 100%">
<el-tab-pane :label="t('fetch.config')" name="first" style="padding-right: 15px;">
<div class="_fd-gfc-info">
{{ t('fetch.info') }}
</div>
<DragForm v-model:api="form.api" v-model="form.formData" :rule="form.rule"
:option="form.options">
<template #title="scope">
<template v-if="scope.rule.warning">
<Warning :tooltip="scope.rule.warning">
{{ scope.rule.title }}
</Warning>
</template>
<template v-else>
{{ scope.rule.title }}
</template>
</template>
</DragForm>
</el-tab-pane>
<el-tab-pane lazy :label="t('fetch.beforeFetch')" name="second">
<template #label>
{{ t('fetch.beforeFetch') }}
<Warning :tooltip="t('warning.beforeFetch')"></Warning>
</template>
<FnEditor style="height: 100%;" v-model="form.beforeFetch" name="beforeFetch"
:args="['config', 'data']"
ref="beforeFetch"></FnEditor>
</el-tab-pane>
<el-tab-pane lazy name="third">
<template #label>
{{ t('fetch.parse') }}
<Warning :tooltip="t('warning.fetchParse')"></Warning>
</template>
<FnEditor style="height: 100%;" v-model="form.parse" name="parse"
:args="[{name:'res', info: t('fetch.response')}, 'rule', 'api']"
ref="parse"></FnEditor>
</el-tab-pane>
<el-tab-pane lazy :label="t('fetch.onError')" name="fourth">
<FnEditor style="height: 100%;" v-model="form.onError" name="onError"
:args="['e']"
ref="error"></FnEditor>
</el-tab-pane>
</el-tabs>
</template>
<template v-else>
<StructEditor v-model="form.data" ref="data"></StructEditor>
</template>
</el-main>
</el-container>
</el-main>
</el-container>
<template #footer>
<div>
<el-button size="default" @click="visible=false">{{ t('props.cancel') }}</el-button>
<el-button type="primary" size="default" @click="submit">{{
t('props.ok')
}}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import unique from '@form-create/utils/lib/unique';
import {deepCopy} from '@form-create/utils/lib/deepextend';
import FnEditor from './FnEditor.vue';
import StructEditor from './StructEditor.vue';
import {defineComponent} from 'vue';
import {designerForm} from '../utils/form';
import errorMessage from '../utils/message';
import VariableConfig from './computed/VariableConfig.vue';
import Warning from './Warning.vue';
const makeRule = (t) => {
return [
{
type: 'input',
field: 'action',
title: t('fetch.action'),
value: '',
validate: [{required: true, message: t('fetch.actionRequired'), trigger: 'blur'}],
inject: true,
on: {
blur({self}, e) {
self._start = e.target.selectionStart;
}
},
children: [
{
type: 'VariableConfig',
slot: 'suffix',
props: {
popover: true,
},
inject: true,
on: {
confirm({api}, val) {
const rule = api.getRule('action');
rule.value = rule.value.substring(0, rule._start) + val + rule.value.substring(rule._start);
},
}
}
]
},
{
type: 'radio',
field: 'method',
title: t('fetch.method'),
value: 'GET',
options: [
{label: 'GET', value: 'GET'},
{label: 'POST', value: 'POST'},
],
$required: true,
},
{
type: 'radio',
field: 'dataType',
title: t('fetch.dataType'),
warning: t('warning.fetchDataType'),
value: 'json',
options: [
{label: 'JSON', value: 'json'},
{label: 'FormData', value: 'formData'},
],
$required: true,
},
{
type: 'FetchTable',
field: 'headers',
title: t('fetch.headers'),
value: {},
},
{
type: 'FetchTable',
field: 'query',
title: t('fetch.query'),
warning: t('warning.fetchQuery'),
value: {},
},
{
type: 'FetchTable',
field: 'data',
title: t('fetch.data'),
warning: t('warning.fetchData'),
value: {},
}];
}
export default defineComponent({
name: 'GlobalFetchConfig',
emits: ['update:modelValue'],
props: {
modelValue: Object,
},
components: {
Warning,
VariableConfig,
DragForm: designerForm.$form(),
FnEditor,
StructEditor
},
inject: ['designer'],
data() {
return {
visible: false,
activeIdx: '',
list: {},
cus: false,
cusValue: '',
form: {
api: {},
formData: {},
rule: [],
options: {
form: {
labelWidth: '90px',
size: 'small'
},
submitBtn: false,
resetBtn: false,
}
},
};
},
computed: {
t() {
return this.designer.setupState.t;
},
types() {
return {fetch: this.t('fetch.remote'), static: this.t('fetch.static')}
},
dataNum() {
return Object.keys(this.modelValue || {}).length;
}
},
watch: {
visible(v) {
if (v) {
this.list = deepCopy(this.modelValue || {});
this.activeIdx = '';
}
},
},
methods: {
open() {
this.visible = true;
},
active(key) {
if (this.activeIdx !== key) {
const formData = this.list[key];
this.form.rule = formData.type === 'static' ? [] : makeRule(this.t);
this.form.formData = {...formData};
this.form.label = formData.label;
this.form.type = formData.type;
this.form.data = formData.data;
this.form.dataType = formData.dataType;
this.form.parse = formData.parse || '';
this.form.beforeFetch = formData.beforeFetch || '';
this.form.onError = formData.onError || '';
this.activeIdx = key;
}
},
addCus() {
const label = this.cusValue && this.cusValue.trim();
if (label) {
const key = 'data_' + unique();
this.list[key] = {
label,
type: this.cus,
data: [],
};
this.active(key);
this.closeCus();
}
},
closeCus() {
this.cus = false;
this.cusValue = '';
},
cusEvent(type) {
this.cus = type;
},
saveData() {
if (!this.$refs.data.save()) {
return;
}
this.list[this.activeIdx].data = this.form.data || [];
this.activeIdx = '';
},
save() {
if (this.list[this.activeIdx].type === 'static') {
return this.saveData();
}
this.form.api.validate().then(() => {
const formData = {...this.form.formData};
if ((this.$refs.parse && !this.$refs.parse.save()) || (this.$refs.beforeFetch && !this.$refs.beforeFetch.save()) || (this.$refs.error && !this.$refs.error.save())) {
return;
}
formData.parse = designerForm.parseFn(this.form.parse);
formData.beforeFetch = designerForm.parseFn(this.form.beforeFetch);
formData.onError = this.form.onError;
formData.label = this.form.label;
formData.type = this.form.type;
this.list[this.activeIdx] = formData;
this.activeIdx = '';
}).catch(err => {
console.error(err);
errorMessage(err[Object.keys(err)[0]][0].message);
});
},
rm(key) {
delete this.list[key];
if (key === this.activeIdx) {
this.activeIdx = '';
}
},
close() {
this.activeIdx = '';
},
submit() {
if (this.activeIdx) {
return errorMessage(this.t('event.saveMsg'));
}
this.$emit('update:modelValue', {...this.list});
this.visible = false;
},
}
});
</script>
<style>
._fd-gfc, ._fd-gfc .el-badge {
width: 100%;
}
._fd-gfc .el-button {
font-weight: 400;
width: 100%;
}
._fd-gfc-dialog .el-tabs__header {
margin-bottom: 0;
}
._fd-gfc-dialog .form-create {
margin-top: 15px;
}
._fd-gfc-dialog ._fc-tabs {
display: flex;
height: 100%;
}
._fd-gfc-dialog ._fc-tabs .el-tabs__content {
display: flex;
flex: 1;
overflow: auto;
}
._fd-gfc-info {
display: flex;
font-size: 12px;
position: relative;
margin-top: 8px;
margin-left: 15px;
padding: 8px 13px;
line-height: 18px;
background: rgba(170, 170, 170, 0.1);
border-radius: 6px;
color: var(--fc-text-color-2);
}
._fd-gfc-con .el-main {
padding: 0;
}
._fd-gfc-l, ._fd-gfc-r {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
border: 1px solid var(--fc-line-color-3);
}
._fd-gfc-head {
display: flex;
padding: 5px 15px;
border-bottom: 1px solid var(--fc-line-color-3);
background: var(--fc-bg-color-3);
align-items: center;
}
._fd-gfc-head .el-button.is-link {
color: var(--fc-style-color-1);
}
._fd-gfc-r {
border-left: 0 none;
}
._fd-gfc-r ._fd-gfc-head {
justify-content: flex-end;
}
._fd-gfc-l > .el-main, ._fd-gfc-r > .el-main {
display: flex;
flex-direction: row;
flex: 1;
flex-basis: auto;
box-sizing: border-box;
min-width: 0;
width: 100%;
}
._fd-gfc-r > .el-main {
flex-direction: column;
}
._fd-gfc-l .el-menu {
padding: 0 10px 5px;
border-right: 0 none;
width: 100%;
border-top: 0 none;
overflow: auto;
}
._fd-gfc-l .el-menu-item.is-active {
background: var(--fc-bg-color-3);
color: var(--fc-text-color-2);
}
._fd-gfc-l .el-menu-item {
height: auto;
line-height: 1em;
border: 1px solid var(--fc-line-color-3);
border-radius: 5px;
padding: 0;
margin-top: 5px;
}
._fd-gfc-method {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
font-size: 14px;
font-family: monospace;
color: #702C71;
overflow: hidden;
white-space: pre-wrap;
}
._fd-gfc-method ._fd-label {
margin-top: 4px;
color: var(--fc-text-color-3);
font-size: 12px;
}
._fd-gfc-title {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 10px 0;
}
._fd-gfc-title .el-input {
width: 200px;
}
._fd-gfc-title .fc-icon {
margin-right: 6px;
font-size: 18px;
color: var(--fc-text-color-2);
}
._fd-gfc-title .el-input__wrapper {
box-shadow: none;
}
._fd-gfc-title .el-menu-item.is-active i {
color: var(--fc-text-color-2);
}
._fd-gfc-con .CodeMirror {
height: 100%;
width: 100%;
}
._fd-gfc-con .CodeMirror-wrap pre.CodeMirror-line {
padding-left: 20px;
}
</style>

View File

@ -0,0 +1,111 @@
<template>
<div class="_fd-gfs">
<el-select v-model="value" clearable filterable @change="input">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<div class="_fd-gfs-handle">
<i @click="refresh" class="fc-icon icon-refresh" :class="{disabled: !value, '_fc-loading': this.loading}"
title="reload"/>
<div class="_fc-manage-text" @click="openConfig"><i
class="fc-icon icon-setting"/></div>
</div>
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'GlobalFetchSelect',
emits: ['update:modelValue'],
props: {
modelValue: [Object, String],
to: String,
},
inject: ['designer'],
computed: {
t() {
return this.designer.setupState.t;
},
options() {
return Object.keys((this.designer.setupState.formOptions.globalData || {})).map(k => {
return {label: this.designer.setupState.formOptions.globalData[k].label, value: k}
})
},
},
watch: {
modelValue() {
this.value = (this.modelValue || {}).key || '';
},
},
data() {
return {
value: (this.modelValue || {}).key || '',
uni: 1,
loading: false,
};
},
methods: {
refresh() {
if (!this.value) {
return;
}
this.uni = this.uni === 1 ? 0 : 1;
this.input();
this.loading = true;
setTimeout(() => {
this.loading = false;
}, 1000);
},
openConfig() {
this.designer.setupState.openGlobalFetchDialog();
},
input() {
const value = typeof this.modelValue === 'object' ? {...this.modelValue} : {};
value.to = this.to || 'options';
value.key = this.value;
value._uni = this.uni;
this.$emit('update:modelValue', value);
},
}
});
</script>
<style>
._fd-gfs {
display: flex;
align-items: center;
width: 100%;
}
._fd-gfs .el-select {
width: 190px;
}
._fd-gfs-handle {
display: inline-flex;
height: 14px;
line-height: 14px;
}
._fd-gfs-handle .fc-icon {
cursor: pointer;
margin-left: 4px;
color: var(--fc-style-color-1);
}
._fd-gfs-handle .icon-refresh.disabled {
color: #a9abb2;
cursor: not-allowed;
}
._fd-gfs-handle ._fc-manage-text {
border-left: 1px solid var(--fc-line-color-3);
padding-left: 4px;
}
</style>

View File

@ -0,0 +1,302 @@
<template>
<div class="_fd-gvc">
<el-badge :value="eventNum" type="warning" :hidden="eventNum < 1">
<el-button class="_fd-plain-button" plain @click="open" size="small">{{t('computed.variable.btn')}}</el-button>
</el-badge>
<el-dialog class="_fd-gvc-dialog _fd-config-dialog" v-model="visible" destroy-on-close
:close-on-click-modal="false"
append-to-body
width="980px">
<template #header>
{{ t('computed.variable.title') }}
<Warning :tooltip="t('warning.globalVariable')"></Warning>
</template>
<el-container class="_fd-gvc-con" style="height: 600px">
<el-aside style="width:255px;">
<el-container class="_fd-gvc-l">
<el-header class="_fd-gvc-head" height="40px">
<el-button link type="primary" size="default" @click="cusEvent">
{{ t('computed.variable.create') }}
</el-button>
</el-header>
<el-main>
<el-menu>
<template v-for="(item, key) in value">
<el-menu-item :class="{'is-active':key === activeIdx}">
<div class="_fd-gvc-title" @click.stop="active(key)">
<div class="_fd-gvc-method">
<span>{{ key }}</span>
<span class="_fd-label" v-if="item.label">{{ item.label }}</span>
</div>
<i class="fc-icon icon-delete" v-if="item.deletable !== false" @click.stop="rm(key)"></i>
</div>
</el-menu-item>
</template>
<el-menu-item v-if="cus" style="padding-left: 10px;">
<div class="_fd-gvc-title" @click.stop>
<el-input type="text" v-model="cusValue" size="default"
@keydown.enter="addCus"
:placeholder="t('computed.variable.placeholder')">
</el-input>
<div>
<i class="fc-icon icon-add" @click.stop="addCus"></i>
<i class="fc-icon icon-delete" @click.stop="closeCus"></i>
</div>
</div>
</el-menu-item>
</el-menu>
</el-main>
</el-container>
</el-aside>
<el-main>
<el-container class="_fd-gvc-r">
<el-header class="_fd-gvc-head" height="40px" v-if="activeIdx">
<el-button size="small" @click="close">{{ t('props.cancel') }}</el-button>
<el-button size="small" type="primary" @click="save">{{
t('props.save')
}}
</el-button>
</el-header>
<el-main v-if="activeIdx" :key="activeIdx">
<FnEditor ref="editor" v-model="handle" name="handle" :args="['get', 'api']"></FnEditor>
</el-main>
</el-container>
</el-main>
</el-container>
<template #footer>
<div>
<el-button size="default" @click="visible=false">{{ t('props.cancel') }}</el-button>
<el-button type="primary" size="default" @click="submit">{{
t('props.ok')
}}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import unique from '@form-create/utils/lib/unique';
import {deepCopy} from '@form-create/utils/lib/deepextend';
import {defineComponent} from 'vue';
import FnEditor from './FnEditor.vue';
import Warning from './Warning.vue';
export default defineComponent({
name: 'GlobalVariableConfig',
emits: ['update:modelValue'],
props: {
modelValue: Object,
},
components: {
Warning,
FnEditor,
},
inject: ['designer'],
data() {
return {
visible: false,
activeIdx: '',
value: {},
cus: false,
cusValue: '',
handle: '',
};
},
computed: {
t() {
return this.designer.setupState.t;
},
eventNum() {
return Object.keys(this.modelValue || {}).length;
},
},
watch: {
visible(v) {
if (v) {
this.activeIdx = '';
this.value = deepCopy(this.modelValue || {});
}
},
},
methods: {
open() {
this.visible = true;
},
active(idx) {
if (this.activeIdx !== idx) {
this.handle = this.value[idx].handle || '';
this.activeIdx = idx;
}
},
addCus() {
const label = this.cusValue && this.cusValue.trim();
if (label) {
const key = 'var_' + unique();
this.value[key] = {
label,
handle: '',
};
this.active(key);
this.closeCus();
}
},
closeCus() {
this.cus = false;
this.cusValue = '';
},
cusEvent() {
this.cus = true;
},
save() {
if (!this.$refs.editor.save()) {
return false;
}
this.value[this.activeIdx].handle = this.handle;
this.activeIdx = '';
},
rm(key) {
delete this.value[key];
if (key === this.activeIdx) {
this.activeIdx = '';
}
},
close() {
this.activeIdx = '';
},
submit() {
if (this.activeIdx && !this.save()) {
return;
}
this.$emit('update:modelValue', {...this.value});
this.visible = false;
},
},
});
</script>
<style>
._fd-gvc, ._fd-gvc .el-badge {
width: 100%;
}
._fd-gvc .el-button {
font-weight: 400;
width: 100%;
}
._fd-gvc-con .el-main {
padding: 0;
}
._fd-gvc-l, ._fd-gvc-r {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
border: 1px solid var(--fc-line-color-3);
}
._fd-gvc-head {
display: flex;
padding: 5px 15px;
border-bottom: 1px solid var(--fc-line-color-3);
background: var(--fc-bg-color-3);
align-items: center;
}
._fd-gvc-head .el-button.is-link {
color: var(--fc-style-color-1);
}
._fd-gvc-r {
border-left: 0 none;
}
._fd-gvc-r ._fd-gvc-head {
justify-content: flex-end;
}
._fd-gvc-l > .el-main, ._fd-gvc-r > .el-main {
display: flex;
flex-direction: row;
flex: 1;
flex-basis: auto;
box-sizing: border-box;
min-width: 0;
width: 100%;
}
._fd-gvc-r > .el-main {
padding: 0;
flex-direction: column;
}
._fd-gvc-l .el-menu {
padding: 0 10px 5px;
border-right: 0 none;
width: 100%;
border-top: 0 none;
overflow: auto;
}
._fd-gvc-l .el-menu-item.is-active {
background: var(--fc-bg-color-3);
color: var(--fc-text-color-2);
}
._fd-gvc-l .el-menu-item {
height: auto;
line-height: 1em;
border: 1px solid var(--fc-line-color-3);
border-radius: 5px;
padding: 0;
margin-top: 5px;
}
._fd-gvc-method {
display: flex;
flex-direction: column;
justify-content: center;
width: 175px;
font-size: 14px;
font-family: monospace;
overflow: hidden;
white-space: pre-wrap;
color: #923B76;
}
._fd-gvc-method ._fd-label {
margin-top: 4px;
color: var(--fc-text-color-3);
font-size: 12px;
}
._fd-gvc-title {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 10px 0;
}
._fd-gvc-title .el-input {
width: 160px;
}
._fd-gvc-title .fc-icon {
margin-right: 6px;
font-size: 18px;
color: var(--fc-text-color-2);
}
._fd-gvc-title .el-input__wrapper {
box-shadow: none;
}
._fd-gvc-con .el-menu-item.is-active i {
color: var(--fc-text-color-2);
}
</style>

View File

@ -0,0 +1,66 @@
<template>
<div class="_fd-hide-config" :class="{disabled: !!disabled, active: modelValue === activeValue}" @click="onInput">
<template v-if="modelValue === activeValue">
<i class="fc-icon icon-eye"></i> {{ t('props.show') }}
</template>
<template v-else>
<i class="fc-icon icon-eye-close"></i> {{ t('props.hide') }}
</template>
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'HideSwitch',
props: {
modelValue: [String, Boolean, Number],
activeValue: {
type: [String, Boolean, Number],
default: true,
},
inactiveValue: {
type: [String, Boolean, Number],
default: false,
},
disabled: Boolean,
},
events: ['update:modelValue'],
inject: ['designer'],
computed: {
t() {
return this.designer.setupState.t;
}
},
methods: {
onInput() {
if (!this.disabled) {
this.$emit('update:modelValue', this.modelValue !== this.activeValue ? this.activeValue : this.inactiveValue);
}
}
}
});
</script>
<style>
._fd-hide-config {
display: flex;
align-items: center;
cursor: pointer;
color: var(--fc-text-color-2);
}
._fd-hide-config .fc-icon {
margin-right: 3px;
}
._fd-hide-config.active {
color: var(--fc-style-color-1);
}
._fd-hide-config.disabled {
color: var(--fc-text-color-3);
cursor: not-allowed;
}
</style>

View File

@ -0,0 +1,124 @@
<template>
<div class="_fd-html-editor">
<el-button class="_fd-plain-button" plain @click="visible=true">{{ title || t('struct.title') }}</el-button>
<el-dialog class="_fd-html-editor-con" :title="title || t('struct.title')" v-model="visible"
:close-on-click-modal="false" append-to-body width="800px">
<div ref="editor" v-if="visible"></div>
<template #footer>
<div>
<el-button @click="visible = false" size="default">{{ t('props.cancel') }}</el-button>
<el-button type="primary" @click="onOk" size="default">{{ t('props.ok') }}</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import 'codemirror/lib/codemirror.css';
import CodeMirror from 'codemirror/lib/codemirror';
import {defineComponent, markRaw} from 'vue';
import errorMessage from '../utils/message';
export default defineComponent({
name: 'HtmlEditor',
emits: ['update:modelValue'],
props: {
modelValue: String,
title: String,
text: Boolean,
defaultValue: {
require: false
},
},
inject: ['designer'],
computed: {
t() {
return this.designer.setupState.t;
},
},
data() {
return {
editor: null,
visible: false,
oldVal: null,
};
},
watch: {
modelValue() {
this.load();
},
visible(n) {
if (n) {
this.load();
}
}
},
methods: {
validateXML(xmlString) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, 'application/xml');
const parseErrors = xmlDoc.getElementsByTagName('parsererror');
if (parseErrors.length > 0) {
return parseErrors[0].innerText.split('\n')[0] ?? '';
} else {
return '';
}
},
load() {
this.oldVal = this.modelValue;
this.$nextTick(() => {
this.editor = markRaw(CodeMirror(this.$refs.editor, {
lineNumbers: true,
mode: 'xml',
lint: true,
line: true,
tabSize: 2,
lineWrapping: true,
value: this.modelValue || ''
}));
});
},
onOk() {
const str = this.editor.getValue();
if (!this.text && this.validateXML(str)) {
errorMessage(this.t('struct.errorMsg'));
return false;
}
this.visible = false;
if (str !== this.oldVal) {
this.$emit('update:modelValue', str);
}
return true;
},
}
});
</script>
<style>
._fd-html-editor {
width: 100%;
}
._fd-html-editor > .el-button {
font-weight: 400;
width: 100%;
}
._fd-html-editor-con .CodeMirror {
height: 450px;
}
._fd-html-editor-con .CodeMirror-line {
line-height: 16px !important;
font-size: 13px !important;
}
._fd-html-editor-con .CodeMirror-lint-tooltip {
z-index: 2021 !important;
}
._fd-html-editor-con .el-dialog__body {
padding: 0px 20px;
}
</style>

39
src/components/Id.vue Normal file
View File

@ -0,0 +1,39 @@
<template>
<el-input :modelValue="designer ? ('' + (prefix || '') + preview) : modelValue" readonly disabled></el-input>
</template>
<script>
import {defineComponent} from 'vue';
import SnowflakeId from 'snowflake-id';
export default defineComponent({
name: 'FcId',
props: ['modelValue', 'prefix'],
emits: ['update:modelValue'],
inject: {
designer: {
default: null
}
},
data() {
return {
preview: '7379787000000000'
}
},
watch: {
modelValue: {
handler: function (val) {
if (!val) {
const snowflake = new SnowflakeId({
mid: 42,
offset: (2025 - 1970) * 31536000 * 1000
});
this.$emit('update:modelValue', '' + (this.prefix || '') + snowflake.generate());
}
},
immediate: true,
},
}
});
</script>

View File

@ -0,0 +1,49 @@
<template>
<div class="_fc-line-form">
<slot></slot>
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'fcInlineForm',
});
</script>
<style>
._fc-line-form {
width: 100%;
display: flex;
flex-flow: wrap;
align-items: flex-start;
}
.form-create-m ._fc-line-form {
display: flex;
flex-wrap: wrap;
}
.form-create ._fc-line-form .el-col-24, .form-create ._fc-line-form ._fd-drag-tool, .form-create ._fc-line-form ._fd-drag-item, .form-create ._fc-line-form ._fc-line-form {
display: inline-flex;
flex-wrap: wrap;
max-width: 100%;
flex: initial;
width: auto !important;
flex: unset !important;
}
._fc-m-con .form-create ._fc-line-form > .el-col-24 {
width: 100% !important;
}
._fc-line-form .el-form-item {
display: inline-flex;
vertical-align: middle;
}
._fc-line-form .el-select, ._fc-line-form .el-slider {
width: 220px;
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<el-container class="_fc-json-preview">
<el-header height="40px" class="_fc-l-tabs">
<div class="_fc-l-tab"
:class="{active: active==='rule'}"
@click="active='rule'"> {{ t('designer.json') }}
</div>
<div class="_fc-l-tab"
:class="{active: active==='options'}"
@click="active='options'"> {{ t('designer.form') }}
</div>
</el-header>
<el-main style="padding: 8px;">
<StructEditor ref="editor" v-model="value" @blur="handleBlur" @focus="handleFocus" format
style="height:100%;"></StructEditor>
</el-main>
</el-container>
</template>
<script>
import {defineComponent} from 'vue';
import StructEditor from './StructEditor.vue';
import {designerForm} from '../utils/form';
export default defineComponent({
name: 'JsonPreview',
components: {StructEditor},
inject: ['designer'],
data() {
return {
active: 'rule',
value: this.designer.setupState.getRule(),
oldValue: '',
}
},
watch: {
active() {
this.updateValue();
}
},
computed: {
change() {
if (this.active === 'rule') {
return this.designer.setupState.children;
} else {
return this.designer.setupState.formOptions;
}
},
t() {
return this.designer.setupState.t;
},
},
methods: {
updateValue() {
if (this.active === 'rule') {
this.value = this.designer.setupState.getRule();
} else {
this.value = this.designer.setupState.getOptions();
}
},
handleFocus() {
this.oldValue = designerForm.toJson(this.value);
},
handleBlur() {
let str;
if (this.$refs.editor.save() && (str = designerForm.toJson(this.value)) !== this.oldValue) {
if (this.active === 'rule') {
this.designer.setupState.setRule(str);
} else {
this.designer.setupState.setOptions(this.value || {});
}
}
}
},
mounted() {
this.$watch(() => this.change, () => {
this.updateValue();
}, {deep: true});
}
});
</script>
<style>
._fc-json-preview {
display: flex;
width: 100%;
color: var(--fc-text-color-1);
}
._fc-json-preview .CodeMirror {
height: 100%;
font-size: 12px;
}
</style>

View File

@ -0,0 +1,172 @@
<template>
<div class="_fd-page-input">
<template v-for="(page, index) in pageData" :key="page.main ? page.main.name : ''">
<div class="_fd-page-item" :class="{active: page === activePage}" @click="$emit('change', index)">
<div>
<div class="_fd-page-label">
<span>{{ getPageLabel(page) }}</span>
<i class="fc-icon icon-yes" v-if="page === activePage"></i>
</div>
<div class="_fd-page-id" v-if="page.main">
ID{{ page.main.name }} <i @click.stop="copy(page.main.name)" class="fc-icon icon-group"></i>
</div>
</div>
<div class="_fd-page-btns" v-if="!page.default">
<div class="_fd-page-copy" @click.stop="$emit('copy', index)">
<i class="fc-icon icon-copy"></i>
</div>
<div class="_fd-page-del" @click.stop="$emit('delete', index)">
<i class="fc-icon icon-delete"></i>
</div>
</div>
</div>
</template>
<el-dropdown size="default" trigger="click">
<el-button link type="primary">
{{ t('designer.addPage') }}<i class="fc-icon icon-down"></i>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item in containerList" :key="item.name"
@click="$emit('add', item.name)">
{{ getPageName(item) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script>
import {defineComponent} from 'vue';
import {copyTextToClipboard, deepGet} from '../utils';
export default defineComponent({
name: 'PageInput',
inject: ['designer'],
emits: ['add', 'delete', 'change'],
computed: {
activePage() {
return this.designer.setupState.activePage;
},
pageData() {
return this.designer.setupState.pageData;
},
t() {
return this.designer.setupState.t;
},
containerList() {
const dragRuleList = this.designer.setupState.dragRuleList;
return Object.keys(dragRuleList).map(k => {
if (dragRuleList[k].container) {
return dragRuleList[k];
}
}).filter(item => !!item);
}
},
methods: {
copy(name) {
copyTextToClipboard(name);
},
getPageName(item) {
return this.t('com.' + item.name + '.name') || item.label
},
getPageLabel(page) {
return page.default ? this.t('designer.main') : (deepGet(page.main, page.config.labelField, '') || this.getPageName(page.main._menu));
},
}
});
</script>
<style>
._fd-page-item {
display: flex;
justify-content: space-between;
flex-direction: row;
align-items: center;
padding: 12px 0;
margin: 0 12px;
height: 30px;
cursor: pointer;
font-size: 12px;
border-bottom: 1px solid var(--fc-line-color-3);
box-sizing: content-box;
}
._fd-page-item.active ._fd-page-label {
color: var(--fc-style-color-1);
}
._fd-page-btns{
display: flex;
}
._fd-page-label {
font-weight: 600;
color: var(--fc-text-color-1);
}
._fd-page-label .fc-icon {
margin-left: 5px;
font-size: 12px;
}
._fd-page-id {
display: flex;
align-items: center;
color: var(--fc-text-color-3);
font-weight: 400;
}
._fd-page-id .fc-icon {
margin-left: 5px;
}
._fd-page-id .fc-icon:hover {
color: var(--fc-style-color-1);
}
._fd-page-input .el-button {
font-weight: 400;
font-size: 12px;
margin-left: 12px;
margin-top: 12px;
color: var(--fc-style-color-1);
}
._fd-page-input .el-button .fc-icon {
margin-left: 5px;
font-size: 12px;
}
._fd-page-del {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin-left: 4px;
border-radius: 25px;
background-color: var(--fc-style-bg-color-3);
}
._fd-page-del .fc-icon {
color: var(--fc-style-color-3);
font-size: 14px;
}
._fd-page-copy {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 25px;
background-color: var(--fc-style-bg-color-1);
}
._fd-page-copy .fc-icon {
color: var(--fc-style-color-1);
font-size: 14px;
}
</style>

View File

@ -0,0 +1,398 @@
<template>
<div class="_fd-print">
<el-tooltip
effect="dark"
:content="t('designer.print.title')"
placement="top"
:hide-after="0"
>
<i class="fc-icon icon-print" @click="open"></i>
</el-tooltip>
<el-dialog class="_fd-print-dialog _fd-config-dialog" v-model="visible" destroy-on-close
:close-on-click-modal="false"
:title="t('designer.print.title')"
append-to-body
width="1080px">
<el-container class="_fd-print-con" style="height: 600px">
<el-aside style="width:255px;">
<el-container class="_fd-print-l">
<el-header class="_fd-print-head" height="40px">
{{ t('designer.print.config') }}
</el-header>
<el-main>
<el-form label-position="top" size="small">
<el-form-item :label="t('props.mode')">
<el-radio-group v-model="formData.type">
<el-radio-button value="form">{{ t('form.formMode') }}</el-radio-button>
<el-radio-button value="read">{{ t('form.previewMode') }}</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('props.style')">
<el-radio-group v-model="formData.style">
<el-radio-button value="default">{{
t('designer.print.defaultStyle')
}}
</el-radio-button>
<el-radio-button value="word">{{
t('designer.print.wordStyle')
}}
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('style.width')">
<el-input-number :min="300" controls-position="right"
v-model="formData.width"></el-input-number>
</el-form-item>
<template v-for="item in padding" :key="item">
<el-form-item :label="t('designer.print.' + item)">
<el-input-number :min="0" controls-position="right"
v-model="formData[item]"></el-input-number>
</el-form-item>
</template>
</el-form>
</el-main>
</el-container>
</el-aside>
<el-main>
<el-container class="_fd-print-r">
<el-main>
<ViewForm class="_fd-print-form" :class="{'_fd-print-form-word': formData.style === 'word'}"
ref="form" :rule="rule" :option="options"
v-if="visible"
:style="{width: formData.width > 0 ? (formData.width + 'px') : '100%'}">
<template v-for="(_, name) in $slots" #[name]="scope">
<slot :name="name" v-bind="scope ?? {}"/>
</template>
</ViewForm>
</el-main>
</el-container>
</el-main>
</el-container>
<template #footer>
<div>
<el-button size="default" @click="visible=false">{{ t('props.cancel') }}</el-button>
<el-button size="default" @click="print(true)">{{
t('designer.print.export')
}}</el-button>
<el-button type="primary" size="default" @click="print(false)" :loading="printing">{{ t('props.print') }}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import {defineComponent, markRaw} from 'vue';
import Warning from './Warning.vue';
import viewForm from '../utils/form';
import loadjs from '../utils/loadjs/loadjs';
import SizeInput from './style/SizeInput.vue';
export default defineComponent({
name: 'PrintForm',
components: {
SizeInput,
Warning,
ViewForm: viewForm.$form(),
},
inject: ['designer'],
data() {
return {
visible: false,
printing: false,
frame: null,
rule: [],
options: {},
padding: ['top', 'bottom', 'left', 'right'],
formData: {
type: 'form',
style: 'default',
left: 20,
right: 20,
top: 20,
bottom: 20,
width: 780,
},
};
},
computed: {
t() {
return this.designer.setupState.t;
},
},
watch: {
visible(v) {
if (v) {
this.rule = viewForm.parseJson(this.designer.setupState.getPreviewRule());
this.options = viewForm.parseJson(this.designer.setupState.getOptionsJson());
this.options.submitBtn = false;
this.options.resetBtn = false;
} else {
this.printing = false;
if (this.frame) {
document.body.removeChild(this.frame);
this.frame = null;
}
}
},
'formData.type': function (n) {
this.options.preview = n === 'read';
}
},
methods: {
open() {
this.visible = true;
},
disableImageSmoothing(ctx) {
ctx.imageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
},
print(flag) {
this.printing = true;
loadjs.ready(['html2canvas', 'jspdf'], () => {
window.html2canvas(this.$refs.form.$el, {
allowTaint: true,
useCORS: true,
}).then((canvas) => {
const pdf = new window.jspdf.jsPDF({
orientation: 'p',
unit: 'pt',
format: 'a4',
});
this.disableImageSmoothing(canvas.getContext('2d'));
const {
left: marginLeft,
right: marginRight,
top: marginTop,
bottom: marginBottom,
} = this.formData;
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const contentWidth = pageWidth - marginLeft - marginRight;
const contentHeight = pageHeight - marginTop - marginBottom;
const scaledHeight = (canvas.height * contentWidth) / canvas.width;
if (scaledHeight <= contentHeight) {
pdf.addImage(
canvas.toDataURL('image/jpeg'),
'JPEG',
marginLeft,
marginTop,
contentWidth,
scaledHeight
);
} else {
let remainingHeight = scaledHeight;
let page = 0;
const clipHeight = canvas.width * contentHeight / contentWidth;
while (remainingHeight > 0) {
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
this.disableImageSmoothing(tempCtx);
const offsetY = page * clipHeight;
const actualClipHeight = Math.min(clipHeight, canvas.height - offsetY);
tempCanvas.width = canvas.width;
tempCanvas.height = actualClipHeight;
tempCtx.drawImage(
canvas,
0, offsetY, canvas.width, actualClipHeight,
0, 0, canvas.width, actualClipHeight
);
const imageHeight = (actualClipHeight / canvas.height) * scaledHeight;
pdf.addImage(
tempCanvas.toDataURL('image/jpeg'),
'JPEG',
marginLeft,
marginTop,
contentWidth,
imageHeight
);
remainingHeight -= contentHeight;
if (remainingHeight > 0) {
pdf.addPage();
page++;
}
}
}
if (flag) {
this.printing = false;
window.open(URL.createObjectURL(pdf.output('blob')));
} else {
this.printPdf(pdf);
}
}).catch((e) => {
this.printing = false;
});
});
},
printPdf(pdf) {
if (!this.frame) {
const frame = markRaw(document.createElement('iframe'));
frame.style.width = '0';
frame.style.position = 'absolute';
frame.style.height = '0';
frame.style.border = 'none';
frame.onload = function () {
setTimeout(() => {
frame.contentWindow.print();
}, 100);
}
document.body.appendChild(frame);
this.frame = frame;
}
this.frame.src = URL.createObjectURL(pdf.output('blob'));
this.printing = false;
},
},
created() {
if (window.html2canvas) {
loadjs.done('html2canvas');
} else if (!loadjs.isDefined('html2canvas')) {
loadjs.loadNpm('html2canvas@1.4.1/dist/html2canvas.min.js', 'html2canvas');
}
if (window.jspdf) {
loadjs.done('jspdf');
} else if (!loadjs.isDefined('jspdf')) {
loadjs.loadNpm('jspdf@3.0.1/dist/jspdf.umd.js', 'jspdf');
}
}
});
</script>
<style>
._fd-print .el-button {
font-weight: 400;
width: 100%;
}
._fd-print-con .el-main {
padding: 0;
}
._fd-print-l, ._fd-print-r {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
border: 1px solid var(--fc-line-color-3);
}
._fd-print-head {
display: flex;
padding: 5px 15px;
border-bottom: 1px solid var(--fc-line-color-3);
background: var(--fc-bg-color-3);
align-items: center;
}
._fd-print-head .el-button.is-link {
color: var(--fc-style-color-1);
}
._fd-print-r {
border-left: 0 none;
}
._fd-print-r ._fd-print-head {
justify-content: flex-end;
}
._fd-print-l > .el-main, ._fd-print-r > .el-main {
display: flex;
flex-direction: column;
flex: 1;
flex-basis: auto;
box-sizing: border-box;
min-width: 0;
width: 100%;
padding: 10px;
}
._fd-print-l .el-form .el-radio-group, ._fd-print-l .el-form .el-radio-button__inner {
width: 100%;
}
._fd-print-l .el-form .el-radio-button {
flex: 1;
}
._fd-print-r > .el-main {
flex-direction: column;
padding: 20px;
position: relative;
}
._fd-print-form {
padding: 2px;
box-sizing: border-box;
}
._fd-print-form .el-input__wrapper, ._fd-print-form .el-textarea__inner, ._fd-print-form .el-select__wrapper {
box-shadow: none !important;
border: 1px solid var(--el-input-border-color, var(--el-border-color));
}
._fd-print-form .el-select__placeholder {
position: unset !important;
top: unset !important;
transform: unset !important;
}
._fd-print-form .is-disabled .el-input__wrapper {
background-color: unset !important;
}
._fd-print-form .is-disabled .el-input__inner {
color: unset !important;
}
._fd-print-form-word .el-input__wrapper, ._fd-print-form-word .el-textarea__inner, ._fd-print-form-word .el-select__wrapper {
border: none !important;
border-bottom: 1px solid var(--el-input-border-color, var(--el-border-color)) !important;
border-color: inherit !important;
border-radius: 0 !important;
}
._fd-print-form-word .el-input-number__decrease, ._fd-print-form-word .el-input-number__increase {
display: none !important;
}
._fd-print-form-word ._fc-read-view {
display: block;
width: 100%;
height: 1.5em;
padding: 0 4px;
line-height: 1.5em;
border-bottom: 1px solid var(--el-input-border-color, var(--el-border-color)) !important;
border-color: inherit !important;
}
._fd-print-page-line {
position: absolute;
border-bottom: 1px dashed var(--fc-line-color-3);
left: 0;
right: 0;
height: 1px;
color: var(--fc-text-color-3);
padding-left: 4px;
box-sizing: border-box;
font-size: 12px;
line-height: 2em;
z-index: 1;
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<el-input :size="size" v-model="value" @blur="onInput" clearable class="_fd-list-input">
<template #append>
<el-dropdown size="default" trigger="click" :popper-class="popperClass">
<i class="fc-icon icon-setting"></i>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item in options" :key="item.value" @click="setValue(item.value)">
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-input>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'PromptInput',
emits: ['update:modelValue', 'change'],
props: {
size: String,
modelValue: String,
popperClass: String,
options: Array,
},
data() {
return {
value: this.modelValue || '',
}
},
methods: {
setValue(val) {
this.value = val;
this.onInput();
},
onInput() {
this.$emit('update:modelValue', this.value);
this.$emit('change', this.value);
},
},
});
</script>
<style>
._fd-list-input {
width: 100%;
}
._fd-list-input .el-input-group__append {
padding: 0 10px;
}
._fd-list-input .fc-icon {
cursor: pointer;
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<Struct class="_fd-props-input" :modelValue="props" @update:modelValue="onInput" :title="t('designer.customProps')">
<el-tooltip
effect="dark"
:content="t('designer.customProps')"
placement="top"
:hide-after="0"
>
<i class="fc-icon icon-edit"></i>
</el-tooltip>
</Struct>
</template>
<script>
import {defineComponent} from 'vue';
import Struct from './Struct.vue';
import extend from '@form-create/utils/lib/extend';
export default defineComponent({
name: 'PropsInput',
components: {Struct},
inject: ['designer'],
data() {
return {}
},
computed: {
t() {
return this.designer.setupState.t;
},
activeRule() {
return this.designer.setupState.activeRule;
},
props() {
const propsKeys = this.activeRule._fc_store?.props_keys || [];
const props = {};
propsKeys.forEach(k => {
if (this.activeRule.props && this.activeRule.props[k] != null) {
props[k] = this.activeRule.props[k];
}
});
return props;
},
},
methods: {
onInput(props) {
if (!this.activeRule.props) {
this.activeRule.props = {};
}
if (!this.activeRule._fc_store) {
this.activeRule._fc_store = {};
}
Object.keys(this.props).forEach(k => {
if ((props || {})[k] == null) {
delete this.activeRule.props[k];
}
});
extend(this.activeRule.props, props || {});
const keys = Object.keys(props || {});
if (keys.length) {
this.activeRule._fc_store.props_keys = keys;
} else {
delete this.activeRule._fc_store.props_keys;
}
}
}
});
</script>
<style>
._fd-props-input {
flex: 1;
text-align: right;
}
._fd-props-input .fc-icon {
cursor: pointer;
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<div class="_fd-quick-layout">
<el-popover ref="pop" placement="bottom" :width="240" :hide-after="0" trigger="click">
<template #reference>
<div>
<el-tooltip
effect="dark"
:content="t('designer.layout')"
placement="top"
:hide-after="0"
>
<i class="fc-icon icon-layout"></i>
</el-tooltip>
</div>
</template>
<div class="_fd-quick-layout-content">
<template v-for="(item,idx) in layout">
<div @click="change(idx)">
<i class="fc-icon" :class="'icon-column' + (idx + 1)"></i>
<span>{{ item.label }}</span>
</div>
</template>
</div>
</el-popover>
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'QuickLayout',
inject: ['designer'],
data() {
return {}
},
computed: {
t() {
return this.designer.setupState.t;
},
layout() {
return [{
span: 24,
label: this.t('designer.col1')
}, {
span: 12,
label: this.t('designer.col2')
}, {
span: 8,
label: this.t('designer.col3')
}, {
span: 6,
label: this.t('designer.col4')
}];
}
},
methods: {
check(rule) {
if (rule?._menu?.subForm) {
return false;
}
let ctx = rule.__fc__.parent;
while (ctx) {
if (ctx.rule?._menu?.menu === 'layout' ||ctx.rule?._menu?.subForm) {
return false;
}
ctx = ctx.parent;
}
return true;
},
change(idx) {
const models = this.designer.setupState.dragForm.api.model();
const span = this.layout[idx].span;
let flag = false;
Object.keys(models).forEach(key => {
const rules = Array.isArray(models[key]) ? models[key] : [models[key]];
rules.forEach(rule => {
if (this.check(rule)) {
if (!rule.col) {
rule.col = {};
}
flag = flag || rule.col.span !== span;
rule.col.span = span;
}
});
})
this.$refs.pop.hide();
if (flag) {
this.designer.setupState.addOperationRecord();
}
}
}
});
</script>
<style>
._fd-quick-layout-content {
display: grid;
grid-template-columns: repeat(4, 1fr);
width: 100%;
grid-column-gap: 10px;
}
._fd-quick-layout-content > div {
display: flex;
flex-direction: column;
text-align: center;
cursor: pointer;
font-size: 12px;
}
._fd-quick-layout-content > div:hover {
color: var(--fc-style-color-1);
}
._fd-quick-layout-content i {
font-size: 24px;
}
</style>

View File

@ -0,0 +1,75 @@
<template>
<div class="_fd-required">
<el-switch v-model="required"></el-switch>
<LanguageInput v-model="requiredMsg" v-if="required"
:placeholder="t('validate.requiredPlaceholder')"></LanguageInput>
</div>
</template>
<script>
import is from '@form-create/utils/lib/type';
import {defineComponent} from 'vue';
import LanguageInput from './language/LanguageInput.vue';
export default defineComponent({
name: 'Required',
components: {LanguageInput},
emits: ['update:modelValue'],
props: {
modelValue: {}
},
inject: ['designer'],
watch: {
required() {
this.update();
},
requiredMsg() {
this.update();
},
modelValue(n) {
const flag = is.String(n);
this.required = n === undefined ? false : (flag ? true : !!n);
this.requiredMsg = flag ? n : '';
},
},
computed: {
t() {
return this.designer.setupState.t;
},
},
data() {
const flag = is.String(this.modelValue);
return {
required: this.modelValue === undefined ? false : (flag ? true : !!this.modelValue),
requiredMsg: flag ? this.modelValue : ''
};
},
methods: {
update() {
let val;
if (this.required === false) {
val = false;
} else {
val = this.requiredMsg || true;
}
this.$emit('update:modelValue', val);
},
}
});
</script>
<style>
._fd-required {
display: flex;
align-items: center;
width: 100%;
}
._fd-required .el-input {
margin-left: 15px;
}
._fd-required .el-switch {
height: 28px;
}
</style>

25
src/components/Row.vue Normal file
View File

@ -0,0 +1,25 @@
<template>
<el-col :span="24">
<div class="_fd-row el-row" :class="{'_fc-child-empty' : !$slots.default}" v-bind="$attrs">
<slot name="default"></slot>
</div>
</el-col>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'fcRow',
mounted() {
}
});
</script>
<style>
._fd-row {
width: 100%;
}
</style>

View File

@ -0,0 +1,135 @@
<template>
<el-tree-select class="_fd-rule-select" :modelValue="modelValue" @update:modelValue="input" :size="size"
:multiple="multiple" checkStrictly
:showCheckbox="multiple" :data="tree">
<template #default="{ data }">
<template v-if="data.value === '___subform'">
<div class="_fd-rule-select-node">
<div>{{ data.label }}</div>
<span>{{ t('props.subform') }}</span>
</div>
</template>
<template v-else>
{{ data.label }}
</template>
</template>
</el-tree-select>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'RuleSelect',
inject: ['designer'],
emits: ['update:modelValue', 'change'],
props: {
modelValue: [String, Number, Array],
onlyField: Boolean,
valueType: String,
size: String,
multiple: Boolean,
},
computed: {
activeRule() {
return this.designer.setupState.activeRule;
},
t() {
return this.designer.setupState.t;
},
tree() {
const activePage = this.designer.setupState.activePage;
let tree = [];
if (activePage.default) {
tree = this.getFields(this.designer.setupState.treeInfo);
} else {
tree = this.getFields(activePage.main.field && activePage.main === this.activeRule ? this.designer.setupState.treeInfo : this.designer.setupState.treeInfo[0].children);
}
let ctx = this.activeRule?.__fc__.parent;
while (ctx) {
if (ctx.rule === activePage.main) {
ctx = undefined;
} else if (ctx.rule._menu && ['array', 'object', 'scope'].indexOf(ctx.rule._menu.subForm) > -1) {
const subTree = this.getFields(this.designer.setupState.findTree(ctx.rule._fc_id))
if (subTree.length) {
tree.unshift({
value: '___subform',
disabled: true,
label: ctx.refRule?.__$title?.value || ctx.rule.title || ctx.rule._menu.label,
children: subTree
})
}
ctx = undefined;
} else {
ctx = ctx.parent;
}
}
return tree;
}
},
methods: {
getFields(children, parent = []) {
const fields = [];
children.forEach(({rule, children}) => {
const temp = [...parent];
if (rule.field) {
temp.push(rule);
}
const childrenFields = ['array', 'scope'].indexOf(rule._menu.subForm) > -1 ? [] : this.getFields(children || [], temp);
if (!this.onlyField || this.onlyField && rule.field) {
const item = {
value: parent.length ? (parent.map(item => item[this.valueType || '_fc_id']).join('.') + '.' + rule[this.valueType || '_fc_id']) : rule[this.valueType || '_fc_id'],
label: (rule?.__fc__?.refRule?.__$title?.value || rule.title || '').trim() || (rule.props && rule.props.label) || this.t('com.' + (rule._menu && rule._menu.name) + '.name') || (rule._menu && rule._menu.label) || rule.type,
rule,
parent,
};
if (childrenFields.length) {
item.children = childrenFields;
}
fields.push(item);
} else {
fields.push(...childrenFields)
}
});
return fields;
},
input(value) {
this.$emit('update:modelValue', value);
this.$emit('change', value);
},
}
});
</script>
<style>
.el-tree._fd-rule-select {
min-width: 200px;
}
._fd-rule-select .el-tree-node:has(._fd-rule-select-node) {
border-bottom: 1px solid var(--fc-line-color-3);
border-bottom-style: dashed;
padding-bottom: 5px;
}
._fd-rule-select .el-tree-node:has(._fd-rule-select-node) > .el-tree-node__content > .el-checkbox {
display: none;
}
._fd-rule-select .el-tree-node:has(._fd-rule-select-node) > .el-tree-node__content > .el-select-dropdown__item {
padding-right: 20px;
}
._fd-rule-select-node {
display: flex;
justify-content: space-between;
}
._fd-rule-select-node > div {
color: #61affe;
}
._fd-rule-select-node > span {
font-size: 12px;
}
</style>

View File

@ -0,0 +1,144 @@
<template>
<div class="_fc-signature">
<template v-if="modelValue">
<div class="_fc-signature-preview">
<i class="fc-icon icon-delete2" @click="remove"></i>
<img :src="modelValue" alt="signature">
</div>
</template>
<template v-else>
<div class="_fc-signature-btn" @click="visible = true">
<i class="fc-icon icon-edit2"></i> {{ formCreateInject.t('signaturePadTip') || '点击添加手写签名' }}
</div>
</template>
<el-dialog class="_fc-signature-dialog" :title="formCreateInject.t('signaturePadTitle') || '请在虚线框内书写'"
v-model="visible"
destroy-on-close
:close-on-click-modal="false"
append-to-body width="640px">
<canvas class="_fc-signature-pad" ref="pad" width="600px" height="270px"></canvas>
<template #footer>
<div>
<el-button size="default" @click="clear()">{{ formCreateInject.t('reset') || '重置' }}</el-button>
<el-button type="primary" :disabled="isEmpty" @click="submit" size="default">{{ formCreateInject.t('ok') || '确定' }}</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import {defineComponent, markRaw} from 'vue';
import SignaturePad from 'signature_pad';
export default defineComponent({
name: 'SignaturePad',
emits: ['update:modelValue', 'change', 'remove'],
data() {
return {
visible: false,
isEmpty: true,
signaturePad: null,
};
},
props: {
modelValue: String,
penColor: String,
formCreateInject: Object,
},
watch: {
visible(val) {
if (val) {
this.isEmpty = true;
this.$nextTick(() => {
this.signaturePad = markRaw(new SignaturePad(this.$refs.pad, {
penColor: this.penColor,
}));
this.signaturePad.addEventListener('endStroke', () => {
this.isEmpty = this.signaturePad.isEmpty();
});
});
} else {
this.signaturePad.off();
this.signaturePad = null;
}
}
},
methods: {
clear() {
this.signaturePad.clear();
this.isEmpty = true;
},
submit() {
const res = this.signaturePad.toDataURL();
this.updateValue(res);
this.visible = false;
},
updateValue(val) {
this.$emit('update:modelValue', val);
this.$emit('change', val);
},
remove() {
this.updateValue('');
this.$emit('remove');
},
},
});
</script>
<style>
._fc-signature {
width: 100%;
}
._fc-signature-btn, ._fc-signature-preview {
width: 100%;
min-width: 160px;
height: 88px;
line-height: 88px;
font-size: 14px;
color: rgb(201, 204, 216);
border-radius: 4px;
border: 1px dashed rgb(212, 215, 224);
text-align: center;
background: rgb(255, 255, 255);
position: relative;
box-sizing: border-box;
}
._fc-signature-btn {
cursor: pointer;
}
._fc-signature-preview > img {
display: inline-block;
height: 88px;
}
._fc-signature-preview .icon-delete2 {
position: absolute;
top: 9px;
right: 9px;
display: inline-block;
line-height: 14px;
font-size: 14px;
cursor: pointer;
}
._fc-signature-btn i {
font-size: 14px;
}
._fc-signature-dialog .el-dialog__body {
text-align: center;
}
._fc-signature-pad {
border-radius: 4px;
border: 1px dashed #D4D7E0;
background-image: linear-gradient(#FFFFFF 14px, transparent 0), linear-gradient(90deg, #FFFFFF 14px, #D4D7E0 0);
background-size: 15px 15px;
}
</style>

View File

@ -0,0 +1,310 @@
<template>
<div class="_fd-slots-config">
<template v-for="(item, key) in easySlots">
<ConfigItem :label="item.label">
<el-input size="small" v-model="item.value" clearable @blur="onChange">
<template #prepend v-if="!item.only">
<el-select size="small" v-model="item.type" @change="changeType(item)">
<template v-for="name in type">
<el-option :label="t('props.' + name)" :value="name"/>
</template>
</el-select>
</template>
<template #append v-if="item.type === 'icon'">
<el-popover :ref="key" placement="bottom" popper-class="_fd-slots-config-pop" :width="400"
trigger="click">
<div class="_fd-slots-icons">
<template v-for="name in icons">
<div class="_fd-slots-icon" @click="changeIcon(item, name, key)">
<i class="fc-icon iconfont" :class="name"></i>
</div>
</template>
</div>
<template #reference>
<i class="fc-icon icon-menu"></i>
</template>
</el-popover>
</template>
</el-input>
</ConfigItem>
</template>
</div>
</template>
<script>
import {defineComponent} from 'vue';
import uniqueId from '@form-create/utils/lib/unique';
import ConfigItem from './style/ConfigItem.vue';
import {uniqueArray} from '../utils';
export default defineComponent({
name: 'SlotsConfig',
inject: ['designer'],
components: {ConfigItem},
data() {
return {
type: ['icon', 'text'],
easySlots: {}
}
},
computed: {
t() {
return this.designer.setupState.t;
},
slots() {
const menu = this.designer.setupState?.activeRule?._menu || {};
const slots = menu.easySlots || [];
return slots.map(slot => {
if (typeof slot === 'string') {
return {
value: slot,
label: this.t('com.' + menu.name + '.slots.' + slot) || this.t('slots.' + slot) || this.t('props.' + slot) || slot,
}
} else {
const item = {...slot};
if (!item.label) {
item.label = this.t('com.' + menu.name + '.slots.' + slot.value) || this.t('slots.' + slot.value) || this.t('props.' + slot.value) || slot.value;
}
return item;
}
})
},
modelValue() {
return this.designer.setupState?.activeRule?.$easySlots || {};
},
icons() {
return uniqueArray([
...this.designer.setupState.getConfig('icons', []),
'icon-layout',
'icon-column1',
'icon-column2',
'icon-column3',
'icon-column4',
'icon-tab',
'icon-config-event',
'icon-step-form',
'icon-slider',
'icon-dialog',
'icon-justify-spacearound',
'icon-upload',
'icon-copy',
'icon-time-range',
'icon-task-add',
'icon-justify-spacebetween',
'icon-import',
'icon-config-base',
'icon-alignitems-stretch',
'icon-alignitems-flexend',
'icon-check',
'icon-auto',
'icon-calendar',
'icon-config-style',
'icon-config-advanced',
'icon-config-props',
'icon-delete-circle2',
'icon-delete-circle',
'icon-delete',
'icon-direction-rowreverse',
'icon-display-flex',
'icon-drag',
'icon-display-block',
'icon-data',
'icon-edit2',
'icon-edit',
'icon-add-col',
'icon-display-inlineblock',
'icon-config-validate',
'icon-down',
'icon-display-inline',
'icon-eye',
'icon-eye-close',
'icon-preview',
'icon-flex-nowrap',
'icon-folder',
'icon-form-circle',
'icon-flex-wrap',
'icon-form',
'icon-form-item',
'icon-icon',
'icon-image',
'icon-justify-flexstart',
'icon-justify-center',
'icon-justify-stretch',
'icon-link2',
'icon-minus',
'icon-menu2',
'icon-more',
'icon-menu',
'icon-language',
'icon-pad',
'icon-mobile',
'icon-page-max',
'icon-move',
'icon-page-min',
'icon-pre-step',
'icon-pc',
'icon-page',
'icon-refresh',
'icon-radius',
'icon-save-filled',
'icon-question',
'icon-scroll',
'icon-script',
'icon-setting',
'icon-save',
'icon-shadow',
'icon-variable',
'icon-yes',
'icon-shadow-inset',
'icon-date',
'icon-date-range',
'icon-collapse',
'icon-switch',
'icon-subform',
'icon-tree-select',
'icon-value',
'icon-alert',
'icon-card',
'icon-checkbox',
'icon-cascader',
'icon-button',
'icon-data-table',
'icon-group',
'icon-divider',
'icon-flex',
'icon-descriptions',
'icon-html',
'icon-editor',
'icon-input',
'icon-link',
'icon-password',
'icon-radio',
'icon-row',
'icon-inline',
'icon-rate',
'icon-color',
'icon-select',
'icon-json',
'icon-number',
'icon-space',
'icon-table-form',
'icon-table-form2',
'icon-time',
'icon-span',
'icon-textarea',
'icon-tooltip',
'icon-slot',
'icon-transfer',
'icon-tag',
'icon-watermark',
'icon-tree',
'icon-table',
'icon-add-child',
'icon-add2',
'icon-add',
'icon-alignitems-baseline',
'icon-add-circle',
'icon-alignitems-center'
]);
}
},
watch: {
modelValue: {
handler: function (val) {
const easySlots = {};
this.slots.forEach(({value, label, type}) => {
if (val[value]) {
easySlots[value] = {...val[value]};
} else if (this.easySlots[value]) {
easySlots[value] = {
type: this.easySlots[value].type,
};
} else {
easySlots[value] = {
type: type || 'icon',
value: '',
};
}
easySlots[value].only = type;
easySlots[value].label = label;
})
this.easySlots = easySlots;
},
immediate: true,
}
},
methods: {
changeIcon(item, icon, key) {
item.value = icon;
this.onChange();
this.$refs[key][0].hide();
},
changeType(item) {
if (item.value) {
item.value = '';
this.onChange();
}
},
onChange() {
if (this.designer.setupState?.activeRule) {
const easySlots = {};
Object.keys(this.easySlots).forEach(key => {
if (this.easySlots[key].value) {
easySlots[key] = {...this.easySlots[key]};
delete easySlots[key].label;
delete easySlots[key].only;
}
})
if (Object.keys(easySlots).length === 0) {
delete this.designer.setupState.activeRule.$easySlots;
} else {
this.designer.setupState.activeRule.$easySlots = easySlots;
}
this.designer.setupState.activeRule.key = uniqueId();
}
}
},
mounted() {
}
});
</script>
<style>
._fd-slots-config .el-input {
width: 170px;
min-width: 170px;
margin-left: 5px;
}
._fd-slots-config .el-select {
width: 60px;
}
._fd-slots-config .el-select input, ._fd-slots-config .fc-icon {
cursor: pointer;
}
._fd-slots-config .el-input-group__append {
padding: 0 5px;
}
._fd-slots-icons {
display: grid;
grid-template-columns: repeat(13, 1fr);
width: 100%;
grid-gap: 10px;
}
._fd-slots-icon {
text-align: center;
color: var(--fc-text-color-1);
cursor: pointer;
}
._fd-slots-config-pop {
max-height: 320px;
overflow: auto;
}
</style>

View File

@ -0,0 +1,59 @@
<template>
<el-radio-group :modelValue="modelValue" class="_fd-span-input">
<el-radio-button :value="item.value" :label="item.value" v-for="item in layout" :key="item.value"
@click="onInput(item.value)">
{{ item.value === 24 ? t('form.row') : item.label }}
</el-radio-button>
</el-radio-group>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'SpanInput',
props: {
modelValue: [Number, String],
},
inject: ['designer'],
computed: {
t() {
return this.designer.setupState.t;
},
},
data() {
return {
layout: [
{label: '1/4', value: 6},
{label: '1/3', value: 8},
{label: '1/2', value: 12},
{label: '2/3', value: 16},
{label: '3/4', value: 18},
{label: '整行', value: 24},
]
}
},
methods: {
onInput(span) {
this.$emit('update:modelValue', span === this.modelValue ? '' : span);
}
}
});
</script>
<style>
._fd-span-input {
width: 100%;
}
._fd-span-input .el-radio-button__inner {
width: 100%;
padding: 4px;
line-height: 16px;
}
._fd-span-input .el-radio-button {
flex: 1;
}
</style>

141
src/components/Struct.vue Normal file
View File

@ -0,0 +1,141 @@
<template>
<div class="_fd-struct">
<el-badge type="warning" is-dot :hidden="!configured">
<div @click="visible=true">
<slot>
<el-button class="_fd-plain-button" plain size="small">
{{ title || t('struct.title') }}
</el-button>
</slot>
</div>
</el-badge>
<el-dialog class="_fd-struct-dialog _fd-config-dialog" :title="title || t('struct.title')" v-model="visible"
destroy-on-close
:close-on-click-modal="false"
append-to-body width="800px">
<div ref="editor" v-if="visible"></div>
<template #footer>
<div>
<el-button @click="visible = false" size="default">{{ t('props.cancel') }}</el-button>
<el-button type="primary" @click="onOk" size="default">{{ t('props.ok') }}</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import 'codemirror/lib/codemirror.css';
import CodeMirror from 'codemirror/lib/codemirror';
import 'codemirror/mode/javascript/javascript';
import {deepParseFn, toJSON} from '../utils/index';
import {deepCopy} from '@form-create/utils/lib/deepextend';
import {defineComponent, markRaw} from 'vue';
import is from '@form-create/utils/lib/type';
import errorMessage from '../utils/message';
import beautify from 'js-beautify';
export default defineComponent({
name: 'Struct',
emits: ['update:modelValue'],
props: {
modelValue: [Object, Array, Function],
title: String,
defaultValue: {
require: false
},
validate: Function,
},
inject: ['designer'],
computed: {
t() {
return this.designer.setupState.t;
},
configured() {
return !is.empty(this.modelValue) && Object.keys(this.modelValue).length > 0;
},
},
data() {
return {
editor: null,
visible: false,
oldVal: null,
};
},
watch: {
modelValue() {
this.load();
},
visible(n) {
if (n) {
this.load();
}
},
},
methods: {
load() {
const val = toJSON(deepParseFn(this.modelValue ? deepCopy(this.modelValue) : this.defaultValue));
this.oldVal = val;
this.$nextTick(() => {
this.editor = markRaw(CodeMirror(this.$refs.editor, {
lineNumbers: true,
mode: 'javascript',
lint: true,
line: true,
tabSize: 2,
lineWrapping: true,
value: val ? beautify.js(val, {
indent_size: '2',
indent_char: ' ',
max_preserve_newlines: '5',
indent_scripts: 'separate',
}) : '',
}));
});
},
onOk() {
const str = (this.editor.getValue() || '').trim();
let val;
try {
val = (new Function('return ' + str))();
} catch (e) {
console.error(e);
errorMessage(this.t('struct.errorMsg'));
return false;
}
if (this.validate && false === this.validate(val)) {
errorMessage(this.t('struct.errorMsg'));
return false;
}
this.visible = false;
if (toJSON(val, null, 2) !== this.oldVal) {
this.$emit('update:modelValue', val);
}
return true;
},
}
});
</script>
<style>
._fd-struct {
width: 100%;
}
._fd-struct .el-badge {
width: 100%;
}
._fd-struct .el-button {
font-weight: 400;
width: 100%;
}
._fd-struct-dialog .CodeMirror {
height: 500px;
}
._fd-struct-dialog .el-dialog__body {
padding: 0px;
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<div class="_fd-struct-editor">
<div ref="editor"></div>
</div>
</template>
<script>
import 'codemirror/lib/codemirror.css';
import CodeMirror from 'codemirror/lib/codemirror';
import 'codemirror/mode/javascript/javascript';
import {toJSON} from '../utils/index';
import {defineComponent, markRaw} from 'vue';
import errorMessage from '../utils/message';
import {designerForm} from '../utils/form';
import beautify from 'js-beautify';
export default defineComponent({
name: 'StructEditor',
props: {
modelValue: [Object, Array, Function],
format: Boolean,
defaultValue: {
require: false
}
},
emits: ['blur', 'focus', 'update:modelValue'],
inject: ['designer'],
data() {
return {
editor: null,
visible: false,
err: false,
oldVal: null,
};
},
computed: {
t() {
return this.designer.setupState.t;
},
},
watch: {
modelValue(n) {
if (this.editor) {
const val = n ? this.toJson(n) : '';
this.oldVal = val;
const scrollInfo = this.editor.getScrollInfo();
const scrollTop = scrollInfo.top;
this.editor.setValue(val);
this.editor.scrollTo(0, scrollTop);
}
}
},
mounted() {
this.$nextTick(() => {
this.load();
});
},
methods: {
toJson(val) {
return this.format ? designerForm.toJson(val, 2) : toJSON(val);
},
load() {
const val = this.modelValue ? this.toJson(this.modelValue) : '';
this.oldVal = val;
this.$nextTick(() => {
this.editor = markRaw(CodeMirror(this.$refs.editor, {
lineNumbers: true,
mode: 'javascript',
lint: true,
line: true,
tabSize: 2,
lineWrapping: true,
value: val ? beautify.js(val, {
indent_size: '2',
indent_char: ' ',
max_preserve_newlines: '5',
indent_scripts: 'separate',
}) : '',
}));
this.editor.on('blur', () => {
this.$emit('blur');
});
this.editor.on('focus', () => {
this.$emit('focus');
});
});
},
save() {
const str = (this.editor.getValue() || '').trim();
let val;
try {
val = (new Function('return ' + str))();
} catch (e) {
console.error(e);
errorMessage(this.t('struct.errorMsg'));
return false;
}
if (this.validate && false === this.validate(val)) {
this.err = true;
return false;
}
this.visible = false;
if (this.toJson(val) !== this.oldVal) {
this.$emit('update:modelValue', val);
}
return true;
},
}
});
</script>
<style>
._fd-struct-editor {
flex: 1;
width: 100%;
}
._fd-struct-editor > div {
height: 100%;
}
</style>

View File

@ -0,0 +1,90 @@
<template>
<div class="_fc-sublist">
<p class="_fc-r-title">
<span>{{ t('designer.sublist') }}</span>
<i class="fc-icon icon-add-circle"
@click="toolHandle(activeRule ,'addChild')"></i>
</p>
<fcDraggable :group="{name:'sub', pull:'clone', put:false}" :sort="true"
handle=".icon-drag" direction="vertical" :animation="0"
itemKey="_fc_id"
@end="end"
:list="activeRuleChildren">
<template #item="{element,index}">
<ConfigItem>
<template #label>
<i class="fc-icon icon-drag"></i>
<span>{{
(t('com.' + (element._menu.name) + '.name') || activeRule._menu.label) + ' ' + (index + 1)
}}</span>
</template>
<i class="fc-icon icon-copy" @click="toolHandle(element ,'copy')"></i>
<i class="fc-icon icon-delete" @click="toolHandle(element ,'delete')"></i>
<template #append v-if="activeRule._menu.subRender">
<VNode
:fn="()=>subRender(activeRule._menu.subRender, activeRule, element)"></VNode>
</template>
</ConfigItem>
</template>
</fcDraggable>
</div>
</template>
<script>
import {defineComponent} from 'vue';
import ConfigItem from './style/ConfigItem.vue';
import VNode from './VNode.vue';
import fcDraggable from 'vuedraggable/src/vuedraggable';
import uniqueId from '@form-create/utils/lib/unique';
export default defineComponent({
name: 'SubList',
components: {fcDraggable, VNode, ConfigItem},
inject: ['designer'],
computed: {
t() {
return this.designer.setupState.t;
},
activeRuleChildren() {
return this.designer.setupState.activeRuleChildren;
},
activeRule() {
return this.designer.setupState.activeRule;
},
},
methods: {
toolHandle(...args) {
this.designer.setupState.toolHandle(...args);
},
subRender(...args) {
return this.designer.setupState.subRender(...args);
},
end({oldIndex, newIndex}) {
if (oldIndex === newIndex) {
return;
}
const rule = this.activeRule.children.splice(oldIndex, 1);
this.activeRule.children.splice(newIndex, 0, rule[0]);
this.activeRule.key = uniqueId();
},
}
});
</script>
<style>
._fc-sublist ._fc-r-title {
display: flex;
align-items: center;
justify-content: space-between;
}
._fc-sublist .fc-icon {
cursor: pointer;
}
._fc-sublist ._fd-config-item + ._fd-config-item {
margin-top: 8px;
}
</style>

View File

@ -0,0 +1,208 @@
<template>
<div class="_td-table-opt">
<el-table
:data="value"
:key="checked ? '2': '1'"
border
:size="size || 'small'"
style="width: 100%">
<template v-for="(col,idx) in overColumn" :key="col.label + idx">
<el-table-column :label="col.label">
<template #default="scope">
<template v-if="col.value">
<ValueInput :size="size || 'small'" :modelValue="scope.row[col.key]"
@update:modelValue="(n)=>(scope.row[col.key] = n)"
@blur="onInput(scope.row)" @change-type="onInput(scope.row)"></ValueInput>
</template>
<template v-else>
<el-input :size="size || 'small'" :modelValue="scope.row[col.key]"
@update:modelValue="(n)=>(scope.row[col.key] = n)"
@blur="onInput(scope.row)"></el-input>
</template>
</template>
</el-table-column>
</template>
<el-table-column width="35" align="center" fixed="right">
<template #default="scope">
<i class="fc-icon icon-delete" @click="del(scope.$index)"></i>
</template>
</el-table-column>
</el-table>
<div class="_td-table-opt-handle">
<el-button link type="primary" @click="add" v-if="!max || max > value.length">
<i class="fc-icon icon-add"></i> {{ t('tableOptions.add') }}
</el-button>
<el-checkbox v-model="checked" :label="t('tableOptions.keyValue')" v-if="keyValue"/>
</div>
</div>
</template>
<script>
import {defineComponent} from 'vue';
import {copy} from '@form-create/utils/lib/extend';
import ValueInput from './computed/ValueInput.vue';
export default defineComponent({
name: 'TableOptions',
components: {ValueInput},
emits: ['update:modelValue', 'change'],
props: {
modelValue: [Array, Object],
column: {
type: Array,
default: () => [{label: 'label', key: 'label'}, {label: 'value', key: 'value'}]
},
valueType: String,
keyValue: String,
max: Number,
size: String,
},
inject: ['designer'],
watch: {
modelValue() {
this.value = this.tidyModelValue();
}
},
computed: {
t() {
return this.designer.setupState.t;
},
overColumn() {
let column = this.column;
if (this.checked) {
for (let i = 0; i < column.length; i++) {
if (column[i].key === this.keyValue) {
return [column[i]];
}
}
}
return column;
},
},
data() {
return {
value: this.tidyModelValue(),
checked: false,
};
},
created() {
if (this.keyValue) {
this.checked = this.isChecked();
this.$watch('checked', (n) => {
n && this.input();
});
}
},
methods: {
isChecked() {
for (let i = 0; i < this.value.length; i++) {
const item = this.value[i];
const keys = Object.keys(item);
const value = item[this.keyValue];
for (let i = 0; i < keys.length; i++) {
if (value !== item[keys[i]]) {
return false;
}
}
}
return true;
},
tidyModelValue() {
const modelValue = this.modelValue;
if (this.valueType === 'string') {
return (modelValue || []).map(value => {
return {value: '' + value}
})
} else if (this.valueType === 'object') {
return Object.keys((modelValue || {})).map(label => {
return {label, value: modelValue[label]}
})
} else {
return [...modelValue || []].map(v => {
return copy(v);
});
}
},
tidyValue() {
if (this.valueType === 'object') {
const obj = {};
this.value.forEach(v => {
if (v.label && v.value) {
obj[v.label] = v.value;
}
})
return obj;
} else {
return this.value.map(v => {
if (this.valueType === 'string') {
return v.value;
}
if (this.checked) {
const value = v[this.keyValue];
return this.column.reduce((item, col) => {
item[col.key] = value;
return item;
}, {});
} else {
return {...v}
}
});
}
},
onInput(item) {
if (this.column.length === 1 && '' === item[this.column[0].key]) {
return;
}
const flag = this.column.every(v => {
if (v.required === false) {
return true;
}
if (['object', 'string'].indexOf(this.valueType) > -1) {
return item[v.key] !== undefined && item[v.key] !== '' && item[v.key] !== null;
}
return item[v.key] !== undefined;
})
if (flag) {
this.input();
}
},
input() {
const value = this.tidyValue();
this.$emit('update:modelValue', value);
this.$emit('change', value);
},
add() {
this.value.push(this.column.reduce((initial, v) => {
initial[v.key] = '';
return initial;
}, {}));
},
del(idx) {
this.value.splice(idx, 1);
this.input();
}
}
});
</script>
<style>
._td-table-opt {
width: 100%;
}
._td-table-opt .icon-delete {
cursor: pointer;
}
._td-table-opt .el-table {
z-index: 1;
}
._td-table-opt-handle {
display: flex;
justify-content: space-between;
align-items: center;
padding-right: 5px;
}
</style>

138
src/components/ToolsBar.vue Normal file
View File

@ -0,0 +1,138 @@
<template>
<div class="_fc-r-tools-bar" v-if="tools.length > 2">
<div class="_fc-r-tools">
<template v-for="item in tools" :key="item.icon">
<el-tooltip
effect="dark"
:content="item.label"
placement="bottom"
persistent
:hide-after="0"
>
<div class="_fc-r-tool" @click="onClick(item.icon)">
<i class="fc-icon" :class="`icon-config-${item.icon}`"></i>
</div>
</el-tooltip>
</template>
<div class="_fc-r-tools-close _fc-r-tool" @click="clearActiveRule">
<i class="fc-icon icon-add2"></i>
</div>
</div>
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'ToolsBar',
inject: ['designer'],
computed: {
t() {
return this.designer.setupState.t;
},
tools() {
const vm = this.designer.setupState;
const tools = [];
if (!vm.activeRule && !vm.customForm.config) {
return tools;
}
if (vm.baseForm.isShow) {
tools.push({
label: this.t('designer.rule'),
icon: 'base'
})
}
if (vm.propsForm.isShow || (vm.customForm.isShow && vm.customForm.propsShow)) {
tools.push({
label: this.t('designer.props'),
icon: 'props'
})
}
if (vm.advancedForm.isShow) {
tools.push({
label: this.t('designer.advanced'),
icon: 'advanced'
})
}
if (vm.styleForm.isShow) {
tools.push({
label: this.t('designer.style'),
icon: 'style'
})
}
if (vm.eventShow) {
tools.push({
label: this.t('designer.event'),
icon: 'event'
})
}
if (vm.validateForm.isShow) {
tools.push({
label: this.t('designer.validate'),
icon: 'validate'
})
}
return tools;
},
},
methods: {
onClick(icon) {
document.querySelector(`#_fd-config-${icon}`).scrollIntoView({
block: 'start',
inline: 'nearest',
behavior: 'smooth'
})
},
clearActiveRule() {
this.designer.setupState.clearActiveRule();
},
}
});
</script>
<style>
._fc-r-tools-bar {
height: 30px;
}
._fc-r-tools-close {
position: absolute;
right: 5px;
transform: rotate(45deg);
color: var(--fc-text-color-2);
}
._fc-r-tools {
display: flex;
border-top: 1px solid var(--fc-line-color-3);
position: absolute;
left: 0;
right: 0;
padding: 0 10px;
align-items: center;
}
._fc-r-tool {
display: flex;
width: 28px;
height: 28px;
align-items: center;
justify-content: center;
cursor: pointer;
}
._fc-r-tool:hover {
color: var(--fc-style-color-1);
}
._fc-r-tool .fc-icon {
font-size: 22px;
}
._fc-r-tools-close .fc-icon {
font-size: 18px;
}
</style>

View File

@ -0,0 +1,226 @@
<template>
<div class="_fd-tree-opt">
<el-tree
:data="value"
node-key="index"
:key="checked ? '1' : '2'"
:indent="5"
:expand-on-click-node="false">
<template #default="{ node, data }">
<div class="_fd-tree-opt-node">
<template v-if="!checked">
<el-input class="_fd-tree-opt-first" v-model="data[overColumns.label]"
@blur="change"/>
<ValueInput class="_fd-tree-opt-last" v-model="data[overColumns.value]" @blur="change"
@change-type="change">
<template #append>
<div class="_fd-tree-opt-btn" @click="add(node, data)">
<i class="fc-icon icon-add"></i>
</div>
<div class="_fd-tree-opt-btn" @click="append(data)">
<i class="fc-icon icon-add-child"></i>
</div>
<div class="_fd-tree-opt-btn _fd-tree-opt-danger" @click="remove(node, data)">
<i class="fc-icon icon-delete"></i>
</div>
</template>
</ValueInput>
</template>
<template v-else>
<el-input class="_fd-tree-opt-last _label" v-model="data[keyValue]" @blur="change">
<template #append>
<div class="_fd-tree-opt-btn" @click="add(node, data)">
<i class="fc-icon icon-add"></i>
</div>
<div class="_fd-tree-opt-btn" @click="append(data)">
<i class="fc-icon icon-add-child"></i>
</div>
<div class="_fd-tree-opt-btn _fd-tree-opt-danger" @click="remove(node, data)">
<i class="fc-icon icon-delete"></i>
</div>
</template>
</el-input>
</template>
</div>
</template>
</el-tree>
<el-checkbox v-if="keyValue" v-model="checked" :label="t('tableOptions.keyValue')"/>
</div>
</template>
<script>
import {defineComponent} from 'vue';
import {deepCopy} from '@form-create/utils/lib/deepextend';
import ValueInput from './computed/ValueInput.vue';
export default defineComponent({
name: 'TreeOptions',
components: {ValueInput},
emits: ['update:modelValue'],
props: {
modelValue: Array,
columns: Object,
keyValue: String,
},
inject: ['designer'],
data() {
return {
value: [...deepCopy(this.modelValue || [])],
checked: false,
};
},
computed: {
t() {
return this.designer.setupState.t;
},
overColumns() {
if (!this.columns) {
return {
label: 'label',
value: 'value',
};
}
return {
label: this.columns.label || 'label',
value: this.columns.value || 'value',
}
}
},
created() {
if (!this.value.length) {
this.value = [{}]
}
if (this.keyValue) {
this.checked = this.isChecked();
this.$watch('checked', (n) => {
n && this.change();
});
}
},
methods: {
isChecked() {
const deepCheck = (list) => {
for (let i = 0; i < list.length; i++) {
const item = list[i];
if (item[this.overColumns.label] !== item[this.overColumns.value]) {
return false;
}
if (item.children && !deepCheck(item.children)) {
return false;
}
}
return true;
}
return deepCheck(this.modelValue || []);
},
tidyValue() {
const deepTidy = (list) => {
let tmp = [];
list.map(v => {
const val = v[this.keyValue];
const item = {
[this.overColumns.label]: val,
[this.overColumns.value]: val,
};
tmp.push(item)
if (v.children) {
item.children = deepTidy(v.children);
}
});
return tmp;
}
if (this.checked && this.keyValue) {
return deepTidy(this.value);
} else {
return deepCopy(this.value);
}
},
change() {
this.$emit('update:modelValue', this.tidyValue());
},
add(node) {
const parent = node.parent;
const children = parent.data.children || parent.data;
children.push({});
},
append(data) {
if (!data.children) {
data.children = [];
}
data.children.push({});
},
remove(node, data) {
const parent = node.parent;
if (parent.data.children) {
parent.data.children.splice(parent.data.children.indexOf(data), 1);
if (!parent.data.children.length) {
delete parent.data.children;
}
} else {
parent.data.splice(parent.data.indexOf(data), 1);
}
this.change();
},
}
});
</script>
<style>
._fd-tree-opt ._fd-tree-opt-btn {
height: 19px;
width: 18px;
color: #fff;
text-align: center;
line-height: 20px;
padding-bottom: 1px;
float: left;
cursor: pointer;
justify-content: center;
background-color: var(--fc-style-color-1);
}
._fd-tree-opt-node {
display: flex;
align-items: center;
}
._fd-tree-opt-first {
width: 60px;
margin-right: 5px;
}
._fd-tree-opt-last {
width: 165px;
}
._fd-tree-opt-last._label {
width: 175px;
}
._fd-tree-opt-last._label .el-input-group__append {
width: 65px;
}
._fd-tree-opt ._fd-tree-opt-danger {
background-color: var(--fc-style-color-3);
border-radius: 0 2px 2px 0;
}
._fd-tree-opt .el-tree-node__content {
margin-bottom: 3px;
height: 28px;
}
._fd-tree-opt .el-input__inner {
border-right: 0 none;
}
._fd-tree-opt .el-input-group__append {
width: 90px;
padding-right: 2px;
padding-left: 1px;
background: var(--fc-bg-color-1);
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<el-dropdown class="_fd-type-select" trigger="click" size="default" popper-class="_fd-type-select-pop"
:disabled="!menus.length" @command="handleCommand">
<el-tag type="success" effect="plain" disable-transitions>
<template v-if="activeRule">
{{ t('com.' + (activeRule._menu.name) + '.name') || activeRule._menu.label }} <i
class="fc-icon icon-down" v-if="menus.length"></i>
</template>
<template v-else>
{{
t('com.' + (customForm.config.name) + '.name') || customForm.config.label || customForm.config.name
}}
</template>
</el-tag>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="item" v-for="item in menus" :key="item.name">
<div><i class="fc-icon" :class="item.icon || 'icon-input'"></i>{{ t('com.' + (item.name) + '.name') || item.label }}</div>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'TypeSelect',
inject: ['designer'],
computed: {
t() {
return this.designer.setupState.t;
},
activeRule() {
return this.designer.setupState.activeRule;
},
customForm() {
return this.designer.setupState.customForm;
},
menus() {
let menus = [];
const designer = this.designer.setupState;
if (this.activeRule) {
const name = this.activeRule._menu.name;
const switchConfig = designer.getConfig('switchType', []);
if (switchConfig === false) {
return menus;
}
let switchs = [];
switchConfig.forEach(lst => {
if (lst.indexOf(name) > -1) {
switchs.push(...lst);
}
});
switchs = switchs.filter((key, idx) => {
return key !== name && switchs.indexOf(key) === idx;
});
if (switchs.length) {
designer.menuList.forEach(item => {
item.list.forEach(menu => {
if (switchs.indexOf(menu.name) > -1) {
menus.push(menu);
}
});
});
} else {
designer.menuList.forEach(item => {
if (item.name === this.activeRule._menu.menu) {
item.list.forEach(menu => {
if (menu.name !== name) {
menus.push(menu);
}
});
}
});
}
}
return menus.filter(menu => this.designer.setupState.hiddenItem.indexOf(menu.name) === -1);
}
},
methods: {
handleCommand(item) {
let activeRule = this.activeRule;
let rule = this.activeRule;
if (!rule._menu.inside) {
rule = rule.__fc__.parent.rule;
}
const children = rule.__fc__.parent.rule.children;
const replaceRule = this.designer.setupState.makeRule(item);
let newRule = replaceRule;
if (replaceRule.type === 'DragTool') {
newRule = replaceRule.children[0];
}
if (newRule.field && activeRule.field) {
['title', 'info', 'field', 'validate', 'computed', 'control', '$required', 'style'].forEach(k => {
newRule[k] = activeRule[k];
});
} else if (activeRule?.computed?.hidden) {
newRule.computed = {hidden: activeRule.computed.hidden}
}
if (activeRule.name) {
newRule.name = activeRule.name;
}
['name', 'wrap', 'class', 'id', 'control', 'on'].forEach(k => {
if (activeRule[k]) {
newRule[k] = activeRule[k];
}
})
children.splice(children.indexOf(rule), 1, replaceRule);
this.$nextTick(() => {
this.designer.setupState.triggerActive(newRule);
});
}
}
});
</script>
<style>
._fd-type-select {
cursor: pointer;
}
._fd-type-select.is-disabled {
cursor: default;
}
._fd-type-select .fc-icon {
font-size: 14px;
}
._fd-type-select-pop {
max-height: 500px;
overflow: auto;
}
._fd-type-select-pop .fc-icon {
font-size: 14px;
}
</style>

19
src/components/VNode.vue Normal file
View File

@ -0,0 +1,19 @@
<script>
import {defineComponent, Fragment, h} from 'vue';
export default defineComponent({
name: 'VNode',
props: {
fn: Function
},
render() {
const vnode = this.fn();
if (Array.isArray(vnode)) {
return h(Fragment, {}, vnode)
} else {
return vnode;
}
}
});
</script>

246
src/components/Validate.vue Normal file
View File

@ -0,0 +1,246 @@
<template>
<div class="_fd-validate">
<template v-for="(item, idx) in validate">
<div class="_fd-validate-item">
<div class="_fd-validate-title">
<div>
<span>{{ idx + 1 }}</span>
{{ modes[item.mode] }}
</div>
<i class="fc-icon icon-delete-circle" @click="remove(idx)"></i>
</div>
<el-row>
<el-col :span="getSpan(item)">
<el-form-item :label="t('validate.mode')">
<el-select v-model="item.trigger" @change="onInput">
<el-option
v-for="item in triggers"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="getSpan(item)">
<el-form-item :label="modes[item.mode]">
<template v-if="item.mode === 'pattern'">
<PatternInput v-model="item[item.mode]" @change="onInput"></PatternInput>
</template>
<template v-else-if="item.mode === 'validator'">
<FnInput v-model="item[item.mode]" name="validator"
:args="['rule', 'value', 'callback']"
@change="onInput">{{ t('validate.modes.validator') }}
</FnInput>
</template>
<template v-else>
<el-input-number v-model="item[item.mode]" @change="onInput"></el-input-number>
</template>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item :label="t('validate.message')">
<LanguageInput v-model="item.message" :placeholder="t('validate.requiredPlaceholder')"
@change="onInput">
</LanguageInput>
</el-form-item>
</el-col>
</el-row>
</div>
</template>
<el-dropdown trigger="click" size="default" popper-class="_fd-validate-pop" @command="handleCommand">
<el-button class="_fd-validate-btn _fd-plain-button" plain size="small">{{ t('validate.rule') }} +
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="value" v-for="(label, value) in modes" :key="value">
<div>{{ label }}</div>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script>
import {defineComponent} from 'vue';
import {localeOptions} from '../utils';
import PatternInput from './computed/PatternInput.vue';
import FnInput from './FnInput.vue';
import {deepCopy} from '@form-create/utils/lib/deepextend';
import LanguageInput from './language/LanguageInput.vue';
export default defineComponent({
name: 'Validate',
inject: ['designer'],
emits: ['update:modelValue'],
props: {
modelValue: Array,
},
components: {
LanguageInput,
FnInput,
PatternInput,
},
watch: {
modelValue(n) {
this.validate = this.parseValue(n || []);
}
},
data() {
return {
validate: this.parseValue(this.modelValue || []),
};
},
computed: {
t() {
return this.designer.setupState.t;
},
modes() {
const activeRule = this.designer.setupState.activeRule;
if (activeRule && activeRule._menu.subForm === 'object') {
return {
validator: this.t('validate.modes.validator'),
}
} else {
return {
min: this.t('validate.modes.min'),
max: this.t('validate.modes.max'),
len: this.t('validate.modes.len'),
pattern: this.t('validate.modes.pattern'),
validator: this.t('validate.modes.validator'),
}
}
},
triggers() {
return localeOptions(this.t, [
{label: 'blur', value: 'blur'},
{label: 'change', value: 'change'},
{label: 'submit', value: 'submit'},
]);
}
},
methods: {
handleCommand(mode) {
this.validate.push({
transform: new Function('val', 'this.type = val == null ? \'string\' : (Array.isArray(val) ? \'array\' : (typeof val)); return val;'),
mode,
trigger: 'blur'
});
},
autoMessage(item) {
const title = this.designer.setupState.activeRule.title;
if (this.designer.setupState.activeRule) {
item.message = this.t('validate.autoRequired', {title})
this.onInput();
}
},
getSpan(item) {
return ['pattern', 'validator', 'required'].indexOf(item.mode) > -1 ? 24 : 12;
},
onInput: function () {
this.$emit('update:modelValue', this.validate.map(item => {
item = {...item};
if (!item.message) {
delete item.message;
}
return item;
}));
},
remove(idx) {
this.validate.splice(idx, 1);
this.onInput();
},
parseValue(val) {
return deepCopy(val.map(v => {
if (v.validator) {
v.mode = 'validator';
}
if (!v.mode) {
Object.keys(v).forEach(k => {
if (['message', 'type', 'trigger', 'mode'].indexOf(k) < 0) {
v.mode = k;
}
});
}
return v;
}));
}
}
});
</script>
<style>
._fd-validate {
display: flex;
flex-direction: column;
width: 100%;
}
._fd-validate-btn {
font-weight: 400;
width: 100%;
}
._fd-validate-pop .el-dropdown-menu__item {
width: 248px;
}
._fd-validate-item {
border-bottom: 1px dashed var(--fc-line-color-3);
margin-bottom: 10px;
}
._fd-validate-item .el-col-12:first-child {
padding-right: 5px;
}
._fd-validate-item .el-col-12 + .el-col-12 {
padding-left: 5px;
}
._fd-validate-item .el-input-number {
width: 100%;
}
._fd-validate-title {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 10px;
}
._fd-validate-title > div {
display: flex;
align-items: center;
}
._fd-validate-title > div > span {
width: 16px;
height: 16px;
background: var(--fc-bg-color-3);
text-align: center;
font-size: 12px;
line-height: 16px;
border-radius: 15px;
margin-right: 5px;
}
._fd-validate-title i {
cursor: pointer;
}
._fd-validate-title i:hover {
color: var(--fc-style-color-3);
}
._fd-validate .append-msg {
cursor: pointer;
}
._fd-validate .el-input-group__append {
padding: 0 10px;
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<el-tooltip
effect="dark"
placement="top-start"
popper-class="_fd-warning-pop"
>
<template #content>
<span v-html="tooltip"></span>
</template>
<template v-if="$slots.default">
<span class="_fd-warning-text">
<slot></slot>
</span>
</template>
<template v-else>
<i class="fc-icon icon-question"></i>
</template>
</el-tooltip>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'Warning',
props: {
tooltip: String,
},
data() {
return {}
},
});
</script>
<style>
._fd-warning-pop {
max-width: 400px;
}
._fd-warning-text {
text-decoration: underline;
text-decoration-style: dashed;
cursor: help;
}
</style>

View File

@ -0,0 +1,431 @@
<template>
<div class="_fd-ai-chat">
<div class="_fd-ai-chat-header">
<div class="_fc-l-label">
<i class="fc-icon icon-ai bright"></i>
FormCreate {{ t('ai.name') }}
</div>
<div class="_fc-l-info">
{{ t('ai.info') }}
</div>
<div class="_fd-ai-chat-prompt">
<span>{{ t('ai.try') }}</span>
<span class="_fd-ai-chat-refresh" @click="refresh"> <i
class="fc-icon icon-refresh2"></i>{{ t('ai.change') }}</span>
</div>
<template v-for="(item) in pageData">
<div class="_fd-ai-chat-question" @click="message = item">
<span>{{ item }}</span>
<i class="fc-icon icon-down"></i>
</div>
</template>
</div>
<div class="_fd-ai-chat-history">
<template v-for="(item,idx) in history" :key="idx">
<div class="_fd-ai-chat-history-item" ref="chat">
<div class="_fd-ai-chat-history-chat">
<div>{{ item.message }}</div>
</div>
<div class="_fd-ai-chat-history-status" :class="item.status || 'success'">
<div>
<template v-if="item.status === 'loading'">
<div>{{ t('ai.loading') }}</div>
</template>
<template v-else-if="item.status === 'fail'">
<div><i class="fc-icon icon-warning"></i>{{ t('ai.fail') }}</div>
</template>
<template v-else>
<div><i class="fc-icon icon-yes"></i>{{ t('ai.success') }}</div>
</template>
</div>
</div>
</div>
</template>
</div>
<div class="_fd-ai-chat-input">
<div class="_fd-ai-chat-clear">
<el-button size="small" text round @click="clear"><i class="fc-icon icon-delete2"></i> 清空</el-button>
</div>
<el-input type="textarea" v-model="message" :placeholder="t('ai.placeholder')" resize="none"></el-input>
<div class="fc-icon icon-suspend" v-if="chat && chat.status === 'loading'" @click="suspend"></div>
<div class="fc-icon icon-send" :class="{disabled: !message || !message.trim()}" v-else @click="send"></div>
</div>
</div>
</template>
<script>
// +-----------------------------------------------------------------------
// | FormCreate [ ]
// +----------------------------------------------------------------------
// | Copyright (c) 2018~2025 https://form-create.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed FormCreate使
// +----------------------------------------------------------------------
// | Author: FormCreate Team <admin@form-create.com>
// +----------------------------------------------------------------------
import {defineComponent} from 'vue';
import {message} from '../../utils/message';
export default defineComponent({
name: 'AiChat',
inject: ['designer'],
data() {
return {
message: '',
page: 0,
limit: 3,
pageData: [],
question: [
'生成一个就诊满意度问卷表单',
'创建一个建议收集表单,包含联系人、联系邮箱、分类和建议内容',
'追加一个用户信息表单',
'添加一个标签组件,显示文本为 "Tag"',
'删除商品简介字段',
'当单选框选择 "选项1" 时,显示输入框组件',
'设置输入框为必填并限制长度必须大于13',
'商品价格字段使用数字输入框组件',
'给输入类组件补充占位提示文本placeholder'
],
chat: null,
history: []
}
},
computed: {
t() {
return this.designer.setupState.t;
},
api() {
return this.designer.props.config?.ai?.api || 'https://api.form-create.com/ai/v1/chat/form';
},
token() {
return this.designer.props.config?.ai?.token;
},
},
methods: {
refresh() {
if (this.page * this.limit < this.question.length) {
this.page++;
} else {
this.page = 1;
}
const startIndex = (this.page - 1) * this.limit;
const endIndex = startIndex + this.limit;
this.pageData = this.question.slice(startIndex, endIndex);
},
send() {
const message = (this.message || '').trim()
if (!message) {
return;
}
this.chat = {
message,
status: 'loading'
};
this.history.push(this.chat);
this.$nextTick(() => {
this.$refs.chat[this.$refs.chat.length - 1].scrollIntoView({block: 'end'});
})
this.message = '';
this.fetch()
},
suspend() {
this.chat.status = 'success';
this.chat = null;
},
fetch() {
fetch(this.api, {
method: 'POST',
headers: {
'Authorization': this.token,
'Content-Type': 'application/json'
},
body: JSON.stringify({
ui: 'element-plus',
message: this.chat.message,
rule: this.designer.setupState.getJson()
})
}).then((res) => {
res.json().then(res => {
if (this.chat) {
if (res.status === 200) {
this.chat.status = 'success';
this.designer.setupState.setRule(res.data.rule);
} else {
this.chat.status = 'fail';
res.message && message(res.message);
}
this.chat = null;
}
})
}).catch(() => {
this.chat = null;
})
},
getHistory() {
const data = localStorage.getItem('fc_ai_history');
if (data) {
this.history = JSON.parse(data)
}
},
clear() {
this.history = [];
localStorage.removeItem('fc_ai_history');
},
},
created() {
this.getHistory();
this.refresh();
},
mounted() {
if (this.$refs.chat) {
this.$nextTick(() => {
this.$refs.chat[this.$refs.chat.length - 1].scrollIntoView({block: 'end'});
})
}
},
unmounted() {
localStorage.setItem('fc_ai_history', JSON.stringify(this.history));
}
});
</script>
<style>
._fd-ai-chat {
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
padding: 12px;
font-size: 12px;
}
._fd-ai-chat-header {
border-radius: 6px;
border: 1px solid var(--fc-line-color-3);
}
._fd-ai-chat-prompt {
display: flex;
justify-content: space-between;
padding: 8px 12px 10px;
}
._fd-ai-chat-prompt .fc-icon {
font-size: 12px;
margin-right: 2px;
}
._fd-ai-chat-refresh {
color: var(--fc-style-color-1);
cursor: pointer;
}
._fd-ai-chat-question {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--fc-bg-color-2);
border-radius: 6px;
padding: 7px 10px;
margin: 0 12px 12px;
color: var(--fc-text-color-2);
cursor: pointer;
}
._fd-ai-chat-question + ._fd-ai-chat-question {
margin-top: 6px;
}
._fd-ai-chat-question .fc-icon {
margin-left: 12px;
}
._fd-ai-chat-question .icon-down:before {
display: inline-block;
transform: rotate(-90deg);
}
._fd-ai-chat-history {
display: flex;
flex-direction: column;
flex: 1;
margin-top: 12px;
overflow: auto;
}
._fd-ai-chat-history-chat {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-bottom: 10px;
}
._fd-ai-chat-history-chat > div {
background: var(--fc-style-bg-color-1);
border-radius: 6px 0px 6px 6px;
color: var(--fc-style-color-1);
padding: 10px;
white-space: pre-wrap;
max-width: 70%;
}
._fd-ai-chat-history-status {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 10px;
}
._fd-ai-chat-history-status > div {
position: relative;
background: var(--fc-bg-color-2);
border-radius: 0px 6px 6px 6px;
color: var(--fc-text-color-2);
padding: 10px;
max-width: 70%;
overflow: hidden;
}
._fd-ai-chat-history-status > div > div {
display: flex;
align-items: center;
position: relative;
z-index: 1;
}
._fd-ai-chat-history-status.loading > div:after {
content: '';
background: linear-gradient(0deg, var(--fc-style-color-1) 0%, var(--fc-style-color-4) 100%);
padding: 1px;
position: absolute;
top: -2px;
left: -2px;
bottom: -2px;
right: -2px;
animation: rotate-animation 3s linear infinite;
filter: blur(5px);
}
._fd-ai-chat-history-status.loading > div {
padding: 1px;
border-radius: 0px 6px 6px 6px;
}
._fd-ai-chat-history-status.loading > div > div {
background-color: var(--fc-bg-color-1);
border-radius: 0px 6px 6px 6px;
padding: 7px 10px;
}
._fd-ai-chat-history-status.success > div {
background: var(--fc-style-bg-color-2);
color: var(--fc-style-color-2);
}
._fd-ai-chat-history-status .fc-icon {
width: 15px;
height: 15px;
background: var(--fc-style-color-2);
color: #fff;
border-radius: 15px;
font-size: 14px;
margin-right: 5px;
text-align: center;
}
._fd-ai-chat-history-status.fail > div {
background: var(--fc-style-bg-color-3);
color: var(--fc-style-color-3);
}
._fd-ai-chat-history-status.fail .fc-icon {
background: var(--fc-style-color-3);
}
._fd-ai-chat-input {
width: 100%;
border-radius: 6px;
position: relative;
margin-top: 12px;
}
._fd-ai-chat-clear {
text-align: right;
margin-bottom: 6px;
}
._fd-ai-chat-clear .el-button {
padding: 0;
margin-right: 2px;
}
._fd-ai-chat-clear .fc-icon {
font-size: 13px;
}
._fd-ai-chat-input .el-textarea {
position: relative;
padding: 1px;
box-sizing: border-box;
height: 120px;
}
._fd-ai-chat-input .el-textarea:before {
content: "";
background: linear-gradient(135deg, var(--fc-style-color-1), var(--fc-style-color-4));
border-radius: 6px;
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
}
._fd-ai-chat-input .el-textarea__inner {
height: 100%;
resize: none;
box-shadow: none;
border-radius: 6px;
background: var(--fc-bg-color-1);
}
._fd-ai-chat-input > .fc-icon {
display: flex;
align-items: center;
justify-content: center;
border-radius: 15px;
position: absolute;
right: 12px;
bottom: 12px;
cursor: pointer;
width: 20px;
height: 20px;
background: var(--fc-style-color-1);;
color: #fff;
text-align: center;
font-size: 14px;
}
._fd-ai-chat-input .icon-suspend {
background: linear-gradient(90deg, var(--fc-style-color-1) 0%, var(--fc-style-color-4) 100%);
}
._fd-ai-chat-input .disabled {
background: var(--fc-text-color-3);
}
@keyframes rotate-animation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>

View File

@ -0,0 +1,46 @@
<template>
<audio
:key="src"
:controls="controls"
:autoplay="autoplay"
:loop="loop"
:preload="preload"
:muted="muted"
@pause="$emit('pause', $event)"
@play="$emit('play', $event)"
@ended="$emit('ended', $event)"
>
<source
:src="src"
:type="type"
/>
Your browser does not support the audio element.
</audio>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'AudioBox',
emits: ['pause', 'play', 'ended'],
data() {
return {};
},
props: {
src: String,
type: String,
controls: {
type: Boolean,
default: true,
},
autoplay: Boolean,
loop: Boolean,
preload: {
type: String,
default: 'auto',
},
muted: Boolean,
},
});
</script>

View File

@ -0,0 +1,60 @@
<template>
<img class="_fc-barcode" ref="bar"/>
</template>
<script>
import {defineComponent} from 'vue';
import JsBarcode from 'jsbarcode'
export default defineComponent({
name: 'BarCodeBox',
data() {
return {};
},
props: {
value: String,
format: String,
displayValue: {
type: Boolean,
default: true,
},
fontSize: Number,
textPosition: String,
textAlign: String,
textMargin: Number,
width: {
type: Number,
default: 2,
},
height: {
type: Number,
default: 50,
},
background: String,
lineColor: String,
},
methods: {},
computed: {},
components: {},
watch: {
'$props': {
handler() {
const value = this.value;
const options = {};
Object.keys(this.$props).forEach((key) => {
if (this.$props[key] != null && this.$props[key] !== '') {
options[key] = this.$props[key];
}
});
delete options.value;
delete options.formCreateInject;
this.$nextTick(() => {
JsBarcode(this.$refs.bar, value || '', options);
});
},
deep: true,
immediate: true,
}
},
});
</script>

View File

@ -0,0 +1,69 @@
<template>
<div class="_fc-title" :class="size || 'h2'" :style="textStyle">
{{ title }}
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'FcTitle',
data() {
return {};
},
props: {
title: String,
size: String,
align: String,
},
computed: {
textStyle() {
return {
textAlign: this.align || 'left',
}
}
}
});
</script>
<style>
._fc-title {
width: 100%;
font-size: 16px;
font-weight: 600;
margin-top: 1em;
margin-bottom: 16px;
}
._fc-title.h1, ._fc-title.h2 {
padding-bottom: .3em;
border-bottom: 1px solid #eee
}
._fc-title.h1 {
font-size: 32px;
line-height: 1.2
}
._fc-title.h2 {
font-size: 24px;
line-height: 1.225
}
._fc-title.h3 {
font-size: 20px;
line-height: 1.43
}
._fc-title.h4 {
font-size: 16px;
}
._fc-title.h5 {
font-size: 14px;
}
._fc-title.h6 {
font-size: 12px;
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<iframe
class="_fc-iframe-box"
:src="src"
frameborder="0"
@load="$emit('load', $event)"
></iframe>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'IframeBox',
emits: ['load'],
data() {
return {};
},
props: {
src: String,
loading: String,
},
});
</script>
<style>
._fc-iframe-box {
width: 100%;
}
</style>

View File

@ -0,0 +1,785 @@
<template>
<div class="_fc-markdown" v-html="html">
</div>
</template>
<script>
import {defineComponent, markRaw} from 'vue';
import {Marked} from 'marked';
export default defineComponent({
name: 'FcMarkdown',
data() {
return {
marked: markRaw(new Marked()),
html: '',
};
},
props: {
content: String,
},
watch: {
content: {
handler() {
this.html = this.marked.parse(this.content || '');
},
immediate: true,
}
}
});
</script>
<style>
._fc-markdown {
color-scheme: light;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
margin: 0;
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
width: 100%;
}
._fc-markdown details,
._fc-markdown figcaption,
._fc-markdown figure {
display: block;
}
._fc-markdown summary {
display: list-item;
}
._fc-markdown [hidden] {
display: none !important;
}
._fc-markdown a {
background-color: transparent;
color: #0969da;
text-decoration: none;
}
._fc-markdown abbr[title] {
border-bottom: none;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
._fc-markdown b,
._fc-markdown strong {
font-weight: 600;
}
._fc-markdown dfn {
font-style: italic;
}
._fc-markdown h1 {
margin: .67em 0;
font-weight: 600;
padding-bottom: .3em;
font-size: 2em;
border-bottom: 1px solid #d1d9e0b3;
}
._fc-markdown mark {
background-color: #fff8c5;
color: #1f2328;
}
._fc-markdown small {
font-size: 90%;
}
._fc-markdown sub,
._fc-markdown sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
._fc-markdown sub {
bottom: -0.25em;
}
._fc-markdown sup {
top: -0.5em;
}
._fc-markdown img {
border-style: none;
max-width: 100%;
box-sizing: content-box;
}
._fc-markdown code,
._fc-markdown kbd,
._fc-markdown pre,
._fc-markdown samp {
font-family: monospace;
font-size: 1em;
}
._fc-markdown figure {
margin: 1em 2.5rem;
}
._fc-markdown hr {
box-sizing: content-box;
overflow: hidden;
background: transparent;
border-bottom: 1px solid #d1d9e0b3;
height: .25em;
padding: 0;
margin: 1.5rem 0;
background-color: #d1d9e0;
border: 0;
}
._fc-markdown input {
font: inherit;
margin: 0;
overflow: visible;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
._fc-markdown [type=button],
._fc-markdown [type=reset],
._fc-markdown [type=submit] {
-webkit-appearance: button;
appearance: button;
}
._fc-markdown [type=checkbox],
._fc-markdown [type=radio] {
box-sizing: border-box;
padding: 0;
}
._fc-markdown [type=number]::-webkit-inner-spin-button,
._fc-markdown [type=number]::-webkit-outer-spin-button {
height: auto;
}
._fc-markdown [type=search]::-webkit-search-cancel-button,
._fc-markdown [type=search]::-webkit-search-decoration {
-webkit-appearance: none;
appearance: none;
}
._fc-markdown ::-webkit-input-placeholder {
color: inherit;
opacity: .54;
}
._fc-markdown ::-webkit-file-upload-button {
-webkit-appearance: button;
appearance: button;
font: inherit;
}
._fc-markdown a:hover {
text-decoration: underline;
}
._fc-markdown ::placeholder {
color: #59636e;
opacity: 1;
}
._fc-markdown hr::before {
display: table;
content: "";
}
._fc-markdown hr::after {
display: table;
clear: both;
content: "";
}
._fc-markdown table {
border-spacing: 0;
border-collapse: collapse;
display: block;
width: max-content;
max-width: 100%;
overflow: auto;
font-variant: tabular-nums;
}
._fc-markdown td,
._fc-markdown th {
padding: 0;
}
._fc-markdown details summary {
cursor: pointer;
}
._fc-markdown a:focus,
._fc-markdown [role=button]:focus,
._fc-markdown input[type=radio]:focus,
._fc-markdown input[type=checkbox]:focus {
outline: 2px solid #0969da;
outline-offset: -2px;
box-shadow: none;
}
._fc-markdown a:focus:not(:focus-visible),
._fc-markdown [role=button]:focus:not(:focus-visible),
._fc-markdown input[type=radio]:focus:not(:focus-visible),
._fc-markdown input[type=checkbox]:focus:not(:focus-visible) {
outline: solid 1px transparent;
}
._fc-markdown a:focus-visible,
._fc-markdown [role=button]:focus-visible,
._fc-markdown input[type=radio]:focus-visible,
._fc-markdown input[type=checkbox]:focus-visible {
outline: 2px solid #0969da;
outline-offset: -2px;
box-shadow: none;
}
._fc-markdown a:not([class]):focus,
._fc-markdown a:not([class]):focus-visible,
._fc-markdown input[type=radio]:focus,
._fc-markdown input[type=radio]:focus-visible,
._fc-markdown input[type=checkbox]:focus,
._fc-markdown input[type=checkbox]:focus-visible {
outline-offset: 0;
}
._fc-markdown kbd {
display: inline-block;
padding: 0.25rem;
font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
line-height: 10px;
color: #1f2328;
vertical-align: middle;
background-color: #f6f8fa;
border: solid 1px #d1d9e0b3;
border-bottom-color: #d1d9e0b3;
border-radius: 6px;
box-shadow: inset 0 -1px 0 #d1d9e0b3;
}
._fc-markdown h1,
._fc-markdown h2,
._fc-markdown h3,
._fc-markdown h4,
._fc-markdown h5,
._fc-markdown h6 {
margin-top: 1.5rem;
margin-bottom: 1rem;
font-weight: 600;
line-height: 1.25;
}
._fc-markdown h2 {
font-weight: 600;
padding-bottom: .3em;
font-size: 1.5em;
border-bottom: 1px solid #d1d9e0b3;
}
._fc-markdown h3 {
font-weight: 600;
font-size: 1.25em;
}
._fc-markdown h4 {
font-weight: 600;
font-size: 1em;
}
._fc-markdown h5 {
font-weight: 600;
font-size: .875em;
}
._fc-markdown h6 {
font-weight: 600;
font-size: .85em;
color: #59636e;
}
._fc-markdown p {
margin-top: 0;
margin-bottom: 10px;
}
._fc-markdown blockquote {
margin: 0;
padding: 0 1em;
color: #59636e;
border-left: .25em solid #d1d9e0;
}
._fc-markdown ul,
._fc-markdown ol {
margin-top: 0;
margin-bottom: 0;
padding-left: 2em;
}
._fc-markdown ol ol,
._fc-markdown ul ol {
list-style-type: lower-roman;
}
._fc-markdown ul ul ol,
._fc-markdown ul ol ol,
._fc-markdown ol ul ol,
._fc-markdown ol ol ol {
list-style-type: lower-alpha;
}
._fc-markdown dd {
margin-left: 0;
}
._fc-markdown tt,
._fc-markdown code,
._fc-markdown samp {
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
font-size: 12px;
}
._fc-markdown pre {
margin-top: 0;
margin-bottom: 0;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
font-size: 12px;
word-wrap: normal;
}
._fc-markdown input::-webkit-outer-spin-button,
._fc-markdown input::-webkit-inner-spin-button {
margin: 0;
appearance: none;
}
._fc-markdown::before {
display: table;
content: "";
}
._fc-markdown::after {
display: table;
clear: both;
content: "";
}
._fc-markdown > *:first-child {
margin-top: 0 !important;
}
._fc-markdown > *:last-child {
margin-bottom: 0 !important;
}
._fc-markdown a:not([href]) {
color: inherit;
text-decoration: none;
}
._fc-markdown p,
._fc-markdown blockquote,
._fc-markdown ul,
._fc-markdown ol,
._fc-markdown dl,
._fc-markdown table,
._fc-markdown pre,
._fc-markdown details {
margin-top: 0;
margin-bottom: 1rem;
}
._fc-markdown blockquote > :first-child {
margin-top: 0;
}
._fc-markdown blockquote > :last-child {
margin-bottom: 0;
}
._fc-markdown h1:hover .anchor,
._fc-markdown h2:hover .anchor,
._fc-markdown h3:hover .anchor,
._fc-markdown h4:hover .anchor,
._fc-markdown h5:hover .anchor,
._fc-markdown h6:hover .anchor {
text-decoration: none;
}
._fc-markdown h1 tt,
._fc-markdown h1 code,
._fc-markdown h2 tt,
._fc-markdown h2 code,
._fc-markdown h3 tt,
._fc-markdown h3 code,
._fc-markdown h4 tt,
._fc-markdown h4 code,
._fc-markdown h5 tt,
._fc-markdown h5 code,
._fc-markdown h6 tt,
._fc-markdown h6 code {
padding: 0 .2em;
font-size: inherit;
}
._fc-markdown summary h1,
._fc-markdown summary h2,
._fc-markdown summary h3,
._fc-markdown summary h4,
._fc-markdown summary h5,
._fc-markdown summary h6 {
display: inline-block;
}
._fc-markdown summary h1,
._fc-markdown summary h2 {
padding-bottom: 0;
border-bottom: 0;
}
._fc-markdown ul.no-list,
._fc-markdown ol.no-list {
padding: 0;
list-style-type: none;
}
._fc-markdown ol[type="a s"] {
list-style-type: lower-alpha;
}
._fc-markdown ol[type="A s"] {
list-style-type: upper-alpha;
}
._fc-markdown ol[type="i s"] {
list-style-type: lower-roman;
}
._fc-markdown ol[type="I s"] {
list-style-type: upper-roman;
}
._fc-markdown ol[type="1"] {
list-style-type: decimal;
}
._fc-markdown div > ol:not([type]) {
list-style-type: decimal;
}
._fc-markdown ul ul,
._fc-markdown ul ol,
._fc-markdown ol ol,
._fc-markdown ol ul {
margin-top: 0;
margin-bottom: 0;
}
._fc-markdown li > p {
margin-top: 1rem;
}
._fc-markdown li + li {
margin-top: .25em;
}
._fc-markdown dl {
padding: 0;
}
._fc-markdown dl dt {
padding: 0;
margin-top: 1rem;
font-size: 1em;
font-style: italic;
font-weight: 600;
}
._fc-markdown dl dd {
padding: 0 1rem;
margin-bottom: 1rem;
}
._fc-markdown table th {
font-weight: 600;
}
._fc-markdown table th,
._fc-markdown table td {
padding: 6px 13px;
border: 1px solid #d1d9e0;
}
._fc-markdown table td > :last-child {
margin-bottom: 0;
}
._fc-markdown table tr {
background-color: #ffffff;
border-top: 1px solid #d1d9e0b3;
}
._fc-markdown table tr:nth-child(2n) {
background-color: #f6f8fa;
}
._fc-markdown table img {
background-color: transparent;
}
._fc-markdown img[align=right] {
padding-left: 20px;
}
._fc-markdown img[align=left] {
padding-right: 20px;
}
._fc-markdown .emoji {
max-width: none;
vertical-align: text-top;
background-color: transparent;
}
._fc-markdown span.frame {
display: block;
overflow: hidden;
}
._fc-markdown span.frame > span {
display: block;
float: left;
width: auto;
padding: 7px;
margin: 13px 0 0;
overflow: hidden;
border: 1px solid #d1d9e0;
}
._fc-markdown span.frame span img {
display: block;
float: left;
}
._fc-markdown span.frame span span {
display: block;
padding: 5px 0 0;
clear: both;
color: #1f2328;
}
._fc-markdown span.align-center {
display: block;
overflow: hidden;
clear: both;
}
._fc-markdown span.align-center > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: center;
}
._fc-markdown span.align-center span img {
margin: 0 auto;
text-align: center;
}
._fc-markdown span.align-right {
display: block;
overflow: hidden;
clear: both;
}
._fc-markdown span.align-right > span {
display: block;
margin: 13px 0 0;
overflow: hidden;
text-align: right;
}
._fc-markdown span.align-right span img {
margin: 0;
text-align: right;
}
._fc-markdown span.float-left {
display: block;
float: left;
margin-right: 13px;
overflow: hidden;
}
._fc-markdown span.float-left span {
margin: 13px 0 0;
}
._fc-markdown span.float-right {
display: block;
float: right;
margin-left: 13px;
overflow: hidden;
}
._fc-markdown span.float-right > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: right;
}
._fc-markdown code,
._fc-markdown tt {
padding: .2em .4em;
margin: 0;
font-size: 85%;
white-space: break-spaces;
background-color: #818b981f;
border-radius: 6px;
}
._fc-markdown code br,
._fc-markdown tt br {
display: none;
}
._fc-markdown del code {
text-decoration: inherit;
}
._fc-markdown samp {
font-size: 85%;
}
._fc-markdown pre code {
font-size: 100%;
}
._fc-markdown pre > code {
padding: 0;
margin: 0;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
._fc-markdown .highlight {
margin-bottom: 1rem;
}
._fc-markdown .highlight pre {
margin-bottom: 0;
word-break: normal;
}
._fc-markdown .highlight pre,
._fc-markdown pre {
padding: 1rem;
overflow: auto;
font-size: 85%;
line-height: 1.45;
color: #1f2328;
background-color: #f6f8fa;
border-radius: 6px;
}
._fc-markdown pre code,
._fc-markdown pre tt {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
._fc-markdown [data-footnote-ref]::before {
content: "[";
}
._fc-markdown [data-footnote-ref]::after {
content: "]";
}
._fc-markdown [role=button]:focus:not(:focus-visible),
._fc-markdown [role=tabpanel][tabindex="0"]:focus:not(:focus-visible),
._fc-markdown button:focus:not(:focus-visible),
._fc-markdown summary:focus:not(:focus-visible),
._fc-markdown a:focus:not(:focus-visible) {
outline: none;
box-shadow: none;
}
._fc-markdown [tabindex="0"]:focus:not(:focus-visible),
._fc-markdown details-dialog:focus:not(:focus-visible) {
outline: none;
}
._fc-markdown g-emoji {
display: inline-block;
min-width: 1ch;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1em;
font-style: normal !important;
font-weight: 400;
line-height: 1;
vertical-align: -0.075em;
}
._fc-markdown g-emoji img {
width: 1em;
height: 1em;
}
._fc-markdown .task-list-item {
list-style-type: none;
}
._fc-markdown .task-list-item label {
font-weight: 400;
}
._fc-markdown .task-list-item.enabled label {
cursor: pointer;
}
._fc-markdown .task-list-item + .task-list-item {
margin-top: 0.25rem;
}
._fc-markdown .task-list-item .handle {
display: none;
}
._fc-markdown .task-list-item-checkbox {
margin: 0 .2em .25em -1.4em;
vertical-align: middle;
}
._fc-markdown ul:dir(rtl) .task-list-item-checkbox {
margin: 0 -1.6em .25em .2em;
}
._fc-markdown ol:dir(rtl) .task-list-item-checkbox {
margin: 0 -1.6em .25em .2em;
}
</style>

View File

@ -0,0 +1,61 @@
<template>
<div class="_fc-qrcode" ref="qr"/>
</template>
<script>
import {defineComponent, markRaw} from 'vue';
import QRCodeStyling from 'qr-code-styling';
export default defineComponent({
name: 'QrCodeBox',
data() {
return {
qrcode: null,
};
},
props: {
data: String,
image: String,
width: Number,
height: Number,
circleType: String,
circleColor: String,
},
methods: {},
computed: {},
components: {},
watch: {
'$props': {
handler() {
const options = {
dotsOptions: {}
};
Object.keys(this.$props).forEach((key) => {
if (this.$props[key] != null && this.$props[key] !== '') {
options[key] = this.$props[key];
}
});
delete options.formCreateInject;
if (options.circleType) {
options.dotsOptions.type = options.circleType;
}
if (options.circleColor) {
options.dotsOptions.color = options.circleColor;
}
delete options.circleColor;
delete options.circleType;
this.$nextTick(() => {
if (this.qrcode) {
this.qrcode.update(options);
} else {
this.qrcode = markRaw(new QRCodeStyling(options));
this.qrcode.append(this.$refs.qr);
}
});
},
deep: true,
immediate: true,
}
},
});
</script>

View File

@ -0,0 +1,82 @@
<template>
<video
ref="video"
class="_fc-video-box"
:controls="controls"
:loop="loop"
@pause="$emit('pause', $event)"
@play="$emit('play', $event)"
@ended="$emit('ended', $event)"
></video>
</template>
<script>
import loadjs from '../../utils/loadjs/loadjs';
import {defineComponent} from 'vue';
export default defineComponent({
name: 'VideoBox',
emits: ['pause', 'play', 'ended', 'error'],
data() {
return {
player: null,
};
},
props: {
src: String,
type: String,
controls: {
type: Boolean,
default: true,
},
autoplay: Boolean,
isLive: Boolean,
withCredentials: Boolean,
loop: Boolean
},
watch: {
src: {
handler: function () {
this.$nextTick(() => {
loadjs.ready('mpegts', () => {
const video = this.$refs.video;
const player = window.mpegts.createPlayer({
isLive: this.isLive,
type: this.type,
url: this.src,
});
player.attachMediaElement(video);
player.on('error', (e) => {
this.$emit('error', e);
});
player.load();
if (this.autoplay) {
player.play().catch((e) => {
this.$emit('error', e);
});
}
this.player = player;
})
})
},
immediate: true
}
},
created() {
if (window.mpegts) {
loadjs.done('mpegts');
} else if (!loadjs.isDefined('mpegts')) {
loadjs.loadNpm('mpegts.js@1.8.0/dist/mpegts.js', 'mpegts');
}
},
});
</script>
<style>
._fc-video-box {
width: 100%;
}
</style>

View File

@ -0,0 +1,23 @@
<template>
<div ref="cell" class="_fc-cell">
<slot name="default"></slot>
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'FcCell',
});
</script>
<style>
._fc-cell {
display: inline-block;
}
._fc-cell .el-input-number, ._fc-cell .el-select, ._fc-cell .el-slider, ._fc-cell .el-cascader, ._fc-cell .el-date-editor {
width: 100%;
}
</style>

View File

@ -0,0 +1,84 @@
<template>
<div ref="cell" class="_fd-cell" :class="{'is-new': isNew}" :style="style">
<div v-bind="$attrs" style="height: 100%;width: 100%;">
<slot name="default"></slot>
</div>
</div>
</template>
<script>
import {defineComponent, nextTick} from 'vue';
export default defineComponent({
name: 'FcCell',
inheritAttrs: false,
data() {
return {
isNew: false,
}
},
computed: {
style() {
const style = this.$attrs.style || {};
const css = {
'--fc-cell-display': style.display || 'block',
'--fc-cell-flexDirection': style.flexDirection || 'inherit',
'--fc-cell-flexWrap': style.flexWrap || 'inherit',
'--fc-cell-alignContent': style.alignContent || 'inherit',
'--fc-cell-justifyContent': style.justifyContent || 'inherit',
'--fc-cell-alignItems': style.alignItems || 'inherit',
}
if (style.height) {
css.height = style.height || 'auto';
}
if (style.width) {
css.width = style.width || 'auto';
}
return css;
}
},
mounted() {
this.isNew = this.$el.parentNode.classList.contains('_fd-drag-item');
if (this.isNew) {
this.$watch('$attrs.style.width', (n) => {
nextTick(() => {
this.$el.parentNode.style.width = n ? n : '100%';
})
}, {
immediate: true,
})
}
}
});
</script>
<style>
._fd-cell {
display: inline-block;
}
._fd-cell.is-new {
width: 100% !important;
}
._fd-cell > div {
box-sizing: border-box;
}
._fd-cell > div > ._fd-drag-tool > ._fd-drag-box, ._fd-cell > div > ._fd-drag-tool.is-inline {
display: var(--fc-cell-display) !important;
flex-direction: var(--fc-cell-flexDirection) !important;
flex-wrap: var(--fc-cell-flexWrap) !important;
align-content: var(--fc-cell-alignContent) !important;
justify-content: var(--fc-cell-justifyContent) !important;
align-items: var(--fc-cell-alignItems) !important;
}
._fd-cell > div > ._fd-drag-tool > ._fd-drag-box > ._fd-drag-item:has( > ._fd-drag-tool >.el-col-24, >.el-col-24), ._fd-cell > div > ._fd-drag-item:has( > ._fd-drag-tool >.el-col-24, >.el-col-24) {
flex: none;
}
._fd-cell .el-input-number, ._fd-cell .el-select, ._fd-cell .el-slider, ._fd-cell .el-cascader, ._fd-cell .el-date-editor {
width: 100%;
}
</style>

View File

@ -0,0 +1,684 @@
<template>
<div class="_fd-computed">
<el-badge type="warning" is-dot :hidden="!configured">
<el-button class="_fd-plain-button" plain @click="visible=true" size="small">{{ btn || title }}</el-button>
</el-badge>
<el-dialog class="_fd-comp-dialog _fd-config-dialog" :title="title" v-model="visible" destroy-on-close
:close-on-click-modal="false" append-to-body
width="980px">
<el-tabs class="_fd-preview-tabs" v-model="status" v-if="type !== 'value'">
<el-tab-pane name="condition">
<template #label>
{{ type === 'linkage' ? t('computed.value.title') : t('computed.condition') }}
<Warning :tooltip="t('warning.computedCondition')"></Warning>
</template>
</el-tab-pane>
<el-tab-pane name="computed">
<template #label>
{{ t('computed.name') }}
<Warning :tooltip="t('warning.computedFormula')"></Warning>
</template>
</el-tab-pane>
</el-tabs>
<el-container class="_fd-comp-condition" v-if="status === 'condition'">
<el-main>
<div class="_fd-comp-title">
{{ t('computed.setting') }}
</div>
<ConditionGroup v-if="visible" v-model="condition" ref="condition"/>
<template v-if="type === 'linkage'">
<div class="_fd-comp-title" style="margin-top: 30px;">
{{ t('computed.linkage.trigger') }}
</div>
<div class="_fd-comp-linkage">
{{ t('computed.linkage.info.0') }}
<RuleSelect v-model="linkage" size="small" onlyField valueType="field"
clearable></RuleSelect>
{{ t('computed.linkage.info.1') }}
</div>
</template>
<template v-else>
<div class="_fd-comp-title" style="margin-top: 30px;">
{{ t('computed.invert') }}
</div>
<el-radio-group v-model="invert">
<el-radio :value="true">{{ invertLabel }}</el-radio>
<el-radio :value="false">{{ validLabel }}</el-radio>
</el-radio-group>
</template>
</el-main>
</el-container>
<el-container class="_fd-comp-con" v-else>
<el-aside>
<el-tree
ref="treeRef"
:data="treeInfo"
:default-expanded-keys="expandedKeys"
:expand-on-click-node="false"
node-key="id"
:indent="10"
@nodeClick="nodeClick"
>
<template #default="{ node, data }">
<div class="_fd-comp-node"
:class="{disabled: data.disabled}"
@mouseover="nodeOver(data)">
<div>
<template v-if="data.rule">
<span v-if="data.rule._menu.subForm === 'object'"
class="_group">{ {{ t('props.group') }} }</span>
<span v-if="data.rule._menu.subForm === 'array'"
class="_subform">[ {{ t('props.collection') }} ]</span>
</template>
<span>{{
data.rule ? getTitle(data.rule) : (data.label || '').trim()
}}</span>
</div>
<span class="_fd-comp-id" v-if="data.rule" @click.stop="setField(data)">
ID
</span>
</div>
</template>
</el-tree>
</el-aside>
<el-main>
<el-container class="_fd-comp-r">
<el-header class="_fd-comp-head" height="40px">
{{ name || title }}
</el-header>
<el-main>
<div ref="editor" class="_fd-comp-script" v-if="visible"></div>
</el-main>
<div class="_fd-comp-info" v-if="formulaInfo || err">
<div v-if="formulaInfo">{{ t('computed.formulaInfo') }}: {{ formulaInfo }}</div>
<div v-if="formulaExample">{{ t('computed.formulaExample') }}: {{ formulaExample }}</div>
<div v-if="err" style="color: #f56c6c;">{{ t('validate.message') }}: {{ err }}</div>
</div>
</el-container>
</el-main>
</el-container>
<template #footer>
<div>
<el-button @click="visible=false" size="default">{{ t('props.cancel') }}</el-button>
<el-button type="primary" @click="submit" size="default">{{
t('props.ok')
}}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import 'codemirror/lib/codemirror.css';
import CodeMirror from 'codemirror/lib/codemirror';
import {defineComponent, markRaw} from 'vue';
import {formulaInfo, formulaTree} from '../../utils/formulas';
import {addAutoKeyMap, escapeRegExp} from '../../utils';
import ConditionGroup from './ConditionGroup.vue';
import is from '@form-create/utils/lib/type';
import {deepCopy} from '@form-create/utils/lib/deepextend';
import RuleSelect from '../RuleSelect.vue';
import Warning from '../Warning.vue';
export default defineComponent({
name: 'ComputedConfig',
components: {Warning, RuleSelect, ConditionGroup},
props: {
modelValue: [String, Object, Array],
type: String,
title: String,
name: String,
btn: String,
validLabel: String,
invertLabel: String,
},
inject: ['designer'],
data() {
const getFields = (children, field, disabled, parent = []) => {
const fields = [];
children.forEach(({rule, children}) => {
const temp = [...parent];
let _disabled = disabled;
if (rule.field) {
temp.push(rule);
if (!_disabled) {
_disabled = (rule.field === field && this.type === 'value');
}
}
const childrenFields = getFields(children || [], field, _disabled, temp);
if (rule.field) {
const item = {
value: rule.field,
label: rule?.__fc__?.refRule?.__$title?.value || rule.title,
rule,
parent,
formula: true,
// disabled: _disabled
};
if (childrenFields.length) {
item.children = childrenFields;
}
fields.push(item);
} else {
fields.push(...childrenFields)
}
});
return fields;
}
return {
editor: null,
visible: false,
expandedKeys: [
'_form', '_formula', '_subform'
],
err: '',
status: 'computed',
value: '',
condition: undefined,
formulaInfo: '',
formulaExample: '',
oldValue: '',
invert: false,
linkage: '',
getFields,
};
},
computed: {
t() {
return this.designer.setupState.t;
},
configured() {
return !!this.modelValue
},
activeRule() {
return this.designer.setupState.activeRule;
},
treeInfo() {
let ctx = this.activeRule?.__fc__.parent;
const activePage = this.designer.setupState.activePage;
let subTree = [];
if (activePage.default) {
subTree = this.getFields(this.designer.setupState.treeInfo, this.activeRule.field);
} else {
subTree = this.getFields(activePage.main.field && activePage.main === this.activeRule ? this.designer.setupState.treeInfo : this.designer.setupState.treeInfo[0].children, this.activeRule.field);
}
const tree = [
{
id: '_form',
label: this.t('computed.form'),
children: subTree
},
{
id: '_formula',
label: this.t('computed.formula'),
children: formulaTree.map(item => {
return {
label: this.t('formula.' + item.key),
children: item.children.map(label => {
return {
label,
info: this.t('formula.' + label),
example: formulaInfo[label] || '',
formula: true,
};
})
}
})
}
];
while (ctx) {
if (ctx.rule === activePage.main) {
ctx = undefined;
} else if (ctx.rule._menu && ['array', 'object'].indexOf(ctx.rule._menu.subForm) > -1) {
const subTree = this.getFields(this.designer.setupState.findTree(ctx.rule._fc_id), this.activeRule.field)
if (subTree.length) {
tree.unshift({
id: '_subform',
label: ctx?.refRule?.__$title?.value || ctx.rule.title || ctx.rule._menu.label,
children: subTree
})
}
ctx = undefined;
} else {
ctx = ctx.parent;
}
}
return tree;
}
},
watch: {
visible(v) {
if (v) {
this.update();
}
},
status(n) {
if (n === 'computed') {
this.load();
}
},
},
beforeUnmount() {
document.querySelector('._fd-comp-script') && document.querySelector('._fd-comp-script').removeEventListener('mouseover', this.spanOver);
},
methods: {
update() {
this.linkage = '';
if (this.type === 'value' || (this.modelValue && is.String(this.modelValue))) {
this.status = 'computed';
this.load();
this.condition = undefined;
} else {
this.status = 'condition';
this.condition = this.modelValue ? deepCopy(this.modelValue) : undefined;
if (this.condition) {
this.invert = this.condition.invert === true;
this.linkage = this.condition.linkage || '';
}
}
},
getTitle(rule) {
return (rule?.__fc__?.refRule?.__$title?.value || rule.title || '').trim() || (rule._menu && rule._menu.label) || rule.field || rule._fc_id;
},
setField(data) {
if (data.disabled === true) {
return;
}
this.markRule(`"${data.rule.field}"`, this.getTitle(data.rule), 'id');
},
spanOver(e) {
if (e.target.classList.contains('cm-keyword')) {
const label = e.target.innerText.trim();
this.formulaInfo = this.t('formula.' + label) || '';
this.formulaExample = formulaInfo[label] || '';
}
},
nodeOver(data) {
this.formulaInfo = data.info || '';
this.formulaExample = data.example || '';
},
markRule(label, title, cls) {
const value = this.editor.getValue();
if (value) {
const ch = this.editor.getCursor().ch;
if ([' ', '(', ',', ')', '{', '}', '[', ']'].indexOf(value.substr(ch - 1, 1)) === -1) {
this.editor.replaceRange(' ', this.editor.getCursor());
}
}
this.editor.replaceRange(label, this.editor.getCursor());
const cur = this.editor.getCursor();
const span = document.createElement('span');
span.innerText = title;
span.classList.add('cm-fc-' + cls);
this.editor.markText({
line: cur.line,
ch: cur.ch - label.length
}, cur, {
replacedWith: span
});
},
nodeClick(data) {
if (!data.formula || data.disabled === true) {
return;
}
if (data.rule) {
const fields = [];
const titles = [];
let flag = false;
data.parent.forEach(item => {
if (item._menu && item._menu.subForm === 'array') {
flag = true;
}
fields.push(item.field);
titles.push(this.getTitle(item));
})
if (flag) {
return this.setColumn(data);
}
fields.push(data.rule.field);
titles.push(this.getTitle(data.rule));
this.markRule(fields.join('.'), titles.join('.'), 'field');
} else {
this.editor.replaceRange(data.label + '()', this.editor.getCursor());
this.editor.moveH(-1, 'char');
}
this.editor.focus();
},
setColumn(data) {
let flag = false;
const fields = [];
const titles = [];
const columns = [];
data.parent.forEach(item => {
if (!flag) {
flag = item._menu && item._menu.subForm === 'array';
fields.push(item.field);
titles.push(this.getTitle(item));
} else {
columns.push(item);
}
});
columns.push(data.rule);
columns.reverse().forEach(rule => {
this.nodeClick({label: 'COLUMN', formula: true});
this.editor.replaceRange(',', this.editor.getCursor());
this.setField({rule});
this.editor.moveH(-1 - rule.field.length - 2, 'char');
});
this.markRule(fields.join('.'), titles.join('.'), 'field');
},
submit() {
if (this.status === 'computed') {
const value = this.editor.getValue().trim();
if (this.oldValue !== value || !is.String(this.modelValue)) {
this.oldValue = value;
this.$emit('update:modelValue', value);
}
} else {
let value = this.condition ? {...this.condition} : this.condition;
if (value) {
if (this.type === 'linkage') {
if (this.linkage) {
value.linkage = this.linkage;
} else {
value = '';
}
} else {
if (this.invert) {
value.invert = true;
} else {
delete value.invert;
}
}
}
this.$emit('update:modelValue', value || '');
}
this.visible = false;
},
setValue(value) {
const fields = this.designer.setupState.fields().map(escapeRegExp);
value = value.replace(new RegExp(`["'](${fields.join('|')})(\\.(${fields.join('|')}))*(?![a-zA-Z0-9_$])["']`, 'g'), v => {
return '__var___' + v + '__var__';
})
value = value.replace(new RegExp(`(?<!__var___")(${fields.join('|')})(\\.(${fields.join('|')}))*(?![a-zA-Z0-9_$])`, 'g'), v => {
return '__var___' + v + '__var__';
})
value.split('__var__').forEach(v => {
let rule;
if (v.indexOf('_') === 0) {
v = v.slice(1);
const flag = ['\'', '"'].indexOf(v[0]) > -1;
if (flag) {
v = v.slice(1).slice(0, -1);
}
let level = 0;
if (v.indexOf('.') > -1) {
const temp = v.split('.');
v = temp.pop();
level = temp.length;
}
rule = this.designer.setupState.dragForm.api.all().filter(item => item && item.field === v)[0];
if (rule) {
if (flag) {
this.setField({rule});
} else {
const fields = [rule.field];
const titles = [this.getTitle(rule)];
let ctx = rule.__fc__.parent;
while (ctx && level > 0) {
if (ctx.input) {
level--;
fields.unshift(ctx.rule.field);
titles.unshift(this.getTitle(ctx.rule));
}
ctx = ctx.parent;
}
this.markRule(fields.join('.'), titles.join('.'), 'field');
}
return;
}
}
this.editor.replaceRange(v, this.editor.getCursor());
});
},
load() {
this.value = is.String(this.modelValue) ? this.modelValue : '';
this.oldValue = this.value;
this.err = this.formulaInfo = '';
this.$nextTick(() => {
document.querySelector('._fd-comp-script').addEventListener('mouseover', this.spanOver);
this.editor = markRaw(CodeMirror(this.$refs.editor, {
lineNumbers: true,
mode: 'fcComputedMode',
line: true,
tabSize: 2,
lineWrapping: true,
value: '',
extraKeys: {
Enter: function () {
return false; //
}
}
}));
this.setValue(this.value || '');
this.editor.on('beforeChange', (cm, change) => {
if (change.origin === 'paste') {
const text = change.text[0] || '';
if (text) {
this.setValue(text);
}
change.cancel();
}
});
addAutoKeyMap(this.editor);
})
},
}
});
</script>
<style>
._fd-computed {
width: 100%;
}
._fd-computed .el-badge {
width: 100%;
}
._fd-computed .el-button {
font-weight: 400;
width: 100%;
}
._fd-comp-con, ._fd-comp-condition {
height: 500px;
}
._fd-comp-condition .el-main {
padding: 20px 5px;
}
._fd-comp-con .el-tree > .el-tree-node {
margin: 1px;
padding: 14px;
font-weight: 500;
font-size: 13px;
color: var(--fc-text-color-1);
}
._fd-comp-con .el-tree > .el-tree-node + .el-tree-node {
border-top: 1px solid var(--fc-line-color-3);
}
._fd-comp-con .el-tree-node {
font-weight: 400;
}
._fd-comp-con .el-tree-node__content {
margin-top: 5px;
}
._fd-comp-dialog .el-dialog__body {
padding: 0 10px;
}
._fd-comp-dialog .el-tabs__header {
margin-bottom: 0;
}
._fd-comp-con .el-main {
padding: 0;
}
._fd-comp-r {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
border: 1px solid var(--fc-line-color-3);
}
._fd-comp-head {
display: flex;
padding: 5px 15px;
font-weight: 500;
font-size: 13px;
border-bottom: 1px solid var(--fc-line-color-3);
height: 38px;
align-items: center;
color: var(--fc-text-color-1);
}
._fd-comp-r > .el-main, ._fd-comp-script {
display: flex;
flex-direction: row;
flex: 1;
flex-basis: auto;
box-sizing: border-box;
min-width: 0;
width: 100%;
}
._fd-comp-script {
width: 100%;
}
._fd-comp-r > .el-main {
flex-direction: column;
}
._fd-comp-info {
background: var(--fc-bg-color-2);
border-radius: 6px;
height: 90px;
margin: 10px;
color: var(--fc-text-color-2);
line-height: 20px;
padding: 15px;
}
._fd-comp-con .CodeMirror {
height: 100%;
width: 100%;
}
._fd-comp-con .CodeMirror-wrap pre.CodeMirror-line {
padding-left: 20px;
}
._fd-comp-node {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 26px;
line-height: 26px;
padding-right: 5px;
font-size: 13px;
}
._fd-comp-node ._group {
color: #61affe;
font-weight: 700;
margin-right: 5px;
}
._fd-comp-node ._subform {
color: #fca130;
font-weight: 700;
margin-right: 5px;
}
._fd-comp-node.disabled {
color: var(--fc-text-color-3);
cursor: not-allowed;
}
._fd-comp-node.disabled ._fd-comp-id {
background-color: #999;
}
._fd-comp-id {
height: 20px;
width: 20px;
color: #fff;
background-color: var(--fc-style-color-1);
text-align: center;
font-weight: 500;
line-height: 20px;
border-radius: 5px;
}
._fd-comp-dialog .el-aside {
width: 300px;
border: 1px solid var(--fc-line-color-3);
border-right: 0 none;
}
._fd-comp-title {
position: relative;
font-weight: 500;
color: var(--fc-text-color-1);
margin-bottom: 15px;
padding-left: 5px;
}
._fd-comp-title:before {
content: ' ';
display: inline-block;
width: 3px;
height: 1em;
background-color: var(--fc-style-color-1);
position: absolute;
top: 3px;
left: -5px;
}
._fd-comp-script .CodeMirror pre.CodeMirror-line {
line-height: 26px;
}
._fd-comp-linkage {
display: flex;
align-items: center;
font-size: 12px;
}
._fd-comp-linkage > ._fd-rule-select {
display: inline-block;
width: 120px;
margin: 0 6px;
}
</style>

View File

@ -0,0 +1,450 @@
<template>
<div class="_fd-cdg-input">
<div class="_fd-cdg-item">
<div class="_fd-cdg-and">
<el-select size="default" v-model="mode" @change="onInput" v-if="list.length > 0">
<el-option label="AND" value="AND"/>
<el-option label="OR" value="OR"/>
</el-select>
</div>
<div class="_fd-cdg-options">
<template v-for="(item, idx) in list">
<div class="_fd-cdg-option is-group" v-if="item.mode != null" :key="item.field + 'a' + idx + list.length">
<ConditionGroup v-model="list[idx]" @change="onInput"></ConditionGroup>
<i class="fc-icon icon-add-circle" :class="{disabled: list.length === 1}"
@click="removeItem(idx)"></i>
</div>
<div class="_fd-cdg-option" v-else :key="idx">
<el-select style="width: 85px;" size="default" v-model="item.type"
@change="changeType(item)">
<el-option :label="t('props.field')" value="field"/>
<el-option :label="t('props.variable')" value="variable"/>
</el-select>
<template v-if="item.type === 'variable'">
<el-input class="_fd-cdg-variable" size="default" v-model="item.field" clearable
@change="changeField(item)" key="variable">
<template #suffix>
<VariableConfig popover
@confirm="(val) => selectVar(item, val)"></VariableConfig>
</template>
</el-input>
</template>
<template v-else>
<RuleSelect class="_fd-cdg-field" size="default" onlyField valueType="field"
v-model="item.field" clearable :multiple="false"
@change="changeField(item)" key="field"></RuleSelect>
</template>
<el-select class="_fd-cdg-term" size="default" v-if="item.formula" v-model="item.condition"
@change="onInput">
<el-option v-for="item in item.formula" :key="item.value" :label="item.label"
:value="item.value"/>
</el-select>
<div class="_fd-cfg-value"
v-if="item.input && ['empty', 'notEmpty'].indexOf(item.condition) === -1">
<template v-if="item.var">
<RuleSelect class="_fd-cdg-field" size="default" onlyField valueType="field"
v-model="item.compare" clearable :multiple="false"
@change="onInput"></RuleSelect>
</template>
<template v-else-if="item.type === 'variable'">
<ValueInput size="default" v-model="item.value" @change="onInput"></ValueInput>
</template>
<template v-else-if="item.condition === 'pattern'">
<PatternInput size="default" :key="item.field" v-model="item.value"
@change="onInput"></PatternInput>
</template>
<template v-else>
<ConditionInput v-bind="item.input" :key="item.field" v-model="item.value"
@change="onInput"></ConditionInput>
</template>
<el-checkbox v-model="item.var" size="default" :label="t('props.field')"/>
</div>
<i class="fc-icon icon-delete"
@click="removeItem(idx)"></i>
</div>
</template>
</div>
</div>
<div class="_fd-cdg-btns">
<el-button link type="primary" @click="addItem">
<i class="fc-icon icon-add-circle"></i>
{{ t('computed.addCondition') }}
</el-button>
<el-button link type="primary" @click="addItemGroup">
<i class="fc-icon icon-add-circle"></i>
{{ t('computed.addGroup') }}
</el-button>
</div>
</div>
</template>
<script>
import {defineComponent, markRaw} from 'vue';
import ConditionInput from './ConditionInput.vue';
import is from '@form-create/utils/lib/type';
import {deepGet} from '../../utils';
import PatternInput from './PatternInput.vue';
import RuleSelect from '../RuleSelect.vue';
import ValueInput from './ValueInput.vue';
import VariableConfig from './VariableConfig.vue';
const formulaType = {
input: ['==', '!=', 'on', 'notOn', 'empty', 'notEmpty', 'pattern'],
select: ['==', '!=', 'on', 'notOn', 'empty', 'notEmpty'],
switch: ['==', '!='],
number: ['==', '!=', '>', '>=', '<', '<=', 'empty', 'notEmpty'],
};
formulaType.cascader = formulaType.select;
const ConditionGroup = defineComponent({
name: 'ConditionGroup',
components: {VariableConfig, ValueInput, RuleSelect, PatternInput, ConditionInput},
inject: ['designer'],
emits: ['update:modelValue', 'change'],
props: {
modelValue: [Object, Array],
},
computed: {
formulaLabel() {
return ['==', '!=', 'on', 'notOn', 'empty', 'notEmpty', 'pattern', '>', '>=', '<', '<='].reduce((p, v) => {
p[v] = this.t('computed.formulas.' + v);
return p;
}, {});
},
activeRule() {
return this.designer.setupState.activeRule;
},
rules() {
let ctx = this.activeRule?.__fc__.parent;
let rules = [];
while (ctx) {
if (ctx.rule._menu && ctx.rule._menu.subForm) {
rules = this.getFields(this.designer.setupState.findTree(ctx.rule._fc_id));
break;
} else {
ctx = ctx.parent;
}
}
return [...rules, ...this.getFields(this.designer.setupState.treeInfo)]
},
t() {
return this.designer.setupState.t;
},
},
data() {
return {
mode: 'AND',
list: [],
ConditionGroup: markRaw(ConditionGroup),
}
},
methods: {
selectVar(item, val) {
item.field = val.slice(2, -2);
this.changeField(item);
},
addItem() {
this.list.push({
type: 'field',
});
},
addItemGroup() {
this.list.push({mode: 'AND'});
},
removeItem(idx) {
this.list.splice(idx, 1);
this.onInput();
},
changeType(item) {
item.field = '';
item.input = null;
item.formula = null;
},
changeField(item) {
if (item.field) {
item.condition = '==';
if (item.type === 'field') {
this.tidyItem(item);
} else {
item.input = true;
item.formula = formulaType.select.map(v => {
return {
label: this.formulaLabel[v],
value: v
}
});
}
} else {
item.input = null;
item.formula = null;
}
this.onInput();
},
getFields(children, parent = []) {
const fields = [];
children.forEach(({rule, children}) => {
const temp = [...parent];
if (rule.field) {
temp.push(rule);
}
const childrenFields = this.getFields(children || [], temp);
if (rule.field) {
const item = {
field: rule.field,
value: parent.length ? parent.map(item => item.field).join('.') + '.' + rule.field : rule.field,
label: rule.title,
rule,
};
fields.push(item, ...childrenFields);
} else {
fields.push(...childrenFields)
}
});
return fields;
},
tidyValue() {
let value = this.modelValue;
if (value) {
if (Array.isArray(value)) {
value = {
mode: 'AND',
group: value
}
}
this.mode = value.mode === 'OR' ? 'OR' : 'AND';
this.list = (value.group || []).map(item => {
if (item.mode != null) {
return item;
} else {
return this.tidyItem({...item});
}
});
}
if (!this.list.length) {
this.list.push({type: 'field'}, {type: 'field'});
}
},
tidyItem(item) {
if (item.variable) {
item.input = true;
item.field = item.variable;
item.formula = formulaType.select.map(v => {
return {
label: this.formulaLabel[v],
value: v
}
});
item.type = 'variable';
return item;
}
item.type = 'field';
this.rules.forEach(data => {
if (data.value === item.field || data.field === item.field) {
const condition = data.rule._menu.condition;
const input = condition ? (is.Function(condition) ? condition(data.rule) : is.String(condition) ? {
type: condition
} : {...condition}) : {
type: 'input'
}
if (input.options) {
input.options = is.String(input.options) ? deepGet(data.rule.__fc__.prop, input.options) : input.options;
}
item.formula = (formulaType[input.type] || formulaType.input).map(v => {
return {
label: this.formulaLabel[v],
value: v
}
});
item.var = !!item.compare;
item.input = input;
}
});
return item;
},
onInput() {
let value = []
this.list.forEach(item => {
if (item.field && item.condition && (item.compare || ['empty', 'notEmpty'].indexOf(item.condition) > -1 || (item.value != null && item.value !== ''))) {
const val = {
[item.type]: item.field,
condition: item.condition,
};
if (item.compare && item.var) {
val.compare = item.compare;
} else {
val.value = item.value;
}
value.push(val);
} else if (item.group) {
value.push(item);
}
});
if (value.length === 1 && value[0].mode != null) {
value = value[0];
} else {
value = value.length > 0 ? {
mode: this.mode,
group: value
} : undefined
}
if (!value && (!this.modelValue || !this.modelValue.group)) {
return;
}
this.$emit('update:modelValue', value);
this.$emit('change', value);
},
},
created() {
this.tidyValue();
}
});
export default ConditionGroup;
</script>
<style>
._fd-cdg-input {
display: flex;
flex-direction: column;
}
._fd-cdg-btns {
margin-top: 10px;
margin-left: 30px;
}
._fd-cdg-btns .el-button {
color: var(--fc-text-color-2);
}
._fd-cdg-item {
display: flex;
}
._fd-cdg-item .el-select {
background-color: var(--fc-bg-color-2);
}
._fd-cdg-and {
display: flex;
width: 100px;
position: relative;
flex-shrink: 0;
}
._fd-cdg-and > .el-select {
position: absolute;
top: 50%;
left: -5px;
width: 80px;
margin-top: -16px;
z-index: 2;
}
._fd-cdg-and:before {
content: "";
position: absolute;
width: 1px;
left: 30px;
background-color: var(--fc-line-color-2);
top: 1px;
bottom: 1px;
margin-top: 14px;
margin-bottom: 16px;
}
._fd-cdg-options {
display: flex;
flex-direction: column;
}
._fd-cdg-option {
position: relative;
display: flex;
align-items: center;
}
._fd-cdg-field {
width: 208px;
}
._fd-cdg-variable {
width: 208px;
height: 32px;
}
._fd-cdg-term {
width: 104px
}
._fd-cdg-option > ._fd-cfg-value {
width: 208px;
display: flex;
align-items: center;
}
._fd-cdg-option > .fc-icon {
margin-left: 10px;
cursor: pointer;
color: var(--fc-text-color-2);
}
._fd-cdg-option > .fc-icon.disabled {
cursor: not-allowed;
}
._fd-cdg-option > ._fd-cfg-value > div {
width: 100%;
}
._fd-cdg-option > .el-select + .el-select, ._fd-cdg-option > .el-input + .el-select, ._fd-cdg-option > .el-select + .el-input {
margin-left: 10px
}
._fd-cfg-value {
margin-left: 10px;
}
._fd-cfg-value .el-checkbox {
margin-left: 10px;
}
._fd-cdg-option:before {
content: "";
position: absolute;
width: 105px;
height: 1px;
background-color: var(--fc-line-color-2);
left: -70px;
top: 50%;
margin-top: -1px;
}
._fd-cdg-option.is-group {
border: 1px dashed #ccd3db;
padding: 14px;
}
._fd-cdg-option.is-group > .fc-icon {
position: absolute;
right: -10px;
top: -10px;
z-index: 2;
transform: rotate(45deg);
font-size: 18px;
}
._fd-cdg-option.is-group:before {
margin-top: -17px;
}
._fd-cdg-option + ._fd-cdg-option {
margin-top: 16px;
}
</style>

View File

@ -0,0 +1,69 @@
<template>
<div class="_fd-cdi-input">
<template v-if="type === 'cascader'">
<el-cascader size="default" :props="{checkStrictly: true, emitPath: false}" v-bind="props || {}"
:options="options"
v-model="value"
@change="onInput"></el-cascader>
</template>
<template v-else-if="type === 'number'">
<el-input-number size="default" v-bind="props || {}" v-model="value" @change="onInput"></el-input-number>
</template>
<template v-else-if="type === 'select'">
<el-select size="default"
filterable
allow-create
default-first-option v-bind="props || {}" v-model="value" @change="onInput">
<el-option v-for="opt in options" :label="opt.label" :value="opt.value" :key="opt.value"></el-option>
</el-select>
</template>
<template v-else-if="type === 'switch'">
<el-switch size="default" v-bind="props || {}" v-model="value" @change="onInput">
</el-switch>
</template>
<template v-else>
<el-input size="default" v-bind="props || {}" v-model="value" @blur="onInput"></el-input>
</template>
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'ConditionInput',
inject: ['designer'],
emits: ['update:modelValue', 'change'],
props: {
type: String,
options: Array,
props: Object,
modelValue: [String, Number, Array, Object, Boolean]
},
watch: {
modelValue() {
this.value = this.modelValue || undefined;
},
},
data() {
return {
value: this.modelValue || undefined,
}
},
methods: {
onInput() {
this.$emit('update:modelValue', this.value);
this.$emit('change', this.value);
},
},
created() {
}
});
</script>
<style>
._fd-cdi-input > div {
width: 100%;
}
</style>

View File

@ -0,0 +1,137 @@
<template>
<div class="_fd-pattern-input">
<el-input :size="size" v-model="value" @blur="onInput" clearable>
<template #append>
<el-dropdown size="default" trigger="click" popper-class="_fd-pattern-popper">
<i class="fc-icon icon-setting"></i>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item in options" :key="item.value" @click="setValue(item.value)">
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-input>
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'PatternInput',
emits: ['update:modelValue', 'change'],
props: {
size: String,
modelValue: String,
},
data() {
return {
value: this.modelValue || '',
options: [
{
label: '邮箱',
value: '^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$'
},
{
label: '域名',
value: '^((http:\\/\\/)|(https:\\/\\/))?([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}(\\/)$'
},
{
label: '手机号',
value: '^(?:(?:\\+|00)86)?1[3-9]\\d{9}$'
},
{
label: '座机电话',
value: '^(?:(?:\\d{3}-)?\\d{8}|^(?:\\d{4}-)?\\d{7,8})(?:-\\d+)?$'
},
{
label: '身份证号',
value: '^[1-9]\\d{5}(?:18|19|20)\\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\\d|30|31)\\d{3}[\\dXx]$'
},
{
label: '银行卡号',
value: '^[1-9]\\d{9,29}$'
},
{
label: '车牌号',
value: '^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$'
},
{
label: '中文',
value: '^(?:[\\u3400-\\u4DB5\\u4E00-\\u9FEA\\uFA0E\\uFA0F\\uFA11\\uFA13\\uFA14\\uFA1F\\uFA21\\uFA23\\uFA24\\uFA27-\\uFA29]|[\\uD840-\\uD868\\uD86A-\\uD86C\\uD86F-\\uD872\\uD874-\\uD879][\\uDC00-\\uDFFF]|\\uD869[\\uDC00-\\uDED6\\uDF00-\\uDFFF]|\\uD86D[\\uDC00-\\uDF34\\uDF40-\\uDFFF]|\\uD86E[\\uDC00-\\uDC1D\\uDC20-\\uDFFF]|\\uD873[\\uDC00-\\uDEA1\\uDEB0-\\uDFFF]|\\uD87A[\\uDC00-\\uDFE0])+$'
},
{
label: '数字',
value: '^\\d+$'
},
{
label: '整数',
value: '^(?:0|(?:-?[1-9]\\d*))$'
},
{
label: '正整数',
value: '^\\+?[1-9]\\d*$'
},
{
label: '负整数',
value: '^-[1-9]\\d*$'
},
{
label: '浮点数',
value: '^(-?[1-9]\\d*\\.\\d+|-?0\\.\\d*[1-9])$'
},
{
label: '正浮点数',
value: '^([1-9]\\d*\\.\\d+|-?0\\.\\d*[1-9])$'
},
{
label: '负浮点数',
value: '^-([1-9]\\d*\\.\\d+|-?0\\.\\d*[1-9])$'
},
{
label: '英文字母',
value: '^[a-zA-Z]+$'
},
{
label: '数字和字母',
value: '^[A-Za-z0-9]+$'
},
]
}
},
methods: {
setValue(val) {
this.value = val;
this.onInput();
},
onInput() {
this.$emit('update:modelValue', this.value);
this.$emit('change', this.value);
},
},
});
</script>
<style>
._fd-pattern-input {
width: 100%;
}
._fd-pattern-input .el-input-group__append {
padding: 0 10px;
}
._fd-pattern-input .fc-icon {
cursor: pointer;
}
._fd-pattern-popper .el-dropdown__list {
height: 350px;
overflow: auto;
}
</style>

View File

@ -0,0 +1,88 @@
<template>
<el-input class="_fd-value-input" v-model="value" @blur="onBlur" v-bind="$attrs">
<template #prepend>
<el-select v-model="type" style="width: 60px">
<el-option :label="t('validate.types.string')" value="1"/>
<el-option :label="t('validate.types.number')" value="2"/>
<el-option :label="t('validate.types.boolean')" value="3"/>
</el-select>
</template>
<template #append v-if="$slots.append">
<slot name="append"></slot>
</template>
</el-input>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'ValueInput',
emits: ['update:modelValue', 'change', 'change-type', 'blur'],
inject: ['designer'],
props: {
modelValue: [String, Number, Boolean],
},
data() {
return {
type: '1',
value: '',
}
},
computed: {
t() {
return this.designer.setupState.t;
}
},
watch: {
modelValue: {
handler: function (val) {
if (typeof val === 'number') {
this.type = '2';
} else if (typeof val === 'boolean') {
this.type = '3';
} else {
this.type = '1';
}
this.value = null == val ? '' : ('' + val);
},
immediate: true,
},
type() {
this.updateValue(this.value);
this.$emit('change-type', this.type);
}
},
methods: {
onBlur(...args) {
if (this.value !== this.toValue(this.modelValue)) {
this.updateValue(this.value);
}
this.$emit('blur', ...args);
},
updateValue(val) {
const value = this.toValue(val);
this.$emit('update:modelValue', value);
this.$emit('change', value);
},
toValue(val) {
if (this.type === '1') {
return '' + val;
} else if (this.type === '2') {
return parseFloat(val) || 0;
}
return val === 'true';
}
}
});
</script>
<style>
._fd-value-input .el-input__validateIcon {
display: none;
}
._fd-value-input .el-select, ._fd-value-input .el-select__wrapper {
height: 100%;
}
</style>

View File

@ -0,0 +1,604 @@
<template>
<div class="_fd-variable">
<template v-if="popover">
<el-popover ref="pop" placement="bottom" :width="330" :hide-after="0" trigger="click" :persistent="false"
popper-class="_fd-variable-pop">
<template #reference>
<i class="fc-icon icon-variable" style="cursor: pointer;"></i>
</template>
<el-container style="height: 100%;">
<el-header height="55px" class="_fd-variable-pop-header">
<el-input size="small" v-model="variable">
<template #prefix>
<span>{&lcub;</span>
</template>
<template #suffix>
}}
</template>
<template #append>
<div @click="confirm">{{ t('props.append') }}</div>
</template>
</el-input>
<i class="fc-icon icon-setting" @click="openVariableConfig"></i>
</el-header>
<el-main>
<el-tree
ref="treeRef"
:data="treeInfo"
:default-expanded-keys="expandedKeys"
:expand-on-click-node="false"
:indent="10"
node-key="id"
@nodeClick="nodeClick"
>
<template #default="{ node, data }">
<div class="_fd-variable-pop-node"
:class="{disabled: data.disabled}">
<div>
<span>{{
(data.label || '').trim() || (data.rule ? getTitle(data.rule) : data.id)
}}</span>
</div>
<span>
{{ data.id }}
</span>
</div>
</template>
</el-tree>
</el-main>
</el-container>
</el-popover>
</template>
<template v-else>
<el-badge :value="eventNum" type="warning" :hidden="eventNum < 1">
<div class="_fd-variable-btn" @click="open">
<i class="fc-icon icon-variable"></i>{{ t('computed.variable.bind') }}
</div>
</el-badge>
<el-dialog class="_fd-variable-dialog _fd-config-dialog"
v-model="visible"
destroy-on-close
:close-on-click-modal="false"
append-to-body
width="980px">
<template #header>
{{ t('computed.variable.bind') }}
<Warning :tooltip="t('warning.variable')"></Warning>
</template>
<el-container class="_fd-variable-con" style="height: 600px">
<el-main>
<el-container class="_fd-variable-l">
<el-header>
<div style="width: 230px">{{ t('computed.variable.attr') }}</div>
<div>{{ t('computed.variable.bind') }}</div>
</el-header>
<el-main>
<template v-for="(item, idx) in fields" :key="item.label">
<div class="_fd-variable-item"
:class="{active: idx === activeIdx, '_fd-variable-top': item.attach === true}">
<div class="_fd-variable-item-label">{{ item.label }}</div>
<div>=</div>
<el-input v-model="item.value" placeholder="_" @focus="activeIdx = idx"
clearable>
<template #prefix>
<span>{&lcub;</span>
</template>
<template #suffix>
}}
</template>
</el-input>
</div>
</template>
</el-main>
</el-container>
</el-main>
<el-aside style="width:328px;">
<el-container class="_fd-variable-r">
<el-header>
{{ t('computed.variable.list') }}
<i class="fc-icon icon-setting" @click="openVariableConfig"></i>
</el-header>
<el-main>
<div class="_fd-variable-info">
{{ t('warning.variableInfo') }}
</div>
<el-tree
ref="treeRef"
:data="treeInfo"
:default-expanded-keys="expandedKeys"
:expand-on-click-node="false"
node-key="id"
:indent="10"
@nodeClick="nodeClick"
>
<template #default="{ node, data }">
<div class="_fd-variable-node"
:class="{disabled: data.disabled}">
<div>
<span>{{
(data.label || '').trim() || (data.rule ? getTitle(data.rule) : data.id)
}}</span>
</div>
<span>
{{ data.id }}
</span>
</div>
</template>
</el-tree>
</el-main>
</el-container>
</el-aside>
</el-container>
<template #footer>
<div>
<el-button size="default" @click="visible=false">{{ t('props.cancel') }}</el-button>
<el-button type="primary" size="default" @click="submit">{{
t('props.ok')
}}
</el-button>
</div>
</template>
</el-dialog>
</template>
</div>
</template>
<script>
import {defineComponent} from 'vue';
import {lower} from '@form-create/utils/lib/tocase';
import Warning from '../Warning.vue';
export default defineComponent({
name: 'VariableConfig',
components: {Warning},
emits: ['submit', 'confirm'],
props: {
popover: Boolean,
},
inject: ['designer'],
data() {
return {
visible: false,
activeIdx: 0,
variable: '',
value: {},
fields: [],
expandedKeys: [
'$topForm', '$cookie', '$localStorage', '$sessionStorage', '$globalData', '$var'
],
};
},
computed: {
t() {
return this.designer.setupState.t;
},
activeRule() {
return this.designer.setupState.activeRule;
},
eventNum() {
return ((this.activeRule || {})._loadData || []).length;
},
treeInfo() {
const varObj = this.toObject(this.designer.setupState.varList || []);
const tree = [
{
id: '$topForm',
label: this.t('computed.form'),
driver: true,
children: this.getFormTree(this.designer.setupState.treeInfo)
},
{
id: '$cookie',
label: 'cookie',
},
{
id: '$localStorage',
label: 'localStorage',
},
{
id: '$sessionStorage',
label: 'sessionStorage',
},
{
id: '$globalData',
label: this.t('props.globalData'),
driver: true,
children: Object.keys((this.designer.setupState.formOptions.globalData || {})).map(k => {
return {
label: this.designer.setupState.formOptions.globalData[k].label,
id: k,
}
})
},
{
id: '$var',
label: this.t('computed.variable.title'),
driver: true,
children: Object.keys((this.designer.setupState.formOptions.globalVariable || {})).map(k => {
return {
label: this.designer.setupState.formOptions.globalVariable[k].label,
id: k,
}
})
},
];
const activePage = this.designer.setupState.activePage;
if (!activePage.default && activePage.main.field && activePage.main !== this.activeRule) {
tree[0].id = '$scopeForm';
if (tree[0].children.length) {
tree[0].children = tree[0].children[0].children;
}
}
let ctx = this.activeRule?.__fc__.parent;
while (ctx) {
if (ctx.rule === activePage.main) {
ctx = undefined;
} else if (ctx.rule._menu && ['array', 'object', 'scope'].indexOf(ctx.rule._menu.subForm) > -1) {
const subTree = this.getFormTree(this.designer.setupState.findTree(ctx.rule._fc_id))
if (subTree.length) {
tree.unshift({
id: '$form',
driver: true,
label: ctx.refRule?.__$title?.value || ctx.rule.title || ctx.rule._menu.label,
children: subTree
})
}
ctx = undefined;
} else {
ctx = ctx.parent;
}
}
if (this.designer.setupState.getConfig('showLanguage') !== false) {
let lang = this.designer.setupState.formOptions?.language || {};
let language = lang[this.designer.props?.locale?.name || 'zh-cn'] || lang[Object.keys(lang)[0]] || {};
tree.push({
id: '$t',
label: this.t('language.name'),
driver: true,
children: Object.keys(language).map(k => {
return {
label: language[k],
id: k,
}
})
});
}
if (varObj['$cookie']) {
tree[1] = {...tree[1], ...varObj['$cookie']};
}
if (varObj['$localStorage']) {
tree[2] = {...tree[2], ...varObj['$localStorage']};
}
if (varObj['$sessionStorage']) {
tree[3] = {...tree[3], ...varObj['$sessionStorage']};
}
if (varObj['$globalData'] && varObj['$globalData'].children) {
tree[4].children = Object.values({...this.toObject(tree[4].children), ...this.toObject(varObj['$globalData'].children || [])});
}
if (varObj['$var'] && varObj['$var'].children) {
tree[5].children = Object.values({...this.toObject(tree[5].children), ...this.toObject(varObj['$var'].children || [])});
}
delete varObj['$cookie'];
delete varObj['$localStorage'];
delete varObj['$sessionStorage'];
delete varObj['$globalData'];
delete varObj['$var'];
tree.push(...Object.values(varObj));
return tree;
}
},
watch: {
visible(v) {
if (v) {
this.updateFields();
this.activeIdx = 0;
}
},
},
methods: {
openVariableConfig() {
this.designer.setupState.openGlobalVariableDialog();
},
toObject(list) {
const data = {};
list && list.forEach(item => {
data[item.id] = item;
})
return data;
},
nodeClick(data, node) {
if ((this.popover || this.fields[this.activeIdx]) && !data.driver) {
let val = data.id;
node = node.parent;
while (node.level >= 1) {
val = (node.data.id) + '.' + val;
node = node.parent;
}
if (this.popover) {
this.variable = val;
} else {
this.fields[this.activeIdx].value = val;
}
}
},
getTitle(rule) {
return (rule?.__fc__?.refRule?.__$title?.value || rule.title || '').trim() || (rule._menu && rule._menu.label) || rule.field || rule._fc_id;
},
getFormTree(children) {
const fields = [];
children.forEach(({rule, children}) => {
const childrenFields = (rule.field && (!rule._menu || rule._menu.subForm !== 'object')) ? [] : this.getFormTree(children || []);
if (rule.field) {
const item = {
id: rule.field,
label: rule?.__fc__?.refRule?.__$title?.value || rule.title,
rule,
};
if (childrenFields.length) {
item.children = childrenFields;
}
fields.push(item);
} else {
fields.push(...childrenFields)
}
});
return fields;
},
tranField(str) {
if (str.indexOf('formCreate') === 0) {
str = lower(str.replace('formCreate', ''));
} else {
str = 'props.' + str;
}
return str.replaceAll('>', '.');
},
updateFields() {
const vm = this.designer.setupState;
const fields = [];
const loadData = {};
(vm.activeRule._loadData || []).forEach(item => {
loadData[item.to] = item.attr;
});
const important = [];
if (vm.activeRule.field) {
important.push({
label: this.t('computed.value.name'),
attach: true,
modify: true,
field: 'value'
});
}
const rules = vm.propsForm.api.model();
Object.keys(rules).forEach(k => {
if (k && (k[0] !== '_' || rules[k]._fc_important_prop) && rules[k].title && rules[k]._fc_important_prop !== false && !rules[k].hidden && false !== rules[k].display) {
const prop = typeof rules[k]._fc_important_prop === 'string' ? rules[k]._fc_important_prop : k;
(rules[k]._fc_important_prop === true ? important : fields).push({
label: rules[k].title,
modify: prop === 'formCreateChild',
field: this.tranField(prop)
})
}
});
fields.unshift(...important);
fields.forEach(item => {
item.value = loadData[item.field] || '';
});
this.fields = fields;
},
open() {
this.visible = true;
},
active(idx) {
if (this.activeIdx !== idx) {
this.activeIdx = idx;
}
},
submit() {
const loadData = [];
this.fields.forEach(item => {
let val = (item.value || '').trim();
if (val) {
const data = {
attr: val,
to: item.field
};
if (item.modify) {
data.modify = true;
}
loadData.push(data);
}
});
this.designer.setupState.activeRule._loadData = loadData;
this.visible = false;
},
confirm() {
const val = (this.variable || '').trim();
if (val) {
this.$emit('confirm', `{{${val}}}`);
this.$refs.pop.hide();
this.variable = '';
}
},
},
});
</script>
<style>
._fd-variable-btn {
margin-left: 6px;
background: var(--fc-style-bg-color-1);
border-radius: 5px;
color: var(--fc-style-color-1);
display: flex;
align-items: center;
font-size: 12px;
padding: 2px 6px;
cursor: pointer;
}
._fd-variable-con .el-main {
padding: 0;
}
._fd-variable-l, ._fd-variable-r {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
border: 1px solid var(--fc-line-color-3);
}
._fd-variable-r {
margin-left: 10px;
}
._fd-variable-l .el-header, ._fd-variable-r .el-header {
display: flex;
align-items: center;
height: 40px;
background: var(--fc-bg-color-3);
color: var(--fc-text-color-1);
font-size: 13px;
}
._fd-variable-r .el-header .fc-icon {
font-size: 13px;
margin-left: 2px;
color: var(--fc-style-color-1);
cursor: pointer;
}
._fd-variable-r .el-main {
padding: 10px;
}
._fd-variable-info {
display: flex;
font-size: 12px;
position: relative;
margin-bottom: 6px;
padding: 8px 13px;
line-height: 18px;
background: rgba(170, 170, 170, 0.1);
border-radius: 6px;
color: var(--fc-text-color-2);
}
._fd-variable-node {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 26px;
line-height: 26px;
padding-right: 5px;
font-size: 13px;
color: var(--fc-text-color-1);
}
._fd-variable-node > span {
color: var(--fc-text-color-3);
font-size: 12px;
}
._fd-variable-item {
display: flex;
align-items: center;
min-height: 40px;
padding: 0 20px;
border-bottom: 1px solid var(--fc-line-color-3);;
}
._fd-variable-top ._fd-variable-item-label {
color: #fca130;
}
._fd-variable-item-label {
width: 198px;
margin-right: 18px;
font-size: 13px;
}
._fd-variable-item .el-input {
display: flex;
flex: 1;
margin-top: 4px;
font-size: 13px;
}
._fd-variable-item.active, ._fd-variable-item.active ._fd-variable-item-label, ._fd-variable-item.active input {
color: var(--fc-style-color-1);
}
._fd-variable-item.active .el-input {
--el-input-icon-color: var(--fc-style-color-1);
}
._fd-variable-item input {
text-align: center;
}
._fd-variable-item .el-input .el-input__wrapper {
box-shadow: none;
}
._fd-variable-pop.el-popover.el-popper {
height: 400px;
padding: 0;
}
._fd-variable-pop-header .fc-icon {
color: var(--fc-style-color-1);
margin-left: 4px;
cursor: pointer;
}
._fd-variable-pop .el-header {
display: flex;
align-items: center;
border-bottom: 1px solid var(--fc-line-color-3);
padding: 0 10px;
background-color: var(--fc-bg-color-2)
}
._fd-variable-pop .el-main {
padding: 10px;
}
._fd-variable-pop .el-tree-node__content > .el-tree-node__expand-icon {
padding: 3px;
}
._fd-variable-pop .el-input-group__append {
background: var(--fc-style-color-1);
color: #FFFFFF;
cursor: pointer;
width: 60px;
padding: 0;
}
._fd-variable-pop .el-input-group__append div {
width: 100%;
text-align: center;
}
._fd-variable-pop-node {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 26px;
line-height: 26px;
padding-right: 5px;
font-size: 13px;
}
._fd-variable-pop-node > span {
color: var(--fc-text-color-3);
font-size: 12px;
}
</style>

View File

@ -0,0 +1,200 @@
<template>
<div class="_fc-data-select">
<el-select :disabled="disabled" :placeholder="placeholder"
:multiple="multiple" :multipleLimit="multipleLimit" :clearable="clearable"
:model-value="selectValue"
@update:modelValue="changeSelectValue"
popper-class="_fc-data-select-pop" @visible-change="handleClick" @clear="handleClear">
<template v-for="item in options" :key="item.value">
<el-option :label="item.label" :value="item.value"></el-option>
</template>
</el-select>
<FcDialog ref="dialog" :formCreateInject="formCreateInject" :rule="formRule" @update:modelValue="formChange"
:footer="multiple" :title="title" width="900px"
@confirm="confirm"></FcDialog>
</div>
</template>
<script>
import {defineComponent} from 'vue';
import FcDialog from '../dialog/Dialog.vue';
import {deepClone} from 'vant/es/utils/deep-clone';
import uniqueId from '@form-create/utils/lib/unique';
import {uniqueArray} from '../../utils';
import debounce from '@form-create/utils/lib/debounce';
export default defineComponent({
name: 'FcDataSelect',
components: {FcDialog},
emits: ['update:modelValue', 'change', 'clear'],
props: {
title: String,
formCreateInject: Object,
placeholder: String,
multiple: Boolean,
disabled: Boolean,
multipleLimit: Number,
valueKey: String,
labelKey: String,
clearable: Boolean,
searchRule: Object,
tableRule: Object,
autoLoad: Boolean,
modelValue: [Object, Array],
},
computed: {
options() {
return this.list.map((item) => {
return {
label: item[this.labelKey || 'label'],
value: item[this.valueKey || 'value'],
}
})
},
selectValue() {
const value = this.options.map((item) => {
return item.value;
})
return this.multiple === true ? value : value[0];
},
fapi() {
return this.$refs?.dialog?.fapi;
},
},
data() {
return {
list: [],
visible: false,
formRule: [],
load: debounce(() => {
this.$refs.dialog.fapi.el(this.formRule[1].name).initPage();
}, 500)
}
},
watch: {
modelValue: {
handler(n) {
if (n) {
this.list = Array.isArray(n) ? n : [n];
} else {
this.list = [];
}
},
immediate: true,
}
},
methods: {
getTableEl() {
return this.$refs.dialog.fapi.el(this.formRule[1].name);
},
getDialogEl() {
return this.$refs.dialog;
},
formChange() {
if (this.autoLoad) {
this.load();
}
},
changeSelectValue(value) {
if (value == null) {
this.list = [];
} else {
this.list = this.list.filter((item) => {
return value.indexOf(item[this.valueKey || 'value']) !== -1;
})
}
this.updateValue();
},
confirm() {
const list = uniqueArray([...this.list, ...this.$refs.dialog.fapi.el(this.formRule[1].name).getEl().getSelectionRows()]);
const keys = list.map((item) => {
return item[this.valueKey || 'value'];
});
this.list = list.filter((item, idx) => keys.indexOf(item[this.valueKey || 'value']) === idx);
this.$refs.dialog.close();
this.updateValue();
},
tableRowClick(value) {
if (this.multiple) {
this.$refs.dialog.fapi.el(this.formRule[1].name).getEl().toggleRowSelection(value);
} else {
this.list = [value];
this.updateValue();
this.$refs.dialog.close();
}
},
updateValue() {
if (this.multipleLimit > 0 && this.list.length > this.multipleLimit) {
this.list = this.list.splice(0, this.multipleLimit);
}
const value = this.list.map((item) => {
return {
[this.valueKey || 'value']: item[this.valueKey || 'value'],
[this.labelKey || 'label']: item[this.labelKey || 'label'],
}
});
this.$emit('update:modelValue', this.multiple ? value : value[0]);
this.$emit('change', this.multiple ? value : value[0]);
},
getFormRule() {
const formRule = deepClone([
this.searchRule,
this.tableRule
]);
if (!formRule[1].on) {
formRule[1].on = {};
}
if (!formRule[1].props) {
formRule[1].props = {};
}
if (this.multiple) {
if (!formRule[1].props.rowKey) {
formRule[1].props.rowKey = this.valueKey || 'value';
}
if (!formRule[1].name) {
formRule[1].name = uniqueId();
}
}
formRule[1].props.selection = true;
if (formRule[1].on.rowClick) {
formRule[1].on.rowClick = [(...args) => this.tableRowClick(...args), formRule[1].on.rowClick];
} else {
formRule[1].on.rowClick = (...args) => this.tableRowClick(...args)
}
if (formRule[1].on.selectionChange) {
formRule[1].on.selectionChange = [(...args) => this.selectionChange(...args), formRule[1].on.selectionChange];
} else {
formRule[1].on.selectionChange = (...args) => this.selectionChange(...args)
}
return formRule;
},
selectionChange(value) {
if (!this.multiple) {
this.tableRowClick(value[0]);
}
},
handleClear(...args) {
this.$emit('clear', ...args);
},
handleClick(flag) {
if (this.disabled || !flag) {
return;
}
this.visible = true;
this.formRule = this.getFormRule();
this.$refs.dialog.open();
}
}
});
</script>
<style>
._fc-data-select-pop {
display: none !important;
}
._fc-data-select {
width: 100%;
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<div class="_fd-data-select" :style="{'--fc-drag-empty': `'${t('com.select.emptyText')}'`}">
<el-select @click.capture="handleClick" :disabled="disabled" :placeholder="placeholder"></el-select>
<div class="el-dialog" v-if="visible">
<header class="el-dialog__header show-close">
<span class="el-dialog__title">{{ title }}</span>
</header>
<slot name="search"></slot>
<slot name="table"></slot>
</div>
</div>
</template>
<script>
import {defineComponent} from 'vue';
import DataTable from '../dataTable/DataTable.vue';
import FcInlineForm from '../InlineForm.vue';
export default defineComponent({
name: 'FcDataSelectView',
components: {FcInlineForm, DataTable},
inject: ['designer'],
props: {
title: String,
placeholder: String,
multiple: Boolean,
disabled: Boolean,
multipleLimit: Number,
valueKey: String,
labelKey: String,
clearable: Boolean,
},
computed: {
t() {
return this.designer.setupState.t;
}
},
data() {
return {
visible: true,
}
},
methods: {
handleClick(e) {
e.stopPropagation();
this.visible = !this.visible;
}
}
});
</script>
<style>
._fd-data-select {
width: 100%;
}
._fd-data-select > .el-dialog {
width: calc(100% - 20px);
margin: 10px;
}
</style>

View File

@ -0,0 +1,412 @@
<script>
import {defineComponent, h, resolveComponent, resolveDirective, withDirectives} from 'vue';
import {parseFn} from '@form-create/utils/lib/json';
export default defineComponent({
name: 'DataTable',
emits: ['sortChange', 'handleClick'],
props: {
column: {
type: Array,
default: () => []
},
globalDataKey: [String, Object],
fetch: Object,
data: {
type: Array,
default: () => []
},
button: Object,
index: Boolean,
selection: Boolean,
page: Object,
formCreateInject: Object,
},
data() {
return {
total: 0,
loading: false,
unwatch: null,
list: [],
currentPage: 1,
id: 1,
order: '',
orderBy: '',
};
},
watch: {
globalDataKey() {
this.initPage();
},
fetch() {
if (!this.globalDataKey) {
this.initPage();
}
},
data() {
if (!this.globalDataKey && !this.fetch) {
this.initPage();
}
},
selection() {
this.id++;
},
index() {
this.id++;
},
page: {
handler() {
this.initPage();
this.id++;
},
deep: true,
},
button: {
handler() {
this.id++;
},
deep: true,
}
},
computed: {
filterList() {
let data = this.list || [];
const filters = [];
this.column.forEach(item => {
if (item.prop && Array.isArray(item.filter) && item.filter.length > 0) {
filters.push((v => {
return item.filter.indexOf(v[item.prop]) > -1;
}))
}
});
filters.forEach(fn => {
data = data.filter(fn);
})
return data;
}
},
render() {
return withDirectives(h('div', {
class: '_fc-data-table'
}, [
h(resolveComponent('el-table'), {
data: this.filterList,
...this.$attrs,
key: this.id,
ref: 'table',
onSortChange: (data) => {
this.$emit('sortChange', data);
if (data.order) {
this.orderBy = data.order === 'descending' ? 'DESC' : 'ASC';
this.order = data.prop
} else {
this.orderBy = '';
this.order = '';
}
this.initPage();
}
}, () => {
const cols = this.column.filter(col => col.hidden !== true).map(col => {
return this.makeColumn(col);
})
if (this.selection) {
cols.unshift(h(resolveComponent('el-table-column'), {
type: 'selection',
width: '50px'
}))
}
const btns = this.makeButtonCol();
if (btns) {
cols.push(btns);
}
if (this.index) {
cols.unshift(h(resolveComponent('el-table-column'), {
type: 'index',
width: '50px'
}))
}
return cols;
}),
this.makePage()
]), [
[resolveDirective('loading'), this.loading]
]);
},
methods: {
getEl() {
return this.$refs.table;
},
deepGet(object, path, defaultValue) {
path = (path || '').split('.');
let index = 0,
length = path.length;
while (object != null && index < length) {
object = object[path[index++]];
}
return (index && index === length) ? (object != null ? object : defaultValue) : defaultValue;
},
initPage() {
this.loading = false;
if (this.page && this.page.open) {
this.currentPage = 1;
this.nextList();
} else {
if (this.globalDataKey || this.fetch) {
this.fetchData().then(({list}) => {
this.list = list;
})
} else {
this.list = this.data;
}
}
},
btnProps(btn, scope) {
const prop = btn.prop || [];
const props = {
type: btn.type,
size: btn.size,
round: prop.indexOf('round') > -1,
link: prop.indexOf('link') > -1,
plain: prop.indexOf('plain') > -1,
disabled: prop.indexOf('disabled') > -1,
onClick: (evt) => {
evt.stopPropagation();
const fn = parseFn(btn.click);
try {
fn && fn(scope, this.formCreateInject.api);
} catch (e) {
console.error(e);
}
this.$emit('handleClick', {name: btn.name, key: btn.key, scope, column: scope.row});
}
}
const fn = parseFn(btn.handle);
try {
const res = fn && fn(props, scope, this.formCreateInject.api);
if (typeof res === 'boolean') {
props.disabled = res;
}
} catch (e) {
console.error(e);
}
return props;
},
getLimit() {
return (this.page.props && this.page.props.pageSize) || 20;
},
nextList() {
if (this.globalDataKey || this.fetch) {
this.fetchData(true).then(({list, total}) => {
this.list = list;
this.total = total;
});
} else {
const data = this.data;
const limit = this.getLimit();
const end = this.currentPage * limit;
this.list = data.slice(end - limit, end);
this.total = data.length;
}
},
fetchData(page) {
this.unwatch && this.unwatch();
return new Promise((resolve) => {
let config = this.fetch;
if (this.globalDataKey) {
const key = typeof this.globalDataKey === 'string' ? this.globalDataKey : this.globalDataKey.key;
config = this.formCreateInject.api.options.globalData[key];
}
if (config) {
if (config.type === 'fetch' || !this.globalDataKey) {
config = {...config};
let params = {};
if (page) {
const limit = (this.page.props && this.page.props.pageSize) || 20;
const pageField = this.page.pageField || 'page';
const pageSizeField = this.page.pageSizeField || 'limit';
params = {
[pageField]: this.currentPage,
[pageSizeField]: limit,
}
}
if (this.order) {
const orderField = this.page.orderField || 'order';
const orderByField = this.page.orderByField || 'orderBy';
params[orderField] = this.order;
params[orderByField] = this.orderBy;
}
const str = Object.keys(params).map(k => {
return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
}, '').join('&');
if (str) {
config.action += (config.action.indexOf('?') !== -1 ? '&' : '?') + str;
}
this.loading = true;
config.wait = 1000;
this.unwatch = this.formCreateInject.api.watchFetch(config, (res, change) => {
this.loading = false;
const totalField = this.page.totalField;
const dataField = this.page.dataField;
const list = dataField ? this.deepGet(res, dataField, []) : res;
let total = totalField ? this.deepGet(res, totalField) : 0;
if (!total) {
total = list.length || 0;
}
resolve({list, total});
}, (e) => {
console.error(e);
this.loading = false;
}, (opt, change) => {
if (change) {
this.unwatch && this.unwatch();
this.unwatch = null;
setTimeout(() => {
this.changePage(1);
})
return false;
}
});
} else {
let list = config.data || [];
let total = config.data.length;
if (page) {
const limit = this.getLimit();
const end = this.currentPage * limit;
list = list.slice(end - limit, end);
total = list.length;
}
resolve({list, total});
}
} else {
resolve({list: [], total: 0});
}
})
},
changePage(n) {
this.currentPage = n;
this.nextList();
},
makePage() {
if (this.page && this.page.open === true) {
return h(resolveComponent('el-pagination'), {
layout: 'prev, pager, next',
total: this.total,
currentPage: this.currentPage,
'onUpdate:currentPage': (n) => {
if (this.currentPage !== n) {
this.changePage(n);
}
},
class: (this.page.position || 'right'),
...(this.page.props || {}),
pageSize: (this.page.props && this.page.props.pageSize) || 20
})
}
},
makeButtonCol() {
if (this.button && this.button.open === true && this.button.column) {
return h(resolveComponent('el-table-column'), {
label: this.button.label || this.formCreateInject.t('operation') || '操作',
fixed: this.button.fixed === undefined ? 'right' : this.button.fixed,
width: this.button.width || '100px',
}, {
default: (scope) => {
return this.button.column.filter(btn => btn.hidden !== true).map(btn => {
return h(resolveComponent('el-button'), this.btnProps(btn, scope), () => [btn.name]);
});
}
});
}
},
makeColumn(col) {
return h(resolveComponent('el-table-column'), {
label: col.label,
prop: col.prop,
width: col.width,
align: col.align,
className: col.className,
fixed: col.fixed,
sortable: col.sortable,
}, {
default: (scope) => {
if (col.children && col.children.length > 0) {
return col.children.map(child => {
return this.makeColumn(child);
});
}
if (!col.format || col.format === 'default') {
return undefined;
}
return this.makeTd(col, scope);
}
})
},
makeTd(col, scope) {
if (col.format === 'custom' && col.render) {
return col.render(scope, h, resolveComponent, this.formCreateInject.api);
} else if (col.format === 'tag') {
return h(resolveComponent('el-tag'), {disableTransitions: true}, () => [this.deepGet(scope.row, col.prop, '')]);
} else if (col.format === 'image') {
return h('div', {
class: '_fc-data-table-img-list'
}, (() => {
let img = this.deepGet(scope.row, col.prop, '');
img = (Array.isArray(img) ? img : [img]).filter(src => !!src);
return img.map((src, i) => {
return h(resolveComponent('el-image'), {
src: src,
previewSrcList: img,
previewTeleported: true,
initialIndex: i,
fit: 'cover'
})
})
})())
} else {
return '' + this.deepGet(scope.row, col.prop, '')
}
},
},
created() {
this.initPage();
this.$watch(() => this.data && this.data.length, () => {
if (!this.globalDataKey && !this.fetch) {
this.initPage();
}
})
}
});
</script>
<style>
._fc-data-table {
width: 100%;
}
._fc-data-table .el-table {
--el-table-header-bg-color: #e8eefc;
}
._fc-data-table .el-pagination {
display: flex;
margin-top: 10px;
}
._fc-data-table .el-pagination.left {
justify-content: flex-start;
}
._fc-data-table .el-pagination.center {
justify-content: center;
}
._fc-data-table .el-pagination.right {
justify-content: flex-end;
}
._fc-data-table ._fc-data-table-img-list .el-image {
max-width: 150px;
height: 60px;
}
</style>

View File

@ -0,0 +1,248 @@
<template>
<div class="_fd-table-button-config">
<el-badge type="warning" is-dot :hidden="!configured">
<el-button class="_fd-plain-button" plain @click="visible=true" size="small">{{ t('com.dataTable.button.btn') }}</el-button>
</el-badge>
<el-dialog class="_fd-tcb-dialog _fd-config-dialog" :title="t('com.dataTable.button.title')" v-model="visible" destroy-on-close
:close-on-click-modal="false" append-to-body
width="980px">
<template v-if="activeRow">
<FnEditor ref="fn" v-model="activeRow[activeKey]" :args="activeArgs"
:name="activeKey"></FnEditor>
</template>
<template v-else>
<el-table :data="column" size="small">
<el-table-column type="index" width="50"/>
<el-table-column :label="t('props.preview')" width="100">
<template #default="{row}">
<el-button v-bind="btnProps(row)">{{ row.name }}</el-button>
</template>
</el-table-column>
<el-table-column width="100">
<template #default="{row}">
<el-input v-model="row.key"></el-input>
</template>
<template #header>
ID<span style="color:red;">*</span>
</template>
</el-table-column>
<el-table-column>
<template #default="{row}">
<el-input v-model="row.name"></el-input>
</template>
<template #header>
{{ t('props.name') }}<span style="color:red;">*</span>
</template>
</el-table-column>
<el-table-column :label="t('event.type')" width="120">
<template #default="{row}">
<el-select v-model="row.type">
<el-option v-for="opt in type"
:label="opt.label"
:value="opt.value"
:key="opt.value"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column :label="t('style.font.size')" width="120">
<template #default="{row}">
<el-select v-model="row.size">
<el-option v-for="opt in size"
:label="opt.label"
:value="opt.value"
:key="opt.value"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column :label="t('style.decoration.name')" width="120">
<template #default="{row}">
<el-select multiple v-model="row.prop">
<el-option v-for="opt in decoration"
:label="opt.label"
:value="opt.value"
:key="opt.value"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column :label="t('props.hide')" width="80">
<template #default="{row}">
<el-switch v-model="row.hidden"></el-switch>
</template>
</el-table-column>
<el-table-column :label="t('props.callback')" width="80">
<template #default="{row}">
<div class="_fd-tcb-btn" @click="handle(row, 'handle', ['props', 'scope', 'api'])">{{ t('com.dataTable.handle') }}<i class="fc-icon icon-edit"></i>
</div>
<div class="_fd-tcb-btn" @click="handle(row, 'click', ['scope', 'api'])">{{ t('com.dataTable.click') }}<i class="fc-icon icon-edit"></i>
</div>
</template>
</el-table-column>
<el-table-column :label="t('tableOptions.handle')" width="80">
<template #default="{$index}">
<i class="fc-icon icon-add-circle" @click="add($index)"></i>
<i class="fc-icon icon-delete-circle" @click="remove($index)"></i>
</template>
</el-table-column>
</el-table>
<el-button link type="primary" @click="add()">
<i class="fc-icon icon-add-circle"></i>
{{ t('tableOptions.add') }}
</el-button>
</template>
<template #footer>
<div>
<el-button size="default" @click="close">{{ t('props.cancel') }}</el-button>
<el-button type="primary" size="default" @click="submit">{{
t('props.ok')
}}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import {defineComponent} from 'vue';
import errorMessage from '../../utils/message';
import {deepCopy} from '@form-create/utils/lib/deepextend';
export default defineComponent({
name: 'TableColumnConfig',
props: {
modelValue: Array,
},
inject: ['designer'],
data() {
return {
column: [],
visible: false,
activeRow: null,
activeKey: '',
activeArgs: [],
};
},
computed: {
t() {
return this.designer.setupState.t;
},
configured() {
return !!this.modelValue
},
decoration() {
return ['link', 'round', 'plain', 'disabled'].map(v => {
return {label: this.t('com.dataTable.button.' + v), value: v};
});
},
size() {
return ['large', 'default', 'small'].map(v => {
return {label: this.t('props.' + v), value: v};
});
},
type() {
return ['primary', 'success', 'warning', 'danger', 'info'].map(v => {
return {label: this.t('props.' + v), value: v};
})
},
},
watch: {
visible(v) {
if (v) {
this.tidyValue();
} else {
this.activeRow = null;
}
},
},
methods: {
btnProps(row) {
const prop = row.prop || [];
return {
type: row.type,
size: row.size,
round: prop.indexOf('round') > -1,
link: prop.indexOf('link') > -1,
plain: prop.indexOf('plain') > -1,
disabled: prop.indexOf('disabled') > -1,
}
},
defaultData() {
return {
key: this.column.length + 1,
name: this.t('props.button') + (this.column.length + 1)
}
},
add(idx) {
idx != null ? this.column.splice(idx + 1, 0, this.defaultData()) : this.column.push(this.defaultData());
},
handle(rule, key, args) {
this.activeKey = key;
this.activeRow = rule;
this.activeArgs = args;
},
remove(idx) {
this.column.splice(idx, 1);
},
tidyValue() {
this.column = deepCopy(this.modelValue || []);
if (!this.column.length) {
this.add();
}
},
close() {
if (this.activeRow) {
this.activeRow = null;
} else {
this.visible = false;
}
},
submit() {
if (this.activeRow) {
if (this.$refs.fn.save()) {
this.activeRow = null;
}
return;
}
const value = [];
for (let i = 0; i < this.column.length; i++) {
const col = this.column[i];
if (!col.name) {
errorMessage(this.t('com.dataTable.requiredName'));
return;
}
if (!col.key) {
errorMessage(this.t('com.dataTable.requiredKey'));
return;
}
value.push({...col});
}
this.$emit('update:modelValue', value);
this.$emit('change', value);
this.visible = false;
},
}
});
</script>
<style>
._fd-table-button-config, ._fd-table-button-config .el-badge {
width: 100%;
}
._fd-table-button-config .el-button {
font-weight: 400;
width: 100%;
}
._fd-tcb-dialog .el-dialog__body {
height: 500px;
overflow: auto;
}
._fd-tcb-btn{
display: flex;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,359 @@
<template>
<div class="_fd-table-column-config">
<el-badge type="warning" is-dot :hidden="!configured">
<el-button class="_fd-plain-button" plain @click="visible=true" size="small">
{{ t('com.dataTable.column.btn') }}
</el-button>
</el-badge>
<el-dialog class="_fd-tcc-dialog _fd-config-dialog" :title="t('com.dataTable.column.title')" v-model="visible"
destroy-on-close
:close-on-click-modal="false" append-to-body
width="980px">
<template v-if="activeRow">
<FnEditor ref="fn" v-model="activeRow.render" :args="['scope', 'h',' resolveComponent', 'api']"
name="render"></FnEditor>
</template>
<el-table v-show="!activeRow" :data="column" size="small" row-key="id" class="_fd-tcc-table">
<el-table-column type="index" width="50"/>
<el-table-column :label="t('com.dataTable.column.prop')" width="130">
<template #default="{row}">
<template v-if="!row.children || !row.children.length">
<el-input v-model="row.prop" v-if="!propColumns || !propColumns.length"></el-input>
<el-select v-model="row.prop" allow-create clearable default-first-option filterable
v-else>
<template v-for="value in propColumns">
<el-option :label="value" :value="value">{{ value }}</el-option>
</template>
</el-select>
</template>
<template v-else>
{{ t('com.dataTable.header') }}
</template>
</template>
</el-table-column>
<el-table-column width="100">
<template #default="{row}">
<el-input v-model="row.label"></el-input>
</template>
<template #header>
{{ t('props.title') }}<span style="color:red;">*</span>
</template>
</el-table-column>
<el-table-column :label="t('style.width')" width="100">
<template #default="{row}">
<el-input v-model="row.width" v-if="!row.children || !row.children.length"></el-input>
</template>
</el-table-column>
<el-table-column :label="t('com.dataTable.filter')" width="120">
<template #default="{row}">
<el-select v-model="row.filter" multiple v-if="!row.children || !row.children.length" clearable>
<template v-for="value in getColumnData(row.prop)">
<el-option :label="value" :value="value">{{ value }}</el-option>
</template>
</el-select>
</template>
</el-table-column>
<el-table-column label="Class">
<template #default="{row}">
<el-input v-model="row.className"></el-input>
</template>
</el-table-column>
<el-table-column :label="t('com.dataTable.column.sort')" width="100">
<template #default="{row}">
<el-select v-model="row.sortable" v-if="!row.children || !row.children.length" clearable>
<el-option v-for="opt in sortable"
:label="opt.label"
:value="opt.value"
:key="opt.value"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column :label="t('props.position')" width="100">
<template #default="{row}">
<el-select v-model="row.fixed" clearable>
<el-option v-for="opt in fixed"
:label="opt.label"
:value="opt.value"
:key="opt.value || 'default'"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column :label="t('style.font.align')" width="100">
<template #default="{row}">
<el-select v-model="row.align" clearable>
<el-option v-for="opt in align"
:label="opt.label"
:value="opt.value"
:key="opt.value"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column width="110">
<template #default="{row}">
<div class="flex">
<el-select v-model="row.format" clearable>
<el-option v-for="opt in format" :label="opt.label" :value="opt.value"
:key="opt.value"></el-option>
</el-select>
<i class="fc-icon icon-edit" v-if="row.format === 'custom'" @click="editFn(row)"></i>
</div>
</template>
<template #header>
{{ t('props.render') }}<span style="color:red;">*</span>
</template>
</el-table-column>
<el-table-column :label="t('props.hide')" width="50" fixed="right">
<template #default="{row}">
<el-switch v-model="row.hidden" v-if="!row.children || !row.children.length"></el-switch>
</template>
</el-table-column>
<el-table-column :label="t('tableOptions.handle')" width="90" fixed="right">
<template #default="{row, $index}">
<i class="fc-icon icon-add-circle" @click="add($index)"></i>
<i class="fc-icon icon-add-child" @click="addChild(row)"></i>
<i class="fc-icon icon-delete-circle" @click="remove(row)"></i>
</template>
</el-table-column>
</el-table>
<el-button v-show="!activeRow" link type="primary" @click="add()">
<i class="fc-icon icon-add-circle"></i>
{{ t('tableOptions.add') }}
</el-button>
<template #footer>
<div>
<el-button size="default" @click="close">{{ t('props.cancel') }}</el-button>
<el-button type="primary" size="default" @click="submit">{{
t('props.ok')
}}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import {defineComponent} from 'vue';
import errorMessage from '../../utils/message';
import {deepCopy} from '@form-create/utils/lib/deepextend';
import {hasProperty} from '@form-create/utils/lib/type';
import FnEditor from '../FnEditor.vue';
import uniqueId from '@form-create/utils/lib/unique';
import {parseFn} from '@form-create/utils/lib/json';
export default defineComponent({
name: 'TableColumnConfig',
components: {FnEditor},
props: {
modelValue: Array,
},
inject: ['designer'],
data() {
return {
column: [],
visible: false,
activeRow: null,
};
},
computed: {
t() {
return this.designer.setupState.t;
},
configured() {
return !!this.modelValue
},
list() {
return (this.designer.setupState.activeRule?.__fc__.el.list) || [];
},
propColumns() {
return Object.keys(this.list[0] || {});
},
format() {
return ['default', 'tag', 'image', 'custom'].map(v => {
return {label: this.t('com.dataTable.format.' + v), value: v};
});
},
align() {
return ['left', 'center', 'right'].map(v => {
return {label: this.t('props.' + v), value: v};
});
},
fixed() {
return [false, 'left', 'right'].map(v => {
return {label: this.t('com.dataTable.fixed.' + (v || 'default')), value: v};
});
},
sortable() {
return [false, true, 'custom'].map(v => {
return {
label: this.t('com.dataTable.sortable.' + (typeof v === 'boolean' ? (v ? 'default' : 'disabled') : 'custom')),
value: v
};
});
},
},
watch: {
visible(v) {
if (v) {
this.tidyValue();
} else {
this.activeRow = null;
}
},
},
methods: {
getColumnData(prop) {
const data = [];
if (!prop) {
return data;
}
(this.list || []).forEach(v => {
if (hasProperty(v, prop) && data.indexOf(v[prop]) === -1) {
data.push(v[prop]);
}
});
return data;
},
add(idx) {
const item = {format: 'default', filter: [], id: uniqueId()};
idx != null ? this.column.splice(idx + 1, 0, item) : this.column.push(item);
},
addChild(column) {
const item = {p: column, format: 'default', filter: [], id: uniqueId()};
if (!column.children) {
column.children = [];
}
column.children.push(item);
},
remove(column) {
const columns = (column.p && column.p.children) || this.column;
columns.splice(columns.indexOf(column), 1);
if (column.p && !columns.length) {
delete column.p.children;
}
},
editFn(row) {
this.activeRow = row;
},
updateFn() {
this.activeRow = null;
},
tidyValue() {
this.column = this.fullId(deepCopy(this.modelValue || []));
if (!this.column.length) {
this.add();
}
},
fullId(columns, p) {
columns.map(column => {
if (!column.id) {
column.id = uniqueId();
}
column.p = p;
if (column.children) {
this.fullId(column.children, column);
}
});
return columns;
},
close() {
if (this.activeRow) {
this.activeRow = null;
} else {
this.visible = false;
}
},
parseColumns(columns) {
return columns.map(col => {
const temp = {...col};
delete temp.p;
if (temp.children && temp.children.length > 0) {
temp.children = this.parseColumns(temp.children);
} else {
delete temp.children;
}
return temp;
})
},
submit() {
if (this.activeRow) {
if (this.$refs.fn.save()) {
this.activeRow = null;
}
return;
}
const value = [];
const columns = this.parseColumns(this.column)
for (let i = 0; i < columns.length; i++) {
const col = columns[i];
if (!col.label) {
errorMessage(this.t('com.dataTable.requiredLabel'));
return;
}
const temp = {...col};
if (temp.label) {
if (!temp.children) {
if (temp.format !== 'custom') {
delete temp.render;
} else if (!temp.render) {
errorMessage(this.t('com.dataTable.requiredRender'));
return;
}
}
if (temp.render) {
temp.render = parseFn(temp.render);
}
value.push(temp);
}
}
this.$emit('update:modelValue', value);
this.$emit('change', value);
this.visible = false;
},
}
});
</script>
<style>
._fd-table-column-config, ._fd-table-column-config .el-badge {
width: 100%;
}
._fd-table-column-config .el-button {
font-weight: 400;
width: 100%;
}
._fd-tcc-dialog .flex {
display: flex;
width: 100%;
}
._fd-tcc-dialog .el-dialog__body {
height: 500px;
overflow: auto;
}
._fd-tcc-dialog ._fd-fn {
height: 100%;
}
._fd-tcc-table .fc-icon {
cursor: pointer;
}
._fd-tcc-table .fc-icon + .fc-icon {
margin-left: 4px;
}
._fd-tcc-table .cell {
display: flex;
flex-direction: row;
align-items: center;
}
._fd-tcc-table .el-table__indent {
padding-left: 8px !important;
}
</style>

View File

@ -0,0 +1,115 @@
<template>
<el-dialog class="_fc-dialog" v-bind="$attrs" :fullscreen="max" v-model="visible" destroyOnClose>
<button class="el-dialog__headerbtn" type="button" style="right: 36px;" v-if="!$attrs.fullscreen">
<i class="fc-icon icon-page-min" v-if="max" @click="max=false"></i>
<i class="fc-icon icon-page-max" v-else @click="max=true"></i>
</button>
<component :is="Form" :option="formOptions" :rule="formRule" :extendOption="true"
v-model:api="fapi"
:model-value="value"
:subForm="false"
@change="formChange"
@emit-event="$emit"></component>
<template #footer v-if="footer !== false">
<el-button @click="close">{{formCreateInject.t('close') || '关闭'}}</el-button>
<el-button type="primary" @click="handleConfirm">{{formCreateInject.t('ok') || '确定'}}</el-button>
</template>
</el-dialog>
</template>
<script>
import {defineComponent, markRaw, onUnmounted, reactive} from 'vue';
import {deepCopy} from '@form-create/utils/lib/deepextend';
export default defineComponent({
name: 'FcDialog',
emits: ['confirm', 'submit', 'validateFail', 'update:modelValue'],
props: {
formData: Object,
options: {
type: Object,
default: () => reactive(({
submitBtn: false,
resetBtn: false,
}))
},
rule: Array,
autoClose: {
type: Boolean,
default: true
},
footer: {
type: Boolean,
default: true
},
preview: Boolean,
modelValue: Object,
formCreateInject: Object,
},
computed: {
formOptions() {
const opt = {...this.options};
if(this.preview) {
opt.preview = this.preview;
}
return opt;
}
},
data() {
return {
visible: false,
max: this.$attrs.fullscreen || false,
fapi: {},
value: {},
formRule: [],
Form: markRaw(this.formCreateInject.form.$form()),
}
},
methods: {
formChange() {
this.$emit('update:modelValue', this.fapi.formData());
},
open(formData) {
this.$nextTick(() => {
this.visible = true;
this.value = deepCopy(formData || this.modelValue || this.formData || {});
this.formRule = deepCopy(this.rule || []);
})
},
close() {
this.visible = false;
},
handleConfirm() {
this.$emit('confirm', this.fapi);
this.fapi.submit().then(formData => {
this.$emit('submit', formData, this.fapi, this.close);
if (this.autoClose) {
this.close();
}
}).catch(e => {
this.$emit('validateFail', e, this.fapi);
});
},
},
mounted() {
this.formCreateInject.api.top.bus.$on('fc.closeDialog', this.close);
onUnmounted(() => {
this.formCreateInject.api.top.bus.$off('fc.closeDialog', this.close);
})
}
});
</script>
<style>
._fc-dialog .el-dialog__headerbtn {
display: flex;
align-items: center;
justify-content: center;
color: var(--el-color-info);
}
._fc-dialog .el-dialog__headerbtn:hover .fc-icon {
color: var(--el-color-primary);
}
</style>

View File

@ -0,0 +1,84 @@
<template>
<div class="_fd-dialog el-dialog" :style="`--fc-dialog-height:${dragConHeight-23}px;`">
<header class="el-dialog__header show-close">
<span class="el-dialog__title">{{ title }}</span>
<button class="el-dialog__headerbtn" type="button" style="right: 48px;" v-if="!fullscreen">
<i class="fc-icon icon-page-max"></i></button>
<button class="el-dialog__headerbtn" type="button">
<i class="el-icon el-dialog__close">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path fill="currentColor"
d="M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"></path>
</svg>
</i>
</button>
</header>
<div class="el-dialog__body">
<slot></slot>
</div>
<footer class="el-dialog__footer">
<template v-if="footer !== false">
<el-button>{{ t('props.close') }}</el-button>
<el-button type="primary">{{ t('props.ok') }}</el-button>
</template>
</footer>
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'FcDialog',
inject: ['designer'],
inheritAttrs: false,
props: {
title: String,
footer: {
type: Boolean,
default: true
},
fullscreen: Boolean,
},
computed: {
dragConHeight() {
return this.designer.setupState.dragConHeight;
},
t() {
return this.designer.setupState.t;
},
},
});
</script>
<style>
._fd-dialog.el-dialog {
width: calc(100% - 20px);
margin: 10px;
}
._fd-dialog .el-dialog__headerbtn {
display: flex;
align-items: center;
justify-content: center;
color: var(--el-color-info);
}
._fd-dialog .el-dialog__headerbtn:hover .fc-icon {
color: var(--el-color-primary);
}
._fd-dialog .el-dialog__body > ._fd-drag-box {
display: flex;
flex-wrap: wrap;
align-content: flex-start;
justify-content: flex-start;
align-items: flex-start;
box-sizing: border-box;
position: relative;
overflow: auto;
height: calc(var(--fc-dialog-height) - 125px);
outline: 1px dashed var(--fc-tool-border-color);
}
</style>

View File

@ -0,0 +1,144 @@
<template>
<el-drawer class="_fc-drawer" v-bind="$attrs" :size="max ? '100%' : size" v-model="visible" destroyOnClose>
<template #header>
<span class="el-drawer__title">
{{ title }}
</span>
<button class="el-drawer__close-btn" type="button" v-if="size !== '100%'">
<i class="fc-icon icon-page-min" v-if="max" @click="max=false"></i>
<i class="fc-icon icon-page-max" v-else @click="max=true"></i>
</button>
</template>
<component :is="Form" :option="formOptions" :rule="formRule" :extendOption="true"
v-model:api="fapi"
:model-value="value"
:subForm="false"
@change="formChange"
@emit-event="$emit"></component>
<template #footer>
<template v-if="footer !== false">
<el-button @click="close">{{formCreateInject.t('close') || '关闭'}}</el-button>
<el-button type="primary" @click="handleConfirm">{{formCreateInject.t('ok') || '确定'}}</el-button>
</template>
</template>
</el-drawer>
</template>
<script>
import {defineComponent, markRaw, onUnmounted, reactive} from 'vue';
import {deepCopy} from '@form-create/utils/lib/deepextend';
export default defineComponent({
name: 'FcDialog',
emits: ['confirm', 'submit', 'validateFail', 'update:modelValue'],
props: {
formData: Object,
options: {
type: Object,
default: () => reactive(({
submitBtn: false,
resetBtn: false,
}))
},
size: [Number, String],
title: String,
rule: Array,
autoClose: {
type: Boolean,
default: true
},
footer: {
type: Boolean,
default: true
},
preview: Boolean,
modelValue: Object,
formCreateInject: Object,
},
computed: {
formOptions() {
const opt = {...this.options};
if(this.preview) {
opt.preview = this.preview;
}
return opt;
}
},
data() {
return {
visible: false,
max: this.size === '100%',
fapi: {},
value: {},
formRule: [],
Form: markRaw(this.formCreateInject.form.$form()),
}
},
methods: {
formChange() {
this.$emit('update:modelValue', this.fapi.formData());
},
open(formData) {
this.$nextTick(() => {
this.visible = true;
this.value = deepCopy(formData || this.modelValue || this.formData || {});
this.formRule = deepCopy(this.rule || []);
});
},
close() {
this.visible = false;
},
handleConfirm() {
this.$emit('confirm', this.fapi);
this.fapi.submit().then(formData => {
this.$emit('submit', formData, this.fapi, this.close);
if (this.autoClose) {
this.close();
}
}).catch(e => {
this.$emit('validateFail', e, this.fapi);
});
},
},
mounted() {
this.formCreateInject.api.top.bus.$on('fc.closeDialog', this.close);
onUnmounted(() => {
this.formCreateInject.api.top.bus.$off('fc.closeDialog', this.close);
})
}
});
</script>
<style>
._fc-drawer .el-drawer__header {
border-bottom: 1px solid var(--fc-line-color-3);
padding: 14px 24px 14px 20px;
margin-bottom: 0;
font-size: 15px;
font-weight: 600;
color: #333
}
._fc-drawer .el-drawer__body {
padding: 10px 24px 50px 24px;
}
._fc-drawer .el-drawer__close-btn {
font-size: 14px;
color: #909399
}
._fc-drawer .el-drawer__footer {
z-index: 10;
position: absolute;
left: 0;
bottom: 0;
right: 0;
width: 100%;
background: var(--fc-bg-color-1);
box-shadow: 0 -2px 4px 0 rgba(0, 0, 0, .05);
text-align: center;
padding: 10px 0;
}
</style>

View File

@ -0,0 +1,97 @@
<template>
<div class="el-drawer _fd-drawer" :style="`--fc-drawer-height:${dragConHeight-23}px;`">
<span class="el-drawer__sr-focus" tabindex="-1"></span>
<header class="el-drawer__header">
<span>{{ title }}</span>
<button class="el-drawer__close-btn" type="button" v-if="size !== '100%'">
<i class="fc-icon icon-page-max"></i></button>
<button class="el-drawer__close-btn" type="button">
<i class="el-icon el-drawer__close">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path fill="currentColor"
d="M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"></path>
</svg>
</i></button>
</header>
<div class="el-drawer__body">
<slot></slot>
</div>
<div class="el-drawer__footer">
<template v-if="footer !== false">
<el-button>{{ t('props.close') }}</el-button>
<el-button type="primary">{{ t('props.ok') }}</el-button>
</template>
</div>
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'FcDialog',
inject: ['designer'],
inheritAttrs: false,
props: {
title: String,
footer: {
type: Boolean,
default: true
},
size: String,
},
computed: {
dragConHeight() {
return this.designer.setupState.dragConHeight;
},
t() {
return this.designer.setupState.t;
},
},
});
</script>
<style>
._fd-drawer.el-drawer {
width: 100%;
box-shadow: unset;
}
._fd-drawer .el-drawer__header {
border-bottom: 1px solid var(--fc-line-color-3);
padding: 14px 24px 14px 20px;
margin-bottom: 0;
font-size: 15px;
font-weight: 600;
color: #333
}
._fd-drawer .el-drawer__body {
padding: 12px;
}
._fd-drawer .el-drawer__close-btn {
font-size: 14px;
color: #909399
}
._fd-drawer .el-drawer__footer {
box-shadow: 0 -2px 4px 0 rgba(0, 0, 0, .05);
text-align: center;
padding: 10px 0;
}
._fd-drawer .el-drawer__body > ._fd-drag-box {
display: flex;
flex-wrap: wrap;
align-content: flex-start;
justify-content: flex-start;
align-items: flex-start;
box-sizing: border-box;
position: relative;
overflow: auto;
height: calc(var(--fc-drawer-height) - 105px);
outline: 1px dashed var(--fc-tool-border-color);
}
</style>

View File

@ -0,0 +1,425 @@
<template>
<div class="_fc-echarts" ref="chart">
</div>
</template>
<script>
import loadjs from '../../utils/loadjs/loadjs';
import {defineComponent, markRaw} from 'vue';
import debounce from '@form-create/utils/lib/debounce';
export default defineComponent({
name: 'FcEcharts',
data() {
return {
chart: null,
debounceLoad: debounce(() => {
this.load();
}, 600),
debounceResize: debounce(() => {
this.chart && this.chart.resize();
}, 10),
};
},
emits: ['beforeLoad', 'loaded', 'click'],
props: {
title: String,
value: Number,
min: Number,
max: Number,
name: String,
valueFormat: String,
subtitle: String,
funnelSort: String,
config: Object,
data: Array,
indicator: Array,
smooth: Boolean,
stripe: Boolean,
showLegend: {
type: Boolean,
default: true
},
loadOptions: {
type: Function,
default: () => {
}
},
showSeriesLabel: Boolean,
type: String,
pieType: String,
stack: Boolean,
barBackgroundColor: String,
},
watch: {
'$props': {
handler() {
this.debounceLoad();
},
deep: true,
}
},
methods: {
getSeries() {
const append = {
type: 'line',
stack: this.stack ? 'Total' : '',
smooth: this.smooth,
showBackground: false,
label: {
show: this.showSeriesLabel,
position: this.stripe ? 'inside' : 'top'
}
};
if (this.type === 'area') {
append.areaStyle = {};
append.emphasis = {
focus: 'series'
};
} else if (this.type === 'bar') {
append.type = 'bar';
if (this.barBackgroundColor) {
append.showBackground = true;
append.backgroundStyle = {
color: this.barBackgroundColor
};
}
}
let series = this.config?.series || [];
if (!series.length) {
return []
}
if (typeof series[0] != 'object') {
series = [{
data: series,
}]
}
series = series.map(item => {
return {
...append, ...item
};
})
return series;
},
getTooltip() {
const tooltip = {
trigger: 'axis',
valueFormat: undefined,
}
if (this.valueFormat) {
tooltip.valueFormatter = (value) => {
if (!this.valueFormat) {
return value;
}
return this.valueFormat.replaceAll('{value}', value);
}
}
if (this.type === 'bar') {
tooltip.axisPointer = {
type: 'shadow'
}
}
return tooltip;
},
getAxis() {
if (!this.stripe) {
return {
xAxis: {
type: 'category',
boundaryGap: this.type === 'bar',
data: this.config?.category
},
yAxis: {
type: 'value'
},
}
} else {
return {
yAxis: {
type: 'category',
boundaryGap: this.type === 'bar',
data: this.config?.category || []
},
xAxis: {
type: 'value'
},
}
}
},
getDefOptions() {
return {
title: {
text: this.title,
subtext: this.subtitle,
},
tooltip: this.getTooltip(),
legend: {
left: 'right',
show: this.showLegend,
},
grid: {
left: '20px',
right: '20px',
bottom: '20px',
containLabel: true
},
...this.getAxis(),
series: this.getSeries()
};
},
getPieOptions() {
const append = {
radius: '50%',
center: '50%',
startAngle: 0,
avoidLabelOverlap: true,
labelLine: {
show: true,
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
};
if (this.pieType === 'doughnut') {
append.radius = ['40%', '70%'];
append.avoidLabelOverlap = false;
} else if (this.pieType === 'half-doughnut') {
append.radius = ['40%', '70%'];
append.center = ['50%', '70%'];
append.startAngle = 180;
append.endAngle = 360;
}
return {
title: {
text: this.title,
subtext: this.subtitle,
left: 'left'
},
tooltip: {
trigger: 'item'
},
legend: {
left: 'right',
show: this.showLegend,
},
series: [
{
type: 'pie',
data: this.data,
...append,
}
]
}
},
getGaugeOptions() {
return {
title: {
text: this.title,
subtext: this.subtitle,
left: 'center'
},
series: [
{
name: 'Pressure',
type: 'gauge',
min: this.min || 0,
max: this.max || 60,
progress: {
show: true
},
detail: {
valueAnimation: true,
formatter: '{value}'
},
data: [
{
value: this.value,
name: this.name
}
]
}
]
}
},
getRadarOptions() {
return {
title: {
text: this.title,
subtext: this.subtitle,
left: 'left'
},
tooltip: {
trigger: 'axis'
},
legend: {
left: 'right',
show: this.showLegend,
},
radar: {
indicator: this.indicator
},
series: [
{
type: 'radar',
tooltip: {
trigger: 'item'
},
data: this.data
}
]
}
},
getScatterOptions() {
return {
title: {
text: this.title,
subtext: this.subtitle,
left: 'left'
},
tooltip: {
trigger: 'axis'
},
legend: {
left: 'right',
show: true,
},
xAxis: {
scale: true
},
yAxis: {
scale: true
},
grid: {
left: '20px',
right: '20px',
bottom: '20px',
containLabel: true
},
series: (this.data || []).map((data) => {
if (Array.isArray(data)) {
return {
type: 'scatter',
data: data
}
} else {
return {type: 'scatter', ...data}
}
})
}
},
getFunnelOptions() {
return {
title: {
text: this.title,
subtext: this.subtitle,
left: 'left'
},
tooltip: {
trigger: 'item'
},
legend: {
left: 'right',
show: this.showLegend,
},
series: [
{
name: 'Funnel',
type: 'funnel',
left: '10%',
top: '40px',
bottom: '20px',
width: '80%',
min: 0,
max: Math.max(...(this.data || []).map(v => v.value)),
minSize: '0%',
maxSize: '100%',
sort: this.funnelSort || 'descending',
gap: 2,
label: {
show: true,
position: 'inside'
},
labelLine: {
length: 10,
lineStyle: {
width: 1,
type: 'solid'
}
},
itemStyle: {
borderColor: '#fff',
borderWidth: 1
},
emphasis: {
label: {}
},
data: this.data,
}
]
}
},
load() {
this.$nextTick(() => {
loadjs.ready('echarts', () => {
this.chart = markRaw(window.echarts.init(this.$refs.chart));
this.chart.on('click', (...args) => {
this.$emit('click', ...args);
});
let options;
if (this.type === 'pie') {
options = this.getPieOptions();
} else if (this.type === 'funnel') {
options = this.getFunnelOptions();
} else if (this.type === 'gauge') {
options = this.getGaugeOptions();
} else if (this.type === 'radar') {
options = this.getRadarOptions();
} else if (this.type === 'scatter') {
options = this.getScatterOptions();
} else if (this.type === 'custom') {
options = this.loadOptions(this.config, this.chart) || {};
if (typeof options.then === 'function') {
options.then(res => {
this.$emit('beforeLoad', this.chart, res);
this.chart.setOption(res);
this.$emit('loaded', this.chart, res);
})
return;
}
} else {
options = this.getDefOptions();
}
this.$emit('beforeLoad', this.chart, options);
this.chart.setOption(options);
this.$emit('loaded', this.chart, options);
});
});
},
},
created() {
if (window.echarts) {
loadjs.done('echarts');
} else if (!loadjs.isDefined('echarts')) {
loadjs.loadNpm('echarts@5.6.0/dist/echarts.min.js', 'echarts');
}
},
mounted() {
this.load();
window.addEventListener('resize', this.debounceResize);
},
unmounted() {
window.removeEventListener('resize', this.debounceResize);
}
});
</script>
<style>
._fc-echarts {
width: 100%;
height: 300px;
}
</style>

View File

@ -0,0 +1,465 @@
<template>
<div class="_fc-infinite-table-form" :class="{'_fc-disabled': disabled}">
<component :is="Form" :option="options" :rule="rule" :extendOption="true"
@change="formChange"
:disabled="disabled"
v-model:api="fapi"
@emit-event="$emit"></component>
<el-button link type="primary" class="fc-clock" v-if="!max || max > this.trs.length"
@click="addRaw(true)"><i class="fc-icon icon-add-circle" style="font-weight: 700;"></i>
{{ formCreateInject.t('add') || '添加' }}
</el-button>
</div>
</template>
<script>
import {markRaw, reactive} from 'vue';
import {deepCopy} from '@form-create/utils/lib/deepextend';
import {hasProperty} from '@form-create/utils/lib/type';
export default {
name: 'InfiniteTableForm',
emits: ['change', 'add', 'delete', 'update:modelValue'],
props: {
formCreateInject: Object,
modelValue: {
type: Array,
default: () => [],
},
columns: {
type: Array,
required: true,
default: () => []
},
options: {
type: Object,
default: () => reactive(({
submitBtn: false,
resetBtn: false,
}))
},
max: Number,
layerMax: {
type: Number,
default: 0,
},
childrenField: String,
disabled: Boolean,
},
computed: {
preview() {
return this.formCreateInject.preview;
},
subField() {
return this.childrenField || 'children';
},
},
watch: {
modelValue() {
this.updateTable()
},
'formCreateInject.preview'(n) {
this.trs.forEach((tr, i) => {
if (tr.children[1]) {
tr.children[1].children[0].props.colspan = this.rule[0].children[0].children[0].children.length - (n ? 1 : 0);
}
tr.children[0].children[0].children[0].hidden = this.layerMax === 1 || n && !(this.modelValue && this.modelValue[i] && Array.isArray(this.modelValue[i][this.subField]) && this.modelValue[i][this.subField].length > 0);
});
},
},
data() {
return {
rule: [],
trs: [],
fapi: {},
Form: markRaw(this.formCreateInject.form.$form()),
copyTrs: '',
oldValue: ''
};
},
methods: {
formChange(field, _, rule, api, flag) {
if (false === flag) {
this.updateValue();
}
},
updateValue() {
const value = this.trs.map((tr, idx) => {
const formData = {
...(this.modelValue[idx] || {}),
...this.fapi.getChildrenFormData(tr)
}
if (!hasProperty(formData, this.subField) && this.modelValue[idx]) {
formData[this.subField] = this.modelValue[idx][this.subField];
}
if (formData[this.subField] == null) {
delete formData[this.subField];
}
return formData;
});
const str = JSON.stringify(value);
if (str !== this.oldValue) {
this.oldValue = str;
this.$emit('update:modelValue', value);
this.$emit('change', value);
}
},
setRawData(idx, formData) {
const raw = this.trs[idx];
this.fapi.setChildrenFormData(raw, formData, true);
},
updateTable() {
const str = JSON.stringify(this.modelValue);
if (this.oldValue === str) {
return;
}
this.oldValue = str;
this.trs = this.trs.splice(0, this.modelValue.length);
if (!this.modelValue.length) {
this.addRaw();
}
this.modelValue.forEach((data, idx) => {
if (!this.trs[idx]) {
this.addRaw();
}
this.setRawData(idx, data || {});
});
this.rule[0].children[1].children = this.trs;
},
delRaw(idx) {
if (this.disabled) {
return;
}
this.trs.splice(idx, 1);
this.updateValue();
if (this.trs.length) {
this.trs.forEach(tr => this.updateRaw(tr));
} else {
this.addRaw();
}
this.$emit('delete', idx);
},
addRaw(flag) {
if (flag && this.disabled) {
return;
}
const tr = this.formCreateInject.form.parseJson(this.copyTrs)[0];
const template = {
type: 'template',
subRule: true,
children: []
};
template.children.push(tr);
this.trs.push(template);
this.trs.forEach(tr => this.updateRaw(tr));
flag && this.$emit('add', this.trs);
},
updateRaw(tr) {
const idx = this.trs.indexOf(tr);
const row = tr.children[0];
row.children[0].children[0].hidden = this.layerMax === 1 || this.preview && !(this.modelValue && this.modelValue[idx] && Array.isArray(this.modelValue[idx][this.subField]) && this.modelValue[idx][this.subField].length > 0);
row.children[0].children[0].props.onClick = (inject) => {
if (this.trs[idx].children.length === 1) {
if (this.disabled && !(this.modelValue && this.modelValue[idx] && Array.isArray(this.modelValue[idx][this.subField]) && this.modelValue[idx][this.subField].length > 0)) {
return;
}
this.trs[idx].children.push({
type: 'tr',
native: true,
display: true,
children: [{
type: 'td',
native: true,
props: {
colspan: this.rule[0].children[0].children[0].children.length - (this.preview ? 1 : 0),
},
class: '_fc-itf-sub',
children: [{
type: 'infiniteTableForm',
field: this.subField,
value: [...((this.modelValue[idx] && this.modelValue[idx][this.subField]) || [])],
props: {
disabled: this.disabled,
layerMax: this.layerMax === 0 ? 0 : (this.layerMax > 1 ? this.layerMax - 1 : 1),
max: this.max || 0,
columns: deepCopy(this.columns),
options: deepCopy(this.options),
}
}]
}]
});
}
const icon = inject.self.children[0] === '-' ? '+' : '-';
inject.self.children = [icon];
this.trs[idx].children[1].display = icon === '-';
};
row.children[1].props.innerText = idx + 1;
row.children[row.children.length - 1].children[0].props.onClick = () => {
this.delRaw(idx);
};
},
loadRule() {
const header = [{
type: 'th',
native: true,
class: '_fc-itf-sub-idx',
}, {
type: 'th',
native: true,
class: '_fc-itf-head-idx',
props: {
innerText: '#'
}
}];
let body = [{
type: 'td',
class: '_fc-itf-idx',
native: true,
children: [
{
type: 'div',
hidden: false,
children: ['+'],
inject: true,
props: {}
},
]
}, {
type: 'td',
class: '_fc-itf-idx',
native: true,
props: {
innerText: '0'
}
}];
this.columns.forEach((column) => {
header.push({
type: 'th',
native: true,
class: column.required ? '_fc-itf-head-required' : '',
style: column.style,
props: {
innerText: column.label || ''
}
});
body.push({
type: 'td',
native: true,
children: [...(column.rule || [])]
});
});
header.push({
type: 'th',
native: true,
class: '_fc-itf-edit fc-clock',
props: {
innerText: this.formCreateInject.t('operation') || '操作'
}
});
body.push({
type: 'td',
native: true,
class: '_fc-itf-btn fc-clock',
children: [
{
type: 'i',
native: true,
class: 'fc-icon icon-delete',
props: {},
}
],
});
this.copyTrs = this.formCreateInject.form.toJson([
{
type: 'tr',
native: true,
children: body
}
]);
this.rule = [
{
type: 'table',
native: true,
class: '_fc-itf-table',
props: {
border: '1',
cellspacing: '0',
cellpadding: '0',
},
children: [
{
type: 'thead',
native: true,
children: [
{
type: 'tr',
native: true,
children: header
}
]
},
{
type: 'tbody',
native: true,
children: this.trs
}
]
}
]
},
},
created() {
this.loadRule();
},
mounted() {
this.updateTable();
}
};
</script>
<style>
._fc-infinite-table-form {
overflow: auto;
color: var(--fc-text-color-2);
}
._fc-infinite-table-form .form-create .el-form-item {
margin-bottom: 1px;
}
._fc-infinite-table-form .form-create .el-form-item.is-error {
margin-bottom: 22px;
}
._fc-infinite-table-form .el-form-item__label, ._fc-infinite-table-form .van-field__label {
display: none !important;
}
._fc-infinite-table-form .el-form-item__content {
display: flex;
margin-left: 0px !important;
width: 100% !important;
}
._fc-itf-table ._fc-itf-head-idx, ._fc-itf-table ._fc-itf-idx {
width: 40px;
min-width: 40px;
font-weight: 500;
text-align: center;
padding: 0;
}
._fc-itf-idx div {
display: inline-flex;
justify-content: center;
width: 18px;
height: 18px;
line-height: 16px;
border: 1px solid #bfbfbf;
border-radius: 6px;
cursor: pointer;
}
._fc-itf-sub-idx {
width: 30px;
}
._fc-itf-edit, ._fc-itf-btn {
width: 70px;
min-width: 70px;
text-align: center;
}
._fc-itf-btn .fc-icon {
cursor: pointer;
}
._fc-infinite-table-form._fc-disabled ._fc-itf-btn .fc-icon, ._fc-infinite-table-form._fc-disabled > .el-button {
cursor: not-allowed;
}
._fc-itf-table {
width: 100%;
height: 100%;
overflow: hidden;
table-layout: fixed;
border: 1px solid #EBEEF5;
border-bottom: 0 none;
}
._fc-itf-table > thead > tr > th {
border: 0 none;
border-bottom: 1px solid #EBEEF5;
height: 40px;
font-weight: 500;
}
._fc-itf-table ._fc-itf-table > thead {
display: none;
}
._fc-itf-table ._fc-itf-table {
border-right: 0 none;
}
._fc-itf-table > thead > tr > th + th {
border-left: 1px solid #EBEEF5;
}
._fc-itf-table tr {
min-height: 50px;
}
._fc-itf-table ._fc-read-view {
text-align: center;
width: 100%;
}
._fc-itf-table td {
padding: 10px;
min-height: 50px;
min-width: 80px;
position: relative;
box-sizing: border-box;
overflow-wrap: break-word;
/*white-space: nowrap;*/
overflow: hidden;
border: 0 none;
border-bottom: 1px solid #EBEEF5;
}
._fc-itf-table td + td {
border-left: 1px solid #EBEEF5;
}
._fc-itf-table .el-input-number, ._fc-itf-table .el-select, ._fc-itf-table .el-slider, ._fc-itf-table .el-cascader, ._fc-itf-table .el-date-editor {
width: 100%;
}
._fc-infinite-table-form ._fc-itf-sub {
padding: 5px 0 5px 10px;
}
._fc-itf-sub ._fc-table-form {
background-color: var(--fc-bg-color-1);
}
._fc-itf-sub ._fc-tf-table {
border: 0 none;
}
/*._fc-itf-sub ._fc-tf-table thead {
background: #fafafa;
}*/
._fc-itf-sub-idx + ._fc-itf-head-idx, ._fc-itf-idx + ._fc-itf-idx {
border-left: 0 none;
}
._fc-itf-head-required:before {
content: '*';
color: #f56c6c;
margin-right: 4px;
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<div class="_fd-itable-form">
<div class="_fd-itf-wrap" v-if="$slots.default">
<slot></slot>
</div>
<div class="_fc-child-empty" v-else></div>
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'InfiniteTableFormView',
inject: ['designer'],
data() {
return {};
},
computed: {
t() {
return this.designer.setupState.t;
},
},
});
</script>
<style>
._fd-itable-form {
min-height: 130px;
width: 100%;
border: 1px solid var(--fc-line-color-3);
background: var(--fc-bg-color-1);
}
._fd-itable-form ._fc-child-empty {
min-height: 130px;
}
._fd-itf-wrap {
display: flex;
overflow: auto;
}
._fd-itf-wrap > ._fd-drag-tool {
flex-shrink: 0;
display: flex;
margin: 2px;
height: auto;
}
</style>

View File

@ -0,0 +1,128 @@
<script>
import {createVNode, defineComponent, Fragment, shallowRef} from 'vue';
import {deepCopy} from '@form-create/utils/lib/deepextend';
export default defineComponent({
name: 'FcJson',
inheritAttrs: false,
props: {
rule: [Array, String, Object],
type: String,
disabled: Boolean,
expand: Number,
button: {
type: Boolean,
default: true
},
max: {
type: Number,
default: 0
},
min: {
type: Number,
default: 0
},
sortBtn: {
type: Boolean,
default: true
},
modelValue: [Object, Array],
formCreateInject: Object,
},
data() {
return {
fcSubForm: shallowRef(this.formCreateInject.form.component('fcSubForm')),
fcGroup: shallowRef(this.formCreateInject.form.component('fcGroup')),
uni: 0,
formRule: [],
formOptions: {
submitBtn: false,
resetBtn: false,
},
}
},
watch: {
rule() {
this.uni++;
this.loadRule();
},
type() {
this.loadRule();
},
},
render() {
if (this.rule) {
if (this.type === 'object') {
return createVNode(this.fcSubForm, {
key: 2,
...this.$attrs,
modelValue: this.modelValue,
'onUpdate:modelValue': (val) => {
this.$emit('update:modelValue', val);
},
disabled: this.disabled,
formCreateInject: this.formCreateInject,
rule: this.formRule,
options: this.formOptions,
})
} else if (this.type === 'array') {
return createVNode(this.fcGroup, {
key: 3,
...this.$attrs,
modelValue: this.modelValue,
'onUpdate:modelValue': (val) => {
this.$emit('update:modelValue', val);
},
sortBtn: this.sortBtn,
min: this.min,
max: this.max,
expand: this.expand,
button: this.button,
disabled: this.disabled,
formCreateInject: this.formCreateInject,
rule: this.formRule,
options: this.formOptions,
})
}
return createVNode(Fragment, {
key: this.uni,
}, [this.$slots?.default?.()]);
}
},
methods: {
loadRule() {
let rule = deepCopy(this.rule);
if (typeof rule === 'string') {
rule = this.formCreateInject.form.parseJson(rule);
}
if (Array.isArray(rule)) {
this.formRule = rule;
} else if (typeof rule === 'object') {
this.formRule = rule.rule || [];
this.formOptions = {
...{
submitBtn: false,
resetBtn: false,
}, ...rule.options || {}
};
}
if (rule != null) {
if (['array', 'object'].indexOf(this.type) === -1) {
this.formCreateInject.rule.children = [
{
type: 'template',
_fc_drag_skip: true,
children: this.formRule,
}
];
}
} else {
this.formCreateInject.rule.children = [];
}
},
},
created() {
this.rule && this.loadRule();
}
});
</script>

View File

@ -0,0 +1,156 @@
<script>
import {createElementVNode, createVNode, defineComponent, Fragment, shallowRef} from 'vue';
import {deepCopy} from '@form-create/utils/lib/deepextend';
export default defineComponent({
name: 'FcJsonView',
inheritAttrs: false,
inject: ['designer'],
props: {
rule: [Array, String, Object],
type: String,
disabled: Boolean,
expand: Number,
button: {
type: Boolean,
default: true
},
max: {
type: Number,
default: 0
},
min: {
type: Number,
default: 0
},
sortBtn: {
type: Boolean,
default: true
},
modelValue: [Object, Array],
formCreateInject: Object,
},
data() {
return {
fcSubForm: shallowRef(this.formCreateInject.form.component('fcSubForm')),
fcGroup: shallowRef(this.formCreateInject.form.component('fcGroup')),
uni: 0,
formRule: [],
formOptions: {
submitBtn: false,
resetBtn: false,
},
}
},
watch: {
rule() {
this.uni++;
this.loadRule();
},
type() {
this.loadRule();
},
},
render() {
if (this.rule) {
let child = null;
if (this.type === 'object') {
child = createVNode(this.fcSubForm, {
key: 2,
...this.$attrs,
modelValue: this.modelValue,
'onUpdate:modelValue': (val) => {
this.$emit('update:modelValue', val);
},
disabled: this.disabled,
formCreateInject: this.formCreateInject,
rule: this.formRule,
options: this.formOptions,
})
} else if (this.type === 'array') {
child = createVNode(this.fcGroup, {
key: 3,
...this.$attrs,
modelValue: this.modelValue,
'onUpdate:modelValue': (val) => {
this.$emit('update:modelValue', val);
},
sortBtn: this.sortBtn,
expand: 1,
button: this.button,
disabled: this.disabled,
formCreateInject: this.formCreateInject,
rule: this.formRule,
options: this.formOptions,
})
} else {
child = createVNode(Fragment, {
key: 1,
}, [this.$slots?.default?.()]);
}
return createElementVNode('div', {
key: this.uni,
style: {'--fc-json-mask': `'${this.designer.setupState.t('com.fcJson.name')}'`},
class: '_fd-json-container',
}, [child])
} else {
return createElementVNode('div', {
class: '_fd-slot-empty',
innerHTML: this.designer.setupState.t('com.fcJson.empty', {tag: '<span>JSON</span>'})
});
}
},
methods: {
loadRule() {
let rule = deepCopy(this.rule);
if (typeof rule === 'string') {
rule = this.formCreateInject.form.parseJson(rule);
}
if (Array.isArray(rule)) {
this.formRule = rule;
} else if (typeof rule === 'object') {
this.formRule = rule.rule || [];
this.formOptions = {
...{
submitBtn: false,
resetBtn: false,
}, ...rule.options || {}
};
}
if (rule != null) {
if (['array', 'object'].indexOf(this.type) === -1) {
this.formCreateInject.rule.children = [
{
type: 'template',
_fc_drag_skip: true,
children: this.formRule,
}
];
}
} else {
this.formCreateInject.rule.children = [];
}
},
},
created() {
this.rule && this.loadRule();
}
});
</script>
<style>
._fd-json-container {
display: flex;
flex-direction: column;
flex-wrap: wrap;
justify-content: center;
flex: 1;
min-height: 20px;
position: relative;
--fc-json-mask: ' ';
}
._fd-json-container ._fd-dialog, ._fd-json-container ._fd-drawer, ._fd-json-container ._fd-popup {
display: none;
}
</style>

View File

@ -0,0 +1,165 @@
<template>
<div class="_fd-language-config">
<div class="_fc-l-label">{{ t('language.name') }}</div>
<div class="_fc-l-info">
{{ t('warning.language') }}
</div>
<div class="_fd-lc-header">
<el-button size="small" @click="addColumn">{{ t('language.add') }}</el-button>
<el-button size="small" type="danger" plain :disabled="!selected.length" @click="batchRmColumn">
{{ t('language.batchRemove') }}
</el-button>
</div>
<div class="_fd-lc-body">
<el-table :data="column" size="small" ref="table"
@selection-change="selectionChange" row-key="key">
<el-table-column type="selection" width="30px"></el-table-column>
<el-table-column prop="key" label="Key" width="90px"></el-table-column>
<template v-for="item in localeOptions" :key="item.value">
<el-table-column :prop="item.value" :label="item.label" min-width="100px">
<template #default="scope">
<template v-if="scope.row.input">
<el-input size="small" v-model="scope.row[item.value]" @blur="saveColumn(scope.row, true)"></el-input>
</template>
<template v-else>
{{ scope.row[item.value] || '-' }}
</template>
</template>
</el-table-column>
</template>
<el-table-column width="75px" :label="t('tableOptions.handle')" fixed="right">
<template #default="scope">
<div class="_fd-lc-handle">
<i class="fc-icon icon-edit" v-if="!scope.row.input" @click="scope.row.input = true"></i>
<i class="fc-icon icon-check" v-else @click="saveColumn(scope.row)"></i>
<i class="fc-icon icon-group" @click="copy(scope.row.key)"></i>
<i class="fc-icon icon-delete-circle" @click="rmColumn(scope.$index)"></i>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script>
import {defineComponent} from 'vue';
import {copyTextToClipboard} from '../../utils';
export default defineComponent({
name: 'LanguageConfig',
inject: ['designer'],
computed: {
localeOptions() {
return this.designer.setupState.getConfig('localeOptions', [
{value: 'zh-cn', label: '简体中文'},
{value: 'en', label: 'English'},
]);
},
t() {
return this.designer.setupState.t;
},
},
data() {
return {
column: [],
uni: 0,
selected: [],
}
},
methods: {
copy(key) {
copyTextToClipboard(key);
},
addColumn() {
this.column.unshift({
key: this.randomString(),
input: true,
})
},
saveColumn(row, input) {
row.input = input || false;
const language = this.designer.setupState.formOptions.language;
this.localeOptions.forEach(item => {
if (!language[item.value]) {
language[item.value] = {};
}
language[item.value][row.key] = row[item.value];
})
},
rmColumn(idx) {
const row = this.column[idx];
this.column.splice(idx, 1);
const language = this.designer.setupState.formOptions.language;
this.localeOptions.forEach(item => {
if (language[item.value]) {
delete language[item.value][row.key]
}
})
},
batchRmColumn() {
this.selected.forEach(item => {
this.rmColumn(this.column.indexOf(item));
});
this.selected = [];
},
selectionChange(list) {
this.selected = list;
},
randomString() {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < 7; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return characters.charAt((this.uni++) % 26) + result;
}
},
mounted() {
const language = this.designer.setupState.formOptions.language || {};
const column = {};
Object.keys(language).forEach(lang => {
Object.keys(language[lang]).forEach(key => {
if (!column[key]) {
column[key] = {
key: key,
}
}
column[key][lang] = language[lang][key];
})
});
this.column = Object.values(column);
}
});
</script>
<style>
._fd-lc-body, ._fd-lc-header {
padding: 0 12px;
}
._fd-lc-body {
overflow: auto;
}
._fd-lc-header {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
._fd-language-config .el-table__cell {
height: 34px;
}
._fd-lc-handle {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,192 @@
<template>
<el-input class="_fd-language-input" :class="{'is-variable': isVar}" :placeholder="placeholder" :disabled="disabled"
:modelValue="modelValue"
@update:modelValue="onInput"
@blur="$emit('blur')"
:size="size || 'small'">
<template #append v-if="showLanguage !== false">
<el-popover placement="bottom-end" :width="300" :hide-after="0" trigger="click" ref="pop"
popper-class="_fd-language-popover">
<template #reference>
<i class="fc-icon icon-language"></i>
</template>
<div class="_fd-language-list">
<div class="_fd-language-header">
<div class="_fd-language-title">
{{ t('language.select') }}<i class="fc-icon icon-setting" @click="openConfig"></i>
</div>
<div class="_fd-language-name">
<template v-for="item in localeList" :key="item.value">
<div>{{ item.label }}</div>
</template>
</div>
</div>
<template v-for="lang in language" :key="lang.key">
<div class="_fd-language-item" @click="clickLang(lang.key)">
<template v-for="item in localeList" :key="item.value">
<div>{{ lang[item.value] || '-' }}</div>
</template>
</div>
</template>
</div>
</el-popover>
</template>
</el-input>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'LanguageInput',
inject: ['designer'],
emits: ['update:modelValue', 'blur', 'change'],
props: {
size: String,
placeholder: String,
modelValue: String,
disabled: Boolean,
},
computed: {
isVar() {
return !!(this.modelValue || '').match(/^\{\{\s*\$t\.(.+)\s*\}\}$/);
},
t() {
return this.designer.setupState.t;
},
showLanguage() {
return this.designer.setupState.getConfig('showLanguage');
},
localeList() {
const localeOptions = this.designer.setupState.getConfig('localeOptions', [
{value: 'zh-cn', label: '简体中文'},
{value: 'en', label: 'English'},
]);
const localeList = [];
const locale = this.designer.props?.locale?.name || 'zh-cn';
localeOptions.forEach((item) => {
if (item.value === locale) {
localeList.unshift(item);
} else if (localeList.length < 2) {
localeList.push(item);
}
});
if (localeList.length > 2) {
localeList.pop();
}
return localeList;
},
language() {
const language = this.designer.setupState.formOptions.language || {};
const column = {};
Object.keys(language).forEach(lang => {
Object.keys(language[lang]).forEach(key => {
if (!column[key]) {
column[key] = {
key: key,
}
}
column[key][lang] = language[lang][key];
})
});
return Object.values(column);
}
},
methods: {
openConfig() {
this.designer.setupState.activeModule = 'language';
},
clickLang(key) {
this.onInput(`{{$t.${key}}}`);
this.$refs.pop.hide();
},
onInput(val) {
this.$emit('update:modelValue', val);
this.$emit('change', val);
}
},
mounted() {
}
});
</script>
<style>
._fd-language-list {
max-height: 320px;
padding-top: 70px;
overflow: auto;
}
._fd-language-input .el-input-group__append {
width: 25px;
padding: 0;
margin: 0;
color: var(--fc-text-color-3);
cursor: pointer;
}
._fd-language-input.is-variable input {
color: var(--fc-style-color-1);
}
._fd-language-header, ._fd-language-item {
display: flex;
border-bottom: 1px solid var(--fc-line-color-3);
padding: 0 12px;
}
._fd-language-header {
font-weight: 500;
padding-top: 10px;
overflow: auto;
color: var(--fc-text-color-1);
position: absolute;
top: 0;
left: 0;
right: 0;
background-color: var(--fc-bg-color-1);
flex-direction: column;
}
._fd-language-name > div, ._fd-language-item > div {
flex: 1;
font-size: 12px;
padding: 5px;
min-width: 70px;
}
._fd-language-title {
margin: 6px 0;
}
._fd-language-title .fc-icon {
color: var(--fc-style-color-1);
cursor: pointer;
font-size: 14px;
}
._fd-language-name {
display: flex;
}
._fd-language-name > div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--fc-text-color-3);
}
._fd-language-item {
cursor: pointer;
}
._fd-language-item:hover {
color: var(--fc-style-color-1);
background-color: var(--fc-style-bg-color-1);
}
._fd-language-popover {
padding: 0 !important;
}
</style>

View File

@ -0,0 +1,141 @@
<template>
<div class="_fc-city-m">
<van-field ref="el" :placeholder="placeholder" readonly :disabled="disabled"
@click="open"
:model-value="inputValue" :border="false" isLink>
<template #right-icon v-if="clearable && inputValue">
<i class="van-badge__wrapper van-icon van-icon-clear van-field__clear"
@click="clear"></i>
</template>
</van-field>
<van-popup :show="show" @update:show="(v) => show = v" round position="bottom">
<van-cascader
:modelValue="(modelValue && modelValue[modelValue.length-1]) || ''"
:options="province"
@close="show = false"
@finish="confirm"
/>
</van-popup>
</div>
</template>
<script>
import {defineComponent, markRaw} from 'vue';
export default defineComponent({
name: 'FcCity',
props: {
modelValue: Array,
clearable: Boolean,
placeholder: String,
disabled: Boolean,
filter: Function,
level: {
type: Number,
default: 3
},
api: String,
},
emits: ['update:modelValue', 'change'],
data() {
return {
inputValue: '',
show: false,
province: [],
}
},
watch: {
modelValue: {
handler(val) {
this.inputValue = (val || []).join(' / ');
},
deep: true,
immediate: true,
}
},
computed: {
city() {
if (this.value.p) {
for (let i = 0; i < this.province.length; i++) {
if (this.province[i].n === this.value.p) {
return this.province[i].d;
}
}
}
return [];
},
area() {
if (this.value.c) {
for (let i = 0; i < this.city.length; i++) {
if (this.city[i].n === this.value.c) {
return this.city[i]?.d || [];
}
}
}
return [];
}
},
methods: {
open() {
if (this.disabled) {
return;
}
this.show = true;
},
confirm({selectedOptions}) {
this.inputValue = selectedOptions.map((option) => option.text).join(' / ');
this.show = false;
const value = selectedOptions.map((option) => option.value);
this.$emit('update:modelValue', value);
this.$emit('change', value);
},
clear(e) {
e.stopPropagation();
this.inputValue = '';
this.$emit('update:modelValue', []);
this.$emit('change', []);
},
loadData(uri) {
return fetch(uri).then((res) => {
return res.json();
}).then((res) => {
this.province = markRaw(this.tidyOptions(this.filter ? this.filter(res) || [] : res, 0));
});
},
tidyOptions(options, level) {
return options.map(opt => {
const item = {
text: opt.text || opt.n,
value: opt.value || opt.text || opt.n
};
if ((opt.children || opt.d) && level + 1 < this.level) {
item.children = this.tidyOptions(opt.children || opt.d, level + 1);
}
return item;
})
},
},
created() {
if (this.api) {
this.loadData(this.api);
} else {
this.loadData('https://unpkg.com/@province-city-china/level/level.min.json').catch(() => {
this.loadData('https://cdn.jsdelivr.net/npm/@province-city-china/level/level.min.json').catch(() => {
this.loadData('https://npm.onmicrosoft.cn/@province-city-china/level/level.min.json');
});
})
}
}
});
</script>
<style>
._fc-city-m {
width: 100%;
}
._fc-city-m .van-cell {
padding: 0;
}
</style>

View File

@ -0,0 +1,152 @@
<template>
<div class="_fc-m-signature">
<template v-if="modelValue">
<div class="_fc-m-signature-preview">
<i class="fc-icon icon-delete2" @click="remove"></i>
<img :src="modelValue" alt="signature">
</div>
</template>
<template v-else>
<div class="_fc-m-signature-btn" @click="visible = true">
<i class="fc-icon icon-edit2"></i> {{ formCreateInject.t('signaturePadTip') || '点击添加手写签名' }}
</div>
</template>
<van-dialog v-model:show="visible" class="_fc-m-signature-dialog"
@confirm="submit" @cancel="clear" :confirm-button-text="formCreateInject.t('ok') || '确定'"
:cancel-button-text="formCreateInject.t('reset') || '重置'"
:confirm-button-disabled="isEmpty">
<template #title>
{{ formCreateInject.t('signaturePadTitle') || '请在虚线框内书写' }}
<i class="fc-icon icon-add2" @click="visible=false"></i>
</template>
<canvas class="_fc-m-signature-pad" ref="pad" width="320px" height="145px"></canvas>
</van-dialog>
</div>
</template>
<script>
import {defineComponent, markRaw} from 'vue';
import SignaturePad from 'signature_pad';
export default defineComponent({
name: 'SignaturePad',
emits: ['update:modelValue', 'change', 'remove'],
data() {
return {
visible: false,
isEmpty: true,
signaturePad: null,
};
},
props: {
modelValue: String,
penColor: String,
formCreateInject: Object,
},
watch: {
visible(val) {
if (val) {
this.isEmpty = true;
this.$nextTick(() => {
this.signaturePad = markRaw(new SignaturePad(this.$refs.pad, {
penColor: this.penColor,
}));
this.signaturePad.addEventListener('endStroke', () => {
this.isEmpty = this.signaturePad.isEmpty();
});
});
} else {
this.signaturePad.off();
this.signaturePad = null;
}
}
},
methods: {
clear() {
this.signaturePad.clear();
this.isEmpty = true;
},
submit() {
const res = this.signaturePad.toDataURL();
this.updateValue(res);
this.visible = false;
},
updateValue(val) {
this.$emit('update:modelValue', val);
this.$emit('change', val);
},
remove() {
this.updateValue('');
this.$emit('remove');
},
},
});
</script>
<style>
._fc-m-signature {
width: 100%;
}
._fc-m-signature-btn, ._fc-m-signature-preview {
width: 100%;
min-width: 160px;
height: 88px;
line-height: 88px;
font-size: 14px;
color: rgb(201, 204, 216);
border-radius: 4px;
border: 1px dashed rgb(212, 215, 224);
text-align: center;
background: rgb(255, 255, 255);
position: relative;
box-sizing: border-box;
}
._fc-m-signature-btn {
cursor: pointer;
}
._fc-m-signature-preview > img {
display: inline-block;
height: 88px;
}
._fc-m-signature-preview .icon-delete2 {
position: absolute;
top: 9px;
right: 9px;
display: inline-block;
line-height: 14px;
font-size: 14px;
cursor: pointer;
}
._fc-m-signature-btn i {
font-size: 14px;
}
._fc-m-signature-dialog .van-dialog__header {
padding: 15px 0;
position: relative;
}
._fc-m-signature-dialog .icon-add2 {
position: absolute;
right: 18px;
display: inline-block;
color: var(--fc-text-color-3);
transform: rotate(45deg);
}
._fc-m-signature-pad {
width: 100%;
box-sizing: border-box;
border-radius: 4px;
border: 1px dashed #D4D7E0;
background-image: linear-gradient(#FFFFFF 14px, transparent 0), linear-gradient(90deg, #FFFFFF 14px, #D4D7E0 0);
background-size: 15px 15px;
}
</style>

View File

@ -0,0 +1,135 @@
<template>
<van-popup class="_fc-popup" closeable v-bind="$attrs" v-model:show="visible">
<div class="_fc-popup-title">{{ title }}</div>
<div class="_fc-popup-content">
<component :is="Form" :option="formOptions" :rule="formRule" :extendOption="true"
v-model:api="fapi"
:model-value="value"
:subForm="false"
@change="formChange"
@emit-event="$emit"></component>
</div>
<div class="_fc-popup-footer">
<template v-if="footer !== false">
<van-button block size="small" type="primary" class="fc-clock" @click="handleConfirm">{{formCreateInject.t('ok') || '确定'}}</van-button>
<van-button block size="small" class="fc-clock" style="margin-top: 10px" @click="close">{{formCreateInject.t('close') || '关闭'}}</van-button>
</template>
</div>
</van-popup>
</template>
<script>
import {defineComponent, markRaw, onUnmounted, reactive} from 'vue';
import {deepCopy} from '@form-create/utils/lib/deepextend';
export default defineComponent({
name: 'FcPopup',
emits: ['confirm', 'submit', 'validateFail', 'update:modelValue'],
props: {
formData: Object,
options: {
type: Object,
default: () => reactive(({
submitBtn: false,
resetBtn: false,
}))
},
rule: Array,
autoClose: {
type: Boolean,
default: true
},
footer: {
type: Boolean,
default: true
},
preview: Boolean,
modelValue: Object,
formCreateInject: Object,
title: String
},
computed: {
formOptions() {
const opt = {...this.options};
if(this.preview) {
opt.preview = this.preview;
}
return opt;
}
},
data() {
return {
visible: false,
fapi: {},
value: {},
formRule: [],
Form: markRaw(this.formCreateInject.form.$form()),
}
},
methods: {
formChange() {
this.$emit('update:modelValue', this.fapi.formData());
},
open(formData) {
this.$nextTick(() => {
this.visible = true;
this.value = deepCopy(formData || this.modelValue || this.formData || {});
this.formRule = deepCopy(this.rule || []);
});
},
close() {
this.visible = false;
},
handleConfirm() {
this.$emit('confirm', this.fapi);
this.fapi.submit().then(formData => {
this.$emit('submit', formData, this.fapi, this.close);
if (this.autoClose) {
this.close();
}
}).catch(e => {
this.$emit('validateFail', e, this.fapi);
});
},
},
mounted() {
this.formCreateInject.api.top.bus.$on('fc.closeDialog', this.close);
onUnmounted(() => {
this.formCreateInject.api.top.bus.$off('fc.closeDialog', this.close);
})
}
});
</script>
<style>
._fc-popup.van-popup {
display: flex;
height: 100%;
padding-top: 50px;
padding-bottom: 110px;
}
._fc-popup-title {
position: absolute;
top: 16px;
left: 0;
color: #333;
width: 100%;
text-align: center;
font-size: 16px;
}
._fc-popup-content {
display: flex;
flex: 1;
overflow: auto;
}
._fc-popup-footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 10px;
}
</style>

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