This commit is contained in:
2026-08-12 17:29:35 +08:00
parent 16c5ed6301
commit d2a246b122
25 changed files with 238 additions and 69 deletions
@@ -19,11 +19,11 @@ const detail = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/co
class ResizeObserverStub { observe() {}; unobserve() {}; disconnect() {} }
function memoryStorage(): Storage { const values = new Map<string, string>(); return { get length() { return values.size }, clear: () => values.clear(), getItem: (key) => values.get(key) ?? null, key: (index) => [...values.keys()][index] ?? null, removeItem: (key) => values.delete(key), setItem: (key, value) => values.set(key, value) } }
async function mountPanel(permissions: string[], customers = [{ accountId: '11', customerName: '客户A' }]) {
async function mountPanel(permissions: string[], customers = [{ accountId: '11', customerName: '客户A' }], containerBookingId = '99') {
const pinia = createPinia(); setActivePinia(pinia)
const auth = useAuthStore(); auth.token = 'token'; auth.permissions = permissions
useAccessPreviewStore().initializeAfterLogin()
const wrapper = mount(ContainerFeesPanel, { attachTo: document.body, props: { planId: '9', customers }, global: { plugins: [pinia, ElementPlus] } })
const wrapper = mount(ContainerFeesPanel, { attachTo: document.body, props: { planId: '9', containerBookingId, customers }, global: { plugins: [pinia, ElementPlus] } })
await flushPromises(); return wrapper
}
@@ -37,7 +37,7 @@ beforeEach(() => {
describe('ContainerFeesPanel contract', () => {
it('is exposed from loading-list detail and derives customer context from real items', () => {
expect(detail).toContain('<ContainerFeesPanel :plan-id="summary.planId" :customers="feeCustomers" />')
expect(detail).toContain(':container-booking-id="summary.containerBookingId"')
expect(detail).toContain('item.accountId')
expect(panel).toContain('该柜次包含多个客户,请先选择费用归属客户')
expect(panel).toContain('装柜明细缺少客户归属,无法安全维护费用')
@@ -79,6 +79,29 @@ describe('ContainerFeesPanel contract', () => {
expect(listContainerCostDetails).not.toHaveBeenCalledWith('11', expect.anything())
})
it('uses the summary container ID when fee rows are empty', async () => {
vi.mocked(listContainerFees).mockResolvedValue([])
await mountPanel(['firstleg:inbound:fee-detail:view'])
expect(listContainerCostDetails).toHaveBeenCalledWith('11', '99')
})
it('uses the summary container ID when fee rows agree', async () => {
vi.mocked(listContainerFees).mockResolvedValue([
{ id: '1', loadingPlanId: '9', containerBookingId: '99', containerNo: null, feeItemId: '1', feeItemName: '费用A', direction: 'RECEIVABLE', partnerId: null, partnerName: null, businessDate: '2026-08-12', amount: 10, currency: 'CNY', settlementIncluded: true, reviewStatus: 'PENDING', sourceStatus: 'ACTIVE', sourceVersion: 1, remark: null, reviewerName: null, reviewedAt: null, reviewReason: null },
])
await mountPanel(['firstleg:inbound:fee-detail:view'])
expect(listContainerCostDetails).toHaveBeenCalledWith('11', '99')
})
it('fails closed when fee rows disagree with the summary container', async () => {
vi.mocked(listContainerFees).mockResolvedValue([
{ id: '2', loadingPlanId: '9', containerBookingId: '100', containerNo: null, feeItemId: '2', feeItemName: '费用B', direction: 'PAYABLE', partnerId: '41', partnerName: '承运商A', businessDate: '2026-08-12', amount: 20, currency: 'CNY', settlementIncluded: true, reviewStatus: 'PENDING', sourceStatus: 'ACTIVE', sourceVersion: 1, remark: null, reviewerName: null, reviewedAt: null, reviewReason: null },
])
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:fee-item:view'])
expect(wrapper.text()).toContain('柜次费用加载失败')
expect(listContainerCostDetails).not.toHaveBeenCalled()
})
it('blocks write entry points without a selected customer or edit permission', async () => {
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view'], [{ accountId: '11', customerName: '客户A' }, { accountId: '22', customerName: '客户B' }])
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; save(): Promise<void>; editorOpen: boolean } }).setupState
@@ -11,7 +11,7 @@ import StatusBadge from '@/shared/components/StatusBadge.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { adjustContainerFee, approveContainerFee, createContainerFee, listApplicableFeeItems, listContainerCostDetails, listContainerFeePartnerOptions, listContainerFees, rejectContainerFee, voidContainerFee, type ApplicableFeeItem, type ContainerCostDetail, type ContainerFeePartnerOption, type ContainerFeeResource, type ContainerFeeSaveRequest } from '../api/containerFees'
const props = defineProps<{ planId: string; customers: Array<{ accountId: string; customerName: string | null }> }>()
const props = defineProps<{ planId: string; containerBookingId: string; customers: Array<{ accountId: string; customerName: string | null }> }>()
const access = useAccessPreviewStore()
const accountId = ref('')
const fees = ref<ContainerFeeResource[]>([])
@@ -46,7 +46,6 @@ const customerOptions = computed(() => {
const selectedCustomer = computed(() => customerOptions.value.find((item) => item.value === accountId.value))
const applicableItems = computed(() => feeItems.value.filter((item) => form.direction === 'RECEIVABLE'
? item.warehouseReceivable || item.pickupReceivable : item.warehousePayable || item.pickupPayable))
const containerId = computed(() => fees.value.find((item) => item.containerBookingId)?.containerBookingId || null)
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', fixed: 'left', width: 300, slotName: 'actions', hideable: false, movable: false },
{ key: 'feeItemName', prop: 'feeItemName', label: '费用项目', minWidth: 150 },
@@ -102,7 +101,9 @@ async function load() {
if (sequence !== loadSequence || requestedAccountId !== accountId.value) return
fees.value = feeRows
costs.value = []
const currentContainerId = feeRows.find((item) => item.containerBookingId)?.containerBookingId
const containerIds = [...new Set(feeRows.map((item) => item.containerBookingId).filter((value): value is string => Boolean(value)))]
if (containerIds.some((value) => value !== props.containerBookingId)) throw new Error('柜次费用与装柜摘要的柜次标识不一致,已停止查询财务分摊')
const currentContainerId = props.containerBookingId
if (currentContainerId) {
const result = await listContainerCostDetails(requestedAccountId, currentContainerId)
if (sequence !== loadSequence || requestedAccountId !== accountId.value) return
@@ -119,7 +119,7 @@ function formatNumber(value?: number | null, digits = 3) {
</DataTable>
</section>
<ContainerFeesPanel :plan-id="summary.planId" :customers="feeCustomers" />
<ContainerFeesPanel :plan-id="summary.planId" :container-booking-id="summary.containerBookingId" :customers="feeCustomers" />
<section class="history-section surface-panel" aria-label="操作历史">
<header><h2>操作历史</h2><span>{{ detail.histories.length }} 条</span></header>