86 lines
6.0 KiB
Vue
86 lines
6.0 KiB
Vue
<script setup lang="ts">
|
||
import { computed, inject, onBeforeUnmount, provide, ref, watch } from 'vue'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||
import CargoVerificationForm from './CargoVerificationForm.vue'
|
||
import { getCargoVerification, type CargoVerificationContext } from './cargoVerificationApi'
|
||
import { resolvePlanningOrder, resolvePlanningSource, type PlanningSourceLookup, type ResolvedPlanningSource } from './planningApi'
|
||
import type { LeaveGuard } from './useWorkDraft'
|
||
|
||
const props = defineProps<{ sources: PlanningSourceLookup[]; active: boolean; disabled?: boolean }>()
|
||
const emit = defineEmits<{ ready: [value: boolean] }>()
|
||
const access = useAccessPreviewStore()
|
||
type Row = { resolved: ResolvedPlanningSource; cargo: CargoVerificationContext }
|
||
const rows = ref<Row[]>([]), loading = ref(false), error = ref<ApiErrorDetails | null>(null)
|
||
const editing = ref<string | null>(null), editor = ref<InstanceType<typeof CargoVerificationForm> | null>(null)
|
||
const editingRow = computed(() => rows.value.find(row => key(row) === editing.value))
|
||
let generation = 0
|
||
const guards = new Set<LeaveGuard>()
|
||
const register = inject<((guard: LeaveGuard) => () => void) | undefined>('appointment-work-leave-guard', undefined)
|
||
provide('appointment-work-leave-guard', (guard: LeaveGuard) => { guards.add(guard); return () => guards.delete(guard) })
|
||
async function canLeave() { for (const guard of guards) if (!await guard()) return false; return true }
|
||
const unregister = register?.(canLeave)
|
||
defineExpose({ canLeave })
|
||
function key(row: Row) { return `${row.resolved.source.customerId}:${row.resolved.source.sourceKey}` }
|
||
function issues(row: Row) {
|
||
const missing: string[] = []
|
||
if (!row.resolved.source.orderId) missing.push('尚未来货建单,请先在原来货登记完成实收关联')
|
||
if (row.cargo.effectivePackages == null || row.cargo.effectivePackages <= 0) missing.push('缺少本票箱数')
|
||
if (row.resolved.coverage.missingFields.includes('FBA_SHIPMENT_REFERENCES')) missing.push('缺少本票 FBA 货件号或追踪号(至少一种)')
|
||
if (row.cargo.sourceChanged) missing.push('原货物资料已变化,请重新核对箱数和货件号')
|
||
return missing
|
||
}
|
||
const ready = computed(() => rows.value.length === props.sources.length && rows.value.length > 0 && !loading.value && !error.value && !editing.value && rows.value.every(row => !issues(row).length))
|
||
watch(ready, value => emit('ready', value), { immediate: true })
|
||
async function load() {
|
||
const current = ++generation
|
||
loading.value = true; error.value = null
|
||
try {
|
||
if (!access.can('platform:appointment:list') || !access.can('platform:overseas-warehouse:last-mile-order:list')) {
|
||
error.value = { message: '无法读取本票核实资料,请联系有预约列表和尾程订单查看权限的同事补齐。' }; return
|
||
}
|
||
const result: Row[] = []
|
||
// 只核对本次明确选择的货物,并由服务端解析原 ORDER / BOOKING 身份。
|
||
for (const lookup of props.sources) {
|
||
const resolved = 'sourceKey' in lookup ? await resolvePlanningSource(lookup) : await resolvePlanningOrder(lookup)
|
||
if (current !== generation) return
|
||
const cargo = await getCargoVerification(resolved.source)
|
||
if (current !== generation) return
|
||
result.push({ resolved, cargo })
|
||
}
|
||
if (current === generation) rows.value = result
|
||
} catch (cause) { if (current === generation) error.value = getApiErrorDetails(cause, '货物缺项读取失败,请重试核对') }
|
||
finally { if (current === generation) loading.value = false }
|
||
}
|
||
async function closeEditor() {
|
||
if (editor.value && !await editor.value.canLeave()) return
|
||
editing.value = null
|
||
}
|
||
async function saved() { editing.value = null; await load() }
|
||
watch(() => [props.active, JSON.stringify(props.sources)] as const, ([active]) => { if (active && !editing.value) void load() }, { immediate: true })
|
||
onBeforeUnmount(() => { ++generation; unregister?.() })
|
||
</script>
|
||
<template>
|
||
<section v-show="active || editing" class="density-dialog-section" aria-label="正式确认前的货物资料" :aria-busy="loading">
|
||
<h3 class="density-dialog-section__title">正式确认前:核对本次送仓货物</h3>
|
||
<el-button :disabled="loading || !!editing || disabled" :loading="loading" @click="load">重新核对资料</el-button>
|
||
<p>本票总箱数用于校验预约货量;FBA 货件号或追踪号用于识别送的是哪批货。外部约号只代表仓库的预约确认号,不能代替货件号。</p>
|
||
<LoadErrorState v-if="error" title="货物资料未核对" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||
<table class="density-table"><thead><tr><th>操作</th><th>入仓码 / 外接登记号</th><th>本票总箱数</th><th>需要补充的资料</th></tr></thead><tbody>
|
||
<tr v-for="row in rows" :key="key(row)">
|
||
<td><el-button v-if="issues(row).length && row.resolved.source.orderId" :disabled="disabled || loading || !!editing || !row.cargo.allowedActions.includes('VERIFY_CARGO')" @click="editing = key(row)">在此补齐资料</el-button><span v-else>—</span></td>
|
||
<td>{{ row.resolved.source.inboundCode || row.resolved.source.bookingNo || '未提供业务编号' }}</td>
|
||
<td>{{ row.cargo.effectivePackages ?? '待补充' }}</td>
|
||
<td>{{ issues(row).join(';') || '资料齐全,无需重复填写' }}</td>
|
||
</tr>
|
||
</tbody></table>
|
||
<div v-if="editingRow">
|
||
<CargoVerificationForm ref="editor" :source="editingRow.resolved.source" :disabled="disabled" confirmation @saved="saved" />
|
||
<el-button :disabled="disabled" @click="closeEditor">收起补充资料</el-button>
|
||
</div>
|
||
<p v-if="!loading && !error && !ready" role="status">请先在上表补齐并保存资料,再点击“保存预约安排”。</p>
|
||
</section>
|
||
</template>
|