This commit is contained in:
2026-08-12 12:35:32 +08:00
parent 11e085e30f
commit 4fb8b85c13
22 changed files with 161 additions and 47 deletions
@@ -9,6 +9,7 @@ import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
import DataTable from '@/shared/components/DataTable.vue'
import { listPlatformWarehouses } from '@/modules/warehouses'
import { useAuthStore } from '@/core/auth/auth'
import { downloadInboundCartonLabels, getPlatformInbound, listPlatformInbounds } from '../core/api'
import type { InboundOrderDetail, InboundOrderSummary } from '../core/types'
@@ -73,6 +74,7 @@ describe('InboundOrdersView', () => {
beforeEach(() => {
vi.clearAllMocks()
setActivePinia(createPinia())
useAuthStore().permissions = ['platform:firstleg:inbound:view']
vi.mocked(listPlatformInbounds).mockResolvedValue({ records: [summary], page: 1, size: 20, total: 1, pages: 1 })
vi.mocked(getPlatformInbound).mockResolvedValue(detail)
vi.mocked(downloadInboundCartonLabels).mockResolvedValue(new Blob(['pdf'], { type: 'application/pdf' }))
@@ -207,6 +209,16 @@ describe('InboundOrdersView', () => {
vi.unstubAllGlobals()
})
it('does not expose row operations without the confirmed inbound view permission', async () => {
useAuthStore().permissions = []
const wrapper = mountPage()
await flushPromises()
expect(wrapper.find('.row-actions').exists()).toBe(false)
expect(wrapper.text()).toContain('无查看权限')
expect(wrapper.text()).not.toContain('打印箱唛')
})
it('uses the app header as the page title and exposes compact accessible status tabs', async () => {
const wrapper = mountPage()
await flushPromises()
@@ -317,6 +329,9 @@ describe('InboundOrdersView', () => {
expect(columnSource).toContain("{ key: 'feeReviewStatus', label: '费用审核状态'")
expect(source).toContain('<template #processStatus="{ row }">')
expect(source).toContain('<template #feeReviewStatus="{ row }">')
expect(source).toContain("access.can('platform:firstleg:inbound:view')")
expect(source).toContain('preference-key="inbound-orders-table"')
expect(columnSource.match(/defaultVisible: false/g)?.length).toBeGreaterThanOrEqual(25)
expect(toolbarSource).toMatch(/@media \(max-width: 720px\)[\s\S]*width: 100%/)
})
})
@@ -3,6 +3,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { ChevronDown, ChevronUp, ClipboardCheck, Eye, Printer, RefreshCw, SlidersHorizontal } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDrawer from '@/shared/components/AppDrawer.vue'
@@ -40,6 +41,7 @@ type FilterModel = QueryModel & {
}
const router = useRouter()
const access = useAccessPreviewStore()
const tab = ref<InboundListTab | ''>('')
const page = ref(1)
const pageSize = ref(20)
@@ -133,35 +135,35 @@ const columns: DataTableColumn[] = [
{ key: 'customerName', label: '客户', minWidth: 142, slotName: 'customerName' },
{ key: 'warehouseName', label: '仓库', minWidth: 140, slotName: 'warehouseName' },
{ key: 'deliveryWarehouseName', label: '交货仓', minWidth: 140, slotName: 'deliveryWarehouseName' },
{ key: 'loadingStatus', label: '配载状态', minWidth: 110, slotName: 'loadingStatus' },
{ key: 'holdFlag', label: '是否扣货', width: 96, slotName: 'holdFlag' },
{ key: 'currentHoldReason', label: '扣货理由', minWidth: 150, slotName: 'currentHoldReason' },
{ key: 'containerNo', label: '集装箱号', minWidth: 132, slotName: 'containerNo' },
{ key: 'cabinetNo', label: '柜号', minWidth: 118, slotName: 'cabinetNo' },
{ key: 'companyName', label: '公司', minWidth: 160, slotName: 'companyName' },
{ key: 'businessType', label: '业务类型', minWidth: 112, slotName: 'businessType' },
{ key: 'channelName', label: '渠道', minWidth: 138, slotName: 'channelName' },
{ key: 'taxMode', label: '买单/退税', minWidth: 108, slotName: 'taxMode' },
{ key: 'containerMode', label: '是否整柜', minWidth: 108, slotName: 'containerMode' },
{ key: 'customsInspection', label: '商检验', width: 92, slotName: 'customsInspection' },
{ key: 'sailingAt', label: '开船时间', minWidth: 164, slotName: 'sailingAt' },
{ key: 'portArrivalAt', label: '到港时间', minWidth: 164, slotName: 'portArrivalAt' },
{ key: 'totalPackageCount', label: '总箱数', width: 90, slotName: 'totalPackageCount' },
{ key: 'actualGrossWeightKg', label: '总毛重(T.K.GW)', minWidth: 136, slotName: 'actualGrossWeightKg' },
{ key: 'chargeableWeightKg', label: '计费毛重', minWidth: 112, slotName: 'chargeableWeightKg' },
{ key: 'actualVolumeCbm', label: '总体积(m³)', minWidth: 118, slotName: 'actualVolumeCbm' },
{ key: 'chargeableVolumeCbm', label: '计费体积(m³)', minWidth: 128, slotName: 'chargeableVolumeCbm' },
{ key: 'palletCount', label: '总托盘数', width: 100, slotName: 'palletCount' },
{ key: 'locationCodes', label: '库位位置', minWidth: 124, slotName: 'locationCodes' },
{ key: 'volumeWeightKg', label: '总材积重', minWidth: 110, slotName: 'volumeWeightKg' },
{ key: 'actualNetWeightKg', label: '总净重(T.K.NW)', minWidth: 136, slotName: 'actualNetWeightKg' },
{ key: 'destinationType', label: '目的仓类型', minWidth: 112, slotName: 'destinationType' },
{ key: 'destinationName', label: '目的仓', minWidth: 132, slotName: 'destinationName' },
{ key: 'measureCompletedAt', label: '测量完成时间', minWidth: 164, slotName: 'measureCompletedAt' },
{ key: 'inboundedAt', label: '入库完成时间', minWidth: 164, slotName: 'inboundedAt' },
{ key: 'expressCompanyName', label: '快递公司', minWidth: 120, slotName: 'expressCompanyName' },
{ key: 'expressTrackingNo', label: '快递单号', minWidth: 150, slotName: 'expressTrackingNo' },
{ key: 'createdAt', label: '创建日期', minWidth: 164, slotName: 'createdAt' },
{ key: 'loadingStatus', label: '配载状态', minWidth: 110, slotName: 'loadingStatus', defaultVisible: false },
{ key: 'holdFlag', label: '是否扣货', width: 96, slotName: 'holdFlag', defaultVisible: false },
{ key: 'currentHoldReason', label: '扣货理由', minWidth: 150, slotName: 'currentHoldReason', defaultVisible: false },
{ key: 'containerNo', label: '集装箱号', minWidth: 132, slotName: 'containerNo', defaultVisible: false },
{ key: 'cabinetNo', label: '柜号', minWidth: 118, slotName: 'cabinetNo', defaultVisible: false },
{ key: 'companyName', label: '公司', minWidth: 160, slotName: 'companyName', defaultVisible: false },
{ key: 'businessType', label: '业务类型', minWidth: 112, slotName: 'businessType', defaultVisible: false },
{ key: 'channelName', label: '渠道', minWidth: 138, slotName: 'channelName', defaultVisible: false },
{ key: 'taxMode', label: '买单/退税', minWidth: 108, slotName: 'taxMode', defaultVisible: false },
{ key: 'containerMode', label: '是否整柜', minWidth: 108, slotName: 'containerMode', defaultVisible: false },
{ key: 'customsInspection', label: '商检验', width: 92, slotName: 'customsInspection', defaultVisible: false },
{ key: 'sailingAt', label: '开船时间', minWidth: 164, slotName: 'sailingAt', defaultVisible: false },
{ key: 'portArrivalAt', label: '到港时间', minWidth: 164, slotName: 'portArrivalAt', defaultVisible: false },
{ key: 'totalPackageCount', label: '总箱数', width: 90, slotName: 'totalPackageCount', defaultVisible: false },
{ key: 'actualGrossWeightKg', label: '总毛重(T.K.GW)', minWidth: 136, slotName: 'actualGrossWeightKg', defaultVisible: false },
{ key: 'chargeableWeightKg', label: '计费毛重', minWidth: 112, slotName: 'chargeableWeightKg', defaultVisible: false },
{ key: 'actualVolumeCbm', label: '总体积(m³)', minWidth: 118, slotName: 'actualVolumeCbm', defaultVisible: false },
{ key: 'chargeableVolumeCbm', label: '计费体积(m³)', minWidth: 128, slotName: 'chargeableVolumeCbm', defaultVisible: false },
{ key: 'palletCount', label: '总托盘数', width: 100, slotName: 'palletCount', defaultVisible: false },
{ key: 'locationCodes', label: '库位位置', minWidth: 124, slotName: 'locationCodes', defaultVisible: false },
{ key: 'volumeWeightKg', label: '总材积重', minWidth: 110, slotName: 'volumeWeightKg', defaultVisible: false },
{ key: 'actualNetWeightKg', label: '总净重(T.K.NW)', minWidth: 136, slotName: 'actualNetWeightKg', defaultVisible: false },
{ key: 'destinationType', label: '目的仓类型', minWidth: 112, slotName: 'destinationType', defaultVisible: false },
{ key: 'destinationName', label: '目的仓', minWidth: 132, slotName: 'destinationName', defaultVisible: false },
{ key: 'measureCompletedAt', label: '测量完成时间', minWidth: 164, slotName: 'measureCompletedAt', defaultVisible: false },
{ key: 'inboundedAt', label: '入库完成时间', minWidth: 164, slotName: 'inboundedAt', defaultVisible: false },
{ key: 'expressCompanyName', label: '快递公司', minWidth: 120, slotName: 'expressCompanyName', defaultVisible: false },
{ key: 'expressTrackingNo', label: '快递单号', minWidth: 150, slotName: 'expressTrackingNo', defaultVisible: false },
{ key: 'createdAt', label: '创建日期', minWidth: 164, slotName: 'createdAt', defaultVisible: false },
]
const selectedCustomerName = computed(() => {
@@ -452,7 +454,7 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
</header>
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="result.records" :columns="columns" preference-key="inbound-orders-table" :loading="loading" :total="result.total" :page-sizes="[20, 50, 100]" row-key="id" :empty-text="listEmptyText" aria-label="入库单列表">
<template #pagination-left>当前显示 {{ currentPageCount }} 条 · 共 {{ result.total }} 条</template>
<template #actions="{ row }"><div class="row-actions"><el-button link type="primary" @click="openWorkspace(row as InboundOrderSummary)"><ClipboardCheck :size="15" />处理</el-button><el-button link :loading="printingInboundId === (row as InboundOrderSummary).id" :disabled="Boolean(printingInboundId)" @click="printCartonLabels(row as InboundOrderSummary)"><Printer :size="15" />打印箱唛</el-button><el-button link @click="openDetail(row as InboundOrderSummary)"><Eye :size="15" />查看</el-button></div></template>
<template #actions="{ row }"><div v-if="access.can('platform:firstleg:inbound:view')" class="row-actions"><el-button link type="primary" @click="openWorkspace(row as InboundOrderSummary)"><ClipboardCheck :size="15" />处理</el-button><el-button link :loading="printingInboundId === (row as InboundOrderSummary).id" :disabled="Boolean(printingInboundId)" @click="printCartonLabels(row as InboundOrderSummary)"><Printer :size="15" />打印箱唛</el-button><el-button link @click="openDetail(row as InboundOrderSummary)"><Eye :size="15" />查看</el-button></div><span v-else class="muted-action">无查看权限</span></template>
<template #inbound="{ row }"><div class="primary-cell"><strong>{{ text((row as InboundOrderSummary).inboundCode) }}</strong><span>订单 {{ text((row as InboundOrderSummary).orderNo) }}</span></div></template>
<template #processStatus="{ row }"><el-tag :type="statusType((row as InboundOrderSummary).processStatus)" effect="light">{{ statusLabel((row as InboundOrderSummary).processStatus) }}</el-tag></template>
<template #orderStatus="{ row }">{{ codeLabel((row as InboundOrderSummary).orderStatusSnapshot, inboundOrderStatusLabels) }}</template>
@@ -5,6 +5,7 @@ import { DOMWrapper, enableAutoUnmount, flushPromises, mount, type VueWrapper }
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import { ApiResponseError } from '@/shared/api/http'
import {
confirmWeightResult,
correctWeightResult,
@@ -482,7 +483,7 @@ describe('WeightOcrPanel confirmation behavior', () => {
})
it('keeps the edited draft available when confirmation fails', async () => {
vi.mocked(confirmWeightResult).mockRejectedValueOnce(new Error('version conflict'))
vi.mocked(confirmWeightResult).mockRejectedValueOnce(new ApiResponseError('数据版本已变化,请刷新后重试', 'A0409', 409))
const wrapper = mountPanel()
await flushPromises()
await openWeightDialog(wrapper)
@@ -493,7 +494,7 @@ describe('WeightOcrPanel confirmation behavior', () => {
await flushPromises()
expect((input.element as HTMLInputElement).value).toBe('286.125')
expect(wrapper.text()).toContain('version conflict')
expect(wrapper.text()).toContain('数据版本已变化,请刷新后重试')
expect(confirmWeightResult).toHaveBeenCalledTimes(1)
})
})