|
@@ -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>
|