☰
Flutter适配OpenHarmony:笔记详情页的存储与交互实践
2026/10/22 3:20:55 网站建设 项目流程

把看书管理记录 App 迁移到 OpenHarmony 平台,书架、阅读页很快就跑通了,真正让我意识到适配工作没那么简单的,是笔记详情这个页面。它看起来只是展示一条用户写的读书笔记,但在实际项目中,它要同时处理书籍信息联动展示、感想正文的长文本排版、编辑保存、收藏删除,还要和 OpenHarmony 的文件系统、输入法交互。Flutter 的跨端特性能把 UI 搭得很快,但平台差异通常集中在本地存储和输入体验这两个地方,恰好笔记详情全踩中了。

这篇文章以一个看书管理记录 App 的笔记详情页为切入点,记录我实现这个页面的完整过程:从需求拆解、数据模型设计,到 UI 构建、状态管理串联,再到真机调试和踩坑优化。如果你正在用 Flutter 适配 OpenHarmony,或者准备在自己的博客类、阅读类应用里做一个结构比较完整的笔记展示页面,这篇内容应该能直接给你参考。

1. 需求拆解与整体设计思路

1.1 笔记详情页要解决什么问题

看书管理记录 App 的核心功能不是阅读器本身,而是“记录”。用户记录一本书记到哪了、当时有什么感想、哪些段落值得回看。笔记列表页解决的是“找到一条笔记”,笔记详情页解决的是“完整回看这条笔记,并且能继续加工它”。

所以详情页不能只是一个文本展示页。它至少要承载几件事:

  • 告诉用户这条笔记是围绕哪本书写的,当前阅读进度是什么位置。
  • 完整展示笔记标题、正文、标签、创建和更新时间。
  • 提供编辑入口,让用户能修改或补充感想。
  • 提供收藏/取消收藏、删除等操作,方便后续做筛选和归档。

如果把笔记详情页做成一个简单的Text展示页,开发成本确实低,但用户回看时会丢失上下文。比如只看“观点很有意思”这句话,不知道是哪本书第几章的感想,这条记录的价值就大打折扣。因此这个页面在信息组织上,必须把“书的上下文”和“笔记本身”一起呈现。

从技术实现角度看,这就意味着页面数据来源不只是笔记表本身,还要关联书籍信息。关联方式有两种:一种是实时查询书籍表,另一种是在创建笔记时把书籍相关信息冗余保存到笔记表里。我后面选了第二种,原因下面细说。

1.2 功能清单与交互状态机

整理需求时,我把页面分成了五个功能模块:

模块功能说明状态
顶部导航返回上一页、显示笔记标题、打开更多操作正常/编辑
书籍卡片展示封面、书名、作者、章节、页码正常
正文区域展示笔记标题、内容、标签、时间正常/编辑
底部操作栏收藏、编辑、删除正常
反馈区加载中、加载失败、空态全局

交互状态机则相对简单,只有两个主状态:阅读态和编辑态。阅读态下,正文用只读富文本展示,底部操作栏可用;编辑态下,标题和正文化为输入框,底部操作栏只保留保存和取消。

我当时差点把“收藏”也做成独立状态,后来想明白,收藏本质上是单字段isFavorite的更新,不需要单独进编辑态,直接在阅读态操作即可。状态机一旦复杂,页面容易失控,能用字段表达的就不要用页面状态表达。

1.3 技术选型的取舍逻辑

Flutter 在 OpenHarmony 上运行,本质上是借助社区维护的对应平台适配分支,把 Dart 代码直接编译为 OpenHarmony 应用。选型时我关注三个点:

第一,状态管理。项目体量不大,我不想去引入过于庞大的状态管理框架。最终采用 Flutter 自带的ChangeNotifier加ListenableBuilder,页面级状态与依赖注入全部手工管理。这个方案在单页面内足够清晰,而且对 OpenHarmony 的适配最少,没有任何第三方依赖带来的兼容性风险。

第二,路由。笔记详情页从列表页进入,唯一入口,使用最朴素的Navigator.push就够了。像 go_router 这类声明式路由,在迁移到适配分支后版本更新较慢,没有必要为一个小功能引入额外成本。

第三,存储。这是整个技术选型里最敏感的部分。因为 OpenHarmony 的沙盒文件路径和原生插件适配都跟原先的移动平台有明显差异,如果贸然用一个依赖原生 SQLite 的存储插件,很可能在真机上验证时直接闪退。后面在第二章节会详细展开我的存储方案选择过程。

我的总体原则是:Flutter 负责 UI 和逻辑,平台相关的细节全部收口在一个仓储层后面,页面不直接感知 OpenHarmony 的 API 差异。这样就算存储方案后期要换,页面代码一行都不用动。

2. 数据模型与存储层设计

2.1 笔记字段设计思路

笔记详情页的 UI 决定了数据模型的长相。我设计的笔记模型字段如下:

字段类型说明冗余
idint自增主键否
bookIdstring书籍唯一标识否
bookNamestring书名快照是
bookAuthorstring作者快照是
chapterNamestring章节名快照是
pageNumberint阅读位置页码否
titlestring笔记标题否
contentstring笔记正文否
tagsList标签集合否
isFavoritebool是否收藏否
createdAtDateTime创建时间否
updatedAtDateTime最后修改时间否

我把书名、作者、章节名做成了冗余字段。这样做的直接原因是,笔记详情页在展示时不需要查询书籍表,一条 SQL 取出笔记,就能把卡片渲染完整。更重要的是,如果用户在管理书籍时把某本书从书架移除,笔记依然保有完整的上下文,不会变成一条没有归宿的文字。

这个思路是从实际使用反馈中来的。最初版本里我只有bookId,详情页靠bookId去查书籍表。测试时发现,一旦书籍数据被清理,详情页就会显示“未知书籍”,非常影响体验。改成冗余快照后,数据完整性问题直接从源头消失。

pageNumber是阅读位置。考虑到很多电子书没有固定分页,我也记录了chapterName作为位置参考。书和章节分开存储,后续做“根据笔记跳回原书对应位置”的功能时也有足够信息。

2.2 存储方案对比与最终选择

笔记数据以用户输入为主,不能丢。针对 OpenHarmony 平台,我对比了几个候选方案:

方案优点缺点是否采用
SQLite查询能力强,事务可靠原生插件在 OpenHarmony 上适配不全暂缓
本地 JSON 文件零原生依赖,适配稳定并发写入要自己做原子操作采用
KV 数据库读取快大字段正文序列化后性能一般不采用
内存缓存快数据易丢仅做二级缓存

说下为什么最终选择本地 JSON 文件。

SQLite 自然是第一直觉,Flutter 生态里的 SQLite 封装库在多数平台都表现稳定,但在 OpenHarmony 上要用到原生 sqlite3 动态库,适配分支对这类原生插件的支持还不完整,真机上经常出现“打开数据库失败”或直接加载不到 so 库。

我当时的替代思路是:既然数据量不大、查询模式简单,完全可以按“文件 + 内存索引”的形态实现,把所有笔记保存成一个 JSON 数组文件,启动时加载到内存,按 id 索引。写操作时更新内存,再整文件落盘。这里没有复杂的跨实体查询,性能完全够用。

KV 数据库的问题在于content这种大字段,如果一条笔记有几千字,KV 序列化反序列化的开销并不比 JSON 文件低,而且同样可能存在适配层的兼容性风险。

2.3 仓储层的实现与平台适配

围绕文件存储,我封装了一个仓储接口:

abstract class NoteRepository { Future<BookNote?> getNoteById(int id); Future<void> saveNote(BookNote note); Future<void> deleteNote(int id); Future<List<BookNote>> getAllNotes(); }

实现类FileNoteRepository的核心逻辑是:构造时传入 OpenHarmony 沙盒下的文件路径,启动时读取文件到内存,之后所有查询都走内存 Map,修改后同步写盘。

class FileNoteRepository implements NoteRepository { FileNoteRepository(this._filePath); final String _filePath; final Map<int, BookNote> _cache = {}; Future<void> _load() async { final file = File(_filePath); if (!await file.exists()) { await file.create(recursive: true); return; } final raw = await file.readAsString(); if (raw.trim().isEmpty) { return; } final list = (jsonDecode(raw) as List) .map((e) => BookNote.fromJson(e as Map<String, dynamic>)) .toList(); _cache ..clear() ..addEntries(list.map((e) => MapEntry(e.id!, e))); } @override Future<BookNote?> getNoteById(int id) async { if (_cache.isEmpty) { await _load(); } return _cache[id]; } @override Future<void> saveNote(BookNote note) async { _cache[note.id!] = note; await _flush(); } Future<void> _flush() async { final tmp = File('$_filePath.tmp'); await tmp.writeAsString(jsonEncode(_cache.values.map((e) => e.toJson()).toList())); await tmp.rename(_filePath); } }

注意_flush里的临时文件加改名操作,这是为了尽量保证原子写。如果直接在原文件上写入,中途崩溃极易损坏整份数据。用户输入型数据,宁可多写一次临时文件也不要在单点文件上冒险。

文件路径在 OpenHarmony 上有一个必须处理的差异点:不能把绝对路径永久保存到笔记数据里。沙盒路径在不同版本、不同设备上可能变化。正确做法是启动时通过平台能力拿到当前应用沙盒目录,再拼上文件名。

Future<String> _resolveStoragePath() async { final dir = await getApplicationDocumentsDirectory(); return '${dir.path}/notes/notes.json'; }

这样仓储层内部完全不需要关心平台差异,切到其他平台也能直接复用。

3. 详情页 UI 与交互实现

3.1 页面布局的三段式结构

详情页我整体采用竖直的CustomScrollView结构,从上到下分成三个语义区域:

  • 顶部:AppBar,左边返回,中间标题,右边“更多”菜单。
  • 内容区:书籍信息卡片 + 笔记头部 + 正文 + 标签。
  • 底部:固定在屏幕底部的操作栏。

之所以不直接用Column硬排,是因为笔记正文可能很长。CustomScrollView加SliverToBoxAdapter可以保证整页滚动自然,同时底部操作栏还能用bottomNavigationBar或者Scaffold.bottomSheet固定在视口底部。

页面主体代码如下:

Scaffold( appBar: _buildAppBar(), body: CustomScrollView( slivers: [ SliverToBoxAdapter(child: _BookInfoCard(note: note)), SliverToBoxAdapter(child: _NoteTitle(note: note)), SliverToBoxAdapter(child: _NoteContent(note: note, editing: isEditing)), SliverToBoxAdapter(child: _TagList(tags: note.tags)), SliverPadding(padding: EdgeInsets.only(bottom: 24)), ], ), bottomNavigationBar: _BottomActionBar( isFavorite: note.isFavorite, onFavorite: vm.toggleFavorite, onEdit: vm.enterEditMode, onDelete: vm.delete, ), )

每个SliverToBoxAdapter内部是一个独立 widget,这样单个区域重绘时不会影响整页。我最初把所有内容堆在一个SingleChildScrollView的Column里,后面发现编辑态切换时整个页面都在重建,视觉效果虽然不严重,但日志里能明显看到多余的重绘。拆开后清爽很多。

3.2 书籍信息卡片的实现

书籍卡片放在正文之前,用一句话概括就是:让用户先看到“这条笔记属于哪本书、写到哪个位置”,再进入具体感想。

卡片布局:左侧封面缩略图,右侧两行文字,第一行书名加作者,第二行章节和页码。

class _BookInfoCard extends StatelessWidget { const _BookInfoCard({required this.note}); final BookNote note; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Card( margin: const EdgeInsets.all(12), child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ _BookCover(bookName: note.bookName, bookId: note.bookId), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(note.bookName, style: theme.textTheme.titleMedium), const SizedBox(height: 4), Text('${note.bookAuthor} · 第${note.chapterIndex}章 ${note.chapterName}', style: theme.textTheme.bodySmall), const SizedBox(height: 4), Text('p.${note.pageNumber}', style: theme.textTheme.labelSmall), ], ), ), IconButton( icon: const Icon(Icons.menu_book_outlined), onPressed: () => _jumpToBook(context, note.bookId), ), ], ), ), ); } }

封面_BookCover我特意做了容错处理:优先尝试读本地封面缓存,读不到就显示一个圆角占位块,里面放书名首字。这里有个小坑:直接把本地缓存路径写进 widget 会导致测试时缓存不存在就一直报错。正确做法是把封面路径解析交给外层,传入的只是bookId,由一层服务去解析,widget 自己守住“拿不到就占位”的底线。

3.3 正文长文本展示与编辑态切换

正文区域是笔记详情的灵魂。阅读态时,我要求三个基本体验:

  • 正文支持选中复制。
  • 字数多时滚动不掉帧。
  • 标题和正文的视觉层级拉开。

用SelectableText是 Flutter 里最省事的方案,它天然支持长按选中。阅读态实现如下:

SelectableText( note.content, style: theme.textTheme.bodyLarge?.copyWith(height: 1.7), )

这里height: 1.7很重要。中文正文不像英文有词间距,行高太矮读起来非常累,实测 1.6 到 1.8 之间观感最好。

编辑态则把标题和正文替换为TextField:

TextField( key: const ValueKey('note-title-input'), controller: _titleController, maxLength: 50, style: theme.textTheme.titleLarge, decoration: const InputDecoration( labelText: '标题', border: OutlineInputBorder(), ), ) TextField( key: const ValueKey('note-content-input'), controller: _contentController, maxLines: null, keyboardType: TextInputType.multiline, style: theme.textTheme.bodyLarge?.copyWith(height: 1.7), )

切换编辑态时,要把原数据填入控制器:

void enterEditMode() { _titleController.text = note.title; _contentController.text = note.content; setState(() => _editing = true); }

保存时则反过来,读取控制器内容写回 model:

Future<void> save() async { final title = _titleController.text.trim(); final content = _contentController.text.trim(); if (content.isEmpty) { _showMessage('笔记内容不能为空'); return; } await widget.vm.saveNote(title: title, content: content); setState(() => _editing = false); }

这里的空内容校验要放在保存逻辑最前面。我曾经遇到过用户只写标题、正文留空的情况,如果放行,列表页会出现一条没有正文价值的笔记,后续筛选和导出都会产生奇怪数据。

3.4 底部操作栏与手势反馈

底部操作栏放在bottomNavigationBar位置,三个按钮:收藏、编辑、删除。

收藏按钮的状态直接用isFavorite驱动。这里我在图标层面做了小小的反馈正反馈:点击后不仅Icon变化,还伴随一个短震动反馈(通过平台通道实现,OpenHarmony 上对应触觉能力),短震比弹 SnackBar 更不干扰阅读。

删除按钮必须走确认弹窗。一个常用技巧是:弹窗里不直接执行删除,而是返回一个Future<bool>,在调用方确认后再删,避免回调里嵌套弹窗导致逻辑混乱。

Future<void> delete() async { final confirmed = await showDialog<bool>( context: context, builder: (context) => AlertDialog( title: const Text('删除这条笔记?'), content: const Text('删除后无法恢复'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text('删除'), ), ], ), ); if (confirmed == true) { await vm.deleteNote(); } }

确认弹窗是实现“危险操作”的标准姿势,这个习惯在任何平台、任何项目里都别丢。

4. 状态管理与业务逻辑串联

4.1 用 ChangeNotifier 管理页面状态

页面涉及加载、编辑、保存、收藏、删除多个异步动作,我不想把逻辑全部塞进State,因为State混杂了 UI 和业务逻辑,页面越来越大之后很难维护。我选择把BookNote的读取和变更都收敛到一个NoteDetailViewModel里。

class NoteDetailViewModel extends ChangeNotifier { NoteDetailViewModel({ required this.noteId, required this.repository, }); final int noteId; final NoteRepository repository; BookNote? note; bool isLoading = true; bool isSaving = false; bool isEditing = false; Future<void> load() async { note = await repository.getNoteById(noteId); isLoading = false; notifyListeners(); } Future<void> saveNote({required String title, required String content}) async { if (note == null) return; isSaving = true; notifyListeners(); note = note!.copyWith( title: title, content: content, updatedAt: DateTime.now(), ); await repository.saveNote(note!); isSaving = false; isEditing = false; notifyListeners(); } Future<void> toggleFavorite() async { if (note == null) return; note = note!.copyWith(isFavorite: !note!.isFavorite); await repository.saveNote(note!); notifyListeners(); } Future<void> deleteNote() async { await repository.deleteNote(noteId); } }

页面里通过addListener监听变化,驱动setState重建。这里不需要依赖任何第三方状态管理库,OpenHarmony 适配分支对ChangeNotifier的支持完全没问题。

4.2 加载到保存的完整数据流

加载阶段我画过一条清晰的链路:

页面initState创建 ViewModel,调用load()。load()从仓储层读取笔记,写入note字段,通知页面刷新。页面根据note == null分别渲染内容或空态。

保存阶段链路则是:

  1. 用户点击编辑,enterEditMode()把 note 数据填入控制器。
  2. 编辑时所有改动只停留在控制器,不污染原始 note。
  3. 点击保存后,saveNote()读取控制器文本,调用copyWith生成新对象,再写回仓储层。
  4. 保存完成后,isEditing置回 false,页面回到阅读态。

这里我比较强调“改动先落在控制器,再通过 copyWith 形成不可变对象”。直接修改note.title = ...在单页面没什么问题,但BookNote到处被引用,一不留神就会被修改旧数据。用copyWith的好处是仓储层之外不会出现对象被意外改掉的情况。

copyWith我这样实现:

BookNote copyWith({ int? id, String? bookName, String? bookAuthor, String? chapterName, int? pageNumber, String? title, String? content, List<String>? tags, bool? isFavorite, DateTime? updatedAt, }) { return BookNote( id: id ?? this.id, bookId: bookId, bookName: bookName ?? this.bookName, bookAuthor: bookAuthor ?? this.bookAuthor, chapterName: chapterName ?? this.chapterName, pageNumber: pageNumber ?? this.pageNumber, title: title ?? this.title, content: content ?? this.content, tags: tags ?? this.tags, isFavorite: isFavorite ?? this.isFavorite, createdAt: createdAt, updatedAt: updatedAt ?? this.updatedAt, ); }

注意bookId和createdAt在编辑中不允许变,所以我直接在copyWith里固定继承旧值,没有放进参数列表。少一个参数,就少一个改错的机会。

4.3 删除与收藏的异常处理

异步操作里最容易出的问题就是页面在请求还没返回时被用户切走,然后回调里继续操作Navigator或context,导致use_build_context_synchronously的警告。我在所有异步回调里统一用if (!mounted) return;拦截。

另一个容易忽略的异常点是收藏切换。用户可能在收藏操作未完成时再次点击,导致isFavorite状态闪回。解决方式是给收藏操作加一个短暂防抖:操作中禁止再次触发,等仓储层返回后再恢复可点击。实现不复杂:

bool _favoriteBusy = false; Future<void> toggleFavorite() async { if (_favoriteBusy) return; _favoriteBusy = true; try { await vm.toggleFavorite(); } finally { _favoriteBusy = false; } }

删除失败的情况也要兜底。文件写入可能因为磁盘空间不足、沙盒权限变化等原因失败,此时不能假装删除成功然后弹回去。我在deleteNote里返回一个布尔值,页面拿到 false 时停留原页并提示“删除失败,请重试”。

Future<bool> deleteNote() async { return repository.deleteNote(noteId); }

就这一行改动,避免了一个潜在的数据丢失场景。

5. 实测踩坑与性能优化

5.1 本地存储方案差点翻车的经过

第一版实现我确实先用了 SQLite 系列的 Flutter 封装。在模拟器/标准 Flutter 环境里一切正常,一到 OpenHarmony 真机就出现两种问题:第一种是找不到 sqlite3 原生库,直接抛异常;第二种是能打开数据库但插入时偶发失败,log 里显示 sqlite 模块初始化异常。

排查过程很典型:先确认是不是适配分支版本太旧,查了一下对应的 OpenHarmony 适配工具链更新记录,SQLite 插件并不是官方适配的范围,完全靠社区再加工,稳定性没法保证。

于是我把存储层切到 JSON 文件方案,核心改动只有仓储层一个文件。页面代码完全没动,这就是我在技术选型时坚持“仓储层隔离平台差异”带来的收益。真机上验证了两天后,文件方案的失败率降到了零。

一个优化细节:笔记文件如果每次写都全量序列化,数据量大时会有内存峰值。我在生产实现里给_flush加了节流,保存操作完成后延迟 100 毫秒再写盘,同一次操作里的多次保存会合并成一次写盘。用户手动编辑保存的频次本来就不高,延迟写盘不会带来数据丢失风险。

5.2 输入法与键盘弹出的适配

OpenHarmony 的输入法在 Flutter 适配分支下的表现和传统移动平台有差异,最典型的是键盘弹出后,输入框被遮挡或者候选栏位置错乱。

这里的关键配置是Scaffold的resizeToAvoidBottomInset必须保持 true(默认值)。我第一次做编辑态时,为了美观给页面套了一个SafeArea,结果键盘弹起后整个视口没有正确收缩,输入框被盖住了。解决办法是让 TextField 所在的编辑区域尽量位于页面中上部,避免键盘弹出后需要滚动很远才能看到光标。

另外,中文输入法候选词的异常在适配早期版本偶尔出现,升级到适配分支最新稳定版本后基本消失。如果后续读者遇到候选词不跟随光标,优先检查是不是适配分支版本过旧。

5.3 长文本溢出与滚动性能

笔记正文超过几千字以后,有两个具体问题:

第一个是文本溢出。中文标点连续排列时,SelectableText在某些行宽下会出现字符溢出,虽然不闪退,但观感很糟。解决方法是给正文区域设置明确的像素约束,不让它无限扩展,例如:

ConstrainedBox( constraints: const BoxConstraints(minHeight: 120), child: SelectableText( note.content, style: ..., ), )

第二个是滚动性能。正文很长时,如果整页还用Column放在SingleChildScrollView里,每次滚动都触发整棵子树的重绘,在低端 OpenHarmony 设备上能明显感觉到掉帧。切换到CustomScrollView加SliverToBoxAdapter后,单个区域的布局与绘制被隔离,滚动时只会重绘可见区域。这个优化对当前数据量来说可能不是刚需,但习惯早养成,后面做笔记列表、书摘列表时直接沿用。

6. 真机调试与打包部署要点

6.1 从构建到真机的完整流程

Flutter 适配 OpenHarmony 的构建流程跟标准 Flutter 有一点区别,但整体套路一致。我这边稳定可用的步骤如下:

  1. 确认本地 Flutter 工具链切换到适配 OpenHarmony 的分支版本。
  2. 项目根目录执行flutter create . --platforms=ohos,让工具生成ohos目录以及必要的配置。
  3. 执行flutter build hap --release --target-platform ohos-arm64,得到 HAP 安装包。
  4. 用配套 IDE 打开ohos/entry目录,配置好应用签名,点击构建,生成带签名的 HAP。
  5. 真机开启开发者模式,连接设备后,用命令行工具hdc install安装 HAP。

这里面最容易卡住的是签名。开发调试时可以使用自动签名,但发布版本必须要有正式签名配置。每个适配版本的生成工具位置略有不同,不过流程大致相似:在 IDE 的签名配置页里生成密钥、证书,然后关联到构建配置。

如果只想快速验证 UI,用 debug 模式加自动签名就够了,省掉一大半配置工作。

6.2 日志定位技巧

真机调试时,Flutter 侧的日志不会自动混入 OpenHarmony 的系统日志。需要先启动 Flutter 的日志转发,或者在系统日志里过滤对应 tag。实践中我依赖两条路径:

  • 用hdc连接设备后,通过日志工具过滤包含flutter关键词的日志,能同时看到 Dart 侧的print/debugPrint输出和引擎底层的异常。
  • 在 Flutter 代码里用debugPrint打印关键路径,但要注意发布模式下的debugPrint默认不输出,所以上线前的日志排查必须基于 release 包特殊处理,或者改用统一的日志模块。

我踩过的一个坑是:release 包在真机上白屏,系统日志里却没有任何异常。后来发现是沙盒目录没创建成功,文件写入失败,被仓储层 catch 后静默吞掉了异常。从那以后,我在仓储层所有catch块里都加上至少一条日志输出,宁可日志多,也不要无声失败。

这次笔记详情页做完,我最大的体会是:Flutter 迁移到 OpenHarmony,真正的成本不在 UI 层,而在平台能力差异的收敛和本地存储的兜底方案。页面布局、状态管理、交互反馈在 Flutter 生态里都是成熟套路,照着写就能跑;但存储方案一旦选错,真机上会以最难看的方式反咬你一口。所以我把这一次最重要的建议放在最后:所有平台相关能力,尽量收口在仓储层或者服务层后面,让页面始终只依赖抽象接口。这样后续适配分支升级、底层方案替换,都不会让业务代码大面积返工。

如果接下来要继续扩展,我会建议在笔记详情页的基础上,增加笔记导出为图片或纯文本的能力,顺便把长文本排版、富文本分段显示这些坑一起解决。这个页面已经把这些基础能力都铺平了,扩展起来会很顺手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询