diff --git a/src/option/business/shipping-template.js b/src/option/business/shipping-template.js index bc573b4..bf5f65b 100644 --- a/src/option/business/shipping-template.js +++ b/src/option/business/shipping-template.js @@ -42,6 +42,7 @@ export const config = { }, excludeVoidedProjects: true, enableAttachmentTable: true, + detailAttachmentDescriptionPlain: true, enableTransportPlanForm: true, enableShippingTemplateFreight: true, editableRoadAddress: true, diff --git a/src/views/business/components/business-crud-page.vue b/src/views/business/components/business-crud-page.vue index a495342..cfe4da6 100644 --- a/src/views/business/components/business-crud-page.vue +++ b/src/views/business/components/business-crud-page.vue @@ -444,27 +444,26 @@ 货物名称 - visible && loadTaskCargoOptionsByPath(row.cargoTypePath) - " - @change="value => handleTaskCargoRowNameChange(row, value)" + @select="item => handleTaskCargoRowSelect(row, item)" > - - + + + {{ formatBillingCargoTypeLabel(item) }} + {{ item.cargoCode }} + + + @@ -655,26 +654,16 @@ label="司机" class="business-crud-page__task-span-1" > - visible && loadTaskDriverOptions()" - @change="handleTaskDriverChange" - > - - + @select="item => handleTaskDriverSelect('form', item)" + /> - visible && loadTaskDriverOptions()" - @change="handleTaskDriverChange" - > - - + @select="item => handleTaskDriverSelect('form', item)" + /> - visible && loadTaskDriverOptions()" - @change="handleTaskDriverChange" - > - - + @select="item => handleTaskDriverSelect('form', item)" + /> - visible && loadTaskCargoOptionsForForm()" - @change="handleTaskCargoChange" + @select="handleTaskCargoSelect" > - - + + + {{ formatBillingCargoTypeLabel(item) }} + {{ item.cargoCode }} + + + - visible && loadTaskCargoOptionsByPath(row.cargoTypePath) - " - @change="value => handleTaskCargoRowNameChange(row, value)" + @select="item => handleTaskCargoRowSelect(row, item)" > - - + + + {{ formatBillingCargoTypeLabel(item) }} + {{ item.cargoCode }} + + + @@ -2814,6 +2784,21 @@ v-if="config.enableAttachmentTable" :title="config.attachmentTitle || '附件'" > + + + + {{ attachmentName(row) }}: + + {{ row.description || '-' }} + + @@ -2823,7 +2808,12 @@ - + {{ formatFileSize(row.size) }} @@ -3477,7 +3467,7 @@ - + 待调度列表 @@ -3487,7 +3477,7 @@ 新增 @@ -3496,7 +3486,7 @@ @@ -3706,17 +3696,20 @@ 货物类型 - visible && ensureBillingCargoTypeOptions()" - @change="value => handleDispatchCargoTypeChange(row, value)" - /> + @change="value => handleDispatchConfiguredCargoTypeChange(row, value)" + > + + @@ -3724,20 +3717,16 @@ visible && loadTaskCargoOptionsByPath(row.cargoTypePath)" - @change="value => handleDispatchCargoRowNameChange(row, value)" + filterable + @change="value => handleDispatchConfiguredCargoNameChange(row, value)" > @@ -3749,8 +3738,13 @@ + + + {{ formatDispatchQuantity(dispatchItemRemainingQuantity(row)) }} + + - 数量 + 本次数量 handleDispatchCargoQuantityInput(row, value)" /> @@ -3787,12 +3781,12 @@ visible && loadTaskFeeUnitOptions()"> - - + {{ cargo.quantityUnit || '吨' }} + {{ dispatchItemFreightCurrencyLabel }} - {{ dispatchItemFreightCurrencyLabel }} - visible && loadShippingTemplateCurrencyOptions()"> handleDispatchNumberInput('otherFeeTotal', value)">{{ dispatchItemFreightCurrencyLabel }} + {{ dispatchItemFreightCurrencyLabel }} + visible && loadShippingTemplateCurrencyOptions()"> @@ -3833,25 +3827,15 @@ - visible && loadDispatchDriverOptions()" - @change="handleDispatchDriverChange" - > - - + @select="item => handleTaskDriverSelect('dispatch', item)" + /> - + - + + + - + + + @@ -3891,7 +3899,7 @@ - + + + 剩余数量:{{ + formatDispatchQuantity(dispatchItemRemainingQuantity(dispatchItemForm)) + }} + {{ dispatchItemForm.quantityUnit || '吨' }} + - visible && loadDispatchDriverOptions()" - @change="handleDispatchDriverChange" - > - - + @select="item => handleTaskDriverSelect('dispatch', item)" + /> @@ -5469,7 +5470,7 @@ export default { zoom: true, }, transportCargoRows: [], - taskFreightCurrency: 'CNY', + taskFreightCurrency: 'RMB', shippingTemplateFreight: defaultShippingTemplateFreight(), shippingTemplateCurrencyOptions: [], shippingTemplateCurrencyLoading: false, @@ -5847,7 +5848,7 @@ export default { return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(3))); }, dispatchPlanGoodsRows() { - return this.parseJsonArray(this.dispatchRow.goodsJson); + return this.dispatchGoodsRows(this.dispatchRow.goodsJson); }, dispatchPlanCargoInfo() { const cargoInfo = this.dispatchPlanGoodsRows @@ -5882,10 +5883,12 @@ export default { }); if (!this.dispatchRows.length) { this.parseJsonArray(this.dispatchRow.dispatchRows).forEach(row => { + if (!this.hasDispatchVehicleIdentifier(row)) return; addQuantity(this.getDispatchQuantityUnit(row), 'assigned', row.quantity); }); } this.dispatchRows.forEach(row => { + if (!this.hasDispatchVehicleIdentifier(row)) return; addQuantity(this.getDispatchQuantityUnit(row), 'assigned', row.quantity); }); @@ -5897,6 +5900,30 @@ export default { dispatchHasRemainingQuantity() { return this.dispatchSummaryItems.some(item => item.remaining > 0); }, + dispatchTableQuantityItems() { + const quantityMap = new Map(); + this.dispatchRows.forEach(row => { + this.getDispatchGoodsSourceRows(row).forEach(goods => { + const unit = this.getDispatchQuantityUnit(goods); + quantityMap.set( + unit, + (quantityMap.get(unit) || 0) + + this.parseDispatchQuantity( + goods.quantity || goods.cargoQuantity || goods.goodsQuantity + ) + ); + }); + }); + return this.dispatchSummaryItems.map(item => ({ + ...item, + listed: quantityMap.get(item.unit) || 0, + })); + }, + dispatchHasAddableQuantity() { + return this.dispatchTableQuantityItems.some( + item => item.total > 0 && item.listed < item.total - 1e-8 + ); + }, dispatchSummaryText() { const formatSummary = field => this.dispatchSummaryItems @@ -6027,8 +6054,7 @@ export default { taskFreightCurrencyLabel() { const currency = this.taskFreightCurrency; if (!currency) return ''; - const option = this.shippingTemplateCurrencyOptions.find(item => item.value === currency); - return this.currencyName(option?.label || currency); + return this.currencyRemark(currency); }, isAdmin() { const authority = this.userInfo && this.userInfo.authority; @@ -6530,7 +6556,7 @@ export default { }; }); this.form.goodsJson = JSON.stringify(enrichedGoodsRows); - this.taskFreightCurrency = freight.currency || this.taskFreightCurrency || 'CNY'; + this.taskFreightCurrency = freight.currency || this.taskFreightCurrency || 'RMB'; this.form.otherFeeTotal = freight.otherFreightAmount ?? freight.otherFeeTotal ?? this.form.otherFeeTotal ?? ''; @@ -7500,12 +7526,12 @@ export default { }, getProjectProcessConfigRows(projectId) { if (!projectId) return Promise.resolve([]); - return getProcessConfigList(1, 100, { projectIds: projectId }).then(res => + return getProcessConfigList(1, 100, { projectIds: projectId, status: 1 }).then(res => extractRecords(res).filter(row => { const projectIds = String(row.projectIds || '') .split(',') .map(item => item.trim()); - return projectIds.includes(String(projectId)); + return Number(row.status) === 1 && projectIds.includes(String(projectId)); }) ); }, @@ -7837,7 +7863,7 @@ export default { this.clearShippingPlan(); } this.transportCargoRows = []; - this.taskFreightCurrency = 'CNY'; + this.taskFreightCurrency = 'RMB'; this.hasProjectProcessConfig = false; this.shippingTemplateFreight = defaultShippingTemplateFreight(); this.billingPlanRows = []; @@ -7947,6 +7973,18 @@ export default { getDispatchQuantityUnit(row = {}) { return row.quantityUnit || row.goodsQuantityUnit || row.unit || '吨'; }, + getDispatchVehicleIdentifier(row = {}) { + return [ + row.vehicleNo, + row.vehicleNumber, + row.flightNo, + row.shipNo, + row.trainNo, + ].find(value => String(value ?? '').trim()) || ''; + }, + hasDispatchVehicleIdentifier(row = {}) { + return Boolean(this.getDispatchVehicleIdentifier(row)); + }, getDispatchSummaryItem(unit) { return this.dispatchSummaryItems.find(item => item.unit === unit); }, @@ -8313,7 +8351,7 @@ export default { this.form.estimatedEndTime = this.form.estimatedEndTime || taskInfo.estimatedEndTime || goodsInfo.estimatedEndTime || ''; this.form.taskRemark = this.form.taskRemark || taskInfo.taskRemark || goodsInfo.remark || ''; - this.taskFreightCurrency = freightInfo.currency || this.taskFreightCurrency || 'CNY'; + this.taskFreightCurrency = freightInfo.currency || this.taskFreightCurrency || 'RMB'; this.loadShippingTemplateCurrencyOptions(); this.form.cargoTypePath = this.normalizeBillingCargoTypePath(this.form.cargoTypePath); if (!this.form.cargoTypePath.length) { @@ -8421,6 +8459,41 @@ export default { this.taskDriverLoading = false; }); }, + fetchTaskDriverSuggestions(queryString, callback) { + const keyword = String(queryString || '').trim(); + this.taskDriverLoading = true; + getDriverList(1, 20, keyword ? { driverName: keyword } : {}) + .then(res => { + const records = extractRecords(res); + this.taskDriverOptions = records; + callback( + records.map(item => ({ + ...item, + value: item.driverName || item.name || '', + })) + ); + }) + .catch(error => { + window.console.log(error); + callback([]); + }) + .finally(() => { + this.taskDriverLoading = false; + }); + }, + handleTaskDriverSelect(target, item = {}) { + const value = item.driverName || item.name || ''; + const driverPhone = item.mobile || item.phone || item.driverPhone || ''; + if (target === 'dispatch') { + this.dispatchItemForm.driverId = item.id || ''; + this.dispatchItemForm.driverName = value; + if (driverPhone) this.dispatchItemForm.driverPhone = driverPhone; + return; + } + this.form.driverId = item.id || ''; + this.form.driverName = value; + if (driverPhone) this.form.driverPhone = driverPhone; + }, handleTaskDriverChange(value) { const driver = this.taskDriverOptions.find(item => [item.driverName, item.name].some(name => String(name || '') === String(value || '')) @@ -8469,19 +8542,21 @@ export default { secondCargoTypeCode: secondCargoType?.cargoCode || secondCargoType?.code || '', }; }, - loadTaskCargoOptionsByPath(path = []) { + loadTaskCargoOptionsByPath(path = [], cargoName = '') { const normalizedPath = this.normalizeBillingCargoTypePath(path); if (!normalizedPath.length) return Promise.resolve([]); return this.ensureBillingCargoTypeOptions().then(() => { const key = this.getTaskCargoTypeOptionsKey(normalizedPath); const params = this.buildTaskCargoListQueryParams(normalizedPath); - if (!params.secondCargoTypeName && !params.secondCargoTypeCode) { + const keyword = String(cargoName || '').trim(); + if (!params.secondCargoTypeName && !params.secondCargoTypeCode && !keyword) { this.setTaskCargoOptionsByKey(key, []); return []; } this.setTaskCargoLoadingByKey(key, true); return getCommonCargoList(1, 500, { ...params, + ...(keyword ? { cargoName: keyword } : {}), allDept: 0, }) .then(res => { @@ -8499,6 +8574,51 @@ export default { }); }); }, + fetchTaskCargoSuggestions(queryString, callback, path = []) { + const keyword = String(queryString || '').trim(); + const normalizedPath = this.normalizeBillingCargoTypePath(path); + if (!keyword && !normalizedPath.length) { + callback([]); + return; + } + let loadingKey = ''; + this.ensureBillingCargoTypeOptions() + .then(() => { + const key = this.getTaskCargoTypeOptionsKey(normalizedPath); + loadingKey = key; + const params = this.buildTaskCargoListQueryParams(normalizedPath); + if (key) this.setTaskCargoLoadingByKey(key, true); + return getCommonCargoList(1, 50, { + ...params, + ...(keyword ? { cargoName: keyword } : {}), + allDept: 0, + }).then(res => { + const records = extractRecords(res); + if (key) this.setTaskCargoOptionsByKey(key, records); + callback( + records.map(item => ({ + ...item, + value: this.formatBillingCargoTypeLabel(item), + })) + ); + return key; + }); + }) + .catch(error => { + window.console.log(error); + callback([]); + }) + .finally(() => { + if (loadingKey) this.setTaskCargoLoadingByKey(loadingKey, false); + }); + }, + handleTaskCargoRowSelect(row, item = {}) { + const value = this.formatBillingCargoTypeLabel(item); + row.cargoName = value; + row.specification = item.specification || item.spec || row.specification || ''; + row.model = item.model || item.modelName || row.model || ''; + this.syncTransportCargoJson(); + }, loadTaskCargoOptionsForForm() { return this.loadTaskCargoOptionsByPath(this.form.cargoTypePath); }, @@ -8525,6 +8645,11 @@ export default { this.form.model = cargo.model || cargo.modelName || this.form.model || ''; } }, + handleTaskCargoSelect(item = {}) { + this.form.cargoName = this.formatBillingCargoTypeLabel(item); + this.form.specification = item.specification || item.spec || this.form.specification || ''; + this.form.model = item.model || item.modelName || this.form.model || ''; + }, getTaskCargoRowOptions(row = {}) { return this.getTaskCargoOptionsByPath(row.cargoTypePath); }, @@ -8826,7 +8951,7 @@ export default { }, serializeTaskFullFreight() { return JSON.stringify({ - currency: this.taskFreightCurrency || '', + currency: this.taskFreightCurrency || 'RMB', totalFreightAmount: this.taskFullFreightTotal, otherFreightAmount: this.form.otherFeeTotal || '', freightItems: this.transportCargoRows.map((cargo, index) => ({ @@ -11411,8 +11536,153 @@ export default { return result.length ? result : rows; }, []); const goodsRows = this.dispatchGoodsRows(plan.goodsJson); - const sourceRows = persistedRows.length ? persistedRows : goodsRows.length ? goodsRows : [plan]; - return sourceRows.map((item, index) => this.normalizeDispatchRow(plan, item, index)); + if (!goodsRows.length) { + const normalizedPersistedRows = persistedRows.map((item, index) => + this.normalizeDispatchRow(plan, item, index) + ); + return normalizedPersistedRows.length + ? normalizedPersistedRows + : [this.normalizeDispatchRow(plan, plan, 0)]; + } + + const assignedQuantities = new Map(); + const pendingQuantities = new Map(); + persistedRows.forEach(row => { + if (!this.hasDispatchVehicleIdentifier(row)) return; + this.getDispatchGoodsSourceRows(row).forEach(goods => { + const key = this.getDispatchCargoIdentity(goods); + assignedQuantities.set( + key, + (assignedQuantities.get(key) || 0) + + this.parseDispatchQuantity( + goods.quantity || goods.cargoQuantity || goods.goodsQuantity + ) + ); + }); + }); + + const normalizedPersistedRows = []; + persistedRows.forEach((item, index) => { + if (this.hasDispatchVehicleIdentifier(item)) { + normalizedPersistedRows.push(this.normalizeDispatchRow(plan, item, index)); + return; + } + const pendingGoodsRows = this.getDispatchGoodsSourceRows(item) + .map(goods => { + const totalQuantity = this.parseDispatchQuantity( + goods.quantity || goods.cargoQuantity || goods.goodsQuantity + ); + if (totalQuantity <= 0) return goods; + const remainingQuantity = Math.max( + totalQuantity - + (assignedQuantities.get(this.getDispatchCargoIdentity(goods)) || 0), + 0 + ); + if (!remainingQuantity) return null; + return { + ...goods, + quantity: this.formatDispatchQuantity(remainingQuantity), + }; + }) + .filter(Boolean); + if (!pendingGoodsRows.length) return; + pendingGoodsRows.forEach(goods => { + const key = this.getDispatchCargoIdentity(goods); + pendingQuantities.set( + key, + (pendingQuantities.get(key) || 0) + + this.parseDispatchQuantity( + goods.quantity || goods.cargoQuantity || goods.goodsQuantity + ) + ); + }); + normalizedPersistedRows.push( + this.normalizeDispatchRow( + plan, + { + ...item, + goodsJson: JSON.stringify(pendingGoodsRows), + quantity: pendingGoodsRows[0].quantity || item.quantity || '', + }, + index + ) + ); + }); + + const remainingRows = goodsRows + .map((goods, index) => { + const totalQuantity = this.parseDispatchQuantity( + goods.quantity || goods.cargoQuantity || goods.goodsQuantity + ); + const key = this.getDispatchCargoIdentity(goods); + const remainingQuantity = Math.max( + totalQuantity - + (assignedQuantities.get(key) || 0) - + (pendingQuantities.get(key) || 0), + 0 + ); + if (!remainingQuantity) return null; + const pendingGoods = { + ...goods, + quantity: this.formatDispatchQuantity(remainingQuantity), + }; + const row = this.normalizeDispatchRow( + plan, + { + ...pendingGoods, + goodsJson: JSON.stringify([pendingGoods]), + taskEntryMode: 'simple', + }, + persistedRows.length + index + ); + row.quantity = this.formatDispatchQuantity(remainingQuantity); + row.taskEntryMode = 'simple'; + return row; + }) + .filter(Boolean); + + return [...normalizedPersistedRows, ...remainingRows]; + }, + getDispatchGoodsSourceRows(row = {}) { + const rows = this.dispatchGoodsRows(row.goodsJson); + return rows.length ? rows : [row]; + }, + getDispatchUnscheduledGoodsRows() { + const goodsRows = this.dispatchPlanGoodsRows; + if (!goodsRows.length) return []; + const assignedQuantities = new Map(); + this.dispatchRows.forEach(row => { + if (!this.hasDispatchVehicleIdentifier(row)) return; + this.getDispatchGoodsSourceRows(row).forEach(goods => { + const key = this.getDispatchCargoIdentity(goods); + assignedQuantities.set( + key, + (assignedQuantities.get(key) || 0) + + this.parseDispatchQuantity( + goods.quantity || goods.cargoQuantity || goods.goodsQuantity + ) + ); + }); + }); + return goodsRows + .filter(goods => { + const totalQuantity = this.parseDispatchQuantity( + goods.quantity || goods.cargoQuantity || goods.goodsQuantity + ); + const assignedQuantity = + assignedQuantities.get(this.getDispatchCargoIdentity(goods)) || 0; + return totalQuantity <= 0 || assignedQuantity < totalQuantity; + }) + .map(goods => this.normalizeTransportCargoRow({ ...goods, quantity: '' })); + }, + getDispatchCargoIdentity(row = {}) { + return [ + row.cargoName || row.goodsName || row.name || '', + row.cargoType || row.secondCargoTypeName || row.goodsType || row.type || '', + this.getDispatchQuantityUnit(row), + ] + .map(value => String(value || '').trim()) + .join('|'); }, normalizeDispatchRow(plan = {}, item = {}, index = 0) { const itemGoodsRows = this.dispatchGoodsRows(item.goodsJson); @@ -11430,7 +11700,18 @@ export default { carrierName: item.carrierName || plan.carrierName || '', driverName: item.driverName || plan.driverName || '', driverPhone: item.driverPhone || plan.driverPhone || '', - vehicleNo: item.vehicleNo || plan.vehicleNo || '', + vehicleNo: + item.vehicleNo || + item.vehicleNumber || + item.flightNo || + item.shipNo || + item.trainNo || + plan.vehicleNo || + plan.vehicleNumber || + plan.flightNo || + plan.shipNo || + plan.trainNo || + '', trailerVehicleNo: item.trailerVehicleNo || '', escortName: item.escortName || '', escortPhone: item.escortPhone || '', @@ -11488,6 +11769,7 @@ export default { '', dispatchTime: item.dispatchTime || plan.dispatchTime || waybillFallback.createTime || item.createTime || '', + goodsJson: item.goodsJson || '', attachmentsJson: item.attachmentsJson || '', remark: item.remark || item.taskRemark || plan.remark || '', }; @@ -11621,6 +11903,117 @@ export default { const item = this.parseJsonObject(value); return Object.keys(item).length ? [item] : []; }, + getDispatchConfiguredGoodsOptions() { + const sourceRows = this.dispatchPlanGoodsRows.length + ? this.dispatchPlanGoodsRows + : this.dispatchRow.cargoName || this.dispatchRow.cargoType + ? [this.dispatchRow] + : []; + return sourceRows + .map((item, index) => { + const cargoTypePath = this.normalizeBillingCargoTypePath(item.cargoTypePath); + const cargoTypeNode = + (cargoTypePath.length && this.findBillingCargoTypeByPath(cargoTypePath)) || + this.findBillingCargoTypeByCodeOrName({ + cargoTypeCode: + item.secondCargoTypeCode || item.cargoTypeCode || item.goodsTypeCode || '', + cargoType: + item.secondCargoTypeName || item.cargoType || item.goodsType || item.goodsTypeName, + }); + const path = cargoTypePath.length ? cargoTypePath : cargoTypeNode?.path || []; + const labels = path.length ? this.getBillingCargoTypePathLabels(path) : []; + const cargoTypeLabel = + item.cargoType || + item.secondCargoTypeName || + item.goodsType || + item.goodsTypeName || + labels.at(-1) || + ''; + const cargoTypeCode = + item.secondCargoTypeCode || + item.cargoTypeCode || + item.goodsTypeCode || + cargoTypeNode?.cargoCode || + cargoTypeNode?.code || + ''; + const cargoName = item.cargoName || item.goodsName || item.name || ''; + return { + ...item, + _dispatchGoodsIndex: index, + cargoTypePath: path, + cargoTypeLabel, + cargoTypeCode, + cargoTypeValue: cargoTypeCode || (path.length ? path.join('/') : cargoTypeLabel), + cargoName, + }; + }) + .filter(item => item.cargoTypeLabel || item.cargoName); + }, + getDispatchConfiguredCargoTypeOptions() { + const options = new Map(); + this.getDispatchConfiguredGoodsOptions().forEach(item => { + if (!item.cargoTypeLabel || options.has(item.cargoTypeValue)) return; + options.set(item.cargoTypeValue, { + label: item.cargoTypeLabel, + value: item.cargoTypeLabel, + cargoTypeCode: item.cargoTypeCode, + cargoTypePath: item.cargoTypePath, + }); + }); + return Array.from(options.values()); + }, + getDispatchConfiguredCargoNameOptions(row = {}) { + const goodsOptions = this.getDispatchConfiguredGoodsOptions(); + const rowType = String(row.cargoType || '').trim(); + const rowTypePath = this.normalizeBillingCargoTypePath(row.cargoTypePath); + const rowTypeValue = rowType || row.cargoTypeCode || rowTypePath.join('/'); + const filtered = rowTypeValue + ? goodsOptions.filter( + item => + item.cargoTypeLabel === rowType || + item.cargoTypeValue === rowTypeValue || + item.cargoTypeCode === row.cargoTypeCode || + item.cargoTypePath.join('/') === rowTypePath.join('/') + ) + : goodsOptions; + const options = new Map(); + filtered.forEach(item => { + if (!item.cargoName || options.has(item.cargoName)) return; + options.set(item.cargoName, { + label: item.cargoName, + value: item.cargoName, + cargoTypeLabel: item.cargoTypeLabel, + cargoTypeCode: item.cargoTypeCode, + cargoTypePath: item.cargoTypePath, + specification: item.specification || item.spec || '', + model: item.model || item.modelName || '', + }); + }); + return Array.from(options.values()); + }, + handleDispatchConfiguredCargoTypeChange(row, value) { + const option = this.getDispatchConfiguredCargoTypeOptions().find( + item => String(item.value) === String(value || '') + ); + row.cargoType = option?.label || ''; + row.cargoTypeCode = option?.cargoTypeCode || ''; + row.cargoTypePath = option?.cargoTypePath || []; + row.cargoName = ''; + row.specification = ''; + row.model = ''; + }, + handleDispatchConfiguredCargoNameChange(row, value) { + const option = this.getDispatchConfiguredCargoNameOptions(row).find( + item => String(item.value) === String(value || '') + ); + 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 || []; + row.specification = option.specification || row.specification || ''; + row.model = option.model || row.model || ''; + }, openDispatchItemDialog(index = -1, row = defaultDispatchRow()) { const defaultSummaryItem = this.dispatchSummaryItems.find(item => item.remaining > 0) || this.dispatchSummaryItems[0]; @@ -11668,6 +12061,22 @@ export default { this.dispatchItemCargoRows = (goodsRows.length ? goodsRows : [this.dispatchItemForm]).map( item => this.normalizeTransportCargoRow(item) ); + const hasMultiplePlanGoods = this.dispatchPlanGoodsRows.length > 1; + const shouldAutoFillFullGoods = + hasMultiplePlanGoods && + (index < 0 || !this.hasDispatchVehicleIdentifier(this.dispatchItemForm)); + if (shouldAutoFillFullGoods) { + const unscheduledGoodsRows = this.getDispatchUnscheduledGoodsRows(); + this.dispatchItemForm.taskEntryMode = 'full'; + this.dispatchItemForm.quantity = ''; + this.dispatchItemCargoRows = ( + unscheduledGoodsRows.length + ? unscheduledGoodsRows + : this.dispatchPlanGoodsRows.map(goods => + this.normalizeTransportCargoRow({ ...goods, quantity: '' }) + ) + ); + } if (!this.dispatchItemCargoRows.length) { this.dispatchItemCargoRows = [this.normalizeTransportCargoRow()]; } @@ -11706,7 +12115,7 @@ export default { loadDispatchCarrierOptions() { if (this.taskCarrierLoading) return Promise.resolve([]); this.taskCarrierLoading = true; - return getCarrierCustomerList(1, 9999, { customerType: '承运商' }) + return getCarrierCustomerList(1, 9999, { customerType: '承运商', status: 1 }) .then(res => { this.taskCarrierOptions = extractRecords(res); return this.taskCarrierOptions; @@ -11776,6 +12185,18 @@ export default { row.model = cargo.model || cargo.modelName || row.model || ''; } }, + handleDispatchCargoRowSelect(row, item = {}) { + row.cargoName = this.formatBillingCargoTypeLabel(item); + row.specification = item.specification || item.spec || row.specification || ''; + row.model = item.model || item.modelName || row.model || ''; + }, + handleDispatchCargoSelect(item = {}) { + this.dispatchItemForm.cargoName = this.formatBillingCargoTypeLabel(item); + this.dispatchItemForm.specification = + item.specification || item.spec || this.dispatchItemForm.specification || ''; + this.dispatchItemForm.model = + item.model || item.modelName || this.dispatchItemForm.model || ''; + }, handleDispatchCargoQuantityInput(row, value) { const text = String(value || '').replace(/[^\d.]/g, ''); const parts = text.split('.'); @@ -11809,6 +12230,89 @@ export default { const amount = quantity * unitPrice; return Number.isInteger(amount) ? String(amount) : String(Number(amount.toFixed(2))); }, + dispatchItemRemainingQuantity(row = {}) { + const unit = this.getDispatchQuantityUnit(row); + let remaining = this.getDispatchRemainingQuantity(unit); + const editingRow = this.dispatchRows[this.dispatchItemIndex]; + if (!editingRow || !this.hasDispatchVehicleIdentifier(editingRow)) return remaining; + const editingGoodsRows = this.dispatchGoodsRows(editingRow.goodsJson); + const goodsRows = editingGoodsRows.length ? editingGoodsRows : [editingRow]; + return remaining + goodsRows.reduce((total, goods) => { + if (this.getDispatchQuantityUnit(goods) !== unit) return total; + return total + this.parseDispatchQuantity( + goods.quantity || goods.cargoQuantity || goods.goodsQuantity + ); + }, 0); + }, + pruneDispatchPendingRows(rows = []) { + const planQuantities = new Map(); + this.dispatchPlanGoodsRows.forEach(goods => { + const key = this.getDispatchCargoIdentity(goods); + const quantity = this.parseDispatchQuantity( + goods.quantity || goods.cargoQuantity || goods.goodsQuantity + ); + planQuantities.set(key, (planQuantities.get(key) || 0) + quantity); + }); + const assignedQuantities = new Map(); + rows.forEach(row => { + if (!this.hasDispatchVehicleIdentifier(row)) return; + this.getDispatchGoodsSourceRows(row).forEach(goods => { + const key = this.getDispatchCargoIdentity(goods); + assignedQuantities.set( + key, + (assignedQuantities.get(key) || 0) + + this.parseDispatchQuantity( + goods.quantity || goods.cargoQuantity || goods.goodsQuantity + ) + ); + }); + }); + const pendingRemaining = new Map( + Array.from(planQuantities.entries()).map(([key, total]) => [ + key, + Math.max(total - (assignedQuantities.get(key) || 0), 0), + ]) + ); + return rows.reduce((result, row) => { + if (this.hasDispatchVehicleIdentifier(row)) { + result.push(row); + return result; + } + const pendingGoodsRows = this.getDispatchGoodsSourceRows(row) + .map(goods => { + const quantity = this.parseDispatchQuantity( + goods.quantity || goods.cargoQuantity || goods.goodsQuantity + ); + if (quantity <= 0) return goods; + const key = this.getDispatchCargoIdentity(goods); + const available = pendingRemaining.has(key) + ? pendingRemaining.get(key) + : quantity; + const remainingQuantity = Math.min(quantity, Math.max(available, 0)); + if (!remainingQuantity) return null; + if (pendingRemaining.has(key)) { + pendingRemaining.set(key, Math.max(available - remainingQuantity, 0)); + } + return { + ...goods, + quantity: this.formatDispatchQuantity(remainingQuantity), + }; + }) + .filter(Boolean); + if (!pendingGoodsRows.length) return result; + const nextRow = { + ...row, + goodsJson: JSON.stringify(pendingGoodsRows), + cargoType: pendingGoodsRows[0].cargoType || row.cargoType || '', + cargoName: pendingGoodsRows[0].cargoName || row.cargoName || '', + quantity: pendingGoodsRows[0].quantity || row.quantity || '', + quantityUnit: pendingGoodsRows[0].quantityUnit || row.quantityUnit || '吨', + }; + nextRow.cargoInfo = this.formatDispatchCargoInfo({}, nextRow); + result.push(nextRow); + return result; + }, []); + }, saveDispatchItem() { const goodsRows = this.dispatchItemForm.taskEntryMode === 'full' @@ -11856,7 +12360,8 @@ export default { const otherDispatchedQuantity = this.dispatchRows.reduce((total, row, index) => { if ( index === this.dispatchItemIndex || - this.getDispatchQuantityUnit(row) !== quantityUnit + this.getDispatchQuantityUnit(row) !== quantityUnit || + !this.hasDispatchVehicleIdentifier(row) ) { return total; } @@ -11864,16 +12369,22 @@ export default { }, 0); const currentQuantity = this.parseDispatchQuantity(nextRow.quantity); const totalQuantity = this.getDispatchSummaryItem(quantityUnit)?.total || 0; - const persistedDispatchedQuantity = this.parseJsonArray(this.dispatchRow.dispatchRows).reduce( - (total, row) => { - if (this.getDispatchQuantityUnit(row) !== quantityUnit) return total; - return total + this.parseDispatchQuantity(row.quantity); - }, - 0 - ); + const persistedDispatchedQuantity = this.dispatchRows.length + ? 0 + : this.parseJsonArray(this.dispatchRow.dispatchRows).reduce((total, row) => { + if ( + this.getDispatchQuantityUnit(row) !== quantityUnit || + !this.hasDispatchVehicleIdentifier(row) + ) { + return total; + } + return total + this.parseDispatchQuantity(row.quantity); + }, 0); + const dispatchedQuantity = + persistedDispatchedQuantity + otherDispatchedQuantity + currentQuantity; if ( totalQuantity > 0 && - persistedDispatchedQuantity + otherDispatchedQuantity + currentQuantity > totalQuantity + dispatchedQuantity - totalQuantity > 1e-8 ) { this.$message.warning( `已调度${quantityUnit}合计不能超过总数量${this.formatDispatchQuantity( @@ -11887,6 +12398,7 @@ export default { } else { this.dispatchRows.push(nextRow); } + this.dispatchRows = this.pruneDispatchPendingRows(this.dispatchRows); this.dispatchItemBox = false; this.dispatchItemIndex = -1; this.dispatchItemForm = defaultDispatchRow(); @@ -11911,8 +12423,8 @@ export default { }); }, handleDispatchAddRow() { - if (!this.dispatchHasRemainingQuantity) { - this.$message.warning('剩余数量为0,不能新增调度明细'); + if (!this.dispatchHasAddableQuantity) { + this.$message.warning('待调度列表货物总量已达到计划总量,不能新增调度明细'); return; } this.openDispatchItemDialog(); @@ -11981,8 +12493,6 @@ export default { if (mode === 'draft') return true; const invalidRow = this.dispatchRows.find(row => { const selfTransport = row.carrierType !== '承运商'; - const carrierPlatform = row.carrierType === '网货平台'; - const optionalSelfEscort = row.taskEntryMode === 'full' && row.carrierType === '自运'; return ( !row.transportType || !row.cargoName || @@ -11995,12 +12505,7 @@ export default { !row.arrivalAddress || (row.carrierType === '承运商' && !row.carrierName) || (selfTransport && - (!row.driverName || - !row.driverPhone || - !row.mileage || - (!optionalSelfEscort && - (carrierPlatform || row.taskEntryMode !== 'full') && - (!row.trailerVehicleNo || !row.escortName || !row.escortPhone)))) + (!row.driverName || !row.driverPhone || !row.mileage)) ); }); if (invalidRow) { @@ -12981,15 +13486,19 @@ export default { :deep(.el-dialog__body) { padding: 16px; background: #f5f6fa; + display: flex; + height: calc(100vh - 140px); max-height: calc(100vh - 140px); - overflow-y: auto; + box-sizing: border-box; + overflow: hidden; } } &__dispatch-shell { display: flex; + flex: 1; flex-direction: column; - min-height: calc(100vh - 240px); + min-height: 0; } &__dispatch-top { @@ -13205,6 +13714,8 @@ export default { } &__dispatch-table { + flex: 1; + min-height: 0; font-size: 13px; :deep(.el-table__header .cell) { @@ -13218,6 +13729,20 @@ export default { } } + &__dispatch-list-card { + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + + :deep(.section-card__body) { + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + } + } + &__dispatch-actions { display: inline-flex; align-items: center; @@ -13286,6 +13811,15 @@ export default { } } + &__dispatch-quantity-hint { + display: block; + width: 100%; + margin-top: 4px; + color: #909399; + font-size: 12px; + line-height: 18px; + } + &__dispatch-item-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -13452,6 +13986,23 @@ export default { width: 100%; } + &__attachment-description { + margin-bottom: 12px; + color: #606266; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; + } + + &__attachment-description-item + &__attachment-description-item { + margin-top: 4px; + } + + &__attachment-description-label { + color: #303133; + font-weight: 600; + } + &__attachment-head { display: flex; align-items: center; diff --git a/src/views/business/components/master-order-dispatch.vue b/src/views/business/components/master-order-dispatch.vue index c2d974b..a8b91e0 100644 --- a/src/views/business/components/master-order-dispatch.vue +++ b/src/views/business/components/master-order-dispatch.vue @@ -127,7 +127,7 @@ visible && loadCarrierOptions()"> - visible && loadDriverOptions()" @change="value => handleDriverChange(route, value)"> + handleDriverSuggestionSelect(route, item)" /> @@ -527,7 +527,11 @@ export default { this.carrierLoading = true; try { this.carrierOptions = unwrapRecords( - await getCustomerList(1, 20, { customerName: keyword, customerType: '承运商' }) + await getCustomerList(1, 20, { + customerName: keyword, + customerType: '承运商', + status: 1, + }) ); } finally { this.carrierLoading = false; @@ -541,6 +545,23 @@ export default { this.driverLoading = false; } }, + fetchDriverSuggestions(queryString, callback) { + this.loadDriverOptions(String(queryString || '').trim()) + .then(() => + callback( + this.driverOptions.map(item => ({ + ...item, + value: this.driverOptionLabel(item), + })) + ) + ) + .catch(() => callback([])); + }, + handleDriverSuggestionSelect(route, item = {}) { + route.driverName = this.driverOptionLabel(item); + route.driverId = item.id || item.driverId || ''; + route.driverPhone = item.mobile || item.driverPhone || item.phone || route.driverPhone || ''; + }, setDriverInput(segmentNo, element) { if (element) this.driverInputs[segmentNo] = element; else delete this.driverInputs[segmentNo]; diff --git a/src/views/business/components/master-order-editor.vue b/src/views/business/components/master-order-editor.vue index 0f55145..55e04c3 100644 --- a/src/views/business/components/master-order-editor.vue +++ b/src/views/business/components/master-order-editor.vue @@ -137,21 +137,22 @@ label="货物名称" min-width="130" > visible && loadCargoOptionsByType(row.cargoTypePath)" - @change="selectCargoName(row, $event)" - > selectCargoSuggestion(row, cargo)" + > + + {{ item.cargoName || item.name || '' }} + {{ item.cargoCode }} + + ({ + ...item, + value: item.cargoName || item.name || '', + })) + ); + } catch (error) { + window.console.log(error); + callback([]); + } finally { + if (key) this.cargoOptionsLoadingMap = { ...this.cargoOptionsLoadingMap, [key]: false }; + } + }, + selectCargoSuggestion(goods, cargo = {}) { + goods.cargoName = cargo.cargoName || cargo.name || ''; + Object.assign(goods, this.normalizeCargoRow(cargo, goods.cargoTypePath)); + }, selectCargoName(goods, cargoName) { if (!cargoName) return; const cargo = this.getCargoOptionsByType(goods.cargoTypePath).find( diff --git a/src/views/business/loading-manage.vue b/src/views/business/loading-manage.vue index c2efde2..33e61d4 100644 --- a/src/views/business/loading-manage.vue +++ b/src/views/business/loading-manage.vue @@ -825,27 +825,15 @@ - visible && loadDriverOptions(dialogForm.driverName)" - > - - + @select="item => handleDriverSuggestionSelect('dialog', item)" + /> @@ -2079,6 +2067,26 @@ export default { this.dialogForm.driverPhone = driver.mobile || driver.driverPhone || driver.phone || ''; } }, + fetchDriverSuggestions(queryString, callback) { + this.loadDriverOptions(String(queryString || '').trim()) + .then(() => + callback( + this.driverOptions.map(item => ({ + ...item, + value: item.driverName || item.name || '', + })) + ) + ) + .catch(() => callback([])); + }, + handleDriverSuggestionSelect(target, item = {}) { + const value = item.driverName || item.name || ''; + if (target === 'dialog') { + this.dialogForm.driverName = value; + const phone = item.mobile || item.driverPhone || item.phone || ''; + if (phone) this.dialogForm.driverPhone = phone; + } + }, handleMileageInput(value) { this.dialogForm.mileage = String(value || '').replace(/[^\d.]/g, ''); }, @@ -2137,7 +2145,11 @@ export default { this.carrierLoading = true; try { this.carrierOptions = unwrapRecords( - await getCustomerList(1, 20, { customerName: keyword }) + await getCustomerList(1, 20, { + customerName: keyword, + customerType: '承运商', + status: 1, + }) ); } finally { this.carrierLoading = false; diff --git a/src/views/business/process-config.vue b/src/views/business/process-config.vue index c5825d2..06f6a1a 100644 --- a/src/views/business/process-config.vue +++ b/src/views/business/process-config.vue @@ -730,6 +730,33 @@ export default { this.form.projectIds = project ? String(project.id) : ''; this.form.projectNames = project ? project.projectName : ''; }, + checkEnabledProjectConfig(projectId, excludeId = '') { + if (!projectId) return Promise.resolve(false); + return api + .getList(1, 100, { + projectIds: String(projectId), + status: 1, + }) + .then(res => { + const data = res?.data?.data || {}; + const records = Array.isArray(data) ? data : data.records || []; + return records.some(item => { + const projectIds = String(item.projectIds || '') + .split(/[,,]/) + .map(value => value.trim()) + .filter(Boolean); + return ( + String(item.id || '') !== String(excludeId || '') && + Number(item.status) === 1 && + projectIds.includes(String(projectId)) + ); + }); + }); + }, + checkEnabledProjectBeforeSubmit(row) { + if (row.id || !row.projectIds) return Promise.resolve(false); + return this.checkEnabledProjectConfig(row.projectIds); + }, handleFinishDaysInput(value) { this.form.defaultFinishDays = String(value || '') .replace(/\D/g, '') @@ -895,7 +922,14 @@ export default { this.stopSubmitLoading(loading); return; } - this.confirmDuplicateVoucher() + this.checkEnabledProjectBeforeSubmit(submitRow) + .then(exists => { + if (exists) { + this.$message.warning('该项目已有启用状态的过程配置,不能重复添加'); + throw { code: 'enabled-project-config' }; + } + }) + .then(() => this.confirmDuplicateVoucher()) .then(() => api.submit(submitRow)) .then(() => { this.onLoad(this.page); @@ -903,7 +937,11 @@ export default { done(); }) .catch(error => { - if (error !== 'cancel' && error !== 'close') { + if ( + error !== 'cancel' && + error !== 'close' && + error?.code !== 'enabled-project-config' + ) { window.console.log(error); } this.stopSubmitLoading(loading); @@ -1089,8 +1127,28 @@ export default { cancelButtonText: '取消', type: 'warning', }) - .then(() => api[action](row.id)) .then(() => { + if (action !== 'enable') return true; + return this.checkEnabledProjectConfig(row.projectIds, row.id) + .then(exists => { + if (exists) { + this.$message.warning('该项目已有其他启用状态的过程配置,不能重复启用'); + return false; + } + return true; + }) + .catch(error => { + window.console.log(error); + this.$message.error('检查项目过程配置失败,暂不能启用'); + return false; + }); + }) + .then(canProceed => { + if (!canProceed) return null; + return api[action](row.id); + }) + .then(result => { + if (!result) return; this.$message.success(`${actionName}成功`); this.onLoad(this.page, this.query); }); diff --git a/src/views/business/voucher-manage.vue b/src/views/business/voucher-manage.vue index 2cfd8c4..436bf2b 100644 --- a/src/views/business/voucher-manage.vue +++ b/src/views/business/voucher-manage.vue @@ -819,7 +819,12 @@ const loadProgress = async () => { businessType: 'voucher', }); const data = res.data?.data || {}; - progressRows.value = data.records || []; + progressRows.value = (data.records || []).slice().sort((left, right) => { + const leftTime = left.createTime ? new Date(left.createTime).getTime() : 0; + const rightTime = right.createTime ? new Date(right.createTime).getTime() : 0; + if (rightTime !== leftTime) return rightTime - leftTime; + return String(right.id || '').localeCompare(String(left.id || '')); + }); progressPage.total = data.total || 0; }; const searchProgress = () => { diff --git a/src/views/vehicle/customer-archive.vue b/src/views/vehicle/customer-archive.vue index 7dd5335..fec1acd 100644 --- a/src/views/vehicle/customer-archive.vue +++ b/src/views/vehicle/customer-archive.vue @@ -664,22 +664,19 @@ :index="changeRecordIndex" /> - - - + label="变更内容" + min-width="560" + > + + + + {{ formatChangeContent(row) }} + + {{ formatChangeContent(row) }} + + + `${field}:${fieldValue ?? ''}`) - .join(';'); + const parsed = JSON.parse(text); + return parsed && typeof parsed === 'object' + ? this.replaceNegativeOneWithBlank(parsed) + : { 变更内容: parsed }; } catch (error) { - return value; + return text.split(/[;;]/).reduce((data, item) => { + const match = item.trim().match(/^([^::]+)\s*[::]\s*(.*)$/); + if (match) data[match[1].trim()] = match[2].trim(); + return data; + }, {}); } }, + getChangeFieldLabel(field) { + const fieldLabelMap = { + accessType: '准入标准', + approvalStatus: '审核状态', + currentNode: '当前节点', + currentProcessor: '当前处理人', + customerCode: '客商编号', + shortName: '客商简称', + fullName: '客商名称', + customerType: '客户类型', + customerNature: '客户性质', + unifiedCreditCode: '统一信用代码', + deptName: '所属组织', + approvedTime: '审核通过时间', + status: '状态', + businessTermType: '营业期限类型', + registeredRegionName: '注册地址', + registeredDetailAddress: '详细地址', + invoiceType: '发票类型', + }; + if (fieldLabelMap[field]) return fieldLabelMap[field]; + const column = (this.option.column || []).find(item => item.prop === field); + return column?.label || field; + }, + formatChangeFieldValue(field, value) { + if (value === undefined || value === null || value === '' || value === '-1') return '空'; + const normalizedField = { + 准入类型: 'accessType', + 准入标准: 'accessType', + 审批状态: 'approvalStatus', + 审核状态: 'approvalStatus', + 状态: 'status', + 客户类型: 'customerType', + }[field] || field; + const valueMap = { + accessType: { temporary: '临时', formal: '正式' }, + approvalStatus: { + draft: '草稿', + reviewing: '审核中', + approved: '审核通过', + rejected: '审核不通过', + }, + status: { 1: '启用', 2: '停用', 0: '停用' }, + }; + if (Array.isArray(value)) { + return value.map(item => this.formatChangeFieldValue(normalizedField, item)).join('、'); + } + if (valueMap[normalizedField]?.[value] !== undefined) { + return valueMap[normalizedField][value]; + } + if (normalizedField === 'customerType') { + return String(value) + .split(/[,,]/) + .map(item => ({ customer: '客户', carrier: '承运商' }[item.trim()] || item.trim())) + .filter(Boolean) + .join('、'); + } + if (typeof value === 'object') return JSON.stringify(value); + return String(value); + }, + formatChangeContent(row) { + const beforeData = this.parseChangeData(row.beforeData); + const afterData = this.parseChangeData(row.afterData); + const fields = [...new Set([...Object.keys(beforeData), ...Object.keys(afterData)])]; + const details = fields + .filter(field => field !== '变更内容') + .map(field => { + const before = this.formatChangeFieldValue(field, beforeData[field]); + const after = this.formatChangeFieldValue(field, afterData[field]); + return `${this.getChangeFieldLabel(field)}:变更前:${before} → 变更后:${after}`; + }); + const content = String(row.changeContent || '').trim(); + return [content, ...details].filter(Boolean).join(';'); + }, emptyContact() { return { contactName: '', @@ -4108,6 +4186,21 @@ export default { width: 100%; } +.change-record-content-cell { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.change-record-content-tooltip { + max-width: 900px; + max-height: 320px; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; +} + :deep(.archive-dialog .el-dialog__body) { max-height: 72vh; overflow: auto;