423 lines
12 KiB
Vue
Raw Normal View History

2025-08-22 19:48:04 +08:00
<template>
<div class="custom-table-wrapper">
<!-- 表格主体 -->
2025-08-28 17:47:20 +08:00
<el-table ref="tableRef" :data="tableData" v-loading="loading" :stripe="config.stripe || false"
2025-08-22 19:48:04 +08:00
:border="config.border" :fit="config.fit !== true" :show-header="config.showHeader !== false"
:highlight-current-row="config.highlightCurrentRow || false" :row-class-name="config.rowClassName"
:header-cell-style="config.headerCellStyle" :cell-style="config.cellStyle" :max-height="config.maxHeight"
2025-10-16 11:20:50 +08:00
:show-summary="config.summary" :summary-method="getSummaries" :sum-text="config.sumText" :height="config.height"
:size="config.size || 'default'" :empty-text="config.emptyText || '暂无数据'"
2025-08-22 19:48:04 +08:00
@selection-change="handleSelectionChange" @current-change="handleCurrentChange" @row-click="handleRowClick"
2025-10-10 17:35:28 +08:00
:header-cell-class-name="enableColumnDrag ? 'el-table-header-cell' : ''" @row-dblclick="handleRowDblclick"
@sort-change="handleSortChange" v-bind="$attrs">
2025-08-22 19:48:04 +08:00
<!-- 动态列 -->
2025-10-10 17:35:28 +08:00
<template v-for="item in columns" :key="item.prop">
2025-09-29 17:30:02 +08:00
<!-- 多选列 -->
<el-table-column v-if="item.type == 'selection' && item.visible" type="selection" :width="item.width || 55"
:fixed="item.fixed" :align="item.align || 'left'" />
<!-- 序号列 -->
<el-table-column v-if="item.type == 'index' && item.visible" type="index" class-name="el-table-column--index"
:label="item.label || '序号'" :width="item.width || 60" :fixed="item.fixed" :align="item.align || 'left'"
:index="getIndex" />
2025-09-01 17:48:41 +08:00
<!-- 自定义插槽列 行数据插槽(有row属性) -->
2025-09-29 17:30:02 +08:00
<el-table-column v-if="!item.type && item.slot && item.visible" :prop="item.prop" :label="item.label"
:width="item.width" :min-width="item.minWidth" :fixed="item.fixed" :align="item.align || 'left'"
:sortable="item.sortable" :show-overflow-tooltip="item.showOverflowTooltip !== false">
2025-08-22 19:48:04 +08:00
<template #default="scope">
2025-10-21 09:55:56 +08:00
<slot :name="item.slot" :row="scope.row" :$index="scope.$index" />
2025-08-22 19:48:04 +08:00
</template>
</el-table-column>
<!-- 普通列 -->
2025-09-29 17:30:02 +08:00
<el-table-column v-if="!item.type && !item.slot && item.visible" :prop="item.prop" :label="item.label"
:width="item.width" :min-width="item.minWidth" :fixed="item.fixed" :align="item.align || 'left'"
:sortable="item.sortable" :show-overflow-tooltip="item.showOverflowTooltip !== false"
:formatter="item.formatter">
2025-09-01 17:48:41 +08:00
<!-- 表头插槽 (无row属性)-->
2025-08-26 17:31:31 +08:00
<template v-if="item.headerSlot" #header="scope">
<slot :name="item.headerSlot" :column="scope.column" :$index="scope.$index" />
2025-08-22 19:48:04 +08:00
</template>
</el-table-column>
</template>
<!-- 操作列 -->
<el-table-column v-if="$slots.action" :label="config.actionLabel || '操作'" :width="config.actionWidth"
:min-width="config.actionMinWidth || 120" :fixed="config.actionFixed || 'right'"
:align="config.actionAlign || 'center'">
<template #default="scope">
<slot name="action" :row="scope.row" :column="scope.column" :$index="scope.$index" />
</template>
</el-table-column>
<!-- 空数据插槽 -->
<template v-if="$slots.empty" #empty>
<slot name="empty" />
</template>
</el-table>
<!-- 分页组件 -->
<div v-if="pagination.show" class="pagination-wrapper">
<el-pagination v-model:current-page="currentPage" v-model:page-size="currentPageSize" :total="pagination.total"
:page-sizes="pagination.pageSizes || [10, 20, 50, 100]"
:layout="pagination.layout || 'total, sizes, prev, pager, next, jumper'"
:background="pagination.background !== false" :small="pagination.small || false"
:disabled="pagination.disabled || false" @size-change="handleSizeChange"
@current-change="handleCurrentPageChange" />
</div>
</div>
</template>
<script setup lang="ts">
2025-09-29 17:30:02 +08:00
import { computed, ref, watch, PropType, onMounted } from "vue";
2025-08-22 19:48:04 +08:00
import { TableColumnCtx } from 'element-plus';
2025-09-29 17:30:02 +08:00
// @ts-ignore
import Sortable from 'sortablejs'; // 引入sortablejs
2025-08-22 19:48:04 +08:00
interface TableColumn {
prop?: string;
2025-09-29 17:30:02 +08:00
type?: string; // 'selection' | 'index' | undefined
2025-08-26 17:31:31 +08:00
key?: string | number;
2025-08-22 19:48:04 +08:00
label?: string;
width?: string | number;
minWidth?: string | number;
fixed?: boolean | string;
align?: string;
sortable?: boolean;
showOverflowTooltip?: boolean;
formatter?: Function;
slot?: string;
headerSlot?: string;
visible?: boolean; // 是否显示列
}
2025-09-01 17:48:41 +08:00
// 在子组件中定义插槽类型
defineSlots<{
// 具名插槽和动态插槽(不含 default)
[slotName: string]: (props: { row?: any; column?: any; $index?: number }) => any;
}>();
2025-08-22 19:48:04 +08:00
// Props 定义
const props = defineProps({
// 表格数据
data: {
type: Array,
default: () => [],
},
// 表格列配置
columns: {
type: Array as PropType<TableColumn[]>,
default: () => [],
},
// 表格配置
config: {
type: Object,
default: () => ({}),
},
// 加载状态
loading: {
type: Boolean,
default: false,
},
// 分页配置
pagination: {
type: Object,
default: () => ({
show: false,
total: 0,
pageSize: 10,
currentPage: 1,
}),
},
2025-09-29 17:30:02 +08:00
// 拖拽列
enableColumnDrag: {
type: Boolean,
default: false
}
2025-08-22 19:48:04 +08:00
});
// Emits 定义
const emit = defineEmits([
"selection-change",
"current-change",
"row-click",
"row-dblclick",
"sort-change",
"page-change",
"size-change",
2025-09-29 17:30:02 +08:00
"column-drag-end",
2025-08-22 19:48:04 +08:00
]);
// 响应式数据
const tableRef = ref();
const currentPage = ref(props.pagination.currentPage || 1); // 第几页
const currentPageSize = ref(props.pagination.pageSize || 10); // 一页显示多少条
// 计算属性
const tableData = computed(() => {
if (props.pagination.show && props.pagination.clientPaging) {
// 客户端分页
const start = (currentPage.value - 1) * currentPageSize.value;
const end = start + currentPageSize.value;
return props.data.slice(start, end);
}
return props.data;
});
// 序号计算
const getIndex = (index: number) => {
if (props.pagination.show && !props.pagination.clientPaging) {
return (currentPage.value - 1) * currentPageSize.value + index + 1;
}
return index + 1;
};
2025-09-29 17:30:02 +08:00
// const headerCellClassName = (column: TableColumnCtx<any>) => {
// if (!column.column.property) {
// return ''; // 不添加类名
// } else {
// return 'el-table-header-cell'; // 添加自定义类名
// }
// };
2025-10-16 11:20:50 +08:00
// 计算列合计
// 自定义汇总方法 - 只计算指定列
const getSummaries = ({ columns, data }: { columns: any, data: any }) => {
const summaries = columns.map(() => '');
if (props.config.summaryField.length > 0) {
props.config.summaryField.forEach((v: any) => {
const index = columns.find((column: any) => column.property === v)?.no;
const summaryData = data.map((item: any) => item[v]);
const summary = summaryData.reduce((acc: any, cur: any) => Number(acc) + Number(cur), 0);
summaries[index] = summary;
});
summaries[0] = props.config.sumText;
}
return summaries;
};
2025-08-22 19:48:04 +08:00
// 事件处理
const handleSelectionChange = (selection: any[]) => {
emit("selection-change", selection);
};
const handleCurrentChange = (currentRow: any, oldCurrentRow: any) => {
emit("current-change", currentRow, oldCurrentRow);
};
const handleRowClick = (row: any, column: TableColumnCtx<any>, event: Event) => {
emit("row-click", row, column, event);
};
const handleRowDblclick = (row: any, column: TableColumnCtx<any>, event: Event) => {
emit("row-dblclick", row, column, event);
};
const handleSortChange = (sortInfo: Object) => {
emit("sort-change", sortInfo);
};
const handleSizeChange = (size: number) => {
currentPageSize.value = size;
currentPage.value = 1;
emit("size-change", {
pageSize: size,
currentPage: 1,
});
};
const handleCurrentPageChange = (page: number) => {
currentPage.value = page;
emit("page-change", {
currentPage: page,
pageSize: currentPageSize.value,
});
};
2025-09-29 17:30:02 +08:00
// 初始化列拖拽功能
onMounted(() => {
if (props.enableColumnDrag && tableRef.value) {
initColumnDrag();
}
});
2025-08-22 19:48:04 +08:00
// 监听分页配置变化
watch(
() => props.pagination.currentPage,
(newVal) => {
if (newVal) {
currentPage.value = newVal;
}
},
);
2025-09-29 17:30:02 +08:00
2025-08-22 19:48:04 +08:00
watch(
() => props.pagination.pageSize,
(newVal) => {
if (newVal) {
currentPageSize.value = newVal;
}
},
);
2025-09-29 17:30:02 +08:00
// 初始化列拖拽
const initColumnDrag = () => {
// 获取表头单元格
const tableHeader = tableRef.value.$el.querySelector('.el-table__header-wrapper thead tr');
if (!tableHeader) return;
// 初始化拖拽
const sortable = new Sortable(tableHeader, {
animation: 150, // 动画时间
handle: '.el-table-header-cell', // 拖拽手柄
ghostClass: 'sortable-ghost', // 拖拽时的占位符样式
// filter: '.el-table-column--selection, .el-table-column--index', // 排除选择列和序号列
onEnd: (evt: any) => {
2025-10-10 17:35:28 +08:00
// console.log('evt==>', evt);
2025-09-29 17:30:02 +08:00
// 排除固定列和选择列、序号列
const visibleColumns = props.columns.filter(col => col.visible !== false);
// 过滤掉固定列
const draggableColumns = visibleColumns.filter(col => !col.fixed);
// 如果发生了位置变化
if (evt.oldIndex !== evt.newIndex && draggableColumns.length) {
// 复制原数组并调整顺序
const newColumns = [...draggableColumns];
const [movedItem] = newColumns.splice(evt.oldIndex, 1);
newColumns.splice(evt.newIndex, 0, movedItem);
// 更新原始columns数组中对应列的位置
const updatedColumns = [...props.columns];
// 先移除所有可拖拽列
const nonDraggableColumns = updatedColumns.filter(col => col.fixed);
// 合并固定列和新顺序的可拖拽列
const resultColumns = nonDraggableColumns.concat(newColumns);
// 触发事件,通知父组件列顺序已改变
emit('column-drag-end', {
oldIndex: evt.oldIndex,
newIndex: evt.newIndex,
columns: resultColumns
});
}
}
});
};
2025-08-22 19:48:04 +08:00
// 暴露方法
const clearSelection = () => {
tableRef.value?.clearSelection();
};
const toggleRowSelection = (row: Object, selected: boolean) => {
tableRef.value?.toggleRowSelection(row, selected);
};
const toggleAllSelection = () => {
tableRef.value?.toggleAllSelection();
};
const setCurrentRow = (row: Object) => {
tableRef.value?.setCurrentRow(row);
};
2025-11-03 18:07:51 +08:00
const setScrollTo = (top: number) => {
tableRef.value?.scrollTo(top);
2025-11-03 12:12:47 +08:00
};
2025-08-22 19:48:04 +08:00
const clearSort = () => {
tableRef.value?.clearSort();
};
const doLayout = () => {
tableRef.value?.doLayout();
};
const sort = (prop: string, order: any) => {
tableRef.value?.sort(prop, order);
};
// 导出方法
defineExpose({
clearSelection,
toggleRowSelection,
toggleAllSelection,
setCurrentRow,
2025-11-03 18:07:51 +08:00
setScrollTo,
2025-08-22 19:48:04 +08:00
clearSort,
doLayout,
sort,
tableRef,
2025-09-29 17:30:02 +08:00
initColumnDrag
2025-08-22 19:48:04 +08:00
});
</script>
<style scoped lang="scss">
.custom-table-wrapper {
width: 100%;
}
2025-09-29 17:30:02 +08:00
2025-10-21 09:55:56 +08:00
// 只在拖拽手柄上显示移动光标
:deep(.el-table-header-cell .cell) {
2025-09-29 17:30:02 +08:00
cursor: move;
2025-10-21 09:55:56 +08:00
/* 恢复默认光标 */
}
// 拖拽手柄样式(使用排序图标作为拖拽手柄)
:deep(.el-table-header-cell .sort-caret) {
// cursor: move;
/* 只在排序图标处显示移动光标 */
// margin-left: 1px;
}
// 保留列宽调整的光标样式
:deep(.el-table__header-wrapper .el-table__header tr th .el-table__header-drag-icon) {
cursor: col-resize !important;
2025-09-29 17:30:02 +08:00
}
2025-10-21 09:55:56 +08:00
2025-08-22 19:48:04 +08:00
.pagination-wrapper {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 12px 0;
margin-top: 16px;
margin-right: 20px;
}
2025-09-26 17:26:06 +08:00
// /* 修改选中行背景色 */
// ::v-deep .el-table__body tr.current-row>td {
// background-color: #a4beef !important;
// }
// /* 可选:修改悬停颜色 */
// ::v-deep .el-table__body tr.hover-row>td {
// background-color: #a4beef !important;
// }
2025-08-22 19:48:04 +08:00
/* 响应式设计 */
@media (max-width: 768px) {
.pagination-wrapper {
justify-content: center;
}
.pagination-wrapper :deep(.el-pagination) {
flex-wrap: wrap;
}
}
</style>