diff --git a/frontend/mobile-app/android/app/src/main/AndroidManifest.xml b/frontend/mobile-app/android/app/src/main/AndroidManifest.xml
index 09bf6b63..d59fc668 100644
--- a/frontend/mobile-app/android/app/src/main/AndroidManifest.xml
+++ b/frontend/mobile-app/android/app/src/main/AndroidManifest.xml
@@ -36,6 +36,11 @@
android:name="flutterEmbedding"
android:value="2" />
+
+
+
{
return _isValidReferralCode(_referralCodeController.text);
}
+ /// 导入助记词恢复账号
+ void _importMnemonic() {
+ debugPrint('[GuidePage] _importMnemonic - 跳转到导入助记词页面');
+ Navigator.of(context).pushNamed(RoutePaths.importMnemonic);
+ }
+
/// 保存推荐码并继续下一步
Future _saveReferralCodeAndProceed() async {
if (!_canProceed) return;
@@ -614,23 +620,28 @@ class _WelcomePageContentState extends ConsumerState<_WelcomePageContent> {
child: Row(
children: [
Expanded(
- child: TextField(
- controller: _referralCodeController,
- decoration: InputDecoration(
- hintText: '请输入推荐码 / 序列号',
- hintStyle: TextStyle(
- fontSize: 16.sp,
- color: Colors.black.withValues(alpha: 0.4),
+ child: GestureDetector(
+ onTap: _openQrScanner,
+ child: AbsorbPointer(
+ child: TextField(
+ controller: _referralCodeController,
+ readOnly: true,
+ decoration: InputDecoration(
+ hintText: '点击扫描推荐码',
+ hintStyle: TextStyle(
+ fontSize: 16.sp,
+ color: Colors.black.withValues(alpha: 0.4),
+ ),
+ border: InputBorder.none,
+ isDense: true,
+ contentPadding: EdgeInsets.zero,
+ ),
+ style: TextStyle(
+ fontSize: 16.sp,
+ color: Colors.black,
+ ),
),
- border: InputBorder.none,
- isDense: true,
- contentPadding: EdgeInsets.zero,
),
- style: TextStyle(
- fontSize: 16.sp,
- color: Colors.black,
- ),
- cursorColor: Colors.black,
),
),
// 扫码按钮
@@ -679,6 +690,70 @@ class _WelcomePageContentState extends ConsumerState<_WelcomePageContent> {
],
),
),
+ SizedBox(height: 32.h),
+ // 分隔线
+ Row(
+ children: [
+ Expanded(
+ child: Container(
+ height: 1,
+ color: Colors.white.withValues(alpha: 0.3),
+ ),
+ ),
+ Padding(
+ padding: EdgeInsets.symmetric(horizontal: 16.w),
+ child: Text(
+ '或',
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: Colors.white.withValues(alpha: 0.7),
+ ),
+ ),
+ ),
+ Expanded(
+ child: Container(
+ height: 1,
+ color: Colors.white.withValues(alpha: 0.3),
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 24.h),
+ // 导入助记词入口
+ GestureDetector(
+ onTap: _importMnemonic,
+ child: Container(
+ width: double.infinity,
+ padding: EdgeInsets.symmetric(vertical: 14.h),
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.15),
+ borderRadius: BorderRadius.circular(12.r),
+ border: Border.all(
+ color: Colors.white.withValues(alpha: 0.3),
+ width: 1,
+ ),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.restore,
+ size: 20.sp,
+ color: Colors.white,
+ ),
+ SizedBox(width: 8.w),
+ Text(
+ '已有账号?导入助记词恢复',
+ style: TextStyle(
+ fontSize: 15.sp,
+ fontWeight: FontWeight.w500,
+ color: Colors.white,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
],
);
}
diff --git a/frontend/mobile-app/lib/features/auth/presentation/pages/splash_page.dart b/frontend/mobile-app/lib/features/auth/presentation/pages/splash_page.dart
index d2b906f6..40e0c95f 100644
--- a/frontend/mobile-app/lib/features/auth/presentation/pages/splash_page.dart
+++ b/frontend/mobile-app/lib/features/auth/presentation/pages/splash_page.dart
@@ -196,55 +196,14 @@ class _SplashPageState extends ConsumerState {
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo
- Container(
+ Image.asset(
+ 'assets/images/logo/app_icon.png',
width: 128,
- height: 152,
- decoration: BoxDecoration(
- color: const Color(0xFF2D2A26),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Container(
- width: 64,
- height: 48,
- decoration: BoxDecoration(
- color: const Color(0xFFD4A84B),
- borderRadius: BorderRadius.circular(8),
- ),
- child: const Icon(
- Icons.workspace_premium,
- size: 32,
- color: Color(0xFF2D2A26),
- ),
- ),
- const SizedBox(height: 16),
- const Text(
- 'DURIAN',
- style: TextStyle(
- fontSize: 12,
- fontWeight: FontWeight.w600,
- letterSpacing: 2,
- color: Color(0xFFD4A84B),
- ),
- ),
- const SizedBox(height: 2),
- const Text(
- 'QUEEN',
- style: TextStyle(
- fontSize: 10,
- fontWeight: FontWeight.w400,
- letterSpacing: 1.5,
- color: Color(0xFFD4A84B),
- ),
- ),
- ],
- ),
+ height: 128,
),
const SizedBox(height: 24),
const Text(
- '榴莲女皇',
+ '榴莲皇后',
style: TextStyle(
fontSize: 32,
fontFamily: 'Noto Sans SC',
diff --git a/frontend/mobile-app/lib/features/withdraw/presentation/pages/withdraw_usdt_page.dart b/frontend/mobile-app/lib/features/withdraw/presentation/pages/withdraw_usdt_page.dart
index aa54cd6c..2ba8bdfb 100644
--- a/frontend/mobile-app/lib/features/withdraw/presentation/pages/withdraw_usdt_page.dart
+++ b/frontend/mobile-app/lib/features/withdraw/presentation/pages/withdraw_usdt_page.dart
@@ -1,7 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:mobile_scanner/mobile_scanner.dart';
import '../../../../core/di/injection_container.dart';
import '../../../../routes/route_paths.dart';
@@ -195,6 +198,47 @@ class _WithdrawUsdtPageState extends ConsumerState {
);
}
+ /// 打开二维码扫描页面
+ Future _openQrScanner() async {
+ final result = await Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (context) => const _AddressQrScannerPage(),
+ ),
+ );
+ if (result != null && result.isNotEmpty) {
+ // 从扫描结果中提取地址
+ final address = _extractAddress(result);
+ setState(() {
+ _addressController.text = address;
+ });
+ }
+ }
+
+ /// 从扫描结果中提取地址
+ /// 支持以下格式:
+ /// - 纯地址: 0x1234... 或 kava1...
+ /// - EIP-681 格式: ethereum:0x1234...
+ /// - 带参数: ethereum:0x1234...?value=100
+ String _extractAddress(String scannedData) {
+ String data = scannedData.trim();
+
+ // 处理 EIP-681 格式 (ethereum:0x... 或 kava:kava1...)
+ if (data.contains(':')) {
+ final colonIndex = data.indexOf(':');
+ data = data.substring(colonIndex + 1);
+
+ // 移除可能的参数 (?value=... 或 @chainId)
+ if (data.contains('?')) {
+ data = data.substring(0, data.indexOf('?'));
+ }
+ if (data.contains('@')) {
+ data = data.substring(0, data.indexOf('@'));
+ }
+ }
+
+ return data.trim();
+ }
+
/// 格式化数字(添加千分位)
String _formatNumber(double number) {
final parts = number.toStringAsFixed(2).split('.');
@@ -545,16 +589,11 @@ class _WithdrawUsdtPageState extends ConsumerState {
),
suffixIcon: IconButton(
icon: const Icon(
- Icons.content_paste,
+ Icons.qr_code_scanner,
color: Color(0xFF8B5A2B),
- size: 20,
+ size: 22,
),
- onPressed: () async {
- final data = await Clipboard.getData('text/plain');
- if (data?.text != null) {
- _addressController.text = data!.text!;
- }
- },
+ onPressed: _openQrScanner,
),
),
),
@@ -836,3 +875,366 @@ class _WithdrawUsdtPageState extends ConsumerState {
);
}
}
+
+/// 地址二维码扫描页面
+class _AddressQrScannerPage extends StatefulWidget {
+ const _AddressQrScannerPage();
+
+ @override
+ State<_AddressQrScannerPage> createState() => _AddressQrScannerPageState();
+}
+
+class _AddressQrScannerPageState extends State<_AddressQrScannerPage> {
+ MobileScannerController? _controller;
+ bool _hasScanned = false;
+ bool _torchOn = false;
+ bool _isProcessingImage = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = MobileScannerController(
+ detectionSpeed: DetectionSpeed.normal,
+ facing: CameraFacing.back,
+ );
+ }
+
+ @override
+ void dispose() {
+ _controller?.dispose();
+ super.dispose();
+ }
+
+ void _onDetect(BarcodeCapture capture) {
+ if (_hasScanned) return;
+
+ final List barcodes = capture.barcodes;
+ for (final barcode in barcodes) {
+ if (barcode.rawValue != null && barcode.rawValue!.isNotEmpty) {
+ _hasScanned = true;
+ Navigator.of(context).pop(barcode.rawValue);
+ return;
+ }
+ }
+ }
+
+ Future _toggleTorch() async {
+ await _controller?.toggleTorch();
+ setState(() {
+ _torchOn = !_torchOn;
+ });
+ }
+
+ /// 从相册选择图片并扫描二维码
+ Future _pickImageAndScan() async {
+ if (_isProcessingImage || _hasScanned) return;
+
+ setState(() {
+ _isProcessingImage = true;
+ });
+
+ try {
+ final ImagePicker picker = ImagePicker();
+ final XFile? image = await picker.pickImage(source: ImageSource.gallery);
+
+ if (image == null) {
+ setState(() {
+ _isProcessingImage = false;
+ });
+ return;
+ }
+
+ // 使用 MobileScannerController 分析图片
+ final BarcodeCapture? result = await _controller?.analyzeImage(image.path);
+
+ if (result != null && result.barcodes.isNotEmpty) {
+ for (final barcode in result.barcodes) {
+ if (barcode.rawValue != null && barcode.rawValue!.isNotEmpty) {
+ _hasScanned = true;
+ if (mounted) {
+ Navigator.of(context).pop(barcode.rawValue);
+ }
+ return;
+ }
+ }
+ }
+
+ // 未识别到二维码
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ '未能识别图片中的二维码,请重新选择',
+ style: TextStyle(fontSize: 14.sp),
+ ),
+ backgroundColor: const Color(0xFF6F6354),
+ behavior: SnackBarBehavior.floating,
+ margin: EdgeInsets.all(16.w),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8.r),
+ ),
+ ),
+ );
+ }
+ } catch (e) {
+ debugPrint('扫描图片失败: $e');
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ '图片扫描失败,请重试',
+ style: TextStyle(fontSize: 14.sp),
+ ),
+ backgroundColor: Colors.red,
+ behavior: SnackBarBehavior.floating,
+ margin: EdgeInsets.all(16.w),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8.r),
+ ),
+ ),
+ );
+ }
+ } finally {
+ if (mounted) {
+ setState(() {
+ _isProcessingImage = false;
+ });
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.black,
+ appBar: AppBar(
+ backgroundColor: Colors.black,
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back, color: Colors.white),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ title: Text(
+ '扫描钱包地址',
+ style: TextStyle(
+ fontSize: 18.sp,
+ color: Colors.white,
+ ),
+ ),
+ centerTitle: true,
+ actions: [
+ IconButton(
+ icon: Icon(
+ _torchOn ? Icons.flash_on : Icons.flash_off,
+ color: Colors.white,
+ ),
+ onPressed: _toggleTorch,
+ ),
+ ],
+ ),
+ body: Stack(
+ children: [
+ // 扫描区域
+ MobileScanner(
+ controller: _controller,
+ onDetect: _onDetect,
+ ),
+ // 扫描框遮罩
+ _buildScanOverlay(),
+ // 底部区域:提示文字和相册按钮
+ Positioned(
+ bottom: 60.h,
+ left: 0,
+ right: 0,
+ child: Column(
+ children: [
+ Text(
+ '将钱包地址二维码放入框内',
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: Colors.white70,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ SizedBox(height: 32.h),
+ // 相册按钮
+ GestureDetector(
+ onTap: _isProcessingImage ? null : _pickImageAndScan,
+ child: Container(
+ padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 12.h),
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.15),
+ borderRadius: BorderRadius.circular(24.r),
+ border: Border.all(
+ color: Colors.white.withValues(alpha: 0.3),
+ width: 1,
+ ),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (_isProcessingImage)
+ SizedBox(
+ width: 18.sp,
+ height: 18.sp,
+ child: const CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor: AlwaysStoppedAnimation(Colors.white),
+ ),
+ )
+ else
+ Icon(
+ Icons.photo_library_outlined,
+ size: 18.sp,
+ color: Colors.white,
+ ),
+ SizedBox(width: 8.w),
+ Text(
+ _isProcessingImage ? '识别中...' : '从相册选择',
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: Colors.white,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ /// 构建扫描框遮罩
+ Widget _buildScanOverlay() {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ final scanAreaSize = 250.w;
+ final left = (constraints.maxWidth - scanAreaSize) / 2;
+ final top = (constraints.maxHeight - scanAreaSize) / 2 - 50.h;
+
+ return Stack(
+ children: [
+ // 半透明背景
+ ColorFiltered(
+ colorFilter: ColorFilter.mode(
+ Colors.black.withValues(alpha: 0.6),
+ BlendMode.srcOut,
+ ),
+ child: Stack(
+ children: [
+ Container(
+ decoration: const BoxDecoration(
+ color: Colors.black,
+ backgroundBlendMode: BlendMode.dstOut,
+ ),
+ ),
+ Positioned(
+ left: left,
+ top: top,
+ child: Container(
+ width: scanAreaSize,
+ height: scanAreaSize,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12.r),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ // 扫描框边角
+ Positioned(
+ left: left,
+ top: top,
+ child: _buildCorner(true, true),
+ ),
+ Positioned(
+ right: left,
+ top: top,
+ child: _buildCorner(false, true),
+ ),
+ Positioned(
+ left: left,
+ bottom: constraints.maxHeight - top - scanAreaSize,
+ child: _buildCorner(true, false),
+ ),
+ Positioned(
+ right: left,
+ bottom: constraints.maxHeight - top - scanAreaSize,
+ child: _buildCorner(false, false),
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+ /// 构建边角装饰
+ Widget _buildCorner(bool isLeft, bool isTop) {
+ return SizedBox(
+ width: 24.w,
+ height: 24.w,
+ child: CustomPaint(
+ painter: _CornerPainter(
+ isLeft: isLeft,
+ isTop: isTop,
+ color: const Color(0xFFD4AF37),
+ strokeWidth: 3.w,
+ ),
+ ),
+ );
+ }
+}
+
+/// 边角绘制器
+class _CornerPainter extends CustomPainter {
+ final bool isLeft;
+ final bool isTop;
+ final Color color;
+ final double strokeWidth;
+
+ _CornerPainter({
+ required this.isLeft,
+ required this.isTop,
+ required this.color,
+ required this.strokeWidth,
+ });
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ final paint = Paint()
+ ..color = color
+ ..strokeWidth = strokeWidth
+ ..style = PaintingStyle.stroke
+ ..strokeCap = StrokeCap.round;
+
+ final path = Path();
+
+ if (isLeft && isTop) {
+ path.moveTo(0, size.height);
+ path.lineTo(0, 0);
+ path.lineTo(size.width, 0);
+ } else if (!isLeft && isTop) {
+ path.moveTo(0, 0);
+ path.lineTo(size.width, 0);
+ path.lineTo(size.width, size.height);
+ } else if (isLeft && !isTop) {
+ path.moveTo(0, 0);
+ path.lineTo(0, size.height);
+ path.lineTo(size.width, size.height);
+ } else {
+ path.moveTo(0, size.height);
+ path.lineTo(size.width, size.height);
+ path.lineTo(size.width, 0);
+ }
+
+ canvas.drawPath(path, paint);
+ }
+
+ @override
+ bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
+}