@@ -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)