<template> <div class="form-designer"> <el-container> <!-- 左侧字段 --> <el-aside :width="leftWidth"> <div class="fields-list"> <div v-for="(field, index) in fields" :key="index"> <template v-if="field.list.find((f) => includeFields.includes(f.type))" > <div class="field-title">{{ field.title }}</div> <draggable tag="ul" :list="field.list" :group="{ name: 'form', pull: 'clone', put: false }" ghost-class="ghost" :sort="false" > <template v-for="(item, index) in field.list"> <li class="field-label" v-if="includeFields.includes(item.type)" :key="index" > <a @click="handleFieldClick(item)"> <i class="icon iconfont" :class="item.icon"></i> <span>{{ item.title || item.label }}</span> </a> </li> </template> </draggable> </template> </div> <div class="field-title">{{ tableName }}字段</div> <template v-if="customFields && customFields.length > 0"> <draggable tag="ul" :list="customFields" :group="{ name: 'form' }" ghost-class="ghost" :sort="false" v-loading="customFieldsLoading" > <template v-for="(item, index) in customFields"> <el-tooltip v-if="item.tips" effect="dark" :content="item.tips" :key="index" > <li class="field-label" :key="index"> <a style="padding: 0 5px" @click="handleFieldClick(item)"> <i :class="item.icon"></i> <span style="margin-left: 5px">{{ item.title || item.label }}</span> </a> </li> </el-tooltip> <li v-else class="field-label" :key="index"> <a style="padding: 0 5px" @click="handleFieldClick(item)"> <i :class="item.icon"></i> <span style="margin-left: 5px">{{ item.title || item.label }}</span> </a> </li> </template> </draggable> </template> <div v-else style="padding-left: 14px; font-size: 12px; color: #c0c4cc" > 暂无数据 </div> </div> </el-aside> <!-- 中间主布局 --> <el-container class="widget-container" direction="vertical"> <el-header class="widget-container-header"> <div> <template v-if="undoRedo"> <el-button type="text" size="medium" icon="el-icon-refresh-left" :disabled="historySteps.index == 0" @click="widgetForm = handleUndo()" >撤销</el-button > <el-button type="text" size="medium" icon="el-icon-refresh-right" :disabled="historySteps.index == historySteps.steps.length - 1" @click="widgetForm = handleRedo()" >重做</el-button > </template> </div> <div style="display: flex; align-items: center"> <iframe src="https://ghbtns.com/github-btn.html?user=sscfaith&repo=avue-form-design&type=star&count=true" frameborder="0" scrolling="0" width="100" height="20" title="GitHub" style="margin-left: 10px" v-if="showGithubStar" ></iframe> <slot name="toolbar-left"></slot> <el-button v-if="toolbar.includes('avue-doc')" type="text" size="medium" icon="el-icon-document" @click="handleAvueDoc" >Avue文档</el-button > <el-button v-if="toolbar.includes('import')" type="text" size="medium" icon="el-icon-upload2" @click="importJsonVisible = true" >导入JSON</el-button > <el-button v-if="toolbar.includes('generate')" type="text" size="medium" icon="el-icon-download" @click="handleGenerateJson" >生成JSON</el-button > <el-button v-if="toolbar.includes('save')" type="text" size="medium" icon="el-icon-download" @click="handleSave" >保存</el-button > <el-button v-if="toolbar.includes('preview')" type="text" size="medium" icon="el-icon-view" @click="handlePreview" >预览</el-button > <el-button v-if="toolbar.includes('clear')" class="danger" type="text" size="medium" icon="el-icon-delete" @click="handleClear" >清空</el-button > <slot name="toolbar"></slot> </div> </el-header> <el-main :style="{ background: widgetForm.column.length == 0 ? `url(${widgetEmpty}) no-repeat 50%` : '', }" > <widget-form ref="widgetForm" :data="widgetForm" :select.sync="widgetFormSelect" @change="handleHistoryChange(widgetForm)" ></widget-form> </el-main> </el-container> <!-- 右侧配置 --> <el-aside class="widget-config-container" :width="rightWidth"> <el-tabs v-model="configTab" stretch> <el-tab-pane label="字段属性" name="widget" style="padding: 0 10px"> <widget-config :data="widgetFormSelect" @change="handleHistoryChange(widgetForm)" prop-not-edit ></widget-config> </el-tab-pane> <el-tab-pane label="表单属性" name="form" lazy style="padding: 0 10px" > <form-config :data="widgetForm" @change="editFieldBatch" ></form-config> </el-tab-pane> </el-tabs> </el-aside> <!-- 弹窗 --> <!-- 导入JSON --> <el-drawer title="导入JSON" :visible.sync="importJsonVisible" size="50%" append-to-body destroy-on-close > <!-- <monaco-editor v-model="importJson" keyIndex="import" height="82%" ></monaco-editor> --> <div class="drawer-foot"> <el-button size="medium" type="primary" @click="handleImportJsonSubmit" >确定</el-button > <el-button size="medium" type="danger" @click="importJsonVisible = false" >取消</el-button > </div> </el-drawer> <!-- 生成JSON --> <el-drawer title="生成JSON" :visible.sync="generateJsonVisible" size="50%" append-to-body destroy-on-close > <!-- <monaco-editor v-model="widgetFormPreview" keyIndex="generate" height="82%" :read-only="true" ></monaco-editor> --> <div class="drawer-foot"> <el-button size="medium" type="primary" @click="handleGenerate" >生成</el-button > <el-popover placement="top" trigger="hover" width="350px"> <el-form v-model="configOption" style="padding: 0 20px" label-suffix=":" label-width="180px" label-position="left" > <el-form-item label="类型"> <el-popover placement="top-start" trigger="hover" content="复制json对象" style="margin-right: 15px" > <el-radio slot="reference" v-model="configOption.generateType" label="json" >json</el-radio > </el-popover> <el-popover placement="top-start" trigger="hover" content="复制string字符串,可直接用于后端保存无需再次处理。" > <el-radio slot="reference" v-model="configOption.generateType" label="string" >string</el-radio > </el-popover> </el-form-item> <el-form-item label="缩进长度-空格数量"> <el-slider v-model="configOption.space" show-stops :marks="{ 1: '1', 2: '2', 3: '3', 4: '4' }" :min="1" :max="4" :step="1" ></el-slider> </el-form-item> <el-form-item label="引号类型"> <el-switch v-model="configOption.quoteType" active-value="single" inactive-value="double" active-text="单引号" inactive-text="双引号" ></el-switch> </el-form-item> <el-form-item label="移除key的引号"> <el-switch v-model="configOption.dropQuotesOnKeys"></el-switch> </el-form-item> <el-form-item label="移除数字字符串的引号"> <el-switch v-model="configOption.dropQuotesOnNumbers" ></el-switch> </el-form-item> </el-form> <el-button size="medium" type="primary" @click="handleCopy" slot="reference" style="margin-left: 10px" >复制</el-button > </el-popover> </div> </el-drawer> <!-- 预览 --> <el-drawer title="预览" :visible.sync="previewVisible" size="60%" append-to-body :before-close="handleBeforeClose" > <div class="preview-form"> <custom-form ref="form" v-if="previewVisible" :options="widgetFormPreview" @handleConfirm="handlePreviewSubmit" ></custom-form> </div> <div class="drawer-foot"> <el-button size="medium" type="primary" @click="handleBeforeClose" >确定</el-button > <el-button size="medium" type="danger" @click="handleBeforeClose" >取消</el-button > </div> </el-drawer> </el-container> </div> </template> <script> import fields from "./fieldsConfig.js" import beautifier from "./utils/json-beautifier" // import MonacoEditor from "./utils/monaco-editor" import widgetEmpty from "./assets/widget-empty.png" import history from "./mixins/history" import Draggable from "vuedraggable" import WidgetForm from "./WidgetForm" import FormConfig from "./FormConfig" import WidgetConfig from "./WidgetConfig" import CustomForm from "@/components/FormComponents/CustomForm/index" export default { name: "FormDesign", components: { Draggable, // MonacoEditor, WidgetForm, FormConfig, WidgetConfig, CustomForm, }, mixins: [history], props: { options: { type: [Object, String], default: () => { return { column: [], } }, }, tableName: { type: String, }, storage: { type: Boolean, default: false, }, asideLeftWidth: { type: [String, Number], default: "270px", }, asideRightWidth: { type: [String, Number], default: "380px", }, showGithubStar: { type: Boolean, default: true, }, toolbar: { type: Array, default: () => { return ["import", "generate", "save", "preview", "clear"] }, }, undoRedo: { type: Boolean, default: true, }, includeFields: { type: Array, default: () => { const arr = [] fields.forEach((f) => { f.list.forEach((c) => { arr.push(c.type) }) }) return arr }, }, customFields: { type: Array, }, }, watch: { options: { handler(val) { let options = val if (typeof options == "string") { try { options = eval("(" + options + ")") } catch (e) { console.error("非法配置") options = { column: [] } } } this.transAvueOptionsToFormDesigner(options).then((res) => { this.widgetForm = { ...this.widgetForm, ...res } this.handleHistoryChange(this.widgetForm) }) }, deep: true, }, }, computed: { leftWidth() { if (typeof this.asideLeftWidth == "string") { return this.asideLeftWidth } else { return `${this.asideLeftWidth}px` } }, rightWidth() { if (typeof this.asideRightWidth == "string") { return this.asideRightWidth } else { return `${this.asideRightWidth}px` } }, }, data() { return { widgetEmpty, fields, customFieldsLoading: false, widgetForm: { column: [], labelPosition: "right", labelSuffix: ":", labelWidth: 180, gutter: 0, menuBtn: true, submitBtn: true, submitText: "提交", emptyBtn: true, emptyText: "清空", nextTabBtn: true, nextTabText: "下一页", menuPosition: "center", }, widgetFormPreview: {}, configTab: "widget", widgetFormSelect: {}, previewVisible: false, generateJsonVisible: false, importJsonVisible: false, importJson: {}, widgetModels: {}, configOption: { generateType: "json", space: 2, quoteType: "single", dropQuotesOnKeys: true, }, history: { index: 0, // 当前下标 maxStep: 20, // 最大记录步数 steps: [], // 历史步数 }, } }, mounted() { this.handleLoadStorage() this.handleLoadCss() window.$myVm = this // 挂载当前实例,用于预览 }, methods: { // 组件初始化时加载本地存储中的options(需开启storage),若不存在则读取用户配置的options async handleLoadStorage() { let options = this.options if (typeof options == "string") { try { options = eval("(" + options + ")") } catch (e) { console.error("非法配置") options = { column: [] } } } if (!options.column) options.column = [] this.widgetForm = this.initHistory({ index: 0, maxStep: 20, steps: [ await this.transAvueOptionsToFormDesigner({ ...this.widgetForm, ...options, }), ], storage: this.storage, }) if (this.undoRedo) { window.addEventListener( "keydown", (evt) => { // 监听 cmd + z / ctrl + z 撤销 if ( (evt.metaKey && !evt.shiftKey && evt.keyCode == 90) || (evt.ctrlKey && !evt.shiftKey && evt.keyCode == 90) ) { this.widgetForm = this.handleUndo() } // 监听 cmd + shift + z / ctrl + shift + z / ctrl + y 重做 if ( (evt.metaKey && evt.shiftKey && evt.keyCode == 90) || (evt.ctrlKey && evt.shiftKey && evt.keyCode == 90) || (evt.ctrlKey && evt.keyCode == 89) ) { this.widgetForm = this.handleRedo() } }, false ) } }, // 加载icon handleLoadCss() { const head = document.getElementsByTagName("head")[0] const script = document.createElement("link") script.rel = "stylesheet" script.type = "text/css" script.href = "https://at.alicdn.com/t/font_1254447_zc9iezc230c.css" head.appendChild(script) // this.loadScript('css', 'https://at.alicdn.com/t/font_1254447_zc9iezc230c.css') }, // Avue文档链接 handleAvueDoc() { window.open("https://avuejs.com/doc/form/form-doc", "_blank") }, // 左侧字段点击 handleFieldClick(item) { const activeIndex = this.widgetForm.column.findIndex( (c) => c.prop == this.widgetFormSelect.prop ) + 1 let newIndex = 0 if (activeIndex == -1) { this.widgetForm.column.push(item) newIndex = this.widgetForm.column.length - 1 } else { this.widgetForm.column.splice(activeIndex, 0, item) newIndex = activeIndex } this.$refs.widgetForm.handleWidgetAdd({ newIndex }) }, // 预览 - 弹窗 handlePreview() { if (!this.widgetForm.column || this.widgetForm.column.length == 0) this.$message.error("没有需要展示的内容") else { this.transformToAvueOptions(this.widgetForm).then((data) => { this.widgetFormPreview = data this.previewVisible = true }) } }, // 导入JSON - 弹窗 - 确定 handleImportJsonSubmit() { try { this.transAvueOptionsToFormDesigner(this.importJson).then((res) => { this.widgetForm = res this.importJsonVisible = false this.handleHistoryChange(this.widgetForm) }) } catch (e) { this.$message.error(e.message) } }, // 生成JSON - 弹窗 handleGenerateJson() { this.transformToAvueOptions(this.widgetForm).then((data) => { this.widgetFormPreview = data this.generateJsonVisible = true }) }, // 生成JSON - 弹窗 - 确定 handleGenerate() { this.transformToAvueOptions(this.widgetForm).then((data) => { if ( this.configOption.generateType && this.configOption.generateType == "string" ) this.$emit( "submit", beautifier(data, { minify: true, ...this.configOption, }) ) else this.$emit("submit", data) }) }, // 生成JSON - 弹窗 - 拷贝 handleCopy() { this.transformToAvueOptions(this.widgetForm).then((data) => { this.$Clipboard({ text: beautifier(data, { minify: true, ...this.configOption, }), }) .then(() => { this.$message.success("复制成功") }) .catch(() => { this.$message.error("复制失败") }) }) }, // 预览 - 弹窗 - 确定 handlePreviewSubmit(form, done) { if (done) { this.$alert(form) .then(() => { done() }) .catch(() => { done() }) } else { this.$refs.form.validate((valid, done) => { if (valid) this.$alert(form) .then(() => { done() }) .catch(() => { done() }) }) } }, // 预览 - 弹窗 - 关闭前 handleBeforeClose() { this.$refs.form.resetForm() this.widgetModels = {} this.previewVisible = false }, handleSave() { this.$emit("handleSave") }, // 清空 handleClear() { if ( this.widgetForm && this.widgetForm.column && this.widgetForm.column.length > 0 ) { this.$confirm("确定要清空吗?", "警告", { type: "warning", }) .then(() => { this.$set(this.widgetForm, "column", []) this.$set(this, "widgetModels", {}) this.$set(this, "widgetFormSelect", {}) this.handleHistoryChange(this.widgetForm) }) .catch(() => {}) } else this.$message.error("没有需要清空的内容") }, // 表单设计器配置项 转化为 Avue配置项 transformToAvueOptions(obj, delGroupCol = true) { return new Promise((resolve, reject) => { try { const data = this.deepClone(obj) for (let i = 0; i < data.column.length; i++) { const col = data.column[i] if ( col.type == "dynamic" && col.children && col.children.column && col.children.column.length > 0 ) { const c = col.children.column c.forEach((item) => { delete item.subfield }) this.transformToAvueOptions(col.children).then((res) => { col.children = res }) } else if (col.type == "group") { if (!data.group) data.group = [] const group = { label: col.label, icon: col.icon, prop: col.prop, arrow: col.arrow, collapse: col.collapse, display: col.display, labelWidth: col.labelWidth, } this.transformToAvueOptions(col.children, false).then((res) => { group.column = res.column data.group.push(group) }) // 第一轮删除group项 if (delGroupCol) { data.column.splice(i, 1) i-- } } else if ( ["checkbox", "radio", "tree", "cascader", "select"].includes( col.type ) ) { if (col.dicOption == "static") { delete col.dicUrl delete col.dicMethod delete col.dicQuery delete col.dicQueryConfig // delete col.dicData //删除字典数据保存 } else if (col.dicOption == "remote") { delete col.dicData if (col.dicQueryConfig && col.dicQueryConfig.length > 0) { const query = {} col.dicQueryConfig.forEach((q) => { if (q.key && q.value) query[q.key] = q.value }) col.dicQuery = query delete col.dicQueryConfig } else delete col.dicQueryConfig } delete col.dicOption } else if (["upload"].includes(col.type)) { if (col.headersConfig && col.headersConfig.length > 0) { const headers = {} col.headersConfig.forEach((h) => { if (h.key && h.value) headers[h.key] = h.value }) col.headers = headers } else delete col.headers delete col.headersConfig if (col.dataConfig && col.dataConfig.length > 0) { const data = {} col.dataConfig.forEach((h) => { if (h.key && h.value) data[h.key] = h.value }) col.data = data } else delete col.data delete col.dataConfig } if (col.change) col.change = new Function("return " + col.change)() else delete col.change if (col.click) col.click = new Function("return " + col.click)() else delete col.click if (col.focus) col.focus = new Function("return " + col.focus)() else delete col.focus if (col.blur) col.blur = new Function("return " + col.blur)() else delete col.blur } resolve(data) } catch (e) { reject(e) } }) }, // Avue配置项 转化为 表单设计器配置项 transAvueOptionsToFormDesigner(obj, options) { if (typeof obj == "string") obj = eval("(" + obj + ")") const data = this.deepClone(obj) return new Promise((resolve, reject) => { try { if (data.column && data.column.length > 0) { data.column.forEach((col) => { if ( col.type == "dynamic" && col.children && col.children.column && col.children.column.length > 0 ) { const c = col.children.column c.forEach((item) => { item.subfield = true }) this.transAvueOptionsToFormDesigner(col.children).then( (res) => { col.children = res } ) } else if ( ["checkbox", "radio", "tree", "cascader", "select"].includes( col.type ) ) { if ( !col.dicData && col.dicQuery && typeof col.dicQuery == "object" ) { const arr = [] for (let key in col.dicQuery) { arr.push({ key, value: col.dicQuery[key], $cellEdit: true, }) } col.dicQueryConfig = arr } if (col.dicUrl) col.dicOption = "remote" else { col.dicOption = "static" // 存在dicType 没有字典的动态获取字典 if (!col.dicData && col.dicType) { this.setDicData(col) } } if (!col.dicData) col.dicData = [] } else if (["upload"].includes(col.type)) { if (col.headers && typeof col.headers == "object") { const arr = [] for (let key in col.headers) { arr.push({ key, value: col.headers[key], $cellEdit: true, }) } col.headersConfig = arr } else col.headersConfig = [] if (col.data && typeof col.data == "object") { const arr = [] for (let key in col.data) { arr.push({ key, value: col.data[key], $cellEdit: true, }) } col.dataConfig = arr } else col.dataConfig = [] } }) } if (data.group && data.group.length > 0) { for (let i = 0; i < data.group.length; i++) { if (!data.column) data.column = [] const col = data.group[i] const group = { type: "group", label: col.label, labelWidth: col.labelWidth, icon: col.icon, prop: col.prop, arrow: col.arrow, collapse: col.collapse, display: col.display, children: {}, } this.transAvueOptionsToFormDesigner(col).then((res) => { group.children.column = res.column data.column.push(group) }) } delete data.group } resolve(data) } catch (e) { reject(e) } }) }, // 添加字典数据 setDicData(c) { const { dicType, type } = c if (dicType) { let dicData = this.deepClone(this.dictMap[dicType]) || [] this.$set(c, "dicData", dicData) } }, // 批量修改字段属性 editFieldBatch(field, val) { function fn(obj) { const { column } = obj if (column && column.length > 0) { column.forEach((col) => { if ( (col.type == "dynamic" || col.type == "group") && col.children ) { fn(col.children) } else { col[field] = val } }) } } fn(this.widgetForm) this.handleHistoryChange(this.widgetForm) }, async getData(type = "json") { if (type == "string") return beautifier(await this.transformToAvueOptions(this.widgetForm), { minify: true, }) else return await this.transformToAvueOptions(this.widgetForm) }, }, } </script> <style lang="scss"> @import "./styles/index.scss"; </style>