优化
This commit is contained in:
@@ -68,6 +68,7 @@ const workspace: WeightWorkspaceResource = {
|
||||
inboundCode: 'RK-20001',
|
||||
processStatus: 'MEASURING',
|
||||
actualGrossWeightKg: null,
|
||||
actualNetWeightKg: null,
|
||||
weightResults: [pendingWeight],
|
||||
}
|
||||
|
||||
@@ -79,7 +80,7 @@ const allWeightPermissions = [
|
||||
'platform:firstleg:inbound:weight:confirm',
|
||||
]
|
||||
|
||||
function mountPanel(permissions = allWeightPermissions) {
|
||||
function mountPanel(permissions = allWeightPermissions, props: Record<string, unknown> = {}) {
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
const auth = useAuthStore()
|
||||
@@ -88,7 +89,7 @@ function mountPanel(permissions = allWeightPermissions) {
|
||||
useAccessPreviewStore().initializeAfterLogin()
|
||||
return mount(WeightOcrPanel, {
|
||||
attachTo: document.body,
|
||||
props: { customerId: '10001', inboundId: '20001' },
|
||||
props: { customerId: '10001', inboundId: '20001', ...props },
|
||||
global: { plugins: [pinia, ElementPlus] },
|
||||
})
|
||||
}
|
||||
@@ -109,9 +110,9 @@ function dialogUnit(index: number) {
|
||||
return new DOMWrapper(option)
|
||||
}
|
||||
|
||||
function dialogConfirmButton() {
|
||||
function dialogConfirmButton(text = '确认重量') {
|
||||
const button = [...document.body.querySelectorAll<HTMLButtonElement>('.el-dialog__footer button')]
|
||||
.find((candidate) => candidate.textContent?.trim() === '确认重量')
|
||||
.find((candidate) => candidate.textContent?.trim() === text)
|
||||
if (!button) throw new Error('weight confirmation button is missing')
|
||||
return new DOMWrapper(button)
|
||||
}
|
||||
@@ -242,7 +243,8 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
.mockResolvedValueOnce(workspace)
|
||||
.mockResolvedValueOnce({
|
||||
...workspace,
|
||||
actualGrossWeightKg: 285.5,
|
||||
actualGrossWeightKg: 1787.5,
|
||||
actualNetWeightKg: 1747.5,
|
||||
weightResults: [{ ...pendingWeight, recognitionStatus: 'CONFIRMED', grossWeightKg: 297.5, netWeightKg: 285.5, version: 1 }],
|
||||
})
|
||||
const wrapper = mountPanel()
|
||||
@@ -252,10 +254,53 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
await dialogConfirmButton().trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.emitted('summaryChanged')).toEqual([[285.5]])
|
||||
expect(wrapper.emitted('summaryChanged')).toEqual([[1787.5, 1747.5]])
|
||||
expect(wrapper.emitted('changed')).toHaveLength(1)
|
||||
expect(wrapper.text()).toContain('货物总重量')
|
||||
expect(wrapper.text()).toContain('285.500 KG')
|
||||
expect(wrapper.text()).toContain('实测毛重')
|
||||
expect(wrapper.text()).toContain('货物重量')
|
||||
expect(wrapper.text()).toContain('1787.500 KG')
|
||||
expect(wrapper.text()).toContain('1747.500 KG')
|
||||
})
|
||||
|
||||
it('atomically corrects and confirms an approved weight during measurement review', async () => {
|
||||
const confirmed = {
|
||||
...pendingWeight,
|
||||
recognitionStatus: 'CONFIRMED',
|
||||
adjustedWeightValue: 120,
|
||||
adjustedWeightUnit: 'KG',
|
||||
grossWeightKg: 120,
|
||||
netWeightKg: 108,
|
||||
version: 1,
|
||||
}
|
||||
vi.mocked(getWeightWorkspace).mockResolvedValue({
|
||||
...workspace,
|
||||
processStatus: 'MEASURE_PENDING',
|
||||
actualGrossWeightKg: 120,
|
||||
actualNetWeightKg: 108,
|
||||
weightResults: [confirmed],
|
||||
})
|
||||
vi.mocked(confirmWeightResult).mockResolvedValue({ ...confirmed, grossWeightKg: 125, netWeightKg: 113, version: 2 })
|
||||
const wrapper = mountPanel(allWeightPermissions, { reviewMode: true, correctionMode: true })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('input[type="file"]').exists()).toBe(false)
|
||||
expect(wrapper.text()).not.toContain('删除图片')
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('修改含托盘毛重'))?.trigger('click')
|
||||
await flushPromises()
|
||||
await dialogWeightInput().setValue('125')
|
||||
const reason = document.body.querySelector<HTMLTextAreaElement>('textarea[placeholder="请填写本次毛重修正原因"]')
|
||||
expect(reason).not.toBeNull()
|
||||
await new DOMWrapper(reason!).setValue('复核磅秤读数')
|
||||
await dialogConfirmButton('保存重量修正').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
|
||||
version: 1,
|
||||
adjustedWeightValue: 125,
|
||||
adjustedWeightUnit: 'KG',
|
||||
adjustmentReason: '复核磅秤读数',
|
||||
})
|
||||
expect(correctWeightResult).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('closes the confirmation dialog immediately after a successful confirmation', async () => {
|
||||
|
||||
@@ -6,9 +6,9 @@ const source = readFileSync(resolve(process.cwd(), 'src/modules/inbounds/weight/
|
||||
|
||||
describe('WeightOcrPanel contract', () => {
|
||||
it('uses customer language at the page boundary while retaining a compatibility input', () => {
|
||||
expect(source).toContain("defineProps<{ customerId?: string; tenantId?: string; inboundId: string }>()")
|
||||
expect(source).toContain('reviewMode?: boolean')
|
||||
expect(source).toContain("const customerId = computed(() => props.customerId || props.tenantId || '')")
|
||||
expect(source).toContain('summaryChanged: [actualGrossWeightKg: number | null]')
|
||||
expect(source).toContain('summaryChanged: [actualGrossWeightKg: number | null, actualNetWeightKg: number | null]')
|
||||
expect(source).toContain("defineExpose({ reload: loadWorkspace })")
|
||||
const template = source.match(/<template>([\s\S]*?)<\/template>/)?.[1] || ''
|
||||
expect(template).not.toContain('租户')
|
||||
@@ -29,11 +29,13 @@ describe('WeightOcrPanel contract', () => {
|
||||
expect(source).toContain('OCR 识别毛重')
|
||||
expect(source).toContain('确认含托盘毛重')
|
||||
expect(source).toContain('payload.adjustedWeightValue = Number(draft.value)')
|
||||
expect(source).toContain('payload.adjustmentReason = buildAdjustmentReason(row)')
|
||||
expect(source).toContain('货物总重量')
|
||||
expect(source).toContain("row.recognitionStatus === 'CONFIRMED' && reviewCorrectionEnabled.value")
|
||||
expect(source).toContain('draft.reason.trim()')
|
||||
expect(source).toContain('实测毛重')
|
||||
expect(source).toContain('货物重量')
|
||||
expect(source).toContain('palletTareWeightKg')
|
||||
expect(source).toContain('netWeightKg')
|
||||
expect(source).toContain("emit('summaryChanged', workspace.value?.actualGrossWeightKg ?? null)")
|
||||
expect(source).toContain("emit('summaryChanged', workspace.value?.actualGrossWeightKg ?? null, workspace.value?.actualNetWeightKg ?? null)")
|
||||
})
|
||||
|
||||
it('matches backend image limits and keeps technical OCR data out of the list', () => {
|
||||
@@ -65,6 +67,9 @@ describe('WeightOcrPanel contract', () => {
|
||||
expect(source).toContain("platform:firstleg:inbound:weight:edit")
|
||||
expect(source).toContain("platform:firstleg:inbound:weight:confirm")
|
||||
expect(source).toContain("new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'])")
|
||||
expect(source).toContain("workspace.value?.processStatus === 'MEASURE_PENDING'")
|
||||
expect(source).toContain('v-if="canEdit && !reviewMode"')
|
||||
expect(source).toContain('v-if="canEdit && editable && !reviewMode"')
|
||||
})
|
||||
|
||||
it('provides error tracking, empty, loading, and narrow-screen states', () => {
|
||||
|
||||
@@ -26,10 +26,19 @@ import type { WeightPalletOption, WeightResultResource, WeightWorkspaceResource
|
||||
import WeightImageThumbnail from './WeightImageThumbnail.vue'
|
||||
import { inboundProcessStatusLabels, inboundStatusLabel } from '../../status'
|
||||
|
||||
const props = defineProps<{ customerId?: string; tenantId?: string; inboundId: string }>()
|
||||
const props = withDefaults(defineProps<{
|
||||
customerId?: string
|
||||
tenantId?: string
|
||||
inboundId: string
|
||||
reviewMode?: boolean
|
||||
correctionMode?: boolean
|
||||
}>(), {
|
||||
reviewMode: false,
|
||||
correctionMode: false,
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
changed: []
|
||||
summaryChanged: [actualGrossWeightKg: number | null]
|
||||
summaryChanged: [actualGrossWeightKg: number | null, actualNetWeightKg: number | null]
|
||||
}>()
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
@@ -49,7 +58,7 @@ const previewError = ref<ApiErrorDetails | null>(null)
|
||||
const previewUrl = ref('')
|
||||
const previewResultId = ref('')
|
||||
const previewSnapshot = ref<WeightResultResource | null>(null)
|
||||
const weightDrafts = ref<Record<string, { value: number | null; unit: 'KG' | 'LB' }>>({})
|
||||
const weightDrafts = ref<Record<string, { value: number | null; unit: 'KG' | 'LB'; reason: string }>>({})
|
||||
let loadSequence = 0
|
||||
|
||||
const editableStatuses = new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'])
|
||||
@@ -60,6 +69,11 @@ const canEdit = computed(() => access.can('platform:firstleg:inbound:weight:edit
|
||||
const canConfirm = computed(() => access.can('platform:firstleg:inbound:weight:confirm'))
|
||||
const canView = computed(() => access.can('platform:firstleg:inbound:measure:view'))
|
||||
const editable = computed(() => Boolean(workspace.value && editableStatuses.has(workspace.value.processStatus)))
|
||||
const reviewCorrectionEnabled = computed(() => Boolean(
|
||||
props.reviewMode
|
||||
&& props.correctionMode
|
||||
&& workspace.value?.processStatus === 'MEASURE_PENDING',
|
||||
))
|
||||
const rows = computed(() => workspace.value?.weightResults ?? [])
|
||||
const previewResult = computed(() => rows.value.find((row) => row.id === previewResultId.value)
|
||||
|| previewSnapshot.value)
|
||||
@@ -67,8 +81,17 @@ const previewConfirmDisabled = computed(() => {
|
||||
const row = previewResult.value
|
||||
return !row
|
||||
|| !isWeightDraftValid(row)
|
||||
|| (row.recognitionStatus === 'CONFIRMED' && reviewCorrectionEnabled.value
|
||||
&& (!hasWeightDraftChanged(row) || !weightDrafts.value[row.id]?.reason.trim()))
|
||||
|| Boolean(activeAction.value)
|
||||
})
|
||||
const previewCanConfirm = computed(() => Boolean(previewResult.value && canConfirm.value
|
||||
&& (editable.value || (reviewCorrectionEnabled.value && canEdit.value))))
|
||||
const previewInputEditable = computed(() => Boolean(previewResult.value && canEdit.value && canConfirm.value
|
||||
&& (editable.value || reviewCorrectionEnabled.value)))
|
||||
const previewConfirmText = computed(() => previewResult.value?.recognitionStatus === 'CONFIRMED'
|
||||
? '保存重量修正'
|
||||
: '确认重量')
|
||||
|
||||
function formatWeight(value: number | null, unit = 'KG') {
|
||||
return value == null ? '—' : `${Number(value).toFixed(3)} ${unit}`
|
||||
@@ -98,6 +121,7 @@ function syncWeightDrafts(results: WeightResultResource[]) {
|
||||
weightDrafts.value = Object.fromEntries(results.map((row) => [row.id, {
|
||||
value: row.adjustedWeightValue ?? row.ocrWeightValue,
|
||||
unit: normalizeUnit(row.adjustedWeightUnit ?? row.ocrWeightUnit),
|
||||
reason: '',
|
||||
}]))
|
||||
}
|
||||
|
||||
@@ -105,6 +129,7 @@ function ensureWeightDraft(row: WeightResultResource) {
|
||||
weightDrafts.value[row.id] ??= {
|
||||
value: row.adjustedWeightValue ?? row.ocrWeightValue,
|
||||
unit: normalizeUnit(row.adjustedWeightUnit ?? row.ocrWeightUnit),
|
||||
reason: '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +185,7 @@ function buildAdjustmentReason(row: WeightResultResource) {
|
||||
}
|
||||
|
||||
function emitWorkspaceChanged() {
|
||||
emit('summaryChanged', workspace.value?.actualGrossWeightKg ?? null)
|
||||
emit('summaryChanged', workspace.value?.actualGrossWeightKg ?? null, workspace.value?.actualNetWeightKg ?? null)
|
||||
emit('changed')
|
||||
}
|
||||
|
||||
@@ -188,6 +213,12 @@ async function loadWorkspace() {
|
||||
}
|
||||
|
||||
async function loadPalletOptions() {
|
||||
if (props.reviewMode) {
|
||||
palletOptions.value = []
|
||||
selectedPalletId.value = ''
|
||||
palletError.value = null
|
||||
return
|
||||
}
|
||||
if (!customerId.value || !props.inboundId || !canView.value) {
|
||||
palletOptions.value = []
|
||||
selectedPalletId.value = ''
|
||||
@@ -282,11 +313,13 @@ async function confirmResult(row: WeightResultResource, closeAfterSuccess = fals
|
||||
}
|
||||
payload.adjustedWeightValue = Number(draft.value)
|
||||
payload.adjustedWeightUnit = draft.unit
|
||||
payload.adjustmentReason = buildAdjustmentReason(row)
|
||||
payload.adjustmentReason = row.recognitionStatus === 'CONFIRMED' && reviewCorrectionEnabled.value
|
||||
? draft.reason.trim()
|
||||
: buildAdjustmentReason(row)
|
||||
}
|
||||
await confirmWeightResult(customerId.value, props.inboundId, row.id, payload)
|
||||
if (closeAfterSuccess) previewOpen.value = false
|
||||
ElMessage.success('重量已确认')
|
||||
ElMessage.success(row.recognitionStatus === 'CONFIRMED' ? '含托盘毛重已修正,货物重量和合计已由服务端重算' : '重量已确认')
|
||||
await loadWorkspace()
|
||||
emitWorkspaceChanged()
|
||||
return true
|
||||
@@ -387,12 +420,13 @@ defineExpose({ reload: loadWorkspace })
|
||||
<h3 id="weight-ocr-title"><Scale :size="18" aria-hidden="true" />称重图片</h3>
|
||||
</div>
|
||||
<div class="weight-heading__actions">
|
||||
<div v-if="workspace" class="weight-summary" aria-live="polite">
|
||||
<span>货物总重量</span>
|
||||
<strong>{{ formatWeight(workspace.actualGrossWeightKg) }}</strong>
|
||||
<div v-if="workspace" class="weight-summary-group" aria-live="polite">
|
||||
<div class="weight-summary"><span>实测毛重</span><strong>{{ formatWeight(workspace.actualGrossWeightKg) }}</strong></div>
|
||||
<div class="weight-summary"><span>货物重量</span><strong>{{ formatWeight(workspace.actualNetWeightKg) }}</strong></div>
|
||||
</div>
|
||||
<div class="pallet-picker">
|
||||
<el-select
|
||||
v-if="!reviewMode"
|
||||
v-model="selectedPalletId"
|
||||
aria-label="选择称重托盘"
|
||||
placeholder="选择称重托盘"
|
||||
@@ -409,6 +443,7 @@ defineExpose({ reload: loadWorkspace })
|
||||
</el-select>
|
||||
</div>
|
||||
<input
|
||||
v-if="!reviewMode"
|
||||
ref="fileInput"
|
||||
class="visually-hidden-input"
|
||||
type="file"
|
||||
@@ -417,7 +452,7 @@ defineExpose({ reload: loadWorkspace })
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
<el-button
|
||||
v-if="canEdit"
|
||||
v-if="canEdit && !reviewMode"
|
||||
type="primary"
|
||||
:loading="uploading"
|
||||
:disabled="loading || palletLoading || !editable || !selectedPalletId || Boolean(palletError)"
|
||||
@@ -464,7 +499,7 @@ defineExpose({ reload: loadWorkspace })
|
||||
>
|
||||
<el-button size="small" @click="loadPalletOptions"><RefreshCw :size="14" />重新加载</el-button>
|
||||
</el-alert>
|
||||
<el-alert v-if="workspace && !editable" type="info" :closable="false" show-icon class="weight-state-note">
|
||||
<el-alert v-if="workspace && !editable && !reviewCorrectionEnabled" type="info" :closable="false" show-icon class="weight-state-note">
|
||||
当前入库状态为 {{ inboundStatusLabel(workspace.processStatus, inboundProcessStatusLabels, '未知状态') }},称重数据仅供查看。
|
||||
</el-alert>
|
||||
|
||||
@@ -482,7 +517,7 @@ defineExpose({ reload: loadWorkspace })
|
||||
:alt="`称重图片 ${index + 1}`"
|
||||
@open="openPreview(row)"
|
||||
/>
|
||||
<el-tooltip v-if="canEdit && editable" content="删除图片" placement="top">
|
||||
<el-tooltip v-if="canEdit && editable && !reviewMode" content="删除图片" placement="top">
|
||||
<el-button
|
||||
plain
|
||||
type="danger"
|
||||
@@ -506,6 +541,13 @@ defineExpose({ reload: loadWorkspace })
|
||||
<div><dt>托盘皮重</dt><dd>{{ formatWeight(row.palletTareWeightKg) }}</dd></div>
|
||||
<div><dt>货物重量</dt><dd>{{ formatWeight(row.netWeightKg) }}</dd></div>
|
||||
</dl>
|
||||
<el-button
|
||||
v-if="reviewCorrectionEnabled && row.recognitionStatus === 'CONFIRMED' && canEdit && canConfirm"
|
||||
class="weight-card__correct"
|
||||
type="primary"
|
||||
plain
|
||||
@click="openPreview(row)"
|
||||
>修改含托盘毛重</el-button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
@@ -519,8 +561,8 @@ defineExpose({ reload: loadWorkspace })
|
||||
:loading="previewLoading"
|
||||
:confirm-loading="Boolean(previewResult && isActionLoading('confirm', previewResult.id))"
|
||||
:confirm-disabled="previewConfirmDisabled"
|
||||
:show-confirm="Boolean(previewResult && previewResult.recognitionStatus !== 'CONFIRMED' && canConfirm && editable)"
|
||||
confirm-text="确认重量"
|
||||
:show-confirm="Boolean(previewResult && previewCanConfirm && (previewResult.recognitionStatus !== 'CONFIRMED' || reviewCorrectionEnabled))"
|
||||
:confirm-text="previewConfirmText"
|
||||
confirm-type="success"
|
||||
width="min(640px, calc(100vw - 28px))"
|
||||
@confirm="previewResult && confirmResult(previewResult, true)"
|
||||
@@ -551,16 +593,20 @@ defineExpose({ reload: loadWorkspace })
|
||||
:max="maximumSafeWeight"
|
||||
:precision="3"
|
||||
:controls="false"
|
||||
:disabled="previewResult.recognitionStatus === 'CONFIRMED' || Boolean(activeAction) || !editable || !canEdit || !canConfirm"
|
||||
:disabled="Boolean(activeAction) || !previewInputEditable"
|
||||
/>
|
||||
</label>
|
||||
<el-segmented
|
||||
v-model="weightDrafts[previewResult.id].unit"
|
||||
aria-label="当前图片重量单位"
|
||||
:options="['KG', 'LB']"
|
||||
:disabled="previewResult.recognitionStatus === 'CONFIRMED' || Boolean(activeAction) || !editable || !canEdit || !canConfirm"
|
||||
:disabled="Boolean(activeAction) || !previewInputEditable"
|
||||
/>
|
||||
</div>
|
||||
<label v-if="reviewCorrectionEnabled && previewResult.recognitionStatus === 'CONFIRMED' && weightDrafts[previewResult.id]" class="correction-reason">
|
||||
<span>修改原因 *</span>
|
||||
<el-input v-model="weightDrafts[previewResult.id].reason" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请填写本次毛重修正原因" />
|
||||
</label>
|
||||
</template>
|
||||
</AppDialog>
|
||||
</section>
|
||||
@@ -575,6 +621,7 @@ defineExpose({ reload: loadWorkspace })
|
||||
.pallet-picker { width: clamp(360px, 36vw, 520px); min-width: 0; }
|
||||
.pallet-picker :deep(.el-select) { width: 100%; }
|
||||
.weight-summary { display: grid; gap: 2px; text-align: right; }
|
||||
.weight-summary-group { display: flex; align-items: center; gap: 18px; }
|
||||
.weight-summary span { color: var(--cc-muted); font-size: 12px; }
|
||||
.weight-summary strong { color: #067647; font-size: 18px; }
|
||||
.visually-hidden-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
||||
@@ -593,6 +640,7 @@ defineExpose({ reload: loadWorkspace })
|
||||
.weight-card__facts div { display: flex; justify-content: space-between; gap: 8px; font-size: 11px; line-height: 1.4; }
|
||||
.weight-card__facts dt { color: var(--cc-muted); }
|
||||
.weight-card__facts dd { min-width: 0; margin: 0; color: #344054; overflow-wrap: anywhere; text-align: right; }
|
||||
.weight-card__correct { width: 100%; margin: 12px 0 0; }
|
||||
.weight-preview { display: block; width: 100%; max-height: min(50vh, 460px); object-fit: contain; border-radius: 6px; background: #f2f4f7; }
|
||||
.weight-key-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; margin-top: 14px; padding: 14px 0; border-block: 1px solid var(--cc-border); }
|
||||
.weight-key-facts div { display: grid; min-width: 0; gap: 4px; }
|
||||
@@ -601,6 +649,7 @@ defineExpose({ reload: loadWorkspace })
|
||||
.preview-weight { display: grid; grid-template-columns: minmax(0, 1fr) 120px; align-items: end; gap: 12px; margin-top: 14px; }
|
||||
.preview-weight label { display: grid; gap: 6px; color: #475467; font-size: 12px; }
|
||||
.preview-weight :deep(.el-input-number), .preview-weight :deep(.el-segmented) { width: 100%; }
|
||||
.correction-reason { display: grid; gap: 6px; margin-top: 14px; color: #475467; font-size: 12px; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.weight-heading { flex-direction: column; }
|
||||
@@ -609,6 +658,7 @@ defineExpose({ reload: loadWorkspace })
|
||||
.pallet-picker :deep(.el-select) { width: 100%; }
|
||||
.weight-heading__actions :deep(.el-button) { width: 100%; min-height: 44px; }
|
||||
.weight-summary { text-align: left; }
|
||||
.weight-summary-group { width: 100%; justify-content: space-between; }
|
||||
.weight-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user