76 lines
5.7 KiB
Vue
76 lines
5.7 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<Partial<Record<OutsourcedBusinessPhotoSlot,string>>>({}), reading = ref<OutsourcedBusinessPhotoSlot | null>(null)
|
|
const uploadable = computed(() => access.can(permission.outsourcedBusinessUpload) && props.detail?.row.actions.upload.allowed === true)
|
|
let actor = '', number = '', generation = 0
|
|
function savedPhoto(slot: OutsourcedBusinessPhotoSlot) { return props.detail?.photos.find(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) {
|
|
const photo = savedPhoto(slot), current = generation
|
|
if (!photo || reading.value) return
|
|
reading.value = slot; error.value = null
|
|
try { const blob = await getOutsourcedBusinessPhotoContent(number, photo); if (current !== generation || !open.value) return; urls.value[slot] = 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) {
|
|
const input = event.target as HTMLInputElement, file = input.files?.[0]; input.value = ''
|
|
if (!file || busy.value || !uploadable.value) return
|
|
if (!['image/jpeg','image/png'].includes(file.type) || !file.size) { error.value = { message: '请选择有效的JPG或PNG图片' }; 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 }); 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]>
|
|
<img v-if="urls[role.slot]" :src="urls[role.slot]" :alt="role.label" class="outsourced-business-photo" />
|
|
<template v-if="savedPhoto(role.slot)"><p>{{ savedPhoto(role.slot)?.fileName }} · 已保存</p><el-button :loading="reading === role.slot" :disabled="Boolean(reading)" @click="preview(role.slot)">查看图片</el-button></template>
|
|
<p v-else>暂无已保存照片</p>
|
|
<label v-if="uploadable" class="outsourced-business-upload">{{ savedPhoto(role.slot) ? '替换图片' : '上传图片' }}<input type="file" accept="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>
|