197 lines
4.4 KiB
Vue
197 lines
4.4 KiB
Vue
|
|
<script lang="ts" setup>
|
|||
|
|
/**
|
|||
|
|
* 通用文件预览:图片用 Ant Image;PDF fetch→blob→iframe;支持多文件切换
|
|||
|
|
* 开票中心与素材库复用,避免两套预览逻辑
|
|||
|
|
*/
|
|||
|
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|||
|
|
|
|||
|
|
import { Button, Image, Modal, Spin, message } from 'ant-design-vue';
|
|||
|
|
|
|||
|
|
const props = withDefaults(
|
|||
|
|
defineProps<{
|
|||
|
|
/** 是否显示 */
|
|||
|
|
open: boolean;
|
|||
|
|
/** 文件 URL 列表 */
|
|||
|
|
files?: string[];
|
|||
|
|
/** 弹窗标题前缀 */
|
|||
|
|
title?: string;
|
|||
|
|
}>(),
|
|||
|
|
{
|
|||
|
|
files: () => [],
|
|||
|
|
title: '文件预览',
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
(e: 'update:open', v: boolean): void;
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
const currentIndex = ref(0);
|
|||
|
|
const previewIsImage = ref(false);
|
|||
|
|
const previewImageUrl = ref('');
|
|||
|
|
const previewPdfUrl = ref('');
|
|||
|
|
const previewLoading = ref(false);
|
|||
|
|
|
|||
|
|
const visible = computed({
|
|||
|
|
get: () => props.open,
|
|||
|
|
set: (v: boolean) => emit('update:open', v),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const currentUrl = computed(() => props.files?.[currentIndex.value] || '');
|
|||
|
|
|
|||
|
|
const displayTitle = computed(() => {
|
|||
|
|
const name = (currentUrl.value || '').split('/').pop() || '';
|
|||
|
|
const total = props.files?.length || 0;
|
|||
|
|
if (total > 1) {
|
|||
|
|
return `${props.title}(${currentIndex.value + 1}/${total})${name ? ' - ' + name : ''}`;
|
|||
|
|
}
|
|||
|
|
return name ? `${props.title} - ${name}` : props.title;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function isImageUrl(url: string) {
|
|||
|
|
return /\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(url || '');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function revokePdfBlob() {
|
|||
|
|
if (previewPdfUrl.value && previewPdfUrl.value.startsWith('blob:')) {
|
|||
|
|
URL.revokeObjectURL(previewPdfUrl.value);
|
|||
|
|
}
|
|||
|
|
previewPdfUrl.value = '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 加载当前文件到预览区
|
|||
|
|
*/
|
|||
|
|
async function loadCurrent() {
|
|||
|
|
const url = currentUrl.value;
|
|||
|
|
revokePdfBlob();
|
|||
|
|
if (!url) {
|
|||
|
|
message.info('暂无文件');
|
|||
|
|
visible.value = false;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (isImageUrl(url)) {
|
|||
|
|
previewIsImage.value = true;
|
|||
|
|
previewImageUrl.value = url;
|
|||
|
|
previewLoading.value = false;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
previewIsImage.value = false;
|
|||
|
|
previewLoading.value = true;
|
|||
|
|
try {
|
|||
|
|
const resp = await fetch(url);
|
|||
|
|
if (!resp.ok) throw new Error('文件加载失败');
|
|||
|
|
const blob = await resp.blob();
|
|||
|
|
const pdfBlob =
|
|||
|
|
blob.type && blob.type !== 'application/octet-stream'
|
|||
|
|
? blob
|
|||
|
|
: new Blob([blob], { type: 'application/pdf' });
|
|||
|
|
previewPdfUrl.value = URL.createObjectURL(pdfBlob);
|
|||
|
|
} catch {
|
|||
|
|
message.error('预览失败,请尝试下载后查看');
|
|||
|
|
visible.value = false;
|
|||
|
|
} finally {
|
|||
|
|
previewLoading.value = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function prevFile() {
|
|||
|
|
if (currentIndex.value > 0) {
|
|||
|
|
currentIndex.value -= 1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function nextFile() {
|
|||
|
|
if (props.files && currentIndex.value < props.files.length - 1) {
|
|||
|
|
currentIndex.value += 1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleClose() {
|
|||
|
|
visible.value = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
watch(
|
|||
|
|
() => [props.open, props.files] as const,
|
|||
|
|
([open]) => {
|
|||
|
|
if (open) {
|
|||
|
|
currentIndex.value = 0;
|
|||
|
|
loadCurrent();
|
|||
|
|
} else {
|
|||
|
|
revokePdfBlob();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
watch(currentIndex, () => {
|
|||
|
|
if (props.open) {
|
|||
|
|
loadCurrent();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
onBeforeUnmount(() => {
|
|||
|
|
revokePdfBlob();
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<Modal
|
|||
|
|
v-model:open="visible"
|
|||
|
|
:title="displayTitle"
|
|||
|
|
:footer="null"
|
|||
|
|
width="860px"
|
|||
|
|
destroy-on-close
|
|||
|
|
@cancel="handleClose"
|
|||
|
|
>
|
|||
|
|
<div class="preview-wrap">
|
|||
|
|
<div v-if="(files?.length || 0) > 1" class="nav">
|
|||
|
|
<Button size="small" :disabled="currentIndex <= 0" @click="prevFile">上一份</Button>
|
|||
|
|
<Button
|
|||
|
|
size="small"
|
|||
|
|
:disabled="currentIndex >= (files?.length || 0) - 1"
|
|||
|
|
@click="nextFile"
|
|||
|
|
>
|
|||
|
|
下一份
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
<Spin :spinning="previewLoading">
|
|||
|
|
<div v-if="previewIsImage" class="img-box">
|
|||
|
|
<Image :src="previewImageUrl" :preview="true" style="max-width: 100%" />
|
|||
|
|
</div>
|
|||
|
|
<iframe
|
|||
|
|
v-else-if="previewPdfUrl"
|
|||
|
|
:src="previewPdfUrl"
|
|||
|
|
class="pdf-frame"
|
|||
|
|
title="pdf-preview"
|
|||
|
|
/>
|
|||
|
|
</Spin>
|
|||
|
|
</div>
|
|||
|
|
</Modal>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.preview-wrap {
|
|||
|
|
min-height: 240px;
|
|||
|
|
color: hsl(var(--foreground));
|
|||
|
|
}
|
|||
|
|
.nav {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 8px;
|
|||
|
|
margin-bottom: 12px;
|
|||
|
|
}
|
|||
|
|
.img-box {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 8px;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
background: hsl(var(--muted) / 0.25);
|
|||
|
|
border: 1px solid hsl(var(--border));
|
|||
|
|
}
|
|||
|
|
.pdf-frame {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 70vh;
|
|||
|
|
border: 1px solid hsl(var(--border));
|
|||
|
|
border-radius: 8px;
|
|||
|
|
background: hsl(var(--background));
|
|||
|
|
}
|
|||
|
|
</style>
|