import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; import '../core/network/api_env.dart'; import '../core/network/api_exception.dart'; import '../core/network/dio_client.dart'; import '../models/photo.dart'; final photoApiProvider = Provider((ref) => PhotoApi(ref.watch(dioProvider))); /// 照片存档接口。 /// /// App 拍完照把**原图**传上来;水印是本地合成的,和这里无关。 /// 上传是「锦上添花」—— 失败了不该影响用户继续编辑和保存照片, /// 所以调用方要自己吞掉异常并给个「重试」入口。 class PhotoApi { PhotoApi(this._dio); final Dio _dio; /// 上传一张照片,返回后端记录 id Future upload({ required String filePath, double? latitude, double? longitude, String? address, String? weather, String? remark, DateTime? takenTime, }) async { try { final form = FormData.fromMap({ 'file': await MultipartFile.fromFile( filePath, filename: _fileName(filePath), ), 'latitude': ?latitude, 'longitude': ?longitude, if (address != null && address.isNotEmpty) 'address': address, if (weather != null && weather.isNotEmpty) 'weather': weather, if (remark != null && remark.isNotEmpty) 'remark': remark, if (takenTime != null) 'takenTime': _formatTime(takenTime), }); final res = await _dio.post( '/photos', data: form, // 原图可能有好几 MB,移动网络下给宽一点 options: Options(receiveTimeout: const Duration(seconds: 60)), ); final data = res.data; if (data is Map) { final id = data['id']; if (id is int) return id; } return null; } on DioException catch (e) { final err = e.error; throw err is ApiException ? err : ApiException(-1, '照片上传失败'); } } /// 分页列表,最新的在前 Future list({int page = 1, int size = 30}) async { try { final res = await _dio.get( '/photos', queryParameters: {'page': page, 'size': size}, ); final data = res.data; if (data is Map) return PhotoPage.fromJson(data); if (data is Map) return PhotoPage.fromJson(Map.from(data)); return PhotoPage.empty; } on DioException catch (e) { final err = e.error; throw err is ApiException ? err : ApiException(-1, '照片列表加载失败'); } } /// 把原图下载到临时文件,返回本地路径。 /// /// 「重新打水印」必须先有本地文件:编辑页要读它拿尺寸、显示预览、合成成品。 /// 原图可能好几 MB,所以留了进度回调让界面能显示进度。 /// /// [url] 是后端给的相对地址(`/uploads/...`),这里用 [resolveAssetUrl] /// 补成绝对地址 —— `/uploads` 不在 `/api` 前缀下,不能直接丢给 dio。 Future downloadOriginal( String url, { void Function(int received, int total)? onProgress, }) async { try { final dir = await getTemporaryDirectory(); final path = '${dir.path}/photo_${DateTime.now().millisecondsSinceEpoch}.jpg'; await _dio.download( resolveAssetUrl(url), path, onReceiveProgress: onProgress, // 原图几 MB,移动网络下给宽一点 options: Options(receiveTimeout: const Duration(seconds: 120)), ); return path; } on DioException catch (e) { final err = e.error; throw err is ApiException ? err : ApiException(-1, '原图下载失败'); } } /// 从路径里取个带扩展名的文件名,后端靠它判断文件类型 static String _fileName(String path) { final normalized = path.replaceAll('\\', '/'); final name = normalized.substring(normalized.lastIndexOf('/') + 1); return name.isEmpty ? 'photo.jpg' : name; } static String _formatTime(DateTime time) { String two(int n) => n.toString().padLeft(2, '0'); return '${time.year}-${two(time.month)}-${two(time.day)} ' '${two(time.hour)}:${two(time.minute)}:${two(time.second)}'; } }