Forráskód Böngészése

feat(广度训练): 迁移广度训练游戏

chenlianwei 6 hónapja
szülő
commit
33e3ea9dbf

+ 9 - 0
build/vite/index.ts

@@ -1,5 +1,6 @@
 import { dirname, resolve } from 'node:path'
 import { fileURLToPath } from 'node:url'
+import process from 'node:process'
 import { unheadVueComposablesImports } from '@unhead/vue'
 import legacy from '@vitejs/plugin-legacy'
 import vue from '@vitejs/plugin-vue'
@@ -14,6 +15,7 @@ import { VitePWA } from 'vite-plugin-pwa'
 import Sitemap from 'vite-plugin-sitemap'
 import VueDevTools from 'vite-plugin-vue-devtools'
 import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
 import { createViteVConsole } from './vconsole'
 
 export function createVitePlugins() {
@@ -115,5 +117,12 @@ export function createVitePlugins() {
         ],
       },
     }),
+
+    createSvgIconsPlugin({
+      // Specify the icon folder to be cached
+      iconDirs: [resolve(process.cwd(), 'src/assets/icons')],
+      // Specify symbolId format
+      symbolId: 'icon-[dir]-[name]',
+    }),
   ]
 }

+ 2 - 0
package.json

@@ -34,6 +34,7 @@
     "vant": "^4.9.2",
     "vconsole": "^3.15.1",
     "vue": "^3.4.33",
+    "vue-countup-v3": "^1.4.2",
     "vue-i18n": "^9.13.1",
     "vue-router": "^4.4.0"
   },
@@ -70,6 +71,7 @@
     "vite-plugin-mock-dev-server": "^1.5.1",
     "vite-plugin-pwa": "^0.20.0",
     "vite-plugin-sitemap": "^0.6.2",
+    "vite-plugin-svg-icons": "^2.0.1",
     "vite-plugin-vconsole": "^2.1.1",
     "vite-plugin-vue-devtools": "^7.3.6",
     "vitest": "^2.0.3",

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 460 - 3
pnpm-lock.yaml


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

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1717481423786" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="985" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M442.88 1010.176C294.912 826.88 120.832 688.128 0 644.608l286.72-172.032 138.24 268.8s225.28-545.792 581.12-727.552c-7.68 130.048-43.008 242.688 17.92 381.44-156.16 35.328-476.672 424.96-581.12 614.912z" p-id="986"></path></svg>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
src/assets/icons/pentagram.svg


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

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1717481430223" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1128" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M916.7 230.5L743 34.6S591.9 266 444.6 501.4C328.6 361.6 229 244.9 229 244.9L107.3 423.7c99.8 59.2 191.4 121.1 273.1 181-116.7 189.3-215.7 359-212.2 384.7 0 0 101.5-141.4 281.9-332.3C669.2 826 800.6 967 800.6 967c0.2-25.7-141.7-205.1-284.1-378.6 110.9-112.5 245.6-237.2 400.2-357.9z" p-id="1129"></path></svg>

BIN
src/assets/images/task/breadthTraining/correct.png


BIN
src/assets/images/task/breadthTraining/star.png


BIN
src/assets/images/task/breadthTraining/wrong.png


+ 2 - 0
src/components.d.ts

@@ -14,6 +14,7 @@ declare module 'vue' {
     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.vue')['default']
     TabBar: typeof import('./components/TabBar.vue')['default']
     VanButton: typeof import('vant/es')['Button']
     VanCell: typeof import('vant/es')['Cell']
@@ -24,6 +25,7 @@ declare module 'vue' {
     VanNavBar: typeof import('vant/es')['NavBar']
     VanPicker: typeof import('vant/es')['Picker']
     VanPopup: typeof import('vant/es')['Popup']
+    VanStepper: typeof import('vant/es')['Stepper']
     VanSwitch: typeof import('vant/es')['Switch']
     VanTabbar: typeof import('vant/es')['Tabbar']
     VanTabbarItem: typeof import('vant/es')['TabbarItem']

+ 36 - 0
src/components/SvgIcon.vue

@@ -0,0 +1,36 @@
+<script setup lang="ts">
+const props = defineProps({
+  prefix: {
+    type: String,
+    default: 'icon',
+  },
+  name: {
+    type: String,
+    required: true,
+  },
+  color: {
+    type: String,
+    default: '#333',
+  },
+  width: {
+    type: Number,
+    default: 20,
+  },
+  height: {
+    type: Number,
+    default: 20,
+  },
+})
+
+const symbolId = computed(() => `#${props.prefix}-${props.name}`)
+</script>
+
+<template>
+  <svg aria-hidden="true" :style="`width:${width}px;height:${height}px`">
+    <use :href="symbolId" :fill="color" />
+  </svg>
+</template>
+
+<style scoped lang="less">
+
+</style>

+ 1 - 0
src/main.ts

@@ -6,6 +6,7 @@ import pinia from '@/stores'
 import 'virtual:uno.css'
 import '@/styles/app.less'
 import { i18n } from '@/utils/i18n'
+import 'virtual:svg-icons-register'
 
 // Vant 桌面端适配
 import '@vant/touch-emulator'

+ 140 - 0
src/pages/cognitiveTasks/BreadthTraining/BTRandomPentagram.vue

@@ -0,0 +1,140 @@
+<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">
+    <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 {
+  box-shadow: var(--el-box-shadow-light);
+  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>

+ 209 - 4
src/pages/cognitiveTasks/BreadthTraining/index.vue

@@ -7,28 +7,233 @@
  */
 // import { shuffle } from 'lodash-es'
 
-const subjectInfo = ref({
-  name: '广度训练',
+import { shuffle } from 'lodash-es'
+import CountUp from 'vue-countup-v3'
+import BTRandomPentagram from './BTRandomPentagram.vue'
+
+interface IMainData {
+  totalScore: number
+  dataList: any[]
+}
+
+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) // 选择的索引
+
+/**
+ * 生成基础数据
+ */
+function generateBaseData() {
+  // 生成选项组
+  const selectDataList: number[][] = [
+    // [3, 4, 5, 6, 7], [3, 4, 5, 6, 7], ...,
+    // [4, 5, 6, 7, 8], [4, 5, 6, 7, 8], ...,
+    ...Array.from({ length: 10 }).fill([3, 4, 5, 6, 7]),
+    ...Array.from({ length: 10 }).fill([4, 5, 6, 7, 8]),
+    ...Array.from({ length: 10 }).fill([5, 6, 7, 8, 9]),
+    ...Array.from({ length: 10 }).fill([6, 7, 8, 9, 10]),
+    ...Array.from({ length: 10 }).fill([7, 8, 9, 10, 11]),
+    ...Array.from({ length: 10 }).fill([8, 9, 10, 11, 12]),
+    ...Array.from({ length: 10 }).fill([9, 10, 11, 12, 13]),
+    ...Array.from({ length: 10 }).fill([10, 11, 12, 13, 14]),
+  ]
+  selectArray = selectDataList.map((item) => {
+    return shuffle(item)
+  })
+
+  // 生成主体数据
+  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, 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('游戏结束')
+  }
+}
+
+/**
+ * 选择点击事件
+ *   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
 }
 
 onMounted(() => {
   showCountDown.value = true
+  generateBaseData()
+  mainData.dataList[currentIndex.value].beginTime = Date.now()
 })
 </script>
 
 <template>
   <section class="app-container">
-    <van-nav-bar :title="subjectInfo.name" />
+    <!--    <van-nav-bar :title="subjectInfo.name" /> -->
     <count-down v-if="showCountDown" :time="5" @end-count-down="exec" />
+
+    <div v-else class="breadth-training-container h-full w-full flex flex-col items-center gap-y-[15px]">
+      <!--      <el-progress -->
+      <!--        :show-text="false" -->
+      <!--        :stroke-width="4" -->
+      <!--        :percentage="percentage" -->
+      <!--        color="#FFC400" -->
+      <!--        stroke-linecap="square" -->
+      <!--        class="w-full" -->
+      <!--      /> -->
+
+      <BTRandomPentagram
+        ref="BTRandomPentagramRef"
+        :count="mainData.dataList[currentIndex]?.count"
+        :min-distance="20"
+        :change-show="changeShow"
+        class="mt-[40px]"
+        @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="flex flex-row gap-x-[20px] flex-justify-between">
+        <div
+          v-for="(item, index) in selectArray[mainData.dataList[currentIndex]?.index]"
+          :key="`${currentIndex}${index}`"
+          class="pos-relative min-w-[50px] cursor-pointer rounded-[8px] bg-[#ffffff] p-y-3 text-center text-[24px]"
+          @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 {
+.breadth-training-container {
+  background-color: #0a0c23;
+  //background-image: url('/bg-breadthTraining.png');
+  background-size: 100% 100%;
+  background-position: center center;
+
+  :deep(.el-progress-bar__outer) {
+    background-color: rgba(0, 0, 0, 0.1);
+  }
 }
 </style>

+ 1 - 0
src/typed-router.d.ts

@@ -22,6 +22,7 @@ declare module 'vue-router/auto-routes' {
     '404': RouteRecordInfo<'404', '/:all(.*)', { all: ParamValue<true> }, { all: ParamValue<false> }>,
     'charts': RouteRecordInfo<'charts', '/charts', Record<never, never>, Record<never, never>>,
     '/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/main/': RouteRecordInfo<'/cognitiveTasks/main/', '/cognitiveTasks/main', Record<never, never>, Record<never, never>>,
     '/cognitiveTasks/PicturePuzzle/': RouteRecordInfo<'/cognitiveTasks/PicturePuzzle/', '/cognitiveTasks/PicturePuzzle', Record<never, never>, Record<never, never>>,
     '/cognitiveTasks/PicturePuzzle/components/PicturePuzzleChild/': RouteRecordInfo<'/cognitiveTasks/PicturePuzzle/components/PicturePuzzleChild/', '/cognitiveTasks/PicturePuzzle/components/PicturePuzzleChild', Record<never, never>, Record<never, never>>,

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott