canvas 画图

📅 2026/7/13 1:58:03
canvas 画图
script setup import { ref, onMounted, onUnmounted, watch } from vue // 响应式变量 const canvasRef ref(null) const maxLogic ref(143190) const leftPadding ref(40) const topPadding ref(30) // X轴标签在顶部 const gridCount ref(37) // 棋盘格子数 const gridOptions [37, 21, 30] // 下拉选项 // 切换格子数 const onGridChange (e) { gridCount.value Number(e.target.value) } // 模拟数据 —— 点位密度数据替换成你自己的接口数据 const colorPalette [#3cb44b,#e6194b, #ffe119, #4363d8, #f58231, #911eb4, #42d4f4] let clickCount 0 // 点击次数用于取颜色 const pointDensityData ref([ { idx: 1, coordX: -30000, coordY: 20000, color: #3cb44b }, { idx: 2, coordX: 10000, coordY: -15000, color: #3cb44b }, { idx: 3, coordX: 40000, coordY: 35000, color: #3cb44b }, { idx: 4, coordX: -50000, coordY: -40000, color: #3cb44b }, { idx: 5, coordX: 20000, coordY: 50000, color: #3cb44b }, ]) // 选中行的 key 列表 const selectedRowKeys ref([2, 4]) // 选中行完整数据用于格子高亮 const selectedRows ref([ { idx: 2, coordX: 10000, coordY: -15000 }, { idx: 4, coordX: -50000, coordY: -40000 }, ]) // 手动高亮的格子列表 const highlightCells ref([]) // 输入框坐标 const inputX ref() const inputY ref() // 批量修改点颜色 // 入参coords 坐标数组 [{ coordX, coordY }, ...]color 目标颜色 // 示例updatePointsColor([{ coordX: 76200, coordY: 76200 }], #ff0000) const updatePointsColor (coords, color) { if (!Array.isArray(coords) || coords.length 0 || !color) return pointDensityData.value pointDensityData.value.map(p { const matched coords.some(c c.coordX p.coordX c.coordY p.coordY) return matched ? { ...p, color } : p }) console.log(已将 ${coords.length} 个点位颜色修改为 ${color}, coords) } // 随机生成点 let nextIdx 6 // 下一个点的 idx初始数据用了1-5 let firstBatchPoints [] // 保存第一组随机生成的点 const generateRandomPoints () { const ori (152400 - maxLogic.value) / 2 - 76200 const minCoord ori maxLogic.value * 0.05 // 留一点边距 const maxCoord ori maxLogic.value * 0.95 const count 5 const color colorPalette[clickCount % colorPalette.length] const newPoints [] for (let i 0; i count; i) { const coordX Math.floor(Math.random() * (maxCoord - minCoord) minCoord) const coordY Math.floor(Math.random() * (maxCoord - minCoord) minCoord) newPoints.push({ idx: nextIdx, coordX, coordY, color }) } // 第一组点保存下来 if (clickCount 0) { firstBatchPoints newPoints.map(p ({ coordX: p.coordX, coordY: p.coordY })) } clickCount pointDensityData.value [...pointDensityData.value, ...newPoints] console.log(本次生成的点位, newPoints) // 随机选中一些新点 const newKeys newPoints .filter(() Math.random() 0.6) .map(p p.idx) selectedRowKeys.value [...selectedRowKeys.value, ...newKeys] selectedRows.value [...selectedRows.value, ...newPoints.filter(p newKeys.includes(p.idx))] } // 把第一组随机点改为黄色 const changeFirstBatchToYellow () { if (firstBatchPoints.length 0) { alert(请先点击随机生成点生成第一组点) return } updatePointsColor(firstBatchPoints, #ffe119) } // 手动高亮格子 // 单个坐标高亮 const highlightCellByCoord () { const x Number(inputX.value) const y Number(inputY.value) if (isNaN(x) || isNaN(y)) { alert(请输入有效的数字坐标) return } highlightCellsByCoords([{ coordX: x, coordY: y }]) } // 批量高亮 // highlightCellsByCoords([ // { coordX: 78000, coordY: 80000 }, // { coordX: -50000, coordY: 30000 }, // { coordX: 60000, coordY: -20000 }, // ]) // 批量坐标高亮入参格式[{ coordX, coordY }, ...] const highlightCellsByCoords (coords) { if (!Array.isArray(coords) || coords.length 0) return const newCells coords.map(c ({ coordX: c.coordX, coordY: c.coordY })) highlightCells.value [...highlightCells.value, ...newCells] console.log(批量高亮坐标, newCells) } // 取消单个高亮入参坐标 { coordX, coordY } const removeHighlightByCoord (coord) { highlightCells.value highlightCells.value.filter( c !(c.coordX coord.coordX c.coordY coord.coordY) ) console.log(取消高亮坐标, coord) } // 批量取消高亮入参坐标数组 [{ coordX, coordY }, ...] const removeHighlightsByCoords (coords) { if (!Array.isArray(coords) || coords.length 0) return const removeSet new Set(coords.map(c ${c.coordX}_${c.coordY})) highlightCells.value highlightCells.value.filter( c !removeSet.has(${c.coordX}_${c.coordY}) ) console.log(批量取消高亮数, removeSet.size) } // 取消全部高亮 const clearAllHighlights () { highlightCells.value [] console.log(已清除所有高亮) } // 输入框取消单个高亮 const removeHighlightByInput () { const x Number(inputX.value) const y Number(inputY.value) if (isNaN(x) || isNaN(y)) { alert(请输入有效的数字坐标) return } removeHighlightByCoord({ coordX: x, coordY: y }) } // 绘制函数 const drawChessboard () { const canvas canvasRef.value if (!canvas) return const gc gridCount.value // 提取 ref 值避免每次取 .value maxLogic.value 143190 leftPadding.value 40 topPadding.value 30 const ori (152400 - maxLogic.value) / 2 - 76200 const ctx canvas.getContext(2d) // 适配容器尺寸 const container canvas.parentElement const containerW container.clientWidth const containerH container.clientHeight if (containerW 0 || containerH 0) return // 容器尺寸为0时不绘制 ctx.clearRect(0, 0, canvas.width, canvas.height) canvas.width containerW canvas.height containerH // 格子宽高一致取较小值作为单元格尺寸棋盘为正方形 const cellSize Math.min((containerW - leftPadding.value) / gc, (containerH - topPadding.value) / gc) const cellW cellSize const cellH cellSize const realBoardW cellSize * gc const realBoardH cellSize * gc // 1. 画棋盘格 ctx.strokeStyle black ctx.lineWidth 1 for (let i 0; i gc; i) { const pxX (i / gc) * realBoardW leftPadding.value ctx.beginPath() ctx.moveTo(pxX, topPadding.value) ctx.lineTo(pxX, topPadding.value realBoardH) ctx.stroke() } for (let i 0; i gc; i) { const pxY topPadding.value (i / gc) * realBoardH ctx.beginPath() ctx.moveTo(leftPadding.value, pxY) ctx.lineTo(leftPadding.value realBoardW, pxY) ctx.stroke() } // 1.5 画手动高亮格子根据当前 gridCount 实时计算格子位置 const gridSizeLogic maxLogic.value / gc highlightCells.value.forEach(cell { let gridX Math.floor((cell.coordX - ori) / gridSizeLogic) let gridY Math.floor((cell.coordY - ori) / gridSizeLogic) gridX Math.max(0, Math.min(gridX, gc - 1)) gridY Math.max(0, Math.min(gridY, gc - 1)) const pxX gridX * cellW leftPadding.value const pxY topPadding.value (gc - 1 - gridY) * cellH ctx.fillStyle rgba(255, 0, 0, 0.4) ctx.fillRect(pxX, pxY, cellW, cellH) ctx.lineWidth 2 ctx.strokeStyle red ctx.strokeRect(pxX, pxY, cellW, cellH) }) // 2. 画所有坐标点 pointDensityData.value.forEach(point { const size 3 ctx.fillStyle point.color || green const pxX ((point.coordX - ori) / maxLogic.value) * realBoardW leftPadding.value const pxY topPadding.value realBoardH - ((point.coordY - ori) / maxLogic.value) * realBoardH ctx.beginPath() ctx.arc(pxX, pxY, size, 0, Math.PI * 2) ctx.fill() }) // 3. 画索引 ctx.fillStyle #333 ctx.font 12px sans-serif ctx.textAlign center ctx.textBaseline middle // X轴从左到右 1→37标签在顶部 for (let i 1; i gc; i) { const centerX leftPadding.value (i - 0.5) * cellW const centerY topPadding.value / 2 ctx.fillText(String(i), centerX, centerY) } ctx.textAlign right ctx.textBaseline middle // Y轴从上到下 1→37右上角为1 for (let i 1; i gc; i) { const centerX leftPadding.value / 2 const centerY topPadding.value (i - 0.5) * cellH ctx.fillText(String(i), centerX, centerY) } } // 生命周期 响应式监听 const wrapperRef ref(null) onMounted(() { drawChessboard() }) // 监听选中数据变化自动重绘 watch([selectedRowKeys, selectedRows, pointDensityData, gridCount, highlightCells], () { drawChessboard() }) // 监听容器尺寸变化自动重绘 let resizeObserver null onMounted(() { if (wrapperRef.value) { resizeObserver new ResizeObserver(() { drawChessboard() }) resizeObserver.observe(wrapperRef.value) } }) onUnmounted(() { if (resizeObserver) resizeObserver.disconnect() }) /script template div classcanvas-container div classtoolbar label classlabel格子数/label select classselect :valuegridCount changeonGridChange option v-foropt in gridOptions :keyopt :valueopt{{ opt }} × {{ opt }}/option /select button classbtn clickgenerateRandomPoints随机生成点/button button classbtn btn-yellow clickchangeFirstBatchToYellow第一组点改黄色/button span classdivider/span label classlabelX坐标/label input classinput v-modelinputX typenumber placeholder如 78000 / label classlabelY坐标/label input classinput v-modelinputY typenumber placeholder如 80000 / button classbtn btn-red clickhighlightCellByCoord确认高亮/button button classbtn btn-gray clickremoveHighlightByInput取消单个高亮/button button classbtn btn-gray clickclearAllHighlights清除全部高亮/button /div div classcanvas-wrapper refwrapperRef canvas refcanvasRef/canvas /div /div /template style scoped langscss .canvas-container { width: 100%; } .toolbar { padding: 8px 12px; display: flex; align-items: center; gap: 12px; .label { font-size: 14px; color: #333; } .select { padding: 6px 10px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 14px; cursor: pointer; outline: none; :focus { border-color: #409eff; } } .btn { padding: 6px 16px; background: #409eff; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; :hover { background: #66b1ff; } } .btn-red { background: #f56c6c; :hover { background: #f78989; } } .btn-yellow { background: #e6a23c; :hover { background: #ebb563; } } .btn-gray { background: #909399; :hover { background: #a6a9ad; } } .divider { width: 1px; height: 24px; background: #dcdfe6; } .input { width: 90px; padding: 6px 10px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 14px; outline: none; :focus { border-color: #409eff; } } } .canvas-wrapper { width: 100%; height: 600px; position: relative; canvas { display: block; width: 100%; height: 100%; } } /style