vue3如何封装自定义表单以及如何成为搜索框和table联动

641 阅读3分钟

1.Form的封装

疯转思想:原组件不单个传递props,传递的参数与原组件保持一直

v-bind="item.fiedProps"的方式可以接受传递过来的数据

inheritAttrs: false, v-bind="$attrs" 父组件的值和组件保持一直

<template>
  <div class="custom-form-wrap p20">
    <!-- form表单 -->
    <a-form :model="formValue" v-bind="$attrs" ref="formRef">
      <a-row :gutter="16">
        <a-col :span="item.span" v-for="item in option" :key="item.id">
          <a-form-item v-bind="item">
            <!--  event: {
      change: (value) => {
        console.log('change:', value);
      },
    }, -->
            <component
              v-if="componentsMap[item.type] !== 'slot' && componentsMap[item.type] !== 'UploadImage'"
              :is="componentsMap[item.type]"
              v-model="formValue[item.field]"
              v-bind="item.fiedProps"
              v-on="
                Object.keys(item.event || {}).reduce((acc, key) => {
                  acc[key] = (value) => {
                    item.event[key](value);
                  };
                  return acc;
                }, {})
              "
            />
            <template v-else-if="item.type === 'UploadImage'">
              <MyUploadImage
                v-model:fileList="formValue[item.field]"
                :v-bind="item.fiedProps"
                v-on="
                  Object.keys(item.event || {}).reduce((acc, key) => {
                    acc[key] = (value) => {
                      item.event[key](value);
                    };
                    return acc;
                  }, {})
                "
              />
            </template>
            <template v-else-if="item.type === 'slot'">
              <slot name="customSlot" :fieldName="item.field" :fiedProps="item.fiedProps" />
            </template>
          </a-form-item>
        </a-col>
      </a-row>
    </a-form>
  </div>
</template>

<script setup lang="jsx">
import { computed, reactive, ref, watch } from 'vue';
import { componentsMap } from '@/config/index';
import MyUploadImage from './MyUploadImage.vue';
const props = defineProps({
  fiedOptions: {
    type: Array,
    default: () => [],
  },
  formValue: {
    type: Object,
    default: () => ({}),
  },
  inheritAttrs: false,
});

watch(
  () => props.fiedOptions,
  (newVal) => {
    console.log('newVal:', newVal);
  },
  {
    deep: true,
  },
);

const $emit = defineEmits(['update:formValue', 'validateFaild', 'validateSuccess']);
const formRef = ref(null);
const option = computed(() => {
  return props.fiedOptions.map((item, index) => ({
    ...item,
    id: index,
  }));
});
const handleSubmit = () => {
  console.log('formRef:', formRef.value);
  formRef.value.validate((errors) => {
    if (errors) {
      $emit('validateFaild', errors);
      return;
    }
    $emit('validateSuccess', props.formValue);
    $emit('update:formValue', props.formValue);
  });
};
const handleReset = () => {
  $emit('update:formValue', {});
  formRef.value.resetFields();
};
const handleClearValidate = () => {
  formRef.value.clearValidate();
};

defineExpose({
  handleSubmit,
  handleReset,
  handleClearValidate,
});
</script>

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


2.form表单的使用

<template>
  <div class="form-demo-wrap">
    <MyForm
      ref="myFormRefs"
      v-model:formValue="formValue"
      @validateSuccess="handleValidateSuccess"
      @validateFaild="handleValidateFaild"
      :fiedOptions="fiedOptions"
      layout="horizontal"
      label-align="left"
      auto-label-width
      scroll-to-first-error
    >
      <template #customSlot="{ fieldName }">
        <!-- a-input -->
        <a-input v-model="formValue[fieldName]" placeholder="Enter your name" />
      </template>
    </MyForm>
    <div class="submit">
      <a-button type="primary" html-type="submit" @click="myFormRefs.handleSubmit()">提交</a-button>
      <!-- 重置 -->
      <a-button @click="myFormRefs.handleReset()">重置</a-button>
    </div>
  </div>
</template>

<script setup>
/**
 * @file MyForm.vue
 * @description 这是一个动态表单组件,支持多种表单项类型,包括输入框、选择框、日期选择器、复选框、单选框、文本域和插槽。
 *
 * @props {Array} fiedOptions - 表单项配置数组,每个表单项包含以下属性:
 *   @property {String} field - 表单项字段名
 *   @property {String} label - 表单项标签
 *   @property {String} type - 表单项类型,可选值:'input'、'select'、'date'、'checkbox'、'radio'、'textarea'、'slot'
 *   @property {Object} fiedProps
 *   @property {String} [fiedProps.placeholder] - 表单项占位符
 *   @property {Array} [fiedProps.options] - 表单项选项数组,仅在类型为'select'、'checkbox'、'radio'时有效
 *   .......
 *   @property {Array} [rules] - 表单项验证规则
 *   @property {Number} span - 表单项所占列数
 *
 * @props {String} formLayout - 表单布局方式,默认值为'vertical'
 * @props {Object} formValue - 表单初始值对象(双向绑定)
 *
 * @emits update:formValue - 表单值更新事件
 * @emits validateFaild - 表单验证失败事件
 * @emits validateSuccess - 表单验证成功事件
 *
 * @methods handleSubmit - 提交表单方法,触发表单验证并根据验证结果触发相应事件
 * @methods handleReset - 重置表单方法,清空表单数据并重置验证状态
 *
 * @example
 * <MyForm
 *   :fiedOptions="fieldOptions"
 *   :formLayout="'horizontal'"
 *   :formValue="formValue"
 *   @update:formValue="handleFormValueUpdate"
 *   @validateFaild="handleValidateFaild"
 *   @validateSuccess="handleValidateSuccess"
 * />
 */
import { ref } from 'vue';
import MyForm from '@/components/MyForm.vue';
const fiedOptions = [
  {
    field: 'name',
    label: 'Name',
    type: 'input',
    fiedProps: {
      placeholder: '请输入内容',
    },
    rules: [{ required: true, message: 'Please input your name', trigger: 'change' }],
    span: 12,
  },
  {
    field: 'select',
    label: 'Select',
    type: 'select',
    fiedProps: {
      placeholder: 'Please select',
      options: [
        { label: 'Option 1', value: '1' },
        { label: 'Option 2', value: '2' },
      ],
    },

    rules: [{ required: true, message: 'Please select', trigger: 'change' }],
    span: 12,
  },
  {
    field: 'date',
    label: 'Date',
    type: 'date',
    fiedProps: {
      placeholder: 'Select date',
    },
    rules: [{ required: true, message: 'Please select date', trigger: 'change' }],
    span: 12,
  },
  {
    field: 'checkbox',
    label: 'Checkbox',
    type: 'checkbox',
    fiedProps: {
      options: [
        { label: 'Apple', value: 'Apple' },
        { label: 'Pear', value: 'Pear' },
        { label: 'Orange', value: 'Orange' },
      ],
    },

    rules: [{ required: true, message: 'Please select at least one', trigger: 'change' }],
    span: 12,
  },
  {
    field: 'radio',
    label: 'Radio',
    type: 'radio',
    fiedProps: {
      options: [
        { label: 'Apple', value: 'Apple' },
        { label: 'Pear', value: 'Pear' },
        { label: 'Orange', value: 'Orange' },
      ],
    },

    rules: [{ required: true, message: 'Please select one', trigger: 'change' }],
    span: 12,
  },
  {
    field: 'textarea',
    label: 'Textarea',
    type: 'textarea',
    fiedProps: {
      placeholder: 'Enter something...',
    },
    rules: [{ required: true, message: 'Please input something', trigger: 'blur' }],
    span: 12,
  },
  {
    field: 'customSlot',
    label: 'CustomSlot',
    type: 'slot',
    span: 12,
    rules: [{ required: true, message: 'Please input something', trigger: 'blur' }],
  },
];
const myFormRefs = ref(null);
const formValue = ref({
  name: '',
  select: '',
  date: '',
  checkbox: [],
  radio: '',
  textarea: '',
  slot: '',
});

const handleValidateFaild = (error) => {
  console.log(error);
};
const handleValidateSuccess = (value) => {
  console.log(value);
};
</script>

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

3.基本用法

<template>
  <MyForm
    :fiedOptions="fiedOptions"
    v-model:formValue="formValue"
    @validateSuccess="onValidateSuccess"
    @validateFaild="onValidateFaild"
  />
</template>

<script setup>
import { ref } from 'vue';
import MyForm from './MyForm.vue';

const fiedOptions = ref([
  {
    field: 'name',
    type: 'input',
    label: '姓名',
    span: 12,
    fiedProps: {
      placeholder: '请输入姓名',
    },
    event: {
      change: (value) => {
        console.log('姓名改变:', value);
      },
    },
  },
  {
    field: 'age',
    type: 'input',
    label: '年龄',
    span: 12,
    fiedProps: {
      placeholder: '请输入年龄',
    },
  },
]);

const formValue = ref({
  name: '',
  age: '',
});

const onValidateSuccess = (value) => {
  console.log('验证成功:', value);
};

const onValidateFaild = (errors) => {
  console.log('验证失败:', errors);
};
</script>

4.插槽用法

<template>
  <MyForm
    :fiedOptions="fiedOptions"
    v-model:formValue="formValue"
  >
    <template #customSlot="{ fieldName, fiedProps }">
      <a-input v-model="formValue[fieldName]" v-bind="fiedProps" />
    </template>
  </MyForm>
</template>

<script setup>
import { ref } from 'vue';
import MyForm from './MyForm.vue';

const fiedOptions = ref([
  {
    field: 'customField',
    type: 'slot',
    label: '自定义字段',
    span: 24,
    fiedProps: {
      placeholder: '请输入自定义字段',
    },
  },
]);

const formValue = ref({
  customField: '',
});
</script>

5.table和form的组合使用

<template>
  <div class="custom-table-wrap g-content g-content-footer pt22 pb22 pl28 pr28">
    <MyForm
      ref="myFormRefs"
      v-model:formValue="searchParam"
      @validateSuccess="handleValidateSuccess"
      @validateFaild="handleValidateFaild"
      :fiedOptions="fiedOptions"
      layout="horizontal"
      label-align="left"
      auto-label-width
      scroll-to-first-error
    >
      <template #customSlot="{ fieldName }">
        <!-- a-input -->
        <a-input v-model="searchParam[fieldName]" placeholder="Enter your name" />
      </template>
    </MyForm>
    <div style="text-align: right" class="mb10">
      <a-button type="primary" @click="search">Search</a-button>
      <a-button @click="reset">Reset</a-button>
    </div>
    <div class="org-table-wrap">
      <a-table :loading="loading" :data="tableData" :columns="columns" :bordered="false" :pagination="false" class="arco-none-border" />
    </div>
    <div class="g-footer">
      <MyPagination
        v-if="tableData.length"
        :total="pagination.count"
        :current-page="pagination.page_num"
        :page-size="pagination.page_size"
        @handleSizeChange="handleSizeChange"
        @handleChange="handleCurrentChange"
      ></MyPagination>
    </div>
  </div>
</template>

<script setup lang="jsx">
import { watch, toRefs, ref } from 'vue';
import MyPagination from '@/components/pagination.vue';
import { useTable } from '@/hooks/useTable';
import MyForm from '@/components/MyForm.vue';
import api from '@/api';
const $api = (req) => api.post('/api/employee/query-employee', req);

const { searchParam, pagination, tableData, loading, handleCurrentChange, handleSizeChange, search, reset } = useTable($api);

const columns = [
  {
    title: '部门',
    dataIndex: 'department_name',
    key: 'department_name',
    align: 'center',
    render: ({ text, record }) => {
      return record?.department_name || '-';
    },
  },
  {
    title: '员工姓名',
    dataIndex: 'employee_name',
    key: 'employee_name',
    align: 'center',
    render: ({ text, record }) => {
      return record?.employee_name || '-';
    },
  },
  {
    title: '登录手机号',
    dataIndex: 'employee_phone',
    key: 'employee_phone',
    align: 'center',
    render: ({ text, record }) => {
      return record?.employee_phone || '-';
    },
  },
  {
    title: '性别',
    dataIndex: 'employee_sex',
    key: 'employee_sex',
    align: 'center',
    render: ({ text, record }) => {
      return record?.employee_sex || '-';
    },
  },
  {
    title: '服务角色',
    dataIndex: 'service_role_list',
    key: 'service_role_list',
    align: 'center',
    render: ({ text, record }) => {
      return record.service_role_list.map((item) => item.role_name).join('、');
    },
  },
  {
    title: '企业微信号',
    dataIndex: 'cpwx_userid',
    key: 'cpwx_userid',
    align: 'center',
    render: ({ text, record }) => {
      return record?.cpwx_userid || '-';
    },
  },
  {
    title: '备注',
    dataIndex: 'remark',
    key: 'remark',
    align: 'center',
    render: ({ text, record }) => {
      return record?.remark || '-';
    },
  },
  {
    title: '状态',
    dataIndex: 'employee_status',
    key: 'employee_status',
    align: 'center',
    render: ({ text, record }) => {
      return record.employee_status ? '启用' : '禁用';
    },
  },
  {
    title: '操作',
    dataIndex: 'operation',
    key: 'operation',
    align: 'center',
    render: ({ text, record }) => {
      return (
        <a-button onClick={() => handleEdit(record)} type="text">
          编辑
        </a-button>
      );
    },
  },
];
const handleEdit = (record) => {
  console.log(record);
  searchParam.value.employee_id = record.id;
  search();
};
const fiedOptions = [
  {
    field: 'name',
    label: 'Name',
    type: 'input',
    fiedProps: {
      placeholder: '请输入内容',
    },
    span: 12,
  },
  {
    field: 'select',
    label: 'Select',
    type: 'select',
    fiedProps: {
      placeholder: 'Please select',
      options: [
        { label: 'Option 1', value: '1' },
        { label: 'Option 2', value: '2' },
      ],
    },

    span: 12,
  },
  {
    field: 'date',
    label: 'Date',
    type: 'date',
    fiedProps: {
      placeholder: 'Select date',
    },
    span: 12,
  },
  {
    field: 'checkbox',
    label: 'Checkbox',
    type: 'checkbox',
    fiedProps: {
      options: [
        { label: 'Apple', value: 'Apple' },
        { label: 'Pear', value: 'Pear' },
        { label: 'Orange', value: 'Orange' },
      ],
    },

    span: 12,
  },
  {
    field: 'radio',
    label: 'Radio',
    type: 'radio',
    fiedProps: {
      options: [
        { label: 'Apple', value: 'Apple' },
        { label: 'Pear', value: 'Pear' },
        { label: 'Orange', value: 'Orange' },
      ],
    },

    span: 12,
  },
  {
    field: 'textarea',
    label: 'Textarea',
    type: 'textarea',
    fiedProps: {
      placeholder: 'Enter something...',
    },
    span: 12,
  },
  {
    field: 'customSlot',
    label: 'CustomSlot',
    type: 'slot',
    span: 12,
  },
];
const myFormRefs = ref(null);
// const formValue = ref({
//   name: '123',
//   select: '1',
//   date: '',
//   checkbox: [],
//   radio: '',
//   textarea: '',
//   slot: '',
// });

const handleValidateFaild = (error) => {
  console.log(error);
};
const handleValidateSuccess = (value) => {
  console.log(value);
};
</script>

<style scoped lang="less"></style>

4.附赠useTable hooks 真的香

import { ref, onMounted, toRefs } from 'vue';
/**
 * @description table 页面表格操作方法封装
 * @param {Function} api 获取表格数据 api 方法(必传)
 * @param {Object} initParam 获取数据初始化参数(非必传,默认为{}) //一直存在的参数
 * @param {Boolean} isPageable 是否有分页(非必传,默认为true)
 * @param {Boolean} isAuto 是否自动请求(非必传,默认为true)
 */
export const useTable = (api, initParam = {}, isPageable = true, isAuto = true) => {
  const state = ref({
    tableData: [],
    pagination: {
      page_num: 1,
      page_size: 10,
      count: 0,
    },
    searchParam: {},
    totalParam: {},
    loading: false,
  });

  onMounted(() => {
    if (isAuto) reset();
  });

  const getTableData = async () => {
    try {
      state.value.loading = true;
      const data = await api(state.value.totalParam);
      const { count, rows } = data;
      state.value.tableData = isPageable ? rows : [];
      // 将tableData反给callback
      if (isPageable) updatePagination({ page_num: state.value.pagination.page_num, page_size: state.value.pagination.page_size, count });
    } catch (err) {
      console.error(err);
    } finally {
      state.value.loading = false;
    }
  };

  const updatePagination = (resPageable) => {
    Object.assign(state.value.pagination, resPageable);
  };

  const updatedTotalParam = () => {
    state.value.totalParam = {};
    let paginationParam = {};
    let nowSearchParam = {};

    Object.keys(state.value.searchParam).forEach((key) => {
      // 空 undefined null 不传
      if (state.value.searchParam[key] !== '' && state.value.searchParam[key] !== undefined && state.value.searchParam[key] !== null) {
        nowSearchParam[key] = state.value.searchParam[key];
      }
    });
    if (isPageable) {
      paginationParam = {
        page_num: state.value.pagination.page_num,
        page_size: state.value.pagination.page_size,
      };
    }
    Object.assign(state.value.totalParam, nowSearchParam, paginationParam, initParam);
  };

  const search = async () => {
    updatedTotalParam();
    await getTableData();
  };

  const reset = async () => {
    state.value.pagination.page_num = 1;
    state.value.pagination.page_size = 10;
    state.value.searchParam = {};
    updatedTotalParam();
    await search();
  };

  const handleSizeChange = async (val) => {
    state.value.pagination.page_num = 1;
    state.value.pagination.page_size = val;
    console.log('state.value.pagination', state.value.pagination);
    await search();
  };

  const handleCurrentChange = async (val) => {
    state.value.pagination.page_num = val;
    await search();
  };

  const { searchParam, pagination, tableData, loading } = toRefs(state.value);

  /**
   * @description 返回值的描述,所有的返回值都是响应式的,可以直接在模板中使用
   * @param {Object} searchParam 搜索参数
   * @param {Object} pagination 分页参数
   * @param {Array} tableData 表格数据
   * @param {Boolean} loading 加载状态
   * @param {Function} handleCurrentChange 分页改变
   * @param {Function} handleSizeChange 每页显示个数改变
   * @param {Function} search 搜索
   * @param {Function} reset 重置
   */

  return {
    searchParam,
    pagination,
    tableData,
    loading,
    handleCurrentChange,
    handleSizeChange,
    search,
    reset,
  };
};