Release/test 合并上生产 #2

Merged
admin merged 736 commits from release/test into main 2026-09-17 11:50:56 +08:00
8 changed files with 381 additions and 12 deletions
Showing only changes of commit 4eda56acf1 - Show all commits
@@ -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)); }
}
+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 {