Files
ttstd_family_care/lib/features/device/presentation/device_scan_page.dart
2026-08-21 17:12:17 +08:00

229 lines
7.3 KiB
Dart

import 'package:cupertino_ui/cupertino_ui.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import '../../../core/widgets/feature_ui.dart';
import '../../../core/widgets/settings_ui.dart';
import '../../../l10n/app_localizations.dart';
/// 二维码扫描页。
///
/// 支持两种识别方式:
/// 1. 相机实时扫码([MobileScanner] 的 `onDetect` 回调);
/// 2. 从相册选取图片后离线解析([MobileScannerController.analyzeImage])。
///
/// 识别成功后通过 `context.pop(code)` 把结果回传给调用方(绑定设备页)。
class DeviceScanPage extends StatefulWidget {
const DeviceScanPage({super.key});
@override
State<DeviceScanPage> createState() => _DeviceScanPageState();
}
class _DeviceScanPageState extends State<DeviceScanPage> {
final MobileScannerController _controller = MobileScannerController(
detectionSpeed: DetectionSpeed.noDuplicates,
formats: const [BarcodeFormat.qrCode],
);
/// 防止连续回调导致多次 pop。
bool _handled = false;
/// 相册解析中,避免重复选图。
bool _pickingImage = false;
bool _torchOn = false;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.scanTitle),
leading: CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.back),
onPressed: () => context.pop(),
),
trailing: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: _toggleTorch,
child: Icon(
_torchOn ? CupertinoIcons.lightbulb_fill : CupertinoIcons.lightbulb,
size: 24,
),
),
),
child: SafeArea(
child: Column(
children: [
Expanded(
child: Stack(
fit: StackFit.expand,
children: [
MobileScanner(
controller: _controller,
onDetect: (capture) => _onDetect(capture, l10n),
errorBuilder: (context, error, child) => _PermissionHint(
message: l10n.scanPermissionDenied,
),
),
// 取景框 + 提示文案
IgnorePointer(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 230,
height: 230,
decoration: BoxDecoration(
border: Border.all(
color: AppColors.green,
width: 3,
),
borderRadius: BorderRadius.circular(20),
),
),
const SizedBox(height: 18),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Text(
l10n.scanHint,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 13.5,
color: CupertinoColors.white,
height: 1.5,
),
),
),
],
),
),
),
if (_pickingImage)
const Positioned.fill(
child: ColoredBox(
color: Color(0x66000000),
child: Center(child: CupertinoActivityIndicator(radius: 14)),
),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: SecondaryButton(
text: l10n.scanFromGallery,
icon: CupertinoIcons.photo_on_rectangle,
onPressed: _pickingImage ? null : () => _pickFromGallery(l10n),
),
),
],
),
),
);
}
/// 相机识别回调:取首个非空条码值后回传并关闭页面。
void _onDetect(BarcodeCapture capture, AppLocalizations l10n) {
if (_handled) return;
final code = _firstValue(capture);
if (code == null) return;
_handled = true;
context.pop(code);
}
/// 从相册选取图片并离线解析二维码。
Future<void> _pickFromGallery(AppLocalizations l10n) async {
setState(() => _pickingImage = true);
try {
final picked = await ImagePicker().pickImage(source: ImageSource.gallery);
if (picked == null) return;
final capture = await _controller.analyzeImage(picked.path);
final code = capture == null ? null : _firstValue(capture);
if (!mounted) return;
if (code == null) {
await AppDialogs.alert(
context,
message: l10n.scanNoCodeFound,
confirmText: l10n.commonConfirm,
);
return;
}
if (!mounted) return;
_handled = true;
context.pop(code);
} finally {
if (mounted) setState(() => _pickingImage = false);
}
}
/// 切换闪光灯,失败时静默忽略(部分设备/模拟器不支持)。
Future<void> _toggleTorch() async {
try {
await _controller.toggleTorch();
if (mounted) setState(() => _torchOn = !_torchOn);
} catch (_) {
// 不支持闪光灯,忽略。
}
}
/// 提取扫描结果中第一个有效的二维码文本。
String? _firstValue(BarcodeCapture capture) {
for (final barcode in capture.barcodes) {
final value = barcode.rawValue;
if (value != null && value.trim().isNotEmpty) return value.trim();
}
return null;
}
}
/// 相机不可用(权限被拒或初始化失败)时的占位提示。
class _PermissionHint extends StatelessWidget {
const _PermissionHint({required this.message});
final String message;
@override
Widget build(BuildContext context) => ColoredBox(
color: const Color(0xFF111827),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
CupertinoIcons.video_camera,
size: 46,
color: CupertinoColors.white,
),
const SizedBox(height: 14),
Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
color: CupertinoColors.white,
height: 1.5,
),
),
],
),
),
),
);
}