1322 lines
42 KiB
Vue
1322 lines
42 KiB
Vue
<template>
|
||
<basic-container class="port-terminal-page">
|
||
<avue-crud
|
||
:option="option"
|
||
:table-loading="loading"
|
||
:data="data"
|
||
v-model:page="page"
|
||
v-model="form"
|
||
ref="crud"
|
||
:permission="permissionList"
|
||
:before-open="beforeOpen"
|
||
@row-save="rowSave"
|
||
@row-update="rowUpdate"
|
||
@row-del="rowDel"
|
||
@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-upload"
|
||
plain
|
||
v-if="hasPermission('port_terminal_import')"
|
||
@click="handleImport"
|
||
>批量导入
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
icon="el-icon-download"
|
||
plain
|
||
v-if="hasPermission('port_terminal_template')"
|
||
@click="handleTemplate"
|
||
>下载模板
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
icon="el-icon-download"
|
||
plain
|
||
v-if="hasPermission('port_terminal_export')"
|
||
@click="handleExport"
|
||
>批量导出
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
icon="el-icon-delete"
|
||
plain
|
||
v-if="hasPermission('port_terminal_delete')"
|
||
@click="handleDelete"
|
||
>批量删除
|
||
</el-button>
|
||
</template>
|
||
<template #parentName="{ row }">
|
||
<span>{{ row.category === '港口' ? '-' : row.parentName }}</span>
|
||
</template>
|
||
<template #parentCode="{ row }">
|
||
<span>{{ row.category === '港口' ? '-' : row.parentCode }}</span>
|
||
</template>
|
||
<template #name-header>
|
||
<span>港口/码头</span>
|
||
</template>
|
||
<template #status="{ row }">
|
||
<el-tag :type="row.status === 1 ? 'primary' : 'info'">
|
||
{{ row.status === 1 ? '启用' : '停用' }}
|
||
</el-tag>
|
||
</template>
|
||
<template #country-form>
|
||
<el-select
|
||
v-model="form.countryCode"
|
||
clearable
|
||
filterable
|
||
:disabled="isParentRegionLocked"
|
||
:placeholder="isParentRegionLocked ? '自动带出' : '请选择国家'"
|
||
style="width: 100%"
|
||
@change="handleCountryChange"
|
||
>
|
||
<el-option
|
||
v-for="item in countryOptions"
|
||
:key="item.id"
|
||
:label="item.title"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
<template #category-form>
|
||
<el-radio-group v-model="form.category" @change="handleCategoryChange">
|
||
<el-radio label="港口">港口</el-radio>
|
||
<el-radio label="码头">码头</el-radio>
|
||
</el-radio-group>
|
||
</template>
|
||
<template #code-form>
|
||
<el-input
|
||
v-if="form.category !== '码头'"
|
||
v-model="form.code"
|
||
clearable
|
||
maxlength="5"
|
||
placeholder="请输入5位大写字母"
|
||
@input="handleCodeChange"
|
||
@blur="handleCodeChange(form.code)"
|
||
/>
|
||
<div v-else>
|
||
<el-input
|
||
v-model="form.terminalCode"
|
||
clearable
|
||
maxlength="24"
|
||
placeholder="请输入码头标识"
|
||
@input="handleTerminalCodeChange"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template #menu="{ row, index }">
|
||
<el-link
|
||
type="primary"
|
||
v-if="hasPermission('port_terminal_edit')"
|
||
@click="$refs.crud.rowEdit(row, index)"
|
||
>
|
||
编辑
|
||
</el-link>
|
||
<el-link
|
||
type="primary"
|
||
v-if="hasPermission('port_terminal_status')"
|
||
@click="handleStatus(row)"
|
||
>
|
||
{{ row.status === 1 ? '停用' : '启用' }}
|
||
</el-link>
|
||
</template>
|
||
<template #cityCode-form>
|
||
<el-select
|
||
v-model="form.cityCode"
|
||
clearable
|
||
filterable
|
||
:loading="cityLoading"
|
||
:disabled="isCityDisabled"
|
||
:placeholder="isParentRegionLocked ? '自动带出' : '请先选择国家'"
|
||
style="width: 100%"
|
||
@change="handleCityChange"
|
||
>
|
||
<el-option
|
||
v-for="item in cityOptions"
|
||
:key="item.id"
|
||
:label="item.title"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
<template #districtCode-form>
|
||
<el-select
|
||
v-model="form.districtCode"
|
||
clearable
|
||
filterable
|
||
:loading="districtLoading"
|
||
:disabled="isDistrictDisabled"
|
||
:placeholder="
|
||
isParentRegionLocked ? '自动带出' : form.cityCode ? '请选择区县' : '请先选择城市'
|
||
"
|
||
style="width: 100%"
|
||
@change="handleDistrictChange"
|
||
>
|
||
<el-option
|
||
v-for="item in districtOptions"
|
||
:key="item.id"
|
||
:label="item.title"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
<template #detailAddress="{ row }">
|
||
<span class="port-terminal-page__detail-address">{{ row.detailAddress || '-' }}</span>
|
||
</template>
|
||
<template #longitude-form>
|
||
<el-input
|
||
v-model="form.longitude"
|
||
clearable
|
||
placeholder="请输入经度"
|
||
@input="handleCoordinateInput('longitude', $event)"
|
||
/>
|
||
</template>
|
||
<template #latitude-form>
|
||
<el-input
|
||
v-model="form.latitude"
|
||
clearable
|
||
placeholder="请输入纬度"
|
||
@input="handleCoordinateInput('latitude', $event)"
|
||
/>
|
||
</template>
|
||
</avue-crud>
|
||
<empty-pagination
|
||
:page="page"
|
||
@size-change="sizeChange"
|
||
@current-change="currentChange"
|
||
@load="onLoad(page, query)"
|
||
/>
|
||
|
||
<el-dialog title="港口码头主数据导入" append-to-body v-model="excelBox" width="555px">
|
||
<avue-form :option="excelOption" v-model="excelForm">
|
||
<template #excelTemplate>
|
||
<el-button type="primary" @click="handleTemplate">
|
||
点击下载<i class="el-icon-download el-icon--right"></i>
|
||
</el-button>
|
||
</template>
|
||
</avue-form>
|
||
</el-dialog>
|
||
</basic-container>
|
||
</template>
|
||
|
||
<script>
|
||
import {
|
||
getList,
|
||
getDetail,
|
||
submit,
|
||
remove,
|
||
changeStatus,
|
||
getPortSelect,
|
||
} from '@/api/base/port-terminal';
|
||
import { getLazyTree } from '@/api/base/region';
|
||
import { exportBlob } from '@/api/common';
|
||
import { mapGetters } from 'vuex';
|
||
import { downloadXls } from '@/utils/util';
|
||
import { openImportDialog } from '@/utils/import-excel';
|
||
import { formatUpdateUserName } from '@/utils/audit';
|
||
import { getToken } from '@/utils/auth';
|
||
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
||
import NProgress from 'nprogress';
|
||
import 'nprogress/nprogress.css';
|
||
|
||
const DEFAULT_COUNTRY_CODE = '+86';
|
||
const EXCLUDED_EXCEL_HEADERS = ['数据来源', '启停状态'];
|
||
const REQUIRED_EXPORT_HEADERS = ['港口码头名称', '港口/码头名称', '类型', '国家', '城市', '经度', '纬度'];
|
||
|
||
const getExcelCellText = value => {
|
||
if (value === undefined || value === null) return '';
|
||
if (typeof value === 'object') {
|
||
if (Array.isArray(value.richText)) {
|
||
return value.richText.map(item => item.text || '').join('');
|
||
}
|
||
if (value.text) return value.text;
|
||
if (value.result) return value.result;
|
||
}
|
||
return String(value);
|
||
};
|
||
|
||
const removeWorkbookColumnsByHeaders = workbook => {
|
||
workbook.eachSheet(worksheet => {
|
||
const maxHeaderRow = Math.min(worksheet.rowCount || 0, 10);
|
||
for (let rowIndex = 1; rowIndex <= maxHeaderRow; rowIndex += 1) {
|
||
const row = worksheet.getRow(rowIndex);
|
||
const indexes = [];
|
||
row.eachCell((cell, colNumber) => {
|
||
const text = getExcelCellText(cell.value).trim();
|
||
if (EXCLUDED_EXCEL_HEADERS.includes(text)) {
|
||
indexes.push(colNumber);
|
||
}
|
||
});
|
||
if (indexes.length) {
|
||
indexes
|
||
.sort((a, b) => b - a)
|
||
.forEach(colNumber => worksheet.spliceColumns(colNumber, 1));
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
const addWorkbookRequiredHeaderMarks = workbook => {
|
||
workbook.eachSheet(worksheet => {
|
||
const maxHeaderRow = Math.min(worksheet.rowCount || 0, 10);
|
||
for (let rowIndex = 1; rowIndex <= maxHeaderRow; rowIndex += 1) {
|
||
const row = worksheet.getRow(rowIndex);
|
||
let hasRequiredHeader = false;
|
||
row.eachCell(cell => {
|
||
const text = getExcelCellText(cell.value).trim();
|
||
const header = text.replace(/\*+$/g, '').trim();
|
||
if (REQUIRED_EXPORT_HEADERS.includes(header)) {
|
||
cell.value = `${header}*`;
|
||
hasRequiredHeader = true;
|
||
}
|
||
});
|
||
if (hasRequiredHeader) break;
|
||
}
|
||
});
|
||
};
|
||
|
||
const removeExcelColumnsByHeaders = async (blob, options = {}) => {
|
||
const { markRequiredHeaders = false } = options;
|
||
const ExcelJS = await import('exceljs');
|
||
const workbook = new ExcelJS.Workbook();
|
||
await workbook.xlsx.load(await blob.arrayBuffer());
|
||
removeWorkbookColumnsByHeaders(workbook);
|
||
if (markRequiredHeaders) {
|
||
addWorkbookRequiredHeaderMarks(workbook);
|
||
}
|
||
const buffer = await workbook.xlsx.writeBuffer();
|
||
return new Blob([buffer], { type: 'application/vnd.ms-excel' });
|
||
};
|
||
|
||
export default {
|
||
data() {
|
||
const validateCode = (rule, value, callback) => {
|
||
const code = String(value || '').toUpperCase();
|
||
if (this.form.category === '港口') {
|
||
if (!/^[A-Z]{5}$/.test(code)) {
|
||
callback(new Error('港口编码为5位大写字母,如:CNSHG'));
|
||
} else {
|
||
callback();
|
||
}
|
||
return;
|
||
}
|
||
if (this.form.category === '码头') {
|
||
if (!/^[A-Z]{5}-[A-Z0-9]+$/.test(code)) {
|
||
callback(new Error('码头编码格式为“港口编码-码头标识”,如:CNSHG-CT1'));
|
||
} else if (this.form.parentCode && !code.startsWith(`${this.form.parentCode}-`)) {
|
||
callback(new Error(`码头编码须以所选上级港口编码 ${this.form.parentCode}- 开头`));
|
||
} else {
|
||
callback();
|
||
}
|
||
return;
|
||
}
|
||
callback();
|
||
};
|
||
const validateLongitude = (rule, value, callback) => {
|
||
const message = getCoordinateValidationMessage(value, 'longitude');
|
||
if (message) {
|
||
callback(new Error(message));
|
||
} else {
|
||
callback();
|
||
}
|
||
};
|
||
const validateLatitude = (rule, value, callback) => {
|
||
const message = getCoordinateValidationMessage(value, 'latitude');
|
||
if (message) {
|
||
callback(new Error(message));
|
||
} else {
|
||
callback();
|
||
}
|
||
};
|
||
return {
|
||
form: {},
|
||
query: {},
|
||
loading: true,
|
||
data: [],
|
||
excelBox: false,
|
||
excelForm: {},
|
||
portOptions: [],
|
||
countryOptions: [],
|
||
cityOptions: [],
|
||
cityLoading: false,
|
||
cityOptionCache: {},
|
||
districtOptions: [],
|
||
districtLoading: false,
|
||
districtOptionCache: {},
|
||
page: {
|
||
pageSize: 10,
|
||
pageSizes: [10, 20, 50, 100],
|
||
currentPage: 1,
|
||
total: 0,
|
||
},
|
||
selectionList: [],
|
||
option: {
|
||
height: 'auto',
|
||
calcHeight: 32,
|
||
dialogWidth: 980,
|
||
labelPosition: 'right',
|
||
labelWidth: 'auto',
|
||
tip: false,
|
||
searchShow: true,
|
||
searchLabelWidth: 160,
|
||
searchIcon: true,
|
||
searchIndex: 4,
|
||
searchMenuSpan: 24,
|
||
searchMenuPosition: 'right',
|
||
border: true,
|
||
index: true,
|
||
indexLabel: '序号',
|
||
indexWidth: 70,
|
||
viewBtn: false,
|
||
delBtn: false,
|
||
editBtn: false,
|
||
selection: true,
|
||
dialogClickModal: false,
|
||
menuWidth: 160,
|
||
column: [
|
||
{
|
||
label: '类型',
|
||
prop: 'category',
|
||
type: 'select',
|
||
search: true,
|
||
formslot: true,
|
||
span: 24,
|
||
dicData: [
|
||
{ label: '全部', value: '' },
|
||
{ label: '港口', value: '港口' },
|
||
{ label: '码头', value: '码头' },
|
||
],
|
||
rules: [{ required: true, message: '请选择类型', trigger: 'change' }],
|
||
change: ({ value }) => this.handleCategoryChange(value),
|
||
},
|
||
{
|
||
label: '上级港口',
|
||
prop: 'parentId',
|
||
type: 'select',
|
||
props: {
|
||
label: 'name',
|
||
value: 'id',
|
||
},
|
||
dicData: [],
|
||
hide: true,
|
||
span: 12,
|
||
placeholder: '请选择',
|
||
rules: [{ required: false, message: '请选择上级港口', trigger: 'change' }],
|
||
change: ({ value }) => this.handleParentChange(value),
|
||
},
|
||
{
|
||
label: '上级港口',
|
||
prop: 'parentName',
|
||
minWidth: 120,
|
||
slot: true,
|
||
addDisplay: false,
|
||
editDisplay: false,
|
||
},
|
||
{
|
||
label: '上级港口编码',
|
||
prop: 'parentCode',
|
||
minWidth: 120,
|
||
slot: true,
|
||
span: 12,
|
||
addDisplay: false,
|
||
editDisplay: false,
|
||
placeholder: '自动带出',
|
||
rules: [{ required: false, message: '请选择上级港口', trigger: 'change' }],
|
||
},
|
||
{
|
||
label: '港口/码头名称',
|
||
prop: 'name',
|
||
minWidth: 140,
|
||
search: true,
|
||
span: 12,
|
||
placeholder: '请输入',
|
||
rules: [{ required: true, message: '请输入港口/码头名称', trigger: 'blur' }],
|
||
},
|
||
{
|
||
label: '编码',
|
||
prop: 'code',
|
||
search: true,
|
||
formslot: true,
|
||
width: 140,
|
||
span: 12,
|
||
placeholder: '请输入编码',
|
||
rules: [
|
||
{ required: true, message: '请输入编码', trigger: 'blur' },
|
||
{ validator: validateCode, trigger: 'blur' },
|
||
],
|
||
change: ({ value }) => this.handleCodeChange(value),
|
||
blur: ({ value }) => this.handleCodeChange(value),
|
||
},
|
||
{
|
||
label: '国家',
|
||
prop: 'country',
|
||
type: 'select',
|
||
search: true,
|
||
formslot: true,
|
||
filterable: true,
|
||
dicData: [],
|
||
span: 12,
|
||
minWidth: 140,
|
||
overHidden: false,
|
||
placeholder: '自动带出',
|
||
searchPlaceholder: '请选择 国家',
|
||
change: ({ value }) => this.handleCountryNameChange(value),
|
||
rules: [{ required: true, message: '请选择国家', trigger: 'change' }],
|
||
},
|
||
{
|
||
label: '城市',
|
||
prop: 'cityCode',
|
||
formslot: true,
|
||
hide: true,
|
||
span: 12,
|
||
placeholder: '自动带出',
|
||
rules: [{ required: true, message: '请选择所属城市', trigger: 'change' }],
|
||
},
|
||
{
|
||
label: '城市',
|
||
prop: 'city',
|
||
type: 'select',
|
||
search: true,
|
||
filterable: true,
|
||
dicData: [],
|
||
addDisplay: false,
|
||
editDisplay: false,
|
||
minWidth: 100,
|
||
change: ({ value }) => this.handleCityNameChange(value),
|
||
},
|
||
{
|
||
label: '区县',
|
||
prop: 'districtCode',
|
||
type: 'select',
|
||
formslot: true,
|
||
hide: true,
|
||
props: {
|
||
label: 'title',
|
||
value: 'id',
|
||
},
|
||
dicData: [],
|
||
span: 12,
|
||
placeholder: '选择城市后再选择区县',
|
||
rules: [{ required: true, message: '请选择区县', trigger: 'change' }],
|
||
},
|
||
{
|
||
label: '区县',
|
||
prop: 'districtName',
|
||
type: 'select',
|
||
search: true,
|
||
filterable: true,
|
||
dicData: [],
|
||
minWidth: 100,
|
||
addDisplay: false,
|
||
editDisplay: false,
|
||
},
|
||
{
|
||
label: '行政区划编号',
|
||
prop: 'regionCode',
|
||
minWidth: 140,
|
||
span: 12,
|
||
placeholder: '选择区县后自动填写',
|
||
addDisplay: false,
|
||
editDisplay: false,
|
||
rules: [{ max: 32, message: '行政区划编码不能超过32字', trigger: 'blur' }],
|
||
},
|
||
{
|
||
label: '详细地址',
|
||
prop: 'detailAddress',
|
||
type: 'textarea',
|
||
slot: true,
|
||
minRows: 3,
|
||
span: 24,
|
||
minWidth: 220,
|
||
overHidden: false,
|
||
placeholder: '请输入',
|
||
maxlength: 255,
|
||
showWordLimit: true,
|
||
rules: [{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' }],
|
||
},
|
||
{
|
||
label: '经度',
|
||
prop: 'longitude',
|
||
formslot: true,
|
||
minWidth: 130,
|
||
span: 12,
|
||
placeholder: '请输入',
|
||
overHidden: false,
|
||
rules: [
|
||
{ required: true, message: '请输入经度', trigger: 'blur' },
|
||
{ validator: validateLongitude, trigger: 'blur' },
|
||
],
|
||
},
|
||
{
|
||
label: '纬度',
|
||
prop: 'latitude',
|
||
formslot: true,
|
||
minWidth: 130,
|
||
span: 12,
|
||
placeholder: '请输入',
|
||
overHidden: false,
|
||
rules: [
|
||
{ required: true, message: '请输入纬度', trigger: 'blur' },
|
||
{ validator: validateLatitude, trigger: 'blur' },
|
||
],
|
||
},
|
||
{
|
||
label: '数据来源',
|
||
prop: 'dataSource',
|
||
type: 'select',
|
||
search: true,
|
||
minWidth: 120,
|
||
overHidden: false,
|
||
addDisplay: false,
|
||
editDisplay: false,
|
||
dicData: [
|
||
{ label: '全部', value: '' },
|
||
{ label: '初始化导入', value: '初始化导入' },
|
||
{ label: '批量导入', value: '批量导入' },
|
||
{ label: '手工导入', value: '手工导入' },
|
||
],
|
||
},
|
||
{
|
||
label: '启停状态',
|
||
prop: 'status',
|
||
type: 'select',
|
||
search: true,
|
||
slot: true,
|
||
dataType: 'number',
|
||
minWidth: 110,
|
||
overHidden: false,
|
||
addDisplay: false,
|
||
editDisplay: false,
|
||
dicData: [
|
||
{ label: '全部', value: '' },
|
||
{ label: '启用', value: 1 },
|
||
{ label: '停用', value: 2 },
|
||
],
|
||
},
|
||
{
|
||
label: '备注',
|
||
prop: 'remark',
|
||
type: 'textarea',
|
||
minRows: 4,
|
||
span: 24,
|
||
hide: true,
|
||
maxlength: 200,
|
||
showWordLimit: true,
|
||
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||
},
|
||
{
|
||
label: '更新人',
|
||
prop: 'updateUserName',
|
||
formatter: formatUpdateUserName,
|
||
addDisplay: false,
|
||
editDisplay: false,
|
||
minWidth: 120,
|
||
},
|
||
{
|
||
label: '更新时间',
|
||
prop: 'updateTime',
|
||
type: 'datetime',
|
||
format: 'YYYY-MM-DD HH:mm:ss',
|
||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||
addDisplay: false,
|
||
editDisplay: false,
|
||
minWidth: 160,
|
||
},
|
||
{
|
||
label: '创建时间',
|
||
prop: 'createTime',
|
||
type: 'datetime',
|
||
format: 'YYYY-MM-DD HH:mm:ss',
|
||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||
addDisplay: false,
|
||
editDisplay: false,
|
||
display: false,
|
||
minWidth: 160,
|
||
},
|
||
],
|
||
},
|
||
excelOption: {
|
||
labelPosition: 'right',
|
||
submitBtn: false,
|
||
emptyBtn: false,
|
||
column: [
|
||
{
|
||
label: '模板上传',
|
||
prop: 'excelFile',
|
||
type: 'upload',
|
||
drag: true,
|
||
loadText: '模板上传中,请稍等',
|
||
span: 24,
|
||
propsHttp: {
|
||
res: 'data',
|
||
},
|
||
tip: '请上传 .xls,.xlsx 标准格式文件',
|
||
accept: '.xls,.xlsx',
|
||
action: '/blade-system/port-terminal/import-port-terminal',
|
||
},
|
||
{
|
||
label: '模板下载',
|
||
prop: 'excelTemplate',
|
||
formslot: true,
|
||
span: 24,
|
||
},
|
||
],
|
||
},
|
||
};
|
||
},
|
||
computed: {
|
||
...mapGetters(['permission', 'userInfo']),
|
||
permissionList() {
|
||
return {
|
||
addBtn: this.hasPermission('port_terminal_add'),
|
||
};
|
||
},
|
||
isAdmin() {
|
||
const authority = this.userInfo && this.userInfo.authority;
|
||
return Array.isArray(authority)
|
||
? authority.includes('admin')
|
||
: String(authority || '').includes('admin');
|
||
},
|
||
isParentRegionLocked() {
|
||
return this.form.category === '码头';
|
||
},
|
||
isCityDisabled() {
|
||
return this.isParentRegionLocked || !this.form.countryCode;
|
||
},
|
||
isDistrictDisabled() {
|
||
return this.isParentRegionLocked || !this.form.cityCode;
|
||
},
|
||
ids() {
|
||
let ids = [];
|
||
this.selectionList.forEach(ele => {
|
||
ids.push(ele.id);
|
||
});
|
||
return ids.join(',');
|
||
},
|
||
},
|
||
created() {
|
||
this.loadCountryOptions();
|
||
this.loadPortOptions();
|
||
},
|
||
methods: {
|
||
hasPermission(code) {
|
||
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
|
||
},
|
||
handleCodeChange(value) {
|
||
const code = String(value || '').toUpperCase();
|
||
if (this.form.category === '港口') {
|
||
this.form.code = code.replace(/[^A-Z]/g, '').slice(0, 5);
|
||
return;
|
||
}
|
||
if (this.form.category === '码头') {
|
||
const terminalCode = this.getTerminalCodePart(code).replace(/[^A-Z0-9]/g, '');
|
||
if (this.form.parentCode) {
|
||
this.handleTerminalCodeChange(terminalCode);
|
||
} else {
|
||
this.form.terminalCode = terminalCode;
|
||
this.form.code = code.replace(/[^A-Z0-9-]/g, '');
|
||
}
|
||
}
|
||
},
|
||
handleTerminalCodeChange(value) {
|
||
const terminalCode = String(value || '')
|
||
.toUpperCase()
|
||
.replace(/[^A-Z0-9]/g, '');
|
||
this.form.terminalCode = terminalCode;
|
||
this.form.code =
|
||
this.form.parentCode && terminalCode ? `${this.form.parentCode}-${terminalCode}` : '';
|
||
},
|
||
getTerminalCodePart(code) {
|
||
const value = String(code || '').toUpperCase();
|
||
if (this.form.parentCode && value.startsWith(`${this.form.parentCode}-`)) {
|
||
return value.slice(this.form.parentCode.length + 1);
|
||
}
|
||
return value.split('-').slice(1).join('-');
|
||
},
|
||
syncTerminalCode() {
|
||
if (this.form.category !== '码头') return;
|
||
this.form.terminalCode = this.getTerminalCodePart(this.form.code);
|
||
this.handleTerminalCodeChange(this.form.terminalCode);
|
||
},
|
||
setColumnDisplay(prop, display) {
|
||
const column = this.findColumn(this.option.column, prop);
|
||
if (!column) return;
|
||
column.display = display;
|
||
column.addDisplay = display;
|
||
column.editDisplay = display;
|
||
column.viewDisplay = display;
|
||
},
|
||
loadCountryOptions() {
|
||
return getLazyTree('0').then(res => {
|
||
this.countryOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||
const countryColumn = this.findColumn(this.option.column, 'country');
|
||
countryColumn.dicData = this.countryOptions.map(country => ({
|
||
label: country.title,
|
||
value: country.title,
|
||
id: country.id,
|
||
}));
|
||
});
|
||
},
|
||
loadPortOptions() {
|
||
getPortSelect().then(res => {
|
||
this.portOptions = res.data.data;
|
||
this.findColumn(this.option.column, 'parentId').dicData = this.portOptions;
|
||
});
|
||
},
|
||
clearParentPortFields() {
|
||
this.form.parentId = undefined;
|
||
this.form.parentCode = '';
|
||
this.form.parentName = '';
|
||
this.form.countryCode = '';
|
||
this.form.country = '';
|
||
this.clearCityValue();
|
||
},
|
||
handleCategoryChange(value, reset = true) {
|
||
const parentColumn = this.findColumn(this.option.column, 'parentId');
|
||
parentColumn.rules[0].required = value === '码头';
|
||
this.setColumnDisplay('parentId', value === '码头');
|
||
if (value === '港口') {
|
||
this.form.parentId = undefined;
|
||
this.form.parentCode = '';
|
||
this.form.parentName = '';
|
||
this.form.terminalCode = '';
|
||
this.form.code = reset ? '' : String(this.form.code || '').split('-')[0];
|
||
} else if (reset) {
|
||
this.clearParentPortFields();
|
||
}
|
||
this.handleCodeChange(this.form.code);
|
||
},
|
||
handleCountryChange(value) {
|
||
const country = this.countryOptions.find(item => item.id === value);
|
||
this.form.country = country ? country.title : '';
|
||
this.clearCityValue();
|
||
this.loadCityOptions(value);
|
||
},
|
||
handleCountryNameChange(value) {
|
||
const country = this.countryOptions.find(item => item.title === value);
|
||
this.setDistrictOptions([]);
|
||
this.loadCityOptions(country ? country.id : '');
|
||
},
|
||
syncCountryName(countryCode) {
|
||
const country = this.countryOptions.find(item => item.id === countryCode);
|
||
if (country) {
|
||
this.form.country = country.title;
|
||
}
|
||
},
|
||
normalizeRegionOption(region) {
|
||
return {
|
||
...region,
|
||
id: region.id === undefined || region.id === null ? region.id : String(region.id),
|
||
parentCode:
|
||
region.parentCode === undefined || region.parentCode === null
|
||
? region.parentCode
|
||
: String(region.parentCode),
|
||
};
|
||
},
|
||
setCityOptions(cityOptions) {
|
||
this.cityOptions = cityOptions;
|
||
const cityCodeColumn = this.findColumn(this.option.column, 'cityCode');
|
||
if (cityCodeColumn) {
|
||
cityCodeColumn.dicData = cityOptions;
|
||
}
|
||
const cityColumn = this.findColumn(this.option.column, 'city');
|
||
if (cityColumn) {
|
||
cityColumn.dicData = cityOptions.map(city => ({
|
||
label: city.title,
|
||
value: city.title,
|
||
id: city.id,
|
||
}));
|
||
}
|
||
},
|
||
clearCityValue() {
|
||
this.form.cityCode = undefined;
|
||
this.form.city = '';
|
||
this.clearDistrictValue();
|
||
},
|
||
clearDistrictValue() {
|
||
this.form.districtCode = undefined;
|
||
this.form.districtName = '';
|
||
this.form.regionCode = '';
|
||
},
|
||
loadCityOptions(countryCode) {
|
||
if (!countryCode) {
|
||
this.setCityOptions([]);
|
||
return Promise.resolve([]);
|
||
}
|
||
if (this.cityOptionCache[countryCode]) {
|
||
this.setCityOptions(this.cityOptionCache[countryCode]);
|
||
return Promise.resolve(this.cityOptionCache[countryCode]);
|
||
}
|
||
this.cityLoading = true;
|
||
return getLazyTree(countryCode)
|
||
.then(res => {
|
||
const firstLevelList = (res.data.data || []).map(this.normalizeRegionOption);
|
||
return Promise.all(
|
||
firstLevelList.map(parent => {
|
||
return getLazyTree(parent.id).then(childRes => {
|
||
const childList = (childRes.data.data || []).map(this.normalizeRegionOption);
|
||
return childList.map(child => ({
|
||
...child,
|
||
parentCode: child.parentCode || parent.id,
|
||
}));
|
||
});
|
||
})
|
||
).then(cityGroups => {
|
||
const cityList = cityGroups.flat();
|
||
const options = cityList.length > 0 ? cityList : firstLevelList;
|
||
this.cityOptionCache[countryCode] = options;
|
||
this.setCityOptions(options);
|
||
return options;
|
||
});
|
||
})
|
||
.finally(() => {
|
||
this.cityLoading = false;
|
||
});
|
||
},
|
||
resolveCityCode(cityName, regionCode) {
|
||
const city = this.cityOptions.find(item => item.title === cityName || item.id === regionCode);
|
||
return city ? city.id : '';
|
||
},
|
||
setDistrictOptions(districtOptions) {
|
||
this.districtOptions = districtOptions;
|
||
const districtCodeColumn = this.findColumn(this.option.column, 'districtCode');
|
||
if (districtCodeColumn) {
|
||
districtCodeColumn.dicData = districtOptions;
|
||
}
|
||
const districtNameColumn = this.findColumn(this.option.column, 'districtName');
|
||
if (districtNameColumn) {
|
||
districtNameColumn.dicData = districtOptions.map(district => ({
|
||
label: district.title,
|
||
value: district.title,
|
||
id: district.id,
|
||
}));
|
||
}
|
||
},
|
||
loadDistrictOptions(cityCode) {
|
||
if (!cityCode) {
|
||
this.setDistrictOptions([]);
|
||
return Promise.resolve([]);
|
||
}
|
||
if (this.districtOptionCache[cityCode]) {
|
||
this.setDistrictOptions(this.districtOptionCache[cityCode]);
|
||
return Promise.resolve(this.districtOptionCache[cityCode]);
|
||
}
|
||
this.districtLoading = true;
|
||
return getLazyTree(cityCode)
|
||
.then(res => {
|
||
const options = (res.data.data || []).map(this.normalizeRegionOption);
|
||
this.districtOptionCache[cityCode] = options;
|
||
this.setDistrictOptions(options);
|
||
return options;
|
||
})
|
||
.finally(() => {
|
||
this.districtLoading = false;
|
||
});
|
||
},
|
||
resolveDistrictCode(districtName, regionCode) {
|
||
const district = this.districtOptions.find(
|
||
item => item.id === regionCode || item.title === districtName
|
||
);
|
||
return district ? district.id : regionCode || '';
|
||
},
|
||
handleParentChange(value) {
|
||
const parent = this.portOptions.find(item => item.id === value);
|
||
if (!parent) {
|
||
this.clearParentPortFields();
|
||
this.syncTerminalCode();
|
||
return;
|
||
}
|
||
const terminalCode =
|
||
this.form.terminalCode ||
|
||
String(this.form.code || '')
|
||
.split('-')
|
||
.slice(1)
|
||
.join('-');
|
||
this.form.parentCode = parent.code;
|
||
this.form.parentName = parent.name;
|
||
this.form.terminalCode = terminalCode;
|
||
this.handleTerminalCodeChange(terminalCode);
|
||
this.form.country = parent.country;
|
||
this.form.city = parent.city;
|
||
this.form.districtCode = parent.districtCode;
|
||
this.form.districtName = parent.districtName;
|
||
this.form.regionCode = parent.regionCode;
|
||
this.resolveCountryCode(parent.country)
|
||
.then(countryCode => {
|
||
this.form.countryCode = countryCode;
|
||
this.syncCountryName(countryCode);
|
||
return this.loadCityOptions(countryCode);
|
||
})
|
||
.then(() => {
|
||
this.form.cityCode = this.resolveCityCode(parent.city, parent.regionCode);
|
||
return this.loadDistrictOptions(this.form.cityCode);
|
||
})
|
||
.then(() => {
|
||
this.form.districtCode = this.resolveDistrictCode(parent.districtName, parent.regionCode);
|
||
this.handleDistrictChange(this.form.districtCode);
|
||
});
|
||
},
|
||
handleCityChange(value) {
|
||
const city = this.cityOptions.find(item => item.id === value);
|
||
this.form.city = city ? city.title : '';
|
||
this.clearDistrictValue();
|
||
this.loadDistrictOptions(value);
|
||
},
|
||
handleCityNameChange(value) {
|
||
const city = this.cityOptions.find(item => item.title === value);
|
||
this.setDistrictOptions([]);
|
||
this.loadDistrictOptions(city ? city.id : '');
|
||
},
|
||
handleDistrictChange(value) {
|
||
const district = this.districtOptions.find(item => item.id === value);
|
||
this.form.districtName = district ? district.title : '';
|
||
this.form.regionCode = value || '';
|
||
},
|
||
handleCoordinateInput(prop, value) {
|
||
this.form[prop] = normalizeCoordinateInput(value);
|
||
},
|
||
resolveCountryCode(country) {
|
||
if (!country) {
|
||
return Promise.resolve('');
|
||
}
|
||
const countryOption = this.countryOptions.find(
|
||
item => item.title === country || item.id === country
|
||
);
|
||
if (countryOption) {
|
||
return Promise.resolve(countryOption.id);
|
||
}
|
||
return this.loadCountryOptions().then(() => {
|
||
const option = this.countryOptions.find(
|
||
item => item.title === country || item.id === country
|
||
);
|
||
return option ? option.id : DEFAULT_COUNTRY_CODE;
|
||
});
|
||
},
|
||
normalizeRow(row) {
|
||
const submitRow = { ...row };
|
||
submitRow.code = String(submitRow.code || '').toUpperCase();
|
||
submitRow.regionCode = String(submitRow.regionCode || '').trim();
|
||
submitRow.districtCode = String(submitRow.districtCode || '').trim();
|
||
submitRow.districtName = String(submitRow.districtName || '').trim();
|
||
submitRow.detailAddress = String(submitRow.detailAddress || '').trim();
|
||
submitRow.longitude = normalizeCoordinateInput(submitRow.longitude);
|
||
submitRow.latitude = normalizeCoordinateInput(submitRow.latitude);
|
||
if (submitRow.category === '港口') {
|
||
submitRow.parentId = undefined;
|
||
submitRow.parentCode = undefined;
|
||
submitRow.parentName = undefined;
|
||
}
|
||
if (!submitRow.dataSource) submitRow.dataSource = '手工导入';
|
||
if (!submitRow.status) submitRow.status = 1;
|
||
delete submitRow.terminalCode;
|
||
delete submitRow.cityCode;
|
||
delete submitRow.countryCode;
|
||
return submitRow;
|
||
},
|
||
validateRequiredFields(row) {
|
||
const isBlank = value => value === undefined || value === null || String(value).trim() === '';
|
||
if (isBlank(row.code)) {
|
||
this.$message.warning('请输入编码');
|
||
return false;
|
||
}
|
||
if (isBlank(row.name)) {
|
||
this.$message.warning('请输入港口/码头名称');
|
||
return false;
|
||
}
|
||
if (isBlank(row.category)) {
|
||
this.$message.warning('请选择类型');
|
||
return false;
|
||
}
|
||
if (row.category === '码头' && isBlank(row.parentId)) {
|
||
this.$message.warning('请选择上级港口');
|
||
return false;
|
||
}
|
||
if (isBlank(row.regionCode)) {
|
||
this.$message.warning('请选择区县');
|
||
return false;
|
||
}
|
||
if (isBlank(row.longitude)) {
|
||
this.$message.warning('请输入经度');
|
||
return false;
|
||
}
|
||
const longitudeMessage = getCoordinateValidationMessage(row.longitude, 'longitude');
|
||
if (longitudeMessage) {
|
||
this.$message.warning(longitudeMessage);
|
||
return false;
|
||
}
|
||
if (isBlank(row.latitude)) {
|
||
this.$message.warning('请输入纬度');
|
||
return false;
|
||
}
|
||
const latitudeMessage = getCoordinateValidationMessage(row.latitude, 'latitude');
|
||
if (latitudeMessage) {
|
||
this.$message.warning(latitudeMessage);
|
||
return false;
|
||
}
|
||
return true;
|
||
},
|
||
stopSubmitLoading(loading) {
|
||
if (typeof loading === 'function') {
|
||
loading();
|
||
}
|
||
},
|
||
rowSave(row, done, loading) {
|
||
const submitRow = this.normalizeRow(row);
|
||
if (!this.validateRequiredFields(submitRow)) {
|
||
this.stopSubmitLoading(loading);
|
||
return;
|
||
}
|
||
submit(submitRow).then(
|
||
() => {
|
||
this.onLoad(this.page);
|
||
this.loadPortOptions();
|
||
this.$message({ type: 'success', message: '操作成功!' });
|
||
done();
|
||
},
|
||
error => {
|
||
window.console.log(error);
|
||
loading();
|
||
}
|
||
);
|
||
},
|
||
rowUpdate(row, index, done, loading) {
|
||
const submitRow = this.normalizeRow(row);
|
||
if (!this.validateRequiredFields(submitRow)) {
|
||
this.stopSubmitLoading(loading);
|
||
return;
|
||
}
|
||
submit(submitRow).then(
|
||
() => {
|
||
this.onLoad(this.page);
|
||
this.loadPortOptions();
|
||
this.$message({ type: 'success', message: '操作成功!' });
|
||
done();
|
||
},
|
||
error => {
|
||
window.console.log(error);
|
||
loading();
|
||
}
|
||
);
|
||
},
|
||
rowDel(row) {
|
||
this.$confirm('确定将选择数据删除?', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
})
|
||
.then(() => remove(row.id))
|
||
.then(() => {
|
||
this.onLoad(this.page);
|
||
this.loadPortOptions();
|
||
this.$message({ type: 'success', message: '操作成功!' });
|
||
});
|
||
},
|
||
handleDelete() {
|
||
if (this.selectionList.length === 0) {
|
||
this.$message.warning('请选择至少一条数据');
|
||
return;
|
||
}
|
||
this.$confirm('确定将选择数据删除?', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
})
|
||
.then(() => remove(this.ids))
|
||
.then(() => {
|
||
this.onLoad(this.page);
|
||
this.loadPortOptions();
|
||
this.$message({ type: 'success', message: '操作成功!' });
|
||
this.$refs.crud.toggleSelection();
|
||
});
|
||
},
|
||
handleStatus(row) {
|
||
const nextStatus = row.status === 1 ? 2 : 1;
|
||
const actionName = nextStatus === 1 ? '启用' : '停用';
|
||
this.$confirm(`是否确认${actionName}该条数据?`, {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
})
|
||
.then(() => changeStatus(row.id, nextStatus))
|
||
.then(() => {
|
||
this.onLoad(this.page);
|
||
this.loadPortOptions();
|
||
this.$message({ type: 'success', message: '操作成功!' });
|
||
});
|
||
},
|
||
beforeOpen(done, type) {
|
||
if (type === 'add') {
|
||
this.form.category = this.form.category || '港口';
|
||
this.form.dataSource = this.form.dataSource || '手工导入';
|
||
this.form.status = this.form.status || 1;
|
||
}
|
||
this.handleCategoryChange(this.form.category || '港口');
|
||
if (['edit', 'view'].includes(type)) {
|
||
getDetail(this.form.id).then(res => {
|
||
this.form = res.data.data;
|
||
this.form.terminalCode = this.getTerminalCodePart(this.form.code);
|
||
this.resolveCountryCode(this.form.country)
|
||
.then(countryCode => {
|
||
this.form.countryCode = countryCode;
|
||
this.syncCountryName(countryCode);
|
||
return this.loadCityOptions(countryCode);
|
||
})
|
||
.then(() => {
|
||
this.form.cityCode = this.resolveCityCode(this.form.city, this.form.regionCode);
|
||
return this.loadDistrictOptions(this.form.cityCode);
|
||
})
|
||
.then(() => {
|
||
this.form.districtCode = this.resolveDistrictCode(
|
||
this.form.districtName,
|
||
this.form.regionCode
|
||
);
|
||
if (!this.form.regionCode) {
|
||
this.handleDistrictChange(this.form.districtCode);
|
||
}
|
||
this.handleCategoryChange(this.form.category, false);
|
||
this.syncTerminalCode();
|
||
done();
|
||
});
|
||
});
|
||
return;
|
||
}
|
||
this.resolveCountryCode(this.form.country)
|
||
.then(countryCode => {
|
||
this.form.countryCode = countryCode;
|
||
this.syncCountryName(countryCode);
|
||
return this.loadCityOptions(countryCode);
|
||
})
|
||
.then(() => {
|
||
this.form.cityCode = this.resolveCityCode(this.form.city, this.form.regionCode);
|
||
return this.loadDistrictOptions(this.form.cityCode);
|
||
})
|
||
.then(() => {
|
||
this.form.districtCode = this.resolveDistrictCode(
|
||
this.form.districtName,
|
||
this.form.regionCode
|
||
);
|
||
if (!this.form.regionCode) {
|
||
this.handleDistrictChange(this.form.districtCode);
|
||
}
|
||
done();
|
||
});
|
||
},
|
||
searchReset() {
|
||
this.query = {};
|
||
this.setCityOptions([]);
|
||
this.setDistrictOptions([]);
|
||
this.onLoad(this.page);
|
||
},
|
||
searchChange(params, done) {
|
||
this.query = params;
|
||
this.page.currentPage = 1;
|
||
this.onLoad(this.page, params);
|
||
done();
|
||
},
|
||
selectionChange(list) {
|
||
this.selectionList = list;
|
||
},
|
||
selectionClear() {
|
||
this.selectionList = [];
|
||
this.$refs.crud.toggleSelection();
|
||
},
|
||
currentChange(currentPage) {
|
||
this.page.currentPage = currentPage;
|
||
},
|
||
sizeChange(pageSize) {
|
||
this.page.pageSize = pageSize;
|
||
},
|
||
refreshChange() {
|
||
this.onLoad(this.page, this.query);
|
||
},
|
||
onLoad(page, params = {}) {
|
||
this.loading = true;
|
||
getList(page.currentPage, page.pageSize, { ...params, ...this.query })
|
||
.then(res => {
|
||
const data = res.data.data;
|
||
this.page.total = data.total;
|
||
this.data = data.records;
|
||
this.selectionClear();
|
||
})
|
||
.finally(() => {
|
||
this.loading = false;
|
||
});
|
||
},
|
||
handleImport() {
|
||
openImportDialog(
|
||
this,
|
||
'港口码头主数据',
|
||
() => {
|
||
this.loadPortOptions();
|
||
},
|
||
{
|
||
failDetailDecorator: removeWorkbookColumnsByHeaders,
|
||
}
|
||
);
|
||
},
|
||
handleExport() {
|
||
this.$confirm('是否导出港口码头主数据?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}).then(() => {
|
||
NProgress.start();
|
||
exportBlob(
|
||
'/blade-system/port-terminal/export-port-terminal',
|
||
this.buildExportParams(),
|
||
{ feedback: true }
|
||
)
|
||
.then(async res => {
|
||
const blob = await removeExcelColumnsByHeaders(res.data, {
|
||
markRequiredHeaders: true,
|
||
});
|
||
downloadXls(
|
||
blob,
|
||
`港口码头主数据${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
|
||
);
|
||
})
|
||
.finally(() => {
|
||
NProgress.done();
|
||
});
|
||
});
|
||
},
|
||
buildExportParams() {
|
||
return {
|
||
...this.query,
|
||
ids: this.ids,
|
||
[this.website.tokenHeader]: getToken(),
|
||
};
|
||
},
|
||
handleTemplate() {
|
||
exportBlob(
|
||
`/blade-system/port-terminal/export-template?${this.website.tokenHeader}=${getToken()}`,
|
||
undefined,
|
||
{ feedback: true }
|
||
).then(async res => {
|
||
const blob = await removeExcelColumnsByHeaders(res.data);
|
||
downloadXls(blob, '港口码头主数据模板.xlsx');
|
||
});
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.port-terminal-page__detail-address {
|
||
display: block;
|
||
white-space: normal;
|
||
word-break: break-all;
|
||
line-height: 20px;
|
||
padding: 4px 0;
|
||
}
|
||
</style>
|