2026-08-04 21:01:39 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-08-07 18:24:05 +08:00
|
|
|
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
2026-08-07 20:07:37 +08:00
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
2026-08-04 21:01:39 +08:00
|
|
|
|
import {
|
|
|
|
|
|
ImagePlus,
|
2026-08-14 17:38:06 +08:00
|
|
|
|
RefreshCw,
|
2026-08-04 21:01:39 +08:00
|
|
|
|
Scale,
|
2026-08-07 20:07:37 +08:00
|
|
|
|
Trash2,
|
2026-08-04 21:01:39 +08:00
|
|
|
|
} from '@lucide/vue'
|
|
|
|
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
|
|
|
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
|
|
|
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|
|
|
|
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
|
|
|
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|
|
|
|
|
import {
|
|
|
|
|
|
confirmWeightResult,
|
|
|
|
|
|
createWeightResult,
|
2026-08-07 20:07:37 +08:00
|
|
|
|
deleteWeightResult,
|
2026-08-04 21:01:39 +08:00
|
|
|
|
downloadWeightImage,
|
2026-08-07 17:46:27 +08:00
|
|
|
|
getWeightOcrRawText,
|
2026-08-14 17:38:06 +08:00
|
|
|
|
getWeightPalletOptions,
|
2026-08-04 21:01:39 +08:00
|
|
|
|
getWeightWorkspace,
|
|
|
|
|
|
uploadWeightImage,
|
|
|
|
|
|
} from '../api/weightOcr'
|
2026-09-04 12:18:39 +08:00
|
|
|
|
import type {
|
|
|
|
|
|
WeightPalletOption,
|
|
|
|
|
|
WeightPutawaySlotStatus,
|
|
|
|
|
|
WeightResultResource,
|
|
|
|
|
|
WeightWorkspaceResource,
|
|
|
|
|
|
} from '../types'
|
2026-08-07 20:07:37 +08:00
|
|
|
|
import WeightImageThumbnail from './WeightImageThumbnail.vue'
|
2026-08-04 21:01:39 +08:00
|
|
|
|
|
2026-08-18 11:33:01 +08:00
|
|
|
|
const props = withDefaults(defineProps<{
|
|
|
|
|
|
customerId?: string
|
|
|
|
|
|
tenantId?: string
|
|
|
|
|
|
inboundId: string
|
|
|
|
|
|
reviewMode?: boolean
|
|
|
|
|
|
correctionMode?: boolean
|
|
|
|
|
|
}>(), {
|
|
|
|
|
|
reviewMode: false,
|
|
|
|
|
|
correctionMode: false,
|
|
|
|
|
|
})
|
2026-08-14 14:50:44 +08:00
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
changed: []
|
2026-08-18 11:33:01 +08:00
|
|
|
|
summaryChanged: [actualGrossWeightKg: number | null, actualNetWeightKg: number | null]
|
2026-08-14 14:50:44 +08:00
|
|
|
|
}>()
|
2026-08-04 21:01:39 +08:00
|
|
|
|
|
|
|
|
|
|
const access = useAccessPreviewStore()
|
|
|
|
|
|
const fileInput = ref<HTMLInputElement>()
|
|
|
|
|
|
const workspace = ref<WeightWorkspaceResource | null>(null)
|
2026-08-14 17:38:06 +08:00
|
|
|
|
const palletOptions = ref<WeightPalletOption[]>([])
|
|
|
|
|
|
const selectedPalletId = ref('')
|
2026-09-01 11:10:22 +08:00
|
|
|
|
const palletKeyword = ref('')
|
2026-08-14 17:38:06 +08:00
|
|
|
|
const palletLoading = ref(false)
|
|
|
|
|
|
const palletError = ref<ApiErrorDetails | null>(null)
|
2026-08-04 21:01:39 +08:00
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const uploading = ref(false)
|
|
|
|
|
|
const loadError = ref<ApiErrorDetails | null>(null)
|
|
|
|
|
|
const activeAction = ref('')
|
|
|
|
|
|
const previewOpen = ref(false)
|
|
|
|
|
|
const previewLoading = ref(false)
|
|
|
|
|
|
const previewError = ref<ApiErrorDetails | null>(null)
|
|
|
|
|
|
const previewUrl = ref('')
|
2026-08-07 20:07:37 +08:00
|
|
|
|
const previewResultId = ref('')
|
|
|
|
|
|
const previewSnapshot = ref<WeightResultResource | null>(null)
|
2026-08-18 11:33:01 +08:00
|
|
|
|
const weightDrafts = ref<Record<string, { value: number | null; unit: 'KG' | 'LB'; reason: string }>>({})
|
2026-08-04 21:01:39 +08:00
|
|
|
|
let loadSequence = 0
|
2026-08-27 11:08:03 +08:00
|
|
|
|
let palletLoadSequence = 0
|
|
|
|
|
|
let palletAbortController: AbortController | null = null
|
2026-09-01 11:10:22 +08:00
|
|
|
|
const PALLET_OPTION_PAGE_SIZE = 50
|
2026-08-04 21:01:39 +08:00
|
|
|
|
|
|
|
|
|
|
const editableStatuses = new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'])
|
2026-08-07 19:23:43 +08:00
|
|
|
|
const minimumWeight = { KG: 0.001, LB: 0.002 } as const
|
|
|
|
|
|
const maximumSafeWeight = 9_000_000_000_000
|
2026-08-04 21:01:39 +08:00
|
|
|
|
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'))
|
|
|
|
|
|
const canView = computed(() => access.can('platform:firstleg:inbound:measure:view'))
|
|
|
|
|
|
const editable = computed(() => Boolean(workspace.value && editableStatuses.has(workspace.value.processStatus)))
|
2026-08-27 11:08:03 +08:00
|
|
|
|
const canLoadPalletOptions = computed(() => Boolean(
|
|
|
|
|
|
!props.reviewMode
|
|
|
|
|
|
&& customerId.value
|
|
|
|
|
|
&& props.inboundId
|
|
|
|
|
|
&& canView.value
|
|
|
|
|
|
&& canEdit.value
|
|
|
|
|
|
&& editable.value,
|
|
|
|
|
|
))
|
|
|
|
|
|
const virtualPalletOptions = computed(() => palletOptions.value.map((option) => ({
|
|
|
|
|
|
label: palletOptionLabel(option),
|
|
|
|
|
|
value: option.palletId,
|
|
|
|
|
|
})))
|
2026-08-18 11:33:01 +08:00
|
|
|
|
const reviewCorrectionEnabled = computed(() => Boolean(
|
|
|
|
|
|
props.reviewMode
|
|
|
|
|
|
&& props.correctionMode
|
|
|
|
|
|
&& workspace.value?.processStatus === 'MEASURE_PENDING',
|
|
|
|
|
|
))
|
2026-08-04 21:01:39 +08:00
|
|
|
|
const rows = computed(() => workspace.value?.weightResults ?? [])
|
2026-08-07 20:07:37 +08:00
|
|
|
|
const previewResult = computed(() => rows.value.find((row) => row.id === previewResultId.value)
|
|
|
|
|
|
|| previewSnapshot.value)
|
2026-08-08 11:05:45 +08:00
|
|
|
|
const previewConfirmDisabled = computed(() => {
|
|
|
|
|
|
const row = previewResult.value
|
|
|
|
|
|
return !row
|
|
|
|
|
|
|| !isWeightDraftValid(row)
|
2026-08-18 11:33:01 +08:00
|
|
|
|
|| (row.recognitionStatus === 'CONFIRMED' && reviewCorrectionEnabled.value
|
|
|
|
|
|
&& (!hasWeightDraftChanged(row) || !weightDrafts.value[row.id]?.reason.trim()))
|
2026-08-08 11:05:45 +08:00
|
|
|
|
|| Boolean(activeAction.value)
|
|
|
|
|
|
})
|
2026-08-18 11:33:01 +08:00
|
|
|
|
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'
|
|
|
|
|
|
? '保存重量修正'
|
|
|
|
|
|
: '确认重量')
|
2026-08-04 21:01:39 +08:00
|
|
|
|
|
|
|
|
|
|
function formatWeight(value: number | null, unit = 'KG') {
|
|
|
|
|
|
return value == null ? '—' : `${Number(value).toFixed(3)} ${unit}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 18:32:41 +08:00
|
|
|
|
function palletOptionLabel(option: WeightPalletOption) {
|
|
|
|
|
|
return `${option.palletType || '未记录类型'} · 皮重 ${formatWeight(option.tareWeightKg)} · 容量 ${option.capacityCbm} m³`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-04 21:01:39 +08:00
|
|
|
|
function actionKey(action: string, id: string) {
|
|
|
|
|
|
return `${action}:${id}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isActionLoading(action: string, id: string) {
|
|
|
|
|
|
return activeAction.value === actionKey(action, id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 18:24:05 +08:00
|
|
|
|
function normalizeUnit(value: string | null) {
|
2026-08-07 19:23:43 +08:00
|
|
|
|
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() || '')
|
2026-08-07 18:24:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function syncWeightDrafts(results: WeightResultResource[]) {
|
|
|
|
|
|
weightDrafts.value = Object.fromEntries(results.map((row) => [row.id, {
|
|
|
|
|
|
value: row.adjustedWeightValue ?? row.ocrWeightValue,
|
|
|
|
|
|
unit: normalizeUnit(row.adjustedWeightUnit ?? row.ocrWeightUnit),
|
2026-08-18 11:33:01 +08:00
|
|
|
|
reason: '',
|
2026-08-07 18:24:05 +08:00
|
|
|
|
}]))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 20:07:37 +08:00
|
|
|
|
function ensureWeightDraft(row: WeightResultResource) {
|
|
|
|
|
|
weightDrafts.value[row.id] ??= {
|
|
|
|
|
|
value: row.adjustedWeightValue ?? row.ocrWeightValue,
|
|
|
|
|
|
unit: normalizeUnit(row.adjustedWeightUnit ?? row.ocrWeightUnit),
|
2026-08-18 11:33:01 +08:00
|
|
|
|
reason: '',
|
2026-08-07 20:07:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 18:24:05 +08:00
|
|
|
|
function isWeightDraftValid(row: WeightResultResource) {
|
|
|
|
|
|
const draft = weightDrafts.value[row.id]
|
2026-08-07 19:23:43 +08:00
|
|
|
|
const value = Number(draft?.value)
|
|
|
|
|
|
return Boolean(draft
|
|
|
|
|
|
&& Number.isFinite(value)
|
|
|
|
|
|
&& value >= minimumWeight[draft.unit]
|
|
|
|
|
|
&& value <= maximumSafeWeight)
|
2026-08-07 18:24:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 19:59:32 +08:00
|
|
|
|
function draftGrossWeightKg(row: WeightResultResource) {
|
|
|
|
|
|
const draft = weightDrafts.value[row.id]
|
|
|
|
|
|
if (!draft || !Number.isFinite(Number(draft.value))) return null
|
|
|
|
|
|
const value = Number(draft.value)
|
|
|
|
|
|
return draft.unit === 'LB' ? value * 0.45359237 : value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showActionError(error: unknown, fallback: string) {
|
|
|
|
|
|
const details = getApiErrorDetails(error, fallback)
|
|
|
|
|
|
const requestReference = details.requestId ? `(请求 ID:${details.requestId})` : ''
|
|
|
|
|
|
ElMessage.error({
|
|
|
|
|
|
message: `${details.message}${requestReference}`,
|
|
|
|
|
|
duration: 5_000,
|
|
|
|
|
|
showClose: true,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 20:07:37 +08:00
|
|
|
|
function cardWeight(row: WeightResultResource) {
|
2026-08-14 17:38:06 +08:00
|
|
|
|
if (row.recognitionStatus === 'CONFIRMED') return formatWeight(row.netWeightKg)
|
2026-08-07 20:07:37 +08:00
|
|
|
|
const draft = weightDrafts.value[row.id]
|
|
|
|
|
|
return formatWeight(draft?.value ?? null, draft?.unit ?? 'KG')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-04 12:18:39 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 18:24:05 +08:00
|
|
|
|
function hasWeightDraftChanged(row: WeightResultResource) {
|
|
|
|
|
|
const draft = weightDrafts.value[row.id]
|
|
|
|
|
|
if (!draft) return false
|
|
|
|
|
|
const savedValue = row.adjustedWeightValue ?? row.ocrWeightValue
|
2026-08-07 19:23:43 +08:00
|
|
|
|
const rawSavedUnit = row.adjustedWeightUnit ?? row.ocrWeightUnit
|
|
|
|
|
|
const savedUnit = normalizeUnit(rawSavedUnit)
|
|
|
|
|
|
if (!isSupportedUnit(rawSavedUnit)) return true
|
2026-08-07 18:24:05 +08:00
|
|
|
|
return Number(draft.value) !== Number(savedValue) || draft.unit !== savedUnit
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 18:44:55 +08:00
|
|
|
|
function buildAdjustmentReason(row: WeightResultResource) {
|
|
|
|
|
|
const draft = weightDrafts.value[row.id]
|
2026-08-07 19:23:43 +08:00
|
|
|
|
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)}`
|
2026-08-07 18:44:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 14:50:44 +08:00
|
|
|
|
function emitWorkspaceChanged() {
|
2026-08-18 11:33:01 +08:00
|
|
|
|
emit('summaryChanged', workspace.value?.actualGrossWeightKg ?? null, workspace.value?.actualNetWeightKg ?? null)
|
2026-08-14 14:50:44 +08:00
|
|
|
|
emit('changed')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-04 21:01:39 +08:00
|
|
|
|
async function loadWorkspace() {
|
|
|
|
|
|
if (!customerId.value || !props.inboundId || !canView.value) {
|
|
|
|
|
|
workspace.value = null
|
2026-08-07 18:24:05 +08:00
|
|
|
|
weightDrafts.value = {}
|
2026-08-04 21:01:39 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const sequence = ++loadSequence
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
loadError.value = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await getWeightWorkspace(customerId.value, props.inboundId)
|
2026-08-07 18:24:05 +08:00
|
|
|
|
if (sequence === loadSequence) {
|
|
|
|
|
|
workspace.value = result
|
|
|
|
|
|
syncWeightDrafts(result.weightResults)
|
|
|
|
|
|
}
|
2026-08-04 21:01:39 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (sequence !== loadSequence) return
|
|
|
|
|
|
loadError.value = getApiErrorDetails(error, '称重与 OCR 数据加载失败,请稍后重试')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (sequence === loadSequence) loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 11:10:22 +08:00
|
|
|
|
async function loadPalletOptions(keyword = palletKeyword.value) {
|
2026-08-27 11:08:03 +08:00
|
|
|
|
if (!canLoadPalletOptions.value) {
|
|
|
|
|
|
cancelPalletOptionsLoad()
|
2026-08-18 11:33:01 +08:00
|
|
|
|
palletOptions.value = []
|
|
|
|
|
|
selectedPalletId.value = ''
|
|
|
|
|
|
palletError.value = null
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-27 11:08:03 +08:00
|
|
|
|
cancelPalletOptionsLoad()
|
|
|
|
|
|
const sequence = ++palletLoadSequence
|
|
|
|
|
|
const controller = new AbortController()
|
|
|
|
|
|
palletAbortController = controller
|
2026-09-01 11:10:22 +08:00
|
|
|
|
palletKeyword.value = keyword.trim()
|
2026-08-14 17:38:06 +08:00
|
|
|
|
palletLoading.value = true
|
|
|
|
|
|
palletError.value = null
|
|
|
|
|
|
try {
|
2026-09-01 11:10:22 +08:00
|
|
|
|
const result = await getWeightPalletOptions(customerId.value, props.inboundId, {
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
size: PALLET_OPTION_PAGE_SIZE,
|
|
|
|
|
|
keyword: palletKeyword.value || undefined,
|
|
|
|
|
|
signal: controller.signal,
|
|
|
|
|
|
})
|
2026-08-27 11:08:03 +08:00
|
|
|
|
if (sequence !== palletLoadSequence || controller.signal.aborted) return
|
2026-08-27 13:50:41 +08:00
|
|
|
|
palletOptions.value = result.records
|
|
|
|
|
|
if (!result.records.some((option) => option.palletId === selectedPalletId.value)) selectedPalletId.value = ''
|
2026-08-14 17:38:06 +08:00
|
|
|
|
} catch (error) {
|
2026-08-27 11:08:03 +08:00
|
|
|
|
if (sequence !== palletLoadSequence || controller.signal.aborted) return
|
2026-08-14 17:38:06 +08:00
|
|
|
|
palletOptions.value = []
|
|
|
|
|
|
selectedPalletId.value = ''
|
|
|
|
|
|
palletError.value = getApiErrorDetails(error, '可选托盘加载失败,请稍后重试')
|
|
|
|
|
|
} finally {
|
2026-08-27 11:08:03 +08:00
|
|
|
|
if (sequence === palletLoadSequence) {
|
|
|
|
|
|
palletLoading.value = false
|
|
|
|
|
|
if (palletAbortController === controller) palletAbortController = null
|
|
|
|
|
|
}
|
2026-08-14 17:38:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 11:10:22 +08:00
|
|
|
|
function searchPalletOptions(keyword: string) {
|
|
|
|
|
|
void loadPalletOptions(keyword)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function reloadPalletOptions() {
|
|
|
|
|
|
void loadPalletOptions()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 11:08:03 +08:00
|
|
|
|
function cancelPalletOptionsLoad() {
|
|
|
|
|
|
++palletLoadSequence
|
|
|
|
|
|
palletAbortController?.abort()
|
|
|
|
|
|
palletAbortController = null
|
|
|
|
|
|
palletLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-04 21:01:39 +08:00
|
|
|
|
function chooseFile() {
|
|
|
|
|
|
if (!editable.value) {
|
|
|
|
|
|
ElMessage.warning('当前入库状态不允许上传称重图片')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-14 17:38:06 +08:00
|
|
|
|
if (!selectedPalletId.value) {
|
|
|
|
|
|
ElMessage.warning('请先选择本次称重使用的托盘')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-04 21:01:39 +08:00
|
|
|
|
fileInput.value?.click()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function validateImage(file: File) {
|
|
|
|
|
|
if (!['image/jpeg', 'image/png', 'image/bmp'].includes(file.type)) return '称重图片仅支持 JPG、PNG、BMP'
|
|
|
|
|
|
if (file.size > 5 * 1024 * 1024) return '称重图片不能超过 5MB'
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleFileChange(event: Event) {
|
|
|
|
|
|
const input = event.target as HTMLInputElement
|
|
|
|
|
|
const file = input.files?.[0]
|
|
|
|
|
|
input.value = ''
|
|
|
|
|
|
if (!file || uploading.value) return
|
|
|
|
|
|
const validationError = validateImage(file)
|
|
|
|
|
|
if (validationError) {
|
|
|
|
|
|
ElMessage.warning(validationError)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
uploading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const upload = await uploadWeightImage(customerId.value, props.inboundId, file)
|
2026-08-07 20:07:37 +08:00
|
|
|
|
const created = await createWeightResult(customerId.value, props.inboundId, {
|
2026-08-04 21:01:39 +08:00
|
|
|
|
fileId: String(upload.fileId),
|
2026-08-14 17:38:06 +08:00
|
|
|
|
palletId: selectedPalletId.value,
|
2026-08-04 21:01:39 +08:00
|
|
|
|
ocrProvider: 'FILE_WEIGHT_OCR',
|
2026-08-07 17:46:27 +08:00
|
|
|
|
ocrRawText: getWeightOcrRawText(upload),
|
2026-08-04 21:01:39 +08:00
|
|
|
|
ocrWeightValue: upload.weightValue,
|
|
|
|
|
|
ocrWeightUnit: upload.weightUnit,
|
|
|
|
|
|
confidence: upload.confidence,
|
|
|
|
|
|
})
|
2026-08-07 20:07:37 +08:00
|
|
|
|
ensureWeightDraft(created)
|
|
|
|
|
|
await openPreview(created, URL.createObjectURL(file))
|
2026-08-04 21:01:39 +08:00
|
|
|
|
await loadWorkspace()
|
2026-08-07 20:07:37 +08:00
|
|
|
|
ensureWeightDraft(created)
|
2026-08-14 14:50:44 +08:00
|
|
|
|
emitWorkspaceChanged()
|
2026-08-04 21:01:39 +08:00
|
|
|
|
} catch (error) {
|
2026-08-14 19:59:32 +08:00
|
|
|
|
showActionError(error, '称重图片上传失败,请检查后重试')
|
2026-08-04 21:01:39 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
uploading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 20:07:37 +08:00
|
|
|
|
async function confirmResult(row: WeightResultResource, closeAfterSuccess = false) {
|
2026-08-07 18:24:05 +08:00
|
|
|
|
const draft = weightDrafts.value[row.id]
|
|
|
|
|
|
if (!draft || !isWeightDraftValid(row)) {
|
|
|
|
|
|
ElMessage.warning('请输入大于 0 的有效重量')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-14 19:59:32 +08:00
|
|
|
|
const grossWeightKg = draftGrossWeightKg(row)
|
|
|
|
|
|
if (grossWeightKg == null || grossWeightKg <= Number(row.palletTareWeightKg)) {
|
|
|
|
|
|
ElMessage.warning(`称重毛重必须大于托盘皮重 ${formatWeight(row.palletTareWeightKg)}`)
|
2026-08-08 11:05:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-04 21:01:39 +08:00
|
|
|
|
const key = actionKey('confirm', row.id)
|
|
|
|
|
|
activeAction.value = key
|
|
|
|
|
|
try {
|
2026-08-07 18:44:55 +08:00
|
|
|
|
const payload = { version: row.version } as Parameters<typeof confirmWeightResult>[3]
|
2026-08-07 18:24:05 +08:00
|
|
|
|
if (hasWeightDraftChanged(row)) {
|
|
|
|
|
|
if (!canEdit.value) {
|
|
|
|
|
|
ElMessage.warning('当前账号缺少重量修改权限')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-07 18:44:55 +08:00
|
|
|
|
payload.adjustedWeightValue = Number(draft.value)
|
|
|
|
|
|
payload.adjustedWeightUnit = draft.unit
|
2026-08-18 11:33:01 +08:00
|
|
|
|
payload.adjustmentReason = row.recognitionStatus === 'CONFIRMED' && reviewCorrectionEnabled.value
|
|
|
|
|
|
? draft.reason.trim()
|
|
|
|
|
|
: buildAdjustmentReason(row)
|
2026-08-07 18:24:05 +08:00
|
|
|
|
}
|
2026-08-07 18:44:55 +08:00
|
|
|
|
await confirmWeightResult(customerId.value, props.inboundId, row.id, payload)
|
2026-08-14 19:59:32 +08:00
|
|
|
|
if (closeAfterSuccess) previewOpen.value = false
|
2026-08-18 11:33:01 +08:00
|
|
|
|
ElMessage.success(row.recognitionStatus === 'CONFIRMED' ? '含托盘毛重已修正,货物重量和合计已由服务端重算' : '重量已确认')
|
2026-08-04 21:01:39 +08:00
|
|
|
|
await loadWorkspace()
|
2026-08-14 14:50:44 +08:00
|
|
|
|
emitWorkspaceChanged()
|
2026-08-07 20:07:37 +08:00
|
|
|
|
return true
|
2026-08-04 21:01:39 +08:00
|
|
|
|
} catch (error) {
|
2026-08-14 19:59:32 +08:00
|
|
|
|
showActionError(error, '重量确认失败,请刷新后重试')
|
2026-08-04 21:01:39 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
activeAction.value = ''
|
|
|
|
|
|
}
|
2026-08-07 20:07:37 +08:00
|
|
|
|
return false
|
2026-08-04 21:01:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function releasePreview() {
|
|
|
|
|
|
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
|
|
|
|
|
|
previewUrl.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 20:07:37 +08:00
|
|
|
|
async function openPreview(row: WeightResultResource, localUrl = '') {
|
2026-08-04 21:01:39 +08:00
|
|
|
|
releasePreview()
|
2026-08-07 20:07:37 +08:00
|
|
|
|
previewResultId.value = row.id
|
|
|
|
|
|
previewSnapshot.value = row
|
2026-08-04 21:01:39 +08:00
|
|
|
|
previewError.value = null
|
|
|
|
|
|
previewOpen.value = true
|
2026-08-07 20:07:37 +08:00
|
|
|
|
if (localUrl) {
|
|
|
|
|
|
previewUrl.value = localUrl
|
|
|
|
|
|
previewLoading.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-04 21:01:39 +08:00
|
|
|
|
previewLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const blob = await downloadWeightImage(customerId.value, props.inboundId, row.fileId)
|
|
|
|
|
|
previewUrl.value = URL.createObjectURL(blob)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
previewError.value = getApiErrorDetails(error, '称重图片读取失败,请稍后重试')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
previewLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 20:07:37 +08:00
|
|
|
|
async function removeResult(row: WeightResultResource) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await ElMessageBox.confirm(
|
|
|
|
|
|
row.recognitionStatus === 'CONFIRMED'
|
2026-09-04 12:18:39 +08:00
|
|
|
|
? '删除后将重新计算货物总重量;对应库位仍为本票保留,不会释放给其他订单。确认删除这张称重图片吗?'
|
|
|
|
|
|
: '对应库位仍为本票保留,不会释放给其他订单。确认删除这张称重图片吗?',
|
2026-08-07 20:07:37 +08:00
|
|
|
|
'删除称重图片',
|
|
|
|
|
|
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' },
|
|
|
|
|
|
)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const key = actionKey('delete', row.id)
|
|
|
|
|
|
activeAction.value = key
|
|
|
|
|
|
try {
|
|
|
|
|
|
await deleteWeightResult(customerId.value, props.inboundId, row.id, row.version)
|
|
|
|
|
|
if (previewResultId.value === row.id) previewOpen.value = false
|
|
|
|
|
|
ElMessage.success('称重图片已删除')
|
|
|
|
|
|
await loadWorkspace()
|
2026-08-14 14:50:44 +08:00
|
|
|
|
emitWorkspaceChanged()
|
2026-08-07 20:07:37 +08:00
|
|
|
|
} catch (error) {
|
2026-08-14 19:59:32 +08:00
|
|
|
|
showActionError(error, '称重图片删除失败,请刷新后重试')
|
2026-08-07 20:07:37 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
activeAction.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-04 21:01:39 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => [customerId.value, props.inboundId, canView.value] as const,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
++loadSequence
|
|
|
|
|
|
workspace.value = null
|
2026-08-14 17:38:06 +08:00
|
|
|
|
palletOptions.value = []
|
|
|
|
|
|
selectedPalletId.value = ''
|
2026-09-01 11:10:22 +08:00
|
|
|
|
palletKeyword.value = ''
|
2026-08-14 17:38:06 +08:00
|
|
|
|
palletError.value = null
|
2026-08-07 18:24:05 +08:00
|
|
|
|
weightDrafts.value = {}
|
2026-08-04 21:01:39 +08:00
|
|
|
|
loadError.value = null
|
|
|
|
|
|
previewOpen.value = false
|
|
|
|
|
|
releasePreview()
|
|
|
|
|
|
void loadWorkspace()
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-27 11:08:03 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => [customerId.value, props.inboundId, canLoadPalletOptions.value] as const,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
cancelPalletOptionsLoad()
|
|
|
|
|
|
palletOptions.value = []
|
|
|
|
|
|
selectedPalletId.value = ''
|
2026-09-01 11:10:22 +08:00
|
|
|
|
palletKeyword.value = ''
|
2026-08-27 11:08:03 +08:00
|
|
|
|
palletError.value = null
|
|
|
|
|
|
if (canLoadPalletOptions.value) void loadPalletOptions()
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-08 11:05:45 +08:00
|
|
|
|
watch(previewOpen, (open) => {
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
releasePreview()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-08-27 11:08:03 +08:00
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
cancelPalletOptionsLoad()
|
|
|
|
|
|
palletOptions.value = []
|
|
|
|
|
|
selectedPalletId.value = ''
|
|
|
|
|
|
releasePreview()
|
|
|
|
|
|
})
|
2026-08-04 21:01:39 +08:00
|
|
|
|
|
|
|
|
|
|
defineExpose({ reload: loadWorkspace })
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<section class="weight-workspace" aria-labelledby="weight-ocr-title">
|
|
|
|
|
|
<header class="weight-heading">
|
|
|
|
|
|
<div>
|
2026-08-07 20:07:37 +08:00
|
|
|
|
<h3 id="weight-ocr-title"><Scale :size="18" aria-hidden="true" />称重图片</h3>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="weight-heading__actions">
|
2026-08-18 11:33:01 +08:00
|
|
|
|
<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>
|
2026-08-14 14:50:44 +08:00
|
|
|
|
</div>
|
2026-08-14 17:38:06 +08:00
|
|
|
|
<div class="pallet-picker">
|
2026-08-27 11:08:03 +08:00
|
|
|
|
<el-select-v2
|
|
|
|
|
|
v-if="canEdit && !reviewMode"
|
2026-08-14 17:38:06 +08:00
|
|
|
|
v-model="selectedPalletId"
|
2026-08-27 11:08:03 +08:00
|
|
|
|
:options="virtualPalletOptions"
|
2026-08-14 17:38:06 +08:00
|
|
|
|
aria-label="选择称重托盘"
|
|
|
|
|
|
placeholder="选择称重托盘"
|
|
|
|
|
|
filterable
|
2026-09-01 11:10:22 +08:00
|
|
|
|
remote
|
|
|
|
|
|
:remote-method="searchPalletOptions"
|
2026-08-14 17:38:06 +08:00
|
|
|
|
:loading="palletLoading"
|
|
|
|
|
|
:disabled="loading || uploading || !editable || !canEdit || Boolean(palletError)"
|
2026-08-27 11:08:03 +08:00
|
|
|
|
/>
|
2026-08-14 17:38:06 +08:00
|
|
|
|
</div>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
<input
|
2026-08-18 11:33:01 +08:00
|
|
|
|
v-if="!reviewMode"
|
2026-08-04 21:01:39 +08:00
|
|
|
|
ref="fileInput"
|
|
|
|
|
|
class="visually-hidden-input"
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
accept="image/jpeg,image/png,image/bmp"
|
|
|
|
|
|
aria-label="选择称重图片"
|
|
|
|
|
|
@change="handleFileChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<el-button
|
2026-08-18 11:33:01 +08:00
|
|
|
|
v-if="canEdit && !reviewMode"
|
2026-08-04 21:01:39 +08:00
|
|
|
|
type="primary"
|
|
|
|
|
|
:loading="uploading"
|
2026-08-14 17:38:06 +08:00
|
|
|
|
:disabled="loading || palletLoading || !editable || !selectedPalletId || Boolean(palletError)"
|
2026-08-04 21:01:39 +08:00
|
|
|
|
@click="chooseFile"
|
|
|
|
|
|
><ImagePlus :size="16" />上传称重图片</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<el-alert v-if="!canView" type="warning" :closable="false" show-icon>
|
|
|
|
|
|
当前账号缺少入库测量查看权限。
|
|
|
|
|
|
</el-alert>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<LoadErrorState
|
|
|
|
|
|
v-if="loadError"
|
|
|
|
|
|
title="称重数据加载失败"
|
|
|
|
|
|
:message="loadError.message"
|
|
|
|
|
|
:loading="loading"
|
|
|
|
|
|
compact
|
|
|
|
|
|
@retry="loadWorkspace"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #details><ApiErrorMeta :details="loadError" /></template>
|
|
|
|
|
|
</LoadErrorState>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else>
|
2026-09-04 12:18:39 +08:00
|
|
|
|
<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>
|
2026-08-14 17:38:06 +08:00
|
|
|
|
<LoadErrorState
|
|
|
|
|
|
v-if="palletError"
|
|
|
|
|
|
title="可选托盘加载失败"
|
|
|
|
|
|
:message="palletError.message"
|
|
|
|
|
|
:loading="palletLoading"
|
|
|
|
|
|
compact
|
|
|
|
|
|
class="weight-action-error"
|
2026-09-01 11:10:22 +08:00
|
|
|
|
@retry="reloadPalletOptions"
|
2026-08-14 17:38:06 +08:00
|
|
|
|
>
|
|
|
|
|
|
<template #details><ApiErrorMeta :details="palletError" /></template>
|
|
|
|
|
|
</LoadErrorState>
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
v-else-if="!palletLoading && palletOptions.length === 0 && editable && canEdit"
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
class="weight-action-error"
|
|
|
|
|
|
title="当前仓库暂无可选托盘,暂时不能上传称重图片。"
|
|
|
|
|
|
>
|
2026-09-01 11:10:22 +08:00
|
|
|
|
<el-button size="small" @click="reloadPalletOptions"><RefreshCw :size="14" />重新加载</el-button>
|
2026-08-14 17:38:06 +08:00
|
|
|
|
</el-alert>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
<div v-loading="loading" class="weight-list" aria-live="polite">
|
|
|
|
|
|
<div v-if="!loading && rows.length === 0" class="weight-empty">
|
|
|
|
|
|
<strong>尚未上传称重图片</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-07 20:07:37 +08:00
|
|
|
|
<div v-else class="weight-grid">
|
|
|
|
|
|
<article v-for="(row, index) in rows" :key="row.id" class="weight-card">
|
|
|
|
|
|
<WeightImageThumbnail
|
|
|
|
|
|
:customer-id="customerId"
|
|
|
|
|
|
:inbound-id="inboundId"
|
|
|
|
|
|
:file-id="row.fileId"
|
|
|
|
|
|
:alt="`称重图片 ${index + 1}`"
|
|
|
|
|
|
@open="openPreview(row)"
|
|
|
|
|
|
/>
|
2026-08-18 11:33:01 +08:00
|
|
|
|
<el-tooltip v-if="canEdit && editable && !reviewMode" content="删除图片" placement="top">
|
2026-08-07 20:07:37 +08:00
|
|
|
|
<el-button
|
2026-08-12 18:00:26 +08:00
|
|
|
|
plain
|
2026-08-07 20:07:37 +08:00
|
|
|
|
type="danger"
|
|
|
|
|
|
class="weight-delete"
|
|
|
|
|
|
:aria-label="`删除称重图片 ${index + 1}`"
|
|
|
|
|
|
:loading="isActionLoading('delete', row.id)"
|
|
|
|
|
|
:disabled="Boolean(activeAction)"
|
|
|
|
|
|
@click="removeResult(row)"
|
|
|
|
|
|
><Trash2 :size="16" /></el-button>
|
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
|
<div class="weight-card__body">
|
2026-09-04 12:18:39 +08:00
|
|
|
|
<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>
|
2026-08-07 20:07:37 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="weight-card__value"
|
|
|
|
|
|
:aria-label="`查看称重图片 ${index + 1},重量 ${cardWeight(row)}`"
|
|
|
|
|
|
@click="openPreview(row)"
|
|
|
|
|
|
><strong>{{ cardWeight(row) }}</strong></button>
|
2026-08-14 17:38:06 +08:00
|
|
|
|
<dl class="weight-card__facts">
|
2026-09-01 11:10:22 +08:00
|
|
|
|
<div><dt>托盘编码</dt><dd :title="row.palletCode">{{ row.palletCode || '未记录编码' }}</dd></div>
|
2026-08-17 18:00:42 +08:00
|
|
|
|
<div><dt>托盘类型</dt><dd>{{ row.palletType || '未记录类型' }}</dd></div>
|
|
|
|
|
|
<div><dt>服务端确认毛重</dt><dd>{{ formatWeight(row.grossWeightKg) }}</dd></div>
|
2026-08-14 17:38:06 +08:00
|
|
|
|
<div><dt>托盘皮重</dt><dd>{{ formatWeight(row.palletTareWeightKg) }}</dd></div>
|
2026-08-17 18:00:42 +08:00
|
|
|
|
<div><dt>货物重量</dt><dd>{{ formatWeight(row.netWeightKg) }}</dd></div>
|
2026-08-14 17:38:06 +08:00
|
|
|
|
</dl>
|
2026-08-18 11:33:01 +08:00
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="reviewCorrectionEnabled && row.recognitionStatus === 'CONFIRMED' && canEdit && canConfirm"
|
|
|
|
|
|
class="weight-card__correct"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
plain
|
|
|
|
|
|
@click="openPreview(row)"
|
|
|
|
|
|
>修改含托盘毛重</el-button>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
</div>
|
2026-08-07 20:07:37 +08:00
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<AppDialog
|
|
|
|
|
|
v-model="previewOpen"
|
2026-08-07 20:07:37 +08:00
|
|
|
|
title="确认称重图片"
|
2026-08-04 21:01:39 +08:00
|
|
|
|
:loading="previewLoading"
|
2026-08-07 20:07:37 +08:00
|
|
|
|
:confirm-loading="Boolean(previewResult && isActionLoading('confirm', previewResult.id))"
|
2026-08-08 11:05:45 +08:00
|
|
|
|
:confirm-disabled="previewConfirmDisabled"
|
2026-08-18 11:33:01 +08:00
|
|
|
|
:show-confirm="Boolean(previewResult && previewCanConfirm && (previewResult.recognitionStatus !== 'CONFIRMED' || reviewCorrectionEnabled))"
|
|
|
|
|
|
:confirm-text="previewConfirmText"
|
2026-08-07 20:07:37 +08:00
|
|
|
|
confirm-type="success"
|
2026-08-14 19:59:32 +08:00
|
|
|
|
width="min(640px, calc(100vw - 28px))"
|
2026-08-07 20:07:37 +08:00
|
|
|
|
@confirm="previewResult && confirmResult(previewResult, true)"
|
2026-08-04 21:01:39 +08:00
|
|
|
|
>
|
|
|
|
|
|
<LoadErrorState
|
|
|
|
|
|
v-if="previewError"
|
|
|
|
|
|
title="图片读取失败"
|
|
|
|
|
|
:message="previewError.message"
|
|
|
|
|
|
:show-retry="false"
|
|
|
|
|
|
compact
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #details><ApiErrorMeta :details="previewError" /></template>
|
|
|
|
|
|
</LoadErrorState>
|
2026-08-07 20:07:37 +08:00
|
|
|
|
<template v-else-if="previewResult">
|
|
|
|
|
|
<img v-if="previewUrl" class="weight-preview" :src="previewUrl" alt="当前入库单的称重图片" />
|
2026-08-14 19:59:32 +08:00
|
|
|
|
<div class="weight-key-facts" aria-label="称重关键数据">
|
2026-08-17 18:00:42 +08:00
|
|
|
|
<div><span>托盘类型</span><strong>{{ previewResult.palletType || '未记录类型' }}</strong></div>
|
2026-08-14 19:59:32 +08:00
|
|
|
|
<div><span>当前托盘皮重</span><strong>{{ formatWeight(previewResult.palletTareWeightKg) }}</strong></div>
|
|
|
|
|
|
<div><span>OCR 识别毛重</span><strong>{{ formatWeight(previewResult.ocrWeightValue, previewResult.ocrWeightUnit || 'KG') }}</strong></div>
|
2026-08-07 20:07:37 +08:00
|
|
|
|
</div>
|
2026-08-14 19:59:32 +08:00
|
|
|
|
<div v-if="weightDrafts[previewResult.id]" class="preview-weight">
|
2026-08-07 20:07:37 +08:00
|
|
|
|
<label>
|
2026-08-14 17:38:06 +08:00
|
|
|
|
<span>确认含托盘毛重</span>
|
2026-08-07 20:07:37 +08:00
|
|
|
|
<el-input-number
|
|
|
|
|
|
v-model="weightDrafts[previewResult.id].value"
|
|
|
|
|
|
aria-label="当前图片确认重量"
|
|
|
|
|
|
:min="minimumWeight[weightDrafts[previewResult.id].unit]"
|
|
|
|
|
|
:max="maximumSafeWeight"
|
|
|
|
|
|
:precision="3"
|
|
|
|
|
|
:controls="false"
|
2026-08-18 11:33:01 +08:00
|
|
|
|
:disabled="Boolean(activeAction) || !previewInputEditable"
|
2026-08-07 20:07:37 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<el-segmented
|
|
|
|
|
|
v-model="weightDrafts[previewResult.id].unit"
|
|
|
|
|
|
aria-label="当前图片重量单位"
|
|
|
|
|
|
:options="['KG', 'LB']"
|
2026-08-18 11:33:01 +08:00
|
|
|
|
:disabled="Boolean(activeAction) || !previewInputEditable"
|
2026-08-07 20:07:37 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-08-18 11:33:01 +08:00
|
|
|
|
<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>
|
2026-08-07 20:07:37 +08:00
|
|
|
|
</template>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
</AppDialog>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.weight-workspace { min-width: 0; color: var(--el-text-color-primary); }
|
2026-08-18 16:09:43 +08:00
|
|
|
|
.weight-heading { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding-bottom: 10px; border-bottom: 1px solid var(--cc-border); }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
.weight-heading h3 { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 15px; }
|
|
|
|
|
|
.weight-heading h3 svg { color: var(--cc-primary); }
|
2026-08-18 16:09:43 +08:00
|
|
|
|
.weight-heading__actions { display: flex; flex: 0 0 auto; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 16px; }
|
2026-08-17 18:32:41 +08:00
|
|
|
|
.pallet-picker { width: clamp(360px, 36vw, 520px); min-width: 0; }
|
|
|
|
|
|
.pallet-picker :deep(.el-select) { width: 100%; }
|
2026-08-18 16:09:43 +08:00
|
|
|
|
.weight-summary { display: flex; align-items: baseline; gap: 6px; white-space: nowrap; }
|
|
|
|
|
|
.weight-summary-group { display: flex; align-items: center; flex-wrap: wrap; gap: 20px; }
|
2026-08-14 14:50:44 +08:00
|
|
|
|
.weight-summary span { color: var(--cc-muted); font-size: 12px; }
|
2026-08-18 16:09:43 +08:00
|
|
|
|
.weight-summary strong { color: #067647; font-size: 16px; }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
.visually-hidden-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
2026-09-04 12:18:39 +08:00
|
|
|
|
.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; }
|
2026-08-14 19:59:32 +08:00
|
|
|
|
.weight-list { min-height: 0; margin-top: 14px; }
|
2026-08-17 18:32:41 +08:00
|
|
|
|
.weight-empty { display: flex; min-height: 0; align-items: center; gap: 10px; padding: 4px 0 0; color: var(--cc-muted); }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
.weight-empty strong { color: #344054; font-size: 14px; }
|
2026-08-07 20:07:37 +08:00
|
|
|
|
.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; }
|
2026-09-04 12:18:39 +08:00
|
|
|
|
.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; }
|
2026-08-07 20:07:37 +08:00
|
|
|
|
.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; }
|
|
|
|
|
|
.weight-card__value:focus-visible { outline: 2px solid var(--cc-primary); outline-offset: 4px; }
|
2026-08-14 17:38:06 +08:00
|
|
|
|
.weight-card__facts { display: grid; gap: 6px; margin: 10px 0 0; padding-top: 10px; border-top: 1px solid var(--cc-border); }
|
|
|
|
|
|
.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; }
|
2026-08-18 11:33:01 +08:00
|
|
|
|
.weight-card__correct { width: 100%; margin: 12px 0 0; }
|
2026-08-14 19:59:32 +08:00
|
|
|
|
.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; }
|
|
|
|
|
|
.weight-key-facts span { color: var(--cc-muted); font-size: 12px; }
|
|
|
|
|
|
.weight-key-facts strong { color: #101828; font-size: 18px; }
|
2026-08-07 20:07:37 +08:00
|
|
|
|
.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%; }
|
2026-08-18 11:33:01 +08:00
|
|
|
|
.correction-reason { display: grid; gap: 6px; margin-top: 14px; color: #475467; font-size: 12px; }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
|
|
|
|
|
|
@media (max-width: 720px) {
|
|
|
|
|
|
.weight-heading { flex-direction: column; }
|
2026-08-14 14:50:44 +08:00
|
|
|
|
.weight-heading__actions { width: 100%; align-items: stretch; flex-direction: column; }
|
2026-08-14 17:38:06 +08:00
|
|
|
|
.pallet-picker { width: 100%; min-width: 0; }
|
|
|
|
|
|
.pallet-picker :deep(.el-select) { width: 100%; }
|
2026-08-14 14:50:44 +08:00
|
|
|
|
.weight-heading__actions :deep(.el-button) { width: 100%; min-height: 44px; }
|
2026-08-18 11:33:01 +08:00
|
|
|
|
.weight-summary-group { width: 100%; justify-content: space-between; }
|
2026-09-04 12:18:39 +08:00
|
|
|
|
.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; }
|
2026-08-07 20:07:37 +08:00
|
|
|
|
.weight-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 360px) {
|
2026-08-14 19:59:32 +08:00
|
|
|
|
.weight-grid { grid-template-columns: 1fr; }
|
2026-08-07 20:07:37 +08:00
|
|
|
|
.preview-weight { grid-template-columns: 1fr; }
|
2026-08-14 19:59:32 +08:00
|
|
|
|
.weight-key-facts { grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
|
|
|
|
.weight-key-facts strong { font-size: 15px; }
|
2026-08-14 17:38:06 +08:00
|
|
|
|
.weight-card__facts div { align-items: flex-start; flex-direction: column; gap: 2px; }
|
|
|
|
|
|
.weight-card__facts dd { text-align: left; }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|