From 4bc16467666e512a8158e793da8c75d67285cb59 Mon Sep 17 00:00:00 2001 From: "2358328281@qq.com" <邮箱地址> Date: Tue, 23 Jun 2026 20:27:16 +0800 Subject: [PATCH] --- package-lock.json | 297 +++++++++++++++++++++++++++ package.json | 1 + src/components/FilePreviewDialog.vue | 203 ++++++++++++++++++ src/views/admin/hospitals/list.vue | 93 ++++++--- src/views/admin/orders/detail.vue | 54 ++--- 5 files changed, 598 insertions(+), 50 deletions(-) create mode 100644 src/components/FilePreviewDialog.vue diff --git a/package-lock.json b/package-lock.json index d0982ec..da985eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "vue-draggable-next": "^2.3.0", "vue-echarts": "^8.0.1", "vue-i18n": "^9.14.5", + "vue-pdf-embed": "^2.1.5", "vue-router": "^4.6.3", "wujie-vue3": "^1.0.29" }, @@ -648,6 +649,271 @@ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz", + "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.100", + "@napi-rs/canvas-darwin-arm64": "0.1.100", + "@napi-rs/canvas-darwin-x64": "0.1.100", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", + "@napi-rs/canvas-linux-arm64-musl": "0.1.100", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-musl": "0.1.100", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", + "@napi-rs/canvas-win32-x64-msvc": "0.1.100" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", + "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", + "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", + "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", + "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", + "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", + "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz", + "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", + "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", + "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", + "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", + "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4977,6 +5243,18 @@ "dev": true, "license": "MIT" }, + "node_modules/pdfjs-dist": { + "version": "5.7.284", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.7.284.tgz", + "integrity": "sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=22.13.0 || >=24" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.100" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -6296,6 +6574,13 @@ "node": ">=0.10.0" } }, + "node_modules/sortablejs": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz", + "integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==", + "license": "MIT", + "peer": true + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -7397,6 +7682,18 @@ "vue": "^3.0.0" } }, + "node_modules/vue-pdf-embed": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/vue-pdf-embed/-/vue-pdf-embed-2.1.5.tgz", + "integrity": "sha512-IGFVBYlnOz2zSql1zk4YJyBu584EZa6RUykk5f8wkHF/AR31khCa+ruJoRag+Ff2UyntkWu0brENIKoikQ7F8g==", + "license": "MIT", + "dependencies": { + "pdfjs-dist": "^5.7.284" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, "node_modules/vue-router": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz", diff --git a/package.json b/package.json index ab31d0e..4b7422e 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "vue-draggable-next": "^2.3.0", "vue-echarts": "^8.0.1", "vue-i18n": "^9.14.5", + "vue-pdf-embed": "^2.1.5", "vue-router": "^4.6.3", "wujie-vue3": "^1.0.29" }, diff --git a/src/components/FilePreviewDialog.vue b/src/components/FilePreviewDialog.vue new file mode 100644 index 0000000..8eff5b8 --- /dev/null +++ b/src/components/FilePreviewDialog.vue @@ -0,0 +1,203 @@ + + !v && handleClose()" + > + + + {{ fileName || $t('btn.preview') }} + + + + + + {{ Math.round(pageScale * 100) }}% + + + + + + + {{ currentPage }} / {{ totalPages }} + + + + + + + {{ $t('btn.download') || '下载' }} + + + + + + + + + + {{ $t('msg.noPreview') || '暂不支持预览该类型' }} + + + + + + + + diff --git a/src/views/admin/hospitals/list.vue b/src/views/admin/hospitals/list.vue index 198b039..a98d294 100644 --- a/src/views/admin/hospitals/list.vue +++ b/src/views/admin/hospitals/list.vue @@ -84,20 +84,16 @@ - - - + (previewVisible = v)" + @close="cleanupPreview" + @download="onPreviewDownload" + /> @@ -108,6 +104,7 @@ import { ElMessage } from "element-plus"; import dayjs from "dayjs"; import { useI18n } from "vue-i18n"; import { getAdminHospitals, getUploadFile } from "@/service/modular/admin"; +import FilePreviewDialog from "@/components/FilePreviewDialog.vue"; const { t: $t } = useI18n(); const router = useRouter(); @@ -171,27 +168,77 @@ const goAdd = () => router.push("/admin/hospitals/edit"); const previewVisible = ref(false); const previewUrl = ref(""); +const previewBlob = ref(null); +const previewName = ref(""); +const previewType = ref(""); // 'pdf' | 'image' | '' -const cleanupPreviewUrl = () => { +const cleanupPreview = () => { if (previewUrl.value) { URL.revokeObjectURL(previewUrl.value); previewUrl.value = ""; } + previewBlob.value = null; + previewName.value = ""; + previewType.value = ""; }; -const closePreview = () => { - previewVisible.value = false; - cleanupPreviewUrl(); +// 从路径中提取最后的文件名(含扩展名)作为下载名 +const getFileNameFromPath = (path) => { + 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) => { if (!attachmentPath) return; - const fileName = String(attachmentPath); + const path = String(attachmentPath); + const downloadName = getFileNameFromPath(path) || path; try { - const blob = await getUploadFile(fileName); - cleanupPreviewUrl(); - previewUrl.value = URL.createObjectURL(blob); - previewVisible.value = true; + const blob = await getUploadFile(path); + const type = getFileType(downloadName, blob); + // PDF / 图片走预览弹窗(PDF 走 vue-pdf-embed),其他格式直接下载 + if (type === "pdf" || type === "image") { + cleanupPreview(); + previewBlob.value = blob; + previewUrl.value = URL.createObjectURL(blob); + previewName.value = downloadName; + previewType.value = type; + previewVisible.value = true; + } else { + downloadBlob(blob, downloadName); + } } catch (e) { ElMessage.error(e?.message || $t('msg.failed')); } diff --git a/src/views/admin/orders/detail.vue b/src/views/admin/orders/detail.vue index 3601445..90cde3e 100644 --- a/src/views/admin/orders/detail.vue +++ b/src/views/admin/orders/detail.vue @@ -244,25 +244,16 @@ - - - - + (previewVisible = v)" + @close="cleanupPreview" + @download="onPreviewDownload" + /> @@ -288,6 +279,7 @@ import { getUploadFile, updateAdminOrder, } from "@/service/modular/admin"; +import FilePreviewDialog from "@/components/FilePreviewDialog.vue"; const { t: $t } = useI18n(); const route = useRoute(); @@ -312,21 +304,20 @@ const goBack = () => router.back(); const previewVisible = ref(false); const previewUrl = ref(""); +const previewBlob = ref(null); +const previewName = ref(""); const previewType = ref(""); // 'pdf' | 'image' | '' -const cleanupPreviewUrl = () => { +const cleanupPreview = () => { if (previewUrl.value) { URL.revokeObjectURL(previewUrl.value); previewUrl.value = ""; } + previewBlob.value = null; + previewName.value = ""; previewType.value = ""; }; -const closePreview = () => { - previewVisible.value = false; - cleanupPreviewUrl(); -}; - // 根据文件名/扩展名+blob 的 mime 判断文件类型 const getFileType = (fileName, blob) => { const name = String(fileName || ""); @@ -342,7 +333,7 @@ const getFileType = (fileName, blob) => { return "other"; }; -// 浏览器直接下载 blob +// 浏览器直接下载 blob(指定文件名) const downloadBlob = (blob, fileName) => { const url = URL.createObjectURL(blob); const a = document.createElement("a"); @@ -354,6 +345,12 @@ const downloadBlob = (blob, fileName) => { setTimeout(() => URL.revokeObjectURL(url), 0); }; +// FilePreviewDialog 下载按钮回调 +const onPreviewDownload = ({ blob, fileName }) => { + if (!blob) return; + downloadBlob(blob, fileName); +}; + const viewReport = async (attachmentPath, originalName) => { if (!attachmentPath) return; const path = String(attachmentPath); @@ -361,9 +358,12 @@ const viewReport = async (attachmentPath, originalName) => { try { const blob = await getUploadFile(path); const type = getFileType(downloadName, blob); + // PDF / 图片走预览弹窗(PDF 走 vue-pdf-embed),其他格式直接下载 if (type === "pdf" || type === "image") { - cleanupPreviewUrl(); + cleanupPreview(); + previewBlob.value = blob; previewUrl.value = URL.createObjectURL(blob); + previewName.value = downloadName; previewType.value = type; previewVisible.value = true; } else {