Files
admin-web/src/modules/loading-lists/pages/ConfirmedLoadingListDetailView.vue
T
2026-08-26 15:32:22 +08:00

183 lines
6.2 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { FileUp } from '@lucide/vue'
import { ElMessage, type UploadFile } from 'element-plus'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import LoadingListDetailDrawer from '../components/LoadingListDetailDrawer.vue'
import {
completeLoadingDeclaration,
getConfirmedLoadingList,
getLoadingDeclaration,
uploadLoadingDeclaration,
type ConfirmedSummary,
type LoadingDetail,
} from '../api/platformLoadingLists'
const route = useRoute()
const router = useRouter()
const access = useAccessPreviewStore()
const loading = ref(false)
const detail = ref<LoadingDetail<ConfirmedSummary> | null>(null)
const error = ref<ApiErrorDetails | null>(null)
const customsDialogOpen = ref(false)
const customsFile = ref<File | null>(null)
const customsSaving = ref(false)
const customsError = ref<ApiErrorDetails | null>(null)
let loadSequence = 0
const planId = computed(() => String(route.params.planId || ''))
const warehouseId = computed(() => typeof route.query.warehouseId === 'string' ? route.query.warehouseId : '')
const canManageCustoms = computed(() => access.can('platform:firstleg:customs:manage'))
async function loadDetail() {
const sequence = ++loadSequence
detail.value = null
if (!planId.value || !warehouseId.value) {
loading.value = false
error.value = { code: 'MISSING_WAREHOUSE_CONTEXT', message: '该清单缺少所属仓库信息,无法安全查询详情,请返回列表重新进入。' }
return
}
loading.value = true
error.value = null
try {
const [result, customsDocument] = await Promise.all([
getConfirmedLoadingList(planId.value, warehouseId.value),
getLoadingDeclaration(planId.value, warehouseId.value).catch((cause) => {
const details = getApiErrorDetails(cause)
if (details.code === 'A0404') return null
throw cause
}),
])
if (sequence !== loadSequence) return
if (result.summary.status !== 'COMPLETED') {
error.value = { code: 'MODE_STATUS_MISMATCH', message: '该计划尚未形成正式装柜清单,请从预装清单入口查看。' }
return
}
detail.value = { ...result, customsDocument }
} catch (cause) {
if (sequence !== loadSequence) return
detail.value = null
error.value = getApiErrorDetails(cause, '装柜清单详情加载失败,请稍后重试')
} finally {
if (sequence === loadSequence) loading.value = false
}
}
function goBack() {
if (window.history.length > 1) router.back()
else void router.push({ name: 'loading-lists' })
}
function openCustomsDocument() {
customsFile.value = null
customsError.value = null
customsDialogOpen.value = true
}
function selectCustomsFile(uploadFile: UploadFile) {
customsFile.value = uploadFile.raw ?? null
customsError.value = null
}
function removeCustomsFile() {
customsFile.value = null
}
async function uploadCustomsDocument() {
if (!detail.value || !customsFile.value) {
customsError.value = { code: 'FILE_REQUIRED', message: '请选择报关资料文件' }
return
}
customsSaving.value = true
customsError.value = null
try {
detail.value.customsDocument = await uploadLoadingDeclaration(
planId.value,
warehouseId.value,
customsFile.value,
detail.value.customsDocument?.version || 0,
'上传装柜清单报关资料',
)
customsFile.value = null
customsDialogOpen.value = false
ElMessage.success('报关资料已上传')
} catch (cause) {
customsError.value = getApiErrorDetails(cause, '报关资料上传失败')
} finally {
customsSaving.value = false
}
}
async function completeCustomsDocument() {
if (!detail.value?.customsDocument) return
customsSaving.value = true
customsError.value = null
try {
detail.value.customsDocument = await completeLoadingDeclaration(
planId.value,
warehouseId.value,
detail.value.customsDocument.version,
'确认装柜清单报关完成',
)
ElMessage.success('已完成报关')
} catch (cause) {
customsError.value = getApiErrorDetails(cause, '报关完成失败')
ElMessage.error(customsError.value.message)
} finally {
customsSaving.value = false
}
}
onMounted(() => void loadDetail())
watch([planId, warehouseId], () => void loadDetail())
</script>
<template>
<AdminPage>
<div class="confirmed-loading-detail-page">
<OperationErrorToast :details="customsError" />
<LoadingListDetailDrawer
mode="confirmed"
:loading="loading"
:detail="detail"
:error="error"
:can-edit="false"
:can-manage-customs="canManageCustoms"
:customs-saving="customsSaving"
@back="goBack"
@retry="loadDetail"
@attach-customs="openCustomsDocument"
@complete-customs="completeCustomsDocument"
/>
</div>
<AppDialog
v-model="customsDialogOpen"
title="装柜清单报关资料"
width="min(520px, calc(100vw - 28px))"
confirm-text="上传资料"
:confirm-loading="customsSaving"
:confirm-disabled="!customsFile"
@confirm="uploadCustomsDocument"
>
<el-form label-position="top">
<el-form-item label="报关资料">
<el-upload :auto-upload="false" :limit="1" :on-change="selectCustomsFile" :on-remove="removeCustomsFile">
<el-button><FileUp :size="16" />选择文件</el-button>
<template #tip><div class="el-upload__tip">{{ detail?.customsDocument ? '上传新文件将替换当前资料。' : '请选择需要提交的装柜清单报关资料。' }}</div></template>
</el-upload>
</el-form-item>
</el-form>
</AppDialog>
</AdminPage>
</template>
<style scoped>
.confirmed-loading-detail-page { display: grid; min-width: 0; max-width: 1280px; gap: 16px; }
@media (min-width: 1920px) { .confirmed-loading-detail-page { width: 100%; max-width: none; gap: 12px; } }
</style>