Merge remote-tracking branch 'origin/release/test' into dev
# Conflicts: # src/modules/container-loading/pages/GlobalSmartLoadingView.vue # src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue # src/modules/domestic-containers/pages/DomesticContainersView.vue
This commit is contained in:
@@ -117,6 +117,27 @@ describe('InboundFeePanel', () => {
|
||||
expect(wrapper.find('.surface-panel').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('supports a display-only fee snapshot without calculation or transition actions', async () => {
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED', confirmedAt: null, sentAt: null })
|
||||
const pinia = setupPermissions([
|
||||
'firstleg:fba-pricing:view', 'firstleg:fba-pricing:calculate', 'firstleg:fba-pricing:adjust',
|
||||
'firstleg:fba-pricing:confirm', 'firstleg:inbound:send-fee',
|
||||
])
|
||||
const wrapper = mount(InboundFeePanel, {
|
||||
props: { customerId: '10001', inboundId: '88', displayOnly: true },
|
||||
global: { plugins: [pinia, ElementPlus] },
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('button[aria-label="刷新费用快照"]').exists()).toBe(true)
|
||||
expect(wrapper.text()).not.toContain('费用试算')
|
||||
expect(wrapper.text()).not.toContain('确认费用')
|
||||
expect(wrapper.text()).not.toContain('确认推送费用')
|
||||
expect(wrapper.find('[data-testid="adjust-fee-button"]').exists()).toBe(false)
|
||||
expect(wrapper.find('[data-testid="confirm-fee-button"]').exists()).toBe(false)
|
||||
expect(wrapper.find('[data-testid="send-fee-button"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the legacy integration prop compatible while the page layer uses customer context', async () => {
|
||||
const pinia = setupPermissions(['firstleg:fba-pricing:view'])
|
||||
mount(InboundFeePanel, {
|
||||
|
||||
@@ -30,6 +30,7 @@ const props = defineProps<{
|
||||
tenantId?: string
|
||||
inboundId: string
|
||||
compact?: boolean
|
||||
displayOnly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -352,20 +353,20 @@ defineExpose({ reload: loadFee })
|
||||
><RefreshCw :size="16" /></el-button>
|
||||
</el-tooltip>
|
||||
<el-button
|
||||
v-if="canCalculate"
|
||||
v-if="!displayOnly && canCalculate"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="trialLoading"
|
||||
@click="openTrial"
|
||||
><CircleDollarSign :size="16" />费用试算</el-button>
|
||||
<el-button
|
||||
v-if="canAdjust && adjustmentEligible"
|
||||
v-if="!displayOnly && canAdjust && adjustmentEligible"
|
||||
data-testid="adjust-fee-button"
|
||||
type="primary"
|
||||
@click="openAdjustment"
|
||||
><PencilLine :size="16" />调整初算费用</el-button>
|
||||
<el-button
|
||||
v-if="canConfirm && snapshot?.feeStatus === 'CALCULATED'"
|
||||
v-if="!displayOnly && canConfirm && snapshot?.feeStatus === 'CALCULATED'"
|
||||
data-testid="confirm-fee-button"
|
||||
type="primary"
|
||||
:loading="feeAction === 'confirm'"
|
||||
@@ -373,14 +374,14 @@ defineExpose({ reload: loadFee })
|
||||
@click="transitionFee('confirm')"
|
||||
><Check :size="16" />确认费用</el-button>
|
||||
<el-button
|
||||
v-if="canConfirm && canSend && snapshot?.feeStatus === 'CALCULATED'"
|
||||
v-if="!displayOnly && canConfirm && canSend && snapshot?.feeStatus === 'CALCULATED'"
|
||||
type="primary"
|
||||
:loading="Boolean(feeAction)"
|
||||
:disabled="Boolean(feeAction)"
|
||||
@click="confirmAndPush"
|
||||
><Send :size="16" />确认推送费用</el-button>
|
||||
<el-button
|
||||
v-if="canSend && snapshot?.feeStatus === 'CONFIRMED'"
|
||||
v-if="!displayOnly && canSend && snapshot?.feeStatus === 'CONFIRMED'"
|
||||
data-testid="send-fee-button"
|
||||
type="primary"
|
||||
:loading="feeAction === 'send'"
|
||||
|
||||
@@ -9,6 +9,8 @@ const orderListSource = readFileSync(resolve(process.cwd(), 'src/modules/unified
|
||||
|
||||
describe('InboundMeasurementDialog', () => {
|
||||
it('owns the complete measurement workspace behind one centered dialog', () => {
|
||||
expect(source).toContain("import AppDialog from '@/shared/components/AppDialog.vue'")
|
||||
expect(source).toContain('<AppDialog')
|
||||
expect(source).toContain('class="inbound-measurement-workspace-dialog"')
|
||||
expect(source).toContain('width="min(1240px, calc(100vw - 32px))"')
|
||||
expect(source).toContain('height: 92vh')
|
||||
@@ -21,6 +23,12 @@ describe('InboundMeasurementDialog', () => {
|
||||
expect(source).toContain('measurementPanelRef.value?.updateActualWeights(actualGrossWeightKg, actualNetWeightKg)')
|
||||
})
|
||||
|
||||
it('offers the shared fullscreen control without forcing fullscreen on open', () => {
|
||||
expect(source).toContain('fullscreenable')
|
||||
expect(source).not.toContain('default-fullscreen')
|
||||
expect(source).not.toContain('<el-dialog')
|
||||
})
|
||||
|
||||
it('shows the inbound code once in the dialog title and not as a second body heading', () => {
|
||||
expect(source).toContain("`测量 · ${props.inboundCode?.trim() || '入仓码待补充'}`")
|
||||
expect(source).not.toContain('props.inboundCode?.trim() || props.inboundId')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import InboundMeasurementPanel from './InboundMeasurementPanel.vue'
|
||||
import WeightOcrPanel from '../weight/components/WeightOcrPanel.vue'
|
||||
|
||||
@@ -59,12 +60,13 @@ function handleSubmitted() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
<AppDialog
|
||||
:model-value="modelValue"
|
||||
class="inbound-measurement-workspace-dialog"
|
||||
:title="title"
|
||||
width="min(1240px, calc(100vw - 32px))"
|
||||
top="4vh"
|
||||
fullscreenable
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
@@ -111,7 +113,7 @@ function handleSubmitted() {
|
||||
>提交审核</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -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"
|
||||
@@ -675,13 +749,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; }
|
||||
@@ -708,6 +813,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