rwadurian/frontend/mining-app/lib/presentation/widgets/qr_scanner_sheet.dart

275 lines
8.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
/// 二维码扫描底部弹窗
/// 扫描成功后自动关闭并返回扫描结果
class QrScannerSheet extends StatefulWidget {
const QrScannerSheet({super.key});
/// 显示扫码底部弹窗
/// 返回扫描到的二维码内容,取消或失败返回 null
static Future<String?> show(BuildContext context) {
return showModalBottomSheet<String>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => const QrScannerSheet(),
);
}
@override
State<QrScannerSheet> createState() => _QrScannerSheetState();
}
class _QrScannerSheetState extends State<QrScannerSheet> {
static const Color _orange = Color(0xFFFF6B00);
static const Color _darkText = Color(0xFF1F2937);
final MobileScannerController _controller = MobileScannerController(
detectionSpeed: DetectionSpeed.normal,
facing: CameraFacing.back,
);
bool _hasScanned = false;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _onDetect(BarcodeCapture capture) {
if (_hasScanned) return;
final List<Barcode> barcodes = capture.barcodes;
if (barcodes.isEmpty) return;
final String? code = barcodes.first.rawValue;
if (code == null || code.isEmpty) return;
_hasScanned = true;
Navigator.of(context).pop(code);
}
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
return Container(
height: screenHeight * 0.7,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Column(
children: [
// 顶部拖动条
Container(
margin: const EdgeInsets.only(top: 12),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
// 标题栏
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.close, color: _darkText),
onPressed: () => Navigator.of(context).pop(),
),
const Expanded(
child: Text(
'扫描二维码',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: _darkText,
),
),
),
// 闪光灯按钮
IconButton(
icon: ValueListenableBuilder(
valueListenable: _controller,
builder: (context, state, child) {
return Icon(
state.torchState == TorchState.on
? Icons.flash_on
: Icons.flash_off,
color: state.torchState == TorchState.on
? _orange
: _darkText,
);
},
),
onPressed: () => _controller.toggleTorch(),
),
],
),
),
// 扫描区域
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 32),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Stack(
children: [
// 相机预览
MobileScanner(
controller: _controller,
onDetect: _onDetect,
),
// 扫描框装饰
Center(
child: Container(
width: 250,
height: 250,
decoration: BoxDecoration(
border: Border.all(
color: _orange.withOpacity(0.8),
width: 2,
),
borderRadius: BorderRadius.circular(12),
),
),
),
// 四角装饰
Center(
child: SizedBox(
width: 250,
height: 250,
child: Stack(
children: [
// 左上角
Positioned(
left: 0,
top: 0,
child: _buildCorner(
borderTop: true,
borderLeft: true,
),
),
// 右上角
Positioned(
right: 0,
top: 0,
child: _buildCorner(
borderTop: true,
borderRight: true,
),
),
// 左下角
Positioned(
left: 0,
bottom: 0,
child: _buildCorner(
borderBottom: true,
borderLeft: true,
),
),
// 右下角
Positioned(
right: 0,
bottom: 0,
child: _buildCorner(
borderBottom: true,
borderRight: true,
),
),
],
),
),
),
],
),
),
),
),
// 提示文字
Padding(
padding: const EdgeInsets.all(24),
child: Text(
'将二维码放入框内,即可自动扫描',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
),
],
),
);
}
Widget _buildCorner({
bool borderTop = false,
bool borderBottom = false,
bool borderLeft = false,
bool borderRight = false,
}) {
return Container(
width: 24,
height: 24,
decoration: BoxDecoration(
border: Border(
top: borderTop
? const BorderSide(color: _orange, width: 4)
: BorderSide.none,
bottom: borderBottom
? const BorderSide(color: _orange, width: 4)
: BorderSide.none,
left: borderLeft
? const BorderSide(color: _orange, width: 4)
: BorderSide.none,
right: borderRight
? const BorderSide(color: _orange, width: 4)
: BorderSide.none,
),
),
);
}
}
/// 解析转账二维码
/// 格式: durian://transfer?phone={phone}
/// 返回手机号,解析失败返回 null
String? parseTransferQrCode(String qrCode) {
try {
final uri = Uri.parse(qrCode);
// 检查 scheme
if (uri.scheme != 'durian') return null;
// 检查 host (即 authority 部分)
if (uri.host != 'transfer') return null;
// 获取 phone 参数
final phone = uri.queryParameters['phone'];
if (phone == null || phone.isEmpty) return null;
// 简单验证手机号格式
if (phone.length != 11 || !RegExp(r'^\d{11}$').hasMatch(phone)) {
return null;
}
return phone;
} catch (e) {
return null;
}
}