CRM_26-08-05#story#9126,车辆管理、服务商管理系统需求

This commit is contained in:
zlf
2026-08-05 18:37:22 +08:00
parent c9f5296e3c
commit cd2aaeef32
3 changed files with 75 additions and 63 deletions
+18 -6
View File
@@ -186,8 +186,8 @@ export default {
video: { facingMode: 'environment' }, video: { facingMode: 'environment' },
audio: false audio: false
}) })
// 等待期间组件已被关闭,立即释放 // 等待期间组件已被关闭或销毁,立即释放
if (!this.visible) { if (!this.visible || this._isDestroyed || this._isBeingDestroyed) {
stream.getTracks().forEach(t => t.stop()) stream.getTracks().forEach(t => t.stop())
return return
} }
@@ -363,7 +363,9 @@ export default {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding-top: 80px; padding-top: 80px;
padding-bottom: 100px; /* 避让底部拍照栏(约 142px 高),防止小屏下提示被遮挡 */
padding-bottom: 150px;
padding-bottom: calc(150px + env(safe-area-inset-bottom));
pointer-events: none; pointer-events: none;
} }
.guideHero { .guideHero {
@@ -408,16 +410,26 @@ export default {
line-height: 1.4; line-height: 1.4;
} }
} }
/* 取景框:宽约为屏宽 80%,宽高比约 1.43:1 */ /* 取景框:宽约为屏宽 80%,宽高比约 1.43:1;小屏按可视高度自适应缩小,避免遮挡上下元素 */
.guideFrameOuter { .guideFrameOuter {
position: relative; position: relative;
width: 80%; width: 80%;
max-width: 340px; /* 纵向可用高度 ≈ 100vh - (导航80 + 拍照栏150 + 提示卡92 + 底部提示92) ≈ 100vh - 420px
padding-top: 69.93%; /* 宽高比约 1.43:1 */ 换算成宽度(×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; flex-shrink: 0;
/* 不用 filter: drop-shadowiOS 上父级 filter 会导致 video 合成层不绘制(黑屏) */ /* 不用 filter: drop-shadowiOS 上父级 filter 会导致 video 合成层不绘制(黑屏) */
box-shadow: 0 4px 16px rgba(20, 30, 50, .12); 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 { .corner {
position: absolute; position: absolute;
width: 30px; width: 30px;
+10 -1
View File
@@ -209,7 +209,16 @@ const routes = [
name: 'vehicleAdd', name: 'vehicleAdd',
component: () => import('@/views/index/vehicleAdd'), component: () => import('@/views/index/vehicleAdd'),
meta: { meta: {
title: '车辆新增或修改' title: '车辆新增或修改',
cache: true // keep-alive 缓存:跳转拍摄页返回时保留表单,再次从其他页面进入时由 activated 重新初始化
}
},
{
path: "/cameraGuide",
name: 'cameraGuide',
component: () => import('@/views/index/cameraGuide'),
meta: {
title: '拍摄行驶证'
} }
}, },
{ {
+28 -37
View File
@@ -454,15 +454,6 @@
</div> </div>
</van-popup> </van-popup>
<camera-guide
:visible="cameraGuideVisible"
:type="cameraGuideType"
:stream="cameraStream"
@confirm="onCameraConfirm"
@cancel="onCameraCancel"
@fallback="onCameraFallback"
/>
</div> </div>
</template> </template>
@@ -475,7 +466,6 @@ import {vehicleTypeList,saveVehicle,getInfoById,supplierServiceTree, uploadImage
userOperationPermissions,saveSupplierApproval} from "@/api/mine" userOperationPermissions,saveSupplierApproval} from "@/api/mine"
import TwoCommonBtn from "@/components/twoBtnCommon.vue" import TwoCommonBtn from "@/components/twoBtnCommon.vue"
import CellGroup from "@/components/cellGroup.vue"; import CellGroup from "@/components/cellGroup.vue";
import CameraGuide from "@/components/CameraGuide.vue";
export default { export default {
name: "vehicleAdd", name: "vehicleAdd",
mixins:[myMixins], mixins:[myMixins],
@@ -511,9 +501,7 @@ export default {
oldSupplierServiceList:[], oldSupplierServiceList:[],
show:false, show:false,
imageUrl: require('@/assets/arr_right.png'), imageUrl: require('@/assets/arr_right.png'),
cameraGuideVisible: false, // 拍摄引导弹层显示
cameraGuideType: 'front', // 当前拍摄槽位 front/back/car cameraGuideType: 'front', // 当前拍摄槽位 front/back/car
cameraStream: null, // 探测成功的相机流,交给 CameraGuide 复用
vehicleLicenseFrontList: [], vehicleLicenseFrontList: [],
vehicleLicenseBackList: [], vehicleLicenseBackList: [],
vehicleLicenseCarPhotoList: [], vehicleLicenseCarPhotoList: [],
@@ -710,6 +698,31 @@ export default {
}, },
}, },
async mounted() { async mounted() {
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.id=this.$route.params?.id
this.approvalForm.supplierId=this.$route.params?.supplierId this.approvalForm.supplierId=this.$route.params?.supplierId
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
@@ -733,7 +746,6 @@ export default {
} }
} }
}, },
methods:{
showDateHandler() { showDateHandler() {
if( this.id ) { // 修改 if( this.id ) { // 修改
if( this.permissonList.includes('hasInsuranceAudit') ) { if( this.permissonList.includes('hasInsuranceAudit') ) {
@@ -948,30 +960,19 @@ export default {
isCameraSupported() { // 是否支持 getUserMedia(不支持则降级系统选择器) isCameraSupported() { // 是否支持 getUserMedia(不支持则降级系统选择器)
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia) && window.isSecureContext !== false return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia) && window.isSecureContext !== false
}, },
async onLicenseSlotClick(type) { // 行驶证三槽位点击拦截:前置探测相机,可用才进引导页,否则直接降级系统选择器 onLicenseSlotClick(type) { // 行驶证三槽位点击拦截:跳独立拍摄页,不支持相机则直接降级系统选择器
this.cameraGuideType = type this.cameraGuideType = type
if (!this.isCameraSupported()) { if (!this.isCameraSupported()) {
// 不支持 getUserMedia(如 iOS 微信、非 HTTPS、老 WebView):直接调系统选择器 // 不支持 getUserMedia(如 iOS 微信、非 HTTPS、老 WebView):直接调系统选择器
this.triggerNativeUpload(type) this.triggerNativeUpload(type)
return return
} }
try { this.$router.push({ name: 'cameraGuide', query: { type } })
// 前置真实探测相机可用性;探测成功的流直接交给 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)
}
}, },
isCameraPermissionError(e) { // 是否为相机权限类错误 isCameraPermissionError(e) { // 是否为相机权限类错误
return !!(e && (e.name === 'NotAllowedError' || e.name === 'SecurityError' || e.name === 'PermissionDeniedError')) return !!(e && (e.name === 'NotAllowedError' || e.name === 'SecurityError' || e.name === 'PermissionDeniedError'))
}, },
handleCameraFail(e) { // 相机不可用统一处理:权限类弹确认框,其他异常提示后由用户点击触发相册(保证点击手势有效) handleCameraFail(e) { // 相机不可用统一处理:权限类弹确认框,其他异常提示后由用户点击触发相册(保证点击手势有效)
this.cameraGuideVisible = false
this.cameraStream = null
if (this.isCameraPermissionError(e)) { if (this.isCameraPermissionError(e)) {
Dialog.confirm({ Dialog.confirm({
title: '提示', title: '提示',
@@ -1005,8 +1006,6 @@ export default {
}) })
}, },
onCameraConfirm({ blob, dataURL }) { // 拍摄确认:构造 {file, content} 走现有压缩→上传→OCR 链路 onCameraConfirm({ blob, dataURL }) { // 拍摄确认:构造 {file, content} 走现有压缩→上传→OCR 链路
this.cameraGuideVisible = false
this.cameraStream = null
const type = this.cameraGuideType const type = this.cameraGuideType
const file = new File([blob], `vehicle_license_${type}_${Date.now()}.jpg`, { type: 'image/jpeg' }) const file = new File([blob], `vehicle_license_${type}_${Date.now()}.jpg`, { type: 'image/jpeg' })
const fileItem = { file, content: dataURL } const fileItem = { file, content: dataURL }
@@ -1024,13 +1023,6 @@ export default {
this[listMap[type]] = [fileItem] this[listMap[type]] = [fileItem]
this[handlerMap[type]](fileItem) this[handlerMap[type]](fileItem)
}, },
onCameraCancel() { // 返回放弃
this.cameraGuideVisible = false
this.cameraStream = null
},
onCameraFallback(e) { // CameraGuide 内 getUserMedia 失败(前置探测已通过,概率极低):按异常类型统一处理
this.handleCameraFail(e)
},
async vehicleLicenseFrontHandler(file) { // 上传 行驶证首页 async vehicleLicenseFrontHandler(file) { // 上传 行驶证首页
console.log('file',file) console.log('file',file)
try { try {
@@ -1702,8 +1694,7 @@ export default {
}, },
components:{ components:{
CellGroup, CellGroup,
TwoCommonBtn, TwoCommonBtn
CameraGuide
} }
} }
</script> </script>