Files
tms-erp-web/src/views/transportCapacity/vehicle.vue
T
gxwebsoft bcdd412d95 style(master-order-dispatch): 修复起终路由条左侧节点被裁切问题
- 修改 .route-overview 由右对齐改为左对齐,调整 padding 保证滚动时两端节点完整显示
- 调整 .route-overview__node 的 flex 属性,限制最小和最大宽度,添加文本溢出省略样式防止撑爆
- 优化 .route-overview__line 的 flex 属性,使其可收缩,提升滚动时平滑度
- 保留 1200px 媒体查询中原有的 justify-content 设置,避免影响现有布局
- 统一给各 transportCapacity 相关 vue 文件中的 .el-form-item 添加了底部外边距样式,增强表单间距
- 调整 element-ui 的 .el-col 底部外边距为0,统一样式规范
2026-08-21 16:03:02 +08:00

1544 lines
52 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<basic-container class="transport-vehicle-page">
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
v-model="form"
ref="crud"
:permission="permissionList"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="primary"
icon="el-icon-plus"
v-if="hasPermission('transport_vehicle_add')"
@click="openVehicle()"
>新增
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="hasPermission('transport_vehicle_export')"
@click="handleExport"
>导出
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="hasPermission('transport_vehicle_delete')"
@click="handleDelete"
>批量删除
</el-button>
</template>
<template #search-menu>
<div class="transport-vehicle-page__expiry-tags">
<el-check-tag
v-for="item in expiryTagOptions"
:key="item.value"
:checked="query.expireStatus === item.value"
@change="handleExpireChange(item.value)"
>
{{ item.label }}{{ expiryStat[item.statKey] || 0 }}
</el-check-tag>
</div>
</template>
<template #plateNo="{ row }">
<el-button type="primary" link @click="openVehicle(row, true)">
{{ row.plateNo }}
</el-button>
</template>
<template #organizationName="{ row }">
{{ String(row.organizationName || '').replace(/^[\s\u3000]+/, '') }}
</template>
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'primary' : 'info'">
{{ row.status === 1 ? '启用' : '停用' }}
</el-tag>
</template>
<template #boundDriver>
<span>-</span>
</template>
<template #certificationStatus="{ row }">
<el-tag :type="getCertificationStatus(row).type">
{{ getCertificationStatus(row).label }}
</el-tag>
</template>
<template #drivingLicenseEndDate="{ row }">
<span :class="getDateClass(row, 'drivingLicenseEndDate', 'drivingLicenseLongTerm')">
{{ formatEndDate(row.drivingLicenseEndDate, row.drivingLicenseLongTerm) }}
</span>
</template>
<template #roadTransportCertEndDate="{ row }">
<span :class="getDateClass(row, 'roadTransportCertEndDate', 'roadTransportCertLongTerm')">
{{ formatEndDate(row.roadTransportCertEndDate, row.roadTransportCertLongTerm) }}
</span>
</template>
<template #annualReviewEndDate="{ row }">
<span :class="getDateClass(row, 'annualReviewEndDate', 'annualReviewLongTerm')">
{{ formatEndDate(row.annualReviewEndDate, row.annualReviewLongTerm) }}
</span>
</template>
<template #menu="{ row }">
<el-link
type="primary"
v-if="hasPermission('transport_vehicle_view')"
@click="openVehicle(row, true)"
>
查看
</el-link>
<el-link
type="primary"
v-if="hasPermission('transport_vehicle_edit')"
@click="openVehicle(row)"
>
修改
</el-link>
<el-link type="primary" @click="openVehicleLedger(row)">综合台账</el-link>
<el-link type="primary" v-if="row.certificationStatus === 0" @click="openCertificationAudit(row)">
审核
</el-link>
<el-link
type="primary"
v-if="hasPermission('transport_vehicle_status')"
@click="handleStatus(row)"
>
{{ row.status === 1 ? '停用' : '启用' }}
</el-link>
</template>
</avue-crud>
<vehicle-ledger v-model="vehicleLedgerBox" :vehicle="ledgerVehicle" />
<el-dialog v-model="certificationAuditBox" title="车辆认证审核" width="92%" top="4vh">
<el-descriptions :column="3" border>
<el-descriptions-item v-for="item in auditDetailItems" :key="item.label" :label="item.label">
{{ formatAuditValue(item) }}
</el-descriptions-item>
</el-descriptions>
<el-form v-if="showRejectReason" class="certification-audit__reason" label-position="right" label-width="auto">
<el-form-item label="驳回原因" required>
<el-input v-model="certificationRejectReason" type="textarea" maxlength="200" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="certificationAuditBox = false">取消</el-button>
<el-button type="danger" @click="handleCertificationReject">认证驳回</el-button>
<el-button type="primary" @click="handleCertificationApprove">认证通过</el-button>
</template>
</el-dialog>
<el-dialog
:title="dialogTitle"
append-to-body
v-model="vehicleBox"
width="92%"
top="4vh"
class="vehicle-dialog"
@closed="resetVehicle"
>
<el-form
ref="vehicleForm"
:model="vehicleForm"
:rules="formRules"
label-position="right"
:disabled="readonly"
class="vehicle-form archive-form"
>
<section-card title="基础车辆信息">
<el-row :gutter="18">
<el-col :span="24">
<el-form-item label="所有组织" prop="organizationName">
<el-select
v-model="vehicleForm.organizationName"
filterable
clearable
placeholder="请选择"
>
<el-option
v-for="item in organizationOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="10">
<el-form-item label="车牌号" prop="plateNo">
<div class="plate-group">
<el-select
v-model="vehicleForm.plateProvince"
placeholder="请选择"
filterable
@change="syncPlateNo(false)"
>
<el-option
v-for="item in plateProvinceOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-input
v-model="vehicleForm.plateNoBody"
maxlength="7"
show-word-limit
@input="handlePlateBodyInput"
/>
<el-select
v-model="vehicleForm.plateColor"
placeholder="请选择"
@change="syncPlateNo(false)"
>
<el-option
v-for="item in plateColorOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="车辆类型" prop="vehicleType">
<el-select
v-model="vehicleForm.vehicleType"
filterable
allow-create
default-first-option
>
<el-option
v-for="item in vehicleTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="外廓尺寸(毫米)">
<div class="dimension-group">
<div class="dimension-group__item">
<span class="dimension-group__label"></span>
<el-input
v-model="vehicleForm.outerLength"
inputmode="numeric"
@input="vehicleForm.outerLength = digitsOnly($event)"
/>
</div>
<div class="dimension-group__item">
<span class="dimension-group__label"></span>
<el-input
v-model="vehicleForm.outerWidth"
inputmode="numeric"
@input="vehicleForm.outerWidth = digitsOnly($event)"
/>
</div>
<div class="dimension-group__item">
<span class="dimension-group__label"></span>
<el-input
v-model="vehicleForm.outerHeight"
inputmode="numeric"
@input="vehicleForm.outerHeight = digitsOnly($event)"
/>
</div>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="核定载质量(KG)" prop="approvedLoadKg">
<el-input
v-model="vehicleForm.approvedLoadKg"
inputmode="numeric"
@input="vehicleForm.approvedLoadKg = digitsOnly($event)"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="准牵引总质量(KG)" prop="tractionMassKg">
<el-input
v-model="vehicleForm.tractionMassKg"
inputmode="numeric"
@input="vehicleForm.tractionMassKg = digitsOnly($event)"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="业务关系" prop="businessRelation">
<el-select v-model="vehicleForm.businessRelation">
<el-option
v-for="item in businessRelationOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="能源类型" prop="energyType">
<el-select
v-model="vehicleForm.energyType"
filterable
allow-create
default-first-option
>
<el-option
v-for="item in energyTypeOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="强制报废日期" prop="compulsoryScrapDate">
<div class="date-with-flag">
<el-date-picker
v-model="vehicleForm.compulsoryScrapDate"
type="date"
value-format="YYYY-MM-DD"
:disabled="vehicleForm.compulsoryScrapLongTerm === 1"
/>
<el-checkbox
v-model="vehicleForm.compulsoryScrapLongTerm"
:true-label="1"
:false-label="0"
>
长期有效
</el-checkbox>
</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="海关备案号" prop="customsRecordNo">
<el-input v-model="vehicleForm.customsRecordNo" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
</section-card>
<section-card title="车辆资质图片">
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="行驶证档案编号" prop="drivingLicenseNo">
<el-input v-model="vehicleForm.drivingLicenseNo" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="行驶证有效期" prop="drivingLicenseEndDate">
<div class="date-with-flag">
<el-date-picker
v-model="vehicleForm.drivingLicenseEndDate"
type="date"
value-format="YYYY-MM-DD"
/>
<el-checkbox
v-model="vehicleForm.drivingLicenseLongTerm"
:true-label="1"
:false-label="0"
>
长期有效
</el-checkbox>
</div>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="道路运输证号" prop="roadTransportCertNo">
<el-input v-model="vehicleForm.roadTransportCertNo" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="道路运输证有效期" prop="roadTransportCertEndDate">
<div class="date-with-flag">
<el-date-picker
v-model="vehicleForm.roadTransportCertEndDate"
type="date"
value-format="YYYY-MM-DD"
/>
<el-checkbox
v-model="vehicleForm.roadTransportCertLongTerm"
:true-label="1"
:false-label="0"
>
长期有效
</el-checkbox>
</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="道路运输年审有效期" prop="annualReviewEndDate">
<div class="date-with-flag">
<el-date-picker
v-model="vehicleForm.annualReviewEndDate"
type="date"
value-format="YYYY-MM-DD"
:disabled="vehicleForm.annualReviewLongTerm === 1"
/>
<el-checkbox
v-model="vehicleForm.annualReviewLongTerm"
:true-label="1"
:false-label="0"
>
长期有效
</el-checkbox>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="机动车登记编号" prop="registrationNo">
<el-input v-model="vehicleForm.registrationNo" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="机动车登记日期" prop="registrationDate">
<el-date-picker
v-model="vehicleForm.registrationDate"
type="date"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="8">
<image-upload-field
label="行驶证主页正面"
prop="drivingLicenseImage"
:value="vehicleForm.drivingLicenseImage"
:readonly="readonly"
:headers="uploadHeaders"
large
class-prefix="vehicle"
@success="
(url, file) =>
handleVehicleCertificateUploadSuccess('drivingLicenseImage', url, file)
"
/>
</el-col>
<el-col :span="8">
<image-upload-field
label="行驶证主页反面"
prop="drivingLicenseMainBack"
:value="vehicleForm.drivingLicenseMainBack"
:readonly="readonly"
:headers="uploadHeaders"
large
class-prefix="vehicle"
@success="
(url, file) =>
handleVehicleCertificateUploadSuccess('drivingLicenseMainBack', url, file)
"
/>
</el-col>
<el-col :span="8">
<image-upload-field
label="行驶证副页正面"
prop="drivingLicenseViceFront"
:value="vehicleForm.drivingLicenseViceFront"
:readonly="readonly"
:headers="uploadHeaders"
large
class-prefix="vehicle"
@success="
(url, file) =>
handleVehicleCertificateUploadSuccess('drivingLicenseViceFront', url, file)
"
/>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="8">
<image-upload-field
label="行驶证副页反面"
prop="drivingLicenseViceBack"
:value="vehicleForm.drivingLicenseViceBack"
:readonly="readonly"
:headers="uploadHeaders"
large
class-prefix="vehicle"
@success="
(url, file) =>
handleVehicleCertificateUploadSuccess('drivingLicenseViceBack', url, file)
"
/>
</el-col>
<el-col :span="8">
<image-upload-field
label="道路运输证图片"
prop="roadTransportCertImage"
:value="vehicleForm.roadTransportCertImage"
:readonly="readonly"
:headers="uploadHeaders"
large
class-prefix="vehicle"
@success="url => handleVehicleCertificateUploadSuccess('roadTransportCertImage', url)"
/>
</el-col>
<el-col :span="8">
<image-upload-field
label="机动车登记本"
prop="registrationImage"
:value="vehicleForm.registrationImage"
:readonly="readonly"
:headers="uploadHeaders"
large
class-prefix="vehicle"
@success="
(url, file) =>
handleVehicleCertificateUploadSuccess('registrationImage', url, file)
"
/>
</el-col>
</el-row>
</section-card>
</el-form>
<template #footer>
<el-button @click="vehicleBox = false">取消</el-button>
<el-button type="primary" v-if="!readonly" :loading="submitLoading" @click="handleSubmit">
确定
</el-button>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import { ElLoading } from 'element-plus';
import { option } from '@/option/transportCapacity/transport-vehicle';
import {
getList,
getDetail,
submit,
remove,
changeStatus,
auditCertification,
getExpiryStat,
recognitionTransportCertificates,
recognizeBaiduOcr,
} from '@/api/transportCapacity/transport-vehicle';
import { getDeptTree } from '@/api/system/dept';
import { exportBlob } from '@/api/common';
import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { downloadXls } from '@/utils/util';
import ImageUploadField from '@/components/image-upload-field/main.vue';
import VehicleLedger from './components/vehicle-ledger.vue';
const emptyForm = () => ({
id: '',
organizationName: '',
plateProvince: '',
plateNoBody: '',
plateNo: '',
plateColor: '',
vehicleType: '',
outerLength: undefined,
outerWidth: undefined,
outerHeight: undefined,
approvedLoadKg: undefined,
tractionMassKg: undefined,
businessRelation: '自有',
energyType: '',
compulsoryScrapDate: '',
compulsoryScrapLongTerm: 0,
customsRecordNo: '',
drivingLicenseNo: '',
drivingLicenseStartDate: '',
drivingLicenseEndDate: '',
drivingLicenseLongTerm: 0,
drivingLicenseImage: '',
drivingLicenseMainBack: '',
drivingLicenseViceFront: '',
drivingLicenseViceBack: '',
roadTransportCertNo: '',
roadTransportCertStartDate: '',
roadTransportCertEndDate: '',
roadTransportCertLongTerm: 0,
roadTransportCertImage: '',
annualReviewEndDate: '',
annualReviewLongTerm: 0,
registrationNo: '',
registrationDate: '',
registrationImage: '',
status: 1,
});
const plateProvinceOptions = [
'京',
'津',
'沪',
'渝',
'冀',
'豫',
'云',
'辽',
'黑',
'湘',
'皖',
'鲁',
'新',
'苏',
'浙',
'赣',
'鄂',
'桂',
'甘',
'晋',
'蒙',
'陕',
'吉',
'闽',
'贵',
'粤',
'青',
'藏',
'川',
'宁',
'琼',
'港',
'澳',
].map(item => ({ label: item, value: item }));
const plateColorOptions = ['黄牌', '蓝牌', '绿牌'].map(item => ({ label: item, value: item }));
export default {
components: {
ImageUploadField,
VehicleLedger,
},
data() {
return {
option,
form: {},
query: {
expireStatus: '',
},
searchForm: {
businessRelation: '',
},
loading: true,
submitLoading: false,
data: [],
ids: '',
selectionList: [],
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
expiryStat: {
total: 0,
within30: 0,
expired: 0,
},
vehicleBox: false,
vehicleLedgerBox: false,
ledgerVehicle: {},
certificationAuditBox: false,
certificationAuditForm: {},
certificationRejectReason: '',
showRejectReason: false,
readonly: false,
vehicleForm: emptyForm(),
vehicleCertificateUploads: {},
uploadHeaders: getUploadHeaders(),
organizationOptions: [],
plateProvinceOptions,
plateColorOptions,
vehicleTypeOptions: [
{ label: '高栏车', value: '高栏' },
{ label: '低栏车', value: '低栏' },
{ label: '平板车', value: '平板' },
{ label: '厢式车', value: '厢式' },
{ label: '拖挂车', value: '拖挂' },
{ label: '半挂车', value: '半挂' },
],
businessRelationOptions: ['自有', '管理挂靠', '业务合作'],
energyTypeOptions: ['柴油', '汽油', '新能源', '天然气'],
formRules: {
organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
plateNo: [
{ required: true, message: '请输入车牌号', trigger: 'change' },
{ validator: this.validatePlateNo, trigger: 'change' },
],
vehicleType: [{ required: true, message: '请选择车辆类型', trigger: 'change' }],
approvedLoadKg: [{ required: true, message: '请输入核定载质量', trigger: 'blur' }],
businessRelation: [{ required: true, message: '请选择业务关系', trigger: 'change' }],
energyType: [{ required: true, message: '请选择能源类型', trigger: 'change' }],
drivingLicenseNo: [{ required: true, message: '请输入行驶证档案编号', trigger: 'blur' }],
drivingLicenseEndDate: [
{ required: true, message: '请选择行驶证有效期', trigger: 'change' },
],
roadTransportCertNo: [{ required: true, message: '请输入道路运输证号', trigger: 'blur' }],
roadTransportCertEndDate: [
{ required: true, message: '请选择道路运输证有效期', trigger: 'change' },
],
annualReviewEndDate: [{ validator: this.validateAnnualReviewEndDate, trigger: 'change' }],
registrationNo: [{ required: true, message: '请输入机动车登记编号', trigger: 'blur' }],
},
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
const authority = this.userInfo.authority || '';
return authority.includes('admin');
},
permissionList() {
return {
addBtn: this.hasPermission('transport_vehicle_add'),
viewBtn: this.hasPermission('transport_vehicle_view'),
delBtn: this.hasPermission('transport_vehicle_delete'),
editBtn: this.hasPermission('transport_vehicle_edit'),
};
},
dialogTitle() {
if (this.readonly) {
return '查看车辆';
}
return this.vehicleForm.id ? '修改车辆' : '新增车辆';
},
auditDetailItems() {
return [
['所属组织', 'organizationName'], ['车牌号', 'plateNo'], ['车牌颜色', 'plateColor'],
['车辆类型', 'vehicleType'], ['外廓长度(mm)', 'outerLength'], ['外廓宽度(mm)', 'outerWidth'],
['外廓高度(mm)', 'outerHeight'], ['核定载质量(KG)', 'approvedLoadKg'], ['准牵引总质量(KG)', 'tractionMassKg'],
['业务关系', 'businessRelation'], ['能源类型', 'energyType'], ['强制报废日期', 'compulsoryScrapDate'],
['海关备案号', 'customsRecordNo'], ['行驶证档案编号', 'drivingLicenseNo'], ['行驶证有效期起', 'drivingLicenseStartDate'],
['行驶证有效期止', 'drivingLicenseEndDate'], ['道路运输证号', 'roadTransportCertNo'], ['道路运输证有效期起', 'roadTransportCertStartDate'],
['道路运输证有效期止', 'roadTransportCertEndDate'], ['道路运输年审有效期', 'annualReviewEndDate'],
['机动车登记编号', 'registrationNo'], ['机动车登记日期', 'registrationDate'], ['备注', 'remark'],
].map(([label, prop]) => ({ label, prop }));
},
expiryTagOptions() {
return [
{ label: '全部', value: '', statKey: 'total' },
{ label: '30天内到期', value: 'within30', statKey: 'within30' },
{ label: '已到期', value: 'expired', statKey: 'expired' },
];
},
},
created() {
this.initDeptTree();
},
methods: {
hasPermission(code) {
return this.isAdmin || this.permission?.[code] === true;
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => {
this.organizationOptions = this.flattenDeptOptions(res.data.data || []);
const column = this.findColumn(this.option.column, 'organizationName');
column.dicData = this.organizationOptions;
if (this.vehicleBox && !this.vehicleForm.id && !this.vehicleForm.organizationName) {
this.vehicleForm.organizationName = this.getCurrentOrganizationName();
}
});
},
flattenDeptOptions(tree = [], level = 0) {
const result = [];
tree.forEach(item => {
const rawLabel = item.title || item.deptName || item.name;
result.push({
label: `${' '.repeat(level)}${rawLabel}`,
value: rawLabel,
rawLabel,
id: item.id || item.value,
});
if (item.children && item.children.length) {
result.push(...this.flattenDeptOptions(item.children, level + 1));
}
});
return result;
},
getCurrentOrganizationName() {
const currentDeptId = this.userInfo.deptId || this.userInfo.dept_id;
const currentDept = this.organizationOptions.find(
item => String(item.id) === String(currentDeptId)
);
return currentDept?.rawLabel || this.userInfo.deptName || this.userInfo.dept_name || '';
},
digitsOnly(value = '') {
return String(value).replace(/\D/g, '');
},
validatePlateNo(rule, value, callback) {
const plateProvince = String(this.vehicleForm.plateProvince || '').trim();
const plateNoBody = String(this.vehicleForm.plateNoBody || '')
.trim()
.toUpperCase();
const plateColor = String(this.vehicleForm.plateColor || '').trim();
if (!plateProvince || !plateNoBody || !plateColor) {
callback(new Error('请完整填写车牌号'));
return;
}
if (!/^[A-Z][A-Z0-9挂学警港澳]{5,6}$/.test(plateNoBody)) {
callback(new Error('请输入标准车牌号'));
return;
}
callback();
},
validateAnnualReviewEndDate(rule, value, callback) {
if (this.vehicleForm.annualReviewLongTerm !== 1 && !value) {
callback(new Error('请选择道路运输年审有效期'));
return;
}
callback();
},
openVehicle(row, readonly = false) {
this.readonly = readonly;
if (!row?.id) {
this.vehicleForm = emptyForm();
this.vehicleForm.organizationName = this.getCurrentOrganizationName();
this.syncPlateNo(false);
this.vehicleBox = true;
return;
}
getDetail(row.id).then(res => {
this.vehicleForm = {
...emptyForm(),
...(res.data.data || {}),
};
this.splitPlateNo();
this.syncPlateNo(false);
this.vehicleBox = true;
});
},
openVehicleLedger(row) {
this.ledgerVehicle = { ...row };
this.vehicleLedgerBox = true;
},
openCertificationAudit(row) {
getDetail(row.id).then(res => {
this.certificationAuditForm = { ...(res.data.data || {}) };
this.certificationRejectReason = '';
this.showRejectReason = false;
this.certificationAuditBox = true;
});
},
formatAuditValue(item) {
const value = this.certificationAuditForm[item.prop];
return value === undefined || value === null || value === '' ? '-' : value;
},
handleCertificationApprove() {
auditCertification(this.certificationAuditForm.id, 1).then(() => {
this.$message.success('认证已通过');
this.certificationAuditBox = false;
this.onLoad(this.page);
});
},
handleCertificationReject() {
if (!this.showRejectReason) { this.showRejectReason = true; return; }
if (!this.certificationRejectReason.trim()) { this.$message.warning('请输入认证驳回原因'); return; }
auditCertification(this.certificationAuditForm.id, 2, this.certificationRejectReason).then(() => {
this.$message.success('认证已驳回');
this.certificationAuditBox = false;
this.onLoad(this.page);
});
},
resetVehicle() {
this.vehicleForm = emptyForm();
this.vehicleCertificateUploads = {};
this.readonly = false;
this.submitLoading = false;
this.$refs.vehicleForm?.clearValidate();
},
setImage(prop, url) {
this.vehicleForm[prop] = url;
this.$refs.vehicleForm?.validateField(prop);
},
handleVehicleCertificateUploadSuccess(prop, url) {
this.setImage(prop, url);
this.vehicleCertificateUploads[prop] = url;
if (prop.startsWith('drivingLicense')) {
if (prop.endsWith('Back')) {
this.recognizeBaiduVehicleOcr(url, 'general', '', '行驶证', data => {
this.applyBaiduVehicleLicenseGeneralRecognition(data);
});
return;
}
const side = prop === 'drivingLicenseViceFront' ? 'back' : 'front';
this.recognizeBaiduVehicleOcr(url, 'vehicle_license', side, '行驶证', data => {
this.applyBaiduVehicleLicenseRecognition(data);
});
return;
}
if (prop === 'roadTransportCertImage') {
this.recognizeBaiduVehicleOcr(
url,
'road_transport_certificate',
'',
'道路运输证',
data => {
this.applyBaiduRoadTransportCertificateRecognition(data);
}
);
return;
}
this.recognizeVehicleCertificates();
},
recognizeBaiduVehicleOcr(url, type, side, documentName, applyRecognition) {
if (!url) {
this.$message.warning(`${documentName}图片上传成功,未获取到图片地址,无法自动识别`);
return;
}
const loading = ElLoading.service({
lock: true,
text: `${documentName}识别中`,
background: 'rgba(255, 255, 255, 0.7)',
});
recognizeBaiduOcr(url, type, side)
.then(res => {
applyRecognition(res.data.data?.result || {});
this.$message.success(`${documentName}识别完成`);
})
.catch(() => {
this.$message.warning(`${documentName}图片上传成功,自动识别失败,请手动填写车辆资质信息`);
})
.finally(() => {
loading.close();
});
},
recognizeVehicleCertificates() {
const objectKeys = [
this.vehicleCertificateUploads.registrationImage || this.vehicleForm.registrationImage,
].filter(Boolean);
if (!objectKeys.length) {
this.$message.warning('车辆资质图片上传成功,未获取到图片地址,无法自动识别');
return;
}
const loading = ElLoading.service({
lock: true,
text: '车辆资质识别中',
background: 'rgba(255, 255, 255, 0.7)',
});
recognitionTransportCertificates(objectKeys)
.then(res => {
this.applyVehicleCertificateRecognition(res.data.data || {});
this.$message.success('车辆资质识别完成');
})
.catch(() => {
this.$message.warning('车辆资质图片上传成功,自动识别失败,请手动填写车辆资质信息');
})
.finally(() => {
loading.close();
});
},
applyBaiduVehicleLicenseRecognition(data = {}) {
const plateNo = this.getBaiduOcrValue(data, ['号牌号码', '车牌号码']);
const vehicleType = this.getBaiduOcrValue(data, ['车辆类型']);
const drivingLicenseNo = this.getBaiduOcrValue(data, ['档案编号']);
const inspectionValidity = this.getBaiduOcrValue(data, ['检验有效期至', '检验有效期', '检验记录']);
const registrationDate = this.getBaiduOcrValue(data, ['注册日期']);
const registrationNo = this.getBaiduOcrValue(data, ['车辆识别代号', '车架号']);
const approvedLoadKg = this.getBaiduOcrValue(data, ['核定载质量']);
const outerDimensions = this.getBaiduOcrValue(data, ['外廓尺寸']);
if (plateNo) {
this.vehicleForm.plateNo = String(plateNo).replace(/\s/g, '').toUpperCase();
this.splitPlateNo();
this.syncPlateNo(false);
}
if (vehicleType) {
this.vehicleForm.vehicleType = this.normalizeVehicleType(vehicleType);
}
if (drivingLicenseNo) {
this.vehicleForm.drivingLicenseNo = String(drivingLicenseNo).replace(/\s/g, '');
}
this.applyDrivingLicenseValidity(inspectionValidity);
if (registrationDate) {
this.vehicleForm.registrationDate = this.normalizeDate(registrationDate);
}
if (registrationNo) {
this.vehicleForm.registrationNo = String(registrationNo).replace(/\s/g, '');
}
if (approvedLoadKg) {
this.vehicleForm.approvedLoadKg = this.normalizeVehicleNumber(approvedLoadKg);
}
this.applyOuterDimensions(outerDimensions);
this.$nextTick(() => {
[
'plateNo',
'vehicleType',
'drivingLicenseNo',
'drivingLicenseEndDate',
'registrationNo',
'approvedLoadKg',
'outerLength',
'outerWidth',
'outerHeight',
].forEach(prop => {
this.$refs.vehicleForm?.validateField(prop);
});
});
},
applyBaiduVehicleLicenseGeneralRecognition(data = {}) {
const words = this.getBaiduOcrWords(data);
const getValue = labels => this.getBaiduGeneralOcrValue(words, labels);
this.applyBaiduVehicleLicenseRecognition({
words_result: {
号牌号码: { words: getValue(['号牌号码', '车牌号码']) },
车辆类型: { words: getValue(['车辆类型']) },
档案编号: { words: getValue(['档案编号']) },
检验有效期至: { words: getValue(['检验有效期至', '检验有效期', '检验记录']) },
注册日期: { words: getValue(['注册日期']) },
车辆识别代号: { words: getValue(['车辆识别代号', '车架号']) },
核定载质量: { words: getValue(['核定载质量']) },
外廓尺寸: { words: getValue(['外廓尺寸']) },
},
});
},
normalizeVehicleNumber(value = '') {
const number = String(value || '').replace(/,/g, '').match(/-?\d+(?:\.\d+)?/);
return number ? number[0] : '';
},
applyOuterDimensions(value = '') {
const dimensions = String(value || '').match(/\d+(?:\.\d+)?/g) || [];
if (dimensions[0]) this.vehicleForm.outerLength = dimensions[0];
if (dimensions[1]) this.vehicleForm.outerWidth = dimensions[1];
if (dimensions[2]) this.vehicleForm.outerHeight = dimensions[2];
},
applyBaiduRoadTransportCertificateRecognition(data = {}) {
const certificateNo = this.getBaiduOcrValue(data, ['道路运输证号', '证号']);
const validity = this.getBaiduOcrValue(data, ['有效期至', '有效期限', '有效期']);
if (certificateNo) {
this.vehicleForm.roadTransportCertNo = String(certificateNo).replace(/\s/g, '');
}
this.applyRoadTransportCertificateValidity(validity);
this.$nextTick(() => {
['roadTransportCertNo', 'roadTransportCertEndDate'].forEach(prop => {
this.$refs.vehicleForm?.validateField(prop);
});
});
},
applyRoadTransportCertificateValidity(validity = '') {
const value = String(validity || '');
if (!value) return;
if (/(长期|永久)/.test(value)) {
this.vehicleForm.roadTransportCertLongTerm = 1;
this.vehicleForm.roadTransportCertEndDate = '';
return;
}
const dateList = value.match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || [];
if (dateList.length) {
this.vehicleForm.roadTransportCertEndDate = this.normalizeDate(dateList[dateList.length - 1]);
this.vehicleForm.roadTransportCertLongTerm = 0;
}
},
applyDrivingLicenseValidity(validity = '') {
const value = String(validity || '');
if (!value) return;
if (/(长期|永久)/.test(value)) {
this.vehicleForm.drivingLicenseLongTerm = 1;
this.vehicleForm.drivingLicenseEndDate = '';
return;
}
const dateList = value.match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || [];
if (dateList.length) {
this.vehicleForm.drivingLicenseEndDate = this.normalizeDate(dateList[dateList.length - 1]);
this.vehicleForm.drivingLicenseLongTerm = 0;
}
},
getBaiduOcrValue(data = {}, keys = []) {
const wordsResult = data.words_result || data.wordsResult || {};
const normalizedKeys = keys.map(key => String(key).toLowerCase());
if (Array.isArray(wordsResult)) {
const wordItem = wordsResult.find(item =>
normalizedKeys.includes(String(item.key || item.name || '').toLowerCase())
);
return wordItem?.words || wordItem?.word || wordItem?.value || '';
}
const wordEntry = Object.entries(wordsResult).find(([key]) =>
normalizedKeys.includes(String(key).toLowerCase())
);
if (!wordEntry) return '';
const value = wordEntry[1];
if (Array.isArray(value)) {
const item = value.find(entry => entry?.words || entry?.word || entry?.value);
return item?.words || item?.word || item?.value || '';
}
return typeof value === 'object' ? value.words || value.word || value.value || '' : value || '';
},
getBaiduOcrWords(data = {}) {
const wordsResult = data.words_result || data.wordsResult || {};
if (Array.isArray(wordsResult)) {
return wordsResult.map(item => item.words || item.word || item.value || '').filter(Boolean);
}
return Object.values(wordsResult)
.map(item => {
if (Array.isArray(item)) {
const entry = item.find(value => value?.words || value?.word || value?.value);
return entry?.words || entry?.word || entry?.value || '';
}
return typeof item === 'object' ? item.words || item.word || item.value || '' : item || '';
})
.filter(Boolean);
},
getBaiduGeneralOcrValue(words = [], labels = []) {
const normalizedWords = words.map(word => String(word || '').trim()).filter(Boolean);
for (const label of labels) {
const labelIndex = normalizedWords.findIndex(word => word.includes(label));
if (labelIndex < 0) continue;
const inlineValue = normalizedWords[labelIndex]
.replace(new RegExp(`^.*${label}\\s*[:]?`), '')
.trim();
if (inlineValue) return inlineValue;
if (normalizedWords[labelIndex + 1]) return normalizedWords[labelIndex + 1];
}
return '';
},
applyVehicleCertificateRecognition(data = {}) {
const plateNo = data.drivingPlateNo || data.vehicleLicensePlateNo || '';
const vehicleType = data.vehicleLicenseVehicleType || data.vehicleType || '';
const drivingLicenseNo =
data.vehicleLicenseFileNo || data.drivingLicenseNo || data.fileNo || '';
const drivingLicenseEndDate =
data.vehicleLicenseInspectionValidDate ||
data.drivingLicenseEndDate ||
data.vehicleLicenseValidDateEnd ||
'';
const registrationDate =
data.vehicleLicenseRegisterDate || data.registrationDate || data.dateRegister || '';
const registrationNo = data.registrationNo || data.vehicleLicenseNo || '';
if (plateNo) {
this.vehicleForm.plateNo = plateNo;
this.splitPlateNo();
this.syncPlateNo(false);
}
if (vehicleType) {
this.vehicleForm.vehicleType = this.normalizeVehicleType(vehicleType);
}
if (drivingLicenseNo) {
this.vehicleForm.drivingLicenseNo = drivingLicenseNo;
}
if (drivingLicenseEndDate) {
this.vehicleForm.drivingLicenseEndDate = this.normalizeDate(drivingLicenseEndDate);
this.vehicleForm.drivingLicenseLongTerm = 0;
}
if (registrationDate) {
this.vehicleForm.registrationDate = this.normalizeDate(registrationDate);
}
if (registrationNo) {
this.vehicleForm.registrationNo = registrationNo;
}
this.$nextTick(() => {
[
'plateNo',
'vehicleType',
'drivingLicenseNo',
'drivingLicenseEndDate',
'registrationNo',
].forEach(prop => {
this.$refs.vehicleForm?.validateField(prop);
});
});
},
normalizeDate(value = '') {
const dateValue = String(value || '').trim();
const match = dateValue.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/);
if (!match) return dateValue;
const [, year, month, day] = match;
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
},
normalizeVehicleType(value = '') {
const vehicleType = String(value || '').trim();
const option = this.vehicleTypeOptions.find(
item => item.value === vehicleType || item.label === vehicleType
);
return option ? option.value : vehicleType;
},
handlePlateBodyInput(value) {
this.vehicleForm.plateNoBody = String(value || '').toUpperCase();
this.syncPlateNo();
},
syncPlateNo(validate = true) {
const plateProvince = String(this.vehicleForm.plateProvince || '').trim();
const plateNoBody = String(this.vehicleForm.plateNoBody || '')
.trim()
.toUpperCase();
const plateColor = String(this.vehicleForm.plateColor || '').trim();
this.vehicleForm.plateNoBody = plateNoBody;
this.vehicleForm.plateNo =
plateProvince && plateNoBody ? `${plateProvince}${plateNoBody}` : '';
this.vehicleForm.plateColor = plateColor;
if (validate) {
this.$refs.vehicleForm?.validateField('plateNo');
}
},
splitPlateNo() {
const plateNo = String(this.vehicleForm.plateNo || '').trim();
if (!plateNo) {
this.vehicleForm.plateProvince = '';
this.vehicleForm.plateNoBody = '';
return;
}
this.vehicleForm.plateProvince = plateNo.slice(0, 1);
this.vehicleForm.plateNoBody = plateNo.slice(1).toUpperCase();
},
handleSubmit() {
this.syncPlateNo();
this.$refs.vehicleForm.validate(valid => {
if (!valid) {
return;
}
this.submitLoading = true;
submit(this.vehicleForm)
.then(() => {
this.$message.success('操作成功');
this.vehicleBox = false;
this.onLoad(this.page);
})
.finally(() => {
this.submitLoading = false;
});
});
},
handleStatus(row) {
const status = row.status === 1 ? 2 : 1;
const text = status === 1 ? '启用' : '停用';
this.$confirm(`是否${text}该车辆?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
changeStatus(row.id, status).then(() => {
this.$message.success('操作成功');
this.onLoad(this.page);
});
});
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('是否删除选中的车辆数据?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
remove(this.ids).then(() => {
this.$message.success('操作成功');
this.onLoad(this.page);
});
});
},
handleExpireChange(expireStatus) {
this.query.expireStatus = expireStatus;
this.page.currentPage = 1;
this.onLoad(this.page);
},
refreshStat() {
getExpiryStat(this.buildQuery(false)).then(res => {
this.expiryStat = {
total: res.data.data?.total || 0,
within30: res.data.data?.within30 || 0,
expired: res.data.data?.expired || 0,
};
});
},
searchReset() {
this.searchForm = {
businessRelation: '',
};
this.query = {
expireStatus: this.query.expireStatus,
};
this.onLoad(this.page);
},
searchChange(params, done) {
this.searchForm = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
this.selectionList = list;
this.ids = list.map(item => item.id).join(',');
},
selectionClear() {
this.selectionList = [];
this.ids = '';
this.$refs.crud?.toggleSelection();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.searchForm);
},
onLoad(page, params = {}) {
this.loading = true;
getList(page.currentPage, page.pageSize, this.buildQuery(true, params))
.then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.selectionClear();
this.refreshStat();
})
.finally(() => {
this.loading = false;
});
},
buildQuery(includeExpire = true, params = {}) {
const query = {
...params,
...this.searchForm,
};
if (includeExpire && this.query.expireStatus) {
query.expireStatus = this.query.expireStatus;
}
return query;
},
handleExport() {
this.$confirm('是否导出车辆数据?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob(
'/blade-transport/transport-vehicle/export-transport-vehicle',
this.buildExportParams(),
{ feedback: true }
)
.then(res => {
downloadXls(res.data, `车辆管理${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
})
.finally(() => {
NProgress.done();
});
});
},
buildExportParams() {
return {
...this.buildQuery(true),
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
},
formatEndDate(date, longTerm) {
if (longTerm === 1) {
return '长期有效';
}
return date || '-';
},
getDateClass(row, dateProp, longTermProp) {
if (row[longTermProp] === 1 || !row[dateProp]) {
return '';
}
const today = this.$dayjs();
const endDate = this.$dayjs(row[dateProp]);
if (endDate.isBefore(today, 'day')) {
return 'date-danger';
}
if (endDate.diff(today, 'day') <= 30) {
return 'date-warning';
}
return '';
},
getCertificationStatus(row) {
if (row.certificationStatus === 0) return { label: '认证中', type: 'warning' };
if (row.certificationStatus === 1) return { label: '已认证', type: 'success' };
if (row.certificationStatus === 2) return { label: '已驳回', type: 'danger' };
const today = this.$dayjs().startOf('day');
const certificateDates = [
[row.drivingLicenseEndDate, row.drivingLicenseLongTerm],
[row.roadTransportCertEndDate, row.roadTransportCertLongTerm],
];
if (certificateDates.some(([date, longTerm]) => longTerm !== 1 && !date)) {
return { label: '未认证', type: 'info' };
}
if (
certificateDates.some(
([date, longTerm]) => longTerm !== 1 && this.$dayjs(date).isBefore(today, 'day')
)
) {
return { label: '已过期', type: 'danger' };
}
return { label: '已认证', type: 'success' };
},
},
};
</script>
<style lang="scss" scoped>
.transport-vehicle-page {
&__expiry-tags {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
order: -1;
margin-right: 12px;
min-width: 0;
:deep(.el-check-tag) {
margin: 0;
white-space: nowrap;
}
}
:deep(.el-table th .cell),
:deep(.el-table td .cell) {
white-space: nowrap;
}
}
.vehicle-form {
:deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper),
:deep(.el-select),
:deep(.el-input),
:deep(.el-input-number) {
width: 100%;
}
.plate-group {
display: grid;
grid-template-columns: 112px minmax(0, 1fr) 112px;
gap: 8px;
align-items: center;
}
.dimension-group {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
align-items: center;
}
.dimension-group__item {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.dimension-group__label {
flex-shrink: 0;
color: #303133;
font-weight: 500;
}
.date-with-flag {
display: flex;
align-items: center;
gap: 8px;
}
.date-with-flag :deep(.el-date-editor) {
flex: 1;
min-width: 0;
}
.date-with-flag :deep(.el-checkbox) {
flex-shrink: 0;
margin-right: 0;
}
.date-with-flag :deep(.el-checkbox__label) {
padding-left: 4px;
}
.date-range {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 8px;
}
:deep(.el-form-item__label) {
white-space: nowrap;
}
}
// 大尺寸证件照:按身份证 1.586:1 比例缩放(约真实尺寸 75%,240×151),左对齐显示
:deep(.vehicle-uploader) {
display: block;
}
:deep(.vehicle-uploader .el-upload),
:deep(.vehicle-uploader.el-upload) {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 170px;
border: 1px dashed #c0c4cc;
background: #fafafa;
overflow: hidden;
}
// 大尺寸模式:缩成 1.586:1 比例,左对齐(不再 100% 占栏)
:deep(.vehicle-uploader--large) {
display: block;
width: 240px;
margin: 0;
}
:deep(.vehicle-uploader--large .el-upload),
:deep(.vehicle-uploader--large.el-upload) {
width: 100%;
height: 151px;
}
:deep(.vehicle-uploader__image) {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
:deep(.vehicle-uploader__empty) {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #606266;
}
.date-warning {
color: #d40000;
font-weight: 600;
}
.date-danger {
color: #d40000;
font-weight: 600;
}
.el-form-item {
margin-bottom: 14px !important;
}
</style>