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:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import '../network/api_client.dart';
|
import '../network/api_client.dart';
|
||||||
import '../storage/secure_storage.dart';
|
import '../storage/secure_storage.dart';
|
||||||
|
|
@ -923,6 +924,14 @@ class AccountService {
|
||||||
await _secureStorage.write(key: StorageKeys.avatarUrl, value: newAvatarUrl);
|
await _secureStorage.write(key: StorageKeys.avatarUrl, value: newAvatarUrl);
|
||||||
debugPrint('$_tag uploadAvatar() - 本地存储已更新');
|
debugPrint('$_tag uploadAvatar() - 本地存储已更新');
|
||||||
|
|
||||||
|
// 将图片复制到本地缓存目录
|
||||||
|
try {
|
||||||
|
final localPath = await _saveAvatarToLocal(imageFile);
|
||||||
|
debugPrint('$_tag uploadAvatar() - 头像已保存到本地: $localPath');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('$_tag uploadAvatar() - 保存本地头像失败 (不影响上传): $e');
|
||||||
|
}
|
||||||
|
|
||||||
return newAvatarUrl;
|
return newAvatarUrl;
|
||||||
} on ApiException catch (e) {
|
} on ApiException catch (e) {
|
||||||
debugPrint('$_tag uploadAvatar() - API 异常: $e');
|
debugPrint('$_tag uploadAvatar() - API 异常: $e');
|
||||||
|
|
@ -963,7 +972,126 @@ class AccountService {
|
||||||
/// 登出
|
/// 登出
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
debugPrint('$_tag logout() - 开始登出,清除所有数据');
|
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();
|
await _secureStorage.deleteAll();
|
||||||
debugPrint('$_tag logout() - 登出完成');
|
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/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
@ -27,6 +28,7 @@ class _MiningPageState extends ConsumerState<MiningPage> {
|
||||||
String _serialNumber = '--';
|
String _serialNumber = '--';
|
||||||
String? _avatarSvg;
|
String? _avatarSvg;
|
||||||
String? _avatarUrl;
|
String? _avatarUrl;
|
||||||
|
String? _localAvatarPath; // 本地头像文件路径
|
||||||
final String _community = '星空社区';
|
final String _community = '星空社区';
|
||||||
final String _province = '广东';
|
final String _province = '广东';
|
||||||
final String _city = '深圳';
|
final String _city = '深圳';
|
||||||
|
|
@ -44,12 +46,30 @@ class _MiningPageState extends ConsumerState<MiningPage> {
|
||||||
final serialNum = await accountService.getUserSerialNum();
|
final serialNum = await accountService.getUserSerialNum();
|
||||||
final avatarSvg = await accountService.getAvatarSvg();
|
final avatarSvg = await accountService.getAvatarSvg();
|
||||||
final avatarUrl = await accountService.getAvatarUrl();
|
final avatarUrl = await accountService.getAvatarUrl();
|
||||||
|
final localAvatarPath = await accountService.getLocalAvatarPath();
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_serialNumber = serialNum?.toString() ?? '--';
|
_serialNumber = serialNum?.toString() ?? '--';
|
||||||
_avatarSvg = avatarSvg;
|
_avatarSvg = avatarSvg;
|
||||||
_avatarUrl = avatarUrl;
|
_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() {
|
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) {
|
if (_avatarUrl != null && _avatarUrl!.isNotEmpty) {
|
||||||
return Image.network(
|
return Image.network(
|
||||||
_avatarUrl!,
|
_avatarUrl!,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
@ -22,6 +23,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
String _serialNumber = '--';
|
String _serialNumber = '--';
|
||||||
String? _avatarSvg;
|
String? _avatarSvg;
|
||||||
String? _avatarUrl;
|
String? _avatarUrl;
|
||||||
|
String? _localAvatarPath; // 本地头像文件路径
|
||||||
final String _referrerSerial = '87654321';
|
final String _referrerSerial = '87654321';
|
||||||
final String _community = '星空社区';
|
final String _community = '星空社区';
|
||||||
final String _parentCommunity = '银河社区';
|
final String _parentCommunity = '银河社区';
|
||||||
|
|
@ -73,6 +75,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
final serialNum = await accountService.getUserSerialNum();
|
final serialNum = await accountService.getUserSerialNum();
|
||||||
final avatarSvg = await accountService.getAvatarSvg();
|
final avatarSvg = await accountService.getAvatarSvg();
|
||||||
final avatarUrl = await accountService.getAvatarUrl();
|
final avatarUrl = await accountService.getAvatarUrl();
|
||||||
|
final localAvatarPath = await accountService.getLocalAvatarPath();
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -80,6 +83,23 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
_serialNumber = serialNum?.toString() ?? '--';
|
_serialNumber = serialNum?.toString() ?? '--';
|
||||||
_avatarSvg = avatarSvg;
|
_avatarSvg = avatarSvg;
|
||||||
_avatarUrl = avatarUrl;
|
_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() {
|
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) {
|
if (_avatarUrl != null && _avatarUrl!.isNotEmpty) {
|
||||||
return Image.network(
|
return Image.network(
|
||||||
_avatarUrl!,
|
_avatarUrl!,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue