Merge release/test and preserve pickup execution and photo workflows
This commit is contained in:
@@ -17,6 +17,8 @@ const emit = defineEmits<{ busy: [value: boolean] }>()
|
||||
const metadata = ref<Record<string, DispatchEvidenceFile>>({}), error = ref<ApiErrorDetails | null>(null), uploading = ref(false), input = ref<HTMLInputElement | null>(null)
|
||||
const imageOpen = ref(false), imageUrl = ref(''), imageName = ref(''), previewLoading = ref(false)
|
||||
const thumbnails = ref<Record<string, string>>({})
|
||||
const previewId = ref(''), imageDecoding = ref(false), imageFailure = ref('')
|
||||
const previewBusy = computed(() => previewLoading.value || imageDecoding.value)
|
||||
const label = computed(() => props.label || (props.kind === 'VEHICLE' ? '本趟车辆证据' : props.kind === 'DRIVER_PHOTO' ? '司机照片' : '本趟司机证照'))
|
||||
const types = ['application/pdf', 'image/jpeg', 'image/png'], maximum = 10 * 1024 * 1024, urls = new Set<string>()
|
||||
const uploadTypes = computed(() => ['DRIVER_PHOTO', 'EXCEPTION'].includes(props.kind) ? types.slice(1) : types)
|
||||
@@ -27,7 +29,18 @@ function valid(value: DispatchEvidenceFile, expected?: string) {
|
||||
return { ...value, fileId: String(value.fileId) }
|
||||
}
|
||||
function fail(cause: unknown, fallback: string) { error.value = getApiErrorDetails(cause, cause instanceof DispatchInputError ? cause.message : fallback) }
|
||||
function closePreview() { previewGeneration++; pendingPopup?.close(); pendingPopup = null; imageOpen.value = false; previewLoading.value = false; if (imageUrl.value) { URL.revokeObjectURL(imageUrl.value); urls.delete(imageUrl.value); imageUrl.value = '' } }
|
||||
function closePreview() { previewGeneration++; pendingPopup?.close(); pendingPopup = null; imageOpen.value = false; previewLoading.value = false; imageDecoding.value = false; imageFailure.value = ''; if (imageUrl.value) { URL.revokeObjectURL(imageUrl.value); urls.delete(imageUrl.value); imageUrl.value = '' } }
|
||||
function imageLoaded(event: Event) {
|
||||
const image = event.target as HTMLImageElement
|
||||
if (!imageOpen.value || image.getAttribute('src') !== imageUrl.value) return
|
||||
imageDecoding.value = false
|
||||
if (image.naturalWidth <= 1 && image.naturalHeight <= 1) imageFailure.value = '原图片仅为 1 × 1 像素,无法查看有效内容,请核对上传的原附件。'
|
||||
}
|
||||
function imageFailed(event: Event) {
|
||||
if (!imageOpen.value || (event.target as HTMLImageElement).getAttribute('src') !== imageUrl.value) return
|
||||
imageDecoding.value = false
|
||||
imageFailure.value = '图片无法解码或加载,请重新预览;若仍失败,请核对原附件是否损坏。'
|
||||
}
|
||||
async function load() {
|
||||
const current = ++generation, no = props.dispatchNo, kind = props.kind, values = [...allIds.value]
|
||||
for (const url of Object.values(thumbnails.value)) { URL.revokeObjectURL(url); urls.delete(url) }
|
||||
@@ -78,6 +91,7 @@ async function readContent(no: string, kind: DispatchEvidenceKind, id: string, f
|
||||
async function preview(id: string) {
|
||||
const file = metadata.value[id]; if (!file || previewLoading.value) return
|
||||
closePreview(); const current = ++previewGeneration, scope = generation
|
||||
previewId.value = id
|
||||
error.value = null; previewLoading.value = true
|
||||
if (file.contentType === 'application/pdf') { pendingPopup = window.open('', '_blank'); if (!pendingPopup) { previewLoading.value = false; fail(new DispatchInputError('请允许本站打开PDF预览窗口'), 'PDF预览未打开'); return } }
|
||||
else { imageName.value = file.fileName; imageOpen.value = true }
|
||||
@@ -86,7 +100,7 @@ async function preview(id: string) {
|
||||
if (current !== previewGeneration || scope !== generation) return
|
||||
const url = URL.createObjectURL(blob); urls.add(url)
|
||||
if (file.contentType === 'application/pdf') { pendingPopup!.location.href = url; pendingPopup = null; window.setTimeout(() => { URL.revokeObjectURL(url); urls.delete(url) }, 60_000) }
|
||||
else imageUrl.value = url
|
||||
else { imageDecoding.value = true; imageUrl.value = url }
|
||||
} catch (cause) { if (current === previewGeneration) { closePreview(); fail(cause, '本车附件预览失败,原引用已保留') } }
|
||||
finally { if (current === previewGeneration) previewLoading.value = false }
|
||||
}
|
||||
@@ -111,8 +125,11 @@ onBeforeUnmount(() => { generation++; closePreview(); for (const url of urls) UR
|
||||
<p v-else class="density-dialog-note">暂无本趟附件。</p>
|
||||
</template>
|
||||
<template v-if="!readonly"><input ref="input" class="evidence-input" type="file" :accept="uploadTypes.join(',')" :aria-label="'上传' + label" :disabled="disabled || uploading || !dispatchNo" @change="upload"><el-button :disabled="disabled || uploading || !dispatchNo" :loading="uploading" @click="choose()">上传{{ label }}</el-button></template>
|
||||
<AppDialog append-to-body class="admin-density-dialog admin-workbench is-dialog" :model-value="imageOpen" :title="imageName || '附件预览'" width="min(900px, calc(100vw - 28px))" fullscreenable :loading="previewLoading" :show-confirm="false" cancel-text="关闭" @update:model-value="closePreview">
|
||||
<div class="density-dialog-sections"><section class="density-dialog-section" data-tone="source" aria-label="本车附件预览信息"><h3 class="density-dialog-section__title">附件信息</h3><table class="density-table"><tbody><tr><th scope="row">排车单号</th><td>{{ dispatchNo || '—' }}</td></tr><tr><th scope="row">附件名称</th><td>{{ imageName || '—' }}</td></tr></tbody></table></section><p v-if="previewLoading" class="density-dialog-note" role="status">正在读取本车附件…</p><img v-if="imageUrl" class="evidence-image" :src="imageUrl" :alt="imageName"></div>
|
||||
<AppDialog append-to-body class="admin-density-dialog admin-workbench is-dialog" :model-value="imageOpen" :title="imageName || '附件预览'" width="min(900px, calc(100vw - 28px))" fullscreenable :loading="previewBusy" :show-confirm="false" cancel-text="关闭" @update:model-value="closePreview">
|
||||
<div class="evidence-preview" :aria-busy="previewBusy">
|
||||
<LoadErrorState v-if="imageFailure" title="图片无法预览" :message="imageFailure" compact retry-text="重新预览" @retry="preview(previewId)" />
|
||||
<img v-else-if="imageUrl" class="evidence-image" :src="imageUrl" :alt="imageName" @load="imageLoaded" @error="imageFailed">
|
||||
</div>
|
||||
</AppDialog>
|
||||
</section>
|
||||
</template>
|
||||
@@ -122,11 +139,12 @@ onBeforeUnmount(() => { generation++; closePreview(); for (const url of urls) UR
|
||||
.dispatch-evidence--readonly .dispatch-evidence__actions { width: 76px; }
|
||||
.dispatch-evidence > .el-button { margin-top: var(--admin-space-3); }
|
||||
.evidence-input { display: none; }
|
||||
.evidence-image { display: block; max-width: 100%; max-height: 72vh; margin: auto; object-fit: contain; }
|
||||
.evidence-photo-card { margin: 0; text-align: center; }
|
||||
.evidence-photo-list { display: grid; gap: var(--admin-space-3); }
|
||||
.evidence-photo-preview, .evidence-photo-empty { box-sizing: border-box; width: 100%; height: 150px; display: flex; align-items: center; justify-content: center; border: 1px solid var(--el-border-color-lighter); border-radius: var(--el-border-radius-base); color: var(--el-text-color-secondary); }
|
||||
.evidence-photo-preview { padding: 0; cursor: pointer; }
|
||||
.evidence-photo-preview img { width: 100%; height: 100%; object-fit: contain; }
|
||||
.evidence-photo-card figcaption { margin-top: var(--admin-space-3); font-weight: 600; }
|
||||
.evidence-preview { display: grid; place-items: center; height: min(60vh, 600px); min-width: 0; }
|
||||
.evidence-image { display: block; width: 100%; height: 100%; min-height: 0; object-fit: contain; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user