Files
admin-web/src/modules/container-loading/components/OceanBookingLinkDialog.vue
T
2026-09-10 14:14:24 +08:00

115 lines
5.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { listOceanBookingLinkCandidates } from '@/modules/ocean-bookings'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { linkContainerOceanBooking } from '../api/platformContainerLoading'
import type { ContainerBookingResource } from '../types'
const props = defineProps<{ modelValue: boolean; container: ContainerBookingResource | null }>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
linked: [container: ContainerBookingResource]
}>()
const loading = ref(false)
const saving = ref(false)
const error = ref<ApiErrorDetails | null>(null)
const candidateKey = ref('')
const containerNo = ref('')
const candidates = ref<Array<{ key: string; oceanBookingId: string; requirementId: string; label: string }>>([])
let requestSequence = 0
function optional(value: string | null | undefined) { return value?.trim() || null }
function normalizeContainerNo(value: string | null | undefined) { return String(value || '').trim().toUpperCase() }
function close() { emit('update:modelValue', false) }
async function loadCandidates() {
const target = props.container
candidateKey.value = ''
containerNo.value = ''
candidates.value = []
error.value = null
if (!target) return
const warehouseId = optional(target.warehouseId)
const channelCode = optional(target.channelCode)
const containerType = optional(target.containerType)
if (!warehouseId || !channelCode || !containerType) {
error.value = { message: '当前模拟柜缺少仓库、渠道或柜型,无法查询可关联订舱' }
return
}
const sequence = ++requestSequence
loading.value = true
try {
const result = await listOceanBookingLinkCandidates({ warehouseId, channelCode, containerType, page: 1, size: 200 })
if (sequence !== requestSequence || !props.modelValue || props.container?.id !== target.id) return
candidates.value = result.records.map((candidate) => ({
key: `${candidate.oceanBookingId}:${candidate.requirementId}`,
oceanBookingId: candidate.oceanBookingId,
requirementId: candidate.requirementId,
label: `${candidate.bookingNo} · ${candidate.containerType} × ${candidate.remainingQuantity} · ${candidate.customerName} · ${candidate.warehouseName} · ${candidate.channelName}`,
}))
} catch (cause) {
if (sequence === requestSequence) error.value = getApiErrorDetails(cause, '已放舱订舱候选加载失败')
} finally {
if (sequence === requestSequence) loading.value = false
}
}
async function save() {
const target = props.container
const candidate = candidates.value.find((item) => item.key === candidateKey.value)
const normalized = normalizeContainerNo(containerNo.value)
containerNo.value = normalized
if (!target || !candidate) {
error.value = { message: '请选择规格一致且仍有剩余数量的柜型需求' }
return
}
saving.value = true
error.value = null
try {
const linked = await linkContainerOceanBooking(target.id, {
oceanBookingId: candidate.oceanBookingId,
requirementId: candidate.requirementId,
containerNo: normalized || null,
expectedVersion: target.version,
})
close()
ElMessage.success(normalized ? '订舱与真实集装箱号已关联,柜次号、装柜计划和明细保持不变' : '订舱已关联,真实集装箱号待司机提柜时录入')
emit('linked', linked)
} catch (cause) {
error.value = getApiErrorDetails(cause, '关联真实订舱柜失败')
} finally { saving.value = false }
}
watch(() => props.modelValue, (open) => {
if (open) void loadCandidates()
else {
requestSequence += 1
loading.value = false
}
}, { immediate: true })
defineExpose({ candidates, candidateKey, containerNo, save })
</script>
<template>
<AppDialog :model-value="modelValue" title="关联已放舱订舱" width="min(680px, calc(100vw - 28px))" confirm-text="确认关联" cancel-text="取消" :confirm-loading="saving" :confirm-disabled="!candidateKey" @update:model-value="emit('update:modelValue', $event)" @confirm="save">
<el-alert title="关联后保留当前柜次号、装柜计划和明细,并补充订舱快照;真实集装箱号可待司机提柜时录入。" type="info" :closable="false" />
<el-form label-position="top">
<el-form-item label="柜次号"><el-input :model-value="container?.cabinetNo || ''" disabled /></el-form-item>
<el-form-item label="已放舱订舱及柜型需求" required><el-select v-model="candidateKey" filterable :loading="loading" placeholder="请选择仍有剩余数量的柜型需求" no-data-text="暂无符合当前装柜仓库、物流渠道和柜型的已放舱需求"><el-option v-for="candidate in candidates" :key="candidate.key" :label="candidate.label" :value="candidate.key" /></el-select></el-form-item>
<el-form-item label="真实集装箱号(可选)"><el-input v-model="containerNo" maxlength="64" placeholder="如未取得可留空,后续显示待补录" @blur="containerNo = normalizeContainerNo(containerNo)" /></el-form-item>
</el-form>
<p class="dialog-hint">仓库、渠道和柜型必须与当前模拟柜一致;容量和限重仅展示柜型固定快照,不影响关联资格。</p>
<LoadErrorState v-if="error" compact title="关联失败" :message="error.message" :show-retry="false"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
</AppDialog>
</template>
<style scoped>
.dialog-hint { margin: 4px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.6; }
</style>