fix(app): Phase 1.1 紧急修复 — SyncEngine 接入 + authorId + catch 异常处理
- feat(sync): SyncEngine 接入 EditorPage, 保存时 enqueue + 网络恢复自动 trySync - fix(editor): authorId 从 AuthBloc 获取, 替代硬编码 'local' - fix(bloc): class_bloc/calendar/profile/parent catch(_).全部改为 debugPrint - feat(editor): 编辑器工具栏拆分 (brush_panel/tag_panel/text_format_bar/dot_grid_painter) - feat(editor): EditorBloc 扩展 + EditorPage 增强 - feat(search): SearchBloc 扩展搜索功能 - feat(home): HomeBloc/HomePage 增强 - feat(auth): LoginPage 增强 - feat(templates): TemplateGalleryPage 重构 - fix(web): 管理端班级/日记页面修复 - fix(server): comment_service + theme_handler 修复 - docs: 添加全链路审计报告和验证截图
This commit is contained in:
@@ -14,10 +14,13 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../core/constants/design_tokens.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../data/models/journal_element.dart';
|
||||
import '../../../data/models/journal_entry.dart';
|
||||
import '../../../data/models/journal_entry.dart' show JournalEntry, Mood;
|
||||
import '../../../data/repositories/journal_repository.dart';
|
||||
import '../../../data/repositories/class_repository.dart';
|
||||
import '../../../data/services/sync_engine.dart';
|
||||
import '../../auth/bloc/auth_bloc.dart';
|
||||
import '../bloc/editor_bloc.dart';
|
||||
import '../widgets/handwriting_canvas.dart';
|
||||
import '../widgets/stroke_model.dart';
|
||||
@@ -27,6 +30,9 @@ import '../widgets/text_input_overlay.dart';
|
||||
import '../widgets/image_picker_handler.dart';
|
||||
import '../widgets/sticker_picker_sheet.dart';
|
||||
import '../widgets/share_bottom_sheet.dart';
|
||||
import '../widgets/tag_panel.dart';
|
||||
import '../widgets/brush_panel.dart';
|
||||
import '../widgets/dot_grid_painter.dart';
|
||||
|
||||
/// 手账编辑器页面
|
||||
class EditorPage extends StatelessWidget {
|
||||
@@ -39,6 +45,8 @@ class EditorPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
// 从 Provider 树获取 JournalRepository(IsarJournalRepository)
|
||||
final repo = context.read<JournalRepository>();
|
||||
// 从 Provider 树获取 SyncEngine(同步到后端)
|
||||
final syncEngine = context.read<SyncEngine>();
|
||||
|
||||
// 可变闭包变量:跟踪已保存的日记 ID
|
||||
// 新建日记首次保存后赋值,后续自动更新使用此 ID
|
||||
@@ -48,7 +56,18 @@ class EditorPage extends StatelessWidget {
|
||||
create: (_) => EditorBloc(
|
||||
onSave: (state) async {
|
||||
try {
|
||||
await _persistState(repo, state, (id) => savedJournalId = id, savedJournalId);
|
||||
// 从 AuthBloc 获取真实用户 ID
|
||||
String authorId = 'local';
|
||||
final authState = context.read<AuthBloc>().state;
|
||||
if (authState is Authenticated) {
|
||||
authorId = authState.user.id;
|
||||
}
|
||||
|
||||
await _persistState(
|
||||
repo, state, (id) => savedJournalId = id, savedJournalId,
|
||||
syncEngine: syncEngine,
|
||||
authorId: authorId,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('自动保存失败: $e');
|
||||
}
|
||||
@@ -66,24 +85,27 @@ class EditorPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// 持久化编辑器状态到 Isar
|
||||
/// 持久化编辑器状态到 Isar,并同步到后端
|
||||
///
|
||||
/// 策略:
|
||||
/// - 首次保存(savedJournalId == null)→ createJournal + addElement
|
||||
/// - 后续保存 → updateJournal + upsert 元素
|
||||
/// - 笔画序列化为 handwriting_ref 元素
|
||||
/// - 保存成功后入队 SyncEngine 等待网络同步
|
||||
Future<void> _persistState(
|
||||
JournalRepository repo,
|
||||
EditorState state,
|
||||
void Function(String) setId,
|
||||
String? savedJournalId,
|
||||
) async {
|
||||
String? savedJournalId, {
|
||||
required SyncEngine syncEngine,
|
||||
String authorId = 'local',
|
||||
}) async {
|
||||
final now = DateTime.now();
|
||||
|
||||
if (savedJournalId == null) {
|
||||
// --- 新建日记 ---
|
||||
final entry = JournalEntry.create(
|
||||
authorId: 'local', // TODO: 从 AuthBloc 获取真实用户 ID
|
||||
authorId: authorId,
|
||||
title: '${now.month}月${now.day}日的日记',
|
||||
date: now,
|
||||
);
|
||||
@@ -99,11 +121,31 @@ class EditorPage extends StatelessWidget {
|
||||
for (final element in state.elements) {
|
||||
await repo.addElement(element.copyWith(journalId: entry.id));
|
||||
}
|
||||
|
||||
// 入队 SyncEngine 等待同步到后端
|
||||
syncEngine.enqueue(PendingOperation(
|
||||
id: entry.id,
|
||||
type: SyncOperationType.create,
|
||||
endpoint: '/diary/journals',
|
||||
data: entry.toJson(),
|
||||
version: entry.version,
|
||||
createdAt: now,
|
||||
));
|
||||
} else {
|
||||
// --- 更新已有日记 ---
|
||||
final existing = await repo.getJournal(savedJournalId);
|
||||
if (existing != null) {
|
||||
await repo.updateJournal(existing);
|
||||
|
||||
// 入队 SyncEngine 等待同步到后端
|
||||
syncEngine.enqueue(PendingOperation(
|
||||
id: existing.id,
|
||||
type: SyncOperationType.update,
|
||||
endpoint: '/diary/journals/${existing.id}',
|
||||
data: existing.toJson(),
|
||||
version: existing.version,
|
||||
createdAt: now,
|
||||
));
|
||||
}
|
||||
|
||||
// 更新笔画
|
||||
@@ -154,21 +196,28 @@ class EditorPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// 显示分享面板并在用户选择后导航
|
||||
static void _showShareSheetAndNavigate(
|
||||
static Future<void> _showShareSheetAndNavigate(
|
||||
BuildContext context,
|
||||
JournalRepository repo,
|
||||
String? savedJournalId,
|
||||
) {
|
||||
) async {
|
||||
// 尝试获取用户的班级信息
|
||||
String? userClassId;
|
||||
String userClassName = '我的班级';
|
||||
|
||||
try {
|
||||
context.read<ClassRepository>();
|
||||
// Phase 1 简化:不等待异步调用,使用默认值
|
||||
userClassId = null; // TODO: 从 AuthBloc/ClassBloc 获取真实班级 ID
|
||||
} catch (_) {
|
||||
// ClassRepository 不可用(未注入)
|
||||
// 从 AuthBloc 获取用户关联的班级
|
||||
final authState = context.read<AuthBloc>().state;
|
||||
if (authState is Authenticated) {
|
||||
final classRepo = context.read<ClassRepository>();
|
||||
final classes = await classRepo.getMyClasses();
|
||||
if (classes.isNotEmpty) {
|
||||
userClassId = classes.first.id;
|
||||
userClassName = classes.first.name;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('获取班级信息失败: $e');
|
||||
}
|
||||
|
||||
showModalBottomSheet(
|
||||
@@ -226,94 +275,251 @@ class _EditorView extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: colorScheme.surface,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// 顶栏
|
||||
_buildTopBar(context),
|
||||
body: Column(
|
||||
children: [
|
||||
// 顶栏(自带状态栏安全区)
|
||||
BlocBuilder<EditorBloc, EditorState>(
|
||||
builder: (context, state) {
|
||||
return _buildTopBar(context, state);
|
||||
},
|
||||
),
|
||||
|
||||
// 编辑区域(三层 Stack)
|
||||
Expanded(
|
||||
child: BlocBuilder<EditorBloc, EditorState>(
|
||||
builder: (context, state) {
|
||||
return _EditorStack(state: state, journalId: journalId);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 底部工具栏
|
||||
BlocBuilder<EditorBloc, EditorState>(
|
||||
// 编辑区域(三层 Stack)
|
||||
Expanded(
|
||||
child: BlocBuilder<EditorBloc, EditorState>(
|
||||
builder: (context, state) {
|
||||
return EditorToolbar(
|
||||
state: state,
|
||||
onEvent: (event) => context.read<EditorBloc>().add(event),
|
||||
);
|
||||
return _EditorStack(state: state, journalId: journalId);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 底部工具栏(自带底部安全区)
|
||||
BlocBuilder<EditorBloc, EditorState>(
|
||||
builder: (context, state) {
|
||||
return EditorToolbar(
|
||||
state: state,
|
||||
onEvent: (event) => context.read<EditorBloc>().add(event),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 顶部操作栏 — 返回/日记标题/完成
|
||||
Widget _buildTopBar(BuildContext context) {
|
||||
/// 顶部操作栏 — 日期/撤销重做/标签/心情/完成
|
||||
Widget _buildTopBar(BuildContext context, EditorState state) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
height: 52,
|
||||
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.spacing8),
|
||||
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
border: Border(
|
||||
bottom: BorderSide(color: colorScheme.outline.withValues(alpha: 0.1)),
|
||||
bottom: BorderSide(
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
child: Column(
|
||||
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
|
||||
? '编辑日记'
|
||||
: templateId != null
|
||||
? '从模板新建'
|
||||
: '新建日记',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
// 主顶栏行 (44px)
|
||||
SizedBox(
|
||||
height: 44,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
// 返回按钮
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
onPressed: () => _handleBack(context),
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
iconSize: 22,
|
||||
),
|
||||
// 日期显示
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
_formatDate(state),
|
||||
style: TextStyle(
|
||||
fontFamily: 'Quicksand',
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 撤销
|
||||
IconButton(
|
||||
icon: const Icon(Icons.undo_rounded, size: 18),
|
||||
onPressed: () => context.read<EditorBloc>().add(Undo()),
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
),
|
||||
// 重做
|
||||
IconButton(
|
||||
icon: const Icon(Icons.redo_rounded, size: 18),
|
||||
onPressed: () => context.read<EditorBloc>().add(Redo()),
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
),
|
||||
// 自动保存状态
|
||||
_buildAutosaveIndicator(state),
|
||||
// 标签按钮
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sell_rounded, size: 18),
|
||||
onPressed: () => _showTagPanel(context, state),
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
),
|
||||
// 完成/保存按钮
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: FilledButton.tonal(
|
||||
onPressed: () => _handleSave(context, state),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
minimumSize: const Size(0, 32),
|
||||
),
|
||||
child: const Text('完成', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// 日期 + 心情条 (40px)
|
||||
_buildDateMoodStrip(context, state),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 完成按钮
|
||||
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)),
|
||||
/// 返回处理
|
||||
void _handleBack(BuildContext context) {
|
||||
if (context.canPop()) {
|
||||
context.pop();
|
||||
} else {
|
||||
context.go('/home');
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存处理
|
||||
void _handleSave(BuildContext context, EditorState state) {
|
||||
onSaveComplete();
|
||||
}
|
||||
|
||||
/// 格式化日期显示
|
||||
String _formatDate(EditorState state) {
|
||||
final now = DateTime.now();
|
||||
const weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||
return '${now.month}月${now.day}日 · ${weekdays[now.weekday - 1]}';
|
||||
}
|
||||
|
||||
/// 自动保存状态指示器
|
||||
Widget _buildAutosaveIndicator(EditorState state) {
|
||||
if (state.lastSavedAt == null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text(
|
||||
'未保存',
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.success,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Text('完成'),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'已保存',
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey[500]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 日期时间 + 心情选择条
|
||||
Widget _buildDateMoodStrip(BuildContext context, EditorState state) {
|
||||
final now = DateTime.now();
|
||||
final timeStr =
|
||||
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
|
||||
final moods = [
|
||||
(Mood.happy, '😊'),
|
||||
(Mood.calm, '😐'),
|
||||
(Mood.sad, '😢'),
|
||||
(Mood.angry, '😡'),
|
||||
(Mood.thinking, '🤔'),
|
||||
];
|
||||
return Container(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
timeStr,
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey[500]),
|
||||
),
|
||||
// 心情快捷按钮
|
||||
const Spacer(),
|
||||
...moods.map((m) {
|
||||
final isSelected = state.selectedMood == m.$1;
|
||||
return GestureDetector(
|
||||
onTap: () =>
|
||||
context.read<EditorBloc>().add(MoodChanged(m.$1)),
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: isSelected
|
||||
? Border.all(color: AppColors.accent, width: 1.5)
|
||||
: null,
|
||||
color: isSelected ? const Color(0xFFFFF3E6) : null,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(m.$2, style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示标签面板
|
||||
void _showTagPanel(BuildContext context, EditorState state) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (ctx) => BlocProvider.value(
|
||||
value: context.read<EditorBloc>(),
|
||||
child: BlocBuilder<EditorBloc, EditorState>(
|
||||
builder: (ctx, state) => TagPanel(
|
||||
selectedTags: state.tags,
|
||||
onTagAdded: (tag) {
|
||||
context.read<EditorBloc>().add(TagAdded(tag));
|
||||
},
|
||||
onTagRemoved: (tag) {
|
||||
context.read<EditorBloc>().add(TagRemoved(tag));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -337,16 +543,46 @@ class _EditorStack extends StatefulWidget {
|
||||
|
||||
class _EditorStackState extends State<_EditorStack> {
|
||||
EditorTool? _lastTool;
|
||||
late final TextEditingController _titleController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_titleController = TextEditingController(text: widget.state.title);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _EditorStack oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final currentTool = widget.state.activeTool;
|
||||
|
||||
// 贴纸工具刚被激活时弹出底部面板(防止重复弹窗)
|
||||
if (currentTool == EditorTool.sticker && _lastTool != EditorTool.sticker) {
|
||||
// 防止重复弹窗:只在工具切换时触发
|
||||
if (currentTool != _lastTool) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _showStickerPicker();
|
||||
if (!mounted) return;
|
||||
|
||||
switch (currentTool) {
|
||||
// 贴纸工具 → 弹出贴纸选择面板
|
||||
case EditorTool.sticker:
|
||||
_showStickerPicker();
|
||||
// 画笔工具 → 弹出画笔设置面板
|
||||
case EditorTool.brush:
|
||||
_showBrushPanel();
|
||||
// 模板工具 → 导航到模板页
|
||||
case EditorTool.template:
|
||||
context.go('/templates');
|
||||
// 更多工具 → 弹出分享/导出选项
|
||||
case EditorTool.more:
|
||||
_showMoreSheet();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
_lastTool = currentTool;
|
||||
@@ -376,24 +612,162 @@ class _EditorStackState extends State<_EditorStack> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示画笔设置底部面板
|
||||
void _showBrushPanel() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => BlocProvider.value(
|
||||
value: context.read<EditorBloc>(),
|
||||
child: BlocBuilder<EditorBloc, EditorState>(
|
||||
builder: (ctx, state) => BrushPanel(
|
||||
activeBrushType: state.brushType,
|
||||
activeColor: state.brushColor,
|
||||
activeWidth: state.brushWidth,
|
||||
activeOpacity: state.brushOpacity,
|
||||
onBrushTypeChanged: (type) => context.read<EditorBloc>().add(
|
||||
BrushChanged(
|
||||
type: type,
|
||||
color: state.brushColor,
|
||||
width: state.brushWidth,
|
||||
),
|
||||
),
|
||||
onColorChanged: (color) => context.read<EditorBloc>().add(
|
||||
BrushChanged(
|
||||
type: state.brushType,
|
||||
color: color,
|
||||
width: state.brushWidth,
|
||||
),
|
||||
),
|
||||
onWidthChanged: (width) => context.read<EditorBloc>().add(
|
||||
BrushChanged(
|
||||
type: state.brushType,
|
||||
color: state.brushColor,
|
||||
width: width,
|
||||
),
|
||||
),
|
||||
onOpacityChanged: (opacity) {
|
||||
// Phase 1 简化:opacity 仅在 marker 模式下生效
|
||||
// 暂无 opacity 事件,后续扩展
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示更多选项底部面板(分享/导出/清除)
|
||||
void _showMoreSheet() {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(22)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 拖拽指示条
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 8),
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 清除画布
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline_rounded),
|
||||
title: const Text('清除画布'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.read<EditorBloc>().add(ClearCanvas());
|
||||
},
|
||||
),
|
||||
// 分享
|
||||
ListTile(
|
||||
leading: const Icon(Icons.share_rounded),
|
||||
title: const Text('分享日记'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// 委托给外层的分享逻辑
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = widget.state;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Layer 1: 手写画布(底层)
|
||||
// Layer 0: 点阵背景(最底层)
|
||||
CustomPaint(
|
||||
painter: const DotGridPainter(),
|
||||
size: Size.infinite,
|
||||
),
|
||||
|
||||
// Layer 1: 手写画布 + 内嵌标题
|
||||
IgnorePointer(
|
||||
ignoring: !state.isDrawingMode,
|
||||
child: HandwritingCanvas(
|
||||
brushType: state.brushType,
|
||||
brushColor: state.brushColor,
|
||||
brushWidth: state.brushWidth,
|
||||
strokes: state.strokes,
|
||||
onStrokeCompleted: (stroke) {
|
||||
context.read<EditorBloc>().add(StrokeCompleted(stroke));
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
// 内嵌标题输入框
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
child: TextField(
|
||||
controller: _titleController,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Quicksand',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: '给日记起个标题吧...',
|
||||
hintStyle: TextStyle(
|
||||
fontFamily: 'Quicksand',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: (value) {
|
||||
context.read<EditorBloc>().add(TitleChanged(value));
|
||||
},
|
||||
),
|
||||
),
|
||||
// 画布区域
|
||||
Expanded(
|
||||
child: HandwritingCanvas(
|
||||
brushType: state.brushType,
|
||||
brushColor: state.brushColor,
|
||||
brushWidth: state.brushWidth,
|
||||
strokes: state.strokes,
|
||||
onStrokeCompleted: (stroke) {
|
||||
context.read<EditorBloc>().add(StrokeCompleted(stroke));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -426,7 +800,7 @@ class _EditorStackState extends State<_EditorStack> {
|
||||
),
|
||||
|
||||
// 图片选择覆盖层(图片工具激活时显示)
|
||||
if (state.activeTool == EditorTool.image)
|
||||
if (state.activeTool == EditorTool.photo)
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
Reference in New Issue
Block a user