Browse Source

上传新版

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
6dbbdc5777
  1. 15
      ios/Podfile.lock
  2. 112
      lib/data/models/user_Info.dart
  3. 4
      lib/data/services/network/api.dart
  4. 43
      lib/modules/devices/controllers/devices_controller.dart
  5. 524
      lib/modules/devices/views/devices_view.dart
  6. 76
      lib/modules/pairing/controllers/pairing_controller.dart
  7. 1
      lib/modules/splash/controllers/splash_controller.dart
  8. 6
      local_plugins/chat_api/ios/chat_api/Package.swift
  9. 258
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift
  10. 38
      local_plugins/location_service/ios/.gitignore
  11. 131
      local_plugins/location_service/ios/Classes/LocationService.swift
  12. 44
      local_plugins/location_service/ios/Classes/LocationServicePlugin.swift
  13. 27
      local_plugins/location_service/ios/location_service.podspec
  14. 21
      local_plugins/location_service/ios/location_service/Package.swift
  15. 7
      local_plugins/location_service/ios/location_service/Sources/location_service/LocationCallback.swift
  16. 16
      local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift
  17. 357
      local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift
  18. 72
      local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift
  19. 3
      local_plugins/location_service/pubspec.yaml

15
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

112
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<UserDevice> 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<String, dynamic> json) {
// 修复1:安全获取嵌套的 user 字段
final userData = json['user'] as Map<String, dynamic>?; // 允许空值
final devicesData = json['devices'] as Map<String, dynamic>?; // 允许空值
//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<dynamic>?)
?.map((e) => UserDevice.fromJson(e as Map<String, dynamic>))
.toList() ??
[], // 处理设备列表
);
}
factory User.fronTokenJson(Map<String, dynamic> json) {
// 修复1:安全获取嵌套的 user 字段
final userData = json['user'] as Map<String, dynamic>?; // 允许空值
print("数据获取fronTokenJson${userData}");
final devicesData = json['devices'] as Map<String, dynamic>?; // 允许空值
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<dynamic>)
.map((e) => UserDevice.fromJson(e as Map<String, dynamic>))
.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<String, dynamic> 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,
};
}

4
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,
);
}

43
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<int, List<UserDevice>> devices;
@override
void onInit() {
super.onInit();
_bleManager = Get.find<BleManager>();
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);

524
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<DevicesController> {
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<DeviceType, List<UserDevice>> 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),
),
),
],
),
);
}

76
lib/modules/pairing/controllers/pairing_controller.dart

@ -589,11 +589,12 @@ class PairingController extends GetxController {
//自动绑定设备 0 未合作商 1 合作商 2,已绑定
Future<int> 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!);
}

1
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;

6
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: []
),

258
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\": \"无法打开录音机应用\"}"
}
}
}

38
local_plugins/location_service/ios/.gitignore

@ -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

131
local_plugins/location_service/ios/Classes/LocationService.swift

@ -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()
}
}

44
local_plugins/location_service/ios/Classes/LocationServicePlugin.swift

@ -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()
}
}
}

27
local_plugins/location_service/ios/location_service.podspec

@ -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

21
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"
)
]
)

7
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)
}

16
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
}
}

357
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
}
}
}

72
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

3
local_plugins/location_service/pubspec.yaml

@ -26,4 +26,5 @@ flutter:
pluginClass: LocationServicePlugin
ios:
pluginClass: LocationServicePlugin
swiftPackage:
path: ios/location_service

Loading…
Cancel
Save