<template>
<div class="fixed-table-container">
<!-- 顶部固定表头 -->
<div class="top-header">
<!-- 左侧表头滚动区 -->
<div class="top-left-scroll" ref="topLeftScroll">
<table>
<thead>
<tr>
<th v-for="(item, index) in leftHeaders" :key="'t-l-' + index">
{{ item }}
</th>
</tr>
</thead>
</table>
</div>
<!-- 中间固定轴表头 -->
<div class="top-middle-axis">
<table>
<thead>
<tr>
<th>{{ middleAxisText }}</th>
</tr>
</thead>
</table>
</div>
<!-- 右侧表头滚动区 -->
<div class="top-right-scroll" ref="topRightScroll">
<table>
<thead>
<tr>
<th v-for="(item, index) in rightHeaders" :key="'t-r-' + index">
{{ item }}
</th>
</tr>
</thead>
</table>
</div>
</div>
<!-- 垂直滚动容器 -->
<div class="vertical-scroll-container">
<!-- 中间内容区域 -->
<div class="content-container">
<!-- 左侧内容滚动区 -->
<div class="content-left-scroll"
ref="contentLeftScroll"
@scroll="syncLeftHorizontalScroll">
<table>
<tbody>
<tr v-for="(row, rowIdx) in tableData" :key="rowIdx">
<td v-for="(val, colIdx) in row.left" :key="'l-' + rowIdx + '-' + colIdx">
<input
type="text"
v-model="tableData[rowIdx].left[colIdx]"
class="content-input" />
</td>
</tr>
</tbody>
</table>
</div>
<!-- 中间固定轴 -->
<div class="content-middle-axis">
<table>
<tbody>
<tr v-for="(row, rowIdx) in tableData" :key="rowIdx">
<td>{{ row.middle }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 右侧内容滚动区 -->
<div class="content-right-scroll"
ref="contentRightScroll"
@scroll="syncRightHorizontalScroll">
<table>
<tbody>
<tr v-for="(row, rowIdx) in tableData" :key="rowIdx">
<td v-for="(val, colIdx) in row.right" :key="'r-' + rowIdx + '-' + colIdx">
<input
type="text"
v-model="tableData[rowIdx].right[colIdx]"
class="content-input" />
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "AlignedBorderTable",
data() {
const columnCount = 10; // 每侧列数
const rowCount = 50; // 行数
// 生成顶部左侧表头
const leftHeaders = Array.from({ length: columnCount }, (_, i) =>
((i + 1) * 0.25).toFixed(2)
).reverse();
// 生成顶部右侧表头
const rightHeaders = Array.from({ length: columnCount }, (_, i) =>
((i + 1) * 0.25).toFixed(2)
);
// 生成表格内容数据(序号格式)
const tableData = Array.from({ length: rowCount }, (_, rowIdx) => ({
left: Array.from({ length: columnCount }, (_, colIdx) => `L${rowIdx}_${colIdx}`),
middle: (rowIdx * 0.25).toFixed(2),
right: Array.from({ length: columnCount }, (_, colIdx) => `R${rowIdx}_${colIdx}`)
}));
return {
middleAxisText: "+/-",
leftHeaders,
rightHeaders,
tableData,
isSyncing: false
};
},
mounted () {
console.log("333")
console.log(this.leftHeaders)
console.log(this.rightHeaders)
console.log(this.tableData)
} ,
methods: {
// 同步左侧水平滚动
syncLeftHorizontalScroll(e) {
if (this.isSyncing) {return;}
this.isSyncing = true;
const scrollLeft = e.target.scrollLeft;
if (this.$refs.topLeftScroll) {
this.$refs.topLeftScroll.scrollLeft = scrollLeft;
}
this.isSyncing = false;
},
// 同步右侧水平滚动
syncRightHorizontalScroll(e) {
if (this.isSyncing) {return;}
this.isSyncing = true;
const scrollLeft = e.target.scrollLeft;
if (this.$refs.topRightScroll) {
this.$refs.topRightScroll.scrollLeft = scrollLeft;
}
this.isSyncing = false;
}
}
};
</script>
<style scoped>
.fixed-table-container {
width: 100%;
max-width: 1200px;
height: 500px;
border: 1px solid #333;
overflow: hidden;
font-family: Arial, sans-serif;
/* 核心:使用border-box确保边框包含在尺寸内 */
box-sizing: border-box;
}
/* 顶部固定表头 */
.top-header {
display: flex;
height: 40px;
background-color: #ffd700;
border-bottom: 1px solid #333;
position: relative;
z-index: 2;
}
/* 顶部左侧滚动区 */
.top-left-scroll {
flex: 1;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
.top-left-scroll::-webkit-scrollbar {
display: none;
}
/* 顶部右侧滚动区 */
.top-right-scroll {
flex: 1;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
.top-right-scroll::-webkit-scrollbar {
display: none;
}
/* 顶部中间固定轴 */
.top-middle-axis {
width: 80px;
height: 100%;
background-color: #ffc107;
border-left: 1px solid #333;
border-right: 1px solid #333;
flex-shrink: 0;
}
/* 表头表格样式 - 关键对齐设置 */
.top-header table {
width: auto;
border-collapse: collapse;
border-spacing: 0;
margin: 0;
padding: 0;
}
.top-header th {
width: 80px; /* 固定宽度,与内容区单元格一致 */
min-width: 80px;
height: 40px; /* 固定高度,与内容区行高一致 */
border-right: 1px solid #333;
text-align: center;
padding: 0; /* 移除内边距避免宽度偏差 */
font-weight: normal;
margin: 0;
}
/* 最后一列去除右边框,避免与中间轴边框重叠 */
.top-left-scroll th:last-child {
border-right: none;
}
.top-right-scroll th:last-child {
border-right: none;
}
/* 垂直滚动容器 */
.vertical-scroll-container {
height: calc(100% - 40px);
overflow-y: auto;
}
/* 内容区域容器 */
.content-container {
display: flex;
min-height: 100%;
}
/* 左侧内容滚动区 */
.content-left-scroll {
flex: 1;
overflow-x: auto;
overflow-y: hidden;
}
/* 中间固定轴内容区 */
.content-middle-axis {
width: 80px;
background-color: #ffc107;
border-right: 1px solid #333;
flex-shrink: 0;
}
/* 右侧内容滚动区 */
.content-right-scroll {
flex: 1;
overflow-x: auto;
overflow-y: hidden;
}
/* 内容区表格样式 - 关键对齐设置 */
.content-left-scroll table,
.content-middle-axis table,
.content-right-scroll table {
width: auto;
border-collapse: collapse;
border-spacing: 0;
margin: 0;
padding: 0;
}
.content-left-scroll td,
.content-middle-axis td,
.content-right-scroll td {
width: 80px; /* 固定宽度,与表头一致 */
min-width: 80px;
height: 40px; /* 固定高度,与表头一致 */
border-right: 1px solid #333;
border-bottom: 1px solid #333;
text-align: center;
padding: 0; /* 移除内边距避免宽度偏差 */
margin: 0;
}
/* 最后一列去除右边框 */
.content-left-scroll td:last-child {
border-right: none;
}
.content-right-scroll td:last-child {
border-right: none;
}
.content-middle-axis td {
border-right: none;
font-weight: bold;
line-height: 40px; /* 垂直居中 */
}
/* 内容区输入框样式 */
.content-input {
width: 100%;
height: 100%;
padding: 0 8px;
border: none; /* 去除输入框自身边框 */
box-sizing: border-box;
text-align: center;
background-color: #fff;
font-size: 14px;
}
.content-input:focus {
outline: none;
background-color: #f0f7ff;
}
/* 修复第一行顶部边框与表头底部边框对齐 */
.content-left-scroll tr:first-child td,
.content-middle-axis tr:first-child td,
.content-right-scroll tr:first-child td {
border-top: none; /* 移除第一行顶部边框,避免与表头底部边框重叠 */
}
</style>