调整 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
@@ -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;