调整 iam
This commit is contained in:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user