feat(app): 集成图片上传到编辑器 — 拍照/相册 + 压缩 + 拖拽定位
This commit is contained in:
@@ -23,6 +23,7 @@ import '../widgets/stroke_model.dart';
|
|||||||
import '../widgets/draggable_element.dart';
|
import '../widgets/draggable_element.dart';
|
||||||
import '../widgets/editor_toolbar.dart';
|
import '../widgets/editor_toolbar.dart';
|
||||||
import '../widgets/text_input_overlay.dart';
|
import '../widgets/text_input_overlay.dart';
|
||||||
|
import '../widgets/image_picker_handler.dart';
|
||||||
|
|
||||||
/// 手账编辑器页面
|
/// 手账编辑器页面
|
||||||
class EditorPage extends StatelessWidget {
|
class EditorPage extends StatelessWidget {
|
||||||
@@ -257,19 +258,25 @@ class _EditorView extends StatelessWidget {
|
|||||||
/// Layer 1 (底层): HandwritingCanvas
|
/// Layer 1 (底层): HandwritingCanvas
|
||||||
/// Layer 2 (中层): 可拖拽元素(贴纸/照片/文字)
|
/// Layer 2 (中层): 可拖拽元素(贴纸/照片/文字)
|
||||||
/// Layer 3 (顶层): 由 _EditorView 中的工具栏处理
|
/// Layer 3 (顶层): 由 _EditorView 中的工具栏处理
|
||||||
class _EditorStack extends StatelessWidget {
|
class _EditorStack extends StatefulWidget {
|
||||||
final EditorState state;
|
final EditorState state;
|
||||||
final String? journalId;
|
final String? journalId;
|
||||||
|
|
||||||
const _EditorStack({required this.state, this.journalId});
|
const _EditorStack({required this.state, this.journalId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_EditorStack> createState() => _EditorStackState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditorStackState extends State<_EditorStack> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final state = widget.state;
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
// Layer 1: 手写画布(底层)
|
// Layer 1: 手写画布(底层)
|
||||||
// 始终渲染,通过 IgnorePointer 控制交互(避免模式切换时销毁重建)
|
|
||||||
IgnorePointer(
|
IgnorePointer(
|
||||||
ignoring: !state.isDrawingMode,
|
ignoring: !state.isDrawingMode,
|
||||||
child: HandwritingCanvas(
|
child: HandwritingCanvas(
|
||||||
@@ -285,7 +292,7 @@ class _EditorStack extends StatelessWidget {
|
|||||||
|
|
||||||
// Layer 2: 可拖拽元素(中层)
|
// Layer 2: 可拖拽元素(中层)
|
||||||
if (state.elements.isNotEmpty)
|
if (state.elements.isNotEmpty)
|
||||||
_buildElementLayer(context),
|
_buildElementLayer(context, state),
|
||||||
|
|
||||||
// 文字输入覆盖层(文字工具激活时显示)
|
// 文字输入覆盖层(文字工具激活时显示)
|
||||||
if (state.activeTool == EditorTool.text)
|
if (state.activeTool == EditorTool.text)
|
||||||
@@ -297,7 +304,7 @@ class _EditorStack extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
context.read<EditorBloc>().add(ElementAdded(
|
context.read<EditorBloc>().add(ElementAdded(
|
||||||
JournalElement.createText(
|
JournalElement.createText(
|
||||||
journalId: journalId ?? '',
|
journalId: widget.journalId ?? '',
|
||||||
text: text,
|
text: text,
|
||||||
position: center,
|
position: center,
|
||||||
fontSize: fontSize,
|
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),
|
_buildEmptyHint(context),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 图片选择逻辑
|
||||||
|
Future<void> _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<EditorBloc>().add(ElementAdded(
|
||||||
|
JournalElement.createImage(
|
||||||
|
journalId: widget.journalId ?? '',
|
||||||
|
filePath: filePath,
|
||||||
|
position: center,
|
||||||
|
thumbnailPath: thumbnailPath,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
context.read<EditorBloc>().add(ToolChanged(EditorTool.select));
|
||||||
|
}
|
||||||
|
|
||||||
/// 元素层 — 所有日记元素叠加显示
|
/// 元素层 — 所有日记元素叠加显示
|
||||||
Widget _buildElementLayer(BuildContext context) {
|
Widget _buildElementLayer(BuildContext context, EditorState state) {
|
||||||
// 按 zIndex 排序
|
// 按 zIndex 排序
|
||||||
final sorted = List<JournalElement>.from(state.elements)
|
final sorted = List<JournalElement>.from(state.elements)
|
||||||
..sort((a, b) => a.zIndex.compareTo(b.zIndex));
|
..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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
// - 单击选中/取消选中
|
// - 单击选中/取消选中
|
||||||
// - 选中时显示边框和删除按钮
|
// - 选中时显示边框和删除按钮
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../../data/models/journal_element.dart';
|
import '../../../data/models/journal_element.dart';
|
||||||
@@ -176,21 +178,18 @@ class _DraggableElementState extends State<DraggableElement> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
case ElementType.image:
|
case ElementType.image:
|
||||||
return Container(
|
final filePath = element.content['filePath'] as String?;
|
||||||
color: Colors.grey.shade200,
|
if (filePath != null && filePath.isNotEmpty) {
|
||||||
alignment: Alignment.center,
|
return ClipRRect(
|
||||||
child: Column(
|
borderRadius: BorderRadius.circular(4),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Image.file(
|
||||||
children: [
|
File(filePath),
|
||||||
Icon(Icons.image_rounded, size: 32, color: Colors.grey.shade400),
|
fit: BoxFit.contain,
|
||||||
const SizedBox(height: 4),
|
errorBuilder: (_, __, ___) => _buildImagePlaceholder(),
|
||||||
Text(
|
),
|
||||||
'照片',
|
);
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
|
}
|
||||||
),
|
return _buildImagePlaceholder();
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
case ElementType.tape:
|
case ElementType.tape:
|
||||||
final tapeColor = _parseColor(element.content['tapeColor'] as String?);
|
final tapeColor = _parseColor(element.content['tapeColor'] as String?);
|
||||||
@@ -230,4 +229,24 @@ class _DraggableElementState extends State<DraggableElement> {
|
|||||||
if (value == null) return const Color(0xFF2D2420);
|
if (value == null) return const Color(0xFF2D2420);
|
||||||
return Color(0xFF000000 + value);
|
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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user