<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>