|
@@ -0,0 +1,274 @@
|
|
|
+<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 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 (!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(() => {
|
|
|
+ currentIndex.value++
|
|
|
+ nextOnce()
|
|
|
+ }, 900)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function nextOnce() {
|
|
|
+ if (currentIndex.value < showDataArr.value.length) {
|
|
|
+ onceStart = performance.now()
|
|
|
+ selectiveIndex.value = -1
|
|
|
+ showImg.value = true
|
|
|
+ showText.value = true
|
|
|
+ // 隐藏文字
|
|
|
+ textTimeout.value = setTimeout(() => {
|
|
|
+ showText.value = false
|
|
|
+ }, 3000)
|
|
|
+
|
|
|
+ // 隐藏图片
|
|
|
+ imgTimeout.value = setTimeout(() => {
|
|
|
+ showImg.value = false
|
|
|
+ }, showDataArr.value[currentIndex.value].seconds)
|
|
|
+
|
|
|
+ // 如果用户未点击,自动清除定时器,进入下一个试次
|
|
|
+ onceTimer.value = setTimeout(() => {
|
|
|
+ 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>
|