Release/test 合并上生产 #2
@@ -0,0 +1,152 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import ElementPlus from 'element-plus'
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
import { listPlatformTenants } from '@/modules/iam'
|
||||
import { listPlatformPartners } from '@/modules/master-data'
|
||||
import { listPlatformWarehouses } from '@/modules/warehouses'
|
||||
import {
|
||||
getOceanBooking,
|
||||
listOceanBookingAgents,
|
||||
listWarehouseLogisticsChannels,
|
||||
releaseOceanBooking,
|
||||
updateOceanBookingDraft,
|
||||
} from '../api/platformOceanBookings'
|
||||
import type { OceanBookingDetail, OceanBookingStatus } from '../types'
|
||||
import OceanBookingDetailView from './OceanBookingDetailView.vue'
|
||||
|
||||
const routerReplace = vi.hoisted(() => vi.fn())
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => ({ name: 'ocean-booking-detail', params: { bookingId: '123' } }),
|
||||
useRouter: () => ({ push: vi.fn(), replace: routerReplace }),
|
||||
}))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
|
||||
vi.mock('@/core/workspace-tabs/workspaceTabs', () => ({ useWorkspaceTabsStore: () => ({ removeByIds: vi.fn(), bumpCacheEpoch: vi.fn() }) }))
|
||||
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn() }))
|
||||
vi.mock('@/modules/master-data', () => ({ listPlatformPartners: vi.fn() }))
|
||||
vi.mock('@/modules/warehouses', () => ({ listPlatformWarehouses: vi.fn() }))
|
||||
vi.mock('../api/platformOceanBookings', () => ({
|
||||
createOceanBooking: vi.fn(), deleteOceanBookingDraft: vi.fn(), getOceanBooking: vi.fn(),
|
||||
listOceanBookingAgents: vi.fn(), listWarehouseLogisticsChannels: vi.fn(),
|
||||
releaseOceanBooking: vi.fn(), updateOceanBookingDraft: vi.fn(),
|
||||
}))
|
||||
|
||||
function booking(status: OceanBookingStatus = 'DRAFT', version = 3, billOfLadingNo = 'BL-001'): OceanBookingDetail {
|
||||
const resource = {
|
||||
id: '123', bookingNo: 'OB-123', status, version, customerId: '456', customerName: '测试客户',
|
||||
billOfLadingNo, shipperName: '发货人', consigneeName: '收货人', containerCount: 2,
|
||||
channelCode: 'SEA_FCL', channelName: '海运整箱', warehouseId: '8', warehouseName: '义乌仓',
|
||||
shippingCompanyId: '10', shippingCompanyName: '测试船公司', carrierCode: 'MATSON' as const,
|
||||
bookingAgentId: '20', bookingAgentName: '订舱代理', vesselName: 'TEST VESSEL', voyageNo: 'V001',
|
||||
departurePortCode: 'CNSHA', departurePortName: '上海', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
|
||||
placeOfDelivery: 'ONT8', serviceTerm: 'CY-CY', freightTerm: 'PREPAID',
|
||||
estimatedSailingAt: '2026-09-01T08:00:00', estimatedArrivalAt: '2026-09-20T08:00:00', remark: null,
|
||||
createdAt: null, updatedAt: null, orderNo: null, carrierName: null, bookingAgent: null,
|
||||
pol: '上海', pod: '洛杉矶', mblShipper: null, mblConsignee: null, soNo: null, cutoffAt: null,
|
||||
etd: null, atd: null, eta: null, ata: null, containerSummary: null, amount: null, currency: null,
|
||||
}
|
||||
return {
|
||||
...resource,
|
||||
oceanBooking: resource,
|
||||
parties: [
|
||||
{ partyType: 'SHIPPER', partyName: '发货人' },
|
||||
{ partyType: 'CONSIGNEE', partyName: '收货人' },
|
||||
{ partyType: 'NOTIFY_PARTY', partyName: '通知人' },
|
||||
],
|
||||
containerRequirements: [{ requirementId: '31', containerType: '40HC', containerKind: 'FCL', quantity: 2, remainingQuantity: 2, capacityCbm: 76, maxWeightKg: 28000 }],
|
||||
containers: [], histories: [], statusHistory: [],
|
||||
}
|
||||
}
|
||||
|
||||
type ViewModel = {
|
||||
form: { data: { billOfLadingNo: string } }
|
||||
detail: OceanBookingDetail | null
|
||||
actionError: { code?: string; requestId?: string; traceId?: string; message: string } | null
|
||||
actionSaving: boolean
|
||||
submitRelease: () => Promise<void>
|
||||
}
|
||||
|
||||
async function mountView() {
|
||||
const wrapper = mount(OceanBookingDetailView, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
stubs: {
|
||||
AdminPage: { template: '<main><slot /></main>' },
|
||||
DetailPageLayout: { template: '<section><slot name="header" /><slot /></section>' },
|
||||
PageToolbar: { template: '<header><slot name="actions" /></header>' },
|
||||
CarrierScheduleQueryPanel: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
return { wrapper, vm: wrapper.vm as unknown as ViewModel }
|
||||
}
|
||||
|
||||
describe('OceanBookingDetailView one-click release', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(getOceanBooking).mockResolvedValue(booking())
|
||||
vi.mocked(listPlatformTenants).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
||||
vi.mocked(listPlatformPartners).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
||||
vi.mocked(listPlatformWarehouses).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
||||
vi.mocked(listWarehouseLogisticsChannels).mockResolvedValue([{ channelCode: 'SEA_FCL', channelName: '海运整箱' }])
|
||||
vi.mocked(listOceanBookingAgents).mockResolvedValue([{ supplierId: '20', name: '订舱代理', carrierCodes: ['MATSON'] }])
|
||||
})
|
||||
|
||||
it('saves dirty form data and releases with the returned latest version', async () => {
|
||||
vi.mocked(updateOceanBookingDraft).mockResolvedValue(booking('DRAFT', 4, 'NEW-BL'))
|
||||
vi.mocked(releaseOceanBooking).mockResolvedValue(booking('RELEASED', 5, 'NEW-BL'))
|
||||
const { vm } = await mountView()
|
||||
vm.form.data.billOfLadingNo = ' NEW-BL '
|
||||
|
||||
await vm.submitRelease()
|
||||
|
||||
expect(updateOceanBookingDraft).toHaveBeenCalledWith('123', expect.objectContaining({ expectedVersion: 3, data: expect.objectContaining({ billOfLadingNo: 'NEW-BL' }) }))
|
||||
expect(releaseOceanBooking).toHaveBeenCalledWith('123', { expectedVersion: 4 })
|
||||
expect(vi.mocked(updateOceanBookingDraft).mock.invocationCallOrder[0]).toBeLessThan(vi.mocked(releaseOceanBooking).mock.invocationCallOrder[0]!)
|
||||
expect(vm.detail?.status).toBe('RELEASED')
|
||||
expect(vm.detail?.version).toBe(5)
|
||||
})
|
||||
|
||||
it('does not call release when automatic draft saving fails', async () => {
|
||||
vi.mocked(updateOceanBookingDraft).mockRejectedValue(new ApiResponseError('保存草稿失败', 'A0409', 409, undefined, 'request-save', 'trace-save'))
|
||||
const { vm } = await mountView()
|
||||
vm.form.data.billOfLadingNo = 'NEW-BL'
|
||||
|
||||
await vm.submitRelease()
|
||||
|
||||
expect(releaseOceanBooking).not.toHaveBeenCalled()
|
||||
expect(vm.form.data.billOfLadingNo).toBe('NEW-BL')
|
||||
expect(vm.actionError).toMatchObject({ code: 'A0409', requestId: 'request-save', traceId: 'trace-save' })
|
||||
})
|
||||
|
||||
it('keeps the saved draft and evidence when release fails after automatic saving', async () => {
|
||||
vi.mocked(updateOceanBookingDraft).mockResolvedValue(booking('DRAFT', 4, 'NEW-BL'))
|
||||
vi.mocked(releaseOceanBooking).mockRejectedValue(new ApiResponseError('放舱失败', 'A0409', 409, undefined, 'request-release', 'trace-release'))
|
||||
const { vm } = await mountView()
|
||||
vm.form.data.billOfLadingNo = 'NEW-BL'
|
||||
|
||||
await vm.submitRelease()
|
||||
|
||||
expect(vm.detail).toMatchObject({ status: 'DRAFT', version: 4, billOfLadingNo: 'NEW-BL' })
|
||||
expect(vm.form.data.billOfLadingNo).toBe('NEW-BL')
|
||||
expect(vm.actionError).toMatchObject({ code: 'A0409', requestId: 'request-release', traceId: 'trace-release' })
|
||||
})
|
||||
|
||||
it('releases an unchanged draft directly and ignores duplicate submissions while busy', async () => {
|
||||
let resolveRelease!: (value: OceanBookingDetail) => void
|
||||
vi.mocked(releaseOceanBooking).mockReturnValue(new Promise((resolve) => { resolveRelease = resolve }))
|
||||
const { vm } = await mountView()
|
||||
|
||||
const first = vm.submitRelease()
|
||||
const duplicate = vm.submitRelease()
|
||||
await flushPromises()
|
||||
expect(vm.actionSaving).toBe(true)
|
||||
expect(updateOceanBookingDraft).not.toHaveBeenCalled()
|
||||
expect(releaseOceanBooking).toHaveBeenCalledTimes(1)
|
||||
expect(releaseOceanBooking).toHaveBeenCalledWith('123', { expectedVersion: 3 })
|
||||
resolveRelease(booking('RELEASED', 4))
|
||||
await Promise.all([first, duplicate])
|
||||
expect(vm.actionSaving).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -31,6 +31,7 @@ describe('OceanBookingDetailView draft and release contract', () => {
|
||||
|
||||
it('saves incomplete DRAFT data and sends null for blank nullable fields', () => {
|
||||
expect(source).toContain('保存草稿')
|
||||
expect(source).toContain('function draftPayload(): OceanBookingDraftData')
|
||||
expect(source).toContain('channelCode: form.data.channelCode?.trim() || null')
|
||||
expect(source).toContain('warehouseId: form.data.warehouseId?.trim() || null')
|
||||
expect(source).toContain('parties: form.data.parties.filter((party) => party.partyName.trim())')
|
||||
@@ -55,10 +56,19 @@ describe('OceanBookingDetailView draft and release contract', () => {
|
||||
expect(source).toContain('预计到港时间不能早于预计开船时间')
|
||||
expect(source).toContain("[form.data.customerId, '客户']")
|
||||
expect(source).toContain("if (!form.data.carrierCode) return '放舱前请重新选择船公司,所选船公司必须配置标准编码'")
|
||||
expect(source).toContain("return '船公司标准编码尚未保存,请先保存草稿后再放舱'")
|
||||
expect(source).not.toContain('请先保存草稿后再放舱')
|
||||
expect(source).toContain('放舱前请填写${missing[1]}')
|
||||
expect(source).toContain('放舱后资料将同步到关联货柜,不能修改、不能删除;填写错误只能由管理员处理数据库,请确认资料无误。')
|
||||
expect(source).toContain('releaseOceanBooking(id.value, { expectedVersion: detail.value.version })')
|
||||
expect(source).toContain('if (hasUnsavedDraftChanges.value)')
|
||||
expect(source).toContain('const saved = await persistDraft(false)')
|
||||
expect(source).toContain('releaseVersion = saved.version')
|
||||
expect(source).toContain('releaseOceanBooking(id.value, { expectedVersion: releaseVersion })')
|
||||
expect(source.indexOf('const saved = await persistDraft(false)')).toBeLessThan(source.indexOf('releaseOceanBooking(id.value, { expectedVersion: releaseVersion })'))
|
||||
expect(source).toContain('if (!saved) return')
|
||||
expect(source).toContain("{{ form.data.billOfLadingNo || '—' }}")
|
||||
expect(source).not.toContain("{{ detail?.billOfLadingNo || '—' }}")
|
||||
expect(source).toContain('const workflowBusy = computed(() => saving.value || actionSaving.value)')
|
||||
expect(source).toContain(':disabled="workflowBusy" @click="openReleaseDialog"')
|
||||
expect(source).not.toContain('releaseReason')
|
||||
})
|
||||
|
||||
|
||||
@@ -97,11 +97,13 @@ function blankData(): OceanBookingDraftData {
|
||||
}
|
||||
}
|
||||
const form = reactive<OceanBookingSaveRequest>({ expectedVersion: 0, data: blankData() })
|
||||
const savedDraftSignature = ref('')
|
||||
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 workflowBusy = computed(() => saving.value || actionSaving.value)
|
||||
const releaseDialogOpen = ref(false)
|
||||
|
||||
function statusLabel(status: OceanBookingStatus | string | null | undefined) {
|
||||
@@ -136,16 +138,25 @@ function selectCustomer(value: unknown) {
|
||||
const customer = customerId == null ? null : customers.value.find((item) => String(item.id) === customerId)
|
||||
form.data.customerName = customer?.tenantName || null
|
||||
}
|
||||
function normalizeDraftData() {
|
||||
form.data.customerId = form.data.customerId || null
|
||||
form.data.customerName = form.data.customerId ? form.data.customerName : null
|
||||
form.data.serviceTerm = form.data.serviceTerm?.trim() || null
|
||||
form.data.freightTerm = form.data.freightTerm?.trim() || null
|
||||
form.data.billOfLadingNo = form.data.billOfLadingNo.trim()
|
||||
form.data.vesselName = form.data.vesselName.trim()
|
||||
form.data.voyageNo = form.data.voyageNo.trim()
|
||||
form.data.containerRequirements.forEach((requirement) => { requirement.quantity = Number(requirement.quantity) })
|
||||
function draftPayload(): OceanBookingDraftData {
|
||||
const customerId = form.data.customerId || null
|
||||
return {
|
||||
...form.data,
|
||||
customerId,
|
||||
customerName: customerId ? form.data.customerName : null,
|
||||
channelCode: form.data.channelCode?.trim() || null,
|
||||
warehouseId: form.data.warehouseId?.trim() || null,
|
||||
serviceTerm: form.data.serviceTerm?.trim() || null,
|
||||
freightTerm: form.data.freightTerm?.trim() || null,
|
||||
billOfLadingNo: form.data.billOfLadingNo.trim(),
|
||||
vesselName: form.data.vesselName.trim(),
|
||||
voyageNo: form.data.voyageNo.trim(),
|
||||
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: Number(quantity), capacityCbm, maxWeightKg })),
|
||||
}
|
||||
}
|
||||
function draftSignature() { return JSON.stringify(draftPayload()) }
|
||||
const hasUnsavedDraftChanges = computed(() => Boolean(detail.value?.status === 'DRAFT' && savedDraftSignature.value && draftSignature() !== savedDraftSignature.value))
|
||||
function selectWarehouse(value: unknown) {
|
||||
const warehouseId = value == null || value === '' ? null : String(value)
|
||||
form.data.warehouseId = warehouseId
|
||||
@@ -229,6 +240,7 @@ function applyDetail(value: OceanBookingDetail) {
|
||||
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 || ''
|
||||
savedDraftSignature.value = draftSignature()
|
||||
channels.value = []
|
||||
if (form.data.warehouseId) void loadChannels(form.data.warehouseId)
|
||||
if (form.data.carrierCode) void loadBookingAgents(form.data.carrierCode)
|
||||
@@ -241,47 +253,45 @@ async function load() {
|
||||
catch (cause) { error.value = getApiErrorDetails(cause) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
async function save() {
|
||||
if (!canSave.value) return
|
||||
actionError.value = null
|
||||
normalizeDraftData()
|
||||
async function validateDraftForm() {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (valid === false) { actionError.value = { message: '请检查表单中的必填项和格式' }; return false }
|
||||
const requirementMessage = containerRequirementError(form.data.containerRequirements)
|
||||
if (requirementMessage) { actionError.value = { message: requirementMessage }; return }
|
||||
saving.value = true
|
||||
if (requirementMessage) { actionError.value = { message: requirementMessage }; return false }
|
||||
return true
|
||||
}
|
||||
async function persistDraft(showSuccess: boolean) {
|
||||
if (!canSave.value || !(await validateDraftForm())) return null
|
||||
actionError.value = null
|
||||
try {
|
||||
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 payload = draftPayload()
|
||||
const saved = isNew.value
|
||||
? await createOceanBooking({ expectedVersion: 0, data: payload })
|
||||
: await updateOceanBookingDraft(id.value, { expectedVersion: detail.value?.version || 0, data: payload })
|
||||
ElMessage.success(isNew.value ? `订舱创建成功:${saved.bookingNo || saved.id}` : '订舱草稿已保存')
|
||||
if (showSuccess) ElMessage.success(isNew.value ? `订舱创建成功:${saved.bookingNo || saved.id}` : '订舱草稿已保存')
|
||||
if (isNew.value) {
|
||||
workspace.removeByIds(['ocean-booking-create'])
|
||||
await router.replace('/ocean-bookings')
|
||||
return
|
||||
return saved
|
||||
}
|
||||
applyDetail(saved)
|
||||
} catch (cause) { actionError.value = getApiErrorDetails(cause) }
|
||||
return saved
|
||||
} catch (cause) { actionError.value = getApiErrorDetails(cause); return null }
|
||||
}
|
||||
async function save() {
|
||||
if (!canSave.value || workflowBusy.value) return
|
||||
saving.value = true
|
||||
try { await persistDraft(true) }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
function openReleaseDialog() {
|
||||
normalizeDraftData()
|
||||
async function openReleaseDialog() {
|
||||
if (!canRelease.value || workflowBusy.value || !(await validateDraftForm())) return
|
||||
const validationError = releaseValidationError()
|
||||
if (validationError) { actionError.value = { message: validationError }; return }
|
||||
releaseDialogOpen.value = true
|
||||
}
|
||||
function releaseValidationError() {
|
||||
if (!form.data.carrierCode) return '放舱前请重新选择船公司,所选船公司必须配置标准编码'
|
||||
if (detail.value && (detail.value.carrierCode !== form.data.carrierCode
|
||||
|| String(detail.value.shippingCompanyId || '') !== String(form.data.shippingCompanyId || ''))) {
|
||||
return '船公司标准编码尚未保存,请先保存草稿后再放舱'
|
||||
}
|
||||
const requiredFields: Array<[unknown, string]> = [
|
||||
[form.data.customerId, '客户'], [form.data.warehouseId, '装柜仓库'], [form.data.channelCode, '物流渠道'],
|
||||
[form.data.billOfLadingNo, '提单号'], [form.data.vesselName, '船名'], [form.data.voyageNo, '航次'],
|
||||
@@ -296,13 +306,20 @@ function releaseValidationError() {
|
||||
return containerRequirementError(form.data.containerRequirements)
|
||||
}
|
||||
async function submitRelease() {
|
||||
if (!detail.value) return
|
||||
const validationError = releaseValidationError()
|
||||
if (validationError) { actionError.value = { message: validationError }; releaseDialogOpen.value = false; return }
|
||||
if (!detail.value || workflowBusy.value) return
|
||||
actionSaving.value = true
|
||||
actionError.value = null
|
||||
try {
|
||||
applyDetail(await releaseOceanBooking(id.value, { expectedVersion: detail.value.version }))
|
||||
if (!(await validateDraftForm())) { releaseDialogOpen.value = false; return }
|
||||
const validationError = releaseValidationError()
|
||||
if (validationError) { actionError.value = { message: validationError }; releaseDialogOpen.value = false; return }
|
||||
let releaseVersion = detail.value.version
|
||||
if (hasUnsavedDraftChanges.value) {
|
||||
const saved = await persistDraft(false)
|
||||
if (!saved) return
|
||||
releaseVersion = saved.version
|
||||
}
|
||||
applyDetail(await releaseOceanBooking(id.value, { expectedVersion: releaseVersion }))
|
||||
releaseDialogOpen.value = false
|
||||
ElMessage.success('确认放舱成功')
|
||||
} catch (cause) { actionError.value = getApiErrorDetails(cause) }
|
||||
@@ -349,9 +366,9 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
<div class="section-title">
|
||||
<div><h2>订舱信息</h2><p v-if="detail?.status === 'DRAFT'">草稿可反复保存;放舱后资料同步至关联货柜并锁定。</p><p v-else-if="detail">已放舱资料只读。</p></div>
|
||||
<div class="actions">
|
||||
<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>
|
||||
<el-button v-if="canDelete" type="danger" plain :loading="actionSaving" :disabled="workflowBusy" @click="removeDraft"><Trash2 :size="16" />删除草稿</el-button>
|
||||
<el-button v-if="canRelease" type="primary" plain :loading="actionSaving" :disabled="workflowBusy" @click="openReleaseDialog">放舱</el-button>
|
||||
<el-button v-if="canSave" type="primary" :loading="saving" :disabled="workflowBusy" @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>
|
||||
@@ -437,15 +454,15 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
</template>
|
||||
</DetailPageLayout>
|
||||
|
||||
<AppDialog v-model="releaseDialogOpen" title="确认放舱" width="min(560px, 94vw)" :confirm-loading="actionSaving" confirm-text="确认放舱" cancel-text="取消" @confirm="submitRelease">
|
||||
<AppDialog v-model="releaseDialogOpen" title="确认放舱" width="min(560px, 94vw)" :confirm-loading="actionSaving" :confirm-disabled="workflowBusy" confirm-text="确认放舱" cancel-text="取消" @confirm="submitRelease">
|
||||
<el-form label-position="top">
|
||||
<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>
|
||||
<el-descriptions-item label="航次">{{ detail?.voyageNo || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预计开船">{{ detail?.estimatedSailingAt || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预计到港">{{ detail?.estimatedArrivalAt || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="提单号">{{ form.data.billOfLadingNo || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="船名">{{ form.data.vesselName || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="航次">{{ form.data.voyageNo || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预计开船">{{ form.data.estimatedSailingAt || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预计到港">{{ form.data.estimatedArrivalAt || '—' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
|
||||
Reference in New Issue
Block a user