HarmonyOS NEXT星河版之在线考试功能实战

一、目标

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、基础搭建

2.1 定义数据

// 题目
export interface ExamItem {
  id: number
  title: string
  options: OptionItem[]
}
// 答案
export interface OptionItem {
  letter: string
  content: string
}

2.2 mock数据

export const mockExamDataList: ExamItem[] = [
  {
    id: 1,
    title: 'Android系统的构建系统叫什么名字?',
    options: [
      { letter: 'A', content: 'Gradle' },
      { letter: 'B', content: 'Maven' },
      { letter: 'C', content: 'Ant' },
      { letter: 'D', content: 'Make' },
    ],
  },
  {
    id: 2,
    title: '以下哪个组件不是Android架构的一部分?',
    options: [
      { letter: 'A', content: 'Activity(活动)' },
      { letter: 'B', content: 'Service(服务)' },
      { letter: 'C', content: 'Content Provider(内容提供者)' },
      { letter: 'D', content: 'Fragment(片段)' },
    ],
  },
  {
    id: 3,
    title: 'Android中的RecyclerView控件有什么用途?',
    options: [
      { letter: 'A', content: '显示一个可滚动的元素列表' },
      { letter: 'B', content: '在不同活动之间导航' },
      { letter: 'C', content: '播放视频内容' },
      { letter: 'D', content: '绘制自定义的形状和路径' },
    ],
  },
];

2.3 主页面布局

2.3.1 布局规划

将主页面布局抽取,封装对应如下:
在这里插入图片描述

2.3.2 标题栏
@Builder
getTitleBar() {
  Stack({ alignContent: Alignment.Start }) {
    Image($r('app.media.ic_left_arrow'))
      .width(24)
    Text('在线考试')
      .width('100%')
      .textAlign(TextAlign.Center)
  }
  .padding({ left: 12, right: 12 })
  .width('100%')
  .height(52)
  .backgroundColor(Color.White)
  .borderWidth({
    bottom: 0.5
  })
  .borderColor('#e5e5e5')
}
2.3.3 进度条
@Builder
getProgressView() {
  Row() {
    Progress({ value: this.currentIndex + 1, total: this.questionList.length })
      .padding({ left: 12, right: 12 })
    Text() {
      Span(`${this.currentIndex + 1}`)
        .fontColor(Color.Black)
      Span('/' + this.questionList.length)
        .fontColor(Color.Gray)
    }
    .layoutWeight(1)
  }
  .width('100%')
}
2.3.4 答题模块
Column() {
  Column({ space: 5 }) {
    Text(this.currentQuestion.title)
      .margin({ bottom: 6, top: 12 })
    ForEach(this.currentQuestion.options, (item: OptionItem) => {
      Row() {
        Text(item.letter + '. ')
        Text(item.content)
      }
      .width('100%')
      .height(40)
      .padding({ left: 12 })
      .backgroundColor('#F9F9F9')
    })
  }
  .alignItems(HorizontalAlign.Start)
  .width('100%')
  .padding({ left: 15, right: 15 })

}.layoutWeight(1)
2.3.5 底部按钮
@Builder
getBottomView() {
  Row() {
    Row({ space: 3 }) {
      Image($r('app.media.ic_arrow_left'))
        .width(15)
        .fillColor(this.getPreEnable() ? Color.Black : '#BABABA')
      Text('上一题')
        .fontColor(this.getPreEnable() ? Color.Black : '#BABABA')
    }
    .onClick(() => {
      this.onPreClick()
    })

    Row({ space: 3 }) {
      Text('下一题')
        .fontColor(this.getNextEnable() ? Color.Black : '#BABABA')
      Image($r('app.media.ic_arrow_right'))
        .width(15)
        .fillColor(this.getNextEnable() ? Color.Black : '#BABABA')
    }
    .onClick(() => {
      this.onNextClick()
    })
  }
  .width('100%')
  .justifyContent(FlexAlign.SpaceBetween)
  .backgroundColor(Color.White)
  .height(50)
  .padding({
    left: 12, right: 12
  })
  .border({
    color: '#e5e5e5',
    width: { top: 1 }
  })
}

2.4 主页面逻辑

2.4.1 加载数据及定义变量
// 题目列表
  @State questionList: ExamItem[] = []
  // 当前显示第N题
  @State currentIndex: number = 0
  // 当前题目
  @State currentQuestion: ExamItem = {} as ExamItem

  aboutToAppear(): void {
    this.loadData()
  }

  async loadData() {
    // 模拟网络获取数据
    this.questionList = await new Promise<ExamItem[]>((resolve, reject) => {
      setTimeout(() => {
        resolve(mockExamDataList)
      }, 500)
    });
    // 默认展示第一条
    this.currentQuestion = this.questionList[this.currentIndex]
  }
2.4.2 上一题、下一题
onPreClick() {
  if (this.getPreEnable()) {
    this.currentIndex--
    this.currentQuestion = this.questionList[this.currentIndex]
  }
}

onNextClick() {
  if (this.getNextEnable()) {
    this.currentIndex++
    this.currentQuestion = this.questionList[this.currentIndex]
  }
}

getPreEnable() {
  return this.currentIndex > 0
}

getNextEnable() {
  return this.currentIndex < this.questionList.length - 1
}

三、选项点击及高亮

3.1 声明对象及变量

export interface UserAnswer {
  questionId: number // 问题ID
  userAnswer: string // 用户选择项
}

在主页面中,定义变量,存储用户做题数据,如下:

// 存储题目和用户答案
@State userAnswerList: Record<number, UserAnswer> = {}

3.2 给选项注册点击事件

ForEach(this.currentQuestion.options, (item: OptionItem) => {
   Row() {
     Text(item.letter + '. ')
     Text(item.content)
   }
   .width('100%')
   .height(40)
   .padding({ left: 12 })
   .onClick(() => {
     this.onUserAnswerClick(item)
   })
 })

处理点击事件:

onUserAnswerClick(option: OptionItem) {
   this.userAnswerList[this.currentQuestion.id] = {
     questionId: this.currentQuestion.id,
     userAnswer: option.letter
   }
 }

3.3 处理背景和文本颜色

Row() {
  Text(item.letter + '. ')
    .fontColor(this.getOptionColor(item, 'font'))
  Text(item.content)
    .fontColor(this.getOptionColor(item, 'font'))
}
...
.backgroundColor(this.getOptionColor(item))
/**
   * 获取选择项背景或文本颜色
   * @param option 当前选择项
   * @param type 类型
   * @returns
   */
getOptionColor(option: OptionItem, type: 'back' | 'font' = 'back') {
  if (this.currentQuestion) {
    const userAnswer = this.userAnswerList[this.currentQuestion.id]
    if (userAnswer?.userAnswer === option.letter) {
      return type === 'back' ? Color.Pink : Color.White
    }
  }
  return type === 'back' ? '#e5e5e5' : Color.Black
}

四、小结

  • UI布局
  • 题目切换处理
  • 做题标记及高亮展示
  • 13
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值