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:
215
app/lib/features/editor/widgets/brush_panel.dart
Normal file
215
app/lib/features/editor/widgets/brush_panel.dart
Normal file
@@ -0,0 +1,215 @@
|
||||
// 画笔面板 -- 底部抽屉
|
||||
// 提供画笔类型/粗细/颜色/透明度设置
|
||||
// 遵循 StickerPickerSheet 底部面板模式
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../bloc/editor_bloc.dart';
|
||||
import 'stroke_model.dart';
|
||||
|
||||
/// 画笔面板 -- 底部抽屉
|
||||
class BrushPanel extends StatelessWidget {
|
||||
final BrushType activeBrushType;
|
||||
final String activeColor;
|
||||
final double activeWidth;
|
||||
final double activeOpacity;
|
||||
final void Function(BrushType type) onBrushTypeChanged;
|
||||
final void Function(String color) onColorChanged;
|
||||
final void Function(double width) onWidthChanged;
|
||||
final void Function(double opacity) onOpacityChanged;
|
||||
|
||||
const BrushPanel({
|
||||
super.key,
|
||||
required this.activeBrushType,
|
||||
required this.activeColor,
|
||||
required this.activeWidth,
|
||||
required this.activeOpacity,
|
||||
required this.onBrushTypeChanged,
|
||||
required this.onColorChanged,
|
||||
required this.onWidthChanged,
|
||||
required this.onOpacityChanged,
|
||||
});
|
||||
|
||||
static const _brushTypes = [
|
||||
(BrushType.pen, '钢笔', Icons.gesture_rounded),
|
||||
(BrushType.pencil, '铅笔', Icons.edit_rounded),
|
||||
(BrushType.marker, '马克笔', Icons.brush_rounded),
|
||||
(BrushType.eraser, '橡皮', Icons.auto_fix_high_rounded),
|
||||
];
|
||||
|
||||
static const _colors = [
|
||||
'#2D2420', '#E07A5F', '#81B29A', '#F2CC8F',
|
||||
'#D4A5A5', '#42A5F5', '#9C27B0', '#FFFFFF',
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 280,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(22)),
|
||||
),
|
||||
child: Column(
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 画笔类型行
|
||||
_buildBrushTypeRow(context),
|
||||
// 粗细滑块
|
||||
_buildSizeSlider(context),
|
||||
// 颜色行
|
||||
_buildColorRow(context),
|
||||
// 透明度滑块(仅马克笔)
|
||||
if (activeBrushType == BrushType.marker)
|
||||
_buildOpacitySlider(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBrushTypeRow(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: _brushTypes.map((bt) {
|
||||
final isActive = activeBrushType == bt.$1;
|
||||
return GestureDetector(
|
||||
onTap: () => onBrushTypeChanged(bt.$1),
|
||||
child: Container(
|
||||
width: 64,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: isActive
|
||||
? Border.all(color: AppColors.accent, width: 2)
|
||||
: Border.all(color: Colors.transparent),
|
||||
color: isActive
|
||||
? AppColors.accent.withValues(alpha: 0.1)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
bt.$3,
|
||||
size: 20,
|
||||
color: isActive ? AppColors.accent : Colors.grey[600],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
bt.$2,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: isActive ? AppColors.accent : Colors.grey[600],
|
||||
fontWeight:
|
||||
isActive ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSizeSlider(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Text('粗细',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: activeWidth,
|
||||
min: 1,
|
||||
max: 20,
|
||||
divisions: 19,
|
||||
activeColor: AppColors.accent,
|
||||
label: activeWidth.round().toString(),
|
||||
onChanged: onWidthChanged,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
activeWidth.round().toString(),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildColorRow(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: _colors.map((c) {
|
||||
final isActive = activeColor == c;
|
||||
final color = _parseHexColor(c);
|
||||
return GestureDetector(
|
||||
onTap: () => onColorChanged(c),
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
border: isActive
|
||||
? Border.all(color: AppColors.accent, width: 2)
|
||||
: (c == '#FFFFFF'
|
||||
? Border.all(color: Colors.grey[300]!)
|
||||
: null),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOpacitySlider(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Text('透明度',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: activeOpacity,
|
||||
min: 0.1,
|
||||
max: 1.0,
|
||||
activeColor: AppColors.accent,
|
||||
onChanged: onOpacityChanged,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${(activeOpacity * 100).round()}%',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _parseHexColor(String hex) {
|
||||
final code = hex.replaceFirst('#', '');
|
||||
return Color(int.parse('FF$code', radix: 16));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user