Bladeren bron

feat: 完成反应力训练页面的相关逻辑和UI

YuJing 2 weken geleden
bovenliggende
commit
e767ad3db5

+ 2 - 0
commons/basic/src/main/ets/models/YTDiaLogModel.ets

@@ -20,6 +20,8 @@ export enum DiaLogPageEnum{
 
   // 历史今日
   HistoryToday,
+  // 气泡设置
+  BubbleSetting,
 }
 
 export interface DiaLogParam<T = ESObject>{

+ 15 - 3
commons/basic/src/main/ets/utils/arkts/utils/YTRouter.ets

@@ -2,7 +2,7 @@ import { IBestToast } from '@ibestservices/ibest-ui'
 
 
 import { DelPhotoParam } from '../../../models'
-import { DiaLogParam } from '../../../models/YTDiaLogModel'
+import { DiaLogPageEnum, DiaLogParam, YTDiaLogModel } from '../../../models/YTDiaLogModel'
 
 class YTRouter extends NavPathStack {
   private static declare instance: YTRouter
@@ -82,8 +82,20 @@ class YTRouter extends NavPathStack {
   /**
    * DiaLog 相关
    */
-  router2DiaLog(param: DiaLogParam) {
-    yTRouter.pushPathByName('YTNaviDiaLog', param)
+  router2DiaLog(param: DiaLogParam, back?: Callback<PopInfo>) {
+    yTRouter.pushPathByName('YTNaviDiaLog', param, back)
+  }
+
+  router2DoubleConfirm(title: string, color: ResourceColor, back?: Callback<PopInfo>){
+    let param: DiaLogParam = {
+      pageEnum: DiaLogPageEnum.Confirm,
+      param: {
+        text: title,
+        color: color
+      },
+      align: YTDiaLogModel.Center
+    }
+    this.router2DiaLog(param, back)
   }
 }
 

+ 72 - 1
features/feature/src/main/ets/components/YtComp/YTDiaLogBuild.ets

@@ -2,9 +2,11 @@ import { BasicType, YTAvoid } from 'basic'
 import { DiaLogPageEnum, DiaLogParam } from 'basic/src/main/ets/models/YTDiaLogModel'
 import { HistoryApis } from '../../Apis/HistoryApis'
 import { HistoryEventDetail } from '../../model/Index'
+import { BubbleStorage } from '../../model/Storage'
 import { NumberKeyBoard, NumberKeyBoardStyle } from './NumberKeyboard'
 import { YtDatePicker } from './YtDatePicker'
 import { _YtHeader } from './_YtHeader'
+import { AppStorageV2 } from '@kit.ArkUI'
 
 
 @Builder
@@ -15,6 +17,7 @@ export function getBuilder(param: DiaLogParam, onBack: (ans?: ESObject) => void)
   else if (param.pageEnum == DiaLogPageEnum.Confirm) DoubleConfirm(onBack, param.param)
   else if (param.pageEnum == DiaLogPageEnum.TextInput) InputComp({onBack: onBack, param: param.param})
   else if (param.pageEnum == DiaLogPageEnum.HistoryToday) HistoryToday({onBack: onBack, param: param.param})
+  else if (param.pageEnum == DiaLogPageEnum.BubbleSetting) BubbleSetting({onBack: onBack, param: param.param})
 }
 
 // 底部菜单
@@ -87,7 +90,7 @@ function DoubleConfirm(onBack: (ans?:ESObject) => void, param?: BasicType) {
         .backgroundColor('#F5F5F7')
         .padding({ left: 36, top: 9, right: 36, bottom: 9})
         .onClick(() => {
-          onBack()
+          onBack('false')
         })
 
       Text('确定')
@@ -359,6 +362,74 @@ struct HistoryToday{
 }
 
 
+@ComponentV2
+struct BubbleSetting{
+  @Event onBack: (ans?:ESObject) => void
+  @Param @Require param: BasicType
+
+  @Local bubbleConnect: BubbleStorage = AppStorageV2.connect(BubbleStorage, () => new BubbleStorage())!
+
+  build() {
+    Column({space: 9}){
+      Row(){
+        Image($r('app.media.icon_close'))
+          .width(24)
+          .aspectRatio(1)
+          .onClick(() => { this.onBack() })
+      }
+      .width("100%")
+      .justifyContent(FlexAlign.End)
+      .padding({right: 14, top: 10, bottom: 10})
+
+      Column({space: 25}){
+        ForEach(['气泡出现频率', '气泡消失时间'], (item: string, index) => {
+          Row({space: 15}){
+            Text(`${item}:`)
+              .fontSize(16)
+              .fontWeight(500)
+              .fontColor('#FF333333')
+
+            Row({space: 12}){
+              TextInput({text: `${index == 0 ? this.bubbleConnect.bubbleFrequency : this.bubbleConnect.bubbleDisappearTime}`})
+                .width(45)
+                .height(30)
+                .padding(0)
+                .maxLength(3)
+                .borderRadius(5)
+                .type(InputType.Number)
+                .textAlign(TextAlign.Center)
+                .border({width: 1, color: '#FFFFA001'})
+                .onChange((text: string) => {
+                  if(text){
+                    let num = parseInt(text) || 1
+                    if(index == 0){
+                      this.bubbleConnect.bubbleFrequency = num
+                    }else{
+                      this.bubbleConnect.bubbleDisappearTime = num
+                    }
+                  }
+                })
+
+              Text('s')
+                .fontSize(16)
+                .fontWeight(500)
+                .fontColor('#FF333333')
+            }
+          }
+        })
+      }
+      .width("100%")
+      .alignItems(HorizontalAlign.Center)
+    }
+    .width(294)
+    .height(163)
+    .borderRadius(20)
+    .backgroundColor(Color.White)
+    .justifyContent(FlexAlign.Start)
+  }
+}
+
+
 
 
 

+ 135 - 0
features/feature/src/main/ets/components/reagencyComp.ets

@@ -0,0 +1,135 @@
+import { Location } from "../model/AnimationModel"
+import image from "@ohos.multimedia.image"
+import { BubbleStorage } from "../model/Storage"
+import { AppStorageV2 } from "@kit.ArkUI"
+
+@ComponentV2
+export struct reagencyComp {
+  // 训练开始与否
+  @Param trainStarted: number = 0
+
+  bubbleConnect: BubbleStorage = AppStorageV2.connect(BubbleStorage, () => new BubbleStorage())!
+
+  // 点击了气泡
+  @Event bubbleClick: (time: number)=>void
+
+  // 气泡的数组
+  @Local bubbles: Array<Location> = []
+  @Local h: number = 0
+  @Local w: number = 0
+
+  timeC: number = -1
+
+  // 训练开始
+  @Monitor('trainStarted') startTrain() {
+    if(this.trainStarted == 1) {
+      // 这里进行气泡的重新赋值
+      this.bubbles.forEach(item => {
+        let time = new Date().getTime() - item.id!
+        let second = Math.max(this.bubbleConnect.bubbleDisappearTime * 1000 - item.id!, 1)
+        item.id = time
+
+        if(item.timer != -1) clearTimeout(item.timer)
+
+        item.timer = setTimeout(() => {
+          this.deleteBubble(item.id!)
+        }, second)
+      })
+
+
+      this.timeC = setInterval(() => {
+        let location = this.generateBubbleLocation()
+        this.getUIContext().animateTo({ duration: 300 }, () => {
+          this.bubbles.push(location)
+        })
+      }, this.bubbleConnect.bubbleFrequency * 1000)
+    } else if (this.trainStarted == 0) {
+      clearInterval(this.timeC)
+      // 暂停不只是要暂停定时器,还要暂停气泡的消失
+      // 1. 将time设置为 当前花费的时间
+      // 2. 在再次开始时, 将 time 设置为 new Date - time
+      // 3. 设置的消失时间也要变化
+      this.bubbles.forEach(item => {
+        clearTimeout(item.timer)
+        item.timer = -1
+        let time = new Date().getTime() - item.id!
+        item.id = time
+      })
+    } else {
+      // 重置
+      if(this.timeC != -1) clearInterval(this.timeC)
+
+      this.bubbles.forEach(item => {
+        if(item.timer != -1)
+          clearTimeout(item.timer)
+      })
+
+      this.bubbles = []
+    }
+  }
+
+  // 生成气泡位置
+  generateBubbleLocation(): Location {
+    // 计算气泡的最大位置范围,避免气泡超出容器边界
+    const maxX = this.w - 50;
+    const maxY = this.h - 50;
+
+    // 生成随机坐标,确保气泡完全在可视区域内
+    const x = Math.random() * maxX;
+    const y = Math.random() * maxY;
+
+    // 创建带随机坐标的location实例
+    let location = new Location(x, y);
+
+    // 设置定时器,在指定时间后自动消失
+    location.timer = setTimeout(() => {
+      location.timer = -1
+      this.deleteBubble(location.id!);
+    }, this.bubbleConnect.bubbleDisappearTime * 1000); // 将秒转换为毫秒
+
+    location.id = new Date().getTime()
+
+    return location;
+  }
+
+  // 删除气泡
+  deleteBubble(id: number, isEvent: boolean = false) {
+    if(this.trainStarted != 1 && isEvent == true) return
+
+    let index = this.bubbles.findIndex(item => item.id == id)
+    let location = this.bubbles[index]
+    let time = new Date().getTime() - location.id!
+
+    clearTimeout(location.timer)
+
+    this.getUIContext().animateTo({ duration: 300 }, () => {
+      this.bubbles.splice(index, 1)
+    })
+
+    if(isEvent) {
+      this.bubbleClick(time)
+    }
+  }
+
+  build() {
+    Column() {
+      Repeat(this.bubbles)
+        .each(item => {
+          Image($r('app.media.icon_bubble'))
+            .width(50)
+            .aspectRatio(1)
+            .position({x: item.item.x, y: item.item.y})
+            .onClick(() => { this.deleteBubble(item.item.id!, true) })
+        })
+        .key(item => `${item.x}-${item.y}`)
+    }
+    .width("100%")
+    .height("100%")
+    .borderRadius(10)
+    .backgroundColor(Color.Black)
+    .onAreaChange((o, n) => {
+      this.w = n.width as number
+      this.h = n.height as number
+    })
+  }
+}

+ 3 - 0
features/feature/src/main/ets/model/AnimationModel.ets

@@ -2,6 +2,9 @@
 export class Location{
   @Trace x: number
   @Trace y: number
+  id?: number
+  // 计时器的唯一标识
+  timer?: number
 
   constructor(x: number, y: number){
     this.x = x

+ 7 - 0
features/feature/src/main/ets/model/Storage.ets

@@ -0,0 +1,7 @@
+@ObservedV2
+export class BubbleStorage{
+  // 出现的频率
+  @Trace bubbleFrequency: number = 1
+  // 消失的时间
+  @Trace bubbleDisappearTime: number = 1
+}

+ 59 - 3
features/feature/src/main/ets/pages/reagencyPage.ets

@@ -1,6 +1,7 @@
 import { RouterPage } from 'basic';
 import { _YtHeader } from '../components/YtComp/_YtHeader';
 import { reagencyViewModel } from '../viewModel/PageVm/reagencyViewModel';
+import { reagencyComp } from '../components/reagencyComp';
 
 @ComponentV2
 @RouterPage
@@ -16,14 +17,69 @@ struct reagencyPage {
         })
 
         Column(){
+          Row(){
+            ForEach(['命中目标', '平均反应时间', '训练时长'], (item: string, index) => {
+              Column({space: 6}){
+                Text(){
+                  if(index == 0) {
+                    Span(`${this.vm.hitCount}个`)
+                  } else if (index == 1) {
+                    Span(`${this.vm.avgTime}${this.vm.avgTime < 1000 ? 'ms' : 's'}`)
+                  } else {
+                    Span(`${this.vm.trainTime}s`)
+                  }
+                }
+                .fontSize(25)
+                .fontWeight(500)
+                .fontColor('#FFFF7B00')
 
+                Text(item)
+                  .fontSize(12)
+                  .fontWeight(400)
+                  .fontColor('#FF333333')
+              }
+            })
+          }
+          .width("100%")
+          .padding({left: 21, right: 21})
+          .justifyContent(FlexAlign.SpaceBetween)
+
+          Column(){
+            reagencyComp({
+              trainStarted: this.vm.trainStarted,
+              bubbleClick: (time: number) => { this.vm.bubbleClick(time) },
+            })
+          }
+          .padding({top: 23})
+          .width("100%")
+          .layoutWeight(1)
+
+          Row(){
+            Text()
+
+            Row(){
+              Text(this.vm.trainStarted ? '暂停训练' : '开始训练')
+            }
+            .borderRadius(8)
+            .padding({top: 16, left: 44, right: 44, bottom: 16})
+            .linearGradient({colors: [['#FFFFCC00', 0], ['#FFFF9F02', 0.7]], angle: 90})
+            .onClick(() => { this.vm.toggleTrain() })
+
+            Image($r('app.media.icon_reset'))
+              .width(33)
+              .aspectRatio(1)
+              .onClick(() => { this.vm.resetTrain() })
+          }
+          .width("100%")
+          .justifyContent(FlexAlign.SpaceBetween)
+          .padding({top: 18, bottom: 46, left: 37, right: 37})
         }
         .width("100%")
         .layoutWeight(1)
         .padding({top: 20})
-        .backgroundColor('rgba(255, 255, 255, 0.3)')
+        .padding({left: 16, right: 16, top: 25})
         .borderRadius({topLeft: 23, topRight: 23})
-        .padding({left: 16, right: 16})
+        .backgroundColor('rgba(255, 255, 255, 0.3)')
       }
       .width('100%')
       .height('100%')
@@ -31,7 +87,7 @@ struct reagencyPage {
       .alignItems(HorizontalAlign.Start)
     }
     .hideTitleBar(true)
-    .padding({ top: this.vm.safeTop })
+    .padding({ top: this.vm.safeTop, bottom: this.vm.safeBottom })
     .linearGradient({ colors: [['#FF7FF9C3', 0], ['#FFEEECED', 0.3]]})
   }
 

+ 10 - 0
features/feature/src/main/ets/utils/RouterUtils.ets

@@ -14,6 +14,16 @@ class RouterUtils {
     }
     yTRouter.router2DiaLog(p)
   }
+
+  router2BubbleSetting(){
+    let p: DiaLogParam = {
+      pageEnum: DiaLogPageEnum.BubbleSetting,
+      align: YTDiaLogModel.Center
+    }
+    yTRouter.router2DiaLog(p)
+  }
+
+
 }
 
 export const uRouter: RouterUtils = new RouterUtils();

+ 89 - 2
features/feature/src/main/ets/viewModel/PageVm/reagencyViewModel.ets

@@ -1,16 +1,103 @@
-import { YTAvoid } from "basic"
+import { YTAvoid, yTRouter } from "basic"
+import { BubbleStorage } from "../../model/Storage"
+import { uRouter } from "../../utils/RouterUtils"
+import { AppStorageV2 } from "@kit.ArkUI"
 
 @ObservedV2
 export class reagencyViewModel{
   @Trace safeTop: number = 0
+  @Trace safeBottom: number = 0
+
+  // 命中目标数量
+  @Trace hitCount: number = 0
+  // 平均反应时间
+  @Trace avgTime: number = 0
+  // 训练时长
+  @Trace trainTime: number = 0
+  // 训练的运行状态 -1 重置 0 未开始 1 开始
+  @Trace trainStarted: number = 0
+
+  // 训练开始时间
+  trainStartTime: number = 0
+  // 命中的毫秒值数组
+  hitTimes: number[] = []
+  // 计时器
+  timer: number = 0
+
+  lastTime: number = 0
 
 
   // 打开设置页面
-  openSetting() {}
+  openSetting() {
+    // 节流
+    let now = new Date().getTime()
+    if(now - this.lastTime < 800) return
+    this.lastTime = now
+
+    if(this.trainStarted == 1) {
+      this.toggleTrain()
+    }
+
+    uRouter.router2BubbleSetting()
+  }
+
+  // 重置训练
+  resetTrain() {
+    // 节流
+    let now = new Date().getTime()
+    if(now - this.lastTime < 800) return
+    this.lastTime = now
+
+    if(this.trainStarted == 1) {
+      this.toggleTrain()
+    }
+    yTRouter.router2DoubleConfirm('重置将清空训练数据', '#96F6CC', (b) => {
+      let ans = b.result
+      if(ans == 'true'){
+        this.trainStarted = -1
 
+        clearInterval(this.timer)
+        this.hitCount = 0
+        this.avgTime = 0
+        this.trainTime = 0
+        this.hitTimes = []
+
+        setTimeout(() => {
+          this.trainStarted = 0
+        }, 10)
+      }
+    })
+  }
+
+
+  // 开始、暂停训练
+  toggleTrain() {
+    this.trainStarted = this.trainStarted === 1 ? 0 : 1
+    if(this.trainStarted == 1) {
+      this.timer = setInterval(() => {
+        this.trainTime += 1
+      }, 1000)
+    } else if(this.trainStarted == 0) {
+      clearInterval(this.timer)
+    }
+  }
+
+  /**
+   * 泡泡点击
+   * @param time 反应时间
+   */
+  bubbleClick(time: number){
+    this.hitTimes.push(time)
+    this.hitCount++
+
+    let sum = this.hitTimes.reduce((accumulator, currentValue) => accumulator + currentValue, 0)
+    sum = sum / this.hitTimes.length
+    this.avgTime =  Math.floor(sum)
+  }
 
   constructor() {
     this.safeTop = AppStorage.get(YTAvoid.SAFE_TOP_KEY) as number
+    this.safeBottom = AppStorage.get(YTAvoid.SAFE_BOTTOM_KEY) as number
   }
 
 

BIN
features/feature/src/main/resources/base/media/icon_bubble.png


BIN
features/feature/src/main/resources/base/media/icon_reset.png


+ 1 - 1
products/entry/src/main/ets/pages/Index.ets

@@ -50,7 +50,7 @@ struct Index {
     Stack() {
       Navigation(yTRouter) {
         Column() {
-          Tabs({ controller: this.tabsController }) {
+          Tabs({ controller: this.tabsController, index: 3 }) {
             ForEach(this.contentList, (_: BasicType<undefined>, index) => {
               TabContent() {
                 if (index == 0) {