化学公式页看起来只是一个List名称放在上面方程式放大下面再补一行说明。真正运行后问题往往不在“能不能显示”而在“能不能快速读懂”。短式子PV nRT很轻松CaCO₃ 2HCl → CaCl₂ H₂O CO₂↑已经开始占宽度如果再把“加热”“电解”和沉淀、气体符号塞进同一个字符串用户很难在长列表中区分反应主体、条件与现象。本文基于化学实验室 1.0.5的真实 ArkTS 源码拆解FormulaPage.ets、FormulaItem.ets、FavoritesPage.ets和DataStore.ets。当前项目共收录 15 条本地公式基础化学 7 条、有机化学 2 条、电化学 1 条、分析化学 5 条。页面已经具备横向分类、公式卡片、收藏持久化和常见反应查询数据量不大但模型、排版、定位和失败反馈之间仍有几个明显断点。本文唯一核验标记CHEM-LAB-ONE12-FORMULA-LIBRARY-READABILITY-20260730。最典型的是“配平”按钮。现有实现并没有求解任意方程式而是识别Fe O2、H2 O2、CH4 O2等 6 组关键词并返回预置结果未知输入也会弹出“已生成配平结果”。收藏页打开一条公式时又只跳回FormulaPage没有携带公式 ID用户还得在列表里重新寻找。更稳的工程判断是公式库不是把化学式放大显示而是把方程式、条件、现象和入口身份组织成可扫描的阅读单元。内容模型保持化学语义展示模型负责视觉层级页面状态负责筛选与空结果路由只使用稳定 ID。本文会完成以下拆解盘点 15 条真实公式及当前页面的数据流。把方程式、反应条件、现象和解释从单字符串中拆开。设计适合手机、平板和小窗口的公式卡片。让分类、搜索、收藏和空状态使用同一份结果列表。明确“常见反应查询”与“通用方程式配平”的产品边界。从全局搜索和收藏页按formulaId定位到具体公式。给出验证矩阵与故障排查顺序。一、先看真实数据而不是先改卡片样式FormulaItem.ets当前模型只有五个字段export interface FormulaItem { id: string name: string formula: string description: string category: string }它足以支撑第一版列表但formula同时承载了方程式、反应条件和现象符号。例如{ id: oxygen_prepare, name: 氧气制取, formula: 2KMnO₄ →Δ K₂MnO₄ MnO₂ O₂↑, description: 高锰酸钾加热分解制取氧气, category: 基础化学 }这里的Δ被写在箭头右侧视觉上像生成物的一部分O₂↑又同时表达物质与气体逸出。如果后续要按“加热反应”筛选、给条件单独着色或者为无障碍朗读补充语义只靠一个字符串会越来越难维护。当前 15 条数据分布如下分类数量典型内容列表风险基础化学7中和、置换、沉淀、气体制取长方程式较多有机化学2甲烷燃烧、乙醇燃烧化学式字符密集电化学1水的电解条件需要突出分析化学5物质的量、浓度、理想气体符号含义需要说明先统计数据能避免两个误判一是不能因为目前只有 15 条就忽略目标定位二是不能把规则公式和反应方程式强行用完全相同的视觉结构展示。二、当前页面的数据流已经形成最小闭环FormulaPage初始化时直接读取全部公式并在出现时加载收藏State selectedCategory: number 0 State favoriteKeys: string[] [] State formulas: FormulaItem[] getAllFormulas() aboutToAppear(): void { this.reloadFavorites() } private async reloadFavorites(): Promisevoid { const items await DataStore.loadAllFavorites() this.favoriteKeys items .filter(item item.type formula) .map(item item.id) }这个实现有两个值得保留的点。第一公式内容是本地静态事实页面不需要为了显示列表引入网络状态。第二收藏用type id区分业务对象不会因为两类内容恰好同名而互相覆盖。页面的职责仍然偏多它既保存内容数组又做分类筛选、输入归一化、规则匹配、收藏持久化和卡片渲染。数据量小时不会造成性能问题但会让每项交互难以单独验证。后续重构应先划分职责不必先引入复杂框架。三、把内容模型与展示模型分开更适合公式库的内容模型可以显式表达条件和现象export type FormulaKind reaction | law export interface FormulaItem { id: string name: string equation: string description: string category: string kind: FormulaKind condition?: string phenomenon?: string aliases: string[] }这里没有把一个方程式拆成完整的化学 AST因为当前产品只需要浏览、筛选和定向查询。equation保存可直接显示的式子condition保存“加热”“电解”等条件phenomenon保存“放出气体”“生成沉淀”等观察信息aliases服务于搜索和输入匹配。页面再生成只读展示对象export interface FormulaDisplayItem { id: string title: string equation: string category: string description: string conditionText: string phenomenonText: string isFavorite: boolean } export function toFormulaDisplay( item: FormulaItem, favoriteIds: string[] ): FormulaDisplayItem { return { id: item.id, title: item.name, equation: item.equation, category: item.category, description: item.description, conditionText: item.condition ?? 常规条件, phenomenonText: item.phenomenon ?? 无额外现象标记, isFavorite: favoriteIds.indexOf(item.id) 0 } }展示对象不修改业务数据也不承担持久化。它只把页面真正需要的字段一次准备好卡片无需反复判断可选字段。四、迁移一条公式时先保持化学含义不变以高锰酸钾制氧为例迁移后不再把条件挤进箭头const oxygenPrepare: FormulaItem { id: oxygen_prepare, name: 高锰酸钾制取氧气, equation: 2KMnO₄ → K₂MnO₄ MnO₂ O₂↑, description: 高锰酸钾受热分解产生锰酸钾、二氧化锰和氧气, category: 基础化学, kind: reaction, condition: 加热, phenomenon: 有气体生成, aliases: [氧气制取, 高锰酸钾分解, KMnO4] }迁移时要遵守一个边界模型拆分是为了显示和检索不应顺手修改化学计量数或反应结论。每条数据都应对照原始内容逐项确认equation、condition和phenomenon避免“重构成功、知识错误”。对于n m / M、PV nRT这类定律kind设为law条件栏可改为“适用范围”不必伪装成反应方程式。五、公式主视觉必须允许自然换行当前页面使用 22vp 加粗文字显示公式Text(item.formula) .fontSize(22) .fontWeight(AppFonts.WEIGHT_BOLD) .fontColor(AppColors.PRIMARY) .margin({ top: 8 })它没有固定宽度这比强制单行更安全但长式子换行后缺少行距条件和说明又紧跟在后面。更稳的卡片应给公式独立的垂直区域Text(item.equation) .width(100%) .fontSize(21) .fontWeight(AppFonts.WEIGHT_BOLD) .fontColor(AppColors.PRIMARY) .lineHeight(32) .textAlign(TextAlign.Start) .margin({ top: 10 }) .constraintSize({ minHeight: 40 })这里不使用单行省略。公式一旦被截断可能正好丢失系数、生成物或状态符号信息就不完整。真正超长的内容可以换行并通过上下间距与说明文字建立层级。六、条件与现象不应和分类标签抢位置如果卡片底部同时放分类、条件、现象和收藏按钮很容易变成一排密集小标签。可以把信息分成三层第一层名称与收藏动作。第二层完整方程式字号最大。第三层条件、现象和解释字号较小但保持正文对比度。示例 ArkUI 结构如下Column({ space: 8 }) { Row() { Text(item.title) .fontSize(AppFonts.BODY_SIZE) .fontWeight(AppFonts.WEIGHT_MEDIUM) .layoutWeight(1) this.FavoriteAction(item) } Text(item.equation) .width(100%) .fontSize(21) .lineHeight(32) .fontWeight(AppFonts.WEIGHT_BOLD) Row({ space: 8 }) { this.MetaChip(item.category) this.MetaChip(item.conditionText) } Text(item.description) .fontSize(AppFonts.CAPTION_SIZE) .fontColor(AppColors.TEXT_SECONDARY) .lineHeight(22) }分类和条件可以使用短标签现象与解释保持普通文本。这样用户扫列表时先识别式子再决定是否阅读说明。七、用独立卡片组件限制页面复杂度卡片不应直接访问DataStore否则列表里的每个子组件都可能发起持久化操作。它只接收数据和点击回调Component struct FormulaCard { Prop item: FormulaDisplayItem onFavorite: (id: string) void () {} build() { Column() { Row() { Text(this.item.title) .layoutWeight(1) .maxLines(2) Button({ type: ButtonType.Circle }) { Text(this.item.isFavorite ? ★ : ☆) .fontSize(20) } .width(44) .height(44) .accessibilityText(this.item.isFavorite ? 取消收藏 : 收藏公式) .onClick(() this.onFavorite(this.item.id)) } Text(this.item.equation) .width(100%) .fontSize(21) .lineHeight(32) Text(this.item.description) .width(100%) .fontSize(14) .lineHeight(22) } .width(100%) .alignItems(HorizontalAlign.Start) .padding(16) } }44vp 的圆形动作区比单独一个星号字符更容易点击也能提供明确的无障碍文本。页面仍负责真正的收藏切换和重新加载。八、ForEach 必须使用公式 ID 作为稳定键当前公式列表没有显式 key 生成器ForEach(this.filteredFormulas(), (item: FormulaItem) { ListItem() { // 卡片内容 } })切换分类后同一条公式可能在不同结果数组中出现。使用稳定 ID 可以让 ArkUI 更准确地识别节点身份ForEach(this.visibleItems, (item: FormulaDisplayItem) { ListItem() { FormulaCard({ item: item, onFavorite: (id: string) this.toggleFavorite(id) }) } }, (item: FormulaDisplayItem) item.id)key 不能使用数组下标。分类变化后下标会改变收藏状态或局部组件状态可能附着到错误卡片。九、分类状态应使用业务值而不是数组位置当前状态是selectedCategory: number筛选时再从categories[index]读取文字。只要数组顺序变化、插入新分类或者从配置中动态生成分类索引就容易失去原来的含义。可以直接保存分类值const ALL_CATEGORY 全部 State selectedCategory: string ALL_CATEGORY private filterByCategory(items: FormulaItem[]): FormulaItem[] { if (this.selectedCategory ALL_CATEGORY) { return items } return items.filter((item: FormulaItem) { return item.category this.selectedCategory }) }这样按钮选中态、筛选条件和埋点文本都使用同一个业务值不需要在多个位置解释数字2到底代表什么。十、分类列表最好从目录数据中校验出来当前分类数组手写为“全部、基础化学、有机化学、电化学、分析化学”。它与模型数据恰好一致但以后新增“无机化学”时开发者可能只加数据忘记加标签。不需要在页面每次构建时去重可以在模型层提供稳定目录export interface FormulaCategory { name: string count: number } export function getFormulaCategories(items: FormulaItem[]): FormulaCategory[] { const order: string[] [基础化学, 有机化学, 电化学, 分析化学] return order.map((name: string) ({ name, count: items.filter((item: FormulaItem) item.category name).length })).filter((item: FormulaCategory) item.count 0) }界面可以显示“基础化学 7”用户切换前就知道分类规模。若出现目录外分类单元测试应直接失败提醒内容维护者补齐顺序定义。十一、分类与关键词需要合并成一个筛选管线长列表真正可用时分类和搜索不能各自维护一份结果。推荐统一计算private buildVisibleItems(): FormulaItem[] { const categoryItems this.filterByCategory(this.formulas) const query normalizeFormulaQuery(this.query) if (query.length 0) { return categoryItems } return categoryItems.filter((item: FormulaItem) { const source normalizeFormulaQuery([ item.name, item.equation, item.description, item.condition ?? , item.aliases.join( ) ].join( )) return source.indexOf(query) 0 }) }执行顺序是先分类、再关键词结果数量更容易解释。15 条数据无需引入全文检索引擎但必须让用户输入KMnO4时也能命中显示为KMnO₄的内容。十二、化学输入归一化要处理上下标字符现有normalizeInput()只删除空格、替换箭头和等号并转小写return value.replace(/\s/g, ) .replace(//g, ) .replace(/→/g, ) .toLowerCase()用户粘贴H₂ O₂时Unicode 下标仍然是₂与规则里的h2不相等。可以建立显式字符映射export function normalizeFormulaQuery(value: string): string { return value .toLowerCase() .replace(/₀/g, 0) .replace(/₁/g, 1) .replace(/₂/g, 2) .replace(/₃/g, 3) .replace(/₄/g, 4) .replace(/₅/g, 5) .replace(/₆/g, 6) .replace(/₇/g, 7) .replace(/₈/g, 8) .replace(/₉/g, 9) .replace(/[→]/g, ) .replace(/\s/g, ) }归一化只服务检索和规则匹配展示时仍保留正确的上下标、箭头和状态符号不能把用户看到的公式改成纯 ASCII。十三、“配平工具”要先说清能力边界当前balanceEquation()通过indexOf()判断反应物能覆盖 6 个预置场景。它不是根据元素守恒建立线性方程组也不会验证用户输入的生成物。因此界面更准确的名称应是“常见反应查询”或“配平示例”。把规则抽成数据后边界更清楚interface BalanceRule { id: string reactants: string[] result: string } const BALANCE_RULES: BalanceRule[] [ { id: iron_oxygen, reactants: [fe, o2], result: 3Fe 2O₂ → Fe₃O₄ }, { id: hydrogen_oxygen, reactants: [h2, o2], result: 2H₂ O₂ → 2H₂O }, { id: methane_oxygen, reactants: [ch4, o2], result: CH₄ 2O₂ → CO₂ 2H₂O } ]这段代码没有冒充通用算法。新增规则需要明确 ID、反应物和已核对结果便于测试每条规则。十四、查询结果必须区分成功、空输入和未收录无论命中与否都弹“已生成配平结果”会让用户误以为未知反应也已被正确计算。可以返回显式状态type LookupStatus matched | empty | unsupported interface LookupResult { status: LookupStatus message: string formulaId?: string } function findBalanceExample(input: string): LookupResult { const key normalizeFormulaQuery(input) if (key.length 0) { return { status: empty, message: 请先输入反应物 } } const rule BALANCE_RULES.find((item: BalanceRule) { return item.reactants.every((reactant: string) key.indexOf(reactant) 0) }) if (!rule) { return { status: unsupported, message: 暂未收录该反应请从公式目录继续查找 } } return { status: matched, message: rule.result, formulaId: rule.id } }页面只有在matched时提示成功empty聚焦输入框unsupported使用中性说明不暗示系统完成了实际计算。十五、收藏操作要防止连续点击造成状态抖动当前点击星号后先写入DataStore再重新读取全部收藏。流程正确但用户连续点击时可能发起重叠异步操作。可以记录正在处理的 IDState pendingFavoriteIds: string[] [] private async toggleFavorite(id: string): Promisevoid { if (this.pendingFavoriteIds.indexOf(id) 0) return const item this.formulas.find((value: FormulaItem) value.id id) if (!item) return this.pendingFavoriteIds this.pendingFavoriteIds.concat(id) try { await DataStore.toggleFavorite({ id: item.id, type: formula, title: item.name, subtitle: ${item.category} · ${item.equation}, route: views/learning/FormulaPage, createdAt: Date.now() }) await this.reloadFavorites() } finally { this.pendingFavoriteIds this.pendingFavoriteIds.filter((value: string) value ! id) } }卡片在 pending 状态禁用收藏按钮并保留原图标避免短时间内星标来回闪烁。十六、收藏和全局搜索必须携带 formulaId当前收藏页对公式执行router.pushUrl({ url: views/learning/FormulaPage })这只能打开公式页无法定位收藏对象。调用方应传递稳定 IDrouter.pushUrl({ url: views/learning/FormulaPage, params: { formulaId: item.id } })全局搜索中的公式结果也使用相同协议。不要传数组下标因为新增、删除或重新排序公式后下标不再代表同一条内容也不要传标题因为标题可能改名或重复。十七、公式页接收 ID 后先验证再滚动List可以绑定Scroller在页面出现后定位目标interface FormulaRouteParams { formulaId?: string } private listScroller: Scroller new Scroller() State targetFormulaId: string aboutToAppear(): void { const params router.getParams() as FormulaRouteParams this.targetFormulaId params.formulaId ?? this.reloadFavorites() } private scrollToTarget(): void { if (this.targetFormulaId.length 0) return const index this.visibleItems.findIndex((item: FormulaDisplayItem) { return item.id this.targetFormulaId }) if (index 0) { this.showToast(未找到目标公式) return } this.listScroller.scrollToIndex(index, true, ScrollAlign.CENTER) }如果当前分类不是“全部”应先切回全部并刷新visibleItems再滚动。目标不存在时留在页面并给出说明不能因为错误参数白屏或崩溃。十八、目标卡片需要短暂高亮而不只是滚到附近长列表滚动后用户仍可能不知道哪一条是入口目标。可以让卡片根据targetFormulaId显示短暂边框.border({ width: item.id this.targetFormulaId ? 2 : 0, color: AppColors.PRIMARY }) .backgroundColor( item.id this.targetFormulaId ? AppColors.PRIMARY_BG : AppColors.CARD_BG )高亮不是永久选中状态。页面完成首次定位后可以延迟清除targetFormulaId让卡片回到普通样式同时不改变收藏状态或分类状态。十九、空结果是筛选流程的一部分当前任一分类都有数据所以开发阶段不容易看到空列表。但加入关键词后“电化学 乙醇”必然为空。页面应区分三种情况状态页面内容用户下一步无查询当前分类全部公式浏览或切换分类有结果结果数量与卡片打开、收藏或修改查询无结果当前关键词与分类说明清除关键词或查看全部ArkUI 可以直接使用条件分支if (this.visibleItems.length 0) { Column({ space: 12 }) { Text(没有找到相关公式) .fontSize(18) .fontWeight(FontWeight.Medium) Text(分类${this.selectedCategory}关键词${this.query}) .fontSize(14) .fontColor(AppColors.TEXT_SECONDARY) .textAlign(TextAlign.Center) Button(清除筛选) .onClick(() this.resetFilters()) } .layoutWeight(1) .justifyContent(FlexAlign.Center) } else { this.FormulaList() }空状态必须提供恢复动作不能只显示一句“暂无数据”。二十、不要让硬编码颜色破坏深浅色适配当前分类未选中背景和输入框背景分别使用#F0F0F0、#F3F4F6。在浅色模式下没有明显问题但如果主题跟随系统硬编码浅灰可能与深色页面形成刺眼色块文字对比也可能失效。这些颜色应进入统一主题Text(category.name) .fontColor( this.selectedCategory category.name ? AppColors.TEXT_ON_PRIMARY : AppColors.TEXT_SECONDARY ) .backgroundColor( this.selectedCategory category.name ? AppColors.PRIMARY : AppColors.CONTROL_BG )公式正文、辅助说明、禁用收藏按钮和目标高亮都要在浅色、深色两种资源下检查。正文与背景需要保持足够对比不能只靠颜色深浅表达是否收藏。二十一、多设备下优先保住公式宽度手机竖屏、横屏小窗口和平板对卡片宽度的需求不同但公式区域都应尽量占满内容宽度。建议策略如下设备/窗口列表布局公式字号分类入口手机竖屏单列20-22vp横向滚动手机横屏/小窗口单列紧凑18-20vp横向滚动平板双列仅用于短公式默认仍单列20-22vp顶部固定2in1限制内容最大宽度20-22vp支持鼠标点击公式库不适合为了“充分利用平板空间”强制多列。双列会明显压缩长方程式的可读宽度只有在按公式长度分组并经过验证后才值得采用。二十二、无障碍与触控细节不能留到最后星号字符虽然直观但屏幕阅读器只可能读成“星形”并不知道当前动作。改成有 44vp 触控区的按钮后还需要动态说明.accessibilityText( item.isFavorite ? 取消收藏${item.title} : 收藏公式${item.title} )方程式本身可以补充可朗读文本例如把↑解释为“气体生成”、↓解释为“沉淀生成”。不要把无障碍文本直接显示在卡片上而是由模型提供spokenText交给组件的可访问属性。二十三、用查询样例验证规则而不是只看界面本地规则最适合做确定性测试。至少覆盖以下输入输入预期状态预期结果Fe O2matched3Fe 2O₂ → Fe₃O₄H₂ O₂matchedUnicode 下标归一化后命中CH4O2matched甲烷燃烧空字符串empty提示输入反应物Na H2Ounsupported明确“暂未收录”KMnO₄matched高锰酸钾分解规则分类测试还应确认 7、2、1、5 的数量不因筛选重构发生变化。收藏测试要确认相同formulaId连续切换后最终只保留一条记录。二十四、页面验收要覆盖完整交互链路建议按以下顺序做手工验收首次进入显示全部 15 条公式卡片没有横向溢出。四个分类数量分别为 7、2、1、5。输入KMnO4能找到显示为KMnO₄的公式。未收录反应不会显示成功提示。收藏任一公式后返回“我的收藏”标题和方程式一致。从收藏页重新打开列表滚到对应公式并短暂高亮。从全局搜索打开公式使用同一formulaId协议。重启应用后收藏仍存在取消收藏后不残留。系统深色模式下分类、卡片、输入框和辅助文字可读。手机横屏、小窗口和平板下没有被状态栏或底部导航遮挡。这组验收同时检查内容正确性、交互可达性和持久化不会只得到一张“看起来不错”的静态截图。二十五、常见故障与排查顺序现象优先检查常见原因修复方向H₂无法命中归一化结果未转换 Unicode 下标补齐上下标映射切换分类后卡片状态错位ForEachkey使用数组下标或无稳定 key使用item.id收藏后星标不刷新异步链路写入后未回读或重复点击pending 状态 reload从收藏页打开仍在列表顶部路由参数未传formulaId统一深链协议公式被截断Text 约束单行省略或固定高度允许换行并设置行距未知反应也提示成功查询状态所有分支共用成功 Toast区分 matched/unsupported深色模式出现浅灰块主题资源页面硬编码颜色使用语义色 token新分类没有标签分类目录数据与手写数组漂移模型层生成并校验分类排查时先确认内容模型再看筛选结果最后检查组件布局和路由。若一开始就调字号很容易掩盖“目标 ID 丢失”或“查询状态错误”这类真正的业务问题。二十六、按最小风险顺序落地这次改造不需要一次性重写整个页面可以分成四步先为ForEach增加稳定 key并让收藏、搜索都传formulaId。再拆分condition、phenomenon和aliases逐条迁移 15 条数据。抽取FormulaCard与统一筛选管线补齐空状态和 pending 状态。最后调整“配平工具”的命名与返回类型并完成多设备、深浅色验证。每一步都能单独回归。第一步不改数据第二步不改交互第三步才改页面结构第四步处理产品文案和规则边界出问题时更容易定位。二十七、源码事实与建议实现的边界公式页同时包含数据目录、筛选、收藏、配平和跨页面定位最容易把“计划中的完整工具”误写成“当前已经具备”。下面把可直接复核的实现与本文建议分开列出。类别当前源码可直接确认本文建议但未声称已合入公式目录FormulaItem.ets当前有 15 条公式和 4 个业务分类增加条件、现象、关键词与展示优先级字段分类状态页面维护 5 个标签selectedCategory保存数组下标使用稳定分类值并校验目录数据列表筛选“全部”返回完整数组其余按category精确过滤将分类与关键词合并为同一筛选管线列表身份当前ForEach没有提供第三个稳定键函数使用item.id作为稳定键公式展示公式文本固定22fp、粗体未声明换行或缩放策略用约束宽度、自然换行与可测试字号保证长式可读收藏点击后调用DataStore.toggleFavorite()随后重新加载收藏增加进行中状态阻止同一公式连续点击配平输入去空白并把、→替换为后转小写明确 token 化与上下标归一化规则配平范围当前仅识别 6 组固定关键词分支并返回预置结果若扩展为通用能力应采用经过验证的化学方程式解析与配平引擎未收录结果返回“暂未收录”文本但仍显示“已生成配平结果”Toast区分成功、空输入和未收录状态跨入口定位收藏 route 和全局搜索当前只打开FormulaPage携带formulaId验证后滚动并短暂高亮颜色适配分类未选中背景、输入背景和阴影含硬编码色值迁移到深浅色资源与主题令牌本文给出的展示模型、统一筛选、目标高亮、输入归一化和交互防抖属于针对现有断点的建议设计并不表示这些结构已经合入源码也没有声称完成 ArkTS 编译、自动化滚动定位或真机大字体验证。落地后仍需按第二十三、二十四节逐项记录真实查询、收藏、路由和多设备结果。总结化学公式库的难点不是List组件本身而是让内容语义、视觉层级和入口身份保持一致。当前项目的 15 条公式、四类筛选和收藏持久化已经形成可用基础但单字符串公式、索引式分类、预置规则伪装成通用配平以及公式入口不携带 ID都会在实际使用中增加理解与寻找成本。更可靠的方案是把方程式、条件、现象和别名拆成明确字段用展示模型组织卡片允许长公式自然换行用稳定 ID 驱动列表、收藏和深链把未知输入视为正常失败状态而不是成功结果。完成这些边界后公式库才真正从“能列出来”变成“能快速看懂、能准确找到、能再次回到原位置”的学习工具。AI 辅助声明本文由 AI 辅助完成源码梳理、结构校对与配图设计技术结论均以文中列出的本地 ArkTS 源码为依据并由作者复核后发布。