css-colspan rowspan cols rows

本文探讨了HTML表格中 rowspan 和 colspan 的应用,展示了如何利用这些属性实现表格的跨行跨列布局。同时,介绍了使用 frameset 实现网页布局的方法,通过设置 rows 和 cols 属性来划分窗口区域。
部署运行你感兴趣的模型镜像
  • col:列,row:行,span:跨度,跨距,范围
  • rowspan、colspan通常使用在tdth标签中
  • rowspan 属性  --  可以实现HTML表格中一列跨越多行
  • colspan 属性  --  可以实现HTML表格中一行跨越多列
  • rows属性 -- 当前控件的宽度
  • cols属性 -- 当前控件的高度

 <td rowspan="2" colspan="2"></td>

 

  <td>

      <textarea cols="80" rows="10"></textared>

  </td>

//将当前的窗体分为左右两部分,左边宽88px

  <frameset id="mainFrameSet" rows="88,*" cols="*" frameborder="no" border="0" framespacing="0">
     <frame src="#" name="topFrame" scrolling="no" noresize="noresize" id="topFrame" title="topFrame" />
     <frame src="#" name="mainFrame" style="overflow-x:hidden;overflow-y:auto;" id="mainFrame" title="mainFrame" />
    </frameset>

您可能感兴趣的与本文相关的镜像

Llama Factory

Llama Factory

模型微调
LLama-Factory

LLaMA Factory 是一个简单易用且高效的大型语言模型(Large Language Model)训练与微调平台。通过 LLaMA Factory,可以在无需编写任何代码的前提下,在本地完成上百种预训练模型的微调

import xml.etree.ElementTree as ET from typing import List, Tuple, Dict, Set def auto_detect_merges(data: List[List[str]]) -> List[Dict]: """ 自动检测需要合并的单元格 返回合并规则列表:[{'row': 起始行, 'col': 起始列, 'rowspan': 行数, 'colspan': 列数}] """ rows = len(data) if rows == 0: return [] cols = len(data[0]) # 记录已被合并的单元格 merged = [[False for _ in range(cols)] for _ in range(rows)] merge_rules = [] for r in range(rows): for c in range(cols): if merged[r][c]: continue current_value = data[r][c] if current_value is None or current_value == "": continue # 检测横向合并 (相同行连续相同值) colspan = 1 while c + colspan < cols and data[r][c + colspan] == current_value: colspan += 1 # 检测纵向合并 (下方行相同列相同值) rowspan = 1 while r + rowspan < rows and all( data[r + rowspan][c + k] == current_value for k in range(colspan) ): rowspan += 1 # 如果发现需要合并 if colspan > 1 or rowspan > 1: merge_rules.append({ 'row': r, 'col': c, 'rowspan': rowspan, 'colspan': colspan }) # 标记已被合并的单元格 for i in range(r, r + rowspan): for j in range(c, c + colspan): if i != r or j != c: # 跳过起始单元格 merged[i][j] = True return merge_rules def create_colored_svg( data: List[List[str]], output_path: str, red_cells: Set[Tuple[int, int]] = None, red_text_condition: callable = None ): """ 创建带颜色标记的SVG表格 参数: - data: 二维表格数据 - output_path: 输出SVG路径 - red_cells: 需要标红的单元格坐标集合,如 {(1,2), (3,4)} - red_text_condition: 动态判断是否标红的函数 lambda cell_value, row, col: bool """ rows = len(data) if rows == 0: raise ValueError("表格数据不能为空") cols = len(data[0]) # 自动检测合并规则 merges = auto_detect_merges(data) # 创建SVG根元素 svg = ET.Element('svg', { 'xmlns': 'http://www.w3.org/2000/svg', 'width': str(cols * 120), 'height': str(rows * 40 ) }) # 添加样式 style = ET.SubElement(svg, 'style') style.text = """ text { font-family: Arial; font-size: 14px; dominant-baseline: middle; text-anchor: middle; } text.red { fill: #ff0000; font-weight: bold; } rect.header { fill: #f2f2f2; stroke: #ddd; stroke-width: 1px; } rect.cell { fill: #fff; stroke: #eee; stroke-width: 1px; } """ # 创建表格组 table_group = ET.SubElement(svg, 'g', {'transform': 'translate(20, 20)'}) # 处理合并单元格 merge_map = {} for merge in merges: for r in range(merge['row'], merge['row'] + merge['rowspan']): for c in range(merge['col'], merge['col'] + merge['colspan']): merge_map[(r, c)] = (merge['row'], merge['col']) # 绘制表格 for r in range(rows): for c in range(cols): if (r, c) in merge_map and merge_map[(r, c)] != (r, c): continue actual_r, actual_c = merge_map.get((r, c), (r, c)) # 计算合并跨度 rowspan = 1 colspan = 1 for merge in merges: if merge['row'] == actual_r and merge['col'] == actual_c: rowspan = merge['rowspan'] colspan = merge['colspan'] break # 绘制单元格 cell_class = "header" if actual_r == 0 else "cell" ET.SubElement(table_group, 'rect', { 'x': str(actual_c * 120), 'y': str(actual_r * 40), 'width': str(colspan * 120), 'height': str(rowspan * 40), 'class': cell_class, 'rx': '5' }) # 判断是否需要红色文本 text_class = "" if red_cells and (r, c) in red_cells: text_class = "red" elif red_text_condition and red_text_condition(data[r][c], r, c): text_class = "red" # 添加文字 ET.SubElement(table_group, 'text', { 'x': str(actual_c * 120 + colspan * 60), 'y': str(actual_r * 40 + rowspan * 20), 'class': text_class }).text = str(data[r][c]) # 保存SVG文件 tree = ET.ElementTree(svg) tree.write(output_path, encoding='utf-8', xml_declaration=True) print(f"SVG表格已生成: {output_path}") 这是一段python 程序,功能是输入数据,渲染成svg,并对部分表格数据标红,现在渲染的svg,表格宽度不一,希望宽度能自适应,同时表格字体内的高度缩小点
07-05
<template> <div class="bg-gray-50 p-6"> <div class="max-w-7xl mx-auto"> <h1 class="text-2xl font-bold text-gray-800 mb-6">交叉决策表</h1> <div class="table-container"> <table id="decision-table" class="decision-table"> <thead id="table-header"> <tr v-for="(headerRow, rowIndex) in tableData.headers" :key="rowIndex" class="table-header" > <th v-for="(cell, colIndex) in headerRow" :key="colIndex" :colspan="cell.colspan" :rowspan="cell.rowspan" :data-isHeader="cell.header==='col'" class="h" :class="{ 'header-cell':true, 'highlight-cell': isCellHighlighted(cell.angelCell.startRow, cell.angelCell.endCol) }" scope="col" @contextmenu.prevent="showContextMenu($event, { isHeader: true, rowIndex: cell.angelCell.startRow, colIndex: cell.angelCell.endCol, cell:cell })" > {{ cell }} </th> </tr> </thead> <tbody id="table-body"> <tr v-for="(row, rowIndex) in tableData.rows" :key="rowIndex" > <td v-for="(cell, colIndex) in row" :key="colIndex" :colspan="cell.colspan" :rowspan="cell.rowspan" :data-isHeader="cell.header==='row'" :class="{ 'header-cell merged-cell': cell.header==='row', 'data-cell': cell.header!=='row', 'highlight-cell': isCellHighlighted(cell.angelCell.startRow, cell.angelCell.endCol) }" :data-scope="cell.header==='row' ? 'row' : 'data'" @contextmenu.prevent="cell.isHeader && showContextMenu($event, { isHeader: true, rowIndex: cell.angelCell.startRow, colIndex: cell.angelCell.endCol })" > {{ cell }} </td> </tr> </tbody> </table> <!-- <table id="decision-table" class="decision-table">--> <!-- <tbody id="table-body">--> <!-- <tr--> <!-- v-for="(row, rowIndex) in this.angelTable"--> <!-- :key="rowIndex"--> <!-- >--> <!-- <td--> <!-- v-for="(cell, colIndex) in row"--> <!-- :key="colIndex"--> <!-- :data-isHeader="cell.cell.header==='row'"--> <!-- :class="{--> <!-- 'header-cell merged-cell': cell.cell.header==='row',--> <!-- 'data-cell': cell.cell.header!=='row',--> <!-- 'highlight-cell': isCellHighlighted(cell.cell.angelCell.startRow, cell.cell.angelCell.endCol)--> <!-- }"--> <!-- :data-scope="cell.header==='row' ? 'row' : 'data'"--> <!-- @contextmenu.prevent="cell.isHeader && showContextMenu($event, {--> <!-- isHeader: true,--> <!-- rowIndex: cell.angelCell.startRow,--> <!-- colIndex: cell.angelCell.endCol--> <!-- })"--> <!-- >--> <!-- {{ cell }}--> <!-- </td>--> <!-- </tr>--> <!-- </tbody>--> <!-- </table>--> </div> <div class="mt-6 text-sm text-gray-500"> <p>提示:在表头单元格上右键点击可打开上下文菜单,进行行和列的添加/删除操作。</p> </div> </div> <!-- 上下文菜单 --> <div id="context-menu" :class="contextMenuClass" :style="{ left: contextMenuLeft + 'px', top: contextMenuTop + 'px' }" > <div class="context-menu-item" @click="executeAction('add-row')"> <i class="fa fa-plus-circle mr-2"></i>新增行 </div> <div class="context-menu-item" @click="executeAction('delete-row')"> <i class="fa fa-minus-circle mr-2"></i>删除行 </div> <div class="context-menu-separator"></div> <div class="context-menu-item" @click="executeAction('add-column')"> <i class="fa fa-plus-square mr-2"></i>新增列 </div> <div class="context-menu-item" @click="executeAction('delete-column')"> <i class="fa fa-minus-square mr-2"></i>删除列 </div> </div> </div> </template> <script> export default { data() { return { // 表格数据模型 tableData:{ headers: [ // 第一级表头 [ { text: '', colspan: 2, rowspan: 2, header: "col" }, { text: '维度A', colspan: 6, rowspan: 1, header: "col" } ], // 第二级表头 [ { text: '指标A', colspan: 2, rowspan: 1, header: "col" }, // { text: 'XXXXX', colspan: 1, rowspan: 1, header: "col" }, { text: '指标B', colspan: 2, rowspan: 1, header: "col" }, { text: '指标C', colspan: 2, rowspan: 1, header: "col" } ], ] ,rows: [ // 第一行数据 [ { text: '0002 设计部', colspan: 1, rowspan: 2, header: "row" }, { text: '设计1部', colspan: 1, rowspan: 1, header: "row" }, { text: '6%', colspan: 1, rowspan: 1 }, { text: '6%', colspan: 1, rowspan: 1 }, // { text: 'xxxx%', colspan: 1, rowspan: 1 }, { text: '5%', colspan: 1, rowspan: 1 }, { text: '3%', colspan: 1, rowspan: 1 }, { text: '4%', colspan: 1, rowspan: 1 }, { text: '4%', colspan: 1, rowspan: 1 } ], // 第二行数据 [ { text: '设计2部', colspan: 1, rowspan: 1, header: "row" }, { text: '10%', colspan: 1, rowspan: 1 }, { text: '8%', colspan: 1, rowspan: 1 }, // { text: 'xxxx%', colspan: 1, rowspan: 1 }, { text: '16%', colspan: 1, rowspan: 1 }, { text: '16%', colspan: 1, rowspan: 1 }, { text: '19%', colspan: 1, rowspan: 1 }, { text: '18%', colspan: 1, rowspan: 1 } ] ] } , // 当前选中的单元格信息 currentCellInfo: null, // 上下文菜单位置 contextMenuLeft: 0, contextMenuTop: 0, // 是否显示上下文菜单 showContextMenuFlag: false, angelTable:[], cloneTable:null, defaultColHeaderCell:{text: '新增列', colspan: 1, rowspan: 1, header: "col"}, defaultRowHeaderCell:{text: '新增行', colspan: 1, rowspan: 1, header: "row"}, defaultRowDataCell:{text: '新增行', colspan: 1, rowspan: 1, header: "data"} }; }, computed: { // 计算上下文菜单的显示状态 contextMenuClass() { // console.log("执行contextMenuClass") return this.showContextMenuFlag ? 'context-menu' : 'context-menu hidden'; }, }, created() { this.getAngelTable(); }, methods: { // 检查单元格是否被高亮 isCellHighlighted(rowIndex, colIndex) { if (!this.currentCellInfo) return false; return ( this.currentCellInfo.rowIndex === rowIndex && this.currentCellInfo.colIndex === colIndex ); }, // 显示上下文菜单 showContextMenu(event, cellInfo) { this.currentCellInfo = cellInfo; this.contextMenuLeft = event.pageX; this.contextMenuTop = event.pageY; this.showContextMenuFlag = true; {{console.log("this.angelTable",this.angelTable)}} // console.log('右键菜单点击',JSON.stringify(this.tableData)); // 新增这行 // console.log(this.getAngelTable(),); }, // 执行菜单操作 executeAction(action) { // console.log('执行菜单操作'); switch (action) { case 'add-row': this.addRow(); break; case 'delete-row': this.deleteRow(); break; case 'add-column': this.addColumn(1); break; case 'delete-column': this.deleteColumn(); break; } this.showContextMenuFlag = false; this.currentCellInfo = null; }, // 新增行 addRow() { console.log("新增行") if (!this.currentCellInfo || !this.currentCellInfo.isHeader) return; const { rowIndex } = this.currentCellInfo; const isHeaderRow = this.currentCellInfo.isHeader && this.currentCellInfo.rowIndex < this.tableData.length; if (isHeaderRow) { // 新增表头行(简化处理) alert('新增表头行功能需要更复杂的逻辑处理,请参考数据行添加方式实现'); } else { // 新增数据行 const newRow = []; // 获取上一行的单元格数量 const prevRowCells = this.tableData[rowIndex] || []; // 创建新行的单元格 prevRowCells.forEach(cell => { // 如果是合并的单元格且跨越多行,需要调整 if (cell.rowspan > 1) { // 减少上一行合并单元格的rowspan cell.rowspan--; // 如果当前行是合并单元格的第一行,不添加新单元格 if (rowIndex === 0 || prevRowCells || !prevRowCells[prevRowCells.indexOf(cell)].skip) { return; } } // 创建新单元格,复制上一行的结构 newRow.push({ text: '', colspan: cell.colspan, rowspan: cell.rowspan, isHeader: cell.isHeader }); }); // 插入新行 this.tableData.rows.splice(rowIndex + 1, 0, newRow); } }, // 删除行 deleteRow() { if (!this.currentCellInfo || !this.currentCellInfo.isHeader) return; const { rowIndex } = this.currentCellInfo; const isHeaderRow = this.currentCellInfo.isHeader && this.currentCellInfo.rowIndex < this.tableData.length; if (isHeaderRow) { // 删除表头行(简化处理) alert('删除表头行功能需要更复杂的逻辑处理,请参考数据行删除方式实现'); } else { // 删除数据行 if (this.tableData.length <= 1) { alert('至少需要保留一行数据'); return; } // 检查是否有合并到当前行的单元格 this.tableData.forEach((row, rIndex) => { if (rIndex >= rowIndex) return; row.forEach(cell => { if (cell.rowspan > 1 && rIndex + cell.rowspan > rowIndex) { // 调整合并单元格的rowspan cell.rowspan--; } }); }); // 删除行 this.tableData.rows.splice(rowIndex, 1); } }, // 新增列 addColumn(col) { if (!this.currentCellInfo || !this.currentCellInfo.isHeader) return; if (col!==1 && col!==-1) return; const { cell } = this.currentCellInfo; // 单元格实际的开始行数 const angelStartRow = cell.angelCell.startRow; // const angelEndRow = cell.angelCell.startRow; // angelTable 不用操作, 直接找到 原Table操作以后,重新生产AngelTable // console.log("this.angelCell",JSON.stringify(cell.angelCell)); // 单元格真实的结束列数 let headerLine = this.cloneTable.headers.length; const angelEndCol = cell.angelCell.endCol; // 处理父级表头行 for (let i = angelStartRow ; i > 0; i--){ console.log("this.angelTable",JSON.stringify(this.angelTable[angelStartRow-1][angelEndCol].cell)); const angelCellParentCell = this.angelTable[angelStartRow-1][angelEndCol].cell; if(col===1){ angelCellParentCell.colspan += col; } if (col===-1){ angelCellParentCell.colspan -= cell.colspan; } console.log("this.angelCellParentCell",JSON.stringify(angelCellParentCell)); } // 处理平级及以下表头行 for (let i = angelStartRow ; i < headerLine; i++){ console.log("this.angelTable[i][angelEndCol]",this.angelTable[i][angelEndCol]); let mergeColIndex = this.angelTable[i][angelEndCol].cell.angelCell.mergeColIndex + 1; console.log("mergeColIndex",mergeColIndex); if(col===1){ this.cloneTable.headers[i].splice(mergeColIndex, 0,this.defaultColHeaderCell); } if(col===-1){ this.cloneTable.headers[i].splice(mergeColIndex-1, 1,); } } // 处理数据行 let rowLine = this.tableData.rows.length; for (let i = 0 ; i < rowLine; i++){ console.log("this.angelTable[i][angelEndCol]",this.angelTable[i][angelEndCol]); let mergeColIndex = this.angelTable[i+headerLine][angelEndCol].cell.angelCell.mergeColIndex + 1; console.log("mergeColIndex",mergeColIndex); if(col===1){ this.cloneTable.rows[i].splice(mergeColIndex, 0,this.defaultRowDataCell); } if(col===-1){ this.cloneTable.rows[i].splice(mergeColIndex-cell.colspan, cell.colspan,); } } this.tableData = this.cloneTable; this.getAngelTable(); }, // 删除列 deleteColumn() { this.addColumn(-1); }, // 计算总列数 countTotalColumns() { if (!this.tableData.headers.length) return 0; let totalColumns = 0; this.tableData.headers[0].forEach(cell => { totalColumns += cell.colspan; }); return totalColumns; }, /** * 转换表格行数据为包含合并单元格信息的二维数组 * @param {Object} rows 表格行数据,每行包含columns属性(单元格数组) * @returns {Array<Array>} 二维数组,每个位置填充对应的合并单元格信息 */ getAngelTable() { // const rows = [].concat(this.cloneTable.headers).concat(this.cloneTable.rows); const rows = [].concat(this.tableData.headers).concat(this.tableData.rows); // 存储所有解析后的单元格信息 const resolvedCells = []; // 记录每行已被占用的列(key:行号,value:被占用的列号集合) const occupiedCols = {}; // let headerRows = this.cloneTable.headers.length; console.log("rows:",rows) // 遍历每行,计算每个单元格的位置 for (let currentRow = 0; currentRow < rows.length; currentRow++) { const row = rows[currentRow]; if (!row) continue; let currentCol = 0; // 当前行的起始列(从0开始) for (let col = 0; col < row.length;col ++) { let cell = row[col]; // 计算当前单元格的startCol:跳过已被占用的列 while (this.isColumnOccupied(currentRow, currentCol, occupiedCols)) { currentCol++; } // 单元格的基本信息 const colSpan = cell.colspan; const rowSpan = cell.rowspan; const startRow = currentRow; const startCol = currentCol; const endRow = startRow + rowSpan - 1; const endCol = startCol + colSpan - 1; // 记录单元格信息 resolvedCells.push({ startRow, startCol, endRow, endCol, cell, // 可根据需要添加原单元格的其他属性 }); const mergeColIndex = col; const mergeRowIndex = currentRow; cell.angelCell = { mergeColIndex, mergeRowIndex, startRow, startCol, endRow, endCol, } // 标记当前单元格覆盖的所有行和列为“已占用” this.markOccupiedColumns(startRow, endRow, startCol, endCol, occupiedCols); // 移动到下一个未被占用的列 currentCol = endCol + 1; } } // this.tableData = this.cloneTable; // 计算表格的总行数和总列数 const totalRows = resolvedCells.length ? Math.max(...resolvedCells.map(cell => cell.endRow)) + 1 : 0; const totalCols = resolvedCells.length ? Math.max(...resolvedCells.map(cell => cell.endCol)) + 1 : 0; // 初始化二维数组并填充null const flatTable = []; for (let i = 0; i < totalRows; i++) { flatTable.push(new Array(totalCols).fill(null)); } // 填充每个单元格到其覆盖的所有位置 for (const cell of resolvedCells) { for (let r = cell.startRow; r <= cell.endRow; r++) { for (let c = cell.startCol; c <= cell.endCol; c++) { flatTable[r][c] = cell; } } } this.cloneTable = this.tableData; this.tableData = JSON.parse(JSON.stringify(this.tableData)); this.angelTable = flatTable; }, /** * 检查当前行的当前列是否被占用 * @param {number} row 行号 * @param {number} col 列号 * @param {Object} occupiedCols 存储每行被占用列的对象 * @returns {boolean} 是否被占用 */ isColumnOccupied(row, col, occupiedCols) { const occupied = occupiedCols[row] || new Set(); return occupied.has(col); }, /** * 标记单元格覆盖的所有行和列为“已占用” * @param {number} startRow 起始行 * @param {number} endRow 结束行(含) * @param {number} startCol 起始列 * @param {number} endCol 结束列(含) * @param {Object} occupiedCols 存储每行被占用列的对象 */ markOccupiedColumns(startRow, endRow, startCol, endCol, occupiedCols) { for (let r = startRow; r <= endRow; r++) { // 为当前行初始化占用列集合 if (!occupiedCols[r]) { occupiedCols[r] = new Set(); } const cols = occupiedCols[r]; // 添加当前单元格覆盖的列 for (let c = startCol; c <= endCol; c++) { cols.add(c); } } } }, mounted() { // 点击其他地方关闭菜单 document.addEventListener('click', () => { this.showContextMenuFlag = false; this.currentCellInfo = null; }); } }; </script> <style> table { width: 100%; max-width: 1000px; margin: 20px auto; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; } /* 所有单元格显示边框 */ th, td { padding: 12px 15px; border: 1px solid #ccc; text-align: center; } /* 列表头(横向表头)样式 - 适用于任意数量的顶部表头行 */ th[scope="col"] { background-color: #4a90e2; /* 列表头蓝色 */ color: white; font-weight: bold; } /* 行表头(纵向表头)样式 - 适用于任意数量的左侧表头列 */ td[data-scope="row"] { background-color: #4a90e2; /* 行表头绿色 */ color: white; font-weight: bold; position: sticky; left: 0; z-index: 1; } /* 表头交叉区域(同时属于行列表头的单元格) */ th[scope="colgroup"] { background-color: #5cb85c; /* 交叉区域橙色 */ color: white; font-weight: bold; z-index: 2; } /* 响应式调整 */ @media screen and (max-width: 768px) { th, td { padding: 8px 10px; font-size: 13px; } } /* 上下文菜单样式 */ .context-menu { position: absolute; background-color: white; border: 1px solid #ccc; border-radius: 4px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); padding: 5px 0; z-index: 1000; } .context-menu-item { padding: 8px 15px; cursor: pointer; white-space: nowrap; } .context-menu-item:hover { background-color: #f5f5f5; } .context-menu-separator { height: 1px; background-color: #eee; margin: 5px 0; } .hidden { display: none; } .highlight-cell { background-color: #ffecb3 !important; } /* 上下文菜单容器 */ .context-menu { /* 核心:绝对定位,基于页面坐标定位 */ position: absolute; /* 确保菜单在最上层,避免被其他元素覆盖 */ z-index: 9999; /* 固定宽度,避免内容撑开导致位置偏移 */ width: 160px; /* 基础外观 */ background-color: #fff; border: 1px solid #e0e0e0; border-radius: 4px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15); /* 去除默认边距 */ margin: 0; padding: 4px 0; /* 避免选中菜单文本 */ user-select: none; } /* 菜单项样式 */ .context-menu-item { padding: 8px 16px; cursor: pointer; font-size: 14px; color: #333; /* hover 效果 */ transition: background-color 0.2s; } .context-menu-item:hover { background-color: #f5f5f5; } /* 菜单图标与文字间距 */ .context-menu-item i { width: 16px; text-align: center; } /* 分隔线样式 */ .context-menu-separator { height: 1px; margin: 4px 0; background-color: #e0e0e0; } /* 隐藏菜单的类(配合 Vue 状态控制) */ .hidden { display: none; } </style>
最新发布
10-25
<template> <div class="bg-gray-50 p-6"> <div class="max-w-7xl mx-auto"> <h1 class="text-2xl font-bold text-gray-800 mb-6">交叉决策表</h1> <div class="table-container"> <table id="decision-table" class="decision-table"> <thead id="table-header"> <tr v-for="(headerRow, rowIndex) in tableData.headers" :key="rowIndex" class="table-header" > <th v-for="(cell, colIndex) in headerRow" :key="colIndex" :colspan="cell.colspan" :rowspan="cell.rowspan" :data-isHeader="cell.isHeader" class="header-cell" scope="col" @contextmenu.prevent="showContextMenu($event, { isHeader: true, rowIndex: rowIndex, colIndex: colIndex, cell:cell })" > {{ cell.text }} </th> </tr> </thead> <tbody id="table-body"> <tr v-for="(row, rowIndex) in tableData.rows" :key="rowIndex" > <td v-for="(cell, colIndex) in row" :key="colIndex" :colspan="cell.colspan" :rowspan="cell.rowspan" :data-isHeader="cell.isHeader" :class="{ 'header-cell merged-cell': cell.isHeader, 'data-cell': !cell.isHeader, 'highlight-cell': isCellHighlighted(rowIndex, colIndex) }" :data-scope="cell.isHeader ? 'row' : 'data'" @contextmenu.prevent="cell.isHeader && showContextMenu($event, { isHeader: true, rowIndex: rowIndex, colIndex: colIndex })" > {{ cell.text }} </td> </tr> </tbody> </table> </div> <div class="mt-6 text-sm text-gray-500"> <p>提示:在表头单元格上右键点击可打开上下文菜单,进行行和列的添加/删除操作。</p> </div> </div> <!-- 上下文菜单 --> <div id="context-menu" :class="contextMenuClass" :style="{ left: contextMenuLeft + 'px', top: contextMenuTop + 'px' }" > <div class="context-menu-item" @click="executeAction('add-row')"> <i class="fa fa-plus-circle mr-2"></i>新增行 </div> <div class="context-menu-item" @click="executeAction('delete-row')"> <i class="fa fa-minus-circle mr-2"></i>删除行 </div> <div class="context-menu-separator"></div> <div class="context-menu-item" @click="executeAction('add-column')"> <i class="fa fa-plus-square mr-2"></i>新增列 </div> <div class="context-menu-item" @click="executeAction('delete-column')"> <i class="fa fa-minus-square mr-2"></i>删除列 </div> </div> </div> </template> <script> export default { data() { return { // 表格数据模型 tableData: { headers: [ // 第一级表头 [ { text: '', colspan: 2, rowspan: 2, isHeader: true }, { text: '维度A', colspan: 6, rowspan: 1, isHeader: true } ], // 第二级表头 [ { text: '指标A', colspan: 2, rowspan: 1, isHeader: true }, { text: '指标B', colspan: 2, rowspan: 1, isHeader: true }, { text: '指标C', colspan: 2, rowspan: 1, isHeader: true } ], // 第三级表头 [ { text: '部门', colspan: 1, rowspan: 1, isHeader: true }, { text: '业务线', colspan: 1, rowspan: 1, isHeader: true }, { text: '目标', colspan: 1, rowspan: 1, isHeader: true }, { text: '实际', colspan: 1, rowspan: 1, isHeader: true }, { text: '目标', colspan: 1, rowspan: 1, isHeader: true }, { text: '实际', colspan: 1, rowspan: 1, isHeader: true }, { text: '目标', colspan: 1, rowspan: 1, isHeader: true }, { text: '实际', colspan: 1, rowspan: 1, isHeader: true } ] ], rows: [ // 第一行数据 [ { text: '0002 设计部', colspan: 1, rowspan: 2, isHeader: true }, { text: '设计1部', colspan: 1, rowspan: 1, isHeader: true }, { text: '6%', colspan: 1, rowspan: 1 }, { text: '6%', colspan: 1, rowspan: 1 }, { text: '5%', colspan: 1, rowspan: 1 }, { text: '3%', colspan: 1, rowspan: 1 }, { text: '4%', colspan: 1, rowspan: 1 }, { text: '4%', colspan: 1, rowspan: 1 } ], // 第二行数据 [ { text: '设计2部', colspan: 1, rowspan: 1, isHeader: true }, { text: '10%', colspan: 1, rowspan: 1 }, { text: '8%', colspan: 1, rowspan: 1 }, { text: '16%', colspan: 1, rowspan: 1 }, { text: '16%', colspan: 1, rowspan: 1 }, { text: '19%', colspan: 1, rowspan: 1 }, { text: '18%', colspan: 1, rowspan: 1 } ] ] }, // 当前选中的单元格信息 currentCellInfo: null, // 上下文菜单位置 contextMenuLeft: 0, contextMenuTop: 0, // 是否显示上下文菜单 showContextMenuFlag: false, angelData:[] }; }, computed: { // 计算上下文菜单的显示状态 contextMenuClass() { // console.log("执行contextMenuClass") return this.showContextMenuFlag ? 'context-menu' : 'context-menu hidden'; } }, created() { this.angelData = this.getAngelTable(); console.log("this.angelData",this.angelData); }, watch: { tableData(newData,oldData){ } }, methods: { // 检查单元格是否被高亮 isCellHighlighted(rowIndex, colIndex) { if (!this.currentCellInfo) return false; return ( this.currentCellInfo.rowIndex === rowIndex && this.currentCellInfo.colIndex === colIndex ); }, // 显示上下文菜单 showContextMenu(event, cellInfo) { this.currentCellInfo = cellInfo; this.contextMenuLeft = event.pageX; this.contextMenuTop = event.pageY; this.showContextMenuFlag = true; console.log('右键菜单点击',cellInfo); // 新增这行 }, // 执行菜单操作 executeAction(action) { // console.log('执行菜单操作'); console.log(this.getAngelTable()) switch (action) { case 'add-row': this.addRow(); break; case 'delete-row': this.deleteRow(); break; case 'add-column': this.addColumn(); break; case 'delete-column': this.deleteColumn(); break; } this.showContextMenuFlag = false; this.currentCellInfo = null; }, // 新增行 addRow() { console.log("新增行") if (!this.currentCellInfo || !this.currentCellInfo.isHeader) return; const { rowIndex } = this.currentCellInfo; const isHeaderRow = this.currentCellInfo.isHeader && this.currentCellInfo.rowIndex < this.tableData.headers.length; if (isHeaderRow) { // 新增表头行(简化处理) alert('新增表头行功能需要更复杂的逻辑处理,请参考数据行添加方式实现'); } else { // 新增数据行 const newRow = []; // 获取上一行的单元格数量 const prevRowCells = this.tableData.rows[rowIndex] || []; // 创建新行的单元格 prevRowCells.forEach(cell => { // 如果是合并的单元格且跨越多行,需要调整 if (cell.rowspan > 1) { // 减少上一行合并单元格的rowspan cell.rowspan--; // 如果当前行是合并单元格的第一行,不添加新单元格 if (rowIndex === 0 || prevRowCells || !prevRowCells[prevRowCells.indexOf(cell)].skip) { return; } } // 创建新单元格,复制上一行的结构 newRow.push({ text: '', colspan: cell.colspan, rowspan: cell.rowspan, isHeader: cell.isHeader }); }); // 插入新行 this.tableData.rows.splice(rowIndex + 1, 0, newRow); } }, // 删除行 deleteRow() { if (!this.currentCellInfo || !this.currentCellInfo.isHeader) return; const { rowIndex } = this.currentCellInfo; const isHeaderRow = this.currentCellInfo.isHeader && this.currentCellInfo.rowIndex < this.tableData.headers.length; if (isHeaderRow) { // 删除表头行(简化处理) alert('删除表头行功能需要更复杂的逻辑处理,请参考数据行删除方式实现'); } else { // 删除数据行 if (this.tableData.rows.length <= 1) { alert('至少需要保留一行数据'); return; } // 检查是否有合并到当前行的单元格 this.tableData.rows.forEach((row, rIndex) => { if (rIndex >= rowIndex) return; row.forEach(cell => { if (cell.rowspan > 1 && rIndex + cell.rowspan > rowIndex) { // 调整合并单元格的rowspan cell.rowspan--; } }); }); // 删除行 this.tableData.rows.splice(rowIndex, 1); } }, // 新增列 addColumn() { if (!this.currentCellInfo || !this.currentCellInfo.isHeader) return; const { colIndex } = this.currentCellInfo; // 更新表头 this.tableData.headers.forEach((headerRow) => { for (let i = 0; i < headerRow.length; i++) { const cell = headerRow[i]; // 如果当前单元格包含插入点,增加其colspan if (cell.colspan > 1 && i <= colIndex && i + cell.colspan > colIndex) { cell.colspan++; break; } // 如果到达插入位置,插入新单元格 if (i === colIndex) { headerRow.splice(i + 1, 0, { text: headerRow === this.tableData.headers[2] ? '新增' : '', colspan: 1, rowspan: 1, isHeader: true }); break; } } }); // 更新数据行 this.tableData.rows.forEach(row => { for (let i = 0; i < row.length; i++) { const cell = row[i]; // 如果当前单元格包含插入点,增加其colspan if (cell.colspan > 1 && i <= colIndex && i + cell.colspan > colIndex) { cell.colspan++; break; } // 如果到达插入位置,插入新单元格 if (i === colIndex) { row.splice(i + 1, 0, { text: '', colspan: 1, rowspan: 1, isHeader: cell.isHeader }); break; } } }); }, // 删除列 deleteColumn() { if (!this.currentCellInfo || !this.currentCellInfo.isHeader) return; const { colIndex } = this.currentCellInfo; // 检查是否可以删除列 const totalColumns = this.countTotalColumns(); if (totalColumns <= 2) { // 至少保留部门和业务线两列 alert('至少需要保留两列'); return; } // 更新表头 this.tableData.headers.forEach((headerRow) => { let currentCol = 0; let i = 0; while (i < headerRow.length) { const cell = headerRow[i]; // 如果当前单元格包含要删除的列 if (currentCol <= colIndex && currentCol + cell.colspan > colIndex) { // 减少colspan cell.colspan--; // 如果colspan变为0,删除单元格 if (cell.colspan === 0) { headerRow.splice(i, 1); continue; } } currentCol += cell.colspan; i++; } }); // 更新数据行 this.tableData.rows.forEach(row => { let currentCol = 0; let i = 0; while (i < row.length) { const cell = row[i]; // 如果当前单元格包含要删除的列 if (currentCol <= colIndex && currentCol + cell.colspan > colIndex) { // 减少colspan cell.colspan--; // 如果colspan变为0,删除单元格 if (cell.colspan === 0) { row.splice(i, 1); continue; } } currentCol += cell.colspan; i++; } }); }, // 计算总列数 countTotalColumns() { if (!this.tableData.headers.length) return 0; let totalColumns = 0; this.tableData.headers[0].forEach(cell => { totalColumns += cell.colspan; }); return totalColumns; }, /** * 转换表格行数据为包含合并单元格信息的二维数组 * @param {Object} rows 表格行数据,每行包含columns属性(单元格数组) * @returns {Array<Array>} 二维数组,每个位置填充对应的合并单元格信息 */ getAngelTable() { const rows = [...this.tableData.headers,...this.tableData.rows]; // 存储所有解析后的单元格信息 const resolvedCells = []; // 记录每行已被占用的列(key:行号,value:被占用的列号集合) const occupiedCols = {}; // 遍历每行,计算每个单元格的位置 for (let currentRow = 0; currentRow < rows.length; currentRow++) { const row = rows[currentRow]; if (!row) continue; let currentCol = 0; // 当前行的起始列(从0开始) for (const cell of row) { // 计算当前单元格的startCol:跳过已被占用的列 while (this.isColumnOccupied(currentRow, currentCol, occupiedCols)) { currentCol++; } // 单元格的基本信息 const colSpan = cell.colspan; const rowSpan = cell.rowspan; const startRow = currentRow; const startCol = currentCol; const endRow = startRow + rowSpan - 1; const endCol = startCol + colSpan - 1; // 记录单元格信息 resolvedCells.push({ startRow, startCol, endRow, endCol // 可根据需要添加原单元格的其他属性 }); // 标记当前单元格覆盖的所有行和列为“已占用” this.markOccupiedColumns(startRow, endRow, startCol, endCol, occupiedCols); // 移动到下一个未被占用的列 currentCol = endCol + 1; } } // 计算表格的总行数和总列数 const totalRows = resolvedCells.length ? Math.max(...resolvedCells.map(cell => cell.endRow)) + 1 : 0; const totalCols = resolvedCells.length ? Math.max(...resolvedCells.map(cell => cell.endCol)) + 1 : 0; // 初始化二维数组并填充null const flatTable = []; for (let i = 0; i < totalRows; i++) { flatTable.push(new Array(totalCols).fill(null)); } // 填充每个单元格到其覆盖的所有位置 for (const cell of resolvedCells) { for (let r = cell.startRow; r <= cell.endRow; r++) { for (let c = cell.startCol; c <= cell.endCol; c++) { flatTable[r][c] = cell; } } } return flatTable; }, /** * 检查当前行的当前列是否被占用 * @param {number} row 行号 * @param {number} col 列号 * @param {Object} occupiedCols 存储每行被占用列的对象 * @returns {boolean} 是否被占用 */ isColumnOccupied(row, col, occupiedCols) { const occupied = occupiedCols[row] || new Set(); return occupied.has(col); }, /** * 标记单元格覆盖的所有行和列为“已占用” * @param {number} startRow 起始行 * @param {number} endRow 结束行(含) * @param {number} startCol 起始列 * @param {number} endCol 结束列(含) * @param {Object} occupiedCols 存储每行被占用列的对象 */ markOccupiedColumns(startRow, endRow, startCol, endCol, occupiedCols) { for (let r = startRow; r <= endRow; r++) { // 为当前行初始化占用列集合 if (!occupiedCols[r]) { occupiedCols[r] = new Set(); } const cols = occupiedCols[r]; // 添加当前单元格覆盖的列 for (let c = startCol; c <= endCol; c++) { cols.add(c); } } } }, mounted() { // 点击其他地方关闭菜单 document.addEventListener('click', () => { this.showContextMenuFlag = false; this.currentCellInfo = null; }); } }; </script> <style> table { width: 100%; max-width: 1000px; margin: 20px auto; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; } /* 所有单元格显示边框 */ th, td { padding: 12px 15px; border: 1px solid #ccc; text-align: center; } /* 列表头(横向表头)样式 - 适用于任意数量的顶部表头行 */ th[scope="col"] { background-color: #4a90e2; /* 列表头蓝色 */ color: white; font-weight: bold; } /* 行表头(纵向表头)样式 - 适用于任意数量的左侧表头列 */ td[scope="row"] { background-color: #4a90e2; /* 行表头绿色 */ color: white; font-weight: bold; position: sticky; left: 0; z-index: 1; } /* 表头交叉区域(同时属于行列表头的单元格) */ th[scope="colgroup"] { background-color: #5cb85c; /* 交叉区域橙色 */ color: white; font-weight: bold; z-index: 2; } /* 响应式调整 */ @media screen and (max-width: 768px) { th, td { padding: 8px 10px; font-size: 13px; } } /* 上下文菜单样式 */ .context-menu { position: absolute; background-color: white; border: 1px solid #ccc; border-radius: 4px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); padding: 5px 0; z-index: 1000; } .context-menu-item { padding: 8px 15px; cursor: pointer; white-space: nowrap; } .context-menu-item:hover { background-color: #f5f5f5; } .context-menu-separator { height: 1px; background-color: #eee; margin: 5px 0; } .hidden { display: none; } .highlight-cell { background-color: #ffecb3 !important; } /* 上下文菜单容器 */ .context-menu { /* 核心:绝对定位,基于页面坐标定位 */ position: absolute; /* 确保菜单在最上层,避免被其他元素覆盖 */ z-index: 9999; /* 固定宽度,避免内容撑开导致位置偏移 */ width: 160px; /* 基础外观 */ background-color: #fff; border: 1px solid #e0e0e0; border-radius: 4px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15); /* 去除默认边距 */ margin: 0; padding: 4px 0; /* 避免选中菜单文本 */ user-select: none; } /* 菜单项样式 */ .context-menu-item { padding: 8px 16px; cursor: pointer; font-size: 14px; color: #333; /* hover 效果 */ transition: background-color 0.2s; } .context-menu-item:hover { background-color: #f5f5f5; } /* 菜单图标与文字间距 */ .context-menu-item i { width: 16px; text-align: center; } /* 分隔线样式 */ .context-menu-separator { height: 1px; margin: 4px 0; background-color: #e0e0e0; } /* 隐藏菜单的类(配合 Vue 状态控制) */ .hidden { display: none; } </style>
10-25
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值