调整 iam

This commit is contained in:
2026-09-16 06:30:30 +08:00
parent 6a87066097
commit c1c4fa18c9
26 changed files with 1517 additions and 666 deletions
+365 -244
View File
@@ -1,116 +1,113 @@
<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">
<!-- 顶部信息区对齐运单详情汇总样式 -->
<section-card class="transport-plan-dispatch-page__summary">
<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 || '-' }}
<el-button icon="el-icon-arrow-left" text @click="handleBack">返回</el-button>
<strong>计划调度</strong>
<span>{{ planData.planNo || planData.loadingNo || '-' }}</span>
<span class="detail-status-text" :class="statusTextClass">
{{ planData.businessStatusName || '-' }}
</span>
<span class="detail-status-text detail-transport-type">
{{ planData.transportTypeName || '公路整车' }}
</span>
</div>
<div class="transport-plan-dispatch-page__summary-grid">
<div class="transport-plan-dispatch-page__summary-item">
<span class="transport-plan-dispatch-page__summary-label">客户</span>
<span class="transport-plan-dispatch-page__summary-value">
{{ planData.customerName || '-' }}
</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 class="transport-plan-dispatch-page__summary-item">
<span class="transport-plan-dispatch-page__summary-label">合同编号</span>
<span class="transport-plan-dispatch-page__summary-value is-link">
{{ planData.contractNo || '-' }}
</span>
</div>
<div class="transport-plan-dispatch-page__summary-item">
<span class="transport-plan-dispatch-page__summary-label">项目</span>
<span class="transport-plan-dispatch-page__summary-value">
{{ planData.projectName || '-' }}
</span>
</div>
<div class="transport-plan-dispatch-page__summary-item">
<span class="transport-plan-dispatch-page__summary-label">计划执行时间</span>
<span class="transport-plan-dispatch-page__summary-value">
{{ formatDateRange(planData.planStartDate, planData.planEndDate) }}
</span>
</div>
<div class="transport-plan-dispatch-page__summary-item is-goods">
<span class="transport-plan-dispatch-page__summary-label">货物信息</span>
<span class="transport-plan-dispatch-page__summary-value is-wrap">
{{ planData.goodsInfo || formatGoodsInfo(planData) }}
</span>
</div>
<div class="transport-plan-dispatch-page__route">
<div class="transport-plan-dispatch-page__route-item is-start">
<div class="transport-plan-dispatch-page__route-marker"></div>
<div class="transport-plan-dispatch-page__route-detail">
<strong class="transport-plan-dispatch-page__route-name">
{{
formatRouteCityDistrict(planData.departureAddress || planData.departureName)
}}
</strong>
<el-tooltip
:content="planData.departureAddress || planData.departureName || '-'"
placement="top"
>
<span class="transport-plan-dispatch-page__route-addr">
{{ planData.departureAddress || planData.departureName || '-' }}
</span>
</el-tooltip>
<div class="transport-plan-dispatch-page__route-contact">
<span>联系人{{ planData.departureContact || '-' }}</span>
<span>联系方式{{ planData.departurePhone || '-' }}</span>
</div>
</div>
</div>
<span class="transport-plan-dispatch-page__route-line" aria-hidden="true" />
<div class="transport-plan-dispatch-page__route-item is-end">
<div class="transport-plan-dispatch-page__route-marker"></div>
<div class="transport-plan-dispatch-page__route-detail">
<strong class="transport-plan-dispatch-page__route-name">
{{ formatRouteCityDistrict(planData.arrivalAddress || planData.arrivalName) }}
</strong>
<el-tooltip
:content="planData.arrivalAddress || planData.arrivalName || '-'"
placement="top"
>
<span class="transport-plan-dispatch-page__route-addr">
{{ planData.arrivalAddress || planData.arrivalName || '-' }}
</span>
</el-tooltip>
<div class="transport-plan-dispatch-page__route-contact">
<span>联系人{{ planData.arrivalContact || '-' }}</span>
<span>联系方式{{ planData.arrivalPhone || '-' }}</span>
</div>
</div>
</div>
</div>
<div class="transport-plan-dispatch-page__attachments">
<span class="transport-plan-dispatch-page__summary-label">附件</span>
<div>
<el-link
v-for="(item, index) in attachmentList"
:key="index"
type="primary"
:href="item.url"
:underline="false"
target="_blank"
>
{{ item.name || item.originalName || '附件' }}
</el-link>
<span v-if="!attachmentList.length">-</span>
</div>
</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>
<!-- 调度列表 -->
<section-card class="transport-plan-dispatch-page__list-card">
@@ -204,7 +201,6 @@
</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';
@@ -216,7 +212,6 @@ const TRANSPORT_PLAN_QUANTITY_UNIT = '吨';
export default {
name: 'TransportPlanDispatch',
components: {
Van,
SectionCard,
TransportPlanPage,
},
@@ -237,11 +232,21 @@ export default {
planId() {
return this.$route.query.planId;
},
statusTextClass() {
const status = this.planData.businessStatus;
if (status === 2 || status === '2') return 'status-text-success';
if (status === 1 || status === '1') return 'is-dispatching';
if (status === 3 || status === '3') return 'is-danger';
return 'is-info';
},
summaryText() {
const goodsList = this.parseGoodsList(this.planData.goodsJson);
if (!goodsList.length) {
const total = Number(this.planData.totalQuantity || 0);
const dispatched = this.calculateDispatchedQuantity();
if (total <= 0) {
return `已调度 ${dispatched.toFixed(2)}`;
}
const remaining = Math.max(total - dispatched, 0);
return `${total}吨 | 已调度 ${dispatched.toFixed(2)}吨,剩余${remaining.toFixed(2)}`;
}
@@ -264,6 +269,9 @@ export default {
});
const parts = Object.entries(summary).map(([unit, data]) => {
if (data.total <= 0) {
return `已调度 ${data.dispatched.toFixed(2)}${unit}`;
}
const remaining = Math.max(data.total - data.dispatched, 0);
return `${data.total}${unit} | 已调度 ${data.dispatched.toFixed(2)}${unit},剩余${remaining.toFixed(
2
@@ -276,13 +284,20 @@ export default {
const goodsList = this.parseGoodsList(this.planData.goodsJson);
if (!goodsList.length) {
const total = Number(this.planData.totalQuantity || 0);
if (total <= 0) return true;
const dispatched = this.calculateDispatchedQuantity();
return dispatched < total;
}
const hasPositiveTotal = goodsList.some(
goods => Number(goods.quantity || 0) > 0
);
if (!hasPositiveTotal) return true;
return goodsList.some(goods => {
const unit = goods.quantityUnit || TRANSPORT_PLAN_QUANTITY_UNIT;
const totalQty = Number(goods.quantity || 0);
if (totalQty <= 0) return true;
const dispatchedQty = this.dispatchList
.filter(item => item.quantityUnit === unit)
.reduce((sum, item) => sum + Number(item.quantity || 0), 0);
@@ -364,15 +379,6 @@ export default {
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(' ~ ') : '-';
@@ -382,9 +388,30 @@ export default {
const match = address.match(/^(.*?省)?(.*?市)?(.*?区|.*?县)?/);
return match ? match[0] || address : address;
},
formatContact(contact, phone) {
const parts = [contact, phone].filter(Boolean);
return parts.length ? parts.join(' / ') : '-';
formatProvinceCityDistrict(value) {
const text = String(value || '').trim();
if (!text) return '-';
const districtMatch = text.match(/(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/);
if (districtMatch) {
return text.slice(0, districtMatch.index + districtMatch[0].length);
}
const cityMatch = text.match(/市/);
return cityMatch ? text.slice(0, cityMatch.index + 1) : text;
},
formatRouteCityDistrict(value) {
const text = String(value || '').trim();
if (!text) return '-';
const provinceMatch = text.match(/^.+?(?:省|自治区|特别行政区)/);
const cityRegion = provinceMatch ? text.slice(provinceMatch[0].length) : text;
const cityMatch = cityRegion.match(/^(.+?市)/);
if (!cityMatch) return this.formatProvinceCityDistrict(text);
const city = cityMatch[1];
const remainder = cityRegion.slice(city.length);
const districtMatch = remainder.match(
/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗){1,2})/
);
const district = districtMatch ? districtMatch[1] : '';
return [city, district].filter(Boolean).join(' ');
},
formatDriver(row) {
const parts = [row.driverName, row.driverPhone].filter(Boolean);
@@ -419,32 +446,33 @@ export default {
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);
}
});
this.openPageDispatchItemDialog(-1);
},
handleEdit(row, index) {
// 调用 transport-plan-page 组件的方法打开弹窗
this.openPageDispatchItemDialog(index, row);
},
openPageDispatchItemDialog(index = -1, row) {
this.$nextTick(() => {
const component = this.$refs.transportPlanPageRef;
if (component) {
// 设置调度数据
component.dispatchRow = this.planData;
component.dispatchRows = [...this.dispatchList];
// 打开编辑弹窗
component.openDispatchItemDialog(index, row);
}
if (!component) return;
component.dispatchRow = this.planData;
component.dispatchRows = [...this.dispatchList];
this.bindDispatchItemSaveSync(component);
if (index >= 0) component.openDispatchItemDialog(index, row);
else component.openDispatchItemDialog(-1);
});
},
bindDispatchItemSaveSync(component) {
if (component.__dispatchListSaveSynced) return;
const originalSave = component.saveDispatchItem;
component.saveDispatchItem = (...args) => {
originalSave.apply(component, args);
if (!component.dispatchItemBox) {
this.dispatchList = [...(component.dispatchRows || [])];
}
};
component.__dispatchListSaveSynced = true;
},
handleDelete(index) {
this.$confirm('确定删除该调度明细?', '提示', {
confirmButtonText: '确定',
@@ -551,126 +579,189 @@ export default {
height: 100%;
}
&__top {
display: flex;
flex-direction: column;
gap: 16px;
&__summary {
:deep(.el-card__body) {
padding: 18px 22px;
}
}
&__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;
align-items: center;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 18px;
&-card {
flex: 1;
strong {
font-size: 20px;
}
&-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
.detail-status-text {
display: inline-flex;
align-items: center;
height: 28px;
padding: 0 12px;
border-radius: 8px;
font-size: 13px;
line-height: 28px;
}
&-item {
display: flex;
flex-direction: column;
gap: 8px;
&.is-wide {
grid-column: 1 / -1;
}
.status-text-success,
.is-dispatching {
color: #67c23a;
background: #e1f3d8;
}
&-label {
font-size: 14px;
.detail-transport-type {
color: #409eff;
background: #ecf5ff;
}
.is-danger {
color: #f56c6c;
background: #fef0f0;
}
.is-info {
color: #909399;
}
&-value {
font-size: 14px;
color: #303133;
background: #f4f4f5;
}
}
&__route-card {
flex: 1;
&__summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(120px, 1fr)) minmax(360px, 1.5fr);
gap: 16px 24px;
}
&__summary-item {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
&.is-goods {
grid-column: 1 / 5;
}
}
&__summary-label {
color: #909399;
}
&__summary-value {
overflow: hidden;
color: #303133;
font-weight: 500;
text-overflow: ellipsis;
white-space: nowrap;
&.is-link {
color: #409eff;
}
&.is-wrap {
white-space: normal;
word-break: break-word;
}
}
&__route {
display: grid;
grid-template-columns: minmax(100px, 1fr) minmax(36px, 0.7fr) minmax(100px, 1fr);
grid-row: 1 / span 3;
grid-column: 5;
align-items: start;
min-width: 0;
padding: 4px 0;
}
&__route-item {
display: flex;
min-width: 0;
flex-direction: column;
align-items: center;
gap: 24px;
text-align: center;
}
&-item {
flex: 1;
&__route-marker {
display: inline-flex;
width: 36px;
height: 36px;
align-items: center;
justify-content: center;
color: #fff;
border-radius: 8px;
background: #409eff;
font-size: 18px;
font-weight: 600;
.is-end & {
background: #67c23a;
}
}
&__route-line {
width: 100%;
height: 6px;
margin-top: 15px;
border-radius: 1px;
background: #e4e7ed;
}
&__route-detail {
display: flex;
min-width: 0;
flex-direction: column;
align-items: center;
gap: 5px;
margin-top: 8px;
width: 100%;
}
&__route-name {
max-width: 100%;
overflow: hidden;
color: #303133;
font-size: 18px;
font-weight: 600;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
&__route-addr {
display: block;
max-width: 100%;
overflow: hidden;
color: #606266;
font-size: 12px;
line-height: 1.5;
text-overflow: ellipsis;
white-space: nowrap;
}
&__route-contact {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2px 8px;
max-width: 100%;
color: #909399;
font-size: 12px;
line-height: 1.5;
}
&__attachments {
grid-column: 1 / 5;
display: flex;
flex-direction: column;
gap: 8px;
div {
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;
flex-wrap: wrap;
gap: 16px;
}
}
@@ -706,12 +797,42 @@ export default {
border-radius: 4px;
}
&__attachment-link {
margin-right: 12px;
@media (max-width: 1200px) {
&__summary-grid {
grid-template-columns: repeat(4, minmax(130px, 1fr));
}
&__summary-item.is-goods,
&__attachments {
grid-column: 1 / -1;
}
&__route {
grid-column: 1 / -1;
grid-row: auto;
}
}
@media (max-width: 800px) {
&__summary-grid {
grid-template-columns: 1fr;
}
&__route {
grid-template-columns: minmax(0, 1fr);
gap: 16px;
}
&__route-line {
width: 10px;
height: 32px;
margin: 0 auto;
}
&__summary-item.is-goods,
&__attachments {
grid-column: auto;
}
}
}
.status-text {
font-size: 12px;
}
</style>