// 手账编辑器页面 — 三层 Stack 架构 // // Layer 1 (底层): HandwritingCanvas — 手写画布 // Layer 2 (中层): DraggableElements — 贴纸/照片/文字元素 // Layer 3 (顶层): EditorToolbar — 底部工具栏 + 顶栏操作 // // 交互逻辑: // - 画笔模式 → Layer 1 接收手势,Layer 2 透传 // - 选择模式 → Layer 2 接收手势,Layer 1 透传 // - 工具栏 → 始终在最顶层 import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; 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'; /// 手账编辑器页面 class EditorPage extends StatelessWidget { final String? journalId; const EditorPage({super.key, this.journalId}); @override Widget build(BuildContext context) { // 从 Provider 树获取 JournalRepository(IsarJournalRepository) final repo = context.read(); // 可变闭包变量:跟踪已保存的日记 ID // 新建日记首次保存后赋值,后续自动更新使用此 ID String? savedJournalId = journalId; return BlocProvider( create: (_) => EditorBloc( onSave: (state) async { try { await _persistState(repo, state, (id) => savedJournalId = id, savedJournalId); } catch (e) { debugPrint('自动保存失败: $e'); } }, ), child: _EditorView( journalId: journalId, onSaveComplete: () { if (context.canPop()) { context.pop(); } else { context.go('/home'); } }, ), ); } /// 持久化编辑器状态到 Isar /// /// 策略: /// - 首次保存(savedJournalId == null)→ createJournal + addElement /// - 后续保存 → updateJournal + upsert 元素 /// - 笔画序列化为 handwriting_ref 元素 Future _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 _saveStrokesAsElement( JournalRepository repo, String journalId, List 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, required this.onSaveComplete}); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( backgroundColor: colorScheme.surface, body: SafeArea( child: Column( children: [ // 顶栏 _buildTopBar(context), // 编辑区域(三层 Stack) Expanded( child: BlocBuilder( builder: (context, state) { return _EditorStack(state: state); }, ), ), // 底部工具栏 BlocBuilder( builder: (context, state) { return EditorToolbar( state: state, onEvent: (event) => context.read().add(event), ); }, ), ], ), ), ); } /// 顶部操作栏 — 返回/日记标题/完成 Widget _buildTopBar(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Container( height: 52, padding: const EdgeInsets.symmetric(horizontal: DesignTokens.spacing8), decoration: BoxDecoration( color: colorScheme.surface, border: Border( bottom: BorderSide(color: colorScheme.outline.withValues(alpha: 0.1)), ), ), child: Row( children: [ // 返回按钮 IconButton( onPressed: () { if (context.canPop()) { context.pop(); } else { context.go('/home'); } }, icon: const Icon(Icons.arrow_back_rounded), tooltip: '返回', ), const SizedBox(width: DesignTokens.spacing8), // 日记标题 Expanded( child: Text( journalId != null ? '编辑日记' : '新建日记', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), ), // 完成按钮 FilledButton.tonal( onPressed: onSaveComplete, style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: DesignTokens.spacing16), minimumSize: const Size(0, 36), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), child: const Text('完成'), ), ], ), ); } } // ============================================================ // 编辑器三层 Stack // ============================================================ /// 编辑器 Stack — 三层叠加结构 /// /// Layer 1 (底层): HandwritingCanvas /// Layer 2 (中层): 可拖拽元素(贴纸/照片/文字) /// Layer 3 (顶层): 由 _EditorView 中的工具栏处理 class _EditorStack extends StatelessWidget { final EditorState state; const _EditorStack({required this.state}); @override Widget build(BuildContext context) { return Stack( fit: StackFit.expand, children: [ // Layer 1: 手写画布(底层) // 始终渲染,通过 IgnorePointer 控制交互(避免模式切换时销毁重建) IgnorePointer( ignoring: !state.isDrawingMode, child: HandwritingCanvas( brushType: state.brushType, brushColor: state.brushColor, brushWidth: state.brushWidth, strokes: state.strokes, onStrokeCompleted: (stroke) { context.read().add(StrokeCompleted(stroke)); }, ), ), // Layer 2: 可拖拽元素(中层) if (state.elements.isNotEmpty) _buildElementLayer(context), // 空状态提示 if (state.strokes.isEmpty && state.elements.isEmpty) _buildEmptyHint(context), ], ); } /// 元素层 — 所有日记元素叠加显示 Widget _buildElementLayer(BuildContext context) { // 按 zIndex 排序 final sorted = List.from(state.elements) ..sort((a, b) => a.zIndex.compareTo(b.zIndex)); return Stack( children: sorted.map((element) { return DraggableElement( key: ValueKey(element.id), element: element, isSelected: state.selectedElementId == element.id, onTap: (id) { context.read().add(ElementSelected(id)); }, onMoved: (id, x, y) { context.read().add(ElementMoved( elementId: id, positionX: x, positionY: y, )); }, onDeleted: (id) { context.read().add(ElementRemoved(id)); }, ); }).toList(), ); } /// 空状态提示 Widget _buildEmptyHint(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.draw_rounded, size: 48, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.15), ), const SizedBox(height: DesignTokens.spacing12), Text( '在这里开始书写吧 ✏️', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3), ), ), ], ), ); } }