前言
文章旨使用 vue3
在基于 element-plus
把 form
、table
、pagination
,三个组件,封装成为可配置化页面,本代码复制可用,显示给出容器宽高即可,代码如下。
.contain {
width: 100%;
height: 100%;
}
header 头部
头部不是重点,仅做简单封装,不通用。
<template>
<div class="dx-header">
<div class="dx-header-title">
<el-button v-if="back" type="primary" @click="goBack" :icon="ArrowLeft" link>返回</el-button>
<div v-if="back" class="dx-header-line"></div>
<span>{{ title }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ArrowLeft } from "@element-plus/icons-vue";
import { useRouter } from "vue-router";
export interface Header {
title?: string;
back?: boolean;
}
const router = useRouter();
const goBack = () => {
router.back();
};
withDefaults(defineProps<Header>(), { title: "", back: false });
</script>
<style lang="scss" scoped>
.dx-header {
display: flex;
flex-direction: column;
&-title {
width: 100%;
background-color: #fff;
font-weight: bold;
font-size: large;
border-bottom: 1px solid var(--menu-logo-line-color);
padding: 12px;
display: flex;
align-items: center;
}
&-line {
width: 1px;
height: 25px;
margin: 0 20px;
background-color: var(--menu-logo-line-color);
}
&-contain {
flex: 1;
display: flex;
box-sizing: border-box;
background-color: #fff;
padding: 18px 12px;
}
}
</style>
form 部分
form
部分,旨在配置成 forms
<template>
<el-form ref="formRef" :model="formModel" v-bind="safeFormProps">
<el-row v-bind="safeRowProps">
<template v-for="item in formItemsFilter" :key="`${item?.prop || item?.slot}`">
<el-col v-bind="getColProps(item)">
<el-form-item v-if="item.prop" :label="item.label" :prop="item.prop" v-bind="getFormItemProps(item)">
<template v-if="item.labelSlot" #label>
<slot :name="item.labelSlot" :prop="item.prop" />
</template>
<template v-if="item.errorSlot" #error>
<slot :name="item.errorSlot" :prop="item.prop" />
</template>
<template v-if="item.slot">
<slot :name="item.slot" :scope="{ formModel, ...item }" />
</template>
<template v-else>
<component
v-if="typeList.includes(item.tags || '')"
:is="item.tags"
v-model="formModel[item.prop]"
v-bind="item.attrs"
/>
<el-select v-else-if="item.tags === 'el-select'" v-model="formModel[item.prop]" v-bind="item.attrs">
<el-option
v-for="opt in item?.options || []"
:key="opt.value"
:label="opt.label"
:value="opt.value"
:disabled="opt.disabled"
/>
</el-select>
<el-radio-group v-else-if="item.tags === 'el-radio'" v-model="formModel[item.prop]" v-bind="item.attrs">
<el-radio v-for="opt in item.options" :key="opt.value" :label="opt.value" :disabled="opt.disabled">
{{ opt.label }}
</el-radio>
</el-radio-group>
<el-checkbox-group
v-else-if="item.tags === 'el-checkbox'"
v-model="formModel[item.prop]"
v-bind="item.attrs"
>
<el-checkbox v-for="opt in item.options" :key="opt.value" :label="opt.value" :disabled="opt.disabled">
{{ opt.label }}
</el-checkbox>
</el-checkbox-group>
</template>
</el-form-item>
<template v-else-if="item.slot">
<slot :name="item.slot" />
</template>
</el-col>
</template>
<template v-if="$slots.formBtn">
<slot name="formBtn" :scope="{ formModel }" />
</template>
</el-row>
</el-form>
</template>
<script setup lang="ts" name="DxForm">
const filterProps = (obj: any, list: Array<string>) => {
return Object.keys(obj || {}).reduce((pre: any, cur: string) => {
return (list || []).includes(cur) ? { ...pre } : { ...pre, [cur]: obj[cur] };
}, {});
};
const typeList = [
"el-input",
"el-textarea",
"el-input-number",
"el-switch",
"el-date-picker",
"el-time-picker",
"el-el-slider",
"el-cascader",
"el-upload",
];
export interface FormProps {
rowProps?: Record<string, any>; // 同 el-row props 属性
gutter?: number;
span?: number;
filterFormItem?: string[];
clearFormModel?: boolean;
[key: string]: any;
}
export interface FormItems {
prop?: string;
tags?: string;
label?: string;
span?: number;
labelSlot?: string;
slot?: string;
errorSlot?: string;
options?: Record<string, any>[];
attrs?: Record<string, any>;
colProps?: Record<string, any>;
formItemProps?: Record<string, any>;
}
const props = defineProps<{
formModel: Record<string, any>;
formProps?: FormProps;
formItems: Array<FormItems>;
}>();
const formRef = ref<any>();
const formItemsFilter = computed(() => {
const formItems = props.formItems;
const filterFormItem = props?.formProps?.filterFormItem;
if (filterFormItem && Array.isArray(filterFormItem)) {
return formItems.filter((item: any) => !filterFormItem.includes(item.prop));
} else return formItems;
});
const safeRowProps = computed(() => {
const rowProps = props?.formProps?.rowProps;
const isHasRowProps = rowProps && typeof rowProps === "object";
const gutter = (() => {
if (isHasRowProps) {
const isHasRowPropsGutter = Object.hasOwnProperty.call(rowProps, "gutter");
if (isHasRowPropsGutter) return rowProps.gutter;
}
const isHasFormPropsGutter = Object.hasOwnProperty.call(props.formProps, "gutter");
return isHasFormPropsGutter ? props.formProps?.gutter : 0;
})();
return isHasRowProps ? { ...rowProps, gutter } : { gutter };
});
const getColProps = (item: any) => {
const colProps = item?.colProps;
const isHasColProps = colProps && typeof colProps === "object";
const span = (() => {
if (isHasColProps) {
const isHasColPropsSpan = Object.hasOwnProperty.call(colProps, "span");
if (isHasColPropsSpan) return colProps.span;
}
const isHasItemSpan = Object.hasOwnProperty.call(item, "span");
if (isHasItemSpan) return item.span;
const isHasFormPropsSpan = Object.hasOwnProperty.call(props.formProps, "span");
return isHasFormPropsSpan ? props.formProps?.span : 24;
})();
return isHasColProps ? { ...colProps, span } : { span };
};
const getFormItemProps = (item: any) => {
const filterArray = ["prop", "label", "labelSlot", "slot", "errorSlot"];
const formItemProps = item?.formItemProps;
const isHasFormItemProps = formItemProps && typeof formItemProps === "object";
return isHasFormItemProps ? filterProps(formItemProps, filterArray) : {};
};
const safeFormProps = computed(() => {
const filterArray = ["formModel", "rowProps", "gutter", "span", "filterFormItem", "clearFormModel"];
return filterProps(props.formProps, filterArray);
});
const emit = defineEmits(["search", "reset", "update:formModel"]);
const search = () => {
if (!formRef.value) return;
const rules = props.formProps?.rules;
if (rules && Object.keys(rules).length > 0) {
formRef.value.validate((valid: boolean) => {
if (valid) emit("search", props.formModel);
});
} else {
emit("search", props.formModel);
}
};
const reset = () => {
const clearFormModel = props?.formProps?.clearFormModel;
if (clearFormModel) {
props.formItems.forEach((item: any) => {
const prop = item.prop;
if (Object.hasOwnProperty.call(props.formModel, prop)) {
props.formModel[prop] = "";
}
});
}
if (formRef.value?.resetFields) {
formRef.value.resetFields();
}
};
defineExpose({
search,
reset,
formRef: formRef.value,
});
const setUpWatchers = () => {
props.formItems.forEach((item: any) => {
const changeRef = toRef(props.formModel, item.prop);
watch(
changeRef,
(val: any) => {
emit("update:formModel", item.prop, val);
},
{
deep: true,
flush: "post",
}
);
});
};
setUpWatchers();
</script>
这个 form
部分代码,封装成 forms
。
table 部分
table
部分涵盖分页,以联动。
<template>
<div class="km-table">
<div class="km-table-wrap">
<div class="km-table-position">
<el-table :data="props.data" :span-method="mergedSpanMethod" v-bind="safeTableProps" v-loading="props.loading">
<template v-for="item in props.columns" :key="item.prop">
<el-table-column v-bind="item">
<template v-if="item.slot" #default="scope">
<slot :name="item.slot" v-bind="scope" />
</template>
</el-table-column>
</template>
</el-table>
</div>
</div>
<div class="km-table-pagination-wrap" v-if="props.pagination">
<el-pagination
:current-page="props.pagination.currentPage"
:page-size="props.pagination.pageSize"
:total="props.pagination.total"
v-bind="safePaginationProps"
@update:current-page="val => emit('update:currentPage', val)"
@update:page-size="val => emit('update:pageSize', val)"
/>
</div>
</div>
</template>
<script setup lang="ts">
export interface DxTableType {
columns: Array<{
prop: string;
label: string;
slot?: string;
mergeField?: boolean;
[key: string]: any;
}>;
data: any[];
loading?: boolean;
tableProps: Record<string, any>;
pagination?: {
currentPage: number;
pageSize: number;
total: number;
};
paginationProps: Record<string, any>;
}
const props = defineProps<DxTableType>();
const emit = defineEmits(["update:currentPage", "update:pageSize"]);
const filterProps = (obj: any, list: Array<string>) => {
return Object.keys(obj || {}).reduce((pre: any, cur: string) => {
return (list || []).includes(cur) ? { ...pre } : { ...pre, [cur]: obj[cur] };
}, {});
};
const safeTableProps = computed(() => {
return filterProps(props.tableProps || {}, ["data", "spanMethod", "loading"]);
});
const safePaginationProps = computed(() => {
return filterProps(props.paginationProps || {}, ["currentPage", "pageSize", "total"]);
});
const mergedSpanMethod = ({ rowIndex, columnIndex }: { rowIndex: number; columnIndex: number }) => {
const column = props.columns[columnIndex];
if (!column || !column.mergeField) return [1, 1];
const field = column.prop;
const currentValue = props.data[rowIndex]?.[field];
const prevValue = props.data[rowIndex - 1]?.[field];
if (currentValue === prevValue) return [0, 0];
let rowspan = 1;
for (let i = rowIndex + 1; i < props.data.length; i++) {
if (props.data[i][field] === currentValue) {
rowspan++;
} else {
break;
}
}
return [rowspan, 1];
};
</script>
<style lang="scss" scoped>
.km-table {
width: 100%;
display: flex;
flex-direction: column;
.km-table-wrap {
flex: 1;
position: relative;
}
.km-table-position {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.km-table-pagination-wrap {
padding: 20px;
}
}
</style>
结合
page
组件最终。
<template>
<div class="dx-layout">
<DxHeader v-if="pageConfig.header" :title="pageConfig.header.title" :back="pageConfig.header.back" />
<div class="dx-layout-form" v-if="Object.keys(pageConfig.form.formItems).length > 0">
<DxForm
ref="dxFormRef"
:formItems="mergedFormItemsProps"
:formModel="pageConfig.form.formModel"
:formProps="mergedFormProps"
@update:form-model="setFormModel"
>
<template v-for="formSlot in formSlots" :key="formSlot" #[formSlot]="scope">
<slot :name="formSlot" v-bind="scope"></slot>
</template>
<template #queryBtn>
<el-button @click="reset">重置</el-button>
<el-button type="primary" @click="queryData">查询</el-button>
</template>
</DxForm>
</div>
<div class="dx-layout-formBtn" v-if="$slots.pageBtn">
<slot name="pageBtn"></slot>
</div>
<div class="dx-layout-table">
<DxTable
:columns="pageConfig.table.columns"
:data="tableData"
:loading="loading"
:tableProps="mergedTableProps"
:pagination="pagination"
:paginationProps="pageConfig.table.paginationProps"
@update:currentPage="
val => {
pagination.currentPage = val;
queryData();
}
"
@update:pageSize="
val => {
pagination.pageSize = val;
pagination.currentPage = 1;
queryData();
}
"
>
<template v-for="tableSlot in tableSlots" :key="tableSlot" #[tableSlot]="scope">
<slot :name="tableSlot" v-bind="scope"></slot>
</template>
</DxTable>
</div>
</div>
</template>
<script setup lang="ts">
import DxHeader, { type Header } from "./DxHeader.vue";
import DxForm, { type FormProps, type FormItems } from "./DxForm.vue";
import DxTable, { type DxTableType } from "./DxTable.vue";
import { useDebounceFn } from "@vueuse/core";
export interface PageConfig {
header: Header;
form: {
formItems: Array<FormItems>;
formModel: Record<string, any>;
formProps?: FormProps;
};
table: DxTableType;
}
const props = defineProps<{
pageConfig: PageConfig;
}>();
const dxFormRef = ref();
const emit = defineEmits(["update:formModel"]);
// 更改为高阶防抖,执行一部分,一部分延迟
const setFormModel = useDebounceFn((prop: string, val: any) => {
emit("update:formModel", prop, val);
pagination.value.currentPage = 1;
queryData();
}, 600);
const tableData = ref<any>([]);
const { currentPage, pageSize, total } = props.pageConfig.table.paginationProps;
const pagination = ref({
currentPage: currentPage || 1,
pageSize: pageSize || 10,
total: total || 0,
});
const defaultTableProps = {
height: "100%",
showHeader: true,
};
const defaultFormProps = {
gutter: 20,
span: 6,
labelWidth: "120px",
};
const mergedTableProps = computed(() => ({
...defaultTableProps,
...props.pageConfig.table.tableProps,
}));
const mergedFormProps = computed(() => ({
...defaultFormProps,
...props.pageConfig.form.formProps,
}));
const mergedFormItemsProps = computed(() => {
const formItems = props.pageConfig.form.formItems;
const { filterFormItem } = props.pageConfig.form?.formProps || {};
const calcFinalspan = (() => {
const totalUsed = (formItems || []).reduce((sum, item) => {
const isIn = item.prop && (filterFormItem || []).includes(item.prop);
return isIn ? sum : sum + (item?.span || 6);
}, 0);
const remain = 24 - (totalUsed % 24);
if (remain < 6) return 24;
return remain;
})();
return [
...formItems,
{
slot: "queryBtn",
span: calcFinalspan,
colProps: {
style: { display: "flex", justifyContent: "flex-end" },
},
},
];
});
const loading = ref<boolean>(false);
const queryData = async () => {
const params = {
...props.pageConfig.form.formModel,
currentPage: pagination.value.currentPage,
pageSize: pagination.value.pageSize,
};
console.log("查询参数", params);
try {
loading.value = true;
const res = await props.pageConfig.table.api(params);
tableData.value = res.data || [];
pagination.value.total = res.total;
pagination.value.pageSize = res.pageSize;
pagination.value.currentPage = res.currentPage;
} catch (e) {
console.error("查询失败", e);
} finally {
loading.value = false;
}
};
const reset = () => {
dxFormRef.value.reset();
pagination.value.currentPage = 1;
queryData();
};
// 插槽处理
const slots: any = useSlots();
const slotsNames = Object.keys(slots);
const formSlots = (props.pageConfig.form.formItems || []).reduce(
(pre: any, cur: any) => {
const list: string[] = [];
if (cur.slot && typeof cur.slot === "string" && slotsNames.includes(cur.slot)) list.push(cur.slot);
if (cur.labelSlot && typeof cur.labelSlot === "string" && slotsNames.includes(cur.labelSlot))
list.push(cur.labelSlot);
if (cur.errorSlot && typeof cur.errorSlot === "string" && slotsNames.includes(cur.errorSlot))
list.push(cur.errorSlot);
return [...pre, ...list];
},
slotsNames.includes("formBtn") ? ["formBtn"] : []
);
const tableSlots = (props.pageConfig.table.columns || [])
.filter((item: any) => item.slot && typeof item.slot === "string" && slotsNames.includes(item.slot))
.map((item: any) => item.slot);
queryData();
</script>
<style lang="scss" scoped>
.dx-layout {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
.dx-layout-table {
padding: 0 20px;
flex: 1;
display: flex;
}
.dx-layout-form {
padding: 20px 20px 0 20px;
}
.dx-layout-formBtn {
padding: 0 20px 20px 20px;
}
}
</style>