一次大的提交
This commit is contained in:
@@ -0,0 +1,581 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../site/presentation/cubit/site_cubit.dart';
|
||||
import '../../domain/entities/device_run_param_entity.dart';
|
||||
import '../../domain/usecases/device_run_param_usecases.dart';
|
||||
import '../../../../../core/services/device_permission_service.dart';
|
||||
|
||||
class RobotParamSettingsPage extends StatefulWidget {
|
||||
final Map<String, dynamic> robot;
|
||||
|
||||
const RobotParamSettingsPage({super.key, required this.robot});
|
||||
|
||||
@override
|
||||
State<RobotParamSettingsPage> createState() => _RobotParamSettingsPageState();
|
||||
}
|
||||
|
||||
class _RobotParamSettingsPageState extends State<RobotParamSettingsPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
bool _isLoading = true;
|
||||
bool _isSaving = false;
|
||||
String? _errorMessage;
|
||||
DeviceRunParamEntity? _param;
|
||||
|
||||
// 表单控制器(仅可编辑的5个字段)
|
||||
late TextEditingController _runSpeedController;
|
||||
late TextEditingController _leftForwardGainController;
|
||||
late TextEditingController _leftBackwardGainController;
|
||||
late TextEditingController _rightForwardGainController;
|
||||
late TextEditingController _rightBackwardGainController;
|
||||
|
||||
String get _deviceId => widget.robot['name'] as String? ?? '';
|
||||
|
||||
// ============ 字段定义 ============
|
||||
static const _editableKeys = {
|
||||
'runSpeed',
|
||||
'leftForwardGain',
|
||||
'leftBackwardGain',
|
||||
'rightForwardGain',
|
||||
'rightBackwardGain',
|
||||
};
|
||||
|
||||
static const _skipKeys = {
|
||||
'createBy', 'createTime', 'updateBy', 'updateTime', 'delFlag', 'remark',
|
||||
};
|
||||
|
||||
static const _fieldLabels = {
|
||||
'id': 'ID',
|
||||
'deviceId': '设备ID',
|
||||
'siteId': '场站ID',
|
||||
'orgId': '组织ID',
|
||||
'runSpeed': '运行速度',
|
||||
'header1': 'Header1',
|
||||
'header2': 'Header2',
|
||||
'cmd': 'CMD',
|
||||
'chipUidSign': '芯片UID标记',
|
||||
'chipUid': '芯片UID',
|
||||
'remoteConfig': '远程配置',
|
||||
'knifeMotorMode': '刀盘电机模式',
|
||||
'walkMotorMode': '行走电机模式',
|
||||
'leftMotorReverse': '左电机反转',
|
||||
'rightMotorReverse': '右电机反转',
|
||||
'swapChannel': '交换通道',
|
||||
'use4G': '使用4G',
|
||||
'forwardSpeedLimit': '前进限速',
|
||||
'turnSpeedLimit': '转弯限速',
|
||||
'knifePolarity': '刀盘极性',
|
||||
'fanPolarity': '风扇极性',
|
||||
'throttlePolarity': '油门极性',
|
||||
'liftProtectTime': '升降保护时间',
|
||||
'dualRtk': '双RTK',
|
||||
'knifeChannel': '刀盘通道',
|
||||
'fanChannel': '风扇通道',
|
||||
'throttleChannel': '油门通道',
|
||||
'remoteType': '遥控类型',
|
||||
'relayBoard': '继电器板',
|
||||
'liftChannel': '升降通道',
|
||||
'chassisChannel': '底盘通道',
|
||||
'armChannel': '机械臂通道',
|
||||
'fuelPumpChannel': '燃油泵通道',
|
||||
'wifiName': 'WiFi名称',
|
||||
'wifiPassword': 'WiFi密码',
|
||||
'batteryType': '电池类型',
|
||||
'driveType': '驱动类型',
|
||||
'gearRatio': '齿轮比',
|
||||
'robotLength': '机器人长度',
|
||||
'robotWidth': '机器人宽度',
|
||||
'robotHeight': '机器人高度',
|
||||
'knifeWidth': '刀盘宽度',
|
||||
'tyreSize': '轮胎尺寸',
|
||||
'leftForwardGain': '左轮前进',
|
||||
'leftBackwardGain': '左轮后退',
|
||||
'rightForwardGain': '右轮前进',
|
||||
'rightBackwardGain': '右轮后退',
|
||||
'firmwareVersion': '固件版本',
|
||||
'crc16': 'CRC16',
|
||||
'tail1': '尾部1',
|
||||
'tail2': '尾部2',
|
||||
};
|
||||
|
||||
// ============ 生命周期 ============
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
_initControllers();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
void _initControllers() {
|
||||
_runSpeedController = TextEditingController();
|
||||
_leftForwardGainController = TextEditingController();
|
||||
_leftBackwardGainController = TextEditingController();
|
||||
_rightForwardGainController = TextEditingController();
|
||||
_rightBackwardGainController = TextEditingController();
|
||||
}
|
||||
|
||||
void _fillControllers(DeviceRunParamEntity param) {
|
||||
_runSpeedController.text = param.runSpeed.toStringAsFixed(0);
|
||||
_leftForwardGainController.text = param.leftForwardGain.toString();
|
||||
_leftBackwardGainController.text = param.leftBackwardGain.toString();
|
||||
_rightForwardGainController.text = param.rightForwardGain.toString();
|
||||
_rightBackwardGainController.text = param.rightBackwardGain.toString();
|
||||
}
|
||||
|
||||
TextEditingController? _controllerForKey(String key) {
|
||||
switch (key) {
|
||||
case 'runSpeed': return _runSpeedController;
|
||||
case 'leftForwardGain': return _leftForwardGainController;
|
||||
case 'leftBackwardGain': return _leftBackwardGainController;
|
||||
case 'rightForwardGain': return _rightForwardGainController;
|
||||
case 'rightBackwardGain': return _rightBackwardGainController;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _showReadonlyTip() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('暂不支持修改'),
|
||||
duration: Duration(seconds: 1),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: EdgeInsets.only(bottom: 80, left: 80, right: 80),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_runSpeedController.dispose();
|
||||
_leftForwardGainController.dispose();
|
||||
_leftBackwardGainController.dispose();
|
||||
_rightForwardGainController.dispose();
|
||||
_rightBackwardGainController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ============ 数据加载 ============
|
||||
Future<void> _loadData() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
final useCase = GetIt.I<GetDeviceRunParamUseCase>();
|
||||
final result = await useCase.execute(_deviceId);
|
||||
|
||||
if (!mounted) return;
|
||||
result.fold(
|
||||
(failure) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = failure.message;
|
||||
});
|
||||
},
|
||||
(param) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_param = param;
|
||||
});
|
||||
_fillControllers(param);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ============ 保存 ============
|
||||
Future<void> _handleSave() async {
|
||||
if (_isSaving || _param == null) return;
|
||||
setState(() => _isSaving = true);
|
||||
|
||||
try {
|
||||
// 🔐 前置权限校验:只有 code=200 && data=true 才允许保存
|
||||
final permissionService = GetIt.I<DevicePermissionService>();
|
||||
final hasPermission = await permissionService.checkPermission(_deviceId);
|
||||
|
||||
if (!mounted) return;
|
||||
if (!hasPermission) {
|
||||
setState(() => _isSaving = false);
|
||||
_showSnackBar('权限校验未通过,无法保存', isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 权限通过提示
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogCtx) => AlertDialog(
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Colors.green, size: 28),
|
||||
SizedBox(width: 8),
|
||||
Text('权限通过'),
|
||||
],
|
||||
),
|
||||
content: const Text('您有权限操作此设备'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogCtx),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
|
||||
final siteId = GetIt.I<SiteCubit>().state.selectedSite?.id ?? 0;
|
||||
final orgId = GetIt.I<AppUserCubit>().state.user?.orgId ?? 0;
|
||||
|
||||
final useCase = GetIt.I<SaveDeviceRunParamUseCase>();
|
||||
final params = <String, dynamic>{
|
||||
'id': _param!.id,
|
||||
'deviceId': _deviceId,
|
||||
'siteId': siteId,
|
||||
'orgId': orgId,
|
||||
'runSpeed': int.tryParse(_runSpeedController.text) ?? _param!.runSpeed,
|
||||
'leftForwardGain':
|
||||
double.tryParse(_leftForwardGainController.text) ??
|
||||
_param!.leftForwardGain,
|
||||
'leftBackwardGain':
|
||||
double.tryParse(_leftBackwardGainController.text) ??
|
||||
_param!.leftBackwardGain,
|
||||
'rightForwardGain':
|
||||
double.tryParse(_rightForwardGainController.text) ??
|
||||
_param!.rightForwardGain,
|
||||
'rightBackwardGain':
|
||||
double.tryParse(_rightBackwardGainController.text) ??
|
||||
_param!.rightBackwardGain,
|
||||
};
|
||||
|
||||
final result = await useCase.execute(params);
|
||||
|
||||
if (!mounted) return;
|
||||
result.fold(
|
||||
(failure) {
|
||||
setState(() => _isSaving = false);
|
||||
_showSnackBar('保存失败: ${failure.message}', isError: true);
|
||||
},
|
||||
(success) {
|
||||
setState(() => _isSaving = false);
|
||||
_showSnackBar('保存成功');
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _isSaving = false);
|
||||
_showSnackBar('保存异常: $e', isError: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showSnackBar(String message, {bool isError = false}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: isError ? Colors.red : Colors.green,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============ UI ============
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.dark.copyWith(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F6F8),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0.5,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'参数设置',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_deviceId,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: true,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(44),
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
indicatorColor: const Color(0xFF165DFF),
|
||||
indicatorWeight: 2,
|
||||
labelColor: const Color(0xFF165DFF),
|
||||
unselectedLabelColor: const Color(0xFF86909C),
|
||||
labelStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
tabs: const [
|
||||
Tab(text: '参数设置'),
|
||||
Tab(text: '增益参数设置'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildParamTab(),
|
||||
_buildGainParamTab(),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: _buildBottomBar(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============ 参数设置 Tab ============
|
||||
Widget _buildParamTab() {
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (_errorMessage != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 48, color: Color(0xFF86909C)),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF86909C)),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _loadData,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF165DFF),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final raw = _param!.rawData;
|
||||
final keys = raw.keys
|
||||
.where((k) => !_skipKeys.contains(k))
|
||||
.toList();
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: List.generate(keys.length, (i) {
|
||||
final key = keys[i];
|
||||
final label = _fieldLabels[key] ?? key;
|
||||
final isEditable = _editableKeys.contains(key);
|
||||
final isLast = i == keys.length - 1;
|
||||
final value = raw[key];
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildFieldRow(key, label, value, isEditable),
|
||||
if (!isLast) _buildDivider(),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============ 增益参数 Tab ============
|
||||
Widget _buildGainParamTab() {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.construction, size: 48, color: Color(0xFFC9CDD4)),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'暂未开放',
|
||||
style: TextStyle(fontSize: 14, color: Color(0xFF86909C)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============ 单行字段 ============
|
||||
Widget _buildFieldRow(
|
||||
String key,
|
||||
String label,
|
||||
dynamic value,
|
||||
bool editable,
|
||||
) {
|
||||
final controller = editable ? _controllerForKey(key) : null;
|
||||
final displayValue = value?.toString() ?? '-';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 90,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF4E5969),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: editable
|
||||
? TextField(
|
||||
controller: controller,
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[\d.]')),
|
||||
],
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF1D2129)),
|
||||
decoration: _inputDecoration(),
|
||||
)
|
||||
: GestureDetector(
|
||||
onTap: _showReadonlyTip,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF2F3F5),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: const Color(0xFFE5E6EB)),
|
||||
),
|
||||
child: Text(
|
||||
displayValue,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InputDecoration _inputDecoration() {
|
||||
return const InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
filled: true,
|
||||
fillColor: Color(0xFFF7F8FA),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
borderSide: BorderSide(color: Color(0xFFE5E6EB)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
borderSide: BorderSide(color: Color(0xFFE5E6EB)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
borderSide: BorderSide(color: Color(0xFF165DFF)),
|
||||
),
|
||||
isDense: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDivider() {
|
||||
return const Divider(height: 1, color: Color(0xFFF2F3F5));
|
||||
}
|
||||
|
||||
// ============ 底部保存按钮 ============
|
||||
Widget _buildBottomBar() {
|
||||
final bottom = MediaQuery.of(context).padding.bottom;
|
||||
return Container(
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 12 + bottom),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading || _isSaving ? null : _handleSave,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF165DFF),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: _isSaving
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'保存',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user