横线笔记本风:ArkUI 让鸿蒙错题页像真实错题本


实例:错题本(Mistake)|风格:横线笔记本
一、设计理念:错题本要「像一本真的笔记」
错题本的 UI 目标是学生的实体错题本——题目写在横线纸上、旁边标着错因和订正。实例 19 的风格「横线笔记本」:错因统计条(彩色圆点)+ 学科/状态筛选 + 横线纸题目卡(下边线)。
页面信息架构:
- 标题栏:错题本 + 总数/待复习 + 刷新;
- 错因统计条:彩色圆点 + 错因名 + 数量(横向滚动);
- 学科筛选:全部/数学/英语/物理/化学;
- 状态 tab:全部/待复习/已掌握;
- 错题列表:横线纸卡(标签行 + 题目 + 知识点 + 我的答案/正确答案 + 操作按钮);
- 详情弹窗:完整题目信息 + 复习。
二、页面骨架与核心状态
@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 自动联动,日后新增错因零成本。
更多推荐




所有评论(0)