You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
691 lines
22 KiB
691 lines
22 KiB
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>数据卡</title>
|
|
<link rel="stylesheet" href="./element-ui.css">
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
background: #fafafa;
|
|
}
|
|
|
|
#app {
|
|
width: 100%;
|
|
height: 100vh;
|
|
}
|
|
|
|
.data-card-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
background: #fafafa;
|
|
}
|
|
|
|
.data-card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0 32px;
|
|
height: 56px;
|
|
background: #fff;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.back-btn {
|
|
padding: 8px 12px;
|
|
font-size: 14px;
|
|
color: #666;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.back-btn:hover {
|
|
color: #409EFF;
|
|
}
|
|
|
|
.data-card-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.data-card-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 16px 32px;
|
|
background: #fff;
|
|
border-bottom: 1px solid #e8e8e8;
|
|
}
|
|
|
|
.toolbar-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.toolbar-label {
|
|
font-size: 14px;
|
|
color: #666;
|
|
}
|
|
|
|
.data-card-content {
|
|
flex: 1;
|
|
padding: 24px 32px;
|
|
overflow: auto;
|
|
}
|
|
|
|
.table-container {
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
padding: 24px;
|
|
display: inline-block;
|
|
}
|
|
|
|
.data-table {
|
|
border-collapse: collapse;
|
|
table-layout: fixed;
|
|
}
|
|
|
|
.data-table td {
|
|
border: 1px solid #ddd;
|
|
min-width: 60px;
|
|
height: 30px;
|
|
position: relative;
|
|
cursor: cell;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.data-table td:hover {
|
|
background-color: #f5f7fa;
|
|
}
|
|
|
|
.data-table td.selected {
|
|
background-color: #ecf5ff;
|
|
border-color: #409EFF;
|
|
}
|
|
|
|
.resize-handle-col {
|
|
position: absolute;
|
|
right: 0;
|
|
top: 0;
|
|
width: 5px;
|
|
height: 100%;
|
|
cursor: col-resize;
|
|
background: transparent;
|
|
z-index: 10;
|
|
}
|
|
|
|
.resize-handle-col:hover {
|
|
background: #409EFF;
|
|
}
|
|
|
|
.resize-handle-row {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 5px;
|
|
cursor: row-resize;
|
|
background: transparent;
|
|
z-index: 10;
|
|
}
|
|
|
|
.resize-handle-row:hover {
|
|
background: #409EFF;
|
|
}
|
|
|
|
.cell-input {
|
|
width: 100%;
|
|
height: 100%;
|
|
border: none;
|
|
outline: none;
|
|
padding: 4px 8px;
|
|
font-size: 14px;
|
|
background: transparent;
|
|
}
|
|
|
|
.color-picker-panel {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
width: 180px;
|
|
}
|
|
|
|
.color-option {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
border: 2px solid transparent;
|
|
}
|
|
|
|
.color-option:hover {
|
|
border-color: #409EFF;
|
|
}
|
|
|
|
.color-option.selected {
|
|
border-color: #409EFF;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<div class="data-card-container">
|
|
<div class="data-card-header">
|
|
<div class="header-left">
|
|
<div class="back-btn" @click="goBack">
|
|
<i class="el-icon-arrow-left"></i>
|
|
返回
|
|
</div>
|
|
<div class="data-card-title">数据卡</div>
|
|
</div>
|
|
<div class="header-right">
|
|
<el-button type="primary" size="small" @click="exportImage">
|
|
<i class="el-icon-picture-outline"></i>
|
|
导出图片
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="data-card-toolbar">
|
|
<div class="toolbar-group">
|
|
<span class="toolbar-label">行数:</span>
|
|
<el-input-number v-model="rows" :min="1" :max="50" size="small" @change="updateTableSize"></el-input-number>
|
|
</div>
|
|
<div class="toolbar-group">
|
|
<span class="toolbar-label">列数:</span>
|
|
<el-input-number v-model="cols" :min="1" :max="20" size="small" @change="updateTableSize"></el-input-number>
|
|
</div>
|
|
<el-divider direction="vertical"></el-divider>
|
|
<div class="toolbar-group">
|
|
<span class="toolbar-label">单元格背景色:</span>
|
|
<el-popover
|
|
placement="bottom"
|
|
width="200"
|
|
trigger="click"
|
|
v-model="colorPopoverVisible">
|
|
<div class="color-picker-panel">
|
|
<div
|
|
v-for="color in colors"
|
|
:key="color"
|
|
class="color-option"
|
|
:style="{ backgroundColor: color }"
|
|
:class="{ selected: selectedColor === color }"
|
|
@click="selectColor(color)">
|
|
</div>
|
|
</div>
|
|
<el-button slot="reference" size="small">
|
|
<span :style="{ display: 'inline-block', width: '16px', height: '16px', backgroundColor: selectedColor, marginRight: '8px', verticalAlign: 'middle', borderRadius: '2px' }"></span>
|
|
选择颜色
|
|
</el-button>
|
|
</el-popover>
|
|
</div>
|
|
<el-divider direction="vertical"></el-divider>
|
|
<div class="toolbar-group">
|
|
<el-button size="small" @click="undo">撤回</el-button>
|
|
<el-button size="small" @click="mergeCells">合并单元格</el-button>
|
|
<el-button type="primary" size="small" @click="saveTable">保存</el-button>
|
|
<el-button type="danger" size="small" @click="clearCell">清空选中</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="data-card-content">
|
|
<div class="table-container" ref="tableContainer">
|
|
<table class="data-table" ref="dataTable">
|
|
<tbody>
|
|
<tr v-for="(row, rowIndex) in tableData" :key="rowIndex">
|
|
<td
|
|
v-for="(cell, colIndex) in row"
|
|
:key="colIndex"
|
|
:class="{ selected: selectedCells.some(c => c.row === rowIndex && c.col === colIndex) }"
|
|
:rowspan="getRowspan(rowIndex, colIndex)"
|
|
:colspan="getColspan(rowIndex, colIndex)"
|
|
:style="getCellStyle(rowIndex, colIndex)"
|
|
@click="editCell(rowIndex, colIndex)"
|
|
@mousedown="startSelect($event, rowIndex, colIndex)"
|
|
@mouseover="continueSelect($event, rowIndex, colIndex)"
|
|
@mouseup="endSelect">
|
|
<div class="resize-handle-col" @mousedown.stop="startResizeCol($event, colIndex)"></div>
|
|
<div class="resize-handle-row" @mousedown.stop="startResizeRow($event, rowIndex)"></div>
|
|
<span v-if="!editingCell || editingCell.row !== rowIndex || editingCell.col !== colIndex">{{ cell.content }}</span>
|
|
<input
|
|
v-else
|
|
class="cell-input"
|
|
v-model="tableData[rowIndex][colIndex].content"
|
|
@blur="finishEditing"
|
|
@keyup.enter="finishEditing"
|
|
ref="cellInput"
|
|
autofocus>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://unpkg.com/vue@2.6.12/dist/vue.js"></script>
|
|
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
|
<script src="https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
|
<script>
|
|
new Vue({
|
|
el: '#app',
|
|
data() {
|
|
return {
|
|
rows: 10,
|
|
cols: 8,
|
|
tableData: [],
|
|
cellWidths: [],
|
|
cellHeights: [],
|
|
selectedCells: [],
|
|
isSelecting: false,
|
|
startCell: null,
|
|
editingCell: null,
|
|
selectedColor: '#ffffff',
|
|
colorPopoverVisible: false,
|
|
colors: [
|
|
'#ffffff', '#f5f7fa', '#e6a23c', '#67c23a', '#409eff',
|
|
'#909399', '#f0f9eb', '#fdf6ec', '#ecf5ff', '#f4f4f5',
|
|
'#ff6b6b', '#51cf66', '#339af0', '#cc5de8', '#ff922b',
|
|
'#ffd43b', '#20c997', '#22b8cf', '#748ffc', '#be4bdb'
|
|
],
|
|
resizingCol: null,
|
|
resizingRow: null,
|
|
startX: 0,
|
|
startY: 0,
|
|
startWidth: 0,
|
|
startHeight: 0,
|
|
mergedCells: [],
|
|
history: [],
|
|
historyIndex: -1
|
|
};
|
|
},
|
|
mounted() {
|
|
this.loadTable();
|
|
if (this.tableData.length === 0) {
|
|
this.generateTable();
|
|
// 初始化历史记录
|
|
this.saveState();
|
|
}
|
|
document.addEventListener('mouseup', this.endSelect);
|
|
document.addEventListener('mousemove', this.handleResize);
|
|
document.addEventListener('mouseup', this.endResize);
|
|
},
|
|
beforeDestroy() {
|
|
document.removeEventListener('mouseup', this.endSelect);
|
|
document.removeEventListener('mousemove', this.handleResize);
|
|
document.removeEventListener('mouseup', this.endResize);
|
|
},
|
|
methods: {
|
|
goBack() {
|
|
window.close();
|
|
},
|
|
generateTable() {
|
|
this.tableData = [];
|
|
this.cellWidths = [];
|
|
this.cellHeights = [];
|
|
this.mergedCells = [];
|
|
for (let i = 0; i < this.rows; i++) {
|
|
const row = [];
|
|
for (let j = 0; j < this.cols; j++) {
|
|
row.push({ content: '', bgColor: '#ffffff' });
|
|
}
|
|
this.tableData.push(row);
|
|
this.cellHeights.push(30);
|
|
}
|
|
for (let j = 0; j < this.cols; j++) {
|
|
this.cellWidths.push(100);
|
|
}
|
|
this.selectedCells = [];
|
|
// 重置历史记录
|
|
this.history = [];
|
|
this.historyIndex = -1;
|
|
},
|
|
|
|
updateTableSize() {
|
|
const oldRows = this.tableData.length;
|
|
const oldCols = oldRows > 0 ? this.tableData[0].length : 0;
|
|
|
|
if (this.rows !== oldRows || this.cols !== oldCols) {
|
|
// 保存当前状态
|
|
this.saveState();
|
|
|
|
// 处理行的增减
|
|
if (this.rows > oldRows) {
|
|
// 增加行
|
|
for (let i = oldRows; i < this.rows; i++) {
|
|
const row = [];
|
|
for (let j = 0; j < this.cols; j++) {
|
|
row.push({ content: '', bgColor: '#ffffff' });
|
|
}
|
|
this.tableData.push(row);
|
|
this.cellHeights.push(30);
|
|
}
|
|
} else if (this.rows < oldRows) {
|
|
// 减少行
|
|
this.tableData.splice(this.rows);
|
|
this.cellHeights.splice(this.rows);
|
|
}
|
|
|
|
// 处理列的增减
|
|
if (this.cols > oldCols) {
|
|
// 增加列
|
|
for (let i = 0; i < this.tableData.length; i++) {
|
|
for (let j = oldCols; j < this.cols; j++) {
|
|
this.tableData[i].push({ content: '', bgColor: '#ffffff' });
|
|
}
|
|
}
|
|
for (let j = oldCols; j < this.cols; j++) {
|
|
this.cellWidths.push(100);
|
|
}
|
|
} else if (this.cols < oldCols) {
|
|
// 减少列
|
|
for (let i = 0; i < this.tableData.length; i++) {
|
|
this.tableData[i].splice(this.cols);
|
|
}
|
|
this.cellWidths.splice(this.cols);
|
|
}
|
|
}
|
|
},
|
|
|
|
mergeCells() {
|
|
if (this.selectedCells.length < 2) {
|
|
this.$message.warning('请至少选择两个单元格进行合并');
|
|
return;
|
|
}
|
|
|
|
// 保存当前状态
|
|
this.saveState();
|
|
|
|
// 计算合并区域的最小和最大行、列
|
|
const minRow = Math.min(...this.selectedCells.map(c => c.row));
|
|
const maxRow = Math.max(...this.selectedCells.map(c => c.row));
|
|
const minCol = Math.min(...this.selectedCells.map(c => c.col));
|
|
const maxCol = Math.max(...this.selectedCells.map(c => c.col));
|
|
|
|
// 获取左上角单元格的内容和背景色
|
|
const firstCell = this.tableData[minRow][minCol];
|
|
const content = firstCell.content;
|
|
const bgColor = firstCell.bgColor;
|
|
|
|
// 清除其他单元格的内容和背景色
|
|
for (let r = minRow; r <= maxRow; r++) {
|
|
for (let c = minCol; c <= maxCol; c++) {
|
|
if (r !== minRow || c !== minCol) {
|
|
this.tableData[r][c].content = '';
|
|
this.tableData[r][c].bgColor = '#ffffff';
|
|
}
|
|
}
|
|
}
|
|
|
|
// 记录合并信息
|
|
this.mergedCells.push({
|
|
startRow: minRow,
|
|
startCol: minCol,
|
|
endRow: maxRow,
|
|
endCol: maxCol
|
|
});
|
|
|
|
this.$message.success('单元格合并成功');
|
|
},
|
|
|
|
saveTable() {
|
|
const tableData = {
|
|
rows: this.rows,
|
|
cols: this.cols,
|
|
tableData: this.tableData,
|
|
cellWidths: this.cellWidths,
|
|
cellHeights: this.cellHeights,
|
|
mergedCells: this.mergedCells
|
|
};
|
|
localStorage.setItem('dataCardTable', JSON.stringify(tableData));
|
|
this.$message.success('表格保存成功!');
|
|
},
|
|
|
|
loadTable() {
|
|
const savedData = localStorage.getItem('dataCardTable');
|
|
if (savedData) {
|
|
try {
|
|
const data = JSON.parse(savedData);
|
|
this.rows = data.rows;
|
|
this.cols = data.cols;
|
|
this.tableData = data.tableData;
|
|
this.cellWidths = data.cellWidths;
|
|
this.cellHeights = data.cellHeights;
|
|
this.mergedCells = data.mergedCells || [];
|
|
this.$message.success('表格加载成功!');
|
|
// 初始化历史记录
|
|
this.saveState();
|
|
} catch (e) {
|
|
console.error('加载表格失败:', e);
|
|
}
|
|
}
|
|
},
|
|
|
|
saveState() {
|
|
// 保存当前状态到历史记录
|
|
const state = {
|
|
rows: this.rows,
|
|
cols: this.cols,
|
|
tableData: JSON.parse(JSON.stringify(this.tableData)),
|
|
cellWidths: [...this.cellWidths],
|
|
cellHeights: [...this.cellHeights],
|
|
mergedCells: JSON.parse(JSON.stringify(this.mergedCells))
|
|
};
|
|
|
|
// 如果当前不是在历史记录的最后,删除后面的历史记录
|
|
if (this.historyIndex < this.history.length - 1) {
|
|
this.history = this.history.slice(0, this.historyIndex + 1);
|
|
}
|
|
|
|
// 添加新状态到历史记录
|
|
this.history.push(state);
|
|
|
|
// 限制历史记录长度,防止内存占用过大
|
|
if (this.history.length > 50) {
|
|
this.history.shift();
|
|
// 调整历史记录索引
|
|
if (this.historyIndex > 0) {
|
|
this.historyIndex--;
|
|
}
|
|
} else {
|
|
this.historyIndex++;
|
|
}
|
|
},
|
|
|
|
undo() {
|
|
if (this.historyIndex > 0) {
|
|
this.historyIndex--;
|
|
const state = this.history[this.historyIndex];
|
|
this.rows = state.rows;
|
|
this.cols = state.cols;
|
|
this.tableData = state.tableData;
|
|
this.cellWidths = state.cellWidths;
|
|
this.cellHeights = state.cellHeights;
|
|
this.mergedCells = state.mergedCells;
|
|
this.$message.success('操作已撤回');
|
|
} else {
|
|
this.$message.info('没有可撤回的操作');
|
|
}
|
|
},
|
|
|
|
startSelect(event, row, col) {
|
|
if (event.button !== 0) return;
|
|
// 只有在按下鼠标并移动时才开始选择
|
|
this.isSelecting = true;
|
|
this.startCell = { row, col };
|
|
this.selectedCells = [{ row, col }];
|
|
},
|
|
continueSelect(event, row, col) {
|
|
if (!this.isSelecting || !this.startCell) return;
|
|
const minRow = Math.min(this.startCell.row, row);
|
|
const maxRow = Math.max(this.startCell.row, row);
|
|
const minCol = Math.min(this.startCell.col, col);
|
|
const maxCol = Math.max(this.startCell.col, col);
|
|
this.selectedCells = [];
|
|
for (let r = minRow; r <= maxRow; r++) {
|
|
for (let c = minCol; c <= maxCol; c++) {
|
|
this.selectedCells.push({ row: r, col: c });
|
|
}
|
|
}
|
|
},
|
|
endSelect() {
|
|
this.isSelecting = false;
|
|
},
|
|
editCell(row, col) {
|
|
// 停止任何正在进行的选择
|
|
this.isSelecting = false;
|
|
this.selectedCells = [{ row, col }];
|
|
|
|
// 进入编辑模式
|
|
this.editingCell = { row, col };
|
|
this.$nextTick(() => {
|
|
const cellInputs = this.$refs.cellInput;
|
|
if (cellInputs) {
|
|
// 确保输入框获得焦点
|
|
if (Array.isArray(cellInputs)) {
|
|
cellInputs.forEach(input => {
|
|
if (input) input.focus();
|
|
});
|
|
} else if (cellInputs) {
|
|
cellInputs.focus();
|
|
}
|
|
}
|
|
});
|
|
},
|
|
finishEditing() {
|
|
// 保存当前状态
|
|
this.saveState();
|
|
this.editingCell = null;
|
|
},
|
|
selectColor(color) {
|
|
if (this.selectedCells.length === 0) return;
|
|
|
|
// 保存当前状态
|
|
this.saveState();
|
|
|
|
this.selectedColor = color;
|
|
this.colorPopoverVisible = false;
|
|
this.selectedCells.forEach(cell => {
|
|
this.tableData[cell.row][cell.col].bgColor = color;
|
|
});
|
|
},
|
|
clearCell() {
|
|
if (this.selectedCells.length === 0) return;
|
|
|
|
// 保存当前状态
|
|
this.saveState();
|
|
|
|
this.selectedCells.forEach(cell => {
|
|
this.tableData[cell.row][cell.col].content = '';
|
|
this.tableData[cell.row][cell.col].bgColor = '#ffffff';
|
|
});
|
|
},
|
|
startResizeCol(event, col) {
|
|
this.resizingCol = col;
|
|
this.startX = event.clientX;
|
|
this.startWidth = this.cellWidths[col];
|
|
},
|
|
startResizeRow(event, row) {
|
|
this.resizingRow = row;
|
|
this.startY = event.clientY;
|
|
this.startHeight = this.cellHeights[row];
|
|
},
|
|
handleResize(event) {
|
|
if (this.resizingCol !== null) {
|
|
const deltaX = event.clientX - this.startX;
|
|
const newWidth = Math.max(60, this.startWidth + deltaX);
|
|
this.$set(this.cellWidths, this.resizingCol, newWidth);
|
|
}
|
|
if (this.resizingRow !== null) {
|
|
const deltaY = event.clientY - this.startY;
|
|
const newHeight = Math.max(30, this.startHeight + deltaY);
|
|
this.$set(this.cellHeights, this.resizingRow, newHeight);
|
|
}
|
|
},
|
|
endResize() {
|
|
this.resizingCol = null;
|
|
this.resizingRow = null;
|
|
},
|
|
getRowspan(row, col) {
|
|
// 检查当前单元格是否是合并单元格的起始单元格
|
|
const mergedCell = this.mergedCells.find(mc => mc.startRow === row && mc.startCol === col);
|
|
return mergedCell ? mergedCell.endRow - mergedCell.startRow + 1 : 1;
|
|
},
|
|
|
|
getColspan(row, col) {
|
|
// 检查当前单元格是否是合并单元格的起始单元格
|
|
const mergedCell = this.mergedCells.find(mc => mc.startRow === row && mc.startCol === col);
|
|
return mergedCell ? mergedCell.endCol - mergedCell.startCol + 1 : 1;
|
|
},
|
|
|
|
getCellStyle(row, col) {
|
|
// 检查当前单元格是否是合并单元格的一部分
|
|
const mergedCell = this.mergedCells.find(mc =>
|
|
row >= mc.startRow && row <= mc.endRow &&
|
|
col >= mc.startCol && col <= mc.endCol
|
|
);
|
|
|
|
// 如果是合并单元格的非起始单元格,隐藏它
|
|
if (mergedCell && (row !== mergedCell.startRow || col !== mergedCell.startCol)) {
|
|
return {
|
|
display: 'none',
|
|
width: this.cellWidths[col] + 'px',
|
|
height: this.cellHeights[row] + 'px'
|
|
};
|
|
}
|
|
|
|
// 正常单元格的样式
|
|
return {
|
|
width: this.cellWidths[col] + 'px',
|
|
height: this.cellHeights[row] + 'px',
|
|
backgroundColor: this.tableData[row][col].bgColor || '#fff'
|
|
};
|
|
},
|
|
|
|
exportImage() {
|
|
const container = this.$refs.tableContainer;
|
|
html2canvas(container, {
|
|
backgroundColor: '#ffffff',
|
|
scale: 2,
|
|
useCORS: true
|
|
}).then(canvas => {
|
|
const link = document.createElement('a');
|
|
link.download = '数据卡.png';
|
|
link.href = canvas.toDataURL('image/png');
|
|
link.click();
|
|
this.$message.success('图片导出成功!');
|
|
}).catch(err => {
|
|
this.$message.error('图片导出失败:' + err.message);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|