优化
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user