63 lines
4.6 KiB
Vue
63 lines
4.6 KiB
Vue
<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>
|