683 lines
19 KiB
Dart
683 lines
19 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
||
/// 属性面板组件
|
||
///
|
||
/// 弹出式属性编辑(元件值、封装、网络名)
|
||
/// 实时预览修改效果
|
||
/// 输入验证与错误提示
|
||
class PropertyPanelWidget extends ConsumerStatefulWidget {
|
||
/// 要编辑的属性数据
|
||
final PropertyData propertyData;
|
||
|
||
/// 属性变更回调
|
||
final Function(PropertyData)? onPropertyChanged;
|
||
|
||
/// 实时预览回调
|
||
final Function(PropertyData)? onPreview;
|
||
|
||
/// 面板位置(底部弹出/侧边弹出)
|
||
final PropertyPanelPosition position;
|
||
|
||
const PropertyPanelWidget({
|
||
super.key,
|
||
required this.propertyData,
|
||
this.onPropertyChanged,
|
||
this.onPreview,
|
||
this.position = PropertyPanelPosition.bottom,
|
||
});
|
||
|
||
@override
|
||
ConsumerState<PropertyPanelWidget> createState() => _PropertyPanelWidgetState();
|
||
}
|
||
|
||
class _PropertyPanelWidgetState extends ConsumerState<PropertyPanelWidget> {
|
||
late PropertyData _editedData;
|
||
final _formKey = GlobalKey<FormState>();
|
||
final _refDesignatorController = TextEditingController();
|
||
final _valueController = TextEditingController();
|
||
final _footprintController = TextEditingController();
|
||
final _netNameController = TextEditingController();
|
||
|
||
// 错误信息
|
||
String? _refDesignatorError;
|
||
String? _valueError;
|
||
String? _footprintError;
|
||
|
||
// 是否已修改
|
||
bool _hasChanges = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_editedData = widget.propertyData.copy();
|
||
_refDesignatorController.text = _editedData.refDesignator ?? '';
|
||
_valueController.text = _editedData.value ?? '';
|
||
_footprintController.text = _editedData.footprint ?? '';
|
||
_netNameController.text = _editedData.netName ?? '';
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_refDesignatorController.dispose();
|
||
_valueController.dispose();
|
||
_footprintController.dispose();
|
||
_netNameController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _validateRefDesignator(String value) {
|
||
if (value.isEmpty) {
|
||
setState(() => _refDesignatorError = '请输入位号');
|
||
return;
|
||
}
|
||
|
||
// 位号格式验证:字母 + 数字 (如 R1, C2, U3)
|
||
final regex = RegExp(r'^[A-Z]+\d+$');
|
||
if (!regex.hasMatch(value)) {
|
||
setState(() => _refDesignatorError = '位号格式错误 (如 R1, C2, U3)');
|
||
return;
|
||
}
|
||
|
||
setState(() => _refDesignatorError = null);
|
||
}
|
||
|
||
void _validateValue(String value) {
|
||
if (value.isEmpty) {
|
||
setState(() => _valueError = null); // 值可以为空
|
||
return;
|
||
}
|
||
|
||
// 根据元件类型验证值格式
|
||
switch (_editedData.componentType) {
|
||
case ComponentType.resistor:
|
||
// 电阻值:10k, 4.7M, 100R 等
|
||
final regex = RegExp(r'^\d+(\.\d+)?[kKmMgGpP]?$');
|
||
if (!regex.hasMatch(value)) {
|
||
setState(() => _valueError = '电阻值格式错误 (如 10k, 4.7M, 100R)');
|
||
return;
|
||
}
|
||
break;
|
||
|
||
case ComponentType.capacitor:
|
||
// 电容值:10u, 100n, 1p 等
|
||
final regex = RegExp(r'^\d+(\.\d+)?[uUnNpPmM]?$');
|
||
if (!regex.hasMatch(value)) {
|
||
setState(() => _valueError = '电容值格式错误 (如 10u, 100n, 1p)');
|
||
return;
|
||
}
|
||
break;
|
||
|
||
default:
|
||
_valueError = null;
|
||
break;
|
||
}
|
||
|
||
setState(() => _valueError = null);
|
||
}
|
||
|
||
void _validateFootprint(String value) {
|
||
if (value.isEmpty) {
|
||
setState(() => _footprintError = null); // 封装可以为空
|
||
return;
|
||
}
|
||
|
||
// 封装格式验证:简单验证是否包含字母和数字
|
||
if (!RegExp(r'[A-Za-z]').hasMatch(value) || !RegExp(r'\d').hasMatch(value)) {
|
||
setState(() => _footprintError = '封装格式错误 (如 0805, SOT23)');
|
||
return;
|
||
}
|
||
|
||
setState(() => _footprintError = null);
|
||
}
|
||
|
||
void _onTextChanged() {
|
||
setState(() {
|
||
_editedData.refDesignator = _refDesignatorController.text;
|
||
_editedData.value = _valueController.text;
|
||
_editedData.footprint = _footprintController.text;
|
||
_editedData.netName = _netNameController.text;
|
||
_hasChanges = true;
|
||
});
|
||
|
||
// 实时预览
|
||
widget.onPreview?.call(_editedData);
|
||
}
|
||
|
||
void _saveChanges() {
|
||
// 验证所有字段
|
||
_validateRefDesignator(_refDesignatorController.text);
|
||
_validateValue(_valueController.text);
|
||
_validateFootprint(_footprintController.text);
|
||
|
||
// 如果有错误,不保存
|
||
if (_refDesignatorError != null || _valueError != null || _footprintError != null) {
|
||
return;
|
||
}
|
||
|
||
widget.onPropertyChanged?.call(_editedData);
|
||
Navigator.of(context).pop(_editedData);
|
||
}
|
||
|
||
void _cancelChanges() {
|
||
Navigator.of(context).pop(null);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withOpacity(0.2),
|
||
blurRadius: 16,
|
||
offset: const Offset(0, -4),
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// 顶部标题栏
|
||
_buildHeader(),
|
||
|
||
const Divider(height: 1),
|
||
|
||
// 属性表单
|
||
Flexible(
|
||
child: SingleChildScrollView(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Form(
|
||
key: _formKey,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// 元件类型信息
|
||
_buildComponentInfo(),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
// 位号编辑
|
||
_buildRefDesignatorField(),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
// 值编辑
|
||
_buildValueField(),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
// 封装编辑
|
||
_buildFootprintField(),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
// 网络名编辑(只读,显示连接的网络)
|
||
_buildNetNameField(),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
// 旋转和镜像
|
||
_buildRotationMirror(),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
|
||
const Divider(height: 1),
|
||
|
||
// 底部操作按钮
|
||
_buildActionButtons(),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildHeader() {
|
||
return Container(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Row(
|
||
children: [
|
||
const Icon(Icons.edit_attributes, size: 24),
|
||
const SizedBox(width: 12),
|
||
const Text(
|
||
'属性编辑',
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
const Spacer(),
|
||
if (_hasChanges)
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: Colors.orange.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Text(
|
||
'未保存',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: Colors.orange[700],
|
||
),
|
||
),
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.close),
|
||
onPressed: _cancelChanges,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildComponentInfo() {
|
||
return Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: Theme.of(context).primaryColor.withOpacity(0.05),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Icon(
|
||
_getComponentIcon(widget.propertyData.componentType),
|
||
color: Theme.of(context).primaryColor,
|
||
size: 32,
|
||
),
|
||
const SizedBox(width: 12),
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
_getComponentTypeName(widget.propertyData.componentType),
|
||
style: const TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
Text(
|
||
widget.propertyData.symbolName ?? '默认符号',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: Colors.grey[600],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildRefDesignatorField() {
|
||
return TextFormField(
|
||
controller: _refDesignatorController,
|
||
decoration: InputDecoration(
|
||
labelText: '位号',
|
||
hintText: '如 R1, C2, U3',
|
||
prefixIcon: const Icon(Icons.tag),
|
||
errorText: _refDesignatorError,
|
||
border: const OutlineInputBorder(),
|
||
helperText: '元件的唯一标识符',
|
||
),
|
||
textCapitalization: TextCapitalization.characters,
|
||
textInputAction: TextInputAction.next,
|
||
onChanged: (value) {
|
||
_validateRefDesignator(value);
|
||
_onTextChanged();
|
||
},
|
||
validator: (value) {
|
||
if (value == null || value.isEmpty) {
|
||
return '请输入位号';
|
||
}
|
||
return _refDesignatorError;
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildValueField() {
|
||
return TextFormField(
|
||
controller: _valueController,
|
||
decoration: InputDecoration(
|
||
labelText: '值',
|
||
hintText: _getValueHint(),
|
||
prefixIcon: const Icon(Icons.memory),
|
||
errorText: _valueError,
|
||
border: const OutlineInputBorder(),
|
||
helperText: _getValueHelper(),
|
||
),
|
||
textInputAction: TextInputAction.next,
|
||
onChanged: (value) {
|
||
_validateValue(value);
|
||
_onTextChanged();
|
||
},
|
||
validator: (value) => _valueError,
|
||
);
|
||
}
|
||
|
||
Widget _buildFootprintField() {
|
||
return TextFormField(
|
||
controller: _footprintController,
|
||
decoration: InputDecoration(
|
||
labelText: '封装',
|
||
hintText: '如 0805, SOT23, SOIC8',
|
||
prefixIcon: const Icon(Icons.grid_view),
|
||
errorText: _footprintError,
|
||
border: const OutlineInputBorder(),
|
||
helperText: 'PCB 封装型号',
|
||
),
|
||
textCapitalization: TextCapitalization.characters,
|
||
textInputAction: TextInputAction.next,
|
||
onChanged: (value) {
|
||
_validateFootprint(value);
|
||
_onTextChanged();
|
||
},
|
||
validator: (value) => _footprintError,
|
||
);
|
||
}
|
||
|
||
Widget _buildNetNameField() {
|
||
return TextFormField(
|
||
controller: _netNameController,
|
||
decoration: InputDecoration(
|
||
labelText: '网络名',
|
||
hintText: '自动或手动命名',
|
||
prefixIcon: const Icon(Icons.link),
|
||
border: const OutlineInputBorder(),
|
||
helperText: '元件引脚连接的网络',
|
||
),
|
||
readOnly: true,
|
||
textInputAction: TextInputAction.done,
|
||
);
|
||
}
|
||
|
||
Widget _buildRotationMirror() {
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const Text(
|
||
'旋转与镜像',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
// 旋转按钮
|
||
Expanded(
|
||
child: _buildRotationButton(0),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: _buildRotationButton(90),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: _buildRotationButton(180),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: _buildRotationButton(270),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
// 镜像按钮
|
||
Expanded(
|
||
child: OutlinedButton.icon(
|
||
onPressed: () {
|
||
setState(() {
|
||
_editedData.mirrorX = !_editedData.mirrorX;
|
||
_onTextChanged();
|
||
});
|
||
},
|
||
icon: Icon(
|
||
_editedData.mirrorX ? Icons.flip : Icons.flip_outlined,
|
||
),
|
||
label: const Text('水平'),
|
||
style: OutlinedButton.styleFrom(
|
||
foregroundColor: _editedData.mirrorX
|
||
? Theme.of(context).primaryColor
|
||
: Colors.grey[700],
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: OutlinedButton.icon(
|
||
onPressed: () {
|
||
setState(() {
|
||
_editedData.mirrorY = !_editedData.mirrorY;
|
||
_onTextChanged();
|
||
});
|
||
},
|
||
icon: Icon(
|
||
Icons.flip,
|
||
color: _editedData.mirrorY
|
||
? Theme.of(context).primaryColor
|
||
: Colors.grey[700],
|
||
),
|
||
label: const Text('垂直'),
|
||
style: OutlinedButton.styleFrom(
|
||
foregroundColor: _editedData.mirrorY
|
||
? Theme.of(context).primaryColor
|
||
: Colors.grey[700],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildRotationButton(int degrees) {
|
||
final isSelected = _editedData.rotation == degrees;
|
||
|
||
return OutlinedButton(
|
||
onPressed: () {
|
||
setState(() {
|
||
_editedData.rotation = degrees;
|
||
_onTextChanged();
|
||
});
|
||
},
|
||
style: OutlinedButton.styleFrom(
|
||
backgroundColor: isSelected
|
||
? Theme.of(context).primaryColor.withOpacity(0.1)
|
||
: Colors.transparent,
|
||
foregroundColor: isSelected
|
||
? Theme.of(context).primaryColor
|
||
: Colors.grey[700],
|
||
),
|
||
child: Transform.rotate(
|
||
angle: degrees * 3.14159 / 180,
|
||
child: const Icon(Icons.rotate_right, size: 20),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildActionButtons() {
|
||
return Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: OutlinedButton(
|
||
onPressed: _cancelChanges,
|
||
style: OutlinedButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
),
|
||
child: const Text('取消'),
|
||
),
|
||
),
|
||
const SizedBox(width: 16),
|
||
Expanded(
|
||
child: ElevatedButton(
|
||
onPressed: _hasChanges ? _saveChanges : null,
|
||
style: ElevatedButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
),
|
||
child: const Text('保存'),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
String _getComponentTypeName(ComponentType type) {
|
||
switch (type) {
|
||
case ComponentType.resistor:
|
||
return '电阻';
|
||
case ComponentType.capacitor:
|
||
return '电容';
|
||
case ComponentType.inductor:
|
||
return '电感';
|
||
case ComponentType.diode:
|
||
return '二极管';
|
||
case ComponentType.transistor:
|
||
return '三极管';
|
||
case ComponentType.ic:
|
||
return '集成电路';
|
||
case ComponentType.connector:
|
||
return '连接器';
|
||
case ComponentType.other:
|
||
return '其他';
|
||
}
|
||
}
|
||
|
||
IconData _getComponentIcon(ComponentType type) {
|
||
switch (type) {
|
||
case ComponentType.resistor:
|
||
return Icons.rectangle_outlined;
|
||
case ComponentType.capacitor:
|
||
return Icons.battery_full;
|
||
case ComponentType.inductor:
|
||
return Icons.waves;
|
||
case ComponentType.diode:
|
||
return Icons.arrow_right_alt;
|
||
case ComponentType.transistor:
|
||
return Icons.memory;
|
||
case ComponentType.ic:
|
||
return Icons.dns;
|
||
case ComponentType.connector:
|
||
return Icons.usb;
|
||
case ComponentType.other:
|
||
return Icons.category;
|
||
}
|
||
}
|
||
|
||
String _getValueHint() {
|
||
switch (widget.propertyData.componentType) {
|
||
case ComponentType.resistor:
|
||
return '如 10k, 4.7M, 100R';
|
||
case ComponentType.capacitor:
|
||
return '如 10u, 100n, 1p';
|
||
case ComponentType.inductor:
|
||
return '如 10uH, 1mH';
|
||
default:
|
||
return '元件值';
|
||
}
|
||
}
|
||
|
||
String _getValueHelper() {
|
||
switch (widget.propertyData.componentType) {
|
||
case ComponentType.resistor:
|
||
return '电阻值 (欧姆)';
|
||
case ComponentType.capacitor:
|
||
return '电容值 (法拉)';
|
||
case ComponentType.inductor:
|
||
return '电感值 (亨利)';
|
||
default:
|
||
return '';
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 属性数据结构
|
||
class PropertyData {
|
||
String? refDesignator; // 位号
|
||
String? value; // 值
|
||
String? footprint; // 封装
|
||
String? netName; // 网络名
|
||
ComponentType componentType; // 元件类型
|
||
String? symbolName; // 符号名称
|
||
int rotation; // 旋转角度 (0, 90, 180, 270)
|
||
bool mirrorX; // 水平镜像
|
||
bool mirrorY; // 垂直镜像
|
||
|
||
PropertyData({
|
||
this.refDesignator,
|
||
this.value,
|
||
this.footprint,
|
||
this.netName,
|
||
required this.componentType,
|
||
this.symbolName,
|
||
this.rotation = 0,
|
||
this.mirrorX = false,
|
||
this.mirrorY = false,
|
||
});
|
||
|
||
PropertyData copy() {
|
||
return PropertyData(
|
||
refDesignator: refDesignator,
|
||
value: value,
|
||
footprint: footprint,
|
||
netName: netName,
|
||
componentType: componentType,
|
||
symbolName: symbolName,
|
||
rotation: rotation,
|
||
mirrorX: mirrorX,
|
||
mirrorY: mirrorY,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 元件类型枚举
|
||
enum ComponentType {
|
||
resistor, // 电阻
|
||
capacitor, // 电容
|
||
inductor, // 电感
|
||
diode, // 二极管
|
||
transistor, // 三极管
|
||
ic, // 集成电路
|
||
connector, // 连接器
|
||
other, // 其他
|
||
}
|
||
|
||
/// 面板位置枚举
|
||
enum PropertyPanelPosition {
|
||
bottom, // 底部弹出
|
||
side, // 侧边弹出
|
||
floating, // 浮动窗口
|
||
}
|
||
|
||
/// 显示属性面板的辅助函数
|
||
Future<PropertyData?> showPropertyPanel(
|
||
BuildContext context, {
|
||
required PropertyData propertyData,
|
||
Function(PropertyData)? onPropertyChanged,
|
||
Function(PropertyData)? onPreview,
|
||
}) {
|
||
return showModalBottomSheet<PropertyData>(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
backgroundColor: Colors.transparent,
|
||
builder: (context) => PropertyPanelWidget(
|
||
propertyData: propertyData,
|
||
onPropertyChanged: onPropertyChanged,
|
||
onPreview: onPreview,
|
||
),
|
||
);
|
||
}
|