feat(app): Isar 本地数据库集成 — Collection + Repository + 编辑器持久化 + SyncEngine 队列
新增文件: - data/local/collections/ 3 个 Isar Collection 定义 + 生成 Schema - data/repositories/isar_journal_repository.dart 完整 CRUD + 乐观锁 修改文件: - app.dart: IsarJournalRepository 注册为主 JournalRepository + SyncEngine 注入 - editor_page.dart: onSave 接入 JournalRepository,笔画/元素自动保存到 Isar - sync_engine.dart: 新增 persistPendingQueue/restorePendingQueue Isar 持久化 - isar_database.dart: 注册 3 个 Collection Schema - main.dart: 启动时初始化 Isar 架构: 离线优先 — Isar 为本地主仓库,Remote 供 SyncEngine 推送
This commit is contained in:
@@ -15,8 +15,11 @@ import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../core/constants/design_tokens.dart';
|
||||
import '../../../data/models/journal_element.dart';
|
||||
import '../../../data/models/journal_entry.dart';
|
||||
import '../../../data/repositories/journal_repository.dart';
|
||||
import '../bloc/editor_bloc.dart';
|
||||
import '../widgets/handwriting_canvas.dart';
|
||||
import '../widgets/stroke_model.dart';
|
||||
import '../widgets/draggable_element.dart';
|
||||
import '../widgets/editor_toolbar.dart';
|
||||
|
||||
@@ -28,22 +31,123 @@ class EditorPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 从 Provider 树获取 JournalRepository(IsarJournalRepository)
|
||||
final repo = context.read<JournalRepository>();
|
||||
|
||||
// 可变闭包变量:跟踪已保存的日记 ID
|
||||
// 新建日记首次保存后赋值,后续自动更新使用此 ID
|
||||
String? savedJournalId = journalId;
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) => EditorBloc(
|
||||
onSave: (state) {
|
||||
// TODO: 通过 JournalRepository 保存到 Isar
|
||||
debugPrint('自动保存: ${state.strokes.length} 笔画, ${state.elements.length} 元素');
|
||||
onSave: (state) async {
|
||||
try {
|
||||
await _persistState(repo, state, (id) => savedJournalId = id, savedJournalId);
|
||||
} catch (e) {
|
||||
debugPrint('自动保存失败: $e');
|
||||
}
|
||||
},
|
||||
),
|
||||
child: _EditorView(journalId: journalId),
|
||||
child: _EditorView(
|
||||
journalId: journalId,
|
||||
onSaveComplete: () => context.pop(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 持久化编辑器状态到 Isar
|
||||
///
|
||||
/// 策略:
|
||||
/// - 首次保存(savedJournalId == null)→ createJournal + addElement
|
||||
/// - 后续保存 → updateJournal + upsert 元素
|
||||
/// - 笔画序列化为 handwriting_ref 元素
|
||||
Future<void> _persistState(
|
||||
JournalRepository repo,
|
||||
EditorState state,
|
||||
void Function(String) setId,
|
||||
String? savedJournalId,
|
||||
) async {
|
||||
final now = DateTime.now();
|
||||
|
||||
if (savedJournalId == null) {
|
||||
// --- 新建日记 ---
|
||||
final entry = JournalEntry.create(
|
||||
authorId: 'local', // TODO: 从 AuthBloc 获取真实用户 ID
|
||||
title: '${now.month}月${now.day}日的日记',
|
||||
date: now,
|
||||
);
|
||||
await repo.createJournal(entry);
|
||||
setId(entry.id);
|
||||
|
||||
// 保存笔画
|
||||
if (state.strokes.isNotEmpty) {
|
||||
await _saveStrokesAsElement(repo, entry.id, state.strokes);
|
||||
}
|
||||
|
||||
// 保存其他元素
|
||||
for (final element in state.elements) {
|
||||
await repo.addElement(element.copyWith(journalId: entry.id));
|
||||
}
|
||||
} else {
|
||||
// --- 更新已有日记 ---
|
||||
final existing = await repo.getJournal(savedJournalId);
|
||||
if (existing != null) {
|
||||
await repo.updateJournal(existing);
|
||||
}
|
||||
|
||||
// 更新笔画
|
||||
if (state.strokes.isNotEmpty) {
|
||||
await _saveStrokesAsElement(repo, savedJournalId, state.strokes);
|
||||
}
|
||||
|
||||
// upsert 元素(先尝试更新,失败则新建)
|
||||
for (final element in state.elements) {
|
||||
try {
|
||||
await repo.updateElement(element);
|
||||
} catch (_) {
|
||||
await repo.addElement(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 将笔画列表序列化为 handwriting_ref 元素并保存
|
||||
///
|
||||
/// 每次保存都替换整个笔画集(Phase 1 简化策略)。
|
||||
Future<void> _saveStrokesAsElement(
|
||||
JournalRepository repo,
|
||||
String journalId,
|
||||
List<Stroke> strokes,
|
||||
) async {
|
||||
final now = DateTime.now();
|
||||
final strokesJson = strokes.map((s) => s.toJson()).toList();
|
||||
|
||||
final element = JournalElement(
|
||||
id: '${journalId}_strokes', // 固定 ID,保证每次覆盖
|
||||
journalId: journalId,
|
||||
elementType: ElementType.handwritingRef,
|
||||
content: {
|
||||
'strokes': strokesJson,
|
||||
'strokeCount': strokes.length,
|
||||
},
|
||||
version: 1,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
);
|
||||
|
||||
try {
|
||||
await repo.updateElement(element);
|
||||
} catch (_) {
|
||||
await repo.addElement(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _EditorView extends StatelessWidget {
|
||||
final String? journalId;
|
||||
final VoidCallback onSaveComplete;
|
||||
|
||||
const _EditorView({this.journalId});
|
||||
const _EditorView({this.journalId, required this.onSaveComplete});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -117,10 +221,7 @@ class _EditorView extends StatelessWidget {
|
||||
|
||||
// 完成按钮
|
||||
FilledButton.tonal(
|
||||
onPressed: () {
|
||||
// TODO: 保存并返回
|
||||
context.pop();
|
||||
},
|
||||
onPressed: onSaveComplete,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.spacing16),
|
||||
minimumSize: const Size(0, 36),
|
||||
|
||||
Reference in New Issue
Block a user