feat(KLPTable): 添加表格浮层功能以显示详细信息

实现表格行悬浮显示详细信息的浮层功能,支持自定义浮层列配置
重构KLPTable组件结构,将浮层逻辑独立为子组件
在用户管理和钢卷管理页面应用新浮层功能
This commit is contained in:
砂糖
2025-11-29 14:10:04 +08:00
parent 4be3a5c1ab
commit 5501987fe5
4 changed files with 216 additions and 89 deletions

View File

@@ -0,0 +1,89 @@
<template>
<transition name="el-fade-in-linear">
<div v-if="tooltipVisible && data" class="row-tooltip" :style="tooltipStyle" ref="rowTooltip">
<slot>
<div class="tooltip-list">
<div class="tooltip-item" v-for="field in visibleColumns" :key="field.prop">
<span class="label">{{ field.label }}</span>
<span class="value">{{ formatTooltipValue(data, field) }}</span>
</div>
</div>
</slot>
</div>
</transition>
</template>
<script>
export default {
name: "KLPTableFloatLayer",
props: {
columns: {
type: Array,
default: () => []
},
data: {
type: Object,
default: () => ({})
},
tooltipVisible: {
type: Boolean,
default: false
},
tooltipStyle: {
type: Object,
default: () => ({})
}
},
computed: {
// 一个列要同时有用 label 和 prop 才显示在浮层中, 并且prop有值时才显示
visibleColumns() {
return this.columns.filter(field => field.label && field.prop && this.data[field.prop])
}
},
methods: {
formatTooltipValue(row, field) {
// 辅助函数:递归/迭代获取多层嵌套的属性值
const getNestedValue = (obj, path) => {
if (!obj || !path) return undefined;
// 将路径拆分为数组(支持 'dept.deptName' 或 ['dept', 'deptName'] 格式)
const pathArr = Array.isArray(path) ? path : path.split('.');
// 逐层取值遇到null/undefined直接返回
return pathArr.reduce((current, key) => {
return current === null || current === undefined ? undefined : current[key];
}, obj);
};
// 获取多层数据的值(核心修改点)
let value = getNestedValue(row, field.prop);
// 空值兜底处理(保留原有逻辑)
if (value === null || value === undefined || value === '') {
return '-';
}
// 字典匹配逻辑(保留原有逻辑)
if (field.dict && this.dict && this.dict.type && this.dict.type[field.dict]) {
const match = this.dict.type[field.dict].find(item => item.value === value);
return match ? match.label : value;
}
return value;
}
}
}
</script>
<style scoped>
.row-tooltip {
position: absolute;
background: #ffffff;
border: 1px solid #dcdcdc;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 12px 14px;
pointer-events: none;
z-index: 5;
max-height: 70%;
overflow: auto;
}
</style>

View File

@@ -6,88 +6,45 @@
<p class="loading-text">{{ loadingText || "加载中..." }}</p>
</div>
<!-- 原生 Table 核心透传 props/事件/插槽 -->
<el-table
:ref="tableRef"
v-bind="$attrs"
v-on="$listeners"
:class="['my-table', customClass]"
>
<!-- 1. 透传列定义插槽原生 columns 中通过 slot 自定义的表头/单元格 -->
<template
v-for="(column, index) in $attrs.columns || []"
v-slot:[`header-${column.prop}`]="scope"
>
<!-- 调用业务层定义的表头插槽 -->
<slot :name="`header-${column.prop}`" v-bind="scope"></slot>
</template>
<template
v-for="(column, index) in $attrs.columns || []"
v-slot:[column.prop]="scope"
>
<!-- 调用业务层定义的单元格插槽 -->
<slot :name="column.prop" v-bind="scope"></slot>
</template>
<!-- 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>
<!-- 3. 透传自定义列插槽业务层直接用 <el-table-column> 嵌套的情况 -->
<slot v-bind:tableRef="tableRef"></slot>
<el-table-column
v-if="selectionColumn"
type="selection"
width="55"
align="center"
></el-table-column>
<el-table-column
v-if="indexColumn"
type="index"
width="55"
align="center"
></el-table-column>
<el-table-column
v-for="(column, index) in customColumns"
v-bind="column"
:width="column.width"
:prop="column.prop"
:align="column.align"
:label="column.label"
:min-width="column.minWidth"
:fixed="column.fixed"
:show-overflow-tooltip="column.showOverflowTooltip"
:sortable="column.sortable"
>
<template v-slot="scope">
<ColumnRender v-if="column.render" :column="column" :data="scope.row" :render="column.render"/>
<Eclipse v-else-if="column.eclipse" :text="scope.row[column.prop]"></Eclipse>
<span v-else>{{ scope.row[column.prop] }}</span>
<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]"
@cell-mouse-enter="handleCellEnter" @row-mouseleave="handleRowLeave">
<!-- 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>
</el-table-column>
</el-table>
<!-- 3. 透传自定义列插槽直接接收<el-table-column> 嵌套的情况 -->
<slot v-bind:tableRef="tableRef"></slot>
<el-table-column v-if="selectionColumn" type="selection" width="55" align="center"></el-table-column>
<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>
<!-- 扩展层可后续统一添加分页如与 MyPagination 组件联动 -->
<slot name="pagination"></slot>
</div>
</template>
<script>
import ColumnRender from './ColumnRender.vue';
import ColumnRender from './ColumnRender.vue';
import Eclipse from './renderer/eclipse.vue';
import KLPTableFloatLayer from './FloatLayer/index.vue';
export default {
name: "KLPTable", // 组件名,便于调试和文档生成
components: {
ColumnRender,
Eclipse
Eclipse,
KLPTableFloatLayer
},
props: {
// 1. 扩展 props新增原生 Table 没有的属性(如加载状态)
@@ -104,10 +61,6 @@ export default {
type: String,
default: ""
},
customColumns: {
type: Array,
default: () => []
},
selectionColumn: {
type: Boolean,
default: false
@@ -115,13 +68,42 @@ export default {
indexColumn: {
type: Boolean,
default: false
},
// floatLayer是否显示浮层
floatLayer: {
type: Boolean,
default: false
},
floatLayerConfig: {
type: Object,
default: () => ({
columns: [],
title: '详细信息'
})
}
},
data() {
return {
tableRef: "myTableRef" // 表格 ref便于后续通过 ref 调用原生方法
tableRef: "myTableRef", // 表格 ref便于后续通过 ref 调用原生方法
// 浮层相关
tooltipVisible: false,
tooltipStyle: {
top: '0px',
left: '0px'
},
hoveredRow: null,
columns: []
};
},
computed: {
floatLayerColumns() {
console.log(this.floatLayerConfig?.columns?.length > 1)
if (this.floatLayerConfig?.columns?.length > 1) {
return this.floatLayerConfig.columns
}
return this.columns;
}
},
methods: {
// 核心方法净化scope移除可能导致循环引用的属性
sanitizeScope(scope) {
@@ -136,7 +118,7 @@ export default {
if (obj === null || typeof obj !== "object") return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 避免处理Vue实例通常带有循环引用
if (obj._isVue) return { _isVue: true };
@@ -166,11 +148,48 @@ export default {
if (table && table.toggleRowSelection) {
table.toggleRowSelection(row, selected);
}
}
},
// 浮层相关
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`
}
})
},
},
mounted() {
// 扩展点:后续可统一添加初始化逻辑(如权限控制、默认排序等)
console.log("MyTable 初始化完成,原生实例:", this.getTableInstance());
// 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
}
};
</script>

View File

@@ -137,7 +137,7 @@
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row>
<KLPTable v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
<KLPTable v-loading="loading" :data="userList" @selection-change="handleSelectionChange" :floatLayer="true">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if="columns[0].visible" />
<el-table-column label="用户名称" align="center" key="userName" prop="userName" v-if="columns[1].visible" :show-overflow-tooltip="true" />

View File

@@ -69,15 +69,15 @@
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="materialCoilList" @selection-change="handleSelectionChange">
<KLPTable v-loading="loading" :data="materialCoilList" @selection-change="handleSelectionChange" :floatLayer="true" :floatLayerConfig="floatLayerConfig">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="入场钢卷号" align="center" prop="enterCoilNo" />
<el-table-column label="当前钢卷号" align="center" prop="currentCoilNo" />
<el-table-column label="厂家卷号" align="center" prop="supplierCoilNo" />
<!-- <el-table-column label="厂家卷号" align="center" prop="supplierCoilNo" /> -->
<el-table-column label="逻辑库位" align="center" prop="warehouseName" v-if="!hideWarehouseQuery" />
<el-table-column label="实际库区" align="center" prop="actualWarehouseName" v-if="!hideWarehouseQuery" />
<el-table-column label="物料类型" align="center" prop="materialType" />
<el-table-column label="产品类型" align="center" prop="itemName">
<!-- <el-table-column label="物料类型" align="center" prop="materialType" /> -->
<el-table-column label="产品类型" align="center">
<template slot-scope="scope">
<ProductInfo v-if="scope.row.itemType == 'product'" :product="scope.row.product">
<template slot-scope="{ product }">
@@ -107,14 +107,14 @@
</el-select>
</template>
</el-table-column>
<el-table-column label="班组" align="center" prop="team" />
<el-table-column label="毛重" align="center" prop="grossWeight" />
<el-table-column label="净重" align="center" prop="netWeight" />
<el-table-column v-if="querys.materialType === '成品'" label="质量状态" align="center" prop="qualityStatus" />
<!-- <el-table-column label="班组" align="center" prop="team" /> -->
<!-- <el-table-column label="毛重" align="center" prop="grossWeight" />
<el-table-column label="净重" align="center" prop="netWeight" /> -->
<!-- <el-table-column v-if="querys.materialType === '成品'" label="质量状态" align="center" prop="qualityStatus" />
<el-table-column v-if="querys.materialType === '成品'" label="切边要求" align="center" prop="trimmingRequirement" />
<el-table-column v-if="querys.materialType === '成品'" label="打包状态" align="center" prop="packingStatus" />
<el-table-column v-if="querys.materialType === '成品'" label="包装要求" align="center" prop="packagingRequirement" />
<el-table-column label="关联信息" align="center" prop="parentCoilNos" :show-overflow-tooltip="true">
<el-table-column v-if="querys.materialType === '成品'" label="包装要求" align="center" prop="packagingRequirement" /> -->
<el-table-column label="关联信息" align="center" :show-overflow-tooltip="true">
<template slot-scope="scope">
<span v-if="scope.row.parentCoilNos && scope.row.hasMergeSplit === 1 && scope.row.dataType === 1">
<el-tag type="warning" size="mini">来自母卷{{ scope.row.parentCoilNos }}</el-tag>
@@ -128,7 +128,7 @@
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip/>
<!-- <el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip/> -->
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="handlePreviewLabel(scope.row)">
@@ -139,7 +139,7 @@
<el-button size="mini" type="text" icon="el-icon-search" @click="handleTrace(scope.row)">追溯</el-button>
</template>
</el-table-column>
</el-table>
</KLPTable>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getList" />
@@ -367,6 +367,25 @@ export default {
visible: false,
data: {},
type: '2'
},
floatLayerConfig: {
columns: [
{ label: '入场钢卷号', prop: 'enterCoilNo' },
{ label: '当前钢卷号', prop: 'currentCoilNo' },
{ label: '厂家卷号', prop: 'supplierCoilNo' },
{ label: '逻辑库位', prop: 'warehouseName' },
{ label: '实际库位', prop: 'actualWarehouseName' },
{ label: '物料类型', prop: 'materialType' },
{ label: '班组', prop: 'team' },
{ label: '净重', prop: 'netWeight' },
{ label: '毛重', prop: 'grossWeight' },
{ label: '备注', prop: 'remark' },
{ label: '质量状态', prop: 'qualityStatus' },
{ label: '打包状态', prop: 'packingStatus' },
{ label: '切边要求', prop: 'edgeRequirement' },
{ label: '包装要求', prop: 'packagingRequirement' }
],
title: '详细信息'
}
};
},