# 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

2. 选择数据处理位置:中国、默认

3. 找到创建的应用

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”等待同步完成。


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. 混淆脚本

打开混淆规则配置文件“obfuscation-rules.txt”。参照示例添加混淆规则。
```
-keep
./oh_modules/@hw-agconnect/auth
```
7. 在 AppGallery Connect 中,点击认证服务,立即开通。在手机号码中点击启用
在 验证码/通知接收地址 里输入: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
### (2)src/main/ets/model
主要是实体类的定义

### (3)src/main/ets/pages
主要是每个页面的实现,包括具体的页面和每个页面对应的相关组件
### (4)src/main/ets/util
包含一系列工具方法,进行数据的相关处理

## 二、数据库相关
### (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 接口


#### 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信息

登录成功后默认进入首页,显示已经完成全部的单词背诵
## 四、整合界面
### (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接口



##### 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
}
```

### 第二部分
#### 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')
}
}
```

------
### 第三部分
#### 书籍类
##### 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%')
}
}
```

点击书籍跳转到选词页面,同时更新当前正在背诵数据以及统计信息
#### /user/voc/book/chapter/display 接口



##### 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%')
}
}
```

##### 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} 接口



#### 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%')
}
}
```
点击屏幕空白,会显示单词信息
点击按钮,根据算法生成背诵记录,将记录保存在数据库的表中,同时跳转到下一个背诵单词
#### 单词信息类
##### 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)
}
}
```
------
## 七、我的界面
获取用户信息,进度显示进度表百分比
### /user/info 接口



#### 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 接口



#### 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 接口



##### 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%')
}
}
```

每日学习量和学习提醒点击触发保存
### /user/day/voc 接口



#### 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 接口



#### 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)
}
}
```

点击购买单词上限进入商品列表
### /product 接口



### 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)
}
}
```

------
点击历史记录展示历史记录界面
### /user/pay/list 接口



#### 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('历史购买记录')
}
}
```
------
#### 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%')
}
}
```