fix(admin): render appointment table actions and history

This commit is contained in:
2026-10-10 08:05:21 +08:00
parent 302cc50a4b
commit df4d3dfd66
4 changed files with 48 additions and 4 deletions
@@ -69,7 +69,7 @@ const statusLabels: Record<string, string> = { PENDING: '预约中', SUCCESS: '
const columns: DataTableColumn[] = [
{ key: 'selection', type: 'selection', reserveSelection: true, width: 46, selectable: (row: InboundOrderSummaryDto) => row.fbaWarehouseCode === searchedWarehouse.value && (!row.appointmentStatus || ['FAILED', 'CANCELLED'].includes(row.appointmentStatus)) },
{ prop: 'inboundCode', label: '入仓码', minWidth: 150 },
{ prop: 'customerName', label: '客户', minWidth: 150 },
{ prop: 'customerName', label: '客户', slotName: 'customer', minWidth: 150 },
{ prop: 'fbaWarehouseCode', label: '目的仓', width: 110 },
{ prop: 'overseasWarehouseName', label: '海外仓', minWidth: 150 },
{ prop: 'packageCount', label: '箱数', width: 80 },
@@ -336,6 +336,7 @@ onBeforeUnmount(() => { ++calendarRequest; ++candidateRequest; ++detailRequest;
</details>
<template v-if="!editing && warehouse">
<DataTable :key="selectionEpoch" v-model:page="candidatePage" v-model:page-size="candidateSize" :data="candidates" :columns="columns" :total="candidateTotal" :loading="candidateLoading" row-key="lineId" :results-visible="!candidateError" @selection-change="selectCandidates" @update:page="loadCandidates()" @update:page-size="candidatePage = 1; loadCandidates()">
<template #customer="{ row }">{{ row.customerName?.trim() || '客户名称缺失' }}</template>
<template #notices><LoadErrorState v-if="candidateError" title="货物加载失败" :message="candidateError.message" @retry="loadCandidates()" /></template>
<template #pagination-left>已选 {{ selected.length }} 票,可跨客户选择同一目的仓货物</template>
</DataTable>
@@ -25,7 +25,7 @@ const error = ref<ApiErrorDetails | null>(null)
let request = 0
let controller: AbortController | undefined
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', slot: 'actions', width: 150, fixed: 'left' },
{ key: 'actions', label: '操作', slotName: 'actions', width: 150, fixed: 'left' },
{ prop: 'inboundCode', label: '入仓码', minWidth: 180 },
{ prop: 'customerName', label: '客户', minWidth: 150 },
{ prop: 'containerNo', label: '柜号', minWidth: 150 },
@@ -35,7 +35,7 @@ const columns: DataTableColumn[] = [
{ prop: 'receivedPackages', label: '实收箱数', width: 100 },
{ prop: 'estimatedArrivalAt', label: '来源预计到港', minWidth: 170 },
{ prop: 'estimatedWarehouseAt', label: '已保存预计到仓', minWidth: 170 },
{ key: 'progress', label: '当前事项', slot: 'progress', minWidth: 170 },
{ key: 'progress', label: '当前事项', slotName: 'progress', minWidth: 170 },
]
async function load(reset = false) {
@@ -32,7 +32,7 @@ const error = ref<ApiErrorDetails | null>(null), saveError = ref<ApiErrorDetails
const pending = ref<SaveFollowup | null>(null)
let generation = 0
const columns: DataTableColumn[] = [
{ key: 'stage', label: '事项', slot: 'stage', width: 120 },
{ key: 'stage', label: '事项', slotName: 'stage', width: 120 },
{ prop: 'contactedParty', label: '联系对象', minWidth: 130 },
{ prop: 'result', label: '联系结果', minWidth: 200 },
{ prop: 'nextAction', label: '下一步', minWidth: 150 },
@@ -0,0 +1,43 @@
import ElementPlus from 'element-plus'
import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import AppointmentCargoPanel from './AppointmentCargoPanel.vue'
import AppointmentFollowupDialog from './AppointmentFollowupDialog.vue'
import { getFollowups, listPlanningCargo, resolvePlanningSource, type PlanningCargo, type PlanningSource } from './planningApi'
vi.mock('./planningApi', () => ({ listPlanningCargo: vi.fn(), resolvePlanningSource: vi.fn(), getFollowups: vi.fn(), saveFollowup: vi.fn() }))
vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) }))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
const source: PlanningSource = { sourceKey: 'ORDER:1', customerId: 'A', orderId: '1', inboundCode: 'IN-A', bookingNo: null, containerNo: 'CONTAINER-A', originWarehouseId: '100', originWarehouseName: '美国仓', destinationKey: 'FBA:ONT8', destinationWarehouseCode: 'ONT8', packages: 20, sourceVersion: 'ORDER:1', canFollowup: true, blockReasons: [] }
const cargo: PlanningCargo = { sourceKey: source.sourceKey, customerId: 'A', customerName: '客户甲', orderId: '1', inboundCode: 'IN-A', containerBookingId: '2', containerNo: 'CONTAINER-A', originWarehouseId: '100', originWarehouseCode: 'US', originWarehouseName: '美国仓', destinationWarehouseCode: 'ONT8', packages: 20, receivedPackages: null, shippedPackages: null, sourcePalletCount: null, volumeCbm: null, estimatedArrivalAt: null, estimatedWarehouseAt: null, orderVersion: 1, standardNode: 'DEPARTED', shippingVersion: null, shippingObservedAt: null, shippingProvider: null, hasInboundPlan: false, hasActiveAppointment: false, detained: false, mixedDestinations: false }
const coverage = { status: 'UNASSIGNED', appointments: [], missingFields: [], changeReviewRequired: false }
describe('appointment tables use actual shared table slots', () => {
beforeEach(() => vi.resetAllMocks())
it('renders working source actions and readable progress through DataTable and RowActions', async () => {
vi.mocked(listPlanningCargo).mockResolvedValue({ records: [cargo], page: 1, size: 20, total: 1, pages: 1 })
vi.mocked(resolvePlanningSource).mockResolvedValue({ source, coverage })
const wrapper = mount(AppointmentCargoPanel, { global: { plugins: [ElementPlus], stubs: { AppointmentFollowupDialog: true } } })
await flushPromises()
const row = wrapper.get('.el-table__body-wrapper .el-table__row')
expect(row.text()).toContain('IN-A')
expect(row.text()).toContain('尚未建入仓计划')
const followup = row.findAll('button').find(button => button.text() === '跟进')
expect(followup).toBeDefined()
expect(row.findAll('button').some(button => button.text() === '查看来源')).toBe(true)
await followup!.trigger('click')
await flushPromises()
expect(resolvePlanningSource).toHaveBeenCalledExactlyOnceWith({ sourceKey: 'ORDER:1', customerId: 'A', destinationKey: 'FBA:ONT8' })
expect(wrapper.findComponent(AppointmentFollowupDialog).exists()).toBe(true)
wrapper.unmount()
})
it('renders followup stage names instead of blank table cells', async () => {
vi.mocked(getFollowups).mockResolvedValue({ history: { records: [{ stage: 'FBA_RECEIVING', version: 1, contactedParty: null, result: '等待回复', nextAction: null, followupDate: null, operatorName: '运营', createdAt: '2026-10-09T12:00:00Z' }], total: 1, page: 1, size: 10, pages: 1 }, versions: { DRAYAGE_CONTACT: 0, TERMINAL_APPOINTMENT: 0, WAREHOUSE_RECEIVING: 0, FBA_RECEIVING: 1 } })
const wrapper = mount(AppointmentFollowupDialog, { props: { source, coverage, canWrite: false }, global: { plugins: [ElementPlus], stubs: { AppDialog: { template: '<section><slot /></section>' } } } })
await flushPromises()
const row = wrapper.get('.el-table__body-wrapper .el-table__row')
expect(row.text()).toContain('FBA收货跟进')
expect(row.text()).toContain('等待回复')
wrapper.unmount()
})
})