优化
This commit is contained in:
@@ -17,7 +17,6 @@ import {
|
||||
removeSmartLoadingGroup,
|
||||
removeSmartLoadingItem,
|
||||
} from '../api/platformContainerLoading'
|
||||
import { batchQueryPlatformCoLoadGroups } from '@/modules/unified-orders'
|
||||
|
||||
const { routerPush } = vi.hoisted(() => ({ routerPush: vi.fn() }))
|
||||
|
||||
@@ -31,9 +30,6 @@ vi.mock('@/core/access/accessPreview', () => ({
|
||||
vi.mock('@/modules/iam', () => ({
|
||||
listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }),
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders', () => ({
|
||||
batchQueryPlatformCoLoadGroups: vi.fn().mockResolvedValue([]),
|
||||
}))
|
||||
vi.mock('../api/platformContainerLoading', () => ({
|
||||
listContainers: vi.fn(),
|
||||
getSmartLoadingPlanByBooking: vi.fn(),
|
||||
@@ -47,6 +43,7 @@ vi.mock('../api/platformContainerLoading', () => ({
|
||||
removeSmartLoadingGroup: vi.fn(),
|
||||
pushSmartLoadingPreload: vi.fn(),
|
||||
}))
|
||||
vi.mock('../smartLoadingPlanGroups', () => ({ loadSmartLoadingPlanCoLoadGroups: vi.fn().mockResolvedValue(new Map()) }))
|
||||
|
||||
const container = {
|
||||
id: '9007199254740993', planId: '9007199254740994', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1',
|
||||
@@ -70,7 +67,6 @@ describe('GlobalSmartLoadingView', () => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [container], page: 1, size: 100, total: 1, pages: 1 })
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(plan)
|
||||
vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue([])
|
||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([{ accountId: '1', inboundId: '5', orderId: '7', orderNo: 'ORD-2', inboundCode: 'IN-2', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '另一企业', goodsSummary: '鞋类', availablePackageCount: 12, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 2.4, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null, locked: null, blockReason: null }])
|
||||
})
|
||||
|
||||
@@ -244,36 +240,6 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(assignSmartLoadingGroup).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('aggregates assigned members into one locked group row and removes the group once', async () => {
|
||||
const baseItems = ['201', '202'].map((inboundId, index) => ({
|
||||
...plan.items[0], id: `70${index}`, accountId: `${index + 1}`, inboundId, inboundDetailId: `80${index}`, orderId: `30${index}`, orderNo: `ORD-${index}`, inboundCode: `NYEXX260829005${index + 1}`, customerName: `客户${index}`, packageCount: 5, grossWeightKg: 50, volumeCbm: 1, loadSequence: index + 1,
|
||||
}))
|
||||
const items = [baseItems[0], { ...baseItems[0], id: '703', inboundId: '203', inboundDetailId: '803', inboundCode: 'NYEXX2608290051-B', packageCount: 3, grossWeightKg: 30, volumeCbm: 0.6 }, baseItems[1]]
|
||||
const groupedPlan = { ...plan, items }
|
||||
const group = {
|
||||
groupId: '2093539779795795969', groupNo: 'CLG-2093539779795795969', status: 'LOCKED' as const, warehouseId: '8', channelCode: 'ZBYHK', lockedPlanId: '99', version: 1,
|
||||
members: items.map((item) => ({ tenantId: item.accountId, orderId: item.orderId, inboundId: item.inboundId, inboundCode: item.inboundCode, fullPackageCount: item.packageCount })), fullPackageCount: 13, createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:30:00',
|
||||
}
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValueOnce(groupedPlan).mockResolvedValue({ ...groupedPlan, version: 3, items: [] })
|
||||
vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue([...new Set(items.map((item) => item.orderId))].map((orderId) => ({ orderId, group })))
|
||||
vi.mocked(removeSmartLoadingGroup).mockResolvedValue({ ...groupedPlan, version: 3, items: [] })
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('CLG-2093539779795795969 · 2 票 · 3 个入仓片段')
|
||||
expect(wrapper.text()).toContain('ORD-0:NYEXX2608290051、NYEXX2608290051-B')
|
||||
expect(wrapper.text()).toContain('固定 13 箱 · 已锁定')
|
||||
expect(wrapper.text()).toContain('整组固定全箱数,不允许单票调整')
|
||||
expect(wrapper.findAllComponents(ElInputNumber).some((input) => input.props('modelValue') === 13)).toBe(false)
|
||||
const removeButton = wrapper.findAll('button').find((button) => button.text().includes('移除整组'))!
|
||||
await removeButton.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(removeSmartLoadingGroup).toHaveBeenCalledWith(plan.id, group.groupId, plan.version)
|
||||
expect(removeSmartLoadingItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps the package count editor inside its table cell without overflow ellipsis', () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
|
||||
expect(source).toContain('<div v-else class="package-count-editor"><el-input-number')
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Plus, RefreshCw, Save, Sparkles, Trash2, Upload } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { batchQueryPlatformCoLoadGroups, type CoLoadGroup } from '@/modules/unified-orders'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
@@ -32,6 +31,7 @@ import type {
|
||||
SmartLoadingPlan,
|
||||
} from '../types'
|
||||
import { groupSmartLoadingItems, type SmartLoadingInboundItem } from '../smartLoadingItems'
|
||||
import { loadSmartLoadingPlanCoLoadGroups, type SmartLoadingPlanCoLoadGroup } from '../smartLoadingPlanGroups'
|
||||
|
||||
const props = withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: false })
|
||||
|
||||
@@ -55,7 +55,7 @@ const assignOpen = ref(false)
|
||||
const selectedCandidate = ref<SmartLoadingCandidateRow | null>(null)
|
||||
const assignForm = reactive({ packageCount: 1 })
|
||||
const quantityDrafts = reactive<Record<string, number>>({})
|
||||
const planGroupsByInboundId = ref(new Map<string, CoLoadGroup>())
|
||||
const planGroupsByInboundId = ref(new Map<string, SmartLoadingPlanCoLoadGroup>())
|
||||
const query = ref<QueryModel>({ customerId: '', inboundCode: '' })
|
||||
const appliedQuery = ref<QueryModel>({ ...query.value })
|
||||
let selectionRequest = 0
|
||||
@@ -95,10 +95,10 @@ function memberOrderSummaries<T extends { orderId: string; orderNo?: string | nu
|
||||
}
|
||||
return [...orders.values()].map((order) => `${order.label}:${order.inboundCodes.join('、')}`)
|
||||
}
|
||||
function groupOrderCount(group: CoLoadGroup) { return new Set(group.members.map((member) => member.orderId)).size }
|
||||
function groupMemberLabel(group: CoLoadGroup) {
|
||||
const orderCount = groupOrderCount(group)
|
||||
return group.members.length === orderCount ? `${orderCount} 票` : `${orderCount} 票 · ${group.members.length} 个入仓片段`
|
||||
function groupMemberLabel(group: SmartLoadingPlanCoLoadGroup) {
|
||||
return group.inboundFragmentCount === group.memberOrderCount
|
||||
? `${group.memberOrderCount} 票`
|
||||
: `${group.memberOrderCount} 票 · ${group.inboundFragmentCount} 个入仓片段`
|
||||
}
|
||||
const displayedCandidates = computed<SmartLoadingCandidateRow[]>(() => {
|
||||
const rows: SmartLoadingCandidateRow[] = []
|
||||
@@ -128,7 +128,7 @@ const displayedCandidates = computed<SmartLoadingCandidateRow[]>(() => {
|
||||
}
|
||||
return rows
|
||||
})
|
||||
type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup: CoLoadGroup | null; memberInboundCodes: string[]; memberOrderSummaries: string[] }
|
||||
type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup: SmartLoadingPlanCoLoadGroup | null; memberInboundCodes: string[]; memberOrderSummaries: string[] }
|
||||
const displayedPlanItems = computed<SmartLoadingPlanRow[]>(() => {
|
||||
const inboundRows = groupSmartLoadingItems(plan.value?.items || [])
|
||||
const result: SmartLoadingPlanRow[] = []
|
||||
@@ -261,18 +261,6 @@ function acceptPlan(next: SmartLoadingPlan, preserved: Record<string, number> =
|
||||
if (syncContainerStatus) container.planStatus = next.status
|
||||
}
|
||||
}
|
||||
async function loadPlanGroups(request = selectionRequest) {
|
||||
planGroupsByInboundId.value = new Map()
|
||||
const currentPlan = plan.value
|
||||
const orderIds = [...new Set((currentPlan?.items || []).map((item) => item.orderId))]
|
||||
if (!currentPlan || !orderIds.length || !canAssign.value) return
|
||||
const groups = await batchQueryPlatformCoLoadGroups(orderIds)
|
||||
if (request !== selectionRequest || plan.value?.id !== currentPlan.id) return
|
||||
const byInbound = new Map<string, CoLoadGroup>()
|
||||
for (const info of groups) for (const member of info.group.members) byInbound.set(member.inboundId, info.group)
|
||||
planGroupsByInboundId.value = byInbound
|
||||
syncDrafts(currentPlan)
|
||||
}
|
||||
function clearSelectionData() {
|
||||
plan.value = null
|
||||
candidates.value = []
|
||||
@@ -303,7 +291,8 @@ async function loadSelectedData(preserved: Record<string, number> = {}) {
|
||||
candidates.value = candidateResult
|
||||
if (planResult.value) {
|
||||
acceptPlan(planResult.value, preserved, false)
|
||||
await loadPlanGroups(request)
|
||||
planGroupsByInboundId.value = await loadSmartLoadingPlanCoLoadGroups(planResult.value)
|
||||
syncDrafts(planResult.value, preserved)
|
||||
}
|
||||
else noDraft.value = planResult.missing
|
||||
} catch (error) {
|
||||
|
||||
@@ -8,14 +8,13 @@ import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import SmartLoadingWorkbenchView from './SmartLoadingWorkbenchView.vue'
|
||||
import { assignSmartLoadingGroup, assignSmartLoadingItem, getContainer, getSmartLoadingPlanByBooking, listSmartLoadingCandidates, recommendSmartLoading, removeSmartLoadingGroup } from '../api/platformContainerLoading'
|
||||
import { batchQueryPlatformCoLoadGroups } from '@/modules/unified-orders'
|
||||
|
||||
vi.mock('../api/platformContainerLoading', () => ({
|
||||
getContainer: vi.fn(), getSmartLoadingPlanByBooking: vi.fn(), listSmartLoadingCandidates: vi.fn(),
|
||||
createSmartLoadingPlan: vi.fn(), assignSmartLoadingItem: vi.fn(), assignSmartLoadingGroup: vi.fn(), recommendSmartLoading: vi.fn(),
|
||||
adjustSmartLoadingItem: vi.fn(), removeSmartLoadingItem: vi.fn(), removeSmartLoadingGroup: vi.fn(), pushSmartLoadingPreload: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders', () => ({ batchQueryPlatformCoLoadGroups: vi.fn().mockResolvedValue([]) }))
|
||||
vi.mock('../smartLoadingPlanGroups', () => ({ loadSmartLoadingPlanCoLoadGroups: vi.fn().mockResolvedValue(new Map()) }))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
|
||||
|
||||
const container = {
|
||||
@@ -178,31 +177,4 @@ describe('SmartLoadingWorkbenchView', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('removes a locked group once instead of looping through member items', async () => {
|
||||
const baseItems = ['201', '202'].map((inboundId, index) => ({
|
||||
id: `70${index}`, loadingPlanId: '99', accountId: `${index + 1}`, inboundId, inboundDetailId: `80${index}`, orderId: `30${index}`,
|
||||
orderNo: `ORD-${index}`, inboundCode: `IN-${inboundId}`, customerName: `客户${index}`, destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9',
|
||||
skuCode: `SKU-${index}`, productNameZh: `货物${index}`, packageCount: 5, productQuantity: 50, grossWeightKg: 50, volumeCbm: 1,
|
||||
loadSequence: index + 1, version: 1, pickedPackageCount: 0, minPackageCount: 1, canAdjust: true, canRemove: true,
|
||||
}))
|
||||
const items = [baseItems[0], { ...baseItems[0], id: '703', inboundId: '203', inboundDetailId: '803', inboundCode: 'IN-203', packageCount: 3, productQuantity: 30, grossWeightKg: 30, volumeCbm: 0.6 }, baseItems[1]]
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...planResponse, items })
|
||||
vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue([...new Set(items.map((item) => item.orderId))].map((orderId) => ({ orderId, group: {
|
||||
groupId: '9900', groupNo: 'CLG-9900', status: 'LOCKED', warehouseId: '8', channelCode: 'ZBYHK', lockedPlanId: '99', version: 1,
|
||||
members: items.map((member) => ({ tenantId: member.accountId, orderId: member.orderId, inboundId: member.inboundId, inboundCode: member.inboundCode, fullPackageCount: member.packageCount })), fullPackageCount: 13, createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:30:00',
|
||||
} })))
|
||||
vi.mocked(removeSmartLoadingGroup).mockResolvedValue({ ...planResponse, version: 3, items: [] })
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
|
||||
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/container-loading/:containerId/smart-loading', component: SmartLoadingWorkbenchView }] })
|
||||
await router.push(`/container-loading/${container.id}/smart-loading`); await router.isReady()
|
||||
const wrapper = mount(SmartLoadingWorkbenchView, { global: { plugins: [ElementPlus, router] } })
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('CLG-9900 · 2 票 · 3 个入仓片段')
|
||||
expect(wrapper.text()).toContain('ORD-0:IN-201、IN-203')
|
||||
expect(wrapper.text()).toContain('全部入仓片段联动,固定 13 箱')
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('移除整组'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(removeSmartLoadingGroup).toHaveBeenCalledWith('99', '9900', 2)
|
||||
expect(ElMessageBox.confirm).toHaveBeenCalledWith(expect.stringContaining('组内所有入仓片段将联动移除'), '移除同柜不可拆组', expect.any(Object))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeft, ListChecks, Plus, RefreshCw, Save, Sparkles, Trash2, Upload } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { batchQueryPlatformCoLoadGroups, type CoLoadGroup } from '@/modules/unified-orders'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
} from '../api/platformContainerLoading'
|
||||
import type { ContainerBookingResource, SmartLoadingCandidate, SmartLoadingPlan } from '../types'
|
||||
import { groupSmartLoadingItems, type SmartLoadingInboundItem } from '../smartLoadingItems'
|
||||
import { loadSmartLoadingPlanCoLoadGroups, type SmartLoadingPlanCoLoadGroup } from '../smartLoadingPlanGroups'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -35,7 +35,7 @@ const assignOpen = ref(false)
|
||||
const selectedCandidate = ref<SmartLoadingCandidateRow | null>(null)
|
||||
const assignForm = reactive({ packageCount: 1 })
|
||||
const quantityDrafts = reactive<Record<string, number>>({})
|
||||
const planGroupsByInboundId = ref(new Map<string, CoLoadGroup>())
|
||||
const planGroupsByInboundId = ref(new Map<string, SmartLoadingPlanCoLoadGroup>())
|
||||
|
||||
const canCreate = computed(() => access.can('platform:firstleg:smart-loading:create'))
|
||||
const canAssign = computed(() => access.can('platform:firstleg:smart-loading:assign'))
|
||||
@@ -56,10 +56,10 @@ function memberOrderSummaries<T extends { orderId: string; orderNo?: string | nu
|
||||
}
|
||||
return [...orders.values()].map((order) => `${order.label}:${order.inboundCodes.join('、')}`)
|
||||
}
|
||||
function groupOrderCount(group: CoLoadGroup) { return new Set(group.members.map((member) => member.orderId)).size }
|
||||
function groupMemberLabel(group: CoLoadGroup) {
|
||||
const orderCount = groupOrderCount(group)
|
||||
return group.members.length === orderCount ? `${orderCount} 票` : `${orderCount} 票 · ${group.members.length} 个入仓片段`
|
||||
function groupMemberLabel(group: SmartLoadingPlanCoLoadGroup) {
|
||||
return group.inboundFragmentCount === group.memberOrderCount
|
||||
? `${group.memberOrderCount} 票`
|
||||
: `${group.memberOrderCount} 票 · ${group.inboundFragmentCount} 个入仓片段`
|
||||
}
|
||||
const displayedCandidates = computed<SmartLoadingCandidateRow[]>(() => {
|
||||
const rows: SmartLoadingCandidateRow[] = []
|
||||
@@ -89,7 +89,7 @@ const displayedCandidates = computed<SmartLoadingCandidateRow[]>(() => {
|
||||
}
|
||||
return rows
|
||||
})
|
||||
type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup: CoLoadGroup | null; memberInboundCodes: string[]; memberOrderSummaries: string[] }
|
||||
type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup: SmartLoadingPlanCoLoadGroup | null; memberInboundCodes: string[]; memberOrderSummaries: string[] }
|
||||
const displayedPlanItems = computed<SmartLoadingPlanRow[]>(() => {
|
||||
const inboundRows = groupSmartLoadingItems(plan.value?.items || [])
|
||||
const result: SmartLoadingPlanRow[] = []
|
||||
@@ -187,15 +187,10 @@ function syncDrafts(preserved: Record<string, number> = {}) {
|
||||
for (const item of displayedPlanItems.value) quantityDrafts[item.id] = preserved[item.id] ?? item.packageCount
|
||||
}
|
||||
function acceptPlan(next: SmartLoadingPlan, preserved: Record<string, number> = {}) { plan.value = next; noDraft.value = false; syncDrafts(preserved) }
|
||||
async function loadPlanGroups() {
|
||||
planGroupsByInboundId.value = new Map()
|
||||
const orderIds = [...new Set((plan.value?.items || []).map((item) => item.orderId))]
|
||||
if (!orderIds.length || !canAssign.value) return
|
||||
const groups = await batchQueryPlatformCoLoadGroups(orderIds)
|
||||
const byInbound = new Map<string, CoLoadGroup>()
|
||||
for (const info of groups) for (const member of info.group.members) byInbound.set(member.inboundId, info.group)
|
||||
planGroupsByInboundId.value = byInbound
|
||||
syncDrafts()
|
||||
async function acceptPlanWithGroups(next: SmartLoadingPlan, preserved: Record<string, number> = {}) {
|
||||
acceptPlan(next, preserved)
|
||||
planGroupsByInboundId.value = await loadSmartLoadingPlanCoLoadGroups(next)
|
||||
syncDrafts(preserved)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
@@ -208,7 +203,7 @@ async function load() {
|
||||
candidates.value = []
|
||||
return
|
||||
}
|
||||
try { acceptPlan(await getSmartLoadingPlanByBooking(containerId.value)); await loadPlanGroups() }
|
||||
try { await acceptPlanWithGroups(await getSmartLoadingPlanByBooking(containerId.value)) }
|
||||
catch (error) {
|
||||
const detail = getApiErrorDetails(error)
|
||||
if (detail.code === 'A0404') { plan.value = null; noDraft.value = true }
|
||||
@@ -230,7 +225,7 @@ function openOperationalList() {
|
||||
async function createPlan() {
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
try { acceptPlan(await createSmartLoadingPlan(containerId.value, null)); ElMessage.success('排柜计划已创建') }
|
||||
try { await acceptPlanWithGroups(await createSmartLoadingPlan(containerId.value, null)); ElMessage.success('排柜计划已创建') }
|
||||
catch (error) { await handleMutationError(error, '创建排柜计划失败') }
|
||||
finally { mutating.value = false }
|
||||
}
|
||||
@@ -251,8 +246,7 @@ async function assign() {
|
||||
const next = selected.coLoadGroupId
|
||||
? await assignSmartLoadingGroup(plan.value.id, selected.inboundId, plan.value.version)
|
||||
: await assignSmartLoadingItem(plan.value.id, { inboundId: selected.inboundId, packageCount: assignForm.packageCount, expectedVersion: plan.value.version })
|
||||
acceptPlan(next)
|
||||
await loadPlanGroups()
|
||||
await acceptPlanWithGroups(next)
|
||||
assignOpen.value = false
|
||||
ElMessage.success(selected.coLoadGroupId ? `同柜不可拆组 ${selected.groupNo} 已整组分配` : '货物已分配')
|
||||
} catch (error) { await handleMutationError(error, '分配失败') }
|
||||
@@ -269,13 +263,13 @@ async function recommend() {
|
||||
mutationError.value = null
|
||||
try {
|
||||
try {
|
||||
acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version))
|
||||
await acceptPlanWithGroups(await recommendSmartLoading(plan.value.id, plan.value.version))
|
||||
} catch (error) {
|
||||
const details = getApiErrorDetails(error, '智能推荐失败')
|
||||
if (details.code !== 'A0409' || !/(版本|并发|已被修改|已变化)/.test(details.message)) throw error
|
||||
await load()
|
||||
if (!plan.value) throw error
|
||||
acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version))
|
||||
await acceptPlanWithGroups(await recommendSmartLoading(plan.value.id, plan.value.version))
|
||||
}
|
||||
ElMessage.success('智能建议已生成,请人工核对后再推送')
|
||||
}
|
||||
@@ -289,7 +283,7 @@ async function adjust(source: object) {
|
||||
if (!Number.isInteger(count) || count < item.minPackageCount) { ElMessage.warning(`分配箱数不能低于已拣下限 ${item.minPackageCount} 箱`); return }
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
try { acceptPlan(await adjustSmartLoadingItem(plan.value.id, item.id, count, plan.value.version, item.version)); ElMessage.success('箱数已调整') }
|
||||
try { await acceptPlanWithGroups(await adjustSmartLoadingItem(plan.value.id, item.id, count, plan.value.version, item.version)); ElMessage.success('箱数已调整') }
|
||||
catch (error) { await handleMutationError(error, '调整失败') }
|
||||
finally { mutating.value = false }
|
||||
}
|
||||
@@ -306,7 +300,7 @@ async function remove(source: object) {
|
||||
mutationError.value = null
|
||||
try {
|
||||
if (row.coLoadGroup) {
|
||||
acceptPlan(await removeSmartLoadingGroup(plan.value!.id, row.coLoadGroup.groupId, plan.value!.version))
|
||||
await acceptPlanWithGroups(await removeSmartLoadingGroup(plan.value!.id, row.coLoadGroup.groupId, plan.value!.version))
|
||||
await load()
|
||||
ElMessage.success(`同柜不可拆组 ${row.coLoadGroup.groupNo} 已整组移除`)
|
||||
return
|
||||
@@ -316,7 +310,7 @@ async function remove(source: object) {
|
||||
const currentItem = current.items.find((value) => value.id === original.id)
|
||||
if (!currentItem) continue
|
||||
current = await removeSmartLoadingItem(current.id, currentItem.id, current.version, currentItem.version)
|
||||
acceptPlan(current)
|
||||
await acceptPlanWithGroups(current)
|
||||
}
|
||||
await load()
|
||||
ElMessage.success('该入仓码已从装柜方案移除')
|
||||
@@ -331,7 +325,7 @@ async function pushPreload() {
|
||||
if (!confirmed || !plan.value) return
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
try { acceptPlan(await pushSmartLoadingPreload(plan.value.id, plan.value.version)); ElMessage.success('预装清单已生成,请继续完成集装箱号和预装确认') }
|
||||
try { await acceptPlanWithGroups(await pushSmartLoadingPreload(plan.value.id, plan.value.version)); ElMessage.success('预装清单已生成,请继续完成集装箱号和预装确认') }
|
||||
catch (error) { await handleMutationError(error, '推送失败') }
|
||||
finally { mutating.value = false }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { loadSmartLoadingPlanCoLoadGroups } from './smartLoadingPlanGroups'
|
||||
import type { SmartLoadingPlan } from './types'
|
||||
|
||||
const item = {
|
||||
id: '701', loadingPlanId: '99', accountId: '1', inboundId: '201', inboundDetailId: '801', orderId: '301',
|
||||
orderNo: 'ORD-1', inboundCode: 'IN-201', customerName: '客户一', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9',
|
||||
skuCode: 'SKU-1', productNameZh: '货物一', packageCount: 5, productQuantity: 50, grossWeightKg: 50, volumeCbm: 1,
|
||||
loadSequence: 1, version: 1, pickedPackageCount: 0, minPackageCount: 1, canAdjust: true, canRemove: true,
|
||||
}
|
||||
|
||||
const plan = {
|
||||
id: '99', planNo: 'LP-99', status: 'EDITING', bookingId: '88', containerNo: null, voyageNo: null, version: 2,
|
||||
canAssign: true, canEdit: true, blockedReason: null, finalCompletionStatus: 'NOT_STARTED', pdaTaskId: null,
|
||||
pdaTaskStatus: null, pdaTaskVersion: null,
|
||||
items: [item, { ...item, id: '702', inboundId: '202', inboundDetailId: '802', orderId: '302', orderNo: 'ORD-2', inboundCode: 'IN-202', packageCount: 7 }],
|
||||
} satisfies SmartLoadingPlan
|
||||
|
||||
describe('smart loading plan co-load groups', () => {
|
||||
it('rebuilds the locked group from the current inbound association contract', async () => {
|
||||
const lookup = vi.fn(async (inboundId: string) => ({
|
||||
inboundId, inboundCode: `IN-${inboundId}`, loadingPlanId: '99', coLoadGroupId: '9001', coLoadGroupNo: 'CLG-9001', items: [],
|
||||
}))
|
||||
const result = await loadSmartLoadingPlanCoLoadGroups(plan, lookup)
|
||||
expect(lookup).toHaveBeenCalledTimes(2)
|
||||
expect(result.get('201')).toEqual({
|
||||
groupId: '9001', groupNo: 'CLG-9001', lockedPlanId: '99', memberOrderCount: 2, inboundFragmentCount: 2, fullPackageCount: 12,
|
||||
})
|
||||
expect(result.get('202')).toBe(result.get('201'))
|
||||
})
|
||||
|
||||
it('leaves ordinary plan items ungrouped', async () => {
|
||||
const result = await loadSmartLoadingPlanCoLoadGroups(plan, async (inboundId) => ({ inboundId, inboundCode: `IN-${inboundId}`, items: [] }))
|
||||
expect(result.size).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { getLoadingAssociationSummaryByInboundId, type LoadingAssociationSummary } from '@/shared/api/inboundCodeRelations'
|
||||
import { groupSmartLoadingItems } from './smartLoadingItems'
|
||||
import type { SmartLoadingPlan } from './types'
|
||||
|
||||
export interface SmartLoadingPlanCoLoadGroup {
|
||||
groupId: string
|
||||
groupNo: string
|
||||
lockedPlanId: string
|
||||
memberOrderCount: number
|
||||
inboundFragmentCount: number
|
||||
fullPackageCount: number
|
||||
}
|
||||
|
||||
type AssociationLookup = (inboundId: string) => Promise<LoadingAssociationSummary>
|
||||
|
||||
export async function loadSmartLoadingPlanCoLoadGroups(
|
||||
plan: SmartLoadingPlan,
|
||||
lookup: AssociationLookup = getLoadingAssociationSummaryByInboundId,
|
||||
) {
|
||||
const inboundRows = groupSmartLoadingItems(plan.items || [])
|
||||
if (!inboundRows.length) return new Map<string, SmartLoadingPlanCoLoadGroup>()
|
||||
|
||||
const associations = await Promise.all(inboundRows.map(async (row) => ({
|
||||
row,
|
||||
association: await lookup(row.inboundId),
|
||||
})))
|
||||
const membersByGroupId = new Map<string, typeof associations>()
|
||||
for (const entry of associations) {
|
||||
const groupId = String(entry.association.coLoadGroupId || '')
|
||||
if (!groupId) continue
|
||||
membersByGroupId.set(groupId, [...(membersByGroupId.get(groupId) || []), entry])
|
||||
}
|
||||
|
||||
const groupsByInboundId = new Map<string, SmartLoadingPlanCoLoadGroup>()
|
||||
for (const [groupId, members] of membersByGroupId) {
|
||||
const group = {
|
||||
groupId,
|
||||
groupNo: members.find((entry) => entry.association.coLoadGroupNo)?.association.coLoadGroupNo || '同柜不可拆组',
|
||||
lockedPlanId: members.find((entry) => entry.association.loadingPlanId)?.association.loadingPlanId || plan.id,
|
||||
memberOrderCount: new Set(members.map((entry) => entry.row.orderId)).size,
|
||||
inboundFragmentCount: members.length,
|
||||
fullPackageCount: members.reduce((sum, entry) => sum + entry.row.packageCount, 0),
|
||||
} satisfies SmartLoadingPlanCoLoadGroup
|
||||
for (const { row } of members) groupsByInboundId.set(row.inboundId, group)
|
||||
}
|
||||
return groupsByInboundId
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { batchQueryPlatformCoLoadGroups, cancelPlatformCoLoadGroup, createPlatformCoLoadGroup } from './coLoadGroups'
|
||||
import { cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode } from './coLoadGroups'
|
||||
|
||||
const group = {
|
||||
groupId: 2093000000000000001n, groupNo: 'CLG-2093000000000000001', status: 'ACTIVE',
|
||||
warehouseId: 2093000000000000002n, channelCode: 'NY-EXX', lockedPlanId: null,
|
||||
version: 0, fullPackageCount: 18, createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:00:00',
|
||||
members: [
|
||||
{ tenantId: 2093000000000000004n, orderId: 2093000000000000005n, inboundId: 2093000000000000006n, inboundCode: 'NYEXX2608310001', fullPackageCount: 8 },
|
||||
{ tenantId: 2093000000000000007n, orderId: 2093000000000000008n, inboundId: 2093000000000000009n, inboundCode: 'NYEXX2608310002', fullPackageCount: 10 },
|
||||
{ tenantId: 2093000000000000004n, orderId: 2093000000000000005n, inboundId: 2093000000000000006n, fullPackageCount: 8 },
|
||||
{ tenantId: 2093000000000000007n, orderId: 2093000000000000008n, inboundId: 2093000000000000009n, fullPackageCount: 10 },
|
||||
],
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ describe('co-load group API', () => {
|
||||
expect(result.groupId).toBe('2093000000000000001')
|
||||
expect(result.members.map((member) => member.orderId)).toEqual(orderIds)
|
||||
expect(result.members.map((member) => member.tenantId)).toEqual(['2093000000000000004', '2093000000000000007'])
|
||||
expect(result.members.map((member) => member.inboundCode)).toEqual(['NYEXX2608310001', 'NYEXX2608310002'])
|
||||
})
|
||||
|
||||
it('cancels only the whole group with optimistic version and an optional reason', async () => {
|
||||
@@ -36,46 +35,26 @@ describe('co-load group API', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('batch-enriches three list rows and normalizes every long id without numeric coercion', async () => {
|
||||
const third = { ...group.members[0], orderId: 2093000000000000010n, inboundId: 2093000000000000011n }
|
||||
const threeMemberGroup = { ...group, fullPackageCount: 23, members: [...group.members, third] }
|
||||
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: threeMemberGroup.members.map((member) => ({ orderId: member.orderId, group: threeMemberGroup })) })
|
||||
const orderIds = ['2093000000000000005', '2093000000000000008', '2093000000000000010']
|
||||
const result = await batchQueryPlatformCoLoadGroups(orderIds)
|
||||
expect(post).toHaveBeenCalledWith('/platform/v1/first-leg/coload-groups/batch-query', { orderIds })
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[2]).toMatchObject({ orderId: '2093000000000000010', group: { groupId: '2093000000000000001', fullPackageCount: 23 } })
|
||||
expect(result[2]).not.toHaveProperty('inboundId')
|
||||
})
|
||||
|
||||
it('keeps every inbound fragment when one order has multiple current inbounds', async () => {
|
||||
const fragment = { ...group.members[0], inboundId: 2093000000000000012n, inboundCode: 'NYEXX2608310001-B', fullPackageCount: 3 }
|
||||
const multiInboundGroup = { ...group, fullPackageCount: 21, members: [group.members[0], fragment, group.members[1]] }
|
||||
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: [
|
||||
{ orderId: group.members[0].orderId, group: multiInboundGroup },
|
||||
{ orderId: group.members[1].orderId, group: multiInboundGroup },
|
||||
] })
|
||||
const result = await batchQueryPlatformCoLoadGroups(['2093000000000000005', '2093000000000000008'])
|
||||
expect(post).toHaveBeenCalledTimes(1)
|
||||
expect(result[0].group.members).toEqual([
|
||||
expect.objectContaining({ orderId: '2093000000000000005', inboundId: '2093000000000000006', inboundCode: 'NYEXX2608310001', fullPackageCount: 8 }),
|
||||
expect.objectContaining({ orderId: '2093000000000000005', inboundId: '2093000000000000012', inboundCode: 'NYEXX2608310001-B', fullPackageCount: 3 }),
|
||||
expect.objectContaining({ orderId: '2093000000000000008', inboundId: '2093000000000000009', inboundCode: 'NYEXX2608310002', fullPackageCount: 10 }),
|
||||
])
|
||||
it('loads the business-only group summary by inbound code without platform batch-query', async () => {
|
||||
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: {
|
||||
status: 'LOCKED', memberCount: '2', members: [{ inboundCode: ' NYEXX2608310001 ' }, { inboundCode: 'NYEXX2608310002' }],
|
||||
} })
|
||||
const post = vi.spyOn(http, 'post')
|
||||
const result = await getPlatformCoLoadGroupByInboundCode('NYEXX2608310001')
|
||||
expect(get).toHaveBeenCalledWith('/platform/v1/first-leg/coload-groups/by-inbound-code/NYEXX2608310001')
|
||||
expect(result).toEqual({ status: 'LOCKED', memberCount: 2, members: [{ inboundCode: 'NYEXX2608310001' }, { inboundCode: 'NYEXX2608310002' }] })
|
||||
expect(post).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('normalizes missing and blank member inbound codes without exposing technical ids as fallbacks', async () => {
|
||||
const incompleteGroup = {
|
||||
...group,
|
||||
const incompleteSummary = {
|
||||
status: 'ACTIVE', memberCount: 4,
|
||||
members: [
|
||||
{ ...group.members[0], inboundCode: undefined },
|
||||
{ ...group.members[1], inboundCode: null },
|
||||
{ ...group.members[1], inboundId: 2093000000000000012n, inboundCode: ' ' },
|
||||
{ ...group.members[1], inboundId: 2093000000000000013n, inboundCode: ' NYEXX2608310003 ' },
|
||||
{ inboundCode: undefined }, { inboundCode: null }, { inboundCode: ' ' }, { inboundCode: ' NYEXX2608310003 ' },
|
||||
],
|
||||
}
|
||||
vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: incompleteGroup })
|
||||
const result = await createPlatformCoLoadGroup(['2093000000000000005', '2093000000000000008'])
|
||||
vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: incompleteSummary })
|
||||
const result = await getPlatformCoLoadGroupByInboundCode('NYEXX2608310001')
|
||||
expect(result.members.map((member) => member.inboundCode)).toEqual(['', '', '', 'NYEXX2608310003'])
|
||||
expect(result.members.map((member) => member.inboundCode)).not.toContain('2093000000000000012')
|
||||
})
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
import { listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders'
|
||||
import { batchQueryPlatformCoLoadGroups, cancelPlatformCoLoadGroup, createPlatformCoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups'
|
||||
import { createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode } from '@/modules/unified-orders/api/coLoadGroups'
|
||||
import UnifiedOrdersView from './UnifiedOrdersView.vue'
|
||||
|
||||
vi.mock('vue-router', () => ({ useRoute: () => ({ params: {}, query: {} }), useRouter: () => ({ push: vi.fn() }) }))
|
||||
@@ -18,7 +17,7 @@ vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
|
||||
unifiedOrderStatusOptions: [{ value: 'WAIT_SPLIT', label: '待分柜' }],
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({
|
||||
batchQueryPlatformCoLoadGroups: vi.fn(), cancelPlatformCoLoadGroup: vi.fn(), createPlatformCoLoadGroup: vi.fn(),
|
||||
createPlatformCoLoadGroup: vi.fn(), getPlatformCoLoadGroupByInboundCode: vi.fn(),
|
||||
}))
|
||||
|
||||
const rows = ['5001', '5002'].map((orderId, index) => ({
|
||||
@@ -30,13 +29,8 @@ const rows = ['5001', '5002'].map((orderId, index) => ({
|
||||
createdAt: null, updatedAt: null, expectedArrivalAt: null, feeReviewStatus: null, requestedPickupAt: null, totalPalletCount: null,
|
||||
chargeableVolumeCbm: null, volumetricWeightKg: null, totalNetWeightKg: null, measurementCompletedAt: null, inboundCompletedAt: null,
|
||||
expressCompany: null, expressTrackingNo: null, skuNos: null, totalQuantity: null, shipmentId: null, referenceId: null,
|
||||
coLoadGroup: null,
|
||||
}))
|
||||
const group = {
|
||||
groupId: '9001', groupNo: 'CLG-9001', status: 'ACTIVE' as const, warehouseId: '8', channelCode: 'NY-EXX', lockedPlanId: null, version: 0, fullPackageCount: 10,
|
||||
members: rows.map((row) => ({ tenantId: String(row.tenantId), orderId: String(row.orderId), inboundId: String(row.inboundId), inboundCode: String(row.inboundCode), fullPackageCount: 5 })),
|
||||
createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:00:00',
|
||||
}
|
||||
|
||||
function mountView() {
|
||||
return shallowMount(UnifiedOrdersView, {
|
||||
global: { directives: { loading: () => undefined }, stubs: {
|
||||
@@ -58,7 +52,6 @@ describe('UnifiedOrdersView co-load group behavior', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rows, page: 1, size: 20, total: 2, pages: 1 })
|
||||
vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue([])
|
||||
})
|
||||
|
||||
it('keeps both selected orders and backend tracing evidence when create validation fails', async () => {
|
||||
@@ -86,24 +79,22 @@ describe('UnifiedOrdersView co-load group behavior', () => {
|
||||
expect(wrapper.text()).not.toContain('ORD-2')
|
||||
expect(wrapper.text()).not.toContain('5001')
|
||||
expect(wrapper.text()).not.toContain('7000')
|
||||
expect(wrapper.text()).not.toContain('履约详情')
|
||||
expect(wrapper.text()).not.toContain('关联数据')
|
||||
})
|
||||
|
||||
it('shows a business-only group summary and all server-provided inbound codes in the dialog', async () => {
|
||||
const multiInboundGroup = {
|
||||
...group,
|
||||
fullPackageCount: 13,
|
||||
members: [group.members[0], { ...group.members[0], inboundId: '7003', inboundCode: 'IN-1-B', fullPackageCount: 3 }, group.members[1]],
|
||||
}
|
||||
vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue(rows.map((row) => ({ orderId: row.orderId, group: multiInboundGroup })))
|
||||
vi.mocked(cancelPlatformCoLoadGroup).mockResolvedValue({ ...multiInboundGroup, status: 'CANCELLED', version: 1 })
|
||||
vi.spyOn(ElMessageBox, 'prompt').mockResolvedValue({ value: '计划调整', action: 'confirm' } as never)
|
||||
const rowsWithGroup = rows.map((row) => ({ ...row, coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-1', 'IN-1-B', 'IN-2'] } }))
|
||||
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rowsWithGroup, page: 1, size: 20, total: 2, pages: 1 })
|
||||
vi.mocked(getPlatformCoLoadGroupByInboundCode).mockResolvedValue({ status: 'ACTIVE', memberCount: 3, members: [{ inboundCode: 'IN-1' }, { inboundCode: 'IN-1-B' }, { inboundCode: 'IN-2' }] })
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('同柜组 · 2 票 · 未锁定')
|
||||
expect(wrapper.text()).not.toContain('CLG-9001')
|
||||
await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(getPlatformCoLoadGroupByInboundCode).toHaveBeenCalledWith('IN-1')
|
||||
expect(wrapper.text()).toContain('3 个成员')
|
||||
expect(wrapper.text()).toContain('13 箱')
|
||||
expect(wrapper.text()).toContain('IN-1')
|
||||
expect(wrapper.text()).toContain('IN-1-B')
|
||||
expect(wrapper.text()).toContain('IN-2')
|
||||
@@ -111,10 +102,6 @@ describe('UnifiedOrdersView co-load group behavior', () => {
|
||||
expect(wrapper.text()).not.toContain('ORD-2')
|
||||
expect(wrapper.text()).not.toContain('7003')
|
||||
expect(wrapper.text()).not.toContain('5001')
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('取消同柜组'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(cancelPlatformCoLoadGroup).toHaveBeenCalledWith('9001', 0, '计划调整')
|
||||
expect(ElMessageBox.prompt).toHaveBeenCalledWith(expect.stringContaining('不支持单独移除成员'), '取消同柜不可拆组', expect.any(Object))
|
||||
})
|
||||
|
||||
it.each([
|
||||
@@ -122,14 +109,13 @@ describe('UnifiedOrdersView co-load group behavior', () => {
|
||||
['null', null],
|
||||
['blank', ' '],
|
||||
])('keeps the member dialog readable when a normalized inbound code is %s', async (_case, inboundCode) => {
|
||||
const incompleteGroup = {
|
||||
...group,
|
||||
members: [{ ...group.members[0], inboundCode: inboundCode as unknown as string }, group.members[1]],
|
||||
}
|
||||
vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue(rows.map((row) => ({ orderId: row.orderId, group: incompleteGroup })))
|
||||
const rowsWithGroup = rows.map((row) => ({ ...row, coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-1', 'IN-2'] } }))
|
||||
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rowsWithGroup, page: 1, size: 20, total: 2, pages: 1 })
|
||||
vi.mocked(getPlatformCoLoadGroupByInboundCode).mockResolvedValue({ status: 'ACTIVE', memberCount: 2, members: [{ inboundCode: inboundCode as unknown as string }, { inboundCode: 'IN-2' }] })
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('同柜组成员入仓码尚未返回,请刷新后重试')
|
||||
expect(wrapper.text()).not.toContain('CLG-9001')
|
||||
expect(wrapper.text()).not.toContain('ORD-1')
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship } from '@/shared/api/inboundCodeRelations'
|
||||
import { getPlatformUnifiedOrder } from '@/modules/unified-orders/api/unifiedOrders'
|
||||
import UnifiedOrdersView from './UnifiedOrdersView.vue'
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => ({ params: { orderId: '5001' }, query: { tenantId: '6001' } }),
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
}))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
|
||||
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) }))
|
||||
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
|
||||
vi.mock('@/modules/inbounds', () => ({
|
||||
downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn(),
|
||||
InboundFeePanel: { template: '<div />' }, InboundMeasurementDialog: { template: '<div />' }, MeasurementEvidenceThumbnail: { template: '<div />' },
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
|
||||
vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({
|
||||
createPlatformCoLoadGroup: vi.fn(), getPlatformCoLoadGroupByInboundCode: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/shared/api/inboundCodeRelations', () => ({
|
||||
getLoadingAssociationSummary: vi.fn(), getOverseasInboundCodeRelationship: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
|
||||
addPlatformUnifiedOrderNote: vi.fn(), approvePlatformUnifiedOrder: vi.fn(), batchApprovePlatformUnifiedOrders: vi.fn(),
|
||||
getPlatformUnifiedOrder: vi.fn(), listPlatformUnifiedOrderFeeDetails: vi.fn(), listPlatformUnifiedOrders: vi.fn(),
|
||||
unifiedOrderStatusOptions: [],
|
||||
}))
|
||||
|
||||
const detail = {
|
||||
summary: {
|
||||
orderId: '5001', tenantId: '6001', orderNo: 'ORD-HIDDEN', mainStatus: 'WAIT_SPLIT', nextAction: null, relation: null,
|
||||
internalOrderStatus: 'WAIT_SPLIT', customerId: '6001', customerName: '测试客户', channelName: '纽约渠道',
|
||||
receivingWarehouseId: '8', receivingWarehouseName: '义乌仓', destinationWarehouseId: '9', destinationWarehouseName: 'ONT8',
|
||||
inboundId: '7001', inboundVersion: 1, inboundCode: 'NYEXX2608310001', expectedShipDate: null, totalPackageCount: 2,
|
||||
locationCodes: null, actualGrossWeightKg: null, actualVolumeCbm: null, measurement: null, preload: null, loading: null, delivery: null, transport: null,
|
||||
coLoadGroup: null, createdAt: null, updatedAt: null, expectedArrivalAt: null, feeReviewStatus: null, requestedPickupAt: null,
|
||||
totalPalletCount: null, chargeableVolumeCbm: null, volumetricWeightKg: null, totalNetWeightKg: null,
|
||||
measurementCompletedAt: null, inboundCompletedAt: null, expressCompany: null, expressTrackingNo: null, skuNos: null,
|
||||
totalQuantity: null, shipmentId: null, referenceId: null,
|
||||
},
|
||||
order: { order: null, cargoes: [], parties: [], internalNotes: [], statusHistory: [] },
|
||||
timeline: [],
|
||||
}
|
||||
|
||||
function mountDetail() {
|
||||
return shallowMount(UnifiedOrdersView, {
|
||||
global: {
|
||||
directives: { loading: () => undefined },
|
||||
stubs: {
|
||||
AdminPage: { template: '<main><slot /></main>' },
|
||||
DetailPageLayout: { template: '<section><slot /></section>' },
|
||||
OrderFulfillmentTreePanel: { template: '<div data-testid="fulfillment-tree" />' },
|
||||
LoadErrorState: { props: ['title', 'message'], template: '<div>{{ title }} {{ message }}<slot name="details" /></div>' },
|
||||
ApiErrorMeta: true,
|
||||
StatusBadge: true,
|
||||
'el-button': { template: '<button @click="$emit(\'click\')"><slot /></button>' },
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('UnifiedOrdersView downstream fulfillment detail', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(getPlatformUnifiedOrder).mockResolvedValue(detail as never)
|
||||
vi.mocked(getLoadingAssociationSummary).mockResolvedValue({
|
||||
inboundId: '7001', inboundCode: 'NYEXX2608310001', loadingPlanId: null, loadingPlanNo: null,
|
||||
containerBookingId: null, cabinetNo: null, containerNo: null, loadingStatus: null, taskId: null,
|
||||
pdaTaskStatus: null, coLoadGroupId: null, coLoadGroupNo: null, items: [],
|
||||
})
|
||||
vi.mocked(getOverseasInboundCodeRelationship).mockResolvedValue({
|
||||
inboundCode: 'NYEXX2608310001', found: false, customer: null, overseasWarehouse: null,
|
||||
overseasReceivingStatus: null, taillegOrders: [], inboundPlans: [], palletCodes: [], inventory: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('shows a compact empty state in order detail when no downstream relationship exists', async () => {
|
||||
const wrapper = mountDetail()
|
||||
await flushPromises()
|
||||
await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('拆分履约'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(getLoadingAssociationSummary).toHaveBeenCalledWith('NYEXX2608310001')
|
||||
expect(getOverseasInboundCodeRelationship).toHaveBeenCalledWith('NYEXX2608310001')
|
||||
expect(wrapper.text()).toContain('暂无后续履约数据')
|
||||
expect(wrapper.text()).not.toContain('新增关联')
|
||||
})
|
||||
|
||||
it('renders actual loading and overseas business identifiers inside order detail', async () => {
|
||||
vi.mocked(getLoadingAssociationSummary).mockResolvedValue({
|
||||
inboundId: '7001', inboundCode: 'NYEXX2608310001', loadingPlanId: '9001', loadingPlanNo: 'LP-260831-001',
|
||||
containerBookingId: '9002', cabinetNo: 'CAB-260831-001', containerNo: 'MSCU1234567', loadingStatus: 'LOADING',
|
||||
taskId: '9003', pdaTaskStatus: 'IN_PROGRESS', coLoadGroupId: null, coLoadGroupNo: null, items: [],
|
||||
})
|
||||
vi.mocked(getOverseasInboundCodeRelationship).mockResolvedValue({
|
||||
inboundCode: 'NYEXX2608310001', found: true, customer: null,
|
||||
overseasWarehouse: { warehouseId: '9004', warehouseCode: 'LAX', warehouseName: '洛杉矶海外仓' },
|
||||
overseasReceivingStatus: 'RECEIVING', taillegOrders: [{ orderNo: 'LL-260831-001' }],
|
||||
inboundPlans: [{ planNo: 'IP-260831-001' }], palletCodes: ['PLT-LAX-001'],
|
||||
inventory: [{ skuCode: 'SKU-001', packageCount: 2 }],
|
||||
})
|
||||
const wrapper = mountDetail()
|
||||
await flushPromises()
|
||||
await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('拆分履约'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('LP-260831-001')
|
||||
expect(wrapper.text()).toContain('MSCU1234567')
|
||||
expect(wrapper.text()).toContain('LL-260831-001')
|
||||
expect(wrapper.text()).toContain('IP-260831-001')
|
||||
expect(wrapper.text()).toContain('PLT-LAX-001')
|
||||
expect(wrapper.text()).not.toContain('9001')
|
||||
expect(wrapper.text()).not.toContain('9004')
|
||||
})
|
||||
})
|
||||
@@ -13,9 +13,9 @@ vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mock
|
||||
vi.mock('@/modules/inbounds', () => ({ downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn() }))
|
||||
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
|
||||
vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({
|
||||
batchQueryPlatformCoLoadGroups: vi.fn().mockResolvedValue([]),
|
||||
cancelPlatformCoLoadGroup: vi.fn(),
|
||||
createPlatformCoLoadGroup: vi.fn(),
|
||||
getPlatformCoLoadGroupByInboundCode: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
|
||||
addPlatformUnifiedOrderNote: vi.fn(),
|
||||
|
||||
@@ -183,10 +183,10 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain("inboundCode || '待生成入仓码'")
|
||||
expect(source).toContain('class="coload-tag-trigger"')
|
||||
expect(source).toContain('coLoadGroupSummary(coLoadGroup(row as UnifiedOrderResource)!)')
|
||||
expect(source).not.toContain('coLoadDetailGroup.groupNo')
|
||||
expect(source).not.toContain('coLoadDetailGroup')
|
||||
expect(source).not.toContain('<span v-else>—</span></template>\n <template #orderStatus')
|
||||
expect(source).toContain('v-for="member in coLoadDetailMembers"')
|
||||
expect(source).toContain('<th>入仓码</th><th>箱数</th>')
|
||||
expect(source).toContain('<thead><tr><th>入仓码</th></tr></thead>')
|
||||
expect(source).toContain('{{ member.inboundCode }}')
|
||||
expect(source).toContain('同柜组成员入仓码尚未返回,请刷新后重试')
|
||||
expect(source).toContain('coLoadDetailHasMissingInboundCodes')
|
||||
@@ -196,11 +196,12 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).not.toContain('客户 ID')
|
||||
})
|
||||
|
||||
it('names the cross-domain relationship entry as fulfillment details', () => {
|
||||
expect(source).toContain('>履约详情</el-button>')
|
||||
expect(source).toContain('aria-label="查看该入仓码的订单、装柜、尾程与库存关系"')
|
||||
expect(source).toContain('title="查看该入仓码的订单、装柜、尾程与库存关系"')
|
||||
it('does not expose a generic relationship probe in list rows', () => {
|
||||
expect(source).not.toContain('>履约详情</el-button>')
|
||||
expect(source).not.toContain('>关联数据</el-button>')
|
||||
expect(source).not.toContain('openInboundRelationship')
|
||||
expect(source).not.toContain('inboundRelationshipDialogOpen')
|
||||
expect(source).not.toContain('<InboundCodeRelationshipDialog')
|
||||
})
|
||||
|
||||
it('keeps editable query values separate from the applied list request', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, GitBranch, History, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, GitBranch, History, Link2, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
getPlatformUnifiedOrder,
|
||||
@@ -23,6 +23,7 @@ import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship, type LoadingAssociationSummary, type OverseasInboundCodeRelationship } from '@/shared/api/inboundCodeRelations'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
@@ -153,10 +154,16 @@ const cargoImagePreviewUrls = new Set<string>()
|
||||
const measurementDialogOpen = ref(false)
|
||||
const smartLoadingOpen = ref(false)
|
||||
const measurementOrder = ref<UnifiedOrderResource | null>(null)
|
||||
const loadingAssociation = ref<LoadingAssociationSummary | null>(null)
|
||||
const overseasRelationship = ref<OverseasInboundCodeRelationship | null>(null)
|
||||
const loadingAssociationError = ref<ApiErrorDetails | null>(null)
|
||||
const overseasRelationshipError = ref<ApiErrorDetails | null>(null)
|
||||
const subsequentFulfillmentLoading = ref(false)
|
||||
let detailLoadingKey = ''
|
||||
let detailRequestSerial = 0
|
||||
let measurementRequestSerial = 0
|
||||
let measurementPreviewRequestSerial = 0
|
||||
let subsequentFulfillmentRequestSerial = 0
|
||||
const showAdvancedFilters = ref(false)
|
||||
const filters = ref<FilterModel>(emptyFilters())
|
||||
const appliedFilters = ref<FilterModel>(emptyFilters())
|
||||
@@ -588,6 +595,36 @@ async function loadMeasurementDetails() {
|
||||
function selectDetailTab(key: string) {
|
||||
activeDetailTab.value = key
|
||||
if (key === 'measurement') void loadMeasurementDetails()
|
||||
if (key === 'fulfillment') void loadSubsequentFulfillment()
|
||||
}
|
||||
|
||||
const hasLoadingAssociation = computed(() => Boolean(loadingAssociation.value
|
||||
&& (loadingAssociation.value.loadingPlanNo || loadingAssociation.value.containerNo || loadingAssociation.value.cabinetNo
|
||||
|| loadingAssociation.value.loadingStatus || loadingAssociation.value.pdaTaskStatus)))
|
||||
const hasOverseasRelationship = computed(() => Boolean(overseasRelationship.value?.found
|
||||
&& (overseasRelationship.value.overseasWarehouse || overseasRelationship.value.overseasReceivingStatus
|
||||
|| overseasRelationship.value.taillegOrders.length || overseasRelationship.value.inboundPlans.length
|
||||
|| overseasRelationship.value.palletCodes.length || overseasRelationship.value.inventory.length)))
|
||||
const hasSubsequentFulfillment = computed(() => hasLoadingAssociation.value || hasOverseasRelationship.value)
|
||||
|
||||
async function loadSubsequentFulfillment() {
|
||||
const inboundCode = detail.value?.summary.inboundCode?.trim() || ''
|
||||
const serial = ++subsequentFulfillmentRequestSerial
|
||||
loadingAssociation.value = null
|
||||
overseasRelationship.value = null
|
||||
loadingAssociationError.value = null
|
||||
overseasRelationshipError.value = null
|
||||
if (!inboundCode) return
|
||||
subsequentFulfillmentLoading.value = true
|
||||
const tasks: Promise<void>[] = []
|
||||
if (can('platform:firstleg:smart-loading:list')) tasks.push(getLoadingAssociationSummary(inboundCode)
|
||||
.then((value) => { if (serial === subsequentFulfillmentRequestSerial) loadingAssociation.value = value })
|
||||
.catch((error) => { if (serial === subsequentFulfillmentRequestSerial) loadingAssociationError.value = getApiErrorDetails(error, '装柜与PDA履约数据加载失败') }))
|
||||
if (can('platform:overseas-warehouse:inventory:list')) tasks.push(getOverseasInboundCodeRelationship(inboundCode)
|
||||
.then((value) => { if (serial === subsequentFulfillmentRequestSerial) overseasRelationship.value = value })
|
||||
.catch((error) => { if (serial === subsequentFulfillmentRequestSerial) overseasRelationshipError.value = getApiErrorDetails(error, '尾程与库存履约数据加载失败') }))
|
||||
await Promise.all(tasks)
|
||||
if (serial === subsequentFulfillmentRequestSerial) subsequentFulfillmentLoading.value = false
|
||||
}
|
||||
|
||||
function toggleMeasurementSection(section: keyof typeof measurementCollapsed) {
|
||||
@@ -989,6 +1026,7 @@ watch(detailRouteKey, (value) => {
|
||||
void loadDetail()
|
||||
} else {
|
||||
detailRequestSerial += 1
|
||||
subsequentFulfillmentRequestSerial += 1
|
||||
detail.value = null
|
||||
detailError.value = null
|
||||
detailLoading.value = false
|
||||
@@ -1170,6 +1208,26 @@ onBeforeUnmount(() => {
|
||||
<template v-else-if="activeDetailTab === 'fulfillment'">
|
||||
<div class="panel-title"><div><h2>拆分履约</h2><p>根单、已完成分片与当前余量</p></div><ListChecks :size="19" /></div>
|
||||
<OrderFulfillmentTreePanel :tenant-id="String(detail.summary.tenantId)" :order-id="String(detail.summary.orderId)" :inbound-code="detail.summary.inboundCode" />
|
||||
<section v-loading="subsequentFulfillmentLoading" class="subsequent-fulfillment" aria-label="后续履约数据">
|
||||
<h3>后续履约数据</h3>
|
||||
<LoadErrorState v-if="loadingAssociationError" title="装柜与PDA履约数据加载失败" :message="loadingAssociationError.message" @retry="loadSubsequentFulfillment"><template #details><ApiErrorMeta :details="loadingAssociationError" /></template></LoadErrorState>
|
||||
<LoadErrorState v-if="overseasRelationshipError" title="尾程与库存履约数据加载失败" :message="overseasRelationshipError.message" @retry="loadSubsequentFulfillment"><template #details><ApiErrorMeta :details="overseasRelationshipError" /></template></LoadErrorState>
|
||||
<dl v-if="hasLoadingAssociation" class="fulfillment-facts">
|
||||
<div><dt>装柜计划</dt><dd>{{ loadingAssociation?.loadingPlanNo || '—' }}</dd></div>
|
||||
<div><dt>柜号</dt><dd>{{ loadingAssociation?.containerNo || loadingAssociation?.cabinetNo || '—' }}</dd></div>
|
||||
<div><dt>装柜状态</dt><dd>{{ loadingAssociation?.loadingStatus || '—' }}</dd></div>
|
||||
<div><dt>PDA状态</dt><dd>{{ loadingAssociation?.pdaTaskStatus || '—' }}</dd></div>
|
||||
</dl>
|
||||
<dl v-if="hasOverseasRelationship" class="fulfillment-facts">
|
||||
<div><dt>海外仓</dt><dd>{{ overseasRelationship?.overseasWarehouse?.warehouseName || '尚未分配海外仓' }}</dd></div>
|
||||
<div><dt>海外收货状态</dt><dd>{{ overseasRelationship?.overseasReceivingStatus || '—' }}</dd></div>
|
||||
<div class="wide"><dt>尾程订单</dt><dd>{{ overseasRelationship?.taillegOrders.map((item) => item.orderNo).join('、') || '—' }}</dd></div>
|
||||
<div class="wide"><dt>入库计划</dt><dd>{{ overseasRelationship?.inboundPlans.map((item) => item.planNo).join('、') || '—' }}</dd></div>
|
||||
<div class="wide"><dt>托盘码</dt><dd>{{ overseasRelationship?.palletCodes.join('、') || '—' }}</dd></div>
|
||||
<div><dt>库存SKU</dt><dd>{{ overseasRelationship?.inventory.length || 0 }} 个</dd></div>
|
||||
</dl>
|
||||
<p v-if="!subsequentFulfillmentLoading && !loadingAssociationError && !overseasRelationshipError && !hasSubsequentFulfillment" class="fulfillment-empty">暂无后续履约数据</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeDetailTab === 'workflow'">
|
||||
@@ -1464,6 +1522,14 @@ onBeforeUnmount(() => {
|
||||
.coload-detail-table td strong,.coload-detail-table td span { display: block; overflow-wrap: anywhere; }
|
||||
.coload-detail-table td span { color: var(--cc-muted); }
|
||||
.coload-detail-table :deep(.el-button) { min-height: 24px; padding: 2px 0; }
|
||||
.subsequent-fulfillment { min-height: 72px; margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--cc-border); }
|
||||
.subsequent-fulfillment h3 { margin: 0 0 10px; font-size: 15px; }
|
||||
.fulfillment-facts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0 0 10px; border: 1px solid var(--cc-border); border-radius: 6px; overflow: hidden; }
|
||||
.fulfillment-facts > div { min-width: 0; padding: 9px 10px; border-right: 1px solid var(--cc-border); background: #fff; }
|
||||
.fulfillment-facts > div.wide { grid-column: span 2; }
|
||||
.fulfillment-facts dt { color: var(--cc-muted); font-size: 12px; }
|
||||
.fulfillment-facts dd { min-width: 0; margin: 4px 0 0; overflow-wrap: anywhere; font-size: 13px; }
|
||||
.fulfillment-empty { margin: 0; padding: 12px; border: 1px dashed var(--cc-border); border-radius: 6px; color: var(--cc-muted); text-align: center; }
|
||||
.table-card :deep(.el-table) { --el-table-border-color: #e5e7eb; --el-table-header-bg-color: #f9fafb; --el-table-row-hover-bg-color: #f3f7fb; --el-table-tr-bg-color: #fff; color: #374151; font-size: 12px; }
|
||||
.table-card :deep(.el-table th.el-table__cell) { height: 42px; padding: 0; background: #f9fafb; color: #374151; font-size: 12px; font-weight: 600; }
|
||||
.table-card :deep(.el-table td.el-table__cell) { min-height: 42px; padding: 9px 0; }
|
||||
|
||||
Reference in New Issue
Block a user