围绕海岸蓝灯塔与沙箱铃声的备考工程构建 HarmonyOS ArkUI 题海灯塔真题试卷下载平台三特性融合
一、技术前言
在研究生入学考试备考这条漫长赛道上,学习者面临的核心痛点始终是"资源获取分散、刷题进度不可视、提醒触达不够个性化"。从研招网报名截止到全真模考开始,从晨间数学刷题到睡前政治带背,每一个备考节点都需要精准的信息触达和进度追踪。传统备考类应用普遍存在三大短板:真题下载来源无法溯源导致资料可信度存疑、通知提醒铃声千篇一律无法区分备考场景、刷题数据仅以静态列表呈现缺乏动态可视化反馈。这些短板叠加在一起,使得备考体验始终停留在"工具"层面,难以升级为"陪练"层面。
HarmonyOS ArkUI 框架为解决上述痛点提供了系统级的声明式 UI 范式支撑。@Component 装饰器将页面拆分为可复用组件单元,@State 管理响应式状态驱动视图自动刷新,@Builder 将复杂 UI 结构抽离为独立构建函数实现关注点分离。@Observed 装饰器让数据模型字段级变化被 UI 感知,实现"完成度更新即进度条刷新"的流畅体验。@Entry 标记页面入口,配合 Stack/Column/Row/Scroll 等容器组件构建出层次分明的页面骨架。
本平台深度融合 HarmonyOS 6.1.1 的三大前沿特性。ArkWeb 下载代理引入了 WebDownloadDelegate 四回调体系——onBeforeDownload 中调用 start() 提供沙箱路径启动下载任务、onDownloadUpdated 中刷新进度百分比、onDownloadFailed 中捕获异常并清零进度、onDownloadFinish 中调用 6.1.1 新增的 getOriginalUrl()(获取文件直链原始 URL)与 getReferrerUrl()(获取触发下载的引用页 URL)双接口实现真题下载来源的双重溯源。Notification Kit 的通知请求 sound 字段在 6.1.1 中支持应用沙箱 EL1 区域内的自定义音频路径,通过 'uri::' + fileUri.getUriFromPath(path) 将正弦波生成的 WAV 文件转为 URI 前缀格式,实现"开考铃""交卷铃"等备考场景铃声的个性化触达。Canvas 绘制通过 drawBar() 方法实现近六个月刷题量渐变柱状图,每根柱体使用 createLinearGradient 从海岸蓝到深海蓝的垂直渐变填充,breath 状态每秒翻转联动柱高在 0.93~1.0 倍率间微幅波动,模拟数据"呼吸"效果。三特性同文件叠加,形成"下载溯源 → 沙箱铃声 → 数据可视化"的完整备考闭环。
二、整体架构流程图
架构以 Page1209 为根组件,使用 Stack 容器层叠:底层 Column 纵向排列头部渐变 Banner、分割线、Scroll 内容区和底部 Tab 栏,顶层是三个独立弹窗(addModal/editModal/delModal 各自条件渲染,共享 modalOverlay 遮罩层)。内容区通过 currentTab 状态值在六个 @Builder 方法间切换,if-else 链式判断决定渲染哪个 Tab 的布局。三大特性分散在不同 Tab 上——题库 Tab 承载 Canvas 柱状图、网页 Tab 承载 ArkWeb 下载代理、下载 Tab 展示双 URL 溯源结果、提醒 Tab 承载通知授权与发布、铃音 Tab 承载沙箱铃声生成——状态变量统一声明在组件顶层实现跨 Tab 共享(如 dlState 在网页 Tab 写入、在下载 Tab 读取展示;currentRingIdx 在铃音 Tab 设置、在提醒 Tab 读取并填入通知 sound 字段)。这种"特性分散、状态集中"的设计既保证了各 Tab 的视觉独立性,又确保了跨 Tab 数据流的清晰可追溯。
三、色彩体系设计
3.1 ColorPalette 接口定义
interface ColorPalette {
bg: string; // 页面底色·浅海雾蓝
card: string; // 卡片底色·纯白
chip: string; // 胶囊/输入底色·浅云蓝
title: string; // 主标题·深海墨蓝
sub: string; // 次级文字·青灰蓝
text3: string; // 弱化文字·雾蓝灰
blue: string; // 主色·海岸蓝
orange: string; // 辅色·珊瑚橙
green: string; // 辅色·海藻绿
purple: string; // 辅色·鸢尾紫
line: string; // 分割线·浅雾线
tabOn: string; // Tab 激活色·海岸蓝
mask: string; // 弹窗遮罩·深海墨
white: string; // 渐变卡上的纯白文字
whiteSoft: string; // 渐变卡上的弱化白文字
trackW: string; // 渐变卡上的进度条轨道色
}
色彩接口集中声明了页面所有视觉元素的颜色字段。与暗色主题不同,这套浅色主题特别为渐变卡片预留了三枚白色系字段——white(纯白主文字)、whiteSoft(0.82 透明度弱化白文字)和 trackW(0.32 透明度进度条轨道色)——专门服务于头部 Banner 和我的 Tab 的渐变大卡上叠加的文字与进度条,确保白字在深色渐变背景上拥有足够对比度。
3.2 COLORS 常量逐色分析
const COLORS: ColorPalette = {
bg: '#F2F6FA', // 浅海雾蓝底色,营造清爽备考氛围
card: '#FFFFFF', // 纯白卡片底,信息层浮于背景之上
chip: '#E7EEF5', // 浅云蓝胶囊底,区分输入区与展示区
title: '#22303C', // 深海墨蓝主标题,浅底高对比
sub: '#5E7285', // 青灰蓝次级文字,层次柔和
text3: '#93A5B5', // 雾蓝灰弱化文字,辅助信息不抢视觉
blue: '#2F7BD9', // 海岸蓝主色,渐变横幅与按钮主色
blueD: '#1F5FA8', // 深海蓝渐变端点,柱状图与渐变卡深色端
orange: '#FF7E5A', // 珊瑚橙辅色,待办提醒与删除操作
green: '#34B37E', // 海藻绿辅色,完成态与进步趋势
purple: '#8A6FD1', // 鸢尾紫辅色,408 科目标识
line: '#DDE7F0', // 浅雾线分割,低对比不干扰
tabOn: '#2F7BD9', // Tab 激活色与主色一致
mask: 'rgba(34,48,60,0.5)', // 半透深海墨遮罩
white: '#FFFFFF', // 渐变卡纯白主文字
whiteSoft: 'rgba(255,255,255,0.82)', // 渐变卡弱化白
trackW: 'rgba(255,255,255,0.32)' // 渐变卡进度条轨道
};
色彩体系以"海岸蓝白 + 珊瑚橙"为核心对比策略。浅海雾蓝 #F2F6FA 作为全局底色奠定清爽基调,纯白 #FFFFFF 卡片如同海面上的灯塔浮标承载所有信息。主色海岸蓝 #2F7BD9 贯穿头部渐变 Banner、按钮主色、柱状图渐变上端和 Tab 选中态,形成统一的视觉锚点。辅色珊瑚橙 #FF7E5A 专用于"待完成"“删除”"未授权"等需用户注意的语义,与蓝色形成暖冷对比。海藻绿 #34B37E 与鸢尾紫 #8A6FD1 分别用于"完成态"和"408 科目"的语义区分,使五科目各有专属色——数学蓝、英语橙、政治绿、408 紫、管综深蓝。值得注意的是渐变卡上的三枚白色字段,trackW 的 0.32 透明度使进度条轨道在深蓝渐变上仅以隐约可见的方式存在,不与主文字争夺注意力。
四、Tab 元数据与辅助数据
4.1 底部导航 Tab 定义
const TAB_LIST: TabMeta[] = [
{ icon: '📚', label: '题库' },
{ icon: '🌐', label: '网页' },
{ icon: '📥', label: '下载' },
{ icon: '⏰', label: '提醒' },
{ icon: '🎵', label: '铃音' },
{ icon: '👤', label: '我的' }
];
底部导航采用六 Tab 单排布局,每个 Tab 由 Emoji 图标和中文标签组成。六 Tab 覆盖了备考全链路:题库管理真题试卷、网页检索与下载真题资源、下载查看溯源记录、提醒管理学习时间轴、铃音定制通知铃声、我的查看备考全貌。Tab 数量选为六个而非传统的四到五个,是因为备考场景天然需要"资源获取 → 下载管理 → 进度提醒 → 个性化铃声"的完整闭环,六个 Tab 恰好映射这条闭环上的每个关键环节。
4.2 科目分类与快捷站点
const SUBJECT_TAGS: string[] = ['全部', '考研数学', '英语一', '政治', '408', '管综'];
const QUICK_SITES: QuickSite[] = [
{ icon: '🏫', name: '中国教育考试网', url: 'https://www.neea.edu.cn' },
{ icon: '🎓', name: '研招网', url: 'https://yz.chsi.com.cn' },
{ icon: '📖', name: '学信网', url: 'https://www.chsi.com.cn' },
{ icon: '📚', name: '中国教育在线', url: 'https://www.eol.cn' }
];
科目分类横滚 chips 以"全部"为首项不过滤数据,其余五项按科目关键词匹配真题清单。快捷站点选取了四个真实的教育考试类官方网站,点击即加载到 Web 组件中,用户可在此浏览并触发真题下载,下载完成后自动进入 delegate 四回调流程。
4.3 图表数据与铃声预设
const MONTH_LABELS: string[] = ['03月', '04月', '05月', '06月', '07月', '08月'];
const BRUSH_VAL: number[] = [128, 196, 242, 168, 286, 324];
const MISTAKE_VAL: number[] = [46, 38, 52, 31, 27, 19];
const RING_FREQ_PRESETS: number[] = [440, 660, 880, 1320];
const RING_DURATION_PRESETS: number[] = [600, 1200, 2000];
两组月度数据分别服务于两张柱状图:BRUSH_VAL 为刷题量数据(逐月上升趋势,从 128 到 324),MISTAKE_VAL 为错题量数据(逐月下降趋势,从 46 到 19),一升一降形成"刷题量增长、错题量减少"的备考进步叙事。铃声生成器的频率预设提供四档(440Hz 标准音、660Hz、880Hz 高八度、1320Hz 超高音),时长预设三档(600ms 短促、1200ms 标准、2000ms 延长),组合可生成 12 种不同铃声。
五、工具函数
5.1 WAV 音频字节生成器
function buildWavBytes(freq: number, durationMs: number): ArrayBuffer {
// 44 字节 WAV 头 + PCM 采样数据
// 16bit 单声道,采样率 44100Hz
// 含起音包络(20ms 爬升)与自然衰减
}
该函数是沙箱自定义铃声的技术基石。它按标准 WAV 格式逐字节构建音频文件:前 44 字节为 RIFF/WAVE 头(含 PCM 编码标识、采样率、声道数、量化位数等元信息),随后是正弦波采样数据。音频叠加了起音包络(前 20ms 振幅从 0 爬升到峰值)和自然衰减(振幅随时间线性衰减到 0),使生成的铃声不会出现突兀的"咔哒"声。最终产物是一个 ArrayBuffer,通过 fs.writeSync 写入沙箱 EL1 目录。
5.2 站点 URL 转域名
function siteHost(url: string): string {
const head = 'https://';
if (url.startsWith(head)) {
return url.slice(head.length);
}
return url;
}
去除 https:// 协议前缀,返回纯域名+路径字符串,供地址栏提示和快捷站点卡展示使用。
5.3 科目颜色映射
function subjectColor(s: string): string {
if (s.indexOf('数学') >= 0) { return COLORS.blue; }
if (s.indexOf('英语') >= 0) { return COLORS.orange; }
if (s.indexOf('政治') >= 0) { return COLORS.green; }
if (s.indexOf('408') >= 0) { return COLORS.purple; }
if (s.indexOf('管综') >= 0) { return COLORS.blueD; }
return COLORS.sub;
}
通过科目名称关键词匹配返回对应主题色。这个映射被真题清单的左侧年份色块、进度条着色和错题本的左侧色条共用,确保同一科目在页面任何位置都呈现一致的颜色身份。
5.4 下载状态颜色映射
function dlStateColor(s: string): string {
if (s.indexOf('完成') >= 0) { return COLORS.green; }
if (s.indexOf('失败') >= 0) { return COLORS.orange; }
if (s === '空闲') { return COLORS.text3; }
return COLORS.blue;
}
下载状态文案映射到语义色——完成绿、失败橙、空闲灰、进行中蓝——使用户一眼即可判断下载状态。
5.5 完成度颜色映射
function doneColor(v: number): string {
if (v >= 80) { return COLORS.green; }
if (v >= 50) { return COLORS.blue; }
return COLORS.orange;
}
完成度百分数映射为三档颜色:80 分以上绿(优秀)、50~79 分蓝(中等)、50 分以下橙(偏低)。这个映射直接作用于真题清单右侧的完成度数字颜色,提供即时的视觉反馈。
六、数据模型层
本平台定义了六个 @Observed 数据模型类,每个类对应一种业务实体。
6.1 PaperItem 真题试卷条目
@Observed export class PaperItem {
subject: string; // 科目
year: string; // 年份卷
count: string; // 题量文本
done: number; // 完成度百分数(0~100)
}
真题试卷是题库 Tab 的核心实体,也是新增/编辑/删除三个弹窗的操作对象。done 字段是唯一可编辑的数值,通过 @Observed 装饰使编辑弹窗中的 Slider 调整即时反映到清单行的进度条和完成度数字上。Mock 数据预置 8 条覆盖五个科目,完成度从 35% 到 90% 不等,既有高分完成的也有偏低待刷的。
6.2 DownloadRecord 下载记录条目
@Observed export class DownloadRecord {
fileName: string; // 文件名
fileSize: string; // 大小文本
finishTime: string; // 完成时间
originalUrl: string; // getOriginalUrl() 结果
referrerUrl: string; // getReferrerUrl() 结果
}
这是 ArkWeb 6.1.1 双 URL 溯源特性的数据载体。originalUrl 存储 getOriginalUrl() 返回的文件直链地址,referrerUrl 存储 getReferrerUrl() 返回的触发下载的页面地址。每条记录的 Mock 数据中,两个 URL 均为带域名、路径和查询参数的完整真实感地址,如 https://files.neea.edu.cn/exam/2026/kaoyan/math1_2025_full.pdf?track=official(原始 URL)和 https://www.neea.edu.cn/exam/download?subject=kaoyan-math(引用页 URL),模拟从官方考试网站下载真题的完整溯源链路。
6.3 RemindItem 学习提醒条目
@Observed export class RemindItem {
time: string; // 提醒时间
title: string; // 提醒事项
repeat: string; // 重复规则
on: boolean; // 是否开启
}
学习提醒时间轴的行数据,on 字段通过 Toggle 开关双向绑定。Mock 数据预置 6 条覆盖典型备考节点——背单词打卡、晨间数学刷题、全真模考、午间错题回顾、报名截止提醒、睡前政治带背——其中 4 条开启、2 条关闭。
6.4 RingItem 铃声条目
@Observed export class RingItem {
name: string; // 铃声名
file: string; // 沙箱文件名
freq: number; // 生成频率 Hz
duration: number; // 时长 ms
size: string; // 文件大小展示
inSandbox: boolean; // 是否已写入沙箱 EL1
}
铃声坊的库行数据,inSandbox 标记该铃声是否已生成 WAV 字节并写入沙箱。初始 5 条铃声均未入沙箱(inSandbox: false,size: '—'),用户点击"生成到沙箱"后才计算实际文件大小并更新状态。
6.5 NoticeLog 通知历史条目
@Observed export class NoticeLog {
title: string; // 通知标题
text: string; // 通知正文
time: string; // 发布时间
}
已发布通知的记录,Mock 数据 3 条,每次调用 publishNotice() 成功后通过 unshift 置顶新增记录。
6.6 MistakeItem 错题本条目
@Observed export class MistakeItem {
subject: string; // 科目
question: string; // 题目摘要
reason: string; // 错因标签
time: string; // 收录时间
}
错题本清单行数据,reason 字段记录错因标签(计算失误、概念混淆、主观臆断等),Mock 数据 7 条覆盖五个科目。每条左侧以 subjectColor() 映射的科目色条标识科目归属。
七、组件主体结构
7.1 @State 状态变量声明
组件 Page1209 在顶部集中声明了全部状态变量,按功能分为五组:
基础状态组:currentTab(当前 Tab 索引)、breath(呼吸动画布尔翻转)、timer(定时器句柄)。呼吸动画每秒翻转一次 breath 值,联动 Canvas 柱状图重绘和传统柱状图柱高波动。
弹窗状态组:addModal/editModal/delModal 三个布尔开关控制三个弹窗的条件渲染,editIdx/delIdx 记录当前操作的真题索引。
列表数据组:paperList/downloadRecords/remindList/ringList/noticeLogs/mistakeList 六个 @State 数组分别绑定六个数据模型的 Mock 数据,数组引用整体刷新(slice() 拷贝)驱动 ForEach 重绘。
表单状态组:formSubject/formYear/formCount/formDone 四个新增表单字段,editDone 一个编辑表单字段。
ArkWeb 状态组:webController(Web 控制器,private)、downloadDelegate(下载代理,private)、urlInput/webUrl(地址栏输入值与实际加载值双状态分离)、dlName/dlPercent/dlState(下载文件名、进度、状态文案)。
Notification 状态组:granted(通知授权状态)、notifyId(通知 id 自增计数器)、noticeCount(已发布条数)、currentRingIdx(当前默认铃声索引)、genFreq/genDuration(生成器参数)、sandboxCount(已落盘铃声数)。
Canvas 状态组:canvasReady(Canvas 就绪标志,onReady 后才允许定时器重绘)、barCtx(Canvas 2D 上下文,private)。
7.2 生命周期
aboutToAppear() {
this.setupDownloadDelegate(); // 绑定下载代理四回调
notificationManager.isNotificationEnabled().then((enabled) => {
this.granted = enabled; // 查询通知授权状态
});
this.timer = setInterval(() => {
this.breath = !this.breath; // 每秒翻转呼吸状态
if (this.canvasReady) {
this.drawBar(); // Canvas 就绪后每秒重绘柱状图
}
}, 1000);
}
aboutToDisappear() {
clearInterval(this.timer); // 销毁时清理定时器防止内存泄漏
}
页面创建时完成三件初始化工作:绑定 ArkWeb 下载代理的四个回调函数、异步查询通知授权状态并写入 granted 状态、启动每秒翻转的呼吸动画定时器。定时器中先翻转 breath,再检查 canvasReady 标志——只有 Canvas 组件触发 onReady 回调后才会调用 drawBar() 重绘,避免 Canvas 未就绪时绘制导致的空指针异常。页面销毁时清理定时器,防止内存泄漏。
7.3 build() 主构建
build() {
Stack() {
Column() {
this.headerMain()
Divider().strokeWidth(1).color(COLORS.line)
Scroll() {
Column() {
if (this.currentTab === 0) { this.tabPaper() }
else if (this.currentTab === 1) { this.tabWeb() }
// ... 其余 Tab 分支
}
}
.layoutWeight(1).scrollBar(BarState.Off)
this.tabBar()
}
if (this.addModal) { this.panelAdd(() => { this.addModal = false }) }
if (this.editModal) { this.panelEdit(() => { this.editModal = false }) }
if (this.delModal) { this.panelDel(() => { this.delModal = false }) }
}
.backgroundColor(COLORS.bg)
}
Stack 层叠容器将主内容列和弹窗分层:底层 Column 纵向排列头部、分割线、滚动内容区和底部 Tab 栏,顶层三个弹窗各自条件渲染。每个弹窗接收一个 onClose 回调函数用于关闭自身。内容区通过 if-else 链根据 currentTab 值切换六个 @Builder 方法,Scroll 包裹保证内容超出屏幕时可滚动,scrollBar(BarState.Off) 隐藏滚动条保持视觉整洁。
八、头部区域详解
头部 headerMain() 由海岸蓝渐变 Banner 和搜索条两部分组成。
8.1 渐变 Banner
Column({ space: 8 }) {
Row() {
Text('🌊 题海灯塔').fontSize(18).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('距 2027 初试 108 天').fontSize(9).fontColor(COLORS.blueD)
.backgroundColor(COLORS.white).borderRadius(11)
}
Text('真题为岸 · 每一套都值得溯源收藏').fontSize(11).fontColor(COLORS.whiteSoft)
Row({ space: 8 }) {
Text('今日刷题 86 题').backgroundColor(COLORS.trackW)
Text('待完成 4 套').backgroundColor(COLORS.trackW)
Text('连续打卡 46 天').backgroundColor(COLORS.trackW)
}
}
.linearGradient({ angle: 120, colors: [[COLORS.blueD, 0], [COLORS.blue, 0.65], [COLORS.blueD, 1]] })
渐变 Banner 使用 120 度三段渐变——深海蓝(0%)→ 海岸蓝(65%)→ 深海蓝(100%)——模拟海岸线从远海到近岸再到远海的色彩过渡。第一行左侧品牌名"题海灯塔"以 18px 粗体白字呈现,右侧倒计时胶囊以白底深蓝字反衬,形成强烈的视觉锚点。第二行品牌语"真题为岸 · 每一套都值得溯源收藏"以弱化白字呈现,点明平台核心价值主张——真题溯源。第三行三枚备考数据胶囊(今日刷题、待完成、连续打卡)使用 trackW 半透白底,数据以弱化白字呈现,在深蓝渐变上形成隐约可见的数据看板效果。
8.2 搜索条与新增按钮
Row({ space: 8 }) {
Row({ space: 6 }) {
Text('🔍').fontSize(12)
Text('搜真题 / 粘贴下载链接').fontSize(10).fontColor(COLORS.text3)
}
.backgroundColor(COLORS.card).borderRadius(17)
.onClick(() => { this.currentTab = 1; }) // 点击跳转网页 Tab
Text('+ 新增真题').fontSize(10).fontColor(COLORS.white)
.backgroundColor(COLORS.blue).borderRadius(17)
.onClick(() => { this.addModal = true; })
}
搜索条以白色卡片底配雾蓝灰占位文字,点击后直接切换到网页 Tab(currentTab = 1),引导用户使用 Web 检索真题。右侧"新增真题"按钮以海岸蓝实心底白字呈现,点击打开新增弹窗。搜索条和新增按钮在同一行内通过 layoutWeight(1) 实现弹性宽度分配。
九、题库 Tab 详解
题库 Tab(tabPaper())由科目横滚 chips、三枚统计小卡、真题试卷清单和 Canvas 柱状图卡四个区块组成,信息密度最高。
9.1 科目分类横滚 chips
Scroll() {
Row({ space: 8 }) {
ForEach(SUBJECT_TAGS, (tag: string, idx: number) => {
Text(tag).fontSize(10)
.fontColor(this.cateIdx === idx ? COLORS.white : COLORS.sub)
.backgroundColor(this.cateIdx === idx ? COLORS.blue : COLORS.card)
.borderRadius(13)
.onClick(() => { this.cateIdx = idx; })
})
}
}
.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
六个科目标签横向滚动排列,选中态以海岸蓝底白字呈现,未选中态以白底青灰蓝字呈现。点击切换 cateIdx 后触发 visiblePapers() 重新过滤真题清单——"全部"返回全部数据,其余按科目关键词匹配。ForEach 的 keyGenerator 使用标签文本本身,确保切换选中态时仅刷新颜色而不重建组件。
9.2 三枚统计小卡
Row({ space: 8 }) {
Column() { Text('1344').fontColor(COLORS.blueD); Text('累计刷题(题)') }
Column() { Text('8').fontColor(COLORS.orange); Text('真题卷(套)') }
Column() { Text('76%').fontColor(COLORS.green); Text('平均完成度') }
}
三枚统计卡等宽排列(layoutWeight(1)),数值分别以深海蓝、珊瑚橙、海藻绿着色——蓝色代表刷题总量、橙色代表待完成量、绿色代表完成度——形成"蓝橙绿"三色数据看板。每枚卡片白底圆角,数值 16px 粗体,标签 8px 雾蓝灰。
9.3 真题试卷清单行
@Builder
paperRow(item: PaperItem, idx: number) {
Row({ space: 10 }) {
// 左侧年份色块(科目主题色)
Column() {
Text(item.year.replace(' 卷', '')).fontSize(11).fontColor(COLORS.white)
Text('卷').fontSize(7).fontColor(COLORS.whiteSoft)
}.backgroundColor(subjectColor(item.subject))
// 中部:科目+题量+完成度进度条
Column({ space: 5 }) {
Text(item.subject + ' · ' + item.year).fontSize(12)
Progress({ value: item.done, total: 100, type: ProgressType.Linear })
.color(subjectColor(item.subject))
Text('共 ' + item.count + ' · 已刷 ' + Math.round(item.done) + '%')
}
// 右侧完成度数字
Column() {
Text(item.done.toString() + '%').fontColor(doneColor(item.done))
Text('完成度')
}
}
.onClick(() => { this.openEditPaper(idx); }) // 点击编辑完成度
.onLongClick(() => { this.delIdx = idx; this.delModal = true; }) // 长按删除
}
真题清单行是题库 Tab 的核心交互单元。左侧 44x44 的年份色块以科目主题色为背景,白色年份数字大字加"卷"字小字组成。中部科目名+题量以粗体标题呈现,下方 Progress 线性进度条的 color 属性同样取科目主题色,使进度条与色块颜色一致。右侧完成度数字以 doneColor() 映射的三档颜色(绿/蓝/橙)着色,提供即时的完成度评估。交互上,点击整行打开编辑弹窗回填当前完成度,长按整行打开删除确认弹窗——两种手势映射两种操作,避免误触。
9.4 Canvas 刷题量柱状图卡
@Builder
barChartCard() {
Column({ space: 8 }) {
Row() {
Text('📊 近 6 个月刷题量').fontWeight(FontWeight.Bold)
Text('合计 1344 题')
}
Canvas(this.barCtx).width('100%').height(210)
.onReady(() => {
this.canvasReady = true;
this.drawBar();
})
Text('渐变柱随呼吸动画微幅波动,顶部实时标注刷题量')
}
}
柱状图卡标题行左侧为粗体标题,右侧为合计数据。Canvas 组件绑定 barCtx 上下文,onReady 回调中设置 canvasReady 标志为 true 并执行首次绘制。此后每秒定时器翻转 breath 时检查该标志,为 true 则调用 drawBar() 重绘,实现柱高微幅波动的"呼吸"效果。底部辅助说明文字以雾蓝灰 8px 呈现。
十、网页 Tab 详解
网页 Tab(tabWeb())是 ArkWeb 下载代理特性的主要承载区域,由地址栏、快捷站点、Web 组件和主动下载按钮四个区块组成。
10.1 地址栏与双状态分离
Row({ space: 8 }) {
TextInput({ text: this.urlInput, placeholder: '输入网址检索真题,如 neea.edu.cn' })
.onChange((value) => { this.urlInput = value; })
Text('前往').backgroundColor(COLORS.blue)
.onClick(() => { this.loadUrl(); })
}
地址栏采用 urlInput(输入值)和 webUrl(实际加载值)双状态分离设计。用户在 TextInput 中敲字时仅更新 urlInput,不触发 Web 组件重新加载;点击"前往"按钮后调用 loadUrl() 方法校验协议前缀(无 http(s):// 时自动补 https://),校验通过后才将值赋给 webUrl,此时 Web 组件才更新加载地址。这种设计避免了用户输入过程中每敲一个字符就触发一次页面加载的性能问题。
10.2 快捷站点横滑
ForEach(QUICK_SITES, (site: QuickSite) => {
Row() {
Text(site.icon)
Text(site.name).fontColor(this.webUrl === site.url ? COLORS.white : COLORS.sub)
}
.backgroundColor(this.webUrl === site.url ? COLORS.blue : COLORS.card)
.onClick(() => { this.urlInput = site.url; this.webUrl = site.url; })
})
四个快捷站点横向滚动排列,当前加载的站点以海岸蓝底白字高亮,点击直接同步两个状态变量实现即时加载。keyGenerator 使用站点 URL,确保站点列表稳定不重建。
10.3 Web 组件本体
Web({ src: this.webUrl, controller: this.webController })
.layoutWeight(1).borderRadius(10).backgroundColor(COLORS.chip)
Web 组件绑定 webController 控制器,网页内点击真题下载链接会自动进入 downloadDelegate 的四回调流程。组件以 layoutWeight(1) 占据地址栏和下载按钮之间的全部剩余空间,圆角裁切保证视觉一致性。
10.4 主动下载按钮
Text('下载数学一真题卷').backgroundColor(COLORS.blue)
.onClick(() => {
this.triggerDownload('https://files.neea.edu.cn/exam/2026/kaoyan/math1_2025_full.pdf?track=official');
})
Text('下载 408 真题卷').border({ width: 1, color: COLORS.blue })
.onClick(() => {
this.triggerDownload('https://dl.eol.cn/408/cs408_2024_real.pdf?track=official&part=all');
})
两个主动下载按钮分别以实心和描边样式呈现,点击调用 triggerDownload() 方法,该方法内部通过 this.webController.startDownload(url) 发起应用侧主动下载(无需网页内点击),下载完成后同样进入 delegate 的 onDownloadFinish 回调,实现双 URL 溯源记录的自动生成。底部辅助文字提示用户"完成后在下载 Tab 查看原始 URL 与引用页 URL 双溯源"。
十一、下载 Tab 详解
下载 Tab(tabDownload())由进行中任务卡和已完成记录列表两个区块组成。
11.1 进行中任务卡
Column({ space: 10 }) {
Row() {
Text('⬇ 下载任务').fontWeight(FontWeight.Bold)
Text(this.dlState).fontColor(dlStateColor(this.dlState))
}
Text(this.dlName === '' ? '暂无进行中任务' : this.dlName)
Progress({ value: this.dlPercent, total: 100, type: ProgressType.Linear })
.color(COLORS.blue).backgroundColor(COLORS.chip)
Row() {
Text('进度 ' + this.dlPercent + '%')
Text('保存至沙箱 filesDir')
}
}
进行中任务卡实时展示 dlName(文件名)、dlPercent(进度百分比)和 dlState(状态文案)三个状态变量。进度条以海岸蓝填充、浅云蓝轨道呈现,状态文案颜色通过 dlStateColor() 动态映射。当无任务时文件名区域显示"暂无进行中任务(可在网页 Tab 主动触发)"的占位提示。
11.2 双 URL 溯源记录卡
@Builder
recordCard(item: DownloadRecord) {
Column({ space: 6 }) {
Row() {
Text('📄 ' + item.fileName).fontWeight(FontWeight.Bold)
Text(item.finishTime)
}
Row() {
Text(item.fileSize).backgroundColor(COLORS.chip)
Text('官方渠道 · PDF').fontColor(COLORS.green)
}
// 原始 URL 行
Row() {
Text('🔗')
Text(item.originalUrl).fontColor(COLORS.blueD).fontFamily('monospace')
}
// 引用页 URL 行
Row() {
Text('📄')
Text(item.referrerUrl).fontColor(COLORS.sub).fontFamily('monospace')
}
}
}
每条下载记录卡片包含四行信息:文件名+完成时间行、文件大小+渠道标识行、🔗 原始 URL 行和 📄 引用页 URL 行。两个 URL 行使用 monospace 等宽字体呈现并以单行截断(maxLines(1) + textOverflow 椭圆省略),原始 URL 以深海蓝着色强调其"文件直链来源"的语义,引用页 URL 以青灰蓝着色表示"触发下载的页面"。这种双 URL 并列展示使用户能够清晰追溯每份真题的下载来路——从哪个页面触发、文件实际存储在哪个服务器。
十二、提醒 Tab 详解
提醒 Tab(tabRemind())由通知授权状态卡、发布学习提醒按钮卡、学习提醒时间轴和通知历史四个区块组成。
12.1 通知授权状态卡
Column({ space: 10 }) {
Row() {
Text('🔔')
Column() {
Text('通知授权状态').fontWeight(FontWeight.Bold)
Text(this.granted ? 'isNotificationEnabled = true' : 'isNotificationEnabled = false')
.fontFamily('monospace')
}
Text(this.granted ? '已授权' : '未授权')
.fontColor(this.granted ? COLORS.green : COLORS.orange)
}
Text(this.granted ? '已授权 · 学习提醒将携带沙箱自定义铃声送达' : '未授权 · 点击右侧按钮申请通知权限')
Row() {
Text(this.granted ? '重新检测授权' : '请求通知授权').backgroundColor(COLORS.blue)
.onClick(() => { this.requestAuth(); })
Text('已发布 ' + this.noticeCount + ' 条').border({ width: 1, color: COLORS.blue })
}
}
授权卡展示 granted 布尔状态——true 时绿色"已授权"标签,false 时橙色"未授权"标签。状态文案下方以等宽字体显示 isNotificationEnabled 的原始返回值,体现技术透明性。两个按钮分别用于请求授权(首次调用弹系统授权框,曾被拒绝则拉起通知设置页二次授权)和展示已发布通知条数。
12.2 发布学习提醒按钮卡
Column({ space: 8 }) {
Row() {
Text('📣 发布学习提醒').fontWeight(FontWeight.Bold)
Text('铃声:' + this.ringList[this.currentRingIdx].name).fontColor(COLORS.blue)
}
Text("通知 sound 字段填 'uri::' + fileUri.getUriFromPath(沙箱路径)")
Text('发布背单词提醒').backgroundColor(COLORS.orange)
.onClick(() => {
this.publishNotice('学习提醒', '该背单词啦:今日计划 120 词,已完成 86 词');
})
}
该卡片是通知 sound 字段沙箱铃声特性的触发入口。标题行右侧显示当前默认铃声名称(从铃音 Tab 的 currentRingIdx 跨 Tab 读取),说明文字以等宽字体展示 sound 字段的拼接逻辑。发布按钮以珊瑚橙底白字呈现(区别于普通蓝色按钮),点击调用 publishNotice() 方法发布携带沙箱自定义铃声的通知。publishNotice 内部先检查当前铃声是否已入沙箱(未入则自动导入),再调用 getSoundValue() 获取 'uri::' + fileUri.getUriFromPath(path) 格式的 sound 值,最终组装 NotificationRequest 并调用 notificationManager.publish() 发布。
12.3 学习提醒时间轴
@Builder
remindRow(item: RemindItem, idx: number) {
Row({ space: 10 }) {
// 时间列(固定宽 50,与行等高居中)
Column() {
Text(item.time).fontColor(item.on ? COLORS.blue : COLORS.text3)
Text(item.repeat)
}.width(50).height('100%')
// 时间轴:圆点+竖线(固定行高 72 内 layoutWeight 填满)
Column() {
Column().width(10).height(10).borderRadius(5)
.backgroundColor(item.on ? COLORS.blue : COLORS.text3)
Column().layoutWeight(1).width(2).backgroundColor(COLORS.line)
}.height('100%')
// 提醒卡:事项+Toggle 开关
Row() {
Column() {
Text(item.title).fontWeight(FontWeight.Bold)
Text(item.on ? '已开启 · 到点提醒' : '已暂停 · 不打扰')
.fontColor(item.on ? COLORS.green : COLORS.text3)
}
Toggle({ type: ToggleType.Switch, isOn: item.on })
.onChange((isOn) => { this.toggleRemind(idx, isOn); })
}
}
.height(72)
}
时间轴行采用"时间列 + 圆点竖线 + 提醒卡"三列布局,固定行高 72px。圆点颜色随 on 状态变化——开启时海岸蓝、关闭时雾蓝灰。竖线以 layoutWeight(1) 填满圆点到底部的剩余高度,形成连续的时间轴视觉效果。右侧提醒卡内嵌 Toggle 开关,切换时调用 toggleRemind() 更新 on 字段并整体刷新数组引用(slice())驱动列表重绘。行高固定为 72px 是关键设计——竖线在固定行高内 layoutWeight 填满才能保证每行竖线等长,避免弹性高度导致竖线长度不一致。
12.4 通知历史
通知历史以简单卡片列表呈现已发布通知的标题、正文和时间,每次 publishNotice() 成功后通过 unshift 置顶新增记录并刷新数组引用。
十三、铃音 Tab 详解
铃音 Tab(tabRing())由当前默认铃声状态卡、正弦波铃声生成器和铃声库列表三个区块组成。
13.1 当前默认铃声状态卡
Column({ space: 8 }) {
Row() {
Text('🎵')
Column() {
Text('当前默认铃声').fontWeight(FontWeight.Bold)
Text(ring.file + ' · ' + ring.size).fontFamily('monospace')
}
Text(ring.inSandbox ? '已入沙箱' : '未生成')
.fontColor(ring.inSandbox ? COLORS.green : COLORS.orange)
}
Text('通知发布时 sound 字段读取该铃声的沙箱 uri(EL1 files 目录)')
}
该卡片展示当前默认铃声(由 currentRingIdx 指定)的文件名、大小和沙箱状态。文件名和大小以等宽字体呈现,沙箱状态以绿色"已入沙箱"或橙色"未生成"标签呈现。底部说明文字点明 sound 字段与沙箱路径的关联关系。
13.2 正弦波铃声生成器
Column({ space: 10 }) {
Text('🎛 正弦波铃声生成器').fontWeight(FontWeight.Bold)
// 频率四档选择
Column() {
Text('基频(Hz)')
Row() {
ForEach(RING_FREQ_PRESETS, (freq) => {
Text(freq.toString() + ' Hz')
.backgroundColor(this.genFreq === freq ? COLORS.blue : COLORS.chip)
.onClick(() => { this.genFreq = freq; })
})
}
}
// 时长三档选择
Column() {
Text('时长(ms)')
Row() {
ForEach(RING_DURATION_PRESETS, (dur) => {
Text(dur.toString() + ' ms')
.backgroundColor(this.genDuration === dur ? COLORS.blue : COLORS.chip)
.onClick(() => { this.genDuration = dur; })
})
}
}
// 生成按钮
Row() {
Text('生成到沙箱 EL1').backgroundColor(COLORS.blue)
.onClick(() => { this.createRingByGen(); })
Text('已落盘 ' + this.sandboxCount + ' 个')
}
}
生成器提供频率(440/660/880/1320 Hz 四档)和时长(600/1200/2000 ms 三档)两个维度的参数选择,选中态以海岸蓝底白字呈现。点击"生成到沙箱 EL1"按钮调用 createRingByGen() 方法,该方法以当前 genFreq 和 genDuration 参数创建一个新的 RingItem,追加到铃声库列表尾部,然后调用 importRingToSandbox() 将 WAV 字节写入沙箱 EL1 目录。右侧"已落盘 N 个"实时展示沙箱中的铃声总数。
13.3 铃声库列表
@Builder
ringRow(item: RingItem, idx: number) {
Row({ space: 10 }) {
// 频率图标块
Column() {
Text('🎶')
Text(item.freq.toString()).fontColor(COLORS.blueD)
}.backgroundColor(COLORS.chip)
// 名称+文件/大小
Column() {
Text(item.name).fontWeight(FontWeight.Bold)
Row() {
Text(item.file).fontFamily('monospace')
Text(item.size)
}
}
// 操作按钮
Column() {
if (item.inSandbox) {
Text('已入沙箱').fontColor(COLORS.green)
} else {
Text('生成到沙箱').fontColor(COLORS.blue)
.onClick(() => { this.importRingToSandbox(idx); })
}
if (this.currentRingIdx === idx) {
Text('默认中 ✓').backgroundColor(COLORS.blue)
} else {
Text('设为默认')
.onClick(() => { this.setCurrentRing(idx); })
}
}
}
.border({ width: 1, color: this.currentRingIdx === idx ? COLORS.blue : COLORS.line })
}
铃声库行采用三列布局:左侧频率图标块(🎶图标+频率数字,浅云蓝底)、中部名称+文件信息(名称粗体、文件名等宽字体+大小文本)、右侧操作按钮区。操作按钮根据状态条件渲染——已入沙箱显示绿色"已入沙箱"标签,未入沙箱显示蓝色"生成到沙箱"可点击按钮;当前默认铃声显示蓝色"默认中 ✓"标签,其余显示"设为默认"可点击按钮。整行边框颜色随是否为当前默认铃声变化——默认铃声以海岸蓝边框高亮,其余以浅雾线边框呈现。
十四、我的 Tab 详解
我的 Tab(tabMine())由备考身份渐变大卡、错题趋势柱状图和错题本清单三个区块组成。
14.1 备考身份渐变大卡
Column({ space: 10 }) {
Row() {
Text('🧑🎓').fontSize(26)
Column() {
Text('陈同学 · 26 届考研人').fontSize(15).fontColor(COLORS.white)
Text('目标:华东师范大学 · 软件工程').fontColor(COLORS.whiteSoft)
}
Text('LV.6').backgroundColor(COLORS.white).fontColor(COLORS.blueD)
}
Row() {
Column() { Text('216 天').fontColor(COLORS.white); Text('累计备考') }
Column().width(1).height(24).backgroundColor(COLORS.trackW)
Column() { Text('1344 题').fontColor(COLORS.white); Text('累计刷题') }
Column().width(1).height(24).backgroundColor(COLORS.trackW)
Column() { Text('213 题').fontColor(COLORS.white); Text('待复盘错题') }
}
Progress({ value: 68, total: 100, type: ProgressType.Linear })
.color(COLORS.white).backgroundColor(COLORS.trackW)
Text('总体备考进度 68% · 冲刺阶段').fontColor(COLORS.whiteSoft)
}
.linearGradient({ angle: 130, colors: [[COLORS.blueD, 0], [COLORS.blue, 1]] })
渐变大卡使用 130 度两段渐变——深海蓝到海岸蓝——比头部 Banner 的三段渐变更简洁,突出个人身份的"沉浸感"。卡片分三层:第一层为身份信息行(Emoji 头像+姓名目标+等级标签),第二层为三列统计数据行(累计备考天数/累计刷题量/待复盘错题数),列间以 trackW 半透白竖线分隔;第三层为总体备考进度条(白色填充+半透白轨道)和进度说明文字。所有文字均使用白色系字段(white/whiteSoft/trackW),与头部 Banner 的白色系处理方式一致。
14.2 错题趋势传统柱状图
@Builder
chartCard() {
Column({ space: 10 }) {
Row() {
Text('📉 近 6 个月错题数').fontWeight(FontWeight.Bold)
Text('持续下降 = 进步').fontColor(COLORS.green)
}
Row({ space: 6 }) {
ForEach(MISTAKE_VAL, (val, idx) => {
Column() {
Text(val.toString())
.fontColor(idx === MISTAKE_VAL.length - 1 ? COLORS.green : COLORS.blueD)
// 柱体:高度随错题量与呼吸动画波动
Column()
.width(20)
.height(Math.max(8, val * (this.breath ? 1.0 : 0.92)))
.linearGradient({ angle: 90, colors: [[COLORS.blue, 0.1], [COLORS.blueD, 1]] })
Text(MONTH_LABELS[idx])
}
})
}.alignItems(VerticalAlign.Bottom)
}
}
与题库 Tab 的 Canvas 柱状图不同,我的 Tab 的错题趋势图采用 Column + ForEach 的传统方式构建。每根柱体是一个 Column 组件,高度由 MISTAKE_VAL 数据值乘以呼吸系数(breath 为 true 时 1.0、false 时 0.92)计算得出,最小高度 8 兜底避免零值消失。柱体使用 90 度垂直渐变(海岸蓝上端→深海蓝下端)填充,borderRadius 仅设顶部圆角。值得注意的是最后一根柱体(最近月份)的数值标注使用海藻绿着色,其余使用深海蓝——绿色表示"最新月份错题最少=进步最大",形成视觉上的积极暗示。整行以 VerticalAlign.Bottom 对齐,保证柱体底部齐平。
14.3 错题本清单
ForEach(this.mistakeList, (item: MistakeItem) => {
Row({ space: 10 }) {
// 左侧科目色条
Column().width(4).height(34).borderRadius(2)
.backgroundColor(subjectColor(item.subject))
// 题目+错因
Column({ space: 4 }) {
Text(item.question).fontSize(11)
Row() {
Text(item.subject).backgroundColor(COLORS.chip)
Text(item.reason).fontColor(COLORS.orange)
Text(item.time)
}
}
}
})
错题本清单行采用左侧色条+右侧信息的简洁布局。左侧 4px 宽色条以科目主题色着色,高度 34px 圆角 2px。右侧上方为题目摘要(单行截断),下方为科目标签+错因标签+收录时间的横排——科目标签以浅云蓝胶囊底呈现,错因标签以珊瑚橙文字呈现(突出错误原因),收录时间以雾蓝灰弱化文字右对齐。keyGenerator 使用题目摘要文本,保证列表稳定。
十五、底部 Tab 栏
@Builder
tabBar() {
Row() {
ForEach(TAB_LIST, (t: TabMeta, idx: number) => {
Column({ space: 3 }) {
Text(t.icon).fontSize(17)
.opacity(this.currentTab === idx ? 1 : 0.65)
Text(t.label).fontSize(9)
.fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
.fontWeight(this.currentTab === idx ? FontWeight.Bold : FontWeight.Normal)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.onClick(() => { this.currentTab = idx; })
})
}
.backgroundColor(COLORS.card)
.border({ width: { top: 1 }, color: COLORS.line })
}
底部导航栏采用六 Tab 等宽单排布局(每个 layoutWeight(1)),选中态与非选中态通过三重视觉差异区分:图标透明度(1 vs 0.65)、标签颜色(海岸蓝 vs 雾蓝灰)、标签粗细(粗体 vs 常规)。顶部以 1px 浅雾线边框与内容区分隔,整体白底呈现。点击任意 Tab 即切换 currentTab 状态值,触发内容区的 if-else 链重新渲染对应 Tab 布局。keyGenerator 使用 Tab 标签文本,确保导航栏不会因状态变化而重建。
十六、弹窗系统
弹窗系统由一个共享遮罩层和三个功能弹窗组成。
16.1 全屏遮罩层
@Builder
modalOverlay(onClose: () => void) {
Stack() {
Column().width('100%').height('100%').backgroundColor(COLORS.mask)
}
.alignContent(Alignment.Center)
.onClick(() => onClose())
}
遮罩层以 rgba(34,48,60,0.5) 半透深海墨色覆盖全屏,点击任意区域调用 onClose 回调关闭弹窗。Stack 容器的 alignContent(Alignment.Center) 使遮罩层内嵌的弹窗面板居中显示。三个功能弹窗都在 Stack 中先调用 modalOverlay(onClose) 铺底,再叠加自己的面板 Column。
16.2 新增真题弹窗
@Builder
panelAdd(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column({ space: 12 }) {
Text('新增真题试卷')
// 科目输入
TextInput({ text: this.formSubject, placeholder: '如:考研数学一' })
// 年份卷输入
TextInput({ text: this.formYear, placeholder: '如:2026 卷' })
// 题量输入
TextInput({ text: this.formCount, placeholder: '如:23 题' })
// 完成度滑杆
Slider({ value: this.formDone, min: 0, max: 100, step: 5 })
.onChange((value) => { this.formDone = value; })
// 取消/保存按钮
Row() {
Text('取消').onClick(() => onClose())
Text('保存').onClick(() => { this.savePaper(); })
}
}
.width('80%').backgroundColor(COLORS.card).borderRadius(14)
}
}
新增弹窗包含四个表单字段:科目(文本输入)、年份卷(文本输入)、题量(文本输入)和完成度(Slider 滑杆,0~100 步进 5)。滑杆的 blockColor、trackColor、selectedColor 均使用主题色配置。保存按钮调用 savePaper() 方法——科目为空时兜底"考研数学一"、年份为空时兜底"2026 卷"、题量为空时兜底"23 题",以 unshift 将新条目置顶并整体刷新数组引用,随后清空表单并关闭弹窗。
16.3 编辑完成度弹窗
@Builder
panelEdit(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column({ space: 12 }) {
Text('编辑完成度')
Text(this.paperList[this.editIdx].subject + ' · ' + this.paperList[this.editIdx].year)
Slider({ value: this.editDone, min: 0, max: 100, step: 5 })
.onChange((value) => { this.editDone = value; })
Row() {
Text('取消').onClick(() => onClose())
Text('保存').onClick(() => { this.updatePaper(); })
}
}
}
}
编辑弹窗仅包含一个完成度滑杆。打开时通过 openEditPaper(idx) 方法回填当前真题的完成度到 editDone 状态变量,展示当前编辑的科目和年份卷信息。保存调用 updatePaper() 方法将 editDone 写入 paperList[editIdx].done 并刷新数组引用。
16.4 删除确认弹窗
@Builder
panelDel(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column({ space: 14 }) {
Text('🗑').fontSize(30)
Text('删除这套真题试卷?')
Text(this.paperList[this.delIdx].subject + ' · ' + this.paperList[this.delIdx].year)
Row() {
Text('取消').onClick(() => onClose())
Text('删除').backgroundColor(COLORS.orange)
.onClick(() => { this.delPaper(); })
}
}
.width('74%').backgroundColor(COLORS.card).borderRadius(14)
}
}
删除弹窗以垃圾桶 Emoji 大图标+确认问句+待删条目信息构成简洁的确认对话框。删除按钮使用珊瑚橙底色(区别于新增/编辑弹窗的蓝色保存按钮),语义上与"删除"这一危险操作匹配。确认后调用 delPaper() 方法通过 splice 删除指定索引的条目并刷新数组引用。
十七、功能模块对比表
| 对比维度 | 题库 Tab | 网页 Tab | 下载 Tab | 提醒 Tab | 铃音 Tab | 我的 Tab |
|---|---|---|---|---|---|---|
| 核心功能 | 真题试卷管理与刷题统计 | 真题检索与下载触发 | 下载溯源记录展示 | 学习提醒与通知发布 | 铃声生成与沙箱落盘 | 备考全貌与错题管理 |
| 主要组件 | chips+统计卡+Progress+Canvas | TextInput+Web+Progress | Progress+ForEach | Toggle+时间轴+ForEach | ForEach+条件渲染 | 渐变卡+ForEach柱状图 |
| 特性承载 | Canvas 柱状图呼吸动画 | ArkWeb 下载代理四回调 | 双 URL 溯源展示 | Notification 授权与发布 | 沙箱 EL1 音频落盘 | 传统柱状图呼吸动画 |
| 数据模型 | PaperItem | DownloadRecord | DownloadRecord | RemindItem+NoticeLog | RingItem | MistakeItem |
| 交互方式 | 点击编辑/长按删除 | 地址栏输入+按钮触发 | 纯展示 | Toggle 开关+按钮发布 | 参数选择+按钮生成 | 纯展示 |
| 色彩重点 | 五科目五色区分 | 海岸蓝主色 | 双 URL 双色(蓝+灰蓝) | 绿/橙授权态 | 蓝/绿沙箱态 | 渐变蓝白身份卡 |
| 弹窗关联 | 新增/编辑/删除真题 | 无 | 无 | 无 | 无 | 无 |
| 动画联动 | Canvas 柱高呼吸波动 | 无 | 无 | 无 | 无 | Column 柱高呼吸波动 |
| 布局特点 | 横滚chips+纵向清单+图表 | 纵向四区+Web占位 | 进度卡+记录列表 | 授权卡+时间轴+历史 | 状态卡+生成器+列表 | 渐变大卡+图表+清单 |
深化解析:从代码结构到业务闭环
布局方式与数据流
备考页面沿着筛选试卷、下载资料、安排提醒、执行练习和复盘错题展开。题库模型保存科目与完成度,网页和下载代理负责获取资料并记录来源,通知与铃声把计划变为可执行提醒。分析时应说明进度条、柱状图、时间轴和下载记录各自回答什么问题,并检查列表更新后统计值是否同步。
页面根结构通常由头部、内容区和底部 Tab 栏组成。头部负责展示当前业务状态,内容区根据索引选择不同的 @Builder,底部导航负责修改索引。这样的结构把“当前显示什么”收敛为一个明确状态:用户点击 Tab 后先更新索引,ArkUI 再重新计算相关分支。各个 Builder 虽然共享主题色和页面级数据,却可以采用完全不同的布局方式;高密度列表适合纵向 Scroll,概览数据适合横向统计卡或双列 Flex,实时预览类组件需要独占有界高度,历史事件则适合时间轴或固定行高 List。
数据模型层承担界面与业务之间的契约。使用 @Observed 的实体保存可编辑字段,页面级 @State 数组负责驱动 ForEach。新增时创建新实体并插入数组,编辑时修改目标实体,删除时移除对应项。为了让列表差分稳定,key 应来自不会改变的唯一标识,不宜使用标题等可编辑字段。统计数字、完成比例和分类数量属于派生信息,可以从数组即时计算,避免同时维护两份状态后出现卡片已经更新、图表仍显示旧值的情况。
弹窗表单使用独立缓存是必要的。打开新增弹窗时清空缓存,打开编辑弹窗时复制目标字段,用户确认后才写回正式模型。这样点击取消不会污染列表数据。若直接把 TextInput 双向绑定到列表实体,用户尚未保存时卡片就可能跟着变化,破坏“确认提交”的交互语义。删除弹窗还需要保存目标索引或唯一标识,并在确认时再次校验目标存在,避免列表变化后误删其他项。
核心代码与状态驱动机制
@State 的价值不是简单替代普通变量,而是建立状态与界面之间的依赖关系。当前 Tab、筛选条件、动画开关、弹窗显隐、下载进度或能力状态发生变化时,只有读取这些变量的组件需要刷新。代码段中连续的修饰器调用分别控制尺寸、间距、背景、字体和事件,它们共同构成声明式描述;阅读时应从容器方向、子项分布、状态绑定和交互回调四个层面理解,而不是逐个孤立翻译属性名称。
ForEach 负责把数组映射为重复 UI。回调中的 item 提供业务字段,index 适合显示顺序,但不适合作为长期身份。列表发生新增或删除时,稳定 key 可以让框架复用未变化节点,减少重建。若直接修改对象属性后界面没有按预期刷新,可在保持实体身份的前提下替换数组引用;但不应为了刷新把所有元素都重新构造,否则会增加无意义渲染并丢失局部状态。
条件渲染体现了页面状态机。空闲时展示引导,准备中展示进度,成功时展示结果,失败时展示原因和重试入口。相比一个布尔值,四态文案更能覆盖异步能力。系统接口调用前先检查权限、设备支持和会话状态,调用后再读取结果校验。异常处理除了记录错误码,还要把可理解的反馈写入响应式状态,让用户知道失败发生在哪一步。
动画效果与颜色使用策略
呼吸动画通常由定时器周期翻转 breath,再把该状态映射为透明度、柱高或圆点半径的小幅变化。它适合表达“正在运行”或让统计图保持生命感,但幅度应克制,不能改变核心数据含义。柱状图的基础高度仍由真实数值计算,动画只能在很小范围内偏移;进度环的角度仍由完成比例决定,不能为了视觉效果显示超过真实进度的结果。页面离开时必须清理定时器,避免后台继续刷新。
颜色常量应按语义使用。主色承担选中态和主要操作,辅助色突出数据或次级动作,绿色表达完成与可用,橙色表达进行中或需要注意,红色只用于失败、逾期和删除等高风险场景。弱文本与分割线降低视觉权重,遮罩色用于聚焦弹窗。颜色不能成为唯一的状态信息,还要配合文字、图标或进度值,保证色觉差异用户也能理解。
渐变更适合头部大卡、核心指标或柱状图,不宜在每个小元素上重复使用。深色主题要检查正文与卡片背景的对比度,浅色主题则要避免辅助文字过淡。选中和未选中 Tab 除颜色差异外,还可以通过字重、图标透明度或底部指示器区分。这样既保持主题统一,又能建立清晰的信息层级。
各 Tab 之间的交互联动
各 Tab 不应只共享一个导航索引,还应围绕业务对象建立必要联动。列表页新增或编辑数据后,头部计数、图表和个人统计要同步更新;网页或地图产生的结果应写入记录模型,供下载、日志或我的页面继续展示;通知、字幕、相机等系统能力的状态应在头部胶囊或对应 Tab 中保持一致。跨 Tab 跳转时先更新必要参数,再修改当前索引,可以避免目标页面读取到旧条件。
切换离开重型组件时需要处理资源边界。相机输入、地图监听、字幕控制器、Web 下载代理和定时器都不能只创建不释放。可以在统一的 switchTab 方法中判断来源与目标,离开能力页时解除监听或停止会话;页面销毁时再执行兜底释放。释放方法应允许重复调用,并对每个资源独立判空,确保一次异常不会阻止后续清理。
交互反馈要覆盖成功与失败。按钮点击后先进入处理中状态并防止重复提交;成功后更新模型、关闭弹窗并显示结果;失败后保留用户输入,展示错误原因和重试入口。权限拒绝、能力不支持、网络失败、文件不存在和输入非法都属于正常业务分支。通过状态卡或行内提示展示这些分支,比只在控制台打印更符合完整产品体验。
边界场景与验证思路
空列表时应显示占位说明和新增入口,不能只留下空白。长标题需要限制行数并使用省略号,数字字段需要限定上下界,文本提交前要去除首尾空格。筛选后无结果应保留清除条件的入口。删除最后一项后,当前选择索引要回退到有效范围。异步搜索连续触发时,应防止较早请求晚返回后覆盖新结果。
验证数据链路时,可以依次检查新增、编辑、删除和筛选:新增后列表条数、统计数字和图表是否同时变化;编辑取消后正式数据是否保持不变;删除后 ForEach key 是否稳定;切换 Tab 再返回时必要数据是否仍在。验证系统能力时分别模拟支持、拒绝和异常,确认界面都有明确状态。验证动画时检查页面离开后是否停止,低性能设备上是否仍保持流畅。
视觉验收需要检查不同屏幕宽度、系统字体放大、深浅背景对比和长文本换行。表格中的布局方式、模型、字段数、核心操作、动画、状态颜色、数据量和特殊组件应与正文一致。Mermaid 图则需要对应真实的数据流和能力链路,节点文字加引号以避免中文或特殊字符导致解析失败。
组件化设计的进一步理解
参数化 Builder 适合抽取重复的统计格、状态行、标签和按钮组。参数只传入渲染所需数据和事件,不让子构建器直接依赖过多页面变量,可以降低耦合。业务复杂后,可把模型与系统能力封装为独立控制器,页面只负责组合 UI 和响应状态。这样既保留声明式代码的直观性,也能让权限、错误码翻译和资源释放得到集中管理。
当前单页面集中展示完整源码,便于博文逐段讲解。若演进为正式项目,可以按领域拆分组件:导航和页面框架位于容器层,列表、图表和弹窗位于展示层,数据读写和 Kit 接入位于服务层。组件之间通过参数、回调、@Link 或 @ObjectLink 传递状态,不使用全局变量代替清晰的数据流。
性能优化首先来自减少不必要刷新。派生数据不要重复存储,动画状态不要进入列表 key,长列表使用稳定标识,Canvas 只在数据或尺寸变化时重绘。其次是控制资源生命周期,页面不可见时停止高成本任务。最后才是微调阴影、渐变和绘制细节。这样的优先级能保证页面在功能增加后仍然可维护。
通过以上补充,可以看到 ArkUI 的声明式模式并非只让布局语法更简洁,它更重要的价值是把数据变化、界面刷新和交互反馈连接为可追踪链路。理解每个代码段读取什么状态、写入什么状态、影响哪些组件,才能真正掌握文章中多个 Tab、图表、弹窗和系统能力协同工作的原理。
十八、总结与展望
本平台以"题海灯塔"为品牌意象,以浅色海岸蓝白 + 珊瑚橙为视觉基调,在 HarmonyOS 6.1.1 上实现了三大前沿特性的同文件叠加融合。ArkWeb 下载代理的 WebDownloadDelegate 四回调体系配合 getOriginalUrl() 与 getReferrerUrl() 双接口,实现了真题下载来源的双重溯源——每份试卷都能追溯到文件直链地址和触发下载的页面地址,为备考资料的可信度提供了技术背书。Notification Kit 的 sound 字段支持沙箱 EL1 区域内的自定义音频路径,通过 'uri::' + fileUri.getUriFromPath(path) 拼接格式,使"开考铃""交卷铃"等备考场景铃声能够真正写入沙箱并随通知触达,告别千篇一律的系统默认铃声。Canvas 绘制的 drawBar() 渐变柱状图通过 createLinearGradient 垂直渐变和 breath 状态每秒翻转联动柱高微幅波动,将静态的刷题数据转化为有生命力的"呼吸"可视化。
在架构层面,六个 Tab 各自承载完全不同的布局风格和功能职责——题库的横滚 chips 配 Canvas 图表、网页的地址栏配 Web 组件、下载的进度卡配双 URL 记录、提醒的时间轴配 Toggle 开关、铃音的参数生成器配铃声库列表、我的的渐变大卡配传统柱状图——但所有状态变量统一声明在组件顶层实现跨 Tab 共享,如 dlState 在网页 Tab 写入、在下载 Tab 读取展示,currentRingIdx 在铃音 Tab 设置、在提醒 Tab 读取并填入通知 sound 字段。这种"特性分散、状态集中"的设计在保持视觉独立性的同时确保了数据流的清晰可追溯。
弹窗系统采用共享 modalOverlay 遮罩层 + 三个独立面板的分层架构,通过条件渲染(if (this.addModal))控制显隐,onClose 回调函数实现面板与遮罩的同步关闭。新增弹窗的表单字段空值兜底策略(科目为空时兜底"考研数学一")体现了防御性编程思维,避免用户提交不完整数据。@Observed 装饰器配合数组引用整体刷新(slice() 拷贝)的驱动模式,使数据变更到视图刷新的链路简洁高效。
展望未来,本平台可在以下方向持续演进:一是引入持久化存储,将真题清单、下载记录和错题本数据持久化到关系型数据库,避免页面销毁后数据丢失;二是接入真实的 PDF 预览能力,在下载 Tab 中增加在线预览真题试卷的组件;三是利用 Canvas 的 drawRadar 方法扩展表达力五维雷达图,将错题按知识点维度进行可视化分析;四是将铃声生成器扩展为可视化波形编辑器,允许用户在频域上绘制自定义铃声波形;五是接入 AI 能力实现智能备考规划推荐,根据刷题量和错题趋势自动调整学习提醒频率和优先级。这些方向的探索将进一步推动备考类应用从"工具"向"智能陪练"的跃迁。
附录:DevEco Studio 创建新项目与查看 SDK 版本
本章节演示如何使用 DevEco Studio 创建一个 HarmonyOS 新项目,并查看当前 IDE 已安装的 SDK 版本,适合作为其他技术博文的补充操作指南。
一、创建新项目
1.1 进入欢迎界面
启动 DevEco Studio 后,首先看到的是欢迎界面。左侧导航栏默认选中 “项目”,右侧提供三个主要入口:
- 新建项目:从头创建新项目
- 打开项目:打开本地已有项目
- 克隆仓库:从 Git 等版本控制拉取代码
点击 “新建项目” 按钮,进入项目创建向导。

1.2 选择项目模板
在弹出的"新建项目"对话框中,左侧分类标签提供了两种项目类型:
| 类型 | 说明 |
|---|---|
| 应用(Application) | 开发标准的 HarmonyOS 应用,具备完整的 Ability 生命周期 |
| 元服务(Atomic Service) | 开发轻量级的原子化服务,无需安装即可使用 |
选择 “应用” 标签后,右侧展示多种模板。对于大多数场景,推荐选择 “Empty Ability” —— 这是一个最基础的入门模板,仅包含 Hello World 功能,适合从零开始构建应用。

1.3 配置项目信息
点击 “下一步” 后,进入项目配置界面,需要填写以下核心参数:
| 配置项 | 示例值 | 说明 |
|---|---|---|
| 项目名称(Project name) | rollboat |
应用的项目名称,建议使用英文命名 |
| 包名(Bundle name) | com.rollboat.myapplication |
应用唯一标识,采用反向域名格式 |
| 保存路径(Save location) | D:\CodeFactory\rollboat |
项目本地存储路径,避免使用中文和空格 |
| 兼容 SDK(Compatible SDK) | 6.1.1(24) |
目标 HarmonyOS API 版本,点击"查看参考"可了解各版本差异 |
| 模块名称(Module name) | entry |
主模块名称,默认 entry 为应用入口模块 |
| 设备类型(Device types) | ☑ Phone | 勾选目标设备:Phone / Tablet / 2in1 / Car / Wearable / TV |
右侧预览区会实时展示当前模板的默认效果 —— 一个居中显示的 “Hello World” 文本。

1.4 完成创建
确认配置无误后,点击右下角 “完成” 按钮,IDE 将自动执行以下操作:
- 生成项目骨架(Stage 模型目录结构)
- 执行
ohpm install安装依赖 - 运行 Hvigor 构建初始化(
Build Init)
构建日志中显示 “退出代码为 0” 表示项目初始化成功。

1.5 项目结构概览
创建完成后,左侧项目面板展示的是标准的 Stage 模型 目录结构:
rollboat/
├── .hvigor/ # Hvigor 构建工具缓存
├── .idea/ # IDE 配置文件
├── AppScope/ # 应用级全局配置
│ └── app.json5
├── entry/ # 主模块(入口模块)
│ ├── src/main/ets/
│ │ ├── entryability/ # Ability 生命周期管理
│ │ │ └── EntryAbility.ets
│ │ └── pages/ # UI 页面
│ │ └── Index.ets # 首页(默认 Hello World)
│ ├── src/main/resources/ # 资源文件
│ ├── module.json5 # 模块配置
│ └── build-profile.json5 # 构建配置
├── oh_modules/ # OHPM 依赖包
├── build-profile.json5 # 工程构建配置
├── hvigorfile.ts # Hvigor 构建脚本
└── oh-package.json5 # 包管理配置
核心文件 Index.ets 的默认代码如下,采用 ArkTS 声明式 UI 语法:
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
RelativeContainer() {
Text(this.message)
.id('HelloWorld')
.fontSize($r('app.float.page_text_font_size'))
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.onClick(() => {
this.message = 'Welcome';
})
}
.height('100%')
.width('100%')
}
}
| 关键语法 | 作用 |
|---|---|
@Entry |
标记为页面入口,可用于路由跳转 |
@Component |
声明为自定义组件 |
@State |
状态变量,数据变更时自动触发 UI 刷新 |
RelativeContainer |
相对布局容器,替代传统线性布局 |
.onClick() |
点击事件,此处点击后文本变为 “Welcome” |
打开右侧 Previewer(预览器),选择 Phone 设备,即可实时预览 Hello World 效果,无需连接真机或启动模拟器。

二、查看 SDK 版本
2.1 查看 HarmonyOS SDK
DevEco Studio 安装时已内置 HarmonyOS SDK,无需单独下载。通过以下路径查看:
文件 → 设置 → HarmonyOS SDK(或快捷键
Ctrl + Alt + S搜索 “HarmonyOS SDK”)
在设置面板中,可以看到当前已安装的 SDK 版本信息:
| 名称 | 阶段 | 状态 |
|---|---|---|
| HarmonyOS 6.1.1 | Release | ✅ 已安装 |
界面顶部提示:“HarmonyOS SDK 已经包含在 IDE,无需单独安装”,省去了手动配置 SDK 的繁琐步骤。

2.2 查看 ArkUI-X SDK(跨平台扩展)
如果项目需要将 ArkUI 框架扩展到多个 OS 平台(Android / iOS / OpenHarmony),还需要配置 ArkUI-X SDK。路径如下:
文件 → 设置 → 语言和框架 → ArkUI-X
在这里可以查看已安装和可选的 ArkUI-X SDK 版本:
| 版本 | SDK 版本号 | 阶段 | 状态 |
|---|---|---|---|
| API Version 24 | 6.1.1.100 | Release | ✅ 已安装 |
| API Version 23 | 6.1.0.28 | Beta1 | 未安装 |
| API Version 22 | 6.0.2.112 | Release | 未安装 |
安装路径示例:D:\DevTools\ArkUI-X\sdk
说明:ArkUI-X 允许开发者使用一套 ArkTS 主代码,同时构建多平台应用。如果仅开发 HarmonyOS 原生应用,无需额外安装 ArkUI-X SDK。

三、小结
| 步骤 | 操作 | 关键点 |
|---|---|---|
| 创建项目 | 欢迎页 → 新建项目 → 选择 Empty Ability 模板 → 配置项目信息 → 完成 | 使用 Stage 模型 + ArkTS 语言 |
| 查看 SDK | 设置 → HarmonyOS SDK | SDK 已内置,无需手动安装 |
| 跨平台扩展 | 设置 → ArkUI-X | 根据需要安装对应 API 版本 |
至此,DevEco Studio 的项目创建与 SDK 环境确认全部完成,可以开始 HarmonyOS 应用的功能开发。
本文基于 DevEco Studio 6.1.1 Release 版本编写,不同版本界面可能存在细微差异。
更多推荐



所有评论(0)