feat(account): 实现多账号管理功能
- 新增 MultiAccountService 处理多账号存储和切换 - 新增 AccountSwitchPage 账号切换页面 - 修改 storage_keys.dart 添加多账号相关键和前缀方法 - 修改 ProfilePage 切换账号按钮跳转到账号切换页面 - 修改退出登录逻辑,保留账号数据只清除会话状态 - 新账号创建时自动添加到账号列表 - 支持旧单账号数据自动迁移到多账号架构 - 支持删除账号(带确认对话框) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
04644ea3f8
commit
f3a475a6f4
|
|
@ -3,6 +3,7 @@ import '../storage/secure_storage.dart';
|
||||||
import '../storage/local_storage.dart';
|
import '../storage/local_storage.dart';
|
||||||
import '../network/api_client.dart';
|
import '../network/api_client.dart';
|
||||||
import '../services/account_service.dart';
|
import '../services/account_service.dart';
|
||||||
|
import '../services/multi_account_service.dart';
|
||||||
import '../services/referral_service.dart';
|
import '../services/referral_service.dart';
|
||||||
import '../services/authorization_service.dart';
|
import '../services/authorization_service.dart';
|
||||||
import '../services/deposit_service.dart';
|
import '../services/deposit_service.dart';
|
||||||
|
|
@ -36,6 +37,12 @@ final accountServiceProvider = Provider<AccountService>((ref) {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Multi Account Service Provider
|
||||||
|
final multiAccountServiceProvider = Provider<MultiAccountService>((ref) {
|
||||||
|
final secureStorage = ref.watch(secureStorageProvider);
|
||||||
|
return MultiAccountService(secureStorage);
|
||||||
|
});
|
||||||
|
|
||||||
// Referral Service Provider
|
// Referral Service Provider
|
||||||
final referralServiceProvider = Provider<ReferralService>((ref) {
|
final referralServiceProvider = Provider<ReferralService>((ref) {
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,349 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import '../storage/secure_storage.dart';
|
||||||
|
import '../storage/storage_keys.dart';
|
||||||
|
|
||||||
|
/// 账号信息摘要(用于账号列表显示)
|
||||||
|
class AccountSummary {
|
||||||
|
final String userSerialNum; // 用户序列号,作为唯一标识
|
||||||
|
final String username; // 用户名
|
||||||
|
final String? avatarSvg; // SVG 头像
|
||||||
|
final String? avatarUrl; // 自定义头像 URL
|
||||||
|
final DateTime createdAt; // 创建时间
|
||||||
|
|
||||||
|
AccountSummary({
|
||||||
|
required this.userSerialNum,
|
||||||
|
required this.username,
|
||||||
|
this.avatarSvg,
|
||||||
|
this.avatarUrl,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'userSerialNum': userSerialNum,
|
||||||
|
'username': username,
|
||||||
|
'avatarSvg': avatarSvg,
|
||||||
|
'avatarUrl': avatarUrl,
|
||||||
|
'createdAt': createdAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
factory AccountSummary.fromJson(Map<String, dynamic> json) {
|
||||||
|
return AccountSummary(
|
||||||
|
userSerialNum: json['userSerialNum'] as String,
|
||||||
|
username: json['username'] as String,
|
||||||
|
avatarSvg: json['avatarSvg'] as String?,
|
||||||
|
avatarUrl: json['avatarUrl'] as String?,
|
||||||
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'AccountSummary(userSerialNum: $userSerialNum, username: $username)';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 多账号管理服务
|
||||||
|
/// 负责管理多个账号的存储和切换
|
||||||
|
class MultiAccountService {
|
||||||
|
static const String _tag = '[MultiAccountService]';
|
||||||
|
|
||||||
|
final SecureStorage _secureStorage;
|
||||||
|
|
||||||
|
MultiAccountService(this._secureStorage);
|
||||||
|
|
||||||
|
/// 获取账号列表
|
||||||
|
Future<List<AccountSummary>> getAccountList() async {
|
||||||
|
debugPrint('$_tag getAccountList() - 获取账号列表');
|
||||||
|
try {
|
||||||
|
final jsonStr = await _secureStorage.read(key: StorageKeys.accountList);
|
||||||
|
if (jsonStr == null || jsonStr.isEmpty) {
|
||||||
|
debugPrint('$_tag getAccountList() - 账号列表为空');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final List<dynamic> jsonList = jsonDecode(jsonStr);
|
||||||
|
final accounts = jsonList
|
||||||
|
.map((e) => AccountSummary.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
debugPrint('$_tag getAccountList() - 找到 ${accounts.length} 个账号');
|
||||||
|
return accounts;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('$_tag getAccountList() - 解析失败: $e');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保存账号列表
|
||||||
|
Future<void> _saveAccountList(List<AccountSummary> accounts) async {
|
||||||
|
final jsonStr = jsonEncode(accounts.map((e) => e.toJson()).toList());
|
||||||
|
await _secureStorage.write(key: StorageKeys.accountList, value: jsonStr);
|
||||||
|
debugPrint('$_tag _saveAccountList() - 保存 ${accounts.length} 个账号');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 添加账号到列表
|
||||||
|
Future<void> addAccount(AccountSummary account) async {
|
||||||
|
debugPrint('$_tag addAccount() - 添加账号: ${account.userSerialNum}');
|
||||||
|
final accounts = await getAccountList();
|
||||||
|
|
||||||
|
// 检查是否已存在
|
||||||
|
final existingIndex = accounts.indexWhere(
|
||||||
|
(a) => a.userSerialNum == account.userSerialNum,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
// 更新现有账号
|
||||||
|
accounts[existingIndex] = account;
|
||||||
|
debugPrint('$_tag addAccount() - 更新现有账号');
|
||||||
|
} else {
|
||||||
|
// 添加新账号
|
||||||
|
accounts.add(account);
|
||||||
|
debugPrint('$_tag addAccount() - 添加新账号');
|
||||||
|
}
|
||||||
|
|
||||||
|
await _saveAccountList(accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从列表中移除账号
|
||||||
|
Future<void> removeAccount(String userSerialNum) async {
|
||||||
|
debugPrint('$_tag removeAccount() - 移除账号: $userSerialNum');
|
||||||
|
final accounts = await getAccountList();
|
||||||
|
accounts.removeWhere((a) => a.userSerialNum == userSerialNum);
|
||||||
|
await _saveAccountList(accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取当前活跃账号ID
|
||||||
|
Future<String?> getCurrentAccountId() async {
|
||||||
|
return await _secureStorage.read(key: StorageKeys.currentAccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置当前活跃账号ID
|
||||||
|
Future<void> setCurrentAccountId(String? accountId) async {
|
||||||
|
if (accountId == null) {
|
||||||
|
await _secureStorage.delete(key: StorageKeys.currentAccountId);
|
||||||
|
} else {
|
||||||
|
await _secureStorage.write(key: StorageKeys.currentAccountId, value: accountId);
|
||||||
|
}
|
||||||
|
debugPrint('$_tag setCurrentAccountId() - 设置为: $accountId');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 切换到指定账号
|
||||||
|
/// 返回 true 表示切换成功
|
||||||
|
Future<bool> switchToAccount(String userSerialNum) async {
|
||||||
|
debugPrint('$_tag switchToAccount() - 切换到账号: $userSerialNum');
|
||||||
|
|
||||||
|
// 验证账号存在
|
||||||
|
final accounts = await getAccountList();
|
||||||
|
final account = accounts.where((a) => a.userSerialNum == userSerialNum).firstOrNull;
|
||||||
|
|
||||||
|
if (account == null) {
|
||||||
|
debugPrint('$_tag switchToAccount() - 账号不存在');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从账号专用存储中恢复数据到当前存储
|
||||||
|
await _restoreAccountData(userSerialNum);
|
||||||
|
|
||||||
|
// 设置当前账号
|
||||||
|
await setCurrentAccountId(userSerialNum);
|
||||||
|
|
||||||
|
debugPrint('$_tag switchToAccount() - 切换成功');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保存当前账号数据到账号专用存储
|
||||||
|
Future<void> saveCurrentAccountData() async {
|
||||||
|
final currentId = await getCurrentAccountId();
|
||||||
|
if (currentId == null) {
|
||||||
|
debugPrint('$_tag saveCurrentAccountData() - 无当前账号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('$_tag saveCurrentAccountData() - 保存账号数据: $currentId');
|
||||||
|
|
||||||
|
// 需要保存的账号相关键
|
||||||
|
final keysToSave = [
|
||||||
|
StorageKeys.userSerialNum,
|
||||||
|
StorageKeys.username,
|
||||||
|
StorageKeys.avatarSvg,
|
||||||
|
StorageKeys.avatarUrl,
|
||||||
|
StorageKeys.referralCode,
|
||||||
|
StorageKeys.inviterSequence,
|
||||||
|
StorageKeys.isAccountCreated,
|
||||||
|
StorageKeys.walletAddressBsc,
|
||||||
|
StorageKeys.walletAddressKava,
|
||||||
|
StorageKeys.walletAddressDst,
|
||||||
|
StorageKeys.mnemonic,
|
||||||
|
StorageKeys.isWalletReady,
|
||||||
|
StorageKeys.isMnemonicBackedUp,
|
||||||
|
StorageKeys.accessToken,
|
||||||
|
StorageKeys.refreshToken,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final key in keysToSave) {
|
||||||
|
final value = await _secureStorage.read(key: key);
|
||||||
|
if (value != null) {
|
||||||
|
final prefixedKey = StorageKeys.withAccountPrefix(currentId, key);
|
||||||
|
await _secureStorage.write(key: prefixedKey, value: value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('$_tag saveCurrentAccountData() - 保存完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从账号专用存储恢复数据到当前存储
|
||||||
|
Future<void> _restoreAccountData(String accountId) async {
|
||||||
|
debugPrint('$_tag _restoreAccountData() - 恢复账号数据: $accountId');
|
||||||
|
|
||||||
|
// 需要恢复的账号相关键
|
||||||
|
final keysToRestore = [
|
||||||
|
StorageKeys.userSerialNum,
|
||||||
|
StorageKeys.username,
|
||||||
|
StorageKeys.avatarSvg,
|
||||||
|
StorageKeys.avatarUrl,
|
||||||
|
StorageKeys.referralCode,
|
||||||
|
StorageKeys.inviterSequence,
|
||||||
|
StorageKeys.isAccountCreated,
|
||||||
|
StorageKeys.walletAddressBsc,
|
||||||
|
StorageKeys.walletAddressKava,
|
||||||
|
StorageKeys.walletAddressDst,
|
||||||
|
StorageKeys.mnemonic,
|
||||||
|
StorageKeys.isWalletReady,
|
||||||
|
StorageKeys.isMnemonicBackedUp,
|
||||||
|
StorageKeys.accessToken,
|
||||||
|
StorageKeys.refreshToken,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final key in keysToRestore) {
|
||||||
|
final prefixedKey = StorageKeys.withAccountPrefix(accountId, key);
|
||||||
|
final value = await _secureStorage.read(key: prefixedKey);
|
||||||
|
if (value != null) {
|
||||||
|
await _secureStorage.write(key: key, value: value);
|
||||||
|
} else {
|
||||||
|
// 如果账号存储中没有该键,删除当前存储中的值
|
||||||
|
await _secureStorage.delete(key: key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('$_tag _restoreAccountData() - 恢复完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 退出当前账号(不删除账号数据)
|
||||||
|
/// 清除当前会话状态,但保留账号在列表中
|
||||||
|
Future<void> logoutCurrentAccount() async {
|
||||||
|
debugPrint('$_tag logoutCurrentAccount() - 退出当前账号');
|
||||||
|
|
||||||
|
// 保存当前账号数据
|
||||||
|
await saveCurrentAccountData();
|
||||||
|
|
||||||
|
// 清除当前会话的 Token(但保留在账号存储中)
|
||||||
|
await _secureStorage.delete(key: StorageKeys.accessToken);
|
||||||
|
await _secureStorage.delete(key: StorageKeys.refreshToken);
|
||||||
|
|
||||||
|
// 清除当前账号标记
|
||||||
|
await setCurrentAccountId(null);
|
||||||
|
|
||||||
|
// 清除当前账号信息(但不删除账号列表)
|
||||||
|
await _secureStorage.delete(key: StorageKeys.userSerialNum);
|
||||||
|
await _secureStorage.delete(key: StorageKeys.username);
|
||||||
|
await _secureStorage.delete(key: StorageKeys.isAccountCreated);
|
||||||
|
|
||||||
|
debugPrint('$_tag logoutCurrentAccount() - 退出完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 完全删除账号(包括所有数据)
|
||||||
|
Future<void> deleteAccount(String userSerialNum) async {
|
||||||
|
debugPrint('$_tag deleteAccount() - 删除账号: $userSerialNum');
|
||||||
|
|
||||||
|
// 从列表中移除
|
||||||
|
await removeAccount(userSerialNum);
|
||||||
|
|
||||||
|
// 删除账号专用存储的所有数据
|
||||||
|
final keysToDelete = [
|
||||||
|
StorageKeys.userSerialNum,
|
||||||
|
StorageKeys.username,
|
||||||
|
StorageKeys.avatarSvg,
|
||||||
|
StorageKeys.avatarUrl,
|
||||||
|
StorageKeys.referralCode,
|
||||||
|
StorageKeys.inviterSequence,
|
||||||
|
StorageKeys.isAccountCreated,
|
||||||
|
StorageKeys.walletAddressBsc,
|
||||||
|
StorageKeys.walletAddressKava,
|
||||||
|
StorageKeys.walletAddressDst,
|
||||||
|
StorageKeys.mnemonic,
|
||||||
|
StorageKeys.isWalletReady,
|
||||||
|
StorageKeys.isMnemonicBackedUp,
|
||||||
|
StorageKeys.accessToken,
|
||||||
|
StorageKeys.refreshToken,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final key in keysToDelete) {
|
||||||
|
final prefixedKey = StorageKeys.withAccountPrefix(userSerialNum, key);
|
||||||
|
await _secureStorage.delete(key: prefixedKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果删除的是当前账号,清除当前账号标记
|
||||||
|
final currentId = await getCurrentAccountId();
|
||||||
|
if (currentId == userSerialNum) {
|
||||||
|
await logoutCurrentAccount();
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('$_tag deleteAccount() - 删除完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 迁移旧数据到多账号架构
|
||||||
|
/// 如果存在旧的单账号数据,将其添加到账号列表
|
||||||
|
Future<void> migrateFromSingleAccount() async {
|
||||||
|
debugPrint('$_tag migrateFromSingleAccount() - 检查是否需要迁移');
|
||||||
|
|
||||||
|
// 检查是否已有账号列表
|
||||||
|
final accounts = await getAccountList();
|
||||||
|
if (accounts.isNotEmpty) {
|
||||||
|
debugPrint('$_tag migrateFromSingleAccount() - 已有账号列表,无需迁移');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否存在旧的单账号数据
|
||||||
|
final userSerialNum = await _secureStorage.read(key: StorageKeys.userSerialNum);
|
||||||
|
final isAccountCreated = await _secureStorage.read(key: StorageKeys.isAccountCreated);
|
||||||
|
|
||||||
|
if (userSerialNum == null || isAccountCreated != 'true') {
|
||||||
|
debugPrint('$_tag migrateFromSingleAccount() - 无旧账号数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取账号信息
|
||||||
|
final username = await _secureStorage.read(key: StorageKeys.username) ?? '未知用户';
|
||||||
|
final avatarSvg = await _secureStorage.read(key: StorageKeys.avatarSvg);
|
||||||
|
final avatarUrl = await _secureStorage.read(key: StorageKeys.avatarUrl);
|
||||||
|
|
||||||
|
// 创建账号摘要
|
||||||
|
final account = AccountSummary(
|
||||||
|
userSerialNum: userSerialNum,
|
||||||
|
username: username,
|
||||||
|
avatarSvg: avatarSvg,
|
||||||
|
avatarUrl: avatarUrl,
|
||||||
|
createdAt: DateTime.now(), // 旧数据没有创建时间,使用当前时间
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加到账号列表
|
||||||
|
await addAccount(account);
|
||||||
|
|
||||||
|
// 设置为当前账号
|
||||||
|
await setCurrentAccountId(userSerialNum);
|
||||||
|
|
||||||
|
// 保存当前账号数据到账号专用存储
|
||||||
|
await saveCurrentAccountData();
|
||||||
|
|
||||||
|
debugPrint('$_tag migrateFromSingleAccount() - 迁移完成: $userSerialNum');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查是否有任何已登录的账号
|
||||||
|
Future<bool> hasAnyAccount() async {
|
||||||
|
final accounts = await getAccountList();
|
||||||
|
return accounts.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取账号数量
|
||||||
|
Future<int> getAccountCount() async {
|
||||||
|
final accounts = await getAccountList();
|
||||||
|
return accounts.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
class StorageKeys {
|
class StorageKeys {
|
||||||
StorageKeys._();
|
StorageKeys._();
|
||||||
|
|
||||||
|
// ===== 多账号管理 =====
|
||||||
|
static const String accountList = 'account_list'; // 账号列表 (JSON 数组)
|
||||||
|
static const String currentAccountId = 'current_account_id'; // 当前活跃账号ID (userSerialNum)
|
||||||
|
|
||||||
// 账号信息
|
// 账号信息
|
||||||
static const String userSerialNum = 'user_serial_num'; // 用户序列号
|
static const String userSerialNum = 'user_serial_num'; // 用户序列号
|
||||||
static const String username = 'username'; // 随机用户名
|
static const String username = 'username'; // 随机用户名
|
||||||
|
|
@ -11,6 +15,12 @@ class StorageKeys {
|
||||||
static const String inviterReferralCode = 'inviter_referral_code'; // 邀请人推荐码(注册前临时存储)
|
static const String inviterReferralCode = 'inviter_referral_code'; // 邀请人推荐码(注册前临时存储)
|
||||||
static const String isAccountCreated = 'is_account_created'; // 账号是否已创建
|
static const String isAccountCreated = 'is_account_created'; // 账号是否已创建
|
||||||
|
|
||||||
|
/// 生成带账号前缀的存储键
|
||||||
|
/// 用于多账号隔离存储
|
||||||
|
static String withAccountPrefix(String accountId, String key) {
|
||||||
|
return 'account_${accountId}_$key';
|
||||||
|
}
|
||||||
|
|
||||||
// 钱包信息
|
// 钱包信息
|
||||||
static const String walletAddressBsc = 'wallet_address_bsc';
|
static const String walletAddressBsc = 'wallet_address_bsc';
|
||||||
static const String walletAddressKava = 'wallet_address_kava';
|
static const String walletAddressKava = 'wallet_address_kava';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,392 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import '../../../../core/di/injection_container.dart';
|
||||||
|
import '../../../../core/services/multi_account_service.dart';
|
||||||
|
import '../../../../routes/route_paths.dart';
|
||||||
|
|
||||||
|
/// 账号切换页面
|
||||||
|
/// 显示所有已登录的账号列表,支持切换和添加新账号
|
||||||
|
class AccountSwitchPage extends ConsumerStatefulWidget {
|
||||||
|
const AccountSwitchPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<AccountSwitchPage> createState() => _AccountSwitchPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AccountSwitchPageState extends ConsumerState<AccountSwitchPage> {
|
||||||
|
List<AccountSummary> _accounts = [];
|
||||||
|
String? _currentAccountId;
|
||||||
|
bool _isLoading = true;
|
||||||
|
bool _isSwitching = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadAccounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadAccounts() async {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
|
final multiAccountService = ref.read(multiAccountServiceProvider);
|
||||||
|
|
||||||
|
// 先执行迁移(如果需要)
|
||||||
|
await multiAccountService.migrateFromSingleAccount();
|
||||||
|
|
||||||
|
final accounts = await multiAccountService.getAccountList();
|
||||||
|
final currentId = await multiAccountService.getCurrentAccountId();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_accounts = accounts;
|
||||||
|
_currentAccountId = currentId;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _switchToAccount(AccountSummary account) async {
|
||||||
|
if (account.userSerialNum == _currentAccountId) {
|
||||||
|
// 已是当前账号,直接返回
|
||||||
|
context.pop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isSwitching = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final multiAccountService = ref.read(multiAccountServiceProvider);
|
||||||
|
|
||||||
|
// 保存当前账号数据
|
||||||
|
await multiAccountService.saveCurrentAccountData();
|
||||||
|
|
||||||
|
// 切换到新账号
|
||||||
|
final success = await multiAccountService.switchToAccount(account.userSerialNum);
|
||||||
|
|
||||||
|
if (success && mounted) {
|
||||||
|
// 切换成功,跳转到主页刷新状态
|
||||||
|
context.go(RoutePaths.ranking);
|
||||||
|
} else if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('切换账号失败')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[AccountSwitchPage] 切换账号失败: $e');
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('切换账号失败: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isSwitching = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _addNewAccount() async {
|
||||||
|
// 保存当前账号数据
|
||||||
|
final multiAccountService = ref.read(multiAccountServiceProvider);
|
||||||
|
await multiAccountService.saveCurrentAccountData();
|
||||||
|
|
||||||
|
// 退出当前账号但保留数据
|
||||||
|
await multiAccountService.logoutCurrentAccount();
|
||||||
|
|
||||||
|
// 跳转到向导页创建新账号
|
||||||
|
if (mounted) {
|
||||||
|
context.go(RoutePaths.guide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteAccount(AccountSummary account) async {
|
||||||
|
// 显示确认对话框
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('删除账号'),
|
||||||
|
content: Text(
|
||||||
|
'确定要删除账号 "${account.username}" 吗?\n\n'
|
||||||
|
'删除后该账号的所有本地数据将被清除,'
|
||||||
|
'如需恢复请使用助记词重新导入。',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('取消'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: const Color(0xFFE57373),
|
||||||
|
),
|
||||||
|
child: const Text('删除'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed != true) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final multiAccountService = ref.read(multiAccountServiceProvider);
|
||||||
|
await multiAccountService.deleteAccount(account.userSerialNum);
|
||||||
|
|
||||||
|
// 重新加载账号列表
|
||||||
|
await _loadAccounts();
|
||||||
|
|
||||||
|
// 如果没有账号了,跳转到向导页
|
||||||
|
if (_accounts.isEmpty && mounted) {
|
||||||
|
context.go(RoutePaths.guide);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[AccountSwitchPage] 删除账号失败: $e');
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('删除账号失败: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFFFFBF5),
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: const Color(0xFFFFFBF5),
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios, color: Color(0xFF5D4037)),
|
||||||
|
onPressed: () => context.pop(),
|
||||||
|
),
|
||||||
|
title: const Text(
|
||||||
|
'切换账号',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: _isLoading
|
||||||
|
? const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Color(0xFFD4AF37),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: _isSwitching
|
||||||
|
? const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(
|
||||||
|
color: Color(0xFFD4AF37),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'正在切换账号...',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
// 账号列表标题
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 12),
|
||||||
|
child: Text(
|
||||||
|
'已登录的账号',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Color(0x995D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 账号列表
|
||||||
|
..._accounts.map((account) => _buildAccountItem(account)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// 添加新账号按钮
|
||||||
|
_buildAddAccountButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAccountItem(AccountSummary account) {
|
||||||
|
final isCurrent = account.userSerialNum == _currentAccountId;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isCurrent ? const Color(0xFFD4AF37).withValues(alpha: 0.1) : const Color(0xFFFFF5E6),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: isCurrent ? const Color(0xFFD4AF37) : const Color(0x33D4AF37),
|
||||||
|
width: isCurrent ? 2 : 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => _switchToAccount(account),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// 头像
|
||||||
|
_buildAvatar(account),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
// 账号信息
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
account.username,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isCurrent) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFD4AF37),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'当前',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
account.userSerialNum,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
color: Color(0x995D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 删除按钮(非当前账号才显示)
|
||||||
|
if (!isCurrent)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.delete_outline,
|
||||||
|
color: Color(0xFFE57373),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
onPressed: () => _deleteAccount(account),
|
||||||
|
),
|
||||||
|
// 当前账号显示勾选图标
|
||||||
|
if (isCurrent)
|
||||||
|
const Icon(
|
||||||
|
Icons.check_circle,
|
||||||
|
color: Color(0xFFD4AF37),
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAvatar(AccountSummary account) {
|
||||||
|
return Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
color: const Color(0xFFE8D5B7),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: account.avatarSvg != null
|
||||||
|
? SvgPicture.string(
|
||||||
|
account.avatarSvg!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: const Icon(
|
||||||
|
Icons.person,
|
||||||
|
size: 28,
|
||||||
|
color: Color(0xFF8B5A2B),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAddAccountButton() {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: _addNewAccount,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFFF5E6),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: const Color(0x33D4AF37),
|
||||||
|
width: 1,
|
||||||
|
style: BorderStyle.solid,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFD4AF37).withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.add,
|
||||||
|
size: 20,
|
||||||
|
color: Color(0xFFD4AF37),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Text(
|
||||||
|
'添加新账号',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import '../../../../routes/route_paths.dart';
|
||||||
import '../../../../routes/app_router.dart';
|
import '../../../../routes/app_router.dart';
|
||||||
import '../../../../core/di/injection_container.dart';
|
import '../../../../core/di/injection_container.dart';
|
||||||
import '../../../../core/storage/storage_keys.dart';
|
import '../../../../core/storage/storage_keys.dart';
|
||||||
|
import '../../../../core/services/multi_account_service.dart';
|
||||||
|
|
||||||
/// 创建账号页面 - 用户首次进入应用时的引导页面
|
/// 创建账号页面 - 用户首次进入应用时的引导页面
|
||||||
/// 提供创建钱包和导入助记词两种选项
|
/// 提供创建钱包和导入助记词两种选项
|
||||||
|
|
@ -130,6 +131,19 @@ class _OnboardingPageState extends ConsumerState<OnboardingPage> {
|
||||||
debugPrint('[OnboardingPage] _createAccount - 已清除临时邀请人推荐码');
|
debugPrint('[OnboardingPage] _createAccount - 已清除临时邀请人推荐码');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将账号添加到多账号列表
|
||||||
|
final multiAccountService = ref.read(multiAccountServiceProvider);
|
||||||
|
await multiAccountService.addAccount(
|
||||||
|
AccountSummary(
|
||||||
|
userSerialNum: response.userSerialNum,
|
||||||
|
username: response.username,
|
||||||
|
avatarSvg: response.avatarSvg,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await multiAccountService.setCurrentAccountId(response.userSerialNum);
|
||||||
|
debugPrint('[OnboardingPage] _createAccount - 已添加到多账号列表');
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
debugPrint('[OnboardingPage] _createAccount - Widget已卸载,忽略响应');
|
debugPrint('[OnboardingPage] _createAccount - Widget已卸载,忽略响应');
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -2867,21 +2867,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 切换账号
|
/// 切换账号 - 跳转到账号切换页面
|
||||||
void _onSwitchAccount() {
|
void _onSwitchAccount() {
|
||||||
showDialog(
|
context.push(RoutePaths.accountSwitch);
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: const Text('切换账号'),
|
|
||||||
content: const Text('多账号管理功能即将上线,敬请期待。'),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: const Text('确定'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 退出登录
|
/// 退出登录
|
||||||
|
|
@ -2890,7 +2878,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('退出登录'),
|
title: const Text('退出登录'),
|
||||||
content: const Text('确定要退出当前账号吗?\n\n退出后需要重新导入助记词才能恢复账号。'),
|
content: const Text('确定要退出当前账号吗?\n\n退出后账号数据会保留在本地,可在账号切换页面重新登录。'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
|
@ -2914,9 +2902,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
/// 执行退出登录
|
/// 执行退出登录
|
||||||
Future<void> _performLogout() async {
|
Future<void> _performLogout() async {
|
||||||
try {
|
try {
|
||||||
final accountService = ref.read(accountServiceProvider);
|
final multiAccountService = ref.read(multiAccountServiceProvider);
|
||||||
// 清除所有本地数据
|
// 退出当前账号(保留账号数据)
|
||||||
await accountService.logout();
|
await multiAccountService.logoutCurrentAccount();
|
||||||
// 导航到向导页
|
// 导航到向导页
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
context.go(RoutePaths.guide);
|
context.go(RoutePaths.guide);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import '../features/security/presentation/pages/bind_email_page.dart';
|
||||||
import '../features/withdraw/presentation/pages/withdraw_usdt_page.dart';
|
import '../features/withdraw/presentation/pages/withdraw_usdt_page.dart';
|
||||||
import '../features/withdraw/presentation/pages/withdraw_confirm_page.dart';
|
import '../features/withdraw/presentation/pages/withdraw_confirm_page.dart';
|
||||||
import '../features/notification/presentation/pages/notification_inbox_page.dart';
|
import '../features/notification/presentation/pages/notification_inbox_page.dart';
|
||||||
|
import '../features/account/presentation/pages/account_switch_page.dart';
|
||||||
import 'route_paths.dart';
|
import 'route_paths.dart';
|
||||||
import 'route_names.dart';
|
import 'route_names.dart';
|
||||||
|
|
||||||
|
|
@ -187,6 +188,13 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
builder: (context, state) => const NotificationInboxPage(),
|
builder: (context, state) => const NotificationInboxPage(),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Account Switch Page (账号切换)
|
||||||
|
GoRoute(
|
||||||
|
path: RoutePaths.accountSwitch,
|
||||||
|
name: RouteNames.accountSwitch,
|
||||||
|
builder: (context, state) => const AccountSwitchPage(),
|
||||||
|
),
|
||||||
|
|
||||||
// Share Page (分享页面)
|
// Share Page (分享页面)
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RoutePaths.share,
|
path: RoutePaths.share,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class RouteNames {
|
||||||
static const referralList = 'referral-list';
|
static const referralList = 'referral-list';
|
||||||
static const earningsDetail = 'earnings-detail';
|
static const earningsDetail = 'earnings-detail';
|
||||||
static const notifications = 'notifications';
|
static const notifications = 'notifications';
|
||||||
|
static const accountSwitch = 'account-switch';
|
||||||
static const deposit = 'deposit';
|
static const deposit = 'deposit';
|
||||||
static const depositUsdt = 'deposit-usdt';
|
static const depositUsdt = 'deposit-usdt';
|
||||||
static const plantingQuantity = 'planting-quantity';
|
static const plantingQuantity = 'planting-quantity';
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class RoutePaths {
|
||||||
static const referralList = '/profile/referrals';
|
static const referralList = '/profile/referrals';
|
||||||
static const earningsDetail = '/profile/earnings';
|
static const earningsDetail = '/profile/earnings';
|
||||||
static const notifications = '/notifications';
|
static const notifications = '/notifications';
|
||||||
|
static const accountSwitch = '/account/switch';
|
||||||
static const deposit = '/deposit';
|
static const deposit = '/deposit';
|
||||||
static const depositUsdt = '/deposit/usdt';
|
static const depositUsdt = '/deposit/usdt';
|
||||||
static const plantingQuantity = '/planting/quantity';
|
static const plantingQuantity = '/planting/quantity';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue