From 68fc21992bdc21792a527b36fc3a02f6b7c57812 Mon Sep 17 00:00:00 2001 From: zhoulinf <2507241354@qq.com> Date: Tue, 4 Aug 2026 19:29:34 +0800 Subject: [PATCH] =?UTF-8?q?CRM=5F26-08-05#story#9126,=E8=BD=A6=E8=BE=86?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E3=80=81=E6=9C=8D=E5=8A=A1=E5=95=86=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=B3=BB=E7=BB=9F=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/CameraGuide.vue | 36 +++++++++++++++++++++++++--------- src/views/index/vehicleAdd.vue | 10 ++++++++-- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/components/CameraGuide.vue b/src/components/CameraGuide.vue index 790940e3..4fb34f30 100644 --- a/src/components/CameraGuide.vue +++ b/src/components/CameraGuide.vue @@ -33,7 +33,7 @@
- +
@@ -134,6 +134,11 @@ export default { visible: { type: Boolean, default: false + }, + // 父组件前置探测时已拿到的相机流,传入则直接复用(iOS 上避免二次 getUserMedia 黑屏) + stream: { + type: Object, + default: null } }, data() { @@ -144,7 +149,7 @@ export default { 'medium', 'tall', 'short wide', 'medium', 'tall', 'short', 'medium wide', 'tall'] return { step: 'guide', // guide 拍摄引导 | preview 预览确认 - stream: null, + activeStream: null, previewBlob: null, previewDataURL: '', barcodeBars: heights @@ -176,7 +181,8 @@ export default { methods: { async startCamera() { try { - const stream = await navigator.mediaDevices.getUserMedia({ + // 优先复用父组件探测时拿到的流,未传入才自行申请 + const stream = this.stream || await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false }) @@ -185,12 +191,23 @@ export default { stream.getTracks().forEach(t => t.stop()) return } - this.stream = stream + this.activeStream = stream await this.$nextTick() const video = this.$refs.video if (video) { + // iOS 自动播放加固:muted/playsinline 属性与特性同时设置,元数据就绪后再 play + video.muted = true + video.setAttribute('playsinline', 'true') + video.setAttribute('webkit-playsinline', 'true') video.srcObject = stream - video.play().catch(() => {}) + const playVideo = () => { + video.play().catch(e => console.warn('video play error:', e && (e.name || e.message || e))) + } + if (video.readyState >= 1) { + playVideo() + } else { + video.onloadedmetadata = playVideo + } } } catch (e) { // 权限拒绝或相机不可用 → 通知父组件降级为系统选择器 @@ -199,9 +216,9 @@ export default { } }, stopCamera() { - if (this.stream) { - this.stream.getTracks().forEach(t => t.stop()) - this.stream = null + if (this.activeStream) { + this.activeStream.getTracks().forEach(t => t.stop()) + this.activeStream = null } }, takePhoto() { @@ -373,7 +390,8 @@ export default { max-width: 340px; padding-top: 69.93%; /* 宽高比约 1.43:1 */ flex-shrink: 0; - filter: drop-shadow(0 4px 16px rgba(20, 30, 50, .12)); + /* 不用 filter: drop-shadow,iOS 上父级 filter 会导致 video 合成层不绘制(黑屏) */ + box-shadow: 0 4px 16px rgba(20, 30, 50, .12); } .corner { position: absolute; diff --git a/src/views/index/vehicleAdd.vue b/src/views/index/vehicleAdd.vue index a6067108..a6419316 100644 --- a/src/views/index/vehicleAdd.vue +++ b/src/views/index/vehicleAdd.vue @@ -457,6 +457,7 @@ t.stop()) + this.cameraStream = stream this.cameraGuideVisible = true } catch (e) { console.warn('probe getUserMedia error:', e && (e.name || e.message || e)) @@ -968,6 +971,7 @@ export default { }, handleCameraFail(e) { // 相机不可用统一处理:权限类弹确认框,其他异常提示后由用户点击触发相册(保证点击手势有效) this.cameraGuideVisible = false + this.cameraStream = null if (this.isCameraPermissionError(e)) { Dialog.confirm({ title: '提示', @@ -1002,6 +1006,7 @@ export default { }, onCameraConfirm({ blob, dataURL }) { // 拍摄确认:构造 {file, content} 走现有压缩→上传→OCR 链路 this.cameraGuideVisible = false + this.cameraStream = null const type = this.cameraGuideType const file = new File([blob], `vehicle_license_${type}_${Date.now()}.jpg`, { type: 'image/jpeg' }) const fileItem = { file, content: dataURL } @@ -1021,6 +1026,7 @@ export default { }, onCameraCancel() { // 返回放弃 this.cameraGuideVisible = false + this.cameraStream = null }, onCameraFallback(e) { // CameraGuide 内 getUserMedia 失败(前置探测已通过,概率极低):按异常类型统一处理 this.handleCameraFail(e)