feat(android): 支持沉浸式状态栏、地图定位与自定义签名
- 配置 edge-to-edge 沉浸式状态栏与刘海屏适配 - 新增百度地图权限、AK 配置及定位权限说明 - 接入 MethodChannel 实现返回键退后台热启动优化 - 配置自定义签名并更新构建逻辑 - 完善 token 刷新与鉴权失效统一处理 - 新增地图页与截图页路由 - 修复返回键拦截导致的 PopScope 失效问题
This commit is contained in:
163
lib/features/device/data/device_api.dart
Normal file
163
lib/features/device/data/device_api.dart
Normal file
@@ -0,0 +1,163 @@
|
||||
import 'package:cupertino_ui/cupertino_ui.dart';
|
||||
import 'package:ttstd_family_care/app/constants/app_constants.dart';
|
||||
import 'package:ttstd_family_care/core/network/api_exception.dart';
|
||||
import 'package:ttstd_family_care/core/network/dio_client.dart';
|
||||
import 'package:ttstd_family_care/features/device/domain/device_models.dart';
|
||||
|
||||
/// 客户端(家属端)设备接口访问层。
|
||||
///
|
||||
/// 后端接口前缀为 `/api/v1/client/sn`(与登录接口所在的 `/api/v1/open` 不同),
|
||||
/// 由于 Dio 在 path 以 `/` 开头时会替换 baseUrl 的完整 path 段(保留 host),
|
||||
/// 这里直接使用绝对路径即可正确命中 client 接口。
|
||||
/// [DioClient.get] 已统一解析为后台返回体中的 `data` 字段;业务失败时抛出
|
||||
/// [ApiException],本层将其降级为「空数据」以适配家属端展示。
|
||||
class DeviceApi {
|
||||
const DeviceApi();
|
||||
|
||||
/// 获取当前用户已绑定的设备列表。
|
||||
Future<List<DeviceBrief>> getMyDevices() async {
|
||||
try {
|
||||
final data = await DioClient.get(
|
||||
'v1/client/sn/my-devices',
|
||||
);
|
||||
return _parseList(data, DeviceBrief.fromJson);
|
||||
} on ApiException {
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取已绑定设备基本信息(无上报数据时返回 null)。
|
||||
Future<DeviceSystemInfo?> getDeviceInfo(String sn) async {
|
||||
try {
|
||||
final data = await DioClient.get(
|
||||
'v1/client/sn/device-info',
|
||||
queryParameters: {'sn': sn},
|
||||
);
|
||||
return data is Map<String, dynamic> ? DeviceSystemInfo.fromJson(data) : null;
|
||||
} on ApiException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取已绑定设备最新定位信息(无上报数据时返回 null)。
|
||||
Future<DeviceLocation?> getLocation(String sn) async {
|
||||
try {
|
||||
final data = await DioClient.get(
|
||||
'v1/client/sn/location',
|
||||
queryParameters: {'sn': sn},
|
||||
);
|
||||
return data is Map<String, dynamic> ? DeviceLocation.fromJson(data) : null;
|
||||
} on ApiException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取已绑定设备已安装应用列表。
|
||||
Future<List<DeviceApkInfo>> getApks(String sn) async {
|
||||
try {
|
||||
final data = await DioClient.get(
|
||||
'v1/client/sn/apks',
|
||||
queryParameters: {'sn': sn},
|
||||
);
|
||||
return _parseList(data, DeviceApkInfo.fromJson);
|
||||
} on ApiException {
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 向设备下发操作指令(重启/关机/截屏/刷新/定位等)。
|
||||
///
|
||||
/// [op] 为后端操作标识,如 `reboot` / `shutdown` / `screenshot` / `refresh` / `locate`,
|
||||
/// 对应接口路径 `/api/v1/client/sn/ops/{op}?sn=xxx`。
|
||||
/// 成功返回 null;失败返回后台错误提示。
|
||||
Future<String?> sendDeviceOp(String sn, String op) async {
|
||||
try {
|
||||
final data = await DioClient.post(
|
||||
'v1/client/sn/ops/$op',
|
||||
queryParameters: {'sn': sn},
|
||||
);
|
||||
debugPrint('[DeviceApi] sendDeviceOp op=$op sn=$sn data=$data');
|
||||
return null;
|
||||
} on ApiException catch (e) {
|
||||
debugPrint('[DeviceApi] sendDeviceOp op=$op sn=$sn failed: ${e.message}');
|
||||
return e.message;
|
||||
}
|
||||
}
|
||||
|
||||
/// 对指定应用执行操作(打开/停止/卸载/清除数据)。
|
||||
///
|
||||
/// [op] 为后端操作标识:`launch` / `stop` / `uninstall` / `clear_data`,
|
||||
/// 对应接口路径 `/api/v1/client/sn/ops/app/{op}?sn=xxx`,请求体携带应用包名。
|
||||
/// 成功返回 null;失败返回后台错误提示。
|
||||
Future<String?> sendAppOp(String sn, String op, String packageName) async {
|
||||
try {
|
||||
final data = await DioClient.post(
|
||||
'v1/client/sn/ops/app/$op',
|
||||
queryParameters: {'sn': sn},
|
||||
data: {'packageName': packageName},
|
||||
);
|
||||
debugPrint('[DeviceApi] sendAppOp op=$op sn=$sn pkg=$packageName data=$data');
|
||||
return null;
|
||||
} on ApiException catch (e) {
|
||||
debugPrint('[DeviceApi] sendAppOp op=$op sn=$sn pkg=$packageName failed: ${e.message}');
|
||||
return e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static List<T> _parseList<T>(
|
||||
dynamic data,
|
||||
T Function(Map<String, dynamic>) fromJson,
|
||||
) {
|
||||
if (data is! List) return const [];
|
||||
return data
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(fromJson)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 获取设备最近截图列表(家属端)。
|
||||
///
|
||||
/// [sn] 为设备序列号(可空,由后台按绑定关系补全)。
|
||||
/// 返回 [ScreenshotVO] 列表,并自动补全图片可访问的完整 URL(拼接 baseUrl)。
|
||||
Future<List<ScreenshotVO>> getRecentScreenshots({String? sn}) async {
|
||||
final query = sn != null && sn.isNotEmpty ? {'sn': sn} : <String, dynamic>{};
|
||||
// DioClient.get 已统一解析出响应体中的 data 字段,
|
||||
// 后台返回结构为 { code, msg, data: [...] },故此处 payload 即为截图数组。
|
||||
final payload = await DioClient.get(
|
||||
'v1/client/sn/upload-screenshot',
|
||||
queryParameters: query,
|
||||
);
|
||||
// 防御性提取:兼容后台直接返回 List,或返回 { data: [...] } 的包裹结构。
|
||||
final List<dynamic> listData;
|
||||
if (payload is List) {
|
||||
listData = payload;
|
||||
} else if (payload is Map && payload['data'] is List) {
|
||||
listData = payload['data'] as List;
|
||||
} else {
|
||||
debugPrint('[DeviceApi] getRecentScreenshots 返回非预期结构: sn=$sn payload=$payload');
|
||||
return const [];
|
||||
}
|
||||
debugPrint('[DeviceApi] getRecentScreenshots sn=$sn count=${listData.length}');
|
||||
final origin = Uri.parse(AppConstants.kBaseUrl).origin;
|
||||
return listData.whereType<Map<String, dynamic>>().map((e) {
|
||||
final vo = ScreenshotVO.fromJson(e);
|
||||
// 后台返回的 url 为相对路径(如 /static/screenshot/xxx),补全为完整地址。
|
||||
final raw = vo.url;
|
||||
final fullUrl = (raw != null && raw.startsWith('/'))
|
||||
? '$origin$raw'
|
||||
: (raw ?? '');
|
||||
return ScreenshotVO(
|
||||
id: vo.id,
|
||||
sn: vo.sn,
|
||||
fileName: vo.fileName,
|
||||
fileSize: vo.fileSize,
|
||||
url: fullUrl,
|
||||
uploadTime: vo.uploadTime,
|
||||
);
|
||||
}).toList(growable: false);
|
||||
}
|
||||
}
|
||||
61
lib/features/device/data/device_providers.dart
Normal file
61
lib/features/device/data/device_providers.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:ttstd_family_care/features/device/data/device_api.dart';
|
||||
import 'package:ttstd_family_care/features/device/data/device_repository_impl.dart';
|
||||
import 'package:ttstd_family_care/features/device/domain/device_models.dart';
|
||||
import 'package:ttstd_family_care/features/device/domain/device_repository.dart';
|
||||
|
||||
/// 设备仓储 Provider。
|
||||
final deviceRepositoryProvider = Provider<DeviceRepository>((ref) {
|
||||
const api = DeviceApi();
|
||||
return DeviceRepositoryImpl(api);
|
||||
});
|
||||
|
||||
/// 当前用户已绑定设备列表。
|
||||
final myDevicesProvider = FutureProvider<List<DeviceBrief>>((ref) async {
|
||||
final repo = ref.watch(deviceRepositoryProvider);
|
||||
return repo.getMyDevices();
|
||||
});
|
||||
|
||||
/// 当前选中的设备序列号(默认取绑定列表首个设备)。
|
||||
///
|
||||
/// 使用普通 Provider(而非 StateProvider)以便随 [myDevicesProvider] 的加载完成
|
||||
/// 自动推导出首个已绑定设备的 SN。若后续需要手动切换设备,可改为 StateProvider。
|
||||
final selectedDeviceSnProvider = Provider<String?>((ref) {
|
||||
final devices = ref.watch(myDevicesProvider);
|
||||
return devices.maybeWhen(
|
||||
data: (list) => list.isEmpty ? null : list.first.serialno,
|
||||
orElse: () => null,
|
||||
);
|
||||
});
|
||||
|
||||
/// 按 SN 获取设备基本信息。
|
||||
final deviceInfoProvider =
|
||||
FutureProvider.family<DeviceSystemInfo?, String>((ref, sn) async {
|
||||
if (sn.isEmpty) return null;
|
||||
final repo = ref.watch(deviceRepositoryProvider);
|
||||
return repo.getDeviceInfo(sn);
|
||||
});
|
||||
|
||||
/// 按 SN 获取设备最新定位信息。
|
||||
final deviceLocationProvider =
|
||||
FutureProvider.family<DeviceLocation?, String>((ref, sn) async {
|
||||
if (sn.isEmpty) return null;
|
||||
final repo = ref.watch(deviceRepositoryProvider);
|
||||
return repo.getLocation(sn);
|
||||
});
|
||||
|
||||
/// 按 SN 获取设备已安装应用列表。
|
||||
final deviceApksProvider =
|
||||
FutureProvider.family<List<DeviceApkInfo>, String>((ref, sn) async {
|
||||
if (sn.isEmpty) return const [];
|
||||
final repo = ref.watch(deviceRepositoryProvider);
|
||||
return repo.getApks(sn);
|
||||
});
|
||||
|
||||
/// 按 SN 获取设备最近截图列表。
|
||||
final recentScreenshotsProvider =
|
||||
FutureProvider.family<List<ScreenshotVO>, String>((ref, sn) async {
|
||||
if (sn.isEmpty) return const [];
|
||||
final repo = ref.watch(deviceRepositoryProvider);
|
||||
return repo.getRecentScreenshots(sn);
|
||||
});
|
||||
35
lib/features/device/data/device_repository_impl.dart
Normal file
35
lib/features/device/data/device_repository_impl.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:ttstd_family_care/features/device/data/device_api.dart';
|
||||
import 'package:ttstd_family_care/features/device/domain/device_models.dart';
|
||||
import 'package:ttstd_family_care/features/device/domain/device_repository.dart';
|
||||
|
||||
/// [DeviceRepository] 的默认实现,委托给 [DeviceApi]。
|
||||
class DeviceRepositoryImpl implements DeviceRepository {
|
||||
const DeviceRepositoryImpl(this._api);
|
||||
|
||||
final DeviceApi _api;
|
||||
|
||||
|
||||
@override
|
||||
Future<List<DeviceBrief>> getMyDevices() => _api.getMyDevices();
|
||||
|
||||
@override
|
||||
Future<DeviceSystemInfo?> getDeviceInfo(String sn) => _api.getDeviceInfo(sn);
|
||||
|
||||
@override
|
||||
Future<DeviceLocation?> getLocation(String sn) => _api.getLocation(sn);
|
||||
|
||||
@override
|
||||
Future<List<DeviceApkInfo>> getApks(String sn) => _api.getApks(sn);
|
||||
|
||||
@override
|
||||
Future<List<ScreenshotVO>> getRecentScreenshots(String sn) =>
|
||||
_api.getRecentScreenshots(sn: sn);
|
||||
|
||||
@override
|
||||
Future<String?> sendDeviceOp(String sn, String op) =>
|
||||
_api.sendDeviceOp(sn, op);
|
||||
|
||||
@override
|
||||
Future<String?> sendAppOp(String sn, String op, String packageName) =>
|
||||
_api.sendAppOp(sn, op, packageName);
|
||||
}
|
||||
Reference in New Issue
Block a user