鸿蒙 Next 睡前故事定制机 App 开发实战:10 个内置故事 + 角色替换引擎 + 语音朗读 + 主题筛选 + 第十九款 App



鸿蒙 Next 睡前故事定制机 App 开发实战:10 个内置故事 + 角色替换引擎 + 语音朗读 + 主题筛选 + 第十九款 App
作者:duluo
SDK 版本:HarmonyOS API 24 (Next)
开发工具:DevEco Studio
语言框架:ArkTS + ArkUI
字数:约 10000 字
目录
- 引言
- 产品概念与数据模型
- 三 Tab 架构设计
- 故事数据库设计
- 角色替换引擎
- 语音朗读引擎
- 主题筛选与分类浏览
- 收藏系统与轻量持久化
- 适老化与儿童友好设计
- 编译错误全记录
- 十九款 App 全景回顾
- 结语
1. 引言
1.1 睡前故事的意义
睡前故事是儿童成长过程中不可或缺的一部分。心理学家认为,睡前故事不仅能帮助孩子放松入眠,还能促进语言发展、培养想象力和建立亲子情感连接。每天晚上,全球有数以亿计的父母在孩子的床边讲述着各种故事。
然而,现代父母面临一个普遍困境:故事不够用了。翻来覆去就那几个童话,孩子早就听腻了。买故事书又要额外花钱,网上的故事又不一定适合自己孩子的年龄和兴趣。
"睡前故事定制机"App 的解决方案是:内置一批高质量故事,并允许父母定制故事中的角色和地点。把"小兔子"换成孩子的名字,把"森林"换成孩子熟悉的公园——熟悉的元素让故事更有代入感,孩子也更愿意听。
1.2 本 App 的技术特色
睡前故事定制机在技术上有几个显著特点。
首先,它构建了一个包含 10 个完整故事的故事数据库,每个故事包含标题、主题、年龄建议、角色和地点等元数据。故事按段落存储(而非整段文本),便于分步朗读和定制替换。
其次,它实现了角色替换引擎——使用 replaceAll 方法将故事中的默认角色名和地点名替换为用户自定义的名称。这个引擎虽然算法简单(字符串替换),但 UX 效果显著——孩子听到自己的名字出现在故事中,代入感会大幅提升。
此外,本 App 将上一次语音朗读引擎(来自语音菜谱 App)升级为段落级朗读 + 高亮同步的完整方案,增加了"朗读完毕"后的晚安提示 Toast。
1.3 第十九款 App 的系列数据
App 数量: 19
代码总行数: ~12,400 行
编译错误数: ~175 个
博客总字数: ~200,000 字
技术博客数: 19 篇
2. 产品概念与数据模型
2.1 功能需求
家长故事:我想给孩子讲一个睡前故事
家长故事:我想把故事里的主角换成我家孩子的名字
家长故事:我想按主题找到适合的故事
家长故事:我想收藏喜欢的故事方便再讲
家长故事:我想让 App 朗读故事,我陪着孩子看
功能清单:
├── F1: 故事列表(10 个内置故事,含主题/时长/年龄标签)
├── F2: 主题筛选(6 种主题 Grid 选择)
├── F3: 角色定制(替换主角名字 + 地点名称)
├── F4: 语音朗读(段落推进 + 高亮 + 自动结束)
├── F5: 收藏系统(收藏/取消 + 收藏列表)
├── F6: 详情弹窗(故事元数据 + 定制输入 + 正文 + 朗读)
└── F7: 数据持久化(收藏 ID 列表)
2.2 数据模型
interface Story {
id: number; // 唯一标识
title: string; // 故事标题
theme: string; // 主题分类
icon: string; // Emoji 图标
content: string; // 一句话简介
paragraphs: string[]; // 段落数组
duration: string; // 朗读时长
age: string; // 适龄建议
characters: string[]; // 可替换角色列表
places: string[]; // 可替换地点列表
defaultChar: string; // 默认主角名(替换目标)
defaultPlace: string; // 默认地点(替换目标)
isFavorite: boolean; // 是否收藏
}
paragraphs 是 string[] 而不是整段文本——这个设计决策很关键。它使得:
- 朗读时可以逐段推进,每段 5 秒
- 每段可以独立高亮
- 定制替换按段落执行
characters 和 places 存储了故事中所有可替换的元素。defaultChar 和 defaultPlace 标记了故事中的"主角名"和"主要场景"——这两个是最常被替换的元素。
2.3 故事结构设计
每个故事包含 8-10 个段落,总朗读时长约 5-6 分钟。以下是一个典型故事的结构:
段落 1:引入主角和环境
段落 2-3:冲突出现
段落 4-6:解决问题(核心情节)
段落 7-8:圆满结局
段落 9:晚安祝福(可选)
这种"引入-冲突-解决-结局"的四段式结构符合儿童故事的经典叙事模式。
3. 三 Tab 架构设计
3.1 Tab 配置
buildTabContent() {
if (this.activeTab === 0) this.buildStoryList() // 故事
else if (this.activeTab === 1) this.buildCustomizeTab() // 定制
else this.buildFavoriteTab() // 收藏
}
| Tab | 图标 | 功能 | 用户场景 |
|---|---|---|---|
| 故事 | 📖 | 浏览 + 筛选全部故事 | “今晚讲哪个?” |
| 定制 | ✨ | 选择故事 + 替换角色 | “把主角换成宝宝名字” |
| 收藏 | ⭐ | 已收藏的故事列表 | “上次那个还要再讲一遍” |
3.2 Tab 之间的协作
与之前的 App 不同,本 App 的"定制"Tab 不直接进行定制操作——它只是一个入口列表。用户点击后跳转到故事详情弹窗,在弹窗中完成定制输入和朗读:
定制 Tab → 选择故事 → 详情弹窗(定制输入 + 朗读)→ 回到故事列表
这种设计把定制功能集成在详情弹窗中,而不是单独开一个定制页面,减少了交互层级。
4. 故事数据库设计
4.1 10 个故事的选取
10 个故事覆盖了 6 种主题:
| 主题 | 图标 | 故事数 | 名称 |
|---|---|---|---|
| 童话 | 🌙 | 3 | 月亮上的兔子、会说话的小星星、云朵棉花糖 |
| 动物 | 🦊 | 2 | 小熊的蜂蜜梦、想飞的小企鹅 |
| 冒险 | 🚀 | 1 | 勇敢的小火车 |
| 魔法 | 🧚 | 1 | 魔法画笔 |
| 自然 | 🌊 | 1 | 小乌龟的慢旅行 |
| 晚安 | 🤗 | 2 | 晚安小怪兽、枕头王国历险记 |
每篇故事的主角名(如"小月"“团团”)和地点(如"森林小屋"“蜜糖森林”)都是可定制的。故事结尾通常包含"晚安"或"好梦"的祝福语。
4.2 故事中可替换元素的标记
故事段落中使用 【】 标记可替换元素:
从前,在很远很远的月亮上,住着一只毛茸茸的小兔子,名字叫【小月】。
defaultChar 字段值为 "小月",replaceAll 引擎在运行时将 "小月" 替换为用户输入的名称(如"宝宝")。
标记使用 【】 而非 {{}} 或 <>,因为 【】 在中文文本中更自然,即使在未替换时也不会让故事显得突兀。
4.3 年龄建议
每个故事标注了适龄建议:
| 年龄 | 故事数 | 特点 |
|---|---|---|
| 2-5 岁 | 3 | 情节简单,角色少,句子短 |
| 2-6 岁 | 3 | 中等复杂度 |
| 3-6 岁 | 1 | |
| 3-7 岁 | 2 | 情节更丰富 |
| 3-8 岁 | 1 | 包含抽象概念 |
年龄建议帮助家长快速筛选适合自己孩子年龄的故事。
5. 角色替换引擎
5.1 替换逻辑
getCustomizedParas(): string[] {
if (this.selected === null) return [];
return this.selected.paragraphs.map(p =>
p.replaceAll(this.selected!.defaultChar, this.customizeChar)
.replaceAll(this.selected!.defaultPlace, this.customizePlace)
);
}
引擎的核心是 String.prototype.replaceAll 方法。它将故事每个段落中的 defaultChar 替换为 customizeChar,将 defaultPlace 替换为 customizePlace。
5.2 替换范围
替换不是全局全文替换,而是基于段落级别逐段进行:
paragraphs.map(p => p.replaceAll(...))
每个段落独立执行替换,这意味着:
- 性能好:大文本分割成小段落后,每次替换的计算量很小
- 可扩展:未来可以针对不同段落做不同替换规则
5.3 实时预览
详情弹窗中提供了两个 TextInput 用于输入自定义名称:
👤 主角 [____________] 🏠 地点 [____________]
用户输入时,正文区的文字实时更新(因为 getCurrentParagraphs() 每次渲染时重新计算):
TextInput({ placeholder: '输入主角名字', text: this.customizeChar })
.onChange((v: string) => { this.customizeChar = v; })
由于正文的展示依赖 this.customizeChar 和 this.customizePlace 这两个 @State 变量,输入框内容变化时,正文会自动重新渲染并展示替换后的文本。
5.4 替换引擎的局限性
当前替换引擎使用简单的字符串匹配,有两个潜在问题:
-
部分匹配:如果用户输入的角色名恰好是故事中其他词的一部分,可能会产生意外替换。例如,将"小月"改为"小亮"后,故事中"月亮"会变成"月亮"(保持不变),但"小月"会被替换。
-
多角色替换:当前只支持主角名和地点两个替换维度。对于有多个角色的故事(如"小乌龟的慢旅行"中只有主角),这个限制不是问题;但对于需要替换更多角色的场景,需要扩展。
6. 语音朗读引擎
6.1 朗读流程
朗读引擎从语音菜谱 App 升级而来,核心逻辑一致:
startReading(): void {
this.isReading = true;
this.currentPara = 0;
let paras = this.getCurrentParagraphs();
this.readTimer = setInterval(() => {
if (this.currentPara < paras.length - 1) {
this.currentPara++;
} else {
this.stopReading();
promptAction.showToast({ message: '🌙 故事讲完了,晚安', duration: 2000 });
}
}, 5000);
}
6.2 与语音菜谱的对比
两个 App 的朗读引擎在核心上一致,但有三个关键差异:
| 特性 | 语音菜谱 | 睡前故事 |
|---|---|---|
| 步进间隔 | 4 秒 | 5 秒 |
| 高亮对象 | 步骤(数字编号圆形) | 段落(背景色) |
| 结束提示 | “🔊 朗读完毕” | “🌙 故事讲完了,晚安” |
5 秒/段的步进步长比菜谱的 4 秒更长,因为故事段落通常比菜谱步骤长。
6.3 段落高亮
当前朗读的段落使用淡紫色背景高亮:
.backgroundColor(
this.isReading && idx === this.currentPara ? C.primary + '10' : 'transparent')
10% 透明度的紫色背景足够明显但不刺眼,适合睡前昏暗的光线环境。
6.4 朗读的停止条件
// 1. 用户手动点击"停止"
// 2. 所有段落朗读完毕
// 3. 用户关闭详情弹窗
// 4. App 进入后台/销毁(aboutToDisappear)
四重停止条件与语音菜谱一致,确保定时器在任何情况下都不会泄漏。
7. 主题筛选与分类浏览
7.1 6 种主题
const THEMES: string[] = ['🌙 童话', '🦊 动物', '🚀 冒险', '🧚 魔法', '🌊 自然', '🤗 晚安'];
每个主题前有一个 Emoji 图标,在 Grid 选择器中展示时一目了然。
7.2 主题选择器
主题选择器使用 3 列 Grid 展示 6 种主题:
Grid() {
ForEach(THEMES, (t: string) => {
GridItem() {
Text(t)
.fontColor(this.filterTheme === t ? C.primary : C.text)
.backgroundColor(this.filterTheme === t ? C.primary + '15' : C.cardBg)
.borderWidth(this.filterTheme === t ? 1 : 0)
.onClick(() => {
this.filterTheme = this.filterTheme === t ? '' : t;
})
}
}, (t: string) => t)
}.columnsTemplate('1fr 1fr 1fr')
点击已选中的主题会取消筛选(回到全部),体现了"即点即切换,再点取消"的交互模式。这个模式在系列中被多次使用(如收藏筛选、情绪选择器)。
7.3 列表中的主题标签
每个故事卡片在列表中展示主题标签:
🐰 月亮上的兔子
🌙 童话 · 5 分钟 · 2-6 岁
从前,在很远很远的月亮上...
主题标签使用主题色(C.primary: #5C6BC0),时长和年龄使用辅助色(C.textLight),层次分明。
8. 收藏系统与轻量持久化
8.1 收藏存储
与语音菜谱 App 一致,收藏数据只存储故事的 ID 列表:
async saveData(): Promise<void> {
if (this.pref) {
let ids = this.stories.filter(s => s.isFavorite).map(s => s.id);
await this.pref.put(STORAGE_KEY, JSON.stringify(ids));
await this.pref.flush();
}
}
8.2 收藏恢复
async loadData(): Promise<void> {
let savedIds = JSON.parse(v as string) as number[];
for (let i = 0; i < this.stories.length; i++) {
if (savedIds.indexOf(this.stories[i].id) >= 0) {
this.stories[i].isFavorite = true;
}
}
this.stories = this.stories.concat([]);
}
这个模式在系列中已经使用了三次(语音菜谱、睡前故事),证明其可靠性和简洁性。
8.3 收藏列表
收藏 Tab 在空状态时显示引导提示:
⭐
还没有收藏的故事
在故事详情中点击 ⭐ 收藏
有收藏时,列表展示与故事列表一致,但收藏状态以金色星星(C.star: #FFD54F)突出显示。
9. 适老化与儿童友好设计
9.1 星空夜晚主题
本 App 首次使用深色主题——深蓝星空渐变背景:
const C: ColorScheme = {
primary: '#5C6BC0', // 紫色
bgStart: '#1A1A2E', // 深蓝
bgEnd: '#16213E', // 深蓝
cardBg: '#FFFFFF', // 白色(弹窗/卡片)
text: '#1A1A2E', // 深色正文
star: '#FFD54F', // 金色星星
moon: '#E8EAF6', // 月光白
};
深色背景 + 白色卡片的设计既减少了屏幕在暗光环境下的刺眼感,又保证了卡片内容的可读性。
9.2 字体与触摸目标
虽然是儿童/家长使用而非老年人,但设计上仍然保持了较大的字体和触摸目标:
| 元素 | 字号 | 触摸目标 |
|---|---|---|
| 故事标题 | 17px | 全宽卡片 |
| 故事正文 | 15px | - |
| 标签信息 | 12-13px | - |
| Tab 图标 | 20-24px | ~50px |
| 定制输入框 | 14px | 36px 高 |
9.3 安全设计
本 App 在儿童使用场景下做了几个安全设计:
- 没有广告:所有内容都是本地内置,不联网
- 没有敏感内容:所有故事都是积极向上的主题
- 退出确认:弹窗都有明确的关闭按钮
- 朗读自动停止:读完所有段落后自动停止,不会继续占用设备
10. 编译错误全记录
10.1 错误概览
本 App 出现 1 个编译错误。
| # | 错误类型 | 位置 | 根因 |
|---|---|---|---|
| 1 | 方法重复定义 | 第 587 行 | getFilteredStories() 被定义两次 |
10.2 唯一的错误:方法重复
现象:编译报错 “Duplicate function implementation” 或类似错误。
根因:在重构代码时,删除了 getFilteredStoriesList() 方法但错误地在原位置留下了一个空的 getFilteredStories() 声明头。同时原始的 getFilteredStories() 方法(第 342 行)仍然存在。编译器检测到两个同名方法。
// 第 342 行(正确)
getFilteredStories(): Story[] {
if (this.filterTheme === '') return this.stories;
return this.stories.filter(s => s.theme === this.filterTheme);
}
// 第 587 行(重复,空的声明头)
getFilteredStories(): Story[] { // 重复定义
修复:删除第 587 行的空方法声明。
教训:在手动编辑代码时,注意检查是否在错误的位置插入了代码。本次错误是在删除 getFilteredStoriesList() 并将其替换为上一行的方法声明头时,没有意识到该位置已经有一个同名方法了。
10.3 十九款 App 错误数趋势
22 → 17 → 16 → 1 → 12 → 12 → 10 → 4 → 11 → 11 → 3 → 8 → 7 → 12 → 1 → 4 → 3 → 2 → 1
第十九款 App 的错误数为 1 个,追平了系列最低纪录(尴尬粉碎机、绿植领养)。
10.4 关于"轮次归零"的思考
经过十九款 App 的实践,从第 10 款开始,错误数就已经稳定在个位数。第 18 款 2 个错误,第 19 款 1 个错误——这不是偶然的下降,而是模式复用的必然结果。
每一款新 App 的开发模式:
- 复用前作的 ColorScheme + 三 Tab + 弹窗模式(0 错误)
- 根据 App 特性新增数据模型和业务逻辑(少数错误)
- 修复少量错误(1-2 轮)
- 验证通过
当模式足够成熟,新增代码足够少时,错误数自然趋向于 0。
11. 十九款 App 全景回顾
11.1 数据总览
| # | App | 行数 | 错误数 | Type |
|---|---|---|---|---|
| 1 | 🎵 白噪音 | 767 | 16 | 工具 |
| 2 | ⏳ 时间胶囊 | 955 | 17 | 工具 |
| 3 | 🧊 冰箱剩菜 | 1320 | 22 | 工具 |
| 4 | 😅 尴尬粉碎机 | 953 | 1 | 工具 |
| 5 | 🛡️ 防骗训练 | 1038 | 12 | 教育 |
| 6 | 💡 碎片学习 | 851 | 12 | 教育 |
| 7 | 🐶 宠物日记 | 450 | 10 | 工具 |
| 8 | 🗑️ 情绪垃圾桶 | 390 | 4 | 工具 |
| 9 | 🧭 线下寻宝 | 447 | 11 | 社交 |
| 10 | 🗡️ 订阅刺客 | 478 | 11 | 工具 |
| 11 | 🎑 声音明信片 | 458 | 3 | 工具 |
| 12 | 🎲 家庭大富翁 | 537 | 8 | 游戏 |
| 13 | 📚 二手书漂流瓶 | 452 | 7 | 社交 |
| 14 | 🧹 废话过滤器 | 542 | 12 | 工具 |
| 15 | 🌱 绿植领养 | 530 | 1 | 社交 |
| 16 | 🌙 梦境解析 | 614 | 4 | 工具 |
| 17 | 🏕️ 断网挑战营 | 418 | 3 | 工具 |
| 18 | 👨🍳 语音菜谱 | 498 | 2 | 工具 |
| 19 | 🌙 睡前故事 | 668 | 1 | 教育 |
11.2 "讲给孩子听"的三款 App
系列中有三款 App 可以归类为"适合与孩子一起使用":
| App | 交互方式 | 核心价值 |
|---|---|---|
| 🐶 宠物日记 | 记录 + 统计 | 培养责任感 |
| 🦊 睡前故事 | 朗读 + 定制 | 亲子阅读时间 |
| 👨🍳 语音菜谱 | 朗读 + 步骤 | 亲子烹饪时间 |
这三款 App 的共同点是:App 不是被孩子独自使用的,而是家长和孩子一起使用。App 提供了内容(故事步骤/菜谱步骤),家长操作,孩子参与体验。
11.3 替换引擎类的三款 App
系列中有三款 App 实现了"替换"功能:
| App | 替换对象 | 引擎 |
|---|---|---|
| 😅 尴尬粉碎机 | 场景随机生成 | 随机选择 + 拼接 |
| 🌙 梦境解析 | 关键词匹配 | indexOf + 遍历 |
| 🌙 睡前故事 | 角色/地点替换 | replaceAll |
三种替换引擎使用的技术不同,但核心思想一致:将用户输入与结构化数据结合,生成个性化的输出。
11.4 十九款 App 的关键教训
| # | App | 最大教训 |
|---|---|---|
| 1 | 白噪音 | 颜色对象需要 interface |
| 2 | 时间胶囊 | @Builder 不能用 let |
| 3 | 冰箱剩菜 | 闭包不能传给 @Builder |
| 4 | 尴尬粉碎机 | 模式复用可大幅降错 |
| 5 | 防骗训练 | 大段 Builder 分批重构 |
| 6 | 碎片学习 | ForEach key 函数作用域 |
| 7 | 宠物日记 | 紧凑风格减少 50% 代码 |
| 8 | 情绪垃圾桶 | ForEach key 用值本身 |
| 9 | 线下寻宝 | 残留代码导致级联错误 |
| 10 | 订阅刺客 | 暗色主题设计 |
| 11 | 声音明信片 | setInterval 要清理 |
| 12 | 家庭大富翁 | 展开运算符替代 |
| 13 | 二手书漂流瓶 | @Builder 注解不能缺 |
| 14 | 废话过滤器 | Text 组件不支持变量声明 |
| 15 | 绿植领养 | 重构也可能引入错误 |
| 16 | 梦境解析 | 内联对象不能作类型 |
| 17 | 断网挑战营 | 已知错误也会重复犯 |
| 18 | 语音菜谱 | 肌肉记忆比语法更难改 |
| 19 | 睡前故事 | 删除代码要检查残留 |
11.5 第 19 条教训:删除代码要检查残留
第 19 条教训是一条关于代码清理的教训。
在重构过程中删除了 getFilteredStoriesList() 方法,但在原位置留下了一个空的 getFilteredStories() 声明头。原因是使用了"编辑文件"工具时,替换文本没有完全匹配旧内容。
这个错误虽然是使用 AI 编辑工具时产生的特定问题,但反映了一个通用的编程原则:删除代码后,要检查是否还有残留。常见的残留包括:
- 空方法声明(如本次)
- 无引用的 import
- 孤立的注释
- 未使用的变量
养成"删除后执行一次完整构建"的习惯,可以快速发现这类残留。
12. 结语
12.1 十九款 App 的开发历程
App1 🎵 白噪音 → 初识 ArkUI
App2 ⏳ 时间胶囊 → 数据持久化
App3 🧊 冰箱剩菜 → Tab 架构
App4 😅 尴尬粉碎机 → 模式复用
App5 🛡️ 防骗训练 → 适老化
App6 💡 碎片学习 → 学习激励
App7 🐶 宠物日记 → 紧凑风格
App8 🗑️ 情绪垃圾桶 → 情感交互
App9 🧭 线下寻宝 → 社交互动
App10 🗡️ 订阅刺客 → 暗色主题
App11 🎑 声音明信片 → 模拟录音
App12 🎲 家庭大富翁 → 回合制游戏
App13 📚 二手书漂流瓶 → 随机匹配
App14 🧹 废话过滤器 → 自然语言检测
App15 🌱 绿植领养 → 缘分匹配
App16 🌙 梦境解析 → 潜意识探索
App17 🏕️ 断网挑战营 → 行为养成
App18 👨🍳 语音菜谱 → 适老化设计
App19 🌙 睡前故事 → 故事定制
12.2 替换引擎:从第三款到第十九款
替换引擎这个概念在系列中经历了三次迭代:
- 第三款(冰箱剩菜):写死在 Builder 中的条件判断,不可配置
- 第十四款(废话过滤器):基于规则库的 indexOf 匹配,可配置规则
- 第十九款(睡前故事):基于 replaceAll 的字符串替换,用户可输入替换值
这种演进体现了从"硬编码"到"规则驱动"再到"用户驱动"的三级跃迁。
12.3 ArkUI 的最终评价(第六次修订)
经过十九款 App 的实践,ArkUI 的评价已经非常成熟。所有重要的"不足"都在第 14 篇博客中完整记录,后续 App 没有遇到任何新的错误类型——只有重复犯的旧错误和代码清理相关的错误。
核心优势(与前作一致):
- 声明式 DSL + @State 响应式
- 编译检查有效
- Preferences API 简洁
核心不足(与前作一致,但没有新发现):
- @Builder 语法约束严格
- 展开运算符限制
- 部分 API 差异
12.4 给开发者的建议
-
段落存储比整段文本更灵活:如果内容需要分步展示或逐段处理,用数组存储比用大段字符串好 10 倍。
-
替换引擎虽然简单但有效:
replaceAll+customizeChar= 孩子听到自己名字时的惊喜。有时候最简单的技术方案就是最好的方案。 -
深色主题关注对比度:深色背景上用浅色文字是基本的,但卡片内部的文字仍然需要足够的对比度。白色卡片 + 深色文字是安全的选择。
-
删除代码后要检查:养成"删除三查"的习惯——查 import、查方法名、查引用。
12.5 感谢与展望
十九款 App、十九篇博客、约 200,000 字。从第一款白噪音 22 个错误到第十九款睡前故事 1 个错误——这条曲线本身就是这本书最好的总结。
现在,打开 DevEco Studio,去创造属于你自己的 App 吧。
附录 A:第十九款 App 核心代码
角色替换引擎
getCustomizedParas(): string[] {
if (this.selected === null) return [];
return this.selected.paragraphs.map(p =>
p.replaceAll(this.selected!.defaultChar, this.customizeChar)
.replaceAll(this.selected!.defaultPlace, this.customizePlace)
);
}
语音朗读引擎
startReading(): void {
this.isReading = true; this.currentPara = 0;
let paras = this.getCurrentParagraphs();
this.readTimer = setInterval(() => {
if (this.currentPara < paras.length - 1) this.currentPara++;
else { this.stopReading(); promptAction.showToast({ message: '🌙 故事讲完了,晚安', duration: 2000 }); }
}, 5000);
}
stopReading(): void {
this.isReading = false; this.currentPara = 0;
if (this.readTimer >= 0) { clearInterval(this.readTimer); this.readTimer = -1; }
}
收藏持久化(ID 列表)
async saveData(): Promise<void> {
let ids = this.stories.filter(s => s.isFavorite).map(s => s.id);
await this.pref.put(STORAGE_KEY, JSON.stringify(ids));
await this.pref.flush();
}
附录 B:系列速查
| 指标 | 数值 |
|---|---|
| App 数量 | 19 |
| 博客总字数 | ~200,000 字 |
| 代码总行数 | ~12,400 行 |
| 编译错误 | ~175 个 |
| @Builder 方法 | ~260 个 |
| 修复轮次 | 37 轮 |
更多推荐




所有评论(0)