90 lines
6.5 KiB
Vue
90 lines
6.5 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { useAuthStore } from '@/core/auth/auth'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||
import { bindLoadingPhoto, getLoadingDocuments, loadingPhotoSlots, readLoadingPhoto, uploadLoadingPhoto, type LoadingDocuments, type LoadingPhotoSlot } from '../api/dispatchLoadingDocuments'
|
||
|
||
const props = defineProps<{ dispatchNo: string }>()
|
||
const emit = defineEmits<{ saved: [] }>()
|
||
const auth = useAuthStore(), access = useAccessPreviewStore()
|
||
const snapshot = ref<LoadingDocuments | null>(null), busy = ref(false), error = ref<ApiErrorDetails | null>(null)
|
||
const images = ref<Record<string, string>>({})
|
||
const actor = auth.user?.id || '', number = props.dispatchNo
|
||
const editable = computed(() => access.can(permission.dispatchEdit) && snapshot.value?.editable === true)
|
||
let active = true
|
||
function ensureScope() { if (!active || auth.user?.id !== actor || props.dispatchNo !== number) throw new Error('资料操作身份已变化,请重新打开') }
|
||
function accept(value: LoadingDocuments) { ensureScope(); if (value.dispatchNo !== number) throw new Error('装车资料与排车单不一致'); snapshot.value = value }
|
||
async function previews() {
|
||
const photos = snapshot.value?.photos || []
|
||
await Promise.allSettled(photos.map(async photo => {
|
||
if (images.value[photo.fileId]) return
|
||
const blob = await readLoadingPhoto(number, photo); ensureScope(); images.value[photo.fileId] = URL.createObjectURL(blob)
|
||
}))
|
||
}
|
||
async function load() {
|
||
if (busy.value) return
|
||
busy.value = true; error.value = null
|
||
try { accept(await getLoadingDocuments(number)); await previews() }
|
||
catch (cause) { if (active) error.value = getApiErrorDetails(cause, '装车资料读取失败') }
|
||
finally { if (active) busy.value = false }
|
||
}
|
||
async function upload(slot: LoadingPhotoSlot, event: Event) {
|
||
const input = event.target as HTMLInputElement, file = input.files?.[0]; input.value = ''
|
||
if (!file || busy.value || !editable.value) return
|
||
if (!['image/jpeg','image/png'].includes(file.type) || !file.size || file.size > 10 * 1024 * 1024) { ElMessage.warning('请选择不超过10MiB的JPG或PNG图片'); return }
|
||
busy.value = true; error.value = null
|
||
try {
|
||
ensureScope()
|
||
const photo = await uploadLoadingPhoto(number, slot, file, crypto.randomUUID()); ensureScope()
|
||
const latest = await getLoadingDocuments(number); ensureScope(); snapshot.value = latest
|
||
if (!latest.editable) throw new Error('当前状态不允许修改照片')
|
||
if (photo.slot !== slot || !photo.fileId) throw new Error('上传结果与照片类别不一致')
|
||
const result = await bindLoadingPhoto(number, slot, { recordVersion: latest.recordVersion, idempotencyKey: crypto.randomUUID(), fileId: photo.fileId }); ensureScope()
|
||
snapshot.value = result; emit('saved'); ElMessage.success('照片已保存')
|
||
await previews()
|
||
} catch (cause) { if (active) error.value = getApiErrorDetails(cause, '图片上传或保存失败,请重新选择图片') }
|
||
finally { busy.value = false }
|
||
}
|
||
onMounted(() => { void load() })
|
||
onBeforeUnmount(() => { active = false; Object.values(images.value).forEach(url => URL.revokeObjectURL(url)) })
|
||
defineExpose({ busy })
|
||
</script>
|
||
<template>
|
||
<div v-loading="busy" class="density-dialog-sections">
|
||
<p v-if="snapshot?.stage === 'LOADING'">可边装车边上传 5 张装车照片;装车确认完成且照片齐全后自动进入派送中。</p>
|
||
<p v-if="snapshot?.stage === 'AFTER_LOADING'">请上传 5 张装车照片,全部保存成功后自动进入派送中。</p>
|
||
<LoadErrorState v-if="error" compact title="装车资料操作失败" :message="error.message" :show-retry="true" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||
<table class="density-table loading-documents-table" aria-label="装车照片"><thead><tr><th class="photo-actions">操作</th><th class="photo-category">照片类别</th><th>文件名称</th><th class="photo-preview">图片预览</th><th class="photo-time">保存时间</th></tr></thead><tbody>
|
||
<tr v-for="(slot, index) in loadingPhotoSlots" :key="slot">
|
||
<td><label v-if="editable" class="photo-upload" :class="{ 'is-disabled': busy }">上传/替换<input type="file" accept="image/jpeg,image/png" :aria-label="`装车照片 ${index + 1}`" :disabled="busy" @change="upload(slot, $event)" /></label><span v-else>—</span></td>
|
||
<td>装车照片 {{ index + 1 }}</td>
|
||
<td><div v-for="photo in snapshot?.photos.filter(item => item.slot === slot) || []" :key="photo.fileId">{{ photo.fileName }}</div><span v-if="!snapshot?.photos.some(item => item.slot === slot)">未上传</span></td>
|
||
<td><template v-for="photo in snapshot?.photos.filter(item => item.slot === slot) || []" :key="photo.fileId">
|
||
<el-image v-if="images[photo.fileId]" :src="images[photo.fileId]" :preview-src-list="[images[photo.fileId]!]" fit="contain" class="loading-photo-image" preview-teleported />
|
||
<el-button v-else link :disabled="busy" @click="load">图片读取失败,点击重试</el-button>
|
||
</template><span v-if="!snapshot?.photos.some(item => item.slot === slot)">—</span></td>
|
||
<td><div v-for="photo in snapshot?.photos.filter(item => item.slot === slot) || []" :key="photo.fileId">{{ photo.savedAt?.replace('T', ' ') || '—' }}</div><span v-if="!snapshot?.photos.some(item => item.slot === slot)">—</span></td>
|
||
</tr>
|
||
</tbody></table>
|
||
</div>
|
||
</template>
|
||
<style scoped>
|
||
.loading-documents-table { table-layout: fixed; }
|
||
.loading-documents-table td { overflow-wrap: anywhere; }
|
||
.photo-actions { width: 110px; }
|
||
.photo-category { width: 120px; }
|
||
.photo-preview { width: 140px; }
|
||
.photo-time { width: 190px; }
|
||
.loading-photo-image { display: block; width: 100px; height: 64px; }
|
||
.photo-upload { position: relative; display: inline-flex; color: var(--el-color-primary); cursor: pointer; }
|
||
.photo-upload input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
|
||
.photo-upload:focus-within { outline: 2px solid var(--el-color-primary); outline-offset: 2px; }
|
||
.photo-upload.is-disabled { color: var(--el-text-color-disabled); cursor: not-allowed; }
|
||
.photo-upload.is-disabled input { cursor: not-allowed; }
|
||
</style>
|