rwadurian/frontend/mobile-app/lib/features/auth/presentation/pages/guide_page.dart

1214 lines
38 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 '../../../../core/storage/storage_keys.dart';
import '../../../../routes/route_paths.dart';
import '../providers/auth_provider.dart';
import 'phone_register_page.dart';
/// 向导页数据模型
class GuidePageData {
final String? imagePath;
final String title;
final String subtitle;
final Widget? customContent;
const GuidePageData({
this.imagePath,
required this.title,
required this.subtitle,
this.customContent,
});
}
/// 向导页面 - 用户首次打开应用时展示
/// 支持左右滑动切换页面
class GuidePage extends ConsumerStatefulWidget {
/// 初始页面索引0-4默认为0
final int initialPage;
const GuidePage({super.key, this.initialPage = 0});
@override
ConsumerState<GuidePage> createState() => _GuidePageState();
}
class _GuidePageState extends ConsumerState<GuidePage> {
late final PageController _pageController;
late int _currentPage;
@override
void initState() {
super.initState();
// 使用传入的初始页面索引
_currentPage = widget.initialPage.clamp(0, 4);
_pageController = PageController(initialPage: _currentPage);
// 延迟到 build 后获取屏幕信息
WidgetsBinding.instance.addPostFrameCallback((_) {
_logScreenInfo();
});
}
/// 打印屏幕信息用于调试
void _logScreenInfo() {
final mediaQuery = MediaQuery.of(context);
final screenSize = mediaQuery.size;
final devicePixelRatio = mediaQuery.devicePixelRatio;
final physicalSize = screenSize * devicePixelRatio;
debugPrint('[GuidePage] ========== 屏幕信息 ==========');
debugPrint('[GuidePage] 逻辑分辨率: ${screenSize.width.toStringAsFixed(1)} x ${screenSize.height.toStringAsFixed(1)}');
debugPrint('[GuidePage] 设备像素比: $devicePixelRatio');
debugPrint('[GuidePage] 物理分辨率: ${physicalSize.width.toStringAsFixed(0)} x ${physicalSize.height.toStringAsFixed(0)}');
debugPrint('[GuidePage] 屏幕宽高比: ${(screenSize.width / screenSize.height).toStringAsFixed(3)} (${screenSize.width.toStringAsFixed(0)}:${screenSize.height.toStringAsFixed(0)})');
debugPrint('[GuidePage] 图片设计比例: 0.5625 (1080:1920 = 9:16)');
debugPrint('[GuidePage] ================================');
}
// 向导页1-5的数据 (第5页为欢迎加入页)
// 支持 png、jpg、webp 等格式
final List<GuidePageData> _guidePages = const [
GuidePageData(
imagePath: 'assets/images/guide_1.jpg',
title: '认种一棵榴莲树\n拥有真实RWA资产',
subtitle: '绑定真实果园20年收益让区块链与农业完美结合',
),
GuidePageData(
imagePath: 'assets/images/guide_2.jpg',
title: '认种即可开启算力\n自动挖矿持续收益',
subtitle: '每一棵树都对应真实资产注入,为算力提供真实价值支撑',
),
GuidePageData(
imagePath: 'assets/images/guide_3.jpg',
title: '分享链接\n获得团队算力与收益',
subtitle: '真实认种数据透明可信 · 团队越大算力越强',
),
GuidePageData(
imagePath: 'assets/images/guide_4.jpg',
title: 'MPC多方安全\n所有地址与收益可审计',
subtitle: '你的资产 · 安全透明 · 不可被篡改',
),
GuidePageData(
imagePath: 'assets/images/guide_5.jpg',
title: '欢迎加入',
subtitle: '创建账号前的最后一步 · 请选择是否有推荐人',
),
];
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
void _onPageChanged(int page) {
setState(() {
_currentPage = page;
});
}
void _goToNextPage() {
if (_currentPage < 4) {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
}
void _goToOnboarding() async {
// 标记已查看向导页
await ref.read(authProvider.notifier).markGuideAsSeen();
if (!mounted) return;
context.go(RoutePaths.onboarding);
}
/// 退出应用
void _exitApp() {
SystemNavigator.pop();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: PageView.builder(
controller: _pageController,
onPageChanged: _onPageChanged,
itemCount: 5, // 4个介绍页 + 1个欢迎加入页
itemBuilder: (context, index) {
if (index < 4) {
return _buildGuidePage(_guidePages[index], index);
} else {
return _buildWelcomePage();
}
},
),
);
}
/// 构建向导页 (页面1-4) - 全屏背景图片,无文字
/// 使用 BoxFit.cover 填满屏幕,保持宽高比(会裁剪超出部分)
Widget _buildGuidePage(GuidePageData data, int index) {
debugPrint('[GuidePage] _buildGuidePage() - 页面 ${index + 1}, 图片: ${data.imagePath}');
return Stack(
fit: StackFit.expand,
children: [
// 全屏背景图片 - 使用 cover 填满屏幕
if (data.imagePath != null)
Image.asset(
data.imagePath!,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
errorBuilder: (context, error, stackTrace) {
debugPrint('[GuidePage] 页面 ${index + 1} 图片加载失败: $error');
return Container(
color: const Color(0xFFFFF8E7),
child: _buildPlaceholderImage(index),
);
},
)
else
Container(
color: const Color(0xFFFFF8E7),
child: _buildPlaceholderImage(index),
),
// 底部页面指示器 - 使用 SafeArea 避免被系统 UI 遮挡
Positioned(
left: 0,
right: 0,
bottom: 0,
child: SafeArea(
child: Padding(
padding: EdgeInsets.only(bottom: 40.h),
child: _buildPageIndicator(),
),
),
),
],
);
}
/// 构建占位图片
Widget _buildPlaceholderImage(int index) {
final icons = [
Icons.nature,
Icons.memory,
Icons.people,
Icons.security,
];
final colors = [
const Color(0xFF8BC34A),
const Color(0xFFD4AF37),
const Color(0xFFFF9800),
const Color(0xFF2196F3),
];
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icons[index],
size: 80.sp,
color: colors[index],
),
SizedBox(height: 16.h),
Text(
'向导页 ${index + 1}',
style: TextStyle(
fontSize: 16.sp,
color: const Color(0xFF57534E),
),
),
],
),
);
}
/// 构建欢迎加入页面 (页面5)
Widget _buildWelcomePage() {
return _WelcomePageContent(
onNext: _goToOnboarding,
onExit: _exitApp, // 退出整个应用
backgroundImage: _guidePages[4].imagePath,
pageIndicator: _buildPageIndicator(),
);
}
/// 构建页面指示器 (所有页面都使用白色系,因为都有背景图)
Widget _buildPageIndicator() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(5, (index) {
final isActive = index == _currentPage;
return Container(
width: 8.w,
height: 8.w,
margin: EdgeInsets.symmetric(horizontal: 4.w),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isActive
? Colors.white
: Colors.white.withValues(alpha: 0.4),
),
);
}),
);
}
}
/// 欢迎加入页面内容 (第5页)
class _WelcomePageContent extends ConsumerStatefulWidget {
final VoidCallback onNext;
final VoidCallback onExit;
final String? backgroundImage;
final Widget pageIndicator;
const _WelcomePageContent({
required this.onNext,
required this.onExit,
this.backgroundImage,
required this.pageIndicator,
});
@override
ConsumerState<_WelcomePageContent> createState() => _WelcomePageContentState();
}
class _WelcomePageContentState extends ConsumerState<_WelcomePageContent> {
bool _hasReferrer = true;
final TextEditingController _referralCodeController = TextEditingController();
@override
void initState() {
super.initState();
// 监听输入框变化以更新按钮状态
_referralCodeController.addListener(_onReferralCodeChanged);
}
@override
void dispose() {
_referralCodeController.removeListener(_onReferralCodeChanged);
_referralCodeController.dispose();
super.dispose();
}
/// 输入框内容变化时刷新UI
void _onReferralCodeChanged() {
setState(() {});
}
/// 验证推荐码是否有效
/// 支持: 纯推荐码(至少3个字符) 或 合法URL
bool _isValidReferralCode(String code) {
final trimmed = code.trim();
if (trimmed.isEmpty) return false;
// 如果是URL格式检查是否能解析
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
try {
final uri = Uri.parse(trimmed);
// URL必须有路径或查询参数
return uri.pathSegments.isNotEmpty || uri.queryParameters.isNotEmpty;
} catch (e) {
return false;
}
}
// 纯推荐码至少3个字符
return trimmed.length >= 3;
}
/// 判断按钮是否可点击
bool get _canProceed {
// 如果选择"没有推荐人",按钮可点击
if (!_hasReferrer) return true;
// 如果选择"有推荐人",需要填写有效的推荐码
return _isValidReferralCode(_referralCodeController.text);
}
/// 导入助记词恢复账号
void _importMnemonic() {
debugPrint('[GuidePage] _importMnemonic - 跳转到导入助记词页面');
context.push(RoutePaths.importMnemonic);
}
/// 手机号注册
Future<void> _goToPhoneRegister() async {
debugPrint('[GuidePage] _goToPhoneRegister - 跳转到手机号注册页面');
// 如果有推荐人且推荐码有效,先保存到本地存储
String? inviterCode;
if (_hasReferrer && _referralCodeController.text.trim().isNotEmpty) {
inviterCode = _referralCodeController.text.trim();
final secureStorage = ref.read(secureStorageProvider);
await secureStorage.write(
key: StorageKeys.inviterReferralCode,
value: inviterCode,
);
debugPrint('[GuidePage] 保存邀请人推荐码: $inviterCode');
}
if (!mounted) return;
context.push(
RoutePaths.phoneRegister,
extra: PhoneRegisterParams(inviterReferralCode: inviterCode),
);
}
/// 保存推荐码并继续下一步 (跳转到手机号注册页面)
Future<void> _saveReferralCodeAndProceed() async {
if (!_canProceed) return;
// 如果有推荐人且推荐码有效,保存到本地存储
String? inviterCode;
if (_hasReferrer && _referralCodeController.text.trim().isNotEmpty) {
inviterCode = _referralCodeController.text.trim();
final secureStorage = ref.read(secureStorageProvider);
await secureStorage.write(
key: StorageKeys.inviterReferralCode,
value: inviterCode,
);
debugPrint('[GuidePage] 保存邀请人推荐码: $inviterCode');
}
if (!mounted) return;
// 跳转到手机号注册页面
context.push(
RoutePaths.phoneRegister,
extra: PhoneRegisterParams(inviterReferralCode: inviterCode),
);
}
/// 打开二维码扫描页面
Future<void> _openQrScanner() async {
final result = await Navigator.of(context).push<String>(
MaterialPageRoute(
builder: (context) => const _QrScannerPage(),
),
);
if (result != null && result.isNotEmpty) {
// 从扫描结果中提取推荐码
final referralCode = _extractReferralCode(result);
setState(() {
_referralCodeController.text = referralCode;
_hasReferrer = true;
});
}
}
/// 从扫描结果中提取推荐码
/// 支持以下格式:
/// - 查询参数: https://app.rwadurian.com/share?ref=ABC123 -> ABC123
/// - 路径格式: https://app.rwadurian.com/r/ABC123 -> ABC123
/// - 短链: https://rwa.link/ABC123 -> ABC123
/// - 纯推荐码: ABC123 -> ABC123
String _extractReferralCode(String scannedData) {
// 去除首尾空格
String data = scannedData.trim();
// 如果是 URL尝试提取推荐码
if (data.startsWith('http://') || data.startsWith('https://')) {
try {
final uri = Uri.parse(data);
// 优先从查询参数中提取 (如 ?ref=ABC123 或 ?code=ABC123)
final refCode = uri.queryParameters['ref'] ??
uri.queryParameters['code'] ??
uri.queryParameters['referral'];
if (refCode != null && refCode.isNotEmpty) {
return refCode;
}
// 其次从路径中提取 (如 /r/ABC123 或 /ABC123)
final pathSegments = uri.pathSegments;
if (pathSegments.isNotEmpty) {
// 取最后一个路径段作为推荐码
final lastSegment = pathSegments.last;
// 排除常见的非推荐码路径段
const excludedSegments = ['share', 'invite', 'r', 'ref', 'referral'];
if (lastSegment.isNotEmpty && !excludedSegments.contains(lastSegment.toLowerCase())) {
return lastSegment;
}
// 如果最后一段是排除项且有倒数第二段,检查倒数第二段
if (pathSegments.length >= 2) {
final secondLastSegment = pathSegments[pathSegments.length - 2];
if (secondLastSegment.isNotEmpty && !excludedSegments.contains(secondLastSegment.toLowerCase())) {
return secondLastSegment;
}
}
}
} catch (e) {
// URL 解析失败,返回原始数据
debugPrint('URL 解析失败: $e');
}
}
// 不是 URL 或解析失败,返回原始数据
return data;
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Stack(
fit: StackFit.expand,
children: [
// 全屏背景图片 - 使用 cover 填满屏幕
if (widget.backgroundImage != null)
Image.asset(
widget.backgroundImage!,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
errorBuilder: (context, error, stackTrace) {
return Container(
color: const Color(0xFFFFF8E7),
);
},
)
else
Container(
color: const Color(0xFFFFF8E7),
),
// 半透明遮罩,让内容更清晰
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.3),
Colors.black.withValues(alpha: 0.6),
],
),
),
),
// 内容区域
SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Column(
children: [
// 退出按钮
Align(
alignment: Alignment.topRight,
child: Padding(
padding: EdgeInsets.only(top: 16.h),
child: GestureDetector(
onTap: widget.onExit,
child: Text(
'退出',
style: TextStyle(
fontSize: 14.sp,
color: Colors.white.withValues(alpha: 0.8),
),
),
),
),
),
SizedBox(height: 60.h),
// 欢迎标题
Text(
'欢迎加入',
style: TextStyle(
fontSize: 28.sp,
fontWeight: FontWeight.w700,
height: 1.33,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black.withValues(alpha: 0.5),
blurRadius: 4,
),
],
),
),
SizedBox(height: 12.h),
// 副标题
Text(
'创建账号前的最后一步 · 请选择是否有推荐人',
style: TextStyle(
fontSize: 14.sp,
height: 1.43,
color: Colors.white.withValues(alpha: 0.9),
shadows: [
Shadow(
color: Colors.black.withValues(alpha: 0.5),
blurRadius: 4,
),
],
),
),
SizedBox(height: 48.h),
// 选项区域 (透明背景)
_buildReferrerOptions(),
const Spacer(),
// 页面指示器
widget.pageIndicator,
SizedBox(height: 24.h),
// 下一步按钮
GestureDetector(
onTap: _canProceed ? _saveReferralCodeAndProceed : null,
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 16.h),
decoration: BoxDecoration(
color: _canProceed
? const Color(0xFFD4A84B)
: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
'下一步 (创建账号)',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
height: 1.5,
color: _canProceed
? Colors.white
: Colors.white.withValues(alpha: 0.6),
),
textAlign: TextAlign.center,
),
),
),
SizedBox(height: 32.h),
],
),
),
),
),
);
},
),
),
],
),
);
}
/// 构建推荐人选项 (透明背景,白色文字)
Widget _buildReferrerOptions() {
return Column(
children: [
// 有推荐人选项
GestureDetector(
onTap: () {
setState(() {
_hasReferrer = true;
});
},
child: Row(
children: [
_buildRadio(_hasReferrer),
SizedBox(width: 12.w),
Text(
'我有推荐人',
style: TextStyle(
fontSize: 16.sp,
height: 1.5,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black.withValues(alpha: 0.5),
blurRadius: 4,
),
],
),
),
],
),
),
SizedBox(height: 14.h),
// 推荐码输入框 - 使用 AnimatedSize 平滑过渡
AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: _hasReferrer
? Container(
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 4.w),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1,
color: Colors.white.withValues(alpha: 0.5),
),
),
),
child: Row(
children: [
Expanded(
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,
),
),
),
),
),
// 扫码按钮
GestureDetector(
onTap: _openQrScanner,
child: Padding(
padding: EdgeInsets.only(left: 8.w),
child: Icon(
Icons.camera_alt_outlined,
size: 20.sp,
color: Colors.white.withValues(alpha: 0.8),
),
),
),
],
),
)
: const SizedBox.shrink(),
),
SizedBox(height: 24.h),
// 没有推荐人选项
GestureDetector(
onTap: () {
setState(() {
_hasReferrer = false;
});
},
child: Row(
children: [
_buildRadio(!_hasReferrer),
SizedBox(width: 12.w),
Text(
'我没有推荐人',
style: TextStyle(
fontSize: 16.sp,
height: 1.5,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black.withValues(alpha: 0.5),
blurRadius: 4,
),
],
),
),
],
),
),
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: _goToPhoneRegister,
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.phone_android,
size: 20.sp,
color: Colors.white,
),
SizedBox(width: 8.w),
Text(
'使用手机号注册',
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
],
),
),
),
SizedBox(height: 12.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,
),
),
],
),
),
),
],
);
}
/// 构建单选按钮 (白色系,适配深色背景)
Widget _buildRadio(bool isSelected) {
return Container(
width: 20.w,
height: 20.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
width: isSelected ? 6.w : 2.w,
color: isSelected
? Colors.white
: Colors.white.withValues(alpha: 0.5),
),
),
);
}
}
/// 二维码扫描页面
class _QrScannerPage extends StatefulWidget {
const _QrScannerPage();
@override
State<_QrScannerPage> createState() => _QrScannerPageState();
}
class _QrScannerPageState extends State<_QrScannerPage> {
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<Barcode> 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<void> _toggleTorch() async {
await _controller?.toggleTorch();
setState(() {
_torchOn = !_torchOn;
});
}
/// 从相册选择图片并扫描二维码
Future<void> _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<Color>(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(0xFFD4A84B),
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;
}