From 93fcd8d1b6de47716b5f0e5086eb31ef7ed32ea8 Mon Sep 17 00:00:00 2001 From: starmc Date: Tue, 3 Jun 2025 23:17:47 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E6=91=84=E5=83=8F=E5=A4=B4=E6=AD=A3=E5=B8=B8=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/camera/index.vue | 91 ++++++++++++++----- pages/login/index.vue | 199 ++++++++++++++++++++++++++++------------- 2 files changed, 204 insertions(+), 86 deletions(-) diff --git a/pages/camera/index.vue b/pages/camera/index.vue index 90d26765..5f6e91e9 100644 --- a/pages/camera/index.vue +++ b/pages/camera/index.vue @@ -1,8 +1,7 @@ @@ -11,37 +10,85 @@ export default { data() { return { videoStream: null, - photo: null + videoFacingMode: 'user', + videoElement: null }; }, - mounted() { - this.startCamera(); - }, methods: { async startCamera() { try { const stream = await navigator.mediaDevices.getUserMedia({ - video: { facingMode: 'user' } + video: { facingMode: this.videoFacingMode }, + audio: false }); this.videoStream = stream; - this.$refs.video.srcObject = stream; + + // 如果之前有 video 元素就移除 + const container = document.getElementById('video-container'); + if (this.videoElement) { + container.removeChild(this.videoElement); + } + + // 创建原生 video 元素 + const video = document.createElement('video'); + video.autoplay = true; + video.muted = true; + video.playsInline = true; + video.style.width = '100%'; + video.style.maxWidth = '600px'; + video.style.height = '400px'; + video.style.background = 'black'; + video.style.objectFit = 'cover'; + video.style.borderRadius = '10px'; + video.style.marginBottom = '16px'; + + // 设置媒体流 + video.srcObject = stream; + + // 添加到容器中 + container.appendChild(video); + this.videoElement = video; + } catch (error) { - alert('摄像头访问失败: ' + error.message); + console.error('摄像头访问失败:', error); + uni.showToast({ + title: '摄像头权限被拒绝或设备不支持', + icon: 'none' + }); } }, - capture() { - const video = this.$refs.video; - const canvas = document.createElement('canvas'); - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - canvas.getContext('2d').drawImage(video, 0, 0); - this.photo = canvas.toDataURL('image/png'); + toggleCamera() { + this.videoFacingMode = this.videoFacingMode === 'user' ? 'environment' : 'user'; + this.stopCamera(); + this.startCamera(); + }, + stopCamera() { + if (this.videoStream) { + this.videoStream.getTracks().forEach(track => track.stop()); + this.videoStream = null; + } } }, + mounted() { + this.startCamera(); + }, beforeDestroy() { - if (this.videoStream) { - this.videoStream.getTracks().forEach(track => track.stop()); - } + this.stopCamera(); } }; - \ No newline at end of file + + + \ No newline at end of file diff --git a/pages/login/index.vue b/pages/login/index.vue index d1c89599..502da02d 100644 --- a/pages/login/index.vue +++ b/pages/login/index.vue @@ -48,13 +48,15 @@
- +
+
+ + + +
+

正在录制,请根据提示做动作...

+

已采集{{recordedFrames.length}}帧,可以点击登录

+ @@ -67,10 +69,6 @@ > -
- - -
@@ -108,8 +106,8 @@ const errorMessage = ref(""); const loggedInMessage = ref(""); - const videoRef = ref(null); - const canvasRef = ref(null); + const videoElement = ref(null); + const videoFacingMode = ref('user'); const isCameraActive = ref(false); const faceImage = ref(''); // 用于显示完整dataURL const stream = ref(null); @@ -122,6 +120,7 @@ const recordedFrames = ref([]); const recordingInterval = ref(null); const frameCaptureInterval = ref(null); + const canvasRef = ref(null); // 登录类型切换 const handleLoginTypeChange = () => { @@ -154,25 +153,78 @@ const startCamera = async () => { try { - stream.value = await navigator.mediaDevices.getUserMedia({ video: true }); - await nextTick(); - if (videoRef.value) { - videoRef.value.srcObject = stream.value; - videoRef.value.play(); - isCameraActive.value = true; + const stream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: videoFacingMode.value }, + audio: false + }); + stream.value = stream; + + // 如果之前有 video 元素就移除 + const container = document.getElementById('video-container'); + if (videoElement.value) { + container.removeChild(videoElement.value); } + + // 创建原生 video 元素 + const video = document.createElement('video'); + video.autoplay = true; + video.muted = true; + video.playsInline = true; + video.style.width = '100%'; + video.style.maxWidth = '600px'; + video.style.height = '400px'; + video.style.background = 'black'; + video.style.objectFit = 'cover'; + video.style.borderRadius = '10px'; + video.style.marginBottom = '16px'; + + // 设置媒体流 + video.srcObject = stream; + + // 添加到容器中 + container.appendChild(video); + videoElement.value = video; + isCameraActive.value = true; + } catch (error) { - console.error('Error accessing camera:', error); + console.error('摄像头访问失败:', error); uni.showToast({ - title: '无法访问摄像头,请确保已授予权限', + title: '摄像头权限被拒绝或设备不支持', icon: 'none' }); isCameraActive.value = false; } }; + const toggleCamera = () => { + videoFacingMode.value = videoFacingMode.value === 'user' ? 'environment' : 'user'; + stopCamera(); + startCamera(); + }; + const startRecording = () => { + uni.showToast({ + title: '123', + icon: 'success', + mask: true + }) if (!isCameraActive.value) return; + uni.showToast({ + title: '234', + icon: 'success', + mask: true + }) + + if (!videoElement.value || !canvasRef.value) { + uni.showToast({ title: '视频未就绪', icon: 'none' }); + return; + } + + uni.showToast({ + title: '345', + icon: 'success', + mask: true + }) isRecording.value = true; recordingTime.value = 0; @@ -186,34 +238,39 @@ stopRecording(); } }, 1000); - + uni.showToast({ + title: '哼哼', + icon: 'success', + mask: true + }) // 每200ms捕获一帧 frameCaptureInterval.value = setInterval(() => { - // #ifdef H5 - if (videoRef.value && canvasRef.value) { + uni.showToast({ + title: '哼啊哼', + icon: 'success', + mask: true + }) + if (videoElement.value && canvasRef.value) { + uni.showToast({ + title: '哼aaa啊啊', + icon: 'success', + mask: true + }); + console.log('canvasRef', canvasRef.value, 'videoElement', videoElement.value); const canvas = canvasRef.value; const context = canvas.getContext('2d'); - canvas.width = videoRef.value.videoWidth; - canvas.height = videoRef.value.videoHeight; - context.drawImage(videoRef.value, 0, 0, canvas.width, canvas.height); + canvas.width = videoElement.value.videoWidth; + canvas.height = videoElement.value.videoHeight; + context.drawImage(videoElement.value, 0, 0, canvas.width, canvas.height); + uni.showToast({ + title: "妈妈", + icon: 'success', + mask: true + }) recordedFrames.value.push(canvas.toDataURL('image/jpeg', 0.9)); + } else { + console.warn('canvasRef 或 videoElement 未就绪'); } - // #endif - - // #ifdef APP-PLUS || MP-WEIXIN - const context = uni.createCameraContext(); - if (context) { - context.takePhoto({ - quality: 'high', - success: (res) => { - recordedFrames.value.push(res.tempImagePath); - }, - fail: (err) => { - console.error('拍照失败:', err); - } - }); - } - // #endif }, 200); }; @@ -224,19 +281,21 @@ // 确保至少录制了5帧 if (recordedFrames.value.length < 5) { - errorMessage.value = "录制时间太短,请至少录制1秒"; + uni.showToast({ + title: recordedFrames.value.length, + icon: 'none' + }); + // errorMessage.value = "录制时间太短,请至少录制1秒"; recordedFrames.value = []; return; } }; const stopCamera = () => { - // #ifdef H5 if (stream.value) { stream.value.getTracks().forEach(track => track.stop()); stream.value = null; } - // #endif isCameraActive.value = false; }; @@ -268,6 +327,10 @@ (loginType.value === "password" && !password.value) || (loginType.value === "code" && !code.value) || (loginType.value === "face" && (recordedFrames.value.length === 0 || !currentAction.value))) { + uni.showToast({ + title: currentAction.value, + icon: 'none' + }); errorMessage.value = "请填写所有必填字段。"; return; } @@ -382,9 +445,9 @@ const captureFace = () => { // #ifdef H5 - if (!videoRef.value) return; + if (!videoElement.value) return; - const video = videoRef.value; + const video = videoElement.value; if (!video.videoWidth || !video.videoHeight) { uni.showToast({ title: '视频未就绪,请稍候', @@ -573,26 +636,34 @@ .face-buttons { display: flex; gap: 10px; + justify-content: center; + margin-top: 10px; } - .video-preview { - width: 100%; - max-width: 260px; - border-radius: 8px; - background: #f0f0f0; + .face-buttons button { + padding: 8px 16px; + background: #4f8cff; + color: #fff; + border: none; + border-radius: 6px; + font-size: 1rem; + cursor: pointer; + transition: background 0.2s; } - .recording-preview { - font-size: 0.95rem; - color: #555; + .face-buttons button:hover { + background: #2563eb; } - .action-prompt { - text-align: center; - font-size: 1rem; - color: #4f8cff; + .face-buttons button:disabled { + background: #ccc; + cursor: not-allowed; } - .recording-status { - color: #d32f2f; - font-weight: bold; - margin-top: 0.5rem; + .video-container { + width: 100%; + max-width: 600px; + height: 400px; + background: #000; + border-radius: 10px; + overflow: hidden; + margin-bottom: 16px; } .face-preview { margin-top: 10px; -- Gitee From 9a7039013da6e6650158b9fb36fd6942822093e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=A5=81=E8=BD=A9?= <15840712+starmc@user.noreply.gitee.com> Date: Wed, 4 Jun 2025 15:34:17 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=BA=E8=84=B8?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E7=99=BB=E5=BD=95=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/forget-password/index.vue | 17 +++----- pages/login/index.vue | 70 +++++++++------------------------ pages/register/index.vue | 17 +++----- 3 files changed, 31 insertions(+), 73 deletions(-) diff --git a/pages/forget-password/index.vue b/pages/forget-password/index.vue index 9e27398c..b05b36b3 100644 --- a/pages/forget-password/index.vue +++ b/pages/forget-password/index.vue @@ -125,11 +125,11 @@ const startCamera = async () => { try { // #ifdef H5 stream.value = await navigator.mediaDevices.getUserMedia({ video: true }); - if (videoRef.value) { - videoRef.value.srcObject = stream.value; - // 等待视频元数据加载完成 + const video = document.querySelector('video'); + if (video) { + video.srcObject = stream.value; await new Promise((resolve) => { - videoRef.value.onloadedmetadata = () => { + video.onloadedmetadata = () => { resolve(); }; }); @@ -154,9 +154,8 @@ const startCamera = async () => { const captureFace = () => { // #ifdef H5 - if (!videoRef.value) return; - - const video = videoRef.value; + const video = document.querySelector('video'); + if (!video) return; if (!video.videoWidth || !video.videoHeight) { uni.showToast({ title: '视频未就绪,请稍候', @@ -164,7 +163,6 @@ const captureFace = () => { }); return; } - const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); if (!context) { @@ -174,10 +172,8 @@ const captureFace = () => { }); return; } - canvas.width = video.videoWidth; canvas.height = video.videoHeight; - try { context.drawImage(video, 0, 0, canvas.width, canvas.height); faceImage.value = canvas.toDataURL('image/jpeg'); @@ -193,7 +189,6 @@ const captureFace = () => { // #ifdef APP-PLUS || MP-WEIXIN if (!isCameraActive.value) return; - const cameraContext = uni.createCameraContext(); if (cameraContext) { cameraContext.takePhoto({ diff --git a/pages/login/index.vue b/pages/login/index.vue index 502da02d..df06af57 100644 --- a/pages/login/index.vue +++ b/pages/login/index.vue @@ -49,6 +49,7 @@
+
@@ -203,73 +204,38 @@ }; const startRecording = () => { - uni.showToast({ - title: '123', - icon: 'success', - mask: true - }) if (!isCameraActive.value) return; - uni.showToast({ - title: '234', - icon: 'success', - mask: true - }) - - if (!videoElement.value || !canvasRef.value) { + if (!videoElement.value) { uni.showToast({ title: '视频未就绪', icon: 'none' }); return; } - uni.showToast({ - title: '345', - icon: 'success', - mask: true - }) - + // 直接用 JS 创建 canvas,不用 ref + const canvas = document.createElement('canvas'); isRecording.value = true; recordingTime.value = 0; recordedFrames.value = []; - - // 每秒更新录制时间 + recordingInterval.value = setInterval(() => { recordingTime.value++; - // 3秒后自动停止 if (recordingTime.value >= 3) { stopRecording(); } }, 1000); - uni.showToast({ - title: '哼哼', - icon: 'success', - mask: true - }) - // 每200ms捕获一帧 + frameCaptureInterval.value = setInterval(() => { - uni.showToast({ - title: '哼啊哼', - icon: 'success', - mask: true - }) - if (videoElement.value && canvasRef.value) { - uni.showToast({ - title: '哼aaa啊啊', - icon: 'success', - mask: true - }); - console.log('canvasRef', canvasRef.value, 'videoElement', videoElement.value); - const canvas = canvasRef.value; - const context = canvas.getContext('2d'); + if (videoElement.value) { canvas.width = videoElement.value.videoWidth; canvas.height = videoElement.value.videoHeight; + const context = canvas.getContext('2d'); + if (!context) { + console.error('Failed to get 2D context'); + return; + } context.drawImage(videoElement.value, 0, 0, canvas.width, canvas.height); - uni.showToast({ - title: "妈妈", - icon: 'success', - mask: true - }) recordedFrames.value.push(canvas.toDataURL('image/jpeg', 0.9)); } else { - console.warn('canvasRef 或 videoElement 未就绪'); + console.warn('videoElement 未就绪'); } }, 200); }; @@ -278,14 +244,11 @@ isRecording.value = false; clearInterval(recordingInterval.value); clearInterval(frameCaptureInterval.value); - - // 确保至少录制了5帧 if (recordedFrames.value.length < 5) { uni.showToast({ - title: recordedFrames.value.length, + title: '录制时间太短,请重试', icon: 'none' }); - // errorMessage.value = "录制时间太短,请至少录制1秒"; recordedFrames.value = []; return; } @@ -420,6 +383,11 @@ checkIfLoggedIn(); if (loginType.value === 'face') { getNewAction(); + setTimeout(() => { + if (canvasRef.value) { + console.log('Canvas is ready:', canvasRef.value); + } + }, 100); } }); diff --git a/pages/register/index.vue b/pages/register/index.vue index 393d9118..21242d0f 100644 --- a/pages/register/index.vue +++ b/pages/register/index.vue @@ -185,11 +185,11 @@ const startCamera = async () => { try { // #ifdef H5 stream.value = await navigator.mediaDevices.getUserMedia({ video: true }); - if (videoRef.value) { - videoRef.value.srcObject = stream.value; - // 等待视频元数据加载完成 + const video = document.querySelector('video'); + if (video) { + video.srcObject = stream.value; await new Promise((resolve) => { - videoRef.value.onloadedmetadata = () => { + video.onloadedmetadata = () => { resolve(); }; }); @@ -214,9 +214,8 @@ const startCamera = async () => { const captureFace = () => { // #ifdef H5 - if (!videoRef.value) return; - - const video = videoRef.value; + const video = document.querySelector('video'); + if (!video) return; if (!video.videoWidth || !video.videoHeight) { uni.showToast({ title: '视频未就绪,请稍候', @@ -224,7 +223,6 @@ const captureFace = () => { }); return; } - const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); if (!context) { @@ -234,10 +232,8 @@ const captureFace = () => { }); return; } - canvas.width = video.videoWidth; canvas.height = video.videoHeight; - try { context.drawImage(video, 0, 0, canvas.width, canvas.height); faceImage.value = canvas.toDataURL('image/jpeg'); @@ -253,7 +249,6 @@ const captureFace = () => { // #ifdef APP-PLUS || MP-WEIXIN if (!isCameraActive.value) return; - const cameraContext = uni.createCameraContext(); if (cameraContext) { cameraContext.takePhoto({ -- Gitee From f095224a2e941cc7d2929c36a3ebc8b21ea8b8ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=A5=81=E8=BD=A9?= <15840712+starmc@user.noreply.gitee.com> Date: Sat, 7 Jun 2025 15:50:24 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.jss | 28 ----- main.ts | 20 ++++ pages.json | 8 ++ pages/exam/index.vue | 273 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 301 insertions(+), 28 deletions(-) delete mode 100644 main.jss create mode 100644 pages/exam/index.vue diff --git a/main.jss b/main.jss deleted file mode 100644 index c2a43f67..00000000 --- a/main.jss +++ /dev/null @@ -1,28 +0,0 @@ -import App from './App' - -// #ifndef VUE3 -import Vue from 'vue' -import './uni.promisify.adaptor' -Vue.config.productionTip = false -App.mpType = 'app' -const app = new Vue({ - ...App -}) -app.use(pinia) // 必须先注册 Pinia -app.$mount() -// #endif - -// #ifdef VUE3 -import { createSSRApp } from 'vue' -import { createPinia } from 'pinia' - -export function createApp() { - const app = createSSRApp(App) - const pinia = createPinia() - app.use(pinia) - return { - app, - pinia - } -} -// #endif \ No newline at end of file diff --git a/main.ts b/main.ts index 617468ad..52aab97a 100644 --- a/main.ts +++ b/main.ts @@ -3,6 +3,26 @@ import { createPinia } from 'pinia' import './style.css' import App from './App.vue' +// setImmediate polyfill for environments that lack it +declare global { + var setImmediate: (callback: (...args: any[]) => void, ...args: any[]) => number; + var clearImmediate: (id: number) => void; +} + +if (typeof setImmediate === 'undefined') { + const globalObj = typeof window !== 'undefined' ? window : + typeof globalThis !== 'undefined' ? globalThis : + typeof global !== 'undefined' ? global : + Function('return this')(); + + globalObj.setImmediate = function(fn: (...args: any[]) => void) { + return setTimeout(fn, 0); + }; + globalObj.clearImmediate = function(id: number) { + clearTimeout(id); + }; +} + // 声明全局函数类型 declare global { interface Uni { diff --git a/pages.json b/pages.json index 8868573e..bfa098d6 100644 --- a/pages.json +++ b/pages.json @@ -138,6 +138,14 @@ "enablePullDownRefresh": false, "path": "/camera" } + }, + { + "path": "pages/exam/index", + "style": { + "navigationBarTitleText": "测试", + "enablePullDownRefresh": false, + "path": "/exam" + } } ], "globalStyle": { diff --git a/pages/exam/index.vue b/pages/exam/index.vue new file mode 100644 index 00000000..f1f99ce0 --- /dev/null +++ b/pages/exam/index.vue @@ -0,0 +1,273 @@ + + + + + \ No newline at end of file -- Gitee