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>
|