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