🦄 refactor: 封装统一表格组件,便于批量扩展表格能力

This commit is contained in:
砂糖
2025-08-27 16:47:33 +08:00
parent 278b0c8258
commit 7ea0de6a67
133 changed files with 465 additions and 432 deletions

View File

@@ -42,7 +42,7 @@
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="bomItemList" @selection-change="handleSelectionChange">
<KLPTable v-loading="loading" :data="bomItemList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="属性名称" align="center" prop="attrKey" />
<el-table-column label="属性值" align="center" prop="attrValue" />
@@ -63,7 +63,7 @@
>删除</el-button>
</template>
</el-table-column>
</el-table>
</KLPTable>
<pagination
v-show="total>0"

View File

@@ -39,7 +39,7 @@
<script>
export default {
name: "klp-list",
name: "KLPList",
components: {},
props: {
/** 列表数据源(必传) */
@@ -76,11 +76,11 @@ export default {
default: false
},
/** 标题最大宽度(像素),控制文字溢出 */
titleMaxWidth: {
/** 标题最大宽度占容器的百分比0-100),控制文字溢出 */
titleMaxPercent: {
type: Number,
required: false,
default: 200
default: 80 // 默认占容器80%宽度
}
},
data() {
@@ -88,7 +88,9 @@ export default {
// 内部管理选中状态:存储当前选中项的唯一键值(单选中)
selectedKey: null,
// 文字溢出检测临时元素(避免重复创建)
overflowCheckElements: {}
overflowCheckElements: {},
// 容器宽度缓存
containerWidth: 0
};
},
methods: {
@@ -107,9 +109,10 @@ export default {
* @param {Object} item - 点击的列表项数据
*/
handleItemClick(item) {
const itemKey = item[this.listKey];
const itemKey = item[this.listKey];
// 单选中逻辑:点击已选中项取消选中,点击未选中项切换选中(取消其他项)
this.selectedKey = this.selectedKey === itemKey ? null : itemKey;
// this.selectedKey = this.selectedKey === itemKey ? null : itemKey;
this.selectedKey = itemKey;
// 向父组件触发事件传递当前选中项null表示无选中
const selectedItem = this.selectedKey ? item : null;
@@ -154,6 +157,15 @@ export default {
// 内容为空时不显示Tooltip
if (!content) return false;
// 获取容器宽度并增加存在性检查
const container = this.$el.querySelector('.klp-list-container');
if (!container) return false; // 容器不存在时直接返回
this.containerWidth = container.clientWidth;
// 计算标题内容最大可用宽度(减去标签和边距)
const labelWidth = this.$el.querySelector('.title-label')?.offsetWidth || 60;
const availableWidth = (this.containerWidth * this.titleMaxPercent / 100) - labelWidth - 20;
// 创建临时元素测量文字实际宽度(复用元素避免性能问题)
if (!this.overflowCheckElements[itemKey]) {
const tempEl = document.createElement("span");
@@ -171,9 +183,17 @@ export default {
this.overflowCheckElements[itemKey] = tempEl;
}
// 比较文字实际宽度与设定的最大宽度
// 比较文字实际宽度与可用宽度
const tempEl = this.overflowCheckElements[itemKey];
return tempEl.offsetWidth > this.titleMaxWidth;
return tempEl.offsetWidth > availableWidth;
},
/**
* 监听容器宽度变化
*/
handleResize() {
// 宽度变化时重新计算溢出状态
this.$forceUpdate();
}
},
watch: {
@@ -190,16 +210,29 @@ export default {
}
},
/** 标题最大宽度变化时,强制重绘以重新计算溢出 */
titleMaxWidth() {
/** 标题最大百分比变化时,重新计算溢出 */
titleMaxPercent() {
this.$forceUpdate();
}
},
/** 组件销毁时清理临时元素,避免内存泄漏 */
mounted() {
this.$nextTick(() => {
const container = this.$el.querySelector('.klp-list-container');
// 增加存在性检查
this.containerWidth = container ? container.clientWidth : 0;
});
// 监听窗口大小变化
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
// 清理临时元素
Object.values(this.overflowCheckElements).forEach(el => {
document.body.removeChild(el);
});
// 移除事件监听
window.removeEventListener('resize', this.handleResize);
}
};
</script>
@@ -211,6 +244,8 @@ export default {
overflow-y: auto;
padding-right: 8px;
margin-top: 10px;
width: 100%; /* 确保容器宽度正确计算 */
box-sizing: border-box;
}
/* 加载状态容器(避免加载时容器塌陷) */
@@ -229,6 +264,7 @@ export default {
justify-content: space-between;
align-items: center;
box-sizing: border-box;
width: 100%; /* 确保列表项占满容器宽度 */
}
/* 列表项选中状态(左侧高亮边框+背景色) */
@@ -242,6 +278,8 @@ export default {
display: flex;
align-items: center;
flex: 1;
min-width: 0; /* 关键允许flex项缩小到内容尺寸以下 */
overflow: hidden; /* 确保内容不会超出容器 */
}
/* 标题前置标签样式 */
@@ -250,6 +288,7 @@ export default {
margin-right: 6px;
font-size: 13px;
white-space: nowrap; /* 标签不换行 */
flex-shrink: 0; /* 标签不缩小 */
}
/* 标题内容样式(溢出省略) */
@@ -260,7 +299,8 @@ export default {
white-space: nowrap; /* 禁止换行 */
overflow: hidden; /* 超出部分隐藏 */
text-overflow: ellipsis; /* 超出部分显示省略号 */
max-width: v-bind(titleMaxWidth + "px"); /* 绑定父组件传入的最大宽度 */
flex: 1; /* 占据剩余空间 */
min-width: 0; /* 关键:允许内容区域缩小 */
}
/* 操作按钮组(按钮间距控制) */
@@ -268,6 +308,8 @@ export default {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0; /* 操作区不缩小 */
margin-left: 8px;
}
/* 空状态样式(居中显示) */
@@ -275,4 +317,5 @@ export default {
padding: 40px 0;
text-align: center;
}
</style>
</style>

View File

@@ -1,148 +1,132 @@
<template>
<div class="base-table">
<!-- 给内部表格添加ref方便暴露 -->
<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>
<!-- 原生 Table 核心透传 props/事件/插槽 -->
<el-table
ref="internalTable"
:ref="tableRef"
v-bind="$attrs"
v-on="$listeners"
:data="data"
:loading="loading"
:class="['my-table', customClass]"
>
<!-- 通过配置数组渲染列 -->
<template v-for="(column, index) in columns">
<el-table-column
v-if="column.visible !== false"
v-bind="column"
>
<!-- 列的自定义内容插槽 -->
<template v-if="column.slot" #default="scope">
<slot :name="column.slot" :scope="scope"></slot>
</template>
<!-- 表头自定义内容 -->
<template v-if="column.headerSlot" #header>
<slot :name="column.headerSlot"></slot>
</template>
</el-table-column>
</template>
<!-- 操作列 -->
<el-table-column
v-if="showActionColumn"
:label="actionColumnLabel"
:width="actionColumnWidth"
:fixed="actionColumnFixed"
:align="actionColumnAlign"
<!-- 1. 透传列定义插槽原生 columns 中通过 slot 自定义的表头/单元格 -->
<template
v-for="(column, index) in $attrs.columns || []"
v-slot:[`header-${column.prop}`]="scope"
>
<template #default="scope">
<slot name="action" :scope="scope"></slot>
</template>
</el-table-column>
<!-- 原生插槽支持直接写el-table-column -->
<slot></slot>
<!-- 调用业务层定义的表头插槽 -->
<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>
<!-- 扩展层可后续统一添加分页如与 MyPagination 组件联动 -->
<slot name="pagination"></slot>
</div>
</template>
<script>
export default {
name: 'BaseTable',
name: "KLPTable", // 组件名,便于调试和文档生成
props: {
// 表格数据
data: {
type: Array,
default: () => []
},
// 列配置数组
columns: {
type: Array,
default: () => []
},
// 是否显示加载状态
// 1. 扩展 props新增原生 Table 没有的属性(如加载状态)
loading: {
type: Boolean,
default: false
},
// 是否显示操作列
showActionColumn: {
type: Boolean,
default: false
},
// 操作列标题
actionColumnLabel: {
loadingText: {
type: String,
default: '操作'
default: "加载中..."
},
// 操作列宽度
actionColumnWidth: {
type: Number,
default: 150
},
// 操作列固定方式
actionColumnFixed: {
// 2. 兼容原生 class 用法(原生 el-table 支持 class 属性,此处显式接收避免 $attrs 冲突)
customClass: {
type: String,
default: 'right'
},
// 操作列内容对齐方式
actionColumnAlign: {
type: String,
default: 'center'
default: ""
}
},
// 暴露内部表格的方法和属性
data() {
return {
tableRef: "myTableRef" // 表格 ref便于后续通过 ref 调用原生方法
};
},
methods: {
/**
* 获取内部el-table实例
*/
// 3. 透传原生 Table 实例方法(如 clearSelection、doLayout 等)
// 业务层可通过 this.$refs.myTable.xxx() 调用原生方法
getTableInstance() {
return this.$refs.internalTable
return this.$refs[this.tableRef];
},
/**
* 代理el-table的常用方法方便直接调用
*/
// 示例:透传原生 clearSelection 方法
clearSelection() {
if (this.$refs.internalTable) {
this.$refs.internalTable.clearSelection()
const table = this.getTableInstance();
if (table && table.clearSelection) {
table.clearSelection();
}
},
// 可根据需要扩展更多原生方法(如 toggleRowSelection、sort 等)
toggleRowSelection(row, selected) {
if (this.$refs.internalTable) {
this.$refs.internalTable.toggleRowSelection(row, selected)
}
},
toggleAllSelection() {
if (this.$refs.internalTable) {
this.$refs.internalTable.toggleAllSelection()
}
},
doLayout() {
if (this.$refs.internalTable) {
this.$refs.internalTable.doLayout()
}
},
sort(prop, order) {
if (this.$refs.internalTable) {
this.$refs.internalTable.sort(prop, order)
const table = this.getTableInstance();
if (table && table.toggleRowSelection) {
table.toggleRowSelection(row, selected);
}
}
},
// 提供一个$refs的代理方便访问内部表格
mounted() {
// 可以在这里添加一些初始化逻辑
// 扩展点:后续可统一添加初始化逻辑(如权限控制、默认排序等)
console.log("MyTable 初始化完成,原生实例:", this.getTableInstance());
}
}
};
</script>
<style scoped>
.base-table {
.my-table-container {
width: 100%;
box-sizing: border-box;
position: relative;
}
</style>
/* 扩展样式:加载状态遮罩(后续可统一调整) */
.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 {
width: 100%;
}
</style>

View File

@@ -22,7 +22,7 @@
<!-- 已完成节点悬浮弹窗 -->
<el-dialog class="comment-dialog" :title="dlgTitle || '审批记录'" :visible.sync="dialogVisible">
<el-row>
<el-table :data="taskCommentList" size="mini" border header-cell-class-name="table-header-gray">
<KLPTable :data="taskCommentList" size="mini" border header-cell-class-name="table-header-gray">
<el-table-column label="序号" header-align="center" align="center" type="index" width="55px" />
<el-table-column label="候选办理" prop="candidate" width="150px" align="center"/>
<el-table-column label="实际办理" prop="assigneeName" width="100px" align="center"/>
@@ -34,7 +34,7 @@
{{scope.row.commentList&&scope.row.commentList[0]?scope.row.commentList[0].fullMessage:''}}
</template>
</el-table-column>
</el-table>
</KLPTable>
</el-row>
</el-dialog>
<div style="position: absolute; top: 0px; left: 0px; width: 100%;">

View File

@@ -29,7 +29,7 @@
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="checkplanList" @current-change="handleCurrent" @row-dblclick="handleRowDbClick">
<KLPTable v-loading="loading" :data="checkplanList" @current-change="handleCurrent" @row-dblclick="handleRowDbClick">
<el-table-column width="55" align="center" >
<template v-slot="scope">
<el-radio v-model="selectedPlanId" :label="scope.row.planId" @change="handleRowChange(scope.row)">{{""}}</el-radio>
@@ -94,7 +94,7 @@
>停用</el-button>
</template>
</el-table-column>
</el-table>
</KLPTable>
<pagination
v-show="total>0"

View File

@@ -29,7 +29,7 @@
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="dvsubjectList" @selection-change="handleSelectionChange">
<KLPTable v-loading="loading" :data="dvsubjectList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="项目编码" align="center" prop="subjectCode" />
<el-table-column label="项目类型" align="center" prop="subjectType">
@@ -45,7 +45,7 @@
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
</el-table>
</KLPTable>
<pagination
v-show="total>0"

View File

@@ -29,7 +29,7 @@
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="dvsubjectList" @current-change="handleCurrent" @row-dblclick="handleRowDbClick">
<KLPTable v-loading="loading" :data="dvsubjectList" @current-change="handleCurrent" @row-dblclick="handleRowDbClick">
<el-table-column width="55" align="center" >
<template v-slot="scope">
<el-radio v-model="selectedId" :label="scope.row.subjectId" @change="handleRowChange(scope.row)">{{""}}</el-radio>
@@ -40,7 +40,7 @@
<el-table-column label="项目内容" align="center" prop="subjectContent" :show-overflow-tooltip="true"/>
<el-table-column label="标准" align="center" prop="subjectStandard" :show-overflow-tooltip="true"/>
<el-table-column label="备注" align="center" prop="remark" />
</el-table>
</KLPTable>
<pagination
v-show="total>0"

View File

@@ -57,7 +57,7 @@
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="machineryList" @selection-change="handleSelectionChange">
<KLPTable v-loading="loading" :data="machineryList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="设备编码" width = "120" align="center" key="machineryCode" prop="machineryCode">
</el-table-column>
@@ -75,7 +75,7 @@
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
</el-table>
</KLPTable>
<pagination
v-show="total>0"

View File

@@ -66,7 +66,7 @@
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="machineryList" @current-change="handleCurrent" @row-dblclick="handleRowDbClick">
<KLPTable v-loading="loading" :data="machineryList" @current-change="handleCurrent" @row-dblclick="handleRowDbClick">
<el-table-column width="50" align="center" >
<template v-slot="scope">
<el-radio v-model="selectedMachineryId" :label="scope.row.machineryId" @change="handleRowChange(scope.row)">{{""}}</el-radio>
@@ -88,7 +88,7 @@
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
</el-table>
</KLPTable>
<pagination
v-show="total>0"

View File

@@ -57,7 +57,7 @@
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
<KLPTable v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
<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" />
@@ -79,7 +79,7 @@
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
</el-table>
</KLPTable>
<pagination
v-show="total>0"

View File

@@ -57,7 +57,7 @@
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="userList" @current-change="handleCurrent" @row-dblclick="handleRowDbClick">
<KLPTable v-loading="loading" :data="userList" @current-change="handleCurrent" @row-dblclick="handleRowDbClick">
<el-table-column width="55" align="center" >
<template v-slot="scope">
<el-radio v-model="selectedId" :label="scope.row.userId" @change="handleRowChange(scope.row)">{{""}}</el-radio>
@@ -82,7 +82,7 @@
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
</el-table>
</KLPTable>
<pagination
v-show="total>0"