feat(android): 支持沉浸式状态栏、地图定位与自定义签名

- 配置 edge-to-edge 沉浸式状态栏与刘海屏适配
- 新增百度地图权限、AK 配置及定位权限说明
- 接入 MethodChannel 实现返回键退后台热启动优化
- 配置自定义签名并更新构建逻辑
- 完善 token 刷新与鉴权失效统一处理
- 新增地图页与截图页路由
- 修复返回键拦截导致的 PopScope 失效问题
This commit is contained in:
TongTongStudio
2026-08-19 03:43:12 +08:00
parent 41d815336a
commit 06cdc77172
51 changed files with 3988 additions and 634 deletions

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