# ReciteWords **Repository Path**: get-down-business/recite-words ## Basic Information - **Project Name**: ReciteWords - **Description**: 使用ArkTS开发的背单词APP - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2025-06-12 - **Last Updated**: 2026-03-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 背单词APP https://gitee.com/get-down-business/recite-words ## 0、配置 首先在DevEco Studio上的右上角,点击用户,登录华为账号 1. 创建新项目:设置应用名称和应用包名,应用包名对应项目中的AppScope/app.json5的bundleName ![image-20250612115305365](README.assets/image-20250612115305365.png) 2. 选择数据处理位置:中国、默认 ![image-20250612115351569](README.assets/image-20250612115351569.png) 3. 找到创建的应用 ![image-20250612115448351](README.assets/image-20250612115448351.png) 4. 进行SDK配置: * 首先下载agconnect-services.json,放到项目中的Appscope/resources/rawfile下 * 配置SDK依赖 1. 打开DevEco Studio应用级(一般为entry)下的“oh-package.json5”文件 2. 在“oh-package.json5”文件里面添加认证服务的编译依赖和SDK依赖 3. ``` "dependencies": { "@hw-agconnect/auth": "^1.0.2" } ``` 4. 打开修改完的“oh-package.json5”文件,右上方出现“Sync Now”链接,点击“Sync Now”等待同步完成。 ![image-20250612115621151](README.assets/image-20250612115621151.png) ![image-20250612135310409](README.assets/image-20250612135310409.png) 5. AGC 项目集成 * 添加网络权限,在`entry/src/main/module.json5`文件中添加网络权限 ``` "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ] ``` * 引入AGC组件 ``` import auth from '@hw-agconnect/auth'; ``` * 在您的应用初始化阶段使用context初始化SDK,推荐在“entry/src/main/ets/entryability/EntryAbility.ets”的onCreate中进行。 ``` //初始化SDK onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET); let file = this.context.resourceManager.getRawFileContentSync('agconnect-services.json'); let json: string = buffer.from(file.buffer).toString(); auth.init(this.context, json); hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); } ``` 6. 混淆脚本 ![image-20250612140009857](README.assets/image-20250612140009857.png) 打开混淆规则配置文件“obfuscation-rules.txt”。参照示例添加混淆规则。 ``` -keep ./oh_modules/@hw-agconnect/auth ``` 7. 在 AppGallery Connect 中,点击认证服务,立即开通。在手机号码中点击启用 image-20250612140351542 在 验证码/通知接收地址 里输入:http://39.101.189.16:8111/huawei/verify 后续可通过`http://39.101.189.16:8111/verify/query/你的电话号码`查看获取的验证码 ## 一、目录结构 ### (1)src/main/ets/api 里面主要包含各个接口的 view object 定义和接口的实现 #### 1)src/main/ets/api/vo 分别包括请求和响应的 view object image-20250617085949474 ### (2)src/main/ets/model 主要是实体类的定义 ![image-20250617090103686](README.assets/image-20250617090103686.png) ### (3)src/main/ets/pages 主要是每个页面的实现,包括具体的页面和每个页面对应的相关组件 image-20250617090318573 ### (4)src/main/ets/util 包含一系列工具方法,进行数据的相关处理 ![image-20250617090439786](README.assets/image-20250617090439786.png) ## 二、数据库相关 ### (1)src/main/ets/util/GlobalContext.ets ``` /* * Copyright (c) 2023 Huawei Device Co., Ltd. * Licensed under the Apache License,Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ export class GlobalContext { private constructor() { } private static instance: GlobalContext; private _objects = new Map(); public static getContext(): GlobalContext { if (!GlobalContext.instance) { GlobalContext.instance = new GlobalContext(); } return GlobalContext.instance; } getObject(value: string): Object | undefined { return this._objects.get(value); } setObject(key: string, objectClass: Object): void { this._objects.set(key, objectClass); } } ``` ### (2)src/main/ets/util/file.ets ``` import resourceManager from "@ohos.resourceManager"; import { fileIo } from "@kit.CoreFileKit"; import { GlobalContext } from "./GlobalContext"; export function copyFile2Sandbox(fileName:string) { const context = GlobalContext.getContext().getObject('context') as Context let progress: number = 0; // 拿到原始文件路径 console.info(fileName) const data: resourceManager.RawFileDescriptor = context.resourceManager.getRawFdSync(fileName); // 保存文件路径 const targetPath: string = context.databaseDir + '/rdb/' + fileName; console.info(`targetPath:${targetPath}`) // 判断是否有操作路径的,没有的话创建文件夹 if (!fileIo.accessSync(context.databaseDir + '/rdb/')){ fileIo.mkdirSync(context.databaseDir + '/rdb/') } const destFile: fileIo.File = fileIo.openSync(targetPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE); let buffSize: number = 4096; const buffer: ArrayBuffer = new ArrayBuffer(Math.min(buffSize, data.length)); // Offset of the record read position (compared with the start offset of the file) let off: number = 0; // Actual length of the read content. let len: number = fileIo.readSync(data.fd, buffer, { offset: data.offset + off, length: buffSize }); // Record the length of the read file. let readLen: number = 0; while (len > 0) { readLen += len; fileIo.writeSync(destFile.fd, buffer, { offset: off, length: len }); progress = Math.floor((readLen / data.length) * 100); off = off + len; if ((data.length - readLen) < buffSize) { buffSize = data.length - readLen; } len = fileIo.readSync(data.fd, buffer, { offset: data.offset + off, length: buffSize }); } fileIo.close(destFile.fd); } ``` ### (3)src/main/ets/util/databaseInit.ets ``` import { relationalStore } from "@kit.ArkData" import { copyFile2Sandbox } from "./file"; import { GlobalContext } from "./GlobalContext"; const DATABASE_RAW_FILE_NAME = 'recite_words_rdb_init.db' export const RELATIONAL_STORE_NAME = 'recite_words_rdb.db' const RELATIONAL_STORE_CONFIG :relationalStore.StoreConfig= { name: RELATIONAL_STORE_NAME, // 数据库文件名 securityLevel: relationalStore.SecurityLevel.S1, // 数据库安全级别 }; export async function getRdbStore(): Promise{ const context = GlobalContext.getContext().getObject('context') as Context return await relationalStore.getRdbStore(context,RELATIONAL_STORE_CONFIG) } export async function initWordsDatabase(){ // 从GlobalContext中获取Context await copyFile2Sandbox(DATABASE_RAW_FILE_NAME) // 将文件复制到沙箱 let store = await getRdbStore(); // 初始化数据库脚本 try { await store.restore(DATABASE_RAW_FILE_NAME) return true } catch (e) { console.info(`数据库初始化错误`) return false } } ``` ## 三、登录界面 ### (1)Login 接口 ![image-20250617085317754](README.assets/image-20250617085317754.png) ![image-20250617085352720](README.assets/image-20250617085352720.png) #### 1)src/main/ets/api/axios.ets axios 相关配置 ``` // 导入 axios 库,这里使用的是 HarmonyOS 相关的 axios 包 import axios from '@ohos/axios'; const BASE_URL = "http://39.101.189.16:8085/"; const axiosInstance = axios.create({ baseURL: BASE_URL,// 设置请求的基础 URL,每次发送请求时,Axios 会自动加上这个基础 URL timeout: 1000, // 设置请求的超时时间(单位为毫秒),这里设置超时时间为 1000ms,即 1 秒,如果请求超时,axios 会抛出错误, headers: { // 设置请求头,通常用于传递 API 验证令牌或其他认证信息 'X-Cskaoyan-Token': 'wangdao' // 自定义请求头,传递一个名为 X-Cskaoyan-Token 的 token 参数 } }); export default axiosInstance; ``` #### 2)src/main/ets/util/LoginUtil.ets LoginUtil工具,接口实现 ``` import auth, { SignInResult } from "@hw-agconnect/auth" export const loginByMessage = async (mobile:string,code:string)=>{ let signInResult: SignInResult = await auth.signIn({ credentialInfo: { kind: 'phone', phoneNumber: mobile, countryCode: '86', verifyCode: code } }) return signInResult } ``` #### 3)src/main/ets/pages/LoginPage.ets 登录界面的UI ``` import auth, { AuthUser, VerifyCodeAction } from '@hw-agconnect/auth' import { loginByMessage } from '../util/LoginUtil' @Component export struct LoginPage { @State mobile: string = '' @State code: string = '' @Consume authUser: AuthUser | null @Consume userId:string @Consume isLogin:boolean async aboutToAppear() { } build() { Column() { // 标题 Row() { Image($r('app.media.layeredImage')) .width('20%') .aspectRatio(1) .margin({ right: 5 }) Text('王道词王')// 显示“登录页面”标题 .fontSize('30fp')// 设置字体大小 .width('40%') } .width('100%') // 设置宽度 .margin(20) // 设置上边距 .justifyContent(FlexAlign.Center) // 手机号输入框 Row(){ TextInput({ placeholder: '请输入手机号', text: $$this.mobile }) .type(InputType.Number) .height(40) .width('100%') .borderRadius(15) .padding({ left: 15, right: 15 }) .margin({ top:20 ,bottom:20}) } .width('90%') .justifyContent(FlexAlign.Center) // 验证码输入框与获取验证码按钮的横向布局 Row() { TextInput({ placeholder: '请输入验证码', text: $$this.code }) .type(InputType.Number) .height(40) .layoutWeight(1) .borderRadius(15) .padding({ left: 15, right: 15 }) Button('获取验证码') .width(120) .height(40) .margin({ left: 10 }) .fontColor('#FFFFFF') .backgroundColor('#1890FF') .borderRadius(8) .onClick(() => { auth.requestVerifyCode({ action: VerifyCodeAction.REGISTER_LOGIN, lang: 'zh_CN', sendInterval: 60, verifyCodeType: { phoneNumber: this.mobile, countryCode: "86", kind: "phone", } }).then(verifyCodeResult => { // 验证码申请成功 console.info(`验证码申请成功:${JSON.stringify(verifyCodeResult)}`) AlertDialog.show({ message: "验证码已发送" }) }).catch((error: Error) => { // 验证码申请失败 AlertDialog.show({ message: "验证码发送失败" }) console.error(`验证码申请失败:${error.message}`) }) }) } .width('90%') .margin({ bottom: 30 }) // 登录按钮 Row(){ Button('登录') .width('80%') .height(40) .fontColor('#FFFFFF') .backgroundColor('#1890FF') .borderRadius(8) .margin({ bottom: 20 }) .onClick(async () => { try { let signInResult = await loginByMessage(this.mobile, this.code) // this.showLogin = true this.authUser = signInResult.getUser() let authUser = signInResult.getUser() this.userId = authUser.getUid() this.isLogin = true AlertDialog.show({ message: `登录成功:${authUser.getUid()}` }) } catch (e) { AlertDialog.show({ message: `登录失败:${e.message}` }) } }) } .width('100%') .justifyContent(FlexAlign.Center) // 底部链接 Row() { Text('忘记密码') .fontSize(14) .fontColor('#666666') .onClick(() => { // 忘记密码点击事件 }) Text(' | ') .margin({ left: 15, right: 15 }) .fontColor('#CCCCCC') Text('注册账号') .fontSize(14) .fontColor('#666666') .onClick(() => { // 注册账号点击事件 }) } .margin({ top: 20 }) } .width('100%') .height('100%') } } ``` 输入手机号现货区验证码,获取验证码之后,点击登录,进入系统,进入系统后弹窗,显示uid信息 ![image-20250617091535822](README.assets/image-20250617091535822.png) 登录成功后默认进入首页,显示已经完成全部的单词背诵 ## 四、整合界面 ### (1)src/main/ets/pages/Index.ets 总整合界面,通过状态变量判断是否已经登录,从而分别跳转到分整合界面或者登录界面 ``` import auth, { AuthUser } from '@hw-agconnect/auth'; import { LoginPage } from './LoginPage'; import { TotalPage } from './TotalPage'; @Entry @Component struct Index { @Provide userId:string = '' @Provide isLogin:boolean = false @Provide finished:boolean = true @Provide authUser: AuthUser | null = null @Provide selectedWordList: number[] = [] async aboutToAppear() { this.userId = '' this.isLogin = false this.authUser = null try { const currentUser = await auth.getCurrentUser() if (currentUser) { this.authUser = currentUser this.userId = currentUser.getUid() this.isLogin = true } } catch (error) { console.error('获取当前用户失败:', error) } } async aboutToDisappear() { try { await auth.signOut() } catch (error) { console.error('登出失败:', error) } } build() { RelativeContainer() { if(!this.isLogin){ LoginPage() } else{ TotalPage() } // TotalPage() } .height('100%') .width('100%') } } ``` ### (2)src/main/ets/pages/TotalPage.ets 实现对背单词App内的界面整合 ``` import { FirstPage } from "./FirstPage" import { PayHistoryPage } from "./FourthComponent/PayHistoryPage" import { ProductPage } from "./FourthComponent/ProductPage" import { FourthPage } from "./FourthPage" import { WordSelectPage } from "./SecondComponent/WordSelectPage" import { SecondPage } from "./SecondPage" import { ThirdPage } from "./ThirdPage" @Preview @Component export struct TotalPage { @State @Watch('tabChange') selectedIndex: number = 0 @Provide navPathStack: NavPathStack = new NavPathStack() @Provide selectedBookId: number = 0 @Provide selectedBookName: string = '' @Provide payVocBound: number = 0 @Provide vocCountOfDay: number = 0 @Provide remindTime: string = '' @Consume selectedWordList: number[] tabChange() { console.info(`标签页切换到了${this.selectedIndex}`) } build() { Navigation(this.navPathStack) { Tabs() { TabContent() { FirstPage() } .tabBar(this.myTabBar('首页', $r('app.media.home_selected'), $r('app.media.home_normal'), 0)) TabContent() { SecondPage() } .tabBar(this.myTabBar('选词', $r('app.media.choice_selected'), $r('app.media.choice_normal'), 1)) TabContent() { ThirdPage() } .tabBar(this.myTabBar('统计', $r('app.media.statistic_selected'), $r('app.media.statistic_normal'), 2)) TabContent() { FourthPage() } .tabBar(this.myTabBar('我的', $r('app.media.mine_selected'), $r('app.media.mine_normal'), 3)) } .height('100%') .width('100%') .barPosition(BarPosition.End) .onChange(index => { this.selectedIndex = index }) } .navDestination(this.pageMap) .width('100%') .height('100%') .hideTitleBar(true) } @Builder myTabBar(title: string, icon1: ResourceStr, icon2: ResourceStr, index: number) { Column() { Image(this.selectedIndex === index ? icon1 : icon2) .height(this.selectedIndex === index ? 30 : 25) .aspectRatio(1) Text(title) .fontColor(this.selectedIndex === index ? Color.Blue : Color.Black) .fontSize(this.selectedIndex === index ? '18fp' : '14fp') .fontWeight(this.selectedIndex === index ? FontWeight.Bold : FontWeight.Normal) } } @Builder pageMap(name: string) { if ('FirstPage' === name) { FirstPage() } else if ('SecondPage' === name) { SecondPage() } else if ('ThirdPage' === name) { ThirdPage() } else if ('FourthPage' === name) { FourthPage() } else if ('WordSelectPage' === name) { WordSelectPage() } else if ('ProductPage' === name) { ProductPage() } else if ('PayHistoryPage' === name) { PayHistoryPage() } } } ``` ### (3)src/main/ets/util/PageConstant.ets ``` export class PageConstant{ static readonly PAGE_APPLICATION = "TotalPage"; } ``` ## 五、选词界面 从RdbStore中获取当前背诵的书籍id信息,从数据库获取背诵的书籍名称 通过请求获取当前书籍的统计信息,并且在文字部分去显示,通过进度条按照百分比来显示颜色 ### 第一部分 #### (1)/user/voc/current/{id}/statistics接口 ![image-20250617141500258](README.assets/image-20250617141500258.png) ![image-20250617141520641](README.assets/image-20250617141520641.png) ![image-20250617141554174](README.assets/image-20250617141554174.png) ##### src/main/ets/api/vo/request/CurrentIdStaticsRequestVo.ets ``` export class CurrentIdStaticsRequestVo{ msg:string = '' code:number = 0 currentIdStatics?:CurrentIdStatics } export class CurrentIdStatics{ familiar:number = 0 general:number = 0 notStarted:number = 0 stopped:number = 0 totalCount:number = 1 unFamiliar:number = 0 } ``` ##### src/main/ets/api/currentIdStatics.ets ``` import { AxiosResponse } from "@ohos/axios" import axiosInstance from "./axios" import { CurrentIdStatics, CurrentIdStaticsRequestVo } from "./vo/request/CurrentIdStaticsRequestVo" export const currentIdStatics = async (id:number)=>{ try { console.log(`[API] 开始请求,ID: ${id}`) const startTime = new Date().getTime() let response:AxiosResponse = await axiosInstance.get(`/user/voc/current/${id}/statistics`) console.log(`[API] 请求完成,耗时: ${new Date().getTime() - startTime}ms`) console.log('[API] 响应状态:', response.status) console.log('[API] 响应数据:', JSON.stringify(response.data)) if (response.status === 200) { return response.data.data || new CurrentIdStatics() } return new CurrentIdStatics() // 返回空数据而非null } catch (error){ console.error('[API] 请求异常:', JSON.stringify(error)) if (error.response) { console.error('[API] 错误响应:', error.response.status, error.response.data) } return new CurrentIdStatics() } } ``` ##### src/main/ets/pages/SecondComponent/FirstStatisitcs.ets ``` import { currentIdStatics } from "../../api/currentIdStatics" import { CurrentIdStatics } from "../../api/vo/request/CurrentIdStaticsRequestVo" @Component @Preview export struct FirstStatics { @Consume selectedBookName: string @Consume @Watch('onselectBookChange') selectedBookId: number @State currentIdStatics: CurrentIdStatics = new CurrentIdStatics() @State proportion:Proportion = new Proportion() async onselectBookChange() { let data = await currentIdStatics(this.selectedBookId) as CurrentIdStatics if (data) { this.currentIdStatics = data this.numProportion() } } numProportion() { const total = Math.max(this.currentIdStatics.totalCount, 1) const MIN_WIDTH = 0.01 // this.proportion.familiar = Math.max(this.currentIdStatics.familiar / total,MIN_WIDTH) // this.proportion.general = Math.max(this.currentIdStatics.general / total,MIN_WIDTH) // this.proportion.unFamiliar = Math.max(this.currentIdStatics.unFamiliar / total,MIN_WIDTH) // this.proportion.notStarted = Math.max(this.currentIdStatics.notStarted / total,MIN_WIDTH) // this.proportion.stopped = Math.max(this.currentIdStatics.stopped / total,MIN_WIDTH) // this.proportion.familiar = this.currentIdStatics.familiar / total this.proportion.general = this.currentIdStatics.general / total this.proportion.unFamiliar = this.currentIdStatics.unFamiliar / total this.proportion.notStarted = this.currentIdStatics.notStarted / total this.proportion.stopped = this.currentIdStatics.stopped / total } build() { Row() { Column() { Image($r('app.media.chart')) .width('95%') .aspectRatio(1) } .justifyContent(FlexAlign.Center) .width('5%') .margin({left:5,right:5}) Column() { Row() { Text(`${this.selectedBookName}`) .fontSize('18fp') .textAlign(TextAlign.Start) } .width('100%') .margin({bottom:10}) Row() { Stack(){ Row() .width(`${this.proportion.familiar * 100}%`) .backgroundColor('#ff0af30a') .height('5%') .position({left:`0%`}) Row() .width(`${this.proportion.general * 100}%`) .height('5%') .backgroundColor('#ffee8b22') .position({left:`${this.proportion.familiar * 100}%`}) Row() .height('5%') .backgroundColor('#ffc820f8') .width(`${this.proportion.unFamiliar * 100}%`) .position({left:`${(this.proportion.familiar + this.proportion.general) * 100}%`}) Row() .height('5%') .backgroundColor('#ff727572') .width(`${this.proportion.notStarted * 100}%`) .position({left:`${(this.proportion.familiar + this.proportion.general + this.proportion.unFamiliar) * 100}%`}) Row() .height('5%') .backgroundColor('#ff100f0f') .width(`${this.proportion.stopped * 100}%`) .position({left:`${(this.proportion.familiar + this.proportion.general + this.proportion.unFamiliar + this.proportion.notStarted) * 100}%`}) } .width('100%') .backgroundColor('#fff6f6f6') } .width('100%') .margin({bottom:10}) Row(){ Text(`熟悉 ${this.currentIdStatics.familiar}`) .fontColor('#ff0af30a') .width('18%') Text('|') .width('2%') Text(`一般 ${this.currentIdStatics.general}`) .fontColor('#ffee8b22') .width('18%') Text('|') .width('2%') Text(`不熟 ${this.currentIdStatics.unFamiliar}`) .fontColor('#ffc820f8') .width('18%') Text('|') .width('2%') Text(`未选 ${this.currentIdStatics.notStarted}`) .fontColor('#ff727572') .width('18%') Text('|') .width('2%') Text(`暂停 ${this.currentIdStatics.stopped}`) .fontColor('#ff100f0f') .width('18%') } .width('100%') .justifyContent(FlexAlign.Start) .margin({bottom:10}) Row(){ Text(`完成情况 `) .fontColor(Color.Gray) Text(`${(this.currentIdStatics.familiar+this.currentIdStatics.general)} / ${this.currentIdStatics.totalCount}`) .fontColor(Color.Blue) } .width('100%') } .width('85%') .padding(5) Column(){ Image($r('app.media.points')) .width('90%') .aspectRatio(1) } .width('5%') .justifyContent(FlexAlign.Center) .margin({left:10}) } .width('100%') .height('100%') } } class Proportion { familiar: number = 0 general: number = 0 unFamiliar: number = 0 notStarted: number = 0 stopped: number = 0 } ``` ![image-20250617142609384](README.assets/image-20250617142609384.png) ### 第二部分 #### src/main/ets/pages/SecondComponent/SecondSelectedPart.ets ``` @Component @Preview export struct SecondSelectedPart{ build() { Column(){ Row(){ Image($r('app.media.star')) .width('15%') .aspectRatio(1) Text('已选单词') .fontSize('20fp') } .width('100%') .height('30%') .justifyContent(FlexAlign.Start) .margin({top:10}) Row(){ Text('全部 | 今日 | 顽固 | 熟知 | 暂停 | 未按规划') .fontColor(Color.Gray) .fontSize('15fp') } .width('100%') .height('30%') .justifyContent(FlexAlign.Center) Row(){ Text(' Word Library') .fontColor(Color.White) .backgroundColor(Color.Blue) .width('50%') .height('100%') .textAlign(TextAlign.Start) } .width('100%') .height('30%') .justifyContent(FlexAlign.Start) } .width('100%') .height('100') } } ``` ![image-20250617142807492](README.assets/image-20250617142807492.png) ------ ### 第三部分 #### 书籍类 ##### src/main/ets/model/dao/BookDao.ets ``` export class BookDao { id: number = 0 bc_id: number = 0 bc_name: string = '' name: string = '' author: string = '' publisher: string = '' isbn: string = '' voc_count: number = 0 comment: string = '' version: string = '' status: number = 1 sort_by: number = 0 chapter_count: number = 0 create_time: string = '' updated_time: string = '' is_deleted: number = 0 constructor(id: number = 0, bc_id: number = 0, bc_name: string = '', name: string = '', author: string = '', publisher: string = '', isbn: string = '', voc_count: number = 0, comment: string = '', version: string = '', status: number = 1, sort_by: number = 0, chapter_count: number = 0, create_time: string = '', updated_time: string = '', is_deleted: number = 0) { this.id = id this.bc_id = bc_id this.bc_name = bc_name this.name = name this.author = author this.publisher = publisher this.isbn = isbn this.voc_count = voc_count this.comment = comment this.version = version this.status = status this.sort_by = sort_by this.chapter_count = chapter_count this.create_time = create_time this.updated_time = updated_time this.is_deleted = is_deleted } } ``` #### 书籍种类 ##### src/main/ets/model/dao/BookCategoriesDao.ets ``` export class BookCategoriesDao { id: number = 0 category_name: string = '' sort_by: number = 0 status: number = 1 creat_time: string = '' updated_time: string = '' is_deleted: number = 0 book_count: string = '' constructor(id: number = 0, category_name: string = '', sort_by: number = 0, status: number = 0, creat_time: string = '', updated_time: string = '', is_deleted: number = 0, book_count: string = '') { this.id = id this.category_name = category_name this.sort_by = sort_by this.status = status this.creat_time = creat_time this.updated_time = updated_time this.is_deleted = is_deleted this.book_count = book_count } } ``` #### 第三部分的UI ##### src/main/ets/pages/SecondComponent/ThirdBooksList.ets ``` import { relationalStore } from "@kit.ArkData" import { BookCategoriesDao } from "../../model/dao/BookCategoriesDao" import { BookDao } from "../../model/dao/BookDao" import { RELATIONAL_STORE_NAME } from "../../util/databaseInit" import file from '@ohos.file.fs' interface FileInfo { name: string; path: string; size: number; lastModifiedTime: number; exists: boolean; isDirectory: boolean; mode: number; owner: number; group: number; } @Component export struct ThirdBooksList { store?: relationalStore.RdbStore @State selectedCategoryId: number = 0 @State num: number = -1 @State isLoading: boolean = false @State errorMessage: string = '' @State dbInitSuccess: boolean = false @State showBookList:string[] = [] @Consume bookCategories: BookCategoriesDao[] @Consume allBooks:BookDao[] @Consume selectedBookName:string @Consume selectedBookId:number @Consume navPathStack: NavPathStack async aboutToAppear() { console.log("开始加载数据库...") try { const isDatabaseExists = await this.checkDatabaseExists() console.log(`数据库文件是否存在: ${isDatabaseExists}`) if (!isDatabaseExists) { this.errorMessage = "数据库文件不存在,请确认初始化" return } const config = { name: RELATIONAL_STORE_NAME, securityLevel: relationalStore.SecurityLevel.S1 } as relationalStore.StoreConfig this.store = await relationalStore.getRdbStore(getContext(), config) console.log("数据库连接成功") this.dbInitSuccess = true const hasRequiredTables = await this.checkTableStructure() if (!hasRequiredTables) { this.errorMessage = "数据库表结构不完整,请确认初始化" return } await this.getBookCategories() await this.getAllBooks() console.log("分类数量:", this.bookCategories.length) // 确认数据是否加载 } catch (error) { console.error("数据库初始化失败:", JSON.stringify(error)) this.errorMessage = `数据库连接失败: ${error.message || '未知错误'}` } await this.getBookCategories(); await this.getAllBooks(); await this.checkBooksData(); } async checkBooksData() { // 检查书籍总数 const totalBooks = this.store?.querySqlSync("SELECT COUNT(*) FROM voc_books"); console.log(`书籍总数: ${totalBooks?.getLong(0)}`); } aboutToDisappear() { try { if (this.store) { this.store.close() console.log("数据库连接已关闭") } } catch (error) { console.error("关闭数据库连接失败:", error) } } async checkDatabaseExists(): Promise { try { const context = getContext() const fileDir = context.filesDir const dbPath = `${fileDir}/${RELATIONAL_STORE_NAME}` // 使用 access API 检查文件是否存在 await file.access(dbPath) return true } catch (error) { console.error("数据库文件不存在:", error) return false } } async checkTableStructure(): Promise { try { if (!this.store) return false // 检查book_categories表是否存在 const resultSet = this.store.querySqlSync("SELECT name FROM sqlite_master WHERE type='table' AND name='book_categories'") if (!resultSet || resultSet.rowCount === 0) { console.error("book_categories表不存在") return false } // 检查voc_books表是否存在 const bookResultSet = this.store.querySqlSync("SELECT name FROM sqlite_master WHERE type='table' AND name='voc_books'") if (!bookResultSet || bookResultSet.rowCount === 0) { console.error("voc_books表不存在") return false } return true } catch (error) { console.error("检查表结构失败:", error) return false } } async getBookCategories() { // let predicates = new relationalStore.RdbPredicates('book_categories') // let resultSet = this.store?.querySync(predicates, // ['id', 'category_name', 'sort_by', 'status', 'creat_time', 'updated_time', 'is_deleted', 'book_count']) try { let resultSet = this.store?.querySqlSync('select * from book_categories') if (!resultSet || resultSet.rowCount <= 0) { console.error('查询失败或没有数据') this.bookCategories = [] return } console.log('查询到的分类数量:', resultSet.rowCount) let categories: BookCategoriesDao[] = [] const columnNames = resultSet.columnNames console.log("可用列名:", columnNames) while (resultSet?.goToNextRow()) { let category = new BookCategoriesDao() if (columnNames.includes('id')) { category.id = resultSet.getLong(resultSet.getColumnIndex('id')) } if (columnNames.includes('category_name')) { category.category_name = resultSet.getString(resultSet.getColumnIndex('category_name')) } if (columnNames.includes('sort_by')) { category.sort_by = resultSet.getLong(resultSet.getColumnIndex('sort_by')) } if (columnNames.includes('status')) { category.status = resultSet.getLong(resultSet.getColumnIndex('status')) } if (columnNames.includes('creat_time')) { // 注意: 可能是 create_time category.creat_time = resultSet.getString(resultSet.getColumnIndex('creat_time')) } if (columnNames.includes('updated_time')) { category.updated_time = resultSet.getString(resultSet.getColumnIndex('updated_time')) } if (columnNames.includes('is_deleted')) { category.is_deleted = resultSet.getLong(resultSet.getColumnIndex('is_deleted')) } if (columnNames.includes('book_count')) { category.book_count = resultSet.getString(resultSet.getColumnIndex('book_count')) } console.log(`分类${categories.length}: ${JSON.stringify(category)}`) categories.push(category) } this.bookCategories = categories console.log("分类数据已更新:", this.bookCategories.length) } catch (error) { console.error("获取分类数据失败:", error) this.errorMessage = `获取分类数据失败: ${error.message || '未知错误'}` this.bookCategories = [] } finally { this.isLoading = false } } async getAllBooks() { try { let resultSet = this.store?.querySqlSync('select * from voc_books') if (!resultSet || resultSet.rowCount <= 0) { console.error('查询失败或没有数据') this.bookCategories = [] return } console.log('查询到的分类数量:', resultSet.rowCount) let books: BookDao[] = [] const columnNames = resultSet.columnNames console.log("可用列名:", columnNames) while (resultSet?.goToNextRow()) { let book = new BookDao() if (columnNames.includes('id')) { book.id = resultSet.getLong(resultSet.getColumnIndex('id')) } if (columnNames.includes('bc_id')) { book.bc_id = resultSet.getLong(resultSet.getColumnIndex('bc_id')) } if (columnNames.includes('bc_name')) { book.bc_name = resultSet.getString(resultSet.getColumnIndex('bc_name')) } if (columnNames.includes('name')) { book.name = resultSet.getString(resultSet.getColumnIndex('name')) } if (columnNames.includes('author')) { book.author = resultSet.getString(resultSet.getColumnIndex('author')) } if (columnNames.includes('publisher')) { book.publisher = resultSet.getString(resultSet.getColumnIndex('publisher')) } if (columnNames.includes('isbn')) { book.isbn = resultSet.getString(resultSet.getColumnIndex('isbn')) } if (columnNames.includes('voc_count')) { book.voc_count = resultSet.getLong(resultSet.getColumnIndex('voc_count')) } if (columnNames.includes('comment')) { book.comment = resultSet.getString(resultSet.getColumnIndex('comment')) } if (columnNames.includes('version')) { book.version = resultSet.getString(resultSet.getColumnIndex('version')) } if (columnNames.includes('status')) { book.status = resultSet.getLong(resultSet.getColumnIndex('status')) } if (columnNames.includes('sort_by')) { book.sort_by = resultSet.getLong(resultSet.getColumnIndex('sort_by')) } if (columnNames.includes('chapter_count')) { book.chapter_count = resultSet.getLong(resultSet.getColumnIndex('chapter_count')) } if (columnNames.includes('create_time')) { book.create_time = resultSet.getString(resultSet.getColumnIndex('create_time')) } if (columnNames.includes('updated_time')) { book.updated_time = resultSet.getString(resultSet.getColumnIndex('updated_time')) } if (columnNames.includes('is_deleted')) { book.is_deleted = resultSet.getLong(resultSet.getColumnIndex('is_deleted')) } console.log(`分类${books.length}: ${JSON.stringify(book)}`) books.push(book) } this.allBooks = books console.log("分类数据已更新:", this.allBooks.length) } catch (error) { console.error("获取分类数据失败:", error) this.errorMessage = `获取分类数据失败: ${error.message || '未知错误'}` this.allBooks = [] } finally { this.isLoading = false } } async getBookList(name:string) { try { let resultSet = this.store?.querySqlSync(`select name from voc_books where bc_name = ?`, [name]) if (!resultSet || resultSet.rowCount <= 0) { console.error('书籍查询失败或没有数据') this.errorMessage = "未找到相关书籍" this.showBookList = [] return } const columnNames = resultSet.columnNames console.log("书籍表可用列名:", columnNames) let bookList: string[] = [] while (resultSet?.goToNextRow()) { let book = '' book = resultSet.getString(resultSet.getColumnIndex('name')) bookList.push(book) } this.showBookList = bookList console.log(`书籍数据已更新,数量: ${this.showBookList.length}`) } catch (error) { console.error("获取书籍数据失败:", error) this.errorMessage = `查询失败: ${JSON.stringify(error)}` this.showBookList = [] } finally { this.isLoading = false } } build() { Column() { if (this.isLoading) { // 加载状态 Column() { Text("加载中...") .fontSize(16) .fontColor(Color.Gray) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } else if (this.errorMessage) { // 错误状态 Column() { Text(this.errorMessage) .fontSize(16) .fontColor(Color.Red) if (this.dbInitSuccess) { Button("重试") .onClick(() => { this.errorMessage = '' this.getBookCategories() this.getAllBooks() }) .margin({ top: 20 }) } } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } else if (this.bookCategories.length === 0) { // 无分类数据 Column() { Text("暂无分类数据") .fontSize(16) .fontColor(Color.Gray) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } else { List() { ForEach(this.bookCategories, (bookCategory: BookCategoriesDao, index: number) => { ListItemGroup({ header: this.bookListBuilder(bookCategory, index) }) { if (this.num === index) { if (this.showBookList.length === 0) { // 没有书籍数据 ListItem() { Text("暂无书籍数据") .fontSize(14) .fontColor(Color.Gray) } .height(60) } else { // 显示书籍列表 ForEach(this.showBookList, (bookName: string) => { ListItem() { Text('* ' + bookName) .fontSize('15fp') .width('100%') .textAlign(TextAlign.Start) .onClick(()=>{ let resultSet = this.store?.querySqlSync(`select id from voc_books where name = ?`, [bookName]) if (resultSet && resultSet.rowCount > 0){ resultSet.goToFirstRow() this.selectedBookId = resultSet.getLong(resultSet.getColumnIndex('id')) } this.selectedBookName = bookName console.log("准备跳转到WordSelectPage") this.navPathStack.pushPathByName("WordSelectPage", null) console.log("跳转完成") }) } .height(60) .padding({ left: 16 }) }) } } } }) } .height('100%') .width('100%') .sticky(StickyStyle.Header) } } .width('100%') .height('100%') .backgroundColor(Color.Grey) } @Builder bookListBuilder(bookCategory: BookCategoriesDao, index: number) { Column() { Text(bookCategory.category_name) .fontSize('20fp') .fontWeight(FontWeight.Bold) .backgroundColor(Color.White) .width('100%') .textAlign(TextAlign.Start) Row(){ } .width('98%') .backgroundColor(Color.Blue) .height(3) } .onClick(async () => { console.log(`点击分类: ${bookCategory.category_name}, index: ${index}`) if (this.num === index) { // 折叠当前分类 this.num = -1 this.showBookList = [] console.log("收起分类") } else { // 展开新分类 this.num = index this.showBookList = [] // 清空旧数据 console.log(`加载分类ID: ${bookCategory.id} 的书籍`) // 等待书籍数据加载完成后再展开 await this.getBookList(bookCategory.category_name) } }) .width('100%') } } ``` ![image-20250618093655995](README.assets/image-20250618093655995.png) 点击书籍跳转到选词页面,同时更新当前正在背诵数据以及统计信息 #### /user/voc/book/chapter/display 接口 ![image-20250618094929108](README.assets/image-20250618094929108.png) ![image-20250618094942448](README.assets/image-20250618094942448.png) ![image-20250618095010017](README.assets/image-20250618095010017.png) ##### src/main/ets/api/vo/request/BookChapterDisplayRequestVo.ets ``` export class BookChapterDisplayRequestVo{ msg:string = '' code:number = 0 bookChapterDisplay?:RequestJsonData } export class RequestJsonData{ pageList:WordsChapter[] = [] totalCount:number = 0 } export class WordsChapter{ chapterName:string = '' // Chapter 1 - Family Members displayVocs:WordInfo[] = [] id:number = 0 } export class WordInfo{ content:string = '' //father id:number = 0 interpretations?:WordInterpretations notRemember:number = 0 reviewed:number = 0 } export class WordInterpretations{ content:string = '' // 父亲 id:number = 0 type:string = '' //名词 } ``` ##### src/main/ets/api/vo/request/BookChapterDisplayRequestVo.ets ``` import { AxiosResponse } from "@ohos/axios" import axiosInstance from "./axios" import { RequestJsonData } from "./vo/request/BookChapterDisplayRequestVo" export const bookChapterDisplay = async (id:number)=> { let response:AxiosResponse = await axiosInstance.get(`/user/voc/book/chapter/display`) if (response.status === 200) { return response.data.data } return new RequestJsonData() } ``` #### 选词页面 点击加入记忆,增加选中单词至当天背诵单词的列表,返回上一页 ##### src/main/ets/pages/SecondComponent/WordSelectPage.ets ``` import { bookChapterDisplay } from "../../api/bookChapterDisplay" import { reviewVoc } from "../../api/review" import { RequestJsonData, WordInfo, WordsChapter } from "../../api/vo/request/BookChapterDisplayRequestVo" import { CommonResponseVo } from "../../api/vo/response/CommonResponseVo" @Component export struct WordSelectPage { @Consume selectedBookName: string @Consume @Watch('onselectBookChange') selectedBookId: number @Consume navPathStack: NavPathStack @State requestJsonData: RequestJsonData = new RequestJsonData() @State chapter: string = '' @State selectedWordString: string[] = [] @State WordList: number[] = [] @State num: number = -1 @Consume selectedWordList: number[] @Consume finished: boolean aboutToAppear(): void { AlertDialog.show({ message: `选择了:${this.selectedBookName}` }) console.log("WordSelectPage aboutToAppear"); console.log(`selectedBookId: ${this.selectedBookId}, selectedBookName: ${this.selectedBookName}`); this.onselectBookChange() } async onselectBookChange() { console.log("开始请求数据,bookId:", this.selectedBookId) try { let response = await bookChapterDisplay(this.selectedBookId) as RequestJsonData if (response) { this.requestJsonData = response } console.log("收到响应数据:", JSON.stringify(response)) } catch (error) { console.error("请求出错:", error) } } build() { NavDestination() { Column() { List() { ForEach(this.requestJsonData?.pageList, (wordChapter: WordsChapter, index: number) => { ListItemGroup({ header: this.wordListBuilder(wordChapter, index) }) { if (this.num === index) { if (wordChapter.displayVocs.length === 0) { // 没有书籍数据 ListItem() { Text("暂无单词数据") .fontSize(14) .fontColor(Color.Gray) } .height(60) } else { ForEach(wordChapter.displayVocs, (wordinfo: WordInfo) => { ListItem() { Column() { Row() { Row() { Toggle({ type: ToggleType.Checkbox, isOn: false }) .width('100%') .onChange((isChecked: boolean) => { if (isChecked) { this.selectedWordString.push(wordinfo.content) this.WordList.push(wordinfo.id) } else { this.selectedWordString.filter(name => name !== wordinfo.content) this.WordList.filter(id => id !== wordinfo.id) } }) } .justifyContent(FlexAlign.Start) .width('5%') Row() { Text(`${wordinfo.content}`) .width('50%') } .justifyContent(FlexAlign.Start) .width('60%') Row() { Image($r('app.media.right_grey')) .width('3%') .aspectRatio(1) } .justifyContent(FlexAlign.End) .margin({ right: 10 }) } .justifyContent(FlexAlign.SpaceBetween) .width('100%') Row() .backgroundColor('#ff515050') .height(2) .width('96%') } .width('100%') } }) } } } }) } .width('100%') .height('90%') .sticky(StickyStyle.Header) Row() { Button('加入记忆') .onClick(async () => { this.selectedWordList = this.WordList this.finished = false let response = await reviewVoc(this.selectedWordList) as CommonResponseVo this.navPathStack.pop() }) } .justifyContent(FlexAlign.Center) } .width('100%') .height('100%') .backgroundColor('#fffdfcfc') } .title(`${this.selectedBookName}`) .width('100%') .height('100%') } @Builder wordListBuilder(wordChapter: WordsChapter, index: number) { Column() { Text(wordChapter.chapterName) .fontSize('20fp') .fontWeight(FontWeight.Bold) .backgroundColor(Color.White) .width('100%') .textAlign(TextAlign.Start) .height('5%') } .onClick(async () => { if (this.num === index) { // 折叠当前分类 this.num = -1 console.log("收起分类") } else { // 展开新分类 this.num = index } }) .width('100%') } } ``` ![image-20250618100618793](README.assets/image-20250618100618793.png) ##### src/main/ets/pages/SecondPage.ets ``` import { BookCategoriesDao } from "../model/dao/BookCategoriesDao" import { BookDao } from "../model/dao/BookDao" import { FirstStatics } from "./SecondComponent/FirstStatisitcs" import { SecondSelectedPart } from "./SecondComponent/SecondSelectedPart" import { ThirdBooksList } from "./SecondComponent/ThirdBooksList" @Component export struct SecondPage{ @Consume navPathStack: NavPathStack @Consume selectedBookId:number @Consume selectedBookName:string @Provide bookCategories: BookCategoriesDao[] = [] @Provide allBooks:BookDao[] = [] build() { NavDestination(){ Column(){ FirstStatics() .height('30%') Row() .backgroundColor(Color.Blue) .width('100%') .height('5vp') SecondSelectedPart() .height('20%') ThirdBooksList() .height('50%') } .width('100%') .height('100%') } .width('100%') .height('100%') } } ``` 添加的单词会同步到数组中,在后续首页界面展示 ## 六、首页界面 ### src/main/ets/pages/FirstPage.ets ``` import { DefaultPage } from "./FirstComponent/DefaultPage" import { ReciteHidePage } from "./FirstComponent/ReciteHidePage" @Component export struct FirstPage{ @Consume navPathStack: NavPathStack @Consume selectedWordList: number[] @Consume finished:boolean @Provide vocIndex:number = 0 @Provide hide:boolean = true aboutToAppear(): void { } build() { NavDestination(){ if(this.finished){ DefaultPage() }else{ ReciteHidePage() } } .width('100%') .height('100%') } } ``` 单词信息来自于选词中的词和每日复习的词,通过id数组从服务器查询获得 ### /user/voc/detail/{vocId} 接口 ![image-20250618102258097](README.assets/image-20250618102258097.png) ![image-20250618102315268](README.assets/image-20250618102315268.png) ![image-20250618102335921](README.assets/image-20250618102335921.png) #### src/main/ets/api/userVocDetail.ets ``` import { AxiosResponse } from "@ohos/axios" import axiosInstance from "./axios" import { UserVocDetail } from "./vo/request/UserVocDetailRequestVo" export const userVocDetail = async (vocId:number)=> { let response:AxiosResponse = await axiosInstance.get(`/user/voc/detail/${vocId}`) if (response.status === 200) { console.info('get 成功') return response.data.data } console.info(`${response.status}`) return new UserVocDetail() } ``` #### src/main/ets/api/vo/request/UserVocDetailRequestVo.ets ``` export class UserVocDetailRequestVo{ msg:string = '' code:number = 0 userVocDetail?:UserVocDetail } export class UserVocDetail{ content:string = '' derivedId:string = '' examples:ExampleInfo[] = [] id:number = 0 interpretations:Interpretations[] = [] notRemember:number = 0 reviewed:number = 0 ukPhonetic:string = '' ukPronounciation:string = '' usPhonetic:string = '' usPronounciation:string = '' userExamples:UserExamples[] = [] userInterpretations:UserInterpretations[] = [] } export class ExampleInfo{ content:string = '' id:number = 0 meaning:string = '' vocId:string = '' } export class Interpretations{ id:string = '' interContent:string = '' type:string = '' vocId:string = '' } export class UserExamples{ id:number = 0 meaning:string = '' sentence:string = '' userId:number = 0 vocContent:string = '' vocId:number = 0 } export class UserInterpretations{ id:number = 0 meaning:string = '' type:string = '' } ``` #### src/main/ets/pages/FirstComponent/ReciteHidePage.ets ``` import { userVocDetail } from "../../api/userVocDetail" import { ExampleInfo, Interpretations, UserVocDetail } from "../../api/vo/request/UserVocDetailRequestVo" import { UserRevStatisticsDao } from "../../model/dao/UserRevStatisticsDao" import { UserVocRevDao } from "../../model/dao/UserVocRevDao" import { getCurrentDateString, newWordDeal } from "../../util/newWordDeal" import { userRevStatisticsDeal } from "../../util/userRevStatisticsDeal" import { relationalStore, ValuesBucket } from "@kit.ArkData" import { RELATIONAL_STORE_NAME } from "../../util/databaseInit" import { reviewWordsDeal } from "../../util/reviewWordsDeal" @Component export struct ReciteHidePage { store?: relationalStore.RdbStore @State vocId: number = 0 @State isLoading: boolean = true @Consume vocIndex: number @Consume selectedWordList: number[] @State userVocDetail: UserVocDetail = new UserVocDetail() @State showExamples: boolean = true @Consume hide: boolean @Consume finished: boolean @State userVocRev: UserVocRevDao = new UserVocRevDao() @State userRevStat: UserRevStatisticsDao = new UserRevStatisticsDao() @State reviewVocArray:UserVocRevDao[] = [] @State isNew:boolean = true async aboutToAppear() { this.onselectWordChange() console.log("开始加载数据库...") try { const config = { name: RELATIONAL_STORE_NAME, securityLevel: relationalStore.SecurityLevel.S1 } as relationalStore.StoreConfig this.store = await relationalStore.getRdbStore(getContext(), config) console.log("数据库连接成功") // this.reviewTest() } catch (error) { console.error("数据库初始化失败:", JSON.stringify(error)) } } async onselectWordChange() { this.isLoading = true try { this.vocId = this.selectedWordList[this.vocIndex] let response = await userVocDetail(this.vocId) as UserVocDetail if (response) { this.userVocDetail = response } console.log("收到响应数据:", JSON.stringify(response)) } catch (error) { console.error("请求出错:", error) } } async clickButtonDeal(familiarity: number) { await this.store?.beginTransaction() if (this.vocIndex < this.selectedWordList.length - 1 + this.reviewVocArray.length) { this.vocIndex++ this.hide = true if(this.reviewVocArray.length === 0){ await this.onselectWordChange() this.userVocRev = newWordDeal(this.userVocRev, familiarity, this.userVocDetail) await this.store?.insert('user_voc_rev', { voc_id: this.userVocRev.voc_id, // 单词id user_id: this.userVocRev.user_id, // 用户id factor: this.userVocRev.factor, // 计算因子 review_interval: this.userVocRev.review_interval, // 本次计算的下次复习间隔(天为单位) next_review_date: this.userVocRev.next_review_date, // 下次的复习天数 type: this.userVocRev.type, // 0 还未学习 1. 正常复习 2. 暂停复习 familiarity: this.userVocRev.familiarity, // 0.不认识 1.模糊 2. 认识 3.简单 consecutive_fail_count: this.userVocRev.consecutive_fail_count, // 连续失败次数,连续失败三次清零 total_review: this.userVocRev.total_review, // 总的学习次数 current_count: this.userVocRev.current_count // 当前复习次数 } as ValuesBucket) } else{ this.userVocRev = reviewWordsDeal(this.reviewVocArray[this.vocIndex-this.selectedWordList.length],familiarity) let values = { familiarity:this.userVocRev.familiarity, consecutive_fail_count:this.userVocRev.consecutive_fail_count, factor:this.userVocRev.factor, total_review:this.userVocRev.total_review, current_count:this.userVocRev.current_count, review_interval:this.userVocRev.review_interval, next_review_date:this.userVocRev.next_review_date, usn:this.userVocRev.usn }as ValuesBucket let predicates = new relationalStore.RdbPredicates('user_voc_rev') predicates.equalTo('voc_id',this.userVocRev.voc_id) await this.store?.update(values,predicates) } this.userRevStat = userRevStatisticsDeal(this.userRevStat, this.isNew, this.userVocRev) } else { if(this.reviewVocArray.length === 0){ this.isNew = false let date = getCurrentDateString() let resultSet = await this.store?.querySql('SELECT voc_id,user_id,factor,review_interval,next_review_date,usn,type,familiarity,consecutive_fail_count,total_review,current_count FROM user_voc_rev WHERE next_review_date=?',[date]) this.reviewVocArray = [] while(resultSet?.goToNextRow()){ let uservoc = new UserVocRevDao() uservoc.voc_id = resultSet.getLong(resultSet.getColumnIndex('voc_id')) uservoc.user_id = resultSet.getLong(resultSet.getColumnIndex('user_id')) uservoc.factor = resultSet.getLong(resultSet.getColumnIndex('factor')) uservoc.review_interval = resultSet.getLong(resultSet.getColumnIndex('review_interval')) uservoc.next_review_date = resultSet.getString(resultSet.getColumnIndex('next_review_date')) uservoc.usn = resultSet.getLong(resultSet.getColumnIndex('usn')) uservoc.type = resultSet.getLong(resultSet.getColumnIndex('type')) uservoc.familiarity = resultSet.getLong(resultSet.getColumnIndex('familiarity')) uservoc.consecutive_fail_count = resultSet.getLong(resultSet.getColumnIndex('consecutive_fail_count')) uservoc.total_review = resultSet.getLong(resultSet.getColumnIndex('total_review')) uservoc.current_count = resultSet.getLong(resultSet.getColumnIndex('current_count')) this.reviewVocArray.push(uservoc) } if(this.reviewVocArray.length === 0) { await this.store?.insert('user_rev_statistics', { user_id: this.userRevStat.user_id, // 用户id voc_new_count: this.userRevStat.voc_new_count, // 新单词数量 voc_review_count: this.userRevStat.voc_review_count, // 复习单词数量 total_count: this.userRevStat.total_count, // 总数量 forget_count: this.userRevStat.forget_count, // 忘记/不认识数量 uncertain_count: this.userRevStat.uncertain_count, // 模糊的数量 review_date: this.userRevStat.review_date, // 回顾的日期 usn: this.userRevStat.usn, // study_time: this.userRevStat.study_time, // 学习时间 known_count: this.userRevStat.known_count, // 已知计数 simple_count: this.userRevStat.simple_count // 简单计数 } as ValuesBucket) this.finished = true this.vocIndex = 0 } } else { this.vocIndex = 0 await this.store?.insert('user_rev_statistics', { user_id: this.userRevStat.user_id, // 用户id voc_new_count: this.userRevStat.voc_new_count, // 新单词数量 voc_review_count: this.userRevStat.voc_review_count, // 复习单词数量 total_count: this.userRevStat.total_count, // 总数量 forget_count: this.userRevStat.forget_count, // 忘记/不认识数量 uncertain_count: this.userRevStat.uncertain_count, // 模糊的数量 review_date: this.userRevStat.review_date, // 回顾的日期 usn: this.userRevStat.usn, // study_time: this.userRevStat.study_time, // 学习时间 known_count: this.userRevStat.known_count, // 已知计数 simple_count: this.userRevStat.simple_count // 简单计数 } as ValuesBucket) this.reviewVocArray = [] this.finished = true } } await this.store?.commit(); } async reviewTest() { let values = [ { // current_count > 1 且 consecutive_fail_count != 3 Previous_Interval * newFactor 10 day(简单) voc_id: 5, // 单词id user_id: 1, // 用户id factor: 1, // 计算因子 review_interval: 10, // 本次计算的下次复习间隔(天为单位) next_review_date: '2025-06-16', // 下次的复习天数 type: 1, // 0 还未学习 1. 正常复习 2. 暂停复习 familiarity: 0, // 0.不认识 1.模糊 2. 认识 3.简单 consecutive_fail_count: 1, // 连续失败次数,连续失败三次清零 total_review: 6, // 总的学习次数 current_count: 10 // 当前复习次数 }, { // current_count > 1 且 consecutive_fail_count != 3 Previous_Interval * newFactor 24day(简单) voc_id: 6, // 单词id user_id: 1, // 用户id factor: 1.2, // 计算因子 review_interval: 20, // 本次计算的下次复习间隔(天为单位) next_review_date: '2025-06-16', // 下次的复习天数 type: 1, // 0 还未学习 1. 正常复习 2. 暂停复习 familiarity: 0, // 0.不认识 1.模糊 2. 认识 3.简单 consecutive_fail_count: 1, // 连续失败次数,连续失败三次清零 total_review: 6, // 总的学习次数 current_count: 10 // 当前复习次数 }, { // current_count = 1 且 consecutive_fail_count != 3 6 day voc_id: 7, // 单词id user_id: 1, // 用户id factor: 1, // 计算因子 review_interval: 5, // 本次计算的下次复习间隔(天为单位) next_review_date: '2025-06-16', // 下次的复习天数 type: 1, // 0 还未学习 1. 正常复习 2. 暂停复习 familiarity: 0, // 0.不认识 1.模糊 2. 认识 3.简单 consecutive_fail_count: 1, // 连续失败次数,连续失败三次清零 total_review: 6, // 总的学习次数 current_count: 0 // 当前复习次数 }, { // current_count = 1 且 consecutive_fail_count === 3 1 day(忘记) voc_id: 8, // 单词id user_id: 1, // 用户id factor: 1, // 计算因子 review_interval: 6, // 本次计算的下次复习间隔(天为单位) next_review_date: '2025-06-16', // 下次的复习天数 type: 1, // 0 还未学习 1. 正常复习 2. 暂停复习 familiarity: 0, // 0.不认识 1.模糊 2. 认识 3.简单 consecutive_fail_count: 2, // 连续失败次数,连续失败三次清零 total_review: 6, // 总的学习次数 current_count: 3 // 当前复习次数 }, ] as ValuesBucket[] await this.store?.batchInsert('user_voc_rev', values) } build() { Column() { Column() { Row() { Image($r('app.media.other')) .width('8%') .aspectRatio(1) .fillColor(Color.White) } .width('100%') .justifyContent(FlexAlign.End) .margin(5) Row() { Text(`${this.userVocDetail.content}`) .fontSize('40fp') .fontColor(Color.White) } .width('100%') .justifyContent(FlexAlign.Center) .margin({ bottom: 10 }) Row() { Text(`美 [${this.userVocDetail.usPhonetic}]`) .fontSize('20fp') .fontColor(Color.White) Image($r('app.media.America')) .width('10%') .aspectRatio(1) } .width('100%') .justifyContent(FlexAlign.Center) .margin({ top: 5, bottom: 5 }) Row() { Text(`英 [${this.userVocDetail.ukPhonetic}]`) .fontSize('20fp') .fontColor(Color.White) Image($r('app.media.england')) .width('10%') .aspectRatio(1) } .width('100%') .justifyContent(FlexAlign.Center) .margin({ top: 5, bottom: 5 }) } .width('100%') .height('30%') .backgroundColor(Color.Blue) if (this.hide) { Column() { Text('请回忆单词发音和释义') Text('点击屏幕显示答案') } .width('100%') .height('70%') .justifyContent(FlexAlign.Center) .onClick(() => { this.hide = false }) } else { Column() { Column() { List() { ForEach(this.userVocDetail.interpretations, (item: Interpretations) => { ListItem() { Row() { Text(`[${item.type}] ${item.interContent}`) .fontSize('20fp') .fontColor(Color.White) .margin({ bottom: 5 }) Image($r('app.media.other')) .width('8%') .aspectRatio(1) .fillColor(Color.White) .margin({ bottom: 5 }) } .width('100%') .backgroundColor(Color.Blue) .justifyContent(FlexAlign.SpaceBetween) } .width('100%') }) } .width('100%') List() { ListItemGroup({ header: this.sentenceExampleBuilder() }) { if (this.showExamples) { ForEach(this.userVocDetail.examples, (item: ExampleInfo) => { ListItem() { Row() { Text(`${item.content}`) .fontSize('15fp') .margin({ left: 5 }) Image($r('app.media.other')) .width('8%') .aspectRatio(1) } .width('100%') .justifyContent(FlexAlign.SpaceBetween) } .width('100%') }) } } .width('100%') } .width('100%') } .width('100%') .height('45%') Row() { Text('词典') .fontSize('20fp') .width('15%') Image($r('app.media.other')) .width('8%') .aspectRatio(1) .margin({ left: 5 }) } .width('100%') .height('5%') .justifyContent(FlexAlign.SpaceBetween) Row() .width('100%') .height(2) .backgroundColor(Color.Grey) Row() { Button('简单') .width('20%') .backgroundColor(Color.Green) .borderRadius(5) .aspectRatio(2) .onClick(() => { this.clickButtonDeal(3) }) Button('认识') .width('20%') .backgroundColor(Color.Pink) .borderRadius(5) .aspectRatio(2) .onClick(() => { this.clickButtonDeal(2) }) Button('模糊') .width('20%') .backgroundColor(Color.Orange) .borderRadius(5) .aspectRatio(2) .onClick(() => { this.clickButtonDeal(1) }) Button('忘记') .width('20%') .backgroundColor(Color.Red) .borderRadius(5) .aspectRatio(2) .onClick(() => { this.clickButtonDeal(0) }) } .width('100%') .height('20%') .justifyContent(FlexAlign.SpaceAround) } } } .width('100%') .height('100%') } @Builder sentenceExampleBuilder() { Column() { Row() { Row() { Text('例句') .fontSize('15fp') } .width('10%') .justifyContent(FlexAlign.Center) Row() { Text('A 中') .fontSize('15fp') Image($r(this.showExamples ? 'app.media.right' : 'app.media.down')) .width('46%') .aspectRatio(1) .margin({ left: 5 }) } .width('18%') } .width('100%') .justifyContent(FlexAlign.SpaceBetween) } .width('100%') .margin({ bottom: 5, top: 5 }) .onClick(async () => { this.showExamples = !this.showExamples }) .width('100%') } } ``` 点击屏幕空白,会显示单词信息 image-20250618103404178 点击按钮,根据算法生成背诵记录,将记录保存在数据库的表中,同时跳转到下一个背诵单词 #### 单词信息类 ##### src/main/ets/model/dao/UserVocRevDao.ets ``` export class UserVocRevDao{ // id:number = 0 // 表项id voc_id:number = 0 // 单词id user_id:number = 0 // 用户id factor:number = 0 // 计算因子 review_interval:number = 0 // 本次计算的下次复习间隔(天为单位) next_review_date:string = '' // 下次的复习天数 usn:number = 0 // DEFAULT type:number = 0 // 0 还未学习 1. 正常复习 2. 暂停复习 // create_time:string = '' // 创建时间 DEFAULT // updated_time:string = '' // 更新时间 DEFAULT // is_deleted:number = 0 // 是否被删除 DEFAULT familiarity:number = 0 // 0.不认识 1.模糊 2. 认识 3.简单 consecutive_fail_count:number = 0 // 连续失败次数,连续失败三次清零 total_review:number = 0 // 总的学习次数 current_count:number = 0 // 当前复习次数 } ``` #### 当日学习统计类 ``` export class UserRevStatisticsDao{ // id:number = 0 // 表项id user_id:number = 0 // 用户id voc_new_count:number = 0 // 新单词数量 voc_review_count:number = 0 // 复习单词数量 total_count:number = 0 // 总数量 forget_count:number = 0 // 忘记/不认识数量 uncertain_count:number = 0 // 模糊的数量 review_date:string = '' // 回顾的日期 usn:number = 0 // study_time:number = 0 // 学习时间 // create_time:string = '' // 创建时间 DEFAULT // updated_time:string = '' // 更新时间 DEFAULT // is_deleted:number = 0 // 是否删除 known_count:number = 0 // 已知计数 simple_count:number = 0 // 简单计数 } ``` #### 计算数据库内容的相关参数 ##### src/main/ets/util/Calculate.ets ``` export function factorCalculate(familiarity: number, pre_factor: number, threeTimes: boolean) { if (threeTimes) { return 1.3 } else { return pre_factor + (0.1 - (3 - familiarity) * (0.08 + (3 - familiarity) * 0.02)) } } export function reviewIntervalCalculate(current_count: number, threeTimes: boolean, review_interval: number, factor: number) { if (threeTimes) { return 1 } else { if (current_count === 0) { return 1 } else if (current_count === 1) { return 6 } else { return review_interval * factor } } } export function addDaysToDateString(dateStr: string, daysToAdd: number): string { const date = new Date(dateStr) if (isNaN(date.getTime())) { throw new Error('Invalid date format. Expected format: YYYY-MM-DD HH:MM:SS'); } date.setDate(date.getDate() + daysToAdd) const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` } ``` #### 新单词处理 ##### src/main/ets/util/newWordDeal.ets ``` import { UserVocDetail } from "../api/vo/request/UserVocDetailRequestVo"; import { UserVocRevDao } from "../model/dao/UserVocRevDao"; import { factorCalculate, reviewIntervalCalculate } from "./Calculate"; export function newWordDeal(userVocRev: UserVocRevDao, familiarity: number, userVocDetail: UserVocDetail) : UserVocRevDao{ let threeTimes: boolean = true let consecutive_fail_count = (familiarity === 0) ? 1 : 0 let factor = factorCalculate(familiarity, 0, threeTimes) let reviewInterval = reviewIntervalCalculate(0, threeTimes, userVocRev.review_interval, factor) return { voc_id: userVocDetail.id, user_id: 0, type: 0, familiarity:familiarity, consecutive_fail_count: consecutive_fail_count, factor: factor, total_review:1, current_count: 0, review_interval:reviewInterval, next_review_date:addDaysFormatted(reviewInterval), usn: 0 } } export function getCurrentDateString(): string { const date = new Date(); const year = date.getFullYear(); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0'); return `${year}-${month}-${day}`; } export function addDaysFormatted(days: number): string { const date = new Date(); date.setDate(date.getDate() + days); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } ``` #### 复习单词处理 ##### src/main/ets/util/reviewWordsDeal.ets ``` import { UserVocRevDao } from "../model/dao/UserVocRevDao"; import { factorCalculate, reviewIntervalCalculate } from "./Calculate"; import { addDaysFormatted } from "./newWordDeal"; export function reviewWordsDeal(userVocRevDao:UserVocRevDao,familiarity:number):UserVocRevDao{ let reset:boolean = false if(familiarity === 0 && userVocRevDao.consecutive_fail_count === 2){ reset = true } let consecutive_fail_count = reset ? 0 : ((familiarity === 0) ? userVocRevDao.consecutive_fail_count + 1 : userVocRevDao.consecutive_fail_count) let factor = factorCalculate(familiarity, userVocRevDao.factor, reset) let total_review = userVocRevDao.total_review + 1 let current_count = userVocRevDao.current_count + 1 let reviewInterval = reviewIntervalCalculate(current_count,reset,userVocRevDao.review_interval,factor) return { voc_id: userVocRevDao.voc_id, user_id: userVocRevDao.user_id, type: userVocRevDao.type, familiarity:familiarity, consecutive_fail_count: consecutive_fail_count, factor: factor, total_review:total_review, current_count: current_count, review_interval:reviewInterval, next_review_date:addDaysFormatted(reviewInterval), usn: 1 } } ``` #### 当日单词统计 ##### src/main/ets/util/userRevStatisticsDeal.ets ``` import { UserRevStatisticsDao } from "../model/dao/UserRevStatisticsDao"; import { UserVocRevDao } from "../model/dao/UserVocRevDao"; export function userRevStatisticsDeal(userRevStat:UserRevStatisticsDao,isNew:boolean,userVocRev: UserVocRevDao):UserRevStatisticsDao{ return { user_id: 0, voc_new_count:isNew ? (userRevStat.voc_new_count+1) : userRevStat.voc_new_count, voc_review_count:isNew ? userRevStat.voc_review_count : userRevStat.voc_review_count+1, total_count:(userRevStat.total_count+1), forget_count:(userVocRev.familiarity === 0) ? (userRevStat.forget_count+1) : userRevStat.forget_count, uncertain_count:(userVocRev.familiarity === 1) ? (userRevStat.uncertain_count+1) : userRevStat.uncertain_count, review_date:userVocRev.next_review_date, usn:userVocRev.usn, study_time:userVocRev.review_interval, known_count:(userVocRev.familiarity === 2) ? (userRevStat.known_count+1) : userRevStat.known_count, simple_count:(userVocRev.familiarity === 3) ? (userRevStat.simple_count+1) : userRevStat.simple_count } } ``` #### 全部背诵完显示默认首页 ##### src/main/ets/pages/FirstComponent/DefaultPage.ets ``` @Component export struct DefaultPage{ build() { Column(){ Row(){ Text('您已完成全部单词背诵') .fontSize('30fp') } .justifyContent(FlexAlign.Center) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } } ``` image-20250618104900687 ------ ## 七、我的界面 获取用户信息,进度显示进度表百分比 ### /user/info 接口 ![image-20250618105201453](README.assets/image-20250618105201453.png) ![image-20250618105219791](README.assets/image-20250618105219791.png) ![image-20250618105243157](README.assets/image-20250618105243157.png) #### src/main/ets/api/vo/request/UserInfoRequestVo.ets ``` export class UserInfoRequestVo{ msg:string = '' code:number = 0 userInfoRequestVo:UserInfo = new UserInfo() } export class UserInfo{ birthday:string = '' email:string = '' hasSignCard:number = 0 headImg:string = '' id:number = 0 levelDate:string = '' levelName:string = '' levelValue:number = 0 nickName:string = '' phoneNumber:string = '' privilegeCodes:string[] = [] statement:string = '' thirdPartyName:string = '' vocCountOfDay:number = 0 } ``` #### src/main/ets/api/userInfo.ets ``` import { AxiosResponse } from "@ohos/axios" import axiosInstance from "./axios" import { UserInfoRequestVo } from "./vo/request/UserInfoRequestVo" export const userInfo = async ()=> { let response:AxiosResponse = await axiosInstance.get(`/user/info`) if (response.status === 200) { return response.data.data } return new UserInfoRequestVo() } ``` ### /user/voc/my/page 接口 ![image-20250618105541988](README.assets/image-20250618105541988.png) ![image-20250618105606782](README.assets/image-20250618105606782.png) ![image-20250618105620708](README.assets/image-20250618105620708.png) #### src/main/ets/api/vo/request/UserVocMyPageRequestVo.ets ``` export class UserVocMyPageRequestVo{ msg:string = '' code : number = 0 userVoc:UserVoc = new UserVoc() } export class UserVoc{ freeVocBound:number = 0 payVocBound:number = 0 reSignCard:number = 0 remindTime:string = '' totalVocBound:number = 0 vocCountOfDay:number = 0 } ``` #### src/main/ets/api/userVocMyPage.ets ``` import { AxiosResponse } from "@ohos/axios" import axiosInstance from "./axios" import { UserVocMyPageRequestVo } from "./vo/request/UserVocMyPageRequestVo" export const useVocMyPage = async ()=> { let response:AxiosResponse = await axiosInstance.get(`/user/voc/my/page`) if (response.status === 200) { return response.data.data } return new UserVocMyPageRequestVo() } ``` ### 第一部分 #### src/main/ets/pages/FourthComponent/FirstPartIcons.ets ``` @Component export struct FirstPartIcons{ build() { Row(){ Image($r('app.media.class')) .width('8%') .aspectRatio(1) Blank() Image($r('app.media.message_wd')) .width('8%') .aspectRatio(1) } .width('100%') .height('100%') } } ``` ### 第二部分 #### src/main/ets/pages/FourthComponent/SecondPartUserInfo.ets ``` import { userInfo } from "../../api/userInfo" import { UserInfo } from "../../api/vo/request/UserInfoRequestVo" @Component export struct SecondPartUserInfo{ @State userinfo:UserInfo = new UserInfo() aboutToAppear(): void { this.onselectBookChange() } async onselectBookChange() { try { let response = await userInfo() as UserInfo if (response) { this.userinfo = response }else { this.userinfo = new UserInfo() } console.log("收到响应数据:", JSON.stringify(response)) }catch (error) { console.error("请求出错:", error) } } build() { Row(){ Row(){ Image($r('app.media.ruike')) .width('80%') .aspectRatio(1) } .width('25%') Column(){ Row(){ Text(`${this.userinfo.nickName}`) .fontSize('30fp') .textAlign(TextAlign.Start) } .width('100%') Row(){ Row(){ Row(){ Text(` LVL ${this.userinfo.levelName}`) .fontSize('13fp') .fontColor(Color.White) .textAlign(TextAlign.Center) } .width('90%') } .width('35%') .aspectRatio(3) .backgroundColor(Color.Blue) .borderRadius(5) Blank() Column(){ Text(`再签到 ${this.userinfo.levelDate} 天升级`) .fontSize('13fp') } .width('50%') .margin({right:5}) } .width('100%') Row(){ Row() .width('30%') .backgroundColor(Color.Blue) .height(2) Row() .width('70%') .backgroundColor(Color.Grey) .height(2) } .width('100%') .margin({ top: 8 }) } .width('75%') .padding(10) } .width('100%') .height('100%') .border({ width: 1, color: '#EBEEF5' }) // 调试边框 } } ``` ### 第三部分 #### /user/checkin/statistics 接口 ![image-20250618115637223](README.assets/image-20250618115637223.png) ![image-20250618115656527](README.assets/image-20250618115656527.png) ![image-20250618115717262](README.assets/image-20250618115717262.png) ##### src/main/ets/api/vo/request/UserCheckinStatisticsRequestVo.ets ``` export class UserCheckinStatisticsRequestVo{ msg:string = '' code:number = 0 userCheckinStatis:UserCheckinStatistics = new UserCheckinStatistics() } export class UserCheckinStatistics{ curContinuousDays:number = 0 curContinuousStartDate:string = '' id:number = 0 maxContinuousDays:number = 0 maxContinuousStartDate:string = '' totalDays:number = 0 totalTime:number = 0 userId:number = 0 usn:number = 0 } ``` ##### src/main/ets/api/checkin.ets ``` import axiosInstance from "./axios" import { AxiosResponse } from '@ohos/axios' export const checkinStatistics = async ()=>{ let response:AxiosResponse = await axiosInstance.get('/user/checkin/statistics') return response.data.data } ``` ##### src/main/ets/pages/FourthComponent/ThirdPartCheckinStatistics.ets ``` import { checkinStatistics } from "../../api/checkin" import { UserCheckinStatistics } from "../../api/vo/request/UserCheckinStatisticsRequestVo" @Component export struct ThirdPartCheckinStatistics{ @State userCheckinStatistics:UserCheckinStatistics = new UserCheckinStatistics() aboutToAppear(): void { this.onselectBookChange() } async onselectBookChange() { try { let response = await checkinStatistics() as UserCheckinStatistics if (response) { this.userCheckinStatistics = response } console.log("收到响应数据:", JSON.stringify(response)) }catch (error) { console.error("请求出错:", error) } } build() { Row(){ Column(){ Text(`${this.userCheckinStatistics.curContinuousDays}`) .fontSize('15fp') .textAlign(TextAlign.Center) Text('连续签到') .fontSize('10fp') .textAlign(TextAlign.Center) } .width('20%') Row() .width(2) .height('90%') .backgroundColor(Color.White) Column(){ Text(`${this.userCheckinStatistics.maxContinuousDays}`) .fontSize('15fp') .textAlign(TextAlign.Center) Text('最大连续签到') .fontSize('10fp') .textAlign(TextAlign.Center) } .width('20%') Row() .width(2) .height('90%') .backgroundColor(Color.White) Column(){ Text(`${this.userCheckinStatistics.totalDays}`) .fontSize('15fp') .textAlign(TextAlign.Center) Text('累计签到') .fontSize('10fp') .textAlign(TextAlign.Center) } .width('20%') Row() .width(2) .height('90%') .backgroundColor(Color.White) Column(){ Text(`${this.userCheckinStatistics.totalTime}`) .fontSize('15fp') .textAlign(TextAlign.Center) Text('累计学习分钟') .fontSize('10fp') .textAlign(TextAlign.Center) } .width('20%') } .width('100%') .height('100%') .borderRadius(5) .justifyContent(FlexAlign.SpaceBetween) .backgroundColor('#aaaaaa') } } ``` ### 第四部分 #### src/main/ets/pages/FourthComponent/FourthPartUserVoc.ets ``` import { useVocMyPage } from "../../api/userVocMyPage" import { UserVoc } from "../../api/vo/request/UserVocMyPageRequestVo" import { NumDialog } from "./NumDialog" import { TimeDialog } from "./TimeDialog" @Component @Preview export struct FourthPartUserVoc{ @Consume payVocBound:number @Consume vocCountOfDay:number @Consume remindTime:string @Consume navPathStack: NavPathStack @State userVoc:UserVoc = new UserVoc() aboutToAppear(): void { this.onselectBookChange() } numDialogController: CustomDialogController = new CustomDialogController({ builder: NumDialog({}) }) timeDialogController: CustomDialogController = new CustomDialogController({ builder: TimeDialog({}) }) async onselectBookChange() { try { let response = await useVocMyPage() as UserVoc if (response) { this.userVoc = response this.payVocBound = this.userVoc.payVocBound this.vocCountOfDay = this.userVoc.vocCountOfDay this.remindTime = this.userVoc.remindTime } console.log("收到响应数据:", JSON.stringify(response)) }catch (error) { console.error("请求出错:", error) } } build() { Column(){ Row(){ Row(){ Text('每日学习量') .fontSize('15fp') } .justifyContent(FlexAlign.Start) .width('50%') .margin(10) Blank() Row(){ Row(){ Text(`${this.vocCountOfDay}`) .fontSize('15fp') .fontColor(Color.White) .textAlign(TextAlign.Center) } .justifyContent(FlexAlign.Center) .width('100%') } .width('15%') .aspectRatio(2) .backgroundColor(Color.Blue) .borderRadius(10) .margin(10) Row(){ Image($r('app.media.right_grey')) } .width('5%') .aspectRatio(1) .margin({right:10}) } .width('100%') .onClick(()=>{ this.numDialogController?.open() }) Row() .width('96%') .height(2) .backgroundColor(Color.Gray) Row(){ Row(){ Text('现单词上限总量') .fontSize('15fp') } .justifyContent(FlexAlign.Start) .width('50%') .margin(10) Blank() Row(){ Row(){ Text(`${this.userVoc.totalVocBound}`) .fontSize('15fp') .fontColor(Color.White) .textAlign(TextAlign.Center) } .justifyContent(FlexAlign.Center) .width('100%') } .width('15%') .aspectRatio(2) .backgroundColor(Color.Blue) .borderRadius(10) .margin(10) Row(){ Image($r('app.media.right_grey')) } .width('5%') .aspectRatio(1) .margin({right:10}) } .width('100%') Row() .width('96%') .height(2) .backgroundColor(Color.Gray) Row(){ Row(){ Text('免费获取单词上限') .fontSize('15fp') } .justifyContent(FlexAlign.Start) .width('50%') .margin(10) Blank() Row(){ Row(){ Text(`${this.userVoc.freeVocBound}`) .fontSize('15fp') .fontColor(Color.White) .textAlign(TextAlign.Center) } .justifyContent(FlexAlign.Center) .width('100%') } .width('15%') .aspectRatio(2) .backgroundColor(Color.Blue) .borderRadius(10) .margin(10) Row(){ Image($r('app.media.right_grey')) } .width('5%') .aspectRatio(1) .margin({right:10}) } .width('100%') Row() .width('96%') .height(2) .backgroundColor(Color.Gray) Row(){ Row(){ Text('购买单词上限') .fontSize('15fp') } .justifyContent(FlexAlign.Start) .width('50%') .margin(10) Blank() Row(){ Row(){ Text(`${this.userVoc.payVocBound}`) .fontSize('15fp') .fontColor(Color.White) .textAlign(TextAlign.Center) } .justifyContent(FlexAlign.Center) .width('100%') } .width('15%') .aspectRatio(2) .backgroundColor(Color.Blue) .borderRadius(10) .margin(10) Row(){ Image($r('app.media.right_grey')) } .width('5%') .aspectRatio(1) .margin({right:10}) } .width('100%') .onClick(()=>{ this.navPathStack.pushPathByName("ProductPage",null) }) Row() .width('96%') .height(2) .backgroundColor(Color.Gray) Row(){ Row(){ Text('学习提醒') .fontSize('15fp') } .justifyContent(FlexAlign.Start) .width('50%') .margin(10) Blank() Row(){ Row(){ Text(`${this.remindTime}`) .fontSize('15fp') .fontColor(Color.White) .textAlign(TextAlign.Center) } .justifyContent(FlexAlign.Center) .width('100%') } .width('15%') .aspectRatio(2) .backgroundColor(Color.Blue) .borderRadius(10) .margin(10) Row(){ Image($r('app.media.right_grey')) } .width('5%') .aspectRatio(1) .margin({right:10}) } .width('100%') .onClick(()=>{ this.timeDialogController?.open() }) Row() .width('96%') .height(2) .backgroundColor(Color.Gray) Row(){ Row(){ Text('剩余补签卡') .fontSize('15fp') } .justifyContent(FlexAlign.Start) .width('50%') .margin(10) Blank() Row(){ Row(){ Text(`${this.userVoc.reSignCard}`) .fontSize('15fp') .fontColor(Color.White) .textAlign(TextAlign.Center) } .justifyContent(FlexAlign.Center) .width('100%') } .width('15%') .aspectRatio(2) .backgroundColor(Color.Blue) .borderRadius(10) .margin(10) Row(){ Image($r('app.media.right_grey')) } .width('5%') .aspectRatio(1) .margin({right:10}) } .width('100%') } .width('100%') .height('100%') } } ``` ![image-20250618110155990](README.assets/image-20250618110155990.png) 每日学习量和学习提醒点击触发保存 ### /user/day/voc 接口 ![image-20250618111049726](README.assets/image-20250618111049726.png) ![image-20250618111100458](README.assets/image-20250618111100458.png) ![image-20250618111114405](README.assets/image-20250618111114405.png) #### src/main/ets/api/vo/response/CommonResponseVo.ets ``` export class CommonResponseVo { msg:string = '成功' code:number = 0 data?:T } ``` #### src/main/ets/api/userDayVoc.ets ``` import { AxiosResponse } from "@ohos/axios" import axiosInstance from "./axios" import { CommonResponseVo } from "./vo/response/CommonResponseVo" export const userDayVoc = async (count:number)=> { let response:AxiosResponse = await axiosInstance.put(`/user/day/voc?count=${count}`) if (response.status === 200) { console.info('put 成功') return response.data } return new CommonResponseVo() } ``` ### /user/remind 接口 ![image-20250618111647027](README.assets/image-20250618111647027.png) ![image-20250618111657504](README.assets/image-20250618111657504.png) ![image-20250618111713473](README.assets/image-20250618111713473.png) #### src/main/ets/api/vo/response/RemindResponseVo.ets ``` export class RemindResponseVo{ time:string = '' uid:string = '' } ``` #### src/main/ets/api/userRemind.ets ``` import { AxiosResponse } from "@ohos/axios" import axiosInstance from "./axios" import { CommonResponseVo } from "./vo/response/CommonResponseVo" import { RemindResponseVo } from "./vo/response/RemindResponseVo" export const userRemind = async (userRemind:RemindResponseVo)=> { // let response:AxiosResponse = await axiosInstance.put(`/user/remind`) // let response:AxiosResponse = await axiosInstance.put(`/user/remind`,body) let response:AxiosResponse = await axiosInstance.put(`/user/remind`,userRemind) if (response.status === 200) { console.info('put 成功') return response.data } console.info(`${response.status}`) return new CommonResponseVo() } ``` #### src/main/ets/pages/FourthComponent/NumDialog.ets ``` import { userDayVoc } from "../../api/userDayVoc" import { CommonResponseVo } from "../../api/vo/response/CommonResponseVo" @CustomDialog export struct NumDialog{ @Consume vocCountOfDay:number @State inputNum:string = '' controller: CustomDialogController build() { Column(){ Row(){ Text('每日学习量') .fontSize('15fp') .textAlign(TextAlign.Center) } .width('100%') .height('30%') .justifyContent(FlexAlign.Center) Row(){ TextInput({placeholder:`${this.vocCountOfDay}`,text:$$this.inputNum}) .borderRadius(5) .width('50%') } .width('100%') .height('30%') .justifyContent(FlexAlign.Center) Row(){ Button('保存') .onClick(async ()=>{ try { this.vocCountOfDay = parseInt(this.inputNum) let response = await userDayVoc(this.vocCountOfDay) as CommonResponseVo console.log("收到响应数据") }catch (error) { console.error("请求出错:", error) } this.controller.close() }) } .width('100%') .height('30%') .justifyContent(FlexAlign.Center) } .height('30%') .backgroundColor(Color.White) .justifyContent(FlexAlign.SpaceBetween) } } ``` src/main/ets/pages/FourthComponent/TimeDialog.ets ``` import { userRemind } from "../../api/userRemind" import { CommonResponseVo } from "../../api/vo/response/CommonResponseVo" import { RemindResponseVo } from "../../api/vo/response/RemindResponseVo" @CustomDialog export struct TimeDialog{ @Consume remindTime:string @State inputTime:string = '' @State userRemind:RemindResponseVo = new RemindResponseVo() @Consume userId:string controller: CustomDialogController build() { Column(){ Row(){ Text('学习提醒') .fontSize('15fp') .textAlign(TextAlign.Center) } .width('100%') .height('30%') .justifyContent(FlexAlign.Center) Row(){ TextInput({placeholder:`${this.remindTime}`,text:$$this.inputTime}) .borderRadius(5) .width('50%') } .width('100%') .height('30%') .justifyContent(FlexAlign.Center) Row(){ Button('保存') .onClick(async ()=>{ try { this.remindTime = this.inputTime this.userRemind.time = this.remindTime this.userRemind.uid = this.userId let response = await userRemind(this.userRemind) as CommonResponseVo console.log("收到响应数据") }catch (error) { console.error("请求出错:", error) } this.controller.close() }) } .width('100%') .height('30%') .justifyContent(FlexAlign.Center) } .height('30%') .backgroundColor(Color.White) .justifyContent(FlexAlign.SpaceBetween) } } ``` ![image-20250618112003066](README.assets/image-20250618112003066.png) 点击购买单词上限进入商品列表 ### /product 接口 ![image-20250618112146912](README.assets/image-20250618112146912.png) ![image-20250618112159895](README.assets/image-20250618112159895.png) ![image-20250618112220748](README.assets/image-20250618112220748.png) ### src/main/ets/api/vo/request/ProductRequestVo.ets ``` export class ProductRequestVo { msg:string = '' code:number = 0 productList:ProductList = new ProductList() } export class ProductList{ pageList:ProductInfo[] = [] totalCount:number = 0 } export class ProductInfo{ desc:string = '' id:string = '' name:string = '' price:number = 0 value:number = 0 } ``` #### src/main/ets/api/product.ets ``` import axiosInstance from "./axios" import { AxiosResponse } from '@ohos/axios' import { ProductList } from "./vo/request/ProductRequestVo" export const product = async ()=>{ let response:AxiosResponse = await axiosInstance.get('/product') if (response.status === 200) { return response.data.data } return new ProductList() } ``` #### src/main/ets/pages/FourthComponent/ProductPage.ets ``` import { product } from "../../api/product" import { ProductInfo, ProductList } from "../../api/vo/request/ProductRequestVo" @Component export struct ProductPage{ @Consume navPathStack: NavPathStack @State productList:ProductList = new ProductList() aboutToAppear(): void { this.onselectBookChange() } async onselectBookChange() { try { let response = await product() as ProductList if (response) { this.productList = response } console.log("收到响应数据:", JSON.stringify(response)) }catch (error) { console.error("请求出错:", error) } } build() { NavDestination(){ Column(){ List(){ ForEach(this.productList.pageList,(productShow:ProductInfo)=>{ ListItem(){ Column(){ Row(){ Row(){ Text(`${productShow.desc}`) } .width('60%') .margin({left:5}) Row(){ Text(`¥ ${productShow.price}`) } .width('30%') .justifyContent(FlexAlign.End) .margin({right:5}) } .width('100%') .margin(10) Row() .width('96%') .height(2) .backgroundColor(Color.Grey) .margin({bottom:5}) } .width('100%') } .width('100%') }) } .width('100%') .height('100%') } .width('100%') .height('100%') } .width('100%') .height('100%') .menus(this.payHistoryMenu()) } @Builder payHistoryMenu(){ Row(){ Row() .width('20%') Row(){ Text('购买') .fontSize('25fp') .fontWeight(FontWeight.Bold) } .width('20%') .justifyContent(FlexAlign.Center) .alignItems(VerticalAlign.Center) Row(){ Text('历史记录') .onClick(()=>{ this.navPathStack.pushPathByName('PayHistoryPage',null) }) } .width('20%') .justifyContent(FlexAlign.End) } .width('100%') .height('100%') .alignItems(VerticalAlign.Center) .justifyContent(FlexAlign.SpaceBetween) } } ``` ![image-20250618113343182](README.assets/image-20250618113343182.png) ------ 点击历史记录展示历史记录界面 ### /user/pay/list 接口 ![image-20250618113438197](README.assets/image-20250618113438197.png) ![image-20250618113452540](README.assets/image-20250618113452540.png) ![image-20250618113512012](README.assets/image-20250618113512012.png) #### src/main/ets/api/vo/request/UserPayListRequestVo.ets ``` export class UserPayListRequestVo{ msg:string = '' code:number = 0 userPayData:UserPayList = new UserPayList() } export class UserPayList{ pageList:UserPay[] = [] totalCount:number = 0 } export class UserPay{ id:number = 0 itemName:string = '' itemType:number = 0 payPrice:number = 0 saleTime:string = '' userId:number = 0 } ``` #### src/main/ets/api/payListHistory.ets ``` import { AxiosResponse } from "@ohos/axios" import axiosInstance from "./axios" import { UserPayList } from "./vo/request/UserPayListRequestVo" export const payListHistory = async ()=>{ let response:AxiosResponse = await axiosInstance.get('/user/pay/list') if (response.status === 200) { return response.data.data } return new UserPayList() } ``` #### src/main/ets/pages/FourthComponent/PayHistoryPage.ets ``` import { payListHistory } from "../../api/payListHistory" import { UserPay, UserPayList } from "../../api/vo/request/UserPayListRequestVo" @Component export struct PayHistoryPage{ @Consume navPathStack: NavPathStack @State payList:UserPayList = new UserPayList() aboutToAppear(): void { this.onselectBookChange() } async onselectBookChange() { try { let response = await payListHistory() as UserPayList if (response) { this.payList = response } console.log("收到响应数据:", JSON.stringify(response)) }catch (error) { console.error("请求出错:", error) } } build() { NavDestination(){ Column(){ List(){ ForEach(this.payList.pageList,(item:UserPay)=>{ ListItem(){ Column(){ Row(){ Row(){ Text(`${item.itemName}`) } .width('30%') .margin({left:5}) Row(){ Text(`¥ ${item.payPrice}`) } .width('30%') Row(){ Text(`${item.saleTime}`) } .width('30%') } .width('100%') .margin(10) .justifyContent(FlexAlign.SpaceBetween) Row() .width('96%') .height(2) .backgroundColor(Color.Grey) .margin({bottom:5}) } .width('100%') } .width('100%') }) } .width('100%') .height('100%') } .width('100%') .height('100%') } .width('100%') .height('100%') .title('历史购买记录') } } ``` image-20250618114953251 ------ #### src/main/ets/pages/FourthPage.ets ``` import auth, { AuthUser } from "@hw-agconnect/auth" import { FirstPartIcons } from "./FourthComponent/FirstPartIcons" import { FourthPartUserVoc } from "./FourthComponent/FourthPartUserVoc" import { SecondPartUserInfo } from "./FourthComponent/SecondPartUserInfo" import { ThirdPartCheckinStatistics } from "./FourthComponent/ThirdPartCheckinStatistics" @Component export struct FourthPage{ @Consume navPathStack: NavPathStack @Consume payVocBound:number @Consume vocCountOfDay:number @Consume remindTime:string @Consume isLogin:boolean @Consume authUser: AuthUser | null build() { NavDestination(){ Column(){ FirstPartIcons() .width('100%') .height('10%') SecondPartUserInfo() .width('100%') .height('16%') ThirdPartCheckinStatistics() .width('100%') .height('8%') FourthPartUserVoc() .width('100%') .height('50%') .margin({top:5}) Button('登出') .width('80%') .height('5%') .onClick(async ()=>{ this.isLogin = false await auth.signOut() }) } .width('100%') .height('100%') .padding(10) } } } ``` ## 八、统计界面 ### src/main/ets/pages/ThirdComponent/CalendarComponent.ets ``` interface DayItem { day: string; isCurrentMonth: boolean; } @Component export struct CalendarComponent { @State currentDate: Date = new Date() @State selectedDate: string = '' @State daysArray: DayItem[] = [] aboutToAppear() { this.updateCalendar() } private updateCalendar(): void { const year = this.currentDate.getFullYear() const month = this.currentDate.getMonth() // 获取当月第一天是星期几 (0-6, 0是星期日) const firstDay = new Date(year, month, 1).getDay() // 获取当月天数 const daysInMonth = new Date(year, month + 1, 0).getDate() // 创建日期数组 const newDaysArray: DayItem[] = [] // 填充前面的空白(上个月的日期) for (let i = 0; i < firstDay; i++) { newDaysArray.push({day: '', isCurrentMonth: false}) } // 填充当月的日期 for (let i = 1; i <= daysInMonth; i++) { newDaysArray.push({day: i.toString(), isCurrentMonth: true}) } // 更新状态 this.daysArray = newDaysArray } private changeMonth(offset: number): void { this.currentDate = new Date( this.currentDate.getFullYear(), this.currentDate.getMonth() + offset, 1 ) this.selectedDate = '' this.updateCalendar() } private handleDayClick(day: string): void { if (!day) return; const dateStr = `${this.currentDate.getFullYear()}-${this.currentDate.getMonth() + 1}-${day.padStart(2, '0')}` this.selectedDate = dateStr; } private isToday(day: string): boolean { const today = new Date() return ( day !== '' && parseInt(day) === today.getDate() && this.currentDate.getMonth() === today.getMonth() && this.currentDate.getFullYear() === today.getFullYear() ); } private isSelected(day: string): boolean { if (!day || !this.selectedDate) return false const selectedDay = this.selectedDate.split('-')[2] return day.padStart(2, '0') === selectedDay } build() { Column() { // 月份导航栏 Row() { Button('←') .width(40) .height(40) .fontSize(16) .onClick(() => this.changeMonth(-1)) Text(`${this.currentDate.getFullYear()}年${this.currentDate.getMonth() + 1}月`) .fontSize(20) .margin({ left: 20, right: 20 }) Button('→') .width(40) .height(40) .fontSize(16) .onClick(() => this.changeMonth(1)) } .justifyContent(FlexAlign.Center) .width('100%') .margin({ bottom: 10 }) // 星期标题 Row() { ForEach(['日', '一', '二', '三', '四', '五', '六'], (weekday: string) => { Text(weekday) .fontSize(16) .fontColor('#333') .width('14.28%') .textAlign(TextAlign.Center) }) } .width('100%') .margin({ bottom: 5 }) // 日期网格 - 使用Grid组件 Grid() { ForEach(this.daysArray, (item: DayItem, index?: number) => { GridItem() { if (item.day !== '') { Column() { Text(item.day) .fontSize(16) .fontColor(this.isToday(item.day) ? Color.Red : '#333') } .width('100%') .height(50) .justifyContent(FlexAlign.Center) .borderRadius(25) .backgroundColor(this.isSelected(item.day) ? '#1890ff' : 'transparent') .onClick(() => { this.handleDayClick(item.day) }) } } .width('100%') .height(50); }) } .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr') .rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr') .rowsGap(5) .columnsGap(5) .width('100%') .height(300) .margin({ top: 5 }) } .width('100%') .height('100%') .padding(10) } } ``` src/main/ets/pages/ThirdPage.ets ``` import { CalendarComponent } from "./ThirdComponent/CalendarComponent" @Component export struct ThirdPage{ @Consume navPathStack: NavPathStack build() { NavDestination(){ Column(){ CalendarComponent() } .width('100%') .width('100%') .justifyContent(FlexAlign.Center) } .width('100%') .height('100%') } } ```