一、类型声明
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>
四、配置文档
待更新