前言
阿里的antd组件说实话真的非常完善了,可是vue版的寥寥无几,vue3的更少了,今天有空就封装一个高仿的ProTable
开始
其实很简单的,我们是基于vue版的antd进行二次封装,我们把表格组件联动到表单组件即可,然后预留一些位置让组件灵活的更高。
介绍
注意点:
- antd的表单组件必须是全局组件
- ProTable算是一个半成品(满足基本使用)
- 需要vuedraggable、screenfull依赖
- 把它转成element 组件的话也非常简单
完成度:
- 自动生成查询表单(支持antd全部表单组件)
- 表格头部、头部下边(可做勾选了多少列)、底部、表单查询、自定义表单查询的卡槽都有空余出来
- 表格的列设置、全屏、密度
其实完成的差不多了,满足我们日常开发需求了。
使用
全局组件(main.ts):
import ProTable from './components/ProTable/IndexView.vue'
app.component('ProTable', ProTable);
ProTable组件代码:
<template>
<div>
<div style="margin: 10px 0px; padding: 24px 24px; background-color: #fff" >
<!--自定义查询-->
<slot name="definitionForm"></slot>
<!--固定查询-->
<a-form
v-if="formShowPage"
ref="formRef"
name="advanced_search_form"
:model="form"
@finish="onFormFinish"
>
<a-row :gutter="24">
<template v-for="(item, key) in formDataList" :key="key">
<a-col v-if="item.search" v-show="formExpand || key <= 1" :span="8" >
<a-form-item
:name="`${item.key}`"
:label="`${item.title}`"
:rules="item.rules"
>
<template v-if="item.valueType">
<component :is="`${item.valueType}`" style="width: 100%"
:options="item.valueEnum"
:tree-data="item.valueEnum"
v-model:value="form[item.key]"
:placeholder="item.placeholder ? item.placeholder : `请输入${item.title}`"
:value-format="item.format"
:show-time="item.showTime"
:picker="item.picker"
></component>
</template>
<template v-else>
<a-input v-model:value="form[`${item.key}`]" :placeholder="`请输入${item.title}`"></a-input>
</template>
</a-form-item>
</a-col>
</template>
<a-col :span="formExpand ? 24 : 8">
<a-row type="flex" justify="end">
<a-col>
<!--固定查询左边-->
<slot name="searchFormLeft"></slot>
</a-col>
<a-col>
<a-button type="primary" html-type="submit">查询</a-button>
<a-button style="margin: 0 8px" @click="() => formRef.resetFields()">重置</a-button>
<a style="font-size: 12px" @click="formExpand = !formExpand">
<template v-if="formExpand">
<UpOutlined />
收起
</template>
<template v-else>
<DownOutlined />
展开
</template>
</a>
</a-col>
</a-row>
</a-col>
</a-row>
</a-form>
</div>
<!--表格全屏容器-->
<div ref="tableRef" style="background-color: #fff">
<a-table
:row-selection="{ selectedRowKeys: tableSelectedRowKeys, onChange: onTableSelectChange }"
:columns="tableColumns"
:data-source="tableData"
:size="tableDensityCurrent[0]"
:loading="tableLoading"
:pagination="tablePagination"
:scroll="tableScroll"
>
<template #title>
<a-row type="flex">
<a-col :span="12">
<!--表格头部左边-->
<slot name="tableHeaderLeft"></slot>
</a-col>
<a-col :span="12" style="text-align: right; cursor: pointer">
<a-row type="flex" justify="end">
<a-col style="margin-right: 16px;">
<!--表格头部右边-->
<slot name="tableHeaderRight"></slot>
</a-col>
<a-col style="margin-right: 16px;">
<span>
<a-tooltip>
<template #title>刷新</template>
<RedoOutlined style="font-size: 16px" @click="onTableRefreshClick" />
</a-tooltip>
</span>
</a-col>
<a-col style="margin-right: 16px;">
<span>
<a-dropdown trigger="click" placement="bottom">
<a-tooltip>
<template #title>密度</template>
<ColumnHeightOutlined style="font-size: 16px" />
</a-tooltip>
<template #overlay>
<a-menu :theme="layoutModule.navBgStyle ? 'dark' : 'light'" style="width: 80px; text-align: center"
v-model:selectedKeys="tableDensityCurrent"
@click="onTableDensityChange"
>
<a-menu-item key="default">
<a href="javascript:;">默认</a>
</a-menu-item>
<a-menu-item key="middle">
<a href="javascript:;">中等</a>
</a-menu-item>
<a-menu-item key="small">
<a href="javascript:;">紧凑</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</span>
</a-col>
<a-col style="margin-right: 16px;">
<span>
<a-popover trigger="click" placement="bottomRight">
<a-tooltip>
<template #title>列设置</template>
<SettingOutlined style="font-size: 16px" />
</a-tooltip>
<template #title>
<a-checkbox style="padding: 5px 0px" v-model:checked="columnSetCheckAll" :indeterminate="columnSetIndeterminate" @change="onTableColumnSetCheckAllChange">
<span> 列展示 / 排序 </span>
<a href="javascript:;" @click="onTableColumnSetResetClick">重置</a>
</a-checkbox>
</template>
<template #content>
<draggable
v-model="columnSetDataList"
group="people"
@start="drag=true"
@end="drag=false"
@change="onTableColumnSetDraggableChange"
item-key="title"
>
<template #item="{ element }">
<div style="cursor: pointer; margin-left: -14px;">
<HolderOutlined style="font-size: 14px; color: rgba(0,0,0,.45)" />
<a-checkbox v-model:checked="element.checked">{{element.title}}</a-checkbox>
</div>
</template>
</draggable>
</template>
</a-popover>
</span>
</a-col>
<a-col>
<span>
<a-tooltip>
<template #title>全屏</template>
<fullscreenExitOutlined v-if="tableFullscreen" @click="onTableFullScreenClick" style="font-size: 16px"/>
<FullscreenOutlined v-else @click="onTableFullScreenClick" style="font-size: 16px" />
</a-tooltip>
</span>
</a-col>
</a-row>
</a-col>
</a-row>
<!--表格头部下边-->
<slot name="tableHeaderBottom"></slot>
</template>
<!--个性化单元格-->
<template #bodyCell="{ text, record, index, column }">
<slot name="tableBodyCell" v-bind="{text, record, index, column}"></slot>
</template>
<!--自定义筛选菜单(加个变量控制不然会导致空白)-->
<!-- <template #customFilterDropdown="{ column }">-->
<!-- <slot name="tableCustomFilterDropdown" v-bind="{column}"></slot>-->
<!-- </template>-->
<!--自定义筛选图标(加个变量控制不然会导致空白)-->
<!-- <template #customFilterIcon="{ filtered, column }">-->
<!-- <slot name="tableCustomFilterIcon" v-bind="{ filtered, column }"></slot>-->
<!-- </template>-->
<!--自定义空数据时的显示内容-->
<!-- <template #emptyText>-->
<!-- <slot name="tableEmptyText"></slot>-->
<!-- </template>-->
<!--自定义总结栏-->
<!-- <template #summary>-->
<!-- <slot name="tableSummary"></slot>-->
<!-- </template>-->
<template #footer>
<!--表格底部-->
<slot name="tableFooter"></slot>
</template>
</a-table>
</div>
</div>
</template>
<script lang="ts">
import { reactive, watch, toRefs, onMounted, onUnmounted } from "vue"
import { useStore } from 'vuex'
import { RedoOutlined, ColumnHeightOutlined, SettingOutlined, FullscreenOutlined,
FullscreenExitOutlined, HolderOutlined, DownOutlined, UpOutlined } from '@ant-design/icons-vue';
import draggable from 'vuedraggable'
import screenfull from "screenfull";
export default {
name: 'ProTable',
components: {
RedoOutlined,
ColumnHeightOutlined,
SettingOutlined,
FullscreenOutlined,
FullscreenExitOutlined,
HolderOutlined,
DownOutlined, UpOutlined,
draggable
},
props: {
formShow: {
type: Boolean,
required: false,
default: () => {
return true
}
},
loading: {
type: Boolean,
required: false,
default: () => {
return false
}
},
sourceColumns: {
type: Array,
required: true,
default: () => {
return []
}
},
sourceData: {
type: Array,
required: true,
default: () => {
return []
}
},
pagination: {
type: Array,
required: false,
default: () => {
return []
}
},
scroll: {
type: Object,
required: false,
default: () => {
return {}
}
}
},
setup(props: any, ctx: any) {
// https://www.antdv.com/components/table-cn#Column
// 与Column相同,下列写自行添加的字段
interface ColumnsItem {
search?: boolean;
valueType?: string;
initialValue?: any;
valueEnum?: any;
rules?: any;
format?: string;
showTime?: boolean;
placeholder?: string | string[];
}
const { sourceColumns, sourceData, loading, pagination, scroll, formShow } = toRefs(props);
const columns:ColumnsItem = sourceColumns.value;
const tableData:any = sourceData.value;
const tableLoading:any = loading;
const formShowPage:any = formShow;
const tablePagination:any = pagination;
const tableScroll:any = scroll;
const store = useStore();
const layoutModule = store.state.layout;
const dataCheckedList:any = JSON.parse(JSON.stringify(columns));
dataCheckedList.forEach((item:any) => {
item.checked = true
});
// 表单
interface FormStateType {
formRef: HTMLElement | string;
formExpand: boolean;
formDataList: any,
form: []
}
const formState = reactive<FormStateType>({
formRef: '',
formExpand: false,
formDataList: JSON.parse(JSON.stringify(columns)),
form: []
});
formState.formDataList.forEach((item: any) => {
// 默认是文本框搜索
if (!item.valueType) {
item.valueType = 'a-input'
}
// 默认值赋值
if (item.initialValue) {
formState.formDataList[item.key] = item.initialValue
}
});
// 表单提交
const onFormFinish = (values: any) => {
ctx.emit("finish", values);
};
interface TableStateType {
tableRef: HTMLElement | undefined;
tableSelectedRowKeys: [];
tableDensityCurrent: string[];
tableFullscreen: boolean;
tableColumns: any;
}
// 表格
const tableState = reactive<TableStateType>({
tableRef: undefined,
tableSelectedRowKeys: [],
tableDensityCurrent: ['middle'],
tableFullscreen: false,
tableColumns: JSON.parse(JSON.stringify(columns))
});
// 表格选中切换
const onTableSelectChange = (selectedRowKeys: []) => {
tableState.tableSelectedRowKeys = selectedRowKeys;
};
// 表格刷新
const onTableRefreshClick = () => {
ctx.emit("refresh");
};
// 表格密集度
const onTableDensityChange = (item: any) => {
tableState.tableDensityCurrent = item.keyPath
};
// 表格列设置
const columnSetState = reactive({
columnSetIndeterminate: false,
columnSetCheckAll: true,
columnSetDataList: JSON.parse(JSON.stringify(dataCheckedList)),
});
// 表格列设置重置
const onTableColumnSetResetClick = () => {
columnSetState.columnSetIndeterminate = false;
columnSetState.columnSetCheckAll = true;
columnSetState.columnSetDataList = JSON.parse(JSON.stringify(dataCheckedList));
};
// 表格列设置拖拽
const onTableColumnSetDraggableChange = () => {
tableProcessingSelectColumnsData();
};
// 数据处理
const tableProcessingSelectColumnsData = () => {
let selectData: any[] = [];
let noSelectData: any[] = [];
columnSetState.columnSetDataList.forEach((item: any) => {
if(item.checked) {
selectData.push(item);
} else {
noSelectData.push(item);
}
});
if (selectData.length === 0) {
selectData.push({});
}
tableState.tableColumns = selectData;
};
// 表格列设置全选
const onTableColumnSetCheckAllChange = (e: any) => {
if (e.target.checked) {
// 全选中
columnSetState.columnSetDataList.forEach((item: any) => {
item.checked = true
});
columnSetState.columnSetCheckAll = true;
columnSetState.columnSetIndeterminate = false;
} else {
// 全取消
columnSetState.columnSetDataList.forEach((item: any) => {
item.checked = false
});
columnSetState.columnSetCheckAll = false;
columnSetState.columnSetIndeterminate = false;
}
};
watch(() => columnSetState.columnSetDataList, (newVal) => {
let checkAllList = [];
let indeterminate = false;
for(let i =0; i < newVal.length; i++) {
let item:any = newVal[i];
if (item.checked) {
indeterminate = true;
checkAllList.push(item);
}
}
if (checkAllList.length == newVal.length) {
columnSetState.columnSetCheckAll = true;
columnSetState.columnSetIndeterminate = false;
} else {
columnSetState.columnSetCheckAll = false;
if (indeterminate) {
columnSetState.columnSetIndeterminate = true;
} else {
columnSetState.columnSetIndeterminate = false;
}
}
tableProcessingSelectColumnsData()
},
{
deep: true, // 深度监听
immediate: false
}
);
// 表格全屏
const onTableFullScreenClick = () => {
if (screenfull.isEnabled) {
screenfull.toggle(tableState.tableRef);
}
};
const onTableFullScreenChange = () => {
tableState.tableFullscreen = screenfull.isFullscreen
};
// 设置监听器
onMounted(() => {
screenfull.on('change', onTableFullScreenChange)
});
// 删除监听器
onUnmounted(() => {
screenfull.off('change', onTableFullScreenChange)
});
return {
tableLoading,
tableScroll,
tablePagination,
formShowPage,
...toRefs(formState),
onFormFinish,
tableData,
...toRefs(tableState),
...toRefs(columnSetState),
onTableSelectChange,
onTableRefreshClick,
onTableDensityChange,
onTableColumnSetResetClick,
onTableColumnSetDraggableChange,
onTableColumnSetCheckAllChange,
onTableFullScreenClick,
layoutModule,
}
},
}
</script>
<style scoped>
</style>
使用代码:
<template>
<div>
<div style="height: 20px; margin: 10px 0px; background-color: #fff">
首页 / 测试页
</div>
<div style="background-color: #f0f2f5; padding: 24px">
<ProTable :sourceColumns="columns" :sourceData="data" :formShow="true"
@finish="finish" @refresh="refresh"
>
<!--自定义某字段样式-->
<template #tableBodyCell="{ column }">
<template v-if="column.key === 'operation'">
<a>编辑</a>
</template>
</template>
</ProTable>
</div>
</div>
</template>
<script lang="ts">
const columns = [
{ title: '序号', width: 100, dataIndex: 'age', key: 'age',
search: true,
valueType: 'a-input-number',
initialValue: 100,
},
{ title: '姓名', width: 100, dataIndex: 'name', key: 'name',
search: true,
valueType: 'a-input',
sorter: true,
rules: [
{
required: true,
message: '请输入序号',
},
{
min: 3,
max: 5,
message: '序号长度 3 to 5',
trigger: 'blur',
}
]
},
{ title: '密码', width: 100, dataIndex: 'password', key: 'password',
search: true,
filters: [
{
text: 'Joe',
value: 'Joe',
},
{
text: 'John',
value: 'John',
},
],
},
{ title: '日期选择框', width: 100, dataIndex: 'datepicker', key: 'datepicker',
search: true,
valueType: 'a-date-picker',
initialValue: '2022/08/01',
format: 'YYYY/MM/DD'
},
{ title: '日期范围选择框', width: 100, dataIndex: 'daterangepicker', key: 'daterangepicker',
search: true,
placeholder: ['测试日期开始时间', '测试日期结束时间'],
valueType: 'a-range-picker',
showTime: true
// initialValue: ['09:00:00'],
// format: 'YYYY:MM:DD'
},
{ title: '时间选择框', width: 100, dataIndex: 'timeepicker', key: 'timeepicker',
search: true,
valueType: 'a-time-picker',
initialValue: '09:00:00',
format: 'HH:mm:ss'
},
{ title: '时间范围选择框', width: 100, dataIndex: 'timerangepicker', key: 'timerangepicker',
search: true,
placeholder: ['测试时间开始时间', '测试时间结束时间'],
valueType: 'a-time-range-picker',
showTime: true,
// initialValue: ['09:00:00', '12:00:00'],
// format: 'HH:mm:ss'
},
{ title: '单选框', width: 100, dataIndex: 'radio', key: 'radio',
initialValue: 1,
valueEnum: [
{value: 0, label: '测试1'},
{value: 1, label: '测试2'},
{value: 2, label: '测试3'}
],
search: true, valueType: 'a-radio-group'
},
{ title: '测试级联选择框', width: 100, dataIndex: 'cascader', key: 'cascader',
initialValue: 'zhejiang',
valueEnum: [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
},
],
},
],
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
},
],
},
]
}
],
search: true, valueType: 'a-cascader'
},
{ title: '测试选择框', width: 100, dataIndex: 'select', key: 'select',
initialValue: 1,
valueEnum: [
{value: 0, label: '测试1'},
{value: 1, label: '测试2'},
{value: 2, label: '测试3'}
],
search: true, valueType: 'a-select'
},
{ title: '测试树形下拉框', width: 100, dataIndex: 'treeselect', key: 'treeselect',
// initialValue: 1,
valueEnum: [
{
title: 'parent 1',
value: 'parent 1',
children: [
{
title: 'parent 1-0',
value: 'parent 1-0',
children: [
{
title: 'my leaf',
value: 'leaf1',
},
{
title: 'your leaf',
value: 'leaf2',
},
],
},
{
title: 'parent 1-1',
value: 'parent 1-1',
},
],
},
],
search: true, valueType: 'a-tree-select'
},
{ title: '性别', dataIndex: 'sex', key: 'sex', width: 150,
search: true,
valueType: 'a-checkbox-group',
initialValue: ['b'],
valueEnum: ['a', 'b', 'c'],
rules: [
{
required: true,
message: '请输入性别',
}
]
},
{
title: '操作',
search: false,
key: 'operation',
width: 100
},
];
interface DataItem {
key: number;
name: string;
age: number;
password: string;
sex: string;
}
const data: DataItem[] = [];
for (let i = 0; i < 10; i++) {
data.push({
key: i,
name: `测试姓名 ${i}`,
age: 32,
password: `密码. ${i}`,
sex: '男'
});
}
export default {
mounted(): void {
console.log("IndexTestView - mounted")
},
components: {},
// data() {
// return {
// a: 1
// }
// },
// created() {
// console.log(this.a) // 1
// console.log(this.$data) // { a: 1 }
// },
setup() {
const finish = (values: any) => {
console.log(values)
};
const refresh = () => {
console.log("刷新")
};
return {
columns,
data,
finish,
refresh
}
},
}
</script>
<style scoped>
</style>
总结
这篇我们深度的使用了table、form组件进行了二次封装,加深了我们对antd组件库的使用以及增加 了我们对封装组件库的能力。