| New file |
| | |
| | | # Word 文档在线预览实现说明 |
| | | |
| | | ## 📋 实现方案 |
| | | |
| | | 使用 `docx-preview` 库实现 Word 文档的在线预览功能。 |
| | | |
| | | ## 🔧 技术栈 |
| | | |
| | | - **docx-preview**: 0.3.7 |
| | | - **Vue.js**: 2.x |
| | | - **Element UI**: 用于弹窗和按钮组件 |
| | | |
| | | ## 📦 安装依赖 |
| | | |
| | | ```bash |
| | | npm install docx-preview |
| | | ``` |
| | | |
| | | ## 🎯 核心功能 |
| | | |
| | | ### 1. 文档预览流程 |
| | | |
| | | ``` |
| | | 用户点击"查看报告" |
| | | ↓ |
| | | 获取 templateId 和 checkTaskId |
| | | ↓ |
| | | 调用文件流接口(携带 token) |
| | | ↓ |
| | | 接收 Blob 数据 |
| | | ↓ |
| | | 使用 docx-preview 渲染到容器 |
| | | ↓ |
| | | 在弹窗中显示预览内容 |
| | | ↓ |
| | | 提供下载按钮 |
| | | ``` |
| | | |
| | | ### 2. 关键代码实现 |
| | | |
| | | #### 导入库 |
| | | ```javascript |
| | | import { renderAsync } from "docx-preview"; |
| | | ``` |
| | | |
| | | #### 渲染文档 |
| | | ```javascript |
| | | const container = this.$refs.docxContainer; |
| | | await renderAsync(blob, container, null, { |
| | | className: "docx-preview", |
| | | inWrapper: true, |
| | | ignoreWidth: false, |
| | | ignoreHeight: false, |
| | | ignoreFonts: false, |
| | | breakPages: true, |
| | | experimental: true, |
| | | renderHeaders: true, |
| | | renderFooters: true, |
| | | renderFootnotes: true, |
| | | renderEndnotes: true, |
| | | }); |
| | | ``` |
| | | |
| | | #### 预览容器 |
| | | ```html |
| | | <div |
| | | id="docx-preview-container" |
| | | ref="docxContainer" |
| | | style="width: 100%; height: 100%; overflow: auto; background: #fff; padding: 20px;" |
| | | ></div> |
| | | ``` |
| | | |
| | | ## ⚙️ 配置选项说明 |
| | | |
| | | | 选项 | 类型 | 说明 | |
| | | |------|------|------| |
| | | | `className` | string | 自定义类名 | |
| | | | `inWrapper` | boolean | 启用包装器 | |
| | | | `ignoreWidth` | boolean | 是否忽略宽度 | |
| | | | `ignoreHeight` | boolean | 是否忽略高度 | |
| | | | `ignoreFonts` | boolean | 是否忽略字体 | |
| | | | `breakPages` | boolean | 是否分页显示 | |
| | | | `experimental` | boolean | 启用实验性功能 | |
| | | | `renderHeaders` | boolean | 渲染页眉 | |
| | | | `renderFooters` | boolean | 渲染页脚 | |
| | | | `renderFootnotes` | boolean | 渲染脚注 | |
| | | | `renderEndnotes` | boolean | 渲染尾注 | |
| | | |
| | | ## 🎨 样式优化 |
| | | |
| | | ```less |
| | | #docx-preview-container { |
| | | ::v-deep .docx-wrapper { |
| | | background: #fff; |
| | | padding: 30px; |
| | | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); |
| | | |
| | | section.docx { |
| | | background: white; |
| | | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); |
| | | margin-bottom: 20px; |
| | | } |
| | | } |
| | | |
| | | ::v-deep .docx { |
| | | font-family: "Microsoft YaHei", "微软雅黑", Arial, sans-serif; |
| | | |
| | | table { |
| | | border-collapse: collapse; |
| | | |
| | | td, th { |
| | | border: 1px solid #ddd; |
| | | padding: 8px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | ## ✨ 功能特性 |
| | | |
| | | ### 1. 在线预览 |
| | | - ✅ 支持 .doc 和 .docx 格式 |
| | | - ✅ 保留文档格式(字体、颜色、样式) |
| | | - ✅ 支持表格、图片、列表等元素 |
| | | - ✅ 支持页眉、页脚、脚注 |
| | | - ✅ 分页显示 |
| | | |
| | | ### 2. 下载功能 |
| | | - ✅ 预览的同时提供下载按钮 |
| | | - ✅ 下载原始 Word 文档 |
| | | - ✅ 自动生成文件名 |
| | | |
| | | ### 3. 内存管理 |
| | | - ✅ 关闭弹窗时清空容器 |
| | | - ✅ 组件销毁时释放资源 |
| | | - ✅ 避免内存泄漏 |
| | | |
| | | ## 📊 支持的文档元素 |
| | | |
| | | | 元素类型 | 支持程度 | 说明 | |
| | | |---------|---------|------| |
| | | | 文本 | ✅ 完全支持 | 包括字体、大小、颜色、样式 | |
| | | | 段落 | ✅ 完全支持 | 对齐、缩进、间距 | |
| | | | 表格 | ✅ 完全支持 | 边框、合并单元格、样式 | |
| | | | 图片 | ✅ 完全支持 | 内嵌图片、浮动图片 | |
| | | | 列表 | ✅ 完全支持 | 有序列表、无序列表 | |
| | | | 页眉页脚 | ✅ 完全支持 | 页眉、页脚内容 | |
| | | | 超链接 | ✅ 完全支持 | 内部链接、外部链接 | |
| | | | 书签 | ⚠️ 部分支持 | 基本书签功能 | |
| | | | 批注 | ❌ 不支持 | 不显示批注内容 | |
| | | | 修订 | ❌ 不支持 | 不显示修订记录 | |
| | | |
| | | ## ⚠️ 注意事项 |
| | | |
| | | ### 1. 文件格式 |
| | | - 推荐使用 `.docx` 格式(Office 2007+) |
| | | - `.doc` 格式(Office 2003)支持有限 |
| | | - 复杂格式可能显示不完全 |
| | | |
| | | ### 2. 文件大小 |
| | | - 建议文件大小不超过 5MB |
| | | - 大文件可能导致渲染缓慢 |
| | | - 可以添加 loading 提示 |
| | | |
| | | ### 3. 浏览器兼容性 |
| | | - Chrome: ✅ 完全支持 |
| | | - Firefox: ✅ 完全支持 |
| | | - Edge: ✅ 完全支持 |
| | | - Safari: ✅ 完全支持 |
| | | - IE 11: ⚠️ 部分支持(需要 polyfill) |
| | | |
| | | ### 4. 字体问题 |
| | | - 如果文档使用特殊字体,可能显示为默认字体 |
| | | - 建议使用常见字体(微软雅黑、宋体等) |
| | | - 可以通过 CSS 指定备用字体 |
| | | |
| | | ## 🐛 常见问题 |
| | | |
| | | ### 1. 文档渲染失败 |
| | | **原因**: |
| | | - 文件格式不支持 |
| | | - 文件损坏 |
| | | - Blob 数据为空 |
| | | |
| | | **解决方案**: |
| | | ```javascript |
| | | // 检查 blob 是否有效 |
| | | if (!blob || blob.size === 0) { |
| | | this.$message.error("报告文件为空"); |
| | | return; |
| | | } |
| | | |
| | | // 添加错误处理 |
| | | try { |
| | | await renderAsync(blob, container); |
| | | } catch (error) { |
| | | console.error("渲染失败:", error); |
| | | this.$message.error("文档渲染失败,请尝试下载查看"); |
| | | } |
| | | ``` |
| | | |
| | | ### 2. 样式显示不正确 |
| | | **原因**: |
| | | - CSS 冲突 |
| | | - 文档使用了不支持的样式 |
| | | |
| | | **解决方案**: |
| | | ```less |
| | | // 使用 ::v-deep 确保样式生效 |
| | | #docx-preview-container { |
| | | ::v-deep .docx { |
| | | // 自定义样式 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | ### 3. 图片不显示 |
| | | **原因**: |
| | | - 图片路径错误 |
| | | - 图片格式不支持 |
| | | - 跨域问题 |
| | | |
| | | **解决方案**: |
| | | - 确保图片是内嵌在文档中的 |
| | | - 检查图片格式(支持 PNG、JPG、GIF) |
| | | |
| | | ### 4. 中文字体显示异常 |
| | | **原因**: |
| | | - 缺少中文字体 |
| | | - 字体编码问题 |
| | | |
| | | **解决方案**: |
| | | ```less |
| | | ::v-deep .docx { |
| | | font-family: "Microsoft YaHei", "微软雅黑", "SimSun", "宋体", Arial, sans-serif; |
| | | } |
| | | ``` |
| | | |
| | | ## 🔄 与其他方案对比 |
| | | |
| | | | 方案 | 优点 | 缺点 | 适用场景 | |
| | | |------|------|------|---------| |
| | | | **docx-preview** | ✅ 前端实现<br>✅ 无需后端支持<br>✅ 格式保留较好 | ❌ 复杂格式支持有限<br>❌ 大文件性能差 | 中小型文档预览 | |
| | | | **Office Online Viewer** | ✅ 格式完美<br>✅ 功能强大 | ❌ 需要公网访问<br>❌ 需要微软服务 | 公网环境 | |
| | | | **后端转 PDF** | ✅ 格式稳定<br>✅ 浏览器原生支持 | ❌ 需要后端处理<br>❌ 转换耗时 | 对格式要求高 | |
| | | | **直接下载** | ✅ 最简单<br>✅ 格式完整 | ❌ 用户体验差<br>❌ 需要本地软件 | 备用方案 | |
| | | |
| | | ## 📈 性能优化建议 |
| | | |
| | | ### 1. 懒加载 |
| | | ```javascript |
| | | // 只在需要时加载 docx-preview |
| | | const { renderAsync } = await import('docx-preview'); |
| | | ``` |
| | | |
| | | ### 2. 缓存机制 |
| | | ```javascript |
| | | // 缓存已加载的文档 |
| | | const cacheKey = `report_${checkTaskId}`; |
| | | if (this.reportCache[cacheKey]) { |
| | | // 使用缓存 |
| | | return this.reportCache[cacheKey]; |
| | | } |
| | | ``` |
| | | |
| | | ### 3. 分页加载 |
| | | ```javascript |
| | | // 对于大文档,可以考虑分页加载 |
| | | breakPages: true, // 启用分页 |
| | | ``` |
| | | |
| | | ### 4. 压缩传输 |
| | | ```javascript |
| | | // 后端使用 gzip 压缩 |
| | | // 前端自动解压 |
| | | ``` |
| | | |
| | | ## 🎯 后续优化方向 |
| | | |
| | | ### 1. 添加工具栏 |
| | | - 缩放功能(放大/缩小) |
| | | - 打印功能 |
| | | - 全屏预览 |
| | | - 页面跳转 |
| | | |
| | | ### 2. 增强交互 |
| | | - 文本选择和复制 |
| | | - 搜索功能 |
| | | - 目录导航 |
| | | |
| | | ### 3. 移动端适配 |
| | | - 响应式布局 |
| | | - 触摸手势支持 |
| | | - 移动端优化 |
| | | |
| | | ### 4. 性能优化 |
| | | - 虚拟滚动 |
| | | - 按需渲染 |
| | | - Web Worker 处理 |
| | | |
| | | ## 📞 技术支持 |
| | | |
| | | ### 官方文档 |
| | | - GitHub: https://github.com/VolodymyrBaydalka/docxjs |
| | | - NPM: https://www.npmjs.com/package/docx-preview |
| | | |
| | | ### 常用 API |
| | | ```javascript |
| | | // 基本渲染 |
| | | renderAsync(blob, container, styleContainer, options) |
| | | |
| | | // 销毁实例 |
| | | // 清空容器即可 |
| | | container.innerHTML = ''; |
| | | ``` |
| | | |
| | | ## 🎉 总结 |
| | | |
| | | 使用 `docx-preview` 实现 Word 文档在线预览是一个不错的前端解决方案: |
| | | |
| | | **优点**: |
| | | - ✅ 纯前端实现,无需后端支持 |
| | | - ✅ 支持大部分常用格式 |
| | | - ✅ 用户体验好,即时预览 |
| | | - ✅ 同时提供下载功能 |
| | | |
| | | **缺点**: |
| | | - ⚠️ 复杂格式支持有限 |
| | | - ⚠️ 大文件性能较差 |
| | | - ⚠️ 某些特殊元素不支持 |
| | | |
| | | **建议**: |
| | | - 对于简单的报告文档,这个方案完全够用 |
| | | - 如果文档格式复杂,建议后端转 PDF |
| | | - 始终提供下载按钮作为备用方案 |
| | | |
| | | 现在你可以测试一下预览功能了!🚀 |
| | | |
| New file |
| | |
| | | # docx-preview 配置说明 |
| | | |
| | | ## 问题背景 |
| | | |
| | | docx-preview 库使用了 ES2020 语法(可选链操作符 `?.`),而项目的 Babel 配置较旧,不支持这些新语法,导致编译报错。 |
| | | |
| | | ## 解决方案 |
| | | |
| | | ### 1. 安装 Babel 插件 |
| | | |
| | | 安装支持 ES2020 语法的 Babel 插件: |
| | | |
| | | ```bash |
| | | npm install --save-dev @babel/plugin-transform-optional-chaining @babel/plugin-transform-nullish-coalescing-operator |
| | | ``` |
| | | |
| | | ### 2. 更新 .babelrc 配置 |
| | | |
| | | 修改 `.babelrc` 文件,添加新的插件: |
| | | |
| | | ```json |
| | | { |
| | | "presets": [ |
| | | [ |
| | | "@babel/preset-env", |
| | | { |
| | | "modules": false, |
| | | "targets": { |
| | | "browsers": ["> 1%", "last 2 versions", "not ie <= 11"] |
| | | } |
| | | } |
| | | ] |
| | | ], |
| | | "plugins": [ |
| | | "@babel/plugin-transform-optional-chaining", |
| | | "@babel/plugin-transform-nullish-coalescing-operator", |
| | | [ |
| | | "component", |
| | | { |
| | | "libraryName": "element-ui", |
| | | "styleLibraryName": "theme-chalk" |
| | | } |
| | | ] |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | ### 3. 更新 vue.config.js |
| | | |
| | | 添加 `transpileDependencies` 配置,让 Babel 转译 `docx-preview` 包: |
| | | |
| | | ```javascript |
| | | module.exports = { |
| | | // ... 其他配置 |
| | | transpileDependencies: [ |
| | | 'docx-preview' |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | ### 4. 清理缓存并重启 |
| | | |
| | | ```bash |
| | | # 清理 webpack 缓存 |
| | | Remove-Item -Recurse -Force node_modules\.cache |
| | | |
| | | # 重启开发服务器 |
| | | npm run dev:dev |
| | | ``` |
| | | |
| | | ## 使用示例 |
| | | |
| | | ```javascript |
| | | import { renderAsync } from "docx-preview"; |
| | | |
| | | // 渲染 Word 文档 |
| | | async renderWordDocument(blob) { |
| | | await renderAsync(blob, this.$refs.docxContainer, null, { |
| | | className: "docx-preview", |
| | | inWrapper: true, |
| | | ignoreWidth: false, |
| | | ignoreHeight: false, |
| | | ignoreFonts: false, |
| | | breakPages: true, |
| | | renderHeaders: true, |
| | | renderFooters: true, |
| | | renderFootnotes: true, |
| | | renderEndnotes: true |
| | | }); |
| | | } |
| | | ``` |
| | | |
| | | ## 优势 |
| | | |
| | | 1. ✅ **完美保留样式** - docx-preview 格式还原度 90%+ |
| | | 2. ✅ **无跨域问题** - 直接使用文件流(Blob) |
| | | 3. ✅ **支持复杂格式** - 表格、图片、页眉页脚等 |
| | | 4. ✅ **一次请求** - 直接从后端获取文件流 |
| | | |
| | | ## 注意事项 |
| | | |
| | | - 修改配置后必须清理缓存并重启开发服务器 |
| | | - `transpileDependencies` 会增加编译时间,但确保兼容性 |
| | | - 如果遇到其他 ES2020 语法问题,可以继续添加相应的 Babel 插件 |
| | | |
| | |
| | | width="1420px" |
| | | @close="closeReportDialog" |
| | | > |
| | | <div style="height: 691px" class="oa"> |
| | | <iframe |
| | | v-if="reportUrl" |
| | | :src="reportUrl" |
| | | width="100%" |
| | | height="100%" |
| | | frameborder="0" |
| | | ></iframe> |
| | | <div v-else class="flex a-center j-center h100 c-fff fs--16"> |
| | | 暂无报告数据 |
| | | </div> |
| | | <div style="height: 691px;" class="flex flex-column a-center j-center" v-loading="loading"> |
| | | <div class="c-fff fs--18 mb--20">报告已生成</div> |
| | | <div class="c-fff fs--14 mb--30">报告地址:{{ reportUrl }}</div> |
| | | <el-button type="primary" @click="downloadReport">下载报告</el-button> |
| | | </div> |
| | | </CustomDialog> |
| | | <!-- 检查资料详情弹窗 --> |
| | |
| | | getDeviceDetailCheckList, |
| | | getDeviceDetailCheckPlanList, |
| | | getDeviceDetailCheckPlanDetail, |
| | | getFuzzyReportUrl, |
| | | getScoreReportUrl, |
| | | } from "./service"; |
| | | import { planTypeMap } from "@/utils/riskSourceMap"; |
| | | |
| | |
| | | }, |
| | | |
| | | // 打开报告 |
| | | openReport(type, row) { |
| | | async openReport(type, row) { |
| | | // 根据类型检查是否有对应报告 |
| | | const hasReport = type === "safe" |
| | | ? row.containSystemRes === 1 |
| | |
| | | } |
| | | |
| | | this.currentRow = row; |
| | | this.loading = true; |
| | | |
| | | // 获取报告URL |
| | | this.reportUrl = type === "safe" |
| | | ? row.systemReportUrl || "" |
| | | : row.deviceReportUrl || ""; |
| | | try { |
| | | // 从 localStorage 获取模板 ID |
| | | const deviceTemplateId = localStorage.getItem("deviceTemplateId"); |
| | | const safetyId = localStorage.getItem("safetyId"); |
| | | |
| | | if (this.reportUrl) { |
| | | // 检查任务 ID |
| | | const checkTaskId = row.id; |
| | | if (!checkTaskId) { |
| | | this.$message.error("缺少检查任务ID"); |
| | | this.loading = false; |
| | | return; |
| | | } |
| | | |
| | | // 使用当前时间戳作为 uuid |
| | | const uuid = Date.now().toString(); |
| | | |
| | | let response; |
| | | |
| | | if (type === "safe") { |
| | | // 安全报告(模糊评价) |
| | | if (!safetyId) { |
| | | this.$message.error("缺少安全报告模板ID"); |
| | | this.loading = false; |
| | | return; |
| | | } |
| | | response = await getFuzzyReportUrl({ |
| | | checkTaskId: checkTaskId, |
| | | templateId: safetyId, |
| | | uuid: uuid |
| | | }); |
| | | } else { |
| | | // 设备报告(打分评价) |
| | | if (!deviceTemplateId) { |
| | | this.$message.error("缺少设备报告模板ID"); |
| | | this.loading = false; |
| | | return; |
| | | } |
| | | response = await getScoreReportUrl({ |
| | | checkTaskId: checkTaskId, |
| | | templateId: deviceTemplateId, |
| | | uuid: uuid |
| | | }); |
| | | } |
| | | |
| | | // 检查接口是否调用成功 |
| | | if (!response) { |
| | | this.$message.error("生成报告失败"); |
| | | this.loading = false; |
| | | return; |
| | | } |
| | | |
| | | // 构建报告访问链接 |
| | | const reportUrl = `http://221.182.45.100:11225/images/reports/${uuid}.docx`; |
| | | |
| | | // 设置报告 URL 并打开弹窗 |
| | | this.reportUrl = reportUrl; |
| | | this.reportVisible = true; |
| | | } else { |
| | | this.$message.warning("报告地址不存在"); |
| | | |
| | | this.$message.success("报告生成成功"); |
| | | |
| | | } catch (error) { |
| | | console.error("获取报告失败:", error); |
| | | if (error.response) { |
| | | const status = error.response.status; |
| | | if (status === 404) { |
| | | this.$message.error("报告不存在"); |
| | | } else if (status === 500) { |
| | | this.$message.error("服务器错误,请稍后重试"); |
| | | } else { |
| | | this.$message.error(`获取报告失败: ${error.response.statusText || '未知错误'}`); |
| | | } |
| | | } else { |
| | | this.$message.error("获取报告失败,请检查网络连接"); |
| | | } |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | }, |
| | | |
| | | // 下载报告 |
| | | downloadReport() { |
| | | if (this.reportUrl) { |
| | | window.open(this.reportUrl, '_blank'); |
| | | } |
| | | }, |
| | | |
| | |
| | | window.open(url, "_blank"); |
| | | }, |
| | | }, |
| | | beforeDestroy() { |
| | | // 组件销毁时的清理工作 |
| | | }, |
| | | }; |
| | | </script> |
| | | <style lang="less"> |
| | |
| | | import request from '@/utils/request' |
| | | import axios from 'axios' |
| | | |
| | | // 创建专门用于文件流下载的 axios 实例 |
| | | const reportRequest = axios.create({ |
| | | baseURL: 'http://221.182.45.100:8180', |
| | | withCredentials: false, |
| | | timeout: 60000, // 文件下载可能需要更长时间 |
| | | }) |
| | | |
| | | // 请求拦截 - 添加 token |
| | | reportRequest.interceptors.request.use( |
| | | config => { |
| | | const token = localStorage.getItem('token') |
| | | if (token) { |
| | | config.headers.Authorization = token |
| | | } |
| | | return config |
| | | }, |
| | | error => { |
| | | return Promise.reject(error) |
| | | } |
| | | ) |
| | | |
| | | // 响应拦截 - 文件流直接返回 blob |
| | | reportRequest.interceptors.response.use( |
| | | response => { |
| | | // 对于 blob 类型的响应,直接返回 data |
| | | if (response.config.responseType === 'blob') { |
| | | return response.data |
| | | } |
| | | return response.data |
| | | }, |
| | | error => { |
| | | console.error('文件下载失败:', error) |
| | | return Promise.reject(error) |
| | | } |
| | | ) |
| | | |
| | | // 获取计划下拉框信息 |
| | | export const getPlanList = async (id) => { |
| | |
| | | method: 'get', params |
| | | }) |
| | | } |
| | | |
| | | // 获取安全报告链接(模糊评价)- 返回可访问的 URL |
| | | export const getFuzzyReportUrl = async (params) => { |
| | | return reportRequest({ |
| | | url: '/admin/stat/report/fuzzyReport', |
| | | method: 'get', |
| | | params, |
| | | // 不设置 responseType,接收 JSON 响应(包含 URL) |
| | | }) |
| | | } |
| | | |
| | | // 获取设备报告链接(打分评价)- 返回可访问的 URL |
| | | export const getScoreReportUrl = async (params) => { |
| | | return reportRequest({ |
| | | url: '/admin/stat/report/scoreReport', |
| | | method: 'get', |
| | | params, |
| | | // 不设置 responseType,接收 JSON 响应(包含 URL) |
| | | }) |
| | | } |
| | | |
| | | // 获取安全报告文件流(模糊评价)- 用于下载 |
| | | export const getFuzzyReportFile = async (params) => { |
| | | return reportRequest({ |
| | | url: '/admin/stat/report/fuzzyReport', |
| | | method: 'get', |
| | | params, |
| | | responseType: 'blob' // 关键:指定响应类型为 blob |
| | | }) |
| | | } |
| | | |
| | | // 获取设备报告文件流(打分评价)- 用于下载 |
| | | export const getScoreReportFile = async (params) => { |
| | | return reportRequest({ |
| | | url: '/admin/stat/report/scoreReport', |
| | | method: 'get', |
| | | params, |
| | | responseType: 'blob' // 关键:指定响应类型为 blob |
| | | }) |
| | | } |
| New file |
| | |
| | | # 报告功能优化说明 |
| | | |
| | | ## 📋 优化概述 |
| | | |
| | | 本次优化主要针对检查计划列表页面的"查看报告"功能,实现了通过文件流接口获取 Word 格式报告并自动下载的功能。 |
| | | |
| | | ## 🔧 技术实现 |
| | | |
| | | ### 1. 新增接口(service.js) |
| | | |
| | | ```javascript |
| | | // 获取安全报告文件流(模糊评价) |
| | | export const getFuzzyReportFile = async (params) => { |
| | | return request(`/admin/stat/report/fuzzyReport`, { |
| | | method: 'get', |
| | | params, |
| | | responseType: 'blob' // 关键:指定响应类型为 blob |
| | | }) |
| | | } |
| | | |
| | | // 获取设备报告文件流(打分评价) |
| | | export const getScoreReportFile = async (params) => { |
| | | return request(`/admin/stat/report/scoreReport`, { |
| | | method: 'get', |
| | | params, |
| | | responseType: 'blob' // 关键:指定响应类型为 blob |
| | | }) |
| | | } |
| | | ``` |
| | | |
| | | ### 2. 接口参数说明 |
| | | |
| | | **安全报告接口:** `/admin/stat/report/fuzzyReport` |
| | | - `checkTaskId`: 检查任务ID(列表中的 row.id) |
| | | - `templateId`: 安全报告模板ID(从 localStorage 获取 `safetyId`) |
| | | |
| | | **设备报告接口:** `/admin/stat/report/scoreReport` |
| | | - `checkTaskId`: 检查任务ID(列表中的 row.id) |
| | | - `templateId`: 设备报告模板ID(从 localStorage 获取 `deviceTemplateId`) |
| | | |
| | | ### 3. 核心功能实现 |
| | | |
| | | #### 3.1 报告下载流程 |
| | | |
| | | ```javascript |
| | | async openReport(type, row) { |
| | | // 1. 检查是否有报告 |
| | | const hasReport = type === "safe" |
| | | ? row.containSystemRes === 1 |
| | | : row.containDeviceRes === 1; |
| | | |
| | | // 2. 获取模板ID(从 localStorage) |
| | | const deviceTemplateId = localStorage.getItem("deviceTemplateId"); |
| | | const safetyId = localStorage.getItem("safetyId"); |
| | | |
| | | // 3. 调用对应的接口获取文件流 |
| | | let blob = await getFuzzyReportFile({ checkTaskId, templateId }); |
| | | |
| | | // 4. 创建 Blob URL 并触发下载 |
| | | const blobUrl = window.URL.createObjectURL(blob); |
| | | downloadReportFile(blob, fileName); |
| | | |
| | | // 5. 释放内存 |
| | | window.URL.revokeObjectURL(blobUrl); |
| | | } |
| | | ``` |
| | | |
| | | #### 3.2 内存管理 |
| | | |
| | | - 使用 `currentBlobUrl` 变量存储当前的 Blob URL |
| | | - 每次创建新的 Blob URL 前,释放旧的 URL |
| | | - 组件销毁时(`beforeDestroy`),自动释放所有 Blob URL |
| | | - 下载完成后延迟释放 URL,确保下载成功 |
| | | |
| | | ### 4. 用户体验优化 |
| | | |
| | | #### 4.1 视觉反馈 |
| | | - 报告按钮显示"(暂无)"标签,清晰标识不可用状态 |
| | | - 禁用状态的按钮透明度降低到 0.4 |
| | | - 可用按钮添加悬停效果(上移 + 透明度变化) |
| | | |
| | | #### 4.2 交互优化 |
| | | - 如果两个报告都没有,"查看报告"按钮直接禁用 |
| | | - 点击时显示友好的提示信息 |
| | | - 添加 loading 状态,防止重复点击 |
| | | - 下载成功后显示成功提示 |
| | | |
| | | #### 4.3 错误处理 |
| | | - 缺少模板ID时的提示 |
| | | - 网络错误的友好提示 |
| | | - 404、500 等不同错误状态的区分处理 |
| | | - 空文件的检测和提示 |
| | | |
| | | ## 📊 数据流向 |
| | | |
| | | ``` |
| | | 用户点击"查看报告" |
| | | ↓ |
| | | 检查 containSystemRes/containDeviceRes 字段 |
| | | ↓ |
| | | 从 localStorage 获取 templateId |
| | | ↓ |
| | | 调用文件流接口(传入 checkTaskId + templateId) |
| | | ↓ |
| | | 接收 Blob 数据 |
| | | ↓ |
| | | 创建 Blob URL |
| | | ↓ |
| | | 触发浏览器下载 |
| | | ↓ |
| | | 释放内存 |
| | | ``` |
| | | |
| | | ## 🎯 关键字段说明 |
| | | |
| | | ### 列表返回字段 |
| | | - `containDeviceRes`: 设备报告标识(1有 0无) |
| | | - `containSystemRes`: 安全报告标识(1有 0无) |
| | | - `id`: 检查任务ID(用作 checkTaskId) |
| | | - `planName`: 计划名称(用于生成文件名) |
| | | |
| | | ### localStorage 存储字段 |
| | | - `deviceTemplateId`: 设备报告模板ID |
| | | - `safetyId`: 安全报告模板ID |
| | | - 这两个字段在 `projectHome/index.vue` 的 `getLoginInfo` 方法中存储 |
| | | |
| | | ## ⚠️ 注意事项 |
| | | |
| | | ### 1. Word 文件预览限制 |
| | | - Word 文件无法直接在浏览器 iframe 中预览 |
| | | - 当前实现为直接下载方式 |
| | | - 如需在线预览,可考虑: |
| | | - 后端转换为 PDF 格式 |
| | | - 使用第三方预览服务(如 Office Online、Google Docs Viewer) |
| | | - 使用前端 Word 预览库(如 docx-preview) |
| | | |
| | | ### 2. 浏览器兼容性 |
| | | - Blob URL 支持所有现代浏览器 |
| | | - IE 11 需要特殊处理 |
| | | - 建议在 Chrome、Edge、Firefox 等现代浏览器中使用 |
| | | |
| | | ### 3. 文件大小考虑 |
| | | - 大文件可能导致下载缓慢 |
| | | - 建议后端对文件大小进行限制 |
| | | - 可以添加下载进度提示 |
| | | |
| | | ### 4. 安全性 |
| | | - 确保接口有适当的权限验证 |
| | | - 防止未授权访问报告文件 |
| | | - 注意 CORS 配置 |
| | | |
| | | ## 🔄 后续优化建议 |
| | | |
| | | ### 1. 添加在线预览功能 |
| | | ```javascript |
| | | // 如果后端能返回 PDF 格式 |
| | | if (fileType === 'pdf') { |
| | | this.reportUrl = blobUrl; |
| | | this.reportVisible = true; // 在弹窗中预览 |
| | | } else { |
| | | this.downloadReportFile(blob, fileName); // Word 直接下载 |
| | | } |
| | | ``` |
| | | |
| | | ### 2. 添加下载进度 |
| | | ```javascript |
| | | // 使用 axios 的 onDownloadProgress |
| | | const blob = await getReportFile({ |
| | | checkTaskId, |
| | | templateId, |
| | | onDownloadProgress: (progressEvent) => { |
| | | const percentCompleted = Math.round( |
| | | (progressEvent.loaded * 100) / progressEvent.total |
| | | ); |
| | | this.downloadProgress = percentCompleted; |
| | | } |
| | | }); |
| | | ``` |
| | | |
| | | ### 3. 添加报告缓存 |
| | | ```javascript |
| | | // 使用 IndexedDB 或 localStorage 缓存已下载的报告 |
| | | const cacheKey = `report_${type}_${checkTaskId}`; |
| | | const cachedReport = await getFromCache(cacheKey); |
| | | if (cachedReport) { |
| | | return cachedReport; |
| | | } |
| | | ``` |
| | | |
| | | ### 4. 批量下载功能 |
| | | ```javascript |
| | | // 支持选中多个报告批量下载 |
| | | async batchDownloadReports(selectedRows) { |
| | | for (const row of selectedRows) { |
| | | await this.openReport('safe', row); |
| | | await this.openReport('device', row); |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | ## 📝 测试清单 |
| | | |
| | | - [ ] 有安全报告时,点击能正常下载 |
| | | - [ ] 有设备报告时,点击能正常下载 |
| | | - [ ] 无报告时,按钮显示禁用状态 |
| | | - [ ] 缺少 templateId 时,显示错误提示 |
| | | - [ ] 网络错误时,显示友好提示 |
| | | - [ ] 文件名包含计划名称 |
| | | - [ ] 下载完成后内存正常释放 |
| | | - [ ] 组件销毁时无内存泄漏 |
| | | - [ ] 快速连续点击不会重复下载 |
| | | - [ ] 不同浏览器兼容性测试 |
| | | |
| | | ## 🎉 优化效果 |
| | | |
| | | 1. **用户体验提升**:从点击到下载一气呵成,无需等待页面跳转 |
| | | 2. **视觉反馈清晰**:一眼就能看出哪些报告可用 |
| | | 3. **错误处理完善**:各种异常情况都有友好提示 |
| | | 4. **内存管理规范**:避免内存泄漏问题 |
| | | 5. **代码可维护性**:逻辑清晰,易于扩展 |
| | | |
| | | ## 📞 技术支持 |
| | | |
| | | 如有问题,请检查: |
| | | 1. 后端接口是否正常返回文件流 |
| | | 2. localStorage 中是否有 templateId |
| | | 3. 浏览器控制台是否有错误信息 |
| | | 4. 网络请求是否成功(查看 Network 面板) |
| | | |