Files
lixi 97a1f46e3f
CI / flutter-gates (push) Successful in 1m31s
重构:宠物列表/详情/编辑页接入真实数据(T2-12,四态齐备)
- 档案 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>
2026-09-08 12:15:32 +08:00

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;
}