mobile-eda/lib/presentation/widgets/component_library_panel.dart

791 lines
22 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:async';
/// 元件库面板组件
///
/// 网格/列表双视图切换
/// 搜索与筛选(按类别、封装、厂商)
/// 拖拽元件到画布
class ComponentLibraryPanel extends ConsumerStatefulWidget {
/// 视图模式:网格或列表
final LibraryViewMode initialViewMode;
/// 元件选择回调
final Function(ComponentLibraryItem)? onComponentSelected;
/// 元件拖拽开始回调
final Function(ComponentLibraryItem)? onDragStarted;
/// 筛选条件变更回调
final Function(FilterOptions)? onFilterChanged;
const ComponentLibraryPanel({
super.key,
this.initialViewMode = LibraryViewMode.grid,
this.onComponentSelected,
this.onDragStarted,
this.onFilterChanged,
});
@override
ConsumerState<ComponentLibraryPanel> createState() => _ComponentLibraryPanelState();
}
class _ComponentLibraryPanelState extends ConsumerState<ComponentLibraryPanel>
with SingleTickerProviderStateMixin {
LibraryViewMode _viewMode = LibraryViewMode.grid;
String _searchQuery = '';
FilterOptions _filterOptions = FilterOptions();
// 筛选面板是否展开
bool _isFilterExpanded = false;
// 搜索防抖
Timer? _searchDebounce;
// 元件数据(模拟)
late List<ComponentLibraryItem> _allComponents;
late List<ComponentLibraryItem> _filteredComponents;
// 当前选中的类别
String? _selectedCategory;
// 当前选中的封装
String? _selectedFootprint;
@override
void initState() {
super.initState();
_viewMode = widget.initialViewMode;
_loadComponents();
}
@override
void dispose() {
_searchDebounce?.cancel();
super.dispose();
}
void _loadComponents() {
// 加载元件数据(实际应从数据源加载)
_allComponents = _getMockComponents();
_applyFilters();
}
void _applyFilters() {
setState(() {
_filteredComponents = _allComponents.where((component) {
// 搜索过滤
if (_searchQuery.isNotEmpty) {
final query = _searchQuery.toLowerCase();
final nameMatch = component.name.toLowerCase().contains(query);
final descMatch = component.description?.toLowerCase().contains(query) ?? false;
if (!nameMatch && !descMatch) return false;
}
// 类别过滤
if (_selectedCategory != null && component.category != _selectedCategory) {
return false;
}
// 封装过滤
if (_selectedFootprint != null && component.footprint != _selectedFootprint) {
return false;
}
return true;
}).toList();
});
widget.onFilterChanged?.call(_filterOptions);
}
void _onSearchChanged(String value) {
_searchDebounce?.cancel();
_searchDebounce = Timer(const Duration(milliseconds: 300), () {
setState(() {
_searchQuery = value;
});
_applyFilters();
});
}
void _toggleViewMode() {
setState(() {
_viewMode = _viewMode == LibraryViewMode.grid
? LibraryViewMode.list
: LibraryViewMode.grid;
});
}
void _toggleFilter() {
setState(() {
_isFilterExpanded = !_isFilterExpanded;
});
}
void _onCategorySelected(String? category) {
setState(() {
_selectedCategory = category;
});
_applyFilters();
}
void _onFootprintSelected(String? footprint) {
setState(() {
_selectedFootprint = footprint;
});
_applyFilters();
}
void _resetFilters() {
setState(() {
_selectedCategory = null;
_selectedFootprint = null;
_searchQuery = '';
});
_applyFilters();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Column(
children: [
// 顶部工具栏
_buildToolbar(),
// 搜索栏
_buildSearchBar(),
// 筛选器
if (_isFilterExpanded) _buildFilterPanel(),
const Divider(height: 1),
// 元件列表/网格
Expanded(
child: _filteredComponents.isEmpty
? _buildEmptyState()
: _buildComponentGrid(),
),
],
),
);
}
Widget _buildToolbar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row(
children: [
// 标题
const Text(
'元件库',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
// 筛选按钮
IconButton(
icon: Icon(
Icons.filter_list,
color: _isFilterExpanded ? Theme.of(context).primaryColor : null,
),
onPressed: _toggleFilter,
tooltip: '筛选',
),
// 视图切换按钮
IconButton(
icon: Icon(
_viewMode == LibraryViewMode.grid
? Icons.view_list
: Icons.grid_view,
),
onPressed: _toggleViewMode,
tooltip: _viewMode == LibraryViewMode.grid ? '列表视图' : '网格视图',
),
],
),
);
}
Widget _buildSearchBar() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: TextField(
decoration: InputDecoration(
hintText: '搜索元件...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
setState(() {
_searchQuery = '';
});
_applyFilters();
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
onChanged: _onSearchChanged,
),
);
}
Widget _buildFilterPanel() {
return Container(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'筛选条件',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
TextButton(
onPressed: _resetFilters,
child: const Text('重置'),
),
],
),
const SizedBox(height: 8),
// 类别筛选
Row(
children: [
const Text('类别:', style: TextStyle(fontSize: 12)),
const SizedBox(width: 8),
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _getCategories().map((category) {
final isSelected = _selectedCategory == category;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(category),
selected: isSelected,
onSelected: (selected) {
_onCategorySelected(selected ? category : null);
},
),
);
}).toList(),
),
),
),
],
),
const SizedBox(height: 8),
// 封装筛选
Row(
children: [
const Text('封装:', style: TextStyle(fontSize: 12)),
const SizedBox(width: 8),
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _getFootprints().map((footprint) {
final isSelected = _selectedFootprint == footprint;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(footprint),
selected: isSelected,
onSelected: (selected) {
_onFootprintSelected(selected ? footprint : null);
},
),
);
}).toList(),
),
),
),
],
),
],
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
_searchQuery.isNotEmpty ? '未找到匹配的元件' : '暂无元件',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
if (_searchQuery.isNotEmpty)
TextButton(
onPressed: _resetFilters,
child: const Text('清除筛选'),
),
],
),
);
}
Widget _buildComponentGrid() {
if (_viewMode == LibraryViewMode.grid) {
return GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1.2,
),
itemCount: _filteredComponents.length,
itemBuilder: (context, index) {
final component = _filteredComponents[index];
return _buildGridItem(component);
},
);
} else {
return ListView.builder(
itemCount: _filteredComponents.length,
itemBuilder: (context, index) {
final component = _filteredComponents[index];
return _buildListItem(component);
},
);
}
}
Widget _buildGridItem(ComponentLibraryItem component) {
return Draggable<ComponentLibraryItem>(
data: component,
feedback: Material(
elevation: 4,
borderRadius: BorderRadius.circular(8),
child: Container(
width: 150,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Theme.of(context).primaryColor, width: 2),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 40),
Text(component.name, style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
),
),
childWhenDragging: Opacity(
opacity: 0.5,
child: _buildGridItemContent(component),
),
child: _buildGridItemContent(component),
);
}
Widget _buildGridItemContent(ComponentLibraryItem component) {
return GestureDetector(
onTap: () => widget.onComponentSelected?.call(component),
child: Container(
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 元件图标
Icon(
_getComponentIcon(component.category),
size: 40,
color: Theme.of(context).primaryColor,
),
const SizedBox(height: 8),
// 元件名称
Text(
component.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
// 封装
Text(
component.footprint,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
const SizedBox(height: 4),
// 类别标签
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
component.category,
style: TextStyle(
fontSize: 10,
color: Theme.of(context).primaryColor,
),
),
),
],
),
),
);
}
Widget _buildListItem(ComponentLibraryItem component) {
return Draggable<ComponentLibraryItem>(
data: component,
feedback: Material(
elevation: 4,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Theme.of(context).primaryColor, width: 2),
),
child: Row(
children: [
Icon(_getComponentIcon(component.category)),
const SizedBox(width: 12),
Text(component.name, style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
),
),
childWhenDragging: Opacity(
opacity: 0.5,
child: _buildListItemContent(component),
),
child: _buildListItemContent(component),
);
}
Widget _buildListItemContent(ComponentLibraryItem component) {
return ListTile(
leading: Icon(
_getComponentIcon(component.category),
color: Theme.of(context).primaryColor,
),
title: Text(component.name),
subtitle: Text('${component.footprint}${component.category}'),
trailing: component.description != null
? Text(
component.description!,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: null,
onTap: () => widget.onComponentSelected?.call(component),
onLongPress: () => _showComponentDetails(component),
);
}
void _showComponentDetails(ComponentLibraryItem component) {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
_getComponentIcon(component.category),
size: 48,
color: Theme.of(context).primaryColor,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
component.name,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
component.footprint,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
),
],
),
const SizedBox(height: 16),
if (component.description != null) ...[
const Text(
'描述',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(component.description!),
const SizedBox(height: 16),
],
ElevatedButton.icon(
onPressed: () {
Navigator.pop(context);
widget.onComponentSelected?.call(component);
},
icon: const Icon(Icons.add),
label: const Text('添加到原理图'),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
),
),
],
),
),
);
}
IconData _getComponentIcon(String category) {
switch (category) {
case '电源':
return Icons.battery_full;
case '被动元件':
return Icons.rectangle_outlined;
case '半导体':
return Icons.memory;
case '连接器':
return Icons.usb;
case '光电器件':
return Icons.lightbulb;
case '集成电路':
return Icons.dns;
default:
return Icons.category;
}
}
List<String> _getCategories() {
return ['电源', '被动元件', '半导体', '连接器', '光电器件', '集成电路'];
}
List<String> _getFootprints() {
return ['0402', '0603', '0805', '1206', 'SOT23', 'SOT223', 'SOIC8', 'DIP8', 'QFN16'];
}
List<ComponentLibraryItem> _getMockComponents() {
return [
ComponentLibraryItem(
name: '电阻',
category: '被动元件',
footprint: '0805',
description: '10kΩ ±1%',
),
ComponentLibraryItem(
name: '电容',
category: '被动元件',
footprint: '0603',
description: '100nF ±10%',
),
ComponentLibraryItem(
name: 'LED',
category: '光电器件',
footprint: 'LED0603',
description: '红色发光二极管',
),
ComponentLibraryItem(
name: '三极管',
category: '半导体',
footprint: 'SOT23',
description: 'NPN 型',
),
ComponentLibraryItem(
name: 'MOSFET',
category: '半导体',
footprint: 'SOT23',
description: 'N 沟道增强型',
),
ComponentLibraryItem(
name: '运放',
category: '集成电路',
footprint: 'SOIC8',
description: '通用运算放大器',
),
ComponentLibraryItem(
name: '稳压器',
category: '电源',
footprint: 'SOT223',
description: '3.3V LDO',
),
ComponentLibraryItem(
name: '排针',
category: '连接器',
footprint: 'HDR1X2',
description: '2.54mm 间距',
),
ComponentLibraryItem(
name: 'USB 接口',
category: '连接器',
footprint: 'USB_MICRO_B',
description: 'Micro USB B 型',
),
ComponentLibraryItem(
name: '晶振',
category: '被动元件',
footprint: 'HC49S',
description: '16MHz',
),
];
}
}
/// 视图模式枚举
enum LibraryViewMode {
grid, // 网格视图
list, // 列表视图
}
/// 元件库项目
class ComponentLibraryItem {
final String name;
final String category;
final String footprint;
final String? description;
final String? manufacturer;
final String? symbolData;
ComponentLibraryItem({
required this.name,
required this.category,
required this.footprint,
this.description,
this.manufacturer,
this.symbolData,
});
}
/// 筛选选项
class FilterOptions {
final String? category;
final String? footprint;
final String? manufacturer;
final String? searchQuery;
FilterOptions({
this.category,
this.footprint,
this.manufacturer,
this.searchQuery,
});
FilterOptions copyWith({
String? category,
String? footprint,
String? manufacturer,
String? searchQuery,
}) {
return FilterOptions(
category: category ?? this.category,
footprint: footprint ?? this.footprint,
manufacturer: manufacturer ?? this.manufacturer,
searchQuery: searchQuery ?? this.searchQuery,
);
}
}
/// 显示元件库面板的辅助函数(作为侧边抽屉)
Future<T?> showComponentLibraryDrawer<T>(
BuildContext context, {
LibraryViewMode initialViewMode = LibraryViewMode.grid,
Function(ComponentLibraryItem)? onComponentSelected,
}) {
return showModalBottomSheet<T>(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.7,
minChildSize: 0.3,
maxChildSize: 0.95,
expand: false,
builder: (context, scrollController) => Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: Column(
children: [
// 拖动手柄
Container(
margin: const EdgeInsets.only(top: 8),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
Expanded(
child: ComponentLibraryPanel(
initialViewMode: initialViewMode,
onComponentSelected: onComponentSelected,
),
),
],
),
),
),
);
}