import 'dart:async'; import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:star_lock/app_settings/app_colors.dart'; import 'package:star_lock/network/api_repository.dart'; class RemoteUnlockOverlay { static Future show({required int lockId, required String lockAlias, required int operateDate, int timeoutSeconds = 60}) async { await Get.to(() => RemoteUnlockOverlayPage(lockId: lockId, lockAlias: lockAlias, operateDate: operateDate, timeoutSeconds: timeoutSeconds), opaque: false); } } class RemoteUnlockOverlayPage extends StatefulWidget { const RemoteUnlockOverlayPage({required this.lockId, required this.lockAlias, required this.operateDate, this.timeoutSeconds = 60, Key? key}) : super(key: key); final int lockId; final String lockAlias; final int operateDate; final int timeoutSeconds; @override State createState() => _RemoteUnlockOverlayPageState(); } class _RemoteUnlockOverlayPageState extends State { late int seconds; Timer? timer; @override void initState() { super.initState(); seconds = widget.timeoutSeconds; timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { if (!mounted) { return; } if (seconds <= 0) { t.cancel(); timer?.cancel(); setState(() { seconds = 0; }); Get.back(); return; } setState(() { seconds = seconds - 1; }); }); } @override void dispose() { timer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { final double cardWidth = 0.9.sw; final double ringSize = 260.r; return PopScope( canPop: false, child: Stack( children: [ Positioned.fill( child: BackdropFilter( filter: ui.ImageFilter.blur(sigmaX: 3, sigmaY: 3), child: Container(color: Colors.black.withOpacity(0.18)), ), ), Center( child: Container( width: cardWidth, padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 20.w), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16.r), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 20.r, offset: const Offset(0, 6))], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Icon(Icons.lock, color: AppColors.mainColor, size: 24.r), SizedBox(width: 8.w), Expanded(child: Text(widget.lockAlias, style: TextStyle(fontSize: 22.sp, color: AppColors.blackColor))), ], ), SizedBox(height: 12.h), Text('远程开锁请求'.tr, style: TextStyle(fontSize: 28.sp, color: AppColors.blackColor, fontWeight: FontWeight.w700)), SizedBox(height: 20.h), SizedBox( width: ringSize, height: ringSize, child: Stack( alignment: Alignment.center, children: [ _DottedCountdownRing(seconds: seconds, size: ringSize), Text('${seconds} s', style: TextStyle(fontSize: 48.sp, color: AppColors.mainColor, fontWeight: FontWeight.w700)), ], ), ), SizedBox(height: 16.h), Text('请确认是否允许该门锁进行远程开锁'.tr, style: TextStyle(fontSize: 18.sp, color: AppColors.btnDisableColor)), SizedBox(height: 8.h), Text('请求将在倒计时结束后自动取消'.tr, style: TextStyle(fontSize: 16.sp, color: AppColors.btnDisableColor)), SizedBox(height: 20.h), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( onTap: _reject, child: Container( width: 0.35.sw, height: 48.h, alignment: Alignment.center, decoration: BoxDecoration(color: Colors.redAccent, borderRadius: BorderRadius.circular(8.r)), child: Text('拒绝'.tr, style: TextStyle(color: Colors.white, fontSize: 22.sp)), ), ), SizedBox(width: 20.w), GestureDetector( onTap: _accept, child: Container( width: 0.35.sw, height: 48.h, alignment: Alignment.center, decoration: BoxDecoration(color: AppColors.mainColor, borderRadius: BorderRadius.circular(8.r)), child: Text('同意'.tr, style: TextStyle(color: Colors.white, fontSize: 22.sp)), ), ), ], ), SizedBox(height: 8.h), Container( padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 12.w), decoration: BoxDecoration(color: const Color(0xFFF7F9FC), borderRadius: BorderRadius.circular(8.r)), child: Row(children: [Icon(Icons.info_outline, color: AppColors.mainColor, size: 18.r), SizedBox(width: 6.w), Expanded(child: Text('远程开锁需确保现场安全与人员确认'.tr, style: TextStyle(color: AppColors.btnDisableColor, fontSize: 14.sp)))]), ), ], ), ), ), ], ), ); } Future _accept() async { timer?.cancel(); await ApiRepository.to.remoteOpenLock(lockId: widget.lockId.toString(), timeOut: 60); Get.back(); } void _reject() { timer?.cancel(); Get.back(); } } class _DottedCountdownRing extends StatelessWidget { const _DottedCountdownRing({required this.seconds, required this.size}); final int seconds; final double size; @override Widget build(BuildContext context) { final double progress = seconds.clamp(0, 60) / 60.0; return CustomPaint(size: Size(size, size), painter: _DottedCirclePainter(progress: progress)); } } class _DottedCirclePainter extends CustomPainter { _DottedCirclePainter({required this.progress}); final double progress; @override void paint(Canvas canvas, Size size) { final Paint paint = Paint() ..color = AppColors.mainColor ..strokeWidth = 4.w ..style = PaintingStyle.stroke; final double radius = size.width / 2 - 2.w; final Offset center = Offset(size.width / 2, size.height / 2); final Path path = Path(); final double angle = -pi / 2 + 2 * pi * progress.clamp(0.0, 1.0); for (int i = 0; i <= 60; i++) { final double startAngle = (2 * pi / 60) * i - pi / 2; final double endAngle = startAngle + (2 * pi / 60) * 0.7; if (startAngle <= angle) { final Path segmentPath = Path(); segmentPath.arcTo(Rect.fromCircle(center: center, radius: radius), startAngle, endAngle - startAngle, true); path.addPath(segmentPath, Offset.zero); } } canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; }