|
|
@@ -1,12 +1,361 @@
|
|
|
<template>
|
|
|
- <canvas ref="canvasRef21" style="border:1px solid #ccc; cursor: grab;"></canvas>
|
|
|
+ <canvas ref="canvasRef" style="border:1px solid #ccc; cursor: grab;"></canvas>
|
|
|
</template>
|
|
|
-<script lang='ts' setup >
|
|
|
-import { ref, onMounted } from 'vue'
|
|
|
+<script lang='ts' setup>
|
|
|
+import { onMounted, PropType, ref, watch } from 'vue'
|
|
|
|
|
|
-const canvasRef21 = ref<HTMLCanvasElement>()
|
|
|
+const props = defineProps({
|
|
|
+ cardData: {
|
|
|
+ type: Object as PropType<API.Card>,
|
|
|
+ default: () => ({})
|
|
|
+ },
|
|
|
+ config: {
|
|
|
+ type: Object as PropType<API.CardJson21>,
|
|
|
+ default: () => ({})
|
|
|
+ }
|
|
|
+})
|
|
|
|
|
|
-</script>
|
|
|
-<style lang='less' scoped >
|
|
|
+const canvasRef = ref<HTMLCanvasElement | null>(null)
|
|
|
+const img = new window.Image()
|
|
|
+img.src = props.cardData.img
|
|
|
+
|
|
|
+// 矩形参数
|
|
|
+const rectWidth = 180
|
|
|
+const rectHeight = 240
|
|
|
+const rects = [] as { id: number, name: string, x: number, y: number, width: number, height: number }[]
|
|
|
+const rectGapX = 127 // 矩形间横向间隔
|
|
|
+const rectGapY = 146 // 矩形间纵向间隔
|
|
|
+const startX = 112 // 图片内第一个矩形的x偏移
|
|
|
+const startY = -56 // 图片内第一个矩形的y偏移
|
|
|
+
|
|
|
+// 生成矩形数据
|
|
|
+function generateRects () {
|
|
|
+ rects.length = 0
|
|
|
+ const firstRowGapX = 96
|
|
|
+ props.config.game_list.forEach(item => {
|
|
|
+ if (item.id < 3) {
|
|
|
+ for (let i = 0; i < 3; i++) {
|
|
|
+ rects.push({
|
|
|
+ id: item.id,
|
|
|
+ name: item.name,
|
|
|
+ x: startX + item.id * (rectWidth - 40 + firstRowGapX) + 1166,
|
|
|
+ y: startY + 116,
|
|
|
+ width: rectWidth - 40,
|
|
|
+ height: rectHeight - 100
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ rects.push({
|
|
|
+ id: item.id,
|
|
|
+ name: item.name,
|
|
|
+ x: startX + item.col * (rectWidth + rectGapX),
|
|
|
+ y: startY + item.row * (rectHeight + rectGapY),
|
|
|
+ width: rectWidth,
|
|
|
+ height: rectHeight
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 图片状态
|
|
|
+const state = {
|
|
|
+ scale: 0.3, // 初始缩放为0.5
|
|
|
+ minScale: 0.2,
|
|
|
+ maxScale: 5,
|
|
|
+ offsetX: 0,
|
|
|
+ offsetY: 0,
|
|
|
+ dragging: false,
|
|
|
+ lastX: 0,
|
|
|
+ lastY: 0,
|
|
|
+ selectedRectId: null as number | null
|
|
|
+}
|
|
|
+
|
|
|
+// 选择框状态
|
|
|
+const selection = {
|
|
|
+ selecting: false,
|
|
|
+ startX: 0,
|
|
|
+ startY: 0,
|
|
|
+ endX: 0,
|
|
|
+ endY: 0
|
|
|
+}
|
|
|
+
|
|
|
+// 多选集合
|
|
|
+const selectedRectIds = ref<number[]>([])
|
|
|
+
|
|
|
+const emits = defineEmits(['operation'])
|
|
|
+
|
|
|
+// 框选操作 and 点选操作
|
|
|
+const handleOperation = (ids: number[]) => {
|
|
|
+ emits('operation', { ids })
|
|
|
+}
|
|
|
+
|
|
|
+function centerImage () {
|
|
|
+ const canvas = canvasRef.value
|
|
|
+ if (!canvas) return
|
|
|
+ const imgW = img.width * state.scale
|
|
|
+ const imgH = img.height * state.scale
|
|
|
+ state.offsetX = (canvas.width - imgW) / 2
|
|
|
+ state.offsetY = (canvas.height - imgH) / 2
|
|
|
+}
|
|
|
+
|
|
|
+function resizeCanvas () {
|
|
|
+ const canvas = canvasRef.value
|
|
|
+ if (!canvas) return
|
|
|
+ canvas.width = window.innerWidth
|
|
|
+ canvas.height = window.innerHeight
|
|
|
+ centerImage()
|
|
|
+ draw()
|
|
|
+}
|
|
|
+
|
|
|
+// 绘制网格、图片和矩形
|
|
|
+function draw () {
|
|
|
+ const canvas = canvasRef.value
|
|
|
+ if (!canvas) return
|
|
|
+ const ctx = canvas.getContext('2d')!
|
|
|
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
|
+
|
|
|
+ // 画网格
|
|
|
+ const gridSize = 20
|
|
|
+ ctx.save()
|
|
|
+ ctx.strokeStyle = '#eee'
|
|
|
+ for (let x = 0; x < canvas.width; x += gridSize) {
|
|
|
+ ctx.beginPath()
|
|
|
+ ctx.moveTo(x, 0)
|
|
|
+ ctx.lineTo(x, canvas.height)
|
|
|
+ ctx.stroke()
|
|
|
+ }
|
|
|
+ for (let y = 0; y < canvas.height; y += gridSize) {
|
|
|
+ ctx.beginPath()
|
|
|
+ ctx.moveTo(0, y)
|
|
|
+ ctx.lineTo(canvas.width, y)
|
|
|
+ ctx.stroke()
|
|
|
+ }
|
|
|
+ ctx.restore()
|
|
|
+
|
|
|
+ // 画图片
|
|
|
+ if (img.complete) {
|
|
|
+ const imgW = img.width * state.scale
|
|
|
+ const imgH = img.height * state.scale
|
|
|
+ ctx.drawImage(img, state.offsetX, state.offsetY, imgW, imgH)
|
|
|
+
|
|
|
+ // 画矩形(随图片缩放和移动)
|
|
|
+ rects.forEach(rect => {
|
|
|
+ const rx = state.offsetX + rect.x * state.scale
|
|
|
+ const ry = state.offsetY + rect.y * state.scale
|
|
|
+ const rw = rect.width * state.scale
|
|
|
+ const rh = rect.height * state.scale
|
|
|
+ ctx.save()
|
|
|
+ ctx.lineWidth = 2
|
|
|
+ const isSelected = rect.id === state.selectedRectId || props.config.touch_key.find(item => item.id === rect.id)?.selected
|
|
|
+ ctx.strokeStyle = isSelected ? '#ff6600' : '#007bff'
|
|
|
+ ctx.globalAlpha = 0.5
|
|
|
+ ctx.fillStyle = isSelected ? '#ff660033' : '#007bff22'
|
|
|
+ ctx.fillRect(rx, ry, rw, rh)
|
|
|
+ ctx.globalAlpha = 1
|
|
|
+ ctx.strokeRect(rx, ry, rw, rh)
|
|
|
+ // 绘制序号
|
|
|
+ ctx.font = `bold ${Math.floor(rh / 1.7)}px sans-serif`
|
|
|
+ ctx.fillStyle = isSelected ? '#ff6600' : '#333'
|
|
|
+ ctx.textAlign = 'center'
|
|
|
+ ctx.textBaseline = 'middle'
|
|
|
+ ctx.fillText(rect.name, rx + rw / 2, ry + rh / 2)
|
|
|
+ ctx.restore()
|
|
|
+ })
|
|
|
|
|
|
+ // 画选择框
|
|
|
+ if (selection.selecting) {
|
|
|
+ ctx.save()
|
|
|
+ ctx.strokeStyle = '#1890ff'
|
|
|
+ ctx.setLineDash([6, 4])
|
|
|
+ ctx.lineWidth = 2
|
|
|
+ ctx.globalAlpha = 0.7
|
|
|
+ const x = Math.min(selection.startX, selection.endX)
|
|
|
+ const y = Math.min(selection.startY, selection.endY)
|
|
|
+ const w = Math.abs(selection.endX - selection.startX)
|
|
|
+ const h = Math.abs(selection.endY - selection.startY)
|
|
|
+ ctx.strokeRect(x, y, w, h)
|
|
|
+ ctx.restore()
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function onWheel (e: WheelEvent) {
|
|
|
+ if (!e.ctrlKey) return
|
|
|
+ e.preventDefault()
|
|
|
+ const { offsetX, offsetY, deltaY } = e
|
|
|
+ // 计算缩放中心
|
|
|
+ const prevScale = state.scale
|
|
|
+ if (deltaY < 0) {
|
|
|
+ state.scale = Math.min(state.maxScale, state.scale * 1.1)
|
|
|
+ } else {
|
|
|
+ state.scale = Math.max(state.minScale, state.scale / 1.1)
|
|
|
+ }
|
|
|
+ // 缩放时保持鼠标点下的内容不动
|
|
|
+ const scaleRatio = state.scale / prevScale
|
|
|
+ state.offsetX = offsetX - (offsetX - state.offsetX) * scaleRatio
|
|
|
+ state.offsetY = offsetY - (offsetY - state.offsetY) * scaleRatio
|
|
|
+ draw()
|
|
|
+}
|
|
|
+
|
|
|
+function onMouseDown (e: MouseEvent) {
|
|
|
+ // 判断是否按住shift进行多选框选
|
|
|
+ if (e.button === 0 && e.shiftKey) {
|
|
|
+ selection.selecting = true
|
|
|
+ selection.startX = e.offsetX
|
|
|
+ selection.startY = e.offsetY
|
|
|
+ selection.endX = e.offsetX
|
|
|
+ selection.endY = e.offsetY
|
|
|
+ draw()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 鼠标左键直接拖拽图片(不需要ctrl)
|
|
|
+ if (e.button === 0) {
|
|
|
+ state.dragging = true
|
|
|
+ state.lastX = e.clientX
|
|
|
+ state.lastY = e.clientY
|
|
|
+ const canvas = canvasRef.value
|
|
|
+ if (canvas) canvas.style.cursor = 'grabbing'
|
|
|
+ // 判断是否点在某个矩形内,若是则不做任何高亮处理
|
|
|
+ const x = e.offsetX
|
|
|
+ const y = e.offsetY
|
|
|
+ for (const rect of rects) {
|
|
|
+ const rx = state.offsetX + rect.x * state.scale
|
|
|
+ const ry = state.offsetY + rect.y * state.scale
|
|
|
+ const rw = rect.width * state.scale
|
|
|
+ const rh = rect.height * state.scale
|
|
|
+ if (x >= rx && x <= rx + rw && y >= ry && y <= ry + rh) {
|
|
|
+ // 点在矩形内,直接return,不清空高亮
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+let rafId: number | null = null
|
|
|
+
|
|
|
+function scheduleDraw () {
|
|
|
+ if (rafId !== null) return
|
|
|
+ rafId = requestAnimationFrame(() => {
|
|
|
+ draw()
|
|
|
+ rafId = null
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function onMouseMove (e: MouseEvent) {
|
|
|
+ if (selection.selecting) {
|
|
|
+ selection.endX = e.offsetX
|
|
|
+ selection.endY = e.offsetY
|
|
|
+ scheduleDraw()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!state.dragging) return
|
|
|
+ const dx = e.clientX - state.lastX
|
|
|
+ const dy = e.clientY - state.lastY
|
|
|
+ if (dx === 0 && dy === 0) return
|
|
|
+ state.offsetX += dx
|
|
|
+ state.offsetY += dy
|
|
|
+ state.lastX = e.clientX
|
|
|
+ state.lastY = e.clientY
|
|
|
+ scheduleDraw()
|
|
|
+}
|
|
|
+
|
|
|
+function onMouseUp (e?: MouseEvent) {
|
|
|
+ if (selection.selecting) {
|
|
|
+ // 计算选择框范围
|
|
|
+ const x1 = Math.min(selection.startX, selection.endX)
|
|
|
+ const y1 = Math.min(selection.startY, selection.endY)
|
|
|
+ const x2 = Math.max(selection.startX, selection.endX)
|
|
|
+ const y2 = Math.max(selection.startY, selection.endY)
|
|
|
+ // 判断哪些rect被框选
|
|
|
+ const selected: number[] = []
|
|
|
+ rects.forEach(rect => {
|
|
|
+ const rx = state.offsetX + rect.x * state.scale
|
|
|
+ const ry = state.offsetY + rect.y * state.scale
|
|
|
+ const rw = rect.width * state.scale
|
|
|
+ const rh = rect.height * state.scale
|
|
|
+ // 判断矩形中心点是否在选择框内
|
|
|
+ const cx = rx + rw / 2
|
|
|
+ const cy = ry + rh / 2
|
|
|
+ if (cx >= x1 && cx <= x2 && cy >= y1 && cy <= y2) {
|
|
|
+ selected.push(rect.id)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ selection.selecting = false
|
|
|
+ draw()
|
|
|
+ handleOperation(selected)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ state.dragging = false
|
|
|
+ const canvas = canvasRef.value
|
|
|
+ if (canvas) canvas.style.cursor = 'grab'
|
|
|
+}
|
|
|
+
|
|
|
+function onClick (e: MouseEvent) {
|
|
|
+ // 计算点击点在图片坐标系下的位置
|
|
|
+ const canvas = canvasRef.value
|
|
|
+ if (!canvas) return
|
|
|
+ // 如果正在批量框选(shift按下),不处理单选逻辑
|
|
|
+ if (e.shiftKey) return
|
|
|
+ const x = e.offsetX
|
|
|
+ const y = e.offsetY
|
|
|
+ let found: number | null = null
|
|
|
+ for (const rect of rects) {
|
|
|
+ const rx = state.offsetX + rect.x * state.scale
|
|
|
+ const ry = state.offsetY + rect.y * state.scale
|
|
|
+ const rw = rect.width * state.scale
|
|
|
+ const rh = rect.height * state.scale
|
|
|
+ if (x >= rx && x <= rx + rw && y >= ry && y <= ry + rh) {
|
|
|
+ found = rect.id
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ handleOperation([found!])
|
|
|
+ draw()
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ img.onload = () => {
|
|
|
+ resizeCanvas()
|
|
|
+ }
|
|
|
+ const canvas = canvasRef.value
|
|
|
+ if (!canvas) return
|
|
|
+ resizeCanvas()
|
|
|
+ window.addEventListener('resize', resizeCanvas)
|
|
|
+ canvas.addEventListener('wheel', onWheel, { passive: false })
|
|
|
+ canvas.addEventListener('mousedown', onMouseDown)
|
|
|
+ window.addEventListener('mousemove', onMouseMove)
|
|
|
+ window.addEventListener('mouseup', onMouseUp)
|
|
|
+ canvas.addEventListener('click', onClick)
|
|
|
+})
|
|
|
+
|
|
|
+const delSelectedRectIds = () => draw()
|
|
|
+
|
|
|
+// watch(
|
|
|
+// () => props.config,
|
|
|
+// () => {
|
|
|
+// const ids = props.config.touch_key.filter(item => item.music_name).map(_ => _.id)
|
|
|
+// selectedRectIds.value = ids
|
|
|
+// generateRects()
|
|
|
+// draw()
|
|
|
+// }, {
|
|
|
+// deep: true
|
|
|
+// }
|
|
|
+// )
|
|
|
+
|
|
|
+defineExpose({
|
|
|
+ delSelectedRectIds
|
|
|
+})
|
|
|
+
|
|
|
+</script>
|
|
|
+<style lang='less' scoped>
|
|
|
+canvas {
|
|
|
+ background: #fff;
|
|
|
+ display: block;
|
|
|
+ position: fixed;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
</style>
|