diff --git a/lib/login/captcha/block_puzzle_captcha.dart b/lib/login/captcha/block_puzzle_captcha.dart new file mode 100644 index 00000000..78143f70 --- /dev/null +++ b/lib/login/captcha/block_puzzle_captcha.dart @@ -0,0 +1,551 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../retrofit/retrofit_api.dart'; +import '../../utils/captcha_util.dart'; +import '../../utils/widget_util.dart'; +import 'click_word_captcha.dart'; + +typedef VoidSuccessCallback = dynamic Function(String v); + +class BlockPuzzleCaptchaPage extends StatefulWidget { + final VoidSuccessCallback onSuccess; //拖放完成后验证成功回调 + final VoidCallback onFail; //拖放完成后验证失败回调 + + BlockPuzzleCaptchaPage({this.onSuccess, this.onFail}); + + @override + _BlockPuzzleCaptchaPageState createState() => _BlockPuzzleCaptchaPageState(); +} + +class _BlockPuzzleCaptchaPageState extends State + with TickerProviderStateMixin { +// String baseImageBase64 = +// "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"; + String baseImageBase64 = ""; + String slideImageBase64 = ""; + String captchaToken = ""; + String secretKey = ""; //加密key + + Size baseSize = Size.zero; //底部基类图片 + Size slideSize = Size.zero; //滑块图片 + + var sliderColor = Colors.white; //滑块的背景色 + var sliderIcon = Icons.arrow_forward; //滑块的图标 + var movedXBorderColor = Colors.white; //滑块拖动时,左边已滑的区域边框颜色 + double sliderStartX = 0; //滑块未拖前的X坐标 + double sliderXMoved = 0; + bool sliderMoveFinish = false; //滑块拖动结束 + bool checkResultAfterDrag = false; //拖动后的校验结果 + + //-------------动画------------ + int _checkMilliseconds = 0; //滑动时间 + bool _showTimeLine = false; //是否显示动画部件 + bool _checkSuccess = false; //校验是否成功 + AnimationController controller; + + //高度动画 + Animation offsetAnimation; + + //底部部件key + GlobalKey _containerKey = new GlobalKey(); + + //背景图key + GlobalKey _baseImageKey = new GlobalKey(); + + //滑块 + GlobalKey _slideImageKey = new GlobalKey(); + double _bottomSliderSize = 60.h; + + //------------动画------------ + + //校验通过 + void checkSuccess(String content) { + setState(() { + checkResultAfterDrag = true; + _checkSuccess = true; + _showTimeLine = true; + }); + _forwardAnimation(); + updateSliderColorIcon(); + + //刷新验证码 + Future.delayed(Duration(milliseconds: 1000)).then((v) { + _reverseAnimation().then((v) { + setState(() { + _showTimeLine = false; + }); + //回调 + if (widget.onSuccess != null) { + widget.onSuccess(content); + } + //关闭验证码 + print(content); + Navigator.pop(context); + }); + }); + } + + //校验失败 + void checkFail() { + setState(() { + _showTimeLine = true; + _checkSuccess = false; + checkResultAfterDrag = false; + }); + _forwardAnimation(); + updateSliderColorIcon(); + + //刷新验证码 + Future.delayed(Duration(milliseconds: 1000)).then((v) { + _reverseAnimation().then((v) { + setState(() { + _showTimeLine = false; + }); + loadCaptcha(); + //回调 + if (widget.onFail != null) { + widget.onFail(); + } + }); + }); + } + + //重设滑动颜色与图标 + void updateSliderColorIcon() { + var _sliderColor; //滑块的背景色 + var _sliderIcon; //滑块的图标 + var _movedXBorderColor; //滑块拖动时,左边已滑的区域边框颜色 + + //滑块的背景色 + if (sliderMoveFinish) { + //拖动结束 + _sliderColor = checkResultAfterDrag ? Colors.green : Colors.red; + _sliderIcon = checkResultAfterDrag ? Icons.check : Icons.close; + _movedXBorderColor = checkResultAfterDrag ? Colors.green : Colors.red; + } else { + //拖动未开始或正在拖动中 + _sliderColor = sliderXMoved > 0 ? Color(0xff447ab2) : Colors.white; + _sliderIcon = Icons.arrow_forward; + _movedXBorderColor = Color(0xff447ab2); + } + + sliderColor = _sliderColor; + sliderIcon = _sliderIcon; + movedXBorderColor = _movedXBorderColor; + setState(() {}); + } + + //加载验证码 + void loadCaptcha() async { + setState(() { + _showTimeLine = false; + sliderMoveFinish = false; + checkResultAfterDrag = false; + sliderColor = Colors.white; //滑块的背景色 + sliderIcon = Icons.arrow_forward; //滑块的图标 + movedXBorderColor = Colors.white; //滑块拖动时,左边已滑的区域边框颜色 + }); + ApiService apiIpService = ApiService(Dio(), context: context); + ClickWordCaptchaModel baseData = await apiIpService + .captchaGet({"captchaType": "blockPuzzle"}).catchError((onError) {}); + if (baseData == null) { + setState(() { + secretKey = ""; + }); + return; + } + + sliderXMoved = 0; + sliderStartX = 0; + captchaToken = ''; + checkResultAfterDrag = false; + + baseImageBase64 = baseData.imgStr; + secretKey = baseData.secretKey; + baseImageBase64 = baseImageBase64.replaceAll('\n', ''); + slideImageBase64 = baseData.jigsawImageBase64; + slideImageBase64 = slideImageBase64.replaceAll('\n', ''); + captchaToken = baseData.token; + + var baseR = await WidgetUtil.getImageWH( + image: Image.memory(Base64Decoder().convert(baseImageBase64))); + baseSize = baseR.size; + + var silderR = await WidgetUtil.getImageWH( + image: Image.memory(Base64Decoder().convert(slideImageBase64))); + slideSize = silderR.size; + + setState(() {}); + } + + //校验验证码 + void checkCaptcha(sliderXMoved, captchaToken, {BuildContext myContext}) { + setState(() { + sliderMoveFinish = true; + }); + //滑动结束,改变滑块的图标及颜色 +// updateSliderColorIcon(); + + //pointJson参数需要aes加密 + +// MediaQueryData mediaQuery = MediaQuery.of(myContext); + var pointMap = {"x": sliderXMoved, "y": 5}; + var pointStr = json.encode(pointMap); + var cryptedStr = pointStr; + + // secretKey 不为空 进行as加密 + if (!CaptchaUtil.isEmpty(secretKey)) { + cryptedStr = CaptchaUtil.aesEncode(key: secretKey, content: pointStr); + // var dcrypt = CaptchaUtil.aesDecode(key: secretKey, content: cryptedStr); + // json.decode(dcrypt); + } + + ApiService apiIpService = ApiService(Dio(), context: context); + apiIpService + .captchaCheck({ + "pointJson": cryptedStr, + "captchaType": "blockPuzzle", + "token": captchaToken + }) + .catchError((onError) {}) + .then((res) { + if (res) { + checkFail(); + return; + } + //如果不加密 将 token 和 坐标序列化 通过 --- 链接成字符串 + var captchaVerification = "$captchaToken---$pointStr"; + if (!CaptchaUtil.isEmpty(secretKey)) { + //如果加密 将 token 和 坐标序列化 通过 --- 链接成字符串 进行加密 加密密钥为 _clickWordCaptchaModel.secretKey + captchaVerification = CaptchaUtil.aesEncode( + key: secretKey, content: captchaVerification); + } + checkSuccess(captchaVerification); + }) + .catchError((error) { + loadCaptcha(); + print(error); + }); + } + + @override + void initState() { + super.initState(); + initAnimation(); + loadCaptcha(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + // 初始化动画 + void initAnimation() { + controller = + AnimationController(duration: Duration(milliseconds: 500), vsync: this); + + offsetAnimation = Tween(begin: 0.5, end: 0) + .animate(CurvedAnimation(parent: controller, curve: Curves.ease)) + ..addListener(() { + this.setState(() {}); + }); + } + + // 反向执行动画 + _reverseAnimation() async { + await controller.reverse(); + } + + // 正向执行动画 + _forwardAnimation() async { + await controller.forward(); + } + + @override + void didUpdateWidget(BlockPuzzleCaptchaPage oldWidget) { + // TODO: implement didUpdateWidget + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return MaxScaleTextWidget( + child: buildContent(context), + ); + } + + Widget buildContent(BuildContext context) { + var mediaQuery = MediaQuery.of(context); + var dialogWidth = 0.9 * mediaQuery.size.width; + if (dialogWidth < 330) { + dialogWidth = mediaQuery.size.width; + } + + return Scaffold( + backgroundColor: Colors.transparent, + body: Center( + child: Container( + key: _containerKey, + width: dialogWidth, + height: 340.h, + color: Colors.white, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _topContainer(), + _middleContainer(), + _bottomContainer(), + ], + ), + ), + ), + ); + } + + ///顶部,提示+关闭 + _topContainer() { + return Container( + height: 50.h, + padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0), + decoration: BoxDecoration( + border: Border(bottom: BorderSide(width: 1.w, color: Color(0xffe5e5e5))), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '请完成安全验证', + style: TextStyle(fontSize: 18), + ), + IconButton( + icon: Icon(Icons.highlight_off), + iconSize: 30, + color: Colors.black38, + onPressed: () { + //退出 + Navigator.pop(context); + }), + ], + ), + ); + } + + _middleContainer() { + ////显示验证码 + return Container( + margin: EdgeInsets.symmetric(vertical: 10.h), + child: Stack( + children: [ + ///底图 310*155 + baseImageBase64.length > 0 + ? Image.memory( + Base64Decoder().convert(baseImageBase64), + fit: BoxFit.fitWidth, + key: _baseImageKey, + gaplessPlayback: true, + ) + : Container( + width: 310.w, + height: 155.h, + alignment: Alignment.center, + child: CircularProgressIndicator(), + ), + + ///滑块图 + (baseImageBase64.length > 0 && slideImageBase64.length > 0) + ? Container( + margin: EdgeInsets.fromLTRB(sliderXMoved, 0, 0, 0), + child: Image.memory( + Base64Decoder().convert(slideImageBase64), + fit: BoxFit.fitHeight, + key: _slideImageKey, + gaplessPlayback: true, + ), + ) + : Container(), + + //刷新按钮 + Positioned( + top: 0, + right: 0, + child: IconButton( + icon: Icon(Icons.refresh), + iconSize: 30, + color: Colors.black54, + onPressed: () { + //刷新 + loadCaptcha(); + }), + ), + Positioned( + bottom: 0, + left: -10.w, + right: -10.w, + child: Offstage( + offstage: !_showTimeLine, + child: FractionalTranslation( + translation: Offset(0, offsetAnimation.value), + child: Container( + margin: EdgeInsets.only(left: 10.w, right: 10.w), + height: 40.h, + color: _checkSuccess + ? Color(0x7F66BB6A) + : Color.fromRGBO(200, 100, 100, 0.4), + alignment: Alignment.centerLeft, + child: Text( + _checkSuccess + ? "${(_checkMilliseconds / (60.0 * 12)).toStringAsFixed(2)}s验证成功" + : "验证失败", + style: TextStyle(color: Colors.white), + ), + ), + ), + )), + Positioned( + bottom: -20.h, + left: 0, + right: 0, + child: Offstage( + offstage: !_showTimeLine, + child: Container( + margin: EdgeInsets.only(left: 10.w, right: 10.w), + height: 20.h, + color: Colors.white, + ), + )) + ], + ), + ); + } + + ///底部,滑动区域 + _bottomContainer() { + return baseSize.width > 0 + ? Container( + height: 70.h, + width: baseSize.width, +// color: Colors.cyanAccent, + child: Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + Container( + height: _bottomSliderSize, + decoration: BoxDecoration( + border: Border.all( + width: 1.w, + color: Color(0xffe5e5e5), + ), + color: Color(0xfff8f9fb), + ), + ), + Container( + alignment: Alignment.center, + child: Text( + '向右拖动滑块填充拼图', + style: TextStyle(fontSize: 16.sp), + ), + ), + Container( + width: sliderXMoved, + height: _bottomSliderSize - 2.h, + decoration: BoxDecoration( + border: Border.all( + width: sliderXMoved > 0 ? 1 : 0, + color: movedXBorderColor, + ), + color: Color(0xfff3fef1), + ), + ), + GestureDetector( + onPanStart: (startDetails) { + ///开始 + _checkMilliseconds = + new DateTime.now().millisecondsSinceEpoch; + // print(startDetails.localPosition); + sliderStartX = startDetails.localPosition.dx; + }, + onPanUpdate: (updateDetails) { + ///更新 + // print(updateDetails.localPosition); + double _w1 = _baseImageKey.currentContext.size.width - + _slideImageKey.currentContext.size.width; + double offset = + updateDetails.localPosition.dx - sliderStartX; + if (offset < 0) { + offset = 0; + } + if (offset > _w1) { + offset = _w1; + } + // print("offset ------ $offset"); + setState(() { + sliderXMoved = offset; + }); + //滑动过程,改变滑块左边框颜色 + updateSliderColorIcon(); + }, + onPanEnd: (endDetails) { + //结束 + // print("endDetails"); + checkCaptcha(sliderXMoved, captchaToken); + int _nowTime = new DateTime.now().millisecondsSinceEpoch; + _checkMilliseconds = _nowTime - _checkMilliseconds; + }, + child: Container( + width: _bottomSliderSize, + height: _bottomSliderSize, + margin: EdgeInsets.only( + left: sliderXMoved > 0 ? sliderXMoved : 1), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + width: 1.w, + color: Color(0xffe5e5e5), + ), + right: BorderSide( + width: 1.w, + color: Color(0xffe5e5e5), + ), + bottom: BorderSide( + width: 1.w, + color: Color(0xffe5e5e5), + ), + ), + color: sliderColor, + ), + child: IconButton( + icon: Icon(sliderIcon), + iconSize: 30, + color: Colors.black54, + onPressed: () {}, + ), + ), + ) + ], + )) + : Container(); + } +} + +class MaxScaleTextWidget extends StatelessWidget { + final double max; + final Widget child; + + MaxScaleTextWidget({Key key, this.max = 1.0, this.child}) : super(key: key); + + @override + Widget build(BuildContext context) { + var data = MediaQuery.of(context); + var textScaleFactor = min(max, data.textScaleFactor); + return MediaQuery( + data: data.copyWith(textScaleFactor: textScaleFactor), child: child); + } +} diff --git a/lib/login/captcha/click_word_captcha.dart b/lib/login/captcha/click_word_captcha.dart new file mode 100644 index 00000000..1afba266 --- /dev/null +++ b/lib/login/captcha/click_word_captcha.dart @@ -0,0 +1,367 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../retrofit/retrofit_api.dart'; +import '../../utils/captcha_util.dart'; +import '../../utils/widget_util.dart'; + +typedef VoidSuccessCallback = dynamic Function(String v); + +class ClickWordCaptcha extends StatefulWidget { + final VoidSuccessCallback onSuccess; //文字点击后验证成功回调 + final VoidCallback onFail; //文字点击完成后验证失败回调 + + const ClickWordCaptcha({Key key, this.onSuccess, this.onFail}) + : super(key: key); + + @override + _ClickWordCaptchaState createState() => _ClickWordCaptchaState(); +} + +class _ClickWordCaptchaState extends State { + ClickWordCaptchaState _clickWordCaptchaState = ClickWordCaptchaState.none; + List _tapOffsetList = []; + ClickWordCaptchaModel _clickWordCaptchaModel = ClickWordCaptchaModel(); + + Color titleColor = Colors.black; + Color borderColor = Color(0xffdddddd); + String bottomTitle = ""; + Size baseSize = Size(310.0, 155.0); + + //改变底部样式及字段 + _changeResultState() { + switch (_clickWordCaptchaState) { + case ClickWordCaptchaState.normal: + titleColor = Colors.black; + borderColor = Color(0xffdddddd); + break; + case ClickWordCaptchaState.success: + _tapOffsetList = []; + titleColor = Colors.green; + borderColor = Colors.green; + bottomTitle = "验证成功"; + break; + case ClickWordCaptchaState.fail: + _tapOffsetList = []; + titleColor = Colors.red; + borderColor = Colors.red; + bottomTitle = "验证失败"; + break; + default: + titleColor = Colors.black; + borderColor = Color(0xffdddddd); + bottomTitle = "数据加载中……"; + break; + } + setState(() {}); + } + + @override + void initState() { + super.initState(); + _loadCaptcha(); + } + + //加载验证码 + _loadCaptcha() async { + _tapOffsetList = []; + _clickWordCaptchaState = ClickWordCaptchaState.none; + _changeResultState(); + ApiService apiIpService = ApiService(Dio(), context: context); + ClickWordCaptchaModel baseData = await apiIpService.captchaGet({"captchaType": "clickWord"}).catchError((onError) {}); + if (baseData == null) { + _clickWordCaptchaModel.secretKey = ""; + bottomTitle = "加载失败,请刷新"; + _clickWordCaptchaState = ClickWordCaptchaState.normal; + _changeResultState(); + return; + } + else { + _clickWordCaptchaModel = baseData; + var baseR = await WidgetUtil.getImageWH( + image: Image.memory( + Base64Decoder().convert(_clickWordCaptchaModel.imgStr))); + baseSize = baseR.size; + + bottomTitle = "请依次点击【${_clickWordCaptchaModel.wordStr}】"; + } + + _clickWordCaptchaState = ClickWordCaptchaState.normal; + _changeResultState(); + } + + //校验验证码 + _checkCaptcha() async { + List> mousePos = []; + _tapOffsetList.map((size) { + mousePos + .add({"x": size.dx.roundToDouble(), "y": size.dy.roundToDouble()}); + }).toList(); + var pointStr = json.encode(mousePos); + + var cryptedStr = pointStr; + + // secretKey 不为空 进行as加密 + if (!CaptchaUtil.isEmpty(_clickWordCaptchaModel.secretKey)) { + cryptedStr = CaptchaUtil.aesEncode( + key: _clickWordCaptchaModel.secretKey, content: pointStr); + // var dcrypt = CaptchaUtil.aesDecode( + // key: _clickWordCaptchaModel.secretKey, content: cryptedStr); + } + +// Map _map = json.decode(dcrypt); + ApiService apiIpService = ApiService(Dio(), context: context); + bool baseData = await apiIpService.captchaCheck({ + "pointJson": cryptedStr, + "captchaType": "clickWord", + "token": _clickWordCaptchaModel.token + }).catchError((onError) {}); + if (baseData) { + _checkFail(); + return; + } + //如果不加密 将 token 和 坐标序列化 通过 --- 链接成字符串 + var captchaVerification = "${_clickWordCaptchaModel.token}---$pointStr"; + if (!CaptchaUtil.isEmpty(_clickWordCaptchaModel.secretKey)) { + //如果加密 将 token 和 坐标序列化 通过 --- 链接成字符串 进行加密 加密密钥为 _clickWordCaptchaModel.secretKey + captchaVerification = CaptchaUtil.aesEncode( + key: _clickWordCaptchaModel.secretKey, + content: captchaVerification); + } + _checkSuccess(captchaVerification); + } + + //校验失败 + _checkFail() async { + _clickWordCaptchaState = ClickWordCaptchaState.fail; + _changeResultState(); + + await Future.delayed(Duration(milliseconds: 1000)); + _loadCaptcha(); + //回调 + if (widget.onFail != null) { + widget.onFail(); + } + } + + //校验成功 + _checkSuccess(String pointJson) async { + _clickWordCaptchaState = ClickWordCaptchaState.success; + _changeResultState(); + + await Future.delayed(Duration(milliseconds: 1000)); + + var cryptedStr = CaptchaUtil.aesEncode(key: 'BGxdEUOZkXka4HSj', content: pointJson); + + print(cryptedStr); + //回调 pointJson 是经过es加密之后的信息 + if (widget.onSuccess != null) { + widget.onSuccess(cryptedStr); + } + //关闭 + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + var data = MediaQuery.of(context); + var dialogWidth = 0.9 * data.size.width; + var isRatioCross = false; + if (dialogWidth < 320.0) { + dialogWidth = data.size.width; + isRatioCross = true; + } + return Scaffold( + backgroundColor: Colors.transparent, + body: Center( + child: Container( + width: dialogWidth, + height: 320.h, + color: Colors.white, + child: Column( + children: [ + _topConttainer(), + _captchaContainer(), + _bottomContainer() + ], + ), + ), + ), + ); + } + + //图片验证码 + _captchaContainer() { + List _widgetList = []; + if (!CaptchaUtil.isEmpty(_clickWordCaptchaModel.imgStr)) { + _widgetList.add(Image( + width: baseSize.width, + height: baseSize.height, + gaplessPlayback: true, + image: MemoryImage( + Base64Decoder().convert(_clickWordCaptchaModel.imgStr)))); + } + + double _widgetW = 20; + for (int i = 0; i < _tapOffsetList.length; i++) { + Offset offset = _tapOffsetList[i]; + _widgetList.add(Positioned( + left: offset.dx - _widgetW * 0.5, + top: offset.dy - _widgetW * 0.5, + child: Container( + alignment: Alignment.center, + width: _widgetW, + height: _widgetW, + decoration: BoxDecoration( + color: Color(0xCC43A047), + borderRadius: BorderRadius.all(Radius.circular(_widgetW))), + child: Text( + "${i + 1}", + style: TextStyle(color: Colors.white, fontSize: 15), + ), + ))); + } + _widgetList.add(//刷新按钮 + Positioned( + top: 0, + right: 0, + child: IconButton( + icon: Icon(Icons.refresh), + iconSize: 30, + color: Colors.deepOrangeAccent, + onPressed: () { + //刷新 + _loadCaptcha(); + }), + )); + + return GestureDetector( + onTapDown: (TapDownDetails details) { + debugPrint( + "onTapDown globalPosition全局坐标系位置: ${details.globalPosition} localPosition组件坐标系位置: ${details.localPosition} "); + if (!CaptchaUtil.isListEmpty(_clickWordCaptchaModel.wordList) && + _tapOffsetList.length < _clickWordCaptchaModel.wordList.length) { + _tapOffsetList.add( + Offset(details.localPosition.dx, details.localPosition.dy)); + } + setState(() {}); + if (!CaptchaUtil.isListEmpty(_clickWordCaptchaModel.wordList) && + _tapOffsetList.length == _clickWordCaptchaModel.wordList.length) { + _checkCaptcha(); + } + }, + child: Container( + width: baseSize.width, + height: baseSize.height, + child: Stack( + children: _widgetList, + ), + )); + } + + //底部提示部件 + _bottomContainer() { + return Container( + height: 50.h, + margin: EdgeInsets.only(top: 10), + alignment: Alignment.center, + width: baseSize.width, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(4)), + border: Border.all(color: borderColor)), + child: + Text(bottomTitle, style: TextStyle(fontSize: 18, color: titleColor)), + ); + } + + //顶部,提示+关闭 + _topConttainer() { + return Container( + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + margin: EdgeInsets.only(bottom: 20, top: 5), + decoration: BoxDecoration( + border: Border(bottom: BorderSide(width: 1, color: Color(0xffe5e5e5))), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '请完成安全验证', + style: TextStyle(fontSize: 18), + ), + IconButton( + icon: Icon(Icons.highlight_off), + iconSize: 35, + color: Colors.black54, + onPressed: () { + //退出 + Navigator.pop(context); + }), + ], + ), + ); + } +} + +//校验状态 +enum ClickWordCaptchaState { + normal, //默认 可自定义描述 + success, //成功 + fail, //失败 + none, //无状态 用于加载使用 +} + +//请求数据模型 +class ClickWordCaptchaModel { + String imgStr; //图表url 目前用base64 data + String jigsawImageBase64; //图表url 目前用base64 data + String token; // 获取的token 用于校验 + List wordList; //显示需要点选的字 + String wordStr; //显示需要点选的字转换为字符串 + String secretKey; //加密key + + ClickWordCaptchaModel( + {this.imgStr = "", + this.jigsawImageBase64 = "", + this.token = "", + this.secretKey = "", + this.wordList = const [], + this.wordStr = ""}); + + //解析数据转换模型 + static ClickWordCaptchaModel fromMap(Map map) { + ClickWordCaptchaModel captchaModel = ClickWordCaptchaModel(); + captchaModel.imgStr = map["originalImageBase64"] ?? ""; + captchaModel.jigsawImageBase64 = map["jigsawImageBase64"] ?? ""; + captchaModel.token = map["token"] ?? ""; + captchaModel.secretKey = map["secretKey"] ?? ""; + captchaModel.wordList = map["wordList"] ?? []; + + if (!CaptchaUtil.isListEmpty(captchaModel.wordList)) { + captchaModel.wordStr = captchaModel.wordList.join(","); + } + + return captchaModel; + } + + //将模型转换 + Map toJson() { + var map = new Map(); + map['imgStr'] = imgStr; + map['jigsawImageBase64'] = jigsawImageBase64; + map['token'] = token; + map['secretKey'] = token; + map['wordList'] = wordList; + map['wordStr'] = wordStr; + return map; + } + + @override + String toString() { + // TODO: implement toString + return JsonEncoder.withIndent(' ').convert(toJson()); + } +} diff --git a/lib/login/login_page.dart b/lib/login/login_page.dart index bc68553f..086cd6d8 100644 --- a/lib/login/login_page.dart +++ b/lib/login/login_page.dart @@ -75,7 +75,8 @@ class _MyLoginPageState extends State with TickerProviderStateMixin { sharedPreferences.getString("token") != null && sharedPreferences.getString("token") != "") { Navigator.of(context).popAndPushNamed('/router/main_page'); - } else {initController(); + } else { + initController(); client = ApiService(Dio(), context: context); isShowLogin = true; @@ -180,15 +181,16 @@ class _MyLoginPageState extends State with TickerProviderStateMixin { _controllerInviteCode.addListener(() { print(_controllerInviteCode.text); - if (_controllerInviteCode.text != null && _controllerInviteCode.text != "") { - if (_controllerInviteCode.text.length == 6 ){ + if (_controllerInviteCode.text != null && + _controllerInviteCode.text != "") { + if (_controllerInviteCode.text.length == 6) { statusInviteTextColor = Color(0xFF353535); - statusInviteLineColor = Color(0xFF32A060); - statusInviteVisible = false; + statusInviteLineColor = Color(0xFF32A060); + statusInviteVisible = false; } else { - statusInviteTextColor = Color(0xFFF72626); - statusInviteLineColor = Color(0xFFF72626); - statusInviteVisible = true; + statusInviteTextColor = Color(0xFFF72626); + statusInviteLineColor = Color(0xFFF72626); + statusInviteVisible = true; } } }); @@ -206,7 +208,6 @@ class _MyLoginPageState extends State with TickerProviderStateMixin { Color statusInviteTextColor = Color(0xFF353535); Color statusInviteLineColor = Color(0xFF32A060); - _sendCode() async { SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); if (!sharedPreferences.containsKey("isShowPrivacyPolicy") || @@ -228,7 +229,8 @@ class _MyLoginPageState extends State with TickerProviderStateMixin { } if (_sendCodeStatus == 0) { client - .sendVerify(areaCode,mobile) + .sendVerify( + {"areaCode": areaCode, "mobile": mobile, "verification": ""}) .then((value) => { if (value.isSuccess) {_sendCodeStatus = 1, countdown()} @@ -242,8 +244,8 @@ class _MyLoginPageState extends State with TickerProviderStateMixin { } }) .catchError((error) { - SmartDialog.showToast("$error", alignment: Alignment.center); - }); + SmartDialog.showToast("$error", alignment: Alignment.center); + }); } } @@ -324,28 +326,35 @@ class _MyLoginPageState extends State with TickerProviderStateMixin { var param = { "capcha": code, "mobile": mobile, - "invite":invite, + "invite": invite, }; - EasyLoading.show(status: S.of(context).zhengzaijiazai,maskType: EasyLoadingMaskType.black); - BaseData value = await client.memberLogin(param).catchError((error) { + EasyLoading.show( + status: S.of(context).zhengzaijiazai, + maskType: EasyLoadingMaskType.black); + BaseData value = + await client.memberLogin(param).catchError((error) { print(error.message); - SmartDialog.showToast(AppUtils.dioErrorTypeToString(error.type), alignment: Alignment.center); + SmartDialog.showToast(AppUtils.dioErrorTypeToString(error.type), + alignment: Alignment.center); }); // EasyLoading.show(status: S.of(context).zhengzaijiazai); - Future.delayed(Duration(seconds:2), () { - if (value !=null && value.isSuccess) { + Future.delayed(Duration(seconds: 2), () { + if (value != null && value.isSuccess) { saveUserJson(value.data.authInfo.toJson()); eventBus.fire(EventType(3)); Navigator.of(context).pushNamedAndRemoveUntil( - '/router/main_page', - (route) => false,arguments:{"invite":invite,"interviewCouponList":value.data.interviewCouponList, - "firstLoginCouponList":value.data.firstLoginCouponList}); + '/router/main_page', (route) => false, + arguments: { + "invite": invite, + "interviewCouponList": value.data.interviewCouponList, + "firstLoginCouponList": value.data.firstLoginCouponList + }); EasyLoading.dismiss(); - } else { - if(value?.msg !=null) - SmartDialog.showToast("${value?.msg ??""}", alignment: Alignment.center); + if (value?.msg != null) + SmartDialog.showToast("${value?.msg ?? ""}", + alignment: Alignment.center); } }); } @@ -515,7 +524,7 @@ class _MyLoginPageState extends State with TickerProviderStateMixin { height: MediaQuery.of(context).size.height * 0.78, margin: EdgeInsets.only(top: 56.h), alignment: Alignment.topCenter, - child:Image.asset( + child: Image.asset( "assets/image/icon_login_logo.webp", width: 91.w, height: 91.h, @@ -708,7 +717,7 @@ class _MyLoginPageState extends State with TickerProviderStateMixin { ), ), Container( - height:30.h, + height: 30.h, width: MediaQuery.of(context).size.width - 80.h, // margin: EdgeInsets.only(top: 12.h), child: TextField( @@ -754,7 +763,7 @@ class _MyLoginPageState extends State with TickerProviderStateMixin { color: statusPhoneLineColor, ), SizedBox( - height:30.h, + height: 30.h, child: Visibility( visible: statusPhoneVisible, child: Text( @@ -775,7 +784,7 @@ class _MyLoginPageState extends State with TickerProviderStateMixin { ), ), Container( - height:30.h, + height: 30.h, width: MediaQuery.of(context).size.width - 80.h, child: Row( mainAxisAlignment: MainAxisAlignment.end, @@ -878,7 +887,7 @@ class _MyLoginPageState extends State with TickerProviderStateMixin { ), ), SizedBox( - height:25.h, + height: 25.h, // child: Visibility( // visible: statusPhoneVisible, // child: Text( @@ -891,89 +900,95 @@ class _MyLoginPageState extends State with TickerProviderStateMixin { // ), ), GestureDetector( - onTap: (){ + onTap: () { setState(() { invitationCode = false; }); }, - child: - invitationCode ? - Container(child: - Column(children: [ - Text( - S.of(context).woyouyaoqingma, - style: TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - Container( - width: 56.w, - height: 0.5, - color: Colors.black, - ), - ],),):Container(child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.of(context).input_invite_code_hide, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - color: Colors.black, - ), + child: invitationCode + ? Container( + child: Column( + children: [ + Text( + S.of(context).woyouyaoqingma, + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + Container( + width: 56.w, + height: 0.5, + color: Colors.black, + ), + ], ), - Container( - height:25.h, - width: MediaQuery.of(context).size.width - 80.h, - child: TextField( - style: TextStyle( - height: 1.h, - fontSize: 16.sp, - color: statusInviteTextColor, + ) + : Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).input_invite_code_hide, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), ), - onChanged: (value){ - setState(() { - }); - }, - controller: _controllerInviteCode, - keyboardType: TextInputType.text, - decoration: InputDecoration( - errorBorder: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - hintText: "", - // contentPadding: EdgeInsets.only(top: 12, bottom: 12, left: 12), - hintStyle: TextStyle( - fontSize: 10.sp, - color: Color(0xFFA29E9E), + Container( + height: 25.h, + width: MediaQuery.of(context).size.width - 80.h, + child: TextField( + style: TextStyle( + height: 1.h, + fontSize: 16.sp, + color: statusInviteTextColor, + ), + onChanged: (value) { + setState(() {}); + }, + controller: _controllerInviteCode, + keyboardType: TextInputType.text, + decoration: InputDecoration( + errorBorder: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + hintText: "", + // contentPadding: EdgeInsets.only(top: 12, bottom: 12, left: 12), + hintStyle: TextStyle( + fontSize: 10.sp, + color: Color(0xFFA29E9E), + ), + ), + textInputAction: TextInputAction.next, + inputFormatters: [ + LengthLimitingTextInputFormatter(6) + ], + cursorColor: Colors.grey, + maxLines: 1, ), ), - textInputAction: TextInputAction.next, - inputFormatters: [LengthLimitingTextInputFormatter(6)], - cursorColor: Colors.grey, - maxLines: 1, - ), - ), - Container( - height: 1.h, - width: MediaQuery.of(context).size.width - 80.h, - color: statusInviteLineColor, - margin: EdgeInsets.only(bottom: 10.h), - ), - Visibility( - visible: statusInviteVisible, - child: Text( - S.of(context).invite_code_error, - style: TextStyle( - color: Color(0xFFF72626), - fontSize: 12.sp, + Container( + height: 1.h, + width: MediaQuery.of(context).size.width - 80.h, + color: statusInviteLineColor, + margin: EdgeInsets.only(bottom: 10.h), ), - ), + Visibility( + visible: statusInviteVisible, + child: Text( + S.of(context).invite_code_error, + style: TextStyle( + color: Color(0xFFF72626), + fontSize: 12.sp, + ), + ), + ), + ], ), - ], - ),), + ), ), Expanded( flex: 1, diff --git a/lib/login/new_login_page.dart b/lib/login/new_login_page.dart index 580b4890..47f2d32b 100644 --- a/lib/login/new_login_page.dart +++ b/lib/login/new_login_page.dart @@ -26,6 +26,7 @@ import 'package:sharesdk_plugin/sharesdk_interface.dart'; import '../main.dart'; import '../retrofit/data/channels_list.dart'; +import 'captcha/block_puzzle_captcha.dart'; class NewLoginPage extends StatefulWidget { final Map arguments; @@ -56,6 +57,7 @@ class _NewLoginPage extends State { String area = "+86"; String channelName; ChannelsList channelsList; + String mobile; @override void initState() { @@ -127,37 +129,22 @@ class _NewLoginPage extends State { showAlertDialog(); return; } - if (!checkStatus) { - SmartDialog.showToast(S.of(context).gouxuanxieyi, - alignment: Alignment.center); - return; - } - var mobile = _controllerPhone.text; + mobile = _controllerPhone.text; if (mobile == "") { mobileStatus = 2; mobileErrorText = S.of(context).qingshurushoujihao; + SmartDialog.showToast(S.of(context).qingshurushoujihao, + alignment: Alignment.center); setState(() {}); return; } + if (!checkStatus) { + SmartDialog.showToast(S.of(context).gouxuanxieyi, + alignment: Alignment.center); + return; + } if (_sendCodeStatus == 0) { - apiService - .sendVerify(area, mobile) - .then((value) => { - if (value.isSuccess) - {_sendCodeStatus = 1, countdown()} - else - { - btnText = S.of(context).send_code, - _sendCodeStatus = 0, - SmartDialog.showToast("${value.msg}", - alignment: Alignment.center), - refresh() - } - }) - .catchError((error) { - SmartDialog.showToast(AppUtils.dioErrorTypeToString(error.type), - alignment: Alignment.center); - }); + loadingBlockPuzzle(context); } } @@ -964,4 +951,41 @@ class _NewLoginPage extends State { r'^((13[0-9])|(14[0-9])|(15[0-9])|(16[0-9])|(17[0-9])|(18[0-9])|(19[0-9]))\d{8}$'); return exp.hasMatch(mobile); } + + //滑动拼图 + loadingBlockPuzzle(BuildContext context, + {barrierDismissible = true}) { + showDialog( + context: context, + barrierDismissible: barrierDismissible, + builder: (BuildContext context) { + return BlockPuzzleCaptchaPage( + onSuccess: (v) { + sendSms(v); + }, + onFail: () { + print("onFail"); + }, + ); + }, + ); + } + + sendSms(v) async{ + BaseData baseData = await apiService.sendVerify({"areaCode":area, "mobile": mobile, "verification": v}).catchError((onError) { + SmartDialog.showToast(AppUtils.dioErrorTypeToString(onError.type), + alignment: Alignment.center);}); + if (baseData != null && baseData.isSuccess) { + _sendCodeStatus = 1; + countdown(); + SmartDialog.showToast(baseData.data, + alignment: Alignment.center); + }else{ + btnText = S.of(context).send_code; + _sendCodeStatus = 0; + SmartDialog.showToast("${baseData.msg}", + alignment: Alignment.center); + refresh(); + } + } } diff --git a/lib/retrofit/min_api.dart b/lib/retrofit/min_api.dart index 8a795c75..c8cf737b 100644 --- a/lib/retrofit/min_api.dart +++ b/lib/retrofit/min_api.dart @@ -27,8 +27,8 @@ import 'data/shopping_home_config.dart'; part 'min_api.g.dart'; -// const localBaseUrl = "http://app-api.test.yixinhuixiang.com/app/";///本地 -const localBaseUrl = "http://pos-test.api.lotus-wallet.com/app/";///测试 +const localBaseUrl = "http://app-api.test.yixinhuixiang.com/app/";///本地 +// const localBaseUrl = "http://pos-test.api.lotus-wallet.com/app/";///测试 const serviceBaseUrl = "https://pos.api.lotus-wallet.com/app/";///线上 diff --git a/lib/retrofit/retrofit_api.dart b/lib/retrofit/retrofit_api.dart index 1ef0eab4..9401370d 100644 --- a/lib/retrofit/retrofit_api.dart +++ b/lib/retrofit/retrofit_api.dart @@ -18,6 +18,7 @@ import 'package:huixiang/retrofit/data/order_info.dart'; import 'package:huixiang/view_widget/login_tips_dialog.dart'; import 'package:retrofit/retrofit.dart'; +import '../login/captcha/click_word_captcha.dart'; import '../utils/flutter_utils.dart'; import 'data/achievement_detail_list.dart'; import 'data/activity_pos.dart'; @@ -65,8 +66,8 @@ import 'data/wx_pay.dart'; part 'retrofit_api.g.dart'; -// const localBaseUrl = "http://platform-api.test.yixinhuixiang.com/app/";///本地 -const localBaseUrl = "http://platform.test.api.lotus-wallet.com/app/";///测试 +const localBaseUrl = "http://platform-api.test.yixinhuixiang.com/app/";///本地 +// const localBaseUrl = "http://platform.test.api.lotus-wallet.com/app/";///测试 const serviceBaseUrl = "https://pos.platform.lotus-wallet.com/app/"; ///线上 @@ -127,8 +128,7 @@ abstract class ApiService { } debugPrint("code = ${response.statusCode}"); - if (response.request.path != "/creditGoods/list") - p(jsonEncode(response.data)); + p(jsonEncode(response.data)); // debugPrint(jsonEncode(response.data), wrapWidth: response.data.toString().length * 10); @@ -165,6 +165,8 @@ abstract class ApiService { static void p(String msg) { //因为String的length是字符数量不是字节数量所以为了防止中文字符过多, // 把4*1024的MAX字节打印长度改为1000字符数 + if(msg.length > 10000) + return; int maxStrLength = 900; //大于1000时 while (msg.length > maxStrLength) { @@ -196,9 +198,8 @@ abstract class ApiService { Future> appChannels(); ///发送验证码 - @GET("/auth/sendVerify/{areaCode}/{mobile}") - Future sendVerify( - @Path("areaCode") String areaCode, @Path("mobile") String mobile); + @POST("/auth/sendVerify") + Future> sendVerify(@Body() Map param); ///积分商城商品列表 @POST("/creditGoods/list") @@ -368,6 +369,16 @@ abstract class ApiService { Future>> msgList( @Body() Map param); + ///加载验证码 + @POST("/captcha/get") + Future captchaGet( + @Body() Map param); + + ///校验验证码 + @POST("/captcha/check") + Future captchaCheck( + @Body() Map param); + ///App消息详情 @GET("/app-msg/{id}") Future queryMsg(@Path("id") String id); diff --git a/lib/retrofit/retrofit_api.g.dart b/lib/retrofit/retrofit_api.g.dart index e5737679..386d5a17 100644 --- a/lib/retrofit/retrofit_api.g.dart +++ b/lib/retrofit/retrofit_api.g.dart @@ -117,24 +117,23 @@ class _ApiService implements ApiService { } @override - Future> sendVerify(areaCode, mobile) async { - ArgumentError.checkNotNull(mobile, 'mobile'); - ArgumentError.checkNotNull(areaCode, 'areaCode'); + Future> sendVerify(param) async { + ArgumentError.checkNotNull(param, 'param'); const _extra = {}; final queryParameters = {}; final _data = {}; - final _result = await _dio.request>( - '/auth/sendVerify/$areaCode/$mobile', + _data.addAll(param ?? {}); + final _result = await _dio.request>('/auth/sendVerify', queryParameters: queryParameters, options: RequestOptions( - method: 'GET', + method: 'POST', headers: {}, extra: _extra, baseUrl: baseUrl), data: _data); final value = BaseData.fromJson( _result.data, - (json) => json as dynamic, + (json) => json == null ? null :json as dynamic, ); return value; } @@ -1032,6 +1031,46 @@ class _ApiService implements ApiService { return value; } + @override + Future captchaGet(param) async { + ArgumentError.checkNotNull(param, 'param'); + const _extra = {}; + final queryParameters = {}; + final _data = {}; + _data.addAll(param ?? {}); + final _result = await _dio.request>('/captcha/get', + queryParameters: queryParameters, + options: RequestOptions( + method: 'POST', + headers: {}, + extra: _extra, + baseUrl: baseUrl), + data: _data); + final value = + (_result.data['repCode'] != '0000' || _result.data['repData'] == null) + ? null + : ClickWordCaptchaModel.fromMap(_result.data['repData']); + return value; + } + + @override + Future captchaCheck(param) async { + ArgumentError.checkNotNull(param, 'param'); + const _extra = {}; + final queryParameters = {}; + final _data = {}; + _data.addAll(param ?? {}); + final _result = await _dio.request>('/captcha/check', + queryParameters: queryParameters, + options: RequestOptions( + method: 'POST', + headers: {}, + extra: _extra, + baseUrl: baseUrl), + data: _data); + return (_result.data['repCode'] != '0000' || _result.data['repData'] == null || !_result.data['repData']['result']); + } + @override Future> queryMsg(id) async { ArgumentError.checkNotNull(id, 'id'); @@ -2119,12 +2158,15 @@ class _ApiService implements ApiService { final _data = {}; final _result = await _dio.request>('/ipJson.jsp', queryParameters: queryParameters, - options: RequestOptions(method: 'GET', extra: _extra, baseUrl: baseUrl,responseType: ResponseType.bytes), + options: RequestOptions( + method: 'GET', + extra: _extra, + baseUrl: baseUrl, + responseType: ResponseType.bytes), data: _data); var ts = gbk.decode(_result.data); - final value = IpData.fromJson(jsonDecode(ts - .substring(ts.indexOf("{\"ip\":\"")) - .replaceAll(");}", ""))); + final value = IpData.fromJson(jsonDecode( + ts.substring(ts.indexOf("{\"ip\":\"")).replaceAll(");}", ""))); return value; } diff --git a/lib/setting/binding_phone_page.dart b/lib/setting/binding_phone_page.dart index b9de973d..d9f21804 100644 --- a/lib/setting/binding_phone_page.dart +++ b/lib/setting/binding_phone_page.dart @@ -14,6 +14,7 @@ import 'package:huixiang/utils/font_weight.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../login/captcha/block_puzzle_captcha.dart'; import '../view_widget/border_text.dart'; class BindingPhonePage extends StatefulWidget { @@ -42,6 +43,8 @@ class _BindingPhonePage extends State { Timer _timer; UserInfo userInfo; int phoneState = 1; + String mobile; + String newMobile; @override void initState() { @@ -114,16 +117,6 @@ class _BindingPhonePage extends State { ///验证验证码 verificationCode() async { - var mobile = _controllerPhone.text; - if (mobile == "" && phoneState == 1) { - SmartDialog.showToast(S.of(context).qingshurushoujihao, alignment: Alignment.center); - return; - } - var newMobile = _controllerNewPhone.text; - if (newMobile == "" && phoneState == 2) { - SmartDialog.showToast(S.of(context).qingshurushoujihao, alignment: Alignment.center); - return; - } if (apiService == null) { SharedPreferences value = await SharedPreferences.getInstance(); apiService = ApiService(Dio(), @@ -466,7 +459,22 @@ class _BindingPhonePage extends State { alignment: Alignment.bottomCenter, child: InkWell( onTap: (){ - verificationCode(); + mobile = _controllerPhone.text; + if (mobile == "" && phoneState == 1) { + SmartDialog.showToast(S.of(context).qingshurushoujihao, alignment: Alignment.center); + return; + } + newMobile = _controllerNewPhone.text; + if (newMobile == "" && phoneState == 2) { + SmartDialog.showToast(S.of(context).qingshurushoujihao, alignment: Alignment.center); + return; + } + if(mobile != (userInfo?.phone??"")){ + SmartDialog.showToast("手机号码不正确", + alignment: Alignment.center); + return ; + } + loadingBlockPuzzle(context); }, child: BorderText( text: btnText, @@ -691,7 +699,17 @@ class _BindingPhonePage extends State { alignment: Alignment.bottomCenter, child: InkWell( onTap:(){ - verificationCode(); + mobile = _controllerPhone.text; + if (mobile == "" && phoneState == 1) { + SmartDialog.showToast(S.of(context).qingshurushoujihao, alignment: Alignment.center); + return; + } + newMobile = _controllerNewPhone.text; + if (newMobile == "" && phoneState == 2) { + SmartDialog.showToast(S.of(context).qingshurushoujihao, alignment: Alignment.center); + return; + } + loadingBlockPuzzle(context); }, child: BorderText( text: btnText, @@ -803,4 +821,23 @@ class _BindingPhonePage extends State { ), ); } + + //滑动拼图 + loadingBlockPuzzle(BuildContext context, + {barrierDismissible = true}) { + showDialog( + context: context, + barrierDismissible: barrierDismissible, + builder: (BuildContext context) { + return BlockPuzzleCaptchaPage( + onSuccess: (v) { + verificationCode(); + }, + onFail: () { + print("onFail"); + }, + ); + }, + ); + } } diff --git a/lib/setting/logout_ing.dart b/lib/setting/logout_ing.dart index f097aea4..0a9f2bdf 100644 --- a/lib/setting/logout_ing.dart +++ b/lib/setting/logout_ing.dart @@ -17,6 +17,7 @@ import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../login/captcha/block_puzzle_captcha.dart'; import '../main.dart'; class LogoutIng extends StatefulWidget { @@ -73,16 +74,20 @@ class _LogoutIng extends State { } if (_sendCodeStatus != 0) return; - if (apiService == null) { - SharedPreferences value = await SharedPreferences.getInstance(); - apiService = ApiService( - Dio(), - context: context, - token: value.getString("token"), - showLoading: true - ); + loadingBlockPuzzle(context); } - BaseData baseData = await apiService.sendVerify("+86",mobile).catchError((onError) {}); + + sendProving(v) async{ + if (apiService == null) { + SharedPreferences value = await SharedPreferences.getInstance(); + apiService = ApiService( + Dio(), + context: context, + token: value.getString("token"), + showLoading: true + ); + } + BaseData baseData = await apiService.sendVerify({"areaCode": "+86", "mobile": phoneController.text, "verification": v}).catchError((onError) {}); if (baseData != null && baseData.isSuccess) { countdown(); SmartDialog.showToast(baseData.data, @@ -497,4 +502,23 @@ class _LogoutIng extends State { // xgFlutterPlugin.stopXg(); // } } + + //滑动拼图 + loadingBlockPuzzle(BuildContext context, + {barrierDismissible = true}) { + showDialog( + context: context, + barrierDismissible: barrierDismissible, + builder: (BuildContext context) { + return BlockPuzzleCaptchaPage( + onSuccess: (v) { + sendProving(v); + }, + onFail: () { + print("onFail"); + }, + ); + }, + ); + } } diff --git a/lib/setting/platform_code_page.dart b/lib/setting/platform_code_page.dart index 713bf933..7a61e7a4 100644 --- a/lib/setting/platform_code_page.dart +++ b/lib/setting/platform_code_page.dart @@ -15,6 +15,8 @@ import 'package:huixiang/view_widget/pay_selected_dialog.dart'; import 'package:pin_input_text_field/pin_input_text_field.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../login/captcha/block_puzzle_captcha.dart'; + class PlatformCodePage extends StatefulWidget { final Map arguments; @@ -189,7 +191,7 @@ class _PlatformCodePage extends State { GestureDetector( onTap: () { setState(() { - sendCode(); + loadingBlockPuzzle(context); }); }, child: Container( @@ -308,4 +310,23 @@ class _PlatformCodePage extends State { ), ); } + + //滑动拼图 + loadingBlockPuzzle(BuildContext context, + {barrierDismissible = true}) { + showDialog( + context: context, + barrierDismissible: barrierDismissible, + builder: (BuildContext context) { + return BlockPuzzleCaptchaPage( + onSuccess: (v) { + sendCode(); + }, + onFail: () { + print("onFail"); + }, + ); + }, + ); + } } diff --git a/lib/utils/captcha_util.dart b/lib/utils/captcha_util.dart new file mode 100644 index 00000000..a84cd08b --- /dev/null +++ b/lib/utils/captcha_util.dart @@ -0,0 +1,86 @@ +import 'dart:convert'; +import 'package:steel_crypt/steel_crypt.dart'; +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; + +class CaptchaUtil{ + ///aes加密 + /// [key]AesCrypt加密key + /// [content] 需要加密的内容字符串 + static String aesEncode({String key, String content}) { + var aesCrypt = AesCrypt( + key: base64UrlEncode(key.codeUnits), padding: PaddingAES.pkcs7); + return aesCrypt.ecb.encrypt(inp: content); + } + + ///aes解密 + /// [key]aes解密key + /// [content] 需要加密的内容字符串 + static String aesDecode({String key, String content}) { + var aesCrypt = AesCrypt( + key: base64UrlEncode(key.codeUnits), padding: PaddingAES.pkcs7); + return aesCrypt.ecb.decrypt(enc: content); + } + /// isEmpty. + static bool isEmpty(Object value) { + if (value == null) return true; + if (value is String && value.isEmpty) { + return true; + } + return false; + } + + //list length == 0 || list == null + static bool isListEmpty(Object value) { + if (value == null) return true; + if (value is List && value.length == 0) { + return true; + } + return false; + } + + static String jsonFormat(Map map) { + Map _map = Map.from(map); + JsonEncoder encoder = JsonEncoder.withIndent(' '); + return encoder.convert(_map); + } + + + static String generateMd5(String data){ + var content = new Utf8Encoder().convert(data); + var digest = md5.convert(content); + return hex.encode(digest.bytes); + } + + static signData(Object params, tokenStr) async { + var time = new DateTime.now().millisecondsSinceEpoch; + String token = tokenStr; + Map reqData = new Map(); + Map paramsObj = new Map(); + paramsObj = params as Map; + var arr = []; + //将字典转成数组 + paramsObj?.forEach((key, value) => arr.add(key)); + //进行签名校验 + Map cr = new Map(); + cr['token'] = token; + cr['time'] = time.toString(); + cr['reqData'] = json.encode(paramsObj); + var array = []; + cr.forEach((key, value) => array.add(key)); + array.sort(); + var str = ''; + for (var i = 0; i < array.length; i++) { + var key = array[i]; + var value = cr[key]; + str += key + value; + } + + reqData["time"] = time; + reqData["token"] = token; + reqData['reqData'] = params; + reqData['sign'] = generateMd5(str); + + return reqData; + } +} \ No newline at end of file diff --git a/lib/utils/widget_util.dart b/lib/utils/widget_util.dart new file mode 100644 index 00000000..dabac5cb --- /dev/null +++ b/lib/utils/widget_util.dart @@ -0,0 +1,133 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; + +import 'captcha_util.dart'; +/** + * @Author: thl + * @GitHub: https://github.com/Sky24n + * @Email: 863764940@qq.com + * @Email: sky24no@gmail.com + * @Description: Widget Util. + * @Date: 2018/9/10 + */ + +/// Widget Util. +class WidgetUtil { + bool _hasMeasured = false; + double _width; + double _height; + + /// Widget rendering listener. + /// Widget渲染监听. + /// context: Widget context. + /// isOnce: true,Continuous monitoring false,Listen only once. + /// onCallBack: Widget Rect CallBack. + void asyncPrepare( + BuildContext context, bool isOnce, ValueChanged onCallBack) { + if (_hasMeasured) return; + WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) { + RenderObject box = context.findRenderObject(); + if (box != null) { + if (isOnce) _hasMeasured = true; + double width = box.semanticBounds.width; + double height = box.semanticBounds.height; + if (_width != width || _height != height) { + _width = width; + _height = height; + if (onCallBack != null) onCallBack(box.semanticBounds); + } + } + }); + } + + /// Widget渲染监听. + void asyncPrepares(bool isOnce, ValueChanged onCallBack) { + if (_hasMeasured) return; + WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) { + if (isOnce) _hasMeasured = true; + if (onCallBack != null) onCallBack(null); + }); + } + + ///get Widget Bounds (width, height, left, top, right, bottom and so on).Widgets must be rendered completely. + ///获取widget Rect + static Rect getWidgetBounds(BuildContext context) { + RenderObject box = context.findRenderObject(); + return (box != null) ? box.semanticBounds : Rect.zero; + } + + ///Get the coordinates of the widget on the screen.Widgets must be rendered completely. + ///获取widget在屏幕上的坐标,widget必须渲染完成 + static Offset getWidgetLocalToGlobal(BuildContext context) { + RenderBox box = context.findRenderObject() as RenderBox; + return box == null ? Offset.zero : box.localToGlobal(Offset.zero); + } + + /// get image width height,load error return Rect.zero.(unit px) + /// 获取图片宽高,加载错误情况返回 Rect.zero.(单位 px) + /// image + /// url network + /// local url , package + static Future getImageWH( + {Image image, String url, String localUrl, String package}) { + if (CaptchaUtil.isEmpty(image) && + CaptchaUtil.isEmpty(url) && + CaptchaUtil.isEmpty(localUrl)) { + return Future.value(Rect.zero); + } + Completer completer = Completer(); + Image img = image ?? ((url != null && url.isNotEmpty) + ? Image.network(url) + : Image.asset(localUrl, package: package)); + img.image + .resolve(const ImageConfiguration()) + .addListener(ImageStreamListener( + (ImageInfo info, bool _) { + completer.complete(Rect.fromLTWH(0, 0, info.image.width.toDouble(), + info.image.height.toDouble())); + }, + onError: (Object exception, StackTrace stackTrace) { + completer.completeError(exception, stackTrace); + }, + )); + return completer.future; + } + + /// get image width height, load error throw exception.(unit px) + /// 获取图片宽高,加载错误会抛出异常.(单位 px) + /// image + /// url network + /// local url (full path/全路径,example:"assets/images/ali_connors.png",""assets/images/3.0x/ali_connors.png"" ); + /// package + static Future getImageWHE( + {Image image, + String url, + String localUrl, + String package}) { + if (CaptchaUtil.isEmpty(image) && + CaptchaUtil.isEmpty(url) && + CaptchaUtil.isEmpty(localUrl)) { + return Future.error("image is null."); + } + Completer completer = Completer(); + Image img = image != null + ? image + : ((url != null && url.isNotEmpty) + ? Image.network(url) + : Image.asset(localUrl, package: package)); + img.image + .resolve(const ImageConfiguration()) + .addListener(ImageStreamListener( + (ImageInfo info, bool _) { + completer.complete(Rect.fromLTWH(0, 0, info.image.width.toDouble(), + info.image.height.toDouble())); + }, + onError: (Object exception, StackTrace stackTrace) { + completer.completeError(exception, stackTrace); + }, + )); + + return completer.future; + } +} diff --git a/pubspec.lock b/pubspec.lock index 3739cdce..21ba12e6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -15,6 +15,20 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" + args: + dependency: transitive + description: + name: args + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.1" + asn1lib: + dependency: transitive + description: + name: asn1lib + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" async: dependency: transitive description: @@ -106,6 +120,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" crypto: dependency: transitive description: @@ -141,6 +162,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" + encrypt: + dependency: "direct main" + description: + name: encrypt + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.1" equatable: dependency: transitive description: @@ -616,6 +644,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" + pointycastle: + dependency: transitive + description: + name: pointycastle + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.7.3" process: dependency: transitive description: @@ -775,6 +810,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" + steel_crypt: + dependency: "direct main" + description: + name: steel_crypt + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0+1" stream_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index aa4fec29..71b7b705 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,6 +62,9 @@ dependencies: visibility_detector: ^0.3.3 + steel_crypt: ^3.0.0+1 + encrypt: ^5.0.1 + event_bus: ^2.0.0 intl: ^0.17.0 shared_preferences: ^2.0.6