1、新增总单
2、fix bug
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import request from '@/axios';
|
||||
|
||||
const baseUrl = '/blade-transport/master-order';
|
||||
|
||||
export const getList = (current, size, params) =>
|
||||
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
|
||||
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
|
||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||
export const saveDraft = data => request({ url: `${baseUrl}/draft`, method: 'post', data });
|
||||
export const copy = id => request({ url: `${baseUrl}/copy`, method: 'post', params: { id } });
|
||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||
export const closeDispatch = id =>
|
||||
request({ url: `${baseUrl}/close-dispatch`, method: 'post', params: { id } });
|
||||
export const dispatch = data => request({ url: `${baseUrl}/dispatch`, method: 'post', data });
|
||||
export const exportList = params =>
|
||||
request({ url: `${baseUrl}/export`, method: 'get', params, responseType: 'blob' });
|
||||
export const getAvailableProjects = () =>
|
||||
request({
|
||||
url: '/blade-transport/project-apply/list',
|
||||
method: 'get',
|
||||
params: {
|
||||
current: 1,
|
||||
size: 9999,
|
||||
approvalStatuses: 'approved,change_approved',
|
||||
},
|
||||
});
|
||||
export const getProjectContracts = projectId =>
|
||||
request({
|
||||
url: '/blade-transport/contract-manage/list',
|
||||
method: 'get',
|
||||
params: { current: 1, size: 9999, projectId },
|
||||
});
|
||||
@@ -124,6 +124,7 @@ export const config = {
|
||||
enableContractSelect: true,
|
||||
enableShippingPlanSelect: true,
|
||||
enableShippingInfoForm: true,
|
||||
fixedTransportAddressType: true,
|
||||
enableTaskInfoForm: true,
|
||||
enableWaybillFooterFreightSummary: true,
|
||||
enableDraftSave: true,
|
||||
|
||||
@@ -3755,7 +3755,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="站点类型">
|
||||
<el-form-item label="地址类型">
|
||||
<el-input :model-value="transportStationTypeLabel" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -4254,6 +4254,10 @@ export default {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
detailId: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -4920,6 +4924,14 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
detailId: {
|
||||
immediate: true,
|
||||
handler(id) {
|
||||
if (id !== undefined && id !== null && id !== '') {
|
||||
this.$nextTick(() => this.openDetail({ id }));
|
||||
}
|
||||
},
|
||||
},
|
||||
'form.transportType'(value, oldValue) {
|
||||
if (!this.shippingInfoFormEnabled || this.suppressTransportTypeClear) return;
|
||||
if (String(value ?? '') === String(oldValue ?? '')) return;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<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>
|
||||
<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 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>
|
||||
<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>
|
||||
</section>
|
||||
</section>
|
||||
<section v-if="master" class="master-goods-card">
|
||||
<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-empty v-if="!(master.goods || []).length" description="暂无货物信息" :image-size="56" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/business/master-order';
|
||||
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||
|
||||
export default {
|
||||
props: { id: [String, Number] },
|
||||
emits: ['back'],
|
||||
data() { return { loading: false, master: null, expandedSegments: {}, ArrowDown, ArrowUp }; },
|
||||
computed: {
|
||||
segments() {
|
||||
const routes = this.master?.routeProgress || this.master?.routes || [];
|
||||
return routes.map((route, index) => {
|
||||
const previous = index ? routes[index - 1] : this.master;
|
||||
return { ...route, departureName: previous?.departureName, departureAddress: previous?.departureAddress, arrivalName: route.departureName || this.master?.arrivalName, arrivalAddress: route.departureAddress || this.master?.arrivalAddress };
|
||||
});
|
||||
},
|
||||
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)}` : '-'; },
|
||||
},
|
||||
async mounted() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await api.getDetail(this.id);
|
||||
this.master = res.data?.data || res.data || res;
|
||||
this.expandedSegments = (this.master.routeProgress || this.master.routes || []).reduce((result, route) => {
|
||||
result[route.segmentNo] = Boolean(route.waybills?.length);
|
||||
return result;
|
||||
}, {});
|
||||
} finally { this.loading = false; }
|
||||
},
|
||||
methods: {
|
||||
quantity(value) { const number = Number(value || 0); return Number.isInteger(number) ? number : number.toFixed(3).replace(/\.?0+$/, ''); },
|
||||
segmentNumber(route, index) { return String(route.segmentNo || '').replace(/^段/, '') || index + 1; },
|
||||
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)); },
|
||||
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 } }); },
|
||||
waybillStatusName(value) { return ({ pending: '待执行', running: '进行中', completed: '已完成', cancelled: '已取消' })[value] || value || '-'; },
|
||||
waybillStatusType(value) { return ({ pending: 'info', running: 'warning', completed: 'success', cancelled: 'danger' })[value] || 'info'; },
|
||||
statusName(value) { return ({ waiting_dispatch: '待调度', dispatching: '调度中', completed: '调度完成', closed: '调度关闭' })[value] || value || '-'; },
|
||||
statusType(value) { return ({ waiting_dispatch: 'warning', dispatching: 'success', completed: 'primary', closed: 'danger' })[value] || 'info'; },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.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; } }
|
||||
.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__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; }
|
||||
.segment-stats { display: flex; gap: 72px; padding: 4px 0 24px; div { display: flex; flex-direction: column; gap: 8px; } span { color: #8a9bb8; font-size: 14px; } strong { color: #1f2d3d; font-size: 32px; line-height: 1; } small { margin-left: 6px; color: #8a9bb8; font-size: 16px; font-weight: 400; } .dispatched { color: #16a34a; } .remaining { color: #f59e0b; } }
|
||||
.waybill-table { :deep(th.el-table__cell) { background: #f5f7fa; color: #60738f; } }
|
||||
.transport-plan-detail { margin-top: 20px; h4 { margin: 0 0 12px; padding-left: 12px; border-left: 4px solid #409eff; font-size: 15px; } }
|
||||
.master-goods-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
||||
.master-goods-card__heading { padding: 20px 24px 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
||||
@media screen and (max-width: 1200px) { .detail-meta { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
</style>
|
||||
@@ -0,0 +1,385 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="master-dispatch">
|
||||
<section v-if="master" class="master-overview">
|
||||
<div class="overview-heading">
|
||||
<div>
|
||||
<h2>多联总单调度 <span>|</span> {{ master.masterNo }}</h2>
|
||||
<el-tag :type="statusType(master.businessStatus)">{{ statusName(master.businessStatus) }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-content">
|
||||
<dl class="overview-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>
|
||||
<div class="route-overview">
|
||||
<template v-for="(route, index) in routes" :key="route.segmentNo">
|
||||
<div class="route-overview__node">
|
||||
<span :class="['route-badge', index === 0 ? 'start' : 'middle']">{{ index === 0 ? '起' : '经' }}</span>
|
||||
<strong>{{ route.departureName || '-' }}</strong>
|
||||
<small>{{ route.departureAddress || '-' }}</small>
|
||||
<small>{{ contactText(route.departureContact, route.departurePhone) }}</small>
|
||||
</div>
|
||||
<div class="route-overview__line"></div>
|
||||
<div v-if="index === routes.length - 1" class="route-overview__node">
|
||||
<span class="route-badge end">终</span>
|
||||
<strong>{{ route.arrivalName || '-' }}</strong>
|
||||
<small>{{ route.arrivalAddress || '-' }}</small>
|
||||
<small>{{ contactText(route.arrivalContact, route.arrivalPhone) }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-for="(route, index) in routes" :key="route.segmentNo" class="dispatch-section" :data-segment="route.segmentNo">
|
||||
<header class="segment-header">
|
||||
<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>
|
||||
</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>
|
||||
<el-col v-if="isRoad(route)" :span="8">
|
||||
<el-form-item label="单据类型" required>
|
||||
<el-radio-group v-model="route.documentType">
|
||||
<el-radio value="计划单">计划单</el-radio><el-radio value="运单">运单</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<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-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>
|
||||
<el-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.arrivalPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="4"><el-form-item :label="dateEndLabel(route)" required><el-date-picker v-model="route.estimatedEndTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<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 prop="quantityUnit" label="数量单位" width="110" />
|
||||
<el-table-column prop="packageType" label="包装" min-width="110" />
|
||||
<el-table-column prop="brand" label="品牌" min-width="110" />
|
||||
<el-table-column prop="specification" label="规格" min-width="110" />
|
||||
<el-table-column prop="model" label="型号" min-width="110" />
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
||||
<el-table-column label="操作" width="90" fixed="right"><template #default="{ $index }"><el-link type="danger" @click="removeGoods(route, $index)">删除</el-link></template></el-table-column>
|
||||
</el-table>
|
||||
|
||||
<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>
|
||||
</el-form>
|
||||
|
||||
<template v-if="route.documentType === '运单'">
|
||||
<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-row :gutter="16">
|
||||
<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="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="车牌号" required><el-input v-model="route.vehicleNo" 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.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="备注"><el-input v-model="route.remark" maxlength="200" show-word-limit placeholder="请输入" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
<div class="segment-actions"><el-button plain @click="addToPending(route, true)">加入并继续调度</el-button><el-button plain @click="addToPending(route, false)">加入调度清单</el-button></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="dispatch-bottom">
|
||||
<section class="pending-panel">
|
||||
<div class="pending-bar">
|
||||
<div class="pending-summary">
|
||||
<strong>待提交 <em>{{ pending.length }}</em> 条 <span class="pending-summary-text">({{ pendingSegmentSummary }})</span></strong>
|
||||
<el-link type="primary" @click="pendingExpanded = !pendingExpanded">{{ pendingExpanded ? '收起待调度列表' : '展开待调度列表' }}</el-link>
|
||||
</div>
|
||||
<div class="pending-actions"><el-button @click="$emit('back')">返回</el-button><el-button type="primary" :loading="submitting" @click="submit">确认调度</el-button></div>
|
||||
</div>
|
||||
<div v-show="pendingExpanded" class="pending-list">
|
||||
<template v-for="(items, segmentNo) in pendingGroups" :key="segmentNo">
|
||||
<h3>{{ segmentNo }}:{{ items[0].departureName }} → {{ items[0].arrivalName }}</h3>
|
||||
<el-table :data="items" border><el-table-column type="index" label="序号" width="70" /><el-table-column label="单据类型" prop="documentType" width="100" /><el-table-column label="货物" min-width="220"><template #default="{ row }">{{ row.cargoName }}{{ row.cargoType ? `,${row.cargoType}` : '' }}</template></el-table-column><el-table-column label="本次数量" width="150"><template #default="{ row }">{{ formatQuantity(row.quantity) }} {{ row.quantityUnit }}</template></el-table-column><el-table-column prop="carrierName" label="承运商" min-width="180" /><el-table-column prop="driverName" label="司机" width="130" /><el-table-column prop="vehicleNo" label="车牌" width="130" /><el-table-column label="操作" width="120" fixed="right"><template #default="{ row }"><div class="table-actions"><el-link type="primary" @click="editPending(row)">编辑</el-link><el-link type="danger" @click="removePending(row.id)">删除</el-link></div></template></el-table-column></el-table>
|
||||
</template>
|
||||
<el-empty v-if="!pending.length" description="暂无待提交调度条目" :image-size="60" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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';
|
||||
|
||||
const unwrapRecords = 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 [];
|
||||
};
|
||||
|
||||
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 };
|
||||
},
|
||||
computed: {
|
||||
totalQuantity() { return (this.master?.goods || []).reduce((sum, item) => sum + Number(item.quantity || 0), 0); },
|
||||
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('、') || '-'; },
|
||||
masterGoods() { return (this.master?.goods || []).map((item, sourceIndex) => ({ ...item, sourceIndex, label: [item.cargoName, 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)}` : '-'; },
|
||||
pendingGroups() { return this.pending.reduce((groups, item) => { (groups[item.segmentNo] ||= []).push(item); return groups; }, {}); },
|
||||
pendingSegmentSummary() {
|
||||
return this.routes.map(route => {
|
||||
const items = this.pending.filter(item => item.segmentNo === route.segmentNo);
|
||||
if (!items.length) return `${route.segmentNo}:无`;
|
||||
const documentSummary = items.reduce((result, item) => {
|
||||
result[item.documentType] = (result[item.documentType] || 0) + 1;
|
||||
return result;
|
||||
}, {});
|
||||
return `${route.segmentNo}:${Object.entries(documentSummary).map(([type, count]) => `${type} x ${count}`).join('、')}`;
|
||||
}).join(' | ');
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await Promise.all([this.load(), this.loadCarrierOptions(), this.loadDriverOptions()]);
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
this.loading = true;
|
||||
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));
|
||||
} finally { this.loading = false; }
|
||||
},
|
||||
createRoute(node, index) {
|
||||
const previous = index ? this.master.routes[index - 1] : this.master;
|
||||
return {
|
||||
...node,
|
||||
selected: false,
|
||||
documentType: '运单',
|
||||
carrierType: '承运商',
|
||||
unitPrice: undefined,
|
||||
priceUnit: '元/吨',
|
||||
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: [],
|
||||
};
|
||||
},
|
||||
isRoad(route) { return String(route.transportType || '').includes('公路'); },
|
||||
remaining(route) { return Math.max(0, this.totalQuantity - Number(route.dispatchedQuantity || 0)); },
|
||||
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;
|
||||
},
|
||||
handleUnitPriceInput(route, value) {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
route.unitPrice = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||
},
|
||||
handleMileageInput(route, value) {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
route.mileage = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||
},
|
||||
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 = {}) {
|
||||
return {
|
||||
...goods,
|
||||
sourceIndex,
|
||||
remainingQuantity: Math.max(0, Number(goods.quantity || 0) - Number(dispatchedGoods[`${goods.cargoName || ''}\u0000${goods.cargoType || ''}`] || 0)),
|
||||
dispatchQuantity: undefined,
|
||||
};
|
||||
},
|
||||
addGoods(route) { route.goods.push(this.createGoodsRow()); },
|
||||
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 || {}));
|
||||
},
|
||||
removeGoods(route, index) { route.goods.splice(index, 1); },
|
||||
handleCarrierTypeChange(route, value) {
|
||||
route.carrierType = value;
|
||||
if (value === '承运商') {
|
||||
route.trailerVehicleNo = '';
|
||||
route.escortName = '';
|
||||
route.escortPhone = '';
|
||||
route.mileage = undefined;
|
||||
} else {
|
||||
route.carrierName = '';
|
||||
}
|
||||
},
|
||||
carrierOptionLabel(item) { return item.customerName || item.carrierName || item.fullName || item.name || ''; },
|
||||
carrierOptionKey(item) { return item.id || item.customerId || item.carrierId || this.carrierOptionLabel(item); },
|
||||
driverOptionLabel(item) { return item.driverName || item.name || ''; },
|
||||
driverOptionKey(item) { return item.id || item.driverId || this.driverOptionLabel(item) || item.mobile; },
|
||||
handleDriverChange(route, value) {
|
||||
const driver = this.driverOptions.find(item => this.driverOptionLabel(item) === value);
|
||||
if (driver) route.driverPhone = driver.mobile || driver.driverPhone || driver.phone || '';
|
||||
},
|
||||
async loadCarrierOptions(keyword = '') {
|
||||
this.carrierLoading = true;
|
||||
try {
|
||||
this.carrierOptions = unwrapRecords(
|
||||
await getCustomerList(1, 20, { customerName: keyword, customerType: '承运商' })
|
||||
);
|
||||
} finally {
|
||||
this.carrierLoading = false;
|
||||
}
|
||||
},
|
||||
async loadDriverOptions(keyword = '') {
|
||||
this.driverLoading = true;
|
||||
try {
|
||||
this.driverOptions = unwrapRecords(await getDriverList(1, 20, { driverName: keyword }));
|
||||
} finally {
|
||||
this.driverLoading = false;
|
||||
}
|
||||
},
|
||||
setDriverInput(segmentNo, element) {
|
||||
if (element) this.driverInputs[segmentNo] = element;
|
||||
else delete this.driverInputs[segmentNo];
|
||||
},
|
||||
resetForNextDispatch(route) {
|
||||
route.driverName = '';
|
||||
route.driverPhone = '';
|
||||
route.vehicleNo = '';
|
||||
route.trailerVehicleNo = '';
|
||||
route.escortName = '';
|
||||
route.escortPhone = '';
|
||||
route.goods.forEach(item => { item.dispatchQuantity = undefined; });
|
||||
this.$nextTick(() => this.driverInputs[route.segmentNo]?.focus());
|
||||
},
|
||||
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 (!route.estimatedStartTime) return this.$message.warning(`请选择${this.dateStartLabel(route)}`);
|
||||
if (!route.estimatedEndTime) return this.$message.warning(`请选择${this.dateEndLabel(route)}`);
|
||||
if (route.documentType === '运单') {
|
||||
if (route.carrierType === '承运商' && (!route.carrierName || !route.vehicleNo)) 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()}`;
|
||||
goods.forEach(item => this.pending.push({
|
||||
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,
|
||||
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,
|
||||
}));
|
||||
this.editingId = null;
|
||||
this.pendingExpanded = true;
|
||||
if (continueDispatch) this.resetForNextDispatch(route);
|
||||
else {
|
||||
route.goods.forEach(item => { item.dispatchQuantity = undefined; });
|
||||
route.selected = false;
|
||||
}
|
||||
this.$message.success('已加入调度清单');
|
||||
},
|
||||
editPending(item) {
|
||||
const route = this.routes.find(routeItem => routeItem.segmentNo === item.segmentNo);
|
||||
if (!route) return;
|
||||
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 || {});
|
||||
route.goods.push(goods);
|
||||
}
|
||||
if (goods) goods.dispatchQuantity = item.quantity;
|
||||
Object.assign(route, item);
|
||||
this.editingId = item.id;
|
||||
this.removePending(item.id);
|
||||
this.$nextTick(() => document.querySelector(`[data-segment="${item.segmentNo}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
|
||||
},
|
||||
removePending(id) { this.pending = this.pending.filter(item => item.id !== id); if (this.editingId === id) this.editingId = null; },
|
||||
async submit() {
|
||||
if (!this.pending.length) return this.$message.warning('请加入待提交调度清单');
|
||||
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; }
|
||||
},
|
||||
statusName(value) { return ({ waiting_dispatch: '待调度', dispatching: '调度中', completed: '调度完成', closed: '调度关闭' })[value] || value || '-'; },
|
||||
statusType(value) { return ({ waiting_dispatch: 'warning', dispatching: 'success', completed: 'primary', closed: 'danger' })[value] || 'info'; },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.master-dispatch { min-height: 100%; padding-bottom: 124px; background: #fff; color: #303133; }
|
||||
.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; } }
|
||||
.overview-content { display: grid; grid-template-columns: minmax(400px, .9fr) minmax(620px, 1.6fr); gap: 28px; padding: 8px 24px 20px; }
|
||||
.overview-meta { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 24px; margin: 0; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
|
||||
.route-overview { display: flex; align-items: flex-start; justify-content: flex-end; padding-top: 8px; overflow-x: auto; }
|
||||
.route-overview__node { display: grid; flex: 0 0 130px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
|
||||
.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-overview__line { flex: 1 0 56px; min-width: 56px; height: 32px; margin-top: 0; border-bottom: 2px solid #606266; }
|
||||
.segment-header { min-height: 46px; padding: 0 20px; border-bottom: 1px solid #eff1f7; font-weight: 600; em { color: #409eff; font-style: normal; } }
|
||||
.segment-content { padding: 16px 24px; }
|
||||
.dispatch-form { :deep(.el-form-item) { margin-bottom: 16px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } }
|
||||
.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; } }
|
||||
.carrier-type-form { margin-top: 16px; }
|
||||
.carrier-form { padding-top: 8px; }
|
||||
.segment-actions { display: flex; justify-content: flex-end; gap: 12px; padding-top: 4px; }
|
||||
.dispatch-bottom { position: fixed; z-index: 10; bottom: 0; left: $sidebar_width; width: calc(100% - #{$sidebar_width}); border-top: 1px solid #eff1f7; background: #fff; box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04); }
|
||||
:global(.avue--collapse) .dispatch-bottom { left: $sidebar_collapse; width: calc(100% - #{$sidebar_collapse}); }
|
||||
:global(.avue-layout--horizontal) .dispatch-bottom { left: 0; width: 100%; }
|
||||
.pending-panel { background: #fff; }
|
||||
.pending-bar { min-height: 58px; padding: 0 32px; em { color: #409eff; font-style: normal; } }
|
||||
.pending-summary, .pending-actions { display: flex; align-items: center; }
|
||||
.pending-summary { gap: 16px; }
|
||||
.pending-summary-text { color: #606266; font-weight: 400; }
|
||||
.pending-actions { gap: 12px; margin-left: auto; }
|
||||
.pending-list { max-height: min(420px, calc(100vh - 130px)); padding: 0 32px 16px; overflow: auto; h3 { margin: 14px 0 10px; font-size: 14px; } }
|
||||
.table-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
@media screen and (max-width: 1200px) { .overview-content { grid-template-columns: 1fr; } .route-overview { justify-content: flex-start; } }
|
||||
@media screen and (max-width: 992px) { .dispatch-bottom { left: 0; width: 100%; } }
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -356,7 +356,7 @@
|
||||
<div class="loading-detail__heading">配载单详情</div>
|
||||
<div>{{ dialogForm.loadingNo || '-' }}</div>
|
||||
<div><el-tag :type="detailStatusType">{{ statusText(dialogForm) }}</el-tag></div>
|
||||
<div><el-tag type="warning">{{ dialogForm.transportType || '-' }}</el-tag></div>
|
||||
<div><el-tag type="warning">{{ transportTypeLabel(dialogForm.transportType) }}</el-tag></div>
|
||||
</div>
|
||||
<div class="loading-detail__task-row">
|
||||
<div class="loading-detail__field"><span class="loading-detail__label">任务信息</span><span class="loading-detail__value">{{ taskSummary }}</span></div>
|
||||
@@ -369,7 +369,7 @@
|
||||
</section-card>
|
||||
<section-card title="货物信息"><el-table :data="cargoRows" border height="220"><el-table-column type="index" label="序号" width="70"/><el-table-column prop="projectName" label="项目名称" min-width="140"/><el-table-column prop="cargoName" label="货物名称" min-width="140"/><el-table-column prop="cargoType" label="货物类型" min-width="120"/><el-table-column prop="packageType" label="包装" min-width="100"/><el-table-column prop="weight" label="重量(吨)" min-width="110"/><el-table-column prop="volume" label="体积(方)" min-width="110"/><el-table-column prop="quantity" label="数量" min-width="90"/><el-table-column prop="materialCode" label="物料编码" min-width="120"/><el-table-column prop="equipmentCode" label="设备编码" min-width="120"/><el-table-column prop="brand" label="品牌" min-width="120"/><el-table-column prop="specificationModel" label="规格型号" min-width="140"/><el-table-column prop="remark" label="备注" min-width="160"/><el-table-column prop="unitPrice" label="货物单价(元)" min-width="140"/><el-table-column prop="planDate" label="计划日期" min-width="130"/></el-table></section-card>
|
||||
<section-card title="关联运单信息"><el-table :data="selectedWaybillRows" border><el-table-column type="index" label="序号" width="70"/><el-table-column prop="waybillNo" label="运单号" min-width="150"><template #default="{ row }"><el-link type="primary">{{ row.waybillNo || '-' }}</el-link></template></el-table-column><el-table-column label="配载单号" min-width="150"><template #default>{{ dialogForm.loadingNo || '-' }}</template></el-table-column><el-table-column prop="projectName" label="项目名称" min-width="150"/><el-table-column prop="customerName" label="客户" min-width="140"/><el-table-column prop="departureAddress" label="发货地址" min-width="220" show-overflow-tooltip/><el-table-column prop="arrivalAddress" label="到货地址" min-width="220" show-overflow-tooltip/></el-table></section-card>
|
||||
<div class="loading-detail__bottom-grid"><section-card title="运输节点"><el-timeline v-if="processNodes.length"><el-timeline-item v-for="(item, index) in processNodes" :key="item.id || index" :timestamp="item.time || ''">{{ item.name || item.nodeName || item.label }}</el-timeline-item></el-timeline><el-empty v-else description="暂无运输节点" :image-size="50"/></section-card><section-card title="物流轨迹"><template #extra><div class="loading-detail__track-actions"><el-button plain @click="handleTrackAction('playback')">轨迹回放</el-button><el-button plain @click="handleTrackAction('locate')">实时定位</el-button></div></template><div ref="detailAmap" class="loading-detail__map"></div></section-card></div>
|
||||
<div class="loading-detail__bottom-grid"><section-card title="运输节点"><template #extra><div class="loading-detail__process-actions"><el-select v-model="processProjectId" filterable remote clearable placeholder="切换项目" :remote-method="loadProjectOptions" :loading="projectLoading" @visible-change="visible => visible && loadProjectOptions()" @change="handleProcessProjectChange"><el-option v-for="item in projectOptions" :key="item.id" :label="item.projectName || item.name" :value="item.id" /></el-select><el-button plain @click="handleProcessAction('supplement')">补录执行</el-button><el-button plain @click="handleProcessAction('batch')">批量补录</el-button></div></template><el-timeline v-if="processNodes.length"><el-timeline-item v-for="(item, index) in processNodes" :key="item.id || index" :timestamp="item.time || ''">{{ item.name || item.nodeName || item.label }}</el-timeline-item></el-timeline><el-empty v-else description="暂无运输节点" :image-size="50"/></section-card><section-card title="物流轨迹"><template #extra><div class="loading-detail__track-actions"><el-button plain @click="handleTrackAction('playback')">轨迹回放</el-button><el-button plain @click="handleTrackAction('locate')">实时定位</el-button></div></template><div ref="detailAmap" class="loading-detail__map"></div></section-card></div>
|
||||
</div>
|
||||
<el-form
|
||||
v-else
|
||||
@@ -1110,6 +1110,7 @@ export default {
|
||||
dataSourceOptions: loadingManageConfig.dataSourceOptions,
|
||||
config: loadingManageConfig,
|
||||
processNodes: [],
|
||||
processProjectId: '',
|
||||
routeChangeVisible: false,
|
||||
routeChangeTab: 'change',
|
||||
routeChangeRows: [],
|
||||
@@ -1285,6 +1286,8 @@ export default {
|
||||
...createDialogForm(),
|
||||
...(res.data?.data || res.data || {}),
|
||||
};
|
||||
this.processProjectId = this.dialogForm.projectId || '';
|
||||
await this.loadProjectOptions(this.dialogForm.projectName || '');
|
||||
await this.restoreWaybillRows(
|
||||
this.dialogForm.waybillIdsJson,
|
||||
this.dialogForm.loadingSubNos
|
||||
@@ -1322,6 +1325,7 @@ export default {
|
||||
this.candidatePage.total = 0;
|
||||
this.candidateSearchVisible = false;
|
||||
this.processNodes = [];
|
||||
this.processProjectId = '';
|
||||
},
|
||||
clearLoadingDialog() {
|
||||
this.resetDialogData();
|
||||
@@ -1346,19 +1350,21 @@ export default {
|
||||
}
|
||||
},
|
||||
async loadProcessNodes() {
|
||||
const waybillProcess = parseJsonArray(this.selectedWaybillRows[0]?.processJson);
|
||||
if (waybillProcess.length) {
|
||||
this.processNodes = waybillProcess;
|
||||
if (!this.processProjectId) {
|
||||
this.processNodes = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const rows = unwrapRecords(await getProcessConfigList(1, 20, { projectId: this.dialogForm.projectId, status: 1 }));
|
||||
const rows = unwrapRecords(await getProcessConfigList(1, 20, { projectId: this.processProjectId, status: 1 }));
|
||||
const config = rows.find(item => item.nodeConfigJson) || {};
|
||||
this.processNodes = parseJsonArray(config.nodeConfigJson);
|
||||
} catch (error) {
|
||||
this.processNodes = [];
|
||||
}
|
||||
},
|
||||
async handleProcessProjectChange() {
|
||||
await this.loadProcessNodes();
|
||||
},
|
||||
previewAttachment(file) {
|
||||
if (!file.url) {
|
||||
ElMessage.info('暂无可预览的附件地址');
|
||||
@@ -1400,6 +1406,9 @@ export default {
|
||||
handleTrackAction(action) {
|
||||
ElMessage.info(action === 'playback' ? '暂无可回放的物流轨迹' : '暂无车辆实时定位数据');
|
||||
},
|
||||
handleProcessAction(action) {
|
||||
ElMessage.info(action === 'supplement' ? '补录执行功能暂未接入' : '批量补录功能暂未接入');
|
||||
},
|
||||
openRouteChangeDialog() {
|
||||
this.routeChangeTab = 'change';
|
||||
this.routeChangeRemark = '';
|
||||
@@ -1868,6 +1877,10 @@ export default {
|
||||
handleMileageInput(value) {
|
||||
this.dialogForm.mileage = String(value || '').replace(/[^\d.]/g, '');
|
||||
},
|
||||
transportTypeLabel(value) {
|
||||
if (!value) return '-';
|
||||
return this.transportTypeOptions.find(item => String(item.value) === String(value))?.label || value;
|
||||
},
|
||||
async loadTransportTypeOptions() {
|
||||
if (this.transportTypeOptions.length) return;
|
||||
const res = await request({
|
||||
@@ -2214,6 +2227,8 @@ export default {
|
||||
.loading-detail__step-tags { display: flex; justify-content: center; gap: 4px; margin-top: 6px; }
|
||||
.loading-detail__bottom-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.loading-detail__track-actions { display: flex; gap: 8px; }
|
||||
.loading-detail__process-actions { display: flex; align-items: center; gap: 8px; }
|
||||
.loading-detail__process-actions .el-select { width: 180px; }
|
||||
.loading-detail__map { width: 100%; min-height: 260px; border: 1px solid #eff1f7; }
|
||||
}
|
||||
.loading-route-change-dialog { .loading-route-change-dialog__original { color: #a8abb2; line-height: 20px; margin-bottom: 4px; }.loading-route-change-dialog__location { cursor: pointer; color: #909399; }.loading-route-change-dialog__hint { color: #909399; }.loading-route-change-dialog__route-list { display: flex; flex-direction: column; gap: 8px; }.loading-route-change-dialog__route-item { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto 24px; align-items: center; gap: 12px; padding: 10px 12px; background: #f5f6f7; cursor: move; }.loading-route-change-dialog__route-type { width: 30px; height: 30px; border-radius: 50%; background: #dcdfe6; text-align: center; line-height: 30px; }.loading-route-change-dialog__tags { display: flex; gap: 4px; }.loading-route-change-dialog__record-content { white-space: normal; overflow-wrap: anywhere; line-height: 22px; } }
|
||||
|
||||
@@ -0,0 +1,549 @@
|
||||
<template>
|
||||
<basic-container class="master-order-page">
|
||||
<template v-if="mode === 'list'">
|
||||
<el-form :model="query" class="master-search" label-width="160px" @submit.prevent>
|
||||
<el-row :gutter="16">
|
||||
<el-col v-for="field in primaryFields" :key="field.prop" :span="6"
|
||||
><el-form-item :label="field.label"
|
||||
><el-input v-model="query[field.prop]" placeholder="请输入" /></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="6"
|
||||
><el-form-item label="状态"
|
||||
><el-select v-model="query.businessStatus" placeholder="全部"
|
||||
><el-option label="全部" value="" /><el-option
|
||||
v-for="item in statuses"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value" /></el-select></el-form-item
|
||||
></el-col>
|
||||
<template v-if="searchExpanded">
|
||||
<el-col v-for="field in secondaryFields" :key="field.prop" :span="6"
|
||||
><el-form-item :label="field.label"
|
||||
><el-input v-model="query[field.prop]" placeholder="请输入" /></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="6"
|
||||
><el-form-item label="计划开始日期"
|
||||
><el-date-picker
|
||||
v-model="query.planStartRange"
|
||||
type="datetimerange"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="请选择"
|
||||
end-placeholder="请选择" /></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="6"
|
||||
><el-form-item label="计划结束日期"
|
||||
><el-date-picker
|
||||
v-model="query.planEndRange"
|
||||
type="datetimerange"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="请选择"
|
||||
end-placeholder="请选择" /></el-form-item
|
||||
></el-col>
|
||||
</template>
|
||||
<el-col :span="24" class="search-actions"
|
||||
><el-button type="primary" @click="search">查询</el-button
|
||||
><el-button @click="reset">重置</el-button
|
||||
><el-link type="primary" @click="searchExpanded = !searchExpanded"
|
||||
><el-icon><component :is="searchExpanded ? ArrowUp : ArrowDown" /></el-icon
|
||||
>{{ searchExpanded ? '折叠' : '展开' }}</el-link
|
||||
></el-col
|
||||
>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<section class="master-list-panel">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="goCreate()">新建多联总单</el-button
|
||||
><el-button type="primary" plain @click="download">批量导出</el-button>
|
||||
</div>
|
||||
<div v-loading="loading" class="master-cards">
|
||||
<article v-for="row in records" :key="row.id" class="master-card">
|
||||
<section class="card-info">
|
||||
<div class="card-title-row">
|
||||
<el-link type="primary" @click="goDetail(row.id)">{{ row.masterNo }}</el-link
|
||||
><el-tag :type="statusType(row.businessStatus)" class="status-tag">{{
|
||||
statusName(row.businessStatus)
|
||||
}}</el-tag>
|
||||
</div>
|
||||
<p class="project-name">{{ row.projectName || '-' }}</p>
|
||||
<p>{{ timeRange(row) }}</p>
|
||||
<p>{{ goodsSummary(row) }}</p>
|
||||
</section>
|
||||
<section class="route-progress">
|
||||
<div class="route-track">
|
||||
<template v-for="(node, index) in routeNodes(row)" :key="node.key">
|
||||
<div class="route-point">
|
||||
<span :class="['route-node', node.type]">{{ node.text }}</span>
|
||||
<div class="route-detail">
|
||||
<strong>{{ node.name }}</strong>
|
||||
<span v-for="line in node.lines" :key="line">{{ line }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="index < routeNodes(row).length - 1" class="route-connector">
|
||||
<small>{{ (row.routeProgress || [])[index]?.transportType }}</small>
|
||||
<el-progress
|
||||
:percentage="progress((row.routeProgress || [])[index], row.totalQuantity)"
|
||||
:show-text="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
<aside class="card-actions">
|
||||
<header>操作</header>
|
||||
<el-link
|
||||
v-for="action in actions(row)"
|
||||
:key="action.text"
|
||||
:type="action.danger ? 'danger' : 'primary'"
|
||||
@click="action.run"
|
||||
>{{ action.text }}</el-link
|
||||
>
|
||||
</aside>
|
||||
</article>
|
||||
<el-empty v-if="!loading && !records.length" description="暂无数据" />
|
||||
</div>
|
||||
<div class="page-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="prev, pager, next, total, sizes"
|
||||
:total="page.total"
|
||||
@current-change="load"
|
||||
@size-change="sizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<master-order-editor
|
||||
v-else-if="mode === 'editor'"
|
||||
:id="routeId"
|
||||
@back="goList"
|
||||
@dispatch="goDispatch"
|
||||
/>
|
||||
<master-order-dispatch v-else-if="mode === 'dispatch'" :id="routeId" @back="goList" />
|
||||
<master-order-detail v-else :id="routeId" @back="goList" />
|
||||
<el-dialog v-model="confirm.visible" title="提示" width="400px"
|
||||
><span>{{ confirm.message }}</span
|
||||
><template #footer
|
||||
><el-button @click="confirm.visible = false">关闭</el-button
|
||||
><el-button type="primary" @click="confirm.run">确认</el-button></template
|
||||
></el-dialog
|
||||
>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/business/master-order';
|
||||
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||
import MasterOrderEditor from './components/master-order-editor.vue';
|
||||
import MasterOrderDispatch from './components/master-order-dispatch.vue';
|
||||
import MasterOrderDetail from './components/master-order-detail.vue';
|
||||
|
||||
export default {
|
||||
components: { MasterOrderEditor, MasterOrderDispatch, MasterOrderDetail },
|
||||
data() {
|
||||
return {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
searchExpanded: false,
|
||||
loading: false,
|
||||
records: [],
|
||||
query: { businessStatus: '' },
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
confirm: { visible: false, message: '', run: () => {} },
|
||||
statuses: [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '待调度', value: 'waiting_dispatch' },
|
||||
{ label: '调度中', value: 'dispatching' },
|
||||
{ label: '调度完成', value: 'completed' },
|
||||
{ label: '调度关闭', value: 'closed' },
|
||||
],
|
||||
primaryFields: [
|
||||
{ label: '总单号', prop: 'masterNos' },
|
||||
{ label: '项目名称', prop: 'projectName' },
|
||||
{ label: '客户名称', prop: 'customerName' },
|
||||
],
|
||||
secondaryFields: [
|
||||
{ label: '货物名称', prop: 'cargoName' },
|
||||
{ label: '货物类型', prop: 'cargoType' },
|
||||
{ label: '发货地址', prop: 'departureAddress' },
|
||||
{ label: '收货地址', prop: 'arrivalAddress' },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
mode() {
|
||||
return this.$route.query.mode || 'list';
|
||||
},
|
||||
routeId() {
|
||||
return this.$route.query.id;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$route.query': {
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.syncTagTitle();
|
||||
if (this.mode === 'list') this.load();
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
this.loading = true;
|
||||
const params = { ...this.query };
|
||||
if (params.planStartRange) [params.planStartTime] = params.planStartRange;
|
||||
if (params.planEndRange) [, params.planEndTime] = params.planEndRange;
|
||||
try {
|
||||
const res = await api.getList(this.page.current, this.page.size, params);
|
||||
const data = res.data?.data || res.data || res;
|
||||
this.records = data.records || [];
|
||||
this.page.total = data.total || 0;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
search() {
|
||||
this.page.current = 1;
|
||||
this.load();
|
||||
},
|
||||
reset() {
|
||||
this.query = { businessStatus: '' };
|
||||
this.search();
|
||||
},
|
||||
sizeChange() {
|
||||
this.page.current = 1;
|
||||
this.load();
|
||||
},
|
||||
goList() {
|
||||
this.$router.push('/business/master-order');
|
||||
},
|
||||
goCreate(id) {
|
||||
this.$router.push({ path: '/business/master-order', query: { mode: 'editor', id } });
|
||||
},
|
||||
goDetail(id) {
|
||||
this.$router.push({ path: '/business/master-order', query: { mode: 'detail', id } });
|
||||
},
|
||||
goDispatch(id) {
|
||||
this.$router.push({ path: '/business/master-order', query: { mode: 'dispatch', id } });
|
||||
},
|
||||
syncTagTitle() {
|
||||
const title = this.mode === 'detail'
|
||||
? '总单详情'
|
||||
: this.mode === 'dispatch'
|
||||
? '多联调度'
|
||||
: this.mode === 'editor'
|
||||
? (this.routeId ? '编辑总单' : '新增总单')
|
||||
: '总单管理';
|
||||
this.$store.commit('SET_TAG', { fullPath: this.$route.fullPath, name: title });
|
||||
this.$router.$avueRouter.setTitle(title);
|
||||
},
|
||||
actions(row) {
|
||||
const result = [
|
||||
{
|
||||
text: '复制',
|
||||
run: async () => {
|
||||
const res = await api.copy(row.id);
|
||||
this.$message.success(`复制成功,新总单号:${(res.data || res).masterNo}`);
|
||||
this.load();
|
||||
},
|
||||
},
|
||||
];
|
||||
if (['draft', 'waiting_dispatch', 'dispatching', 'completed'].includes(row.businessStatus))
|
||||
result.unshift({ text: '编辑', run: () => this.goCreate(row.id) });
|
||||
if (['waiting_dispatch', 'dispatching'].includes(row.businessStatus))
|
||||
result.unshift({ text: '调度', run: () => this.goDispatch(row.id) });
|
||||
if (row.businessStatus !== 'draft')
|
||||
result.unshift({ text: '详情', run: () => this.goDetail(row.id) });
|
||||
if (['dispatching', 'completed'].includes(row.businessStatus))
|
||||
result.push({
|
||||
text: '关闭调度',
|
||||
run: () =>
|
||||
this.confirmAction('确认关闭调度?关闭后将不可恢复!', () => api.closeDispatch(row.id)),
|
||||
});
|
||||
if (['draft', 'waiting_dispatch'].includes(row.businessStatus))
|
||||
result.push({
|
||||
text: '删除',
|
||||
danger: true,
|
||||
run: () =>
|
||||
this.confirmAction('确认删除这条数据?删除后将不可恢复!', () => api.remove(row.id)),
|
||||
});
|
||||
return result;
|
||||
},
|
||||
confirmAction(message, request) {
|
||||
this.confirm = {
|
||||
visible: true,
|
||||
message,
|
||||
run: async () => {
|
||||
await request();
|
||||
this.confirm.visible = false;
|
||||
this.$message.success('操作成功');
|
||||
this.load();
|
||||
},
|
||||
};
|
||||
},
|
||||
statusName(value) {
|
||||
return (this.statuses.find(item => item.value === value) || {}).label || '-';
|
||||
},
|
||||
statusType(value) {
|
||||
return {
|
||||
draft: 'info',
|
||||
waiting_dispatch: 'warning',
|
||||
dispatching: 'success',
|
||||
completed: 'primary',
|
||||
closed: 'danger',
|
||||
}[value];
|
||||
},
|
||||
timeRange(row) {
|
||||
return row.planStartTime && row.planEndTime
|
||||
? `${String(row.planStartTime).slice(0, 16)} - ${String(row.planEndTime).slice(0, 16)}`
|
||||
: '-';
|
||||
},
|
||||
goodsSummary(row) {
|
||||
const item = (row.goods || [])[0] || {};
|
||||
if (!item.cargoName && !item.cargoType) return '-';
|
||||
return `${item.cargoName || '-'},${item.cargoType || '-'} | ${this.number(row.totalQuantity)} 吨`;
|
||||
},
|
||||
number(value) {
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
routeNumber(value) {
|
||||
const number = Number(value || 0);
|
||||
return Number.isInteger(number) ? String(number) : number.toFixed(2);
|
||||
},
|
||||
routeArrivedQuantity(route) {
|
||||
const keys = [
|
||||
'arrivedQuantity',
|
||||
'arrivalQuantity',
|
||||
'arriveQuantity',
|
||||
'reachedQuantity',
|
||||
'receiveQuantity',
|
||||
'unloadQuantity',
|
||||
];
|
||||
const key = keys.find(item => route[item] !== undefined && route[item] !== null);
|
||||
return key ? route[key] : 0;
|
||||
},
|
||||
routeNodes(row = {}) {
|
||||
const routes = row.routeProgress || [];
|
||||
const total = this.routeNumber(row.totalQuantity);
|
||||
if (!routes.length) {
|
||||
return [
|
||||
{
|
||||
key: 'start',
|
||||
type: 'start',
|
||||
text: '起',
|
||||
name: row.departureName || row.departureAddress || '-',
|
||||
lines: [`已调度0/${total}吨`],
|
||||
},
|
||||
{
|
||||
key: 'end',
|
||||
type: 'end',
|
||||
text: '终',
|
||||
name: row.arrivalName || row.arrivalAddress || '-',
|
||||
lines: [`到达0/${total}吨`],
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
key: 'start',
|
||||
type: 'start',
|
||||
text: '起',
|
||||
name: row.departureName || row.departureAddress || '-',
|
||||
lines: [`已调度${this.routeNumber(routes[0]?.dispatchedQuantity)}/${total}吨`],
|
||||
},
|
||||
...routes.map((route, index) => {
|
||||
const isEnd = index === routes.length - 1;
|
||||
const arrived = this.routeNumber(this.routeArrivedQuantity(route));
|
||||
const dispatched = this.routeNumber(routes[index + 1]?.dispatchedQuantity);
|
||||
return {
|
||||
key: route.segmentNo || `route-${index}`,
|
||||
type: isEnd ? 'end' : 'middle',
|
||||
text: isEnd ? '终' : '经',
|
||||
name: route.arrivalName || route.departureName || row.arrivalName || '-',
|
||||
lines: isEnd
|
||||
? [`到达${arrived}/${total}吨`]
|
||||
: [`到达 ${arrived}/${total}吨`, `已调度 ${dispatched}/到达${arrived}/${total}吨`],
|
||||
};
|
||||
}),
|
||||
];
|
||||
},
|
||||
progress(route, total) {
|
||||
return total
|
||||
? Math.min(100, (Number(route?.dispatchedQuantity || 0) / Number(total)) * 100)
|
||||
: 0;
|
||||
},
|
||||
async download() {
|
||||
const blob = await api.exportList(this.query);
|
||||
const url = URL.createObjectURL(new Blob([blob.data || blob]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = '总单运单明细.xlsx';
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.master-search {
|
||||
margin-bottom: 8px;
|
||||
padding: 12px 12px 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
.el-form-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.el-input,
|
||||
.el-select,
|
||||
.el-date-editor {
|
||||
width: 100%;
|
||||
}
|
||||
.search-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 40px;
|
||||
.el-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.master-list-panel {
|
||||
background: #fff;
|
||||
.toolbar {
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
}
|
||||
.master-card {
|
||||
display: flex;
|
||||
min-height: 150px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #eff1f7;
|
||||
background: #fff;
|
||||
.card-info {
|
||||
flex: 0 0 380px;
|
||||
padding: 18px 20px;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
.card-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
p {
|
||||
margin: 18px 0 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.project-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
.status-tag {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.route-progress {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
min-height: 170px;
|
||||
padding: 22px 24px 18px;
|
||||
border-right: 1px solid #eff1f7;
|
||||
border-left: 1px solid #eff1f7;
|
||||
}
|
||||
.route-track {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
min-width: max-content;
|
||||
}
|
||||
.route-point {
|
||||
width: 150px;
|
||||
flex: 0 0 150px;
|
||||
text-align: center;
|
||||
}
|
||||
.route-node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
&.start {
|
||||
background: #409eff;
|
||||
}
|
||||
&.middle {
|
||||
background: #67c23a;
|
||||
}
|
||||
&.end {
|
||||
background: #e6a23c;
|
||||
}
|
||||
}
|
||||
.route-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
white-space: nowrap;
|
||||
strong {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
.route-connector {
|
||||
width: clamp(180px, 20vw, 344px);
|
||||
flex: 0 0 clamp(180px, 20vw, 344px);
|
||||
padding-top: 2px;
|
||||
margin: 0 -6px;
|
||||
small {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-bottom: 6px;
|
||||
color: #a8abb2;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
:deep(.el-progress-bar__outer) {
|
||||
height: 8px !important;
|
||||
background-color: #f1f2f5;
|
||||
}
|
||||
:deep(.el-progress-bar__inner) {
|
||||
background-color: #409eff;
|
||||
}
|
||||
}
|
||||
.card-actions {
|
||||
width: 110px;
|
||||
padding: 16px;
|
||||
header {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.el-link {
|
||||
margin: 0 8px 8px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.page-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="220" />
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :detail-id="$route.query.detailId" :menu-width="220" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="250" />
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :detail-id="$route.query.detailId" :menu-width="250" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
Reference in New Issue
Block a user