791 lines
22 KiB
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|