Browse Source
- 去掉底部中间的「AI」按钮(连同按设备挑助手的 openAiAssistant),EMAI / EaiCar 车载助手 回到首页网格当普通卡片,拖拽排序改回 service.move - 首页左上角设备入口:从没连过也没绑过任何设备时显示加号,点了直接进「选择设备」; 有过设备时显示最后连接那台的小图标,连着亮、没连置灰 - 设备页:主图下面直接铺已绑定设备卡片(原「设备管理」页内容,抽成 BoundDeviceCard 两处共用) 和固件升级一行(当前/最新版本,设备版本低于服务端时右边直接「开始升级」,进度在行内走, 升级中返回先确认);「添加设备」挪到页面最下面 - 原「设备管理」「固件升级」页保留(语音纪要「我的」、固件更新提醒弹窗仍在用) 验证:dart analyze 无新增 error/warning;flutter test 224 全过;release 包装到 ALN-AL80 看过首页与设备页(未连接态)。连接态电量/升级按钮、首次加号形态未在真机上看。 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>main
10 changed files with 777 additions and 589 deletions
@ -0,0 +1,361 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import 'package:get/get.dart'; |
|||
|
|||
import '/data/models/device_images.dart'; |
|||
import '/data/models/user_Info.dart'; |
|||
import '../../controllers/my_devices_controller.dart'; |
|||
import 'device_settings_sheet.dart'; |
|||
|
|||
enum _MyDeviceCategory { |
|||
unknown(0), |
|||
classicHeadset(1), |
|||
bleHeadset(2), |
|||
voiceRecorder(4), |
|||
smartEye(5); |
|||
|
|||
const _MyDeviceCategory(this.value); |
|||
final int value; |
|||
|
|||
static _MyDeviceCategory fromValue(int value) => |
|||
_MyDeviceCategory.values.firstWhere( |
|||
(c) => c.value == value, |
|||
orElse: () => _MyDeviceCategory.unknown, |
|||
); |
|||
|
|||
String get displayName { |
|||
switch (this) { |
|||
case _MyDeviceCategory.unknown: |
|||
return 'deviceTypeUnknown'.tr; |
|||
case _MyDeviceCategory.classicHeadset: |
|||
return 'deviceTypeClassicBluetoothHeadset'.tr; |
|||
case _MyDeviceCategory.bleHeadset: |
|||
return 'deviceTypeBleBluetoothHeadset'.tr; |
|||
case _MyDeviceCategory.voiceRecorder: |
|||
return 'deviceTypeSmartVoiceRecorder'.tr; |
|||
case _MyDeviceCategory.smartEye: |
|||
return 'deviceTypeSmartEye'.tr; |
|||
} |
|||
} |
|||
|
|||
IconData get icon { |
|||
switch (this) { |
|||
case _MyDeviceCategory.classicHeadset: |
|||
case _MyDeviceCategory.bleHeadset: |
|||
return Icons.headphones; |
|||
case _MyDeviceCategory.voiceRecorder: |
|||
return Icons.mic; |
|||
case _MyDeviceCategory.smartEye: |
|||
return Icons.visibility; |
|||
default: |
|||
return Icons.device_unknown; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 已绑定设备的一行卡片:产品小图标 + 名称/MAC + 连接状态 + 解绑, |
|||
/// 有设置面板的设备整行可点。 |
|||
/// |
|||
/// 「设备管理」页与设备页(2026-09-26 起设备管理直接铺在设备主图下面)共用这一份。 |
|||
/// 依赖 [MyDevicesController]:连接状态、能否打开面板、解绑都走它。 |
|||
class BoundDeviceCard extends StatelessWidget { |
|||
const BoundDeviceCard({ |
|||
super.key, |
|||
required this.device, |
|||
required this.controller, |
|||
required this.isDark, |
|||
}); |
|||
|
|||
final UserDevice device; |
|||
final MyDevicesController controller; |
|||
final bool isDark; |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
final category = _MyDeviceCategory.fromValue(device.devicetype); |
|||
final connected = controller.isConnected(device); |
|||
const connectedColor = Color(0xFF22C55E); |
|||
// 2026-09-22:有设置面板的设备(目前只有 EaiCar)整行可点,点开底部面板。 |
|||
// 解绑按钮自己的 GestureDetector 在子树里先吃掉事件,不会冒泡到这里; |
|||
// 其它设备 onTap 为 null,行为与以前一字不差。 |
|||
final canOpen = controller.canOpenSettings(device); |
|||
|
|||
return GestureDetector( |
|||
behavior: HitTestBehavior.opaque, |
|||
onTap: canOpen ? () => DeviceSettingsSheet.show(device) : null, |
|||
child: Container( |
|||
margin: EdgeInsets.only(bottom: 10.h), |
|||
padding: EdgeInsets.all(18.w), |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(18.r), |
|||
color: isDark |
|||
? (connected |
|||
? connectedColor.withValues(alpha: 0.04) |
|||
: Colors.white.withValues(alpha: 0.03)) |
|||
: Colors.white, |
|||
border: Border.all( |
|||
color: connected |
|||
? connectedColor.withValues(alpha: 0.25) |
|||
: (isDark |
|||
? Colors.white.withValues(alpha: 0.05) |
|||
: Colors.black.withValues(alpha: 0.04)), |
|||
), |
|||
boxShadow: isDark |
|||
? null |
|||
: [ |
|||
BoxShadow( |
|||
color: Colors.black.withValues(alpha: 0.03), |
|||
blurRadius: 10, |
|||
offset: const Offset(0, 2), |
|||
), |
|||
], |
|||
), |
|||
child: Column( |
|||
children: [ |
|||
Row( |
|||
children: [ |
|||
// 产品小图标(<设备名>_icon.png,取图规则见 DeviceImages)。 |
|||
// ⚠️ 这里用的是**服务端下发的** device.devicename,不是蓝牙广播名—— |
|||
// 这一页列的是「绑定关系」,可能包含此刻没连上、甚至不在身边的设备, |
|||
// 拿当前连接的名字去取图会让所有行显示成同一台。 |
|||
Container( |
|||
width: 44.w, |
|||
height: 44.w, |
|||
padding: EdgeInsets.all(4.w), |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(12.r), |
|||
// 底色仍然区分连接状态:换成实拍图之后,原来靠渐变色 |
|||
// 传达的「这台连着」就没了,得留个视觉线索。 |
|||
color: connected |
|||
? connectedColor.withValues(alpha: 0.12) |
|||
: (isDark |
|||
? Colors.white.withValues(alpha: 0.06) |
|||
: const Color(0xFF6366F1).withValues(alpha: 0.08)), |
|||
), |
|||
child: Image.asset( |
|||
DeviceImages.iconFor(device.devicename), |
|||
fit: BoxFit.contain, |
|||
// 图缺失时退回品类图标,不让整行塌成红色报错块 |
|||
errorBuilder: (_, __, ___) => Icon( |
|||
category.icon, |
|||
color: connected |
|||
? connectedColor |
|||
: const Color(0xFF6366F1), |
|||
size: 22.sp, |
|||
), |
|||
), |
|||
), |
|||
SizedBox(width: 14.w), |
|||
Expanded( |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Row( |
|||
children: [ |
|||
Flexible( |
|||
child: Text( |
|||
device.devicename.isNotEmpty |
|||
? device.devicename |
|||
: 'deviceTypeUnknown'.tr, |
|||
maxLines: 1, |
|||
overflow: TextOverflow.ellipsis, |
|||
style: TextStyle( |
|||
fontSize: 16.sp, |
|||
fontWeight: FontWeight.w700, |
|||
color: isDark |
|||
? Colors.white |
|||
: const Color(0xFF1A1A2E), |
|||
), |
|||
), |
|||
), |
|||
if (connected) ...[ |
|||
SizedBox(width: 8.w), |
|||
Container( |
|||
padding: EdgeInsets.symmetric( |
|||
horizontal: 8.w, vertical: 3.h), |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(6.r), |
|||
color: connectedColor.withValues(alpha: 0.12), |
|||
), |
|||
child: Text( |
|||
'connected'.tr, |
|||
style: TextStyle( |
|||
fontSize: 10.sp, |
|||
fontWeight: FontWeight.w600, |
|||
color: connectedColor, |
|||
), |
|||
), |
|||
), |
|||
], |
|||
], |
|||
), |
|||
SizedBox(height: 3.h), |
|||
Text( |
|||
device.devicemac.isNotEmpty |
|||
? device.devicemac |
|||
: 'noMacAddress'.tr, |
|||
style: TextStyle( |
|||
fontSize: 11.sp, |
|||
fontWeight: FontWeight.w500, |
|||
color: isDark |
|||
? Colors.white.withValues(alpha: 0.3) |
|||
: const Color(0xFF999999), |
|||
fontFamily: 'Menlo', |
|||
letterSpacing: 0.5, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
if (canOpen) ...[ |
|||
Icon( |
|||
Icons.tune, |
|||
size: 18.sp, |
|||
color: connected |
|||
? connectedColor |
|||
: (isDark |
|||
? Colors.white.withValues(alpha: 0.3) |
|||
: const Color(0xFFBBBBBB)), |
|||
), |
|||
SizedBox(width: 10.w), |
|||
], |
|||
GestureDetector( |
|||
onTap: () => _showUnbindDialog(device, isDark), |
|||
child: Container( |
|||
padding: |
|||
EdgeInsets.symmetric(horizontal: 12.w, vertical: 7.h), |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(10.r), |
|||
border: Border.all( |
|||
color: isDark |
|||
? Colors.white.withValues(alpha: 0.08) |
|||
: Colors.black.withValues(alpha: 0.08), |
|||
), |
|||
), |
|||
child: Text( |
|||
'unbind'.tr, |
|||
style: TextStyle( |
|||
fontSize: 12.sp, |
|||
fontWeight: FontWeight.w600, |
|||
color: isDark |
|||
? Colors.white.withValues(alpha: 0.4) |
|||
: const Color(0xFF999999), |
|||
), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
if (device.hasRealLicense || device.cmei.isNotEmpty) ...[ |
|||
SizedBox(height: 14.h), |
|||
Container( |
|||
padding: EdgeInsets.all(12.w), |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(12.r), |
|||
color: isDark |
|||
? Colors.white.withValues(alpha: 0.03) |
|||
: const Color(0xFFF7F7FA), |
|||
), |
|||
child: Column( |
|||
children: [ |
|||
if (device.hasRealLicense) |
|||
_buildFieldRow( |
|||
label: 'licenseCode'.tr, |
|||
value: device.license, |
|||
isDark: isDark), |
|||
if (device.hasRealLicense && device.cmei.isNotEmpty) |
|||
SizedBox(height: 8.h), |
|||
if (device.cmei.isNotEmpty) |
|||
_buildFieldRow( |
|||
label: 'CMEI', value: device.cmei, isDark: isDark), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildFieldRow({ |
|||
required String label, |
|||
required String value, |
|||
required bool isDark, |
|||
}) { |
|||
return Row( |
|||
children: [ |
|||
SizedBox( |
|||
width: 50.w, |
|||
child: Text( |
|||
label, |
|||
style: TextStyle( |
|||
fontSize: 10.sp, |
|||
fontWeight: FontWeight.w600, |
|||
color: isDark |
|||
? Colors.white.withValues(alpha: 0.25) |
|||
: const Color(0xFFBBBBBB), |
|||
letterSpacing: 0.5, |
|||
), |
|||
), |
|||
), |
|||
SizedBox(width: 8.w), |
|||
Expanded( |
|||
child: Text( |
|||
value, |
|||
style: TextStyle( |
|||
fontSize: 11.sp, |
|||
fontWeight: FontWeight.w500, |
|||
color: isDark |
|||
? Colors.white.withValues(alpha: 0.5) |
|||
: const Color(0xFF666666), |
|||
fontFamily: 'Menlo', |
|||
letterSpacing: 0.3, |
|||
), |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
void _showUnbindDialog(UserDevice device, bool isDark) { |
|||
Get.dialog( |
|||
AlertDialog( |
|||
backgroundColor: |
|||
isDark ? const Color(0xFF1C1C1E) : Colors.white, |
|||
title: Text( |
|||
'confirmUnbind'.tr, |
|||
style: TextStyle(color: isDark ? Colors.white : Colors.black87), |
|||
), |
|||
content: Text( |
|||
'confirmUnbindMessage' |
|||
.tr |
|||
.replaceAll('{deviceName}', device.devicename), |
|||
style: |
|||
TextStyle(color: isDark ? Colors.white70 : Colors.black54), |
|||
), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () => Get.back(), |
|||
child: Text( |
|||
'cancel'.tr, |
|||
style: TextStyle( |
|||
color: isDark ? Colors.white70 : Colors.grey[600]), |
|||
), |
|||
), |
|||
TextButton( |
|||
onPressed: () { |
|||
Get.back(); |
|||
controller.unbindDevice(device); |
|||
}, |
|||
child: Text( |
|||
'confirmUnbindAction'.tr, |
|||
style: const TextStyle(color: Colors.red), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue