Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -49,6 +49,16 @@ const parseJsonObject = value => {
|
|||||||
const parseProcessNodes = value => {
|
const parseProcessNodes = value => {
|
||||||
const result = parseJsonArray(value);
|
const result = parseJsonArray(value);
|
||||||
if (result.length) return result;
|
if (result.length) return result;
|
||||||
|
if (typeof value === 'string' && value.trim()) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
|
return Array.isArray(parsed.nodes) ? parsed.nodes : [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return [];
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return [];
|
||||||
return Array.isArray(value.nodes) ? value.nodes : [];
|
return Array.isArray(value.nodes) ? value.nodes : [];
|
||||||
};
|
};
|
||||||
@@ -62,6 +72,75 @@ const requiresDriverAcceptConfirmation = processJson =>
|
|||||||
node.confirmDriver === true
|
node.confirmDriver === true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getProcessConfigNodes = row => {
|
||||||
|
const sources = [
|
||||||
|
row.processJson,
|
||||||
|
row.processConfigJson,
|
||||||
|
row.projectProcessJson,
|
||||||
|
row.processConfig,
|
||||||
|
row.projectProcessConfig,
|
||||||
|
];
|
||||||
|
return sources.flatMap(source => {
|
||||||
|
const nodes = parseProcessNodes(source);
|
||||||
|
if (nodes.length || !source || typeof source !== 'object' || Array.isArray(source)) {
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
const configuredNodes = parseProcessNodes(source.nodeConfigJson);
|
||||||
|
const includedNodes = String(source.includedNodes || '')
|
||||||
|
.split(',')
|
||||||
|
.map(item => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return configuredNodes.filter(
|
||||||
|
node =>
|
||||||
|
!includedNodes.length ||
|
||||||
|
includedNodes.includes(node.name) ||
|
||||||
|
includedNodes.includes(node.key)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasAcceptProcessNode = row =>
|
||||||
|
getProcessConfigNodes(row).some(
|
||||||
|
node => node.enabled !== false && (node.key === 'accept' || node.name === '接单')
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasDriverAcceptRecord = row => {
|
||||||
|
const recordValues = [
|
||||||
|
row.driverAcceptTime,
|
||||||
|
row.driverAcceptedTime,
|
||||||
|
row.acceptTime,
|
||||||
|
row.driverAcceptId,
|
||||||
|
row.driverAcceptedBy,
|
||||||
|
row.acceptUserId,
|
||||||
|
row.acceptUserName,
|
||||||
|
];
|
||||||
|
if (recordValues.some(value => value !== null && value !== undefined && value !== '')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const statusValues = [
|
||||||
|
row.driverAcceptStatus,
|
||||||
|
row.driverAccepted,
|
||||||
|
row.driverAccept,
|
||||||
|
row.accepted,
|
||||||
|
row.isAccepted,
|
||||||
|
row.acceptStatus,
|
||||||
|
];
|
||||||
|
return statusValues.some(value => {
|
||||||
|
if (value === true) return true;
|
||||||
|
if (value === false || value === null || value === undefined || value === '') return false;
|
||||||
|
return ['1', 'true', 'accepted', 'confirmed', 'success', '已接单', '已确认'].includes(
|
||||||
|
String(value).toLowerCase()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isInProgressStatus = (row, defaultText) =>
|
||||||
|
['processing', 'running', 'in_progress', 'inProgress'].includes(
|
||||||
|
String(row.businessStatus || row.status || row.waybillStatus || '')
|
||||||
|
) ||
|
||||||
|
String(defaultText || '').includes('进行中') ||
|
||||||
|
String(row.businessStatusName || row.statusName || '').includes('进行中');
|
||||||
|
|
||||||
const getFirstValue = (row, props) => {
|
const getFirstValue = (row, props) => {
|
||||||
const prop = props.find(item => !isEmpty(row[item]));
|
const prop = props.find(item => !isEmpty(row[item]));
|
||||||
return prop ? row[prop] : '';
|
return prop ? row[prop] : '';
|
||||||
@@ -222,6 +301,14 @@ export const config = {
|
|||||||
statusProp: 'businessStatus',
|
statusProp: 'businessStatus',
|
||||||
statusTextProp: 'businessStatusName',
|
statusTextProp: 'businessStatusName',
|
||||||
formatStatus(row, prop, defaultText) {
|
formatStatus(row, prop, defaultText) {
|
||||||
|
if (
|
||||||
|
prop === 'businessStatus' &&
|
||||||
|
hasAcceptProcessNode(row) &&
|
||||||
|
isInProgressStatus(row, defaultText) &&
|
||||||
|
!hasDriverAcceptRecord(row)
|
||||||
|
) {
|
||||||
|
return '待执行';
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
prop === 'businessStatus' &&
|
prop === 'businessStatus' &&
|
||||||
row.businessStatus === 'pending' &&
|
row.businessStatus === 'pending' &&
|
||||||
@@ -432,7 +519,7 @@ export const option = {
|
|||||||
{
|
{
|
||||||
label: '运费',
|
label: '运费',
|
||||||
prop: 'freight',
|
prop: 'freight',
|
||||||
formatter: row => formatFreightTotal(row),
|
formatter: row => formatFreight(row),
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
@@ -538,6 +625,19 @@ export const option = {
|
|||||||
viewDisplay: false,
|
viewDisplay: false,
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '关联单号',
|
||||||
|
prop: 'relationNo',
|
||||||
|
search: true,
|
||||||
|
searchOrder: 2,
|
||||||
|
span: 6,
|
||||||
|
order: 820,
|
||||||
|
minWidth: 140,
|
||||||
|
hide: false,
|
||||||
|
addDisplay: true,
|
||||||
|
editDisplay: true,
|
||||||
|
viewDisplay: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '预计发货日期',
|
label: '预计发货日期',
|
||||||
prop: 'estimatedStartTime',
|
prop: 'estimatedStartTime',
|
||||||
@@ -684,16 +784,6 @@ export const option = {
|
|||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
viewDisplay: false,
|
viewDisplay: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: '关联单号',
|
|
||||||
prop: 'relationNo',
|
|
||||||
search: true,
|
|
||||||
searchOrder: 2,
|
|
||||||
span: 6,
|
|
||||||
order: 820,
|
|
||||||
minWidth: 140,
|
|
||||||
hide: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: '货物信息',
|
label: '货物信息',
|
||||||
prop: 'goodsJson',
|
prop: 'goodsJson',
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-loading="loading" class="master-detail">
|
<div v-loading="loading" class="master-detail">
|
||||||
<section v-if="master" class="detail-overview">
|
<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>
|
<div class="detail-heading"><div><h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2><el-tag :type="statusType(master.businessStatus)">{{ statusName(master.businessStatus) }}</el-tag><el-tag v-if="transportFlowLabel" type="info" class="transport-flow-tag">{{ transportFlowLabel }}</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 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>
|
<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>
|
<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>
|
</section>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<section v-if="master" class="execution-detail-card">
|
<section v-if="master" class="execution-detail-card">
|
||||||
<div class="execution-detail-heading"><h3>分段执行明细</h3></div>
|
<div class="execution-detail-heading"><h3>分段执行明细</h3></div>
|
||||||
<section v-for="(route, index) in segments" :key="route.segmentNo" class="segment-detail">
|
<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 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>
|
<header @click="toggleSegment(route.segmentNo)"><h3><span class="segment-index">{{ segmentNumber(route, index) }}</span><span>{{ route.departureName || '-' }} → {{ route.arrivalName || '-' }}</span><span class="segment-detail__transport-type">{{ segmentTransportType(route) || '-' }}</span><span class="segment-detail__progress-bar"><span :style="{ width: `${segmentProgress(route)}%` }"></span></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 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>
|
<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-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>
|
||||||
@@ -25,7 +25,22 @@
|
|||||||
</section>
|
</section>
|
||||||
<section v-if="master" class="master-goods-card">
|
<section v-if="master" class="master-goods-card">
|
||||||
<div class="master-goods-card__heading"><h3>货物信息</h3></div>
|
<div class="master-goods-card__heading"><h3>货物信息</h3></div>
|
||||||
<el-table :data="master.goods || []" border><el-table-column type="index" label="序号" width="64" /><el-table-column prop="cargoName" label="货物名称" min-width="150" /><el-table-column prop="cargoType" label="货物类型" min-width="130" /><el-table-column label="数量" width="130"><template #default="{ row }">{{ quantity(row.quantity) }} {{ row.quantityUnit || '' }}</template></el-table-column><el-table-column prop="packageType" label="包装" min-width="120" /><el-table-column prop="brand" label="品牌" min-width="120" /><el-table-column prop="specification" label="规格" min-width="120" /><el-table-column prop="model" label="型号" min-width="120" /></el-table>
|
<el-table :data="master.goods || []" border>
|
||||||
|
<el-table-column type="index" label="序号" width="64" />
|
||||||
|
<el-table-column prop="cargoName" label="货物名称" min-width="150" />
|
||||||
|
<el-table-column prop="cargoType" label="货物类型" min-width="130" />
|
||||||
|
<el-table-column prop="packageType" label="包装" min-width="120" />
|
||||||
|
<el-table-column label="重量(吨)" min-width="110"><template #default="{ row }">{{ row.weight ?? row.weightTon ?? (row.quantityUnit === '吨' ? row.quantity : '-') }}</template></el-table-column>
|
||||||
|
<el-table-column label="体积(方)" min-width="110"><template #default="{ row }">{{ row.volume ?? row.volumeCubic ?? '-' }}</template></el-table-column>
|
||||||
|
<el-table-column label="数量" min-width="100"><template #default="{ row }">{{ row.quantity ?? '-' }}{{ row.quantityUnit ? ` ${row.quantityUnit}` : '' }}</template></el-table-column>
|
||||||
|
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
||||||
|
<el-table-column prop="deviceCode" label="设备编码" min-width="130" />
|
||||||
|
<el-table-column prop="brand" label="品牌" min-width="120" />
|
||||||
|
<el-table-column label="规格型号" min-width="150"><template #default="{ row }">{{ [row.specification, row.model].filter(Boolean).join('/') || '-' }}</template></el-table-column>
|
||||||
|
<el-table-column prop="remark" label="备注" min-width="160" />
|
||||||
|
<el-table-column label="货物单价(元)" min-width="130"><template #default="{ row }">{{ row.unitPrice ?? '-' }}</template></el-table-column>
|
||||||
|
<el-table-column label="计划日期" min-width="130"><template #default="{ row }">{{ row.planDate || row.planStartDate || '-' }}</template></el-table-column>
|
||||||
|
</el-table>
|
||||||
<el-empty v-if="!(master.goods || []).length" description="暂无货物信息" :image-size="56" />
|
<el-empty v-if="!(master.goods || []).length" description="暂无货物信息" :image-size="56" />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,6 +71,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
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 []; } },
|
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 []; } },
|
||||||
|
transportFlowLabel() { return this.segments.map(route => this.segmentTransportType(route).slice(0, 1)).filter(Boolean).join(' → '); },
|
||||||
goodsNames() { return (this.master?.goods || []).map(item => item.cargoName).filter(Boolean).join('、') || '-'; },
|
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('、') || '-'; },
|
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)}` : '-'; },
|
dateRange() { return this.master?.planStartTime && this.master?.planEndTime ? `${String(this.master.planStartTime).slice(0, 10)} ~ ${String(this.master.planEndTime).slice(0, 10)}` : '-'; },
|
||||||
@@ -112,6 +128,7 @@ export default {
|
|||||||
.master-detail { padding-bottom: 20px; color: #303133; }
|
.master-detail { padding-bottom: 20px; color: #303133; }
|
||||||
.detail-overview, .execution-detail-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
.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-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; } }
|
||||||
|
.transport-flow-tag { margin-left: 8px; }
|
||||||
.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; } }
|
.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 { 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__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; } }
|
||||||
@@ -122,7 +139,7 @@ export default {
|
|||||||
.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 { 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__progress { color: #606266; font-size: 14px; font-weight: 400; }
|
||||||
.segment-detail__transport-type { color: #409eff; font-size: 14px; font-weight: 500; }
|
.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 { display: inline-flex; width: 144px; 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__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-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-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; }
|
||||||
|
|||||||
@@ -56,12 +56,16 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="16">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="发货地址" required><el-input :model-value="addressText(route.departureName, route.departureAddress)" readonly /></el-form-item>
|
<el-form-item label="发货地址" required><el-input :model-value="addressText(route.departureName, route.departureAddress)" readonly /></el-form-item>
|
||||||
</el-col>
|
</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.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="联系方式"><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="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-row>
|
||||||
|
<el-row :gutter="16">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="收货地址" required><el-input :model-value="addressText(route.arrivalName, route.arrivalAddress)" readonly /></el-form-item>
|
<el-form-item label="收货地址" required><el-input :model-value="addressText(route.arrivalName, route.arrivalAddress)" readonly /></el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -121,6 +125,7 @@
|
|||||||
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form carrier-type-form"><el-form-item label="承运类型" required><el-radio-group v-model="route.carrierType" @change="value => handleCarrierTypeChange(route, value)"><el-radio-button label="承运商" /><el-radio-button label="自运" /><el-radio-button label="网货平台" /></el-radio-group></el-form-item></el-form>
|
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form carrier-type-form"><el-form-item label="承运类型" required><el-radio-group v-model="route.carrierType" @change="value => handleCarrierTypeChange(route, value)"><el-radio-button label="承运商" /><el-radio-button label="自运" /><el-radio-button label="网货平台" /></el-radio-group></el-form-item></el-form>
|
||||||
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form carrier-form">
|
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form carrier-form">
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
|
<template v-if="isRoad(route)">
|
||||||
<el-col v-if="route.carrierType === '承运商'" :span="6"><el-form-item label="承运商" required><el-select v-model="route.carrierName" filterable remote clearable placeholder="请选择" :remote-method="loadCarrierOptions" :loading="carrierLoading" @visible-change="visible => visible && loadCarrierOptions()"><el-option v-for="item in carrierOptions" :key="carrierOptionKey(item)" :label="carrierOptionLabel(item)" :value="carrierOptionLabel(item)" /></el-select></el-form-item></el-col>
|
<el-col v-if="route.carrierType === '承运商'" :span="6"><el-form-item label="承运商" required><el-select v-model="route.carrierName" filterable remote clearable placeholder="请选择" :remote-method="loadCarrierOptions" :loading="carrierLoading" @visible-change="visible => visible && loadCarrierOptions()"><el-option v-for="item in carrierOptions" :key="carrierOptionKey(item)" :label="carrierOptionLabel(item)" :value="carrierOptionLabel(item)" /></el-select></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="司机" :required="route.carrierType !== '承运商'"><el-select :ref="element => setDriverInput(route.segmentNo, element)" v-model="route.driverName" filterable remote clearable placeholder="请选择" :remote-method="loadDriverOptions" :loading="driverLoading" @visible-change="visible => visible && loadDriverOptions()" @change="value => handleDriverChange(route, value)"><el-option v-for="item in driverOptions" :key="driverOptionKey(item)" :label="driverOptionLabel(item)" :value="driverOptionLabel(item)" /></el-select></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="司机" :required="route.carrierType !== '承运商'"><el-select :ref="element => setDriverInput(route.segmentNo, element)" v-model="route.driverName" filterable remote clearable placeholder="请选择" :remote-method="loadDriverOptions" :loading="driverLoading" @visible-change="visible => visible && loadDriverOptions()" @change="value => handleDriverChange(route, value)"><el-option v-for="item in driverOptions" :key="driverOptionKey(item)" :label="driverOptionLabel(item)" :value="driverOptionLabel(item)" /></el-select></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item :label="route.carrierType === '承运商' ? '手机号' : '司机手机号'" :required="route.carrierType !== '承运商'"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item :label="route.carrierType === '承运商' ? '手机号' : '司机手机号'" :required="route.carrierType !== '承运商'"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||||
@@ -128,6 +133,15 @@
|
|||||||
<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.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.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="押运人手机号" required><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-col v-if="route.carrierType === '承运商'" :span="6"><el-form-item label="承运商" required><el-select v-model="route.carrierName" filterable remote clearable placeholder="请选择" :remote-method="loadCarrierOptions" :loading="carrierLoading" @visible-change="visible => visible && loadCarrierOptions()"><el-option v-for="item in carrierOptions" :key="carrierOptionKey(item)" :label="carrierOptionLabel(item)" :value="carrierOptionLabel(item)" /></el-select></el-form-item></el-col>
|
||||||
|
<el-col :span="6"><el-form-item label="船/航/班列号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
||||||
|
<el-col :span="6"><el-form-item label="船长" required><el-input v-model="route.captainName" placeholder="请输入" /></el-form-item></el-col>
|
||||||
|
<el-col :span="6"><el-form-item label="联系电话" required><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||||
|
<el-col :span="6"><el-form-item label="箱号" required><el-input v-model="route.containerNo" placeholder="请输入" /></el-form-item></el-col>
|
||||||
|
<el-col :span="6"><el-form-item label="舱位" required><el-input v-model="route.cabinNo" placeholder="请输入" /></el-form-item></el-col>
|
||||||
|
</template>
|
||||||
<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="里程(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-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-row>
|
||||||
@@ -284,7 +298,7 @@ export default {
|
|||||||
else Object.assign(row, { sourceIndex: undefined, cargoName: '', quantity: '', quantityUnit: '' });
|
else Object.assign(row, { sourceIndex: undefined, cargoName: '', quantity: '', quantityUnit: '' });
|
||||||
this.syncFreightItems(route);
|
this.syncFreightItems(route);
|
||||||
},
|
},
|
||||||
isRoad(route) { return String(route.transportType || '').includes('公路'); },
|
isRoad(route) { const type = String(route.transportType || '').toLowerCase(); return type.includes('公路') || type === 'road'; },
|
||||||
dispatchedQuantity(route) {
|
dispatchedQuantity(route) {
|
||||||
return this.quantityNumber(route.dispatchedQuantity) + this.pendingSegmentQuantity(route);
|
return this.quantityNumber(route.dispatchedQuantity) + this.pendingSegmentQuantity(route);
|
||||||
},
|
},
|
||||||
@@ -344,9 +358,13 @@ export default {
|
|||||||
},
|
},
|
||||||
dispatchedGoodsQuantity(route, row) {
|
dispatchedGoodsQuantity(route, row) {
|
||||||
const dispatchedGoods = route.dispatchedGoods || {};
|
const dispatchedGoods = route.dispatchedGoods || {};
|
||||||
return this.quantityNumber(
|
const exactValue = dispatchedGoods[this.goodsKey(row)] ?? dispatchedGoods[this.goodsKey(row, false)];
|
||||||
dispatchedGoods[this.goodsKey(row)] ?? dispatchedGoods[this.goodsKey(row, false)]
|
if (exactValue !== undefined && exactValue !== null) return this.quantityNumber(exactValue);
|
||||||
);
|
const matchedValue = Object.entries(dispatchedGoods).find(([key]) => {
|
||||||
|
const [cargoName, cargoType] = String(key).split('\u0000');
|
||||||
|
return cargoName === row.cargoName && cargoType === row.cargoType;
|
||||||
|
})?.[1];
|
||||||
|
return this.quantityNumber(matchedValue);
|
||||||
},
|
},
|
||||||
baseGoodsRemainingQuantity(route, row) {
|
baseGoodsRemainingQuantity(route, row) {
|
||||||
return Math.max(0, this.quantityNumber(row.quantity) - this.dispatchedGoodsQuantity(route, row));
|
return Math.max(0, this.quantityNumber(row.quantity) - this.dispatchedGoodsQuantity(route, row));
|
||||||
@@ -493,7 +511,6 @@ export default {
|
|||||||
route.trailerVehicleNo = '';
|
route.trailerVehicleNo = '';
|
||||||
route.escortName = '';
|
route.escortName = '';
|
||||||
route.escortPhone = '';
|
route.escortPhone = '';
|
||||||
route.mileage = undefined;
|
|
||||||
} else {
|
} else {
|
||||||
route.carrierName = '';
|
route.carrierName = '';
|
||||||
}
|
}
|
||||||
@@ -532,6 +549,9 @@ export default {
|
|||||||
route.driverName = '';
|
route.driverName = '';
|
||||||
route.driverPhone = '';
|
route.driverPhone = '';
|
||||||
route.vehicleNo = '';
|
route.vehicleNo = '';
|
||||||
|
route.captainName = '';
|
||||||
|
route.containerNo = '';
|
||||||
|
route.cabinNo = '';
|
||||||
route.trailerVehicleNo = '';
|
route.trailerVehicleNo = '';
|
||||||
route.escortName = '';
|
route.escortName = '';
|
||||||
route.escortPhone = '';
|
route.escortPhone = '';
|
||||||
@@ -548,15 +568,19 @@ export default {
|
|||||||
if (!route.estimatedEndTime) return this.$message.warning(`请选择${this.dateEndLabel(route)}`);
|
if (!route.estimatedEndTime) return this.$message.warning(`请选择${this.dateEndLabel(route)}`);
|
||||||
if (!this.validateRoutePhones(route)) return;
|
if (!this.validateRoutePhones(route)) return;
|
||||||
if (route.documentType === '运单') {
|
if (route.documentType === '运单') {
|
||||||
|
if (this.isRoad(route)) {
|
||||||
if (route.carrierType === '承运商' && (!route.carrierName || !route.vehicleNo || !route.mileage)) 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('请补全自运或网货平台的车辆与人员信息');
|
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('请补全自运或网货平台的车辆与人员信息');
|
||||||
|
} else if (!route.vehicleNo || !route.captainName || !route.driverPhone || !route.containerNo || !route.cabinNo || route.mileage === undefined || route.mileage === null || route.mileage === '' || (route.carrierType === '承运商' && !route.carrierName)) {
|
||||||
|
return this.$message.warning('请补全非公路运输的承运信息和里程');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const batchNo = `${route.segmentNo}-${Date.now()}`;
|
const batchNo = `${route.segmentNo}-${Date.now()}`;
|
||||||
goods.forEach(item => this.pending.push({
|
goods.forEach(item => this.pending.push({
|
||||||
id: `${route.segmentNo}-${item.cargoName}-${Date.now()}-${Math.random()}`,
|
id: `${route.segmentNo}-${item.cargoName}-${Date.now()}-${Math.random()}`,
|
||||||
batchNo,
|
batchNo,
|
||||||
segmentNo: route.segmentNo, relationNo: route.segmentNo, documentType: route.documentType, transportType: route.transportType, carrierType: route.carrierType,
|
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: 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,
|
carrierName: route.carrierName, driverName: route.driverName, driverPhone: route.driverPhone, vehicleNo: route.vehicleNo, captainName: route.captainName, cabinNo: route.cabinNo, containerNo: route.containerNo, 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,
|
departureName: route.departureName, departureAddress: route.departureAddress, departureContact: route.departureContact, departurePhone: route.departurePhone,
|
||||||
arrivalName: route.arrivalName, arrivalAddress: route.arrivalAddress, arrivalContact: route.arrivalContact, arrivalPhone: route.arrivalPhone,
|
arrivalName: route.arrivalName, arrivalAddress: route.arrivalAddress, arrivalContact: route.arrivalContact, arrivalPhone: route.arrivalPhone,
|
||||||
estimatedStartTime: route.estimatedStartTime, estimatedEndTime: route.estimatedEndTime,
|
estimatedStartTime: route.estimatedStartTime, estimatedEndTime: route.estimatedEndTime,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
</el-form>
|
</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>
|
<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>
|
</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: 2, 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>
|
<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 === 'date'" v-model="row[column.prop]" type="date" format="YYYY-MM-DD" value-format="YYYY-MM-DD" 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>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -84,8 +84,8 @@ const detailColumns = [
|
|||||||
{ prop: 'arrivalAddress', label: '到货地址', editor: 'textarea', minWidth: 260 },
|
{ prop: 'arrivalAddress', label: '到货地址', editor: 'textarea', minWidth: 260 },
|
||||||
{ prop: 'arrivalContact', label: '收货联系人', editor: 'input', minWidth: 140 },
|
{ prop: 'arrivalContact', label: '收货联系人', editor: 'input', minWidth: 140 },
|
||||||
{ prop: 'arrivalPhone', label: '收货联系人电话', editor: 'input', minWidth: 160 },
|
{ prop: 'arrivalPhone', label: '收货联系人电话', editor: 'input', minWidth: 160 },
|
||||||
{ prop: 'startDate', label: '开始时间', editor: 'datetime', minWidth: 190 },
|
{ prop: 'startDate', label: '开始时间', editor: 'date', minWidth: 150 },
|
||||||
{ prop: 'endDate', label: '结束时间', editor: 'datetime', minWidth: 190 },
|
{ prop: 'endDate', label: '结束时间', editor: 'date', minWidth: 150 },
|
||||||
{ prop: 'unitPrice', label: '单价', editor: 'number', minWidth: 130 },
|
{ prop: 'unitPrice', label: '单价', editor: 'number', minWidth: 130 },
|
||||||
{ prop: 'freight', label: '运费', editor: 'number', minWidth: 130 },
|
{ prop: 'freight', label: '运费', editor: 'number', minWidth: 130 },
|
||||||
{ prop: 'otherFeeTotal', label: '其他费用合计', editor: 'number', minWidth: 150 },
|
{ prop: 'otherFeeTotal', label: '其他费用合计', editor: 'number', minWidth: 150 },
|
||||||
@@ -256,9 +256,17 @@ const fileChange = async (file, list) => {
|
|||||||
const XLSX = await import('xlsx');
|
const XLSX = await import('xlsx');
|
||||||
const workbook = XLSX.read(await file.raw.arrayBuffer(), { type: 'array', cellDates: true });
|
const workbook = XLSX.read(await file.raw.arrayBuffer(), { type: 'array', cellDates: true });
|
||||||
const source = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], { defval: '' }).map(item => Object.fromEntries(Object.entries(item).map(([key, value]) => [key.replace(/^\*/, ''), value])));
|
const source = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], { defval: '' }).map(item => Object.fromEntries(Object.entries(item).map(([key, value]) => [key.replace(/^\*/, ''), value])));
|
||||||
const keyMap = { '原始单号': 'originalNo', '车牌号/航班号/船号/班列号': 'vehicleNo', '司机/船长': 'driverName', '运输类型': 'transportType', '货物名称': 'cargoName', '货物类型': 'cargoType', '重量': 'quantity', '发货地址': 'departureAddress', '发货联系人': 'departureContact', '发货联系人电话': 'departurePhone', '到货地址': 'arrivalAddress', '收货联系人': 'arrivalContact', '收货联系人电话': 'arrivalPhone', '开始时间': 'startDate', '结束时间': 'endDate', '单价': 'unitPrice', '运费': 'freight', '其他费用合计': 'otherFeeTotal', '运费合计': 'freightTotal', '备注': 'remark' };
|
const keyMap = { originalNo: ['原始单号'], vehicleNo: ['车牌号/航班号/船号/班列号'], driverName: ['司机/船长'], transportType: ['运输类型'], cargoName: ['货物名称'], cargoType: ['货物类型'], quantity: ['重量'], departureAddress: ['发货地址'], departureContact: ['发货联系人'], departurePhone: ['发货联系人电话'], arrivalAddress: ['到货地址'], arrivalContact: ['到货联系人', '收货联系人'], arrivalPhone: ['收货联系人电话'], startDate: ['开始时间'], endDate: ['结束时间'], unitPrice: ['单价'], freight: ['运费'], otherFeeTotal: ['其他费用合计'], freightTotal: ['运费合计'], remark: ['备注'] };
|
||||||
|
const normalizeImportDate = value => {
|
||||||
|
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
||||||
|
const pad = number => String(number).padStart(2, '0');
|
||||||
|
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
|
||||||
|
}
|
||||||
|
const text = String(value ?? '').trim();
|
||||||
|
return text.slice(0, 10);
|
||||||
|
};
|
||||||
const count = new Map();
|
const count = new Map();
|
||||||
rows.value = source.map((item, index) => { const row = { _key: `${Date.now()}-${index}`, batchNo: '', ...item }; Object.entries(keyMap).forEach(([label, key]) => { row[key] = item[label] ?? item[key] ?? ''; }); const duplicateKey = JSON.stringify(Object.values(keyMap).map(key => row[key])); count.set(duplicateKey, (count.get(duplicateKey) || 0) + 1); row._duplicateKey = duplicateKey; return row; });
|
rows.value = source.map((item, index) => { const row = { _key: `${Date.now()}-${index}`, batchNo: '', ...item }; Object.entries(keyMap).forEach(([key, labels]) => { row[key] = labels.map(label => item[label]).find(value => String(value ?? '').trim()) ?? item[key] ?? ''; }); row.startDate = normalizeImportDate(row.startDate); row.endDate = normalizeImportDate(row.endDate); const duplicateKey = JSON.stringify(Object.keys(keyMap).map(key => row[key])); count.set(duplicateKey, (count.get(duplicateKey) || 0) + 1); row._duplicateKey = duplicateKey; return row; });
|
||||||
rows.value.forEach(row => { row._duplicate = count.get(row._duplicateKey) > 1; });
|
rows.value.forEach(row => { row._duplicate = count.get(row._duplicateKey) > 1; });
|
||||||
};
|
};
|
||||||
const fileRemove = () => { files.value = []; form.file = null; };
|
const fileRemove = () => { files.value = []; form.file = null; };
|
||||||
|
|||||||
@@ -263,7 +263,19 @@
|
|||||||
<el-table-column label="配载子单号" prop="loadingSubNos" min-width="220">
|
<el-table-column label="配载子单号" prop="loadingSubNos" min-width="220">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="loading-manage-page__sub-nos">
|
<span class="loading-manage-page__sub-nos">
|
||||||
{{ splitText(row.loadingSubNos).join(',') || '-' }}
|
<template v-if="splitText(row.loadingSubNos).length">
|
||||||
|
<el-link
|
||||||
|
v-for="(waybill, index) in splitText(row.loadingSubNos)"
|
||||||
|
:key="`${waybill}-${index}`"
|
||||||
|
type="primary"
|
||||||
|
class="loading-manage-page__sub-no-link"
|
||||||
|
@click="openWaybillDetail({ waybillNo: waybill, id: findWaybillId(row, waybill) })"
|
||||||
|
>
|
||||||
|
{{ waybill }}
|
||||||
|
<template v-if="index < splitText(row.loadingSubNos).length - 1">,</template>
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
<span v-else>-</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -285,17 +297,23 @@
|
|||||||
min-width="180"
|
min-width="180"
|
||||||
>
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<el-tooltip :content="formatFullRouteAddress(row, 'departure')" placement="top">
|
||||||
<div class="loading-manage-page__address-cell">{{ formatRouteAddress(row, 'departure') }}</div>
|
<div class="loading-manage-page__address-cell">{{ formatRouteAddress(row, 'departure') }}</div>
|
||||||
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="途经地" prop="transitAddress" min-width="190">
|
<el-table-column label="途经地" prop="transitAddress" min-width="190">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<el-tooltip :content="formatFullRouteAddress(row, 'transit')" placement="top">
|
||||||
<div class="loading-manage-page__address-cell">{{ formatRouteAddress(row, 'transit') }}</div>
|
<div class="loading-manage-page__address-cell">{{ formatRouteAddress(row, 'transit') }}</div>
|
||||||
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="到货地" prop="arrivalAddress" min-width="180">
|
<el-table-column label="到货地" prop="arrivalAddress" min-width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<el-tooltip :content="formatFullRouteAddress(row, 'arrival')" placement="top">
|
||||||
<div class="loading-manage-page__address-cell">{{ formatRouteAddress(row, 'arrival') }}</div>
|
<div class="loading-manage-page__address-cell">{{ formatRouteAddress(row, 'arrival') }}</div>
|
||||||
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="承运商" prop="carrierName" min-width="160" show-overflow-tooltip />
|
<el-table-column label="承运商" prop="carrierName" min-width="160" show-overflow-tooltip />
|
||||||
@@ -1259,8 +1277,42 @@ export default {
|
|||||||
.map(item => item.trim())
|
.map(item => item.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
},
|
},
|
||||||
|
findWaybillId(row, waybillNo) {
|
||||||
|
const rows = this.splitText(row.loadingSubNos);
|
||||||
|
const index = rows.findIndex(item => item === waybillNo);
|
||||||
|
const linkedRows = this.parseJsonArray(row.waybillList || row.waybillRows || row.waybills);
|
||||||
|
if (linkedRows[index]?.id || linkedRows[index]?.waybillId) {
|
||||||
|
return linkedRows[index].id || linkedRows[index].waybillId;
|
||||||
|
}
|
||||||
|
const ids = this.parseJsonArray(row.waybillIdsJson);
|
||||||
|
return ids[index] || '';
|
||||||
|
},
|
||||||
|
async openWaybillDetail(row = {}) {
|
||||||
|
let id = row.id || row.waybillId || '';
|
||||||
|
if (!id && row.waybillNo) {
|
||||||
|
try {
|
||||||
|
const res = await getWaybillList(1, 1, { waybillNo: row.waybillNo });
|
||||||
|
const page = unwrapPage(res);
|
||||||
|
id = page.records?.[0]?.id || page[0]?.id || '';
|
||||||
|
} catch (error) {
|
||||||
|
id = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!id) {
|
||||||
|
ElMessage.info('当前子运单暂无详情数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$router.push({ path: '/business/waybill-manage', query: { detailId: id } });
|
||||||
|
},
|
||||||
formatRouteAddress(row, type) {
|
formatRouteAddress(row, type) {
|
||||||
const prefix = type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit';
|
const prefix = type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit';
|
||||||
|
if (type !== 'transit') {
|
||||||
|
const address = row[`${prefix}Address`];
|
||||||
|
const region = this.routeProvinceName(row, prefix);
|
||||||
|
const district = row[`${prefix}DistrictName`];
|
||||||
|
const source = this.mergeRouteAddressParts(region, district, address);
|
||||||
|
return this.formatProvinceCityDistrict(source) || '-';
|
||||||
|
}
|
||||||
const values = [
|
const values = [
|
||||||
row[`${prefix}RegionName`],
|
row[`${prefix}RegionName`],
|
||||||
row[`${prefix}DistrictName`],
|
row[`${prefix}DistrictName`],
|
||||||
@@ -1268,24 +1320,50 @@ export default {
|
|||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.flatMap(value => this.splitText(value))
|
.flatMap(value => this.splitText(value))
|
||||||
.map(value => this.formatCityDistrict(value))
|
.map(value => this.formatProvinceCityDistrict(value))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
return [...new Set(values)].join(',') || '-';
|
return [...new Set(values)].join(',') || '-';
|
||||||
},
|
},
|
||||||
formatCityDistrict(value) {
|
formatFullRouteAddress(row, type) {
|
||||||
|
const prefix = type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit';
|
||||||
|
if (type !== 'transit') {
|
||||||
|
const address = row[`${prefix}Address`];
|
||||||
|
const region = this.routeProvinceName(row, prefix);
|
||||||
|
const district = row[`${prefix}DistrictName`];
|
||||||
|
return this.mergeRouteAddressParts(region, district, address) || '-';
|
||||||
|
}
|
||||||
|
const values = [row.transitRegionName, row.transitDistrictName, row.transitAddress]
|
||||||
|
.filter(Boolean)
|
||||||
|
.flatMap(value => this.splitText(value));
|
||||||
|
return [...new Set(values)].join(',') || '-';
|
||||||
|
},
|
||||||
|
mergeRouteAddressParts(region, district, address) {
|
||||||
|
const values = [region, district, address].map(value => String(value || '').trim()).filter(Boolean);
|
||||||
|
if (!values.length) return '';
|
||||||
|
return values.reduce((result, value) => {
|
||||||
|
if (!result) return value;
|
||||||
|
if (result.includes(value) || value.includes(result)) return result.length >= value.length ? result : value;
|
||||||
|
return `${result}${value}`;
|
||||||
|
}, '');
|
||||||
|
},
|
||||||
|
routeProvinceName(row, prefix) {
|
||||||
|
return [
|
||||||
|
row[`${prefix}ProvinceName`],
|
||||||
|
row[`${prefix}Province`],
|
||||||
|
row[`${prefix}RegionName`],
|
||||||
|
row[`${prefix}Region`],
|
||||||
|
row[`${prefix}Name`],
|
||||||
|
row[`${prefix}AdministrativeRegionName`],
|
||||||
|
row[`${prefix}AddressRegionName`],
|
||||||
|
].find(value => String(value || '').trim()) || '';
|
||||||
|
},
|
||||||
|
formatProvinceCityDistrict(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
const cityIndex = text.indexOf('市');
|
const districtMatch = text.match(/(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/);
|
||||||
const provinceIndex = Math.max(text.lastIndexOf('省', cityIndex), text.lastIndexOf('自治区', cityIndex));
|
if (districtMatch) return text.slice(0, districtMatch.index + districtMatch[0].length);
|
||||||
const cityStart = provinceIndex > -1 ? provinceIndex + 1 : 0;
|
const cityMatch = text.match(/市/);
|
||||||
const districtStart = cityIndex > -1 ? cityIndex + 1 : cityStart;
|
return cityMatch ? text.slice(0, cityMatch.index + 1) : text;
|
||||||
const districtMatch = text
|
|
||||||
.slice(districtStart)
|
|
||||||
.match(/(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/);
|
|
||||||
if (districtMatch) {
|
|
||||||
return text.slice(cityStart, districtStart + districtMatch.index + districtMatch[0].length);
|
|
||||||
}
|
|
||||||
return cityIndex > -1 ? text.slice(cityStart, cityIndex + 1) : text;
|
|
||||||
},
|
},
|
||||||
rowActions(row) {
|
rowActions(row) {
|
||||||
const status = row.businessStatus;
|
const status = row.businessStatus;
|
||||||
@@ -2125,9 +2203,17 @@ export default {
|
|||||||
|
|
||||||
.loading-manage-page__sub-nos {
|
.loading-manage-page__sub-nos {
|
||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
line-height: 20px;
|
||||||
text-overflow: ellipsis;
|
white-space: normal;
|
||||||
white-space: nowrap;
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-manage-page__sub-no-link {
|
||||||
|
display: inline;
|
||||||
|
white-space: normal;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-manage-page__address-cell {
|
.loading-manage-page__address-cell {
|
||||||
|
|||||||
@@ -376,8 +376,13 @@ export default {
|
|||||||
: 0;
|
: 0;
|
||||||
},
|
},
|
||||||
async download() {
|
async download() {
|
||||||
const blob = await api.exportList(this.query);
|
const response = await api.exportList(this.query);
|
||||||
const url = URL.createObjectURL(new Blob([blob.data || blob]));
|
const blob = response.data || response;
|
||||||
|
if (!(blob instanceof Blob) || !blob.size) {
|
||||||
|
this.$message.error('导出失败,未生成有效文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = '总单运单明细.xlsx';
|
link.download = '总单运单明细.xlsx';
|
||||||
|
|||||||
@@ -182,7 +182,7 @@
|
|||||||
accept=".zip,.7z"
|
accept=".zip,.7z"
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
:limit="1"
|
:limit="1"
|
||||||
:on-change="uploadVoucher"
|
:on-change="selectVoucherFile"
|
||||||
:on-remove="clearFile"
|
:on-remove="clearFile"
|
||||||
><el-button
|
><el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -429,7 +429,8 @@ const loading = ref(false),
|
|||||||
closingProgressDialog = ref(false),
|
closingProgressDialog = ref(false),
|
||||||
cancelledTaskIds = ref(new Set()),
|
cancelledTaskIds = ref(new Set()),
|
||||||
uploadCancelled = ref(false),
|
uploadCancelled = ref(false),
|
||||||
activeUploadTaskId = ref();
|
activeUploadTaskId = ref(),
|
||||||
|
selectedVoucherFile = ref();
|
||||||
const query = reactive({
|
const query = reactive({
|
||||||
voucherBatchNo: '',
|
voucherBatchNo: '',
|
||||||
auditStatus: '',
|
auditStatus: '',
|
||||||
@@ -503,6 +504,7 @@ const changeProject = projectId => {
|
|||||||
};
|
};
|
||||||
const openUpload = async row => {
|
const openUpload = async row => {
|
||||||
await loadProjects();
|
await loadProjects();
|
||||||
|
selectedVoucherFile.value = undefined;
|
||||||
Object.assign(editing, {
|
Object.assign(editing, {
|
||||||
id: row?.id || '',
|
id: row?.id || '',
|
||||||
voucherBatchNo: row?.voucherBatchNo || '',
|
voucherBatchNo: row?.voucherBatchNo || '',
|
||||||
@@ -524,7 +526,10 @@ const openUpload = async row => {
|
|||||||
editing.waybillImportBatchIds = selectedBatches.value.map(item => item.id);
|
editing.waybillImportBatchIds = selectedBatches.value.map(item => item.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const clearFile = () => Object.assign(editing, { fileName: '', fileUrl: '', fileTaskId: '' });
|
const clearFile = () => {
|
||||||
|
selectedVoucherFile.value = undefined;
|
||||||
|
Object.assign(editing, { fileName: '', fileUrl: '', fileTaskId: '' });
|
||||||
|
};
|
||||||
const isCurrentUploadTask = taskId =>
|
const isCurrentUploadTask = taskId =>
|
||||||
taskId && activeUploadTaskId.value && String(taskId) === String(activeUploadTaskId.value);
|
taskId && activeUploadTaskId.value && String(taskId) === String(activeUploadTaskId.value);
|
||||||
const stopCurrentUpload = async taskId => {
|
const stopCurrentUpload = async taskId => {
|
||||||
@@ -547,11 +552,11 @@ const stopCurrentUpload = async taskId => {
|
|||||||
ElMessage.error(error.message || '暂停上传失败');
|
ElMessage.error(error.message || '暂停上传失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const closeUploadDialog = async () => {
|
const closeUploadDialog = async (pauseUpload = true) => {
|
||||||
if (closingUploadDialog.value) return;
|
if (closingUploadDialog.value) return;
|
||||||
closingUploadDialog.value = true;
|
closingUploadDialog.value = true;
|
||||||
try {
|
try {
|
||||||
await stopCurrentUpload(editing.fileTaskId);
|
if (pauseUpload) await stopCurrentUpload(editing.fileTaskId);
|
||||||
uploadVisible.value = false;
|
uploadVisible.value = false;
|
||||||
} finally {
|
} finally {
|
||||||
closingUploadDialog.value = false;
|
closingUploadDialog.value = false;
|
||||||
@@ -637,6 +642,7 @@ const uploadParts = async (file, task, sessionId) => {
|
|||||||
editing.fileName = file.name;
|
editing.fileName = file.name;
|
||||||
editing.fileTaskId = task.id;
|
editing.fileTaskId = task.id;
|
||||||
editing.fileUrl = fileUrlRes.data?.data || '';
|
editing.fileUrl = fileUrlRes.data?.data || '';
|
||||||
|
if (!editing.fileUrl) throw new Error('未获取到上传文件地址,请稍后重试');
|
||||||
await api.completeUploadFile({
|
await api.completeUploadFile({
|
||||||
voucherBatchNo: task.businessId,
|
voucherBatchNo: task.businessId,
|
||||||
fileTaskId: task.id,
|
fileTaskId: task.id,
|
||||||
@@ -730,14 +736,23 @@ const uploadVoucher = async file => {
|
|||||||
cancelledTaskIds.value.delete(String(task.id));
|
cancelledTaskIds.value.delete(String(task.id));
|
||||||
resumeTarget.value = undefined;
|
resumeTarget.value = undefined;
|
||||||
await uploadParts(raw, task, sessionId);
|
await uploadParts(raw, task, sessionId);
|
||||||
|
return Boolean(editing.fileUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name !== 'AbortError')
|
if (error.name !== 'AbortError')
|
||||||
ElMessage.error(error.message || '上传中断,可在上传进度中继续上传');
|
ElMessage.error(error.message || '上传中断,可在上传进度中继续上传');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const selectVoucherFile = file => {
|
||||||
|
if (!file?.raw) return;
|
||||||
|
selectedVoucherFile.value = file.raw;
|
||||||
|
editing.fileName = file.raw.name;
|
||||||
|
editing.fileUrl = '';
|
||||||
|
editing.fileTaskId = '';
|
||||||
|
};
|
||||||
watch(uploadVisible, visible => {
|
watch(uploadVisible, visible => {
|
||||||
// 兜底处理:右上角、遮罩、Esc 直接修改 v-model 时仍必须停止上传。
|
// 兜底处理:右上角、遮罩、Esc 直接修改 v-model 时仍必须停止上传。
|
||||||
if (!visible) void stopCurrentUpload(editing.fileTaskId);
|
if (!visible && !closingUploadDialog.value) void stopCurrentUpload(editing.fileTaskId);
|
||||||
});
|
});
|
||||||
const openBatchDialog = async () => {
|
const openBatchDialog = async () => {
|
||||||
batchVisible.value = true;
|
batchVisible.value = true;
|
||||||
@@ -777,21 +792,23 @@ const removeBatch = row => {
|
|||||||
const submitUpload = async () => {
|
const submitUpload = async () => {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
if (uploading.value && editing.fileTaskId) {
|
|
||||||
await stopCurrentUpload(editing.fileTaskId);
|
|
||||||
}
|
|
||||||
const valid = await uploadFormRef.value.validate().catch(() => false);
|
const valid = await uploadFormRef.value.validate().catch(() => false);
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
if (!editing.fileUrl && !editing.fileTaskId) {
|
if (!editing.fileUrl) {
|
||||||
|
if (!selectedVoucherFile.value) {
|
||||||
ElMessage.warning('请先选择执行凭证');
|
ElMessage.warning('请先选择执行凭证');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const uploaded = await uploadVoucher({ raw: selectedVoucherFile.value });
|
||||||
|
if (!uploaded) return;
|
||||||
|
selectedVoucherFile.value = undefined;
|
||||||
|
}
|
||||||
await api.submit({
|
await api.submit({
|
||||||
...editing,
|
...editing,
|
||||||
waybillImportBatchIds: selectedBatches.value.map(item => item.id),
|
waybillImportBatchIds: selectedBatches.value.map(item => item.id),
|
||||||
});
|
});
|
||||||
ElMessage.success('提交成功');
|
ElMessage.success('提交成功');
|
||||||
await closeUploadDialog();
|
await closeUploadDialog(false);
|
||||||
load();
|
load();
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false;
|
saving.value = false;
|
||||||
|
|||||||
@@ -218,11 +218,11 @@
|
|||||||
<el-input v-model="itemForm.itemName" maxlength="20" />
|
<el-input v-model="itemForm.itemName" maxlength="20" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="分值" required>
|
<el-form-item label="分值" required>
|
||||||
<el-input-number v-model="itemForm.score" :min="0" :precision="2" :controls="false" />
|
<el-input-number v-model="itemForm.score" :min="1" :precision="0" :step="1" :controls="false" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
<el-form-item label="得分说明">
|
<el-form-item label="得分说明">
|
||||||
<el-input v-model="itemForm.scoreDescription" maxlength="300" />
|
<el-input v-model="itemForm.scoreDescription" maxlength="300" class="score-description-input" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="选项描述">
|
<el-form-item label="选项描述">
|
||||||
<el-input v-model="itemForm.optionDescription" maxlength="100" />
|
<el-input v-model="itemForm.optionDescription" maxlength="100" />
|
||||||
@@ -232,7 +232,7 @@
|
|||||||
<div class="option-row" v-for="(option, index) in itemForm.options" :key="index">
|
<div class="option-row" v-for="(option, index) in itemForm.options" :key="index">
|
||||||
<el-input v-model="option.optionName" maxlength="100" />
|
<el-input v-model="option.optionName" maxlength="100" />
|
||||||
<div class="option-score">
|
<div class="option-score">
|
||||||
<el-input-number v-model="option.score" :min="0" :precision="2" :controls="false" />
|
<el-input-number v-model="option.score" :min="1" :precision="0" :step="1" :controls="false" />
|
||||||
<span class="score-unit">分</span>
|
<span class="score-unit">分</span>
|
||||||
</div>
|
</div>
|
||||||
<el-link
|
<el-link
|
||||||
@@ -534,10 +534,10 @@ export default {
|
|||||||
emptyItem() {
|
emptyItem() {
|
||||||
return {
|
return {
|
||||||
itemName: '',
|
itemName: '',
|
||||||
score: 0,
|
score: 1,
|
||||||
scoreDescription: '',
|
scoreDescription: '',
|
||||||
optionDescription: '',
|
optionDescription: '',
|
||||||
options: [{ optionName: '', score: 0 }],
|
options: [{ optionName: '', score: 1 }],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
emptyStandard() {
|
emptyStandard() {
|
||||||
@@ -561,7 +561,7 @@ export default {
|
|||||||
items: (category.items || []).map(item => ({
|
items: (category.items || []).map(item => ({
|
||||||
...item,
|
...item,
|
||||||
options:
|
options:
|
||||||
item.options && item.options.length ? item.options : [{ optionName: '', score: 0 }],
|
item.options && item.options.length ? item.options : [{ optionName: '', score: 1 }],
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -624,6 +624,10 @@ export default {
|
|||||||
this.$message.warning('请输入分值');
|
this.$message.warning('请输入分值');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!this.isPositiveInteger(this.itemForm.score)) {
|
||||||
|
this.$message.warning('分值只能输入正整数');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.itemForm.options.length) {
|
if (!this.itemForm.options.length) {
|
||||||
this.$message.warning('至少添加1条档位选项');
|
this.$message.warning('至少添加1条档位选项');
|
||||||
return;
|
return;
|
||||||
@@ -639,12 +643,18 @@ export default {
|
|||||||
this.$message.warning('请完整填写档位选项');
|
this.$message.warning('请完整填写档位选项');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.itemForm.options.some(option => !this.isPositiveInteger(option.score))) {
|
||||||
|
this.$message.warning('选项分数只能输入正整数');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const item = JSON.parse(JSON.stringify(this.itemForm));
|
const item = JSON.parse(JSON.stringify(this.itemForm));
|
||||||
item.itemName = String(item.itemName || '').trim();
|
item.itemName = String(item.itemName || '').trim();
|
||||||
item.options = item.options.map(option => ({
|
item.options = item.options.map(option => ({
|
||||||
...option,
|
...option,
|
||||||
optionName: String(option.optionName || '').trim(),
|
optionName: String(option.optionName || '').trim(),
|
||||||
|
score: Number(option.score),
|
||||||
}));
|
}));
|
||||||
|
item.score = Number(item.score);
|
||||||
if (this.currentItemIndex >= 0) {
|
if (this.currentItemIndex >= 0) {
|
||||||
this.currentCategory.items.splice(this.currentItemIndex, 1, item);
|
this.currentCategory.items.splice(this.currentItemIndex, 1, item);
|
||||||
} else {
|
} else {
|
||||||
@@ -660,7 +670,7 @@ export default {
|
|||||||
this.currentCategory.items.splice(index, 1);
|
this.currentCategory.items.splice(index, 1);
|
||||||
},
|
},
|
||||||
addOption() {
|
addOption() {
|
||||||
this.itemForm.options.push({ optionName: '', score: 0 });
|
this.itemForm.options.push({ optionName: '', score: 1 });
|
||||||
},
|
},
|
||||||
removeOption(index) {
|
removeOption(index) {
|
||||||
this.itemForm.options.splice(index, 1);
|
this.itemForm.options.splice(index, 1);
|
||||||
@@ -779,6 +789,9 @@ export default {
|
|||||||
isBlankValue(value) {
|
isBlankValue(value) {
|
||||||
return value === undefined || value === null || value === '';
|
return value === undefined || value === null || value === '';
|
||||||
},
|
},
|
||||||
|
isPositiveInteger(value) {
|
||||||
|
return /^\d+$/.test(String(value)) && Number(value) > 0;
|
||||||
|
},
|
||||||
hasRangeOverlap(list, lowerProp, upperProp) {
|
hasRangeOverlap(list, lowerProp, upperProp) {
|
||||||
const ranges = list
|
const ranges = list
|
||||||
.map(item => ({
|
.map(item => ({
|
||||||
@@ -810,10 +823,18 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (const item of category.items) {
|
for (const item of category.items) {
|
||||||
|
if (!this.isPositiveInteger(item.score)) {
|
||||||
|
this.$message.warning(`${item.itemName || '评分项目'}的分值只能为正整数`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!item.options || item.options.length === 0) {
|
if (!item.options || item.options.length === 0) {
|
||||||
this.$message.warning(`${item.itemName}至少存在1条档位选项`);
|
this.$message.warning(`${item.itemName}至少存在1条档位选项`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (item.options.some(option => !this.isPositiveInteger(option.score))) {
|
||||||
|
this.$message.warning(`${item.itemName || '评分项目'}的选项分数只能为正整数`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const creditLevels = this.configForm.standards
|
const creditLevels = this.configForm.standards
|
||||||
@@ -842,6 +863,10 @@ export default {
|
|||||||
this.$message.warning('发布前请完整填写评分项目名称');
|
this.$message.warning('发布前请完整填写评分项目名称');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!this.isPositiveInteger(item.score)) {
|
||||||
|
this.$message.warning(`${item.itemName || '评分项目'}的分值只能为正整数`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!item.options || item.options.length === 0) {
|
if (!item.options || item.options.length === 0) {
|
||||||
this.$message.warning(`${item.itemName || '评分项目'}至少配置1个档位选项`);
|
this.$message.warning(`${item.itemName || '评分项目'}至少配置1个档位选项`);
|
||||||
return false;
|
return false;
|
||||||
@@ -853,6 +878,10 @@ export default {
|
|||||||
this.$message.warning(`${item.itemName || '评分项目'}的档位选项未填写完整`);
|
this.$message.warning(`${item.itemName || '评分项目'}的档位选项未填写完整`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (item.options.some(option => !this.isPositiveInteger(option.score))) {
|
||||||
|
this.$message.warning(`${item.itemName || '评分项目'}的选项分数只能为正整数`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1103,6 +1132,10 @@ export default {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.score-description-input .el-input__inner) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
&__top {
|
&__top {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 360px 360px;
|
grid-template-columns: 360px 360px;
|
||||||
|
|||||||
@@ -564,9 +564,9 @@
|
|||||||
|
|
||||||
<section-card title="客商材料">
|
<section-card title="客商材料">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div v-if="!readonly" class="section-actions">
|
<div class="section-actions">
|
||||||
<el-button icon="el-icon-download" @click="downloadAttachments">批量下载</el-button>
|
<el-button icon="el-icon-download" @click="downloadAttachments">批量下载</el-button>
|
||||||
<el-button type="primary" icon="el-icon-upload" @click="addAttachment">
|
<el-button v-if="!readonly" type="primary" icon="el-icon-upload" @click="addAttachment">
|
||||||
OCR上传识别
|
OCR上传识别
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1860,7 +1860,7 @@ export default {
|
|||||||
const page = this.changeRecordPage;
|
const page = this.changeRecordPage;
|
||||||
getChangeRecordList(this.archiveForm.id, page.currentPage, page.pageSize).then(res => {
|
getChangeRecordList(this.archiveForm.id, page.currentPage, page.pageSize).then(res => {
|
||||||
const data = res.data.data || {};
|
const data = res.data.data || {};
|
||||||
page.records = data.records || [];
|
page.records = this.replaceNegativeOneWithBlank(data.records || []);
|
||||||
page.total = Number(data.total || 0);
|
page.total = Number(data.total || 0);
|
||||||
page.currentPage = Number(data.current || page.currentPage);
|
page.currentPage = Number(data.current || page.currentPage);
|
||||||
});
|
});
|
||||||
@@ -1883,10 +1883,11 @@ export default {
|
|||||||
return (this.changeRecordPage.currentPage - 1) * this.changeRecordPage.pageSize + index + 1;
|
return (this.changeRecordPage.currentPage - 1) * this.changeRecordPage.pageSize + index + 1;
|
||||||
},
|
},
|
||||||
formatChangeData(row, column, value) {
|
formatChangeData(row, column, value) {
|
||||||
if (!value) return '-';
|
if (!value || value === '-1') return '';
|
||||||
try {
|
try {
|
||||||
return Object.entries(JSON.parse(value))
|
const data = this.replaceNegativeOneWithBlank(JSON.parse(value));
|
||||||
.map(([field, fieldValue]) => `${field}:${fieldValue ?? '-'}`)
|
return Object.entries(data)
|
||||||
|
.map(([field, fieldValue]) => `${field}:${fieldValue ?? ''}`)
|
||||||
.join(';');
|
.join(';');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return value;
|
return value;
|
||||||
@@ -2866,6 +2867,19 @@ export default {
|
|||||||
handleQualificationSelectionChange(rows) {
|
handleQualificationSelectionChange(rows) {
|
||||||
this.selectedQualificationFiles = rows || [];
|
this.selectedQualificationFiles = rows || [];
|
||||||
},
|
},
|
||||||
|
replaceNegativeOneWithBlank(value) {
|
||||||
|
if (value === -1 || value === '-1') return '';
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(item => this.replaceNegativeOneWithBlank(item));
|
||||||
|
}
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return Object.keys(value).reduce((result, key) => {
|
||||||
|
result[key] = this.replaceNegativeOneWithBlank(value[key]);
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
normalizeDetail(detail) {
|
normalizeDetail(detail) {
|
||||||
const businessScope = this.splitValue(detail.businessScope);
|
const businessScope = this.splitValue(detail.businessScope);
|
||||||
const registeredDetailAddress =
|
const registeredDetailAddress =
|
||||||
@@ -2970,12 +2984,15 @@ export default {
|
|||||||
this.resetChangeRecordPage();
|
this.resetChangeRecordPage();
|
||||||
if (row && row.id) {
|
if (row && row.id) {
|
||||||
getDetail(row.id).then(res => {
|
getDetail(row.id).then(res => {
|
||||||
this.archiveForm = this.normalizeDetail(res.data.data);
|
const archive = this.normalizeDetail(res.data.data);
|
||||||
|
this.archiveForm = readonly ? this.replaceNegativeOneWithBlank(archive) : archive;
|
||||||
this.originalAccessType = this.archiveForm.accessType || '';
|
this.originalAccessType = this.archiveForm.accessType || '';
|
||||||
this.qualificationFiles = this.parseAttachments(
|
const qualificationFiles = this.parseAttachments(this.archiveForm.qualificationAttachments);
|
||||||
this.archiveForm.qualificationAttachments
|
this.qualificationFiles = readonly
|
||||||
);
|
? this.replaceNegativeOneWithBlank(qualificationFiles)
|
||||||
|
: qualificationFiles;
|
||||||
this.qualificationUploadFiles = [];
|
this.qualificationUploadFiles = [];
|
||||||
|
this.selectedQualificationFiles = [];
|
||||||
this.archiveBox = true;
|
this.archiveBox = true;
|
||||||
this.loadChangeRecords();
|
this.loadChangeRecords();
|
||||||
});
|
});
|
||||||
@@ -3516,7 +3533,9 @@ export default {
|
|||||||
this.handleScoreCurrentChange(1);
|
this.handleScoreCurrentChange(1);
|
||||||
},
|
},
|
||||||
formatScoreValue(value) {
|
formatScoreValue(value) {
|
||||||
return value === undefined || value === null || value === '' ? '' : value;
|
return value === undefined || value === null || value === '' || value === -1 || value === '-1'
|
||||||
|
? ''
|
||||||
|
: value;
|
||||||
},
|
},
|
||||||
rowDel(row) {
|
rowDel(row) {
|
||||||
this.$confirm('仅草稿状态客商可删除,确定删除该数据?', {
|
this.$confirm('仅草稿状态客商可删除,确定删除该数据?', {
|
||||||
|
|||||||
Reference in New Issue
Block a user