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

文章目录

  • 前言
  • 一、配置化的前提
  • 二、配置的相关组件
    • 1、新建form.vue组件
    • 2、新建input.vue组件
    • 3、新建select.vue组件
    • 4、新建v-html.vue组件
    • 5、新建upload.vue组件
    • 6、新建switch.vue组件
    • 7、新建radio.vue组件
    • 8、新建checkbox.vue组件
    • 9、新建date.vue组件
    • 10、新建time-picker.vue组件
    • 11、新建cascader.vue组件
    • 12、新建/ueditor/组件
    • 13、新建/kind-editor组件
  • 二、配置的表单及使用


前言

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

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

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

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


一、配置化的前提

表单内一般包含

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

二、配置的相关组件

以下文件均建立在 @/componentsform-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">{{ 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 CancalBookingEntity {
  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 CancalBookingEntity;

  • 引入上传配置数据并应用form组件
<FormList
   class="register-info-form"
   ref="FormList"
   :fields="businessInquiry.formFields"
   :formData="businessInquiry.formData"
   :rules="businessInquiry.formRules"
   labelWidth="120px"
   />
export default {
  setup() {
  	const FormListRef = ref()
    const businessInquiry =  reactive(new BusinessInquiry())
    const validate = throttle(() => {
      companyInfo.value.$refs.formData.validate((valid: boolean) => {
        if (valid) ...
      })
    }, 1500)
    return {
      businessInquiry,
      FormList,
      validate 
    }
  },
}

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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/127216.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

泄露35TB数据,医疗巨头Henry Schein遭受黑猫勒索组织攻击

近日&#xff0c;据Bleeping Computer 网站消息&#xff0c;BlackCat&#xff08;黑猫&#xff09;勒索软件团伙将医疗保健巨头Henry Schein 添加到了其暗网泄露网站&#xff0c;并声称其破坏了该公司的网络&#xff0c;窃取了35 TB的敏感文件&#xff0c;这些文件包括了Henry …

【算法】道路与航线(保姆级题解)

题目 农夫约翰正在一个新的销售区域对他的牛奶销售方案进行调查。 他想把牛奶送到 T 个城镇&#xff0c;编号为 1∼T。 &#xff08;存在T个点&#xff09; 这些城镇之间通过 R 条道路 (编号为 1 到 R) 和 P 条航线 (编号为 1 到 P) 连接。 &#xff08;存在R条道路&#…

Bytebase 2.11.0 - 支持 OceanBase Oracle 模式

&#x1f680; 新功能 支持 OceanBase Oracle 模式。支持设置 MySQL 在线变更参数。新增项目数据库查看者的角色。 &#x1f384; 改进 支持在项目中直接选择所有用户并为之添加角色。 调整了项目页面的布局。在 SQL 编辑器中通过悬浮面板展示表和列的详情。 &#x1faa6; …

全局后置路由守卫(afterEach)

全局后置路由守卫&#xff08;afterEach&#xff09; 功能&#xff1a;每一次切换任意路由组件之后都会被调用&#xff0c;相当于在进入下一个路由组件之后设置一个权限。 使用原理 代码创建的位置&#xff1a; 在创建router之后&#xff08;const router new VueRouter&…

基于自然语言处理的结构化数据库问答机器人系统

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长 Wechat / QQ 名片 :) 1. 项目简介 知识库&#xff0c;就是人们总结出的一些历史知识的集合&#xff0c;存储、索引以后&#xff0c;可以被方便的检索出来供后人查询/学习。QnA Maker是用于建立知识库的工具&#xff0c;使用…

JAVA IDEA 下载

超简单步骤一&#xff1a; IntelliJ IDEA 官方下载链接 点击以上链接进入下图&#xff0c;点击下载 继续点下载&#xff0c;然后等待下载完后打开安装包即可 步骤二&#xff1a; 打开下好的安装包&#xff0c;点击Browse...我们把它下载到自己喜欢的地方&#xff08;主要是别占…

Java类和对象详解

文章目录 面向对象概述类和对象类定义和使用定义使用 对象引用对象的初始化和构造构造方法默认初始化就地初始化 面向对象概述 面向对象是一种现在主流的程序设计方法&#xff0c;现如今的大部分语言都支持面向对象&#xff0c;Java的面向对象是由C的面向对象衍生而来&#xf…

Talk | 马里兰大学博士生吴曦旸:分布式多智能体强化学习在复杂交通轨迹规划中的应用

本期为TechBeat人工智能社区第545期线上Talk&#xff01; 北京时间11月09日(周四)20:00&#xff0c;马里兰大学博士生—吴曦旸的Talk已准时在TechBeat人工智能社区开播&#xff01; 他与大家分享的主题是: “分布式多智能体强化学习在复杂交通轨迹规划中的应用”&#xff0c;介…

SpringBoot定时任务打成jar 引入到新的项目中后并自动执行

一、springBoot开发定时任务 ①&#xff1a;连接数据库实现新增功能 1. 引入依赖 <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional> </dependency> <dependen…

阿里云竞争加剧,腾讯云双十一服务器优惠力度爆表!

腾讯云对于新客户和老客户都有相互照顾的优惠力度。特别是在今年的双十一活动中&#xff0c;腾讯云推出了一系列的优惠活动。首先&#xff0c;轻量服务器和云服务器产品的首购活动中&#xff0c;三年的云服务器仅需540元&#xff0c;这是一个非常低廉的价格。其次&#xff0c;香…

2.3.4 交换机的DHCP技术

实验2.3.4 交换机的DHCP技术 一、任务描述二、任务分析三、具体要求四、实验拓扑五、任务实施1.交换机的基本配置。2.将交换机的接口配置为trunk模式&#xff0c;并允许vlan10 和vlan20通过。3.开启交换机的DHCP功能。4.配置交换机的DHCP服务。5.配置vlan的vlanif接口的IP地址&…

【Spring】事务实现原理

在使用事务的时候需要添加EnableTransactionManagement注解来开启事务&#xff0c;Spring事务底层是通过AOP来实现的&#xff0c;所以启用事务后&#xff0c;同样会向容器中注入一个代理对象创建器&#xff0c;AOP使用的是AnnotationAwareAspectJAutoProxyCreator&#xff0c;事…

易点易动固定资产管理系统:实现财务与OA系统的无缝对接,高效管理固定资产

在现代企业经营中&#xff0c;固定资产管理是一个非常重要的环节。准确记录和管理固定资产不仅对企业的财务状况有直接影响&#xff0c;还能提高资产利用率、降低运营成本&#xff0c;并确保企业的合规性。然而&#xff0c;传统的固定资产管理方式往往存在繁琐、效率低下的问题…

计算机考研408到底有多难?25届开个好头很有必要

前言 大家好&#xff0c;我是陈橘又青&#xff0c;相信关注我的各位小伙伴们中&#xff0c;大多都是在计算机专业的大学生吧&#xff01; 每天都有许多人在后台私信我&#xff0c;问我要不要考研&#xff0c;我想说这个东西是因人而异的&#xff0c;像我本人就选择了就业&…

ADS微带单枝短截线匹配电路的仿真

ADS微带单枝短截线匹配电路的仿真 简介环境原理图过程版图过程 简介 利用ADS2020软件设计匹配电路通常有5种方法&#xff0c;本小节首先介绍如何通过“Design-Guide”进行微带单枝短截线匹配电路的设计与仿真。 环境 ADS2020 《ADS2011射频电路设计与仿真实例》 [徐兴福著][…

10 个适用于 Windows 的最佳 PDF 编辑器,用于轻松编辑 PDF 文件

PDF 是当今最流行的文件格式之一。Adobe 于 1993 年开发了 PDF 文件格式。PDF&#xff08;便携式文档格式&#xff09;主要用于存储复杂的文本文档和电子书。PDF 文件包含固定的布局属性&#xff0c;并且可以存储大量文本和图形。PDF 文件格式主要用于分发大型文档。 使用 PDF…

vmware16.2内部win7联网

1、主机配置 前置条件&#xff1a;DHCP和NAT服务已启动 设置无线IP与虚拟机IP为自动获取 二者都是&#xff1a;右键-属性 选择IPv4 自动获取 2、虚拟机配置 设置虚拟机上网方式为NAT 菜单栏-虚拟机-设置 NMnet8改为NAT模式 菜单栏-编辑-虚拟网络编辑器 win7系统内部网…

Linux设置N天未登录强制冻结

1、创建普通用户 2、90天未登录强制冻结 chage -E $(date -d "90 days" %Y-%m-%d) 用户名 3.更改系统日期 sudo date -s "2024-02-07 20:18:00" 4.过期未登录,提示如下 5.账号解冻 chage -E -1 用户名

Apinto 网关进阶教程,使用 API Mock 生成模拟数据

什么是 API Mock &#xff1f; API Mock 是一种技术&#xff0c;它允许程序员在不依赖后端数据的情况下&#xff0c;模拟 web服务器端 API 的响应。通常使用 API Mock 来测试前端应用程序&#xff0c;而无需等待后端程序构建完成。API Mock 可以模拟任何 HTTP 请求方法&#x…

【操作系统】测试二

文章目录 单选题判断题填空题 单选题 在操作系统中&#xff0c;进行资源分配、调度和管理的最小独立单位是&#xff08;&#xff09;。 【 正确答案: C】 A. 作业 B. 程序 C. 进程 D. 用户 进程在发出I/O请求后&#xff0c;可能导致下列哪种进程状态演变&#xff1f; 【 正确答…