vue实现T型二维表格

news/2025/11/10 17:43:09/文章来源:https://www.cnblogs.com/DCL1314/p/19207788
  • 图片

868dd558e34542ef8c11447ea08f186b

  • 实现T形2维表,上下滚动,T形左右可以各自水平滚动

  • 底部和顶部水平滚动保持一致

  • 实现excle复制粘贴

  • T形左右宽度各自撑开

  • 代码如下

      <template><div class="fixed-table-container"ref="tableContainer"><!-- 顶部固定表头 --><div class="top-header"><!-- 左侧表头滚动区(可手动滚动) --><div class="top-left-scroll":style="leftAreaStyle"ref="topLeftScroll"><table><thead><tr><th v-for="(item, index) in leftHeaders":style="leftCellStyle":key="'t-l-' + index">{{ item }}</th></tr></thead></table></div><!-- 中间固定轴表头 --><div class="top-middle-axis":style="middleAxisStyle"><table><thead><tr><th :style="middleCellStyle">{{ middleAxisText }}</th></tr></thead></table></div><!-- 右侧表头滚动区(可手动滚动) --><div class="top-right-scroll":style="rightAreaStyle"ref="topRightScroll"><table><thead><tr><th v-for="(item, index) in rightHeaders":style="rightCellStyle":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":style="leftAreaStyle"@paste="(e) => handleTablePaste('left',e)"@scroll="syncLeftHorizontalScroll"><table><tbody><tr v-for="(row, rowIdx) in tableData":key="rowIdx"><td v-for="(val, colIdx) in row.left":style="leftCellStyle"@click.stop="setActiveCell(rowIdx, colIdx, 'left')":key="'l-' + rowIdx + '-' + colIdx"><input type="text"@click.stop="setActiveCell(rowIdx, colIdx, 'left')"v-model="tableData[rowIdx].left[colIdx]"class="content-input" /></td></tr></tbody></table></div><!-- 中间固定轴 --><div class="content-middle-axis":style="middleAxisStyle"><table><tbody><tr v-for="(row, rowIdx) in tableData":key="rowIdx"><td :style="middleCellStyle">{{ row.middle }}</td></tr></tbody></table></div><!-- 右侧内容滚动区 --><div class="content-right-scroll"ref="contentRightScroll":style="rightAreaStyle"@paste="(e) => handleTablePaste('right',e)"@scroll="syncRightHorizontalScroll"><table><tbody><tr v-for="(row, rowIdx) in tableData":key="rowIdx"><td v-for="(val, colIdx) in row.right":style="rightCellStyle"@click="setActiveCell(rowIdx, colIdx, 'right')":key="'r-' + rowIdx + '-' + colIdx"><input type="text"@click.stop="setActiveCell(rowIdx, colIdx, 'right')"v-model="tableData[rowIdx].right[colIdx]"class="content-input" /></td></tr></tbody></table></div></div></div></div></template><script lang="ts">import { Component, Vue } from "vue-property-decorator";@Component({ name: "", components: {} })export default class extends Vue {$refs = {tableContainer: null,topLeftScroll: null,topRightScroll: null,contentLeftScroll: null,contentRightScroll: null,};middleAxisText = "+/-";columnCount = 10; // 每侧列数columnCount1 = 5; // 每侧列数rowCount = 50; // 行数// 生成顶部左侧表头(倒序)leftHeaders = Array.from({ length: this.columnCount }, (_, i) =>((i + 1) * 0.25).toFixed(2)).reverse();// 生成顶部右侧表头(正序)rightHeaders = Array.from({ length: this.columnCount1 }, (_, i) =>((i + 1) * 0.25).toFixed(2));// 生成表格内容数据tableData = Array.from({ length: this.rowCount }, (_, rowIdx) => ({left: Array.from({ length: this.columnCount },(_, colIdx) => `L${rowIdx}_${colIdx}`),middle: (rowIdx * 0.25).toFixed(2),right: Array.from({ length: this.columnCount1 },(_, colIdx) => `R${rowIdx}_${colIdx}`),}));isSyncing = false; // 防重复触发的同步锁containerWidth = 0; // 容器实际宽度(用于计算均分)middleAxisWidth = 100; // 中间固定轴宽度headerHeight = 40; // 表头高度cellHeight = 40; // 单元格高度// 新增:记录当前激活的单元格(粘贴起始位置)activeCell = {tableType: "left", // 当前激活的表格(left/right)rowIdx: 0, // 激活行索引colIdx: 0, // 激活列索引};mounted() {// 初始化容器宽度this.updateContainerWidth();// 监听窗口大小变化,重新计算宽度window.addEventListener("resize", this.updateContainerWidth);// 初始化滚动位置对齐this.syncInitialScroll();// 页面加载后左侧滚动区自动滚动到最右侧this.scrollLeftToRight();// 绑定顶部表头滚动事件(Vue 2 兼容写法)this.bindTopScrollEvents();}beforeDestroy() {window.removeEventListener("resize", this.updateContainerWidth);// 销毁时移除事件监听,避免内存泄漏(Vue 2 生命周期)this.unbindTopScrollEvents();}// 记录当前激活的单元格(点击单元格时触发)setActiveCell(rowIdx, colIdx, tableType: "left" | "right") {this.activeCell = { tableType, rowIdx, colIdx };}// 辅助函数:判断是否为数字(含小数)isNumber(str: string): boolean {return /^-?\d+(\.\d+)?$/.test(str.trim());}// 处理表格粘贴事件handleTablePaste(tableType, e) {e.preventDefault(); // 阻止默认粘贴行为(避免直接粘贴到输入框)// 1. 获取粘贴板内容const clipboardData = e.clipboardData || (window as any).clipboardData;if (!clipboardData) {return;}const pastedText = clipboardData.getData("text");if (!pastedText.trim()) {return;}// 2. 解析粘贴内容为二维数组(Excel格式:\t分隔列,\n分隔行)const pastedRows = pastedText.split(/\r?\n/) // 兼容 Windows(\r\n) 和 Mac(\n) 换行.filter(row => row.trim() !== "") // 过滤空行.map(row => row.split("\t").map(cell => cell.trim())); // 按制表符分割列,去除单元格首尾空格// 3. 获取当前激活的起始位置和目标表格数据const { rowIdx: startRow, colIdx: startCol } = this.activeCell;const targetData = this.tableData; // 直接操作原表格数据数组const maxRows = targetData.length;if (maxRows === 0) {return}// 4. 循环赋值到表格(从起始单元格开始,超出范围忽略)pastedRows.forEach((pastedRow, rowOffset) => {const currentRow = startRow + rowOffset;// 超出表格行数,停止当前行粘贴if (currentRow >= maxRows) {return}pastedRow.forEach((pastedValue, colOffset) => {const currentCol = startCol + colOffset;const targetRow = targetData[currentRow];const targetColumn = targetRow[tableType]; // 定位到 left/right 列数组// 超出当前列数,忽略该单元格if (currentCol >= targetColumn.length) {return}// 5. 处理数值格式(保留数字,限制1位小数,空值设为""避免显示0)let finalValue = "";if (pastedValue) {// 过滤非数字字符(保留负号和小数点)const cleanedValue = pastedValue.replace(/[^-0-9.]/g, "");// 验证是否为有效数字if (this.isNumber(cleanedValue)) {// 限制1位小数finalValue = Number(cleanedValue).toFixed(1);// 去除末尾多余的 .0(可选,根据需求调整)finalValue = finalValue.endsWith(".0") ? finalValue.slice(0, -2) : finalValue;} else {// 非数字保持原值(可选,也可设为"")finalValue = pastedValue;}}// 6. 响应式更新表格数据(Vue 2 兼容写法)this.$set(targetColumn, currentCol, finalValue);});});// 7. 触发自定义事件,通知父组件数据变化this.$emit("table-pasted", {tableType,startRow,startCol,pastedRows,updatedData: [...this.tableData] // 传递更新后的数据副本});}// 更新容器宽度(关键:用于计算均分)updateContainerWidth() {const container = this.$refs.tableContainer;if (container) {this.containerWidth = container.offsetWidth;}}// 初始化滚动位置对齐syncInitialScroll() {if (this.$refs.contentLeftScroll && this.$refs.topLeftScroll) {this.$refs.topLeftScroll.scrollLeft =this.$refs.contentLeftScroll.scrollLeft;}if (this.$refs.contentRightScroll && this.$refs.topRightScroll) {this.$refs.topRightScroll.scrollLeft =this.$refs.contentRightScroll.scrollLeft;}}// 左侧滚动区(顶部+内容)滚动到最右侧scrollLeftToRight() {this.$nextTick(() => {// 顶部左侧滚动区const topLeftScroll = this.$refs.topLeftScroll;if (topLeftScroll) {topLeftScroll.scrollLeft =topLeftScroll.scrollWidth - topLeftScroll.clientWidth;}// 内容左侧滚动区const contentLeftScroll = this.$refs.contentLeftScroll;if (contentLeftScroll) {contentLeftScroll.scrollLeft =contentLeftScroll.scrollWidth - contentLeftScroll.clientWidth;}});}// 绑定顶部表头滚动事件(实现表头→表体同步)bindTopScrollEvents() {this.$nextTick(() => {const topLeftScroll = this.$refs.topLeftScroll;if (topLeftScroll) {// Vue 2 兼容的事件绑定方式topLeftScroll.addEventListener("scroll", this.handleTopLeftScroll);}const topRightScroll = this.$refs.topRightScroll;if (topRightScroll) {topRightScroll.addEventListener("scroll", this.handleTopRightScroll);}});}// 移除顶部表头滚动事件unbindTopScrollEvents() {const topLeftScroll = this.$refs.topLeftScroll;if (topLeftScroll) {topLeftScroll.removeEventListener("scroll", this.handleTopLeftScroll);}const topRightScroll = this.$refs.topRightScroll;if (topRightScroll) {topRightScroll.removeEventListener("scroll", this.handleTopRightScroll);}}// 顶部左侧滚动 → 同步内容左侧滚动handleTopLeftScroll(e) {if (this.isSyncing) {return;}this.isSyncing = true;const scrollLeft = e.target.scrollLeft;if (this.$refs.contentLeftScroll) {this.$refs.contentLeftScroll.scrollLeft = scrollLeft;}this.isSyncing = false;}// 顶部右侧滚动 → 同步内容右侧滚动handleTopRightScroll(e) {if (this.isSyncing) {return;}this.isSyncing = true;const scrollLeft = e.target.scrollLeft;if (this.$refs.contentRightScroll) {this.$refs.contentRightScroll.scrollLeft = scrollLeft;}this.isSyncing = false;}// 内容左侧滚动 → 同步顶部左侧滚动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;}// 左侧区域样式(固定宽度,确保左右侧宽度一致)get leftAreaStyle() {const sideWidth = (this.containerWidth - this.middleAxisWidth) / 2;return {width: `${sideWidth}px`,minWidth: `${sideWidth}px`,};}// 右侧区域样式(与左侧宽度一致)get rightAreaStyle() {const sideWidth = (this.containerWidth - this.middleAxisWidth) / 2;return {width: `${sideWidth}px`,minWidth: `${sideWidth}px`,};}// 左侧单元格样式(均分宽度)get leftCellStyle() {if (this.leftHeaders.length < 8) {const sideWidth = (this.containerWidth - this.middleAxisWidth) / 2;const cellWidth = sideWidth / this.leftHeaders.length;return {width: `${cellWidth}px`,minWidth: `${cellWidth}px`,height: `${this.cellHeight}px`,lineHeight: `${this.cellHeight}px`,};} else {return {width: `${this.middleAxisWidth}px`,minWidth: `${this.middleAxisWidth}px`,height: `${this.cellHeight}px`,lineHeight: `${this.cellHeight}px`,};}}// 右侧单元格样式(均分宽度)get rightCellStyle() {if (this.rightHeaders.length < 8) {const sideWidth = (this.containerWidth - this.middleAxisWidth) / 2;const cellWidth = sideWidth / this.rightHeaders.length;return {width: `${cellWidth}px`,minWidth: `${cellWidth}px`,height: `${this.cellHeight}px`,lineHeight: `${this.cellHeight}px`,};} else {return {width: `${this.middleAxisWidth}px`,minWidth: `${this.middleAxisWidth}px`,height: `${this.cellHeight}px`,lineHeight: `${this.cellHeight}px`,};}}// 中间单元格样式(高度统一,宽度与轴一致)get middleCellStyle() {return {width: `${this.middleAxisWidth}px`,height: `${this.cellHeight}px`,lineHeight: `${this.cellHeight}px`,};}// 中间固定轴样式(宽度固定)get middleAxisStyle() {return {width: `${this.middleAxisWidth}px`,height: `100%`,};}}</script><style scoped  lang="scss">.fixed-table-container {width: 100%;max-width: 1200px;height: 500px;overflow: hidden;box-sizing: border-box;background: #fff;/* 顶部固定表头 */.top-header {display: flex;position: relative;z-index: 2;box-sizing: border-box;}/* 顶部左侧滚动区 */.top-left-scroll {background-color: #ffeb3b;height: 100%;overflow-x: auto;overflow-y: hidden;-ms-overflow-style: none;border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;flex-shrink: 0; /* 固定宽度,不伸缩 */tr {th {&:first-child {border-left: 1px solid #ccc;}}}}/* 顶部右侧滚动区 */.top-right-scroll {background-color: #ffeb3b;border-right: 1px solid #ccc;border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;height: 100%;overflow-x: auto;overflow-y: hidden;-ms-overflow-style: none;flex-shrink: 0; /* 固定宽度,不伸缩 */}/* 顶部中间固定轴 */.top-middle-axis {height: 100%;background-color: #ffc107;border-left: 1px solid #ccc;border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;flex-shrink: 0;box-sizing: border-box;}/* 表头表格样式 - 关键对齐设置 */.top-header table {width: auto;border-collapse: collapse;border-spacing: 0;margin: 0;padding: 0;}.top-header th {border-right: 1px solid #ccc;text-align: center;padding: 0 8px;font-weight: normal;margin: 0;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;box-sizing: border-box;}/* 最后一列去除右边框,避免与中间轴边框重叠 */.top-left-scroll th:last-child,.top-right-scroll th:last-child {border-right: none;}/* 垂直滚动容器 */.vertical-scroll-container {height: calc(100% - 50px);overflow-y: auto; /* 仅垂直滚动 */overflow-x: hidden; /* 隐藏水平滚动条 */}/* 内容区域容器(禁止水平滚动,仅左右子元素可滚动) */.content-container {display: flex;min-height: 100%;width: 100%;overflow-x: hidden; /* 关键:父容器禁止水平滚动 */}/* 左侧内容滚动区 */.content-left-scroll {overflow-x: auto; /* 显示左侧水平滚动条 */overflow-y: hidden;flex-shrink: 0;tr {td {&:first-child {border-left: 1px solid #ccc;box-sizing: border-box;}}}}/* 中间固定轴内容区 */.content-middle-axis {box-sizing: border-box;background-color: #ffc107;border-right: 1px solid #ccc;border-left: 1px solid #ccc;flex-shrink: 0;height: 100%;}/* 右侧内容滚动区 */.content-right-scroll {overflow-x: auto; /* 显示右侧水平滚动条 */overflow-y: hidden;flex-shrink: 0;}/* 内容区表格样式 - 关键对齐设置 */.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 {box-sizing: border-box;border-right: 1px solid #ccc;border-bottom: 1px solid #ccc;text-align: center;padding: 0; /* 移除内边距避免宽度偏差 */margin: 0;}.content-left-scroll td:last-child {border-right: none;}.content-middle-axis td {border-right: none;font-weight: bold;}/* 内容区输入框样式 */.content-input {width: 100%;height: 100%;padding: 0 8px;border: none;box-sizing: border-box;text-align: center;background: transparent;vertical-align: top;font-size: 14px;}.content-input:disabled {background-color: #f5f7fa;color: #c0c4cc;cursor: not-allowed;}.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; /* 移除第一行顶部边框,避免与表头底部边框重叠 */}}/* 响应式适配 */@media (max-width: 768px) {.fixed-table-container {height: 100%;}.middleAxisWidth {width: 60px !important;}.top-middle-axis,.content-middle-axis {width: 60px !important;}.top-left-scroll,.top-right-scroll {width: calc((100% - 60px) / 2) !important;}}</style>
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/961609.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

antd table 列表树形结构展示

// 原始数据(子节点字段为 subNodes) const rawData = [{key: 1,name: 父节点,subNodes: [{ key: 1-1, name: 子节点 },],}, ];// 转换函数:递归将 subNodes 改为 children const transformData = (data: any) =>…

2025年深圳救护车运转公司权威推荐榜单:正规救护车出租/急救车出租/出租救护车源头公司精选

在医疗服务需求多元化与人口老龄化趋势加速的背景下,深圳救护车运转服务市场正经历着从基础运输向专业化、分级化的转型升级。行业数据显示,社会对非急救转运服务的需求持续上升,尤其是在康复出院、跨省转院、异地就…

对隐式类型转换保持警觉

操作符重载引起的隐式类型转换 缺点:可能导致非预期的函数被调用 解决:以功能对等的另一个函数取代类型转换操作符 举例: class Rational{ public: Rational(int num = 0,int deno = 1):num_(num),deno_(deno){}; o…

es中批量删除数据

创建bulk_delete.json 文件 {"delete":{"_index":"vivian-scene-warn-history","_type":"warnHistory","_id":"5befb3a1b25c4841bca3637efc36a320&…

docker安装mysql/Redis/nacos/minio/es/xxl-job

yum安装jdk yum -y list java*yum install -y java-1.8.0-openjdk.x86_64#检查是否安装成功java -versiondocker安装mysql docker pull mysql:5.8 docker images mkdir -p /home/service/mysql/data mkdir -p /hom…

低代码高价值场景:让设备管理真正成为企业数字化资产

本文作者:得帆信息联合创始人&CIO刘鑫 制造业设备管理的痛点 在制造业的生产现场,设备是产能的根基。无论是冲压机、注塑机还是检测设备,一旦停机,生产节拍就被打乱,交付计划可能瞬间失控。设备的稳定运行不仅…

re-BABYRE-攻防世界

有关花指令和异或加密 第一步查壳可以看出是ELF文件,拖进IDA里面看一下,shift+F12查看字符串,双击“Please input flag:”或者“Right!"跳转到反汇编窗口进入mian函数并F5查看伪代码点击查看代码 int __fastca…

二维数组去重

二维数组去重def quchong(lst): # 使用了list传引用的特性.加速了计算.for d,i in enumerate(lst):for d1,i1 in enumerate(lst,start=d):if set(i)<set(i1):lst.pop(d)if set(i1)<set(i):lst.pop(d1)return lst…

Pinely Round 5 (Div. 1 + Div. 2) A-D细解

Pinely Round 5 (Div. 1 + Div. 2) A. Round Trip 【题目】 参加cf比赛,分两类div1 div2, div1对所有人rated, div2只对<X的rated。 你可以主动选择在rated的场次加分或者减分,如果当前场次能够rated,当前分为R…

2025年三相滤波器源头厂家权威推荐榜单:EMI电源滤波器/防雷滤波器/电源滤波器源头厂家精选

在工业自动化与电能质量要求不断提升的背景下,三相滤波器作为抑制电磁干扰、保障设备稳定运行的关键元件,其性能直接影响整个电力系统的可靠性。根据行业数据显示,全球电源滤波器市场规模预计到2027年将达到13.6亿美…

UT010029: Stream is closed

做下载文件的时候遇到了一个报错:UT010029: Stream is closed经排查,是因为下载接口有返回值导致的

官宣上线!RocketMQ for AI:企业级 AI 应用异步通信首选方案

随着 AI 技术的快速发展和应用落地,RocketMQ 已完成向“AI MQ”方向的战略升级,不仅支持传统的微服务应用,也致力于为企业级 AI 应用的开发和集成提供一站式异步通信解决方案,涵盖会话管理、Agent 通信、知识库构建…

GD32VW553-IOT V2 测评和移植 - 实践

GD32VW553-IOT V2 测评和移植 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monac…

什么是 FFmpeg:开源免费的多媒体处理框架 - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

AI元人文宪章:在缺陷中前行——价值权衡时代的协作体系

AI元人文宪章:在缺陷中前行——价值权衡时代的协作体系 前言:拥抱不完美的规则 规则,是人类智慧的结晶,也是人类无知的映射。我们制定规则以求秩序与公平,却必须清醒地认识到:任何规则体系,从其诞生之初便内嵌了…

2025年台湾铨盛仪表公司口碑推荐榜

2025年台湾铨盛仪表公司口碑推荐榜专业推荐首选:昆山凯沃自动化控制设备有限公司作为台湾铨盛仪表(ADTEK)在中国大陆地区授权一级代理商已达18年的专业服务商,昆山凯沃自动化控制设备有限公司凭借其深厚的技术底蕴…

2025年销量高的前置过滤器口碑推荐榜

2025年销量高的前置过滤器口碑推荐榜随着人们对饮用水安全的日益重视,前置过滤器作为家庭净水系统的第一道防线,已成为越来越多家庭的必备选择。在众多品牌中,GOOTHO库硕凭借其卓越的性能和贴心的定制服务,在2025年…

2025年靠谱的藤椒火锅底料口碑推荐榜单

2025年藤椒火锅底料口碑推荐榜单:饭巢藤椒火锅底料领跑行业榜单前言随着消费者对健康饮食需求的不断提升,藤椒火锅底料凭借其清爽麻香、不油腻的特点,成为2025年火锅市场的新宠。经过对全国3000余家商超专柜销售数据…

2025年离心管道风机定制厂家推荐排行榜

2025年离心管道风机定制厂家推荐排行榜在工业通风和空调系统领域,离心管道风机作为核心设备,其性能和质量直接影响整个系统的运行效率。随着2025年的到来,市场对高品质离心管道风机的需求持续增长。以下是经过综合评…

CF2119E And Constraint

学到了一些东西。 首先考虑到 \(b_i\) 的二进制位一定包含了 \(a_{i - 1} | a_i\) 的二进制位,我们不妨求一个 \(p_i\) 表示将 \(b_i\) 最少加多少可以满足这个条件的数。 但是我们发现可能存在一些的与的数比 \(a_i\…