USB 摄像头适配iPadOS17+

This commit is contained in:
2026-10-22 12:53:24 +08:00
parent 582242e9aa
commit 58a23ef218
18 changed files with 288 additions and 140 deletions

View File

@@ -25,23 +25,12 @@ class _RobotParamSettingsPageState extends State<RobotParamSettingsPage>
String? _errorMessage;
DeviceRunParamEntity? _param;
// 表单控制器(仅可编辑的5个字段)
late TextEditingController _runSpeedController;
late TextEditingController _leftForwardGainController;
late TextEditingController _leftBackwardGainController;
late TextEditingController _rightForwardGainController;
late TextEditingController _rightBackwardGainController;
// 表单控制器:字段名 -> 输入框控制器(所有展示字段均可编辑,数据驱动)
final Map<String, TextEditingController> _controllers = {};
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',
@@ -49,10 +38,38 @@ class _RobotParamSettingsPageState extends State<RobotParamSettingsPage>
static const _fieldLabels = {
'id': 'ID',
'deviceId': '设备ID',
'deviceId': '设备序列号',
'siteId': '场站ID',
'orgId': '组织ID',
'runSpeed': '运行速度',
// ===== 设备参数实体字段(取自后端 @Excel 中文名)=====
'productModel': '产品型号',
'deviceType': '设备类型',
'rtkMode': 'RTK模式',
'cameraNum': '摄像头数量',
'powerType': '动力类型',
'wheelType': '车轮类型',
'engineType': '发动机类型',
'hasHost': '是否搭载上位机',
'walkingDriveType': '行走驱动类型',
'cutterDriveType': '割刀驱动类型',
'rtkAntennasNum': 'RTK天线数',
'handleType': '手柄类型',
'pcbVersion': 'PCB硬件版本',
'dimensions': '长宽高',
'bladeWidth': '割刀宽度',
'speedRatio': '转速比',
'cellularInfo': '蜂窝网络通信信息',
'bluetoothName': '蓝牙广播名',
'hardwareIdentifiers': '硬件标识信息',
'imuModel': 'IMU型号',
'tempHumiditySensor': '温湿度传感器',
'barometerPresent': '气压计在位',
'ultrasonicPresent': '超声波在位',
'dacPresent': 'DAC在位',
'operator': '质检员',
'inspectionNo': '检测报告号',
'dealer': '经销商',
'header1': 'Header1',
'header2': 'Header2',
'cmd': 'CMD',
@@ -106,56 +123,33 @@ class _RobotParamSettingsPageState extends State<RobotParamSettingsPage>
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_initControllers();
_loadData();
}
void _initControllers() {
_runSpeedController = TextEditingController();
_leftForwardGainController = TextEditingController();
_leftBackwardGainController = TextEditingController();
_rightForwardGainController = TextEditingController();
_rightBackwardGainController = TextEditingController();
}
/// 根据接口返回的全部字段动态创建输入控制器(跳过 _skipKeys),全部可编辑
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;
for (final c in _controllers.values) {
c.dispose();
}
_controllers.clear();
for (final entry in param.rawData.entries) {
if (_skipKeys.contains(entry.key)) continue;
_controllers[entry.key] = TextEditingController(
text: entry.value?.toString() ?? '',
);
}
}
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),
),
);
}
/// 数值字段用数字键盘,其余用文本键盘(依据接口返回值的原始类型判断)
bool _isNumericField(String key) => _param?.rawData[key] is num;
@override
void dispose() {
_tabController.dispose();
_runSpeedController.dispose();
_leftForwardGainController.dispose();
_leftBackwardGainController.dispose();
_rightForwardGainController.dispose();
_rightBackwardGainController.dispose();
for (final c in _controllers.values) {
c.dispose();
}
_controllers.clear();
super.dispose();
}
@@ -193,7 +187,9 @@ class _RobotParamSettingsPageState extends State<RobotParamSettingsPage>
setState(() => _isSaving = true);
try {
// 🔐 前置权限校验:只有 code=200 && data=true 才允许保存
// ⛔ 业务变更:设备参数保存不再校验权限,保留代码备查
// (原逻辑:先调 GET /iot/device/hasPermission,通过后再弹"权限通过"确认框)
/*
final permissionService = GetIt.I<DevicePermissionService>();
final hasPermission = await permissionService.checkPermission(_deviceId);
@@ -204,7 +200,6 @@ class _RobotParamSettingsPageState extends State<RobotParamSettingsPage>
return;
}
// 权限通过提示
await showDialog(
context: context,
barrierDismissible: false,
@@ -226,30 +221,32 @@ class _RobotParamSettingsPageState extends State<RobotParamSettingsPage>
),
);
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 params = <String, dynamic>{..._param!.rawData};
for (final entry in _controllers.entries) {
final original = _param!.rawData[entry.key];
final text = entry.value.text.trim();
if (original is int) {
params[entry.key] = int.tryParse(text) ?? original;
} else if (original is double) {
params[entry.key] = double.tryParse(text) ?? original;
} else if (original is num) {
params[entry.key] = num.tryParse(text) ?? original;
} else {
params[entry.key] = text;
}
}
// 关键字段兜底:接口未返回时用当前上下文补齐
params['id'] ??= _param!.id;
params['deviceId'] ??= _deviceId;
params['siteId'] ??= siteId;
params['orgId'] ??= orgId;
final result = await useCase.execute(params);
@@ -411,14 +408,11 @@ class _RobotParamSettingsPageState extends State<RobotParamSettingsPage>
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),
_buildFieldRow(key),
if (!isLast) _buildDivider(),
],
);
@@ -446,65 +440,54 @@ class _RobotParamSettingsPageState extends State<RobotParamSettingsPage>
}
// ============ 单行字段 ============
Widget _buildFieldRow(
String key,
String label,
dynamic value,
bool editable,
) {
final controller = editable ? _controllerForKey(key) : null;
final displayValue = value?.toString() ?? '-';
Widget _buildFieldRow(String key) {
final controller = _controllers[key];
final isNumeric = _isNumericField(key);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 90,
child: Text(
label,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF4E5969),
fontWeight: FontWeight.w500,
),
width: 130,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 主行:始终显示字段名(英文 key),避免中英混排
Text(
key,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF4E5969),
fontWeight: FontWeight.w500,
),
),
// 副行:有中文映射才显示(小字灰色),没有则不显示
if (_fieldLabels[key] != null)
Text(
_fieldLabels[key]!,
style: const TextStyle(
fontSize: 11,
color: Color(0xFF86909C),
),
),
],
),
),
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),
),
),
),
),
child: TextField(
controller: controller,
keyboardType: isNumeric
? const TextInputType.numberWithOptions(decimal: true)
: TextInputType.text,
inputFormatters: isNumeric
? [FilteringTextInputFormatter.allow(RegExp(r'[\d.\-]'))]
: null,
style: const TextStyle(fontSize: 14, color: Color(0xFF1D2129)),
decoration: _inputDecoration(),
),
),
],
),