当前位置:   article > 正文

华为(OpenHarmony)如何扫码自动配网_生成wifi配置qr码

生成wifi配置qr码

背景

随着移动互联网的发展,WiFi已成为人们生活中不可或缺的网络接入方式。但在连接WiFi时,用户常需要手动输入一个复杂的密钥,这带来了一定的不便。针对这一痛点,利用QR码连接WiFi的方案应运而生。QR码连接WiFi的工作流程是:商家或公共场所提供含有WiFi密钥的QR码,用户只需使用手机扫一扫即可读取密钥信息并连接WiFi,无需手动输入,这种连接方式大大简化了用户的操作。随着智能手机摄像头识别能力的提升,以及用户需求的引领,利用QR码连接WiFi的方式未来还将得到更广泛的应用,为用户提供更稳定便捷的上网体验。它利用了移动互联网时代的技术优势,解决了传统WiFi连接中的痛点,是一种值得推广的网络连接方式。

效果

页面截图

配网连接中配网连接成功配网连接失败

图片

图片

图片

优势

使用QR码连接WiFi具有以下优势:

  1. 提高了连接成功率,避免因手动输入密钥错误导致的连接失败问题。

  2. 加快了连接速度,扫码相对于手动输入更高效方便。

  3. 提升了用户体验,无需记忆和输入复杂密钥,操作更人性化。

  4. 方便密钥分享和更改,通过更新QR码即可实现。

  5. 在一些需要频繁连接不同WiFi的场景下尤其便利,如酒店、餐厅、机场等。

  6. 一些App可以自动识别WiFi二维码,实现零点击连接。

开发与实现

开发环境

开发平台:windows10、DevEco Studio 3.1 Release 

系统:OpenHarmony 3.2 Release,API9(Full SDK 3.2.11.9) 

设备:SD100(工业平板设备、平台:RK3568、屏幕像素:1920 * 1200)

项目开发

需求分析

1、支持相机扫码,并可以解析二维码信息;

2、获取二维码中的wifi连接信息,自动完成网络连接;

3、网络连接成功,则提示用户成功;

4、网络连接失败,则提示用户失败,可以重新连接;

5、UI界面符合OpenHarmony设计原则,应用界面简洁高效、自然流畅。

项目流程图

图片

界面

说明:从需求上分析,可以有两个界面,一是扫码界面、二是wifi连接等待和显示结果界面。

图片

详细开发

一、创建项目

说明:通过DevEco Studio创建一个OpenHarmony的项目。

图片

图片

二、申请权限

说明:在应用中涉及到使用相机和wifi的操作,需要动态申请一些必要的权限,我们可以在 EntryAbility.ts中实现,EntryAbility.ts继承UIAbility,用于管理应用的生面周期,在OnCreate是实例冷启动时触发,在此函数中实现权限申请。具体代码如下:

  1. let permissionList: Array<Permissions> = [
  2.   "ohos.permission.GET_WIFI_INFO",
  3.   "ohos.permission.INTERNET",
  4.   'ohos.permission.CAMERA',
  5.   'ohos.permission.READ_MEDIA',
  6.   'ohos.permission.WRITE_MEDIA',
  7.   'ohos.permission.MEDIA_LOCATION',
  8.   'ohos.permission.LOCATION',
  9.   'ohos.permission.APPROXIMATELY_LOCATION'
  10. ]
  11. onCreate(want, launchParam) {
  12.   hilog.info(0x0000'testTag''%{public}s''Ability onCreate');
  13.   this.requestPermissions()
  14. }
  15. private requestPermissions() {
  16.   let AtManager = abilityAccessCtrl.createAtManager()
  17.   AtManager.requestPermissionsFromUser(this.context, permissionList).then(async (data=> {
  18.     Logger.info(`${TAG} data permissions: ${JSON.stringify(data.permissions)}`)
  19.     Logger.info(`${TAG} data authResult: ${JSON.stringify(data.authResults)}`)
  20.     // 判断授权是否完成
  21.     let resultCount: number = 0
  22.     for (let result of data.authResults) {
  23.       if (result === 0) {
  24.         resultCount += 1
  25.       }
  26.     }
  27.     let permissionResult : boolean = false
  28.     if (resultCount === permissionList.length) {
  29.       permissionResult = true
  30.     }
  31.     AppStorage.SetOrCreate(KEY_IS_PERMISSION, true)
  32.     this.sendPermissionResult(permissionResult)
  33.   })
  34. }
  35. sendPermissionResult(result : boolean) {
  36.   let eventData: emitter.EventData = {
  37.     data: {
  38.       "result": result
  39.     }
  40.   };
  41.   let innerEvent: emitter.InnerEvent = {
  42.     eventId: EVENT_PERMISSION_ID,
  43.     priority: emitter.EventPriority.HIGH
  44.   };
  45.   emitter.emit(innerEvent, eventData);
  46.   Logger.info(`${TAG} sendPermissionResult`)
  47. }
  48. onDestroy() {
  49.   Logger.info(`${TAG} onDestroy`)
  50.   emitter.off(EVENT_PERMISSION_ID)
  51. }
 

代码解析

1、在应用中使用到相机和操作wifi需要根据需要动态申请相关权限,具体的权限用途可以查看:应用权限列表

2、应用动态授权需要使用到@ohos.abilityAccessCtrl (程序访问控制管理),通过abilityAccessCtrl.createAtManager()获取到访问控制对象 AtManager。

3、通过AtManager.requestPermissionsFromUser() 拉起请求用户授权弹窗,由用户动态授权。

4、授权成功后通过Emitter(@ohos.events.emitter)向主界面发送授权结果。

5、在onDestroy()应用退出函数中取消Emitter事件订阅。

三、首页

说明:首页即为扫码页面,用于识别二维码获取二维码信息,为网络连接准备。所以此页面有有个功能,加载相机和识别二维码。

媒体相机

相机的启动借鉴社区提供的代码案例:二维码扫码

  • 相机功能在CameraServices中,源码参考CameraServices.ets

  • 获取相机实例使用到媒体相机接口@ohos.multimedia.camera (相机管理)。

  • 首先使用camera.getCameraManager方法获取相机管理器,然后使用cameraManager.getSupportedCameras方法得到设备列表, 这里默认点亮列表中的首个相机;

  • 打开相机:使用 cameraManager.createCameraInput方法创建CameraInput实例,调用open方法打开相机;

  • 获取相机输出流:使用getSupportedOutputCapability查询相机设备在模式下支持的输出能力,然后使用createPreviewOutput创建相机输出流。

  • 获取拍照输出流,使用@ohos.multimedia.image接口的 createImageReceiver 方法创建ImageReceiver实例,并通过其getReceivingS_urfaceId()获取S_urfaceId,通过CameraManager.createPhotoOutput()函数构建拍照输出流,并将imageReceive 的 S_urfaceId与其建立绑定关系。

  • 获取相片输出:首先使用createCaptureSession方法创建捕获会话的实例,然后使用beginConfig方法配置会话,接下来使用addInput方法添加一个摄像头输入流,使用addOutput添加一个摄像头和相机照片的输出流,使用commitConfig方法提交会话配置后,调用会话的start方法开始捕获相片输出。

  • 这里也可以使用相机预览流获取图像数据,但在界面上需要预览,所以这里需要构建两条预览流,一条预览流用于显示,在XComponent组件中渲染,另外一条预览流用于获取头像数据用于解析,根据实践发现,开启两条预览流后,相机帧率为:7fsp,表现为预览卡顿,所以为提升预览效果,使用定时拍照的方式获取图像数据。

  • 获取图像的在SaveCameraAsset.ets中实现,扫码页面启动后每间隔1.5s调用PhotoOutput.capture()实现拍照,通过imageReceiver.on('imageArrival')接收图片,使用imageReceiver.readNextImage()获取图像对象,通过Image.getComponent()获取图像缓存数据。

具体实现代码:

CameraService
  1. import camera from '@ohos.multimedia.camera';
  2. import image from '@ohos.multimedia.image';
  3. import SaveCameraAsset from './SaveCameraAsset'
  4. import { QRCodeScanConst, SCAN_TYPE } from './QRCodeScanConst'
  5. import { Logger } from '@ohos/common'
  6. import common from '@ohos.app.ability.common'
  7. let TAG: string = 'CameraService'
  8. /**
  9.  * 拍照保存图片回调
  10.  */
  11. export interface FunctionCallBack {
  12.   onCaptureSuccess(thumbnail: image.PixelMap, resourceUri: string): void
  13.   onCaptureFailure(): void
  14.   onRecordSuccess(thumbnail: image.PixelMap): void
  15.   onRecordFailure(): void
  16.   /**
  17.    * 缩略图
  18.    */
  19.   thumbnail(thumbnail: image.PixelMap): void
  20.   /**
  21.    * AI 识别结果
  22.    * @param result 识别结果
  23.    */
  24.   aiResult(result: string): void
  25. }
  26. export interface PreviewCallBack {
  27.   onFrameStart()
  28.   onFrameEnd()
  29. }
  30. export interface MetaDataCallBack {
  31.   onRect(rect: camera.Rect)
  32. }
  33. export default class CameraService {
  34.   private static instance: CameraService = null
  35.   private mCameraManager: camera.CameraManager = null
  36.   private mCameraCount: number = 0 // 相机总数
  37.   private mCameraMap: Map<string, Array<camera.CameraDevice>> = new Map()
  38.   private mCurCameraDevice: camera.CameraDevice = null
  39.   private mCameraInput: camera.CameraInput = null
  40.   private mPreviewOutput: camera.PreviewOutput = null
  41.   private mPreviewOutputByImage: camera.PreviewOutput = null
  42.   private mPhotoOutput: camera.PhotoOutput = null
  43.   private mSaveCameraAsset: SaveCameraAsset = new SaveCameraAsset()
  44.   private mCaptureSession: camera.CaptureSession
  45.   private mMetadataOutput: camera.MetadataOutput
  46.   private constructor() {
  47.   }
  48.   /**
  49.    * 单例
  50.    */
  51.   public static getInstance(): CameraService {
  52.     if (this.instance === null) {
  53.       this.instance = new CameraService()
  54.     }
  55.     return this.instance
  56.   }
  57.   /**
  58.    * 初始化
  59.    */
  60.   public async initCamera(): Promise<number> {
  61.     Logger.info(`${TAG} initCamera`)
  62.     if (this.mCameraManager === null) {
  63.       this.mCameraManager = camera.getCameraManager(AppStorage.Get('context'))
  64.       // 注册监听相机状态变化
  65.       this.mCameraManager.on('cameraStatus', (cameraStatusInfo) => {
  66.         Logger.info(`${TAG} camera Status: ${JSON.stringify(cameraStatusInfo)}`)
  67.       })
  68.       // 获取相机列表
  69.       let cameras: Array<camera.CameraDevice> = this.mCameraManager.getSupportedCameras()
  70.       if (cameras) {
  71.         this.mCameraCount = cameras.length
  72.         Logger.info(`${TAG} mCameraCount: ${this.mCameraCount}`)
  73.         if (this.mCameraCount === 0) {
  74.           return this.mCameraCount
  75.         }
  76.         for (let i = 0; i < cameras.length; i++) {
  77.           Logger.info(`${TAG} --------------Camera Info-------------`)
  78.           const tempCameraId: string = cameras[i].cameraId
  79.           Logger.info(`${TAG} camera_id: ${tempCameraId}`)
  80.           Logger.info(`${TAG} cameraPosition: ${cameras[i].cameraPosition}`)
  81.           Logger.info(`${TAG} cameraType: ${cameras[i].cameraType}`)
  82.           const connectionType = cameras[i].connectionType
  83.           Logger.info(`${TAG} connectionType: ${connectionType}`)
  84.           // 判断本地相机还是远程相机
  85.           if (connectionType === camera.ConnectionType.CAMERA_CONNECTION_BUILT_IN) {
  86.             // 本地相机
  87.             this.displayCameraDevice(QRCodeScanConst.LOCAL_DEVICE_ID, cameras[i])
  88.           } else if (connectionType === camera.ConnectionType.CAMERA_CONNECTION_REMOTE) {
  89.             // 远程相机 相机ID格式 :deviceID__Camera_cameraID 例如:3c8e510a1d0807ea51c2e893029a30816ed940bf848754749f427724e846fab7__Camera_lcam001
  90.             const cameraKey: string = tempCameraId.split('__Camera_')[0]
  91.             Logger.info(`${TAG} cameraKey: ${cameraKey}`)
  92.             this.displayCameraDevice(cameraKey, cameras[i])
  93.           }
  94.         }
  95.         // todo test 选择首个相机
  96.         this.mCurCameraDevice = cameras[0]
  97.         Logger.info(`${TAG} mCurCameraDevice: ${this.mCurCameraDevice.cameraId}`)
  98.       }
  99.     }
  100.     return this.mCameraCount
  101.   }
  102.   /**
  103.    * 处理相机设备
  104.    * @param key
  105.    * @param cameraDevice
  106.    */
  107.   private displayCameraDevice(keystring, cameraDevice: camera.CameraDevice) {
  108.     Logger.info(`${TAG} displayCameraDevice ${key}`)
  109.     if (this.mCameraMap.has(key&& this.mCameraMap.get(key)?.length > 0) {
  110.       Logger.info(`${TAG} displayCameraDevice has mCameraMap`)
  111.       // 判断相机列表中是否已经存在此相机
  112.       let isExist: boolean = false
  113.       for (let item of this.mCameraMap.get(key)) {
  114.         if (item.cameraId === cameraDevice.cameraId) {
  115.           isExist = true
  116.           break
  117.         }
  118.       }
  119.       // 添加列表中没有的相机
  120.       if (!isExist) {
  121.         Logger.info(`${TAG} displayCameraDevice not exist , push ${cameraDevice.cameraId}`)
  122.         this.mCameraMap.get(key).push(cameraDevice)
  123.       } else {
  124.         Logger.info(`${TAG} displayCameraDevice has existed`)
  125.       }
  126.     } else {
  127.       let cameras: Array<camera.CameraDevice> = []
  128.       Logger.info(`${TAG} displayCameraDevice push ${cameraDevice.cameraId}`)
  129.       cameras.push(cameraDevice)
  130.       this.mCameraMap.set(key, cameras)
  131.     }
  132.   }
  133.  /**
  134.    * 创建相机输入流
  135.    * @param cameraIndex 相机下标
  136.    * @param deviceId 设备ID
  137.    */
  138.   public async createCameraInput(cameraIndex?: number, deviceId?: string) {
  139.     Logger.info(`${TAG} createCameraInput`)
  140.     if (this.mCameraManager === null) {
  141.       Logger.error(`${TAG} mCameraManager is null`)
  142.       return
  143.     }
  144.     if (this.mCameraCount <= 0) {
  145.       Logger.error(`${TAG} not camera device`)
  146.       return
  147.     }
  148.     if (this.mCameraInput) {
  149.       this.mCameraInput.close()
  150.     }
  151.     if (deviceId && this.mCameraMap.has(deviceId)) {
  152.       if (cameraIndex < this.mCameraMap.get(deviceId)?.length) {
  153.         this.mCurCameraDevice = this.mCameraMap.get(deviceId)[cameraIndex]
  154.       } else {
  155.         this.mCurCameraDevice = this.mCameraMap.get(deviceId)[0]
  156.       }
  157.     }
  158.     Logger.info(`${TAG} mCurCameraDevice: ${this.mCurCameraDevice?.cameraId}`)
  159.     try {
  160.       this.mCameraInput = this.mCameraManager.createCameraInput(this.mCurCameraDevice)
  161.       Logger.info(`${TAG} mCameraInput: ${JSON.stringify(this.mCameraInput)}`)
  162.       this.mCameraInput.on('error', this.mCurCameraDevice, (error=> {
  163.         Logger.error(`${TAG} CameraInput error: ${JSON.stringify(error)}`)
  164.       })
  165.       await this.mCameraInput.open()
  166.     } catch (err) {
  167.       if (err) {
  168.         Logger.error(`${TAG} failed to createCameraInput`)
  169.       }
  170.     }
  171.   }
  172.   /**
  173.    * 释放相机输入流
  174.    */
  175.   public async releaseCameraInput() {
  176.     Logger.info(`${TAG} releaseCameraInput`)
  177.     if (this.mCameraInput) {
  178.       try {
  179.         await this.mCameraInput.close()
  180.         Logger.info(`${TAG} releaseCameraInput closed`)
  181.       } catch (err) {
  182.         Logger.error(`${TAG} releaseCameraInput ${err}}`)
  183.       }
  184.       this.mCameraInput = null
  185.     }
  186.   }
  187.   /**
  188.    * 创建相机预览输出流
  189.    */
  190.   public async createPreviewOutput(s_urfaceId: string, callback?: PreviewCallBack) {
  191.     Logger.info(`${TAG} createPreviewOutput s_urfaceId ${s_urfaceId}`)
  192.     if (this.mCameraManager === null) {
  193.       Logger.error(`${TAG} createPreviewOutput mCameraManager is null`)
  194.       return
  195.     }
  196.     // 获取当前相机设备支持的输出能力
  197.     let cameraOutputCap = this.mCameraManager.getSupportedOutputCapability(this.mCurCameraDevice)
  198.     if (!cameraOutputCap) {
  199.       Logger.error(`${TAG} createPreviewOutput getSupportedOutputCapability error}`)
  200.       return
  201.  }
  202.     Logger.info(`${TAG} createPreviewOutput cameraOutputCap ${JSON.stringify(cameraOutputCap)}`)
  203.     let previewProfilesArray = cameraOutputCap.previewProfiles
  204.     let previewProfiles: camera.Profile
  205.     if (!previewProfilesArray || previewProfilesArray.length <= 0) {
  206.       Logger.error(`${TAG} createPreviewOutput previewProfilesArray error}`)
  207.       previewProfiles = {
  208.         format1,
  209.         size: {
  210.           width: QRCodeScanConst.DEFAULT_WIDTH,
  211.           height: QRCodeScanConst.DEFAULT_HEIGHT
  212.         }
  213.       }
  214.     } else {
  215.       Logger.info(`${TAG} createPreviewOutput previewProfile length ${previewProfilesArray.length}`)
  216.       previewProfiles = previewProfilesArray[0]
  217.     }
  218.     Logger.info(`${TAG} createPreviewOutput previewProfile[0] ${JSON.stringify(previewProfiles)}`)
  219.     try {
  220.       this.mPreviewOutput = this.mCameraManager.createPreviewOutput(previewProfiles, s_urfaceId)
  221.       Logger.info(`${TAG} createPreviewOutput success`)
  222.       // 监听预览帧开始
  223.       this.mPreviewOutput.on('frameStart', () => {
  224.         Logger.info(`${TAG} createPreviewOutput camera frame Start`)
  225.         if (callback) {
  226.           callback.onFrameStart()
  227.         }
  228.       })
  229.       this.mPreviewOutput.on('frameEnd', () => {
  230.         Logger.info(`${TAG} createPreviewOutput camera frame End`)
  231.         if (callback) {
  232.           callback.onFrameEnd()
  233.         }
  234.       })
  235.       this.mPreviewOutput.on('error', (error=> {
  236.         Logger.error(`${TAG} createPreviewOutput error: ${error}`)
  237.       })
  238.     } catch (err) {
  239.       Logger.error(`${TAG} failed to createPreviewOutput ${err}`)
  240.     }
  241.   }
  242.   /**
  243.    *  释放预览输出流
  244.    */
  245.   public async releasePreviewOutput() {
  246.     Logger.info(`${TAG} releaseCamera PreviewOutput`)
  247.     if (this.mPreviewOutput) {
  248.       await this.mPreviewOutput.release()
  249.       Logger.info(`${TAG} releaseCamera PreviewOutput release`)
  250.       this.mPreviewOutput = null
  251.     }
  252.   }
  253.   /**
  254.    * 创建拍照输出流
  255.    */
  256.   public async createPhotoOutput(functionCallback: FunctionCallBack) {
  257.     Logger.info(`${TAG} createPhotoOutput`)
  258.     if (!this.mCameraManager) {
  259.       Logger.error(`${TAG} createPhotoOutput mCameraManager is null`)
  260.       return
  261.     }
  262.     // 通过宽、高、图片格式、容量创建ImageReceiver实例
  263.     const receiver: image.ImageReceiver = image.createImageReceiver(QRCodeScanConst.DEFAULT_WIDTH, QRCodeScanConst.DEFAULT_HEIGHT, image.ImageFormat.JPEG, 8)
  264.     const imageS_urfaceId: string = await receiver.getReceivingS_urfaceId()
  265.     Logger.info(`${TAG} createPhotoOutput imageS_urfaceId: ${imageS_urfaceId}`)
  266.     let cameraOutputCap = this.mCameraManager.getSupportedOutputCapability(this.mCurCameraDevice)
  267.     Logger.info(`${TAG} createPhotoOutput cameraOutputCap ${cameraOutputCap}`)
  268.     if (!cameraOutputCap) {
  269.       Logger.error(`${TAG} createPhotoOutput getSupportedOutputCapability error}`)
  270.       return
  271.     }
  272.  let photoProfilesArray = cameraOutputCap.photoProfiles
  273.     let photoProfiles: camera.Profile
  274.     if (!photoProfilesArray || photoProfilesArray.length <= 0) {
  275.       // 使用自定义的配置
  276.       photoProfiles = {
  277.         format: camera.CameraFormat.CAMERA_FORMAT_JPEG,
  278.         size: {
  279.           width: QRCodeScanConst.DEFAULT_WIDTH,
  280.           height: QRCodeScanConst.DEFAULT_HEIGHT
  281.         }
  282.       }
  283.     } else {
  284.       Logger.info(`${TAG} createPhotoOutput photoProfile length ${photoProfilesArray.length}`)
  285.       photoProfiles = photoProfilesArray[0]
  286.     }
  287.     Logger.info(`${TAG} createPhotoOutput photoProfile ${JSON.stringify(photoProfiles)}`)
  288.     try {
  289.       this.mPhotoOutput = this.mCameraManager.createPhotoOutput(photoProfiles, imageS_urfaceId)
  290.       Logger.info(`${TAG} createPhotoOutput mPhotoOutput success`)
  291.       // 保存图片
  292.       this.mSaveCameraAsset.saveImage(receiver, functionCallback)
  293.     } catch (err) {
  294.       Logger.error(`${TAG} createPhotoOutput failed to createPhotoOutput ${err}`)
  295.     }
  296.   }
  297.   /**
  298.    * 释放拍照输出流
  299.    */
  300.   public async releasePhotoOutput() {
  301.     Logger.info(`${TAG} releaseCamera PhotoOutput`)
  302.     if (this.mPhotoOutput) {
  303.       await this.mPhotoOutput.release()
  304.       Logger.info(`${TAG} releaseCamera PhotoOutput release`)
  305.       this.mPhotoOutput = null
  306.     }
  307.   }
  308.   public async createSession() {
  309.     Logger.info(`${TAG} createSession`)
  310.     this.mCaptureSession = await this.mCameraManager.createCaptureSession()
  311.     Logger.info(`${TAG} createSession mCaptureSession ${this.mCaptureSession}`)
  312.     this.mCaptureSession.on('error', (error=> {
  313.       Logger.error(`${TAG} CaptureSession error ${JSON.stringify(error)}`)
  314.     })
  315.     try {
  316.       this.mCaptureSession?.beginConfig()
  317.       this.mCaptureSession?.addInput(this.mCameraInput)
  318.       if (this.mPreviewOutputByImage != null) {
  319.         Logger.info(`${TAG} createSession addOutput PreviewOutputByImage`)
  320.         this.mCaptureSession?.addOutput(this.mPreviewOutputByImage)
  321.       }
  322.       if (this.mPreviewOutput != null) {
  323.         Logger.info(`${TAG} createSession addOutput PreviewOutput`)
  324.         this.mCaptureSession?.addOutput(this.mPreviewOutput)
  325.       }
  326.       if (this.mPhotoOutput != null) {
  327.         Logger.info(`${TAG} createSession addOutput PhotoOutput`)
  328.         this.mCaptureSession?.addOutput(this.mPhotoOutput)
  329.       }
  330.       if (this.mMetadataOutput != null) {
  331.         Logger.info(`${TAG} createSession addOutput mMetadataOutput`)
  332.   this.mCaptureSession?.addOutput(this.mMetadataOutput)
  333.       }
  334.     } catch (err) {
  335.       if (err) {
  336.         Logger.error(`${TAG} createSession beginConfig fail err:${JSON.stringify(err)}`)
  337.       }
  338.     }
  339.     try {
  340.       await this.mCaptureSession?.commitConfig()
  341.     } catch (err) {
  342.       if (err) {
  343.         Logger.error(`${TAG} createSession commitConfig fail err:${JSON.stringify(err)}`)
  344.       }
  345.     }
  346.     try {
  347.       await this.mCaptureSession?.start()
  348.     } catch (err) {
  349.       if (err) {
  350.         Logger.error(`${TAG} createSession start fail err:${JSON.stringify(err)}`)
  351.       }
  352.     }
  353.     if (this.mMetadataOutput) {
  354.       this.mMetadataOutput.start().then(() => {
  355.         Logger.info(`${TAG} Callback returned with metadataOutput started`)
  356.       }).catch((err) => {
  357.         Logger.error(`${TAG} Failed to metadataOutput start ${err.code}`)
  358.       })
  359.     }
  360.     Logger.info(`${TAG} createSession mCaptureSession start`)
  361.   }
  362.   public async releaseSession() {
  363.     Logger.info(`${TAG} releaseCamera Session`)
  364.     if (this.mCaptureSession) {
  365.       await this.mCaptureSession.release()
  366.       Logger.info(`${TAG} releaseCamera Session release`)
  367.       this.mCaptureSession = null
  368.     }
  369.   }
  370.   /**
  371.    * 拍照
  372.    */
  373.   public async takePicture() {
  374.     Logger.info(`${TAG} takePicture`)
  375.     if (!this.mCaptureSession) {
  376.       Logger.info(`${TAG} takePicture session is release`)
  377.       return
  378.     }
  379.     if (!this.mPhotoOutput) {
  380.       Logger.info(`${TAG} takePicture mPhotoOutput is null`)
  381.       return
  382.     }
  383.     try {
  384.       const photoCaptureSetting: camera.PhotoCaptureSetting = {
  385.         quality: camera.QualityLevel.QUALITY_LEVEL_HIGH,
  386.         rotation: camera.ImageRotation.ROTATION_0,
  387.         location: {
  388.           latitude: 0,
  389.           longitude: 0,
  390.           altitude: 0
  391.         },
  392.         mirror: false
  393.       }
  394.       await this.mPhotoOutput.capture(photoCaptureSetting)
  395.     } catch (err) {
  396.       Logger.error(`${TAG} takePicture err:${JSON.stringify(err)}`)
  397.     }
  398.   }
  399.   /**
  400. * 获取设备的相机列表
  401.    * @param deviceId 设备ID
  402.    */
  403.   public getDeviceCameras(deviceId: string): Array<camera.CameraDevice> {
  404.     Logger.info(`${TAG} getDeviceCameras ${deviceId} size ${this.mCameraMap.size}`)
  405.     return this.mCameraMap.get(deviceId)
  406.   }
  407.   public getCameraCount(): number {
  408.     return this.mCameraCount
  409.   }
  410.   /**
  411.    * 释放相机
  412.    */
  413.   public async releaseCamera(): Promise<boolean> {
  414.     Logger.info(`${TAG} releaseCamera`)
  415.     let result: boolean = false
  416.     let tempStartTime: number = new Date().getTime()
  417.     try {
  418.       await this.releaseCameraInput()
  419.       await this.releasePhotoOutput()
  420.       await this.releasePreviewOutput()
  421.       await this.releaseSession()
  422.       result = true
  423.     } catch (err) {
  424.       Logger.error(`${TAG} releaseCamera fail ${JSON.stringify(err)}`)
  425.     }
  426.     let tempTime: number = new Date().getTime() - tempStartTime
  427.     Logger.info(`${TAG} releaseCamera finish time: ${tempTime}`)
  428.     return result
  429.   }
  430.   public async selectPic() {
  431.     Logger.info("getSingleImageFromAlbum start")
  432.     let context = AppStorage.Get('context'as common.UIAbilityContext
  433.     let abilityResult = await context.startAbilityForResult({
  434.       bundleName: 'com.ohos.photos',
  435.       abilityName: 'com.ohos.photos.MainAbility',
  436.       parameters: {
  437.         uri: 'singleselect' // 只选取单个文件
  438.       }
  439.     })
  440.     if (abilityResult.want === null || abilityResult.want === undefined) {
  441.       Logger.info("getSingleImageFromAlbum end. abilityResult.want is null.")
  442.       return null
  443.     }
  444.     if (abilityResult.want.parameters === null || abilityResult.want.parameters === undefined) {
  445.       Logger.info("getSingleImageFromAlbum end. abilityResult.want.parameters is null.")
  446.       return null
  447.     }
  448.     let images = abilityResult.want.parameters['select-item-list']
  449.     let imageUri = images[0]
  450.     Logger.info("getSingleImageFromAlbum end. uri:" + imageUri)
  451.     return imageUri
  452.   }
  453. }
SaveCameraAsset
  1. import image from '@ohos.multimedia.image'
  2. import { FunctionCallBack } from './CameraService'
  3. import { Logger } from '@ohos/common'
  4. import CodeRuleUtil from '../utils/CodeRuleUtil'
  5. const TAG: string = 'SaveCameraAsset'
  6. /**
  7.  * 保存相机拍照的资源
  8.  */
  9. export default class SaveCameraAsset {
  10.   constructor() {
  11.   }
  12.   /**
  13.    *  保存拍照图片
  14.    * @param imageReceiver 图像接收对象
  15.    * @param thumbWidth 宽度
  16.    * @param thumbHeight 高度
  17.    * @param callback 回调
  18.    */
  19.   public saveImage(imageReceiver: image.ImageReceiver, callback: FunctionCallBack) {
  20.     console.info(`${TAG} saveImage`)
  21.     let buffer = new ArrayBuffer(4096)
  22.     const imgWidth: number = imageReceiver.size.width
  23.     const imgHeight: number = imageReceiver.size.height
  24.     Logger.info(`${TAG} saveImage size ${JSON.stringify(imageReceiver.size)}`)
  25.     // 接收图片回调
  26.     imageReceiver.on('imageArrival', async () => {
  27.       console.info(`${TAG} saveImage ImageArrival`)
  28.       // 使用当前时间命名
  29.       imageReceiver.readNextImage((err, imageObj: image.Image) => {
  30.         if (imageObj === undefined) {
  31.           Logger.error(`${TAG} saveImage failed to get valid image error = ${err}`)
  32.           return
  33.         }
  34.         // 根据图像的组件类型从图像中获取组件缓存 4-JPEG类型
  35.         imageObj.getComponent(image.ComponentType.JPEG, async (errMsg, imgComponent) => {
  36.           if (imgComponent === undefined) {
  37.             Logger.error(`${TAG} getComponent failed to get valid buffer error = ${errMsg}`)
  38.             return
  39.           }
  40.           if (imgComponent.byteBuffer) {
  41.             Logger.info(`${TAG} getComponent imgComponent.byteBuffer ${imgComponent.byteBuffer.byteLength}`)
  42.             buffer = imgComponent.byteBuffer
  43.             // todo 内置解码库不开源
  44.             let resultRGB: string = qr.decode(buffer)
  45.             Logger.info(`${TAG} AI uimg result RGB ${resultRGB}`)
  46.             if (callback) {
  47.               callback.aiResult(CodeRuleUtil.getRuleResult(resultRGB))
  48.             }
  49.           } else {
  50.             Logger.info(`${TAG} getComponent imgComponent.byteBuffer is undefined`)
  51.           }
解码

说明:解码使用内部的解码库因为不开源,非常抱歉,当然可以使用开源解码可以,如jsqr、zxing

  1. "dependencies": {
  2.     "jsqr""^1.4.0",
  3.     "@ohos/zxing""^2.0.0"
  4.   }
四、配网协议

说明:处于通用性考虑,需要对配网的二维码解析约定一个协议,也就是约定联网二维码数据的格式:##ssid##pwd##securityType

  • ssid :热点的SSID,编码格式为UTF-8。

  • pwd :热点的密钥

  • securityType :加密类型,这可以参看wifiManager.WifiSecurityType

在项目中也提供了协议解析类AnalyticResult.ts,具体代码如下:

  1. /**
  2.  * 结果解析类
  3.  */
  4. export type ResultType = {
  5.   ssid: string,
  6.   pwd: string,
  7.   securityType : number
  8. }
  9. const SEPARATOR: string = '##'
  10. export class Analytic {
  11.   constructor() {
  12.   }
  13.   getResult(msg: string): ResultType {
  14.     let result: ResultType = null
  15.     if (msg && msg.length > 0 && msg.indexOf(SEPARATOR) >= 0) {
  16.       let resultArr: string[] = msg.split(SEPARATOR)
  17.       if (resultArr.length >= 4) {
  18.         result = {
  19.           ssid: resultArr[1],
  20.           pwd: resultArr[2],
  21.           securityType: parseInt(resultArr[3])
  22.         }
  23.       }
  24.     }
  25.     return result
  26.   }
  27. }
五、配网页面

说明:通过对配网二维码的解析获取到热点的ssid、密钥、加密类型,就可以通过@ohos.wifiManager(WLAN)提供的网络连接接口实现配网。因为网络连接需要调用系统的一些验证流程,需要消耗一些时间,为了优化交互,需要一个网络连接等待界面ConnectPage.ets,界面截图如下:

具体代码如下:

  1. import { WifiConnectStatus } from '../model/Constant'
  2. import router from '@ohos.router';
  3. import { Logger } from '@ohos/common'
  4. import wifi from '@ohos.wifiManager';
  5. import { ResultType } from '../model/AnalyticResult'
  6. import { WifiModel } from '../model/WifiModel'
  7. /**
  8.  * 网络连接页面
  9.  */
  10. const TAG: string = '[ConnectPage]'
  11. const MAX_TIME_OUT: number = 60000 // 最大超时时间
  12. @Entry
  13. @Component
  14. struct ConnectPage {
  15.   @State mConnectSsid: string = ''
  16.   @State mConnectStatus: WifiConnectStatus = WifiConnectStatus.CONNECTING
  17.   @State mConnectingAngle : number = 0
  18.   @State mConnectFailResource : Resource = $r('app.string.connect_wifi_fail')
  19.   private linkedInfo: wifi.WifiLinkedInfo = null
  20.   private mWifiModel: WifiModel = new WifiModel()
  21.   private mTimeOutId: number = -1
  22.   private mAnimationTimeOutId : number = -1
  23.   async aboutToAppear() {
  24.     Logger.info(`${TAG} aboutToAppear`)
  25.     this.showConnecting()
  26.     let wifiResult: ResultType = router.getParams()['wifiResult']
  27.     Logger.info(`${TAG} wifiResult : ${JSON.stringify(wifiResult)}`)
  28.     // 如果wifi是开的,就记录下状态,然后扫描wifi,并获取连接信息
  29.     if (!wifi.isWifiActive()) {
  30.       Logger.info(TAG, 'enableWifi')
  31.       try {
  32.         wifi.enableWifi()
  33.       } catch (error) {
  34.         Logger.error(`${TAG} wifi enable fail, ${JSON.stringify(error)}`)
  35.       }
  36.     }
  37.     await this.getLinkedInfo()
  38.     // 启动监听
  39.     this.addListener()
  40.     if (wifiResult == null) {
  41.       Logger.info(TAG, 'wifiResult is null')
  42.       this.mConnectFailResource = $r('app.string.scan_code_data_error')
  43.       this.mConnectStatus = WifiConnectStatus.FAIL
  44.     } else {
  45.       this.mConnectSsid = wifiResult.ssid
  46.       Logger.info(`${TAG} connect wifi ${this.mConnectSsid}`)
  47.       this.disposeWifiConnect(wifiResult)
  48.     }
  49.   }
  50.   /**
  51.    * 启动超时任务
  52.    */
  53.   startTimeOut(): void {
  54.     Logger.info(TAG, `startTimeOut`)
  55.     this.mTimeOutId = setTimeout(() => {
  56.       // 如果超过1分钟没有连接上网络,则认为网络连接超时
  57.       try {
  58.         this.mConnectFailResource = $r('app.string.connect_wifi_fail')
  59.         this.mConnectStatus = WifiConnectStatus.FAIL
  60.         wifi.disconnect();
  61.       } catch (error) {
  62.         Logger.error(TAG, `failed,code:${JSON.stringify(error.code)},message:${JSON.stringify(error.message)}`)
  63.       }
  64.     }, MAX_TIME_OUT)
  65.   }
  66.   /**
  67.    * 取消超时任务
  68.    */
  69.   cancelTimeOut() {
  70.     Logger.info(TAG, `cancelTimeOut id:${this.mTimeOutId}`)
  71.     if (this.mTimeOutId >= 0) {
  72.       clearTimeout(this.mTimeOutId)
  73.       this.mTimeOutId = -1
  74.     }
  75.   }
  76.   // 监听wifi的变化
  77.   addListener() {
  78.     // 连接状态改变时,修改连接信息
  79.     wifi.on('wifiConnectionChange', async state => {
  80.       Logger.info(TAG, `wifiConnectionChange: ${state}`)
  81.       // 判断网络是否连接 0=断开  1=连接
  82.       if (state === 1) {
  83.         this.mConnectStatus = WifiConnectStatus.SUCCESS
  84.         this.cancelTimeOut()
  85.       }
  86.       await this.getLinkedInfo()
  87.     })
  88.     // wifi状态改变时,先清空wifi列表,然后判断是否是开启状态,如果是就扫描
  89.     wifi.on('wifiStateChange', state => {
  90.       Logger.info(TAG, `wifiStateLisener state: ${state}`)
  91.     })
  92.   }
  93.   // 获取有关Wi-Fi连接的信息,存入linkedInfo
  94.   async getLinkedInfo() {
  95.     try {
  96.       let wifiLinkedInfo = await wifi.getLinkedInfo()
  97.   if (wifiLinkedInfo === null || wifiLinkedInfo.bssid === '') {
  98.         this.linkedInfo = null
  99.         return
  100.       }
  101.       this.linkedInfo = wifiLinkedInfo
  102.     } catch (err) {
  103.       Logger.info(`getLinkedInfo failed err is ${JSON.stringify(err)}`)
  104.     }
  105.   }
  106.   /**
  107.    * 处理wifi连接
  108.    * @param wifiResult
  109.    */
  110.   disposeWifiConnect(wifiResult: ResultType): void {
  111.     this.mConnectStatus = WifiConnectStatus.CONNECTING
  112.     if (this.linkedInfo) {
  113.       // 说明wifi已经连接,需要确认需要连接的wifi和已连接的wifi是否为相同
  114.       let linkedSsid: string = this.linkedInfo.ssid;
  115.       if (linkedSsid === wifiResult.ssid) {
  116.         Logger.info(`${TAG} The same ssid`);
  117.         this.mConnectStatus = WifiConnectStatus.SUCCESS
  118.         return;
  119.       }
  120.       // 如果wifi不同,则先断开网络连接,再重新连接
  121.       try {
  122.         wifi.disconnect();
  123.         this.connectWifi(wifiResult.ssid, wifiResult.pwd, wifiResult.securityType)
  124.       } catch (error) {
  125.         Logger.error(TAG, `failed,code:${JSON.stringify(error.code)},message:${JSON.stringify(error.message)}`)
  126.       }
  127.     } else {
  128.       this.connectWifi(wifiResult.ssid, wifiResult.pwd, wifiResult.securityType)
  129.     }
  130.   }
  131.   private connectWifi(ssid: string, pwd: string, securityType : number) {
  132.     this.startTimeOut()
  133.     this.mWifiModel.connectNetwork(ssid, pwd, securityType)
  134.   }
  135.   async gotoIndex() {
  136.     try {
  137.       let options: router.RouterOptions = {
  138.         url: "pages/Index"
  139.       }
  140.       await router.replaceUrl(options)
  141.     } catch (error) {
  142.       Logger.error(`${TAG} go to index fail, err: ${JSON.stringify(error)}`)
  143.     }
  144.   }
  145.   showConnecting() {
  146.     this.mConnectingAngle = 0
  147.     this.mAnimationTimeOutId = setTimeout(() => {
  148.       this.mConnectingAngle = 360
  149.     }, 500)
  150.   }
  151.   closeConnecting() {
  152.     if (this.mAnimationTimeOutId > -1) {
  153.       clearTimeout(this.mAnimationTimeOutId)
  154.     }
  155.   }
  156.   aboutToDisappear() {
  157.     wifi.off('wifiConnectionChange')
  158.    wifi.off('wifiStateChange')
  159.     this.cancelTimeOut()
  160.     this.closeConnecting()
  161.   }
  162.   build() {
  163.     Column() {
  164.       // back
  165.       Row() {
  166.         Image($r('app.media.icon_back'))
  167.           .width(30)
  168.           .height(30)
  169.           .objectFit(ImageFit.Contain)
  170.           .onClick(() => {
  171.             router.back()
  172.           })
  173.       }
  174.       .width('90%')
  175.       .height('10%')
  176.       .justifyContent(FlexAlign.Start)
  177.       .alignItems(VerticalAlign.Center)
  178.       Stack() {
  179.         // 背景
  180.         Column() {
  181.           Image($r('app.media.bg_connect_wifi'))
  182.             .width('100%')
  183.             .height('100%')
  184.             .objectFit(ImageFit.Contain)
  185.             .rotate({
  186.               x: 0,
  187.               y: 0,
  188.               z: 1,
  189.               centerX: '50%',
  190.               centerY: '49%',
  191.               angle: this.mConnectingAngle
  192.             })
  193.             .animation({
  194.               duration: 2000// 动画时长
  195.               curve: Curve.Linear, // 动画曲线
  196.               delay: 0// 动画延迟
  197.               iterations: -1// 播放次数
  198.               playMode: PlayMode.Normal // 动画模式
  199.             })
  200.         }
  201.         Column({ space20 }) {
  202.           if (this.mConnectStatus === WifiConnectStatus.SUCCESS) {
  203.             // 连接成功
  204.             Image($r('app.media.icon_connect_wifi_success'))
  205.               .width(80)
  206.               .height(80)
  207.               .objectFit(ImageFit.Contain)
  208.             Text($r('app.string.connect_wifi_success'))
  209.               .fontSize(32)
  210.               .fontColor($r('app.color.connect_wifi_text'))
  211.             Text(this.mConnectSsid)
  212.               .fontSize(22)
  213.               .fontColor($r('app.color.connect_wifi_text'))
  214.           } else if (this.mConnectStatus === WifiConnectStatus.FAIL) {
  215.             // 连接失败
  216.             Image($r('app.media.icon_connect_wifi_fail'))
  217.               .width(80)
  218.               .height(80)
  219.               .objectFit(ImageFit.Contain)
  220.             Text(this.mConnectFailResource)
  221.               .fontSize(32)
  222.               .fontColor($r('app.color.connect_wifi_text'))
  223.             Button($r('app.string.reconnect_wifi'))
  224.               .width(260)
  225.               .height(55)
  226.               .backgroundColor($r('app.color.connect_fail_but_bg'))
  227.               .onClick(() => {
  228.                 this.gotoIndex()
  229.               })
  230.           } else {
  231.             // 连接中
  232.    Image($r('app.media.icon_connect_wifi'))
  233.               .width(100)
  234.               .height(100)
  235.               .objectFit(ImageFit.Contain)
  236.             Text($r('app.string.connect_wifi_hint'))
  237.               .fontSize(16)
  238.               .fontColor($r('app.color.connect_wifi_text'))
  239.             Text($r('app.string.connecting_wifi'))
  240.               .fontSize(32)
  241.               .fontColor($r('app.color.connect_wifi_text'))
  242.             Text(this.mConnectSsid)
  243.               .fontSize(22)
  244.               .fontColor($r('app.color.connect_wifi_text'))
  245.           }
  246.         }
  247.         .width('100%')
  248.         .height('100%')
  249.         .justifyContent(FlexAlign.Center)
  250.         .alignItems(HorizontalAlign.Center)
  251.       }
  252.       .width('100%')
  253.       .height('80%')
  254.     }
  255.     .width('100%')
  256.     .height('100%')
  257.     .backgroundColor($r('app.color.connect_bg'))
  258.   }
  259. }

整个界面比较简单,主要显示当前的连接状态:连接中、连接成功、连接超时,特别强调连接超时,计划热点最长连接60s,如果在预定时间未连接成功,则显示超时,超时后可以通过重新配网按钮进行重新扫码连接,根据实际测试,在热点未打开状态下扫码连接耗时平均值12s。

亮点

界面中最大的亮点,增加了一个发光圆形的属性动画animation,圆形在2s内绕着z轴旋从0度转到360度。

  1. Image($r('app.media.bg_connect_wifi'))
  2.             .width('100%')
  3.             .height('100%')
  4.             .objectFit(ImageFit.Contain)
  5.             .rotate({
  6.               x: 0,
  7.               y: 0,
  8.               z: 1,
  9.               centerX: '50%',
  10.               centerY: '49%',
  11.               angle: this.mConnectingAngle
  12.             })
  13.             .animation({
  14.               duration: 2000// 动画时长
  15.               curve: Curve.Linear, // 动画曲线
  16.               delay: 0// 动画延迟
  17.               iterations: -1// 播放次数
  18.               playMode: PlayMode.Normal // 动画模式
  19.             })
六、网络自动连接

说明:网络自动连接主要是通过@ohos.wifiManager(WLAN)提供的连接接口实现,具体代码如下:

  1. import wifi from '@ohos.wifiManager'
  2. import { Logger } from '@ohos/common'
  3. const TAGstring = '[WiFiModel]'
  4. export type WifiType = {
  5.   ssidstring,
  6.   bssidstring,
  7.   securityType: wifi.WifiSecurityType,
  8.   rssinumber,
  9.   bandnumber,
  10.   frequencynumber,
  11.   timestampnumber
  12. }
  13. export class WifiModel {
  14.   async getScanInfos(): Promise<Array<WifiType>> {
  15.     Logger.info(TAG'scanWifi begin')
  16.     let wifiListArray<WifiType> = []
  17.     let resultArray<wifi.WifiScanInfo> = []
  18.     try {
  19.       result = await wifi.getScanResults()
  20.     } catch (err) {
  21.       Logger.info(TAG`scan info err: ${JSON.stringify(err)}`)
  22.       return wifiList
  23.     }
  24.     Logger.info(TAG`scan info call back: ${result.length}`)
  25.     for (var i = 0; i < result.length; ++i) {
  26.       wifiList.push({
  27.         ssid: result[i].ssid,
  28.         bssid: result[i].bssid,
  29.         securityType: result[i].securityType,
  30.         rssi: result[i].rssi,
  31.         band: result[i].band,
  32.         frequency: result[i].frequency,
  33.         timestamp: result[i].timestamp
  34.       })
  35.     }
  36.     return wifiList
  37.   }
  38.   connectNetwork(wifiSsidstringpswstring, securityType : number): void {
  39.     Logger.debug(TAG`connectNetwork bssid=${wifiSsid} securityType:${securityType}`)
  40.     // securityType 加密类型默认:Pre-shared key (PSK)加密类型
  41.     let deviceConfig: wifi.WifiDeviceConfig  = {
  42.       ssid: wifiSsid,
  43.       preSharedKey: psw,
  44.       isHiddenSsidfalse,
  45.       securityType: securityType
  46.     }
  47.     try {
  48.       wifi.connectToDevice(deviceConfig)
  49.       Logger.info(TAG`connectToDevice success`)
  50.     } catch (err) {
  51.       Logger.error(TAG`connectToDevice fail err is ${JSON.stringify(err)}`)
  52.     }
  53.     try {
  54.       wifi.addDeviceConfig(deviceConfig)
  55.     } catch (err) {
  56.       Logger.error(TAG`addDeviceConfig fail err is ${JSON.stringify(err)}`)
  57.     }
  58.   }
  59. }

网络连接主要是通过wifi.connectToDevice(deviceConfig)实现,其中:deviceConfig: wifi.WifiDeviceConfig为WLAN配置信息,在连接网络时必填三个参数ssid、preSharedKey、securityType。

  • ssid:热点的SSID

  • preSharedKey:热点密钥

  • securityType:加密类型

注意:在调用connectToDevice()函数连接网络时,如果网络已经连接,则需要先调用disconnect()接口断开网络后再执行。

至此,你已经完成了扫码即可连接网络的应用。

感谢

如果您能看到最后,还希望您能动动手指点个赞,一个人能走多远关键在于与谁同行,我用跨越山海的一路相伴,希望得到您的点赞。

最后

如果你想快速提升鸿蒙技术,那么可以直接领取这份包含了:【OpenHarmony多媒体技术、Stage模型、ArkUI多端部署、分布式应用开发、音频、视频、WebGL、Napi组件、OpenHarmony内核、Harmony南向开发、鸿蒙项目实战】等技术知识点。

鸿蒙Next全套VIP学习资料←点击领取!(安全链接,放心点击

1.鸿蒙核心技术学习路线

2.大厂面试必问面试题

3.鸿蒙南向开发技术

 4.鸿蒙APP开发必备

 5.HarmonyOS Next 最新全套视频教程

 6.鸿蒙生态应用开发白皮书V2.0PDF

这份全套完整版的学习资料已经全部打包好,朋友们如果需要可以点击鸿蒙Next全套VIP学习资料免费领取(安全链接,放心点击

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/887655
推荐阅读
相关标签
  

闽ICP备14008679号