12 changed files with 689 additions and 167 deletions
-
BINsrc/assets/images/faceH5/img4.png
-
2src/main.js
-
2src/router/index.js
-
5src/router/routers.js
-
20src/utils/upload.js
-
8src/views/faceRecognition/faceRecLog.vue
-
553src/views/faceRecognition/module/camera.vue
-
33src/views/faceRecognition/module/faceSearch.vue
-
1src/views/faceRecognition/module/selfRegister.vue
-
180src/views/faceRecognition/personInfoManage.vue
-
44src/views/faceRegisterH5/fail.vue
-
8src/views/faceRegisterH5/selfRegister.vue
|
After Width: 1287 | Height: 1000 | Size: 112 KiB |
@ -0,0 +1,553 @@ |
|||
<template> |
|||
<div class="camera-container"> |
|||
<input v-model="imgValue" type="hidden"> |
|||
<el-button class="subsystembtn" @click="openCamera">打开摄像头</el-button> |
|||
<el-select v-if="devices.length > 0" v-model="selectedDeviceId" @change="getStream"> |
|||
<el-option |
|||
v-for="(device,index) in devices" |
|||
:key="index" |
|||
:value="device.deviceId" |
|||
:label="`摄像头 ${device.index + 1}`" |
|||
/> |
|||
</el-select> |
|||
|
|||
<!-- 错误提示 --> |
|||
<div v-if="errorMessage" class="error-message">{{ errorMessage }}</div> |
|||
|
|||
<!-- 加载提示 --> |
|||
<div v-if="isLoading" class="loading-message">正在获取摄像头画面,请稍候...</div> |
|||
|
|||
<!-- 调试信息 (生产环境可隐藏) --> |
|||
<div v-if="showDebugInfo" class="debug-info"> |
|||
<p>调试信息:</p> |
|||
<p>设备数量: {{ devices.length }}</p> |
|||
<p>选中设备: {{ selectedDeviceId ? '存在' : '未选择' }}</p> |
|||
<p>视频状态: {{ getVideoStatusText() }}</p> |
|||
<p>流状态: {{ stream ? '已获取' : '未获取' }}</p> |
|||
<p v-if="stream">轨道状态: {{ stream.getVideoTracks()[0]?.readyState || '未知' }}</p> |
|||
<p v-if="videoDimensions">视频尺寸: {{ videoDimensions.width }}x{{ videoDimensions.height }}</p> |
|||
</div> |
|||
|
|||
<div v-if="showVideo" id="videoContainer"> |
|||
<!-- 视频容器增加背景和边框,明确显示区域 --> |
|||
<div style="display: flex; justify-content: center;"> |
|||
<div class="video-wrapper"> |
|||
<video |
|||
ref="video" |
|||
:style="{display: videoVisible ? 'block' : 'none'}" |
|||
width="240" |
|||
autoplay |
|||
playsinline |
|||
muted |
|||
class="video-feed" |
|||
/> |
|||
</div> |
|||
|
|||
<canvas |
|||
ref="canvasPreview" |
|||
:style="{display: canvasVisible ? 'block' : 'none', marginLeft: '15px'}" |
|||
width="240" |
|||
height="320" |
|||
/> |
|||
<canvas |
|||
ref="canvasUpload" |
|||
style="display: none;" |
|||
width="240" |
|||
height="320" |
|||
/> |
|||
</div> |
|||
<button |
|||
class="subsystembtn take-btn" |
|||
:style="{display: videoVisible ? 'block' : 'none'}" |
|||
:disabled="isLoading || !isVideoPlaying" |
|||
@click="takePhoto" |
|||
> |
|||
拍照 |
|||
</button> |
|||
<button |
|||
class="subsystembtn take-btn" |
|||
:style="{display: canvasVisible ? 'block' : 'none',}" |
|||
@click="retakePhoto" |
|||
> |
|||
重拍 |
|||
</button> |
|||
<!-- 调试按钮 --> |
|||
<!-- <button |
|||
class="debug-btn" |
|||
@click="toggleDebugInfo" |
|||
> |
|||
{{ showDebugInfo ? '隐藏调试' : '显示调试' }} |
|||
</button> --> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'CameraCapture', |
|||
data() { |
|||
return { |
|||
imgValue: '', |
|||
devices: [], |
|||
selectedDeviceId: '', |
|||
showVideo: false, |
|||
videoVisible: true, |
|||
canvasVisible: false, |
|||
stream: null, |
|||
errorMessage: '', |
|||
isLoading: false, |
|||
isVideoPlaying: false, |
|||
videoDimensions: null, // 存储视频尺寸信息 |
|||
showDebugInfo: false, // 是否显示调试信息 |
|||
videoInterval: null // 视频状态检查定时器 |
|||
} |
|||
}, |
|||
mounted() { |
|||
// this.checkBrowserSupport() |
|||
// this.enumerateDevices() |
|||
}, |
|||
beforeDestroy() { |
|||
this.stopStream() |
|||
if (this.videoInterval) { |
|||
clearInterval(this.videoInterval) |
|||
} |
|||
}, |
|||
methods: { |
|||
// 获取视频状态文本描述 |
|||
getVideoStatusText() { |
|||
if (!this.$refs.video) return '未初始化' |
|||
if (this.isLoading) return '加载中' |
|||
if (this.isVideoPlaying) return '播放中' |
|||
if (this.stream) return '已获取流但未播放' |
|||
return '未获取流' |
|||
}, |
|||
|
|||
// 切换调试信息显示 |
|||
toggleDebugInfo() { |
|||
this.showDebugInfo = !this.showDebugInfo |
|||
}, |
|||
|
|||
// 检查浏览器支持 |
|||
checkBrowserSupport() { |
|||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { |
|||
this.errorMessage = '您的浏览器不支持摄像头访问,请使用Chrome、Firefox或Edge等现代浏览器' |
|||
return false |
|||
} |
|||
return true |
|||
}, |
|||
|
|||
// 枚举设备 |
|||
enumerateDevices() { |
|||
return navigator.mediaDevices.enumerateDevices() |
|||
.then(devices => { |
|||
this.handleDevices(devices) |
|||
return devices |
|||
}) |
|||
.catch(error => { |
|||
this.handleError(error) |
|||
return Promise.reject(error) |
|||
}) |
|||
}, |
|||
|
|||
handleDevices(deviceInfos) { |
|||
this.devices = deviceInfos |
|||
.filter(device => device.kind === 'videoinput' && device.deviceId) // 过滤掉没有deviceId的设备 |
|||
.map((device, index) => ({ |
|||
...device, |
|||
index, |
|||
deviceId: device.deviceId || `device-${index}` // 确保有一个有效ID |
|||
})) |
|||
console.log('检测到的摄像头设备:', this.devices) |
|||
|
|||
if (this.devices.length > 0 && !this.selectedDeviceId) { |
|||
this.selectedDeviceId = this.devices[0].deviceId |
|||
} else if (this.devices.length === 0) { |
|||
this.errorMessage = '未检测到摄像头设备,请检查硬件连接' |
|||
} |
|||
}, |
|||
|
|||
// 获取视频流 - 增加更多兼容性处理 |
|||
getStream() { |
|||
// 清除之前的定时器 |
|||
if (this.videoInterval) { |
|||
clearInterval(this.videoInterval) |
|||
} |
|||
|
|||
this.stopStream() |
|||
this.errorMessage = '' |
|||
this.isLoading = true |
|||
this.isVideoPlaying = false |
|||
this.videoDimensions = null |
|||
|
|||
// 最基础的约束配置,最大化兼容性 |
|||
// 修改getStream中的constraints |
|||
const constraints = { |
|||
audio: false, |
|||
video: { |
|||
...(this.selectedDeviceId ? { deviceId: { exact: this.selectedDeviceId }} : {}), |
|||
facingMode: 'user', |
|||
width: { ideal: 240 }, |
|||
height: { ideal: 320 }, |
|||
aspectRatio: 3 / 4 // 匹配640x480的比例 |
|||
} |
|||
} |
|||
|
|||
// 检查设备是否存在 |
|||
if (this.selectedDeviceId && !this.devices.some(d => d.deviceId === this.selectedDeviceId)) { |
|||
this.errorMessage = '所选摄像头设备不存在' |
|||
this.isLoading = false |
|||
return |
|||
} |
|||
|
|||
// 尝试获取流 |
|||
const streamPromise = navigator.mediaDevices.getUserMedia(constraints) |
|||
|
|||
// 10秒超时检测 |
|||
Promise.race([ |
|||
streamPromise, |
|||
new Promise((_, reject) => |
|||
setTimeout(() => reject(new Error('获取摄像头流超时,请检查设备是否被占用')), 10000) |
|||
) |
|||
]) |
|||
.then(stream => this.handleStream(stream)) |
|||
.catch(error => this.handleError(error)) |
|||
.finally(() => { |
|||
this.isLoading = false |
|||
}) |
|||
}, |
|||
|
|||
handleStream(stream) { |
|||
this.stream = stream |
|||
|
|||
if (!this.$refs.video) { |
|||
this.errorMessage = '视频元素未初始化' |
|||
return |
|||
} |
|||
|
|||
if (this.$refs.video) { |
|||
this.$refs.video.style.transform = 'translateZ(0)' |
|||
this.$refs.video.style.willChange = 'auto' |
|||
} |
|||
|
|||
// 清除之前的事件监听器 |
|||
this.$refs.video.oncanplay = null |
|||
this.$refs.video.onerror = null |
|||
this.$refs.video.onplaying = null |
|||
this.$refs.video.onended = null |
|||
this.$refs.video.onloadedmetadata = null |
|||
|
|||
// 停止可能的旧播放 |
|||
this.$refs.video.pause() |
|||
this.$refs.video.srcObject = null |
|||
|
|||
// 监控视频轨道状态 |
|||
const videoTracks = stream.getVideoTracks() |
|||
if (videoTracks.length > 0) { |
|||
console.log('视频轨道状态:', videoTracks[0].readyState) |
|||
videoTracks[0].onended = () => { |
|||
console.log('视频轨道已结束') |
|||
this.errorMessage = '视频轨道已中断,请重新连接' |
|||
this.isVideoPlaying = false |
|||
} |
|||
} |
|||
|
|||
// 设置新流 |
|||
this.$refs.video.srcObject = stream |
|||
|
|||
// 监听视频元数据加载事件 |
|||
this.$refs.video.onloadedmetadata = () => { |
|||
console.log('视频元数据加载完成') |
|||
this.videoDimensions = { |
|||
width: this.$refs.video.videoWidth, |
|||
height: this.$refs.video.videoHeight |
|||
} |
|||
console.log('视频尺寸:', this.videoDimensions) |
|||
} |
|||
|
|||
// 监听视频错误事件 |
|||
this.$refs.video.onerror = (e) => { |
|||
console.error('视频元素错误:', e) |
|||
console.error('错误代码:', e.target.error.code) |
|||
const errorCodes = { |
|||
1: '用户终止', |
|||
2: '网络错误', |
|||
3: '解码错误', |
|||
4: '无法播放', |
|||
5: '加密错误' |
|||
} |
|||
this.errorMessage = `视频播放失败: ${errorCodes[e.target.error.code] || '未知错误'}` |
|||
} |
|||
|
|||
// 监听可播放事件 |
|||
this.$refs.video.oncanplay = () => { |
|||
console.log('视频流已准备就绪') |
|||
this.errorMessage = '' |
|||
// 主动触发播放 |
|||
this.$refs.video.play().catch(err => this.handlePlayError(err)) |
|||
} |
|||
|
|||
// 监听播放事件 |
|||
this.$refs.video.onplaying = () => { |
|||
console.log('视频开始播放') |
|||
this.isVideoPlaying = true |
|||
this.errorMessage = '' |
|||
} |
|||
|
|||
// 监听结束事件 |
|||
this.$refs.video.onended = () => { |
|||
console.log('视频流已结束') |
|||
this.isVideoPlaying = false |
|||
this.errorMessage = '视频流已中断,请重新获取' |
|||
} |
|||
|
|||
// 主动播放视频 |
|||
this.$refs.video.play().catch(err => this.handlePlayError(err)) |
|||
|
|||
// 设置定时器检查视频状态 |
|||
this.videoInterval = setInterval(() => { |
|||
if (this.$refs.video && this.$refs.video.readyState >= 1) { |
|||
if (!this.isVideoPlaying) { |
|||
console.log('视频未播放,尝试重新播放') |
|||
this.$refs.video.play().catch(err => this.handlePlayError(err)) |
|||
} |
|||
} |
|||
}, 2000) |
|||
}, |
|||
|
|||
// 处理播放错误 |
|||
handlePlayError(err) { |
|||
console.error('播放失败:', err) |
|||
const playErrors = { |
|||
'NotAllowedError': '播放被阻止,可能是自动播放策略限制', |
|||
'NotSupportedError': '浏览器不支持此视频格式', |
|||
'AbortError': '播放被中止', |
|||
'InvalidStateError': '视频元素状态无效' |
|||
} |
|||
this.errorMessage = `播放失败: ${playErrors[err.name] || err.message}` |
|||
}, |
|||
|
|||
// 停止流 |
|||
stopStream() { |
|||
if (this.stream) { |
|||
try { |
|||
this.stream.getTracks().forEach(track => { |
|||
track.stop() |
|||
}) |
|||
} catch (e) { |
|||
console.error('停止流时出错:', e) |
|||
} |
|||
this.stream = null |
|||
} |
|||
|
|||
if (this.$refs.video) { |
|||
this.$refs.video.pause() |
|||
this.$refs.video.srcObject = null |
|||
} |
|||
|
|||
this.isVideoPlaying = false |
|||
}, |
|||
|
|||
// 打开摄像头 |
|||
openCamera() { |
|||
if (!this.checkBrowserSupport()) return |
|||
|
|||
this.enumerateDevices().then(() => { |
|||
if (this.devices.length > 0) { |
|||
this.showVideo = true |
|||
this.videoVisible = true |
|||
this.canvasVisible = false |
|||
this.getStream() |
|||
} else { |
|||
this.errorMessage = '未检测到摄像头设备' |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 拍照 |
|||
takePhoto() { |
|||
if (!this.$refs.video || !this.$refs.canvasPreview || !this.$refs.canvasUpload) { |
|||
this.errorMessage = '组件未完全加载' |
|||
return |
|||
} |
|||
|
|||
if (!this.isVideoPlaying) { |
|||
this.errorMessage = '视频未就绪,请稍候再试' |
|||
return |
|||
} |
|||
|
|||
try { |
|||
const previewCtx = this.$refs.canvasPreview.getContext('2d') |
|||
previewCtx.drawImage(this.$refs.video, 0, 0, 240, 320) |
|||
|
|||
const uploadCtx = this.$refs.canvasUpload.getContext('2d') |
|||
uploadCtx.drawImage(this.$refs.video, 0, 0, 240, 320) |
|||
|
|||
this.videoVisible = false |
|||
this.canvasVisible = true |
|||
this.imgValue = this.$refs.canvasUpload.toDataURL('image/jpeg') |
|||
this.errorMessage = '' |
|||
|
|||
// uploadFaceImgBase64(this.baseApi, this.imgValue).then(res => { |
|||
// console.log(res) |
|||
// if (res.data.code === 200) { |
|||
// // this.form.personPhotoUrl = res.data.data |
|||
// this.$emit('cameraData', this.imgValue, res.data.data) |
|||
// } |
|||
// }) |
|||
} catch (e) { |
|||
console.error('拍照失败:', e) |
|||
this.errorMessage = '拍照失败,请重试' |
|||
} |
|||
}, |
|||
|
|||
// 重拍 |
|||
retakePhoto() { |
|||
this.videoVisible = true |
|||
this.canvasVisible = false |
|||
this.imgValue = '' |
|||
if (!this.stream || !this.isVideoPlaying) { |
|||
this.getStream() |
|||
} |
|||
}, |
|||
|
|||
// 获取图片数据 |
|||
getImgData() { |
|||
return this.imgValue |
|||
}, |
|||
|
|||
// 错误处理 |
|||
handleError(error) { |
|||
console.error('摄像头错误:', error) |
|||
|
|||
switch (error.name) { |
|||
case 'NotAllowedError': |
|||
this.errorMessage = '请授予摄像头访问权限(通常在浏览器地址栏附近)' |
|||
break |
|||
case 'NotFoundError': |
|||
this.errorMessage = '未找到指定的摄像头设备' |
|||
break |
|||
case 'NotReadableError': |
|||
this.errorMessage = '摄像头被占用或无法访问,请关闭其他使用摄像头的程序' |
|||
break |
|||
case 'OverconstrainedError': |
|||
this.errorMessage = '摄像头不支持所需的参数,请尝试其他设备' |
|||
break |
|||
case 'TypeError': |
|||
this.errorMessage = '摄像头参数错误,请刷新页面重试' |
|||
break |
|||
default: |
|||
this.errorMessage = `${error.name}: ${error.message}` |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.camera-container { |
|||
text-align: center; |
|||
} |
|||
|
|||
#videoContainer { |
|||
margin-top: 15px; |
|||
position: relative; |
|||
} |
|||
|
|||
.video-wrapper { |
|||
display: inline-block; |
|||
border: 1px solid #ddd; |
|||
border-radius: 4px; |
|||
/* background-color: #000; */ |
|||
position: relative; |
|||
} |
|||
|
|||
/* 视频加载时显示提示 */ |
|||
.video-wrapper::before { |
|||
content: '等待视频流...'; |
|||
position: absolute; |
|||
color: #aaa; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
pointer-events: none; |
|||
} |
|||
|
|||
.video-feed { |
|||
width: 240px !important; |
|||
height: 320px !important; |
|||
display: block !important; |
|||
opacity: 1 !important; |
|||
z-index: 9999 !important; |
|||
background-color: transparent !important; |
|||
} |
|||
|
|||
.subsystembtn { |
|||
width: 120px; |
|||
height: 30px; |
|||
background-color: #1d5db2; |
|||
border: 0; |
|||
color: white; |
|||
font-size: 14px; |
|||
cursor: pointer; |
|||
margin: 5px; |
|||
border-radius: 4px; |
|||
transition: background-color 0.3s; |
|||
|
|||
} |
|||
|
|||
.subsystembtn:disabled { |
|||
background-color: #999; |
|||
cursor: not-allowed; |
|||
} |
|||
|
|||
.subsystembtn:hover:not(:disabled) { |
|||
background-color: #164b8c; |
|||
} |
|||
|
|||
.take-btn{ |
|||
margin: 15px auto 0 auto; |
|||
} |
|||
|
|||
.el-select { |
|||
margin: 0 10px; |
|||
width: auto; |
|||
} |
|||
|
|||
.error-message { |
|||
color: #dc3545; |
|||
margin: 10px 0; |
|||
padding: 8px; |
|||
background-color: #f8d7da; |
|||
border-radius: 4px; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.loading-message { |
|||
color: #666; |
|||
margin: 10px 0; |
|||
padding: 8px; |
|||
background-color: #f8f9fa; |
|||
border-radius: 4px; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.debug-info { |
|||
margin: 10px 0; |
|||
padding: 10px; |
|||
background-color: #f0f7ff; |
|||
border-radius: 4px; |
|||
font-size: 12px; |
|||
text-align: left; |
|||
color: #333; |
|||
} |
|||
|
|||
.debug-btn { |
|||
margin-top: 10px; |
|||
padding: 5px 10px; |
|||
font-size: 12px; |
|||
background-color: #f0f0f0; |
|||
border: 1px solid #ddd; |
|||
border-radius: 3px; |
|||
cursor: pointer; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,44 @@ |
|||
<template> |
|||
<div class="face-mobile"> |
|||
<!-- 查到用户信息,并且已经绑定过人脸 --> |
|||
<div class="face-valid"> |
|||
<img src="@/assets/images/faceH5/img4.png" alt=""> |
|||
<p>未成功识别人脸,人脸注册失败!</p> |
|||
<div class="reg-btn-group"> |
|||
<div class="return-btn other-return" @click="returnValid">返回首页</div> |
|||
<div class="return-btn" @click="toSelfRegister">重新注册</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'Registered', |
|||
data() { |
|||
return { |
|||
} |
|||
}, |
|||
watch: { |
|||
}, |
|||
methods: { |
|||
returnValid() { |
|||
this.$router.push({ |
|||
path: '/faceRegisterH5', |
|||
query: { 'strLibcode': this.$route.query.strLibcode } |
|||
}) |
|||
}, |
|||
toSelfRegister() { |
|||
this.$router.push({ |
|||
path: '/faceSelfRegister', |
|||
query: { 'strLibcode': this.$route.query.strLibcode } |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "~@/assets/styles/faceMobile.scss"; |
|||
</style> |
|||
|
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue