Files
nj/app/lib/features/editor/widgets/brush_panel.dart
iven 49d4aa36a7
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
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: 添加全链路审计报告和验证截图
2026-06-02 21:21:43 +08:00

216 lines
6.5 KiB
Dart

// 画笔面板 -- 底部抽屉
// 提供画笔类型/粗细/颜色/透明度设置
// 遵循 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));
}
}