card-template-21.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. <template>
  2. <canvas ref="canvasRef" style="border:1px solid #ccc; cursor: grab;"></canvas>
  3. </template>
  4. <script lang='ts' setup>
  5. import { onMounted, PropType, ref, watch } from 'vue'
  6. import { CardController } from '@/controller'
  7. const props = defineProps({
  8. cardData: {
  9. type: Object as PropType<API.Card>,
  10. default: () => ({})
  11. },
  12. config: {
  13. type: Object as PropType<API.CardJson21>,
  14. default: () => ({})
  15. },
  16. highlightedButtonKey: {
  17. type: String,
  18. default: ''
  19. },
  20. openState: {
  21. type: Object as PropType<{
  22. defaultConfig21Open: boolean
  23. }>,
  24. default: () => ({
  25. defaultConfig21Open: false
  26. })
  27. }
  28. })
  29. const cardButtonRowMap = new Map([
  30. [1, 'A'],
  31. [2, 'B'],
  32. [3, 'C'],
  33. [4, 'D'],
  34. [5, 'E'],
  35. [6, 'F']
  36. ])
  37. const canvasRef = ref<HTMLCanvasElement | null>(null)
  38. const img = new window.Image()
  39. img.src = props.cardData.img
  40. // 矩形参数
  41. const rectWidth = 180
  42. const rectHeight = 240
  43. const rects = [] as { id: number, name: string, x: number, y: number, width: number, height: number }[]
  44. const rectGapX = 127 // 矩形间横向间隔
  45. const rectGapY = 146 // 矩形间纵向间隔
  46. const startX = 112 // 图片内第一个矩形的x偏移
  47. const startY = -56 // 图片内第一个矩形的y偏移
  48. // 生成矩形数据
  49. function generateRects () {
  50. rects.length = 0
  51. const firstRowGapX = 96
  52. for (let i = 0; i < 3; i++) {
  53. rects.push({
  54. id: i,
  55. name: CardController.cardButtonTitleMap.get(i)!,
  56. x: startX + i * (rectWidth - 40 + firstRowGapX) + 1166,
  57. y: startY + 116,
  58. width: rectWidth - 40,
  59. height: rectHeight - 100
  60. })
  61. }
  62. for (let row = 1; row < 7; row++) {
  63. for (let col = 1; col < 7; col++) {
  64. rects.push({
  65. id: row * 6 + col,
  66. name: cardButtonRowMap.get(row)! + col,
  67. x: startX + (col - 1) * (rectWidth + rectGapX),
  68. y: startY + row * (rectHeight + rectGapY),
  69. width: rectWidth,
  70. height: rectHeight
  71. })
  72. }
  73. }
  74. }
  75. // 生成顶部的配置按钮数据
  76. function generateTopRects () {
  77. const firstRowGapX = 96
  78. for (let i = 0; i < 3; i++) {
  79. rects.push({
  80. id: i,
  81. name: CardController.cardButtonTitleMap.get(i)!,
  82. x: startX + i * (rectWidth - 40 + firstRowGapX) + 1166,
  83. y: startY + 116,
  84. width: rectWidth - 40,
  85. height: rectHeight - 100
  86. })
  87. }
  88. }
  89. // generateTopRects()
  90. // 图片状态
  91. const state = {
  92. scale: 0.2, // 初始缩放为0.5
  93. minScale: 0.2,
  94. maxScale: 5,
  95. offsetX: 0,
  96. offsetY: 0,
  97. dragging: false,
  98. lastX: 0,
  99. lastY: 0,
  100. selectedRectId: null as number | null
  101. }
  102. // 选择框状态
  103. const selection = {
  104. selecting: false,
  105. startX: 0,
  106. startY: 0,
  107. endX: 0,
  108. endY: 0
  109. }
  110. const emits = defineEmits(['operation'])
  111. function centerImage () {
  112. const canvas = canvasRef.value
  113. if (!canvas) return
  114. const imgW = img.width * state.scale
  115. const imgH = img.height * state.scale
  116. state.offsetX = (canvas.width - imgW) / 2 - window.innerWidth / 4// 额外偏移量
  117. state.offsetY = (canvas.height - imgH) / 2
  118. }
  119. function resizeCanvas () {
  120. const canvas = canvasRef.value
  121. if (!canvas) return
  122. canvas.width = window.innerWidth
  123. canvas.height = window.innerHeight
  124. centerImage()
  125. draw()
  126. }
  127. // 绘制网格、图片和矩形
  128. function draw () {
  129. const canvas = canvasRef.value
  130. if (!canvas) return
  131. const ctx = canvas.getContext('2d')!
  132. ctx.clearRect(0, 0, canvas.width, canvas.height)
  133. // 画网格
  134. const gridSize = 20
  135. ctx.save()
  136. ctx.strokeStyle = '#eee'
  137. for (let x = 0; x < canvas.width; x += gridSize) {
  138. ctx.beginPath()
  139. ctx.moveTo(x, 0)
  140. ctx.lineTo(x, canvas.height)
  141. ctx.stroke()
  142. }
  143. for (let y = 0; y < canvas.height; y += gridSize) {
  144. ctx.beginPath()
  145. ctx.moveTo(0, y)
  146. ctx.lineTo(canvas.width, y)
  147. ctx.stroke()
  148. }
  149. ctx.restore()
  150. // 画图片
  151. if (img.complete) {
  152. const imgW = img.width * state.scale
  153. const imgH = img.height * state.scale
  154. // 根据openState.defaultConfig21Open调整图片位置
  155. const offsetX = props.openState.defaultConfig21Open ? state.offsetX + canvas.width * 0.1 : state.offsetX
  156. ctx.drawImage(img, offsetX, state.offsetY, imgW, imgH)
  157. // 获取图片的实际偏移量
  158. // const offsetX = props.openState.defaultConfig21Open ? state.offsetX + canvas.width * 0.1 : state.offsetX
  159. // 画矩形(随图片缩放和移动)
  160. rects.forEach(rect => {
  161. const rx = offsetX + rect.x * state.scale
  162. const ry = state.offsetY + rect.y * state.scale
  163. const rw = rect.width * state.scale
  164. const rh = rect.height * state.scale
  165. ctx.save()
  166. ctx.lineWidth = 2
  167. const isSelected = rect.id === state.selectedRectId
  168. ctx.strokeStyle = isSelected ? '#ff6600' : '#007bff'
  169. ctx.globalAlpha = 0.5
  170. ctx.fillStyle = isSelected ? '#ff660033' : '#007bff22'
  171. ctx.fillRect(rx, ry, rw, rh)
  172. ctx.globalAlpha = 1
  173. ctx.strokeRect(rx, ry, rw, rh)
  174. // 绘制序号
  175. ctx.font = `bold ${Math.floor(rh / 1.7)}px sans-serif`
  176. ctx.fillStyle = isSelected ? '#ff6600' : '#333'
  177. ctx.textAlign = 'center'
  178. ctx.textBaseline = 'middle'
  179. ctx.fillText(rect.name, rx + rw / 2, ry + rh / 2)
  180. ctx.restore()
  181. })
  182. // 画选择框
  183. if (selection.selecting) {
  184. ctx.save()
  185. ctx.strokeStyle = '#1890ff'
  186. ctx.setLineDash([6, 4])
  187. ctx.lineWidth = 2
  188. ctx.globalAlpha = 0.7
  189. const x = Math.min(selection.startX, selection.endX)
  190. const y = Math.min(selection.startY, selection.endY)
  191. const w = Math.abs(selection.endX - selection.startX)
  192. const h = Math.abs(selection.endY - selection.startY)
  193. ctx.strokeRect(x, y, w, h)
  194. ctx.restore()
  195. }
  196. }
  197. }
  198. function onWheel (e: WheelEvent) {
  199. if (!e.ctrlKey) return
  200. e.preventDefault()
  201. const { offsetX, offsetY, deltaY } = e
  202. // 计算缩放中心
  203. const prevScale = state.scale
  204. if (deltaY < 0) {
  205. state.scale = Math.min(state.maxScale, state.scale * 1.1)
  206. } else {
  207. state.scale = Math.max(state.minScale, state.scale / 1.1)
  208. }
  209. // 缩放时保持鼠标点下的内容不动
  210. const scaleRatio = state.scale / prevScale
  211. state.offsetX = offsetX - (offsetX - state.offsetX) * scaleRatio
  212. state.offsetY = offsetY - (offsetY - state.offsetY) * scaleRatio
  213. draw()
  214. }
  215. function onMouseDown (e: MouseEvent) {
  216. // 判断是否按住shift进行多选框选
  217. if (e.button === 0 && e.shiftKey) {
  218. selection.selecting = true
  219. selection.startX = e.offsetX
  220. selection.startY = e.offsetY
  221. selection.endX = e.offsetX
  222. selection.endY = e.offsetY
  223. draw()
  224. return
  225. }
  226. // 鼠标左键直接拖拽图片(不需要ctrl)
  227. if (e.button === 0) {
  228. state.dragging = true
  229. state.lastX = e.clientX
  230. state.lastY = e.clientY
  231. const canvas = canvasRef.value
  232. if (canvas) canvas.style.cursor = 'grabbing'
  233. // 判断是否点在某个矩形内,若是则不做任何高亮处理
  234. const x = e.offsetX
  235. const y = e.offsetY
  236. for (const rect of rects) {
  237. const rx = state.offsetX + rect.x * state.scale
  238. const ry = state.offsetY + rect.y * state.scale
  239. const rw = rect.width * state.scale
  240. const rh = rect.height * state.scale
  241. if (x >= rx && x <= rx + rw && y >= ry && y <= ry + rh) {
  242. // 点在矩形内,直接return,不清空高亮
  243. return
  244. }
  245. }
  246. }
  247. }
  248. let rafId: number | null = null
  249. function scheduleDraw () {
  250. if (rafId !== null) return
  251. rafId = requestAnimationFrame(() => {
  252. draw()
  253. rafId = null
  254. })
  255. }
  256. function onMouseMove (e: MouseEvent) {
  257. if (selection.selecting) {
  258. selection.endX = e.offsetX
  259. selection.endY = e.offsetY
  260. scheduleDraw()
  261. return
  262. }
  263. if (!state.dragging) return
  264. const dx = e.clientX - state.lastX
  265. const dy = e.clientY - state.lastY
  266. if (dx === 0 && dy === 0) return
  267. state.offsetX += dx
  268. state.offsetY += dy
  269. state.lastX = e.clientX
  270. state.lastY = e.clientY
  271. scheduleDraw()
  272. }
  273. function onMouseUp (e?: MouseEvent) {
  274. if (selection.selecting) {
  275. // 计算选择框范围
  276. const x1 = Math.min(selection.startX, selection.endX)
  277. const y1 = Math.min(selection.startY, selection.endY)
  278. const x2 = Math.max(selection.startX, selection.endX)
  279. const y2 = Math.max(selection.startY, selection.endY)
  280. const canvas = canvasRef.value
  281. if (!canvas) return
  282. // 获取图片的实际偏移量
  283. const offsetX = props.openState.defaultConfig21Open ? state.offsetX + canvas.width * 0.1 : state.offsetX
  284. // 判断哪些rect被框选
  285. const selected: number[] = []
  286. rects.forEach(rect => {
  287. const rx = offsetX + rect.x * state.scale
  288. const ry = state.offsetY + rect.y * state.scale
  289. const rw = rect.width * state.scale
  290. const rh = rect.height * state.scale
  291. // 判断矩形中心点是否在选择框内
  292. const cx = rx + rw / 2
  293. const cy = ry + rh / 2
  294. if (cx >= x1 && cx <= x2 && cy >= y1 && cy <= y2) {
  295. selected.push(rect.id)
  296. }
  297. })
  298. selection.selecting = false
  299. draw()
  300. // if (selected.length > 0) handleOperation(selected)
  301. return
  302. }
  303. state.dragging = false
  304. const canvas = canvasRef.value
  305. if (canvas) canvas.style.cursor = 'grab'
  306. }
  307. function onClick (e: MouseEvent) {
  308. // 计算点击点在图片坐标系下的位置
  309. const canvas = canvasRef.value
  310. if (!canvas) return
  311. // 如果正在批量框选(shift按下),不处理单选逻辑
  312. if (e.shiftKey) return
  313. const x = e.offsetX
  314. const y = e.offsetY
  315. // 获取图片的实际偏移量
  316. const offsetX = props.openState.defaultConfig21Open ? state.offsetX + canvas.width * 0.1 : state.offsetX
  317. let found: number | null = null
  318. for (const rect of rects) {
  319. const rx = offsetX + rect.x * state.scale
  320. const ry = state.offsetY + rect.y * state.scale
  321. const rw = rect.width * state.scale
  322. const rh = rect.height * state.scale
  323. if (x >= rx && x <= rx + rw && y >= ry && y <= ry + rh) {
  324. found = rect.id
  325. break
  326. }
  327. }
  328. // found && handleOperation([found])
  329. draw()
  330. }
  331. onMounted(() => {
  332. generateRects()
  333. img.onload = () => {
  334. resizeCanvas()
  335. }
  336. const canvas = canvasRef.value
  337. if (!canvas) return
  338. resizeCanvas()
  339. window.addEventListener('resize', resizeCanvas)
  340. canvas.addEventListener('wheel', onWheel, { passive: false })
  341. canvas.addEventListener('mousedown', onMouseDown)
  342. window.addEventListener('mousemove', onMouseMove)
  343. window.addEventListener('mouseup', onMouseUp)
  344. canvas.addEventListener('click', onClick)
  345. })
  346. const delSelectedRectIds = () => draw()
  347. watch(() => props.highlightedButtonKey, (newKey) => {
  348. if (!newKey) {
  349. state.selectedRectId = null
  350. } else {
  351. const foundRect = rects.find(rect => rect.name === newKey)
  352. state.selectedRectId = foundRect ? foundRect.id : null
  353. }
  354. draw()
  355. })
  356. // 监听openState.defaultConfig21Open的变化,重新绘制画布
  357. watch(() => props.openState.defaultConfig21Open, () => {
  358. draw()
  359. })
  360. // watch(
  361. // () => props.config,
  362. // () => {
  363. // const ids = props.config.touch_key.filter(item => item.music_name).map(_ => _.id)
  364. // selectedRectIds.value = ids
  365. // generateRects()
  366. // draw()
  367. // }, {
  368. // deep: true
  369. // }
  370. // )
  371. const showReactByType = (type: 'content' | 'top') => {
  372. type === 'content' ? generateRects() : generateTopRects()
  373. draw()
  374. }
  375. defineExpose({
  376. delSelectedRectIds,
  377. showReactByType
  378. })
  379. </script>
  380. <style lang='less' scoped>
  381. canvas {
  382. background: #fff;
  383. display: block;
  384. position: fixed;
  385. left: 0;
  386. top: 0;
  387. width: 100vw;
  388. height: 100vh;
  389. z-index: 10;
  390. }
  391. </style>