lvkun996 9 months ago
parent
commit
ae0238e1b7

+ 2 - 1
config/proxy.ts

@@ -6,7 +6,8 @@
 module.exports = {
   dev: {
     '/api': {
-      target: 'http://192.168.1.94:9098/xiaodou-ai-admin/admin',
+      // target: 'http://192.168.1.94:9098/xiaodou-ai-admin/admin',
+      target: 'http://localhost:9098/xiaodou-ai-admin/admin',
       changeOrigin: true,
       pathRewrite: { '^/api': '' }
     }

+ 42 - 2
src/controller/CardController.ts

@@ -70,12 +70,12 @@ export class CardController {
    * selected字段代表是否选择, 存在music_name是 selected是true 否则为false
    *
    */
-  static async cardJsonById (id: string) {
+  static async card5JsonById (id: string) {
     const { data } = await getCardJsonById(id)
 
     const dataJson = JSON.parse(data) as API.CardJson
 
-    dataJson.touch_key = dataJson.touch_key!.map((item, index) => {
+    (dataJson as API.CardJson).touch_key = (dataJson as API.CardJson).touch_key.map((item, index) => {
       if ('music_name' in item) {
         return {
           ...item,
@@ -97,6 +97,25 @@ export class CardController {
     return { data: dataJson }
   }
 
+  static async card21JsonById (id:string) {
+    const { data } = await getCardJsonById(id)
+
+    const dataJson = JSON.parse(data) as API.CardJson
+
+    dataJson.game_list = dataJson.game_list.map((item) => {
+      return {
+        ...item,
+        selected: false,
+        ok_key: CardController.createRect(),
+        err_key: CardController.createRect(),
+        ok_key_voice: CardController.createRect(),
+        err_key_voice: CardController.createRect()
+      }
+    })
+
+    return { data: dataJson }
+  }
+
   static createRectByIndex (index: number) {
     if (index < 3) {
       return {
@@ -117,4 +136,25 @@ export class CardController {
       }
     }
   }
+
+  static createRect () {
+    const obj = [] as any[]
+    for (let row = 1; row < 7; row++) {
+      for (let col = 1; col < 7; col++) {
+        const prefix = CardController.cardButtonRowMap.get(row)!
+        const suffix = col
+        obj.push({
+          id: col + (row - 1) * 6,
+          name: prefix + suffix,
+          row: row,
+          col: suffix - 1,
+          music_name: '',
+          is_break: 1,
+          value: '',
+          select: false
+        })
+      }
+    }
+    return obj
+  }
 }

+ 355 - 6
src/pages/card/components/card-template-21.vue

@@ -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>

+ 2 - 5
src/pages/card/components/card-template.vue

@@ -17,7 +17,6 @@ const props = defineProps({
 
 const canvasRef = ref<HTMLCanvasElement | null>(null)
 const img = new window.Image()
-// img.src = require('@/assets/template.png')
 img.src = props.cardData.img
 
 // 矩形参数
@@ -86,7 +85,7 @@ const selectedRectIds = ref<number[]>([])
 const emits = defineEmits(['operation'])
 
 // 框选操作 and 点选操作
-const handleOperation = (ids: string) => {
+const handleOperation = (ids: number[]) => {
   emits('operation', { ids })
 }
 
@@ -310,9 +309,7 @@ function onClick (e: MouseEvent) {
       break
     }
   }
-
-  // props.config.touch_key[found].selected = !props.config.touch_key[found].selected
-  handleOperation([found])
+  handleOperation([found!])
   draw()
 }
 

+ 26 - 4
src/pages/card/index.vue

@@ -26,12 +26,21 @@
   </div>
 
   <CardTemplate
+    v-if="cardInfo.card_type == 5"
     ref="cardTemplateDom"
     @operation="handleOperation"
     :cardData="cardInfo"
     :config="cardJson"
   />
 
+  <CardTemplate21
+    v-else
+    ref="cardTemplateDom21"
+    @operation="handleOperation"
+    :cardData="cardInfo"
+    :config="cardJson21"
+  />
+
   <!-- 右侧配置区域 -->
   <a-drawer
     :width="500"
@@ -90,6 +99,7 @@
 <script lang='ts'  setup >
 import { InfoCircleOutlined } from '@ant-design/icons-vue'
 import CardTemplate from './components/card-template.vue'
+import CardTemplate21 from './components/card-template-21.vue'
 import { ref, onMounted, reactive } from 'vue'
 import ConfigCard from './components/config-card.vue'
 import CardDefault from './components/card-default.vue'
@@ -132,8 +142,15 @@ const cardJson = reactive<API.CardJson>({
   slide_knob: null
 })
 
+const cardJson21 = reactive<API.CardJson21>({
+  header: null,
+  game_list: []
+})
+
 const cardTemplateDom = ref()
 
+const cardTemplateDom21 = ref()
+
 const onClose = () => openState.configCardOpen = false
 
 const handleVerify = (verifyKey: 'buttonIsValid' | 'cardIsValid', value: boolean) => {
@@ -218,8 +235,8 @@ const handleOperation = ({ ids }: {ids: number[]}) => {
   })
 }
 
-const getCardJson = async () => {
-  const { data } = await CardController.cardJsonById(cardInfo.id!)
+const getCardJson5 = async () => {
+  const { data } = await CardController.card5JsonById(cardInfo.id!)
   cardJson.header = data.header
   cardJson.slide_knob = data.slide_knob
   cardJson.touch_key = data.touch_key
@@ -238,13 +255,18 @@ const getCardJson = async () => {
       isValids.cardIsValid = false
     }
   })
-  console.log('data:', data)
+}
+
+const getCardJson21 = async () => {
+  const { data } = await CardController.card21JsonById(cardInfo.id!)
+  cardJson21.header = data.header
+  cardJson21.game_list = data.game_list
 }
 
 function onBack () { window.history.back() }
 
 onMounted(() => {
-  getCardJson()
+  cardInfo.card_type == 5 ? getCardJson5() : getCardJson21()
 })
 </script>
 <style lang='less' scoped >

+ 2 - 1
src/pages/card/preview.vue

@@ -36,7 +36,8 @@ const handleClick = (card: API.Card) => {
       id: card.id,
       name: card.name,
       img: card.img,
-      parentId: card.parentId
+      parentId: card.parentId,
+      card_type: Number(card.page) % 2 === 0 ? 21 : 5
     }
   })
 }

+ 15 - 5
src/typeing.d.ts

@@ -43,6 +43,7 @@ declare namespace API {
     'parentId': string,
     'img': string,
     'thumbnail': string
+    card_type: 5 | 21
   }
 
   interface Audio {
@@ -60,6 +61,15 @@ declare namespace API {
 
   type BaseItem = {'music_name': string, 'is_break': is_break }
 
+  interface CardDot {
+    name: string,
+    col: number,
+    row: number
+    selected: boolean
+    id: number
+    music_name: number
+  }
+
   interface CardJson {
     header: {
       card_type: 5 | 21
@@ -122,7 +132,7 @@ declare namespace API {
         is_break: number
       }
     }
-    touch_key?: {
+    touch_key: {
       music_name: string,
       is_break: number,
       id: number,
@@ -304,10 +314,10 @@ declare namespace API {
                   'err': string,
                   'eb': number
               },
-              'ok_key': ({ 'value': number} & BaseItem)[] // 正确的键位key
-              'err_key': ({ 'value': number} & BaseItem)[], // 错误的键位key
-              'ok_key_voice': { 'value': number} & BaseItem // 正确的声音
-              'err_key_voice': { 'value': number} & BaseItem // 点击错误的声音
+              'ok_key': ({ 'value': number} & BaseItem & CardDot)[] // 正确的键位key
+              'err_key': ({ 'value': number} & BaseItem & CardDot)[], // 错误的键位key
+              'ok_key_voice': { 'value': number} & BaseItem & CardDot // 正确的声音
+              'err_key_voice': { 'value': number} & BaseItem & CardDot // 点击错误的声音
             }[]
         }
     ]