import 'json_util.dart'; import 'watermark.dart'; /// 一张已上传的照片。 /// /// ⚠️ [url] 指向的是**未加水印的原图** —— 服务器只存原图,水印一直是 /// App 本地合成的。所以「重新打水印」(换颜色、换位置、补备注)随时可行。 class PhotoItem { const PhotoItem({ required this.id, required this.url, required this.thumbUrl, required this.takenAt, required this.address, required this.weather, required this.remark, required this.latitude, required this.longitude, }); final int id; /// 原图地址(相对路径,如 `/uploads/photo/2026/09/28/xxx.jpg`) final String url; /// 缩略图地址。为 null 表示后端没生成出来,客户端只能回退用原图。 final String? thumbUrl; /// 拍摄时间,已格式化;后端两个时间字段取到哪个用哪个 final String takenAt; final String address; /// 「晴 25℃」这种拼好的天气文本 final String weather; final String remark; final double? latitude; final double? longitude; factory PhotoItem.fromJson(Map json) { // 拍摄时间是客户端传的,可能为空(格式没对上时后端会存 null), // 那就退回记录创建时间 —— 至少能显示个日期 final taken = formatDateTime(json['takenTime']); final created = formatDateTime(json['createTime']); return PhotoItem( id: asInt(json['id']), url: asString(json['url']), thumbUrl: asString(json['thumbUrl']).isEmpty ? null : asString(json['thumbUrl']), takenAt: taken.isNotEmpty ? taken : created, address: asString(json['address']), weather: asString(json['weather']), remark: asString(json['remark']), latitude: asNullableDouble(json['latitude']), longitude: asNullableDouble(json['longitude']), ); } /// 还原成水印数据,「重新打水印」时用。 /// /// 关键点是**复用这张照片当时的地址和天气**,而不是拿当前位置 —— /// 不然给三年前的照片重新加个水印,地点会变成「你现在站的地方」。 WatermarkData toWatermarkData() { final split = splitWeatherText(weather); return WatermarkData( time: parsePhotoTime(takenAt), address: address, weather: split.weather, temperature: split.temperature, remark: remark, ); } } /// 把后端存的 `yyyy-MM-dd HH:mm` 解析回 DateTime;解析不了就用当前时间 DateTime parsePhotoTime(String value) { final normalized = value.trim().replaceFirst(' ', 'T'); return DateTime.tryParse(normalized) ?? DateTime.now(); } /// 拆开后端存的天气文本(「晴 25℃」→ weather=晴, temperature=25℃)。 /// /// 存的时候是一个字段(上传时客户端就是这么拼的),所以这里拆回来。 /// 两种情况都要兜住:只有天气没有温度(「晴」)、只有温度没有天气(「25℃」)。 ({String weather, String temperature}) splitWeatherText(String combined) { final text = combined.trim(); if (text.isEmpty) return (weather: '', temperature: ''); final parts = text.split(' '); if (parts.last.endsWith('℃')) { return ( weather: parts.sublist(0, parts.length - 1).join(' '), temperature: parts.last, ); } return (weather: text, temperature: ''); } /// 照片分页 class PhotoPage { const PhotoPage({ required this.items, required this.total, required this.hasMore, }); final List items; final int total; final bool hasMore; static const empty = PhotoPage(items: [], total: 0, hasMore: false); factory PhotoPage.fromJson(Map json) { return PhotoPage( items: asMapList(json['list']).map(PhotoItem.fromJson).toList(), total: asInt(json['total']), hasMore: json['hasMore'] == true, ); } }