import 'package:flutter/foundation.dart'; import 'package:patbond_flutter/core/network/api_exception.dart'; import 'package:patbond_flutter/features/auth/auth_models.dart'; import 'package:patbond_flutter/features/auth/auth_repository.dart'; import 'package:patbond_flutter/features/community/community_models.dart'; import 'package:patbond_flutter/features/community/community_repository.dart'; /// 资料主链路四态(沿 pets/community 两域惯例)。 enum ProfileLoadPhase { initial, loading, ready, error } /// 统计块的独立三态:`/me` 成功而统计失败时**只降级这一块**,不把整页 /// 打成 error——昵称与头像已经拿到了,为两个数字丢掉整页是过度反应。 enum ProfileStatsPhase { loading, ready, error } /// 「我的资料」状态控制器(T3.5-08)。 /// /// 数据源三处: /// - `GET /api/v1/me` → 昵称 / 头像(主链路,失败即页面 error 态) /// - `GET /api/v1/me/community-stats` → 获赞总数 / 作品数 /// - `GET /api/v1/users/{me}/follow-stats` → 粉丝数 / 关注数 /// /// 首页问候语(T3.5-10)与资料页共用本控制器的 [displayName], /// 保证两处展示名同源、改昵称后一起变(一次 `/me` 供两个消费点)。 class ProfileController extends ChangeNotifier { ProfileController({ required AuthRepository authRepository, required CommunityRepository communityRepository, }) : _auth = authRepository, _community = communityRepository; final AuthRepository _auth; final CommunityRepository _community; ProfileLoadPhase _phase = ProfileLoadPhase.initial; ProfileStatsPhase _statsPhase = ProfileStatsPhase.loading; UserProfile? _profile; CommunityStats? _communityStats; FollowStats? _followStats; ApiException? _lastError; bool _disposed = false; ProfileLoadPhase get phase => _phase; ProfileStatsPhase get statsPhase => _statsPhase; /// 本人资料(ready 态非 null)。 UserProfile? get profile => _profile; CommunityStats? get communityStats => _communityStats; FollowStats? get followStats => _followStats; ApiException? get lastError => _lastError; /// 展示名 `nickname ?? username`;资料未到手时为 null(调用方不造假名)。 String? get displayName => _profile?.displayName; /// 加载 / 重试。主链路错误收敛为 error 态供页面渲染 + 重试,不外抛。 /// /// 已有资料副本时刷新失败**保留副本**(`_phase` 停在 ready):与详情页 /// 「有副本即不打断阅读」的既有取舍一致。 Future refresh() async { if (_profile == null) { _phase = ProfileLoadPhase.loading; } _lastError = null; _notify(); try { _profile = await _auth.me(); _phase = ProfileLoadPhase.ready; } on ApiException catch (error) { _lastError = error; if (_profile == null) { _phase = ProfileLoadPhase.error; _notify(); return; } } _notify(); await refreshStats(); } /// 只重取两块统计(统计块的「重试」入口;主链路资料不动)。 Future refreshStats() async { final userId = _profile?.userId; if (userId == null) return; _statsPhase = ProfileStatsPhase.loading; _notify(); try { // 顺序取(同一服务、量级极小):任一失败即整块 error。两个数字并列 // 在同一张卡上,只有一半是数字、另一半是「—」比整块「加载失败 + // 重试」更费解。 final community = await _community.getMyCommunityStats(); final follow = await _community.getFollowStats(userId); _communityStats = community; _followStats = follow; _statsPhase = ProfileStatsPhase.ready; } on ApiException { _statsPhase = ProfileStatsPhase.error; } _notify(); } /// 提交资料变更(三态语义见 [UpdateMeRequest])。 /// /// **空 patch 直接短路不发请求**:服务端对空 patch 答 400/40000(刻意不 /// 静默 200),客户端没有理由去撞这一枪。 /// 成功即以服务端回显的全量资料替换本地副本(`avatarUrl` 现签, /// 不做本地拼装)。失败按类型化异常外抛给编辑页分层呈现。 Future save(UpdateMeRequest request) async { if (request.isEmpty) return _profile; final updated = await _auth.updateMe(request); _profile = updated; _phase = ProfileLoadPhase.ready; _notify(); return updated; } /// 登出清空:回 initial 并清资料,避免上一账号昵称/头像跨会话泄漏 /// (沿 `PetsController.reset` 先例)。 void reset() { _phase = ProfileLoadPhase.initial; _statsPhase = ProfileStatsPhase.loading; _profile = null; _communityStats = null; _followStats = null; _lastError = null; _notify(); } void _notify() { if (!_disposed) notifyListeners(); } @override void dispose() { _disposed = true; super.dispose(); } }