USB 摄像头适配iPadOS17+
This commit is contained in:
@@ -96,6 +96,8 @@
|
||||
"battery": "Battery",
|
||||
"chip_temp": "Chip Temp",
|
||||
"knife_speed": "Knife Speed",
|
||||
"linear_speed": "Linear Speed",
|
||||
"angular_speed": "Angular Speed",
|
||||
"control_mode": "Control Mode",
|
||||
"longitude": "Longitude",
|
||||
"latitude": "Latitude",
|
||||
@@ -552,7 +554,11 @@
|
||||
"path_planning": "Path Planning",
|
||||
"path_planning_desc": "View and edit the robot patrol path",
|
||||
"release_permission_success": "Remote control permission released",
|
||||
"release_permission_failed": "Failed to release control permission"
|
||||
"release_permission_failed": "Failed to release control permission",
|
||||
"stat_online_devices": "Online Devices",
|
||||
"stat_working": "Working",
|
||||
"stat_standby": "Standby",
|
||||
"stat_offline": "Offline"
|
||||
},
|
||||
|
||||
"alarm_center": {
|
||||
|
||||
@@ -96,6 +96,8 @@
|
||||
"battery": "电量",
|
||||
"chip_temp": "芯片温度",
|
||||
"knife_speed": "割刀速度",
|
||||
"linear_speed": "线速度",
|
||||
"angular_speed": "角速度",
|
||||
"control_mode": "控制模式",
|
||||
"longitude": "经度",
|
||||
"latitude": "纬度",
|
||||
@@ -565,7 +567,11 @@
|
||||
"path_planning": "路径规划",
|
||||
"path_planning_desc": "查看和编辑机器人巡检路径",
|
||||
"release_permission_success": "已释放远程控制权限",
|
||||
"release_permission_failed": "控制权限释放失败"
|
||||
"release_permission_failed": "控制权限释放失败",
|
||||
"stat_online_devices": "在线设备",
|
||||
"stat_working": "作业中",
|
||||
"stat_standby": "待命",
|
||||
"stat_offline": "离线数"
|
||||
},
|
||||
|
||||
"alarm_center": {
|
||||
|
||||
@@ -376,6 +376,28 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
_buildTableCell('--'),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
_buildTableCell(
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
).translate('running_status.linear_speed') +
|
||||
"(m/s)",
|
||||
),
|
||||
_buildTableCell(status.linearSpeed.toStringAsFixed(2)),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
_buildTableCell(
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
).translate('running_status.angular_speed') +
|
||||
"(rad/s)",
|
||||
),
|
||||
_buildTableCell(status.angularSpeed.toStringAsFixed(2)),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
_buildTableCell(
|
||||
|
||||
@@ -28,6 +28,8 @@ class HostRealtimeMessageEntity extends Equatable {
|
||||
final double rightMotorRpm; // 实时右电机转速
|
||||
final double roll; // 实时翻滚角度
|
||||
final String controlMode; // 当前控制模式(遥控器拨杆对应)
|
||||
final double linearSpeed; // 实时线速度 m/s(前进为正,后退为负)
|
||||
final double angularSpeed; // 实时角速度 rad/s(左转为正,右转(偏航)为负)
|
||||
|
||||
/// 原始 data 报文,便于后端补字段时兜底
|
||||
final Map<String, dynamic> rawData;
|
||||
@@ -56,6 +58,8 @@ class HostRealtimeMessageEntity extends Equatable {
|
||||
this.rightMotorRpm = 0,
|
||||
this.roll = 0,
|
||||
this.controlMode = '',
|
||||
this.linearSpeed = 0,
|
||||
this.angularSpeed = 0,
|
||||
this.rawData = const {},
|
||||
});
|
||||
|
||||
@@ -85,6 +89,8 @@ class HostRealtimeMessageEntity extends Equatable {
|
||||
rightMotorRpm: _toDouble(data['right_motor_rpm']),
|
||||
roll: _toDouble(data['roll']),
|
||||
controlMode: data['control_mode']?.toString() ?? '',
|
||||
linearSpeed: _toDouble(data['linear_speed']),
|
||||
angularSpeed: _toDouble(data['angular_speed']),
|
||||
rawData: data,
|
||||
);
|
||||
}
|
||||
@@ -128,5 +134,7 @@ class HostRealtimeMessageEntity extends Equatable {
|
||||
rightMotorRpm,
|
||||
roll,
|
||||
controlMode,
|
||||
linearSpeed,
|
||||
angularSpeed,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ class RunningStatusEntity extends Equatable {
|
||||
|
||||
// === 主机实时话题(device/{sn}/host/realTime/message)扩展字段(新增,纯附加) ===
|
||||
final double totalWorkDistance; // host 累计作业里程(供"巡检里程"展示)
|
||||
final double linearSpeed; // host 实时线速度 m/s(前进为正,后退为负)
|
||||
final double angularSpeed; // host 实时角速度 rad/s(左转为正,右转为负)
|
||||
final bool hostDataReceived; // 是否已收到 host 实时数据(未收到时 UI 显示"未知")
|
||||
|
||||
const RunningStatusEntity({
|
||||
@@ -72,6 +74,8 @@ class RunningStatusEntity extends Equatable {
|
||||
this.workingArea = '0',
|
||||
this.obstacleFlag = '0',
|
||||
this.totalWorkDistance = 0.0,
|
||||
this.linearSpeed = 0.0,
|
||||
this.angularSpeed = 0.0,
|
||||
this.hostDataReceived = false,
|
||||
});
|
||||
|
||||
@@ -173,6 +177,8 @@ class RunningStatusEntity extends Equatable {
|
||||
String? workingArea,
|
||||
String? obstacleFlag,
|
||||
double? totalWorkDistance,
|
||||
double? linearSpeed,
|
||||
double? angularSpeed,
|
||||
bool? hostDataReceived,
|
||||
}) {
|
||||
return RunningStatusEntity(
|
||||
@@ -201,6 +207,8 @@ class RunningStatusEntity extends Equatable {
|
||||
workingArea: workingArea ?? this.workingArea,
|
||||
obstacleFlag: obstacleFlag ?? this.obstacleFlag,
|
||||
totalWorkDistance: totalWorkDistance ?? this.totalWorkDistance,
|
||||
linearSpeed: linearSpeed ?? this.linearSpeed,
|
||||
angularSpeed: angularSpeed ?? this.angularSpeed,
|
||||
hostDataReceived: hostDataReceived ?? this.hostDataReceived,
|
||||
);
|
||||
}
|
||||
@@ -232,6 +240,8 @@ class RunningStatusEntity extends Equatable {
|
||||
workingArea,
|
||||
obstacleFlag,
|
||||
totalWorkDistance,
|
||||
linearSpeed,
|
||||
angularSpeed,
|
||||
hostDataReceived,
|
||||
];
|
||||
|
||||
|
||||
@@ -327,6 +327,9 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
workingArea: host.totalWorkArea.toString(),
|
||||
// 🔥 供全功能模式(pv_control)展示:累计作业里程 + host 已推数据标记
|
||||
totalWorkDistance: host.totalWorkDistance,
|
||||
// 🔥 车体实测线速度/角速度(后端新增 linear_speed / angular_speed)
|
||||
linearSpeed: host.linearSpeed,
|
||||
angularSpeed: host.angularSpeed,
|
||||
hostDataReceived: true,
|
||||
);
|
||||
|
||||
@@ -334,7 +337,7 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
_cachedGps ??= GPSEntity(_cachedStatus!.latitude, _cachedStatus!.longitude);
|
||||
|
||||
debugPrint(
|
||||
'${LogTime.now()} 🛰️ [MQTT-主机实时] 已合并 → 电量=${host.batteryPercentage}% 电压=${host.batteryVoltage}V 控制模式=${host.controlMode}',
|
||||
'${LogTime.now()} 🛰️ [MQTT-主机实时] 已合并 → 电量=${host.batteryPercentage}% 电压=${host.batteryVoltage}V 控制模式=${host.controlMode} 线速度=${host.linearSpeed}m/s 角速度=${host.angularSpeed}rad/s',
|
||||
);
|
||||
|
||||
// 500ms 节流:与 location/车辆实时 发射链路一致
|
||||
|
||||
@@ -426,25 +426,33 @@ class _RobotListViewState extends State<RobotListView> {
|
||||
children: [
|
||||
_buildStatusItem(
|
||||
value: '$onlineCount',
|
||||
label: '在线设备',
|
||||
label: AppLocalizations.of(
|
||||
context,
|
||||
).translate('device_list_v2.stat_online_devices'),
|
||||
valueColor: context.appColors.primary,
|
||||
),
|
||||
Container(width: 1, height: 40, color: context.appColors.divider),
|
||||
_buildStatusItem(
|
||||
value: '$workingCount',
|
||||
label: '作业中',
|
||||
label: AppLocalizations.of(
|
||||
context,
|
||||
).translate('device_list_v2.stat_working'),
|
||||
valueColor: context.appColors.success,
|
||||
),
|
||||
Container(width: 1, height: 40, color: context.appColors.divider),
|
||||
_buildStatusItem(
|
||||
value: '$standbyCount',
|
||||
label: '待命',
|
||||
label: AppLocalizations.of(
|
||||
context,
|
||||
).translate('device_list_v2.stat_standby'),
|
||||
valueColor: context.appColors.warning,
|
||||
),
|
||||
Container(width: 1, height: 40, color: context.appColors.divider),
|
||||
_buildStatusItem(
|
||||
value: '$faultCount',
|
||||
label: '离线数',
|
||||
label: AppLocalizations.of(
|
||||
context,
|
||||
).translate('device_list_v2.stat_offline'),
|
||||
valueColor: context.appColors.danger,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -157,10 +157,27 @@ class DeviceItemWidget extends StatelessWidget {
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: Text(status, style: TextStyle(fontSize: 12, color: textColor)),
|
||||
child: Text(
|
||||
_displayStatus(context, status),
|
||||
style: TextStyle(fontSize: 12, color: textColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔥 仅做显示层映射:把内部语义状态值转成当前语言文案,未命中则原样显示
|
||||
/// 数据层与颜色 switch 仍以 '在线'/'离线'/'异常' 为比较键,此处不改数据
|
||||
String _displayStatus(BuildContext context, String status) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
switch (status) {
|
||||
case '在线':
|
||||
return loc.translate('device_list_v2.online');
|
||||
case '离线':
|
||||
return loc.translate('device_list_v2.offline');
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDeviceData(BuildContext context, DeviceEntity device) {
|
||||
final dataItems = <Widget>[];
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||||
import 'package:maibu_satabot_v2/core/theme/AppTheme.dart';
|
||||
|
||||
import '../../domain/entities/drone_station_entity.dart';
|
||||
@@ -99,7 +100,7 @@ class DroneStationItemCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
// 机场状态徽章
|
||||
_buildStatusBadge(station.isOnline),
|
||||
_buildStatusBadge(context, station.isOnline),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -230,7 +231,7 @@ class DroneStationItemCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// 状态徽章
|
||||
Widget _buildStatusBadge( bool isOnline) {
|
||||
Widget _buildStatusBadge(BuildContext context, bool isOnline) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
@@ -263,7 +264,9 @@ class DroneStationItemCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
'${isOnline ? '在线' : '离线'}',
|
||||
AppLocalizations.of(context).translate(
|
||||
isOnline ? 'device_list_v2.online' : 'device_list_v2.offline',
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
|
||||
@@ -208,10 +208,26 @@ class RobotItemCard extends StatelessWidget {
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(status, style: TextStyle(fontSize: 12, color: textColor)),
|
||||
child: Text(
|
||||
_displayStatus(context, status),
|
||||
style: TextStyle(fontSize: 12, color: textColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔥 仅做显示层映射:把内部语义状态值转成当前语言文案,未命中则原样显示
|
||||
String _displayStatus(BuildContext context, String status) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
switch (status) {
|
||||
case '在线':
|
||||
return loc.translate('device_list_v2.online');
|
||||
case '离线':
|
||||
return loc.translate('device_list_v2.offline');
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔥 仅做显示层映射:把内部语义状态值转成当前语言文案,未命中则原样显示
|
||||
/// 注意:model 默认值与列表过滤逻辑仍使用 '待机中'/'充电中',此处不改数据
|
||||
String _displayTask(BuildContext context, String task) {
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -39,6 +39,17 @@ class UsbCameraCubit extends Cubit<UsbCameraState> {
|
||||
Future<void> start() async {
|
||||
_safeEmit(state.copyWith(phase: UsbCameraPhase.scanning));
|
||||
try {
|
||||
// 平台/机型能力探测:iPhone 系统层面不开放外接摄像头输入,直接给专属提示,
|
||||
// 避免枚举到空列表后显示含糊的「未检测到 USB 摄像头」。
|
||||
final supported = await plugin.UsbCameraAndroid.isSupported();
|
||||
if (!supported) {
|
||||
_safeEmit(state.copyWith(
|
||||
phase: UsbCameraPhase.openFailed,
|
||||
message: 'iPhone 不支持 USB 摄像头输入\n请改用 iPad(USB-C 接口,iPadOS 17 及以上)连接采集卡',
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
final devices = await _service.enumerateDevices();
|
||||
|
||||
if (devices.isEmpty) {
|
||||
|
||||
@@ -39,6 +39,9 @@ public class UsbCameraAndroidPlugin implements FlutterPlugin, MethodChannel.Meth
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
switch (call.method) {
|
||||
case "isSupported":
|
||||
result.success(true);
|
||||
break;
|
||||
case "getDeviceList":
|
||||
result.success(engine.getDeviceList());
|
||||
break;
|
||||
|
||||
@@ -46,6 +46,19 @@ class UsbCameraAndroid {
|
||||
return _eventStream!;
|
||||
}
|
||||
|
||||
/// 当前平台/设备是否支持 USB(UVC)摄像头。
|
||||
/// - Android:始终 true。
|
||||
/// - iOS:仅 iPadOS 17+ 的 iPad 为 true;iPhone 或 iOS<17 为 false。
|
||||
/// 通道未注册(异常)时保守返回 true,交由上层继续走原有枚举/错误流程。
|
||||
static Future<bool> isSupported() async {
|
||||
try {
|
||||
final result = await _methodChannel.invokeMethod('isSupported');
|
||||
return result as bool? ?? true;
|
||||
} catch (_) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// 枚举当前连接的 UVC 设备。
|
||||
static Future<List<UsbCameraDeviceInfo>> getDeviceList() async {
|
||||
final List<dynamic> result =
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Android 原生 UVC 预览画面。
|
||||
/// 原生 UVC 预览画面(Android: AndroidView / iOS: UiKitView)。
|
||||
class UsbCameraPreviewView extends StatelessWidget {
|
||||
const UsbCameraPreviewView({super.key});
|
||||
|
||||
@@ -9,6 +10,13 @@ class UsbCameraPreviewView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS) {
|
||||
return const UiKitView(
|
||||
viewType: _viewType,
|
||||
layoutDirection: TextDirection.ltr,
|
||||
creationParamsCodec: StandardMessageCodec(),
|
||||
);
|
||||
}
|
||||
return const AndroidView(
|
||||
viewType: _viewType,
|
||||
layoutDirection: TextDirection.ltr,
|
||||
|
||||
@@ -13,6 +13,8 @@ flutter:
|
||||
android:
|
||||
package: com.maibu.usb_camera_android
|
||||
pluginClass: UsbCameraAndroidPlugin
|
||||
ios:
|
||||
pluginClass: UsbCameraAndroidPlugin
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
|
||||
20
packages/video_player_android/README.md
Normal file
20
packages/video_player_android/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# video\_player\_android
|
||||
|
||||
The Android implementation of [`video_player`][1].
|
||||
|
||||
## Usage
|
||||
|
||||
This package is [endorsed][2], which means you can simply use `video_player`
|
||||
normally. This package will be automatically included in your app when you do,
|
||||
so you do not need to add it to your `pubspec.yaml`.
|
||||
|
||||
However, if you `import` this package to use any of its APIs directly, you
|
||||
should add it to your `pubspec.yaml` as usual.
|
||||
|
||||
## Known issues
|
||||
|
||||
Using `VideoViewType.platformView` is not currently recommended on Android due to a known [issue][3] affecting platform views on Android.
|
||||
|
||||
[1]: https://pub.dev/packages/video_player
|
||||
[2]: https://flutter.dev/to/endorsed-federated-plugin
|
||||
[3]: https://github.com/flutter/flutter/issues/164899
|
||||
9
packages/video_player_android/example/README.md
Normal file
9
packages/video_player_android/example/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Platform Implementation Test App
|
||||
|
||||
This is a test app for manual testing and automated integration testing
|
||||
of this platform implementation. It is not intended to demonstrate actual use of
|
||||
this package, since the intent is that plugin clients use the app-facing
|
||||
package.
|
||||
|
||||
Unless you are making changes to this implementation package, this example is
|
||||
very unlikely to be relevant.
|
||||
Reference in New Issue
Block a user