| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- <template>
- <canvas ref="canvasRef" style="border:1px solid #ccc; cursor: grab;"></canvas>
- </template>
- <script lang='ts' setup>
- import { onMounted, PropType, ref, watch } from 'vue'
- const props = defineProps({
- cardData: {
- type: Object as PropType<API.Card>,
- default: () => ({})
- },
- config: {
- type: Object as PropType<API.CardJson>,
- default: () => ({})
- }
- })
- const canvasRef = ref<HTMLCanvasElement | null>(null)
- const img = new window.Image()
- img.src = props.cardData.img
- type Rect = { id: number, name: string, x: number, y: number, width: number, height: number, music_name: string, selected: boolean }
- // 矩形参数
- const rectWidth = 180
- const rectHeight = 240
- const rects = [] as Rect[]
- const rectGapX = 127 // 矩形间横向间隔
- const rectGapY = 146 // 矩形间纵向间隔
- const startX = 112 // 图片内第一个矩形的x偏移
- const startY = -56 // 图片内第一个矩形的y偏移
- // 未选择 选中了但是没有音频 选中了有音频 三种模式的矩形样式
- const rectStyles = {
- unselected: {
- fillStyle: '007bff22',
- strokeStyle: '#007bff',
- lineWidth: 2,
- lineDash: []
- },
- selectedNoAudio: {
- fillStyle: '007bff22',
- strokeStyle: '#ff0000',
- lineWidth: 2,
- lineDash: [4, 6]
- },
- selectedWithAudio: {
- fillStyle: 'ff660033',
- strokeStyle: '#ff6600',
- lineWidth: 2,
- lineDash: []
- }
- }
- const generateRectStyle = (rect: Rect) => {
- if (rect.selected) {
- return rect.music_name ? rectStyles.selectedWithAudio : rectStyles.selectedNoAudio
- }
- return rectStyles.unselected
- }
- // 生成矩形数据
- function generateRects () {
- rects.length = 0
- const firstRowGapX = 96
- props.config.touch_key.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,
- music_name: item.music_name || '',
- selected: item.selected
- })
- }
- } 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,
- music_name: item.music_name || '',
- selected: item.selected
- })
- }
- })
- }
- // 图片状态
- 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)
- console.log('rects:', rects)
- // 画矩形(随图片缩放和移动)
- 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.selected
- ctx.strokeStyle = isSelected ? '#ff6600' : '#007bff'
- ctx.globalAlpha = 0.7
- ctx.fillStyle = isSelected ? '#ff660033' : '#3374e5dd'
- ctx.fillRect(rx, ry, rw, rh)
- ctx.globalAlpha = 1
- ctx.setLineDash([]) // 确保边框为实线
- ctx.strokeRect(rx, ry, rw, rh)
- // 绘制序号
- ctx.font = `bold ${Math.floor(rh / 1.7)}px sans-serif`
- ctx.strokeStyle = isSelected ? '#ff6600' : '#333'
- ctx.lineWidth = 1
- ctx.textAlign = 'center'
- ctx.textBaseline = 'middle'
- // 根据选中状态设置线条样式
- ctx.setLineDash(isSelected ? [] : []) // 选中时使用虚线,否则实线
- if (rect.id < 3) {
- ctx.strokeText(rect.name, rx + rw / 2, ry + rh / 2)
- } else {
- ctx.strokeText(rect.name, rx + rw / 2, ry + rh / 4)
- }
- ctx.restore()
- if (rect.id >= 3) {
- ctx.font = `bold ${Math.floor(rh / 1.7)}px sans-serif`
- ctx.strokeStyle = '#333'
- // ctx.lineWidth = 1
- ctx.textAlign = 'center'
- ctx.textBaseline = 'middle'
- // ctx.setLineDash([6, 4]) // 设置为实线
- ctx.strokeText((rect.id + 1).toString(), rx + rw / 2, ry + rh / 1.3)
- 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()
- const reload = () => 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,
- reload
- })
- </script>
- <style lang='less' scoped>
- canvas {
- background: #fff;
- display: block;
- position: fixed;
- left: 0;
- top: 0;
- width: 100vw;
- height: 100vh;
- z-index: 10;
- }
- </style>
|