feat(admin): synchronize fleet photos and multi-image loading documents
This commit is contained in:
@@ -9,12 +9,13 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { getDispatchEvidence, getDispatchEvidenceContent, uploadDispatchEvidence } from '../api/dispatches'
|
||||
import { downloadDriverLicense } from '@/modules/overseas-master-data'
|
||||
import { downloadDriverLicense, getResourcePhoto, downloadResourcePhoto, type ResourcePhotoKind } from '@/modules/overseas-master-data'
|
||||
import { DispatchInputError, type DispatchEvidenceFile, type DispatchEvidenceKind } from '../dispatchContracts'
|
||||
const props = defineProps<{ dispatchNo: string; kind: DispatchEvidenceKind; disabled?: boolean; readonly?: boolean; photoCard?: boolean; label?: string; saveHint?: string; referenceIds?: string[]; masterLicense?: { driverId: string; fileId: string } | null }>()
|
||||
const props = defineProps<{ dispatchNo: string; kind: DispatchEvidenceKind; disabled?: boolean; readonly?: boolean; photoCard?: boolean; label?: string; saveHint?: string; referenceIds?: string[]; masterResource?: { kind: ResourcePhotoKind; id: string } | null; masterLicense?: { driverId: string; fileId: string } | null }>()
|
||||
const ids = defineModel<string[]>({ required: true })
|
||||
const allIds = computed(() => [...new Set([...(props.referenceIds || []), ...ids.value])])
|
||||
const emit = defineEmits<{ busy: [value: boolean] }>()
|
||||
const profilePhotoId = ref<string | null>(null)
|
||||
const allIds = computed(() => [...new Set([...(props.referenceIds || []), ...ids.value, ...(profilePhotoId.value ? [profilePhotoId.value] : [])])])
|
||||
const emit = defineEmits<{ busy: [value: boolean]; available: [value: boolean] }>()
|
||||
const metadata = ref<Record<string, DispatchEvidenceFile>>({}), error = ref<ApiErrorDetails | null>(null), uploading = ref(false), input = ref<HTMLInputElement | null>(null)
|
||||
const imageOpen = ref(false), imageUrl = ref(''), imageName = ref(''), previewLoading = ref(false)
|
||||
const thumbnails = ref<Record<string, string>>({})
|
||||
@@ -23,8 +24,8 @@ const previewId = ref(''), imageDecoding = ref(false), imageFailure = ref('')
|
||||
const previewBusy = computed(() => previewLoading.value || imageDecoding.value)
|
||||
const label = computed(() => props.label || (props.kind === 'VEHICLE' ? '本趟车辆证据' : props.kind === 'DRIVER_PHOTO' ? '司机照片' : '本趟司机证照'))
|
||||
const types = ['application/pdf', 'image/jpeg', 'image/png'], maximum = 10 * 1024 * 1024, urls = new Set<string>()
|
||||
const uploadTypes = computed(() => (props.photoCard || ['DRIVER_PHOTO', 'EXCEPTION'].includes(props.kind)) ? types.slice(1) : types)
|
||||
const uploadFormats = computed(() => ['DRIVER_PHOTO', 'EXCEPTION'].includes(props.kind) ? 'JPG、PNG' : 'PDF、JPG、PNG')
|
||||
const uploadTypes = computed(() => (props.kind !== 'DRIVER' && (props.photoCard || ['DRIVER_PHOTO', 'EXCEPTION'].includes(props.kind))) ? types.slice(1) : types)
|
||||
const uploadFormats = computed(() => uploadTypes.value.includes('application/pdf') ? 'PDF、JPG、PNG' : 'JPG、PNG')
|
||||
let generation = 0, previewGeneration = 0, replacing: string | null = null, pendingPopup: Window | null = null
|
||||
function valid(value: DispatchEvidenceFile, expected?: string) {
|
||||
if (!value || !/^[1-9]\d*$/.test(String(value.fileId)) || typeof value.fileId === 'number' && !Number.isSafeInteger(value.fileId) || expected && String(value.fileId) !== expected || !value.fileName?.trim() || !types.includes(value.contentType) || !Number.isSafeInteger(value.sizeBytes) || value.sizeBytes <= 0 || value.sizeBytes > maximum) throw new DispatchInputError('附件元数据未确认本车有效文件,请重新读取')
|
||||
@@ -44,13 +45,30 @@ function imageFailed(event: Event) {
|
||||
imageFailure.value = '图片无法解码或加载,请重新预览;若仍失败,请核对原附件是否损坏。'
|
||||
}
|
||||
async function load() {
|
||||
const current = ++generation, no = props.dispatchNo, kind = props.kind, values = [...allIds.value]
|
||||
const current = ++generation, no = props.dispatchNo, kind = props.kind, values = [...new Set([...(props.referenceIds || []), ...ids.value])]
|
||||
for (const url of Object.values(thumbnails.value)) { URL.revokeObjectURL(url); urls.delete(url) }
|
||||
thumbnails.value = {}
|
||||
closePreview(); metadata.value = {}; error.value = null; masterBlob = null
|
||||
closePreview(); metadata.value = {}; error.value = null; masterBlob = null; profilePhotoId.value = null; emit('available', false)
|
||||
if (!no) return
|
||||
// 未保存且没有本趟照片时展示档案图片;保存由服务端复制,不提交跨归属文件ID。
|
||||
if (!values.length && props.masterResource) {
|
||||
const resource = props.masterResource
|
||||
try {
|
||||
const source = await getResourcePhoto(resource.kind, resource.id)
|
||||
if (current !== generation) return
|
||||
if (source.photo) {
|
||||
const blob = await downloadResourcePhoto(resource.kind, resource.id, source.photo.fileId)
|
||||
if (current !== generation) return
|
||||
const file = valid(source.photo)
|
||||
if (blob.size !== file.sizeBytes || blob.type !== file.contentType) throw new DispatchInputError('档案照片内容与元数据不一致')
|
||||
metadata.value[file.fileId] = file; masterBlob = { fileId: file.fileId, blob }
|
||||
profilePhotoId.value = file.fileId; values.push(file.fileId); emit('available', true)
|
||||
}
|
||||
} catch (cause) { if (current === generation) fail(cause, '档案照片读取失败,请重试') }
|
||||
}
|
||||
const master = props.kind === 'DRIVER' ? props.masterLicense : null
|
||||
const results = await Promise.allSettled(values.map(async id => {
|
||||
if (id === profilePhotoId.value && metadata.value[id]) return metadata.value[id]!
|
||||
// 未保存的司机主档证照按司机身份读取;本车补传仍走排车文件归属。
|
||||
if (master && master.fileId === id) {
|
||||
const blob = await downloadDriverLicense(master.driverId, id)
|
||||
@@ -89,7 +107,7 @@ async function upload(event: Event) {
|
||||
}
|
||||
function remove(id: string) { if (!props.readonly && !props.disabled && !uploading.value) ids.value = ids.value.filter(value => value !== id) }
|
||||
async function readContent(no: string, kind: DispatchEvidenceKind, id: string, file: DispatchEvidenceFile) {
|
||||
const blob = kind === 'DRIVER' && masterBlob?.fileId === id ? masterBlob.blob : await getDispatchEvidenceContent(no, kind, id)
|
||||
const blob = masterBlob?.fileId === id ? masterBlob.blob : await getDispatchEvidenceContent(no, kind, id)
|
||||
const type = blob.type.split(';')[0]?.toLowerCase()
|
||||
if (type?.includes('json')) {
|
||||
let body: Partial<{ code: string; message: string; requestId: string; traceId: string }>
|
||||
@@ -115,7 +133,7 @@ async function preview(id: string) {
|
||||
} catch (cause) { if (current === previewGeneration) { closePreview(); fail(cause, '本车附件预览失败,原引用已保留') } }
|
||||
finally { if (current === previewGeneration) previewLoading.value = false }
|
||||
}
|
||||
watch(() => JSON.stringify([props.dispatchNo, props.kind, allIds.value, props.masterLicense]), load, { immediate: true })
|
||||
watch(() => JSON.stringify([props.dispatchNo, props.kind, ids.value, props.referenceIds, props.masterLicense, props.masterResource]), load, { immediate: true })
|
||||
onBeforeUnmount(() => { generation++; closePreview(); for (const url of urls) URL.revokeObjectURL(url); urls.clear(); emit('busy', false) })
|
||||
</script>
|
||||
<template>
|
||||
@@ -130,6 +148,7 @@ onBeforeUnmount(() => { generation++; closePreview(); for (const url of urls) UR
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="evidence-photo-empty">暂无照片</div>
|
||||
<p v-if="profilePhotoId" class="density-dialog-note">已带入档案照片,保存后用于本趟排车</p>
|
||||
<figcaption>{{ label }}<InlineHelp v-if="!readonly" title="附件上传说明"><p>{{ uploadFormats }},单文件最大10MiB;{{ saveHint || '上传后保存车辆信息才完成本车引用。' }}</p></InlineHelp></figcaption>
|
||||
</figure>
|
||||
<template v-else>
|
||||
|
||||
Reference in New Issue
Block a user