1、大量页面调整

2、完善运力模块
3、完善规范、需求卡
This commit is contained in:
2026-07-31 01:32:41 +08:00
parent ccaca521d3
commit 81b2a3e99b
54 changed files with 5641 additions and 1965 deletions

View File

@@ -1,7 +1,22 @@
<template>
<basic-container class="driver-page">
<div class="driver-toolbar">
<div class="driver-toolbar__actions">
<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"
@@ -26,30 +41,20 @@
@click="handleDelete"
>批量删除
</el-button>
</div>
<el-radio-group v-model="query.expireStatus" class="driver-stat" @change="handleExpireChange">
<el-radio-button label="">全部{{ expiryStat.total }}</el-radio-button>
<el-radio-button label="within30">30天内到期{{ expiryStat.within30 }}</el-radio-button>
<el-radio-button label="expired">已到期{{ expiryStat.expired }}</el-radio-button>
</el-radio-group>
</div>
<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>
<template #menu-right>
<el-radio-group
v-model="query.expireStatus"
class="driver-stat"
@change="handleExpireChange"
>
<el-radio-button label="">全部{{ expiryStat.total }}</el-radio-button>
<el-radio-button label="within30"
>30天内到期{{ expiryStat.within30 }}</el-radio-button
>
<el-radio-button label="expired">已到期{{ expiryStat.expired }}</el-radio-button>
</el-radio-group>
</template>
<template #driverName="{ row }">
<el-button type="primary" link @click="openDriver(row, true)">
{{ row.driverName }}
@@ -61,12 +66,28 @@
</el-tag>
</template>
<template #drivingLicenseEndDate="{ row }">
<span :class="getDateClass(row, 'drivingLicenseEndDate', 'drivingLicenseLongTerm')">
<span
:class="{
'driver-page__date-expiring': isDateExpiring(
row,
'drivingLicenseEndDate',
'drivingLicenseLongTerm'
),
}"
>
{{ formatEndDate(row.drivingLicenseEndDate, row.drivingLicenseLongTerm) }}
</span>
</template>
<template #qualificationEndDate="{ row }">
<span :class="getDateClass(row, 'qualificationEndDate', 'qualificationLongTerm')">
<span
:class="{
'driver-page__date-expiring': isDateExpiring(
row,
'qualificationEndDate',
'qualificationLongTerm'
),
}"
>
{{ formatEndDate(row.qualificationEndDate, row.qualificationLongTerm) }}
</span>
</template>
@@ -120,12 +141,12 @@
>
<div class="section-title">基础身份信息</div>
<el-row :gutter="18">
<el-col :span="6">
<el-col :span="8">
<el-form-item label="司机姓名" prop="driverName">
<el-input v-model="driverForm.driverName" maxlength="20" show-word-limit />
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :span="8">
<el-form-item label="身份证号" prop="idCardNo">
<el-input
v-model="driverForm.idCardNo"
@@ -135,12 +156,14 @@
/>
</el-form-item>
</el-col>
<el-col :span="4">
<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-col :span="4">
</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="男" />
@@ -148,12 +171,14 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-col :span="8">
<el-form-item label="民族" prop="nation">
<el-input v-model="driverForm.nation" maxlength="20" />
<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="4">
<el-col :span="8">
<el-form-item label="学历" prop="education">
<el-select v-model="driverForm.education" clearable filterable>
<el-option
@@ -165,6 +190,8 @@
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="住址" prop="addressRegion">
<el-input
@@ -179,15 +206,7 @@
<el-input v-model="driverForm.address" maxlength="200" show-word-limit />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="状态" prop="status">
<el-select v-model="driverForm.status">
<el-option label="启用" :value="1" />
<el-option label="停用" :value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-col :span="8">
<el-form-item label="岗位" prop="postList">
<el-checkbox-group v-model="driverForm.postList">
<el-checkbox label="司机" />
@@ -196,6 +215,8 @@
</el-checkbox-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="8">
<image-upload-field
label="身份证正面照"
@@ -203,6 +224,7 @@
:value="driverForm.idCardFront"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => setImage('idCardFront', url)"
/>
</el-col>
@@ -213,6 +235,7 @@
:value="driverForm.idCardBack"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => setImage('idCardBack', url)"
/>
</el-col>
@@ -223,6 +246,7 @@
:value="driverForm.headPhoto"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => setImage('headPhoto', url)"
/>
</el-col>
@@ -253,7 +277,7 @@
<el-input v-model="driverForm.drivingLicenseNo" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="有效期" prop="drivingLicenseEndDate">
<div class="date-range">
<el-date-picker
@@ -261,6 +285,7 @@
type="date"
value-format="YYYY-MM-DD"
placeholder="起"
@change="validateFormField('drivingLicenseEndDate')"
/>
<el-date-picker
v-model="driverForm.drivingLicenseEndDate"
@@ -268,21 +293,25 @@
value-format="YYYY-MM-DD"
placeholder="止"
:disabled="driverForm.drivingLicenseLongTerm === 1"
@change="validateFormField('drivingLicenseEndDate')"
/>
</div>
</el-form-item>
</el-col>
<el-col :span="4">
<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="驾驶证主页"
@@ -310,7 +339,7 @@
<div class="section-title">从业资格证信息</div>
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="从业资格证类型" prop="qualificationType">
<el-form-item label="从业资格证类型" prop="qualificationType">
<el-select
v-model="driverForm.qualificationType"
clearable
@@ -318,8 +347,12 @@
allow-create
default-first-option
>
<el-option label="道路运输从业资格证" value="道路运输从业资格证" />
<el-option label="危险货物运输从业资格证" value="危险货物运输从业资格证" />
<el-option
v-for="item in qualificationTypeOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
@@ -339,20 +372,24 @@
type="date"
value-format="YYYY-MM-DD"
:disabled="driverForm.qualificationLongTerm === 1"
@change="validateFormField('qualificationEndDate')"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<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="封面页"
@@ -379,30 +416,32 @@
<div class="section-title">司机联系信息</div>
<el-row :gutter="18">
<el-col :span="6">
<el-col :span="8">
<el-form-item label="司机类型" prop="driverType">
<el-select v-model="driverForm.driverType">
<el-select v-model="driverForm.driverType" filterable>
<el-option label="自有" value="自有" />
<el-option label="外协" value="外协" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :span="8">
<el-form-item label="手机号" prop="mobile">
<el-input v-model="driverForm.mobile" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :span="8">
<el-form-item label="紧急联系人姓名" prop="emergencyContactName">
<el-input v-model="driverForm.emergencyContactName" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="6">
</el-row>
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="紧急联系人手机号" prop="emergencyContactMobile">
<el-input v-model="driverForm.emergencyContactMobile" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :span="8">
<el-form-item label="与联系人关系" prop="contactRelation">
<el-select
v-model="driverForm.contactRelation"
@@ -420,26 +459,16 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :span="8">
<el-form-item label="所属组织" prop="organizationName">
<el-cascader
v-model="driverForm.organizationName"
:options="deptCascaderOptions"
:props="deptCascaderProps"
clearable
filterable
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注" prop="remark">
<el-input
v-model="driverForm.remark"
type="textarea"
maxlength="200"
show-word-limit
/>
<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>
@@ -470,6 +499,7 @@ import { getDeptTree } from '@/api/system/dept';
import { exportBlob } from '@/api/common';
import { getToken } from '@/utils/auth';
import { downloadXls } from '@/utils/util';
import ImageUploadField from '@/components/image-upload-field/main.vue';
const emptyForm = () => ({
id: '',
@@ -505,64 +535,12 @@ const emptyForm = () => ({
organizationName: '',
emergencyContactName: '',
emergencyContactMobile: '',
remark: '',
status: 1,
});
const imageUploadField = {
name: 'ImageUploadField',
props: {
label: String,
prop: String,
value: String,
readonly: Boolean,
headers: Object,
large: Boolean,
},
emits: ['success'],
methods: {
handleSuccess(res) {
if (res.code === 200 && res.data) {
this.$emit('success', res.data.link || res.data.url || res.data.domain || '');
} else {
this.$message.error(res.msg || '上传失败');
}
},
beforeUpload(file) {
const validType = ['image/jpeg', 'image/png', 'image/jpg', 'image/bmp'].includes(file.type);
const validSize = file.size / 1024 / 1024 < 5;
if (!validType) {
this.$message.error('仅支持 JPG、PNG、BMP 图片');
}
if (!validSize) {
this.$message.error('图片大小不能超过 5MB');
}
return validType && validSize;
},
},
template: `
<el-form-item :label="label" :prop="prop">
<el-upload
class="driver-uploader"
:class="{ 'driver-uploader--large': large }"
action="/api/blade-resource/oss/endpoint/put-file"
name="file"
:headers="headers"
:show-file-list="false"
:disabled="readonly"
:before-upload="beforeUpload"
:on-success="handleSuccess"
>
<img v-if="value" :src="value" class="driver-uploader__image" />
<div v-else class="driver-uploader__empty">上传证件图片</div>
</el-upload>
</el-form-item>
`,
};
export default {
components: {
ImageUploadField: imageUploadField,
ImageUploadField,
},
data() {
return {
@@ -594,24 +572,108 @@ export default {
'Blade-Auth': `bearer ${getToken()}`,
'Blade-Requested-With': 'BladeHttpRequest',
},
nationOptions: [
'汉族',
'蒙古族',
'回族',
'藏族',
'维吾尔族',
'苗族',
'彝族',
'壮族',
'布依族',
'朝鲜族',
'满族',
'侗族',
'瑶族',
'白族',
'土家族',
'哈尼族',
'哈萨克族',
'傣族',
'黎族',
'傈僳族',
'佤族',
'畲族',
'高山族',
'拉祜族',
'水族',
'东乡族',
'纳西族',
'景颇族',
'柯尔克孜族',
'土族',
'达斡尔族',
'仫佬族',
'羌族',
'布朗族',
'撒拉族',
'毛南族',
'仡佬族',
'锡伯族',
'阿昌族',
'普米族',
'塔吉克族',
'怒族',
'乌孜别克族',
'俄罗斯族',
'鄂温克族',
'德昂族',
'保安族',
'裕固族',
'京族',
'塔塔尔族',
'独龙族',
'鄂伦春族',
'赫哲族',
'门巴族',
'珞巴族',
'基诺族',
],
educationOptions: ['初中', '高中', '中专', '大专', '本科', '硕士及以上'],
drivingTypeOptions: ['A1', 'A2', 'A3', 'B1', 'B2', 'C1', 'C2'],
qualificationTypeOptions: [
'放射品押运员',
'道路运输从业资格证',
'危险货物运输从业资格证',
'A1',
'A2',
'B1',
'B2',
'C1',
'C2',
],
relationOptions: ['父母', '配偶', '子女', '兄弟姐妹', '朋友', '同事'],
deptCascaderOptions: [],
deptCascaderProps: {
label: 'label',
value: 'value',
children: 'children',
checkStrictly: true,
emitPath: false,
},
organizationOptions: [],
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: [
@@ -625,7 +687,6 @@ export default {
idCardBack: [{ required: true, message: '请上传身份证反面照', trigger: 'change' }],
drivingLicenseFront: [{ required: true, message: '请上传驾驶证主页', trigger: 'change' }],
drivingLicenseBack: [{ required: true, message: '请上传驾驶证副页', trigger: 'change' }],
remark: [{ max: 200, message: '备注最多200字', trigger: 'blur' }],
},
};
},
@@ -659,31 +720,43 @@ export default {
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => {
this.deptCascaderOptions = this.formatDeptCascaderOptions(res.data.data || []);
this.organizationOptions = this.formatDeptOptions(res.data.data || []);
const column = this.findColumn(this.option.column, 'organizationName');
column.dicData = this.deptCascaderOptions;
column.dicData = this.organizationOptions;
});
},
formatDeptCascaderOptions(tree = []) {
return tree.map(item => {
formatDeptOptions(tree = [], level = 0) {
const result = [];
tree.forEach(item => {
const label = item.title || item.deptName || item.name;
const option = {
label,
result.push({
label: `${' '.repeat(level)}${label}`,
value: label,
};
});
if (item.children && item.children.length) {
option.children = this.formatDeptCascaderOptions(item.children);
result.push(...this.formatDeptOptions(item.children, level + 1));
}
return option;
});
return result;
},
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();
},
openDriver(row, readonly = false) {
this.readonly = readonly;
if (!row?.id) {
@@ -711,6 +784,9 @@ export default {
this.driverForm[prop] = url;
this.$refs.driverForm?.validateField(prop);
},
validateFormField(prop) {
this.$refs.driverForm?.validateField(prop);
},
handleSubmit() {
this.$refs.driverForm.validate(valid => {
if (!valid) {
@@ -863,19 +939,13 @@ export default {
}
return date || '-';
},
getDateClass(row, dateProp, longTermProp) {
isDateExpiring(row, dateProp, longTermProp) {
if (row[longTermProp] === 1 || !row[dateProp]) {
return '';
return false;
}
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 '';
return endDate.isValid() && endDate.diff(today, 'day') <= 30;
},
},
};
@@ -883,24 +953,22 @@ export default {
<style lang="scss" scoped>
.driver-page {
.driver-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
}
.driver-toolbar__actions {
display: flex;
align-items: center;
gap: 8px;
}
.driver-stat {
flex-shrink: 0;
}
:deep(.avue-crud__search .avue-form__menu),
:deep(.avue-crud__search .avue-form__menu--right) {
flex: 0 0 auto;
width: auto;
max-width: none;
}
:deep(.avue-crud__search .avue-form__menu--right) {
margin-left: 12px;
justify-content: flex-end;
}
:deep(.el-table th .cell),
:deep(.el-table td .cell) {
white-space: nowrap;
@@ -935,7 +1003,16 @@ export default {
}
}
:deep(.driver-uploader .el-upload) {
:deep(.driver-uploader) {
display: block;
width: 100%;
}
: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;
@@ -943,7 +1020,8 @@ export default {
overflow: hidden;
}
:deep(.driver-uploader--large .el-upload) {
:deep(.driver-uploader--large .el-upload),
:deep(.driver-uploader--large.el-upload) {
height: 260px;
}
@@ -963,21 +1041,23 @@ export default {
color: #606266;
}
.date-warning {
color: #f56c6c;
font-weight: 600;
.driver-dialog {
:deep(.el-dialog__body) {
max-height: 72vh;
overflow: auto;
}
}
.date-danger {
.driver-page__date-expiring {
color: #d40000;
font-weight: 600;
}
@media (max-width: 1200px) {
.driver-page {
.driver-toolbar {
align-items: flex-start;
flex-direction: column;
.driver-stat {
display: flex;
flex-wrap: wrap;
}
}
}