You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
646 lines
21 KiB
646 lines
21 KiB
import '../../../routes/app_routes.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:get/get.dart';
|
|
import '/core/theme/app_colors.dart';
|
|
import '/data/models/user_Info.dart';
|
|
import '/data/services/ble_manager.dart';
|
|
import '../controllers/devices_controller.dart';
|
|
|
|
// 设备类型枚举
|
|
enum DeviceType {
|
|
Unknown(0),
|
|
Classic_bluetooth_Headset(1),
|
|
BLE_Bluetooth_Headset(2),
|
|
Smart_Voice_Recorder(3),
|
|
SmartEye(4);
|
|
|
|
const DeviceType(this.value);
|
|
final int value;
|
|
|
|
String get displayName {
|
|
switch (this) {
|
|
case DeviceType.Unknown:
|
|
return "deviceTypeUnknown".tr;
|
|
case DeviceType.Classic_bluetooth_Headset:
|
|
return "deviceTypeClassicBluetoothHeadset".tr;
|
|
case DeviceType.BLE_Bluetooth_Headset:
|
|
return "deviceTypeBleBluetoothHeadset".tr;
|
|
case DeviceType.Smart_Voice_Recorder:
|
|
return "deviceTypeSmartVoiceRecorder".tr;
|
|
case DeviceType.SmartEye:
|
|
return "deviceTypeSmartEye".tr;
|
|
}
|
|
}
|
|
|
|
static DeviceType fromValue(int value) {
|
|
return DeviceType.values.firstWhere(
|
|
(type) => type.value == value,
|
|
orElse: () => DeviceType.Unknown,
|
|
);
|
|
}
|
|
|
|
IconData get icon {
|
|
switch (this) {
|
|
case DeviceType.Classic_bluetooth_Headset:
|
|
case DeviceType.BLE_Bluetooth_Headset:
|
|
return Icons.headphones;
|
|
case DeviceType.Smart_Voice_Recorder:
|
|
return Icons.mic;
|
|
case DeviceType.SmartEye:
|
|
return Icons.visibility;
|
|
default:
|
|
return Icons.device_unknown;
|
|
}
|
|
}
|
|
}
|
|
|
|
class DevicesView extends GetView<DevicesController> {
|
|
const DevicesView({super.key});
|
|
|
|
bool _isConnected(UserDevice device) {
|
|
try {
|
|
final bleManager = Get.find<BleManager>();
|
|
return bleManager.currentDeviceAddress != null &&
|
|
bleManager.currentDeviceAddress == device.devicemac;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
final bg = isDark ? const Color(0xFF0A0A0F) : const Color(0xFFF5F6FA);
|
|
final topBg = isDark ? const Color(0xFF0F1018) : const Color(0xFFF0F1F6);
|
|
|
|
return Scaffold(
|
|
backgroundColor: bg,
|
|
body: Column(
|
|
children: [
|
|
// 顶部区域
|
|
_buildTopArea(context, isDark, bg, topBg),
|
|
// 设备列表
|
|
Expanded(child: _buildDevicesList(isDark, topBg)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTopArea(
|
|
BuildContext context, bool isDark, Color bg, Color topBg) {
|
|
return Container(
|
|
padding: EdgeInsets.only(
|
|
top: MediaQuery.of(context).padding.top + 8.h,
|
|
left: 20.w,
|
|
right: 20.w,
|
|
bottom: 24.h,
|
|
),
|
|
decoration: BoxDecoration(color: bg),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 导航栏
|
|
Row(
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () => Get.back(),
|
|
child: Container(
|
|
width: 40.w,
|
|
height: 40.w,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: isDark
|
|
? Colors.white.withOpacity(0.06)
|
|
: Colors.black.withOpacity(0.04),
|
|
),
|
|
child: Icon(
|
|
Icons.arrow_back_ios_new,
|
|
size: 18.sp,
|
|
color: isDark ? Colors.white : const Color(0xFF222222),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 16.w),
|
|
Text(
|
|
"deviceManagement".tr,
|
|
style: TextStyle(
|
|
fontSize: 26.sp,
|
|
fontWeight: FontWeight.w800,
|
|
color: isDark ? Colors.white : const Color(0xFF111111),
|
|
letterSpacing: -0.5,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 20.h),
|
|
// 摘要卡片
|
|
Obx(() {
|
|
final totalDevices = controller.devices.values
|
|
.fold<int>(0, (sum, list) => sum + list.length);
|
|
final connectedCount = controller.devices.values
|
|
.expand((list) => list)
|
|
.where((d) => _isConnected(d))
|
|
.length;
|
|
return Row(
|
|
children: [
|
|
_buildSummaryCard(
|
|
number: totalDevices.toString(),
|
|
label: "boundDevices".tr,
|
|
isActive: true,
|
|
isDark: isDark,
|
|
),
|
|
SizedBox(width: 12.w),
|
|
_buildSummaryCard(
|
|
number: connectedCount.toString(),
|
|
label: "currentConnection".tr,
|
|
isActive: false,
|
|
isDark: isDark,
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSummaryCard({
|
|
required String number,
|
|
required String label,
|
|
required bool isActive,
|
|
required bool isDark,
|
|
}) {
|
|
final activeGradient = isDark
|
|
? [
|
|
const Color(0xFF6366F1).withOpacity(0.15),
|
|
const Color(0xFF8B5CF6).withOpacity(0.1),
|
|
]
|
|
: [const Color(0xFFEEF2FF), const Color(0xFFF5F3FF)];
|
|
|
|
return Expanded(
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.h),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(16.r),
|
|
gradient: isActive ? LinearGradient(colors: activeGradient) : null,
|
|
color: isActive
|
|
? null
|
|
: (isDark
|
|
? Colors.white.withOpacity(0.04)
|
|
: Colors.white),
|
|
border: Border.all(
|
|
color: isActive
|
|
? (isDark
|
|
? const Color(0xFF6366F1).withOpacity(0.2)
|
|
: const Color(0xFF6366F1).withOpacity(0.15))
|
|
: (isDark
|
|
? Colors.white.withOpacity(0.06)
|
|
: Colors.black.withOpacity(0.04)),
|
|
),
|
|
boxShadow: isDark
|
|
? null
|
|
: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
number,
|
|
style: TextStyle(
|
|
fontSize: 24.sp,
|
|
fontWeight: FontWeight.w700,
|
|
color: isActive
|
|
? (isDark
|
|
? const Color(0xFFA5B4FC)
|
|
: const Color(0xFF6366F1))
|
|
: (isDark ? Colors.white : const Color(0xFF222222)),
|
|
),
|
|
),
|
|
SizedBox(height: 2.h),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 11.sp,
|
|
fontWeight: FontWeight.w500,
|
|
color: isActive
|
|
? (isDark
|
|
? const Color(0xFFA5B4FC).withOpacity(0.6)
|
|
: const Color(0xFF818CF8))
|
|
: (isDark
|
|
? Colors.white.withOpacity(0.35)
|
|
: const Color(0xFF999999)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDevicesList(bool isDark, Color topBg) {
|
|
return Obx(() {
|
|
final groupedDevices = controller.devices;
|
|
|
|
if (groupedDevices.isEmpty) {
|
|
return _buildEmptyState(isDark);
|
|
}
|
|
|
|
return ListView(
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
|
children: [
|
|
...groupedDevices.entries.map((entry) {
|
|
final deviceType = DeviceType.fromValue(entry.key);
|
|
final deviceList = entry.value;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 分类标题
|
|
Padding(
|
|
padding: EdgeInsets.only(left: 8.w, bottom: 12.h),
|
|
child: Text(
|
|
deviceType.displayName.toUpperCase(),
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: isDark
|
|
? Colors.white.withOpacity(0.25)
|
|
: const Color(0xFFAAAAAA),
|
|
letterSpacing: 1.5,
|
|
),
|
|
),
|
|
),
|
|
...deviceList.map((device) => _buildDeviceCard(
|
|
device: device,
|
|
deviceType: deviceType,
|
|
isDark: isDark,
|
|
)),
|
|
SizedBox(height: 16.h),
|
|
],
|
|
);
|
|
}),
|
|
],
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget _buildEmptyState(bool isDark) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.devices_other,
|
|
size: 80.sp,
|
|
color: isDark ? Colors.white54 : Colors.grey[400],
|
|
),
|
|
SizedBox(height: 20.h),
|
|
Text(
|
|
"noDevicesBound".tr,
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
color: isDark ? Colors.white70 : Colors.grey[600],
|
|
),
|
|
),
|
|
SizedBox(height: 12.h),
|
|
Text(
|
|
"pleaseBindDeviceFirst".tr,
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
color: isDark ? Colors.white54 : Colors.grey[500],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDeviceCard({
|
|
required UserDevice device,
|
|
required DeviceType deviceType,
|
|
required bool isDark,
|
|
}) {
|
|
final connected = _isConnected(device);
|
|
final connectedColor = const Color(0xFF22C55E);
|
|
|
|
return GestureDetector(
|
|
onTap: () => Get.toNamed(Routes.eqSettings),
|
|
child: Container(
|
|
margin: EdgeInsets.only(bottom: 10.h),
|
|
padding: EdgeInsets.all(20.w),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(20.r),
|
|
color: isDark
|
|
? (connected
|
|
? connectedColor.withOpacity(0.04)
|
|
: Colors.white.withOpacity(0.03))
|
|
: (connected
|
|
? connectedColor.withOpacity(0.03)
|
|
: Colors.white),
|
|
border: Border.all(
|
|
color: connected
|
|
? connectedColor.withOpacity(0.2)
|
|
: (isDark
|
|
? Colors.white.withOpacity(0.05)
|
|
: Colors.black.withOpacity(0.04)),
|
|
),
|
|
boxShadow: isDark
|
|
? null
|
|
: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.03),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// 上部:图标 + 名称/MAC + 解绑
|
|
Row(
|
|
children: [
|
|
// 设备图标
|
|
Container(
|
|
width: 46.w,
|
|
height: 46.w,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(14.r),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: connected
|
|
? [connectedColor, const Color(0xFF16A34A)]
|
|
: [const Color(0xFF6366F1), const Color(0xFF8B5CF6)],
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: (connected ? connectedColor : const Color(0xFF6366F1))
|
|
.withOpacity(0.3),
|
|
blurRadius: 16,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Icon(
|
|
deviceType.icon,
|
|
color: Colors.white,
|
|
size: 22.sp,
|
|
),
|
|
),
|
|
SizedBox(width: 14.w),
|
|
// 设备名 + MAC
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(
|
|
device.devicename.isNotEmpty
|
|
? device.devicename
|
|
: "deviceTypeUnknown".tr,
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w700,
|
|
color: isDark
|
|
? Colors.white
|
|
: const Color(0xFF1A1A2E),
|
|
),
|
|
),
|
|
if (connected) ...[
|
|
SizedBox(width: 8.w),
|
|
Container(
|
|
width: 6.w,
|
|
height: 6.w,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: connectedColor,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: connectedColor.withOpacity(0.6),
|
|
blurRadius: 8,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(width: 6.w),
|
|
Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 8.w, vertical: 3.h),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(6.r),
|
|
color: connectedColor.withOpacity(0.1),
|
|
),
|
|
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: 12.sp,
|
|
fontWeight: FontWeight.w500,
|
|
color: isDark
|
|
? Colors.white.withOpacity(0.3)
|
|
: const Color(0xFF999999),
|
|
fontFamily: 'Menlo',
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// 解绑按钮
|
|
GestureDetector(
|
|
onTap: () => _showUnbindDialog(device, isDark),
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 14.w, vertical: 8.h),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(10.r),
|
|
border: Border.all(
|
|
color: isDark
|
|
? Colors.white.withOpacity(0.08)
|
|
: Colors.black.withOpacity(0.08),
|
|
),
|
|
),
|
|
child: Text(
|
|
"unbind".tr,
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: isDark
|
|
? Colors.white.withOpacity(0.4)
|
|
: const Color(0xFF999999),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
// 下部:固件版本 / 授权码 / CMEI
|
|
Obx(() {
|
|
final bleManager = Get.find<BleManager>();
|
|
// 必须无条件读取 deviceInfo.value,否则未连接设备的 Obx 内没有任何
|
|
// observable,会触发 "improper use of a GetX" 错误
|
|
final info = bleManager.deviceInfo.value;
|
|
final fwVersion =
|
|
connected ? (info?.firmwareVersion ?? '') : '';
|
|
// 固件版本原始格式:左 3 段 + 右 3 段("0.0.1.0.0.1"),只展示左版本
|
|
final parts = fwVersion.split('.');
|
|
final fwDisplay =
|
|
parts.length >= 3 ? parts.take(3).join('.') : fwVersion;
|
|
final hasFirmware = fwVersion.isNotEmpty;
|
|
final hasLicense = device.license.isNotEmpty;
|
|
final hasCmei = device.cmei.isNotEmpty;
|
|
|
|
if (!hasFirmware && !hasLicense && !hasCmei) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
return Padding(
|
|
padding: EdgeInsets.only(top: 16.h),
|
|
child: Container(
|
|
padding: EdgeInsets.all(12.w),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
color: isDark
|
|
? Colors.white.withOpacity(0.03)
|
|
: const Color(0xFFF7F7FA),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
if (hasFirmware)
|
|
_buildFieldRow(
|
|
label: "firmwareVersion".tr,
|
|
value: fwDisplay,
|
|
isDark: isDark,
|
|
),
|
|
if (hasFirmware && (hasLicense || hasCmei))
|
|
SizedBox(height: 8.h),
|
|
if (hasLicense)
|
|
_buildFieldRow(
|
|
label: "licenseCode".tr,
|
|
value: device.license,
|
|
isDark: isDark,
|
|
),
|
|
if (hasLicense && hasCmei) SizedBox(height: 8.h),
|
|
if (hasCmei)
|
|
_buildFieldRow(
|
|
label: "CMEI",
|
|
value: device.cmei,
|
|
isDark: isDark,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFieldRow({
|
|
required String label,
|
|
required String value,
|
|
required bool isDark,
|
|
}) {
|
|
return Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 72.w,
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 10.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: isDark
|
|
? Colors.white.withOpacity(0.2)
|
|
: 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.withOpacity(0.5)
|
|
: const Color(0xFF666666),
|
|
fontFamily: 'Menlo',
|
|
letterSpacing: 0.3,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
void _showUnbindDialog(UserDevice device, bool isDark) {
|
|
Get.dialog(
|
|
AlertDialog(
|
|
backgroundColor: isDark ? AppColors.cardBackground : 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.unbindearphone(device);
|
|
},
|
|
child: Text(
|
|
"confirmUnbindAction".tr,
|
|
style: const TextStyle(color: Colors.red),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|