前言
在本文中,我将手把手教你使用 Vue3 + Electron 实现一套纯本地、离线运行、不上云的人脸识别登录系统。功能包含:
- 开机直接进入人脸识别登录页
- 人脸登录成功 → 进入首页
- 人脸不匹配 → 本页显示失败
- 登录页可跳转到人脸录入页
- 录入成功自动返回登录
- 全程本地运行,无需网络
技术栈
- Vue3 + VueRouter
- Electron(打包桌面端 + 调用摄像头)
- @vladmandic/face-api(本地人脸识别库)
- 模型本地加载,100% 离线运行
一、项目初始化
1. 创建 Vue3 项目
vue create electron-face-local
cd electron-face-local
-
安装依赖
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())
-
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>
-
人脸录入页(src/views/Register.vue
<template></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>人脸录入
<canvas ref="canvas"></canvas>{{ status }}<button @click="register" class="btn">录入人脸</button> -
登录成功首页(src/views/Home.vue)
<template></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>首页
人脸识别登录成功
<button @click="back">返回登录</button>
五、项目运行
npm run serve
npm run electron
六、功能流程总结
- 打开软件 → 人脸识别登录页
- 未录入 → 点击按钮跳转到录入页
- 输入姓名 → 点击录入 → 保存本地
- 自动返回登录页
- 人脸匹配 → 进入首页
- 人脸不匹配 → 本页提示登录失败
七、常见问题解决
- Device in use:摄像头被占用,关闭所有软件重新运行
- 模型加载失败:确保模型放在 public/models
- TextEncoder 报错:使用 @vladmandic/face-api 即可解决
八、总结
本项目完全基于本地离线运行,不依赖任何云端服务,非常适合一体机、门禁、考勤等场景。使用 Vue3 做界面,Electron 打包桌面端,face-api 实现本地人脸识别,代码简洁、可直接商用二次开发。
成果展示:不露脸嘿嘿

