组件具备功能
- 表格有分页和触底两种加载数据两种风格(性能优化方式)
- 组件包含:表格组件、搜索组件、分页器组件
- hook包含:表格相关数据处理、拖拽功能
- 接受参数包括获取数据方法request、查询参数initParam、表格字段数组columns、mock数据(开启mock数据就请求数据)、获取数据后的回调函数handleData(对接口返回数据进行处理后返回)、pagination是否显示分页器或触底加载、supportUrge是否支持多选等。
- 某个字段数据特殊处理:使用插槽方式,表格组件封装时预留具名插槽,字段名为插槽名称,可在父组件自定义该字段的展示方式。
- 搜索实现:搜索组件自定事件search,父组件监听,拿到搜索条件后,更改查询参数。表格组件侦听属性监听initParam的变化重新请求数据实现搜索功能。
- 分页实现:分页器组件监听页码,当前页的变化,发出自定义事件,表格组件修改initParam,触发数据请求方法。
- 触底加载实现:监听表格滑动区域,触底修改initParam的当前页码触发请求下一页的数据。
- 批量操作实现:表格组件自定义选中事件通过参数带出选中数据行,父组件监听并拿到数据,实现编辑删除表格某条数据操作。
- 拖拽实现:使用sortablejs库进行封装
① 准备两个数组oldList用于展示初始字段名 newList用于新增或者拖拽后的字段名。
② 通过类名draggable制定某些列可以拖拽,新增列时给该列赋予类名draggable,钩子onMove(){//获取类名判断是否可以拖拽,返回布尔值}。钩子onEnd(evt){//拖拽后处理newList移动拖拽的字段位置,evt会有oldIndex和newIndex}
③ 新增表格数据,添加表格字段tableItems.columnName,从可选数据中获取到columnName的系数添加进表格数据tableData.columnName.coefficient。更新oldList和newList。
- mock数据:使用mockjs库实现
项目目录
├─ src
│ ├─ assets
│ ├─ components
│ │ ├─ STable
│ │ │ ├─ SearchForm.vue
│ │ │ ├─ STable.vue
│ │ │ ├─ useTable.js
│ │ │ ├─ Pagination.vue
│ │ ├─ veBaseComponents
│ │ │ ├─ VeCommonPagination.vue
│ │ │ ├─ VeCommonTable.vue
表格组件
<template>
<div class="table">
<VeCommonTable
ref="tableRef"
:rowKey="selectParam"
:data="tableData"
:border="border"
:stripe="stripe"
v-loading="loading"
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
@expand-change="onExpandChange"
v-bind="$attrs"
>
<template v-for="item in tableColumns" :key="item">
<el-table-column
v-if="item.type === 'selection' || item.type === 'index'"
:type="item.type"
:reserve-selection="item.type === 'selection'"
:label="item.label"
:width="item.width"
:min-width="item.minWidth"
:fixed="item.fixed"
:selectable="canSelect"
>
</el-table-column>
<el-table-column
v-if="!item.type && item.prop && item.isShow"
:prop="item.prop"
:label="item.label"
:width="item.width"
:min-width="item.minWidth"
:sortable="item.sortable"
:show-overflow-tooltip="item.prop !== 'operation'"
:resizable="true"
:fixed="item.fixed"
align="center"
>
<template #default="scope">
<slot :name="item.prop" :row="scope.row">
{{ scope.row[item.prop] }}
</slot>
</template>
</el-table-column>
</template>
</VeCommonTable>
<Pagination
v-if="pagination"
:pageable="pageable"
:handleSizeChange="handleSizeChange"
:handleCurrentChange="handleCurrentChange"
style="height: 42px; margin: 12px auto"
/>
</div>
</template>
<script setup>
import { watch, ref } from "vue";
import { useTable } from "./useTable.js";
import VeCommonTable from "../veBaseComponents/VeCommonTable.vue";
import Pagination from "./Pagination.vue";
const props = defineProps({
columns: { type: Array, default: [] },
border: { type: Boolean, default: false },
request: {
type: Function,
default: () => {},
},
dataPosition: { type: String, default: "" },
initParam: {},
isPageable: { type: Boolean, default: true },
dataCallback: { type: Function, default: undefined },
stripe: { type: Boolean, default: true },
pagination: { type: Boolean, default: true }, // 是否需要分页
selectParam: { type: String, default: "id" },
isNeedLoad: { type: Boolean, default: true },
mockData: { type: Array },
description: { type: String, default: "暂无数据" },
isSingleChoose: { type: Boolean, default: false },
});
const canSelect = (row) => row?.supportUrge !== 1;
const emits = defineEmits([
"selectionChange",
"rowClick",
"currentRowChange",
"expandChange",
]);
// 表格列
const tableColumns = ref([]);
watch(
() => props.columns,
(newColumns) => {
tableColumns.value = newColumns.map((item) => {
return {
...item,
isShow: typeof item.isShow === "undefined" || item.isShow,
};
});
},
{ immediate: true, deep: true }
);
const {
tableData,
loading,
pageable,
reset,
resetPage,
getTableList,
handleSizeChange,
handleCurrentChange,
} = useTable(
props.request,
props.initParam,
props.isPageable,
props.dataCallback,
props.isNeedLoad,
props.mockData,
props.dataPosition
);
// 监听页面 initParam 改化,重新获取表格数据,无论切换页码还是查询数据都是通过这个参数实现的
watch(
() => props.initParam,
(val) => getTableList({ ...val }),
{ deep: true }
);
const tableRef = ref();
const handleSelectionChange = (selection) => {
if (props.isSingleChoose) {
if (selection.length > 1) {
tableRef.value.clearSelection();
tableRef.value.toggleRowSelection(selection.pop(), true);
} else if (selection.length == 1) {
emits("selectionChange", selection);
}
} else {
emits("selectionChange", selection);
}
};
// 单击表格某行
const handleRowClick = (row, column, e) =>
emits("rowClick", { row, column, e });
// 列表展开/收起处理 -----start------/
const expands = ref([]); // 当前表格展开的所有行对应的id
watch(
() => tableData.value,
(val) => {
expands.value = [];
emits("expandChange", [...expands.value]);
}
);
const onExpandChange = (row, expanded) => {
const len = expands.value.length;
if (expanded) {
expands.value.push(row.id);
} else {
for (let i = 0; i <= len; i++) {
if (expands.value[i] == row.id) {
expands.value.splice(i, 1);
}
}
}
emits("expandChange", [...expands.value]);
};
// 列表展开/收起处理 -----end------/
const getPage = () => pageable.value;
const clearSelection = () => {
tableRef?.value?.tableRef?.clearSelection?.();
};
const toggleRowExpansion = (isUnfold) => {
const table = tableRef?.value?.tableRef?.data;
if (table) {
table.forEach((row) => {
tableRef?.value?.tableRef?.toggleRowExpansion?.(row, isUnfold);
});
}
};
defineExpose({
getTableList,
getPage,
reset,
resetPage,
clearSelection,
toggleRowExpansion,
});
</script>
<style scoped lang="scss">
.table {
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
:deep(.el-table) {
flex: 1;
height: 100%;
}
}
</style>
import { reactive, computed, onMounted, toRefs, inject } from "vue";
import { GlobalStore } from "@/store";
export const useTable = (request, initParam, isPageable, dataCallBack, isNeedLoad, mockData, dataPosition) => {
const state = reactive({
tableData: [], // 表格数据
pageable: {
pageIndex: 1, //当前页数
pageSize: 50, // 每页条数
total: 0, // 总条数
},
searchParam: {}, // 查询参数(只包括查询)
searchInitParam: {}, // 初始化默认的查询参数
totalParam: {}, // 总参数(包含分页和查询参数)
loading: false,
});
onMounted(() => {
if (isNeedLoad) {
reset();
}
});
const reset = () => {
// 分页数据重置
state.pageable.pageIndex = GlobalStore().initTablePage || 1;
state.pageable.pageSize = 50;
Object.keys(state.searchInitParam).forEach((key) => { state.searchParam[key] = state.searchInitParam[key] });
updatedTotalParam();
getTableList();
};
const resetPage = () => {
// 分页数据重置
state.pageable.pageIndex = 1;
state.pageable.pageSize = 50;
};
const updatedTotalParam = () => {
state.totalParam = {};
// 处理查询参数,可以给查询参数加自定义前缀操作
let nowSearchParam = {};
for (let key in state.searchParam) {
if (state.searchParam[key] || state.searchParam[key] === false || state.searchParam[key] === 0) {
nowSearchParam[key] = state.searchParam[key];
}
}
Object.assign(state.totalParam, nowSearchParam, isPageable ? pageParam.value : {});
};
// 分页参数
const pageParam = computed({
get: () => {
return {
pageIndex: state.pageable.pageIndex,
pageSize: state.pageable.pageSize,
};
},
});
const getTableList = async(newVal) => {
try {
state.loading = true;
Object.assign(state.totalParam, newVal ? newVal : initParam, isPageable ? pageParam.value : {});
if (mockData) {
state.tableData = mockData
return
}
let res = await request(state.totalParam);
dataPosition ? res.data = res.data[dataPosition] : '';
const data = (dataCallBack && await dataCallBack(res.data.data)) || res.data.data;
let index = 0;
for (let i = 0; i < data.length; i++) {
const item = data[i];
if (item.submissionList) {
// item.id = item.vehicleClassDistributionId;
item.id = (++index + new Date().getTime()).toString();
item.no = `${i + 1}`;
item.children = item.submissionList;
for (let j = 0; j < item.children.length; j++) {
const subItem = item.children[j];
subItem.id = (++index + new Date().getTime()).toString();
subItem.no = `${i + 1}.${j + 1}`
}
} else {
item.id = (++index + new Date().getTime()).toString();
item.no = `${i + 1}`;
}
}
state.tableData = data
const { total, count } = res.data;
isPageable && updatePageable({ total: total || count || 0 });
} catch (error) {} finally {
state.loading = false;
}
};
const updatePageable = (resPageable) => Object.assign(state.pageable, resPageable);
const handleSizeChange = (size) => {
state.pageable.pageIndex = 1;
state.pageable.pageSize = size;
getTableList();
};
const handleCurrentChange = (currentPage) => {
state.pageable.pageIndex = currentPage;
getTableList();
};
const search = () => {
state.pageable.pageIndex = 1;
updatedTotalParam();
getTableList();
};
return {
...toRefs(state),
getTableList,
reset,
resetPage,
search,
handleSizeChange,
handleCurrentChange,
};
};
分页器组件
<template>
<el-table
v-bind="getBind()"
ref="tableRef"
row-key="id"
:row-style="rowStyle"
>
<slot></slot>
<template #append v-if="slots.append">
<slot name="append"></slot>
</template>
<template #empty>
<slot name="empty"></slot>
<veEmpty description="暂无数据" v-if="!slots.empty"></veEmpty>
</template>
</el-table>
</template>
<script>
import { ElTable } from "element-plus";
import { ref, useAttrs, useSlots } from "vue";
export default {
name: "VeCommonTable",
// extends: ElTable,
directives: {},
inheritAttrs: false,
props: {},
setup(props, context) {
const attrs = useAttrs();
const slots = useSlots();
const defaultAttrs = {
data: [],
fit: true,
stripe: false,
border: false,
showHeader: true,
showSummary: false,
highlightCurrentRow: true,
defaultExpandAll: false,
selectOnIndeterminate: true,
indent: 16, //展示树形数据时,树节点的缩进
lazy: false, //是否懒加载子节点数据
style: {}, //表格样式
className: "ve-common-table", //表格自定义类名
tableLayout: "fixed", //设置表格单元、行和列的布局方式
scrollbarAlwaysOn: false, //总是显示滚动条
flexible: false, //确保主轴的最小尺寸
headerRowClassName: "ve-common-table-header",
};
const getBind = () => {
return Object.assign(defaultAttrs, attrs);
};
const rowStyle = ({ row }, index) => {
if (row?.no?.indexOf(".") > -1) {
return {
backgroundColor: "#FBFBFE",
};
}
};
const tableRef = ref(null);
return {
tableRef,
getBind,
slots,
rowStyle,
};
},
};
</script>
<style lang="scss" scoped>
.ve-common-table {
color: #5c5f66;
:deep(.ve-common-table-header) {
th {
background: #f4f7f9;
color: #3c3e41;
font-weight: normal;
}
}
:deep(.el-table__body-wrapper)
> .el-scrollbar
> .el-scrollbar__wrap--hidden-default
> .el-scrollbar__view {
height: 100%;
}
.elplus-table__body tr.elplus-table__row--striped td.elplus-table__cell {
background: rgb(251, 251, 254);
}
}
</style>
/**
* 全局注册组件
* 使用方法:
* 1、main.js文件引入
* import veBaseComponents from "@/components/veBaseComponents";
* 2、在app实例上注册使用
* const app= createApp(App);
* app.use(veBaseComponents);
*/
export default {
install: (app) => {
const files = require.context(
"@/components/veBaseComponents",
false,
/\.vue$/
);
files.keys().forEach((key) => {
// 获取组件配置
const componentConfig = files(key);
// 全局注册组件
app.component(
componentConfig.default.name,
componentConfig.default
);
});
},
};
搜索组件
<template>
<!-- <el-form :model="form" :label-width="80" :inline="true" ref="formRef" class="searchForm"> -->
<el-form
:model="form"
:inline="true"
ref="formRef"
class="searchForm"
@keydown.enter="handleEnter"
>
<el-row :gutter="20">
<el-col
style="height: 35px"
v-for="item in config"
:md="8"
:lg="4"
:key="item.label"
>
<el-form-item :label="item.label" :prop="item.prop" style="width: 100%">
<el-select
v-if="item.type === 'select'"
v-model="form[item.prop]"
style="width: 100%"
clearable
:placeholder="item.placeholder || '请选择内容'"
>
<el-option
v-for="(opt, index) in item.options"
:label="opt.label"
:value="opt.value"
:key="index"
/>
</el-select>
<el-input
v-else-if="item.type === 'input'"
:disabled="item.disabled"
:placeholder="item.placeholder || '请输入内容'"
v-model="form[item.prop]"
clearable
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col style="height: 35px" :md="8" :lg="4">
<el-form-item>
<el-space>
<el-button type="primary" plain @click="handleConfirm"
>查询</el-button
>
<el-button type="info" plain @click="handleReset">重置</el-button>
</el-space>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup>
import { reactive, ref } from "vue";
const { config } = defineProps({
config: {
type: Array,
default: () => {
return [];
},
},
});
const form = reactive({});
const emits = defineEmits(["search"]);
const handleConfirm = () => emits("search", { ...form });
const formRef = ref();
const handleReset = () => {
formRef.value.resetFields();
emits("search", { ...form });
};
const handleEnter = (e) => {
e.preventDefault();
emits("search", { ...form });
};
</script>
<style lang="scss" scoped>
.searchForm {
height: 64px;
background-color: #fbfbfe;
.el-row {
padding: 15px 20px;
}
:deep(.el-select) {
.el-input {
width: 100% !important;
}
}
}
</style>