From 3e01f69044709d49ca8d6c01a1b6d1e5dcad759e Mon Sep 17 00:00:00 2001 From: hailin Date: Mon, 8 Dec 2025 12:23:50 -0800 Subject: [PATCH] fix(mobile-app): cache avatar locally instead of always loading from network MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add local avatar caching in AccountService: - getLocalAvatarPath(): get cached avatar file path - _saveAvatarToLocal(): save avatar to local after upload - downloadAndCacheAvatar(): download and cache remote avatar - Update profile_page and mining_page to use local avatar: - Priority: local file > network URL > SVG > default icon - Background download and cache if remote URL exists but no local cache - Clean up local avatar file on logout 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../lib/core/services/account_service.dart | 128 ++++++++++++++++++ .../presentation/pages/mining_page.dart | 45 +++++- .../presentation/pages/profile_page.dart | 45 +++++- 3 files changed, 214 insertions(+), 4 deletions(-) diff --git a/frontend/mobile-app/lib/core/services/account_service.dart b/frontend/mobile-app/lib/core/services/account_service.dart index 7e462b70..3a11de8c 100644 --- a/frontend/mobile-app/lib/core/services/account_service.dart +++ b/frontend/mobile-app/lib/core/services/account_service.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; import '../network/api_client.dart'; import '../storage/secure_storage.dart'; @@ -923,6 +924,14 @@ class AccountService { await _secureStorage.write(key: StorageKeys.avatarUrl, value: newAvatarUrl); debugPrint('$_tag uploadAvatar() - 本地存储已更新'); + // 将图片复制到本地缓存目录 + try { + final localPath = await _saveAvatarToLocal(imageFile); + debugPrint('$_tag uploadAvatar() - 头像已保存到本地: $localPath'); + } catch (e) { + debugPrint('$_tag uploadAvatar() - 保存本地头像失败 (不影响上传): $e'); + } + return newAvatarUrl; } on ApiException catch (e) { debugPrint('$_tag uploadAvatar() - API 异常: $e'); @@ -963,7 +972,126 @@ class AccountService { /// 登出 Future logout() async { debugPrint('$_tag logout() - 开始登出,清除所有数据'); + // 删除本地头像文件 + try { + final localAvatarPath = await getLocalAvatarPath(); + if (localAvatarPath != null) { + final file = File(localAvatarPath); + if (await file.exists()) { + await file.delete(); + debugPrint('$_tag logout() - 已删除本地头像文件'); + } + } + } catch (e) { + debugPrint('$_tag logout() - 删除本地头像失败: $e'); + } await _secureStorage.deleteAll(); debugPrint('$_tag logout() - 登出完成'); } + + // ==================== 本地头像管理 ==================== + + /// 获取本地头像文件路径 + /// 返回本地头像的完整路径,如果不存在则返回 null + Future getLocalAvatarPath() async { + debugPrint('$_tag getLocalAvatarPath() - 获取本地头像路径'); + try { + final directory = await getApplicationDocumentsDirectory(); + final avatarDir = Directory('${directory.path}/avatars'); + + if (!await avatarDir.exists()) { + debugPrint('$_tag getLocalAvatarPath() - 头像目录不存在'); + return null; + } + + // 查找头像文件(支持多种格式) + final files = await avatarDir.list().toList(); + for (final file in files) { + if (file is File && file.path.contains('avatar.')) { + debugPrint('$_tag getLocalAvatarPath() - 找到本地头像: ${file.path}'); + return file.path; + } + } + + debugPrint('$_tag getLocalAvatarPath() - 未找到本地头像'); + return null; + } catch (e) { + debugPrint('$_tag getLocalAvatarPath() - 获取路径失败: $e'); + return null; + } + } + + /// 保存头像到本地 + /// [imageFile] - 图片文件 + /// 返回保存后的本地路径 + Future _saveAvatarToLocal(File imageFile) async { + debugPrint('$_tag _saveAvatarToLocal() - 保存头像到本地'); + + final directory = await getApplicationDocumentsDirectory(); + final avatarDir = Directory('${directory.path}/avatars'); + + // 创建头像目录 + if (!await avatarDir.exists()) { + await avatarDir.create(recursive: true); + debugPrint('$_tag _saveAvatarToLocal() - 创建头像目录: ${avatarDir.path}'); + } + + // 删除旧的头像文件 + final files = await avatarDir.list().toList(); + for (final file in files) { + if (file is File && file.path.contains('avatar.')) { + await file.delete(); + debugPrint('$_tag _saveAvatarToLocal() - 删除旧头像: ${file.path}'); + } + } + + // 获取文件扩展名 + final extension = imageFile.path.split('.').last.toLowerCase(); + final localPath = '${avatarDir.path}/avatar.$extension'; + + // 复制文件 + await imageFile.copy(localPath); + debugPrint('$_tag _saveAvatarToLocal() - 头像已保存: $localPath'); + + return localPath; + } + + /// 从网络下载头像并保存到本地 + /// [url] - 头像 URL + /// 返回保存后的本地路径 + Future downloadAndCacheAvatar(String url) async { + debugPrint('$_tag downloadAndCacheAvatar() - 下载头像: $url'); + + try { + final directory = await getApplicationDocumentsDirectory(); + final avatarDir = Directory('${directory.path}/avatars'); + + // 创建头像目录 + if (!await avatarDir.exists()) { + await avatarDir.create(recursive: true); + } + + // 从 URL 获取扩展名 + String extension = 'jpg'; + if (url.contains('.png')) { + extension = 'png'; + } else if (url.contains('.gif')) { + extension = 'gif'; + } else if (url.contains('.webp')) { + extension = 'webp'; + } + + final localPath = '${avatarDir.path}/avatar.$extension'; + + // 使用 Dio 下载文件 + final dio = Dio(); + await dio.download(url, localPath); + + debugPrint('$_tag downloadAndCacheAvatar() - 头像已下载到: $localPath'); + return localPath; + } catch (e) { + debugPrint('$_tag downloadAndCacheAvatar() - 下载失败: $e'); + return null; + } + } } diff --git a/frontend/mobile-app/lib/features/mining/presentation/pages/mining_page.dart b/frontend/mobile-app/lib/features/mining/presentation/pages/mining_page.dart index 7bc98ab2..2c0d8787 100644 --- a/frontend/mobile-app/lib/features/mining/presentation/pages/mining_page.dart +++ b/frontend/mobile-app/lib/features/mining/presentation/pages/mining_page.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -27,6 +28,7 @@ class _MiningPageState extends ConsumerState { String _serialNumber = '--'; String? _avatarSvg; String? _avatarUrl; + String? _localAvatarPath; // 本地头像文件路径 final String _community = '星空社区'; final String _province = '广东'; final String _city = '深圳'; @@ -44,12 +46,30 @@ class _MiningPageState extends ConsumerState { final serialNum = await accountService.getUserSerialNum(); final avatarSvg = await accountService.getAvatarSvg(); final avatarUrl = await accountService.getAvatarUrl(); + final localAvatarPath = await accountService.getLocalAvatarPath(); if (mounted) { setState(() { _serialNumber = serialNum?.toString() ?? '--'; _avatarSvg = avatarSvg; _avatarUrl = avatarUrl; + _localAvatarPath = localAvatarPath; + }); + + // 如果有远程URL但没有本地缓存,后台下载并缓存 + if (avatarUrl != null && avatarUrl.isNotEmpty && localAvatarPath == null) { + _downloadAndCacheAvatar(avatarUrl); + } + } + } + + /// 后台下载并缓存头像 + Future _downloadAndCacheAvatar(String url) async { + final accountService = ref.read(accountServiceProvider); + final localPath = await accountService.downloadAndCacheAvatar(url); + if (mounted && localPath != null) { + setState(() { + _localAvatarPath = localPath; }); } } @@ -361,9 +381,30 @@ class _MiningPageState extends ConsumerState { ); } - /// 构建头像内容(支持网络URL和SVG) + /// 构建头像内容(优先本地文件,其次网络URL,最后SVG) Widget _buildAvatarContent() { - // 优先显示已上传的网络图片URL + // 1. 优先显示本地缓存的头像文件 + if (_localAvatarPath != null && _localAvatarPath!.isNotEmpty) { + final file = File(_localAvatarPath!); + return Image.file( + file, + width: 80, + height: 80, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + // 本地文件加载失败,尝试网络URL + return _buildNetworkOrSvgAvatar(); + }, + ); + } + + // 2. 没有本地缓存,尝试网络URL或SVG + return _buildNetworkOrSvgAvatar(); + } + + /// 构建网络URL或SVG头像 + Widget _buildNetworkOrSvgAvatar() { + // 尝试显示网络图片URL if (_avatarUrl != null && _avatarUrl!.isNotEmpty) { return Image.network( _avatarUrl!, diff --git a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart index 19118f41..c45b84de 100644 --- a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart +++ b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -22,6 +23,7 @@ class _ProfilePageState extends ConsumerState { String _serialNumber = '--'; String? _avatarSvg; String? _avatarUrl; + String? _localAvatarPath; // 本地头像文件路径 final String _referrerSerial = '87654321'; final String _community = '星空社区'; final String _parentCommunity = '银河社区'; @@ -73,6 +75,7 @@ class _ProfilePageState extends ConsumerState { final serialNum = await accountService.getUserSerialNum(); final avatarSvg = await accountService.getAvatarSvg(); final avatarUrl = await accountService.getAvatarUrl(); + final localAvatarPath = await accountService.getLocalAvatarPath(); if (mounted) { setState(() { @@ -80,6 +83,23 @@ class _ProfilePageState extends ConsumerState { _serialNumber = serialNum?.toString() ?? '--'; _avatarSvg = avatarSvg; _avatarUrl = avatarUrl; + _localAvatarPath = localAvatarPath; + }); + + // 如果有远程URL但没有本地缓存,后台下载并缓存 + if (avatarUrl != null && avatarUrl.isNotEmpty && localAvatarPath == null) { + _downloadAndCacheAvatar(avatarUrl); + } + } + } + + /// 后台下载并缓存头像 + Future _downloadAndCacheAvatar(String url) async { + final accountService = ref.read(accountServiceProvider); + final localPath = await accountService.downloadAndCacheAvatar(url); + if (mounted && localPath != null) { + setState(() { + _localAvatarPath = localPath; }); } } @@ -340,9 +360,30 @@ class _ProfilePageState extends ConsumerState { ); } - /// 构建头像内容(支持网络URL和SVG) + /// 构建头像内容(优先本地文件,其次网络URL,最后SVG) Widget _buildAvatarContent() { - // 优先显示已上传的网络图片URL + // 1. 优先显示本地缓存的头像文件 + if (_localAvatarPath != null && _localAvatarPath!.isNotEmpty) { + final file = File(_localAvatarPath!); + return Image.file( + file, + width: 80, + height: 80, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + // 本地文件加载失败,尝试网络URL + return _buildNetworkOrSvgAvatar(); + }, + ); + } + + // 2. 没有本地缓存,尝试网络URL或SVG + return _buildNetworkOrSvgAvatar(); + } + + /// 构建网络URL或SVG头像 + Widget _buildNetworkOrSvgAvatar() { + // 尝试显示网络图片URL if (_avatarUrl != null && _avatarUrl!.isNotEmpty) { return Image.network( _avatarUrl!,