import { ref, type Ref } from 'vue' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { listDispatchCandidates } from './api/dispatches' import { DispatchInputError, assertSelectionGroupVersions, type Candidate, type CandidateQuery } from './dispatchContracts' /** One visible server page; selected cargo remains owned by the caller or server draft. */ export function useDispatchCandidates(error: Ref, fetchCandidates = listDispatchCandidates, allowAllWarehouses = false) { const candidates = ref([]), page = ref(1), size = ref(20), total = ref(0), loading = ref(false) let generation = 0, warehouse = '', current = false let pending: { key: string; promise: Promise } | undefined const groupVersions = new Map() const sourceGroups = new Map() function reset() { generation++; pending = undefined; warehouse = ''; current = false candidates.value = []; page.value = 1; total.value = 0; loading.value = false groupVersions.clear(); sourceGroups.clear() } function assertCurrent(rows: Candidate[]) { if (rows.length && (loading.value || !current)) throw new DispatchInputError('货物候选尚未读取完成,请刷新后重试') assertSelectionGroupVersions(rows, candidates.value) for (const row of rows) { const group = row.selectionGroup if ((warehouse && row.warehouseId !== warehouse) || (groupVersions.has(group.groupKey) && groupVersions.get(group.groupKey) !== group.groupVersion) || group.sources.some(source => { const latest = sourceGroups.get(source.sourceKey) return latest && (latest.groupKey !== group.groupKey || latest.sourceVersion !== source.selection.sourceVersion) })) throw new DispatchInputError('选货组组成已变化,请重新读取并选择货物') } } function load(query: CandidateQuery): Promise { if (!query.warehouseId && !allowAllWarehouses) { reset(); return Promise.resolve() } if (warehouse !== (query.warehouseId || '')) { reset(); warehouse = query.warehouseId || '' } const request = { ...query, page: query.page ?? page.value, size: query.size ?? size.value, warehouseTab: query.warehouseTab ?? 'ALL' } const key = JSON.stringify(request) if (pending?.key === key) return pending.promise const sequence = ++generation // Keep the previous page visible but non-actionable until its replacement is validated. // Preserve the total too: zero would make the pager emit page 1. loading.value = true; current = false; error.value = null const promise = (async () => { try { const result = await fetchCandidates(request) if (sequence !== generation) return if (result.page !== request.page || result.size !== request.size) throw new DispatchInputError('候选响应页码不一致,请重新读取') if (result.candidates.some(row => !row.warehouseId || (request.warehouseId && row.warehouseId !== request.warehouseId) || !row.sourceVersion || row.selection.sourceVersion !== row.sourceVersion)) throw new DispatchInputError('候选起运仓或来源版本不完整,请重新读取') for (const row of result.candidates) { groupVersions.set(row.selectionGroup.groupKey, row.selectionGroup.groupVersion) for (const source of row.selectionGroup.sources) sourceGroups.set(source.sourceKey, { groupKey: row.selectionGroup.groupKey, sourceVersion: source.selection.sourceVersion }) } current = true candidates.value = result.candidates; page.value = result.page; size.value = result.size; total.value = result.total } catch (cause) { if (sequence === generation) { candidates.value = []; current = false error.value = getApiErrorDetails(cause, cause instanceof DispatchInputError ? cause.message : '货物候选读取失败') } } finally { if (sequence === generation) { loading.value = false; pending = undefined } } })() pending = { key, promise } return promise } return { candidates, page, size, total, loading, load, reset, assertCurrent } }