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' },
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-shadowiOS 上父级 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;
+10 -1
View File
@@ -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: '拍摄行驶证'
}
},
{
+47 -56
View File
@@ -454,15 +454,6 @@
</div>
</van-popup>
<camera-guide
:visible="cameraGuideVisible"
:type="cameraGuideType"
:stream="cameraStream"
@confirm="onCameraConfirm"
@cancel="onCameraCancel"
@fallback="onCameraFallback"
/>
</div>
</template>
@@ -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
}
}
</script>