lvkun996 hai 8 meses
pai
achega
dcac422334

+ 2 - 2
config/proxy.ts

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

+ 68 - 17
src/controller/CardController.ts

@@ -101,23 +101,9 @@ export class CardController {
   static async card21JsonById (id:string) {
     const { data } = await getCardJsonById(id)
 
-    const dataJson = JSON.parse(data) as API.CardJson
-
-    // 思路,在ok_key里记录高亮的id === ? 就是value,用value和卡片模版对比,来进行高亮
-    // 直接点击对应的ok_key 或者 err_key, 再点击对应的模版按钮来往里push value,没点击一个选择一个音频
-
-    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 }
+    const dataJson = JSON.parse(data) as API.CardJson21
+    console.log('卡片是21时返回的参数', dataJson)
+    return { data: CardController.itemsToSteps(dataJson) }
   }
 
   static createRectByIndex (index: number) {
@@ -161,4 +147,69 @@ export class CardController {
     }
     return obj
   }
+
+  // 把json中的steps 解析到 items下其他项
+  static stepsToItems (data: API.CardJson21) {
+    console.log('解析steps', data)
+
+    const newData = JSON.parse(JSON.stringify(data))
+
+    newData.game_list.forEach((game: any) => {
+      if (game.rule === '7' || game.rule === 7) {
+        game.rule = 7
+        if (game.items && Array.isArray(game.items)) {
+          game.items.forEach((item: any) => {
+            if (item.steps && Array.isArray(item.steps)) {
+              item.steps.forEach((step: any, index: number) => {
+                if (step.button_key) {
+                  // 更新 ok_key
+                  item.ok_key.push({
+                    value: step.button_key,
+                    music_name: step.audio_name,
+                    is_break: 1,
+                    select: true
+                  })
+                }
+              })
+              delete item.steps
+            }
+          })
+        }
+      }
+    })
+
+    return newData
+  }
+
+  // 把json中items下其他项解析到steps
+  static itemsToSteps (data: API.CardJson21) {
+    const newData = JSON.parse(JSON.stringify(data))
+
+    newData.game_list.forEach((game: any) => {
+      if (game.rule === 7) {
+        if (game.items && Array.isArray(game.items)) {
+          game.items.forEach((item: any) => {
+            const newSteps: { button_key: number, audio_name: string, id: number }[] = []
+            if (item.ok_key && Array.isArray(item.ok_key)) {
+              // 根据 ok_key 中的 value (button_key) 排序以确保顺序
+              const sortedOkKey = [...item.ok_key].sort((a, b) => a.value - b.value)
+
+              sortedOkKey.forEach((keyItem: any) => {
+                if (keyItem.value !== undefined && keyItem.music_name !== undefined) {
+                  newSteps.push({
+                    button_key: keyItem.value,
+                    audio_name: keyItem.music_name,
+                    id: keyItem.id // 使用 keyItem 的 id 作为 step 的 id
+                  })
+                }
+              })
+            }
+            item.steps = newSteps
+          })
+        }
+      }
+    })
+
+    return newData
+  }
 }

+ 142 - 0
src/pages/card/components/card-default-21.vue

@@ -0,0 +1,142 @@
+<template>
+  <div class="card-default" >
+     <a-form
+        :model="formState"
+        name="basic"
+        :label-col="{ span: 6 }"
+        :wrapper-col="{ span: 16 }"
+        autocomplete="off"
+        ref="formDom"
+      >
+        <template
+          v-for="(item, index) in formState.default"
+          :key="item.title"
+        >
+          <a-card
+            :title="item.laebl"
+            style="margin-bottom: 16px"
+          >
+              <a-form-item
+                label="音频名称"
+                :name="['default', index, 'music_name']"
+                :rules="[{ required: true, message: '请选择音频' }]"
+              >
+                <SelectAudio
+                  v-model="item.music_name"
+                  placeholder="请选择音频"
+                  @search="handleSearch"
+                  @change="handleChange"
+                  @blur="searchKey = ''"
+                />
+              </a-form-item>
+
+              <a-form-item
+                label="可中止"
+              >
+                <a-select v-model:value="item.is_break">
+                  <a-select-option :value="1">是</a-select-option>
+                  <a-select-option :value="0">否</a-select-option>
+                </a-select>
+              </a-form-item>
+          </a-card>
+        </template>
+      </a-form>
+  </div>
+</template>
+<script lang='ts'  setup >
+import { reactive, watch, onMounted, ref, defineProps, watchEffect } from 'vue'
+import { CardController } from '@/controller/index'
+import SelectAudio from './select-audio-new.vue'
+import { Form } from 'ant-design-vue'
+
+const useForm = Form.useForm
+
+interface FormState {
+  [key: string]: string | number
+  'music_name': string
+  'is_break': number // 1 | 0
+  keyName: string
+  laebl: string,
+}
+
+const props = defineProps<{
+  header: API.CardJson21.header,
+}>()
+
+const formDom = ref()
+
+const formState = reactive<{default: FormState[]}>({
+  default: [
+    { title: '', keyName: 'title', music_name: '', is_break: 1, laebl: '标题音频' },
+    { card_insert: '', keyName: 'card_insert', music_name: '', is_break: 1, laebl: '卡片插入音频' },
+    { card_remove: '', keyName: 'card_remove', music_name: '', is_break: 1, laebl: '卡片移除音频' },
+    { finish: '', keyName: 'finish', music_name: '', is_break: 1, laebl: '完成音频' }
+  ]
+})
+
+const searchKey = ref('')
+
+// 当前表单的验证状态 为true的时候 父组件校验通过
+const isValid = ref(false)
+
+const handleChange = () => searchKey.value = ''
+
+let debounceTimer: ReturnType<typeof setTimeout> | null = null
+
+const handleSearch = (text: string) => {
+  if (debounceTimer) clearTimeout(debounceTimer)
+  debounceTimer = setTimeout(() => {
+    searchKey.value = text
+  }, 300)
+}
+
+const generateDefaultJson = async () => {
+  return new Promise((resolve, reject) => {
+    formDom.value.validate().then(res => {
+      console.log('then', res)
+      isValid.value = true
+      resolve({
+        status: 200,
+        data: formState.default
+      })
+    }).catch(err => {
+      console.log('catch', err)
+      isValid.value = false
+      resolve({
+        status: 0,
+        data: formState.default
+      })
+    })
+  })
+}
+
+watch(
+  () => props.header,
+  (newHeader) => {
+    console.log('newHeader:', newHeader)
+
+    formState.default.forEach(item => {
+      item.music_name = newHeader[item.keyName].music_name
+      item.title = newHeader[item.keyName].music_name
+      item.is_break = newHeader[item.keyName].is_break
+    })
+  },
+  {
+    immediate: true
+  }
+)
+
+defineExpose({
+  formState,
+  generateDefaultJson,
+  isValid
+})
+
+</script>
+<style lang='less' scoped >
+.card-default {
+  height: 700px;
+  overflow: hidden;
+  overflow-y: scroll;
+}
+</style>

+ 29 - 80
src/pages/card/components/card-default.vue

@@ -21,29 +21,13 @@
                 :name="['default', index, 'music_name']"
                 :rules="[{ required: true, message: '请选择音频' }]"
               >
-               <a-select
-                  v-model:value="item.music_name"
-                  show-search
-                  placeholder="输入或者搜索音频名"
-                  style="width: 200px"
-                  :default-active-first-option="false"
-                  :show-arrow="false"
-                  :filter-option="false"
-                  :not-found-content="null"
-                  :options="audioList"
-                  @blur="searchKey = ''"
+                <SelectAudio
+                  v-model="item.music_name"
+                  placeholder="请选择音频"
                   @search="handleSearch"
                   @change="handleChange"
-                  :fieldNames="{ label: 'title', value: 'title'}"
-                >
-                  <template #option="{title, url, state, id}"  >
-                    <SelectAudio
-                      :audio='{title, url, state, id}'
-                      @play="playAudio"
-                      @paused="pausedAudio"
-                    />
-                  </template>
-                </a-select>
+                  @blur="searchKey = ''"
+                />
               </a-form-item>
 
               <a-form-item
@@ -60,10 +44,9 @@
   </div>
 </template>
 <script lang='ts'  setup >
-import { reactive, watch, onMounted, ref } from 'vue'
-import { MediaController, CardController } from '@/controller/index'
-import SelectAudio from './select-audio.vue'
-import { audioManager } from '@/utils/AudioManaer'
+import { reactive, watch, onMounted, ref, defineProps, watchEffect } from 'vue'
+import { CardController } from '@/controller/index'
+import SelectAudio from './select-audio-new.vue'
 import { Form } from 'ant-design-vue'
 
 const useForm = Form.useForm
@@ -78,19 +61,6 @@ interface FormState {
 
 const formDom = ref()
 
-// const formState = reactive<FormState[]>([
-//   { card_insert: '', keyName: 'card_insert', music_name: '', is_break: 1, title: '卡片插入音频' },
-//   { card_remove: '', keyName: 'card_remove', music_name: '', is_break: 1, title: '卡片移除音频' },
-//   { button_rep: '', keyName: 'button_rep', music_name: '', is_break: 1, title: '按钮音频' },
-//   { ack_ok: '', keyName: 'ack_ok', music_name: '', is_break: 1, title: '一次全部正确' },
-//   { ack_mdf: '', keyName: 'ack_mdf', music_name: '', is_break: 1, title: '多次全部正确' },
-//   { remind_ack: '', keyName: 'remind_ack', music_name: '', is_break: 1, title: '答错两次以上' },
-//   { remind_button: '', keyName: 'remind_button', music_name: '', is_break: 1, title: '提示钮' },
-//   { remind_not_ack: '', keyName: 'remind_not_ack', music_name: '', is_break: 1, title: '点击过后5秒未答题' },
-//   { wait_30s: '', keyName: 'wait_30s', music_name: '', is_break: 1, title: '30秒未操作' },
-//   { wait_90s: '', keyName: 'wait_90s', music_name: '', is_break: 1, title: '90秒未操作' }
-// ])
-
 const formState = reactive<{default: FormState[]}>({
   default: [
     { card_insert: '', keyName: 'card_insert', music_name: '', is_break: 1, title: '卡片插入音频' },
@@ -104,43 +74,19 @@ const formState = reactive<{default: FormState[]}>({
     { wait_30s: '', keyName: 'wait_30s', music_name: '', is_break: 1, title: '30秒未操作' },
     { wait_90s: '', keyName: 'wait_90s', music_name: '', is_break: 1, title: '90秒未操作' }
   ]
+
 })
 
 const searchKey = ref('')
 
-const audioList = ref<API.Audio[]>([])
-
 // 当前表单的验证状态 为true的时候 父组件校验通过
 const isValid = ref(false)
 // cost { resetFields, validate, validateInfos } = useForm(modelRef, rulesRef, {
 //   onValidate: (...args) => console.log(...args),
 // });
 
-const getAudioList = async () => {
-  const { data } = await MediaController.getAudioList({ page: 1, key: searchKey.value })
-  audioList.value = data as API.Audio[]
-}
-
-const setAudioListState = (state: 'playing' | 'paused' | 'stopped') => {
-  audioList.value.forEach(audio => {
-    audio.state = state
-  })
-}
-
-const playAudio = async (audio: API.Audio) => {
-  setAudioListState('stopped')
-  await audioManager.playFromUrl(audio.url)
-  audio.state = audioManager.getState()
-}
-
-const pausedAudio = (audio: API.Audio) => {
-  audioManager.pause()
-  audio.state = audioManager.getState()
-}
-
 const handleChange = () => {
   searchKey.value = ''
-  getAudioList()
 }
 
 let debounceTimer: ReturnType<typeof setTimeout> | null = null
@@ -149,14 +95,16 @@ const handleSearch = (text: string) => {
   if (debounceTimer) clearTimeout(debounceTimer)
   debounceTimer = setTimeout(() => {
     searchKey.value = text
-    getAudioList()
   }, 300)
 }
 
 const getDefaultJson = async () => {
   const { data } = await CardController.getDefaultJson()
   formState.default.forEach(item => {
-    item.music_name = data[item.keyName].music_name
+    // 确保data中存在对应的keyName
+    if (data && data[item.keyName]) {
+      item.music_name = data[item.keyName].music_name
+    }
   })
 }
 
@@ -165,25 +113,26 @@ const generateDefaultJson = async () => {
 
   // const a = await formDom.value.validate()
   // console.log('generateDefaultJson', a)
-  formDom.value.validate().then(res => {
-    console.log('then', res)
-    isValid.value = true
-    return {
-      status: 200,
-      data: formState.default
-    }
-  }).catch(err => {
-    console.log('catch', err)
-    isValid.value = false
-    return {
-      status: 0,
-      data: formState.default
-    }
+  return new Promise((resolve, reject) => {
+    formDom.value.validate().then(res => {
+      console.log('then', res)
+      isValid.value = true
+      resolve({
+        status: 200,
+        data: formState.default
+      })
+    }).catch(err => {
+      console.log('catch', err)
+      isValid.value = false
+      resolve({
+        status: 0,
+        data: formState.default
+      })
+    })
   })
 }
 
 onMounted(() => {
-  getAudioList()
   getDefaultJson()
 })
 

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

@@ -12,6 +12,18 @@ const props = defineProps({
   config: {
     type: Object as PropType<API.CardJson21>,
     default: () => ({})
+  },
+  highlightedButtonKey: {
+    type: String,
+    default: ''
+  },
+  openState: {
+    type: Object as PropType<{
+      defaultConfig21Open: boolean
+    }>,
+    default: () => ({
+      defaultConfig21Open: false
+    })
   }
 })
 
@@ -134,11 +146,16 @@ function draw () {
   if (img.complete) {
     const imgW = img.width * state.scale
     const imgH = img.height * state.scale
-    ctx.drawImage(img, state.offsetX, state.offsetY, imgW, imgH)
+    // 根据openState.defaultConfig21Open调整图片位置
+    const offsetX = props.openState.defaultConfig21Open ? state.offsetX + canvas.width * 0.1 : state.offsetX
+    ctx.drawImage(img, offsetX, state.offsetY, imgW, imgH)
+
+    // 获取图片的实际偏移量
+    // const offsetX = props.openState.defaultConfig21Open ? state.offsetX + canvas.width * 0.1 : state.offsetX
 
     // 画矩形(随图片缩放和移动)
     rects.forEach(rect => {
-      const rx = state.offsetX + rect.x * state.scale
+      const rx = 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
@@ -264,10 +281,17 @@ function onMouseUp (e?: MouseEvent) {
     const y1 = Math.min(selection.startY, selection.endY)
     const x2 = Math.max(selection.startX, selection.endX)
     const y2 = Math.max(selection.startY, selection.endY)
+
+    const canvas = canvasRef.value
+    if (!canvas) return
+
+    // 获取图片的实际偏移量
+    const offsetX = props.openState.defaultConfig21Open ? state.offsetX + canvas.width * 0.1 : state.offsetX
+
     // 判断哪些rect被框选
     const selected: number[] = []
     rects.forEach(rect => {
-      const rx = state.offsetX + rect.x * state.scale
+      const rx = 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
@@ -280,7 +304,7 @@ function onMouseUp (e?: MouseEvent) {
     })
     selection.selecting = false
     draw()
-    if (selected.length > 0) handleOperation(selected)
+    // if (selected.length > 0) handleOperation(selected)
     return
   }
   state.dragging = false
@@ -296,9 +320,11 @@ function onClick (e: MouseEvent) {
   if (e.shiftKey) return
   const x = e.offsetX
   const y = e.offsetY
+  // 获取图片的实际偏移量
+  const offsetX = props.openState.defaultConfig21Open ? state.offsetX + canvas.width * 0.1 : state.offsetX
   let found: number | null = null
   for (const rect of rects) {
-    const rx = state.offsetX + rect.x * state.scale
+    const rx = 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
@@ -307,7 +333,7 @@ function onClick (e: MouseEvent) {
       break
     }
   }
-  found && handleOperation([found])
+  // found && handleOperation([found])
   draw()
 }
 
@@ -329,6 +355,21 @@ onMounted(() => {
 
 const delSelectedRectIds = () => draw()
 
+watch(() => props.highlightedButtonKey, (newKey) => {
+  if (!newKey) {
+    state.selectedRectId = null
+  } else {
+    const foundRect = rects.find(rect => rect.name === newKey)
+    state.selectedRectId = foundRect ? foundRect.id : null
+  }
+  draw()
+})
+
+// 监听openState.defaultConfig21Open的变化,重新绘制画布
+watch(() => props.openState.defaultConfig21Open, () => {
+  draw()
+})
+
 // watch(
 //   () => props.config,
 //   () => {

+ 69 - 52
src/pages/card/components/config-game.vue

@@ -4,8 +4,8 @@
     :open="open"
     :mask="false"
     width="540px"
-    @close="emit('update:open', false)"
-    @ok="emit('update:open', false)"
+    @close="emits('update:open', false)"
+    @ok="saveGameConfig"
     styles="position: fixed;top: 60px;right: 90px; pointer-events: none;"
   >
     <template #title>
@@ -76,7 +76,7 @@
         <div v-else-if="currentLevel === 1">
           <a-row class="config-game-list" :gutter="[8, 8]">
             <a-col
-              :span="12"
+              :span="10"
               class="config-game-item"
               v-for="(item, index) in cardJson.game_list[currentGameIndex].items"
               :key="index"
@@ -89,12 +89,14 @@
           <a-empty  style="margin: 0 auto;" v-if="cardJson.game_list[currentGameIndex].items.length === 0" description="暂无游戏"> </a-empty>
         </div>
 
-        <!-- 三级内容:游戏步骤配置 v-else-if="currentLevel === 2" -->
-        <div >
+        <!-- 三级内容:游戏步骤配置 -->
+        <div v-else-if="currentLevel === 2">
           <div class="card-config-content">
-            <!-- :item="cardJson.game_list[currentGameIndex].items[currentSubFolderIndex]" -->
             <gameStage3
-
+              v-if="cardJson.game_list[currentGameIndex]?.items?.[currentSubFolderIndex]"
+              v-model="cardJson.game_list[currentGameIndex].items[currentSubFolderIndex].steps"
+              @update:modelValue="changedSteps"
+              @step-selected="emits('step-selected', $event)"
             />
           </div>
         </div>
@@ -215,7 +217,7 @@
 <script lang='ts' setup>
 import { CardController } from '@/controller'
 import { LeftOutlined, PlusOutlined, SmallDashOutlined, EditOutlined, DeleteOutlined, CheckCircleFilled, ExclamationCircleFilled } from '@ant-design/icons-vue'
-import { reactive, ref } from 'vue'
+import { reactive, ref, PropType, computed, watch } from 'vue'
 import { message } from 'ant-design-vue'
 import SelectAudioNew from './select-audio-new.vue'
 import gameStage3 from './game-stage-3.vue'
@@ -224,38 +226,35 @@ const props = defineProps({
   open: {
     type: Boolean,
     default: false
+  },
+  config: {
+    type: Object as PropType<API.CardJson21>,
+    default: () => ({})
   }
 })
-const emit = defineEmits(['update:open'])
+
+const emits = defineEmits(['update:open', 'step-selected', 'update:config'])
+
+// 卡片category为21的json
+const cardJson = computed({
+  get: () => props.config,
+  set: (value) => emits('update:config', value)
+})
 
 // 一级folders的form
 const game1FormJson = {
-  rule: '7',
-  mainSubject: { music_name: '102.mp3', is_break: 1 },
-  ordered_multiple_err: { music_name: '102.mp3', is_break: 1 },
-  has_click_single: { music_name: '102.mp3', is_break: 1 },
-  still_have: { music_name: '102.mp3', is_break: 1 },
-  has_click_group: { music_name: '102.mp3', is_break: 1 },
-  wait_30s: { music_name: '102.mp3', is_break: 1 },
-  wait_90s: { music_name: '102.mp3', is_break: 1 }
+  rule: '',
+  main_subject: { music_name: '', is_break: 1 },
+  ordered_multiple_err: { music_name: '', is_break: 1 },
+  has_click_single: { music_name: '', is_break: 1 },
+  still_have: { music_name: '', is_break: 1 },
+  has_click_group: { music_name: '', is_break: 1 },
+  wait_30s: { music_name: '', is_break: 1 },
+  wait_90s: { music_name: '', is_break: 1 }
 }
 
 // 当前层级:0表示顶级文件夹,1表示sub文件夹,2表示游戏配置
-const currentLevel = ref(2)
-
-// 点击路径的index
-const currentFolderIndex = reactive({
-  folder: 0,
-  subFolder: 0,
-  step: 0
-})
-
-// 卡片category为21的json
-const cardJson = reactive<API.CardJson21>({
-  game_list: []
-})
-
-const dropdownVisible = ref(false)
+const currentLevel = ref(0)
 
 // 规则下拉选项
 const ruleOptions = CardController.ruleList
@@ -287,8 +286,8 @@ const subItemForm = reactive({
   },
   ok_key: [],
   err_key: [],
-  ok_key_voice: [],
-  err_key_voice: []
+  ok_key_voice: {},
+  err_key_voice: {}
 })
 
 // 当前选中的游戏索引
@@ -312,14 +311,14 @@ const getModalTitle = () => {
 }
 
 const createGameOne = () => {
-  cardJson.game_list.push(JSON.parse(JSON.stringify(game1FormJson)))
+  cardJson.value.game_list.push(JSON.parse(JSON.stringify(game1FormJson)))
 }
 
 // 校验游戏配置是否完整
 const validateGameConfig = (gameConfig: any) => {
   const requiredFields = [
     'rule',
-    'mainSubject.music_name',
+    'main_subject.music_name',
     'ordered_multiple_err.music_name',
     'has_click_single.music_name',
     'still_have.music_name',
@@ -351,7 +350,7 @@ const validateGameConfig = (gameConfig: any) => {
 
 // 进入二级文件夹
 const enterFolder = (index: number) => {
-  const gameConfig = cardJson.game_list[index]
+  const gameConfig = cardJson.value.game_list[index]
   const validation = validateGameConfig(gameConfig)
 
   if (!validation.isValid) {
@@ -375,14 +374,18 @@ const enterFolder = (index: number) => {
   }
 
   // 验证通过,创建 items 数组(如果不存在)
-  if (!cardJson.game_list[index].items) {
-    cardJson.game_list[index].items = []
+  if (!cardJson.value.game_list[index].items) {
+    cardJson.value.game_list[index].items = []
   }
 
   currentGameIndex.value = index
   currentLevel.value = 1
 }
 
+const changedSteps = (steps: any) => {
+  console.log('changedSteps:', cardJson.value.game_list[currentGameIndex.value].items)
+}
+
 const createSubItem = () => {
   // 重置表单并打开弹窗
   subItemForm.sub_subject.music_name = ''
@@ -393,8 +396,8 @@ const createSubItem = () => {
   subItemForm.sub_subject.eb = ''
   subItemForm.ok_key = []
   subItemForm.err_key = []
-  subItemForm.ok_key_voice = []
-  subItemForm.err_key_voice = []
+  subItemForm.ok_key_voice = {}
+  subItemForm.err_key_voice = {}
 
   subItemModalVisible.value = true
 }
@@ -408,7 +411,7 @@ const handleSaveSubItem = () => {
   }
 
   const index = currentGameIndex.value
-  const currentGame = cardJson.game_list[index]
+  const currentGame = cardJson.value.game_list[index]
   if (!currentGame) {
     message.error('未选择有效的游戏')
     return
@@ -423,16 +426,16 @@ const handleSaveSubItem = () => {
   const newItem = {
     sub_subject: {
       music_name: subItemForm.sub_subject.music_name,
-      mb: '',
+      mb: 1,
       ok: '',
-      ob: '',
+      ob: 1,
       err: '',
-      eb: ''
+      eb: 1
     },
     ok_key: [],
     err_key: [],
-    ok_key_voice: [],
-    err_key_voice: []
+    ok_key_voice: {},
+    err_key_voice: {}
   }
 
   // 追加到 items
@@ -446,9 +449,8 @@ const handleSaveSubItem = () => {
 // 进入卡片配置
 const enterCardConfig = (index: number) => {
   currentSubFolderIndex.value = index
-  const currentItem = cardJson.game_list[currentGameIndex.value]?.items?.[index]
+  const currentItem = cardJson.value.game_list[currentGameIndex.value]?.items?.[index]
   if (currentItem) {
-    // 初始化步骤数组(如果不存在)
     if (!currentItem.steps) {
       currentItem.steps = []
     }
@@ -467,21 +469,21 @@ const goBack = () => {
 const handleEdit = (index: number) => {
   editingIndex.value = index
   // 将当前游戏数据复制到编辑表单
-  Object.assign(editForm, JSON.parse(JSON.stringify(cardJson.game_list[index])))
+  Object.assign(editForm, JSON.parse(JSON.stringify(cardJson.value.game_list[index])))
   editModalVisible.value = true
 }
 
 // 处理删除游戏
 const handleDelete = (index: number) => {
   // 从游戏列表中删除指定索引的游戏
-  cardJson.game_list.splice(index, 1)
+  cardJson.value.game_list.splice(index, 1)
 }
 
 // 保存编辑
 const handleSaveEdit = () => {
   if (editingIndex.value >= 0) {
     // 将编辑表单的数据保存到游戏列表
-    Object.assign(cardJson.game_list[editingIndex.value], JSON.parse(JSON.stringify(editForm)))
+    Object.assign(cardJson.value.game_list[editingIndex.value], JSON.parse(JSON.stringify(editForm)))
   }
   editModalVisible.value = false
   editingIndex.value = -1
@@ -493,6 +495,21 @@ const handleCancelEdit = () => {
   editingIndex.value = -1
 }
 
+// 保存游戏配置
+const saveGameConfig = async () => {
+  console.log('保存游戏配置:', CardController.stepsToItems(cardJson.value))
+  const defaultJson = await CardController.getDefaultJson()
+  CardController.add({
+    header: {
+      card_type: 21,
+      ...cardJson.value.header
+    },
+    game_list: CardController.stepsToItems(cardJson.value).game_list
+  })
+  // 在这里可以添加保存逻辑,比如调用API保存cardJson
+  // emit('update:open', false)
+}
+
 </script>
 
 <style lang='less' scoped>

+ 52 - 39
src/pages/card/components/game-stage-3.vue

@@ -6,7 +6,6 @@
         新增步骤
       </a-button>
     </div>
-<!--      -->
 
     <VueDraggableNext
       :list="steps"
@@ -24,11 +23,11 @@
           <div class="step-info">
             <span class="step-number">步骤 {{ index + 1 }}</span>
             <div class="step-details">
-              <span>按钮: <strong>{{ element.button_key }}</strong></span>
+              <span>按钮: <strong>{{ getButtonLabel(element.button_key) }}</strong></span>
               <span>音频: <strong>{{ element.audio_name }}</strong></span>
             </div>
           </div>
-          <div class="step-actions">
+          <div class="step-actions">0
             <a-popconfirm
               title="确定要删除这个步骤吗?"
               ok-text="确定"
@@ -57,11 +56,11 @@
           <div class="button-grid">
             <a-button
               v-for="btn in buttonOptions"
-              :key="btn"
-              :type="newStepForm.button_key === btn ? 'primary' : 'default'"
+              :key="btn.value"
+              :type="newStepForm.button_key === btn.value ? 'primary' : 'default'"
               @click="selectButton(btn)"
             >
-              {{ btn }}
+              {{ btn.label }}
             </a-button>
           </div>
         </a-form-item>
@@ -85,57 +84,60 @@ import { message } from 'ant-design-vue'
 
 interface Step {
   id: number;
-  button_key: string;
+  button_key: number;
   audio_name: string;
 }
 
 interface Props {
-  item: API.CardJson21['game_list']['0']['items']['0'];
+  modelValue: Step[]
 }
 
-interface Person {
-  id: number
-  name: string
-}
-
-const list = ref<Person[]>([
-  { id: 1, name: 'John' },
-  { id: 2, name: 'Jane' },
-  { id: 3, name: 'Bob' }
-])
-
-// const props = defineProps<Props>()
-const emits = defineEmits(['update:modelValue'])
-
-const steps = ref<Step[]>([])
+const props = defineProps<Props>()
+const emits = defineEmits(['update:modelValue', 'step-selected'])
 
-// const steps = computed({
-//   get: () => props.modelValue,
-//   set: (value) => emits('update:modelValue', value)
-// })
+const steps = computed({
+  get: () => props.modelValue,
+  set: (value) => emits('update:modelValue', value)
+})
 
 const addStepModalVisible = ref(false)
 const activeStepIndex = ref<number | null>(null)
 
 const newStepForm = reactive({
-  button_key: '',
+  button_key: null as number | null,
   audio_name: ''
 })
 
-const buttonOptions = Array.from({ length: 6 }, (_, i) => String.fromCharCode(65 + i))
-  .flatMap(row => Array.from({ length: 6 }, (_, j) => `${row}${j + 1}`))
+const buttonOptions = (() => {
+  const options: {label: string, value: number}[] = []
+  const letters = ['A', 'B', 'C', 'D', 'E', 'F']
+  let value = 3
+  for (const letter of letters) {
+    for (let i = 1; i <= 6; i++) {
+      options.push({
+        label: `${letter}${i}`,
+        value: value
+      })
+      value++
+    }
+  }
+  return options
+})()
+
+const buttonMap = new Map<number, string>(buttonOptions.map(opt => [opt.value, opt.label]))
+const getButtonLabel = (value: number) => buttonMap.get(value) || '未知'
 
 const showAddStepModal = () => {
   resetNewStepForm()
   addStepModalVisible.value = true
 }
 
-const selectButton = (btn: string) => {
-  newStepForm.button_key = btn
+const selectButton = (btn: { label: string, value: number }) => {
+  newStepForm.button_key = btn.value
 }
 
 const handleAddStep = () => {
-  if (!newStepForm.button_key) {
+  if (newStepForm.button_key === null) {
     message.error('请选择一个按钮')
     return
   }
@@ -144,18 +146,25 @@ const handleAddStep = () => {
     return
   }
 
-  const newStep: Step = {
+  const newStep = {
     id: Date.now(),
     button_key: newStepForm.button_key,
     audio_name: newStepForm.audio_name
   }
 
-  // const newSteps = [...(steps.value || []), newStep]
+  const ok_key = { value: newStep.button_key, is_break: 1, music_name: '' }
+
+  const ok_key_voice = { music_name: newStep.audio_name, is_break: 1, value: newStep.button_key }
+
+  // steps.value.ok_key.push(ok_key)
+  // steps.value.ok_key_voice.push(ok_key_voice)
 
-  steps.value.push(newStep)
-  console.log(' steps.value:', steps.value)
+  console.log('steps.value:', steps.value)
+
+  const newSteps = [...(steps.value || []), newStep]
+
+  emits('update:modelValue', newSteps)
 
-  // emits('update:modelValue', newSteps)
   addStepModalVisible.value = false
 }
 
@@ -164,7 +173,7 @@ const cancelAddStep = () => {
 }
 
 const resetNewStepForm = () => {
-  newStepForm.button_key = ''
+  newStepForm.button_key = null
   newStepForm.audio_name = ''
 }
 
@@ -174,12 +183,16 @@ const deleteGameStep = (index: number) => {
     emits('update:modelValue', steps.value)
     if (activeStepIndex.value === index) {
       activeStepIndex.value = null
+      emits('step-selected', null)
     }
   }
 }
 
 const selectStep = (index: number) => {
   activeStepIndex.value = index
+  if (steps.value && steps.value[index]) {
+    emits('step-selected', steps.value[index].button_key)
+  }
 }
 
 const onDragEnd = () => {

+ 7 - 6
src/pages/card/components/select-audio-new.vue

@@ -9,7 +9,7 @@
       <div class="empty-icon">
         <SoundOutlined />
       </div>
-      <div class="empty-text">{{ placeholder }}</div>
+      <!-- <div class="empty-text">{{ placeholder }}</div> -->
       <div class="empty-action">点击选择音频</div>
     </div>
 
@@ -306,11 +306,12 @@ watch(searchKey, () => {
   width: 100%;
 
   .audio-empty-state {
+    height: 60px;
     display: flex;
     flex-direction: column;
     align-items: center;
     justify-content: center;
-    padding: 24px;
+    padding: 12px;
     border: 2px dashed #d9d9d9;
     border-radius: 8px;
     cursor: pointer;
@@ -323,15 +324,15 @@ watch(searchKey, () => {
     }
 
     .empty-icon {
-      font-size: 32px;
+      font-size: 24px;
       color: #bfbfbf;
-      margin-bottom: 8px;
+      margin-bottom: 4px;
     }
 
     .empty-text {
-      font-size: 14px;
+      font-size: 13px;
       color: #666;
-      margin-bottom: 4px;
+      margin-bottom: 2px;
     }
 
     .empty-action {

+ 52 - 12
src/pages/card/index.vue

@@ -19,7 +19,8 @@
       </a-space>
     </div>
     <a-space>
-      <a-button  @click="openDefaultConfig">默认配置</a-button>
+      <a-button v-if="Number(cardInfo.card_type) === 5" @click="openDefaultConfig(5)">默认配置</a-button>
+      <a-button v-if="Number(cardInfo.card_type) === 21"  @click="openDefaultConfig(21)">默认配置</a-button>
       <a-button v-if="Number(cardInfo.card_type) === 5"  @click="openState.configButtonOpen = true">按钮配置</a-button>
       <a-button v-if="Number(cardInfo.card_type) === 5" type="primary" @click="openConfigCard(5)">打开配置板</a-button>
       <a-button v-if="Number(cardInfo.card_type) === 21" type="primary" @click="openConfigCard(21)">打开配置板</a-button>
@@ -40,6 +41,7 @@
     @operation="handleOperation"
     :cardData="cardInfo"
     :config="cardJson21"
+    :highlighted-button-key="highlightedButtonKey"
   />
 
   <!-- 右侧配置区域 -->
@@ -80,8 +82,24 @@
    <CardDefault  ref="cardDefaultDom"  />
   </modal-pro>
 
+  <modal-pro
+    label="默认配置"
+    :open='openState.defaultConfig21Open'
+    @close="openState.defaultConfig21Open = false"
+    @confirm="saveDefault21Config"
+    :mask="false"
+
+    styles="position: fixed;top: 60px;right: 90px; pointer-events: none;"
+  >
+   <CardDefault21  :header="cardJson21.header" ref="cardDefaultDom21"  />
+  </modal-pro>
+
   <!-- catetype = 21 时  的游戏分层配置 -->
-    <GameStage  v-model:open="openState.configGameOpen" />
+    <ConfigGame
+      v-model:config="cardJson21"
+      v-model:open="openState.configGameOpen"
+      @step-selected="handleStepSelected"
+    />
   <!-- catetype = 21 时 的 游戏配置 -->
 
   <modal-pro
@@ -104,11 +122,12 @@ 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'
+import CardDefault21 from './components/card-default-21.vue'
 import ConfigButton from './components/config-button.vue'
 import { useRoute } from 'vue-router'
 import { CardController } from '@/controller'
 import { message } from 'ant-design-vue'
-import GameStage from './components/config-game.vue'
+import ConfigGame from './components/config-game.vue'
 
 const operationTip = `
   1. 点击矩形可以选中,选中后进行配置<br>
@@ -122,6 +141,7 @@ const openState = reactive({
   configCardOpen: false,
   configGameOpen: false,
   defaultConfigOpen: false,
+  defaultConfig21Open: false,
   configButtonOpen: false
 })
 
@@ -133,26 +153,33 @@ const isValids = reactive({
 const cardCategory = ref(null)
 
 const cardDefaultDom = ref()
+const cardDefaultDom21 = ref()
 
 const cardConfigDom = ref()
 
 const configButtonDom = ref()
 
 const cardJson = reactive<API.CardJson>({
-  header: null,
-  touch_key: null,
-  slide_knob: null
+  header: {} as any,
+  touch_key: [],
+  slide_knob: undefined
 })
 
 const cardJson21 = reactive<API.CardJson21>({
-  header: null,
-  game_list: []
+  header: {} as any,
+  game_list: [] as any
 })
 
 const cardTemplateDom = ref()
 
 const cardTemplateDom21 = ref()
 
+const highlightedButtonKey = ref('')
+
+const handleStepSelected = (buttonKey: string) => {
+  highlightedButtonKey.value = buttonKey
+}
+
 const onClose = () => openState.configCardOpen = false
 
 const handleVerify = (verifyKey: 'buttonIsValid' | 'cardIsValid', value: boolean) => {
@@ -185,12 +212,23 @@ const saveConfigCard = async () => {
 
 const saveDefaultConfig = async () => {
   const obj = {} as API.CardJsonDefault
+  console.log('cardDefaultDom.value:', cardDefaultDom.value)
+
   const { data } = await cardDefaultDom.value.generateDefaultJson()
+  console.log('生成的默认配置:', data)
   data.forEach(item => obj[item.keyName] = { music_name: item.music_name, is_break: item.is_break })
   CardController.addDefaulJson(obj)
   onSave()
 }
 
+const saveDefault21Config = async () => {
+  const { data } = await cardDefaultDom21.value.generateDefaultJson()
+
+  data.forEach(item => cardJson21.header[item.keyName] = { music_name: item.music_name, is_break: item.is_break })
+
+  openState.defaultConfig21Open = false
+}
+
 const saveButtonConfig = () => {
   const { result, verify } = configButtonDom.value.generateButtonJson()
   cardJson.slide_knob = result
@@ -202,8 +240,8 @@ const openConfigCard = (type: 5 | 21) => {
   type === 5 ? openState.configCardOpen = true : openState.configGameOpen = true
 }
 
-const openDefaultConfig = () => {
-  openState.defaultConfigOpen = true
+const openDefaultConfig = (type: 5 | 21) => {
+  type === 5 ? openState.defaultConfigOpen = true : openState.defaultConfig21Open = true
 }
 
 const handleCardTemplateDelete = (ids) => {
@@ -261,8 +299,10 @@ const getCardJson5 = async () => {
 
 const getCardJson21 = async () => {
   const { data } = await CardController.card21JsonById(cardInfo.id!)
-  cardJson21.header = data.header
-  cardJson21.game_list = data.game_list
+  console.log('获取到的卡片数据:', data)
+
+  cardJson21.header = data.header as API.CardJson21.header
+  cardJson21.game_list = data.game_list as any
 }
 
 function onBack () { window.history.back() }

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

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

+ 5 - 6
src/typeing.d.ts

@@ -293,8 +293,7 @@ declare namespace API {
         'card_remove': BaseItem,
         'finish': BaseItem
     },
-     'game_list': [ // value 是每个按钮的值
-        {
+     'game_list': {
             'rule': Rule, // 卡片规则
             'main_subject': BaseItem, // 每个游戏读题音
             'ordered_multiple_err': BaseItem, // 顺序多选错误音
@@ -316,10 +315,10 @@ declare namespace API {
               },
               '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 // 点击错误的声音
+              'ok_key_voice': ({ 'value': number} & BaseItem & CardDot) // 正确的声音
+              'err_key_voice': ({ 'value': number} & BaseItem & CardDot) // 点击错误的声音
+              steps: {button_key: number, audio_name: string, id: number}[] // 数据收集 综合作用
             }[]
-        }
-    ]
+      }[]
   }
 }