ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

【天体运行模拟|11】HarmonyOS ArkTS 收藏与笔记实战:同步知识页和个人学习记录

【天体运行模拟|11】HarmonyOS ArkTS 收藏与笔记实战:同步知识页和个人学习记录 学习型应用里“收藏”和“笔记”看起来只是两个列表真正难点却是让它们记住用户在哪个知识点、哪个实验场景下做了什么。如果收藏只保存实验 ID笔记只保存标题和正文那么数据虽然落到了本地学习上下文却断了用户从“圆轨道”知识页写下一条笔记回到“我的笔记”后无法重新打开原知识点收藏页能进入实验却不知道这次收藏源自哪段学习内容。“天体运行模拟”的真实源码已经完成两个可运行的基础闭环FavoritesPage.ets读取实验 ID映射到实验目录并跳回模拟页NotesPage.ets通过NoteEditorDialog新建笔记使用DataStore写入 Preferences。本文不把尚未实现的能力说成现状而是先复核这两条真实链路再给出一套面向 HarmonyOS 5.0 及以上版本的演进方案用稳定资源引用连接知识页、公式页、实验页与个人记录同时处理页面恢复、并发写入、坏数据、空状态和多设备布局。本文会解决四个具体问题收藏 ID 如何映射为可展示、可跳转的实验对象为什么页面同时在aboutToAppear与onPageShow刷新笔记模型怎样携带知识点和实验上下文而不复制整份正文如何把 Preferences 的“能存下来”升级为可校验、可迁移、可测试的学习记录仓库。版本基线应用版本1.0.0targetSdkVersion 6.0.2(22)compatibleSdkVersion 6.0.1(21)设备范围包含 phone、tablet 与 2in1。文中的工程方法面向 HarmonyOS 5.0 及以上版本API 细节以项目实际 SDK 为准。一、先看真实边界收藏实验笔记独立创建当前收藏页的状态不是Experiment[]的持久化副本而是一组实验 ID。页面恢复时先读取 ID再通过getAllExperiments()找回完整模型private async reload(): Promisevoid { const ids await DataStore.loadFavorites() const all getAllExperiments() const list: Experiment[] [] for (const id of ids) { const found all.find(e e.id id) if (found) { list.push(found) } } this.favorites list }这段实现有两个值得保留的设计持久层只保存稳定 ID展示数据仍由实验目录负责目录里不存在的旧 ID 会被忽略不会让整个列表崩溃。它也揭示了当前边界收藏对象是“实验”并不是“知识点”或“公式”。笔记链路则相对独立。页面保存的NoteItem只有五个字段interface NoteItem { id: string title: string content: string timestamp: string category: string }它能表达一条普通笔记却没有knowledgeId、experimentId或来源页面。因此“同步知识页和个人学习记录”是本文要完成的工程演进目标而不是对现有源码的夸大描述。二、两条真实运行链路收藏从实验页或实验列表发起。LabPage和ExperimentSimPage都调用同一组DataStore.loadFavorites()/saveFavorites()“我的收藏”页再把 ID 还原为实验并携带参数打开模拟页。router.pushUrl({ url: views/experiment/ExperimentSimPage, params: { expId: exp.id, expName: exp.name } })笔记从NotesPage的自定义弹窗发起。标题或正文为空时弹窗直接保留不调用保存回调输入合法时页面生成 ID 和日期把新记录插到数组头部再整体写回private async addNote( title: string, content: string, category: string ): Promisevoid { const now new Date() const item: NoteItem { id: n_ now.getTime(), title, content, timestamp: this.formatDate(now), category } this.notes [item, ...this.notes] await DataStore.saveNotesNoteItem(this.notes) }这里的“整体写回”对少量本地笔记很直接但会引出并发覆盖和模型迁移问题后文会分别处理。三、Preferences 为什么适合当前规模项目的DataStore使用kit.ArkData中的 Preferences并把数组序列化为 JSON 字符串static async saveFavorites(ids: string[]): Promisevoid { await DataStore.putString( favorite_experiments, JSON.stringify(ids) ) } static async loadFavorites(): Promisestring[] { const json await DataStore.getString( favorite_experiments, [] ) try { return JSON.parse(json) as string[] } catch { return [] } }收藏 ID、少量笔记、开关和统计快照都属于轻量键值数据Preferences 足够简单也符合当前离线应用的体量。若未来需要全文检索、按资源 ID 联表查询、数万条记录、复杂排序或迁移才应评估关系型数据库而不是因为“笔记”两个字就提前引入重型方案。数据特征当前选择何时需要升级少量实验 IDPreferences通常无需升级数十到数百条短笔记Preferences需要全文检索或复杂索引时结构化学习轨迹可先用版本化 JSON需要多条件统计和增量迁移时图片、附件文件目录 元数据不应把二进制塞进 Preferences四、页面恢复为何调用两次 reloadFavoritesPage和NotesPage都在两个回调中刷新aboutToAppear(): void { this.reload() } onPageShow(): void { this.reload() }首次创建页面时需要加载数据从模拟页或编辑流程返回时又要拿到最新收藏和笔记。这个策略能覆盖常见返回路径但两个异步读取可能在页面首次显示时重叠。数据量小的时候通常看不出问题工程化后最好增加请求序号避免慢请求覆盖新结果State private loading: boolean false private reloadVersion: number 0 private async reload(): Promisevoid { const version this.reloadVersion this.loading true const result await this.repository.listNotes() if (version ! this.reloadVersion) { return } this.notes result this.loading false }请求序号不负责取消 I/O只负责保证最后一次刷新拥有状态写入权。这对路由快速往返、窗口切换和多次生命周期触发都更稳。五、收藏应保存 ID不应复制实验对象直接持久化完整Experiment看似省去映射实际会复制名称、描述、分类、图标引用和参数定义。应用升级后旧副本可能与新目录不一致资源对象也不适合直接 JSON 化。更稳的持久化模型仍然是export interface FavoriteRecord { resourceType: experiment resourceId: string createdAt: number }相比现有string[]它只增加资源类型和时间戳却保留了“目录为权威数据源”的原则。时间戳可以支持最近收藏排序resourceType为未来知识点收藏留出边界。六、先为学习资源建立统一引用收藏、笔记和最近学习记录不应该各自发明跳转参数。可以先定义一个只描述身份的联合类型export type LearningResourceType | knowledge | formula | experiment export interface LearningResourceRef { type: LearningResourceType id: string }LearningResourceRef不保存页面 URL也不复制页面标题。它回答“这条记录属于谁”标题、摘要、图标、路由目标则由统一目录解析。这样知识内容改名时用户笔记仍能指向同一个稳定 ID。七、让笔记携带上下文而不是复制知识正文建议把笔记模型升级为显式版本并把来源资源设为可选export interface LearningNote { schemaVersion: 2 id: string title: string content: string category: string createdAt: number updatedAt: number source?: LearningResourceRef }独立笔记可以没有source从知识页、公式页或模拟页创建的笔记则带上稳定引用。不要把知识点全文复制进笔记否则原文更新后会产生两套互相冲突的内容。这个模型还修复了当前timestamp: string的一个限制展示日期适合 UI但排序和跨时区处理更适合数值时间戳。日期格式应在视图层生成。八、从知识页创建笔记的路由契约知识页只需要传资源身份和一个可选的建议标题interface NoteEditorParams { sourceType?: LearningResourceType sourceId?: string suggestedTitle?: string } router.pushUrl({ url: views/mine/NotesPage, params: { sourceType: knowledge, sourceId: knowledge.id, suggestedTitle: 学习${knowledge.title} } })页面接收参数后必须验证sourceType和sourceId不能直接相信路由输入。建议标题只是编辑体验不应成为身份字段。真正的关联始终由{ type, id }决定。九、资源目录集中负责解析和跳转页面不应散落if (type ...)。一个窄职责目录可以同时完成资源解析和路由构造export interface LearningResourceSummary { ref: LearningResourceRef title: string category: string route: string params: Recordstring, string } export class LearningCatalog { resolve(ref: LearningResourceRef): LearningResourceSummary | undefined { if (ref.type experiment) { const exp getAllExperiments() .find(item item.id ref.id) if (!exp) return undefined return { ref, title: exp.name, category: exp.category, route: views/experiment/ExperimentSimPage, params: { expId: exp.id, expName: exp.name } } } return this.resolveLearningContent(ref) } }目录负责把稳定 ID 解析为当前版本的标题和页面契约。笔记页只消费结果能解析就显示“查看来源”不能解析就显示“来源内容已不可用”而不是崩溃或跳到错误页面。十、仓库层统一读、写、校验当前页面直接调用DataStore小项目足够清晰。关联类型增加后可以在页面与 Preferences 之间加一个LearningRecordRepository让校验和迁移只有一个入口export class LearningRecordRepository { async listNotes(): PromiseLearningNote[] { const raw await DataStore.loadNotesobject() return raw .map(item this.migrateNote(item)) .filter((item): item is LearningNote item ! undefined) } async saveNotes(notes: LearningNote[]): Promisevoid { const normalized this.deduplicateNotes(notes) await DataStore.saveNotesLearningNote(normalized) } }页面负责交互状态仓库负责存储格式目录负责学习资源。三个边界分开后测试不需要启动完整 ArkUI 页面。十一、旧笔记迁移不能只靠类型断言JSON.parse(json) as T[]只告诉编译器“把它当作 T”不会校验磁盘数据。旧版笔记没有schemaVersion、createdAt和source需要显式迁移private migrateNote(raw: object): LearningNote | undefined { const value raw as Recordstring, string | number if (typeof value.id ! string || typeof value.title ! string || typeof value.content ! string) { return undefined } const fallbackTime Date.now() return { schemaVersion: 2, id: value.id, title: value.title, content: value.content, category: typeof value.category string ? value.category : 未分类, createdAt: typeof value.createdAt number ? value.createdAt : fallbackTime, updatedAt: typeof value.updatedAt number ? value.updatedAt : fallbackTime } }生产实现还可以解析旧YYYY-MM-DD字符串。重点不是补默认值而是让错误记录被识别、被隔离并为升级路径留下可复核的规则。十二、避免“读—改—写”覆盖现有收藏切换采用“读取数组、修改数组、整体保存”。如果两个页面几乎同时切换收藏后写入者可能覆盖先写入者。可以在仓库内部串行化写操作private writeChain: Promisevoid Promise.resolve() toggleFavorite(id: string): Promisevoid { this.writeChain this.writeChain.then(async () { const ids await DataStore.loadFavorites() const set new Set(ids) if (set.has(id)) { set.delete(id) } else { set.add(id) } await DataStore.saveFavorites([...set]) }) return this.writeChain }串行队列适用于单进程内的轻量 Preferences 写入。若未来出现跨进程、多端同步或云端合并就需要版本号、冲突策略或事务能力不能继续依赖内存队列。十三、按钮点击与父级点击要分清LabPage的实验卡片整体可点击右侧星标也可点击。实际设备上要验证点击星标时是否同时触发卡片跳转。更稳的 UI 结构是把收藏按钮放在独立命中区域并在交互测试中明确“收藏不跳转卡片才跳转”。Row() { ExperimentSummary(exp) .layoutWeight(1) .onClick(() this.openExperiment(exp)) Button(this.isFavorite(exp.id) ? ★ : ☆) .width(44) .height(44) .onClick(() this.toggleFavorite(exp.id)) }44vp 左右的触控区域比只给一个字符绑定点击更适合手机也兼顾平板和 2in1 的指针操作。十四、保存笔记要有进行中和失败状态当前NoteEditorDialog调用onSave后立即关闭而onSave的类型是同步void。真实持久化是异步的如果写入失败用户会误以为已经保存。可以把回调改为返回结果export interface SaveNoteResult { ok: boolean message?: string } onSave: ( title: string, content: string, category: string ) PromiseSaveNoteResult弹窗增加saving状态保存中禁用按钮成功后关闭失败时保留输入并显示错误。这样数据可靠性不是藏在日志里而是成为用户能理解的页面状态。十五、删除笔记不能先乐观消失再静默失败现有代码先过滤this.notes再保存而DataStore.putString()捕获异常后不向上传递。若落盘失败列表当次看起来已删除重新进入又会出现。可采用“保存成功后提交 UI”private async removeNote(id: string): Promisevoid { const next this.notes.filter(note note.id ! id) const result await this.repository.replaceNotes(next) if (!result.ok) { this.errorMessage 删除失败请重试 return } this.notes next }数据量很小时这种保守策略足够直观。若选择乐观更新也必须在失败时恢复旧数组并提示用户。十六、空状态要把用户带回有效入口当前两个空状态的文案清晰但没有动作。收藏为空时可以提供“去实验地图”笔记为空时可以提供“浏览知识点”或“新建笔记”。目标必须与按钮文案一致Button(浏览知识点) .onClick(() { router.pushUrl({ url: views/learning/KnowledgeListPage }) })空状态不是装饰页而是恢复用户任务的最短路径。不要把“去学习”绑定到默认模拟场景否则文案和行为仍然脱节。十七、加载、空、错误、内容应当互斥只用notes.length 0无法区分“还没读完”和“确实为空”。建议显式建模type PageState loading | empty | content | error State pageState: PageState loading State errorMessage: string 读取成功后根据数组长度进入empty或content解析失败、初始化失败进入error错误状态提供重试。这样首次加载不会短暂闪出“暂无笔记”。十八、AppStorage 只放统计快照不放完整记录项目在收藏写入后更新favorite_count并通过stats_version通知统计页刷新。这种做法适合跨页面展示计数AppStorage.setOrCreatenumber( favorite_count, ids.length )不要把完整笔记数组或实验模型塞进AppStorage。完整数据仍由仓库和 Preferences 管理AppStorage只承载需要被多个页面即时观察的轻量快照避免形成两个权威数据源。十九、多设备布局列表能伸缩操作必须可达当前页面根容器使用width(100%)、height(100%)列表用layoutWeight(1)并读取状态栏和底部导航栏高度。这为 phone、tablet、2in1 提供了基础。进一步检查时要关注平板宽屏不要把单条笔记拉成过长行可限制内容最大宽度2in1 窗口缩窄后分类标签应换行或横向滚动“新建笔记”按钮底部至少保留系统避让区域长标题使用maxLines与省略号正文允许两到三行编辑删除按钮需要稳定宽度不能挤压标题到不可读。Column() { this.NoteList() } .width(100%) .constraintSize({ maxWidth: 840 }) .alignSelf(ItemAlign.Center)固定的是内容阅读宽度不是窗口宽度。这样大屏上更易读小窗中仍可自然收缩。二十、深浅色和对比度不能绕过主题令牌页面主体使用AppColors是正确方向但NoteEditorDialog仍包含#F5F7FA、#F0F0F0、#F2F2F2等硬编码浅色。系统切换深色模式时这些输入区和按钮可能与文本令牌冲突。建议补齐语义颜色export class AppColors { static readonly INPUT_BG: Resource $r(app.color.input_background) static readonly MUTED_ACTION_BG: Resource $r(app.color.muted_action_background) }浅色、深色资源使用同名 token页面不需要判断颜色模式。正文文字与背景对比度应达到 4.5:1关键图标和按钮至少达到 3:1。二十一、隐私边界离线笔记仍然是用户数据当前DataStore把收藏和笔记保存在应用 Preferences 中没有看到上传、账号或第三方同步逻辑。文章中的“同步”指应用内部页面与个人记录的一致关联不代表云同步或跨设备上传。发布材料应如实说明笔记与收藏保存在本地不收集账号、通讯录或位置卸载应用后本地数据按系统机制清理若未来增加云同步必须重新评估权限、隐私政策、服务端位置和删除机制。不要用“多端同步”描述尚未存在的能力。内部状态同步与网络数据同步是两个完全不同的承诺。二十二、验证收藏链路至少执行以下用例在实验地图收藏stable_orbit进入“我的收藏”能看到“稳定双体系统”点击收藏项模拟页收到正确expId和expName在模拟页取消收藏返回收藏页后条目消失手工放入一个目录不存在的旧 ID页面忽略坏项且不崩溃连续快速点击星标不出现重复 ID杀进程重启后收藏仍存在。可把纯映射逻辑提取后做单元测试export function resolveFavoriteExperiments( ids: string[], all: Experiment[] ): Experiment[] { const byId new Map(all.map(item [item.id, item])) return ids .map(id byId.get(id)) .filter((item): item is Experiment item ! undefined) }测试重点不是 ArkUI 渲染而是顺序、坏 ID 和重复值的业务规则。二十三、验证笔记与来源关联笔记链路需要覆盖空标题或空正文不允许保存保存中按钮禁用避免重复提交从知识点进入时笔记带正确{ type, id }独立新建的笔记允许没有来源点击“查看来源”能回到对应知识页、公式页或实验页来源已删除时显示不可用状态不跳默认页面旧版笔记能迁移坏数据被隔离删除失败时 UI 不伪装成功。建议为路由解析写表驱动测试const cases: LearningResourceRef[] [ { type: knowledge, id: orbit_1 }, { type: formula, id: gravity_force }, { type: experiment, id: stable_orbit } ]每个引用都要验证标题、目标路由和参数而不是只验证“没有抛异常”。二十四、常见问题与定位顺序现象优先检查修复方向收藏后列表不更新返回时是否执行 reload在页面显示阶段刷新增加请求序号收藏项点击进入错误场景expId是否真实传递统一由目录构造路由参数笔记重启后丢失Preferences 是否已初始化、flush 是否成功返回可观察的保存结果删除后重新出现是否先改 UI、落盘却失败成功后提交 UI 或失败回滚旧数据导致空白页JSON 只做了类型断言增加字段校验和版本迁移快速操作丢收藏并发读改写覆盖仓库内串行化写入深色模式输入框刺眼弹窗存在浅色硬编码改为深浅色同名资源笔记无法回到知识页模型没有来源引用保存稳定资源类型与 ID排查时先确认DataStore.init()是否完成再看存储键值最后检查页面生命周期和目录解析。不要一开始就怀疑 ArkUI 列表组件。二十五、上线前的最小验收清单[ ] 收藏和笔记只保存必要本地数据[ ] Preferences 初始化失败有错误状态[ ] 收藏 ID 去重坏 ID 不导致崩溃[ ] 笔记有版本字段和迁移策略[ ] 来源使用稳定资源 ID不复制知识正文[ ] 路由目标与按钮文案一致[ ] 保存、删除具备失败反馈[ ] 首次加载不会闪现错误空状态[ ] phone、tablet、2in1 下操作均可达[ ] 深浅色文字、输入框、按钮满足对比度[ ] 隐私材料没有宣称不存在的云同步[ ] 完成安装、启动、核心流程、退出和卸载冒烟验证。总结收藏与笔记真正共享的不是一个页面而是一套稳定的学习资源身份。现有源码已经把实验收藏和本地笔记分别跑通收藏以实验 ID 为核心笔记以 JSON 数组写入 Preferences。下一步应保持这两个真实基础不变把{ type, id }引入个人记录让知识点、公式和实验都由统一目录解析再由仓库集中处理迁移、校验和串行写入。这样做之后“我的收藏”和“我的笔记”才不只是两个数据列表而是可以回到原学习现场的入口。页面负责交互目录负责资源仓库负责可靠存储三者各自守住边界应用内部的学习记录才能稳定同步。LEARNING-ONE13-FAVORITE-NOTE-CONTEXT-20260726收藏保存稳定资源引用笔记携带可选学习来源目录解析跳转仓库统一迁移与并发写入。本文部分内容由 AI 辅助整理源码事实、工程边界与验证结论均依据文中所列项目文件复核。
返回列表