截止‘运力管理’模块

This commit is contained in:
2026-07-20 11:26:51 +08:00
parent e66b4a3680
commit c6dffb5d97
98 changed files with 19429 additions and 176 deletions

View File

@@ -0,0 +1,875 @@
<template>
<basic-container class="driver-page">
<div class="driver-toolbar">
<div class="driver-toolbar__actions">
<el-button
type="primary"
icon="el-icon-plus"
plain
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>
</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 #driverName="{ row }">
<el-button type="primary" link @click="openDriver(row, true)">
{{ row.driverName }}
</el-button>
</template>
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'primary' : 'info'">
{{ row.status === 1 ? '启用' : '停用' }}
</el-tag>
</template>
<template #drivingLicenseEndDate="{ row }">
<span :class="getDateClass(row, 'drivingLicenseEndDate', 'drivingLicenseLongTerm')">
{{ formatEndDate(row.drivingLicenseEndDate, row.drivingLicenseLongTerm) }}
</span>
</template>
<template #qualificationEndDate="{ row }">
<span :class="getDateClass(row, 'qualificationEndDate', 'qualificationLongTerm')">
{{ formatEndDate(row.qualificationEndDate, row.qualificationLongTerm) }}
</span>
</template>
<template #menu="{ row }">
<el-button
type="primary"
text
icon="el-icon-view"
v-if="hasPermission('driver_view')"
@click="openDriver(row, true)"
>
查看
</el-button>
<el-button
type="primary"
text
icon="el-icon-edit"
v-if="hasPermission('driver_edit')"
@click="openDriver(row)"
>
修改
</el-button>
<el-button
type="primary"
text
:icon="row.status === 1 ? 'el-icon-close' : 'el-icon-check'"
v-if="hasPermission('driver_status')"
@click="handleStatus(row)"
>
{{ row.status === 1 ? '停用' : '启用' }}
</el-button>
</template>
</avue-crud>
<el-dialog
:title="dialogTitle"
append-to-body
v-model="driverBox"
width="92%"
top="4vh"
class="driver-dialog"
@closed="resetDriver"
>
<el-form
ref="driverForm"
:model="driverForm"
:rules="formRules"
label-position="top"
:disabled="readonly"
class="driver-form"
>
<div class="section-title">基础身份信息</div>
<el-row :gutter="18">
<el-col :span="6">
<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-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="4">
<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-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="4">
<el-form-item label="民族" prop="nation">
<el-input v-model="driverForm.nation" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="4">
<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-col :span="8">
<el-form-item label="住址" prop="addressRegion">
<el-input v-model="driverForm.addressRegion" maxlength="100" placeholder="省 / 市 / 区" />
</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="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-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-col :span="8">
<image-upload-field
label="身份证正面照"
prop="idCardFront"
:value="driverForm.idCardFront"
:readonly="readonly"
:headers="uploadHeaders"
@success="url => setImage('idCardFront', url)"
/>
</el-col>
<el-col :span="8">
<image-upload-field
label="身份证反面照"
prop="idCardBack"
:value="driverForm.idCardBack"
:readonly="readonly"
:headers="uploadHeaders"
@success="url => setImage('idCardBack', url)"
/>
</el-col>
<el-col :span="8">
<image-upload-field
label="大头照"
prop="headPhoto"
:value="driverForm.headPhoto"
:readonly="readonly"
:headers="uploadHeaders"
@success="url => setImage('headPhoto', url)"
/>
</el-col>
</el-row>
<div class="section-title">驾驶证信息</div>
<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="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="起"
/>
<el-date-picker
v-model="driverForm.drivingLicenseEndDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="止"
:disabled="driverForm.drivingLicenseLongTerm === 1"
/>
</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">
</el-checkbox>
</el-form-item>
</el-col>
<el-col :span="12">
<image-upload-field
label="驾驶证主页"
prop="drivingLicenseFront"
:value="driverForm.drivingLicenseFront"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => setImage('drivingLicenseFront', url)"
/>
</el-col>
<el-col :span="12">
<image-upload-field
label="驾驶证副页"
prop="drivingLicenseBack"
:value="driverForm.drivingLicenseBack"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => setImage('drivingLicenseBack', url)"
/>
</el-col>
</el-row>
<div class="section-title">从业资格证信息</div>
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="从业资格证类型" prop="qualificationType">
<el-select v-model="driverForm.qualificationType" clearable filterable allow-create default-first-option>
<el-option label="道路运输从业资格证" value="道路运输从业资格证" />
<el-option label="危险货物运输从业资格证" value="危险货物运输从业资格证" />
</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"
/>
</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">
</el-checkbox>
</el-form-item>
</el-col>
<el-col :span="12">
<image-upload-field
label="封面页"
prop="qualificationFront"
:value="driverForm.qualificationFront"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => setImage('qualificationFront', url)"
/>
</el-col>
<el-col :span="12">
<image-upload-field
label="内容页"
prop="qualificationBack"
:value="driverForm.qualificationBack"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => setImage('qualificationBack', url)"
/>
</el-col>
</el-row>
<div class="section-title">司机联系信息</div>
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="司机类型" prop="driverType">
<el-select v-model="driverForm.driverType">
<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" />
</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-input v-model="driverForm.organizationName" maxlength="50" />
</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-form-item>
</el-col>
</el-row>
</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 { option } from '@/option/transportCapacity/driver';
import { getList, getDetail, submit, remove, changeStatus, getExpiryStat } from '@/api/transportCapacity/driver';
import { exportBlob } from '@/api/common';
import { getToken } from '@/utils/auth';
import { downloadXls } from '@/utils/util';
const emptyForm = () => ({
id: '',
driverName: '',
idCardNo: '',
birthday: '',
gender: '',
nation: '',
education: '',
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: '',
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,
},
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(),
uploadHeaders: {
'Blade-Auth': `bearer ${getToken()}`,
'Blade-Requested-With': 'BladeHttpRequest',
},
educationOptions: ['初中', '高中', '中专', '大专', '本科', '硕士及以上'],
drivingTypeOptions: ['A1', 'A2', 'A3', 'B1', 'B2', 'C1', 'C2'],
relationOptions: ['父母', '配偶', '子女', '兄弟姐妹', '朋友', '同事'],
formRules: {
driverName: [{ required: true, message: '请输入司机姓名', trigger: 'blur' }],
idCardNo: [{ required: true, message: '请输入身份证号', trigger: 'blur' }],
drivingType: [{ required: true, message: '请选择准驾车型', trigger: 'change' }],
drivingLicenseEndDate: [{ validator: this.validateDrivingLicenseEndDate, trigger: 'change' }],
driverType: [{ required: true, message: '请选择司机类型', trigger: 'change' }],
mobile: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
emergencyContactName: [{ required: true, message: '请输入紧急联系人姓名', trigger: 'blur' }],
emergencyContactMobile: [{ required: true, message: '请输入紧急联系人手机号', trigger: 'blur' }],
organizationName: [{ required: true, message: '请输入所属组织', trigger: 'blur' }],
idCardFront: [{ required: true, message: '请上传身份证正面照', trigger: 'change' }],
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' }],
},
};
},
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 ? '修改司机' : '新增司机';
},
},
methods: {
hasPermission(code) {
return this.isAdmin || this.permission?.[code] === true;
},
validateDrivingLicenseEndDate(rule, value, callback) {
if (this.driverForm.drivingLicenseLongTerm !== 1 && !value) {
callback(new Error('请选择驾驶证有效期止'));
return;
}
callback();
},
openDriver(row, readonly = false) {
this.readonly = readonly;
if (!row?.id) {
this.driverForm = emptyForm();
this.driverBox = true;
return;
}
getDetail(row.id).then(res => {
const detail = res.data.data || {};
this.driverForm = {
...emptyForm(),
...detail,
postList: detail.posts ? detail.posts.split(',').filter(Boolean) : [],
};
this.driverBox = true;
});
},
resetDriver() {
this.driverForm = emptyForm();
this.readonly = false;
this.submitLoading = false;
this.$refs.driverForm?.clearValidate();
},
setImage(prop, url) {
this.driverForm[prop] = url;
this.$refs.driverForm?.validateField(prop);
},
handleSubmit() {
this.$refs.driverForm.validate(valid => {
if (!valid) {
return;
}
const row = {
...this.driverForm,
qualificationNo: this.driverForm.qualificationNo || this.driverForm.idCardNo,
posts: this.driverForm.postList.join(','),
};
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() {
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 (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())
.then(res => {
downloadXls(res.data, `司机管理${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
})
.finally(() => {
NProgress.done();
});
});
},
buildExportParams() {
return {
...this.buildQuery(true),
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
},
formatEndDate(date, longTerm) {
if (longTerm === 1) {
return '长期有效';
}
return date || '-';
},
getDateClass(row, dateProp, longTermProp) {
if (row[longTermProp] === 1 || !row[dateProp]) {
return '';
}
const today = this.$dayjs();
const endDate = this.$dayjs(row[dateProp]);
if (endDate.isBefore(today, 'day')) {
return 'date-danger';
}
if (endDate.diff(today, 'day') <= 30) {
return 'date-warning';
}
return '';
},
},
};
</script>
<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(.el-table th .cell),
:deep(.el-table td .cell) {
white-space: nowrap;
}
}
.driver-form {
.section-title {
margin: 18px 0 14px;
padding-left: 10px;
border-left: 3px solid #e74b5f;
color: #303133;
font-weight: 600;
line-height: 18px;
}
.section-title:first-child {
margin-top: 0;
}
:deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper),
:deep(.el-select),
:deep(.el-input) {
width: 100%;
}
.date-range {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 8px;
}
}
:deep(.driver-uploader .el-upload) {
width: 100%;
height: 170px;
border: 1px dashed #c0c4cc;
background: #fafafa;
overflow: hidden;
}
:deep(.driver-uploader--large .el-upload) {
height: 260px;
}
: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;
}
.date-warning {
color: #f56c6c;
font-weight: 600;
}
.date-danger {
color: #d40000;
font-weight: 600;
}
@media (max-width: 1200px) {
.driver-page {
.driver-toolbar {
align-items: flex-start;
flex-direction: column;
}
}
}
</style>

View File

@@ -0,0 +1,603 @@
<template>
<basic-container class="transport-ship-page">
<div class="ship-toolbar">
<div class="ship-toolbar__actions">
<el-button type="primary" icon="el-icon-plus" plain v-if="hasPermission('transport_ship_add')" @click="openShip()">
新增
</el-button>
<el-button type="primary" icon="el-icon-download" plain v-if="hasPermission('transport_ship_export')" @click="handleExport">
导出
</el-button>
<el-button type="danger" icon="el-icon-delete" plain v-if="hasPermission('transport_ship_delete')" @click="handleDelete">
批量删除
</el-button>
</div>
<el-radio-group v-model="query.expireStatus" class="ship-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 #shipName="{ row }">
<el-button type="primary" link @click="openShip(row, true)">
{{ row.shipName }}
</el-button>
</template>
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'primary' : 'info'">
{{ row.status === 1 ? '启用' : '停用' }}
</el-tag>
</template>
<template #nationalityCertEndDate="{ row }">
<span :class="getDateClass(row, 'nationalityCertEndDate', 'nationalityCertLongTerm')">
{{ formatEndDate(row.nationalityCertEndDate, row.nationalityCertLongTerm) }}
</span>
</template>
<template #safeManningCertEndDate="{ row }">
<span :class="getDateClass(row, 'safeManningCertEndDate', 'safeManningCertLongTerm')">
{{ formatEndDate(row.safeManningCertEndDate, row.safeManningCertLongTerm) }}
</span>
</template>
<template #businessTransportCertEndDate="{ row }">
<span :class="getDateClass(row, 'businessTransportCertEndDate', 'businessTransportCertLongTerm')">
{{ formatEndDate(row.businessTransportCertEndDate, row.businessTransportCertLongTerm) }}
</span>
</template>
<template #leaseEndDate="{ row }">
<span :class="getDateClass(row, 'leaseEndDate', 'leaseLongTerm')">
{{ formatEndDate(row.leaseEndDate, row.leaseLongTerm) }}
</span>
</template>
<template #menu="{ row }">
<el-button type="primary" text icon="el-icon-edit" v-if="hasPermission('transport_ship_edit')" @click="openShip(row)">
修改
</el-button>
<el-button type="primary" text icon="el-icon-view" v-if="hasPermission('transport_ship_view')" @click="openShip(row, true)">
综合台账
</el-button>
<el-button
type="primary"
text
:icon="row.status === 1 ? 'el-icon-close' : 'el-icon-check'"
v-if="hasPermission('transport_ship_status')"
@click="handleStatus(row)"
>
{{ row.status === 1 ? '停用' : '启用' }}
</el-button>
</template>
</avue-crud>
<el-dialog :title="dialogTitle" append-to-body v-model="shipBox" width="92%" top="4vh" @closed="resetShip">
<el-form ref="shipForm" :model="shipForm" :rules="formRules" label-position="top" :disabled="readonly" class="ship-form">
<div class="section-title">基础船舶信息</div>
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="船舶名" prop="shipName">
<el-input v-model="shipForm.shipName" maxlength="50" show-word-limit />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="船舶识别号" prop="shipIdentifierNo">
<el-input v-model="shipForm.shipIdentifierNo" maxlength="50" show-word-limit @input="shipForm.shipIdentifierNo = String(shipForm.shipIdentifierNo || '').toUpperCase()" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="所属组织" prop="organizationName">
<el-input v-model="shipForm.organizationName" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="船检登记号" prop="shipInspectionNo">
<el-input v-model="shipForm.shipInspectionNo" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="船舶类型" prop="shipType">
<el-select v-model="shipForm.shipType" filterable allow-create default-first-option>
<el-option v-for="item in shipTypeOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="状态" prop="status">
<el-select v-model="shipForm.status">
<el-option label="启用" :value="1" />
<el-option label="停用" :value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注" prop="remark">
<el-input v-model="shipForm.remark" type="textarea" maxlength="200" show-word-limit />
</el-form-item>
</el-col>
</el-row>
<div class="section-title">国籍证信息</div>
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="国籍证有效期至" prop="nationalityCertEndDate">
<el-date-picker v-model="shipForm.nationalityCertEndDate" type="date" value-format="YYYY-MM-DD" :disabled="shipForm.nationalityCertLongTerm === 1" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="长期有效" prop="nationalityCertLongTerm">
<el-checkbox v-model="shipForm.nationalityCertLongTerm" :true-label="1" :false-label="0"></el-checkbox>
</el-form-item>
</el-col>
<el-col :span="12">
<image-upload-field label="国籍证图片" prop="nationalityCertImage" :value="shipForm.nationalityCertImage" :readonly="readonly" :headers="uploadHeaders" large @success="url => setImage('nationalityCertImage', url)" />
</el-col>
</el-row>
<div class="section-title">安全配员证信息</div>
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="最低安全配员证书有效期至" prop="safeManningCertEndDate">
<el-date-picker v-model="shipForm.safeManningCertEndDate" type="date" value-format="YYYY-MM-DD" :disabled="shipForm.safeManningCertLongTerm === 1" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="长期有效" prop="safeManningCertLongTerm">
<el-checkbox v-model="shipForm.safeManningCertLongTerm" :true-label="1" :false-label="0"></el-checkbox>
</el-form-item>
</el-col>
<el-col :span="12">
<image-upload-field label="最低安全配员证书图片" prop="safeManningCertImage" :value="shipForm.safeManningCertImage" :readonly="readonly" :headers="uploadHeaders" large @success="url => setImage('safeManningCertImage', url)" />
</el-col>
</el-row>
<div class="section-title">营业运输证信息</div>
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="营业运输证有效期至" prop="businessTransportCertEndDate">
<el-date-picker v-model="shipForm.businessTransportCertEndDate" type="date" value-format="YYYY-MM-DD" :disabled="shipForm.businessTransportCertLongTerm === 1" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="长期有效" prop="businessTransportCertLongTerm">
<el-checkbox v-model="shipForm.businessTransportCertLongTerm" :true-label="1" :false-label="0"></el-checkbox>
</el-form-item>
</el-col>
<el-col :span="12">
<image-upload-field label="营业运输证图片" prop="businessTransportCertImage" :value="shipForm.businessTransportCertImage" :readonly="readonly" :headers="uploadHeaders" large @success="url => setImage('businessTransportCertImage', url)" />
</el-col>
</el-row>
<div class="section-title">承租信息</div>
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="承租有效期至" prop="leaseEndDate">
<el-date-picker v-model="shipForm.leaseEndDate" type="date" value-format="YYYY-MM-DD" :disabled="shipForm.leaseLongTerm === 1" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="长期有效" prop="leaseLongTerm">
<el-checkbox v-model="shipForm.leaseLongTerm" :true-label="1" :false-label="0"></el-checkbox>
</el-form-item>
</el-col>
<el-col :span="12">
<image-upload-field label="承租合同图片" prop="leaseContractImage" :value="shipForm.leaseContractImage" :readonly="readonly" :headers="uploadHeaders" large @success="url => setImage('leaseContractImage', url)" />
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="shipBox = 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 { option } from '@/option/transportCapacity/transport-ship';
import { getList, getDetail, submit, remove, changeStatus, getExpiryStat } from '@/api/transportCapacity/transport-ship';
import { exportBlob } from '@/api/common';
import { getToken } from '@/utils/auth';
import { downloadXls } from '@/utils/util';
const emptyForm = () => ({
id: '',
shipName: '',
shipIdentifierNo: '',
organizationName: '',
shipInspectionNo: '',
shipType: '',
nationalityCertEndDate: '',
nationalityCertLongTerm: 0,
nationalityCertImage: '',
safeManningCertEndDate: '',
safeManningCertLongTerm: 0,
safeManningCertImage: '',
businessTransportCertEndDate: '',
businessTransportCertLongTerm: 0,
businessTransportCertImage: '',
leaseEndDate: '',
leaseLongTerm: 0,
leaseContractImage: '',
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="ship-uploader"
:class="{ 'ship-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="ship-uploader__image" />
<div v-else class="ship-uploader__empty">上传证件图片</div>
</el-upload>
</el-form-item>
`,
};
export default {
components: { ImageUploadField: 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 },
shipBox: false,
readonly: false,
shipForm: emptyForm(),
uploadHeaders: {
'Blade-Auth': `bearer ${getToken()}`,
'Blade-Requested-With': 'BladeHttpRequest',
},
shipTypeOptions: ['干货船', '集装箱船', '散货船', '油船', '拖轮', '驳船'],
formRules: {
shipName: [{ required: true, message: '请输入船舶名', trigger: 'blur' }],
shipIdentifierNo: [{ required: true, message: '请输入船舶识别号', trigger: 'blur' }],
organizationName: [{ required: true, message: '请输入所属组织', trigger: 'blur' }],
shipType: [{ required: true, message: '请选择船舶类型', trigger: 'change' }],
nationalityCertEndDate: [{ validator: this.validateNationalityCertEndDate, trigger: 'change' }],
safeManningCertEndDate: [{ validator: this.validateSafeManningCertEndDate, trigger: 'change' }],
businessTransportCertEndDate: [{ validator: this.validateBusinessTransportCertEndDate, trigger: 'change' }],
remark: [{ max: 200, message: '备注最多200字', trigger: 'blur' }],
},
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
const authority = this.userInfo.authority || '';
return authority.includes('admin');
},
permissionList() {
return {
addBtn: this.hasPermission('transport_ship_add'),
viewBtn: this.hasPermission('transport_ship_view'),
delBtn: this.hasPermission('transport_ship_delete'),
editBtn: this.hasPermission('transport_ship_edit'),
};
},
dialogTitle() {
if (this.readonly) return '船舶综合台账';
return this.shipForm.id ? '修改船舶' : '新增船舶';
},
},
methods: {
hasPermission(code) {
return this.isAdmin || this.permission?.[code] === true;
},
validateNationalityCertEndDate(rule, value, callback) {
if (this.shipForm.nationalityCertLongTerm !== 1 && !value) return callback(new Error('请选择国籍证有效期至'));
callback();
},
validateSafeManningCertEndDate(rule, value, callback) {
if (this.shipForm.safeManningCertLongTerm !== 1 && !value) return callback(new Error('请选择最低安全配员证书有效期至'));
callback();
},
validateBusinessTransportCertEndDate(rule, value, callback) {
if (this.shipForm.businessTransportCertLongTerm !== 1 && !value) return callback(new Error('请选择营业运输证有效期至'));
callback();
},
openShip(row, readonly = false) {
this.readonly = readonly;
if (!row?.id) {
this.shipForm = emptyForm();
this.shipBox = true;
return;
}
getDetail(row.id).then(res => {
this.shipForm = { ...emptyForm(), ...(res.data.data || {}) };
this.shipBox = true;
});
},
resetShip() {
this.shipForm = emptyForm();
this.readonly = false;
this.submitLoading = false;
this.$refs.shipForm?.clearValidate();
},
setImage(prop, url) {
this.shipForm[prop] = url;
this.$refs.shipForm?.validateField(prop);
},
handleSubmit() {
this.$refs.shipForm.validate(valid => {
if (!valid) return;
this.submitLoading = true;
submit(this.shipForm)
.then(() => {
this.$message.success('操作成功');
this.shipBox = 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() {
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 (includeExpire && this.query.expireStatus) query.expireStatus = this.query.expireStatus;
return query;
},
handleExport() {
this.$confirm('是否导出船舶数据?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
NProgress.start();
exportBlob('/blade-transport/transport-ship/export-transport-ship', this.buildExportParams())
.then(res => {
downloadXls(res.data, `船舶管理${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
})
.finally(() => {
NProgress.done();
});
});
},
buildExportParams() {
return { ...this.buildQuery(true), ids: this.ids, [this.website.tokenHeader]: getToken() };
},
formatEndDate(date, longTerm) {
if (longTerm === 1) return '长期有效';
return date || '-';
},
getDateClass(row, dateProp, longTermProp) {
if (row[longTermProp] === 1 || !row[dateProp]) return '';
const today = this.$dayjs();
const endDate = this.$dayjs(row[dateProp]);
if (endDate.isBefore(today, 'day')) return 'date-danger';
if (endDate.diff(today, 'day') <= 30) return 'date-warning';
return '';
},
},
};
</script>
<style lang="scss" scoped>
.transport-ship-page {
.ship-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
}
.ship-toolbar__actions {
display: flex;
align-items: center;
gap: 8px;
}
.ship-stat {
flex-shrink: 0;
}
:deep(.el-table th .cell),
:deep(.el-table td .cell) {
white-space: nowrap;
}
}
.ship-form {
.section-title {
margin: 18px 0 14px;
padding-left: 10px;
border-left: 3px solid #e74b5f;
color: #303133;
font-weight: 600;
line-height: 18px;
}
.section-title:first-child {
margin-top: 0;
}
:deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper),
:deep(.el-select),
:deep(.el-input) {
width: 100%;
}
}
:deep(.ship-uploader .el-upload) {
width: 100%;
height: 170px;
border: 1px dashed #c0c4cc;
background: #fafafa;
overflow: hidden;
}
:deep(.ship-uploader--large .el-upload) {
height: 260px;
}
:deep(.ship-uploader__image) {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
:deep(.ship-uploader__empty) {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #606266;
}
.date-warning {
color: #f56c6c;
font-weight: 600;
}
.date-danger {
color: #d40000;
font-weight: 600;
}
@media (max-width: 1200px) {
.transport-ship-page {
.ship-toolbar {
align-items: flex-start;
flex-direction: column;
}
}
}
</style>

View File

@@ -0,0 +1,852 @@
<template>
<basic-container class="transport-vehicle-page">
<div class="vehicle-toolbar">
<div class="vehicle-toolbar__actions">
<el-button
type="primary"
icon="el-icon-plus"
plain
v-if="hasPermission('transport_vehicle_add')"
@click="openVehicle()"
>新增
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="hasPermission('transport_vehicle_export')"
@click="handleExport"
>导出
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="hasPermission('transport_vehicle_delete')"
@click="handleDelete"
>批量删除
</el-button>
</div>
<el-radio-group v-model="query.expireStatus" class="vehicle-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 #plateNo="{ row }">
<el-button type="primary" link @click="openVehicle(row, true)">
{{ row.plateNo }}
</el-button>
</template>
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'primary' : 'info'">
{{ row.status === 1 ? '启用' : '停用' }}
</el-tag>
</template>
<template #drivingLicenseEndDate="{ row }">
<span :class="getDateClass(row, 'drivingLicenseEndDate', 'drivingLicenseLongTerm')">
{{ formatEndDate(row.drivingLicenseEndDate, row.drivingLicenseLongTerm) }}
</span>
</template>
<template #roadTransportCertEndDate="{ row }">
<span :class="getDateClass(row, 'roadTransportCertEndDate', 'roadTransportCertLongTerm')">
{{ formatEndDate(row.roadTransportCertEndDate, row.roadTransportCertLongTerm) }}
</span>
</template>
<template #annualReviewEndDate="{ row }">
<span :class="getDateClass(row, 'annualReviewEndDate', 'annualReviewLongTerm')">
{{ formatEndDate(row.annualReviewEndDate, row.annualReviewLongTerm) }}
</span>
</template>
<template #menu="{ row }">
<el-button
type="primary"
text
icon="el-icon-view"
v-if="hasPermission('transport_vehicle_view')"
@click="openVehicle(row, true)"
>
查看
</el-button>
<el-button
type="primary"
text
icon="el-icon-edit"
v-if="hasPermission('transport_vehicle_edit')"
@click="openVehicle(row)"
>
修改
</el-button>
<el-button
type="primary"
text
:icon="row.status === 1 ? 'el-icon-close' : 'el-icon-check'"
v-if="hasPermission('transport_vehicle_status')"
@click="handleStatus(row)"
>
{{ row.status === 1 ? '停用' : '启用' }}
</el-button>
</template>
</avue-crud>
<el-dialog
:title="dialogTitle"
append-to-body
v-model="vehicleBox"
width="92%"
top="4vh"
class="vehicle-dialog"
@closed="resetVehicle"
>
<el-form
ref="vehicleForm"
:model="vehicleForm"
:rules="formRules"
label-position="top"
:disabled="readonly"
class="vehicle-form"
>
<div class="section-title">基础车辆信息</div>
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="所属组织" prop="organizationName">
<el-input v-model="vehicleForm.organizationName" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="车牌号" prop="plateNo">
<el-input
v-model="vehicleForm.plateNo"
maxlength="16"
show-word-limit
@input="vehicleForm.plateNo = String(vehicleForm.plateNo || '').toUpperCase()"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="车辆类型" prop="vehicleType">
<el-select v-model="vehicleForm.vehicleType" filterable allow-create default-first-option>
<el-option v-for="item in vehicleTypeOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="业务关系" prop="businessRelation">
<el-select v-model="vehicleForm.businessRelation">
<el-option v-for="item in businessRelationOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="能源类型" prop="energyType">
<el-select v-model="vehicleForm.energyType" filterable allow-create default-first-option>
<el-option v-for="item in energyTypeOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="核定载质量(KG)" prop="approvedLoadKg">
<el-input-number v-model="vehicleForm.approvedLoadKg" :min="0" :precision="0" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="准牵引总质量(KG)" prop="tractionMassKg">
<el-input-number v-model="vehicleForm.tractionMassKg" :min="0" :precision="0" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="状态" prop="status">
<el-select v-model="vehicleForm.status">
<el-option label="启用" :value="1" />
<el-option label="停用" :value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="外廓长度(mm)" prop="outerLength">
<el-input-number v-model="vehicleForm.outerLength" :min="0" :precision="0" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="外廓宽度(mm)" prop="outerWidth">
<el-input-number v-model="vehicleForm.outerWidth" :min="0" :precision="0" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="外廓高度(mm)" prop="outerHeight">
<el-input-number v-model="vehicleForm.outerHeight" :min="0" :precision="0" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="海关备案号" prop="customsRecordNo">
<el-input v-model="vehicleForm.customsRecordNo" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="强制报废日期" prop="compulsoryScrapDate">
<el-date-picker
v-model="vehicleForm.compulsoryScrapDate"
type="date"
value-format="YYYY-MM-DD"
:disabled="vehicleForm.compulsoryScrapLongTerm === 1"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="长期有效" prop="compulsoryScrapLongTerm">
<el-checkbox v-model="vehicleForm.compulsoryScrapLongTerm" :true-label="1" :false-label="0">
</el-checkbox>
</el-form-item>
</el-col>
</el-row>
<div class="section-title">行驶证信息</div>
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="行驶证档案编号" prop="drivingLicenseNo">
<el-input v-model="vehicleForm.drivingLicenseNo" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="行驶证有效期" prop="drivingLicenseEndDate">
<div class="date-range">
<el-date-picker v-model="vehicleForm.drivingLicenseStartDate" type="date" value-format="YYYY-MM-DD" placeholder="起" />
<el-date-picker
v-model="vehicleForm.drivingLicenseEndDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="止"
:disabled="vehicleForm.drivingLicenseLongTerm === 1"
/>
</div>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="长期有效" prop="drivingLicenseLongTerm">
<el-checkbox v-model="vehicleForm.drivingLicenseLongTerm" :true-label="1" :false-label="0">
</el-checkbox>
</el-form-item>
</el-col>
<el-col :span="12">
<image-upload-field
label="行驶证图片"
prop="drivingLicenseImage"
:value="vehicleForm.drivingLicenseImage"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => setImage('drivingLicenseImage', url)"
/>
</el-col>
</el-row>
<div class="section-title">道路运输证信息</div>
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="道路运输证号" prop="roadTransportCertNo">
<el-input v-model="vehicleForm.roadTransportCertNo" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="道路运输证有效期" prop="roadTransportCertEndDate">
<div class="date-range">
<el-date-picker v-model="vehicleForm.roadTransportCertStartDate" type="date" value-format="YYYY-MM-DD" placeholder="起" />
<el-date-picker
v-model="vehicleForm.roadTransportCertEndDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="止"
:disabled="vehicleForm.roadTransportCertLongTerm === 1"
/>
</div>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="长期有效" prop="roadTransportCertLongTerm">
<el-checkbox v-model="vehicleForm.roadTransportCertLongTerm" :true-label="1" :false-label="0">
</el-checkbox>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="道路运输年审有效期" prop="annualReviewEndDate">
<el-date-picker
v-model="vehicleForm.annualReviewEndDate"
type="date"
value-format="YYYY-MM-DD"
:disabled="vehicleForm.annualReviewLongTerm === 1"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="年审长期有效" prop="annualReviewLongTerm">
<el-checkbox v-model="vehicleForm.annualReviewLongTerm" :true-label="1" :false-label="0">
</el-checkbox>
</el-form-item>
</el-col>
<el-col :span="12">
<image-upload-field
label="道路运输证图片"
prop="roadTransportCertImage"
:value="vehicleForm.roadTransportCertImage"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => setImage('roadTransportCertImage', url)"
/>
</el-col>
</el-row>
<div class="section-title">机动车登记本信息</div>
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="机动车登记编号" prop="registrationNo">
<el-input v-model="vehicleForm.registrationNo" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="机动车登记日期" prop="registrationDate">
<el-date-picker v-model="vehicleForm.registrationDate" type="date" value-format="YYYY-MM-DD" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注" prop="remark">
<el-input v-model="vehicleForm.remark" type="textarea" maxlength="200" show-word-limit />
</el-form-item>
</el-col>
<el-col :span="12">
<image-upload-field
label="机动车登记本图片"
prop="registrationImage"
:value="vehicleForm.registrationImage"
:readonly="readonly"
:headers="uploadHeaders"
large
@success="url => setImage('registrationImage', url)"
/>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="vehicleBox = false">取消</el-button>
<el-button type="primary" v-if="!readonly" :loading="submitLoading" @click="handleSubmit">
确定
</el-button>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import { option } from '@/option/transportCapacity/transport-vehicle';
import {
getList,
getDetail,
submit,
remove,
changeStatus,
getExpiryStat,
} from '@/api/transportCapacity/transport-vehicle';
import { exportBlob } from '@/api/common';
import { getToken } from '@/utils/auth';
import { downloadXls } from '@/utils/util';
const emptyForm = () => ({
id: '',
organizationName: '',
plateNo: '',
vehicleType: '',
outerLength: undefined,
outerWidth: undefined,
outerHeight: undefined,
approvedLoadKg: undefined,
tractionMassKg: undefined,
businessRelation: '自有',
energyType: '',
compulsoryScrapDate: '',
compulsoryScrapLongTerm: 0,
customsRecordNo: '',
drivingLicenseNo: '',
drivingLicenseStartDate: '',
drivingLicenseEndDate: '',
drivingLicenseLongTerm: 0,
drivingLicenseImage: '',
roadTransportCertNo: '',
roadTransportCertStartDate: '',
roadTransportCertEndDate: '',
roadTransportCertLongTerm: 0,
roadTransportCertImage: '',
annualReviewEndDate: '',
annualReviewLongTerm: 0,
registrationNo: '',
registrationDate: '',
registrationImage: '',
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="vehicle-uploader"
:class="{ 'vehicle-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="vehicle-uploader__image" />
<div v-else class="vehicle-uploader__empty">上传证件图片</div>
</el-upload>
</el-form-item>
`,
};
export default {
components: {
ImageUploadField: 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,
},
vehicleBox: false,
readonly: false,
vehicleForm: emptyForm(),
uploadHeaders: {
'Blade-Auth': `bearer ${getToken()}`,
'Blade-Requested-With': 'BladeHttpRequest',
},
vehicleTypeOptions: ['厢式', '高栏', '平板', '仓栅', '牵引车', '冷藏车'],
businessRelationOptions: ['自有', '业务合作', '管理挂靠'],
energyTypeOptions: ['柴油', '汽油', '新能源', '天然气'],
formRules: {
organizationName: [{ required: true, message: '请输入所属组织', trigger: 'blur' }],
plateNo: [
{ required: true, message: '请输入车牌号', trigger: 'blur' },
{ pattern: /^[\u4e00-\u9fa5][A-Z][A-Z0-9挂学警港澳]{5,6}$/, message: '请输入标准车牌号', trigger: 'blur' },
],
vehicleType: [{ required: true, message: '请选择车辆类型', trigger: 'change' }],
approvedLoadKg: [{ required: true, message: '请输入核定载质量', trigger: 'blur' }],
businessRelation: [{ required: true, message: '请选择业务关系', trigger: 'change' }],
energyType: [{ required: true, message: '请选择能源类型', trigger: 'change' }],
drivingLicenseNo: [{ required: true, message: '请输入行驶证档案编号', trigger: 'blur' }],
drivingLicenseEndDate: [{ validator: this.validateDrivingLicenseEndDate, trigger: 'change' }],
roadTransportCertNo: [{ required: true, message: '请输入道路运输证号', trigger: 'blur' }],
roadTransportCertEndDate: [{ validator: this.validateRoadTransportCertEndDate, trigger: 'change' }],
annualReviewEndDate: [{ validator: this.validateAnnualReviewEndDate, trigger: 'change' }],
registrationNo: [{ required: true, message: '请输入机动车登记编号', trigger: 'blur' }],
remark: [{ max: 200, message: '备注最多200字', trigger: 'blur' }],
},
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
const authority = this.userInfo.authority || '';
return authority.includes('admin');
},
permissionList() {
return {
addBtn: this.hasPermission('transport_vehicle_add'),
viewBtn: this.hasPermission('transport_vehicle_view'),
delBtn: this.hasPermission('transport_vehicle_delete'),
editBtn: this.hasPermission('transport_vehicle_edit'),
};
},
dialogTitle() {
if (this.readonly) {
return '查看车辆';
}
return this.vehicleForm.id ? '修改车辆' : '新增车辆';
},
},
methods: {
hasPermission(code) {
return this.isAdmin || this.permission?.[code] === true;
},
validateDrivingLicenseEndDate(rule, value, callback) {
if (this.vehicleForm.drivingLicenseLongTerm !== 1 && !value) {
callback(new Error('请选择行驶证有效期止'));
return;
}
callback();
},
validateRoadTransportCertEndDate(rule, value, callback) {
if (this.vehicleForm.roadTransportCertLongTerm !== 1 && !value) {
callback(new Error('请选择道路运输证有效期止'));
return;
}
callback();
},
validateAnnualReviewEndDate(rule, value, callback) {
if (this.vehicleForm.annualReviewLongTerm !== 1 && !value) {
callback(new Error('请选择道路运输年审有效期'));
return;
}
callback();
},
openVehicle(row, readonly = false) {
this.readonly = readonly;
if (!row?.id) {
this.vehicleForm = emptyForm();
this.vehicleBox = true;
return;
}
getDetail(row.id).then(res => {
this.vehicleForm = {
...emptyForm(),
...(res.data.data || {}),
};
this.vehicleBox = true;
});
},
resetVehicle() {
this.vehicleForm = emptyForm();
this.readonly = false;
this.submitLoading = false;
this.$refs.vehicleForm?.clearValidate();
},
setImage(prop, url) {
this.vehicleForm[prop] = url;
this.$refs.vehicleForm?.validateField(prop);
},
handleSubmit() {
this.$refs.vehicleForm.validate(valid => {
if (!valid) {
return;
}
this.submitLoading = true;
submit(this.vehicleForm)
.then(() => {
this.$message.success('操作成功');
this.vehicleBox = false;
this.onLoad(this.page);
})
.finally(() => {
this.submitLoading = false;
});
});
},
handleStatus(row) {
const status = row.status === 1 ? 2 : 1;
const text = status === 1 ? '启用' : '停用';
this.$confirm(`是否${text}该车辆?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
changeStatus(row.id, status).then(() => {
this.$message.success('操作成功');
this.onLoad(this.page);
});
});
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('是否删除选中的车辆数据?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
remove(this.ids).then(() => {
this.$message.success('操作成功');
this.onLoad(this.page);
});
});
},
handleExpireChange() {
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 (includeExpire && this.query.expireStatus) {
query.expireStatus = this.query.expireStatus;
}
return query;
},
handleExport() {
this.$confirm('是否导出车辆数据?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob('/blade-transport/transport-vehicle/export-transport-vehicle', this.buildExportParams())
.then(res => {
downloadXls(res.data, `车辆管理${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
})
.finally(() => {
NProgress.done();
});
});
},
buildExportParams() {
return {
...this.buildQuery(true),
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
},
formatEndDate(date, longTerm) {
if (longTerm === 1) {
return '长期有效';
}
return date || '-';
},
getDateClass(row, dateProp, longTermProp) {
if (row[longTermProp] === 1 || !row[dateProp]) {
return '';
}
const today = this.$dayjs();
const endDate = this.$dayjs(row[dateProp]);
if (endDate.isBefore(today, 'day')) {
return 'date-danger';
}
if (endDate.diff(today, 'day') <= 30) {
return 'date-warning';
}
return '';
},
},
};
</script>
<style lang="scss" scoped>
.transport-vehicle-page {
.vehicle-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
}
.vehicle-toolbar__actions {
display: flex;
align-items: center;
gap: 8px;
}
.vehicle-stat {
flex-shrink: 0;
}
:deep(.el-table th .cell),
:deep(.el-table td .cell) {
white-space: nowrap;
}
}
.vehicle-form {
.section-title {
margin: 18px 0 14px;
padding-left: 10px;
border-left: 3px solid #e74b5f;
color: #303133;
font-weight: 600;
line-height: 18px;
}
.section-title:first-child {
margin-top: 0;
}
:deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper),
:deep(.el-select),
:deep(.el-input),
:deep(.el-input-number) {
width: 100%;
}
.date-range {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 8px;
}
}
:deep(.vehicle-uploader .el-upload) {
width: 100%;
height: 170px;
border: 1px dashed #c0c4cc;
background: #fafafa;
overflow: hidden;
}
:deep(.vehicle-uploader--large .el-upload) {
height: 260px;
}
:deep(.vehicle-uploader__image) {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
:deep(.vehicle-uploader__empty) {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #606266;
}
.date-warning {
color: #f56c6c;
font-weight: 600;
}
.date-danger {
color: #d40000;
font-weight: 600;
}
@media (max-width: 1200px) {
.transport-vehicle-page {
.vehicle-toolbar {
align-items: flex-start;
flex-direction: column;
}
}
}
</style>