vue 实现人脸识别活体检测

<template>
  <div class="face-verify-approve-index">
    <div class="loading-wrap" v-if="loading">
      <div class="loading">
        <img src="@/assets/img/setting/loading.png" alt=""/>
        <p>{{ $t("approveText.text7") }}</p>
      </div>
    </div>
    <Header1 class="header" headerId="35"></Header1>
    <section class="all-header1-index">
      <div class="page">
        <p class="title" v-if="currentTaskId">{{ $t("faceVerify.tasks" + currentTaskId) }}</p>
        <div class="video-page">
          <van-circle
            v-model="currentRate"
            size="232"
            :stroke-width="30"
            :rate="rate"
            :color="gradientColor"
            layer-color="#F1F1F6"
            class="circle-container"
          >
            <video class="video" id="video" playsinline autoplay x5-video-player-type="h5"></video>
            <!-- <video ref="videoElement" id="video" class="video" autoplay></video> -->
          </van-circle>
          <!-- <input
              id="imgFile"
              class="imgFile"
              type="file"
              accept="video/*"
              capture="camcorder"
              @change="uploadVideo()"
          /> -->

          <!-- 检测结果 -->
          <p class="text" v-if="notFace">{{ notFaceText }}</p>
        </div>
      </div>
    </section>
    <VerifyResultDialog @showDialog="showDialog" :isShowDialog="isShowDialog" :type="type"
                        :url="url"></VerifyResultDialog>
  </div>
</template>
<script>
import {mapState} from "vuex";
import {generateRandomNumber} from "@/utils/index";
import COS from "cos-js-sdk-v5";
import md5 from "@/service/md5.js";
import * as faceapi from "face-api.js";

export default {
  data() {
    return {
      verifyType: "", //认证类型 1为认证 2为校验
      url: "",
      generateRandomNumber,
      md5,
      currentRate: 0,
      rate: 100,
      gradientColor: {
        "0%": "#95B8FA",
        "100%": "#3B80FF",
      },
      timer: null,
      currentTaskId: null,
      selectedTasks: [],
      notFace: false,
      notFaceText: "",
      tasks: [
        //1左右摇头 2点头 3眨眼
        {id: 1, type: false},
        {id: 2, type: false},
        {id: 3, type: false},
      ],
      tasks4: {id: 4, type: false},
      stillTimer: null,
      stillTimeThreshold: 3000, // 3秒
      cos: null, // COS实例
      file: null, // 选择的文件
      imageUrl: "", // 上传后的图片URL
      isShowDialog: false, //人脸认证结果弹框
      type: 3, //认证失败弹框是否可以重新认证 1,不可以 2,可以 ,3,认证成功 4,显示去认证弹框
      countdownTimer: null, // 用于倒计时的计时器
      loading: false,
      nodCounter: 0, // 点头计数器
      nodThreshold: 2, // 点头动作的阈值
      isStill: false, // 是否保持不动
      lastPitch: 0, // 上一次的头部倾斜角度
      shakeCounter: 0, // 摇头计数器
      shakeThreshold: 2, // 摇头动作的阈值
      isStillshake: false, // 摇头是否保持不动
      shakelastPitch: 0, // 摇头上一次的头部倾斜角度
      isFrontCamera: true,
      videoStream: null,
    };
  },
  components: {
    Header1: () => import("@/components/header/header1.vue"),
    VerifyResultDialog: () => import("@/components/faceVerify/verifyResult-dialog.vue"),
  },
  mounted() {
    this.verifyType = this.$route.query.type;
    this.url = this.$route.query.url;
    this.startTasks(); // 初始化COS实例
  },
  computed: {
    ...mapState(["lang", "userInfo"]),
  },
  methods: {
    // 关于navigator.mediaDevices.getUserMedia兼容问题
    // 有一部分老的浏览器不能兼容navigator.mediaDevices.getUserMedia来获取麦克风和摄像头,可用如下来获取
    getAudio() {
      if (
        navigator.mediaDevices.getUserMedia ||
        navigator.getUserMedia ||
        navigator.webkitGetUserMedia ||
        navigator.mozGetUserMedia
      ) {
        this.getUserMediaFun({video: true}); // 调用用户媒体设备,访问摄像头、录音、
      } else {
        console.log("你的浏览器不支持访问用户媒体设备");
      }
    },
    getUserMediaFun(constrains) {
      let that = this;
      if (navigator.mediaDevices.getUserMedia) {
        // 最新标准API、
        navigator.mediaDevices
          .getUserMedia(constrains)
          .then((stream) => {
            that.success(stream);
          })
          .catch((err) => {
            that.error(err);
          });
      } else if (navigator.webkitGetUserMedia || navigator.mozGetUserMedia) {
        // webkit内核浏览器
        if (navigator.mediaDevices === undefined) {
          navigator.mediaDevices = {};
        }

        // 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia
        // 因为这样可能会覆盖已有的属性。这里我们只会在没有getUserMedia属性的时候添加它。
        if (navigator.mediaDevices.getUserMedia === undefined) {
          navigator.mediaDevices.getUserMedia = function (constraints) {
            // 首先,如果有getUserMedia的话,就获得它
            var getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

            // 一些浏览器根本没实现它 - 那么就返回一个error到promise的reject来保持一个统一的接口
            if (!getUserMedia) {
              return Promise.reject(new Error("getUserMedia is not implemented in this browser"));
            }

            // 否则,为老的navigator.getUserMedia方法包裹一个Promise
            return new Promise(function (resolve, reject) {
              getUserMedia.call(navigator, constraints, resolve, reject);
            });
          };
        }
        navigator.mediaDevices
          .getUserMedia(constrains)
          .then((stream) => {
            that.success(stream);
          })
          .catch((err) => {
            that.error(err);
          });
      } else if (navigator.getUserMedia) {
        // 旧版API
        navigator
          .getUserMedia(constrains)
          .then((stream) => {
            that.success(stream);
          })
          .catch((err) => {
            that.error(err);
          });
      }
    },
    // 成功的回调函数
    success(stream) {
      const videoElement = document.getElementById("video"); // 获取视频元素
      this.videoStream = stream; // 保存视频流,以便在组件销毁时停止摄像头
      videoElement.srcObject = stream;
      // 如果是前置摄像头,添加翻转类
      videoElement.classList.add("flipped");
    },
    // 异常的回调函数
    error(error) {
      console.log("访问用户媒体设备失败:", error.name, error.message);
    },

    startTasks() {
      // 随机选择两个任务
      this.selectedTasks = this.shuffleArray(this.tasks).slice(0, 2);
      this.completeTask();
    },
    shuffleArray(array) {
      // 随机打乱数组顺序
      for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
      }
      return array;
    },
    completeTask() {
      // console.log("循环了", this.timer);
      // 如果当前任务是保持不动的任务,完成后直接调用接口
      // console.log(this.selectedTasks, "剩余任务");
      if (!this.timer) {
        this.startCamera()
          .then(() => {
            this.timer = window.setInterval(this.detectFace, 1000);
          })
          .catch((err) => {
            console.log("startCamera函数报错了", err);
          });
      }
      const allTasksCompleted = this.selectedTasks.every((task) => task.type === true);
      // console.log("完成了所有的任务:", allTasksCompleted);
      if (allTasksCompleted) {
        //随机两项全部通过,开始最后静止不动3s
        this.currentTaskId = 4;
        if (this.timer) {
          clearInterval(this.timer);
          this.timer = null; // Add this line to set timer to null
        }
      } else {
        if (!this.selectedTasks[0].type) {
          this.currentTaskId = this.selectedTasks[0].id;
        } else if (!this.selectedTasks[1].type) {
          this.currentTaskId = this.selectedTasks[1].id;
        } else if (!this.selectedTasks[2].type) {
          this.currentTaskId = this.selectedTasks[2].id;
        } else {
          //全部通过
          this.currentTaskId = null;
          if (this.timer) {
            clearInterval(this.timer);
            this.timer = null; // Add this line to set timer to null
          }
        }
      }
    },
    // 在 async 函数中初始化摄像头和 face-api.js
    async startCamera() {
      // try {
      //     const stream = await navigator.mediaDevices.getUserMedia({ video: true });
      //     this.videoStream = stream; // 保存视频流,以便在组件销毁时停止摄像头
      //     this.$refs.videoElement.srcObject = stream;
      //     console.log(this.$refs.videoElement.srcObject);
      // } catch (error) {
      //     console.error("Error starting camera:", error);
      // }
      this.getAudio();
      // 初始化 face-api.js 模型
      await Promise.all([
        faceapi.nets.tinyFaceDetector.loadFromUri(`${process.env.BASE_URL}/models`),
        faceapi.nets.faceLandmark68Net.loadFromUri(`${process.env.BASE_URL}/models`),
      ]);
    },
    // 开始实时捕获
    async detectFace() {
      const video = document.getElementById("video");
      if (!video) {
        return; // 视频元素不存在,直接返回,避免报错
      }
      const detections = await faceapi
        .detectSingleFace(video, new faceapi.TinyFaceDetectorOptions({inputSize: 128}))
        .withFaceLandmarks();
      // const detections = await faceapi.detectSingleFace(video, new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks();
      if (detections) {
        // 如果检测到人脸
        this.notFace = false; // 重置未检测到人脸的标志
        this.notFaceText = ""; // 清空提示文本
        if (this.currentTaskId) {
          // 如果当前有任务正在进行
          if (this.currentTaskId === 1) {
            this.shakeHeadfunc(detections);
          } else if (this.currentTaskId === 2) {
            this.nodHeadfunc(detections);
          } else if (this.currentTaskId === 3) {
            this.winkfunc(detections);
          } else if (this.currentTaskId === 4) {
            // 静止不动任务
            if (!this.stillTimer) {
              this.stillTimer = setTimeout(() => {
                this.stillTimer = null;
                this.captureStillImage();
              }, this.stillTimeThreshold);
            }
            if (!this.countdownTimer) {
              this.countdownTimer = setTimeout(() => {
                this.currentTaskId = null; // 完成所有任务,重置当前任务
                this.countdownTimer = null; // 清除计时器
              }, this.stillTimeThreshold);
            }
          }
        } else {
          // 没有任务正在进行,进行下一个任务
          this.completeTask();
        }
      } else {
        // 没有检测到人脸
        this.notFace = true;
        this.notFaceText = this.$t("approveText.text8");
      }
    },
    //循环任务摇头中的处理
    shakeHeadfunc(detections) {
      console.log("摇头");
      if (detections) {
        const landmarks = detections.landmarks;
        const leftEye = landmarks.getLeftEye()[0];
        const rightEye = landmarks.getRightEye()[3];
        const pitch = this.calculateYaw(leftEye, rightEye);
        // 判断用户是否摇头
        if (Math.abs(pitch - this.shakelastPitch) < 1) {
          console.log("用户保持不动");
        } else {
          if (this.isShaking(pitch)) {
            this.shakeCounter++;
            if (this.shakeCounter >= this.shakeThreshold) {
              this.shakeCounter = 0;
              this.notFace = false;
              this.notFaceText = this.$t("approveText.text9");
              this.selectedTasks = this.selectedTasks.filter((task) => task.id !== this.currentTaskId);
            }
          } else {
            this.shakeCounter = 0;
            this.notFace = true;
            this.notFaceText = this.$t("approveText.text10");
          }
        }
        setTimeout(() => {
          this.completeTask();
        }, 1000);
        this.shakelastPitch = pitch;
      }
    },
    //循环点头中的处理
    nodHeadfunc(detections) {
      console.log("点头");
      if (detections) {
        const landmarks = detections.landmarks;
        // const nose = landmarks.getNose();
        const leftEye = landmarks.getLeftEye();
        const rightEye = landmarks.getRightEye();
        // 计算头部倾斜角度
        const pitch = this.calculatePitch(leftEye, rightEye);
        // 判断点头动作
        if (Math.abs(pitch - this.lastPitch) < 1) {
          console.log("用户保持不动");
        } else {
          if (this.isNodding(pitch)) {
            this.nodCounter++;
            if (this.nodCounter >= this.nodThreshold) {
              this.nodCounter = 0;
              this.notFace = false;
              this.notFaceText = this.$t("approveText.text11");
              this.selectedTasks = this.selectedTasks.filter((task) => task.id !== this.currentTaskId);
            }
          } else {
            this.nodCounter = 0;
            this.notFace = true;
            this.notFaceText = this.$t("approveText.text12");
          }
        }
        setTimeout(() => {
          this.completeTask();
        }, 1000);
        this.lastPitch = pitch;
      }
    },
    //眨眼中的处理
    winkfunc(detections) {
      console.log("眨眼");
      if (detections) {
        const landmarks = detections.landmarks;
        const leftEye = landmarks.getLeftEye();
        const rightEye = landmarks.getRightEye();
        // 计算左眼和右眼的闭合程度
        const leftEyeOpenness = this.calculateEyeOpenness(leftEye);
        const rightEyeOpenness = this.calculateEyeOpenness(rightEye);
        // 判断是否眨眼(根据闭合程度)
        const isBlinking = leftEyeOpenness < 10 && rightEyeOpenness < 10;
        if (isBlinking) {
          this.notFace = false;
          this.notFaceText = this.$t("approveText.text13");
          //删除任务数组对应项
          this.selectedTasks = this.selectedTasks.filter((task) => task.id !== this.currentTaskId);
        } else {
          this.notFace = true;
          this.notFaceText = this.$t("approveText.text14");
        }
        setTimeout(() => {
          this.completeTask();
        }, 1000);
      }
    },
    //摇头计算方法
    calculateYaw(leftEye, rightEye) {
      const deltaX = rightEye.x - leftEye.x;
      const deltaY = rightEye.y - leftEye.y;
      const angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
      return angle;
    },
    //判断摇头动作
    isShaking(pitch) {
      const mediumRange = {min: -10, max: 10}; // 范围可根据实际情况调整
      return pitch >= mediumRange.min && pitch <= mediumRange.max;
    },
    //点头计算方法
    calculatePitch(leftEye, rightEye) {
      // 在这里计算头部倾斜角度
      const leftEyeCenter = {
        x: (leftEye[0].x + leftEye[3].x) / 2,
        y: (leftEye[0].y + leftEye[3].y) / 2,
      };
      const rightEyeCenter = {
        x: (rightEye[0].x + rightEye[3].x) / 2,
        y: (rightEye[0].y + rightEye[3].y) / 2,
      };

      const deltaX = rightEyeCenter.x - leftEyeCenter.x;
      const deltaY = rightEyeCenter.y - leftEyeCenter.y;
      const radians = Math.atan2(deltaY, deltaX);
      // 将弧度转换为角度
      const degrees = radians * (180 / Math.PI);
      return degrees;
    },
    //判断点头动作
    isNodding(pitch) {
      // console.log("角度:", pitch);
      // 在这里判断点头动作
      // 根据头部倾斜角度进行判断
      // 返回 true 或 false
      const mediumRange = {min: -10, max: 10}; // 范围可根据实际情况调整
      return pitch >= mediumRange.min && pitch <= mediumRange.max;
    },
    //眨眼计算方法
    calculateEyeOpenness(eyeLandmarks) {
      // 计算眼睛的闭合程度
      const eyeTop = eyeLandmarks[1].y;
      const eyeBottom = eyeLandmarks[5].y;
      const eyeHeight = eyeBottom - eyeTop;
      // console.log(eyeHeight, "eyeHeight");
      const eyeOpenness = eyeHeight / 3; // 根据实际情况进行调整
      return eyeOpenness;
    },
    // 保存当前帧作为图像
    async captureStillImage() {
      if (!this.imageUrl) {
        const video = document.getElementById("video");
        const canvas = document.createElement("canvas");
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;
        const context = canvas.getContext("2d");
        context.drawImage(video, 0, 0, canvas.width, canvas.height);
        const imgDataUrl = canvas.toDataURL("image/jpeg");
        // 现在你可以将imgDataUrl用作捕获的静态图像。
        // console.log("捕获的图像数据URL:", imgDataUrl);
        // 如果需要,你还可以在界面上显示捕获的图像。
        // 例如,你可以创建一个<img>元素,并将其src属性设置为imgDataUrl。
        this.getOssObject(imgDataUrl);
      }
    },
    //调后台接口获取腾讯云上传临时参数
    getOssObject(imgDataUrl) {
      this.loading = true;
      this.$api
        .tencentOss({})
        .then((res) => {
          this.cos = new COS({
            SecretId: res.data.cred.Credentials.TmpSecretId,
            SecretKey: res.data.cred.Credentials.TmpSecretKey,
            SecurityToken: res.data.cred.Credentials.Token,
          });
          // 上传文件到腾讯云对象存储
          this.cos.putObject(
            {
              Bucket: res.data.bucket,
              Region: res.data.region,
              Key: `/resource/being_dev/user_face/${this.userInfo.user_info.uid}_${Date.now()}.jpg`, // 设置文件在COS中的路径
              Body: this.dataURLtoBlob(imgDataUrl),
              // ContentLength: base64Image.length,
            },
            (err, data) => {
              if (err) {
                console.error("Upload error:", err);
              } else {
                this.imageUrl = data.Location; // 获取上传后的图片URL
                if (this.verifyType == "1") {
                  this.personalBindorCheck(this.imageUrl, "1");
                } else if (this.verifyType == "2") {
                  this.personalBindorCheck(this.imageUrl, "2");
                }
              }
            }
          );
        })
        .catch((err) => {
          clearInterval(this.timer);
          console.log(err);
        });
    },
    // 替换域名方法
    replaceDomainWithCom(originalUrl, newDomain) {
      const regex = /^(https?:\/\/)?(www\.)?[^\/]+/;
      const replacedUrl = originalUrl.replace(regex, newDomain);
      return replacedUrl;
    },
    // 人脸绑定/校验接口
    personalBindorCheck(imageUrl, val) {
      const newDomain = 'https://b1.being.com';
      const replacedUrl = this.replaceDomainWithCom(imageUrl, newDomain); //处理后的新的url
      const currentDate = new Date();
      currentDate.setHours(0, 0, 0, 0); // 设置时分秒为0
      const currentDateTimestamp = Math.floor(currentDate.getTime() / 1000); // 转换为秒级时间戳
      let ramdomCount = this.generateRandomNumber(32);
      let deviceModel = `${this.userInfo.user_info.uid}-${currentDateTimestamp}-${this.userInfo.user_info.uid}-${currentDateTimestamp}`;
      let md5Count = `${replacedUrl}${ramdomCount.slice(-5)}`;
      let md5param = md5.hexMD5(md5Count);
      let actionType = "";
      if (val == "1") {
        actionType = "personnel_bind";
      } else if (val == "2") {
        actionType = "face_verify";
      }
      let params = {
        url: replacedUrl, // 图
        device: deviceModel, // 设备号,
        rand_str: ramdomCount, //随机数
        md5: md5param, //加密数
        action: actionType, //方法名
      };
      let apiMethod = null;
      if (val === "1") {
        apiMethod = this.$api.personnelBind;
      } else if (val === "2") {
        apiMethod = this.$api.faceVerify;
      }
      apiMethod(params)
        .then((res) => {
          this.loading = false;
          this.isShowDialog = true;
          if (!res.error_code) {
            if (val === "1") {
              this.type = 3;
            } else if (val === "2") {
              this.type = 4;
            }
          } else {
            this.type = res.error_code;
          }
        })
        .catch((err) => {
          this.loading = false;
          console.log(err);
        });
    },
    // 去掉Base64头部
    dataURLtoBlob(dataurl) {
      var arr = dataurl.split(",");
      var mime = arr[0].match(/:(.*?);/)[1];
      var bstr = atob(arr[1]);
      var n = bstr.length;
      var u8arr = new Uint8Array(n);
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return new Blob([u8arr], {type: mime});
    },
    showDialog() {
      this.isShowDialog = false;
    },
  },
  beforeDestroy() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null; // Add this line to set timer to null
    }
    if (this.stillTimer) {
      clearTimeout(this.stillTimer);
      this.stillTimer = null;
    }
    if (this.countdownTimer) {
      clearTimeout(this.countdownTimer);
      this.countdownTimer = null;
    }
    if (this.videoStream) {
      this.videoStream.getTracks().forEach((track) => track.stop());
    }
  },
};
</script>

<style>
/* 在这里可以添加样式 */
</style>

<style scoped lang="less">
.face-verify-approve-index {
  flex: 1;
  display: flex;
  flex-direction: column;
  background: #fff;
  position: relative;

  .loading-wrap {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: #d9d9d9;
    z-index: 99999;
    display: flex; /* 使用Flex布局 */
    justify-content: center; /* 水平居中 */
    align-items: center; /* 垂直居中 */

    .loading {
      width: 1.26rem;
      height: 1.26rem;
      background: #ffffff;
      border-radius: 0.12rem;
      z-index: 99999;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;

      & > img {
        width: 0.44rem;
        height: 0.44rem;
        animation: rotate 1s linear infinite;
      }

      @keyframes rotate {
        from {
          transform: rotate(0deg);
        }
        to {
          transform: rotate(360deg);
        }
      }

      & > p {
        font-size: 0.16rem;
        font-weight: bold;
        color: #000000;
        margin-top: 0.1rem;
      }
    }
  }

  .header {
    background-color: #fff;
    border-bottom: 1px solid #f6f7f8;
    color: #000;
  }

  section {
    flex: 1;
    display: flex;
    flex-direction: column;
    padding: 0.4rem 0.2rem;
    box-sizing: border-box;

    .page {
      width: 100%;
      display: flex;
      flex-direction: column;
      align-items: center;

      .title {
        font-size: 0.24rem;
        font-weight: bold;
        color: #000000;
        line-height: 0.22rem;
        text-align: center;
      }

      .video-page {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        position: relative;
        margin-top: 0.24rem;
        width: 232px;
        height: 232px;
        overflow: hidden;
        border-radius: 232px;

        .circle-container {
          width: 232px;
          height: 232px;
          display: flex;
          align-items: center;
          justify-content: center;
          position: relative;
          overflow: hidden;
        }

        /* 使用深度选择器,确保样式作用到 .circle-container 内的 svg 元素 */

        /deep/ .circle-container > svg {
          z-index: 11 !important;
        }

        video {
          width: 215px;
          height: 215px;
          border-radius: 50%;
          // position: absolute;
          // top: 50%;
          // left: 50%;
          // transform: translate(-50%, -50%);
          overflow: hidden;
          display: flex;
          align-items: center;
          justify-content: center;
          object-fit: cover; /* 保持视频比例,填满容器 */
          // z-index: 99;
        }

        .flipped {
          width: 215px;
          height: 215px;
          border-radius: 50%;
          // position: absolute;
          // top: 50%;
          // left: 50%;
          transform: translate(-50%, -50%);
          overflow: hidden;
          display: flex;
          align-items: center;
          justify-content: center;
          object-fit: cover; /* 保持视频比例,填满容器 */
          transform: scaleX(-1);
        }

        .text {
          position: absolute;
          bottom: 0.07rem;
          left: 50%;
          transform: translate(-50%, 0);
          width: 215px;
          height: 68px;
          background: rgba(0, 0, 0, 0.5);
          font-size: 0.16rem;
          color: #fff;
          line-height: 0.18rem;
          text-align: center;
          padding: 0.16rem 0.35rem;
          box-sizing: border-box;
          // z-index: 999;
        }
      }
    }
  }
}
</style>

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值