<script lang="ts" setup>
|
import type { PreviewFileType } from '@vben/utils';
|
|
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
|
import { getFileIcon } from '@vben/utils';
|
|
import { Button, Modal, Spin } from 'ant-design-vue';
|
|
import { IconifyIcon } from '@vben/icons';
|
|
const VueOfficePdf = defineAsyncComponent(() => import('@vue-office/pdf'));
|
const VueOfficeDocx = defineAsyncComponent(() => import('@vue-office/docx'));
|
const VueOfficeExcel = defineAsyncComponent(() => import('@vue-office/excel'));
|
const VueOfficePptx = defineAsyncComponent(() => import('@vue-office/pptx'));
|
|
// @vue-office/excel 需要额外导入 CSS
|
// @vue-office/excel 需要额外导入 CSS
|
// @vue-office/excel 需要额外导入 CSS
|
import '@vue-office/excel-css';
|
|
const props = withDefaults(
|
defineProps<{
|
fileUrl?: string;
|
fileName?: string;
|
visible?: boolean;
|
width?: number | string;
|
}>(),
|
{
|
fileUrl: '',
|
fileName: '',
|
visible: false,
|
width: '90vw',
|
},
|
);
|
|
const emit = defineEmits<{
|
'update:visible': [value: boolean];
|
}>();
|
|
const loading = ref(false);
|
const hasError = ref(false);
|
|
const open = computed({
|
get: () => props.visible,
|
set: (val) => emit('update:visible', val),
|
});
|
|
const displayName = computed(() => props.fileName || '文件预览');
|
|
// 根据 fileName 或 fileUrl 判断文件类型
|
const fileType = computed<PreviewFileType>(() => {
|
const name = props.fileName || props.fileUrl;
|
if (!name) return 'unknown';
|
const ext = name.split('.').pop()?.toLowerCase() || '';
|
if (ext === 'pdf') return 'pdf';
|
if (['doc', 'docx'].includes(ext)) return 'docx';
|
if (['xls', 'xlsx'].includes(ext)) return 'excel';
|
if (['ppt', 'pptx'].includes(ext)) return 'pptx';
|
return 'unknown';
|
});
|
|
const canPreview = computed(
|
() => ['pdf', 'docx', 'excel', 'pptx'].includes(fileType.value),
|
);
|
|
watch(open, (val) => {
|
if (val) {
|
hasError.value = false;
|
loading.value = canPreview.value;
|
}
|
});
|
|
function onRendered() {
|
loading.value = false;
|
}
|
|
function onError() {
|
loading.value = false;
|
hasError.value = true;
|
}
|
|
function openInNewTab() {
|
if (props.fileUrl) {
|
window.open(props.fileUrl, '_blank');
|
}
|
}
|
</script>
|
|
<template>
|
<Modal
|
v-model:open="open"
|
:title="displayName"
|
:width="width"
|
:footer="null"
|
destroy-on-close
|
centered
|
:style="{ top: '20px' }"
|
:body-style="{ height: '80vh', overflow: 'auto' }"
|
>
|
<!-- Loading -->
|
<div
|
v-if="loading"
|
class="flex h-full items-center justify-center"
|
>
|
<Spin size="large" tip="加载中..." />
|
</div>
|
|
<!-- Error -->
|
<div
|
v-else-if="hasError"
|
class="flex h-full flex-col items-center justify-center"
|
>
|
<IconifyIcon icon="lucide:file-x" :size="64" class="text-red-400" />
|
<p class="mt-4 text-gray-500">文件预览失败</p>
|
<Button type="primary" class="mt-4" @click="openInNewTab">
|
在新标签页打开
|
</Button>
|
</div>
|
|
<!-- Unsupported -->
|
<div
|
v-else-if="!canPreview"
|
class="flex h-full flex-col items-center justify-center"
|
>
|
<IconifyIcon
|
:icon="getFileIcon(fileName || fileUrl)"
|
:size="64"
|
class="text-gray-400"
|
/>
|
<p class="mt-4 text-gray-500">不支持预览此文件类型</p>
|
<Button type="primary" class="mt-4" @click="openInNewTab">
|
下载文件
|
</Button>
|
</div>
|
|
<!-- PDF -->
|
<VueOfficePdf
|
v-else-if="fileType === 'pdf'"
|
:src="fileUrl"
|
@rendered="onRendered"
|
@error="onError"
|
/>
|
|
<!-- Word -->
|
<VueOfficeDocx
|
v-else-if="fileType === 'docx'"
|
:src="fileUrl"
|
@rendered="onRendered"
|
@error="onError"
|
/>
|
|
<!-- Excel -->
|
<VueOfficeExcel
|
v-else-if="fileType === 'excel'"
|
:src="fileUrl"
|
@rendered="onRendered"
|
@error="onError"
|
/>
|
|
<!-- PowerPoint -->
|
<VueOfficePptx
|
v-else-if="fileType === 'pptx'"
|
:src="fileUrl"
|
@rendered="onRendered"
|
@error="onError"
|
/>
|
</Modal>
|
</template>
|