修改pdf预览方法

master
2358328281@qq.com 1 day ago
parent 0492acf424
commit 392e195307

@ -0,0 +1,203 @@
<template>
<el-dialog
:model-value="visible"
:title="fileName || $t('btn.preview')"
width="80%"
top="5vh"
destroy-on-close
:before-close="handleClose"
@update:model-value="(v) => !v && handleClose()"
>
<template #header>
<div class="file-preview-header">
<span class="file-preview-title" :title="fileName">{{ fileName || $t('btn.preview') }}</span>
<div class="file-preview-tools">
<template v-if="fileType === 'pdf'">
<el-button size="small" :disabled="pageScale <= 0.5" @click="scaleDown">
<el-icon><ZoomOut /></el-icon>
</el-button>
<span class="scale-text">{{ Math.round(pageScale * 100) }}%</span>
<el-button size="small" :disabled="pageScale >= 2.5" @click="scaleUp">
<el-icon><ZoomIn /></el-icon>
</el-button>
<el-button size="small" :disabled="currentPage <= 1" @click="prevPage">
<el-icon><ArrowLeft /></el-icon>
</el-button>
<span class="page-text">{{ currentPage }} / {{ totalPages }}</span>
<el-button size="small" :disabled="currentPage >= totalPages" @click="nextPage">
<el-icon><ArrowRight /></el-icon>
</el-button>
</template>
<el-button type="primary" size="small" @click="handleDownload">
<el-icon><Download /></el-icon>
<span style="margin-left: 4px">{{ $t('btn.download') || '下载' }}</span>
</el-button>
</div>
</div>
</template>
<div v-loading="loading" class="file-preview-body">
<VuePdfEmbed
v-if="fileType === 'pdf' && blobUrl"
:source="blobUrl"
:page="currentPage"
:scale="pageScale"
class="pdf-viewer"
@loaded="onPdfLoaded"
@loading-failed="onPdfError"
/>
<img
v-else-if="fileType === 'image' && blobUrl"
:src="blobUrl"
class="image-viewer"
/>
<div v-else class="file-preview-empty">
{{ $t('msg.noPreview') || '暂不支持预览该类型' }}
</div>
</div>
</el-dialog>
</template>
<script setup>
import { ref, watch } from "vue";
import VuePdfEmbed from "vue-pdf-embed";
import { ArrowLeft, ArrowRight, Download, ZoomIn, ZoomOut } from "@element-plus/icons-vue";
const props = defineProps({
visible: { type: Boolean, default: false },
blobUrl: { type: String, default: "" },
fileName: { type: String, default: "" },
fileType: { type: String, default: "" }, // 'pdf' | 'image' | 'other'
// Blob blobUrl
blob: { type: Blob, default: null },
});
const emit = defineEmits(["update:visible", "close", "download"]);
const loading = ref(false);
const currentPage = ref(1);
const totalPages = ref(0);
const pageScale = ref(1);
//
const handleClose = () => {
emit("update:visible", false);
emit("close");
};
// downloadBlob +
const handleDownload = () => {
emit("download", { blob: props.blob, blobUrl: props.blobUrl, fileName: props.fileName });
};
// PDF
const onPdfLoaded = (pdfDoc) => {
loading.value = false;
totalPages.value = pdfDoc?.numPages || 0;
currentPage.value = 1;
};
const onPdfError = (err) => {
loading.value = false;
console.error("PDF 加载失败:", err);
};
const prevPage = () => {
if (currentPage.value > 1) currentPage.value -= 1;
};
const nextPage = () => {
if (currentPage.value < totalPages.value) currentPage.value += 1;
};
const scaleDown = () => {
if (pageScale.value > 0.5) pageScale.value = Math.round((pageScale.value - 0.25) * 100) / 100;
};
const scaleUp = () => {
if (pageScale.value < 2.5) pageScale.value = Math.round((pageScale.value + 0.25) * 100) / 100;
};
// visible loading
watch(
() => props.visible,
(v) => {
if (v) {
loading.value = props.fileType === "pdf";
currentPage.value = 1;
totalPages.value = 0;
pageScale.value = 1;
}
},
);
</script>
<style scoped lang="scss">
.file-preview-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
padding-right: 8px;
}
.file-preview-title {
flex: 1;
font-size: 16px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-preview-tools {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.scale-text,
.page-text {
min-width: 48px;
text-align: center;
font-size: 13px;
color: #606266;
}
.file-preview-body {
min-height: 60vh;
max-height: 80vh;
overflow: auto;
background: #f5f7fa;
border-radius: 4px;
padding: 8px;
display: flex;
justify-content: center;
align-items: flex-start;
}
.pdf-viewer {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.image-viewer {
max-width: 100%;
max-height: 75vh;
object-fit: contain;
display: block;
margin: auto;
}
.file-preview-empty {
width: 100%;
text-align: center;
color: #909399;
padding: 60px 0;
}
</style>

@ -84,20 +84,16 @@
</div> </div>
</el-card> </el-card>
<el-dialog <FilePreviewDialog
v-model="previewVisible" :visible="previewVisible"
:title="$t('btn.preview')" :blob-url="previewUrl"
width="80%" :blob="previewBlob"
top="5vh" :file-name="previewName"
destroy-on-close :file-type="previewType"
:before-close="closePreview" @update:visible="(v) => (previewVisible = v)"
> @close="cleanupPreview"
<iframe @download="onPreviewDownload"
v-if="previewUrl"
:src="previewUrl"
class="preview-iframe"
/> />
</el-dialog>
</div> </div>
</template> </template>
@ -108,6 +104,7 @@ import { ElMessage } from "element-plus";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { getAdminHospitals, getUploadFile } from "@/service/modular/admin"; import { getAdminHospitals, getUploadFile } from "@/service/modular/admin";
import FilePreviewDialog from "@/components/FilePreviewDialog.vue";
const { t: $t } = useI18n(); const { t: $t } = useI18n();
const router = useRouter(); const router = useRouter();
@ -171,27 +168,77 @@ const goAdd = () => router.push("/admin/hospitals/edit");
const previewVisible = ref(false); const previewVisible = ref(false);
const previewUrl = ref(""); const previewUrl = ref("");
const previewBlob = ref(null);
const previewName = ref("");
const previewType = ref(""); // 'pdf' | 'image' | ''
const cleanupPreviewUrl = () => { const cleanupPreview = () => {
if (previewUrl.value) { if (previewUrl.value) {
URL.revokeObjectURL(previewUrl.value); URL.revokeObjectURL(previewUrl.value);
previewUrl.value = ""; previewUrl.value = "";
} }
previewBlob.value = null;
previewName.value = "";
previewType.value = "";
}; };
const closePreview = () => { //
previewVisible.value = false; const getFileNameFromPath = (path) => {
cleanupPreviewUrl(); if (!path) return "";
return String(path).replace(/\\/g, "/").split("/").filter(Boolean).pop() || "";
};
// /+blob mime
const getFileType = (fileName, blob) => {
const name = String(fileName || "");
const ext = name.split(".").pop()?.toLowerCase() || "";
const mime = blob?.type || "";
if (ext === "pdf" || mime === "application/pdf") return "pdf";
if (
["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(ext) ||
(mime && mime.startsWith("image/"))
) {
return "image";
}
return "other";
};
// blob
const downloadBlob = (blob, fileName) => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName || "download";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 0);
};
// FilePreviewDialog
const onPreviewDownload = ({ blob, fileName }) => {
if (!blob) return;
downloadBlob(blob, fileName);
}; };
const viewReport = async (attachmentPath) => { const viewReport = async (attachmentPath) => {
if (!attachmentPath) return; if (!attachmentPath) return;
const fileName = String(attachmentPath); const path = String(attachmentPath);
const downloadName = getFileNameFromPath(path) || path;
try { try {
const blob = await getUploadFile(fileName); const blob = await getUploadFile(path);
cleanupPreviewUrl(); const type = getFileType(downloadName, blob);
// PDF / PDF vue-pdf-embed
if (type === "pdf" || type === "image") {
cleanupPreview();
previewBlob.value = blob;
previewUrl.value = URL.createObjectURL(blob); previewUrl.value = URL.createObjectURL(blob);
previewName.value = downloadName;
previewType.value = type;
previewVisible.value = true; previewVisible.value = true;
} else {
downloadBlob(blob, downloadName);
}
} catch (e) { } catch (e) {
ElMessage.error(e?.message || $t('msg.failed')); ElMessage.error(e?.message || $t('msg.failed'));
} }

@ -244,25 +244,16 @@
</template> </template>
</el-dialog> </el-dialog>
<el-dialog <FilePreviewDialog
v-model="previewVisible" :visible="previewVisible"
:title="$t('btn.preview')" :blob-url="previewUrl"
width="80%" :blob="previewBlob"
top="5vh" :file-name="previewName"
destroy-on-close :file-type="previewType"
:before-close="closePreview" @update:visible="(v) => (previewVisible = v)"
> @close="cleanupPreview"
<iframe @download="onPreviewDownload"
v-if="previewType === 'pdf' && previewUrl"
:src="previewUrl"
class="preview-iframe"
/> />
<img
v-else-if="previewType === 'image' && previewUrl"
:src="previewUrl"
class="preview-image"
/>
</el-dialog>
</div> </div>
</template> </template>
@ -288,6 +279,7 @@ import {
getUploadFile, getUploadFile,
updateAdminOrder, updateAdminOrder,
} from "@/service/modular/admin"; } from "@/service/modular/admin";
import FilePreviewDialog from "@/components/FilePreviewDialog.vue";
const { t: $t } = useI18n(); const { t: $t } = useI18n();
const route = useRoute(); const route = useRoute();
@ -312,21 +304,20 @@ const goBack = () => router.back();
const previewVisible = ref(false); const previewVisible = ref(false);
const previewUrl = ref(""); const previewUrl = ref("");
const previewBlob = ref(null);
const previewName = ref("");
const previewType = ref(""); // 'pdf' | 'image' | '' const previewType = ref(""); // 'pdf' | 'image' | ''
const cleanupPreviewUrl = () => { const cleanupPreview = () => {
if (previewUrl.value) { if (previewUrl.value) {
URL.revokeObjectURL(previewUrl.value); URL.revokeObjectURL(previewUrl.value);
previewUrl.value = ""; previewUrl.value = "";
} }
previewBlob.value = null;
previewName.value = "";
previewType.value = ""; previewType.value = "";
}; };
const closePreview = () => {
previewVisible.value = false;
cleanupPreviewUrl();
};
// /+blob mime // /+blob mime
const getFileType = (fileName, blob) => { const getFileType = (fileName, blob) => {
const name = String(fileName || ""); const name = String(fileName || "");
@ -342,7 +333,7 @@ const getFileType = (fileName, blob) => {
return "other"; return "other";
}; };
// blob // blob
const downloadBlob = (blob, fileName) => { const downloadBlob = (blob, fileName) => {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
@ -354,6 +345,12 @@ const downloadBlob = (blob, fileName) => {
setTimeout(() => URL.revokeObjectURL(url), 0); setTimeout(() => URL.revokeObjectURL(url), 0);
}; };
// FilePreviewDialog
const onPreviewDownload = ({ blob, fileName }) => {
if (!blob) return;
downloadBlob(blob, fileName);
};
const viewReport = async (attachmentPath, originalName) => { const viewReport = async (attachmentPath, originalName) => {
if (!attachmentPath) return; if (!attachmentPath) return;
const path = String(attachmentPath); const path = String(attachmentPath);
@ -361,9 +358,12 @@ const viewReport = async (attachmentPath, originalName) => {
try { try {
const blob = await getUploadFile(path); const blob = await getUploadFile(path);
const type = getFileType(downloadName, blob); const type = getFileType(downloadName, blob);
// PDF / PDF vue-pdf-embed
if (type === "pdf" || type === "image") { if (type === "pdf" || type === "image") {
cleanupPreviewUrl(); cleanupPreview();
previewBlob.value = blob;
previewUrl.value = URL.createObjectURL(blob); previewUrl.value = URL.createObjectURL(blob);
previewName.value = downloadName;
previewType.value = type; previewType.value = type;
previewVisible.value = true; previewVisible.value = true;
} else { } else {

Loading…
Cancel
Save