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

news2024/11/24 17:09:04

文章目录

  • 前言
  • 一、配置化的前提
  • 二、配置的相关组件
    • 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组件
  • 二、配置的表单及使用


前言

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

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

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

本文以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 
    }
  },
}

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


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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Pytorch实战教程(一)-神经网络与模型训练

0. 前言 人工神经网络 (Artificial Neural Network, ANN) 是一种监督学习算法,其灵感来自人类大脑的运作方式。类似于人脑中神经元连接和激活的方式,神经网络接受输入,通过某些函数在网络中进行传递,导致某些后续神经元被激活,从而产生输出。函数越复杂,网络对于输入的数…

传统企业数字化转型都要面临哪些挑战?_数据治理平台_光点科技

数字化转型已经成为传统企业发展的必经之路&#xff0c;但在这个过程中&#xff0c;企业往往会遭遇多方面的挑战。 1.文化和组织惯性 最大的挑战之一是企业文化和组织惯性的阻力。传统企业往往有着深厚的历史和根深蒂固的工作方式&#xff0c;员工和管理层可能对新的数字化工作…

FFMPEG库实现mp4/flv文件(H264+AAC)的封装与分离

ffmepeg 4.4&#xff08;亲测可用&#xff09; 一、使用FFMPEG库封装264视频和acc音频数据到 mp4/flv 文件中 封装流程 1.使用avformat_open_input分别打开视频和音频文件&#xff0c;初始化其AVFormatContext&#xff0c;使用avformat_find_stream_info获取编码器基本信息 2.使…

一文入门Springboot+actuator+Prometheus+Grafana

环境介绍 技术栈 springbootmybatis-plusmysqloracleactuatorPrometheusGrafana 软件 版本 mysql 8 IDEA IntelliJ IDEA 2022.2.1 JDK 1.8 Spring Boot 2.7.13 mybatis-plus 3.5.3.2 本地主机应用 192.168.1.9:8007 PrometheusGrafana安装在同一台主机 http://…

CSS特效005:绘制一个环环相扣的五个环

css实战中&#xff0c;怎么制作这样的一个环环相扣的五个环呢&#xff1f; 绘制五个圈圈很容易&#xff0c;关键是要环环相扣&#xff0c;尤其要注意环环相交部分的处理。这里要用到transform-style: preserve-3d; 和 transform: rotateY( 1deg ) 等关键的css技术。 效果图 源…

无需公网IP!部署Apache服务器与内网穿透实现公网访问

Apache服务安装配置与结合内网穿透实现公网访问 文章目录 Apache服务安装配置与结合内网穿透实现公网访问前言1.Apache服务安装配置1.1 进入官网下载安装包1.2 Apache服务配置 2.安装cpolar内网穿透2.1 注册cpolar账号2.2 下载cpolar客户端 3. 获取远程桌面公网地址3.1 登录cpo…

密码学 - RSA签名算法

实验九 RSA签名算法- 一、实验目的 通过实验掌握GMP开源软件的用法&#xff0c;理解RSA数字签名算法&#xff0c;学会RSA数字签名算法程序设计&#xff0c;提高一般数字签名算法的设计能力。 二、实验要求 (1)基于GMP开源软件&#xff0c;实现RSA签名算法。 (2)要求有对应…

GEE:基于 Landsat 计算的 kNDVI 应用 APP

作者:CSDN @ _养乐多_ 本文记录了在Google Earth Engine(GEE)平台中,使用 Landsat 遥感数据计算 kNDVI 的应用 APP 链接,并介绍该 APP 的使用方法和步骤。该APP可以为用户展示 NDVI 和 kNDVI 的遥感影像,进行对比分析。该 APP 在 Google Earth Engine(GEE)平台中实现。…

在 Arduino IDE 2.0 中安装 ESP32 板(Windows、Mac OS X、Linux)

有一个新的 Arduino IDE——Arduino IDE 2.0&#xff08;测试版&#xff09;。在本教程中&#xff0c;您将学习如何在 Arduino IDE 2.0 中安装 ESP32 板并将代码上传到板。本教程与 Windows、Mac OS X 和 Linux 操作系统兼容。 据 Arduino 网站称&#xff1a;“ Arduino IDE 2.…

【微服务专题】手写模拟SpringBoot

目录 前言阅读对象阅读导航前置知识笔记正文一、工程项目准备1.1 新建项目1.1 pom.xml1.2 业务模拟 二、模拟SpringBoot启动&#xff1a;好戏开场2.1 启动配置类2.1.1 shen-base-springboot新增2.1.2 shen-example客户端新增启动类 三、run方法的实现3.1 步骤一&#xff1a;启动…

Javaweb之javascript的BOM对象的详细解析

1.5.2 BOM对象 接下来我们学习BOM对象&#xff0c;BOM的全称是Browser Object Model,翻译过来是浏览器对象模型。也就是JavaScript将浏览器的各个组成部分封装成了对象。我们要操作浏览器的部分功能&#xff0c;可以通过操作BOM对象的相关属性或者函数来完成。例如&#xff1a…

asp.net core weapi 结合identity完成登录/注册/角色/权限分配

1.安装所需要的nuget包 <PackageReference Include"Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version"6.0.24" /><PackageReference Include"Microsoft.EntityFrameworkCore" Version"6.0.24" /><PackageR…

Docker学习——⑥

文章目录 1、什么是存储卷?2、为什么需要存储卷?3、存储卷分类4、管理卷 Volume5、绑定卷 bind mount6、临时卷 tmpfs7、综合实战-MySQL 灾难恢复8、常见问题 1、什么是存储卷? 存储卷就是将宿主机的本地文件系统中存在的某个目录直接与容器内部的文件系统上的某一目录建立…

实战leetcode(二)

Practice makes perfect&#xff01; 实战一&#xff1a; 这里我们运用快慢指针的思想&#xff0c;我们的slow和fast都指向第一个节点&#xff0c;我们的快指针一次走两步&#xff0c;慢指针一次走一步&#xff0c;当我们的fast指针走到尾的时候&#xff0c;我们的慢指针正好…

FPGA UDP RGMII 千兆以太网(3)ODDR

1 xilinx原语 在 7 系列 FPGA 中实现 RGMII 接口需要借助 5 种原语,分别是:IDDR、ODDR、IDELAYE2、ODELAYE2(A7 中没有)、IDELAYCTRL。其中,IDDR和ODDR分别是输入和输出的双边沿寄存器,位于IOB中。IDELAYE2和ODELAYE2,分别用于控制 IO 口输入和输出延时。同时,IDELAYE2 …

使用切面实现前端重复提交(防抖)

使用切面实现前端重复提交&#xff08;防抖&#xff09; 代码结构定义注解请求锁切面处理器入参对象使用注解 代码结构 原理&#xff1a; 1、前端提交保存操作&#xff1b; 2、后端通过注解指定重复提交的关键字段进行识别&#xff0c;可以有多个&#xff1b; 3、拼接关键字段&…

科普测量开关电源输出波形的三种方法及电源波形自动化测试步骤

开关电源波形测试就是对开关电源的输出波形进行检测和分析&#xff0c;观察开关电源参数变化&#xff0c;以此来判断开关电源的性能是否符合要求。好的开关电源对于设备以及整个电路的正常运行是非常重要的&#xff0c;因此开关电源输出波形测试是开关电源测试的重要环节&#…

RK3399平台开发系列讲解(内存篇)free 命令查看内存占用情况介绍

🚀返回专栏总目录 文章目录 一、free的使用二、free的内容📢free 指令会显示内存的使用情况,包括实体内存,虚拟的交换文件内存,共享内存区段,以及系统核心使用的缓冲区等。 一、free的使用 -b  以 Byte 为单位显示内存使用情况。-k  以 KB 为单位显示内存使用情况。…

华为防火墙双机热备配置案例

思路&#xff1a; IP和路由、ospf要两台防火墙单配&#xff0c;hrp不会同步 其它zone和策略会同步&#xff0c;只在master上配就行了 FW_A主要配置&#xff1a; hrp enable hrp interface GigabitEthernet1/0/2 remote 172.16.0.2 interface GigabitEthernet1/0/0 undo shut…

海康Visionmaster-通讯管理:ModBus 通信发送非整型 数据的方法

Modbus 通信发送数据只能为 Int 类型&#xff0c;如下图所示&#xff1a; 可以发送 Int 和 Float 数据&#xff0c;如下图所示 通信设备配置如下&#xff1a; 发送事件配置如下&#xff1a; 通信管理界面显示有问题&#xff0c;显示为 Int 类型存在一定误导&#xff1b;可以…