Merge pull request 'ddj' (#425) from ddj into dev

Reviewed-on: http://git.feashow.cn/clay/tunnel-cloud-web/pulls/425
This commit is contained in:
2025-09-13 03:38:13 +00:00
7 changed files with 1939 additions and 118 deletions

BIN
public/images/img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

View File

@@ -27,6 +27,11 @@ onMounted(() => {
height: "100vh", height: "100vh",
width: "100vw", width: "100vw",
}, },
{
el: "#imghot",
height: "100vh",
width: "100vw",
},
], ],
}); });
}); });

View File

@@ -0,0 +1,234 @@
<template>
<div class="polygon-selector">
<!--用来和鼠标进行交互操作的canvas-->
<canvas
id="canvas"
ref="canvasRef"
:width="width + 'px'"
:height="height + 'px'"
:style="{ backgroundImage: 'url(' + backgroundImg + ')', cursor: 'crosshair' }"
></canvas>
<!--存储已生成的点线避免被清空-->
<canvas
id="canvasSave"
ref="canvasSaveRef"
:width="width + 'px'"
:height="height + 'px'"
class="canvas-save"
></canvas>
<div class="controls">
<el-button @click="clearSelection" type="primary">清空选区</el-button>
<slot name="controls"></slot>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
name: 'PolygonSelector',
props: {
width: {
type: [Number, String],
default: 1222
},
height: {
type: [Number, String],
default: 789
},
backgroundImg: {
type: String,
default: '/images/img.png'
}
},
emits: ['selection-complete', 'points-change'],
setup(props, { emit }) {
const canvasRef = ref(null)
const canvasSaveRef = ref(null)
let ctx = null
let ctxSave = null
let pointArr = [] // 存放坐标的数组
let oIndex = -1 // 判断鼠标是否移动到起始点处,-1为否1为是
onMounted(() => {
initCanvas()
})
const initCanvas = () => {
const canvas = canvasRef.value
const canvasSave = canvasSaveRef.value
if (!canvas || !canvasSave) return
ctx = canvas.getContext('2d')
ctxSave = canvasSave.getContext('2d')
// 设置样式
ctx.strokeStyle = 'rgba(102,168,255,1)' // 线条颜色
ctx.lineWidth = 4 // 线条粗细
ctxSave.strokeStyle = 'rgba(102,168,255,1)' // 线条颜色
ctxSave.lineWidth = 4 // 线条粗细
// 绑定事件
canvas.addEventListener('click', handleClick)
canvas.addEventListener('mousemove', handleMouseMove)
}
const handleClick = (e) => {
const canvas = canvasRef.value
const rect = canvas.getBoundingClientRect()
const pointX = e.clientX - rect.left
const pointY = e.clientY - rect.top
let piX, piY
if (oIndex > 0 && pointArr.length > 0) {
piX = pointArr[0].x
piY = pointArr[0].y
// 画点
makearc(ctx, piX, piY, getRandomNum(2, 2), 0, 180, 'rgba(102,168,255,0.5)')
pointArr.push({x: piX, y: piY})
canvasSave(pointArr) // 保存点线同步到另一个canvas
saveCanvas() // 生成画布
} else {
piX = pointX
piY = pointY
makearc(ctx, piX, piY, getRandomNum(2, 2), 0, 180, 'rgba(102,168,255,0.5)')
pointArr.push({x: piX, y: piY})
canvasSave(pointArr) // 保存点线同步到另一个canvas
}
}
const handleMouseMove = (e) => {
const canvas = canvasRef.value
const rect = canvas.getBoundingClientRect()
const pointX = e.clientX - rect.left
const pointY = e.clientY - rect.top
let piX, piY
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 鼠标下跟随的圆点
makearc(ctx, pointX, pointY, getRandomNum(4, 4), 0, 180, 'rgba(102,168,255,1)')
if (pointArr.length > 0) {
if ((pointX > pointArr[0].x-15 && pointX < pointArr[0].x+15) &&
(pointY > pointArr[0].y-15 && pointY < pointArr[0].y+15)) {
if (pointArr.length > 1) {
piX = pointArr[0].x
piY = pointArr[0].y
ctx.clearRect(0, 0, canvas.width, canvas.height)
makearc(ctx, piX, piY, getRandomNum(4, 4), 0, 180, 'rgba(102,168,255,0.5)')
oIndex = 1
}
} else {
piX = pointX
piY = pointY
oIndex = -1
}
// 开始绘制
ctx.beginPath()
ctx.moveTo(pointArr[0].x, pointArr[0].y)
if (pointArr.length > 1) {
for (let i = 1; i < pointArr.length; i++) {
ctx.lineTo(pointArr[i].x, pointArr[i].y)
}
}
ctx.lineTo(piX, piY)
ctx.fillStyle = 'rgba(161,195,255,0.5)' // 填充颜色
ctx.fill() // 填充
ctx.stroke() // 绘制
}
}
// 存储已生成的点线
const canvasSave = (points) => {
ctxSave.clearRect(0, 0, ctxSave.canvas.width, ctxSave.canvas.height)
ctxSave.beginPath()
if (points.length > 1) {
ctxSave.moveTo(points[0].x, points[0].y)
for (let i = 1; i < points.length; i++) {
ctxSave.lineTo(points[i].x, points[i].y)
ctxSave.fillStyle = 'rgba(161,195,255,0.5)' // 填充颜色
// ctxSave.fill()
ctxSave.stroke() // 绘制
}
ctxSave.closePath()
emit('points-change', [...points])
}
}
// 生成画布 结束绘画
const saveCanvas = () => {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
ctxSave.closePath() // 结束路径状态,结束当前路径,如果是一个未封闭的图形,会自动将首尾相连封闭起来
ctxSave.fill() // 填充
ctxSave.stroke() // 绘制
emit('selection-complete', [...pointArr])
pointArr = []
oIndex = -1
}
// 清空选区
const clearSelection = () => {
const canvas = canvasRef.value
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctxSave.clearRect(0, 0, ctxSave.canvas.width, ctxSave.canvas.height)
pointArr = []
oIndex = -1
emit('points-change', [])
}
// canvas生成圆点
const getRandomNum = (min, max) => {
const range = max - min
const rand = Math.random()
return (min + Math.round(rand * range))
}
const makearc = (ctx, x, y, r, s, e, color) => {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) // 清空画布
ctx.beginPath()
ctx.fillStyle = color
ctx.arc(x, y, r, s, e)
ctx.fill()
}
return {
canvasRef,
canvasSaveRef,
clearSelection
}
}
}
</script>
<style scoped>
.polygon-selector {
position: relative;
display: inline-block;
}
#canvas {
position: absolute;
left: 0;
top: 0;
z-index: 1;
}
.canvas-save {
position: absolute;
left: 0;
top: 0;
z-index: 0;
}
.controls {
position: absolute;
right: 0;
top: 0;
z-index: 2;
}
</style>

View File

@@ -0,0 +1,260 @@
<template>
<div class="polygon-selector" :style="{ width: '100%', height: '100%' }">
<!--用来和鼠标进行交互操作的canvas-->
<canvas
id="canvas"
ref="canvasRef"
:width="canvasWidth + 'px'"
:height="canvasHeight + 'px'"
:style="{ backgroundImage: 'url(' + backgroundImg + ')', cursor: 'crosshair' }"
></canvas>
<!--存储已生成的点线避免被清空-->
<canvas
id="canvasSave"
ref="canvasSaveRef"
:width="canvasWidth + 'px'"
:height="canvasHeight + 'px'"
class="canvas-save"
></canvas>
<div class="controls">
<el-button @click="clearSelection" type="primary">清空选区</el-button>
<slot name="controls"></slot>
</div>
</div>
</template>
<script>
import { ref, onMounted, onUpdated } from 'vue'
export default {
name: 'PolygonSelector',
props: {
width: {
type: [Number, String],
default: 1222
},
height: {
type: [Number, String],
default: 789
},
backgroundImg: {
type: String,
default: '/images/img.png'
}
},
emits: ['selection-complete', 'points-change'],
setup(props, { emit }) {
const canvasRef = ref(null)
const canvasSaveRef = ref(null)
let ctx = null
let ctxSave = null
let pointArr = [] // 存放坐标的数组
let oIndex = -1 // 判断鼠标是否移动到起始点处,-1为否1为是
// 响应式canvas尺寸
const canvasWidth = ref(1222)
const canvasHeight = ref(789)
onMounted(() => {
initCanvas()
window.addEventListener('resize', handleResize)
})
onUpdated(() => {
// 当组件更新时重新初始化canvas
if (canvasRef.value) {
canvasRef.value.width = canvasWidth.value
canvasRef.value.height = canvasHeight.value
}
})
const handleResize = () => {
// 根据窗口尺寸调整canvas大小
const container = canvasRef.value?.parentElement
if (container) {
canvasWidth.value = container.clientWidth
canvasHeight.value = container.clientHeight
}
}
const initCanvas = () => {
const canvas = canvasRef.value
const canvasSave = canvasSaveRef.value
if (!canvas || !canvasSave) return
ctx = canvas.getContext('2d')
ctxSave = canvasSave.getContext('2d')
// 设置样式
ctx.strokeStyle = 'rgba(102,168,255,1)' // 线条颜色
ctx.lineWidth = 4 // 线条粗细
ctxSave.strokeStyle = 'rgba(102,168,255,1)' // 线条颜色
ctxSave.lineWidth = 4 // 线条粗细
// 绑定事件
canvas.addEventListener('click', handleClick)
canvas.addEventListener('mousemove', handleMouseMove)
}
const handleClick = (e) => {
const canvas = canvasRef.value
const rect = canvas.getBoundingClientRect()
const pointX = e.clientX - rect.left
const pointY = e.clientY - rect.top
let piX, piY
if (oIndex > 0 && pointArr.length > 0) {
piX = pointArr[0].x
piY = pointArr[0].y
// 画点
makearc(ctx, piX, piY, getRandomNum(2, 2), 0, 180, 'rgba(102,168,255,0.5)')
pointArr.push({x: piX, y: piY})
canvasSave(pointArr) // 保存点线同步到另一个canvas
saveCanvas() // 生成画布
} else {
piX = pointX
piY = pointY
makearc(ctx, piX, piY, getRandomNum(2, 2), 0, 180, 'rgba(102,168,255,0.5)')
pointArr.push({x: piX, y: piY})
canvasSave(pointArr) // 保存点线同步到另一个canvas
}
}
const handleMouseMove = (e) => {
const canvas = canvasRef.value
const rect = canvas.getBoundingClientRect()
const pointX = e.clientX - rect.left
const pointY = e.clientY - rect.top
let piX, piY
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 鼠标下跟随的圆点
makearc(ctx, pointX, pointY, getRandomNum(4, 4), 0, 180, 'rgba(102,168,255,1)')
if (pointArr.length > 0) {
if ((pointX > pointArr[0].x-15 && pointX < pointArr[0].x+15) &&
(pointY > pointArr[0].y-15 && pointY < pointArr[0].y+15)) {
if (pointArr.length > 1) {
piX = pointArr[0].x
piY = pointArr[0].y
ctx.clearRect(0, 0, canvas.width, canvas.height)
makearc(ctx, piX, piY, getRandomNum(4, 4), 0, 180, 'rgba(102,168,255,0.5)')
oIndex = 1
}
} else {
piX = pointX
piY = pointY
oIndex = -1
}
// 开始绘制
ctx.beginPath()
ctx.moveTo(pointArr[0].x, pointArr[0].y)
if (pointArr.length > 1) {
for (let i = 1; i < pointArr.length; i++) {
ctx.lineTo(pointArr[i].x, pointArr[i].y)
}
}
ctx.lineTo(piX, piY)
ctx.fillStyle = 'rgba(161,195,255,0.5)' // 填充颜色
ctx.fill() // 填充
ctx.stroke() // 绘制
}
}
// 存储已生成的点线
const canvasSave = (points) => {
ctxSave.clearRect(0, 0, ctxSave.canvas.width, ctxSave.canvas.height)
ctxSave.beginPath()
if (points.length > 1) {
ctxSave.moveTo(points[0].x, points[0].y)
for (let i = 1; i < points.length; i++) {
ctxSave.lineTo(points[i].x, points[i].y)
ctxSave.fillStyle = 'rgba(161,195,255,0.5)' // 填充颜色
// ctxSave.fill()
ctxSave.stroke() // 绘制
}
ctxSave.closePath()
emit('points-change', [...points])
}
}
// 生成画布 结束绘画
const saveCanvas = () => {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
ctxSave.closePath() // 结束路径状态,结束当前路径,如果是一个未封闭的图形,会自动将首尾相连封闭起来
ctxSave.fill() // 填充
ctxSave.stroke() // 绘制
emit('selection-complete', [...pointArr])
pointArr = []
oIndex = -1
}
// 清空选区
const clearSelection = () => {
const canvas = canvasRef.value
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctxSave.clearRect(0, 0, ctxSave.canvas.width, ctxSave.canvas.height)
pointArr = []
oIndex = -1
emit('points-change', [])
}
// canvas生成圆点
const getRandomNum = (min, max) => {
const range = max - min
const rand = Math.random()
return (min + Math.round(rand * range))
}
const makearc = (ctx, x, y, r, s, e, color) => {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) // 清空画布
ctx.beginPath()
ctx.fillStyle = color
ctx.arc(x, y, r, s, e)
ctx.fill()
}
return {
canvasRef,
canvasSaveRef,
clearSelection,
canvasWidth,
canvasHeight
}
}
}
</script>
<style scoped>
.polygon-selector {
position: relative;
display: inline-block;
width: 100%;
height: 100%;
}
#canvas {
position: absolute;
left: 0;
top: 0;
z-index: 1;
}
.canvas-save {
position: absolute;
left: 0;
top: 0;
z-index: 0;
}
.controls {
position: absolute;
right: 0;
top: 0;
z-index: 2;
}
</style>

View File

@@ -23,6 +23,15 @@ const routes = [
breadcrumb: true breadcrumb: true
}, },
}, },
{
path: '/polygon-demo',
name: 'polygon-demo',
component: () => import('@/views/tunnel/polygon-demo.vue'),
meta: {
title: '图片热区',
breadcrumb: true
},
},
{ {
path: '/:tunnelId/:siteId', path: '/:tunnelId/:siteId',
name: 'changeSitePreview', name: 'changeSitePreview',

File diff suppressed because it is too large Load Diff