From d4992db19810f3151df60983fccf6d6a0b9a8e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B4=BE=E5=90=8C=E5=AD=A6?= Date: Tue, 23 Sep 2025 15:15:20 +0800 Subject: [PATCH 1/4] =?UTF-8?q?UPDATE:=20=E4=BC=98=E5=8C=96=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E5=AF=BC=E5=85=A5=E6=8F=90=E7=A4=BA=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/ImportExcel/index.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/ImportExcel/index.vue b/frontend/src/components/ImportExcel/index.vue index ae4b8bc..099cb97 100644 --- a/frontend/src/components/ImportExcel/index.vue +++ b/frontend/src/components/ImportExcel/index.vue @@ -106,6 +106,7 @@ const uploadExcel = async (param: UploadRequestOptions) => { isCover.value && excelFormData.append('isCover', isCover.value as unknown as Blob) //await parameter.value.importApi!(excelFormData); await parameter.value.importApi!(excelFormData).then(res => handleImportResponse(res)) + parameter.value.getTableList && parameter.value.getTableList() dialogVisible.value = false } @@ -121,7 +122,10 @@ async function handleImportResponse(res: any) { if (jsonData.code === 'A0000') { ElMessage.success('导入成功') } else { - ElMessage.error(jsonData.message) + ElMessageBox.alert(jsonData.message, { + title: '导入结果', + type: 'error' + }) } emit('result', jsonData.data) } catch (err) { From 5730b9c5cf404f0ca1e871d873a71933fe6a31dc Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Tue, 23 Sep 2025 16:14:03 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E6=AF=94=E5=AF=B9=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E7=9A=84=E6=A3=80=E6=B5=8B=E6=8A=A5=E5=91=8A=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=92=8C=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/index.ts | 10 +- frontend/src/api/plan/plan.ts | 5 + frontend/src/hooks/useDownload.ts | 119 +++++++++++++++++- .../home/components/reportResultPopup.vue | 31 ++++- frontend/src/views/home/components/table.vue | 18 ++- 5 files changed, 174 insertions(+), 9 deletions(-) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 9328762..31e2671 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -133,6 +133,10 @@ class RequestHttp { await router.replace(LOGIN_URL) return Promise.reject(data) } + // 对于blob类型的响应,返回完整的response对象以保留响应头 + if (response.config.responseType === 'blob') { + return response + } return data }, async (error: AxiosError) => { @@ -171,6 +175,10 @@ class RequestHttp { } download(url: string, params?: object, _object = {}): Promise { + return this.service.post(url, params, { ..._object, responseType: 'blob' }).then(res => res.data) + } + + downloadWithHeaders(url: string, params?: object, _object = {}): Promise> { return this.service.post(url, params, { ..._object, responseType: 'blob' }) } @@ -189,7 +197,7 @@ class RequestHttp { ..._object, headers: { 'Content-Type': 'multipart/form-data' }, responseType: 'blob' - }) + }).then(res => res.data) } // 添加SSE连接方法 diff --git a/frontend/src/api/plan/plan.ts b/frontend/src/api/plan/plan.ts index e30c361..632d652 100644 --- a/frontend/src/api/plan/plan.ts +++ b/frontend/src/api/plan/plan.ts @@ -85,6 +85,11 @@ export const downloadDevData = (params: Device.ReqDevReportParams) => { return http.download(`/report/downloadReport`, params) } +// 装置检测报告下载(带响应头) +export const downloadDevDataWithHeaders = (params: Device.ReqDevReportParams) => { + return http.downloadWithHeaders(`/report/downloadReport`, params) +} + export const staticsAnalyse = (params: { id: string[] }) => { return http.download('/adPlan/analyse', params) } diff --git a/frontend/src/hooks/useDownload.ts b/frontend/src/hooks/useDownload.ts index 3593d8b..bfdb183 100644 --- a/frontend/src/hooks/useDownload.ts +++ b/frontend/src/hooks/useDownload.ts @@ -1,4 +1,5 @@ import { ElNotification } from "element-plus"; +import type { AxiosResponse } from "axios"; /** * @description 接收数据流生成 blob,创建链接,下载文件 @@ -8,6 +9,55 @@ import { ElNotification } from "element-plus"; * @param {Boolean} isNotify 是否有导出消息提示 (默认为 true) * @param {String} fileType 导出的文件格式 (默认为.xlsx) * */ +/** + * 从 Content-Disposition 头解析文件名 + */ +const getFileNameFromContentDisposition = (contentDisposition: string | undefined | null): string | null => { + if (!contentDisposition) return null; + + // 优先匹配 filename*=UTF-8'' 格式(RFC 5987) + const filenameStarRegex = /filename\*\s*=\s*UTF-8''([^;]+)/i; + const starMatches = filenameStarRegex.exec(contentDisposition); + + if (starMatches && starMatches[1]) { + try { + return decodeURIComponent(starMatches[1]); + } catch (e) { + console.warn('解码 filename* 失败:', e); + } + } + + // 其次匹配 filename="文件名" 或 filename=文件名 格式 + const filenameRegex = /filename\s*=\s*([^;]+)/i; + const matches = filenameRegex.exec(contentDisposition); + + if (matches && matches[1]) { + let filename = matches[1].trim(); + + // 去除引号 + filename = filename.replace(/^["']|["']$/g, ""); + + // 尝试解码 URL 编码的文件名 + try { + // 检查是否包含 URL 编码字符 + if (filename.includes('%') || filename.includes('UTF-8')) { + // 处理可能的 UTF-8 前缀 + if (filename.startsWith("UTF-8''") || filename.startsWith("utf-8''")) { + filename = filename.substring(7); + } + filename = decodeURIComponent(filename); + } + } catch (e) { + // 如果解码失败,返回原始文件名 + console.warn('解码文件名失败,使用原始值:', e); + } + + return filename; + } + + return null; +}; + export const useDownload = async ( api: (param: any) => Promise, tempName: string, @@ -39,6 +89,73 @@ export const useDownload = async ( document.body.removeChild(exportFile); window.URL.revokeObjectURL(blobUrl); } catch (error) { - + + } +}; + +/** + * @description 支持服务器文件名的下载方法,会从 Content-Disposition 头获取文件名 + * @param {Function} api 导出表格的api方法 (必传) + * @param {String} fallbackName 备用文件名,当服务器未提供时使用 (可选) + * @param {Object} params 导出的参数 (默认{}) + * @param {Boolean} isNotify 是否有导出消息提示 (默认为 true) + * @param {String} fallbackFileType 备用文件格式,当服务器未提供时使用 (默认为.xlsx) + */ +export const useDownloadWithServerFileName = async ( + api: (param: any) => Promise | any>, + fallbackName: string = "", + params: any = {}, + isNotify: boolean = true, + fallbackFileType: string = ".xlsx" +) => { + if (isNotify) { + ElNotification({ + title: "温馨提示", + message: "如果数据庞大会导致下载缓慢哦,请您耐心等待!", + type: "info", + duration: 3000 + }); + } + try { + const res = await api(params); + + // 检查响应是否包含 data 属性(AxiosResponse)还是直接是 Blob + const blob = res.data ? new Blob([res.data]) : new Blob([res]); + + // 尝试从响应头获取文件名(如果存在) + let serverFileName: string | null = null; + if (res.headers) { + const headers = res.headers || {}; + const contentDisposition = headers['content-disposition'] || headers['Content-Disposition']; + serverFileName = getFileNameFromContentDisposition(contentDisposition); + } + + // 确定最终使用的文件名 + let finalFileName: string; + if (serverFileName) { + finalFileName = serverFileName; + } else if (fallbackName) { + finalFileName = `${fallbackName}${fallbackFileType}`; + } else { + finalFileName = `download${fallbackFileType}`; + } + + // 兼容 edge 不支持 createObjectURL 方法 + if ("msSaveOrOpenBlob" in navigator) { + return window.navigator.msSaveOrOpenBlob(blob, finalFileName); + } + + const blobUrl = window.URL.createObjectURL(blob); + const exportFile = document.createElement("a"); + exportFile.style.display = "none"; + exportFile.download = finalFileName; + exportFile.href = blobUrl; + document.body.appendChild(exportFile); + exportFile.click(); + // 去除下载对 url 的影响 + document.body.removeChild(exportFile); + window.URL.revokeObjectURL(blobUrl); + } catch (error) { + console.error('文件下载失败:', error); } }; diff --git a/frontend/src/views/home/components/reportResultPopup.vue b/frontend/src/views/home/components/reportResultPopup.vue index 9125db8..ee688d8 100644 --- a/frontend/src/views/home/components/reportResultPopup.vue +++ b/frontend/src/views/home/components/reportResultPopup.vue @@ -89,7 +89,7 @@ import { getMonitorDataSourceResult, getMonitorResult, updateMonitorResult } from '@/api/result/result' import { type MonitorResult } from '@/api/result/interface' +import { generateDevReport } from '@/api/plan/plan' +import { useCheckStore } from '@/stores/modules/check' +import { ElMessage } from 'element-plus' const dialogVisible = ref(false) const dialogSourceVisible = ref(false) const devData = ref() const activeName = ref(0) +const checkStore = useCheckStore() + +// 定义 emit 事件 +const emit = defineEmits<{ + (e: 'reportGenerated'): void +}>() const resultData = ref([]) const resultSourceData = ref({}) const whichTimeData = ref([]) @@ -228,6 +237,26 @@ const handleSureChoose = () => { dialogSourceVisible.value = false }) } + +// 处理确认生成报告 +const handleConfirmGenerate = async () => { + try { + await generateDevReport({ + planId: checkStore.plan.id, + devIdList: [devData.value.id], + scriptId: checkStore.plan.scriptId, + planCode: checkStore.plan.code + '', + pageNum: 1, + pageSize: 999 + }) + ElMessage.success({ message: `报告生成成功!` }) + dialogVisible.value = false + emit('reportGenerated') // 触发事件通知父组件 + } catch (error) { + ElMessage.error('报告生成失败') + console.error('报告生成错误:', error) + } +} defineExpose({ open }) diff --git a/frontend/src/views/home/components/table.vue b/frontend/src/views/home/components/table.vue index 83062ef..ffb5485 100644 --- a/frontend/src/views/home/components/table.vue +++ b/frontend/src/views/home/components/table.vue @@ -195,7 +195,7 @@ :append-to-body="true" /> - + @@ -214,13 +214,13 @@ import { type Device } from '@/api/device/interface/device' import { type ColumnProps, type ProTableInstance } from '@/components/ProTable/interface' import { type Plan } from '@/api/plan/interface' import { type StandardDevice } from '@/api/device/interface/standardDevice' -import { downloadDevData, generateDevReport, getBoundPqDevList } from '@/api/plan/plan' +import { downloadDevData, downloadDevDataWithHeaders, generateDevReport, getBoundPqDevList } from '@/api/plan/plan' import { getPqDev } from '@/api/device/device' import { useAppSceneStore, useModeStore } from '@/stores/modules/mode' // 引入模式 store import { useCheckStore } from '@/stores/modules/check' import { CheckData } from '@/api/check/interface' import { useAuthStore } from '@/stores/modules/auth' -import { useDownload } from '@/hooks/useDownload' +import { useDownload, useDownloadWithServerFileName } from '@/hooks/useDownload' import { documentedPqDev } from '@/api/device/report' import { ResultEnum } from '@/enums/httpEnum' import { getPqMonList } from '@/api/device/monitor/index.ts' @@ -1033,6 +1033,7 @@ const openDrawer = async (title: string, row: any) => { // 单个设备报告生成 if (title === '报告生成') { if (modeStore.currentMode == '比对式'){ + console.log(row) reportPopup.value?.open(row) }else{ await generateDevReport({ @@ -1049,9 +1050,9 @@ const openDrawer = async (title: string, row: any) => { } if (title === '报告下载') { - await useDownload( - downloadDevData, - row.createId, + await useDownloadWithServerFileName( + downloadDevDataWithHeaders, + row.createId, // 备用文件名 { planId: checkStore.plan.id, devId: row.id @@ -1121,6 +1122,11 @@ const handleQuitClicked = () => { emit('batchGenerateClicked') // 触发事件 } +// 处理报告生成完成事件 +const handleReportGenerated = () => { + emit('batchGenerateClicked') // 触发事件通知父组件刷新数据 +} + defineExpose({ changeActiveTabs }) + diff --git a/frontend/src/views/plan/planList/components/childrenPlan.vue b/frontend/src/views/plan/planList/components/childrenPlan.vue index e92c89f..97142d4 100644 --- a/frontend/src/views/plan/planList/components/childrenPlan.vue +++ b/frontend/src/views/plan/planList/components/childrenPlan.vue @@ -7,11 +7,12 @@ :title="title" :width="width" class="table-box" - top="114px" + top="111px" @close="handleClose" >
@@ -23,19 +24,22 @@ :name="item.name" > - +