鸿蒙(Harmony)实现滑块验证码

打印 上一主题 下一主题

主题 997|帖子 997|积分 2991

在Android和ios两头已经使用的滑块验证码框架还未适配鸿蒙版,于是需要自己去实现类似如下的滑块验证码:

那么实现这样的验证码重要涉及到几个内容:
1、自界说弹窗

2、base64图片转换

3、滑动组件与滑块的联动,以及横移间隔转换等

自界说弹窗:

自界说一个可导出的弹窗组件CustomDialog,最重要是使用 @CustomDialog 修饰符。
  1. @CustomDialog
  2. export struct BlockPuzzleDialog {
  3. phoneNum: number | string = ''
  4. controller: CustomDialogController = new CustomDialogController({
  5.     builder: BlockPuzzleDialog({}),
  6.   })
  7.     build() {
  8.          Column(){
  9.         
  10.         }
  11.     }
  12.    
  13.    
  14.    // 验证码校验回调给使用页面
  15.   blockCheckCallback: (token: string) => void = (token: string) => {
  16.   }
  17. }
复制代码
在使用页面创建构造器与弹窗绑定
  1. @Entry
  2. @Component
  3. struct LoginPage {
  4.    dialogController: CustomDialogController = new CustomDialogController({
  5.     builder: BlockPuzzleDialog({
  6.       phoneNum: this.phoneNum, blockCheckCallback: (token: string) => {
  7.         this.blockPuzzleSuccessCallback(token)
  8.       }
  9.     }),
  10.     autoCancel: false,//弹窗是否自动取消
  11.     alignment: DialogAlignment.Center,// 弹窗位置
  12.     cornerRadius: 8,
  13.     width: '90%'// 弹窗宽度
  14.   })
  15.    
  16.     build(){
  17.       ...
  18.     }
  19. }
复制代码
弹窗UI组件的实现:核心组件就一个预先挖孔的底图上面叠加滑块图片再加上一个slider组件
  1. build(){
  2. ......
  3. Stack() {
  4.         Image(this.coverUri).width('100%').margin({ top: 10 }).objectFit(ImageFit.Auto).onComplete((event) => {
  5.           this.scaleRatio = event!!.componentWidth / event?.width!!
  6.         })
  7.         Image(this.blockUri)
  8.           .width(this.blockW + "px")
  9.           .height(this.blockH + "px")
  10.           .margin({ top: 10 })
  11.           .objectFit(ImageFit.Auto)
  12.           .onComplete((event) => {
  13.             this.blockW = event?.width!! * this.scaleRatio
  14.             this.blockH = event?.height!! * this.scaleRatio
  15.             this.slideMax = Const.mWidth * 0.9 - 24 - px2vp(this.blockW)
  16.           })
  17.           .translate({ x: this.bolckTranslateX + "px" })
  18.         this.loading()
  19.       }.width('100%').alignContent(Alignment.Start)
  20.       RelativeContainer() {
  21.         Text('向右拖动滑动填充拼图')
  22.           .fontSize(18)
  23.           .fontColor($r('app.color.C_BEBEC6'))
  24.           .id('blockTip')
  25.           .alignRules({
  26.             "top": {
  27.               "anchor": "slider",
  28.               "align": VerticalAlign.Top
  29.             },
  30.             "bottom": {
  31.               "anchor": "slider",
  32.               "align": VerticalAlign.Bottom
  33.             },
  34.             "left": {
  35.               "anchor": "slider",
  36.               "align": HorizontalAlign.Start
  37.             },
  38.             "right": {
  39.               "anchor": "slider",
  40.               "align": HorizontalAlign.End
  41.             },
  42.           })
  43.           .textAlign(TextAlign.Center)
  44.         Slider({
  45.           style: SliderStyle.InSet,
  46.           value: $$this.sliderValue,
  47.           step: 1,
  48.           max: vp2px(this.slideMax)
  49.         })
  50.           .trackColor(this.sliderConfig.trackColor)
  51.           .selectedColor(this.sliderConfig.selectedColor)
  52.           .blockSize({ height: 40, width: 44 })
  53.           .blockStyle({
  54.             type: SliderBlockType.IMAGE,
  55.             image: this.sliderConfig.blockImg
  56.           })// .sliderInteractionMode(SliderInteraction.SLIDE_ONLY)
  57.           .trackBorderRadius(Const.BORDER_RADIUS_4)
  58.           .trackThickness(40)
  59.           .width('100%')
  60.           .onChange((value: number, mode: SliderChangeMode) => {
  61.             // this.bolckTranslateX = this.slideMax * (value / this.slideMax)
  62.             this.bolckTranslateX = value
  63.             console.info('滑块滑动:滑块滑动数值==' + value + " 图片位移==" + this.bolckTranslateX)
  64.             if (mode == SliderChangeMode.End) {
  65.               // this.sliderValue = value
  66.               let point = new Point()
  67.               point.x = parseFloat((this.bolckTranslateX / this.scaleRatio).toFixed(0))
  68.               console.info('滑动结束:滑动数值 this.sliderValue==' + this.sliderValue + " this.bolckTranslateX==" +
  69.               this.bolckTranslateX + " 转像素==" + point.x)
  70.               this.checkCaptcha(point)
  71.             }
  72.           })
  73.           .id('slider')
  74.       }.width('100%').height(40).margin({ top: 10 })
  75. ......
  76. }
复制代码
滑块图片translate的值就是Slider组件的滑动值。使用
  1. this.dialogController.open() 弹窗
复制代码
Base64图片的下载与转换

  1. aboutToAppear(): void {
  2.     this.getSlideImage()
  3. }
  4. ......
  5. // 获取底图和滑块图片的base64数据并保存到本地,同时获取到滑块校验相关信息。
  6. getSlideImage() {
  7.     this.sliderConfig.showLoading = true
  8.     HttpUtil.getData<BlockResult>(Const.URL_BLOCK_IMG).then((result) => {
  9.       if (result !== undefined && result !== null) {
  10.         this.blockResult = result
  11.         this.coverBase64 = this.blockResult.repData?.originalImageBase64!!
  12.         this.blockBase64 = this.blockResult.repData?.jigsawImageBase64!!
  13.         console.info("滑块:获取到base64 ==" + this.coverBase64)
  14.         let coverName = "coverBase64_" + Date.now().toString() + ".png"
  15.         let blockName = "blockBase64_" + Date.now().toString() + ".png"
  16.         this.coverPath = this.context.filesDir + "/temp/" + coverName;
  17.         this.blockPath = this.context.filesDir + "/temp/" + blockName;
  18.         this.coverUri =
  19.           Utils.saveBase64Image(this.coverBase64, this.context, coverName)
  20.         this.blockUri =
  21.           Utils.saveBase64Image(this.blockBase64, this.context, blockName)
  22.         this.sliderConfig.showLoading = false
  23.         this.reset()
  24.       }
  25.     })
  26.   }
复制代码
可以参考官网示例 通过buffer.from的方法,将base64编码格式的字符串创建为新的Buffer对象,接着用fileIo.writeSync方法将转换好的Buffer对象写入文件。
  1. let context = getContext(this) as common.UIAbilityContext;
  2. let filesDir = context.filesDir;
  3. // data为需要转换的base64字符串,返回沙箱路径uri
  4. export async function writeFile(data: string): Promise<string> {
  5.   let uri = ''
  6.   try {
  7.     let filePath = filesDir + "/1.png";
  8.     uri = fileUri.getUriFromPath(filePath);
  9.     let file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
  10.     console.info("file fd: " + file.fd);
  11.     const reg = new RegExp("data:image/\\w+;base64,")
  12.     const base64 = data.replace(reg, "");
  13.     console.log("base64flag", base64)
  14.     const dataBuffer = buffer.from(base64, 'base64')
  15.     let writeLen = fileIo.writeSync(file.fd, dataBuffer.buffer);
  16.     hilog.info(0xA0c0d0,'uri',uri)
  17.     fileIo.closeSync(file);
  18.   }
  19.   catch (Error) {
  20.     hilog.error(0xA0c0d0,'Error',Error.code)
  21.   }
  22.   return uri;
  23. }
复制代码
当然你还可以直接将Base64转换成PiexlMap.先将base64字符串剖析成arraybuffer,然后使用这个arraybuffer构建新PixelMap,需要注意的是,使用decodeSync对base64字符串解码时,传入的base64字符串不能有'data:image/jpeg;base64,'这样的前缀。
  1. import CommonConstants from '../common/constants/CommonContants';
  2. import { util } from '@kit.ArkTS';
  3. import { image } from '@kit.ImageKit';
  4. @Entry
  5. @Component
  6. struct Index {
  7.   @State message: string = 'Base64ToPixelMap';
  8.   private base64: string = CommonConstants.Image_Base64_String; // 该变量为图片的base64格式字符串
  9.   @State private pixelMap: PixelMap | null = null;
  10.   build() {
  11.     Row() {
  12.       Column() {
  13.         Text(this.message)
  14.           .fontSize(50)
  15.           .fontWeight(FontWeight.Bold)
  16.           .onClick(async () => {
  17.             let helper = new util.Base64Helper();
  18.             let buffer: ArrayBuffer = helper.decodeSync(this.base64, util.Type.MIME).buffer as ArrayBuffer;
  19.             let imageSource = image.createImageSource(buffer);
  20.             let opts: image.DecodingOptions = { editable: true };
  21.             this.pixelMap = await imageSource.createPixelMap(opts);
  22.           })
  23.         Image(this.pixelMap)
  24.           .width(200).height(200).margin(15)
  25.       }
  26.       .width('100%')
  27.     }
  28.     .height('100%')
  29.   }
  30. }
复制代码
将得到的图片当地保存地点uri大概转换成的piexlMap设置给底图和滑动图片。
滑动值校验

上面已经说过,滑块的移动值就是Slider滑动值。其中slider 步长设置为1,滑动的最大值slideMax=底图的宽度-滑块图片的宽度。这样滑动值转换更方便,联动结果也更好。这里注意下 底图在填满控件的时间有肯定的缩放,滑动图片组件也需要按照这个缩放比例设置宽高。
  1. step: 1,
  2. max: vp2px(this.slideMax)
复制代码
最后在slider的onchange回调中校验滑动值是不是正确,注意滑动值要除以上面的底图缩放比例。
将滑动值加上校验token传给校验接口获取校验结果。
  1. .onChange((value: number, mode: SliderChangeMode) => {
  2.   
  3.   this.bolckTranslateX = value
  4.   console.info('滑块滑动:滑块滑动数值==' + value + " 图片位移==" + this.bolckTranslateX)
  5.   if (mode == SliderChangeMode.End) {
  6.     // this.sliderValue = value
  7.     let point = new Point()
  8.     point.x = parseFloat((this.bolckTranslateX / this.scaleRatio).toFixed(0))
  9.     console.info('滑动结束:滑动数值 this.sliderValue==' + this.sliderValue + " this.bolckTranslateX==" +
  10.     this.bolckTranslateX + " 转像素==" + point.x)
  11.     this.checkCaptcha(point)
  12.   }
  13. })
复制代码
  1. checkFail() {
  2.     this.sliderConfig.showLoading = false
  3.     this.sliderConfig.trackColor = $r('app.color.C_0DF32222')
  4.     this.sliderConfig.selectedColor = $r('app.color.C_F32222')
  5.     this.sliderConfig.blockImg = $r('app.media.drag_btn_error')
  6.     this.sliderValue = 0
  7.     this.bolckTranslateX = 0
  8.     setTimeout(() => {
  9.       // 删掉滑块图片
  10.       FileUtil.delFile(this.coverPath)
  11.       FileUtil.delFile(this.blockPath)
  12.       this.getSlideImage()
  13.     }, 300)
  14.   }
  15.   checkSuccess() {
  16.     this.sliderConfig.showLoading = false
  17.     this.sliderConfig.trackColor = $r('app.color.C_0D1264E0')
  18.     this.sliderConfig.selectedColor = $r('app.color.C_1264E0')
  19.     this.sliderConfig.blockImg = $r('app.media.drag_btn_success')
  20.     setTimeout(() => {
  21.       this.controller.close()
  22.       // 删掉滑块图片
  23.       FileUtil.delFile(this.coverPath)
  24.       FileUtil.delFile(this.blockPath)
  25.       if (this.blockCheckCallback !== undefined) {
  26.         this.blockCheckCallback(this.blockResult?.token!!)
  27.       }
  28.     }, 300)
  29.   }
复制代码
调用刚刚界说的回调方法将校验结果回调给登录页面this.blockCheckCallback(this.blockResult?.token!!)
至此导致流程已结束,当然还有一些细节需要自己根据业务实现。最后完成结果如下:


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

万有斥力

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表