Kaynağa Gözat

test: type=21的测试

lvkun996 8 ay önce
ebeveyn
işleme
9a437018ba

+ 16 - 0
src/controller/CardController.ts

@@ -38,6 +38,20 @@ export class CardController {
     [6, 'F']
   ])
 
+  static breakList = [
+    { value: 0, title: '能被其他音频打断,不能打断其他音频' },
+    { value: 1, title: '能被其他音频打断,能打断其他音频' },
+    { value: 2, title: '不能被其他音频打断,不能打断其他音频' },
+    { value: 3, title: '不能被其他音频打断,能打断其他音频' }
+  ]
+
+  static breakMap = new Map([
+    [0, '能被其他音频打断,不能打断其他音频'],
+    [1, '能被其他音频打断,能打断其他音频'],
+    [2, '不能被其他音频打断,不能打断其他音频'],
+    [3, '不能被其他音频打断,能打断其他音频']
+  ])
+
   static async getCardList (parentId: string) {
     const { data } = await getCardList(parentId)
     return { data }
@@ -57,6 +71,7 @@ export class CardController {
 
   static async getDefaultJson () {
     const { data } = await getDefaultCard()
+
     return { data: data }
   }
 
@@ -64,6 +79,7 @@ export class CardController {
     const { data } = await getCardJsonById(id)
 
     const dataJson = JSON.parse(data)
+    console.log('卡片数据详情:', dataJson)
 
     if (dataJson.header.card_type === 5) {
       return CardController.card5JsonById(dataJson)

+ 3 - 5
src/pages/card/components/card-default-21.vue

@@ -31,12 +31,9 @@
               </a-form-item>
 
               <a-form-item
-                label="可中止"
+                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>
+                <select-break v-model:value="item.is_break" />
               </a-form-item>
           </a-card>
         </template>
@@ -47,6 +44,7 @@
 import { reactive, watch, onMounted, ref, defineProps, watchEffect } from 'vue'
 import { CardController } from '@/controller/index'
 import SelectAudio from './select-audio-new.vue'
+import SelectBreak from './select-break.vue'
 import { Form } from 'ant-design-vue'
 
 const useForm = Form.useForm

+ 46 - 23
src/pages/card/components/card-default.vue

@@ -10,10 +10,10 @@
       >
         <template
           v-for="(item, index) in formState.default"
-          :key="item.title"
+          :key="item.keyName"
         >
           <a-card
-            :title="item.title"
+            :title="item.label"
             style="margin-bottom: 16px"
           >
               <a-form-item
@@ -31,12 +31,9 @@
               </a-form-item>
 
               <a-form-item
-                label="可中止"
+                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>
+                <select-break v-model:value="item.is_break" />
               </a-form-item>
           </a-card>
         </template>
@@ -44,9 +41,10 @@
   </div>
 </template>
 <script lang='ts'  setup >
-import { reactive, watch, onMounted, ref, defineProps, watchEffect } from 'vue'
+import { reactive, watch, onMounted, ref, defineProps, PropType } from 'vue'
 import { CardController } from '@/controller/index'
 import SelectAudio from './select-audio-new.vue'
+import SelectBreak from './select-break.vue'
 import { Form } from 'ant-design-vue'
 
 const useForm = Form.useForm
@@ -56,27 +54,46 @@ interface FormState {
   'music_name': string
   'is_break': number // 1 | 0
   keyName: string
-  title: string,
+  label: string,
 }
 
+const props = defineProps({
+  config: {
+    type: Object as PropType<API.CardJson>,
+    default: () => ({})
+  }
+})
+
 const formDom = ref()
 
 const formState = reactive<{default: FormState[]}>({
   default: [
-    { 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秒未操作' }
+    { title: '', keyName: 'title', music_name: '', is_break: 1, label: '读题音频' },
+    { card_insert: '', keyName: 'card_insert', music_name: '', is_break: 1, label: '卡片插入音频' },
+    { card_remove: '', keyName: 'card_remove', music_name: '', is_break: 1, label: '卡片移除音频' },
+    { button_rep: '', keyName: 'button_rep', music_name: '', is_break: 1, label: '按钮音频' },
+    { ack_ok: '', keyName: 'ack_ok', music_name: '', is_break: 1, label: '一次全部正确' },
+    { ack_mdf: '', keyName: 'ack_mdf', music_name: '', is_break: 1, label: '多次全部正确' },
+    { remind_ack: '', keyName: 'remind_ack', music_name: '', is_break: 1, label: '答错两次以上' },
+    { remind_button: '', keyName: 'remind_button', music_name: '', is_break: 1, label: '提示钮' },
+    { remind_not_ack: '', keyName: 'remind_not_ack', music_name: '', is_break: 1, label: '点击过后5秒未答题' },
+    { wait_30s: '', keyName: 'wait_30s', music_name: '', is_break: 1, label: '30秒未操作' },
+    { wait_90s: '', keyName: 'wait_90s', music_name: '', is_break: 1, label: '90秒未操作' }
   ]
-
 })
 
+// watch(
+//   () => props.config,
+//   () => {
+//     formState.default.forEach(item => {
+//       item.music_name = props.config.header[item.keyName].music_name || item.music_name
+//     })
+//   },
+//   {
+//     immediate: true
+//   }
+// )
+
 const searchKey = ref('')
 
 // 当前表单的验证状态 为true的时候 父组件校验通过
@@ -100,10 +117,16 @@ const handleSearch = (text: string) => {
 
 const getDefaultJson = async () => {
   const { data } = await CardController.getDefaultJson()
+  console.log(' props.config.header:', data)
+
   formState.default.forEach(item => {
-    // 确保data中存在对应的keyName
-    if (data && data[item.keyName]) {
-      item.music_name = data[item.keyName].music_name
+    console.log('错误的music_name:', item)
+
+    const is_break = props.config.header[item.keyName].is_break
+
+    if (typeof item === 'object' && !!item) {
+      item.music_name = props.config.header[item.keyName].music_name || data[item.keyName].music_name || ''
+      item.is_break = CardController.breakList.map(item => item.value).includes(is_break) ? is_break : 1
     }
   })
 }

+ 5 - 3
src/pages/card/components/card-template-21.vue

@@ -96,7 +96,7 @@ function generateTopRects () {
 // generateTopRects()
 // 图片状态
 const state = {
-  scale: 0.2, // 初始缩放为0.5
+  scale: 0.3, // 初始缩放为0.5
   minScale: 0.2,
   maxScale: 5,
   offsetX: 0,
@@ -189,10 +189,12 @@ function draw () {
       ctx.strokeRect(rx, ry, rw, rh)
       // 绘制序号
       ctx.font = `bold ${Math.floor(rh / 1.7)}px sans-serif`
-      ctx.fillStyle = isSelected ? '#ff6600' : '#333'
+      ctx.strokeStyle = isSelected ? '#ff6600' : '#333'
+      ctx.lineWidth = 1
       ctx.textAlign = 'center'
       ctx.textBaseline = 'middle'
-      ctx.fillText(rect.name, rx + rw / 2, ry + rh / 2)
+      ctx.setLineDash([]) // 设置为实线
+      ctx.strokeText(rect.name, rx + rw / 2, ry + rh / 2)
       ctx.restore()
     })
 

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

@@ -155,10 +155,12 @@ function draw () {
       ctx.strokeRect(rx, ry, rw, rh)
       // 绘制序号
       ctx.font = `bold ${Math.floor(rh / 1.7)}px sans-serif`
-      ctx.fillStyle = isSelected ? '#ff6600' : '#333'
+      ctx.strokeStyle = isSelected ? '#ff6600' : '#333'
+      ctx.lineWidth = 1
       ctx.textAlign = 'center'
       ctx.textBaseline = 'middle'
-      ctx.fillText(rect.name, rx + rw / 2, ry + rh / 2)
+      ctx.setLineDash([]) // 设置为实线
+      ctx.strokeText(rect.name, rx + rw / 2, ry + rh / 2)
       ctx.restore()
     })
 

+ 3 - 5
src/pages/card/components/config-card.vue

@@ -32,13 +32,10 @@
               </a-form-item>
 
               <a-form-item
-                label="可中止"
+                label="播放权重"
                 name="is_break"
               >
-                <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>
+                <select-break v-model:value="item.is_break" />
               </a-form-item>
           </a-card>
         </template>
@@ -49,6 +46,7 @@
 import { reactive, watch, onMounted, ref, Ref, defineExpose, computed } from 'vue'
 import { MediaController } from '@/controller/index'
 import SelectAudioNew from './select-audio-new.vue'
+import SelectBreak from './select-break.vue'
 
 const props = defineProps<{
   config: API.CardJson

+ 36 - 32
src/pages/card/components/config-game.vue

@@ -3,14 +3,24 @@
     :label="getModalTitle()"
     :open="open"
     :mask="false"
-    width="540px"
+    width="640px"
     @close="emits('update:open', false)"
-    @ok="saveGameConfig"
+    @confirm="saveGameConfig"
     styles="position: fixed;top: 60px;right: 90px; pointer-events: none;"
   >
     <template #title>
       <div  style="display: flex;justify-content: space-between;" >
-      <div>游戏配置</div>
+      <div>
+        <a-space>
+          <div>游戏配置 {{gameStateComputed ? `:   ${gameStateComputed}` : gameStateComputed}}</div>
+          <a-tooltip placement="bottom" :getPopupContainer="(node) => node.offsetParent">
+            <template #title>
+              <span v-html="operationTip"></span>
+            </template>
+            <InfoCircleOutlined />
+          </a-tooltip>
+        </a-space>
+      </div>
       <a-button v-if="currentLevel === 0" type="primary" size="small" @click="createGameOne">
         <template #icon><plus-outlined /></template>
         新增
@@ -108,10 +118,6 @@
                           <EditOutlined />
                           <span style="margin-left: 8px;">编辑</span>
                         </a-menu-item>
-                        <a-menu-item key="top" @click="handleEditTop(index)">
-                          <VerticalAlignTopOutlined />
-                          <span style="margin-left: 8px;">顶部配置</span>
-                        </a-menu-item>
                         <a-menu-item key="delete" @click="handleDeleteSubItem(index)">
                           <DeleteOutlined style="color: #ff4d4f;" />
                           <span style="margin-left: 8px;">删除</span>
@@ -298,15 +304,19 @@ import {
   DeleteOutlined,
   CheckCircleFilled,
   ExclamationCircleFilled,
-  SoundOutlined,
-  VerticalAlignTopOutlined
+  SoundOutlined
 } from '@ant-design/icons-vue'
-import { reactive, ref, PropType, computed, watch } from 'vue'
+import { reactive, ref, PropType, computed } from 'vue'
 import { message } from 'ant-design-vue'
 import SelectAudioNew from './select-audio-new.vue'
 import gameStage3 from './game-stage-3.vue'
 import { VueDraggableNext } from 'vue-draggable-next'
 
+const operationTip = `
+  1. 拖拽游戏文件可以调整顺序
+  2. 单点单播和有序单点无错误选项
+`
+
 const props = defineProps({
   open: {
     type: Boolean,
@@ -315,13 +325,16 @@ const props = defineProps({
   config: {
     type: Object as PropType<API.CardJson21>,
     default: () => ({})
-  },
-  templateDom21: {
-    type: Object as PropType<HTMLElement>,
-    default: () => document.body
   }
 })
 
+const gameStateComputed = computed(() => {
+  const rule = cardJson.value.game_list[currentGameIndex.value].rule
+  console.log('CardController.ruleList.toMap:', rule)
+  if (currentLevel.value === 0) return ''
+  return CardController.ruleList.toMap('value', 'title').get(rule) || ''
+})
+
 const emits = defineEmits(['update:open', 'step-selected', 'update:config'])
 
 // 卡片category为21的json
@@ -433,7 +446,6 @@ const validateGameConfig = (gameConfig: any) => {
     'wait_30s.music_name',
     'wait_90s.music_name'
   ]
-  console.log('gameConfig:', gameConfig)
 
   const missingFields: string[] = []
 
@@ -599,14 +611,6 @@ const handleEditSubItem = (index: number) => {
   editSubItemForm.sub_subject.err = currentItem.sub_subject.err
   editSubItemForm.sub_subject.eb = currentItem.sub_subject.eb
   editSubItemModalVisible.value = true
-  // props.templateDom21.showReactByType('content')
-}
-
-// 编辑顶部配置
-const handleEditTop = () => {
-  console.log('handleEditTop', props.templateDom21)
-
-  props.templateDom21.showReactByType('top')
 }
 
 // 处理删除子项目
@@ -660,18 +664,15 @@ const handleCancelEdit = () => {
 }
 
 // 保存游戏配置
-const saveGameConfig = async () => {
+const saveGameConfig = async ({ confirmStart, confirmed }) => {
   console.log('保存游戏配置:', CardController.stepsToItems(cardJson.value))
-  const defaultJson = await CardController.getDefaultJson()
-  CardController.add({
-    header: {
-      card_type: 21,
-      ...cardJson.value.header
-    },
+  confirmStart()
+  // const defaultJson = await Car dController.getDefaultJson()
+  await CardController.add({
+    header: cardJson.value.header,
     game_list: CardController.stepsToItems(cardJson.value).game_list
   })
-  // 在这里可以添加保存逻辑,比如调用API保存cardJson
-  // emit('update:open', false)
+  confirmed()
 }
 
 </script>
@@ -829,6 +830,9 @@ const saveGameConfig = async () => {
 }
 
 .card-config-content {
+  height: 700px;
+  overflow: hidden;
+  overflow-y: scroll;
   padding: 16px;
 
   h3 {

+ 96 - 49
src/pages/card/components/game-stage-3.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="game-stage-3">
     <div class="top-config">
-      <a-collapse :bordered="false">
+      <a-collapse :bordered="false" :activeKey="1">
         <a-collapse-panel key="1">
           <template #header>
             <div class="collapse-header">
@@ -12,7 +12,7 @@
             <a-form-item label="按钮音频">
               <SelectAudioNew
                 v-model="touch_keys[0][0].music_name"
-                placeholder="请选择队员按钮音频"
+                placeholder="请选择按钮音频"
               />
             </a-form-item>
 
@@ -33,8 +33,8 @@
         </a-collapse-panel>
       </a-collapse>
     </div>
-    <div class="step-list" v-if="getBtnRuleByRule === 'multiple'">
-      <a-collapse :bordered="false" >
+    <div class="step-list" >
+      <a-collapse :bordered="false" :activeKey="2">
         <a-collapse-panel key="2" >
           <template #header>
             <div class="collapse-header">
@@ -57,10 +57,18 @@
                 @click="selectStep(element.value)"
               >
                 <div class="step-info">
-                  <span class="step-number">按钮 {{ index + 1 }}</span>
+                  <div class="step-number">按钮 {{getButtonLabel(element.value) }}</div>
                   <div class="step-details">
-                    <span>按钮: <strong>{{ getButtonLabel(element.value) }}</strong></span>
-                    <span>音频: <strong>{{ element.music_name }}</strong></span>
+                    <div class="step-detail-row">
+                      <span class="detail-label">音频:</span>
+                      <strong class="audio-name">{{ element.music_name }}</strong>
+                    </div>
+                    <div class="step-detail-row break-row">
+                      <span class="detail-label">可中止:</span>
+                      <div class="break-select">
+                        <select-break v-model:value="element.is_break" />
+                      </div>
+                    </div>
                   </div>
                 </div>
                 <div class="step-actions">
@@ -82,7 +90,7 @@
       </a-collapse>
     </div>
     <div class="step-list" v-if="getBtnRuleByRule === 'multiple'">
-      <a-collapse :bordered="false">
+      <a-collapse :bordered="false" :activeKey="3">
         <a-collapse-panel key="3">
           <template #header>
             <div class="collapse-header">
@@ -105,10 +113,22 @@
                 @click="selectStep(element.value)"
               >
                 <div class="step-info">
-                  <span class="step-number">按钮 {{ index + 1 }}</span>
+                  <div class="step-number">按钮 {{ index + 1 }}</div>
                   <div class="step-details">
-                    <span>按钮: <strong>{{ getButtonLabel(element.value) }}</strong></span>
-                    <span>音频: <strong>{{ element.music_name }}</strong></span>
+                    <div class="step-detail-row">
+                      <span class="detail-label">按钮:</span>
+                      <strong>{{ getButtonLabel(element.value) }}</strong>
+                    </div>
+                    <div class="step-detail-row">
+                      <span class="detail-label">音频:</span>
+                      <strong class="audio-name">{{ element.music_name }}</strong>
+                    </div>
+                    <div class="step-detail-row break-row">
+                      <span class="detail-label">可中止:</span>
+                      <div class="break-select">
+                        <select-break v-model:value="element.is_break" />
+                      </div>
+                    </div>
                   </div>
                 </div>
                 <div class="step-actions">
@@ -171,16 +191,16 @@
           </div>
         </a-form-item>
         <a-form-item label="选择语音">
-          <span
+          <div
             style="margin-bottom: 8px;"
             :key="item.value"
              v-for="item in steps.ok_key"
           >
             <SelectAudioNew
               v-model="item.music_name"
-              placeholder="请选择步骤语音"
+              :placeholder="'请选择' + getButtonLabel(item.value) + '音频'"
             />
-          </span>
+          </div>
         </a-form-item>
       </a-form>
       <a-form :model="steps.err_key" layout="vertical" v-else>
@@ -198,16 +218,16 @@
           </div>
         </a-form-item>
         <a-form-item label="选择语音">
-          <span
+          <div
             style="margin-bottom: 8px;"
             v-for="item in steps.err_key"
             :key="item.value"
           >
             <SelectAudioNew
               v-model="item.music_name"
-              placeholder="请选择步骤语音"
+              :placeholder="'请选择' + getButtonLabel(item.value) + '音频'"
             />
-          </span>
+          </div>
         </a-form-item>
       </a-form>
     </a-modal>
@@ -219,6 +239,7 @@ import { ref, computed, reactive, watch } from 'vue'
 import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue'
 import { VueDraggableNext } from 'vue-draggable-next'
 import SelectAudioNew from './select-audio-new.vue'
+import SelectBreak from './select-break.vue'
 import { message } from 'ant-design-vue'
 
 interface Props {
@@ -234,6 +255,7 @@ watch(
   },
   { immediate: true }
 )
+
 watch(
   () => props.touch_key,
   (newVal) => {
@@ -245,6 +267,12 @@ watch(
 const props = defineProps<Props>()
 const emits = defineEmits(['update:modelValue', 'step-selected', 'update:touch_key'])
 
+// 正确按钮
+const ok_key = ref([])
+
+// 错误按钮
+const err_key = ref([])
+
 const steps = computed({
   get: () => props.modelValue,
   set: (value) => emits('update:modelValue', value)
@@ -288,9 +316,9 @@ const buttonMap = new Map<number, string>(buttonOptions.map(opt => [opt.value, o
 const getButtonLabel = (value: number) => buttonMap.get(value) || '未知'
 
 const getBtnTypeByKey = (key: number) => {
-  if (steps.value.ok_key.some(item => item.value === key)) {
+  if (steps.value.ok_key.some(item => item.value === key) && btnMode.value === 'success') {
     return 'primary'
-  } else if (steps.value.err_key.some(item => item.value === key)) {
+  } else if (steps.value.err_key.some(item => item.value === key) && btnMode.value === 'error') {
     return 'primary'
   }
   return 'default'
@@ -306,10 +334,20 @@ const selectButton = (btn: { label: string, value: number }) => {
     if (getBtnRuleByRule.value === 'single') {
       steps.value.ok_key[0] = { value: btn.value, music_name: '', is_break: 1 }
     } else {
-      steps.value.ok_key.push({ value: btn.value, music_name: '', is_break: 1 })
+      const targetIndex = steps.value.ok_key.findIndex(item => btn.value === item.value)
+      if (targetIndex === -1) {
+        steps.value.ok_key.push({ value: btn.value, music_name: '', is_break: 1 })
+      } else {
+        steps.value.ok_key.splice(targetIndex, 1)
+      }
     }
   } else {
-    steps.value.err_key.push({ value: btn.value, music_name: '', is_break: 1 })
+    const targetIndex = steps.value.err_key.findIndex(item => btn.value === item.value)
+    if (targetIndex === -1) {
+      steps.value.err_key.push({ value: btn.value, music_name: '', is_break: 1 })
+    } else {
+      steps.value.err_key.splice(targetIndex, 1)
+    }
   }
 }
 
@@ -410,14 +448,13 @@ const handleDeleteButton = (value: number) => {
   }
 
   .step-item-wrapper {
-    // width: 100%;
     margin-bottom: 12px;
   }
 
   .step-item {
     width: 90%;
     display: flex;
-    align-items: center;
+    align-items: flex-start;
     justify-content: space-between;
     padding: 16px;
     border: 1px solid #d9d9d9;
@@ -440,34 +477,62 @@ const handleDeleteButton = (value: number) => {
 
   .step-info {
     display: flex;
-    align-items: center;
-    gap: 24px;
+    flex-direction: column;
+    width: 100%;
   }
 
   .step-number {
     font-weight: 600;
     font-size: 16px;
+    margin-bottom: 12px;
+    color: #1890ff;
   }
 
   .step-details {
     display: flex;
     flex-direction: column;
-    gap: 4px;
+    gap: 8px;
     font-size: 14px;
     color: #555;
+    width: 100%;
+    padding-left: 0;
+  }
 
-    span > strong {
-      color: #333;
-      margin-left: 8px;
-    }
+  .step-detail-row {
+    display: flex;
+    align-items: center;
+    padding: 4px 0;
+  }
+
+  .detail-label {
+    width: 60px;
+    color: #666;
+    font-weight: 500;
+  }
+
+  .audio-name {
+    color: #1890ff;
+    max-width: 200px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .break-row {
+    margin-top: 4px;
+  }
+
+  .break-select {
+    flex: 1;
+    max-width: 300px;
   }
 
   .step-actions {
     cursor: pointer;
     color: #ff4d4f;
     font-size: 16px;
+    padding-top: 4px;
   }
-
 }
 </style>
 <style lang="less" scoped>
@@ -527,24 +592,6 @@ const handleDeleteButton = (value: number) => {
   margin-top: 4px;
 }
 
-.button-wrapper {
-  position: relative;
-}
-
-.delete-icon {
-  position: absolute;
-  top: -8px;
-  right: -8px;
-  color: #ff4d4f;
-  background-color: #fff;
-  border-radius: 50%;
-  padding: 2px;
-  font-size: 14px;
-  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
-  cursor: pointer;
-  z-index: 10;
-}
-
 .panel-header {
   display: flex;
   justify-content: flex-end;

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

@@ -9,8 +9,8 @@
       <div class="empty-icon">
         <SoundOutlined />
       </div>
-      <!-- <div class="empty-text">{{ placeholder }}</div> -->
-      <div class="empty-action">点击选择音频</div>
+      <div class="empty-text empty-action">{{ placeholder || "点击选择音频" }}</div>
+      <!-- <div class="empty-action">点击选择音频</div> -->
     </div>
 
     <!-- 已选择状态 -->

+ 157 - 0
src/pages/card/components/select-break.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="custom-select-break">
+    <div
+      class="selected-option"
+      @click="toggleDropdown"
+      :class="{ 'is-open': isOpen }"
+    >
+      <span>{{ getCurrentTitle() }}</span>
+      <span class="arrow-icon">
+        <svg viewBox="0 0 1024 1024" width="1em" height="1em" fill="currentColor">
+          <path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path>
+        </svg>
+      </span>
+    </div>
+    <div class="dropdown-menu" v-if="isOpen">
+      <div
+        v-for="item in breakList"
+        :key="item.value"
+        class="dropdown-item"
+        :class="{ 'is-active': selectedValue === item.value }"
+        @click="selectOption(item.value)"
+      >
+        {{ item.title }}
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch, defineProps, defineEmits, onMounted, onBeforeUnmount } from 'vue'
+import { CardController } from '@/controller'
+
+const props = defineProps({
+  value: {
+    type: Number,
+    default: 0
+  }
+})
+
+const emit = defineEmits(['update:value'])
+
+const breakList = CardController.breakList
+const selectedValue = ref(props.value)
+const isOpen = ref(false)
+
+const getCurrentTitle = () => {
+  const selected = breakList.find(item => item.value === selectedValue.value)
+  return selected ? selected.title : '请选择'
+}
+
+const toggleDropdown = () => {
+  isOpen.value = !isOpen.value
+}
+
+const selectOption = (value: number) => {
+  selectedValue.value = value
+  isOpen.value = false
+}
+
+// 点击外部关闭下拉菜单
+const handleClickOutside = (event: MouseEvent) => {
+  const target = event.target as HTMLElement
+  if (!target.closest('.custom-select-break')) {
+    isOpen.value = false
+  }
+}
+
+watch(() => props.value, (newVal) => {
+  selectedValue.value = newVal
+})
+
+watch(selectedValue, (newVal) => {
+  emit('update:value', newVal)
+})
+
+onMounted(() => {
+  document.addEventListener('click', handleClickOutside)
+})
+
+onBeforeUnmount(() => {
+  document.removeEventListener('click', handleClickOutside)
+})
+</script>
+
+<style lang="less" scoped>
+.custom-select-break {
+  position: relative;
+  width: 100%;
+  font-size: 14px;
+
+  .selected-option {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    height: 32px;
+    padding: 0 11px;
+    background-color: #fff;
+    border: 1px solid #d9d9d9;
+    border-radius: 2px;
+    cursor: pointer;
+    transition: all 0.3s;
+
+    &:hover {
+      border-color: #40a9ff;
+    }
+
+    &.is-open {
+      border-color: #40a9ff;
+      box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+
+      .arrow-icon {
+        transform: rotate(180deg);
+      }
+    }
+
+    .arrow-icon {
+      color: #999;
+      transition: transform 0.3s;
+    }
+  }
+
+  .dropdown-menu {
+    position: absolute;
+    top: 100%;
+    left: 0;
+    width: 100%;
+    max-height: 250px;
+    overflow-y: auto;
+    margin-top: 4px;
+    padding: 4px 0;
+    background-color: #fff;
+    border-radius: 2px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+    z-index: 1050;
+
+    .dropdown-item {
+      padding: 5px 12px;
+      line-height: 22px;
+      cursor: pointer;
+      transition: background 0.3s;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+
+      &:hover {
+        background-color: #f5f5f5;
+      }
+
+      &.is-active {
+        font-weight: 600;
+        background-color: #e6f7ff;
+        color: #1890ff;
+      }
+    }
+  }
+}
+</style>

+ 30 - 13
src/pages/card/index.vue

@@ -19,16 +19,16 @@
       </a-space>
     </div>
     <a-space>
-      <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>
+      <a-button v-if="Number(cardType) === 5" @click="openDefaultConfig(5)">基础配置</a-button>
+      <a-button v-if="Number(cardType) === 21"  @click="openDefaultConfig(21)">基础配置</a-button>
+      <a-button v-if="Number(cardType) === 5"  @click="openState.configButtonOpen = true">按钮配置</a-button>
+      <a-button v-if="Number(cardType) === 5" type="primary" @click="openConfigCard(5)">打开配置板</a-button>
+      <a-button v-if="Number(cardType) === 21" type="primary" @click="openConfigCard(21)">打开配置板</a-button>
     </a-space>
   </div>
 
   <CardTemplate
-    v-if="Number(cardInfo.card_type) === 5"
+    v-if="Number(cardType) === 5"
     ref="cardTemplateDom"
     @operation="handleOperation"
     :cardData="cardInfo"
@@ -71,7 +71,7 @@
   </a-drawer>
    <!-- 默认配置 -->
   <modal-pro
-    label="默认配置"
+    label="基础配置"
     :open='openState.defaultConfigOpen'
     @close="openState.defaultConfigOpen = false"
     @confirm="saveDefaultConfig"
@@ -79,11 +79,12 @@
 
     styles="position: fixed;top: 60px;right: 90px; pointer-events: none;"
   >
-   <CardDefault  ref="cardDefaultDom"  />
+   <CardDefault  ref="cardDefaultDom"  :config="cardJson" />
+
   </modal-pro>
 
   <modal-pro
-    label="默认配置"
+    label="基础配置"
     :open='openState.defaultConfig21Open'
     @close="openState.defaultConfig21Open = false"
     @confirm="saveDefault21Config"
@@ -138,7 +139,7 @@ const operationTip = `
 
 const cardInfo = useRoute().query as unknown as API.Card
 
-const cardTye = ref<5 | 21>()
+const cardType = ref<5 | 21>(5)
 
 const openState = reactive({
   configCardOpen: false,
@@ -197,7 +198,22 @@ const onSave = async () => {
   const defaultConfigVaild = Object.keys(defaultJson).filter(x => !!defaultJson[x]).findIndex(key => !defaultJson[key].music_name) !== -1
   // && defaultConfigVaild
   if (isValids.cardIsValid && isValids.buttonIsValid) {
-    await CardController.add(cardJson)
+    const { data } = await cardDefaultDom.value.generateDefaultJson()
+    console.log({
+      ...cardJson,
+      header: {
+        ...cardJson.header,
+        title: { music_name: data[0].music_name, is_break: 1, page: cardJson.header.title.page }
+      }
+    })
+
+    await CardController.add({
+      ...cardJson,
+      header: {
+        ...cardJson.header,
+        title: { music_name: data[0].music_name, is_break: data[0].is_break, page: cardJson.header.title.page }
+      }
+    })
     message.success('卡片配置成功')
     return
   }
@@ -280,7 +296,9 @@ const handleOperation = ({ ids }: {ids: number[]}) => {
 
 const getCardJsonById = async () => {
   const { data, cardType: type } = await CardController.cardJsonById(cardInfo.id!)
-  cardTye.value = type
+  cardType.value = type
+  console.log('卡片类型:', type)
+
   if (type === 5) {
     cardJson.header = data.header
     cardJson.slide_knob = data.slide_knob
@@ -310,7 +328,6 @@ function onBack () { window.history.back() }
 
 onMounted(() => {
   getCardJsonById()
-  console.log('卡片类型', cardInfo.card_type)
 })
 </script>
 <style lang='less' scoped >

+ 11 - 12
src/pages/card/preview.vue

@@ -1,19 +1,19 @@
 <template>
   <div class="preview-page">
-    <a-row :gutter="[32, 16]" justify="flex-start" style="margin-top: 24px;">
+    <a-row :gutter="[24, 16]" justify="flex-start" style="margin-top: 24px;">
       <a-col
-        :xs="4"
-        :sm="4"
-        :md="4"
-        :lg="4"
-        :xl="4"
+        :xs="3"
+        :sm="3"
+        :md="3"
+        :lg="3"
+        :xl="3"
         v-for="card in cardList"
         :key="card.id"
-        style="margin-bottom: 16px;"
+        style="margin-bottom: 16px;height: 340px;"
       >
-        <a-card @click="handleClick(card)" :bordered="false" hoverable style="width: 100%;height: 320px;" :title="card.name" >
+        <a-card @click="handleClick(card)" :bordered="false" hoverable style="width: 100%;height: 100%;" :title="card.name" >
           <div  class="card-bg" >
-             <img style="width: 50%;Object-fit: contain;" :src="card.thumbnail" alt="example" />
+             <img style="width: 80%;Object-fit: contain;display: block;" :src="card.thumbnail" alt="example" />
           </div>
         </a-card>
       </a-col>
@@ -45,8 +45,7 @@ const handleClick = (card: API.Card) => {
       id: card.id,
       name: card.name,
       img: card.img,
-      parentId: card.parentId,
-      card_type: Number(card.page) % 2 !== 0 ? 21 : 5
+      parentId: card.parentId
     }
   })
 }
@@ -71,7 +70,7 @@ onMounted(() => {
 .preview-page {
   .card-bg {
     width: 100%;
-    height: 100%;
+    height: 90%;
     display: flex;
     justify-content: center;
     align-items: center;

+ 7 - 0
src/typeing.d.ts

@@ -53,6 +53,13 @@ declare namespace API {
     state: 'stopped' | 'playing' | 'paused'
   }
 
+  /* is_break 音乐是否需要打断 */
+  /*  0,能被其他音频打断,不能打断其他音频;
+      1,能被其他音频打断,能打断其他音频;
+      2,不能被其他音频打断,不能打断其他音频;
+      3,不能被其他音频打断,能打断其他音频;
+  */
+
   type is_break = 0 | 1 | 2 | 3
 
   type Btn_select = 'G' | 'R' | 'P' | 'T' | 'B' | 'Y'