uniapp开发安卓APP自定义相机界面(用于人脸识别,ocr蒙版等功能)

项目中使用uniapp开发安卓APP,有一个需求需要自定义相机界面,拍照点击确定后上传到服务器,同时不退出相机界面上传成功后继续拍摄下一张

先看看呈现效果:
在这里插入图片描述
实现功能点:

其中功能点包含:打开相机、获取摄像头、默认打开后置摄像头、切换摄像头、自定义相机界面,关闭相机、打开闪光灯,压缩图片等

实现思路:

原理是在app内部打开一个web-view,在web-view中使用navigator.mediaDevices.getUserMedia获取到摄像头,并使用window.URL.createObjectURL(stream)将摄像头的视频流传到页面中的video标签,达到相机的效果,同时定义界面样式,我这里是定义了类似相机的样式,也可添加蒙版图片做成人脸识别的效果
点击拍照,使用canvas元素 context.drawImage 绘画出video中的图片,并显示canvas元素,隐藏video元素
点击完成,将图片转成base64格式
通过uni.postMessage传到父组件,在父组件中调用上传接口,上传,上传成功后修改url中的参数传到web-view中更新状态,判断是否最后一张,如果不是则拍摄下一张,是则退出拍照界面
实现代码如下
目录结构:
在这里插入图片描述
src\hybrid\html\index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>调用摄像头拍照</title>
    <!-- <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script> -->
    <script type="text/javascript" src="./js/jquery.js"></script>
    <link rel="stylesheet" href="./css/index.css" />
    <script>
      // 是否是手机
      function isMobile() {
        let userAgentInfo = navigator.userAgent;
        let mobileAgents = [
          "Android",
          "iPhone",
          "SymbianOS",
          "Windows Phone",
          "iPad",
          "iPod",
        ];
        let mobile_flag = false;
        //根据userAgent判断是否是手机
        for (let v = 0; v < mobileAgents.length; v++) {
          if (userAgentInfo.indexOf(mobileAgents[v]) > 0) {
            mobile_flag = true;
            break;
          }
        }
        let screen_width = window.screen.width;
        let screen_height = window.screen.height;
        //根据屏幕分辨率判断是否是手机
        if (screen_width > 325 && screen_height < 750) {
          mobile_flag = true;
        }
        console.log(mobile_flag, "mobile_flag");
        return mobile_flag;
      }

      /**
       * @description 本地图片转base64方法(兼容APP、H5、小程序)
       * @param {number} path 图片本地路径
       * @returns Promise对象
       */
      function toBase64(path) {
        return new Promise((resolve, reject) => {
          // #ifdef APP-PLUS
          plus.io.resolveLocalFileSystemURL(path, (entry) => {
            entry.file((file) => {
              let fileReader = new plus.io.FileReader();
              fileReader.readAsDataURL(file);
              fileReader.onloadend = (evt) => {
                console.log(evt, "evt");
                let base64 = evt.target.result.split(",")[1];
                resolve(base64);
              };
            });
          });
        });
      }
    </script>
  </head>

  <body>
    <div id="loading" class="loading">
      <div class="loading-spinner"></div>
      <div class="loading-text">上传中...</div>
    </div>
    <div id="navbar" class="nav">
      <span id="backIcon" class="back-icon">
        <svg
          t="1740124748321"
          class="icon"
          viewBox="0 0 1024 1024"
          version="1.1"
          xmlns="http://www.w3.org/2000/svg"
          p-id="2597"
          width="160"
          height="80">
          <path
            d="M872.802928 755.99406 872.864326 755.99406 872.864326 755.624646Z"
            fill="#e6e6e6"
            p-id="2598"></path>
          <path
            d="M927.846568 511.997953c0-229.315756-186.567139-415.839917-415.838893-415.839917-229.329059 0-415.85322 186.524161-415.85322 415.839917 0 229.300406 186.524161 415.84094 415.85322 415.84094C741.278405 927.838893 927.846568 741.29836 927.846568 511.997953M512.007675 868.171955c-196.375529 0-356.172979-159.827125-356.172979-356.174002 0-196.374506 159.797449-356.157629 356.172979-356.157629 196.34483 0 356.144326 159.783123 356.144326 356.157629C868.152001 708.34483 708.352505 868.171955 512.007675 868.171955"
            fill="#e6e6e6"
            p-id="2599"></path>
          <path
            d="M682.378947 642.227993 553.797453 513.264806 682.261267 386.229528c11.661597-11.514241 11.749602-30.332842 0.234337-41.995463-11.514241-11.676947-30.362518-11.765975-42.026162-0.222057L511.888971 471.195665 385.223107 344.130711c-11.602246-11.603269-30.393217-11.661597-42.025139-0.059352-11.603269 11.618619-11.603269 30.407544-0.059352 42.011836l126.518508 126.887922L342.137823 639.104863c-11.662621 11.543917-11.780301 30.305213-0.23536 41.96988 5.830799 5.89015 13.429871 8.833179 21.086248 8.833179 7.53972 0 15.136745-2.8847 20.910239-8.569166l127.695311-126.311801L640.293433 684.195827c5.802146 5.8001 13.428847 8.717546 21.056572 8.717546 7.599072 0 15.165398-2.917446 20.968567-8.659217C693.922864 672.681586 693.950494 653.889591 682.378947 642.227993"
            fill="#e6e6e6"
            p-id="2600"></path>
        </svg>
      </span>
      <div id="navTitle" class="nav-title">拍照上传</div>
      <span id="switchCameraIcon" class="switch-camera-icon">
        <svg
          t="1740126202808"
          class="icon"
          viewBox="0 0 1024 1024"
          version="1.1"
          xmlns="http://www.w3.org/2000/svg"
          p-id="5573"
          width="160"
          height="80">
          <path
            d="M837.096193 221.735104h-74.30738l-20.898982-60.375066c-16.255219-46.442752-60.375066-78.952166-109.1397-78.952167H388.927987c-51.086515 0-95.206362 32.509415-109.139699 78.952167l-20.898983 60.375066h-71.985498c-102.173031 0-185.768961 83.59593-185.76896 185.76896v348.317057c0 102.173031 83.59593 185.768961 185.76896 185.768961h650.192386c102.173031 0 185.768961-83.59593 185.76896-185.768961V407.505088c0-102.173031-83.59593-185.769984-185.76896-185.769984z m116.105344 534.087041c0 65.018829-51.086515 116.105345-116.105344 116.105344H186.903807c-65.018829 0-116.105345-51.086515-116.105344-116.105344V407.505088c0-65.018829 51.086515-116.105345 116.105344-116.105345h123.072013l16.255219-48.764634 20.898983-60.375065c6.966669-18.577101 23.220864-32.509415 44.119847-32.509415h243.822144c20.898982 0 37.154201 13.932314 44.119847 32.509415l20.898982 60.375065 16.255219 48.764634h120.750132c65.018829 0 116.105345 51.086515 116.105344 116.105345v348.317057z m-218.278375-97.529268c-39.476083 88.240717-127.715777 143.970996-222.923162 143.970996-83.59593 0-160.226215-44.119847-204.346061-113.783463l-37.154202 11.610432 32.509415-141.649113 106.816794 99.851148-32.509415 9.288551c32.509415 39.476083 81.274048 65.018829 134.682446 65.018829 69.663616 0 132.360564-39.476083 160.226214-104.494912 6.966669-18.577101 27.865651-25.542746 46.442752-18.577101 16.255219 9.28855 23.220864 30.187533 16.255219 48.764633z m-13.933337-225.245043l34.831296-9.288551-34.831296 134.682446-99.851149-95.206363 30.187533-9.28855c-32.509415-44.119847-83.59593-69.663616-139.327232-69.663616-65.018829 0-123.072013 34.831296-153.259546 92.88448-9.28855 16.255219-30.187533 23.220864-46.442752 13.932314s-23.220864-30.187533-13.932314-46.442752c41.797965-78.952166 125.393895-130.038682 215.956493-130.038681 83.596953 0.002047 162.54912 46.443775 206.668967 118.429273z"
            fill="#fff"
            p-id="5574"></path>
        </svg>
      </span>
    </div>
    <div id="container" class="container">
      <canvas id="canvas"></canvas>
    </div>
    <!-- </div> -->
    <!-- 蒙版图片 -->
    <img id="bgImg" src="" alt="" class="bg-img" />
    <div class="mb"></div>
    <video id="video" width="100%"></video>
    <div class="btmBox">
      <span id="albumBtn">
        <svg
          t="1740198369293"
          class="icon"
          viewBox="0 0 1024 1024"
          version="1.1"
          xmlns="http://www.w3.org/2000/svg"
          p-id="7554"
          width="200"
          height="100">
          <path
            d="M160 160v704h704V160H160zM128 96h768a31.146667 31.146667 0 0 1 23.04 8.96c5.973333 6.016 8.96 13.696 8.96 23.04v768a31.146667 31.146667 0 0 1-8.96 23.04 31.146667 31.146667 0 0 1-23.04 8.96H128a31.146667 31.146667 0 0 1-23.04-8.96 31.146667 31.146667 0 0 1-8.96-23.04V128a31.146667 31.146667 0 0 1 8.96-23.04 31.146667 31.146667 0 0 1 23.04-8.96z m256 192c42.666667 0 64 21.333333 64 64S426.666667 416 384 416s-64-21.333333-64-64 21.333333-64 64-64zM185.002667 877.013333l-50.048-39.04 216.021333-282.026666a91.733333 91.733333 0 0 1 63.488-35.968 94.208 94.208 0 0 1 70.485333 18.005333l125.013334 100.992a32.256 32.256 0 0 0 23.466666 6.485333 33.749333 33.749333 0 0 0 21.504-11.477333l216.021334-270.037333 50.005333 40.021333-216.021333 270.037333c-16.64 20.650667-38.144 32.298667-64.469334 34.986667a95.402667 95.402667 0 0 1-70.528-20.010667l-123.989333-99.968a32.256 32.256 0 0 0-23.466667-6.528 28.970667 28.970667 0 0 0-20.522666 12.501334l-216.96 282.026666z"
            fill="#ffffff"
            fill-opacity=".8"
            p-id="7555"></path>
        </svg>
      </span>
      <button id="capture">拍照</button>
      <button id="finishBtn">完成</button>
    </div>
  </body>
  <!-- uniSdk -->
  <script type="text/javascript" src="./js/unisdk.js"></script>
  <script type="text/javascript">
    let flag = false;
    let strType;
    // 参数
    let urlParams = {};
    // 获取dom
    const backIcon = document.getElementById("backIcon");
    const switchCameraIcon = document.getElementById("switchCameraIcon");
    const albumBtn = document.getElementById("albumBtn");
    const video = document.getElementById("video"); // video
    const allImg = document.getElementById("allImg");
    const finishBtn = document.getElementById("finishBtn");
    const capture = document.getElementById("capture");
    const canvas = document.getElementById("canvas");
    const bgImg = document.getElementById("bgImg");
    const navTitle = document.getElementById("navTitle");
    const navbar = document.getElementById("navbar");
    const loadingElement = document.getElementById("loading");
    const container = document.getElementById("container");
    const context = canvas.getContext("2d");
    let currentStream = null;
    let currentVideoDeviceIndex = null;
    let timer = null;
    $(finishBtn).hide();
    $(allImg).hide();
    $(video).hide();
    $(canvas).hide();
    $(container).hide();
    $(bgImg).hide();
    // 获取传惨
    function getPramas() {
      var params = new URLSearchParams(window.location.search);
      urlParams = {
        title: params.get("title"),
        index: params.get("index"),
        titleList: params.get("titleList").split(","),
        type: params.get("type"),
        // result: params.get("result"),
      };
      if (params.get("topHight")) {
        urlParams.topHight = Number(params.get("topHight")) + 10;
      } else {
        urlParams.topHight = 0;
      }
    }
    // 更新标题
    function updateTitle() {
      if (urlParams.title) {
        $("#navTitle").html(urlParams.title);
      } else {
        urlParams.title = "拍照上传";
      }
      if (urlParams.titleList) {
        if (urlParams.titleList && (urlParams.index || urlParams.index == 0)) {
          console.log(urlParams.titleList[urlParams.index], "3333");
          $("#navTitle").html(urlParams.titleList[urlParams.index]);
        } else {
          urlParams.title = "拍照上传";
        }
      }
    }
    getPramas();
    updateTitle();
    navbar.style["padding-top"] = `${urlParams.topHight}px`;
    // 获取屏幕区域
    const screen_W = window.innerWidth || document.body.clientWidth;
    const screen_H = window.innerHeight || document.body.clientHeight;
    const videoWidth = screen_W;
    const videoHeight = screen_H - 298 - 180 - urlParams.topHight;
    video.style.height = `${screen_H - 298 - 180 - urlParams.topHight}px`;
    //  视频流分辨率设置
    let constraints = {
      // video: true
      video: {
        // 前置摄像头&后置摄像头  默认后置摄像头
        facingMode: {
          exact: "environment",
        },
      },
    };
    setTimeout(() => {
      canvas.width = screen_W;
      canvas.height = videoHeight;
      // 判断是否是手机
      if (isMobile()) {
        $(canvas).css({
          width: "100%",
        });
        canvas.width = screen_W;
        canvas.height = videoHeight;

        // alert("phone");
      }
      if (navigator.mediaDevices === undefined) {
        navigator.mediaDevices = {};
      }
      // 判断摄像是否开启
      if (navigator.mediaDevices.getUserMedia === undefined) {
        navigator.mediaDevices.getUserMedia = function (constraints) {
          vargetUserMedia =
            navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
          if (!getUserMedia) {
            returnPromise.reject(
              newError("getUserMedia is not implemented in this browser")
            );
          }
          return newPromise(function (resolve, reject) {
            getUserMedia.call(navigator, constraints, resolve, reject);
          });
        };
      }
      // 获取摄像头
      navigator.mediaDevices
        .getUserMedia(constraints)
        .then(function (stream) {
          $(video).show();
          if ("srcObject" in video) {
            video.srcObject = stream;
            currentStream = stream;
          } else {
            video.src = window.URL.createObjectURL(stream);
          }
          video.onloadedmetadata = function (e) {
            video.play();
          };
        })
        .catch(function (err) {
          var r = confirm("是否授权使用媒体相机采集图片");
          if (r == true) {
            // 这里可以调用5+ API了,为了更好的兼容性,应该使用以下代码进行判断
            if (window.plus) {
              var cmr = plus.camera.getCamera();
            } else {
              // 兼容老版本的plusready事件
              document.addEventListener(
                "plusready",
                function () {
                  var cmr = plus.camera.getCamera();
                },
                false
              );
            }
          } else {
            uni.webView.navigateBack();
          }
        });
      // 拍照&重新拍照
      capture.addEventListener("click", function (e) {
        if (!flag) {
          $(container).show();
          $(canvas).show();
          let ratio = video.videoWidth / video.videoHeight;
          canvas.height = canvas.width / ratio;
          // console.log(canvas.width / ratio, "height");
          context.drawImage(video, 0, 0, canvas.width, canvas.width / ratio);
          // context.drawImage(video, 50, 185, 400, 280, 50, 280, 880, 450);
          $(allImg).show();
          $(finishBtn).show();
          $(video).hide();
          $(capture).text("重新拍照");
          flag = true;
        } else {
          flag = false;
          context.clearRect(0, 0, canvas.width, canvas.height);
          $(allImg).hide();
          $(video).show();
          $(finishBtn).hide();
          $(container).hide();
          $(canvas).hide();
          $(capture).text("拍照");
        }
      });
      // 打开相册
      albumBtn.addEventListener("click", function (e) {
        plus.gallery.pick(
          function (path) {
            loadingElement.style.display = "flex";
            uni.postMessage({
              data: {
                path: path,
                action: "getImagePath",
              },
            });
            // setTimeout(() => {
            //   if (urlParams.index || urlParams.index == 0) {
            //     ++urlParams.index;
            //   }
            // }, 500);
            console.log(path, "path");
          },
          function (e) {
            console.log("取消选择图片");
          },
          {
            filter: "image",
          }
        );
      });
      //确认照片
      finishBtn.addEventListener("click", function (e) {
        sureImg();
      });
      // 返回
      backIcon.addEventListener("click", function (e) {
        // 返回前清除计时器
        clearTimer();
        uni.postMessage({
          data: {
            action: "close",
          },
        });
      });
      // 切换摄像头
      switchCameraIcon.addEventListener("click", switchCamera);
    }, 500);
    // 切换摄像头
    async function switchCamera() {
      try {
        // 获取当前所有媒体设备
        const devices = await navigator.mediaDevices.enumerateDevices();
        const videoDevices = devices.filter(
          (device) => device.kind === "videoinput"
        );

        // 如果只有一个视频设备,则无法切换
        if (videoDevices.length <= 1) {
          console.log("Only one camera available.");
          return;
        }
        // 获取当前活动的流(如果有的话)并停止它
        if (currentStream) {
          currentStream.getTracks().forEach((track) => track.stop());
        }
        // 切换摄像头设备
        if (constraints.video.facingMode.exact == "user") {
          constraints.video.facingMode.exact = "environment"; // 后置摄像头
        } else {
          constraints.video.facingMode.exact = "user"; // 前置摄像头
        }
        // 请求新的媒体流
        const newStream = await navigator.mediaDevices.getUserMedia(
          constraints
        );
        updateVideoElement(newStream); // 更新视频元素以显示新流
      } catch (error) {
        console.error("Error switching camera.", error);
      }
    }
    // 更新视频元素以显示新流
    function updateVideoElement(stream) {
      $(video).show();
      if ("srcObject" in video) {
        video.srcObject = stream;
      } else {
        video.src = window.URL.createObjectURL(stream);
      }
      video.onloadedmetadata = function (e) {
        video.play();
      };
    }
    /**
     * 确认照片
     */
    function sureImg() {
      loadingElement.style.display = "flex";
      let imgDataSrc = null;
      compressImage(canvas.toDataURL("image/png"), (file) => {
        imgDataSrc = file;
        uni.postMessage({
          data: {
            imageFile: imgDataSrc,
            action: "getImageFile",
          },
        });
      });
    }
    /**
     * 压缩照片
     */
    function compressImage(base64, callback) {
      var targSize = 1024 * 1024; //1024KB
      if (base64.length <= targSize) {
        callback(base64);
        // console.log("直接返回")
        return;
      }
      var newImage = new Image();
      newImage.src = base64;
      // newImage.setAttribute("crossOrigin", 'Anonymous'); //url为外域时需要
      newImage.onload = function () {
        // let ratio = video.videoWidth / video.videoHeight;
        var quality = 1; //压缩系数
        // var canvas = document.createElement("canvas");
        // var ctx = canvas.getContext("2d");
        let ratio = video.videoWidth / video.videoHeight;
        context.clearRect(0, 0, canvas.width, canvas.width / ratio);
        // ctx.drawImage(video, 0, 0, canvas.width, canvas.width / ratio);
        canvas.height = canvas.width / ratio;
        context.drawImage(video, 0, 0, canvas.width, canvas.width / ratio);
        var base64 = canvas.toDataURL("image/png", quality);
        callback(base64); //必须通过回调函数返回,否则无法及时拿到该值
      };
    }

    /**
     * 打开闪光灯
     */
    function onTorch() {
      try {
        var os = plus.os.name;
        if ("iOS" == os) {
          var device = plus.ios.invoke(
            "AVCaptureDevice",
            "defaultDeviceWithMediaType:",
            "vide"
          );
          plus.ios.invoke(device, "lockForConfiguration:", null);
          plus.ios.invoke(device, "setTorchMode:", 1);
          plus.ios.invoke(device, "setFlashMode:", 1);
          plus.ios.invoke(device, "unlockForConfiguration");
        } else {
          var main = plus.android.runtimeMainActivity();
          var camera = main.getSystemService("camera");
          var ids = plus.android.invoke(camera, "getCameraIdList");
          for (var i = 0; i < ids.length; i++) {
            var c = plus.android.invoke(
              camera,
              "getCameraCharacteristics",
              ids[i]
            );
            var available = plus.android.invoke(
              c,
              "get",
              plus.android.getAttribute(c, "FLASH_INFO_AVAILABLE")
            );
            var facing = plus.android.invoke(
              c,
              "get",
              plus.android.getAttribute(c, "LENS_FACING")
            );
            if (
              null != available &&
              available &&
              null != facing &&
              1 == facing
            ) {
              plus.android.invoke(camera, "setTorchMode", ids[i], true);
            }
          }
        }
      } catch (e) {
        console.error(e, "error @onTorch!!");
      }
    }

    /**
     * 关闭闪光灯
     */
    function offTorch() {
      try {
        var os = plus.os.name;
        if ("iOS" == os) {
          var device = plus.ios.invoke(
            "AVCaptureDevice",
            "defaultDeviceWithMediaType:",
            "vide"
          );
          plus.ios.invoke(device, "lockForConfiguration:", null);
          plus.ios.invoke(device, "setTorchMode:", 0);
          plus.ios.invoke(device, "setFlashMode:", 0);
          plus.ios.invoke(device, "unlockForConfiguration");
        } else {
          var main = plus.android.runtimeMainActivity();
          var camera = main.getSystemService("camera");
          var ids = plus.android.invoke(camera, "getCameraIdList");
          for (var i = 0; i < ids.length; i++) {
            var c = plus.android.invoke(
              camera,
              "getCameraCharacteristics",
              ids[i]
            );
            var available = plus.android.invoke(
              c,
              "get",
              plus.android.getAttribute(c, "FLASH_INFO_AVAILABLE")
            );
            var facing = plus.android.invoke(
              c,
              "get",
              plus.android.getAttribute(c, "LENS_FACING")
            );
            if (
              null != available &&
              available &&
              null != facing &&
              1 == facing
            ) {
              plus.android.invoke(camera, "setTorchMode", ids[i], false);
            }
          }
        }
      } catch (e) {
        console.error("error @offTorch!!");
      }
    }
    /**
     *  上传图片回掉处理逻辑 start
     * */
    // 上传成功回调
    function uploadCallback() {
      var params = new URLSearchParams(window.location.search);
      if (params.get("result")) {
        let { code, data } = params.get("result");
        if (code == 200) {
          getPramas();
          updateTitle();
          setTimeout(() => {
            loadingElement.style.display = "none";
          }, 500);
        }
      }
    }

    let params = null;
    function getQueryParams() {
      const search = window.location.search.substring(1);
      return JSON.parse(
        '{"' +
          decodeURIComponent(search)
            .replace(/"/g, '\\"')
            .replace(/&/g, '","')
            .replace(/=/g, '":"') +
          '"}'
      );
    }
    // 通过侦听URL变化判断是否上传成功
    timer = setInterval(() => {
      const newParams = getQueryParams();
      if (JSON.stringify(newParams) !== JSON.stringify(params)) {
        console.log("Params changed:", newParams);
        // 更新 params
        params = newParams;
        uploadCallback();
      }
    }, 1000); // 每秒检查一次
    function clearTimer() {
      console.log("清除计时器");
      if (timer) {
        clearInterval(timer);
        timer = null;
      }
    }
    // 侦听页面卸载 清除计数器
    window.addEventListener("beforeunload", clearTimer);
    window.addEventListener("unload", clearTimer);
    /**
     *  上传图片回掉处理逻辑 end
     * */
  </script>
</html>

src\hybrid\html\js\unisdk.js

! function (e, n) {
    "object" == typeof exports && "undefined" != typeof module ? module.exports = n() : "function" == typeof define &&
        define.amd ? define(n) : (e = e || self).uni = n()
}(this, (function () {
    "use strict";
    try {
        var e = {};
        Object.defineProperty(e, "passive", {
            get: function () {
                !0
            }
        }), window.addEventListener("test-passive", null, e)
    } catch (e) { }
    var n = Object.prototype.hasOwnProperty;

    function i (e, i) {
        return n.call(e, i)
    }
    var t = [];

    function r () {
        return window.__dcloud_weex_postMessage || window.__dcloud_weex_
    }
    var o = function (e, n) {
        var i = {
            options: {
                timestamp: +new Date
            },
            name: e,
            arg: n
        };
        if (r()) {
            if ("postMessage" === e) {
                var o = {
                    data: [n]
                };
                return window.__dcloud_weex_postMessage ? window.__dcloud_weex_postMessage(o) : window
                    .__dcloud_weex_.postMessage(JSON.stringify(o))
            }
            var a = {
                type: "WEB_INVOKE_APPSERVICE",
                args: {
                    data: i,
                    webviewIds: t
                }
            };
            window.__dcloud_weex_postMessage ? window.__dcloud_weex_postMessageToService(a) : window
                .__dcloud_weex_.postMessageToService(JSON.stringify(a))
        }
        if (!window.plus) return window.parent.postMessage({
            type: "WEB_INVOKE_APPSERVICE",
            data: i,
            pageId: ""
        }, "*");
        if (0 === t.length) {
            var d = plus.webview.currentWebview();
            if (!d) throw new Error("plus.webview.currentWebview() is undefined");
            var s = d.parent(),
                w = "";
            w = s ? s.id : d.id, t.push(w)
        }
        if (plus.webview.getWebviewById("__uniapp__service")) plus.webview.postMessageToUniNView({
            type: "WEB_INVOKE_APPSERVICE",
            args: {
                data: i,
                webviewIds: t
            }
        }, "__uniapp__service");
        else {
            var u = JSON.stringify(i);
            plus.webview.getLaunchWebview().evalJS('UniPlusBridge.subscribeHandler("'.concat(
                "WEB_INVOKE_APPSERVICE", '",').concat(u, ",").concat(JSON.stringify(t), ");"))
        }
    },
        a = {
            navigateTo: function () {
                var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
                    n = e.url;
                o("navigateTo", {
                    url: encodeURI(n)
                })
            },
            navigateBack: function () {
                var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
                    n = e.delta;
                o("navigateBack", {
                    delta: parseInt(n) || 1
                })
            },
            switchTab: function () {
                var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
                    n = e.url;
                o("switchTab", {
                    url: encodeURI(n)
                })
            },
            reLaunch: function () {
                var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
                    n = e.url;
                o("reLaunch", {
                    url: encodeURI(n)
                })
            },
            redirectTo: function () {
                var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
                    n = e.url;
                o("redirectTo", {
                    url: encodeURI(n)
                })
            },
            getEnv: function (e) {
                r() ? e({
                    nvue: !0
                }) : window.plus ? e({
                    plus: !0
                }) : e({
                    h5: !0
                })
            },
            postMessage: function () {
                var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {};
                o("postMessage", e.data || {})
            }
        },
        d = /uni-app/i.test(navigator.userAgent),
        s = /Html5Plus/i.test(navigator.userAgent),
        w = /complete|loaded|interactive/;
    var u = window.my && navigator.userAgent.indexOf(["t", "n", "e", "i", "l", "C", "y", "a", "p", "i", "l",
        "A"
    ].reverse().join("")) > -1;
    var g = window.swan && window.swan.webView && /swan/i.test(navigator.userAgent);
    var v = window.qq && window.qq.miniProgram && /QQ/i.test(navigator.userAgent) && /miniProgram/i.test(
        navigator.userAgent);
    var c = window.tt && window.tt.miniProgram && /toutiaomicroapp/i.test(navigator.userAgent);
    var m = window.wx && window.wx.miniProgram && /micromessenger/i.test(navigator.userAgent) &&
        /miniProgram/i.test(navigator.userAgent);
    var p = window.qa && /quickapp/i.test(navigator.userAgent);
    var f = window.ks && window.ks.miniProgram && /micromessenger/i.test(navigator.userAgent) &&
        /miniProgram/i.test(navigator.userAgent);
    var l = window.tt && window.tt.miniProgram && /Lark|Feishu/i.test(navigator.userAgent);
    var _ = window.jd && window.jd.miniProgram && /micromessenger/i.test(navigator.userAgent) &&
        /miniProgram/i.test(navigator.userAgent);
    var E = window.xhs && window.xhs.miniProgram && /xhsminiapp/i.test(navigator.userAgent);
    for (var h, P = function () {
        window.UniAppJSBridge = !0, document.dispatchEvent(new CustomEvent("UniAppJSBridgeReady", {
            bubbles: !0,
            cancelable: !0
        }))
    }, b = [function (e) {
        if (d || s) return window.__dcloud_weex_postMessage || window.__dcloud_weex_ ? document
            .addEventListener("DOMContentLoaded", e) : window.plus && w.test(document
                .readyState) ? setTimeout(e, 0) : document.addEventListener("plusready", e), a
    }, function (e) {
        if (m) return window.WeixinJSBridge && window.WeixinJSBridge.invoke ? setTimeout(e, 0) :
            document.addEventListener("WeixinJSBridgeReady", e), window.wx.miniProgram
    }, function (e) {
        if (v) return window.QQJSBridge && window.QQJSBridge.invoke ? setTimeout(e, 0) : document
            .addEventListener("QQJSBridgeReady", e), window.qq.miniProgram
    }, function (e) {
        if (u) {
            document.addEventListener("DOMContentLoaded", e);
            var n = window.my;
            return {
                navigateTo: n.navigateTo,
                navigateBack: n.navigateBack,
                switchTab: n.switchTab,
                reLaunch: n.reLaunch,
                redirectTo: n.redirectTo,
                postMessage: n.postMessage,
                getEnv: n.getEnv
            }
        }
    }, function (e) {
        if (g) return document.addEventListener("DOMContentLoaded", e), window.swan.webView
    }, function (e) {
        if (c) return document.addEventListener("DOMContentLoaded", e), window.tt.miniProgram
    }, function (e) {
        if (p) {
            window.QaJSBridge && window.QaJSBridge.invoke ? setTimeout(e, 0) : document
                .addEventListener("QaJSBridgeReady", e);
            var n = window.qa;
            return {
                navigateTo: n.navigateTo,
                navigateBack: n.navigateBack,
                switchTab: n.switchTab,
                reLaunch: n.reLaunch,
                redirectTo: n.redirectTo,
                postMessage: n.postMessage,
                getEnv: n.getEnv
            }
        }
    }, function (e) {
        if (f) return window.WeixinJSBridge && window.WeixinJSBridge.invoke ? setTimeout(e, 0) :
            document.addEventListener("WeixinJSBridgeReady", e), window.ks.miniProgram
    }, function (e) {
        if (l) return document.addEventListener("DOMContentLoaded", e), window.tt.miniProgram
    }, function (e) {
        if (_) return window.JDJSBridgeReady && window.JDJSBridgeReady.invoke ? setTimeout(e, 0) :
            document.addEventListener("JDJSBridgeReady", e), window.jd.miniProgram
    }, function (e) {
        if (E) return window.xhs.miniProgram
    }, function (e) {
        return document.addEventListener("DOMContentLoaded", e), a
    }], y = 0; y < b.length && !(h = b[y](P)); y++);
    h || (h = {});
    var B = "undefined" != typeof uni ? uni : {};
    if (!B.navigateTo)
        for (var S in h) i(h, S) && (B[S] = h[S]);
    return B.webView = h, B
}));

src\hybrid\html\js\jquery.js

! function (e, n) {
    "object" == typeof exports && "undefined" != typeof module ? module.exports = n() : "function" == typeof define &&
        define.amd ? define(n) : (e = e || self).uni = n()
}(this, (function () {
    "use strict";
    try {
        var e = {};
        Object.defineProperty(e, "passive", {
            get: function () {
                !0
            }
        }), window.addEventListener("test-passive", null, e)
    } catch (e) { }
    var n = Object.prototype.hasOwnProperty;

    function i (e, i) {
        return n.call(e, i)
    }
    var t = [];

    function r () {
        return window.__dcloud_weex_postMessage || window.__dcloud_weex_
    }
    var o = function (e, n) {
        var i = {
            options: {
                timestamp: +new Date
            },
            name: e,
            arg: n
        };
        if (r()) {
            if ("postMessage" === e) {
                var o = {
                    data: [n]
                };
                return window.__dcloud_weex_postMessage ? window.__dcloud_weex_postMessage(o) : window
                    .__dcloud_weex_.postMessage(JSON.stringify(o))
            }
            var a = {
                type: "WEB_INVOKE_APPSERVICE",
                args: {
                    data: i,
                    webviewIds: t
                }
            };
            window.__dcloud_weex_postMessage ? window.__dcloud_weex_postMessageToService(a) : window
                .__dcloud_weex_.postMessageToService(JSON.stringify(a))
        }
        if (!window.plus) return window.parent.postMessage({
            type: "WEB_INVOKE_APPSERVICE",
            data: i,
            pageId: ""
        }, "*");
        if (0 === t.length) {
            var d = plus.webview.currentWebview();
            if (!d) throw new Error("plus.webview.currentWebview() is undefined");
            var s = d.parent(),
                w = "";
            w = s ? s.id : d.id, t.push(w)
        }
        if (plus.webview.getWebviewById("__uniapp__service")) plus.webview.postMessageToUniNView({
            type: "WEB_INVOKE_APPSERVICE",
            args: {
                data: i,
                webviewIds: t
            }
        }, "__uniapp__service");
        else {
            var u = JSON.stringify(i);
            plus.webview.getLaunchWebview().evalJS('UniPlusBridge.subscribeHandler("'.concat(
                "WEB_INVOKE_APPSERVICE", '",').concat(u, ",").concat(JSON.stringify(t), ");"))
        }
    },
        a = {
            navigateTo: function () {
                var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
                    n = e.url;
                o("navigateTo", {
                    url: encodeURI(n)
                })
            },
            navigateBack: function () {
                var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
                    n = e.delta;
                o("navigateBack", {
                    delta: parseInt(n) || 1
                })
            },
            switchTab: function () {
                var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
                    n = e.url;
                o("switchTab", {
                    url: encodeURI(n)
                })
            },
            reLaunch: function () {
                var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
                    n = e.url;
                o("reLaunch", {
                    url: encodeURI(n)
                })
            },
            redirectTo: function () {
                var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
                    n = e.url;
                o("redirectTo", {
                    url: encodeURI(n)
                })
            },
            getEnv: function (e) {
                r() ? e({
                    nvue: !0
                }) : window.plus ? e({
                    plus: !0
                }) : e({
                    h5: !0
                })
            },
            postMessage: function () {
                var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {};
                o("postMessage", e.data || {})
            }
        },
        d = /uni-app/i.test(navigator.userAgent),
        s = /Html5Plus/i.test(navigator.userAgent),
        w = /complete|loaded|interactive/;
    var u = window.my && navigator.userAgent.indexOf(["t", "n", "e", "i", "l", "C", "y", "a", "p", "i", "l",
        "A"
    ].reverse().join("")) > -1;
    var g = window.swan && window.swan.webView && /swan/i.test(navigator.userAgent);
    var v = window.qq && window.qq.miniProgram && /QQ/i.test(navigator.userAgent) && /miniProgram/i.test(
        navigator.userAgent);
    var c = window.tt && window.tt.miniProgram && /toutiaomicroapp/i.test(navigator.userAgent);
    var m = window.wx && window.wx.miniProgram && /micromessenger/i.test(navigator.userAgent) &&
        /miniProgram/i.test(navigator.userAgent);
    var p = window.qa && /quickapp/i.test(navigator.userAgent);
    var f = window.ks && window.ks.miniProgram && /micromessenger/i.test(navigator.userAgent) &&
        /miniProgram/i.test(navigator.userAgent);
    var l = window.tt && window.tt.miniProgram && /Lark|Feishu/i.test(navigator.userAgent);
    var _ = window.jd && window.jd.miniProgram && /micromessenger/i.test(navigator.userAgent) &&
        /miniProgram/i.test(navigator.userAgent);
    var E = window.xhs && window.xhs.miniProgram && /xhsminiapp/i.test(navigator.userAgent);
    for (var h, P = function () {
        window.UniAppJSBridge = !0, document.dispatchEvent(new CustomEvent("UniAppJSBridgeReady", {
            bubbles: !0,
            cancelable: !0
        }))
    }, b = [function (e) {
        if (d || s) return window.__dcloud_weex_postMessage || window.__dcloud_weex_ ? document
            .addEventListener("DOMContentLoaded", e) : window.plus && w.test(document
                .readyState) ? setTimeout(e, 0) : document.addEventListener("plusready", e), a
    }, function (e) {
        if (m) return window.WeixinJSBridge && window.WeixinJSBridge.invoke ? setTimeout(e, 0) :
            document.addEventListener("WeixinJSBridgeReady", e), window.wx.miniProgram
    }, function (e) {
        if (v) return window.QQJSBridge && window.QQJSBridge.invoke ? setTimeout(e, 0) : document
            .addEventListener("QQJSBridgeReady", e), window.qq.miniProgram
    }, function (e) {
        if (u) {
            document.addEventListener("DOMContentLoaded", e);
            var n = window.my;
            return {
                navigateTo: n.navigateTo,
                navigateBack: n.navigateBack,
                switchTab: n.switchTab,
                reLaunch: n.reLaunch,
                redirectTo: n.redirectTo,
                postMessage: n.postMessage,
                getEnv: n.getEnv
            }
        }
    }, function (e) {
        if (g) return document.addEventListener("DOMContentLoaded", e), window.swan.webView
    }, function (e) {
        if (c) return document.addEventListener("DOMContentLoaded", e), window.tt.miniProgram
    }, function (e) {
        if (p) {
            window.QaJSBridge && window.QaJSBridge.invoke ? setTimeout(e, 0) : document
                .addEventListener("QaJSBridgeReady", e);
            var n = window.qa;
            return {
                navigateTo: n.navigateTo,
                navigateBack: n.navigateBack,
                switchTab: n.switchTab,
                reLaunch: n.reLaunch,
                redirectTo: n.redirectTo,
                postMessage: n.postMessage,
                getEnv: n.getEnv
            }
        }
    }, function (e) {
        if (f) return window.WeixinJSBridge && window.WeixinJSBridge.invoke ? setTimeout(e, 0) :
            document.addEventListener("WeixinJSBridgeReady", e), window.ks.miniProgram
    }, function (e) {
        if (l) return document.addEventListener("DOMContentLoaded", e), window.tt.miniProgram
    }, function (e) {
        if (_) return window.JDJSBridgeReady && window.JDJSBridgeReady.invoke ? setTimeout(e, 0) :
            document.addEventListener("JDJSBridgeReady", e), window.jd.miniProgram
    }, function (e) {
        if (E) return window.xhs.miniProgram
    }, function (e) {
        return document.addEventListener("DOMContentLoaded", e), a
    }], y = 0; y < b.length && !(h = b[y](P)); y++);
    h || (h = {});
    var B = "undefined" != typeof uni ? uni : {};
    if (!B.navigateTo)
        for (var S in h) i(h, S) && (B[S] = h[S]);
    return B.webView = h, B
}));

src\hybrid\html\css\index.css

  * {
      margin: 0;
      padding: 0;
  }

  body {
      background-color: black;
  }

  .nav {
      display: flex;
      justify-content: space-between;
      width: 100%;
      height: 160px;
      align-items: center;
  }

  .back-icon,
  .switch-camera-icon {
      font-size: 64px;
      color: #fff;
  }

  .nav-title {
      font-size: 38px;
      color: #fff;
      font-weight: 700;
      flex: 1;
      text-align: center;
  }


  .container {
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
  }

  canvas {
      width: 100%;
  }

  video {
      /* margin-top: 20px; */
      /* width: 100%; */
      /* height: calc(100% - 300px); */
      /* height:200px; */
  }

  .btmBox {
      position: fixed;
      bottom: 0px;
      height: 300px;
      width: 100%;
      background-color: black;
  }

  #albumBtn {
      width: 100px;
      height: 100px;
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      left: 10%;
  }

  #finishBtn {
      width: 160px;
      height: 80px;
      padding: 0;
      background-color: #05b23b;
      border: none;
      border-radius: 4px;
      color: #fff;
      font-size: 32px;
      line-height: 80px;
      text-align: center;
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      right: 10%;
  }

  #capture {
      font-size: 28px;
      width: 180px;
      height: 180px;
      border-radius: 50%;
      line-height: 50px;
      text-align: center;
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
  }

  .mb {}

  .bg-img {
      position: absolute;
      top: 0;
      width: 100%;
      height: calc(100% - 300px);
      z-index: 99;
  }

  .loading {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.7);
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    z-index: 1000;
    display: none; /* 默认隐藏 */
}

.loading-spinner {
    border: 8px solid #f3f3f3;
    border-top: 8px solid #3498db;
    border-radius: 50%;
    width: 80px;
    height: 80px;
    animation: spin 1s linear infinite;
}

.loading-text {
    margin-top: 26px;
    color: #fff;
    font-size: 32px;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

父组件中使用:

<template>
  <view>
    <web-view
      ref="webviewRef"
      id="webview"
      :src="webViewUrl"
      @message="handleMessage"></web-view>
  </view>
</template>

<script setup>
  import {
    onMounted,
    getCurrentInstance,
    ref,
    reactive,
    nextTick,
    computed,
    watch,
  } from "vue";
  import { useUserStore, useAppStore } from "@/yaoli/store";
  import api from "@/api/factory/appearance";
  import { apiUpload } from "@/api/public";
  import { netConfig } from "@/common/config";
  import { pathToBase64, base64ToPath } from "image-tools";
  import { onLoad } from "@dcloudio/uni-app";
  const emits = defineEmits(["close", "success", "update:isShow"]);
  const webviewRef = ref();
  const props = defineProps({
    id: {
      type: Number,
      default: 0,
    },
    maxUploadCount: {
      type: Number,
    },
    titleList: {
      type: Array,
      default: () => [],
    },
    title: {
      type: String,
    },
  });
  const mainRef = ref();
  const { proxy, ctx } = getCurrentInstance();
  const { $util } = proxy;
  const { $dict } = proxy;
  const userStore = useUserStore();
  const scrollRef = ref(); // 滚动条Ref
  const appStore = useAppStore();
  const parmas = appStore.getParmas();
  // 自定义相机类型 可根据此参数更改相机样式与业务逻辑
  const dataType = ref("");
  let uploadCount = ref(null);
  // 上传结果
  let uploadResult = ref("");
  // web-view地址
  const webViewUrl = computed(() => {
    return `/hybrid/html/index.html?type=${dataType}&title=${props.title}&titleList=${props.titleList}&index=${uploadCount.value}&topHight=${topHight}&result=${uploadResult.value}`;
  });
  const handleMessage = ({ detail }) => {
    let action = detail?.data[0].action;
    if (action == "getImageFile") {
      saveImage(detail);
    }
    if (action == "getImagePath") {
      let data = detail?.data[0] || {};
      uploadImage(data.path);
    }
    if (action == "close") {
      onClose();
    }
  };
  // 上级路由信息
  let lastRoute = {};
  const onClose = () => {
    emits("close", false);
  };
  let uploadImageData = {};
  // base64格式图片转file格式并保存到本地
  const saveImage = (detail) => {
    let data = detail?.data[0] || {};
    base64ToPath(data.imageFile)
      .then((path) => {
        // 上传图片
        uploadImage(path);
      })
      .catch((err) => {
        uni.$showMsg("上传失败,稍后重试");
        uploadResult.value = JSON.stringify({
          code: 500,
          random: Math.random(), // 添加随机数引起url变化触发web-view侦听
        });
      });
  };
  const uploadImage = (filePath) => {
    //本地图片上传默认配置
    let defaultOption2 = {
      url: netConfig.baseURL + "files/file/upload",
      filePath: filePath, //图片路径
      name: "file", // 服务器端接受文件的参数名
      header: {},
      success: (res) => {
        console.log("上传成功", res);
        let result = JSON.parse(res.data);
        console.log(uploadCount.value, "uploadCount.value");
        if (result.code == 200) {
          uni.$showMsg("上传成功");
          emits("success", res, uploadCount.value);
          // setTimeout(() => {
          // 判断是否限制拍照数量,超过数量退出
          if (props.maxUploadCount) {
            ++uploadCount.value;
            if (uploadCount.value >= props.maxUploadCount) {
              onClose();
            }
          }
          // }, 1000);
        } else {
          uni.$showMsg(result.message || "上传失败,稍后重试");
        }
        uploadResult.value = JSON.stringify({
          ...result,
          random: Math.random(), // 添加随机数引起url变化触发web-view侦听
        });
        // 向web-view传参
        // setTimeout(() => {
        //   let resultString = JSON.stringify({ uploadCount.value, data: result });
        //   // embed.evalJS(`uniMsg({uploadCount:${uploadCount.value},data:${result}})`);
        //   embed.evalJS(`uniMsg(${resultString})`);
        // }, 200);
        // // webviewRef.value.postMessage({result})
      },
      fail: (err) => {
        console.error("上传失败:", err);
        uni.$showMsg("上传失败,稍后重试");
        uploadResult.value = JSON.stringify({
          code: 500,
          random: Math.random(), // 添加随机数引起url变化触发web-view侦听
        });
      },
    };
    uni.uploadFile(defaultOption2);
  };
  // 获取顶部安全区域
  let app = uni.getSystemInfoSync();
  let topHight = app.statusBarHeight;
  console.log("顶部安全距离", topHight); //44
  let wv = null;
  let embed = null;
  onLoad(() => {
    setTimeout(() => {
      //   // 注册web-view
      //   wv = plus.webview.currentWebview();
      //   // embed = plus.webview.getWebviewById(plus.runtime.appid);
      //   embed = plus.webview.create(
      //     `/hybrid/html/index.html?type=${dataType}&title=${props.title}&titleList=${props.titleList}&index=${props.id}&topHight=${topHight}`,
      //     "",
      //     { top: "0px", bottom: "0px" }
      //   );
      //   embed.addEventListener("message", handleMessage);
      //   wv.append(embed);
      //   console.log(wv, "wv");
    }, 200);
  });
  const show = () => {
    // console.log("show");
    // plus.webview.show(embed);
  };
  watch(
    () => props.id,
    () => {
      uploadCount.value = props.id;
    },
    { immediate: true }
  );
  defineExpose({
    show,
  });
</script>

<style lang="scss" scoped>
  @import "./index.scss";
</style>

参考文档:
https://uniapp.dcloud.net.cn/component/web-view.html#web-view
https://www.html5plus.org/doc/zh_cn/nativeobj.html

UniApp 开发中,如果你想创建一个自定义相机,可以使用 `plus.camera` 模块提供的功能,并结合 CSS 和 JavaScript 来实现。以下是一个简单的例子,展示如何创建一个带圆形相机界面: 1. 首先,在你的项目中安装 `uni-camera-plugin` 插件,如果还没有安装,可以在 `uni-app.config.js` 中添加: ```javascript plugin: { 'uni-camera': {}, }, ``` 2. 然后在你需要的页面中导入并使用 `Camera` 组件: ```html <template> <view class="camera-container"> <plus-camera :style="{width: cameraWidth + 'px', height: cameraHeight + 'px'}" :options="cameraOptions" capture-success="onCaptureSuccess"></plus-camera> <canvas class="mask-canvas" style="position:absolute; width: {{ maskWidth }}px; height: {{ maskWidth }}px; border-radius: 50%; background-color: rgba(0, 0, 0, 0.5); z-index: -1;"></canvas> </view> </template> <script setup> import { ref } from 'vue'; import plus from '@/uni-app/plugins/plus'; // 定义相机尺寸变量 const cameraWidth = 300; const cameraHeight = 300; const maskWidth = Math.min(cameraWidth, cameraHeight); // 圆形大小 const cameraOptions = { camera: Camera.CameraType.back, toPhotoLibrary: true, // 是否自动保存到相册 }; async function onCaptureSuccess(imageInfo) { // 图片拍摄成功后的回调,你可以在这里对图片进行进一步处理 const imgBase64 = imageInfo.tempFilePath; // 获取临时文件路径 //... } </script> <style scoped> .camera-container { position: relative; overflow: hidden; } </style> ``` 在这个例子中,我们创建了一个圆形,当拍照成功时,会触发 `onCaptureSuccess` 回调函数。你可以根据需求调整样式、位置以及捕捉的照片是否自动保存等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值