Merge remote-tracking branch 'origin/master'
# Conflicts: # src/option/business/shipping-template.js # src/option/business/transport-plan.js # src/option/business/waybill-manage.js # src/views/business/components/business-crud-page.vue # src/views/business/components/master-order-detail.vue # src/views/business/components/master-order-editor.vue # src/views/business/components/waybill-import-dialog.vue
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -2,19 +2,24 @@
|
||||
<div v-loading="loading" class="master-detail">
|
||||
<section v-if="master" class="detail-overview">
|
||||
<div class="detail-heading"><div><h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2><el-tag :type="statusType(master.businessStatus)">{{ statusName(master.businessStatus) }}</el-tag></div><el-button @click="$emit('back')">返回</el-button></div>
|
||||
<dl class="detail-meta"><div><dt>客户</dt><dd>{{ master.customerName || '-' }}</dd></div><div><dt>合同编号</dt><dd>{{ master.contractNo || '-' }}</dd></div><div><dt>项目</dt><dd>{{ master.projectName || '-' }}</dd></div><div><dt>货物名称</dt><dd>{{ goodsNames }}</dd></div><div><dt>货物类型</dt><dd>{{ goodsTypes }}</dd></div><div><dt>运输周期</dt><dd>{{ dateRange }}</dd></div></dl>
|
||||
<dl class="detail-meta"><div><dt>客户</dt><dd>{{ master.customerName || '-' }}</dd></div><div><dt>合同编号</dt><dd>{{ master.contractNo || '-' }}</dd></div><div><dt>项目</dt><dd>{{ master.projectName || '-' }}</dd></div><div class="detail-meta__attachments"><dt>附件</dt><dd><template v-if="attachments.length"><el-link v-for="file in attachments" :key="file.url || file.link || file.name || file.originalName" type="primary" @click="previewAttachment(file)">{{ attachmentName(file) }}</el-link></template><span v-else>-</span></dd></div></dl>
|
||||
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ route.departureName || '-' }}</strong><small>{{ route.departureAddress || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ route.arrivalName || '-' }}</strong><small>{{ route.arrivalAddress || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }}</small></div></template></div>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="documentPreviewVisible" :title="previewFile.name || '附件预览'" append-to-body destroy-on-close width="90%" top="4vh">
|
||||
<open-file-viewer v-if="documentPreviewVisible && previewFile.url" :file="previewFile.url" :file-name="previewFile.name" :mime-type="previewFile.mimeType" width="100%" height="72vh" fit="contain" theme="auto" locale="zh-CN" :toolbar="viewerToolbar" :plugins="viewerPlugins" />
|
||||
</el-dialog>
|
||||
<el-image-viewer v-if="imagePreviewVisible" :url-list="imagePreviewUrls" :initial-index="imagePreviewIndex" @close="imagePreviewVisible = false" />
|
||||
|
||||
<section v-if="master" class="execution-detail-card">
|
||||
<div class="execution-detail-heading"><h3>分段执行明细</h3></div>
|
||||
<section v-for="(route, index) in segments" :key="route.segmentNo" class="segment-detail">
|
||||
<header @click="toggleSegment(route.segmentNo)"><h3><span class="segment-index">{{ segmentNumber(route, index) }}</span><span>{{ route.departureName || '-' }} → {{ route.arrivalName || '-' }}</span></h3><div class="segment-detail__header-right"><span>已调度 <em>{{ quantity(route.dispatchedQuantity) }}</em> / {{ quantity(master.totalQuantity) }},已到达 <em>{{ quantity(route.arrivedQuantity) }}</em> / {{ quantity(master.totalQuantity) }}</span><el-tooltip :content="isSegmentExpanded(route.segmentNo) ? '折叠' : '展开'" placement="top"><el-button circle :icon="isSegmentExpanded(route.segmentNo) ? ArrowUp : ArrowDown" @click.stop="toggleSegment(route.segmentNo)" /></el-tooltip></div></header>
|
||||
<header @click="toggleSegment(route.segmentNo)"><h3><span class="segment-index">{{ segmentNumber(route, index) }}</span><span class="segment-detail__transport-type">{{ segmentTransportType(route) || '-' }}</span><span class="segment-detail__progress-bar"><span :style="{ width: `${segmentProgress(route)}%` }"></span></span><span>{{ route.departureName || '-' }} → {{ route.arrivalName || '-' }}</span><span class="segment-detail__progress">已调度 <em>{{ quantity(route.dispatchedQuantity) }}</em> / {{ quantity(plannedQuantity(route)) }} {{ plannedUnit(route) }}</span></h3><div class="segment-detail__header-right"><el-tooltip :content="isSegmentExpanded(route.segmentNo) ? '折叠' : '展开'" placement="top"><el-button circle :icon="isSegmentExpanded(route.segmentNo) ? ArrowUp : ArrowDown" @click.stop="toggleSegment(route.segmentNo)" /></el-tooltip></div></header>
|
||||
<div v-show="isSegmentExpanded(route.segmentNo)" class="segment-execution">
|
||||
<div class="segment-stats"><div><span>总量</span><strong>{{ quantity(master.totalQuantity) }}<small>吨</small></strong></div><div><span>已调度</span><strong class="dispatched">{{ quantity(route.dispatchedQuantity) }}<small>吨</small></strong></div><div><span>剩余</span><strong class="remaining">{{ quantity(remainingQuantity(route)) }}<small>吨</small></strong></div></div>
|
||||
<el-table :data="route.waybills || []" border class="waybill-table"><el-table-column label="运单号" min-width="180"><template #default="{ row }"><el-link type="primary" @click="openWaybill(row)">{{ row.waybillNo }}</el-link></template></el-table-column><el-table-column prop="carrierName" label="承运商" min-width="190" /><el-table-column label="司机/车牌" min-width="180"><template #default="{ row }">{{ driverVehicle(row) }}</template></el-table-column><el-table-column prop="cargoType" label="货物类型" min-width="120" /><el-table-column label="数量(吨)" width="130"><template #default="{ row }">{{ quantity(row.quantity) }}</template></el-table-column><el-table-column label="状态" width="130"><template #default="{ row }"><el-tag :type="waybillStatusType(row.businessStatus)" size="small">{{ waybillStatusName(row.businessStatus) }}</el-tag></template></el-table-column><el-table-column prop="createTime" label="创建时间" min-width="180" /></el-table>
|
||||
<el-empty v-if="!(route.waybills || []).length" description="暂无运单明细" :image-size="56" />
|
||||
<div v-if="(route.transportPlans || []).length" class="transport-plan-detail"><h4>关联计划单</h4><el-table :data="route.transportPlans" border><el-table-column label="计划单号" min-width="160"><template #default="{ row }"><el-link type="primary" @click="openPlan(row)">{{ row.planNo }}</el-link></template></el-table-column><el-table-column prop="planName" label="计划名称" min-width="180" /><el-table-column prop="transportType" label="运输类型" width="130" /><el-table-column prop="planStartDate" label="计划开始日期" width="130" /><el-table-column prop="planEndDate" label="计划结束日期" width="130" /><el-table-column prop="businessStatus" label="状态" width="110" /></el-table></div>
|
||||
<div v-if="(route.transportPlans || []).length" class="transport-plan-detail"><h4>关联计划单</h4><el-table :data="route.transportPlans" border><el-table-column label="计划单号" min-width="160"><template #default="{ row }"><el-link type="primary" @click="openPlan(row)">{{ row.planNo }}</el-link></template></el-table-column><el-table-column prop="planName" label="计划名称" min-width="180" /><el-table-column prop="transportType" label="运输方式" width="130" /><el-table-column prop="planStartDate" label="计划开始日期" width="130" /><el-table-column prop="planEndDate" label="计划结束日期" width="130" /><el-table-column prop="businessStatus" label="状态" width="110" /></el-table></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
@@ -29,13 +34,19 @@
|
||||
<script>
|
||||
import * as api from '@/api/business/master-order';
|
||||
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
||||
import '@open-file-viewer/core/style.css';
|
||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
|
||||
const viewerPlugins = [imagePlugin(), pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }), officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }), textPlugin(), fallbackPlugin()];
|
||||
|
||||
export default {
|
||||
components: { SectionCard },
|
||||
components: { ElImageViewer, OpenFileViewer },
|
||||
props: { id: [String, Number] },
|
||||
emits: ['back'],
|
||||
data() { return { loading: false, master: null, expandedSegments: {}, ArrowDown, ArrowUp }; },
|
||||
data() { return { loading: false, master: null, expandedSegments: {}, ArrowDown, ArrowUp, imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { download: true, fullscreen: true, print: true, rotate: true, zoom: true } }; },
|
||||
computed: {
|
||||
segments() {
|
||||
const routes = this.master?.routeProgress || this.master?.routes || [];
|
||||
@@ -44,6 +55,7 @@ export default {
|
||||
return { ...route, departureName: previous?.departureName, departureAddress: previous?.departureAddress, arrivalName: route.departureName || this.master?.arrivalName, arrivalAddress: route.departureAddress || this.master?.arrivalAddress };
|
||||
});
|
||||
},
|
||||
attachments() { const value = this.master?.attachmentsJson; if (Array.isArray(value)) return value; if (!value) return []; try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : []; } catch (error) { return []; } },
|
||||
goodsNames() { return (this.master?.goods || []).map(item => item.cargoName).filter(Boolean).join('、') || '-'; },
|
||||
goodsTypes() { return [...new Set((this.master?.goods || []).map(item => item.cargoType).filter(Boolean))].join('、') || '-'; },
|
||||
dateRange() { return this.master?.planStartTime && this.master?.planEndTime ? `${String(this.master.planStartTime).slice(0, 10)} ~ ${String(this.master.planEndTime).slice(0, 10)}` : '-'; },
|
||||
@@ -65,6 +77,26 @@ export default {
|
||||
isSegmentExpanded(segmentNo) { return Boolean(this.expandedSegments[segmentNo]); },
|
||||
toggleSegment(segmentNo) { this.expandedSegments = { ...this.expandedSegments, [segmentNo]: !this.isSegmentExpanded(segmentNo) }; },
|
||||
remainingQuantity(route) { return Math.max(0, Number(this.master?.totalQuantity || 0) - Number(route.dispatchedQuantity || 0)); },
|
||||
segmentTransportType(route = {}) { return route.transportTypeName || route.transportType || ''; },
|
||||
plannedQuantity(route = {}) { return route.planQuantity || route.totalQuantity || this.master?.totalQuantity || 0; },
|
||||
plannedUnit(route = {}) { return route.quantityUnit || this.master?.goods?.[0]?.quantityUnit || '吨'; },
|
||||
segmentProgress(route = {}) { const planned = Number(this.plannedQuantity(route)); return planned > 0 ? Math.min(100, Math.max(0, (Number(route.dispatchedQuantity || 0) / planned) * 100)) : 0; },
|
||||
attachmentUrl(file = {}) { return file.url || file.link || file.fileUrl || file.downloadUrl || file.domain || ''; },
|
||||
attachmentName(file = {}) { return file.originalName || file.name || file.fileName || '附件'; },
|
||||
attachmentExtension(file = {}) { const source = this.attachmentName(file).split('?')[0]; const index = source.lastIndexOf('.'); return index > -1 ? source.slice(index + 1).toLowerCase() : ''; },
|
||||
isImageAttachment(file) { return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(this.attachmentExtension(file)); },
|
||||
previewAttachment(file) {
|
||||
const url = this.attachmentUrl(file);
|
||||
if (!url) return this.$message.warning('附件地址为空,无法预览');
|
||||
if (this.isImageAttachment(file)) {
|
||||
this.imagePreviewUrls = this.attachments.filter(item => this.isImageAttachment(item)).map(item => this.attachmentUrl(item)).filter(Boolean);
|
||||
this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0);
|
||||
this.imagePreviewVisible = true;
|
||||
return;
|
||||
}
|
||||
this.previewFile = { name: this.attachmentName(file), url, mimeType: file.mimeType || file.contentType || '' };
|
||||
this.documentPreviewVisible = true;
|
||||
},
|
||||
driverVehicle(row) { return [row.driverName, row.vehicleNo].filter(Boolean).join(' / ') || '-'; },
|
||||
openWaybill(row) { this.$router.push({ path: '/business/waybill-manage', query: { detailId: row.id } }); },
|
||||
openPlan(row) { this.$router.push({ path: '/business/transport-plan', query: { detailId: row.id } }); },
|
||||
@@ -80,14 +112,18 @@ export default {
|
||||
.master-detail { padding-bottom: 20px; color: #303133; }
|
||||
.detail-overview, .execution-detail-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
||||
.detail-heading { display: flex; align-items: center; justify-content: space-between; padding: 18px 24px; h2 { display: inline-block; margin: 0 16px 0 0; font-size: 20px; } h2 span { margin: 0 8px; color: #909399; font-weight: 400; } }
|
||||
.detail-meta { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
|
||||
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
|
||||
.route-map { display: flex; align-items: flex-start; padding: 16px 24px 20px; border-top: 1px solid #eff1f7; overflow-x: auto; }
|
||||
.route-map__node { display: grid; flex: 0 0 150px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
|
||||
.route-map__progress { color: #409eff !important; }
|
||||
.route-badge { display: inline-flex; width: 32px; height: 32px; align-items: center; justify-content: center; border-radius: 4px; background: #67c23a; color: #fff; font-weight: 600; &.start { background: #409eff; } &.end { background: #e6a23c; } }
|
||||
.route-map__line { flex: 1 0 64px; min-width: 64px; height: 32px; border-bottom: 2px solid #606266; }
|
||||
.execution-detail-heading { padding: 20px 24px 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
||||
.segment-detail { padding: 16px 24px; border-top: 1px solid #eff1f7; header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; cursor: pointer; h3 { display: flex; align-items: center; gap: 12px; margin: 0; font-size: 16px; } em { color: #409eff; font-style: normal; } } }
|
||||
.segment-detail { padding: 16px 24px; border-top: 1px solid #eff1f7; header { display: flex; align-items: center; justify-content: space-between; margin: -16px -24px 8px; padding: 12px 24px; cursor: pointer; background: #fafbfc; h3 { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 12px; margin: 0; font-size: 16px; } em { color: #409eff; font-style: normal; } } }
|
||||
.segment-detail__progress { color: #606266; font-size: 14px; font-weight: 400; }
|
||||
.segment-detail__transport-type { color: #409eff; font-size: 14px; font-weight: 500; }
|
||||
.segment-detail__progress-bar { display: inline-flex; width: 72px; height: 6px; overflow: hidden; border-radius: 3px; background: #e4e7ed; }
|
||||
.segment-detail__progress-bar span { display: block; height: 100%; background: #409eff; transition: width 0.2s ease; }
|
||||
.segment-detail__header-right { display: flex; align-items: center; gap: 12px; }
|
||||
.segment-index { display: inline-flex; width: 36px; height: 36px; align-items: center; justify-content: center; border-radius: 50%; background: #2088ee; color: #fff; font-size: 20px; font-weight: 500; }
|
||||
.segment-execution { padding-top: 12px; }
|
||||
|
||||
@@ -41,13 +41,13 @@
|
||||
<div>
|
||||
<el-checkbox v-model="route.selected" :label="`${route.segmentNo}:${route.departureName} → ${route.arrivalName}`" />
|
||||
</div>
|
||||
<div>总量 {{ formatQuantity(totalQuantity) }},已调度 <em>{{ formatQuantity(route.dispatchedQuantity) }}</em>,剩余 <em>{{ formatQuantity(remaining(route)) }}</em></div>
|
||||
<div>总量 {{ formatQuantity(totalQuantity) }},已调度 <em>{{ formatQuantity(dispatchedQuantity(route)) }}</em>,剩余 <em>{{ formatQuantity(remaining(route)) }}</em></div>
|
||||
</header>
|
||||
<div v-show="route.selected" class="segment-content">
|
||||
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="运输类型" required class="transport-type-field"><el-input :model-value="route.transportType" readonly /></el-form-item>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="运输方式" required class="transport-type-field"><el-input :model-value="route.transportType" readonly /></el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="isRoad(route)" :span="8">
|
||||
<el-form-item label="单据类型" required>
|
||||
@@ -56,13 +56,13 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="发货地址" required><el-input :model-value="addressText(route.departureName, route.departureAddress)" readonly /></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4"><el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.departurePhone" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="4"><el-form-item :label="dateStartLabel(route)" required><el-date-picker v-model="route.estimatedStartTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择" /></el-form-item></el-col>
|
||||
<el-col :span="8">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="收货地址" required><el-input :model-value="addressText(route.arrivalName, route.arrivalAddress)" readonly /></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4"><el-form-item label="联系人"><el-input v-model="route.arrivalContact" placeholder="请输入" /></el-form-item></el-col>
|
||||
@@ -74,10 +74,10 @@
|
||||
<div class="goods-heading"><h3>货物信息</h3><div><el-link type="primary" @click="addGoods(route)">新增货物</el-link></div></div>
|
||||
<el-table :data="route.goods" border class="goods-table">
|
||||
<el-table-column type="index" label="序号" width="64" />
|
||||
<el-table-column label="货物名称" min-width="180"><template #default="{ row }"><el-select v-model="row.sourceIndex" placeholder="请选择总单货物" @change="selectGoods(route, row)"><el-option v-for="item in masterGoods" :key="item.sourceIndex" :label="item.label" :value="item.sourceIndex" /></el-select></template></el-table-column>
|
||||
<el-table-column label="货物类型" min-width="130"><template #default="{ row }"><el-input :model-value="row.cargoType" readonly /></template></el-table-column>
|
||||
<el-table-column label="剩余数量" width="120"><template #default="{ row }">{{ formatQuantity(row.remainingQuantity) }}</template></el-table-column>
|
||||
<el-table-column label="本次数量" width="160"><template #default="{ row }"><el-input :model-value="row.dispatchQuantity" inputmode="decimal" placeholder="请输入" @input="value => handleDispatchQuantityInput(row, value)" /></template></el-table-column>
|
||||
<el-table-column label="货物类型" min-width="180"><template #default="{ row }"><el-cascader v-model="row.cargoTypePath" :options="cargoTypeOptions" :props="cargoTypeCascaderProps" placeholder="请选择货物类型" clearable filterable @change="value => handleCargoTypeChange(route, row, value)" /></template></el-table-column>
|
||||
<el-table-column label="货物名称" min-width="180"><template #default="{ row }"><el-select v-model="row.sourceIndex" placeholder="请选择总单货物" @change="selectGoods(route, row)"><el-option v-for="item in masterGoodsByCargoType(row)" :key="item.sourceIndex" :label="item.cargoName" :value="item.sourceIndex" /></el-select></template></el-table-column>
|
||||
<el-table-column prop="remainingQuantity" column-key="remainingQuantity" label="剩余数量" width="150" class-name="goods-table__remaining"><template #default="{ row }">{{ formatQuantity(goodsRemainingQuantity(route, row)) }}</template></el-table-column>
|
||||
<el-table-column prop="dispatchQuantity" column-key="dispatchQuantity" label="本次数量" width="200" class-name="goods-table__dispatch"><template #default="{ row }"><el-input :model-value="row.dispatchQuantity" inputmode="decimal" placeholder="请输入" @input="value => handleDispatchQuantityInput(route, row, value)" /></template></el-table-column>
|
||||
<el-table-column prop="quantityUnit" label="数量单位" width="110" />
|
||||
<el-table-column prop="packageType" label="包装" min-width="110" />
|
||||
<el-table-column prop="brand" label="品牌" min-width="110" />
|
||||
@@ -89,11 +89,31 @@
|
||||
|
||||
<div class="freight-heading"><h3>运费信息</h3></div>
|
||||
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form freight-form">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6"><el-form-item label="单价"><el-input :model-value="route.unitPrice" inputmode="decimal" placeholder="请输入" @input="value => handleUnitPriceInput(route, value)" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="计价单位"><el-select v-model="route.priceUnit"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="重量合计"><el-input :model-value="formatQuantity(dispatchWeight(route))" readonly><template #append>吨</template></el-input></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="运费"><el-input :model-value="formatAmount(freightAmount(route))" readonly><template #append>元</template></el-input></el-form-item></el-col>
|
||||
<el-row v-for="(item, index) in route.freightItems" :key="item.sourceIndex" :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="`单价${index + 1}`" required>
|
||||
<el-input :model-value="item.unitPrice" inputmode="decimal" placeholder="请输入" @input="value => handleFreightUnitPriceInput(item, value)">
|
||||
<template #append><el-select v-model="item.priceUnit" class="freight-unit-select"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /><el-option label="元/方" value="元/方" /></el-select></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="`数量${index + 1}`" required>
|
||||
<el-input :model-value="formatQuantity(item.quantity)" readonly>
|
||||
<template #append><el-select v-model="item.quantityUnit" class="freight-unit-select" @change="value => handleFreightQuantityUnitChange(route, item, value)"><el-option v-for="unit in quantityUnitOptions" :key="unit" :label="unit" :value="unit" /></el-select></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="`运费${index + 1}`">
|
||||
<el-input :model-value="formatAmount(freightAmount(item))" readonly><template #suffix>{{ currencyLabel(route.currency) }}</template></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-if="route.freightItems.length" :gutter="16">
|
||||
<el-col :span="6"><el-form-item label="运费合计"><el-input :model-value="formatAmount(freightTotal(route))" readonly><template #suffix>{{ currencyLabel(route.currency) }}</template></el-input></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="其他费用合计"><el-input :model-value="route.otherFeeTotal" inputmode="decimal" placeholder="请输入" @input="value => handleOtherFeeTotalInput(route, value)" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="币种"><el-select v-model="route.currency"><el-option label="人民币" value="CNY" /><el-option label="美元" value="USD" /><el-option label="欧元" value="EUR" /></el-select></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
@@ -108,7 +128,7 @@
|
||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="挂车车牌号" required><el-input v-model="route.trailerVehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人" required><el-input v-model="route.escortName" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人手机号" required><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="里程(km)" required><el-input :model-value="route.mileage" inputmode="decimal" placeholder="请输入" @input="value => handleMileageInput(route, value)" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="里程(km)" required><el-input :model-value="route.mileage" inputmode="numeric" maxlength="10" placeholder="请输入" @input="value => handleMileageInput(route, value)" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="备注"><el-input v-model="route.remark" maxlength="200" show-word-limit placeholder="请输入" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
@@ -135,6 +155,7 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div v-if="pendingExpanded" class="pending-list-spacer" aria-hidden="true"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -142,6 +163,7 @@
|
||||
import * as api from '@/api/business/master-order';
|
||||
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
|
||||
const unwrapRecords = res => {
|
||||
const data = res?.data || res;
|
||||
@@ -156,7 +178,7 @@ export default {
|
||||
props: { id: [String, Number] },
|
||||
emits: ['back'],
|
||||
data() {
|
||||
return { loading: false, submitting: false, master: null, routes: [], pending: [], pendingExpanded: false, editingId: null, driverInputs: {}, carrierOptions: [], driverOptions: [], carrierLoading: false, driverLoading: false };
|
||||
return { loading: false, submitting: false, master: null, routes: [], pending: [], pendingExpanded: false, editingId: null, driverInputs: {}, carrierOptions: [], driverOptions: [], carrierLoading: false, driverLoading: false, cargoTypeOptions: [], cargoTypeCascaderProps: { label: 'cargoName', value: 'id', children: 'children', emitPath: true }, quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'] };
|
||||
},
|
||||
computed: {
|
||||
totalQuantity() { return (this.master?.goods || []).reduce((sum, item) => sum + Number(item.quantity || 0), 0); },
|
||||
@@ -186,60 +208,285 @@ export default {
|
||||
try {
|
||||
const res = await api.getDetail(this.id);
|
||||
this.master = res.data?.data || res.data || res;
|
||||
this.routes = (this.master.routes || []).map((node, index) => this.createRoute(node, index));
|
||||
this.cargoTypeOptions = this.buildMasterCargoTypeOptions(this.master.goods || []);
|
||||
this.routes = this.dispatchRouteNodes().map((node, index) => this.createRoute(node, index));
|
||||
} finally { this.loading = false; }
|
||||
},
|
||||
dispatchRouteNodes() {
|
||||
const routes = Array.isArray(this.master?.routes) ? this.master.routes : [];
|
||||
if (routes.length) return routes;
|
||||
return [{
|
||||
segmentNo: '段1',
|
||||
transportType: this.master?.transportType || '',
|
||||
departureName: this.master?.arrivalName || '',
|
||||
departureAddress: this.master?.arrivalAddress || '',
|
||||
departureContact: this.master?.arrivalContact || '',
|
||||
departurePhone: this.master?.arrivalPhone || '',
|
||||
}];
|
||||
},
|
||||
createRoute(node, index) {
|
||||
const previous = index ? this.master.routes[index - 1] : this.master;
|
||||
return {
|
||||
const routeNodes = this.dispatchRouteNodes();
|
||||
const previous = index ? routeNodes[index - 1] : this.master;
|
||||
const route = {
|
||||
...node,
|
||||
selected: false,
|
||||
documentType: '运单',
|
||||
carrierType: '承运商',
|
||||
unitPrice: undefined,
|
||||
priceUnit: '元/吨',
|
||||
currency: 'CNY',
|
||||
otherFeeTotal: '',
|
||||
freightItems: [],
|
||||
departureName: previous.departureName || '', departureAddress: previous.departureAddress || '', departureContact: previous.departureContact || '', departurePhone: previous.departurePhone || '',
|
||||
arrivalName: node.departureName || this.master.arrivalName || '', arrivalAddress: node.departureAddress || this.master.arrivalAddress || '', arrivalContact: node.departureContact || '', arrivalPhone: node.departurePhone || '',
|
||||
goods: [],
|
||||
goods: (this.master.goods || []).map((goods, sourceIndex) => this.createGoodsRow(goods, sourceIndex)),
|
||||
};
|
||||
this.syncFreightItems(route);
|
||||
return route;
|
||||
},
|
||||
buildMasterCargoTypeOptions(goods = []) {
|
||||
return [...new Set(goods.map(item => item.cargoType).filter(Boolean))].map(cargoType => ({
|
||||
id: cargoType,
|
||||
cargoName: cargoType,
|
||||
}));
|
||||
},
|
||||
masterGoodsByCargoType(row = {}) {
|
||||
return this.masterGoods.filter(item => !row.cargoType || item.cargoType === row.cargoType);
|
||||
},
|
||||
findCargoTypeByPath(path = []) {
|
||||
let options = this.cargoTypeOptions;
|
||||
let selected;
|
||||
(path || []).forEach(id => {
|
||||
selected = (options || []).find(item => String(item.id) === String(id));
|
||||
options = selected?.children || [];
|
||||
});
|
||||
return selected;
|
||||
},
|
||||
findCargoTypePath(cargoType, options = this.cargoTypeOptions, parentPath = []) {
|
||||
for (const option of options || []) {
|
||||
const path = [...parentPath, option.id];
|
||||
if (option.cargoName === cargoType) return path;
|
||||
if (option.children?.length) {
|
||||
const result = this.findCargoTypePath(cargoType, option.children, path);
|
||||
if (result.length) return result;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
},
|
||||
handleCargoTypeChange(route, row, value) {
|
||||
const path = Array.isArray(value) ? value : [];
|
||||
const cargoType = this.findCargoTypeByPath(path);
|
||||
row.cargoTypePath = path;
|
||||
row.cargoType = cargoType?.cargoName || '';
|
||||
const firstGoods = this.masterGoodsByCargoType(row)[0];
|
||||
if (firstGoods) {
|
||||
row.sourceIndex = firstGoods.sourceIndex;
|
||||
this.selectGoods(route, row);
|
||||
}
|
||||
else Object.assign(row, { sourceIndex: undefined, cargoName: '', quantity: '', quantityUnit: '' });
|
||||
this.syncFreightItems(route);
|
||||
},
|
||||
isRoad(route) { return String(route.transportType || '').includes('公路'); },
|
||||
remaining(route) { return Math.max(0, this.totalQuantity - Number(route.dispatchedQuantity || 0)); },
|
||||
dispatchedQuantity(route) {
|
||||
return this.quantityNumber(route.dispatchedQuantity) + this.pendingSegmentQuantity(route);
|
||||
},
|
||||
remaining(route) {
|
||||
return Math.max(
|
||||
0,
|
||||
this.totalQuantity -
|
||||
this.dispatchedQuantity(route)
|
||||
);
|
||||
},
|
||||
formatQuantity(value) { const number = Number(value || 0); return Number.isInteger(number) ? number : number.toFixed(3).replace(/\.?0+$/, ''); },
|
||||
formatAmount(value) { return Number(value || 0).toFixed(2); },
|
||||
dispatchWeight(route) { return (route.goods || []).reduce((sum, item) => sum + Number(item.dispatchQuantity || 0), 0); },
|
||||
freightAmount(route) { return Number(route.unitPrice || 0) * this.dispatchWeight(route); },
|
||||
handleDispatchQuantityInput(row, value) {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
row.dispatchQuantity = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 3)}` : integer;
|
||||
freightAmount(item = {}) { return Number(item.unitPrice || 0) * Number(item.quantity || 0); },
|
||||
freightTotal(route) {
|
||||
const goodsFreight = (route.freightItems || []).reduce(
|
||||
(sum, item) => sum + this.freightAmount(item),
|
||||
0
|
||||
);
|
||||
return goodsFreight + Number(route.otherFeeTotal || 0);
|
||||
},
|
||||
handleUnitPriceInput(route, value) {
|
||||
buildFreightJson(route) {
|
||||
return JSON.stringify({
|
||||
currency: route.currency || 'CNY',
|
||||
totalFreightAmount: this.freightTotal(route),
|
||||
otherFreightAmount: route.otherFeeTotal || '',
|
||||
freightItems: (route.freightItems || []).map(item => ({
|
||||
...item,
|
||||
freightAmount: this.freightAmount(item),
|
||||
})),
|
||||
});
|
||||
},
|
||||
currencyLabel(value) { return ({ CNY: '人民币', USD: '美元', EUR: '欧元' })[value] || value || '人民币'; },
|
||||
quantityNumber(value) { return Number(value || 0); },
|
||||
goodsKey(goods = {}, includeUnit = true) {
|
||||
return [
|
||||
goods.cargoName || '',
|
||||
goods.cargoType || '',
|
||||
includeUnit ? goods.quantityUnit || '' : '',
|
||||
].join('\u0000');
|
||||
},
|
||||
masterGoodsIndex(goods = {}) {
|
||||
if (goods.sourceIndex !== undefined && goods.sourceIndex !== null && goods.sourceIndex !== '') {
|
||||
return Number(goods.sourceIndex);
|
||||
}
|
||||
return (this.master?.goods || []).findIndex(item =>
|
||||
item.cargoName === goods.cargoName &&
|
||||
item.cargoType === goods.cargoType &&
|
||||
(!goods.quantityUnit || !item.quantityUnit || item.quantityUnit === goods.quantityUnit)
|
||||
);
|
||||
},
|
||||
isSameGoods(left = {}, right = {}) {
|
||||
const leftIndex = this.masterGoodsIndex(left);
|
||||
const rightIndex = this.masterGoodsIndex(right);
|
||||
if (leftIndex >= 0 && rightIndex >= 0) return leftIndex === rightIndex;
|
||||
return left.cargoName === right.cargoName && left.cargoType === right.cargoType && left.quantityUnit === right.quantityUnit;
|
||||
},
|
||||
dispatchedGoodsQuantity(route, row) {
|
||||
const dispatchedGoods = route.dispatchedGoods || {};
|
||||
return this.quantityNumber(
|
||||
dispatchedGoods[this.goodsKey(row)] ?? dispatchedGoods[this.goodsKey(row, false)]
|
||||
);
|
||||
},
|
||||
baseGoodsRemainingQuantity(route, row) {
|
||||
return Math.max(0, this.quantityNumber(row.quantity) - this.dispatchedGoodsQuantity(route, row));
|
||||
},
|
||||
pendingSegmentQuantity(route) {
|
||||
return this.pending.reduce((sum, item) => {
|
||||
if (!this.isSameRouteSegment(route, item)) return sum;
|
||||
return sum + this.quantityNumber(item.quantity);
|
||||
}, 0);
|
||||
},
|
||||
pendingGoodsQuantity(route, row) {
|
||||
const goodsIndex = this.masterGoodsIndex(row);
|
||||
return this.pending.reduce((sum, item) => {
|
||||
if (!this.isSameRouteSegment(route, item)) return sum;
|
||||
const pendingGoodsIndex = this.masterGoodsIndex(item);
|
||||
if (
|
||||
(goodsIndex >= 0 && pendingGoodsIndex >= 0 && pendingGoodsIndex !== goodsIndex) ||
|
||||
(goodsIndex < 0 && !this.isSameGoods(item, row))
|
||||
) {
|
||||
return sum;
|
||||
}
|
||||
return sum + this.quantityNumber(item.quantity);
|
||||
}, 0);
|
||||
},
|
||||
isSameRouteSegment(route = {}, item = {}) {
|
||||
const routeSegment = route.segmentNo || route.relationNo || '';
|
||||
const itemSegment = item.segmentNo || item.relationNo || '';
|
||||
return String(routeSegment) === String(itemSegment);
|
||||
},
|
||||
editingGoodsQuantity(route, row) {
|
||||
return (route.goods || []).reduce((sum, item) => {
|
||||
if (item === row || !this.isSameGoods(item, row)) return sum;
|
||||
return sum + this.quantityNumber(item.dispatchQuantity);
|
||||
}, 0);
|
||||
},
|
||||
goodsRemainingQuantity(route, row) {
|
||||
return Math.max(
|
||||
0,
|
||||
this.baseGoodsRemainingQuantity(route, row) -
|
||||
this.pendingGoodsQuantity(route, row) -
|
||||
this.editingGoodsQuantity(route, row)
|
||||
);
|
||||
},
|
||||
availableDispatchQuantity(route, row) { return this.goodsRemainingQuantity(route, row); },
|
||||
handleDispatchQuantityInput(route, row, value) {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
route.unitPrice = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||
const nextValue = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 3)}` : integer;
|
||||
const availableQuantity = this.availableDispatchQuantity(route, row);
|
||||
if (Number(nextValue || 0) > availableQuantity) {
|
||||
row.dispatchQuantity = this.formatQuantity(availableQuantity);
|
||||
this.syncFreightItems(route);
|
||||
this.$message.warning('本次数量不能超过剩余数量');
|
||||
return;
|
||||
}
|
||||
row.dispatchQuantity = nextValue;
|
||||
this.syncFreightItems(route);
|
||||
},
|
||||
handleFreightUnitPriceInput(item, value) {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
item.unitPrice = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||
},
|
||||
handleOtherFeeTotalInput(route, value) {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
route.otherFeeTotal = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||
},
|
||||
validateFreightItems(route) {
|
||||
for (const [index, item] of (route.freightItems || []).entries()) {
|
||||
const fields = [
|
||||
['unitPrice', '单价'],
|
||||
['priceUnit', '单价单位'],
|
||||
['quantity', '数量'],
|
||||
['quantityUnit', '数量单位'],
|
||||
];
|
||||
const emptyField = fields.find(
|
||||
([prop]) => item[prop] === undefined || item[prop] === null || item[prop] === ''
|
||||
);
|
||||
if (emptyField) {
|
||||
this.$message.warning(`第${index + 1}条货物的${emptyField[1]}不能为空`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
handleFreightQuantityUnitChange(route, freightItem, value) {
|
||||
const goods = (route.goods || []).find(item => String(item.sourceIndex) === String(freightItem.sourceIndex));
|
||||
if (goods) goods.quantityUnit = value;
|
||||
},
|
||||
handleMileageInput(route, value) {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
route.mileage = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||
route.mileage = String(value || '').replace(/\D/g, '').slice(0, 10);
|
||||
},
|
||||
normalizeMileage(value) {
|
||||
if (value === undefined || value === null || value === '') return '';
|
||||
const mileage = Number(value);
|
||||
return Number.isFinite(mileage) && mileage >= 0 ? String(Math.trunc(mileage)) : '';
|
||||
},
|
||||
addressText(name, address) { return [name, address].filter(Boolean).join(' / ') || '-'; },
|
||||
contactText(name, phone) { return [name, phone].filter(Boolean).join(' - ') || '-'; },
|
||||
dateStartLabel(route) { return route.documentType === '计划单' ? '计划开始日期' : '预计发货日期'; },
|
||||
dateEndLabel(route) { return route.documentType === '计划单' ? '计划结束日期' : '预计完成日期'; },
|
||||
createGoodsRow(goods = {}, sourceIndex, dispatchedGoods = {}) {
|
||||
createGoodsRow(goods = {}, sourceIndex) {
|
||||
return {
|
||||
...goods,
|
||||
sourceIndex,
|
||||
remainingQuantity: Math.max(0, Number(goods.quantity || 0) - Number(dispatchedGoods[`${goods.cargoName || ''}\u0000${goods.cargoType || ''}`] || 0)),
|
||||
cargoTypePath: this.findCargoTypePath(goods.cargoType),
|
||||
dispatchQuantity: undefined,
|
||||
};
|
||||
},
|
||||
addGoods(route) { route.goods.push(this.createGoodsRow()); },
|
||||
addGoods(route) {
|
||||
const firstGoods = this.masterGoods[0];
|
||||
route.goods.push(
|
||||
firstGoods
|
||||
? this.createGoodsRow(firstGoods, firstGoods.sourceIndex)
|
||||
: this.createGoodsRow()
|
||||
);
|
||||
this.syncFreightItems(route);
|
||||
},
|
||||
selectGoods(route, row) {
|
||||
const source = (this.master.goods || [])[Number(row.sourceIndex)];
|
||||
if (!source) return;
|
||||
Object.assign(row, this.createGoodsRow(source, Number(row.sourceIndex), route.dispatchedGoods || {}));
|
||||
Object.assign(row, this.createGoodsRow(source, Number(row.sourceIndex)));
|
||||
this.syncFreightItems(route);
|
||||
},
|
||||
removeGoods(route, index) { route.goods.splice(index, 1); this.syncFreightItems(route); },
|
||||
syncFreightItems(route) {
|
||||
const oldItems = route.freightItems || [];
|
||||
route.freightItems = (route.goods || [])
|
||||
.filter(item => item.sourceIndex !== undefined && item.sourceIndex !== '')
|
||||
.map(item => {
|
||||
const old = oldItems.find(entry => String(entry.sourceIndex) === String(item.sourceIndex)) || {};
|
||||
return {
|
||||
...old,
|
||||
sourceIndex: item.sourceIndex,
|
||||
cargoName: item.cargoName || '',
|
||||
quantity: item.dispatchQuantity || '',
|
||||
quantityUnit: item.quantityUnit || '',
|
||||
unitPrice: old.unitPrice || '',
|
||||
priceUnit: old.priceUnit || `元/${item.quantityUnit || '吨'}`,
|
||||
};
|
||||
});
|
||||
},
|
||||
removeGoods(route, index) { route.goods.splice(index, 1); },
|
||||
handleCarrierTypeChange(route, value) {
|
||||
route.carrierType = value;
|
||||
if (value === '承运商') {
|
||||
@@ -294,11 +541,14 @@ export default {
|
||||
addToPending(route, continueDispatch) {
|
||||
const goods = route.goods.filter(item => Number(item.dispatchQuantity) > 0);
|
||||
if (!goods.length) return this.$message.warning('请填写本次数量');
|
||||
if (goods.some(item => Number(item.dispatchQuantity) > Number(item.remainingQuantity))) return this.$message.warning('本次数量不能超过该货物的可调度数量');
|
||||
if (goods.some(item => Number(item.dispatchQuantity) > this.availableDispatchQuantity(route, item))) return this.$message.warning('本次数量不能超过剩余数量');
|
||||
this.syncFreightItems(route);
|
||||
if (!this.validateFreightItems(route)) return;
|
||||
if (!route.estimatedStartTime) return this.$message.warning(`请选择${this.dateStartLabel(route)}`);
|
||||
if (!route.estimatedEndTime) return this.$message.warning(`请选择${this.dateEndLabel(route)}`);
|
||||
if (!this.validateRoutePhones(route)) return;
|
||||
if (route.documentType === '运单') {
|
||||
if (route.carrierType === '承运商' && (!route.carrierName || !route.vehicleNo)) return this.$message.warning('请填写承运商和车牌号');
|
||||
if (route.carrierType === '承运商' && (!route.carrierName || !route.vehicleNo || !route.mileage)) return this.$message.warning('请填写承运商、车牌号和里程');
|
||||
if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo || !route.trailerVehicleNo || !route.escortName || !route.escortPhone || route.mileage === undefined || route.mileage === null || route.mileage === '')) return this.$message.warning('请补全自运或网货平台的车辆与人员信息');
|
||||
}
|
||||
const batchNo = `${route.segmentNo}-${Date.now()}`;
|
||||
@@ -306,11 +556,11 @@ export default {
|
||||
id: `${route.segmentNo}-${item.cargoName}-${Date.now()}-${Math.random()}`,
|
||||
batchNo,
|
||||
segmentNo: route.segmentNo, relationNo: route.segmentNo, documentType: route.documentType, transportType: route.transportType, carrierType: route.carrierType,
|
||||
carrierName: route.carrierName, driverName: route.driverName, driverPhone: route.driverPhone, vehicleNo: route.vehicleNo, trailerVehicleNo: route.trailerVehicleNo, escortName: route.escortName, escortPhone: route.escortPhone, mileage: route.mileage, unitPrice: route.unitPrice, priceUnit: route.priceUnit, remark: route.remark,
|
||||
carrierName: route.carrierName, driverName: route.driverName, driverPhone: route.driverPhone, vehicleNo: route.vehicleNo, trailerVehicleNo: route.trailerVehicleNo, escortName: route.escortName, escortPhone: route.escortPhone, mileage: this.normalizeMileage(route.mileage), unitPrice: this.freightItemsForGoods(route, item).unitPrice || '', priceUnit: this.freightItemsForGoods(route, item).priceUnit || '', currency: route.currency || 'CNY', freightAmount: this.freightAmount(this.freightItemsForGoods(route, item)), freightTotal: this.freightTotal(route), otherFeeTotal: route.otherFeeTotal || '', freightJson: this.buildFreightJson(route), remark: route.remark,
|
||||
departureName: route.departureName, departureAddress: route.departureAddress, departureContact: route.departureContact, departurePhone: route.departurePhone,
|
||||
arrivalName: route.arrivalName, arrivalAddress: route.arrivalAddress, arrivalContact: route.arrivalContact, arrivalPhone: route.arrivalPhone,
|
||||
estimatedStartTime: route.estimatedStartTime, estimatedEndTime: route.estimatedEndTime,
|
||||
cargoName: item.cargoName, cargoType: item.cargoType, quantity: item.dispatchQuantity, quantityUnit: item.quantityUnit, packageType: item.packageType, brand: item.brand, specification: item.specification, model: item.model, materialCode: item.materialCode,
|
||||
sourceIndex: this.masterGoodsIndex(item), cargoName: item.cargoName, cargoType: item.cargoType, quantity: item.dispatchQuantity, quantityUnit: item.quantityUnit, packageType: item.packageType, brand: item.brand, specification: item.specification, model: item.model, materialCode: item.materialCode,
|
||||
}));
|
||||
this.editingId = null;
|
||||
this.pendingExpanded = true;
|
||||
@@ -327,20 +577,38 @@ export default {
|
||||
const sourceIndex = (this.master.goods || []).findIndex(goods => goods.cargoName === item.cargoName && goods.cargoType === item.cargoType);
|
||||
let goods = route.goods.find(goodsItem => goodsItem.cargoName === item.cargoName && goodsItem.cargoType === item.cargoType);
|
||||
if (!goods && sourceIndex >= 0) {
|
||||
goods = this.createGoodsRow(this.master.goods[sourceIndex], sourceIndex, route.dispatchedGoods || {});
|
||||
goods = this.createGoodsRow(this.master.goods[sourceIndex], sourceIndex);
|
||||
route.goods.push(goods);
|
||||
}
|
||||
if (goods) goods.dispatchQuantity = item.quantity;
|
||||
Object.assign(route, item);
|
||||
this.syncFreightItems(route);
|
||||
const freightItem = this.freightItemsForGoods(route, goods);
|
||||
if (freightItem) { freightItem.unitPrice = item.unitPrice || ''; freightItem.priceUnit = item.priceUnit || freightItem.priceUnit; }
|
||||
this.editingId = item.id;
|
||||
this.removePending(item.id);
|
||||
this.$nextTick(() => document.querySelector(`[data-segment="${item.segmentNo}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
|
||||
},
|
||||
freightItemsForGoods(route, goods) {
|
||||
return (route.freightItems || []).find(item => String(item.sourceIndex) === String(goods?.sourceIndex)) || {};
|
||||
},
|
||||
removePending(id) { this.pending = this.pending.filter(item => item.id !== id); if (this.editingId === id) this.editingId = null; },
|
||||
validateRoutePhones(route = {}) {
|
||||
const invalidPhone = [
|
||||
[route.departurePhone, '发货联系方式'],
|
||||
[route.arrivalPhone, '收货联系方式'],
|
||||
[route.driverPhone, '手机号'],
|
||||
[route.escortPhone, '押运人手机号'],
|
||||
].find(([value]) => String(value || '').trim() && !isMobile(value));
|
||||
if (!invalidPhone) return true;
|
||||
this.$message.warning(`${invalidPhone[1]}格式不正确`);
|
||||
return false;
|
||||
},
|
||||
async submit() {
|
||||
if (!this.pending.length) return this.$message.warning('请加入待提交调度清单');
|
||||
if (this.pending.some(item => !this.validateRoutePhones(item))) return;
|
||||
this.submitting = true;
|
||||
try { await api.dispatch({ id: this.master.id, dispatches: this.pending.map(({ id, ...item }) => item) }); this.$message.success('调度成功'); this.$emit('back'); } finally { this.submitting = false; }
|
||||
try { await api.dispatch({ id: this.master.id, dispatches: this.pending.map(({ id, ...item }) => ({ ...item, mileage: this.normalizeMileage(item.mileage) })) }); this.$message.success('调度成功'); this.$emit('back'); } finally { this.submitting = false; }
|
||||
},
|
||||
statusName(value) { return ({ waiting_dispatch: '待调度', dispatching: '调度中', completed: '调度完成', closed: '调度关闭' })[value] || value || '-'; },
|
||||
statusType(value) { return ({ waiting_dispatch: 'warning', dispatching: 'success', completed: 'primary', closed: 'danger' })[value] || 'info'; },
|
||||
@@ -350,6 +618,7 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.master-dispatch { min-height: 100%; padding-bottom: 124px; background: #fff; color: #303133; }
|
||||
.pending-list-spacer { height: min(436px, calc(100vh - 130px)); }
|
||||
.master-overview, .dispatch-section { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
||||
.overview-heading, .segment-header, .pending-bar { display: flex; align-items: center; justify-content: space-between; }
|
||||
.overview-heading { padding: 18px 24px 12px; h2 { display: inline-block; margin: 0 16px 0 0; font-size: 20px; } h2 span { margin: 0 8px; color: #909399; font-weight: 400; } }
|
||||
@@ -365,7 +634,8 @@ export default {
|
||||
.transport-type-field :deep(.el-input) { width: 240px; }
|
||||
.goods-heading { display: flex; align-items: center; justify-content: space-between; margin: 0 -24px 12px; padding: 14px 24px; border-top: 1px solid #eff1f7; border-bottom: 1px solid #eff1f7; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } span { color: #909399; font-size: 13px; } }
|
||||
.freight-heading { margin: 16px 0 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
||||
.goods-table { :deep(th.el-table__cell), :deep(td.el-table__cell) { border-color: #eff1f7; } :deep(.el-table__row--striped td.el-table__cell) { background: #fafafa; } }
|
||||
.goods-table { :deep(th.el-table__cell), :deep(td.el-table__cell) { border-color: #eff1f7; } :deep(.el-table__row--striped td.el-table__cell) { background: #fafafa; } :deep(.goods-table__remaining .cell) { white-space: nowrap; } :deep(.goods-table__dispatch .el-input) { width: 100%; min-width: 0; } }
|
||||
.freight-form { :deep(.freight-unit-select) { width: 92px; } }
|
||||
.carrier-type-form { margin-top: 16px; }
|
||||
.carrier-form { padding-top: 8px; }
|
||||
.segment-actions { display: flex; justify-content: flex-end; gap: 12px; padding-top: 4px; }
|
||||
|
||||
@@ -49,13 +49,16 @@
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<h3>收发货信息</h3>
|
||||
<el-link type="primary" @click="openRouteDialog">选择线路</el-link>
|
||||
<div class="section-head__actions">
|
||||
<el-link type="primary" @click="addRoute">新增途经点</el-link>
|
||||
<el-link type="primary" @click="openRouteDialog">选择线路</el-link>
|
||||
</div>
|
||||
</div>
|
||||
<el-form :model="form" label-position="right" label-width="auto"
|
||||
><div class="route-steps">
|
||||
<el-steps direction="vertical" :active="form.routes.length + 1">
|
||||
<el-step>
|
||||
<template #icon><span class="route-step-icon">起</span></template>
|
||||
<template #icon><span class="route-step-icon route-step-icon--start">起</span></template>
|
||||
<template #description>
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="发货地址" required class="route-address-form-item"
|
||||
@@ -80,27 +83,27 @@
|
||||
</template>
|
||||
</el-step>
|
||||
<el-step v-for="(route, index) in form.routes" :key="index">
|
||||
<template #icon><span class="route-step-icon">经</span></template>
|
||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ index + 1 }}</span><span class="route-title-path">{{ getRouteTitle(index).replace(`段${index + 1}:`, '') }}</span><div class="route-transport-control"><span>运输类型</span><el-select v-model="route.transportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
||||
<template #icon><span class="route-step-icon route-step-icon--middle">经</span></template>
|
||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ index + 1 }}</span><span class="route-title-path">{{ getRouteTitle(index).replace(`段${index + 1}:`, '') }}</span><div class="route-transport-control"><span>运输方式</span><el-select v-model="route.transportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
||||
<template #description>
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="途经地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-model="route.departureName" class="route-address-name" readonly :disabled="!route.transportType" placeholder="请先选择运输类型" @click="openAddressMap(`route-${index}`)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType" placeholder="请先选择运输类型" @click="openAddressMap(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
><el-input v-model="route.departureName" class="route-address-name" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" @click="openAddressMap(`route-${index}`)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" @click="openAddressMap(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item>
|
||||
<el-form-item label="联系方式"><div class="route-contact-actions"><el-input v-model="route.departurePhone" placeholder="请输入" /><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip v-if="form.routes.length > 1" content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></div></el-form-item>
|
||||
<el-form-item label="联系方式"><div class="route-contact-actions"><el-input v-model="route.departurePhone" placeholder="请输入" /><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></div></el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
</el-step>
|
||||
<el-step>
|
||||
<template #icon><span class="route-step-icon">终</span></template>
|
||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ form.routes.length + 1 }}</span><span class="route-title-path">{{ getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '') }}</span><div class="route-transport-control"><span>运输类型</span><el-select v-model="form.finalTransportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
||||
<template #icon><span class="route-step-icon route-step-icon--end">终</span></template>
|
||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ form.routes.length + 1 }}</span><span class="route-title-path">{{ getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '') }}</span><div class="route-transport-control"><span>运输方式</span><el-select v-model="form.finalTransportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
||||
<template #description>
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="收货地址" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-model="form.arrivalName" class="route-address-name" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输类型" @click="openAddressMap('arrival')" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输类型" @click="openAddressMap('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
><el-input v-model="form.arrivalName" class="route-address-name" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" @click="openAddressMap('arrival')" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" @click="openAddressMap('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人"
|
||||
><el-input v-model="form.arrivalContact" placeholder="请输入" /></el-form-item>
|
||||
@@ -125,8 +128,8 @@
|
||||
<div class="goods-heading">
|
||||
<h3>货物信息</h3>
|
||||
<div class="goods-toolbar">
|
||||
<el-link type="primary" @click="cargoImportVisible = true">导入货物</el-link>
|
||||
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
|
||||
<el-link type="primary" @click="cargoImportVisible = true">导入货物</el-link>
|
||||
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="form.goods" border
|
||||
@@ -137,30 +140,39 @@
|
||||
><el-select
|
||||
v-model="row.cargoName"
|
||||
filterable
|
||||
remote
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
placeholder="请输入并选择"
|
||||
:loading="cargoNameLoading"
|
||||
:remote-method="searchCargoOptions"
|
||||
@visible-change="loadCargoOptions"
|
||||
placeholder="请选择或输入"
|
||||
:loading="isCargoOptionsLoading(row.cargoTypePath)"
|
||||
@visible-change="visible => visible && loadCargoOptionsByType(row.cargoTypePath)"
|
||||
@change="selectCargoName(row, $event)"
|
||||
><el-option
|
||||
v-for="cargo in cargoOptions"
|
||||
v-for="cargo in getCargoOptionsByType(row.cargoTypePath)"
|
||||
:key="cargo.id"
|
||||
:label="cargo.cargoName"
|
||||
:value="cargo.cargoName" /></el-select
|
||||
></template></el-table-column
|
||||
><el-table-column label="货物类型" min-width="130"
|
||||
><template #default="{ row }"
|
||||
><el-input
|
||||
v-model="row.cargoType"
|
||||
placeholder="从货物类型获取" /></template></el-table-column
|
||||
><el-table-column label="数量" width="104" class-name="master-editor__quantity-cell"
|
||||
><el-cascader
|
||||
v-model="row.cargoTypePath"
|
||||
:options="cargoTypeOptions"
|
||||
:props="cargoTypeCascaderProps"
|
||||
:placeholder="row.cargoType || '请选择'"
|
||||
clearable
|
||||
filterable
|
||||
:loading="cargoTypeLoading"
|
||||
@visible-change="visible => visible && loadCargoTypeOptions()"
|
||||
@change="value => handleCargoTypeChange(row, value)"
|
||||
/></template></el-table-column
|
||||
><el-table-column label="数量" width="180" class-name="master-editor__quantity-cell"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
><el-input
|
||||
v-model="row.quantity"
|
||||
:min="0"
|
||||
controls-position="right" /></template></el-table-column
|
||||
inputmode="decimal"
|
||||
placeholder="请输入"
|
||||
@input="value => handleQuantityInput(row, value)" /></template></el-table-column
|
||||
><el-table-column label="数量单位" width="120"
|
||||
><template #default="{ row }"
|
||||
><el-select v-model="row.quantityUnit"
|
||||
@@ -200,14 +212,85 @@
|
||||
</section>
|
||||
<section>
|
||||
<h3>附件</h3>
|
||||
<el-upload
|
||||
multiple
|
||||
action="/api/blade-resource/oss/endpoint/put-file"
|
||||
:headers="uploadHeaders"
|
||||
:on-success="attach"
|
||||
><el-button type="primary" plain>上传附件</el-button></el-upload
|
||||
>
|
||||
<div class="master-editor__attachment">
|
||||
<div class="master-editor__attachment-head">
|
||||
<vehicle-attachment-upload
|
||||
v-model="attachmentRows"
|
||||
:file-types="attachmentFileTypes"
|
||||
:max-size="500"
|
||||
:show-tip="false"
|
||||
:show-file-list="false"
|
||||
button-text="上传附件"
|
||||
@change="handleAttachmentChange"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!attachmentRows.length"
|
||||
@click="handleAttachmentBatchDownload"
|
||||
>
|
||||
批量下载
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
:data="attachmentRows"
|
||||
border
|
||||
class="master-editor__attachment-table"
|
||||
@selection-change="handleAttachmentSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="240" align="center" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件大小" width="120" align="center">
|
||||
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||||
<el-table-column prop="uploadTime" label="上传时间" width="170" align="center" sortable />
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ row, $index }">
|
||||
<el-link type="danger" @click="removeAttachment($index)">删除</el-link>
|
||||
<el-link type="primary" @click="downloadAttachment(row)">下载</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
<el-dialog
|
||||
v-model="attachmentDocumentPreviewVisible"
|
||||
:title="attachmentPreviewFile.name || '附件预览'"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
width="90%"
|
||||
top="4vh"
|
||||
class="master-editor__attachment-viewer-dialog"
|
||||
>
|
||||
<open-file-viewer
|
||||
v-if="attachmentDocumentPreviewVisible && attachmentPreviewFile.url"
|
||||
:file="attachmentPreviewFile.url"
|
||||
:file-name="attachmentPreviewFile.name"
|
||||
:mime-type="attachmentPreviewFile.mimeType"
|
||||
width="100%"
|
||||
height="72vh"
|
||||
fit="contain"
|
||||
theme="auto"
|
||||
locale="zh-CN"
|
||||
:toolbar="attachmentViewerToolbar"
|
||||
:plugins="attachmentViewerPlugins"
|
||||
@unsupported="handleAttachmentPreviewUnsupported"
|
||||
@error="handleAttachmentPreviewError"
|
||||
/>
|
||||
</el-dialog>
|
||||
<el-image-viewer
|
||||
v-if="attachmentImagePreviewVisible"
|
||||
:url-list="attachmentImagePreviewUrls"
|
||||
:initial-index="attachmentImagePreviewIndex"
|
||||
@close="attachmentImagePreviewVisible = false"
|
||||
/>
|
||||
<el-dialog
|
||||
v-model="addressDialogVisible"
|
||||
title="选择常用地址"
|
||||
@@ -400,20 +483,38 @@ import { getList as getCommonAddressList } from '@/api/base/common-address';
|
||||
import { getList as getAirportMasterList } from '@/api/base/airport-master';
|
||||
import { getList as getPortTerminalList } from '@/api/base/port-terminal';
|
||||
import { getList as getRailwayStationList } from '@/api/base/railway-station';
|
||||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||||
import { exportBlob, importBlob } from '@/api/common';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { getUploadHeaders } from '@/utils/upload';
|
||||
import { List, Location, Search } from '@element-plus/icons-vue';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { List, Search } from '@element-plus/icons-vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
||||
import '@open-file-viewer/core/style.css';
|
||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
const quantityUnitOptions = ['吨', '件', '方'];
|
||||
const attachmentViewerPlugins = [
|
||||
imagePlugin(),
|
||||
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
|
||||
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
|
||||
textPlugin(),
|
||||
fallbackPlugin(),
|
||||
];
|
||||
let amapLoader;
|
||||
import { getList as getCommonRouteList } from '@/api/business/common-route';
|
||||
import * as api from '@/api/business/master-order';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
export default {
|
||||
components: {
|
||||
ElImageViewer,
|
||||
OpenFileViewer,
|
||||
},
|
||||
props: { id: [String, Number] },
|
||||
emits: ['back', 'dispatch'],
|
||||
data() {
|
||||
@@ -451,11 +552,44 @@ export default {
|
||||
commonCargoLoading: false,
|
||||
commonCargoRows: [],
|
||||
commonCargoPage: { current: 1, size: 10, total: 0 },
|
||||
cargoNameLoading: false,
|
||||
cargoOptions: [],
|
||||
cargoTypeLoading: false,
|
||||
cargoTypeOptions: [],
|
||||
cargoTypeFlatOptions: [],
|
||||
cargoOptionsMap: {},
|
||||
cargoOptionsLoadingMap: {},
|
||||
cargoImportVisible: false,
|
||||
cargoImportLoading: false,
|
||||
uploadHeaders: getUploadHeaders(),
|
||||
attachmentRows: [],
|
||||
selectedAttachmentRows: [],
|
||||
attachmentImagePreviewVisible: false,
|
||||
attachmentImagePreviewUrls: [],
|
||||
attachmentImagePreviewIndex: 0,
|
||||
attachmentDocumentPreviewVisible: false,
|
||||
attachmentPreviewFile: {},
|
||||
attachmentViewerPlugins,
|
||||
attachmentViewerToolbar: {
|
||||
download: true,
|
||||
fullscreen: true,
|
||||
print: true,
|
||||
rotate: true,
|
||||
zoom: true,
|
||||
},
|
||||
attachmentFileTypes: [
|
||||
'pdf',
|
||||
'bmp',
|
||||
'jpeg',
|
||||
'png',
|
||||
'jpg',
|
||||
'doc',
|
||||
'docx',
|
||||
'ppt',
|
||||
'pptx',
|
||||
'xlsx',
|
||||
'xls',
|
||||
'eml',
|
||||
'msg',
|
||||
'zip',
|
||||
],
|
||||
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
|
||||
quantityUnitOptions,
|
||||
form: {
|
||||
@@ -471,6 +605,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo']),
|
||||
lastRouteName() {
|
||||
return this.form.routes[this.form.routes.length - 1]?.departureName || '途经地';
|
||||
},
|
||||
@@ -481,11 +616,19 @@ export default {
|
||||
air: '空港机场',
|
||||
}[this.addressTransportMode(this.addressTarget)] || '站点';
|
||||
},
|
||||
cargoTypeCascaderProps() {
|
||||
return {
|
||||
value: 'id',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
emitPath: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await this.loadProjects();
|
||||
async mounted() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await Promise.all([this.loadProjects(), this.loadCargoTypeOptions()]);
|
||||
if (this.id) {
|
||||
await this.loadDetail(this.id);
|
||||
}
|
||||
@@ -513,10 +656,24 @@ export default {
|
||||
? finalSegment.transportType
|
||||
: detail.finalTransportType || '',
|
||||
goods: detail.goods || [{ quantityUnit: quantityUnitOptions[0] }],
|
||||
attachmentsJson: detail.attachmentsJson || '[]',
|
||||
transportOrganizationType: '多式联运',
|
||||
};
|
||||
this.restoreGoodsCargoTypePaths();
|
||||
this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
|
||||
this.selectedAttachmentRows = [];
|
||||
if (this.form.projectId) await this.loadContracts(this.form.projectId, false);
|
||||
},
|
||||
parseJsonArray(value) {
|
||||
if (!value) return [];
|
||||
if (Array.isArray(value)) return value;
|
||||
try {
|
||||
const data = JSON.parse(value);
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
getRouteTitle(index) {
|
||||
const departureName =
|
||||
index === 0
|
||||
@@ -826,31 +983,117 @@ export default {
|
||||
this.commonCargoLoading = false;
|
||||
}
|
||||
},
|
||||
async loadCargoOptions(visible) {
|
||||
if (!visible || this.cargoOptions.length) return;
|
||||
await this.searchCargoOptions('');
|
||||
},
|
||||
async searchCargoOptions(keyword) {
|
||||
this.cargoNameLoading = true;
|
||||
async loadCargoTypeOptions() {
|
||||
if (this.cargoTypeOptions.length || this.cargoTypeLoading) return;
|
||||
this.cargoTypeLoading = true;
|
||||
try {
|
||||
const response = await getCommonCargoList(1, 50, {
|
||||
allDept: 0,
|
||||
cargoName: keyword || undefined,
|
||||
});
|
||||
const data = response?.data?.data || response?.data || response || {};
|
||||
this.cargoOptions = data.records || [];
|
||||
const response = await getCargoTypeList(1, 9999, { allDept: 0 });
|
||||
const rows = this.responseRecords(response);
|
||||
this.cargoTypeOptions = this.buildCargoTypeTree(rows);
|
||||
} finally {
|
||||
this.cargoNameLoading = false;
|
||||
this.cargoTypeLoading = false;
|
||||
}
|
||||
},
|
||||
buildCargoTypeTree(rows = []) {
|
||||
const nodes = new Map();
|
||||
rows.forEach(row => {
|
||||
const id = String(row.id);
|
||||
nodes.set(id, {
|
||||
...row,
|
||||
id,
|
||||
label: row.cargoName || row.cargoTypeName || row.name || row.cargoCode || '',
|
||||
children: [],
|
||||
});
|
||||
});
|
||||
const roots = [];
|
||||
nodes.forEach(node => {
|
||||
const parentId = node.parentId == null ? '' : String(node.parentId);
|
||||
if (parentId && parentId !== '0' && nodes.has(parentId)) {
|
||||
nodes.get(parentId).children.push(node);
|
||||
} else {
|
||||
roots.push(node);
|
||||
}
|
||||
});
|
||||
this.cargoTypeFlatOptions = [];
|
||||
const normalize = (node, path = []) => {
|
||||
const nextPath = [...path, node.id];
|
||||
const children = node.children.map(child => normalize(child, nextPath));
|
||||
const normalized = { ...node, children };
|
||||
this.cargoTypeFlatOptions.push({ ...normalized, path: nextPath });
|
||||
return normalized;
|
||||
};
|
||||
return roots.map(node => normalize(node));
|
||||
},
|
||||
getCargoTypeByPath(path = []) {
|
||||
const normalizedPath = Array.isArray(path) ? path.map(value => String(value)) : [];
|
||||
const id = normalizedPath.at(-1);
|
||||
return this.cargoTypeFlatOptions.find(item => item.id === id) || null;
|
||||
},
|
||||
getCargoOptionsKey(path = []) {
|
||||
const cargoType = this.getCargoTypeByPath(path);
|
||||
return String(cargoType?.cargoCode || cargoType?.code || cargoType?.id || '');
|
||||
},
|
||||
getCargoOptionsByType(path = []) {
|
||||
const key = this.getCargoOptionsKey(path);
|
||||
return key ? this.cargoOptionsMap[key] || [] : [];
|
||||
},
|
||||
isCargoOptionsLoading(path = []) {
|
||||
const key = this.getCargoOptionsKey(path);
|
||||
return key ? Boolean(this.cargoOptionsLoadingMap[key]) : false;
|
||||
},
|
||||
async loadCargoOptionsByType(path = []) {
|
||||
const cargoType = this.getCargoTypeByPath(path);
|
||||
const key = this.getCargoOptionsKey(path);
|
||||
if (!key || !cargoType) return [];
|
||||
if (this.cargoOptionsMap[key] || this.cargoOptionsLoadingMap[key]) {
|
||||
return this.cargoOptionsMap[key] || [];
|
||||
}
|
||||
this.cargoOptionsLoadingMap = { ...this.cargoOptionsLoadingMap, [key]: true };
|
||||
try {
|
||||
const response = await getCommonCargoList(1, 500, {
|
||||
allDept: 0,
|
||||
secondCargoTypeName: cargoType.label,
|
||||
secondCargoTypeCode: cargoType.cargoCode || cargoType.code || '',
|
||||
});
|
||||
const options = this.responseRecords(response);
|
||||
this.cargoOptionsMap = { ...this.cargoOptionsMap, [key]: options };
|
||||
return options;
|
||||
} finally {
|
||||
this.cargoOptionsLoadingMap = { ...this.cargoOptionsLoadingMap, [key]: false };
|
||||
}
|
||||
},
|
||||
selectCargoName(goods, cargoName) {
|
||||
if (!cargoName) return;
|
||||
const cargo = this.cargoOptions.find(item => item.cargoName === cargoName);
|
||||
const cargo = this.getCargoOptionsByType(goods.cargoTypePath).find(
|
||||
item => item.cargoName === cargoName
|
||||
);
|
||||
if (!cargo) return;
|
||||
Object.assign(goods, this.normalizeCargoRow(cargo));
|
||||
Object.assign(goods, this.normalizeCargoRow(cargo, goods.cargoTypePath));
|
||||
},
|
||||
handleCargoTypeChange(goods, value) {
|
||||
const path = Array.isArray(value) ? value.map(item => String(item)) : [];
|
||||
const cargoType = this.getCargoTypeByPath(path);
|
||||
goods.cargoTypePath = path;
|
||||
goods.cargoType = cargoType?.label || '';
|
||||
goods.cargoTypeCode = cargoType?.cargoCode || cargoType?.code || cargoType?.id || '';
|
||||
goods.cargoName = '';
|
||||
goods.specification = '';
|
||||
goods.model = '';
|
||||
this.loadCargoOptionsByType(path);
|
||||
},
|
||||
restoreGoodsCargoTypePaths() {
|
||||
this.form.goods.forEach(goods => {
|
||||
if (Array.isArray(goods.cargoTypePath) && goods.cargoTypePath.length) return;
|
||||
const cargoType = this.cargoTypeFlatOptions.find(
|
||||
item =>
|
||||
String(item.cargoCode || item.code || '') === String(goods.cargoTypeCode || '') ||
|
||||
item.label === goods.cargoType
|
||||
);
|
||||
if (cargoType) goods.cargoTypePath = cargoType.path;
|
||||
});
|
||||
},
|
||||
selectCommonCargo(row) {
|
||||
const cargo = this.normalizeCargoRow(row);
|
||||
const cargo = this.normalizeCargoRow(row, this.resolveCargoTypePath(row));
|
||||
const firstGoods = this.form.goods[0];
|
||||
const isFirstGoodsEmpty =
|
||||
firstGoods &&
|
||||
@@ -872,12 +1115,35 @@ export default {
|
||||
}
|
||||
this.commonCargoVisible = false;
|
||||
},
|
||||
normalizeCargoRow(row = {}) {
|
||||
resolveCargoTypePath(row = {}) {
|
||||
return (
|
||||
this.cargoTypeFlatOptions.find(
|
||||
item =>
|
||||
String(item.cargoCode || item.code || '') ===
|
||||
String(row.secondCargoTypeCode || row.cargoTypeCode || '') ||
|
||||
item.label === (row.secondCargoTypeName || row.cargoType || row.goodsType)
|
||||
)?.path || []
|
||||
);
|
||||
},
|
||||
normalizeCargoRow(row = {}, cargoTypePath = this.resolveCargoTypePath(row)) {
|
||||
const cargoType = this.getCargoTypeByPath(cargoTypePath);
|
||||
return {
|
||||
...row,
|
||||
cargoName: row.cargoName || row.goodsName || '',
|
||||
cargoType:
|
||||
row.cargoType || row.goodsType || row.secondCargoTypeName || row.firstCargoTypeName || '',
|
||||
cargoType?.label ||
|
||||
row.cargoType ||
|
||||
row.goodsType ||
|
||||
row.secondCargoTypeName ||
|
||||
row.firstCargoTypeName ||
|
||||
'',
|
||||
cargoTypeCode:
|
||||
cargoType?.cargoCode ||
|
||||
cargoType?.code ||
|
||||
row.cargoTypeCode ||
|
||||
row.secondCargoTypeCode ||
|
||||
'',
|
||||
cargoTypePath,
|
||||
quantity: row.quantity || '',
|
||||
quantityUnit: row.quantityUnit || quantityUnitOptions[0],
|
||||
packageType: row.packageType || row.package || '',
|
||||
@@ -980,7 +1246,7 @@ export default {
|
||||
this.form.routes.push(this.createRoute());
|
||||
},
|
||||
removeRoute(index) {
|
||||
if (this.form.routes.length > 1) this.form.routes.splice(index, 1);
|
||||
this.form.routes.splice(index, 1);
|
||||
},
|
||||
addGoods(index) {
|
||||
this.form.goods.splice(index + 1, 0, { quantityUnit: quantityUnitOptions[0] });
|
||||
@@ -988,12 +1254,114 @@ export default {
|
||||
removeGoods(index) {
|
||||
if (this.form.goods.length > 1) this.form.goods.splice(index, 1);
|
||||
},
|
||||
attach(response) {
|
||||
const list = JSON.parse(this.form.attachmentsJson || '[]');
|
||||
list.push(response.data || response);
|
||||
this.form.attachmentsJson = JSON.stringify(list);
|
||||
handleQuantityInput(row, value) {
|
||||
const text = String(value || '').replace(/[^\d.]/g, '');
|
||||
const [integer = '', ...decimals] = text.split('.');
|
||||
row.quantity = decimals.length ? `${integer}.${decimals.join('').slice(0, 3)}` : integer;
|
||||
},
|
||||
syncAttachmentsJson() {
|
||||
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
|
||||
},
|
||||
handleAttachmentChange(list) {
|
||||
const userName = this.userInfo?.realName || this.userInfo?.userName || '';
|
||||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
this.attachmentRows = (list || []).map(item => ({
|
||||
...item,
|
||||
description: item.description || '',
|
||||
uploadUserName: item.uploadUserName || userName,
|
||||
uploadTime: item.uploadTime || uploadTime,
|
||||
}));
|
||||
this.selectedAttachmentRows = [];
|
||||
this.syncAttachmentsJson();
|
||||
},
|
||||
handleAttachmentSelectionChange(rows) {
|
||||
this.selectedAttachmentRows = rows || [];
|
||||
},
|
||||
removeAttachment(index) {
|
||||
this.attachmentRows.splice(index, 1);
|
||||
this.selectedAttachmentRows = this.selectedAttachmentRows.filter(item =>
|
||||
this.attachmentRows.includes(item)
|
||||
);
|
||||
this.syncAttachmentsJson();
|
||||
},
|
||||
formatFileSize(size) {
|
||||
const number = Number(size);
|
||||
if (!number) return '';
|
||||
if (number < 1024) return `${number}B`;
|
||||
if (number < 1024 * 1024) return `${(number / 1024).toFixed(1)}KB`;
|
||||
return `${(number / 1024 / 1024).toFixed(1)}MB`;
|
||||
},
|
||||
attachmentUrl(row = {}) {
|
||||
return row.url || row.link || row.fileUrl || row.domain || '';
|
||||
},
|
||||
attachmentName(row = {}) {
|
||||
return row.originalName || row.name || row.fileName || '附件';
|
||||
},
|
||||
attachmentExtension(row = {}) {
|
||||
const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0];
|
||||
const index = source.lastIndexOf('.');
|
||||
return index > -1 ? source.slice(index + 1).toLowerCase() : '';
|
||||
},
|
||||
isAttachmentImage(row) {
|
||||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(this.attachmentExtension(row));
|
||||
},
|
||||
previewAttachment(row) {
|
||||
const url = this.attachmentUrl(row);
|
||||
if (!url) {
|
||||
this.$message.warning('附件地址为空,无法预览');
|
||||
return;
|
||||
}
|
||||
if (this.isAttachmentImage(row)) {
|
||||
this.attachmentImagePreviewUrls = this.attachmentRows
|
||||
.filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item))
|
||||
.map(item => this.attachmentUrl(item));
|
||||
this.attachmentImagePreviewIndex = Math.max(this.attachmentImagePreviewUrls.indexOf(url), 0);
|
||||
this.attachmentImagePreviewVisible = true;
|
||||
return;
|
||||
}
|
||||
this.attachmentPreviewFile = {
|
||||
name: this.attachmentName(row),
|
||||
url,
|
||||
mimeType: row.mimeType || row.contentType || '',
|
||||
};
|
||||
this.attachmentDocumentPreviewVisible = true;
|
||||
},
|
||||
handleAttachmentPreviewUnsupported() {
|
||||
this.$message.warning('当前文件暂不支持在线预览');
|
||||
},
|
||||
handleAttachmentPreviewError() {
|
||||
this.$message.error('附件预览失败');
|
||||
},
|
||||
downloadAttachment(row) {
|
||||
const url = this.attachmentUrl(row);
|
||||
if (!url) {
|
||||
this.$message.warning('附件地址为空');
|
||||
return;
|
||||
}
|
||||
downloadFileByUrl(url, this.attachmentName(row));
|
||||
},
|
||||
handleAttachmentBatchDownload() {
|
||||
const rows = this.selectedAttachmentRows.length
|
||||
? this.selectedAttachmentRows
|
||||
: this.attachmentRows;
|
||||
rows.forEach(row => this.downloadAttachment(row));
|
||||
},
|
||||
async save(draft = false) {
|
||||
const phoneFields = [
|
||||
[this.form.departurePhone, '发货联系方式'],
|
||||
...this.form.routes.map((route, index) => [
|
||||
route.departurePhone,
|
||||
`第${index + 1}个途经地联系方式`,
|
||||
]),
|
||||
[this.form.arrivalPhone, '收货联系方式'],
|
||||
];
|
||||
const invalidPhone = phoneFields.find(
|
||||
([value]) => String(value || '').trim() && !isMobile(value)
|
||||
);
|
||||
if (invalidPhone) {
|
||||
this.$message.warning(`${invalidPhone[1]}格式不正确`);
|
||||
return false;
|
||||
}
|
||||
if (!draft) {
|
||||
await this.$refs.form.validate();
|
||||
if (!this.form.departureAddress || !this.form.arrivalAddress)
|
||||
@@ -1001,14 +1369,14 @@ export default {
|
||||
if (!this.form.goods.some(item => item.cargoType && item.quantity > 0 && item.quantityUnit))
|
||||
return this.$message.warning('请至少填写一条有效货物');
|
||||
if (
|
||||
!this.form.routes.length ||
|
||||
this.form.routes.some(item => !item.departureName || !item.transportType) ||
|
||||
!this.form.finalTransportType
|
||||
)
|
||||
return this.$message.warning('请完整填写途经地及运输类型');
|
||||
return this.$message.warning('请完整填写途经地及运输方式');
|
||||
}
|
||||
const payload = {
|
||||
...this.form,
|
||||
attachmentsJson: JSON.stringify(this.attachmentRows),
|
||||
routes: [
|
||||
...this.form.routes.map((route, index) => ({ ...route, segmentNo: `段${index + 1}` })),
|
||||
{
|
||||
@@ -1024,26 +1392,35 @@ export default {
|
||||
delete payload.finalTransportType;
|
||||
const res = await (draft ? api.saveDraft(payload) : api.submit(payload));
|
||||
this.$message.success('保存成功');
|
||||
return res.data || res;
|
||||
// 兼容 Axios 响应及 BladeX 常见的 data.data 两层响应结构。
|
||||
return res?.data?.data || res?.data || res;
|
||||
},
|
||||
async saveDraft() {
|
||||
await this.save(true);
|
||||
const result = await this.save(true);
|
||||
if (result === false) return;
|
||||
this.$emit('back');
|
||||
},
|
||||
async confirmCreate() {
|
||||
await this.save();
|
||||
const result = await this.save();
|
||||
if (result === false) return;
|
||||
this.$emit('back');
|
||||
},
|
||||
async createAndDispatch() {
|
||||
const data = await this.save();
|
||||
this.$emit('dispatch', data.id);
|
||||
if (data === false) return;
|
||||
const id = data?.id || data?.masterOrderId || data?.masterId;
|
||||
if (!id) {
|
||||
this.$message.error('保存成功但未获取到总单ID,无法进入调度');
|
||||
return;
|
||||
}
|
||||
this.$emit('dispatch', id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.master-editor section {
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid #eff1f7;
|
||||
background: #fff;
|
||||
@@ -1054,22 +1431,21 @@ export default {
|
||||
font-size: 16px;
|
||||
}
|
||||
.el-form-item {
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
.goods-heading {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
h3 {
|
||||
margin-right: auto;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.goods-toolbar {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
gap: 8px;
|
||||
}
|
||||
.goods-actions {
|
||||
@@ -1077,18 +1453,45 @@ export default {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
:deep(.master-editor__quantity-cell .el-input-number) {
|
||||
width: 80px;
|
||||
.master-editor__attachment {
|
||||
width: 100%;
|
||||
}
|
||||
.master-editor__attachment-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.master-editor__attachment-table {
|
||||
width: 100%;
|
||||
:deep(.el-table__header th) {
|
||||
background: #f5f7fa;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
:deep(.el-table__cell) {
|
||||
border-color: #eff1f7;
|
||||
}
|
||||
:deep(.el-table__row:nth-child(even) td.el-table__cell) {
|
||||
background: #fafafa;
|
||||
}
|
||||
}
|
||||
:deep(.master-editor__quantity-cell .el-input) {
|
||||
width: 156px;
|
||||
}
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 16px;
|
||||
h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.section-head__actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
.route-dialog__search {
|
||||
margin-bottom: 8px;
|
||||
padding: 12px 12px 4px;
|
||||
@@ -1139,6 +1542,12 @@ export default {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
&--middle {
|
||||
background: #67c23a;
|
||||
}
|
||||
&--end {
|
||||
background: #e6a23c;
|
||||
}
|
||||
}
|
||||
}
|
||||
.route-node-form {
|
||||
|
||||
@@ -27,26 +27,31 @@
|
||||
|
||||
<el-dialog v-model="detailVisible" title="导入运单详情" width="90%" append-to-body>
|
||||
<el-form :inline="true" :model="detailQuery"><el-form-item label="车牌号/船号"><el-input v-model="detailQuery.vehicleNo" clearable /></el-form-item><el-form-item label="货物名称"><el-input v-model="detailQuery.cargoName" clearable /></el-form-item><el-button type="primary" @click="loadDetails">查询</el-button></el-form>
|
||||
<el-table :data="details" border><el-table-column type="index" label="序号" width="65" /><el-table-column prop="batchNo" label="运单批次号" min-width="140" /><el-table-column prop="originalNo" label="原始单号" min-width="130" /><el-table-column prop="vehicleNo" label="车牌号/航班号/船号/班列号" min-width="190" /><el-table-column prop="driverName" label="司机/船长" min-width="120" /><el-table-column prop="transportType" label="运输类型" width="110" /><el-table-column prop="cargoName" label="货物名称" width="130" /><el-table-column prop="cargoType" label="货物类型" width="120" /><el-table-column prop="quantity" label="重量" width="90" /><el-table-column prop="departureAddress" label="发货地址" min-width="220" show-overflow-tooltip /><el-table-column prop="departureContact" label="发货联系人" width="110" /><el-table-column prop="departurePhone" label="发货联系人电话" width="140" /><el-table-column prop="arrivalAddress" label="到货地址" min-width="220" show-overflow-tooltip /><el-table-column prop="arrivalContact" label="收货联系人" width="110" /><el-table-column prop="arrivalPhone" label="收货联系人电话" width="140" /><el-table-column prop="startDate" label="开始时间" width="160" sortable/><el-table-column prop="endDate" label="结束时间" width="160" sortable/><el-table-column prop="unitPrice" label="单价" width="90" /><el-table-column prop="freight" label="运费" width="100" /><el-table-column prop="otherFeeTotal" label="其他费用合计" width="120" /><el-table-column prop="freightTotal" label="运费合计" width="100" /><el-table-column prop="remark" label="备注" min-width="160" /><el-table-column label="操作" width="80">-</el-table-column></el-table>
|
||||
<el-table :data="details" border><el-table-column type="index" label="序号" width="65" /><el-table-column prop="batchNo" label="运单批次号" min-width="140" /><el-table-column prop="originalNo" label="原始单号" min-width="130" /><el-table-column prop="vehicleNo" label="车牌号/航班号/船号/班列号" min-width="190" /><el-table-column prop="driverName" label="司机/船长" min-width="120" /><el-table-column prop="transportType" label="运输方式" width="110" /><el-table-column prop="cargoName" label="货物名称" width="130" /><el-table-column prop="cargoType" label="货物类型" width="120" /><el-table-column prop="quantity" label="重量" width="90" /><el-table-column prop="departureAddress" label="发货地址" min-width="220" show-overflow-tooltip /><el-table-column prop="departureContact" label="发货联系人" width="110" /><el-table-column prop="departurePhone" label="发货联系人电话" width="140" /><el-table-column prop="arrivalAddress" label="到货地址" min-width="220" show-overflow-tooltip /><el-table-column prop="arrivalContact" label="收货联系人" width="110" /><el-table-column prop="arrivalPhone" label="收货联系人电话" width="140" /><el-table-column prop="startDate" label="开始时间" width="160" sortable/><el-table-column prop="endDate" label="结束时间" width="160" sortable/><el-table-column prop="unitPrice" label="单价" width="90" /><el-table-column prop="freight" label="运费" width="100" /><el-table-column prop="otherFeeTotal" label="其他费用合计" width="120" /><el-table-column prop="freightTotal" label="运费合计" width="100" /><el-table-column prop="remark" label="备注" min-width="160" /><el-table-column label="操作" width="80">-</el-table-column></el-table>
|
||||
<el-pagination v-model:current-page="detailPage.current" v-model:page-size="detailPage.size" layout="total, prev, pager, next, sizes" :page-sizes="[10, 20, 50]" :total="detailPage.total" @current-change="loadDetails" @size-change="loadDetails" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="createVisible" title="新建导入" width="85%" append-to-body destroy-on-close class="waybill-import-create dialog-form--carded">
|
||||
<section-card title="导入信息">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="calc(6em + 24px)">
|
||||
<el-row :gutter="18"><el-col :span="6"><el-form-item label="项目" prop="projectId"><el-select v-model="form.projectId" filterable clearable placeholder="模糊查询后选择" @change="projectChange"><el-option v-for="item in projects" :key="item.id" :label="item.projectName" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="客户" prop="customerName"><el-input v-model="form.customerName" disabled placeholder="自动带出" /></el-form-item></el-col><el-col :span="6"><el-form-item label="客户合同" prop="contractId"><el-select v-model="form.contractId" filterable clearable placeholder="自动带出"><el-option v-for="item in contracts" :key="item.id" :label="item.contractName" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="承运类型" prop="carrierType"><el-select v-model="form.carrierType"><el-option label="承运商" value="承运商" /><el-option label="自运" value="自运" /><el-option label="网货平台" value="网货平台" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="承运商"><el-select v-model="form.carrierIds" multiple filterable placeholder="自动带出,多个需选择"><el-option v-for="item in carriers" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="导入状态" prop="status"><el-select v-model="form.status"><el-option label="完成" value="completed" /><el-option label="进行中" value="processing" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="导入类型" prop="importType"><el-select v-model="form.importType"><el-option label="运单" value="waybill" /><el-option label="结算单" value="settlement" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="发货计划"><el-select v-model="form.planId" clearable filterable placeholder="请选择"><el-option v-for="item in plans" :key="item.id" :label="item.planName" :value="item.id" /></el-select></el-form-item></el-col></el-row>
|
||||
<el-dialog v-model="createVisible" title="新建导入" width="85%" append-to-body destroy-on-close class="waybill-import-create">
|
||||
<section class="waybill-import-create__form-panel">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="auto" class="archive-form">
|
||||
<el-row :gutter="20"><el-col :span="6"><el-form-item label="项目" prop="projectId"><el-select v-model="form.projectId" filterable clearable placeholder="模糊查询后选择" @change="projectChange"><el-option v-for="item in projects" :key="item.id" :label="item.projectName" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="客户" prop="customerName"><el-input v-model="form.customerName" disabled placeholder="自动带出" /></el-form-item></el-col><el-col :span="6"><el-form-item label="客户合同" prop="contractId"><el-select v-model="form.contractId" filterable clearable placeholder="自动带出" @change="contractChange"><el-option v-for="item in contracts" :key="item.id" :label="item.contractName" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="承运类型" prop="carrierType"><el-select v-model="form.carrierType"><el-option label="承运商" value="承运商" /><el-option label="自运" value="自运" /><el-option label="网货平台" value="网货平台" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="承运商"><el-select v-model="form.carrierIds" multiple filterable placeholder="自动带出,多个需选择"><el-option v-for="item in carriers" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="导入状态" prop="status"><el-select v-model="form.status"><el-option label="完成" value="completed" /><el-option label="进行中" value="processing" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="导入类型" prop="importType"><el-select v-model="form.importType"><el-option label="运单" value="waybill" /><el-option label="结算单" value="settlement" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="计划名称"><el-select v-model="form.planId" clearable filterable placeholder="请选择"><el-option v-for="item in plans" :key="item.id" :label="item.planName" :value="item.id" /></el-select></el-form-item></el-col></el-row>
|
||||
<el-form-item label="运单明细表" prop="file"><el-upload action="#" accept=".xls,.xlsx" :auto-upload="false" :limit="1" :file-list="files" :on-change="fileChange" :on-remove="fileRemove"><el-button type="primary">添加附件</el-button></el-upload><span class="waybill-import-create__file-tip">请上传运单明细表,仅支持 excel 格式</span><el-link type="primary" @click="downloadTemplate">下载模板</el-link></el-form-item>
|
||||
</el-form>
|
||||
<div class="waybill-import-create__form-actions"><el-button @click="createVisible = false">关闭</el-button><el-button @click="saveDraft">保存草稿</el-button><el-button type="primary" @click="confirmImport">确认导入</el-button></div>
|
||||
</section-card>
|
||||
<section class="waybill-import-create__detail-panel" v-if="rows.length"><div class="waybill-import-create__detail-head"><div class="dialog-section-title">导入数据明细</div><el-button type="danger" plain :disabled="!rowSelection.length" @click="removeSelectedRows">批量删除</el-button></div><el-tabs v-model="previewTab"><el-tab-pane :label="`运单数(${rows.length})`" name="all" /><el-tab-pane :label="`疑似重复(${duplicateRows.length})`" name="duplicate" /></el-tabs><el-table :data="previewRows" border max-height="440" @selection-change="rowSelection = $event"><el-table-column type="selection" width="55" /><el-table-column type="index" label="序号" width="65" /><el-table-column v-for="column in detailColumns" :key="column.prop" :prop="column.prop" :label="column.label" min-width="140" show-overflow-tooltip /><el-table-column label="操作" width="180" fixed="right"><template #default="{ row }"><el-link type="primary" @click="cancelRow(row)">取消</el-link><el-link type="primary" @click="saveRow(row)">保存</el-link><el-link type="primary" @click="editRow(row)">编辑</el-link><el-link type="danger" @click="deleteRow(row)">删除</el-link></template></el-table-column></el-table></section>
|
||||
</section>
|
||||
<section class="waybill-import-create__detail-panel" v-if="rows.length"><div class="waybill-import-create__detail-head"><div class="dialog-section-title">导入数据明细</div><el-button type="danger" plain :disabled="!rowSelection.length" @click="removeSelectedRows">批量删除</el-button></div><el-tabs v-model="previewTab"><el-tab-pane :label="`运单数(${rows.length})`" name="all" /><el-tab-pane :label="`疑似重复(${duplicateRows.length})`" name="duplicate" /></el-tabs><el-table :data="previewRows" border max-height="440" @selection-change="rowSelection = $event"><el-table-column type="selection" width="55" /><el-table-column type="index" label="序号" width="65" /><el-table-column v-for="column in detailColumns" :key="column.prop" :prop="column.prop" :label="column.label" :min-width="column.minWidth || 150" show-overflow-tooltip><template #default="{ row }"><template v-if="row._editing"><el-select v-if="column.editor === 'transportType'" v-model="row.transportType" clearable filterable placeholder="请选择"><el-option v-for="item in transportTypeOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select><el-select v-else-if="column.editor === 'driver'" v-model="row.driverName" clearable filterable placeholder="请选择" @change="value => handleEditorChange(column, row, value)"><el-option v-for="item in driverOptions" :key="item.id || item.driverName || item.name" :label="item.driverName || item.name" :value="item.driverName || item.name" /></el-select><el-cascader v-else-if="column.editor === 'cargoType'" :model-value="resolveCargoTypePath(row)" :options="cargoTypeOptions" :props="{ label: 'cargoName', value: 'id', children: 'children', emitPath: true }" clearable filterable placeholder="请选择" @change="value => handleEditorChange(column, row, value)" /><el-select v-else-if="column.editor === 'cargoName'" v-model="row.cargoName" clearable filterable placeholder="请选择" @visible-change="visible => visible && loadCommonCargoOptions(row)" @change="value => handleEditorChange(column, row, value)"><el-option v-for="item in getCommonCargoOptions(row)" :key="item.id || item.cargoName || item.name" :label="formatCargoTypeLabel(item)" :value="formatCargoTypeLabel(item)" /></el-select><el-date-picker v-else-if="column.editor === 'datetime'" v-model="row[column.prop]" type="datetime" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择" /><el-input v-else-if="column.editor === 'textarea'" v-model="row[column.prop]" type="textarea" :autosize="{ minRows: 1, maxRows: 3 }" :placeholder="`请输入${column.label}`" /><el-input v-else-if="column.editor === 'number'" :model-value="row[column.prop]" inputmode="decimal" :placeholder="`请输入${column.label}`" @input="value => updateEditorValue(column, row, value)" /><el-input v-else v-model="row[column.prop]" :placeholder="`请输入${column.label}`" /></template><span v-else>{{ formatCell(row, column) }}</span></template></el-table-column><el-table-column label="操作" width="180" fixed="right"><template #default="{ row }"><template v-if="row._editing"><el-link type="primary" @click="saveRow(row)">保存</el-link><el-link type="primary" @click="cancelRow(row)">取消</el-link></template><template v-else><el-link type="primary" @click="editRow(row)">编辑</el-link><el-link type="danger" @click="deleteRow(row)">删除</el-link></template></template></el-table-column></el-table></section>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||||
import { getList as getContractList } from '@/api/business/contract-manage';
|
||||
import { getList as getProjectList } from '@/api/business/project-apply';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import * as api from '@/api/business/waybill-manage';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
|
||||
@@ -56,21 +61,162 @@ const visible = computed({ get: () => props.modelValue, set: value => emit('upda
|
||||
const createVisible = ref(false), detailVisible = ref(false), formRef = ref();
|
||||
const query = reactive({ batchNo: '', carrierId: '', createUser: '', createTimeRange: [] });
|
||||
const detailQuery = reactive({ vehicleNo: '', cargoName: '' });
|
||||
const form = reactive({ projectId: '', customerName: '', contractId: '', carrierType: '承运商', carrierIds: [], status: 'completed', importType: 'waybill', planId: '', file: null });
|
||||
const form = reactive({ projectId: '', customerName: '', contractId: '', contractName: '', carrierType: '承运商', carrierIds: [], status: 'completed', importType: 'waybill', planId: '', file: null });
|
||||
const rules = { projectId: [{ required: true, message: '请选择项目' }], customerName: [{ required: true, message: '客户不能为空' }], contractId: [{ required: true, message: '请选择客户合同' }], carrierType: [{ required: true, message: '请选择承运类型' }], status: [{ required: true, message: '请选择导入状态' }], importType: [{ required: true, message: '请选择导入类型' }], file: [{ required: true, message: '请上传明细表' }] };
|
||||
const batches = ref([]), details = ref([]), rows = ref([]), selected = ref([]), files = ref([]), projects = ref([]), contracts = ref([]), carriers = ref([]), creators = ref([]), plans = ref([]);
|
||||
const transportTypeOptions = ref([]), cargoTypeOptions = ref([]), cargoTypeFlatOptions = ref([]), commonCargoOptionsMap = ref({}), commonCargoLoadingMap = ref({}), driverOptions = ref([]);
|
||||
const rowSelection = ref([]), previewTab = ref('all');
|
||||
const duplicateRows = computed(() => rows.value.filter(row => row._duplicate));
|
||||
const previewRows = computed(() => previewTab.value === 'duplicate' ? duplicateRows.value : rows.value);
|
||||
const page = reactive({ current: 1, size: 10, total: 0 }), detailPage = reactive({ current: 1, size: 10, total: 0 });
|
||||
const detailColumns = [{ prop: 'batchNo', label: '运单批次号' }, { prop: 'originalNo', label: '原始单号' }, { prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' }, { prop: 'driverName', label: '司机/船长' }, { prop: 'transportType', label: '运输类型' }, { prop: 'cargoName', label: '货物名称' }, { prop: 'cargoType', label: '货物类型' }, { prop: 'quantity', label: '重量' }, { prop: 'departureAddress', label: '发货地址' }, { prop: 'departureContact', label: '发货联系人' }, { prop: 'departurePhone', label: '发货联系人电话' }, { prop: 'arrivalAddress', label: '到货地址' }, { prop: 'arrivalContact', label: '收货联系人' }, { prop: 'arrivalPhone', label: '收货联系人电话' }, { prop: 'startDate', label: '开始时间' }, { prop: 'endDate', label: '结束时间' }, { prop: 'unitPrice', label: '单价' }, { prop: 'freight', label: '运费' }, { prop: 'otherFeeTotal', label: '其他费用合计' }, { prop: 'freightTotal', label: '运费合计' }, { prop: 'remark', label: '备注' }];
|
||||
const requiredDetailFields = [{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' }, { prop: 'driverName', label: '司机/船长' }, { prop: 'transportType', label: '运输类型' }, { prop: 'cargoName', label: '货物名称' }, { prop: 'cargoType', label: '货物类型' }, { prop: 'departureAddress', label: '发货地址' }, { prop: 'departureContact', label: '发货联系人' }, { prop: 'departurePhone', label: '发货联系人电话' }, { prop: 'arrivalAddress', label: '到货地址' }, { prop: 'arrivalContact', label: '到货联系人' }, { prop: 'arrivalPhone', label: '收货联系人电话' }, { prop: 'startDate', label: '开始时间' }, { prop: 'endDate', label: '结束时间' }, { prop: 'unitPrice', label: '单价' }, { prop: 'freight', label: '运费' }];
|
||||
const detailColumns = [
|
||||
{ prop: 'batchNo', label: '运单批次号', editor: 'input', minWidth: 150 },
|
||||
{ prop: 'originalNo', label: '原始单号', editor: 'input', minWidth: 150 },
|
||||
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号', editor: 'input', minWidth: 220 },
|
||||
{ prop: 'driverName', label: '司机/船长', editor: 'driver', minWidth: 170 },
|
||||
{ prop: 'transportType', label: '运输方式', editor: 'transportType', minWidth: 150 },
|
||||
{ prop: 'cargoType', label: '货物类型', editor: 'cargoType', minWidth: 220 },
|
||||
{ prop: 'cargoName', label: '货物名称', editor: 'cargoName', minWidth: 220 },
|
||||
{ prop: 'quantity', label: '重量', editor: 'number', minWidth: 130 },
|
||||
{ prop: 'departureAddress', label: '发货地址', editor: 'textarea', minWidth: 260 },
|
||||
{ prop: 'departureContact', label: '发货联系人', editor: 'input', minWidth: 140 },
|
||||
{ prop: 'departurePhone', label: '发货联系人电话', editor: 'input', minWidth: 160 },
|
||||
{ prop: 'arrivalAddress', label: '到货地址', editor: 'textarea', minWidth: 260 },
|
||||
{ prop: 'arrivalContact', label: '收货联系人', editor: 'input', minWidth: 140 },
|
||||
{ prop: 'arrivalPhone', label: '收货联系人电话', editor: 'input', minWidth: 160 },
|
||||
{ prop: 'startDate', label: '开始时间', editor: 'datetime', minWidth: 190 },
|
||||
{ prop: 'endDate', label: '结束时间', editor: 'datetime', minWidth: 190 },
|
||||
{ prop: 'unitPrice', label: '单价', editor: 'number', minWidth: 130 },
|
||||
{ prop: 'freight', label: '运费', editor: 'number', minWidth: 130 },
|
||||
{ prop: 'otherFeeTotal', label: '其他费用合计', editor: 'number', minWidth: 150 },
|
||||
{ prop: 'freightTotal', label: '运费合计', editor: 'number', minWidth: 130 },
|
||||
{ prop: 'remark', label: '备注', editor: 'textarea', minWidth: 180 },
|
||||
];
|
||||
const requiredDetailFields = [{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' }, { prop: 'driverName', label: '司机/船长' }, { prop: 'transportType', label: '运输方式' }, { prop: 'cargoName', label: '货物名称' }, { prop: 'cargoType', label: '货物类型' }, { prop: 'departureAddress', label: '发货地址' }, { prop: 'departureContact', label: '发货联系人' }, { prop: 'departurePhone', label: '发货联系人电话' }, { prop: 'arrivalAddress', label: '到货地址' }, { prop: 'arrivalContact', label: '到货联系人' }, { prop: 'arrivalPhone', label: '收货联系人电话' }, { prop: 'startDate', label: '开始时间' }, { prop: 'endDate', label: '结束时间' }, { prop: 'unitPrice', label: '单价' }, { prop: 'freight', label: '运费' }];
|
||||
const projectQueryParams = { approvalStatuses: 'approved,change_approved' };
|
||||
const extractRecords = res => {
|
||||
const data = res?.data || res;
|
||||
if (Array.isArray(data)) return data;
|
||||
if (Array.isArray(data?.records)) return data.records;
|
||||
if (Array.isArray(data?.data)) return data.data;
|
||||
if (Array.isArray(data?.data?.records)) return data.data.records;
|
||||
return [];
|
||||
};
|
||||
const formatCargoTypeLabel = item => item?.cargoName || item?.name || item?.typeName || item?.label || '';
|
||||
const buildCargoTypeTree = cargoTypes => {
|
||||
const flatCargoTypes = [];
|
||||
const collectCargoTypes = list => {
|
||||
(list || []).forEach(item => {
|
||||
flatCargoTypes.push({ ...item, children: undefined });
|
||||
if (item.children?.length) collectCargoTypes(item.children);
|
||||
});
|
||||
};
|
||||
collectCargoTypes(cargoTypes);
|
||||
const nodeMap = new Map();
|
||||
const codeMap = new Map();
|
||||
flatCargoTypes.forEach(item => {
|
||||
const id = item.id ?? item.cargoCode ?? item.code;
|
||||
if (id === undefined || id === null) return;
|
||||
const node = {
|
||||
...item,
|
||||
id: String(id),
|
||||
cargoName: formatCargoTypeLabel(item),
|
||||
cargoCode: item.cargoCode || item.code || '',
|
||||
parentId: item.parentId === undefined || item.parentId === null ? '' : String(item.parentId),
|
||||
parentCargoCode: item.parentCargoCode || item.parentCode || '',
|
||||
children: [],
|
||||
};
|
||||
nodeMap.set(node.id, node);
|
||||
if (node.cargoCode) codeMap.set(String(node.cargoCode), node);
|
||||
});
|
||||
const rootNodes = [];
|
||||
nodeMap.forEach(node => {
|
||||
const parent = nodeMap.get(node.parentId) || codeMap.get(String(node.parentCargoCode || '')) || codeMap.get(String(node.parentId || ''));
|
||||
if (parent && parent !== node && Number(node.typeLevel) !== 1) parent.children.push(node);
|
||||
else rootNodes.push(node);
|
||||
});
|
||||
const normalize = (cargoType, level = 1, parentPath = []) => {
|
||||
if (level > 2) return null;
|
||||
const id = String(cargoType.id);
|
||||
const path = [...parentPath, id];
|
||||
const children = level === 1 ? (cargoType.children || []).map(item => normalize(item, level + 1, path)).filter(Boolean) : [];
|
||||
return { ...cargoType, id, cargoName: formatCargoTypeLabel(cargoType), path, leaf: level === 2, children: children.length ? children : undefined };
|
||||
};
|
||||
return rootNodes.map(item => normalize(item, 1)).filter(Boolean);
|
||||
};
|
||||
const flattenCargoTypeOptions = options => {
|
||||
const result = [];
|
||||
const collect = list => (list || []).forEach(item => {
|
||||
result.push(item);
|
||||
if (item.children?.length) collect(item.children);
|
||||
});
|
||||
collect(options);
|
||||
return result;
|
||||
};
|
||||
const getCargoTypePathLabels = path => {
|
||||
let options = cargoTypeOptions.value;
|
||||
const labels = [];
|
||||
(path || []).forEach(value => {
|
||||
const option = (options || []).find(item => String(item.id) === String(value));
|
||||
if (!option) return;
|
||||
labels.push(option.cargoName || '');
|
||||
options = option.children || [];
|
||||
});
|
||||
return labels.filter(Boolean);
|
||||
};
|
||||
const findCargoTypeByPath = path => {
|
||||
const id = Array.isArray(path) ? path[path.length - 1] : path;
|
||||
return cargoTypeFlatOptions.value.find(item => String(item.id) === String(id));
|
||||
};
|
||||
const resolveCargoTypePath = row => {
|
||||
if (Array.isArray(row.cargoTypePath) && row.cargoTypePath.length) return row.cargoTypePath;
|
||||
const cargoType = cargoTypeFlatOptions.value.find(item => [item.cargoName, item.name, item.typeName, item.cargoCode].some(value => String(value || '') === String(row.cargoType || row.cargoTypeCode || '')));
|
||||
return cargoType?.path || [];
|
||||
};
|
||||
const commonCargoKey = row => {
|
||||
const path = resolveCargoTypePath(row);
|
||||
const cargoType = findCargoTypeByPath(path);
|
||||
return String(cargoType?.cargoCode || cargoType?.code || path.join('/'));
|
||||
};
|
||||
const getCommonCargoOptions = row => commonCargoOptionsMap.value[commonCargoKey(row)] || [];
|
||||
const loadCommonCargoOptions = async row => {
|
||||
const path = resolveCargoTypePath(row);
|
||||
const cargoType = findCargoTypeByPath(path);
|
||||
const key = commonCargoKey(row);
|
||||
if (!key || commonCargoLoadingMap.value[key]) return;
|
||||
commonCargoLoadingMap.value = { ...commonCargoLoadingMap.value, [key]: true };
|
||||
try {
|
||||
const labels = getCargoTypePathLabels(path);
|
||||
const res = await getCommonCargoList(1, 500, {
|
||||
secondCargoTypeName: labels[1] || row.cargoType || '',
|
||||
secondCargoTypeCode: cargoType?.cargoCode || cargoType?.code || '',
|
||||
allDept: 0,
|
||||
});
|
||||
commonCargoOptionsMap.value = { ...commonCargoOptionsMap.value, [key]: extractRecords(res) };
|
||||
} finally {
|
||||
commonCargoLoadingMap.value = { ...commonCargoLoadingMap.value, [key]: false };
|
||||
}
|
||||
};
|
||||
const loadEditorOptions = async () => {
|
||||
const [dictRes, cargoTypeRes, driverRes] = await Promise.all([
|
||||
getDictionary({ code: 'transport_type' }),
|
||||
getCargoTypeList(1, 9999),
|
||||
getDriverList(1, 9999, {}),
|
||||
]);
|
||||
transportTypeOptions.value = extractRecords(dictRes).map(item => ({
|
||||
label: item.dictValue || item.label,
|
||||
value: item.dictKey || item.value,
|
||||
}));
|
||||
cargoTypeOptions.value = buildCargoTypeTree(extractRecords(cargoTypeRes));
|
||||
cargoTypeFlatOptions.value = flattenCargoTypeOptions(cargoTypeOptions.value);
|
||||
driverOptions.value = extractRecords(driverRes);
|
||||
};
|
||||
const loadBatches = async () => { const res = await api.getImportBatches({ ...query, current: page.current, size: page.size }); batches.value = res.data?.data?.records || []; page.total = res.data?.data?.total || 0; };
|
||||
const resetQuery = () => { Object.assign(query, { batchNo: '', carrierId: '', createUser: '', createTimeRange: [] }); page.current = 1; loadBatches(); };
|
||||
const loadDetails = async () => { const res = await api.getImportDetails({ batchId: detailQuery.batchId, ...detailQuery, current: detailPage.current, size: detailPage.size }); details.value = res.data?.data?.records || []; detailPage.total = res.data?.data?.total || 0; };
|
||||
const openCreate = async () => {
|
||||
createVisible.value = true;
|
||||
const projectRes = await getProjectList(1, 9999, {});
|
||||
const [projectRes] = await Promise.all([getProjectList(1, 9999, projectQueryParams), loadEditorOptions()]);
|
||||
projects.value = projectRes.data?.data?.records || [];
|
||||
api.getImportOptions?.().then(res => {
|
||||
const data = res.data?.data || {};
|
||||
@@ -84,7 +230,27 @@ const openDetail = row => { detailQuery.batchId = row.id; detailVisible.value =
|
||||
const editBatch = row => { openCreate(); Object.assign(form, row); };
|
||||
const removeBatches = () => api.removeImportBatches(selected.value.map(item => item.id).join(',')).then(loadBatches);
|
||||
const removeBatch = row => api.removeImportBatches(row.id).then(loadBatches);
|
||||
const projectChange = id => { const item = projects.value.find(row => row.id === id); form.customerName = item?.customerName || ''; api.getImportContracts?.(id).then(res => { contracts.value = res.data?.data || []; }); };
|
||||
const projectChange = async id => {
|
||||
const item = projects.value.find(row => row.id === id);
|
||||
form.customerName = item?.customerName || item?.customerNames || item?.customer || '';
|
||||
form.contractId = '';
|
||||
form.contractName = '';
|
||||
contracts.value = [];
|
||||
if (!id) return;
|
||||
const res = await getContractList(1, 9999, {
|
||||
projectId: id,
|
||||
projectName: item?.projectName,
|
||||
});
|
||||
contracts.value = res.data?.data?.records || [];
|
||||
if (contracts.value.length) {
|
||||
form.contractId = contracts.value[0].id;
|
||||
form.contractName = contracts.value[0].contractName || '';
|
||||
}
|
||||
};
|
||||
const contractChange = id => {
|
||||
const item = contracts.value.find(row => row.id === id);
|
||||
form.contractName = item?.contractName || '';
|
||||
};
|
||||
const fileChange = async (file, list) => {
|
||||
files.value = list.slice(-1); form.file = file.raw;
|
||||
const XLSX = await import('xlsx');
|
||||
@@ -96,6 +262,44 @@ const fileChange = async (file, list) => {
|
||||
rows.value.forEach(row => { row._duplicate = count.get(row._duplicateKey) > 1; });
|
||||
};
|
||||
const fileRemove = () => { files.value = []; form.file = null; };
|
||||
const formatCell = (row, column) => row[column.prop] || '';
|
||||
const updateEditorValue = (column, row, value) => {
|
||||
if (column.editor === 'number') {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
row[column.prop] = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||
return;
|
||||
}
|
||||
row[column.prop] = value;
|
||||
};
|
||||
const handleEditorChange = (column, row, value) => {
|
||||
if (column.editor === 'driver') {
|
||||
const driver = driverOptions.value.find(item => [item.driverName, item.name].some(name => String(name || '') === String(value || '')));
|
||||
row.driverName = value || '';
|
||||
if (driver) row.driverPhone = driver.mobile || driver.phone || driver.driverPhone || row.driverPhone || '';
|
||||
}
|
||||
if (column.editor === 'cargoType') {
|
||||
const path = Array.isArray(value) ? value : [];
|
||||
const cargoType = findCargoTypeByPath(path);
|
||||
const labels = getCargoTypePathLabels(path);
|
||||
row.cargoTypePath = path;
|
||||
row.cargoType = labels.at(-1) || '';
|
||||
row.cargoTypeCode = cargoType?.cargoCode || cargoType?.code || cargoType?.id || '';
|
||||
row.cargoName = '';
|
||||
row.specification = '';
|
||||
row.model = '';
|
||||
loadCommonCargoOptions(row);
|
||||
}
|
||||
if (column.editor === 'cargoName') {
|
||||
const cargo = getCommonCargoOptions(row).find(item => formatCargoTypeLabel(item) === value);
|
||||
row.cargoName = value || '';
|
||||
if (cargo) {
|
||||
row.specification = cargo.specification || cargo.spec || row.specification || '';
|
||||
row.model = cargo.model || cargo.modelName || row.model || '';
|
||||
row.packageType = cargo.packageType || row.packageType || '';
|
||||
row.brand = cargo.brand || row.brand || '';
|
||||
}
|
||||
}
|
||||
};
|
||||
const downloadTemplate = async () => {
|
||||
try {
|
||||
const res = await api.exportImportTemplate();
|
||||
@@ -119,9 +323,9 @@ const validateImportRows = () => {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const editRow = row => { row._editing = true; };
|
||||
const saveRow = row => { row._editing = false; };
|
||||
const cancelRow = row => { row._cancelled = true; };
|
||||
const editRow = row => { row._editSnapshot = JSON.parse(JSON.stringify(row)); row._editing = true; };
|
||||
const saveRow = row => { row._editing = false; delete row._editSnapshot; };
|
||||
const cancelRow = row => { if (row._editSnapshot) Object.assign(row, row._editSnapshot); row._editing = false; delete row._editSnapshot; };
|
||||
const deleteRow = row => { rows.value = rows.value.filter(item => item !== row); };
|
||||
const removeSelectedRows = () => { const keys = new Set(rowSelection.value.map(row => row._key)); rows.value = rows.value.filter(row => !keys.has(row._key)); rowSelection.value = []; };
|
||||
const saveDraft = () => api.saveImportDraft({ ...form, rows: rows.value });
|
||||
@@ -153,15 +357,22 @@ const confirmImport = async () => { await formRef.value?.validate(); if (!valida
|
||||
}
|
||||
|
||||
.waybill-import-create {
|
||||
&__form-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
|
||||
&__form-panel,
|
||||
&__detail-panel {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
padding: 14px 16px;
|
||||
margin-top: 8px;
|
||||
border: 1px solid #eff1f7;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
&__detail-panel { margin-top: 12px; }
|
||||
&__form-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
|
||||
&__detail-head { display: flex; align-items: center; gap: 24px; margin-bottom: 8px; }
|
||||
&__file-tip { margin: 0 24px; color: #606266; }
|
||||
|
||||
:deep(.el-table .el-input),
|
||||
:deep(.el-table .el-select),
|
||||
:deep(.el-table .el-cascader),
|
||||
:deep(.el-table .el-date-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user