element-plus表单封装

32 阅读3分钟

一、类型声明

import { VNode, ComponentPublicInstance } from "vue";
import type { ComponentSize, FormProps } from 'element-plus'


export interface ObjectType {
    [key: string]: any
}
interface RenderParams {
    column: ColumnItem;
    formData: any;
    row: any
}
export interface PropsObjectType extends ObjectType {
    modelValue?: string; // 双向绑定变量
    render?: (renderParams: RenderParams) => VNode;
    regex?: RegExp | string | ((val: any) => any) // 参数正则过滤
}

export interface OtherProps extends ObjectType {
    size?: ComponentSize
}

export type RefItem = Element | ComponentPublicInstance | null;
export interface RefObject {
    // [key: string]: RefItem
    [key: string]: any
}

export interface HandleAllEvent {
    prop: string;
    column: ColumnItem;
    formData: ObjectType;
    val: any;
    enum: any[];
    eventName: string
}

export type FieldNames = {
    label: string;
    value: string;
    children?: FieldNames[];
};

type EventHandler = (data: HandleAllEvent) => any

interface BaseEvent {
    change?: EventHandler;
    click?: EventHandler;
    input?: EventHandler;
    blur?: EventHandler;
    clear?: EventHandler;
    select?: EventHandler;
}

export interface Event extends BaseEvent {
    [key: string]: EventHandler | undefined
}

export interface ColumnItem {
    el: string;
    label: string;
    prop: string;
    title?: string;
    titleStyle?: ObjectType;
    labelWidth?: string;
    enum?: any[] | (() => Promise<any>);
    fieldNames?: FieldNames;
    colSpan?: number;
    props?: PropsObjectType;
    defaultValue?: any;
    render?: () => VNode;
    isShow?: boolean;
    showFun?: (column?: ColumnItem) => boolean;
    rules?: any[];
    required?: boolean;
    event?: Event;
    domOperation?: ((el: RefItem) => void) // 对dom的操作(如获取光标)
}

export interface BaseForm {
    columns: ColumnItem[];
    formData: ObjectType;
    handleCancel?: (handleResetForm: () => void) => void;
    handleSubmit?: (formData: ObjectType) => void;
    cancelText?: string;
    submitText?: string;
    labelWidth?: string;
    labelSuffix?: string;
    hideRequiredAsterisk?: boolean // 是否隐藏必填星号
    rules?: {
        [key: string]: any;
    };
    // labelPosition?: 'top' | 'right' | 'left';
    labelPosition?: FormProps['labelPosition'];
    gutter?: number;
    footerBtn?: boolean | ('cancel' | 'confirm')[];
    otherProps?: OtherProps
}

export interface FormItem {
    column: ColumnItem;
    formData: ObjectType;
    compRefObject: RefObject
    labelWidth?: string;
    labelSuffix?: string;
    rule?: any;
}


二、表单父组件

<template>
  <el-form
    ref="ruleFormRef"
    :label-position="props.labelPosition"
    :label-width="props.labelWidth"
    :label-suffix="props.labelSuffix"
    :rules="props.rules ?? {}"
    :model="formData"
    :hide-required-asterisk="hideRequiredAsterisk"
    v-bind="{ ...otherProps }"
  >
    <el-row :gutter="props.gutter">
      <el-col v-for="(column, index) in _columns" :key="index" :span="column.colSpan ?? 12">
        <FormItem
          :column="column"
          :labelSuffix="props.labelSuffix"
          :labelWidth="column.labelWidth ?? props.labelWidth"
          :formData="formData"
          :rule="props.rules[column.prop]"
          @handleAllEvent="handleAllEvent"
          v-model:compRefObject="compRefObject"
        >
          <template #[column.prop]>
            <slot :name="column.prop" :column="column" :formData="formData" :rule="props.rules[column.prop]"></slot>
          </template>
        </FormItem>
      </el-col>
    </el-row>
    <slot name="buttons">
      <div class="btn-container" v-if="props.footerBtn">
        <el-button
          type="default"
          @click="handleInnerCancel"
          :size="props.otherProps?.size ?? 'small'"
          v-if="showFooterButton('cancel')"
          >{{ cancelText }}</el-button
        >
        <el-button
          type="primary"
          @click="handleInnerSubmit"
          :size="props.otherProps?.size ?? 'small'"
          v-if="showFooterButton('confirm')"
          >{{ submitText }}</el-button
        >
      </div>
    </slot>
  </el-form>
</template>

<script lang="ts" setup>
import { computed, nextTick, ref } from "vue";
import FormItem from "./components/formItem.vue";
import type { BaseForm, RefObject, RefItem, HandleAllEvent } from "./interface/index";
import { FormInstance } from "element-plus";

const props = withDefaults(defineProps<BaseForm>(), {
  formData: () => ({}),
  columns: () => [],
  rules: () => ({}),
  labelWidth: "100px",
  labelSuffix: " :",
  hideRequiredAsterisk: false,
  cancelText: "取消",
  submitText: "确定",
  labelPosition: "right",
  gutter: 20,
  footerBtn: true,
  otherProps: () => ({
    size: "small"
  })
});

const ruleFormRef = ref<FormInstance | null>(null);
// 表单ref集合
const compRefObject = ref<RefObject>({});

const showFooterButton = (key: "cancel" | "confirm") => {
  return Array.isArray(props.footerBtn) ? props.footerBtn.includes(key) : props.footerBtn;
};

const _columns = computed(() => {
  return props.columns.filter(item => {
    // enum处理
    if (typeof item.enum === "function") {
      try {
        item.enum().then(res => {
          item.enum = res;
        });
      } catch (error) {
        item.enum = [];
      }
    }
    // DOM 操作
    nextTick(() => {
      if (item.domOperation && typeof item.domOperation === "function") {
        const el: RefItem = compRefObject.value[item.prop + "Ref"];
        item.domOperation(el);
      }
    });
    if (item.showFun) return item.showFun(item);
    return item.isShow ?? true;
  });
});

const handleResetForm = () => {
  ruleFormRef.value?.resetFields();
};

const handleInnerCancel = () => {
  props.handleCancel && props.handleCancel(handleResetForm);
};
const handleInnerSubmit = () => {
  ruleFormRef.value?.validate(valid => {
    console.log("props.formData :>> ", props.formData);
    if (!valid) return;
    props.handleSubmit && props.handleSubmit(props.formData);
  });
};

const emits = defineEmits<{
  handleAllEvent: [data: HandleAllEvent];
}>();

const handleAllEvent = (data: HandleAllEvent) => {
  emits("handleAllEvent", data);
};

defineExpose({
  ruleFormRef,
  handleResetForm,
  compRefObject
});
</script>

<style lang="scss" scoped>
.btn-container {
  text-align: right;
}
</style>

三、表单子组件

<template>
  <div class="title mb10" :style="column.titleStyle ?? titleStyle" v-if="column.title">
    {{ column.title === "nbsp" ? "\u00a0" : column.title }}
  </div>
  <slot :name="column.prop" :column="column" :formData="formData" :rule="props.rule">
    <el-form-item :label="props.column.el !== 'button' ? column.label : ''" :prop="column.prop" :rules="_rules">
      <component
        :is="props.column.render ?? `el-${props.column.el}`"
        v-bind="{ ...handleSearchProps, ...placeholder, clearable }"
        v-model.trim="vData"
        :[modelValue]="vData"
        v-on:[modelValueEventName]="(data: any) => (vData = data)"
        :options="['cascader'].includes(column.el) ? column.enum : []"
        :ref="(el: RefItem) => setRef(props.column.prop, el)"
        v-on="vBindEvent"
      >
        <template #default="{ data }" v-if="props.column.el === 'cascader'">
          <span>{{ data[fieldNames.label] }}</span>
        </template>
        <template v-if="props.column.el === 'select'">
          <component
            :is="`el-option`"
            v-for="(col, index) in props.column.enum"
            :key="index"
            :label="col[fieldNames.label]"
            :value="col[fieldNames.value]"
          ></component>
        </template>
        <template v-if="['button', 'tag'].includes(props.column.el)">
          <component :is="getEnumVal"></component>
        </template>
      </component>
    </el-form-item>
  </slot>
</template>

<script lang="ts" setup>
import { computed, h } from "vue";
import type { FormItem, ObjectType, RefItem, HandleAllEvent } from "../interface/index";

const emits = defineEmits<{
  "update:compRefObject": [val: any];
  handleAllEvent: [data: HandleAllEvent];
}>();

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

function getValueFromPath(obj: ObjectType, path: string) {
  const keys = path.split(".");
  let value = obj;
  for (let key of keys) {
    value = value[key];
    if (value === undefined) {
      return undefined; // 如果路径无效,返回 undefined
    }
  }
  return value;
}
function setValueAtPath(obj: ObjectType, path: string, newValue: any) {
  const keys = path.split(".");
  let current = obj;

  for (let i = 0; i < keys.length - 1; i++) {
    if (!current[keys[i]]) {
      current[keys[i]] = {};
    }
    current = current[keys[i]];
  }

  current[keys[keys.length - 1]] = newValue;
}

const modelValue = computed(() => {
  return props.column?.props?.modelValue || "";
});
const modelValueEventName = computed(() => {
  const modelValue = props.column?.props?.modelValue;
  return modelValue ? "update:" + modelValue : "";
});

const vData = computed({
  get(oldV) {
    return getValueFromPath(props.formData, props.column.prop);
  },
  set(newV) {
    if (props.column?.props?.regex) {
      const { regex } = props.column.props;
      if (regex instanceof RegExp || typeof regex === "string") {
        newV = (newV as string).replace(regex, "");
      } else if (typeof regex === "function") {
        newV = regex(newV);
      }
    }
    setValueAtPath(props.formData, props.column.prop, newV);
  }
});

const getEnumVal = computed(() => {
  if (props.column.props?.render) {
    return props.column.props.render({ column: props.column, formData: props.formData, row: vData.value });
  } else if (props.column.el === "tag") {
    if (props.column?.enum && Array.isArray(props.column.enum)) {
      const curItem = props.column.enum.find(col => {
        return col[fieldNames.value["value"]] === vData.value;
      });
      return h("span", null, curItem ? curItem[fieldNames.value["label"]] : "");
    } else {
      return h("span", null, vData.value as any);
    }
  } else if (props.column.el === "button") {
    return h("span", null, props.column.label);
  }
});

// 规则
const _rules = computed(() => {
  if (props.column.rules) {
    const _index = props.column.rules.findIndex(rule => {
      return rule.hasOwnProperty("required");
    });
    if (_index !== -1) {
      // 没有定义提示内容,则用props的参数
      !props.column.rules[_index].message &&
        (props.column.rules[_index].message =
          props.column.props?.placeholder ?? (props.column.el === "input" ? "请输入" : "请选择"));
      // 没有触发事件,则采用默认
      !props.column.rules[_index].trigger &&
        (props.column.rules[_index].trigger = props.column.el === "input" ? "blur" : "change");
    }
    return props.column.rules ?? [];
  }
  // 默认required
  if (props.column.required && !props.column.rules) {
    return [
      {
        required: true,
        message: props.column.props?.placeholder ?? (props.column.el === "input" ? "请输入" : "请选择"),
        trigger: props.column.el === "input" ? "blur" : "change"
      }
    ];
  }
});

const titleStyle = computed(() => {
  return {
    fontSize: "16px",
    fontWeight: "blod"
  };
});

// 判断 fieldNames 设置 label && value && children 的 key 值
const fieldNames = computed(() => {
  return {
    label: props.column.fieldNames?.label ?? "label",
    value: props.column.fieldNames?.value ?? "value",
    children: props.column.fieldNames?.children ?? "children"
  };
});

// 处理透传的 searchProps (el 为 tree-select、cascader 的时候需要给下默认 label && value && children)
const handleSearchProps = computed(() => {
  let searchProps = props.column?.props ?? {};

  return searchProps;
});

// 处理默认 placeholder
const placeholder = computed(() => {
  if (["datetimerange", "daterange", "monthrange"].includes(props.column.props?.type) || props.column.props?.isRange) {
    return {
      rangeSeparator: props.column.props?.rangeSeparator ?? "至",
      startPlaceholder: props.column.props?.startPlaceholder ?? "开始时间",
      endPlaceholder: props.column.props?.endPlaceholder ?? "结束时间"
    };
  }
  const placeholder = props.column.props?.placeholder ?? (props.column.el?.includes("input") ? "请输入" : "请选择");
  return { placeholder };
});

// 是否有清除按钮 (当搜索项有默认值时,清除按钮不显示)
const clearable = computed(() => {
  return (
    props.column?.props?.clearable ??
    (props.formData[props.column.prop] == null || props.formData[props.column.prop] == undefined)
  );
});

// 绑定事件
const handleAllEvent = (val: any, eventName: string, event: any) => {
  const data = {
    val,
    prop: props.column.prop,
    column: props.column,
    formData: props.formData,
    enum: (props.column.enum || []) as any[],
    eventName
  };
  event(data);
  emits("handleAllEvent", data);
};
const vBindEvent = computed(() => {
  const { event } = props.column;
  if (!event || typeof event !== "object") return {};
  const eventObj: ObjectType = {};
  Object.keys(event).forEach(key => {
    eventObj[key] = (val: any) => handleAllEvent(val, key, event[key as keyof typeof event]);
  });
  return eventObj;
});

const setRef = (prop: string, el: RefItem) => {
  const compRefObject = props.compRefObject;
  compRefObject[prop + "Ref"] = el;
  emits("update:compRefObject", compRefObject);
  return prop + "Ref";
};
</script>

<style lang="scss" scoped>
.el-select {
  width: 100%;
}
:deep(.el-cascader) {
  width: 100%;
}
</style>

四、配置文档

待更新