This commit is contained in:
2026-08-15 12:33:29 +08:00
parent e214672779
commit fc80d09faa
4 changed files with 220 additions and 78 deletions
@@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules, type UploadFile } from 'element-plus'
import { Eye, FileUp, Pencil } from '@lucide/vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { Eye, Pencil } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
@@ -15,11 +15,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { loadingListStatusLabel } from '../status'
import LoadingListDetailDrawer from './LoadingListDetailDrawer.vue'
import {
uploadLoadingDeclaration,
completeLoadingDeclaration,
getConfirmedLoadingList,
getPreloadPlan,
listConfirmedLoadingLists,
listPreloadPlans,
@@ -56,10 +52,6 @@ const editError = ref<ApiErrorDetails | null>(null)
const editFormRef = ref<FormInstance>()
const editForm = reactive({ containerNo: '', voyageNo: '', operationReason: '' })
const handledDeepLink = ref('')
const customsDialogOpen = ref(false)
const customsFile = ref<File | null>(null)
const customsSaving = ref(false)
const customsError = ref<ApiErrorDetails | null>(null)
const isPreload = computed(() => props.mode === 'preload')
const title = computed(() => isPreload.value ? '预装清单' : '装柜清单')
@@ -67,7 +59,6 @@ const listPermission = computed(() => isPreload.value ? 'platform:firstleg:prelo
const canList = computed(() => access.can(listPermission.value))
const canView = computed(() => canList.value)
const canEdit = computed(() => isPreload.value && access.can('platform:firstleg:preload:edit'))
const canManageCustoms = computed(() => !isPreload.value && access.can('platform:firstleg:customs:manage'))
const fields = computed<QueryField[]>(() => [
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
{ key: 'keyword', type: 'input', label: '关键词', placeholder: isPreload.value ? '预装单号/集装箱号/柜号' : '装柜清单号/集装箱号/柜号', width: 250 },
@@ -165,20 +156,7 @@ async function openDeepLinkedDetail() {
await router.replace({ name: 'preload-plan-detail', params: { planId }, query: { warehouseId } })
return
}
selectedPlanId.value = planId
selectedWarehouseId.value = warehouseId
detail.value = null
detailError.value = null
await loadDetail()
const expectedStatuses = isPreload.value ? ['EDITING', 'LOADING'] : ['COMPLETED']
const loadedDetail = detail.value as LoadingDetail | null
if (loadedDetail && !expectedStatuses.includes(loadedDetail.summary.status)) {
detail.value = null
detailError.value = {
code: 'MODE_STATUS_MISMATCH',
message: `该计划不属于当前${title.value}状态,请从对应清单入口查看。`,
}
}
await router.replace({ name: 'loading-list-detail', params: { planId }, query: { warehouseId } })
}
function search() {
@@ -200,9 +178,7 @@ async function loadDetail() {
detailLoading.value = true
detailError.value = null
try {
detail.value = isPreload.value
? await getPreloadPlan(selectedPlanId.value, selectedWarehouseId.value)
: await getConfirmedLoadingList(selectedPlanId.value, selectedWarehouseId.value)
detail.value = await getPreloadPlan(selectedPlanId.value, selectedWarehouseId.value)
} catch (cause) {
detailError.value = getApiErrorDetails(cause, '清单详情加载失败,请稍后重试')
} finally {
@@ -216,11 +192,7 @@ function openDetail(rowValue: object) {
void router.push({ name: 'preload-plan-detail', params: { planId: String(row.planId) }, query: { warehouseId: String(row.warehouseId || '') } })
return
}
selectedPlanId.value = String(row.planId)
selectedWarehouseId.value = String(row.warehouseId || '')
detail.value = null
detailError.value = null
void loadDetail()
void router.push({ name: 'loading-list-detail', params: { planId: String(row.planId) }, query: { warehouseId: String(row.warehouseId || '') } })
}
async function openEditFromRow(rowValue: object) {
@@ -287,20 +259,6 @@ async function loadCustomers() {
customersLoading.value = false
}
}
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(selectedPlanId.value, selectedWarehouseId.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(selectedPlanId.value, selectedWarehouseId.value, detail.value.customsDocument.version, '确认装柜清单报关完成'); ElMessage.success('已完成报关') } catch (cause) { customsError.value = getApiErrorDetails(cause, '报关完成失败') } finally { customsSaving.value = false }
}
watch([page, pageSize], () => void load())
watch(() => [route.query.planId, route.query.warehouseId], () => void openDeepLinkedDetail())
onMounted(() => {
@@ -361,24 +319,6 @@ onMounted(() => {
</section>
</ListPageLayout>
<LoadingListDetailDrawer
v-if="!isPreload && (detailLoading || detailError || detail)"
class="confirmed-inline-detail surface-panel"
:mode="mode"
:loading="detailLoading"
:detail="detail"
:error="detailError"
:can-edit="canEdit"
:can-manage-customs="canManageCustoms"
:customs-saving="customsSaving"
@retry="loadDetail"
@edit="openEdit"
@attach-customs="openCustomsDocument"
@complete-customs="completeCustomsDocument"
/>
<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><LoadErrorState v-if="customsError" compact title="操作失败" :message="customsError.message" :show-retry="false"><template #details><ApiErrorMeta :details="customsError" /></template></LoadErrorState></AppDialog>
<AppDialog
v-model="editOpen"
class="loading-transport-dialog"
@@ -406,11 +346,10 @@ onMounted(() => {
<style scoped>
.loading-lists-page { display: grid; min-width: 0; gap: 16px; }
.table-panel { min-width: 0; overflow: hidden; border: 1px solid var(--cc-border); background: #fff; }
.confirmed-inline-detail { padding: 20px; border: 1px solid var(--cc-border); background: #fff; }
.transport-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; margin-top: 16px; }
.transport-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
.row-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.row-actions :deep(.el-button) { margin-left: 0; }
:global(.loading-transport-dialog.el-dialog) { max-height: 90vh; overflow: hidden; }
@media (max-width: 720px) { .confirmed-inline-detail { padding: 12px; } .transport-form { grid-template-columns: 1fr; } .transport-form :deep(.el-form-item:last-child) { grid-column: auto; } .row-actions { gap: 4px; } }
@media (max-width: 720px) { .transport-form { grid-template-columns: 1fr; } .transport-form :deep(.el-form-item:last-child) { grid-column: auto; } .row-actions { gap: 4px; } }
</style>