调整 iam
This commit is contained in:
@@ -1,8 +1,51 @@
|
||||
<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)" class="status-text">{{ statusName(master.businessStatus) }}</el-tag><el-tag v-if="transportFlowLabel" type="info" class="transport-flow-tag">{{ transportFlowLabel }}</el-tag></div><el-button @click="$emit('back')">取消</el-button></div>
|
||||
<dl class="detail-meta"><div><dt>客户</dt><dd>{{ master.customerName || '-' }}</dd></div><div><dt>合同编号</dt><dd>{{ master.contractNo || '-' }}</dd></div><div><dt>项目</dt><dd>{{ master.projectName || '-' }}</dd></div><div class="detail-meta__attachments"><dt>附件</dt><dd><template v-if="attachments.length"><el-link v-for="file in attachments" :key="file.url || file.link || file.name || file.originalName" type="primary" @click="previewAttachment(file)">{{ attachmentName(file) }}</el-link></template><span v-else>-</span></dd></div></dl>
|
||||
<div class="detail-heading"><div><h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2><el-tag :type="statusType(master.businessStatus)" class="status-text">{{ statusName(master.businessStatus) }}</el-tag><el-tag v-if="transportFlowLabel" type="info" class="transport-flow-tag">{{ transportFlowLabel }}</el-tag></div></div>
|
||||
<dl class="detail-meta">
|
||||
<div>
|
||||
<dt>客户</dt>
|
||||
<dd>
|
||||
<el-link v-if="master.customerName" type="primary" @click="openCustomer">{{
|
||||
master.customerName
|
||||
}}</el-link>
|
||||
<span v-else>-</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>合同编号</dt>
|
||||
<dd>
|
||||
<el-link v-if="master.contractNo && master.contractId" type="primary" @click="openContract">{{
|
||||
master.contractNo
|
||||
}}</el-link>
|
||||
<span v-else>{{ master.contractNo || '-' }}</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>项目</dt>
|
||||
<dd>
|
||||
<el-link v-if="master.projectName && master.projectId" type="primary" @click="openProject">{{
|
||||
master.projectName
|
||||
}}</el-link>
|
||||
<span v-else>{{ master.projectName || '-' }}</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="detail-meta__attachments">
|
||||
<dt>附件</dt>
|
||||
<dd>
|
||||
<template v-if="attachments.length">
|
||||
<el-link
|
||||
v-for="file in attachments"
|
||||
:key="file.url || file.link || file.name || file.originalName"
|
||||
type="primary"
|
||||
@click="previewAttachment(file)"
|
||||
>{{ attachmentName(file) }}</el-link
|
||||
>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ segmentLocationName(route, 'departure') }}</strong><small>{{ route.departureAddress || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(segments[index - 1])}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(route)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</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>{{ segmentLocationName(route, 'arrival') }}</strong><small>{{ route.arrivalAddress || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div></template></div>
|
||||
</section>
|
||||
|
||||
@@ -42,6 +85,8 @@
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/business/master-order';
|
||||
import { getDetail as getProjectDetail } from '@/api/business/project-apply';
|
||||
import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive';
|
||||
import { CaretBottom, CaretTop } from '@element-plus/icons-vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
@@ -51,10 +96,26 @@ import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
|
||||
const viewerPlugins = [imagePlugin(), pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }), officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }), textPlugin(), fallbackPlugin()];
|
||||
|
||||
const parseJsonArray = value => {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (!value) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (Array.isArray(parsed)) return parsed;
|
||||
return parsed && typeof parsed === 'object' ? [parsed] : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const customerRowLabel = (row = {}) =>
|
||||
row.customer || row.fullName || row.shortName || row.customerName || row.customerCode || '';
|
||||
|
||||
const customerRowId = (row = {}) => row.id || row.customerId || '';
|
||||
|
||||
export default {
|
||||
components: { ElImageViewer, OpenFileViewer },
|
||||
props: { id: [String, Number] },
|
||||
emits: ['back'],
|
||||
data() { return { loading: false, master: null, expandedSegments: {}, CaretBottom, CaretTop, imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { download: true, fullscreen: true, print: true, rotate: true, zoom: true } }; },
|
||||
computed: {
|
||||
segments() {
|
||||
@@ -148,6 +209,68 @@ export default {
|
||||
this.$router.push({ path: '/business/waybill-manage/detail', query: { id: row.id } });
|
||||
},
|
||||
openPlan(row) { this.$router.push({ path: '/business/transport-plan', query: { detailId: row.id } }); },
|
||||
openContract() {
|
||||
if (!this.master?.contractId) {
|
||||
this.$message.warning('合同信息缺失,无法打开');
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
path: '/business/contract-manage/detail',
|
||||
query: { id: this.master.contractId, name: '合同详情' },
|
||||
});
|
||||
},
|
||||
openProject() {
|
||||
if (!this.master?.projectId) {
|
||||
this.$message.warning('项目信息缺失,无法打开');
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
path: '/business/project-apply/form',
|
||||
query: { mode: 'view', id: this.master.projectId, name: '查看项目管理' },
|
||||
});
|
||||
},
|
||||
async resolveCustomerId() {
|
||||
if (this.master?.customerId) return this.master.customerId;
|
||||
const name = String(this.master?.customerName || '').trim();
|
||||
if (!name) return '';
|
||||
if (this.master?.projectId) {
|
||||
try {
|
||||
const res = await getProjectDetail(this.master.projectId);
|
||||
const rows = parseJsonArray(res.data?.data?.customerJson);
|
||||
const matched =
|
||||
rows.find(row => customerRowLabel(row) === name) ||
|
||||
rows.find(row => {
|
||||
const label = customerRowLabel(row);
|
||||
return label && (name.includes(label) || label.includes(name));
|
||||
});
|
||||
const id = customerRowId(matched);
|
||||
if (id) return id;
|
||||
} catch (error) {
|
||||
// 项目客户解析失败时回退到客商档案检索
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await getCustomerArchiveList(1, 20, { fullName: name });
|
||||
const records = res.data?.data?.records || [];
|
||||
const exact =
|
||||
records.find(item => item.fullName === name || item.shortName === name) || records[0];
|
||||
return exact?.id || '';
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
async openCustomer() {
|
||||
if (!this.master?.customerName) return;
|
||||
const customerId = await this.resolveCustomerId();
|
||||
if (!customerId) {
|
||||
this.$message.warning('未找到对应客商档案,无法打开');
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
path: '/vehicle/customer-archive/form',
|
||||
query: { id: String(customerId), name: '查看客商档案', view: '1' },
|
||||
});
|
||||
},
|
||||
waybillStatusName(value) { return ({ pending: '待执行', waiting: '待执行', running: '进行中', processing: '进行中', completed: '已完成', cancelled: '已取消' })[value] || value || '-'; },
|
||||
waybillStatusType(value) { return ({ pending: 'info', waiting: 'info', running: 'warning', processing: 'warning', completed: 'success', cancelled: 'danger' })[value] || 'info'; },
|
||||
statusName(value) { return ({ waiting_dispatch: '待调度', dispatching: '调度中', completed: '调度完成', closed: '调度关闭' })[value] || value || '-'; },
|
||||
@@ -161,7 +284,7 @@ export default {
|
||||
.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; } }
|
||||
.transport-flow-tag { margin-left: 8px; }
|
||||
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
|
||||
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #303133; font-size: 14px; word-break: break-all; } :deep(.el-link) { font-size: 14px; } }
|
||||
.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: #303133 !important; }
|
||||
|
||||
@@ -95,10 +95,51 @@
|
||||
<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 min-width="180"><template #header><span>货物类型<span class="goods-required-mark">*</span></span></template><template #default="{ row }"><el-cascader v-model="row.cargoTypePath" class="goods-table__cargo-type" :options="cargoTypeOptions" :props="cargoTypeCascaderProps" placeholder="请选择货物类型" clearable filterable @change="value => handleCargoTypeChange(route, row, value)" /></template></el-table-column>
|
||||
<el-table-column min-width="180"><template #header><span>货物名称<span class="goods-required-mark">*</span></span></template><template #default="{ row }"><el-select v-model="row.sourceIndex" class="goods-table__cargo-name" placeholder="请选择总单货物" @change="selectGoods(route, row)"><el-option v-for="item in masterGoodsByCargoType(row)" :key="item.sourceIndex" :label="item.cargoName" :value="item.sourceIndex" /></el-select></template></el-table-column>
|
||||
<el-table-column prop="remainingQuantity" column-key="remainingQuantity" label="剩余数量" width="150" class-name="goods-table__remaining"><template #default="{ row }">{{ formatQuantity(goodsRemainingQuantity(route, row)) }}</template></el-table-column>
|
||||
<el-table-column prop="dispatchQuantity" column-key="dispatchQuantity" width="200" class-name="goods-table__dispatch"><template #header><span>本次数量<span class="goods-required-mark">*</span></span></template><template #default="{ row }"><el-input :model-value="row.dispatchQuantity" inputmode="decimal" placeholder="请输入" @input="value => handleDispatchQuantityInput(route, row, value)" /></template></el-table-column>
|
||||
<el-table-column min-width="180"
|
||||
><template #header
|
||||
><span>货物类型<span class="goods-required-mark">*</span></span></template
|
||||
><template #default="{ row }"
|
||||
><el-autocomplete
|
||||
v-model="row.cargoType"
|
||||
class="goods-table__cargo-type"
|
||||
clearable
|
||||
:debounce="300"
|
||||
placeholder="请输入货物类型"
|
||||
:fetch-suggestions="fetchCargoTypeSuggestions"
|
||||
@select="item => handleCargoTypeSelect(route, row, item)"
|
||||
@change="value => handleCargoTypeChange(route, row, value)"
|
||||
/></template>
|
||||
</el-table-column>
|
||||
<el-table-column min-width="180"
|
||||
><template #header
|
||||
><span>货物名称<span class="goods-required-mark">*</span></span></template
|
||||
><template #default="{ row }"
|
||||
><el-select
|
||||
v-model="row.sourceIndex"
|
||||
class="goods-table__cargo-name"
|
||||
placeholder="请选择总单货物"
|
||||
@change="selectGoods(route, row)"
|
||||
><el-option
|
||||
v-for="item in masterGoodsByCargoType(row)"
|
||||
:key="item.sourceIndex"
|
||||
:label="item.cargoName"
|
||||
:value="item.sourceIndex" /></el-select
|
||||
></template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="dispatchQuantity"
|
||||
column-key="dispatchQuantity"
|
||||
width="200"
|
||||
class-name="goods-table__dispatch"
|
||||
><template #header
|
||||
><span>本次数量<span class="goods-required-mark">*</span></span></template
|
||||
><template #default="{ row }"
|
||||
><el-input
|
||||
:model-value="row.dispatchQuantity"
|
||||
inputmode="decimal"
|
||||
placeholder="请输入"
|
||||
@input="value => handleDispatchQuantityInput(route, row, value)" /></template
|
||||
></el-table-column>
|
||||
<el-table-column prop="quantityUnit" width="110"><template #header><span>数量单位<span class="goods-required-mark">*</span></span></template></el-table-column>
|
||||
<el-table-column prop="packageType" label="包装" min-width="110" />
|
||||
<el-table-column prop="brand" label="品牌" min-width="110" />
|
||||
@@ -208,7 +249,7 @@ export default {
|
||||
props: { id: [String, Number] },
|
||||
emits: ['back'],
|
||||
data() {
|
||||
return { loading: false, submitting: false, master: null, routes: [], pending: [], pendingExpanded: false, editingId: null, driverInputs: {}, carrierOptions: [], driverOptions: [], carrierLoading: false, driverLoading: false, escortLoading: false, cargoTypeOptions: [], cargoTypeCascaderProps: { label: 'cargoName', value: 'id', children: 'children', emitPath: true }, quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'] };
|
||||
return { loading: false, submitting: false, master: null, routes: [], pending: [], pendingExpanded: false, editingId: null, driverInputs: {}, carrierOptions: [], driverOptions: [], carrierLoading: false, driverLoading: false, escortLoading: false, quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'] };
|
||||
},
|
||||
computed: {
|
||||
totalQuantity() { return (this.master?.goods || []).reduce((sum, item) => sum + Number(item.quantity || 0), 0); },
|
||||
@@ -216,6 +257,9 @@ export default {
|
||||
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(' / ') })); },
|
||||
cargoTypeSelectOptions() {
|
||||
return [...new Set((this.master?.goods || []).map(item => item.cargoType).filter(Boolean))];
|
||||
},
|
||||
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, index) => {
|
||||
@@ -256,7 +300,6 @@ export default {
|
||||
const res = await api.getDetail(this.id);
|
||||
this.master = res.data?.data || res.data || res;
|
||||
await this.loadContractCurrency();
|
||||
this.cargoTypeOptions = this.buildMasterCargoTypeOptions(this.master.goods || []);
|
||||
this.routes = this.dispatchRouteNodes().map((node, index) => this.createRoute(node, index));
|
||||
} finally { this.loading = false; }
|
||||
},
|
||||
@@ -318,46 +361,42 @@ export default {
|
||||
this.syncFreightItems(route);
|
||||
return route;
|
||||
},
|
||||
buildMasterCargoTypeOptions(goods = []) {
|
||||
return [...new Set(goods.map(item => item.cargoType).filter(Boolean))].map(cargoType => ({
|
||||
id: cargoType,
|
||||
cargoName: cargoType,
|
||||
}));
|
||||
},
|
||||
masterGoodsByCargoType(row = {}) {
|
||||
return this.masterGoods.filter(item => !row.cargoType || item.cargoType === row.cargoType);
|
||||
const cargoType = String(row.cargoType || '').trim();
|
||||
if (!cargoType) return this.masterGoods;
|
||||
const matched = this.masterGoods.filter(item => item.cargoType === cargoType);
|
||||
return matched.length ? matched : this.masterGoods;
|
||||
},
|
||||
findCargoTypeByPath(path = []) {
|
||||
let options = this.cargoTypeOptions;
|
||||
let selected;
|
||||
(path || []).forEach(id => {
|
||||
selected = (options || []).find(item => String(item.id) === String(id));
|
||||
options = selected?.children || [];
|
||||
});
|
||||
return selected;
|
||||
fetchCargoTypeSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim().toLowerCase();
|
||||
const options = this.cargoTypeSelectOptions.map(item => ({ value: item }));
|
||||
callback(
|
||||
keyword ? options.filter(item => String(item.value).toLowerCase().includes(keyword)) : options
|
||||
);
|
||||
},
|
||||
findCargoTypePath(cargoType, options = this.cargoTypeOptions, parentPath = []) {
|
||||
for (const option of options || []) {
|
||||
const path = [...parentPath, option.id];
|
||||
if (option.cargoName === cargoType) return path;
|
||||
if (option.children?.length) {
|
||||
const result = this.findCargoTypePath(cargoType, option.children, path);
|
||||
if (result.length) return result;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
handleCargoTypeSelect(route, row, item = {}) {
|
||||
this.handleCargoTypeChange(route, row, item.value || '');
|
||||
},
|
||||
handleCargoTypeChange(route, row, value) {
|
||||
const path = Array.isArray(value) ? value : [];
|
||||
const cargoType = this.findCargoTypeByPath(path);
|
||||
row.cargoTypePath = path;
|
||||
row.cargoType = cargoType?.cargoName || '';
|
||||
const firstGoods = this.masterGoodsByCargoType(row)[0];
|
||||
row.cargoType = String(value ?? row.cargoType ?? '').trim();
|
||||
const matched = this.masterGoods.filter(item => item.cargoType === row.cargoType);
|
||||
const firstGoods = matched[0];
|
||||
if (firstGoods) {
|
||||
row.sourceIndex = firstGoods.sourceIndex;
|
||||
this.selectGoods(route, row);
|
||||
} else {
|
||||
Object.assign(row, {
|
||||
sourceIndex: undefined,
|
||||
cargoName: '',
|
||||
quantity: '',
|
||||
quantityUnit: '',
|
||||
packageType: '',
|
||||
brand: '',
|
||||
specification: '',
|
||||
model: '',
|
||||
materialCode: '',
|
||||
});
|
||||
}
|
||||
else Object.assign(row, { sourceIndex: undefined, cargoName: '', quantity: '', quantityUnit: '' });
|
||||
this.syncFreightItems(route);
|
||||
},
|
||||
isRoad(route) {
|
||||
@@ -542,53 +581,48 @@ export default {
|
||||
);
|
||||
return false;
|
||||
},
|
||||
pendingGoodsQuantity(route, row) {
|
||||
const goodsIndex = this.masterGoodsIndex(row);
|
||||
return this.pending.reduce((sum, item) => {
|
||||
if (!this.isSameRouteSegment(route, item)) return sum;
|
||||
const pendingGoodsIndex = this.masterGoodsIndex(item);
|
||||
if (
|
||||
(goodsIndex >= 0 && pendingGoodsIndex >= 0 && pendingGoodsIndex !== goodsIndex) ||
|
||||
(goodsIndex < 0 && !this.isSameGoods(item, row))
|
||||
) {
|
||||
return sum;
|
||||
}
|
||||
return sum + this.quantityNumber(item.quantity);
|
||||
}, 0);
|
||||
},
|
||||
isSameRouteSegment(route = {}, item = {}) {
|
||||
const routeSegment = route.segmentNo || route.relationNo || '';
|
||||
const itemSegment = item.segmentNo || item.relationNo || '';
|
||||
return String(routeSegment) === String(itemSegment);
|
||||
},
|
||||
editingGoodsQuantity(route, row) {
|
||||
return (route.goods || []).reduce((sum, item) => {
|
||||
if (item === row || !this.isSameGoods(item, row)) return sum;
|
||||
return sum + this.quantityNumber(item.dispatchQuantity);
|
||||
}, 0);
|
||||
},
|
||||
goodsRemainingQuantity(route, row) {
|
||||
return Math.max(
|
||||
0,
|
||||
this.baseGoodsRemainingQuantity(route, row) -
|
||||
this.pendingGoodsQuantity(route, row) -
|
||||
this.editingGoodsQuantity(route, row)
|
||||
);
|
||||
},
|
||||
availableDispatchQuantity(route, row) { return this.goodsRemainingQuantity(route, row); },
|
||||
handleDispatchQuantityInput(route, row, value) {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
const nextValue = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 3)}` : integer;
|
||||
const availableQuantity = this.availableDispatchQuantity(route, row);
|
||||
if (Number(nextValue || 0) > availableQuantity) {
|
||||
row.dispatchQuantity = this.formatQuantity(availableQuantity);
|
||||
this.syncFreightItems(route);
|
||||
this.$message.warning('本次数量不能超过剩余数量');
|
||||
return;
|
||||
}
|
||||
row.dispatchQuantity = nextValue;
|
||||
row.dispatchQuantity =
|
||||
decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 3)}` : integer;
|
||||
this.syncFreightItems(route);
|
||||
},
|
||||
validatePendingTotals() {
|
||||
for (const route of this.routes) {
|
||||
const items = this.pending.filter(item => this.isSameRouteSegment(route, item));
|
||||
if (!items.length) continue;
|
||||
if (!this.validatePreviousSegmentCompletedQuantity(route, [])) return false;
|
||||
|
||||
const totals = new Map();
|
||||
for (const item of items) {
|
||||
const index = this.masterGoodsIndex(item);
|
||||
const key = index >= 0 ? `i:${index}` : this.goodsKey(item);
|
||||
totals.set(key, (totals.get(key) || 0) + this.quantityNumber(item.quantity));
|
||||
}
|
||||
for (const [key, qty] of totals.entries()) {
|
||||
if (!key.startsWith('i:')) continue;
|
||||
const sourceIndex = Number(key.slice(2));
|
||||
const masterGoods = this.master?.goods?.[sourceIndex];
|
||||
if (!masterGoods) continue;
|
||||
const available = this.baseGoodsRemainingQuantity(route, {
|
||||
...masterGoods,
|
||||
sourceIndex,
|
||||
});
|
||||
if (qty > available) {
|
||||
this.$message.warning(
|
||||
`${route.segmentNo}「${masterGoods.cargoName || ''}」调度总量不能超过剩余数量(${this.formatQuantity(available)} ${masterGoods.quantityUnit || this.quantityUnitLabel})`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
handleFreightUnitPriceInput(item, value) {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
item.unitPrice = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||
@@ -618,7 +652,7 @@ export default {
|
||||
const index = (route.goods || []).indexOf(item) + 1;
|
||||
const fields = [
|
||||
[!String(item.cargoName || '').trim() || item.sourceIndex === undefined || item.sourceIndex === null || item.sourceIndex === '', '货物名称'],
|
||||
[!String(item.cargoType || '').trim() && !(item.cargoTypePath || []).length, '货物类型'],
|
||||
[!String(item.cargoType || '').trim(), '货物类型'],
|
||||
[item.dispatchQuantity === undefined || item.dispatchQuantity === null || item.dispatchQuantity === '' || Number(item.dispatchQuantity) <= 0, '本次数量'],
|
||||
[!String(item.quantityUnit || '').trim(), '数量单位'],
|
||||
];
|
||||
@@ -650,7 +684,6 @@ export default {
|
||||
return {
|
||||
...goods,
|
||||
sourceIndex,
|
||||
cargoTypePath: this.findCargoTypePath(goods.cargoType),
|
||||
dispatchQuantity: undefined,
|
||||
};
|
||||
},
|
||||
@@ -666,7 +699,8 @@ export default {
|
||||
selectGoods(route, row) {
|
||||
const source = (this.master.goods || [])[Number(row.sourceIndex)];
|
||||
if (!source) return;
|
||||
Object.assign(row, this.createGoodsRow(source, Number(row.sourceIndex)));
|
||||
const cargoType = String(row.cargoType || '').trim() || source.cargoType;
|
||||
Object.assign(row, this.createGoodsRow(source, Number(row.sourceIndex)), { cargoType });
|
||||
this.syncFreightItems(route);
|
||||
},
|
||||
removeGoods(route, index) { route.goods.splice(index, 1); this.syncFreightItems(route); },
|
||||
@@ -854,8 +888,6 @@ export default {
|
||||
const goods = route.goods.filter(item => Number(item.dispatchQuantity) > 0);
|
||||
if (!goods.length) return this.$message.warning('请填写本次数量');
|
||||
if (!this.validateDispatchGoods(route, goods)) return;
|
||||
if (goods.some(item => Number(item.dispatchQuantity) > this.availableDispatchQuantity(route, item))) return this.$message.warning('本次数量不能超过剩余数量');
|
||||
if (!this.validatePreviousSegmentCompletedQuantity(route, goods)) return;
|
||||
this.syncFreightItems(route);
|
||||
if (!this.validateFreightItems(route)) return;
|
||||
if (!this.validateRoutePhones(route)) return;
|
||||
@@ -954,6 +986,7 @@ export default {
|
||||
async submit() {
|
||||
if (!this.pending.length) return this.$message.warning('请加入待提交调度清单');
|
||||
if (this.pending.some(item => !this.validateRoutePhones(item))) return;
|
||||
if (!this.validatePendingTotals()) return;
|
||||
this.submitting = true;
|
||||
try { await api.dispatch({ id: this.master.id, dispatches: this.pending.map(({ id, ...item }) => ({ ...item, mileage: this.normalizeMileage(item.mileage) })) }); this.$message.success('调度成功'); this.$emit('back'); } finally { this.submitting = false; }
|
||||
},
|
||||
@@ -985,7 +1018,7 @@ export default {
|
||||
.transport-type-field :deep(.el-input) { width: 240px; }
|
||||
.goods-heading { display: flex; align-items: center; justify-content: space-between; margin: 0 -24px 12px; padding: 14px 24px; border-top: 1px solid #eff1f7; border-bottom: 1px solid #eff1f7; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } span { color: #909399; font-size: 13px; } }
|
||||
.freight-heading { margin: 16px 0 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
||||
.goods-table { :deep(th.el-table__cell), :deep(td.el-table__cell) { border-color: #eff1f7; } :deep(.el-table__row--striped td.el-table__cell) { background: #fafafa; } :deep(.goods-table__remaining .cell) { white-space: nowrap; } :deep(.goods-table__dispatch .el-input) { width: 100%; min-width: 0; } :deep(.goods-table__cargo-type .el-input__inner), :deep(.goods-table__cargo-name .el-select__selected-item) { color: #303133; } }
|
||||
.goods-table { :deep(th.el-table__cell), :deep(td.el-table__cell) { border-color: #eff1f7; } :deep(.el-table__row--striped td.el-table__cell) { background: #fafafa; } :deep(.goods-table__dispatch .el-input) { width: 100%; min-width: 0; } :deep(.goods-table__cargo-type), :deep(.goods-table__cargo-name) { width: 100%; } :deep(.goods-table__cargo-type .el-input__inner), :deep(.goods-table__cargo-name .el-select__selected-item) { color: #303133; } }
|
||||
.goods-required-mark { margin-left: 2px; color: #f56c6c; }
|
||||
.freight-form { :deep(.freight-unit-select) { width: 92px; flex: 0 0 92px; min-width: 0; } }
|
||||
.carrier-type-form { margin-top: 16px; }
|
||||
|
||||
@@ -146,8 +146,9 @@
|
||||
/>
|
||||
<el-input
|
||||
v-model="form.departureAddress"
|
||||
placeholder="请输入发货地址"
|
||||
:readonly="transportStationMode || !config.editableRoadAddress"
|
||||
class="shipping-template-page__address-map-input"
|
||||
readonly
|
||||
placeholder="请选择发货地址"
|
||||
:disabled="transportAddressSelectDisabled"
|
||||
@click="handleTransportSecondaryAddressInputClick('departure')"
|
||||
>
|
||||
@@ -189,8 +190,9 @@
|
||||
/>
|
||||
<el-input
|
||||
v-model="form.arrivalAddress"
|
||||
placeholder="请输入收货地址"
|
||||
:readonly="transportStationMode || !config.editableRoadAddress"
|
||||
class="shipping-template-page__address-map-input"
|
||||
readonly
|
||||
placeholder="请选择收货地址"
|
||||
:disabled="transportAddressSelectDisabled"
|
||||
@click="handleTransportSecondaryAddressInputClick('arrival')"
|
||||
>
|
||||
@@ -1131,7 +1133,7 @@ const defaultCargo = () => ({
|
||||
cargoTypePath: [],
|
||||
packageType: '',
|
||||
quantity: '',
|
||||
quantityUnit: '',
|
||||
quantityUnit: '吨',
|
||||
brand: '',
|
||||
specification: '',
|
||||
model: '',
|
||||
@@ -1258,6 +1260,7 @@ export default {
|
||||
crudDialogType: '',
|
||||
standaloneFormKey: '',
|
||||
copyingRow: null,
|
||||
suppressTransportTypeClear: false,
|
||||
detailBox: false,
|
||||
detailLoading: false,
|
||||
detailRow: {},
|
||||
@@ -1511,10 +1514,10 @@ export default {
|
||||
},
|
||||
},
|
||||
'form.transportType'(value, oldValue) {
|
||||
if (value !== oldValue) {
|
||||
this.handleTransportTypeChange(value);
|
||||
this.syncFreightItems();
|
||||
}
|
||||
if (this.suppressTransportTypeClear) return;
|
||||
if (String(value ?? '') === String(oldValue ?? '')) return;
|
||||
this.handleTransportTypeChange(value);
|
||||
this.syncFreightItems();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
@@ -1527,6 +1530,20 @@ export default {
|
||||
methods: {
|
||||
buildOption(option) {
|
||||
const next = { ...option, column: (option.column || []).map(column => ({ ...column })) };
|
||||
const tableOrder = this.config.tableColumnOrder || [];
|
||||
if (tableOrder.length) {
|
||||
const orderMap = new Map(tableOrder.map((prop, index) => [prop, index]));
|
||||
next.column.sort((left, right) => {
|
||||
const leftOrder = orderMap.has(left.prop) ? orderMap.get(left.prop) : tableOrder.length;
|
||||
const rightOrder = orderMap.has(right.prop)
|
||||
? orderMap.get(right.prop)
|
||||
: tableOrder.length;
|
||||
return leftOrder - rightOrder;
|
||||
});
|
||||
next.column.forEach(column => {
|
||||
if (column.prop) column.hide = !orderMap.has(column.prop);
|
||||
});
|
||||
}
|
||||
if (this.menuWidth != null) next.menuWidth = this.menuWidth;
|
||||
if (
|
||||
this.standaloneFormPage &&
|
||||
@@ -1790,6 +1807,7 @@ export default {
|
||||
console.log('进入复制模式分支');
|
||||
const copyData = { ...this.copyingRow };
|
||||
this.copyingRow = null;
|
||||
this.suppressTransportTypeClear = true;
|
||||
this.form = { ...(this.config.defaultForm || {}), ...copyData };
|
||||
console.log('合并后的 form:', this.form);
|
||||
// 删除模板编号,等待系统生成
|
||||
@@ -1842,7 +1860,11 @@ export default {
|
||||
done?.();
|
||||
},
|
||||
applyFormDetail(row) {
|
||||
this.suppressTransportTypeClear = true;
|
||||
this.form = { ...(row || {}) };
|
||||
this.$nextTick(() => {
|
||||
this.suppressTransportTypeClear = false;
|
||||
});
|
||||
this.selectedProjectId = this.form.projectId || '';
|
||||
this.transportCargoRows = this.parseJsonArray(this.form.goodsJson).map(item =>
|
||||
this.normalizeCargo(item)
|
||||
@@ -1924,14 +1946,6 @@ export default {
|
||||
this.$message.warning(`第${index + 1}行货物类型不能为空`);
|
||||
return false;
|
||||
}
|
||||
if (!String(row.cargoName || '').trim() || !row.quantity || !row.quantityUnit) {
|
||||
this.$message.warning(`第${index + 1}行货物信息不完整`);
|
||||
return false;
|
||||
}
|
||||
if (Number(row.quantity) <= 0) {
|
||||
this.$message.warning(`第${index + 1}行数量必须大于0`);
|
||||
return false;
|
||||
}
|
||||
if (String(row.remark || '').length > 200) {
|
||||
this.$message.warning(`第${index + 1}行备注不能超过200个字符`);
|
||||
return false;
|
||||
@@ -2013,6 +2027,8 @@ export default {
|
||||
delete detail.updateUser;
|
||||
delete detail.updateUserName;
|
||||
delete detail.status;
|
||||
// 模板名称追加「副本」
|
||||
detail.templateName = `${detail.templateName || ''}副本`;
|
||||
|
||||
console.log('处理后的数据:', detail);
|
||||
console.log('isStandaloneBusinessPage:', this.isStandaloneBusinessPage);
|
||||
@@ -2286,13 +2302,60 @@ export default {
|
||||
},
|
||||
handleTransportTypeChange(value) {
|
||||
this.form.transportTypeName = this.getTransportTypeLabel(value);
|
||||
this.clearTransportShippingInfo();
|
||||
},
|
||||
clearTransportShippingInfo() {
|
||||
[
|
||||
'departureName',
|
||||
'departureAddress',
|
||||
'departureAddressCode',
|
||||
'departureSiteCode',
|
||||
'departureLongitude',
|
||||
'departureLatitude',
|
||||
'departureContact',
|
||||
'departurePhone',
|
||||
'arrivalName',
|
||||
'arrivalAddress',
|
||||
'arrivalAddressCode',
|
||||
'arrivalSiteCode',
|
||||
'arrivalLongitude',
|
||||
'arrivalLatitude',
|
||||
'arrivalContact',
|
||||
'arrivalPhone',
|
||||
].forEach(prop => {
|
||||
this.form[prop] = '';
|
||||
});
|
||||
this.transportMapSelected = {};
|
||||
this.transportRouteBox = false;
|
||||
this.transportAddressBox = false;
|
||||
this.transportStationBox = false;
|
||||
this.transportMapBox = false;
|
||||
this.transportAddressTarget = '';
|
||||
this.transportStationTarget = '';
|
||||
this.transportMapTarget = '';
|
||||
this.transportMapKeyword = '';
|
||||
this.transportMapStatus = '可搜索地址或点击地图选点';
|
||||
if (this.transportAmapMarker) {
|
||||
this.transportAmapMarker.setMap(null);
|
||||
this.transportAmapMarker = null;
|
||||
}
|
||||
this.$refs.crud?.clearValidate?.([
|
||||
'departureAddress',
|
||||
'arrivalAddress',
|
||||
'departureName',
|
||||
'arrivalName',
|
||||
'departureContact',
|
||||
'departurePhone',
|
||||
'arrivalContact',
|
||||
'arrivalPhone',
|
||||
]);
|
||||
},
|
||||
openTransportPrimaryAddressPicker(target) {
|
||||
if (this.transportStationMode) this.openTransportStationDialog(target);
|
||||
},
|
||||
handleTransportSecondaryAddressInputClick(target) {
|
||||
if (this.transportStationMode || !this.config.editableRoadAddress)
|
||||
this.openTransportSecondaryAddressPicker(target);
|
||||
if (this.transportAddressSelectDisabled) return;
|
||||
this.openTransportSecondaryAddressPicker(target);
|
||||
},
|
||||
openTransportSecondaryAddressPicker(target) {
|
||||
this.transportStationMode
|
||||
@@ -3328,6 +3391,13 @@ export default {
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.shipping-template-page__address-map-input {
|
||||
cursor: pointer;
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-input__inner) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.shipping-template-page__section-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
@@ -85,15 +85,23 @@
|
||||
</template>
|
||||
|
||||
<template #departureAddress="{ row }">
|
||||
<el-tooltip v-if="row.departureAddress" :content="row.departureAddress" placement="top">
|
||||
<span>{{ formatTransportPlanProvinceCityDistrict(row.departureAddress) }}</span>
|
||||
<el-tooltip
|
||||
v-if="row.departureAddress || row.departureName"
|
||||
:content="row.departureAddress || row.departureName"
|
||||
placement="top"
|
||||
>
|
||||
<span>{{ formatTransportPlanListAddress(row, 'departure') }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #arrivalAddress="{ row }">
|
||||
<el-tooltip v-if="row.arrivalAddress" :content="row.arrivalAddress" placement="top">
|
||||
<span>{{ formatTransportPlanProvinceCityDistrict(row.arrivalAddress) }}</span>
|
||||
<el-tooltip
|
||||
v-if="row.arrivalAddress || row.arrivalName"
|
||||
:content="row.arrivalAddress || row.arrivalName"
|
||||
placement="top"
|
||||
>
|
||||
<span>{{ formatTransportPlanListAddress(row, 'arrival') }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
@@ -574,7 +582,7 @@
|
||||
>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="hasPermission(`${config.permission}_view`) && !showDetailButton(row)"
|
||||
v-if="showViewButton(row)"
|
||||
@click="$refs.crud.rowView(row, index)"
|
||||
>
|
||||
查看
|
||||
@@ -1492,27 +1500,38 @@
|
||||
<el-input
|
||||
v-model="dispatchItemForm.departureName"
|
||||
:placeholder="dispatchStationNamePlaceholder"
|
||||
:suffix-icon="dispatchStationMode ? Search : undefined"
|
||||
disabled
|
||||
/>
|
||||
:readonly="dispatchStationMode"
|
||||
@click="openDispatchPrimaryAddressPicker('departure')"
|
||||
>
|
||||
<template v-if="dispatchStationMode" #suffix>
|
||||
<el-icon
|
||||
class="transport-plan-page__map-suffix"
|
||||
@click.stop="openDispatchSecondaryAddressPicker('departure')"
|
||||
>
|
||||
<Search />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input
|
||||
v-model="dispatchItemForm.departureAddress"
|
||||
:placeholder="dispatchAddressDetailPlaceholder"
|
||||
disabled
|
||||
:readonly="dispatchStationMode"
|
||||
@click="handleDispatchSecondaryAddressInputClick('departure')"
|
||||
>
|
||||
<template v-if="!dispatchStationMode" #suffix>
|
||||
<el-link
|
||||
type="primary"
|
||||
:underline="false"
|
||||
class="transport-plan-page__address-choose-link"
|
||||
@click.stop="openTransportMapDialog('departure')"
|
||||
>选择</el-link
|
||||
>
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="transport-plan-page__route-label">联系人</span>
|
||||
<el-input v-model="dispatchItemForm.departureContact" placeholder="请输入" disabled />
|
||||
<el-input v-model="dispatchItemForm.departureContact" placeholder="请输入" />
|
||||
<span class="transport-plan-page__route-label">联系方式</span>
|
||||
<el-input v-model="dispatchItemForm.departurePhone" placeholder="请输入" disabled />
|
||||
<el-input v-model="dispatchItemForm.departurePhone" placeholder="请输入" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="收货地址" required>
|
||||
@@ -1520,27 +1539,38 @@
|
||||
<el-input
|
||||
v-model="dispatchItemForm.arrivalName"
|
||||
:placeholder="dispatchStationNamePlaceholder"
|
||||
:suffix-icon="dispatchStationMode ? Search : undefined"
|
||||
disabled
|
||||
/>
|
||||
:readonly="dispatchStationMode"
|
||||
@click="openDispatchPrimaryAddressPicker('arrival')"
|
||||
>
|
||||
<template v-if="dispatchStationMode" #suffix>
|
||||
<el-icon
|
||||
class="transport-plan-page__map-suffix"
|
||||
@click.stop="openDispatchSecondaryAddressPicker('arrival')"
|
||||
>
|
||||
<Search />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input
|
||||
v-model="dispatchItemForm.arrivalAddress"
|
||||
:placeholder="dispatchAddressDetailPlaceholder"
|
||||
disabled
|
||||
:readonly="dispatchStationMode"
|
||||
@click="handleDispatchSecondaryAddressInputClick('arrival')"
|
||||
>
|
||||
<template v-if="!dispatchStationMode" #suffix>
|
||||
<el-link
|
||||
type="primary"
|
||||
:underline="false"
|
||||
class="transport-plan-page__address-choose-link"
|
||||
@click.stop="openTransportMapDialog('arrival')"
|
||||
>选择</el-link
|
||||
>
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="transport-plan-page__route-label">联系人</span>
|
||||
<el-input v-model="dispatchItemForm.arrivalContact" placeholder="请输入" disabled />
|
||||
<el-input v-model="dispatchItemForm.arrivalContact" placeholder="请输入" />
|
||||
<span class="transport-plan-page__route-label">联系方式</span>
|
||||
<el-input v-model="dispatchItemForm.arrivalPhone" placeholder="请输入" disabled />
|
||||
<el-input v-model="dispatchItemForm.arrivalPhone" placeholder="请输入" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
@@ -1576,20 +1606,15 @@
|
||||
><span class="transport-plan-page__required-column">货物类型</span></template
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
<el-autocomplete
|
||||
v-model="row.cargoType"
|
||||
placeholder="请选择货物类型"
|
||||
placeholder="请输入货物类型"
|
||||
clearable
|
||||
filterable
|
||||
:debounce="300"
|
||||
:fetch-suggestions="fetchDispatchConfiguredCargoTypeSuggestions"
|
||||
@select="item => handleDispatchConfiguredCargoTypeSelect(row, item)"
|
||||
@change="value => handleDispatchConfiguredCargoTypeChange(row, value)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in getDispatchConfiguredCargoTypeOptions()"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column min-width="240" align="center">
|
||||
@@ -1597,20 +1622,18 @@
|
||||
><span class="transport-plan-page__required-column">货物名称</span></template
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
<el-autocomplete
|
||||
v-model="row.cargoName"
|
||||
placeholder="请选择货物名称"
|
||||
placeholder="请输入货物名称"
|
||||
clearable
|
||||
filterable
|
||||
:debounce="300"
|
||||
:fetch-suggestions="
|
||||
(queryString, callback) =>
|
||||
fetchDispatchConfiguredCargoNameSuggestions(queryString, callback, row)
|
||||
"
|
||||
@select="item => handleDispatchConfiguredCargoNameSelect(row, item)"
|
||||
@change="value => handleDispatchConfiguredCargoNameChange(row, value)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in getDispatchConfiguredCargoNameOptions(row)"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="包装" min-width="130" align="center">
|
||||
@@ -1627,7 +1650,11 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余数量" min-width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDispatchQuantity(dispatchItemRemainingQuantity(row)) }}
|
||||
{{
|
||||
isDispatchPlanQuantityUnlimited(row)
|
||||
? '-'
|
||||
: formatDispatchQuantity(dispatchItemRemainingQuantity(row))
|
||||
}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column min-width="130" align="center">
|
||||
@@ -1848,36 +1875,33 @@
|
||||
<el-input v-model="dispatchItemForm.escortPhone" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<el-form-item label="货物类型" required>
|
||||
<el-select
|
||||
<el-autocomplete
|
||||
v-model="dispatchItemForm.cargoType"
|
||||
placeholder="请选择货物类型"
|
||||
placeholder="请输入货物类型"
|
||||
clearable
|
||||
filterable
|
||||
@change="handleDispatchConfiguredCargoTypeChange(dispatchItemForm, $event)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in getDispatchConfiguredCargoTypeOptions()"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
:debounce="300"
|
||||
:fetch-suggestions="fetchDispatchConfiguredCargoTypeSuggestions"
|
||||
@select="item => handleDispatchConfiguredCargoTypeSelect(dispatchItemForm, item)"
|
||||
@change="value => handleDispatchConfiguredCargoTypeChange(dispatchItemForm, value)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="货物名称" required>
|
||||
<el-select
|
||||
<el-autocomplete
|
||||
v-model="dispatchItemForm.cargoName"
|
||||
placeholder="请选择货物名称"
|
||||
placeholder="请输入货物名称"
|
||||
clearable
|
||||
filterable
|
||||
@change="handleDispatchConfiguredCargoNameChange(dispatchItemForm, $event)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in getDispatchConfiguredCargoNameOptions(dispatchItemForm)"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
:debounce="300"
|
||||
:fetch-suggestions="
|
||||
(queryString, callback) =>
|
||||
fetchDispatchConfiguredCargoNameSuggestions(
|
||||
queryString,
|
||||
callback,
|
||||
dispatchItemForm
|
||||
)
|
||||
"
|
||||
@select="item => handleDispatchConfiguredCargoNameSelect(dispatchItemForm, item)"
|
||||
@change="value => handleDispatchConfiguredCargoNameChange(dispatchItemForm, value)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="规格">
|
||||
<el-input v-model="dispatchItemForm.specification" placeholder="请输入" />
|
||||
@@ -1902,7 +1926,10 @@
|
||||
</el-select>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="transport-plan-page__dispatch-quantity-hint">
|
||||
<div
|
||||
v-if="!isDispatchPlanQuantityUnlimited(dispatchItemForm)"
|
||||
class="transport-plan-page__dispatch-quantity-hint"
|
||||
>
|
||||
剩余数量:{{
|
||||
formatDispatchQuantity(dispatchItemRemainingQuantity(dispatchItemForm))
|
||||
}}
|
||||
@@ -3412,15 +3439,21 @@ export default {
|
||||
}));
|
||||
},
|
||||
dispatchHasAddableQuantity() {
|
||||
return this.dispatchTableQuantityItems.some(
|
||||
item => item.total > 0 && item.listed < item.total - 1e-8
|
||||
);
|
||||
const items = this.dispatchTableQuantityItems;
|
||||
if (!items.length) return true;
|
||||
// 计划总量为 0 时不限制新增调度
|
||||
if (items.every(item => item.total <= 0)) return true;
|
||||
return items.some(item => item.total > 0 && item.listed < item.total - 1e-8);
|
||||
},
|
||||
dispatchSummaryText() {
|
||||
const formatSummary = field =>
|
||||
this.dispatchSummaryItems
|
||||
.map(item => `${this.formatDispatchQuantity(item[field])}${item.unit}`)
|
||||
.join('、');
|
||||
const unlimited = this.dispatchSummaryItems.every(item => item.total <= 0);
|
||||
if (unlimited) {
|
||||
return `已调度 ${formatSummary('assigned') || `0${TRANSPORT_PLAN_QUANTITY_UNIT}`}`;
|
||||
}
|
||||
return `共${formatSummary('total')} | 已调度 ${formatSummary(
|
||||
'assigned'
|
||||
)},剩余${formatSummary('remaining')}`;
|
||||
@@ -3606,12 +3639,13 @@ export default {
|
||||
return ['water', 'rail', 'air'].includes(this.transportMode);
|
||||
},
|
||||
transportStationTypeLabel() {
|
||||
const mode = this.dispatchItemBox ? this.dispatchTransportMode : this.transportMode;
|
||||
const labelMap = {
|
||||
water: '港口/码头',
|
||||
rail: '铁路车站',
|
||||
air: '空港机场',
|
||||
};
|
||||
return labelMap[this.transportMode] || '';
|
||||
return labelMap[mode] || '';
|
||||
},
|
||||
transportStationNamePlaceholder() {
|
||||
if (!this.transportStationMode) return '行政区划自动带出';
|
||||
@@ -3798,6 +3832,37 @@ export default {
|
||||
const cityMatch = text.match(/市/);
|
||||
return cityMatch ? text.slice(0, cityMatch.index + 1) : text;
|
||||
},
|
||||
formatTransportPlanListAddress(row = {}, field) {
|
||||
const address = String(row[`${field}Address`] || '').trim();
|
||||
const name = String(row[`${field}Name`] || '').trim();
|
||||
const fromAddress = this.extractTransportPlanProvinceCityDistrict(address);
|
||||
if (fromAddress) return fromAddress;
|
||||
const fromName = this.extractTransportPlanProvinceCityDistrict(name);
|
||||
if (fromName) return fromName;
|
||||
return name || address || '-';
|
||||
},
|
||||
extractTransportPlanProvinceCityDistrict(value) {
|
||||
const text = String(value || '')
|
||||
.trim()
|
||||
.replace(/[\\/||、,,\s]+/g, '');
|
||||
if (!text) return '';
|
||||
const districtPattern = '(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)';
|
||||
const parts = [];
|
||||
let remainder = text;
|
||||
const province = text.match(/^(.+?(?:省|自治区|特别行政区))/);
|
||||
if (province) {
|
||||
parts.push(province[1]);
|
||||
remainder = text.slice(province[1].length);
|
||||
}
|
||||
const city = remainder.match(/^(.+?市)/);
|
||||
if (city) {
|
||||
parts.push(city[1]);
|
||||
remainder = remainder.slice(city[1].length);
|
||||
}
|
||||
const district = remainder.match(new RegExp(`^(.+?${districtPattern})`));
|
||||
if (district) parts.push(district[1]);
|
||||
return parts.length ? parts.join('/') : '';
|
||||
},
|
||||
formatTransportPlanRoadCityDistrict(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (!text) return '-';
|
||||
@@ -3955,6 +4020,11 @@ export default {
|
||||
const status = this.statusValue(row);
|
||||
return status !== 'draft';
|
||||
},
|
||||
showViewButton(row) {
|
||||
if (!this.hasPermission(`${this.config.permission}_view`)) return false;
|
||||
if (this.statusValue(row) === 'draft') return false;
|
||||
return !this.showDetailButton(row);
|
||||
},
|
||||
formatDetailValue(row, prop) {
|
||||
if (!row) return '-';
|
||||
if (prop === 'businessStatus') return this.displayStatus(row, prop);
|
||||
@@ -4540,6 +4610,37 @@ export default {
|
||||
getDispatchRemainingQuantity(unit) {
|
||||
return this.getDispatchSummaryItem(unit)?.remaining || 0;
|
||||
},
|
||||
/** 计划总量为 0 时不限制调度数量 */
|
||||
isDispatchPlanQuantityUnlimited(row = {}) {
|
||||
const unit = this.getDispatchQuantityUnit(row);
|
||||
const hasCargoIdentity = Boolean(
|
||||
String(row.cargoName || row.goodsName || row.name || '').trim() ||
|
||||
String(row.cargoType || row.secondCargoTypeName || row.goodsType || row.type || '').trim()
|
||||
);
|
||||
const planGoodsRows = this.dispatchPlanGoodsRows;
|
||||
if (planGoodsRows.length && hasCargoIdentity) {
|
||||
const cargoKey = this.getDispatchCargoIdentity(row);
|
||||
const matchedPlanRows = planGoodsRows.filter(
|
||||
goods => this.getDispatchCargoIdentity(goods) === cargoKey
|
||||
);
|
||||
if (!matchedPlanRows.length) {
|
||||
return (this.getDispatchSummaryItem(unit)?.total || 0) <= 0;
|
||||
}
|
||||
const total = matchedPlanRows.reduce(
|
||||
(sum, goods) =>
|
||||
sum +
|
||||
this.parseDispatchQuantity(
|
||||
goods.quantity || goods.cargoQuantity || goods.goodsQuantity
|
||||
),
|
||||
0
|
||||
);
|
||||
return total <= 0;
|
||||
}
|
||||
if (!planGoodsRows.length) {
|
||||
return Number(this.dispatchRow.totalQuantity || this.dispatchRow.quantity || 0) <= 0;
|
||||
}
|
||||
return (this.getDispatchSummaryItem(unit)?.total || 0) <= 0;
|
||||
},
|
||||
formatDispatchQuantity(value) {
|
||||
const number = Number(value || 0);
|
||||
if (!Number.isFinite(number)) return '0';
|
||||
@@ -5238,6 +5339,13 @@ export default {
|
||||
}
|
||||
},
|
||||
ensureTransportTypeBeforeAddress() {
|
||||
if (this.dispatchItemBox) {
|
||||
const transportType =
|
||||
this.dispatchItemForm.transportType || this.dispatchRow?.transportType || '';
|
||||
if (!this.isBillingFieldEmpty(transportType)) return true;
|
||||
this.$message.warning('请先选择运输方式');
|
||||
return false;
|
||||
}
|
||||
if (!this.shippingInfoFormEnabled || !this.isBillingFieldEmpty(this.form.transportType)) {
|
||||
return true;
|
||||
}
|
||||
@@ -5314,19 +5422,21 @@ export default {
|
||||
},
|
||||
openTransportStationDialog(target) {
|
||||
if (!this.ensureTransportTypeBeforeAddress()) return;
|
||||
if (!this.transportStationMode) return;
|
||||
const stationMode = this.dispatchItemBox ? this.dispatchStationMode : this.transportStationMode;
|
||||
if (!stationMode) return;
|
||||
this.transportStationTarget = target;
|
||||
this.transportStationQuery = {};
|
||||
this.transportStationPage.currentPage = 1;
|
||||
this.transportStationBox = true;
|
||||
},
|
||||
getTransportStationListRequest() {
|
||||
const mode = this.dispatchItemBox ? this.dispatchTransportMode : this.transportMode;
|
||||
const requestMap = {
|
||||
water: getPortTerminalList,
|
||||
rail: getRailwayStationList,
|
||||
air: getAirportMasterList,
|
||||
};
|
||||
return requestMap[this.transportMode];
|
||||
return requestMap[mode];
|
||||
},
|
||||
loadTransportStationList() {
|
||||
const request = this.getTransportStationListRequest();
|
||||
@@ -5436,7 +5546,28 @@ export default {
|
||||
water: '港口/码头',
|
||||
air: '空港机场',
|
||||
};
|
||||
return typeMap[this.transportMode] || '';
|
||||
const mode = this.dispatchItemBox ? this.dispatchTransportMode : this.transportMode;
|
||||
return typeMap[mode] || '';
|
||||
},
|
||||
getTransportAddressForm() {
|
||||
return this.dispatchItemBox ? this.dispatchItemForm : this.form;
|
||||
},
|
||||
openDispatchPrimaryAddressPicker(target) {
|
||||
if (this.dispatchStationMode) {
|
||||
this.openTransportStationDialog(target);
|
||||
}
|
||||
},
|
||||
openDispatchSecondaryAddressPicker(target) {
|
||||
if (this.dispatchStationMode) {
|
||||
this.openTransportStationDialog(target);
|
||||
return;
|
||||
}
|
||||
this.openTransportMapDialog(target);
|
||||
},
|
||||
handleDispatchSecondaryAddressInputClick(target) {
|
||||
if (this.dispatchStationMode) {
|
||||
this.openDispatchSecondaryAddressPicker(target);
|
||||
}
|
||||
},
|
||||
loadTransportAddressList() {
|
||||
this.transportAddressLoading = true;
|
||||
@@ -5487,35 +5618,39 @@ export default {
|
||||
},
|
||||
applyTransportAddress(target, address = {}) {
|
||||
const prefix = target === 'departure' ? 'departure' : 'arrival';
|
||||
this.form[`${prefix}Name`] = address.preferRegionName
|
||||
const addressForm = this.getTransportAddressForm();
|
||||
addressForm[`${prefix}Name`] = address.preferRegionName
|
||||
? address.regionName || address.addressName || ''
|
||||
: address.addressName || address.regionName || '';
|
||||
this.form[`${prefix}Address`] = address.detailAddress || address.address || '';
|
||||
this.form[`${prefix}AddressCode`] = address.addressCode || '';
|
||||
this.form[`${prefix}SiteCode`] =
|
||||
addressForm[`${prefix}Address`] = address.detailAddress || address.address || '';
|
||||
addressForm[`${prefix}AddressCode`] = address.addressCode || '';
|
||||
addressForm[`${prefix}SiteCode`] =
|
||||
address.addressType === '常规地址'
|
||||
? '/'
|
||||
: address.siteCodeDisplay || address.siteCode || '';
|
||||
this.form[`${prefix}Longitude`] = address.longitude || '';
|
||||
this.form[`${prefix}Latitude`] = address.latitude || '';
|
||||
this.form[`${prefix}Contact`] = address.contactName || this.form[`${prefix}Contact`] || '';
|
||||
this.form[`${prefix}Phone`] = address.contactPhone || this.form[`${prefix}Phone`] || '';
|
||||
addressForm[`${prefix}Longitude`] = address.longitude || '';
|
||||
addressForm[`${prefix}Latitude`] = address.latitude || '';
|
||||
addressForm[`${prefix}Contact`] =
|
||||
address.contactName || addressForm[`${prefix}Contact`] || '';
|
||||
addressForm[`${prefix}Phone`] = address.contactPhone || addressForm[`${prefix}Phone`] || '';
|
||||
// 地址值在本轮响应式更新后再清理校验状态,避免 Avue 以旧空值重新触发必填提示。
|
||||
this.$nextTick(() => {
|
||||
this.$refs.crud?.clearValidate?.([`${prefix}Address`]);
|
||||
});
|
||||
},
|
||||
openTransportMapDialog(target) {
|
||||
if (this.dialogReadonly) return;
|
||||
if (this.dialogReadonly && !this.dispatchItemBox) return;
|
||||
if (!this.ensureTransportTypeBeforeAddress()) return;
|
||||
const prefix = target === 'departure' ? 'departure' : 'arrival';
|
||||
const addressForm = this.getTransportAddressForm();
|
||||
this.transportMapTarget = prefix;
|
||||
this.transportMapKeyword = this.form[`${prefix}Address`] || this.form[`${prefix}Name`] || '';
|
||||
this.transportMapKeyword =
|
||||
addressForm[`${prefix}Address`] || addressForm[`${prefix}Name`] || '';
|
||||
this.transportMapSelected = this.buildTransportMapSelection({
|
||||
lng: this.form[`${prefix}Longitude`],
|
||||
lat: this.form[`${prefix}Latitude`],
|
||||
address: this.form[`${prefix}Address`],
|
||||
regionName: this.form[`${prefix}Name`],
|
||||
lng: addressForm[`${prefix}Longitude`],
|
||||
lat: addressForm[`${prefix}Latitude`],
|
||||
address: addressForm[`${prefix}Address`],
|
||||
regionName: addressForm[`${prefix}Name`],
|
||||
});
|
||||
this.transportMapStatus = this.transportMapSelected.longitude
|
||||
? '已加载当前选点,可重新选点'
|
||||
@@ -5673,12 +5808,13 @@ export default {
|
||||
return;
|
||||
}
|
||||
const prefix = this.transportMapTarget;
|
||||
this.form[`${prefix}Address`] =
|
||||
this.transportMapSelected.detailAddress || this.form[`${prefix}Address`];
|
||||
this.form[`${prefix}Name`] =
|
||||
this.transportMapSelected.regionName || this.form[`${prefix}Name`];
|
||||
this.form[`${prefix}Longitude`] = this.transportMapSelected.longitude;
|
||||
this.form[`${prefix}Latitude`] = this.transportMapSelected.latitude;
|
||||
const addressForm = this.getTransportAddressForm();
|
||||
addressForm[`${prefix}Address`] =
|
||||
this.transportMapSelected.detailAddress || addressForm[`${prefix}Address`];
|
||||
addressForm[`${prefix}Name`] =
|
||||
this.transportMapSelected.regionName || addressForm[`${prefix}Name`];
|
||||
addressForm[`${prefix}Longitude`] = this.transportMapSelected.longitude;
|
||||
addressForm[`${prefix}Latitude`] = this.transportMapSelected.latitude;
|
||||
this.transportMapBox = false;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.crud?.clearValidate?.([`${prefix}Address`]);
|
||||
@@ -7287,8 +7423,9 @@ export default {
|
||||
item.cargoTypePath.join('/') === rowTypePath.join('/')
|
||||
)
|
||||
: goodsOptions;
|
||||
const source = filtered.length ? filtered : goodsOptions;
|
||||
const options = new Map();
|
||||
filtered.forEach(item => {
|
||||
source.forEach(item => {
|
||||
if (!item.cargoName || options.has(item.cargoName)) return;
|
||||
options.set(item.cargoName, {
|
||||
label: item.cargoName,
|
||||
@@ -7302,26 +7439,73 @@ export default {
|
||||
});
|
||||
return Array.from(options.values());
|
||||
},
|
||||
handleDispatchConfiguredCargoTypeChange(row, value) {
|
||||
const option = this.getDispatchConfiguredCargoTypeOptions().find(
|
||||
item => String(item.value) === String(value || '')
|
||||
fetchDispatchConfiguredCargoTypeSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim().toLowerCase();
|
||||
const options = this.getDispatchConfiguredCargoTypeOptions().map(item => ({
|
||||
...item,
|
||||
value: item.label,
|
||||
}));
|
||||
callback(
|
||||
keyword
|
||||
? options.filter(item => String(item.value).toLowerCase().includes(keyword))
|
||||
: options
|
||||
);
|
||||
row.cargoType = option?.label || '';
|
||||
row.cargoTypeCode = option?.cargoTypeCode || '';
|
||||
row.cargoTypePath = option?.cargoTypePath || [];
|
||||
},
|
||||
fetchDispatchConfiguredCargoNameSuggestions(queryString, callback, row = {}) {
|
||||
const keyword = String(queryString || '').trim().toLowerCase();
|
||||
const options = this.getDispatchConfiguredCargoNameOptions(row).map(item => ({
|
||||
...item,
|
||||
value: item.label,
|
||||
}));
|
||||
callback(
|
||||
keyword
|
||||
? options.filter(item => String(item.value).toLowerCase().includes(keyword))
|
||||
: options
|
||||
);
|
||||
},
|
||||
handleDispatchConfiguredCargoTypeSelect(row, item = {}) {
|
||||
this.handleDispatchConfiguredCargoTypeChange(row, item.value || item.label || '');
|
||||
},
|
||||
handleDispatchConfiguredCargoNameSelect(row, item = {}) {
|
||||
this.handleDispatchConfiguredCargoNameChange(row, item.value || item.label || '');
|
||||
},
|
||||
handleDispatchConfiguredCargoTypeChange(row, value) {
|
||||
const nextValue = String(value ?? row.cargoType ?? '').trim();
|
||||
const option = this.getDispatchConfiguredCargoTypeOptions().find(
|
||||
item => String(item.value) === nextValue || item.label === nextValue
|
||||
);
|
||||
if (option) {
|
||||
row.cargoType = option.label || '';
|
||||
row.cargoTypeCode = option.cargoTypeCode || '';
|
||||
row.cargoTypePath = option.cargoTypePath || [];
|
||||
} else {
|
||||
row.cargoType = nextValue;
|
||||
row.cargoTypeCode = '';
|
||||
row.cargoTypePath = [];
|
||||
}
|
||||
row.cargoName = '';
|
||||
row.specification = '';
|
||||
row.model = '';
|
||||
},
|
||||
handleDispatchConfiguredCargoNameChange(row, value) {
|
||||
const nextValue = String(value ?? row.cargoName ?? '').trim();
|
||||
const option = this.getDispatchConfiguredCargoNameOptions(row).find(
|
||||
item => String(item.value) === String(value || '')
|
||||
item => String(item.value) === nextValue || item.label === nextValue
|
||||
);
|
||||
row.cargoName = option?.value || '';
|
||||
if (!option) return;
|
||||
row.cargoType = option.cargoTypeLabel || row.cargoType || '';
|
||||
row.cargoTypeCode = option.cargoTypeCode || row.cargoTypeCode || '';
|
||||
row.cargoTypePath = option.cargoTypePath || row.cargoTypePath || [];
|
||||
if (!option) {
|
||||
row.cargoName = nextValue;
|
||||
return;
|
||||
}
|
||||
const previousType = String(row.cargoType || '').trim();
|
||||
const isKnownType = this.getDispatchConfiguredCargoTypeOptions().some(
|
||||
item => item.label === previousType || String(item.value) === previousType
|
||||
);
|
||||
row.cargoName = option.value || '';
|
||||
if (!previousType || isKnownType) {
|
||||
row.cargoType = option.cargoTypeLabel || previousType;
|
||||
row.cargoTypeCode = option.cargoTypeCode || row.cargoTypeCode || '';
|
||||
row.cargoTypePath = option.cargoTypePath || row.cargoTypePath || [];
|
||||
}
|
||||
row.specification = option.specification || row.specification || '';
|
||||
row.model = option.model || row.model || '';
|
||||
},
|
||||
@@ -7345,10 +7529,7 @@ export default {
|
||||
vehicleNo: this.dispatchRow.vehicleNo || '',
|
||||
cargoName: defaultGoods.cargoName || this.dispatchRow.cargoName || '',
|
||||
cargoType: defaultGoods.cargoType || this.dispatchRow.cargoType || '',
|
||||
quantity:
|
||||
index >= 0
|
||||
? defaultGoods.quantity || ''
|
||||
: this.formatDispatchQuantity(this.getDispatchRemainingQuantity(defaultUnit)),
|
||||
quantity: '',
|
||||
quantityUnit: defaultUnit,
|
||||
specification: defaultGoods.specification || '',
|
||||
model: defaultGoods.model || '',
|
||||
@@ -8003,6 +8184,7 @@ export default {
|
||||
quantityGroups.set(key, group);
|
||||
});
|
||||
for (const { row: goods, quantity } of quantityGroups.values()) {
|
||||
if (this.isDispatchPlanQuantityUnlimited(goods)) continue;
|
||||
const unit = this.getDispatchQuantityUnit(goods);
|
||||
const remaining = this.dispatchItemRemainingQuantity(goods);
|
||||
if (quantity - remaining > 1e-8) {
|
||||
|
||||
@@ -5177,15 +5177,28 @@ export default {
|
||||
delete sourceData.code;
|
||||
delete sourceData.waybillNo;
|
||||
delete sourceData.waybillStatus;
|
||||
delete sourceData.businessStatus;
|
||||
delete sourceData.businessStatusName;
|
||||
delete sourceData.driverAcceptStatus;
|
||||
delete sourceData.driverAcceptTime;
|
||||
delete sourceData.driverAcceptDriverId;
|
||||
delete sourceData.driverRejectTime;
|
||||
delete sourceData.driverRejectReason;
|
||||
delete sourceData.loadingNo;
|
||||
delete sourceData.masterNo;
|
||||
delete sourceData.createTime;
|
||||
delete sourceData.updateTime;
|
||||
delete sourceData.createUser;
|
||||
delete sourceData.updateUser;
|
||||
delete sourceData.createDept;
|
||||
delete sourceData.createUserName;
|
||||
delete sourceData.updateUserName;
|
||||
|
||||
this.applyFormDetail(sourceData);
|
||||
// 标记为手工创建
|
||||
// 标记为手工创建,状态与新增一致(暂存=草稿,确认后按过程配置校正)
|
||||
this.form.dataSource = '手工创建';
|
||||
this.form.businessStatus = '';
|
||||
this.form.businessStatusName = '';
|
||||
done();
|
||||
});
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user