116 lines
5.9 KiB
Vue
116 lines
5.9 KiB
Vue
<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">
|
||
<el-form class="density-form" label-position="left" label-width="160px">
|
||
<div class="workbench-evidence"><table class="density-table" 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>
|