优化
This commit is contained in:
@@ -5,6 +5,8 @@ import { resolve } from 'node:path'
|
|||||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/components/LoadingListsView.vue'), 'utf8')
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/components/LoadingListsView.vue'), 'utf8')
|
||||||
const detailSource = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/components/LoadingListDetailDrawer.vue'), 'utf8')
|
const detailSource = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/components/LoadingListDetailDrawer.vue'), 'utf8')
|
||||||
const preloadDetailSource = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/pages/PreloadPlanDetailView.vue'), 'utf8')
|
const preloadDetailSource = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/pages/PreloadPlanDetailView.vue'), 'utf8')
|
||||||
|
const confirmedDetailSource = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/pages/ConfirmedLoadingListDetailView.vue'), 'utf8')
|
||||||
|
const routesSource = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/routes.ts'), 'utf8')
|
||||||
|
|
||||||
describe('LoadingListsView', () => {
|
describe('LoadingListsView', () => {
|
||||||
it('filters both list modes by an active name-only customer selector', () => {
|
it('filters both list modes by an active name-only customer selector', () => {
|
||||||
@@ -41,12 +43,16 @@ describe('LoadingListsView', () => {
|
|||||||
expect(detailSource).toContain('ContainerFeesPanel')
|
expect(detailSource).toContain('ContainerFeesPanel')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens preload detail as a route and confirmed detail inline on the list page', () => {
|
it('opens both preload and confirmed details as independent route pages', () => {
|
||||||
expect(source).toContain("name: 'preload-plan-detail'")
|
expect(source).toContain("name: 'preload-plan-detail'")
|
||||||
expect(source).toContain('v-if="!isPreload && (detailLoading || detailError || detail)"')
|
expect(source).toContain("name: 'loading-list-detail'")
|
||||||
expect(source).toContain('class="confirmed-inline-detail surface-panel"')
|
expect(source).not.toContain('confirmed-inline-detail')
|
||||||
expect(preloadDetailSource).toContain('返回列表')
|
expect(preloadDetailSource).toContain('返回列表')
|
||||||
expect(preloadDetailSource).toContain('router.back()')
|
expect(preloadDetailSource).toContain('router.back()')
|
||||||
|
expect(confirmedDetailSource).toContain('返回列表')
|
||||||
|
expect(confirmedDetailSource).toContain('router.back()')
|
||||||
|
expect(routesSource).toContain("path: 'loading-lists/:planId'")
|
||||||
|
expect(routesSource).toContain("name: 'loading-list-detail'")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps the real transport edit contract in a 620px centered modal', () => {
|
it('keeps the real transport edit contract in a 620px centered modal', () => {
|
||||||
@@ -88,8 +94,9 @@ describe('LoadingListsView', () => {
|
|||||||
expect(source).toContain('useRoute()')
|
expect(source).toContain('useRoute()')
|
||||||
expect(source).toContain('route.query.planId')
|
expect(source).toContain('route.query.planId')
|
||||||
expect(source).toContain('route.query.warehouseId')
|
expect(source).toContain('route.query.warehouseId')
|
||||||
expect(source).toContain("const expectedStatuses = isPreload.value ? ['EDITING', 'LOADING'] : ['COMPLETED']")
|
expect(source).toContain("router.replace({ name: 'loading-list-detail'")
|
||||||
expect(source).toContain('MODE_STATUS_MISMATCH')
|
expect(confirmedDetailSource).toContain("result.summary.status !== 'COMPLETED'")
|
||||||
|
expect(confirmedDetailSource).toContain('MODE_STATUS_MISMATCH')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('passes translated status text directly to the badge', () => {
|
it('passes translated status text directly to the badge', () => {
|
||||||
@@ -125,12 +132,14 @@ describe('LoadingListsView', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('uploads declaration documents from a file picker and freezes them after completion', () => {
|
it('uploads declaration documents from a file picker and freezes them after completion', () => {
|
||||||
expect(source).toContain('<el-upload :auto-upload="false" :limit="1"')
|
expect(confirmedDetailSource).toContain('<el-upload :auto-upload="false" :limit="1"')
|
||||||
expect(source).toContain('@confirm="uploadCustomsDocument"')
|
expect(confirmedDetailSource).toContain('@confirm="uploadCustomsDocument"')
|
||||||
expect(source).toContain('uploadLoadingDeclaration(selectedPlanId.value, selectedWarehouseId.value, customsFile.value')
|
expect(confirmedDetailSource).toContain('uploadLoadingDeclaration(')
|
||||||
|
expect(confirmedDetailSource).toContain('getLoadingDeclaration(planId.value, warehouseId.value)')
|
||||||
|
expect(confirmedDetailSource).toContain("details.code === 'A0404'")
|
||||||
expect(detailSource).toContain("canManageCustoms && !detail.customsDocument?.completedAt")
|
expect(detailSource).toContain("canManageCustoms && !detail.customsDocument?.completedAt")
|
||||||
expect(source).not.toContain('文件 ID')
|
expect(confirmedDetailSource).not.toContain('文件 ID')
|
||||||
expect(source).not.toContain('fileId')
|
expect(confirmedDetailSource).not.toContain('fileId')
|
||||||
expect(source).not.toContain('上传入口待')
|
expect(confirmedDetailSource).not.toContain('上传入口待')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage, type FormInstance, type FormRules, type UploadFile } from 'element-plus'
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||||
import { Eye, FileUp, Pencil } from '@lucide/vue'
|
import { Eye, Pencil } from '@lucide/vue'
|
||||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
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 QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||||
import { loadingListStatusLabel } from '../status'
|
import { loadingListStatusLabel } from '../status'
|
||||||
import LoadingListDetailDrawer from './LoadingListDetailDrawer.vue'
|
|
||||||
import {
|
import {
|
||||||
uploadLoadingDeclaration,
|
|
||||||
completeLoadingDeclaration,
|
|
||||||
getConfirmedLoadingList,
|
|
||||||
getPreloadPlan,
|
getPreloadPlan,
|
||||||
listConfirmedLoadingLists,
|
listConfirmedLoadingLists,
|
||||||
listPreloadPlans,
|
listPreloadPlans,
|
||||||
@@ -56,10 +52,6 @@ const editError = ref<ApiErrorDetails | null>(null)
|
|||||||
const editFormRef = ref<FormInstance>()
|
const editFormRef = ref<FormInstance>()
|
||||||
const editForm = reactive({ containerNo: '', voyageNo: '', operationReason: '' })
|
const editForm = reactive({ containerNo: '', voyageNo: '', operationReason: '' })
|
||||||
const handledDeepLink = ref('')
|
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 isPreload = computed(() => props.mode === 'preload')
|
||||||
const title = computed(() => isPreload.value ? '预装清单' : '装柜清单')
|
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 canList = computed(() => access.can(listPermission.value))
|
||||||
const canView = computed(() => canList.value)
|
const canView = computed(() => canList.value)
|
||||||
const canEdit = computed(() => isPreload.value && access.can('platform:firstleg:preload:edit'))
|
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[]>(() => [
|
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: '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 },
|
{ 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 } })
|
await router.replace({ name: 'preload-plan-detail', params: { planId }, query: { warehouseId } })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selectedPlanId.value = planId
|
await router.replace({ name: 'loading-list-detail', params: { planId }, query: { warehouseId } })
|
||||||
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}状态,请从对应清单入口查看。`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function search() {
|
function search() {
|
||||||
@@ -200,9 +178,7 @@ async function loadDetail() {
|
|||||||
detailLoading.value = true
|
detailLoading.value = true
|
||||||
detailError.value = null
|
detailError.value = null
|
||||||
try {
|
try {
|
||||||
detail.value = isPreload.value
|
detail.value = await getPreloadPlan(selectedPlanId.value, selectedWarehouseId.value)
|
||||||
? await getPreloadPlan(selectedPlanId.value, selectedWarehouseId.value)
|
|
||||||
: await getConfirmedLoadingList(selectedPlanId.value, selectedWarehouseId.value)
|
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
detailError.value = getApiErrorDetails(cause, '清单详情加载失败,请稍后重试')
|
detailError.value = getApiErrorDetails(cause, '清单详情加载失败,请稍后重试')
|
||||||
} finally {
|
} 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 || '') } })
|
void router.push({ name: 'preload-plan-detail', params: { planId: String(row.planId) }, query: { warehouseId: String(row.warehouseId || '') } })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selectedPlanId.value = String(row.planId)
|
void router.push({ name: 'loading-list-detail', params: { planId: String(row.planId) }, query: { warehouseId: String(row.warehouseId || '') } })
|
||||||
selectedWarehouseId.value = String(row.warehouseId || '')
|
|
||||||
detail.value = null
|
|
||||||
detailError.value = null
|
|
||||||
void loadDetail()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openEditFromRow(rowValue: object) {
|
async function openEditFromRow(rowValue: object) {
|
||||||
@@ -287,20 +259,6 @@ async function loadCustomers() {
|
|||||||
customersLoading.value = false
|
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([page, pageSize], () => void load())
|
||||||
watch(() => [route.query.planId, route.query.warehouseId], () => void openDeepLinkedDetail())
|
watch(() => [route.query.planId, route.query.warehouseId], () => void openDeepLinkedDetail())
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -361,24 +319,6 @@ onMounted(() => {
|
|||||||
</section>
|
</section>
|
||||||
</ListPageLayout>
|
</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
|
<AppDialog
|
||||||
v-model="editOpen"
|
v-model="editOpen"
|
||||||
class="loading-transport-dialog"
|
class="loading-transport-dialog"
|
||||||
@@ -406,11 +346,10 @@ onMounted(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.loading-lists-page { display: grid; min-width: 0; gap: 16px; }
|
.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; }
|
.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 { 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; }
|
.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 { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
|
||||||
.row-actions :deep(.el-button) { margin-left: 0; }
|
.row-actions :deep(.el-button) { margin-left: 0; }
|
||||||
:global(.loading-transport-dialog.el-dialog) { max-height: 90vh; overflow: hidden; }
|
: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>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ArrowLeft, 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 ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||||
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||||
|
import LoadErrorState from '@/shared/components/LoadErrorState.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)
|
||||||
|
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() {
|
||||||
|
if (!planId.value || !warehouseId.value) {
|
||||||
|
detail.value = null
|
||||||
|
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 (result.summary.status !== 'COMPLETED') {
|
||||||
|
detail.value = null
|
||||||
|
error.value = { code: 'MODE_STATUS_MISMATCH', message: '该计划尚未形成正式装柜清单,请从预装清单入口查看。' }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
detail.value = { ...result, customsDocument }
|
||||||
|
} catch (cause) {
|
||||||
|
detail.value = null
|
||||||
|
error.value = getApiErrorDetails(cause, '装柜清单详情加载失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
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())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AdminPage>
|
||||||
|
<div class="confirmed-loading-detail-page">
|
||||||
|
<el-button link type="primary" class="back-button" @click="goBack"><ArrowLeft :size="16" />返回列表</el-button>
|
||||||
|
<LoadErrorState v-if="error && !detail" title="装柜清单详情加载失败" :message="error.message" :loading="loading" @retry="loadDetail">
|
||||||
|
<template #details><ApiErrorMeta :details="error" /></template>
|
||||||
|
</LoadErrorState>
|
||||||
|
<LoadingListDetailDrawer
|
||||||
|
v-else
|
||||||
|
mode="confirmed"
|
||||||
|
:loading="loading"
|
||||||
|
:detail="detail"
|
||||||
|
:error="error"
|
||||||
|
:can-edit="false"
|
||||||
|
:can-manage-customs="canManageCustoms"
|
||||||
|
:customs-saving="customsSaving"
|
||||||
|
@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>
|
||||||
|
<LoadErrorState v-if="customsError" compact title="操作失败" :message="customsError.message" :show-retry="false">
|
||||||
|
<template #details><ApiErrorMeta :details="customsError" /></template>
|
||||||
|
</LoadErrorState>
|
||||||
|
</AppDialog>
|
||||||
|
</AdminPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.confirmed-loading-detail-page { display: grid; min-width: 0; max-width: 1280px; gap: 16px; }
|
||||||
|
.back-button { width: fit-content; }
|
||||||
|
</style>
|
||||||
@@ -31,4 +31,14 @@ export const loadingListRoutes = [
|
|||||||
permission: 'platform:firstleg:loading-list:list',
|
permission: 'platform:firstleg:loading-list:list',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'loading-lists/:planId',
|
||||||
|
name: 'loading-list-detail',
|
||||||
|
component: () => import('./pages/ConfirmedLoadingListDetailView.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '装柜清单详情',
|
||||||
|
description: '查看装柜清单基本信息、货物明细、费用和操作历史',
|
||||||
|
permission: 'platform:firstleg:loading-list:list',
|
||||||
|
},
|
||||||
|
},
|
||||||
] satisfies RouteRecordRaw[]
|
] satisfies RouteRecordRaw[]
|
||||||
|
|||||||
Reference in New Issue
Block a user