13 changed files with 1031 additions and 34 deletions
@ -0,0 +1,154 @@ |
|||||
|
import 'dart:io'; |
||||
|
|
||||
|
import 'package:device_jieli/device_jieli.dart'; |
||||
|
|
||||
|
import '../../core/utils/logger.dart'; |
||||
|
|
||||
|
/// 杰理设备广播包解析结果:从扫描到的 [JieliDevice.rawAdv] 中提取出 |
||||
|
/// 服务端绑定接口需要的 PID / MAC / 设备授权码 (code)。 |
||||
|
class JieliBindInfo { |
||||
|
/// 产品 ID(小端字节解析) |
||||
|
final int pid; |
||||
|
|
||||
|
/// 设备真实 MAC(XOR 0x5A 解码后),格式 "AA:BB:CC:DD:EE:FF" |
||||
|
final String mac; |
||||
|
|
||||
|
/// 设备授权码 (license / SN),hex 大写 |
||||
|
final String code; |
||||
|
|
||||
|
/// 解析得到的 PID 是否与扫描结果上的 BLE/EDR 地址一致 |
||||
|
/// (仅用于日志,不强制要求一致) |
||||
|
final bool macMatchesAddress; |
||||
|
|
||||
|
const JieliBindInfo({ |
||||
|
required this.pid, |
||||
|
required this.mac, |
||||
|
required this.code, |
||||
|
required this.macMatchesAddress, |
||||
|
}); |
||||
|
|
||||
|
@override |
||||
|
String toString() => |
||||
|
'JieliBindInfo(pid=0x${pid.toRadixString(16).toUpperCase()}, mac=$mac, code=$code, match=$macMatchesAddress)'; |
||||
|
} |
||||
|
|
||||
|
/// 杰理 BLE 广播解析。 |
||||
|
/// |
||||
|
/// 字节布局(与旧版一致,基于扫到的 raw scanRecord 字节): |
||||
|
/// Android: |
||||
|
/// - bytes[6..7] → PID |
||||
|
/// - bytes[9..14] → MAC(每字节 XOR 0x5A) |
||||
|
/// - bytes[15..24] → 授权码 code |
||||
|
/// iOS: |
||||
|
/// - bytes[4..5] → PID |
||||
|
/// - bytes[7..12] → MAC(每字节 XOR 0x5A) |
||||
|
/// - bytes[13..22] → 授权码 code |
||||
|
class JieliAdvParser { |
||||
|
static const String _tag = 'JieliAdvParser'; |
||||
|
|
||||
|
/// 用 [device.rawAdv] 直接解析。若 raw 数据不完整或 device 未携带广播包,返回 null。 |
||||
|
static JieliBindInfo? parse(JieliDevice device) { |
||||
|
final raw = device.rawAdv; |
||||
|
if (raw == null || raw.isEmpty) { |
||||
|
Logger.w(_tag, '设备 ${device.address} 没有 rawAdv,无法解析绑定信息'); |
||||
|
return null; |
||||
|
} |
||||
|
return parseHex(raw, deviceAddress: device.address); |
||||
|
} |
||||
|
|
||||
|
/// 直接对 hex 字符串解析,便于测试。 |
||||
|
static JieliBindInfo? parseHex(String hex, {String? deviceAddress}) { |
||||
|
final bytes = _hexToBytes(hex); |
||||
|
if (bytes == null) { |
||||
|
Logger.w(_tag, 'rawAdv 不是合法 hex: $hex'); |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
final int pidStart; |
||||
|
final int pidEnd; |
||||
|
final int macStart; |
||||
|
final int macEnd; |
||||
|
final int codeStart; |
||||
|
final int codeEnd; |
||||
|
|
||||
|
if (Platform.isIOS) { |
||||
|
pidStart = 4; |
||||
|
pidEnd = 5; |
||||
|
macStart = 7; |
||||
|
macEnd = 12; |
||||
|
codeStart = 13; |
||||
|
codeEnd = 22; |
||||
|
} else { |
||||
|
// Android 默认;其他平台沿用 Android 偏移。 |
||||
|
pidStart = 6; |
||||
|
pidEnd = 7; |
||||
|
macStart = 9; |
||||
|
macEnd = 14; |
||||
|
codeStart = 15; |
||||
|
codeEnd = 24; |
||||
|
} |
||||
|
|
||||
|
if (bytes.length <= codeEnd) { |
||||
|
Logger.w(_tag, |
||||
|
'rawAdv 长度不足 ${codeEnd + 1} 字节(实际 ${bytes.length}),跳过解析'); |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
final pid = _bytesToIntBigEndian(bytes, pidStart, pidEnd); |
||||
|
final mac = _decodeMac(bytes, macStart, macEnd); |
||||
|
final code = _bytesToHexReversed(bytes, codeStart, codeEnd); |
||||
|
|
||||
|
bool match = false; |
||||
|
if (deviceAddress != null && deviceAddress.isNotEmpty) { |
||||
|
final a = deviceAddress.replaceAll(':', '').toUpperCase(); |
||||
|
final b = mac.replaceAll(':', '').toUpperCase(); |
||||
|
match = a == b; |
||||
|
} |
||||
|
|
||||
|
final info = JieliBindInfo( |
||||
|
pid: pid, |
||||
|
mac: mac, |
||||
|
code: code, |
||||
|
macMatchesAddress: match, |
||||
|
); |
||||
|
Logger.i(_tag, '解析广播: $info (deviceAddress=$deviceAddress)'); |
||||
|
return info; |
||||
|
} |
||||
|
|
||||
|
static List<int>? _hexToBytes(String hex) { |
||||
|
final s = hex.startsWith('0x') || hex.startsWith('0X') ? hex.substring(2) : hex; |
||||
|
if (s.length.isOdd) return null; |
||||
|
final out = <int>[]; |
||||
|
for (int i = 0; i < s.length; i += 2) { |
||||
|
final v = int.tryParse(s.substring(i, i + 2), radix: 16); |
||||
|
if (v == null) return null; |
||||
|
out.add(v); |
||||
|
} |
||||
|
return out; |
||||
|
} |
||||
|
|
||||
|
static int _bytesToIntBigEndian(List<int> bytes, int start, int end) { |
||||
|
int result = 0; |
||||
|
for (int i = start; i <= end; i++) { |
||||
|
result = (result << 8) | (bytes[i] & 0xFF); |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
static String _bytesToHexReversed(List<int> bytes, int start, int end) { |
||||
|
final slice = bytes.sublist(start, end + 1); |
||||
|
return slice |
||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0')) |
||||
|
.join() |
||||
|
.toUpperCase(); |
||||
|
} |
||||
|
|
||||
|
static String _decodeMac(List<int> bytes, int start, int end) { |
||||
|
final out = <String>[]; |
||||
|
for (int i = start; i <= end; i++) { |
||||
|
final decoded = bytes[i] ^ 0x5A; |
||||
|
out.add(decoded.toRadixString(16).padLeft(2, '0').toUpperCase()); |
||||
|
} |
||||
|
return out.join(':'); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
import '../controllers/my_devices_controller.dart'; |
||||
|
|
||||
|
class MyDevicesBinding extends Bindings { |
||||
|
@override |
||||
|
void dependencies() { |
||||
|
Get.lazyPut<MyDevicesController>(() => MyDevicesController()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
import '/core/utils/logger.dart'; |
||||
|
import '/data/models/user_Info.dart'; |
||||
|
import '/data/services/ble_manager.dart'; |
||||
|
import '/data/services/network/api.dart'; |
||||
|
import '/routes/app_routes.dart'; |
||||
|
|
||||
|
/// 用户已绑定设备管理页:拉取用户绑定列表、解绑。 |
||||
|
class MyDevicesController extends GetxController { |
||||
|
static const String _tag = 'MyDevicesController'; |
||||
|
|
||||
|
late final BleManager _bleManager; |
||||
|
|
||||
|
/// 按设备类型分组的已绑定设备 |
||||
|
final RxMap<int, List<UserDevice>> devicesByType = |
||||
|
<int, List<UserDevice>>{}.obs; |
||||
|
|
||||
|
final RxBool isLoading = false.obs; |
||||
|
|
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
_bleManager = Get.find<BleManager>(); |
||||
|
_refreshFromUser(); |
||||
|
Future.microtask(loadDevices); |
||||
|
} |
||||
|
|
||||
|
/// 从服务端拉取最新的设备列表,并刷新 [User.instance.devices]。 |
||||
|
Future<void> loadDevices() async { |
||||
|
if (!User.isLoggedIn()) return; |
||||
|
isLoading.value = true; |
||||
|
try { |
||||
|
final response = await Api.getdevices(); |
||||
|
if (response != null) { |
||||
|
final list = response['devices'] as List<dynamic>?; |
||||
|
if (list != null) { |
||||
|
User.instance.devices = list |
||||
|
.map((e) => UserDevice.fromJson(e as Map<String, dynamic>)) |
||||
|
.toList(); |
||||
|
} else { |
||||
|
User.instance.devices = []; |
||||
|
} |
||||
|
Logger.i(_tag, '设备列表刷新完成: ${User.instance.devices.length} 条'); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
Logger.e(_tag, '获取设备列表失败: $e'); |
||||
|
} finally { |
||||
|
_refreshFromUser(); |
||||
|
isLoading.value = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 解绑设备:调接口成功后从本地列表移除;如果是当前连接的设备,断开并跳到扫描页。 |
||||
|
Future<void> unbindDevice(UserDevice device) async { |
||||
|
try { |
||||
|
final response = await Api.unbinddevice({'id': device.id}); |
||||
|
Logger.i(_tag, '解绑接口返回: $response'); |
||||
|
if (response == null) return; |
||||
|
|
||||
|
User.instance.removeUserDevice(device.id); |
||||
|
_refreshFromUser(); |
||||
|
|
||||
|
if (device.devicemac.isNotEmpty && |
||||
|
_bleManager.currentDeviceAddress == device.devicemac) { |
||||
|
Logger.i(_tag, '解绑的设备就是当前连接,断开并回到扫描页'); |
||||
|
await _bleManager.disconnect(); |
||||
|
await Future.delayed(const Duration(milliseconds: 300)); |
||||
|
Get.offAllNamed(Routes.devices); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
Logger.e(_tag, '解绑异常: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void _refreshFromUser() { |
||||
|
final grouped = <int, List<UserDevice>>{}; |
||||
|
if (User.isLoggedIn()) { |
||||
|
for (final d in User.instance.devices) { |
||||
|
grouped.putIfAbsent(d.devicetype, () => <UserDevice>[]).add(d); |
||||
|
} |
||||
|
} |
||||
|
devicesByType.value = grouped; |
||||
|
devicesByType.refresh(); |
||||
|
} |
||||
|
|
||||
|
bool isConnected(UserDevice device) { |
||||
|
if (device.devicemac.isEmpty) return false; |
||||
|
return _bleManager.isConnected && |
||||
|
_bleManager.currentDeviceAddress == device.devicemac; |
||||
|
} |
||||
|
|
||||
|
void goToScan() { |
||||
|
Get.toNamed(Routes.devices); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,581 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
import '/data/models/user_Info.dart'; |
||||
|
import '../controllers/my_devices_controller.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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class MyDevicesView extends GetView<MyDevicesController> { |
||||
|
const MyDevicesView({super.key}); |
||||
|
|
||||
|
@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), |
||||
|
Expanded(child: _buildDevicesList(isDark, topBg)), |
||||
|
_buildAddDeviceButton(isDark), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildTopArea(BuildContext context, bool isDark, Color bg) { |
||||
|
return Container( |
||||
|
padding: EdgeInsets.only( |
||||
|
top: MediaQuery.of(context).padding.top + 8.h, |
||||
|
left: 20.w, |
||||
|
right: 20.w, |
||||
|
bottom: 20.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.withValues(alpha: 0.06) |
||||
|
: Colors.black.withValues(alpha: 0.04), |
||||
|
), |
||||
|
child: Icon( |
||||
|
Icons.arrow_back_ios_new, |
||||
|
size: 18.sp, |
||||
|
color: isDark ? Colors.white : const Color(0xFF222222), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
SizedBox(width: 16.w), |
||||
|
Expanded( |
||||
|
child: Text( |
||||
|
'deviceManagement'.tr, |
||||
|
style: TextStyle( |
||||
|
fontSize: 24.sp, |
||||
|
fontWeight: FontWeight.w800, |
||||
|
color: isDark ? Colors.white : const Color(0xFF111111), |
||||
|
letterSpacing: -0.5, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
Obx(() => controller.isLoading.value |
||||
|
? SizedBox( |
||||
|
width: 18.w, |
||||
|
height: 18.w, |
||||
|
child: const CircularProgressIndicator(strokeWidth: 2), |
||||
|
) |
||||
|
: IconButton( |
||||
|
onPressed: controller.loadDevices, |
||||
|
icon: Icon( |
||||
|
Icons.refresh, |
||||
|
size: 20.sp, |
||||
|
color: isDark ? Colors.white70 : Colors.black54, |
||||
|
), |
||||
|
)), |
||||
|
], |
||||
|
), |
||||
|
SizedBox(height: 16.h), |
||||
|
Obx(() { |
||||
|
final total = controller.devicesByType.values |
||||
|
.fold<int>(0, (sum, list) => sum + list.length); |
||||
|
final connected = controller.devicesByType.values |
||||
|
.expand((list) => list) |
||||
|
.where(controller.isConnected) |
||||
|
.length; |
||||
|
return Row( |
||||
|
children: [ |
||||
|
_buildSummaryCard( |
||||
|
number: '$total', |
||||
|
label: 'boundDevices'.tr, |
||||
|
isActive: true, |
||||
|
isDark: isDark), |
||||
|
SizedBox(width: 12.w), |
||||
|
_buildSummaryCard( |
||||
|
number: '$connected', |
||||
|
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).withValues(alpha: 0.15), |
||||
|
const Color(0xFF8B5CF6).withValues(alpha: 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.withValues(alpha: 0.04) |
||||
|
: Colors.white), |
||||
|
border: Border.all( |
||||
|
color: isActive |
||||
|
? const Color(0xFF6366F1).withValues(alpha: 0.18) |
||||
|
: (isDark |
||||
|
? Colors.white.withValues(alpha: 0.06) |
||||
|
: Colors.black.withValues(alpha: 0.04)), |
||||
|
), |
||||
|
), |
||||
|
child: Column( |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
children: [ |
||||
|
Text( |
||||
|
number, |
||||
|
style: TextStyle( |
||||
|
fontSize: 22.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: isDark |
||||
|
? Colors.white.withValues(alpha: 0.5) |
||||
|
: const Color(0xFF999999), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildDevicesList(bool isDark, Color topBg) { |
||||
|
return Obx(() { |
||||
|
final groups = controller.devicesByType; |
||||
|
if (groups.isEmpty) { |
||||
|
return _buildEmptyState(isDark); |
||||
|
} |
||||
|
return ListView( |
||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), |
||||
|
children: groups.entries.map((entry) { |
||||
|
final category = _MyDeviceCategory.fromValue(entry.key); |
||||
|
final list = entry.value; |
||||
|
return Column( |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
children: [ |
||||
|
Padding( |
||||
|
padding: EdgeInsets.only(left: 8.w, bottom: 10.h), |
||||
|
child: Text( |
||||
|
category.displayName.toUpperCase(), |
||||
|
style: TextStyle( |
||||
|
fontSize: 12.sp, |
||||
|
fontWeight: FontWeight.w600, |
||||
|
color: isDark |
||||
|
? Colors.white.withValues(alpha: 0.25) |
||||
|
: const Color(0xFFAAAAAA), |
||||
|
letterSpacing: 1.2, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
...list.map((d) => _buildDeviceCard(d, category, isDark)), |
||||
|
SizedBox(height: 14.h), |
||||
|
], |
||||
|
); |
||||
|
}).toList(), |
||||
|
); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
Widget _buildEmptyState(bool isDark) { |
||||
|
return Center( |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.symmetric(horizontal: 32.w), |
||||
|
child: Column( |
||||
|
mainAxisAlignment: MainAxisAlignment.center, |
||||
|
children: [ |
||||
|
Icon( |
||||
|
Icons.devices_other, |
||||
|
size: 72.sp, |
||||
|
color: isDark ? Colors.white54 : Colors.grey[400], |
||||
|
), |
||||
|
SizedBox(height: 16.h), |
||||
|
Text( |
||||
|
'noDevicesBound'.tr, |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
color: isDark ? Colors.white70 : Colors.grey[700], |
||||
|
), |
||||
|
), |
||||
|
SizedBox(height: 8.h), |
||||
|
Text( |
||||
|
'pleaseBindDeviceFirst'.tr, |
||||
|
textAlign: TextAlign.center, |
||||
|
style: TextStyle( |
||||
|
fontSize: 13.sp, |
||||
|
color: isDark ? Colors.white54 : Colors.grey[500], |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildDeviceCard( |
||||
|
UserDevice device, _MyDeviceCategory category, bool isDark) { |
||||
|
final connected = controller.isConnected(device); |
||||
|
const connectedColor = Color(0xFF22C55E); |
||||
|
|
||||
|
return 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: [ |
||||
|
Container( |
||||
|
width: 44.w, |
||||
|
height: 44.w, |
||||
|
decoration: BoxDecoration( |
||||
|
borderRadius: BorderRadius.circular(12.r), |
||||
|
gradient: LinearGradient( |
||||
|
begin: Alignment.topLeft, |
||||
|
end: Alignment.bottomRight, |
||||
|
colors: connected |
||||
|
? [connectedColor, const Color(0xFF16A34A)] |
||||
|
: [const Color(0xFF6366F1), const Color(0xFF8B5CF6)], |
||||
|
), |
||||
|
), |
||||
|
child: Icon( |
||||
|
category.icon, |
||||
|
color: Colors.white, |
||||
|
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, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
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.license.isNotEmpty || 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.license.isNotEmpty) |
||||
|
_buildFieldRow( |
||||
|
label: 'licenseCode'.tr, |
||||
|
value: device.license, |
||||
|
isDark: isDark), |
||||
|
if (device.license.isNotEmpty && 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, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildAddDeviceButton(bool isDark) { |
||||
|
return Padding( |
||||
|
padding: EdgeInsets.fromLTRB(20.w, 8.h, 20.w, 20.h), |
||||
|
child: SizedBox( |
||||
|
width: double.infinity, |
||||
|
height: 48.h, |
||||
|
child: ElevatedButton.icon( |
||||
|
onPressed: controller.goToScan, |
||||
|
icon: Icon(Icons.add, size: 18.sp), |
||||
|
label: Text( |
||||
|
'addDevice'.tr, |
||||
|
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w600), |
||||
|
), |
||||
|
style: ElevatedButton.styleFrom( |
||||
|
backgroundColor: const Color(0xFF0066CC), |
||||
|
foregroundColor: Colors.white, |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.circular(14.r), |
||||
|
), |
||||
|
elevation: 0, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
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