在这里插入图片描述

在这里插入图片描述

实例:错题本(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')
  }
}

三、注册与运行

  1. main_pages.json 追加 "pages/samples/MistakePage"
  2. Index.ets 追加:
    Button('📓 19 错题本').fontSize(15).width('70%')
      .onClick(() => this.getUIContext().getRouter().pushUrl({ url: 'pages/samples/MistakePage' }))
    
  3. 构建验证 → 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 是清单勾选列表。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐