Merge branch 'release/test' of http://8.163.30.128:10082/html/admin-web into release/test
This commit is contained in:
@@ -104,6 +104,8 @@ function item(raw: SmartLoadingItem): SmartLoadingItem {
|
||||
packageCount: numberValue(raw.packageCount),
|
||||
pickedPackageCount: numberValue(raw.pickedPackageCount),
|
||||
minPackageCount: numberValue(raw.minPackageCount, 1),
|
||||
totalPackageCount: numberValue(raw.totalPackageCount, raw.packageCount),
|
||||
remainingPackageCount: numberValue(raw.remainingPackageCount),
|
||||
canAdjust: Boolean(raw.canAdjust), canRemove: Boolean(raw.canRemove),
|
||||
productQuantity: raw.productQuantity == null ? null : numberValue(raw.productQuantity),
|
||||
grossWeightKg: raw.grossWeightKg == null ? null : numberValue(raw.grossWeightKg),
|
||||
|
||||
@@ -255,6 +255,37 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(wrapper.text()).toContain('柜子数量2 个')
|
||||
})
|
||||
|
||||
it('selects only the double-clicked channel as a shortcut', async () => {
|
||||
const other = {
|
||||
...container,
|
||||
id: '9007199254740998',
|
||||
planId: '9007199254740999',
|
||||
cabinetNo: 'CB-2',
|
||||
containerNo: 'MSCU-2',
|
||||
channelCode: 'HDHK',
|
||||
channelName: '合德海卡包税',
|
||||
}
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [container, other], page: 1, size: 100, total: 2, pages: 1 })
|
||||
mockContainerSearch([container, other])
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => ({ ...plan, bookingId }))
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
const hedeChannel = wrapper.findAllComponents(ElCheckboxButton).find((item) => item.text() === '合德')!
|
||||
await hedeChannel.find('input').setValue(true)
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('.channel-tags .el-checkbox-button.is-checked')).toHaveLength(2)
|
||||
|
||||
await hedeChannel.trigger('dblclick')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.findAll('.channel-tags .el-checkbox-button.is-checked').map((item) => item.text())).toEqual(['合德'])
|
||||
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith(expect.objectContaining({ channelGroupCodes: ['HEDE'] }))
|
||||
expect(listSmartLoadingWorkbenchContainers).toHaveBeenLastCalledWith({ channelGroupCodes: ['HEDE'], page: 1, size: 200 })
|
||||
expect(wrapper.text()).toContain('MSCU-2')
|
||||
expect(wrapper.text()).not.toContain('MSCU-1')
|
||||
})
|
||||
|
||||
it('merges detailed pricing channels into the same confirmed display group', async () => {
|
||||
const parcelContainer = {
|
||||
...container,
|
||||
@@ -523,9 +554,12 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(assignSmartLoadingGroup).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps the package count editor inside its table cell without overflow ellipsis', () => {
|
||||
it('shows the package summary first and opens the editor from an icon', () => {
|
||||
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')
|
||||
expect(source).toContain('class="package-count-summary"')
|
||||
expect(source).toContain('aria-label="调整箱数"')
|
||||
expect(source).toContain('v-if="(row.remainingPackageCount ?? 0) > 0"')
|
||||
expect(source).toContain('v-if="editingQuantityId === row.id"')
|
||||
expect(source).toContain(':controls="false"')
|
||||
expect(source).toContain('@blur="adjust(row)" @keyup.enter="adjust(row)"')
|
||||
expect(source).not.toContain('<Save')
|
||||
@@ -541,6 +575,8 @@ describe('GlobalSmartLoadingView', () => {
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.find('[aria-label="调整箱数"]').trigger('click')
|
||||
await flushPromises()
|
||||
const quantityInput = wrapper.findAllComponents(ElInputNumber).find((input) => input.props('modelValue') === 6)
|
||||
expect(quantityInput?.props('controls')).toBe(false)
|
||||
quantityInput?.vm.$emit('update:modelValue', 4)
|
||||
@@ -809,6 +845,8 @@ describe('GlobalSmartLoadingView', () => {
|
||||
|
||||
expect(wrapper.text()).toContain('已拣 3 箱')
|
||||
expect(wrapper.findAll('.row-actions .el-button--danger')).toHaveLength(0)
|
||||
await wrapper.find('[aria-label="调整箱数"]').trigger('click')
|
||||
await flushPromises()
|
||||
const quantityInput = wrapper.findAllComponents(ElInputNumber).find((input) => input.props('modelValue') === 6)
|
||||
expect(quantityInput?.props('min')).toBe(3)
|
||||
expect(wrapper.findAll('button').some((button) => button.text().includes('移除未拣'))).toBe(false)
|
||||
@@ -832,11 +870,20 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(wrapper.findAll('button').some((button) => button.text().includes('加入'))).toBe(false)
|
||||
})
|
||||
|
||||
it('limits adjusted package count to the assigned count plus remaining availability', async () => {
|
||||
it('uses the server supplied total package count as the adjustment maximum', async () => {
|
||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{ accountId: '1', inboundId: '2', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '示例企业', goodsSummary: '服装', availablePackageCount: 4, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 0.8, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null, locked: null, blockReason: null }]))
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
|
||||
...plan,
|
||||
items: [{ ...plan.items[0], totalPackageCount: 10, remainingPackageCount: 4 }],
|
||||
})
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('使用:6')
|
||||
expect(wrapper.text()).toContain('未用:4')
|
||||
expect(wrapper.text()).not.toContain('共 10 箱')
|
||||
await wrapper.find('[aria-label="调整箱数"]').trigger('click')
|
||||
await flushPromises()
|
||||
const maxima = wrapper.findAllComponents(ElInputNumber).map((input) => input.props('max'))
|
||||
expect(maxima).toContain(10)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Plus, RefreshCw, Sparkles, Trash2 } from '@lucide/vue'
|
||||
import { Pencil, Plus, RefreshCw, Sparkles, Trash2 } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from '../smartLoadingChannelGroups'
|
||||
|
||||
const props = withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: false })
|
||||
const editingQuantityId = ref('')
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const containers = ref<SmartLoadingWorkbenchContainer[]>([])
|
||||
@@ -234,7 +235,7 @@ const itemColumns: DataTableColumn[] = [
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140, fixed: 'left', showOverflowTooltip: true },
|
||||
{ key: 'coLoadGroup', label: '同柜', minWidth: 80, slotName: 'coLoadGroup' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 70, slotName: 'customerName' },
|
||||
{ key: 'packageCount', label: '分配箱数', width: 100, slotName: 'packageCount' },
|
||||
{ key: 'packageCount', label: '分配箱数', width: 140, slotName: 'packageCount' },
|
||||
{ key: 'grossWeightKg', label: '毛重', width: 90, slotName: 'weight' },
|
||||
{ key: 'volumeCbm', label: '体积', width: 90, slotName: 'volume' },
|
||||
]
|
||||
@@ -250,8 +251,12 @@ function candidateRow(source: object) { return source as SmartLoadingCandidateRo
|
||||
function itemRow(source: object) { return source as SmartLoadingInboundItem }
|
||||
function adjustmentMax(source: object) {
|
||||
const item = itemRow(source)
|
||||
const remaining = candidates.value.find((candidate) => candidate.inboundId === item.inboundId)?.availablePackageCount || 0
|
||||
return Math.max(item.minPackageCount, item.packageCount + remaining)
|
||||
return Math.max(item.minPackageCount, item.totalPackageCount || item.packageCount)
|
||||
}
|
||||
function startQuantityEdit(source: object) {
|
||||
const item = itemRow(source)
|
||||
quantityDrafts[item.id] = item.packageCount
|
||||
editingQuantityId.value = item.id
|
||||
}
|
||||
function displayCabinetNo(container: SmartLoadingWorkbenchContainer | null | undefined) {
|
||||
if (!container) return '待生成'
|
||||
@@ -440,6 +445,12 @@ async function selectChannels(codes: string[]) {
|
||||
candidatePage.value = 1
|
||||
await loadWorkbenchData()
|
||||
}
|
||||
async function selectOnlyChannel(code: string) {
|
||||
if (selectedChannelCodes.value.length === 1 && selectedChannelCodes.value[0] === code) return
|
||||
selectedChannelCodes.value = [code]
|
||||
candidatePage.value = 1
|
||||
await loadWorkbenchData()
|
||||
}
|
||||
async function applyFilters(model: QueryModel) {
|
||||
appliedQuery.value = {
|
||||
...model,
|
||||
@@ -539,6 +550,7 @@ async function adjust(source: object) {
|
||||
await mutate(async () => {
|
||||
acceptPlan(await adjustSmartLoadingItem(plan.value!.id, item.id, count, plan.value!.version, item.version))
|
||||
await loadCandidateData()
|
||||
editingQuantityId.value = ''
|
||||
ElMessage.success('箱数已调整')
|
||||
}, '调整箱数失败', { preserveDrafts: { ...quantityDrafts } })
|
||||
}
|
||||
@@ -623,7 +635,14 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
<div v-if="channelOptions.length" class="channel-summary-row">
|
||||
<el-checkbox-group :model-value="selectedChannelCodes" class="channel-tags" aria-label="渠道筛选,至少选择一项" @change="selectChannels($event as string[])">
|
||||
<span class="channel-tags__label">渠道</span>
|
||||
<el-checkbox-button v-for="channel in channelOptions" :key="channel.code" :label="channel.code" :value="channel.code">{{ channel.name }}</el-checkbox-button>
|
||||
<el-checkbox-button
|
||||
v-for="channel in channelOptions"
|
||||
:key="channel.code"
|
||||
:label="channel.code"
|
||||
:value="channel.code"
|
||||
title="双击仅选择此渠道"
|
||||
@dblclick.stop.prevent="selectOnlyChannel(channel.code)"
|
||||
>{{ channel.name }}</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
<dl class="compact-summary" aria-label="已选渠道装柜统计">
|
||||
<div><dt>候选明细</dt><dd><strong>{{ candidateTotal }}</strong> 条</dd></div>
|
||||
@@ -761,7 +780,7 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canDelete && editable && row.canRemove" size="small" type="danger" text :disabled="mutating" @click="remove(row)"><Trash2 :size="15" />删除</el-button></div></template>
|
||||
<template #customerName="{ row }"><LineClampText :content="row.customerName" /></template>
|
||||
<template #coLoadGroup="{ row }"><div v-if="row.coLoadGroup" class="candidate-group"><el-tag size="small" type="warning"><LineClampText :content="`${row.coLoadGroup.groupNo} · ${groupMemberLabel(row.coLoadGroup)}`" /></el-tag><LineClampText :content="row.memberOrderSummaries.join(';')" /><LineClampText :content="`固定 ${row.coLoadGroup.fullPackageCount} 箱 · ${row.coLoadGroup.lockedPlanId ? '已锁定' : '未锁定'}`" /></div><LineClampText v-else content="普通单票" /></template>
|
||||
<template #packageCount="{ row }"><LineClampText v-if="row.coLoadGroup" :content="`${row.packageCount} 箱(整组固定)`" /><div v-else class="package-count-editor"><el-input-number v-model="quantityDrafts[row.id]" :min="row.minPackageCount" :max="adjustmentMax(row)" :step="1" :controls="false" :disabled="mutating || !canAssign || !editable || !row.canAdjust" @blur="adjust(row)" @keyup.enter="adjust(row)" /><LineClampText v-if="row.detailCount > 1 && editable" content="多规格,需删除后重加" /><LineClampText v-if="row.pickedPackageCount > 0" :content="`已拣 ${row.pickedPackageCount} 箱`" /></div></template>
|
||||
<template #packageCount="{ row }"><LineClampText v-if="row.coLoadGroup" :content="`${row.packageCount} 箱(整组固定)`" /><div v-else class="package-count-editor"><el-input-number v-if="editingQuantityId === row.id" v-model="quantityDrafts[row.id]" :min="row.minPackageCount" :max="adjustmentMax(row)" :step="1" :controls="false" :disabled="mutating" @blur="adjust(row)" @keyup.enter="adjust(row)" /><div v-else class="package-count-summary"><div class="package-count-used"><strong>使用:{{ row.packageCount }}</strong><el-button v-if="canAssign && editable && row.canAdjust" link circle :disabled="mutating" aria-label="调整箱数" @click="startQuantityEdit(row)"><Pencil :size="14" /></el-button></div><small v-if="(row.remainingPackageCount ?? 0) > 0">未用:{{ row.remainingPackageCount }}</small></div><LineClampText v-if="row.detailCount > 1 && editable" content="多规格,需删除后重加" /><LineClampText v-if="row.pickedPackageCount > 0" :content="`已拣 ${row.pickedPackageCount} 箱`" /></div></template>
|
||||
<template #weight="{ row }">{{ formatNumber(Number(row.grossWeightKg || 0), 1) }} kg</template>
|
||||
<template #volume="{ row }">{{ formatNumber(Number(row.volumeCbm || 0)) }} m³</template>
|
||||
</DataTable>
|
||||
@@ -845,6 +864,10 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
.readonly-status-card p { max-width: 460px; margin: 0; color: var(--cc-muted); line-height: 1.6; }
|
||||
.meter-title { justify-content: space-between; margin-bottom: 8px; font-size: 12px; }
|
||||
.package-count-editor { display: grid; min-width: 0; gap: 4px; }
|
||||
.package-count-summary { display: grid; gap: 3px; line-height: 1.35; }
|
||||
.package-count-used { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
|
||||
.package-count-summary strong { font-size: 13px; font-weight: 600; }
|
||||
.package-count-summary small { color: var(--cc-muted); font-size: 12px; }
|
||||
.plan-pane :deep(.package-count-editor .el-input-number) { width: 100%; min-width: 0; }
|
||||
.assign-dialog p { margin: 0 0 10px; }
|
||||
.assign-dialog :deep(.el-input-number) { width: 100%; }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeft, ListChecks, Plus, RefreshCw, Sparkles, Trash2 } from '@lucide/vue'
|
||||
import { ArrowLeft, ListChecks, Pencil, Plus, RefreshCw, Sparkles, Trash2 } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
@@ -33,6 +33,7 @@ const mutating = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const mutationError = ref<ApiErrorDetails | null>(null)
|
||||
const noDraft = ref(false)
|
||||
const editingQuantityId = ref('')
|
||||
const assignOpen = ref(false)
|
||||
const selectedCandidate = ref<SmartLoadingCandidateRow | null>(null)
|
||||
const assignForm = reactive({ packageCount: 1 })
|
||||
@@ -152,6 +153,15 @@ function formatNumber(value: number, digits = 3) { return Number.isFinite(value)
|
||||
function percentage(value: number) { return Number.isFinite(value) ? Number(Math.max(value, 0).toFixed(2)) : 0 }
|
||||
function candidateRow(source: object) { return source as SmartLoadingCandidateRow }
|
||||
function itemRow(source: object) { return source as SmartLoadingInboundItem }
|
||||
function adjustmentMax(source: object) {
|
||||
const item = itemRow(source)
|
||||
return Math.max(item.minPackageCount, item.totalPackageCount || item.packageCount)
|
||||
}
|
||||
function startQuantityEdit(source: object) {
|
||||
const item = itemRow(source)
|
||||
quantityDrafts[item.id] = item.packageCount
|
||||
editingQuantityId.value = item.id
|
||||
}
|
||||
function explainCandidate(source: object) {
|
||||
const candidate = candidateRow(source)
|
||||
const volume = Number(candidate.averageUnitVolumeCbm || 0)
|
||||
@@ -268,7 +278,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 { await acceptPlanWithGroups(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)); editingQuantityId.value = ''; ElMessage.success('箱数已调整') }
|
||||
catch (error) { await handleMutationError(error, '调整失败') }
|
||||
finally { mutating.value = false }
|
||||
}
|
||||
@@ -364,7 +374,7 @@ onMounted(load)
|
||||
<template #customerName="{ row }"><LineClampText :content="row.customerName" /></template>
|
||||
<template #coLoadGroup="{ row }"><div v-if="row.coLoadGroup" class="candidate-group"><el-tag size="small" type="warning"><LineClampText :content="`${row.coLoadGroup.groupNo} · ${groupMemberLabel(row.coLoadGroup)}`" /></el-tag><LineClampText :content="row.memberOrderSummaries.join(';')" /><LineClampText :content="`全部入仓片段联动,固定 ${row.coLoadGroup.fullPackageCount} 箱`" /></div><LineClampText v-else content="普通单票" /></template>
|
||||
<template #goods="{ row }"><strong>{{ row.productNameZh || '未命名货物' }}</strong><small>{{ row.skuCode || '无 SKU' }}</small></template>
|
||||
<template #packageCount="{ row }"><LineClampText v-if="row.coLoadGroup" :content="`${row.packageCount} 箱(整组固定)`" /><div v-else class="package-count-editor"><el-input-number v-model="quantityDrafts[row.id]" :min="row.minPackageCount" :step="1" :controls="false" :disabled="mutating || !canAssign || !editable || !row.canAdjust" @blur="adjust(row)" @keyup.enter="adjust(row)" /><LineClampText v-if="row.detailCount > 1 && editable" :content="`含 ${row.detailCount} 个规格,需删除后重加`" /><LineClampText v-if="row.pickedPackageCount > 0" :content="`已拣 ${row.pickedPackageCount} 箱`" /></div></template>
|
||||
<template #packageCount="{ row }"><LineClampText v-if="row.coLoadGroup" :content="`${row.packageCount} 箱(整组固定)`" /><div v-else class="package-count-editor"><el-input-number v-if="editingQuantityId === row.id" v-model="quantityDrafts[row.id]" :min="row.minPackageCount" :max="adjustmentMax(row)" :step="1" :controls="false" :disabled="mutating" @blur="adjust(row)" @keyup.enter="adjust(row)" /><div v-else class="package-count-summary"><div class="package-count-used"><strong>使用:{{ row.packageCount }}</strong><el-button v-if="canAssign && editable && row.canAdjust" link circle :disabled="mutating" aria-label="调整箱数" @click="startQuantityEdit(row)"><Pencil :size="14" /></el-button></div><small v-if="(row.remainingPackageCount ?? 0) > 0">未用:{{ row.remainingPackageCount }}</small></div><LineClampText v-if="row.detailCount > 1 && editable" :content="`含 ${row.detailCount} 个规格,需删除后重加`" /><LineClampText v-if="row.pickedPackageCount > 0" :content="`已拣 ${row.pickedPackageCount} 箱`" /></div></template>
|
||||
<template #volume="{ row }">{{ formatNumber(Number(row.volumeCbm || 0)) }} m³</template><template #weight="{ row }">{{ formatNumber(Number(row.grossWeightKg || 0),1) }} kg</template>
|
||||
</DataTable>
|
||||
<DataTable :data="displayedCandidates" :columns="candidateColumns" :loading="loading || mutating" stripe preference-key="smart-loading-candidates" column-settings-mode="hide" empty-text="当前仓库暂无可排柜货物">
|
||||
@@ -405,6 +415,10 @@ onMounted(load)
|
||||
.meter-title { justify-content: space-between; gap: 12px; margin-bottom: 10px; font-size: 13px; }
|
||||
.row-actions { min-width: 150px; }
|
||||
.package-count-editor { display: grid; min-width: 0; gap: 4px; }
|
||||
.package-count-summary { display: grid; gap: 3px; line-height: 1.35; }
|
||||
.package-count-used { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
|
||||
.package-count-summary strong { font-size: 13px; font-weight: 600; }
|
||||
.package-count-summary small { color: var(--cc-muted); font-size: 12px; }
|
||||
.workbench :deep(.package-count-editor .el-input-number) { width: 100%; min-width: 0; }
|
||||
.assign-dialog p { margin: 0 0 10px; }
|
||||
.assign-dialog :deep(.el-input-number) { width: 100%; }
|
||||
|
||||
@@ -42,6 +42,8 @@ export function groupSmartLoadingItems(items: SmartLoadingItem[]): SmartLoadingI
|
||||
loadSequence: loadSequences.length ? Math.min(...loadSequences) : null,
|
||||
pickedPackageCount,
|
||||
minPackageCount: sourceItems.reduce((sum, item) => sum + Number(item.minPackageCount || 0), 0),
|
||||
totalPackageCount: sourceItems.reduce((sum, item) => sum + Number(item.totalPackageCount || item.packageCount || 0), 0),
|
||||
remainingPackageCount: sourceItems.reduce((sum, item) => sum + Number(item.remainingPackageCount || 0), 0),
|
||||
canAdjust: sourceItems.length === 1 && first.canAdjust,
|
||||
canRemove: sourceItems.every((item) => item.canRemove),
|
||||
sourceItems,
|
||||
|
||||
@@ -341,6 +341,8 @@ export interface SmartLoadingItem {
|
||||
version: number
|
||||
pickedPackageCount: number
|
||||
minPackageCount: number
|
||||
totalPackageCount?: number
|
||||
remainingPackageCount?: number
|
||||
canAdjust: boolean
|
||||
canRemove: boolean
|
||||
}
|
||||
|
||||
@@ -231,20 +231,15 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
expect(wrapper.text()).not.toContain('raw-0')
|
||||
})
|
||||
|
||||
it('renders server-bound pallet sequence, target location, and pending putaway status ahead of OCR facts', async () => {
|
||||
it('renders the server-bound location and status once in the compact card facts', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
const card = wrapper.get('.weight-card')
|
||||
expect(card.text()).toContain('第 1 托')
|
||||
expect(card.text()).toContain('目标库位')
|
||||
expect(card.text()).toContain('A01')
|
||||
expect(card.text()).toContain('整票预留库位')
|
||||
expect(card.text()).toContain('库位A01 · 待上架')
|
||||
expect(card.text()).toContain('待上架')
|
||||
const putawayGuide = card.get('.weight-card__putaway').element
|
||||
expect(putawayGuide.compareDocumentPosition(
|
||||
card.get('.weight-card__value').element,
|
||||
) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
expect(card.find('.weight-card__putaway').exists()).toBe(false)
|
||||
expect(card.get('.weight-card__facts').text()).toContain('库位A01 · 待上架')
|
||||
})
|
||||
|
||||
it('renders the authoritative whole-inbound reservation summary and an empty reserved slot', async () => {
|
||||
@@ -274,12 +269,13 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
expect(guide.text()).toContain('A03预留空位本票预留空位')
|
||||
})
|
||||
|
||||
it('shows an explicit whole-inbound unassigned state when no guide exists', async () => {
|
||||
it('does not render a full-width placeholder when no whole-inbound guide exists', async () => {
|
||||
vi.mocked(getWeightWorkspace).mockResolvedValue({ ...workspace, palletLocationGuide: null })
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('尚未分配整票预留库位')
|
||||
expect(wrapper.text()).not.toContain('尚未分配整票预留库位')
|
||||
expect(wrapper.find('.putaway-guide-empty').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('marks A0 as a temporary overflow location from the server flag', async () => {
|
||||
@@ -299,7 +295,7 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('A0 临时溢出位')
|
||||
expect(wrapper.get('.weight-card__putaway').classes()).toContain('is-overflow')
|
||||
expect(wrapper.get('.weight-card__facts').text()).toContain('库位A0 · 待上架 · A0 临时溢出位')
|
||||
expect(wrapper.get('.putaway-guide__slots li').classes()).toContain('is-overflow')
|
||||
})
|
||||
|
||||
@@ -336,8 +332,8 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('托盘未分配')
|
||||
expect(wrapper.text()).toContain('目标库位未分配')
|
||||
expect(wrapper.text()).not.toContain('托盘未分配')
|
||||
expect(wrapper.get('.weight-card__facts').text()).toContain('库位未分配')
|
||||
})
|
||||
|
||||
it('deletes a pending image with its optimistic-lock version', async () => {
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('WeightOcrPanel contract', () => {
|
||||
expect(source).toContain('货物重量')
|
||||
expect(source).toContain('palletTareWeightKg')
|
||||
expect(source).toContain('netWeightKg')
|
||||
expect(source).toContain('row.palletSequence')
|
||||
expect(source).toContain('slot.palletSequence')
|
||||
expect(source).toContain('row.locationCode')
|
||||
expect(source).toContain('row.sharedOverflow')
|
||||
expect(source).toContain('putawayStatusLabel(row.slotStatus)')
|
||||
@@ -43,6 +43,9 @@ describe('WeightOcrPanel contract', () => {
|
||||
expect(source).toContain('workspace.palletLocationGuide.actualPalletCount')
|
||||
expect(source).toContain('workspace.palletLocationGuide.reservedEmptySlotCount')
|
||||
expect(source).toContain('本票预留空位')
|
||||
expect(source).not.toContain('尚未分配整票预留库位')
|
||||
expect(source).not.toContain('class="weight-card__putaway"')
|
||||
expect(source).toContain('<div><dt>库位</dt><dd')
|
||||
expect(source).toContain('对应库位仍为本票保留,不会释放给其他订单')
|
||||
expect(source).toContain("emit('summaryChanged', workspace.value?.actualGrossWeightKg ?? null, workspace.value?.actualNetWeightKg ?? null)")
|
||||
})
|
||||
|
||||
@@ -588,10 +588,6 @@ defineExpose({ reload: loadWorkspace })
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
<div v-else-if="workspace" class="putaway-guide-empty" role="status">
|
||||
<strong>尚未分配整票预留库位</strong>
|
||||
<span>称重图片绑定库位后,将按服务端上架引导显示目标库位。</span>
|
||||
</div>
|
||||
<LoadErrorState
|
||||
v-if="palletError"
|
||||
title="可选托盘加载失败"
|
||||
@@ -639,20 +635,6 @@ defineExpose({ reload: loadWorkspace })
|
||||
><Trash2 :size="16" /></el-button>
|
||||
</el-tooltip>
|
||||
<div class="weight-card__body">
|
||||
<div class="weight-card__putaway" :class="{ 'is-overflow': row.sharedOverflow }">
|
||||
<div class="weight-card__putaway-title">
|
||||
<strong>{{ row.palletSequence == null ? '托盘未分配' : `第 ${row.palletSequence} 托` }}</strong>
|
||||
<el-tag size="small" effect="light" :type="putawayStatusType(row.slotStatus)">
|
||||
{{ putawayStatusLabel(row.slotStatus) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="weight-card__location">
|
||||
<span>目标库位</span>
|
||||
<strong>{{ row.locationCode || '未分配' }}</strong>
|
||||
</div>
|
||||
<span v-if="row.sharedOverflow" class="weight-card__overflow">A0 临时溢出位</span>
|
||||
<span v-else-if="row.locationCode" class="weight-card__normal-location">整票预留库位</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="weight-card__value"
|
||||
@@ -662,6 +644,7 @@ defineExpose({ reload: loadWorkspace })
|
||||
<dl class="weight-card__facts">
|
||||
<div><dt>托盘编码</dt><dd :title="row.palletCode">{{ row.palletCode || '未记录编码' }}</dd></div>
|
||||
<div><dt>托盘类型</dt><dd>{{ row.palletType || '未记录类型' }}</dd></div>
|
||||
<div><dt>库位</dt><dd :title="row.locationCode || '未分配'">{{ row.locationCode || '未分配' }}<template v-if="row.locationCode"> · {{ putawayStatusLabel(row.slotStatus) }}</template><template v-if="row.sharedOverflow"> · A0 临时溢出位</template></dd></div>
|
||||
<div><dt>服务端确认毛重</dt><dd>{{ formatWeight(row.grossWeightKg) }}</dd></div>
|
||||
<div><dt>托盘皮重</dt><dd>{{ formatWeight(row.palletTareWeightKg) }}</dd></div>
|
||||
<div><dt>货物重量</dt><dd>{{ formatWeight(row.netWeightKg) }}</dd></div>
|
||||
@@ -768,26 +751,12 @@ defineExpose({ reload: loadWorkspace })
|
||||
.putaway-guide__slots li.is-overflow .putaway-guide__slot-title strong { color: #93370d; }
|
||||
.putaway-guide__slots li > span { color: #475467; font-size: 12px; overflow-wrap: anywhere; }
|
||||
.putaway-guide__slots small { width: fit-content; color: #93370d; font-weight: 600; }
|
||||
.putaway-guide-empty { display: grid; gap: 3px; margin: 14px 0 18px; padding: 13px 15px; border: 1px dashed #98a2b3; border-radius: 8px; background: #f9fafb; }
|
||||
.putaway-guide-empty strong { color: #344054; font-size: 14px; }
|
||||
.putaway-guide-empty span { color: #667085; font-size: 12px; }
|
||||
.weight-list { min-height: 0; margin-top: 14px; }
|
||||
.weight-empty { display: flex; min-height: 0; align-items: center; gap: 10px; padding: 4px 0 0; color: var(--cc-muted); }
|
||||
.weight-empty strong { color: #344054; font-size: 14px; }
|
||||
.weight-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 14px; }
|
||||
.weight-card { position: relative; min-width: 0; overflow: hidden; border: 1px solid var(--cc-border); border-radius: 6px; background: #fff; }
|
||||
.weight-card__body { padding: 12px; text-align: center; }
|
||||
.weight-card__putaway { display: grid; gap: 8px; margin: -12px -12px 12px; padding: 12px; border-bottom: 1px solid #b2ddff; background: #eff8ff; text-align: left; }
|
||||
.weight-card__putaway.is-overflow { border-color: #fec84b; background: #fffaeb; }
|
||||
.weight-card__putaway-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.weight-card__putaway-title strong { color: #1849a9; font-size: 15px; }
|
||||
.weight-card__putaway.is-overflow .weight-card__putaway-title strong { color: #93370d; }
|
||||
.weight-card__location { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
|
||||
.weight-card__location span { color: #475467; font-size: 12px; }
|
||||
.weight-card__location strong { color: #101828; font-size: 22px; line-height: 1; overflow-wrap: anywhere; }
|
||||
.weight-card__overflow, .weight-card__normal-location { width: fit-content; padding: 2px 7px; border-radius: 999px; font-size: 11px; font-weight: 600; }
|
||||
.weight-card__overflow { background: #fef0c7; color: #93370d; }
|
||||
.weight-card__normal-location { background: #d1e9ff; color: #175cd3; }
|
||||
.weight-delete { position: absolute; z-index: 2; top: 8px; right: 8px; width: 30px; height: 30px; padding: 0; border: 1px solid rgb(255 255 255 / 82%); background: rgb(255 255 255 / 92%); box-shadow: 0 1px 3px rgb(16 24 40 / 16%); }
|
||||
.weight-card__value { width: 100%; padding: 0; border: 0; background: transparent; color: inherit; cursor: pointer; text-align: center; }
|
||||
.weight-card__value strong { color: #101828; font-size: 17px; }
|
||||
|
||||
Reference in New Issue
Block a user