Files
xk-admin/apps/web-antd/src/components/file-preview/FilePreviewModal.vue
2026-08-07 20:24:49 +08:00

197 lines
4.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
/**
* 通用文件预览:图片用 Ant ImagePDF 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>