Files
tms-erp-web/src/views/base/port-terminal.vue
T
gxwebsoft 490efe2ce5 style(business): 优化业务弹窗布局与样式统一
- 修改 dispatchItemForm 表单标签宽度为 calc(6em + 24px),统一label宽度标准
- 调整 business-crud-page 内 section-card 间距,避免卡片紧贴,符合整体风格
- 规范 detail-content 内部元素间距及白卡底部边距,提升视觉一致性
- 调整 contract-manage select 组件列占比,优化表单布局
- 调整 customer-archive 弹窗间距,增强界面美观
- 全站弹窗新增底部操作栏固定与滚动内容区,提升弹窗交互体验
- port-terminal、railway-station 弹窗表单标签宽度调整及样式改进,提高表单对齐效果
- 统一多处弹窗及表单组件底部间距,确保全局风格一致
- port-terminal 弹窗取消分组白卡,恢复原有结构以满足用户需求
2026-08-13 16:35:35 +08:00

1424 lines
45 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="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 ? '自动带出' : '请选择国家'"
autocomplete="off"
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 #detailAddress-form>
<el-input
v-model="form.detailAddress"
class="detail-address-input"
clearable
maxlength="255"
show-word-limit
placeholder="请输入"
/>
</template>
<template #remark-form>
<el-input
v-model="form.remark"
class="remark-input"
type="textarea"
:rows="2"
maxlength="200"
show-word-limit
placeholder="请输入"
/>
</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="500px">
<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' });
};
const newLocal = '请选择类型';
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: 900,
dialogCustomClass: 'port-terminal-edit-dialog',
labelPosition: 'right',
labelWidth: '108px',
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: 180,
column: [
{
label: '编码',
prop: 'code',
search: true,
formslot: true,
width: 140,
span: 12,
order: 89,
placeholder: '请输入编码',
rules: [
{ required: true, message: '请输入编码', trigger: 'blur' },
{ validator: validateCode, trigger: 'blur' },
],
change: ({ value }) => this.handleCodeChange(value),
blur: ({ value }) => this.handleCodeChange(value),
},
{
label: '港口/码头名称',
prop: 'name',
minWidth: 140,
search: true,
searchOrder: 7,
span: 12,
order: 90,
placeholder: '请输入',
rules: [{ required: true, message: '请输入港口/码头名称', trigger: 'blur' }],
},
{
label: '类型',
prop: 'category',
type: 'select',
search: true,
searchOrder: 6,
formslot: true,
span: 24,
order: 100,
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,
order: 100,
placeholder: '请选择',
rules: [{ required: false, message: '请选择上级港口', trigger: 'change' }],
change: ({ value }) => this.handleParentChange(value),
},
{
label: '上级港口',
prop: 'parentName',
minWidth: 140,
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: 'country',
type: 'select',
search: true,
searchOrder: 5,
formslot: true,
filterable: true,
dicData: [],
span: 12,
order: 89,
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,
order: 88,
placeholder: '自动带出',
rules: [{ required: true, message: '请选择所属城市', trigger: 'change' }],
},
{
label: '城市',
prop: 'city',
type: 'select',
search: true,
searchOrder: 4,
filterable: true,
dicData: [],
addDisplay: false,
editDisplay: false,
minWidth: 100,
change: ({ value }) => this.handleCityNameChange(value),
},
{
label: '区县',
prop: 'districtCode',
type: 'select',
formslot: true,
hide: true,
order: 87,
props: {
label: 'title',
value: 'id',
},
dicData: [],
span: 12,
placeholder: '选择城市后再选择区县',
rules: [{ required: true, message: '请选择区县', trigger: 'change' }],
},
{
label: '区县',
prop: 'districtName',
type: 'select',
search: true,
searchOrder: 3,
filterable: true,
dicData: [],
minWidth: 100,
hide: true,
addDisplay: false,
editDisplay: false,
},
{
label: '经度',
prop: 'longitude',
formslot: true,
minWidth: 130,
span: 12,
order: 68,
placeholder: '请输入',
overHidden: false,
rules: [
{ required: true, message: '请输入经度', trigger: 'blur' },
{ validator: validateLongitude, trigger: 'blur' },
],
},
{
label: '纬度',
prop: 'latitude',
formslot: true,
minWidth: 130,
span: 12,
order: 66,
placeholder: '请输入',
overHidden: false,
rules: [
{ required: true, message: '请输入纬度', trigger: 'blur' },
{ validator: validateLatitude, trigger: 'blur' },
],
},
{
label: '行政区划编码',
prop: 'regionCode',
minWidth: 140,
span: 12,
placeholder: '选择区县后自动填写',
addDisplay: false,
editDisplay: false,
rules: [{ max: 32, message: '行政区划编码不能超过32字', trigger: 'blur' }],
},
{
label: '详细地址',
prop: 'detailAddress',
slot: true,
formslot: true,
span: 12,
minWidth: 220,
overHidden: false,
order: 86,
placeholder: '请输入',
maxlength: 255,
showWordLimit: true,
rules: [{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' }],
},
{
label: '数据来源',
prop: 'dataSource',
type: 'select',
search: true,
searchOrder: 2,
minWidth: 120,
overHidden: false,
addDisplay: false,
editDisplay: false,
dicData: [
{ label: '全部', value: '' },
{ label: '初始化导入', value: '初始化导入' },
{ label: '批量导入', value: '批量导入' },
{ label: '手工导入', value: '手工导入' },
],
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
minRows: 2,
span: 24,
formslot: true,
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',
sortable: true,
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',
sortable: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
addDisplay: false,
editDisplay: false,
minWidth: 160,
},
{
label: '状态',
prop: 'status',
type: 'select',
search: true,
searchOrder: 1,
slot: true,
dataType: 'number',
minWidth: 110,
overHidden: false,
addDisplay: false,
editDisplay: false,
dicData: [
{ label: '全部', value: '' },
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
],
},
],
},
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 required = value === '码头';
const parentColumn = this.findColumn(this.option.column, 'parentId');
if (parentColumn && parentColumn.rules && parentColumn.rules[0]) {
parentColumn.rules[0].required = required;
}
this.setColumnDisplay('parentId', required);
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(newLocal);
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();
setTimeout(() => this.disableDialogAutocomplete(), 50);
});
});
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();
setTimeout(() => this.disableDialogAutocomplete(), 50);
});
},
disableDialogAutocomplete() {
this.$nextTick(() => {
const dialog = document.querySelector('.port-terminal-edit-dialog');
if (!dialog) return;
dialog.querySelectorAll('input, textarea').forEach(el => {
el.setAttribute('autocomplete', 'off');
el.setAttribute('autocorrect', 'off');
el.setAttribute('autocapitalize', 'off');
el.setAttribute('spellcheck', 'false');
});
});
},
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>
<style lang="scss">
/* 港口码头编辑/新增弹窗:对齐 railway-station 统一风格(灰底 + 白卡表单 + 浮动底栏,padding 24px 与全局弹窗标准一致) */
//.port-terminal-edit-dialog {
// /* 灰底内容区 */
// .el-dialog__body {
// background-color: #f2f2f2;
// padding: 8px;
// }
// /* 表单白卡(与全局 .el-dialog .avue-form 一致,padding 24px 对齐 customer-archive / railway-station */
// .avue-form {
// background: #fff;
// border-radius: 6px;
// box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
// padding: 24px !important;
// margin-bottom: 0;
// }
// /* 底部按钮区白卡(Avue 弹窗 footer 在 body 内,接续白卡) */
// .avue-dialog__footer {
// background: #fff;
// border-radius: 0 0 6px 6px;
// box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
// padding: 16px 24px !important;
// display: flex;
// justify-content: flex-end;
// }
// /* label 宽度统一 108px(对齐 railway-station,标签对齐更整齐) */
// .el-form-item__label {
// width: 108px !important;
// }
// /* 所有 input 文本框、select 下拉控件统一宽度 250px */
// .el-input,
// .el-select {
// width: 250px !important;
// }
// /* 备注宽度 85% */
// .remark-input {
// width: 85% !important;
// }
//}
</style>