集光 - 智能笔记应用
📱 应用简介
集光是一款专为HarmonyOS生态设计的智能笔记应用,提供安全、高效、美观的笔记管理体验。应用集成了华为账号服务,支持多种笔记编辑功能,让您的创意和想法得到完美记录。
✨ 核心特性
🔐 安全可靠
- 生物识别解锁:支持指纹、面部识别等生物认证
- 手势密码:自定义手势密码保护隐私内容
- 应用锁:为整个应用提供额外的安全保护
📝 强大编辑
- 富文本编辑:支持文字样式、颜色、大小等格式设置
- 多媒体支持:插入图片、截图,丰富笔记内容
- 智能排版:自动优化文本布局,提升阅读体验
🗂️ 智能管理
- 分类管理:创建、编辑、删除笔记分类
- 搜索功能:快速定位所需笔记内容
- 多种视图:列表视图和卡片视图自由切换
- 排序方式:按创建时间、修改时间灵活排序
🌙 个性化体验
- 夜间模式:护眼模式,适合夜间使用
- 主题切换:支持明暗主题自动切换
- 多选操作:批量管理笔记,提高效率
🏗️ 技术架构
模块化设计
集光
├── 主应用模块 (product/phone)
├── 公共组件 (components)
│ ├── 应用密码设置 (secretlock)
│ ├── 富文本编辑器 (richeditor)
│ ├── 用户登录 (login)
│ ├── 意见反馈 (feed_back)
│ └── 应用更新检测 (check_app_update)
└── 公共工具 (common)
├── 数据源管理 (datasource)
└── 工具类库 (utils)
核心技术
- HarmonyOS 5.0+:原生HarmonyOS应用开发
- ArkTS:现代化的应用开发语言
- Stage模型:新一代应用开发模型
🚀 快速开始
环境要求
- DevEco Studio 5.0.3 Release 及以上
- HarmonyOS SDK 5.0.3 Release 及以上
- 支持设备:华为手机(包括双折叠和阔折叠)
- 系统版本:HarmonyOS 5.0.3(15) 及以上
安装配置
-
克隆项目
bashgit clone [项目地址] cd JiGaung -
配置华为账号服务
- 在AppGallery Connect创建应用
- 配置Client ID到
product/phone/src/main/module.json5 - 申请
quickLoginMobilePhone权限
-
应用签名
- 进行手工签名配置
- 添加证书公钥指纹
-
运行应用
- 连接调试设备
- 选择"Run > Run 'phone'"或"Run > Debug 'phone'"
📱 主要功能
首页功能
- 笔记展示:支持列表和卡片两种展示模式
- 分类管理:创建、编辑、删除笔记分类
- 搜索笔记:快速查找笔记内容
- 多选操作:长按多选,批量管理
- 排序功能:按时间排序,方便查找
笔记编辑
- 富文本编辑:支持文字格式、颜色、大小设置
- 图片插入:支持本地图片和截图插入
- 撤销重做:操作历史记录,支持撤销重做
- 内容分享:支持笔记内容分享到其他应用
- 自动保存:实时保存,防止内容丢失
个人中心
- 用户信息:华为账号一键登录,头像昵称管理
- 回收站:删除笔记恢复,彻底删除功能
- 隐私设置:生物识别、手势密码等安全设置
- 应用设置:夜间模式、通知设置、版本检测
- 意见反馈:问题反馈,帮助改进应用
🔧 开发指南
组件使用
如需单独使用某个组件,请参考对应组件的使用指导:
| 组件 | 描述 | 使用指导 |
|---|---|---|
| secretlock | 应用密码设置 | 使用指导 |
| richeditor | 富文本编辑器 | 使用指导 |
| login | 用户登录 | 使用指导 |
| feed_back | 意见反馈 | 使用指导 |
自定义开发
- 修改
AppScope/app.json5中的bundleName - 调整
AppScope/resources/base/element/string.json中的应用名称 - 根据需要修改主题色彩和样式
🧩 系统设计与实现说明
本节面向希望深入学习和二开的开发者,按"从需求到实现"的顺序介绍集光的整体架构、核心业务流程以及关键模块的实现思路。
1. 整体架构概览
-
UIAbility 入口层:
EntryAbility.ets作为应用入口,负责:- 初始化主题控制器
ThemeController,同步系统深浅色模式; - 初始化窗口尺寸,写入
AppStorage,供 UI 自适配(含折叠屏断点适配WindowUtil.registerBreakPoint); - 初始化数据库:调用
DatabaseManager.instance.initDatabase()创建和迁移数据表; - 配置状态栏、导航栏、通知授权等系统级能力。
- 初始化主题控制器
-
页面与导航层:
MainPage.ets作为主页面,使用Tabs + Navigation构建四个核心 Tab:PictureView:图片集NotesView:笔记集(本项目核心)ToDoView:待办集MineView:我的
- 通过
NavPathStack实现页面栈导航(如从NotesView跳转到EditNotes)。
-
业务页面层:
NotesView.ets:笔记列表页,负责分类筛选、多选操作、置顶、排序等;EditNotes.ets:笔记编辑页,集成富文本编辑器、背景色、分享/复制等能力;- 其它如
EditCategory.ets、SearchPage.ets等负责辅助业务。
-
组件与服务层:
- 公共组件在
components/下:richeditor:富文本编辑器组件与相关数据模型;secretlock:手势锁、应用锁逻辑;login:华为账号登录;feed_back:意见反馈;check_app_update:检查应用更新。
- 公共工具在
common/:datasource:数据库访问、实体与服务NoteService/CategoryService等;utils:GlobalInfoModel、AppUtil、WindowUtil等工具与全局状态。
- 公共组件在
-
数据存储层:
- 使用
@ohos.data.relationalStore构建notes.db; DatabaseManager.ets负责数据库初始化、迁移和默认数据插入。
- 使用
整体上可理解为:
UIAbility(入口) → MainPage(Tab 容器) → 各业务页面(Notes / EditNotes 等) → 业务服务层(NoteService / CategoryService) → DatabaseManager(RdbStore)
2. 数据模型与数据库设计
数据库通过 DatabaseManager 管理,使用 RDB 存储数据:
-
数据库配置:
- 名称:
notes.db - 安全级别:
SecurityLevel.S1
- 名称:
-
主要表结构:
categories表(分类):id TEXT PRIMARY KEY:分类 ID(含特殊 ID:-1= 全部笔记,-2= 未分类笔记);name TEXT:分类名称;totalCount INTEGER:该分类下笔记数量,用于快速展示计数。
notes表(笔记):id TEXT PRIMARY KEY:笔记 ID;title TEXT:标题;categoryId TEXT:所属分类 ID;content TEXT:纯文本内容;styledContent TEXT:富文本内容(通过StyleSerializer序列化的 JSON 字符串);description TEXT:用于卡片预览的摘要;createTime / updateTime TEXT:创建/更新时间;isPinned INTEGER:是否置顶;backgroundColor TEXT:笔记卡片/详情背景颜色;importance INTEGER:重要度等级;isDeleted INTEGER:是否进入回收站。
-
自动迁移逻辑:
DatabaseManager.migrateDatabase()使用PRAGMA table_info(notes)查询列信息;- 若缺少某个列(如
isPinned/backgroundColor/importance/styledContent/isDeleted),则通过ALTER TABLE动态新增; - 这样既支持老版本平滑升级,又避免手工 SQL 迁移出错。
-
默认分类初始化:
initDefaultCategories()保证:- 若不存在
id = '-1',则插入"全部笔记"; - 若不存在
id = '-2',则插入"未分类笔记"。
- 若不存在
关键实现示例(节选自
DatabaseManager.ets):
ts
export class DatabaseManager {
private static _instance: DatabaseManager;
private rdbStore: relationalStore.RdbStore | null = null;
private readonly STORE_CONFIG: relationalStore.StoreConfig = {
name: 'notes.db',
securityLevel: relationalStore.SecurityLevel.S1
};
async initDatabase(): Promise<void> {
const context = getContext(this);
this.rdbStore = await relationalStore.getRdbStore(context, this.STORE_CONFIG);
await this.createTables();
await this.migrateDatabase();
}
private async createTables(): Promise<void> {
if (!this.rdbStore) return;
const categoryTableSql = `
CREATE TABLE IF NOT EXISTS categories (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
totalCount INTEGER DEFAULT 0
)
`;
const noteTableSql = `
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
title TEXT,
categoryId TEXT,
content TEXT,
styledContent TEXT,
description TEXT,
createTime TEXT,
updateTime TEXT,
isPinned INTEGER DEFAULT 0,
backgroundColor TEXT DEFAULT "#fefefe",
importance INTEGER DEFAULT 0,
isDeleted INTEGER DEFAULT 0,
FOREIGN KEY (categoryId) REFERENCES categories (id)
)
`;
await this.rdbStore.executeSql(categoryTableSql);
await this.rdbStore.executeSql(noteTableSql);
await this.migrateDatabase();
await this.initDefaultCategories();
}
private async migrateDatabase(): Promise<void> {
if (!this.rdbStore) return;
const resultSet = await this.rdbStore.querySql('PRAGMA table_info(notes)');
let hasIsPinnedColumn = false;
// 省略其它列检查...
if (resultSet && resultSet.rowCount > 0) {
while (await resultSet.goToNextRow()) {
const columnName = await resultSet.getString(1);
if (columnName === 'isPinned') {
hasIsPinnedColumn = true;
}
// 根据实际名称检查 backgroundColor / importance / styledContent / isDeleted 等
}
}
if (!hasIsPinnedColumn) {
await this.rdbStore.executeSql('ALTER TABLE notes ADD COLUMN isPinned INTEGER DEFAULT 0');
}
// 同理为其它字段做 ALTER TABLE
}
private async initDefaultCategories(): Promise<void> {
if (!this.rdbStore) return;
const allNotesCountResult = await this.rdbStore
.querySql('SELECT COUNT(*) as count FROM categories WHERE id = ?', ['-1']);
// 若不存在"全部笔记",则插入默认分类
// ...
}
}
3. 首页与笔记列表(NotesView)实现思路
3.1 MainPage:Tab 容器
- 使用
Tabs+TabContent实现四个子页面:图片集、笔记集、待办集、我的; - 自定义
tabBarBuilder绘制底部图标和文字; - 通过
curIndex控制当前选中 Tab; - 使用
GlobalInfoModel与各页面共享刷新方法,例如:- 当切换到"笔记集"Tab 时,调用
globalInfo.refreshNotesView()强制 NotesView 刷新数据; - 当切换到"图片集"Tab 时,调用
globalInfo.refreshPictureView()刷新图片数据。
- 当切换到"笔记集"Tab 时,调用
这种设计的好处是:Tab 之间无需强耦合,统一通过 GlobalInfoModel 进行"跨页面刷新"通信。
3.2 NotesView:笔记列表核心逻辑
NotesView.ets 是笔记模块的核心列表页面,主要职责:
-
业务服务依赖:
NoteService:提供笔记增删改查;CategoryService:管理分类与计数;SortController:管理排序字段与顺序;NoteSearchController:管理搜索关键字与结果;SelectedController:管理多选状态;SettingController:应用设置相关(如通知授权)。
-
本地状态管理:
@Local noteList: Note[]:当前列表中的笔记;@Local dataList: LazyDataSource<Note>:用于懒加载/瀑布流展示的数据源;@Local categoryList: Array<Category>:分类列表;@Local showListType: SHOW_METHOD_ENUM:列表/卡片等展示方式。
-
生命周期逻辑(aboutToAppear):
- 重置多选状态:
selectedController.recoverInitState(); - 若开启手势锁,则跳转至
DrawLock页面; - 重置搜索关键字,获取默认分类
getFirstCategory(); - 根据当前分类、排序、搜索条件获取笔记:
noteService.getNoteList(...); - 初始化所有笔记的选中状态:
noteService.initSelectedState(); - 遍历所有分类并统计每个分类的笔记数量:
refreshAllCategoryNoteCounts; - 调用
updateDataList()同步noteList到dataList; - 将
refreshNotesView方法挂到GlobalInfoModel,供外部刷新调用。
- 重置多选状态:
-
刷新逻辑(refreshData):
- 重新读取当前分类下笔记并更新
noteList; - 若不在多选模式,重置选中状态;
- 调用
forceRefreshUI()清空并重建dataList,通过notifyDataChange触发 UI 刷新; - 重新统计分类计数,保证分类弹窗中的数量与实际一致。
- 重新读取当前分类下笔记并更新
-
多选与置顶等交互:
- 通过
SelectedController记录选中 ID 集合与计数; - 长按进入多选模式:
handleLangPress; - "全选"、"取消全选"、批量删除/移动/置顶都基于
selectedIds实现。
- 通过
关键实现示例
MainPage:Tab 切换与跨页面刷新(节选自 MainPage.ets)
ts
@Entry
@ComponentV2
struct MainPage {
@Local curIndex: number = 0;
@Provider('appPathStack') appPathStack: NavPathStack = new NavPathStack();
@Local globalInfo: GlobalInfoModel = AppStorageV2
.connect<GlobalInfoModel>(GlobalInfoModel, () => new GlobalInfoModel())!;
build() {
Navigation(this.appPathStack) {
Column() {
Tabs({ barPosition: BarPosition.End, index: this.curIndex }) {
TabContent() {
PictureView();
}.tabBar(this.tabBarBuilder(0));
TabContent() {
NotesView();
}.tabBar(this.tabBarBuilder(1));
TabContent() {
ToDoView();
}.tabBar(this.tabBarBuilder(2));
TabContent() {
MineView();
}.tabBar(this.tabBarBuilder(3));
}
.onChange((index: number) => {
this.curIndex = index;
if (index === 1 && this.globalInfo.refreshNotesView) {
setTimeout(() => this.globalInfo.refreshNotesView &&
this.globalInfo.refreshNotesView(), 100);
}
if (index === 0 && this.globalInfo.refreshPictureView) {
setTimeout(() => this.globalInfo.refreshPictureView &&
this.globalInfo.refreshPictureView(), 100);
}
})
}
}
}
}
NotesView:初始化与刷新逻辑(节选自 NotesView.ets)
ts
@ComponentV2
export struct NotesView {
noteService: NoteService = NoteService.instance;
categoryService: CategoryService = CategoryService.instance;
// ... 省略其它依赖
@Local noteList: Note[] = [];
@Local dataList: LazyDataSource<Note> = new LazyDataSource();
async aboutToAppear(): Promise<void> {
this.selectedController.recoverInitState();
if (this.secretLock.gesture) {
const params: Record<string, Object> =
{ 'fromEntrance': true, 'appPathStack': this.appPathStack };
this.appPathStack.pushPathByName('DrawLock', params);
}
this.noteSearchController.searchKeyword = '';
this.currentCategory = await this.categoryService.getFirstCategory();
this.noteList = await this.noteService.getNoteList(
this.currentCategory.id,
this.sortController.sortBy,
this.noteSearchController.searchKeyword
);
this.noteService.initSelectedState();
const allNotes = await this.noteService.getNoteList('-1');
const categoryNotesMap = new Map<string, Note[]>();
for (const category of await this.categoryService.getCategoryList()) {
if (category.id !== '-1') {
const categoryNotes = await this.noteService.getNoteList(category.id);
categoryNotesMap.set(category.id, categoryNotes);
}
}
await this.categoryService.refreshAllCategoryNoteCounts(allNotes, categoryNotesMap);
this.categoryList = await this.categoryService.getCategoryList();
this.updateDataList();
this.updateNoteCount();
this.syncSelectedState();
this.globalInfo.refreshNotesView = this.refreshData.bind(this);
}
async refreshData(): Promise<void> {
this.noteList = await this.noteService.getNoteList(
this.currentCategory.id,
this.sortController.sortBy,
this.noteSearchController.searchKeyword
);
if (!this.selectedController.isCtrl) {
this.noteService.initSelectedState();
}
this.forceRefreshUI();
this.updateNoteCount();
this.syncSelectedState();
}
private forceRefreshUI(): void {
this.dataList.clear();
this.dataList.notifyDataChange(0);
setTimeout(() => {
this.noteList.forEach((item: Note) => this.dataList.pushData(item));
this.dataList.notifyDataChange(0);
}, 0);
}
}
4. 笔记编辑(EditNotes + 富文本编辑器)实现思路
4.1 富文本编辑器组件
-
富文本编辑器封装在
components/richeditor中,对外通过Index.ets提供:RichEditorController:编辑器控制器,负责光标、选区、历史记录(撤销/重做)和当前样式状态;RichEditorArea:真正的富文本输入区域组件;Note/LazyDataSource<Note>等数据模型;StyleSerializer:负责将样式(加粗、斜体、下划线、阴影、对齐方式等)序列化到字符串,并在读取时反序列化回枚举和结构体。
-
样式持久化:
- 编辑器内部维护
MutableStyledString,记录每段文本的样式; - 保存时将
MutableStyledString通过StyleSerializer转为 JSON 字符串,存入notes.styledContent; - 读取时反序列化回
MutableStyledString,然后调用RichEditorController.restoreStateFromStyledString恢复光标位置、当前样式按钮状态等。
- 编辑器内部维护
4.2 EditNotes 页面结构
EditNotes.ets 是笔记编辑的核心页面,关键成员包括:
-
核心依赖:
RichEditorController:单例控制器,跨页面持有编辑状态;SnapShotController:用于对当前笔记区域截图分享;NoteService:保存/更新笔记数据;ThemeController:感知深浅色模式;GlobalInfoModel:用于在保存笔记后通知NotesView刷新数据。
-
本地状态:
@Local currentNote: Note:当前正在编辑的笔记对象;@Local noteTitle: string:标题输入内容;@Local selectedBackgroundColor: string:当前笔记背景色;@Local lightModeColors[] / darkModeColors[]:深浅色模式下可选背景色列表;@Local isEditNote: boolean:标记是"新建"还是"编辑已有笔记"。
-
键盘与主题适配:
aboutToAppear中将KeyboardAvoidMode设置为RESIZE_WITH_CARET,避免软键盘遮挡输入区域;- 根据
ThemeController.currentColorMode设置默认背景色; - 启动定时器轮询主题变化,若系统从浅色切到深色,会自动切换一组更适合阅读的背景色。
-
工具栏与更多操作:
toolBar()中提供:撤销/重做/保存 等基础操作;moreFunctionMenu()中提供:- 分享:通过
SnapShotController.onceSnapshot()截图当前笔记区域; - 复制:通过
UnifiedData + SystemPasteboard将富文本转为纯文本复制到系统剪贴板。
- 分享:通过
-
保存流程(简化版):
- 用户点击工具栏中的"保存"图标;
- 调用
saveNote():- 将编辑器中的内容序列化为
content/styledContent/description; - 若为新笔记,生成
id与createTime并插入数据库; - 若为编辑已有笔记,更新数据库记录(包括
description,确保列表卡片实时更新摘要);
- 将编辑器中的内容序列化为
- 保存成功后:
- 关闭键盘与编辑状态:
controller.stopEditing(); - 将
RichEditorController.showMoreFunction设置为true,显示更多功能菜单; - 通知
NotesView刷新列表; - 结束编辑状态并返回。
- 关闭键盘与编辑状态:
关键实现示例(节选自
EditNotes.ets)
ts
@ComponentV2
struct EditNotes {
richEditorController: RichEditorController = RichEditorController.instance;
noteService: NoteService = NoteService.instance;
@Local currentNote: Note = new Note(new MutableStyledString(''));
@Local noteTitle: string = '';
@Local isEditNote: boolean = false;
@Local selectedBackgroundColor: string = '#fefefe';
async saveNote(): Promise<boolean> {
let styledString = this.richEditorController.controller.getStyledString();
let content = styledString.getString();
// 新建且内容和标题都为空时不保存
if (content === '' && this.noteTitle === '') {
return this.isEditNote ? false : true;
}
if (this.isEditNote) {
this.currentNote.updateContent(
styledString,
this.noteTitle,
undefined,
this.selectedBackgroundColor
);
await this.noteService.updateNote(this.currentNote);
} else {
this.currentNote.title = this.noteTitle;
this.currentNote.styledString = styledString;
this.currentNote.description = styledString.getString();
this.currentNote.backgroundColor = this.selectedBackgroundColor;
await this.noteService.addNote(this.currentNote);
this.isEditNote = true;
}
if (this.globalInfo && this.globalInfo.refreshNotesView) {
this.globalInfo.refreshNotesView();
}
return true;
}
build() {
NavDestination() {
Column() {
RichEditorArea({
noteTitle: this.currentNote.title,
noteContent: this.currentNote.styledString,
snapShotController: this.snapShotController,
titleChange: (title: string) => {
this.noteTitle = title;
}
})
}
.backgroundColor(this.selectedBackgroundColor);
}
.menus(this.toolBar());
}
}
5. 安全与隐私(SecretLock 等)
-
SecretLock组件负责应用级安全:- 手势密码解锁:在进入
NotesView时,如果检测到已设置手势密码,则通过appPathStack.pushPathByName('DrawLock', params)先进入手势解锁页面; - 解锁成功后才允许继续访问笔记内容。
- 手势密码解锁:在进入
-
生物识别解锁(指纹/人脸)也可以与
secretlock联动,在应用启动或从后台回到前台时进行校验。
6. 典型业务流程串联
6.1 应用启动 → 显示主页
- 系统启动
EntryAbility; onCreate中初始化主题、数据库;onWindowStageCreate中:- 注册折叠屏断点;
- 设置状态栏/导航栏属性;
windowStage.loadContent('pages/MainPage')加载MainPage;
MainPage构建 Tabs,默认显示第一个 Tab(图片集),用户可切换到"笔记集"。
6.2 进入笔记列表 NotesView
- 用户点击"笔记集"Tab,
curIndex = 1; NotesView.aboutToAppear执行:- 若开启手势锁,则跳转到手势解锁页面;
- 加载当前分类下的笔记列表;
- 同步分类计数;
- 初始化
dataList,触发 UI 渲染; - 将
refreshNotesView注册到全局,便于其它页面保存后刷新列表。
6.3 新建 / 编辑笔记
- 在
NotesView点击"新建笔记"或笔记卡片,导航到EditNotes; EditNotes.aboutToAppear:配置键盘避让、主题模式、背景色等;- 用户使用富文本工具栏编辑内容(加粗、斜体、下划线、阴影、对齐方式等);
- 点击"保存":
- 将样式序列化为
styledContent; - 写入/更新
notes表中的记录; - 通知
NotesView刷新列表; - 结束编辑状态并返回。
- 将样式序列化为
7. 二开与扩展建议
如果你希望在本项目基础上做二次开发,可以参考以下思路:
-
增加字段:
- 在
notes表中新增字段(例如标签、提醒时间等); - 在
DatabaseManager.migrateDatabase()中按现有模式检测并ALTER TABLE,保持向后兼容; - 在
Note模型与NoteService中补充对应字段的读写逻辑。
- 在
-
扩展富文本能力:
- 在
richeditor组件中增加新的样式(例如高亮、引用块等); - 更新
StyleSerializer的序列化/反序列化逻辑; - 在
EditNotes的工具栏中增加对应的按钮和交互。
- 在
-
增加云同步/多端能力:
- 在
NoteService层增加与云端的同步逻辑(基于华为云或自建服务); - 建议保持本地 RDB 为"真源",云端做备份与协同,避免弱网络导致编辑卡顿。
- 在
-
自定义安全策略:
- 扩展
secretlock支持更多解锁策略(如时间锁、地理位置锁等); - 在
EntryAbility.onForeground/NotesView.aboutToAppear中按需插入校验逻辑。
- 扩展
通过阅读本节并结合对应的源码文件(EntryAbility.ets、MainPage.ets、NotesView.ets、EditNotes.ets、DatabaseManager.ets、components/richeditor 等),你可以较为系统地掌握集光项目的整体设计思路,并在此基础上快速完成功能扩展或二次开发。
📄 开源协议
本项目采用 Apache 2.0 开源协议,欢迎贡献代码和提出建议。
集光 - 让记录更智能,让创意更闪耀 ✨