diff --git a/src/api/business/common.js b/src/api/business/common.js index c2dd4f3..14cc03b 100644 --- a/src/api/business/common.js +++ b/src/api/business/common.js @@ -92,13 +92,12 @@ export const createCrudApi = baseUrl => ({ data, }); }, - reassign(id) { + reassign(data) { + const payload = data && typeof data === 'object' ? data : { id: data }; return request({ url: `${baseUrl}/reassign`, method: 'post', - params: { - id, - }, + data: payload, }); }, batchComplete(ids) { diff --git a/src/api/business/waybill-manage.js b/src/api/business/waybill-manage.js index 78d93e9..316f053 100644 --- a/src/api/business/waybill-manage.js +++ b/src/api/business/waybill-manage.js @@ -26,6 +26,13 @@ export const maintainMileage = data => method: 'post', data, }); +/** 运单打卡记录 + 司机上传凭证图 */ +export const getPunchRecords = waybillId => + request({ + url: `${baseUrl}/punch-records`, + method: 'get', + params: { waybillId }, + }); export const roadLoading = ids => request({ url: `${baseUrl}/road-loading`, diff --git a/src/option/business/common.js b/src/option/business/common.js index 7d07f95..26b5cea 100644 --- a/src/option/business/common.js +++ b/src/option/business/common.js @@ -41,7 +41,7 @@ export const planStatusOptions = [ export const waybillStatusOptions = [ { label: '草稿', value: 'draft' }, { label: '待执行', value: 'pending' }, - { label: '进行中', value: 'processing' }, + { label: '进行中', value: 'running' }, { label: '已完成', value: 'completed' }, { label: '已取消', value: 'cancelled' }, ]; diff --git a/src/option/business/waybill-manage.js b/src/option/business/waybill-manage.js index e9f2af6..60b94e6 100644 --- a/src/option/business/waybill-manage.js +++ b/src/option/business/waybill-manage.js @@ -92,14 +92,32 @@ const getProcessConfigNodes = row => { const isAcceptProcessNode = node => node.key === 'accept' || node.name === '接单'; +const isTruthyFlag = value => { + if (value === true || value === 1) return true; + if (value === false || value === 0 || value === null || value === undefined || value === '') { + return false; + } + return ['true', '1', 'yes'].includes(String(value).trim().toLowerCase()); +}; + const requiresDriverAcceptConfirmation = row => - getProcessConfigNodes(row).some( - node => - isAcceptProcessNode(node) && - node.enabled !== false && - node.confirmMode === 'yes' && - node.confirmDriver === true - ); + getProcessConfigNodes(row).some(node => { + if (!isAcceptProcessNode(node) || node.enabled === false) return false; + if (node.confirmMode === 'no_confirm_accept') return false; + // 与后端一致:是否确认=是 即需接单(不强制依赖 confirmDriver) + return node.confirmMode === 'yes' || !node.confirmMode; + }); + +const getDriverRejectReason = row => { + if (!row) return ''; + const reason = + row.driverRejectReason || + row.driverRefuseReason || + row.acceptRejectReason || + row.rejectReason || + ''; + return String(reason || '').trim(); +}; const isDriverRejectedStatus = value => { if (value === null || value === undefined || typeof value === 'boolean') return false; @@ -122,6 +140,7 @@ const isDriverRejectedStatus = value => { }; const hasDriverRejectRecord = row => { + if (String(row.driverAcceptStatus || '').toLowerCase() === 'rejected') return true; const rejectRecordValues = [ row.driverRejectTime, row.driverRejectedTime, @@ -161,9 +180,16 @@ const hasDriverRejectRecord = row => { }; const isDriverRejectedWaybill = row => - requiresDriverAcceptConfirmation(row) && hasDriverRejectRecord(row); + (row.requireAccept === true || + row.requireAccept === 1 || + requiresDriverAcceptConfirmation(row)) && + hasDriverRejectRecord(row); const hasDriverAcceptRecord = row => { + if (String(row.driverAcceptStatus || '').toLowerCase() === 'accepted') return true; + if (['rejected', 'pending'].includes(String(row.driverAcceptStatus || '').toLowerCase())) { + return false; + } const recordValues = [ row.driverAcceptTime, row.driverAcceptedTime, @@ -172,6 +198,7 @@ const hasDriverAcceptRecord = row => { row.driverAcceptedBy, row.acceptUserId, row.acceptUserName, + row.driverAcceptDriverId, ]; if (recordValues.some(value => value !== null && value !== undefined && value !== '')) { return true; @@ -425,26 +452,26 @@ export const config = { statusProp: 'businessStatus', statusTextProp: 'businessStatusName', formatStatus(row, prop, defaultText) { - if ( - prop === 'businessStatus' && - requiresDriverAcceptConfirmation(row) && - isInProgressStatus(row, defaultText) && - !hasDriverAcceptRecord(row) - ) { + if (prop !== 'businessStatus') return defaultText; + const status = String(row.businessStatus || ''); + const terminalStatuses = ['draft', 'completed', 'cancelled', 'waiting_dispatch', 'dispatching']; + const needAccept = + row.requireAccept === true || + row.requireAccept === 1 || + requiresDriverAcceptConfirmation(row); + // 需司机接单且尚未接单 → 待执行(含库中仍为 running 的历史数据展示校正) + if (needAccept && !hasDriverAcceptRecord(row) && !terminalStatuses.includes(status)) { return '待执行'; } - if ( - prop === 'businessStatus' && - row.businessStatus === 'pending' && - !requiresDriverAcceptConfirmation(row) - ) { - return '进行中'; - } + if (status === 'pending') return '待执行'; + if (status === 'running' || status === 'processing') return '进行中'; return defaultText; }, canReassign(row) { return isDriverRejectedWaybill(row); }, + getDriverRejectReason, + isDriverRejectedWaybill, canEdit(row) { // 进行中的运单不允许编辑 if (isInProgressStatus(row, row.businessStatusName)) { diff --git a/src/views/business/components/business-crud-page.vue b/src/views/business/components/business-crud-page.vue index f237a35..419eb99 100644 --- a/src/views/business/components/business-crud-page.vue +++ b/src/views/business/components/business-crud-page.vue @@ -6874,7 +6874,7 @@ export default { const status = this.statusValue(row); return this.config.permission === 'transport_plan' ? ['waiting_dispatch', 'dispatching'].includes(status) - : ['pending', 'processing'].includes(status); + : ['pending', 'processing', 'running'].includes(status); }, canComplete(row) { if ( @@ -6888,7 +6888,7 @@ export default { const status = this.statusValue(row); return this.config.permission === 'transport_plan' ? status === 'dispatching' - : status === 'processing'; + : status === 'processing' || status === 'running'; }, canMaintainMileage(row) { return ( diff --git a/src/views/business/components/waybill-manage-page.vue b/src/views/business/components/waybill-manage-page.vue index 3e5c39c..39ee839 100644 --- a/src/views/business/components/waybill-manage-page.vue +++ b/src/views/business/components/waybill-manage-page.vue @@ -100,9 +100,20 @@ + + +
+
+ 运单号 + {{ + reassignDrawer.row?.waybillNo || '-' + }} +
+
+ 拒绝原因 + {{ + driverRejectReasonText(reassignDrawer.row) || '-' + }} +
+
+
+ + + + + + + + + +
+
+ +
+ - - {{ node.name || node.nodeName || node.label }} - - +
+ + +
+
+ {{ record.nodeName || record.nodeCode || '打卡' }} + {{ record.statusName || (record.punched ? '已打卡' : '未打卡') }} + 在途 + 异常 +
+ +
暂未打卡
+
+
+
+ +
- + +
+
+ +
+ {{ photo.label || '凭证' }} +
+
+ +
+
@@ -2837,6 +3023,7 @@ import { getList as getProcessConfigList, getVoucherImages as getProcessConfigVoucherImages, } from '@/api/business/process-config'; +import { getPunchRecords as getWaybillPunchRecords } from '@/api/business/waybill-manage'; import { getList as getDriverList } from '@/api/transportCapacity/driver'; import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary as getSystemDictionary } from '@/api/system/dict'; @@ -2848,7 +3035,7 @@ import { openImportDialog } from '@/utils/import-excel'; import { applyTableMenuWidth } from '@/utils/table-menu'; import { downloadFileByUrl, downloadXls } from '@/utils/util'; import { isMobile } from '@/utils/validate'; -import { InfoFilled, Location, OfficeBuilding, Rank, Search } from '@element-plus/icons-vue'; +import { InfoFilled, Location, OfficeBuilding, Rank, Search, WarnTriangleFilled } from '@element-plus/icons-vue'; import { ElImageViewer } from 'element-plus'; import { OpenFileViewer } from '@open-file-viewer/vue'; import { @@ -3023,6 +3210,7 @@ export default { PageAvueForm, PageDetail, InfoFilled, + WarnTriangleFilled, Rank, ElImageViewer, OpenFileViewer, @@ -3079,6 +3267,9 @@ export default { detailLoading: false, detailRow: {}, waybillDetailProcessNodes: [], + waybillPunchRecords: [], + waybillDriverUploads: [], + waybillPunchRecordsLoading: false, waybillProcessDetailTab: 'punch', waybillHasRelatedVoucher: false, waybillVoucherImages: [], @@ -3100,6 +3291,41 @@ export default { submitting: false, row: null, }, + reassignDrawer: { + visible: false, + submitting: false, + driverLoading: false, + row: null, + }, + reassignForm: { + id: '', + driverId: '', + driverName: '', + driverPhone: '', + vehicleNo: '', + }, + reassignDriverOptions: [], + reassignRules: { + driverName: [{ required: true, message: '请选择司机', trigger: 'change' }], + driverPhone: [ + { required: true, message: '请输入手机号', trigger: 'blur' }, + { + validator: (rule, value, callback) => { + if (!value) { + callback(); + return; + } + if (!isMobile(value)) { + callback(new Error('请输入正确的手机号')); + return; + } + callback(); + }, + trigger: 'blur', + }, + ], + vehicleNo: [{ required: true, message: '请输入车牌号', trigger: 'blur' }], + }, mileageForm: { id: '', mileage: '', @@ -3602,6 +3828,7 @@ export default { }, waybillProcessDetailTab(tab) { if (tab === 'batchSupplement') this.loadWaybillVoucherImages(); + if (tab === 'punch' || tab === 'driverUpload') this.loadWaybillPunchRecords(); }, 'form.transportType'(value, oldValue) { if (!this.shippingInfoFormEnabled || this.suppressTransportTypeClear) return; @@ -3884,7 +4111,7 @@ export default { return false; } const status = this.statusValue(row); - return ['pending', 'processing'].includes(status); + return ['pending', 'processing', 'running'].includes(status); }, canComplete(row) { if ( @@ -3896,7 +4123,7 @@ export default { return false; } const status = this.statusValue(row); - return status === 'processing'; + return status === 'processing' || status === 'running'; }, canMaintainMileage(row) { return ( @@ -4163,6 +4390,9 @@ export default { this.detailRow = { ...row }; this.waybillProcessDetailTab = 'punch'; this.waybillHasRelatedVoucher = false; + this.waybillDetailProcessNodes = []; + this.waybillPunchRecords = []; + this.waybillDriverUploads = []; this.waybillVoucherImages = []; this.waybillVoucherFolders = []; this.waybillVoucherFolder = null; @@ -4176,6 +4406,7 @@ export default { .then(res => { const detail = res?.data?.data || res?.data || row; this.detailRow = detail; + this.loadWaybillPunchRecords(); this.loadWaybillVoucherImages(); if (detail.contractId) { return getContractDetail(detail.contractId) @@ -4259,6 +4490,37 @@ export default { }) .catch(() => []); }, + loadWaybillPunchRecords() { + const waybillId = this.detailRow?.id; + if (!waybillId || this.waybillPunchRecordsLoading) return; + this.waybillPunchRecordsLoading = true; + const request = + typeof this.api.getPunchRecords === 'function' + ? this.api.getPunchRecords(waybillId) + : getWaybillPunchRecords(waybillId); + request + .then(res => { + const data = res?.data?.data || res?.data || {}; + this.waybillPunchRecords = Array.isArray(data.records) ? data.records : []; + this.waybillDriverUploads = Array.isArray(data.driverUploads) + ? data.driverUploads + : []; + }) + .catch(() => { + this.waybillPunchRecords = []; + this.waybillDriverUploads = []; + }) + .finally(() => { + this.waybillPunchRecordsLoading = false; + }); + }, + previewDriverUploadPhoto(photos, index) { + const list = (photos || []).map(item => item?.url).filter(Boolean); + if (!list.length) return; + this.attachmentImagePreviewUrls = list; + this.attachmentImagePreviewIndex = Math.min(Math.max(Number(index) || 0, 0), list.length - 1); + this.attachmentImagePreviewVisible = true; + }, loadWaybillVoucherImages() { if ( !this.detailRow.id || @@ -7813,6 +8075,10 @@ export default { this.$refs.mileageFormRef?.clearValidate(); }, handleAction(action, row) { + if (action === 'reassign') { + this.openReassignDrawer(row); + return; + } const actionName = { enable: '启用', disable: '停用', @@ -7831,6 +8097,113 @@ export default { this.onLoad(this.page, this.query); }); }, + driverRejectReasonText(row) { + if (!row) return ''; + if (typeof this.config.getDriverRejectReason === 'function') { + return this.config.getDriverRejectReason(row) || ''; + } + return String( + row.driverRejectReason || + row.driverRefuseReason || + row.acceptRejectReason || + row.rejectReason || + '' + ).trim(); + }, + openReassignDrawer(row) { + if (!row?.id) return; + this.reassignDrawer.row = row; + this.reassignForm = { + id: row.id, + driverId: '', + driverName: '', + driverPhone: '', + vehicleNo: '', + }; + this.reassignDrawer.visible = true; + this.$nextTick(() => this.$refs.reassignFormRef?.clearValidate()); + }, + resetReassignDrawer() { + this.reassignDrawer.row = null; + this.reassignDrawer.submitting = false; + this.reassignDrawer.driverLoading = false; + this.reassignForm = { + id: '', + driverId: '', + driverName: '', + driverPhone: '', + vehicleNo: '', + }; + this.reassignDriverOptions = []; + this.$refs.reassignFormRef?.clearValidate(); + }, + fetchReassignDriverSuggestions(queryString, callback) { + const keyword = String(queryString || '').trim(); + this.reassignDrawer.driverLoading = true; + getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '司机' }) + .then(res => { + const records = extractRecords(res); + this.reassignDriverOptions = records; + callback( + records.map(item => ({ + ...item, + value: item.driverName || item.name || '', + })) + ); + }) + .catch(error => { + window.console.log(error); + callback([]); + }) + .finally(() => { + this.reassignDrawer.driverLoading = false; + }); + }, + handleReassignDriverSelect(item = {}) { + const value = item.driverName || item.name || ''; + const driverPhone = item.mobile || item.phone || item.driverPhone || ''; + const drivingVehicle = String(item.drivingVehicle || '').trim(); + this.reassignForm.driverId = item.id || ''; + this.reassignForm.driverName = value; + if (driverPhone) this.reassignForm.driverPhone = driverPhone; + if (drivingVehicle) this.reassignForm.vehicleNo = drivingVehicle; + }, + handleReassignDriverChange(value) { + const driver = this.reassignDriverOptions.find(item => + [item.driverName, item.name].some(name => String(name || '') === String(value || '')) + ); + this.reassignForm.driverId = driver?.id || ''; + this.reassignForm.driverName = value || ''; + if (driver) { + this.reassignForm.driverPhone = + driver.mobile || driver.phone || driver.driverPhone || this.reassignForm.driverPhone || ''; + if (String(driver.drivingVehicle || '').trim()) { + this.reassignForm.vehicleNo = String(driver.drivingVehicle).trim(); + } + } + }, + submitReassign() { + this.$refs.reassignFormRef?.validate(valid => { + if (!valid) return; + this.reassignDrawer.submitting = true; + this.api + .reassign({ + id: this.reassignForm.id, + driverId: this.reassignForm.driverId || null, + driverName: String(this.reassignForm.driverName || '').trim(), + driverPhone: String(this.reassignForm.driverPhone || '').trim(), + vehicleNo: String(this.reassignForm.vehicleNo || '').trim(), + }) + .then(() => { + this.$message.success('重新派单成功'); + this.reassignDrawer.visible = false; + this.onLoad(this.page, this.query); + }) + .finally(() => { + this.reassignDrawer.submitting = false; + }); + }); + }, handleCustomOperation(operation, row) { if (operation.action === 'createFromTemplate') { this.api.getDetail(row.id).then(res => { @@ -7933,7 +8306,7 @@ export default { } const unavailableWaybills = this.selectionList.filter( row => - this.statusValue(row) !== 'pending' || + !['processing', 'running'].includes(this.statusValue(row)) || row.loadingId || row.loadingManageId || row.loadingNo @@ -9075,6 +9448,63 @@ export default { min-height: 120px; } + &__punch-record { + display: flex; + flex-direction: column; + gap: 6px; + } + + &__punch-record-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 600; + color: #303133; + } + + &__punch-record-line { + font-size: 13px; + color: #606266; + + span + span { + margin-left: 12px; + } + } + + &__punch-record-photos, + &__driver-upload-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; + margin-top: 4px; + min-height: 80px; + } + + &__driver-upload-item { + display: flex; + flex-direction: column; + gap: 6px; + cursor: pointer; + + .el-image { + display: block; + width: 100%; + aspect-ratio: 1; + border: 1px solid #eff1f7; + border-radius: 4px; + overflow: hidden; + } + } + + &__driver-upload-label { + font-size: 12px; + line-height: 1.4; + color: #606266; + text-align: center; + word-break: break-all; + } + &__voucher-image-item { position: relative; aspect-ratio: 1; @@ -9456,6 +9886,78 @@ export default { :global(.waybill-manage-page__mileage-dialog .waybill-manage-page__mileage-form .el-textarea) { width: 100%; } + +.waybill-manage-page__status-cell { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.waybill-manage-page__reject-tip-icon { + color: #e6a23c; + cursor: pointer; + font-size: 16px; + vertical-align: middle; +} + +:global(.waybill-manage-page__reassign-drawer .el-drawer__body) { + padding: 12px 20px 0; +} + +:global(.waybill-manage-page__reassign-drawer .waybill-manage-page__reassign-form) { + width: 100%; +} + +.waybill-manage-page__reassign-row { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + column-gap: 16px; + row-gap: 8px; + margin-bottom: 12px; + align-items: start; +} + +.waybill-manage-page__reassign-span-1 { + grid-column: span 1; + margin-bottom: 0; + width: 100%; +} + +.waybill-manage-page__reassign-meta { + grid-column: span 1; + display: flex; + align-items: flex-start; + min-width: 0; + line-height: 32px; + font-size: 14px; +} + +.waybill-manage-page__reassign-meta--wide { + grid-column: span 3; +} + +.waybill-manage-page__reassign-meta-label { + flex: none; + width: 72px; + color: #606266; + text-align: right; + padding-right: 12px; + box-sizing: border-box; +} + +.waybill-manage-page__reassign-meta-value { + flex: 1; + min-width: 0; + color: #303133; + word-break: break-all; +} + +.waybill-manage-page__reassign-footer { + display: flex; + justify-content: flex-end; + gap: 12px; +} + .el-input__icon { color: #1e90ff !important; } diff --git a/src/views/business/process-config.vue b/src/views/business/process-config.vue index 2716639..b871102 100644 --- a/src/views/business/process-config.vue +++ b/src/views/business/process-config.vue @@ -227,7 +227,7 @@ 无需确认接单 @@ -235,12 +235,7 @@
确认接单人 - 司机 + 司机
@@ -551,10 +546,19 @@ export default { hasPermission(code) { return this.isAdmin || this.validData(this.permission && this.permission[code], false); }, + handleAcceptConfirmChange(row) { + row.confirmDriver = row.confirmMode === 'yes'; + this.syncNodeFields(); + }, handleReturnConfirmChange(row) { row.confirmInternal = row.confirmMode === 'yes'; this.syncNodeFields(); }, + enforceAcceptDriver(node) { + if (!node || (node.key !== 'accept' && node.name !== '接单')) return node; + node.confirmDriver = node.confirmMode === 'yes'; + return node; + }, initDeptOptions() { getDeptTree(this.userInfo.tenantId).then(res => { const deptColumn = this.findColumn(this.option.column, 'searchDeptId'); @@ -610,7 +614,7 @@ export default { return 'warning'; }, canEdit(row) { - return this.hasPermission('process_config_edit') && !row.readonly; + return this.hasPermission('process_config_edit') && !row.readonly && !row.hasRelatedWaybill; }, canDelete(row) { return ( @@ -626,7 +630,7 @@ export default { return this.hasPermission('process_config_disable') && !row.readonly && row.status === 1; }, cloneNode(template) { - return { + const node = { enabled: false, punch: true, location: true, @@ -641,10 +645,16 @@ export default { supportVoucher: false, voucherOptions: [], confirmMode: 'yes', - confirmDriver: false, + confirmDriver: true, confirmInternal: false, ...template, }; + if ((node.key === 'accept' || node.name === '接单') && node.confirmMode === 'yes') { + node.confirmDriver = true; + } else if ((node.key === 'accept' || node.name === '接单') && node.confirmMode !== 'yes') { + node.confirmDriver = false; + } + return node; }, resetNodeRows() { this.nodeRows = NODE_TEMPLATES.map(item => this.cloneNode(item)); @@ -769,6 +779,7 @@ export default { this.syncNodeFields(); }, syncNodeFields() { + this.nodeRows.forEach(item => this.enforceAcceptDriver(item)); const enabledNodes = this.nodeRows.filter(item => item.enabled); this.form.includedNodes = enabledNodes.map(item => item.name).join(','); this.form.nodeConfigJson = JSON.stringify( @@ -935,7 +946,7 @@ export default { ); }, openConfig(row, readonly = false) { - this.dialogReadonly = readonly; + this.dialogReadonly = readonly || !!(row && row.hasRelatedWaybill); if (!row || !row.id) { this.form = { configName: '', @@ -952,6 +963,7 @@ export default { } api.getDetail(row.id).then(res => { const detail = res.data.data || {}; + this.dialogReadonly = readonly || !!detail.hasRelatedWaybill; this.form = { ...detail, defaultFinishDays: @@ -969,6 +981,10 @@ export default { }, submitConfig(mode) { if (this.submitting) return; + if (this.form.id && this.form.hasRelatedWaybill) { + this.$message.warning('该项目已有运单,过程配置不可修改'); + return; + } this.syncNodeFields(); const submitRow = this.normalizeRow({ ...this.form,