Bläddra i källkod

fix: rule7的配置

lvkun996 8 månader sedan
förälder
incheckning
ecdde4e44b

+ 7 - 6
src/controller/CardController.ts

@@ -9,15 +9,16 @@ export class CardController {
   ]
 
   static ruleList = [
-    { title: '单点单播', value: 7, desc: '无err_key, 只加ok_key' },
-    { title: '无序多点', value: 9, desc: '单点操作,多个答案的无序单选' },
-    { title: '有序单点', value: 10, desc: '单点操作,多个正确答案的有序单选' },
+    { title: '单点单播', value: 7, desc: '无err_key, 只加ok_key' }, // 点一下播放一个语音,此时语音放在ok_key里
+    { title: '无序多点', value: 9, desc: '单点操作,多个答案的无序单选' }, // 点完所有答案播放语音,语音放在ok_key_voice里 按照步骤
+    { title: '有序单点', value: 10, desc: '单点操作,多个正确答案的有序单选' }, // 点完所有答案播放语音,语音放在ok_key_voice里 按照步骤
     // { title: '双点', value: 11, desc: '' },
     // { title: '无序多选(有一个固定按钮)', value: 12, desc: '' },
     // { title: '有序多选(有一个固定按钮)', value: 13, desc: '' },
-    { title: '无序多选', value: 14, desc: '' },
-    { title: '有序多选', value: 15, desc: '' },
-    { title: 'pk对战', value: 16, desc: '' },
+    { title: '无序多选', value: 14, desc: '' }, // 每组有两个答案,每个答案有一个语音,语音放在ok_key_voice里 按照步骤
+
+    { title: '有序多选', value: 15, desc: '' }, // 每组有两个答案,每个答案有一个语音,语音放在ok_key_voice里 按照步骤
+    { title: 'pk对战', value: 16, desc: '' }, // ok_key下的item同时有两项语音放在ok_key_voice里 按照步骤,错误的err_key数量位置,语音放在err_key_voice里
     { title: '有序单点和无序单点混搭', value: 17, desc: '' }, // 17时order代表是有序单点还是无序单点
     { title: '新手指引', value: 18, desc: '' }
   ]

+ 20 - 21
src/pages/card/components/card-template-21.vue

@@ -15,6 +15,15 @@ const props = defineProps({
   }
 })
 
+const cardButtonRowMap = new Map([
+  [1, 'A'],
+  [2, 'B'],
+  [3, 'C'],
+  [4, 'D'],
+  [5, 'E'],
+  [6, 'F']
+])
+
 const canvasRef = ref<HTMLCanvasElement | null>(null)
 const img = new window.Image()
 img.src = props.cardData.img
@@ -32,29 +41,18 @@ 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 {
+  for (let row = 1; row < 7; row++) {
+    for (let col = 1; col < 7; col++) {
       rects.push({
-        id: item.id,
-        name: item.name,
-        x: startX + item.col * (rectWidth + rectGapX),
-        y: startY + item.row * (rectHeight + rectGapY),
+        id: row * 6 + col,
+        name: cardButtonRowMap.get(row)! + col,
+        x: startX + (col - 1) * (rectWidth + rectGapX),
+        y: startY + row * (rectHeight + rectGapY),
         width: rectWidth,
         height: rectHeight
       })
     }
-  })
+  }
 }
 
 // 图片状态
@@ -146,7 +144,7 @@ function draw () {
       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
+      const isSelected = rect.id === state.selectedRectId
       ctx.strokeStyle = isSelected ? '#ff6600' : '#007bff'
       ctx.globalAlpha = 0.5
       ctx.fillStyle = isSelected ? '#ff660033' : '#007bff22'
@@ -282,7 +280,7 @@ function onMouseUp (e?: MouseEvent) {
     })
     selection.selecting = false
     draw()
-    handleOperation(selected)
+    if (selected.length > 0) handleOperation(selected)
     return
   }
   state.dragging = false
@@ -309,11 +307,12 @@ function onClick (e: MouseEvent) {
       break
     }
   }
-  handleOperation([found!])
+  found && handleOperation([found])
   draw()
 }
 
 onMounted(() => {
+  generateRects()
   img.onload = () => {
     resizeCanvas()
   }

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

@@ -25,28 +25,10 @@
                 :name="['default', index, 'music_name']"
                 :rules="[{ required: true, message: '请选择音频' }]"
               >
-               <a-select
-                  v-model:value="item.music_name"
-                  show-search
-                  placeholder="输入或者搜索音频名"
-                  :default-active-first-option="false"
-                  :show-arrow="false"
-                  :filter-option="false"
-                  :not-found-content="null"
-                  :options="audioList"
-                  @blur="searchKey = ''"
-                  @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>
+                <SelectAudioNew
+                  v-model="item.music_name"
+                  placeholder="请选择或搜索音频"
+                />
               </a-form-item>
 
               <a-form-item
@@ -66,8 +48,7 @@
 <script lang='ts'  setup >
 import { reactive, watch, onMounted, ref, Ref, defineExpose, computed } from 'vue'
 import { MediaController } from '@/controller/index'
-import SelectAudio from './select-audio.vue'
-import { audioManager } from '@/utils/AudioManaer'
+import SelectAudioNew from './select-audio-new.vue'
 
 const props = defineProps<{
   config: API.CardJson
@@ -84,10 +65,6 @@ const formDom = ref()
 // 当前表单的验证状态 为true的时候 父组件校验通过
 const isValid = ref(false)
 
-const searchKey = ref('')
-
-const audioList = ref<API.Audio[]>([])
-
 const touchKeyIdsComputed = computed(() => props.config.touch_key.filter(item => item.selected).map(x => x.id) || [])
 
 watch(
@@ -137,46 +114,6 @@ watch(
   }
 )
 
-const handleChange = () => {
-  searchKey.value = ''
-  getAudioList()
-}
-
-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()
-}
-
-let debounceTimer: ReturnType<typeof setTimeout> | null = null
-
-const handleSearch = (text: string) => {
-  if (debounceTimer) clearTimeout(debounceTimer)
-  debounceTimer = setTimeout(() => {
-    searchKey.value = text
-    getAudioList()
-    // 这里可以放实际的搜索逻辑
-  }, 300)
-}
-
-const getAudioList = async () => {
-  const { data } = await MediaController.getAudioList({ page: 1, key: searchKey.value })
-  audioList.value = data
-  console.log('getAudioList:', data)
-}
-
 const onDelete = (item: API.CardJson['touch_key'][0]) => {
   item.selected = false
   emits('delete', item.id)
@@ -205,7 +142,6 @@ const generateConfigfCardJson = () => {
 }
 
 onMounted(() => {
-  getAudioList()
 })
 
 defineExpose({

+ 18 - 72
src/pages/card/components/config-game.vue

@@ -1,9 +1,11 @@
 <template>
   <modal-pro
     :label="getModalTitle()"
-    :open="true"
+    :open="open"
     :mask="false"
     width="540px"
+    @close="emit('update:open', false)"
+    @ok="emit('update:open', false)"
     styles="position: fixed;top: 60px;right: 90px; pointer-events: none;"
   >
     <template #title>
@@ -87,36 +89,13 @@
           <a-empty  style="margin: 0 auto;" v-if="cardJson.game_list[currentGameIndex].items.length === 0" description="暂无游戏"> </a-empty>
         </div>
 
-        <!-- 三级内容:游戏步骤配置 -->
-        <div v-else-if="currentLevel === 2">
+        <!-- 三级内容:游戏步骤配置 v-else-if="currentLevel === 2" -->
+        <div >
           <div class="card-config-content">
-            <a-button type="primary" size="small" @click="createGameStep" style="margin-bottom: 16px;">
-              <template #icon><plus-outlined /></template>
-              新建游戏步骤
-            </a-button>
-            <draggable
-              v-if="cardJson.game_list[currentGameIndex]?.items?.[currentSubFolderIndex]?.steps"
-              class="step-list"
-              v-model="cardJson.game_list[currentGameIndex].items[currentSubFolderIndex].steps"
-              item-key="id"
-            >
-              <template #item="{ element: step, index }">
-                <div
-                  class="step-item"
-                  :class="{ 'step-item-active': activeStepIndex === index }"
-                  @click="selectStep(index)"
-                >
-                  <div class="step-number">步骤 {{ index + 1 }}</div>
-                  <div class="step-circles">
-                    <div class="circle" v-for="(circle, cIndex) in step.circles" :key="cIndex"></div>
-                  </div>
-                  <div class="step-actions">
-                    <delete-outlined @click.stop="deleteGameStep(index)" style="color: #ff4d4f;" />
-                  </div>
-                </div>
-              </template>
-            </draggable>
-            <a-empty v-if="cardJson.game_list[currentGameIndex].items.length === 0" description="暂无步骤"> </a-empty>
+            <!-- :item="cardJson.game_list[currentGameIndex].items[currentSubFolderIndex]" -->
+            <gameStage3
+
+            />
           </div>
         </div>
       </div>
@@ -239,10 +218,15 @@ import { LeftOutlined, PlusOutlined, SmallDashOutlined, EditOutlined, DeleteOutl
 import { reactive, ref } from 'vue'
 import { message } from 'ant-design-vue'
 import SelectAudioNew from './select-audio-new.vue'
-import draggable from 'vue-draggable-next'
+import gameStage3 from './game-stage-3.vue'
 
-// 文件夹数据
-const folders = reactive<{ name: string; rule: string; mainSubject: string }[]>([])
+const props = defineProps({
+  open: {
+    type: Boolean,
+    default: false
+  }
+})
+const emit = defineEmits(['update:open'])
 
 // 一级folders的form
 const game1FormJson = {
@@ -257,7 +241,7 @@ const game1FormJson = {
 }
 
 // 当前层级:0表示顶级文件夹,1表示sub文件夹,2表示游戏配置
-const currentLevel = ref(0)
+const currentLevel = ref(2)
 
 // 点击路径的index
 const currentFolderIndex = reactive({
@@ -313,9 +297,6 @@ const currentGameIndex = ref(0)
 // 当前选中的子文件夹索引
 const currentSubFolderIndex = ref(0)
 
-// 游戏步骤相关
-const activeStepIndex = ref<number | null>(null)
-
 // 获取弹窗标题
 const getModalTitle = () => {
   switch (currentLevel.value) {
@@ -473,41 +454,6 @@ const enterCardConfig = (index: number) => {
     }
   }
   currentLevel.value = 2
-  activeStepIndex.value = null // 重置选中的步骤
-}
-
-// 创建游戏步骤
-const createGameStep = () => {
-  const rule = cardJson.game_list[currentGameIndex.value].rule
-  const currentItem = cardJson.game_list[currentGameIndex.value]?.items?.[currentSubFolderIndex.value]
-  console.log('创建游戏步骤', currentItem)
-
-  if (currentItem) {
-    if (!currentItem.steps) {
-      currentItem.steps = []
-    }
-    const newStep = {
-      id: Date.now(), // 使用时间戳作为唯一ID
-      circles: [{}, {}, {}] // 示例:每个步骤包含3个小圆圈
-    }
-    currentItem.steps.push(newStep)
-  }
-}
-
-// 删除游戏步骤
-const deleteGameStep = (index: number) => {
-  const currentItem = cardJson.game_list[currentGameIndex.value]?.items?.[currentSubFolderIndex.value]
-  if (currentItem?.steps) {
-    currentItem.steps.splice(index, 1)
-    if (activeStepIndex.value === index) {
-      activeStepIndex.value = null
-    }
-  }
-}
-
-// 选中步骤
-const selectStep = (index: number) => {
-  activeStepIndex.value = index
 }
 
 // 返回上级文件夹

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

@@ -1,9 +1,278 @@
 <template>
-vue3
+  <div class="game-stage-3">
+    <div class="header">
+      <a-button type="primary" size="small" @click="showAddStepModal">
+        <template #icon><plus-outlined /></template>
+        新增步骤
+      </a-button>
+    </div>
+<!--      -->
+
+    <VueDraggableNext
+      :list="steps"
+      group="people"
+      item-key="id"
+      :animation="300"
+      @end="onDragEnd"
+    >
+      <div v-for="(element, index) in steps" :key="element.id" class="step-item-wrapper">
+        <div
+          class="step-item"
+          :class="{ 'step-item-active': activeStepIndex === index }"
+          @click="selectStep(index)"
+        >
+          <div class="step-info">
+            <span class="step-number">步骤 {{ index + 1 }}</span>
+            <div class="step-details">
+              <span>按钮: <strong>{{ element.button_key }}</strong></span>
+              <span>音频: <strong>{{ element.audio_name }}</strong></span>
+            </div>
+          </div>
+          <div class="step-actions">
+            <a-popconfirm
+              title="确定要删除这个步骤吗?"
+              ok-text="确定"
+              cancel-text="取消"
+              @confirm.stop="deleteGameStep(index)"
+              @cancel.stop
+            >
+              <delete-outlined @click.stop />
+            </a-popconfirm>
+          </div>
+        </div>
+      </div>
+    </VueDraggableNext>
+    <!-- <a-empty v-else description="暂无步骤,请点击新增步骤" /> -->
+
+    <a-modal
+      v-model:open="addStepModalVisible"
+      title="新增步骤"
+      width="600px"
+      @ok="handleAddStep"
+      @cancel="cancelAddStep"
+      wrap-class-name="game-stage-3-modal"
+    >
+      <a-form :model="newStepForm" layout="vertical">
+        <a-form-item label="选择按钮">
+          <div class="button-grid">
+            <a-button
+              v-for="btn in buttonOptions"
+              :key="btn"
+              :type="newStepForm.button_key === btn ? 'primary' : 'default'"
+              @click="selectButton(btn)"
+            >
+              {{ btn }}
+            </a-button>
+          </div>
+        </a-form-item>
+        <a-form-item label="选择语音">
+          <SelectAudioNew
+            v-model="newStepForm.audio_name"
+            placeholder="请选择步骤语音"
+          />
+        </a-form-item>
+      </a-form>
+    </a-modal>
+  </div>
 </template>
-<script lang='ts'  setup >
 
+<script lang="ts" setup>
+import { ref, computed, reactive } from 'vue'
+import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue'
+import { VueDraggableNext } from 'vue-draggable-next'
+import SelectAudioNew from './select-audio-new.vue'
+import { message } from 'ant-design-vue'
+
+interface Step {
+  id: number;
+  button_key: string;
+  audio_name: string;
+}
+
+interface Props {
+  item: API.CardJson21['game_list']['0']['items']['0'];
+}
+
+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 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: '',
+  audio_name: ''
+})
+
+const buttonOptions = Array.from({ length: 6 }, (_, i) => String.fromCharCode(65 + i))
+  .flatMap(row => Array.from({ length: 6 }, (_, j) => `${row}${j + 1}`))
+
+const showAddStepModal = () => {
+  resetNewStepForm()
+  addStepModalVisible.value = true
+}
+
+const selectButton = (btn: string) => {
+  newStepForm.button_key = btn
+}
+
+const handleAddStep = () => {
+  if (!newStepForm.button_key) {
+    message.error('请选择一个按钮')
+    return
+  }
+  if (!newStepForm.audio_name) {
+    message.error('请选择一个语音')
+    return
+  }
+
+  const newStep: Step = {
+    id: Date.now(),
+    button_key: newStepForm.button_key,
+    audio_name: newStepForm.audio_name
+  }
+
+  // const newSteps = [...(steps.value || []), newStep]
+
+  steps.value.push(newStep)
+  console.log(' steps.value:', steps.value)
+
+  // emits('update:modelValue', newSteps)
+  addStepModalVisible.value = false
+}
+
+const cancelAddStep = () => {
+  addStepModalVisible.value = false
+}
+
+const resetNewStepForm = () => {
+  newStepForm.button_key = ''
+  newStepForm.audio_name = ''
+}
+
+const deleteGameStep = (index: number) => {
+  if (steps.value) {
+    steps.value.splice(index, 1)
+    emits('update:modelValue', steps.value)
+    if (activeStepIndex.value === index) {
+      activeStepIndex.value = null
+    }
+  }
+}
+
+const selectStep = (index: number) => {
+  activeStepIndex.value = index
+}
+
+const onDragEnd = () => {
+  // draggable already updated the model
+  emits('update:modelValue', steps.value)
+}
 </script>
-<style lang='less' scoped >
 
+<style lang="less" scoped>
+.game-stage-3 {
+  .header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+  }
+
+  .step-item-wrapper {
+    margin-bottom: 12px;
+  }
+
+  .step-item {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 16px;
+    border: 1px solid #d9d9d9;
+    border-radius: 8px;
+    cursor: grab;
+    background-color: #fafafa;
+    transition: all 0.3s;
+
+    &:hover {
+      border-color: #40a9ff;
+      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
+    }
+  }
+
+  .step-item-active {
+    border-color: #1890ff;
+    background-color: #e6f7ff;
+    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+  }
+
+  .step-info {
+    display: flex;
+    align-items: center;
+    gap: 24px;
+  }
+
+  .step-number {
+    font-weight: 600;
+    font-size: 16px;
+  }
+
+  .step-details {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+    font-size: 14px;
+    color: #555;
+
+    span > strong {
+      color: #333;
+      margin-left: 8px;
+    }
+  }
+
+  .step-actions {
+    cursor: pointer;
+    color: #ff4d4f;
+    font-size: 16px;
+  }
+
+}
+</style>
+<style lang="less">
+.game-stage-3-modal .button-grid {
+  display: grid;
+  grid-template-columns: repeat(6, 1fr);
+  gap: 10px;
+  padding: 16px;
+  border-radius: 6px;
+  background-color: #f7f7f7;
+  border: 1px solid #e8e8e8;
+}
+
+.game-stage-3-modal .button-grid .ant-btn {
+  padding: 0;
+  height: 40px;
+  font-weight: 500;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
 </style>

+ 53 - 0
src/pages/card/components/game-stage-4.vue

@@ -0,0 +1,53 @@
+<template>
+  <div class="lists-container">
+    <div class="list-column">
+      <h3>Todo</h3>
+      <draggable
+        v-model="todoList"
+        group="tasks"
+        class="drag-area"
+        :animation="150"
+      >
+        <div
+          v-for="item in todoList"
+          :key="item.id"
+          class="task-item"
+        >
+          {{ item.text }}
+        </div>
+      </draggable>
+    </div>
+
+    <div class="list-column">
+      <h3>Done</h3>
+      <draggable
+        v-model="doneList"
+        group="tasks"
+        class="drag-area"
+        :animation="150"
+      >
+        <div
+          v-for="item in doneList"
+          :key="item.id"
+          class="task-item done"
+        >
+          {{ item.text }}
+        </div>
+      </draggable>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { VueDraggableNext as draggable } from 'vue-draggable-next'
+
+const todoList = ref([
+  { id: 1, text: 'Learn Vue 3' },
+  { id: 2, text: 'Build awesome apps' }
+])
+
+const doneList = ref([
+  { id: 3, text: 'Read documentation' }
+])
+</script>

+ 8 - 6
src/pages/card/index.vue

@@ -20,8 +20,9 @@
     </div>
     <a-space>
       <a-button  @click="openDefaultConfig">默认配置</a-button>
-      <a-button  @click="openState.configButtonOpen = true">按钮配置</a-button>
-      <a-button type="primary" @click="openConfigCard">打开配置板</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-space>
   </div>
 
@@ -80,7 +81,7 @@
   </modal-pro>
 
   <!-- catetype = 21 时  的游戏分层配置 -->
-     <GameStage />
+    <GameStage  v-model:open="openState.configGameOpen" />
   <!-- catetype = 21 时 的 游戏配置 -->
 
   <modal-pro
@@ -119,6 +120,7 @@ const cardInfo = useRoute().query as unknown as API.Card
 
 const openState = reactive({
   configCardOpen: false,
+  configGameOpen: false,
   defaultConfigOpen: false,
   configButtonOpen: false
 })
@@ -196,8 +198,8 @@ const saveButtonConfig = () => {
   verify && onSave()
 }
 
-const openConfigCard = () => {
-  openState.configCardOpen = true
+const openConfigCard = (type: 5 | 21) => {
+  type === 5 ? openState.configCardOpen = true : openState.configGameOpen = true
 }
 
 const openDefaultConfig = () => {
@@ -266,7 +268,7 @@ const getCardJson21 = async () => {
 function onBack () { window.history.back() }
 
 onMounted(() => {
-  cardInfo.card_type === 5 ? getCardJson5() : getCardJson21()
+  Number(cardInfo.card_type) === 5 ? getCardJson5() : getCardJson21()
   console.log('卡片类型', cardInfo.card_type)
 })
 </script>