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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user