12 KiB
12 KiB
Phase 2 UI 组件库实现文档
阶段: Week 3-4
交付日期: 2026-03-07
负责人: UI/UX 设计师
状态: ✅ 已完成
📦 交付内容
任务 1:工具栏组件 ✅
文件: lib/presentation/widgets/toolbar_widget.dart
功能特性:
- ✅ 顶部工具栏:撤销/重做/保存/设置
- ✅ 底部工具栏:元件库/走线模式/选择模式
- ✅ 支持可折叠/隐藏
- ✅ 模式切换状态管理
- ✅ 动画过渡效果
- ✅ 响应式布局
核心 API:
ToolbarWidget(
showTopToolbar: true,
showBottomToolbar: true,
collapsible: true,
onUndo: () => ...,
onRedo: () => ...,
onSave: () => ...,
onSettings: () => ...,
onComponentLibrary: () => ...,
onWireMode: () => ...,
onSelectMode: () => ...,
)
设计亮点:
- 采用 Material Design 卡片式设计
- 顶部工具栏支持滑动手势收起/展开
- 底部工具栏模式按钮有高亮状态
- 工具栏按钮带有 Tooltip 提示
任务 2:属性面板组件 ✅
文件: lib/presentation/widgets/property_panel_widget.dart
功能特性:
- ✅ 弹出式属性编辑(元件值、封装、网络名)
- ✅ 实时预览修改效果
- ✅ 输入验证与错误提示
- ✅ 位号格式验证(R1, C2, U3)
- ✅ 元件值格式验证(10k, 100n 等)
- ✅ 封装格式验证
- ✅ 旋转控制(0°, 90°, 180°, 270°)
- ✅ 镜像控制(水平/垂直)
- ✅ 未保存状态提示
核心 API:
// 直接使用组件
PropertyPanelWidget(
propertyData: PropertyData(
refDesignator: 'R1',
value: '10k',
footprint: '0805',
componentType: ComponentType.resistor,
),
onPropertyChanged: (data) => ...,
onPreview: (data) => ...,
)
// 或使用辅助函数
final result = await showPropertyPanel(
context,
propertyData: ...,
onPropertyChanged: (data) => ...,
);
数据模型:
class PropertyData {
String? refDesignator; // 位号
String? value; // 值
String? footprint; // 封装
String? netName; // 网络名
ComponentType componentType;
String? symbolName;
int rotation; // 0, 90, 180, 270
bool mirrorX;
bool mirrorY;
}
验证规则:
- 位号:必须为
字母 + 数字格式(如 R1, C2, U3) - 电阻值:支持
10k,4.7M,100R等格式 - 电容值:支持
10u,100n,1p等格式 - 封装:必须包含字母和数字(如 0805, SOT23)
设计亮点:
- 底部抽屉式弹出,符合移动端交互习惯
- 实时输入验证,错误即时提示
- 旋转按钮可视化展示角度
- 未保存状态有明显提示
任务 3:元件库面板组件 ✅
文件: lib/presentation/widgets/component_library_panel.dart
功能特性:
- ✅ 网格/列表双视图切换
- ✅ 搜索与筛选(按类别、封装、厂商)
- ✅ 拖拽元件到画布
- ✅ 类别筛选(FilterChip)
- ✅ 封装筛选
- ✅ 搜索防抖(300ms)
- ✅ 长按查看详情
- ✅ 可拖拽拖动手柄
核心 API:
// 直接使用组件
ComponentLibraryPanel(
initialViewMode: LibraryViewMode.grid,
onComponentSelected: (item) => ...,
onDragStarted: (item) => ...,
onFilterChanged: (filters) => ...,
)
// 或使用辅助函数(抽屉式)
await showComponentLibraryDrawer(
context,
initialViewMode: LibraryViewMode.grid,
onComponentSelected: (item) => ...,
);
数据模型:
class ComponentLibraryItem {
final String name;
final String category;
final String footprint;
final String? description;
final String? manufacturer;
final String? symbolData;
}
enum LibraryViewMode {
grid, // 网格视图
list, // 列表视图
}
筛选功能:
- 类别:电源、被动元件、半导体、连接器、光电器件、集成电路
- 封装:0402, 0603, 0805, 1206, SOT23, SOT223, SOIC8, DIP8, QFN16
- 搜索:支持名称和描述模糊匹配
拖拽支持:
- 使用 Flutter Draggable 组件
- 拖拽时显示半透明原位置
- 拖拽反馈组件带有高亮边框
- 支持长按查看详情后再拖拽
设计亮点:
- 网格视图:2 列布局,卡片式设计
- 列表视图:ListTile 布局,信息更详细
- 搜索栏带清除按钮
- 筛选面板可展开/收起
- 空状态友好提示
🎨 设计规范
遵循 Phase 1 触摸交互规范 v1.0
- 最小触控区域: 所有按钮 ≥ 44x44pt(iOS 人机指南)
- 手势支持:
- 单击:选择/触发
- 长按:上下文菜单/详情
- 拖拽:元件放置
- 双指缩放:画布导航(编辑器层面)
- 视觉反馈:
- 按钮按下有涟漪效果
- 选中状态有高亮
- 拖拽时有半透明提示
- 动画过渡: 200ms 标准动画时长
Material Design 3 风格
- 圆角:8px(小组件)、12px(卡片)、16px(面板)
- 阴影:轻度阴影(模糊 8px,偏移 0,2)
- 配色:使用 Theme.of(context).primaryColor
- 字体:系统默认字体,字号 10-20sp
🔌 集成指南
1. 在原理图编辑器中集成
import 'package:mobile_eda/presentation/widgets/widgets.dart';
class SchematicEditorScreen extends ConsumerStatefulWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// 画布
SchematicCanvas(),
// 工具栏
ToolbarWidget(
onUndo: _undo,
onRedo: _redo,
onSave: _save,
onSettings: _showSettings,
onComponentLibrary: _showLibrary,
onWireMode: _setWireMode,
onSelectMode: _setSelectMode,
),
],
),
// 或作为底部抽屉显示元件库
bottomSheet: ComponentLibraryPanel(
onComponentSelected: _addComponent,
),
);
}
}
2. 属性编辑集成
// 双击元件时显示属性面板
void _onComponentDoubleTap(SchematicComponent component) async {
final propertyData = PropertyData(
refDesignator: component.ref,
value: component.value,
footprint: component.footprint,
componentType: _mapToComponentType(component.type),
rotation: component.rotation,
mirrorX: component.mirrorX,
mirrorY: component.mirrorY,
);
final result = await showPropertyPanel(
context,
propertyData: propertyData,
onPreview: (data) {
// 实时预览:临时更新画布显示
_previewComponentChanges(data);
},
onPropertyChanged: (data) {
// 应用更改
_updateComponent(component.id, data);
},
);
if (result != null) {
// 保存成功
_commitChanges();
} else {
// 取消,恢复原状
_revertPreview();
}
}
3. 元件拖拽集成
// 在画布上接收拖拽
class SchematicCanvas extends StatefulWidget {
@override
Widget build(BuildContext context) {
return DragTarget<ComponentLibraryItem>(
onWillAccept: (data) => true,
onAccept: (component) {
// 在拖拽位置放置元件
_placeComponent(component, _lastDragPosition);
},
builder: (context, candidateData, rejectedData) {
return CustomPaint(painter: SchematicPainter());
},
);
}
}
4. Riverpod 状态管理集成
// 创建状态提供者
final toolbarModeProvider = StateProvider<ToolbarMode>((ref) {
return ToolbarMode.select;
});
final propertyPanelProvider = StateProvider<PropertyData?>((ref) {
return null;
});
final componentLibraryFilterProvider = StateProvider<FilterOptions>((ref) {
return FilterOptions();
});
// 在组件中使用
class _SchematicEditorScreenState extends ConsumerState {
@override
Widget build(BuildContext context) {
final mode = ref.watch(toolbarModeProvider);
return ToolbarWidget(
onSelectMode: () => ref.read(toolbarModeProvider.notifier).state = ToolbarMode.select,
onWireMode: () => ref.read(toolbarModeProvider.notifier).state = ToolbarMode.wire,
);
}
}
📁 文件结构
mobile-eda/lib/presentation/widgets/
├── widgets.dart # 导出文件(barrel)
├── toolbar_widget.dart # 工具栏组件
├── property_panel_widget.dart # 属性面板组件
└── component_library_panel.dart # 元件库面板组件
🧪 测试建议
单元测试
test('PropertyData copy creates independent copy', () {
final original = PropertyData(
refDesignator: 'R1',
value: '10k',
componentType: ComponentType.resistor,
);
final copy = original.copy();
copy.refDesignator = 'R2';
expect(original.refDesignator, equals('R1'));
expect(copy.refDesignator, equals('R2'));
});
test('ToolbarWidget callbacks are invoked', () {
var undoCalled = false;
final widget = ToolbarWidget(
onUndo: () => undoCalled = true,
);
// 使用 tester.tap() 模拟点击撤销按钮
// expect(undoCalled, isTrue);
});
组件测试
testWidgets('PropertyPanelWidget validates ref designator', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: PropertyPanelWidget(
propertyData: PropertyData(
componentType: ComponentType.resistor,
),
),
),
);
// 输入无效位号
await tester.enterText(find.byType(TextFormField).first, 'invalid');
await tester.pump();
// 验证错误提示
expect(find.text('位号格式错误 (如 R1, C2, U3)'), findsOneWidget);
});
🚀 后续优化建议
性能优化
- 元件库懒加载: 当元件数量 > 100 时,使用分页或虚拟列表
- 图片缓存: 元件符号图片使用 cached_network_image
- 防抖优化: 搜索已实现 300ms 防抖
功能增强
- 最近使用: 添加"最近使用元件"快速访问
- 收藏功能: 允许用户收藏常用元件
- 自定义元件: 支持用户创建和导入自定义元件
- 批量编辑: 支持多选元件批量修改属性
用户体验
- 快捷键支持: 桌面端可添加键盘快捷键(Ctrl+Z 撤销等)
- 语音输入: 支持语音输入元件值("十千欧")
- AR 预览: 使用 AR 查看元件实物图
📞 与 EDA 引擎专家协作
需要对接的 API
-
元件放置:
// 需要 EDA 引擎提供 void placeComponent(ComponentLibraryItem item, Offset position); -
属性更新:
// 需要 EDA 引擎提供 void updateComponentProperties(String componentId, PropertyData properties); -
模式切换:
// 需要 EDA 引擎提供 void setEditorMode(EditorMode mode); -
撤销/重做:
// 需要 EDA 引擎提供 void undo(); void redo(); bool get canUndo; bool get canRedo;
数据格式约定
// 元件唯一标识
typedef ComponentId = String;
// 坐标系统
// - 画布坐标:逻辑像素,原点在左上角
// - 网格吸附:默认 10.0 网格大小
// 旋转角度
// - 0: 默认方向
// - 90: 顺时针 90 度
// - 180: 顺时针 180 度
// - 270: 顺时针 270 度
✅ 验收标准
- 工具栏组件可正常显示/隐藏
- 工具栏按钮点击有响应
- 属性面板可弹出/关闭
- 属性输入有验证提示
- 元件库支持网格/列表切换
- 元件库支持搜索筛选
- 元件可拖拽(Draggable)
- 代码符合 Flutter 规范(flutter analyze 通过)
- 组件文档完整
📝 更新日志
2026-03-07 - 初始版本
- ✅ 完成工具栏组件
- ✅ 完成属性面板组件
- ✅ 完成元件库面板组件
- ✅ 编写集成文档
交付完成 🎉
所有组件已实现并经过基本测试,可集成到原理图编辑器中使用。