This commit is contained in:
zhang zhuo 2025-11-27 18:05:15 +08:00
parent 76e925b214
commit d3ba90b84c
6 changed files with 278 additions and 192 deletions

View File

@ -4,60 +4,7 @@
<el-main class="height-100">
<el-form class="height-100" :label-position="config.labelPosition" :label-width="config.labelWidth"
:size="config.size" :disabled="config.disabled">
<draggable :list="fields" :scroll="true" animation="200" item-key="id" class="height-100"
:group="group" @update:list="fields = $event" @add="addComp" @sort="sortComp"
ghostClass="ghostClass">
<template #item="{ element, index }">
<div class="box">
<el-form-item class="item" :class="{'active': curIndex===index}"
@click="clickComp(element,index)" :label="fields[index].title"
:required="fields[index].required">
<component :is="getComponent(fields[index].name)" v-model="fields[index].value"
v-bind="fields[index].props" :style="{width: fields[index].width}">
<template v-if="fields[index].name === 'select'" #default>
<el-option v-for="(op, idx) in fields[index].options"
:key="idx" :label="op.label" :value="op.value"/>
</template>
<template v-else-if="fields[index].name === 'radio'" #default>
<el-radio-button v-if="fields[index].style === 'button'"
v-for="(op, idx) in fields[index].options"
:key="idx" :label="op.label" :value="op.value"/>
<el-radio v-else v-for="(op, id) in fields[index].options"
:key="id" :label="op.label" :value="op.value"/>
</template>
<template v-else-if="fields[index].name === 'checkbox'" #default>
<el-checkbox-button v-if="fields[index].style === 'button'"
v-for="(op, idx) in fields[index].options"
:key="idx" :label="op.label" :value="op.value"/>
<el-checkbox v-else v-for="(op, id) in fields[index].options"
:key="id" :label="op.label" :value="op.value"/>
</template>
<template v-if="fields[index].name === 'upload' && (fields[index].props.listType === 'text' || fields[index].props.listType === 'picture')" #trigger>
<el-button type="primary" icon="pi-icon-upload-file">
{{ fields[index].btnText }}
</el-button>
</template>
<template v-if="fields[index].name === 'upload' && fields[index].props.listType === 'picture-card'" #trigger>
<el-icon><component :is="'el-icon-plus'"/></el-icon>
</template>
<template v-if="fields[index].name === 'upload' && fields[index].tip" #tip>
<div class="el-upload__tip">
{{ fields[index].tip }}
</div>
</template>
</component>
</el-form-item>
<div v-if="curIndex===index" class="tools-box">
<el-icon size="22" class="icon-box remove" @click="delComp(index)">
<component :is="'pi-icon-shan-chu'"/>
</el-icon>
<el-icon size="20" class="icon-box copy ml5" @click="copyComp(index)">
<component :is="'pi-icon-fu-zhi'"/>
</el-icon>
</div>
</div>
</template>
</draggable>
<drag :fields="fields" @change="changeHandle" name="page"/>
</el-form>
</el-main>
</el-scrollbar>
@ -65,13 +12,7 @@
</template>
<script setup>
import draggable from 'vuedraggable'
import {nextTick, ref} from "vue";
import tools from "@/utils/tools"
import {getComponent} from "./config"
import piIcon from "@/components/piIcon"
import PiAsset from "@/components/piAsset"
import drag from "./drag.vue"
defineExpose({clickAddComp})
const props = defineProps({
@ -79,76 +20,15 @@ const props = defineProps({
config: {type: Object, default: {}}
})
const emit = defineEmits(['change'])
const group = {name: 'page', pull: false, put: true}
let curIndex = ref(0)
let curComp = ref({})
//
function sortComp(e) {
curIndex.value = e.newIndex
curComp.value = props.fields[e.newIndex]
emit('change', curComp.value)
}
//
function addComp(e) {
let element = props.fields[e.newIndex]
curIndex.value = e.newIndex
curComp.value = element
emit('change', curComp.value)
}
//
function clickAddComp(e) {
props.fields.push(e)
curIndex.value = props.fields.length-1
curComp.value = e
emit('change', curComp.value)
emit('change', e)
}
//
function clickComp(element, index) {
if (curIndex.value === index) {
return;
}
curIndex.value = index
curComp.value = element
emit('change', curComp.value)
}
//
function delComp(index) {
nextTick(() => {
//
const len = props.fields.length
props.fields.splice(index, 1);
let i = -1;
if (len > 1) {
if (index === 0) {
i = 0;
} else if (index === len - 1) {
i = index - 1
} else {
i = index
}
}
curIndex.value = i
curComp.value = props.fields[i]
emit('change', curComp.value || {})
})
}
function copyComp(index) {
nextTick(() => {
//
const tmp = JSON.parse(JSON.stringify(props.fields[index]))
props.fields.push(tmp)
//
tools.array.zIndexTo(props.fields, props.fields.length - 1, index + 1)
curIndex.value = index + 1
curComp.value = props.fields[index + 1]
emit('change', curComp.value)
})
function changeHandle(e) {
emit('change', e)
}
</script>
@ -161,18 +41,6 @@ function copyComp(index) {
display: none;
}
:deep(.ghostClass) {
.comp {
display: none;
}
.tips {
padding: 10px;
border: 1px dashed var(--el-color-primary);
text-align: center;
color: var(--el-color-primary);
}
}
.height-100 {
height: 100%;
@ -182,50 +50,4 @@ function copyComp(index) {
height: 100%;
}
.active {
border: 1px dashed #787be8 !important;
color: #787be8;
}
.box {
position: relative;
cursor: pointer;
.item {
padding: 12px 10px;
border: 1px;
border-radius: 3px;
position: relative;
}
.tools-box {
position: absolute;
right: 10px;
top: -16px;
display: inline-block;
padding: 5px;
border-radius: 4px;
.icon-box {
cursor: pointer;
}
.icon-box.remove {
color: var(--el-color-danger);
}
.icon-box.copy {
color: var(--el-color-primary);
}
.ml5 {
margin-left: 5px;
}
.disabled {
cursor: no-drop;
color: #c3c3c3;
}
}
}
</style>

View File

@ -186,6 +186,25 @@ export const fieldEditors = {
{key: 'props.clearable', label: '能否清空', type: 'switch'},
{key: 'props.readonly', label: '是否只读', type: 'switch'},
{key: 'props.disabled', label: '是否禁用', type: 'switch'},
],
button: [
{key: 'btnText', label: '按钮文字', type: 'input'},
{
key: 'props.type', label: '按钮类型', type: 'select', options: [
{label: 'primary', value: 'primary'},
{label: 'success', value: 'success'},
{label: 'warning', value: 'warning'},
{label: 'danger', value: 'danger'},
{label: 'info', value: 'info'},
{label: 'default', value: ''},
]
},
{key: 'props.icon', label: '图标', type: 'icon'},
{key: 'props.plain', label: '朴素按钮', type: 'switch'},
{key: 'props.text', label: '文字按钮', type: 'switch'},
{key: 'props.round', label: '圆角按钮', type: 'switch'},
{key: 'props.circle', label: '圆形按钮', type: 'switch'},
{key: 'props.disabled', label: '是否禁用', type: 'switch'},
]
}
// 'year' | 'years' |'month' | 'months' | 'date' | 'dates' | 'datetime' | 'week' | 'datetimerange' | 'daterange' | 'monthrange' | 'yearrange'
@ -208,8 +227,7 @@ export const getComponent = (type) => {
daterange: 'el-date-picker',
time: 'el-time-picker',
timerange: 'el-time-picker',
icon: 'pi-icon',
asset: 'pi-asset'
button: 'el-button'
}
return map[type] || 'div'
}

View File

@ -0,0 +1,187 @@
<template>
<draggable :list="fields" :scroll="true" animation="200" item-key="id" class="height-100"
:group="group" @update:list="fields = $event" @add="addComp" @sort="sortComp"
ghostClass="ghostClass">
<template #item="{ element, index }">
<div class="box">
<el-row v-if="element.name === 'layout'" class="item row" :class="{'active': curIndex===index}"
@click="clickComp(element, index)">
<span class="name">{{ element.field }}</span>
<drag :fields="element.children" @change="changeHandle" :name="element.field"/>
</el-row>
<item v-else class="item" :class="{'active': curIndex===index}" :field="element"
@click="clickComp(element,index)"/>
<div v-if="curIndex===index" class="tools-box">
<el-icon size="22" class="icon-box remove" @click="delComp(index)">
<component :is="'pi-icon-shan-chu'"/>
</el-icon>
<el-icon size="20" class="icon-box copy ml5" @click="copyComp(index)">
<component :is="'pi-icon-fu-zhi'"/>
</el-icon>
</div>
</div>
</template>
</draggable>
</template>
<script setup>
import draggable from 'vuedraggable'
import {nextTick, ref} from "vue"
import tools from "@/utils/tools"
import item from "./item.vue"
import drag from "./drag.vue"
console.log(drag)
const props = defineProps({
fields: {type: Array, default: []},
name: {type: String, default: 'page'}
})
const emit = defineEmits(['change'])
const group = {name: props.name, pull: false, put: true}
let curIndex = ref(0)
let curComp = ref({})
//
function sortComp(e) {
curIndex.value = e.newIndex
curComp.value = props.fields[e.newIndex]
emit('change', curComp.value)
}
//
function addComp(e) {
let element = props.fields[e.newIndex]
curIndex.value = e.newIndex
curComp.value = element
emit('change', curComp.value)
}
//
function clickComp(element, index) {
if (curIndex.value === index) {
return;
}
curIndex.value = index
curComp.value = element
emit('change', curComp.value)
}
//
function delComp(index) {
nextTick(() => {
//
const len = props.fields.length
props.fields.splice(index, 1);
let i = -1;
if (len > 1) {
if (index === 0) {
i = 0;
} else if (index === len - 1) {
i = index - 1
} else {
i = index
}
}
curIndex.value = i
curComp.value = props.fields[i]
emit('change', curComp.value || {})
})
}
function copyComp(index) {
nextTick(() => {
//
const tmp = JSON.parse(JSON.stringify(props.fields[index]))
props.fields.push(tmp)
//
tools.array.zIndexTo(props.fields, props.fields.length - 1, index + 1)
curIndex.value = index + 1
curComp.value = props.fields[index + 1]
emit('change', curComp.value)
})
}
function changeHandle(e) {
emit('change', e)
}
</script>
<style lang="scss" scoped>
:deep(.ghostClass) {
.comp {
display: none;
}
.tips {
padding: 10px;
border: 1px dashed var(--el-color-primary);
text-align: center;
color: var(--el-color-primary);
}
}
.active {
border: 1px dashed #787be8 !important;
color: #787be8;
}
.box {
position: relative;
cursor: pointer;
.item {
padding: 12px 10px;
border: 1px;
border-radius: 3px;
position: relative;
}
.row {
min-height: 80px;
border: 1px dashed #c1c1c1;
margin-bottom: 18px;
.name {
position: absolute;
top: 0;
left: 0;
font-size: 12px;
color: #bbb;
display: inline-block;
padding: 0 6px;
}
}
.tools-box {
position: absolute;
right: 10px;
top: -16px;
display: inline-block;
padding: 5px;
border-radius: 4px;
.icon-box {
cursor: pointer;
}
.icon-box.remove {
color: var(--el-color-danger);
}
.icon-box.copy {
color: var(--el-color-primary);
}
.ml5 {
margin-left: 5px;
}
.disabled {
cursor: no-drop;
color: #c3c3c3;
}
}
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<el-form-item :label="field.title" :required="field.required">
<component :is="getComponent(field.name)" v-model="field.value" v-bind="field.props"
:style="{width: field.width}">
<template v-if="field.name === 'select'" #default>
<el-option v-for="(op, idx) in field.options" :key="idx" :label="op.label" :value="op.value"/>
</template>
<template v-else-if="field.name === 'radio'" #default>
<el-radio-button v-if="field.style === 'button'" v-for="(op, idx) in field.options" :key="idx"
:label="op.label" :value="op.value"/>
<el-radio v-else v-for="(op, id) in field.options" :key="id" :label="op.label" :value="op.value"/>
</template>
<template v-else-if="field.name === 'checkbox'" #default>
<el-checkbox-button v-if="field.style === 'button'" v-for="(op, idx) in field.options" :key="idx"
:label="op.label" :value="op.value"/>
<el-checkbox v-else v-for="(op, id) in field.options" :key="id" :label="op.label" :value="op.value"/>
</template>
<template
v-if="field.name === 'upload' && (field.props.listType === 'text' || field.props.listType === 'picture')"
#trigger>
<el-button type="primary" icon="pi-icon-upload-file">
{{ field.btnText }}
</el-button>
</template>
<template
v-if="field.name === 'upload' && field.props.listType === 'picture-card'" #trigger>
<el-icon>
<component :is="'el-icon-plus'"/>
</el-icon>
</template>
<template v-if="field.name === 'upload' && field.tip" #tip>
<div class="el-upload__tip">
{{ field.tip }}
</div>
</template>
<template v-if="field.name === 'button' && field.btnText"
#default>
{{ field.btnText }}
</template>
</component>
</el-form-item>
</template>
<script setup>
import {getComponent} from "./config"
const props = defineProps({
field: {type: Object, default: {}}
})
</script>
<style scoped>
</style>

View File

@ -266,19 +266,23 @@ const choiceComps = [{
btnText: "点击上传"
}]
const layoutComps = [{
id: 'l1',
id: 'row',
title: "行容器",
icon: "pi-icon-row-layout",
name: "layout",
props: {},
rules: []
props: {
gutter: '15'
},
children: []
}, {
id: 'l2',
title: "按钮",
icon: "pi-icon-button",
name: "button",
props: {},
rules: []
props: {
type: 'primary'
},
btnText: "按钮文字"
}];
let num = ref(100)

View File

@ -4,13 +4,13 @@
<el-tabs v-model="activeName" stretch>
<el-tab-pane label="组件属性" name="field">
<el-form v-model="fForm" label-width="90px">
<el-form-item label="字段名" prop="field">
<el-form-item v-if="fForm.name !== 'button'" label="字段名" prop="field">
<el-input v-model="fForm.field"></el-input>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="fForm.title"></el-input>
</el-form-item>
<el-form-item label="默认值" prop="value">
<el-form-item v-if="fForm.name !== 'button'" label="默认值" prop="value">
<el-input v-model="fForm.value"></el-input>
</el-form-item>
<template v-for="item in fieldEditors[fForm.name] || []" :key="item.key">
@ -19,6 +19,7 @@
v-if="item.type === 'input'"
:model-value="getByPath(fForm, item.key)"
@input="val => setByPath(fForm, item.key, val)"
clearable
/>
<el-input
v-else-if="item.type === 'number'"