diff --git a/app/lib/features/editor/views/editor_page.dart b/app/lib/features/editor/views/editor_page.dart index def1fd5..c2f2091 100644 --- a/app/lib/features/editor/views/editor_page.dart +++ b/app/lib/features/editor/views/editor_page.dart @@ -23,6 +23,7 @@ import '../widgets/stroke_model.dart'; import '../widgets/draggable_element.dart'; import '../widgets/editor_toolbar.dart'; import '../widgets/text_input_overlay.dart'; +import '../widgets/image_picker_handler.dart'; /// 手账编辑器页面 class EditorPage extends StatelessWidget { @@ -257,19 +258,25 @@ class _EditorView extends StatelessWidget { /// Layer 1 (底层): HandwritingCanvas /// Layer 2 (中层): 可拖拽元素(贴纸/照片/文字) /// Layer 3 (顶层): 由 _EditorView 中的工具栏处理 -class _EditorStack extends StatelessWidget { +class _EditorStack extends StatefulWidget { final EditorState state; final String? journalId; const _EditorStack({required this.state, this.journalId}); + @override + State<_EditorStack> createState() => _EditorStackState(); +} + +class _EditorStackState extends State<_EditorStack> { @override Widget build(BuildContext context) { + final state = widget.state; + return Stack( fit: StackFit.expand, children: [ // Layer 1: 手写画布(底层) - // 始终渲染,通过 IgnorePointer 控制交互(避免模式切换时销毁重建) IgnorePointer( ignoring: !state.isDrawingMode, child: HandwritingCanvas( @@ -285,7 +292,7 @@ class _EditorStack extends StatelessWidget { // Layer 2: 可拖拽元素(中层) if (state.elements.isNotEmpty) - _buildElementLayer(context), + _buildElementLayer(context, state), // 文字输入覆盖层(文字工具激活时显示) if (state.activeTool == EditorTool.text) @@ -297,7 +304,7 @@ class _EditorStack extends StatelessWidget { ); context.read().add(ElementAdded( JournalElement.createText( - journalId: journalId ?? '', + journalId: widget.journalId ?? '', text: text, position: center, fontSize: fontSize, @@ -311,15 +318,60 @@ class _EditorStack extends StatelessWidget { }, ), + // 图片选择覆盖层(图片工具激活时显示) + if (state.activeTool == EditorTool.image) + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _ImageSourceButton( + icon: Icons.camera_alt_outlined, + label: '拍照', + onTap: () => _pickImage(fromCamera: true), + ), + const SizedBox(width: 24), + _ImageSourceButton( + icon: Icons.photo_library_outlined, + label: '从相册', + onTap: () => _pickImage(fromCamera: false), + ), + ], + ), + ), + // 空状态提示 - if (state.strokes.isEmpty && state.elements.isEmpty) + if (state.strokes.isEmpty && state.elements.isEmpty && state.activeTool == EditorTool.select) _buildEmptyHint(context), ], ); } + /// 图片选择逻辑 + Future _pickImage({required bool fromCamera}) async { + final filePath = await ImagePickerHandler.pickImage(fromCamera: fromCamera); + if (filePath == null || !mounted) return; + + final thumbnailPath = await ImagePickerHandler.generateThumbnail(filePath); + if (!mounted) return; + + final center = Offset( + MediaQuery.of(context).size.width / 2 - 80, + MediaQuery.of(context).size.height / 3, + ); + + context.read().add(ElementAdded( + JournalElement.createImage( + journalId: widget.journalId ?? '', + filePath: filePath, + position: center, + thumbnailPath: thumbnailPath, + ), + )); + context.read().add(ToolChanged(EditorTool.select)); + } + /// 元素层 — 所有日记元素叠加显示 - Widget _buildElementLayer(BuildContext context) { + Widget _buildElementLayer(BuildContext context, EditorState state) { // 按 zIndex 排序 final sorted = List.from(state.elements) ..sort((a, b) => a.zIndex.compareTo(b.zIndex)); @@ -371,3 +423,46 @@ class _EditorStack extends StatelessWidget { ); } } + +/// 图片来源按钮 — 拍照 / 从相册 +class _ImageSourceButton extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onTap; + + const _ImageSourceButton({ + required this.icon, + required this.label, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 32, color: Theme.of(context).colorScheme.primary), + const SizedBox(height: 8), + Text(label, style: Theme.of(context).textTheme.bodySmall), + ], + ), + ), + ); + } +} diff --git a/app/lib/features/editor/widgets/draggable_element.dart b/app/lib/features/editor/widgets/draggable_element.dart index 5c9bf10..b214466 100644 --- a/app/lib/features/editor/widgets/draggable_element.dart +++ b/app/lib/features/editor/widgets/draggable_element.dart @@ -7,6 +7,8 @@ // - 单击选中/取消选中 // - 选中时显示边框和删除按钮 +import 'dart:io'; + import 'package:flutter/material.dart'; import '../../../data/models/journal_element.dart'; @@ -176,21 +178,18 @@ class _DraggableElementState extends State { ); case ElementType.image: - return Container( - color: Colors.grey.shade200, - alignment: Alignment.center, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.image_rounded, size: 32, color: Colors.grey.shade400), - const SizedBox(height: 4), - Text( - '照片', - style: TextStyle(fontSize: 12, color: Colors.grey.shade500), - ), - ], - ), - ); + final filePath = element.content['filePath'] as String?; + if (filePath != null && filePath.isNotEmpty) { + return ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Image.file( + File(filePath), + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => _buildImagePlaceholder(), + ), + ); + } + return _buildImagePlaceholder(); case ElementType.tape: final tapeColor = _parseColor(element.content['tapeColor'] as String?); @@ -230,4 +229,24 @@ class _DraggableElementState extends State { if (value == null) return const Color(0xFF2D2420); return Color(0xFF000000 + value); } + + /// 图片加载失败时的占位符 + Widget _buildImagePlaceholder() { + return Container( + width: 160, + height: 120, + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(4), + ), + child: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.broken_image_outlined, color: Colors.grey), + SizedBox(height: 4), + Text('图片加载失败', style: TextStyle(color: Colors.grey, fontSize: 11)), + ], + ), + ); + } }