图:
代码
<template>
<div class="mnlbb">
<!-- 操作按钮 -->
<div class="btn-group">
<div>
<el-dropdown @command="handleInsertRowCommand">
<el-button>插入行<i class="el-icon-arrow-down el-icon--right"></i></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="before">在当前行前插入</el-dropdown-item>
<el-dropdown-item command="after">在当前行后插入</el-dropdown-item>
<el-dropdown-item command="end">在末尾添加</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-dropdown @command="handleInsertColumnCommand">
<el-button style="margin: 0 10px">插入列<i class="el-icon-arrow-down el-icon--right"></i></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="before">在当前列前插入</el-dropdown-item>
<el-dropdown-item command="after">在当前列后插入</el-dropdown-item>
<el-dropdown-item command="end">在末尾添加</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-button @click="deleteSelectedRows" :disabled="selectedRows.length === 0">删除选中行</el-button>
<el-button @click="deleteSelectedColumns" :disabled="selectedColumns.length === 0">删除选中列</el-button>
<el-button @click="mergeSelectedCells" :disabled="!canMerge">合并单元格</el-button>
<el-button @click="unmergeCells" :disabled="!canUnmerge">取消合并</el-button>
</div>
<div>
<el-button type="primary" @click="saveTable">保存</el-button>
</div>
</div>
<!-- 表格 -->
<div class="table-container" ref="tableContainer">
<table border ref="table">
<thead>
<tr>
<th width="40"></th>
<th
v-for="(header, index) in tableHeaders"
:key="index"
:width="header.width || currentColumnWidth"
:height="currentRowHeight"
:data-col="index"
:style="{ minWidth: '50px' }"
>
<div class="header-cell" @dblclick="startEditingHeader(index)">
<el-checkbox
:value="selectedColumns.includes(index)"
@change="() => toggleColumnSelection(index)"
></el-checkbox>
<span style="margin-left: 5px">{{ header.name }}</span>
</div>
<div class="resize-handle" @mousedown="startResizeColumn($event, index)"></div>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(row, rowIndex) in tableData"
:key="rowIndex"
:height="row.height || currentRowHeight"
:data-row="rowIndex"
>
<td width="30" style="text-align: center">
<el-checkbox
:value="selectedRows.includes(rowIndex)"
@change="() => toggleRowSelection(rowIndex)"
></el-checkbox>
<div class="resize-handle vertical" @mousedown="startResizeRow($event, rowIndex)"></div>
</td>
<td
v-for="(cell, colIndex) in row"
:key="colIndex"
:width="tableHeaders[colIndex].width || currentColumnWidth"
:rowspan="cell.merged && cell.isMaster ? cell.rowspan : 1"
:colspan="cell.merged && cell.isMaster ? cell.colspan : 1"
:class="{
'merged-cell': cell.merged && !cell.isMaster,
'selected-cell': isSelectedCell(rowIndex, colIndex),
'selected-range': isInSelectedRange(rowIndex, colIndex),
'merged-master': cell.merged && cell.isMaster,
}"
style="text-align: center"
@dblclick="startEditingCell(rowIndex, colIndex)"
@mousedown="startSelection(rowIndex, colIndex, $event)"
@mouseover="extendSelection(rowIndex, colIndex, $event)"
>
<template v-if="!cell.merged || cell.isMaster">
<span v-if="!isEditingCell(rowIndex, colIndex)">{{ cell.name }}</span>
<div v-if="isEditingCell(rowIndex, colIndex)" style="display: flex; width: 100%">
<el-input
type="textarea"
autosize
:value="cell.name"
@input="updateCellValue(rowIndex, colIndex, $event)"
@blur="stopEditingCell(rowIndex, colIndex)"
@keyup.enter.native="stopEditingCell(rowIndex, colIndex)"
size="mini"
autofocus
style="flex: 1"
class="zdyCenter"
></el-input>
<i
class="el-icon-more"
@click="handleVariableClick('tableData', rowIndex, colIndex)"
style="margin-left: 5px; line-height: 28px"
></i>
</div>
</template>
<template v-else>
<!-- 显示合并单元格的内容 -->
<span>{{ cell.name }}</span>
</template>
</td>
</tr>
</tbody>
</table>
</div>
<VariableSelectDialog :isShow.sync="isShowVarDialog" @confirm="handleVarConfirm" />
</div>
</template>
<script>
import VariableSelectDialog from '../../CustomDialog/VariableSelectDialog.vue'
import { reportFormSave, reportFormList } from '@/api/menuLeft'
export default {
components: {
VariableSelectDialog,
},
data() {
return {
tableData: this.generateDefaultTableData(),
tableHeaders: this.generateDefaultTableHeaders(),
currentColumnWidth: 150,
currentRowHeight: 40,
editingHeaderIndex: null,
editingCell: null,
selectedRows: [],
selectedColumns: [],
allRowsSelected: false,
allColumnsSelected: false,
selectIndex1: 0,
selectIndex2: 0,
selectType: '',
isShowVarDialog: false,
selectionStart: null,
selectionEnd: null,
isResizing: false,
resizingColumn: null,
resizingRow: null,
startX: 0,
startY: 0,
startWidth: 0,
startHeight: 0,
currentPage: {},
details: {},
}
},
computed: {
canMerge() {
if (!this.selectionStart || !this.selectionEnd) return false
const [startRow, startCol] = this.selectionStart
const [endRow, endCol] = this.selectionEnd
return (
(startRow !== endRow || startCol !== endCol)
&& !this.isInMergedArea(startRow, startCol)
&& !this.isInMergedArea(endRow, endCol)
)
},
canUnmerge() {
if (!this.selectionStart || !this.selectionEnd) return false
const [row, col] = this.selectionStart
return this.tableData[row] && this.tableData[row][col] && this.tableData[row][col].merged
},
selectedRange() {
if (!this.selectionStart || !this.selectionEnd) return null
const [startRow, startCol] = this.selectionStart
const [endRow, endCol] = this.selectionEnd
return {
startRow: Math.min(startRow, endRow),
endRow: Math.max(startRow, endRow),
startCol: Math.min(startCol, endCol),
endCol: Math.max(startCol, endCol),
}
},
},
created() {
this.currentPage = JSON.parse(localStorage.getItem('pageData'))
if (this.currentPage.report_type == 12) {
this.getreportFormList()
}
console.log('自定义表格组件创建', this.currentPage)
},
methods: {
getreportFormList() {
reportFormList({ page_id: this.currentPage.id })
.then((res) => {
if (res.code === 0) {
this.details = res.data.list.length > 0 ? res.data.list[0] : {}
this.tableData = JSON.parse(this.details.tableData || this.generateDefaultTableData())
this.tableHeaders = JSON.parse(this.details.tableHeaders || this.generateDefaultTableHeaders())
}
})
.catch((error) => {
console.error('获取报表列表失败:', error)
})
},
updateCellValue(rowIndex, colIndex, value) {
// 确保使用Vue.set来更新响应式数据
this.$set(this.tableData[rowIndex][colIndex], 'name', value)
// 如果是合并单元格的主单元格,更新合并区域的所有单元格
if (this.tableData[rowIndex][colIndex].merged && this.tableData[rowIndex][colIndex].isMaster) {
const cell = this.tableData[rowIndex][colIndex]
for (let r = rowIndex; r < rowIndex + cell.rowspan; r++) {
for (let c = colIndex; c < colIndex + cell.colspan; c++) {
if (r === rowIndex && c === colIndex) continue
this.$set(this.tableData[r][c], 'name', value)
}
}
}
// 强制更新视图
this.$forceUpdate()
},
// 生成Excel风格的列名 (A, B, ..., Z, AA, AB, ...)
generateColumnName(index) {
let columnName = ''
index++ // 从1开始而不是0
while (index > 0) {
const remainder = (index - 1) % 26
columnName = String.fromCharCode(65 + remainder) + columnName
index = Math.floor((index - 1) / 26)
}
return columnName
},
// 更新表头序号
updateHeaderNames() {
this.tableHeaders.forEach((header, index) => {
header.name = this.generateColumnName(index)
})
},
isSelectedCell(row, col) {
return this.selectionStart && this.selectionStart[0] === row && this.selectionStart[1] === col
},
isInSelectedRange(row, col) {
if (!this.selectedRange) return false
const { startRow, endRow, startCol, endCol } = this.selectedRange
return row >= startRow && row <= endRow && col >= startCol && col <= endCol
},
generateDefaultTableData() {
const data = []
for (let i = 0; i < 10; i++) {
const row = []
for (let j = 0; j < 8; j++) {
row.push({
name: '',
merged: false,
isMaster: false,
rowspan: 1,
colspan: 1,
})
}
data.push(row)
}
return data
},
generateDefaultTableHeaders() {
const headers = []
for (let i = 0; i < 8; i++) {
headers.push({
name: this.generateColumnName(i),
width: 150,
})
}
return headers
},
isEditingCell(rowIndex, colIndex) {
return this.editingCell && this.editingCell[0] === rowIndex && this.editingCell[1] === colIndex
},
startEditingHeader(index) {
this.editingHeaderIndex = index
},
stopEditingHeader(index) {
this.editingHeaderIndex = null
},
startEditingCell(rowIndex, colIndex) {
if (this.tableData[rowIndex][colIndex].merged && !this.tableData[rowIndex][colIndex].isMaster) {
return
}
this.editingCell = [rowIndex, colIndex]
},
stopEditingCell(rowIndex, colIndex) {
this.editingCell = null
// 确保数据同步
this.$forceUpdate()
},
startSelection(rowIndex, colIndex, event) {
if (this.tableData[rowIndex][colIndex].merged && !this.tableData[rowIndex][colIndex].isMaster) {
return
}
if (event.shiftKey && this.selectionStart) {
this.selectionEnd = [rowIndex, colIndex]
return
}
this.selectionStart = [rowIndex, colIndex]
this.selectionEnd = [rowIndex, colIndex]
},
extendSelection(rowIndex, colIndex, event) {
if (!this.selectionStart || !event.buttons) return
if (this.tableData[rowIndex][colIndex].merged && !this.tableData[rowIndex][colIndex].isMaster) {
return
}
this.selectionEnd = [rowIndex, colIndex]
},
isInMergedArea(row, col) {
for (let r = 0; r < this.tableData.length; r++) {
for (let c = 0; c < this.tableData[r].length; c++) {
const cell = this.tableData[r][c]
if (cell.merged && cell.isMaster) {
if (row >= r && row < r + cell.rowspan && col >= c && col < c + cell.colspan) {
return true
}
}
}
}
return false
},
mergeSelectedCells() {
if (!this.selectedRange) return
const { startRow, endRow, startCol, endCol } = this.selectedRange
// 计算合并的行列数
const rowspan = endRow - startRow + 1
const colspan = endCol - startCol + 1
// 设置主单元格
this.tableData[startRow][startCol] = {
...this.tableData[startRow][startCol],
merged: true,
isMaster: true,
rowspan,
colspan,
}
// 标记其他单元格为合并状态
for (let r = startRow; r <= endRow; r++) {
for (let c = startCol; c <= endCol; c++) {
if (r === startRow && c === startCol) continue
this.tableData[r][c] = {
...this.tableData[r][c],
merged: true,
isMaster: false,
rowspan: 1,
colspan: 1,
}
}
}
this.clearSelection()
},
unmergeCells() {
if (!this.selectedRange) return
const [row, col] = this.selectionStart
const cell = this.tableData[row][col]
if (!cell.merged) return
const rowspan = cell.rowspan
const colspan = cell.colspan
// 恢复所有单元格
for (let r = row; r < row + rowspan; r++) {
for (let c = col; c < col + colspan; c++) {
this.tableData[r][c] = {
...this.tableData[r][c],
name: r === row && c === col ? cell.name : '',
merged: false,
isMaster: false,
rowspan: 1,
colspan: 1,
}
}
}
this.clearSelection()
},
clearSelection() {
this.selectionStart = null
this.selectionEnd = null
},
startResizeColumn(e, colIndex) {
e.preventDefault()
e.stopPropagation()
this.isResizing = true
this.resizingColumn = colIndex
this.startX = e.clientX
this.startWidth = this.tableHeaders[colIndex].width || this.currentColumnWidth
document.addEventListener('mousemove', this.handleColumnResize)
document.addEventListener('mouseup', this.stopResize)
},
handleColumnResize(e) {
if (!this.isResizing || this.resizingColumn === null) return
const delta = e.clientX - this.startX
const newWidth = Math.max(50, this.startWidth + delta)
this.tableHeaders[this.resizingColumn].width = newWidth
},
startResizeRow(e, rowIndex) {
e.preventDefault()
e.stopPropagation()
this.isResizing = true
this.resizingRow = rowIndex
this.startY = e.clientY
this.startHeight = this.tableData[rowIndex].height || this.currentRowHeight
document.addEventListener('mousemove', this.handleRowResize)
document.addEventListener('mouseup', this.stopResize)
},
handleRowResize(e) {
if (!this.isResizing || this.resizingRow === null) return
const delta = e.clientY - this.startY
const newHeight = Math.max(30, this.startHeight + delta)
this.$set(this.tableData[this.resizingRow], 'height', newHeight)
},
stopResize() {
this.isResizing = false
this.resizingColumn = null
this.resizingRow = null
document.removeEventListener('mousemove', this.handleColumnResize)
document.removeEventListener('mousemove', this.handleRowResize)
document.removeEventListener('mouseup', this.stopResize)
},
handleInsertRowCommand(command) {
if (command !== 'end' && this.selectedRows.length === 0) {
this.$message.warning('请先选中要插入位置的行')
return
}
const refRowIndex = command === 'end' ? null : this.selectedRows[0]
this.insertRow(command, refRowIndex)
},
handleInsertColumnCommand(command) {
if (command !== 'end' && this.selectedColumns.length === 0) {
this.$message.warning('请先选中要插入位置的列')
return
}
const refColIndex = command === 'end' ? null : this.selectedColumns[0]
this.insertColumn(command, refColIndex)
},
toggleRowSelection(rowIndex) {
const index = this.selectedRows.indexOf(rowIndex)
if (index === -1) {
this.selectedRows.push(rowIndex)
} else {
this.selectedRows.splice(index, 1)
}
this.updateAllRowsSelected()
},
toggleColumnSelection(colIndex) {
const index = this.selectedColumns.indexOf(colIndex)
if (index === -1) {
this.selectedColumns.push(colIndex)
} else {
this.selectedColumns.splice(index, 1)
}
this.updateAllColumnsSelected()
},
saveTable() {
console.log(this.tableData, this.tableHeaders, '保存数据====')
const content = {
page_id: this.currentPage.id,
tableData: JSON.stringify(this.tableData),
tableHeaders: JSON.stringify(this.tableHeaders),
id: this.details.id || 0,
}
reportFormSave(content)
.then((res) => {
if (res.code === 0) {
this.currentPage = JSON.parse(localStorage.getItem('pageData'))
this.$message.success('保存成功')
this.getreportFormList()
}
})
.catch((error) => {
console.error('保存失败:', error)
})
},
handleVariableClick(type, index1, index2) {
this.selectType = type
this.selectIndex1 = index1
this.selectIndex2 = index2
this.isShowVarDialog = true
},
handleVarConfirm(addr, fieldName) {
if (addr == '') {
this.$message.warning('请选择变量')
return
}
if (this.selectType == 'tableHeaders') {
this.tableHeaders[this.selectIndex1].name = fieldName
this.tableHeaders[this.selectIndex1].addr = addr
} else if (this.selectType == 'tableData') {
this.tableData[this.selectIndex1][this.selectIndex2].name = fieldName
this.tableData[this.selectIndex1][this.selectIndex2].addr = addr
} else {
console.log('选择类型错误')
}
},
insertRow(position = 'end', refRowIndex = null) {
const newRow = []
for (let i = 0; i < this.tableHeaders.length; i++) {
newRow.push({
name: '',
merged: false,
isMaster: false,
rowspan: 1,
colspan: 1,
})
}
if (position === 'end' || refRowIndex === null) {
this.tableData.push(newRow)
} else if (position === 'before') {
this.tableData.splice(refRowIndex, 0, newRow)
} else if (position === 'after') {
this.tableData.splice(refRowIndex + 1, 0, newRow)
}
},
insertColumn(position = 'end', refColIndex = null) {
const newHeader = {
name: this.generateColumnName(this.tableHeaders.length),
width: this.currentColumnWidth,
}
if (position === 'end' || refColIndex === null) {
this.tableHeaders.push(newHeader)
this.tableData.forEach((row) =>
row.push({
name: '',
merged: false,
isMaster: false,
rowspan: 1,
colspan: 1,
}),
)
} else if (position === 'before') {
this.tableHeaders.splice(refColIndex, 0, newHeader)
this.tableData.forEach((row) =>
row.splice(refColIndex, 0, {
name: '',
merged: false,
isMaster: false,
rowspan: 1,
colspan: 1,
}),
)
} else if (position === 'after') {
this.tableHeaders.splice(refColIndex + 1, 0, newHeader)
this.tableData.forEach((row) =>
row.splice(refColIndex + 1, 0, {
name: '',
merged: false,
isMaster: false,
rowspan: 1,
colspan: 1,
}),
)
}
// 更新所有表头名称
this.updateHeaderNames()
},
unmergeCell(row, col) {
const cell = this.tableData[row][col]
const rowspan = cell.rowspan
const colspan = cell.colspan
for (let r = row; r < row + rowspan; r++) {
for (let c = col; c < col + colspan; c++) {
this.tableData[r][c] = {
...this.tableData[r][c],
name: r === row && c === col ? cell.name : '',
merged: false,
isMaster: false,
rowspan: 1,
colspan: 1,
}
}
}
},
deleteSelectedRows() {
this.selectedRows
.sort((a, b) => b - a)
.forEach((rowIndex) => {
// 检查要删除的行是否在某个合并区域内
for (let r = 0; r < this.tableData.length; r++) {
for (let c = 0; c < this.tableData[r].length; c++) {
const cell = this.tableData[r][c]
if (cell.merged && cell.isMaster) {
// 如果删除的行在合并区域内
if (rowIndex >= r && rowIndex < r + cell.rowspan) {
// 调整合并区域的行数
cell.rowspan -= 1
// 如果合并区域行数变为1,自动取消合并
if (cell.rowspan === 1) {
this.unmergeCell(r, c)
}
}
// 如果删除的行在合并区域上方,调整合并区域起始行
else if (rowIndex < r) {
this.$set(this.tableData[r][c], 'rowspan', cell.rowspan)
this.$set(this.tableData[r - 1][c], 'rowspan', cell.rowspan)
this.tableData[r - 1][c] = { ...cell }
this.tableData[r][c] = {
name: '',
merged: false,
isMaster: false,
rowspan: 1,
colspan: 1,
}
}
}
}
}
// 删除行
this.tableData.splice(rowIndex, 1)
})
this.selectedRows = []
},
deleteSelectedColumns() {
this.selectedColumns
.sort((a, b) => b - a)
.forEach((colIndex) => {
// 检查要删除的列是否在某个合并区域内
for (let r = 0; r < this.tableData.length; r++) {
for (let c = 0; c < this.tableData[r].length; c++) {
const cell = this.tableData[r][c]
if (cell.merged && cell.isMaster) {
// 如果删除的列在合并区域内
if (colIndex >= c && colIndex < c + cell.colspan) {
// 如果删除的是主列,需要重新指定主单元格
if (colIndex === c) {
// 找到合并区域内第一个未删除的列作为新的主列
let newMasterCol = c + 1
while (newMasterCol < c + cell.colspan && this.selectedColumns.includes(newMasterCol)) {
newMasterCol++
}
if (newMasterCol < c + cell.colspan) {
// 将新列设为主单元格
this.tableData[r][newMasterCol] = {
...this.tableData[r][newMasterCol],
merged: true,
isMaster: true,
rowspan: cell.rowspan,
colspan: cell.colspan - 1,
}
// 标记其他单元格为合并状态
for (let mr = r; mr < r + cell.rowspan; mr++) {
for (let mc = c; mc < c + cell.colspan; mc++) {
if (mc === newMasterCol) continue
this.tableData[mr][mc] = {
...this.tableData[mr][mc],
merged: true,
isMaster: false,
rowspan: 1,
colspan: 1,
}
}
}
}
} else {
// 调整合并区域的列数
cell.colspan -= 1
// 如果合并区域列数变为1,自动取消合并
if (cell.colspan === 1) {
this.unmergeCell(r, c)
}
}
}
// 如果删除的列在合并区域左侧,调整合并区域起始列
else if (colIndex < c) {
this.$set(this.tableData[r][c], 'colspan', cell.colspan)
this.$set(this.tableData[r][c - 1], 'colspan', cell.colspan)
this.tableData[r][c - 1] = { ...cell }
this.tableData[r][c] = {
name: '',
merged: false,
isMaster: false,
rowspan: 1,
colspan: 1,
}
}
}
}
}
// 删除表头
this.tableHeaders.splice(colIndex, 1)
// 删除每行中对应的列数据
this.tableData.forEach((row) => {
row.splice(colIndex, 1)
})
// 更新表头名称
this.updateHeaderNames()
})
this.selectedColumns = []
},
toggleAllColumns(checked) {
this.selectedColumns = checked ? [...Array(this.tableHeaders.length).keys()] : []
},
updateAllColumnsSelected() {
this.allColumnsSelected = this.selectedColumns.length === this.tableHeaders.length
},
toggleAllRows(checked) {
this.selectedRows = checked ? [...Array(this.tableData.length).keys()] : []
},
updateAllRowsSelected() {
this.allRowsSelected = this.selectedRows.length === this.tableData.length
},
},
}
</script>
<style lang="scss" scoped>
.mnlbb {
margin: 10px;
height: calc(100vh - 120px);
overflow: auto;
}
.table-container {
margin-top: 10px;
height: calc(100% - 60px);
overflow: auto;
table {
border-collapse: collapse;
width: 100%;
table-layout: fixed;
th,
td {
padding: 5px;
border: 1px solid #ddd;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
background-color: #f5f7fa;
cursor: pointer;
}
}
th {
background-color: #f8f8f8;
font-weight: bold;
text-align: center;
}
.header-cell {
display: flex;
justify-content: center;
.el-checkbox {
margin-bottom: 5px;
}
.el-input {
width: 100%;
::v-deep .el-input__inner {
padding: 0 5px;
height: 28px;
line-height: 28px;
}
}
}
.merged-cell {
display: none;
}
.merged-master {
background-color: #f0f9eb;
border: 2px solid #67c23a !important;
}
.selected-cell {
background-color: #ecf5ff !important;
border: 2px solid #409eff !important;
}
.selected-range {
background-color: #d9ecff !important;
}
.resize-handle {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 5px;
background: transparent;
cursor: col-resize;
&.vertical {
right: auto;
bottom: 0;
left: 0;
top: auto;
width: 100%;
height: 5px;
cursor: row-resize;
}
&:hover {
background-color: #409eff;
}
}
}
}
.btn-group {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.el-checkbox {
margin-right: 0;
}
</style>
<style>
.zdyCenter .el-textarea__inner {
text-align: center !important;
}
</style>