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.

648 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";按 6 段拆左右
final parts = fwVersion.split('.');
final hasLeftRight = parts.length >= 6;
final fwDisplay = hasLeftRight
? 'L ${parts.take(3).join('.')} · R ${parts.skip(3).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),
),
),
],
),
);
}
}