错题本全量代码与效果:ArkTS 在 HarmonyOS 的数字错题本


实例:错题本(Mistake)|收官文章
一、文件清单
| 文件 | 职责 | 行数 |
|---|---|---|
database/MistakeDao.ets |
数据层:复习轮次、条件更新、统计、10 道种子 | 约 308 行 |
pages/samples/MistakePage.ets |
UI 层:错因统计条 + 筛选 + 横线纸卡片 + 复习交互 | 约 210 行 |
resources/base/profile/main_pages.json |
路由注册:pages/samples/MistakePage |
追加一行 |
pages/Index.ets |
首页入口按钮 | 追加一个按钮 |
本篇文章完整展示可编译运行的 MistakePage 代码,最后描述运行效果。
二、MistakePage 完整代码
import { common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { MistakeDao, Mistake, REASON_COLORS, ReasonStat, MistakeOverview } from '../../database/MistakeDao';
@Entry
@Component
struct MistakePage {
@State mistakes: Mistake[] = [];
@State overview: MistakeOverview = { total: 0, pending: 0, mastered: 0, toReview: 0 };
@State reasonStats: ReasonStat[] = [];
@State subjectFilter: string = '全部';
@State tab: number = 0; // 0 全部 / 1 待复习 / 2 已掌握
@State detailVisible: boolean = false;
@State current: Mistake | null = null;
private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
aboutToAppear(): void {
this.refresh();
}
async refresh(): Promise<void> {
try {
await MistakeDao.initSeedData(this.context);
this.overview = await MistakeDao.overview(this.context);
this.reasonStats = await MistakeDao.reasonStats(this.context);
await this.applyFilter();
} catch (e) {
promptAction.showToast({ message: `加载失败: ${e}` });
}
}
async applyFilter(): Promise<void> {
if (this.tab === 1) {
this.mistakes = await MistakeDao.queryReviewQueue(this.context);
} else {
this.mistakes = await MistakeDao.queryBySubject(this.context, this.subjectFilter);
if (this.tab === 2) {
this.mistakes = this.mistakes.filter((m: Mistake) => m.mastered === 1);
} else if (this.tab === 0 && this.subjectFilter === '全部') {
this.mistakes = await MistakeDao.queryAll(this.context);
}
}
}
async switchTab(t: number): Promise<void> {
this.tab = t;
await this.applyFilter();
}
async switchSubject(s: string): Promise<void> {
this.subjectFilter = s;
await this.applyFilter();
}
showDetail(m: Mistake): void {
this.current = m;
this.detailVisible = true;
}
/** 复习一次 */
async doReview(m: Mistake): Promise<void> {
await MistakeDao.reviewOnce(this.context, m.id);
this.detailVisible = false;
await this.refresh();
promptAction.showToast({ message: m.rounds + 1 >= 5 ? '🎉 复习 5 次,已标记掌握!' : `✅ 复习第 ${m.rounds + 1} 次` });
}
toggleMastered(m: Mistake): void {
MistakeDao.markMastered(this.context, m.id, m.mastered === 1 ? 0 : 1).then(async () => {
await this.refresh();
promptAction.showToast({ message: m.mastered === 1 ? '↩️ 已移出掌握区' : '✅ 已标记掌握' });
});
}
deleteMistake(m: Mistake): void {
promptAction.showDialog({
title: '删除错题',
message: `删除「${m.question.substring(0, 20)}…」?`,
buttons: [
{ text: '取消', color: '#808080' },
{ text: '删除', color: '#EF4444' },
],
}).then((res: promptAction.ShowDialogSuccessResponse) => {
if (res.index === 1) {
MistakeDao.delete(this.context, m.id).then(async () => {
await this.refresh();
promptAction.showToast({ message: '🗑 已删除' });
});
}
});
}
private fmtDate(ts: number): string {
const d = new Date(ts);
return `${d.getMonth() + 1}月${d.getDate()}日`;
}
build() {
Column() {
// ===== 标题栏 =====
Row() {
Column() {
Text('📓 错题本').fontSize(22).fontWeight(FontWeight.Bold)
Text(`共 ${this.overview.total} 道 · 待复习 ${this.overview.toReview}`)
.fontSize(12).fontColor('#999999').margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('↻').fontSize(22).onClick(() => this.refresh())
}.width('100%').padding({ left: 16, right: 16, top: 12 })
// ===== 错因统计条 =====
Scroll() {
Row({ space: 8 }) {
ForEach(this.reasonStats, (r: ReasonStat) => {
Row({ space: 4 }) {
Row().width(8).height(8).borderRadius(4).backgroundColor(REASON_COLORS[r.reason] ?? '#6B7280')
Text(`${r.reason} ${r.count}`).fontSize(12).fontColor('#4B5563')
}
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12).backgroundColor(Color.White)
}, (r: ReasonStat) => r.reason)
}.padding({ left: 16, right: 16, top: 10 })
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('100%')
// ===== 筛选 =====
Scroll() {
Row({ space: 8 }) {
ForEach(['全部', '数学', '英语', '物理', '化学'], (s: string) => {
Text(s)
.fontSize(13).padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.subjectFilter === s ? '#111827' : '#FFFFFF')
.fontColor(this.subjectFilter === s ? Color.White : '#4B5563')
.onClick(() => this.switchSubject(s))
}, (s: string) => s)
}.padding({ left: 16, right: 16, top: 8 })
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('100%')
Row({ space: 8 }) {
ForEach(['全部', '待复习', '已掌握'], (t: string, idx: number) => {
Text(t)
.fontSize(12).padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(12)
.backgroundColor(this.tab === idx ? '#3B82F6' : '#FFFFFF')
.fontColor(this.tab === idx ? Color.White : '#6B7280')
.onClick(() => this.switchTab(idx))
}, (t: string, idx: number) => `t-${idx}`)
}.width('94%').margin({ top: 8 })
// ===== 错题列表(横线纸风格) =====
List({ space: 10 }) {
ForEach(this.mistakes, (m: Mistake) => {
ListItem() {
Column() {
Row() {
Text(m.subject).fontSize(11).padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8).backgroundColor('#DBEAFE').fontColor('#3B82F6')
Text(REASON_COLORS[m.reason] !== undefined ? m.reason : m.reason)
.fontSize(11).padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8).backgroundColor((REASON_COLORS[m.reason] ?? '#6B7280') + '22')
.fontColor(REASON_COLORS[m.reason] ?? '#6B7280')
Text(`复习 ${m.rounds}/5 次`)
.fontSize(11).padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8).backgroundColor('#E5F6EC').fontColor('#059669')
Blank()
Text(m.mastered === 1 ? '✅ 已掌握' : '⏳ 复习中').fontSize(11).fontColor(m.mastered === 1 ? '#059669' : '#F59E0B')
}.width('100%')
// 横线纸题目
Column() {
Text(m.question).fontSize(14).fontWeight(FontWeight.Medium).lineHeight(24)
if (m.knowledge) {
Text(`🏷 ${m.knowledge}`).fontSize(11).fontColor('#6B7280').margin({ top: 6 })
}
Text(`❌ 我的答案:${m.myAnswer}`).fontSize(12).fontColor('#DC2626').margin({ top: 6 })
Text(`✅ 正确答案:${m.answer}`).fontSize(12).fontColor('#059669').margin({ top: 2 })
}
.width('100%').padding(10)
.backgroundColor('#FAFBFD')
.border({ width: { bottom: 1 }, color: '#E5E9F0' })
.margin({ top: 8 })
Row({ space: 8 }) {
Text(`录入于 ${this.fmtDate(m.createdTime)}`).fontSize(10).fontColor('#9CA3AF').layoutWeight(1)
Button(m.mastered === 1 ? '取消掌握' : '标记掌握').height(28).fontSize(11)
.backgroundColor(m.mastered === 1 ? '#EEF2F7' : '#E5F6EC')
.fontColor(m.mastered === 1 ? '#6B7280' : '#059669')
.onClick(() => this.toggleMastered(m))
Button('复习').height(28).fontSize(11).backgroundColor('#3B82F6')
.onClick(() => this.doReview(m))
Button('✕').width(28).height(28).fontSize(12).backgroundColor('#FEE2E2').fontColor('#EF4444')
.onClick(() => this.deleteMistake(m))
}.width('100%').margin({ top: 8 })
}
.padding(12).backgroundColor(Color.White).borderRadius(10)
.border({ width: 1, color: '#E5E9F0' })
.onClick(() => this.showDetail(m))
}
}, (m: Mistake) => `${m.id}-${m.question}`)
}
.width('94%').layoutWeight(1).margin({ top: 10 })
.scrollBar(BarState.Off)
}
.width('100%').height('100%').backgroundColor('#F5F6F8')
}
}
三、注册与运行
main_pages.json追加"pages/samples/MistakePage";Index.ets追加:Button('📓 19 错题本').fontSize(15).width('70%') .onClick(() => this.getUIContext().getRouter().pushUrl({ url: 'pages/samples/MistakePage' }))- 构建验证 →
BUILD SUCCESSFUL。
四、运行效果描述
进入「📓 19 错题本」:
第一屏:标题栏「📓 错题本 · 共 10 道 · 待复习 8」;错因统计条(●概念不清 4 / ●计算失误 3 / ●思路错误 2 / ●粗心大意 1 白色圆角块);学科筛选(全部/数学/英语/物理/化学);状态 tab(全部/待复习/已掌握);下方 10 张错题卡——每卡标签行(数学蓝标 + 概念不清黄标 + 复习 2/5 次绿标 + ⏳ 复习中)+ 横线纸区(题目 + 🏷 知识点 + ❌ 我的答案红 + ✅ 正确答案绿)+ 操作行(录入日期 + 标记掌握/取消掌握 + 复习 + ✕)。
交互一(复习闭环):点「化学配平」卡 → 复习 → Toast「✅ 复习第 1 次」→ 标签变「复习 1/5 次」→ 再点 4 次 → 第 5 次 Toast「🎉 复习 5 次,已标记掌握!」→ 状态变「✅ 已掌握」、待复习 8→7。
交互二(标记/取消掌握):点「sin 75°」卡「取消掌握」→ Toast「↩️ 已移出掌握区」→ 该题回未掌握(rounds 保留 4)→ 再点「标记掌握」→ 恢复。
交互三(待复习队列):点「待复习」tab → 8 道未掌握,f(x+1)(最久没复习)排最前 → 点「已掌握」→ 2 道。
交互四(学科筛选):点「化学」→ 1 道(配平)→ 点「数学」→ 4 道。
交互五(删除):点某卡 ✕ → 确认框(题目前 20 字 + …)→ 删除 → 总数减一。
五、代码质量要点回顾
| 关注点 | 本实例做法 |
|---|---|
| 轮次自增 | rounds = rounds + 1 |
| 条件赋值 | CASE WHEN 满 5 掌握 |
| 条件计数 | SUM(CASE WHEN) 四统计 |
| 复习队列 | mastered=0 + rounds<5 + review ASC |
| 横线纸 | 单边下边框 |
| 红绿对照 | ❌/✅ 答案对照 |
| 双维度筛选 | tab + 学科组合 |
六、文章小结
实例 19「错题本」收官。五篇文章覆盖:复习字段建模(19-1)→ 横线笔记本风 UI(19-2)→ CASE WHEN 条件更新与条件计数(19-3)→ 10 道错题种子(19-4)→ 全量代码(19-5)。核心技术是 rounds 轮次 + CASE WHEN 条件赋值(一条 UPDATE 完成「自增 + 刷新 + 满 5 自动掌握」)、SUM(CASE WHEN) 条件计数(一次扫描四统计)、复习队列(最久未复习优先)、横线纸单边边框与红绿答案对照。这是「学习复习类应用」的完整范式。
最后一个实例(20 物品清单)回归清单管理:物品分类、数量、优先级、已购状态——待购筛选与优先级排序,UI 是清单勾选列表。
更多推荐



所有评论(0)