优化
This commit is contained in:
@@ -20,13 +20,12 @@ import { listSuppliers, type Supplier } from '@/modules/suppliers'
|
||||
import { listPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses'
|
||||
import {
|
||||
createOceanBooking,
|
||||
deleteOceanBookingDraft,
|
||||
getOceanBooking,
|
||||
performOceanBookingAction,
|
||||
releaseOceanBooking,
|
||||
requestOceanBookingChange,
|
||||
listWarehouseLogisticsChannels,
|
||||
updateOceanBookingDraft,
|
||||
type OceanLogisticsChannel,
|
||||
type OceanBookingAction,
|
||||
} from '../api/platformOceanBookings'
|
||||
import type {
|
||||
OceanBookingDetail,
|
||||
@@ -36,7 +35,7 @@ import type {
|
||||
OceanBookingStatus,
|
||||
} from '../types'
|
||||
import { oceanBookingStatusLabel } from '../status'
|
||||
import { bookingContainerError, createBlankBookingContainer, selectBookingContainerType } from '../containerRequirements'
|
||||
import { containerRequirementError, createBlankContainerRequirement, selectContainerRequirementType } from '../containerRequirements'
|
||||
import { isActiveBookingAgent } from '../bookingAgents'
|
||||
|
||||
const partyTypes = [
|
||||
@@ -49,20 +48,6 @@ const containerKindOptions = [
|
||||
{ value: 'FCL', label: '整箱 FCL' },
|
||||
{ value: 'LCL', label: '拼箱 LCL' },
|
||||
]
|
||||
const actionLabels: Record<OceanBookingAction, string> = {
|
||||
release: '确认放舱', cancel: '取消订舱',
|
||||
'change-request': '申请改船', 'change-confirm': '确认改船', 'change-fail': '改船失败',
|
||||
}
|
||||
const actionPermissions: Record<OceanBookingAction, string> = {
|
||||
release: 'platform:firstleg:ocean-booking:release',
|
||||
cancel: 'platform:firstleg:ocean-booking:cancel', 'change-request': 'platform:firstleg:ocean-booking:change',
|
||||
'change-confirm': 'platform:firstleg:ocean-booking:change', 'change-fail': 'platform:firstleg:ocean-booking:change',
|
||||
}
|
||||
const actionsByStatus: Partial<Record<OceanBookingStatus, OceanBookingAction[]>> = {
|
||||
CONFIRMED: ['release', 'change-request', 'cancel'],
|
||||
CHANGE_PENDING: ['change-confirm', 'change-fail', 'cancel'],
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const access = useAccessPreviewStore()
|
||||
@@ -100,30 +85,16 @@ function blankData(): OceanBookingDraftData {
|
||||
return {
|
||||
customerId: null, customerName: null, channelCode: '', warehouseId: '', serviceTerm: null, freightTerm: null, parties: blankParties(),
|
||||
billOfLadingNo: '', vesselName: '', voyageNo: '', estimatedSailingAt: null, estimatedArrivalAt: null,
|
||||
containers: [createBlankBookingContainer()],
|
||||
containerRequirements: [createBlankContainerRequirement()],
|
||||
}
|
||||
}
|
||||
const form = reactive<OceanBookingSaveRequest>({ expectedVersion: 0, data: blankData() })
|
||||
const rules: FormRules = {
|
||||
warehouseId: [{ required: true, message: '请选择装柜仓库', trigger: 'change' }],
|
||||
channelCode: [{ required: true, message: '请选择物流渠道', trigger: 'change' }],
|
||||
billOfLadingNo: [{ required: true, message: '请输入提单号', trigger: 'blur' }],
|
||||
vesselName: [{ required: true, message: '请输入船名', trigger: 'blur' }],
|
||||
voyageNo: [{ required: true, message: '请输入航次', trigger: 'blur' }],
|
||||
estimatedSailingAt: [{ required: true, message: '请选择预计开船时间', trigger: 'change' }],
|
||||
estimatedArrivalAt: [{ required: true, message: '请选择预计到港时间', trigger: 'change' }],
|
||||
serviceTerm: [{ required: true, message: '请输入主要运输条款 Service Term', trigger: 'blur' }],
|
||||
freightTerm: [{ required: true, message: '请输入运费条款 Freight Term', trigger: 'blur' }],
|
||||
}
|
||||
const editable = computed(() => isNew.value)
|
||||
const canSave = computed(() => isNew.value && access.can('platform:firstleg:ocean-booking:create'))
|
||||
const availableActions = computed(() => (actionsByStatus[detail.value?.status || 'CONFIRMED'] || [])
|
||||
.filter((action) => access.can(actionPermissions[action])))
|
||||
|
||||
const changeDialogOpen = ref(false)
|
||||
const changeForm = reactive({ vesselName: '', voyageNo: '', estimatedSailingAt: null as string | null, estimatedArrivalAt: null as string | null, reason: '' })
|
||||
const rules: FormRules = {}
|
||||
const editable = computed(() => isNew.value || detail.value?.status === 'DRAFT')
|
||||
const canSave = computed(() => editable.value && access.can(isNew.value ? 'platform:firstleg:ocean-booking:create' : 'platform:firstleg:ocean-booking:edit'))
|
||||
const canDelete = computed(() => detail.value?.status === 'DRAFT' && access.can('platform:firstleg:ocean-booking:delete'))
|
||||
const canRelease = computed(() => detail.value?.status === 'DRAFT' && access.can('platform:firstleg:ocean-booking:release'))
|
||||
const releaseDialogOpen = ref(false)
|
||||
const releaseReason = ref('')
|
||||
|
||||
function statusLabel(status: OceanBookingStatus | string | null | undefined) {
|
||||
return oceanBookingStatusLabel(status)
|
||||
@@ -162,10 +133,10 @@ function normalizeDraftData() {
|
||||
form.data.billOfLadingNo = form.data.billOfLadingNo.trim()
|
||||
form.data.vesselName = form.data.vesselName.trim()
|
||||
form.data.voyageNo = form.data.voyageNo.trim()
|
||||
form.data.containers.forEach((container) => { container.containerNo = container.containerNo.trim() })
|
||||
form.data.containerRequirements.forEach((requirement) => { requirement.quantity = Number(requirement.quantity) })
|
||||
}
|
||||
function selectWarehouse(value: unknown) {
|
||||
const warehouseId = String(value || '')
|
||||
const warehouseId = value == null || value === '' ? null : String(value)
|
||||
form.data.warehouseId = warehouseId
|
||||
form.data.channelCode = ''
|
||||
selectedChannelName.value = ''
|
||||
@@ -215,7 +186,7 @@ function applyDetail(value: OceanBookingDetail) {
|
||||
placeOfDelivery: value.placeOfDelivery, serviceTerm: value.serviceTerm, freightTerm: value.freightTerm,
|
||||
remark: value.remark,
|
||||
parties: value.parties.length ? value.parties.map(({ partyType, partyName, contactName, contactPhone, address }) => ({ partyType, partyName, contactName, contactPhone, address })) : blankParties(),
|
||||
containers: value.containers.length ? value.containers.map(({ containerNo, containerType, containerKind, capacityCbm, maxWeightKg }) => ({ containerNo, containerType, containerKind, capacityCbm, maxWeightKg })) : [createBlankBookingContainer()],
|
||||
containerRequirements: value.containerRequirements.length ? value.containerRequirements.map(({ requirementId, containerType, containerKind, quantity, remainingQuantity, capacityCbm, maxWeightKg }) => ({ requirementId, containerType, containerKind, quantity, remainingQuantity, capacityCbm, maxWeightKg })) : [createBlankContainerRequirement()],
|
||||
}
|
||||
selectedChannelName.value = value.channelName || ''
|
||||
channels.value = []
|
||||
@@ -233,111 +204,77 @@ async function save() {
|
||||
if (!canSave.value) return
|
||||
actionError.value = null
|
||||
normalizeDraftData()
|
||||
if (!await formRef.value?.validate().catch(() => false)) {
|
||||
actionError.value = { message: '请完善装柜仓库、物流渠道、真实运输资料和运输条款等必填信息' }
|
||||
return
|
||||
}
|
||||
if (form.data.estimatedArrivalAt! < form.data.estimatedSailingAt!) {
|
||||
actionError.value = { message: '预计到港时间不能早于预计开船时间' }
|
||||
return
|
||||
}
|
||||
if (form.data.parties.some((party) => !party.partyName.trim())) {
|
||||
actionError.value = { message: '发货人、收货人和通知人名称均为必填项' }
|
||||
return
|
||||
}
|
||||
if (!validateBookingContainers()) return
|
||||
const requirementMessage = containerRequirementError(form.data.containerRequirements)
|
||||
if (requirementMessage) { actionError.value = { message: requirementMessage }; return }
|
||||
saving.value = true
|
||||
actionError.value = null
|
||||
try {
|
||||
const payload = { ...form.data }
|
||||
const created = await createOceanBooking({ expectedVersion: 0, data: payload })
|
||||
ElMessage.success('线下订舱资料已登记并确认')
|
||||
await router.replace(`/ocean-bookings/${created.id}`)
|
||||
applyDetail(created)
|
||||
const payload = {
|
||||
...form.data,
|
||||
channelCode: form.data.channelCode?.trim() || null,
|
||||
warehouseId: form.data.warehouseId?.trim() || null,
|
||||
parties: form.data.parties.filter((party) => party.partyName.trim()).map((party) => ({ ...party, partyName: party.partyName.trim() })),
|
||||
containerRequirements: form.data.containerRequirements.map(({ containerType, containerKind, quantity, capacityCbm, maxWeightKg }) => ({ containerType, containerKind, quantity, capacityCbm, maxWeightKg })),
|
||||
}
|
||||
const saved = isNew.value
|
||||
? await createOceanBooking({ expectedVersion: 0, data: payload })
|
||||
: await updateOceanBookingDraft(id.value, { expectedVersion: detail.value?.version || 0, data: payload })
|
||||
ElMessage.success('订舱草稿已保存')
|
||||
if (isNew.value) await router.replace(`/ocean-bookings/${saved.id}`)
|
||||
applyDetail(saved)
|
||||
} catch (cause) { actionError.value = getApiErrorDetails(cause) }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
async function requiredReason(action: OceanBookingAction) {
|
||||
if (!['cancel', 'change-fail'].includes(action)) return null
|
||||
const result = await ElMessageBox.prompt(`请填写“${actionLabels[action]}”原因,服务端将写入状态历史。`, actionLabels[action], {
|
||||
inputValidator: (value) => Boolean(value?.trim()) || '原因不能为空',
|
||||
confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning',
|
||||
})
|
||||
return result.value.trim()
|
||||
}
|
||||
function openChangeDialog() {
|
||||
changeForm.vesselName = detail.value?.vesselName || ''
|
||||
changeForm.voyageNo = detail.value?.voyageNo || ''
|
||||
changeForm.estimatedSailingAt = detail.value?.estimatedSailingAt || null
|
||||
changeForm.estimatedArrivalAt = detail.value?.estimatedArrivalAt || null
|
||||
changeForm.reason = ''
|
||||
changeDialogOpen.value = true
|
||||
}
|
||||
function openReleaseDialog() {
|
||||
releaseReason.value = ''
|
||||
normalizeDraftData()
|
||||
const validationError = releaseValidationError()
|
||||
if (validationError) { actionError.value = { message: validationError }; return }
|
||||
releaseDialogOpen.value = true
|
||||
}
|
||||
function releaseValidationError() {
|
||||
const requiredFields: Array<[unknown, string]> = [
|
||||
[form.data.customerId, '客户'], [form.data.warehouseId, '装柜仓库'], [form.data.channelCode, '物流渠道'],
|
||||
[form.data.billOfLadingNo, '提单号'], [form.data.vesselName, '船名'], [form.data.voyageNo, '航次'],
|
||||
[form.data.estimatedSailingAt, '预计开船时间'], [form.data.estimatedArrivalAt, '预计到港时间'],
|
||||
[form.data.serviceTerm, 'Service Term'], [form.data.freightTerm, 'Freight Term'],
|
||||
[form.data.shippingCompanyId, '船公司'], [form.data.bookingAgentId, '订舱代理'],
|
||||
]
|
||||
const missing = requiredFields.find(([value]) => !String(value || '').trim())
|
||||
if (missing) return `放舱前请填写${missing[1]}`
|
||||
if (form.data.estimatedArrivalAt! < form.data.estimatedSailingAt!) return '预计到港时间不能早于预计开船时间'
|
||||
if (form.data.parties.some((party) => !party.partyName.trim())) return '放舱前请填写发货人、收货人和通知人名称'
|
||||
return containerRequirementError(form.data.containerRequirements)
|
||||
}
|
||||
async function submitRelease() {
|
||||
if (!detail.value) return
|
||||
const billOfLadingNo = detail.value.billOfLadingNo?.trim()
|
||||
const vesselName = detail.value.vesselName?.trim()
|
||||
const voyageNo = detail.value.voyageNo?.trim()
|
||||
const estimatedSailingAt = detail.value.estimatedSailingAt
|
||||
const estimatedArrivalAt = detail.value.estimatedArrivalAt
|
||||
if (!billOfLadingNo || !vesselName || !voyageNo || !estimatedSailingAt || !estimatedArrivalAt) {
|
||||
actionError.value = getApiErrorDetails(new Error('当前订舱缺少已确认运输资料,无法放舱'))
|
||||
return
|
||||
}
|
||||
if (estimatedArrivalAt < estimatedSailingAt) {
|
||||
actionError.value = getApiErrorDetails(new Error('预计到港时间不能早于预计开船时间'))
|
||||
return
|
||||
}
|
||||
const validationError = releaseValidationError()
|
||||
if (validationError) { actionError.value = { message: validationError }; releaseDialogOpen.value = false; return }
|
||||
actionSaving.value = true
|
||||
actionError.value = null
|
||||
try {
|
||||
applyDetail(await releaseOceanBooking(id.value, { expectedVersion: detail.value.version, billOfLadingNo, vesselName, voyageNo, estimatedSailingAt, estimatedArrivalAt, reason: releaseReason.value.trim() || null }))
|
||||
applyDetail(await releaseOceanBooking(id.value, { expectedVersion: detail.value.version }))
|
||||
releaseDialogOpen.value = false
|
||||
ElMessage.success('确认放舱成功')
|
||||
} catch (cause) { actionError.value = getApiErrorDetails(cause) }
|
||||
finally { actionSaving.value = false }
|
||||
}
|
||||
async function submitChange() {
|
||||
if (!detail.value || !changeForm.vesselName.trim() || !changeForm.voyageNo.trim() || !changeForm.reason.trim()) return
|
||||
actionSaving.value = true
|
||||
async function removeDraft() {
|
||||
if (!detail.value || !canDelete.value) return
|
||||
actionError.value = null
|
||||
try {
|
||||
applyDetail(await requestOceanBookingChange(id.value, { expectedVersion: detail.value.version, ...changeForm }))
|
||||
changeDialogOpen.value = false
|
||||
ElMessage.success('改船申请已提交')
|
||||
} catch (cause) { actionError.value = getApiErrorDetails(cause) }
|
||||
finally { actionSaving.value = false }
|
||||
}
|
||||
async function runAction(action: OceanBookingAction) {
|
||||
if (!detail.value || action === 'change-request') { if (action === 'change-request') openChangeDialog(); return }
|
||||
if (action === 'release') { openReleaseDialog(); return }
|
||||
actionError.value = null
|
||||
try {
|
||||
const reason = await requiredReason(action)
|
||||
await ElMessageBox.confirm(`确认执行“${actionLabels[action]}”吗?`, actionLabels[action], { confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' })
|
||||
await ElMessageBox.confirm('确认删除该订舱草稿?删除后无法恢复。', '删除草稿', { confirmButtonText: '确认删除', cancelButtonText: '取消', type: 'warning' })
|
||||
actionSaving.value = true
|
||||
applyDetail(await performOceanBookingAction(id.value, action, { expectedVersion: detail.value.version, reason }))
|
||||
ElMessage.success(`${actionLabels[action]}成功`)
|
||||
await deleteOceanBookingDraft(id.value, detail.value.version)
|
||||
ElMessage.success('订舱草稿已删除')
|
||||
await router.replace('/ocean-bookings')
|
||||
} catch (cause) {
|
||||
if (cause !== 'cancel' && !(cause instanceof Error && cause.message === 'cancel')) actionError.value = getApiErrorDetails(cause)
|
||||
} finally { actionSaving.value = false }
|
||||
}
|
||||
function validateBookingContainers() {
|
||||
const message = bookingContainerError(form.data.containers)
|
||||
if (message) {
|
||||
actionError.value = { message }
|
||||
return false
|
||||
}
|
||||
return true
|
||||
function addRequirement() {
|
||||
if (form.data.containerRequirements.length < 20) form.data.containerRequirements.push(createBlankContainerRequirement())
|
||||
}
|
||||
function addContainer() {
|
||||
if (form.data.containers.length < 20) form.data.containers.push(createBlankBookingContainer())
|
||||
}
|
||||
function removeContainer(index: number) { if (form.data.containers.length > 1) form.data.containers.splice(index, 1) }
|
||||
function removeRequirement(index: number) { if (form.data.containerRequirements.length > 1) form.data.containerRequirements.splice(index, 1) }
|
||||
|
||||
onMounted(() => { void load(); void loadOptions() })
|
||||
</script>
|
||||
@@ -349,7 +286,7 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
<template #header>
|
||||
<div class="detail-heading">
|
||||
<el-button @click="router.push('/ocean-bookings')"><ArrowLeft :size="16" />返回列表</el-button>
|
||||
<PageToolbar class="booking-page-toolbar" as="section" :title="detail?.bookingNo || '订舱资料'" :description="detail ? `${detail.customerName || '未关联客户'} · ${detail.pol || '—'} → ${detail.pod || '—'}` : '登记线下已确认的真实订舱资料'">
|
||||
<PageToolbar class="booking-page-toolbar" as="section" :title="detail?.bookingNo || '订舱草稿'" :description="detail ? `${detail.customerName || '未关联客户'} · ${detail.pol || '—'} → ${detail.pod || '—'}` : '先保存订舱草稿,资料完整后再确认放舱'">
|
||||
<template v-if="detail" #actions><div class="heading-status"><StatusBadge :status="statusLabel(detail.status)" /></div></template>
|
||||
</PageToolbar>
|
||||
</div>
|
||||
@@ -359,10 +296,11 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
<template v-else>
|
||||
<section class="surface-panel">
|
||||
<div class="section-title">
|
||||
<div><h2>订舱信息</h2><p v-if="detail && !editable">已确认订舱资料仅可查看,船期变更请使用“申请改船”。</p></div>
|
||||
<div><h2>订舱信息</h2><p v-if="detail?.status === 'DRAFT'">草稿可反复保存;放舱后资料同步至关联货柜并锁定。</p><p v-else-if="detail">已放舱资料只读。</p></div>
|
||||
<div class="actions">
|
||||
<el-button v-for="item in availableActions" :key="item" :loading="actionSaving" @click="runAction(item)">{{ actionLabels[item] }}</el-button>
|
||||
<el-button v-if="canSave" type="primary" :loading="saving" @click="save"><Save :size="16" />登记订舱</el-button>
|
||||
<el-button v-if="canDelete" type="danger" plain :loading="actionSaving" @click="removeDraft"><Trash2 :size="16" />删除草稿</el-button>
|
||||
<el-button v-if="canRelease" type="primary" plain :loading="actionSaving" @click="openReleaseDialog">放舱</el-button>
|
||||
<el-button v-if="canSave" type="primary" :loading="saving" @click="save"><Save :size="16" />保存草稿</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-alert v-if="optionsError" type="warning" :closable="false" show-icon :title="optionsError"><el-button link type="warning" :loading="optionsLoading" @click="loadOptions">重新加载选项</el-button></el-alert>
|
||||
@@ -370,10 +308,10 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
<section class="booking-group" aria-label="基本信息">
|
||||
<h3>基本信息</h3><div class="booking-group__body booking-group__body--fields">
|
||||
<el-form-item label="订舱号"><el-input :model-value="detail?.bookingNo || '保存后生成'" disabled /></el-form-item>
|
||||
<el-form-item label="客户(可选)"><el-select :model-value="form.data.customerId" filterable clearable placeholder="请选择客户(可选)" no-data-text="暂无可选客户" :loading="optionsLoading" style="width:100%" @update:model-value="selectCustomer"><el-option v-if="form.data.customerId && !selectableCustomers.some((customer) => String(customer.id) === String(form.data.customerId))" :label="form.data.customerName || '客户名称缺失'" :value="String(form.data.customerId)" /><el-option v-for="customer in selectableCustomers" :key="String(customer.id)" :label="customer.tenantName" :value="String(customer.id)" /></el-select></el-form-item>
|
||||
<el-form-item label="状态"><el-input :model-value="detail ? statusLabel(detail.status) : '登记后直接确认'" disabled /></el-form-item>
|
||||
<el-form-item label="客户(放舱前必填)"><el-select :model-value="form.data.customerId" filterable clearable placeholder="草稿可暂不选择" no-data-text="暂无可选客户" :loading="optionsLoading" style="width:100%" @update:model-value="selectCustomer"><el-option v-if="form.data.customerId && !selectableCustomers.some((customer) => String(customer.id) === String(form.data.customerId))" :label="form.data.customerName || '客户名称缺失'" :value="String(form.data.customerId)" /><el-option v-for="customer in selectableCustomers" :key="String(customer.id)" :label="customer.tenantName" :value="String(customer.id)" /></el-select></el-form-item>
|
||||
<el-form-item label="状态"><el-input :model-value="detail ? statusLabel(detail.status) : '保存后为草稿'" disabled /></el-form-item>
|
||||
<el-form-item label="装柜仓库" prop="warehouseId"><el-select :model-value="form.data.warehouseId" filterable placeholder="请选择装柜仓库" :loading="optionsLoading" style="width:100%" @update:model-value="selectWarehouse"><el-option v-for="warehouse in warehouses" :key="String(warehouse.id)" :label="warehouse.warehouseName" :value="String(warehouse.id)" /></el-select></el-form-item>
|
||||
<el-form-item label="物流渠道" prop="channelCode"><el-select :model-value="form.data.channelCode" filterable :disabled="!form.data.warehouseId || channelsLoading || !!channelsError || channelUnavailable" :loading="channelsLoading" :placeholder="form.data.warehouseId ? '请选择物流渠道' : '请先选择装柜仓库'" no-data-text="该仓库暂无有效物流渠道" style="width:100%" @update:model-value="selectChannel(String($event || ''))"><el-option v-if="form.data.channelCode && !channels.some((channel) => channel.channelCode === form.data.channelCode)" :label="selectedChannelName || form.data.channelCode" :value="form.data.channelCode" /><el-option v-for="channel in channels" :key="channel.channelCode" :label="channel.channelName" :value="channel.channelCode"><span>{{ channel.channelName }}</span><small class="channel-code">{{ channel.channelCode }}</small></el-option></el-select><div v-if="channelsError" class="field-error"><span>{{ channelsError }}</span><el-button link type="primary" :loading="channelsLoading" @click="loadChannels(form.data.warehouseId)">重新加载</el-button></div><div v-else-if="channelUnavailable" class="field-hint">该仓库暂无有效物流渠道,请更换仓库或联系管理员。</div></el-form-item>
|
||||
<el-form-item label="物流渠道" prop="channelCode"><el-select :model-value="form.data.channelCode" filterable :disabled="!form.data.warehouseId || channelsLoading || !!channelsError || channelUnavailable" :loading="channelsLoading" :placeholder="form.data.warehouseId ? '请选择物流渠道' : '请先选择装柜仓库'" no-data-text="该仓库暂无有效物流渠道" style="width:100%" @update:model-value="selectChannel(String($event || ''))"><el-option v-if="form.data.channelCode && !channels.some((channel) => channel.channelCode === form.data.channelCode)" :label="selectedChannelName || form.data.channelCode" :value="form.data.channelCode" /><el-option v-for="channel in channels" :key="channel.channelCode" :label="channel.channelName" :value="channel.channelCode"><span>{{ channel.channelName }}</span><small class="channel-code">{{ channel.channelCode }}</small></el-option></el-select><div v-if="channelsError" class="field-error"><span>{{ channelsError }}</span><el-button link type="primary" :loading="channelsLoading" @click="form.data.warehouseId && loadChannels(form.data.warehouseId)">重新加载</el-button></div><div v-else-if="channelUnavailable" class="field-hint">该仓库暂无有效物流渠道,请更换仓库或联系管理员。</div></el-form-item>
|
||||
<el-form-item label="提单号" prop="billOfLadingNo"><el-input v-model="form.data.billOfLadingNo" maxlength="128" placeholder="请输入提单号" /></el-form-item>
|
||||
<el-form-item label="船公司"><el-select :model-value="form.data.shippingCompanyId" filterable clearable placeholder="请选择船公司" :loading="optionsLoading" style="width:100%" @update:model-value="selectShippingCompany(String($event || ''))"><el-option v-if="form.data.shippingCompanyId && !shippingCompanies.some((partner) => String(partner.id) === String(form.data.shippingCompanyId))" :label="form.data.shippingCompanyName || '船公司名称缺失'" :value="String(form.data.shippingCompanyId)" /><el-option v-for="partner in shippingCompanies" :key="String(partner.id)" :label="partner.partnerName" :value="String(partner.id)" /></el-select></el-form-item>
|
||||
<el-form-item label="订舱代理"><el-select :model-value="form.data.bookingAgentId" filterable clearable placeholder="请选择订舱代理" :loading="optionsLoading" no-data-text="暂无启用的订舱代理" style="width:100%" @update:model-value="selectBookingAgent(String($event || ''))"><el-option v-if="!editable && form.data.bookingAgentId && !selectableBookingAgents.some((supplier) => String(supplier.id) === String(form.data.bookingAgentId))" :label="form.data.bookingAgentName || '订舱代理名称缺失'" :value="String(form.data.bookingAgentId)" /><el-option v-for="supplier in selectableBookingAgents" :key="String(supplier.id)" :label="supplier.name" :value="String(supplier.id)" /></el-select></el-form-item>
|
||||
@@ -392,34 +330,34 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
<el-form-item label="预计到港" prop="estimatedArrivalAt"><el-date-picker v-model="form.data.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计到港时间" style="width:100%" /></el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
<section class="booking-group" aria-label="条款与真实柜">
|
||||
<h3>条款与真实柜</h3><div class="booking-group__body">
|
||||
<section class="booking-group" aria-label="条款与柜型需求">
|
||||
<h3>条款与柜型需求</h3><div class="booking-group__body">
|
||||
<div class="booking-terms">
|
||||
<el-form-item label="主要运输条款 Service Term" prop="serviceTerm"><el-input v-model="form.data.serviceTerm" clearable maxlength="32" placeholder="请输入 Service Term" /></el-form-item>
|
||||
<el-form-item label="运费条款 Freight Term" prop="freightTerm"><el-input v-model="form.data.freightTerm" clearable maxlength="32" placeholder="请输入 Freight Term" /></el-form-item>
|
||||
</div>
|
||||
<div class="container-table-wrap" aria-label="真实柜登记">
|
||||
<div class="container-table-wrap" aria-label="柜型需求">
|
||||
<table class="container-table">
|
||||
<thead><tr><th scope="col">操作</th><th scope="col">集装箱号</th><th scope="col">柜型</th><th scope="col">箱种</th><th scope="col">容量</th><th scope="col">限重</th><th v-if="detail" scope="col">柜次号</th></tr></thead>
|
||||
<thead><tr><th scope="col">操作</th><th scope="col">柜型</th><th scope="col">箱种</th><th scope="col">数量</th><th scope="col">容量</th><th scope="col">限重</th><th v-if="detail?.status === 'RELEASED'" scope="col">剩余可关联</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="(container, index) in form.data.containers" :key="detail?.containers[index]?.containerBookingId || index">
|
||||
<tr v-for="(requirement, index) in form.data.containerRequirements" :key="requirement.requirementId || index">
|
||||
<td class="container-table__action">
|
||||
<el-tooltip v-if="editable" :content="form.data.containers.length === 1 ? '至少保留一个真实柜' : '删除该真实柜'" placement="top">
|
||||
<span><el-button circle plain size="small" type="danger" :aria-label="`删除真实柜 ${index + 1}`" :disabled="form.data.containers.length === 1" @click="removeContainer(index)"><Trash2 :size="14" /></el-button></span>
|
||||
<el-tooltip v-if="editable" :content="form.data.containerRequirements.length === 1 ? '至少保留一项柜型需求' : '删除该柜型需求'" placement="top">
|
||||
<span><el-button circle plain size="small" type="danger" :aria-label="`删除柜型需求 ${index + 1}`" :disabled="form.data.containerRequirements.length === 1" @click="removeRequirement(index)"><Trash2 :size="14" /></el-button></span>
|
||||
</el-tooltip>
|
||||
<span v-else aria-hidden="true">—</span>
|
||||
</td>
|
||||
<td><el-input v-model="container.containerNo" :disabled="!editable" maxlength="64" placeholder="请输入真实集装箱号" /></td>
|
||||
<td><el-select :model-value="container.containerType" :disabled="!editable" placeholder="请选择柜型" @update:model-value="selectBookingContainerType(container, String($event || ''))"><el-option v-for="item in containerTypeOptions" :key="item.type" :label="item.type" :value="item.type" /></el-select></td>
|
||||
<td><el-select v-model="container.containerKind" :disabled="!editable" placeholder="请选择箱种"><el-option v-for="item in containerKindOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></td>
|
||||
<td class="container-table__specification"><span>{{ container.capacityCbm }} m³</span></td>
|
||||
<td class="container-table__specification"><span>{{ container.maxWeightKg }} kg</span></td>
|
||||
<td v-if="detail"><span>{{ detail.containers[index]?.cabinetNo || '—' }}</span></td>
|
||||
<td><el-select :model-value="requirement.containerType" :disabled="!editable" placeholder="请选择柜型" @update:model-value="selectContainerRequirementType(requirement, String($event || ''))"><el-option v-for="item in containerTypeOptions" :key="item.type" :label="item.type" :value="item.type" /></el-select></td>
|
||||
<td><el-select v-model="requirement.containerKind" :disabled="!editable" placeholder="请选择箱种"><el-option v-for="item in containerKindOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></td>
|
||||
<td><el-input-number v-model="requirement.quantity" :disabled="!editable" :min="1" :max="99" controls-position="right" /></td>
|
||||
<td class="container-table__specification"><span>{{ requirement.capacityCbm }} m³</span></td>
|
||||
<td class="container-table__specification"><span>{{ requirement.maxWeightKg }} kg</span></td>
|
||||
<td v-if="detail?.status === 'RELEASED'"><span>{{ requirement.remainingQuantity ?? requirement.quantity }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-if="editable" class="container-actions"><el-button class="add-container" size="small" :disabled="form.data.containers.length >= 20" @click="addContainer"><Plus :size="15" />新增真实柜</el-button><span>{{ form.data.containers.length }} / 20 个</span></div>
|
||||
<div v-if="editable" class="container-actions"><el-button class="add-container" size="small" :disabled="form.data.containerRequirements.length >= 20" @click="addRequirement"><Plus :size="15" />新增柜型需求</el-button><span>{{ form.data.containerRequirements.length }} / 20 项</span></div>
|
||||
<el-form-item label="备注"><el-input v-model="form.data.remark" type="textarea" :rows="3" placeholder="请输入订舱备注(可选)" /></el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
@@ -446,18 +384,9 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
</template>
|
||||
</DetailPageLayout>
|
||||
|
||||
<AppDialog v-model="changeDialogOpen" title="申请改船" width="min(560px, 94vw)" :confirm-loading="actionSaving" :confirm-disabled="!changeForm.vesselName.trim() || !changeForm.voyageNo.trim() || !changeForm.reason.trim()" confirm-text="提交申请" @confirm="submitChange">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="新船名" required><el-input v-model="changeForm.vesselName" placeholder="请输入新船名" /></el-form-item>
|
||||
<el-form-item label="新航次" required><el-input v-model="changeForm.voyageNo" placeholder="请输入新航次" /></el-form-item>
|
||||
<el-form-item label="预计开船"><el-date-picker v-model="changeForm.estimatedSailingAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计开船时间" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="预计到港"><el-date-picker v-model="changeForm.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计到港时间" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="变更原因" required><el-input v-model="changeForm.reason" type="textarea" placeholder="请说明改船原因" /></el-form-item>
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
<AppDialog v-model="releaseDialogOpen" title="确认放舱" width="min(560px, 94vw)" :confirm-loading="actionSaving" confirm-text="确认放舱" cancel-text="取消" @confirm="submitRelease">
|
||||
<el-form label-position="top">
|
||||
<el-alert title="请复核已登记的真实订舱资料。放舱不会重新采集或修改这些信息。" type="info" :closable="false" show-icon />
|
||||
<el-alert title="放舱后资料将同步到关联货柜,不能修改、不能删除;填写错误只能由管理员处理数据库,请确认资料无误。" type="warning" :closable="false" show-icon />
|
||||
<el-descriptions class="release-summary" :column="2" border>
|
||||
<el-descriptions-item label="提单号">{{ detail?.billOfLadingNo || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="船名">{{ detail?.vesselName || '—' }}</el-descriptions-item>
|
||||
@@ -465,7 +394,6 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
<el-descriptions-item label="预计开船">{{ detail?.estimatedSailingAt || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预计到港">{{ detail?.estimatedArrivalAt || '—' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-form-item label="备注"><el-input v-model="releaseReason" type="textarea" maxlength="1000" placeholder="请输入放舱备注(可选)" /></el-form-item>
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
</AdminPage>
|
||||
|
||||
Reference in New Issue
Block a user