SingleTickerProviderStateMixin

使用SingleTickerProviderStateMixin管理异步动画控制器
本文档介绍如何在Flutter中使用SingleTickerProviderStateMixin来创建一个只在当前树启用时才会滴答的Ticker,并与AnimationController配合使用。这个混合类适用于只有一个AnimationController的情况。如果在应用生命周期内可能有多个AnimationController,建议使用完整的TickerProviderStateMixin。

Provides a single [Ticker] that is configured to only tick while the current tree is enabled, as defined by [TickerMode].

To create the [AnimationController] in a [State] that only uses a single [AnimationController], mix in this class, then pass vsync: this to the animation controller constructor.

This mixin only supports vending a single ticker. If you might have multiple [AnimationController] objects over the lifetime of the [State], use a full [TickerProviderStateMixin] instead.

如果在类的后面加上AnimationController之类的,在有状态类的后面要加上SingleTickerProviderStateMixin,以便于异步动画控制器

import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:oktoast/oktoast.dart'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:permission_handler/permission_handler.dart'; import '../api/trace_utils.dart'; /// 公共页面 - 扫码页面 class ComScanPage extends StatefulWidget { const ComScanPage({super.key}); @override State<StatefulWidget> createState() => _ComScanPageState(); } class _ComScanPageState extends State<ComScanPage> with SingleTickerProviderStateMixin { MobileScannerController controller = MobileScannerController( formats: [BarcodeFormat.all], ); bool _isProcessing = false; bool _hasPermission = false; bool _permissionChecked = false; //识别本地图片 final ImagePicker _imagePicker = ImagePicker(); // 新增动画控制器和扫描线位置动画 late AnimationController _scanLineController; late Animation<double> _scanLineAnimation; @override void initState() { super.initState(); controller = MobileScannerController( autoStart: false, // 不自动启动 detectionSpeed: DetectionSpeed.normal, facing: CameraFacing.back, torchEnabled: false, formats: [BarcodeFormat.all], ); // 初始化动画控制器(持续2秒,循环播放) _scanLineController = AnimationController( vsync: this, duration: const Duration(seconds: 2), )..repeat(reverse: false); // 重复动画 // 初始化扫描线位置动画(从0到1,映射到扫描框高度) _scanLineAnimation = CurvedAnimation( parent: _scanLineController, curve: Curves.linear, // 线性移动 ); printLog("当前页面-相机扫描页面:ComScanPage", 'blue'); _checkCameraPermission(); } // 检查相机权限 Future<void> _checkCameraPermission() async { final status = await Permission.camera.status; setState(() { _hasPermission = status.isGranted; _permissionChecked = true; }); if (_hasPermission) { WidgetsBinding.instance.addPostFrameCallback((_) { _startScanner(); }); } else { _requestCameraPermission(); } } // 启动扫码器 Future<void> _startScanner() async { try { if (!controller.value.isRunning) { await controller.start(); printLog("扫码器启动成功", 'green'); } } catch (e) { printLog("启动扫码器失败: $e", 'red'); Future.delayed(const Duration(milliseconds: 500), _startScanner); } } // 请求相机权限 Future<void> _requestCameraPermission() async { final status = await Permission.camera.request(); setState(() { _hasPermission = status.isGranted; }); if (_hasPermission) { WidgetsBinding.instance.addPostFrameCallback((_) { _startScanner(); }); } else { _showPermissionDialog(); } } // 处理扫描结果 Future<void> _handleBarcode(BarcodeCapture capture) async { printLog("=== 扫描回调触发 ===", 'green'); printLog("扫描到二维码结果: ${capture.barcodes.length} 个", 'green'); if (_isProcessing || !mounted) return; _isProcessing = true; if (capture.barcodes.isEmpty) { printLog("未识别到有效二维码", 'yellow'); _resumeScanning(); return; } final barcode = capture.barcodes.first; final String codeString = barcode.rawValue ?? ''; printLog("扫描到二维码: $codeString", 'green'); try { await controller.stop(); printLog("扫码器已暂停", 'blue'); _processBarcodeString(codeString); } catch (e) { printLog("处理二维码出错: $e", 'red'); showToast("处理二维码时出错"); _resumeScanning(); } finally { if (mounted) { Future.delayed(const Duration(seconds: 2), () { setState(() { _isProcessing = false; }); }); } } } // 恢复扫描 void _resumeScanning() { if (mounted) { Future.delayed(const Duration(seconds: 2), () { if (mounted) { setState(() { _isProcessing = false; }); if (!controller.value.isRunning) { _startScanner(); } } }); } } // 处理二维码字符串 void _processBarcodeString(String codeString) { if (codeString.contains('http://') || codeString.contains('https://')) { printLog("识别为网页链接", 'blue'); _navigateToWebView(codeString); } else if (codeString.contains('[jlm]')) { if (codeString.contains('[bmd]')) { final String bmdid = codeString.replaceAll('[jlm][bmd]', ''); printLog("识别为报名点二维码: $bmdid", 'blue'); _navigateToEnrollInfo(bmdid); } else { printLog("不支持的驾了么二维码类型", 'yellow'); showToast("暂不支持此类型的驾了么二维码"); _resumeScanning(); } } else { printLog("未识别二维码类型", 'yellow'); showToast("未识别信息"); _resumeScanning(); } } // 跳转到网页 void _navigateToWebView(String url) { printLog("跳转至-Web页面:webView?url=$url", 'purple'); GoRouter.of(context).pushNamed("webView", queryParameters: {'url': url}); } // 跳转到报名点详情 void _navigateToEnrollInfo(String bmdid) { printLog("跳转至-报名点详情页面:enrollInfo?id=$bmdid", 'purple'); GoRouter.of(context).pushNamed("enrollInfo", queryParameters: {'id': bmdid}); } // 新增:选择并识别本地图片二维码 Future<void> _pickImageAndScan() async { try { // 检查当前权限状态 PermissionStatus status; // 根据平台使用不同的权限 if (Platform.isAndroid) { // Android 10+ 使用 READ_MEDIA_IMAGES,旧版本使用 READ_EXTERNAL_STORAGE if (await DeviceInfoPlugin().androidInfo.then((info) => info.version.sdkInt >= 33)) { status = await Permission.photos.status; } else { status = await Permission.storage.status; } } else { // iOS 使用照片权限 status = await Permission.photos.status; } // 如果权限已被永久拒绝,引导用户去设置 if (status.isPermanentlyDenied) { _showPhotoPermissionDialog(); return; } // 如果权限被拒绝或未确定,请求权限 if (status.isDenied) { PermissionStatus newStatus; if (Platform.isAndroid && await DeviceInfoPlugin().androidInfo.then((info) => info.version.sdkInt >= 33)) { newStatus = await Permission.photos.request(); } else if (Platform.isAndroid) { newStatus = await Permission.storage.request(); } else { newStatus = await Permission.photos.request(); } if (!newStatus.isGranted) { showToast("需要相册权限来选择图片"); return; } } final XFile? image = await _imagePicker.pickImage( source: ImageSource.gallery, maxWidth: 1920, maxHeight: 1080, imageQuality: 80, ); if (image != null) { _scanImageQrCode(File(image.path)); } } catch (e) { printLog("选择图片失败: $e", 'red'); showToast("选择图片失败"); } } // 显示相册权限对话框 void _showPhotoPermissionDialog() { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: const Text("相册权限未开启"), content: const Text("您未开启驾了么的相册访问权限,无法选择图片进行二维码识别。"), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text("暂不"), ), TextButton( onPressed: () { openAppSettings().then((_) => Navigator.of(context).pop()); }, child: const Text("去设置"), ), ], ); }, ); } // 扫描图片中的二维码 Future<void> _scanImageQrCode(File imageFile) async { // 使用局部变量跟踪对话框状态 bool isDialogOpen = true; BuildContext? dialogContext; showDialog( context: context, barrierDismissible: false, builder: (context) { dialogContext = context; // 保存对话框上下文 return PopScope( canPop: false, // 禁用返回键关闭 child: const AlertDialog( content: Row( children: [ CircularProgressIndicator(), SizedBox(width: 20), Text('正在识别图片中的二维码...'), ], ), ), ); }, ); try { // 使用 MobileScanner 的图片识别功能 BarcodeCapture? result = await controller.analyzeImage(imageFile.path,formats:[BarcodeFormat.qrCode]); // 安全关闭对话框 if (isDialogOpen && dialogContext != null && Navigator.canPop(dialogContext!)) { Navigator.of(dialogContext!).pop(); isDialogOpen = false; } if (result != null && result.barcodes.isNotEmpty) { final barcode = result.barcodes.first; final String codeString = barcode.rawValue ?? ''; printLog("从图片中识别到二维码: $codeString", 'green'); _processBarcodeString(codeString); } else { if (mounted) showToast("未在图片中识别到二维码"); } } catch (e) { // 安全关闭对话框 if (isDialogOpen && dialogContext != null && Navigator.canPop(dialogContext!)) { Navigator.of(dialogContext!).pop(); isDialogOpen = false; } printLog("识别图片二维码失败: $e", 'red'); if (mounted) showToast("识别图片二维码失败"); // 备用方案:尝试使用 BarcodeDetector _tryAlternativeImageScan(imageFile); } } // 备用图片识别方案 Future<void> _tryAlternativeImageScan(File imageFile) async { bool isDialogOpen = true; BuildContext? dialogContext; try { showDialog( context: context, barrierDismissible: false, builder: (context) { dialogContext = context; return PopScope( canPop: false, // 禁用返回键关闭 child: const AlertDialog( content: Row( children: [ CircularProgressIndicator(), SizedBox(width: 20), Text('尝试备用识别方案...'), ], ), ), ); }, ); // // 使用 BarcodeScanner 进行识别 // final pickedFile = await ImagePicker().pickImage(source: ImageSource.gallery); // // 读取图片为字节 // final bytes = await File(imageFile.path).readAsBytes(); // 解析二维码 BarcodeCapture? result = await MobileScannerController(formats:[BarcodeFormat.qrCode],autoStart: false).analyzeImage(imageFile.path); // 安全关闭对话框 if (isDialogOpen && dialogContext != null && Navigator.canPop(dialogContext!)) { Navigator.of(dialogContext!).pop(); isDialogOpen = false; } if (result!.barcodes.isNotEmpty) { final barcode = result.barcodes.first; final String codeString = barcode.rawValue ?? ''; printLog("备用方案识别到二维码: $codeString", 'green'); _processBarcodeString(codeString); } else { showToast("无法识别图片中的二维码"); } } catch (e) { // 安全关闭对话框 if (isDialogOpen && dialogContext != null && Navigator.canPop(dialogContext!)) { Navigator.of(dialogContext!).pop(); isDialogOpen = false; } printLog("备用识别方案也失败: $e", 'red'); showToast("二维码识别失败,请尝试其他图片"); } } @override void dispose() { _scanLineController.dispose(); // 释放动画资源 controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( backgroundColor: Colors.black, elevation: 0, leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.of(context).pop(), ), title: const Text( '扫一扫', style: TextStyle(color: Colors.white), ), ), body: _buildBody(), ); } Widget _buildBody() { // 等待权限检查完成 if (!_permissionChecked) { return _buildLoadingView('检查相机权限...'); } // 权限被拒绝 if (!_hasPermission) { return _buildPermissionDeniedView(); } // 正常扫描界面 return Stack( fit: StackFit.expand, children: [ MobileScanner( controller: controller, fit: BoxFit.cover, onDetect: _handleBarcode, errorBuilder: (context, error) { printLog("MobileScanner错误: ${error.errorCode}", 'red'); return _buildErrorView(error); }, ), _buildScanOverlay(), _buildHintText(), _buildControlButtons(), // 添加调试信息(开发时使用,发布时移除) _buildDebugInfo(), ], ); } // 调试信息显示 Widget _buildDebugInfo() { return Positioned( top: 100, left: 10, child: Container( padding: const EdgeInsets.all(8), color: Colors.black54, child: ValueListenableBuilder<MobileScannerState>( valueListenable: controller, builder: (context, state, child) { return Text( '状态: ${state.isRunning ? "运行中" : "停止"}\n' '错误: ${state.error != null ? state.error?.errorCode : "无"}\n' '处理中: $_isProcessing', style: const TextStyle(color: Colors.white, fontSize: 12), ); }, ), ), ); } // 扫描框覆盖层 Widget _buildScanOverlay() { // 使用动画构建器包裹扫描框 return AnimatedBuilder( animation: _scanLineAnimation, builder: (context, child) { return CustomPaint( painter: _ScannerOverlay(animationValue: _scanLineAnimation.value), ); }, ); } // 提示文字 Widget _buildHintText() { return Positioned( top: (MediaQuery.of(context).size.height / 3) * 2 + 20, left: 0, right: 0, child: const Column( children: [ Text( '将二维码放入框内,即可自动扫描', style: TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w400, ), textAlign: TextAlign.center, ), SizedBox(height: 8), Text( '支持驾了么二维码和网页链接', style: TextStyle( color: Colors.white70, fontSize: 12, ), textAlign: TextAlign.center, ), ], ), ); } // 控制按钮 Widget _buildControlButtons() { return Positioned( bottom: 100, right: 20, child: Column( children: [ // 相册按钮 IconButton( icon: const Icon(Icons.photo_library, color: Colors.white, size: 32), onPressed: _pickImageAndScan, ), const SizedBox(height: 16), // 手电筒按钮 ValueListenableBuilder( valueListenable: controller, builder: (context, state, child) { return IconButton( icon: Icon( state.torchState == TorchState.on ? Icons.flash_on : Icons.flash_off, color: state.torchState == TorchState.on ? Colors.yellow : Colors.white, size: 32, ), onPressed: () => controller.toggleTorch(), ); }, ), const SizedBox(height: 16), // 切换摄像头按钮 ValueListenableBuilder( valueListenable: controller, builder: (context, state, child) { return IconButton( icon: const Icon(Icons.cameraswitch, color: Colors.white, size: 32), onPressed: () => controller.switchCamera(), ); }, ), ], ), ); } // 加载视图 Widget _buildLoadingView(String message) { return Container( color: Colors.black, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(color: Colors.white), const SizedBox(height: 16), Text( message, style: const TextStyle(color: Colors.white), ), ], ), ), ); } // 权限被拒绝视图 Widget _buildPermissionDeniedView() { return Container( color: Colors.black, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.camera_alt, size: 64, color: Colors.white54), const SizedBox(height: 20), const Text( '需要相机权限', style: TextStyle(color: Colors.white, fontSize: 18), ), const SizedBox(height: 10), const Padding( padding: EdgeInsets.symmetric(horizontal: 40), child: Text( '请允许应用访问相机以使用扫码功能', style: TextStyle(color: Colors.white70, fontSize: 14), textAlign: TextAlign.center, ), ), const SizedBox(height: 30), ElevatedButton( onPressed: _requestCameraPermission, style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, ), child: const Text('授权相机权限'), ), const SizedBox(height: 16), TextButton( onPressed: () => openAppSettings(), child: const Text( '去设置中开启', style: TextStyle(color: Colors.white70), ), ), ], ), ), ); } // 错误视图 Widget _buildErrorView(MobileScannerException error) { String errorMessage; switch (error.errorCode) { case MobileScannerErrorCode.permissionDenied: errorMessage = '相机权限被拒绝'; break; case MobileScannerErrorCode.genericError: errorMessage = '此设备不支持扫码功能'; break; default: errorMessage = '发生未知错误'; } return Container( color: Colors.black, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, color: Colors.white, size: 64), const SizedBox(height: 16), Text( errorMessage, style: const TextStyle(color: Colors.white, fontSize: 16), textAlign: TextAlign.center, ), const SizedBox(height: 24), ElevatedButton( onPressed: () { if (error.errorCode == MobileScannerErrorCode.permissionDenied) { _requestCameraPermission(); } else { controller.start(); } }, child: const Text('重试'), ), ], ), ), ); } void _showPermissionDialog() { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: const Text("相机权限未开启"), content: const Text("您未开启驾了么的相机服务,扫码功能需要相机权限才能正常使用。"), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text("暂不"), ), TextButton( onPressed: () { openAppSettings(); Navigator.of(context).pop(); }, child: const Text("去设置"), ), ], ); }, ); } } // 扫描框UI class _ScannerOverlay extends CustomPainter { final double animationValue; // 0.0 到 1.0 的动画值 _ScannerOverlay({this.animationValue = 0.0}); @override void paint(Canvas canvas, Size size) { final background = Paint()..color = Colors.black.withValues(alpha:0.6); final border = Paint() ..color = Colors.green ..style = PaintingStyle.stroke ..strokeWidth = 2.0; // 计算扫描框位置和大小 final width = size.width * 0.7; final height = width; final left = (size.width - width) / 2; final top = (size.height - height) / 3; final scanWindow = Rect.fromLTWH(left, top, width, height); // // 保存图层,确保 BlendMode.clear 只作用于当前半透明背景层 // canvas.saveLayer(Rect.fromLTWH(0, 0, size.width, size.height), Paint()); // // 绘制半透明背景(覆盖整个屏幕) // canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), background); // // 清除扫描窗口区域 // canvas.drawRect(scanWindow, Paint()..blendMode = BlendMode.clear); // // 恢复图层 // canvas.restore(); // 绘制上方的半透明区域 canvas.drawRect(Rect.fromLTRB(0, 0, size.width, top), background); // 绘制下方的半透明区域 canvas.drawRect(Rect.fromLTRB(0, top + height, size.width, size.height), background); // 绘制左侧的半透明区域 canvas.drawRect(Rect.fromLTRB(0, top, left, top + height), background); // 绘制右侧的半透明区域 canvas.drawRect(Rect.fromLTRB(left + width, top, size.width, top + height), background); // 绘制扫描窗口边框 canvas.drawRect(scanWindow, border); // 绘制四个角 final cornerLength = 20.0; final corner = Paint() ..color = Colors.green ..style = PaintingStyle.stroke ..strokeWidth = 4.0 ..strokeCap = StrokeCap.round; // 左上角 canvas.drawLine(scanWindow.topLeft, scanWindow.topLeft + Offset(cornerLength, 0), corner); canvas.drawLine(scanWindow.topLeft, scanWindow.topLeft + Offset(0, cornerLength), corner); // 右上角 canvas.drawLine(scanWindow.topRight, scanWindow.topRight - Offset(cornerLength, 0), corner); canvas.drawLine(scanWindow.topRight, scanWindow.topRight + Offset(0, cornerLength), corner); // 左下角 canvas.drawLine(scanWindow.bottomLeft, scanWindow.bottomLeft + Offset(cornerLength, 0), corner); canvas.drawLine(scanWindow.bottomLeft, scanWindow.bottomLeft - Offset(0, cornerLength), corner); // 右下角 canvas.drawLine(scanWindow.bottomRight, scanWindow.bottomRight - Offset(cornerLength, 0), corner); canvas.drawLine(scanWindow.bottomRight, scanWindow.bottomRight - Offset(0, cornerLength), corner); // 绘制微信风格的扫描线 _drawWeChatScanLine(canvas, scanWindow); // // 新增:绘制动态扫描线 // final scanLineY = top + (height * animationValue); // 计算扫描线Y坐标 // final scanLine = Rect.fromLTWH( // left + 10, // 左右缩进10,避免贴边框 // scanLineY - scanLineHeight / 2, // 居中对齐 // width - 20, // 宽度减去缩进 // scanLineHeight, // ); // final scanLinePaint = Paint() // ..color = scanLineColor.withValues(alpha:0.8) // ..style = PaintingStyle.fill; // // 扫描线主体 // canvas.drawRect(scanLine, scanLinePaint); // // // 新增:扫描线渐变光晕(增强视觉效果) // final gradient = LinearGradient( // colors: [ // scanLineColor.withValues(alpha:0), // scanLineColor.withValues(alpha:0.6), // scanLineColor.withValues(alpha:0), // ], // stops: const [0, 0.5, 1], // ); // final glowPaint = Paint() // ..shader = gradient.createShader(scanLine) // ..style = PaintingStyle.fill; // canvas.drawRect( // Rect.fromLTWH( // scanLine.left, // scanLine.top - 10, // 光晕高度比线条高 // scanLine.width, // scanLine.height + 20, // ), // glowPaint, // ); } // 增强版微信风格扫描线 void _drawWeChatScanLine(Canvas canvas, Rect scanWindow) { final lineY = scanWindow.top + scanWindow.height * animationValue; // 微信风格的扫描线效果 _drawScanLineGlow(canvas, scanWindow, lineY); _drawMainScanLine(canvas, scanWindow, lineY); //_drawScanLineHead(canvas, scanWindow, lineY); } // 微信风格的扫描线效果 void _drawScanLineGlow(Canvas canvas, Rect scanWindow, double lineY) { // 三层光晕效果 final gradients = [ LinearGradient( colors: [ Colors.green.withValues(alpha: 0), Colors.green.withValues(alpha: 0.1), Colors.green.withValues(alpha: 0), ], ), LinearGradient( colors: [ Colors.green.withValues(alpha: 0.1), Colors.green.withValues(alpha: 0.3), Colors.green.withValues(alpha: 0.1), ], ), LinearGradient( colors: [ Colors.green.withValues(alpha: 0.2), Colors.greenAccent.withValues(alpha: 0.5), Colors.green.withValues(alpha: 0.2), ], ), ]; final glowHeights = [16.0, 12.0, 8.0]; for (int i = 0; i < gradients.length; i++) { final glowRect = Rect.fromLTRB( scanWindow.left, lineY - glowHeights[i] / 2, scanWindow.right, lineY + glowHeights[i] / 2, ); final glowPaint = Paint() ..shader = gradients[i].createShader(glowRect) ..style = PaintingStyle.fill; canvas.drawRect(glowRect, glowPaint); } } // 主扫描线 void _drawMainScanLine(Canvas canvas, Rect scanWindow, double lineY) { // 主扫描线 - 微信风格的细亮线 final linePaint = Paint() ..color = Colors.greenAccent ..strokeWidth = 1 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; canvas.drawLine( Offset(scanWindow.left + 10, lineY), // 稍微缩进,避免触及边框 Offset(scanWindow.right - 10, lineY), linePaint, ); } // 扫描线头部 void _drawScanLineHead(Canvas canvas, Rect scanWindow, double lineY) { // 微信风格的扫描线头部 final headPaint = Paint() ..color = Colors.greenAccent ..style = PaintingStyle.fill; // 绘制三角形箭头 final path = Path(); final headSize = 8.0; path.moveTo(scanWindow.left + scanWindow.width / 2, lineY - headSize); path.lineTo(scanWindow.left + scanWindow.width / 2 - headSize / 2, lineY); path.lineTo(scanWindow.left + scanWindow.width / 2 + headSize / 2, lineY); path.close(); canvas.drawPath(path, headPaint); // 绘制光点效果 final dotPaint = Paint() ..color = Colors.greenAccent ..style = PaintingStyle.fill; // 在扫描线头部周围绘制光点 final positions = [ Offset(scanWindow.left + scanWindow.width / 4, lineY), Offset(scanWindow.left + scanWindow.width * 3/4, lineY), ]; for (final position in positions) { canvas.drawCircle(position, 1.5, dotPaint); } } @override bool shouldRepaint(covariant _ScannerOverlay oldDelegate) { // 扫描线位置变化时重绘 return oldDelegate.animationValue != animationValue; } } 这个页面我不用 mobile_scanner 插件 我需要更更换为 flutter_scankit: ^2.0.6
最新发布
09-25
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

computerclass

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值