优化
This commit is contained in:
@@ -52,16 +52,18 @@ const workspace: WeightWorkspaceResource = {
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
function mountPanel() {
|
||||
const allWeightPermissions = [
|
||||
'platform:firstleg:inbound:measure:view',
|
||||
'platform:firstleg:inbound:weight:edit',
|
||||
'platform:firstleg:inbound:weight:confirm',
|
||||
]
|
||||
|
||||
function mountPanel(permissions = allWeightPermissions) {
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
const auth = useAuthStore()
|
||||
auth.token = 'token'
|
||||
auth.permissions = [
|
||||
'platform:firstleg:inbound:measure:view',
|
||||
'platform:firstleg:inbound:weight:edit',
|
||||
'platform:firstleg:inbound:weight:confirm',
|
||||
]
|
||||
auth.permissions = permissions
|
||||
useAccessPreviewStore().initializeAfterLogin()
|
||||
return mount(WeightOcrPanel, {
|
||||
attachTo: document.body,
|
||||
@@ -112,4 +114,226 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
})
|
||||
expect(correctWeightResult).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sends an LB adjustment atomically after switching units', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.get('input[aria-label="称重图片序号 1 的确认重量"]').setValue('10.125')
|
||||
await wrapper.findAll('.el-segmented__item').at(1)?.trigger('click')
|
||||
await wrapper.get('.weight-row__editor .el-button--success').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
|
||||
version: 0,
|
||||
adjustedWeightValue: 10.125,
|
||||
adjustedWeightUnit: 'LB',
|
||||
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 10.125 LB',
|
||||
})
|
||||
expect(correctWeightResult).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects an LB value that would round to zero kilograms', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.get('input[aria-label="称重图片序号 1 的确认重量"]').setValue('0.001')
|
||||
await wrapper.findAll('.el-segmented__item').at(1)?.trigger('click')
|
||||
|
||||
expect(wrapper.get('.weight-row__editor .el-button--success').attributes('disabled')).toBeDefined()
|
||||
expect(confirmWeightResult).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('accepts the smallest LB value that remains representable in kilograms', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.get('input[aria-label="称重图片序号 1 的确认重量"]').setValue('0.002')
|
||||
await wrapper.findAll('.el-segmented__item').at(1)?.trigger('click')
|
||||
await wrapper.get('.weight-row__editor .el-button--success').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
|
||||
version: 0,
|
||||
adjustedWeightValue: 0.002,
|
||||
adjustedWeightUnit: 'LB',
|
||||
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 0.002 LB',
|
||||
})
|
||||
})
|
||||
|
||||
it('clamps a value outside the safe three-decimal JavaScript range', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
const input = wrapper.get('input[aria-label="称重图片序号 1 的确认重量"]')
|
||||
|
||||
await input.setValue('9000000000001')
|
||||
expect((input.element as HTMLInputElement).value).toBe('9000000000000.000')
|
||||
await wrapper.get('.weight-row__editor .el-button--success').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
|
||||
version: 0,
|
||||
adjustedWeightValue: 9_000_000_000_000,
|
||||
adjustedWeightUnit: 'KG',
|
||||
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 9000000000000.000 KG',
|
||||
})
|
||||
})
|
||||
|
||||
it('rounds an input with more than three decimals before confirmation', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
const input = wrapper.get('input[aria-label="称重图片序号 1 的确认重量"]')
|
||||
|
||||
await input.setValue('1.2345')
|
||||
expect((input.element as HTMLInputElement).value).toBe('1.235')
|
||||
await wrapper.get('.weight-row__editor .el-button--success').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
|
||||
version: 0,
|
||||
adjustedWeightValue: 1.235,
|
||||
adjustedWeightUnit: 'KG',
|
||||
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 1.235 KG',
|
||||
})
|
||||
})
|
||||
|
||||
it('confirms an existing corrected value without creating another correction', async () => {
|
||||
vi.mocked(getWeightWorkspace).mockResolvedValue({
|
||||
...workspace,
|
||||
weightResults: [{
|
||||
...pendingWeight,
|
||||
adjustedWeightValue: 286.125,
|
||||
adjustedWeightUnit: 'KG',
|
||||
adjustmentReason: '首次修正',
|
||||
version: 1,
|
||||
}],
|
||||
})
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.get('.weight-row__editor .el-button--success').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', { version: 1 })
|
||||
expect(correctWeightResult).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('allows a confirm-only operator to accept an unchanged OCR value', async () => {
|
||||
const wrapper = mountPanel([
|
||||
'platform:firstleg:inbound:measure:view',
|
||||
'platform:firstleg:inbound:weight:confirm',
|
||||
])
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.get('input[aria-label="称重图片序号 1 的确认重量"]').attributes('disabled')).toBeDefined()
|
||||
await wrapper.get('.weight-row__editor .el-button--success').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', { version: 0 })
|
||||
})
|
||||
|
||||
it('does not render confirmation for an edit-only operator', async () => {
|
||||
const wrapper = mountPanel([
|
||||
'platform:firstleg:inbound:measure:view',
|
||||
'platform:firstleg:inbound:weight:edit',
|
||||
])
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('.weight-row__editor .el-button--success').exists()).toBe(false)
|
||||
expect(confirmWeightResult).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('supports manual entry when OCR did not return a value', async () => {
|
||||
vi.mocked(getWeightWorkspace).mockResolvedValue({
|
||||
...workspace,
|
||||
weightResults: [{
|
||||
...pendingWeight,
|
||||
ocrRawText: null,
|
||||
ocrWeightValue: null,
|
||||
ocrWeightUnit: null,
|
||||
recognitionStatus: 'NEEDS_MANUAL',
|
||||
}],
|
||||
})
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.get('.weight-row__editor .el-button--success').attributes('disabled')).toBeDefined()
|
||||
await wrapper.get('input[aria-label="称重图片序号 1 的确认重量"]').setValue('1.500')
|
||||
await wrapper.get('.weight-row__editor .el-button--success').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
|
||||
version: 0,
|
||||
adjustedWeightValue: 1.5,
|
||||
adjustedWeightUnit: 'KG',
|
||||
adjustmentReason: 'OCR 原值 —,人工确认 1.500 KG',
|
||||
})
|
||||
})
|
||||
|
||||
it('normalizes a supported OCR unit alias with surrounding whitespace', async () => {
|
||||
vi.mocked(getWeightWorkspace).mockResolvedValue({
|
||||
...workspace,
|
||||
weightResults: [{ ...pendingWeight, ocrWeightUnit: ' lbs ' }],
|
||||
})
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.get('.weight-row__editor .el-button--success').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', { version: 0 })
|
||||
})
|
||||
|
||||
it('sends an explicit correction when OCR returns an unsupported unit', async () => {
|
||||
vi.mocked(getWeightWorkspace).mockResolvedValue({
|
||||
...workspace,
|
||||
weightResults: [{ ...pendingWeight, ocrWeightUnit: 'G' }],
|
||||
})
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.get('.weight-row__editor .el-button--success').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
|
||||
version: 0,
|
||||
adjustedWeightValue: 285.5,
|
||||
adjustedWeightUnit: 'KG',
|
||||
adjustmentReason: 'OCR 原值 285.500 G,人工确认 285.500 KG',
|
||||
})
|
||||
})
|
||||
|
||||
it('sends an explicit correction when OCR returns a value without a unit', async () => {
|
||||
vi.mocked(getWeightWorkspace).mockResolvedValue({
|
||||
...workspace,
|
||||
weightResults: [{ ...pendingWeight, ocrWeightUnit: null }],
|
||||
})
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.get('.weight-row__editor .el-button--success').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
|
||||
version: 0,
|
||||
adjustedWeightValue: 285.5,
|
||||
adjustedWeightUnit: 'KG',
|
||||
adjustmentReason: 'OCR 原值 285.500(单位缺失),人工确认 285.500 KG',
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the edited draft available when confirmation fails', async () => {
|
||||
vi.mocked(confirmWeightResult).mockRejectedValueOnce(new Error('version conflict'))
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
const input = wrapper.get('input[aria-label="称重图片序号 1 的确认重量"]')
|
||||
|
||||
await input.setValue('286.125')
|
||||
await wrapper.get('.weight-row__editor .el-button--success').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect((input.element as HTMLInputElement).value).toBe('286.125')
|
||||
expect(wrapper.text()).toContain('version conflict')
|
||||
expect(confirmWeightResult).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -45,6 +45,8 @@ const weightDrafts = ref<Record<string, { value: number | null; unit: 'KG' | 'LB
|
||||
let loadSequence = 0
|
||||
|
||||
const editableStatuses = new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'])
|
||||
const minimumWeight = { KG: 0.001, LB: 0.002 } as const
|
||||
const maximumSafeWeight = 9_000_000_000_000
|
||||
const customerId = computed(() => props.customerId || props.tenantId || '')
|
||||
const canEdit = computed(() => access.can('platform:firstleg:inbound:weight:edit'))
|
||||
const canConfirm = computed(() => access.can('platform:firstleg:inbound:weight:confirm'))
|
||||
@@ -83,7 +85,11 @@ function isActionLoading(action: string, id: string) {
|
||||
}
|
||||
|
||||
function normalizeUnit(value: string | null) {
|
||||
return value?.toUpperCase().startsWith('LB') ? 'LB' as const : 'KG' as const
|
||||
return value?.trim().toUpperCase().startsWith('LB') ? 'LB' as const : 'KG' as const
|
||||
}
|
||||
|
||||
function isSupportedUnit(value: string | null) {
|
||||
return ['KG', 'KGS', 'LB', 'LBS'].includes(value?.trim().toUpperCase() || '')
|
||||
}
|
||||
|
||||
function syncWeightDrafts(results: WeightResultResource[]) {
|
||||
@@ -95,21 +101,30 @@ function syncWeightDrafts(results: WeightResultResource[]) {
|
||||
|
||||
function isWeightDraftValid(row: WeightResultResource) {
|
||||
const draft = weightDrafts.value[row.id]
|
||||
return Boolean(draft && Number.isFinite(Number(draft.value)) && Number(draft.value) > 0)
|
||||
const value = Number(draft?.value)
|
||||
return Boolean(draft
|
||||
&& Number.isFinite(value)
|
||||
&& value >= minimumWeight[draft.unit]
|
||||
&& value <= maximumSafeWeight)
|
||||
}
|
||||
|
||||
function hasWeightDraftChanged(row: WeightResultResource) {
|
||||
const draft = weightDrafts.value[row.id]
|
||||
if (!draft) return false
|
||||
const savedValue = row.adjustedWeightValue ?? row.ocrWeightValue
|
||||
const savedUnit = normalizeUnit(row.adjustedWeightUnit ?? row.ocrWeightUnit)
|
||||
const rawSavedUnit = row.adjustedWeightUnit ?? row.ocrWeightUnit
|
||||
const savedUnit = normalizeUnit(rawSavedUnit)
|
||||
if (!isSupportedUnit(rawSavedUnit)) return true
|
||||
return Number(draft.value) !== Number(savedValue) || draft.unit !== savedUnit
|
||||
}
|
||||
|
||||
function buildAdjustmentReason(row: WeightResultResource) {
|
||||
const draft = weightDrafts.value[row.id]
|
||||
const ocrUnit = normalizeUnit(row.ocrWeightUnit)
|
||||
return `OCR 原值 ${formatWeight(row.ocrWeightValue, ocrUnit)},人工确认 ${formatWeight(Number(draft.value), draft.unit)}`
|
||||
const ocrUnit = row.ocrWeightUnit?.trim().toUpperCase()
|
||||
const ocrValue = row.ocrWeightValue == null
|
||||
? '—'
|
||||
: `${Number(row.ocrWeightValue).toFixed(3)}${ocrUnit ? ` ${ocrUnit}` : '(单位缺失)'}`
|
||||
return `OCR 原值 ${ocrValue},人工确认 ${formatWeight(Number(draft.value), draft.unit)}`
|
||||
}
|
||||
|
||||
async function loadWorkspace() {
|
||||
@@ -390,8 +405,8 @@ defineExpose({ reload: loadWorkspace })
|
||||
<el-input-number
|
||||
v-model="weightDrafts[row.id].value"
|
||||
:aria-label="`称重图片序号 ${index + 1} 的确认重量`"
|
||||
:min="0.001"
|
||||
:max="999999999999999.999"
|
||||
:min="minimumWeight[weightDrafts[row.id].unit]"
|
||||
:max="maximumSafeWeight"
|
||||
:precision="3"
|
||||
:controls="false"
|
||||
:disabled="Boolean(activeAction) || !editable || !canEdit || !canConfirm"
|
||||
|
||||
Reference in New Issue
Block a user