优化
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } 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 LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import { bindVehiclePhoto, getVehicleDocuments, readVehiclePhoto, recoverVehiclePhoto, uploadVehiclePhoto, type BindVehiclePhoto, type FuelSlot, type VehicleDocuments, type VehiclePhoto } from '../api/dispatchVehicleDocuments'
|
||||
|
||||
const props = defineProps<{ dispatchNo: string }>()
|
||||
const auth = useAuthStore(), access = useAccessPreviewStore()
|
||||
const emit = defineEmits<{ saved: [] }>()
|
||||
const snapshot = ref<VehicleDocuments | null>(null), busy = ref(false), error = ref<ApiErrorDetails | null>(null)
|
||||
interface Pending { slot: FuelSlot; key: string; photo?: VehiclePhoto; binding?: BindVehiclePhoto }
|
||||
const pending = ref<Pending | null>(null), preview = ref(''), corrupted = ref(false)
|
||||
const actor = auth.user?.id || '', number = props.dispatchNo
|
||||
const storageKey = `dispatch-vehicle-documents:${actor}:${number}`
|
||||
const editable = computed(() => !corrupted.value && access.can(permission.dispatchEdit) && snapshot.value?.editable === true)
|
||||
const roles: Array<{ slot: FuelSlot; label: string }> = [{ slot: 'FUEL_START', label: '起始油耗表盘' }, { slot: 'FUEL_END', label: '结束油耗表盘' }]
|
||||
let active = true
|
||||
function ensureScope() { if (!active || auth.user?.id !== actor || props.dispatchNo !== number) throw new Error('资料操作身份已变化,请重新打开') }
|
||||
function persist() { if (pending.value) sessionStorage.setItem(storageKey, JSON.stringify(pending.value)); else sessionStorage.removeItem(storageKey) }
|
||||
async function load() {
|
||||
busy.value = true; error.value = null
|
||||
try { const result = await getVehicleDocuments(number); ensureScope(); if (result.dispatchNo !== number) throw new Error('车辆资料与排车单不一致'); snapshot.value = result }
|
||||
catch (cause) { if (active) error.value = getApiErrorDetails(cause, '车辆资料读取失败') }
|
||||
finally { if (active) busy.value = false }
|
||||
}
|
||||
async function bind() {
|
||||
const item = pending.value
|
||||
if (!item?.photo || !snapshot.value) return
|
||||
if (item.photo.slot !== item.slot) throw new Error('上传结果与图片分类不一致')
|
||||
if (!item.binding) { item.binding = { recordVersion: snapshot.value.recordVersion, idempotencyKey: crypto.randomUUID(), fileId: item.photo.fileId }; persist() }
|
||||
const result = await bindVehiclePhoto(number, item.slot, item.binding)
|
||||
ensureScope(); snapshot.value = result; pending.value = null; persist(); emit('saved'); ElMessage.success('油耗图片已保存')
|
||||
}
|
||||
async function recover() {
|
||||
if (busy.value || !editable.value || !pending.value) return
|
||||
busy.value = true; error.value = null
|
||||
try { ensureScope(); if (!pending.value.photo) { pending.value.photo = await recoverVehiclePhoto(number, pending.value.key); ensureScope(); persist() }; await bind() }
|
||||
catch (cause) { if (active) error.value = getApiErrorDetails(cause, '原上传或保存结果尚未确认') }
|
||||
finally { if (active) busy.value = false }
|
||||
}
|
||||
async function upload(slot: FuelSlot, event: Event) {
|
||||
const input = event.target as HTMLInputElement, file = input.files?.[0]; input.value = ''
|
||||
if (!file || busy.value || pending.value || !editable.value) return
|
||||
if (!['image/jpeg', 'image/png'].includes(file.type) || !file.size || file.size > 10 * 1024 * 1024) { ElMessage.warning('请选择不超过10MiB的JPG或PNG图片'); return }
|
||||
busy.value = true; error.value = null
|
||||
try {
|
||||
ensureScope(); pending.value = { slot, key: crypto.randomUUID() }; persist()
|
||||
pending.value.photo = await uploadVehiclePhoto(number, slot, file, pending.value.key); ensureScope(); persist(); await bind()
|
||||
} catch (cause) { if (active) error.value = getApiErrorDetails(cause, '图片上传或保存失败,请核对原请求') }
|
||||
finally { if (active) busy.value = false }
|
||||
}
|
||||
async function view(photo: VehiclePhoto) {
|
||||
if (busy.value) return
|
||||
busy.value = true; error.value = null
|
||||
try { const blob = await readVehiclePhoto(number, photo); ensureScope(); if (preview.value) URL.revokeObjectURL(preview.value); preview.value = URL.createObjectURL(blob) }
|
||||
catch (cause) { if (active) error.value = getApiErrorDetails(cause, '图片读取失败') }
|
||||
finally { if (active) busy.value = false }
|
||||
}
|
||||
onMounted(() => {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(storageKey)
|
||||
if (raw) { const value = JSON.parse(raw) as Pending; if (!roles.some(role => role.slot === value.slot) || typeof value.key !== 'string' || !value.key) throw new Error('原图片请求记录无法读取'); pending.value = value }
|
||||
} catch (cause) { corrupted.value = true; error.value = getApiErrorDetails(cause, '原图片请求记录无法读取') }
|
||||
if (!corrupted.value) void load()
|
||||
})
|
||||
onBeforeUnmount(() => { active = false; if (preview.value) URL.revokeObjectURL(preview.value) })
|
||||
defineExpose({ busy })
|
||||
</script>
|
||||
<template>
|
||||
<div v-loading="busy" class="density-dialog-sections">
|
||||
<LoadErrorState v-if="error" compact title="车辆资料操作失败" :message="error.message" :show-retry="!pending && !corrupted" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<div v-if="pending" role="status">有图片尚未确认保存。<el-button :disabled="busy || !editable" @click="recover">核对原请求并继续保存</el-button></div>
|
||||
<section v-for="role in roles" :key="role.slot" class="density-dialog-section">
|
||||
<h3 class="density-dialog-section__title">{{ role.label }}</h3>
|
||||
<div v-for="photo in snapshot?.photos.filter(item => item.slot === role.slot) || []" :key="photo.fileId"><el-button link :disabled="busy" @click="view(photo)">{{ photo.fileName }}</el-button></div>
|
||||
<span v-if="snapshot && !snapshot.photos.some(item => item.slot === role.slot)">未上传</span>
|
||||
<label v-if="editable" class="photo-upload">上传/替换<input type="file" accept="image/jpeg,image/png" :aria-label="role.label" :disabled="busy || Boolean(pending)" @change="upload(role.slot, $event)" /></label>
|
||||
</section>
|
||||
<img v-if="preview" :src="preview" alt="油耗图片预览" class="photo-preview" />
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.photo-upload { display: block; margin-top: var(--admin-space-3); }
|
||||
.photo-upload input { margin-left: var(--admin-space-3); }
|
||||
.photo-preview { display: block; max-width: 100%; max-height: 400px; object-fit: contain; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user