2026-10-10 14:03:43 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
|
|
|
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
|
|
|
|
|
import QueryToolbar, { type QueryModel, type QueryField } from '@/shared/components/QueryToolbar.vue'
|
|
|
|
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|
|
|
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|
|
|
|
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
|
|
|
|
|
import { listExternalAppointmentCandidates, type ExternalAppointmentCandidate, type PlanningSource } from './planningApi'
|
|
|
|
|
|
|
|
|
|
|
|
defineProps<{ canArrange: boolean; selectedKeys: string[]; busy: boolean }>()
|
|
|
|
|
|
const emit = defineEmits<{ select: [source: PlanningSource]; followup: [row: ExternalAppointmentCandidate] }>()
|
|
|
|
|
|
const page = ref(1), size = ref(20), total = ref(0), loading = ref(false)
|
|
|
|
|
|
const rows = ref<ExternalAppointmentCandidate[]>([]), error = ref<ApiErrorDetails | null>(null)
|
|
|
|
|
|
const query = ref<QueryModel>({ keyword: '' })
|
|
|
|
|
|
const fields: QueryField[] = [{ key: 'keyword', type: 'input', label: '外接货', placeholder: '登记号、车次号、客户、来货号', width: 300 }]
|
|
|
|
|
|
const columns: DataTableColumn[] = [
|
|
|
|
|
|
{ key: 'actions', label: '操作', slotName: 'actions', width: 160, fixed: 'left' },
|
|
|
|
|
|
{ key: 'booking', label: '原登记号', slotName: 'booking', minWidth: 190 },
|
|
|
|
|
|
{ prop: 'customerName', label: '客户', minWidth: 130 },
|
|
|
|
|
|
{ prop: 'tripNo', label: '车次号', minWidth: 190 },
|
|
|
|
|
|
{ key: 'origin', label: '接仓点', slotName: 'origin', minWidth: 140 },
|
|
|
|
|
|
{ key: 'destination', label: '目的FBA仓', slotName: 'destination', width: 110 },
|
|
|
|
|
|
{ key: 'packages', label: '箱数', slotName: 'packages', width: 90 },
|
|
|
|
|
|
{ key: 'coverage', label: '预约情况', slotName: 'coverage', minWidth: 180 },
|
|
|
|
|
|
]
|
|
|
|
|
|
let request = 0
|
|
|
|
|
|
let controller: AbortController | undefined
|
|
|
|
|
|
async function load(reset = false) {
|
|
|
|
|
|
if (reset) page.value = 1
|
|
|
|
|
|
const current = ++request
|
|
|
|
|
|
controller?.abort(); controller = new AbortController()
|
|
|
|
|
|
loading.value = true; error.value = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await listExternalAppointmentCandidates({ page: page.value, size: size.value, keyword: String(query.value.keyword || '').trim() || undefined }, controller.signal)
|
|
|
|
|
|
if (current === request) { rows.value = result.records; total.value = result.total }
|
|
|
|
|
|
} catch (cause) {
|
|
|
|
|
|
if (current === request) { rows.value = []; total.value = 0; error.value = getApiErrorDetails(cause, '外接候选加载失败') }
|
|
|
|
|
|
} finally { if (current === request) loading.value = false }
|
|
|
|
|
|
}
|
|
|
|
|
|
onMounted(() => load())
|
|
|
|
|
|
onBeforeUnmount(() => { ++request; controller?.abort() })
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<DataTable v-model:page="page" v-model:page-size="size" appearance="integrated" :data="rows" :columns="columns" :total="total" :loading="loading" :results-visible="!error" @update:page="load()" @update:page-size="load(true)">
|
|
|
|
|
|
<template #toolbar-actions><slot name="toolbar-actions" /></template>
|
2026-10-12 11:45:00 +08:00
|
|
|
|
<template #filters>
|
|
|
|
|
|
<section class="integrated-query-group" aria-label="外接货物类型与筛选">
|
|
|
|
|
|
<slot name="query-tabs" />
|
|
|
|
|
|
<QueryToolbar v-model="query" ariaLabel="外接预约候选查询" :fields="fields" :loading="loading" @search="load(true)" />
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
2026-10-10 14:03:43 +08:00
|
|
|
|
<template #notices>
|
|
|
|
|
|
<slot name="notices" />
|
|
|
|
|
|
<LoadErrorState v-if="error" title="外接候选加载失败" :message="error.message" @retry="load()"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
|
|
|
|
|
<p v-else>已确认承运的美国FBA货物,来货前后保留同一原登记;箱数未知可先保存待确认安排。</p>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template #actions="{ row }"><template v-if="row.source && row.coverage">
|
|
|
|
|
|
<el-button v-if="canArrange" link type="primary" :disabled="busy || selectedKeys.includes(row.source.sourceKey) || !row.source.canFollowup || row.coverage.appointments.length > 0" @click="emit('select', row.source)">{{ selectedKeys.includes(row.source.sourceKey) ? '已加入' : '加入合批' }}</el-button>
|
|
|
|
|
|
<el-button link type="primary" @click="emit('followup', row as ExternalAppointmentCandidate)">跟进</el-button>
|
|
|
|
|
|
</template></template>
|
|
|
|
|
|
<template #booking="{ row }">{{ row.source?.bookingNo || '—' }}</template>
|
|
|
|
|
|
<template #origin="{ row }">{{ row.source?.originWarehouseName || '—' }}</template>
|
|
|
|
|
|
<template #destination="{ row }">{{ row.source?.destinationWarehouseCode || '—' }}</template>
|
|
|
|
|
|
<template #packages="{ row }">{{ row.source?.packages ?? '待核实' }}</template>
|
|
|
|
|
|
<template #coverage="{ row }"><span v-if="row.source && row.coverage">{{ row.coverage.appointments.length ? (row.coverage.changeReviewRequired ? '已有预约,待复核' : '已有预约') : (row.source.canFollowup ? '待安排' : row.source.blockReasons.join(';')) }}</span></template>
|
|
|
|
|
|
</DataTable>
|
|
|
|
|
|
</template>
|