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:
@@ -1,22 +1,22 @@
|
||||
// 编辑器工具栏 — 底部工具面板
|
||||
// 编辑器工具栏 — 底部单行 6 按钮面板
|
||||
//
|
||||
// 三段式布局:
|
||||
// - 工具选择行(画笔/选择/文字/贴纸/照片)
|
||||
// - 工具选项行(颜色/大小 — 根据当前工具动态变化)
|
||||
// - 操作行(撤销/重做/清除)
|
||||
// 精简布局:
|
||||
// - 单行 6 个工具按钮(贴纸/模板/画笔/照片/文字/更多)
|
||||
// - 高度 72px + 底部安全区
|
||||
// - 每个按钮:Column(icon 20px + label 10px),最小 36x36
|
||||
//
|
||||
// 详细选项已移至独立面板:
|
||||
// - 画笔选项 → BrushPanel(底部抽屉)
|
||||
// - 撤销/重做 → 顶栏
|
||||
// - 清除 → 顶栏
|
||||
//
|
||||
// 设计规范:触摸目标 ≥ 44px,圆角 22px (pill)
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/constants/design_tokens.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../bloc/editor_bloc.dart';
|
||||
|
||||
/// 工具栏高度
|
||||
const double _toolbarHeight = 160;
|
||||
|
||||
/// 编辑器工具栏
|
||||
/// 编辑器工具栏 — 精简版
|
||||
class EditorToolbar extends StatelessWidget {
|
||||
final EditorState state;
|
||||
final ValueChanged<EditorEvent> onEvent;
|
||||
@@ -30,61 +30,35 @@ class EditorToolbar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
height: _toolbarHeight,
|
||||
height: 72 + MediaQuery.of(context).padding.bottom,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.08),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, -2),
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
|
||||
),
|
||||
],
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(22)),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 工具选择行
|
||||
_buildToolRow(context, colorScheme),
|
||||
const Divider(height: 1),
|
||||
|
||||
// 工具选项行(颜色/大小)
|
||||
_buildOptionsRow(context, colorScheme),
|
||||
const Divider(height: 1),
|
||||
|
||||
// 操作行(撤销/重做/清除)
|
||||
_buildActionRow(context, colorScheme),
|
||||
],
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_toolBtn(context, EditorTool.sticker, Icons.emoji_emotions_rounded, '贴纸'),
|
||||
_toolBtn(context, EditorTool.template, Icons.dashboard_customize_rounded, '模板'),
|
||||
_toolBtn(context, EditorTool.brush, Icons.gesture_rounded, '画笔'),
|
||||
_toolBtn(context, EditorTool.photo, Icons.add_photo_alternate_rounded, '照片'),
|
||||
_toolBtn(context, EditorTool.text, Icons.text_fields_rounded, '文字'),
|
||||
_toolBtn(context, EditorTool.more, Icons.more_horiz_rounded, '更多'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 工具选择行
|
||||
// ============================================================
|
||||
|
||||
Widget _buildToolRow(BuildContext context, ColorScheme colorScheme) {
|
||||
return SizedBox(
|
||||
height: 52,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_toolButton(context, EditorTool.pen, Icons.gesture_rounded, '钢笔'),
|
||||
_toolButton(context, EditorTool.pencil, Icons.edit_rounded, '铅笔'),
|
||||
_toolButton(context, EditorTool.marker, Icons.brush_rounded, '马克笔'),
|
||||
_toolButton(context, EditorTool.eraser, Icons.auto_fix_high_rounded, '橡皮'),
|
||||
_toolButton(context, EditorTool.select, Icons.near_me_rounded, '选择'),
|
||||
_toolButton(context, EditorTool.text, Icons.text_fields_rounded, '文字'),
|
||||
_toolButton(context, EditorTool.sticker, Icons.emoji_emotions_rounded, '贴纸'),
|
||||
_toolButton(context, EditorTool.image, Icons.add_photo_alternate_rounded, '照片'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _toolButton(
|
||||
/// 工具按钮 — icon + label 垂直排列
|
||||
Widget _toolBtn(
|
||||
BuildContext context,
|
||||
EditorTool tool,
|
||||
IconData icon,
|
||||
@@ -93,265 +67,37 @@ class EditorToolbar extends StatelessWidget {
|
||||
final isActive = state.activeTool == tool;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return SizedBox(
|
||||
width: 44,
|
||||
height: 44,
|
||||
child: IconButton(
|
||||
onPressed: () => onEvent(ToolChanged(tool)),
|
||||
icon: Icon(icon, size: 22),
|
||||
color: isActive ? colorScheme.primary : colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: isActive
|
||||
? colorScheme.primaryContainer.withValues(alpha: 0.3)
|
||||
: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
tooltip: label,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 工具选项行(颜色 + 大小)
|
||||
// ============================================================
|
||||
|
||||
static const _colors = [
|
||||
'#2D2420', // 主文字
|
||||
'#E07A5F', // 珊瑚
|
||||
'#81B29A', // 鼠尾草绿
|
||||
'#F2CC8F', // 暖金
|
||||
'#D4A5A5', // 玫瑰粉
|
||||
'#42A5F5', // 信息蓝
|
||||
'#9C27B0', // 紫色
|
||||
'#FFFFFF', // 白色
|
||||
];
|
||||
|
||||
static const _widths = [1.5, 3.0, 5.0, 8.0, 12.0];
|
||||
|
||||
Widget _buildOptionsRow(BuildContext context, ColorScheme colorScheme) {
|
||||
// 画笔模式:颜色 + 粗细
|
||||
if (state.isDrawingMode) {
|
||||
return _buildBrushOptions(context, colorScheme);
|
||||
}
|
||||
|
||||
// 文字工具提示
|
||||
if (state.activeTool == EditorTool.text) {
|
||||
return const SizedBox(
|
||||
height: 44,
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.text_fields, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text('点击画布输入文字', style: TextStyle(fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 贴纸工具提示
|
||||
if (state.activeTool == EditorTool.sticker) {
|
||||
return const SizedBox(
|
||||
height: 44,
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.emoji_emotions_outlined, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text('选择一个贴纸放到日记上', style: TextStyle(fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 图片工具提示
|
||||
if (state.activeTool == EditorTool.image) {
|
||||
return const SizedBox(
|
||||
height: 44,
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.add_photo_alternate_outlined, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text('选择照片添加到日记', style: TextStyle(fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 选择工具
|
||||
return const SizedBox(
|
||||
height: 44,
|
||||
child: Center(child: Text('选择元素或添加内容')),
|
||||
);
|
||||
}
|
||||
|
||||
/// 画笔模式选项 — 颜色 + 粗细
|
||||
Widget _buildBrushOptions(BuildContext context, ColorScheme colorScheme) {
|
||||
|
||||
return SizedBox(
|
||||
height: 44,
|
||||
child: Row(
|
||||
children: [
|
||||
// 颜色选择
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.spacing12),
|
||||
itemCount: _colors.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 6),
|
||||
itemBuilder: (context, index) {
|
||||
final color = _colors[index];
|
||||
final isActive = state.brushColor == color;
|
||||
return GestureDetector(
|
||||
onTap: () => onEvent(BrushChanged(
|
||||
type: state.brushType,
|
||||
color: color,
|
||||
width: state.brushWidth,
|
||||
)),
|
||||
child: Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: _parseHexColor(color),
|
||||
shape: BoxShape.circle,
|
||||
border: isActive
|
||||
? Border.all(color: colorScheme.primary, width: 2.5)
|
||||
: Border.all(color: Colors.grey.shade300, width: 1),
|
||||
),
|
||||
child: color == '#FFFFFF'
|
||||
? const Icon(Icons.check, size: 16, color: Colors.grey)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
return GestureDetector(
|
||||
onTap: () => onEvent(ToolChanged(tool)),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: isActive
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
|
||||
// 分隔线
|
||||
Container(width: 1, height: 24, color: colorScheme.outline.withValues(alpha: 0.2)),
|
||||
|
||||
// 笔刷大小
|
||||
SizedBox(
|
||||
width: 160,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: _widths.map((w) {
|
||||
final isActive = (state.brushWidth - w).abs() < 0.5;
|
||||
return GestureDetector(
|
||||
onTap: () => onEvent(BrushChanged(
|
||||
type: state.brushType,
|
||||
color: state.brushColor,
|
||||
width: w,
|
||||
)),
|
||||
child: Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: isActive
|
||||
? Border.all(color: colorScheme.primary, width: 2)
|
||||
: null,
|
||||
),
|
||||
child: Container(
|
||||
width: (w / 12 * 16 + 4).clamp(4, 20),
|
||||
height: (w / 12 * 16 + 4).clamp(4, 20),
|
||||
decoration: BoxDecoration(
|
||||
color: _parseHexColor(state.brushColor),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 操作行
|
||||
// ============================================================
|
||||
|
||||
Widget _buildActionRow(BuildContext context, ColorScheme colorScheme) {
|
||||
return SizedBox(
|
||||
height: 44,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// 撤销
|
||||
IconButton(
|
||||
onPressed: state.strokes.isNotEmpty
|
||||
? () => onEvent(Undo())
|
||||
: null,
|
||||
icon: const Icon(Icons.undo_rounded),
|
||||
tooltip: '撤销',
|
||||
),
|
||||
|
||||
// 重做
|
||||
IconButton(
|
||||
onPressed: state.redoStack.isNotEmpty
|
||||
? () => onEvent(Redo())
|
||||
: null,
|
||||
icon: const Icon(Icons.redo_rounded),
|
||||
tooltip: '重做',
|
||||
),
|
||||
|
||||
// 清除
|
||||
IconButton(
|
||||
onPressed: state.strokes.isNotEmpty || state.elements.isNotEmpty
|
||||
? () => onEvent(ClearCanvas())
|
||||
: null,
|
||||
icon: const Icon(Icons.delete_outline_rounded),
|
||||
tooltip: '清除',
|
||||
),
|
||||
|
||||
// 保存状态指示
|
||||
if (state.isDirty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.spacing8),
|
||||
child: Text(
|
||||
'未保存',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (state.lastSavedAt != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.spacing8),
|
||||
child: Text(
|
||||
'已保存',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.success,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: isActive
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 工具函数
|
||||
// ============================================================
|
||||
|
||||
Color _parseHexColor(String hex) {
|
||||
final hexStr = hex.replaceFirst('#', '');
|
||||
if (hexStr.length != 6) return const Color(0xFF2D2420);
|
||||
final value = int.tryParse(hexStr, radix: 16);
|
||||
if (value == null) return const Color(0xFF2D2420);
|
||||
return Color(0xFF000000 + value);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user