Release/test 合并上生产 #2
@@ -18,6 +18,7 @@ import {
|
||||
listSmartLoadingCandidates,
|
||||
listSmartLoadingWorkbenchContainers,
|
||||
recommendSmartLoading,
|
||||
adjustSmartLoadingItem,
|
||||
removeAllUnpickedSmartLoadingItems,
|
||||
removeSmartLoadingGroup,
|
||||
removeSmartLoadingItem,
|
||||
@@ -525,11 +526,32 @@ describe('GlobalSmartLoadingView', () => {
|
||||
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')
|
||||
expect(source).toContain('@change="adjust(row)"')
|
||||
expect(source).toContain(':controls="false"')
|
||||
expect(source).toContain('@blur="adjust(row)" @keyup.enter="adjust(row)"')
|
||||
expect(source).not.toContain('<Save')
|
||||
expect(source).toContain('.plan-pane :deep(.package-count-editor .el-input-number) { width: 100%; min-width: 0; }')
|
||||
})
|
||||
|
||||
it('saves a typed package count through the adjustment API when editing finishes', async () => {
|
||||
vi.mocked(adjustSmartLoadingItem).mockResolvedValue({
|
||||
...plan,
|
||||
version: 3,
|
||||
items: [{ ...plan.items[0], packageCount: 4, version: 2 }],
|
||||
})
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
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)
|
||||
await flushPromises()
|
||||
quantityInput?.vm.$emit('blur')
|
||||
await flushPromises()
|
||||
|
||||
expect(adjustSmartLoadingItem).toHaveBeenCalledTimes(1)
|
||||
expect(adjustSmartLoadingItem).toHaveBeenCalledWith(plan.id, plan.items[0].id, 4, plan.version, plan.items[0].version)
|
||||
})
|
||||
|
||||
it('rounds capacity percentages and hides column settings in embedded tables', async () => {
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
|
||||
...plan,
|
||||
|
||||
@@ -527,8 +527,9 @@ async function recommend() {
|
||||
}
|
||||
async function adjust(source: object) {
|
||||
const item = itemRow(source)
|
||||
if (!plan.value) return
|
||||
if (!plan.value || mutating.value) return
|
||||
const count = Number(quantityDrafts[item.id])
|
||||
if (count === item.packageCount) return
|
||||
if (!Number.isInteger(count) || count < item.minPackageCount) {
|
||||
ElMessage.warning(`分配箱数不能低于已拣下限 ${item.minPackageCount} 箱`)
|
||||
return
|
||||
@@ -760,7 +761,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" :disabled="mutating || !canAssign || !editable || !row.canAdjust" controls-position="right" @change="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-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 #weight="{ row }">{{ formatNumber(Number(row.grossWeightKg || 0), 1) }} kg</template>
|
||||
<template #volume="{ row }">{{ formatNumber(Number(row.volumeCbm || 0)) }} m³</template>
|
||||
</DataTable>
|
||||
|
||||
@@ -132,7 +132,8 @@ describe('SmartLoadingWorkbenchView', () => {
|
||||
it('keeps the package count editor inside its table cell without overflow ellipsis', () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue'), 'utf8')
|
||||
expect(source).toContain('<div v-else class="package-count-editor"><el-input-number')
|
||||
expect(source).toContain('@change="adjust(row)"')
|
||||
expect(source).toContain(':controls="false"')
|
||||
expect(source).toContain('@blur="adjust(row)" @keyup.enter="adjust(row)"')
|
||||
expect(source).not.toContain('<Save')
|
||||
expect(source).toContain('.workbench :deep(.package-count-editor .el-input-number) { width: 100%; min-width: 0; }')
|
||||
})
|
||||
|
||||
@@ -262,8 +262,9 @@ async function recommend() {
|
||||
}
|
||||
async function adjust(source: object) {
|
||||
const item = itemRow(source)
|
||||
if (!plan.value) return
|
||||
if (!plan.value || mutating.value) return
|
||||
const count = Number(quantityDrafts[item.id])
|
||||
if (count === item.packageCount) return
|
||||
if (!Number.isInteger(count) || count < item.minPackageCount) { ElMessage.warning(`分配箱数不能低于已拣下限 ${item.minPackageCount} 箱`); return }
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
@@ -363,7 +364,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" :disabled="mutating || !canAssign || !editable || !row.canAdjust" controls-position="right" @change="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-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 #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="当前仓库暂无可排柜货物">
|
||||
|
||||
@@ -61,6 +61,14 @@ const pendingWeight: WeightResultResource = {
|
||||
adjustedAt: null,
|
||||
confirmedBy: null,
|
||||
confirmedAt: null,
|
||||
palletSequence: 1,
|
||||
blockId: '70001',
|
||||
slotNo: 1,
|
||||
reservationId: '71001',
|
||||
locationId: '72001',
|
||||
locationCode: 'A01',
|
||||
sharedOverflow: false,
|
||||
slotStatus: 'BOUND',
|
||||
version: 0,
|
||||
}
|
||||
|
||||
@@ -71,6 +79,24 @@ const workspace: WeightWorkspaceResource = {
|
||||
actualGrossWeightKg: null,
|
||||
actualNetWeightKg: null,
|
||||
weightResults: [pendingWeight],
|
||||
palletLocationGuide: {
|
||||
blockId: '70001',
|
||||
reservedSlotCount: 1,
|
||||
actualPalletCount: 1,
|
||||
reservedEmptySlotCount: 0,
|
||||
slots: [{
|
||||
slotNo: 1,
|
||||
reservationId: '71001',
|
||||
locationId: '72001',
|
||||
locationCode: 'A01',
|
||||
sharedOverflow: false,
|
||||
slotStatus: 'BOUND',
|
||||
boundWeightResultId: '30001',
|
||||
palletSequence: 1,
|
||||
palletId: '50001',
|
||||
palletCode: 'TP-001',
|
||||
}],
|
||||
},
|
||||
}
|
||||
|
||||
function palletPage(records = [{
|
||||
@@ -205,9 +231,137 @@ 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 () => {
|
||||
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('待上架')
|
||||
const putawayGuide = card.get('.weight-card__putaway').element
|
||||
expect(putawayGuide.compareDocumentPosition(
|
||||
card.get('.weight-card__value').element,
|
||||
) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders the authoritative whole-inbound reservation summary and an empty reserved slot', async () => {
|
||||
vi.mocked(getWeightWorkspace).mockResolvedValue({
|
||||
...workspace,
|
||||
palletLocationGuide: {
|
||||
blockId: '70001',
|
||||
reservedSlotCount: 3,
|
||||
actualPalletCount: 2,
|
||||
reservedEmptySlotCount: 1,
|
||||
slots: [
|
||||
{ slotNo: 1, reservationId: '71001', locationId: '72001', locationCode: 'A01', sharedOverflow: false, slotStatus: 'BOUND', boundWeightResultId: '30001', palletSequence: 1, palletId: '50001', palletCode: 'TP-001' },
|
||||
{ slotNo: 2, reservationId: '71002', locationId: '72002', locationCode: 'A02', sharedOverflow: false, slotStatus: 'TRANSFERRED', boundWeightResultId: '30002', palletSequence: 2, palletId: '50002', palletCode: 'TP-002' },
|
||||
{ slotNo: 3, reservationId: '71003', locationId: '72003', locationCode: 'A03', sharedOverflow: false, slotStatus: 'RESERVED_EMPTY', boundWeightResultId: null, palletSequence: null, palletId: null, palletCode: null },
|
||||
],
|
||||
},
|
||||
})
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
const guide = wrapper.get('.putaway-guide')
|
||||
expect(guide.text()).toContain('整票预留库位')
|
||||
expect(guide.text()).toContain('A01 / A02 / A03')
|
||||
expect(guide.text()).toContain('预留槽位数3')
|
||||
expect(guide.text()).toContain('实际托数2')
|
||||
expect(guide.text()).toContain('预留空位数1')
|
||||
expect(guide.text()).toContain('A03预留空位本票预留空位')
|
||||
})
|
||||
|
||||
it('shows an explicit whole-inbound unassigned state when no guide exists', async () => {
|
||||
vi.mocked(getWeightWorkspace).mockResolvedValue({ ...workspace, palletLocationGuide: null })
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('尚未分配整票预留库位')
|
||||
})
|
||||
|
||||
it('marks A0 as a temporary overflow location from the server flag', async () => {
|
||||
vi.mocked(getWeightWorkspace).mockResolvedValue({
|
||||
...workspace,
|
||||
weightResults: [{ ...pendingWeight, locationCode: 'A0', sharedOverflow: true }],
|
||||
palletLocationGuide: {
|
||||
...workspace.palletLocationGuide!,
|
||||
slots: [{
|
||||
...workspace.palletLocationGuide!.slots[0]!,
|
||||
locationCode: 'A0',
|
||||
sharedOverflow: true,
|
||||
}],
|
||||
},
|
||||
})
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('A0 临时溢出位')
|
||||
expect(wrapper.get('.weight-card__putaway').classes()).toContain('is-overflow')
|
||||
expect(wrapper.get('.putaway-guide__slots li').classes()).toContain('is-overflow')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['TRANSFERRED', '已上架'],
|
||||
['RELEASED', '已释放'],
|
||||
['RESERVED_EMPTY', '预留空位'],
|
||||
] as const)('renders the authoritative %s slot status as %s', async (slotStatus, label) => {
|
||||
vi.mocked(getWeightWorkspace).mockResolvedValue({
|
||||
...workspace,
|
||||
weightResults: [{ ...pendingWeight, slotStatus }],
|
||||
})
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain(label)
|
||||
})
|
||||
|
||||
it('shows an explicit unassigned state when the server has not bound a slot', async () => {
|
||||
vi.mocked(getWeightWorkspace).mockResolvedValue({
|
||||
...workspace,
|
||||
weightResults: [{
|
||||
...pendingWeight,
|
||||
palletSequence: null,
|
||||
blockId: null,
|
||||
slotNo: null,
|
||||
reservationId: null,
|
||||
locationId: null,
|
||||
locationCode: null,
|
||||
sharedOverflow: null,
|
||||
slotStatus: null,
|
||||
}],
|
||||
})
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('托盘未分配')
|
||||
expect(wrapper.text()).toContain('目标库位未分配')
|
||||
})
|
||||
|
||||
it('deletes a pending image with its optimistic-lock version', async () => {
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||
const confirm = vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||
vi.mocked(deleteWeightResult).mockResolvedValue(undefined)
|
||||
vi.mocked(getWeightWorkspace)
|
||||
.mockResolvedValueOnce(workspace)
|
||||
.mockResolvedValueOnce({
|
||||
...workspace,
|
||||
weightResults: [],
|
||||
palletLocationGuide: {
|
||||
...workspace.palletLocationGuide!,
|
||||
actualPalletCount: 0,
|
||||
reservedEmptySlotCount: 1,
|
||||
slots: [{
|
||||
...workspace.palletLocationGuide!.slots[0]!,
|
||||
slotStatus: 'RESERVED_EMPTY',
|
||||
boundWeightResultId: null,
|
||||
palletSequence: null,
|
||||
palletId: null,
|
||||
palletCode: null,
|
||||
}],
|
||||
},
|
||||
})
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
@@ -215,6 +369,48 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(deleteWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', 0)
|
||||
expect(confirm).toHaveBeenCalledWith(
|
||||
expect.stringContaining('对应库位仍为本票保留,不会释放给其他订单'),
|
||||
'删除称重图片',
|
||||
expect.any(Object),
|
||||
)
|
||||
expect(getWeightWorkspace).toHaveBeenCalledTimes(2)
|
||||
expect(wrapper.findAll('.weight-card')).toHaveLength(0)
|
||||
expect(wrapper.get('.putaway-guide').text()).toContain('预留空位数1')
|
||||
expect(wrapper.get('.putaway-guide').text()).toContain('A01预留空位本票预留空位')
|
||||
})
|
||||
|
||||
it('keeps the image and server workspace visible when deletion fails', async () => {
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||
const errorMessage = vi.spyOn(ElMessage, 'error')
|
||||
vi.mocked(deleteWeightResult).mockRejectedValueOnce(
|
||||
new ApiResponseError('无权删除当前称重图片', 'A0403', 403),
|
||||
)
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.get('button[aria-label="删除称重图片 1"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.findAll('.weight-card')).toHaveLength(1)
|
||||
expect(wrapper.text()).toContain('A01')
|
||||
expect(getWeightWorkspace).toHaveBeenCalledTimes(1)
|
||||
expect(errorMessage).toHaveBeenCalledWith(expect.objectContaining({
|
||||
message: '当前账号无权访问该数据,请联系管理员确认权限',
|
||||
}))
|
||||
})
|
||||
|
||||
it('shows a retryable workspace load failure without fabricating putaway guidance', async () => {
|
||||
vi.mocked(getWeightWorkspace).mockRejectedValueOnce(
|
||||
new ApiResponseError('称重工作区暂不可用', 'A0500', 503),
|
||||
)
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('称重数据加载失败')
|
||||
expect(wrapper.text()).toContain('称重工作区暂不可用')
|
||||
expect(wrapper.find('.putaway-guide').exists()).toBe(false)
|
||||
expect(wrapper.find('.putaway-guide-empty').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows only the key weights and allows direct confirmation', async () => {
|
||||
|
||||
@@ -35,6 +35,15 @@ describe('WeightOcrPanel contract', () => {
|
||||
expect(source).toContain('货物重量')
|
||||
expect(source).toContain('palletTareWeightKg')
|
||||
expect(source).toContain('netWeightKg')
|
||||
expect(source).toContain('row.palletSequence')
|
||||
expect(source).toContain('row.locationCode')
|
||||
expect(source).toContain('row.sharedOverflow')
|
||||
expect(source).toContain('putawayStatusLabel(row.slotStatus)')
|
||||
expect(source).toContain('workspace.palletLocationGuide.reservedSlotCount')
|
||||
expect(source).toContain('workspace.palletLocationGuide.actualPalletCount')
|
||||
expect(source).toContain('workspace.palletLocationGuide.reservedEmptySlotCount')
|
||||
expect(source).toContain('本票预留空位')
|
||||
expect(source).toContain('对应库位仍为本票保留,不会释放给其他订单')
|
||||
expect(source).toContain("emit('summaryChanged', workspace.value?.actualGrossWeightKg ?? null, workspace.value?.actualNetWeightKg ?? null)")
|
||||
})
|
||||
|
||||
|
||||
@@ -22,7 +22,12 @@ import {
|
||||
getWeightWorkspace,
|
||||
uploadWeightImage,
|
||||
} from '../api/weightOcr'
|
||||
import type { WeightPalletOption, WeightResultResource, WeightWorkspaceResource } from '../types'
|
||||
import type {
|
||||
WeightPalletOption,
|
||||
WeightPutawaySlotStatus,
|
||||
WeightResultResource,
|
||||
WeightWorkspaceResource,
|
||||
} from '../types'
|
||||
import WeightImageThumbnail from './WeightImageThumbnail.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
@@ -180,6 +185,26 @@ function cardWeight(row: WeightResultResource) {
|
||||
return formatWeight(draft?.value ?? null, draft?.unit ?? 'KG')
|
||||
}
|
||||
|
||||
function putawayStatusLabel(status: WeightResultResource['slotStatus']) {
|
||||
return ({
|
||||
RESERVED_EMPTY: '预留空位',
|
||||
BOUND: '待上架',
|
||||
TRANSFERRED: '已上架',
|
||||
RELEASED: '已释放',
|
||||
} as const)[status as Exclude<WeightResultResource['slotStatus'], null>] || '未分配'
|
||||
}
|
||||
|
||||
function putawayStatusType(status: WeightResultResource['slotStatus']) {
|
||||
if (status === 'TRANSFERRED') return 'success'
|
||||
if (status === 'BOUND') return 'warning'
|
||||
if (status === 'RELEASED') return 'info'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function guideSlotStatusType(status: WeightPutawaySlotStatus) {
|
||||
return putawayStatusType(status)
|
||||
}
|
||||
|
||||
function hasWeightDraftChanged(row: WeightResultResource) {
|
||||
const draft = weightDrafts.value[row.id]
|
||||
if (!draft) return false
|
||||
@@ -403,8 +428,8 @@ async function removeResult(row: WeightResultResource) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
row.recognitionStatus === 'CONFIRMED'
|
||||
? '删除后将重新计算货物总重量,确认删除这张称重图片吗?'
|
||||
: '确认删除这张称重图片吗?',
|
||||
? '删除后将重新计算货物总重量;对应库位仍为本票保留,不会释放给其他订单。确认删除这张称重图片吗?'
|
||||
: '对应库位仍为本票保留,不会释放给其他订单。确认删除这张称重图片吗?',
|
||||
'删除称重图片',
|
||||
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' },
|
||||
)
|
||||
@@ -532,6 +557,41 @@ defineExpose({ reload: loadWorkspace })
|
||||
</LoadErrorState>
|
||||
|
||||
<template v-else>
|
||||
<section v-if="workspace?.palletLocationGuide" class="putaway-guide" aria-labelledby="putaway-guide-title">
|
||||
<div class="putaway-guide__heading">
|
||||
<div>
|
||||
<span class="putaway-guide__eyebrow">整票上架引导</span>
|
||||
<h4 id="putaway-guide-title">整票预留库位</h4>
|
||||
</div>
|
||||
<div class="putaway-guide__metrics" aria-label="整票库位汇总">
|
||||
<div><span>预留库位</span><strong>{{ workspace.palletLocationGuide.slots.map((slot) => slot.locationCode).join(' / ') }}</strong></div>
|
||||
<div><span>预留槽位数</span><strong>{{ workspace.palletLocationGuide.reservedSlotCount }}</strong></div>
|
||||
<div><span>实际托数</span><strong>{{ workspace.palletLocationGuide.actualPalletCount }}</strong></div>
|
||||
<div><span>预留空位数</span><strong>{{ workspace.palletLocationGuide.reservedEmptySlotCount }}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
<ol class="putaway-guide__slots" aria-label="整票预留槽位">
|
||||
<li
|
||||
v-for="slot in workspace.palletLocationGuide.slots"
|
||||
:key="slot.reservationId"
|
||||
:class="{ 'is-empty': slot.slotStatus === 'RESERVED_EMPTY', 'is-overflow': slot.sharedOverflow }"
|
||||
>
|
||||
<div class="putaway-guide__slot-title">
|
||||
<strong>{{ slot.locationCode }}</strong>
|
||||
<el-tag size="small" effect="light" :type="guideSlotStatusType(slot.slotStatus)">
|
||||
{{ putawayStatusLabel(slot.slotStatus) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-if="slot.palletSequence != null">第 {{ slot.palletSequence }} 托 · {{ slot.palletCode || '托盘编码未记录' }}</span>
|
||||
<span v-else>本票预留空位</span>
|
||||
<small v-if="slot.sharedOverflow">A0 临时溢出位</small>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
<div v-else-if="workspace" class="putaway-guide-empty" role="status">
|
||||
<strong>尚未分配整票预留库位</strong>
|
||||
<span>称重图片绑定库位后,将按服务端上架引导显示目标库位。</span>
|
||||
</div>
|
||||
<LoadErrorState
|
||||
v-if="palletError"
|
||||
title="可选托盘加载失败"
|
||||
@@ -579,6 +639,20 @@ 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"
|
||||
@@ -676,13 +750,44 @@ defineExpose({ reload: loadWorkspace })
|
||||
.weight-summary span { color: var(--cc-muted); font-size: 12px; }
|
||||
.weight-summary strong { color: #067647; font-size: 16px; }
|
||||
.visually-hidden-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
||||
.weight-action-error { margin-bottom: 14px; }
|
||||
.weight-action-error { margin-bottom: 14px; }
|
||||
.putaway-guide { margin: 14px 0 18px; padding: 16px; border: 1px solid #84caff; border-radius: 8px; background: #f5fbff; }
|
||||
.putaway-guide__heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; }
|
||||
.putaway-guide__eyebrow { color: #175cd3; font-size: 11px; font-weight: 700; letter-spacing: .08em; }
|
||||
.putaway-guide h4 { margin: 3px 0 0; color: #102a56; font-size: 18px; }
|
||||
.putaway-guide__metrics { display: grid; grid-template-columns: minmax(180px, 1.8fr) repeat(3, minmax(90px, .6fr)); gap: 10px; flex: 1; max-width: 820px; }
|
||||
.putaway-guide__metrics div { display: grid; min-width: 0; gap: 3px; padding: 9px 11px; border-radius: 6px; background: #fff; box-shadow: 0 1px 2px rgb(16 24 40 / 6%); }
|
||||
.putaway-guide__metrics span { color: #667085; font-size: 11px; }
|
||||
.putaway-guide__metrics strong { color: #101828; font-size: 15px; overflow-wrap: anywhere; }
|
||||
.putaway-guide__slots { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px; margin: 14px 0 0; padding: 0; list-style: none; }
|
||||
.putaway-guide__slots li { display: grid; gap: 5px; min-width: 0; padding: 11px; border: 1px solid #b2ddff; border-radius: 6px; background: #fff; }
|
||||
.putaway-guide__slots li.is-empty { border-style: dashed; background: #f9fafb; }
|
||||
.putaway-guide__slots li.is-overflow { border-color: #fec84b; background: #fffaeb; }
|
||||
.putaway-guide__slot-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.putaway-guide__slot-title strong { color: #1849a9; font-size: 20px; }
|
||||
.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__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; }
|
||||
@@ -709,6 +814,9 @@ defineExpose({ reload: loadWorkspace })
|
||||
.pallet-picker :deep(.el-select) { width: 100%; }
|
||||
.weight-heading__actions :deep(.el-button) { width: 100%; min-height: 44px; }
|
||||
.weight-summary-group { width: 100%; justify-content: space-between; }
|
||||
.putaway-guide__heading { flex-direction: column; }
|
||||
.putaway-guide__metrics { width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.putaway-guide__metrics div:first-child { grid-column: 1 / -1; }
|
||||
.weight-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
export type WeightRecognitionStatus = 'PENDING_CONFIRM' | 'NEEDS_MANUAL' | 'CONFIRMED' | string
|
||||
export type WeightPutawaySlotStatus = 'RESERVED_EMPTY' | 'BOUND' | 'TRANSFERRED' | 'RELEASED'
|
||||
|
||||
export interface WeightPalletLocationSlot {
|
||||
slotNo: number
|
||||
reservationId: string
|
||||
locationId: string
|
||||
locationCode: string
|
||||
sharedOverflow: boolean
|
||||
slotStatus: WeightPutawaySlotStatus
|
||||
boundWeightResultId: string | null
|
||||
palletSequence: number | null
|
||||
palletId: string | null
|
||||
palletCode: string | null
|
||||
}
|
||||
|
||||
export interface WeightPalletLocationGuide {
|
||||
blockId: string
|
||||
reservedSlotCount: number
|
||||
actualPalletCount: number
|
||||
reservedEmptySlotCount: number
|
||||
slots: WeightPalletLocationSlot[]
|
||||
}
|
||||
|
||||
export interface WeightResultResource {
|
||||
id: string
|
||||
@@ -23,6 +45,14 @@ export interface WeightResultResource {
|
||||
adjustedAt: string | null
|
||||
confirmedBy: string | null
|
||||
confirmedAt: string | null
|
||||
palletSequence: number | null
|
||||
blockId: string | null
|
||||
slotNo: number | null
|
||||
reservationId: string | null
|
||||
locationId: string | null
|
||||
locationCode: string | null
|
||||
sharedOverflow: boolean | null
|
||||
slotStatus: WeightPutawaySlotStatus | null
|
||||
version: number
|
||||
}
|
||||
|
||||
@@ -33,6 +63,7 @@ export interface WeightWorkspaceResource {
|
||||
actualGrossWeightKg: number | null
|
||||
actualNetWeightKg: number | null
|
||||
weightResults: WeightResultResource[]
|
||||
palletLocationGuide: WeightPalletLocationGuide | null
|
||||
}
|
||||
|
||||
export interface WeightPalletOption {
|
||||
|
||||
Reference in New Issue
Block a user