Files
admin-web/src/modules/overseas-master-data/components/ResourcePhotoPanel.vue
T

63 lines
4.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import AppDialog from '@/shared/components/AppDialog.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { getResourcePhoto, uploadResourcePhoto, downloadResourcePhoto, type ResourcePhotoKind, type ResourcePhotoDetail } from '../api/resourcePhotos'
import { overseasMasterDataPermissions as permission } from '../permissions'
const props = defineProps<{ kind: ResourcePhotoKind; resourceId: string }>()
const access = useAccessPreviewStore(), detail = ref<ResourcePhotoDetail | null>(null), loading = ref(false), busy = ref(false)
const error = ref<ApiErrorDetails | null>(null), input = ref<HTMLInputElement | null>(null), previewOpen = ref(false), url = ref('')
const label = computed(() => props.kind === 'VEHICLE' ? '车辆图片' : '司机照片')
const editable = computed(() => access.can(props.kind === 'VEHICLE' ? permission.vehicleEdit : permission.driverUpload))
let generation = 0
function closePreview() { previewOpen.value = false; if (url.value) URL.revokeObjectURL(url.value); url.value = '' }
async function load() {
const current = ++generation; closePreview(); detail.value = null; loading.value = true; error.value = null; busy.value = false
try { const result = await getResourcePhoto(props.kind, props.resourceId); if (current === generation) detail.value = result }
catch (cause) { if (current === generation) error.value = getApiErrorDetails(cause, '档案照片加载失败') }
finally { if (current === generation) loading.value = false }
}
async function upload(event: Event) {
const element = event.target as HTMLInputElement, file = element.files?.[0]; element.value = ''
if (!file || !detail.value || busy.value || !editable.value) return
if (!['image/jpeg', 'image/png'].includes(file.type) || !file.size || file.size > 10 * 1024 * 1024) { ElMessage.warning('请选择非空 JPG 或 PNG,最大 10MiB'); return }
const current = generation; busy.value = true; error.value = null
try {
const saved = await uploadResourcePhoto(props.kind, props.resourceId, detail.value.recordVersion, file)
if (current !== generation) return
detail.value = saved; closePreview(); ElMessage.success(`${label.value}已保存,新排车会自动带入`)
} catch (cause) { if (current === generation) error.value = getApiErrorDetails(cause, '照片保存未完成,请重新读取后重试') }
finally { if (current === generation) busy.value = false }
}
async function preview() {
if (!detail.value?.photo || busy.value) return
const current = generation; busy.value = true; error.value = null
try {
const blob = await downloadResourcePhoto(props.kind, props.resourceId, detail.value.photo.fileId)
if (current !== generation) return
closePreview(); url.value = URL.createObjectURL(blob); previewOpen.value = true
} catch (cause) { if (current === generation) error.value = getApiErrorDetails(cause, '照片预览失败') }
finally { if (current === generation) busy.value = false }
}
watch(() => [props.kind, props.resourceId], load, { immediate: true })
onBeforeUnmount(() => { generation++; closePreview() })
</script>
<template>
<section v-loading="loading" :aria-label="label">
<LoadErrorState v-if="error" title="档案照片操作未完成" :message="error.message" compact @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<div class="density-actions">
<el-button v-if="detail?.photo" :disabled="busy" @click="preview">查看{{ label }}</el-button><span v-else-if="detail">尚未上传{{ label }}</span>
<el-button v-if="editable" :disabled="!detail || busy" :loading="busy" @click="input?.click()">上传 / 替换{{ label }}</el-button>
<input ref="input" hidden type="file" accept="image/jpeg,image/png" :aria-label="'上传' + label" @change="upload">
</div>
<p class="density-dialog-note">排车选择此档案后自动带入照片;替换不影响已保存的排车资料。JPG、PNG,最大 10MiB。</p>
<AppDialog :model-value="previewOpen" class="admin-density-dialog" :title="label" width="620px" :show-confirm="false" cancel-text="关闭" @update:model-value="closePreview"><img v-if="url" class="resource-photo-preview" :src="url" :alt="label"></AppDialog>
</section>
</template>
<style scoped>.resource-photo-preview { display: block; width: 100%; max-height: 60vh; object-fit: contain; }</style>