From c936b90238e391354ae5b88f8fb7d165abd3f48b Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 22 Jul 2025 17:29:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=B0=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Podfile.lock | 15 +- lib/data/models/user_Info.dart | 112 ++-- lib/data/services/network/api.dart | 4 +- .../controllers/devices_controller.dart | 43 +- lib/modules/devices/views/devices_view.dart | 524 ++++++++++-------- .../controllers/pairing_controller.dart | 76 +-- .../splash/controllers/splash_controller.dart | 1 + .../chat_api/ios/chat_api/Package.swift | 6 +- .../chat_api/SystemFunctionHandler.swift | 258 +++------ local_plugins/location_service/ios/.gitignore | 38 -- .../ios/Classes/LocationService.swift | 131 ----- .../ios/Classes/LocationServicePlugin.swift | 44 -- .../ios/location_service.podspec | 27 - .../ios/location_service/Package.swift | 21 + .../location_service/LocationCallback.swift | 7 + .../location_service/LocationData.swift | 16 + .../location_service/LocationService.swift | 357 ++++++++++++ .../LocationServicePlugin.swift | 72 +++ local_plugins/location_service/pubspec.yaml | 3 +- 19 files changed, 976 insertions(+), 779 deletions(-) delete mode 100644 local_plugins/location_service/ios/.gitignore delete mode 100644 local_plugins/location_service/ios/Classes/LocationService.swift delete mode 100644 local_plugins/location_service/ios/Classes/LocationServicePlugin.swift delete mode 100644 local_plugins/location_service/ios/location_service.podspec create mode 100644 local_plugins/location_service/ios/location_service/Package.swift create mode 100644 local_plugins/location_service/ios/location_service/Sources/location_service/LocationCallback.swift create mode 100644 local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift create mode 100644 local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift create mode 100644 local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9a4298032..ad056d922 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,8 +3,6 @@ PODS: - AMapNavi (~> 10.0.900) - Flutter - AMapFoundation (1.8.2) - - AMapLocation (2.10.0): - - AMapFoundation (>= 1.8.0) - AMapNavi (10.0.900): - AMapFoundation (>= 1.8.2) - audio_waveforms (0.0.1): @@ -56,9 +54,6 @@ PODS: - libwebp/sharpyuv (1.5.0) - libwebp/webp (1.5.0): - libwebp/sharpyuv - - location_service (0.0.1): - - AMapLocation (~> 2.10.0) - - Flutter - Mantle (2.2.0): - Mantle/extobjc (= 2.2.0) - Mantle/extobjc (2.2.0) @@ -85,7 +80,7 @@ PODS: - SDWebImage/Core (~> 5.17) - spotify_sdk (0.0.1): - Flutter - - tencentcloud_cos_sdk_plugin (1.2.3): + - tencentcloud_cos_sdk_plugin (1.2.4): - Flutter - QCloudCOSXML (= 6.4.7) - WechatOpenSDK-XCFramework (2.0.4) @@ -105,7 +100,6 @@ DEPENDENCIES: - fluwx (from `.symlinks/plugins/fluwx/ios`) - haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`) - keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`) - - location_service (from `.symlinks/plugins/location_service/ios`) - ota (from `.symlinks/plugins/ota/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - record_darwin (from `.symlinks/plugins/record_darwin/ios`) @@ -116,7 +110,6 @@ DEPENDENCIES: SPEC REPOS: trunk: - AMapFoundation - - AMapLocation - AMapNavi - FBAEMKit - FBSDKCoreKit @@ -156,8 +149,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/haptic_feedback/ios" keyboard_height_plugin: :path: ".symlinks/plugins/keyboard_height_plugin/ios" - location_service: - :path: ".symlinks/plugins/location_service/ios" ota: :path: ".symlinks/plugins/ota/ios" permission_handler_apple: @@ -174,7 +165,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: amap_service: 1c3e7e07f51cbbdff03719a4a079c60d015c1c0b AMapFoundation: 9885c48fc3a78fdfb84a0299a2293e56ea3c9fec - AMapLocation: 5248aec2455ebb5d104b367813c946430a2ee033 AMapNavi: fceb4715f9a3b5a25f330facebee2aa2bdba42dd audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf classic_bluetooth: 29f392f1ab7c52862a9ef036c94f2713b02e0c50 @@ -192,7 +182,6 @@ SPEC CHECKSUMS: haptic_feedback: fd1d8509833f58f164fc12122c0357fa9b2750e0 keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 - location_service: fab4ef23d473296a215c49e12c129e466574d94e Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d ota: 20c3e522c57e7ba70ce118109e73650acc9fec76 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d @@ -203,7 +192,7 @@ SPEC CHECKSUMS: SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 spotify_sdk: a48400bb29f70c4fe251ebfdc9135c37097ac5ca - tencentcloud_cos_sdk_plugin: c3d78b2c3bae42b5a20009dc047f3f1621e6272b + tencentcloud_cos_sdk_plugin: 7bed564dbe72df23e7b5cacb1e51b1a20cb1639c WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f wifi_iot: f645260a2be8608517b2a9bf4c39b98e97003acc diff --git a/lib/data/models/user_Info.dart b/lib/data/models/user_Info.dart index 3f3f9fa86..04fa89610 100644 --- a/lib/data/models/user_Info.dart +++ b/lib/data/models/user_Info.dart @@ -1,3 +1,5 @@ +import 'dart:ffi'; + class User { // 单例实例 static User? _instance; @@ -21,15 +23,7 @@ class User { String phonemac; String password; int viplv = 0; //添加会员等级字段,0表示普通用户 - String earphonename; - String earphonemac; - String earphonelicense; - String earphoneauthcode; - // final Int64 lastbettime; - // final Int64 createtime; - // final String wxopenid; - // final String googleopenid; - // final String facebookopenid; + List devices; // 私有构造函数 User._({ @@ -42,21 +36,14 @@ class User { required this.phone, required this.phonemac, required this.password, - required this.viplv, // 添加默认值 - required this.earphonename, - required this.earphonemac, - required this.earphonelicense, - required this.earphoneauthcode, - // required this.lastbettime, - // required this.createtime, - // required this.wxopenid, - // required this.googleopenid, - // required this.facebookopenid, + required this.viplv, + required this.devices, }); factory User.fromJson(Map json) { // 修复1:安全获取嵌套的 user 字段 final userData = json['user'] as Map?; // 允许空值 + final devicesData = json['devices'] as Map?; // 允许空值 //final token = json['data']['token'] as String? ?? ''; //final token = ''; // 更新单例实例 @@ -74,21 +61,17 @@ class User { phonemac: (userData?['phonemac'] as String?) ?? '', password: (userData?['password'] as String?) ?? '', viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值 - earphonename: (userData?['earphonename'] as String?) ?? '', - earphonemac: (userData?['earphonemac'] as String?) ?? '', - earphonelicense: (userData?['earphonelicense'] as String?) ?? '', - earphoneauthcode: (userData?['earphoneauthcode'] as String?) ?? '', -// lastbettime: _parseTimestamp(userData['lastbettime']), -// createtime: _parseTimestamp(userData['createtime']), -// wxopenid: (userData?['wxopenid'] as String?) ?? '', -// googleopenid: (userData?['googleopenid'] as String?) ?? '', -// facebookopenid: (userData?['facebookopenid'] as String?) ?? '', + devices: (devicesData as List?) + ?.map((e) => UserDevice.fromJson(e as Map)) + .toList() ?? + [], // 处理设备列表 ); } factory User.fronTokenJson(Map json) { // 修复1:安全获取嵌套的 user 字段 final userData = json['user'] as Map?; // 允许空值 - print("数据获取fronTokenJson${userData}"); + final devicesData = json['devices'] as Map?; // 允许空值 + print("数据获取fronTokenJson${userData} ${devicesData}"); // 更新单例实例 return _instance = User._( token: '', @@ -101,17 +84,12 @@ class User { phone: (userData?['phone'] as String?) ?? '', phonemac: (userData?['phonemac'] as String?) ?? '', password: (userData?['password'] as String?) ?? '', - viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值 - earphonename: (userData?['earphonename'] as String?) ?? '', - earphonemac: (userData?['earphonemac'] as String?) ?? '', - earphonelicense: (userData?['earphonelicense'] as String?) ?? '', - earphoneauthcode: (userData?['earphoneauthcode'] as String?) ?? '', -// lastbettime: _parseTimestamp(userData['lastbettime']), -// createtime: _parseTimestamp(userData['createtime']), -// wxopenid: (userData?['wxopenid'] as String?) ?? '', -// googleopenid: (userData?['googleopenid'] as String?) ?? '', -// facebookopenid: (userData?['facebookopenid'] as String?) ?? '', + devices: devicesData != null + ? (devicesData as List) + .map((e) => UserDevice.fromJson(e as Map)) + .toList() + : [], // 处理设备列表 ); } @@ -121,7 +99,6 @@ class User { 'name': name, 'uid': uid, 'avatar': avatar, - 'language': language, 'token': token, 'mail': mail, @@ -129,17 +106,52 @@ class User { 'phonemac': phonemac, 'password': password, 'viplv': viplv, + 'devices': devices.map((device) => device.toJson()).toList(), + }, + }; + + // 添加设备 + addUserDevice(UserDevice device) { + devices.add(device); + } + + // 查找设备 + removeUserDevice(int id) { + devices.removeWhere((device) => device.id == id); + } +} + +class UserDevice { + int id; + String uid; + int devicetype; + String devicename; + String devicemac; + String license; + UserDevice({ + required this.id, + required this.uid, + required this.devicetype, + required this.devicename, + required this.devicemac, + required this.license, + }); - 'earphonename': earphonename, - 'earphonemac': earphonemac, - "earphonelicense": earphonelicense, - 'earphoneauthcode': earphoneauthcode, + factory UserDevice.fromJson(Map json) => UserDevice( + id: json['id'] as int, + uid: json['uid'] as String, + devicetype: (json['devicetype'] as num).toInt(), + devicename: json['devicename'] as String, + devicemac: json['devicemac'] as String, + license: json['license'] as String, + ); - // 'lastbettime': lastbettime, - // 'createtime': createtime, - // 'wxopenid': wxopenid, - // 'googleopenid': googleopenid, - // 'facebookopenid': facebookopenid, - } + toJson() => { + 'id': id, + 'uid': uid, + 'devicetype': devicetype, + 'devicename': devicename, + 'devicemac': devicemac, + 'license': license, }; } diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index c44f1b391..0301daa87 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -137,10 +137,10 @@ class Api { } //绑定设备 - static bindearphone([params]) { + static binddevice([params]) { return DioManager().request( NWMethod.post, - '/api/home/user_bindearphone', + '/api/home/user_binddevice', params: params, ); } diff --git a/lib/modules/devices/controllers/devices_controller.dart b/lib/modules/devices/controllers/devices_controller.dart index 1104cdb29..6121ed7f8 100644 --- a/lib/modules/devices/controllers/devices_controller.dart +++ b/lib/modules/devices/controllers/devices_controller.dart @@ -1,3 +1,5 @@ +import 'dart:ffi'; + import '/core/utils/logger.dart'; import '/data/models/user_Info.dart'; import '/data/services/ble_manager.dart'; @@ -12,50 +14,33 @@ import 'package:get/get_state_manager/src/simple/get_controllers.dart'; */ class DevicesController extends GetxController { late final BleManager _bleManager; - //耳机绑定 - final earphonestate = 0.obs; - final earphoneName = "".obs; - final earphoneMac = "".obs; - final earphoneLicense = "".obs; - final earphoneCode = "".obs; - - //录音笔 - final voicepenstate = 0.obs; - final isvoicepen = false.obs; - final voicepenName = "".obs; - final voicepenMac = "".obs; + late final Map> devices; @override void onInit() { super.onInit(); _bleManager = Get.find(); - earphoneName.value = User.instance.earphonename; - earphoneMac.value = User.instance.earphonemac; - earphoneLicense.value = User.instance.earphonelicense; - earphoneCode.value = User.instance.earphoneauthcode; - if (earphoneCode.value != "") { - earphonestate.value = 1; - } - if (earphoneLicense.value != "") { - earphonestate.value = 2; - } } @override void onClose() { super.onClose(); + for (var device in User.instance.devices) { + if (!devices.containsKey(device.id)) { + devices[device.devicetype] = []; + } else { + devices[device.devicetype]!.add(device); + } + } } - void unbindearphone() async { + void unbindearphone(UserDevice device) async { final response = await Api.unbindearphone({ - "code": earphoneLicense.value, - "earphonname": earphoneMac.value, + "id": device.id, }); if (response != null) { - User.instance.earphonename = ""; - User.instance.earphonemac = ""; - User.instance.earphonelicense = ""; - earphonestate.value = 0; + devices[device.devicetype]!.remove(device); + User.instance.removeUserDevice(device.id); _bleManager.clearPairedDevices(); _bleManager.disconnect(); Get.toNamed(Routes.pairing); diff --git a/lib/modules/devices/views/devices_view.dart b/lib/modules/devices/views/devices_view.dart index 6b30d165b..0d5c20779 100644 --- a/lib/modules/devices/views/devices_view.dart +++ b/lib/modules/devices/views/devices_view.dart @@ -4,286 +4,328 @@ import '/modules/devices/controllers/devices_controller.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 '../controllers/devices_controller.dart'; + +// 设备类型枚举 +enum DeviceType { + Unknown(0, "未知设备"), + Classic_bluetooth_Headset(1, "经典蓝牙耳机"), + BLE_Bluetooth_Headset(2, "BLE蓝牙耳机"), + Smart_Voice_Recorder(3, "智能录音笔"), + SmartEye(4, "智能眼镜"); + + const DeviceType(this.value, this.displayName); + final int value; + final String displayName; + + 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 { - const DevicesView({Key? key}) : super(key: key); + const DevicesView({super.key}); @override Widget build(BuildContext context) { - // 直接使用 Theme.of(context) 获取当前主题 final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - appBar: AppBar( - automaticallyImplyLeading: false, - centerTitle: true, - elevation: 0, - backgroundColor: isDarkMode ? Colors.black : Colors.white, - leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - size: 20.sp, - color: isDarkMode ? Colors.white : Colors.black, + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.white, + body: Column( + children: [ + // 顶部标题栏 + Container( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 10.h, + left: 20.w, + right: 20.w, + bottom: 20.h, + ), + decoration: BoxDecoration( + color: isDarkMode ? Colors.black : Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 24.sp, + ), + ), + SizedBox(width: 16.w), + Text( + "deviceManagement".tr, + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), ), - onPressed: () { - Get.back(); // 正常返回 - }, - ), - title: Text( - 'deviceManagement'.tr, - style: TextStyle( - fontSize: 20.sp, - color: isDarkMode ? Colors.white : Colors.black, + // 设备列表内容 + Expanded( + child: _buildDevicesList(isDarkMode), ), - ), - ), - body: ListView( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), - children: [ - _buildEarphoneCard(isDarkMode), - SizedBox(height: 20.h), - _buildVoiceRecorderCard(isDarkMode), - SizedBox(height: 20.h), ], ), ); } - // 耳机 - Widget _buildEarphoneCard(bool isDarkMode) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + Widget _buildDevicesList(bool isDarkMode) { + final devices = User.instance.devices; + + if (devices.isEmpty) { + return _buildEmptyState(isDarkMode); + } + + // 按设备类型分组 + final Map> groupedDevices = {}; + for (var device in devices) { + final deviceType = DeviceType.fromValue(device.devicetype); + if (!groupedDevices.containsKey(deviceType)) { + groupedDevices[deviceType] = []; + } + groupedDevices[deviceType]!.add(device); + } + + return ListView( + padding: EdgeInsets.all(20.w), children: [ - Padding( - padding: EdgeInsets.only(left: 4.w, bottom: 8.h), - child: Text( - 'earphone'.tr, // 耳机 - style: TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.blue[400] : Colors.blue[600], - letterSpacing: 1, - ), - ), - ), - Container( - decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, - borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), + // 遍历每个设备类型 + ...groupedDevices.entries.map((entry) { + final deviceType = entry.key; + final deviceList = entry.value; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 设备类型标题 + Padding( + padding: EdgeInsets.only(bottom: 12.h), + child: Text( + deviceType.displayName, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), ), + // 该类型下的设备列表 + ...deviceList.map((device) => _buildDeviceCard( + device: device, + deviceType: deviceType, + isDarkMode: isDarkMode, + )), + SizedBox(height: 20.h), ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(16.r), - child: BackdropFilter( - filter: isDarkMode - ? ImageFilter.blur(sigmaX: 5, sigmaY: 5) - : ImageFilter.blur(sigmaX: 0, sigmaY: 0), - child: Column( - children: [ - Obx( - () => - // 耳机列表 - _buildSimpleNavigationSetting( - title: controller.earphonestate.value != 0 - ? controller.earphoneName.value - : "notBound".tr, // 未绑定 - subtitle: controller.earphonestate.value != 0 - ? controller.earphoneMac.value - : "deviceNotBoundYet".tr, // 还未绑定设备 - icon: Icons.headphones, - iconBgColor: isDarkMode - ? const Color.fromARGB(255, 255, 255, 255)! - .withOpacity(0.3) - : const Color.fromARGB(255, 0, 0, 0)!, - iconColor: isDarkMode - ? const Color.fromARGB(255, 0, 0, 0)! - : const Color.fromARGB(255, 255, 255, 255)!, - titleColor: isDarkMode ? Colors.white : null, - subtitleColor: isDarkMode ? Colors.white70 : null, - isDarkMode: isDarkMode, - showbutt: controller.earphonestate == 2, - onPressed: () { - controller.unbindearphone(); - }, - ), - ) - ], - ), - ), - ), - ), + ); + }).toList(), ], ); } - // 录音笔 - Widget _buildVoiceRecorderCard(bool isDarkMode) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(left: 4.w, bottom: 8.h), - child: Text( - 'voicerecorder'.tr, //AI设置 + Widget _buildEmptyState(bool isDarkMode) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.devices_other, + size: 80.sp, + color: isDarkMode ? Colors.white54 : Colors.grey[400], + ), + SizedBox(height: 20.h), + Text( + "暂未绑定任何设备", style: TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.blue[400] : Colors.blue[600], - letterSpacing: 1, + fontSize: 16.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[600], ), ), - ), - Container( - decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, - borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(16.r), - child: BackdropFilter( - filter: isDarkMode - ? ImageFilter.blur(sigmaX: 5, sigmaY: 5) - : ImageFilter.blur(sigmaX: 0, sigmaY: 0), - child: Column( - children: [ - // 实时对话入口 - Obx( - () => _buildSimpleNavigationSetting( - title: 'notBound'.tr, // 未绑定 - subtitle: 'deviceNotBoundYet'.tr, // 还未绑定设备 - icon: Icons.edit, - iconBgColor: isDarkMode - ? const Color.fromARGB(255, 255, 255, 255)! - .withOpacity(0.3) - : const Color.fromARGB(255, 0, 0, 0)!, - iconColor: isDarkMode - ? const Color.fromARGB(255, 0, 0, 0)! - : const Color.fromARGB(255, 255, 255, 255)!, - titleColor: isDarkMode ? Colors.white : null, - subtitleColor: isDarkMode ? Colors.white70 : null, - isDarkMode: isDarkMode, - showbutt: controller.isvoicepen.value, - onPressed: () {}, - ), - ) - ], - ), + SizedBox(height: 12.h), + Text( + "请先绑定设备后再进行管理", + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white54 : Colors.grey[500], ), ), - ), - ], + ], + ), ); } - // 简单导航设置项 - Widget _buildSimpleNavigationSetting({ - required String title, - String? subtitle, - required IconData icon, - required Color iconBgColor, - required Color iconColor, - Color? titleColor, - Color? subtitleColor, + Widget _buildDeviceCard({ + required UserDevice device, + required DeviceType deviceType, required bool isDarkMode, - required bool showbutt, - required VoidCallback onPressed, }) { - return InkWell( - child: Padding( - padding: EdgeInsets.all(16.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Row( - children: [ - Container( - width: 32.w, - height: 32.w, - decoration: BoxDecoration( - color: iconBgColor, - borderRadius: BorderRadius.circular(16.r), - ), - child: Icon( - icon, - size: 16.w, - color: iconColor, - ), + return Container( + margin: EdgeInsets.only(bottom: 12.h), + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + // 设备图标 + Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + color: + isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], + borderRadius: BorderRadius.circular(12.r), + ), + child: Icon( + deviceType.icon, + color: isDarkMode ? Colors.white : Colors.black87, + size: 24.sp, + ), + ), + SizedBox(width: 16.w), + // 设备信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + device.devicename.isNotEmpty ? device.devicename : "未知设备", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + SizedBox(height: 4.h), + Text( + device.devicemac.isNotEmpty ? device.devicemac : "无MAC地址", + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[600], ), - SizedBox(width: 12.w), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - color: titleColor ?? - (isDarkMode ? Colors.white : Colors.black87), - ), - overflow: TextOverflow.ellipsis, - ), - if (subtitle != null) SizedBox(height: 2.h), - if (subtitle != null) - Text( - subtitle, - style: TextStyle( - fontSize: 12.sp, - color: subtitleColor ?? - (isDarkMode - ? Colors.white70 - : Colors.black54), - ), - overflow: TextOverflow.ellipsis, - ), - ], + ), + if (device.license.isNotEmpty) ...[ + SizedBox(height: 4.h), + Text( + "授权码: ${device.license}", + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white54 : Colors.grey[500], ), ), ], + ], + ), + ), + // 解绑按钮 + GestureDetector( + onTap: () => _showUnbindDialog(device, isDarkMode), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 6.h, + ), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(6.r), + border: Border.all( + color: Colors.red.withOpacity(0.3), + width: 1, + ), + ), + child: Text( + "unbind".tr, + style: TextStyle( + fontSize: 12.sp, + color: Colors.red, + fontWeight: FontWeight.w500, + ), ), ), - if (showbutt) - Container( - width: 74.w, - height: 40.w, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - padding: EdgeInsets.symmetric(horizontal: 2, vertical: 2), - ), - onPressed: onPressed, - child: Text( - 'unbind'.tr, // 解绑 - style: TextStyle( - color: Colors.white, - ), - ), - )), - ], + ), + ], + ), + ); + } + + void _showUnbindDialog(UserDevice device, bool isDarkMode) { + Get.dialog( + AlertDialog( + backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, + title: Text( + "确认解绑", + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + content: Text( + "确定要解绑设备 \"${device.devicename}\" 吗?", + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + ), ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + "取消", + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.grey[600], + ), + ), + ), + TextButton( + onPressed: () { + Get.back(); + controller.unbindearphone(device); + }, + child: const Text( + "确认解绑", + style: TextStyle(color: Colors.red), + ), + ), + ], ), ); } diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 891371051..927606ad1 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -589,11 +589,12 @@ class PairingController extends GetxController { //自动绑定设备 0 未合作商 1 合作商 2,已绑定 Future authbindingDevice(BluetoothDevice device) async { - if (User.instance.earphoneauthcode != "" && - User.instance.earphonemac == device.address) { - // 已经绑定 - return 9; - } + // if (User.instance.earphoneauthcode != "" && + // User.instance.earphonemac == device.address) { + // // 已经绑定 + // return 9; + // } + // 检查设备名称是否包含 DeepSound 或 DeapSound final deviceName = device.name; @@ -632,13 +633,13 @@ class PairingController extends GetxController { _verifiedDeviceAddress = uuids; print('找到匹配设备,获取到 uuids: $uuids'); // 检查是否已经绑定 - if (User.instance.earphonelicense != "" && - User.instance.earphonemac == device.address) { - // 已经绑定 - return 10; - } else { - return 1; - } + // if (User.instance.earphonelicense != "" && + // User.instance.earphonemac == device.address) { + // // 已经绑定 + // return 10; + // } else { + return 1; + // } } } else if (Platform.isAndroid) { // Android设备,提取15-24字节的授权码 @@ -649,13 +650,13 @@ class PairingController extends GetxController { deviceCode.value = code; _verifiedDeviceAddress = device.address; // 检查是否已经绑定 - if (User.instance.earphonelicense != "" && - User.instance.earphonemac == device.address) { - // 已经绑定 - return 10; - } else { - return 1; - } + // if (User.instance.earphonelicense != "" && + // User.instance.earphonemac == device.address) { + // // 已经绑定 + // return 10; + // } else { + return 1; + // } } } else { break; @@ -729,19 +730,21 @@ class PairingController extends GetxController { //确认绑定设备 void confirmBindDevice() async { - final response = await Api.bindearphone({ + final response = await Api.binddevice({ "code": deviceCode.value, - "earphonname": currentDevice?.name, - "earphonemac": currentDevice?.address, // 使用 address 替代 deviceId + "devicename": currentDevice?.name, + "devicemac": currentDevice?.address, // 使用 address 替代 deviceId }); Logger.i(_tag, '手动授权结果 ${response}'); if (response != null) { - Logger.i(_tag, '绑定设备成功'); - User.instance.earphonemac = currentDevice?.address ?? ''; // 使用 address - User.instance.earphonename = currentDevice?.name ?? ''; - User.instance.earphonelicense = deviceCode.value; - _transition(PairingState.verifying); - _verifyDevice(_currentClassicDevice!); + Logger.i(_tag, '绑定设备成功 $response'); + if (response['device'] != '') { + // 根据API设计调整 + final device = UserDevice.fromJson(response['device']); + User.instance.addUserDevice(device); + _transition(PairingState.verifying); + _verifyDevice(_currentClassicDevice!); + } return; } else { Logger.e(_tag, '绑定设备失败!'); @@ -777,9 +780,9 @@ class PairingController extends GetxController { }); Logger.i(_tag, '手动授权结果 ${response}'); if (response != null) { - User.instance.earphonemac = currentDevice?.address ?? ''; // 使用 address - User.instance.earphonename = currentDevice?.name ?? ''; - User.instance.earphoneauthcode = codeInputController.text.trim(); + // User.instance.earphonemac = currentDevice?.address ?? ''; // 使用 address + // User.instance.earphonename = currentDevice?.name ?? ''; + // User.instance.earphoneauthcode = codeInputController.text.trim(); _transition(PairingState.verifying); _verifyDevice(_currentClassicDevice!); } else { @@ -859,7 +862,16 @@ class PairingController extends GetxController { void onSkipBindingTap() { Logger.i(_tag, '用户选择临时跳过手动绑定'); - User.instance.earphonemac = currentDevice?.address ?? ''; + User.instance.addUserDevice( + UserDevice( + id: 0, // 临时设备ID + uid: User.instance.uid, + devicetype: 1, + devicemac: _currentClassicDevice?.address ?? '', + devicename: _currentClassicDevice?.name ?? 'Unknown Device', + license: "", // 临时授权码 + ), + ); _transition(PairingState.verifying); _verifyDevice(_currentClassicDevice!); } diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index c477bb755..4f9dc5a3c 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -122,6 +122,7 @@ class SplashController extends GetxController { print('getLoginToken:进来了'); // 根据API设计调整 final user = User.fromJson(response); + // 存储用户信息(示例) await _storage.write('user_info', user.toJson()); return true; diff --git a/local_plugins/chat_api/ios/chat_api/Package.swift b/local_plugins/chat_api/ios/chat_api/Package.swift index dcd6fe2dc..590fd4a5c 100644 --- a/local_plugins/chat_api/ios/chat_api/Package.swift +++ b/local_plugins/chat_api/ios/chat_api/Package.swift @@ -14,14 +14,16 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/MacPaw/OpenAI.git", from: "0.4.3"), - .package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "0.9.0") + .package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "0.9.0"), + .package(path: "../../../location_service/ios/location_service"), ], targets: [ .target( name: "chat_api", dependencies: [ .product(name: "MCP", package: "swift-sdk"), - .product(name: "OpenAI", package: "OpenAI") + .product(name: "OpenAI", package: "OpenAI"), + .product(name: "location-service", package: "location_service") ], exclude: [] ), diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift index 1d7b7746d..41c3df391 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift @@ -1,12 +1,11 @@ import Foundation import UIKit -import Contacts import EventKit import CoreLocation -import MediaPlayer -import AVFoundation +import MessageUI +import location_service -/// 操作结果 +/// 结果结构体 public struct Result { let success: Bool let message: String @@ -20,26 +19,14 @@ public struct Result { /// 系统功能处理器 public class SystemFunctionHandler: NSObject { - // 位置管理器 - private var locationManager: CLLocationManager? - private var locationCompletion: ((Result) -> Void)? - // 事件存储 private var eventStore: EKEventStore? public override init() { super.init() - setupLocationManager() setupEventStore() } - /// 设置位置管理器 - private func setupLocationManager() { - locationManager = CLLocationManager() - locationManager?.delegate = self - locationManager?.desiredAccuracy = kCLLocationAccuracyBest - } - /// 设置事件存储 private func setupEventStore() { eventStore = EKEventStore() @@ -67,7 +54,7 @@ public class SystemFunctionHandler: NSObject { registerGetCurrentTimeFunction(client: client) // 注册获取当前位置函数 - // registerGetCurrentLocationFunction(client: client) + registerGetCurrentLocationFunction(client: client) // 注册媒体控制函数 registerMediaPlayFunction(client: client) @@ -112,8 +99,8 @@ public class SystemFunctionHandler: NSObject { let handler = TranslationModeHandler() _ = client.registerLocalFunction( - name: "enter_translation_mode", - description: "用户请求进入实时翻译模式时,启动实时翻译功能", + name: "translation_mode", + description: "进入翻译模式", parameters: parameters, handler: handler ) @@ -130,7 +117,7 @@ public class SystemFunctionHandler: NSObject { ], "message": [ "type": "string", - "description": "短信内容" + "description": "要发送的短信内容" ] ], "required": ["contact", "message"] @@ -182,7 +169,7 @@ public class SystemFunctionHandler: NSObject { ], "time": [ "type": "string", - "description": "提醒时间,格式为'yyyy-MM-dd HH:mm',如'2023-12-31 14:30'" + "description": "提醒时间,格式:yyyy-MM-dd HH:mm" ] ], "required": ["title", "time"] @@ -191,7 +178,7 @@ public class SystemFunctionHandler: NSObject { let handler = SetReminderHandler(eventStore: eventStore) _ = client.registerLocalFunction( name: "set_reminder", - description: "设置提醒事项", + description: "设置提醒", parameters: parameters, handler: handler ) @@ -204,7 +191,7 @@ public class SystemFunctionHandler: NSObject { "properties": [ "format": [ "type": "string", - "description": "时间格式,可选,默认为标准格式" + "description": "时间格式,可选" ] ], "required": [] @@ -213,7 +200,7 @@ public class SystemFunctionHandler: NSObject { let handler = GetCurrentTimeHandler() _ = client.registerLocalFunction( name: "get_current_time", - description: "获取当前日期和时间", + description: "获取当前时间", parameters: parameters, handler: handler ) @@ -227,16 +214,16 @@ public class SystemFunctionHandler: NSObject { "required": [] ] - let handler = GetCurrentLocationHandler(systemHandler: self) + let handler = GetCurrentLocationHandler() _ = client.registerLocalFunction( name: "get_current_location", - description: "获取当前地理位置", + description: "获取当前位置", parameters: parameters, handler: handler ) } - /// 注册媒体播放功能 + /// 注册媒体播放函数 private func registerMediaPlayFunction(client: MCPClient) { let parameters: [String: Any] = [ "type": "object", @@ -253,7 +240,7 @@ public class SystemFunctionHandler: NSObject { ) } - /// 注册媒体暂停功能 + /// 注册媒体暂停函数 private func registerMediaPauseFunction(client: MCPClient) { let parameters: [String: Any] = [ "type": "object", @@ -270,7 +257,7 @@ public class SystemFunctionHandler: NSObject { ) } - /// 注册媒体上一首功能 + /// 注册媒体上一首函数 private func registerMediaPreviousFunction(client: MCPClient) { let parameters: [String: Any] = [ "type": "object", @@ -287,7 +274,7 @@ public class SystemFunctionHandler: NSObject { ) } - /// 注册媒体下一首功能 + /// 注册媒体下一首函数 private func registerMediaNextFunction(client: MCPClient) { let parameters: [String: Any] = [ "type": "object", @@ -304,7 +291,7 @@ public class SystemFunctionHandler: NSObject { ) } - /// 注册打开录音机功能 + /// 注册打开录音机函数 private func registerOpenRecorderFunction(client: MCPClient) { let parameters: [String: Any] = [ "type": "object", @@ -315,122 +302,72 @@ public class SystemFunctionHandler: NSObject { let handler = OpenRecorderHandler() _ = client.registerLocalFunction( name: "open_recorder", - description: "打开系统录音机并开始录音", + description: "打开录音机", parameters: parameters, handler: handler ) } +} + +// MARK: - 功能处理器实现 + +/// 获取当前位置处理器 +private class GetCurrentLocationHandler: FunctionHandler { - // MARK: - 内部方法 - - /// 获取当前位置 - internal func getCurrentLocation() async -> Result { - return await withCheckedContinuation { continuation in - locationCompletion = { result in - continuation.resume(returning: result) - } + func handle(arguments: [String: Any]) async throws -> String { + // 检查位置权限 + guard LocationService.shared.hasLocationPermission() else { + let errorResult: [String: Any] = [ + "success": false, + "message": "位置权限被拒绝。请前往 设置 > 隐私与安全 > 定位服务 > Deep Voice 启用定位权限" + ] + let jsonData = try JSONSerialization.data(withJSONObject: errorResult) + return String(data: jsonData, encoding: .utf8) ?? "{\"error\": \"JSON序列化失败\"}" + } + + // 先尝试获取缓存的位置数据 + if let cachedLocation = LocationService.shared.getCachedLocationData() { + let message = "当前位置:\(cachedLocation.address ?? "未知地址")(纬度 \(cachedLocation.latitude),经度 \(cachedLocation.longitude))" - // 设置15秒超时 - DispatchQueue.global().asyncAfter(deadline: .now() + 15.0) { [weak self] in - if self?.locationCompletion != nil { - self?.locationCompletion?(Result(success: false, message: "位置请求超时")) - self?.locationCompletion = nil + // 异步触发最新定位(不等待结果) + Task { + do { + _ = try await LocationService.shared.getCurrentLocation() + } catch { + print("后台更新位置失败: \(error)") } } - // 检查位置服务是否可用 - guard CLLocationManager.locationServicesEnabled() else { - locationCompletion?(Result(success: false, message: "位置服务未启用。请前往 设置 > 隐私与安全 > 定位服务 启用定位服务")) - locationCompletion = nil - return - } - - // 确保locationManager已初始化 - guard let locationManager = locationManager else { - locationCompletion?(Result(success: false, message: "位置管理器未初始化")) - locationCompletion = nil - return - } - - // 检查位置权限 - let authStatus: CLAuthorizationStatus - if #available(iOS 14.0, *) { - authStatus = locationManager.authorizationStatus - } else { - authStatus = CLLocationManager.authorizationStatus() - } - - print("当前位置权限状态: \(authStatus.rawValue)") - - switch authStatus { - case .notDetermined: - print("请求位置权限...") - // 请求权限 - locationManager.requestWhenInUseAuthorization() - // 权限结果会在 didChangeAuthorization 回调中处理 - case .denied: - locationCompletion?(Result(success: false, message: "位置权限被拒绝。请前往 设置 > 隐私与安全 > 定位服务 > Deep Voice 启用定位权限")) - locationCompletion = nil - case .restricted: - locationCompletion?(Result(success: false, message: "位置权限受限制,可能是由于家长控制或企业配置限制")) - locationCompletion = nil - case .authorizedWhenInUse, .authorizedAlways: - print("已有位置权限,开始获取位置...") - // 已有权限,直接请求位置 - locationManager.requestLocation() - @unknown default: - locationCompletion?(Result(success: false, message: "未知的位置权限状态: \(authStatus.rawValue)")) - locationCompletion = nil - } - } - } -} - -// MARK: - CLLocationManagerDelegate -extension SystemFunctionHandler: CLLocationManagerDelegate { - public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - guard let location = locations.last else { - locationCompletion?(Result(success: false, message: "无法获取位置信息")) - return - } - - let latitude = location.coordinate.latitude - let longitude = location.coordinate.longitude - let message = "当前位置:纬度 \(latitude),经度 \(longitude)" - - locationCompletion?(Result(success: true, message: message)) - locationCompletion = nil - } - - public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - locationCompletion?(Result(success: false, message: "获取位置失败:\(error.localizedDescription)")) - locationCompletion = nil - } - - public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { - // 只有在有位置请求等待时才处理权限变化 - guard locationCompletion != nil else { - return + let result: [String: Any] = [ + "success": true, + "message": message + ] + let jsonData = try JSONSerialization.data(withJSONObject: result) + return String(data: jsonData, encoding: .utf8) ?? "{\"error\": \"JSON序列化失败\"}" } - switch status { - case .authorizedWhenInUse, .authorizedAlways: - manager.requestLocation() - case .denied, .restricted: - locationCompletion?(Result(success: false, message: "位置权限被拒绝。请前往 设置 > 隐私与安全 > 定位服务 > Deep Voice 启用定位权限")) - locationCompletion = nil - case .notDetermined: - // 权限仍未确定,等待用户操作 - break - @unknown default: - locationCompletion?(Result(success: false, message: "位置权限状态异常,请重试")) - locationCompletion = nil + // 如果没有缓存数据,同步获取位置 + do { + let locationData = try await LocationService.shared.getCurrentLocation() + let message = "当前位置:\(locationData.address ?? "未知地址")(纬度 \(locationData.latitude),经度 \(locationData.longitude))" + + let result: [String: Any] = [ + "success": true, + "message": message + ] + let jsonData = try JSONSerialization.data(withJSONObject: result) + return String(data: jsonData, encoding: .utf8) ?? "{\"error\": \"JSON序列化失败\"}" + } catch { + let errorResult: [String: Any] = [ + "success": false, + "message": "获取位置失败:\(error.localizedDescription)" + ] + let jsonData = try JSONSerialization.data(withJSONObject: errorResult) + return String(data: jsonData, encoding: .utf8) ?? "{\"error\": \"JSON序列化失败\"}" } } } -// MARK: - 功能处理器实现 - /// 退出交互处理器 private class ExitInteractionHandler: FunctionHandler { func handle(arguments: [String: Any]) async throws -> String { @@ -460,22 +397,23 @@ private class SendTextMessageHandler: FunctionHandler { } private func sendTextMessage(contact: String, message: String) async -> Result { - guard let url = URL(string: "sms:\(contact)&body=\(message.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")") else { - return Result(success: false, message: "无法创建短信URL") - } - return await withCheckedContinuation { continuation in DispatchQueue.main.async { - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) { success in - if success { - continuation.resume(returning: Result(success: true, message: "已打开短信应用")) - } else { - continuation.resume(returning: Result(success: false, message: "打开短信应用失败")) - } + if MFMessageComposeViewController.canSendText() { + let messageVC = MFMessageComposeViewController() + messageVC.recipients = [contact] + messageVC.body = message + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first, + let rootViewController = window.rootViewController { + rootViewController.present(messageVC, animated: true) + continuation.resume(returning: Result(success: true, message: "短信界面已打开")) + } else { + continuation.resume(returning: Result(success: false, message: "无法打开短信界面")) } } else { - continuation.resume(returning: Result(success: false, message: "无法打开短信应用")) + continuation.resume(returning: Result(success: false, message: "设备不支持发送短信")) } } } @@ -591,34 +529,16 @@ private class GetCurrentTimeHandler: FunctionHandler { private func getCurrentTime(format: String?) -> Result { let formatter = DateFormatter() - if let format = format, !format.isEmpty { - formatter.dateFormat = format + if let customFormat = format, !customFormat.isEmpty { + formatter.dateFormat = customFormat } else { - formatter.dateStyle = .full - formatter.timeStyle = .full - formatter.locale = Locale(identifier: "zh_CN") + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" } - let currentTime = formatter.string(from: Date()) - return Result(success: true, message: currentTime) - } -} - -/// 获取当前位置处理器 -private class GetCurrentLocationHandler: FunctionHandler { - private let systemHandler: SystemFunctionHandler - - init(systemHandler: SystemFunctionHandler) { - self.systemHandler = systemHandler - } - - func handle(arguments: [String: Any]) async throws -> String { - // guard let systemHandler = systemHandler else { - // throw ChatApiException("系统处理器未初始化") - // } + formatter.locale = Locale(identifier: "zh_CN") + let timeString = formatter.string(from: Date()) - let result: Result = await systemHandler.getCurrentLocation() - return "{\"result\": \"\(result.message)\", \"success\": \(result.success)}" + return Result(success: true, message: timeString) } } @@ -666,4 +586,4 @@ private class OpenRecorderHandler: FunctionHandler { return "{\"result\": \"无法打开录音机应用\"}" } -} +} diff --git a/local_plugins/location_service/ios/.gitignore b/local_plugins/location_service/ios/.gitignore deleted file mode 100644 index 034771fc9..000000000 --- a/local_plugins/location_service/ios/.gitignore +++ /dev/null @@ -1,38 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/ephemeral/ -/Flutter/flutter_export_environment.sh diff --git a/local_plugins/location_service/ios/Classes/LocationService.swift b/local_plugins/location_service/ios/Classes/LocationService.swift deleted file mode 100644 index d71179ee3..000000000 --- a/local_plugins/location_service/ios/Classes/LocationService.swift +++ /dev/null @@ -1,131 +0,0 @@ -import Foundation -import AMapLocationKit - -extension Notification.Name { - static let locationstartEvent = Notification.Name("locationstartEvent") //启动定位 - static let updatelocationEvent = Notification.Name("updatelocationEvent") //更新位置事件 -} -class LocationService: NSObject { - static let shared = LocationService() - private let TAG = "LocationService" - - private var locationManager: AMapLocationManager! - private var currentLocation: CLLocation? - private var currentReGeocode: AMapLocationReGeocode? - private var initialized = false - - private override init() { - super.init() - } - - // 初始化定位服务 - func initialize() { - if initialized { return } - print("\(TAG)--------- 初始化高德定位服务") - - locationManager = AMapLocationManager() - locationManager.delegate = self - - locationManager.desiredAccuracy = kCLLocationAccuracyBest // 定位精度 - locationManager.distanceFilter = 10.0 // 移动距离阈值(单位:米) - locationManager.locatingWithReGeocode = true // 是否返回逆地理编码信息 - locationManager.allowsBackgroundLocationUpdates = true // 是否允许后台定位 - locationManager.locationTimeout = 10 // 定位超时时间(秒) - locationManager.reGeocodeTimeout = 5 // 逆地理编码超时时间(秒) - - locationManager.startUpdatingLocation() - - initialized = true - } - - // 获取缓存的位置信息 - func getCachedLocation() -> [String: String]? { - guard let location = currentLocation else { return nil } - - var locationInfo: [String: String] = [ - "user_lat": "\(location.coordinate.latitude)", - "user_lng": "\(location.coordinate.longitude)" - ] - - if let regeocode = currentReGeocode { - let address = [ - regeocode.province, // 省 - regeocode.city, // 市 - regeocode.district, // 区 - regeocode.street, // 街道 - regeocode.number // 门牌号 - ].compactMap { $0 }.joined(separator: "") - - locationInfo["user_location"] = address - } - - return locationInfo - } - - // 获取当前位置(带回调) - func getCurrentLocation(completion: @escaping ([String: String]?, Error?) -> Void) { - if let cached = getCachedLocation() { - completion(cached, nil) - } else { - let error = NSError( - domain: "LocationService", - code: -1, - userInfo: [NSLocalizedDescriptionKey: "当前位置还未获取"] - ) - completion(nil, error) - } - } - - // 停止定位服务 - func stop() { - locationManager.stopUpdatingLocation() - locationManager = nil - currentLocation = nil - currentReGeocode = nil - initialized = false - } -} - -// MARK: - AMapLocationManagerDelegate -extension LocationService: AMapLocationManagerDelegate { - // 定位更新回调 - func amapLocationManager(_ manager: AMapLocationManager!, didUpdate location: CLLocation!, reGeocode: AMapLocationReGeocode!) { - currentLocation = location - currentReGeocode = reGeocode - - print("\(TAG)--------- 定位更新: \(location.coordinate.latitude), \(location.coordinate.longitude)") - - var locationInfo: [String: String] = [ - "user_lat": "\(location.coordinate.latitude)", - "user_lng": "\(location.coordinate.longitude)" - ] - if let regeocode = currentReGeocode { - let address = [ - reGeocode.province, // 省 - reGeocode.city, // 市 - reGeocode.district, // 区 - reGeocode.street, // 街道 - reGeocode.number // 门牌号 - ].compactMap { $0 }.joined(separator: "") - print("\(TAG)--------- 定位地址: \(address)") - locationInfo["user_location"] = address - } - - - NotificationCenter.default.post( - name: .updatelocationEvent, - object: nil, - userInfo: locationInfo - ) - } - - // 定位失败回调 - func amapLocationManager(_ manager: AMapLocationManager!, didFailWithError error: Error!) { - print("\(TAG)--------- 定位失败: \(error.localizedDescription)") - } - - // 权限变更回调 - func amapLocationManager(_ manager: AMapLocationManager!, doRequireLocationAuth locationManager: CLLocationManager!) { - locationManager.requestWhenInUseAuthorization() - } -} diff --git a/local_plugins/location_service/ios/Classes/LocationServicePlugin.swift b/local_plugins/location_service/ios/Classes/LocationServicePlugin.swift deleted file mode 100644 index ad30a36f7..000000000 --- a/local_plugins/location_service/ios/Classes/LocationServicePlugin.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Flutter -import UIKit -import AMapLocationKit - -public class LocationServicePlugin: NSObject, FlutterPlugin { - private static var channel: FlutterMethodChannel? - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "com.yunqiinnovation.location_service", binaryMessenger: registrar.messenger()) - let instance = LocationServicePlugin() - // 必须先设置隐私合规 - DispatchQueue.main.async { - // 2. API Key - AMapServices.shared()?.apiKey = "aa60a03be9f6531bcc64029e60f95d03" - AMapServices.shared().enableHTTPS = true - // 3. 打印验证(调试用) - print("高德SDK初始化完成") - } - registrar.addMethodCallDelegate(instance, channel: channel) - self.channel = channel - instance.addstartlocationmonitor() - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "location": - result(nil) - default: - result(FlutterMethodNotImplemented) - } - } - - private func addstartlocationmonitor(){ - // 通常放在启动后或模块初始化时 - NotificationCenter.default.addObserver( - forName: .locationstartEvent, - object: nil, - queue: .main - ) { notification in - print("📥 收到 启动定位服务事件") - LocationService.shared.initialize() - } - } -} diff --git a/local_plugins/location_service/ios/location_service.podspec b/local_plugins/location_service/ios/location_service.podspec deleted file mode 100644 index 549e6c473..000000000 --- a/local_plugins/location_service/ios/location_service.podspec +++ /dev/null @@ -1,27 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint location_service.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'location_service' - s.version = '0.0.1' - s.summary = 'A new Flutter project.' - s.description = <<-DESC -A new Flutter project. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'Flutter' - s.dependency 'AMapLocation', '~> 2.10.0' - - s.static_framework = true - s.pod_target_xcconfig = { - 'DEFINES_MODULE' => 'YES', - 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' - } - s.platform = :ios, '12.0' - s.swift_version = '5.0' -end diff --git a/local_plugins/location_service/ios/location_service/Package.swift b/local_plugins/location_service/ios/location_service/Package.swift new file mode 100644 index 000000000..796eef704 --- /dev/null +++ b/local_plugins/location_service/ios/location_service/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "location_service", + platforms: [ + .iOS(.v16), + .macOS(.v13) + ], + products: [ + .library(name: "location-service", targets: ["location_service"]) + ], + dependencies: [ ], + targets: [ + .target( + name: "location_service", + dependencies: [], + path: "Sources/location_service" + ) + ] +) diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationCallback.swift b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationCallback.swift new file mode 100644 index 000000000..8a778f9ad --- /dev/null +++ b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationCallback.swift @@ -0,0 +1,7 @@ +import Foundation + +/// 定位回调协议 +public protocol LocationCallback: AnyObject { + func onLocationSuccess(latitude: Double, longitude: Double, address: String) + func onLocationError(errorCode: Int, errorMessage: String) +} \ No newline at end of file diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift new file mode 100644 index 000000000..af7f9827c --- /dev/null +++ b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift @@ -0,0 +1,16 @@ +import Foundation + +/// 位置数据结构 +public struct LocationData: Codable { + public let latitude: Double + public let longitude: Double + public let address: String + public let timestamp: TimeInterval + + public init(latitude: Double, longitude: Double, address: String, timestamp: TimeInterval) { + self.latitude = latitude + self.longitude = longitude + self.address = address + self.timestamp = timestamp + } +} diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift new file mode 100644 index 000000000..04c991abc --- /dev/null +++ b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift @@ -0,0 +1,357 @@ +import Foundation +import CoreLocation +import UIKit + +public extension Notification.Name { + static let locationStartEvent = Notification.Name("locationStartEvent") + static let updateLocationEvent = Notification.Name("updateLocationEvent") +} + +public class LocationService: NSObject { + private static let TAG = "LocationService" + private static let CACHE_PREFS_NAME = "location_cache" + private static let PERMISSION_REQUEST_CODE = 1001 + + public static let shared = LocationService() + + private var locationManager: CLLocationManager? + private var initialized = false + private var permissionRetryTimer: Timer? + + // 存储当前的定位请求回调(Flutter用) + private var currentLocationCallback: ((Result<[String: Any], Error>) -> Void)? + // 存储权限请求回调(Flutter用) + private var permissionRequestCallback: ((Result<[String: Bool], Error>) -> Void)? + + // 存储服务间调用的回调 + private var serviceCallbacks: [LocationCallback] = [] + + // 缓存的位置数据 + private var cachedLocationData: LocationData? + + private override init() { + super.init() + } + + /// 检查应用是否在前台运行 + public func isAppInForeground() -> Bool { + return UIApplication.shared.applicationState == .active + } + + /// 初始化定位服务 + public func initialize() { + if initialized { return } + + if !checkPermissions() { + print("\(LocationService.TAG): 没有定位权限,等待用户授权中...") + startPermissionMonitor() + return + } + + setupLocationManager() + initialized = true + print("\(LocationService.TAG): 定位服务初始化完成") + } + + private func setupLocationManager() { + locationManager = CLLocationManager() + locationManager?.delegate = self + locationManager?.desiredAccuracy = kCLLocationAccuracyBest + locationManager?.distanceFilter = 10 + } + + private func startPermissionMonitor() { + permissionRetryTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in + guard let self = self else { return } + let hasPermission = self.checkPermissions() + if hasPermission && !self.initialized { + print("\(LocationService.TAG): 权限已授予,重新初始化定位服务") + self.initialize() + self.stopPermissionMonitor() + } + } + } + + private func stopPermissionMonitor() { + permissionRetryTimer?.invalidate() + permissionRetryTimer = nil + } + + /// 检查定位权限 + private func checkPermissions() -> Bool { + return hasForegroundLocationPermission() + } + + private func hasForegroundLocationPermission() -> Bool { + let status = CLLocationManager.authorizationStatus() + return status == .authorizedWhenInUse || status == .authorizedAlways + } + + private func hasBackgroundLocationPermission() -> Bool { + return CLLocationManager.authorizationStatus() == .authorizedAlways + } + + /// 检查定位权限状态(Flutter用) + public func checkLocationPermission(completion: @escaping (Result<[String: Bool], Error>) -> Void) { + let hasPermission = checkPermissions() + let result: [String: Bool] = [ + "hasPermission": hasPermission, + "hasForegroundPermission": hasForegroundLocationPermission(), + "hasBackgroundPermission": hasBackgroundLocationPermission() + ] + completion(.success(result)) + } + + /// 请求定位权限(Flutter用) + public func requestLocationPermission(completion: @escaping (Result<[String: Bool], Error>) -> Void) { + if checkPermissions() { + completion(.success(["granted": true])) + return + } + + permissionRequestCallback = completion + locationManager?.requestWhenInUseAuthorization() + } + + /// 按需获取当前位置(Flutter用) + public func getCurrentLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) { + if !checkPermissions() { + completion(.failure(NSError(domain: "LocationService", code: -2, userInfo: [NSLocalizedDescriptionKey: "需要定位权限"]))) + return + } + + if !initialized { + completion(.failure(NSError(domain: "LocationService", code: -3, userInfo: [NSLocalizedDescriptionKey: "定位服务未初始化"]))) + return + } + + if currentLocationCallback != nil { + completion(.failure(NSError(domain: "LocationService", code: -4, userInfo: [NSLocalizedDescriptionKey: "正在定位中,请稍后再试"]))) + return + } + + // 保存回调,开始定位 + currentLocationCallback = completion + locationManager?.requestLocation() + } + + // MARK: - 供其他iOS服务使用的接口 + + /// 检查是否有定位权限(供其他服务使用) + public func hasLocationPermission() -> Bool { + return checkPermissions() + } + + /// 异步获取当前位置(供其他iOS服务使用) + public func getCurrentLocation() async throws -> LocationData { + return try await withCheckedThrowingContinuation { continuation in + getCurrentLocation { result in + switch result { + case .success(let locationInfo): + // 从返回的字典中提取位置信息 + if let latStr = locationInfo["user_lat"] as? String, + let lngStr = locationInfo["user_lng"] as? String, + let latitude = Double(latStr), + let longitude = Double(lngStr), + let address = locationInfo["user_location"] as? String { + + let locationData = LocationData( + latitude: latitude, + longitude: longitude, + address: address, + timestamp: Date().timeIntervalSince1970 * 1000 + ) + continuation.resume(returning: locationData) + } else { + continuation.resume(throwing: NSError(domain: "LocationService", code: -5, userInfo: [NSLocalizedDescriptionKey: "位置数据格式错误"])) + } + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + /// 获取当前位置(供其他iOS服务使用) + public func requestLocation(callback: LocationCallback) { + if !checkPermissions() { + callback.onLocationError(errorCode: -2, errorMessage: "没有定位权限") + return + } + + if !initialized { + callback.onLocationError(errorCode: -3, errorMessage: "定位服务未初始化") + return + } + + // 添加到回调列表 + serviceCallbacks.append(callback) + + // 如果没有正在进行的定位请求,开始定位 + if currentLocationCallback == nil && serviceCallbacks.count == 1 { + locationManager?.requestLocation() + } + } + + /// 同步获取缓存的位置信息(供其他服务使用) + /// 注意:这个方法返回的可能是过期的位置信息 + public func getCachedLocationData() -> LocationData? { + // 优先返回内存中的缓存 + if let cached = cachedLocationData { + return cached + } + + // 从UserDefaults中读取缓存 + return loadCachedLocationFromUserDefaults() + } + + private func saveCachedLocationToUserDefaults(_ locationData: LocationData) { + do { + let encoder = JSONEncoder() + let data = try encoder.encode(locationData) + UserDefaults.standard.set(data, forKey: "latitude") + UserDefaults.standard.set(String(locationData.latitude), forKey: "latitude") + UserDefaults.standard.set(String(locationData.longitude), forKey: "longitude") + UserDefaults.standard.set(locationData.address, forKey: "address") + UserDefaults.standard.set(Double(locationData.timestamp), forKey: "timestamp") + UserDefaults.standard.synchronize() + } catch { + print("\(LocationService.TAG): 保存位置缓存失败: \(error.localizedDescription)") + } + } + + private func loadCachedLocationFromUserDefaults() -> LocationData? { + do { + guard let latitudeStr = UserDefaults.standard.string(forKey: "latitude"), + let longitudeStr = UserDefaults.standard.string(forKey: "longitude"), + let latitude = Double(latitudeStr), + let longitude = Double(longitudeStr) else { + return nil + } + + let address = UserDefaults.standard.string(forKey: "address") ?? "未知地址" + let timestampDouble = UserDefaults.standard.double(forKey: "timestamp") + + if timestampDouble == 0 { return nil } + + return LocationData( + latitude: latitude, + longitude: longitude, + address: address, + timestamp: TimeInterval(timestampDouble) + ) + } catch { + print("\(LocationService.TAG): 读取位置缓存失败: \(error.localizedDescription)") + return nil + } + } + + /// 停止定位服务 + public func stop() { + locationManager?.stopUpdatingLocation() + locationManager = nil + currentLocationCallback = nil + permissionRequestCallback = nil + serviceCallbacks.removeAll() + initialized = false + stopPermissionMonitor() + } + + private func handleLocationResult(_ location: CLLocation?, error: Error?) { + // 处理Flutter回调 + let flutterCallback = currentLocationCallback + currentLocationCallback = nil + + if let location = location, error == nil { + print("\(LocationService.TAG): 定位成功: \(location.coordinate.latitude), \(location.coordinate.longitude)") + + // 进行逆地理编码获取地址 + let geocoder = CLGeocoder() + geocoder.reverseGeocodeLocation(location) { [weak self] placemarks, geocodeError in + guard let self = self else { return } + + let address = placemarks?.first?.name ?? "未知地址" + + // 缓存位置数据 + let locationData = LocationData( + latitude: location.coordinate.latitude, + longitude: location.coordinate.longitude, + address: address, + timestamp: TimeInterval(Date().timeIntervalSince1970) * 1000 + ) + self.cachedLocationData = locationData + self.saveCachedLocationToUserDefaults(locationData) + + // 通知Flutter + if let callback = flutterCallback { + let result: [String: Any] = [ + "user_location": address, + "user_lat": String(location.coordinate.latitude), + "user_lng": String(location.coordinate.longitude) + ] + callback(.success(result)) + } + + // 通知其他iOS服务 + self.serviceCallbacks.forEach { callback in + callback.onLocationSuccess( + latitude: location.coordinate.latitude, + longitude: location.coordinate.longitude, + address: address + ) + } + + // 清空服务回调列表(单次定位) + self.serviceCallbacks.removeAll() + } + } else { + let errorMessage = error?.localizedDescription ?? "未知错误" + print("\(LocationService.TAG): 定位失败: \(errorMessage)") + + // 通知Flutter + if let callback = flutterCallback { + callback(.failure(NSError(domain: "LocationService", code: -1, userInfo: [NSLocalizedDescriptionKey: "定位失败: \(errorMessage)"]))) + } + + // 通知其他iOS服务 + serviceCallbacks.forEach { callback in + callback.onLocationError(errorCode: -1, errorMessage: errorMessage) + } + + // 清空服务回调列表 + serviceCallbacks.removeAll() + } + } +} + +// MARK: - CLLocationManagerDelegate +extension LocationService: CLLocationManagerDelegate { + public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let location = locations.last else { return } + handleLocationResult(location, error: nil) + } + + public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + handleLocationResult(nil, error: error) + } + + public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + print("\(LocationService.TAG): 定位权限状态变更: \(status.rawValue)") + + switch status { + case .authorizedWhenInUse, .authorizedAlways: + if !initialized { + initialize() + } + permissionRequestCallback?(.success(["granted": true])) + permissionRequestCallback = nil + case .denied, .restricted: + permissionRequestCallback?(.success(["granted": false])) + permissionRequestCallback = nil + case .notDetermined: + break + @unknown default: + break + } + } +} diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift new file mode 100644 index 000000000..80b383126 --- /dev/null +++ b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift @@ -0,0 +1,72 @@ +import Foundation + +#if canImport(Flutter) +import Flutter +import UIKit + +public class LocationServicePlugin: NSObject, FlutterPlugin { + private static var channel: FlutterMethodChannel? + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "com.yunqiinnovation.location_service", binaryMessenger: registrar.messenger()) + let instance = LocationServicePlugin() + + registrar.addMethodCallDelegate(instance, channel: channel) + self.channel = channel + instance.addStartLocationMonitor() + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "location": + result(nil) + case "initialize": + LocationService.shared.initialize() + result(nil) + case "getCurrentLocation": + LocationService.shared.getCurrentLocation { locationResult in + switch locationResult { + case .success(let locationInfo): + result(locationInfo) + case .failure(let error): + result(FlutterError(code: "LOCATION_ERROR", message: error.localizedDescription, details: nil)) + } + } + case "checkPermission": + LocationService.shared.checkLocationPermission { permissionResult in + switch permissionResult { + case .success(let permissions): + result(permissions) + case .failure(let error): + result(FlutterError(code: "PERMISSION_ERROR", message: error.localizedDescription, details: nil)) + } + } + case "requestPermission": + LocationService.shared.requestLocationPermission { permissionResult in + switch permissionResult { + case .success(let permissions): + result(permissions) + case .failure(let error): + result(FlutterError(code: "PERMISSION_ERROR", message: error.localizedDescription, details: nil)) + } + } + case "stop": + LocationService.shared.stop() + result(nil) + default: + result(FlutterMethodNotImplemented) + } + } + + private func addStartLocationMonitor() { + NotificationCenter.default.addObserver( + forName: .locationStartEvent, + object: nil, + queue: .main + ) { notification in + print("📥 收到启动定位服务事件") + LocationService.shared.initialize() + } + } +} +#endif \ No newline at end of file diff --git a/local_plugins/location_service/pubspec.yaml b/local_plugins/location_service/pubspec.yaml index 04f1a58f4..27ec7a94e 100644 --- a/local_plugins/location_service/pubspec.yaml +++ b/local_plugins/location_service/pubspec.yaml @@ -26,4 +26,5 @@ flutter: pluginClass: LocationServicePlugin ios: pluginClass: LocationServicePlugin - + swiftPackage: + path: ios/location_service