Files
tms-erp-web/src/views/business/transport-plan-dispatch.vue
T
b2894lxlx cba2ad3b3b 1、调整凭证
2、调整运单
2026-09-08 17:50:34 +08:00

718 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<basic-container class="transport-plan-dispatch-page">
<div v-loading="loading" class="transport-plan-dispatch-page__shell">
<!-- 顶部信息区 -->
<div class="transport-plan-dispatch-page__top">
<div class="transport-plan-dispatch-page__heading">
<div class="transport-plan-dispatch-page__heading-title">
<el-button icon="el-icon-arrow-left" text @click="handleBack">返回</el-button>
<span class="transport-plan-dispatch-page__heading-name">计划调度</span>
<span class="transport-plan-dispatch-page__heading-no">
{{ planData.planNo || planData.loadingNo || '-' }}
</span>
<el-tag :type="getStatusTagType(planData.businessStatus)" effect="light" class="status-text">
{{ planData.businessStatusName || '-' }}
</el-tag>
<el-tag type="primary" effect="light">
{{ planData.transportTypeName || '公路整车' }}
</el-tag>
</div>
</div>
<div class="transport-plan-dispatch-page__summary">
<section-card title="基本信息" class="transport-plan-dispatch-page__summary-card">
<div class="transport-plan-dispatch-page__summary-grid">
<div class="transport-plan-dispatch-page__summary-item">
<div class="transport-plan-dispatch-page__summary-label">客户</div>
<div class="transport-plan-dispatch-page__summary-value">
{{ planData.customerName || '-' }}
</div>
</div>
<div class="transport-plan-dispatch-page__summary-item">
<div class="transport-plan-dispatch-page__summary-label">项目</div>
<div class="transport-plan-dispatch-page__summary-value">
{{ planData.projectName || '-' }}
</div>
</div>
<div class="transport-plan-dispatch-page__summary-item">
<div class="transport-plan-dispatch-page__summary-label">合同编号</div>
<div class="transport-plan-dispatch-page__summary-value">
{{ planData.contractNo || '-' }}
</div>
</div>
<div class="transport-plan-dispatch-page__summary-item">
<div class="transport-plan-dispatch-page__summary-label">计划执行时间</div>
<div class="transport-plan-dispatch-page__summary-value">
{{ formatDateRange(planData.planStartDate, planData.planEndDate) }}
</div>
</div>
<div class="transport-plan-dispatch-page__summary-item is-wide">
<div class="transport-plan-dispatch-page__summary-label">货物信息</div>
<div class="transport-plan-dispatch-page__summary-value">
{{ planData.goodsInfo || formatGoodsInfo(planData) }}
</div>
</div>
<div class="transport-plan-dispatch-page__summary-item">
<div class="transport-plan-dispatch-page__summary-label">附件</div>
<div class="transport-plan-dispatch-page__summary-value">
<template v-if="attachmentList.length">
<el-link
v-for="(item, index) in attachmentList"
:key="index"
type="primary"
:href="item.url"
:underline="false"
target="_blank"
class="transport-plan-dispatch-page__attachment-link"
>
{{ item.name || item.originalName || '附件' }}
</el-link>
</template>
<span v-else>-</span>
</div>
</div>
</div>
</section-card>
<section-card title="收发货路线" class="transport-plan-dispatch-page__route-card">
<div class="transport-plan-dispatch-page__route">
<div class="transport-plan-dispatch-page__route-item">
<span class="transport-plan-dispatch-page__route-badge is-start"></span>
<div class="transport-plan-dispatch-page__route-body">
<div class="transport-plan-dispatch-page__route-title">
{{ planData.departureName || '-' }}
</div>
<div class="transport-plan-dispatch-page__route-address">
{{ formatAddress(planData.departureAddress) }}
</div>
<div class="transport-plan-dispatch-page__route-contact">
{{ formatContact(planData.departureContact, planData.departurePhone) }}
</div>
</div>
</div>
<div class="transport-plan-dispatch-page__route-vehicle">
<el-icon><Van /></el-icon>
</div>
<div class="transport-plan-dispatch-page__route-item">
<span class="transport-plan-dispatch-page__route-badge is-end"></span>
<div class="transport-plan-dispatch-page__route-body">
<div class="transport-plan-dispatch-page__route-title">
{{ planData.arrivalName || '-' }}
</div>
<div class="transport-plan-dispatch-page__route-address">
{{ formatAddress(planData.arrivalAddress) }}
</div>
<div class="transport-plan-dispatch-page__route-contact">
{{ formatContact(planData.arrivalContact, planData.arrivalPhone) }}
</div>
</div>
</div>
</div>
</section-card>
</div>
</div>
<!-- 调度列表 -->
<section-card class="transport-plan-dispatch-page__list-card">
<template #title>
<span>待调度列表</span>
<span class="transport-plan-dispatch-page__list-summary">
{{ summaryText }}
</span>
</template>
<template #extra>
<el-button type="primary" :disabled="!canAddDispatch" @click="handleAdd">
新增
</el-button>
</template>
<el-table
:data="dispatchList"
border
height="100%"
row-key="id"
class="transport-plan-dispatch-page__table"
>
<el-table-column type="index" label="序号" width="70" align="center" fixed="left" />
<el-table-column label="运输方式" min-width="180" align="center" show-overflow-tooltip>
<template #default="{ row }">
{{ row.transportTypeName || '-' }}
</template>
</el-table-column>
<el-table-column prop="carrierType" label="承运类型" min-width="150" align="center" show-overflow-tooltip />
<el-table-column prop="carrierName" label="承运商" min-width="150" align="center" show-overflow-tooltip />
<el-table-column prop="driverName" label="司机" min-width="260" align="center" show-overflow-tooltip>
<template #default="{ row }">
<span>{{ formatDriver(row) }}</span>
</template>
</el-table-column>
<el-table-column prop="vehicleNo" label="车牌号" min-width="150" align="center" show-overflow-tooltip />
<el-table-column prop="cargoType" label="货物类型" min-width="170" align="center" show-overflow-tooltip />
<el-table-column prop="cargoInfo" label="货物信息" min-width="280" align="center" show-overflow-tooltip>
<template #default="{ row }">
{{ row.cargoInfo || formatCargoInfo(row) }}
</template>
</el-table-column>
<el-table-column label="发货地址" min-width="220" align="center" show-overflow-tooltip>
<template #default="{ row }">
{{ formatAddress(row.departureAddress) }}
</template>
</el-table-column>
<el-table-column label="收货地址" min-width="220" align="center" show-overflow-tooltip>
<template #default="{ row }">
{{ formatAddress(row.arrivalAddress) }}
</template>
</el-table-column>
<el-table-column prop="quantity" label="数量" min-width="120" align="center">
<template #default="{ row }">
{{ row.quantity ? `${row.quantity}${row.quantityUnit || '吨'}` : '-' }}
</template>
</el-table-column>
<el-table-column prop="unitPrice" label="单价" min-width="120" align="center" />
<el-table-column prop="freight" label="运费" min-width="120" align="center" />
<el-table-column prop="otherFeeTotal" label="其他费用" min-width="120" align="center" />
<el-table-column prop="freightTotal" label="运费合计" min-width="120" align="center" />
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="{ row, $index }">
<div class="transport-plan-dispatch-page__actions">
<el-link type="primary" @click="handleEdit(row, $index)">编辑</el-link>
<el-link type="danger" @click="handleDelete($index)">删除</el-link>
</div>
</template>
</el-table-column>
</el-table>
</section-card>
<!-- 底部按钮 -->
<div class="transport-plan-dispatch-page__footer">
<el-button @click="handleBack">关闭</el-button>
<el-button type="primary" :loading="saving" @click="handleSubmit">
确认生成
</el-button>
</div>
</div>
<!-- 调度明细编辑弹窗 - 将使用原组件的弹窗 -->
<transport-plan-page
v-if="transportPlanPageVisible"
ref="transportPlanPageRef"
:api="api"
:config="config"
:crud-option="option"
style="display: none"
/>
</basic-container>
</template>
<script>
import { Van } from '@element-plus/icons-vue';
import { getDetail, dispatch } from '@/api/business/transport-plan';
import SectionCard from '@/components/section-card/main.vue';
import TransportPlanPage from './components/transport-plan-page.vue';
import * as api from '@/api/business/transport-plan';
import { config, option } from '@/option/business/transport-plan';
const TRANSPORT_PLAN_QUANTITY_UNIT = '吨';
export default {
name: 'TransportPlanDispatch',
components: {
Van,
SectionCard,
TransportPlanPage,
},
data() {
return {
loading: false,
saving: false,
planData: {},
dispatchList: [],
attachmentList: [],
transportPlanPageVisible: false,
api,
config,
option,
};
},
computed: {
planId() {
return this.$route.query.planId;
},
summaryText() {
const goodsList = this.parseGoodsList(this.planData.goodsJson);
if (!goodsList.length) {
const total = Number(this.planData.totalQuantity || 0);
const dispatched = this.calculateDispatchedQuantity();
const remaining = Math.max(total - dispatched, 0);
return `共${total}吨 | 已调度 ${dispatched.toFixed(2)}吨,剩余${remaining.toFixed(2)}吨`;
}
const summary = {};
goodsList.forEach(goods => {
const unit = goods.quantityUnit || TRANSPORT_PLAN_QUANTITY_UNIT;
if (!summary[unit]) {
summary[unit] = { total: 0, dispatched: 0 };
}
summary[unit].total += Number(goods.quantity || 0);
});
this.dispatchList.forEach(item => {
const unit = item.quantityUnit || TRANSPORT_PLAN_QUANTITY_UNIT;
if (!summary[unit]) {
summary[unit] = { total: 0, dispatched: 0 };
}
summary[unit].dispatched += Number(item.quantity || 0);
});
const parts = Object.entries(summary).map(([unit, data]) => {
const remaining = Math.max(data.total - data.dispatched, 0);
return `${data.total}${unit} | 已调度 ${data.dispatched.toFixed(2)}${unit},剩余${remaining.toFixed(
2
)}${unit}`;
});
return parts.join(' | ');
},
canAddDispatch() {
const goodsList = this.parseGoodsList(this.planData.goodsJson);
if (!goodsList.length) {
const total = Number(this.planData.totalQuantity || 0);
const dispatched = this.calculateDispatchedQuantity();
return dispatched < total;
}
return goodsList.some(goods => {
const unit = goods.quantityUnit || TRANSPORT_PLAN_QUANTITY_UNIT;
const totalQty = Number(goods.quantity || 0);
const dispatchedQty = this.dispatchList
.filter(item => item.quantityUnit === unit)
.reduce((sum, item) => sum + Number(item.quantity || 0), 0);
return dispatchedQty < totalQty;
});
},
},
created() {
this.loadPlanData();
},
mounted() {
// 初始化 transport-plan-page 组件用于打开弹窗
this.transportPlanPageVisible = true;
},
methods: {
loadPlanData() {
if (!this.planId) {
this.$message.error('缺少计划ID参数');
this.handleBack();
return;
}
this.loading = true;
getDetail(this.planId)
.then(res => {
if (res.data?.success) {
this.planData = res.data.data || {};
this.attachmentList = this.parseAttachments(this.planData.attachmentsJson);
this.dispatchList = this.parseDispatchList(this.planData);
} else {
this.$message.error(res.data?.msg || '加载数据失败');
}
})
.catch(err => {
console.error('加载计划数据失败:', err);
this.$message.error('加载数据失败');
})
.finally(() => {
this.loading = false;
});
},
parseAttachments(json) {
try {
const data = JSON.parse(json || '[]');
return Array.isArray(data) ? data : [];
} catch {
return [];
}
},
parseDispatchList(planData) {
try {
const sources = [
planData.waybillList,
planData.dispatchRows,
planData.dispatchRecordList,
planData.dispatchList,
];
for (const source of sources) {
if (Array.isArray(source) && source.length) {
return source.map((item, index) => ({
...item,
id: item.id || `temp-${index}`,
}));
}
}
return [];
} catch {
return [];
}
},
parseGoodsList(json) {
try {
const data = JSON.parse(json || '[]');
return Array.isArray(data) ? data : [];
} catch {
return [];
}
},
calculateDispatchedQuantity() {
return this.dispatchList.reduce((sum, item) => sum + Number(item.quantity || 0), 0);
},
getStatusTagType(status) {
const statusMap = {
0: 'info',
1: 'warning',
2: 'success',
3: 'danger',
};
return statusMap[status] || 'info';
},
formatDateRange(startDate, endDate) {
const dates = [startDate, endDate].filter(Boolean);
return dates.length ? dates.join(' ~ ') : '-';
},
formatAddress(address) {
if (!address) return '-';
const match = address.match(/^(.*?省)?(.*?市)?(.*?区|.*?县)?/);
return match ? match[0] || address : address;
},
formatContact(contact, phone) {
const parts = [contact, phone].filter(Boolean);
return parts.length ? parts.join(' / ') : '-';
},
formatDriver(row) {
const parts = [row.driverName, row.driverPhone].filter(Boolean);
return parts.length ? parts.join(' / ') : '-';
},
formatGoodsInfo(data) {
const goodsList = this.parseGoodsList(data.goodsJson);
if (!goodsList.length) {
return data.cargoName || '-';
}
return goodsList
.map(goods => {
const parts = [goods.cargoName || goods.goodsName];
if (goods.quantity) {
parts.push(`${goods.quantity}${goods.quantityUnit || TRANSPORT_PLAN_QUANTITY_UNIT}`);
}
return parts.join(' ');
})
.join('');
},
formatCargoInfo(row) {
const parts = [row.cargoName];
if (row.specification) parts.push(row.specification);
if (row.model) parts.push(row.model);
return parts.filter(Boolean).join(' / ') || '-';
},
handleBack() {
this.$router.back();
},
handleAdd() {
if (!this.canAddDispatch) {
this.$message.warning('待调度列表货物总量已达到计划总量,不能新增调度明细');
return;
}
// 调用 transport-plan-page 组件的方法打开弹窗
this.$nextTick(() => {
const component = this.$refs.transportPlanPageRef;
if (component) {
// 设置调度数据
component.dispatchRow = this.planData;
component.dispatchRows = [...this.dispatchList];
// 打开新增弹窗
component.openDispatchItemDialog(-1);
}
});
},
handleEdit(row, index) {
// 调用 transport-plan-page 组件的方法打开弹窗
this.$nextTick(() => {
const component = this.$refs.transportPlanPageRef;
if (component) {
// 设置调度数据
component.dispatchRow = this.planData;
component.dispatchRows = [...this.dispatchList];
// 打开编辑弹窗
component.openDispatchItemDialog(index, row);
}
});
},
handleDelete(index) {
this.$confirm('确定删除该调度明细?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.dispatchList.splice(index, 1);
this.$message.success('删除成功');
});
},
handleSubmit() {
if (!this.dispatchList.length) {
this.$message.warning('请先添加调度明细');
return;
}
for (const [index, row] of this.dispatchList.entries()) {
if (!row.carrierName) {
this.$message.warning(`第${index + 1}条调度明细缺少承运商信息`);
return;
}
if (!row.driverName) {
this.$message.warning(`第${index + 1}条调度明细缺少司机信息`);
return;
}
if (!row.vehicleNo) {
this.$message.warning(`第${index + 1}条调度明细缺少车牌号`);
return;
}
}
this.$confirm(
`请确认调度信息,将生成 <span style="color: #409eff; font-size: 20px; padding: 0 4px;">${this.dispatchList.length}</span> 条运单,是否继续?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: true,
}
).then(() => {
this.submitDispatch();
});
},
submitDispatch() {
this.saving = true;
const payload = {
id: this.planId,
mode: 'submit',
waybills: this.dispatchList.map(row => ({
...row,
taskEntryMode: row.taskEntryMode || 'simple',
taskRemark: row.remark || '',
estimatedStartTime: row.estimatedStartTime || this.planData.planStartDate || '',
estimatedEndTime: row.estimatedEndTime || this.planData.planEndDate || '',
goodsJson:
row.goodsJson ||
JSON.stringify([
{
cargoName: row.cargoName,
cargoType: row.cargoType,
quantity: row.quantity,
quantityUnit: row.quantityUnit,
specification: row.specification,
model: row.model,
packageType: row.packageType,
brand: row.brand,
},
]),
})),
};
dispatch(payload)
.then(res => {
if (res.data?.success) {
this.$message.success('调度成功,运单已生成');
this.handleBack();
} else {
this.$message.error(res.data?.msg || '调度失败');
}
})
.catch(err => {
console.error('调度失败:', err);
this.$message.error('调度失败');
})
.finally(() => {
this.saving = false;
});
},
},
};
</script>
<style lang="scss" scoped>
.transport-plan-dispatch-page {
height: 100%;
display: flex;
flex-direction: column;
&__shell {
display: flex;
flex-direction: column;
gap: 16px;
height: 100%;
}
&__top {
display: flex;
flex-direction: column;
gap: 16px;
}
&__heading {
padding: 16px;
background: #fff;
border-radius: 4px;
&-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: 600;
}
&-name {
font-size: 18px;
}
&-no {
font-size: 14px;
color: #606266;
}
}
&__summary {
display: flex;
gap: 16px;
&-card {
flex: 1;
}
&-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
&-item {
display: flex;
flex-direction: column;
gap: 8px;
&.is-wide {
grid-column: 1 / -1;
}
}
&-label {
font-size: 14px;
color: #909399;
}
&-value {
font-size: 14px;
color: #303133;
}
}
&__route-card {
flex: 1;
}
&__route {
display: flex;
align-items: center;
gap: 24px;
&-item {
flex: 1;
display: flex;
gap: 12px;
}
&-badge {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 14px;
flex-shrink: 0;
&.is-start {
background: #67c23a;
}
&.is-end {
background: #409eff;
}
}
&-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
&-title {
font-size: 16px;
font-weight: 600;
}
&-address,
&-contact {
font-size: 14px;
color: #606266;
}
&-vehicle {
font-size: 24px;
color: #909399;
}
}
&__list-card {
flex: 1;
display: flex;
flex-direction: column;
}
&__list-summary {
margin-left: 12px;
font-size: 14px;
color: #606266;
font-weight: normal;
}
&__table {
flex: 1;
}
&__actions {
display: flex;
gap: 8px;
justify-content: center;
}
&__footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px;
background: #fff;
border-radius: 4px;
}
&__attachment-link {
margin-right: 12px;
}
}
.status-text {
font-size: 12px;
}
</style>