diff --git a/lib/tools/remote_unlock_overlay.dart b/lib/tools/remote_unlock_overlay.dart new file mode 100644 index 00000000..d4ebce03 --- /dev/null +++ b/lib/tools/remote_unlock_overlay.dart @@ -0,0 +1,206 @@ +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; +}