This commit is contained in:
2026-08-07 15:44:19 +08:00
parent 56729c8cc9
commit a8c495a102
4 changed files with 710 additions and 81 deletions
+5
View File
@@ -12,8 +12,12 @@ export const config = {
exportName: '发货模板', exportName: '发货模板',
enableAllDept: false, enableAllDept: false,
enableProjectSelect: true, enableProjectSelect: true,
excludeVoidedProjects: true,
enableAttachmentTable: true, enableAttachmentTable: true,
enableTransportPlanForm: true, enableTransportPlanForm: true,
enableShippingTemplateFreight: true,
editableRoadAddress: true,
fixedTransportAddressType: true,
enableTemplateCodePreview: true, enableTemplateCodePreview: true,
attachmentTitle: '附件', attachmentTitle: '附件',
defaultForm: { defaultForm: {
@@ -178,6 +182,7 @@ export const option = {
labelWidth: '0px', labelWidth: '0px',
className: 'business-crud-page__full-form-item', className: 'business-crud-page__full-form-item',
}, },
{ label: '运费信息', prop: 'freightJson', hide: true, display: false },
{ {
label: '', label: '',
prop: 'attachmentTitle', prop: 'attachmentTitle',
+1 -1
View File
@@ -97,7 +97,7 @@ const formatGoodsInfo = row => {
const text = Object.entries(groups) const text = Object.entries(groups)
.map(([unit, group]) => { .map(([unit, group]) => {
const quantity = `${formatGoodsQuantity(group.quantity)}${unit}`; const quantity = `${formatGoodsQuantity(group.quantity)}${unit}`;
return `${group.typeName}${group.count}货物 | ${quantity}`; return `${group.typeName}${group.count}货物 | ${quantity}`;
}) })
.join('; '); .join('; ');
@@ -104,6 +104,40 @@
</el-tag> </el-tag>
</template> </template>
<template #goodsInfo="{ row }">
<el-tooltip
v-if="isTransportPlanPage"
:content="formatTransportPlanGoodsInfo(row)"
placement="top"
:disabled="formatTransportPlanGoodsInfo(row).length <= 10"
>
<span>{{ truncateTransportPlanText(formatTransportPlanGoodsInfo(row), 10) }}</span>
</el-tooltip>
<span v-else>{{ row.goodsInfo || '-' }}</span>
</template>
<template #departureAddress="{ row }">
<el-tooltip
v-if="isTransportPlanPage"
:content="row.departureAddress || '-'"
placement="top"
>
<span>{{ formatTransportPlanDistrictAddress(row.departureAddress) }}</span>
</el-tooltip>
<span v-else>{{ row.departureAddress || '-' }}</span>
</template>
<template #arrivalAddress="{ row }">
<el-tooltip
v-if="isTransportPlanPage"
:content="row.arrivalAddress || '-'"
placement="top"
>
<span>{{ formatTransportPlanDistrictAddress(row.arrivalAddress) }}</span>
</el-tooltip>
<span v-else>{{ row.arrivalAddress || '-' }}</span>
</template>
<template #basicInfoTitle-form> <template #basicInfoTitle-form>
<div class="dialog-section-title">基本信息</div> <div class="dialog-section-title">基本信息</div>
</template> </template>
@@ -245,25 +279,36 @@
v-model="form.departureName" v-model="form.departureName"
readonly readonly
:placeholder="transportStationNamePlaceholder" :placeholder="transportStationNamePlaceholder"
:suffix-icon="transportStationMode ? Search : undefined"
:disabled="transportAddressSelectDisabled" :disabled="transportAddressSelectDisabled"
@click="openTransportPrimaryAddressPicker('departure')" @click="openTransportPrimaryAddressPicker('departure')"
/> >
<template v-if="transportStationMode" #suffix>
<el-icon class="business-crud-page__map-suffix" @click.stop="openTransportSecondaryAddressPicker('departure')">
<Search />
</el-icon>
</template>
</el-input>
<el-input <el-input
v-model="form.departureAddress" v-model="form.departureAddress"
readonly :readonly="transportStationMode || !config.editableRoadAddress"
:placeholder="transportAddressDetailPlaceholder" :placeholder="transportAddressDetailPlaceholder"
:suffix-icon="transportStationMode ? undefined : Location" :suffix-icon="transportStationMode || config.editableRoadAddress ? undefined : Location"
:disabled="transportAddressSelectDisabled" :disabled="transportAddressSelectDisabled"
@click="openTransportSecondaryAddressPicker('departure')" @click="handleTransportSecondaryAddressInputClick('departure')"
/> >
<template v-if="!transportStationMode && config.editableRoadAddress" #suffix>
<el-icon class="business-crud-page__map-suffix" @click.stop="openTransportMapDialog('departure')">
<Location />
</el-icon>
</template>
</el-input>
<el-tooltip content="选择常用地址" placement="top"> <el-tooltip content="选择常用地址" placement="top">
<el-button <el-button
type="primary" type="primary"
:icon="List" :icon="List"
link link
:disabled="transportAddressSelectDisabled" :disabled="transportAddressSelectDisabled"
@click="openTransportAddressDialog('departure')" @click="openTransportAddressPicker('departure')"
/> />
</el-tooltip> </el-tooltip>
<span class="business-crud-page__route-label">联系人</span> <span class="business-crud-page__route-label">联系人</span>
@@ -283,25 +328,36 @@
v-model="form.arrivalName" v-model="form.arrivalName"
readonly readonly
:placeholder="transportStationNamePlaceholder" :placeholder="transportStationNamePlaceholder"
:suffix-icon="transportStationMode ? Search : undefined"
:disabled="transportAddressSelectDisabled" :disabled="transportAddressSelectDisabled"
@click="openTransportPrimaryAddressPicker('arrival')" @click="openTransportPrimaryAddressPicker('arrival')"
/> >
<template v-if="transportStationMode" #suffix>
<el-icon class="business-crud-page__map-suffix" @click.stop="openTransportSecondaryAddressPicker('arrival')">
<Search />
</el-icon>
</template>
</el-input>
<el-input <el-input
v-model="form.arrivalAddress" v-model="form.arrivalAddress"
readonly :readonly="transportStationMode || !config.editableRoadAddress"
:placeholder="transportAddressDetailPlaceholder" :placeholder="transportAddressDetailPlaceholder"
:suffix-icon="transportStationMode ? undefined : Location" :suffix-icon="transportStationMode || config.editableRoadAddress ? undefined : Location"
:disabled="transportAddressSelectDisabled" :disabled="transportAddressSelectDisabled"
@click="openTransportSecondaryAddressPicker('arrival')" @click="handleTransportSecondaryAddressInputClick('arrival')"
/> >
<template v-if="!transportStationMode && config.editableRoadAddress" #suffix>
<el-icon class="business-crud-page__map-suffix" @click.stop="openTransportMapDialog('arrival')">
<Location />
</el-icon>
</template>
</el-input>
<el-tooltip content="选择常用地址" placement="top"> <el-tooltip content="选择常用地址" placement="top">
<el-button <el-button
type="primary" type="primary"
:icon="List" :icon="List"
link link
:disabled="transportAddressSelectDisabled" :disabled="transportAddressSelectDisabled"
@click="openTransportAddressDialog('arrival')" @click="openTransportAddressPicker('arrival')"
/> />
</el-tooltip> </el-tooltip>
<span class="business-crud-page__route-label">联系人</span> <span class="business-crud-page__route-label">联系人</span>
@@ -1235,9 +1291,150 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="business-crud-page__cargo-total"> <div v-if="!shippingTemplateFreightVisible" class="business-crud-page__cargo-total">
运输数量合计{{ transportCargoQuantityTotal }} 运输数量合计{{ transportCargoQuantityTotal }}
</div> </div>
<section v-if="shippingTemplateFreightVisible" class="business-crud-page__freight-section">
<div class="dialog-section-title">
运费信息{{ transportMode === 'road' ? '(公路)' : '' }}
</div>
<template v-if="transportMode === 'road'">
<el-form
:model="shippingTemplateFreight"
label-position="right"
label-width="auto"
class="business-crud-page__freight-form business-crud-page__freight-form--road"
>
<template v-for="(row, index) in shippingTemplateFreight.freightItems" :key="index">
<el-form-item :label="`单价${index + 1}`">
<el-input
v-model="row.unitPrice"
placeholder="请输入"
:disabled="dialogReadonly"
@input="value => handleShippingTemplateFreightNumberInput(row, 'unitPrice', value)"
>
<template #append>
<el-select
v-model="row.priceUnit"
class="business-crud-page__append-select"
placeholder="单位"
:disabled="dialogReadonly"
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
>
<el-option
v-for="item in taskFeeUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item :label="`数量合计${index + 1}`">
<el-input :model-value="shippingTemplateRoadFreightQuantity(index)" disabled>
<template #append>
<el-select
:model-value="transportCargoRows[index]?.quantityUnit"
class="business-crud-page__append-select"
placeholder="单位"
disabled
>
<el-option
:label="transportCargoRows[index]?.quantityUnit || '单位'"
:value="transportCargoRows[index]?.quantityUnit || ''"
/>
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item :label="`运费${index + 1}`">
<el-input :model-value="shippingTemplateRoadFreightAmount(row, index)" disabled>
<template #suffix>{{ shippingTemplateCurrencyLabel }}</template>
</el-input>
</el-form-item>
</template>
<el-form-item label="运费合计">
<el-input :model-value="shippingTemplateRoadFreightTotal" disabled>
<template #suffix>{{ shippingTemplateCurrencyLabel }}</template>
</el-input>
</el-form-item>
<el-form-item label="其他运费合计">
<el-input
v-model="shippingTemplateFreight.otherFreightAmount"
placeholder="请输入"
:disabled="dialogReadonly"
@input="value => handleShippingTemplateFreightNumberInput(shippingTemplateFreight, 'otherFreightAmount', value)"
>
<template #suffix>{{ shippingTemplateCurrencyLabel }}</template>
</el-input>
</el-form-item>
<el-form-item label="币种">
<el-select
v-model="shippingTemplateFreight.currency"
placeholder="请选择"
clearable
:disabled="dialogReadonly"
@visible-change="visible => visible && loadShippingTemplateCurrencyOptions()"
>
<el-option
v-for="item in shippingTemplateCurrencyOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
</template>
<el-form
v-else
:model="shippingTemplateFreight"
label-position="right"
label-width="auto"
class="business-crud-page__freight-form business-crud-page__freight-form--non-road"
>
<el-form-item label="运费">
<el-input
v-model="shippingTemplateFreight.freightAmount"
placeholder="请输入"
:disabled="dialogReadonly"
@input="value => handleShippingTemplateFreightNumberInput(shippingTemplateFreight, 'freightAmount', value)"
>
<template #suffix>{{ shippingTemplateCurrencyLabel }}</template>
</el-input>
</el-form-item>
<el-form-item label="数量">
<el-input :model-value="transportCargoQuantityTotal" disabled />
</el-form-item>
<el-form-item label="其他运费合计">
<el-input
v-model="shippingTemplateFreight.otherFreightAmount"
placeholder="请输入"
:disabled="dialogReadonly"
@input="value => handleShippingTemplateFreightNumberInput(shippingTemplateFreight, 'otherFreightAmount', value)"
>
<template #suffix>{{ shippingTemplateCurrencyLabel }}</template>
</el-input>
</el-form-item>
<el-form-item label="币种">
<el-select
v-model="shippingTemplateFreight.currency"
placeholder="请选择"
clearable
:disabled="dialogReadonly"
@visible-change="visible => visible && loadShippingTemplateCurrencyOptions()"
>
<el-option
v-for="item in shippingTemplateCurrencyOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
</section>
</div> </div>
</template> </template>
@@ -1653,6 +1850,21 @@
</template> </template>
<template #menu-form-before> <template #menu-form-before>
<div
v-if="config.enableShippingTemplateFreight && !dialogReadonly"
class="business-crud-page__shipping-template-footer-summary"
>
<span>
运费合计<strong>{{ formatShippingTemplateFooterFreightTotal }}</strong>
</span>
<el-link
type="primary"
:disabled="!form.projectId"
@click="openShippingTemplateProcessConfig"
>
查看过程配置
</el-link>
</div>
<el-button <el-button
v-if="config.enableDraftSave && !dialogReadonly" v-if="config.enableDraftSave && !dialogReadonly"
type="primary" type="primary"
@@ -3323,6 +3535,7 @@
clearable clearable
filterable filterable
placeholder="请选择" placeholder="请选择"
:disabled="!!getTransportFixedAddressType()"
> >
<el-option <el-option
v-for="item in addressTypeOptions" v-for="item in addressTypeOptions"
@@ -3405,7 +3618,7 @@
</el-dialog> </el-dialog>
<el-dialog <el-dialog
title="选择站点" :title="`选择${transportStationTypeLabel || '站点'}`"
append-to-body append-to-body
v-model="transportStationBox" v-model="transportStationBox"
width="1180px" width="1180px"
@@ -3634,8 +3847,8 @@
</el-dialog> </el-dialog>
<el-dialog title="导入货物" append-to-body v-model="cargoImportBox" width="560px"> <el-dialog title="导入货物" append-to-body v-model="cargoImportBox" width="560px">
<div class="business-crud-page__cargo-import">
<el-upload <el-upload
drag
action="#" action="#"
accept=".xls,.xlsx" accept=".xls,.xlsx"
:auto-upload="false" :auto-upload="false"
@@ -3643,19 +3856,50 @@
:disabled="cargoImportLoading" :disabled="cargoImportLoading"
:on-change="handleCargoImportFileChange" :on-change="handleCargoImportFileChange"
> >
<i class="el-icon-upload"></i> <el-button type="primary" :loading="cargoImportLoading">上传</el-button>
<div class="el-upload__text"> Excel 文件拖到此处或点击上传</div>
<template #tip> <template #tip>
<div class="el-upload__tip"> <div class="el-upload__tip">支持.xlsx.xls的文件格式单个文件大小限制50M</div>
按货物信息表头读取货物名称货物类型数量数量单位包装品牌规格型号物料编码设备编码备注
</div>
</template> </template>
</el-upload> </el-upload>
<el-button type="primary" @click="handleCargoImportTemplate">点击下载模板</el-button>
</div>
<template #footer> <template #footer>
<el-button @click="cargoImportBox = false">关闭</el-button> <el-button @click="cargoImportBox = false">关闭</el-button>
</template> </template>
</el-dialog> </el-dialog>
<el-dialog
v-model="shippingTemplateProcessConfigBox"
title="过程配置"
append-to-body
width="980px"
class="business-crud-page__process-config-dialog"
>
<div v-loading="shippingTemplateProcessConfigLoading">
<el-table :data="shippingTemplateProcessConfigRows" border empty-text="当前项目暂无过程配置">
<el-table-column prop="configCode" label="配置编号" min-width="160" />
<el-table-column prop="configName" label="配置名称" min-width="180" />
<el-table-column prop="projectNames" label="项目" min-width="220" />
<el-table-column label="过程节点" min-width="260">
<template #default="{ row }">
{{ formatShippingTemplateProcessNodes(row.includedNodes) }}
</template>
</el-table-column>
<el-table-column prop="defaultFinishDays" label="默认完成时限(天)" width="160" />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="Number(row.status) === 1 ? 'success' : 'info'">
{{ Number(row.status) === 1 ? '启用' : '停用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<el-button type="primary" @click="shippingTemplateProcessConfigBox = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog :title="`${config.title}导入`" append-to-body v-model="excelBox" width="555px"> <el-dialog :title="`${config.title}导入`" append-to-body v-model="excelBox" width="555px">
<avue-form :option="excelOptionConfig" v-model="excelForm"> <avue-form :option="excelOptionConfig" v-model="excelForm">
<template #excelTemplate> <template #excelTemplate>
@@ -3669,7 +3913,7 @@
</template> </template>
<script> <script>
import { exportBlob } from '@/api/common'; import { exportBlob, importBlob } from '@/api/common';
import { getList as getCommonAddressList } from '@/api/base/common-address'; import { getList as getCommonAddressList } from '@/api/base/common-address';
import { getList as getAirportMasterList } from '@/api/base/airport-master'; import { getList as getAirportMasterList } from '@/api/base/airport-master';
import { getList as getCargoTypeList } from '@/api/base/cargo-type'; import { getList as getCargoTypeList } from '@/api/base/cargo-type';
@@ -3682,9 +3926,11 @@ import { getList as getContractList } from '@/api/business/contract-manage';
import { getList as getProjectList } from '@/api/business/project-apply'; import { getList as getProjectList } from '@/api/business/project-apply';
import { getList as getCommonRouteList } from '@/api/business/common-route'; import { getList as getCommonRouteList } from '@/api/business/common-route';
import { getList as getTransportPlanList } from '@/api/business/transport-plan'; import { getList as getTransportPlanList } from '@/api/business/transport-plan';
import { getList as getProcessConfigList } from '@/api/business/process-config';
import { getList as getCarrierCustomerList } from '@/api/vehicle/customer-archive'; import { getList as getCarrierCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver'; import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary } from '@/api/system/dictbiz';
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { getDeptTree } from '@/api/system/dept'; import { getDeptTree } from '@/api/system/dept';
import { addressTypeOptions } from '@/option/base/common-address'; import { addressTypeOptions } from '@/option/base/common-address';
import { packageOptions } from '@/option/business/common'; import { packageOptions } from '@/option/business/common';
@@ -3804,6 +4050,18 @@ const defaultTransportCargo = () => ({
remark: '', remark: '',
}); });
const defaultShippingTemplateFreight = () => ({
currency: '',
otherFreightAmount: '',
freightAmount: '',
freightItems: [],
});
const defaultShippingTemplateFreightItem = () => ({
unitPrice: '',
priceUnit: '',
});
const defaultDispatchRow = () => ({ const defaultDispatchRow = () => ({
taskEntryMode: 'simple', taskEntryMode: 'simple',
transportType: '', transportType: '',
@@ -3953,6 +4211,12 @@ export default {
attachmentRows: [], attachmentRows: [],
contractFileRows: [], contractFileRows: [],
transportCargoRows: [], transportCargoRows: [],
shippingTemplateFreight: defaultShippingTemplateFreight(),
shippingTemplateCurrencyOptions: [],
shippingTemplateCurrencyLoading: false,
shippingTemplateProcessConfigBox: false,
shippingTemplateProcessConfigLoading: false,
shippingTemplateProcessConfigRows: [],
transportRouteBox: false, transportRouteBox: false,
transportRouteLoading: false, transportRouteLoading: false,
transportRouteQuery: {}, transportRouteQuery: {},
@@ -4128,6 +4392,10 @@ export default {
this.loadTaskFeeUnitOptions(); this.loadTaskFeeUnitOptions();
this.ensureBillingCargoTypeOptions(); this.ensureBillingCargoTypeOptions();
} }
if (this.config.enableShippingTemplateFreight) {
this.loadShippingTemplateCurrencyOptions();
this.loadTaskFeeUnitOptions();
}
this.consumeTemplateCreatePayload(); this.consumeTemplateCreatePayload();
}, },
computed: { computed: {
@@ -4140,6 +4408,9 @@ export default {
detailSections() { detailSections() {
return this.config.detailSections || []; return this.config.detailSections || [];
}, },
isTransportPlanPage() {
return this.config.permission === 'transport_plan';
},
transportPlanImportDuplicateCount() { transportPlanImportDuplicateCount() {
return this.transportPlanImportRows.filter(row => row._duplicate).length; return this.transportPlanImportRows.filter(row => row._duplicate).length;
}, },
@@ -4445,6 +4716,37 @@ export default {
}, 0); }, 0);
return Number.isInteger(total) ? String(total) : total.toFixed(2); return Number.isInteger(total) ? String(total) : total.toFixed(2);
}, },
shippingTemplateFreightVisible() {
return (
this.config.enableShippingTemplateFreight === true && this.form.templateType === '运单'
);
},
shippingTemplateCurrencyLabel() {
const currency = this.shippingTemplateFreight.currency;
if (!currency) return '';
const option = this.shippingTemplateCurrencyOptions.find(item => item.value === currency);
return option?.label || currency;
},
shippingTemplateRoadFreightTotal() {
const total = this.shippingTemplateFreight.freightItems.reduce(
(sum, item, index) => sum + Number(this.shippingTemplateRoadFreightAmount(item, index) || 0),
0
);
return this.formatShippingTemplateFreightNumber(total);
},
shippingTemplateFooterFreightTotal() {
const freightAmount =
this.transportMode === 'road'
? Number(this.shippingTemplateRoadFreightTotal || 0)
: Number(this.shippingTemplateFreight.freightAmount || 0);
return freightAmount + Number(this.shippingTemplateFreight.otherFreightAmount || 0);
},
formatShippingTemplateFooterFreightTotal() {
return this.shippingTemplateFooterFreightTotal.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
},
transportAddressSelectDisabled() { transportAddressSelectDisabled() {
return this.dialogReadonly || this.isBillingFieldEmpty(this.form.transportType); return this.dialogReadonly || this.isBillingFieldEmpty(this.form.transportType);
}, },
@@ -4480,6 +4782,7 @@ export default {
if (!this.shippingInfoFormEnabled || this.suppressTransportTypeClear) return; if (!this.shippingInfoFormEnabled || this.suppressTransportTypeClear) return;
if (String(value ?? '') === String(oldValue ?? '')) return; if (String(value ?? '') === String(oldValue ?? '')) return;
this.handleTransportTypeChange(value); this.handleTransportTypeChange(value);
this.syncShippingTemplateFreightItems();
}, },
}, },
methods: { methods: {
@@ -4490,6 +4793,31 @@ export default {
}; };
return applyTableMenuWidth(nextOption, menuButtonCount); return applyTableMenuWidth(nextOption, menuButtonCount);
}, },
formatTransportPlanGoodsInfo(row = {}) {
if (row.goodsInfo) return String(row.goodsInfo);
const goodsRows = this.parseJsonArray(row.goodsJson);
return goodsRows
.map(item => item.cargoType || item.goodsType || item.cargoName || item.goodsName || '')
.filter(Boolean)
.join('/');
},
truncateTransportPlanText(value, length = 10) {
const text = String(value || '');
return text.length > length ? `${text.slice(0, length)}...` : text || '-';
},
formatTransportPlanDistrictAddress(value) {
const text = String(value || '').trim();
if (!text) return '-';
const cityIndex = text.indexOf('市');
const districtStart = cityIndex > -1 ? cityIndex + 1 : 0;
const districtMatch = text
.slice(districtStart)
.match(/(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/);
if (districtMatch) {
return text.slice(0, districtStart + districtMatch.index + districtMatch[0].length);
}
return cityIndex > -1 ? text.slice(0, cityIndex + 1) : text;
},
hasPermission(code) { hasPermission(code) {
return this.isAdmin || this.validData(this.permission && this.permission[code], false); return this.isAdmin || this.validData(this.permission && this.permission[code], false);
}, },
@@ -4901,6 +5229,10 @@ export default {
if (this.config.enableTransportPlanForm) { if (this.config.enableTransportPlanForm) {
if (!this.validateTransportPlanForm(submitRow)) return false; if (!this.validateTransportPlanForm(submitRow)) return false;
submitRow.goodsJson = JSON.stringify(this.transportCargoRows); submitRow.goodsJson = JSON.stringify(this.transportCargoRows);
if (this.config.enableShippingTemplateFreight) {
if (!this.validateShippingTemplateFreight()) return false;
submitRow.freightJson = this.serializeShippingTemplateFreight();
}
} }
if (this.taskInfoFormEnabled) { if (this.taskInfoFormEnabled) {
if (!this.validateTaskInfoForm(submitRow)) return false; if (!this.validateTaskInfoForm(submitRow)) return false;
@@ -4980,6 +5312,142 @@ export default {
} }
return true; return true;
}, },
loadShippingTemplateCurrencyOptions() {
if (this.shippingTemplateCurrencyOptions.length || this.shippingTemplateCurrencyLoading) {
return Promise.resolve(this.shippingTemplateCurrencyOptions);
}
this.shippingTemplateCurrencyLoading = true;
return getSystemDictionary({ code: 'currency_type' })
.then(res => {
this.shippingTemplateCurrencyOptions = this.normalizeDictOptions(res.data?.data || []);
const cny = this.shippingTemplateCurrencyOptions.find(item =>
['CNY', '人民币', 'RMB'].includes(String(item.value).toUpperCase())
);
if (!this.shippingTemplateFreight.currency) {
this.shippingTemplateFreight.currency = (cny || this.shippingTemplateCurrencyOptions[0])?.value || '';
}
return this.shippingTemplateCurrencyOptions;
})
.finally(() => {
this.shippingTemplateCurrencyLoading = false;
});
},
initShippingTemplateFreight() {
if (!this.config.enableShippingTemplateFreight) return;
const freight = this.parseJsonObject(this.form.freightJson);
this.shippingTemplateFreight = {
...defaultShippingTemplateFreight(),
...freight,
freightItems: (freight.freightItems || []).map(item => ({
...defaultShippingTemplateFreightItem(),
...item,
})),
};
this.syncShippingTemplateFreightItems();
this.loadShippingTemplateCurrencyOptions();
},
syncShippingTemplateFreightItems() {
if (!this.config.enableShippingTemplateFreight || this.transportMode !== 'road') return;
const rows = this.shippingTemplateFreight.freightItems || [];
this.shippingTemplateFreight.freightItems = this.transportCargoRows.map((_, index) => ({
...defaultShippingTemplateFreightItem(),
...(rows[index] || {}),
}));
},
handleShippingTemplateFreightNumberInput(row, prop, value) {
const text = String(value || '').replace(/[^\d.]/g, '');
const parts = text.split('.');
row[prop] =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
},
shippingTemplateRoadFreightQuantity(index) {
return this.formatShippingTemplateFreightNumber(this.transportCargoRows[index]?.quantity || 0);
},
shippingTemplateRoadFreightAmount(item, index) {
const amount = Number(item?.unitPrice || 0) * Number(this.transportCargoRows[index]?.quantity || 0);
return this.formatShippingTemplateFreightNumber(amount);
},
formatShippingTemplateFreightNumber(value) {
const number = Number(value || 0);
if (!Number.isFinite(number)) return '';
return Number(number.toFixed(2)).toString();
},
serializeShippingTemplateFreight() {
if (this.form.templateType !== '运单') return '';
const freight = {
currency: this.shippingTemplateFreight.currency || '',
otherFreightAmount: this.shippingTemplateFreight.otherFreightAmount || '',
};
if (this.transportMode === 'road') {
freight.totalFreightAmount = this.shippingTemplateRoadFreightTotal;
freight.freightItems = this.shippingTemplateFreight.freightItems.map((item, index) => ({
cargoIndex: index,
unitPrice: item.unitPrice || '',
priceUnit: item.priceUnit || '',
quantity: this.shippingTemplateRoadFreightQuantity(index),
freightAmount: this.shippingTemplateRoadFreightAmount(item, index),
}));
} else {
freight.quantity = this.transportCargoQuantityTotal;
freight.freightAmount = this.shippingTemplateFreight.freightAmount || '';
}
return JSON.stringify(freight);
},
validateShippingTemplateFreight() {
if (!this.shippingTemplateFreightVisible) return true;
if (!this.shippingTemplateFreight.currency) {
this.$message.warning('请选择币种');
return false;
}
if (this.transportMode === 'road') {
const invalid = this.shippingTemplateFreight.freightItems.findIndex(
item => Number(item.unitPrice || 0) < 0
);
if (invalid > -1) {
this.$message.warning(`${invalid + 1}条货物单价不能小于0`);
return false;
}
const missingPriceUnit = this.shippingTemplateFreight.freightItems.findIndex(
item => !item.priceUnit
);
if (missingPriceUnit > -1) {
this.$message.warning(`请选择第${missingPriceUnit + 1}条货物的计价单位`);
return false;
}
} else if (Number(this.shippingTemplateFreight.freightAmount || 0) < 0) {
this.$message.warning('运费不能小于0');
return false;
}
if (Number(this.shippingTemplateFreight.otherFreightAmount || 0) < 0) {
this.$message.warning('其他运费合计不能小于0');
return false;
}
return true;
},
openShippingTemplateProcessConfig() {
if (!this.form.projectId || this.shippingTemplateProcessConfigLoading) return;
this.shippingTemplateProcessConfigBox = true;
this.shippingTemplateProcessConfigLoading = true;
getProcessConfigList(1, 100, { projectIds: this.form.projectId })
.then(res => {
this.shippingTemplateProcessConfigRows = extractRecords(res).filter(row => {
const projectIds = String(row.projectIds || '')
.split(',')
.map(item => item.trim());
return projectIds.includes(String(this.form.projectId));
});
})
.finally(() => {
this.shippingTemplateProcessConfigLoading = false;
});
},
formatShippingTemplateProcessNodes(nodes) {
if (Array.isArray(nodes)) return nodes.join('、');
return String(nodes || '')
.split(',')
.filter(Boolean)
.join('、');
},
stopSubmitLoading(loading) { stopSubmitLoading(loading) {
if (typeof loading === 'function') { if (typeof loading === 'function') {
loading(); loading();
@@ -5235,6 +5703,7 @@ export default {
this.clearShippingPlan(); this.clearShippingPlan();
} }
this.transportCargoRows = []; this.transportCargoRows = [];
this.shippingTemplateFreight = defaultShippingTemplateFreight();
this.billingPlanRows = []; this.billingPlanRows = [];
this.settlementRuleForm = defaultSettlementRule(); this.settlementRuleForm = defaultSettlementRule();
this.reconciliationForm = defaultReconciliation(); this.reconciliationForm = defaultReconciliation();
@@ -5343,7 +5812,9 @@ export default {
this.projectLoading = true; this.projectLoading = true;
getProjectList(1, 9999, this.config.projectQueryParams || {}) getProjectList(1, 9999, this.config.projectQueryParams || {})
.then(res => { .then(res => {
this.projectOptions = extractRecords(res); this.projectOptions = extractRecords(res).filter(
item => !this.config.excludeVoidedProjects || item.approvalStatus !== 'voided'
);
this.syncCurrentProjectOption(); this.syncCurrentProjectOption();
}) })
.finally(() => { .finally(() => {
@@ -5352,6 +5823,7 @@ export default {
}, },
syncCurrentProjectOption() { syncCurrentProjectOption() {
if (!this.form.projectId || !this.form.projectName) return; if (!this.form.projectId || !this.form.projectName) return;
if (this.config.excludeVoidedProjects) return;
const exists = this.projectOptions.some( const exists = this.projectOptions.some(
item => String(item.id) === String(this.form.projectId) item => String(item.id) === String(this.form.projectId)
); );
@@ -6001,6 +6473,7 @@ export default {
this.transportCargoRows = this.parseJsonArray(this.form.goodsJson).map(row => this.transportCargoRows = this.parseJsonArray(this.form.goodsJson).map(row =>
this.normalizeTransportCargoRow(row) this.normalizeTransportCargoRow(row)
); );
this.initShippingTemplateFreight();
this.syncTransportCargoJson(); this.syncTransportCargoJson();
this.syncCurrentContractOption(); this.syncCurrentContractOption();
this.restoreTransportCargoTypePaths(); this.restoreTransportCargoTypePaths();
@@ -6054,9 +6527,13 @@ export default {
} else { } else {
this.transportCargoRows.push(nextRow); this.transportCargoRows.push(nextRow);
} }
this.syncShippingTemplateFreightItems();
this.syncTransportCargoJson(); this.syncTransportCargoJson();
}, },
removeTransportCargoRow(index) { removeTransportCargoRow(index) {
if (this.config.enableShippingTemplateFreight && this.transportMode === 'road') {
this.shippingTemplateFreight.freightItems.splice(index, 1);
}
this.transportCargoRows.splice(index, 1); this.transportCargoRows.splice(index, 1);
if ( if (
!this.transportCargoRows.length && !this.transportCargoRows.length &&
@@ -6064,6 +6541,7 @@ export default {
) { ) {
this.transportCargoRows.push(this.normalizeTransportCargoRow()); this.transportCargoRows.push(this.normalizeTransportCargoRow());
} }
this.syncShippingTemplateFreightItems();
this.syncTransportCargoJson(); this.syncTransportCargoJson();
}, },
handleTransportCargoQuantityInput(row, value) { handleTransportCargoQuantityInput(row, value) {
@@ -6071,6 +6549,7 @@ export default {
const parts = text.split('.'); const parts = text.split('.');
row.quantity = row.quantity =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 3)}` : parts[0]; parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 3)}` : parts[0];
this.syncShippingTemplateFreightItems();
this.syncTransportCargoJson(); this.syncTransportCargoJson();
}, },
handleTransportCargoTypeChange(row, value) { handleTransportCargoTypeChange(row, value) {
@@ -6207,6 +6686,11 @@ export default {
} }
this.openTransportMapDialog(target); this.openTransportMapDialog(target);
}, },
handleTransportSecondaryAddressInputClick(target) {
if (this.transportStationMode || !this.config.editableRoadAddress) {
this.openTransportSecondaryAddressPicker(target);
}
},
ensureTransportTypeBeforeAddress() { ensureTransportTypeBeforeAddress() {
if (!this.shippingInfoFormEnabled || !this.isBillingFieldEmpty(this.form.transportType)) { if (!this.shippingInfoFormEnabled || !this.isBillingFieldEmpty(this.form.transportType)) {
return true; return true;
@@ -6381,10 +6865,31 @@ export default {
if (!this.ensureTransportTypeBeforeAddress()) return; if (!this.ensureTransportTypeBeforeAddress()) return;
this.transportAddressTarget = target; this.transportAddressTarget = target;
this.transportAddressSelected = null; this.transportAddressSelected = null;
this.transportAddressQuery = {}; this.resetTransportAddressQuery();
this.transportAddressPage.currentPage = 1; this.transportAddressPage.currentPage = 1;
this.transportAddressBox = true; this.transportAddressBox = true;
}, },
openTransportAddressPicker(target) {
if (this.transportStationMode) {
this.openTransportStationDialog(target);
return;
}
this.openTransportAddressDialog(target);
},
resetTransportAddressQuery() {
const addressType = this.getTransportFixedAddressType();
this.transportAddressQuery = addressType ? { addressType } : {};
},
getTransportFixedAddressType() {
if (!this.config.fixedTransportAddressType) return '';
const typeMap = {
road: '常规地址',
rail: '铁路车站',
water: '港口/码头',
air: '空港机场',
};
return typeMap[this.transportMode] || '';
},
loadTransportAddressList() { loadTransportAddressList() {
this.transportAddressLoading = true; this.transportAddressLoading = true;
getCommonAddressList( getCommonAddressList(
@@ -6409,7 +6914,7 @@ export default {
this.loadTransportAddressList(); this.loadTransportAddressList();
}, },
handleTransportAddressReset() { handleTransportAddressReset() {
this.transportAddressQuery = {}; this.resetTransportAddressQuery();
this.transportAddressPage.currentPage = 1; this.transportAddressPage.currentPage = 1;
this.loadTransportAddressList(); this.loadTransportAddressList();
}, },
@@ -6812,44 +7317,26 @@ export default {
async handleCargoImportFileChange(file) { async handleCargoImportFileChange(file) {
const rawFile = file.raw; const rawFile = file.raw;
if (!rawFile) return; if (!rawFile) return;
if (!/\.(xls|xlsx)$/i.test(rawFile.name || '')) {
this.$message.warning('请上传 .xlsx、.xls 格式文件');
return;
}
if (rawFile.size > 50 * 1024 * 1024) {
this.$message.warning('单个文件大小不能超过50M');
return;
}
this.cargoImportLoading = true; this.cargoImportLoading = true;
try { try {
const ExcelJSModule = await import('exceljs'); const res = await importBlob('/blade-transport/shipping-template/import-goods', rawFile);
const ExcelJS = ExcelJSModule.default || ExcelJSModule; const contentType = res.headers['content-type'] || res.data.type || '';
const workbook = new ExcelJS.Workbook(); if (contentType.includes('application/vnd.ms-excel')) {
await workbook.xlsx.load(await rawFile.arrayBuffer()); downloadXls(res.data, `发货模板货物导入失败明细${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
const worksheet = workbook.worksheets[0]; this.$message.warning('部分数据导入失败,已下载失败明细');
const headerRow = worksheet.getRow(1); return;
const headerMap = {};
headerRow.eachCell((cell, colNumber) => {
headerMap[String(cell.value || '').trim()] = colNumber;
});
const fieldMap = {
货物名称: 'cargoName',
货物类型: 'cargoType',
数量: 'quantity',
数量单位: 'quantityUnit',
包装: 'packageType',
品牌: 'brand',
规格: 'specification',
型号: 'model',
物料编码: 'materialCode',
设备编码: 'deviceCode',
备注: 'remark',
};
const rows = [];
worksheet.eachRow((row, rowNumber) => {
if (rowNumber === 1) return;
const nextRow = defaultTransportCargo();
Object.entries(fieldMap).forEach(([label, prop]) => {
const columnIndex = headerMap[label];
if (!columnIndex) return;
nextRow[prop] = String(row.getCell(columnIndex).value || '').trim();
});
if (Object.values(nextRow).some(value => String(value || '').trim())) {
rows.push(nextRow);
} }
}); const result = JSON.parse(await res.data.text());
if (result.code !== 200) throw new Error(result.msg || '货物导入失败');
const rows = result.data || [];
if (!rows.length) { if (!rows.length) {
this.$message.warning('未读取到货物数据'); this.$message.warning('未读取到货物数据');
return; return;
@@ -6864,6 +7351,11 @@ export default {
this.cargoImportLoading = false; this.cargoImportLoading = false;
} }
}, },
handleCargoImportTemplate() {
exportBlob('/blade-transport/shipping-template/export-goods-template').then(res => {
downloadXls(res.data, '发货模板货物导入模板.xlsx');
});
},
loadOrganizationOptions() { loadOrganizationOptions() {
if (!this.config.enableOrganizationSelect || this.organizationLoading) return; if (!this.config.enableOrganizationSelect || this.organizationLoading) return;
this.organizationLoading = true; this.organizationLoading = true;
@@ -9043,6 +9535,15 @@ export default {
white-space: nowrap; white-space: nowrap;
} }
&__map-suffix {
cursor: pointer;
color: #909399;
&:hover {
color: #409eff;
}
}
&__cargo-wrap { &__cargo-wrap {
display: block; display: block;
width: 100%; width: 100%;
@@ -9050,6 +9551,20 @@ export default {
overflow-x: auto; overflow-x: auto;
} }
&__cargo-import {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 56px;
padding: 12px 0 8px;
:deep(.el-upload__tip) {
margin: 16px 0 0;
color: #606266;
font-size: 16px;
}
}
&__cargo-table { &__cargo-table {
width: 100%; width: 100%;
min-width: 1870px; min-width: 1870px;
@@ -9118,6 +9633,87 @@ export default {
font-weight: 600; font-weight: 600;
} }
&__freight-section {
margin-top: 20px;
}
&__freight-table {
width: 100%;
:deep(.el-table__header th) {
background: #f5f7fa;
color: #303133;
font-weight: 600;
}
:deep(.el-table__cell) {
border-color: #eff1f7;
}
:deep(.el-input),
:deep(.el-select) {
width: 100%;
}
}
&__freight-summary {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
&__freight-form {
display: grid;
grid-template-columns: repeat(3, minmax(220px, 1fr));
gap: 16px 24px;
width: min(100%, 920px);
:deep(.el-form-item) {
margin-bottom: 0;
}
:deep(.el-input),
:deep(.el-select) {
width: 100%;
}
&--non-road {
width: 100%;
grid-template-columns: repeat(4, minmax(220px, 1fr));
}
}
&__shipping-template-footer-summary {
position: absolute;
left: 16px;
bottom: 10px;
display: inline-flex;
align-items: center;
gap: 24px;
height: 28px;
color: #303133;
font-size: 13px;
strong {
margin-left: 4px;
font-size: 15px;
}
:deep(.el-link) {
font-size: 13px;
}
}
&__process-config-dialog {
:deep(.el-table__cell) {
border-color: #eff1f7;
}
:deep(.el-table__row:nth-child(even) td) {
background: #fafafa;
}
}
&__dialog-search { &__dialog-search {
padding: 12px 12px 4px; padding: 12px 12px 4px;
margin-bottom: 12px; margin-bottom: 12px;
@@ -9162,6 +9758,16 @@ export default {
:deep(.el-select) { :deep(.el-select) {
width: 100%; width: 100%;
} }
&--road {
grid-template-columns: repeat(3, minmax(0, 1fr));
column-gap: 56px;
row-gap: 16px;
:deep(.business-crud-page__append-select) {
width: 128px;
}
}
} }
&__dialog-pagination { &__dialog-pagination {
+25 -7
View File
@@ -1015,7 +1015,7 @@
v-for="item in scoreQuantificationOptions" v-for="item in scoreQuantificationOptions"
:key="item.id" :key="item.id"
:label="item.name" :label="item.name"
:value="item.id" :value="String(item.id)"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -1960,14 +1960,14 @@ export default {
}, },
initScoreQuantificationOptions() { initScoreQuantificationOptions() {
this.scoreQuantificationLoading = true; this.scoreQuantificationLoading = true;
getCreditScoreQuantificationList(1, 9999, { status: 1 }) return getCreditScoreQuantificationList(1, 9999, { status: 1 })
.then(res => { .then(res => {
const data = res.data.data || {}; const data = res.data.data || {};
const records = Array.isArray(data) ? data : data.records || []; const records = Array.isArray(data) ? data : data.records || [];
this.scoreQuantificationOptions = records this.scoreQuantificationOptions = records
.filter(item => Number(item.status) === 1) .filter(item => Number(item.status) === 1)
.map(item => ({ .map(item => ({
id: item.id, id: String(item.id),
name: item.name || item.configName || item.title || item.id, name: item.name || item.configName || item.title || item.id,
})); }));
}) })
@@ -1975,6 +1975,21 @@ export default {
this.scoreQuantificationLoading = false; this.scoreQuantificationLoading = false;
}); });
}, },
ensureScoreQuantificationOption(quantificationId) {
if (!quantificationId) return Promise.resolve();
const id = String(quantificationId);
if (this.scoreQuantificationOptions.some(item => String(item.id) === id)) {
return Promise.resolve();
}
return getCreditScoreQuantificationDetail(quantificationId).then(res => {
const item = res.data.data || {};
if (!item.id) return;
this.scoreQuantificationOptions.push({
id: String(item.id),
name: item.name || item.configName || item.title || String(item.id),
});
});
},
loadDictOptions(code, target, fallback = [], callback) { loadDictOptions(code, target, fallback = [], callback) {
getDictionary({ code }).then(res => { getDictionary({ code }).then(res => {
const options = (res.data.data || []).map(item => ({ const options = (res.data.data || []).map(item => ({
@@ -2021,6 +2036,7 @@ export default {
const deptNames = deptIds const deptNames = deptIds
.map(id => this.deptOptions.find(item => String(item.value) === String(id))?.rawLabel) .map(id => this.deptOptions.find(item => String(item.value) === String(id))?.rawLabel)
.filter(Boolean); .filter(Boolean);
this.archiveForm.deptId = deptIds;
this.archiveForm.deptIds = deptIds.join(','); this.archiveForm.deptIds = deptIds.join(',');
this.archiveForm.deptName = deptNames.join(','); this.archiveForm.deptName = deptNames.join(',');
this.$refs.archiveForm?.validateField('deptName'); this.$refs.archiveForm?.validateField('deptName');
@@ -2691,6 +2707,7 @@ export default {
attachments: [], attachments: [],
details: [], details: [],
...score, ...score,
quantificationId: score.quantificationId ? String(score.quantificationId) : '',
tempApplyCreditLimit: this.normalizeOptionalAmount(score.tempApplyCreditLimit), tempApplyCreditLimit: this.normalizeOptionalAmount(score.tempApplyCreditLimit),
attachments: score.attachments || this.parseAttachments(score.proofAttachments), attachments: score.attachments || this.parseAttachments(score.proofAttachments),
details: score.details || [], details: score.details || [],
@@ -2807,7 +2824,7 @@ export default {
.map(id => this.deptOptions.find(item => String(item.value) === String(id))?.rawLabel) .map(id => this.deptOptions.find(item => String(item.value) === String(id))?.rawLabel)
.filter(Boolean); .filter(Boolean);
archive.deptIds = deptIds.join(','); archive.deptIds = deptIds.join(',');
archive.deptId = deptIds.length ? deptIds[0] : ''; archive.deptId = deptIds.length ? Number(deptIds[0]) : null;
archive.deptName = deptNames.length ? deptNames.join(',') : archive.deptName; archive.deptName = deptNames.length ? deptNames.join(',') : archive.deptName;
archive.registeredAddress = this.formatArchiveAddress(archive); archive.registeredAddress = this.formatArchiveAddress(archive);
delete archive.registeredRegionPath; delete archive.registeredRegionPath;
@@ -2884,9 +2901,10 @@ export default {
this.scoreAttachmentFiles = JSON.parse(JSON.stringify(this.currentScore.attachments || [])); this.scoreAttachmentFiles = JSON.parse(JSON.stringify(this.currentScore.attachments || []));
this.expandedScoreCategoryKeys = []; this.expandedScoreCategoryKeys = [];
this.scoreBox = true; this.scoreBox = true;
if (!this.scoreQuantificationOptions.length) { const loadOptions = this.scoreQuantificationOptions.length
this.initScoreQuantificationOptions(); ? Promise.resolve()
} : this.initScoreQuantificationOptions();
loadOptions.then(() => this.ensureScoreQuantificationOption(this.currentScore.quantificationId));
}, },
resetScoreDialog() { resetScoreDialog() {
this.scoreRecordIndex = -1; this.scoreRecordIndex = -1;