Files
admin-web/src/modules/container-loading/components/OceanBookingLinkDialog.vue
T
2026-10-20 23:34:11 +08:00

116 lines
6.0 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 '@/styles/adminDensity.css'
import '@/styles/adminWorkbench.css'
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 { containerDialogTitle } from '../containerDialogTitle'
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 = normalizeContainerNo(target?.containerNo)
candidates.value = []
error.value = null
if (!target) return
const warehouseId = optional(target.warehouseId)
const channelGroupCode = optional(target.channelGroupCode)
const containerType = optional(target.containerType)
if (!warehouseId || !channelGroupCode || !containerType) {
error.value = { message: '当前模拟柜缺少仓库、计划渠道或柜型,无法查询可关联订舱' }
return
}
const sequence = ++requestSequence
loading.value = true
try {
const result = await listOceanBookingLinkCandidates({ warehouseId, channelGroupCode, 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.billOfLadingNo} · ${candidate.containerType} × ${candidate.remainingQuantity} · ${candidate.customerName?.trim() || '未关联客户'} · ${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 class="admin-density-dialog admin-workbench is-dialog" :model-value="modelValue" :title="containerDialogTitle('关联已放舱订舱', container)" fullscreenable 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">
<section class="density-dialog-section" data-tone="work">
<el-form class="density-form" label-position="left" label-width="160px">
<div class="workbench-evidence"><table class="density-table density-table--editable" aria-label="关联订舱资料"><tbody>
<tr><td><el-form-item label="柜次号"><span>{{ container?.cabinetNo || '—' }}</span></el-form-item></td></tr>
<tr><td><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></td></tr>
<tr><td><el-form-item label="真实集装箱号(可选)"><el-input v-model="containerNo" maxlength="64" placeholder="如未取得可留空,后续显示待补录" @blur="containerNo = normalizeContainerNo(containerNo)" /></el-form-item></td></tr>
</tbody></table></div>
</el-form>
</section>
<LoadErrorState v-if="error" compact title="关联失败" :message="error.message" :show-retry="false"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
</AppDialog>
</template>