Files
client-web/src/modules/overseas-warehouse/components/CompletionConfirmationDialog.vue
T

106 lines
6.2 KiB
Vue
Raw Normal View History

2026-10-15 15:45:22 +08:00
<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>