402 lines
10 KiB
Vue
402 lines
10 KiB
Vue
<!-- 子组件 transform-canvas.vue -->
|
||
<template>
|
||
<view>
|
||
<canvas
|
||
id="myCanvas"
|
||
canvas-id="myCanvas"
|
||
type="2d"
|
||
:style="{
|
||
width: `${canvasDisplayWidth}px`,
|
||
height: `${canvasDisplayHeight}px`,
|
||
border: '1px solid #eee'
|
||
}"
|
||
></canvas>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
props: {
|
||
width: Number, // 画布实际宽度(像素)
|
||
height: Number, // 画布实际高度(像素)
|
||
imgUrl: String, // 图片URL
|
||
matrix: { // 变换矩阵
|
||
type: Object,
|
||
default: () => ({
|
||
a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0
|
||
})
|
||
},
|
||
areaData:{},
|
||
seatData:{},
|
||
selectedCodes:{}
|
||
},
|
||
data() {
|
||
return {
|
||
canvasContext: null,
|
||
image: null,
|
||
dpr: 1,
|
||
canvasDisplayWidth: 800, // 默认显示尺寸
|
||
canvasDisplayHeight: 600,
|
||
canvasActualWidth: 800, // 实际像素尺寸
|
||
canvasActualHeight: 600,
|
||
nowSelectedCodes:this.selectedCodes
|
||
};
|
||
},
|
||
watch: {
|
||
selectedCodes:{
|
||
handler(newVal) {
|
||
this.nowSelectedCodes = new Set(newVal);
|
||
},
|
||
immediate: true,
|
||
deep: true // 添加深度监听
|
||
},
|
||
imgUrl: {
|
||
handler(newUrl) {
|
||
if (newUrl) this.loadImage(newUrl);
|
||
},
|
||
immediate: true,
|
||
|
||
},
|
||
width: {
|
||
handler() {
|
||
this.updateCanvasSize();
|
||
},
|
||
immediate: true
|
||
},
|
||
height: {
|
||
handler() {
|
||
this.updateCanvasSize();
|
||
},
|
||
immediate: true
|
||
},
|
||
matrix: {
|
||
deep: true,
|
||
immediate: true, // 添加立即触发
|
||
handler() {
|
||
this.$nextTick(this.redraw); // 确保在下一个tick重绘
|
||
}
|
||
},
|
||
areaData: {
|
||
deep: true,
|
||
handler() {
|
||
this.$nextTick(this.redraw);
|
||
}
|
||
},
|
||
seatData: {
|
||
deep: true,
|
||
handler() {
|
||
this.$nextTick(this.redraw);
|
||
}
|
||
}
|
||
},
|
||
mounted() {
|
||
this.initCanvas();
|
||
},
|
||
methods: {
|
||
|
||
// 修正坐标转换方法
|
||
invertPoint(x, y) {
|
||
const det = this.matrix.a * this.matrix.d - this.matrix.b * this.matrix.c;
|
||
return {
|
||
x: (this.matrix.d * x - this.matrix.c * y -
|
||
this.matrix.d * this.matrix.tx + this.matrix.c * this.matrix.ty) / det,
|
||
y: (-this.matrix.b * x + this.matrix.a * y +
|
||
this.matrix.b * this.matrix.tx - this.matrix.a * this.matrix.ty) / det
|
||
};
|
||
},
|
||
|
||
// 添加移动速度检查
|
||
shouldIgnoreClick() {
|
||
return Date.now() - this.lastTouchTime < 300;
|
||
},
|
||
|
||
// 添加点击检测方法
|
||
checkHitArea(x, y) {
|
||
if (!this.areaData) return null;
|
||
|
||
// 转换坐标到原始画布空间
|
||
const inverted = this.matrix.invertPoint(x, y, this.dpr);
|
||
// console.log('checkHitArea',inverted)
|
||
// 遍历所有区域检测
|
||
for (const area of this.areaData) {
|
||
if (this.pointInPolygon(inverted.x, inverted.y, area.polygon)) {
|
||
return area;
|
||
}
|
||
}
|
||
return null;
|
||
},
|
||
|
||
pointInPolygon(x, y, polygon) {
|
||
const points = [];
|
||
for (let i = 0; i < polygon.length; i += 2) {
|
||
points.push([polygon[i], polygon[i + 1]]);
|
||
}
|
||
|
||
let inside = false;
|
||
let j = points.length - 1;
|
||
|
||
for (let i = 0; i < points.length; j = i++) {
|
||
const [xi, yi] = points[i];
|
||
const [xj, yj] = points[j];
|
||
|
||
// 检测点是否在线段上
|
||
if (this.pointOnLineSegment(x, y, xi, yi, xj, yj)) return true;
|
||
|
||
// 射线法核心逻辑
|
||
if (yi <= y && yj > y || yj <= y && yi > y) {
|
||
const slope = (xj - xi) / (yj - yi);
|
||
if (x <= xi + (y - yi) * slope) {
|
||
inside = !inside;
|
||
}
|
||
}
|
||
}
|
||
|
||
return inside;
|
||
},
|
||
|
||
|
||
|
||
// 添加点是否在线段上的检查
|
||
pointOnLineSegment(x, y, x1, y1, x2, y2) {
|
||
// 线段的长度
|
||
const dx = x2 - x1;
|
||
const dy = y2 - y1;
|
||
const segmentLength = Math.sqrt(dx * dx + dy * dy);
|
||
|
||
// 点到线段起点的向量
|
||
const vx = x - x1;
|
||
const vy = y - y1;
|
||
|
||
// 向量在方向上的投影
|
||
const dotProduct = vx * dx + vy * dy;
|
||
|
||
// 计算参数t (0-1之间表示在线段上)
|
||
const t = dotProduct / (segmentLength * segmentLength);
|
||
|
||
// 点在线段上
|
||
if (t >= 0 && t <= 1) {
|
||
// 计算点在线段上的位置
|
||
const projX = x1 + t * dx;
|
||
const projY = y1 + t * dy;
|
||
|
||
// 检查点与投影点的距离(容差5px)
|
||
const distance = Math.sqrt((x - projX) * 2 + (y - projY) * 2);
|
||
return distance < 5;
|
||
}
|
||
|
||
return false;
|
||
},
|
||
|
||
initCanvas() {
|
||
const query = uni.createSelectorQuery().in(this);
|
||
query.select('#myCanvas')
|
||
.fields({ node: true, size: true })
|
||
.exec(async (res) => {
|
||
if (!res[0]) return;
|
||
|
||
this.canvas = res[0].node;
|
||
this.ctx = res[0].node.getContext('2d');
|
||
|
||
// 获取设备像素比
|
||
this.dpr = uni.getSystemInfoSync().pixelRatio;
|
||
|
||
// 更新画布尺寸
|
||
this.updateCanvasSize();
|
||
|
||
// 如果已有图片URL,加载图片
|
||
if (this.imgUrl) {
|
||
await this.loadImage(this.imgUrl);
|
||
}
|
||
});
|
||
},
|
||
|
||
updateCanvasSize() {
|
||
// 显示尺寸使用prop传入的width/height
|
||
this.canvasDisplayWidth = this.width;
|
||
this.canvasDisplayHeight = this.height;
|
||
// * this.dpr
|
||
// * this.dpr
|
||
// 实际像素尺寸考虑DPR
|
||
this.canvasActualWidth = this.width;
|
||
this.canvasActualHeight = this.height;
|
||
|
||
if (this.canvas) {
|
||
this.canvas.width = this.canvasActualWidth;
|
||
this.canvas.height = this.canvasActualHeight;
|
||
// this.ctx.scale(this.dpr, this.dpr);
|
||
this.redraw();
|
||
}
|
||
},
|
||
async loadImage(src) {
|
||
try {
|
||
// 等待canvas初始化
|
||
if (!this.canvas) {
|
||
await this.initCanvas();
|
||
}
|
||
|
||
this.image = await new Promise((resolve, reject) => {
|
||
if (!this.canvas) {
|
||
// reject(new Error("Canvas not initialized"));
|
||
return;
|
||
}
|
||
|
||
const image = this.canvas.createImage();
|
||
image.src = src;
|
||
image.onload = () => resolve(image);
|
||
image.onerror = reject;
|
||
});
|
||
this.redraw();
|
||
} catch (e) {
|
||
console.error("图片加载失败:", e);
|
||
this.image = null;
|
||
}
|
||
},
|
||
|
||
// 在redraw方法中添加视图判断
|
||
checkSeatHit(x, y) {
|
||
if (!this.seatData) return null;
|
||
|
||
// 转换坐标到原始画布空间
|
||
const inverted = this.matrix.invertPoint(x, y, this.dpr);
|
||
|
||
// 遍历所有座位检测
|
||
for (const seat of this.seatData) {
|
||
const dx = seat.x - inverted.x;
|
||
const dy = seat.y - inverted.y;
|
||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||
|
||
// 命中检测(8px半径)
|
||
if (distance < 8) {
|
||
return seat;
|
||
}
|
||
}
|
||
return null;
|
||
},
|
||
redraw() {
|
||
if (!this.ctx) return;
|
||
|
||
this.ctx.save();
|
||
this.ctx.resetTransform();
|
||
this.ctx.clearRect(0, 0, this.canvasActualWidth, this.canvasActualHeight);
|
||
this.ctx.scale(this.dpr, this.dpr);
|
||
|
||
// 应用变换矩阵
|
||
const { a, b, c, d, tx, ty } = this.matrix;
|
||
this.ctx.setTransform(a, b, c, d, tx, ty);
|
||
|
||
// 仅在区域视图显示背景图片
|
||
if (this.image && this.$parent?.currentView === 'area') {
|
||
this.ctx.drawImage(
|
||
this.image,
|
||
0,
|
||
0,
|
||
this.canvasDisplayWidth,
|
||
this.canvasDisplayHeight
|
||
);
|
||
}
|
||
|
||
// 绘制区域(仅在区域视图)
|
||
if (this.areaData && this.areaData.length > 0 && this.$parent?.currentView === 'area') {
|
||
this.drawAreas();
|
||
}
|
||
|
||
// 绘制座位(仅在座位视图)
|
||
if (this.seatData && this.seatData.length > 0 && this.$parent?.currentView === 'seat') {
|
||
this.drawSeats();
|
||
}
|
||
|
||
this.ctx.restore();
|
||
},
|
||
// 添加绘制区域方法
|
||
drawAreas() {
|
||
this.areaData.forEach(area => {
|
||
const points = [];
|
||
for (let i = 0; i < area.polygon.length; i += 2) {
|
||
points.push([area.polygon[i], area.polygon[i + 1]]);
|
||
}
|
||
|
||
this.ctx.beginPath();
|
||
points.forEach(([x, y], index) => {
|
||
if (index === 0) this.ctx.moveTo(x, y);
|
||
else this.ctx.lineTo(x, y);
|
||
});
|
||
this.ctx.closePath();
|
||
this.ctx.strokeStyle = 'rgba(255, 0, 0, 0.7)';
|
||
this.ctx.lineWidth = 2;
|
||
this.ctx.stroke();
|
||
this.ctx.fillStyle = 'rgba(0, 255, 0, 0.2)';
|
||
this.ctx.fill();
|
||
});
|
||
},
|
||
drawSeats() {
|
||
this.ctx.save();
|
||
|
||
// 应用变换矩阵
|
||
const { a, b, c, d, tx, ty } = this.matrix;
|
||
this.ctx.setTransform(a, b, c, d, tx, ty);
|
||
|
||
// 获取当前缩放比例
|
||
const scale = Math.sqrt(a * a + b * b);
|
||
|
||
// 确保 nowSelectedCodes 是 Set 类型
|
||
const selectedSet = this.nowSelectedCodes instanceof Set
|
||
? this.nowSelectedCodes
|
||
: new Set(this.nowSelectedCodes || []);
|
||
|
||
|
||
this.seatData.forEach(seat => {
|
||
const x = seat.x;
|
||
const y = seat.y;
|
||
|
||
// 根据缩放调整座位大小
|
||
const radius = 8 / scale;
|
||
// console.
|
||
// 检查座位是否被选中
|
||
const isSelected = selectedSet.has(seat.id) && seat.status === 1;
|
||
// console.log('selectedSet',selectedSet)
|
||
// console.log('seat',seat)
|
||
if (isSelected) {
|
||
// 绘制选中样式
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(x, y, radius + 1, 0, Math.PI * 2);
|
||
this.ctx.strokeStyle = '#FFD700'; // 金色边框
|
||
this.ctx.lineWidth = 2 / scale;
|
||
this.ctx.stroke();
|
||
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||
this.ctx.fillStyle = 'rgba(255, 215, 0, 0.3)'; // 半透明金色背景
|
||
this.ctx.fill();
|
||
|
||
// 绘制对勾
|
||
this.ctx.strokeStyle = '#fff';
|
||
this.ctx.lineWidth = 1 / scale;
|
||
this.ctx.lineCap = 'round';
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(x - 2.5, y);
|
||
this.ctx.lineTo(x - 0.5, y + 1.5);
|
||
this.ctx.lineTo(x + 2.5, y - 2);
|
||
this.ctx.stroke();
|
||
} else {
|
||
// 绘制普通座位
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||
this.ctx.fillStyle = seat.status === 1 ? '#4cd964' : '#dd524d';
|
||
this.ctx.fill();
|
||
}
|
||
|
||
// 不需要此段代码了
|
||
// if (scale > 0.5) {
|
||
// this.ctx.fillStyle = '#fff';
|
||
// this.ctx.font = `bold ${Math.max(10 / scale, 8)}px sans-serif`;
|
||
// this.ctx.textAlign = 'center';
|
||
// this.ctx.textBaseline = 'middle';
|
||
// this.ctx.fillText(seat.name, x, y);
|
||
// }
|
||
});
|
||
|
||
this.ctx.restore();
|
||
}
|
||
}
|
||
}
|
||
</script> |