Files
tms-erp-web/src/views/transportCapacity/driver.vue
T
2026-09-10 22:50:18 +08:00

1877 lines
64 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 #header>
<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="6">
<el-form-item label="司机姓名" prop="driverName">
<el-input
v-model="driverForm.driverName"
maxlength="10"
show-word-limit
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="身份证号" prop="idCardNo">
<el-input
v-model="driverForm.idCardNo"
maxlength="18"
show-word-limit
placeholder="请输入"
@input="driverForm.idCardNo = String(driverForm.idCardNo || '').toUpperCase()"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="出生年月" prop="birthday">
<el-date-picker
v-model="driverForm.birthday"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="性别" prop="gender">
<el-select v-model="driverForm.gender" clearable placeholder="请选择">
<el-option label="男" value="男" />
<el-option label="女" value="女" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="民族" prop="nation">
<el-select v-model="driverForm.nation" clearable filterable placeholder="请选择">
<el-option
v-for="item in nationOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学历" prop="education">
<el-select
v-model="driverForm.education"
clearable
filterable
placeholder="请选择"
>
<el-option
v-for="item in educationOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<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="6">
<el-form-item label="详细地址" prop="address">
<el-input
v-model="driverForm.address"
maxlength="200"
show-word-limit
placeholder="请输入"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="驾驶车辆" prop="drivingVehicle">
<el-input
v-model="driverForm.drivingVehicle"
maxlength="30"
clearable
readonly
placeholder="请选择车辆"
:disabled="readonly"
@clear="driverForm.drivingVehicle = ''"
@click="!readonly && openVehicleSelector()"
>
<template #append>
<el-tooltip content="选择车辆" placement="top">
<el-button
:icon="List"
:disabled="readonly"
aria-label="选择车辆"
@click="openVehicleSelector"
/>
</el-tooltip>
</template>
</el-input>
</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="6">
<image-upload-field
label="身份证正面照"
prop="idCardFront"
:value="driverForm.idCardFront"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => handleIdCardUploadSuccess('idCardFront', url)"
/>
</el-col>
<el-col :span="6">
<image-upload-field
label="身份证反面照"
prop="idCardBack"
:value="driverForm.idCardBack"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => handleIdCardUploadSuccess('idCardBack', url)"
/>
</el-col>
<el-col :span="6">
<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
placeholder="请选择"
>
<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"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<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="起"
:disabled="driverForm.drivingLicenseLongTerm === 1"
@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="4">
<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="6">
<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="6">
<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
placeholder="请选择"
>
<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="8">
<el-form-item label="有效期" prop="qualificationEndDate">
<div class="date-range">
<el-date-picker
v-model="driverForm.qualificationStartDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="起"
:disabled="driverForm.qualificationLongTerm === 1"
@change="validateFormField('qualificationEndDate')"
/>
<el-date-picker
v-model="driverForm.qualificationEndDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="止"
:disabled="driverForm.qualificationLongTerm === 1"
@change="validateFormField('qualificationEndDate')"
/>
</div>
</el-form-item>
</el-col>
<el-col :span="4">
<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="6">
<image-upload-field
label="封面页"
prop="qualificationFront"
:value="driverForm.qualificationFront"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => handleQualificationUploadSuccess('qualificationFront', url)"
/>
</el-col>
<el-col :span="6">
<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 placeholder="请选择">
<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"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="紧急联系人姓名" prop="emergencyContactName">
<el-input
v-model="driverForm.emergencyContactName"
maxlength="20"
placeholder="请输入"
@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"
placeholder="请输入"
/>
</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
placeholder="请选择"
>
<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-cascader
v-model="organizationCascaderValue"
:options="organizationTreeOptions"
:props="organizationCascaderProps"
filterable
clearable
placeholder="请选择"
/>
</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>
<el-dialog
v-model="vehicleSelectorVisible"
title="选择车辆"
append-to-body
width="92%"
top="4vh"
class="vehicle-selector-dialog"
@closed="resetVehicleSelector"
>
<avue-crud
v-if="vehicleSelectorVisible"
ref="vehicleSelectorCrud"
v-model="vehicleSelectorForm"
v-model:page="vehicleSelectorPage"
:option="vehicleSelectorOption"
:table-loading="vehicleSelectorLoading"
:data="vehicleSelectorData"
@search-change="vehicleSelectorSearchChange"
@search-reset="vehicleSelectorSearchReset"
@current-change="vehicleSelectorCurrentChange"
@size-change="vehicleSelectorSizeChange"
@refresh-change="vehicleSelectorRefreshChange"
@on-load="loadVehicleSelector"
>
<template #plateNo="{ row }">
<span>{{ row.plateNo || '-' }}</span>
</template>
<template #organizationName="{ row }">
{{ String(row.organizationName || '').replace(/^[\s\u3000]+/, '') }}
</template>
<template #boundDriver="{ row }">
<span>{{ row.boundDriver || '-' }}</span>
</template>
<template #certificationStatus="{ row }">
<el-tag :type="getVehicleCertificationStatus(row).type" class="status-text">
{{ getVehicleCertificationStatus(row).label }}
</el-tag>
</template>
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text">
{{ row.status === 1 ? '启用' : '停用' }}
</el-tag>
</template>
<template #drivingLicenseEndDate="{ row }">
{{ formatEndDate(row.drivingLicenseEndDate, row.drivingLicenseLongTerm) }}
</template>
<template #roadTransportCertEndDate="{ row }">
{{ formatEndDate(row.roadTransportCertEndDate, row.roadTransportCertLongTerm) }}
</template>
<template #annualReviewEndDate="{ row }">
{{ formatEndDate(row.annualReviewEndDate, row.annualReviewLongTerm) }}
</template>
<template #menu="{ row }">
<el-link type="primary" @click="selectDrivingVehicle(row)">选择</el-link>
</template>
</avue-crud>
</el-dialog>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import { ElLoading } from 'element-plus';
import { List } from '@element-plus/icons-vue';
import { option } from '@/option/transportCapacity/driver';
import { option as transportVehicleOption } from '@/option/transportCapacity/transport-vehicle';
import {
getList,
getDetail,
submit,
remove,
changeStatus,
getExpiryStat,
recognitionIDCard,
recognitionTransportCertificates,
recognizeBaiduOcr,
} from '@/api/transportCapacity/driver';
import { getList as getVehicleList } from '@/api/transportCapacity/transport-vehicle';
import { getDeptTree } from '@/api/system/dept';
import organizationSearch from '@/mixins/organization-search';
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 vehicleSelectorOption = {
...transportVehicleOption,
height: 430,
selection: false,
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: false,
columnBtn: false,
menuWidth: 90,
menuFixed: 'right',
column: transportVehicleOption.column.map(column => ({
...column,
dicData: Array.isArray(column.dicData)
? column.dicData.map(item => ({ ...item }))
: column.dicData,
})),
};
const emptyForm = () => ({
id: '',
driverName: '',
idCardNo: '',
birthday: '',
gender: '',
nation: '',
education: '',
addressRegionPath: [],
addressRegion: '',
address: '',
drivingVehicle: '',
postList: ['司机'],
posts: '',
idCardFront: '',
idCardBack: '',
headPhoto: '',
drivingType: '',
drivingLicenseNo: '',
drivingLicenseStartDate: '',
drivingLicenseEndDate: '',
drivingLicenseLongTerm: 0,
drivingLicenseFront: '',
drivingLicenseBack: '',
qualificationType: '',
qualificationNo: '',
qualificationStartDate: '',
qualificationEndDate: '',
qualificationLongTerm: 0,
qualificationFront: '',
qualificationBack: '',
driverType: '自有',
mobile: '',
contactRelation: '',
organizationName: '',
emergencyContactName: '',
emergencyContactMobile: '',
status: 1,
});
export default {
components: {
ImageUploadField,
},
mixins: [organizationSearch],
data() {
return {
option,
List,
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,
vehicleSelectorVisible: false,
vehicleSelectorLoading: false,
vehicleSelectorOption,
vehicleSelectorForm: {},
vehicleSelectorData: [],
vehicleSelectorSearchForm: {},
vehicleSelectorPage: {
pageSize: 10,
currentPage: 1,
total: 0,
},
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: [
{ required: true, validator: this.validateDrivingLicenseEndDate, trigger: 'change' },
],
qualificationType: [
{ required: true, message: '请选择从业资格认证类型', trigger: 'change' },
],
qualificationNo: [{ required: true, message: '请输入资格证号', trigger: 'blur' }],
qualificationEndDate: [
{ required: true, validator: this.validateQualificationEndDate, 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']),
// 所属组织级联:表单存组织名称,级联控件用 id 路径,两者在此双向转换
organizationCascaderValue: {
get() {
return this.resolveOrgPath(this.driverForm.organizationName);
},
set(path) {
this.driverForm.organizationName = this.resolveOrgName(path);
},
},
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.loadOrganizationOptions();
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;
const vehicleColumn = this.findColumn(
this.vehicleSelectorOption.column,
'organizationName'
);
vehicleColumn.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.drivingLicenseLongTerm === 1) {
callback();
return;
}
if (!this.driverForm.drivingLicenseStartDate) {
callback(new Error('请选择驾驶证有效期起'));
return;
}
if (!value) {
callback(new Error('请选择驾驶证有效期止'));
return;
}
callback();
},
validateQualificationEndDate(rule, value, callback) {
// 勾选长期有效:起止均已禁用,不再校验
if (this.driverForm.qualificationLongTerm === 1) {
callback();
return;
}
if (!this.driverForm.qualificationStartDate) {
callback(new Error('请选择从业资格证有效期起'));
return;
}
if (!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();
},
openVehicleSelector() {
if (this.readonly) return;
this.vehicleSelectorPage.currentPage = 1;
this.vehicleSelectorVisible = true;
},
resetVehicleSelector() {
this.vehicleSelectorForm = {};
this.vehicleSelectorData = [];
this.vehicleSelectorSearchForm = {};
this.vehicleSelectorPage = {
pageSize: 10,
currentPage: 1,
total: 0,
};
},
normalizeVehicleSelectorQuery(params = {}) {
const query = {
...this.vehicleSelectorSearchForm,
...params,
};
if (Array.isArray(query.organizationName)) {
query.organizationName = query.organizationName[query.organizationName.length - 1] || '';
}
return query;
},
loadVehicleSelector(page = this.vehicleSelectorPage, params = {}) {
this.vehicleSelectorLoading = true;
getVehicleList(page.currentPage, page.pageSize, this.normalizeVehicleSelectorQuery(params))
.then(res => {
const data = res.data.data || {};
this.vehicleSelectorData = data.records || [];
this.vehicleSelectorPage.total = data.total || 0;
})
.finally(() => {
this.vehicleSelectorLoading = false;
});
},
vehicleSelectorSearchChange(params, done) {
this.vehicleSelectorSearchForm = params;
this.vehicleSelectorPage.currentPage = 1;
this.loadVehicleSelector(this.vehicleSelectorPage, params);
done();
},
vehicleSelectorSearchReset() {
this.vehicleSelectorSearchForm = {};
this.vehicleSelectorPage.currentPage = 1;
this.loadVehicleSelector();
},
vehicleSelectorCurrentChange(currentPage) {
this.vehicleSelectorPage.currentPage = currentPage;
},
vehicleSelectorSizeChange(pageSize) {
this.vehicleSelectorPage.pageSize = pageSize;
},
vehicleSelectorRefreshChange() {
this.loadVehicleSelector();
},
selectDrivingVehicle(row) {
this.driverForm.drivingVehicle = row.plateNo || '';
this.vehicleSelectorVisible = false;
},
getVehicleCertificationStatus(row) {
const statusMap = {
0: { label: '认证中', type: 'warning' },
1: { label: '已认证', type: 'success' },
2: { label: '已驳回', type: 'danger' },
};
return statusMap[row.certificationStatus] || { label: '-', type: 'info' };
},
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),
qualificationStartDate: this.normalizeBirthday(this.driverForm.qualificationStartDate),
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) {
// 所属组织级联返回的是 id 路径,转成组织名称传给后端
this.normalizeOrganizationSearch(params);
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;
margin-top: 8px;
margin-bottom: 16px;
: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>