pengwei
2025-04-14 3e60db98fdf6c5b59768ffc81576da3679fafbea
ÏîÄ¿´úÂë/client/src/views/Login.vue
@@ -105,34 +105,91 @@
                  cursor: pointer;
                  font-size: 1rem;
                "
                @click="show = false"
                @click="recognition"
                >人脸识别登录</span
              >
            </div>
          </el-form-item>
        </el-form>
        <div class="face-login" v-else>
          <span style="font-size: 0.88rem; font-weight: bold; color: #333333"
          <span
            style="
              text-align: center;
              font-size: 0.88rem;
              font-weight: bold;
              color: #333333;
            "
            >请将脸部正对蓝色显示框内,并保持光线充足</span
          >
          <div
            style="
              display: flex;
              justify-content: center;
              align-items: center;
              border: 1px solid #4386ff;
              border-radius: 50%;
              width: 18.75rem;
              height: 18.75rem;
              margin: 2.06rem 0;
              background-color: #f1fcff;
            "
          >
            <img src="@/assets/login/face.png" alt="" />
          <div style="width: 100%; display: flex; justify-content: center">
            <div
              style="
                display: flex;
                justify-content: center;
                align-items: center;
                border: 1px solid #4386ff;
                border-radius: 50%;
                width: 18.75rem;
                height: 18.75rem;
                margin: 2.06rem 0;
                background-color: #f1fcff;
              "
            >
              <img v-if="!face" src="@/assets/login/face.png" alt="" />
              <!-- <canvas v-else ref="canvasDom" /> -->
              <div v-else>
                <!-- æ’­æ”¾å™¨ï¼Œç”¨æ¥æ’­æ”¾æ‹æ‘„的视频 -->
                <video class="camera_video" ref="videoDom" />
                <!--  æ˜¾ç¤ºç…§ç‰‡  -->
                <!-- <img
                  style="border-radius: 50%; width: 18.75rem; height: 18.75rem"
                  v-else
                  :src="imgurl"
                /> -->
              </div>
            </div>
          </div>
          <el-button type="primary" size="small" style="width: 100%"
          <!-- <el-button
            v-if="!reBtn"
            type="primary"
            size="small"
            style="width: 100%"
            @click="takePhoto"
            >开始识别</el-button
          >
          <el-button
            v-else
            type="primary"
            size="small"
            style="width: 100%"
            @click="REtakePhoto"
            >重新识别</el-button
          > -->
          <div style="width: 100%; margin-top: 1rem; display: flex">
            <el-button
              type="primary"
              size="small"
              style="width: 100%"
              @click="recognition"
              >重新获取摄像头</el-button
            >
            <el-select
              v-if="videoArr.length > 1"
              v-model="constraints"
              placeholder="请选择摄像头"
              size="samll"
              @change="changeconstraints"
              style="height: 2rem"
              :disabled="!videoArr.length > 1"
            >
              <el-option
                v-for="item in videoArr"
                :key="item.id"
                :label="item.label"
                :value="item.id"
              />
            </el-select>
          </div>
          <div
            style="
              display: flex;
@@ -150,7 +207,7 @@
                cursor: pointer;
                font-size: 1rem;
              "
              @click="show = true"
              @click="accountlogin"
              >账号登录</span
            >
          </div>
@@ -161,11 +218,13 @@
</template>
<script setup>
import { getCodeImg, login } from "@/api/login";
import { getCodeImg, login, CleanUnusedImages } from "@/api/login";
import { useRouter, useRoute } from "vue-router";
import { getCurrentInstance, ref, nextTick, onMounted } from "vue";
import { ElMessage } from "element-plus";
import store from "@/store";
import axios from "axios";
const router = useRouter();
const route = useRoute();
@@ -176,7 +235,14 @@
const codeUrl = ref(""); // éªŒè¯ç 
const loading = ref(false); // ç™»å½•加载状态
const face = ref(false); // äººè„¸è¯†åˆ«ç™»å½•
// ç…§ç‰‡è·¯å¾„
const imgurl = ref(null);
// canvas控件对象
const canvasDom = ref(null);
// video æŽ§ä»¶å¯¹è±¡
const videoDom = ref(null);
const reBtn = ref(false);
const loginForm = ref({
  userName: "",
  password: "",
@@ -236,12 +302,237 @@
nextTick(() => {
  userNameRef.value.focus();
});
const videoArr = ref([]);
//选择的摄像头的id
const constraints = ref("");
const videoConstraints = ref({});
//人脸识别登录
const recognition = () => {
  show.value = false;
  videoArr.value = [];
  setTimeout(async () => {
    face.value = true;
    navigator.mediaDevices
      .enumerateDevices()
      .then((devices) => {
        devices.forEach(function (device) {
          if (device.kind == "videoinput") {
            videoArr.value.push({
              label: device.label,
              id: device.deviceId,
            });
            constraints.value = videoArr.value[0].id;
          }
        });
        openCamera();
      })
      .catch(function (err) {
        layer.msg(err.name + ": " + err.message);
      });
  }, 1000);
  ElMessage({
    message: "正在调用摄像头,请稍等...",
    type: "warning",
    plain: true,
    duration: 1000,
  });
  // äººè„¸è¯†åˆ«ç™»å½•逻辑
};
//账号登录
function accountlogin() {
  stop();
  ElMessage({
    message: "正在关闭摄像头,请稍等...",
    type: "warning",
    plain: true,
    duration: 2000,
  });
  setTimeout(() => {
    ElMessage({
      message: "关闭摄像头成功",
      type: "success",
      plain: true,
    });
  }, 3000);
  setTimeout(() => {
    show.value = true;
    face.value = false;
  }, 2000);
}
//打开摄像头
const openCamera = async () => {
  videoConstraints.value.deviceId = { exact: constraints.value };
  // æ£€æµ‹æµè§ˆå™¨æ˜¯å¦æ”¯æŒmediaDevices
  if (navigator.mediaDevices) {
    navigator.mediaDevices
      // å¼€å¯è§†é¢‘,关闭音频
      .getUserMedia({
        audio: false,
        video: {
          width: 300, // è®¾ç½®è§†é¢‘宽度
          height: 300, // è®¾ç½®è§†é¢‘高度
          facingMode: "user", // ä½¿ç”¨å‰ç½®æ‘„像头
          deviceId: videoConstraints.value.deviceId,
        },
      })
      .then((stream) => {
        // å°†è§†é¢‘流传入viedo控件
        videoDom.value.srcObject = stream;
        // æ’­æ”¾
        videoDom.value.play();
        Facerecognition();
        ElMessage({
          message: "摄像头调用成功",
          type: "success",
          plain: true,
        });
      })
      .catch((err) => {
        console.log(err);
      });
  } else {
    window.alert("该浏览器不支持开启摄像头,请更换最新版浏览器");
  }
};
const changeconstraints = (res) => {
  openCamera();
};
// å¼€å§‹è¯†åˆ«
const takePhoto = async () => {
  // å¦‚果已经拍照了就重新启动摄像头
  // if (imgurl.value) {
  //   imgurl.value = null;
  //   openCamera();
  //   return;
  // }
  console.log(videoDom.value.videoWidth, videoDom.value.videoHeight);
  // åˆ›å»ºä¸€ä¸ªç”»å¸ƒå…ƒç´ ï¼Œè®¾ç½®ç”»å¸ƒå°ºå¯¸ä¸ºè§†é¢‘流的尺寸
  const canvas = document.createElement("canvas");
  // è®¾ç½®ç”»å¸ƒå¤§å°ä¸Žæ‘„像大小一致
  canvas.width = videoDom.value.videoWidth;
  canvas.height = videoDom.value.videoHeight;
  // èŽ·å–ç”»å¸ƒä¸Šä¸‹æ–‡å¯¹è±¡
  const ctx = canvas.getContext("2d");
  // ç»˜åˆ¶å½“前视频帧到画布上
  ctx.drawImage(videoDom.value, 0, 0, canvas.width, canvas.height);
  // å°†ç”»å¸ƒå†…容转为 Base64 æ•°æ®
  const imageDataUrl = canvas.toDataURL("image/png");
  // å­˜å‚¨å›¾ç‰‡è·¯å¾„
  imgurl.value = imageDataUrl;
  // // åˆ›å»ºä¸€ä¸ªå›¾ç‰‡å…ƒç´ 
  // const imageElement = new Image();
  // // å°† Base64 æ•°æ®è®¾ç½®ä¸ºå›¾ç‰‡çš„ src å±žæ€§
  // imageElement.src = imageDataUrl;
  // console.log(imageElement, imageDataUrl, "图片路径");
  // return;
  // canvasDom.value.width = videoDom.value.videoWidth;
  // canvasDom.value.height = videoDom.value.videoHeight;
  // // æ‰§è¡Œç”»çš„æ“ä½œ
  // canvasDom.value.getContext("2d").drawImage(videoDom.value, 0, 0);
  // // å°†ç»“果转换为可展示的格式
  // imgurl.value = canvasDom.value.toDataURL("image/webp");
  // å…³é—­æ‘„像头
  let files = dataURLtoFile(imgurl.value, new Date().getTime() + ".png");
  const formdata = new FormData();
  formdata.append("files", files);
  let response = await axios.post("/api/User/SaveFiles", formdata, {
    headers: {
      "Content-Type": "multipart/form-data",
    },
  });
  ElMessage({
    message: "开始识别中,请稍等...",
    type: "warning",
    plain: true,
    duration: 2000,
  });
  setTimeout(() => {
    login({
      userName: "",
      password: "",
      path: response.data.data,
    })
      .then((res) => {
        if (res.status) {
          store.commit("setUserInfo", { ...res.data });
          ElMessage({
            message: "识别成功,开始登录",
            type: "success",
            plain: true,
            duration: 2000,
          });
          CleanUnusedImages();
          stop();
          setTimeout(() => {
            router.push({ path: "/" });
          }, 1000);
        }
      })
      .catch((err) => {
        loading.value = false;
        return proxy.$message.error(err.message);
      });
    setTimeout(() => {
      loading.value = false;
    }, 1000);
  }, 1000);
  reBtn.value = true;
};
//重新识别
// const REtakePhoto = () => {
//   reBtn.value = false;
//   takePhoto();
// };
// å°† base64 è½¬æ¢ä¸º Blob
const dataURLtoFile = (dataurl, filename) => {
  let arr = dataurl.split(","),
    mime = arr[0].match(/:(.*?);/)[1],
    bstr = atob(arr[1]),
    n = bstr.length,
    u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  return new File([u8arr], filename, {
    type: mime,
  });
};
// å…³é—­æ‘„像头
const stop = () => {
  let stream = videoDom.value.srcObject;
  if (!stream) return;
  let tracks = stream.getTracks();
  tracks.forEach((x) => {
    x.stop();
  });
  clearInterval(timer.value);
};
//获取验证码
getCode();
//定时人脸识别
const timer = ref(null);
const Facerecognition = () => {
  clearInterval(timer.value);
  if (!show.value) {
    timer.value = setInterval(() => {
      takePhoto();
    }, 3000);
  }
};
</script>
<style lang="less" scoped>
// * {
//   box-sizing: border-box;
//   padding: 0;
//   margin: 0;
// }
.login-container {
  width: 100%;
  min-height: 100vh;
@@ -261,7 +552,7 @@
  }
  .login-box {
    width: 100%;
    height: ceil(100vh - 8rem);
    height: calc(100vh - 4.57rem);
    display: flex;
    background-color: #f6f7fc;
    .left-img {
@@ -296,9 +587,19 @@
      }
    }
    .face-login {
      width: 30rem;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-content: center;
      text-align: center;
    }
  }
}
.camera_video {
  width: 18.75rem;
  height: 18.75rem;
  border: 1px black solid;
  border-radius: 50% 50%;
}
</style>