Files
2022-12-20 17:27:12 +08:00

1041 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="container">
<div class="left-board">
<div class="logo-wrapper">
<div class="logo"><img :src="logo" alt="logo" /> Form Generator</div>
</div>
<el-scrollbar class="left-scrollbar">
<div class="components-list">
<div v-for="(item, listIndex) in leftComponents" :key="listIndex">
<div class="components-title">
<svg-icon icon-class="component" />
{{ item.title }}
</div>
<!-- {{ item.list[0].__config__.label }} -->
<draggable
:list="item.list"
class="components-draggable"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }"
:clone="cloneComponent"
item-key="id"
:sort="false"
@end="onEnd"
>
<template #item="{ element }">
<div class="components-item" @click="addComponent(element)">
<div class="components-body">
<svg-icon :icon-class="element.__config__.tagIcon" />
{{ element.__config__.label }}
</div>
</div>
</template>
</draggable>
<!-- <draggable
:item-key="`${listIndex}`"
class="components-draggable"
:list="item.list"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }"
:clone="cloneComponent"
draggable=".components-item"
:sort="false"
@end="onEnd"
>
</draggable> -->
</div>
</div>
</el-scrollbar>
</div>
<div class="center-board">
<div class="action-bar">
<el-button icon="el-icon-plus" link @click="handleForm">
保存
</el-button>
<el-button icon="el-icon-video-play" link @click="run">
运行
</el-button>
<el-button icon="el-icon-view" link @click="showJson">
查看json
</el-button>
<el-button icon="el-icon-download" link @click="download">
导出vue文件
</el-button>
<el-button
class="copy-btn-main"
icon="el-icon-document-copy"
link
@click="copy"
>
复制代码
</el-button>
<el-button class="delete-btn" icon="el-icon-delete" link @click="empty">
清空
</el-button>
</div>
<el-scrollbar class="center-scrollbar">
<el-row class="center-board-row" :gutter="formConf.gutter">
<el-form
:style="{
width: '100%',
}"
:size="formConf.size"
:label-position="formConf.labelPosition"
:disabled="formConf.disabled"
:label-width="formConf.labelWidth + 'px'"
>
<draggable
class="drawing-board"
:list="drawingList"
:animation="340"
group="componentsGroup"
item-key="id"
>
<template #item="{ element, index }">
<draggable-item
:drawing-list="drawingList"
:current-item="element"
:index="index"
:active-id="activeId"
:form-conf="formConf"
@activeItem="activeFormItem"
@copyItem="drawingItemCopy"
@deleteItem="drawingItemDelete"
/>
</template>
<!-- v-for="(item, index) in drawingList"
:key="item.renderKey" -->
</draggable>
<div v-show="!drawingList.length" class="empty-info">
从左侧拖入或点选组件进行表单设计
</div>
</el-form>
</el-row>
</el-scrollbar>
</div>
<right-panel
:active-data="activeData"
:form-conf="formConf"
:show-field="!!drawingList.length"
@tag-change="tagChange"
@fetch-data="fetchData"
/>
<form-drawer
v-model="drawerVisible"
:form-data="formData"
size="100%"
:generate-conf="generateConf"
/>
<json-drawer
size="60%"
v-model="jsonDrawerVisible"
:json-str="JSON.stringify(formData)"
@refresh="refreshJson"
/>
<code-type-dialog
v-model="dialogVisible"
title="选择生成类型"
:show-file-name="showFileName"
@confirm="generate"
/>
<input id="copyNode" type="hidden" />
<!-- 表单配置详情 -->
<el-dialog
:title="formTitle"
v-model="formOpen"
width="500px"
append-to-body
>
<el-form ref="formRef" :model="formObj" :rules="rules" label-width="80px">
<el-form-item label="表单名称" prop="formName">
<el-input v-model="formObj.formName" placeholder="请输入表单名称" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formObj.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { addForm, getForm, updateForm } from "@/api/flowable/form";
import { debounce } from "throttle-debounce";
import ClipboardJS from "clipboard";
import draggable from "vuedraggable";
import CodeTypeDialog from "./CodeTypeDialog.vue";
import logo from "@/assets/logo/logo.png";
import { beautifierConf, titleCase, deepClone } from "@/utils/index";
import drawingDefault from "@/utils/generator/drawingDefault";
import draggableItem from "./DraggableItem";
import RightPanel from "./RightPanel.vue";
import loadBeautifier from "@/utils/loadBeautifier";
import JsonDrawer from "./JsonDrawer";
import FormDrawer from "./FormDrawer.vue";
import { ElMessage, ElMessageBox, ElNotification } from "element-plus";
import $download from "@/plugins/download.js";
import {
inputComponents,
selectComponents,
layoutComponents,
formConf as formConfig,
} from "@/utils/generator/config";
import {
makeUpHtml,
vueTemplate,
vueScript,
cssStyle,
} from "@/utils/generator/html";
import { makeUpJs } from "@/utils/generator/js";
import { makeUpCss } from "@/utils/generator/css";
import tab from "@/plugins/tab";
import {
getDrawingList,
saveDrawingList,
getIdGlobal,
saveIdGlobal,
getFormConf,
} from "@/utils/db";
import { nextTick, onMounted, reactive, ref, toRefs, watch } from "vue";
import axios from "axios";
import { useRoute, useRouter } from "vue-router";
let tempActiveData;
let oldActiveId;
const route = useRoute();
const router = useRouter();
const drawingListInDB = getDrawingList();
const formConfInDB = getFormConf();
const idGlobal = ref(getIdGlobal());
const leftComponents = ref([
{
title: "输入型组件",
list: inputComponents,
},
{
title: "选择型组件",
list: selectComponents,
},
{
title: "布局型组件",
list: layoutComponents,
},
]);
const drawerVisible = ref(false);
const dialogVisible = ref(false);
const jsonDrawerVisible = ref(false);
const generateConf = ref(null);
const showFileName = ref(false);
const drawingList = ref([]);
const operationType = ref(null);
const activeData = ref(drawingDefault[0]);
const activeId = ref(drawingDefault[0].formId);
const formOpen = ref(false);
const formTitle = ref("");
const formRef = ref();
const data = reactive({
formData: {},
// 表单参数
formObj: {
formId: undefined,
formName: undefined,
content: undefined,
remark: undefined,
},
// 表单校验
rules: {},
formConf: formConfig,
});
const { formData, rules, formObj, formConf } = toRefs(data);
const onEnd = (obj) => {
if (obj.from !== obj.to) {
fetchData(tempActiveData);
activeData.value = tempActiveData;
activeId.value = idGlobal.value;
}
};
const setObjectValueReduce = (obj, strKeys, data) => {
const arr = strKeys.split(".");
arr.reduce((pre, item, i) => {
if (arr.length === i + 1) {
pre[item] = data;
} else if (!(pre[item] instanceof Object)) {
pre[item] = {};
}
return pre[item];
}, obj);
};
const setLoading = (component, val) => {
const { directives } = component;
if (Array.isArray(directives)) {
const t = directives.find((d) => d.name === "loading");
if (t) t.value = val;
}
};
const setRespData = (component, resp) => {
const { dataKey, renderKey, dataConsumer } = component.__config__;
if (!dataKey || !dataConsumer) return;
const respData = dataKey.split(".").reduce((pre, item) => pre[item], resp);
// 将请求回来的数据,赋值到指定属性。
// 以el-tabel为例根据Element文档应该将数据赋值给el-tabel的data属性所以dataConsumer的值应为'data';
// 此时赋值代码可写成 component[dataConsumer] = respData
// 但为支持更深层级的赋值dataConsumer的值为'options.data',使用setObjectValueReduce
setObjectValueReduce(component, dataConsumer, respData);
const i = drawingList.value.findIndex(
(item) => item.__config__.renderKey === renderKey
);
if (i > -1) drawingList.value[i] = component;
};
const fetchData = (component) => {
const { dataType, method, url } = component.__config__;
if (dataType === "dynamic" && method && url) {
setLoading(component, true);
axios({
method,
url,
}).then((resp) => {
setLoading(component, false);
setRespData(component, resp.data);
});
}
};
const activeFormItem = (currentItem) => {
// console.log(currentItem);
activeData.value = currentItem;
activeId.value = currentItem.__config__.formId;
};
const addComponent = (item) => {
const clone = cloneComponent(item);
fetchData(clone);
drawingList.value.push(clone);
activeFormItem(clone);
};
const cloneComponent = (origin) => {
const clone = deepClone(origin);
const config = clone.__config__;
config.span = formConf.value.span; // 生成代码时会根据span做精简判断
createIdAndKey(clone);
clone.placeholder !== undefined && (clone.placeholder += config.label);
tempActiveData = clone;
return tempActiveData;
};
const createIdAndKey = (item) => {
const config = item.__config__;
config.formId = ++idGlobal.value;
config.renderKey = `${config.formId}${+new Date()}`; // 改变renderKey后可以实现强制更新组件
if (config.layout === "colFormItem") {
item.__vModel__ = `field${idGlobal.value}`;
} else if (config.layout === "rowFormItem") {
config.componentName = `row${idGlobal.value}`;
!Array.isArray(config.children) && (config.children = []);
delete config.label; // rowFormItem无需配置label属性
}
if (Array.isArray(config.children)) {
config.children = config.children.map((childItem) =>
createIdAndKey(childItem)
);
}
return item;
};
const showJson = () => {
AssembleFormData();
jsonDrawerVisible.value = true;
};
const download = () => {
dialogVisible.value = true;
showFileName.value = true;
operationType.value = "download";
};
const run = () => {
// TODO 弹窗类型异常
// this.dialogVisible = true
// this.showFileName = false
operationType.value = "run";
let data = {
fileName: undefined,
type: "file",
};
generate(data);
};
const saveIdGlobalDebounce = debounce(340, saveIdGlobal);
const saveDrawingListDebounce = debounce(340, saveDrawingList);
/** 表单基本信息 */
const handleForm = () => {
formData.value = {
...formConf.value,
fields: deepClone(drawingList.value),
};
formObj.value.content = JSON.stringify(formData.value);
formOpen.value = true;
formTitle.value = "添加表单";
};
// 表单重置
const reset = () => {
formObj.value = {
formId: null,
formName: null,
content: null,
remark: null,
};
formRef.value && formRef.value.resetFields();
};
// 取消按钮
const cancel = () => {
formOpen.value = false;
reset();
};
const AssembleFormData = () => {
formData.value = {
...formConf.value,
fields: deepClone(drawingList.value),
};
};
const generate = (data) => {
const func = `exec${titleCase(operationType.value)}`;
generateConf.value = data;
func && eval(`${func}(data)`);
};
const execRun = (data) => {
AssembleFormData();
drawerVisible.value = true;
};
const execDownload = (data) => {
const codeStr = generateCode();
const blob = new Blob([codeStr], { type: "text/plain;charset=utf-8" });
$download.saveAs(blob, data.fileName);
};
const execCopy = (data) => {
document.getElementById("copyNode").click();
};
const empty = () => {
ElMessageBox.confirm("确定要清空所有组件吗?", "提示", {
type: "warning",
}).then(() => {
drawingList.value = [];
idGlobal.value = 100;
});
};
const drawingItemCopy = (item, list) => {
let clone = deepClone(item);
clone = createIdAndKey(clone);
list.push(clone);
activeFormItem(clone);
};
const drawingItemDelete = (index, list) => {
list.splice(index, 1);
nextTick(() => {
const len = drawingList.value.length;
if (len) {
activeFormItem(drawingList.value[len - 1]);
}
});
};
const generateCode = () => {
const { type } = generateConf.value;
AssembleFormData();
const extraScript = vueScript(`
export default {
inheritAttrs: false
}
`);
const script = vueScript(makeUpJs(formData.value, type), true);
const html = vueTemplate(makeUpHtml(formData.value, type));
const css = cssStyle(makeUpCss(formData.value));
return beautifier.html(
html + extraScript + script + css,
beautifierConf.html
);
};
const copy = () => {
dialogVisible.value = true;
showFileName.value = false;
operationType.value = "copy";
};
const tagChange = (newTag) => {
newTag = cloneComponent(newTag);
const config = newTag.__config__;
newTag.__vModel__ = activeData.value.__vModel__;
config.formId = activeId.value;
config.span = activeData.value.__config__.span;
activeData.value.__config__.tag = config.tag;
activeData.value.__config__.tagIcon = config.tagIcon;
activeData.value.__config__.document = config.document;
if (
typeof activeData.value.__config__.defaultValue ===
typeof config.defaultValue
) {
config.defaultValue = activeData.value.__config__.defaultValue;
}
Object.keys(newTag).forEach((key) => {
if (activeData.value[key] !== undefined) {
newTag[key] = activeData.value[key];
}
});
activeData.value = newTag;
updateDrawingList(newTag, drawingList.value);
};
const updateDrawingList = (newTag, list) => {
const index = list.findIndex(
(item) => item.__config__.formId === activeId.value
);
if (index > -1) {
list.splice(index, 1, newTag);
} else {
list.forEach((item) => {
if (Array.isArray(item.__config__.children))
updateDrawingList(newTag, item.__config__.children);
});
}
};
const refreshJson = (data) => {
drawingList.value = deepClone(data.fields);
delete data.fields;
formConf.value = data;
};
/** 保存表单信息 */
const submitForm = () => {
formRef.value.validate((valid) => {
// console.log(formObj.value);
// return;
if (valid) {
if (formObj.value.formId != null) {
updateForm(formObj.value).then((response) => {
ElMessage.success("修改成功");
});
} else {
addForm(formObj.value).then((response) => {
ElMessage.success("新增成功");
});
}
drawingList.value = [];
idGlobal.value = 100;
open.value = false;
// 关闭当前标签页并返回上个页面
tab.closeOpenPage();
router.back();
}
});
};
onMounted(() => {
// const that = this;
if (Array.isArray(drawingListInDB) && drawingListInDB.length > 0) {
drawingList.value = drawingListInDB;
} else {
drawingList.value = drawingDefault;
}
activeFormItem(drawingList.value[0]);
drawingList.value = [];
const formId = route.query && route.query.formId;
if (formId) {
getForm(formId).then((res) => {
formConf.value = JSON.parse(res.data.content);
drawingList.value = formConf.value.fields;
formObj.value = res.data;
});
} else {
if (formConfInDB) {
formConf.value = formConfInDB;
formConf.value.fields = null;
}
}
loadBeautifier((btf) => {
beautifier = btf;
});
const clipboard = new ClipboardJS("#copyNode", {
text: (trigger) => {
const codeStr = generateCode();
ElNotification({
title: "成功",
message: "代码已复制到剪切板,可粘贴。",
type: "success",
});
return codeStr;
},
});
clipboard.on("error", (e) => {
ElMessage.error("代码复制失败");
});
});
watch(
() => activeData.value.__config__.label,
(val, oldVal) => {
if (
activeData.value.placeholder === undefined ||
!activeData.value.__config__.tag ||
oldActiveId !== activeId.value
) {
return;
}
activeData.value.placeholder =
activeData.value.placeholder.replace(oldVal, "") + val;
}
);
watch(
activeId,
(val) => {
oldActiveId = val;
},
{ immediate: true }
);
watch(
drawingList,
(val) => {
saveDrawingListDebounce(val);
if (val.length === 0) idGlobal.value = 100;
},
{ deep: true }
);
watch(
idGlobal,
(val) => {
saveIdGlobalDebounce(val);
},
{ immediate: true }
);
// 防止 firefox 下 拖拽 会新打卡一个选项卡
document.body.ondrop = (event) => {
event.preventDefault();
event.stopPropagation();
};
</script>
<style lang="scss">
body,
html {
margin: 0;
padding: 0;
background: #fff;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
sans-serif, Apple Color Emoji, Segoe UI Emoji;
}
input,
textarea {
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
sans-serif, Apple Color Emoji, Segoe UI Emoji;
}
.editor-tabs {
background: #121315;
.el-tabs__header {
margin: 0;
border-bottom-color: #121315;
.el-tabs__nav {
border-color: #121315;
}
}
.el-tabs__item {
height: 32px;
line-height: 32px;
color: #888a8e;
border-left: 1px solid #121315 !important;
background: #363636;
margin-right: 5px;
user-select: none;
}
.el-tabs__item.is-active {
background: #1e1e1e;
border-bottom-color: #1e1e1e !important;
color: #fff;
}
.el-icon-edit {
color: #f1fa8c;
}
.el-icon-document {
color: #a95812;
}
}
// home
.right-scrollbar {
.el-scrollbar__view {
padding: 12px 18px 15px 15px;
}
}
.left-scrollbar .el-scrollbar__wrap {
box-sizing: border-box;
overflow-x: hidden !important;
margin-bottom: 0 !important;
}
.center-tabs {
.el-tabs__header {
margin-bottom: 0 !important;
}
.el-tabs__item {
width: 50%;
text-align: center;
}
.el-tabs__nav {
width: 100%;
}
}
.reg-item {
padding: 12px 6px;
background: #f8f8f8;
position: relative;
border-radius: 4px;
.close-btn {
position: absolute;
right: -6px;
top: -6px;
display: block;
width: 16px;
height: 16px;
line-height: 16px;
background: rgba(0, 0, 0, 0.2);
border-radius: 50%;
color: #fff;
text-align: center;
z-index: 1;
cursor: pointer;
font-size: 12px;
&:hover {
background: rgba(210, 23, 23, 0.5);
}
}
& + .reg-item {
margin-top: 18px;
}
}
.action-bar {
& .el-button + .el-button {
margin-left: 15px;
}
& i {
font-size: 20px;
vertical-align: middle;
position: relative;
top: -1px;
}
}
.custom-tree-node {
width: 100%;
font-size: 14px;
.node-operation {
float: right;
}
i[class*="el-icon"] + i[class*="el-icon"] {
margin-left: 6px;
}
.el-icon-plus {
color: #409eff;
}
.el-icon-delete {
color: #157a0c;
}
}
.left-scrollbar .el-scrollbar__view {
overflow-x: hidden;
}
.el-rate {
display: inline-block;
vertical-align: text-top;
}
.el-upload__tip {
line-height: 1.2;
}
$selectedColor: #f6f7ff;
$lighterBlue: #409eff;
.container {
position: relative;
width: 100%;
height: 100%;
}
.components-list {
padding: 8px;
box-sizing: border-box;
height: 100%;
.components-item {
display: inline-block;
width: 48%;
margin: 1%;
transition: transform 0ms !important;
}
}
.components-draggable {
padding-bottom: 20px;
}
.components-title {
font-size: 14px;
color: #222;
margin: 6px 2px;
.svg-icon {
color: #666;
font-size: 18px;
}
}
.components-body {
padding: 8px 10px;
background: $selectedColor;
font-size: 12px;
cursor: move;
border: 1px dashed $selectedColor;
border-radius: 3px;
.svg-icon {
color: #777;
font-size: 15px;
}
&:hover {
border: 1px dashed #787be8;
color: #787be8;
.svg-icon {
color: #787be8;
}
}
}
.left-board {
width: 260px;
position: absolute;
left: 0;
top: 0;
height: 100vh;
}
.left-scrollbar {
height: calc(100vh - 42px);
overflow: hidden;
}
.center-scrollbar {
height: calc(100vh - 42px);
overflow: hidden;
border-left: 1px solid #f1e8e8;
border-right: 1px solid #f1e8e8;
box-sizing: border-box;
}
.center-board {
height: 100vh;
width: auto;
margin: 0 350px 0 260px;
box-sizing: border-box;
}
.empty-info {
position: absolute;
top: 46%;
left: 0;
right: 0;
text-align: center;
font-size: 18px;
color: #ccb1ea;
letter-spacing: 4px;
}
.action-bar {
position: relative;
height: 42px;
text-align: right;
padding: 0 15px;
box-sizing: border-box;
border: 1px solid #f1e8e8;
border-top: none;
border-left: none;
.delete-btn {
color: #f56c6c;
}
}
.logo-wrapper {
position: relative;
height: 42px;
background: #fff;
border-bottom: 1px solid #f1e8e8;
box-sizing: border-box;
}
.logo {
position: absolute;
left: 12px;
top: 6px;
line-height: 30px;
color: #00afff;
font-weight: 600;
font-size: 17px;
white-space: nowrap;
> img {
width: 30px;
height: 30px;
vertical-align: top;
}
.github {
display: inline-block;
vertical-align: sub;
margin-left: 15px;
> img {
height: 22px;
}
}
}
.center-board-row {
padding: 12px 12px 15px 12px;
box-sizing: border-box;
& > .el-form {
// 69 = 12+15+42
height: calc(100vh - 69px);
}
}
.drawing-board {
height: 100%;
position: relative;
.components-body {
padding: 0;
margin: 0;
font-size: 0;
}
.sortable-ghost {
position: relative;
display: block;
overflow: hidden;
&::before {
content: " ";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: rgb(89, 89, 223);
z-index: 2;
}
}
.components-item.sortable-ghost {
width: 100%;
height: 60px;
background-color: $selectedColor;
}
.active-from-item {
& > .el-form-item {
background: $selectedColor;
border-radius: 6px;
}
& > .drawing-item-copy,
& > .drawing-item-delete {
display: initial;
}
& > .component-name {
color: $lighterBlue;
}
}
.el-form-item {
margin-bottom: 15px;
}
}
.drawing-item {
position: relative;
cursor: move;
&.unfocus-bordered:not(.activeFromItem) > div:first-child {
border: 1px dashed #ccc;
}
.el-form-item {
padding: 12px 10px;
}
}
.drawing-row-item {
position: relative;
cursor: move;
box-sizing: border-box;
border: 1px dashed #ccc;
border-radius: 3px;
padding: 0 2px;
margin-bottom: 15px;
.drawing-row-item {
margin-bottom: 2px;
}
.el-col {
margin-top: 22px;
}
.el-form-item {
margin-bottom: 0;
}
.drag-wrapper {
min-height: 80px;
}
&.active-from-item {
border: 1px dashed $lighterBlue;
}
.component-name {
position: absolute;
top: 0;
left: 0;
font-size: 12px;
color: #bbb;
display: inline-block;
padding: 0 6px;
}
}
.drawing-item,
.drawing-row-item {
&:hover {
& > .el-form-item {
background: $selectedColor;
border-radius: 6px;
}
& > .drawing-item-copy,
& > .drawing-item-delete {
display: initial;
}
}
& > .drawing-item-copy,
& > .drawing-item-delete {
display: none;
position: absolute;
top: -10px;
width: 22px;
height: 22px;
line-height: 22px;
text-align: center;
border-radius: 50%;
font-size: 12px;
border: 1px solid;
cursor: pointer;
z-index: 1;
}
& > .drawing-item-copy {
right: 56px;
border-color: $lighterBlue;
color: $lighterBlue;
background: #fff;
&:hover {
background: $lighterBlue;
color: #fff;
}
}
& > .drawing-item-delete {
right: 24px;
border-color: #f56c6c;
color: #f56c6c;
background: #fff;
&:hover {
background: #f56c6c;
color: #fff;
}
}
}
</style>