Files
tms-erp-web/src/views/base/port-terminal.vue
T
gxwebsoft 3bb0fddcca style(transportCapacity): 调整到期筛选标签位置和样式,统一标签展示逻辑
- transportCapacity/driver、vehicle、ship 页的「全部/30天内到期/已到期」筛选标签改为靠右对齐
- 统一使用 #search-menu 插槽替代 #expireStatus-search,标签移至搜索按钮前
- 标签容器删除宽度限制,增加 order:-1 和 margin-right:12px,保证标签和按钮为一体
- transportCapacity/vehicle 和 ship 的 expireStatus 列 search 改为 false,避免多余输入框出现
- transportCapacity/vehicle、ship 的 searchIndex 提升至6,避免搜索条件折叠,标签默认展开
- 暂用样式更改保证标签与搜索按钮间距和垂直对齐
- temporary-credit-limit 弹窗新增 dialogCustomClass,实现弹窗label固定六字宽度并自动换行
- element-ui.scss 新增临时额度弹窗 label 样式,修改label宽度、换行、对齐,提升可读性
- 调整多处搜索表单内 label 宽度由160px变为88px,优化表单紧凑度
- 多处文本域的最小行数 minRows 统一由较高数值改为2,改善表单布局和输入体验
- 更新 vite.config.mjs 开发服务器端口及代理配置,切换至本地测试环境
- waybill-import-dialog.vue 中修正文本域 autosize 最小行数为2,更符合表单设计规范
- 修改业务页 contract-manage.vue 菜单宽度由245px调整至252px,优化界面布局
2026-08-14 00:33:32 +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: 88,
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>