element-plus组件封装(vue@3)

640 阅读8分钟

编辑类表单

AddEditFormComponent.vue
<template>
  <el-form ref="addEditFormRef" :model="form" :rules="rules" class="addEdit_form" :label-width="labelWidth" :disabled="disabled">
    <template v-for="item in formItemList" :key="item.prop">
      <el-form-item :prop="item.prop" :label="item.label" :label-width="item.labelWidth" :class="{ 'required ': item.isRequired }">
        <slot :name="item.prop" :value="form[item.prop]" :form="form">
          <template v-if="item.type === 'input'">
            <el-input v-model.trim="form[item.prop]" :placeholder="item.placeholder || '请输入'" clearable :maxlength="item.maxlength || null">
              <template #suffix>
                <span v-if="item.suffix">{{ item.suffix }}</span>
              </template>
            </el-input>
          </template>
          <template v-else-if="item.type === 'textarea'">
            <el-input
              v-model="form[item.prop]"
              :placeholder="item.placeholder || '请输入内容'"
              clearable
              :autosize="{ minRows: 3 }"
              type="textarea"
              :disabled="item.disabled || false"
              :maxlength="item.maxlength || null"
              show-word-limit
            />
          </template>
          <!-- 适合详情页展示 -->
          <template v-else-if="item.type === 'text'">
            <span class="form-text">{{ item.formatter ? item.formatter(form[item.prop]) : form[item.prop] }}</span>
          </template>
          <template v-else-if="item.type === 'select'">
            <el-select v-model="form[item.prop]" :placeholder="item.placeholder || '请选择'" clearable :filterable="item.filterable" :disabled="item.disabled || false">
              <el-option v-for="option in item.options || []" :key="item.prop + '_option_' + option.value" :label="option.name" :value="option.value" />
            </el-select>
          </template>
          <template v-else-if="item.type === 'radio'">
            <el-radio-group v-model="form[item.prop]" :disabled="item.disabled || false">
              <el-radio v-for="option in item.options || []" :key="item.prop + '_option_' + option.value" :label="option.value">
                {{ option.name }}
              </el-radio>
            </el-radio-group>
          </template>
          <template v-else-if="item.type === 'checkbox'">
            <el-checkbox-group v-model="form[item.prop]" :disabled="item.disabled || false">
              <el-checkbox v-for="option in item.options || []" :key="item.prop + '_option_' + option.value" :label="option.value">
                {{ option.name }}
              </el-checkbox>
            </el-checkbox-group>
          </template>
          <template v-else-if="item.type === 'daterange'">
            <el-date-picker v-model="form[item.prop]" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" clearable :disabled="item.disabled || false" />
          </template>
          <template v-else-if="item.type === 'date'">
            <el-date-picker v-model="form[item.prop]" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" clearable :disabled="item.disabled || false" />
          </template>
          <template v-else-if="item.type === 'yearrange'">
            <date-picker v-model="form[item.prop]" type="yearrange" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY" clearable :disabled="item.disabled || false" />
          </template>
          <template v-else-if="item.type === 'year'"><el-date-picker v-model="form[item.prop]" type="year" value-format="YYYY" placeholder="请选择"></el-date-picker></template>
          <!-- TODO:上传组件 -->
          <template v-else-if="item.type === 'upload'">
            <BasicUploadComponent v-model:fileList="form[item.prop]" :maxLength="item.maxLength" @success="onUploadSuccess(item.prop)"></BasicUploadComponent>
          </template>
        </slot>
      </el-form-item>
    </template>
  </el-form>
</template>

<script setup>
const { $deepCopy } = getCurrentInstance().appContext.config.globalProperties;
import { validateFormsByFormRefs } from '@/utils/validateForms';
import BasicUploadComponent from './BasicUploadComponent.vue';
const props = defineProps({
  rules: {
    type: Object,
    default: () => {},
  },
  labelWidth: {
    type: String,
    default: '80px',
  },
  formItemList: {
    type: Array,
    default: () => [],
  },
  formData: {
    type: Object,
    default: () => {},
  },
  disabled: {
    type: Boolean,
    default: false,
  },
});

const form = ref({});
const addEditFormRef = ref(null);
watch(
  props.formData,
  (newVal) => {
    console.log('watch---formData', newVal);
    form.value = $deepCopy(newVal);
  },
  { deep: true, immediate: true }
);

const onUploadSuccess = (prop) => {
  addEditFormRef.value.clearValidate(prop);
};
const submitForm = async () => {
  await Promise.all(validateFormsByFormRefs([addEditFormRef.value])).catch((err) => {
    console.log('err', err);
    return Promise.reject(err);
  });
  return $deepCopy(form.value);
};

const resetFields = () => {
  addEditFormRef.value && addEditFormRef.value.resetFields();
};
defineExpose({
  resetFields,
  submitForm,
});
</script>

<style lang="scss" scoped>
.addEdit_form {
  ::v-deep .required {
    .el-form-item__label {
      &::before {
        content: '*';
        color: #ff4d4f;
        margin-right: 4px;
      }
    }
  }
}
</style>

示例
<template>
  <h2>addEditFormView</h2>
  <AddEditFormComponent style="width: 600px" ref="addEditFormComponentRef" :formData="formData" :formItemList="formItemList" :rules="rules" labelWidth="160px"></AddEditFormComponent>
  <el-button @click="onSubmit" type="primary">提交</el-button>
</template>

<script setup>
import AddEditFormComponent from '@/components/AddEditFormComponent.vue';

const formData = reactive({
  artist: 'bwf', //艺术家
  type: 2, //类型
  creationYear: '', //创作年代
  artworkDescribe: '', //描述
  imgs: [],
});
const formItemList = ref([
  { prop: 'artist', label: '艺术家', type: 'input' },
  { prop: 'type', type: 'select', label: '类型:', options: [] },
  { prop: 'creationYear', type: 'year', label: '创作年代:' },
  { prop: 'artworkDescribe', label: '艺术品描述:', isRequired: true, type: 'textarea' },
  { prop: 'imgs', label: '艺术家图片', type: 'upload', maxLength: 1 },
]);

const setTypeOptions = async () => {
  const res = await Promise.resolve([
    { name: '类型1', value: 1 },
    { name: '类型2', value: 2 },
    { name: '类型3', value: 3 },
  ]);
  formItemList.value.find((item) => item.prop === 'type').options = res;
};
// mock api
setTimeout(() => {
  setTypeOptions();
}, 500);

const rules = {
  artist: [{ required: true, message: '请输入艺术家' }],
  type: [{ required: true, message: '请选择类型' }],
  creationYear: [{ required: true, message: '请选择创作年代' }],
  artworkDescribe: [{ required: true, message: '请输入艺术品描述' }],
  imgs: [{ required: true, message: '请上传艺术家图片' }],
};
const addEditFormComponentRef = ref(null);
const onSubmit = async () => {
  const formData = await addEditFormComponentRef.value.submitForm();
  console.log('onSubmit', formData);
};
</script>

<style scoped></style>

上传组件

BasicUploadComponent.vue
<template>
  <!-- 支持单张以及批量 手动上传 -->
  <el-upload
    action=""
    :class="{ hidden: isDisabled || fileList.length >= maxLength }"
    multiple
    :file-list="fileList"
    class="upload_wrapper"
    :style="{ width: uploadBoxWidth }"
    :limit="maxLength"
    :on-exceed="onExceed"
    :before-upload="beforeUpload"
    list-type="picture-card"
    :http-request="handleUpload"
    :on-remove="onRemove"
    :on-success="onSuccess"
    :on-preview="onPreview"
  >
    <el-icon><Plus /></el-icon>
  </el-upload>
  <el-dialog v-model="dialogVisible">
    <img w-full :src="dialogImageUrl" alt="Preview Image" />
  </el-dialog>
</template>

<script setup>
import { Plus } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';

const props = defineProps({
  // url:渲染图片的key  uid:upload组件本身需要的key(必须唯一,用于内部v-for)
  fileList: {
    type: Array,
    default: () => [],
  },
  // 最大上传数量 单位张
  maxLength: {
    type: Number,
    default: 10,
  },
  uploadBoxWidth: {
    type: String,
  },
  // 是否禁用上传
  isDisabled: {
    type: Boolean,
    default: false,
  },
  // 允许的上传的文件类型类型
  allowTypes: {
    type: Array,
    default: () => ['image/jpeg', 'image/png', 'image/jpg', 'image/bmp', 'image/gif'],
  },
  // 允许的上传的文件大小 单位M
  maxSize: {
    type: Number,
    default: 5,
  },
});
const emits = defineEmits(['update:fileList', 'success']);
const dialogImageUrl = ref('');
const dialogVisible = ref(false);

const onPreview = (file) => {
  console.log('onPreview', file);
  dialogImageUrl.value = file.url;
  dialogVisible.value = true;
};

/**
 * @desc 删除文件时触发的回调函数
 * @param {file}  被删除的文件对象
 * @param {fileList}  剩余的文件列表
 */
const onRemove = (file, fileList) => {
  console.log('onRemove', file, fileList);
  emits('update:fileList', fileList);
};

/**
 * @desc 当超出限制时,执行的钩子函数
 */
const onExceed = () => {
  ElMessage({
    message: `最大允许上传${props.maxLength}张图片`,
    type: 'warning',
  });
};

/**
 * @desc 上传文件之前的钩子,参数为上传的文件,
 * @param {file} 上传的文件对象
 * @return {boolean} 若返回false或者返回 Promise 且被 reject,则停止上传。
 */
const beforeUpload = (file) => {
  console.log('beforeUpload', file);
  const typePassed = props.allowTypes.includes(file.type);
  const sizePassed = file.size / 1024 / 1024 < props.maxSize;
  if (!typePassed) {
    ElMessage({
      message: `文件格式不正确!`,
      type: 'error',
    });
    return false;
  }
  if (!sizePassed) {
    ElMessage({
      message: `上传文件大小不能超过 ${props.maxSize}MB!`,
      type: 'error',
    });
    return false;
  }
  return true;
};

/**
 * @desc 文件上传
 * @param {fileInfo}  文件对象
 * @return {res} 上传后的返回值
 */
const handleUpload = async (fileInfo) => {
  // TODO: 以下代码 模拟调用后端接口上传文件,真实业务中换成接口即可
  console.log('handleUpload', fileInfo);
  const file = fileInfo.file;
  const formData = new FormData();
  formData.append('file', file);
  const imgTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/bmp', 'image/gif'];
  const fileTypes = ['application/pdf']; /**适用于pdf超大文件的上传 */
  let res = {};
  if (imgTypes.includes(file.type)) {
    // res = await fileImageUploadRequest(formData);
    res = await new Promise((resolve) => {
      setTimeout(() => {
        resolve({
          url: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
          fileId: `${new Date().getTime()}`,
        });
      }, 800);
    });
  } else if (fileTypes.includes(fileInfo.file.type)) {
    res = await bigFileUploadRequest(formData);
  }
  return res;
};

/**
 * @desc
 * @param {res}  res 是 handleUpload的返回值
 * @return {}
 */
const onSuccess = (res, file, fileList) => {
  console.log('onSuccess', fileList);
  const files = fileList.map((item) => {
    const file = { ...(item.response ?? item), uid: item.uid };
    return file;
  });
  emits('update:fileList', files);
  emits('success', files);
};
</script>

<style lang="scss" scoped>
.upload_wrapper {
  &.hidden {
    ::v-deep .el-upload {
      display: none;
    }
  }
}
</style>

示例
<template>
  <h2>basicUploadView</h2>
  <BasicUploadComponent v-model:fileList="fileList" :maxLength="1"></BasicUploadComponent>
  <el-button @click="onSubmit" type="primary">提交</el-button>
</template>

<script setup>
import BasicUploadComponent from '@/components/BasicUploadComponent.vue';
const fileList = ref([]);
const onSubmit = () => {
  console.log('onSubmit', fileList.value);
};
</script>

<style scoped></style>

筛选组件

FilterFormComponent.vue
<template>
  <el-form ref="filterFormRef" class="filter_form" :model="filterModel" :inline="isInline" :label-width="labelWidth" :label-position="labelPosition">
    <div class="filter_wrapper">
      <el-form-item v-for="item in filters" :key="item.prop" :label="item.label + ':'" :prop="item.prop">
        <slot :name="item.prop">
          <template v-if="item.type === 'input'">
            <el-input v-model.trim="filterModel[item.prop]" :placeholder="item.placeholder || '请输入'" clearable :style="{ width: item.width }"> </el-input>
          </template>
          <template v-if="item.type === 'select'">
            <el-select v-model="filterModel[item.prop]" :placeholder="item.placeholder || '请选择'" clearable :multiple="item.multiple || false" collapse-tags :style="{ width: item.width }">
              <el-option v-for="option in item.options || []" :key="option.value" :label="option.label" :value="option.value" />
            </el-select>
          </template>
          <template v-if="item.type === 'daterange'">
            <el-date-picker
              v-model="filterModel[item.prop]"
              type="daterange"
              :value-format="item.valueFormat || 'YYYY-MM-DD'"
              start-placeholder="开始日期"
              end-placeholder="结束日期"
              :placeholder="item.placeholder || '请选择'"
              clearable
              :style="{ width: item.width }"
            />
          </template>
          <template v-if="item.type === 'yearrange'">
            <date-picker
              v-model="filterModel[item.prop]"
              type="yearrange"
              start-placeholder="开始时间"
              end-placeholder="结束时间"
              value-format="YYYY"
              :placeholder="item.placeholder || '请选择'"
              clearable
              :style="{ width: item.width }"
            />
          </template>
        </slot>
      </el-form-item>
    </div>
    <div class="handle_wrapper">
      <el-form-item>
        <el-button type="primary" @click="onSubmit">搜索</el-button>
        <el-button @click="onReset">重置</el-button>
        <slot name="exOperation"></slot>
      </el-form-item>
    </div>
  </el-form>
</template>

<script setup>
const props = defineProps({
  labelPosition: {
    type: String,
    default: 'right',
  },
  isInline: {
    type: Boolean,
    default: true,
  },
  filters: {
    type: Array,
    default: () => [],
  },
  labelWidth: {
    type: String,
  },
});
const emits = defineEmits(['submit', 'reset']);
const filterFormRef = ref(null);
const filterModel = reactive({});

const onSubmit = () => {
  console.log('onSubmit', filterModel);
  emits('submit', filterModel);
};
const onReset = () => {
  console.log('onReset');
  filterFormRef.value.resetFields();
};
</script>

<style lang="scss" scoped>
.filter_form {
  padding: 0 24px;
  display: flex;
  align-items: flex-end;
  .filter_wrapper {
    flex: 1;
  }
}
</style>

分页

PaginationComponent.vue
<template>
  <div class="pagination_wrapper">
    <el-pagination
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :page-sizes="sizesOptions"
      layout="total, sizes, prev, pager, next, jumper"
      :total="total"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />
  </div>
</template>

<script setup>
import { paginationKey } from '@/keys';

const { currentPage, pageSize, total, updateCurrentPage, updatePagesize, paginationChange, sizesOptions = [10, 20, 50, 100] } = inject(paginationKey);

const handleSizeChange = (val) => {
  updateCurrentPage(1);
  updatePagesize(val);
  paginationChange();
};
const handleCurrentChange = (val) => {
  updateCurrentPage(val);
  paginationChange();
};
</script>

<style lang="scss" scoped>
.pagination_wrapper {
  margin-top: 10px;
  display: flex;
  justify-content: flex-end;
  padding: 0 24px;
}
</style>

表格

BasicTableComponent.vue
<template>
  <el-table ref="basicTableComponentRef" @sort-change="onSortChange" :data="tableDatas" style="width: 100%" stripe @selection-change="onSelectionChange" v-bind="$attrs" v-loading="loading">
    <template v-for="item in columns">
      <el-table-column
        v-if="['index', 'selection'].includes(item.type)"
        :key="item.type"
        :type="item.type || ''"
        :index="item.index || columnIndex"
        :label="item.label"
        :align="item.align || 'center'"
        v-bind="item.attrs || {}"
      />
      <el-table-column
        v-else
        :sortable="item.sortable || false"
        :key="item.prop"
        :prop="item.prop"
        :label="item.label"
        :align="item.align || 'center'"
        :show-overflow-tooltip="item.showOverflowTooltip"
        v-bind="item.attrs || {}"
      >
        <template #default="{ row, column }">
          <slot :name="item.prop" :row="row" :column="column">
            {{ item.formatter ? item.formatter(row[column.property], row) : row[column.property] }}
          </slot>
        </template>
      </el-table-column>
    </template>
  </el-table>
  <PaginationComponent v-if="showPagination"></PaginationComponent>
</template>

<script setup>
// 引入分页组件
import PaginationComponent from './PaginationComponent.vue';
const props = defineProps({
  tableDatas: {
    type: Array,
    default: () => [],
  },
  columns: {
    type: Array,
    default: () => [],
  },
  loading: {
    type: Boolean,
    default: false,
  },
  showPagination: {
    type: Boolean,
    default: true,
  },
});
const emits = defineEmits(['sortChange']);
const multipleSelection = ref([]);
const columnIndex = (index) => {
  return index + 1;
};
const onSelectionChange = (val) => {
  multipleSelection.value = val;
  console.log('multipleSelection', multipleSelection.value);
};
const onSortChange = ({ column, prop, order }) => {
  console.log('onSortChange', column, prop, order);
  emits('sortChange', { prop, order });
};

defineExpose({
  multipleSelection,
});
</script>

<style scoped></style>

示例
<template>
  <h2>basicTableView</h2>
  <FilterFormComponent :filters="filters" @submit="onSubmit"></FilterFormComponent>
  <p style="text-align: right">
    <el-button type="primary" @click="onBatchDel">批量删除</el-button>
  </p>
  <BasicTableComponent ref="tableComponentRef" :tableDatas="tableDatas" :columns="columns" @sortChange="onSortChange">
    <template #operation="{ row }">
      <el-button type="primary" @click="onEdit(row)">编辑</el-button>
    </template>
  </BasicTableComponent>
</template>

<script setup>
import BasicTableComponent from '@/components/BasicTableComponent.vue';
import FilterFormComponent from '@/components/FilterFormComponent.vue';
import { paginationKey } from '@/keys';
import usePaginations from '@/composables/usePaginations';
const { paginations, updateCurrentPage, updatePagesize } = usePaginations();

const filters = reactive([
  {
    prop: 'name',
    label: '姓名',
    type: 'input',
    width: '200px',
  },
  {
    prop: 'date',
    label: '日期',
    type: 'daterange',
  },
  {
    prop: 'gender',
    label: '性别',
    type: 'select',
    options: [
      { label: '男', value: 'male' },
      { label: '女', value: 'female' },
    ],
  },
]);
const tableComponentRef = ref(null);
const tableDatas = ref([
  // 初始化3条数据
]);
const columns = [
  { type: 'selection', label: '' },
  { prop: 'date', label: '日期' },
  { prop: 'name', label: '姓名' },
  { prop: 'age', label: '年龄' },
  {
    prop: 'gender',
    label: '性别',
    formatter: (val, row) => {
      if (val === 'female') return '女';
      return '男';
    },
  },
  { prop: 'address', label: '地址' },
  { prop: 'operation', label: '操作' },
];

const onSubmit = (filters) => {
  console.log('onSubmit', filters);
};

const paginationChange = () => {
  console.log('pagination---chage', paginations);
};
provide(paginationKey, {
  ...toRefs(paginations),
  updateCurrentPage,
  updatePagesize,
  paginationChange,
});

const onEdit = (row) => {
  console.log('onEdit---row', row);
};

const onBatchDel = () => {
  console.log(
    'onBatchDel',
    tableComponentRef.value.multipleSelection.map((item) => item.id)
  );
};

const gettableDatass = async () => {
  const datas = await Promise.resolve([
    {
      id: '1',
      date: '2016-05-03',
      name: 'Tom',
      age: 18,
      gender: 'female',
      address: 'No. 189, Grove St, Los Angeles',
    },
    {
      id: '2',
      date: '2018-05-03',
      name: 'Alice',
      age: 23,
      gender: 'female',
      address: 'No. 189, Grove St, Los Angeles',
    },
    {
      id: '3',
      date: '2015-05-03',
      name: 'Jack',
      age: 20,
      gender: 'male',
      address: 'No. 189, Grove St, Los Angeles',
    },
  ]);
  tableDatas.value = datas;
  paginations.total = 200;
};
gettableDatass();
</script>
<style lang="scss" scoped></style>

编辑性表格

FormTableComponent.vue
<template>
  <div class="form_table_component">
    <slot name="addRow">
      <div style="text-align: right">
        <el-button type="primary" style="text-align: right" @click="onAddRow(tableName)">添加</el-button>
      </div>
    </slot>

    <el-form :model="form" ref="formRef">
      <el-table :data="form[tableName]" border style="width: 100%; margin: 10px 0 20px">
        <el-table-column v-for="item in columns" :key="item.prop" :label="item.label" :width="item.width" :align="item.align || 'center'">
          <template #default="{ $index, row }">
            <el-form-item :prop="tableName + '.' + $index + '.' + item.prop" :rules="item.rules">
              <!-- 暂时只封装三种类型 input select date-picker -->
              <el-input v-if="item.type == 'input'" v-model="row[item.prop]" :placeholder="item.placeholder || '请输入'"></el-input>
              <el-select v-if="item.type == 'select'" v-model="row[item.prop]" :style="{ width: '100%', display: 'flex' }" @change="(e) => item.change && item.change(e, row)">
                <!-- affectedOptions 受约束的options  -->
                <el-option :label="option.label" :value="option.value" v-for="option in item.options || row[item.affectedOptions]" :key="option.value"></el-option>
              </el-select>
              <el-date-picker
                v-if="item.type == 'date'"
                type="date"
                :placeholder="item.placeholder || '请选择'"
                :style="{ width: '100%', display: 'flex' }"
                :value-format="item.valueFormat || 'YYYY-MM-DD'"
                v-model="row[item.prop]"
              ></el-date-picker>
            </el-form-item>
          </template>
        </el-table-column>

        <el-table-column fixed="right" label="操作" :width="handlerColumnWidth" align="center">
          <template #default="scope">
            <slot name="handlerColumn" :scope="scope" :tableName="tableName">
              <el-button @click="onDelRow(scope.$index)">删除</el-button>
            </slot>
          </template>
        </el-table-column>
      </el-table>
    </el-form>
  </div>
</template>

<script setup>
const props = defineProps({
  form: {
    type: Object,
    default: () => {},
  },
  tableName: {
    type: String,
  },
  columns: {
    type: Array,
    default: () => [],
  },
  handlerColumnWidth: {
    type: Number,
    default: 100,
  },
});

const onAddRow = (tableName) => {
  props.form[tableName].push({});
};
const onDelRow = (index) => {
  props.form[props.tableName].splice(index, 1);
};

defineExpose({
  onAddRow,
  onDelRow,
});
</script>

<style lang="scss" scoped>
.form_table_component {
  ::v-deep .select-trigger {
    flex: 1;
  }
}
</style>

示例
<template>
  <h2>formTableView</h2>
  <el-form ref="ruleFormRef" :model="form" :rules="rules" label-width="120px">
    <el-form-item label="活动名称" prop="name">
      <el-input v-model="form.name" />
    </el-form-item>
    <el-form-item label="活动区域" prop="region">
      <el-select v-model="form.region" placeholder="请选择">
        <el-option label="Zone one" value="shanghai" />
        <el-option label="Zone two" value="beijing" />
      </el-select>
    </el-form-item>
  </el-form>

  <FormTableComponent ref="formTableComponentRef" :form="form" tableName="users" :columns="columns" />
  <FormTableComponent ref="formTableComponentRef2" :form="form" tableName="users2" :columns="columns2" />

  <p>
    <el-button type="primary" @click="onSubmit">提交</el-button>
  </p>
</template>

<script setup>
import FormTableComponent from '@/components/FormTableComponent.vue';
import { validateFormsByFormRefs } from '@/utils/validateForms';
import { nameCnValidator } from '@/utils/validateFormItem';
import { reactive } from 'vue';

const form = reactive({
  name: 'Hello',
  region: '',
  users: [],
  users2: [],
});

const rules = reactive({
  name: [
    { required: true, message: 'Please input Activity name', trigger: 'blur' },
    { min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
  ],
  region: [
    {
      required: true,
      message: 'Please select Activity zone',
      trigger: 'change',
    },
  ],
});

const columns = [
  { prop: 'birthDate', label: '出生日期', type: 'date' },
  {
    prop: 'name',
    label: '姓名',
    type: 'input',
    rules: [{ required: true, message: '请输入姓名', trigger: 'blur' }, { validator: nameCnValidator }],
  },
  { prop: 'age', label: '年龄', type: 'input' },
  {
    prop: 'gender',
    label: '性别',
    type: 'select',
    options: [
      { label: '男', value: 'male' },
      { label: '女', value: 'female' },
    ],
  },
];
const getCityOptions = async (pVal) => {
  const cityOptions = await new Promise((resolve) => {
    setTimeout(() => {
      if (pVal === 'shanghai') {
        return resolve([
          { label: '浦东', value: 'pudong' },
          { label: '徐汇', value: 'xuhui' },
        ]);
      } else {
        resolve([
          { label: '武汉', value: 'wuhan' },
          { label: '黄冈', value: 'huanggang' },
        ]);
      }
    }, 300);
  });
  return cityOptions;
};

// 模拟有关联列的formTable
const columns2 = ref([
  {
    prop: 'name',
    label: '姓名',
    type: 'input',
    rules: [{ required: true, message: '请输入姓名', trigger: 'blur' }, { validator: nameCnValidator }],
  },
  {
    prop: 'province',
    label: '省份',
    type: 'select',
    options: [
      { label: '上海', value: 'shanghai' },
      { label: '湖北', value: 'hubei' },
    ],
    async change(val, row) {
      row.city = '';
      const cityOptions = await getCityOptions(val);
      row.cityOptions = cityOptions;
    },
  },
  {
    prop: 'city',
    label: '城市',
    type: 'select',
    affectedOptions: 'cityOptions',
  },
]);

const ruleFormRef = ref(null);
const formTableComponentRef = ref(null);
const formTableComponentRef2 = ref(null);

const onSubmit = async () => {
  const formRef = formTableComponentRef.value.$refs.formRef;
  const formRef2 = formTableComponentRef2.value.$refs.formRef;
  await Promise.all(validateFormsByFormRefs([ruleFormRef.value, formRef, formRef2]));
  console.log('onSubmit', form);
};
</script>

<style scoped></style>

筛选表单+表格+分页

FilterFormTablePigination

<template>
  <div class="app-container component-formTable">
    <el-form :model="queryParams" ref="queryRef" :inline="true" v-if="formColumns.length" v-show="showSearch">
      <template v-for="item in formColumns" :key="item.prop">
        <el-form-item :label="item.label" :prop="item.prop" v-if="item.type == 'select'">
          <el-select
            v-model="queryParams[item.prop]"
            :placeholder="item.placeholder || '请选择'"
            style="width: 200px"
            :clearable="!item.hideClearable"
            :multiple="item.multiple"
            collapse-tags
            collapse-tags-tooltip
          >
            <el-option v-if="item.needAll" label="全部" :value="item.defaultAllVal ?? ''" />
            <DictOptions v-if="item.dictType" :dict-type="item.dictType" />
            <template v-else>
              <el-option v-for="opt in item.options" :key="opt.value" :label="opt.label" :value="opt.value" />
            </template>
          </el-select>
        </el-form-item>

        <el-form-item :label="item.label" :prop="item.prop" v-else-if="item.type === 'daterange'">
          <el-date-picker
            v-model="queryParams[item.prop]"
            type="daterange"
            range-separator="—"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            :value-format="item.valueFormat ?? 'YYYY-MM-DD'"
          />
        </el-form-item>

        <el-form-item :label="item.label" :prop="item.prop" v-else>
          <el-input
            v-model="queryParams[item.prop]"
            :placeholder="item.placeholder || '请输入' + item.label"
            :clearable="!item.hideClearable"
            :maxlength="item.maxlength || 999999"
            style="width: 200px"
            @keyup.enter="handleQuery"
          />
        </el-form-item>
      </template>
      <el-form-item>
        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
        <el-button icon="Refresh" @click="handleResetQuery">重置</el-button>
        <slot name="filterFormBtns"></slot>
      </el-form-item>
      <slot name="formItemAfter"></slot>
    </el-form>

    <!-- 对表格的操作 加入 折叠搜索面板 + 刷新当前分页 + 显示隐藏列 -->
    <div class="tableTools-wrap">
      <slot name="tableTools"></slot>
      <right-toolbar v-model:showSearch="showSearch" @queryTable="getListRequest" :columns="tableColumns"></right-toolbar>
    </div>

    <!-- 表格汇总信息 -->
    <slot name="tableBefore" :data="dataList"></slot>

    <el-table v-loading="loading" :data="dataList" style="width: 100%" :row-key="rowKey">
      <el-table-column label="序号" width="50" type="index" align="center" fixed>
        <template #default="scope">
          <span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
        </template>
      </el-table-column>

      <template v-for="item in visibleTableColumns" :key="item.prop">
        <el-table-column
          :label="item.label"
          :align="item.align || 'center'"
          :prop="item.prop"
          :show-overflow-tooltip="item.tooltip || true"
          :width="item.width || 'auto'"
          :formatter="item.formatter"
          :fixed="item.fixed || false"
        >
          <template #default="scope" v-if="!item.formatter">
            <template v-if="item.format">
              <span v-html="item.format(scope.row[item.prop], scope.row)"></span>
            </template>
            <template v-else-if="item.useDict">{{ dictColumnHandler(item.useDict, scope.row[item.prop]) }}</template>
            <template v-else-if="item.useSlot">
              <slot :name="item.prop" :row="scope.row"></slot>
            </template>
            <template v-else>
              {{ scope.row[item.prop] || '-' }}
            </template>
          </template>
        </el-table-column>
      </template>
      <!-- 操作按钮 -->
      <slot name="tableOperate"></slot>
    </el-table>
    <slot name="tableAfter" :data="dataList"></slot>

    <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize" @pagination="getListRequest" />
  </div>
</template>

<script setup name="FormTable">
import DictOptions from '../DictOptions/index.vue';
import useDict from '@/utils/dict.js';

const { proxy } = getCurrentInstance();
const showSearch = ref(true);
const props = defineProps({
  /**
   * @desc 搜索项
   */
  formColumns: {
    type: Array,
    default: [],
  },
  /**
   * @desc 表格列
   */
  tableColumns: {
    type: Array,
    default: [],
  },
  /**
   * @desc 搜索表单初始值
   */
  initForm: {
    type: Object,
    default: {},
  },
  /**
   * @desc 接口地址 默认参数
   */
  initData: {
    type: Object,
    default: {},
  },
  /**
   * @desc 接口地址
   */
  apiRequest: {
    type: Function,
    default: () => Promise.resolve({}),
  },
  /**
   * @desc id
   */
  rowKey: {
    type: [Function, String],
    default: 'id',
  },
  /**
   * @desc 接口返回的数据渲染之前 预处理
   */
  handleData: {
    type: Function,
    default: null,
  },

  /**
   * @desc queryParams 处理器
   */
  queryParamsHandler: {
    type: Function,
    default: () => {},
  },
});
const loading = ref(true);
const total = ref(0);
const pageNum = ref(1);
const pageSize = ref(10);
const dataList = ref([]);
const queryParams = ref({ ...props.initForm });
const visibleTableColumns = computed(() => props.tableColumns.filter(column => !column.hide));

// dict表格列处理器
function dictColumnHandler(dictType, val) {
  if (!val && val !== 0) {
    return '-';
  }
  const dicts = useDict(dictType)[dictType]?.value ?? [];
  return dicts.find(dict => dict.value === val)?.label ?? '-';
}

/** 查询列表 */
async function getListRequest() {
  loading.value = true;
  await props.queryParamsHandler(queryParams.value);
  const params = {
    ...props.initData,
    ...queryParams.value,
    pageNum: pageNum.value,
    pageNo: pageNum.value, // 兼容后端接口需要 pageNo 字段
    pageSize: pageSize.value,
  };
  console.log('getListRequest---params', params);
  props
    .apiRequest(params)
    .then(response => {
      let rows = response.rows || response.data || [];
      if (typeof props.handleData == 'function') {
        rows = props.handleData(rows);
      }
      dataList.value = rows;
      total.value = response.total || 0;
      loading.value = false;
    })
    .catch(err => {
      dataList.value = [];
      loading.value = false;
    });
}

/** 搜索按钮操作 */
function handleQuery() {
  pageNum.value = 1;
  getListRequest();
}

/** 重置按钮操作 */
function handleResetQuery() {
  proxy.resetForm('queryRef');
  handleQuery();
}

getListRequest();

defineExpose({
  getListRequest,
  // 获取筛选项参数
  getFilterParams: () => ({
    ...props.initData,
    ...queryParams.value,
  }),
  // 获取表格数据
  getDataList: () => dataList.value,
  // 初始化搜索
  handleQuery,
});
</script>

<style scoped lang="scss">
.component-formTable {
  .tableTools-wrap {
    display: flex;
    justify-content: flex-end;
    margin-bottom: 12px;
    .top-right-btn {
      margin-left: 12px;
    }
  }
}
</style>

DictOptions

<template>
  <el-option v-for="opt in options" :key="opt.value" :label="opt.label" :value="opt.value" />
</template>

<script setup>
import useDict from '@/utils/dict.js';

const props = defineProps({
  dictType: {
    type: String,
    default: '',
  },
});

const options = computed(() => {
  if (!props.dictType) return [];
  const dicts = useDict(props.dictType);
  try {
    return dicts[props.dictType].value;
  } catch (error) {
    return [];
  }
});
</script>

<style scoped>
.el-tag + .el-tag {
  margin-left: 10px;
}
</style>

utils/dict.js

import useDictStore from '@/store/modules/dict';
import { getDicts } from '@/api/system/dict/data';

/**
 * 获取字典数据
 */
export default function useDict(...args) {
  const res = ref({});
  return (() => {
    args.forEach((dictType, index) => {
      res.value[dictType] = [];
      const dicts = useDictStore().getDict(dictType);
      if (dicts) {
        res.value[dictType] = dicts;
      } else {
        useDictStore().setDict(dictType, []);
        getDicts(dictType).then(resp => {
          res.value[dictType] = resp.data.map(p => ({
            label: p.dictLabel,
            value: p.dictValue,
            elTagType: p.listClass,
            elTagClass: p.cssClass,
          }));
          useDictStore().setDict(dictType, res.value[dictType]);
        });
      }
    });
    return toRefs(res.value);
  })();
}

@/store/modules/dict

const useDictStore = defineStore('dict', {
  state: () => ({
    dict: [], // [ { key:"is_no", value:[{ label:"是", value:"1" }, { label:"否", value:"0" }] } ]
  }),
  actions: {
    // 获取字典
    getDict(_key) {
      if (_key == null && _key == '') {
        return null;
      }
      try {
        for (let i = 0; i < this.dict.length; i++) {
          if (this.dict[i].key == _key) {
            return this.dict[i].value;
          }
        }
      } catch (e) {
        return null;
      }
    },
    // 设置字典
    setDict(_key, value) {
      if (_key !== null && _key !== '') {
        // 若已存在就更新
        const index = this.dict.findIndex(item => item.key === _key);
        if (index !== -1) {
          this.dict.splice(index, 1);
        }
        this.dict.push({
          key: _key,
          value,
        });
      }
    },
    // 删除字典
    removeDict(_key) {
      let bln = false;
      try {
        for (let i = 0; i < this.dict.length; i++) {
          if (this.dict[i].key == _key) {
            this.dict.splice(i, 1);
            return true;
          }
        }
      } catch (e) {
        bln = false;
      }
      return bln;
    },
    // 清空字典
    cleanDict() {
      this.dict = [];
    },
    // 初始字典
    initDict() {},
  },
});

export default useDictStore;

RightToolbar

<template>
  <div class="top-right-btn" :style="style">
    <el-row>
      <el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
        <el-button circle :icon="showSearch ? 'CaretTop' : 'CaretBottom'" @click="toggleSearch()" />
      </el-tooltip>
      <el-tooltip class="item" effect="dark" content="刷新当前页" placement="top">
        <el-button circle icon="Refresh" @click="refresh()" />
      </el-tooltip>
      <el-tooltip class="item" effect="dark" content="显示隐藏表格列" placement="top" v-if="columns">
        <el-button circle icon="Menu" @click="showColumn()" v-if="showColumnsType == 'transfer'" />
        <el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
          <el-button circle icon="Menu" />
          <template #dropdown>
            <el-dropdown-menu>
              <template v-for="item in columns" :key="item.key">
                <el-dropdown-item>
                  <el-checkbox :checked="!item.hide" @change="checkboxChange($event, item.label)" :label="item.label" />
                </el-dropdown-item>
              </template>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </el-tooltip>
    </el-row>
    <el-dialog :title="title" v-model="open" append-to-body>
      <el-transfer :titles="['显示', '隐藏']" v-model="value" :data="columns" @change="dataChange"></el-transfer>
    </el-dialog>
  </div>
</template>

<script setup>
const props = defineProps({
  /* 是否显示检索条件 */
  showSearch: {
    type: Boolean,
    default: true,
  },
  /* 显隐列信息 */
  columns: {
    type: Array,
  },
  /* 是否显示检索图标 */
  search: {
    type: Boolean,
    default: true,
  },
  /* 显隐列类型(transfer穿梭框、checkbox复选框) */
  showColumnsType: {
    type: String,
    default: 'checkbox',
  },
  /* 右外边距 */
  gutter: {
    type: Number,
    default: 10,
  },
});

const emits = defineEmits(['update:showSearch', 'queryTable']);

// 显隐数据
const value = ref([]);
// 弹出层标题
const title = ref('显示/隐藏');
// 是否显示弹出层
const open = ref(false);

const style = computed(() => {
  const ret = {};
  if (props.gutter) {
    ret.marginRight = `${props.gutter / 2}px`;
  }
  return ret;
});

// 搜索
function toggleSearch() {
  emits('update:showSearch', !props.showSearch);
}

// 刷新
function refresh() {
  emits('queryTable');
}

// 右侧列表元素变化
function dataChange(data) {
  for (const item in props.columns) {
    const { key } = props.columns[item];
    props.columns[item].hide = !data.includes(key);
  }
}

// 打开显隐列dialog
function showColumn() {
  open.value = true;
}

if (props.showColumnsType == 'transfer') {
  // 显隐列初始默认隐藏列
  for (const item in props.columns) {
    if (props.columns[item].hide === true) {
      value.value.push(parseInt(item));
    }
  }
}

// 勾选
function checkboxChange(event, label) {
  props.columns.filter(item => item.label == label)[0].hide = !event;
}
</script>

<style lang="scss" scoped>
:deep(.el-transfer__button) {
  border-radius: 50%;
  display: block;
  margin-left: 0px;
}
:deep(.el-transfer__button:first-child) {
  margin-bottom: 10px;
}
:deep(.el-dropdown-menu__item) {
  line-height: 30px;
  padding: 0 17px;
}
</style>

Pagination

<template>
  <div :class="{ hidden: hidden }" class="pagination-container">
    <el-pagination
      :background="background"
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :layout="layout"
      :page-sizes="pageSizes"
      :pager-count="pagerCount"
      :total="total"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />
  </div>
</template>

<script setup>
import scrollTo from '@/utils/scroll-to';

const props = defineProps({
  total: {
    required: true,
    type: Number,
  },
  page: {
    type: Number,
    default: 1,
  },
  limit: {
    type: Number,
    default: 20,
  },
  pageSizes: {
    type: Array,
    default() {
      return [10, 20, 30, 50];
    },
  },
  // 移动端页码按钮的数量端默认值5
  pagerCount: {
    type: Number,
    default: document.body.clientWidth < 992 ? 5 : 7,
  },
  layout: {
    type: String,
    default: 'total, sizes, prev, pager, next, jumper',
  },
  background: {
    type: Boolean,
    default: true,
  },
  autoScroll: {
    type: Boolean,
    default: true,
  },
  hidden: {
    type: Boolean,
    default: false,
  },
});

const emit = defineEmits();
const currentPage = computed({
  get() {
    return props.page;
  },
  set(val) {
    emit('update:page', val);
  },
});
const pageSize = computed({
  get() {
    return props.limit;
  },
  set(val) {
    emit('update:limit', val);
  },
});
function handleSizeChange(val) {
  if (currentPage.value * val > props.total) {
    currentPage.value = 1;
  }
  emit('pagination', { page: currentPage.value, limit: val });
  if (props.autoScroll) {
    scrollTo(0, 800);
  }
}
function handleCurrentChange(val) {
  emit('pagination', { page: val, limit: pageSize.value });
  if (props.autoScroll) {
    scrollTo(0, 800);
  }
}
</script>

<style scoped>
.pagination-container {
  background: #fff;
  padding: 16px 20px;
  height: initial;
  line-height: initial;
}
.pagination-container.hidden {
  display: none;
}
</style>

使用示例

<template>
  <div class="page-channelAgencyInfo-ppl">
    <FormTable
      ref="formTableRef"
      :formColumns="formColumnsClone"
      :tableColumns="tableColumnsClone"
      :apiRequest="clueNewestPPLListRequest"
      :queryParamsHandler="queryParamsHandler"
    >
      <template #tableTools>
        <el-button type="primary" @click="onExport" icon="Download">导出</el-button>
      </template>
      <template #tableOperate>
        <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200" :fixed="'right'">
          <template #default="{ row }">
            <el-button link type="primary" @click="onShowPPLHistoryDialog(row)">更新</el-button>
          </template>
        </el-table-column>
      </template>
    </FormTable>
    <PPLHistoryDialog ref="pplHistoryDialogRef" @handleQuery="handleQuery"></PPLHistoryDialog>
  </div>
</template>

<script setup>
import { formColumns, tableColumns } from './ppl.js';
import { deepClone } from '@/utils/index.js';
import { clueNewestPPLListRequest } from '@/api/channel-agency/ppl.js';
import PPLHistoryDialog from './ppl-component/pplHistoryDialog/index.vue';

const { proxy } = getCurrentInstance();
const formColumnsClone = deepClone(formColumns);
const tableColumnsClone = ref(deepClone(tableColumns));
const pplHistoryDialogRef = ref(null);
const formTableRef = ref(null);

// 将 创建时间 处理成后端需要的字段
const queryParamsHandler = queryParams => {
  if (Array.isArray(queryParams.createdDate)) {
    queryParams.startTime = queryParams.createdDate[0];
    queryParams.endTime = queryParams.createdDate[1];
  } else {
    delete queryParams.startTime;
    delete queryParams.endTime;
  }
};

const onShowPPLHistoryDialog = (row = {}) => {
  pplHistoryDialogRef.value.showPPLHistoryDialog(row);
};

// 导出列表数据
const onExport = async () => {
  const filterParams = formTableRef.value.getFilterParams();
  await proxy.download('tamper/institution/clue/newestExport', filterParams, `ppl.xlsx`);
};

// 刷新列表
const handleQuery = async () => {
  formTableRef.value.handleQuery();
};
</script>

<style lang="scss"></style>

formColumns tableColumns

export const clueStatusOptions = [
  { label: '全部', value: '' },
  { label: '待审核', value: '0' },
  { label: '正常', value: '1' },
];

export const clueTypeOptions = [
  { label: '线索', value: '0' },
  { label: '机构', value: '1' },
];

export const formColumns = [
  {
    label: '渠道名称',
    prop: 'channelName',
    type: 'text',
  },
  {
    label: '机构真实名称',
    prop: 'institutionName',
    type: 'text',
  },
  {
    label: '区域经理',
    prop: 'regionalManager',
    type: 'text',
  },
  {
    label: '状态',
    prop: 'clueStatus',
    type: 'select',
    options: clueStatusOptions,
  },
  {
    label: '机构编码',
    prop: 'channelCode',
    type: 'text',
  },
  {
    label: '最后跟进时间',
    prop: 'createdDate',
    type: 'daterange',
  },
  {
    label: '合作进度',
    prop: 'progressList',
    type: 'select',
    multiple: true,
    dictType: 'cooperation_progress',
  },
];

export const tableColumns = [
  {
    label: '渠道编码',
    prop: 'channelCode',
    formatter(row) {
      return row.channelCode || '未生成';
    },
  },
  {
    label: '渠道线索名称',
    prop: 'channelName',
  },
  {
    label: '机构真实名称',
    prop: 'institutionName',
    formatter(row) {
      return row.institutionName || '待补充';
    },
  },
  {
    label: '区域经理',
    prop: 'regionalManager',
  },
  {
    label: '工号',
    prop: 'managerId',
  },
  {
    label: '创建时间',
    prop: 'createTime',
  },
  {
    label: '合作进度',
    prop: 'cooperationProgress',
    useDict: 'cooperation_progress',
  },
  {
    label: '最后跟进时间',
    prop: 'lastUpdateTime',
  },
];

export const tableColumns = [
  {
    label: '渠道编码',
    prop: 'channelCode',
    formatter(row) {
      return row.channelCode || '未生成';
    },
  },
  {
    label: '渠道线索名称',
    prop: 'channelName',
  },
  {
    label: '机构真实名称',
    prop: 'institutionName',
    formatter(row) {
      return row.institutionName || '待补充';
    },
  },
  {
    label: '区域经理',
    prop: 'regionalManager',
  },
  {
    label: '工号',
    prop: 'managerId',
  },
  {
    label: '合作进度',
    prop: 'cooperationProgress',
    useDict: 'cooperation_progress',
  },
  {
    label: '最新PPL',
    prop: 'newestPPL',
  },
  {
    label: '最后更新时间',
    prop: 'lastUpdateTime',
  },
  {
    label: '备注',
    prop: 'remark',
  },
];

便捷性表单

MyForm

<template>
  <el-form status-icon ref="formRef" :model="formData" :rules="rules" class="component-form" :label-width="labelWidth" :disabled="disabled">
    <el-row>
      <template v-for="item in formItemList" :key="item.prop">
        <el-col :span="item.colSpan ?? 24" v-if="!item.hide">
          <el-form-item :prop="item.prop" :label="item.label" :label-width="item.labelWidth">
            <slot :name="item.prop" :value="formData[item.prop]" :formData="formData">
              <el-input
                v-model.number="formData[item.prop]"
                type="number"
                v-if="['number'].includes(item.type)"
                :placeholder="item.placeholder || '请输入数值'"
              />
              <span v-else-if="['text'].includes(item.type)">{{ formData[item.prop] }}</span>
              <component
                v-else
                :disabled="item.disabled"
                :is="typeComponentMap.get(item.type).is"
                v-bind="typeComponentMap.get(item.type).attrs"
                v-model.trim="formData[item.prop]"
                :clearable="!item.hideClearable"
                :multiple="['select'].includes(item.type) && item.multiple"
                @change="val => item.changeCb && item.changeCb(val)"
                :placeholder="item.placeholder || (['input', 'textarea'].includes(item.type) ? '请输入' : '请选择') + item.label"
              >
                <template v-if="['select', 'radio', 'checkbox'].includes(item.type)">
                  <DictOptions v-if="item.dictType" :dict-type="item.dictType" />
                  <component
                    v-else
                    :is="typeComponentMap.get(item.type).child"
                    v-for="option in item.options"
                    :key="option.value"
                    :label="option.label"
                    :value="option.value"
                  ></component>
                </template>
              </component>
            </slot>
          </el-form-item>
        </el-col>
      </template>
    </el-row>
    <el-form-item>
      <slot name="btns" :formData="formData" :onSubmit="onSubmit">
        <!-- <el-button type="primary" @click="onSubmit">提交</el-button> -->
      </slot>
    </el-form-item>
  </el-form>
</template>
<script>
export default {
  components: { ElInput, ElSelect, ElRadioGroup, ElCheckboxGroup, ElDatePicker, ElOption, ElRadio, ElCheckbox },
};
</script>

<script setup>
const props = defineProps({
  rules: {
    type: Object,
    default: () => ({}),
  },
  labelWidth: {
    type: String,
    default: '140px',
  },
  // 表单项配置
  formItemList: {
    type: Array,
    default: () => [],
  },
  // 禁用表单
  disabled: {
    type: Boolean,
    default: false,
  },
  // 初始化数据
  initFormData: {
    type: Object,
    default: () => ({}),
  },
});
const formData = reactive(props.initFormData);
const formRef = ref(null);
const typeComponentMap = new Map([
  ['input', { is: 'el-input', attrs: {} }],
  ['select', { is: 'el-select', attrs: {}, child: 'el-option' }],
  ['radio', { is: 'el-radio-group', attrs: {}, child: 'el-radio' }],
  ['checkbox', { is: 'el-checkbox-group', attrs: {}, child: 'el-checkbox' }],
  ['daterange', { is: 'el-date-picker', attrs: { type: 'daterange', 'value-format': 'YYYY-MM-DD' } }],
  ['year', { is: 'el-date-picker', attrs: { type: 'year', 'value-format': 'YYYY' } }],
  ['textarea', { is: 'el-input', attrs: { type: 'textarea' } }],
  ['datetime', { is: 'el-date-picker', attrs: { type: 'datetime', 'value-format': 'YYYY-MM-DD HH:mm:ss' } }],
]);

const onSubmit = async () => {
  console.log('formData', formData);
  await formRef.value.validate();
  console.log('passed');
  return { ...formData };
};
const resetFields = () => {
  formRef.value?.resetFields();
};
defineExpose({
  onSubmit,
  resetFields,
});
</script>
<style lang="scss">
.component-form {
  /* 针对WebKit浏览器,比如Chrome、Safari */
  input[type='number']::-webkit-inner-spin-button,
  input[type='number']::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
  }

  /* 针对Firefox浏览器 */
  input[type='number'] {
    -moz-appearance: textfield;
  }
}
</style>

使用示例

<template>
  <el-dialog v-model="createTalkDialogVisible" title="新增沟通跟进" width="900">
    <h3>基本信息</h3>
    <p class="header-p">
      <span>
        <em>渠道名称:</em>
        {{ rowInfo.channelName }}
      </span>
      <span>
        <em>机构真实名称:</em>
        {{ rowInfo.institutionName || '未补充' }}
      </span>
      <span>
        <em>机构编码:</em>
        {{ rowInfo.channelCode || '未生成' }}
      </span>
    </p>
    <h3>沟通信息</h3>
    <MyForm :formItemList="formItemList" :rules="formRules" ref="formRef" :initFormData="initFormData"></MyForm>
    <footer>
      <el-button type="primary" @click="onSubmit">确认新增</el-button>
    </footer>
  </el-dialog>
</template>

<script setup>
import { formItemConfigs, formRules } from './index.js';
import { clueInsertCommunicateRequest } from '@/api/channel-agency/talk.js';
import useUserStore from '@/store/modules/user';

const emit = defineEmits(['handleQuery']);
const userStore = useUserStore();
const formRef = ref(null);
const createTalkDialogVisible = ref(false);
const rowInfo = reactive({});
const formItemList = ref(formItemConfigs);
const initFormData = reactive({});

const handleQuery = () => {
  createTalkDialogVisible.value = false;
  emit('handleQuery');
};

const showCreateTalkDialog = async row => {
  Object.assign(rowInfo, row);
  createTalkDialogVisible.value = true;
  formRef.value?.resetFields();
};

const onSubmit = async () => {
  const formData = await formRef.value.onSubmit();
  await clueInsertCommunicateRequest({ clueId: rowInfo.clueId, ...formData, creator: userStore.nickName });
  formRef.value?.resetFields();
  handleQuery();
};

defineExpose({
  showCreateTalkDialog,
});
</script>

<style lang="scss" scoped>
.header-p {
  span {
    margin-right: 40px;
  }
}
footer {
  display: flex;
  justify-content: center;
}
</style>

配置项

import { chineseEnglishNumRule, mobileRule, lengthLimitRule } from '@/utils/formRules.js';

export const communicateMethodOptions = [
  { label: '面谈', value: 'face' },
  { label: '电话沟通', value: 'tel' },
];

export const formItemConfigs = [
  { prop: 'communicateTime', label: '沟通时间', type: 'datetime', colSpan: 12 },
  { prop: 'communicateMethod', label: '沟通方式', type: 'select', colSpan: 12, options: communicateMethodOptions },
  { prop: 'communicateObj', label: '沟通对象', type: 'input', colSpan: 8 },
  { prop: 'communicatePhone', label: '沟通对象手机号', type: 'input', colSpan: 8 },
  { prop: 'communicatePost', label: '沟通对象岗位', type: 'input', colSpan: 8 },
  { prop: 'communicateContent', label: '沟通记录', type: 'textarea' },
  { prop: 'requiredHelp', label: '所需支持', type: 'textarea' },
];

export const formRules = {
  communicateTime: [{ required: true, message: '请选择', trigger: 'blur' }],
  communicateMethod: [{ required: true, message: '请选择', trigger: 'blur' }],
  openingBankAccount: [{ required: true, message: '请输入', trigger: 'blur' }],
  communicatePhone: [mobileRule()],
  communicateObj: [{ required: true, message: '请输入', trigger: 'blur' }, chineseEnglishNumRule(), lengthLimitRule({ maxLength: 50 })],
  communicatePost: [{ required: true, message: '请输入', trigger: 'blur' }, chineseEnglishNumRule(), lengthLimitRule({ maxLength: 50 })],
  communicateContent: [{ required: true, message: '请输入', trigger: 'blur' }, lengthLimitRule()],
  requiredHelp: [lengthLimitRule()],
};