81 lines
6.6 KiB
Vue
81 lines
6.6 KiB
Vue
<script setup lang="ts">
|
|
import '@/styles/adminWorkbench.css'
|
|
import { computed, onBeforeUnmount, ref, watch } 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 ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|
import OutsourcedBusinessPhotosDialog from './OutsourcedBusinessPhotosDialog.vue'
|
|
import { outsourcedBusinessPhotoRoles, outsourcedBusinessVersion, type OutsourcedBusinessDetail, type OutsourcedBusinessPhotoSlot } from '../outsourcedBusinessTypes'
|
|
import { getOutsourcedBusiness, getOutsourcedBusinessPhotoContent, uploadOutsourcedBusinessPhoto, bindOutsourcedBusinessPhoto } from '../api/outsourcedBusiness'
|
|
import { overseasMasterDataPermissions as permission } from '../permissions'
|
|
|
|
const open = defineModel<boolean>({ default: false })
|
|
const props = defineProps<{ detail: OutsourcedBusinessDetail | null }>()
|
|
const emit = defineEmits<{ saved: [value: OutsourcedBusinessDetail]; busy: [value: boolean] }>()
|
|
const auth = useAuthStore(), access = useAccessPreviewStore(), busy = ref(false), error = ref<ApiErrorDetails | null>(null)
|
|
const urls = ref<Record<string,string>>({}), reading = ref<string | null>(null)
|
|
const uploadable = computed(() => access.can(permission.outsourcedBusinessUpload) && props.detail?.row.actions.upload.allowed === true)
|
|
let actor = '', number = '', generation = 0
|
|
function savedPhotos(slot: OutsourcedBusinessPhotoSlot) { return props.detail?.photos.filter(photo => photo.slot === slot && photo.savedAt) || [] }
|
|
function release() { Object.values(urls.value).forEach(url => { if (url) URL.revokeObjectURL(url) }); urls.value = {} }
|
|
function ensureScope() { if (auth.user?.id !== actor || props.detail?.row.dispatchNo !== number) throw new Error('图片作业身份或排车单已变化,请重新打开') }
|
|
watch(busy, value => emit('busy', value))
|
|
watch(open, value => {
|
|
generation++; release(); reading.value = null; error.value = null
|
|
if (!value || !props.detail) return
|
|
actor = auth.user?.id || ''; number = props.detail.row.dispatchNo
|
|
}, { immediate: true })
|
|
watch(() => props.detail?.photos, () => { generation++; release(); reading.value = null })
|
|
onBeforeUnmount(() => { generation++; release() })
|
|
async function preview(slot: OutsourcedBusinessPhotoSlot, fileId: string) {
|
|
const photo = savedPhotos(slot).find(item => item.fileId === fileId), current = generation
|
|
if (!photo || reading.value) return
|
|
reading.value = fileId; error.value = null
|
|
try { const blob = await getOutsourcedBusinessPhotoContent(number, photo); if (current !== generation || !open.value) return; urls.value[fileId] = URL.createObjectURL(blob) }
|
|
catch (cause) { if (current === generation) error.value = getApiErrorDetails(cause, '图片读取失败') }
|
|
finally { if (current === generation) reading.value = null }
|
|
}
|
|
async function upload(slot: OutsourcedBusinessPhotoSlot, event: Event, replaceFileId?: string) {
|
|
const input = event.target as HTMLInputElement, file = input.files?.[0]; input.value = ''
|
|
if (!file || busy.value || !uploadable.value) return
|
|
if (!replaceFileId && savedPhotos(slot).length >= (slot.startsWith('FUEL_') ? 1 : 5)) return
|
|
if (!(['image/jpeg','image/png'].includes(file.type) || slot === 'LOADING_SCENE' && file.type === 'application/pdf') || !file.size) { error.value = { message: '请选择JPG/PNG,司机驾驶证也支持PDF' }; return }
|
|
busy.value = true; error.value = null
|
|
try {
|
|
ensureScope()
|
|
const photo = await uploadOutsourcedBusinessPhoto(number, slot, file, crypto.randomUUID()); ensureScope()
|
|
const latest = await getOutsourcedBusiness(number); ensureScope()
|
|
if (!latest.row.actions.upload.allowed) throw new Error(latest.row.actions.upload.blockReason || '当前不允许上传照片')
|
|
const result = await bindOutsourcedBusinessPhoto(number, slot, { recordVersion: outsourcedBusinessVersion(latest.row.recordVersion), idempotencyKey: crypto.randomUUID(), fileId: photo.fileId, ...(replaceFileId ? { replaceFileId } : {}) }); ensureScope()
|
|
emit('saved', result); ElMessage.success('照片已保存')
|
|
} catch (cause) { error.value = getApiErrorDetails(cause, '图片上传或保存失败,请重新选择图片') }
|
|
finally { busy.value = false }
|
|
}
|
|
function beforeClose(done: () => void) { if (!busy.value) done() }
|
|
</script>
|
|
|
|
<template>
|
|
<OutsourcedBusinessPhotosDialog v-model="open" :dispatch-no="detail?.row.dispatchNo" :busy="busy" :before-close="beforeClose">
|
|
<template #state><LoadErrorState v-if="error" title="照片作业未完成" :message="error.message" :show-retry="false"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState><p v-if="!uploadable">{{ detail?.row.actions.upload.blockReason || '当前仅可查看照片' }}</p></template>
|
|
<template v-for="role in outsourcedBusinessPhotoRoles" :key="role.slot" #[role.viewSlot]>
|
|
<div v-for="photo in savedPhotos(role.slot)" :key="photo.fileId">
|
|
<a v-if="urls[photo.fileId] && photo.contentType === 'application/pdf'" :href="urls[photo.fileId]" target="_blank" rel="noopener noreferrer">查看驾驶证 PDF</a>
|
|
<img v-else-if="urls[photo.fileId]" :src="urls[photo.fileId]" :alt="role.label" class="outsourced-business-photo" />
|
|
<p>{{ photo.fileName }} · 已保存</p><el-button :loading="reading === photo.fileId" :disabled="Boolean(reading)" @click="preview(role.slot, photo.fileId)">查看资料</el-button>
|
|
<label v-if="uploadable" class="outsourced-business-upload">替换资料<input type="file" :accept="role.slot === 'LOADING_SCENE' ? 'image/jpeg,image/png,application/pdf' : 'image/jpeg,image/png'" :disabled="busy" @change="upload(role.slot, $event, photo.fileId)" /></label>
|
|
</div>
|
|
<p v-if="!savedPhotos(role.slot).length">暂无已保存照片</p>
|
|
<label v-if="uploadable && savedPhotos(role.slot).length < (role.slot.startsWith('FUEL_') ? 1 : 5)" class="outsourced-business-upload">上传资料<input type="file" :accept="role.slot === 'LOADING_SCENE' ? 'image/jpeg,image/png,application/pdf' : 'image/jpeg,image/png'" :disabled="busy" @change="upload(role.slot, $event)" /></label>
|
|
</template>
|
|
</OutsourcedBusinessPhotosDialog>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.outsourced-business-photo { display: block; width: 100%; max-height: 160px; object-fit: contain; }
|
|
.outsourced-business-upload { display: block; margin-top: var(--admin-space-3); }
|
|
.outsourced-business-upload input { display: block; width: 100%; margin-top: var(--admin-space-2); }
|
|
</style>
|