Merge origin/release/test and preserve procurement direction with request unlock

This commit is contained in:
2026-10-26 13:51:37 +08:00
170 changed files with 3122 additions and 2458 deletions
@@ -7,6 +7,7 @@ import { ChevronDown, ChevronUp, Download, Pencil, PackagePlus, SlidersHorizonta
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
import { listContainerArrivalTimes, projectWarehouseArrival, type ContainerArrivalTime } from '@/modules/container-loading'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
@@ -110,12 +111,23 @@ const inboundPlanSourcePalletCount = ref<number | null>(null)
const inboundPlanPreviewLoading = ref(false)
const inboundPlanPreviewError = ref('')
const inboundPlanIsDropship = computed(() => actionTarget.value?.orderTypeCode === 'ONE_CLICK_DROPSHIP')
const inboundPlanSubmitDisabled = computed(() => !actionTarget.value?.estimatedWarehouseAt
const inboundPlanArrival = ref<ContainerArrivalTime | null>(null)
const inboundPlanArrivalLoading = ref(false)
const inboundPlanArrivalError = ref('')
let inboundPlanArrivalRequest = 0
const inboundPlanArrivalProjection = computed(() => {
const arrival = inboundPlanArrival.value
return projectWarehouseArrival(arrival ? { ...arrival, plannedAt: arrival.estimatedWarehouseArrivalAt } : undefined)
})
const inboundPlanSubmitDisabled = computed(() => inboundPlanArrivalLoading.value || !inboundPlanArrivalProjection.value.date
|| (inboundPlanIsDropship.value
&& (inboundPlanPreviewLoading.value || inboundPlanSourcePalletCount.value == null
|| inboundPlanSourcePalletCount.value <= 0)))
const inboundPlanForm = reactive({ inboundCode: '', palletCount: 1, palletType: 'WOOD' as 'STANDARD' | 'WOOD' | 'PAPER', assigned: false, mergeRequired: false, urgent: false, remark: '' })
const inboundPlanExpectedArrival = computed(() => actionTarget.value?.warehouseEta || '—')
const inboundPlanExpectedArrival = computed(() => {
const { date, time, isDefault } = inboundPlanArrivalProjection.value
return date ? `${date}${time ? ` ${time}` : ''}${isDefault ? '(默认推算)' : ''}` : '—'
})
const holdOpen = ref(false)
const holdForm = reactive({ held: true, chargeable: true, reason: '' })
const manualNodesOpen = ref(false)
@@ -633,9 +645,37 @@ function openInboundPlan(source?: object) {
inboundPlanPreviewError.value = ''
Object.assign(inboundPlanForm, { inboundCode: target.inboundCode === '—' ? '' : target.inboundCode, palletCount: inboundPlanMinimumPalletCount.value, palletType: 'WOOD', assigned: false, mergeRequired: false, urgent: false, remark: '' })
actionError.value = null
inboundPlanArrival.value = null
inboundPlanArrivalError.value = ''
inboundPlanOpen.value = true
void refreshInboundPlanArrival(target)
void refreshManualPalletMinimum(target)
}
/** 每次打开重新读取柜级权威日期,避免使用列表旧值或在页面推算默认日期。 */
async function refreshInboundPlanArrival(target: LastMileOrderListItem) {
const request = ++inboundPlanArrivalRequest
inboundPlanArrivalLoading.value = true
try {
if (!target.sourceContainerBookingId) {
inboundPlanArrivalError.value = '尾程订单未绑定来源柜,无法取得预计到仓时间'
return
}
const arrivals = await listContainerArrivalTimes([target.sourceContainerBookingId])
if (request !== inboundPlanArrivalRequest || !inboundPlanOpen.value) return
const arrival = arrivals.find(item => item.sourceContainerBookingId === target.sourceContainerBookingId)
if (!arrival || arrival.version < 0) {
inboundPlanArrivalError.value = '来源柜预计到仓日期缺失或不一致,请先核对柜级时间'
return
}
inboundPlanArrival.value = arrival
} catch (error) {
if (request === inboundPlanArrivalRequest && inboundPlanOpen.value) {
inboundPlanArrivalError.value = getApiErrorDetails(error, '预计到仓时间加载失败,请重新打开入库计划重试').message
}
} finally {
if (request === inboundPlanArrivalRequest) inboundPlanArrivalLoading.value = false
}
}
async function refreshManualPalletMinimum(target: LastMileOrderListItem) {
if (!target.sourceContainerBookingId) return
inboundPlanPreviewLoading.value = true
@@ -671,7 +711,7 @@ async function submitInboundPlan() {
ElMessage.warning('一件代发必须按头程已封托托盘数整体生成入库计划')
return
}
if (!row || !row.overseasWarehouseId || !row.estimatedWarehouseAt || !frozenInboundCode || frozenInboundCode === '—' || inboundPlanForm.palletCount < inboundPlanMinimumPalletCount.value) {
if (!row || !row.overseasWarehouseId || inboundPlanArrivalLoading.value || !inboundPlanArrivalProjection.value.date || !frozenInboundCode || frozenInboundCode === '—' || inboundPlanForm.palletCount < inboundPlanMinimumPalletCount.value) {
ElMessage.warning('当前柜须已分配接柜仓、具备预计到仓时间,并完整填写托数')
return
}
@@ -1288,25 +1328,28 @@ onMounted(() => { void load() })
</LoadErrorState>
</el-form>
</AppDialog>
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="inboundPlanOpen" :title="`入库计划 · ${actionTarget?.orderNo || '—'}`" width="min(720px, calc(100vw - 28px))" fullscreenable confirm-text="保存并生成入库单" :loading="inboundPlanPreviewLoading" :confirm-loading="actionLoading" :confirm-disabled="inboundPlanSubmitDisabled" @confirm="submitInboundPlan">
<el-alert title="保存后生成入库单;清关完成后入库计划不可更改。" type="info" :closable="false" show-icon />
<AppDialog class="inbound-plan-dialog admin-density-dialog admin-workbench is-dialog" v-model="inboundPlanOpen" :title="`入库计划 · ${actionTarget?.orderNo || '—'}`" width="min(1160px, calc(100vw - 48px))" top="6vh" fullscreenable confirm-text="保存并生成入库单" :loading="inboundPlanPreviewLoading || inboundPlanArrivalLoading" :confirm-loading="actionLoading" :confirm-disabled="inboundPlanSubmitDisabled" @confirm="submitInboundPlan">
<div class="inbound-plan-content">
<el-alert title="保存后生成入库单;首个入库计划生成后,整柜预计到仓时间不可更改。" type="info" :closable="false" show-icon />
<el-alert v-if="inboundPlanPreviewError" :title="inboundPlanPreviewError" type="error" :closable="false" show-icon />
<el-form class="command-form density-form density-dialog-sections" label-position="left" label-width="110px" :disabled="actionLoading">
<section class="density-dialog-section" data-tone="source" aria-label="入库来源"><h3 class="density-dialog-section__title">入库来源 <InlineHelp title="入库来源说明">取整柜预计到仓时间,同柜所有入库计划保持一致;已生成计划后锁定。接柜仓按柜统一;如需变更,请在柜子工作台执行“变更接柜仓”。</InlineHelp></h3>
<table class="density-table last-mile-dialog-facts"><tbody>
<tr><th scope="row">预计到仓时间</th><td>{{ inboundPlanExpectedArrival }} <el-button v-if="access.can('platform:firstleg:terminal-drayage:list') && actionTarget?.containerNo" link type="primary" @click="router.push({ name: 'overseas-container-workbench', query: { keyword: actionTarget.containerNo } })">前往货柜维护预计到仓</el-button></td></tr>
<tr><th scope="row">计划入库仓</th><td>{{ inboundPlanWarehouseLabel || '—' }}</td></tr><tr><th scope="row">入仓码</th><td>{{ inboundPlanForm.inboundCode || '—' }}</td></tr>
<el-alert v-if="inboundPlanArrivalError" :title="inboundPlanArrivalError" type="error" :closable="false" show-icon />
<el-form class="inbound-plan-form command-form density-form density-dialog-sections" label-position="left" label-width="110px" :disabled="actionLoading">
<section class="density-dialog-section" data-tone="source" aria-label="入库来源"><h3 class="density-dialog-section__title">来源摘要 <InlineHelp title="入库来源说明">优先使用整柜人工维护的预计到仓时间;未维护时,按到港当地日期加两个自然日默认推算。首个入库计划生成后冻结日期,同柜计划保持一致。接柜仓按柜统一;如需变更,请在柜子工作台执行“变更接柜仓”。</InlineHelp></h3>
<table class="density-table last-mile-dialog-facts inbound-plan-source"><colgroup><col class="inbound-plan-source__label" /><col /><col class="inbound-plan-source__label" /><col /></colgroup><tbody>
<tr><th scope="row">预计到仓时间</th><td colspan="3"><div class="inbound-plan-arrival">{{ inboundPlanExpectedArrival }} <el-button v-if="access.can('platform:firstleg:terminal-drayage:list') && actionTarget?.containerNo" link type="primary" @click="router.push({ name: 'overseas-container-workbench', query: { keyword: actionTarget.containerNo } })">前往货柜维护预计到仓</el-button></div></td></tr>
<tr><th scope="row">计划入库仓</th><td>{{ inboundPlanWarehouseLabel || '—' }}</td><th scope="row">入仓码</th><td>{{ inboundPlanForm.inboundCode || '—' }}</td></tr>
</tbody></table>
</section>
<section class="density-dialog-section" data-tone="work" aria-label="入库计划登记"><h3 class="density-dialog-section__title">计划登记</h3>
<section class="density-dialog-section" aria-label="入库计划登记"><h3 class="density-dialog-section__title">计划设置</h3>
<table class="density-table density-table--editable last-mile-command-table"><tbody>
<tr><td><el-form-item label="托数" required><span v-if="inboundPlanIsDropship" class="frozen-pallet-count">{{ inboundPlanForm.palletCount }}</span><el-input-number v-else v-model="inboundPlanForm.palletCount" :min="inboundPlanMinimumPalletCount" :max="999" controls-position="right" /><span class="density-dialog-note" v-if="inboundPlanIsDropship">一件代发固定按头程已封托托盘数整体入库,不可增减。</span><span class="density-dialog-note" v-else>按柜级预览至少 {{ inboundPlanMinimumPalletCount }} 托,可增加,不能减少。</span></el-form-item></td><td><el-form-item label="托盘类型"><el-select v-model="inboundPlanForm.palletType" placeholder="请选择托盘类型"><el-option label="标准托盘" value="STANDARD" /><el-option label="木托盘" value="WOOD" /><el-option label="纸托盘" value="PAPER" /></el-select></el-form-item></td></tr>
<tr><td><el-form-item label="是否分配"><el-switch v-model="inboundPlanForm.assigned" inline-prompt active-text="是" inactive-text="否" /></el-form-item></td><td><el-form-item label="允许合托"><el-switch v-model="inboundPlanForm.mergeRequired" inline-prompt active-text="是" inactive-text="否" /><InlineHelp title="合托提示说明">仅作为现场可选提示,不强制工人合托。</InlineHelp></el-form-item></td></tr>
<tr><td><el-form-item label="托数" required><span v-if="inboundPlanIsDropship" class="frozen-pallet-count">{{ inboundPlanForm.palletCount }}</span><el-input-number v-else v-model="inboundPlanForm.palletCount" :min="inboundPlanMinimumPalletCount" :max="999" controls-position="right" /><span class="inbound-plan-pallet-note density-dialog-note" v-if="inboundPlanIsDropship">一件代发固定按头程已封托托盘数整体入库,不可增减。</span><span class="inbound-plan-pallet-note density-dialog-note" v-else>按柜级预览至少 {{ inboundPlanMinimumPalletCount }} 托,可增加,不能减少。</span></el-form-item></td><td><el-form-item label="托盘类型"><el-select v-model="inboundPlanForm.palletType" placeholder="请选择托盘类型"><el-option label="标准托盘" value="STANDARD" /><el-option label="木托盘" value="WOOD" /><el-option label="纸托盘" value="PAPER" /></el-select></el-form-item></td></tr>
<tr><td><el-form-item label="是否外配"><el-switch v-model="inboundPlanForm.assigned" inline-prompt active-text="是" inactive-text="否" /></el-form-item></td><td><el-form-item label="允许合托"><el-switch v-model="inboundPlanForm.mergeRequired" inline-prompt active-text="是" inactive-text="否" /><InlineHelp title="合托提示说明">仅作为现场可选提示,不强制工人合托。</InlineHelp></el-form-item></td></tr>
<tr><td colspan="2"><el-form-item label="是否加急"><el-switch v-model="inboundPlanForm.urgent" inline-prompt active-text="是" inactive-text="否" /></el-form-item></td></tr>
<tr><td colspan="2"><el-form-item label="备注"><el-input v-model="inboundPlanForm.remark" type="textarea" :rows="3" maxlength="1000" show-word-limit /></el-form-item></td></tr>
</tbody></table>
</section>
</el-form>
</div>
</AppDialog>
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="holdOpen" :title="`${holdForm.held ? '暂扣订单' : '解除暂扣'} · ${actionTarget?.orderNo || '—'}`" width="min(620px, calc(100vw - 28px))" fullscreenable confirm-text="保存" :confirm-loading="actionLoading" @confirm="submitHold">
<el-alert :title="holdForm.held ? '暂扣后不可创建出库计划;解除暂扣后恢复出库计划操作。' : '解除暂扣后恢复该订单的出库计划操作。'" type="warning" :closable="false" show-icon />
@@ -1332,6 +1375,14 @@ onMounted(() => { void load() })
.last-mile-dialog-facts, .last-mile-command-table, .manual-nodes-editor { table-layout: fixed; }
.last-mile-dialog-facts th { width: var(--admin-label-width); }
.last-mile-command-table td:not([colspan]) { width: 50%; }
.inbound-plan-content { display: grid; min-width: 0; gap: var(--admin-space-5); }
.inbound-plan-form { margin-top: 0; }
.inbound-plan-source__label { width: 112px; }
.inbound-plan-source td { overflow-wrap: anywhere; }
.inbound-plan-arrival { display: flex; align-items: center; flex-wrap: wrap; gap: var(--admin-space-3); }
.inbound-plan-form :deep(.el-form-item__content) { min-width: 0; }
.inbound-plan-form :deep(.el-select) { width: 100%; }
.inbound-plan-pallet-note { flex-basis: 100%; margin-top: var(--admin-space-2); }
.manual-nodes-editor th { width: 180px; }
.relabel-suborder { min-height: 30px; line-height: 30px; white-space: nowrap; }
.last-mile-toolbar { min-width: 0; }