feat(mobile-app): add import mnemonic page and fix share URL
- Add import mnemonic page for account recovery - Add serial number input field for recovery - Add RecoverAccountResponse class for API response parsing - Fix share page URL path (remove duplicate /api) - Fix bottom nav bar height for better adaptability - Update routes for import mnemonic page 🤖 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
c1670d2439
commit
cfd5bd9bde
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
|
|
@ -175,6 +175,44 @@ String _maskAddress(String? address) {
|
|||
return '${address.substring(0, 6)}...${address.substring(address.length - 4)}';
|
||||
}
|
||||
|
||||
/// 恢复账户响应
|
||||
class RecoverAccountResponse {
|
||||
final String userId;
|
||||
final int userSerialNum; // accountSequence
|
||||
final String username; // nickname
|
||||
final String? avatarSvg; // avatarUrl
|
||||
final String referralCode;
|
||||
final String accessToken;
|
||||
final String refreshToken;
|
||||
|
||||
RecoverAccountResponse({
|
||||
required this.userId,
|
||||
required this.userSerialNum,
|
||||
required this.username,
|
||||
this.avatarSvg,
|
||||
required this.referralCode,
|
||||
required this.accessToken,
|
||||
required this.refreshToken,
|
||||
});
|
||||
|
||||
factory RecoverAccountResponse.fromJson(Map<String, dynamic> json) {
|
||||
debugPrint('[AccountService] 解析 RecoverAccountResponse: ${json.keys.toList()}');
|
||||
return RecoverAccountResponse(
|
||||
userId: json['userId'] as String,
|
||||
userSerialNum: json['accountSequence'] as int,
|
||||
username: json['nickname'] as String,
|
||||
avatarSvg: json['avatarUrl'] as String?,
|
||||
referralCode: json['referralCode'] as String,
|
||||
accessToken: json['accessToken'] as String,
|
||||
refreshToken: json['refreshToken'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'RecoverAccountResponse(userSerialNum: $userSerialNum, username: $username, referralCode: $referralCode)';
|
||||
}
|
||||
|
||||
/// 账号服务
|
||||
///
|
||||
/// 处理账号创建、钱包获取等功能
|
||||
|
|
@ -596,6 +634,142 @@ class AccountService {
|
|||
return result;
|
||||
}
|
||||
|
||||
/// 通过助记词恢复账户
|
||||
///
|
||||
/// 使用序列号和 12 个助记词恢复已有账户
|
||||
/// [accountSequence] - 用户序列号
|
||||
/// [mnemonic] - 12 个助记词,用空格分隔
|
||||
Future<RecoverAccountResponse> recoverByMnemonic(int accountSequence, String mnemonic) async {
|
||||
debugPrint('$_tag recoverByMnemonic() - 开始恢复账户');
|
||||
debugPrint('$_tag recoverByMnemonic() - 序列号: $accountSequence');
|
||||
|
||||
try {
|
||||
// 获取设备ID
|
||||
final deviceId = await getDeviceId();
|
||||
debugPrint('$_tag recoverByMnemonic() - 获取设备ID成功');
|
||||
|
||||
// 获取设备硬件信息
|
||||
final deviceInfo = await getDeviceHardwareInfo();
|
||||
debugPrint('$_tag recoverByMnemonic() - 获取设备硬件信息成功');
|
||||
|
||||
// 调用 API
|
||||
debugPrint('$_tag recoverByMnemonic() - 调用 POST /user/recover-by-mnemonic');
|
||||
final response = await _apiClient.post(
|
||||
'/user/recover-by-mnemonic',
|
||||
data: {
|
||||
'accountSequence': accountSequence,
|
||||
'mnemonic': mnemonic,
|
||||
'newDeviceId': deviceId,
|
||||
'deviceName': '${deviceInfo.brand ?? ''} ${deviceInfo.model ?? ''}'.trim(),
|
||||
},
|
||||
);
|
||||
debugPrint('$_tag recoverByMnemonic() - API 响应状态码: ${response.statusCode}');
|
||||
|
||||
if (response.data == null) {
|
||||
debugPrint('$_tag recoverByMnemonic() - 错误: API 返回空响应');
|
||||
throw const ApiException('恢复账户失败: 空响应');
|
||||
}
|
||||
|
||||
debugPrint('$_tag recoverByMnemonic() - 解析响应数据');
|
||||
final responseData = response.data as Map<String, dynamic>;
|
||||
final data = responseData['data'] as Map<String, dynamic>;
|
||||
final result = RecoverAccountResponse.fromJson(data);
|
||||
debugPrint('$_tag recoverByMnemonic() - 解析成功: $result');
|
||||
|
||||
// 保存账号数据 (使用恢复账户专用的保存方法)
|
||||
debugPrint('$_tag recoverByMnemonic() - 保存账号数据');
|
||||
await _saveRecoverAccountData(result, deviceId);
|
||||
|
||||
// 保存助记词
|
||||
debugPrint('$_tag recoverByMnemonic() - 保存助记词');
|
||||
await _secureStorage.write(
|
||||
key: StorageKeys.mnemonic,
|
||||
value: mnemonic,
|
||||
);
|
||||
|
||||
// 标记助记词已备份 (恢复的用户肯定已经备份过了)
|
||||
await _secureStorage.write(
|
||||
key: StorageKeys.isMnemonicBackedUp,
|
||||
value: 'true',
|
||||
);
|
||||
|
||||
debugPrint('$_tag recoverByMnemonic() - 账户恢复完成');
|
||||
return result;
|
||||
} on ApiException catch (e) {
|
||||
debugPrint('$_tag recoverByMnemonic() - API 异常: $e');
|
||||
rethrow;
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('$_tag recoverByMnemonic() - 未知异常: $e');
|
||||
debugPrint('$_tag recoverByMnemonic() - 堆栈: $stackTrace');
|
||||
throw ApiException('恢复账户失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存恢复账户数据
|
||||
Future<void> _saveRecoverAccountData(
|
||||
RecoverAccountResponse response,
|
||||
String deviceId,
|
||||
) async {
|
||||
debugPrint('$_tag _saveRecoverAccountData() - 开始保存恢复账号数据');
|
||||
|
||||
// 保存用户序列号
|
||||
debugPrint('$_tag _saveRecoverAccountData() - 保存 userSerialNum: ${response.userSerialNum}');
|
||||
await _secureStorage.write(
|
||||
key: StorageKeys.userSerialNum,
|
||||
value: response.userSerialNum.toString(),
|
||||
);
|
||||
|
||||
debugPrint('$_tag _saveRecoverAccountData() - 保存 referralCode: ${response.referralCode}');
|
||||
await _secureStorage.write(
|
||||
key: StorageKeys.referralCode,
|
||||
value: response.referralCode,
|
||||
);
|
||||
|
||||
// 保存用户信息
|
||||
debugPrint('$_tag _saveRecoverAccountData() - 保存 username: ${response.username}');
|
||||
await _secureStorage.write(
|
||||
key: StorageKeys.username,
|
||||
value: response.username,
|
||||
);
|
||||
|
||||
if (response.avatarSvg != null) {
|
||||
debugPrint('$_tag _saveRecoverAccountData() - 保存 avatarSvg (长度: ${response.avatarSvg!.length})');
|
||||
await _secureStorage.write(
|
||||
key: StorageKeys.avatarSvg,
|
||||
value: response.avatarSvg!,
|
||||
);
|
||||
}
|
||||
|
||||
// 保存 Token
|
||||
debugPrint('$_tag _saveRecoverAccountData() - 保存 accessToken (长度: ${response.accessToken.length})');
|
||||
await _secureStorage.write(
|
||||
key: StorageKeys.accessToken,
|
||||
value: response.accessToken,
|
||||
);
|
||||
|
||||
debugPrint('$_tag _saveRecoverAccountData() - 保存 refreshToken (长度: ${response.refreshToken.length})');
|
||||
await _secureStorage.write(
|
||||
key: StorageKeys.refreshToken,
|
||||
value: response.refreshToken,
|
||||
);
|
||||
|
||||
// 保存设备 ID
|
||||
debugPrint('$_tag _saveRecoverAccountData() - 保存 deviceId: ${_maskAddress(deviceId)}');
|
||||
await _secureStorage.write(
|
||||
key: StorageKeys.deviceId,
|
||||
value: deviceId,
|
||||
);
|
||||
|
||||
// 标记账号已创建
|
||||
debugPrint('$_tag _saveRecoverAccountData() - 标记账号已创建');
|
||||
await _secureStorage.write(
|
||||
key: StorageKeys.isAccountCreated,
|
||||
value: 'true',
|
||||
);
|
||||
|
||||
debugPrint('$_tag _saveRecoverAccountData() - 恢复账号数据保存完成');
|
||||
}
|
||||
|
||||
/// 登出
|
||||
Future<void> logout() async {
|
||||
debugPrint('$_tag logout() - 开始登出,清除所有数据');
|
||||
|
|
|
|||
|
|
@ -111,11 +111,11 @@ class ReferralService {
|
|||
|
||||
/// 获取当前用户信息 (包含默认推荐链接)
|
||||
///
|
||||
/// 调用 GET /api/me
|
||||
/// 调用 GET /me
|
||||
Future<MeResponse> getMe() async {
|
||||
try {
|
||||
debugPrint('获取用户信息...');
|
||||
final response = await _apiClient.get('/api/me');
|
||||
final response = await _apiClient.get('/me');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
|
@ -132,7 +132,7 @@ class ReferralService {
|
|||
|
||||
/// 生成推荐链接 (短链)
|
||||
///
|
||||
/// 调用 POST /api/referrals/links
|
||||
/// 调用 POST /referrals/links
|
||||
/// 返回包含 shortUrl 和 fullUrl 的响应
|
||||
///
|
||||
/// [channel] - 渠道标识: wechat, telegram, twitter 等
|
||||
|
|
@ -145,7 +145,7 @@ class ReferralService {
|
|||
debugPrint('生成推荐链接: channel=$channel, campaignId=$campaignId');
|
||||
|
||||
final response = await _apiClient.post(
|
||||
'/api/referrals/links',
|
||||
'/referrals/links',
|
||||
data: {
|
||||
if (channel != null) 'channel': channel,
|
||||
if (campaignId != null) 'campaignId': campaignId,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,523 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../../../../routes/route_paths.dart';
|
||||
|
||||
/// 导入助记词页面 - 用于恢复已有账户
|
||||
/// 用户输入序列号和12个助记词来恢复钱包
|
||||
class ImportMnemonicPage extends ConsumerStatefulWidget {
|
||||
const ImportMnemonicPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ImportMnemonicPage> createState() => _ImportMnemonicPageState();
|
||||
}
|
||||
|
||||
class _ImportMnemonicPageState extends ConsumerState<ImportMnemonicPage> {
|
||||
// 序列号输入控制器
|
||||
final TextEditingController _serialNumController = TextEditingController();
|
||||
|
||||
// 12个助记词输入控制器
|
||||
final List<TextEditingController> _controllers = List.generate(
|
||||
12,
|
||||
(_) => TextEditingController(),
|
||||
);
|
||||
|
||||
// 12个焦点节点
|
||||
final List<FocusNode> _focusNodes = List.generate(
|
||||
12,
|
||||
(_) => FocusNode(),
|
||||
);
|
||||
|
||||
// 是否正在提交
|
||||
bool _isSubmitting = false;
|
||||
|
||||
// 错误信息
|
||||
String? _errorMessage;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_serialNumController.dispose();
|
||||
for (final controller in _controllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (final node in _focusNodes) {
|
||||
node.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 获取完整的助记词字符串
|
||||
String get _mnemonic {
|
||||
return _controllers
|
||||
.map((c) => c.text.trim().toLowerCase())
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/// 获取序列号
|
||||
int? get _serialNum {
|
||||
final text = _serialNumController.text.trim();
|
||||
if (text.isEmpty) return null;
|
||||
return int.tryParse(text);
|
||||
}
|
||||
|
||||
/// 检查是否所有单词都已填写
|
||||
bool get _isComplete {
|
||||
final hasSerialNum = _serialNumController.text.trim().isNotEmpty;
|
||||
final hasAllWords = _controllers.every((c) => c.text.trim().isNotEmpty);
|
||||
return hasSerialNum && hasAllWords;
|
||||
}
|
||||
|
||||
/// 导入并恢复账号
|
||||
Future<void> _importAndRecover() async {
|
||||
// 验证序列号
|
||||
final serialNum = _serialNum;
|
||||
if (serialNum == null) {
|
||||
setState(() {
|
||||
_errorMessage = '请输入有效的序列号';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证助记词
|
||||
if (!_controllers.every((c) => c.text.trim().isNotEmpty)) {
|
||||
setState(() {
|
||||
_errorMessage = '请填写完整的12个助记词';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final accountService = ref.read(accountServiceProvider);
|
||||
|
||||
// 调用恢复账户API
|
||||
final response = await accountService.recoverByMnemonic(
|
||||
serialNum,
|
||||
_mnemonic,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// 恢复成功,跳转到主页
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('账户恢复成功!序列号: ${response.userSerialNum}'),
|
||||
backgroundColor: const Color(0xFF52C41A),
|
||||
),
|
||||
);
|
||||
|
||||
// 跳转到主页
|
||||
context.go(RoutePaths.ranking);
|
||||
} catch (e) {
|
||||
debugPrint('恢复账户失败: $e');
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_errorMessage = e.toString().replaceAll('Exception: ', '');
|
||||
});
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回创建账号页面
|
||||
void _goBack() {
|
||||
context.pop();
|
||||
}
|
||||
|
||||
/// 处理单词输入完成,自动跳转到下一个
|
||||
void _onWordChanged(int index, String value) {
|
||||
// 清除错误信息
|
||||
if (_errorMessage != null) {
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
});
|
||||
}
|
||||
|
||||
// 如果输入了空格,可能是粘贴了多个单词
|
||||
if (value.contains(' ')) {
|
||||
_handlePaste(index, value);
|
||||
return;
|
||||
}
|
||||
|
||||
// 刷新状态以更新按钮
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
/// 处理粘贴多个单词的情况
|
||||
void _handlePaste(int startIndex, String value) {
|
||||
final words = value.trim().split(RegExp(r'\s+'));
|
||||
|
||||
for (int i = 0; i < words.length && startIndex + i < 12; i++) {
|
||||
_controllers[startIndex + i].text = words[i].toLowerCase();
|
||||
}
|
||||
|
||||
// 焦点移动到最后一个填写的位置或下一个空位
|
||||
final nextIndex = (startIndex + words.length).clamp(0, 11);
|
||||
_focusNodes[nextIndex].requestFocus();
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFFFFF7E6), // 浅米色
|
||||
Color(0xFFEAE0C8), // 浅橙色
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// 顶部导航栏
|
||||
_buildAppBar(),
|
||||
// 内容区域
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
// 序列号输入
|
||||
_buildSerialNumInput(),
|
||||
const SizedBox(height: 16),
|
||||
// 助记词标签
|
||||
const Text(
|
||||
'助记词',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF745D43),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 助记词输入区域
|
||||
_buildMnemonicInputArea(),
|
||||
const SizedBox(height: 12),
|
||||
// 提示文字
|
||||
const Text(
|
||||
'支持 KAVA / DST / BSC 链的钱包导入。请在私密环境中操作。',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
height: 1.5,
|
||||
color: Color(0xFF745D43),
|
||||
),
|
||||
),
|
||||
// 错误信息
|
||||
if (_errorMessage != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFE4E4),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
_errorMessage!,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFFD32F2F),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 底部按钮区域
|
||||
_buildFooter(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建顶部导航栏
|
||||
Widget _buildAppBar() {
|
||||
return Container(
|
||||
height: 56,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// 返回按钮
|
||||
Positioned(
|
||||
left: 0,
|
||||
child: GestureDetector(
|
||||
onTap: _goBack,
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Color(0xFF5D4037),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 标题
|
||||
const Text(
|
||||
'导入助记词',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.27,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建序列号输入框
|
||||
Widget _buildSerialNumInput() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'序列号',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF745D43),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFFDF8),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: const Color(0x338B5A2B),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _serialNumController,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
hintText: '请输入账户序列号',
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0x668B5A2B),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
onChanged: (_) {
|
||||
if (_errorMessage != null) {
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
});
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建助记词输入区域
|
||||
Widget _buildMnemonicInputArea() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFFDF8),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0x338B5A2B),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 4行,每行3个单词
|
||||
for (int row = 0; row < 4; row++) ...[
|
||||
if (row > 0) const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
for (int col = 0; col < 3; col++) ...[
|
||||
if (col > 0) const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildWordInput(row * 3 + col),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建单个单词输入框
|
||||
Widget _buildWordInput(int index) {
|
||||
return Container(
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF7E6),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: const Color(0x338B5A2B),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 序号
|
||||
Container(
|
||||
width: 28,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF8B5A2B),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 输入框
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controllers[index],
|
||||
focusNode: _focusNodes[index],
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 10),
|
||||
isDense: true,
|
||||
),
|
||||
textInputAction: index < 11 ? TextInputAction.next : TextInputAction.done,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
onChanged: (value) => _onWordChanged(index, value),
|
||||
onSubmitted: (_) {
|
||||
if (index < 11) {
|
||||
_focusNodes[index + 1].requestFocus();
|
||||
} else {
|
||||
// 最后一个,尝试提交
|
||||
if (_isComplete) {
|
||||
_importAndRecover();
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建底部按钮区域
|
||||
Widget _buildFooter() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||
child: Column(
|
||||
children: [
|
||||
// 导入并恢复账号按钮
|
||||
GestureDetector(
|
||||
onTap: _isSubmitting ? null : _importAndRecover,
|
||||
child: Opacity(
|
||||
opacity: _isComplete && !_isSubmitting ? 1.0 : 0.5,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF8B5A2B),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'导入并恢复账号',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.24,
|
||||
color: Color(0xB3FFFFFF),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 返回创建账号链接
|
||||
GestureDetector(
|
||||
onTap: _goBack,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: const Text(
|
||||
'返回创建账号',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
decoration: TextDecoration.underline,
|
||||
color: Color(0xFF745D43),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -181,8 +181,8 @@ class _OnboardingPageState extends ConsumerState<OnboardingPage> {
|
|||
|
||||
/// 导入助记词
|
||||
void _importMnemonic() {
|
||||
// TODO: 跳转到导入助记词页面
|
||||
debugPrint('导入助记词');
|
||||
debugPrint('[OnboardingPage] _importMnemonic - 跳转到导入助记词页面');
|
||||
context.push(RoutePaths.importMnemonic);
|
||||
}
|
||||
|
||||
/// 跳转到备份助记词页面(账号已创建的情况)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
/// 底部导航栏组件
|
||||
/// 包含四个Tab:龙虎榜、矿机、交易、我
|
||||
/// 使用响应式设计适配各种屏幕尺寸
|
||||
/// 自定义设计风格,使用 LayoutBuilder 确保自适应
|
||||
class BottomNavBar extends StatelessWidget {
|
||||
final int currentIndex;
|
||||
final Function(int) onTap;
|
||||
|
|
@ -16,8 +15,12 @@ class BottomNavBar extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 获取底部安全区域高度
|
||||
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
|
||||
return Container(
|
||||
height: 65.h,
|
||||
// 内容高度 + 底部安全区域
|
||||
height: 56 + bottomPadding,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFFF5E6),
|
||||
border: Border(
|
||||
|
|
@ -27,10 +30,10 @@ class BottomNavBar extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
// 底部留出安全区域
|
||||
padding: EdgeInsets.only(bottom: bottomPadding),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildNavItem(
|
||||
index: 0,
|
||||
|
|
@ -74,26 +77,29 @@ class BottomNavBar extends StatelessWidget {
|
|||
child: GestureDetector(
|
||||
onTap: () => onTap(index),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
isSelected ? activeIcon : icon,
|
||||
size: 24.sp,
|
||||
color: isSelected ? const Color(0xFFD4AF37) : const Color(0xFF8B5A2B),
|
||||
),
|
||||
SizedBox(height: 2.h),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.33,
|
||||
child: SizedBox(
|
||||
height: 56,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
isSelected ? activeIcon : icon,
|
||||
size: 24,
|
||||
color: isSelected ? const Color(0xFFD4AF37) : const Color(0xFF8B5A2B),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.33,
|
||||
color: isSelected ? const Color(0xFFD4AF37) : const Color(0xFF8B5A2B),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -119,16 +119,21 @@ class _SharePageState extends ConsumerState<SharePage> {
|
|||
_buildAppBar(),
|
||||
// 内容区域
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 100),
|
||||
// QR 码区域
|
||||
_buildQrCodeSection(),
|
||||
const SizedBox(height: 40),
|
||||
// 链接输入框
|
||||
_buildLinkSection(),
|
||||
],
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// QR 码区域
|
||||
_buildQrCodeSection(),
|
||||
const SizedBox(height: 32),
|
||||
// 链接输入框
|
||||
_buildLinkSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import '../features/auth/presentation/pages/onboarding_page.dart';
|
|||
import '../features/auth/presentation/pages/backup_mnemonic_page.dart';
|
||||
import '../features/auth/presentation/pages/verify_mnemonic_page.dart';
|
||||
import '../features/auth/presentation/pages/wallet_created_page.dart';
|
||||
import '../features/auth/presentation/pages/import_mnemonic_page.dart';
|
||||
import '../features/home/presentation/pages/home_shell_page.dart';
|
||||
import '../features/ranking/presentation/pages/ranking_page.dart';
|
||||
import '../features/mining/presentation/pages/mining_page.dart';
|
||||
|
|
@ -109,6 +110,13 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
|||
builder: (context, state) => const OnboardingPage(),
|
||||
),
|
||||
|
||||
// Import Mnemonic (导入助记词)
|
||||
GoRoute(
|
||||
path: RoutePaths.importMnemonic,
|
||||
name: RouteNames.importMnemonic,
|
||||
builder: (context, state) => const ImportMnemonicPage(),
|
||||
),
|
||||
|
||||
// Backup Mnemonic (备份助记词 - 会调用 API 获取钱包信息)
|
||||
GoRoute(
|
||||
path: RoutePaths.backupMnemonic,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class RouteNames {
|
|||
static const verifyMnemonic = 'verify-mnemonic';
|
||||
static const walletCreated = 'wallet-created';
|
||||
static const importWallet = 'import-wallet';
|
||||
static const importMnemonic = 'import-mnemonic';
|
||||
|
||||
// Main tabs
|
||||
static const ranking = 'ranking';
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class RoutePaths {
|
|||
static const verifyMnemonic = '/auth/verify-mnemonic';
|
||||
static const walletCreated = '/auth/wallet-created';
|
||||
static const importWallet = '/auth/import';
|
||||
static const importMnemonic = '/auth/import-mnemonic';
|
||||
|
||||
// Main tabs
|
||||
static const ranking = '/ranking';
|
||||
|
|
|
|||
Loading…
Reference in New Issue