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

1109 lines
35 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> {
// 移除 _hasReferrer推荐码现在是必填的
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 {
// 必须填写有效的推荐码才能继续
return _isValidReferralCode(_referralCodeController.text);
}
/// 导入助记词恢复账号
void _importMnemonic() {
debugPrint('[GuidePage] _importMnemonic - 跳转到导入助记词页面');
context.push(RoutePaths.importMnemonic);
}
/// 保存推荐码并继续下一步 (跳转到手机号注册页面)
Future<void> _saveReferralCodeAndProceed() async {
if (!_canProceed) return;
// 推荐码必填,保存到本地存储
final 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;
});
}
}
/// 从扫描结果中提取推荐码
/// 支持以下格式:
/// - 查询参数: 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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 推荐码输入框 - 支持手动输入和扫码
Container(
padding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 4.w),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.white.withValues(alpha: 0.8),
width: 2,
),
),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _referralCodeController,
decoration: InputDecoration(
hintText: '请输入推荐码或点击右侧扫码',
hintStyle: TextStyle(
fontSize: 16.sp,
color: Colors.white.withValues(alpha: 0.6),
),
border: InputBorder.none,
isDense: false,
contentPadding: EdgeInsets.symmetric(vertical: 4.h),
),
style: TextStyle(
fontSize: 17.sp,
color: const Color(0xFFFFD700),
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
),
),
SizedBox(width: 16.w),
// 扫码按钮
GestureDetector(
onTap: _openQrScanner,
child: Container(
padding: EdgeInsets.all(8.w),
child: Icon(
Icons.qr_code_scanner,
size: 28.sp,
color: Colors.white,
),
),
),
],
),
),
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: () {
debugPrint('[GuidePage] 跳转到恢复账号页面');
context.push(RoutePaths.phoneLogin);
},
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.login,
size: 20.sp,
color: Colors.white,
),
SizedBox(width: 8.w),
Text(
'恢复账号',
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
],
),
),
),
// 导入助记词入口 - 保留代码但隐藏
if (false) // 设置为 false 隐藏,需要时改为 true
Padding(
padding: EdgeInsets.only(top: 12.h),
child: 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,
),
),
],
),
),
),
),
],
);
}
}
/// 二维码扫描页面
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;
}