Files
tms-erp-web/src/views/transportCapacity/ship.vue
2026-07-27 17:19:36 +08:00

766 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<basic-container class="transport-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="right"
: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>