125 lines
3.9 KiB
Dart
125 lines
3.9 KiB
Dart
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<String, dynamic> 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<PhotoItem> items;
|
||
final int total;
|
||
final bool hasMore;
|
||
|
||
static const empty = PhotoPage(items: [], total: 0, hasMore: false);
|
||
|
||
factory PhotoPage.fromJson(Map<String, dynamic> json) {
|
||
return PhotoPage(
|
||
items: asMapList(json['list']).map(PhotoItem.fromJson).toList(),
|
||
total: asInt(json['total']),
|
||
hasMore: json['hasMore'] == true,
|
||
);
|
||
}
|
||
}
|