news 2026/9/12 2:23:26

HarmonyOS日记本应用开发:Stage模型、ArkTS与本地存储实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS日记本应用开发:Stage模型、ArkTS与本地存储实践

简介:基于HarmonyOS打造的个人日记本应用完整源码,面向有一定ArkTS基础或对鸿蒙应用开发感兴趣的开发者,覆盖从登录、日记列表、撰写编辑到多媒体记录与关系型数据库存储等核心功能,并展示了跨设备数据同步思路,适合日常记录与鸿蒙应用开发练手。压缩包共101个文件,体积仅391KB,其中26个ets界面与逻辑文件构成主要页面,11个json和9个json5文件负责工程配置与模块声明,7个ts文件提供辅助脚本,另有39个png图片、1个gif动效及项目构建说明等,整体结构清晰,便于按模块阅读。已有437人学习或下载。代码围绕日记列表、写日记、登录页、下拉刷新、数据库工具等典型模块展开,覆盖界面布局、事件交互、本地数据持久化、智能提醒、内容加密与社区交互等实现,能帮助理解HarmonyOS应用工程的组织方式,也可直接作为二次开发或毕设源码基础,模块注释规范、命名清晰,方便在DevEco Studio中打开研读,对想了解鸿蒙系统开发规范与完整应用落地的读者很有价值。

1. 为什么在 HarmonyOS 里做日记本:Stage 模型、ArkTS 与本地优先的选型

个人日记本这类 APP,功能边界最简单,技术容量却一点不小。文本编辑、日期索引、图片附件、数据持久化、隐私保护,每一样都要选型,而这些选型在 HarmonyOS 上的答案和 Android、iOS 并不完全一样。拿到一份“基于 HarmonyOS 开发的个人日记本应用 APP 源代码”,第一件事不是急着看页面代码,而是先搞懂它跑在哪个应用模型上、UI 是声明式还是命令式、数据层用的是关系型数据库还是首选项。这三个问题决定了后续改功能、换图标、加搜索时是不是要大重构。常见方案是 ArkTS + ArkUI 声明式开发 + Stage 模型,配合 RelationalStore 存正文元数据、文件目录存图片,这套组合对日记场景足够稳,也是 HarmonyOS 应用基础认证考试覆盖的主流路线。下面从工程结构开始,把一套可以落地的日记本源码拆开讲。

2. 搭 ArkTS 工程:从 DevEco Studio 空工程到第一次运行

2.1 拿到源代码先看哪几个文件

HarmonyOS 工程和 Android 工程最大的差异在目录约定。打开一份日记本项目,先别急着点运行,按这个顺序看文件:

  • build-profile.json5:确认compatibleSdkVersiontargetSdkVersion,源码能不能编译,九成取决于这里的版本与本地 SDK 是否匹配。
  • AppScope/app.json5:应用级配置,bundleName是应用唯一标识,改包名要同步这里。
  • entry/src/main/module.json5:模块级配置,页面路径、权限声明、入口 Ability 都在这里。
  • entry/src/main/ets/pages:ArkTS 页面目录,日记列表、编辑页、设置页一般都在这个目录下。
  • entry/src/main/resources/base/profile/main_pages.json:页面路由注册表。每新增一个页面,必须在这个 JSON 里登记,否则router.pushUrl会直接报错找不到页面。
文件路径作用常见踩坑
build-profile.json5SDK 版本、签名配置版本号对不上,编译报 API 不匹配
module.json5权限、Ability 声明没声明权限,运行时静默失败
main_pages.json页面路由表新增页面忘记注册,跳转白屏
entry/src/main/etsArkTS 源码目录页面代码全放 pages 会导致路由混乱

提示:main_pages.json里第一个页面就是应用冷启动入口。日记本 App 通常是“日记列表页”而不是“新建页”,改启动页只调这个数组的顺序即可,不要动 Ability 的onWindowStageCreate

2.2 最小可运行的日记编辑页

不管源代码多复杂,核心页面永远长这样:一个标题输入框、一个正文多行输入框、一个保存按钮。用 ArkUI 写出来是这样的:

@Entry @Component struct DiaryEditorPage { @State title: string = ''; @State content: string = ''; build() { Column({ space: 12 }) { TextInput({ placeholder: '补一个标题,比如:今天加班改 bug', text: this.title }) .onChange((value: string) => { this.title = value; }) TextArea({ placeholder: '写点细节,别只写“今天很累”', text: this.content }) .onChange((value: string) => { this.content = value; }) Button('保存这篇日记') .width('100%') .onClick(() => { // 这里不要直接写数据库操作,应该调数据层方法 saveDiary(this.title, this.content); }) } .padding(16) .width('100%') .height('100%') .backgroundColor('#F5F5F5') } }

逻辑说明:@State装饰器是 ArkUI 声明式响应的核心,titlecontent一旦被onChange回调改写,UI 会自动刷新,不需要像传统命令式写法那样手动操作组件实例。

参数说明:TextInput适合单行输入,TextArea适合多行正文;.width('100%').padding(16)是链式调用的通用属性方法,顺序不影响结果。保存按钮的onClick里只调saveDiary函数,具体插入逻辑放在数据层,这是让源码可维护的关键习惯。

2.3 在模拟器与真机上跑通

DevEco Studio 里点右上角 Run 按钮之前,确认三件事:登录华为账号并完成自动签名,否则 HAP 装不进设备;开发者模式已开启,通过hdc list targets能看到设备;模拟器冷启动较慢,首次编译要等几十秒属正常现象。

命令行方式也可以验证构建产物:

hdc list targets hdc install entry/build/default/outputs/default/entry-default-signed.hap hdc shell aa start -a EntryAbility -b com.example.diary

说明:hdc list targets查看设备连接状态;hdc install安装签名后的 HAP 包;aa start-a指定 Ability 名、-b指定 bundleName,用来验证应用启动是否正常。如果aa start报错,先回module.json5里核对 Ability 名称是否写成了EntryAbility,这是初学者最容易踩的坑。

3. 写日记的核心:Editor 组件、RDB 存储与 DatePicker 索引

3.1 日记表字段设计:日期字符串与时间戳的边界

日记本的数据结构不同 App 差异很大,但“按天查看”这个核心诉求决定了表结构的关键设计:日期字段存纯日期字符串,时间戳单独存。把2025-06-14拆成day字段存TEXT,把created_atupdated_at存成INTEGER毫秒时间戳。这样做的原因是 SQL 查询时可以直接用等值匹配查某一天的日记,而不需要写BETWEEN 00:00:00 AND 23:59:59这种区间条件。

字段名类型说明
idINTEGER主键,自增
titleTEXT标题,允许为空
contentTEXT正文,核心字段
dayTEXT日期,格式yyyy-MM-dd,加索引
moodTEXT心情标签,如“开心”“疲惫”
imagesTEXT图片相对路径的 JSON 数组
created_atINTEGER首次保存时间戳
updated_atINTEGER最近修改时间戳

3.2 用 RelationalStore 初始化数据库

HarmonyOS 的 RelationalStore 是官方关系型数据库能力,适合日记这类结构化数据。初始化代码:

import relationalStore from '@ohos.data.relationalStore'; import common from '@ohos.app.ability.common'; const DB_NAME = 'diary.db'; const TABLE_NAME = 'diary'; async function initDb(context: common.UIAbilityContext): Promise<relationalStore.RdbStore> { const config: relationalStore.StoreConfig = { name: DB_NAME, securityLevel: relationalStore.SecurityLevel.S1, }; const store = await relationalStore.getRdbStore(context, config); await store.executeSql( `CREATE TABLE IF NOT EXISTS ${TABLE_NAME} ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL DEFAULT '', content TEXT NOT NULL DEFAULT '', day TEXT NOT NULL, mood TEXT DEFAULT '', images TEXT DEFAULT '[]', created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL )` ); await store.executeSql( `CREATE INDEX IF NOT EXISTS idx_diary_day ON ${TABLE_NAME} (day)` ); return store; }

逻辑说明:getRdbStore是异步接口,首次调用会创建数据库文件并缓存连接,后续调用复用同一实例。建表语句里的IF NOT EXISTS保证重复初始化不报错,这是源码里必须有的防御性写法。

参数说明:securityLevel是 RelationalStore 的隐私分级参数,S1 最低、S4 最高。日记内容属于个人隐私,按华为应用市场的审核惯例,建议至少设为 S2。如果源代码里用的是 S1且要上架,这里需要改。

3.3 插入、更新与按日查询

插入日记时把day格式化好,图片列表序列化成 JSON 字符串:

async function insertDiary(store: relationalStore.RdbStore, data: DiaryItem): Promise<number> { const values: relationalStore.ValuesBucket = { 'title': data.title, 'content': data.content, 'day': data.day, 'mood': data.mood, 'images': JSON.stringify(data.images), 'created_at': Date.now(), 'updated_at': Date.now(), }; return await store.insert(TABLE_NAME, values); }

查询某一天的日记用RdbPredicates构造等值条件:

async function queryByDay(store: relationalStore.RdbStore, day: string): Promise<DiaryItem[]> { const predicates = new relationalStore.RdbPredicates(TABLE_NAME); predicates.equalTo('day', day).orderByDesc('created_at'); const resultSet = await store.query(predicates, ['id', 'title', 'content', 'day', 'mood', 'images', 'created_at']); const list: DiaryItem[] = []; while (resultSet.goToNextRow()) { list.push({ id: resultSet.getLong(resultSet.getColumnIndex('id')), title: resultSet.getString(resultSet.getColumnIndex('title')), content: resultSet.getString(resultSet.getColumnIndex('content')), day: resultSet.getString(resultSet.getColumnIndex('day')), mood: resultSet.getString(resultSet.getColumnIndex('mood')), images: JSON.parse(resultSet.getString(resultSet.getColumnIndex('images'))), createdAt: resultSet.getLong(resultSet.getColumnIndex('created_at')), }); } resultSet.close(); // 不 close 会造成游标泄漏,查询变慢 return list; }

逻辑说明:ResultSet类似数据库游标,goToNextRow()判断是否有下一行,配合getColumnIndex定位列。这里最容易出的问题是忘记close(),导致后续查询越跑越慢。images字段因为是 JSON 数组,取出后要JSON.parse还原成数组。

参数说明:orderByDesc('created_at')让同一天的多条记录按时间倒序,适合“补写多条”的场景。如果改成orderByAsc,列表顺序会反过来,按用户习惯调整即可。

3.4 日期切换:DatePicker 的月份从 0 开始

日记本的日期索引层,常见做法是页面顶部放一个日期选择器。HarmonyOS 的DatePicker组件使用方式:

@State selectedDate: Date = new Date(); // 把 Date 格式化为 yyyy-MM-dd,注意 getMonth() 从 0 开始计数 function formatDay(d: Date): string { const y = d.getFullYear().toString().padStart(4, '0'); const m = (d.getMonth() + 1).toString().padStart(2, '0'); const day = d.getDate().toString().padStart(2, '0'); return `${y}-${m}-${day}`; } // 在 build() 中使用 DatePicker({ selected: this.selectedDate, onDateChange: (value: Date) => { this.selectedDate = value; this.currentDay = formatDay(value); this.loadDiary(this.currentDay); } })

逻辑说明:onDateChange回调返回的是完整Date对象,必须通过formatDay转成字符串再传给查询层。getMonth()返回 0-11,不做+1处理会让索引永远差一个月,这个 bug 在源码里非常常见。

参数说明:selected属性是受控绑定,外部修改selectedDate会同步滚轮位置;onDateChange只在用户滑动时触发,代码里主动set日期不会触发回调,所以页面加载时要手动调一次loadDiary

4. 让日记更像产品:图片附件、草稿自动保存与密码锁

4.1 图片附件:photoAccessHelper 选图后复制到沙箱

老源码里常见用mediaLibrary.requestPermission申请读图权限,但 API 10 之后这条路已经废弃,换成PhotoViewPicker免权限选图。把选中的图片复制到应用沙箱,既能避免权限问题,又能防止原图被删除导致日记图片失效:

import picker from '@ohos.file.picker'; import fs from '@ohos.file.fs'; async function attachImages(context: common.UIAbilityContext): Promise<string[]> { const photoPicker = new picker.PhotoViewPicker(); const result = await photoPicker.select({ MIMEType: picker.PhotoViewMIMETypes.IMAGE_TYPE, maxSelectNumber: 9, }); const dir = `${context.filesDir}/diary_images`; fs.mkdirSync(dir); const savedPaths: string[] = []; for (let i = 0; i < result.photoUris.length; i++) { const dest = `${dir}/${Date.now()}_${i}.jpg`; await fs.copyFile(result.photoUris[i], dest); savedPaths.push(dest); } return savedPaths; }

逻辑说明:PhotoViewPicker.select返回的是系统授权后的临时 URI 列表,应用只能通过这个 URI 读取内容。直接保存 URI 到数据库并不可靠,因为系统可能在应用退出后回收访问权限。因此选完图立刻copyFilefilesDir才是正确用法。

参数说明:maxSelectNumber: 9控制一次最多选 9 张;MIMEType过滤文件类型,只选图片。copyFile的源参数支持file://协议 URI,这是 API 10 之后的行为。如果源码里用的是fs.open+ 手动读取字节的老写法,可以整体替换成这段。

4.2 草稿自动保存:防抖定时器与页面生命周期

写日记是高频输入场景,每敲一个字就写一次数据库会产生大量无效写入。常见做法是加一个 800 毫秒的防抖定时器,停止输入后才落盘:

private timer: number = -1; private scheduleSave(newContent: string) { if (this.timer !== -1) { clearTimeout(this.timer); } this.timer = setTimeout(() => { this.saveDraft(newContent); this.timer = -1; }, 800); } aboutToDisappear() { if (this.timer !== -1) { clearTimeout(this.timer); this.saveDraft(this.content); } }

逻辑说明:scheduleSave每次被调用先清掉上一次定时器,再重新计时,只有停顿满 800 毫秒才触发saveDraft。这一步能把几百次输入合并成几次数据库写入。

参数说明:aboutToDisappear是 ArkUI 页面销毁生命周期,用户返回列表或退出应用时,如果定时器还没触发,就立即保存最后一次内容,避免草稿丢失。这种“防抖 + 兜底”组合是日记类源码最值得保留的部分。

4.3 密码锁:哈希校验与首选项的边界

给日记加上锁需要先想清楚一件事:HarmonyOS 应用沙箱内的数据库文件,普通用户拿不到,但 root 设备或备份恢复场景下可被读取。所以密码锁只防“打开应用的人”,不防“拿到文件的人”。存储密码校验值时不要存明文,常见做法是用cryptoFramework的 SHA-256 做摘要后加盐保存,验证时用同样流程计算后比对:

import preferences from '@ohos.data.preferences'; async function verifyPin(context: common.UIAbilityContext, input: string): Promise<boolean> { const pref = await preferences.getPreferences(context, { name: 'diary_security' }); const savedHash = await pref.get('pin_hash', ''); if (savedHash === '') { const newHash = sha256WithSalt(input); await pref.put('pin_hash', newHash); await pref.flush(); return true; } return savedHash === sha256WithSalt(input); }

逻辑说明:preferences是轻量键值库,适合存密码哈希这类小数据,不适合存日记正文。sha256WithSalt内部用cryptoFramework.createMd('SHA256')实现,具体构造方式在不同 API 版本下有差异,但核心思路一致:密码加固定盐值后取摘要,数据库中永远不出现原文。

参数说明:pref.flush()是持久化落盘操作,put之后不 flush,数据在应用重启后可能丢失。没有自定义生物认证需求时,密码锁做到这个程度已经够用;想用指纹,则需要额外接入@ohos.biometrics模块并申请ohos.permission.ACCESS_BIOMETRIC权限。

4.4 全文搜索:LIKE 查询与索引失效

按内容搜日记是本类 App 的高频诉求,直接给字段加索引并不能解决问题。SQL 里LIKE '%关键词%'这种前置通配写法无法利用索引,必须做全表扫描。日记数据量在万条以内时性能足够,不需要上全文索引:

async function searchDiary(store: relationalStore.RdbStore, keyword: string): Promise<DiaryItem[]> { const predicates = new relationalStore.RdbPredicates(TABLE_NAME); predicates.like('title', `%${keyword}%`).or().like('content', `%${keyword}%`); const resultSet = await store.query(predicates, ['id', 'title', 'content', 'day', 'created_at']); // 遍历逻辑同 queryByDay,这里省略 }

说明:.like().or().like()链式调用生成WHERE title LIKE ? OR content LIKE ?like条件用or连接时,要保证or()前后都有完整条件。搜索结果的排序建议加orderByDesc('updated_at'),因为用户更关心最近修改的日记,而不是创建最早的。如果后续日记量增长到数万条,再考虑引入fts4全文索引,现阶段不必过度设计。

5. 让简单功能具备工程化能力:签名打包、备份与代码级检查

5.1 DevEco Studio 签名打包的三步

从源码到可安装 HAP,必须经过签名。DevEco Studio 菜单栏打开 File > Project Structure > Signing Configs,勾选 Automatically generate signature,登录华为账号后会自动生成调试签名;上架华为应用市场需要的是发布证书,要进 AppGallery Connect 后台申请。

hdc file recv /data/app/el2/100/base/com.example.diary/haps/entry/files/diary.db ./backup/ hdc shell aa force-stop com.example.diary

第一行是导出应用沙箱里的数据库文件,com.example.diary要换成源码里的真实bundleName,路径中的el2/100指设备用户 100 的加密存储分区。第二行是强制停止应用,用来验证数据写入是否完整。日常调试时这两个命令组合使用,可以快速拿到真实数据排查问题。

5.2 应用内导出备份:JSON 兜底方案

沙箱数据库会随应用卸载而清空,这是源码交付后用户反馈“数据没了”的最常见原因。在设置页加一个“导出全部日记”按钮,比教用户备份文件更可靠。核心逻辑是把查询结果序列化成 JSON,用文件流写入沙箱,再引导用户通过分享或hdc取走:

async function exportToJson(context: common.UIAbilityContext, list: DiaryItem[]): Promise<string> { const dest = `${context.filesDir}/diary_backup_${Date.now()}.json`; const file = fs.openSync(dest, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC); fs.writeSync(file.fd, JSON.stringify(list, null, 2)); fs.closeSync(file.fd); return dest; }

说明:OpenMode.TRUNC表示打开时清空旧内容,防止同一命名下的文件残留过期数据;writeSync是同步写,对万条级别的日记数据量足够快,不需要引入异步任务队列。

5.3 交付前过一遍代码检查点

检查项操作达标标准
SQL 注入所有查询走RdbPredicates参数绑定无字符串拼接 SQL
游标泄漏搜索ResultSetclose()调用每个查询路径都有 close
敏感权限module.json5里逐项核对没有READ_MEDIA之类旧权限
防抖兜底aboutToDisappear中保存草稿强制停止应用后草稿不丢
日期格式所有getMonth()是否 +1跨月日记索引正确
图片引用图片存沙箱相对路径不直接存原图 URI

最后再验证一遍:清空应用数据后安装 HAP,确认首次启动能自动建库;写一篇带图片的日记,杀进程重启,确认数据和图片都在;切换日期到昨天再切回来,确认索引无串号。这三步跑通,这份 HarmonyOS 日记本源码才算真正交付完成。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 2:19:04

Kaggle MNIST ZIP源码本地复现指南:绕过torchvision 404与CUDA适配

简介&#xff1a;本资源是一份面向深度学习初学者与Kaggle入门者的MNIST手写数字识别竞赛实战源码包&#xff0c;聚焦图像分类任务的端到端实现&#xff0c;解决模型构建、训练调优与结果提交等核心问题。压缩包共9个文件&#xff0c;包含3个关键数据/模型压缩包&#xff08;tr…

作者头像 李华
网站建设 2026/9/12 2:18:59

IC烧录:芯片量产的隐形门槛与可靠性核心

1. 这个“烧录”不是烤芯片&#xff0c;而是芯片出厂前的最后一道指纹刻印 IC烧录这个词&#xff0c;乍一听容易让人联想到车间里高温烘烤的流水线——其实完全不是。它更像给新生儿打疫苗时在手臂上留下的那一针&#xff1a;看不见、摸不着&#xff0c;但决定了这个“生命体”…

作者头像 李华
网站建设 2026/9/12 2:17:55

基于Java Web的图书馆管理系统:源码、数据库设计与部署实践

简介&#xff1a;这是一份基于Java Web的图书馆管理系统课程设计资源&#xff0c;面向正在学习Java Web开发或需要完成课程设计的学生。项目使用Eclipse与MySQL构建&#xff0c;包含图书借阅、归还、查询、读者管理、登录验证等常见功能模块。压缩包共179个文件&#xff0c;约4…

作者头像 李华
网站建设 2026/9/12 2:17:43

Spring Boot与微信小程序实现路线分享系统开发

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 2:15:32

鲸跃Link双机穿梭原理:物理层协议实现无感跨屏

1. 项目概述&#xff1a;这不是一个“鼠标”&#xff0c;而是一套跨设备协同的物理交互协议 “鲸跃Link 鼠标双机穿梭器 轻轻一划穿梭两台电脑”——这个标题里藏着三个被大众严重低估的关键信息点&#xff1a; “双机”不是泛指&#xff0c;“穿梭”不是动效&#xff0c;“轻…

作者头像 李华