比对模式的检测报告生成和下载

This commit is contained in:
2025-09-23 16:14:03 +08:00
parent d4992db198
commit 5730b9c5cf
5 changed files with 174 additions and 9 deletions

View File

@@ -133,6 +133,10 @@ class RequestHttp {
await router.replace(LOGIN_URL) await router.replace(LOGIN_URL)
return Promise.reject(data) return Promise.reject(data)
} }
// 对于blob类型的响应返回完整的response对象以保留响应头
if (response.config.responseType === 'blob') {
return response
}
return data return data
}, },
async (error: AxiosError) => { async (error: AxiosError) => {
@@ -171,6 +175,10 @@ class RequestHttp {
} }
download(url: string, params?: object, _object = {}): Promise<BlobPart> { download(url: string, params?: object, _object = {}): Promise<BlobPart> {
return this.service.post(url, params, { ..._object, responseType: 'blob' }).then(res => res.data)
}
downloadWithHeaders(url: string, params?: object, _object = {}): Promise<AxiosResponse<Blob>> {
return this.service.post(url, params, { ..._object, responseType: 'blob' }) return this.service.post(url, params, { ..._object, responseType: 'blob' })
} }
@@ -189,7 +197,7 @@ class RequestHttp {
..._object, ..._object,
headers: { 'Content-Type': 'multipart/form-data' }, headers: { 'Content-Type': 'multipart/form-data' },
responseType: 'blob' responseType: 'blob'
}) }).then(res => res.data)
} }
// 添加SSE连接方法 // 添加SSE连接方法

View File

@@ -85,6 +85,11 @@ export const downloadDevData = (params: Device.ReqDevReportParams) => {
return http.download(`/report/downloadReport`, params) return http.download(`/report/downloadReport`, params)
} }
// 装置检测报告下载(带响应头)
export const downloadDevDataWithHeaders = (params: Device.ReqDevReportParams) => {
return http.downloadWithHeaders(`/report/downloadReport`, params)
}
export const staticsAnalyse = (params: { id: string[] }) => { export const staticsAnalyse = (params: { id: string[] }) => {
return http.download('/adPlan/analyse', params) return http.download('/adPlan/analyse', params)
} }

View File

@@ -1,4 +1,5 @@
import { ElNotification } from "element-plus"; import { ElNotification } from "element-plus";
import type { AxiosResponse } from "axios";
/** /**
* @description 接收数据流生成 blob创建链接下载文件 * @description 接收数据流生成 blob创建链接下载文件
@@ -8,6 +9,55 @@ import { ElNotification } from "element-plus";
* @param {Boolean} isNotify 是否有导出消息提示 (默认为 true) * @param {Boolean} isNotify 是否有导出消息提示 (默认为 true)
* @param {String} fileType 导出的文件格式 (默认为.xlsx) * @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 ( export const useDownload = async (
api: (param: any) => Promise<any>, api: (param: any) => Promise<any>,
tempName: string, tempName: string,
@@ -39,6 +89,73 @@ export const useDownload = async (
document.body.removeChild(exportFile); document.body.removeChild(exportFile);
window.URL.revokeObjectURL(blobUrl); window.URL.revokeObjectURL(blobUrl);
} catch (error) { } 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<AxiosResponse<Blob> | 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);
} }
}; };

View File

@@ -89,7 +89,7 @@
</el-tabs> </el-tabs>
<template #footer> <template #footer>
<el-button type="primary" size="small" @click="handleChooseClick">重新选择</el-button> <el-button type="primary" size="small" @click="handleChooseClick">重新选择</el-button>
<el-button type="primary">确认生成</el-button> <el-button type="primary" @click="handleConfirmGenerate">确认生成</el-button>
</template> </template>
<!-- 选择检测数据源弹框--> <!-- 选择检测数据源弹框-->
<el-dialog <el-dialog
@@ -157,11 +157,20 @@
<script setup lang="ts" name="reportPopup"> <script setup lang="ts" name="reportPopup">
import { getMonitorDataSourceResult, getMonitorResult, updateMonitorResult } from '@/api/result/result' import { getMonitorDataSourceResult, getMonitorResult, updateMonitorResult } from '@/api/result/result'
import { type MonitorResult } from '@/api/result/interface' 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 dialogVisible = ref(false)
const dialogSourceVisible = ref(false) const dialogSourceVisible = ref(false)
const devData = ref<any>() const devData = ref<any>()
const activeName = ref<number>(0) const activeName = ref<number>(0)
const checkStore = useCheckStore()
// 定义 emit 事件
const emit = defineEmits<{
(e: 'reportGenerated'): void
}>()
const resultData = ref<MonitorResult[]>([]) const resultData = ref<MonitorResult[]>([])
const resultSourceData = ref<any>({}) const resultSourceData = ref<any>({})
const whichTimeData = ref<any>([]) const whichTimeData = ref<any>([])
@@ -228,6 +237,26 @@ const handleSureChoose = () => {
dialogSourceVisible.value = false 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({ defineExpose({
open open
}) })

View File

@@ -195,7 +195,7 @@
:append-to-body="true" :append-to-body="true"
/> />
<!-- 报告生成弹框 --> <!-- 报告生成弹框 -->
<ReportResultPopup ref="reportPopup"></ReportResultPopup> <ReportResultPopup ref="reportPopup" @reportGenerated="handleReportGenerated"></ReportResultPopup>
</div> </div>
</template> </template>
@@ -214,13 +214,13 @@ import { type Device } from '@/api/device/interface/device'
import { type ColumnProps, type ProTableInstance } from '@/components/ProTable/interface' import { type ColumnProps, type ProTableInstance } from '@/components/ProTable/interface'
import { type Plan } from '@/api/plan/interface' import { type Plan } from '@/api/plan/interface'
import { type StandardDevice } from '@/api/device/interface/standardDevice' 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 { getPqDev } from '@/api/device/device'
import { useAppSceneStore, useModeStore } from '@/stores/modules/mode' // 引入模式 store import { useAppSceneStore, useModeStore } from '@/stores/modules/mode' // 引入模式 store
import { useCheckStore } from '@/stores/modules/check' import { useCheckStore } from '@/stores/modules/check'
import { CheckData } from '@/api/check/interface' import { CheckData } from '@/api/check/interface'
import { useAuthStore } from '@/stores/modules/auth' import { useAuthStore } from '@/stores/modules/auth'
import { useDownload } from '@/hooks/useDownload' import { useDownload, useDownloadWithServerFileName } from '@/hooks/useDownload'
import { documentedPqDev } from '@/api/device/report' import { documentedPqDev } from '@/api/device/report'
import { ResultEnum } from '@/enums/httpEnum' import { ResultEnum } from '@/enums/httpEnum'
import { getPqMonList } from '@/api/device/monitor/index.ts' import { getPqMonList } from '@/api/device/monitor/index.ts'
@@ -1033,6 +1033,7 @@ const openDrawer = async (title: string, row: any) => {
// 单个设备报告生成 // 单个设备报告生成
if (title === '报告生成') { if (title === '报告生成') {
if (modeStore.currentMode == '比对式'){ if (modeStore.currentMode == '比对式'){
console.log(row)
reportPopup.value?.open(row) reportPopup.value?.open(row)
}else{ }else{
await generateDevReport({ await generateDevReport({
@@ -1049,9 +1050,9 @@ const openDrawer = async (title: string, row: any) => {
} }
if (title === '报告下载') { if (title === '报告下载') {
await useDownload( await useDownloadWithServerFileName(
downloadDevData, downloadDevDataWithHeaders,
row.createId, row.createId, // 备用文件名
{ {
planId: checkStore.plan.id, planId: checkStore.plan.id,
devId: row.id devId: row.id
@@ -1121,6 +1122,11 @@ const handleQuitClicked = () => {
emit('batchGenerateClicked') // 触发事件 emit('batchGenerateClicked') // 触发事件
} }
// 处理报告生成完成事件
const handleReportGenerated = () => {
emit('batchGenerateClicked') // 触发事件通知父组件刷新数据
}
defineExpose({ changeActiveTabs }) defineExpose({ changeActiveTabs })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>