fix(mobile-app): cache avatar locally instead of always loading from network

- 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 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-08 12:23:50 -08:00
parent e8e0c6db86
commit 3e01f69044
3 changed files with 214 additions and 4 deletions

View File

@ -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<void> 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<String?> 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<String> _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<String?> 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;
}
}
}

View File

@ -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<MiningPage> {
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<MiningPage> {
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<void> _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<MiningPage> {
);
}
/// URL和SVG
/// URLSVG
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!,

View File

@ -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<ProfilePage> {
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<ProfilePage> {
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<ProfilePage> {
_serialNumber = serialNum?.toString() ?? '--';
_avatarSvg = avatarSvg;
_avatarUrl = avatarUrl;
_localAvatarPath = localAvatarPath;
});
// URL但没有本地缓存
if (avatarUrl != null && avatarUrl.isNotEmpty && localAvatarPath == null) {
_downloadAndCacheAvatar(avatarUrl);
}
}
}
///
Future<void> _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<ProfilePage> {
);
}
/// URL和SVG
/// URLSVG
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!,