Browse Source

同步代码

JutarryWu 5 months ago
parent
commit
76dd5d4928

+ 44 - 9
.idea/workspace.xml

@@ -5,9 +5,30 @@
   </component>
   <component name="ChangeListManager">
     <list default="true" id="b9dc0b93-aea2-4509-84d8-c1e57bc059b1" name="更改" comment="">
-      <change beforePath="$PROJECT_DIR$/.env.development" beforeDir="false" afterPath="$PROJECT_DIR$/.env.development" afterDir="false" />
-      <change beforePath="$PROJECT_DIR$/.env.production" beforeDir="false" afterPath="$PROJECT_DIR$/.env.production" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/assets/icons/correct.svg" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/assets/icons/pentagram.svg" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/assets/icons/wrong.svg" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/assets/images/bg-main.png" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/assets/images/bg-soa.png" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/components/CountDown/index.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/components/RoundSlider/index.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/components/VoiceImp/index.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/components/WuIsCorrect/index.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/views/cognitiveTasks/BreadthTraining/BTRandomPentagram.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/views/cognitiveTasks/BreadthTraining/index.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/views/cognitiveTasks/ContinueAddition/index.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/views/cognitiveTasks/PictureNaming/Topics.json" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/views/cognitiveTasks/PictureNaming/index.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/views/cognitiveTasks/PicturePuzzle/Topics.json" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/views/cognitiveTasks/PicturePuzzle/components/PPCountDown/index.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/views/cognitiveTasks/PicturePuzzle/components/PicturePuzzleChild/index.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/views/cognitiveTasks/PicturePuzzle/index.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/views/cognitiveTasks/cocos/index.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/views/cognitiveTasks/main/index.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/src/views/cognitiveTasks/spatialOrientationAbility/index.vue" afterDir="false" />
       <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/src/types/components.d.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/types/components.d.ts" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/src/types/typed-router.d.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/types/typed-router.d.ts" afterDir="false" />
     </list>
     <option name="SHOW_DIALOG" value="false" />
     <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -28,7 +49,7 @@
   <component name="Git.Settings">
     <option name="RECENT_BRANCH_BY_REPOSITORY">
       <map>
-        <entry key="$PROJECT_DIR$" value="dev-20240819" />
+        <entry key="$PROJECT_DIR$" value="dev" />
       </map>
     </option>
     <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@@ -41,6 +62,10 @@
             <RecentBranchesForRepo>
               <option name="branches">
                 <list>
+                  <RecentBranch>
+                    <option name="branchName" value="20240919-Jutarry" />
+                    <option name="lastUsedInstant" value="1726732719" />
+                  </RecentBranch>
                   <RecentBranch>
                     <option name="branchName" value="dev" />
                     <option name="lastUsedInstant" value="1726731162" />
@@ -73,7 +98,7 @@
   <component name="PropertiesComponent">{
   &quot;keyToString&quot;: {
     &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
-    &quot;git-widget-placeholder&quot;: &quot;dev&quot;,
+    &quot;git-widget-placeholder&quot;: &quot;20240919-Jutarry&quot;,
     &quot;javascript.nodejs.core.library.configured.version&quot;: &quot;20.13.1&quot;,
     &quot;javascript.nodejs.core.library.typings.version&quot;: &quot;20.13.0&quot;,
     &quot;last_opened_file_path&quot;: &quot;E:/WorkSpace/Web/insomnia-cognition-h5/src/components&quot;,
@@ -98,12 +123,14 @@
   <component name="RecentsManager">
     <key name="CopyFile.RECENT_KEYS">
       <recent name="E:\WorkSpace\Web\insomnia-cognition-h5\src\components" />
+      <recent name="E:\WorkSpace\Web\insomnia-cognition-h5\src\views" />
       <recent name="E:\WorkSpace\Web\insomnia-cognition-h5\public" />
       <recent name="E:\WorkSpace\Web\insomnia-cognition-h5\src\pages\cognitiveTasks" />
       <recent name="E:\WorkSpace\Web\insomnia-cognition-h5\src\api" />
-      <recent name="E:\WorkSpace\Web\insomnia-cognition-h5\src\assets\images" />
     </key>
     <key name="MoveFile.RECENT_KEYS">
+      <recent name="E:\WorkSpace\Web\insomnia-cognition-h5\src\assets\images" />
+      <recent name="E:\WorkSpace\Web\insomnia-cognition-h5\src\assets\icons" />
       <recent name="E:\WorkSpace\Web\insomnia-cognition-h5\src\styles" />
     </key>
   </component>
@@ -142,7 +169,7 @@
       <workItem from="1725608929876" duration="1001000" />
       <workItem from="1726126375188" duration="978000" />
       <workItem from="1726196050458" duration="5789000" />
-      <workItem from="1726726752821" duration="5138000" />
+      <workItem from="1726726752821" duration="5807000" />
     </task>
     <task id="LOCAL-00001" summary="新增图片">
       <option name="closed" value="true" />
@@ -368,7 +395,15 @@
       <option name="project" value="LOCAL" />
       <updated>1726732470277</updated>
     </task>
-    <option name="localTasksCounter" value="29" />
+    <task id="LOCAL-00029" summary="修改后台请求接口路径">
+      <option name="closed" value="true" />
+      <created>1726732700840</created>
+      <option name="number" value="00029" />
+      <option name="presentableId" value="LOCAL-00029" />
+      <option name="project" value="LOCAL" />
+      <updated>1726732700840</updated>
+    </task>
+    <option name="localTasksCounter" value="30" />
     <servers />
   </component>
   <component name="TypeScriptGeneratedFilesManager">
@@ -418,7 +453,6 @@
     </option>
   </component>
   <component name="VcsManagerConfiguration">
-    <MESSAGE value="feat" />
     <MESSAGE value="倒计时组件" />
     <MESSAGE value="feat:倒计时组件&#10;创建倒计时组件" />
     <MESSAGE value="feat(组件类): 新增倒计时组件" />
@@ -443,6 +477,7 @@
     <MESSAGE value="feat(认知任务): 部署服务后优化" />
     <MESSAGE value="feat(认知任务): 优化" />
     <MESSAGE value="init" />
-    <option name="LAST_COMMIT_MESSAGE" value="init" />
+    <MESSAGE value="修改后台请求接口路径" />
+    <option name="LAST_COMMIT_MESSAGE" value="修改后台请求接口路径" />
   </component>
 </project>

+ 1 - 0
src/assets/icons/correct.svg

@@ -0,0 +1 @@
+<svg 
 xmlns="http://www.w3.org/2000/svg"
 xmlns:xlink="http://www.w3.org/1999/xlink"
 width="49px" height="43px">
<defs>
<filter filterUnits="userSpaceOnUse" id="Filter_0" x="-1px" y="-1px" width="50px" height="44px"  >
                <feOffset in="SourceAlpha" dx="0" dy="2" />
                <feGaussianBlur result="blurOut" stdDeviation="0" />
                <feFlood flood-color="rgb(39, 48, 113)" result="floodOut" />
                <feComposite operator="atop" in="floodOut" in2="blurOut" />
                <feComponentTransfer><feFuncA type="linear" slope="0.4"/></feComponentTransfer>
                <feMerge>
    <feMergeNode/>
    <feMergeNode in="SourceGraphic"/>
  </feMerge>
            </filter>

</defs>
<g filter="url(#Filter_0)">
<path fill-rule="evenodd"  stroke="rgb(255, 255, 255)" stroke-width="2px" stroke-linecap="butt" stroke-linejoin="miter" fill="rgb(100, 197, 17)"
 d="M9.0,11.999 C9.0,11.999 11.499,11.59 13.0,11.0 C14.589,10.937 15.187,11.771 15.226,12.409 L19.199,16.375 C19.199,16.375 19.762,16.912 20.193,16.375 C20.624,15.838 32.115,3.486 32.115,3.486 C32.115,3.486 33.828,1.276 36.89,3.486 C38.350,5.696 44.37,11.418 44.37,11.418 C44.37,11.418 45.572,12.781 43.43,15.383 C40.515,17.985 22.180,36.203 22.180,36.203 C22.180,36.203 19.241,39.118 16.219,36.203 C13.197,33.288 3.303,23.314 3.303,23.314 C3.303,23.314 2.0,22.377 2.0,21.0 C2.0,19.942 3.303,18.358 3.303,18.358 L9.0,11.999 Z"/>
</g>
<path fill-rule="evenodd"  fill="rgb(255, 255, 255)"
 d="M5.293,19.535 L7.884,16.944 C8.274,16.553 8.907,16.553 9.298,16.944 C9.688,17.334 9.688,17.967 9.298,18.358 L6.706,20.949 C6.316,21.340 5.683,21.340 5.293,20.949 C4.902,20.559 4.902,19.925 5.293,19.535 Z"/>
<path fill-rule="evenodd"  fill="rgb(255, 255, 255)"
 d="M10.127,15.253 L11.255,14.125 C11.425,13.955 11.701,13.955 11.871,14.125 C12.41,14.295 12.41,14.571 11.871,14.740 L10.743,15.868 C10.572,16.39 10.297,16.39 10.127,15.868 C9.957,15.699 9.957,15.423 10.127,15.253 Z"/>
</svg>

File diff suppressed because it is too large
+ 0 - 0
src/assets/icons/pentagram.svg


+ 1 - 0
src/assets/icons/wrong.svg

@@ -0,0 +1 @@
+<svg 
 xmlns="http://www.w3.org/2000/svg"
 xmlns:xlink="http://www.w3.org/1999/xlink"
 width="43px" height="42px">
<defs>
<filter filterUnits="userSpaceOnUse" id="Filter_0" x="-1px" y="-1px" width="44px" height="43px"  >
                <feOffset in="SourceAlpha" dx="0" dy="2" />
                <feGaussianBlur result="blurOut" stdDeviation="0" />
                <feFlood flood-color="rgb(39, 48, 113)" result="floodOut" />
                <feComposite operator="atop" in="floodOut" in2="blurOut" />
                <feComponentTransfer><feFuncA type="linear" slope="0.4"/></feComponentTransfer>
                <feMerge>
    <feMergeNode/>
    <feMergeNode in="SourceGraphic"/>
  </feMerge>
            </filter>

</defs>
<g filter="url(#Filter_0)">
<path fill-rule="evenodd"  stroke="rgb(255, 255, 255)" stroke-width="2px" stroke-linecap="butt" stroke-linejoin="miter" fill="rgb(245, 50, 50)"
 d="M31.456,19.166 L37.486,24.825 C39.76,26.316 39.120,28.777 37.586,30.323 L32.366,35.582 C30.832,37.127 28.300,37.170 26.710,35.679 L20.376,29.736 L13.935,35.880 C12.355,37.387 9.838,37.343 8.313,35.782 L3.124,30.468 C1.599,28.906 1.644,26.418 3.224,24.911 L9.180,19.230 L3.727,14.113 C2.137,12.622 2.92,10.160 3.627,8.615 L8.847,3.356 C10.381,1.811 12.913,1.768 14.503,3.259 L20.259,8.660 L26.69,3.118 C27.649,1.611 30.166,1.656 31.690,3.217 L36.879,8.531 C38.404,10.92 38.360,12.580 36.780,14.87 L31.456,19.166 Z"/>
</g>
<path fill-rule="evenodd"  fill="rgb(255, 255, 255)"
 d="M5.292,10.535 L7.884,7.943 C8.274,7.553 8.908,7.553 9.298,7.943 C9.689,8.334 9.689,8.968 9.298,9.358 L6.707,11.949 C6.316,12.340 5.683,12.340 5.292,11.949 C4.902,11.559 4.902,10.926 5.292,10.535 Z"/>
<path fill-rule="evenodd"  fill="rgb(255, 255, 255)"
 d="M10.127,6.253 L11.255,5.125 C11.425,4.954 11.701,4.954 11.871,5.125 C12.41,5.295 12.41,5.571 11.871,5.741 L10.743,6.869 C10.573,7.39 10.297,7.39 10.127,6.869 C9.957,6.699 9.957,6.423 10.127,6.253 Z"/>
</svg>

BIN
src/assets/images/bg-main.png


BIN
src/assets/images/bg-soa.png


+ 71 - 0
src/components/CountDown/index.vue

@@ -0,0 +1,71 @@
+<script setup lang="ts">
+/*
+   * 组件名: CountDown
+   * 组件用途: 倒计时组件
+   * 创建日期: 2024/8/16
+   * 编写者: JutarryWu
+   */
+const props = defineProps({
+  time: {
+    type: Number,
+    default: 0,
+  },
+  text: {
+    type: String,
+    default: '测试训练即将开始!',
+  },
+  color: {
+    type: String,
+    default: 'black',
+  },
+  size: {
+    type: Number,
+    default: 36,
+  },
+})
+const emit = defineEmits(['endCountDown'])
+const countTimer = ref<ReturnType<typeof setInterval> | null>(null)
+
+const secondNum = ref(0)
+const countDownStr = ref('')
+const showSpan = ref(false)
+
+async function exec() {
+  secondNum.value = props.time
+  countDownStr.value = props.text
+
+  countTimer.value = setInterval(() => {
+    countDownStr.value = `${secondNum.value}`
+    showSpan.value = true
+    secondNum.value--
+    if (secondNum.value < 0) {
+      clearInterval(Number(countTimer.value))
+      countTimer.value = null
+      showSpan.value = false
+
+      emit('endCountDown')
+    }
+  }, 1000)
+}
+
+onMounted(() => {
+  exec()
+})
+
+onBeforeUnmount(() => {
+  clearInterval(Number(countTimer.value))
+  countTimer.value = null
+  showSpan.value = false
+})
+</script>
+
+<template>
+  <section
+    class="van-count-down-container absolute-center h-[45px] w-[90%] translate-[-50%] text-center align-bottom"
+    :style="{ color, fontSize: `${size}px` }"
+  >
+    {{ countDownStr }}<span v-if="showSpan" class="ml-[4px] mt-[8px] text-[28px]">s</span>
+  </section>
+</template>
+
+<style scoped lang="scss"></style>

+ 229 - 0
src/components/RoundSlider/index.vue

@@ -0,0 +1,229 @@
+<script setup lang="ts">
+import { onMounted, ref } from 'vue'
+
+const props = defineProps({
+  centerName: {
+    type: String,
+    default: '中心点',
+  },
+  topName: {
+    type: String,
+    default: '',
+  },
+  moveName: {
+    type: String,
+    default: '3333',
+  },
+})
+const emits = defineEmits(['curValue'])
+
+// 定义角度值
+const angle = ref(0)
+const angleDiff = ref(0)
+
+// 获取滑块元素
+const sliderElement = ref<HTMLDivElement | null>(null)
+
+// 鼠标按下事件
+function onMouseDown(event: MouseEvent) {
+  startDrag(event)
+}
+
+// 触摸开始事件
+function onTouchStart(event: TouchEvent) {
+  startDrag(event.touches[0])
+}
+
+// 开始拖动
+function startDrag(event: MouseEvent | Touch) {
+  document.addEventListener('mousemove', onMouseMove)
+  document.addEventListener('mouseup', onMouseUp)
+  document.addEventListener('touchmove', onTouchMove)
+  document.addEventListener('touchend', onMouseUp)
+
+  // 计算初始角度
+  const rect = sliderElement.value!.getBoundingClientRect()
+  const x = event.clientX - (rect.left + rect.width / 2)
+  const y = event.clientY - (rect.top + rect.height / 2)
+  angle.value = calculateAngle(x, y) + 90
+  angleDiff.value = calculateAngleDiff(angle.value)
+  emits('curValue', angleDiff.value)
+}
+
+// 鼠标移动事件
+function onMouseMove(event: MouseEvent) {
+  updateAngle(event)
+}
+
+// 触摸移动事件
+function onTouchMove(event: TouchEvent) {
+  updateAngle(event.touches[0])
+}
+
+// 更新角度
+function updateAngle(event: MouseEvent | Touch) {
+  const rect = sliderElement.value!.getBoundingClientRect()
+  const x = event.clientX - (rect.left + rect.width / 2)
+  const y = event.clientY - (rect.top + rect.height / 2)
+  angle.value = calculateAngle(x, y) + 90
+  angleDiff.value = calculateAngleDiff(angle.value)
+  emits('curValue', angleDiff.value)
+}
+
+// 鼠标抬起事件
+function onMouseUp() {
+  document.removeEventListener('mousemove', onMouseMove)
+  document.removeEventListener('mouseup', onMouseUp)
+  document.removeEventListener('touchmove', onTouchMove)
+  document.removeEventListener('touchend', onMouseUp)
+}
+
+// 计算角度
+function calculateAngle(x: number, y: number): number {
+  const rad = Math.atan2(y, x)
+  const deg = (rad * 180) / Math.PI
+  return deg >= 0 ? deg : 360 + deg
+}
+
+// 计算角度差
+function calculateAngleDiff(currentAngle: number): number {
+  let diff = currentAngle
+  if (diff > 360 && diff <= 450) {
+    diff -= 360
+  }
+  else if (diff >= 180 && diff <= 360) {
+    diff = 360 - diff
+  }
+  return diff
+}
+
+onMounted(() => {
+  if (sliderElement.value) {
+    // 初始化角度
+    angle.value = calculateAngle(0, 0) // 假设初始位置在正上方
+    angleDiff.value = calculateAngleDiff(angle.value)
+  }
+
+  const touchArea = document.getElementById('touchArea')
+  // 监听触摸事件
+  touchArea.addEventListener('touchstart', (event) => {
+    // 记录开始触摸的位置
+    const xStart = event.touches[0].pageX
+    touchArea.addEventListener('touchmove', (event) => {
+      // 计算移动的距离
+      const xMove = event.touches[0].pageX - xStart
+      // 如果移动的距离大于0,则为左滑,小于0则为右滑
+      if (Math.abs(xMove) > 0) {
+        // 阻止页面滚动
+        event.preventDefault()
+        // 执行滑动处理
+        // console.log(`Swipe detected, distance: ${xMove}`)
+      }
+    })
+  }, false)
+})
+</script>
+
+<template>
+  <div class="slider-container">
+    <div
+      id="touchArea"
+      ref="sliderElement"
+      class="circle-slider"
+      @mousedown="onMouseDown"
+      @mousemove="onMouseMove"
+      @mouseup="onMouseUp"
+      @touchstart="onTouchStart"
+      @touchmove="onTouchMove"
+      @touchend="onMouseUp"
+    >
+      <div class="center-point" />
+      <div class="absolute-center z-399 w-[100px] text-center text-[16px] text-[#3A3A3A]">
+        {{ props.centerName }}
+      </div>
+      <div class="target-point flex-column flex-center text-[16px] text-[#3A3A3A]">
+        <van-icon name="play" class="rotate-90" />
+        <span>{{ props.topName }}</span>
+      </div>
+      <div class="move-point" :style="{ transform: `translateX(-50%) rotate(${angle}deg)` }">
+        <span class="absolute left-26px block w-[180px] text-[16px] text-[#F87171]">{{ props.moveName }}</span>
+      </div>
+      <img
+        src="@/assets/images/task/spatialOrientationAbility/icon_pointer.png"
+        class="absolute left-[50%] top-[38px] z-19 h-[150px] w-[20px] translate-x-[-50%]"
+        alt=""
+      >
+      <img
+        src="@/assets/images/task/spatialOrientationAbility/icon_pointer_red.png"
+        class="red-line absolute left-[50%] top-[40px] z-19 h-[130px] w-[4px] rounded-[2px]"
+        :style="{ transform: `translateX(-50%) rotate(${angle}deg)` }"
+        alt=""
+      >
+      <span class="absolute top-[295px] block text-[15px] text-[#3A3A3A]">{{ angleDiff.toFixed(5) }}°</span>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.slider-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  position: relative;
+}
+
+.circle-slider {
+  position: relative;
+  width: 280px; /* 直径 */
+  height: 280px; /* 直径 */
+  border-radius: 50%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background-image: url('@/assets/images/task/spatialOrientationAbility/bg_circle.png');
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: center; /* 可选,让图片居中对齐 */
+}
+
+.center-point {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 48px;
+  height: 48px;
+  border-radius: 50%;
+  z-index: 99;
+  background-image: url('@/assets/images/task/spatialOrientationAbility/icon_center.png');
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: center; /* 可选,让图片居中对齐 */
+}
+
+.target-point {
+  position: absolute;
+  top: -40px;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 160px;
+  height: 40px;
+}
+
+.move-point {
+  position: absolute;
+  top: 20px;
+  left: 50%;
+  transform-origin: center 550%;
+  width: 22px;
+  height: 22px;
+  border-radius: 50%;
+  background-color: #f3f3f3 !important;
+  box-shadow: 0 0 4px 0 #000 !important;
+  cursor: move;
+}
+
+.red-line {
+  transform-origin: 50% 76%;
+}
+</style>

+ 71 - 0
src/components/VoiceImp/index.vue

@@ -0,0 +1,71 @@
+<script setup lang="ts">
+/*
+ * 组件名: VoiceImp
+ * 组件用途: 声音反馈组件
+ * 创建日期: 2024/8/7
+ * 编写者: JutarryWu
+ */
+const VoiceUrlObj: Record<string, string> = {
+  click: '/static/voice/click.mp3',
+  right: '/static/voice/right.mp3',
+  error: '/static/voice/error.mp3',
+  bg: '/static/voice/bg-music.mp3',
+}
+const VideoRef_1 = ref<HTMLVideoElement>()
+const VideoRef_2 = ref<HTMLVideoElement>()
+const VideoRef_3 = ref<HTMLVideoElement>()
+const VideoRef_bg = ref<HTMLVideoElement>()
+
+function videoPlay(type: string = 'click') {
+  if (type === 'click') {
+    VideoRef_1.value?.play()
+  }
+  else if (type === 'right') {
+    VideoRef_2.value?.play()
+  }
+  else if (type === 'error') {
+    VideoRef_3.value?.play()
+  }
+  else if (type === 'bg') {
+    VideoRef_bg.value?.play()
+  }
+}
+
+function videoPause(type: string = 'bg') {
+  if (type === 'bg') {
+    VideoRef_bg.value?.pause()
+  }
+}
+
+defineExpose({
+  videoPlay,
+  videoPause,
+})
+</script>
+
+<template>
+  <section class="voice-container">
+    <video ref="VideoRef_1" :src="VoiceUrlObj.click" />
+    <video ref="VideoRef_2" :src="VoiceUrlObj.right" />
+    <video ref="VideoRef_3" :src="VoiceUrlObj.error" />
+    <video ref="VideoRef_bg" :src="VoiceUrlObj.bg" loop />
+  </section>
+</template>
+
+<style scoped lang="less">
+.voice-container {
+  width: 0;
+  height: 0;
+  overflow: hidden;
+
+  video {
+    position: absolute;
+    width: 0;
+    height: 0;
+    opacity: 0;
+    z-index: -1;
+    left: 1000vw;
+    top: 1000vh;
+  }
+}
+</style>

+ 23 - 0
src/components/WuIsCorrect/index.vue

@@ -0,0 +1,23 @@
+<script setup lang="ts">
+const props = defineProps({
+  correct: {
+    type: Boolean,
+    default: false,
+    required: true,
+    immediate: true,
+  },
+})
+</script>
+
+<template>
+  <div class="flex-center absolute bottom-[6px] right-[6px]">
+    <transition name="el-fade-in">
+      <SvgIcon v-if="props.correct" name="correct" class="text-[34px]" />
+    </transition>
+    <transition name="el-fade-in">
+      <SvgIcon v-if="!props.correct" name="wrong" class="text-[34px]" />
+    </transition>
+  </div>
+</template>
+
+<style scoped lang="less"></style>

+ 4 - 0
src/types/components.d.ts

@@ -10,6 +10,7 @@ declare module 'vue' {
     AppSetting: typeof import('./../components/AppSetting/index.vue')['default']
     Auth: typeof import('./../components/Auth/index.vue')['default']
     AuthAll: typeof import('./../components/AuthAll/index.vue')['default']
+    CountDown: typeof import('./../components/CountDown/index.vue')['default']
     HBadge: typeof import('./../ui-kit/HBadge.vue')['default']
     HButton: typeof import('./../ui-kit/HButton.vue')['default']
     HDialog: typeof import('./../ui-kit/HDialog.vue')['default']
@@ -20,6 +21,7 @@ declare module 'vue' {
     NotAllowed: typeof import('./../components/NotAllowed/index.vue')['default']
     PageLayout: typeof import('./../components/PageLayout/index.vue')['default']
     PageMain: typeof import('./../components/PageMain/index.vue')['default']
+    RoundSlider: typeof import('./../components/RoundSlider/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default']
@@ -27,5 +29,7 @@ declare module 'vue' {
     VanFieldCalendar: typeof import('./../components/VanFieldCalendar/index.vue')['default']
     VanFieldDatePicker: typeof import('./../components/VanFieldDatePicker/index.vue')['default']
     VanFieldPicker: typeof import('./../components/VanFieldPicker/index.vue')['default']
+    VoiceImp: typeof import('./../components/VoiceImp/index.vue')['default']
+    WuIsCorrect: typeof import('./../components/WuIsCorrect/index.vue')['default']
   }
 }

+ 8 - 0
src/types/typed-router.d.ts

@@ -20,6 +20,14 @@ declare module 'vue-router/auto-routes' {
   export interface RouteNamedMap {
     '/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
     '/[...all]': RouteRecordInfo<'/[...all]', '/:all(.*)', { all: ParamValue<true> }, { all: ParamValue<false> }>,
+    '/cognitiveTasks/BreadthTraining/': RouteRecordInfo<'/cognitiveTasks/BreadthTraining/', '/cognitiveTasks/BreadthTraining', Record<never, never>, Record<never, never>>,
+    '/cognitiveTasks/BreadthTraining/BTRandomPentagram': RouteRecordInfo<'/cognitiveTasks/BreadthTraining/BTRandomPentagram', '/cognitiveTasks/BreadthTraining/BTRandomPentagram', Record<never, never>, Record<never, never>>,
+    '/cognitiveTasks/cocos/': RouteRecordInfo<'/cognitiveTasks/cocos/', '/cognitiveTasks/cocos', Record<never, never>, Record<never, never>>,
+    '/cognitiveTasks/ContinueAddition/': RouteRecordInfo<'/cognitiveTasks/ContinueAddition/', '/cognitiveTasks/ContinueAddition', Record<never, never>, Record<never, never>>,
+    '/cognitiveTasks/main/': RouteRecordInfo<'/cognitiveTasks/main/', '/cognitiveTasks/main', Record<never, never>, Record<never, never>>,
+    '/cognitiveTasks/PictureNaming/': RouteRecordInfo<'/cognitiveTasks/PictureNaming/', '/cognitiveTasks/PictureNaming', Record<never, never>, Record<never, never>>,
+    '/cognitiveTasks/PicturePuzzle/': RouteRecordInfo<'/cognitiveTasks/PicturePuzzle/', '/cognitiveTasks/PicturePuzzle', Record<never, never>, Record<never, never>>,
+    '/cognitiveTasks/spatialOrientationAbility/': RouteRecordInfo<'/cognitiveTasks/spatialOrientationAbility/', '/cognitiveTasks/spatialOrientationAbility', Record<never, never>, Record<never, never>>,
     'login': RouteRecordInfo<'login', '/login', Record<never, never>, Record<never, never>>,
     'reload': RouteRecordInfo<'reload', '/reload', Record<never, never>, Record<never, never>>,
     '/user/': RouteRecordInfo<'/user/', '/user', Record<never, never>, Record<never, never>>,

+ 137 - 0
src/views/cognitiveTasks/BreadthTraining/BTRandomPentagram.vue

@@ -0,0 +1,137 @@
+<script setup lang="ts">
+/*
+ * 组件名: BTRandomPentagram
+ * 组件用途: 随机生成不重复且有一定间隔的五角星
+ * 创建日期: 2024/6/3
+ * 编写者: JutarryWu
+ */
+import { onMounted } from 'vue'
+
+interface pentagram {
+  x: number
+  y: number
+  angle: number
+}
+const $props = defineProps({
+  count: {
+    type: Number,
+    default: 5,
+  },
+  minDistance: {
+    type: Number,
+    default: 100,
+  },
+  changeShow: {
+    type: Boolean,
+    default: false,
+  },
+})
+const $emits = defineEmits(['reOver'])
+// 星星的宽度
+const starWidth = 30
+// 星星的高度
+const starHeight = 30
+
+// 游戏面板宽度
+const gameWidth = ref(350)
+// 游戏面板高度
+const gameHeight = ref(500)
+
+const pentagramList = ref<pentagram[]>([])
+
+function generateRandomAngle() {
+  return Math.random() * 180
+}
+
+/**
+ * 在指定区域内(width, height), 随机生成指定数量、不重复且有间距(minDistance)的随机点
+ * @param count
+ * @param minDistance
+ * @param width
+ * @param height
+ */
+function generateRandomPoints(count: number, minDistance: number, width: number, height: number): pentagram[] {
+  const points: pentagram[] = []
+  while (points.length < count) {
+    const x = Math.random() * (width - $props.minDistance - 48)
+    const y = Math.random() * (height - $props.minDistance - 48)
+    let validPoint = true
+    for (let i = 0; i < points.length; i++) {
+      const p = points[i]
+      const dx = Math.abs(x - p.x)
+      const dy = Math.abs(y - p.y)
+      const distance = Math.sqrt(dx * dx + dy * dy)
+      if (distance < minDistance) {
+        validPoint = false
+        break
+      }
+    }
+    if (validPoint) {
+      points.push({ x, y, angle: generateRandomAngle() })
+    }
+  }
+  return points
+}
+
+function reGeneratePentagramList() {
+  pentagramList.value = generateRandomPoints($props.count, $props.minDistance, gameWidth.value, gameHeight.value)
+  $emits('reOver')
+}
+
+async function exec() {
+  reGeneratePentagramList()
+}
+
+onMounted(() => {
+  exec()
+})
+
+// 暴露变量
+defineExpose({
+  reGeneratePentagramList,
+})
+</script>
+
+<template>
+  <section class="b-t-random-pentagram-container glass-mask flex-center rounded-[8px]">
+    <div class="pos-relative" :style="{ width: `${gameWidth}px`, height: `${gameHeight}px` }">
+      <img
+        v-for="p in pentagramList"
+        :key="p.x + p.y"
+        :style="{ left: `${p.x}px`, top: `${p.y}px`, transform: `rotate(${p.angle}deg)` }"
+        class="pentagram-svg pos-absolute"
+        :class="{ 'change-show': !changeShow }"
+        :width="starWidth"
+        :height="starHeight"
+        src="@/assets/images/task/breadthTraining/star.png"
+        alt=""
+      >
+    </div>
+  </section>
+</template>
+
+<style scoped lang="less">
+.b-t-random-pentagram-container {
+  border: 1px solid #ffc400;
+
+  .pentagram-svg {
+    opacity: 1;
+    transition:
+      opacity 80ms linear,
+      left 240ms linear,
+      top 240ms linear;
+
+    &.change-show {
+      left: 500px !important;
+      top: 300px !important;
+      opacity: 0;
+    }
+  }
+}
+
+.glass-mask {
+  background-color: rgba(255, 255, 255, 0.2);
+  backdrop-filter: blur(0.01769912rem);
+  -webkit-backdrop-filter: blur(0.01769912rem);
+}
+</style>

+ 331 - 0
src/views/cognitiveTasks/BreadthTraining/index.vue

@@ -0,0 +1,331 @@
+<script setup lang="ts">
+/*
+ * 组件名: BreadthTraining
+ * 组件用途: 广度训练
+ * 创建日期: 2024/8/16
+ * 编写者: JutarryWu
+ */
+// import { shuffle } from 'lodash-es'
+
+import { shuffle } from 'lodash-es'
+import CountUp from 'vue-countup-v3'
+import { showSuccessToast } from 'vant'
+import BTRandomPentagram from './BTRandomPentagram.vue'
+import GameAPI, { type GameResultVO, type GameVO } from '@/api/game'
+
+const router = useRouter()
+interface IMainData {
+  totalScore: number
+  dataList: any[]
+}
+
+interface IMainChildData {
+  count?: number
+  score?: number
+  extraScore?: number
+}
+
+const subjectInfo = ref<GameVO>({
+  name: '广度训练',
+})
+
+let selectArray: any[] = reactive([])
+const BTRandomPentagramRef = ref()
+const tempBeginScore = ref(0)
+const tempEndScore = ref(0)
+const changeShow = ref(false)
+const choiceAreaShow = ref(true)
+const currentIndex = ref(0) // 当前序列
+const mainData: IMainData = reactive({
+  totalScore: 0,
+  dataList: [],
+})
+
+// 当前进度
+// const percentage = computed(() => {
+//   return Math.round((currentIndex.value / mainData.dataList.length) * 100)
+// })
+const selectiveIndex = ref(-1) // 选择的索引
+// 游戏收集的数据
+const gameData: GameResultVO = {
+  finish: '1',
+  gameId: subjectInfo.value.id,
+  gameName: subjectInfo.value.name,
+  userId: sessionStorage.getItem('userId'),
+  paramList: [],
+  levelList: [],
+}
+/**
+ * 生成基础数据
+ */
+function generateBaseData() {
+  // 生成选项组
+  const selectDataList = [
+    ...Array.from({ length: 5 }).fill([2, 4, 5, 9, 7]),
+    ...Array.from({ length: 5 }).fill([3, 8, 5, 6, 7]),
+
+    ...Array.from({ length: 5 }).fill([1, 9, 6, 7, 8]),
+    ...Array.from({ length: 5 }).fill([4, 5, 6, 2, 3]),
+
+    ...Array.from({ length: 5 }).fill([11, 10, 7, 8, 9]),
+    ...Array.from({ length: 5 }).fill([5, 6, 7, 4, 8]),
+
+    ...Array.from({ length: 5 }).fill([6, 7, 8, 9, 5]),
+    ...Array.from({ length: 5 }).fill([7, 8, 9, 10, 11]),
+
+    ...Array.from({ length: 5 }).fill([7, 8, 9, 5, 6]),
+    ...Array.from({ length: 5 }).fill([8, 9, 10, 11, 12]),
+
+    ...Array.from({ length: 5 }).fill([8, 9, 10, 11, 12]),
+    ...Array.from({ length: 5 }).fill([6, 7, 8, 9, 10]),
+
+    ...Array.from({ length: 5 }).fill([10, 11, 12, 13, 14]),
+    ...Array.from({ length: 5 }).fill([8, 9, 10, 11, 13]),
+
+    ...Array.from({ length: 5 }).fill([12, 13, 14, 15, 16]),
+    ...Array.from({ length: 5 }).fill([8, 10, 11, 12, 13]),
+  ]
+  selectArray = selectDataList.map((item) => {
+    return shuffle(item as number[])
+  })
+
+  // 生成主体数据
+  const baseDataList = [
+    // { count: 5, score: 10, extraScore: 0 }, { count: 5, score: 10, extraScore: 0 }, ...
+    // { count: 5, score: 10, extraScore: 0 }, { count: 5, score: 10, extraScore: 0 }, ...
+    ...Array.from({ length: 10 }).fill({ count: 5, score: 10, extraScore: 0 }),
+    ...Array.from({ length: 10 }).fill({ count: 6, score: 20, extraScore: 0 }),
+    ...Array.from({ length: 10 }).fill({ count: 7, score: 30, extraScore: 0 }),
+    ...Array.from({ length: 10 }).fill({ count: 8, score: 40, extraScore: 0 }),
+    ...Array.from({ length: 10 }).fill({ count: 9, score: 50, extraScore: 0 }),
+    ...Array.from({ length: 10 }).fill({ count: 10, score: 60, extraScore: 0 }),
+    ...Array.from({ length: 10 }).fill({ count: 11, score: 70, extraScore: 0 }),
+    ...Array.from({ length: 10 }).fill({ count: 12, score: 80, extraScore: 0 }),
+  ]
+
+  mainData.dataList = baseDataList.map((item: IMainChildData, index) => {
+    return {
+      index,
+      ...item,
+      ...{ reactionTime: 0, beginTime: 0, endTime: 0, choice: '' },
+    }
+  })
+
+  mainData.dataList = shuffle(mainData.dataList)
+}
+
+/**
+ * 下一步
+ */
+function nextClick() {
+  tempBeginScore.value = mainData.totalScore
+  if (currentIndex.value < mainData.dataList.length - 1) {
+    selectiveIndex.value = -1
+    currentIndex.value++
+    changeShow.value = false
+    setTimeout(() => {
+      mainData.dataList[currentIndex.value].beginTime = Date.now()
+      BTRandomPentagramRef.value.reGeneratePentagramList()
+      choiceAreaShow.value = true
+    }, 320)
+  }
+  else {
+    // console.log('游戏结束')
+    sendData()
+  }
+}
+
+/**
+ * 选择点击事件
+ *   1、将选择的数量choice 赋值给当前序列的choice
+ *   2、计算当前序列的得分
+ *   3、计算当前序列反应时间,判断反应时间是否超过2000ms,超过则加30分
+ *   4、隐式计算CountUp的变化数值:tempBeginScore,tempEndScore
+ *   5、判断是否超过80,超过则结束
+ * @param choice
+ * @param index
+ */
+function choiceClick(choice: number, index: number) {
+  setTimeout(() => {
+    choiceAreaShow.value = false
+  }, 400)
+  if (mainData.dataList[currentIndex.value].choice === '') {
+    selectiveIndex.value = index
+    mainData.dataList[currentIndex.value].endTime = Date.now()
+    mainData.dataList[currentIndex.value].reactionTime = Date.now() - mainData.dataList[currentIndex.value].beginTime
+    mainData.dataList[currentIndex.value].choice = choice
+    if (mainData.dataList[currentIndex.value].count === choice) {
+      tempEndScore.value += mainData.dataList[currentIndex.value].score
+      if (mainData.dataList[currentIndex.value].reactionTime <= 2000) {
+        mainData.dataList[currentIndex.value].extraScore = 30
+        setTimeout(() => {
+          tempBeginScore.value = tempEndScore.value
+          tempEndScore.value += 30
+        }, 1200)
+      }
+      mainData.totalScore
+        += mainData.dataList[currentIndex.value].extraScore + mainData.dataList[currentIndex.value].score
+    }
+
+    setTimeout(() => {
+      nextClick()
+    }, 2300)
+  }
+}
+
+/**
+ * 重新开始
+ */
+function reOverFn() {
+  setTimeout(() => {
+    changeShow.value = true
+  }, 320)
+  setTimeout(() => {
+    changeShow.value = false
+  }, 2000)
+}
+
+const showCountDown = ref(false) // 显示倒计时
+
+function exec() {
+  showCountDown.value = false
+  changeShow.value = true
+}
+
+function sendData() {
+  const { totalReactionTime, gameTime } = mainData.dataList.reduce(
+    (obj, it) => {
+      obj.gameTime = obj.gameTime + (it.endTime - it.beginTime)
+      obj.totalReactionTime += it.reactionTime
+      return obj
+    },
+    { gameTime: 0, totalReactionTime: 0 },
+  )
+
+  gameData.paramList = [
+    {
+      code: 'score',
+      name: '得分',
+      value: mainData.totalScore,
+    },
+    {
+      code: 'gameTime',
+      name: '游戏用时',
+      value: `${(gameTime / 1000).toFixed(2)}s`,
+    },
+    {
+      code: 'avrTime',
+      name: '平均反应时长',
+      value: `${Math.ceil(totalReactionTime / mainData.dataList.length)}ms`,
+    },
+    {
+      code: 'attentionBreadthPath',
+      name: '注意广度曲线图',
+      value: JSON.stringify(mainData.dataList),
+    },
+    {
+      code: 'attentionBreadth',
+      name: '注意广度',
+      value: JSON.stringify(mainData.dataList),
+    },
+  ]
+
+  GameAPI.add(gameData).then(() => {
+    showSuccessToast('本次训练已结束')
+    setTimeout(() => {
+      router.go(-1)
+    }, 1300)
+  })
+}
+
+onMounted(() => {
+  const temp = sessionStorage.getItem('subjectInfo')
+  if (temp) {
+    subjectInfo.value = JSON.parse(temp)
+  }
+  showCountDown.value = true
+  generateBaseData()
+  mainData.dataList[currentIndex.value].beginTime = Date.now()
+})
+</script>
+
+<template>
+  <section class="app-container">
+    <van-nav-bar class="self-nav-bar" :title="subjectInfo.name" left-arrow @click-left="router.go(-1)" />
+    <count-down v-if="showCountDown" :time="5" color="#fff" @end-count-down="exec" />
+
+    <div v-else class="breadth-training-container w-full flex flex-col items-center gap-y-[15px]">
+      <BTRandomPentagram
+        ref="BTRandomPentagramRef"
+        :count="mainData.dataList[currentIndex]?.count"
+        :min-distance="20"
+        :change-show="changeShow"
+        class="mt-[10px]"
+        @re-over="reOverFn"
+      />
+      <div class="flex flex-row items-center text-[16px] text-white">
+        <span>累计得分:</span>
+        <CountUp :start-val="tempBeginScore" :end-val="tempEndScore" :duration="1.8" />
+      </div>
+      <div v-if="choiceAreaShow" class="w-full flex flex-row items-center justify-around">
+        <div
+          v-for="(item, index) in selectArray[mainData.dataList[currentIndex]?.index]"
+          :key="`${currentIndex}${index}`"
+          class="pos-relative min-w-[68px] cursor-pointer rounded-[8px] bg-[#ffffff] p-y-3 text-center text-[24px] text-[#3A3A3A]"
+          @click="choiceClick(item, index)"
+        >
+          {{ item }}
+          <transition name="el-fade-in">
+            <img
+              v-if="selectiveIndex === index && item === mainData.dataList[currentIndex].count"
+              src="@/assets/images/task/breadthTraining/correct.png"
+              alt="correct"
+              width="20"
+              height="17"
+              class="pos-absolute bottom-[4px] right-[4px]"
+            >
+          </transition>
+          <transition name="el-fade-in">
+            <img
+              v-if="selectiveIndex === index && item !== mainData.dataList[currentIndex].count"
+              src="@/assets/images/task/breadthTraining/wrong.png"
+              alt="wrong"
+              width="20"
+              height="20"
+              class="pos-absolute bottom-[4px] right-[4px]"
+            >
+          </transition>
+        </div>
+      </div>
+    </div>
+  </section>
+</template>
+
+<style scoped lang="less">
+.app-container {
+  background-image: url('/static/image/game/bg-breadthTraining.png');
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: center; /* 可选,让图片居中对齐 */
+
+  :deep(.el-progress-bar__outer) {
+    background-color: rgba(0, 0, 0, 0.1);
+  }
+
+  :deep(.van-nav-bar) {
+    &.self-nav-bar {
+      .van-nav-bar__title {
+        color: #ffffff;
+      }
+
+      .van-icon {
+        color: #ffffff;
+      }
+    }
+  }
+
+  .breadth-training-container {
+    height: calc(100% - 46px);
+  }
+}
+</style>

+ 248 - 0
src/views/cognitiveTasks/ContinueAddition/index.vue

@@ -0,0 +1,248 @@
+<script setup lang="ts">
+import { showSuccessToast } from 'vant'
+import GameAPI, { type GameResultVO, type GameVO, type Result, type ResultLevel } from '@/api/game'
+
+const router = useRouter()
+
+const subjectInfo = ref<GameVO>({})
+
+// 控制倒计时的显隐
+const showCountDown = ref(true)
+// 显示题目文本
+const showSpanText = ref('')
+// 测试总次数
+const totalCount = ref(0)
+// 当前题答案
+const currentAnswer = ref(0)
+// 正确总次数
+const rightCount = ref(0)
+// 当前轮正确次数
+const currentRoundRightCount = ref(0)
+// 连续对3次
+const rightThree = ref(0)
+// 连续错两次
+const wrongTwo = ref(0)
+// 当前轮需要计算的数字个数
+const additionNumCount = ref(2)
+// 统计每个层级答对个数
+const rightCountList: Result[] = reactive([])
+// 测试时长 100 秒
+const gameDuration = ref(100 * 1000)
+// 游戏结束时间戳
+const gameEndTime = ref(0)
+// 游戏开始时间戳
+const gameStartTime = ref(0)
+// 游戏收集的数据
+const gameData: GameResultVO = {
+  finish: '1',
+  gameId: subjectInfo.value.id,
+  gameName: subjectInfo.value.name,
+  userId: sessionStorage.getItem('userId'),
+  paramList: [],
+  levelList: [],
+}
+
+function nNumRightCount() {
+  const isIndex = rightCountList.findIndex(item => Number.parseInt(item.name) === additionNumCount.value)
+  if (isIndex !== -1) {
+    if (typeof rightCountList[isIndex].value === 'number') {
+      rightCountList[isIndex].value++
+    }
+  }
+  else {
+    rightCountList.push({
+      name: additionNumCount.value.toString(),
+      value: 1,
+    })
+  }
+}
+
+function createComputeSpanText() {
+  // 生成下一道题和答案
+  const tempSpanText: number[] = []
+  let additionResult = 0
+  for (let i = additionNumCount.value; i > 0; i--) {
+    const tempNum = Math.floor(Math.random() * 10)
+    additionResult += tempNum
+    tempSpanText.push(tempNum)
+  }
+  showSpanText.value = tempSpanText.join('+')
+  currentAnswer.value = additionResult % 10
+}
+
+function userClick(answer: number) {
+  // 如果游戏时长大于或等于 100 秒,则游戏结束
+  gameEndTime.value = performance.now()
+  const duration = gameEndTime.value - gameStartTime.value
+  if (duration >= gameDuration.value) {
+    // 游戏结束,发送数据
+    sendData()
+    return
+  }
+
+  // 用户点击动作
+  totalCount.value++
+  if (answer === currentAnswer.value) {
+    // 正确次数
+    rightCount.value++
+    // 当前数字个数计算正确数加1
+    nNumRightCount()
+    // 本轮正确次数
+    currentRoundRightCount.value++
+
+    rightThree.value++
+    wrongTwo.value = 0
+    if (rightThree.value >= 3 && wrongTwo.value === 0) {
+      additionNumCount.value++
+      rightThree.value = 0
+      wrongTwo.value = 0
+    }
+  }
+  else {
+    // 正式测试时要做降级处理
+    currentRoundRightCount.value = 0
+    rightThree.value = 0
+    wrongTwo.value++
+    if (additionNumCount.value > 2 && rightThree.value === 0 && wrongTwo.value >= 2) {
+      additionNumCount.value--
+      rightThree.value = 0
+      wrongTwo.value = 0
+    }
+  }
+
+  // 生成下一道题
+  createComputeSpanText()
+}
+
+/**
+ * 倒计时结束时的回调
+ */
+function endCountDown() {
+  // 隐藏倒计时组件
+  showCountDown.value = false
+  // 开始生成游戏的第一道题
+  createComputeSpanText()
+  // 记录游戏开始时间戳
+  gameStartTime.value = performance.now()
+  // 重置游戏结束时间戳
+  gameEndTime.value = 0
+  // 重置收集的游戏数据
+}
+
+function sendData() {
+  const totalScore = rightCountList.reduce((acc, curr) => {
+    acc += curr.value as number * 2 ** (Number.parseInt(curr.name) - 2)
+    return acc
+  }, 0)
+
+  gameData.levelList = rightCountList.map((item) => {
+    return {
+      level: item.name,
+      levelParamList: [{ ...item }],
+    } as ResultLevel
+  })
+
+  gameData.paramList = [
+    {
+      code: 'totalScore',
+      name: '总分',
+      value: totalScore === 0 ? 0 : Number.parseFloat(Math.log2(totalScore).toString()).toFixed(2),
+    },
+    {
+      code: 'totalCount',
+      name: '题目总数',
+      value: totalCount.value,
+    },
+    {
+      code: 'rightCount',
+      name: '总正确数',
+      value: rightCount.value,
+    },
+  ]
+
+  GameAPI.add(gameData).then(() => {
+    showSuccessToast('本次训练已结束')
+    setTimeout(() => {
+      router.go(-1)
+    }, 1300)
+  })
+}
+
+onMounted(() => {
+  const temp = sessionStorage.getItem('subjectInfo')
+  if (temp) {
+    subjectInfo.value = JSON.parse(temp)
+  }
+})
+</script>
+
+<template>
+  <section class="app-container">
+    <van-nav-bar class="self-nav-bar" :title="subjectInfo.name" left-arrow @click-left="router.go(-1)" />
+    <count-down v-if="showCountDown" :time="5" color="white" @end-count-down="endCountDown" />
+    <div v-else style="" class="w-[90%] mx-auto mt-[15px] p-[15px] border-6 border-white border-solid rounded-[8px] bg-[#425363]">
+      <div class="h-[80px] flex flex-row justify-center items-center bg-[#D2E2F1] rounded-[8px]">
+        <span class="text-[40px] text-[#222222]">{{ showSpanText }}</span>
+      </div>
+      <div class="flex flex-row justify-between mt-[24px]">
+        <div class="w-[72px] h-[72px] flex flex-row justify-center items-center bg-white rounded-[8px]" @click="userClick(1)">
+          <span class="text-[32px] text-[#222222]">1</span>
+        </div>
+        <div class="w-[72px] h-[72px] flex flex-row justify-center items-center bg-white rounded-[8px]" @click="userClick(2)">
+          <span class="text-[32px] text-[#222222]">2</span>
+        </div>
+        <div class="w-[72px] h-[72px] flex flex-row justify-center items-center bg-white rounded-[8px]" @click="userClick(3)">
+          <span class="text-[32px] text-[#222222]">3</span>
+        </div>
+      </div>
+      <div class="flex flex-row justify-between mt-[24px]">
+        <div class="w-[72px] h-[72px] flex flex-row justify-center items-center bg-white rounded-[8px]" @click="userClick(4)">
+          <span class="text-[32px] text-[#222222]">4</span>
+        </div>
+        <div class="w-[72px] h-[72px] flex flex-row justify-center items-center bg-white rounded-[8px]" @click="userClick(5)">
+          <span class="text-[32px] text-[#222222]">5</span>
+        </div>
+        <div class="w-[72px] h-[72px] flex flex-row justify-center items-center bg-white rounded-[8px]" @click="userClick(6)">
+          <span class="text-[32px] text-[#222222]">6</span>
+        </div>
+      </div>
+      <div class="flex flex-row justify-between mt-[24px]">
+        <div class="w-[72px] h-[72px] flex flex-row justify-center items-center bg-white rounded-[8px]" @click="userClick(7)">
+          <span class="text-[32px] text-[#222222]">7</span>
+        </div>
+        <div class="w-[72px] h-[72px] flex flex-row justify-center items-center bg-white rounded-[8px]" @click="userClick(8)">
+          <span class="text-[32px] text-[#222222]">8</span>
+        </div>
+        <div class="w-[72px] h-[72px] flex flex-row justify-center items-center bg-white rounded-[8px]" @click="userClick(9)">
+          <span class="text-[32px] text-[#222222]">9</span>
+        </div>
+      </div>
+      <div class="flex flex-row justify-center mt-[24px]">
+        <div class="w-[72px] h-[72px] flex flex-row justify-center items-center bg-white rounded-[8px]" @click="userClick(0)">
+          <span class="text-[32px] text-[#222222]">0</span>
+        </div>
+      </div>
+    </div>
+  </section>
+</template>
+
+<style scoped lang="less">
+.app-container {
+  background-image: url('/static/image/game/bg-continue-addition.png');
+  background-size: 100% 100%;
+  background-position: center center;
+  background-repeat: no-repeat;
+
+  :deep(.van-nav-bar) {
+    &.self-nav-bar {
+      .van-nav-bar__title {
+        color: #ffffff;
+      }
+
+      .van-icon {
+        color: #ffffff;
+      }
+    }
+  }
+}
+</style>

+ 47 - 0
src/views/cognitiveTasks/PictureNaming/Topics.json

@@ -0,0 +1,47 @@
+[
+  ["萝", "芹", "土", "蘑"],
+  ["橙", "苹", "草", "梨"],
+  ["桃", "苹", "草", "梨"],
+  ["豆", "白", "茄", "萝"],
+  ["茄", "豆", "白", "萝"],
+  ["黄", "南", "辣", "西"],
+  ["南", "黄", "辣", "西"],
+  ["芒", "榴", "苹", "菠"],
+  ["辣", "南", "黄", "西"],
+  ["西", "辣", "南", "黄"],
+  ["西", "南", "冬", "黄"],
+  ["蘑", "萝", "芹", "土"],
+  ["梨", "桃", "苹", "草"],
+  ["香", "桃", "苹", "草"],
+  ["樱", "桃", "苹", "草"],
+  ["菠", "芒", "榴", "苹"],
+  ["石", "芒", "菠", "苹"],
+  ["电", "汽", "卡", "拖"],
+  ["汽", "电", "卡", "拖"],
+  ["蓝", "排", "足", "乒"],
+  ["苹", "菠", "芒", "榴"],
+  ["狮", "猴", "狐", "猫"],
+  ["猪", "牛", "马", "鹿"],
+  ["熊", "猴", "狐", "猫"],
+  ["虎", "狐", "猫", "熊"],
+  ["狗", "狐", "猫", "猪"],
+  ["猫", "狐", "羊", "猪"],
+  ["鳄", "狐", "羊", "猪"],
+  ["燕", "鳄", "羊", "猪"],
+  ["象", "鹿", "鹦", "兔"],
+  ["蜜", "鸭", "斑", "鸡"],
+  ["鸭", "蜜", "斑", "鸡"],
+  ["斑", "蜜", "鸭", "鸡"],
+  ["鸡", "蜜", "鸭", "斑"],
+  ["兔", "象", "鹿", "鹦"],
+  ["鱼", "珊", "乌", "鹰"],
+  ["羊", "马", "牛", "鹿"],
+  ["马", "羊", "牛", "鹿"],
+  ["牛", "羊", "马", "鹿"],
+  ["鹿", "羊", "马", "牛"],
+  ["青", "河", "狐", "熊"],
+  ["河", "青", "狐", "熊"],
+  ["狐", "青", "河", "熊"],
+  ["熊", "青", "河", "狐"],
+  ["恐", "青", "河", "狐"]
+]

+ 284 - 0
src/views/cognitiveTasks/PictureNaming/index.vue

@@ -0,0 +1,284 @@
+<script setup lang="ts">
+/*
+ * 组件名: PictureNaming
+ * 组件用途: 图片命名
+ * 创建日期: 2024/8/20
+ * 编写者: JutarryWu
+ */
+import { shuffle } from 'lodash-es'
+import { showSuccessToast } from 'vant'
+import Topics from './Topics.json'
+import GameAPI, { type GameResultVO, type GameVO } from '@/api/game'
+
+interface IData {
+  choices?: string[]
+  img?: string
+  seconds?: number
+  correct?: string
+  isRight?: boolean
+  checked?: boolean
+  score?: number
+  reactionTime?: number
+}
+
+const router = useRouter()
+const VoiceImpRef = ref()
+const subjectInfo = ref<GameVO>({})
+const showCountDown = ref(true) // 显示倒计时
+const showImg = ref(true) // 显示图片
+const showText = ref(false) // 显示文字
+const musicFlag = ref(true) // 页面音乐开关
+const userCanClickFlag = ref(false) // 用户是否可以点击开关
+const showDataArr = ref<IData[]>([])
+const seconds = [3000, 2000, 1500]
+const currentIndex = ref(0) // 当前索引
+const selectiveIndex = ref(-1) //  用户选择的索引
+const textTimeout = ref<NodeJS.Timeout | null>(null)
+const imgTimeout = ref<NodeJS.Timeout | null>(null)
+const onceTimer = ref<NodeJS.Timeout | null>(null)
+let onceStart = 0 // 试次开始时间
+
+function initData() {
+  for (let i = 0; i < 3; i++) {
+    const tempArr = []
+    for (let j = 0; j < 15; j++) {
+      tempArr.push({
+        choices: shuffle(Topics[i * 15 + j]),
+        img: `/static/image/game/name/${i * 15 + j}.png`,
+        seconds: seconds[i], // 图片显示时长
+        correct: Topics[i * 15 + j][0], // 正确答案
+        isRight: false, // 是否正确
+        score: 0, // 正确: 1, 错误:0
+        reactionTime: 4000, // 反应时间(ms)
+        checked: false, // 是否选过
+      })
+    }
+    showDataArr.value = [...showDataArr.value, ...shuffle(tempArr)]
+  }
+}
+
+function musicClick(flag: number) {
+  musicFlag.value = !musicFlag.value
+  if (musicFlag.value) {
+    VoiceImpRef.value.videoPlay('click')
+    VoiceImpRef.value.videoPlay('bg', flag)
+  }
+  else {
+    VoiceImpRef.value.videoPause('bg')
+  }
+}
+
+function choiceClick(item: string, index: number) {
+  if (!userCanClickFlag.value) {
+    return
+  }
+  if (!showDataArr.value[currentIndex.value].checked) {
+    showDataArr.value[currentIndex.value].checked = true
+    selectiveIndex.value = index
+    if (item === showDataArr.value[currentIndex.value].correct) {
+      showDataArr.value[currentIndex.value].isRight = true
+      showDataArr.value[currentIndex.value].score = 1
+      showDataArr.value[currentIndex.value].reactionTime = Number((performance.now() - onceStart).toFixed(3))
+      if (musicFlag.value) {
+        VoiceImpRef.value.videoPlay('right')
+      }
+    }
+    else {
+      if (musicFlag.value) {
+        VoiceImpRef.value.videoPlay('error')
+      }
+    }
+    clearTimeout(textTimeout.value)
+    clearTimeout(imgTimeout.value)
+    clearTimeout(onceTimer.value)
+    setTimeout(() => {
+      userCanClickFlag.value = false
+      showText.value = false
+      showImg.value = false
+      currentIndex.value++
+      nextOnce()
+    }, 900)
+  }
+}
+
+function nextOnce() {
+  if (currentIndex.value < showDataArr.value.length) {
+    onceStart = performance.now()
+    selectiveIndex.value = -1
+    showImg.value = true
+
+    // 隐藏图片
+    imgTimeout.value = setTimeout(() => {
+      showImg.value = false
+      showText.value = true
+      userCanClickFlag.value = true
+      // 隐藏文字
+      textTimeout.value = setTimeout(() => {
+        showText.value = false
+      }, 3000)
+    }, showDataArr.value[currentIndex.value].seconds)
+
+    // 如果用户未点击,自动清除定时器,进入下一个试次
+    onceTimer.value = setTimeout(() => {
+      userCanClickFlag.value = false
+      showText.value = false
+      showImg.value = false
+      clearTimeout(textTimeout.value)
+      clearTimeout(imgTimeout.value)
+      clearTimeout(onceTimer.value)
+      currentIndex.value++
+      nextOnce()
+    }, 7000)
+  }
+  else {
+    submit()
+  }
+}
+
+function submit() {
+  let correctNum = 0
+  let wrongNum = 0
+  let accuracy = 0
+  let totalScore = 0
+  let averageReactionTimeForCorrect = 0
+  showDataArr.value.forEach((item) => {
+    if (item.isRight) {
+      correctNum++
+      totalScore++
+      averageReactionTimeForCorrect += item.reactionTime
+    }
+  })
+  wrongNum = 45 - correctNum
+  accuracy = Number((correctNum / 45).toFixed(4))
+  averageReactionTimeForCorrect = Number((averageReactionTimeForCorrect / correctNum).toFixed(3))
+
+  const data: GameResultVO = {
+    finish: '1',
+    gameId: subjectInfo.value.id,
+    gameName: subjectInfo.value.name,
+    paramList: [
+      {
+        code: 'correctNum',
+        name: '正确数',
+        value: correctNum,
+      },
+      {
+        code: 'wrongNum',
+        name: '错误数',
+        value: wrongNum,
+      },
+      {
+        code: 'accuracy',
+        name: '正确率',
+        value: accuracy,
+      },
+      {
+        code: 'totalScore',
+        name: '得分',
+        value: totalScore,
+      },
+      {
+        code: 'averageReactionTimeForCorrect',
+        name: '正确点击的反应时长均值',
+        value: `${averageReactionTimeForCorrect}ms`,
+      },
+    ],
+    userId: sessionStorage.getItem('userId'),
+  }
+  GameAPI.add(data).then(() => {
+    showSuccessToast({
+      className: 'rotate-toast',
+      message: '本次训练已结束',
+    })
+    setTimeout(() => {
+      router.go(-1)
+    }, 1300)
+  })
+}
+
+function exec() {
+  showCountDown.value = false
+  nextOnce()
+}
+
+onMounted(() => {
+  const temp = sessionStorage.getItem('subjectInfo')
+  if (temp) {
+    subjectInfo.value = JSON.parse(temp)
+  }
+  if (musicFlag.value) {
+    VoiceImpRef.value.videoPlay('bg')
+  }
+  initData()
+})
+
+onBeforeUnmount(() => {
+  clearInterval(onceTimer.value)
+})
+</script>
+
+<template>
+  <section class="app-container">
+    <van-nav-bar class="self-nav-bar" :title="subjectInfo.name" left-arrow @click-left="router.go(-1)">
+      <template #right>
+        <img v-if="musicFlag" src="/static/image/game/video/play.png" alt="" class="h-[34px] w-[34px]" @click="musicClick(0)">
+        <img v-else src="/static/image/game/video/stop.png" alt="" class="h-[34px] w-[34px]" @click="musicClick(1)">
+      </template>
+    </van-nav-bar>
+    <count-down v-if="showCountDown" :time="5" color="#fff" @end-count-down="exec" />
+    <VoiceImp ref="VoiceImpRef" />
+
+    <div v-if="!showCountDown && currentIndex < showDataArr.length" class="absolute-center flex-column h-[78%] w-[100%]">
+      <div class="center-div flex-center ml-[4%] h-[390px] w-[92%]">
+        <img v-if="showImg" :src="showDataArr[currentIndex].img" alt="" class="h-[50%] w-[58%]">
+      </div>
+      <div class="mt-[20px] h-[80px] w-[100%] flex-row justify-around text-center text-[42px] text-[#3A3A3AE2] line-height-[80px]">
+        <div
+          v-for="(item, index) in showDataArr[currentIndex].choices"
+          :key="index"
+          class="text-area relative h-[80px] w-[80px] rounded-[20px] text-center line-height-[80px] shadow-lg"
+          @click="choiceClick(item, index)"
+        >
+          <span v-if="showText">{{ item }}</span>
+          <WuIsCorrect
+            v-if="selectiveIndex === index && showDataArr[currentIndex].checked"
+            :correct="showDataArr[currentIndex].isRight"
+          />
+        </div>
+      </div>
+    </div>
+  </section>
+</template>
+
+<style scoped lang="less">
+.app-container {
+  background-image: url('/static/image/game/bg-pic-naming.png');
+  background-size: 100% 100%;
+  background-repeat: no-repeat;
+  background-position: center center;
+
+  :deep(.van-nav-bar) {
+    .van-nav-bar__title {
+      color: #fff;
+    }
+
+    .van-icon {
+      color: #fff;
+    }
+  }
+
+  .center-div {
+    background-image: url('/static/image/game/bg-center.png');
+    background-size: 100% 100%;
+    background-repeat: no-repeat;
+    background-position: center center;
+  }
+
+  .text-area {
+    background-image: url('/static/image/game/bg-text.png');
+    background-size: 100% 100%;
+    background-repeat: no-repeat;
+    background-position: center center;
+  }
+}
+</style>

+ 23 - 0
src/views/cognitiveTasks/PicturePuzzle/Topics.json

@@ -0,0 +1,23 @@
+[
+  [
+    { "level": 1, "title": "", "choices": ["3/1/1", "3/1/2", "3/1/3", "3/1/4", "3/1/5", "3/1/6", "3/1/7", "3/1/8", "3/1/9"], "tipImg": "3/1/puzzle" },
+    { "level": 1, "title": "", "choices": ["3/2/1", "3/2/2", "3/2/3", "3/2/4", "3/2/5", "3/2/6", "3/2/7", "3/2/8", "3/2/9"], "tipImg": "3/2/puzzle" },
+    { "level": 1, "title": "", "choices": ["3/3/1", "3/3/2", "3/3/3", "3/3/4", "3/3/5", "3/3/6", "3/3/7", "3/3/8", "3/3/9"], "tipImg": "3/3/puzzle" },
+    { "level": 1, "title": "", "choices": ["3/4/1", "3/4/2", "3/4/3", "3/4/4", "3/4/5", "3/4/6", "3/4/7", "3/4/8", "3/4/9"], "tipImg": "3/4/puzzle" },
+    { "level": 1, "title": "", "choices": ["3/5/1", "3/5/2", "3/5/3", "3/5/4", "3/5/5", "3/5/6", "3/5/7", "3/5/8", "3/5/9"], "tipImg": "3/5/puzzle" }
+  ],
+  [
+    { "level": 2, "title": "", "choices": ["4/1/1", "4/1/2", "4/1/3", "4/1/4", "4/1/5", "4/1/6", "4/1/7", "4/1/8", "4/1/9", "4/1/10", "4/1/11", "4/1/12", "4/1/13", "4/1/14", "4/1/15", "4/1/16"], "tipImg": "4/1/puzzle" },
+    { "level": 2, "title": "", "choices": ["4/2/1", "4/2/2", "4/2/3", "4/2/4", "4/2/5", "4/2/6", "4/2/7", "4/2/8", "4/2/9", "4/2/10", "4/2/11", "4/2/12", "4/2/13", "4/2/14", "4/2/15", "4/2/16"], "tipImg": "4/2/puzzle" },
+    { "level": 2, "title": "", "choices": ["4/3/1", "4/3/2", "4/3/3", "4/3/4", "4/3/5", "4/3/6", "4/3/7", "4/3/8", "4/3/9", "4/3/10", "4/3/11", "4/3/12", "4/3/13", "4/3/14", "4/3/15", "4/3/16"], "tipImg": "4/3/puzzle" },
+    { "level": 2, "title": "", "choices": ["4/4/1", "4/4/2", "4/4/3", "4/4/4", "4/4/5", "4/4/6", "4/4/7", "4/4/8", "4/4/9", "4/4/10", "4/4/11", "4/4/12", "4/4/13", "4/4/14", "4/4/15", "4/4/16"], "tipImg": "4/4/puzzle" },
+    { "level": 2, "title": "", "choices": ["4/5/1", "4/5/2", "4/5/3", "4/5/4", "4/5/5", "4/5/6", "4/5/7", "4/5/8", "4/5/9", "4/5/10", "4/5/11", "4/5/12", "4/5/13", "4/5/14", "4/5/15", "4/5/16"], "tipImg": "4/5/puzzle" }
+  ],
+  [
+    { "level": 3, "title": "", "choices": ["5/1/1", "5/1/2", "5/1/3", "5/1/4", "5/1/5", "5/1/6", "5/1/7", "5/1/8", "5/1/9", "5/1/10", "5/1/11", "5/1/12", "5/1/13", "5/1/14", "5/1/15", "5/1/16", "5/1/17", "5/1/18", "5/1/19", "5/1/20", "5/1/21", "5/1/22", "5/1/23", "5/1/24", "5/1/25"], "tipImg": "5/1/puzzle" },
+    { "level": 3, "title": "", "choices": ["5/2/1", "5/2/2", "5/2/3", "5/2/4", "5/2/5", "5/2/6", "5/2/7", "5/2/8", "5/2/9", "5/2/10", "5/2/11", "5/2/12", "5/2/13", "5/2/14", "5/2/15", "5/2/16", "5/2/17", "5/2/18", "5/2/19", "5/2/20", "5/2/21", "5/2/22", "5/2/23", "5/2/24", "5/2/25"], "tipImg": "5/2/puzzle" },
+    { "level": 3, "title": "", "choices": ["5/3/1", "5/3/2", "5/3/3", "5/3/4", "5/3/5", "5/3/6", "5/3/7", "5/3/8", "5/3/9", "5/3/10", "5/3/11", "5/3/12", "5/3/13", "5/3/14", "5/3/15", "5/3/16", "5/3/17", "5/3/18", "5/3/19", "5/3/20", "5/3/21", "5/3/22", "5/3/23", "5/3/24", "5/3/25"], "tipImg": "5/3/puzzle" },
+    { "level": 3, "title": "", "choices": ["5/4/1", "5/4/2", "5/4/3", "5/4/4", "5/4/5", "5/4/6", "5/4/7", "5/4/8", "5/4/9", "5/4/10", "5/4/11", "5/4/12", "5/4/13", "5/4/14", "5/4/15", "5/4/16", "5/4/17", "5/4/18", "5/4/19", "5/4/20", "5/4/21", "5/4/22", "5/4/23", "5/4/24", "5/4/25"], "tipImg": "5/4/puzzle" },
+    { "level": 3, "title": "", "choices": ["5/5/1", "5/5/2", "5/5/3", "5/5/4", "5/5/5", "5/5/6", "5/5/7", "5/5/8", "5/5/9", "5/5/10", "5/5/11", "5/5/12", "5/5/13", "5/5/14", "5/5/15", "5/5/16", "5/5/17", "5/5/18", "5/5/19", "5/5/20", "5/5/21", "5/5/22", "5/5/23", "5/5/24", "5/5/25"], "tipImg": "5/5/puzzle" }
+  ]
+]

+ 94 - 0
src/views/cognitiveTasks/PicturePuzzle/components/PPCountDown/index.vue

@@ -0,0 +1,94 @@
+<script setup lang="ts">
+/*
+ * 组件名: PPCountDown
+ * 组件用途: 分钟倒计时
+ * 创建日期: 2024/8/9
+ * 编写者: JutarryWu
+ */
+const props = defineProps({
+  time: {
+    // 倒计时时间,单位秒
+    type: Number,
+    default: 0,
+  },
+  visible: {
+    // 倒计时时间,单位秒
+    type: Boolean,
+    default: false,
+  },
+})
+const emits = defineEmits(['countDownEnd', 'update:visible'])
+let TimerInterVal: NodeJS.Timeout // 倒计时
+// const startTime = Date.now() // 当前时刻
+const showTimer = ref('') // 时间显示文字
+
+const timeDown = ref(0)
+
+const visibleVal = computed({
+  get() {
+    return props.visible
+  },
+  set(value) {
+    emits('update:visible', value)
+  },
+})
+
+watch(
+  () => visibleVal.value,
+  (newVal) => {
+    if (newVal) {
+      exec()
+    }
+    else {
+      clearInterval(TimerInterVal)
+    }
+  },
+)
+
+function countDoneEnd() {
+  clearInterval(TimerInterVal)
+  emits('countDownEnd')
+}
+
+function countDownBegin() {
+  const hours = Math.floor(timeDown.value / 3600)
+  const minutes = Math.floor((timeDown.value % 3600) / 60)
+  const seconds = Math.floor((timeDown.value % 3600) % 60)
+
+  showTimer.value = `${hours.toString().padStart(2, '0')}时${minutes.toString().padStart(2, '0')}分${seconds.toString().padStart(2, '0')}秒`
+  timeDown.value--
+
+  if (timeDown.value === -1) {
+    countDoneEnd()
+  }
+}
+
+async function exec() {
+  // 每隔一秒调用 countDown 函数
+  timeDown.value = props.time
+  TimerInterVal = setInterval(countDownBegin, 1000)
+}
+
+onMounted(() => {
+  // exec()
+})
+
+onBeforeUnmount(() => {
+  clearInterval(TimerInterVal)
+})
+
+defineExpose({
+  countDoneEnd,
+})
+</script>
+
+<template>
+  <section class="p-p-count-down-container">
+    {{ showTimer }}
+  </section>
+</template>
+
+<style scoped lang="less">
+.p-p-count-down-container {
+}
+</style>

+ 327 - 0
src/views/cognitiveTasks/PicturePuzzle/components/PicturePuzzleChild/index.vue

@@ -0,0 +1,327 @@
+<script setup lang="ts">
+/*
+ * 组件名: SeriesInferenceChild
+ * 组件用途: 图片拼图子组件
+ * 创建日期: 2024/6/18
+ * 编写者: JutarryWu
+ */
+
+import { shuffle } from 'lodash-es'
+import { deepClone } from 'vant/es/utils/deep-clone'
+import PPCountDown from '../PPCountDown/index.vue'
+import type { IGameBaseChildData } from '@/typing'
+
+const $emits = defineEmits(['beginLoop', 'endLoop'])
+const showData = ref<IGameBaseChildData>({
+  title: '',
+  correctValue: '',
+})
+const imgList = ref<string[]>([])
+const VoiceImpRef = ref()
+const PPCountDownRef = ref()
+const activeIndex_left = ref(0)
+const activeIndex_right = ref(-1)
+const activeIndex_svg = ref(-1)
+
+const containerWidth_left = 332
+const containerHeight_left = 325
+const containerWidth_right = 302
+const svgNum_left = ref([3, 4, 5])
+const svgNum_right = ref([
+  [5, 2, 9],
+  [7, 4, 16],
+  [8, 5, 25],
+])
+const countDownVisible = ref(false)
+const countDownVals = ref([2 * 60, 4 * 60, 6 * 60])
+const levelStrArr = ref(['一', '二', '三'])
+const showResult = ref(false)
+
+const coordinateSet = ref<Record<string, string>[]>([]) // 坐标集合
+const boxWidth_left = computed(() => Math.floor(containerWidth_left / svgNum_left.value[showData.value.level! - 1]))
+const boxHeight_left = computed(() => Math.floor(containerHeight_left / svgNum_left.value[showData.value.level! - 1]))
+const boxWidth_right = computed(() =>
+  Math.floor(containerWidth_right / svgNum_right.value[showData.value.level! - 1][0]),
+)
+const boxes_left = ref<Record<string, string>[]>([])
+const boxes_right = ref<Record<string, string>[]>([])
+
+function imgUrl(val: string) {
+  return `/static/image/game/puzzle/${val!}.png`
+}
+
+const showSubmitFlag = computed(() => {
+  const result = coordinateSet.value?.filter(item => item.positionDes.indexOf('left'))
+  return result?.length === 0
+})
+
+function coordinateSetFn(n: number, boxWidth: number, boxHeight: number) {
+  const result: Record<string, string>[] = []
+  for (let i = 0; i < n * n; i++) {
+    const row = Math.floor(i / n)
+    const col = i % n
+    const x = col * boxWidth
+    const y = row * boxHeight
+    result.push({
+      left: `${x / 6.67}vh`,
+      top: `${y / 3.75}vw`,
+      width: `${(boxWidth - 4) / 6.67}vh`,
+      height: `${(boxHeight - 4) / 3.75}vw`,
+      positionDes: `left_${i + 1}`,
+    })
+  }
+  return result
+}
+
+function coordinateSetFn_right(n: number, m: number, boxWidth: number) {
+  const result: Record<string, string>[] = []
+  for (let i = 0; i < m; i++) {
+    for (let j = 0; j < n; j++) {
+      if (result.length < imgList.value?.length) {
+        result.push({
+          left: `${(j * boxWidth + 334) / 6.67}vh`,
+          top: `${(i * boxWidth + 160) / 3.75}vw`,
+          width: `${(boxWidth - 4) / 6.67}vh`,
+          height: `${(boxWidth - 4) / 3.75}vw`,
+          positionDes: `right_${j * (i + 1) + 1}`,
+        })
+      }
+    }
+  }
+  return result
+}
+
+/**
+ * @description: 交换数组元素
+ * @param array
+ * @param index1
+ * @param index2
+ */
+function swapElements(array: any[], index1: number, index2: number) {
+  ;[array[index1], array[index2]] = [array[index2], array[index1]]
+}
+
+let tempIndex = -1
+/**
+ * @description: 切换图片
+ * @param flag 0表示点击左侧div,1表示点击右侧div 2表示点击svg
+ * @param index
+ */
+function changeImg(flag: number, index: number) {
+  VoiceImpRef.value.videoPlay()
+  if (flag === 2) {
+    if (tempIndex === -1) {
+      tempIndex = index
+      activeIndex_svg.value = index
+    }
+    else {
+      if (coordinateSet.value[tempIndex].positionDes.includes('right') && coordinateSet.value[index].positionDes.includes('right')) {
+        tempIndex = index
+        activeIndex_svg.value = index
+        return
+      }
+      else {
+        swapElements(coordinateSet.value, tempIndex, index)
+        tempIndex = -1
+        activeIndex_svg.value = -1
+      }
+    }
+  }
+  if (flag === 0 || flag === 1) {
+    if (tempIndex !== -1) {
+      if (flag === 0) {
+        coordinateSet.value[tempIndex] = boxes_left.value[index]
+      }
+      else {
+        coordinateSet.value[tempIndex] = boxes_right.value[index]
+      }
+      tempIndex = -1
+    }
+  }
+}
+
+function resetIndex() {
+  activeIndex_left.value = -1
+  activeIndex_right.value = -1
+  activeIndex_svg.value = -1
+}
+
+function resetHtml() {
+  resetIndex()
+  showResult.value = false
+  countDownVisible.value = true
+}
+
+function setData(data: IGameBaseChildData) {
+  resetHtml()
+  showData.value = {
+    ...{
+      title: '',
+      correctValue: '',
+      choices: [],
+      level: 1,
+      score: 0, // 得分
+      correct: false, // 是否正确
+      accuracy: 0, // 正确率
+      choiceClickFlag: false, // 是否点击过选项 false表示未点击,true表示点击过
+      tipImg: '',
+    },
+    ...data,
+  }
+  if (showData.value.choices && showData.value.choices.length > 0) {
+    imgList.value = shuffle(showData.value.choices)
+    showData.value.correctValue = imgList.value
+      ?.map((item) => {
+        return item.split('/')[2]
+      })
+      .join('') as string
+    boxes_left.value = coordinateSetFn(
+      svgNum_left.value[showData.value.level! - 1],
+      boxWidth_left.value,
+      boxHeight_left.value,
+    )
+    boxes_right.value = coordinateSetFn_right(
+      svgNum_right.value[showData.value.level! - 1][0],
+      svgNum_right.value[showData.value.level! - 1][1],
+      boxWidth_right.value,
+    )
+    coordinateSet.value = deepClone(boxes_right.value)
+
+    countDownVisible.value = true
+  }
+}
+
+async function exec() {
+  $emits('beginLoop')
+}
+
+function submitPre() {
+  PPCountDownRef.value.countDoneEnd()
+}
+
+function submitFn() {
+  if (!showData.value.choiceClickFlag) {
+    const tempResult = coordinateSet.value?.map(item => item.positionDes.split('_')[1]).join('')
+    showResult.value = true
+    showData.value.correct = tempResult === showData.value.correctValue
+    VoiceImpRef.value.videoPlay(showData.value.correct ? 'right' : 'error')
+    showData.value.choiceClickFlag = true
+    countDownVisible.value = false
+
+    setTimeout(() => {
+      $emits('endLoop', showData.value)
+    }, 800)
+  }
+}
+
+onMounted(() => {
+  exec()
+})
+
+// 暴露变量
+defineExpose({
+  setData,
+})
+</script>
+
+<template>
+  <section class="picture-puzzle-child-container relative h-full w-full" @click="resetIndex">
+    <van-image
+      :src="imgUrl(showData.tipImg)"
+      class="absolute left-[51vh] top-0 h-[20%] w-[20%] overflow-hidden rounded-[8px]"
+    />
+    <div
+      class="tips-center absolute right-[10px] top-[-46px] z999 h-[80px] w-[24%] overflow-hidden text-center text-[24px] font-600 line-height-[104px]"
+    >
+      当前难度{{ levelStrArr[showData.level! - 1] }}
+    </div>
+    <div
+      class="absolute left-[75.5%] top-[55px] z29 h-[38px] w-[21vh] overflow-hidden text-center text-[19px] font-600 line-height-[38px]"
+    >
+      <PPCountDown
+        ref="PPCountDownRef"
+        v-model:visible="countDownVisible"
+        :time="countDownVals[showData.level! - 1]"
+        @count-down-end="submitFn"
+      />
+    </div>
+    <div
+      v-for="(box, index) in boxes_left"
+      :key="index"
+      :style="box"
+      :class="{ '!border-[2px]!border-[#134FA4]': activeIndex_left === index }"
+      class="absolute m-[4px] cursor-pointer overflow-hidden border border-white rounded-[8px] bg-[#f3f3bdCE] shadow-lg hover:shadow-2xl"
+      @click.stop="changeImg(0, index)"
+    />
+    <template v-if="svgNum_right && svgNum_right[showData.level! - 1]">
+      <div
+        v-for="(svg, index) in svgNum_right[showData.level! - 1][2]"
+        :key="index"
+        :style="boxes_right[index]"
+        :class="{ '!border-[2px]!border-[#134FA4]': activeIndex_right === index }"
+        class="absolute m-[4px] cursor-pointer overflow-hidden border border-white rounded-[8px] bg-[#f3f3bdCE] text-[#FFFFFF00] shadow-lg hover:shadow-2xl"
+        @click.stop="changeImg(1, index)"
+      >
+        {{ svg }}
+      </div>
+    </template>
+    <van-image
+      v-for="(item, index) in imgList"
+      :key="index"
+      :src="imgUrl(item)"
+      :style="coordinateSet[index]"
+      :class="{ activeImg: activeIndex_svg === index }"
+      class="z49 m-[4px] cursor-pointer overflow-hidden border-white rounded-[10px] !absolute hover:border-[4px] hover:shadow-2xl"
+      @click.stop="changeImg(2, index)"
+    />
+
+    <transition name="el-fade-in-linear">
+      <div
+        v-if="showSubmitFlag"
+        class="submit-flag absolute right-[36px] top-[105px] h-[38px] w-[120px] cursor-pointer text-[18px]"
+        @click="submitPre"
+      >
+        <WuIsCorrect v-if="showResult" :correct="showData.correct" />
+      </div>
+    </transition>
+    <VoiceImp ref="VoiceImpRef" />
+  </section>
+</template>
+
+<style scoped lang="less">
+.picture-puzzle-child-container {
+  :deep(.van-image) {
+    &.activeImg {
+      scale: 1.06;
+      border: 4px solid white;
+      box-shadow: 0 0 10px #134fa4;
+    }
+
+    transition:
+      scale 0.1s linear,
+      border 0.1s linear,
+      left 0.35s ease-in-out,
+      top 0.35s ease-in-out,
+      width 0.35s linear,
+      height 0.35s linear;
+  }
+
+  .tips-center {
+    background-image: url('/static/image/game/icon-board.png');
+    background-size: 100% 100%;
+    background-position: center center;
+    letter-spacing: 5px;
+  }
+
+  .submit-flag {
+    background-image: url('/static/image/game/icon-submit.png');
+    background-size: 100% 100%;
+    background-position: center center;
+    transition: all 0.16s linear;
+
+    &:hover {
+      scale: 1.1;
+    }
+  }
+}
+</style>

+ 178 - 0
src/views/cognitiveTasks/PicturePuzzle/index.vue

@@ -0,0 +1,178 @@
+<script setup lang="ts">
+/*
+ * 组件名: PicturePuzzle
+ * 组件用途: 图片拼图
+ * 创建日期: 2024/6/13
+ * 编写者: JutarryWu
+ */
+import { shuffle } from 'lodash-es'
+import { showSuccessToast } from 'vant'
+import Topics from './Topics.json'
+import PicturePuzzleChild from './components/PicturePuzzleChild/index.vue'
+import type { IGameBaseData } from '@/typing'
+import { formatSeconds, isAndroidOrIos } from '@/utils'
+import GameAPI, { type GameResultVO, type GameVO } from '@/api/game'
+
+const router = useRouter()
+const subjectInfo = ref<GameVO>({})
+const PicturePuzzleChildRef = ref()
+const TopicsArr = Topics.map((item) => {
+  item = shuffle(item)
+  return item
+})
+let gameStartTime = null // 游戏开始时间
+const currentLevel = ref(0) // 当前难度
+const tempIndex = ref(0) // 当前难度下的次序
+const baseData: IGameBaseData = reactive({
+  totalScore: 0,
+  beginTime: 0,
+  endTime: 0,
+  reactionTime: 0,
+  maxLevel: 1,
+  dataList: [],
+})
+const scoreList = [10, 30, 60, 100]
+
+function endGame() {
+  // 计算游戏得分
+  const totalScore = baseData.totalScore
+  const gameTime = formatSeconds(Math.ceil(performance.now() - gameStartTime))
+
+  const data: GameResultVO = {
+    finish: '1',
+    gameId: subjectInfo.value.id,
+    gameName: subjectInfo.value.name,
+    paramList: [
+      {
+        code: 'score',
+        name: '得分',
+        value: totalScore,
+      },
+      {
+        code: 'gameTime',
+        name: '游戏时长',
+        value: gameTime,
+      },
+      {
+        code: 'maxLevel',
+        name: '最终难度',
+        value: currentLevel.value + 1,
+      },
+    ],
+    userId: sessionStorage.getItem('userId'),
+  }
+  GameAPI.add(data).then(() => {
+    showSuccessToast({
+      className: 'rotate-toast',
+      message: '本次训练已结束',
+    })
+    setTimeout(() => {
+      router.go(-1)
+    }, 1300)
+  })
+}
+
+function nextLevel() {
+  currentLevel.value++
+  if (currentLevel.value < 5) {
+    tempIndex.value = 0
+    setChildData()
+  }
+  else {
+    endGame()
+  }
+}
+
+function nextIndex() {
+  if (tempIndex.value === 0) {
+    tempIndex.value++
+    setChildData()
+  }
+  else {
+    if (
+      baseData.dataList[baseData.dataList.length - 1].correct === baseData.dataList[baseData.dataList.length - 2].correct
+    ) {
+      if (baseData.dataList[baseData.dataList.length - 1].correct) {
+        nextLevel()
+      }
+      else {
+        endGame()
+      }
+    }
+    else {
+      if (tempIndex.value === 4) {
+        endGame()
+      }
+      else {
+        tempIndex.value++
+        setChildData()
+      }
+    }
+  }
+}
+
+function setChildData() {
+  PicturePuzzleChildRef.value.setData(TopicsArr[currentLevel.value][tempIndex.value])
+}
+
+function endLoop(data: any) {
+  if (data.correct) {
+    baseData.totalScore += scoreList[currentLevel.value]
+    baseData.maxLevel = baseData.maxLevel && baseData.maxLevel > currentLevel.value ? baseData.maxLevel : currentLevel.value
+  }
+  baseData.dataList.push(data)
+  nextIndex()
+}
+
+async function exec() {
+  const temp = sessionStorage.getItem('subjectInfo')
+  if (temp) {
+    subjectInfo.value = JSON.parse(temp)
+  }
+  baseData.beginTime = Date.now()
+  gameStartTime = performance.now()
+}
+
+const windowHeight = ref(0)
+const userAgent = isAndroidOrIos()
+onMounted(() => {
+  windowHeight.value = window.innerHeight
+  exec()
+})
+</script>
+
+<template>
+  <section class="app-container" :style="{ width: userAgent === 'ios' ? '100vh' : `${windowHeight}px` }">
+    <van-nav-bar class="self-nav-bar" :title="subjectInfo.name" left-arrow @click-left="router.go(-1)" />
+    <div class="h-full w-full flex flex-col items-center text-white">
+      <PicturePuzzleChild ref="PicturePuzzleChildRef" @begin-loop="setChildData" @end-loop="endLoop" />
+    </div>
+  </section>
+</template>
+
+<style scoped lang="less">
+.app-container {
+  transform: rotate(90deg);
+  transform-origin: right top;
+  width: 100vh; /* 适应横屏的高度 */
+  overflow-x: hidden;
+  position: absolute;
+  top: 100%;
+  right: 0;
+
+  background-image: url('/static/image/game/bg-puzzle.png');
+  background-size: 100% 100%;
+  background-repeat: no-repeat;
+  background-position: center center;
+
+  :deep(.van-nav-bar) {
+    .van-nav-bar__title {
+      color: #fff;
+    }
+
+    .van-icon {
+      color: #fff;
+    }
+  }
+}
+</style>

+ 36 - 0
src/views/cognitiveTasks/cocos/index.vue

@@ -0,0 +1,36 @@
+<script setup lang="ts">
+/*
+ * 组件名: cocos
+ * 组件用途: cocos外框
+ * 创建日期: 2024/8/21
+ * 编写者: JutarryWu
+ */
+const userId = sessionStorage.getItem('userId')
+const src = `https://byly.jue-ming.com/xuanZeXunLian/?userId=${userId}`
+async function exec() {
+}
+
+onMounted(() => {
+  exec()
+})
+</script>
+
+<template>
+  <section class="app-container">
+    <div v-if="false" class="absolute right-[12px] top-[12px] z-[999] cursor-pointer">
+      <van-icon name="arrow-up" class="text-[24px] text-[#fff]" />
+    </div>
+    <iframe
+      :src="src"
+      width="100%"
+      height="100%"
+      frameborder="0"
+      scrolling="no"
+    />
+  </section>
+</template>
+
+<style scoped lang="less">
+.app-container {
+}
+</style>

+ 88 - 0
src/views/cognitiveTasks/main/index.vue

@@ -0,0 +1,88 @@
+<script setup lang="ts">
+/*
+ * 组件名: index
+ * 组件用途: XXX
+ * 创建日期: 2024/8/16
+ * 编写者: JutarryWu
+ */
+import GameAPI, { type GameVO } from '@/api/game'
+
+const router = useRouter()
+const route = useRoute()
+
+const taskId = ref('')
+const userId = ref('')
+
+const subjectInfo = ref<GameVO>({})
+const urlList = [
+  '/cognitiveTasks/spatialOrientationAbility',
+  '/cognitiveTasks/ContinueAddition',
+  '/cognitiveTasks/cocos',
+  '/cognitiveTasks/BreadthTraining',
+  '/cognitiveTasks/PicturePuzzle',
+  '/cognitiveTasks/PictureNaming',
+]
+
+function startTest() {
+  // console.log(urlList[Number(taskId.value) - 1])
+  router.push({
+    path: urlList[Number(taskId.value) - 1],
+  })
+}
+
+async function exec() {
+  taskId.value = route.query.taskId as string
+  userId.value = route.query.userId as string
+  GameAPI.findById(taskId.value).then((res) => {
+    subjectInfo.value = res.data
+    sessionStorage.setItem('userId', userId.value)
+    sessionStorage.setItem('subjectInfo', JSON.stringify(res.data))
+  })
+}
+
+onMounted(() => {
+  exec()
+})
+</script>
+
+<template>
+  <section class="app-container">
+    <van-nav-bar class="self-nav-bar" title="认知任务" />
+    <div class="ml-[12px] mt-[18px] min-h-[180px] w-[350px] rounded-[16px] bg-white p-x-[15px] p-y-[15px]">
+      <div class="flex-row">
+        <!-- <van-image :src="`/src/assets/images/task/${subjectInfo.id}.png`" class="h-[100px] w-[100px] overflow-hidden rounded-[8px]" /> -->
+        <img class="h-[100px] w-[100px] overflow-hidden rounded-[8px]" :src="`/static/image/game/logo/${subjectInfo.id}.png`" alt="">
+        <div class="ml-[24px] text-[20px] text-black font-600">
+          {{ subjectInfo.name }}
+        </div>
+      </div>
+      <div class="mb-[10px] mt-[20px] text-[18px] text-[#00AABD]">
+        操作提示:
+      </div>
+      <div class="text-[15px] text-[#999999] line-height-[20px]">
+        {{ subjectInfo.intro }}
+      </div>
+    </div>
+    <van-button type="primary" color="#00AABD" class="bottom-block-btn" @click="startTest">
+      开始测试
+    </van-button>
+  </section>
+</template>
+
+<style scoped lang="less">
+.app-container {
+  background-color: #f0f6f6 !important;
+  background-image: url('@/assets/images/bg-main.png');
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: center; /* 可选,让图片居中对齐 */
+
+  :deep(.van-nav-bar) {
+    background-color: transparent;
+
+    &.van-hairline--bottom:after {
+      border: none;
+    }
+  }
+}
+</style>

+ 403 - 0
src/views/cognitiveTasks/spatialOrientationAbility/index.vue

@@ -0,0 +1,403 @@
+<script setup lang="ts">
+/*
+ * 组件名: spatialOrientationAbility
+ * 组件用途: 空间定向训练
+ * 创建日期: 2024/8/16
+ * 编写者: JutarryWu
+ */
+import { shuffle } from 'lodash-es'
+import { showSuccessToast } from 'vant'
+import { formatSeconds, generateRandomNumbers } from '@/utils'
+import Url_forest from '@/assets/images/task/spatialOrientationAbility/forest.png'
+import Url_hospital from '@/assets/images/task/spatialOrientationAbility/hospital.png'
+import Url_lakes from '@/assets/images/task/spatialOrientationAbility/lakes.png'
+import Url_massif from '@/assets/images/task/spatialOrientationAbility/massif.png'
+import Url_cat from '@/assets/images/task/spatialOrientationAbility/cat.png'
+import Url_flower from '@/assets/images/task/spatialOrientationAbility/flower.png'
+import Url_car from '@/assets/images/task/spatialOrientationAbility/car.png'
+import Url_trafficLight from '@/assets/images/task/spatialOrientationAbility/trafficLight.png'
+import Url_stop from '@/assets/images/task/spatialOrientationAbility/stop.png'
+import Url_house from '@/assets/images/task/spatialOrientationAbility/house.png'
+import GameAPI, { type GameResultVO, type GameVO, type ResultLevel } from '@/api/game'
+
+const router = useRouter()
+
+interface IData {
+  level?: number
+  onceStartTime?: string
+  onceEndTime?: string
+  reactionTime?: number
+  xAxis?: string | number
+  yAxis?: string | number
+  id?: number
+  text?: string
+  name?: string
+  value?: string
+  imgUrl?: string
+  angle?: string
+}
+
+const subjectInfo = ref<GameVO>({})
+const showCountDown = ref(false) // 显示倒计时
+const showCountDownBottom = ref(false) // 显示底部倒计时
+const showSubmit = ref(false) // 显示画布
+const isSubmit = ref(false) // 是否提交进程中
+
+const levelNum = [3, 4, 5] // 正式测试:3个难度等级需要呈现的刺激目标数
+const tryCount = 1 // 正式测试:每个难度等级需要进行的试次数
+const currentIndex = ref(0) // 当前试次索引值:练习模式2次最大为1, 正式测试45次
+const itemAngle = ref(0) // 选项物品实际角度
+const roundSliderVal = ref(0) // 圆形滑杆产生的值(即:用户操作实际角度)
+const offsetAngle = ref(0) // 角度偏差值(用户绘制时产生的实际偏差值)
+let gameStartTime = null // 游戏开始时间
+
+const showDataArr = ref<IData[][]>([]) // 伪随机数组
+let arrDataDemo: IData[] = [
+  { id: 0, text: '森林', name: '森林', value: 'forest', imgUrl: Url_forest, angle: '', xAxis: '', yAxis: '' },
+  { id: 1, text: '医院', name: '医院', value: 'hospital', imgUrl: Url_hospital, angle: '', xAxis: '', yAxis: '' },
+  { id: 2, text: '湖泊', name: '湖泊', value: 'lakes', imgUrl: Url_lakes, angle: '', xAxis: '', yAxis: '' },
+  { id: 3, text: '山', name: '山', value: 'massif', imgUrl: Url_massif, angle: '', xAxis: '', yAxis: '' },
+  { id: 4, text: '猫', name: '猫', value: 'cat', imgUrl: Url_cat, angle: '', xAxis: '', yAxis: '' },
+  { id: 5, text: '花', name: '花', value: 'flower', imgUrl: Url_flower, angle: '', xAxis: '', yAxis: '' },
+  { id: 6, text: '车', name: '车', value: 'car', imgUrl: Url_car, angle: '', xAxis: '', yAxis: '' },
+  {
+    id: 7,
+    text: '红绿灯',
+    name: '红绿灯',
+    value: 'trafficLight',
+    imgUrl: Url_trafficLight,
+    angle: '',
+    xAxis: '',
+    yAxis: '',
+  },
+  { id: 8, text: '停止牌', name: '停止牌', value: 'stop', imgUrl: Url_stop, angle: '', xAxis: '', yAxis: '' },
+  { id: 9, text: '房子', name: '房子', value: 'house', imgUrl: Url_house, angle: '', xAxis: '', yAxis: '' },
+]
+const firstObj = ref<IData>()
+const centerCoordinate = ref<IData>()
+const secondObj = ref<IData>()
+const topCoordinate = ref<IData>()
+const thirdObj = ref<IData>()
+const thirdCoordinate = ref<IData>()
+const levelList = ref<ResultLevel[]>([]) // 存放结果数组
+
+/**
+ * 生成不重叠随机点
+ * @param centerX
+ * @param centerY
+ * @param radius
+ * @param existingPoints
+ */
+function generateRandomPoint(centerX: number, centerY: number, radius: number, existingPoints: Record<string, any>[]) {
+  const angle = Math.random() * 2 * Math.PI
+  const distance = Math.sqrt(Math.random()) * radius // 确保点更均匀地分布在圆内
+  const xAxis = centerX + distance * Math.cos(angle)
+  const yAxis = centerY + distance * Math.sin(angle)
+
+  // 检查是否与现有点重叠
+  for (const point of existingPoints) {
+    const dx = xAxis - point.xAxis
+    const dy = yAxis - point.yAxis
+    if (Math.sqrt(dx * dx + dy * dy) < radius / 3) {
+      // 阈值设为半径的1/4
+      return generateRandomPoint(centerX, centerY, radius, existingPoints) // 递归调用直到找到合适的点
+    }
+  }
+
+  return { xAxis, yAxis }
+}
+
+function generateNonOverlappingPoints(centerX: number, centerY: number, radius: number, num: number) {
+  const points = [{ xAxis: centerX, yAxis: centerY }] // 中心点
+  for (let i = 0; i < num; i++) {
+    const newPoint = generateRandomPoint(centerX, centerY, radius, points)
+    points.push(newPoint)
+  }
+  return points.map((item) => {
+    return {
+      xAxis: item.xAxis + 8,
+      yAxis: item.yAxis + 8,
+    }
+  })
+}
+
+function createThreeObj() {
+  const arr = generateRandomNumbers(3, showDataArr.value[currentIndex.value].length).map((item: number) => item - 1)
+
+  firstObj.value = showDataArr.value[currentIndex.value][arr[0]]
+  centerCoordinate.value = showDataArr.value[currentIndex.value][arr[0]]
+
+  secondObj.value = showDataArr.value[currentIndex.value][arr[1]]
+  topCoordinate.value = showDataArr.value[currentIndex.value][arr[1]]
+
+  thirdObj.value = showDataArr.value[currentIndex.value][arr[2]]
+  thirdCoordinate.value = showDataArr.value[currentIndex.value][arr[2]]
+
+  showCountDownBottom.value = false
+  submitQuestion()
+}
+
+function submitQuestion() {
+  // 计算选项角度
+  const topCenterX = Number(topCoordinate.value.xAxis) - Number(centerCoordinate.value.xAxis)
+  const topCenterY = Number(topCoordinate.value.yAxis) - Number(centerCoordinate.value.yAxis)
+  const thirdCenterX = Number(thirdCoordinate.value.xAxis) - Number(centerCoordinate.value.xAxis)
+  const thirdCenterY = Number(thirdCoordinate.value.yAxis) - Number(centerCoordinate.value.yAxis)
+  getAngle({ x: topCenterX, y: topCenterY }, { x: thirdCenterX, y: thirdCenterY })
+
+  // 显示画布
+  showSubmit.value = true
+}
+
+function getAngle({ x: x1, y: y1 }, { x: x2, y: y2 }) {
+  const dot = x1 * x2 + y1 * y2
+  const det = x1 * y2 - y1 * x2
+  // const angle = Math.atan2(det, dot) / Math.PI * 180;
+  const angle = Math.atan2(det, dot) * (180 / Math.PI)
+  // return (angle + 360) % 360;
+  itemAngle.value = Math.abs(angle)
+  // console.log('---------------------------------------------------------------')
+  // console.log('物品实际角度值: ', itemAngle.value)
+  // console.log('---------------------------------------------------------------')
+}
+
+// 以下生成随机位置显示图例方法
+function initImgData(level: number, num: number) {
+  // 随机生成6个物品的位置
+  const items = generateNonOverlappingPoints(160, 170, 160, num - 1)
+  arrDataDemo = shuffle(arrDataDemo)
+  const tempItems: IData[] = arrDataDemo.slice(0, num)
+  const extraInfo = {
+    level: levelNum[level],
+    onceStartTime: '',
+    onceEndTime: '',
+    reactionTime: 0,
+  }
+  items.forEach((item, index) => {
+    tempItems[index] = { ...tempItems[index], ...item, ...extraInfo }
+  })
+  return tempItems
+}
+
+function initImgDataList() {
+  for (let i = 0; i < levelNum.length; i++) {
+    let tempTryCount = tryCount
+    while (tempTryCount > 0) {
+      const tempData: IData[] = initImgData(i, levelNum[i])
+      showDataArr.value.push(tempData)
+      tempTryCount--
+    }
+  }
+  showDataArr.value = shuffle(showDataArr.value)
+}
+
+function angleDiffVal(val: number) {
+  roundSliderVal.value = val
+}
+
+function // 提交数据
+submitAngle() {
+  // showDataArr.value[currentIndex.value].onceEndTime = Date.now()
+  isSubmit.value = true
+  offsetAngle.value = Number((Math.abs(itemAngle.value - roundSliderVal.value)).toFixed(2))
+  // console.log('---------------------------------------------------------------')
+  // console.log('物品实际角度值: ', itemAngle)
+  // console.log('用户操作角度值: ', roundSliderVal)
+  // console.log('用户操作逻辑角度值: ', roundSliderVal)
+  // console.log('角度偏差值: ', offsetAngle)
+  // console.log('---------------------------------------------------------------')
+  levelList.value.push({
+    level: showDataArr.value[currentIndex.value][0].level,
+    levelParamList: [
+      {
+        code: 'itemAngle',
+        name: '物品实际角度值',
+        value: itemAngle.value.toFixed(2),
+      },
+      {
+        code: 'rotationAngle',
+        name: '用户操作逻辑角度值',
+        value: Math.abs(roundSliderVal.value).toFixed(2),
+      },
+      {
+        code: 'offsetAngle',
+        name: '角度偏差值',
+        value: offsetAngle.value,
+      },
+      {
+        code: 'CenterName',
+        name: '中心点物体',
+        value: centerCoordinate.value.name,
+      },
+      {
+        code: 'BaseName',
+        name: '顶点物体',
+        value: topCoordinate.value.name,
+      },
+      {
+        code: 'TargetName',
+        name: '目标物体',
+        value: thirdCoordinate.value.name,
+      },
+    ],
+    // onceStartTime: dayjs(new Date(showDataArr.value[currentIndex.value].onceStartTime)).format('YY-mm-dd HH:MM:SS'),
+    // onceEndTime: dayjs(new Date(showDataArr.value[currentIndex.value].onceEndTime)).format('YY-mm-dd HH:MM:SS'),
+    // reactionTime: (showDataArr.value[currentIndex.value].onceEndTime - showDataArr.value[currentIndex.value].onceStartTime).toFixed(2),
+    // randomArr: dealArrDataPoint(showDataArr.value[currentIndex.value], centerCoordinate.value), // 物品随机坐标位置
+  })
+  currentIndex.value++
+  // curPercentage.value = Number(roundToFixed((currentIndex.value / showDataArr.value.length), 2))
+  if (currentIndex.value < showDataArr.value.length) {
+    resetGame()
+  }
+  else {
+    const gameTime = formatSeconds(Math.ceil(performance.now() - gameStartTime))
+
+    const data: GameResultVO = {
+      finish: '1',
+      gameId: subjectInfo.value.id,
+      gameName: subjectInfo.value.name,
+      paramList: [
+        {
+          code: 'gameTime',
+          name: '游戏时长',
+          value: gameTime,
+        },
+      ],
+      levelList: levelList.value,
+      userId: sessionStorage.getItem('userId'),
+    }
+    GameAPI.add(data).then(() => {
+      showSuccessToast('本次训练已结束')
+      setTimeout(() => {
+        router.go(-1)
+      }, 1300)
+    })
+  }
+}
+
+/**
+ * 以centerPoint为中心坐标(0, 0), 修改其他图标的坐标值
+ * @param arrData
+ * @param centerPoint
+ */
+// function dealArrDataPoint(arrData: IData[], centerPoint: IData) {
+//   const xAxisOffset = new Decimal(centerPoint.xAxis)
+//   const yAxisOffset = new Decimal(centerPoint.yAxis)
+//   return arrData.map((item) => {
+//     const xAxis = new Decimal(item.xAxis)
+//     const yAxis = new Decimal(item.yAxis)
+//     item.xAxis = xAxis.minus(xAxisOffset).toNumber()
+//     item.yAxis = yAxis.minus(yAxisOffset).toNumber()
+//     return item
+//   })
+// }
+
+function resetGame() {
+  showCountDown.value = false
+  isSubmit.value = false
+  offsetAngle.value = 0
+  itemAngle.value = 0
+  roundSliderVal.value = 0
+  firstObj.value = null
+  secondObj.value = null
+  thirdObj.value = null
+  centerCoordinate.value = null
+  // 调用任务方法开始任务
+  // 随机点位已生成,展示在画布上
+  showSubmit.value = false
+  showCountDownBottom.value = true
+}
+
+function exec() {
+  showCountDown.value = false
+  showCountDownBottom.value = true
+  gameStartTime = performance.now()
+}
+
+onMounted(() => {
+  const temp = sessionStorage.getItem('subjectInfo')
+  if (temp) {
+    subjectInfo.value = JSON.parse(temp)
+  }
+  showCountDown.value = true
+  initImgDataList()
+})
+</script>
+
+<template>
+  <section class="app-container">
+    <van-nav-bar class="self-nav-bar" :title="subjectInfo.name" left-arrow @click-left="router.go(-1)" />
+    <count-down v-if="showCountDown" :time="5" @end-count-down="exec" />
+    <div v-if="!showCountDown && !showSubmit" class="opt-bg-area absolute-center h-[370px] w-[350px] rounded-[8px] bg-[#FFFFFF8B] shadow-lg">
+      <div
+        v-for="(arr, index) in showDataArr[currentIndex]"
+        :key="index"
+        class="item flex-column absolute translate-[-50%] text-[15px] text-black items-center"
+        :style="{ top: `${arr.yAxis}px`, left: `${arr.xAxis}px` }"
+      >
+        <img :src="arr.imgUrl" alt="" class="h-[24px] w-[24px]">
+        <span>{{ arr.name }}</span>
+      </div>
+    </div>
+
+    <div class="absolute bottom-[12px] right-[8px] h-[118px] w-[108px]">
+      <count-down v-if="showCountDownBottom" :time="10" text="" color="rgba(194, 194, 194, 0.6)" :size="74" @end-count-down="createThreeObj" />
+    </div>
+
+    <template v-if="showSubmit">
+      <div class="ml-[4%] mt-[10px] w-[92%] text-center text-[15px] text-[#3A3A3A]">
+        在刚刚呈现的图片中,请您想象当您站在
+        <span class="text-[18px] text-[#F87171] font-600">{{ firstObj.name }}</span>
+        的位置同时面向
+        <span class="text-[18px] text-[#F87171] font-600">{{ secondObj.name }}</span>
+        时,用红色线条标出
+        <span class="text-[18px] text-[#F87171] font-600">{{ thirdObj.name }}</span>
+        所在的方向。
+      </div>
+
+      <div class="absolute-center h-[370px] w-[350px] rounded-[8px] bg-[#FFFFFF8B] shadow-lg">
+        <div class="opt-bg-circle absolute-center h-[286px] w-[284px]">
+          <RoundSlider
+            :center-name="firstObj.name"
+            :top-name="secondObj.name"
+            :move-name="thirdObj.name"
+            @cur-value="angleDiffVal"
+          />
+        </div>
+      </div>
+      <van-button
+        type="primary"
+        color="#00AABD"
+        class="bottom-block-btn"
+        :disabled="isSubmit"
+        @click="submitAngle()"
+      >
+        确定
+      </van-button>
+    </template>
+  </section>
+</template>
+
+<style scoped lang="less">
+.app-container {
+  background-image: url('@/assets/images/bg-soa.png');
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: center; /* 可选,让图片居中对齐 */
+
+  .opt-bg-area {
+    background-image: url('@/assets/images/task/spatialOrientationAbility/bg_shape.png');
+    background-size: cover;
+    background-repeat: no-repeat;
+    background-position: center; /* 可选,让图片居中对齐 */
+
+    .opt-bg-circle {
+      background-image: url('@/assets/images/task/spatialOrientationAbility/bg_circle.png');
+      background-size: cover;
+      background-repeat: no-repeat;
+      background-position: center; /* 可选,让图片居中对齐 */
+    }
+  }
+}
+</style>

Some files were not shown because too many files changed in this diff