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

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