131 lines
8.6 KiB
Vue
131 lines
8.6 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { useAuthStore } from '@/modules/auth/public'
|
||
import { confirmDiscardCompletion, confirmRelabelCompletion, confirmTransferMeasurement, downloadDiscardPhoto, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, getDiscard, getRelabel, getTransfer, type DiscardPhoto } from '../api/warehouseServices'
|
||
|
||
const props = defineProps<{ modelValue: boolean; kind: 'disposal' | 'labeling' | 'transfer'; documentNo: string }>()
|
||
const emit = defineEmits<{ 'update:modelValue': [value: boolean]; confirmed: [documentNo: string] }>()
|
||
const auth = useAuthStore()
|
||
const title = computed(() => props.kind === 'transfer' ? '确认测量数据' : props.kind === 'disposal' ? '废弃确认' : '贴标完成确认')
|
||
interface MeasurementEvidence {
|
||
status?: string
|
||
customerConfirmedAt?: string
|
||
cartons?: Array<{ cartonMark: string; status: string; length?: number; width?: number; height?: number; weight?: number }>
|
||
pallets?: Array<{ palletNo?: string; sequenceNo?: number; cartonCount?: number; lengthCm?: number; widthCm?: number; heightCm?: number; grossWeightKg?: number; volumeCbm?: number }>
|
||
measurementPhotos?: Array<{ fileId: string | number }>
|
||
}
|
||
const measurement = ref<MeasurementEvidence | null>(null)
|
||
const loading = ref(false)
|
||
const busy = ref(false)
|
||
const error = ref('')
|
||
const fileIds = ref<string[]>([])
|
||
const urls = ref<Record<string, string>>({})
|
||
const failed = ref<Record<string, boolean>>({})
|
||
const photoLoading = ref<Record<string, boolean>>({})
|
||
const checked = ref(false)
|
||
const eligible = ref(false)
|
||
let generation = 0
|
||
const previewUrls = computed(() => fileIds.value.map(id => urls.value[id]).filter(Boolean))
|
||
const canConfirm = computed(() => eligible.value && checked.value && !loading.value && !busy.value && !error.value
|
||
&& fileIds.value.length > 0 && fileIds.value.every(id => Boolean(urls.value[id]))
|
||
&& auth.hasPermission(`merchant:overseas-warehouse:${props.kind === 'transfer' ? 'transfer' : props.kind === 'disposal' ? 'discard' : 'relabel'}:confirm`))
|
||
function clear() {
|
||
generation++
|
||
Object.values(urls.value).forEach(URL.revokeObjectURL)
|
||
urls.value = {}; failed.value = {}; photoLoading.value = {}; fileIds.value = []; checked.value = false; eligible.value = false
|
||
measurement.value = null
|
||
}
|
||
async function loadPhoto(fileId: string) {
|
||
if (photoLoading.value[fileId]) return
|
||
const currentGeneration = generation; const no = props.documentNo; const kind = props.kind
|
||
photoLoading.value[fileId] = true; failed.value[fileId] = false
|
||
try {
|
||
const blob = kind === 'transfer' ? await downloadTransferMeasurementPhoto(no, fileId) : kind === 'disposal' ? await downloadDiscardPhoto(no, fileId) : await downloadRelabelCompletionPhoto(no, fileId)
|
||
if (generation !== currentGeneration || !props.modelValue) return
|
||
if (urls.value[fileId]) URL.revokeObjectURL(urls.value[fileId])
|
||
urls.value[fileId] = URL.createObjectURL(blob)
|
||
} catch { if (generation === currentGeneration) failed.value[fileId] = true }
|
||
finally { if (generation === currentGeneration) photoLoading.value[fileId] = false }
|
||
}
|
||
async function loadEvidence() {
|
||
clear(); error.value = ''; loading.value = true
|
||
const currentGeneration = generation
|
||
try {
|
||
if (props.kind === 'transfer') {
|
||
const detail = await getTransfer(props.documentNo) as MeasurementEvidence
|
||
if (generation !== currentGeneration) return
|
||
measurement.value = detail
|
||
eligible.value = detail.status === 'WAIT_CUSTOMER_CONFIRMATION' && !detail.customerConfirmedAt
|
||
fileIds.value = (detail.measurementPhotos || []).map(photo => String(photo.fileId))
|
||
} else if (props.kind === 'disposal') {
|
||
const detail = await getDiscard(props.documentNo) as { summary?: { status?: string }; photos?: DiscardPhoto[] }
|
||
if (generation !== currentGeneration) return
|
||
eligible.value = detail.summary?.status === 'PDA_COMPLETED'
|
||
fileIds.value = (detail.photos || []).map(photo => String(photo.fileId))
|
||
} else {
|
||
const detail = await getRelabel(props.documentNo)
|
||
if (generation !== currentGeneration) return
|
||
eligible.value = detail.application.status === 'CUSTOMER_CONFIRM_PENDING' && !detail.application.customerConfirmedAt
|
||
fileIds.value = (detail.completionEvidence?.photoFileIds || []).map(String)
|
||
}
|
||
fileIds.value = [...new Set(fileIds.value)]
|
||
if (!eligible.value) error.value = '当前单据已确认或尚未完成 PDA 作业,请刷新列表核对。'
|
||
else if (!fileIds.value.length) error.value = props.kind === 'transfer' ? '暂无 PDA 测量打托照片,暂不能确认。' : '暂无 PDA 完工作业照片,暂不能确认。'
|
||
await Promise.all(fileIds.value.map(loadPhoto))
|
||
} catch (e) { if (generation === currentGeneration) error.value = e instanceof Error ? e.message : '作业照片加载失败' }
|
||
finally { if (generation === currentGeneration) loading.value = false }
|
||
}
|
||
async function submit() {
|
||
if (!canConfirm.value) return
|
||
busy.value = true
|
||
try {
|
||
if (props.kind === 'transfer') await confirmTransferMeasurement(props.documentNo)
|
||
else if (props.kind === 'disposal') await confirmDiscardCompletion(props.documentNo)
|
||
else await confirmRelabelCompletion(props.documentNo)
|
||
ElMessage.success(props.kind === 'transfer' ? '仓库实测数据已确认' : props.kind === 'disposal' ? '废弃已确认,单据已完结' : '贴标已确认,订单进入后续出库流程')
|
||
emit('confirmed', props.documentNo)
|
||
emit('update:modelValue', false)
|
||
} catch (e) { ElMessage.error(e instanceof Error ? e.message : '确认失败,请重试') }
|
||
finally { busy.value = false }
|
||
}
|
||
watch(() => [props.modelValue, props.kind, props.documentNo], () => { if (props.modelValue) void loadEvidence(); else clear() }, { immediate: true })
|
||
onBeforeUnmount(clear)
|
||
</script>
|
||
|
||
<template>
|
||
<el-dialog :model-value="modelValue" :title="title" width="760px" :close-on-click-modal="false" :close-on-press-escape="!busy" :show-close="!busy" @update:model-value="!busy && emit('update:modelValue', $event)">
|
||
<p>单号:{{ documentNo }}</p>
|
||
<p>{{ kind === 'transfer' ? '请核对 PDA 回传的箱数、重量、打托数据及现场照片,确认无误后再提交。' : '请核对以下 PDA 作业照片,确认作业结果无误后再提交。' }}</p>
|
||
<template v-if="kind === 'transfer' && measurement">
|
||
<p>实际签收箱数:{{ measurement.cartons?.filter(carton => carton.status === 'COUNTED').length ?? 0 }} 箱;托盘数:{{ measurement.pallets?.length ?? 0 }} 托</p>
|
||
<el-table :data="measurement.pallets || []" max-height="220" size="small" border empty-text="暂无 PDA 打托测量数据">
|
||
<el-table-column label="托盘号" min-width="180"><template #default="{ row }">{{ row.palletNo || `第${row.sequenceNo || '—'}托` }}</template></el-table-column>
|
||
<el-table-column prop="cartonCount" label="箱数" width="65" />
|
||
<el-table-column prop="grossWeightKg" label="毛重(kg)" width="95" />
|
||
<el-table-column prop="volumeCbm" label="体积(m³)" width="95" />
|
||
</el-table>
|
||
</template>
|
||
<p v-if="loading">正在读取作业照片…</p>
|
||
<el-alert v-if="error" :title="error" type="error" :closable="false" />
|
||
<el-button v-if="error" :disabled="loading || busy" @click="loadEvidence">重新读取</el-button>
|
||
<div class="completion-photo-grid">
|
||
<div v-for="(fileId, index) in fileIds" :key="fileId">
|
||
<el-image v-if="urls[fileId]" :src="urls[fileId]" :preview-src-list="previewUrls" :initial-index="previewUrls.indexOf(urls[fileId])" fit="contain" preview-teleported />
|
||
<span v-else-if="photoLoading[fileId]">照片 {{ index + 1 }} 加载中…</span>
|
||
<el-button v-else :disabled="busy" @click="loadPhoto(fileId)">{{ failed[fileId] ? '照片加载失败,点击重试' : '加载照片' }}</el-button>
|
||
</div>
|
||
</div>
|
||
<el-checkbox v-model="checked" :disabled="loading || busy || !!error || previewUrls.length !== fileIds.length">{{ kind === 'transfer' ? '我已核对 PDA 实测数据及现场照片,确认无误' : '我已核对 PDA 作业照片,确认作业结果无误' }}</el-checkbox>
|
||
<template #footer>
|
||
<el-button :disabled="busy" @click="emit('update:modelValue', false)">取消</el-button>
|
||
<el-button type="primary" :loading="busy" :disabled="!canConfirm" @click="submit">{{ kind === 'transfer' ? '确认数据' : kind === 'disposal' ? '确认废弃完结' : '确认贴标完成' }}</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.completion-photo-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 16px 0; }
|
||
.completion-photo-grid .el-image { width: 100%; height: 180px; }
|
||
</style>
|