This commit is contained in:
2026-08-21 11:20:09 +08:00
parent 595c998214
commit 6e9bb6e00c
45 changed files with 1051 additions and 329 deletions
@@ -1,10 +1,9 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { CalendarCheck2, Eye, Pencil } from '@lucide/vue'
import { CalendarCheck2, Eye, Pencil, Truck } from '@lucide/vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
@@ -14,6 +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 SmartDispatchDialog from '../components/SmartDispatchDialog.vue'
import { createFbaAppointment, getFbaAppointment, listInboundOrders, updateFbaAppointment, type InboundOrderSummaryDto } from '../api/inboundOrders'
import { overseasWarehousePermissions as permission } from '../permissions'
import type { InboundOrderListItem } from '../types'
@@ -22,30 +22,27 @@ const access = useAccessPreviewStore()
const router = useRouter()
const rows = ref<InboundOrderListItem[]>([])
const selectedRows = ref<InboundOrderListItem[]>([])
const customers = ref<PlatformTenantResource[]>([])
const loading = ref(false)
const customersLoading = ref(false)
const saving = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const customersError = ref<ApiErrorDetails | null>(null)
const mutationError = ref<ApiErrorDetails | null>(null)
const page = ref(1)
const pageSize = ref(20)
const total = ref(0)
const filters = ref<QueryModel>({ customerId: '', orderNo: '', inboundCode: '', status: '', warehouse: '', fbaWarehouse: '', appointmentStatus: '', dispatchStatus: '', inboundDate: [] })
const filters = ref<QueryModel>({ orderNo: '', inboundCode: '', status: '', warehouse: '', fbaWarehouse: '', appointmentStatus: '', dispatchStatus: '', inboundDate: [] })
const fields = computed<QueryField[]>(() => [
{ key: 'customerId', type: 'select', label: '客户名称', width: 240, placeholder: '请选择客户', props: { filterable: true, clearable: true, loading: customersLoading.value }, options: customers.value.map((item) => ({ label: item.tenantName, value: String(item.id) })) },
{ key: 'orderNo', type: 'input', label: '入库单号', placeholder: '请输入入库单号' },
{ key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码' },
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', options: [{ label: '待分配', value: 'PENDING_ASSIGNMENT' }, { label: '入库中', value: 'IN_PROGRESS' }, { label: '已完成', value: 'COMPLETED' }] },
{ key: 'warehouse', type: 'input', label: '海外仓', placeholder: '仓库编码或名称' },
{ key: 'fbaWarehouse', type: 'input', label: 'FBA仓库', placeholder: '请输入FBA仓库编码' },
{ key: 'appointmentStatus', type: 'select', label: '预约状态', placeholder: '全部预约状态', options: [{ label: '未预约', value: 'UNAPPOINTED' }, { label: '预约中', value: 'PENDING' }, { label: '预约成功', value: 'SUCCESS' }, { label: '预约失败', value: 'FAILED' }, { label: '已取消', value: 'CANCELLED' }] },
{ key: 'dispatchStatus', type: 'select', label: '排车状态', placeholder: '全部排车状态', options: [{ label: '未排车', value: 'UNSCHEDULED' }, { label: '已排车', value: 'SCHEDULED' }] },
{ key: 'inboundDate', type: 'daterange', label: '入库时间' },
{ key: 'orderNo', type: 'input', label: '入库单号', width: 165, placeholder: '入库单号' },
{ key: 'inboundCode', type: 'input', label: '入仓码', width: 145, placeholder: '入仓码' },
{ key: 'status', type: 'select', label: '状态', width: 130, placeholder: '全部状态', options: [{ label: '待分配', value: 'PENDING_ASSIGNMENT' }, { label: '入库中', value: 'IN_PROGRESS' }, { label: '已完成', value: 'COMPLETED' }] },
{ key: 'warehouse', type: 'input', label: '海外仓', width: 155, placeholder: '全部海外仓' },
{ key: 'fbaWarehouse', type: 'input', label: 'FBA仓库', width: 145, placeholder: '全部FBA仓库' },
{ key: 'appointmentStatus', type: 'select', label: '预约状态', width: 145, placeholder: '全部预约状态', options: [{ label: '未预约', value: 'UNAPPOINTED' }, { label: '预约中', value: 'PENDING' }, { label: '预约成功', value: 'SUCCESS' }, { label: '预约失败', value: 'FAILED' }, { label: '已取消', value: 'CANCELLED' }] },
{ key: 'dispatchStatus', type: 'select', label: '排车状态', width: 140, placeholder: '全部排车状态', options: [{ label: '未排车', value: 'UNSCHEDULED' }, { label: '已排车', value: 'SCHEDULED' }] },
{ key: 'inboundDate', type: 'daterange', label: '入库时间', width: 260 },
])
const appointmentOpen = ref(false)
const smartDispatchOpen = ref(false)
const appointmentLoading = ref(false)
const appointmentMode = ref<'create' | 'edit'>('create')
const editingAppointmentNo = ref('')
@@ -59,7 +56,7 @@ function selectableRow(source: object) {
const row = source as InboundOrderListItem
if (!row.fbaWarehouse || row.fbaWarehouse === '—') return false
const anchor = selectedRows.value[0]
return !anchor || (anchor.customerId === row.customerId && anchor.fbaWarehouse === row.fbaWarehouse)
return !anchor || anchor.fbaWarehouse === row.fbaWarehouse
}
const columns: DataTableColumn[] = [
@@ -115,6 +112,13 @@ function openAppointment() {
appointmentOpen.value = true
}
function openSmartDispatch() {
const error = !selectedRows.value.length ? '请至少选择 1 条入库单'
: new Set(selectedRows.value.map((row) => row.fbaWarehouse)).size !== 1 ? '所选入库单必须属于同一 FBA 仓库' : ''
if (error) { ElMessage.warning(error); return }
smartDispatchOpen.value = true
}
async function openAppointmentEdit(source: object) {
const row = source as InboundOrderListItem
if (!row.appointmentNo || row.appointmentNo === '—') return
@@ -144,16 +148,6 @@ async function submitAppointment() {
finally { saving.value = false }
}
async function loadCustomers() {
customersLoading.value = true; customersError.value = null
try {
const records: PlatformTenantResource[] = []; let currentPage = 1; let pages = 1
do { const result = await listPlatformTenants({ page: currentPage, size: 200, status: 'ACTIVE' }); records.push(...result.records); pages = Math.max(1, result.pages); currentPage += 1 } while (currentPage <= pages)
customers.value = records
} catch (error) { customersError.value = getApiErrorDetails(error, '客户列表加载失败,请重试') }
finally { customersLoading.value = false }
}
function dateRange() {
const range = Array.isArray(filters.value.inboundDate) ? filters.value.inboundDate : []
return { createdFrom: range[0] ? `${String(range[0]).slice(0, 10)}T00:00:00` : undefined, createdTo: range[1] ? `${String(range[1]).slice(0, 10)}T23:59:59` : undefined }
@@ -161,12 +155,10 @@ function dateRange() {
async function load(model: QueryModel = filters.value) {
Object.assign(filters.value, model)
const customerId = String(filters.value.customerId || '').trim()
selectedRows.value = []
if (!customerId) { rows.value = []; total.value = 0; loadError.value = null; return }
loading.value = true; loadError.value = null
try {
const result = await listInboundOrders({ customerId, page: page.value, size: pageSize.value, planNo: String(filters.value.orderNo || '').trim() || undefined, inboundCode: String(filters.value.inboundCode || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined, warehouse: String(filters.value.warehouse || '').trim() || undefined, fbaWarehouseCode: String(filters.value.fbaWarehouse || '').trim() || undefined, appointmentStatus: String(filters.value.appointmentStatus || '').trim() || undefined, dispatchStatus: String(filters.value.dispatchStatus || '').trim() || undefined, ...dateRange() })
const result = await listInboundOrders({ page: page.value, size: pageSize.value, planNo: String(filters.value.orderNo || '').trim() || undefined, inboundCode: String(filters.value.inboundCode || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined, warehouse: String(filters.value.warehouse || '').trim() || undefined, fbaWarehouseCode: String(filters.value.fbaWarehouse || '').trim() || undefined, appointmentStatus: String(filters.value.appointmentStatus || '').trim() || undefined, dispatchStatus: String(filters.value.dispatchStatus || '').trim() || undefined, ...dateRange() })
rows.value = result.records.map(toListItem); total.value = result.total
} catch (error) { rows.value = []; total.value = 0; loadError.value = getApiErrorDetails(error, '入库单加载失败,请重试') }
finally { loading.value = false }
@@ -175,20 +167,20 @@ async function load(model: QueryModel = filters.value) {
function search(model: QueryModel) { page.value = 1; void load(model) }
function reset(model: QueryModel) { page.value = 1; void load(model) }
function openDetail(source: object) { const row = source as InboundOrderListItem; void router.push({ name: 'overseas-inbound-order-detail', params: { orderId: row.orderNo }, query: { customerId: row.customerId } }) }
onMounted(loadCustomers)
onMounted(() => { void load() })
</script>
<template>
<AdminPage><OperationErrorToast :details="mutationError" /><ListPageLayout aria-label="海外仓入库单列表">
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" :loading="loading" ariaLabel="入库单筛选" preference-key="overseas-inbound-orders-query" @search="search" @reset="reset" /></template>
<template v-if="customersError || loadError" #state><LoadErrorState title="入库单加载失败" :message="(customersError || loadError)?.message" :loading="customersLoading || loading" @retry="customersError ? loadCustomers() : load()"><template #details><ApiErrorMeta :details="(customersError || loadError)!" /></template></LoadErrorState></template>
<section v-if="!customersError && !loadError" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-inbound-orders" row-key="lineId" :empty-text="filters.customerId ? '暂无入库单' : '请先选择客户后查询'" @selection-change="onSelectionChange" @update:page="load()" @update:page-size="page = 1; load()">
<template #toolbar-actions><el-button v-if="access.can(permission.inboundOrderAppointment)" type="primary" :disabled="!selectedRows.length" @click="openAppointment"><CalendarCheck2 :size="16" />FBA预约</el-button></template>
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button><el-button v-if="(row as InboundOrderListItem).appointmentNo !== '—' && access.can(permission.inboundOrderAppointment)" size="small" plain @click="openAppointmentEdit(row)"><Pencil :size="15" />编辑</el-button></template>
<template v-if="loadError" #state><LoadErrorState title="入库单加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template>
<section v-if="!loadError" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-inbound-orders" row-key="lineId" empty-text="暂无入库单" @selection-change="onSelectionChange" @update:page="load()" @update:page-size="page = 1; load()">
<template #toolbar-actions><el-button v-if="access.can(permission.inboundOrderEdit)" plain :disabled="!selectedRows.length" @click="openAppointment"><CalendarCheck2 :size="16" />预约</el-button><el-button v-if="access.can(permission.inboundOrderEdit)" type="primary" :disabled="!selectedRows.length" @click="openSmartDispatch"><Truck :size="16" />智能排车</el-button></template>
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button><el-button v-if="(row as InboundOrderListItem).appointmentNo !== '—' && access.can(permission.inboundOrderEdit)" size="small" plain @click="openAppointmentEdit(row)"><Pencil :size="15" />编辑</el-button></template>
<template #pagination-left>共 {{ total }} 条入库单 · 已选 {{ selectedRows.length }} 条</template>
</DataTable></section>
</ListPageLayout>
<AppDialog v-model="appointmentOpen" :title="appointmentTitle" width="min(720px, calc(100vw - 28px))" confirm-text="保存预约" :confirm-loading="saving" @confirm="submitAppointment">
<AppDialog v-model="appointmentOpen" :title="appointmentTitle" width="min(1080px, calc(100vw - 48px))" confirm-text="保存预约" :confirm-loading="saving" @confirm="submitAppointment">
<div v-loading="appointmentLoading">
<div class="appointment-summary">已选择 {{ appointmentSelectedCount }} 条,FBA仓库:{{ appointmentFbaWarehouse }}</div>
<el-form label-position="top" class="appointment-form">
@@ -200,6 +192,7 @@ onMounted(loadCustomers)
</el-form>
</div>
</AppDialog>
<SmartDispatchDialog v-model="smartDispatchOpen" :orders="selectedRows" @created="selectedRows = []; load()" />
</AdminPage>
</template>