diff --git a/src/views/business/components/waybill-manage-page.vue b/src/views/business/components/waybill-manage-page.vue index 78f93be..fe7ec36 100644 --- a/src/views/business/components/waybill-manage-page.vue +++ b/src/views/business/components/waybill-manage-page.vue @@ -2057,6 +2057,100 @@ + + + + + + + + + + + + + + + +
@@ -2356,6 +2450,284 @@ + +
+ + + + + {{ waybillExceptionValue(field.prop) }} + + + + + + + + + {{ + waybillExceptionValue('reporterName') + }} + + + + + {{ + waybillExceptionValue('reportTime') + }} + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + 删除 +
+ +
+ 添加
+
+
+
+
+
+
+
+ + +
+ +
+ ({ label: item.label, value: item.value })); + }, + waybillExceptionReasonOptions() { + return ( + exceptionCategoryOptions.find( + item => item.value === this.waybillExceptionEditForm.exceptionType + )?.children || [] + ); + }, + waybillExceptionPhotoUrls() { + return this.parseWaybillExceptionPhotos(this.waybillExceptionDetail.scenePhotos); + }, + waybillExceptionUploadHeaders() { + return getUploadHeaders(); + }, permissionList() { return { addBtn: this.canCreate, @@ -4629,6 +5081,7 @@ export default { this.waybillVoucherFolder = null; this.waybillVoucherFolderView = false; this.waybillVoucherImagesLoaded = false; + this.waybillExceptionRows = []; const request = typeof this.api.getDetail === 'function' ? this.api.getDetail(row.id) @@ -4639,6 +5092,7 @@ export default { this.detailRow = detail; this.loadWaybillPunchRecords(); this.loadWaybillVoucherImages(); + this.loadWaybillExceptions(detail); if (detail.contractId) { return getContractDetail(detail.contractId) .then(contractRes => { @@ -4668,6 +5122,206 @@ export default { this.detailLoading = false; }); }, + // 异常处置列表接口不支持按 waybillId 精确过滤,只有 waybillNo 模糊匹配, + // 而运单号存在前缀包含(YD001 会命中 YD0011),故取回后必须再按 waybillId 精确筛一次。 + // 多取一页降低「同前缀运单占满首页」导致漏数据的概率。 + // 注意 String() 比较只是避免类型不一致,挡不住 int64 精度:运单 id 超出 JS 安全整数范围, + // 两个相差很小的 id 在响应 JSON 解析时就已被解析成同一个浮点值,此处无从分辨。 + async loadWaybillExceptions(detail) { + const waybillId = detail?.id; + const waybillNo = detail?.waybillNo; + if (!waybillId || !waybillNo) { + this.waybillExceptionRows = []; + return; + } + this.waybillExceptionLoading = true; + try { + const res = await exceptionDisposalApi.getList(1, 100, { waybillNo }); + const data = res?.data?.data || res?.data || res || {}; + this.waybillExceptionRows = (data.records || []) + .filter(item => String(item.waybillId) === String(waybillId)) + .map(this.decorateWaybillExceptionRow); + } catch (error) { + this.waybillExceptionRows = []; + } finally { + this.waybillExceptionLoading = false; + } + }, + decorateWaybillExceptionRow(row) { + return { + ...row, + exceptionTypeName: this.waybillExceptionName(row.exceptionType), + disposalStatusName: + disposalStatusOptions.find(item => item.value === row.disposalStatus)?.label || + row.disposalStatus || + '-', + }; + }, + waybillExceptionName(value) { + if (!value) return '-'; + // 一级存中文大类名(如「交通异常」),二级存子类名(如「货损」),两级都查 + const group = exceptionCategoryOptions.find(item => item.value === value); + if (group) return group.label; + for (const item of exceptionCategoryOptions) { + const match = item.children.find(child => child.value === value); + if (match) return match.label; + } + return value; + }, + async openWaybillException(row, mode) { + this.waybillExceptionDialog = { + visible: true, + mode, + loading: true, + submitting: false, + }; + this.waybillExceptionFollowForm = { followContent: '', attachments: [] }; + try { + const res = await exceptionDisposalApi.getDetail(row.id); + const detail = res?.data?.data || res?.data || res || {}; + this.waybillExceptionDetail = this.decorateWaybillExceptionRow(detail); + if (mode === 'edit') { + this.waybillExceptionEditForm = { + exceptionType: this.waybillExceptionDetail.exceptionType || '', + exceptionReason: this.waybillExceptionDetail.exceptionReason || '', + reportDescription: this.waybillExceptionDetail.reportDescription || '', + scenePhotoList: this.parseWaybillExceptionPhotos( + this.waybillExceptionDetail.scenePhotos + ), + }; + } + } finally { + this.waybillExceptionDialog.loading = false; + } + }, + waybillExceptionValue(prop) { + const value = this.waybillExceptionDetail[prop]; + return value === null || value === undefined || value === '' ? '-' : value; + }, + handleWaybillExceptionTypeChange() { + // 一级类型变了,旧二级可能不属于新类型 + const stillValid = this.waybillExceptionReasonOptions.some( + item => item.value === this.waybillExceptionEditForm.exceptionReason + ); + if (!stillValid) { + this.waybillExceptionEditForm.exceptionReason = ''; + } + }, + beforeWaybillExceptionPhotoUpload(file) { + const validType = ['image/jpeg', 'image/png', 'image/jpg', 'image/bmp'].includes(file.type); + if (!validType) { + this.$message.error('仅支持 JPG、PNG、BMP 图片'); + return false; + } + if (file.size / 1024 / 1024 >= 5) { + this.$message.error('图片大小不能超过 5MB'); + return false; + } + return true; + }, + handleWaybillExceptionPhotoSuccess(res) { + if (res.code === 200 && res.data) { + const url = res.data.link || res.data.url || res.data.domain || ''; + if (url) { + this.waybillExceptionEditForm.scenePhotoList = [ + ...this.waybillExceptionEditForm.scenePhotoList, + url, + ]; + } + } else { + this.$message.error(res.msg || '上传失败'); + } + }, + handleWaybillExceptionPhotoError() { + this.$message.error('上传失败'); + }, + removeWaybillExceptionPhoto(index) { + const list = [...this.waybillExceptionEditForm.scenePhotoList]; + list.splice(index, 1); + this.waybillExceptionEditForm.scenePhotoList = list; + }, + parseWaybillExceptionPhotos(value) { + if (!value) return []; + try { + const parsed = JSON.parse(value); + if (Array.isArray(parsed)) return parsed.filter(Boolean); + } catch (error) { + return String(value) + .split(',') + .map(item => item.trim()) + .filter(Boolean); + } + return []; + }, + parseWaybillExceptionAttachments(value) { + if (!value) return []; + if (Array.isArray(value)) return value; + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed.filter(item => item && item.url) : []; + } catch (error) { + return []; + } + }, + async submitWaybillExceptionEdit() { + await this.$refs.waybillExceptionEditFormRef.validate(); + this.waybillExceptionDialog.submitting = true; + try { + await exceptionDisposalApi.update({ + id: this.waybillExceptionDetail.id, + exceptionType: this.waybillExceptionEditForm.exceptionType, + exceptionReason: this.waybillExceptionEditForm.exceptionReason, + reportDescription: this.waybillExceptionEditForm.reportDescription, + scenePhotos: this.waybillExceptionEditForm.scenePhotoList.join(','), + }); + this.$message.success('编辑成功'); + this.waybillExceptionDialog.visible = false; + this.refreshWaybillExceptions(); + } finally { + this.waybillExceptionDialog.submitting = false; + } + }, + async submitWaybillExceptionFollow() { + await this.$refs.waybillExceptionFollowFormRef.validate(); + const followContent = (this.waybillExceptionFollowForm.followContent || '').trim(); + const attachmentsJson = this.waybillExceptionFollowForm.attachments.length + ? JSON.stringify(this.waybillExceptionFollowForm.attachments) + : ''; + // 后端要求二者至少有其一 + if (!followContent && !attachmentsJson) { + this.$message.warning('请填写跟进说明或上传附件'); + return; + } + this.waybillExceptionDialog.submitting = true; + try { + await exceptionDisposalApi.follow({ + id: this.waybillExceptionDetail.id, + followContent, + attachmentsJson, + }); + this.$message.success('跟进成功'); + this.waybillExceptionDialog.visible = false; + this.refreshWaybillExceptions(); + } finally { + this.waybillExceptionDialog.submitting = false; + } + }, + async completeWaybillExceptionRow(row) { + await this.$confirm('确认完成该异常处置?', '提示', { type: 'warning' }); + await exceptionDisposalApi.complete({ id: row.id }); + this.$message.success('完成成功'); + this.refreshWaybillExceptions(); + }, + async completeWaybillExceptionFromDialog() { + await this.$confirm('确认完成该异常处置?', '提示', { type: 'warning' }); + await exceptionDisposalApi.complete({ id: this.waybillExceptionDetail.id }); + this.$message.success('完成成功'); + this.waybillExceptionDialog.visible = false; + this.refreshWaybillExceptions(); + }, + refreshWaybillExceptions() { + this.loadWaybillExceptions(this.detailRow); + }, closeDetail() { this.destroyWaybillLocateMap(); this.destroyWaybillTrackMap(); @@ -10046,6 +10700,81 @@ export default { padding-top: 12px; } + &__exception-actions { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + } + + &__exception-dialog { + padding: 4px 0 0; + + :deep(.section-card + .section-card) { + margin-top: 16px; + } + } + + &__exception-readonly { + color: #606266; + word-break: break-all; + } + + &__exception-attachment-link { + display: block; + margin-bottom: 2px; + } + + &__exception-photos { + display: flex; + flex-wrap: wrap; + gap: 12px; + min-height: 80px; + + :deep(.el-image) { + width: 96px; + height: 96px; + border: 1px solid #eff1f7; + } + } + + &__exception-edit-photos { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; + } + + &__exception-edit-photo { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + + :deep(.el-image) { + width: 96px; + height: 96px; + border: 1px solid #eff1f7; + } + } + + &__exception-edit-photo-add { + display: flex; + align-items: center; + justify-content: center; + width: 96px; + height: 96px; + border: 1px dashed #dcdfe6; + border-radius: 4px; + color: #909399; + cursor: pointer; + + &:hover { + border-color: #409eff; + color: #409eff; + } + } + &__waybill-detail-summary { :deep(.el-card__body) { padding: 18px 22px;