106 lines
6.2 KiB
Vue
106 lines
6.2 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, downloadDiscardPhoto, downloadRelabelCompletionPhoto, getDiscard, getRelabel, type DiscardPhoto } from '../api/warehouseServices'
|
|||
|
|
|
|||
|
|
const props = defineProps<{ modelValue: boolean; kind: 'disposal' | 'labeling'; documentNo: string }>()
|
|||
|
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean]; confirmed: [documentNo: string] }>()
|
|||
|
|
const auth = useAuthStore()
|
|||
|
|
const title = computed(() => props.kind === 'disposal' ? '废弃确认' : '贴标完成确认')
|
|||
|
|
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 === '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
|
|||
|
|
}
|
|||
|
|
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 === '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 === '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 = '暂无 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 === 'disposal') await confirmDiscardCompletion(props.documentNo)
|
|||
|
|
else await confirmRelabelCompletion(props.documentNo)
|
|||
|
|
ElMessage.success(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>请核对以下 PDA 作业照片,确认作业结果无误后再提交。</p>
|
|||
|
|
<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">我已核对 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 === '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>
|