2026-10-15 17:11:03 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-10-20 10:11:31 +08:00
|
|
|
|
import InlineHelp from '@/shared/components/InlineHelp.vue'
|
2026-10-19 17:56:31 +08:00
|
|
|
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
2026-10-15 17:11:03 +08:00
|
|
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
2026-10-19 17:56:31 +08:00
|
|
|
|
import { getDispatch, stageLabel, type DispatchDetail } from '@/modules/overseas-warehouse'
|
2026-10-15 17:11:03 +08:00
|
|
|
|
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
|
|
|
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|
|
|
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
2026-10-19 17:56:31 +08:00
|
|
|
|
import WorkSharedTripForm from './WorkSharedTripForm.vue'
|
|
|
|
|
|
import { resolveBatchWork } from './workApi'
|
2026-10-19 08:44:26 +08:00
|
|
|
|
const props = defineProps<{ dispatches: Array<{ batchKey: string; dispatchNo: string; customerId?: string }>; canWrite: boolean }>()
|
|
|
|
|
|
const emit = defineEmits<{ updated: []; external: [path: string]; ready: [valid: boolean] }>()
|
2026-10-19 17:56:31 +08:00
|
|
|
|
const access = useAccessPreviewStore(), rows = ref<DispatchDetail[]>([]), loading = ref(false), error = ref<ApiErrorDetails | null>(null)
|
|
|
|
|
|
const addingNo = ref(''), addingBusy = ref(false)
|
|
|
|
|
|
const readable = computed(() => access.can('platform:overseas-warehouse:dispatch:list') && (!props.dispatches.some(row => row.batchKey.startsWith('DISPATCH:')) || access.can('platform:appointment:list')))
|
|
|
|
|
|
function path(no: string) { return `/overseas-warehouse/dispatches/${encodeURIComponent(no)}` }
|
|
|
|
|
|
function canEdit(row: DispatchDetail) { return props.canWrite && readable.value && access.can('platform:overseas-warehouse:dispatch:edit') && row.status === 'ACTIVE' && row.actions.EDIT?.allowed === true }
|
|
|
|
|
|
function canAdd(row: DispatchDetail) { return props.canWrite && readable.value && access.can('platform:overseas-warehouse:dispatch:edit') && row.status === 'ACTIVE' && row.stage === 'BEFORE_LOADING' && row.actions.ADD_CARGO?.allowed === true }
|
|
|
|
|
|
let generation = 0
|
2026-10-15 17:11:03 +08:00
|
|
|
|
async function load() {
|
2026-10-19 17:56:31 +08:00
|
|
|
|
const current = ++generation; emit('ready', false); rows.value = []; error.value = null
|
2026-10-15 17:11:03 +08:00
|
|
|
|
if (!readable.value) return
|
2026-10-19 17:56:31 +08:00
|
|
|
|
loading.value = true
|
2026-10-15 17:11:03 +08:00
|
|
|
|
try {
|
2026-10-19 17:56:31 +08:00
|
|
|
|
const references = props.dispatches.map(row => ({ ...row }))
|
|
|
|
|
|
const details = await Promise.all([...new Set(references.map(row => row.dispatchNo))].map(async no => {
|
|
|
|
|
|
const row = await getDispatch(no)
|
|
|
|
|
|
if (row.dispatchNo !== no || row.status === 'CANCELLED') throw new ApiResponseError('排车单与工作批次关联不一致,请刷新核对', 'WORK_DISPATCH_MISMATCH')
|
|
|
|
|
|
for (const reference of references.filter(value => value.dispatchNo === no)) {
|
|
|
|
|
|
let matches = false
|
|
|
|
|
|
if (/^FBA_APPOINTMENT:\d+$/.test(reference.batchKey)) {
|
|
|
|
|
|
matches = Boolean(reference.customerId && row.dispatchInfo.appointmentBatches.some(batch => batch.batchKey === reference.batchKey && batch.customerId === reference.customerId))
|
|
|
|
|
|
} else if (/^DISPATCH:\d+$/.test(reference.batchKey)) {
|
|
|
|
|
|
// The work domain resolves its opaque batch identity; Detail has no internal dispatch ID.
|
|
|
|
|
|
const resolution = await resolveBatchWork(reference.batchKey, reference.customerId)
|
|
|
|
|
|
matches = resolution.batchKey === reference.batchKey && (resolution.detail
|
|
|
|
|
|
? resolution.detail.item.batchKey === reference.batchKey && resolution.detail.item.batchNo === no && (!reference.customerId || resolution.customerId === reference.customerId)
|
|
|
|
|
|
: resolution.candidates.length > 0 && resolution.candidates.every(candidate => candidate.batchNo === no) && (!reference.customerId || resolution.candidates.some(candidate => candidate.customerId === reference.customerId)))
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!matches) throw new ApiResponseError('排车单与工作批次关联不一致,请刷新核对', 'WORK_DISPATCH_MISMATCH')
|
|
|
|
|
|
}
|
2026-10-15 17:11:03 +08:00
|
|
|
|
return row
|
|
|
|
|
|
}))
|
2026-10-19 17:56:31 +08:00
|
|
|
|
if (current !== generation) return
|
|
|
|
|
|
rows.value = details; emit('ready', details.length > 0)
|
|
|
|
|
|
} catch (cause) { if (current === generation) error.value = getApiErrorDetails(cause, '原排车单读取失败') }
|
|
|
|
|
|
finally { if (current === generation) loading.value = false }
|
2026-10-15 17:11:03 +08:00
|
|
|
|
}
|
2026-10-19 17:56:31 +08:00
|
|
|
|
function add(row: DispatchDetail) { if (canAdd(row) && !loading.value && !error.value && !addingBusy.value) addingNo.value = row.dispatchNo }
|
|
|
|
|
|
function updated() { emit('updated'); void load() }
|
|
|
|
|
|
watch(() => props.dispatches, load, { immediate: true, deep: true })
|
|
|
|
|
|
onBeforeUnmount(() => { generation++ })
|
2026-10-15 17:11:03 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
<template>
|
2026-10-20 10:11:31 +08:00
|
|
|
|
<section aria-label="沿用原整车排车"><h3>沿用原整车排车 <InlineHelp title="排车说明"><p>在原排车单维护车辆、司机和货物;一张排车单就是一趟车。</p></InlineHelp></h3>
|
2026-10-19 17:56:31 +08:00
|
|
|
|
<p v-if="!readable">查看原排车单及批次关联需要相应列表权限。</p><p v-if="loading">正在核对原排车单与批次…</p>
|
2026-10-15 17:11:03 +08:00
|
|
|
|
<LoadErrorState v-if="error" title="原排车单读取失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
2026-10-19 17:56:31 +08:00
|
|
|
|
<table v-if="!loading && !error && readable" class="density-table"><thead><tr><th>操作</th><th>排车单号</th><th>车牌号</th><th>箱数</th><th>阶段</th></tr></thead><tbody><tr v-for="row in rows" :key="row.dispatchNo"><td><el-button v-if="canEdit(row)" :disabled="addingBusy" @click="emit('external', path(row.dispatchNo))">继续维护本车</el-button><el-button v-if="canAdd(row)" :disabled="addingBusy" @click="add(row)">为本车添加同仓货物</el-button><el-button @click="emit('external', path(row.dispatchNo))">查看原排车单</el-button></td><td>{{ row.dispatchNo }}</td><td>{{ row.vehicleInfo.vehicle?.plate || '—' }}</td><td>{{ row.counts.packageCount }}</td><td>{{ stageLabel(row.stage) }}</td></tr></tbody></table>
|
|
|
|
|
|
<WorkSharedTripForm v-if="addingNo" :key="addingNo" :dispatch-no="addingNo" :can-write="canWrite" @busy="addingBusy = $event" @updated="updated" @external="value => emit('external', value)" />
|
2026-10-15 17:11:03 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|