2358328281@qq.com 1 day ago
parent 0492acf424
commit 4bc1646766

297
package-lock.json generated

@ -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",

@ -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"
},

@ -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>
</el-card>
<el-dialog
v-model="previewVisible"
:title="$t('btn.preview')"
width="80%"
top="5vh"
destroy-on-close
:before-close="closePreview"
>
<iframe
v-if="previewUrl"
:src="previewUrl"
class="preview-iframe"
<FilePreviewDialog
:visible="previewVisible"
:blob-url="previewUrl"
:blob="previewBlob"
:file-name="previewName"
:file-type="previewType"
@update:visible="(v) => (previewVisible = v)"
@close="cleanupPreview"
@download="onPreviewDownload"
/>
</el-dialog>
</div>
</template>
@ -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();
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'));
}

@ -244,25 +244,16 @@
</template>
</el-dialog>
<el-dialog
v-model="previewVisible"
:title="$t('btn.preview')"
width="80%"
top="5vh"
destroy-on-close
:before-close="closePreview"
>
<iframe
v-if="previewType === 'pdf' && previewUrl"
:src="previewUrl"
class="preview-iframe"
<FilePreviewDialog
:visible="previewVisible"
:blob-url="previewUrl"
:blob="previewBlob"
:file-name="previewName"
:file-type="previewType"
@update:visible="(v) => (previewVisible = v)"
@close="cleanupPreview"
@download="onPreviewDownload"
/>
<img
v-else-if="previewType === 'image' && previewUrl"
:src="previewUrl"
class="preview-image"
/>
</el-dialog>
</div>
</template>
@ -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 {

Loading…
Cancel
Save