This commit is contained in:
zhang zhuo 2025-12-10 17:39:40 +08:00
parent 4037ebe090
commit a06da14463
13 changed files with 576 additions and 405 deletions

View File

@ -29,5 +29,10 @@ export default {
show: async function (data = {}) {
return await http.get("gen_table/show", data);
},
},
form: {
build: async function (data = {}) {
return await http.post("form/build", data);
},
}
}

View File

@ -0,0 +1,3 @@
<template>
<svg t="1765353827346" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5302" width="256" height="256"><path d="M216.4 107.1h50.2c5.7 0 10.4 4.7 10.4 10.4V156c0 5.7-4.7 10.4-10.4 10.4h-45.8c-5.7 0-13.4 4.6-13.4 10.4l-1 246.6c-1.3 35.2-25.1 90-58.6 91.4 23.2-1 59.6 39.5 59.6 89.4l-1 242.4c0 5.8 4.7 19.6 10.6 19.6h49.3c5.9 0 10.8 6.8 10.8 12.8v30.9c0 5.9-4.8 10.8-10.8 10.8h-49.8c-32.4-12.2-60.6-40.7-60.6-90.4v-181c0-49.9-27.1-90.4-60.6-90.4H77.1c-6.8 0-12.2-5.5-12.2-12.2v-65.9c0-6.8 5.5-12.2 12.2-12.2h18.1c33.5 0 60.6-40.5 60.6-90.4V197.4c0-49.9 27.1-90.3 60.6-90.3M808.6 107.1c33.5 0 60.6 40.4 60.6 90.3v180.8c0 49.9 27.1 90.4 60.6 90.4h18.1c6.7 0 12.2 5.4 12.2 12.2v65.9c0 6.7-5.4 12.2-12.2 12.2h-18.2c-33.5 0-60.6 40.5-60.6 90.4v181c0 49.7-28.2 78.2-60.6 90.4h-49.8c-6 0-10.8-4.9-10.8-10.8V879c0-6 4.9-12.8 10.8-12.8H808c5.9 0 10.6-13.8 10.6-19.6l-1-242.4c0-49.9 36.4-90.4 59.6-89.4-33.5-1.4-57.3-56.2-58.6-91.4l-1-246.6c0-5.8-7.7-10.4-13.4-10.4h-45.8c-5.7 0-10.4-4.7-10.4-10.4v-38.5c0-5.7 4.7-10.4 10.4-10.4h50.2M508.4 667.8c16.8 0 30.4 13.6 30.4 30.4 0 16.8-13.6 30.4-30.4 30.4-16.8 0-30.4-13.6-30.4-30.4 0-16.8 13.6-30.4 30.4-30.4M386.8 667.8c16.8 0 30.4 13.6 30.4 30.4 0 16.8-13.6 30.4-30.4 30.4-16.8 0-30.4-13.6-30.4-30.4 0-16.8 13.6-30.4 30.4-30.4M630 667.8c16.8 0 30.4 13.6 30.4 30.4 0 16.8-13.6 30.4-30.4 30.4-16.8 0-30.4-13.6-30.4-30.4 0-16.8 13.6-30.4 30.4-30.4z" p-id="5303"></path></svg>
</template>

View File

@ -0,0 +1,3 @@
<template>
<svg t="1765332605221" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11956" width="256" height="256"><path d="M929.6896 425.7792c50.0736 0 91.4432 39.1168 94.208 89.088l0.1024 5.2224v323.3792c0 50.0736-39.1168 91.392-89.1392 94.208l-5.1712 0.1024h-215.552a40.448 40.448 0 0 1-3.8912-80.64l3.84-0.2048h215.552c6.5536 0 12.1344-4.608 13.312-11.0592l0.2048-2.4064v-323.3792a13.4656 13.4656 0 0 0-11.0592-13.312l-2.4064-0.1536h-215.552a40.448 40.448 0 0 1-3.8912-80.6912l3.84-0.1536h215.552zM498.5344 102.4a121.2416 121.2416 0 0 1 121.2416 121.2416v592.896a121.2416 121.2416 0 0 1-121.2416 121.2416H121.2416A121.2416 121.2416 0 0 1 0 816.4864V223.6416A121.2416 121.2416 0 0 1 121.2416 102.4z m0 80.8448H121.2416a40.448 40.448 0 0 0-40.448 40.448v592.7936c0 22.3232 18.1248 40.448 40.448 40.448h377.2928a40.448 40.448 0 0 0 40.448-40.448V223.6416a40.448 40.448 0 0 0-40.448-40.448zM371.8656 716.8a26.9312 26.9312 0 0 1 0 53.9136H264.0896a26.9312 26.9312 0 0 1 0-53.9136h107.776z m336.896-606.3104a237.1584 237.1584 0 0 1 226.304 180.5312h53.9136c3.2768 0 6.4 1.024 7.3728 3.9936 0.9728 2.9184 0.0512 4.864-1.9968 6.8096l-78.1312 75.4176a21.5552 21.5552 0 0 1-21.5552 0l-78.1824-75.4176A5.376 5.376 0 0 1 819.2 293.7344h64.6656a185.9584 185.9584 0 0 0-175.104-132.096h-37.7856v-51.2z" p-id="11957"></path></svg>
</template>

View File

@ -0,0 +1,3 @@
<template>
<svg t="1765333944912" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13131" width="256" height="256"><path d="M811.52 927.744h-599.04c-81.92 0-148.992-66.56-148.992-148.992V188.928c0-81.92 66.56-148.992 148.992-148.992h599.04c81.92 0 148.992 66.56 148.992 148.992v589.824c0 82.432-67.072 148.992-148.992 148.992m-599.04-829.44c-50.176 0-90.624 40.96-90.624 90.624v589.824c0 50.176 40.96 90.624 90.624 90.624h599.04c50.176 0 90.624-40.96 90.624-90.624V188.928c0-50.176-40.96-90.624-90.624-90.624h-599.04m12.288 307.2v-307.2h574.464v307.2c0 41.472-33.792 74.752-74.752 74.752h-424.96c-40.96 0-74.752-33.28-74.752-74.752z m516.096-8.704V98.304H283.136v298.496c0 16.896 13.824 30.72 30.72 30.72h396.8c16.896-0.512 30.208-13.824 30.208-30.72z m-125.44-53.248h22.016c9.728 0 17.92-8.192 17.92-17.92V178.176c0-9.728-8.192-17.92-17.92-17.92h-22.016c-9.728 0-17.92 8.192-17.92 17.92v146.944c0 10.24 8.192 18.432 17.92 18.432z" p-id="13132"></path></svg>
</template>

View File

@ -25,6 +25,10 @@ import {CustomLineModel, CustomLine, CUSTOM_LINE} from "./model/CustomLine"
import {ElMessageBox} from 'element-plus'
import {useI18n} from "vue-i18n"
defineOptions({
name: 'toolsFlow'
})
const containerRef = ref(HTMLElement | null);
const {t} = useI18n()
let lf = ref(LogicFlow | null);
@ -256,7 +260,7 @@ const handleKeydown = (event) => {
.right-panel {
width: 320px;
background: #fff;
border-left: 1px solid #ddd;
border-left: 1px solid var(--el-border-color-light);
padding: 10px;
}

View File

@ -1,67 +1,148 @@
<template>
<section class="panel">
<el-scrollbar class="h100">
<el-main class="h100">
<el-form class="h100" :label-position="config.labelPosition" :label-width="config.labelWidth"
:size="config.size" :disabled="config.disabled">
<drag :fields="fields" @change="changeHandle" name="page" v-model:curKey="curKey"
v-model:counter="counter"/>
</el-form>
</el-main>
<section class="header">
<el-icon size="24" title="切换设备类型" :class="{active: data.config.isMobile}" @click="toggleDevice">
<component :is="'pi-icon-qie-huan'"/>
</el-icon>
<el-icon size="22" title="暂存" @click="tempSave">
<component :is="'pi-icon-zan-cun'"/>
</el-icon>
<el-icon size="24" title="清空" @click="empty">
<component :is="'el-icon-delete'"/>
</el-icon>
<el-icon size="24" title="复制代码" @click="copyCode">
<component :is="'el-icon-document'"/>
</el-icon>
<el-icon size="24" title="复制代码" @click="downFile">
<component :is="'el-icon-download'"/>
</el-icon>
<el-icon size="24" title="复制JSON" @click="copyJson">
<component :is="'pi-icon-json'"/>
</el-icon>
</section>
<el-scrollbar style="height: calc(100% - 20px);" :class="{mobile: data.config.isMobile}">
<el-form class="h100 pad10" :label-position="data.config.labelPosition"
:label-width="data.config.labelWidth" :size="data.config.size" :disabled="data.config.disabled">
<drag :data="data" name="page"/>
</el-form>
</el-scrollbar>
</section>
</template>
<script setup>
import {computed, ref} from "vue"
import {getCurrentInstance, ref} from "vue"
import drag from "./drag.vue"
defineExpose({clickAddComp})
import tools from "@/utils/tools"
import {ElMessageBox} from "element-plus"
import {useI18n} from "vue-i18n"
import FormBuild from "./formBuild"
import api from "@/api/index.js";
const props = defineProps({
fields: {type: Array, default: []},
config: {type: Object, default: {}},
counter: {type: Number, default: 0}
data: {type: FormBuild, default: []}
})
const emit = defineEmits(['change', "update:counter"])
let curKey = ref("")
const {t} = useI18n()
const {proxy} = getCurrentInstance()
const counter = computed({
get() {
return props.counter
},
set(value) {
emit('update:counter', value)
}
})
//
function clickAddComp(e) {
props.fields.push(e)
emit('change', e)
function toggleDevice() {
props.data.config.isMobile = !props.data.config.isMobile
}
function changeHandle(e) {
emit('change', e)
function tempSave() {
tools.data.set("FORM-FIELDS", props.data.fields)
tools.data.set("FORM-CONFIG", props.data.config)
proxy.$message.success("保存成功")
}
function empty() {
ElMessageBox.confirm("确定要清空所有表单数据吗", t('system.warning'), {
confirmButtonText: t('system.ok'), cancelButtonText: t('system.cancel'), type: 'warning'
}).then(() => {
props.data.restData()
}).catch(() => {
})
}
function copyJson() {
navigator.clipboard.writeText(JSON.stringify({
fields: props.data.fields,
config: props.data.config
}))
proxy.$message.success("复制成功")
}
async function copyCode() {
const res = await api.tools.form.build({
fields: props.data.fields,
config: props.data.config
})
console.log(res.data['form.vue'])
navigator.clipboard.writeText(res.data['form.vue'])
proxy.$message.success("复制成功")
}
async function downFile() {
const res = await api.tools.form.build({
fields: props.data.fields,
config: props.data.config
})
console.log(res)
}
</script>
<style lang="scss" scoped>
.panel {
flex: 1;
user-select: none;
}
::-webkit-scrollbar {
display: none;
}
.h100 {
height: 100%;
}
.pad10 {
padding: 10px;
}
.mobile {
width: 414px;
height: 736px !important;
border: 1px solid var(--el-border-color);
border-radius: 5px;
margin: 20px auto;
}
:deep(.el-scrollbar__view) {
height: 100%;
}
.header {
display: flex;
align-items: center;
background: #f6f7ff;
padding: 2px 10px;
gap: 8px;
border-radius: 3px;
justify-content: end;
.active {
color: var(--el-color-primary);
}
i {
cursor: pointer;
color: #666666;
padding: 5px;
}
i:hover {
background-color: #c9ced3;
border-radius: 50%;
opacity: 0.6;
}
}
</style>

View File

@ -246,3 +246,193 @@ export function setByPath(obj, path, value) {
keys.slice(0, -1).forEach(k => o = o[k])
o[keys[keys.length - 1]] = value
}
export const inputComps = [{
title: "单行文本",
icon: "pi-icon-line-input",
name: "text",
props: {
type: 'text'
},
rules: [],
width: "100%",
required: false
}, {
title: "多行文本",
icon: "pi-icon-multi-input",
name: "textarea",
props: {
type: 'textarea'
},
rules: [],
width: "100%",
required: false
}, {
title: "密码",
icon: "pi-icon-lock",
name: "password",
props: {
type: 'password',
showPassword: true
},
rules: [],
width: "100%",
required: false
}, {
title: "计数器",
icon: "pi-icon-number-input",
name: "number",
props: {
controlsPosition: ''
},
rules: [],
width: "100%",
required: false,
value: 1
}]
export const choiceComps = [{
title: "下拉组件",
icon: "pi-icon-select",
name: "select",
props: {},
rules: [],
options: [],
width: "100%",
required: false,
}, {
title: "级联组件",
icon: "pi-icon-cascader",
name: "cascader",
props: {
props: {
label: 'label',
value: 'value',
multiple: false,
},
options: [{label: '一号楼', value: 'r1', children: [{label: '二单元', value: 'c2'}]}]
},
rules: [],
width: "100%",
required: false,
}, {
title: "单选组件",
icon: "pi-icon-radio",
name: "radio",
props: {},
rules: [],
options: [],
width: "100%",
style: '',
required: false,
}, {
title: "多选组件",
icon: "pi-icon-checkbox",
name: "checkbox",
props: {},
rules: [],
options: [],
style: '',
required: false,
}, {
title: "开关",
icon: "pi-icon-switch",
name: "switch",
props: {},
rules: [],
required: false,
}, {
title: "滑块",
icon: "pi-icon-slider",
name: "slider",
props: {},
rules: [],
width: "100%",
required: false,
}, {
title: "时间选择",
icon: "pi-icon-time-picker",
name: "time",
props: {
format: 'HH:mm:ss',
valueFormat: 'HH:mm:ss'
},
rules: []
}, {
title: "时间范围",
icon: "pi-icon-time-range",
name: "timerange",
props: {
isRange: true,
format: 'HH:mm:ss',
valueFormat: 'HH:mm:ss'
},
rules: [],
width: "100%",
required: false,
}, {
title: "日期选择",
icon: "pi-icon-date-picker",
name: "date",
props: {
type: "date",
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD'
},
rules: [],
width: "100%",
required: false,
}, {
title: "日期范围",
icon: "pi-icon-date-range",
name: "daterange",
props: {
type: "daterange",
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD'
},
rules: [],
width: "100%",
required: false,
}, {
title: "评分",
icon: "pi-icon-rate",
name: "rate",
props: {},
rules: [],
required: false,
}, {
title: "颜色选择",
icon: "pi-icon-color-picker",
name: "color",
props: {},
rules: [],
required: false,
}, {
title: "上传",
icon: "pi-icon-upload-file",
name: "upload",
props: {
listType: 'text',
name: 'file'
},
rules: [],
required: false,
btnText: "点击上传"
}]
export const layoutComps = [{
title: "行容器",
icon: "pi-icon-row-layout",
name: "layout",
props: {
gutter: 15
},
children: []
}, {
title: "按钮",
icon: "pi-icon-button",
name: "button",
props: {
type: 'primary'
},
btnText: "按钮文字"
}];

View File

@ -0,0 +1,76 @@
<template>
<el-dialog :title="titleMap[mode]" v-model="visible" :width="500" destroy-on-close @closed="$emit('closed')">
<el-form :model="form" :rules="rules" :disabled="mode==='show'" ref="formRef"
label-width="70" label-position="right" size="default">
<el-form-item label="字典名称" prop="dict_name">
<el-input type="text" v-model="form.dict_name" placeholder="请输入字典名称" clearable></el-input>
</el-form-item>
<el-form-item label="字典类型" prop="dict_type">
<el-input type="text" v-model="form.dict_type" placeholder="请输入字典类型" clearable></el-input>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input type="textarea" :rows="3" v-model="form.remark" placeholder="请输入备注" clearable></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible=false"> </el-button>
<el-button v-if="mode!=='show'" type="primary" :loading="isSaveing" @click="submit()"> </el-button>
</template>
</el-dialog>
</template>
<script setup>
import {getCurrentInstance, ref} from 'vue'
import api from "@/api/index"
defineExpose({
open
})
const emit = defineEmits(['success', 'closed'])
const formRef = ref(null)
const {proxy} = getCurrentInstance()
let mode = ref('add')
let titleMap = ref({
add: '新增',
edit: '编辑',
show: '查看'
})
let visible = ref(false)
let isSaveing = ref(false)
let form = ref({
dict_id: null,
dict_name: null,
dict_type: null,
remark: null
})
const rules = ref({
dict_name: [
{required: true, message: '请填写字典名称'}
],
dict_type: [
{required: true, message: '请填写字典类型'}
]
})
function open(m = 'add', data = null) {
mode.value = m
visible.value = true
Object.assign(form.value, data)
}
async function submit() {
//
const validate = await formRef.value.validate().catch(() => {
});
if (!validate) {
return false
}
isSaveing.value = true;
const res = form.value.dict_id ? await api.system.dict.edit(form.value) : await api.system.dict.add(form.value);
isSaveing.value = false;
emit('success')
visible.value = false;
proxy.$message.success(res.msg)
}
</script>

View File

@ -1,18 +1,18 @@
<template>
<draggable :list="fields" :scroll="true" animation="200" item-key="id" class="w100 h100"
<draggable :list="data.fields" :scroll="true" animation="200" item-key="id" class="w100 h100"
:group="group" @update:list="fields = $event" @add="addComp" @sort="sortComp"
ghostClass="ghostClass" :class="{empty: fields.length === 0, layout: isRow}">
ghostClass="ghostClass" :class="{empty: data.fields.length === 0, layout: isRow}">
<template #item="{ element, index }">
<el-col :span="element.span || 24" class="col">
<el-row v-if="element.name === 'layout'" class="item row" :class="{'active': curr===element.id}"
<el-row v-if="element.name === 'layout'" class="item row" :class="{'active': field.id===element.id}"
@click.stop="clickComp(element)" v-bind="element.props">
<span class="name">{{ element.field_name }}</span>
<drag :fields="element.children" v-model:curKey="curr" :name="element.field_name"
:is-row="true" v-model:counter="counter" @change="changeHandle"/>
<drag :fields="element.children" :config="data.config" :data
:name="element.field_name" :is-row="true"/>
</el-row>
<item v-else class="item" :class="{'active': curr===element.id}" :field="element"
<item v-else class="item" :class="{'active': field.id===element.id}" :field="element"
@click.stop="clickComp(element)"/>
<div v-if="curr===element.id" class="tools-box">
<div v-if="field.id===element.id" class="tools-box">
<el-icon size="22" class="icon-box remove" @click.stop="delComp(index)">
<component :is="'pi-icon-shan-chu'"/>
</el-icon>
@ -30,71 +30,52 @@ import draggable from 'vuedraggable'
import {computed, nextTick} from "vue"
import item from "./item.vue"
import drag from "./drag.vue"
import tools from "@/utils/tools.js"
const emit = defineEmits(['change', 'update:curKey', 'update:counter'])
import tools from "@/utils/tools"
import FormBuild from "./formBuild"
const props = defineProps({
fields: {type: Array, default: []},
data: {type: FormBuild, default: {}},
curKey: {type: String, default: ''},
isRow: {type: Boolean, default: false},
counter: {type: Number, default: 0},
name: {type: String, default: 'page'}
})
const group = {name: props.name, pull: true, put: true}
const curr = computed({
get() {
return props.curKey
},
set(value) {
emit('update:curKey', value)
}
})
const counter = computed({
get() {
return props.counter
},
set(value) {
emit('update:counter', value)
}
const field = computed(() => {
return props.data.activeField || {}
})
//
function sortComp(e) {
let element = props.fields[e.newIndex]
let element = props.data.fields[e.newIndex]
if (element) {
curr.value = element.id
emit('change', element)
props.data.setActiveField(element)
}
}
//
function addComp(e) {
let element = props.fields[e.newIndex]
let element = props.data.fields[e.newIndex]
if (props.isRow) {
element.span = 12
}
curr.value = element.id
emit('change', element)
props.data.setActiveField(element)
}
//
function clickComp(element) {
if (curr.value === element.id) {
if (field.value.id === element.id) {
return;
}
curr.value = element.id
emit('change', element)
props.data.setActiveField(element)
}
//
function delComp(index) {
nextTick(() => {
//
const len = props.fields.length
props.fields.splice(index, 1);
const len = props.data.fields.length
props.data.fields.splice(index, 1);
let i = 0;
if (len > 1) {
if (index === 0) {
@ -105,37 +86,31 @@ function delComp(index) {
i = index
}
}
let element = props.fields[i] || {}
curr.value = element.id || null
emit('change', element)
let element = props.data.fields[i] || {}
props.data.setActiveField(element)
})
}
function copyComp(index) {
nextTick(() => {
//
const tmp = JSON.parse(JSON.stringify(props.fields[index]))
const tmp = JSON.parse(JSON.stringify(props.data.fields[index]))
tmp.field_name = increaseNum(tmp.field_name)
tmp.id = increaseNum(tmp.id)
counter.value++
props.fields.push(tmp)
props.data.config.counter++
props.data.fields.push(tmp)
//
tools.array.zIndexTo(props.fields, props.fields.length - 1, index + 1)
let element = props.fields[index + 1]
curr.value = element.id
emit('change', element)
tools.array.zIndexTo(props.data.fields, props.data.fields.length - 1, index + 1)
let element = props.data.fields[index + 1]
props.data.setActiveField(element)
})
}
function increaseNum(str) {
return str.replace(/(\d+)(?!.*\d)/, () => {
return props.counter.toString();
return props.data.config.counter.toString();
});
}
function changeHandle(e) {
emit('change', e)
}
</script>
<style lang="scss" scoped>

View File

@ -0,0 +1,60 @@
interface Config {
ref: string,
model: string,
rules: string,
size: string,
labelPosition: string,
labelWidth: number,
disabled: boolean,
isMobile: boolean,
counter: 0
}
export default class FormBuild {
// 表单配置
config: Config
// 字段信息
fields: Array<any>
// 激活字段
activeField: Object
constructor() {
this.config = {
ref: 'formRef',
model: 'form',
rules: 'rules',
size: 'default',
labelPosition: 'right',
labelWidth: 100,
disabled: false,
isMobile: false,
counter: 0
}
this.fields = []
this.activeField = null
}
restData() {
this.config = {
ref: 'formRef',
model: 'form',
rules: 'rules',
size: 'default',
labelPosition: 'right',
labelWidth: 100,
disabled: false,
isMobile: false,
counter: 0
}
this.fields = []
}
initData(config: Config, fields: Array<any>) {
this.config = config
this.fields = fields
}
setActiveField(field: Object) {
this.activeField = field
}
}

View File

@ -1,67 +1,43 @@
<template>
<el-container class="pi-panel">
<el-main class="main">
<left-panel @addField="addField" v-model:counter="counter"></left-panel>
<el-divider direction="vertical" style="height: 100%"/>
<center-panel ref="centerRef" :fields="fields" :config="config" @change="setField"
v-model:counter="counter"></center-panel>
<el-divider direction="vertical" style="height: 100%"/>
<right-panel :config="config" :curField="curField" @save="tempSave"></right-panel>
</el-main>
<left-panel :data="formData"></left-panel>
<center-panel ref="centerRef" :data="formData"></center-panel>
<right-panel :data="formData" :fieldd="field"></right-panel>
</el-container>
</template>
<script setup>
import {onMounted, ref, getCurrentInstance} from "vue"
import {onMounted, ref, reactive} from "vue"
import leftPanel from "./left"
import centerPanel from "./center"
import rightPanel from "./right"
import tools from "@/utils/tools"
import FormBuild from "./formBuild"
const {proxy} = getCurrentInstance()
const centerRef = ref(null)
let fields = ref([])
let config = ref({
ref: 'formRef',
model: 'form',
rules: 'rules',
size: 'default',
labelPosition: 'right',
labelWidth: 100,
disabled: false
defineOptions({
name: "toolsForm"
})
let curField = ref({})
let counter = ref(0)
const centerRef = ref(null)
let formData = reactive(new FormBuild())
let field = ref({})
onMounted(() => {
fields.value = tools.data.get("FORM-FIELDS-DATA") || []
const _config = tools.data.get("FORM-CONFIG-DATA")
if (_config) {
config.value = _config
const fields = tools.data.get("FORM-FIELDS")
const config = tools.data.get("FORM-CONFIG")
if (fields && config) {
formData.initData(config, fields)
}
counter.value = tools.data.get("FORM-COUNTER-DATA") || 0
})
function setField(v) {
curField.value = v
function activeField(v) {
field.value = v
}
function addField(e) {
centerRef.value.clickAddComp(e)
}
function tempSave() {
console.log("保存参数", fields.value)
tools.data.set("FORM-FIELDS-DATA", fields.value)
tools.data.set("FORM-CONFIG-DATA", config.value)
tools.data.set("FORM-COUNTER-DATA", counter.value)
proxy.$message.success("保存成功")
}
</script>
<style scoped>
.main {
.pi-panel {
display: flex;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<section class="panel">
<el-aside class="panel">
<el-scrollbar>
<div class="title">
<el-text>
@ -9,7 +9,8 @@
输入组件
</el-text>
</div>
<draggable v-model="inputComps" animation="200" item-key="name" class="go-base" :group="group" :sort="false"
<draggable v-model="inputComps" animation="200" item-key="name" class="draggroup" :group="group"
:sort="false"
:clone="cloneField">
<template #item="{ element }">
<div class="item" @click="handleClick(element)">
@ -31,7 +32,8 @@
选择组件
</el-text>
</div>
<draggable v-model="choiceComps" animation="200" item-key="name" class="go-base" :group="group" :sort="false"
<draggable v-model="choiceComps" animation="200" item-key="name" class="draggroup" :group="group"
:sort="false"
:clone="cloneField">
<template #item="{ element }">
<div class="item" @click="handleClick(element)">
@ -53,7 +55,8 @@
布局组件
</el-text>
</div>
<draggable v-model="layoutComps" animation="200" item-key="name" class="go-base" :group="group" :sort="false"
<draggable v-model="layoutComps" animation="200" item-key="name" class="draggroup" :group="group"
:sort="false"
:clone="cloneField">
<template #item="{ element }">
<div class="item" @click="handleClick(element)">
@ -68,234 +71,38 @@
</template>
</draggable>
</el-scrollbar>
</section>
</el-aside>
</template>
<script setup>
import draggable from 'vuedraggable'
import {computed, ref} from "vue"
import FormBuild from "./formBuild"
import {inputComps, choiceComps, layoutComps} from "./config"
const emit = defineEmits(['addField', 'update:counter'])
const props = defineProps({
counter: {type: Number, default: 0}
data: {type: FormBuild, default: {}}
})
const group = {name: 'base', pull: 'clone', put: false}
const inputComps = [{
title: "单行文本",
icon: "pi-icon-line-input",
name: "text",
props: {
type: 'text'
},
rules: [{regex: '', message: ''}],
width: "100%",
required: false
}, {
title: "多行文本",
icon: "pi-icon-multi-input",
name: "textarea",
props: {
type: 'textarea'
},
rules: [],
width: "100%",
required: false
}, {
title: "密码",
icon: "pi-icon-lock",
name: "password",
props: {
type: 'password',
showPassword: true
},
rules: [],
width: "100%",
required: false
}, {
title: "计数器",
icon: "pi-icon-number-input",
name: "number",
props: {
controlsPosition: ''
},
rules: [],
width: "100%",
required: false,
value: 1
}]
const choiceComps = [{
title: "下拉组件",
icon: "pi-icon-select",
name: "select",
props: {},
rules: [],
options: [],
width: "100%",
required: false,
}, {
title: "级联组件",
icon: "pi-icon-cascader",
name: "cascader",
props: {
props: {
label: 'label',
value: 'value',
multiple: false,
},
options: [{label: '一号楼', value: 'r1', children: [{label: '二单元', value: 'c2'}]}]
},
rules: [],
width: "100%",
required: false,
}, {
title: "单选组件",
icon: "pi-icon-radio",
name: "radio",
props: {},
rules: [],
options: [],
width: "100%",
style: '',
required: false,
}, {
title: "多选组件",
icon: "pi-icon-checkbox",
name: "checkbox",
props: {},
rules: [],
options: [],
style: '',
required: false,
}, {
title: "开关",
icon: "pi-icon-switch",
name: "switch",
props: {},
rules: [],
required: false,
}, {
title: "滑块",
icon: "pi-icon-slider",
name: "slider",
props: {},
rules: [],
width: "100%",
required: false,
}, {
title: "时间选择",
icon: "pi-icon-time-picker",
name: "time",
props: {
format: 'HH:mm:ss',
valueFormat: 'HH:mm:ss'
},
rules: []
}, {
title: "时间范围",
icon: "pi-icon-time-range",
name: "timerange",
props: {
isRange: true,
format: 'HH:mm:ss',
valueFormat: 'HH:mm:ss'
},
rules: [],
width: "100%",
required: false,
}, {
title: "日期选择",
icon: "pi-icon-date-picker",
name: "date",
props: {
type: "date",
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD'
},
rules: [],
width: "100%",
required: false,
}, {
title: "日期范围",
icon: "pi-icon-date-range",
name: "daterange",
props: {
type: "daterange",
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD'
},
rules: [],
width: "100%",
required: false,
}, {
title: "评分",
icon: "pi-icon-rate",
name: "rate",
props: {},
rules: [],
required: false,
}, {
title: "颜色选择",
icon: "pi-icon-color-picker",
name: "color",
props: {},
rules: [],
required: false,
}, {
title: "上传",
icon: "pi-icon-upload-file",
name: "upload",
props: {
listType: 'text',
name: 'file'
},
rules: [],
required: false,
btnText: "点击上传"
}]
const layoutComps = [{
title: "行容器",
icon: "pi-icon-row-layout",
name: "layout",
props: {
gutter: 15
},
children: []
}, {
title: "按钮",
icon: "pi-icon-button",
name: "button",
props: {
type: 'primary'
},
btnText: "按钮文字"
}];
const counter = computed({
get() {
return props.counter
},
set(value) {
emit('update:counter', value)
}
})
function cloneField(e) {
const field = JSON.parse(JSON.stringify(e));
field.field_name = field.name + "_" + counter.value;
field.id = field.name + counter.value
counter.value++
field.field_name = field.name + "_" + props.data.config.counter;
field.id = field.name + props.data.config.counter
props.data.config.counter++
return field
}
function handleClick(e) {
emit('addField', cloneField(e))
let field = cloneField(e)
props.data.fields.push(field)
props.data.setActiveField(field)
}
</script>
<style lang="scss" scoped>
.panel {
width: 260px;
padding: 10px;
.title {
margin-bottom: 10px;
@ -318,14 +125,14 @@ function handleClick(e) {
display: none !important;
}
.go-base {
.draggroup {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: 10px;
.item {
width: 125px;
width: 115px;
font-size: 12px;
cursor: move;
background: #f6f7ff;

View File

@ -1,69 +1,71 @@
<template>
<section class="panel">
<el-aside class="panel">
<el-scrollbar>
<el-tabs v-model="activeName" stretch>
<el-tab-pane label="组件属性" name="field">
<el-form v-model="fForm" label-width="90px">
<el-form-item v-if="fForm.name !== 'button' && fForm.name !== 'layout'" label="字段名" prop="field_name">
<el-input v-model="fForm.field_name"></el-input>
<el-form :model="field" label-width="90px">
<el-form-item v-if="field.name !== 'button' && field.name !== 'layout'" label="字段名"
prop="field_name">
<el-input v-model="field.field_name"></el-input>
</el-form-item>
<el-form-item v-if="fForm.name !== 'layout'" label="标题" prop="title">
<el-input v-model="fForm.title"></el-input>
<el-form-item v-if="field.name !== 'layout'" label="标题" prop="title">
<el-input v-model="field.title"></el-input>
</el-form-item>
<el-form-item v-if="fForm.name !== 'button' && fForm.name !== 'layout'" label="默认值" prop="value">
<el-input v-model="fForm.value"></el-input>
<el-form-item v-if="field.name !== 'button' && field.name !== 'layout'" label="默认值"
prop="value">
<el-input v-model="field.value"></el-input>
</el-form-item>
<el-form-item v-if="fForm.span !== undefined" label="栅格列数" prop="span">
<el-slider v-model="fForm.span" :min="1" :max="24"/>
<el-form-item v-if="field.span !== undefined" label="栅格列数" prop="span">
<el-slider v-model="field.span" :min="1" :max="24"/>
</el-form-item>
<template v-for="item in fieldEditors[fForm.name] || []" :key="item.key">
<template v-for="item in fieldEditors[field.name] || []" :key="item.key">
<el-form-item :label="item.label">
<el-input
v-if="item.type === 'input'"
:model-value="getByPath(fForm, item.key)"
@input="val => setByPath(fForm, item.key, val)"
:model-value="getByPath(field, item.key)"
@input="val => setByPath(field, item.key, val)"
clearable
/>
<el-input
v-else-if="item.type === 'number'"
type="number"
:model-value="getByPath(fForm, item.key)"
@input="val => setByPath(fForm, item.key, Number(val))"
:model-value="getByPath(field, item.key)"
@input="val => setByPath(field, item.key, Number(val))"
/>
<el-switch
v-else-if="item.type === 'switch'"
:model-value="getByPath(fForm, item.key)"
@change="val => setByPath(fForm, item.key, val)"
:model-value="getByPath(field, item.key)"
@change="val => setByPath(field, item.key, val)"
/>
<el-color-picker v-else-if="item.type === 'color'" show-alpha
:model-value="getByPath(fForm, item.key)"
@change="val => setByPath(fForm, item.key, val)"/>
:model-value="getByPath(field, item.key)"
@change="val => setByPath(field, item.key, val)"/>
<el-radio-group v-else-if="item.type === 'radio'"
:model-value="getByPath(fForm, item.key)"
@change="val => setByPath(fForm, item.key, val)">
:model-value="getByPath(field, item.key)"
@change="val => setByPath(field, item.key, val)">
<el-radio-button v-for="op in item.options" :key="op.value"
:value="op.value">{{ op.label }}
</el-radio-button>
</el-radio-group>
<el-select v-else-if="item.type === 'select'" :model-value="getByPath(fForm, item.key)"
@change="val => setByPath(fForm, item.key, val)">
<el-select v-else-if="item.type === 'select'" :model-value="getByPath(field, item.key)"
@change="val => setByPath(field, item.key, val)">
<el-option v-for="op in item.options" :key="op.value" :value="op.value"
:label="op.label">
</el-option>
</el-select>
<pi-icon
v-else-if="item.type === 'icon'"
:model-value="getByPath(fForm, item.key)"
@update:modelValue="val => setByPath(fForm, item.key, val)"
:model-value="getByPath(field, item.key)"
@update:modelValue="val => setByPath(field, item.key, val)"
/>
</el-form-item>
</template>
<el-form-item label="是否必填" v-if="fForm.required !== undefined">
<el-switch v-model="fForm.required"/>
<el-form-item label="是否必填" v-if="field.required !== undefined">
<el-switch v-model="field.required"/>
</el-form-item>
<template v-if="fForm.options !== undefined">
<template v-if="field.options !== undefined">
<el-divider>选项</el-divider>
<pi-draggable v-model="fForm.options" item-key="label" :template="{label:'', value:''}">
<pi-draggable v-model="field.options" item-key="label" :template="{label:'', value:''}">
<template #default="scope">
<div style="display:flex;">
<el-input v-model="scope.element.label" placeholder="label"/>
@ -72,9 +74,9 @@
</template>
</pi-draggable>
</template>
<template v-if="fForm.name !== 'button' && fForm.name !== 'layout'">
<template v-if="field.name !== 'button' && field.name !== 'layout'">
<el-divider>正则校验</el-divider>
<pi-draggable v-model="fForm.rules" item-key="regex" :template="{regex:'',message:''}">
<pi-draggable v-model="field.rules" item-key="regex" :template="{regex:'',message:''}">
<template #default="scope">
<el-form-item label="表达式">
<el-input v-model="scope.element.regex"/>
@ -88,44 +90,41 @@
</el-form>
</el-tab-pane>
<el-tab-pane label="表单属性" name="form">
<el-form v-model="cForm" label-width="90px">
<el-form :model="data.config" label-width="90px">
<el-form-item label="表单名称" prop="ref">
<el-input v-model="cForm.ref"></el-input>
<el-input v-model="data.config.ref"></el-input>
</el-form-item>
<el-form-item label="表单模型" prop="model">
<el-input v-model="cForm.model"></el-input>
<el-input v-model="data.config.model"></el-input>
</el-form-item>
<el-form-item label="校验模型" prop="rules">
<el-input v-model="cForm.rules"></el-input>
<el-input v-model="data.config.rules"></el-input>
</el-form-item>
<el-form-item label="表单尺寸" prop="size">
<el-radio-group v-model="cForm.size">
<el-radio-group v-model="data.config.size">
<el-radio-button value="large">较大</el-radio-button>
<el-radio-button value="default">默认</el-radio-button>
<el-radio-button value="small">较小</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="标签对齐" prop="labelPosition">
<el-radio-group v-model="cForm.labelPosition">
<el-radio-group v-model="data.config.labelPosition">
<el-radio-button value="left">左对齐</el-radio-button>
<el-radio-button value="right">右对齐</el-radio-button>
<el-radio-button value="top">顶部对齐</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="标签宽度" prop="labelWidth">
<el-input-number v-model="cForm.labelWidth"/>
<el-input-number v-model="data.config.labelWidth"/>
</el-form-item>
<el-form-item label="禁用表单" prop="disabled">
<el-switch v-model="cForm.disabled"/>
</el-form-item>
<el-form-item prop="act">
<el-button type="primary" @click="emit('save')">暂存</el-button>
<el-switch v-model="data.config.disabled"/>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</el-scrollbar>
</section>
</el-aside>
</template>
<script setup>
@ -133,34 +132,23 @@ import {computed, ref} from "vue";
import piIcon from "@/components/piIcon"
import piDraggable from "@/components/piDraggable"
import {fieldEditors, getByPath, setByPath} from "./config"
import FormBuild from "./formBuild";
const emit = defineEmits(['update:field'])
const props = defineProps({
curField: {type: Object, default: {}},
config: {type: Object, default: {}}
data: {type: FormBuild, default: {}}
})
const emit = defineEmits(['update:modelValue', 'save'])
let activeName = ref("field")
let cForm = computed({
get() {
return props.config
},
set(value) {
this.$emit('update:modelValue', value)
}
})
let fForm = computed({
get() {
return props.curField
},
set(value) {
this.$emit('update:modelValue', value)
}
const field = computed(() => {
return props.data.activeField || {}
})
</script>
<style lang="scss" scoped>
.panel {
width: 350px
width: 350px;
padding: 10px;
border-left: 1px solid var(--el-border-color-light);
}
:deep(.el-scrollbar__bar) {