2025-11-14 18:13:45 +08:00

431 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="custom-table-wrapper">
<!-- 表格主体 -->
<el-table ref="tableRef" :data="tableData" v-loading="loading" :stripe="config.stripe || false"
: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"
:show-summary="config.summary" :summary-method="getSummaries" :sum-text="config.sumText" :height="config.height"
:size="config.size || 'default'" :empty-text="config.emptyText || '暂无数据'"
@selection-change="handleSelectionChange" @current-change="handleCurrentChange" @row-click="handleRowClick"
:header-cell-class-name="enableColumnDrag ? 'el-table-header-cell' : ''" @row-dblclick="handleRowDblclick"
@sort-change="handleSortChange" v-bind="$attrs">
<!-- 动态列 -->
<template v-for="item in columns" :key="item.prop">
<!-- 多选列 -->
<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" />
<!-- 自定义插槽列 行数据插槽(有row属性) -->
<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">
<template #default="scope">
<slot :name="item.slot" :row="scope.row" :$index="scope.$index" />
</template>
</el-table-column>
<!-- 普通列 -->
<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">
<!-- 表头插槽 (无row属性)-->
<template v-if="item.headerSlot" #header="scope">
<slot :name="item.headerSlot" :column="scope.column" :$index="scope.$index" />
</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"
:size="aotuSize" :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">
import { computed, ref, watch, PropType, onMounted } from "vue";
import { TableColumnCtx } from 'element-plus';
import { classCom } from "@/utils/classCom";
const aotuSize = classCom.useAutoSize();
// @ts-ignore
import Sortable from 'sortablejs'; // 引入sortablejs
interface TableColumn {
prop?: string;
type?: string; // 'selection' | 'index' | undefined
key?: string | number;
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; // 是否显示列
}
// 在子组件中定义插槽类型
defineSlots<{
// 具名插槽和动态插槽(不含 default)
[slotName: string]: (props: { row?: any; column?: any; $index?: number }) => any;
}>();
// 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,
}),
},
// 拖拽列
enableColumnDrag: {
type: Boolean,
default: false
}
});
// Emits 定义
const emit = defineEmits([
"selection-change",
"current-change",
"row-click",
"row-dblclick",
"sort-change",
"page-change",
"size-change",
"column-drag-end",
]);
// 响应式数据
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;
};
// const headerCellClassName = (column: TableColumnCtx<any>) => {
// if (!column.column.property) {
// return ''; // 不添加类名
// } else {
// return 'el-table-header-cell'; // 添加自定义类名
// }
// };
// 计算列合计
// 自定义汇总方法 - 只计算指定列
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;
};
// 事件处理
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,
});
};
// 初始化列拖拽功能
onMounted(() => {
if (props.enableColumnDrag && tableRef.value) {
initColumnDrag();
}
});
// 监听分页配置变化
watch(
() => props.pagination.currentPage,
(newVal) => {
if (newVal) {
currentPage.value = newVal;
}
},
);
watch(
() => props.pagination.pageSize,
(newVal) => {
if (newVal) {
currentPageSize.value = newVal;
}
},
);
// 初始化列拖拽
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) => {
// console.log('evt==>', evt);
// 排除固定列和选择列、序号列
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
});
}
}
});
};
// 暴露方法
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);
};
const setScrollTo = (top: number) => {
tableRef.value?.scrollTo(top);
};
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,
setScrollTo,
clearSort,
doLayout,
sort,
tableRef,
initColumnDrag
});
</script>
<style scoped lang="scss">
.custom-table-wrapper {
width: 100%;
}
// 只在拖拽手柄上显示移动光标
:deep(.el-table-header-cell .cell) {
cursor: move;
/* 恢复默认光标 */
}
// 拖拽手柄样式(使用排序图标作为拖拽手柄)
: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;
}
:deep(.el-table__footer-wrapper tfoot td.el-table__cell) {
color: #1f6dd3 !important;
font-weight: 700;
/* 可替换为任意颜色值 */
}
.pagination-wrapper {
display: flex;
align-items: center;
justify-content: flex-end;
padding: .3125rem 0;
margin-right: 20px;
}
// /* 修改选中行背景色 */
// ::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;
// }
/* 响应式设计 */
@media (max-width: 768px) {
.pagination-wrapper {
justify-content: center;
}
.pagination-wrapper :deep(.el-pagination) {
flex-wrap: wrap;
}
}
</style>