前语

企业应用中怎么提炼公共模块,怎么更好的扩展,怎么让更多场景复用。以下将根据vue3 ts antd进行公共表单开发

FormItemComps

将每一种表单项分别拆分,易于后期扩展和管理。

    // index.ts
    import FormItemInput from "./FormItemInput.vue";
    import FormItemInputNumber from "./FormItemInputNumber.vue";
    interface ComsType {
      [key: string]: any
    }
    const ComponentMap: ComsType = {
      "input": FormItemInput,
      "input-number": FormItemInputNumber,
    }
    export default ComponentMap;

FormItemInput

根底的输入组件,有几个点咱们需要先理解。

  1. 数据呼应
  2. antd input props 项有很多,咱们怎么接入。

首要装备根底的表单项 name label rules等

运用 useChange 进行数据传递

运用 v-bind props.props 进行a-input的所有属性传递

// FormItemInput.vue
<template>
  <a-form-item
    :name="props.name"
    :label="props.label"
    :rules="props.rules"
  >
    <a-input
      v-model:value="inputValue"
      v-bind="props.props"
    />
  </a-form-item>
</template>
<script lang="ts" setup>
  // props.props 具体参数请查阅官方文档 https://antdv.com/components/input-cn/#API
  import { ref, watch } from "vue";
  import { AnyPropName } from "@/types";
  import { useChange } from "./useFormItem";
  import { formItemDefaultProps } from "./const";
  interface FormItemEmits {
    (e: "update:modelValue", value: any): void,
    (e: "onChange", value: any, key: string): void,
  }
  // vue3 当时版本暂不支撑外部导入props类型界说 wait fix
  interface FormItemProps {
    modelValue: any, // v-model呼应值
    name: string, // 数据键
    label: string, // 表单项文本名
    rules?: Array<AnyPropName>, // 校验规矩 同antd 表单校验规矩共同
    props?: AnyPropName, // 组件额外 props 同antd组件props共同
    change?: (value: any, key: string) => void, // 当时项值变化时触发
  }
  const emits = defineEmits<FormItemEmits>();
  const props = withDefaults(defineProps<FormItemProps>(), {...formItemDefaultProps});
  const inputValue = ref(props.modelValue);
  watch(inputValue, value => {
    useChange(emits, props, value);
  })
  watch(() => props.modelValue, value => {
    inputValue.value = value;
  })
</script>

FormItemInputNumber

数字输入组件,核心逻辑同输入组件共同,今后要扩展组件核心逻辑依然是不变的。咱们仅有需要做的便是运用不同的a-xx组件或许你的自界说组件

<template>
  <a-form-item
    :name="props.name"
    :label="props.label"
    :rules="props.rules"
  >
    <a-input-number
      class="form-item-input-number"
      v-model:value="inputValue"
      v-bind="props.props"
    />
  </a-form-item>
</template>
<script lang="ts" setup>
  // props.props 具体参数请查阅官方文档 https://antdv.com/components/input-number-cn#API
  import { ref, watch } from "vue";
  import { AnyPropName } from "@/types";
  import { useChange } from "./useFormItem";
  import { formItemDefaultProps } from "./const";
  interface FormItemEmits {
    (e: "update:modelValue", value: any): void,
    (e: "onChange", value: any, key: string): void,
  }
  // vue3 当时版本暂不支撑外部导入props类型界说 wait fix
  interface FormItemProps {
    modelValue: any, // v-model呼应值
    name: string, // 数据键
    label: string, // 表单项文本名
    rules?: Array<AnyPropName>, // 校验规矩 同antd 表单校验规矩共同
    props?: AnyPropName, // 组件额外 props 同antd组件props共同
    change?: (value: any, key: string) => void, // 当时项值变化时触发
  }
  const emits = defineEmits<FormItemEmits>();
  const props = withDefaults(defineProps<FormItemProps>(), {...formItemDefaultProps});
  const inputValue = ref(props.modelValue);
  watch(inputValue, value => {
    useChange(emits, props, value);
  })
  watch(() => props.modelValue, value => {
    inputValue.value = value;
  })
</script>
<style lang="less">
  .form-item-input-number{
    width: 100%;
  }
</style>

useChange

呼应事情逻辑是共同的,提取到公共模块。

import { FormItemProps, FormItemEmits } from "./types";
export function useChange(emits: FormItemEmits, props: FormItemProps, value: any) {
  const { change, name } = props;
  emits("update:modelValue", value); // v-model呼应
  emits("onChange", value, name); // formSearch事情呼应
  change!(value, name); // 数据项中传入的事情呼应
}

BaseForm

整合界说好的表单项,并经过数据束缚输出表单。

核心点:数据传入,修正表单内容,获取表单内容,校验提交表单。

<template>
  <a-spin :spinning="loading" :tip="loadingTip" :delay="delayTime">
    <a-form
      ref="formRef"
      name="base-form-instance"
      v-bind="props"
      :model="formState"
      :labelCol="labelCol"
      :wrapperCol="wrapperCol"
    >
      <a-row :gutter="24">
        <template v-for="item in data" :key="item.key">
          <a-col :span="item.span || colSpan">
            <a-tooltip :title="item.tip" :mouseEnterDelay="0.2">
              <component
                @onChange="onChange"
                :is="ComponentMap[item.type]"
                v-model="formState[item.key]"
                v-bind="item"
                :name="item.key"
              />
            </a-tooltip>
          </a-col>
        </template>
      </a-row>
    </a-form>
  </a-spin>
</template>
<script lang="ts" setup>
  import { ref, toRefs, reactive, nextTick } from "vue";
  import type { FormInstance } from "ant-design-vue";
  import type { AnyPropName, FormListDatas } from "@types";
  import ComponentMap from "@components/FormItemComs";
  import { defaultProps } from "./const";
  interface PropsType {
    data: FormListDatas, // 数据源
    loading?: boolean, // 是否加载中
    loadingTip?: string, // 自界说加载提示案牍
    delayTime?: number, // 推迟显现loading状况, 当loading状况在 delayTime 时间内完毕, 则不显现loding UI状况 单位ms
    colSpan?: number // 表单项一行占多少列 n/24
    labelCol?: AnyPropName, // 表单项 label 装备; https://www.antdv.com/components/grid-cn/#Col
    wrapperCol?: AnyPropName // 表单项 输入控件装备; 类型同 labelCol共同
  }
  const emits = defineEmits(["change"]);
  const props = withDefaults(defineProps<PropsType>(), { ...defaultProps }) // defineProps<>(["visible", "modelValue"]);
  const { data, loading, loadingTip, delayTime, labelCol } = toRefs(props);
  const formRef = ref<FormInstance>();
  const formState = reactive<AnyPropName>({});
  setDefaultFormState();
  let timeoutValidate: any;
  // 对 component 运用 onChange 的原因是组件自身有change事情这儿不进行混合; 并且change参数类型也不相同会提示报错
  const onChange = (key: string, value: any): void => {
    emits("change", key, value);
    // 当数据变化时手动触发校验, 由于嵌套v-model导致表单主动校验会出现过错呼应 预期与实际不共同
    clearTimeout(timeoutValidate);
    timeoutValidate = setTimeout(() => {
      formRef.value?.validateFields([key]);
    }, 120)
  }
  function setDefaultFormState() {
    data.value.forEach(v => {
      if (formState[v.key] !== v.defaultValue) formState[v.key] = v.defaultValue;      
    });
  }
  // 重置表单后会触发n次 change 呼应事情
  const resetFields = () => {
    formRef.value?.resetFields(); // 清空表单状况; 当经过setFormState从头设置值之后,只调用resetFields一次无法重置
    nextTick(() => { 
      setDefaultFormState(); // 也能够再调用一次 formRef.value?.resetFields(); 可是这样从头设置的 data[n].defaultValue 无法收效;
    });
  }
  const getFormState = (payload?: string | string[]): string | object => {
    const type = payload ? typeof payload : "not";
    if (type === "not") return { ...formState };
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    if (type === "string") return formState[payload];
    if (Array.isArray(payload)) {
      const ret: AnyPropName = {};
      payload.forEach(key => {
        ret[key] = formState[key]
      });
      return ret;
    }
    console.error(`传入参数类型需为 string | string[]; 当时参数${JSON.stringify(payload)}`);
    return {};
  };
  const setFormState = (key: string | AnyPropName, value?: any) => {
    const type = key ? typeof key : "not";
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    if (type === "string") formState[key] = value;
    else if (!Array.isArray(key) && type === "object") {
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      for (const name in key) {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        formState[name] = key[name];
      }
    } else {
      console.error(`传入参数类型需为 string | object<{key: value}>; 当时参数${JSON.stringify(key)}`);
    }
  }
  const submit = async (nameList?: string | string[]): Promise<AnyPropName> => {
    return formRef.value!.validateFields(nameList);
  }
  defineExpose({ getFormState, setFormState, resetFields, submit });
</script>

结语

关于怎么运用表单请前往GitHub查看。由于这是一系列东西,文章上写出来太多反而影响阅览。