index.vue 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. <script setup lang="ts">
  2. /*
  3. * 组件名: PictureNaming
  4. * 组件用途: 图片命名
  5. * 创建日期: 2024/8/20
  6. * 编写者: JutarryWu
  7. */
  8. import { shuffle } from 'lodash-es'
  9. import { showSuccessToast } from 'vant'
  10. import Topics from './Topics.json'
  11. import GameAPI, { type GameResultVO, type GameVO } from '@/api/game'
  12. interface IData {
  13. choices?: string[]
  14. img?: string
  15. seconds?: number
  16. correct?: string
  17. isRight?: boolean
  18. checked?: boolean
  19. score?: number
  20. reactionTime?: number
  21. }
  22. const router = useRouter()
  23. const VoiceImpRef = ref()
  24. const subjectInfo = ref<GameVO>({})
  25. const showCountDown = ref(true) // 显示倒计时
  26. const showImg = ref(true) // 显示图片
  27. const showText = ref(false) // 显示文字
  28. const musicFlag = ref(true) // 页面音乐开关
  29. const userCanClickFlag = ref(false) // 用户是否可以点击开关
  30. const showDataArr = ref<IData[]>([])
  31. const seconds = [3000, 2000, 1500]
  32. const currentIndex = ref(0) // 当前索引
  33. const selectiveIndex = ref(-1) // 用户选择的索引
  34. const textTimeout = ref<NodeJS.Timeout | null>(null)
  35. const imgTimeout = ref<NodeJS.Timeout | null>(null)
  36. const onceTimer = ref<NodeJS.Timeout | null>(null)
  37. let onceStart = 0 // 试次开始时间
  38. function initData() {
  39. for (let i = 0; i < 3; i++) {
  40. const tempArr = []
  41. for (let j = 0; j < 15; j++) {
  42. tempArr.push({
  43. choices: shuffle(Topics[i * 15 + j]),
  44. img: `/static/image/game/name/${i * 15 + j}.png`,
  45. seconds: seconds[i], // 图片显示时长
  46. correct: Topics[i * 15 + j][0], // 正确答案
  47. isRight: false, // 是否正确
  48. score: 0, // 正确: 1, 错误:0
  49. reactionTime: 4000, // 反应时间(ms)
  50. checked: false, // 是否选过
  51. })
  52. }
  53. showDataArr.value = [...showDataArr.value, ...shuffle(tempArr)]
  54. }
  55. }
  56. function musicClick(flag: number) {
  57. musicFlag.value = !musicFlag.value
  58. if (musicFlag.value) {
  59. VoiceImpRef.value.videoPlay('click')
  60. VoiceImpRef.value.videoPlay('bg', flag)
  61. }
  62. else {
  63. VoiceImpRef.value.videoPause('bg')
  64. }
  65. }
  66. function choiceClick(item: string, index: number) {
  67. if (!userCanClickFlag.value) {
  68. return
  69. }
  70. if (!showDataArr.value[currentIndex.value].checked) {
  71. showDataArr.value[currentIndex.value].checked = true
  72. selectiveIndex.value = index
  73. if (item === showDataArr.value[currentIndex.value].correct) {
  74. showDataArr.value[currentIndex.value].isRight = true
  75. showDataArr.value[currentIndex.value].score = 1
  76. showDataArr.value[currentIndex.value].reactionTime = Number((performance.now() - onceStart).toFixed(3))
  77. if (musicFlag.value) {
  78. VoiceImpRef.value.videoPlay('right')
  79. }
  80. }
  81. else {
  82. if (musicFlag.value) {
  83. VoiceImpRef.value.videoPlay('error')
  84. }
  85. }
  86. clearTimeout(textTimeout.value)
  87. clearTimeout(imgTimeout.value)
  88. clearTimeout(onceTimer.value)
  89. setTimeout(() => {
  90. userCanClickFlag.value = false
  91. showText.value = false
  92. showImg.value = false
  93. currentIndex.value++
  94. nextOnce()
  95. }, 900)
  96. }
  97. }
  98. function nextOnce() {
  99. if (currentIndex.value < showDataArr.value.length) {
  100. onceStart = performance.now()
  101. selectiveIndex.value = -1
  102. showImg.value = true
  103. // 隐藏图片
  104. imgTimeout.value = setTimeout(() => {
  105. showImg.value = false
  106. showText.value = true
  107. userCanClickFlag.value = true
  108. // 隐藏文字
  109. textTimeout.value = setTimeout(() => {
  110. showText.value = false
  111. }, 3000)
  112. }, showDataArr.value[currentIndex.value].seconds)
  113. // 如果用户未点击,自动清除定时器,进入下一个试次
  114. onceTimer.value = setTimeout(() => {
  115. userCanClickFlag.value = false
  116. showText.value = false
  117. showImg.value = false
  118. clearTimeout(textTimeout.value)
  119. clearTimeout(imgTimeout.value)
  120. clearTimeout(onceTimer.value)
  121. currentIndex.value++
  122. nextOnce()
  123. }, 7000)
  124. }
  125. else {
  126. submit()
  127. }
  128. }
  129. function submit() {
  130. let correctNum = 0
  131. let wrongNum = 0
  132. let accuracy = 0
  133. let totalScore = 0
  134. let averageReactionTimeForCorrect = 0
  135. showDataArr.value.forEach((item) => {
  136. if (item.isRight) {
  137. correctNum++
  138. totalScore++
  139. averageReactionTimeForCorrect += item.reactionTime
  140. }
  141. })
  142. wrongNum = 45 - correctNum
  143. accuracy = Number((correctNum / 45).toFixed(4))
  144. averageReactionTimeForCorrect = Number((averageReactionTimeForCorrect / correctNum).toFixed(3))
  145. const data: GameResultVO = {
  146. finish: '1',
  147. gameId: subjectInfo.value.id,
  148. gameName: subjectInfo.value.name,
  149. paramList: [
  150. {
  151. code: 'correctNum',
  152. name: '正确数',
  153. value: correctNum,
  154. },
  155. {
  156. code: 'wrongNum',
  157. name: '错误数',
  158. value: wrongNum,
  159. },
  160. {
  161. code: 'accuracy',
  162. name: '正确率',
  163. value: accuracy,
  164. },
  165. {
  166. code: 'totalScore',
  167. name: '得分',
  168. value: totalScore,
  169. },
  170. {
  171. code: 'averageReactionTimeForCorrect',
  172. name: '正确点击的反应时长均值',
  173. value: `${averageReactionTimeForCorrect}ms`,
  174. },
  175. ],
  176. userId: sessionStorage.getItem('userId'),
  177. }
  178. GameAPI.add(data).then(() => {
  179. showSuccessToast({
  180. className: 'rotate-toast',
  181. message: '本次训练已结束',
  182. })
  183. setTimeout(() => {
  184. router.go(-1)
  185. }, 1300)
  186. })
  187. }
  188. function exec() {
  189. showCountDown.value = false
  190. nextOnce()
  191. }
  192. onMounted(() => {
  193. const temp = sessionStorage.getItem('subjectInfo')
  194. if (temp) {
  195. subjectInfo.value = JSON.parse(temp)
  196. }
  197. if (musicFlag.value) {
  198. VoiceImpRef.value.videoPlay('bg')
  199. }
  200. initData()
  201. })
  202. onBeforeUnmount(() => {
  203. clearInterval(onceTimer.value)
  204. })
  205. </script>
  206. <template>
  207. <section class="app-container">
  208. <van-nav-bar class="self-nav-bar" :title="subjectInfo.name" left-arrow @click-left="router.go(-1)">
  209. <template #right>
  210. <img v-if="musicFlag" src="/static/image/game/video/play.png" alt="" class="h-[34px] w-[34px]" @click="musicClick(0)">
  211. <img v-else src="/static/image/game/video/stop.png" alt="" class="h-[34px] w-[34px]" @click="musicClick(1)">
  212. </template>
  213. </van-nav-bar>
  214. <count-down v-if="showCountDown" :time="5" color="#fff" @end-count-down="exec" />
  215. <VoiceImp ref="VoiceImpRef" />
  216. <div v-if="!showCountDown && currentIndex < showDataArr.length" class="absolute-center flex-column h-[78%] w-[100%]">
  217. <div class="center-div flex-center ml-[4%] h-[390px] w-[92%]">
  218. <img v-if="showImg" :src="showDataArr[currentIndex].img" alt="" class="h-[50%] w-[58%]">
  219. </div>
  220. <div class="mt-[20px] h-[80px] w-[100%] flex-row justify-around text-center text-[42px] text-[#3A3A3AE2] line-height-[80px]">
  221. <div
  222. v-for="(item, index) in showDataArr[currentIndex].choices"
  223. :key="index"
  224. class="text-area relative h-[80px] w-[80px] rounded-[20px] text-center line-height-[80px] shadow-lg"
  225. @click="choiceClick(item, index)"
  226. >
  227. <span v-if="showText">{{ item }}</span>
  228. <WuIsCorrect
  229. v-if="selectiveIndex === index && showDataArr[currentIndex].checked"
  230. :correct="showDataArr[currentIndex].isRight"
  231. />
  232. </div>
  233. </div>
  234. </div>
  235. </section>
  236. </template>
  237. <style scoped lang="less">
  238. .app-container {
  239. background-image: url('/static/image/game/bg-pic-naming.png');
  240. background-size: 100% 100%;
  241. background-repeat: no-repeat;
  242. background-position: center center;
  243. :deep(.van-nav-bar) {
  244. .van-nav-bar__title {
  245. color: #fff;
  246. }
  247. .van-icon {
  248. color: #fff;
  249. }
  250. }
  251. .center-div {
  252. background-image: url('/static/image/game/bg-center.png');
  253. background-size: 100% 100%;
  254. background-repeat: no-repeat;
  255. background-position: center center;
  256. }
  257. .text-area {
  258. background-image: url('/static/image/game/bg-text.png');
  259. background-size: 100% 100%;
  260. background-repeat: no-repeat;
  261. background-position: center center;
  262. }
  263. }
  264. </style>