化学公式页看起来只是一个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(): Promise<void> { 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,再重新读取全部收藏。流程正确,但用户连续点击时可能发起重叠异步操作。可以记录正在处理的 ID:
@State pendingFavoriteIds: string[] = [] private async toggleFavorite(id: string): Promise<void> { 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' })这只能打开公式页,无法定位收藏对象。调用方应传递稳定 ID:
router.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 + O2 | matched | 3Fe + 2O₂ → Fe₃O₄ |
H₂ + O₂ | matched | Unicode 下标归一化后命中 |
CH4+O2 | matched | 甲烷燃烧 |
| 空字符串 | empty | 提示输入反应物 |
Na + H2O | unsupported | 明确“暂未收录” |
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 源码为依据,并由作者复核后发布。