基于element-plus定义表单配置化


前言

网站基本离不开表单提交,结合之前保险项目及各种检验平台,都是需要填写大量数据,均涉及多个不同表单提交。

所以表单提交配置化是很有必要的 – 基于面向对象

也有很多业务需求需要表格行编辑,可参考 基于element-plus定义表格行内编辑配置化

本文以vue3+element-plus为例,由于时间有限,部分配置可结合项目实际扩展

先展示实际效果图
在这里插入图片描述


一、配置化的前提

表单内一般包含

  • 需要什么字段
  • 字段填写形式(输入框、下拉框、多级联动、开关、日期、附件上传、富文本编辑器等等)
  • 字段校验(是否必填、存在特殊校验,例如邮箱格式等)
  • 字段排序及各种属性配置

二、配置的相关组件

以下文件均建立在 @/components/form-configuration目录下

1、新建form.vue组件

用于接收配置信息

<template>
  <el-form 
    class="customForm"
    :model="fields"
    :rules="rules"
    ref="formData"
    :size="size"
    :disabled="disabled"
    :validate-on-rule-change="validateOnRuleChange"
    :hide-required-asterisk="hideAsterisk"
    :label-position="labelPosition"
    :label-width="labelWidth">
    <el-row :gutter="gutter">
      <el-col v-for="(item, index) in formData" v-show="item.show" :key="item.field + '-' + index" :span="item.colSize">
        <el-form-item :prop="item.field" :label="item.title" :class="item.class">
          <component v-model:content="fields[item.field]" v-model="fields[item.field]" :property="{...item.property, name: item.field}" :is="item.type" @fieldChange="(val) => change(val, item.field)" />
        </el-form-item>
      </el-col>
    </el-row>
  </el-form>
</template>
<script>
import Input from '@/components/form-configuration/input.vue'
import Select from '@/components/form-configuration/select.vue'
import Vhtml from '@/components/form-configuration/v-html.vue'
import Upload from '@/components/form-configuration/upload.vue'
import Switch from '@/components/form-configuration/switch.vue'
import Radio from '@/components/form-configuration/radio.vue'
import Checkbox from '@/components/form-configuration/checkbox.vue'
import Date from '@/components/form-configuration/date.vue'
import TimePicker from '@/components/form-configuration/time-picker.vue'
import Cascader from '@/components/form-configuration/cascader.vue'
import UEditor from '@/components/form-configuration/ueditor/index.vue'
import KindEditor from '@/components/form-configuration/kind-editor/index.vue'
import { defineComponent, reactive, ref, watch, computed, nextTick } from 'vue'


export default {
  components: {
    Input,
    Select,
    Vhtml,
    Upload,
    Switch,
    Radio,
    Checkbox,
    Date,
    TimePicker,
    Cascader,
  },
  props: {
    disabled: {
      type: Boolean,
      default: false // 
    },
    size: {
      type: String,
      default: 'default' // "", "default", "small", "large"
    },
    gutter: {
      type: Number,
      default: 20
    },
    formData: {
      type: Object,
      default() {
        return {}
      }
    },
    hideAsterisk: {
      type: Boolean,
      default: false
    },
    validateOnRuleChange: {
      type: Boolean,
      default: false
    },
    fields: {}, // form相关字段值
    rules: {}, // form校验
    labelWidth: {
      type: String,
      default: '180px'
    },
    labelPosition: {
      type: String,
      default: 'right', // left/right/top
    }
  },
  setup(props, { emit }) {
    const change = (val, field) => {
      emit('change', {
        val, field
      }) // 触发
    }
    return {
      change
    }
  }
}
</script>
<style lang="less" scoped>
</style>

2、新建input.vue组件

<template>
  <el-input v-model="input"
    :type="fieldProperty.type"
    :clearable="fieldProperty.clearable"
    :placeholder="fieldProperty.placeholder" 
    :maxlength="fieldProperty.maxlength"
    :readonly="fieldProperty.readonly"
    @blur="blur"
    @change="change"
    autocomplete="on"
    :name="fieldProperty.name"
    :disabled="fieldProperty.disabled" />
</template>
<script lang="ts">
import { computed } from 'vue'
export default {
  props: {
    modelValue: [String, Number], // 组件绑定值
    content: [String, Number], // 组件绑定值
    property: {
      type: Object,
      default() {
        return {
          
        }
      }
    }
  },
  setup(props, { emit }) {
    const fieldProperty = computed(() => {
      return {
        placeholder: '请输入', // 提示语
        maxlength: -1, // 可输入最大长度
        readonly: false, // 是否只读
        disabled: false, // 是否可输入
        clearable: false, // 是否可清空输入框
        type: 'text', // 输入框类型 input | textarea | password | text
        ...props.property
      }
    })
    const input = computed({
      get() {
        return props.modelValue
      },
      set(val) {
        emit('update:modelValue', val)
      }
    })
    const change = (val: any) => {
      emit('fieldChange', val) // 触发
    }
    const blur = () => {
      emit('fieldBlur', input.value) // 触发
    }
    return {
      input,
      change,
      blur,
      fieldProperty
    }
  },
}
</script>
<style lang="less" scoped></style>

3、新建select.vue组件

<template>
  <el-select v-model="val"
    ref="selectRef"
    :multiple="fieldProperty.multiple"
    :filterable="fieldProperty.filterable"
    :clearable="fieldProperty.clearable"
    :disabled="fieldProperty.disabled"
    @change="change"
    :placeholder="fieldProperty.placeholder">
    <el-option v-for="(item, index) in fieldProperty.data"
      :key="item[fieldProperty.value] + item[fieldProperty.label] + '-' + index"
      :label="item[fieldProperty.label]" :value="item[fieldProperty.value]">
      <!-- {{ item[fieldProperty.label] }} -->
    </el-option>
  </el-select>
</template>
<script lang="ts">
import { computed } from 'vue'
export default {
  props: {
    modelValue : [String, Number, Array],
    property: {
      type: Object,
      default() {
        return {
          
        }
      }
    }
  },
  setup(props, { emit }) {
    const fieldProperty = computed(() => {
      return {
        placeholder: '请选择', // 提示语
        data: [], // 下拉可选数据
        label: 'label', // 选择框的文案字段
        value: 'value', // 选择框的值字段
        multiple: false, // 是否多选
        filterable: true, // 是否可搜索
        clearable: false, // 是否可以清空选项
        disabled: false, // 是否禁用
        ...props.property
      }
    })
    const val = computed({
      get() {
        return props.modelValue 
      },
      set(val) {
        emit('update:modelValue', val) // 触发
      }
    })
    const change = (val: any) => {
      emit('fieldChange', val) // 触发
    }
    return {
      val,
      fieldProperty,
      change
    } 
  }
}
</script>
<style lang="less" scoped>
</style>

4、新建v-html.vue组件

<template>
  <div v-html="val" class="pre-line" style="line-height: 26px"></div>
</template>
<script>
export default {
  props: {
    modelValue: [String, Number]
  },
  model: {
    prop: 'modelValue', // 指定 v-model 要绑定的参数叫什么名字,来自于 props 中定义的参数
    event: 'change' // 指定要触发的事件名字,将被用于 $emit
  },
  data() {
    return {
    }
  },
  computed: {
    val: {
      // 这里的计算属性使用了 getter、setter,可以简化代码
      // 可参见链接 https://cn.vuejs.org/v2/guide/computed.html#%E8%AE%A1%E7%AE%97%E5%B1%9E%E6%80%A7%E7%9A%84-setter
      get() {
        return this.modelValue
      },
      set(val) {
        this.$emit('change', val) // 触发
      }
    }
  },
  methods: {}
}
</script>
<style lang="less" scoped>
</style>

5、新建upload.vue组件

<template>
  <el-upload
    ref="upload"
    class="upload-demo"
    :file-list="val"
    :multiple="fieldProperty.multiple"
    :limit="fieldProperty.limit"
    :disabled="fieldProperty.disabled"
    action="#"
    :auto-upload="false"
    :on-exceed="handleExceed"
    :on-preview="handlePreview"
    :on-change="handleChange"
    :accept="fieldProperty.accept"
    :on-remove="handleRemove">
    <el-button type="primary" :loading="fieldProperty.loading">{{ fieldProperty.btnName }}</el-button>
    <div class="el-upload__tip">{{ fieldProperty.tips}}</div>
  </el-upload>
</template>
<script>
import { watch, reactive, computed, ref } from 'vue'
import { ElMessage, genFileId } from 'element-plus'
import { DownloadFileAPI } from "@/server/Base"
import { formDataDownFile } from "@/utils"
export default {
  props: {
    modelValue: [String, Number, Array], // 组件绑定值
    property: {
      type: Object,
      default() {
        return {}
      }
    }
  },
  setup(props, { emit }) {
    const upload = ref()
    const fieldProperty = computed(() => {
      return {
        multiple: false, // 是否可多选
        limit: 10, // 依次最大可上传
        btnName: '点击上传', // 出发上传按钮文案
        tips: '', // 上传文案提示
        accept: '', // 文件上传类型
        disabled: false,
        maximum: 20, // 默认最大 20M ; 1M = 1024b * 1024KB
        loading: false,
        ...props.property
      }
    })
    const val = computed({
      get() {
        return props.modelValue 
      },
      set(val) {
        emit('update:modelValue', val) // 触发
      }
    })
    const handleChange = (file, fileList) => {
      const fileSuffix = file.name.substring(file.name.lastIndexOf(".") + 1).toLocaleLowerCase();
      const whiteList = fieldProperty.value.accept.toLocaleLowerCase().split(',')
      if (whiteList.indexOf('.'+fileSuffix) === -1 && fieldProperty.value.accept) {
        val.value = fileList.filter((fie) => fie.uid !== file.uid)
        ElMessage.error('The file type can only be ' + fieldProperty.value.accept)
        return false;
      }
      if (file.size > fieldProperty.value.maximum * 1024 * 1024) {
        val.value = fileList.filter((fie) => fie.uid !== file.uid)
        ElMessage.error('The maximum upload size is ' + fieldProperty.value.maximum + 'M')
        return
      }
      val.value = fileList
      emit('fieldChange', { file, index: fileList.length - 1, fileList}) // 触发当前上传附件
    }
    const handleRemove = (file, fileList) => {
      val.value = fileList
      emit('fieldChange', fileList) // 触发
    }
    const handlePreview = (file) => {
      const params = {
        fullName: file.url || file.filePath?.FullName,
        oriFileName: file.name || file.filePath?.OriginalName
      }
      const action = DownloadFileAPI(params)
      formDataDownFile(action, params)
    }
    const handleExceed = (files) => {
      console.log('100000', fieldProperty.multiple)
      if (!fieldProperty.multiple) {
        upload.value.clearFiles()
        const file = files[0]
        file.uid = genFileId()
        upload.value.handleStart(file) 
      }
    }
    return {
      handleChange,
      handleRemove,
      handleExceed,
      handlePreview,
      fieldProperty,
      upload,
      val
    }
  }
}
</script>
<style lang="less" scoped>
</style>

6、新建switch.vue组件

<template>
  <el-switch v-model="val"
    :disabled="fieldProperty.disabled"
    :active-color="fieldProperty.activeColor"
    :inactive-color="fieldProperty.inactiveColor">
  </el-switch>
</template>
<script>
import { computed, reactive} from 'vue'
export default {
  name: 'SmSwitch',
  props: {
    modelvalue: [Boolean],
    property: {
      type: Object,
      default() {
        return {}
      }
    }
  },
  setup(props, { emit }) {
    const fieldProperty = reactive({
      activeColor: '#13ce66',
      inactiveColor: '#DCDFE6',
      disabled: false,
      ...props.property
    })
    const val = computed({
      get() {
        return props.modelvalue
      },
      set(val) {
        emit('update:modelvalue', val) // 触发
      }
    })
    return {
      val,
      fieldProperty
    }
  }
}
</script>
<style lang="less" scoped></style>

7、新建radio.vue组件

<template>
  <el-radio-group v-model="val" class="ml-4" :disabled="fieldProperty.disabled" :size="fieldProperty.size">
    <el-radio v-for="(item, index) in fieldProperty.data" :key="item + index + RandomNumber()" :label="item[fieldProperty.value]">{{ item[fieldProperty.label] }}</el-radio>
  </el-radio-group>
</template>
<script lang="ts">
import { computed, reactive} from 'vue'
import { RandomNumber } from '@/utils'
export default {
  name: 'Radio',
  props: {
    modelvalue: [Boolean],
    property: {
      type: Object,
      default() {
        return {}
      }
    }
  },
  setup(props, { emit }) {
    const fieldProperty = reactive({
      readonly: false,
      disabled: false,
      label: 'label', // 选择框的文案字段
      value: 'value', // 选择框的值字段
      data: [],
      size: 'default', // 'large' | 'default' | 'small'
      ...props.property
    })
    const val = computed({
      get() {
        return props.modelvalue
      },
      set(val) {
        emit('update:modelvalue', val) // 触发
      }
    })
    return {
      val,
      fieldProperty,
      RandomNumber
    }
  }
}
</script>
<style lang="less" scoped></style>

8、新建checkbox.vue组件

<template>
  <el-checkbox-group
    v-if="fieldProperty.data.length"
    v-model="val"
    :disabled="fieldProperty.disabled"
    @change="handleCheckedChange"
  >
    <el-checkbox
      v-for="(item, index) in fieldProperty.data"
      :key="item.label + index + RandomNumber()"
      :disabled="item.disabled"
      :label="item.value"
    >
      {{ item.label }}
    </el-checkbox>
  </el-checkbox-group>
  <el-checkbox
    v-else
    v-model="val"
    :disabled="fieldProperty.disabled"
    :label="fieldProperty.label"
  />
</template>
<script lang="ts">
import { computed, reactive } from "vue";
import { RandomNumber } from "@/utils";
import type { CheckboxData } from "@/interface/form";
export default {
  name: "Radio",
  props: {
    modelvalue: [Array, Boolean],
    property: {
      type: Object,
      default() {
        return {};
      },
    },
  },
  setup(props, { emit }) {
    const fieldProperty = computed(() => {
      return {
        readonly: false,
        disabled: false,
        label: "label", // 单选框的文案字段
        data: [] as Array<CheckboxData>,
        size: "default", // 'large' | 'default' | 'small'
        ...props.property,
      };
    });
    const handleCheckedChange = () => {};
    const val = computed({
      get() {
        return props.modelvalue;
      },
      set(val) {
        emit("update:modelvalue", val); // 触发
      },
    });
    return {
      val,
      RandomNumber,
      handleCheckedChange,
      fieldProperty,
    };
  },
};
</script>
<style lang="less" scoped></style>

9、新建date.vue组件

<template>
  <el-date-picker
    style="width: 100%"
    v-model="date"
    :type="fieldProperty.type"
    :disabled="fieldProperty.disabled"
    :range-separator="fieldProperty.ngeSeparator"
    :placeholder="fieldProperty.placeholder"
    :start-placeholder="fieldProperty.startPlaceholder"
    :disabled-date="fieldProperty.disabledDate"
    :end-placeholder="fieldProperty.endPlaceholder"
    @change="change"
  />
</template>
<script lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
export default {
  props: {
    modelValue: [String, Number], // 组件绑定值
    content: [String, Number], // 组件绑定值
    property: {
      type: Object,
      default() {
        return {}
      }
    }
  },
  setup(props, { emit }) {
    const { t } = useI18n()
    const fieldProperty = computed(() => {
      return {
        startPlaceholder: t('Startdate'), // 提示语
        endPlaceholder: t('Enddate'), // 提示语
        placeholder: t('Selectdate'), // 提示语
        readonly: false, // 是否只读
        disabled: false, // 是否可输入
        ngeSeparator: t('To'), 
        type: 'date', // year / month / date / dates / datetime / week / datetimerange / daterange / monthrange
        disabledDate: (time: Date) => {
          const beginDateVal = props.property.EffectDate
          if(beginDateVal){
            return time.valueOf()<new Date(beginDateVal).valueOf()- 24*60*60*1000
          }
          const endDateVal = props.property.endDate
          if(endDateVal){
            return time.valueOf()  > new Date(beginDateVal).valueOf()- 24*60*60*1000
          }
        },
        ...props.property
      }
    })
    const date = computed({
      get() {
        return props.modelValue
      },
      set(val) {
        emit('update:modelValue', val)
      }
    })
    
    const change = (val: any) => {
      emit('fieldChange', val) // 触发
    }
    return {
      date,
      change,
      fieldProperty
    }
  },
}
</script>
<style lang="less" scoped></style>

10、新建time-picker.vue组件

<template>
  <el-time-picker
    style="width: 100%"
    v-model="date"
    :is-range="fieldProperty.type"
    :disabled="fieldProperty.disabled"
    :range-separator="fieldProperty.ngeSeparator"
    :placeholder="fieldProperty.placeholder"
    :start-placeholder="fieldProperty.startPlaceholder"
    :end-placeholder="fieldProperty.endPlaceholder"
  />
</template>
<script lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
export default {
  props: {
    modelValue: [String, Number], // 组件绑定值
    content: [String, Number], // 组件绑定值
    property: {
      type: Object,
      default() {
        return {}
      }
    }
  },
  setup(props, { emit }) {
    const { t } = useI18n()
    const fieldProperty = computed(() => {
      return {
        startPlaceholder: t('Startdate'), // 提示语
        endPlaceholder: t('Enddate'), // 提示语
        placeholder: t('Selectdate'), // 提示语
        readonly: false, // 是否只读
        disabled: false, // 是否可输入
        ngeSeparator: t('To'), 
        type: true, // true / false
        ...props.property
      }
    })
    const date = computed({
      get() {
        return props.modelValue
      },
      set(val) {
        emit('update:modelValue', val)
      }
    })
    return {
      date,
      fieldProperty
    }
  },
}
</script>
<style lang="less" scoped></style>

11、新建cascader.vue组件

<template>
  <el-cascader
    v-model="value"
    :filterable="fieldProperty.filterable"
    :placeholder="fieldProperty.placeholder"
    :disabled="fieldProperty.disabled"
    :clearable="fieldProperty.clearable"
    :options="fieldProperty.data"
    :collapse-tags="true"
    :collapse-tags-tooltip="true"
    @change="handleChange"
  />
</template>
<script lang="ts">
import { computed, reactive, watch } from 'vue'
export default {
  props: {
    modelValue: Array, // 组件绑定值
    content: Array, // 组件绑定值
    property: {
      type: Object,
      default() {
        return {}
      }
    }
  },
  setup(props, { emit }) {
    const fieldProperty = computed(() => {
      return {
        placeholder: '请选择', // 提示语
        disabled: false, // 是否可输入
        clearable: true, // 是否支持清空选项
        filterable: true, // 是否可搜索
        data: [],
        ...props.property
      }
    })
    const value = computed({
      get() {
        return props.modelValue
      },
      set(val) {
        emit('update:modelValue', val)
      }
    })
    const handleChange = (val) => {
      console.log('handleChange', val)
    }
    return {
      value,
      fieldProperty,
      handleChange
    }
  },
}
</script>
<style lang="less" scoped></style>

12、新建/ueditor/组件

  • index.vue
<template>
  <vue-ueditor-wrap
    v-model="content"
    :config="VueUeditorWrapConfig"
    :editor-id="editorId"
  ></vue-ueditor-wrap>
</template>
<script>
import { default as VueUeditorWrapConfig } from "./ueditor.js";
import { RandomNumber } from '@/utils'
export default {
  props: {
    value: [String],
    contentDetail: {
      type: String,
      default: "",
    },
    property: {
      type: Object,
      default() {
        return {};
      },
    },
  },
  model: {
    prop: "value", // 指定 v-model 要绑定的参数叫什么名字,来自于 props 中定义的参数
    event: "change", // 指定要触发的事件名字,将被用于 $emit
  },
  data() {
    return {
      show: false,
      VueUeditorWrapConfig,
      isPosting: false,
      edit_show: true,
      index: 0,
      editorId: 'editor' + RandomNumber()
    };
  },
  computed: {
    content: {
      get() {
        console.log('10000', this.value)
        return this.value;
      },
      set(val) {
        this.$emit("change", val);
      },
    },
  },
};
</script>
<style lang="less" scoped>
/* 弹框输入框长度 */
.lengthWidth {
  width: 80%;
}

.editor_if {
  width: 100%;
  height: 400px;
}

.edui1 {
  z-index: 90 !important;
}
</style>

  • ueditor.js
// let env = process.env.NODE_ENV || ''
let ueditorPath = ''
// if (env === 'development') {
//   ueditorPath = '/static/UEditor/'
// } else if (env === 'production') {
//   ueditorPath = '/static/UEditor/'
// } else {
//   ueditorPath = '/static/UEditor/'
// }
ueditorPath = '/ueditor/'
export default {
  UEDITOR_HOME_URL: ueditorPath,
  maximumWords: 1000 * 100,
  // toolbars: [
  //   [
  //     'source', '|', 'undo', 'redo', '|',
  //     'bold', 'italic', 'underline', 'fontborder', 'strikethrough', 'superscript', 'subscript', 'removeformat', 'formatmatch', 'autotypeset', 'blockquote', 'pasteplain', '|', 'forecolor', 'backcolor', 'insertorderedlist', 'insertunorderedlist', 'selectall', 'cleardoc', '|',
  //     'rowspacingtop', 'rowspacingbottom', 'lineheight', '|',
  //     'customstyle', 'paragraph', 'fontfamily', 'fontsize', '|',
  //     'directionalityltr', 'directionalityrtl', 'indent', '|',
  //     'justifyleft', 'justifycenter', 'justifyright', 'justifyjustify', '|', 'touppercase', 'tolowercase', '|',
  //     'imagenone', 'imageleft', 'imageright', 'imagecenter', '|',
  //     'horizontal', 'date', 'time',
  //     'inserttable', 'simpleupload', 'preview',
  //   ]
  // ],
  toolbars: [
    [
      'source', '|', 'undo', 'redo', '|',
      'bold', 'italic', 'underline', 'fontborder', 'strikethrough', 'superscript', 'subscript', 'removeformat', 'formatmatch', 'autotypeset', 'blockquote', 'pasteplain', '|', 'forecolor', 'backcolor', 'insertorderedlist', 'insertunorderedlist', 'selectall', 'cleardoc', '|',
      'rowspacingtop', 'rowspacingbottom', 'lineheight', '|',
      'customstyle', 'paragraph', 'fontfamily', 'fontsize', '|',
      'directionalityltr', 'directionalityrtl', 'indent', '|',
      'justifyleft', 'justifycenter', 'justifyright', 'justifyjustify', '|', 'touppercase', 'tolowercase', '|',
      'imagenone', 'imageleft', 'imageright', 'imagecenter', '|',
      'horizontal'
    ]
  ],
  enableAutoSave: false,
  elementPathEnabled: false,
  disabledTableInTable: false,
  serverUrl: '' // 暂无接口 APP_CONFIG.baseURL + '/file/upload'
}

13、新建/kind-editor组件

  • index.vue
<template>
  <vue3-kind-editor
    :id="editorId"
    :height="fieldProperty.height"
    :width="fieldProperty.width"
    v-model="content"
    :items="fieldProperty.items"
    :loadStyleMode="fieldProperty.loadStyleMode">
  </vue3-kind-editor>
</template>
<script lang="ts" src="./index.js" />
<style lang="less" scoped>
/* 弹框输入框长度 */
.lengthWidth {
  width: 80%;
}

.editor_if {
  width: 100%;
  height: 400px;
}

.edui1 {
  z-index: 90 !important;
}
</style>
  • index.js

// import Vue3KindEditor from '@zhj-target/vue3-kind-editor'
import { computed } from 'vue';
import Vue3KindEditor from './editor/index.vue'
import { RandomNumber } from '@/utils'
window._instances = []
export default {
  components: {
    Vue3KindEditor,
  },
  props: {
    value: [String],
    property: {
      type: Object,
      default() {
        return {};
      },
    },
  },
  model: {
    prop: "value", // 指定 v-model 要绑定的参数叫什么名字,来自于 props 中定义的参数
    event: "change", // 指定要触发的事件名字,将被用于 $emit
  },
  setup(props, { emit }) {
    const fieldProperty = computed(() => {
      return {
        height: '150px',
        width: '100%',
        loadStyleMode: false,
        items: [
          'preview', 'template', '|', 'fontsize', '|', 'forecolor', 'hilitecolor', 'bold', 'italic', 'underline',
          'removeformat', '|', 'justifyleft', 'justifycenter', 'justifyright', 'insertorderedlist',
          'insertunorderedlist', '|', 'link'
        ],
        ...props.property
      }
    })
    const content = computed({
      get() {
        return props.value
      },
      set(val) {
        emit('update:value', val) // 触发
      }
    })
    const editorId = computed(() => {
      return 'editor' + RandomNumber()
    })
    return {
      editorId,
      content,
      fieldProperty
    }
  }
};
  • editor/index.vue
<template>
  <div class="kindeditor">
    <textarea :id="id" name="content" v-html="state.outContent"></textarea>
  </div>
</template>
<script lang="ts" src="./index.ts" />
<style scoped>
.kindeditor {
  width: 100%;
}
</style>
  • editor/index.js

import { onMounted, reactive, watch } from "vue"
import '@public/kind-editor/themes/default/default.css'
import '@public/kind-editor/kindeditor-all.js'
import '@public/kind-editor/lang/zh-CN.js'
import '@public/kind-editor/lang/en.js'
export default {
  name: 'vue3-kind-editor',
  props: {
    modelValue: {
      type: String,
      default: ''
    },
    id: {
      type: String,
      required: true
    },
    width: {
      type: String
    },
    height: {
      type: String
    },
    minWidth: {
      type: Number,
      default: 650
    },
    minHeight: {
      type: Number,
      default: 100
    },
    items: {
      type: Array,
      default: function () {
        return [
          'source', '|', 'undo', 'redo', '|', 'preview', 'print', 'code', 'cut', 'copy', 'paste',
          'plainpaste', 'wordpaste', '|', 'justifyleft', 'justifycenter', 'justifyright',
          'justifyfull', 'insertorderedlist', 'insertunorderedlist', 'indent', 'outdent', 'subscript',
          'superscript', 'clearhtml', 'quickformat', 'selectall', '|', 'fullscreen', '/',
          'formatblock', 'fontname', 'fontsize', '|', 'forecolor', 'hilitecolor', 'bold',
          'italic', 'underline', 'strikethrough', 'lineheight', 'removeformat', '|', 'image',
          'media', 'insertfile', 'table', 'hr', 'pagebreak',
          'anchor', 'link', 'unlink',
        ]
      }
    },
    noDisableItems: {
      type: Array,
      default: function () {
        return ['source', 'fullscreen']
      }
    },
    filterMode: {
      type: Boolean,
      default: true
    },
    htmlTags: {
      type: Object,
      default: function () {
        return {
          font: ['color', 'size', 'face', '.background-color'],
          span: ['style'],
          div: ['class', 'align', 'style'],
          table: ['class', 'border', 'cellspacing', 'cellpadding', 'width', 'height', 'align', 'style'],
          'td,th': ['class', 'align', 'valign', 'width', 'height', 'colspan', 'rowspan', 'bgcolor', 'style'],
          a: ['class', 'href', 'target', 'name', 'style'],
          embed: ['src', 'width', 'height', 'type', 'loop', 'autostart', 'quality',
            'style', 'align', 'allowscriptaccess', '/'],
          img: ['src', 'width', 'height', 'border', 'alt', 'title', 'align', 'style', '/'],
          hr: ['class', '/'],
          br: ['/'],
          'p,ol,ul,li,blockquote,h1,h2,h3,h4,h5,h6': ['align', 'style'],
          'tbody,tr,strong,b,sub,sup,em,i,u,strike': []
        }
      }
    },
    wellFormatMode: {
      type: Boolean,
      default: true
    },
    resizeType: {
      type: Number,
      default: 2
    },
    themeType: {
      type: String,
      default: 'default'
    },
    langType: {
      type: String,
      default: 'en'
    },
    designMode: {
      type: Boolean,
      default: true
    },
    fullscreenMode: {
      type: Boolean,
      default: false
    },
    basePath: {
      type: String
    },
    themesPath: {
      type: String
    },
    pluginsPath: {
      type: String,
      default: ''
    },
    langPath: {
      type: String
    },
    minChangeSize: {
      type: Number,
      default: 5
    },
    loadStyleMode: {
      type: Boolean,
      default: true
    },
    urlType: {
      type: String,
      default: ''
    },
    newlineTag: {
      type: String,
      default: 'p'
    },
    pasteType: {
      type: Number,
      default: 2
    },
    dialogAlignType: {
      type: String,
      default: 'page'
    },
    shadowMode: {
      type: Boolean,
      default: true
    },
    zIndex: {
      type: Number,
      default: 811213
    },
    useContextmenu: {
      type: Boolean,
      default: true
    },
    syncType: {
      type: String,
      default: 'form'
    },
    indentChar: {
      type: String,
      default: '\t'
    },
    cssPath: {
      type: [String, Array]
    },
    cssData: {
      type: String
    },
    bodyClass: {
      type: String,
      default: 'ke-content'
    },
    colorTable: {
      type: Array
    },
    afterCreate: {
      type: Function
    },
    afterChange: {
      type: Function
    },
    afterTab: {
      type: Function
    },
    afterFocus: {
      type: Function
    },
    afterBlur: {
      type: Function
    },
    afterUpload: {
      type: Function
    },
    uploadJson: {
      type: String
    },
    fileManagerJson: {
      type: Function
    },
    allowPreviewEmoticons: {
      type: Boolean,
      default: true
    },
    allowImageUpload: {
      type: Boolean,
      default: true
    },
    allowFlashUpload: {
      type: Boolean,
      default: true
    },
    allowMediaUpload: {
      type: Boolean,
      default: true
    },
    allowFileUpload: {
      type: Boolean,
      default: true
    },
    allowFileManager: {
      type: Boolean,
      default: false
    },
    fontSizeTable: {
      type: Array,
      default: function () {
        return ['9px', '10px', '12px', '14px', '16px', '18px', '24px', '32px']
      }
    },
    imageTabIndex: {
      type: Number,
      default: 0
    },
    formatUploadUrl: {
      type: Boolean,
      default: true
    },
    fullscreenShortcut: {
      type: Boolean,
      default: false
    },
    extraFileUploadParams: {
      type: Array,
      default: function () {
        return []
      }
    },
    filePostName: {
      type: String,
      default: 'imgFile'
    },
    fillDescAfterUploadImage: {
      type: Boolean,
      default: false
    },
    afterSelectFile: {
      type: Function
    },
    pagebreakHtml: {
      type: String,
      default: '<hr style=”page-break-after: always;” class=”ke-pagebreak” />'
    },
    allowImageRemote: {
      type: Boolean,
      default: true
    },
    autoHeightMode: {
      type: Boolean,
      default: false
    },
    fixToolBar: {
      type: Boolean,
      default: false
    },
    tabIndex: {
      type: Number
    }
  },
  setup(props: any, { emit }: any) {
    const state = reactive({
      editor: {} as any,
      outContent: props.modelValue
    })
    watch(() => props.modelValue, (val: any) => {
      state.editor && val !== state.outContent && state.editor.html(val)
    })
    watch(() => state.outContent, (val: any) => {
      emit('update:modelValue', val)
    })
    onMounted(async() => {
      state.editor = (window as any).KindEditor.create('#' + props.id, {
        width: props.width,
        height: props.height,
        minWidth: props.minWidth,
        minHeight: props.minHeight,
        items: props.items,
        noDisableItems: props.noDisableItems,
        filterMode: props.filterMode,
        htmlTags: props.htmlTags,
        wellFormatMode: props.wellFormatMode,
        resizeType: props.resizeType,
        themeType: props.themeType,
        langType: props.langType,
        designMode: props.designMode,
        fullscreenMode: props.fullscreenMode,
        basePath: props.basePath,
        themesPath: props.cssPath,
        pluginsPath: props.pluginsPath,
        langPath: props.langPath,
        minChangeSize: props.minChangeSize,
        loadStyleMode: props.loadStyleMode,
        urlType: props.urlType,
        newlineTag: props.newlineTag,
        pasteType: props.pasteType,
        dialogAlignType: props.dialogAlignType,
        shadowMode: props.shadowMode,
        zIndex: props.zIndex,
        useContextmenu: props.useContextmenu,
        syncType: props.syncType,
        indentChar: props.indentChar,
        cssPath: props.cssPath,
        cssData: props.cssData,
        bodyClass: props.bodyClass,
        colorTable: props.colorTable,
        afterCreate: props.afterCreate,
        afterChange: function () {
          props.afterChange
          state.outContent = this.html()
        },
        afterTab: props.afterTab,
        afterFocus: props.afterFocus,
        afterBlur: props.afterBlur,
        afterUpload: props.afterUpload,
        uploadJson: props.uploadJson,
        fileManagerJson: props.fileManagerJson,
        allowPreviewEmoticons: props.allowPreviewEmoticons,
        allowImageUpload: props.allowImageUpload,
        allowFlashUpload: props.allowFlashUpload,
        allowMediaUpload: props.allowMediaUpload,
        allowFileUpload: props.allowFileUpload,
        allowFileManager: props.allowFileManager,
        fontSizeTable: props.fontSizeTable,
        imageTabIndex: props.imageTabIndex,
        formatUploadUrl: props.formatUploadUrl,
        fullscreenShortcut: props.fullscreenShortcut,
        extraFileUploadParams: props.extraFileUploadParams,
        filePostName: props.filePostName,
        fillDescAfterUploadImage: props.fillDescAfterUploadImage,
        afterSelectFile: props.afterSelectFile,
        pagebreakHtml: props.pagebreakHtml,
        allowImageRemote: props.allowImageRemote,
        autoHeightMode: props.autoHeightMode,
        fixToolBar: props.fixToolBar,
        tabIndex: props.tabIndex
      })
    })
    return {
      state

    }
  }

}

二、配置的表单及使用

  • 定义表单配置类
interface Rules {
  [key: string]: Array<Rule>;
}
interface Rule {
  required: boolean;
  message?: string;
  trigger: string;
  validator?: Function;
}
interface unKnow {
  [key: string]: any;
}
type DefaultFields = unKnow;
interface FormData {
  [key: string]: FormDataInfo;
}
interface FormDataInfo {
  title?: string;
  field?: string;
  colSize: number;
  show: boolean;
  type: string;
  class?: Array<string>;
  zIndex?: number;
  property: FieldProperty;
}
type SelectList = unKnow;
interface FieldProperty {
  disabled?: boolean;
  readonly?: boolean;
  placeholder?: string;
  maxlength?: number;
  label?: string;
  value?: string;
  filterable?: boolean;
  clearable?: boolean;
  data?: Array<SelectList>;
  multiple?: boolean;
  btnName?: string;
  activeColor?: string;
  inactiveColor?: string;
  img?: string;
  TitleT?: string;
  time?: number;
  type?: string | number;
  search?: boolean;
  del?: boolean;
  sameAsSupplier?: boolean;
  loading?: boolean;
  limit?: number;
  accept?: string;
}
class FormConfigurationExampleEntity {
  public formRules: Rules = {};
  public formFields: DefaultFields = {};
  public formData: FormData = {};
  constructor() {
    this.formFields = {
      Input: "",
      Select: "",
      Vhtml: "<div style='color: red'>color</div>",
      Upload: "",
      Switch: "",
      Radio: "0",
      Checkbox: true,
      Date: "",
      TimePicker: "",
      Cascader: "",
      UEditor: "",
      KindEditor: "",
    };
    this.formData = {
      Input: {
        type: "Input",
        colSize: 12,
        show: true,
        class: [],
        title: "Input",
        field: "Input",
        property: {
          type: "text",
          placeholder: "text",
        },
      },
      Reason: {
        type: "Select",
        colSize: 12,
        show: true,
        class: [],
        title: "Select",
        field: "Select",
        property: {
          data: [
            {
              label: "请选择",
              value: "",
            },
            {
              label: "Select",
              value: "Select",
            },
          ],
          label: "label",
          value: "value",
        },
      },
      Vhtml: {
        type: "Vhtml",
        colSize: 12,
        show: true,
        class: [],
        title: "Vhtml",
        field: "Vhtml",
        property: {},
      },
      upload: {
        title: "upload:",
        field: "upload",
        type: "Upload",
        colSize: 12,
        show: true,
        property: {
          readonly: false,
          multiple: true,
          btnName: "Browse...",
        },
      },
      Switch: {
        title: "Switch:",
        field: "Switch",
        type: "Switch",
        colSize: 12,
        show: true,
        property: {
          readonly: false,
          multiple: true,
          btnName: "Browse...",
        },
      },
      Radio: {
        type: "Radio",
        colSize: 12,
        show: true,
        title: "Radio",
        field: "Radio",
        class: [],
        property: {
          data: [
            { label: "男", value: "1" },
            { label: "女", value: "0" },
          ],
        },
      },
      Checkbox: {
        type: "Checkbox",
        colSize: 12,
        show: true,
        class: [],
        title: "Checkbox",
        field: "Checkbox",
        property: {
          label: "",
        },
      },
      Date: {
        type: "Date",
        colSize: 12,
        show: true,
        class: [],
        title: "Date",
        field: "Date",
        property: {
          placeholder: "Date",
          type: "daterange",
        },
      },
      TimePicker: {
        type: "TimePicker",
        colSize: 12,
        show: true,
        class: [],
        title: "TimePicker",
        field: "TimePicker",
        property: {
          placeholder: "TimePicker",
        },
      },
      Cascader: {
        type: "Cascader",
        colSize: 12,
        show: true,
        class: [],
        title: "Cascader",
        field: "Cascader",
        property: {
          data: [
            {
              value: "CN",
              label: "中国",
              children: [
                {
                  value: "CN",
                  label: "中国",
                  children: [
                    {
                      value: "CN",
                      label: "中国",
                    },
                  ],
                },
              ],
            },
          ],
        },
      },
      UEditor: {
        type: 'UEditor',
        colSize: 24,
        show: true,
        class: [],
        title: 'UEditor',
        field: 'UEditor',
        property: {
          placeholder: 'UEditor',
        }
      },
      KindEditor: {
        type: 'KindEditor',
        colSize: 24,
        show: true,
        class: [],
        title: 'KindEditor',
        field: 'KindEditor',
        property: {
          placeholder: 'KindEditor',
        }
      },
    };
  }
}
export default FormConfigurationExampleEntity;

  • 引入上传配置数据并应用form组件
<template>
  <FormList
    class="register-info-form"
    ref="FormListRef"
    :fields="formEntity.formFields"
    :formData="formEntity.formData"
    :rules="formEntity.formRules"
    labelWidth="120px"
  />
</template>

<script lang="ts" src="./index.ts"></script>
import { throttle } from "lodash-es";
import { defineComponent, reactive, ref } from "vue";
import FormConfigurationExampleEntity from "./field";
import FormList from "../form.vue";

export default defineComponent({
  components: {
    FormList,
  },
  setup() {
    const FormListRef = ref();
    const formEntity = reactive(new FormConfigurationExampleEntity());
    const validate = throttle(() => {
      FormListRef.value.$refs.formData.validate((valid: boolean) => {});
    }, 1500);
    return {
      formEntity,
      FormListRef,
      validate,
    };
  },
});

以上配置完即可显示配置好的表单啦~ ,更多详细细节后续有时间慢慢加上,然后再附上案例项目
在这里插入图片描述


  • 21
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值