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:
parent
e8e0c6db86
commit
3e01f69044
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
/// 构建头像内容(优先本地文件,其次网络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!,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
/// 构建头像内容(优先本地文件,其次网络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!,
|
||||
|
|
|
|||
Loading…
Reference in New Issue