效果

image.png
組件使用
<sd-form ref="formref" :config="config" size="mini" border v-model="formData">
<!-- 具名插槽 -->
<template #testSlot>
<el-input v-model="formData.slotName" placeholder="這是自定義表單"></el-input>
</template>
<el-button type="primary" @click="formSave">保 存</el-button>
</sd-form>
數(shù)據(jù)校驗(yàn)
// 測試保存
formSave() {
this.$refs.formref.validate((valid) => {
if(valid) {
console.log(this.formData)
}
})
}
參數(shù)結(jié)構(gòu)
// config為表單數(shù)據(jù)
// formData為綁定數(shù)據(jù)結(jié)構(gòu)
created() {
this.config = {
labelWidth: '120px', // label寬度
labelPosition: 'right', // label對(duì)齊方式
size: 'medium', // 表單尺寸
formItems: [ // 表單元素
{
label: "自定義表單", //表單名稱
name: "slotName", // formData綁定的key
span: 8, // el-col的span
slotName: 'testSlot', // 具名插槽
rules: [ // 校驗(yàn)規(guī)則
{required: true, message: "Please input Activity name", trigger: "blur"}
],
},
{
label: "輸入框",
name: "name",
component: "input", // 表單類型
span: 8,
options: {
maxlength: "20",
placeholder: "Activity name",
},
rules: [
{required: true, message: "Please input Activity name", trigger: "blur"}
],
requiredHandle: "$.required==true", // 是否需要校驗(yàn)
},
{
label: "柵格(12/24)",
name: "name2",
component: "input",
span: 8,
options: {
placeholder: "span: 12",
}
},
{
label: "柵格(12/24)",
name: "name3",
component: "input",
span: 24,
options: {
placeholder: "span: 12",
}
},
{
label: "級(jí)聯(lián)選擇器",
name: "cascader",
component: "cascader",
span: 12,
options: {
items:[
{
label: "Guide",
value: "guide",
children: [
{
label: "Disciplines",
value: "disciplines"
},
{
label: "Consistency",
value: "consistency"
},
]
},
{
label: "Resource",
value: "resource",
children: [
{
label: "Axure Components",
value: "axure"
},
{
label: "Sketch Templates",
value: "sketch"
},
{
label: "Design Documentation",
value: "docs"
}
]
},
{
label: "Component",
value: "component"
},
]
}
},
{
label: "多選框",
name: "checkbox",
component: "checkbox",
span: 12,
tips: "多選框配置加上 name 表示擁有嵌套關(guān)系。否則將值“平鋪”在form對(duì)象",
options: { // 表單具體屬性
items:[
{
label: "選項(xiàng)1",
name: "option1"
},
{
label: "選項(xiàng)2",
name: "option2"
}
]
},
hideHandle: "$.required==true"
},
{
label: "多選框組",
name: "checkboxGroup",
component: "checkboxGroup",
span: 24,
options: {
items:[
{
label: "選項(xiàng)1",
value: "option1"
},
{
label: "選項(xiàng)2",
value: "option2"
}
]
},
hideHandle: "$.required==true" // 動(dòng)態(tài)顯示隱藏此表單
},
{
label: "單選",
name: "radio",
component: "radio",
options: {
items:[
{
label: "選項(xiàng)1",
value: "1"
},
{
label: "選項(xiàng)2",
value: "2"
}
]
},
hideHandle: "$.required==true"
},
{
label: "開關(guān)",
name: "required",
span: 12,
message: "演示如何使用表達(dá)式動(dòng)態(tài)顯隱和必填,試試打開和關(guān)閉開關(guān)",
component: "switch",
},
{
label: "日期/時(shí)間",
name: "date",
span: 12,
component: "date",
options: {
type: "datetime",
valueFormat: "yyyy-MM-dd HH:mm:ss",
},
rules: [
{required: true, message: "Please input Data", trigger: "change"}
],
},
{
label: "數(shù)值",
name: "number",
component: "number",
},
{
label: "顏色",
name: "color",
component: "color",
},
{
label: "評(píng)分",
name: "rate",
component: "rate",
}
]
}
this.formData = {
slotName: '自定義表單',
name: '',
name2: '',
name3: '',
cascader: '',
checkbox: {},
checkboxGroup: [],
radio: '1',
required: false,
date: '',
slider: 8,
number: 0,
color: '',
rate: 0
}
}
組件封裝
<template>
<el-form class="sd-form" :class="border && 'sd-form-border'" ref="form" :model="form" :size="size" :label-width="config.labelWidth" :label-position="config.labelPosition" v-loading="loading" element-loading-text="Loading...">
<el-row>
<template v-for="(item, index) in config.formItems">
<el-col :span="item.span || 24" v-if="!hideHandle(item)" :key="index">
<sd-title v-if="item.component=='title'" :title="item.label"></sd-title>
<el-form-item v-else :prop="item.name" :rules="rulesHandle(item)">
<template #label>
<span ref="refFormItem" class="refFormItem">{{item.label}}</span>
<el-tooltip v-if="item.tips" :content="item.tips">
<el-icon><el-icon-question-filled /></el-icon>
</el-tooltip>
</template>
<template v-if="item.slotName">
<slot :name="item.slotName"></slot>
</template>
<!-- input -->
<template v-else-if="item.component=='input'" >
<el-input v-model="form[item.name]" :placeholder="item.options.placeholder" clearable :maxlength="item.options.maxlength" show-word-limit></el-input>
</template>
<!-- checkbox -->
<template v-else-if="item.component=='checkbox'" >
<template v-if="item.name" >
<el-checkbox v-model="form[item.name][_item.name]" :label="_item.label" v-for="(_item, _index) in item.options.items" :key="_index"></el-checkbox>
</template>
<template v-else >
<el-checkbox v-model="form[_item.name]" :label="_item.label" v-for="(_item, _index) in item.options.items" :key="_index"></el-checkbox>
</template>
</template>
<!-- checkboxGroup -->
<template v-else-if="item.component=='checkboxGroup'" >
<el-checkbox-group v-model="form[item.name]">
<el-checkbox v-for="_item in item.options.items" :key="_item.value" :label="_item.value">{{_item.label}}</el-checkbox>
</el-checkbox-group>
</template>
<!-- switch -->
<template v-else-if="item.component=='switch'" >
<el-switch v-model="form[item.name]" />
</template>
<!-- select -->
<template v-else-if="item.component=='select'" >
<el-select v-model="form[item.name]" :multiple="item.options.multiple" :placeholder="item.options.placeholder" clearable filterable style="width: 100%;">
<el-option v-for="option in item.options.items" :key="option.value" :label="option.label" :value="option.value"></el-option>
</el-select>
</template>
<!-- cascader -->
<template v-else-if="item.component=='cascader'" >
<el-cascader v-model="form[item.name]" :options="item.options.items" clearable></el-cascader>
</template>
<!-- date -->
<template v-else-if="item.component=='date'" >
<el-date-picker v-model="form[item.name]" :type="item.options.type" :shortcuts="item.options.shortcuts" :default-time="item.options.defaultTime" :value-format="item.options.valueFormat" :placeholder="item.options.placeholder || '請(qǐng)選擇'"></el-date-picker>
</template>
<!-- number -->
<template v-else-if="item.component=='number'" >
<el-input-number v-model="form[item.name]" controls-position="right"></el-input-number>
</template>
<!-- radio -->
<template v-else-if="item.component=='radio'" >
<el-radio-group v-model="form[item.name]">
<el-radio v-for="_item in item.options.items" :key="_item.value" :label="_item.value">{{_item.label}}</el-radio>
</el-radio-group>
</template>
<!-- color -->
<template v-else-if="item.component=='color'" >
<el-color-picker v-model="form[item.name]" />
</template>
<!-- rate -->
<template v-else-if="item.component=='rate'" >
<el-rate style="margin-top: 6px;" v-model="form[item.name]"></el-rate>
</template>
<!-- slider -->
<template v-else-if="item.component=='slider'" >
<el-slider v-model="form[item.name]" :marks="item.options.marks"></el-slider>
</template>
<!-- noComponent -->
<template v-else>
<el-tag type="danger">[{{item.component}}] Component not found</el-tag>
</template>
<div v-if="item.message" class="el-form-item-msg">{{item.message}}</div>
</el-form-item>
</el-col>
</template>
<el-col :span="24">
<el-form-item>
<slot>
<el-button type="primary" @click="submit">提交</el-button>
</slot>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script>
import http from "@/utils/request"
import SdTitle from './components/scTitle.vue'
export default {
props: {
modelValue: { type: Object, default: () => {} },
config: { type: Object, default: () => {} },
loading: { type: Boolean, default: false },
size: { type: String, default: 'medium' },
border: { type: Boolean, default: false },
},
data() {
return {
form: {}
}
},
components: {
SdTitle
},
// 給modelValue綁定事件
model: {
prop: 'modelValue',
event: 'getValue'
},
watch:{
// 監(jiān)聽modelValue變化復(fù)制給當(dāng)前綁定元素實(shí)現(xiàn)綁定
modelValue: {
immediate: true,
handler(val) {
this.form = val;
}
},
form:{
handler(val){
this.$emit("getValue", val)
},
deep: true,
immediate: true
}
},
computed: {
hasConfig(){
return Object.keys(this.config).length>0
},
hasValue(){
return Object.keys(this.modelValue).length>0
}
},
created() {
},
mounted() {
this.getHeight()
},
methods: {
// 初始化label高度
getHeight() {
this.$nextTick(() => {
if(this.$refs.refFormItem) {
this.$refs.refFormItem.forEach((item, index) => {
const height = item.parentNode.parentNode.offsetHeight
document.querySelectorAll('.refFormItem')[index].style.height = height + 'px'
// this.$forceUpdate()
})
}
})
},
//處理遠(yuǎn)程選項(xiàng)數(shù)據(jù)
getData() {
var remoteData = []
this.config.formItems.forEach((item) => {
if(item.options && item.options.remote){
var req = http.get(item.options.remote.api, item.options.remote.data).then(res=>{
item.options.items = res.data
})
remoteData.push(req)
}
})
},
//合并深結(jié)構(gòu)對(duì)象
deepMerge(obj1, obj2) {
let key;
for (key in obj2) {
obj1[key] = obj1[key] && obj1[key].toString() === "[object Object]" && (obj2[key] && obj2[key].toString() === "[object Object]") ? this.deepMerge(obj1[key], obj2[key]) : (obj1[key] = obj2[key])
}
return obj1
//return JSON.parse(JSON.stringify(obj1))
},
//處理動(dòng)態(tài)隱藏
hideHandle(item){
if(item.hideHandle){
const exp = eval(item.hideHandle.replace(/\$/g,"this.form"))
return exp
}
return false
},
//處理動(dòng)態(tài)必填
rulesHandle(item){
if(item.requiredHandle){
const exp = eval(item.requiredHandle.replace(/\$/g,"this.form"))
var requiredRule = item.rules.find(t => 'required' in t)
requiredRule.required = exp
}
return item.rules
},
//數(shù)據(jù)驗(yàn)證
validate(valid, obj){
return this.$refs.form.validate(valid, obj)
},
scrollToField(prop){
return this.$refs.form.scrollToField(prop)
},
resetFields(){
return this.$refs.form.resetFields()
},
//提交
submit(){
this.$emit("submit", this.form)
}
}
}
</script>
<style lang="scss">
.sd-form {
.el-row {
display: flex;
flex-wrap: wrap;
.el-col {
padding: 5px !important;
box-sizing: border-box;
}
}
.el-form-item {
margin: 0;
display: flex;
align-items: center;
width: 100% !important;
height: 100%;
.el-form-item__label {
background: #EAF2FF;
display: flex;
justify-content: flex-end;
align-items: center;
span {
display: flex;
align-items: center;
justify-content: flex-end;
}
}
.el-form-item__content {
margin: 0 !important;
padding: 0 5px;
box-sizing: border-box;
flex: 1;
}
}
}
.sd-form-border {
.el-row {
border: 1px solid #e5e5e5;
.el-col {
border-right: 1px solid #e5e5e5;
border-bottom: 1px solid #e5e5e5;
}
}
}
</style>
npm使用
npm i web-bing@0.1.4
// main.js
import componentsBing from 'web-bing'
import 'web-bing/dist/web-bing.css'
Vue.use(componentsBing)
組件使用
<template>
<div id="app">
<sd-form ref="formref" :config="config" size="mini" border v-model="formData">
<!-- 具名插槽 -->
<template #testSlot>
<el-input v-model="formData.slotName" placeholder="這是自定義表單"></el-input>
</template>
<!-- 具名插槽 -->
<template #testSlot1>
<el-input v-model="formData.slotName1" placeholder="這是自定義表單1"></el-input>
</template>
<el-button type="primary" @click="formSave">保 存</el-button>
</sd-form>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
config: {},
formData: {}
}
},
// config為表單數(shù)據(jù)
// formData為綁定數(shù)據(jù)結(jié)構(gòu)
created() {
this.config = {
labelWidth: '120px', // label寬度
labelPosition: 'right', // label對(duì)齊方式
size: 'medium', // 表單尺寸
formItems: [ // 表單元素
{
label: "自定義表單", //表單名稱
name: "slotName", // formData綁定的key
span: 8, // el-col的span
slotName: 'testSlot', // 具名插槽
rules: [ // 校驗(yàn)規(guī)則
{required: true, message: "Please input Activity name", trigger: "blur"}
],
},
{
label: "輸入框",
name: "name",
component: "input", // 表單類型
span: 8,
options: {
maxlength: "20",
placeholder: "Activity name",
},
rules: [
{required: true, message: "Please input Activity name", trigger: "blur"}
],
requiredHandle: "$.required==true", // 是否需要校驗(yàn)
},
{
label: "柵格(12/24)",
name: "name2",
component: "input",
span: 8,
options: {
placeholder: "span: 12",
}
},
{
label: "柵格(12/24)",
name: "name3",
component: "input",
span: 24,
options: {
placeholder: "span: 12",
}
},
{
label: "自定義表單1", //表單名稱
name: "slotName1", // formData綁定的key
span: 12, // el-col的span
slotName: 'testSlot1', // 具名插槽
rules: [ // 校驗(yàn)規(guī)則
{required: true, message: "Please input Activity name", trigger: "blur"}
],
},
{
label: "級(jí)聯(lián)選擇器",
name: "cascader",
component: "cascader",
span: 12,
options: {
items:[
{
label: "Guide",
value: "guide",
children: [
{
label: "Disciplines",
value: "disciplines"
},
{
label: "Consistency",
value: "consistency"
},
]
},
{
label: "Resource",
value: "resource",
children: [
{
label: "Axure Components",
value: "axure"
},
{
label: "Sketch Templates",
value: "sketch"
},
{
label: "Design Documentation",
value: "docs"
}
]
},
{
label: "Component",
value: "component"
},
]
}
},
{
label: "多選框",
name: "checkbox",
component: "checkbox",
span: 12,
tips: "多選框配置加上 name 表示擁有嵌套關(guān)系。否則將值“平鋪”在form對(duì)象",
options: { // 表單具體屬性
items:[
{
label: "選項(xiàng)1",
name: "option1"
},
{
label: "選項(xiàng)2",
name: "option2"
}
]
},
hideHandle: "$.required==true"
},
{
label: "多選框組",
name: "checkboxGroup",
component: "checkboxGroup",
span: 24,
options: {
items:[
{
label: "選項(xiàng)1",
value: "option1"
},
{
label: "選項(xiàng)2",
value: "option2"
}
]
},
hideHandle: "$.required==true" // 動(dòng)態(tài)顯示隱藏此表單
},
{
label: "單選",
name: "radio",
component: "radio",
options: {
items:[
{
label: "選項(xiàng)1",
value: "1"
},
{
label: "選項(xiàng)2",
value: "2"
}
]
},
hideHandle: "$.required==true"
},
{
label: "開關(guān)",
name: "required",
span: 12,
message: "演示如何使用表達(dá)式動(dòng)態(tài)顯隱和必填,試試打開和關(guān)閉開關(guān)",
component: "switch",
},
{
label: "日期/時(shí)間",
name: "date",
span: 12,
component: "date",
options: {
type: "datetime",
valueFormat: "yyyy-MM-dd HH:mm:ss",
},
rules: [
{required: true, message: "Please input Data", trigger: "change"}
],
},
{
label: "數(shù)值",
name: "number",
component: "number",
},
{
label: "顏色",
name: "color",
component: "color",
},
{
label: "評(píng)分",
name: "rate",
component: "rate",
}
]
}
this.formData = {
slotName: '自定義表單',
slotName1: '123',
name: '',
name2: '',
name3: '',
cascader: '',
checkbox: {},
checkboxGroup: [],
radio: '1',
required: false,
date: '',
slider: 8,
number: 0,
color: '',
rate: 0
}
},
methods: {
formSave() {
}
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>