优化
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const configSource = readFileSync(resolve(process.cwd(), 'vite.config.ts'), 'utf8')
|
||||
|
||||
describe('Vite development server', () => {
|
||||
it('fails fast when the dedicated admin development port is occupied', () => {
|
||||
expect(configSource).toMatch(/server:\s*\{\s*strictPort: true,/)
|
||||
})
|
||||
})
|
||||
@@ -6,17 +6,20 @@ import { ApiResponseError } from '@/shared/api/http'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import {
|
||||
downloadInboundMeasurementImage,
|
||||
getInboundMeasurement,
|
||||
saveInboundMeasurementDraft,
|
||||
submitInboundMeasurement,
|
||||
updateConfirmedPackageCount,
|
||||
} from './api'
|
||||
import type { InboundMeasurementDetail } from './types'
|
||||
import InboundMeasurementPanel from './InboundMeasurementPanel.vue'
|
||||
|
||||
vi.mock('./api', () => ({
|
||||
downloadInboundMeasurementImage: vi.fn(),
|
||||
getInboundMeasurement: vi.fn(),
|
||||
saveInboundMeasurementDraft: vi.fn(),
|
||||
submitInboundMeasurement: vi.fn(),
|
||||
updateConfirmedPackageCount: vi.fn(),
|
||||
}))
|
||||
|
||||
class ResizeObserverStub implements ResizeObserver {
|
||||
@@ -122,7 +125,10 @@ describe('InboundMeasurementPanel', () => {
|
||||
vi.stubGlobal('sessionStorage', createMemoryStorage())
|
||||
document.body.innerHTML = ''
|
||||
vi.mocked(getInboundMeasurement).mockReset().mockResolvedValue(measurement())
|
||||
vi.mocked(saveInboundMeasurementDraft).mockReset().mockResolvedValue({ measurementId: '9001', version: 3, status: 'DRAFT' })
|
||||
vi.mocked(updateConfirmedPackageCount).mockReset().mockResolvedValue({
|
||||
measurementId: '9001', measurementVersion: 3, itemId: '9101',
|
||||
boxCode: 'RK202608040001#10-1', confirmedPackageCount: 10,
|
||||
})
|
||||
vi.mocked(submitInboundMeasurement).mockReset().mockResolvedValue({
|
||||
measurementId: '9001', version: 2, processStatus: 'MEASURE_PENDING', measureReviewStatus: 'PENDING',
|
||||
recommendedPalletType: 'STANDARD', feeId: null, feeNo: null, feeStatus: null,
|
||||
@@ -139,7 +145,10 @@ describe('InboundMeasurementPanel', () => {
|
||||
expect(wrapper.text()).toContain('箱规周长')
|
||||
expect(wrapper.text()).toContain('190 cm')
|
||||
expect(wrapper.text()).toContain('0.6 CBM')
|
||||
expect((wrapper.find('input[placeholder="请输入本箱规的补充说明"]').element as HTMLInputElement).value).toBe('设备自动回传')
|
||||
expect(wrapper.text()).toContain('MJ20210327')
|
||||
const measuredAtInput = wrapper.findAll('label').find((label) => label.text().includes('测量时间'))?.find('input')
|
||||
expect((measuredAtInput?.element as HTMLInputElement).value).toBe('2026-08-04 10:30:00+08:00')
|
||||
expect(wrapper.text()).toContain('预览图片')
|
||||
})
|
||||
|
||||
it('follows the prototype grouping, labels, and editable field order', async () => {
|
||||
@@ -158,11 +167,11 @@ describe('InboundMeasurementPanel', () => {
|
||||
const labels = wrapper.findAll('.measurement-item__grid label > span').map((label) => label.text())
|
||||
expect(labels.slice(0, 8)).toEqual([
|
||||
'箱号',
|
||||
'测量长(cm)*',
|
||||
'测量宽(cm)*',
|
||||
'测量高(cm)*',
|
||||
'测量长(cm)',
|
||||
'测量宽(cm)',
|
||||
'测量高(cm)',
|
||||
'确认箱数 *',
|
||||
'每箱毛重(kg)*',
|
||||
'每箱毛重(kg)',
|
||||
'机器编号',
|
||||
'测量时间',
|
||||
])
|
||||
@@ -176,46 +185,45 @@ describe('InboundMeasurementPanel', () => {
|
||||
expect(wrapper.find('.status-badge').text()).toBe('测量中')
|
||||
})
|
||||
|
||||
it('saves only editable raw fields with the current measurement version', async () => {
|
||||
it('saves only the changed confirmed package count with the current measurement version', async () => {
|
||||
vi.mocked(updateConfirmedPackageCount).mockResolvedValueOnce({
|
||||
measurementId: '9001', measurementVersion: 3, itemId: '9101',
|
||||
boxCode: 'RK202608040001#10-1', confirmedPackageCount: 9,
|
||||
})
|
||||
const wrapper = await mountPanel()
|
||||
await wrapper.find('input[placeholder="请输入实际货物名称"]').setValue('智能音箱')
|
||||
const save = wrapper.findAll('button').find((button) => button.text().includes('保存草稿'))
|
||||
const countInput = wrapper.findAll('label').find((label) => label.text().includes('确认箱数'))?.find('input')
|
||||
await countInput?.setValue('9')
|
||||
const save = wrapper.findAll('button').find((button) => button.text().includes('保存确认箱数'))
|
||||
await save?.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(saveInboundMeasurementDraft).toHaveBeenCalledOnce()
|
||||
expect(saveInboundMeasurementDraft).toHaveBeenCalledWith('10001', '7001', expect.objectContaining({
|
||||
measurementId: '9001',
|
||||
version: 2,
|
||||
actualGoodsName: '智能音箱',
|
||||
items: [expect.objectContaining({
|
||||
itemNo: '001', packageCount: 10, lengthCm: 50, intelligentImageFileId: '88001',
|
||||
})],
|
||||
}))
|
||||
const payload = vi.mocked(saveInboundMeasurementDraft).mock.calls[0][2]
|
||||
expect(payload.items[0]).not.toHaveProperty('totalVolumeCbm')
|
||||
expect(updateConfirmedPackageCount).toHaveBeenCalledWith('10001', '7001', '9101', {
|
||||
measurementVersion: 2,
|
||||
confirmedPackageCount: 9,
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves the edited form when the backend reports a version conflict', async () => {
|
||||
vi.mocked(saveInboundMeasurementDraft).mockRejectedValueOnce(
|
||||
vi.mocked(updateConfirmedPackageCount).mockRejectedValueOnce(
|
||||
new ApiResponseError('数据版本已变化,请刷新后重试', 'A0409', 200, null, 'req-conflict', 'trace-conflict'),
|
||||
)
|
||||
const wrapper = await mountPanel()
|
||||
const goodsInput = wrapper.find('input[placeholder="请输入实际货物名称"]')
|
||||
await goodsInput.setValue('保留我的修改')
|
||||
const save = wrapper.findAll('button').find((button) => button.text().includes('保存草稿'))
|
||||
const countInput = wrapper.findAll('label').find((label) => label.text().includes('确认箱数'))?.find('input')
|
||||
await countInput?.setValue('9')
|
||||
const save = wrapper.findAll('button').find((button) => button.text().includes('保存确认箱数'))
|
||||
await save?.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('数据版本已变化,当前修改尚未丢失')
|
||||
expect(wrapper.text()).toContain('req-conflict')
|
||||
expect((goodsInput.element as HTMLInputElement).value).toBe('保留我的修改')
|
||||
expect((countInput?.element as HTMLInputElement).value).toBe('9')
|
||||
expect(getInboundMeasurement).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('requires the dirty draft to be saved before submitting for review', async () => {
|
||||
const wrapper = await mountPanel()
|
||||
await wrapper.find('input[placeholder="请输入实际货物名称"]').setValue('尚未保存')
|
||||
const countInput = wrapper.findAll('label').find((label) => label.text().includes('确认箱数'))?.find('input')
|
||||
await countInput?.setValue('9')
|
||||
const submit = wrapper.findAll('button').find((button) => button.text().includes('提交审核'))
|
||||
await submit?.trigger('click')
|
||||
await flushPromises()
|
||||
@@ -250,6 +258,17 @@ describe('InboundMeasurementPanel', () => {
|
||||
expect(submitInboundMeasurement).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps device facts read-only but allows manual count correction after submission', async () => {
|
||||
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ processStatus: 'MEASURE_PENDING' }))
|
||||
const wrapper = await mountPanel()
|
||||
const countInput = wrapper.findAll('label').find((label) => label.text().includes('确认箱数'))?.find('input')
|
||||
|
||||
expect((countInput?.element as HTMLInputElement).matches(':disabled')).toBe(false)
|
||||
expect(wrapper.findAll('button').find((button) => button.text().includes('保存确认箱数'))?.attributes('disabled')).toBeUndefined()
|
||||
expect(wrapper.findAll('button').find((button) => button.text().includes('提交审核'))?.attributes('disabled')).toBeDefined()
|
||||
expect(vi.mocked(downloadInboundMeasurementImage)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('clears the previous customer context before loading the next one', async () => {
|
||||
const wrapper = await mountPanel()
|
||||
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ inboundId: '7002', inboundCode: 'RK202608040002', companyName: '第二企业' }))
|
||||
|
||||
@@ -8,11 +8,10 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { downloadInboundMeasurementImage, getInboundMeasurement, saveInboundMeasurementDraft, submitInboundMeasurement } from './api'
|
||||
import { downloadInboundMeasurementImage, getInboundMeasurement, submitInboundMeasurement, updateConfirmedPackageCount } from './api'
|
||||
import type {
|
||||
InboundMeasurementDetail,
|
||||
InboundMeasurementItem,
|
||||
SaveInboundMeasurementDraftRequest,
|
||||
SaveInboundMeasurementItemRequest,
|
||||
} from './types'
|
||||
|
||||
@@ -27,6 +26,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
interface EditableMeasurementItem extends SaveInboundMeasurementItemRequest {
|
||||
itemId: string
|
||||
clientKey: string
|
||||
serverValues: Pick<InboundMeasurementItem, 'girthCm' | 'unitVolumeCbm' | 'totalWeightKg' | 'totalVolumeCbm' | 'volumeWeightKg'>
|
||||
}
|
||||
@@ -73,8 +73,9 @@ const canEdit = computed(() => access.can('platform:firstleg:inbound:measure:edi
|
||||
const canSubmit = computed(() => access.can('platform:firstleg:inbound:measure:submit'))
|
||||
const customerId = computed(() => props.customerId || props.tenantId || '')
|
||||
const hasContext = computed(() => Boolean(customerId.value && props.inboundId))
|
||||
const editableProcess = computed(() => ['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'].includes(detail.value?.processStatus || ''))
|
||||
const mayMaintain = computed(() => canEdit.value && editableProcess.value && !loading.value && !submitting.value)
|
||||
const countEditableProcess = computed(() => ['WAIT_MEASURE', 'MEASURING', 'MEASURE_PENDING', 'MEASURE_REJECTED'].includes(detail.value?.processStatus || ''))
|
||||
const submitEditableProcess = computed(() => ['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'].includes(detail.value?.processStatus || ''))
|
||||
const mayMaintain = computed(() => canEdit.value && countEditableProcess.value && !loading.value && !submitting.value)
|
||||
const confirmedPackageCount = computed(() => form.items.reduce((sum, item) => sum + (Number.isFinite(item.packageCount) ? item.packageCount : 0), 0))
|
||||
const expectedPackageCount = computed(() => detail.value?.originalPackageCount ?? null)
|
||||
const packageCountDifference = computed(() => expectedPackageCount.value == null ? null : confirmedPackageCount.value - expectedPackageCount.value)
|
||||
@@ -85,7 +86,7 @@ const packageCountDifferenceLabel = computed(() => {
|
||||
? `超出 ${packageCountDifference.value} 箱`
|
||||
: `还差 ${Math.abs(packageCountDifference.value)} 箱`
|
||||
})
|
||||
const maySubmit = computed(() => canSubmit.value && editableProcess.value && Boolean(form.measurementId) && packageCountMatched.value && !loading.value && !saving.value)
|
||||
const maySubmit = computed(() => canSubmit.value && submitEditableProcess.value && Boolean(form.measurementId) && packageCountMatched.value && !loading.value && !saving.value)
|
||||
const snapshot = computed(() => JSON.stringify(form))
|
||||
const isDirty = computed(() => Boolean(detail.value) && snapshot.value !== baseline.value)
|
||||
const registrationOpen = computed(() => hasContext.value && canView.value && Boolean(detail.value))
|
||||
@@ -101,11 +102,6 @@ const { captureBaseline } = useUnsavedWorkRegistration({
|
||||
requiredPermission,
|
||||
})
|
||||
|
||||
function optionalText(value: string) {
|
||||
const normalized = value.trim()
|
||||
return normalized || null
|
||||
}
|
||||
|
||||
function nextItemKey() {
|
||||
itemSequence += 1
|
||||
return `measurement-item-${itemSequence}`
|
||||
@@ -113,6 +109,7 @@ function nextItemKey() {
|
||||
|
||||
function editableItem(item: InboundMeasurementItem): EditableMeasurementItem {
|
||||
return {
|
||||
itemId: String(item.id),
|
||||
clientKey: nextItemKey(),
|
||||
boxCode: item.boxCode,
|
||||
itemNo: item.itemNo,
|
||||
@@ -202,36 +199,14 @@ async function reloadLatest() {
|
||||
await loadMeasurement()
|
||||
}
|
||||
|
||||
function isPositive(value: number | null) {
|
||||
return value != null && Number.isFinite(value) && value > 0
|
||||
}
|
||||
|
||||
function isNonNegative(value: number | null) {
|
||||
return value == null || (Number.isFinite(value) && value >= 0)
|
||||
}
|
||||
|
||||
function validateDraft() {
|
||||
const errors: Record<string, string[]> = {}
|
||||
const general: string[] = []
|
||||
if (!form.items.length) general.push('请至少保留一条实际扫描箱测量记录')
|
||||
if (form.items.length > 500) general.push('单个测量批次最多包含 500 条实际扫描箱记录')
|
||||
if (form.originalDocumentNo.trim().length > 64) general.push('原始单据号不能超过 64 个字符')
|
||||
if (form.actualGoodsName.trim().length > 255) general.push('实际品名不能超过 255 个字符')
|
||||
if (!isNonNegative(form.originalGrossWeightKg)) general.push('申报毛重不能小于 0')
|
||||
if (!isNonNegative(form.originalVolumeCbm)) general.push('申报体积不能小于 0')
|
||||
form.items.forEach((item, index) => {
|
||||
const itemErrors: string[] = []
|
||||
if (item.boxCode && item.boxCode.length > 128) itemErrors.push('箱号不能超过 128 个字符')
|
||||
if (!item.itemNo.trim()) itemErrors.push('请输入测量记录序号')
|
||||
else if (item.itemNo.trim().length > 32) itemErrors.push('箱规序号不能超过 32 个字符')
|
||||
if (!item.boxSpecification.trim()) itemErrors.push('请输入箱规描述')
|
||||
else if (item.boxSpecification.trim().length > 64) itemErrors.push('箱规描述不能超过 64 个字符')
|
||||
if (!Number.isInteger(item.packageCount) || item.packageCount < 1 || item.packageCount > 1_000_000) itemErrors.push('箱数必须是 1 至 1,000,000 的整数')
|
||||
if (!isPositive(item.lengthCm) || !isPositive(item.widthCm) || !isPositive(item.heightCm)) itemErrors.push('长、宽、高必须大于 0')
|
||||
if (!isPositive(item.unitWeightKg)) itemErrors.push('单箱重量必须大于 0')
|
||||
if (![item.outerLengthCm, item.outerWidthCm, item.outerHeightCm].every(isNonNegative)) itemErrors.push('外包装尺寸不能小于 0')
|
||||
if (item.intelligentImageFileId && !/^\d+$/.test(item.intelligentImageFileId)) itemErrors.push('测量图片文件 ID 必须是正整数')
|
||||
if ((item.overLimitWarning || '').length > 500 || (item.remark || '').length > 500) itemErrors.push('提示或备注不能超过 500 个字符')
|
||||
if (itemErrors.length) errors[item.clientKey] = itemErrors.map((message) => `第 ${index + 1} 条:${message}`)
|
||||
})
|
||||
if (general.length) errors.general = general
|
||||
@@ -239,35 +214,6 @@ function validateDraft() {
|
||||
return Object.keys(errors).length === 0
|
||||
}
|
||||
|
||||
function buildPayload(): SaveInboundMeasurementDraftRequest {
|
||||
return {
|
||||
measurementId: form.measurementId,
|
||||
version: form.version,
|
||||
originalDocumentNo: optionalText(form.originalDocumentNo),
|
||||
originalGrossWeightKg: form.originalGrossWeightKg,
|
||||
originalVolumeCbm: form.originalVolumeCbm,
|
||||
actualGoodsName: optionalText(form.actualGoodsName),
|
||||
items: form.items.map((item) => ({
|
||||
boxCode: optionalText(item.boxCode || ''),
|
||||
itemNo: item.itemNo.trim(),
|
||||
boxSpecification: item.boxSpecification.trim(),
|
||||
packageCount: item.packageCount,
|
||||
lengthCm: item.lengthCm,
|
||||
widthCm: item.widthCm,
|
||||
heightCm: item.heightCm,
|
||||
unitWeightKg: item.unitWeightKg,
|
||||
intelligentImageFileId: item.intelligentImageFileId || null,
|
||||
machineNo: optionalText(item.machineNo || ''),
|
||||
measuredAt: item.measuredAt || null,
|
||||
overLimitWarning: optionalText(item.overLimitWarning || ''),
|
||||
outerLengthCm: item.outerLengthCm,
|
||||
outerWidthCm: item.outerWidthCm,
|
||||
outerHeightCm: item.outerHeightCm,
|
||||
remark: optionalText(item.remark || ''),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function setOperationError(error: unknown, fallback: string) {
|
||||
const details = getApiErrorDetails(error, fallback)
|
||||
operationError.value = details
|
||||
@@ -285,10 +231,18 @@ async function saveDraft() {
|
||||
operationError.value = null
|
||||
conflictDetected.value = false
|
||||
try {
|
||||
const saved = await saveInboundMeasurementDraft(customerId.value, props.inboundId, buildPayload())
|
||||
form.measurementId = String(saved.measurementId)
|
||||
form.version = saved.version
|
||||
ElMessage.success('测量草稿已保存,计算结果已由服务端更新')
|
||||
const originalCounts = new Map((detail.value?.items || []).map((item) => [String(item.id), item.packageCount]))
|
||||
const changedItems = form.items.filter((item) => originalCounts.get(item.itemId) !== item.packageCount)
|
||||
let version = form.version
|
||||
for (const item of changedItems) {
|
||||
const saved = await updateConfirmedPackageCount(customerId.value, props.inboundId, item.itemId, {
|
||||
measurementVersion: version,
|
||||
confirmedPackageCount: item.packageCount,
|
||||
})
|
||||
version = saved.measurementVersion
|
||||
}
|
||||
form.version = version
|
||||
ElMessage.success('确认箱数已保存,计算结果已由服务端更新')
|
||||
await loadMeasurement()
|
||||
emit('changed')
|
||||
} catch (error) {
|
||||
@@ -307,7 +261,7 @@ async function submitForReview() {
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'提交后将校验测量图片和全部称重结果,并由后端准备托盘与库位占用。提交成功后本批次不能继续编辑。',
|
||||
'提交后将校验测量图片和全部称重结果,并由后端准备托盘与库位占用。提交成功后本批次不能继续编辑,设备也不能再覆盖已提交数据。',
|
||||
'提交测量审核',
|
||||
{ type: 'warning', confirmButtonText: '确认提交', cancelButtonText: '取消' },
|
||||
)
|
||||
@@ -399,7 +353,7 @@ onBeforeUnmount(releaseImagePreview)
|
||||
<RefreshCw :size="15" />刷新
|
||||
</el-button>
|
||||
<el-button v-if="canEdit" type="primary" plain :loading="saving" :disabled="!mayMaintain" @click="saveDraft">
|
||||
<Save :size="15" />保存草稿
|
||||
<Save :size="15" />保存确认箱数
|
||||
</el-button>
|
||||
<el-button v-if="canSubmit" type="primary" :loading="submitting" :disabled="!maySubmit" @click="submitForReview">
|
||||
<Send :size="15" />提交审核
|
||||
@@ -452,9 +406,9 @@ onBeforeUnmount(releaseImagePreview)
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<el-alert v-if="!editableProcess" type="info" :closable="false" show-icon>
|
||||
<el-alert v-if="!countEditableProcess" type="info" :closable="false" show-icon>
|
||||
<template #title>当前状态为“{{ processStatusLabel(detail.processStatus) }}”,测量数据仅可查看</template>
|
||||
测量草稿只允许在待测量、测量中或审核驳回状态维护。
|
||||
确认箱数只允许在待测量、测量中、待审核或审核驳回状态维护。
|
||||
</el-alert>
|
||||
|
||||
<el-alert v-else-if="!canEdit" type="info" :closable="false" show-icon title="当前账号仅可查看测量数据" />
|
||||
@@ -491,16 +445,16 @@ onBeforeUnmount(releaseImagePreview)
|
||||
<div><h3>原单数据</h3><p>原单值仅用于审核对比,不参与前端计算。</p></div>
|
||||
</div>
|
||||
<div class="measurement-form-grid">
|
||||
<label><span>原单据号码(CKW)</span><el-input v-model="form.originalDocumentNo" maxlength="64" placeholder="请输入原单据号码" /></label>
|
||||
<label><span>原单据毛重(T.K.GW)</span><el-input-number v-model="form.originalGrossWeightKg" :min="0" :precision="3" :controls="false" placeholder="0.000" /></label>
|
||||
<label><span>原单据总体积(T.CBM)</span><el-input-number v-model="form.originalVolumeCbm" :min="0" :precision="4" :controls="false" placeholder="0.0000" /></label>
|
||||
<label><span>实际品名</span><el-input v-model="form.actualGoodsName" maxlength="255" placeholder="请输入实际货物名称" /></label>
|
||||
<label><span>原单据号码(CKW)</span><el-input :model-value="form.originalDocumentNo || '—'" readonly /></label>
|
||||
<label><span>原单据毛重(T.K.GW)</span><el-input :model-value="formatNumber(form.originalGrossWeightKg)" readonly /></label>
|
||||
<label><span>原单据总体积(T.CBM)</span><el-input :model-value="formatNumber(form.originalVolumeCbm, 4)" readonly /></label>
|
||||
<label><span>实际品名</span><el-input :model-value="form.actualGoodsName || '—'" readonly /></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="measurement-section measurement-items-section">
|
||||
<div class="measurement-section__heading">
|
||||
<div><h3>扫描箱测量数据明细</h3><p>每条记录对应一个实际扫描箱;人工只确认该条记录代表的箱数,设备数据由服务端计算汇总。</p></div>
|
||||
<div><h3>扫描箱测量数据明细</h3><p>每条记录对应一个实际扫描箱;人工只确认该条记录代表的箱数,设备实测值不可在此修改。</p></div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!form.items.length" description="暂无扫描箱测量数据" />
|
||||
@@ -511,25 +465,15 @@ onBeforeUnmount(releaseImagePreview)
|
||||
</header>
|
||||
<div class="measurement-item__grid">
|
||||
<label><span>箱号</span><el-input :model-value="item.boxCode || '—'" readonly /></label>
|
||||
<label><span>测量长(cm)*</span><el-input-number v-model="item.lengthCm" :min="0.001" :precision="3" :controls="false" /></label>
|
||||
<label><span>测量宽(cm)*</span><el-input-number v-model="item.widthCm" :min="0.001" :precision="3" :controls="false" /></label>
|
||||
<label><span>测量高(cm)*</span><el-input-number v-model="item.heightCm" :min="0.001" :precision="3" :controls="false" /></label>
|
||||
<label><span>测量长(cm)</span><el-input :model-value="formatNumber(item.lengthCm)" readonly /></label>
|
||||
<label><span>测量宽(cm)</span><el-input :model-value="formatNumber(item.widthCm)" readonly /></label>
|
||||
<label><span>测量高(cm)</span><el-input :model-value="formatNumber(item.heightCm)" readonly /></label>
|
||||
<label><span>确认箱数 *</span><el-input-number v-model="item.packageCount" :min="1" :max="1000000" :step="1" step-strictly controls-position="right" /></label>
|
||||
<label><span>每箱毛重(kg)*</span><el-input-number v-model="item.unitWeightKg" :min="0.001" :precision="3" :controls="false" /></label>
|
||||
<label><span>每箱毛重(kg)</span><el-input :model-value="formatNumber(item.unitWeightKg)" readonly /></label>
|
||||
<label><span>机器编号</span><el-input :model-value="item.machineNo || '—'" readonly /></label>
|
||||
<label><span>测量时间</span><el-input :model-value="item.measuredAt ? item.measuredAt.replace('T', ' ') : '—'" readonly /></label>
|
||||
<label class="measurement-image-field"><span>当前有效图片</span><el-button :disabled="!item.intelligentImageFileId" @click="previewImage(item.intelligentImageFileId)"><Image :size="15" />{{ item.intelligentImageFileId ? '预览图片' : '暂无图片' }}</el-button></label>
|
||||
</div>
|
||||
<details class="measurement-item__advanced">
|
||||
<summary>外包装尺寸与补充说明</summary>
|
||||
<div class="measurement-item__grid">
|
||||
<label><span>外箱规长(cm)</span><el-input-number v-model="item.outerLengthCm" :min="0" :precision="3" :controls="false" /></label>
|
||||
<label><span>外箱规宽(cm)</span><el-input-number v-model="item.outerWidthCm" :min="0" :precision="3" :controls="false" /></label>
|
||||
<label><span>外箱规高(cm)</span><el-input-number v-model="item.outerHeightCm" :min="0" :precision="3" :controls="false" /></label>
|
||||
<label class="span-two"><span>超限提醒</span><el-input v-model="item.overLimitWarning" maxlength="500" placeholder="设备或人工发现的尺寸异常" /></label>
|
||||
<label class="span-two"><span>备注</span><el-input v-model="item.remark" maxlength="500" placeholder="请输入本箱规的补充说明" /></label>
|
||||
</div>
|
||||
</details>
|
||||
<dl class="measurement-item__results" aria-label="服务端计算结果">
|
||||
<div><dt>箱规周长</dt><dd>{{ formatNumber(item.serverValues.girthCm) }} cm</dd></div>
|
||||
<div><dt>单箱体积</dt><dd>{{ formatNumber(item.serverValues.unitVolumeCbm, 4) }} CBM</dd></div>
|
||||
@@ -546,7 +490,7 @@ onBeforeUnmount(releaseImagePreview)
|
||||
<span v-if="isDirty"><AlertTriangle :size="15" />有尚未保存的修改</span>
|
||||
<span v-else>当前显示版本 {{ form.version }}</span>
|
||||
<div>
|
||||
<el-button v-if="canEdit" type="primary" plain :loading="saving" :disabled="!mayMaintain" @click="saveDraft"><Save :size="15" />保存草稿</el-button>
|
||||
<el-button v-if="canEdit" type="primary" plain :loading="saving" :disabled="!mayMaintain" @click="saveDraft"><Save :size="15" />保存确认箱数</el-button>
|
||||
<el-button v-if="canSubmit" type="primary" :loading="submitting" :disabled="!maySubmit" @click="submitForReview"><Send :size="15" />提交审核</el-button>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { getInboundMeasurement, saveInboundMeasurementDraft, submitInboundMeasurement } from './api'
|
||||
import { downloadInboundMeasurementImage, getInboundMeasurement, saveInboundMeasurementDraft, submitInboundMeasurement, updateConfirmedPackageCount } from './api'
|
||||
import type { SaveInboundMeasurementDraftRequest } from './types'
|
||||
|
||||
vi.mock('@/shared/api/http', () => ({
|
||||
@@ -75,4 +75,30 @@ describe('inbound measurement API', () => {
|
||||
{ params: { tenantId: '10001' } },
|
||||
)
|
||||
})
|
||||
|
||||
it('downloads the current measurement image using the scoped file route', async () => {
|
||||
const image = new Blob(['image-data'], { type: 'image/jpeg' })
|
||||
vi.mocked(http.get).mockResolvedValue(image)
|
||||
|
||||
await expect(downloadInboundMeasurementImage('100/01', '7001#1', '88001')).resolves.toBe(image)
|
||||
expect(http.get).toHaveBeenCalledWith(
|
||||
'/platform/v1/files/measurements/100%2F01/7001%231/88001',
|
||||
{ responseType: 'blob' },
|
||||
)
|
||||
})
|
||||
|
||||
it('updates one scanned carton confirmed package count', async () => {
|
||||
const saved = { measurementId: '9001', measurementVersion: 3, itemId: '1', boxCode: 'RK#10-1', confirmedPackageCount: 9 }
|
||||
vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: saved })
|
||||
|
||||
await expect(updateConfirmedPackageCount('10001', '7001', '1', {
|
||||
measurementVersion: 2,
|
||||
confirmedPackageCount: 9,
|
||||
})).resolves.toEqual(saved)
|
||||
expect(http.put).toHaveBeenCalledWith(
|
||||
'/platform/v1/first-leg/inbounds/7001/measurement/items/1/confirmed-package-count',
|
||||
{ measurementVersion: 2, confirmedPackageCount: 9 },
|
||||
{ params: { tenantId: '10001' } },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,8 @@ import type {
|
||||
InboundMeasurementDetail,
|
||||
InboundMeasurementDraftSaved,
|
||||
InboundMeasurementSubmitted,
|
||||
CartonMeasurementResult,
|
||||
ConfirmedPackageCountRequest,
|
||||
SaveInboundMeasurementDraftRequest,
|
||||
SubmitInboundMeasurementRequest,
|
||||
} from './types'
|
||||
@@ -11,11 +13,27 @@ function measurementPath(inboundId: string) {
|
||||
return `/platform/v1/first-leg/inbounds/${encodeURIComponent(inboundId)}/measurement`
|
||||
}
|
||||
|
||||
export async function updateConfirmedPackageCount(
|
||||
customerId: string,
|
||||
inboundId: string,
|
||||
itemId: string,
|
||||
payload: ConfirmedPackageCountRequest,
|
||||
) {
|
||||
const response = await http.put<ApiEnvelope<CartonMeasurementResult>, ApiEnvelope<CartonMeasurementResult>>(
|
||||
`${measurementPath(inboundId)}/items/${segment(itemId)}/confirmed-package-count`,
|
||||
payload,
|
||||
{ params: { tenantId: customerId } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
function segment(value: string) {
|
||||
return encodeURIComponent(value)
|
||||
}
|
||||
|
||||
export async function getInboundMeasurement(customerId: string, inboundId: string) {
|
||||
// Device-scanned fields expected from the backend: originalPackageCount and
|
||||
// items[].boxCode, machineNo, measuredAt. Keep this compatibility mapping here.
|
||||
const response = await http.get<ApiEnvelope<InboundMeasurementDetail>, ApiEnvelope<InboundMeasurementDetail>>(
|
||||
measurementPath(inboundId),
|
||||
{ params: { tenantId: customerId } },
|
||||
|
||||
@@ -120,6 +120,19 @@ export interface InboundMeasurementDraftSaved {
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface ConfirmedPackageCountRequest {
|
||||
measurementVersion: number
|
||||
confirmedPackageCount: number
|
||||
}
|
||||
|
||||
export interface CartonMeasurementResult {
|
||||
measurementId: MeasurementIdentifier
|
||||
measurementVersion: number
|
||||
itemId: MeasurementIdentifier
|
||||
boxCode: string
|
||||
confirmedPackageCount: number
|
||||
}
|
||||
|
||||
export interface SubmitInboundMeasurementRequest {
|
||||
measurementId: MeasurementIdentifier
|
||||
version: number
|
||||
|
||||
@@ -27,6 +27,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
},
|
||||
server: {
|
||||
strictPort: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: proxyTarget,
|
||||
|
||||
Reference in New Issue
Block a user