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:
2026-09-04 12:35:34 +08:00
24 changed files with 649 additions and 156 deletions
@@ -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, {
+6 -5
View File
@@ -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)); }
}
+31
View File
@@ -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 {