功能(文档与桌面):完善文件预览下载与客户端构建基线
- 保存文档版本原始文件名,规范下载响应并持久化上传目录\n- 增加 PDF.js 预览、桌面保存打开流程及统一错误反馈\n- 统一 Node.js 22.13 构建基线并收紧临时文件权限门禁\n- 补充迁移、单元测试、发布检查与运维文档
This commit is contained in:
@@ -114,7 +114,7 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.actions" width="160" fixed="right">
|
||||
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.actions" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-actions">
|
||||
<el-button link type="primary" size="small" @click="previewVersion(row)" v-if="canReadDocument">
|
||||
@@ -172,14 +172,14 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-dialog v-if="previewVisible" append-to-body v-model="previewVisible" :title="previewTitle" width="720px" :close-on-click-modal="false">
|
||||
<el-dialog v-if="previewVisible" append-to-body v-model="previewVisible" :title="previewTitle" :width="previewType === 'pdf' ? 'min(86vw, 1440px)' : 'min(1180px, 94vw)'" :top="previewType === 'pdf' ? '8vh' : '3vh'" :close-on-click-modal="false" :show-close="previewType !== 'pdf'" :class="['pdf-preview-dialog', { 'pdf-preview-dialog--flush': previewType === 'pdf' }]">
|
||||
<div v-if="previewError" class="preview-error">
|
||||
{{ previewError }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-loading="previewLoading" class="preview-body">
|
||||
<img v-if="previewType === 'image' && previewUrl" :src="previewUrl" class="preview-media" />
|
||||
<iframe v-else-if="previewType === 'pdf' && previewUrl" :src="previewUrl" class="preview-frame" />
|
||||
<PdfViewer v-else-if="previewType === 'pdf' && previewUrl" :src="previewUrl" :filename="previewTitle" show-close @close="previewVisible = false" />
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -350,7 +350,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||
import { computed, defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from "element-plus";
|
||||
import { Edit, Upload, Share } from "@element-plus/icons-vue";
|
||||
@@ -380,8 +380,12 @@ import StateError from "../../components/StateError.vue";
|
||||
import StateLoading from "../../components/StateLoading.vue";
|
||||
import { onDesktopRefreshCurrentView } from "../../composables/useDesktopRefresh";
|
||||
import { openFileWithFeedback, pickFilesWithFeedback, saveFileWithFeedback } from "../../utils/fileTaskFeedback";
|
||||
import { getApiErrorMessage } from "../../utils/apiErrorMessage";
|
||||
import { getContentDispositionFilename } from "../../utils/contentDisposition";
|
||||
import { prepareSaveFile } from "../../runtime";
|
||||
|
||||
const route = useRoute();
|
||||
const PdfViewer = defineAsyncComponent(() => import("../../components/PdfViewer.vue"));
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
const { roleLabel, loadRoleTemplates } = useRoleTemplateMeta();
|
||||
@@ -753,24 +757,24 @@ const submitUpload = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const getFilename = (header?: string | null) => {
|
||||
if (!header) return null;
|
||||
const match = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(header);
|
||||
if (!match) return null;
|
||||
return decodeURIComponent(match[1] || match[2] || "");
|
||||
};
|
||||
|
||||
const getVersionFileName = (version: DocumentVersion) => {
|
||||
if (version.original_filename?.trim()) return version.original_filename.trim();
|
||||
const uri = version.file_uri || "";
|
||||
const name = uri.split(/[\\/]/).filter(Boolean).pop() || "";
|
||||
return name.toLowerCase();
|
||||
const uuidStorageName = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}(\.[^.]*)?$/i;
|
||||
if (uuidStorageName.test(name)) {
|
||||
const extension = name.match(/(\.[^.]*)$/)?.[1] || "";
|
||||
const title = detail.title?.trim() || `document-${version.version_no || version.id}`;
|
||||
return extension && title.toLowerCase().endsWith(extension.toLowerCase()) ? title : `${title}${extension}`;
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
const detectPreviewType = (version: DocumentVersion) => {
|
||||
const mime = (version.mime_type || "").toLowerCase();
|
||||
if (mime.startsWith("image/")) return "image";
|
||||
if (mime === "application/pdf") return "pdf";
|
||||
const name = getVersionFileName(version);
|
||||
const name = getVersionFileName(version).toLowerCase();
|
||||
if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg"].some((ext) => name.endsWith(ext))) return "image";
|
||||
if (name.endsWith(".pdf")) return "pdf";
|
||||
return "other";
|
||||
@@ -786,7 +790,7 @@ const previewVersion = async (version: DocumentVersion) => {
|
||||
if (!canReadDocument.value) { ElMessage.warning("权限不足"); return; }
|
||||
clearPreviewObjectUrl();
|
||||
previewType.value = detectPreviewType(version);
|
||||
previewTitle.value = `${detail.title || TEXT.modules.fileVersionManagement.title} V${version.version_no}`;
|
||||
previewTitle.value = getVersionFileName(version) || `${detail.title || TEXT.modules.fileVersionManagement.title} V${version.version_no}`;
|
||||
previewUrl.value = "";
|
||||
previewError.value = "";
|
||||
previewVisible.value = true;
|
||||
@@ -802,34 +806,49 @@ const previewVersion = async (version: DocumentVersion) => {
|
||||
previewObjectUrl.value = URL.createObjectURL(blob);
|
||||
previewUrl.value = previewObjectUrl.value;
|
||||
} catch (e: any) {
|
||||
previewError.value = e?.response?.data?.message || TEXT.common.messages.previewNotSupported;
|
||||
previewError.value = await getApiErrorMessage(e, TEXT.common.messages.previewNotSupported);
|
||||
} finally {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const downloadVersion = async (version: DocumentVersion) => {
|
||||
const suggestedName = getVersionFileName(version) || `document-${version.version_no || version.id}.bin`;
|
||||
try {
|
||||
const destination = await prepareSaveFile(suggestedName);
|
||||
if (!destination) {
|
||||
ElMessage.info("已取消保存文件");
|
||||
return;
|
||||
}
|
||||
const response = await downloadDocumentVersion(version.id);
|
||||
const contentType = response.headers?.["content-type"] || "application/octet-stream";
|
||||
const filename = getFilename(response.headers?.["content-disposition"]) || `document-${version.version_no || version.id}.bin`;
|
||||
const filename = getContentDispositionFilename(response.headers?.["content-disposition"]) || suggestedName;
|
||||
const blob = new Blob([response.data], { type: contentType });
|
||||
await saveFileWithFeedback({ suggestedName: filename, mimeType: contentType, data: blob });
|
||||
} catch (e: any) { ElMessage.error(e?.response?.data?.message || TEXT.common.messages.downloadFailed); }
|
||||
await saveFileWithFeedback({ suggestedName: filename, mimeType: contentType, data: blob }, {}, destination);
|
||||
} catch (e: any) { ElMessage.error(await getApiErrorMessage(e, TEXT.common.messages.downloadFailed)); }
|
||||
};
|
||||
|
||||
const openVersion = async (version: DocumentVersion) => {
|
||||
let response;
|
||||
try {
|
||||
response = await downloadDocumentVersion(version.id);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(await getApiErrorMessage(e, TEXT.common.messages.downloadFailed));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await downloadDocumentVersion(version.id);
|
||||
const contentType = response.headers?.["content-type"] || "application/octet-stream";
|
||||
const filename = getFilename(response.headers?.["content-disposition"]) || `document-${version.version_no || version.id}.bin`;
|
||||
const filename = getContentDispositionFilename(response.headers?.["content-disposition"])
|
||||
|| getVersionFileName(version)
|
||||
|| `document-${version.version_no || version.id}.bin`;
|
||||
await openFileWithFeedback({
|
||||
suggestedName: filename,
|
||||
mimeType: contentType,
|
||||
data: new Blob([response.data], { type: contentType }),
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.downloadFailed);
|
||||
} catch {
|
||||
ElMessage.error(TEXT.common.messages.openFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1119,12 +1138,6 @@ onBeforeUnmount(() => {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.preview-frame {
|
||||
width: 100%;
|
||||
height: 520px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.preview-media {
|
||||
max-width: 100%;
|
||||
max-height: 520px;
|
||||
|
||||
Reference in New Issue
Block a user