用deepseek生成的在浏览器中运行的 canvas版的类excel简单组件,可以添加行,添加列,加载示例数据,可以对单元格进行编辑;可以调整行高,列高等。可以在此基础上不断完善和添加功能。
一、运行效果
二、源代码
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Excel表格</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.container {
width: 100%;
max-width: 1200px;
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
header {
background: linear-gradient(to right, #2c3e50, #4a6491);
color: white;
padding: 20px;
text-align: center;
border-bottom: 2px solid #3498db;
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
}
.subtitle {
font-size: 1.1rem;
opacity: 0.9;
}
.main-content {
display: flex;
padding: 20px;
}
.controls {
width: 250px;
background: #f8f9fa;
padding: 20px;
border-right: 1px solid #ddd;
border-radius: 8px 0 0 8px;
}
.control-group {
margin-bottom: 25px;
}
h3 {
color: #2c3e50;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 2px solid #3498db;
}
.btn {
display: block;
width: 100%;
padding: 12px;
margin: 10px 0;
background: linear-gradient(to right, #3498db, #2c3e50);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn:active {
transform: translateY(0);
}
.table-container {
flex: 1;
padding: 20px;
position: relative;
overflow: hidden;
background: #fff;
}
#shinersheet {
border: 1px solid #ddd;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
background: white;
cursor: cell;
}
#cellEditor {
position: absolute;
border: 2px solid #3498db;
padding: 5px;
font-size: 16px;
display: none;
z-index: 100;
box-shadow: 0 0 10px rgba(52, 152, 219, 0.5);
}
.info-panel {
padding: 15px;
background: #f8f9fa;
border-top: 1px solid #ddd;
font-size: 14px;
color: #555;
display: flex;
justify-content: space-between;
}
.status {
display: flex;
align-items: center;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: #2ecc71;
margin-right: 8px;
}
.key-shortcuts {
display: flex;
gap: 15px;
}
.key {
display: flex;
align-items: center;
gap: 5px;
}
.key kbd {
background: #e0e0e0;
padding: 3px 7px;
border-radius: 4px;
border: 1px solid #ccc;
font-size: 12px;
}
footer {
text-align: center;
padding: 15px;
color: #fff;
background: rgba(0, 0, 0, 0.7);
}
.highlight {
background: linear-gradient(to right, #3498db, #2c3e50);
color: white;
padding: 3px 6px;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Canvas Excel表格组件</h1>
<p class="subtitle">支持单元格编辑和行列大小调整功能</p>
</header>
<div class="main-content">
<div class="controls">
<div class="control-group">
<h3>表格操作</h3>
<button id="addRowBtn" class="btn">添加行 (+)</button>
<button id="addColBtn" class="btn">添加列 (+)</button>
<button id="clearBtn" class="btn">清空表格</button>
</div>
<div class="control-group">
<h3>示例数据</h3>
<button id="sampleDataBtn" class="btn">加载示例数据</button>
</div>
<div class="control-group">
<h3>使用说明</h3>
<ul style="padding-left: 20px; color: #555;">
<li>单击单元格进行编辑</li>
<li>按Enter键保存内容</li>
<li>拖拽行/列分隔线调整大小</li>
<li>使用方向键导航单元格</li>
</ul>
</div>
</div>
<div class="table-container">
<canvas id="shinersheet" width="800" height="500"></canvas>
<input type="text" id="cellEditor">
</div>
</div>
<div class="info-panel">
<div class="status">
<div class="status-indicator"></div>
<span>就绪</span>
</div>
<div class="key-shortcuts">
<div class="key"><kbd>Enter</kbd> 保存编辑</div>
<div class="key"><kbd>Esc</kbd> 取消编辑</div>
<div class="key"><kbd>←↑→↓</kbd> 导航单元格</div>
</div>
</div>
<footer>
<p>使用HTML5 Canvas开发的Excel风格表格组件 | 支持单元格编辑和行列调整</p>
</footer>
</div>
<script>
// 表格组件主类
class Shinersheet {
constructor(canvasId, editorId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.cellEditor = document.getElementById(editorId);
// 表格配置
this.config = {
rowHeight: 30,
colWidth: 100,
headerHeight: 40,
headerWidth: 50,
rowCount: 15,
colCount: 8,
cellPadding: 8,
headerBg: '#3498db',
headerTextColor: '#fff',
gridColor: '#ddd',
cellBg: '#fff',
cellTextColor: '#333',
selectedCellBorder: '#e74c3c'
};
// 表格数据
this.data = [];
this.initializeData();
// 当前选中单元格
this.selectedCell = { row: 0, col: 0 };
// 行列尺寸数组
this.rowHeights = Array(this.config.rowCount).fill(this.config.rowHeight);
this.colWidths = Array(this.config.colCount).fill(this.config.colWidth);
// 调整行列状态
this.resizing = {
row: -1,
col: -1,
startY: 0,
startX: 0
};
// 绑定事件
this.bindEvents();
// 初始化渲染
this.render();
}
// 初始化表格数据
initializeData() {
this.data = [];
for (let i = 0; i < this.config.rowCount; i++) {
this.data[i] = [];
for (let j = 0; j < this.config.colCount; j++) {
this.data[i][j] = '';
}
}
}
// 绑定事件处理函数
bindEvents() {
this.canvas.addEventListener('click', this.handleClick.bind(this));
this.canvas.addEventListener('dblclick', this.handleDoubleClick.bind(this));
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
// 单元格编辑器事件
this.cellEditor.addEventListener('keydown', this.handleEditorKeydown.bind(this));
this.cellEditor.addEventListener('blur', this.finishEditing.bind(this));
// 键盘导航
window.addEventListener('keydown', this.handleKeydown.bind(this));
}
// 处理画布点击
handleClick(e) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 检查是否点击了行标题
if (y > 0 && y < this.config.headerHeight && x > 0 && x < this.config.headerWidth) {
return; // 点击了左上角标题
}
// 检查是否点击了列标题
if (y > 0 && y < this.config.headerHeight && x > this.config.headerWidth) {
const col = this.getColAtX(x);
if (col >= 0) {
this.selectedCell.col = col;
this.render();
}
return;
}
// 检查是否点击了行标题
if (x > 0 && x < this.config.headerWidth && y > this.config.headerHeight) {
const row = this.getRowAtY(y);
if (row >= 0) {
this.selectedCell.row = row;
this.render();
}
return;
}
// 检查是否点击了单元格
const row = this.getRowAtY(y);
const col = this.getColAtX(x);
if (row >= 0 && col >= 0) {
this.selectedCell = { row, col };
this.render();
}
}
// 处理画布双击(开始编辑)
handleDoubleClick(e) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const row = this.getRowAtY(y);
const col = this.getColAtX(x);
if (row >= 0 && col >= 0) {
this.selectedCell = { row, col };
this.startEditing();
}
}
// 处理鼠标按下(开始调整行高/列宽)
handleMouseDown(e) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 检查是否在列分隔线上
if (y < this.config.headerHeight) {
for (let col = 0; col < this.colWidths.length; col++) {
const colX = this.getColX(col);
if (Math.abs(x - (colX + this.colWidths[col])) < 5) {
this.resizing.col = col;
this.resizing.startX = x;
this.canvas.style.cursor = 'col-resize';
return;
}
}
}
// 检查是否在行分隔线上
if (x < this.config.headerWidth) {
for (let row = 0; row < this.rowHeights.length; row++) {
const rowY = this.getRowY(row);
if (Math.abs(y - (rowY + this.rowHeights[row])) < 5) {
this.resizing.row = row;
this.resizing.startY = y;
this.canvas.style.cursor = 'row-resize';
return;
}
}
}
}
// 处理鼠标移动(调整行高/列宽)
handleMouseMove(e) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 调整列宽
if (this.resizing.col >= 0) {
const delta = x - this.resizing.startX;
const newWidth = Math.max(30, this.colWidths[this.resizing.col] + delta);
this.colWidths[this.resizing.col] = newWidth;
this.resizing.startX = x;
this.render();
return;
}
// 调整行高
if (this.resizing.row >= 0) {
const delta = y - this.resizing.startY;
const newHeight = Math.max(20, this.rowHeights[this.resizing.row] + delta);
this.rowHeights[this.resizing.row] = newHeight;
this.resizing.startY = y;
this.render();
return;
}
// 设置鼠标样式
if (y < this.config.headerHeight) {
for (let col = 0; col < this.colWidths.length; col++) {
const colX = this.getColX(col);
if (Math.abs(x - (colX + this.colWidths[col])) < 5) {
this.canvas.style.cursor = 'col-resize';
return;
}
}
}
if (x < this.config.headerWidth) {
for (let row = 0; row < this.rowHeights.length; row++) {
const rowY = this.getRowY(row);
if (Math.abs(y - (rowY + this.rowHeights[row])) < 5) {
this.canvas.style.cursor = 'row-resize';
return;
}
}
}
this.canvas.style.cursor = 'default';
}
// 处理鼠标释放(结束调整)
handleMouseUp(e) {
this.resizing.row = -1;
this.resizing.col = -1;
this.canvas.style.cursor = 'default';
}
// 处理编辑器按键
handleEditorKeydown(e) {
if (e.key === 'Enter') {
this.finishEditing();
} else if (e.key === 'Escape') {
this.cellEditor.style.display = 'none';
}
}
// 处理键盘导航
handleKeydown(e) {
if (this.cellEditor.style.display === 'block') return;
switch (e.key) {
case 'ArrowUp':
if (this.selectedCell.row > 0) {
this.selectedCell.row--;
this.render();
}
e.preventDefault();
break;
case 'ArrowDown':
if (this.selectedCell.row < this.rowHeights.length - 1) {
this.selectedCell.row++;
this.render();
}
e.preventDefault();
break;
case 'ArrowLeft':
if (this.selectedCell.col > 0) {
this.selectedCell.col--;
this.render();
}
e.preventDefault();
break;
case 'ArrowRight':
if (this.selectedCell.col < this.colWidths.length - 1) {
this.selectedCell.col++;
this.render();
}
e.preventDefault();
break;
case 'Enter':
this.startEditing();
e.preventDefault();
break;
}
}
// 开始编辑单元格
startEditing() {
const { row, col } = this.selectedCell;
const cellX = this.getColX(col);
const cellY = this.getRowY(row);
const cellWidth = this.colWidths[col];
const cellHeight = this.rowHeights[row];
this.cellEditor.value = this.data[row][col] || '';
this.cellEditor.style.display = 'block';
this.cellEditor.style.left = (this.canvas.offsetLeft + cellX) + 'px';
this.cellEditor.style.top = (this.canvas.offsetTop + cellY) + 'px';
this.cellEditor.style.width = (cellWidth - 2) + 'px';
this.cellEditor.style.height = (cellHeight - 2) + 'px';
this.cellEditor.focus();
this.cellEditor.select();
}
// 结束编辑
finishEditing() {
const { row, col } = this.selectedCell;
this.data[row][col] = this.cellEditor.value;
this.cellEditor.style.display = 'none';
this.render();
}
// 获取指定列起始X坐标
getColX(col) {
let x = this.config.headerWidth;
for (let i = 0; i < col; i++) {
x += this.colWidths[i];
}
return x;
}
// 获取指定行起始Y坐标
getRowY(row) {
let y = this.config.headerHeight;
for (let i = 0; i < row; i++) {
y += this.rowHeights[i];
}
return y;
}
// 根据Y坐标获取行索引
getRowAtY(y) {
if (y < this.config.headerHeight) return -1;
let currentY = this.config.headerHeight;
for (let row = 0; row < this.rowHeights.length; row++) {
currentY += this.rowHeights[row];
if (y < currentY) {
return row;
}
}
return -1;
}
// 根据X坐标获取列索引
getColAtX(x) {
if (x < this.config.headerWidth) return -1;
let currentX = this.config.headerWidth;
for (let col = 0; col < this.colWidths.length; col++) {
currentX += this.colWidths[col];
if (x < currentX) {
return col;
}
}
return -1;
}
// 添加新行
addRow() {
this.rowHeights.push(this.config.rowHeight);
const newRow = Array(this.colWidths.length).fill('');
this.data.push(newRow);
this.render();
}
// 添加新列
addColumn() {
this.colWidths.push(this.config.colWidth);
for (let row = 0; row < this.data.length; row++) {
this.data[row].push('');
}
this.render();
}
// 清空表格
clearTable() {
if (confirm('确定要清空表格数据吗?')) {
this.initializeData();
this.render();
}
}
// 加载示例数据
loadSampleData() {
this.data = [
['产品', '一月', '二月', '三月', '季度总计'],
['笔记本电脑', '120', '150', '180', '=SUM(B2:D2)'],
['智能手机', '200', '220', '250', '=SUM(B3:D3)'],
['平板电脑', '80', '90', '110', '=SUM(B4:D4)'],
['季度总计', '=SUM(B2:B4)', '=SUM(C2:C4)', '=SUM(D2:D4)', '=SUM(E2:E4)'],
['', '', '', '', ''],
['平均销量', '=AVERAGE(B2:B4)', '=AVERAGE(C2:C4)', '=AVERAGE(D2:D4)', '=AVERAGE(E2:E4)']
];
// 更新行列计数
this.rowHeights = Array(this.data.length).fill(this.config.rowHeight);
this.colWidths = Array(this.data[0].length).fill(this.config.colWidth);
this.render();
}
// 渲染表格
render() {
const ctx = this.ctx;
const canvas = this.canvas;
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 计算画布所需尺寸
const totalWidth = this.config.headerWidth + this.colWidths.reduce((a, b) => a + b, 0);
const totalHeight = this.config.headerHeight + this.rowHeights.reduce((a, b) => a + b, 0);
// 更新画布尺寸
canvas.width = totalWidth;
canvas.height = totalHeight;
// 绘制左上角标题
ctx.fillStyle = this.config.headerBg;
ctx.fillRect(0, 0, this.config.headerWidth, this.config.headerHeight);
ctx.strokeStyle = this.config.gridColor;
ctx.lineWidth = 1;
ctx.strokeRect(0, 0, this.config.headerWidth, this.config.headerHeight);
// 绘制列标题
let x = this.config.headerWidth;
for (let col = 0; col < this.colWidths.length; col++) {
const width = this.colWidths[col];
ctx.fillStyle = this.config.headerBg;
ctx.fillRect(x, 0, width, this.config.headerHeight);
ctx.strokeStyle = this.config.gridColor;
ctx.strokeRect(x, 0, width, this.config.headerHeight);
// 列标题文本
ctx.fillStyle = this.config.headerTextColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = 'bold 14px Arial';
ctx.fillText(String.fromCharCode(65 + col), x + width / 2, this.config.headerHeight / 2);
x += width;
}
// 绘制行标题
let y = this.config.headerHeight;
for (let row = 0; row < this.rowHeights.length; row++) {
const height = this.rowHeights[row];
ctx.fillStyle = this.config.headerBg;
ctx.fillRect(0, y, this.config.headerWidth, height);
ctx.strokeStyle = this.config.gridColor;
ctx.strokeRect(0, y, this.config.headerWidth, height);
// 行标题文本
ctx.fillStyle = this.config.headerTextColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = 'bold 14px Arial';
ctx.fillText((row + 1).toString(), this.config.headerWidth / 2, y + height / 2);
y += height;
}
// 绘制单元格
y = this.config.headerHeight;
for (let row = 0; row < this.rowHeights.length; row++) {
const rowHeight = this.rowHeights[row];
x = this.config.headerWidth;
for (let col = 0; col < this.colWidths.length; col++) {
const colWidth = this.colWidths[col];
// 绘制单元格背景
ctx.fillStyle = this.config.cellBg;
ctx.fillRect(x, y, colWidth, rowHeight);
// 绘制单元格边框
ctx.strokeStyle = this.config.gridColor;
ctx.strokeRect(x, y, colWidth, rowHeight);
// 绘制单元格文本
const cellValue = this.data[row]?.[col] || '';
ctx.fillStyle = this.config.cellTextColor;
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.font = '14px Arial';
// 文本截断处理
const maxWidth = colWidth - this.config.cellPadding * 2;
let displayText = cellValue;
let textWidth = ctx.measureText(displayText).width;
if (textWidth > maxWidth) {
// 简单截断处理
while (textWidth > maxWidth && displayText.length > 0) {
displayText = displayText.substring(0, displayText.length - 1);
textWidth = ctx.measureText(displayText + '...').width;
}
displayText += '...';
}
ctx.fillText(displayText, x + this.config.cellPadding, y + rowHeight / 2);
// 高亮选中单元格
if (row === this.selectedCell.row && col === this.selectedCell.col) {
ctx.strokeStyle = this.config.selectedCellBorder;
ctx.lineWidth = 2;
ctx.strokeRect(x + 1, y + 1, colWidth - 2, rowHeight - 2);
}
x += colWidth;
}
y += rowHeight;
}
}
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
const shinersheet = new Shinersheet('shinersheet', 'cellEditor');
// 绑定按钮事件
document.getElementById('addRowBtn').addEventListener('click', () => shinersheet.addRow());
document.getElementById('addColBtn').addEventListener('click', () => shinersheet.addColumn());
document.getElementById('clearBtn').addEventListener('click', () => shinersheet.clearTable());
document.getElementById('sampleDataBtn').addEventListener('click', () => shinersheet.loadSampleData());
});
</script>
</body>
</html>