一、背景
在 HOW - Canvas 入门系列之表格绘制工具(二) 中我们介绍过基于原生 Canvas 实现表格绘制,并支持如下能力:
- 绘制表格数据并填充单元格内容
- 单元格点击触发事件
- 支持动态调整列宽
今天我们将将基于 vue-konva 实现的一个表格渲染的组件。
二、涉及 konva 组件
- v-stage:用于场景一个画布,它是Konva应用程序的根容器,所有的图形都是在画布上绘制的。画布定义了Canvas的大小以及应用程序的全局属性。
- v-layer:用于创建一个图层,它是Konva中用于组织和管理可视元素的容器。通常情况下,每个图形都被添加到一个图层上,然后图层再添加到舞台上。使用图层可以实现对不同部分的图形进行分组和独立管理。
- v-group:用于创建一个群组,它是Konva中用于将多个形状或群组组合在一起的容器。群组可以包含任意数量的形状、群组或其他Konva节点,并可以一起移动、旋转和缩放。
- v-shape:用于创建任意形状的图形,比如矩形、圆形、线条等。通过指定不同的type属性,可以创建不同类型的形状。除了预定义的形状外,还可以通过sceneFunc属性自定义绘制函数来创建特定形状的图形。
- v-rect: 用于在 Konva 画布上绘制矩形,可以通过设置属性来定义矩形的位置、大小、填充颜色、边框颜色、边框宽度等属性。
- v-text:用于在 Konva 画布上显示文本,并且可以通过设置 属性来定义文本的位置、内容、字体、字体大小、字体颜色等属性。
三、具体实现
回顾一下我们要实现的能力:
- 绘制表格数据并填充单元格内容
- 单元格点击触发事件
- 支持动态调整列宽
下面是一个简单的示例,演示如何使用Vue Konva来实现一个多维表格渲染,支持绘制表格数据、填充单元格内容、单元格点击事件:
<template>
<v-stage :config="stageConfig" @click="handleStageClick">
<v-layer>
<!-- 绘制表格 -->
<v-group>
<!-- 绘制表头 -->
<v-rect
v-for="(column, columnIndex) in columns"
:key="'header-' + columnIndex"
:config="getHeaderRectConfig(columnIndex)"
/>
<v-text
v-for="(column, columnIndex) in columns"
:key="'header-text-' + columnIndex"
:config="getHeaderTextConfig(columnIndex)"
/>
<!-- 绘制表格内容 -->
<v-rect
v-for="(row, rowIndex) in rows"
:key="'row-' + rowIndex"
:config="getRowRectConfig(rowIndex)"
:fill="highlightedCell && highlightedCell.row === rowIndex ? (highlightedCell.column === null ? 'yellow' : 'lightyellow') : (rowIndex % 2 === 0 ? 'lightblue' : 'lightcyan')"
/>
<v-text
v-for="(row, rowIndex) in rows"
:key="'row-text-' + rowIndex"
:config="getRowTextConfig(rowIndex)"
/>
</v-group>
</v-layer>
</v-stage>
</template>
<script>
import { Vue as VueKonva, Stage, Layer, Group, Rect, Text } from 'vue-konva';
export default {
components: {
VueKonva,
Stage,
Layer,
Group,
Rect,
Text,
},
data() {
return {
stageConfig: {
width: 800,
height: 600,
},
columns: ['Column 1', 'Column 2', 'Column 3'], // 列名
rows: ['Row 1', 'Row 2', 'Row 3'], // 行名
cellWidth: 100,
cellHeight: 50,
highlightedCell: null, // 记录当前高亮的单元格
};
},
methods: {
// 处理舞台点击事件
handleStageClick(event) {
// 点击事件处理逻辑
const clickedX = event.evt.offsetX;
const clickedY = event.evt.offsetY;
const columnIndex = Math.floor(clickedX / this.cellWidth);
const rowIndex = Math.floor(clickedY / this.cellHeight);
this.highlightedCell = { row: rowIndex, column: columnIndex };
},
// 获取表头矩形配置
getHeaderRectConfig(columnIndex) {
return {
x: columnIndex * this.cellWidth,
y: 0,
width: this.cellWidth,
height: this.cellHeight,
fill: 'lightgray',
stroke: 'black',
strokeWidth: 1,
};
},
// 获取表头文本配置
getHeaderTextConfig(columnIndex) {
return {
x: columnIndex * this.cellWidth + 5,
y: 5,
text: this.columns[columnIndex],
};
},
// 获取行矩形配置
getRowRectConfig(rowIndex) {
return {
x: 0,
y: rowIndex * this.cellHeight,
width: this.cellWidth * this.columns.length,
height: this.cellHeight,
fill: rowIndex % 2 === 0 ? 'lightblue' : 'lightcyan',
stroke: 'black',
strokeWidth: 1,
};
},
// 获取行文本配置
getRowTextConfig(rowIndex) {
return {
x: 5,
y: rowIndex * this.cellHeight + 5,
text: this.rows[rowIndex],
};
},
},
};
</script>
<style scoped>
/* Add your styles here */
</style>
在这个示例中,我们使用了Vue Konva的各个组件来绘制多维表格。其中:
<v-stage>
用于创建舞台。<v-layer>
用于创建图层。<v-group>
用于组织表格中的各个元素。<v-rect>
用于绘制矩形,代表表格的单元格。<v-text>
用于绘制文本,显示表格的行名和列名。
这个示例中还包含了一些方法,用于动态计算单元格的位置和尺寸,并根据表格的行数和列数来绘制相应的内容。同时,还提供了一个舞台点击事件的处理方法handleStageClick
,你可以在其中添加你需要的点击事件处理逻辑。
你可以根据需要扩展这个示例,实现更复杂的表格功能,比如动态调整列宽等:
<template>
<v-stage :config="stageConfig" @mousedown="handleMouseDown" @mouseup="handleMouseUp" @mousemove="handleMouseMove">
<v-layer>
<!-- 绘制表格 -->
<v-group>
<!-- 绘制表头 -->
<v-rect
v-for="(column, columnIndex) in columns"
:key="'header-' + columnIndex"
:config="getHeaderRectConfig(columnIndex)"
/>
<v-text
v-for="(column, columnIndex) in columns"
:key="'header-text-' + columnIndex"
:config="getHeaderTextConfig(columnIndex)"
/>
<!-- 绘制表格内容 -->
<v-rect
v-for="(row, rowIndex) in rows"
:key="'row-' + rowIndex"
:config="getRowRectConfig(rowIndex)"
/>
<v-text
v-for="(row, rowIndex) in rows"
:key="'row-text-' + rowIndex"
:config="getRowTextConfig(rowIndex)"
/>
</v-group>
<!-- 拖拽调整列宽的辅助线 -->
<v-line v-if="isResizing" :config="resizerLineConfig"/>
</v-layer>
</v-stage>
</template>
<script>
import { Vue as VueKonva, Stage, Layer, Group, Rect, Text, Line } from 'vue-konva';
export default {
components: {
VueKonva,
Stage,
Layer,
Group,
Rect,
Text,
Line,
},
data() {
return {
stageConfig: {
width: 800,
height: 600,
},
columns: ['Column 1', 'Column 2', 'Column 3'], // 列名
rows: ['Row 1', 'Row 2', 'Row 3'], // 行名
cellHeight: 50,
isResizing: false, // 是否正在调整列宽
resizeStartX: 0, // 调整列宽的起始坐标
resizingColumnIndex: -1, // 正在调整宽度的列索引
columnWidths: [100, 100, 100], // 列宽度数组
};
},
methods: {
// 处理鼠标按下事件
handleMouseDown(event) {
if (event.target.name() === 'resizer') {
this.isResizing = true;
this.resizeStartX = event.evt.clientX;
this.resizingColumnIndex = parseInt(event.target.attrs.columnIndex);
}
},
// 处理鼠标移动事件
handleMouseMove(event) {
if (this.isResizing) {
const movementX = event.evt.clientX - this.resizeStartX;
const newWidth = Math.max(30, this.columnWidths[this.resizingColumnIndex] + movementX);
this.$set(this.columnWidths, this.resizingColumnIndex, newWidth);
}
},
// 处理鼠标释放事件
handleMouseUp() {
if (this.isResizing) {
this.isResizing = false;
this.resizeStartX = 0;
this.resizingColumnIndex = -1;
}
},
// 获取表头矩形配置
getHeaderRectConfig(columnIndex) {
return {
x: this.getCellX(columnIndex),
y: 0,
width: this.columnWidths[columnIndex],
height: this.cellHeight,
fill: 'lightgray',
stroke: 'black',
strokeWidth: 1,
name: 'resizer', // 设置名称以便于识别
draggable: true, // 允许拖拽
columnIndex: columnIndex, // 保存列索引
};
},
// 获取表头文本配置
getHeaderTextConfig(columnIndex) {
return {
x: this.getCellX(columnIndex) + 5,
y: 5,
text: this.columns[columnIndex],
};
},
// 获取行矩形配置
getRowRectConfig(rowIndex) {
return {
x: 0,
y: rowIndex * this.cellHeight,
width: this.columnWidths.reduce((acc, cur) => acc + cur, 0),
height: this.cellHeight,
stroke: 'black',
strokeWidth: 1,
};
},
// 获取行文本配置
getRowTextConfig(rowIndex) {
return {
x: 5,
y: rowIndex * this.cellHeight + 5,
text: this.rows[rowIndex],
};
},
// 获取单元格的 x 坐标
getCellX(columnIndex) {
let x = 0;
for (let i = 0; i < columnIndex; i++) {
x += this.columnWidths[i];
}
return x;
},
},
computed: {
// 获取调整列宽时的辅助线配置
resizerLineConfig() {
return {
points: [this.resizeStartX, 0, this.resizeStartX, this.stageConfig.height],
stroke: 'black',
strokeWidth: 1,
};
},
},
};
</script>
可以发现我们对于 cell 的宽度将通过一个 columnWidths 来维护,并根据鼠标事件来动态变更当前列每个cell渲染宽度和辅助线的位置。