This commit is contained in:
2026-09-07 15:07:39 +08:00
parent 189d1eae11
commit d488e9ab40
10 changed files with 260 additions and 10 deletions
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { Download, Eye } from '@lucide/vue'
import { Download, Eye, Pencil } from '@lucide/vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
@@ -13,7 +13,7 @@ import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { convertLastMileContainerToInboundPlans, convertLastMileOrderToInboundPlan, downloadLastMileOrders, getContainerPlanProgress, listContainerPlanProgress, listLastMileOrders, transferLastMileOrders, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto } from '../api/lastMileOrders'
import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, convertLastMileOrderToInboundPlan, downloadLastMileOrders, getContainerPlanProgress, listContainerPlanProgress, listLastMileOrders, transferLastMileOrders, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto } from '../api/lastMileOrders'
import OrderReferenceText from '../components/OrderReferenceText.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { overseasWarehousePermissions as permission } from '../permissions'
@@ -50,6 +50,14 @@ const containerOptions = ref<ContainerPlanProgressSummaryDto[]>([])
const containerOptionsLoading = ref(false)
const containerLoadError = ref<ApiErrorDetails | null>(null)
const selectedContainerBookingId = ref('')
const receivingWarehouseOpen = ref(false)
const receivingWarehouseLoading = ref(false)
const receivingWarehouseError = ref<ApiErrorDetails | null>(null)
const receivingWarehouseActionError = ref<ApiErrorDetails | null>(null)
const receivingWarehouseOptions = ref<TransferWarehouseOption[]>([])
const receivingWarehouseTarget = ref<ContainerPlanProgressSummaryDto | null>(null)
const receivingWarehouseForm = reactive({ warehouseId: '', remark: '' })
let receivingWarehouseIdempotencyKey = ''
let containerOptionRequest = 0
const statusTab = ref<'全部' | '待分配' | '柜子'>('全部')
const filters = ref<QueryModel>({ orderNo: '', orderType: '', warehouse: '', inboundCode: '', lxCode: '', sourceOrderNo: '', containerNo: '', status: '', transferWarehouseCode: '', abnormal: '', detained: '', createdAt: [] })
@@ -112,6 +120,17 @@ function toTransferWarehouseOption(warehouse: PlatformWarehouseSummaryResource):
label: [warehouseCode, warehouseName, region].filter(Boolean).join(' · '),
}
}
function receivingWarehouseLabel(container: ContainerPlanProgressSummaryDto | null | undefined) {
if (!container) return '—'
const code = String(container.overseasWarehouseCode || '').trim()
const name = String(container.overseasWarehouseName || '').trim()
return [code, name].filter(Boolean).join(' · ') || '—'
}
function newReceivingWarehouseIdempotencyKey() {
return `container-receiving-warehouse-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
}
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 270, fixed: 'left', slotName: 'actions' },
{ key: 'selection', type: 'selection', width: 48, fixed: 'left' },
@@ -256,6 +275,65 @@ async function openContainerDetail(row: ContainerPlanProgressSummaryDto) {
containerDetailLoading.value = false
}
}
async function loadReceivingWarehouseOptions() {
receivingWarehouseLoading.value = true
receivingWarehouseError.value = null
receivingWarehouseOptions.value = []
try {
const warehouses = await listAllPlatformWarehouses({ warehouseType: 'OVERSEAS', status: 'ACTIVE' })
receivingWarehouseOptions.value = warehouses.map(toTransferWarehouseOption)
} catch (error) {
receivingWarehouseError.value = getApiErrorDetails(error, '海外接柜仓候选加载失败,请重试')
} finally {
receivingWarehouseLoading.value = false
}
}
function openReceivingWarehouseChange(container: ContainerPlanProgressSummaryDto) {
if (!container.canChangeReceivingWarehouse) return
receivingWarehouseTarget.value = container
Object.assign(receivingWarehouseForm, { warehouseId: container.overseasWarehouseId || '', remark: '' })
receivingWarehouseIdempotencyKey = newReceivingWarehouseIdempotencyKey()
receivingWarehouseOptions.value = []
receivingWarehouseError.value = null
receivingWarehouseActionError.value = null
actionError.value = null
receivingWarehouseOpen.value = true
void loadReceivingWarehouseOptions()
}
async function submitReceivingWarehouseChange() {
const target = receivingWarehouseTarget.value
const selected = receivingWarehouseOptions.value.find((warehouse) => warehouse.warehouseId === receivingWarehouseForm.warehouseId)
if (!target || !selected) {
ElMessage.warning('请选择有效的海外接柜仓')
return
}
actionLoading.value = true
actionError.value = null
try {
await changeContainerReceivingWarehouse(target.sourceContainerBookingId, {
overseasWarehouseId: selected.warehouseId,
expectedVersion: target.assignmentVersion,
remark: receivingWarehouseForm.remark.trim() || undefined,
}, receivingWarehouseIdempotencyKey)
receivingWarehouseOpen.value = false
receivingWarehouseTarget.value = null
ElMessage.success('海外接柜仓修改成功')
await load()
} catch (error) {
const details = getApiErrorDetails(error, '海外接柜仓修改失败,请重试')
receivingWarehouseActionError.value = details.code === 'A0409'
? { ...details, message: '接柜仓信息已被其他操作更新,请刷新后重试' }
: details
actionError.value = receivingWarehouseActionError.value
if (details.code === 'A0409') {
receivingWarehouseOpen.value = false
receivingWarehouseTarget.value = null
await load()
}
} finally {
actionLoading.value = false
}
}
function onSelectionChange(selection: object[]) { selectedRows.value = selection as LastMileOrderListItem[] }
async function loadTransferWarehouseOptions() {
transferWarehouseLoading.value = true
@@ -418,6 +496,10 @@ onMounted(() => { void load() })
<div v-if="containerRows.length" class="container-workbench__grid">
<article v-for="container in containerRows" :key="container.sourceContainerBookingId" class="container-workbench-card">
<span class="container-workbench-card__top"><strong>{{ container.containerNo || '柜号待补充' }}</strong></span>
<span class="container-workbench-card__warehouse">
<small>海外接柜仓</small><strong>{{ receivingWarehouseLabel(container) }}</strong>
<small v-if="!container.canChangeReceivingWarehouse && container.receivingWarehouseLockReason" class="is-locked">{{ container.receivingWarehouseLockReason }}</small>
</span>
<span class="container-workbench-card__arrival"><small>计划到仓</small><strong>{{ formatDateTime(container.plannedArrivalAt) }}</strong></span>
<span class="container-workbench-card__pending">
<strong>{{ pendingInboundCodeCount(container) }}</strong><span>个入仓码待生成计划</span>
@@ -427,6 +509,7 @@ onMounted(() => { void load() })
</span>
<span class="container-workbench-card__actions">
<el-button class="container-workbench-card__detail" plain @click="openContainerDetail(container)">查看详情</el-button>
<el-button v-if="access.can(permission.lastMileOrderTransfer) && container.canChangeReceivingWarehouse" plain @click="openReceivingWarehouseChange(container)"><Pencil :size="15" />修改接柜仓</el-button>
<el-button v-if="access.can(permission.inboundOrderCreate)" type="primary" :disabled="actionLoading || !canGenerateContainerPlans(container)" @click="openContainerConversionFor(container)">生成入库计划</el-button>
</span>
</article>
@@ -479,16 +562,36 @@ onMounted(() => { void load() })
<template v-else-if="containerDetail">
<div class="container-detail-summary">
<span>柜号:{{ containerDetail.summary.containerNo || '—' }}</span>
<span>海外接柜仓:{{ receivingWarehouseLabel(containerDetail.summary) }}</span>
<span>计划到仓:{{ formatDateTime(containerDetail.summary.plannedArrivalAt) }}</span>
<span>订单:{{ containerDetail.summary.totalOrderCount }}</span>
<span>入仓码:{{ containerDetail.summary.inboundCodeCount }}</span>
<span>入库计划:{{ containerDetail.summary.planCount }},待生成 {{ pendingInboundCodeCount(containerDetail.summary) }}</span>
<span v-if="!containerDetail.summary.canChangeReceivingWarehouse">修改状态:{{ containerDetail.summary.receivingWarehouseLockReason || '当前已锁定' }}</span>
</div>
<DataTable :data="containerDetail.orders" :columns="containerDetailColumns" :loading="containerDetailLoading" preference-key="overseas-last-mile-container-detail" row-key="orderId" :total="undefined" empty-text="该柜暂无尾程订单">
<template #plannedArrivalAt="{ row }">{{ formatDateTime((row as ContainerPlanProgressDetailDto['orders'][number]).plannedArrivalAt) }}</template>
</DataTable>
</template>
</AppDialog>
<AppDialog v-model="receivingWarehouseOpen" title="修改海外接柜仓" width="min(600px, calc(100vw - 28px))" confirm-text="确认修改" :loading="receivingWarehouseLoading" :confirm-loading="actionLoading" :confirm-disabled="!receivingWarehouseForm.warehouseId || Boolean(receivingWarehouseError)" @confirm="submitReceivingWarehouseChange">
<LoadErrorState v-if="receivingWarehouseError" title="海外接柜仓候选加载失败" :message="receivingWarehouseError.message" compact @retry="loadReceivingWarehouseOptions">
<template #details><ApiErrorMeta :details="receivingWarehouseError" /></template>
</LoadErrorState>
<el-form v-else label-position="top">
<el-form-item label="当前海外接柜仓"><el-input :model-value="receivingWarehouseLabel(receivingWarehouseTarget)" readonly /></el-form-item>
<el-form-item label="新的海外接柜仓" required>
<el-select v-model="receivingWarehouseForm.warehouseId" filterable clearable placeholder="请选择 ACTIVE 海外接柜仓">
<el-option v-for="warehouse in receivingWarehouseOptions" :key="warehouse.warehouseId" :label="warehouse.label" :value="warehouse.warehouseId" />
</el-select>
</el-form-item>
<el-alert v-if="!receivingWarehouseLoading && !receivingWarehouseOptions.length" title="暂无可用的 ACTIVE 海外接柜仓" type="info" :closable="false" show-icon />
<el-form-item label="备注"><el-input v-model="receivingWarehouseForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入修改备注(选填)" /></el-form-item>
<LoadErrorState v-if="receivingWarehouseActionError" title="海外接柜仓修改失败" :message="receivingWarehouseActionError.message" compact>
<template #details><ApiErrorMeta :details="receivingWarehouseActionError" /></template>
</LoadErrorState>
</el-form>
</AppDialog>
</template>
<style scoped>
@@ -510,6 +613,10 @@ onMounted(() => { void load() })
.container-workbench-card__arrival { display: grid; gap: 4px; }
.container-workbench-card__arrival small { color: var(--cc-muted); font-size: 12px; }
.container-workbench-card__arrival strong { color: var(--cc-text); font-size: 15px; }
.container-workbench-card__warehouse { display: grid; gap: 4px; }
.container-workbench-card__warehouse small { color: var(--cc-muted); font-size: 12px; }
.container-workbench-card__warehouse strong { overflow-wrap: anywhere; color: var(--cc-text); font-size: 14px; }
.container-workbench-card__warehouse .is-locked { color: #c2410c; }
.container-workbench-card__pending { display: flex; align-items: baseline; gap: 8px; padding: 12px 14px; border-left: 3px solid #f59e0b; background: #fffaf0; }
.container-workbench-card__pending strong { color: #b45309; font-size: 28px; line-height: 1; }
.container-workbench-card__pending span { color: #92400e; font-size: 13px; }