From 764c0f4581a0a79f67e7b0d6f979a05e9df9f8f9 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 11 Sep 2026 14:39:30 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E8=BF=90=E5=8D=95=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/business/common.js | 7 +- src/api/business/waybill-manage.js | 7 + src/option/business/common.js | 2 +- src/option/business/waybill-manage.js | 69 ++- .../components/business-crud-page.vue | 4 +- .../components/waybill-manage-page.vue | 542 +++++++++++++++++- src/views/business/process-config.vue | 38 +- 7 files changed, 610 insertions(+), 59 deletions(-) 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, From dbeb174ae225ca09b7db71bbe3435f21e168a667 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sun, 13 Sep 2026 01:01:24 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/business/project-apply.js | 6 + src/option/business/project-apply.js | 18 +- src/views/business/process-config.vue | 11 +- src/views/business/project-apply.vue | 447 +++++++++++++++++++----- src/views/transportCapacity/driver.vue | 53 ++- src/views/transportCapacity/vehicle.vue | 68 +++- 6 files changed, 493 insertions(+), 110 deletions(-) diff --git a/src/api/business/project-apply.js b/src/api/business/project-apply.js index cf95c97..3c41e5b 100644 --- a/src/api/business/project-apply.js +++ b/src/api/business/project-apply.js @@ -6,6 +6,12 @@ const api = createCrudApi(baseUrl); export const getList = api.getList; export const getDetail = api.getDetail; +export const getChangeRecordDetail = (id, recordIndex) => + request({ + url: `${baseUrl}/change-record/detail`, + method: 'get', + params: { id, recordIndex }, + }); export const submit = api.submit; export const remove = api.remove; diff --git a/src/option/business/project-apply.js b/src/option/business/project-apply.js index 399287b..84a8dcb 100644 --- a/src/option/business/project-apply.js +++ b/src/option/business/project-apply.js @@ -192,6 +192,15 @@ export const option = createCrudOption([ formatter: row => formatUserRealName(row, 'principal'), rules: textRule('项目负责人', 50, true), }, + { + label: '资金使用风险', + prop: 'fundUseRisk', + slot: true, + minWidth: 150, + addDisplay: false, + editDisplay: false, + hide: false, + }, { label: '项目负责人ID', prop: 'principalUserId', @@ -433,14 +442,5 @@ export const option = createCrudOption([ span: 24, hide: true, }, - { - label: '资金使用风险', - prop: 'fundUseRisk', - slot: true, - minWidth: 130, - addDisplay: false, - editDisplay: false, - hide: false, - }, ...auditColumns.map(column => ({ ...column, hide: true })), ]); diff --git a/src/views/business/process-config.vue b/src/views/business/process-config.vue index b871102..f3147c0 100644 --- a/src/views/business/process-config.vue +++ b/src/views/business/process-config.vue @@ -786,14 +786,21 @@ export default { this.nodeRows.map(item => ({ key: item.key, name: item.name, + type: item.type, enabled: item.enabled, confirmMode: item.confirmMode, confirmDriver: item.confirmDriver, confirmInternal: item.confirmInternal, punch: item.punch, location: item.location, - uploadCargo: item.uploadCargo, - cargoTypes: item.uploadCargo ? item.cargoTypes : [], + // 在途不支持货量;其它节点按配置 + uploadCargo: item.type === 'transit' || item.key === 'transit' ? false : item.uploadCargo, + cargoTypes: + item.type === 'transit' || item.key === 'transit' + ? [] + : item.uploadCargo + ? item.cargoTypes + : [], frequencyDays: item.frequencyDays, timeStart: item.timeStart, timeEnd: item.timeEnd, diff --git a/src/views/business/project-apply.vue b/src/views/business/project-apply.vue index d417b91..362fc20 100644 --- a/src/views/business/project-apply.vue +++ b/src/views/business/project-apply.vue @@ -66,9 +66,10 @@ - + + + + + @@ -5412,13 +5465,19 @@ const defaultBillingPlan = () => ({ }); const defaultSettlementRule = () => ({ - autoGenerate: 1, + autoGenerate: '', billStartDate: '', - settlementType: '月结', - billCycleType: '固定截单日', - billCutoffDay: 25, + settlementType: '', + billCycleType: '', + billCutoffDay: '', cycleDays: '', + customPeriods: [], }); +const normalizeCustomPeriods = (periods = []) => + (periods || []).map(item => ({ + startDay: Number(item?.startDay) > 0 ? Number(item.startDay) : '', + endDay: Number(item?.endDay) > 0 ? Number(item.endDay) : '', + })); const defaultReconciliation = () => ({ skipReconciliation: 1, @@ -5837,7 +5896,7 @@ export default { 按数量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], }, settlementTypeOptions: ['月结', '日结', '周结', '半月结', '固定天数周期结算'], - billCycleTypeOptions: ['固定截单日', '自然月'], + billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'], reconciliationModeOptions: ['明细逐行核对', '按账单汇总核对', '跳过自动核对'], packageOptions, quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'], @@ -6448,6 +6507,11 @@ export default { this.showSettlementBillCycleType && this.settlementRuleForm.billCycleType === '固定截单日' ); }, + showSettlementCustomPeriods() { + return ( + this.showSettlementBillCycleType && this.settlementRuleForm.billCycleType === '自定义多周期' + ); + }, showSettlementCycleDays() { return this.settlementRuleForm.settlementType === '固定天数周期结算'; }, @@ -7025,7 +7089,7 @@ export default { if (!row) return '-'; if (prop === 'businessStatus') return this.displayStatus(row, prop); if (prop === 'feeGenerationMode') { - return row[prop] === 'manual' ? '手动生成' : '系统生成'; + return row[prop] === 'manual' ? '账单导入生成' : '系统生成'; } const column = this.findColumn(this.option.column, prop); if (column?.formatter) return column.formatter(row, {}, row[prop]); @@ -7566,23 +7630,32 @@ export default { isFixedBillCutoffSettlement(rule = this.settlementRuleForm) { return this.isMonthlySettlement(rule) && rule.billCycleType === '固定截单日'; }, + isCustomMultiPeriodSettlement(rule = this.settlementRuleForm) { + return this.isMonthlySettlement(rule) && rule.billCycleType === '自定义多周期'; + }, isFixedCycleSettlement(rule = this.settlementRuleForm) { return rule.settlementType === '固定天数周期结算'; }, handleSettlementTypeChange(value) { this.clearSettlementRuleError('settlementType'); if (value === '月结') { - if (!this.settlementRuleForm.billCycleType) { - this.settlementRuleForm.billCycleType = '固定截单日'; - } if (this.settlementRuleForm.billCycleType === '固定截单日') { - this.settlementRuleForm.billCutoffDay = this.settlementRuleForm.billCutoffDay || 25; + this.settlementRuleForm.customPeriods = []; + } else if (this.settlementRuleForm.billCycleType === '自定义多周期') { + this.settlementRuleForm.billCutoffDay = ''; + this.settlementRuleForm.customPeriods = normalizeCustomPeriods( + this.settlementRuleForm.customPeriods || [] + ); + } else { + this.settlementRuleForm.billCutoffDay = ''; + this.settlementRuleForm.customPeriods = []; } this.settlementRuleForm.cycleDays = ''; return; } this.settlementRuleForm.billCycleType = ''; this.settlementRuleForm.billCutoffDay = ''; + this.settlementRuleForm.customPeriods = []; if (value !== '固定天数周期结算') { this.settlementRuleForm.cycleDays = ''; } @@ -7590,10 +7663,17 @@ export default { handleSettlementBillCycleTypeChange(value) { this.clearSettlementRuleError('billCycleType'); if (value === '固定截单日') { - this.settlementRuleForm.billCutoffDay = this.settlementRuleForm.billCutoffDay || 25; + this.settlementRuleForm.customPeriods = []; return; } this.settlementRuleForm.billCutoffDay = ''; + if (value === '自定义多周期') { + this.settlementRuleForm.customPeriods = normalizeCustomPeriods( + this.settlementRuleForm.customPeriods || [] + ); + return; + } + this.settlementRuleForm.customPeriods = []; }, normalizeSettlementRule(rule = this.settlementRuleForm) { const nextRule = { @@ -7603,14 +7683,94 @@ export default { if (!this.isMonthlySettlement(nextRule)) { nextRule.billCycleType = ''; nextRule.billCutoffDay = ''; + nextRule.customPeriods = []; } else if (!this.isFixedBillCutoffSettlement(nextRule)) { nextRule.billCutoffDay = ''; } + if (this.isCustomMultiPeriodSettlement(nextRule)) { + nextRule.customPeriods = normalizeCustomPeriods( + Array.isArray(nextRule.customPeriods) ? nextRule.customPeriods : [] + ); + } else { + nextRule.customPeriods = []; + } if (!this.isFixedCycleSettlement(nextRule)) { nextRule.cycleDays = ''; } return nextRule; }, + customPeriodEndDayOptions(row) { + const startDay = Number(row?.startDay); + if (!Number.isFinite(startDay) || startDay < 1) return this.billCutoffDayOptions; + return Array.from({ length: 31 - startDay + 1 }, (_, index) => { + const value = startDay + index; + return { label: `${value}日`, value }; + }); + }, + handleCustomPeriodStartDayChange(index, value) { + const periods = normalizeCustomPeriods(this.settlementRuleForm.customPeriods || []); + const row = periods[index]; + if (!row) return; + row.startDay = value || ''; + const startDay = Number(row.startDay); + const endDay = Number(row.endDay); + if ( + Number.isFinite(startDay) && + Number.isFinite(endDay) && + (endDay < startDay || endDay > 31) + ) { + row.endDay = ''; + } + this.settlementRuleForm.customPeriods = periods; + }, + handleCustomPeriodEndDayChange(index, value) { + const periods = normalizeCustomPeriods(this.settlementRuleForm.customPeriods || []); + const row = periods[index]; + if (!row) return; + row.endDay = value || ''; + this.settlementRuleForm.customPeriods = periods; + }, + addCustomPeriodRow() { + const periods = normalizeCustomPeriods(this.settlementRuleForm.customPeriods || []); + periods.push({ startDay: '', endDay: '' }); + this.settlementRuleForm.customPeriods = periods; + }, + removeCustomPeriodRow(index) { + if (index <= 0) return; + const periods = [...(this.settlementRuleForm.customPeriods || [])]; + periods.splice(index, 1); + this.settlementRuleForm.customPeriods = normalizeCustomPeriods(periods); + }, + validateCustomPeriods(periods = [], label = '') { + const rows = normalizeCustomPeriods(periods); + const prefix = label ? `${label}:` : ''; + if (!rows.length) { + this.$message.warning(`${prefix}请至少配置一段自定义周期`); + return false; + } + for (let index = 0; index < rows.length; index += 1) { + const row = rows[index]; + const startDay = Number(row.startDay); + const endDay = Number(row.endDay); + if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { + this.$message.warning(`${prefix}请选择第${index + 1}行运单区间开始日`); + return false; + } + if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { + this.$message.warning(`${prefix}请选择第${index + 1}行运单区间结束日`); + return false; + } + if (endDay < startDay) { + this.$message.warning(`${prefix}第${index + 1}行结束日不能早于开始日`); + return false; + } + if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { + this.$message.warning(`${prefix}自定义多周期区间必须连续,不允许重叠或存在日期缺口`); + return false; + } + } + return true; + }, clearSettlementRuleError(field) { if (!this.settlementRuleErrors[field]) return; this.settlementRuleErrors = { @@ -7632,8 +7792,8 @@ export default { return false; } if (this.isMonthlySettlement(rule) && this.isBillingFieldEmpty(rule.billCycleType)) { - this.settlementRuleErrors = { billCycleType: '请选择账单周期类型' }; - this.$message.warning(`${label}${label ? ':' : ''}请选择账单周期类型`); + this.settlementRuleErrors = { billCycleType: '请选择结算周期' }; + this.$message.warning(`${label}${label ? ':' : ''}请选择结算周期`); return false; } if (this.isFixedBillCutoffSettlement(rule)) { @@ -7649,6 +7809,9 @@ export default { return false; } } + if (this.isCustomMultiPeriodSettlement(rule)) { + return this.validateCustomPeriods(rule.customPeriods, label); + } if (this.isFixedCycleSettlement(rule)) { const cycleDays = Number(rule.cycleDays); if (!Number.isInteger(cycleDays) || cycleDays < 1 || cycleDays > 365) { @@ -11239,8 +11402,10 @@ export default { if (!this.canEditBillingLimit(row)) { continue; } - const key = row.billingElement || ''; - groups[key] = groups[key] || []; + const feeItem = String(row.feeItem || '').trim(); + const billingElement = String(row.billingElement || '').trim(); + const key = JSON.stringify([feeItem, billingElement]); + groups[key] = groups[key] || { feeItem, billingElement, ranges: [] }; const ranges = this.getBillingLimitRanges(row); for (const range of ranges) { const lower = Number(range.lowerLimit); @@ -11258,14 +11423,14 @@ export default { this.$message.warning('计费要素下限不能大于上限'); return false; } - groups[key].push({ lower, upper }); + groups[key].ranges.push({ lower, upper }); } } return this.validateBillingLimitRangeGroups(groups); }, validateBillingLimitRangeGroups(groups) { const precision = 0.000001; - for (const [billingElement, ranges] of Object.entries(groups)) { + for (const { feeItem, billingElement, ranges } of Object.values(groups)) { if (ranges.length <= 1) continue; const sortedRanges = [...ranges].sort((prev, next) => { if (prev.lower !== next.lower) return prev.lower - next.lower; @@ -11275,11 +11440,13 @@ export default { const prevRange = sortedRanges[index - 1]; const currentRange = sortedRanges[index]; if (currentRange.lower < prevRange.upper - precision) { - this.$message.warning(`${billingElement}的计费要素区间不能重叠`); + this.$message.warning(`费用项“${feeItem}”${billingElement}的计费要素区间不能重叠`); return false; } if (currentRange.lower > prevRange.upper + precision) { - this.$message.warning(`${billingElement}的计费要素区间必须连续,不能存在间隙`); + this.$message.warning( + `费用项“${feeItem}”${billingElement}的计费要素区间必须连续,不能存在间隙` + ); return false; } } diff --git a/src/views/business/components/contract-attachment-section.vue b/src/views/business/components/contract-attachment-section.vue new file mode 100644 index 0000000..fdf4225 --- /dev/null +++ b/src/views/business/components/contract-attachment-section.vue @@ -0,0 +1,493 @@ + + + + + diff --git a/src/views/business/components/waybill-manage-page.vue b/src/views/business/components/waybill-manage-page.vue index 5a6d7ab..2dc76b5 100644 --- a/src/views/business/components/waybill-manage-page.vue +++ b/src/views/business/components/waybill-manage-page.vue @@ -4287,7 +4287,7 @@ export default { if (!row) return '-'; if (prop === 'businessStatus') return this.displayStatus(row, prop); if (prop === 'feeGenerationMode') { - return row[prop] === 'manual' ? '手动生成' : '系统生成'; + return row[prop] === 'manual' ? '账单导入生成' : '系统生成'; } const column = this.findColumn(this.option.column, prop); if (column?.formatter) return column.formatter(row, {}, row[prop]); diff --git a/src/views/business/contract-manage-change.vue b/src/views/business/contract-manage-change.vue index c654141..71071ff 100644 --- a/src/views/business/contract-manage-change.vue +++ b/src/views/business/contract-manage-change.vue @@ -25,6 +25,10 @@ + + + + @@ -42,7 +46,7 @@
计费信息
添加
- 系统生成手动生成 + 系统生成账单导入生成
@@ -65,10 +69,17 @@ - + + + + + + + +
@@ -144,15 +155,27 @@ const normalizeOptionalPositiveInteger = value => { const number = Number(value); return Number.isInteger(number) && number > 0 ? number : null; }; +const normalizeOptionalAmount = value => { + if (value === undefined || value === null || value === '') return null; + const number = Number(value); + return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : null; +}; + + +const normalizeCustomPeriods = (periods = []) => + (periods || []).map(item => ({ + startDay: Number(item?.startDay) > 0 ? Number(item.startDay) : '', + endDay: Number(item?.endDay) > 0 ? Number(item.endDay) : '', + })); export default { components: { BillingPlanEditor, ElImageViewer, OpenFileViewer }, - data() { return { form: {}, period: [], plans: [], attachments: [], contractFiles: [], contractFileRows: [], selectedContractFiles: [], paymentRatioRows: [], settlementConfigTab: 'pre', changeMaterials: [], imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { zoom: true, rotate: true, download: true, fullscreen: true }, feeGenerationMode: 'system', settlementRule: { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: '' }, preSettlementConfig: {}, formalSettlementConfig: {}, settlementTypeOptions: ['月结','日结','周结','半月结','固定天数周期结算'], billCycleTypeOptions: ['固定截单日','自然月'], billCutoffDayOptions: Array.from({ length: 31 }, (_, index) => ({ label: `${index + 1}日`, value: index + 1 })), cycleDayOptions: [7,15,30,60].map(value => ({ label: `${value}天`, value })), planDialogVisible: false, planEditor: { planName: '', defaultPlan: false, remark: '', rules: [] }, planEditorIndex: -1, billingElements: ['按重量','按体积','按车辆','按里程','按吨·公里','固定金额(整单一口价)','按数量'], attachmentFileTypes: ['pdf','bmp','jpeg','png','jpg','doc','docx','ppt','pptx','xlsx','xls','eml','msg','zip'], rules: { contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }], changeReason: [{ required: true, message: '请输入变更原因', trigger: 'blur' }] } }; }, - computed: { showSettlementBillCycleType() { return this.settlementRule.settlementType === '月结'; }, showSettlementBillCutoffDay() { return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '固定截单日'; }, showSettlementCycleDays() { return this.settlementRule.settlementType === '固定天数周期结算'; } }, + data() { return { form: {}, period: [], plans: [], attachments: [], contractFiles: [], contractFileRows: [], selectedContractFiles: [], paymentRatioRows: [], settlementConfigTab: 'pre', changeMaterials: [], imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { zoom: true, rotate: true, download: true, fullscreen: true }, feeGenerationMode: 'system', settlementRule: { autoGenerate: '', billStartDate: '', settlementType: '', billCycleType: '', billCutoffDay: '', cycleDays: '', customPeriods: [] }, preSettlementConfig: {}, formalSettlementConfig: {}, settlementTypeOptions: ['月结','日结','周结','半月结','固定天数周期结算'], billCycleTypeOptions: ['固定截单日','自然月','自定义多周期'], billCutoffDayOptions: Array.from({ length: 31 }, (_, index) => ({ label: `${index + 1}日`, value: index + 1 })), cycleDayOptions: [7,15,30,60].map(value => ({ label: `${value}天`, value })), planDialogVisible: false, planEditor: { planName: '', defaultPlan: false, remark: '', rules: [] }, planEditorIndex: -1, billingElements: ['按重量','按体积','按车辆','按里程','按吨·公里','固定金额(整单一口价)','按数量'], attachmentFileTypes: ['pdf','bmp','jpeg','png','jpg','doc','docx','ppt','pptx','xlsx','xls','eml','msg','zip'], rules: { contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }], changeReason: [{ required: true, message: '请输入变更原因', trigger: 'blur' }] } }; }, + computed: { showSettlementBillCycleType() { return this.settlementRule.settlementType === '月结'; }, showSettlementBillCutoffDay() { return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '固定截单日'; }, showSettlementCustomPeriods() { return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '自定义多周期'; }, showSettlementCycleDays() { return this.settlementRule.settlementType === '固定天数周期结算'; } }, mounted() { this.load(); }, watch: { settlementConfigTab(tab, oldTab) { if (tab === oldTab) return; if (oldTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; this.settlementRule = { ...(tab === 'pre' ? this.preSettlementConfig : this.formalSettlementConfig) }; } }, methods: { - async load() { const id = this.$route.query.id; if (!id) return; const res = await api.getDetail(id); const data = res.data?.data || res.data || {}; this.form = { ...data, changeContent: '', changeReason: '', changeAttachmentsJson: '', copyCount: normalizeOptionalPositiveInteger(data.copyCount), paymentDays: normalizeOptionalPositiveInteger(data.paymentDays), changeType: '合同信息变更' }; this.changeMaterials = []; this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : []; this.plans = this.parse(data.billingPlanJson); this.attachments = this.mergeChangeAttachments(this.parse(data.attachmentsJson), this.parse(data.changeAttachmentsJson)); this.contractFileRows = this.parse(data.contractFileJson); const rules = this.parseObject(data.settlementRuleJson); const pre = this.parseObject(data.preSettlementConfigJson); const formal = this.parseObject(data.formalSettlementConfigJson); const legacy = Object.keys(rules).some(key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)) ? rules : {}; this.preSettlementConfig = rules.preSettlementConfig || (Object.keys(pre).length ? pre : legacy); this.formalSettlementConfig = rules.formalSettlementConfig || (Object.keys(formal).length ? formal : legacy); this.settlementRule = { ...this.preSettlementConfig }; this.feeGenerationMode = data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system'); this.paymentRatioRows = this.parse(data.paymentRatioJson); }, + async load() { const id = this.$route.query.id; if (!id) return; const res = await api.getDetail(id); const data = res.data?.data || res.data || {}; this.form = { ...data, changeContent: '', changeReason: '', changeAttachmentsJson: '', copyCount: normalizeOptionalPositiveInteger(data.copyCount), paymentDays: normalizeOptionalPositiveInteger(data.paymentDays), contractAmount: normalizeOptionalAmount(data.contractAmount), templateFlag: data.templateFlag ?? 0, electronicSealFlag: data.electronicSealFlag ?? 0, changeType: '合同信息变更' }; this.changeMaterials = []; this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : []; this.plans = this.parse(data.billingPlanJson); this.attachments = this.mergeChangeAttachments(this.parse(data.attachmentsJson), this.parse(data.changeAttachmentsJson)); this.contractFileRows = this.parse(data.contractFileJson); const rules = this.parseObject(data.settlementRuleJson); const pre = this.parseObject(data.preSettlementConfigJson); const formal = this.parseObject(data.formalSettlementConfigJson); const legacy = Object.keys(rules).some(key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)) ? rules : {}; const normalizeRule = rule => { const next = { autoGenerate: '', billStartDate: '', settlementType: '', billCycleType: '', billCutoffDay: '', cycleDays: '', customPeriods: [], ...(rule || {}) }; if (next.settlementType === '月结' && next.billCycleType === '自定义多周期') next.customPeriods = normalizeCustomPeriods(Array.isArray(next.customPeriods) ? next.customPeriods : []); else next.customPeriods = []; return next; }; this.preSettlementConfig = normalizeRule(rules.preSettlementConfig || (Object.keys(pre).length ? pre : legacy)); this.formalSettlementConfig = normalizeRule(rules.formalSettlementConfig || (Object.keys(formal).length ? formal : legacy)); this.settlementRule = { ...this.preSettlementConfig }; this.feeGenerationMode = data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system'); this.paymentRatioRows = this.parse(data.paymentRatioJson); }, parse(value) { try { const result = JSON.parse(value || '[]'); return Array.isArray(result) ? result : []; } catch { return []; } }, // 审核通过后上一次的变更材料归集到「其它附件」,按文件地址/文件名去重,避免重复展示 mergeChangeAttachments(attachments = [], changeAttachments = []) { @@ -163,14 +186,14 @@ export default { .map(item => ({ ...item, size: /^\d+$/.test(String(item.size ?? '')) ? this.formatFileSize(item.size) : item.size })); return [...attachments, ...merged]; }, - parseObject(value) { try { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15, ...(JSON.parse(value || '{}') || {}) }; } catch { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15 }; } }, + parseObject(value) { try { return { autoGenerate: '', billStartDate: '', settlementType: '', billCycleType: '', billCutoffDay: '', cycleDays: '', customPeriods: [], ...(JSON.parse(value || '{}') || {}) }; } catch { return { autoGenerate: '', billStartDate: '', settlementType: '', billCycleType: '', billCutoffDay: '', cycleDays: '', customPeriods: [] }; } }, positiveIntegerInput(prop, value) { this.form[prop] = String(value ?? '').replace(/\D/g, '').replace(/^0+/, ''); }, addPlan() { this.planEditorIndex = -1; this.planEditor = { planName: `计费方案${this.plans.length + 1}`, defaultPlan: !this.plans.length, remark: '', rules: [{}] }; this.planDialogVisible = true; }, editPlan(row, index) { this.planEditorIndex = index; this.planEditor = JSON.parse(JSON.stringify(row)); this.planDialogVisible = true; }, toggleDefaultPlan(value) { if (value) this.plans.forEach(item => { item.defaultPlan = false; }); }, savePlan(value, index) { if (index < 0) this.plans.push(value); else this.plans.splice(index, 1, value); if (value.defaultPlan) this.plans.forEach((item, current) => { if (current !== (index < 0 ? this.plans.length - 1 : index)) item.defaultPlan = false; }); }, - handleSettlementTypeChange(value) { if (value !== '月结') { this.settlementRule.billCycleType = ''; this.settlementRule.billCutoffDay = ''; } else if (!this.settlementRule.billCycleType) this.settlementRule.billCycleType = '固定截单日'; if (value !== '固定天数周期结算') this.settlementRule.cycleDays = ''; }, - handleCycleTypeChange(value) { if (value === '固定截单日' && !this.settlementRule.billCutoffDay) this.settlementRule.billCutoffDay = 25; if (value !== '固定截单日') this.settlementRule.billCutoffDay = ''; }, + handleSettlementTypeChange(value) { if (value !== '月结') { this.settlementRule.billCycleType = ''; this.settlementRule.billCutoffDay = ''; this.settlementRule.customPeriods = []; } else if (this.settlementRule.billCycleType === '自定义多周期') this.settlementRule.customPeriods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); else this.settlementRule.customPeriods = []; if (value !== '固定天数周期结算') this.settlementRule.cycleDays = ''; }, + handleCycleTypeChange(value) { if (value !== '固定截单日') this.settlementRule.billCutoffDay = ''; if (value === '自定义多周期') this.settlementRule.customPeriods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); else this.settlementRule.customPeriods = []; }, currentUploadUserName() { const userInfo = this.$store.getters.userInfo || {}; return userInfo.realName || userInfo.userName || ''; @@ -209,7 +232,14 @@ export default { handleContractFileBatchDownload() { (this.selectedContractFiles.length ? this.selectedContractFiles : this.contractFileRows).forEach(row => { if (row.url) window.open(row.url, '_blank'); }); }, formatFileSize(value) { const size = Number(value || 0); return size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(2)}MB` : `${Math.max(1, Math.ceil(size / 1024))}KB`; }, addPaymentRatioRow() { this.paymentRatioRows.push({ paymentTerm: `第${this.paymentRatioRows.length + 1}笔`, ratioLimit: '', remark: '' }); }, - async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); this.$message.success('变更已提交'); this.$router.back(); }, + customPeriodEndDayOptions(row) { const startDay = Number(row?.startDay); if (!Number.isFinite(startDay) || startDay < 1) return this.billCutoffDayOptions; return Array.from({ length: 31 - startDay + 1 }, (_, index) => ({ label: `${startDay + index}日`, value: startDay + index })); }, + handleCustomPeriodStartDayChange(index, value) { const periods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); const row = periods[index]; if (!row) return; row.startDay = value || ''; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (Number.isFinite(startDay) && Number.isFinite(endDay) && (endDay < startDay || endDay > 31)) row.endDay = ''; this.settlementRule.customPeriods = periods; }, + handleCustomPeriodEndDayChange(index, value) { const periods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); const row = periods[index]; if (!row) return; row.endDay = value || ''; this.settlementRule.customPeriods = periods; }, + addCustomPeriodRow() { const periods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); periods.push({ startDay: '', endDay: '' }); this.settlementRule.customPeriods = periods; }, + removeCustomPeriodRow(index) { if (index <= 0) return; const periods = [...(this.settlementRule.customPeriods || [])]; periods.splice(index, 1); this.settlementRule.customPeriods = normalizeCustomPeriods(periods); }, + validateCustomPeriods(periods = [], label) { const rows = normalizeCustomPeriods(periods); if (!rows.length) { this.$message.warning(`${label}:请至少配置一段自定义周期`); return false; } for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间开始日`); return false; } if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间结束日`); return false; } if (endDay < startDay) { this.$message.warning(`${label}:第${index + 1}行结束日不能早于开始日`); return false; } if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { this.$message.warning(`${label}:自定义多周期区间必须连续,不允许重叠或存在日期缺口`); return false; } } return true; }, + validateSettlementRule(rule, label) { if (Number(rule.autoGenerate) !== 1) return true; if (!rule.billStartDate || !rule.settlementType) { this.$message.warning(`${label}:请完整填写账单起始日期和结算类型`); return false; } if (rule.settlementType === '月结' && !rule.billCycleType) { this.$message.warning(`${label}:请选择结算周期`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '固定截单日' && !rule.billCutoffDay) { this.$message.warning(`${label}:请选择账单截单日`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') return this.validateCustomPeriods(rule.customPeriods, label); if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { this.$message.warning(`${label}:请选择周期天数`); return false; } return true; }, + async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); this.$message.success('变更已提交'); this.$router.back(); }, }, }; diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index c8fc9fd..6d0f968 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -64,6 +64,12 @@ + + - -
- - - 搜索 - -
-
- -
-
-
- {{ contactMapStatus }} - - 行政区划:{{ contactMapSelected.regionName }} - -
- -
- item !== undefined && item !== null && item !== '') + : []; + const deptId = ids.length ? String(ids[ids.length - 1]) : ''; + this.contactForm.branchName = deptId ? this.findDeptLabel(deptId) : ''; + this.$refs.contactForm?.validateField('branchName'); + }, + }, ids() { let ids = []; this.selectionList.forEach(ele => { @@ -2650,11 +2621,9 @@ export default { contactName: '', contactPhone: '', email: '', + regionPath: [], regionName: '', - regionCode: '', detailAddress: '', - longitude: '', - latitude: '', contactAddress: '', branchName: '', positionName: '', @@ -2948,6 +2917,67 @@ export default { } return ''; }, + findDeptPathByName(name, tree = this.deptTree, parents = []) { + const target = String(name || '').trim(); + if (!target) return []; + for (const node of tree || []) { + const path = [...parents, String(node.value)]; + if (String(node.label || '').trim() === target) return path; + const matched = this.findDeptPathByName(name, node.children || [], path); + if (matched.length) return matched; + } + return []; + }, + getCurrentOrganizationName() { + const currentDeptId = String(this.userInfo.deptId || this.userInfo.dept_id || '') + .split(',') + .map(item => item.trim()) + .filter(Boolean)[0]; + if (currentDeptId) { + const label = this.findDeptLabel(currentDeptId); + if (label) return label; + } + return this.userInfo.deptName || this.userInfo.dept_name || ''; + }, + findRegionPathByName(options = [], regionName = '', parents = []) { + const target = String(regionName || '').replace(/\s+/g, ''); + if (!target) return []; + for (const item of options || []) { + const nextParents = [...parents, item]; + const nextName = nextParents.map(region => region.title || region.name || '').join(''); + if (nextName === target) { + return nextParents.map(region => region.id); + } + const childPath = this.findRegionPathByName(item.children, target, nextParents); + if (childPath.length) return childPath; + } + return []; + }, + getContactRegionLabels(value) { + const nodes = this.$refs.contactRegionCascader?.getCheckedNodes?.() || []; + if (nodes.length && nodes[0].pathLabels?.length) { + return nodes[0].pathLabels; + } + return (value || []) + .map(code => this.findRegionOption(this.regionOptions, code)?.title) + .filter(Boolean); + }, + handleContactRegionChange(value) { + if (!value || !value.length) { + this.contactForm.regionName = ''; + return; + } + this.$nextTick(() => { + this.contactForm.regionName = this.getContactRegionLabels(value).join(''); + }); + }, + syncContactRegionPath() { + if (!this.contactForm.regionName || this.contactForm.regionPath?.length) return; + const path = this.findRegionPathByName(this.regionOptions, this.contactForm.regionName); + if (path.length) { + this.contactForm.regionPath = path; + } + }, renderDeptSearch(scope) { // 搜索区所属组织:单选级联(与新增/编辑表单一致) // 级联返回的是根到选中节点的 ID 路径数组,提交时取末级 ID 转部门名称 @@ -3071,11 +3101,20 @@ export default { normalizeContact(contact = {}) { const contactAddress = contact.contactAddress || contact.address || this.formatContactAddress(contact); + const regionPath = Array.isArray(contact.regionPath) + ? contact.regionPath + : this.findRegionPathByName(this.regionOptions, contact.regionName); return { ...this.emptyContact(), ...contact, + regionPath, contactAddress, - branchName: contact.branchName || contact.deptName || this.archiveForm.deptName || '', + branchName: + contact.branchName || + contact.deptName || + this.getCurrentOrganizationName() || + this.archiveForm.deptName || + '', }; }, openContact(row, index = -1) { @@ -3084,9 +3123,10 @@ export default { ? this.normalizeContact(row) : { ...this.emptyContact(), - branchName: this.archiveForm.deptName || '', + branchName: this.getCurrentOrganizationName() || this.archiveForm.deptName || '', }; this.contactBox = true; + this.$nextTick(() => this.syncContactRegionPath()); }, resetContact() { this.contactIndex = -1; @@ -3100,6 +3140,7 @@ export default { ...this.contactForm, contactAddress: this.formatContactAddress(this.contactForm), }); + delete contact.regionPath; if (this.contactIndex > -1) { this.archiveForm.contacts.splice(this.contactIndex, 1, contact); } else { @@ -3121,279 +3162,6 @@ export default { }) .catch(() => {}); }, - handleContactMapPick() { - this.contactMapTarget = 'contact'; - this.contactMapKeyword = this.contactForm.detailAddress || this.contactForm.regionName || ''; - this.contactMapSelected = this.buildContactMapSelection({ - lng: this.contactForm.longitude, - lat: this.contactForm.latitude, - address: this.contactForm.detailAddress, - regionName: this.contactForm.regionName, - regionCode: this.contactForm.regionCode, - }); - this.contactMapStatus = this.contactMapSelected.longitude - ? '已加载当前选点,可重新选点' - : '可搜索地址或点击地图选点'; - this.contactMapBox = true; - }, - loadAmap() { - if (window.AMap && window.AMap.Map) { - return Promise.resolve(); - } - if (!amapLoader) { - amapLoader = new Promise((resolve, reject) => { - window._AMapSecurityConfig = { - securityJsCode: AMAP_SECURITY_CODE, - }; - const script = document.createElement('script'); - script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`; - script.async = true; - script.onload = resolve; - script.onerror = () => reject(new Error('高德地图组件加载失败')); - document.body.appendChild(script); - }); - } - return amapLoader; - }, - initContactAmap() { - this.loadAmap() - .then(() => { - this.$nextTick(() => { - if (!this.contactAmap) { - const center = this.toLngLat( - this.contactMapSelected.longitude || 116.40769, - this.contactMapSelected.latitude || 39.89945 - ); - this.contactAmap = new window.AMap.Map(this.$refs.contactAmap, { - center, - zoom: this.contactMapSelected.longitude ? 14 : 11, - }); - window.AMap.plugin(['AMap.ToolBar', 'AMap.Geocoder'], () => { - this.contactAmap.addControl(new window.AMap.ToolBar()); - if (!this.contactAmapGeocoder) { - this.contactAmapGeocoder = new window.AMap.Geocoder(); - } - }); - this.contactAmap.on('click', event => this.pickContactMapPoint(event.lnglat)); - } else if (typeof this.contactAmap.resize === 'function') { - this.contactAmap.resize(); - } - if (this.contactMapSelected.longitude) { - this.renderContactMapMarker( - this.toLngLat(this.contactMapSelected.longitude, this.contactMapSelected.latitude) - ); - } else { - this.clearContactMapMarker(); - } - }); - }) - .catch(() => { - this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗'); - this.contactMapBox = false; - }); - }, - searchContactMapKeyword() { - const keyword = String(this.contactMapKeyword || '').trim(); - if (!keyword) { - this.$message.warning('请输入地址关键词'); - return; - } - this.loadAmap() - .then(() => { - this.contactMapLoading = true; - this.ensureContactAmapGeocoder() - .then(() => this.runAmapGeocode('location', keyword)) - .then(result => { - this.contactMapSearchResults = (result.geocodes || []).map((item, index) => ({ - id: item.id || index, - name: item.formattedAddress || keyword, - address: item.formattedAddress || keyword, - location: item.location, - })); - const point = this.resolveMapPoint(result); - if (!point) { - this.contactMapStatus = '未找到匹配地址'; - this.$message.warning('地图搜索无匹配地址'); - return; - } - this.pickContactMapPoint(point, keyword); - }) - .catch(() => { - this.contactMapStatus = '地图搜索失败'; - this.$message.error('地图搜索失败,请稍后重试'); - }) - .finally(() => { - this.contactMapLoading = false; - }); - }) - .catch(() => { - this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗'); - }); - }, - selectContactMapSearchResult(item) { - if (item && item.location) { - this.pickContactMapPoint(item.location, item.address || item.name || ''); - } - }, - pickContactMapPoint(lnglat, keyword) { - const longitude = this.getPointLng(lnglat); - const latitude = this.getPointLat(lnglat); - if (longitude === undefined || latitude === undefined) { - this.$message.warning('选点坐标无效'); - return; - } - const point = this.toLngLat(longitude, latitude); - this.renderContactMapMarker(point); - this.contactMapSelected = this.buildContactMapSelection({ - lng: longitude, - lat: latitude, - address: keyword, - }); - this.contactMapStatus = '正在反查地址...'; - this.ensureContactAmapGeocoder() - .then(() => this.runAmapGeocode('address', point)) - .then(result => { - const address = this.resolveMapAddress(result); - this.contactMapSelected = { - ...this.contactMapSelected, - detailAddress: address.detailAddress || this.contactMapSelected.detailAddress, - regionName: address.regionName || this.contactMapSelected.regionName, - regionCode: address.regionCode || this.contactMapSelected.regionCode, - }; - this.contactMapKeyword = this.contactMapSelected.detailAddress || this.contactMapKeyword; - this.contactMapStatus = this.contactMapSelected.detailAddress || '已选点,可确认回填'; - }) - .catch(() => { - this.contactMapStatus = '反查地址失败'; - this.$message.error('反查地址失败,请重新选点'); - }); - }, - renderContactMapMarker(point) { - if (!this.contactAmap || !window.AMap) return; - this.clearContactMapMarker(); - this.contactAmapMarker = new window.AMap.Marker({ - position: point, - }); - this.contactAmapMarker.setMap(this.contactAmap); - this.contactAmap.setCenter(point); - }, - clearContactMapMarker() { - if (this.contactAmapMarker) { - this.contactAmapMarker.setMap(null); - this.contactAmapMarker = null; - } - }, - confirmContactMapPick() { - if (!this.contactMapSelected.longitude) { - this.$message.warning('请先搜索或点击地图完成选点'); - return; - } - this.contactForm.detailAddress = - this.contactMapSelected.detailAddress || this.contactForm.detailAddress; - this.contactForm.regionName = - this.contactMapSelected.regionName || this.contactForm.regionName; - this.contactForm.regionCode = - this.contactMapSelected.regionCode || this.contactForm.regionCode; - this.contactForm.longitude = this.contactMapSelected.longitude; - this.contactForm.latitude = this.contactMapSelected.latitude; - this.$refs.contactForm?.validateField('detailAddress'); - this.contactMapBox = false; - }, - buildContactMapSelection({ lng, lat, address, regionName, regionCode }) { - const longitude = this.formatCoordinate(lng); - const latitude = this.formatCoordinate(lat); - return { - longitude, - latitude, - detailAddress: address || '', - regionName: regionName || '', - regionCode: regionCode || '', - }; - }, - formatCoordinate(value) { - if (value === undefined || value === null || value === '') { - return ''; - } - const numberValue = Number(value); - return Number.isFinite(numberValue) ? numberValue.toFixed(6) : ''; - }, - toLngLat(lng, lat) { - return new window.AMap.LngLat(Number(lng), Number(lat)); - }, - getPointLng(point) { - if (!point) return undefined; - if (typeof point.getLng === 'function') return point.getLng(); - return point.lng ?? point.lon; - }, - getPointLat(point) { - if (!point) return undefined; - if (typeof point.getLat === 'function') return point.getLat(); - return point.lat; - }, - ensureContactAmapGeocoder() { - if (this.contactAmapGeocoder) { - return Promise.resolve(this.contactAmapGeocoder); - } - return new Promise((resolve, reject) => { - if (!window.AMap || !window.AMap.plugin) { - reject(new Error('高德地图组件未就绪')); - return; - } - window.AMap.plugin(['AMap.Geocoder'], () => { - try { - this.contactAmapGeocoder = new window.AMap.Geocoder(); - resolve(this.contactAmapGeocoder); - } catch (error) { - reject(error); - } - }); - }); - }, - runAmapGeocode(action, input) { - return new Promise((resolve, reject) => { - const timer = window.setTimeout(() => { - reject(new Error('高德地图请求超时')); - }, 10000); - const done = (status, result) => { - window.clearTimeout(timer); - if (status === 'complete' && result) { - resolve(result); - return; - } - reject(new Error('高德地图请求失败')); - }; - try { - if (action === 'location') { - this.contactAmapGeocoder.getLocation(input, done); - } else { - this.contactAmapGeocoder.getAddress(input, done); - } - } catch (error) { - window.clearTimeout(timer); - reject(error); - } - }); - }, - resolveMapPoint(result) { - if (!result) return null; - const geocode = Array.isArray(result.geocodes) ? result.geocodes[0] : null; - if (geocode && geocode.location) return geocode.location; - if (result.location) return result.location; - if (result.lnglat) return result.lnglat; - if (result.getLng || result.lng || result.lon) return result; - return null; - }, - resolveMapAddress(result = {}) { - const regeocode = result.regeocode || {}; - const component = regeocode.addressComponent || {}; - const city = Array.isArray(component.city) ? '' : component.city; - const regionName = [component.province, city, component.district].filter(Boolean).join(''); - return { - detailAddress: regeocode.formattedAddress || '', - regionName, - regionCode: component.adcode || '', - }; - }, normalizeReceipt(receipt = {}) { return { ...this.emptyReceiptAccount(), @@ -4020,7 +3788,12 @@ export default { contacts: (detail.contacts || []).map(item => this.normalizeContact({ ...item, - branchName: item.branchName || item.deptName || detail.deptName || '', + branchName: + item.branchName || + item.deptName || + this.getCurrentOrganizationName() || + detail.deptName || + '', }) ), receiptAccounts: (detail.receiptAccounts || []).map(item => this.normalizeReceipt(item)), @@ -4226,12 +3999,14 @@ export default { delete archive.registeredRegionPath; archive.qualificationAttachments = this.stringifyAttachments(this.qualificationFiles); archive.contacts = (archive.contacts || []) - .map(item => - this.normalizeContact({ + .map(item => { + const contact = this.normalizeContact({ ...item, contactAddress: this.formatContactAddress(item), - }) - ) + }); + delete contact.regionPath; + return contact; + }) .filter(item => item.contactName || item.contactPhone); archive.receiptAccounts = (archive.receiptAccounts || []) .map(item => this.normalizeReceipt(item)) @@ -5048,6 +4823,12 @@ export default { :deep(.el-table td .cell) { white-space: nowrap; } + + // 表头含问号图标,避免被压缩成省略号 + :deep(.el-table th .cell:has(.fund-use-risk-header)) { + overflow: visible; + text-overflow: clip; + } } .archive-tag--deep-blue { @@ -5073,13 +4854,15 @@ export default { .fund-use-risk-header { display: inline-flex; align-items: center; + gap: 4px; + white-space: nowrap; } .fund-use-risk-help { - margin-left: 4px; color: #a8abb2; cursor: help; font-size: 14px; + flex: none; } .archive-form { @@ -5417,23 +5200,9 @@ export default { } } -.contact-form__address { - display: flex; - gap: 10px; - width: 100%; - - .el-input { - flex: 1; - min-width: 0; - } - - .el-input:first-child { - flex: 0 0 38%; - } -} - .contact-form__compact-field { - :deep(.el-input) { + :deep(.el-input), + :deep(.el-cascader) { width: 60%; } } @@ -5463,32 +5232,6 @@ export default { gap: 12px; } -.contact-form__map-toolbar { - display: flex; - gap: 10px; - margin-bottom: 12px; - - .el-input { - flex: 1; - } -} - -.contact-form__map { - width: 100%; - height: 460px; - border: 1px solid #dcdfe6; -} - -.contact-form__map-info { - display: flex; - justify-content: space-between; - gap: 16px; - min-height: 22px; - margin-top: 10px; - color: #606266; - font-size: 13px; -} - .score-detail-table { width: 100%; margin-top: 12px; From c1c4fa18c915b1e4191c760e9d088780b14a4bc3 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 16 Sep 2026 06:30:30 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E8=B0=83=E6=95=B4=20iam?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 2 + src/api/business/loading-manage.js | 9 + src/api/system/user.js | 7 + src/docker/Dockerfile | 1 + src/error.js | 4 + src/main.js | 2 + src/option/business/shipping-template.js | 34 +- src/option/business/transport-plan.js | 10 +- src/page/index/layout.vue | 13 +- src/permission.js | 23 +- src/router/avue-router.js | 23 +- src/router/tab.js | 2 +- src/store/getters.js | 7 +- src/utils/chunk-reload.js | 144 ++++- .../components/master-order-detail.vue | 131 +++- .../components/master-order-dispatch.vue | 195 +++--- .../components/shipping-template-page.vue | 108 +++- .../components/transport-plan-page.vue | 400 ++++++++---- .../components/waybill-manage-page.vue | 15 +- src/views/business/loading-manage.vue | 43 +- src/views/business/master-order.vue | 326 ++++++---- .../business/transport-plan-dispatch.vue | 609 +++++++++++------- src/views/business/voucher-manage.vue | 32 +- .../settlement/receivable-payable-detail.vue | 12 - src/views/system/user.vue | 29 + src/views/vehicle/customer-archive.vue | 2 +- 26 files changed, 1517 insertions(+), 666 deletions(-) diff --git a/index.html b/index.html index 5a4c44b..eae241d 100644 --- a/index.html +++ b/index.html @@ -13,6 +13,8 @@ + + diff --git a/src/api/business/loading-manage.js b/src/api/business/loading-manage.js index bb6be5c..5d116bc 100644 --- a/src/api/business/loading-manage.js +++ b/src/api/business/loading-manage.js @@ -79,6 +79,15 @@ export const cancel = id => }, }); +export const start = id => + request({ + url: `${baseUrl}/start`, + method: 'post', + params: { + id, + }, + }); + export const complete = id => request({ url: `${baseUrl}/complete`, diff --git a/src/api/system/user.js b/src/api/system/user.js index 1334f0c..70d5b20 100644 --- a/src/api/system/user.js +++ b/src/api/system/user.js @@ -31,6 +31,13 @@ export const add = row => { }); }; +export const syncIamAccounts = () => { + return request({ + url: '/blade-system/user/sync-iam-accounts', + method: 'post', + }); +}; + export const update = row => { return request({ url: '/blade-system/user/update', diff --git a/src/docker/Dockerfile b/src/docker/Dockerfile index 88425be..88c648f 100644 --- a/src/docker/Dockerfile +++ b/src/docker/Dockerfile @@ -1,6 +1,7 @@ FROM nginx VOLUME /tmp ENV LANG en_US.UTF-8 +ADD ./src/docker/nginx.conf /etc/nginx/conf.d/default.conf ADD ./dist/ /usr/share/nginx/html/ EXPOSE 80 EXPOSE 443 \ No newline at end of file diff --git a/src/error.js b/src/error.js index d2b5611..1c507e5 100644 --- a/src/error.js +++ b/src/error.js @@ -1,8 +1,12 @@ import store from './store'; +import { isChunkLoadError, reloadForChunkError } from './utils/chunk-reload'; export default { install: app => { app.config.errorHandler = (err, vm, info) => { + if (isChunkLoadError(err) && reloadForChunkError()) { + return; + } store.commit('ADD_LOGS', { type: 'error', message: err.message, diff --git a/src/main.js b/src/main.js index 9e780e5..c53d2e6 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,5 @@ import { createApp } from 'vue'; +import { installChunkReload } from './utils/chunk-reload'; import website from './config/website'; import axios from './axios'; import router from './router/'; @@ -46,6 +47,7 @@ import sectionCard from './components/section-card/main.vue'; import mapSearchResults from './components/map-search-results/main.vue'; window.$crudCommon = crudCommon; +installChunkReload(); debug(); window.axios = axios; const app = createApp(App); diff --git a/src/option/business/shipping-template.js b/src/option/business/shipping-template.js index eb90940..ad2f735 100644 --- a/src/option/business/shipping-template.js +++ b/src/option/business/shipping-template.js @@ -55,8 +55,8 @@ export const config = { 'templateType', 'transportType', 'createUserName', - 'remark', 'updateTime', + 'remark', 'createTime', ], exportColumns: [ @@ -65,8 +65,8 @@ export const config = { { prop: 'templateType', label: '模板类型' }, { prop: 'transportType', label: '运输方式' }, { prop: 'createUserName', label: '创建人' }, - { prop: 'remark', label: '备注' }, { prop: 'updateTime', label: '更新时间' }, + { prop: 'remark', label: '备注' }, { prop: 'createTime', label: '创建时间' }, ], enableAllDept: false, @@ -82,12 +82,14 @@ export const config = { detailAttachmentDescriptionPlain: true, enableTransportPlanForm: true, enableShippingTemplateFreight: true, - editableRoadAddress: true, + editableRoadAddress: false, fixedTransportAddressType: true, enableTemplateCodePreview: true, attachmentTitle: '附件', defaultForm: { templateType: '运输计划', + transportType: '公路运输', + transportTypeName: '公路运输', }, transportFormRequiredFields: [ ['projectName', '项目'], @@ -119,15 +121,16 @@ export const option = { labelWidth: 0, }, { - label: '模板编号', - prop: 'templateCode', + label: '模板类型', + prop: 'templateType', + type: 'select', search: true, - searchOrder: 4, + searchOrder: 2, span: 8, order: 390, - minWidth: 170, - disabled: true, - placeholder: '系统自动生成', + dicData: templateTypeOptions, + minWidth: 120, + rules: selectRule('模板类型'), display: true, }, { @@ -142,16 +145,15 @@ export const option = { display: true, }, { - label: '模板类型', - prop: 'templateType', - type: 'select', + label: '模板编号', + prop: 'templateCode', search: true, - searchOrder: 2, + searchOrder: 4, span: 8, order: 370, - dicData: templateTypeOptions, - minWidth: 120, - rules: selectRule('模板类型'), + minWidth: 170, + disabled: true, + placeholder: '系统自动生成', display: true, }, { diff --git a/src/option/business/transport-plan.js b/src/option/business/transport-plan.js index 45b9154..5fb8150 100644 --- a/src/option/business/transport-plan.js +++ b/src/option/business/transport-plan.js @@ -209,12 +209,12 @@ export const config = { attachmentTitle: '附件', searchRangeMap: { planStartDateRange: ['planStartDateStart', 'planStartDateEnd'], - planEndDateRange: ['planEndDateStart', 'planEndDateEnd'], + createTimeRange: ['createTimeStart', 'createTimeEnd', '00:00:00', '23:59:59'], }, actions: ['copy', 'complete'], statusProp: 'businessStatus', statusTextProp: 'businessStatusName', - deleteStatus: ['draft', 'waiting_dispatch'], + deleteStatus: ['draft'], editStatus: ['draft', 'waiting_dispatch', 'dispatching'], detailButton: true, detailSections: [ @@ -341,6 +341,7 @@ export const option = { { label: '发货地址', prop: 'departureAddress', + slot: true, formslot: true, search: true, searchOrder: 7, @@ -352,6 +353,7 @@ export const option = { { label: '到货地址', prop: 'arrivalAddress', + slot: true, formslot: true, search: true, searchOrder: 6, @@ -577,8 +579,8 @@ export const option = { viewDisplay: false, }, { - label: '计划结束日期', - prop: 'planEndDateRange', + label: '创建时间', + prop: 'createTimeRange', type: 'date', format: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD', diff --git a/src/page/index/layout.vue b/src/page/index/layout.vue index 0439730..f0531f9 100644 --- a/src/page/index/layout.vue +++ b/src/page/index/layout.vue @@ -1,7 +1,18 @@ + + diff --git a/src/permission.js b/src/permission.js index 27b2028..9051363 100644 --- a/src/permission.js +++ b/src/permission.js @@ -3,6 +3,8 @@ import store from './store'; import { tabKeyOf } from '@/router/tab'; import { getToken } from '@/utils/auth'; import { + consumeReloadQuery, + hasReloadQuery, isChunkLoadError, reloadForChunkError, setPendingRoutePath, @@ -17,19 +19,20 @@ const lockPage = '/lock'; //锁屏页 router.onError(error => { if (!isChunkLoadError(error)) return; if (reloadForChunkError()) { - ElMessage.warning('页面资源加载失败,正在重新加载…'); - } -}); - -window.addEventListener('unhandledrejection', event => { - if (!isChunkLoadError(event.reason)) return; - if (reloadForChunkError()) { - event.preventDefault(); - ElMessage.warning('页面资源加载失败,正在重新加载…'); + ElMessage.warning('页面资源已更新,正在重新加载…'); } }); router.beforeEach((to, from, next) => { + if (hasReloadQuery(to.query)) { + next({ + path: to.path, + query: consumeReloadQuery(to.query), + hash: to.hash, + replace: true, + }); + return; + } setPendingRoutePath(to.fullPath); const meta = to.meta || {}; const isMenu = meta.menu === undefined ? to.query.menu : meta.menu; @@ -59,7 +62,7 @@ router.beforeEach((to, from, next) => { fullPath: tabKeyOf(to), params: to.params, query: to.query, - meta: meta, + meta: { ...meta, keepAlive: true }, }); } next(); diff --git a/src/router/avue-router.js b/src/router/avue-router.js index 7c0a38c..3785072 100644 --- a/src/router/avue-router.js +++ b/src/router/avue-router.js @@ -2,6 +2,7 @@ import website from '@/config/website'; import { getToken } from '@/utils/auth'; import store from '@/store'; import { generateIframePath, processUrlForQuery, isURL } from './router'; +import { wrapViewLoader } from '@/utils/chunk-reload'; const modules = import.meta.glob('../**/**/*.vue'); // 将多级路由扁平化为二级路由,支持 keep-alive 跨层级缓存 @@ -96,21 +97,23 @@ RouterPlugin.install = function (option = {}) { component: (() => { // 判断是否为首路由 if (first) { - return modules[ - option.store.getters.isMacOs || !website.setting.menu - ? '../page/index/layout.vue' - : '../page/index/index.vue' - ]; + return wrapViewLoader( + modules[ + option.store.getters.isMacOs || !website.setting.menu + ? '../page/index/layout.vue' + : '../page/index/index.vue' + ] + ); // 判断是否为多层路由 } else if (isChild && !first) { - return modules['../page/index/layout.vue']; + return wrapViewLoader(modules['../page/index/layout.vue']); // 判断是否为最终的页面视图 } else { let result = modules[`../${component}.vue`]; if (!result) { isComponent = false; } - return result; + return wrapViewLoader(result); } })(), name, @@ -127,7 +130,7 @@ RouterPlugin.install = function (option = {}) { if (first) { oMenu[propsDefault.path] = `${path}`; let componentPath = oMenu.component || component; - let result = modules[`../${componentPath}.vue`]; + let result = wrapViewLoader(modules[`../${componentPath}.vue`]); if (!result) { isComponent = false; } @@ -173,7 +176,7 @@ export const formatPath = (ele, first) => { const icon = ele[propsDefault.icon]; ele[propsDefault.icon] = icon || ''; ele.meta = { - keepAlive: ele.isOpen === 2, + keepAlive: true, }; const iframeComponent = 'components/iframe/main'; const iframeSrc = href => { @@ -215,7 +218,7 @@ export const formatPath = (ele, first) => { ele[propsDefault.children].forEach(child => { child.component = 'views' + child[propsDefault.path]; child.meta = { - keepAlive: child.isOpen === 2, + keepAlive: true, }; if (isURL(child[propsDefault.href])) { let href = child[propsDefault.href]; diff --git a/src/router/tab.js b/src/router/tab.js index f715c00..d904ad1 100644 --- a/src/router/tab.js +++ b/src/router/tab.js @@ -128,5 +128,5 @@ export function tabView(route, Component) { }; wrapperMap.set(tabKey, wrapper); } - return h(wrapper); + return wrapper; } diff --git a/src/store/getters.js b/src/store/getters.js index 3edda43..fb53825 100644 --- a/src/store/getters.js +++ b/src/store/getters.js @@ -16,11 +16,8 @@ const getters = { lockPasswd: state => state.common.lockPasswd, tagList: state => state.tags.tagList, tagsKeep: (state, getters) => { - return getters.tagList - .filter(ele => { - return (ele.meta || {}).keepAlive; - }) - .map(ele => ele.fullPath); + // 所有已打开标签均纳入 keep-alive,关闭标签后自动从白名单移除并释放实例 + return getters.tagList.map(ele => ele.fullPath).filter(Boolean); }, tagWel: state => state.tags.tagWel, token: state => state.user.token, diff --git a/src/utils/chunk-reload.js b/src/utils/chunk-reload.js index 2d42f54..3c30bb4 100644 --- a/src/utils/chunk-reload.js +++ b/src/utils/chunk-reload.js @@ -1,38 +1,168 @@ /** * 懒加载 chunk / CSS preload 失败后的整页恢复。 * 常见于:部署后旧 hash 失效、静态服务空闲断连、代理 CONNECTION_RESET。 + * + * 失败后必须整页刷新:旧入口里的 hashed 资源地址不会自行更新, + * 仅捕获路由错误而不刷新时,未打开过的页面会一直无法进入。 */ const RELOAD_FLAG = 'app:chunk-reload-ts'; -const RELOAD_COOLDOWN_MS = 10000; +const RELOAD_QUERY = '_chunkreload'; +const RELOAD_COOLDOWN_MS = 15000; const CHUNK_ERROR_RE = - /Failed to fetch dynamically imported module|Importing a module script failed|Unable to preload CSS|error loading dynamically imported module|Loading CSS chunk|Loading chunk .+ failed|ChunkLoadError/i; + /Failed to fetch dynamically imported module|Importing a module script failed|Unable to preload CSS|error loading dynamically imported module|Loading CSS chunk|Loading chunk .+ failed|ChunkLoadError|Unable to preload|error loading module|Load failed/i; /** 最近一次路由跳转目标,供 onError 时整页落到正确地址 */ let pendingFullPath = ''; +let installed = false; +let reloading = false; export function setPendingRoutePath(fullPath = '') { pendingFullPath = fullPath || ''; } +function collectErrorText(error, depth = 0) { + if (!error || depth > 3) return ''; + if (typeof error === 'string') return error; + const parts = [ + error.message, + error.msg, + error.name, + error.stack, + error.error && collectErrorText(error.error, depth + 1), + error.reason && collectErrorText(error.reason, depth + 1), + error.cause && collectErrorText(error.cause, depth + 1), + error.payload && collectErrorText(error.payload, depth + 1), + ]; + const target = error.target; + if (target && (target.src || target.href)) { + parts.push(target.src || target.href); + } + try { + parts.push(String(error)); + } catch (e) { + /* ignore */ + } + return parts.filter(Boolean).join(' '); +} + export function isChunkLoadError(error) { - if (!error) return false; - const message = error.message || String(error); - return CHUNK_ERROR_RE.test(message); + const text = collectErrorText(error); + if (!text) return false; + if (CHUNK_ERROR_RE.test(text)) return true; + return /Failed to fetch/i.test(text) && /\.m?js(\?|$|:)/i.test(text); +} + +export function hasReloadQuery(query = {}) { + return !!(query && query[RELOAD_QUERY] !== undefined); +} + +export function consumeReloadQuery(query = {}) { + if (!hasReloadQuery(query)) return query; + const next = { ...query }; + delete next[RELOAD_QUERY]; + return next; +} + +function buildReloadUrl(targetPath) { + const url = new URL(targetPath, window.location.origin); + url.searchParams.set(RELOAD_QUERY, String(Date.now())); + return url.pathname + url.search + url.hash; } /** * @returns {boolean} 是否已触发刷新(冷却期内返回 false,避免死循环) */ export function reloadForChunkError(targetPath) { + if (reloading) return false; const now = Date.now(); const last = Number(sessionStorage.getItem(RELOAD_FLAG) || 0); if (now - last < RELOAD_COOLDOWN_MS) { return false; } + reloading = true; sessionStorage.setItem(RELOAD_FLAG, String(now)); - const path = targetPath || pendingFullPath || window.location.pathname + window.location.search + window.location.hash; - window.location.assign(path); + const path = + targetPath || + pendingFullPath || + `${window.location.pathname}${window.location.search}${window.location.hash}`; + const dest = buildReloadUrl(path); + // cache: 'reload' 会回写 HTTP 缓存,降低 index.html 仍指向旧 hash 资源的概率 + fetch(`${window.location.pathname}?${RELOAD_QUERY}=${now}`, { + cache: 'reload', + credentials: 'same-origin', + headers: { + Pragma: 'no-cache', + 'Cache-Control': 'no-cache', + }, + }) + .catch(() => {}) + .finally(() => { + window.location.replace(dest); + }); return true; } + +export function wrapViewLoader(loader) { + if (typeof loader !== 'function') return loader; + return () => + Promise.resolve() + .then(() => loader()) + .catch(error => { + if (isChunkLoadError(error)) { + reloadForChunkError(); + } + throw error; + }); +} + +export function installChunkReload() { + if (installed || typeof window === 'undefined') return; + installed = true; + + window.addEventListener('vite:preloadError', event => { + if (reloadForChunkError()) { + event.preventDefault(); + } + }); + + window.addEventListener('unhandledrejection', event => { + if (!isChunkLoadError(event.reason)) return; + if (reloadForChunkError()) { + event.preventDefault(); + } + }); + + window.addEventListener( + 'error', + event => { + const el = event.target; + const isAssetNode = + el && + el !== window && + (el.tagName === 'SCRIPT' || + (el.tagName === 'LINK' && /modulepreload|stylesheet/i.test(el.rel || ''))); + if (isAssetNode || isChunkLoadError(event.error || event.message)) { + if (reloadForChunkError()) { + event.preventDefault(); + } + } + }, + true + ); + + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') checkDeployedAssets(); + }); +} + +function checkDeployedAssets() { + const el = document.querySelector('script[type="module"][src*="/assets/"]'); + if (!el || !el.src) return; + fetch(el.src, { method: 'HEAD', cache: 'no-store', credentials: 'same-origin' }) + .then(res => { + if (res.status === 404) reloadForChunkError(); + }) + .catch(() => {}); +} diff --git a/src/views/business/components/master-order-detail.vue b/src/views/business/components/master-order-detail.vue index 5a668c4..730fe09 100644 --- a/src/views/business/components/master-order-detail.vue +++ b/src/views/business/components/master-order-detail.vue @@ -1,8 +1,51 @@ -
- 暂无变更记录 -
@@ -2734,12 +2728,6 @@ export default { padding: 16px 0; } -.settlement-detail-page__empty { - padding: 24px 0; - color: #909399; - text-align: center; -} - .settlement-detail-page__dialog-form { padding: 16px 20px; background: #fff; diff --git a/src/views/system/user.vue b/src/views/system/user.vue index bb4c73d..1ed0669 100644 --- a/src/views/system/user.vue +++ b/src/views/system/user.vue @@ -56,6 +56,14 @@ @click="handleAudit" >审 核 + 同步人员 + { + this.iamSyncLoading = true; + return syncIamAccounts(); + }) + .then(res => { + const count = res?.data?.data ?? 0; + this.$message.success(`IAM人员同步完成,共处理${count}条`); + this.onLoad(this.page, this.query); + }) + .finally(() => { + this.iamSyncLoading = false; + }); + }, handleSetLeader(row) { const tip = row.isLeader === 1 ? '确定取消用户的主管职务?' : '确定设置用户为主管职务?'; const message = row.isLeader === 1 ? '取消主管成功!' : '设置主管成功!'; diff --git a/src/views/vehicle/customer-archive.vue b/src/views/vehicle/customer-archive.vue index 9c36d1a..1325abc 100644 --- a/src/views/vehicle/customer-archive.vue +++ b/src/views/vehicle/customer-archive.vue @@ -4758,7 +4758,7 @@ export default { }, selectionClear() { this.selectionList = []; - this.$refs.crud.toggleSelection(); + this.$refs.crud?.toggleSelection(); }, currentChange(currentPage) { this.page.currentPage = currentPage; From 84d9e098ba7f807687df7dba11002c7c3c98da4c Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 16 Sep 2026 23:43:12 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E8=B0=83=E6=95=B4=20=E4=B8=9A=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oaLogin.html | 52 ++ src/api/system/dept.js | 7 + src/docker/nginx.conf | 19 + .../components/shipping-template-page.vue | 4 +- .../components/waybill-import-dialog.vue | 398 +++++++++--- .../components/waybill-manage-page.vue | 8 +- src/views/business/loading-manage.vue | 607 +++++++++++------- src/views/business/waybill-import.vue | 27 +- src/views/system/dept.vue | 39 +- 9 files changed, 795 insertions(+), 366 deletions(-) create mode 100644 oaLogin.html create mode 100644 src/docker/nginx.conf diff --git a/oaLogin.html b/oaLogin.html new file mode 100644 index 0000000..7c8f80f --- /dev/null +++ b/oaLogin.html @@ -0,0 +1,52 @@ + + + + +oa登录 + + + + + + + + \ No newline at end of file diff --git a/src/api/system/dept.js b/src/api/system/dept.js index fc2f0ad..7bf3f68 100644 --- a/src/api/system/dept.js +++ b/src/api/system/dept.js @@ -41,6 +41,13 @@ export const add = row => { }); }; +export const syncIamOrganizations = () => { + return request({ + url: '/blade-system/dept/sync-iam-organizations', + method: 'post', + }); +}; + export const update = row => { return request({ url: '/blade-system/dept/submit', diff --git a/src/docker/nginx.conf b/src/docker/nginx.conf new file mode 100644 index 0000000..0155f82 --- /dev/null +++ b/src/docker/nginx.conf @@ -0,0 +1,19 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + location /assets/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri =404; + } + + location / { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + try_files $uri $uri/ /index.html; + } +} diff --git a/src/views/business/components/shipping-template-page.vue b/src/views/business/components/shipping-template-page.vue index 323d15c..3b33a8f 100644 --- a/src/views/business/components/shipping-template-page.vue +++ b/src/views/business/components/shipping-template-page.vue @@ -2027,8 +2027,8 @@ export default { delete detail.updateUser; delete detail.updateUserName; delete detail.status; - // 模板名称追加「副本」 - detail.templateName = `${detail.templateName || ''}副本`; + // 模板名称追加「-副本」 + detail.templateName = `${detail.templateName || ''}-副本`; console.log('处理后的数据:', detail); console.log('isStandaloneBusinessPage:', this.isStandaloneBusinessPage); diff --git a/src/views/business/components/waybill-import-dialog.vue b/src/views/business/components/waybill-import-dialog.vue index 33585a8..57dc3a0 100644 --- a/src/views/business/components/waybill-import-dialog.vue +++ b/src/views/business/components/waybill-import-dialog.vue @@ -10,107 +10,200 @@ :class="{ 'waybill-import-page': standalone }" @closed="handleClosed" > -
-
+
+
+ 新建导入 + 批量删除
+
+ + + + +
- - - - - - - + - + + + + - - - + + + + + + + + +
+ +
+ + + + + {{ item.label }} + + + + + 查询 + + + - - +
+ +
emit('closed'); const createVisible = ref(props.createPage), detailVisible = ref(false), formRef = ref(); +const searchVisible = ref(true); +const columnSettingVisible = ref(false); +const tableSize = ref('default'); +const tableSizeOptions = ['default', 'large', 'small']; +const columnOptions = [ + { prop: 'batchNo', label: '运单批次号' }, + { prop: 'projectName', label: '项目名称' }, + { prop: 'carrierName', label: '承运商' }, + { prop: 'carrierType', label: '承运类型' }, + { prop: 'waybillCount', label: '运单数' }, + { prop: 'importTypeName', label: '导入方式' }, + { prop: 'createUserName', label: '创建人' }, + { prop: 'statusName', label: '状态' }, + { prop: 'createTime', label: '创建时间' }, + { prop: 'updateTime', label: '更新时间' }, +]; +const visibleColumnKeys = ref(columnOptions.map(item => item.prop)); +const columnVisible = computed(() => + Object.fromEntries(columnOptions.map(item => [item.prop, visibleColumnKeys.value.includes(item.prop)])) +); +const toggleTableSize = () => { + const index = tableSizeOptions.indexOf(tableSize.value); + tableSize.value = tableSizeOptions[(index + 1) % tableSizeOptions.length]; +}; const query = reactive({ batchNo: '', carrierId: '', createUser: '', createTimeRange: [] }); -const detailQuery = reactive({ vehicleNo: '', cargoName: '' }); +const detailQuery = reactive({ vehicleNo: '', driverName: '' }); const createDefaultForm = () => ({ id: '', batchNo: '', @@ -862,6 +985,20 @@ const loadEditorOptions = async () => { cargoTypeFlatOptions.value = flattenCargoTypeOptions(cargoTypeOptions.value); driverOptions.value = extractRecords(driverRes); }; +const ensureTransportTypeOptions = async () => { + if (transportTypeOptions.value.length) return; + const dictRes = await getDictionary({ code: 'transport_type' }); + transportTypeOptions.value = extractRecords(dictRes).map(item => ({ + label: item.dictValue || item.label, + value: item.dictKey || item.value, + })); +}; +const transportTypeLabel = value => { + if (value === null || value === undefined || value === '') return '-'; + return ( + transportTypeOptions.value.find(item => String(item.value) === String(value))?.label || value + ); +}; const loadBatches = async () => { const res = await api.getImportBatches({ ...query, current: page.current, size: page.size }); batches.value = res.data?.data?.records || []; @@ -922,9 +1059,13 @@ const closeCreate = () => { } createVisible.value = false; }; -const openDetail = row => { +const openDetail = async row => { detailQuery.batchId = row.id; + detailQuery.vehicleNo = ''; + detailQuery.driverName = ''; + detailPage.current = 1; detailVisible.value = true; + await ensureTransportTypeOptions(); loadDetails(); }; // 草稿明细已落库为草稿运单,编辑时回填到明细表,避免重新上传附件。 @@ -1329,34 +1470,81 @@ const confirmImport = async () => { diff --git a/src/views/system/dept.vue b/src/views/system/dept.vue index aac5268..edad1f7 100644 --- a/src/views/system/dept.vue +++ b/src/views/system/dept.vue @@ -22,6 +22,14 @@ @tree-load="treeLoad" >