html
复制代码
<template>
<div class="t-table-wrapper">
<div class="t-table-scroll" :style="{ height: height, maxHeight: maxHeight }">
<table class="t-table">
<colgroup>
<col
v-for="(col, idx) in leafColumns"
:key="idx"
:style="{
width: col.width
? typeof col.width === 'number'
? col.width + 'px'
: col.width
: 'auto',
}"
/>
</colgroup>
<thead>
<template v-for="(row, rowIdx) in headerRows">
<tr :key="rowIdx">
<th
v-for="(cell, cellIdx) in row"
:key="cellIdx"
:colspan="cell.colspan"
:rowspan="cell.rowspan"
:style="{
textAlign: cell.align ? cell.align : 'left',
fontWeight:cell.bold ? cell.bold : 'bold',
position: 'sticky',
top: `${rowIdx * 11.2}vw`,
zIndex: 2,
}"
>
<span>{{ cell.label }}</span>
<span class="sort_table" v-if="cell.sortable && cell.prop">
<span
class="sort-btn"
:class="{ active: sortProp === cell.prop && sortOrder === 'asc' }"
@click.stop="changeSort(cell.prop, 'asc')"
title="升序"
>▲</span>
<span
class="sort-btn"
:class="{ active: sortProp === cell.prop && sortOrder === 'desc' }"
@click.stop="changeSort(cell.prop, 'desc')"
title="降序"
>▼</span>
</span>
</th>
</tr>
</template>
</thead>
<tbody>
<template v-if="data && data.length">
<tr
v-for="(row, rowIndex) in data"
:key="rowIndex"
@click="rowClick(row, rowIndex)"
:class="{ 't-table-row-striped': rowIndex % 2 === 1 && stripe }"
>
<td
v-for="(col, colIndex) in leafColumns"
:key="colIndex"
:style="{
textAlign: col.align ? col.align : 'left',
width: col.width
? typeof col.width === 'number'
? col.width + 'px'
: col.width
: 'auto',
}"
>
<template v-if="col.slotName">
<slot :name="col.slotName" :scope="row"></slot>
</template>
<template v-if="col.render">{{col.render(row, rowIndex)}}</template>
<template v-if="!col.render && !col.slotName">
{{
row[col.prop]
}}
</template>
</td>
</tr>
</template>
<template v-else>
<tr>
<td
:colspan="leafColumns.length"
style="text-align: center; color: #999; padding: 20px 0"
>{{ emptyText }}</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<script>
export default {
name: "TTable",
data() {
return {
sortProp: "",
sortOrder: "",
};
},
props: {
columns: {
type: Array,
required: true,
},
data: {
type: Array,
required: true,
},
height: {
type: String,
default: "100%",
},
maxHeight: {
type: String,
default: "",
},
emptyText: {
type: String,
default: "暂无数据",
},
stripe: {
type: Boolean,
default: false,
},
},
computed: {
headerRows() {
return this.getHeaderRows(this.columns);
},
leafColumns() {
return this.getLeafColumns(this.columns);
},
},
methods: {
changeSort(prop, order) {
if (this.sortProp === prop && this.sortOrder === order) {
this.sortProp = '';
this.sortOrder = '';
this.$emit('sort-change', { prop: '', order: '' });
} else {
this.sortProp = prop;
this.sortOrder = order;
this.$emit('sort-change', { prop, order });
}
},
rowClick(row, rowIndex) {
this.$emit("row-click", row, rowIndex);
},
getHeaderRows(columns, level = 0, rows = []) {
rows[level] = rows[level] || [];
columns.forEach(col => {
const cell = {
label: col.label,
align: col.align,
colspan: 1,
rowspan: 1,
prop: col.prop,
sortable: col.sortable,
};
if (col.children && col.children.length) {
cell.colspan = this.getLeafColumns(col.children).length;
cell.rowspan = 1;
rows[level].push(cell);
this.getHeaderRows(col.children, level + 1, rows);
} else {
cell.rowspan = this.getMaxLevel(columns) - level;
rows[level].push(cell);
}
});
return rows;
},
getLeafColumns(columns) {
let result = [];
columns.forEach(col => {
if (col.children && col.children.length) {
result = result.concat(this.getLeafColumns(col.children));
} else {
result.push(col);
}
});
return result;
},
getMaxLevel(columns, level = 1) {
let max = level;
columns.forEach(col => {
if (col.children && col.children.length) {
const childLevel = this.getMaxLevel(col.children, level + 1);
if (childLevel > max) max = childLevel;
}
});
return max;
}
},
};
</script>
<style lang="scss" scoped>
.t-table-wrapper {
width: 100%;
overflow-x: auto;
background-color: #fff;
.t-table-scroll {
min-width: 100%;
overflow: auto;
max-height: 100%;
.t-table {
width: max-content;
border-collapse: collapse;
min-width: 100%;
th,
td {
border: 1px solid #c8c8c8;
height: 42px;
padding: 0 10px;
text-align: left;
white-space: nowrap;
font-size: 14px;
white-space: normal; // 支持换行
word-break: break-all; // 长单词或长内容换行
}
th {
background: #f5f5f5;
position: relative;
}
.sort_table {
display: flex;
align-items: center;
flex-direction: column;
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
.sort-btn {
cursor: pointer;
margin-left: 2px;
font-size: 12px;
color: #bbb;
&.active {
color: #1890ff;
font-weight: bold;
}
}
}
.t-table-row-striped {
background-color: #f5f5f5; // 条纹行背景色
}
}
}
}
</style>