🎈 perf: 处理一些细节问题
This commit is contained in:
278
klp-ui/src/components/KLPUI/KLPList/index.vue
Normal file
278
klp-ui/src/components/KLPUI/KLPList/index.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<div class="klp-list-container">
|
||||
<!-- 列表加载状态 -->
|
||||
<div v-loading="loading" class="list-loading-wrapper">
|
||||
<!-- 列表项渲染 -->
|
||||
<div
|
||||
v-for="(item, index) in listData"
|
||||
:key="item[listKey] || index"
|
||||
class="klp-list-item"
|
||||
:class="{ 'active': isSelected(item) }"
|
||||
@click.stop="handleItemClick(item)"
|
||||
>
|
||||
<!-- 列表标题区域 - 文字溢出省略+Tooltip -->
|
||||
<div class="klp-list-title">
|
||||
<span class="title-label">{{ titleLabel }}:</span>
|
||||
<el-tooltip
|
||||
:content="item[titleField]"
|
||||
placement="top"
|
||||
:disabled="!isContentOverflow(item)"
|
||||
effect="light"
|
||||
>
|
||||
<span class="title-value">{{ item[titleField] }}</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作按钮组 -->
|
||||
<div class="klp-list-actions">
|
||||
<slot name="actions" :item="item" :isSelected="isSelected(item)"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态提示 -->
|
||||
<div v-if="listData.length === 0 && !loading" class="klp-list-empty">
|
||||
<el-empty description="暂无数据" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "klp-list",
|
||||
components: {},
|
||||
props: {
|
||||
/** 列表数据源(必传) */
|
||||
listData: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
|
||||
/** 列表项唯一标识字段(必传,如orderId、id) */
|
||||
listKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
/** 列表标题前置标签(如"订单编号:") */
|
||||
titleLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "标题"
|
||||
},
|
||||
|
||||
/** 列表项标题对应的字段名(如orderCode、name) */
|
||||
titleField: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "title"
|
||||
},
|
||||
|
||||
/** 列表加载状态 */
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
|
||||
/** 标题最大宽度(像素),控制文字溢出 */
|
||||
titleMaxWidth: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 200
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 内部管理选中状态:存储当前选中项的唯一键值(单选中)
|
||||
selectedKey: null,
|
||||
// 文字溢出检测临时元素(避免重复创建)
|
||||
overflowCheckElements: {}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 判断当前列表项是否被选中
|
||||
* @param {Object} item - 列表项数据
|
||||
* @returns {Boolean} 选中状态
|
||||
*/
|
||||
isSelected(item) {
|
||||
const itemKey = item[this.listKey];
|
||||
return this.selectedKey === itemKey;
|
||||
},
|
||||
|
||||
/**
|
||||
* 列表项点击事件:切换选中状态(单选中逻辑)
|
||||
* @param {Object} item - 点击的列表项数据
|
||||
*/
|
||||
handleItemClick(item) {
|
||||
const itemKey = item[this.listKey];
|
||||
// 单选中逻辑:点击已选中项取消选中,点击未选中项切换选中(取消其他项)
|
||||
this.selectedKey = this.selectedKey === itemKey ? null : itemKey;
|
||||
|
||||
// 向父组件触发事件:传递当前选中项(null表示无选中)
|
||||
const selectedItem = this.selectedKey ? item : null;
|
||||
this.$emit("item-click", selectedItem, item);
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空选中状态(支持父组件通过ref调用)
|
||||
*/
|
||||
clearSelection() {
|
||||
this.selectedKey = null;
|
||||
// 触发清空选中事件
|
||||
this.$emit("selection-cleared");
|
||||
},
|
||||
|
||||
/**
|
||||
* 主动设置选中项(支持父组件通过ref调用)
|
||||
* @param {String/Number} targetKey - 要选中项的唯一键值
|
||||
*/
|
||||
setSelection(targetKey) {
|
||||
const isExist = this.listData.some(item => item[this.listKey] === targetKey);
|
||||
if (isExist) {
|
||||
this.selectedKey = targetKey;
|
||||
// 触发选中事件
|
||||
const selectedItem = this.listData.find(item => item[this.listKey] === targetKey);
|
||||
this.$emit("item-click", selectedItem);
|
||||
} else {
|
||||
this.selectedKey = null;
|
||||
console.warn(`列表中不存在key为${targetKey}的项`);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 判断文字是否溢出(控制Tooltip显示/隐藏)
|
||||
* @param {Object} item - 列表项数据
|
||||
* @returns {Boolean} 是否溢出
|
||||
*/
|
||||
isContentOverflow(item) {
|
||||
const itemKey = item[this.listKey];
|
||||
const content = item[this.titleField] || "";
|
||||
|
||||
// 内容为空时不显示Tooltip
|
||||
if (!content) return false;
|
||||
|
||||
// 创建临时元素测量文字实际宽度(复用元素避免性能问题)
|
||||
if (!this.overflowCheckElements[itemKey]) {
|
||||
const tempEl = document.createElement("span");
|
||||
// 复制title-value的样式(确保测量准确)
|
||||
tempEl.style.cssText = `
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
`;
|
||||
tempEl.textContent = content;
|
||||
document.body.appendChild(tempEl);
|
||||
this.overflowCheckElements[itemKey] = tempEl;
|
||||
}
|
||||
|
||||
// 比较文字实际宽度与设定的最大宽度
|
||||
const tempEl = this.overflowCheckElements[itemKey];
|
||||
return tempEl.offsetWidth > this.titleMaxWidth;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
/** 列表数据变化时,重置选中状态和溢出检测元素 */
|
||||
listData: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.clearSelection();
|
||||
// 清理临时测量元素
|
||||
Object.values(this.overflowCheckElements).forEach(el => {
|
||||
document.body.removeChild(el);
|
||||
});
|
||||
this.overflowCheckElements = {};
|
||||
}
|
||||
},
|
||||
|
||||
/** 标题最大宽度变化时,强制重绘以重新计算溢出 */
|
||||
titleMaxWidth() {
|
||||
this.$forceUpdate();
|
||||
}
|
||||
},
|
||||
/** 组件销毁时清理临时元素,避免内存泄漏 */
|
||||
beforeDestroy() {
|
||||
Object.values(this.overflowCheckElements).forEach(el => {
|
||||
document.body.removeChild(el);
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 列表容器基础样式 */
|
||||
.klp-list-container {
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 加载状态容器(避免加载时容器塌陷) */
|
||||
.list-loading-wrapper {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
/* 列表项样式 */
|
||||
.klp-list-item {
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 6px;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 列表项选中状态(左侧高亮边框+背景色) */
|
||||
.klp-list-item.active {
|
||||
border-left: 3px solid #409eff;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
/* 列表标题区域(占满中间空间,让操作按钮靠右) */
|
||||
.klp-list-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 标题前置标签样式 */
|
||||
.klp-list-title .title-label {
|
||||
color: #606266;
|
||||
margin-right: 6px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap; /* 标签不换行 */
|
||||
}
|
||||
|
||||
/* 标题内容样式(溢出省略) */
|
||||
.klp-list-title .title-value {
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
white-space: nowrap; /* 禁止换行 */
|
||||
overflow: hidden; /* 超出部分隐藏 */
|
||||
text-overflow: ellipsis; /* 超出部分显示省略号 */
|
||||
max-width: v-bind(titleMaxWidth + "px"); /* 绑定父组件传入的最大宽度 */
|
||||
}
|
||||
|
||||
/* 操作按钮组(按钮间距控制) */
|
||||
.klp-list-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 空状态样式(居中显示) */
|
||||
.klp-list-empty {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user