2025-08-25 11:46:03 +08:00
|
|
|
|
<template>
|
2025-08-27 16:47:33 +08:00
|
|
|
|
<div class="my-table-container">
|
|
|
|
|
|
<!-- 扩展层:可后续统一添加(如加载动画、导出按钮等) -->
|
|
|
|
|
|
<div v-if="loading" class="table-loading">
|
|
|
|
|
|
<el-loading-spinner></el-loading-spinner>
|
|
|
|
|
|
<p class="loading-text">{{ loadingText || "加载中..." }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-11-29 14:10:04 +08:00
|
|
|
|
<div class="el-table-container" ref="elTableWrapper" @mouseleave="handleTableLeave">
|
|
|
|
|
|
<!-- 原生 Table 核心:透传 props/事件/插槽 -->
|
|
|
|
|
|
<el-table :ref="tableRef" v-bind="$attrs" v-on="$listeners" :class="['my-table', customClass]"
|
2026-03-20 13:34:56 +08:00
|
|
|
|
@cell-mouse-enter="handleCellEnter" @row-mouseleave="handleRowLeave" :height="height">
|
2025-11-29 14:10:04 +08:00
|
|
|
|
<!-- 2. 透传原生内置插槽(如 empty 空数据插槽、append 底部插槽等) -->
|
|
|
|
|
|
<template v-slot:empty="scope">
|
|
|
|
|
|
<slot name="empty" v-bind="scope"></slot>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-slot:append="scope">
|
|
|
|
|
|
<slot name="append" v-bind="scope"></slot>
|
|
|
|
|
|
</template>
|
2025-08-28 15:06:03 +08:00
|
|
|
|
|
2025-11-29 14:10:04 +08:00
|
|
|
|
<!-- 3. 透传“自定义列”插槽(直接接收<el-table-column> 嵌套的情况) -->
|
|
|
|
|
|
<slot v-bind:tableRef="tableRef"></slot>
|
2025-08-28 15:06:03 +08:00
|
|
|
|
|
2025-11-29 14:10:04 +08:00
|
|
|
|
<el-table-column v-if="selectionColumn" type="selection" width="55" align="center"></el-table-column>
|
2025-08-27 16:47:33 +08:00
|
|
|
|
|
2025-11-29 14:10:04 +08:00
|
|
|
|
<el-table-column v-if="indexColumn" type="index" width="55" align="center"></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<!-- 浮层组件 -->
|
|
|
|
|
|
<KLPTableFloatLayer v-if="floatLayer" :columns="floatLayerColumns" :data="hoveredRow" :tooltipVisible="tooltipVisible"
|
|
|
|
|
|
:tooltipStyle="tooltipStyle" />
|
|
|
|
|
|
</div>
|
2025-08-27 16:47:33 +08:00
|
|
|
|
<!-- 扩展层:可后续统一添加分页(如与 MyPagination 组件联动) -->
|
|
|
|
|
|
<slot name="pagination"></slot>
|
2025-08-25 11:46:03 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2025-11-29 14:10:04 +08:00
|
|
|
|
import ColumnRender from './ColumnRender.vue';
|
2025-08-28 15:06:03 +08:00
|
|
|
|
import Eclipse from './renderer/eclipse.vue';
|
2025-11-29 14:10:04 +08:00
|
|
|
|
import KLPTableFloatLayer from './FloatLayer/index.vue';
|
2025-08-28 15:06:03 +08:00
|
|
|
|
|
2025-08-25 11:46:03 +08:00
|
|
|
|
export default {
|
2025-08-28 15:06:03 +08:00
|
|
|
|
name: "KLPTable", // 组件名,便于调试和文档生成
|
|
|
|
|
|
components: {
|
|
|
|
|
|
ColumnRender,
|
2025-11-29 14:10:04 +08:00
|
|
|
|
Eclipse,
|
|
|
|
|
|
KLPTableFloatLayer
|
2025-08-28 15:06:03 +08:00
|
|
|
|
},
|
2025-08-25 11:46:03 +08:00
|
|
|
|
props: {
|
2025-08-27 16:47:33 +08:00
|
|
|
|
// 1. 扩展 props:新增原生 Table 没有的属性(如加载状态)
|
2025-08-25 11:46:03 +08:00
|
|
|
|
loading: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
2025-08-27 16:47:33 +08:00
|
|
|
|
loadingText: {
|
2025-08-25 11:46:03 +08:00
|
|
|
|
type: String,
|
2025-08-27 16:47:33 +08:00
|
|
|
|
default: "加载中..."
|
2025-08-25 11:46:03 +08:00
|
|
|
|
},
|
2025-08-27 16:47:33 +08:00
|
|
|
|
// 2. 兼容原生 class 用法(原生 el-table 支持 class 属性,此处显式接收避免 $attrs 冲突)
|
|
|
|
|
|
customClass: {
|
2025-08-25 11:46:03 +08:00
|
|
|
|
type: String,
|
2025-08-27 16:47:33 +08:00
|
|
|
|
default: ""
|
2025-08-28 15:06:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
selectionColumn: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
|
|
|
|
|
indexColumn: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
2025-11-29 14:10:04 +08:00
|
|
|
|
},
|
|
|
|
|
|
// floatLayer:是否显示浮层
|
|
|
|
|
|
floatLayer: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
|
|
|
|
|
floatLayerConfig: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: () => ({
|
|
|
|
|
|
columns: [],
|
|
|
|
|
|
title: '详细信息'
|
|
|
|
|
|
})
|
2026-03-20 13:34:56 +08:00
|
|
|
|
},
|
|
|
|
|
|
height: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
2025-08-25 11:46:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-08-27 16:47:33 +08:00
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
2025-11-29 14:10:04 +08:00
|
|
|
|
tableRef: "myTableRef", // 表格 ref,便于后续通过 ref 调用原生方法
|
|
|
|
|
|
// 浮层相关
|
|
|
|
|
|
tooltipVisible: false,
|
|
|
|
|
|
tooltipStyle: {
|
|
|
|
|
|
top: '0px',
|
|
|
|
|
|
left: '0px'
|
|
|
|
|
|
},
|
|
|
|
|
|
hoveredRow: null,
|
|
|
|
|
|
columns: []
|
2025-08-27 16:47:33 +08:00
|
|
|
|
};
|
|
|
|
|
|
},
|
2025-11-29 14:10:04 +08:00
|
|
|
|
computed: {
|
|
|
|
|
|
floatLayerColumns() {
|
|
|
|
|
|
console.log(this.floatLayerConfig?.columns?.length > 1)
|
|
|
|
|
|
if (this.floatLayerConfig?.columns?.length > 1) {
|
|
|
|
|
|
return this.floatLayerConfig.columns
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.columns;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-08-25 11:46:03 +08:00
|
|
|
|
methods: {
|
2025-08-28 15:06:03 +08:00
|
|
|
|
// 核心方法:净化scope,移除可能导致循环引用的属性
|
|
|
|
|
|
sanitizeScope(scope) {
|
|
|
|
|
|
if (!scope) return {};
|
|
|
|
|
|
// 只保留安全的属性(根据实际需求调整)
|
|
|
|
|
|
const { row, column, $index, store } = scope;
|
|
|
|
|
|
// 深拷贝避免引用传递,同时过滤循环引用
|
|
|
|
|
|
return this.deepClone({ row, column, $index, store });
|
|
|
|
|
|
},
|
|
|
|
|
|
// 安全的深拷贝方法(避免JSON.parse/stringify无法处理的类型)
|
|
|
|
|
|
deepClone(obj) {
|
|
|
|
|
|
if (obj === null || typeof obj !== "object") return obj;
|
|
|
|
|
|
if (obj instanceof Date) return new Date(obj);
|
|
|
|
|
|
if (obj instanceof RegExp) return new RegExp(obj);
|
2025-11-29 14:10:04 +08:00
|
|
|
|
|
2025-08-28 15:06:03 +08:00
|
|
|
|
// 避免处理Vue实例(通常带有循环引用)
|
|
|
|
|
|
if (obj._isVue) return { _isVue: true };
|
|
|
|
|
|
|
|
|
|
|
|
const clone = Array.isArray(obj) ? [] : {};
|
|
|
|
|
|
for (const key in obj) {
|
|
|
|
|
|
if (obj.hasOwnProperty(key)) {
|
|
|
|
|
|
clone[key] = this.deepClone(obj[key]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return clone;
|
|
|
|
|
|
},
|
2025-08-27 16:47:33 +08:00
|
|
|
|
// 3. 透传原生 Table 实例方法(如 clearSelection、doLayout 等)
|
|
|
|
|
|
// 业务层可通过 this.$refs.myTable.xxx() 调用原生方法
|
2025-08-25 11:46:03 +08:00
|
|
|
|
getTableInstance() {
|
2025-08-27 16:47:33 +08:00
|
|
|
|
return this.$refs[this.tableRef];
|
2025-08-25 11:46:03 +08:00
|
|
|
|
},
|
2025-08-27 16:47:33 +08:00
|
|
|
|
// 示例:透传原生 clearSelection 方法
|
2025-08-25 11:46:03 +08:00
|
|
|
|
clearSelection() {
|
2025-08-27 16:47:33 +08:00
|
|
|
|
const table = this.getTableInstance();
|
|
|
|
|
|
if (table && table.clearSelection) {
|
|
|
|
|
|
table.clearSelection();
|
2025-08-25 11:46:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-08-27 16:47:33 +08:00
|
|
|
|
// 可根据需要扩展更多原生方法(如 toggleRowSelection、sort 等)
|
2025-08-25 11:46:03 +08:00
|
|
|
|
toggleRowSelection(row, selected) {
|
2025-08-27 16:47:33 +08:00
|
|
|
|
const table = this.getTableInstance();
|
|
|
|
|
|
if (table && table.toggleRowSelection) {
|
|
|
|
|
|
table.toggleRowSelection(row, selected);
|
2025-08-25 11:46:03 +08:00
|
|
|
|
}
|
2025-11-29 14:10:04 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 浮层相关
|
|
|
|
|
|
handleCellEnter(row, column, cell, event) {
|
|
|
|
|
|
if (!row || !event) return
|
|
|
|
|
|
this.hoveredRow = row
|
|
|
|
|
|
this.tooltipVisible = true
|
|
|
|
|
|
this.updateTooltipPosition(event)
|
|
|
|
|
|
},
|
|
|
|
|
|
handleRowLeave() {
|
|
|
|
|
|
this.tooltipVisible = false
|
|
|
|
|
|
this.hoveredRow = null
|
|
|
|
|
|
},
|
|
|
|
|
|
handleTableLeave() {
|
|
|
|
|
|
this.tooltipVisible = false
|
|
|
|
|
|
this.hoveredRow = null
|
|
|
|
|
|
},
|
|
|
|
|
|
updateTooltipPosition(event) {
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
const wrapper = this.$refs.elTableWrapper
|
|
|
|
|
|
if (!wrapper) return
|
|
|
|
|
|
|
|
|
|
|
|
const wrapperRect = wrapper.getBoundingClientRect()
|
|
|
|
|
|
let left = event.clientX - wrapperRect.left + 16
|
|
|
|
|
|
let top = event.clientY - wrapperRect.top + 12
|
|
|
|
|
|
|
|
|
|
|
|
this.tooltipStyle = {
|
|
|
|
|
|
top: `${top}px`,
|
|
|
|
|
|
left: `${left}px`
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
2025-08-25 11:46:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
mounted() {
|
2025-08-27 16:47:33 +08:00
|
|
|
|
// 扩展点:后续可统一添加初始化逻辑(如权限控制、默认排序等)
|
2025-11-29 14:10:04 +08:00
|
|
|
|
// console.log("MyTable 初始化完成,原生实例:", this.getTableInstance());
|
|
|
|
|
|
// 几个特殊的列,需要特殊处理 index, selection, action
|
|
|
|
|
|
const columns = this.$slots.default.filter(item => item.tag?.includes('ElTableColumn') && item.componentInstance && item.componentInstance.columnConfig && item.componentOptions && item.componentOptions.propsData).map(item => ({
|
|
|
|
|
|
...item.componentInstance.columnConfig,
|
|
|
|
|
|
...item.componentOptions.propsData
|
|
|
|
|
|
}))
|
|
|
|
|
|
this.columns = columns
|
2025-08-25 11:46:03 +08:00
|
|
|
|
}
|
2025-08-27 16:47:33 +08:00
|
|
|
|
};
|
2025-08-25 11:46:03 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2025-08-27 16:47:33 +08:00
|
|
|
|
.my-table-container {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 扩展样式:加载状态遮罩(后续可统一调整) */
|
|
|
|
|
|
.table-loading {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.8);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading-text {
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 原生 Table 样式兼容:避免封装层影响原生样式 */
|
|
|
|
|
|
.my-table {
|
2025-08-25 11:46:03 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
2025-08-27 16:47:33 +08:00
|
|
|
|
</style>
|