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> getMyDevices() async { try { final data = await DioClient.get( 'v1/client/sn/my-devices', ); return _parseList(data, DeviceBrief.fromJson); } on ApiException { return const []; } } /// 绑定设备(手动输入 SN 或扫码得到 SN 后调用)。 /// /// 成功返回 null;失败返回后台错误提示。 Future bindDevice(String sn, {String? remark}) async { try { final data = await DioClient.post( 'v1/client/sn/bind', queryParameters: {'sn': sn}, data: remark != null && remark.isNotEmpty ? {'remark': remark} : null, ); debugPrint('[DeviceApi] bindDevice sn=$sn data=$data'); return null; } on ApiException catch (e) { debugPrint('[DeviceApi] bindDevice sn=$sn failed: ${e.message}'); return e.message; } } /// 解绑设备。 /// /// 成功返回 null;失败返回后台错误提示。 Future unbindDevice(String sn) async { try { final data = await DioClient.post( 'v1/client/sn/unbind', queryParameters: {'sn': sn}, ); debugPrint('[DeviceApi] unbindDevice sn=$sn data=$data'); return null; } on ApiException catch (e) { debugPrint('[DeviceApi] unbindDevice sn=$sn failed: ${e.message}'); return e.message; } } /// 获取已绑定设备基本信息(无上报数据时返回 null)。 Future getDeviceInfo(String sn) async { try { final data = await DioClient.get( 'v1/client/sn/device-info', queryParameters: {'sn': sn}, ); return data is Map ? DeviceSystemInfo.fromJson(data) : null; } on ApiException { return null; } } /// 获取已绑定设备最新定位信息(无上报数据时返回 null)。 Future getLocation(String sn) async { try { final data = await DioClient.get( 'v1/client/sn/location', queryParameters: {'sn': sn}, ); return data is Map ? DeviceLocation.fromJson(data) : null; } on ApiException { return null; } } /// 获取已绑定设备已安装应用列表。 Future> 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 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 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 _parseList( dynamic data, T Function(Map) fromJson, ) { if (data is! List) return const []; return data .whereType>() .map(fromJson) .toList(growable: false); } // --------------------------------------------------------------------------- /// 获取设备最近截图列表(家属端)。 /// /// [sn] 为设备序列号(可空,由后台按绑定关系补全)。 /// 返回 [ScreenshotVO] 列表,并自动补全图片可访问的完整 URL(拼接 baseUrl)。 Future> getRecentScreenshots({String? sn}) async { final query = sn != null && sn.isNotEmpty ? {'sn': sn} : {}; // DioClient.get 已统一解析出响应体中的 data 字段, // 后台返回结构为 { code, msg, data: [...] },故此处 payload 即为截图数组。 final payload = await DioClient.get( 'v1/client/sn/upload-screenshot', queryParameters: query, ); // 防御性提取:兼容后台直接返回 List,或返回 { data: [...] } 的包裹结构。 final List 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((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); } /// 删除单张设备截图(家属端 client 接口)。 /// /// [sn] 设备序列号,[id] 截图ID。成功返回 null;失败返回后台错误提示。 Future deleteScreenshot(String sn, int id) async { try { await DioClient.delete( 'v1/client/sn/ops/screenshots/$id?sn=${Uri.encodeQueryComponent(sn)}', ); debugPrint('[DeviceApi] deleteScreenshot sn=$sn id=$id success'); return null; } on ApiException catch (e) { debugPrint('[DeviceApi] deleteScreenshot sn=$sn id=$id failed: ${e.message}'); return e.message; } } /// 清空设备全部截图(家属端 client 接口)。 /// /// [sn] 设备序列号。成功返回 null;失败返回后台错误提示。 Future clearScreenshots(String sn) async { try { await DioClient.delete( 'v1/client/sn/ops/screenshots?sn=${Uri.encodeQueryComponent(sn)}', ); debugPrint('[DeviceApi] clearScreenshots sn=$sn success'); return null; } on ApiException catch (e) { debugPrint('[DeviceApi] clearScreenshots sn=$sn failed: ${e.message}'); return e.message; } } }