我的模块


打卡
打卡逻辑
目标:实现打卡,已登录打卡后跳转打卡记录页面,未登录跳登录页
实现步骤:
- 封装一个打卡工具函数,打卡成功后需要更新 User 打卡次数
- 首页和我的页面调用,准备一个打卡页面
落地代码:
1)工具函数 utils/index.ets
ts
import { promptAction, router } from '@kit.ArkUI'
import { auth } from './Auth'
import { http } from './Http'
interface ClockRes {
clockinNumbers: number
}
export const requestClockIn = async () => {
const user = auth.getUser()
if (user.token) {
if (user.clockinNumbers === 0) {
const res = await http.request<ClockRes>({ url: 'clockin', method: 'post' })
user.clockinNumbers = res.clockinNumbers
auth.setUser(user)
promptAction.showToast({ message: '打卡成功' })
}
router.pushUrl({ url: 'pages/ClockInPage' })
} else {
router.pushUrl({ url: 'pages/LoginPage' })
}
}
2)调用打卡
HomePage.ets
ts
@StorageProp(UserStoreKey) user: User = {} as User
// ...
HcClockIn({ clockInCount: this.user.clockinNumbers })
.onClick(() => {
requestClockIn()
})
MinePage.ets
ts
HcClockIn({ clockInCount: this.user.clockinNumbers })
.onClick(() => {
requestClockIn()
})
ClockInPage.ets
ts
@Entry
@Component
struct ClockInPage {
build() {
Column(){
Text('打卡')
}
.height('100%')
.width('100%')
}
}
打卡页面
目标:基于 @ohmos/calendar 实现打卡页面
实现步骤:
- 准备基本页面结构
- 使安装使用 @ohmos/calendar 实现打卡页面
落地代码:
1)基本结构
ts
import { HcNavBar } from '../commons/components'
interface DayBuilderParams {
day: number
text: string
}
@Entry
@Component
struct ClockInPage {
@Builder
dayBuilder(params: DayBuilderParams) {
Column() {
Row() {
Text(params.day.toString())
.fontSize(40)
.fontWeight(FontWeight.Bold)
Text('天')
.fontSize(10)
.fontColor($r('app.color.common_gray_01'))
.margin({ bottom: 8, left: 10 })
}
.alignItems(VerticalAlign.Bottom)
Text(params.text)
.fontSize(10)
.fontColor($r('app.color.common_gray_01'))
}.margin({ right: 36 })
}
build() {
Column({ space: 16 }) {
HcNavBar({ title: '每日打卡', showRightIcon: false })
Row() {
this.dayBuilder({ day: 100, text: '累计打卡' })
this.dayBuilder({ day: 10, text: '连续打卡' })
}
.padding({ top: 10, bottom: 25, left: 16, right: 16 })
.width('100%')
.justifyContent(FlexAlign.Start)
Row() {
Row()
.width('100%')
.height(350)
.borderRadius(8)
.border({ width: 0.5, color: '#ededed' })
.shadow({ color: '#ededed', radius: 16 })
.backgroundColor($r('app.color.white'))
}
.padding({ left: 16, right: 16 })
}
.width('100%')
.height('100%')
.backgroundImage($r('app.media.clocked_bg'))
.backgroundImageSize({ width: '100%' })
}
}
2)使用日历组件
bash
ohpm install @ohmos/calendar
ts
import { HmCalendar } from '@ohmos/calendar'
ts
HmCalendar({
selectedDays: [
{ date: '2024-08-11' },
]
})
.borderRadius(8)
.border({ width: 0.5, color: '#ededed' })
.shadow({ color: '#ededed', radius: 16 })
.backgroundColor($r('app.color.white'))
切换月份
目标:实现打卡页数据展示,实现月份切换功能
实现步骤:
- 查看接口文档,定义查询参数类型和响应数据类型
- 组件初始化,调用获取数据方法获取数据,渲染页面
- 使用切换月份事件,改变的时候重新获取数据,选中当前月份打卡天数
落地代码:
1)参数类型,数据类型 models/index.ets
ts
export interface ClockInItem {
id: string,
createdAt: string
}
export interface ClockInfo {
flag: boolean
clockinNumbers: number
totalClockinNumber: number
clockins: ClockInItem[]
}
export interface ClockInfoParams {
year: string,
month: string
}
2)初始化获取数据渲染页面 ClockInPage.ets
ts
@State clockInfo: ClockInfo = {
flag: false,
clockinNumbers: 0,
totalClockinNumber: 0,
clockins: []
}
@State selectedDays: HmCalendarSelectedDay[] = []
aboutToAppear(): void {
const current = new Date()
this.getClockInfo({ year: current.getFullYear().toString(), month: (current.getMonth() + 1).toString() })
}
async getClockInfo (params: ClockInfoParams) {
const res = await http.request<ClockInfo>({ url: 'clockinInfo', params })
this.clockInfo = res
this.selectedDays = res.clockins.map(item=>({ date: item.createdAt } as HmCalendarSelectedDay))
}
ts
Row() {
this.dayBuilder({ day: this.clockInfo.totalClockinNumber, text: '累计打卡' })
this.dayBuilder({ day: this.clockInfo.clockinNumbers, text: '连续打卡' })
}
// ...
HmCalendar({
selectedDays: this.selectedDays
})
3)切换月份
ts
HmCalendar({
selectedDays: this.selectedDays,
onChangeMonth: month => {
const arr = month.split('-')
this.getClockInfo({ year: arr[0], month: arr[1] })
}
})
累计学时

累计学时-页面
目标:熟悉累计学时页面结构
ts
import { HcNavBar } from '../commons/components/HcNavBar'
@Entry
@Component
struct StudyTimePage {
@StorageProp('bottomHeight') bottomHeight: number = 0
@Builder
ProgressBuilder(title: string, done: number, total: number, color: string = '#87E0CD') {
GridCol() {
Column() {
Row() {
Text(title)
.fontColor($r('app.color.common_gray_01'))
.fontSize(14)
Text('10%')
.fontColor($r('app.color.common_gray_01'))
.fontSize(14)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ bottom: 5 })
Progress({ value: done, total: total })
.color(color)
.width('100%')
}
}
}
@Builder
DotBuilder(color: string, isDark: boolean) {
Row() {
Text()
.width(7)
.aspectRatio(1)
.borderRadius(4)
.backgroundColor(isDark ? color : $r('app.color.common_gray_01'))
.margin({ right: 4 })
Text(isDark ? '已学占比' : '未学占比')
.fontSize(12)
.fontColor(isDark ? $r('app.color.common_gray_03') : $r('app.color.common_gray_01'))
}
}
build() {
Column() {
HcNavBar({ title: '学习数据', showRightIcon: false })
Column() {
Scroll() {
Column() {
// time
Column() {
Row() {
Text('累计学习时长')
.margin({ right: 5 })
.fontColor($r('app.color.common_gray_03'))
Image($r('sys.media.ohos_ic_public_clock'))
.width(16)
.aspectRatio(1)
.fillColor($r('app.color.common_green'))
}
.width('100%')
.margin({ bottom: 10 })
Row() {
Text('100')
.fontSize(40)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.common_green'))
Text('分钟')
.margin({ bottom: 8, left: 8 })
.fontColor($r('app.color.common_green'))
}
.width('100%')
.alignItems(VerticalAlign.Bottom)
}
// knowledge
Text('知识点学习进度')
.margin({ bottom: 10, top: 30 })
.width('100%')
GridRow({ columns: 2, gutter: 20 }) {
this.ProgressBuilder('ArkTS', 20, 100)
this.ProgressBuilder('ArkTS', 20, 100)
this.ProgressBuilder('ArkTS', 20, 100)
this.ProgressBuilder('ArkTS', 20, 100)
this.ProgressBuilder('ArkTS', 20, 100)
this.ProgressBuilder('ArkTS', 20, 100)
this.ProgressBuilder('ArkTS', 20, 100)
GridCol({ span: 2 }) {
Row({ space: 16 }) {
this.DotBuilder('#87E0CD', true)
this.DotBuilder('#87E0CD', false)
}
.margin({ top: 10 })
}
}
.backgroundColor($r('app.color.white'))
.padding(16)
.borderRadius(8)
.shadow({ color: $r('app.color.common_gray_border'), radius: 16 })
// project
Text('项目学习进度')
.margin({ bottom: 10, top: 30 })
.width('100%')
GridRow({ columns: 1, gutter: 20 }) {
this.ProgressBuilder('HarmonyOS NEXT', 30, 200, '#C7B5ED')
this.ProgressBuilder('HarmonyOS NEXT', 30, 200, '#C7B5ED')
GridCol() {
Row({ space: 16 }) {
this.DotBuilder('#C7B5ED', true)
this.DotBuilder('#C7B5ED', false)
}
.margin({ top: 10 })
}
}
.backgroundColor($r('app.color.white'))
.padding(16)
.borderRadius(8)
.shadow({ color: $r('app.color.common_gray_border'), radius: 16 })
}
.padding(16)
.margin({ bottom: this.bottomHeight })
}
.scrollBar(BarState.Off)
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundImage($r('app.media.study_time_bg'))
.backgroundImageSize({ width: '100%' })
.backgroundColor($r('app.color.common_gray_bg'))
}
}
累计学时-实现
目的:实现累计学时页面数据展示
实现步骤:
- 根据接口文档定义响应数据类型
- 页面初始化获取数据
- 进行渲染
落地代码:
1)数据类型 models/index.ets
ts
// 学习时间
export interface StudyTimeItem {
id: string
name: string
total: number
done: number
undone: number
}
export interface StudyTimeCate {
type: string,
list: StudyTimeItem[]
}
export interface StudyTimeData {
totalTime: number,
studyData: StudyTimeCate[]
}
2)获取数据 StudyTimePage.ets
ts
@State data: StudyTimeData = {
totalTime: 0,
studyData: []
}
dialog: CustomDialogController = new CustomDialogController({
builder: HcLoadingDialog({ message: '加载中...' }),
customStyle: true,
alignment: DialogAlignment.Center
})
async aboutToAppear() {
this.dialog.open()
const res = await http.request<StudyTimeData>({ url: 'studyInfo' })
this.data = res
this.dialog.close()
}
3)进行渲染 StudyTimePage.ets
累计学时:
ts
Row() {
Text(formatTime(this.data.totalTime).replace(/分钟|小时|天/,''))
.fontSize(40)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.common_green'))
Text(formatTime(this.data.totalTime).replace(/\d|\./g,''))
.margin({ bottom: 8, left: 8 })
.fontColor($r('app.color.common_green'))
}
知识点学习进度:
ts
if (this.data.studyData[0]) {
// knowledge
Text('知识点学习进度')
.margin({ bottom: 10, top: 30 })
.width('100%')
GridRow({ columns: 2, gutter: 20 }) {
ForEach(this.data.studyData[0].list, (item: StudyTimeItem) => {
this.ProgressBuilder(item.name, item.done, item.total)
})
GridCol({ span: 2 }) {
Row({ space: 16 }) {
this.DotBuilder('#87E0CD', true)
this.DotBuilder('#87E0CD', false)
}
.margin({ top: 10 })
}
}
.backgroundColor($r('app.color.white'))
.padding(16)
.borderRadius(8)
.shadow({ color: $r('app.color.common_gray_border'), radius: 16 })
}
项目学习进度:
ts
if (this.data.studyData[1]) {
// project
Text('项目学习进度')
.margin({ bottom: 10, top: 30 })
.width('100%')
GridRow({ columns: 1, gutter: 20 }) {
ForEach(this.data.studyData[1].list, (item: StudyTimeItem) => {
this.ProgressBuilder(item.name, item.done, item.total, '#C7B5ED')
})
GridCol() {
Row({ space: 16 }) {
this.DotBuilder('#C7B5ED', true)
this.DotBuilder('#C7B5ED', false)
}
.margin({ top: 10 })
}
}
.backgroundColor($r('app.color.white'))
.padding(16)
.borderRadius(8)
.shadow({ color: $r('app.color.common_gray_border'), radius: 16 })
}
百分比转换:
ts
Text(title)
.fontColor($r('app.color.common_gray_01'))
.fontSize(14)
Text(calcPercentage(done, total))
.fontColor($r('app.color.common_gray_01'))
.fontSize(14)
utils/index.ets
ts
export const calcPercentage = (done: number, total: number) => {
return Math.round(done / total * 100) + '%'
}
常用单词

页面结构
WordPage.ets
ts
import { HcNavBar } from '../commons/components'
@Entry
@Component
struct WordPage {
build() {
Column() {
HcNavBar({ title: '常用单词', showRightIcon: false })
Row() {
Column({ space: 4 }) {
Text('开发常用词汇')
Text(`共 0 个单词`)
.fontSize(12)
.fontColor($r('app.color.common_gray_03'))
}
.alignItems(HorizontalAlign.Start)
Row() {
Text('HarmonyOS')
.fontSize(12)
.fontColor($r('app.color.common_gray_01'))
Image($r('sys.media.ohos_ic_public_arrow_down'))
.width(16)
.aspectRatio(1)
.fillColor($r('app.color.common_gray_01'))
}
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding(16)
.border({ width: { top: 0.5 }, color: $r('app.color.common_gray_bg') })
Divider()
.strokeWidth(8)
.color($r('app.color.common_gray_bg'))
List() {
ForEach([1, 2, 3, 4, 5, 6, 7, 8], () => {
ListItem() {
Row({ space: 6 }) {
Image($r('sys.media.ohos_ic_public_sound'))
.width(20)
.aspectRatio(1)
.alignSelf(ItemAlign.Start)
.fillColor($r('app.color.common_gray_03'))
Column({ space: 10 }) {
Text('title')
.fontWeight(FontWeight.Bold)
Text('标题')
.fontSize(14)
.fontColor($r('app.color.common_gray_03'))
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Row() {
Text('详细代码')
.fontSize(12)
.fontColor($r('app.color.common_gray_01'))
Image($r('sys.media.ohos_ic_public_arrow_right'))
.width(16)
.aspectRatio(1)
.fillColor($r('app.color.common_gray_01'))
}
.alignSelf(ItemAlign.End)
}
.padding(16)
}
})
}
.divider({
strokeWidth: 0.5,
color: $r('app.color.common_gray_bg')
})
.layoutWeight(1)
}
.width('100%')
.height('100%')
}
}
获取rawfile的文件数据
目标:获取 rawfile 中 json 文件的内容
前置准备:
- 下载文件 word.json 放到
rawfile
目录 - 获取资源 resourceManager.getRawFileContentSync 获取二进制数据
- 转换数据 textDecoder.decodeWithStream 把二进制数据转成字符串
落地代码:
1)根据 json 文件约定数据类型
ts
export interface WordItem {
zh: string
en: string
code: string
}
type Words = Record<string, WordItem[]>
2)初始化获取数据
ts
words: Words = {}
aboutToAppear() {
this.initWords()
}
initWords() {
const ctx = getContext(this)
// 获取二进制数据
const uint8Array = ctx.resourceManager.getRawFileContentSync('word.json')
// 实例化文本解析工具
const textDecoder = new util.TextDecoder()
// 解析文本
// const jsonStr = textDecoder.decodeWithStream(uint8Array)
const jsonStr = textDecoder.decodeToString(uint8Array) // API12+
// 转成对象
this.words = JSON.parse(jsonStr) as Words
}
根据数据渲染
根据获取的数据进行默认渲染
落地代码:
1)将来会切换分类,定义一个当前分类KEY的状态,默认是第一条
ts
@State wordKey: string = ''
initWords() {
// 省略...
// 默认显示第一个分类
this.wordKey = Object.keys(this.words)[0]
}
2)渲染页面
ts
Column() {
HcNavBar({ title: '常用单词', showRightIcon: false })
Row() {
Column({ space: 4 }) {
Text('开发常用词汇')
Text(`共 ${this.words[this.wordKey].length} 个单词`)
.fontSize(12)
.fontColor($r('app.color.common_gray_03'))
}
.alignItems(HorizontalAlign.Start)
Row() {
Text(this.wordKey)
.fontSize(12)
.fontColor($r('app.color.common_gray_01'))
Image($r('sys.media.ohos_ic_public_arrow_down'))
.width(16)
.aspectRatio(1)
.fillColor($r('app.color.common_gray_01'))
}
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding(16)
.border({ width: { top: 0.5 }, color: $r('app.color.common_gray_bg') })
Divider()
.strokeWidth(8)
.color($r('app.color.common_gray_bg'))
List() {
ForEach(this.words[this.wordKey], (item: WordItem) => {
ListItem() {
Row({ space: 6 }) {
Image($r('sys.media.ohos_ic_public_sound'))
.width(20)
.aspectRatio(1)
.alignSelf(ItemAlign.Start)
.fillColor($r('app.color.common_gray_03'))
Column({ space: 10 }) {
Text(item.en)
.fontWeight(FontWeight.Bold)
Text(item.zh)
.fontSize(14)
.fontColor($r('app.color.common_gray_03'))
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Row() {
Text('详细代码')
.fontSize(12)
.fontColor(item.code ? $r('app.color.common_gray_01') : '#dddddd')
Image($r('sys.media.ohos_ic_public_arrow_right'))
.width(16)
.aspectRatio(1)
.fillColor(item.code ? $r('app.color.common_gray_01'): '#dddddd')
}
.alignSelf(ItemAlign.End)
}
.padding(16)
}
})
}
.divider({
strokeWidth: 0.5,
color: $r('app.color.common_gray_bg')
})
.layoutWeight(1)
}
.width('100%')
.height('100%')
选择单词分类
1)准备选择单词分类弹窗,选中当前分类
ts
@State showTypeSheet: boolean = false
@Builder
TypeSheetBuilder() {
Column() {
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) {
ForEach(Object.keys(this.words), (key: string) => {
Button() {
Text(key)
.fontSize(14)
.fontColor(key === this.wordKey ? $r('app.color.common_green') : $r('app.color.common_gray_01'))
}
.backgroundColor($r('app.color.common_gray_bg'))
.padding({ top: 6, right: 12, bottom: 6, left: 12 })
.margin({ right: 12, bottom: 12 })
})
}
}
.padding({ left: 16, right: 16, top: 8, bottom: 34 })
}
ts
Row() {
Text(this.wordKey)
.fontSize(12)
.fontColor($r('app.color.common_gray_01'))
Image($r('sys.media.ohos_ic_public_arrow_down'))
.width(16)
.aspectRatio(1)
.fillColor($r('app.color.common_gray_01'))
}
.onClick(() => {
this.showTypeSheet = true
})
.bindSheet($$this.showTypeSheet, this.TypeSheetBuilder, {
title: { title: '选择阶段' },
backgroundColor: $r('app.color.white'),
height: 400
})
2)完成选择单词分类交互,切换后关闭弹窗+回到顶部
ts
Button() {
Text(key)
.fontSize(14)
.fontColor(key === this.wordKey ? $r('app.color.common_green') : $r('app.color.common_gray_01'))
}
.backgroundColor($r('app.color.common_gray_bg'))
.padding({ top: 6, right: 12, bottom: 6, left: 12 })
.margin({ right: 12, bottom: 12 })
.onClick(() => {
this.wordKey = key
this.showTypeSheet = false
this.scroller.scrollTo({ yOffset: 0, xOffset: 0 })
})
ts
scroller = new Scroller()
// ...
List({ scroller: this.scroller }) {
示例代码半模态框
1)准备代码弹出
ts
@State currentCode: string = ''
webController = new webview.WebviewController()
ts
@Builder
CodeSheetBuilder() {
Column() {
Web({ src: $rawfile('word.html'), controller: this.webController })
.width('100%')
.height(400)
.backgroundColor($r('app.color.common_gray_bg'))
.onPageEnd(() => {
this.webController.runJavaScript(`writeHtml(\`${this.currentCode}\`)`)
})
}
.padding({ left: 16, right: 16, top: 8, bottom: 34 })
}
2)绑定和现实代码半模态框
ts
@State showCodeSheet: boolean = false
ts
.width('100%')
.height('100%')
.bindSheet($$this.showCodeSheet, this.CodeSheetBuilder, {
detents: [SheetSize.FIT_CONTENT],
backgroundColor: $r('app.color.white'),
title: { title: '详细代码' }
})
3)点击代码
ts
Row() {
Text('详细代码')
.fontSize(12)
.fontColor(item.code ? $r('app.color.common_gray_01') : '#dddddd')
Image($r('sys.media.ohos_ic_public_arrow_right'))
.width(16)
.aspectRatio(1)
.fillColor(item.code ? $r('app.color.common_gray_01'): '#dddddd')
}
.alignSelf(ItemAlign.End)
.onClick(() => {
this.currentCode = item.code
if (this.currentCode) {
this.showCodeSheet = true
} else {
promptAction.showToast({ message: '暂无代码' })
}
})
单词朗读 Dialog
1)准备静态的 Dialog 组件 views/Word/WordDialog.ets
ts
@CustomDialog
export struct WordDialog {
controller: CustomDialogController
@Prop en: string = ''
@Prop zh: string = ''
build() {
Column({ space: 10 }) {
Row({ space: 10 }) {
Text(this.en)
.fontSize(20)
.fontColor($r('app.color.white'))
.fontWeight(500)
WordSoundComp()
}
Text(this.zh)
.fontColor($r('app.color.white'))
}
.constraintSize({ minWidth: 175 })
.padding({ left: 16, right: 16 })
.height(90)
.borderRadius(45)
.backgroundColor('#8f000000')
.justifyContent(FlexAlign.Center)
}
}
@Component
struct WordSoundComp {
@State flag: boolean = false
timerId?: number
aboutToAppear(): void {
this.timerId = setInterval(() => {
this.flag = !this.flag
}, 500)
}
aboutToDisappear(): void {
clearInterval(this.timerId)
}
build() {
Image($r('sys.media.ohos_ic_public_sound'))
.width(20)
.aspectRatio(1)
.fillColor(this.flag ? $r('app.color.common_green') : $r('app.color.white'))
}
}
2)打开 Dialog pages/WordPage.ets
ts
@State currentEn: string = ''
@State currentZh: string = ''
dialog = new CustomDialogController({
builder: WordDialog({ en: this.currentEn, zh: this.currentZh }),
customStyle: true,
alignment: DialogAlignment.Center
})
ts
Image($r('sys.media.ohos_ic_public_sound'))
.width(20)
.aspectRatio(1)
.alignSelf(ItemAlign.Start)
.fillColor($r('app.color.common_gray_03'))
.onClick(() => {
this.currentEn = item.en
this.currentZh = item.zh
this.dialog.open()
})
使用 AvPlayer 实现朗读
目标:了解如何播放网络音频
准备知识:
- AvPlayer 播放管理类,用于管理和播放媒体资源。
- AVPlayerState 播放的全流程包含:创建AVPlayer,设置播放资源,设置播放参数(音量/倍速/焦点模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。
落地代码:
views/Word/WordDialog.ets
ts
avPlayer?: media.AVPlayer
async aboutToAppear() {
const avPlayer = await media.createAVPlayer()
avPlayer.on('stateChange', state => {
if (state === 'initialized') {
avPlayer.prepare()
} else if ( state === 'prepared') {
avPlayer.loop = true
avPlayer.play()
}
})
avPlayer.url = `https://dict.youdao.com/dictvoice?type=1&audio=${this.en}`
this.avPlayer = avPlayer
}
aboutToDisappear(): void {
if (this.avPlayer) {
this.avPlayer.stop()
this.avPlayer.release()
}
}