在这里插入图片描述
在这里插入图片描述

实例:错题本(Mistake)|风格:横线笔记本

一、设计理念:错题本要「像一本真的笔记」

错题本的 UI 目标是学生的实体错题本——题目写在横线纸上、旁边标着错因和订正。实例 19 的风格「横线笔记本」:错因统计条(彩色圆点)+ 学科/状态筛选 + 横线纸题目卡(下边线)

页面信息架构:

  1. 标题栏:错题本 + 总数/待复习 + 刷新;
  2. 错因统计条:彩色圆点 + 错因名 + 数量(横向滚动);
  3. 学科筛选:全部/数学/英语/物理/化学;
  4. 状态 tab:全部/待复习/已掌握;
  5. 错题列表:横线纸卡(标签行 + 题目 + 知识点 + 我的答案/正确答案 + 操作按钮);
  6. 详情弹窗:完整题目信息 + 复习。

二、页面骨架与核心状态

@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;

核心状态:mistakes(列表)、overview(总览统计)、reasonStats(错因统计)、subjectFilter(学科)、tab(状态 tab)、current/detailVisible(详情弹窗)。

build 骨架(无 Stack——详情用弹窗浮层):

Column() {
  // 标题栏
  // 错因统计条
  // 学科筛选
  // 状态 tab
  // 错题列表
}

三、错因统计条:彩色圆点诊断

顶部错因统计条——学习诊断的视觉化

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%')

彩色圆点 + 错因 + 数量● 计算失误 4 / ● 概念不清 4 / ● 思路错误 2——一眼看到错因分布。REASON_COLORS 是错因 → 颜色的映射(DAO 导出常量):

export const REASON_COLORS: Record<string, string> = {
  '计算失误': '#EF4444', '概念不清': '#F59E0B', '粗心大意': '#3B82F6', '思路错误': '#8B5CF6',
};

?? '#6B7280' 兜底:未知错因灰色——颜色映射的兜底

四、学科筛选 + 状态 tab:双维度过滤

学科筛选(写死四科):

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%')

状态 tab(全部/待复习/已掌握):

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

双维度过滤的组合逻辑(applyFilter):

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);
    }
  }
}

分支策略

  • tab=1(待复习)→ 直接查复习队列(数据库条件查询);
  • 其他 → 先按学科查,tab=2 再内存 filter 已掌握;
  • tab=0 + 学科全部 → queryAll 全量。

「数据库查询 + 内存 filter 混合」——待复习走数据库(条件复杂)、已掌握走内存(数据已取回)——**「查询放数据库、微调放内存」**的权衡。

五、横线纸题目卡:核心视觉

每张错题卡 = 标签行 + 横线纸题目区 + 操作行:

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

卡片四区

1. 标签行四标签:学科(蓝底)、错因(错因色 22% 透明底 + 错因色字)、复习进度(复习 2/5 次 绿底)、右侧状态(✅ 已掌握 / ⏳ 复习中)。「‘22’ 透明后缀」(REASON_COLORS[m.reason] ?? '#6B7280') + '22'——颜色加 22 透明度的浅色底(标签底色)。

2. 横线纸题目区lineHeight(24) 题目 + 🏷 知识点 + ❌ 我的答案(红)vs ✅ 正确答案(绿)对照——「红绿对照」是错题本的核心视觉(一眼看差在哪)。浅灰底 #FAFBFD + 底部单边边框 border({ width: { bottom: 1 }, color: '#E5E9F0' })——「单边下边框 = 横线纸的横线」,横线笔记本的视觉精髓。

3. 操作行三按钮:标记掌握/取消掌握(绿↔灰切换)+ 复习(蓝)+ ✕ 删除(红底)。按钮状态随 mastered 切换

4. 卡片描边border({ width: 1, color: '#E5E9F0' }) 浅灰描边——纸质感。

六、详情弹窗与复习

点卡片 → showDetail(设置 current + detailVisible)→ 弹窗展示完整信息 + 复习按钮(落地版弹窗在 19-5 完整代码展示,此处描述逻辑):

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

复习后 Toast 差异化:第 5 次复习 → 「🎉 已标记掌握!」(轮次达标)、前 4 次 → 「✅ 复习第 N 次」——「状态转变的庆祝提示」(5 次是里程碑)。

toggleMastered

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 ? '↩️ 已移出掌握区' : '✅ 已标记掌握' });
  });
}

掌握/取消掌握切换m.mastered === 1 ? 0 : 1——当前掌握 → 取消(0)、当前未掌握 → 标记(1)——「二元切换」(与影音 toggle 筛选同模式)。

七、UI 风格要素一览

风格项 取值 说明
页面背景 #F5F6F8 浅灰 笔记本纸感
横线纸区 #FAFBFD + 单边下边框 横线纸横线
错因色 REASON_COLORS 映射 诊断彩色
红绿对照 ❌ 红 / ✅ 绿 答案对照
标签行 四标签 学科/错因/进度/状态
复习进度 复习 N/5 次 轮次可见

八、文章小结

本篇文章完成了实例 19 的 UI 层:错因统计条(彩色圆点诊断)+ 学科筛选/状态 tab 双维度 + 横线纸题目卡(单边下边框横线 + 红绿答案对照 + 四标签行)+ 复习/标记掌握交互。设计核心是「笔记本纸感」——border({ width: { bottom: 1 } }) 单边边框模拟横线、❌红/✅绿对照订正;技术亮点是双维度过滤的分支策略(数据库查询 + 内存 filter)与状态转变的庆祝 Toast。

下一篇(19-3)深入数据层,讲解复习轮次的条件更新与统计——reviewOnce 的 CASE WHEN 是本实例的技术核心。

九、补充:横线纸视觉细节

1. 单边下边框:border 的 width 对象写法

横线纸的「横线」用单边边框模拟——border({ width: { bottom: 1 }, color: '#E5E9F0' })。这里 width 传的是对象而非数字:{ bottom: 1 } 表示只有下边 1vp,其余三边为 0。对比:

写法 效果
border({ width: 1 }) 四边 1vp 矩形框(贴纸感)
border({ width: { bottom: 1 } }) 仅下边 1vp(横线纸横线)

要点:多道错题卡纵向排列时,每卡下边框首尾相接,视觉上连成连续的横线——这正是真实横线笔记本的行线。若改用四边框,卡片间会出现矩形格,纸感全失。

2. 红绿对照:❌ 错答 vs ✅ 正答

题目区 ❌ 我的答案#DC2626 红)与 ✅ 正确答案#059669 绿)上下相邻。红的语义是「错误信号」、绿的语义是「正确结论」——视线先被红吸引、再落向绿,完成一次「发现错误 → 获得订正」的扫读。红绿同为 12 号字、同 margin 间距,保证对照公平,不因字号差异误导判断。

3. 标签行四标签配色规则

标签 底色 字色 语义
学科 #DBEAFE 蓝底 #3B82F6 领域归属
错因 错因色 + ‘22’ 透明底 错因色 诊断分类
进度 #E5F6EC 绿底 #059669 复习轮次
状态 无底纯文字 绿/橙 掌握与否

规则:前三个标签「浅底 + 同色系深字」形成一组,右侧状态用纯文字做视觉收尾——四标签一紧一松,不抢题目区焦点。

4. FAQ

Q1:单边边框能写成 top 或 left 吗?
可以,width: { top: 1 } / { left: 1 } 同理,但横线纸只需下边——一处边框一个语义,其余三边留白交给卡片间距。

Q2:为什么错因标签底色要加 ‘22’ 而不是直接写浅色?
'22' 是十六进制透明度(约 13%),错因色加透明后缀自动得到同色系浅底——无需为四种错因各配一个浅色,配色随 REASON_COLORS 自动联动,日后新增错因零成本。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐