优化
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import ContainerSimulationCreate from '../components/ContainerSimulationCreate.vue'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Boxes, Check, Eye, FileText, FileUp, Link2, ListChecks, Mail, MoreHorizontal, Pencil, Sparkles } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
@@ -9,7 +9,7 @@ import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
|
||||
import { listPlatformPartners, type PlatformPartnerResource } from '@/modules/master-data'
|
||||
import { listSuppliers, type Supplier } from '@/modules/suppliers'
|
||||
import { listOceanBookingLinkCandidates, listOceanBookings, type OceanBookingResource } from '@/modules/ocean-bookings'
|
||||
import { listOceanBookings, type OceanBookingResource } from '@/modules/ocean-bookings'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
@@ -19,7 +19,8 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { CONTAINER_SPECIFICATIONS, applyContainerSpecification, containerSpecification } from '@/shared/domain/containerSpecifications'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { getContainer, linkContainerOceanBooking, listContainers, updateContainer } from '../api/platformContainerLoading'
|
||||
import { getContainer, listContainers, updateContainer } from '../api/platformContainerLoading'
|
||||
import OceanBookingLinkDialog from '../components/OceanBookingLinkDialog.vue'
|
||||
import {
|
||||
getTerminalDrayageEmails,
|
||||
sendTerminalDrayageEmail,
|
||||
@@ -43,7 +44,6 @@ type ContainerForm = ContainerSaveRequest & Pick<ContainerBookingResource, 'cabi
|
||||
const props = withDefaults(defineProps<{ workbench?: 'DOMESTIC' | 'OVERSEAS' }>(), { workbench: 'DOMESTIC' })
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const access = useAccessPreviewStore()
|
||||
const rows = ref<ContainerWorkbenchRow[]>([])
|
||||
const customers = ref<PlatformTenantResource[]>([])
|
||||
@@ -73,13 +73,7 @@ const editorError = ref<ApiErrorDetails | null>(null)
|
||||
const saving = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const linkOpen = ref(false)
|
||||
const linkLoading = ref(false)
|
||||
const linkSaving = ref(false)
|
||||
const linkError = ref<ApiErrorDetails | null>(null)
|
||||
const linkTarget = ref<ContainerBookingResource | null>(null)
|
||||
const linkCandidateKey = ref('')
|
||||
const linkContainerNo = ref('')
|
||||
const linkCandidates = ref<Array<{ key: string; oceanBookingId: string; requirementId: string; label: string; disabled: boolean }>>([])
|
||||
const selectedTruckingSupplierId = ref('')
|
||||
const unresolvedTruckingSupplierName = ref<string | null>(null)
|
||||
const drayages = ref<Record<string, TerminalDrayageView>>({})
|
||||
@@ -157,7 +151,6 @@ async function retryEmail(mail: TerminalDrayageEmail) {
|
||||
}
|
||||
let listSequence = 0
|
||||
let detailSequence = 0
|
||||
let linkSequence = 0
|
||||
let warehousesLoaded = false
|
||||
let warehouseOptionsPromise: Promise<void> | null = null
|
||||
let partnersLoaded = false
|
||||
@@ -279,7 +272,6 @@ const drayageHistoryColumns: DataTableColumn[] = [
|
||||
]
|
||||
|
||||
function optional(value: string | null) { return value?.trim() || null }
|
||||
function normalizedContainerNo(value: string | null | undefined) { return String(value || '').trim().toUpperCase() }
|
||||
function statusText(status: ContainerPlanStatus) { return ({ WAIT_LOADING: '待装柜', EDITING: '预装柜中', LOADING: '拣货中', COMPLETED: '装柜完成' })[status] }
|
||||
function documentStatusText(status: ContainerDocumentStatus | null | undefined) { return status ? ({ MISSING: '未上传', UPLOADED: '已上传', COMPLETED: '已完成' })[status] : '—' }
|
||||
function formatTime(value: string | null | undefined) { return value?.replace('T', ' ').slice(0, 16) || '—' }
|
||||
@@ -514,61 +506,10 @@ async function loadSimulationWarehouses() {
|
||||
if (warehouseError.value) throw warehouseError.value
|
||||
return warehouses.value
|
||||
}
|
||||
async function openLink(source: object) {
|
||||
if (linkLoading.value) return
|
||||
function openLink(source: object) {
|
||||
const row = source as ContainerBookingResource
|
||||
const warehouseId = optional(row.warehouseId)
|
||||
const channelCode = optional(row.channelCode)
|
||||
if (!warehouseId || !channelCode || !optional(row.containerType)) {
|
||||
linkTarget.value = row
|
||||
linkCandidates.value = []
|
||||
linkError.value = { message: '当前模拟柜缺少仓库、渠道或柜型,无法查询可关联订舱' }
|
||||
linkOpen.value = true
|
||||
return
|
||||
}
|
||||
const sequence = ++linkSequence
|
||||
linkTarget.value = row
|
||||
linkCandidateKey.value = ''
|
||||
linkContainerNo.value = ''
|
||||
linkCandidates.value = []
|
||||
linkError.value = null
|
||||
linkOpen.value = true
|
||||
linkLoading.value = true
|
||||
try {
|
||||
const result = await listOceanBookingLinkCandidates({
|
||||
warehouseId, channelCode, containerType: row.containerType,
|
||||
page: 1, size: 200,
|
||||
})
|
||||
if (sequence !== linkSequence || linkTarget.value?.id !== row.id) return
|
||||
linkCandidates.value = result.records.map((candidate) => ({
|
||||
key: `${candidate.oceanBookingId}:${candidate.requirementId}`,
|
||||
oceanBookingId: candidate.oceanBookingId,
|
||||
requirementId: candidate.requirementId,
|
||||
disabled: false,
|
||||
label: `${candidate.bookingNo} · ${candidate.containerType} × ${candidate.remainingQuantity} · ${candidate.customerName} · ${candidate.warehouseName} · ${candidate.channelName}`,
|
||||
}))
|
||||
} catch (error) {
|
||||
if (sequence === linkSequence) linkError.value = getApiErrorDetails(error, '已放舱订舱候选加载失败')
|
||||
} finally {
|
||||
if (sequence === linkSequence) linkLoading.value = false
|
||||
}
|
||||
}
|
||||
async function saveLink() {
|
||||
const target = linkTarget.value
|
||||
const candidate = linkCandidates.value.find((item) => item.key === linkCandidateKey.value)
|
||||
const normalized = normalizedContainerNo(linkContainerNo.value)
|
||||
const containerNo = normalized || null
|
||||
linkContainerNo.value = normalized
|
||||
if (!target || !candidate || candidate.disabled) { linkError.value = { message: '请选择规格一致且仍有剩余数量的柜型需求' }; return }
|
||||
linkSaving.value = true
|
||||
linkError.value = null
|
||||
try {
|
||||
await linkContainerOceanBooking(target.id, { oceanBookingId: candidate.oceanBookingId, requirementId: candidate.requirementId, containerNo, expectedVersion: target.version })
|
||||
linkOpen.value = false
|
||||
ElMessage.success(containerNo ? '订舱与真实集装箱号已关联,柜次号、装柜计划和明细保持不变' : '订舱已关联,真实集装箱号待司机提柜时录入')
|
||||
await loadRows()
|
||||
} catch (error) { linkError.value = getApiErrorDetails(error, '关联真实订舱柜失败') }
|
||||
finally { linkSaving.value = false }
|
||||
}
|
||||
function replaceDrayage(value: TerminalDrayageView) {
|
||||
drayages.value = { ...drayages.value, [value.containerBookingId]: value }
|
||||
@@ -731,28 +672,7 @@ watch([page, pageSize], ([nextPage, nextSize], [previousPage, previousSize]) =>
|
||||
if (nextPage !== previousPage || nextSize !== previousSize) void loadRows()
|
||||
})
|
||||
watch(scope, () => { if (page.value === 1) void loadRows(); else page.value = 1 })
|
||||
watch(linkOpen, (open) => {
|
||||
if (open) return
|
||||
linkSequence += 1
|
||||
linkLoading.value = false
|
||||
})
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadCustomers(), loadRows()])
|
||||
const linkContainerId = typeof route.query.linkContainerId === 'string' ? route.query.linkContainerId : ''
|
||||
if (!isDomestic.value || !linkContainerId || !canEdit.value) return
|
||||
try {
|
||||
const target = await getContainer(linkContainerId, 'DOMESTIC')
|
||||
if (target.sourceType !== 'SIMULATION' || target.oceanBookingId || !hasAction(target, 'EDIT_CONTAINER')) {
|
||||
ElMessage.warning('当前货柜已关联订舱或所处阶段不允许关联')
|
||||
return
|
||||
}
|
||||
await openLink(target)
|
||||
} catch (error) {
|
||||
ElMessage.error(getApiErrorDetails(error, '关联货柜加载失败').message)
|
||||
} finally {
|
||||
void router.replace({ name: 'domestic-container-workbench' })
|
||||
}
|
||||
})
|
||||
onMounted(async () => { await Promise.all([loadCustomers(), loadRows()]) })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -797,12 +717,7 @@ onMounted(async () => {
|
||||
</section>
|
||||
</ListPageLayout>
|
||||
|
||||
<AppDialog v-model="linkOpen" title="关联已放舱订舱" width="min(680px, calc(100vw - 28px))" confirm-text="确认关联" cancel-text="取消" :confirm-loading="linkSaving" :confirm-disabled="!linkCandidateKey" @confirm="saveLink">
|
||||
<el-alert title="关联后保留当前柜次号、装柜计划和明细,并补充订舱快照;真实集装箱号可待司机提柜时录入。" type="info" :closable="false" />
|
||||
<el-form label-position="top"><el-form-item label="柜次号"><el-input :model-value="linkTarget?.cabinetNo || ''" disabled /></el-form-item><el-form-item label="已放舱订舱及柜型需求" required><el-select v-model="linkCandidateKey" filterable :loading="linkLoading" placeholder="请选择仍有剩余数量的柜型需求" no-data-text="暂无符合当前装柜仓库、物流渠道和柜型的已放舱需求"><el-option v-for="candidate in linkCandidates" :key="candidate.key" :label="candidate.label" :value="candidate.key" :disabled="candidate.disabled" /></el-select></el-form-item><el-form-item label="真实集装箱号(可选)"><el-input v-model="linkContainerNo" maxlength="64" placeholder="如未取得可留空,后续显示待补录" @blur="linkContainerNo = normalizedContainerNo(linkContainerNo)" /></el-form-item></el-form>
|
||||
<p class="dialog-hint">仓库、渠道和柜型必须与当前模拟柜一致;容量和限重仅展示柜型固定快照,不影响关联资格。</p>
|
||||
<LoadErrorState v-if="linkError" compact title="关联失败" :message="linkError.message" :show-retry="false"><template #details><ApiErrorMeta :details="linkError" /></template></LoadErrorState>
|
||||
</AppDialog>
|
||||
<OceanBookingLinkDialog v-model="linkOpen" :container="linkTarget" @linked="loadRows" />
|
||||
|
||||
<AppDialog v-model="editorOpen" class="container-editor-dialog" :title="editorTitle" width="min(820px, calc(100vw - 28px))" :confirm-loading="saving" :confirm-disabled="Boolean(warehouseError)" confirm-text="确定" cancel-text="关闭" @confirm="save">
|
||||
<LoadErrorState v-if="editorError" title="货柜保存失败" :message="editorError.message" compact :retry-text="form.id ? '刷新最新数据' : '重试保存'" @retry="form.id ? openEdit({ id: form.id }) : save()"><template #details><ApiErrorMeta :details="editorError" /></template></LoadErrorState>
|
||||
@@ -853,7 +768,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container-page { display: grid; gap: 16px; min-width: 0; }.container-page small { display: block; margin-top: 4px; color: var(--cc-muted); }.container-query-panel { overflow: hidden; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }.container-scope-tabs { display: flex; max-width: 100%; overflow-x: auto; margin-bottom: 0; border-bottom: 1px solid #e5e7eb; background: #f9fafb; scrollbar-width: thin; }.container-scope-tab { position: relative; flex: 0 0 auto; min-height: 44px; padding: 0 18px; border: 0; background: transparent; color: var(--cc-muted); font: inherit; font-size: 14px; cursor: pointer; }.container-scope-tab::after { position: absolute; right: 12px; bottom: 0; left: 12px; height: 3px; background: transparent; content: ''; }.container-scope-tab:hover { color: var(--cc-primary); background: #f6f9fd; }.container-scope-tab:focus-visible { outline: 2px solid var(--cc-primary); outline-offset: -2px; }.container-scope-tab.is-active { color: var(--cc-primary); font-weight: 600; }.container-scope-tab.is-active::after { background: var(--cc-primary); }.container-query-toolbar { border: 0; }.row-actions { display: flex; align-items: center; flex-wrap: nowrap; gap: 4px; white-space: nowrap; }.row-actions :deep(.el-button + .el-button) { margin-left: 0; }.form-grid { display: grid; grid-template-columns: minmax(0,1fr); gap: 0; }.form-grid :deep(.el-select),.form-grid :deep(.el-date-editor),.form-grid :deep(.el-input-number),.simulation-form :deep(.el-select),.simulation-form :deep(.el-input-number),.fleet-select { width: 100%; }.details :deep(.el-descriptions__label) { width: 120px; }.dialog-hint { margin: 4px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.6; }.drayage-load-alert { margin-bottom: 12px; }.drayage-summary { margin-bottom: 14px; }
|
||||
.container-page { display: grid; gap: 16px; min-width: 0; }.container-page small { display: block; margin-top: 4px; color: var(--cc-muted); }.container-query-panel { overflow: hidden; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }.container-scope-tabs { display: flex; max-width: 100%; overflow-x: auto; margin-bottom: 0; border-bottom: 1px solid #e5e7eb; background: #f9fafb; scrollbar-width: thin; }.container-scope-tab { position: relative; flex: 0 0 auto; min-height: 44px; padding: 0 18px; border: 0; background: transparent; color: var(--cc-muted); font: inherit; font-size: 14px; cursor: pointer; }.container-scope-tab::after { position: absolute; right: 12px; bottom: 0; left: 12px; height: 3px; background: transparent; content: ''; }.container-scope-tab:hover { color: var(--cc-primary); background: #f6f9fd; }.container-scope-tab:focus-visible { outline: 2px solid var(--cc-primary); outline-offset: -2px; }.container-scope-tab.is-active { color: var(--cc-primary); font-weight: 600; }.container-scope-tab.is-active::after { background: var(--cc-primary); }.container-query-toolbar { border: 0; }.row-actions { display: flex; align-items: center; flex-wrap: nowrap; gap: 4px; white-space: nowrap; }.row-actions :deep(.el-button + .el-button) { margin-left: 0; }.form-grid { display: grid; grid-template-columns: minmax(0,1fr); gap: 0; }.form-grid :deep(.el-select),.form-grid :deep(.el-date-editor),.form-grid :deep(.el-input-number),.simulation-form :deep(.el-select),.simulation-form :deep(.el-input-number),.fleet-select { width: 100%; }.details :deep(.el-descriptions__label) { width: 120px; }.drayage-load-alert { margin-bottom: 12px; }.drayage-summary { margin-bottom: 14px; }
|
||||
:global(.container-editor-dialog.el-dialog) { display: flex; width: min(820px, calc(100vw - 28px)) !important; max-height: 90vh; margin-block: 5vh !important; overflow: hidden; flex-direction: column; border-radius: 12px; }
|
||||
:global(.container-editor-dialog .el-dialog__header) { flex: 0 0 auto; margin: 0; padding: 20px 28px; border-bottom: 1px solid #e5eaf1; }
|
||||
:global(.container-editor-dialog .el-dialog__title) { color: #1e293b; font-size: 18px; font-weight: 700; }
|
||||
|
||||
Reference in New Issue
Block a user