Files
tms-erp-web/src/views/transportCapacity/driver.vue
T
gxwebsoft 2bd9d8c047 style(ui): 全站 el-tag 状态语义统一为纯文本样式
- 按用户要求,将全站所有状态语义的 el-tag 加入 class="status-text"
- 覆盖范围包括业务状态、启用停用、工作流状态、是否封存等多达 35 余处文件
- 保留 el-tag 的 :type 绑定,仅通过样式隐藏背景边框及圆点,变为纯文本呈现
- 修正 .status-text 的 font-size、line-height、height、vertical-align,确保字体和单元格普通文本完全对齐
- 例外保留原样式的标签类型有类型标签、值类字段等,未受影响
- 更新 element-ui.scss 添加 .el-tag.status-text 的全局统一样式
- 相关模板中均添加了 class="status-text",保证样式一致自动生效
2026-08-25 11:56:16 +08:00

1617 lines
55 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="driver-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('driver_add')"
@click="openDriver()"
>新增
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="hasPermission('driver_export')"
@click="handleExport"
>导出
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="hasPermission('driver_delete')"
@click="handleDelete"
>批量删除
</el-button>
</template>
<template #search-menu>
<div class="driver-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 #driverName="{ row }">
<el-link type="primary" @click="openDriver(row, true)">{{ row.driverName }}</el-link>
</template>
<template #idCardNo="{ row }">
<span>{{ row.idCardNo || '-' }}</span>
</template>
<template #organizationName="{ row }">
{{ String(row.organizationName || '').replace(/^[\s\u3000]+/, '') }}
</template>
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text">
{{ row.status === 1 ? '启用' : '停用' }}
</el-tag>
</template>
<template #drivingLicenseEndDate="{ row }">
<span
:class="{
'driver-page__date-expiring': isDateExpiring(
row,
'drivingLicenseEndDate',
'drivingLicenseLongTerm'
),
}"
>
{{ formatEndDate(row.drivingLicenseEndDate, row.drivingLicenseLongTerm) }}
</span>
</template>
<template #qualificationEndDate="{ row }">
<span
:class="{
'driver-page__date-expiring': isDateExpiring(
row,
'qualificationEndDate',
'qualificationLongTerm'
),
}"
>
{{ formatEndDate(row.qualificationEndDate, row.qualificationLongTerm) }}
</span>
</template>
<template #menu="{ row }">
<el-link type="primary" v-if="hasPermission('driver_view')" @click="openDriver(row, true)">
查看
</el-link>
<el-link type="primary" v-if="hasPermission('driver_edit')" @click="openDriver(row)">
修改
</el-link>
<el-link type="primary" v-if="hasPermission('driver_status')" @click="handleStatus(row)">
{{ row.status === 1 ? '停用' : '启用' }}
</el-link>
</template>
</avue-crud>
<el-dialog
:title="dialogTitle"
append-to-body
v-model="driverBox"
width="90%"
top="4vh"
class="driver-dialog"
@closed="resetDriver"
>
<el-form
ref="driverForm"
:model="driverForm"
:rules="formRules"
label-position="right"
:disabled="readonly"
class="driver-form archive-form"
>
<section-card title="基础身份信息">
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="司机姓名" prop="driverName">
<el-input v-model="driverForm.driverName" maxlength="10" show-word-limit />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="身份证号" prop="idCardNo">
<el-input
v-model="driverForm.idCardNo"
maxlength="18"
show-word-limit
@input="driverForm.idCardNo = String(driverForm.idCardNo || '').toUpperCase()"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="出生年月" prop="birthday">
<el-date-picker
v-model="driverForm.birthday"
type="date"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="性别" prop="gender">
<el-select v-model="driverForm.gender" clearable>
<el-option label="男" value="男" />
<el-option label="女" value="女" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="民族" prop="nation">
<el-select v-model="driverForm.nation" clearable filterable>
<el-option
v-for="item in nationOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="学历" prop="education">
<el-select v-model="driverForm.education" clearable filterable>
<el-option
v-for="item in educationOptions"
: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="addressRegion">
<el-cascader
ref="addressRegionCascader"
v-model="driverForm.addressRegionPath"
:options="regionOptions"
:props="regionCascaderProps"
:placeholder="driverForm.addressRegion || '请选择省市区'"
clearable
filterable
@change="handleAddressRegionChange"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="详细地址" prop="address">
<el-input v-model="driverForm.address" maxlength="200" show-word-limit />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="岗位" prop="postList">
<el-checkbox-group v-model="driverForm.postList">
<el-checkbox label="司机" />
<el-checkbox label="押运员" />
<el-checkbox label="装卸管理员" />
</el-checkbox-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="8">
<image-upload-field
label="身份证正面照"
prop="idCardFront"
:value="driverForm.idCardFront"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => handleIdCardUploadSuccess('idCardFront', url)"
/>
</el-col>
<el-col :span="8">
<image-upload-field
label="身份证反面照"
prop="idCardBack"
:value="driverForm.idCardBack"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => handleIdCardUploadSuccess('idCardBack', url)"
/>
</el-col>
<el-col :span="8">
<image-upload-field
label="大头照"
prop="headPhoto"
:value="driverForm.headPhoto"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => setImage('headPhoto', url)"
/>
</el-col>
</el-row>
</section-card>
<section-card title="驾驶证信息">
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="准驾车型" prop="drivingType">
<el-select
v-model="driverForm.drivingType"
clearable
filterable
allow-create
default-first-option
>
<el-option
v-for="item in drivingTypeOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="档案编号" prop="drivingLicenseNo">
<el-input v-model="driverForm.drivingLicenseNo" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="有效期" prop="drivingLicenseEndDate">
<div class="date-range">
<el-date-picker
v-model="driverForm.drivingLicenseStartDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="起"
@change="validateFormField('drivingLicenseEndDate')"
/>
<el-date-picker
v-model="driverForm.drivingLicenseEndDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="止"
:disabled="driverForm.drivingLicenseLongTerm === 1"
@change="validateFormField('drivingLicenseEndDate')"
/>
</div>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="长期有效" prop="drivingLicenseLongTerm">
<el-checkbox
v-model="driverForm.drivingLicenseLongTerm"
:true-label="1"
:false-label="0"
@change="validateFormField('drivingLicenseEndDate')"
>
</el-checkbox>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="12">
<image-upload-field
label="驾驶证主页"
prop="drivingLicenseFront"
:value="driverForm.drivingLicenseFront"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="
(url, file) => handleDrivingLicenseUploadSuccess('drivingLicenseFront', url, file)
"
/>
</el-col>
<el-col :span="12">
<image-upload-field
label="驾驶证副页"
prop="drivingLicenseBack"
:value="driverForm.drivingLicenseBack"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="
(url, file) => handleDrivingLicenseUploadSuccess('drivingLicenseBack', url, file)
"
/>
</el-col>
</el-row>
</section-card>
<section-card title="从业资格证信息">
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="从业资格认证类型" prop="qualificationType">
<el-select
v-model="driverForm.qualificationType"
clearable
filterable
>
<el-option
v-for="item in qualificationTypeOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="资格证号" prop="qualificationNo">
<el-input
v-model="driverForm.qualificationNo"
maxlength="50"
placeholder="默认身份证号"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="有效期" prop="qualificationEndDate">
<el-date-picker
v-model="driverForm.qualificationEndDate"
type="date"
value-format="YYYY-MM-DD"
:disabled="driverForm.qualificationLongTerm === 1"
@change="validateFormField('qualificationEndDate')"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="长期有效" prop="qualificationLongTerm">
<el-checkbox
v-model="driverForm.qualificationLongTerm"
:true-label="1"
:false-label="0"
@change="validateFormField('qualificationEndDate')"
>
</el-checkbox>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="12">
<image-upload-field
label="封面页"
prop="qualificationFront"
:value="driverForm.qualificationFront"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => handleQualificationUploadSuccess('qualificationFront', url)"
/>
</el-col>
<el-col :span="12">
<image-upload-field
label="内容页"
prop="qualificationBack"
:value="driverForm.qualificationBack"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => handleQualificationUploadSuccess('qualificationBack', url)"
/>
</el-col>
</el-row>
</section-card>
<section-card title="司机联系信息">
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="司机类型" prop="driverType">
<el-select v-model="driverForm.driverType" filterable>
<el-option label="自有" value="自有" />
<el-option label="外协" value="外协" />
<el-option label="承运管理员" value="承运管理员" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="手机号" prop="mobile">
<el-input v-model="driverForm.mobile" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="紧急联系人姓名" prop="emergencyContactName">
<el-input
v-model="driverForm.emergencyContactName"
maxlength="20"
@input="driverForm.emergencyContactName = removeDigits($event)"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="紧急联系人手机号" prop="emergencyContactMobile">
<el-input v-model="driverForm.emergencyContactMobile" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="与联系人关系" prop="contactRelation">
<el-select
v-model="driverForm.contactRelation"
clearable
filterable
allow-create
default-first-option
>
<el-option
v-for="item in relationOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="所属组织" prop="organizationName">
<el-select v-model="driverForm.organizationName" clearable filterable>
<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>
</section-card>
</el-form>
<template #footer>
<el-button @click="driverBox = 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/driver';
import {
getList,
getDetail,
submit,
remove,
changeStatus,
getExpiryStat,
recognitionIDCard,
recognitionTransportCertificates,
recognizeBaiduOcr,
} from '@/api/transportCapacity/driver';
import { getDeptTree } from '@/api/system/dept';
import { getDictionary } from '@/api/system/dictbiz';
import { getLazyTree } from '@/api/base/region';
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';
const emptyForm = () => ({
id: '',
driverName: '',
idCardNo: '',
birthday: '',
gender: '',
nation: '',
education: '',
addressRegionPath: [],
addressRegion: '',
address: '',
postList: ['司机'],
posts: '',
idCardFront: '',
idCardBack: '',
headPhoto: '',
drivingType: '',
drivingLicenseNo: '',
drivingLicenseStartDate: '',
drivingLicenseEndDate: '',
drivingLicenseLongTerm: 0,
drivingLicenseFront: '',
drivingLicenseBack: '',
qualificationType: '',
qualificationNo: '',
qualificationEndDate: '',
qualificationLongTerm: 0,
qualificationFront: '',
qualificationBack: '',
driverType: '自有',
mobile: '',
contactRelation: '',
organizationName: '',
emergencyContactName: '',
emergencyContactMobile: '',
status: 1,
});
export default {
components: {
ImageUploadField,
},
data() {
return {
option,
form: {},
query: {
expireStatus: '',
},
searchForm: {},
loading: true,
submitLoading: false,
data: [],
ids: '',
selectionList: [],
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
expiryStat: {
total: 0,
within30: 0,
expired: 0,
},
driverBox: false,
readonly: false,
driverForm: emptyForm(),
drivingLicenseUploads: {},
uploadHeaders: getUploadHeaders(),
nationOptions: [
'汉族',
'蒙古族',
'回族',
'藏族',
'维吾尔族',
'苗族',
'彝族',
'壮族',
'布依族',
'朝鲜族',
'满族',
'侗族',
'瑶族',
'白族',
'土家族',
'哈尼族',
'哈萨克族',
'傣族',
'黎族',
'傈僳族',
'佤族',
'畲族',
'高山族',
'拉祜族',
'水族',
'东乡族',
'纳西族',
'景颇族',
'柯尔克孜族',
'土族',
'达斡尔族',
'仫佬族',
'羌族',
'布朗族',
'撒拉族',
'毛南族',
'仡佬族',
'锡伯族',
'阿昌族',
'普米族',
'塔吉克族',
'怒族',
'乌孜别克族',
'俄罗斯族',
'鄂温克族',
'德昂族',
'保安族',
'裕固族',
'京族',
'塔塔尔族',
'独龙族',
'鄂伦春族',
'赫哲族',
'门巴族',
'珞巴族',
'基诺族',
],
educationOptions: ['初中', '高中', '中专', '大专', '本科', '硕士及以上'],
drivingTypeOptions: ['A1', 'A2', 'A3', 'B1', 'B2', 'C1', 'C2'],
qualificationTypeOptions: [],
relationOptions: ['父母', '配偶', '子女', '兄弟姐妹', '朋友', '同事', '其他'],
organizationOptions: [],
regionOptions: [],
formRules: {
driverName: [{ required: true, message: '请输入司机姓名', trigger: 'blur' }],
idCardNo: [{ required: true, message: '请输入身份证号', trigger: 'blur' }],
gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
postList: [
{
type: 'array',
required: true,
min: 1,
message: '请至少选择一个岗位',
trigger: 'change',
},
],
drivingType: [{ required: true, message: '请选择准驾车型', trigger: 'change' }],
drivingLicenseNo: [{ required: true, message: '请输入档案编号', trigger: 'blur' }],
drivingLicenseEndDate: [
{ validator: this.validateDrivingLicenseEndDate, trigger: 'change' },
],
drivingLicenseLongTerm: [
{ required: true, message: '请选择驾驶证是否长期有效', trigger: 'change' },
],
qualificationType: [
{ required: true, message: '请选择从业资格认证类型', trigger: 'change' },
],
qualificationNo: [{ required: true, message: '请输入资格证号', trigger: 'blur' }],
qualificationEndDate: [{ validator: this.validateQualificationEndDate, trigger: 'change' }],
qualificationLongTerm: [
{ required: true, message: '请选择从业资格证是否长期有效', trigger: 'change' },
],
driverType: [{ required: true, message: '请选择司机类型', trigger: 'change' }],
mobile: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
emergencyContactName: [
{ required: true, message: '请输入紧急联系人姓名', trigger: 'blur' },
{ validator: this.validateEmergencyContactName, trigger: 'blur' },
],
emergencyContactMobile: [
{ required: true, message: '请输入紧急联系人手机号', trigger: 'blur' },
],
organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
idCardFront: [{ required: true, message: '请上传身份证正面照', trigger: 'change' }],
idCardBack: [{ required: true, message: '请上传身份证反面照', trigger: 'change' }],
drivingLicenseFront: [{ required: true, message: '请上传驾驶证主页', trigger: 'change' }],
drivingLicenseBack: [{ required: true, message: '请上传驾驶证副页', trigger: 'change' }],
},
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
const authority = this.userInfo.authority || '';
return authority.includes('admin');
},
permissionList() {
return {
addBtn: this.hasPermission('driver_add'),
viewBtn: this.hasPermission('driver_view'),
delBtn: this.hasPermission('driver_delete'),
editBtn: this.hasPermission('driver_edit'),
};
},
dialogTitle() {
if (this.readonly) {
return '查看司机';
}
return this.driverForm.id ? '修改司机' : '新增司机';
},
regionCascaderProps() {
return {
value: 'id',
label: 'title',
children: 'children',
emitPath: true,
};
},
expiryTagOptions() {
return [
{ label: '全部', value: '', statKey: 'total' },
{ label: '30天内到期', value: 'within30', statKey: 'within30' },
{ label: '已到期', value: 'expired', statKey: 'expired' },
];
},
},
created() {
this.initDeptTree();
this.initRegionOptions();
this.initQualificationTypeOptions();
},
methods: {
hasPermission(code) {
return this.isAdmin || this.permission?.[code] === true;
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => {
this.organizationOptions = this.formatDeptOptions(res.data.data || []);
const column = this.findColumn(this.option.column, 'organizationName');
column.dicData = this.organizationOptions;
if (this.driverBox && !this.driverForm.id && !this.driverForm.organizationName) {
this.driverForm.organizationName = this.getCurrentOrganizationName();
}
});
},
initQualificationTypeOptions() {
getDictionary({ code: 'type_of_cert' }).then(res => {
this.qualificationTypeOptions = (res.data.data || [])
.map(item => item.dictValue)
.filter(Boolean);
const matchedType = this.matchQualificationType(this.driverForm.qualificationType);
if (matchedType) {
this.driverForm.qualificationType = matchedType;
}
});
},
formatDeptOptions(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.formatDeptOptions(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 || '';
},
initRegionOptions() {
getLazyTree().then(res => {
const regions = this.buildRegionTree(res.data.data || []);
this.regionOptions = this.extractChinaRegionOptions(regions);
this.syncAddressRegionPath();
});
},
buildRegionTree(regions = []) {
const flatRegions = [];
const collectRegions = list => {
(list || []).forEach(item => {
flatRegions.push({
...item,
children: undefined,
});
if (item.children?.length) collectRegions(item.children);
});
};
collectRegions(regions);
const nodeMap = new Map();
flatRegions.forEach(item => {
const id = item.id === undefined || item.id === null ? item.value : item.id;
if (id === undefined || id === null) return;
nodeMap.set(String(id), {
...item,
id: String(id),
parentId:
item.parentId === undefined || item.parentId === null ? '' : String(item.parentId),
children: [],
});
});
const rootNodes = [];
nodeMap.forEach(node => {
const parent = nodeMap.get(node.parentId);
if (parent && parent !== node) {
parent.children.push(node);
} else {
rootNodes.push(node);
}
});
return rootNodes.map(item => this.normalizeRegionTreeNode(item));
},
normalizeRegionTreeNode(region) {
const children = (region.children || []).map(item => this.normalizeRegionTreeNode(item));
return {
...region,
children: children.length ? children : undefined,
leaf: !children.length,
};
},
isChinaRegion(region = {}) {
const title = String(region.title || region.name || '').trim();
return title === '中国' || title === '中华人民共和国';
},
extractChinaRegionOptions(regions) {
const china = (regions || []).find(item => this.isChinaRegion(item));
if (china?.children?.length) return china.children;
if (regions.length === 1 && regions[0].children?.length) return regions[0].children;
return regions;
},
findRegionOption(options, value) {
for (const item of options || []) {
if (String(item.id) === String(value)) return item;
const child = this.findRegionOption(item.children, value);
if (child) return child;
}
return null;
},
findRegionPathByName(options = [], addressRegion = '', parents = []) {
const target = String(addressRegion || '').replace(/\s+/g, '');
if (!target) return [];
for (const item of options || []) {
const nextParents = [...parents, item];
const nextName = nextParents.map(region => region.title || region.name || '').join('');
if (nextName === target) {
return nextParents.map(region => region.id);
}
const childPath = this.findRegionPathByName(item.children, target, nextParents);
if (childPath.length) return childPath;
}
return [];
},
getAddressRegionLabels(value) {
const nodes = this.$refs.addressRegionCascader?.getCheckedNodes?.() || [];
if (nodes.length && nodes[0].pathLabels?.length) {
return nodes[0].pathLabels;
}
return (value || [])
.map(code => this.findRegionOption(this.regionOptions, code)?.title)
.filter(Boolean);
},
handleAddressRegionChange(value) {
if (!value || !value.length) {
this.driverForm.addressRegion = '';
this.$refs.driverForm?.validateField('addressRegion');
return;
}
this.$nextTick(() => {
this.driverForm.addressRegion = this.getAddressRegionLabels(value).join('');
this.$refs.driverForm?.validateField('addressRegion');
});
},
syncAddressRegionPath() {
if (!this.driverForm.addressRegion || this.driverForm.addressRegionPath?.length) return;
const path = this.findRegionPathByName(this.regionOptions, this.driverForm.addressRegion);
if (path.length) {
this.driverForm.addressRegionPath = path;
}
},
validateDrivingLicenseEndDate(rule, value, callback) {
if (!this.driverForm.drivingLicenseStartDate) {
callback(new Error('请选择驾驶证有效期起'));
return;
}
if (this.driverForm.drivingLicenseLongTerm !== 1 && !value) {
callback(new Error('请选择驾驶证有效期止'));
return;
}
callback();
},
validateQualificationEndDate(rule, value, callback) {
if (this.driverForm.qualificationLongTerm !== 1 && !value) {
callback(new Error('请选择从业资格证有效期'));
return;
}
callback();
},
validateEmergencyContactName(rule, value, callback) {
if (/\d/.test(value || '')) {
callback(new Error('紧急联系人姓名不能包含数字'));
return;
}
callback();
},
removeDigits(value = '') {
return String(value).replace(/\d/g, '');
},
openDriver(row, readonly = false) {
this.readonly = readonly;
if (!row?.id) {
this.driverForm = emptyForm();
this.driverForm.organizationName = this.getCurrentOrganizationName();
this.driverBox = true;
return;
}
getDetail(row.id).then(res => {
const detail = res.data.data || {};
this.driverForm = {
...emptyForm(),
...detail,
addressRegionPath: Array.isArray(detail.addressRegionPath)
? detail.addressRegionPath
: [],
postList: detail.posts ? detail.posts.split(',').filter(Boolean) : [],
};
this.syncAddressRegionPath();
this.driverBox = true;
});
},
resetDriver() {
this.driverForm = emptyForm();
this.drivingLicenseUploads = {};
this.readonly = false;
this.submitLoading = false;
this.$refs.driverForm?.clearValidate();
},
setImage(prop, url) {
this.driverForm[prop] = url;
this.$refs.driverForm?.validateField(prop);
},
handleIdCardUploadSuccess(prop, url) {
this.setImage(prop, url);
if (prop !== 'idCardFront') {
return;
}
this.recognizeBaiduOcr(url, 'id_card', 'front', '身份证', data => {
this.applyIdCardRecognition(data);
});
},
handleDrivingLicenseUploadSuccess(prop, url) {
this.setImage(prop, url);
this.drivingLicenseUploads[prop] = url;
this.recognizeBaiduOcr(
url,
'driving_license',
prop === 'drivingLicenseBack' ? 'back' : 'front',
'驾驶证',
data => {
this.applyDrivingLicenseRecognition(data);
}
);
},
handleQualificationUploadSuccess(prop, url) {
this.setImage(prop, url);
this.recognizeBaiduOcr(url, 'general', '', '从业资格证', data => {
this.applyQualificationRecognition(data);
});
},
recognizeBaiduOcr(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();
});
},
recognizeIdCard(url = '') {
if (!url) {
this.$message.warning('身份证图片上传成功,未获取到图片地址,无法自动识别');
return;
}
const loading = ElLoading.service({
lock: true,
text: '身份证识别中',
background: 'rgba(255, 255, 255, 0.7)',
});
recognitionIDCard(url)
.then(res => {
const data = res.data.data || {};
this.applyIdCardRecognition(data);
this.$message.success('身份证识别完成');
})
.catch(() => {
this.$message.warning('身份证图片上传成功,自动识别失败,请手动填写身份信息');
})
.finally(() => {
loading.close();
});
},
recognizeDrivingLicense() {
const certificates = this.buildDrivingLicenseRecognitionCertificates();
if (!certificates.length) {
this.$message.warning('驾驶证图片上传成功,未获取到图片地址,无法自动识别');
return;
}
const loading = ElLoading.service({
lock: true,
text: '驾驶证识别中',
background: 'rgba(255, 255, 255, 0.7)',
});
recognitionTransportCertificates(certificates)
.then(res => {
const data = res.data.data || {};
this.applyDrivingLicenseRecognition(data);
this.$message.success('驾驶证识别完成');
})
.catch(() => {
this.$message.warning('驾驶证图片上传成功,自动识别失败,请手动填写驾驶证信息');
})
.finally(() => {
loading.close();
});
},
buildDrivingLicenseRecognitionCertificates() {
return ['drivingLicenseFront', 'drivingLicenseBack']
.map(prop => {
const url = this.drivingLicenseUploads[prop] || this.driverForm[prop];
if (!url) return null;
return {
driverLicenseUrl: url,
};
})
.filter(Boolean);
},
applyDrivingLicenseRecognition(data = {}) {
const driverName =
data.driverLicenseName || data.name || this.getOcrValue(data, ['name', '姓名']);
const idCardNo = String(
data.driverLicenseIdNo ||
data.idNo ||
data.idCardNo ||
this.getOcrValue(data, ['ID_no', 'idNo', 'idCardNo', '证号'])
).toUpperCase();
const drivingType =
data.driverLicenseType ||
data.drivingType ||
data.driverClass ||
this.getOcrValue(data, ['class', '准驾车型']);
const drivingLicenseNo =
data.driverLicenseFileNo ||
data.drivingLicenseNo ||
data.fileNo ||
this.getOcrValue(data, ['file_no', '档案编号']);
const validDateStart =
data.driverLicenseValidDateStart ||
data.drivingLicenseStartDate ||
this.getOcrValue(data, ['date_vaild_start', '有效期限(起始时间)', '有效起始日期']);
const validDateEnd =
data.driverLicenseValidDateEnd ||
data.drivingLicenseEndDate ||
this.getOcrValue(data, ['date_vaild_end', '有效期限(终止时间)', '有效期限(终⽌时间)', '有效截止日期']);
if (driverName && !this.driverForm.driverName) {
this.driverForm.driverName = driverName;
}
if (idCardNo && !this.driverForm.idCardNo) {
this.driverForm.idCardNo = idCardNo;
}
if (idCardNo && !this.driverForm.qualificationNo) {
this.driverForm.qualificationNo = idCardNo;
}
if (drivingType) {
this.driverForm.drivingType = drivingType;
}
if (drivingLicenseNo) {
this.driverForm.drivingLicenseNo = drivingLicenseNo;
}
if (validDateStart) {
this.driverForm.drivingLicenseStartDate = this.normalizeBirthday(validDateStart);
}
if (validDateEnd) {
this.driverForm.drivingLicenseEndDate = this.normalizeBirthday(validDateEnd);
this.driverForm.drivingLicenseLongTerm = 0;
}
this.applyDrivingLicenseValidity(data);
this.$nextTick(() => {
[
'driverName',
'idCardNo',
'drivingType',
'drivingLicenseNo',
'drivingLicenseEndDate',
'qualificationNo',
].forEach(prop => {
this.$refs.driverForm?.validateField(prop);
});
});
},
applyDrivingLicenseValidity(data = {}) {
const validity = this.getOcrValue(data, ['有效期限', '有效期']);
if (!validity) return;
const dateList = validity.match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || [];
if (!this.driverForm.drivingLicenseStartDate && dateList[0]) {
this.driverForm.drivingLicenseStartDate = this.normalizeBirthday(dateList[0]);
}
if (/(长期|永久)/.test(validity)) {
this.driverForm.drivingLicenseLongTerm = 1;
this.driverForm.drivingLicenseEndDate = '';
} else if (!this.driverForm.drivingLicenseEndDate && dateList[1]) {
this.driverForm.drivingLicenseEndDate = this.normalizeBirthday(dateList[1]);
this.driverForm.drivingLicenseLongTerm = 0;
}
},
applyIdCardRecognition(data = {}) {
const driverName = data.name || data.driverName || this.getOcrValue(data, ['name', '姓名']);
const idCardNo = String(
data.idNo ||
data.idCardNo ||
data.idCard ||
this.getOcrValue(data, ['idNo', 'idCardNo', 'number', '公民身份号码', '身份证号'])
).toUpperCase();
const ocrGender = data.gender || this.getOcrValue(data, ['gender', '性别']);
const ocrNation =
data.nation ||
data.ethnicGroup ||
this.getOcrValue(data, ['ethnic_group', 'nation', '民族']);
const ocrAddress = data.address || this.getOcrValue(data, ['address', '住址', '地址']);
if (driverName) {
this.driverForm.driverName = driverName;
}
if (ocrGender) {
this.driverForm.gender = ocrGender;
}
if (ocrNation) {
this.driverForm.nation = this.normalizeNation(ocrNation);
}
if (ocrAddress) {
this.driverForm.address = String(ocrAddress).trim();
}
if (idCardNo) {
this.driverForm.idCardNo = idCardNo;
if (!this.driverForm.qualificationNo) {
this.driverForm.qualificationNo = idCardNo;
}
const idCardInfo = this.parseIdCardInfo(idCardNo);
if (idCardInfo.birthday) {
this.driverForm.birthday = idCardInfo.birthday;
}
if (!this.driverForm.gender && idCardInfo.gender) {
this.driverForm.gender = idCardInfo.gender;
}
}
this.$nextTick(() => {
[
'driverName',
'idCardNo',
'birthday',
'gender',
'nation',
'qualificationNo',
'address',
].forEach(prop => {
this.$refs.driverForm?.validateField(prop);
});
});
},
getOcrValue(data = {}, keys = []) {
const keyInfos = data.keyInfos || data.keyInfo || data.ocrKeyInfos || [];
const list = Array.isArray(keyInfos) ? keyInfos : [];
const item = list.find(info => {
const key = String(info.key || '').toLowerCase();
const description = String(info.description || '');
return keys.some(value => key === String(value).toLowerCase() || description === value);
});
if (item) return item.value || '';
const wordsResult = data.words_result || data.wordsResult || {};
const normalizedKeys = keys.map(key => String(key).toLowerCase());
if (Array.isArray(wordsResult)) {
const wordItem = wordsResult.find(info =>
normalizedKeys.includes(String(info.key || info.name || '').toLowerCase())
);
return wordItem?.words || wordItem?.value || '';
}
const wordEntry = Object.entries(wordsResult).find(([key]) =>
normalizedKeys.includes(String(key).toLowerCase())
);
if (!wordEntry) return '';
const value = wordEntry[1];
return typeof value === 'object' ? value.words || value.value || '' : value || '';
},
applyQualificationRecognition(data = {}) {
const qualificationType = this.getQualificationType(data);
const qualificationNo = this.getQualificationNo(data);
const matchedType = this.matchQualificationType(qualificationType);
if (matchedType) {
this.driverForm.qualificationType = matchedType;
} else if (qualificationType && !this.qualificationTypeOptions.length) {
this.driverForm.qualificationType = qualificationType;
}
if (qualificationNo && !this.driverForm.qualificationNo) {
this.driverForm.qualificationNo = String(qualificationNo).replace(/\s/g, '');
}
const words = this.getBaiduOcrWords(data).join(' ');
const dateList = words.match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || [];
if (/(长期|永久有效)/.test(words)) {
this.driverForm.qualificationLongTerm = 1;
this.driverForm.qualificationEndDate = '';
} else if (dateList.length) {
this.driverForm.qualificationEndDate = this.normalizeBirthday(dateList[dateList.length - 1]);
this.driverForm.qualificationLongTerm = 0;
}
this.$nextTick(() => {
['qualificationType', 'qualificationNo', 'qualificationEndDate'].forEach(prop => {
this.$refs.driverForm?.validateField(prop);
});
});
},
getQualificationType(data = {}) {
const fieldValue = this.getOcrValue(data, ['从业资格类别', '从业资格类型']);
if (fieldValue) return String(fieldValue).trim();
const words = this.getBaiduOcrWords(data);
const labelIndex = words.findIndex(word => /从业资格类别|从业资格类型/.test(word));
if (labelIndex < 0) return '';
const firstValue = words[labelIndex].replace(/^.*?(?:从业资格类别|从业资格类型)\s*[::]?/, '').trim();
const values = firstValue ? [firstValue] : [];
for (let index = labelIndex + 1; index < words.length; index += 1) {
const word = String(words[index] || '').trim();
if (!word || /^(有效起始日期|有效期限|核发机关|继续教育信息|诚信考核信息)/.test(word)) {
break;
}
values.push(word);
}
return values.join('').replace(/[,;]+$/, '').trim();
},
matchQualificationType(value = '') {
const text = String(value || '').replace(/\s/g, '');
if (!text) return '';
const options = this.qualificationTypeOptions || [];
const normalizedOptions = options.map(option => ({
value: option,
text: String(option).replace(/\s/g, ''),
}));
const exact = normalizedOptions.find(option => option.text === text);
if (exact) return exact.value;
const candidates = text.split(/[,;]/).filter(Boolean);
const matched = normalizedOptions.find(option =>
candidates.some(candidate => candidate.includes(option.text) || option.text.includes(candidate))
);
return matched?.value || '';
},
getQualificationNo(data = {}) {
const fieldValue = this.getOcrValue(data, ['从业资格证号', '资格证号', '证书编号']);
if (fieldValue) return fieldValue;
const words = this.getBaiduOcrWords(data);
const labelIndex = words.findIndex(word => /^(?:(?:从业)?资格证号?|证书编号)[:]?$/.test(word));
if (labelIndex >= 0 && words[labelIndex + 1]) {
return words[labelIndex + 1];
}
const certificateLine = words.find(word => /(?:从业)?资格证号?|证书编号/.test(word));
const match = certificateLine?.match(/(?:(?:从业)?资格证号?|证书编号)\s*[:]?\s*([A-Z0-9-]{6,})/i);
return match?.[1] || '';
},
getBaiduOcrWords(data = {}) {
const wordsResult = data.words_result || data.wordsResult || {};
if (Array.isArray(wordsResult)) {
return wordsResult.map(item => item.words || item.value || '').filter(Boolean);
}
return Object.values(wordsResult)
.map(item => (typeof item === 'object' ? item.words || item.value || '' : item || ''))
.filter(Boolean);
},
normalizeNation(value = '') {
const nation = String(value || '').trim();
if (!nation) return '';
if (this.nationOptions.includes(nation)) return nation;
const fullNation = `${nation}族`;
return this.nationOptions.includes(fullNation) ? fullNation : nation;
},
normalizeBirthday(value = '') {
const birthday = String(value || '').trim();
const compactMatch = birthday.match(/^(\d{4})(\d{2})(\d{2})$/);
if (compactMatch) {
const [, year, month, day] = compactMatch;
return `${year}-${month}-${day}`;
}
const match = birthday.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/);
if (!match) return birthday;
const [, year, month, day] = match;
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
},
parseIdCardInfo(idCardNo = '') {
if (!/^\d{17}[\dX]$/.test(idCardNo)) {
return {};
}
const birthday = `${idCardNo.slice(6, 10)}-${idCardNo.slice(10, 12)}-${idCardNo.slice(
12,
14
)}`;
const date = new Date(`${birthday}T00:00:00`);
const validDate =
!Number.isNaN(date.getTime()) &&
date.getFullYear() === Number(idCardNo.slice(6, 10)) &&
date.getMonth() + 1 === Number(idCardNo.slice(10, 12)) &&
date.getDate() === Number(idCardNo.slice(12, 14));
return {
birthday: validDate ? birthday : '',
gender: Number(idCardNo.slice(16, 17)) % 2 === 1 ? '男' : '女',
};
},
validateFormField(prop) {
this.$refs.driverForm?.validateField(prop);
},
handleSubmit() {
this.$refs.driverForm.validate(valid => {
if (!valid) {
return;
}
const row = {
...this.driverForm,
birthday: this.normalizeBirthday(this.driverForm.birthday),
drivingLicenseStartDate: this.normalizeBirthday(this.driverForm.drivingLicenseStartDate),
drivingLicenseEndDate: this.normalizeBirthday(this.driverForm.drivingLicenseEndDate),
qualificationEndDate: this.normalizeBirthday(this.driverForm.qualificationEndDate),
qualificationNo: this.driverForm.qualificationNo || this.driverForm.idCardNo,
posts: this.driverForm.postList.join(','),
};
delete row.addressRegionPath;
this.submitLoading = true;
submit(row)
.then(() => {
this.$message.success('操作成功');
this.driverBox = 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 = {};
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 (Array.isArray(query.organizationName)) {
query.organizationName = query.organizationName[query.organizationName.length - 1] || '';
}
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/driver/export-driver', 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 || '-';
},
isDateExpiring(row, dateProp, longTermProp) {
if (row[longTermProp] === 1 || !row[dateProp]) {
return false;
}
const today = this.$dayjs();
const endDate = this.$dayjs(row[dateProp]);
return endDate.isValid() && endDate.diff(today, 'day') <= 30;
},
},
};
</script>
<style lang="scss" scoped>
.driver-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;
}
}
.driver-form {
:deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper),
:deep(.el-cascader),
:deep(.el-select),
:deep(.el-input) {
width: 100%;
}
.date-range {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 8px;
}
}
// 大尺寸证件照:按身份证 1.586:1 比例缩放(约真实尺寸 75%,240×151),左对齐显示
:deep(.driver-uploader) {
display: block;
}
:deep(.driver-uploader .el-upload),
:deep(.driver-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(.driver-uploader--large) {
display: block;
width: 240px;
margin: 0;
}
:deep(.driver-uploader--large .el-upload),
:deep(.driver-uploader--large.el-upload) {
width: 100%;
height: 151px;
}
:deep(.driver-uploader__image) {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
:deep(.driver-uploader__empty) {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #606266;
}
// 弹窗内可滚动(弹窗 body 灰底已由全局 .el-dialog__body 规则生效)
.driver-dialog {
:deep(.el-dialog__body) {
max-height: 72vh;
overflow: auto;
}
}
.driver-page__date-expiring {
color: #d40000;
font-weight: 600;
}
.el-form-item {
margin-bottom: 14px !important;
}
</style>