97a1f46e3f
CI / flutter-gates (push) Successful in 1m31s
- 档案 Tab 替换为真实宠物列表(05 号规范 P1):PetsController 四态 驱动(loading/empty/error+重试/ready),空态插画 + 建档 CTA, 下拉刷新,虚线添加卡;页面一律走 T2-11 数据层、不直连 ApiClient - 新增 PetDetailPage(P2 档案信息部分):内存副本首屏 + getPet 刷新, 40401 防枚举 → 不存在态返回列表并刷新;仅 owner 显示编辑入口 (40300 语义);体重/疫苗/时间线区块留待 T2-13/14 接摘要与记录接口 - 新增 PetFormPage(建档/编辑):品种目录接口 + 自定义品种互斥 (目录失败回落自定义 + 重试)、性别契约必填、生日+估算、芯片号; 失焦+提交双校验、错误三层模型沿用登录纵切 - 40902 版本冲突:明确提示 + 自动拉取最新 version 重提路径(有测试); 40903 芯片号冲突字段级报错(有测试) - 埋点挂接:pet_create_started(表单首次输入、每次进入一次)/ succeeded / failed 三事件接线;建宠表单路由名 pet_form、详情页 pet_detail 进入既有 RouteObserver 采集;档案 Tab 页名 pet_archive 改报 pet_list(字典 v2);编辑表单不带路由名(漏斗到达段不掺编辑) - app.dart 装配:pet 服务 ApiClient(:8083)共享 TokenRefresher; 登出 reset 控制器防跨账号泄漏;PetsController.loadBreeds 按物种缓存 - AppState 清理:pets 页原 demo 消费方移除,vaccines 及其模型/持久化 删除;pet demo 仅存首页问候/创作页/主壳头像(后续工单收敛) - 头像按 ADR-010 本地占位,不做上传 - 测试 145 → 177 全绿(列表/详情/表单四态与冲突路径 widget 测试、 控制器 loadBreeds/reset、展示纯函数);analyze 0;format 无 diff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
298 lines
7.6 KiB
Dart
298 lines
7.6 KiB
Dart
enum PetGender { male, female }
|
|
|
|
enum WeatherCondition { sunny, cloudy, rainy, overcast, snow }
|
|
|
|
class LocationWeather {
|
|
const LocationWeather({
|
|
required this.city,
|
|
required this.district,
|
|
required this.temperature,
|
|
required this.condition,
|
|
required this.conditionText,
|
|
required this.highTemperature,
|
|
required this.lowTemperature,
|
|
required this.humidity,
|
|
});
|
|
|
|
final String city;
|
|
final String district;
|
|
final int temperature;
|
|
final WeatherCondition condition;
|
|
final String conditionText;
|
|
final int highTemperature;
|
|
final int lowTemperature;
|
|
final int humidity;
|
|
|
|
String get displayArea => district.isEmpty ? city : '$city · $district';
|
|
|
|
String get petAdvice {
|
|
if (condition == WeatherCondition.rainy) {
|
|
return '今天有雨,外出记得带雨具并及时擦干脚掌。';
|
|
}
|
|
if (condition == WeatherCondition.snow) {
|
|
return '天气寒冷,短毛宠物外出记得做好保暖。';
|
|
}
|
|
if (temperature >= 30) {
|
|
return '今天较热,建议清晨或傍晚带毛孩子出门。';
|
|
}
|
|
if (temperature <= 8) {
|
|
return '气温偏低,散步时间不宜过长,注意保暖。';
|
|
}
|
|
if (condition == WeatherCondition.sunny) {
|
|
return '天气不错,适合和毛孩子一起去户外散步。';
|
|
}
|
|
return '体感舒适,适合安排一次轻松的户外活动。';
|
|
}
|
|
|
|
factory LocationWeather.fromJson(Map<String, dynamic> json) {
|
|
return LocationWeather(
|
|
city: json['city'] as String,
|
|
district: json['district'] as String? ?? '',
|
|
temperature: json['temperature'] as int,
|
|
condition: WeatherCondition.values.firstWhere(
|
|
(value) => value.name == json['condition'],
|
|
orElse: () => WeatherCondition.sunny,
|
|
),
|
|
conditionText: json['conditionText'] as String,
|
|
highTemperature: json['highTemperature'] as int,
|
|
lowTemperature: json['lowTemperature'] as int,
|
|
humidity: json['humidity'] as int,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'city': city,
|
|
'district': district,
|
|
'temperature': temperature,
|
|
'condition': condition.name,
|
|
'conditionText': conditionText,
|
|
'highTemperature': highTemperature,
|
|
'lowTemperature': lowTemperature,
|
|
'humidity': humidity,
|
|
};
|
|
}
|
|
|
|
class PetProfile {
|
|
const PetProfile({
|
|
required this.name,
|
|
required this.breed,
|
|
required this.gender,
|
|
required this.birthday,
|
|
required this.weight,
|
|
required this.tags,
|
|
required this.avatarUrl,
|
|
});
|
|
|
|
final String name;
|
|
final String breed;
|
|
final PetGender gender;
|
|
final String birthday;
|
|
final double weight;
|
|
final List<String> tags;
|
|
final String avatarUrl;
|
|
|
|
PetProfile copyWith({
|
|
String? name,
|
|
String? breed,
|
|
PetGender? gender,
|
|
String? birthday,
|
|
double? weight,
|
|
List<String>? tags,
|
|
String? avatarUrl,
|
|
}) {
|
|
return PetProfile(
|
|
name: name ?? this.name,
|
|
breed: breed ?? this.breed,
|
|
gender: gender ?? this.gender,
|
|
birthday: birthday ?? this.birthday,
|
|
weight: weight ?? this.weight,
|
|
tags: tags ?? this.tags,
|
|
avatarUrl: avatarUrl ?? this.avatarUrl,
|
|
);
|
|
}
|
|
|
|
factory PetProfile.fromJson(Map<String, dynamic> json) {
|
|
return PetProfile(
|
|
name: json['name'] as String,
|
|
breed: json['breed'] as String,
|
|
gender: json['gender'] == 'female' ? PetGender.female : PetGender.male,
|
|
birthday: json['birthday'] as String,
|
|
weight: (json['weight'] as num).toDouble(),
|
|
tags: List<String>.from(json['tags'] as List),
|
|
avatarUrl: json['avatarUrl'] as String,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'name': name,
|
|
'breed': breed,
|
|
'gender': gender.name,
|
|
'birthday': birthday,
|
|
'weight': weight,
|
|
'tags': tags,
|
|
'avatarUrl': avatarUrl,
|
|
};
|
|
}
|
|
|
|
class CommentModel {
|
|
const CommentModel({
|
|
required this.id,
|
|
required this.authorName,
|
|
required this.authorAvatar,
|
|
required this.content,
|
|
required this.time,
|
|
});
|
|
|
|
final String id;
|
|
final String authorName;
|
|
final String authorAvatar;
|
|
final String content;
|
|
final String time;
|
|
|
|
factory CommentModel.fromJson(Map<String, dynamic> json) {
|
|
return CommentModel(
|
|
id: json['id'] as String,
|
|
authorName: json['authorName'] as String,
|
|
authorAvatar: json['authorAvatar'] as String,
|
|
content: json['content'] as String,
|
|
time: json['time'] as String,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'id': id,
|
|
'authorName': authorName,
|
|
'authorAvatar': authorAvatar,
|
|
'content': content,
|
|
'time': time,
|
|
};
|
|
}
|
|
|
|
class PostModel {
|
|
const PostModel({
|
|
required this.id,
|
|
required this.authorName,
|
|
required this.authorAvatar,
|
|
required this.time,
|
|
required this.breedTag,
|
|
required this.content,
|
|
required this.mainImage,
|
|
required this.likes,
|
|
required this.tags,
|
|
required this.comments,
|
|
this.hasLiked = false,
|
|
this.hasBookmarked = false,
|
|
});
|
|
|
|
final String id;
|
|
final String authorName;
|
|
final String authorAvatar;
|
|
final String time;
|
|
final String breedTag;
|
|
final String content;
|
|
final String mainImage;
|
|
final int likes;
|
|
final List<String> tags;
|
|
final List<CommentModel> comments;
|
|
final bool hasLiked;
|
|
final bool hasBookmarked;
|
|
|
|
PostModel copyWith({
|
|
int? likes,
|
|
List<CommentModel>? comments,
|
|
bool? hasLiked,
|
|
bool? hasBookmarked,
|
|
}) {
|
|
return PostModel(
|
|
id: id,
|
|
authorName: authorName,
|
|
authorAvatar: authorAvatar,
|
|
time: time,
|
|
breedTag: breedTag,
|
|
content: content,
|
|
mainImage: mainImage,
|
|
likes: likes ?? this.likes,
|
|
tags: tags,
|
|
comments: comments ?? this.comments,
|
|
hasLiked: hasLiked ?? this.hasLiked,
|
|
hasBookmarked: hasBookmarked ?? this.hasBookmarked,
|
|
);
|
|
}
|
|
|
|
factory PostModel.fromJson(Map<String, dynamic> json) {
|
|
return PostModel(
|
|
id: json['id'] as String,
|
|
authorName: json['authorName'] as String,
|
|
authorAvatar: json['authorAvatar'] as String,
|
|
time: json['time'] as String,
|
|
breedTag: json['breedTag'] as String,
|
|
content: json['content'] as String,
|
|
mainImage: json['mainImage'] as String,
|
|
likes: json['likes'] as int,
|
|
tags: List<String>.from(json['tags'] as List),
|
|
comments: (json['comments'] as List)
|
|
.map((item) => CommentModel.fromJson(item as Map<String, dynamic>))
|
|
.toList(),
|
|
hasLiked: json['hasLiked'] as bool? ?? false,
|
|
hasBookmarked: json['hasBookmarked'] as bool? ?? false,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'id': id,
|
|
'authorName': authorName,
|
|
'authorAvatar': authorAvatar,
|
|
'time': time,
|
|
'breedTag': breedTag,
|
|
'content': content,
|
|
'mainImage': mainImage,
|
|
'likes': likes,
|
|
'tags': tags,
|
|
'comments': comments.map((comment) => comment.toJson()).toList(),
|
|
'hasLiked': hasLiked,
|
|
'hasBookmarked': hasBookmarked,
|
|
};
|
|
}
|
|
|
|
enum ProviderKind { hospital, grooming, personal }
|
|
|
|
class ServiceProviderModel {
|
|
const ServiceProviderModel({
|
|
required this.id,
|
|
required this.kind,
|
|
required this.name,
|
|
required this.distance,
|
|
required this.rating,
|
|
required this.description,
|
|
required this.tags,
|
|
required this.price,
|
|
required this.image,
|
|
this.verified = false,
|
|
});
|
|
|
|
final String id;
|
|
final ProviderKind kind;
|
|
final String name;
|
|
final String distance;
|
|
final double rating;
|
|
final String description;
|
|
final List<String> tags;
|
|
final int price;
|
|
final String image;
|
|
final bool verified;
|
|
}
|
|
|
|
class CreationStyle {
|
|
const CreationStyle({
|
|
required this.id,
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.image,
|
|
});
|
|
|
|
final String id;
|
|
final String title;
|
|
final String subtitle;
|
|
final String image;
|
|
}
|