Form表单组件封装和使用

发布时间 2023-11-18 13:24:39作者: 柯基与佩奇

表单Form是中后台频繁使用的组件,以下是一个基于arco design vue组件库封装的表单组件。

这个表单组件特点:

  1. 所有配置都是直接继承组件库组件的props,无需其他文档
  2. 可配置展开折叠
  3. 支持响应式布局
  4. 表单项支持动态隐藏
  5. 插槽支持,自定义扩展
  6. 组件库的良好支持,封装代码简洁优雅
  7. placeholder无需手写,输入框允许清除默认true
  8. 自带hooks省时省力
  9. TS代码提示支持

数据类型结构如下

form-ts.png

组件代码

form.png

组件自带 hooks

form-hooks.png

示例 1 折叠查询表单

form-demo1.png

演示效果

20231029182548_rec_.gif

示例 2 动态隐藏表单项

form-demo2.png

演示效果

20231029183213_rec_.gif

示例 3 自带 hooks 的使用

form-demo3.png

演示效果

20231029183711_rec_.gif

QQ图片20231014194031.jpg

源码

import type * as A from '@arco-design/web-vue'

export type FormType =
  | 'input'
  | 'select'
  | 'radio-group'
  | 'checkbox-group'
  | 'textarea'
  | 'date-picker'
  | 'time-picker'
  | 'input-number'
  | 'rate'
  | 'switch'
  | 'slider'
  | 'cascader'
  | 'tree-select'

export type ColumnsItemPropsKey =
  | keyof A.InputInstance['$props']
  | keyof A.SelectInstance['$props']
  | keyof A.TextareaInstance['$props']
  | keyof A.DatePickerInstance['$props']
  | keyof A.TimePickerInstance['$props']
  | keyof A.RadioGroupInstance['$props']
  | keyof A.CheckboxGroupInstance['$props']
  | keyof A.InputNumberInstance['$props']
  | keyof A.RateInstance['$props']
  | keyof A.SwitchInstance['$props']
  | keyof A.SliderInstance['$props']
  | keyof A.CascaderInstance['$props']
  | keyof A.TreeSelectInstance['$props']

export type ColumnsItemHide = boolean | ((form?: any) => boolean)

export interface ColumnsItem {
  type: FormType
  label: A.FormItemInstance['label']
  field: A.FormItemInstance['field']
  span?: number
  col?: A.ColProps
  item?: Omit<A.FormItemInstance['$props'], 'label' | 'field'>
  props?:
    | A.InputInstance['$props']
    | A.SelectInstance['$props']
    | A.TextareaInstance['$props']
    | A.DatePickerInstance['$props']
    | A.TimePickerInstance['$props']
    | A.RadioGroupInstance['$props']
    | A.CheckboxGroupInstance['$props']
    | A.InputNumberInstance['$props']
    | A.RateInstance['$props']
    | A.SwitchInstance['$props']
    | A.SliderInstance['$props']
    | A.CascaderInstance['$props']
    | A.TreeSelectInstance['$props']
  rules?: A.FormItemInstance['$props']['rules']
  options?:
    | A.SelectInstance['$props']['options']
    | A.RadioGroupInstance['$props']['options']
    | A.CheckboxGroupInstance['$props']['options']
    | A.CascaderInstance['$props']['options']
  data?: A.TreeSelectInstance['$props']['data']
  hide?: ColumnsItemHide
}

export interface Options {
  form: Omit<A.FormInstance['$props'], 'model'>
  row?: Partial<typeof import('@arco-design/web-vue')['Row']['__defaults']>
  columns: ColumnsItem[]
  btns?: { hide?: boolean; span?: number; col?: A.ColProps; searchBtnText?: string }
  fold?: { enable?: boolean; index?: number }
}
<template>
  <a-form
    :auto-label-width="true"
    v-bind="options.form"
    ref="formRef"
    :model="modelValue"
  >
    <a-row :gutter="14" v-bind="options.row" class="w-full">
      <template v-for="(item, index) in options.columns" :key="item.field">
        <a-col
          v-if="!isHide(item.hide)"
          :span="item.span || 12"
          v-bind="item.col"
          v-show="
            index <= (options.fold?.index || 0) ||
            (index >= (options.fold?.index || 0) && !collapsed)
          "
        >
          <a-form-item
            v-bind="item.item"
            :label="item.label"
            :field="item.field"
            :rules="item.rules"
          >
            <slot :name="item.field">
              <template v-if="item.type === 'input'">
                <a-input
                  :allow-clear="true"
                  :placeholder="`请输入${item.label}`"
                  :max-length="20"
                  v-bind="(item.props as A.InputInstance['$props'])"
                  :model-value="modelValue[item.field as keyof typeof modelValue]"
                  @update:model-value="valueChange($event, item.field)"
                ></a-input>
              </template>

              <template v-if="item.type === 'input-number'">
                <a-input-number
                  :placeholder="`请输入${item.label}`"
                  v-bind="(item.props as A.InputNumberInstance['$props'])"
                  :model-value="modelValue[item.field as keyof typeof modelValue]"
                  @update:model-value="valueChange($event, item.field)"
                ></a-input-number>
              </template>

              <template v-if="item.type === 'textarea'">
                <a-textarea
                  :allow-clear="true"
                  :placeholder="`请填写${item.label}`"
                  :max-length="200"
                  :show-word-limit="true"
                  v-bind="(item.props as A.TextareaInstance['$props'])"
                  :model-value="modelValue[item.field as keyof typeof modelValue]"
                  @update:model-value="valueChange($event, item.field)"
                ></a-textarea>
              </template>

              <template v-if="item.type === 'select'">
                <a-select
                  :allow-clear="true"
                  :placeholder="`请选择${item.label}`"
                  v-bind="(item.props as A.SelectInstance['$props'])"
                  :options="(item.options as A.SelectInstance['$props']['options'])"
                  :model-value="modelValue[item.field as keyof typeof modelValue]"
                  @update:model-value="valueChange($event, item.field)"
                ></a-select>
              </template>

              <template v-if="item.type === 'cascader'">
                <a-cascader
                  :allow-clear="true"
                  :placeholder="`请选择${item.label}`"
                  v-bind="(item.props as A.CascaderInstance['$props'])"
                  :options="(item.options as A.CascaderInstance['$props']['options'])"
                  :model-value="modelValue[item.field as keyof typeof modelValue]"
                  @update:model-value="valueChange($event, item.field)"
                />
              </template>

              <template v-if="item.type === 'tree-select'">
                <a-tree-select
                  :allow-clear="true"
                  :placeholder="`请选择${item.label}`"
                  v-bind="(item.props as A.TreeSelectInstance['$props'])"
                  :data="(item.data as A.TreeSelectInstance['$props']['data'])"
                  :model-value="modelValue[item.field as keyof typeof modelValue]"
                  @update:model-value="valueChange($event, item.field)"
                >
                </a-tree-select>
              </template>

              <template v-if="item.type === 'radio-group'">
                <a-radio-group
                  v-bind="(item.props as A.RadioGroupInstance['$props'])"
                  :options="(item.options as A.RadioGroupInstance['$props']['options'])"
                  :model-value="modelValue[item.field as keyof typeof modelValue]"
                  @update:model-value="valueChange($event, item.field)"
                ></a-radio-group>
              </template>

              <template v-if="item.type === 'checkbox-group'">
                <a-checkbox-group
                  v-bind="(item.props as A.CheckboxGroupInstance['$props'])"
                  :options="(item.options as A.CheckboxGroupInstance['$props']['options'])"
                  :model-value="modelValue[item.field as keyof typeof modelValue]"
                  @update:model-value="valueChange($event, item.field)"
                ></a-checkbox-group>
              </template>

              <template v-if="item.type === 'date-picker'">
                <a-date-picker
                  :allow-clear="true"
                  :placeholder="`请选择日期`"
                  v-bind="(item.props as A.DatePickerInstance['$props'])"
                  :model-value="modelValue[item.field as keyof typeof modelValue]"
                  @update:model-value="valueChange($event, item.field)"
                ></a-date-picker>
              </template>

              <template v-if="item.type === 'time-picker'">
                <a-time-picker
                  :allow-clear="true"
                  :placeholder="`请选择时间`"
                  v-bind="(item.props as A.TimePickerInstance['$props'])"
                  :model-value="modelValue[item.field as keyof typeof modelValue]"
                  @update:model-value="valueChange($event, item.field)"
                >
                </a-time-picker>
              </template>

              <template v-if="item.type === 'rate'">
                <a-rate
                  :allow-clear="true"
                  v-bind="(item.props as A.RateInstance['$props'])"
                  :model-value="modelValue[item.field as keyof typeof modelValue]"
                  @update:model-value="valueChange($event, item.field)"
                />
              </template>

              <template v-if="item.type === 'switch'">
                <a-switch
                  v-bind="(item.props as A.SwitchInstance['$props'])"
                  :model-value="modelValue[item.field as keyof typeof modelValue]"
                  @update:model-value="valueChange($event, item.field)"
                />
              </template>

              <template v-if="item.type === 'slider'">
                <a-slider
                  v-bind="(item.props as A.SliderInstance['$props'])"
                  :model-value="modelValue[item.field as keyof typeof modelValue]"
                  @update:model-value="valueChange($event, item.field)"
                />
              </template>
            </slot>
          </a-form-item>
        </a-col>
      </template>
      <a-col
        :span="options.btns?.span || 12"
        v-bind="options.btns?.col"
        v-if="!options.btns?.hide"
      >
        <a-space wrap>
          <slot name="footer">
            <a-button type="primary" @click="emit('search')">
              <template #icon><icon-search /></template>
              <template #default>{{
                options.btns?.searchBtnText || "搜索"
              }}</template>
            </a-button>
            <a-button @click="emit('reset')">重置</a-button>
            <a-button
              v-if="options.fold?.enable"
              type="text"
              size="mini"
              @click="collapsed = !collapsed"
            >
              <template #icon>
                <icon-up v-if="!collapsed" />
                <icon-down v-else />
              </template>
              <template #default>{{ collapsed ? "展开" : "收起" }}</template>
            </a-button>
          </slot>
        </a-space>
      </a-col>
    </a-row>
  </a-form>
</template>

<script setup lang="ts">
import type { Options, ColumnsItemHide } from "./type";
import type * as A from "@arco-design/web-vue";

interface Props {
  modelValue: object;
  options: Options;
}

const props = withDefaults(defineProps<Props>(), {});

const emit = defineEmits<{
  (e: "update:modelValue", value: any): void;
  (e: "search"): void;
  (e: "reset"): void;
}>();

const valueChange = (value: any, field: string) => {
  emit(
    "update:modelValue",
    Object.assign(props.modelValue, { [field]: value })
  );
};

const collapsed = ref(false);
const formRef = ref<A.FormInstance>();
defineExpose({ formRef });

const isHide = (hide?: ColumnsItemHide) => {
  if (hide === undefined) return false;
  if (typeof hide === "boolean") return hide;
  if (typeof hide === "function") {
    return hide(props.modelValue);
  }
};
</script>

<style lang="scss" scoped></style>
import { reactive } from "vue";
import _ from "lodash";
import type { Options, ColumnsItem, ColumnsItemPropsKey } from "./type";
import { Message } from "@arco-design/web-vue";

export function useGiForm(initValue: Options) {
  const getInitValue = () => _.cloneDeep(initValue);

  const options = reactive(getInitValue());

  const resetOptions = () => {
    Object.assign(options, getInitValue());
  };

  const setValue = <T>(field: string, key: keyof ColumnsItem, value: T) => {
    if (!options.columns.length) return;
    const obj = options.columns.find((i) => i.field === field);
    if (obj) {
      obj[key] = value as never;
    } else {
      Message.warning(`没有这个field属性值-${field},请检查!`);
    }
  };

  const setPropsValue = <T>(
    field: string,
    key: ColumnsItemPropsKey,
    value: T
  ) => {
    if (!options.columns.length) return;
    const obj = options.columns.find((i) => i.field === field);
    if (obj) {
      if (!obj.props) {
        obj.props = {};
      }
      obj.props[key as keyof ColumnsItem["props"]] = value as never;
    } else {
      Message.warning(`没有这个field属性值-${field},请检查!`);
    }
  };

  return {
    /** 配置项 */
    options,
    /** 重置 options */
    resetOptions,
    /** 设置 options.columns 某个对象属性的值 */
    setValue,
    /** 设置 options.columns.props 某个属性的值 */
    setPropsValue,
  };
}