Vue3 + Electron 实现纯本地人脸识别登录一体机(离线可用、无云端、带页面跳转)

前言

在本文中,我将手把手教你使用 Vue3 + Electron 实现一套纯本地、离线运行、不上云的人脸识别登录系统。功能包含:

  • 开机直接进入人脸识别登录页
  • 人脸登录成功 → 进入首页
  • 人脸不匹配 → 本页显示失败
  • 登录页可跳转到人脸录入页
  • 录入成功自动返回登录
  • 全程本地运行,无需网络

技术栈

  • Vue3 + VueRouter
  • Electron(打包桌面端 + 调用摄像头)
  • @vladmandic/face-api(本地人脸识别库)
  • 模型本地加载,100% 离线运行

一、项目初始化

1. 创建 Vue3 项目

复制代码
vue create electron-face-local
cd electron-face-local
  1. 安装依赖

    npm install electron electron-builder vue-router@4
    npm install @vladmandic/face-api

3. 创建 Electron 入口文件

根目录新建:electron-main.js

复制代码
const { app, BrowserWindow } = require('electron')

let win
function createWindow() {
  win = new BrowserWindow({
    width: 1280,
    height: 720,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
    }
  })
  // 开发环境加载 Vue 服务
  win.loadURL('http://localhost:8080')
}

app.whenReady().then(createWindow)
app.on('window-all-closed', () => app.quit())
  1. package.json 配置

    "main": "electron-main.js",
    "scripts": {
    "serve": "vue-cli-service serve",
    "electron": "electron .",
    "build": "vue-cli-service build",
    "electron:build": "vue-cli-service build && electron-builder"
    }

二、配置路由(实现页面跳转)

src/router/index.js

复制代码
import { createRouter, createWebHashHistory } from 'vue-router'
import Login from '../views/Login.vue'
import Register from '../views/Register.vue'
import Home from '../views/Home.vue'

const routes = [
  { path: '/', redirect: '/login' },
  { path: '/login', component: Login },
  { path: '/register', component: Register },
  { path: '/home', component: Home }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

src/main.js

复制代码
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')

App.vue

复制代码
<template>
  <router-view></router-view>
</template>

三、放入本地人脸识别模型

从官方下载模型,全部放入 public/models/ 目录https://github.com/justadudewhohacks/face-api.js/tree/master/weights

复制代码
public/models/
   所有 .bin / .shaperes 文件

四、核心页面代码(直接复制)

1. 人脸识别登录页(src/views/Login.vue)

复制代码
<template>
  <div class="page">
    <h2>人脸识别登录</h2>
    <div class="box">
      <video ref="video" autoplay muted></video>
      <canvas ref="canvas"></canvas>
    </div>
    <div class="status" :style="{color:statusColor}">{{ statusText }}</div>
    <button @click="goToRegister" class="btn">去录入人脸</button>
  </div>
</template>

<script>
import * as faceapi from '@vladmandic/face-api'
export default {
  data() {
    return {
      statusText: '加载模型中...',
      statusColor: '#42b983',
      localFace: null
    }
  },
  mounted() {
    this.loadFace()
    this.start()
  },
  methods: {
    loadFace() {
      const data = localStorage.getItem('faceUser')
      if (!data) return
      const user = JSON.parse(data)
      const desc = new Float32Array(user.descriptors[0])
      this.localFace = new faceapi.LabeledFaceDescriptors(user.label, [desc])
    },
    async start() {
      await faceapi.loadSsdMobilenetv1Model('/models')
      await faceapi.loadFaceLandmarkModel('/models')
      await faceapi.loadFaceRecognitionModel('/models')
      this.startCamera()
    },
    async startCamera() {
      const stream = await navigator.mediaDevices.getUserMedia({ video: true })
      this.$refs.video.srcObject = stream
      this.detectLoop()
    },
    async detectLoop() {
      const video = this.$refs.video
      const canvas = this.$refs.canvas
      const size = { width: video.videoWidth, height: video.videoHeight }
      faceapi.matchDimensions(canvas, size)

      const detect = await faceapi.detectSingleFace(video).withFaceLandmarks().withFaceDescriptor()
      const ctx = canvas.getContext('2d')
      ctx.clearRect(0,0,canvas.width,canvas.height)

      if (!detect) {
        this.statusText = '未检测到人脸'
        this.statusColor = '#f56c6c'
        requestAnimationFrame(() => this.detectLoop())
        return
      }

      if (!this.localFace) {
        this.statusText = '请先录入人脸'
        this.statusColor = '#f56c6c'
        requestAnimationFrame(() => this.detectLoop())
        return
      }

      const matcher = new faceapi.FaceMatcher([this.localFace])
      const res = matcher.findBestMatch(detect.descriptor)

      if (res.label === 'unknown') {
        this.statusText = '登录失败,人脸不匹配'
        this.statusColor = '#f56c6c'
      } else {
        this.statusText = '登录成功,欢迎 ' + res.label
        setTimeout(() => this.$router.push('/home'), 1000)
        return
      }
      requestAnimationFrame(() => this.detectLoop())
    },
    goToRegister() {
      this.$router.push('/register')
    }
  }
}
</script>

<style scoped>
.page{width:100vw;height:100vh;background:#111;color:#fff;text-align:center;padding:20px;box-sizing:border-box}
.box{position:relative;width:700px;height:500px;margin:0 auto}
video,canvas{position:absolute;left:0;top:0;width:100%;height:100%}
.status{font-size:22px;margin:10px 0}
.btn{padding:10px 20px;background:#42b983;color:#fff;border:none;border-radius:4px}
</style>
  1. 人脸录入页(src/views/Register.vue

    <template>

    人脸录入

    <canvas ref="canvas"></canvas>
    {{ status }}
    <button @click="register" class="btn">录入人脸</button>
    </template> <script> import * as faceapi from '@vladmandic/face-api' export default { data() { return { name: '', status: '加载模型...' } }, mounted() { this.init() }, methods: { async init() { await faceapi.loadSsdMobilenetv1Model('/models') await faceapi.loadFaceLandmarkModel('/models') await faceapi.loadFaceRecognitionModel('/models') const stream = await navigator.mediaDevices.getUserMedia({ video: true }) this.$refs.video.srcObject = stream }, async register() { const res = await faceapi.detectSingleFace(this.$refs.video).withFaceLandmarks().withFaceDescriptor() if (res) { const faceData = { label: this.name, descriptors: [Array.from(res.descriptor)] } localStorage.setItem('faceUser', JSON.stringify(faceData)) this.status = '录入成功,返回登录' setTimeout(() => this.$router.push('/login'), 1000) } } } } </script> <style scoped> .page{width:100vw;height:100vh;background:#111;color:#fff;text-align:center;padding:20px;box-sizing:border-box} .box{position:relative;width:700px;height:500px;margin:0 auto} video,canvas{position:absolute;left:0;top:0;width:100%;height:100%} .status{font-size:18px;color:#42b983} input{padding:10px;width:200px;margin:10px} .btn{padding:10px 20px;background:#42b983;color:#fff;border:none;border-radius:4px} </style>
  2. 登录成功首页(src/views/Home.vue)

    <template>

    首页

    人脸识别登录成功

    <button @click="back">返回登录</button>
    </template> <script> export default { methods: { back() { this.$router.push('/login') } } } </script> <style scoped> .page{width:100vw;height:100vh;background:#111;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center} button{padding:10px 20px;background:#42b983;color:#fff;border:none;border-radius:4px;margin-top:20px} </style>

五、项目运行

复制代码
npm run serve
npm run electron

六、功能流程总结

  1. 打开软件 → 人脸识别登录页
  2. 未录入 → 点击按钮跳转到录入页
  3. 输入姓名 → 点击录入 → 保存本地
  4. 自动返回登录页
  5. 人脸匹配 → 进入首页
  6. 人脸不匹配 → 本页提示登录失败

七、常见问题解决

  1. Device in use:摄像头被占用,关闭所有软件重新运行
  2. 模型加载失败:确保模型放在 public/models
  3. TextEncoder 报错:使用 @vladmandic/face-api 即可解决

八、总结

本项目完全基于本地离线运行,不依赖任何云端服务,非常适合一体机、门禁、考勤等场景。使用 Vue3 做界面,Electron 打包桌面端,face-api 实现本地人脸识别,代码简洁、可直接商用二次开发。

成果展示:不露脸嘿嘿

相关推荐
德莱厄斯2 小时前
比阿里开源的 page-agent 更强?AutoPilot: 网页内置一个真正能"稳定跑完"的智能体
前端·agent·浏览器
新缸中之脑2 小时前
Chrome DevTools MCP
前端·chrome·chrome devtools
卸载引擎2 小时前
NTP 授时(Network Time Protocol)核心解读,工控机electron程序自动联网授时案例
前端·javascript·electron
xiaokangzhe2 小时前
web技术与nginx网站环境部署
运维·前端·nginx
小奶包他干奶奶2 小时前
什么是原型链(Prototype Chain)?proto和prototype的关系与区别是什么?
前端·javascript
Access开发易登软件2 小时前
在 Access 实现标签输入控件:VBA + HTML 混合开发实战
前端·数据库·信息可视化·html·excel·vba·access
studyForMokey2 小时前
【跨端技术ReactNative】JavaScript学习
android·javascript·学习·react native·react.js
૮・ﻌ・2 小时前
Nodejs - 02:模块化、npm、yarn、cnpm
前端·npm·node.js·express·yarn·cnpm·包管理工具
大雷神2 小时前
HarmonyOS APP<玩转React>开源教程十:组件化开发概述
前端·react.js·开源·harmonyos