diff --git a/src/components/CameraGuide.vue b/src/components/CameraGuide.vue index 7f09f246..133bfe5c 100644 --- a/src/components/CameraGuide.vue +++ b/src/components/CameraGuide.vue @@ -186,8 +186,8 @@ export default { video: { facingMode: 'environment' }, audio: false }) - // 等待期间组件已被关闭,立即释放 - if (!this.visible) { + // 等待期间组件已被关闭或销毁,立即释放 + if (!this.visible || this._isDestroyed || this._isBeingDestroyed) { stream.getTracks().forEach(t => t.stop()) return } @@ -363,7 +363,9 @@ export default { align-items: center; justify-content: center; padding-top: 80px; - padding-bottom: 100px; + /* 避让底部拍照栏(约 142px 高),防止小屏下提示被遮挡 */ + padding-bottom: 150px; + padding-bottom: calc(150px + env(safe-area-inset-bottom)); pointer-events: none; } .guideHero { @@ -408,16 +410,26 @@ export default { line-height: 1.4; } } -/* 取景框:宽约为屏宽 80%,宽高比约 1.43:1 */ +/* 取景框:宽约为屏宽 80%,宽高比约 1.43:1;小屏按可视高度自适应缩小,避免遮挡上下元素 */ .guideFrameOuter { position: relative; width: 80%; - max-width: 340px; - padding-top: 69.93%; /* 宽高比约 1.43:1 */ + /* 纵向可用高度 ≈ 100vh - (导航80 + 拍照栏150 + 提示卡92 + 底部提示92) ≈ 100vh - 420px, + 换算成宽度(×1.43)并限制在 170~340px 之间,大屏表现与原 80%/340px 一致 */ + width: clamp(170px, min(80%, calc((100vh - 420px) * 1.43)), 340px); + width: clamp(170px, min(80%, calc((100dvh - 420px) * 1.43)), 340px); + /* 宽高比约 1.43:1(与拍照裁剪比例一致);不用 padding-top 百分比——它相对父级宽度,无法跟随自适应宽度 */ + aspect-ratio: 1.43 / 1; flex-shrink: 0; /* 不用 filter: drop-shadow,iOS 上父级 filter 会导致 video 合成层不绘制(黑屏) */ box-shadow: 0 4px 16px rgba(20, 30, 50, .12); } +/* 兼容不支持 aspect-ratio 的旧浏览器:padding-top 相对父级宽度,80% / 1.43 ≈ 55.94% */ +@supports not (aspect-ratio: 1 / 1) { + .guideFrameOuter { + padding-top: 55.94%; + } +} .corner { position: absolute; width: 30px; diff --git a/src/router/index.js b/src/router/index.js index 21aca5b7..ed4231b5 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -209,7 +209,16 @@ const routes = [ name: 'vehicleAdd', component: () => import('@/views/index/vehicleAdd'), meta: { - title: '车辆新增或修改' + title: '车辆新增或修改', + cache: true // keep-alive 缓存:跳转拍摄页返回时保留表单,再次从其他页面进入时由 activated 重新初始化 + } + }, + { + path: "/cameraGuide", + name: 'cameraGuide', + component: () => import('@/views/index/cameraGuide'), + meta: { + title: '拍摄行驶证' } }, { diff --git a/src/views/index/vehicleAdd.vue b/src/views/index/vehicleAdd.vue index a6419316..19172d83 100644 --- a/src/views/index/vehicleAdd.vue +++ b/src/views/index/vehicleAdd.vue @@ -454,15 +454,6 @@ - - @@ -475,7 +466,6 @@ import {vehicleTypeList,saveVehicle,getInfoById,supplierServiceTree, uploadImage userOperationPermissions,saveSupplierApproval} from "@/api/mine" import TwoCommonBtn from "@/components/twoBtnCommon.vue" import CellGroup from "@/components/cellGroup.vue"; -import CameraGuide from "@/components/CameraGuide.vue"; export default { name: "vehicleAdd", mixins:[myMixins], @@ -511,9 +501,7 @@ export default { oldSupplierServiceList:[], show:false, imageUrl: require('@/assets/arr_right.png'), - cameraGuideVisible: false, // 拍摄引导弹层显示 cameraGuideType: 'front', // 当前拍摄槽位 front/back/car - cameraStream: null, // 探测成功的相机流,交给 CameraGuide 复用 vehicleLicenseFrontList: [], vehicleLicenseBackList: [], vehicleLicenseCarPhotoList: [], @@ -710,30 +698,54 @@ export default { }, }, async mounted() { - this.id=this.$route.params?.id - this.approvalForm.supplierId=this.$route.params?.supplierId - const urlParams = new URLSearchParams(window.location.search); - const rawId = this.$route.query.supplierId || urlParams.get('supplierId'); - this.supplierId = rawId ? Number(rawId) : ''; - await this.getSupplierServiceTree(); - await this.getTypeList(); - if( this.id){ - await this.vehicleInfo() - }else{ - this.originData={ - vehicleType:'', - vehicleStatus:'', - virtualVehicle:'', - serviceIds:[], - hasLiabilityInsurance:'', - insuranceCorp:'', - liabilityInsuranceStartTime:'', - liabilityInsuranceEndTime:'', - insurancePicturePhoto:'' + await this.initPage() + this.hasInited = true // 标记首次初始化完成(keep-alive 下 activated 据此判断再次进入需重新初始化) + }, + activated() { + // 从拍摄页返回:取回拍照结果/降级信息,保留表单不重新初始化 + const cam = this.$store.state.camera + if (cam.result || cam.fallback) { + const { result, fallback } = cam + this.$store.commit('camera/clear') + if (result) { + this.cameraGuideType = result.type + this.onCameraConfirm(result) + } else { + this.cameraGuideType = fallback.type + this.handleCameraFail(fallback.error) } + return + } + // 从车辆管理等其他页面再次进入(keep-alive 缓存复用):按当前路由重新初始化 + if (this.hasInited) { + this.initPage() } }, methods:{ + async initPage() { + this.id=this.$route.params?.id + this.approvalForm.supplierId=this.$route.params?.supplierId + const urlParams = new URLSearchParams(window.location.search); + const rawId = this.$route.query.supplierId || urlParams.get('supplierId'); + this.supplierId = rawId ? Number(rawId) : ''; + await this.getSupplierServiceTree(); + await this.getTypeList(); + if( this.id){ + await this.vehicleInfo() + }else{ + this.originData={ + vehicleType:'', + vehicleStatus:'', + virtualVehicle:'', + serviceIds:[], + hasLiabilityInsurance:'', + insuranceCorp:'', + liabilityInsuranceStartTime:'', + liabilityInsuranceEndTime:'', + insurancePicturePhoto:'' + } + } + }, showDateHandler() { if( this.id ) { // 修改 if( this.permissonList.includes('hasInsuranceAudit') ) { @@ -948,30 +960,19 @@ export default { isCameraSupported() { // 是否支持 getUserMedia(不支持则降级系统选择器) return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia) && window.isSecureContext !== false }, - async onLicenseSlotClick(type) { // 行驶证三槽位点击拦截:前置探测相机,可用才进引导页,否则直接降级系统选择器 + onLicenseSlotClick(type) { // 行驶证三槽位点击拦截:跳独立拍摄页,不支持相机则直接降级系统选择器 this.cameraGuideType = type if (!this.isCameraSupported()) { // 不支持 getUserMedia(如 iOS 微信、非 HTTPS、老 WebView):直接调系统选择器 this.triggerNativeUpload(type) return } - try { - // 前置真实探测相机可用性;探测成功的流直接交给 CameraGuide 复用, - // 避免 iOS 上 stop 后立即二次 getUserMedia 拿到不渲染的“死流”(黑屏) - const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false }) - this.cameraStream = stream - this.cameraGuideVisible = true - } catch (e) { - console.warn('probe getUserMedia error:', e && (e.name || e.message || e)) - this.handleCameraFail(e) - } + this.$router.push({ name: 'cameraGuide', query: { type } }) }, isCameraPermissionError(e) { // 是否为相机权限类错误 return !!(e && (e.name === 'NotAllowedError' || e.name === 'SecurityError' || e.name === 'PermissionDeniedError')) }, handleCameraFail(e) { // 相机不可用统一处理:权限类弹确认框,其他异常提示后由用户点击触发相册(保证点击手势有效) - this.cameraGuideVisible = false - this.cameraStream = null if (this.isCameraPermissionError(e)) { Dialog.confirm({ title: '提示', @@ -1005,8 +1006,6 @@ 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 } @@ -1024,13 +1023,6 @@ export default { this[listMap[type]] = [fileItem] this[handlerMap[type]](fileItem) }, - onCameraCancel() { // 返回放弃 - this.cameraGuideVisible = false - this.cameraStream = null - }, - onCameraFallback(e) { // CameraGuide 内 getUserMedia 失败(前置探测已通过,概率极低):按异常类型统一处理 - this.handleCameraFail(e) - }, async vehicleLicenseFrontHandler(file) { // 上传 行驶证首页 console.log('file',file) try { @@ -1702,8 +1694,7 @@ export default { }, components:{ CellGroup, - TwoCommonBtn, - CameraGuide + TwoCommonBtn } }