feat: add inbound hold evidence controls
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Download, Eye, GitBranch, History, Link2, ListChecks, MoreHorizontal, PackageOpen, Ruler, Send, ShieldAlert, SlidersHorizontal, StickyNote, Undo2, WalletCards } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Download, Eye, FileUp, GitBranch, History, Link2, ListChecks, MoreHorizontal, PackageOpen, Ruler, Send, ShieldAlert, SlidersHorizontal, StickyNote, Undo2, WalletCards } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
getPlatformUnifiedOrder,
|
||||
@@ -63,8 +63,10 @@ import {
|
||||
getLatestInboundFee,
|
||||
getInboundMeasurement,
|
||||
getPlatformInboundHold,
|
||||
downloadPlatformInboundHoldEvidence,
|
||||
holdPlatformInbound,
|
||||
releasePlatformInbound,
|
||||
uploadPlatformInboundHoldEvidence,
|
||||
saveInboundMeasurementDraft,
|
||||
InboundFeePanel,
|
||||
InboundMeasurementDialog,
|
||||
@@ -174,6 +176,11 @@ const holdDialogOpen = ref(false)
|
||||
const holdSaving = ref(false)
|
||||
const holdAction = ref<'HOLD' | 'RELEASE'>('HOLD')
|
||||
const holdReason = ref('')
|
||||
const holdEvidenceFile = ref<File | null>(null)
|
||||
const holdEvidenceUploadKey = ref(0)
|
||||
const holdUploadedEvidenceFileId = ref('')
|
||||
const holdEvidenceError = ref<ApiErrorDetails | null>(null)
|
||||
const holdDownloadingFileId = ref('')
|
||||
const relationOpen = ref(false)
|
||||
const relationOrder = ref<UnifiedOrderResource | null>(null)
|
||||
const measurementDetail = ref<InboundMeasurementDetail | null>(null)
|
||||
@@ -1372,10 +1379,46 @@ function openHoldDialog(action: 'HOLD' | 'RELEASE') {
|
||||
if (!allowed || !holdDetail.value) return
|
||||
holdAction.value = action
|
||||
holdReason.value = ''
|
||||
holdEvidenceFile.value = null
|
||||
holdUploadedEvidenceFileId.value = ''
|
||||
holdEvidenceError.value = null
|
||||
holdEvidenceUploadKey.value += 1
|
||||
holdDialogOpen.value = true
|
||||
}
|
||||
|
||||
const holdEvidenceMaxBytes = 10 * 1024 * 1024
|
||||
const holdEvidenceExtensions = new Set(['txt', 'pdf', 'jpg', 'jpeg', 'png'])
|
||||
|
||||
function selectHoldEvidenceFile(upload: UploadFile) {
|
||||
const file = upload.raw || null
|
||||
holdEvidenceFile.value = null
|
||||
holdUploadedEvidenceFileId.value = ''
|
||||
holdEvidenceError.value = null
|
||||
if (!file) return
|
||||
const extension = file.name.split('.').pop()?.toLowerCase() || ''
|
||||
if (!holdEvidenceExtensions.has(extension)) {
|
||||
holdEvidenceError.value = { code: 'FILE_TYPE_INVALID', message: '关联文件仅支持 TXT、PDF、JPG、PNG' }
|
||||
holdEvidenceUploadKey.value += 1
|
||||
return
|
||||
}
|
||||
if (file.size <= 0 || file.size > holdEvidenceMaxBytes) {
|
||||
holdEvidenceError.value = { code: 'FILE_SIZE_INVALID', message: '关联文件不能为空且不能超过 10MB' }
|
||||
holdEvidenceUploadKey.value += 1
|
||||
return
|
||||
}
|
||||
holdEvidenceFile.value = file
|
||||
}
|
||||
|
||||
function removeHoldEvidenceFile() {
|
||||
holdEvidenceFile.value = null
|
||||
holdUploadedEvidenceFileId.value = ''
|
||||
holdEvidenceError.value = null
|
||||
}
|
||||
|
||||
async function submitHoldAction() {
|
||||
if (holdSaving.value) return
|
||||
const allowed = holdAction.value === 'HOLD' ? canHoldInbound.value : canReleaseInbound.value
|
||||
if (!allowed) return
|
||||
const reason = holdReason.value.trim()
|
||||
const inboundId = normalizedInboundId.value
|
||||
const tenantId = normalizedIdentifier(detail.value?.summary.tenantId)
|
||||
@@ -1384,9 +1427,28 @@ async function submitHoldAction() {
|
||||
ElMessage.warning(holdAction.value === 'HOLD' ? '请填写扣货原因' : '请填写解除原因')
|
||||
return
|
||||
}
|
||||
if (reason.length > 1000) {
|
||||
ElMessage.warning('原因不能超过 1000 个字符')
|
||||
return
|
||||
}
|
||||
holdSaving.value = true
|
||||
try {
|
||||
const command = { expectedVersion: holdDetail.value.version, reason }
|
||||
if (holdEvidenceFile.value && !holdUploadedEvidenceFileId.value) {
|
||||
try {
|
||||
holdEvidenceError.value = null
|
||||
const uploaded = await uploadPlatformInboundHoldEvidence(tenantId, inboundId, holdEvidenceFile.value)
|
||||
holdUploadedEvidenceFileId.value = uploaded.fileId
|
||||
} catch (error) {
|
||||
holdEvidenceError.value = getApiErrorDetails(error, '关联文件上传失败,请稍后重试')
|
||||
ElMessage.error(holdEvidenceError.value.message)
|
||||
return
|
||||
}
|
||||
}
|
||||
const command = {
|
||||
expectedVersion: holdDetail.value.version,
|
||||
reason,
|
||||
...(holdUploadedEvidenceFileId.value ? { evidenceFileId: holdUploadedEvidenceFileId.value } : {}),
|
||||
}
|
||||
holdDetail.value = holdAction.value === 'HOLD'
|
||||
? await holdPlatformInbound(tenantId, inboundId, command)
|
||||
: await releasePlatformInbound(tenantId, inboundId, command)
|
||||
@@ -1400,6 +1462,32 @@ async function submitHoldAction() {
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadHoldEvidence(fileId: string) {
|
||||
const inboundId = normalizedInboundId.value
|
||||
const tenantId = normalizedIdentifier(detail.value?.summary.tenantId)
|
||||
if (!fileId || !inboundId || !tenantId || holdDownloadingFileId.value || !can('platform:firstleg:inbound:list')) return
|
||||
holdDownloadingFileId.value = fileId
|
||||
try {
|
||||
const blob = await downloadPlatformInboundHoldEvidence(tenantId, inboundId, fileId)
|
||||
const extension = blob.type === 'application/pdf' ? '.pdf'
|
||||
: blob.type === 'image/png' ? '.png'
|
||||
: blob.type === 'image/jpeg' ? '.jpg'
|
||||
: blob.type === 'text/plain' ? '.txt' : ''
|
||||
const url = URL.createObjectURL(blob)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = `扣货凭证${extension}`
|
||||
document.body.appendChild(anchor)
|
||||
anchor.click()
|
||||
anchor.remove()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
ElMessage.error(getApiErrorDetails(error, '扣货凭证下载失败,请稍后重试').message)
|
||||
} finally {
|
||||
holdDownloadingFileId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCustomers() {
|
||||
customerLoading.value = true
|
||||
customerLoadError.value = null
|
||||
@@ -2318,7 +2406,7 @@ onBeforeUnmount(() => {
|
||||
<template v-else>
|
||||
<p v-if="holdDetail?.actionBlockReason && !canHoldInbound && !canReleaseInbound" class="hold-block-reason">{{ holdDetail.actionBlockReason }}</p>
|
||||
<h3 class="hold-history-title">扣货历史</h3>
|
||||
<div v-loading="holdLoading" class="hold-history-table"><el-table :data="holdDetail?.history || []" border stripe empty-text="暂无扣货历史"><el-table-column label="操作" width="110"><template #default="{ row }"><el-tag :type="row.action === 'HOLD' ? 'danger' : 'success'" effect="plain">{{ row.action === 'HOLD' ? '扣货' : '解除扣货' }}</el-tag></template></el-table-column><el-table-column prop="reason" label="原因" min-width="260" /><el-table-column label="凭证" width="130"><template #default="{ row }">{{ row.evidenceFileId ? '已关联凭证' : '—' }}</template></el-table-column><el-table-column label="操作人" width="150"><template #default="{ row }">{{ row.operatorName || '系统' }}</template></el-table-column><el-table-column label="操作时间" width="180"><template #default="{ row }">{{ formatDate(row.operatedAt) }}</template></el-table-column></el-table></div>
|
||||
<div v-loading="holdLoading" class="hold-history-table"><el-table :data="holdDetail?.history || []" border stripe empty-text="暂无扣货历史"><el-table-column label="操作" width="110"><template #default="{ row }"><el-tag :type="row.action === 'HOLD' ? 'danger' : 'success'" effect="plain">{{ row.action === 'HOLD' ? '扣货' : '解除扣货' }}</el-tag></template></el-table-column><el-table-column prop="reason" label="原因" min-width="260" /><el-table-column label="凭证" width="150"><template #default="{ row }"><el-button v-if="row.evidenceFileId" link type="primary" :loading="holdDownloadingFileId === row.evidenceFileId" @click="downloadHoldEvidence(row.evidenceFileId)"><Download :size="14" />下载凭证</el-button><span v-else>—</span></template></el-table-column><el-table-column label="操作人" width="150"><template #default="{ row }">{{ row.operatorName || '系统' }}</template></el-table-column><el-table-column label="操作时间" width="180"><template #default="{ row }">{{ formatDate(row.operatedAt) }}</template></el-table-column></el-table></div>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
@@ -2502,9 +2590,9 @@ onBeforeUnmount(() => {
|
||||
<el-input v-model="noteContent" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入仅平台内部可见的备注" aria-label="内部备注内容" />
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog v-model="holdDialogOpen" :title="holdAction === 'HOLD' ? '确认扣货' : '确认解除扣货'" :confirm-text="holdAction === 'HOLD' ? '确认扣货' : '确认解除'" :confirm-type="holdAction === 'HOLD' ? 'danger' : 'primary'" :confirm-loading="holdSaving" width="min(600px, calc(100vw - 28px))" destroy-on-close @confirm="submitHoldAction">
|
||||
<AppDialog v-model="holdDialogOpen" :title="holdAction === 'HOLD' ? '确认扣货' : '确认解除扣货'" :confirm-text="holdAction === 'HOLD' ? '确认扣货' : '确认解除'" :confirm-type="holdAction === 'HOLD' ? 'danger' : 'primary'" :confirm-loading="holdSaving" :confirm-disabled="!holdReason.trim() || holdReason.trim().length > 1000" width="min(600px, calc(100vw - 28px))" destroy-on-close @confirm="submitHoldAction">
|
||||
<el-alert :title="holdAction === 'HOLD' ? '扣货后服务端将阻止后续实物履约操作。' : '解除后是否可继续履约仍以服务端状态为准。'" :type="holdAction === 'HOLD' ? 'warning' : 'info'" :closable="false" show-icon />
|
||||
<el-form label-position="top" class="hold-command-form"><el-form-item :label="holdAction === 'HOLD' ? '扣货原因' : '解除原因'" required><el-input v-model="holdReason" type="textarea" :rows="4" maxlength="1000" show-word-limit :placeholder="holdAction === 'HOLD' ? '请输入扣货原因' : '请输入解除扣货原因'" aria-label="扣货操作原因" /></el-form-item><p class="hold-evidence-hint">历史记录中的已有凭证可在扣货历史中查看关联状态。</p></el-form>
|
||||
<el-form label-position="top" class="hold-command-form"><el-form-item :label="holdAction === 'HOLD' ? '扣货原因' : '解除原因'" required><el-input v-model="holdReason" type="textarea" :rows="4" maxlength="1000" show-word-limit :placeholder="holdAction === 'HOLD' ? '请输入扣货原因' : '请输入解除扣货原因'" aria-label="扣货操作原因" /></el-form-item><el-form-item label="关联文件(选填)"><el-upload :key="holdEvidenceUploadKey" :auto-upload="false" :limit="1" accept=".txt,.pdf,.jpg,.jpeg,.png,text/plain,application/pdf,image/jpeg,image/png" :disabled="holdSaving" :on-change="selectHoldEvidenceFile" :on-remove="removeHoldEvidenceFile"><el-button :disabled="holdSaving"><FileUp :size="15" />选择文件</el-button><template #tip><div class="el-upload__tip">仅支持 TXT、PDF、JPG、PNG,单文件不超过 10MB;服务端将校验文件真实内容。</div></template></el-upload></el-form-item><LoadErrorState v-if="holdEvidenceError" compact title="关联文件处理失败" :message="holdEvidenceError.message" :show-retry="false"><template #details><ApiErrorMeta :details="holdEvidenceError" /></template></LoadErrorState></el-form>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog v-model="approvalOpen" title="审核订单" confirm-text="审核通过" confirm-type="success" :confirm-loading="approvalSaving" destroy-on-close @confirm="approveOrder" @cancel="closeApproval">
|
||||
|
||||
Reference in New Issue
Block a user