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

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

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