diff --git a/src/option/business/shipping-template.js b/src/option/business/shipping-template.js index efa9933..43823f2 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/option/business/transport-plan.js b/src/option/business/transport-plan.js index 5d5fd8f..96c1365 100644 --- a/src/option/business/transport-plan.js +++ b/src/option/business/transport-plan.js @@ -51,9 +51,9 @@ const formatGoodsQuantity = value => { }; const getSecondCargoTypeName = item => - item?.cargoType || item?.secondCargoTypeName || item?.secondCargoType || + item?.cargoType || item?.goodsType || item?.type || item?.cargoTypeName || @@ -97,10 +97,10 @@ const formatGoodsInfo = row => { const text = Object.entries(groups) .map(([unit, group]) => { - const quantity = `${formatGoodsQuantity(group.quantity)}${unit}`; - return `${group.typeName}等${group.count}种货物 | ${quantity}`; + const quantity = formatGoodsQuantity(group.quantity); + return `${group.typeName}等${group.count}种货物 | ${quantity}${unit ? ` ${unit}` : ''}`; }) - .join('; '); + .join('; '); return text || row.goodsInfo || ''; }; diff --git a/src/option/business/waybill-manage.js b/src/option/business/waybill-manage.js index 08651f1..0a38799 100644 --- a/src/option/business/waybill-manage.js +++ b/src/option/business/waybill-manage.js @@ -261,6 +261,7 @@ export const config = { importUrl: '/blade-transport/waybill-manage/import-waybill-manage', defaultForm: { carrierType: '承运商', + transportType: 'road', quantityUnit: '吨', priceUnit: '元/吨', }, diff --git a/src/views/business/components/business-crud-page.vue b/src/views/business/components/business-crud-page.vue index d910227..4bb64b2 100644 --- a/src/views/business/components/business-crud-page.vue +++ b/src/views/business/components/business-crud-page.vue @@ -108,6 +108,24 @@ {{ formatTransportPlanGoodsInfo(row) || '-' }} + + + {{ row.waybillNo }} + + - + + + + + {{ row.loadingNo }} + + - + + 货物名称 - visible && loadTaskCargoOptionsByPath(row.cargoTypePath) - " - @change="value => handleTaskCargoRowNameChange(row, value)" + @select="item => handleTaskCargoRowSelect(row, item)" > - - + + + {{ formatBillingCargoTypeLabel(item) }} + {{ item.cargoCode }} + + + @@ -655,26 +672,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 }} + + + @@ -2475,7 +2463,7 @@ {{ formatTransportPlanProvinceCityDistrict(row.departureAddress) }} - + + + + + {{ attachmentName(row) }}: + + {{ row.description || '-' }} + + @@ -2823,7 +2826,12 @@ - + {{ formatFileSize(row.size) }} @@ -3477,7 +3485,7 @@ - + 待调度列表 @@ -3487,7 +3495,7 @@ 新增 @@ -3496,7 +3504,7 @@ @@ -3706,17 +3714,20 @@ 货物类型 - visible && ensureBillingCargoTypeOptions()" - @change="value => handleDispatchCargoTypeChange(row, value)" - /> + @change="value => handleDispatchConfiguredCargoTypeChange(row, value)" + > + + @@ -3724,20 +3735,16 @@ visible && loadTaskCargoOptionsByPath(row.cargoTypePath)" - @change="value => handleDispatchCargoRowNameChange(row, value)" + filterable + @change="value => handleDispatchConfiguredCargoNameChange(row, value)" > @@ -3749,8 +3756,13 @@ + + + {{ formatDispatchQuantity(dispatchItemRemainingQuantity(row)) }} + + - 数量 + 本次数量 handleDispatchCargoQuantityInput(row, value)" /> @@ -3787,12 +3799,12 @@ visible && loadTaskFeeUnitOptions()"> - - + {{ cargo.quantityUnit || '吨' }} + {{ dispatchItemFreightCurrencyLabel }} - {{ dispatchItemFreightCurrencyLabel }} - visible && loadShippingTemplateCurrencyOptions()"> handleDispatchNumberInput('otherFeeTotal', value)">{{ dispatchItemFreightCurrencyLabel }} + {{ dispatchItemFreightCurrencyLabel }} + visible && loadShippingTemplateCurrencyOptions()"> @@ -3833,25 +3845,15 @@ - visible && loadDispatchDriverOptions()" - @change="handleDispatchDriverChange" - > - - + @select="item => handleTaskDriverSelect('dispatch', item)" + /> - + - + + + - + + + @@ -3891,7 +3917,7 @@ - + + + 剩余数量:{{ + formatDispatchQuantity(dispatchItemRemainingQuantity(dispatchItemForm)) + }} + {{ dispatchItemForm.quantityUnit || '吨' }} + - visible && loadDispatchDriverOptions()" - @change="handleDispatchDriverChange" - > - - + @select="item => handleTaskDriverSelect('dispatch', item)" + /> @@ -5103,6 +5122,7 @@ import { getList as getContractList, } from '@/api/business/contract-manage'; import { getList as getProjectList } from '@/api/business/project-apply'; +import { getList as getLoadingList } from '@/api/business/loading-manage'; import { getList as getCommonRouteList } from '@/api/business/common-route'; import { getDetail as getTransportPlanDetail, @@ -5469,7 +5489,7 @@ export default { zoom: true, }, transportCargoRows: [], - taskFreightCurrency: 'CNY', + taskFreightCurrency: 'RMB', shippingTemplateFreight: defaultShippingTemplateFreight(), shippingTemplateCurrencyOptions: [], shippingTemplateCurrencyLoading: false, @@ -5703,7 +5723,7 @@ export default { return this.isTransportPlanPage && this.detailBox; }, transportPlanGoodsText() { - const text = this.formatDetailValue(this.detailRow, 'goodsInfo'); + const text = this.formatTransportPlanGoodsInfo(this.detailRow); return text && text !== '-' ? text : '暂无货物信息'; }, transportPlanDateRange() { @@ -5847,7 +5867,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 +5902,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 +5919,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 +6073,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; @@ -6278,17 +6323,39 @@ export default { return applyTableMenuWidth(nextOption, menuButtonCount); }, formatTransportPlanGoodsInfo(row = {}) { - if (row.goodsInfo || row.cargoInfo) return String(row.goodsInfo || row.cargoInfo); const goodsRows = this.parseJsonArray(row.goodsJson); - const goods = - goodsRows.find(item => (item.quantityUnit || item.cargoUnit || item.unit) === '吨') || - goodsRows[0] || - {}; - const name = goods.cargoName || goods.goodsName || goods.name || ''; - const type = goods.cargoType || goods.goodsType || goods.typeName || ''; - const quantity = goods.quantity ?? goods.cargoQuantity ?? goods.goodsQuantity ?? ''; - const unit = goods.quantityUnit || goods.cargoUnit || goods.unit || ''; - return [name, type, quantity === '' ? '' : `${quantity}${unit}`].filter(Boolean).join('/'); + if (!goodsRows.length) return String(row.goodsInfo || row.cargoInfo || ''); + const groups = goodsRows.reduce((result, item = {}) => { + const unit = item.quantityUnit || item.goodsQuantityUnit || item.cargoUnit || item.unit || ''; + const key = unit || '__empty__'; + if (!result[key]) { + result[key] = { + typeName: + item.secondCargoTypeName || + item.secondCargoType || + item.cargoType || + item.goodsType || + item.typeName || + '货物', + count: 0, + quantity: 0, + unit, + }; + } + result[key].count += 1; + result[key].quantity += this.parseDispatchQuantity( + item.quantity ?? item.cargoQuantity ?? item.goodsQuantity + ); + return result; + }, {}); + return Object.values(groups) + .map(group => { + const quantity = this.formatDispatchQuantity(group.quantity); + return `${group.typeName}等${group.count}种货物 | ${quantity}${ + group.unit ? ` ${group.unit}` : '' + }`; + }) + .join('; '); }, formatTransportPlanDistrictAddress(value) { const text = String(value || '').trim(); @@ -6530,7 +6597,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 ?? ''; @@ -6810,6 +6877,24 @@ export default { this.detailLoading = false; }); }, + async openLoadingDetail(row = {}) { + let id = row.loadingId || row.loadingManageId || ''; + if (!id && row.loadingNo) { + try { + const res = await getLoadingList(1, 1, { loadingNo: row.loadingNo }); + const data = res?.data?.data || res?.data || res || {}; + const records = data.records || data.rows || (Array.isArray(data) ? data : []); + id = records[0]?.id || ''; + } catch (error) { + id = ''; + } + } + if (!id) { + this.$message.info('当前配载单暂无详情数据'); + return; + } + this.$router.push({ path: '/business/loading-manage', query: { detailId: id } }); + }, loadWaybillDetailProcessNodes(projectId) { this.waybillDetailProcessNodes = []; if (!projectId) return Promise.resolve([]); @@ -7500,12 +7585,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 +7922,7 @@ export default { this.clearShippingPlan(); } this.transportCargoRows = []; - this.taskFreightCurrency = 'CNY'; + this.taskFreightCurrency = 'RMB'; this.hasProjectProcessConfig = false; this.shippingTemplateFreight = defaultShippingTemplateFreight(); this.billingPlanRows = []; @@ -7947,6 +8032,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 +8410,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 +8518,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 +8601,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 +8633,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 +8704,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 +9010,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 +11595,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 +11759,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 +11828,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 || '', }; @@ -11582,9 +11923,9 @@ export default { const sourceRows = goodsRows.length ? goodsRows : itemGoodsRows.length ? itemGoodsRows : [item]; const groups = sourceRows.reduce((result, goods = {}) => { const cargoType = - goods.cargoType || goods.secondCargoTypeName || goods.secondCargoType || + goods.cargoType || goods.goodsType || goods.type || plan.cargoType || @@ -11610,9 +11951,9 @@ export default { group => `${group.cargoType}等${group.count}种货物 | ${this.formatDispatchQuantity( group.quantity - )}${group.unit || ''}` + )}${group.unit ? ` ${group.unit}` : ''}` ) - .join(';'); + .join('; '); return cargoInfo || plan.goodsInfo || ''; }, dispatchGoodsRows(value) { @@ -11621,6 +11962,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 +12120,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 +12174,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 +12244,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 +12289,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 +12419,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 +12428,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 +12457,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 +12482,8 @@ export default { }); }, handleDispatchAddRow() { - if (!this.dispatchHasRemainingQuantity) { - this.$message.warning('剩余数量为0,不能新增调度明细'); + if (!this.dispatchHasAddableQuantity) { + this.$message.warning('待调度列表货物总量已达到计划总量,不能新增调度明细'); return; } this.openDispatchItemDialog(); @@ -11981,8 +12552,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 +12564,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) { @@ -12980,15 +13544,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 { @@ -13204,6 +13772,8 @@ export default { } &__dispatch-table { + flex: 1; + min-height: 0; font-size: 13px; :deep(.el-table__header .cell) { @@ -13217,6 +13787,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; @@ -13285,6 +13869,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)); @@ -13451,6 +14044,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 efa6f6c..5a8545f 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 6f756c4..7d37ed8 100644 --- a/src/views/business/loading-manage.vue +++ b/src/views/business/loading-manage.vue @@ -583,6 +583,7 @@ clearable filterable placeholder="请选择" + :disabled="dialogMode === 'add'" @visible-change="visible => visible && loadTransportTypeOptions()" > - visible && loadDriverOptions(dialogForm.driverName)" - > - - + @select="item => handleDriverSuggestionSelect('dialog', item)" + /> @@ -853,13 +842,13 @@ - + - + - + @@ -1047,7 +1036,7 @@ const createDialogForm = () => ({ batchNo: '', currentProcessNode: '接单', mileage: '', - estimatedStartDate: dayjs().format('YYYY-MM-DD'), + estimatedStartDate: '', estimatedEndDate: '', taskRemark: '', goodsJson: '', @@ -1207,6 +1196,13 @@ export default { mounted() { this.loadTransportTypeOptions(); this.loadTable(); + const detailId = this.$route.query.detailId; + if (detailId) this.openLoadingDialog('view', { id: detailId }); + }, + watch: { + '$route.query.detailId'(detailId) { + if (detailId) this.openLoadingDialog('view', { id: detailId }); + }, }, methods: { buildQueryParams(form) { @@ -1280,11 +1276,11 @@ export default { findWaybillId(row, waybillNo) { const rows = this.splitText(row.loadingSubNos); const index = rows.findIndex(item => item === waybillNo); - const linkedRows = this.parseJsonArray(row.waybillList || row.waybillRows || row.waybills); + const linkedRows = parseJsonArray(row.waybillList || row.waybillRows || row.waybills); if (linkedRows[index]?.id || linkedRows[index]?.waybillId) { return linkedRows[index].id || linkedRows[index].waybillId; } - const ids = this.parseJsonArray(row.waybillIdsJson); + const ids = parseJsonArray(row.waybillIdsJson); return ids[index] || ''; }, async openWaybillDetail(row = {}) { @@ -1415,6 +1411,7 @@ export default { this.dialogVisible = true; this.dialogLoading = true; this.resetDialogData(); + if (mode === 'add') this.candidateQuery.transportType = 'road'; try { if (row?.id) { const res = await loadingApi.getDetail(row.id); @@ -1435,6 +1432,7 @@ export default { } else { this.dialogForm = createDialogForm(); this.candidateQuery = createCandidateSearchForm(); + this.candidateQuery.transportType = 'road'; await this.loadCandidateWaybills(); } } finally { @@ -1465,6 +1463,7 @@ export default { }, clearLoadingDialog() { this.resetDialogData(); + if (this.dialogMode === 'add') this.candidateQuery.transportType = 'road'; this.loadCandidateWaybills(); }, restoreRouteAndCargo() { @@ -1719,6 +1718,7 @@ export default { ...this.buildQueryParams(this.candidateQuery), businessStatus: 'pending', onlyUnassignedLoading: 1, + ...(this.dialogMode === 'add' ? { transportType: 'road' } : {}), } ); const page = unwrapPage(res); @@ -1738,6 +1738,7 @@ export default { }, handleCandidateReset() { this.candidateQuery = createCandidateSearchForm(); + if (this.dialogMode === 'add') this.candidateQuery.transportType = 'road'; this.candidatePage.currentPage = 1; this.loadCandidateWaybills(); }, @@ -1805,18 +1806,6 @@ export default { if (!this.dialogForm.transportType) { this.dialogForm.transportType = rows.find(row => row.transportType)?.transportType || ''; } - if (!this.dialogForm.carrierName) { - this.dialogForm.carrierName = rows.find(row => row.carrierName)?.carrierName || ''; - } - if (!this.dialogForm.driverName) { - this.dialogForm.driverName = rows.find(row => row.driverName)?.driverName || ''; - } - if (!this.dialogForm.driverPhone) { - this.dialogForm.driverPhone = rows.find(row => row.driverPhone)?.driverPhone || ''; - } - if (!this.dialogForm.vehicleNo) { - this.dialogForm.vehicleNo = rows.find(row => row.vehicleNo)?.vehicleNo || ''; - } if (rebuildRoute) { this.routeNodes = this.buildRouteNodes(rows); } @@ -2079,7 +2068,11 @@ export default { }); }, handleCarrierTypeChange() { - if (['自运', '网货平台'].includes(this.dialogForm.carrierType)) { + if (this.dialogForm.carrierType === '承运商') { + this.dialogForm.trailerVehicleNo = ''; + this.dialogForm.escortName = ''; + this.dialogForm.escortPhone = ''; + } else if (['自运', '网货平台'].includes(this.dialogForm.carrierType)) { this.dialogForm.carrierName = ''; } }, @@ -2089,6 +2082,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, ''); }, @@ -2147,7 +2160,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 5c29c76..06f6a1a 100644 --- a/src/views/business/process-config.vue +++ b/src/views/business/process-config.vue @@ -8,6 +8,10 @@ v-model="form" ref="crud" :permission="permissionList" + :before-open="beforeOpen" + @row-save="rowSave" + @row-update="rowUpdate" + @row-del="rowDel" @search-change="searchChange" @search-reset="searchReset" @selection-change="selectionChange" @@ -21,7 +25,7 @@ type="primary" icon="el-icon-plus" v-if="hasPermission('process_config_add')" - @click="openProcessConfig()" + @click="$refs.crud.rowAdd()" >新建过程配置 + + + + 保存草稿 + + + 保存并启用 + + + + {{ row.statusName || displayStatus(row.status) }} @@ -54,154 +79,101 @@ - - - 查看 - - - 编辑 - - - 复制 - - - 启用 - - - 停用 - - - 删除 - - - - - - + - - - - - - - - - - - - - - - - - 默认 + + + + - 天后完成运输 - - - - - - - - - + + + + + + + + + + + + + 默认 + + 天后完成运输 + + + + + + + + + + + - - - - - - {{ node.name }} - - {{ text }} + + + + + + + + {{ node.name }} + + {{ text }} + - - - + + - + - - - - - - - 保存草稿 - - - 保存并启用 - - - {{ dialogReadonly ? '关闭' : '取消' }} + + - + + + + 查看 + + + 编辑 + + + 复制 + + + 启用 + + + 停用 + + + 删除 + + + item.enabled); }, - dialogTitle() { - if (this.dialogReadonly) return '查看过程配置'; - return this.form.id ? '编辑过程配置' : '新增过程配置'; - }, }, created() { this.resetNodeRows(); @@ -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, '') @@ -853,6 +880,11 @@ export default { } return true; }, + stopSubmitLoading(loading) { + if (typeof loading === 'function') { + loading(); + } + }, needsDuplicateVoucherConfirm() { const signNode = this.nodeRows.find(item => item.key === 'sign'); const returnNode = this.nodeRows.find(item => item.key === 'return'); @@ -882,74 +914,50 @@ export default { } ); }, - openProcessConfig(row, readonly = false) { - this.dialogReadonly = readonly; + submitRow(row, done, loading, status = this.submitStatus) { this.submitStatus = 1; - if (row && row.id) { - api.getDetail(row.id).then(res => { - const data = res.data.data || {}; - this.form = data; - this.selectedProjectId = data.projectIds ? String(data.projectIds) : ''; - this.ensureSelectedProjectOption(); - this.parseNodeConfig(data); - this.processBox = true; - }); + this.syncNodeFields(); + const submitRow = this.normalizeRow({ ...row, status }); + if (!this.validateRow(submitRow)) { + this.stopSubmitLoading(loading); return; } - this.form = { - configName: '', - projectIds: '', - projectNames: '', - defaultFinishDays: '', - remark: '', - status: 0, - }; - this.selectedProjectId = ''; - this.resetNodeRows(); - this.processBox = true; + 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); + this.$message({ type: 'success', message: '操作成功!' }); + done(); + }) + .catch(error => { + if ( + error !== 'cancel' && + error !== 'close' && + error?.code !== 'enabled-project-config' + ) { + window.console.log(error); + } + this.stopSubmitLoading(loading); + }); }, - submitProcessConfig(mode) { - this.$refs.processForm.validate(valid => { - if (!valid) return; - this.submitting = true; - this.submitStatus = mode === 'draft' ? 2 : 1; - this.syncNodeFields(); - const submitRow = this.normalizeRow({ ...this.form, status: this.submitStatus }); - if (!this.validateRow(submitRow)) { - this.submitting = false; - return; - } - this.confirmDuplicateVoucher() - .then(() => api.submit(submitRow)) - .then(() => { - this.submitting = false; - this.processBox = false; - this.onLoad(this.page); - this.$message({ type: 'success', message: '操作成功!' }); - }) - .catch(error => { - this.submitting = false; - if (error !== 'cancel' && error !== 'close') { - window.console.log(error); - } - }); - }); + rowSave(row, done, loading) { + this.submitRow(row, done, loading); }, - resetProcess() { - this.dialogReadonly = false; - this.submitting = false; - this.submitStatus = 1; - this.form = { - configName: '', - projectIds: '', - projectNames: '', - defaultFinishDays: '', - remark: '', - status: 0, - }; - this.selectedProjectId = ''; - this.resetNodeRows(); - this.$refs.processForm?.clearValidate(); + rowUpdate(row, index, done, loading) { + this.submitRow(row, done, loading); + }, + handleProcessConfigSave(mode) { + this.submitStatus = mode === 'draft' ? 2 : 1; + if (this.$refs.crud && typeof this.$refs.crud.rowSave === 'function') { + this.$refs.crud.rowSave(); + } }, rowDel(row) { this.$confirm('确定将选择数据删除?', { @@ -993,6 +1001,35 @@ export default { } this.selectionClear(); }, + beforeOpen(done, type) { + this.dialogReadonly = type === 'view'; + this.submitStatus = 1; + if (type === 'add') { + this.form = { + configName: '', + projectIds: '', + projectNames: '', + defaultFinishDays: '', + remark: '', + status: 0, + }; + this.selectedProjectId = ''; + this.resetNodeRows(); + done(); + return; + } + if (['edit', 'view'].includes(type)) { + api.getDetail(this.form.id).then(res => { + this.form = res.data.data || {}; + this.selectedProjectId = this.form.projectIds ? String(this.form.projectIds) : ''; + this.ensureSelectedProjectOption(); + this.parseNodeConfig(this.form); + done(); + }); + return; + } + done(); + }, searchReset() { this.query = {}; this.applyDefaultDeptSearch(); @@ -1090,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); }); @@ -1240,10 +1297,5 @@ export default { :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) { background: #fafafa; } - - :deep(.process-dialog .el-dialog__body) { - max-height: 72vh; - overflow: auto; - } } diff --git a/src/views/business/voucher-manage.vue b/src/views/business/voucher-manage.vue index 9b66f2f..c631c23 100644 --- a/src/views/business/voucher-manage.vue +++ b/src/views/business/voucher-manage.vue @@ -823,7 +823,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..1ac93f7 100644 --- a/src/views/vehicle/customer-archive.vue +++ b/src/views/vehicle/customer-archive.vue @@ -655,7 +655,7 @@ - + - - - + label="变更内容" + min-width="560" + > + + + + {{ formatChangeContent(row) }} + + {{ formatChangeContent(row) }} + + + + + + 详情 + + + + + 变更日期:{{ changeRecordDetail.changeTime || '-' }} + 变更账号:{{ changeRecordDetail.changeUserName || '-' }} + + + + + + + + + 关闭 + + + {{ readonly ? '关闭' : '取消' }} 保存 @@ -1354,6 +1390,9 @@ export default { total: 0, records: [], }, + changeRecordDetailVisible: false, + changeRecordDetail: null, + changeRecordDetailRows: [], contactIndex: -1, contactForm: this.emptyContact(), contactMapBox: false, @@ -1882,17 +1921,286 @@ export default { changeRecordIndex(index) { return (this.changeRecordPage.currentPage - 1) * this.changeRecordPage.pageSize + index + 1; }, - formatChangeData(row, column, value) { - if (!value || value === '-1') return ''; + parseChangeData(value) { + if (!value || value === '-1') return {}; + if (typeof value === 'object') return this.replaceNegativeOneWithBlank(value); + const text = String(value).trim(); + if (!text || text === '-1') return {}; try { - const data = this.replaceNegativeOneWithBlank(JSON.parse(value)); - return Object.entries(data) - .map(([field, fieldValue]) => `${field}:${fieldValue ?? ''}`) - .join(';'); + const parsed = JSON.parse(text); + return parsed && typeof parsed === 'object' + ? this.replaceNegativeOneWithBlank(parsed) + : { 变更内容: parsed }; + } catch (error) { + 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: '发票类型', + qualificationAttachments: '客商材料', + customerAttachments: '客商材料', + 客商材料: '客商材料', + }; + if (fieldLabelMap[field]) return fieldLabelMap[field]; + const column = (this.option.column || []).find(item => item.prop === field); + return column?.label || field; + }, + normalizeChangeFieldValue(value) { + if (typeof value !== 'string') return value; + const text = value.trim(); + if (!text || (!text.startsWith('[') && !text.startsWith('{') && text !== 'null')) { + return value; + } + try { + return JSON.parse(text); } catch (error) { return value; } }, + isEmptyChangeValue(value) { + const normalized = this.normalizeChangeFieldValue(value); + if (normalized === undefined || normalized === null || normalized === '' || normalized === '-1') { + return true; + } + if (Array.isArray(normalized)) return normalized.length === 0; + if (typeof normalized === 'object') return Object.keys(normalized).length === 0; + return false; + }, + formatScoreChangeValue(value) { + const scores = Array.isArray(value) ? value : [value]; + const categoryNames = { basic: '基础得分项', plus: '加分项目', minus: '减分项目' }; + return scores + .filter(item => item && typeof item === 'object') + .map((score, index) => { + const summary = [ + score.scoreDate && `评分日期:${score.scoreDate}`, + score.selfScore !== undefined && score.selfScore !== '' + ? `自评得分:${this.formatScoreValue(score.selfScore)}` + : '', + score.reviewScore !== undefined && score.reviewScore !== '' + ? `复评得分:${this.formatScoreValue(score.reviewScore)}` + : '', + score.finalScore !== undefined && score.finalScore !== '' + ? `最终得分:${this.formatScoreValue(score.finalScore)}` + : '', + score.creditLevel ? `信用等级:${score.creditLevel}` : '', + ].filter(Boolean); + const scoreDetails = this.normalizeChangeFieldValue(score.details); + const details = (Array.isArray(scoreDetails) ? scoreDetails : []) + .map(detail => { + const category = + detail.categoryName || + categoryNames[this.getScoreDetailCategoryCode(detail)] || + ''; + const itemName = detail.itemName || '评分项目'; + const option = detail.selectedOption || detail.scoreDescription || ''; + const points = [ + detail.selfScore !== undefined && detail.selfScore !== '' + ? `自评${this.formatScoreValue(detail.selfScore)}分` + : '', + detail.reviewScore !== undefined && detail.reviewScore !== '' + ? `复评${this.formatScoreValue(detail.reviewScore)}分` + : '', + ].filter(Boolean); + if ( + !points.length && + detail.score !== undefined && + detail.score !== '' && + detail.score !== -1 && + detail.score !== '-1' + ) { + points.push(`得分${this.formatScoreValue(detail.score)}分`); + } + const label = `${category ? `${category}-` : ''}${itemName}`; + return `${label}${option ? `:${option}` : ''}${points.length ? `(${points.join(',')})` : ''}`; + }) + .filter(Boolean); + if (details.length) summary.push(`评分明细:${details.join(';')}`); + return summary.length ? summary.join(';') : `第${index + 1}条评分`; + }) + .join(';'); + }, + normalizeScoreChangeEntries(value) { + const normalized = this.normalizeChangeFieldValue(value); + if (Array.isArray(normalized)) return normalized; + return normalized && typeof normalized === 'object' ? [normalized] : []; + }, + formatScoreChangeDiff(beforeValue, afterValue) { + const beforeEntries = this.normalizeScoreChangeEntries(beforeValue); + const afterEntries = this.normalizeScoreChangeEntries(afterValue); + const size = Math.max(beforeEntries.length, afterEntries.length); + const fields = [ + ['scoreDate', '评分日期'], + ['selfScore', '自评得分'], + ['reviewScore', '复评得分'], + ['finalScore', '最终得分'], + ['creditLevel', '信用等级'], + ['details', '评分明细'], + ]; + const formatValue = (field, value) => { + if (field === 'details') { + return this.formatScoreChangeValue([{ details: value }]).replace(/^第\d+条评分$/, '空'); + } + if (value === undefined || value === null || value === '' || value === '-1') return '空'; + return field.endsWith('Score') ? `${this.formatScoreValue(value)}分` : String(value); + }; + const before = []; + const after = []; + for (let index = 0; index < size; index += 1) { + const left = beforeEntries[index] || {}; + const right = afterEntries[index] || {}; + fields.forEach(([field, label]) => { + const leftValue = left[field]; + const rightValue = right[field]; + if (JSON.stringify(leftValue) === JSON.stringify(rightValue)) return; + const prefix = size > 1 ? `第${index + 1}条-${label}` : label; + before.push(`${prefix}:${formatValue(field, leftValue)}`); + after.push(`${prefix}:${formatValue(field, rightValue)}`); + }); + } + return { before: before.join(';') || '空', after: after.join(';') || '空' }; + }, + formatAttachmentChangeValue(value) { + const attachments = Array.isArray(value) + ? value + : value && typeof value === 'object' + ? [value] + : this.parseAttachments(value); + const names = attachments + .map(item => { + const fileName = + typeof item === 'string' + ? item + : item?.originalName || item?.name || item?.fileName || item?.url || item?.link || ''; + const name = String(fileName).trim().split('?')[0].split('/').pop(); + try { + return decodeURIComponent(name); + } catch (error) { + return name; + } + }) + .filter(Boolean); + return names.length ? names.join('、') : '空'; + }, + formatChangeFieldValue(field, value) { + value = this.normalizeChangeFieldValue(value); + if (value === undefined || value === null || value === '' || value === '-1') return '空'; + const normalizedField = { + 准入类型: 'accessType', + 准入标准: 'accessType', + 审批状态: 'approvalStatus', + 审核状态: 'approvalStatus', + 状态: 'status', + 客户类型: 'customerType', + 客商材料: 'qualificationAttachments', + }[field] || field; + if (['qualificationAttachments', 'customerAttachments'].includes(normalizedField)) { + return this.formatAttachmentChangeValue(value); + } + if (normalizedField === '评分信息' || normalizedField === 'scores') { + return this.formatScoreChangeValue(value) || '空'; + } + 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); + }, + buildChangeRecordDetailRows(row = {}) { + const beforeData = this.parseChangeData(row.beforeData); + const afterData = this.parseChangeData(row.afterData); + const fields = [...new Set([...Object.keys(beforeData), ...Object.keys(afterData)])]; + const rows = fields + .filter( + field => + field !== '变更内容' && + !(this.isEmptyChangeValue(beforeData[field]) && this.isEmptyChangeValue(afterData[field])) + ) + .map(field => ({ + field: this.getChangeFieldLabel(field), + ...(this.isScoreChangeField(field) + ? this.formatScoreChangeDiff(beforeData[field], afterData[field]) + : { + before: this.formatChangeFieldValue(field, beforeData[field]), + after: this.formatChangeFieldValue(field, afterData[field]), + }), + })); + const content = String(row.changeContent || '').trim(); + if (content) { + rows.unshift({ field: '变更内容', before: '空', after: content }); + } + return rows; + }, + isScoreChangeField(field) { + return ['评分信息', 'scores'].includes(field); + }, + openChangeRecordDetail(row) { + this.changeRecordDetail = row; + this.changeRecordDetailRows = this.buildChangeRecordDetailRows(row); + this.changeRecordDetailVisible = true; + }, + 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 !== '变更内容' && + !(this.isEmptyChangeValue(beforeData[field]) && this.isEmptyChangeValue(afterData[field])) + ) + .map(field => { + const scoreDiff = this.isScoreChangeField(field) + ? this.formatScoreChangeDiff(beforeData[field], afterData[field]) + : null; + const before = scoreDiff?.before || this.formatChangeFieldValue(field, beforeData[field]); + const after = scoreDiff?.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: '', @@ -3007,6 +3315,9 @@ export default { }, resetArchive() { this.readonly = false; + this.changeRecordDetailVisible = false; + this.changeRecordDetail = null; + this.changeRecordDetailRows = []; this.archiveForm = this.emptyArchive(); this.originalAccessType = ''; this.qualificationFiles = []; @@ -4108,6 +4419,42 @@ 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(.change-record-detail-dialog .el-dialog__body) { + max-height: 65vh; + overflow: auto; +} + +:deep(.change-record-detail-dialog .change-record-detail-value .cell) { + white-space: pre-wrap; + word-break: break-word; + overflow: visible; + text-overflow: clip; + line-height: 1.6; +} + +.change-record-detail-meta { + display: flex; + gap: 32px; + margin-bottom: 16px; + color: #606266; + font-size: 14px; +} + :deep(.archive-dialog .el-dialog__body) { max-height: 72vh; overflow: auto;