| | |
| | | "axios": "^1.5.0", |
| | | "core-js": "^2.6.5", |
| | | "crypto-js": "^4.2.0", |
| | | "docx-preview": "^0.1.18", |
| | | "echarts": "^5.5.0", |
| | | "echarts-gl": "^2.0.9", |
| | | "element-ui": "^2.15.14", |
| | |
| | | "xgplayer-hls.js": "^3.0.14" |
| | | }, |
| | | "devDependencies": { |
| | | "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", |
| | | "@babel/plugin-transform-optional-chaining": "^7.28.6", |
| | | "@types/echarts": "^4.9.22", |
| | | "@vue/cli-plugin-babel": "^3.11.0", |
| | | "@vue/cli-plugin-eslint": "^3.11.0", |
| | |
| | | router.matcher = newRouter.matcher // reset router |
| | | } |
| | | |
| | | // 路由守卫:防止未登录用户访问受保护的页面 |
| | | router.beforeEach((to, from, next) => { |
| | | const token = localStorage.getItem('token') |
| | | |
| | | // 如果访问的是登录页 |
| | | if (to.path === '/') { |
| | | // 如果已登录,重定向到首页 |
| | | if (token) { |
| | | const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}') |
| | | if (userInfo.type == 2) { |
| | | next('/projectHome') |
| | | } else { |
| | | next('/home') |
| | | } |
| | | } else { |
| | | next() |
| | | } |
| | | } else { |
| | | // 访问其他页面,需要验证登录状态 |
| | | if (token) { |
| | | next() |
| | | } else { |
| | | // 未登录,重定向到登录页 |
| | | next('/') |
| | | } |
| | | } |
| | | }) |
| | | |
| | | export default router |
| | |
| | | <div class="txt-left fs--24 SHSBold"> |
| | | <div>{{ currentDate }}</div> |
| | | </div> |
| | | <img src="../../assets/img/home/iconx.png" class="w--30 h--30 ml--28" /> |
| | | <img |
| | | src="../../assets/img/home/iconx.png" |
| | | class="w--30 h--30 ml--28 cursor-pointer logout-icon" |
| | | @click="showLogoutDialog = true" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- 退出登录确认弹窗 --> |
| | | <transition name="fade"> |
| | | <div v-if="showLogoutDialog" class="logout-mask" @click.self="showLogoutDialog = false"> |
| | | <div class="logout-dialog"> |
| | | <div class="dialog-title">退出登录</div> |
| | | <div class="dialog-content"> |
| | | <div class="dialog-icon"> |
| | | <svg width="60" height="60" viewBox="0 0 60 60" fill="none"> |
| | | <circle cx="30" cy="30" r="28" stroke="#09B9FF" stroke-width="2" opacity="0.3"/> |
| | | <path d="M30 20V32M30 38V40" stroke="#09B9FF" stroke-width="3" stroke-linecap="round"/> |
| | | </svg> |
| | | </div> |
| | | <div class="dialog-text">确定要退出登录吗?</div> |
| | | </div> |
| | | <div class="dialog-footer"> |
| | | <div class="btn-cancel" @click="showLogoutDialog = false">取消</div> |
| | | <div class="btn-confirm" @click="handleLogout">确定</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </transition> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | clientName: localStorage.getItem("clientName"), |
| | | clientLogo: localStorage.getItem("clientLogo"), |
| | | VUE_APP_IMAGE_BASE_URL: process.env.VUE_APP_IMAGE_BASE_URL, |
| | | showLogoutDialog: false, |
| | | }; |
| | | }, |
| | | mounted() { |
| | |
| | | day: "numeric", |
| | | }); |
| | | }, |
| | | handleLogout() { |
| | | // 清除所有登录信息 |
| | | localStorage.clear(); |
| | | |
| | | // 关闭弹窗 |
| | | this.showLogoutDialog = false; |
| | | |
| | | // 跳转到登录页 |
| | | this.$router.replace({ |
| | | path: "/", |
| | | }); |
| | | }, |
| | | }, |
| | | }; |
| | | </script> |
| | |
| | | top: 31px; |
| | | left: 20px; |
| | | } |
| | | |
| | | .logout-icon { |
| | | transition: all 0.3s ease; |
| | | |
| | | &:hover { |
| | | transform: scale(1.1); |
| | | filter: brightness(1.2); |
| | | } |
| | | |
| | | &:active { |
| | | transform: scale(0.95); |
| | | } |
| | | } |
| | | |
| | | // 退出登录弹窗样式 |
| | | .logout-mask { |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.6); |
| | | backdrop-filter: blur(4px); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | z-index: 9999; |
| | | } |
| | | |
| | | .logout-dialog { |
| | | width: 480px; |
| | | background: linear-gradient(180deg, #0a1e3f 0%, #051429 100%); |
| | | border: 2px solid rgba(9, 185, 255, 0.3); |
| | | border-radius: 12px; |
| | | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), |
| | | 0 0 60px rgba(9, 185, 255, 0.15); |
| | | overflow: hidden; |
| | | animation: dialogSlideIn 0.3s ease-out; |
| | | } |
| | | |
| | | @keyframes dialogSlideIn { |
| | | from { |
| | | opacity: 0; |
| | | transform: translateY(-30px) scale(0.95); |
| | | } |
| | | to { |
| | | opacity: 1; |
| | | transform: translateY(0) scale(1); |
| | | } |
| | | } |
| | | |
| | | .dialog-title { |
| | | padding: 24px 30px; |
| | | font-family: "Source Han Sans CN Bold", sans-serif; |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #ffffff; |
| | | background: linear-gradient(90deg, rgba(9, 185, 255, 0.15) 0%, rgba(77, 104, 255, 0.05) 100%); |
| | | border-bottom: 1px solid rgba(9, 185, 255, 0.2); |
| | | letter-spacing: 1px; |
| | | } |
| | | |
| | | .dialog-content { |
| | | padding: 50px 30px 40px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | } |
| | | |
| | | .dialog-icon { |
| | | margin-bottom: 24px; |
| | | animation: iconPulse 2s ease-in-out infinite; |
| | | } |
| | | |
| | | @keyframes iconPulse { |
| | | 0%, 100% { |
| | | transform: scale(1); |
| | | opacity: 1; |
| | | } |
| | | 50% { |
| | | transform: scale(1.05); |
| | | opacity: 0.9; |
| | | } |
| | | } |
| | | |
| | | .dialog-text { |
| | | font-size: 18px; |
| | | color: rgba(255, 255, 255, 0.9); |
| | | line-height: 28px; |
| | | text-align: center; |
| | | font-family: "Source Han Sans CN", sans-serif; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | padding: 0 30px 30px; |
| | | display: flex; |
| | | gap: 20px; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .btn-cancel, |
| | | .btn-confirm { |
| | | flex: 1; |
| | | height: 48px; |
| | | line-height: 48px; |
| | | text-align: center; |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | border-radius: 8px; |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | font-family: "Source Han Sans CN Bold", sans-serif; |
| | | letter-spacing: 2px; |
| | | } |
| | | |
| | | .btn-cancel { |
| | | background: rgba(131, 143, 187, 0.3); |
| | | color: rgba(255, 255, 255, 0.8); |
| | | border: 1px solid rgba(131, 143, 187, 0.5); |
| | | |
| | | &:hover { |
| | | background: rgba(131, 143, 187, 0.5); |
| | | color: #ffffff; |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 4px 12px rgba(131, 143, 187, 0.3); |
| | | } |
| | | |
| | | &:active { |
| | | transform: translateY(0); |
| | | } |
| | | } |
| | | |
| | | .btn-confirm { |
| | | background: linear-gradient(270deg, #09b9ff 0%, #4d68ff 100%); |
| | | color: #ffffff; |
| | | border: 1px solid rgba(9, 185, 255, 0.3); |
| | | |
| | | &:hover { |
| | | background: linear-gradient(270deg, #0ad0ff 0%, #5d79ff 100%); |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 4px 16px rgba(9, 185, 255, 0.4); |
| | | } |
| | | |
| | | &:active { |
| | | transform: translateY(0); |
| | | } |
| | | } |
| | | |
| | | // 淡入淡出动画 |
| | | .fade-enter-active, |
| | | .fade-leave-active { |
| | | transition: opacity 0.3s ease; |
| | | } |
| | | |
| | | .fade-enter, |
| | | .fade-leave-to { |
| | | opacity: 0; |
| | | } |
| | | |
| | | .fade-enter-active .logout-dialog { |
| | | animation: dialogSlideIn 0.3s ease-out; |
| | | } |
| | | |
| | | .fade-leave-active .logout-dialog { |
| | | animation: dialogSlideOut 0.3s ease-in; |
| | | } |
| | | |
| | | @keyframes dialogSlideOut { |
| | | from { |
| | | opacity: 1; |
| | | transform: translateY(0) scale(1); |
| | | } |
| | | to { |
| | | opacity: 0; |
| | | transform: translateY(-30px) scale(0.95); |
| | | } |
| | | } |
| | | </style> |
| | |
| | | width="1420px" |
| | | @close="closeReportDialog" |
| | | > |
| | | <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 style="height: 691px;" class="flex flex-column" v-loading="loading"> |
| | | <!-- Word文档预览容器 --> |
| | | <div |
| | | v-if="showPreview" |
| | | id="docx-preview-container" |
| | | ref="docxContainer" |
| | | style="width: 100%; height: 100%; overflow: auto; background: #fff; padding: 20px;" |
| | | ></div> |
| | | <!-- 加载提示 --> |
| | | <div v-else class="flex flex-column a-center j-center h100"> |
| | | <div class="c-fff fs--18 mb--20">正在加载报告...</div> |
| | | </div> |
| | | <!-- 底部操作按钮 --> |
| | | <div class="flex j-center gap--20 mt--20"> |
| | | <el-button type="primary" @click="downloadReport" :disabled="!reportUrl">下载报告</el-button> |
| | | <el-button @click="closeReportDialog">关闭</el-button> |
| | | </div> |
| | | </div> |
| | | </CustomDialog> |
| | | <!-- 检查资料详情弹窗 --> |
| | |
| | | <script> |
| | | import HeaderBox from "../header/index.vue"; |
| | | import moment from "moment"; |
| | | import * as docx from "docx-preview"; |
| | | import { |
| | | getPlanList, |
| | | getDeviceDetailCheckList, |
| | |
| | | // 报告相关 |
| | | reportUrl: "", |
| | | loading: false, |
| | | showPreview: false, // 是否显示预览 |
| | | |
| | | // 枚举映射 |
| | | planTypeMap: planTypeMap, // 计划类型映射 |
| | |
| | | } |
| | | |
| | | this.currentRow = row; |
| | | this.reportVisible = true; |
| | | this.showPreview = false; |
| | | this.loading = true; |
| | | |
| | | try { |
| | |
| | | // 使用当前时间戳作为 uuid |
| | | const uuid = Date.now().toString(); |
| | | |
| | | let response; |
| | | let blob; |
| | | |
| | | if (type === "safe") { |
| | | // 安全报告(模糊评价) |
| | |
| | | this.loading = false; |
| | | return; |
| | | } |
| | | response = await getFuzzyReportUrl({ |
| | | // 直接获取文件流 |
| | | blob = await getFuzzyReportUrl({ |
| | | checkTaskId: checkTaskId, |
| | | templateId: safetyId, |
| | | uuid: uuid |
| | |
| | | this.loading = false; |
| | | return; |
| | | } |
| | | response = await getScoreReportUrl({ |
| | | // 直接获取文件流 |
| | | blob = await getScoreReportUrl({ |
| | | checkTaskId: checkTaskId, |
| | | templateId: deviceTemplateId, |
| | | uuid: uuid |
| | | }); |
| | | } |
| | | |
| | | // 检查接口是否调用成功 |
| | | if (!response) { |
| | | this.$message.error("生成报告失败"); |
| | | // 检查 blob 是否有效 |
| | | console.log('接口返回的数据类型:', blob); |
| | | console.log('是否为Blob:', blob instanceof Blob); |
| | | console.log('Blob大小:', blob.size); |
| | | |
| | | if (!blob || blob.size === 0) { |
| | | 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; |
| | | |
| | | this.$message.success("报告生成成功"); |
| | | // 直接使用文件流预览 |
| | | await this.previewWordDocument(blob); |
| | | |
| | | this.$message.success("报告加载成功"); |
| | | |
| | | } catch (error) { |
| | | console.error("获取报告失败:", error); |
| | |
| | | } else { |
| | | this.$message.error("获取报告失败,请检查网络连接"); |
| | | } |
| | | this.showPreview = false; |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | }, |
| | | |
| | | // 预览Word文档 |
| | | async previewWordDocument(blob) { |
| | | try { |
| | | // 等待 DOM 更新 |
| | | this.showPreview = true; |
| | | await this.$nextTick(); |
| | | |
| | | // 获取容器 |
| | | const container = this.$refs.docxContainer; |
| | | if (!container) { |
| | | this.$message.error("预览容器未找到"); |
| | | return; |
| | | } |
| | | |
| | | // 清空容器 |
| | | container.innerHTML = ''; |
| | | console.log('docx docx docx',docx) |
| | | |
| | | // 渲染文档 |
| | | await docx.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, |
| | | }); |
| | | |
| | | } catch (error) { |
| | | console.error("预览文档失败:", error); |
| | | this.$message.error("文档预览失败,请尝试下载查看"); |
| | | this.showPreview = false; |
| | | throw error; |
| | | } |
| | | }, |
| | | |
| | |
| | | closeReportDialog() { |
| | | this.reportVisible = false; |
| | | this.reportUrl = ""; |
| | | this.showPreview = false; |
| | | // 清空预览容器,释放内存 |
| | | if (this.$refs.docxContainer) { |
| | | this.$refs.docxContainer.innerHTML = ''; |
| | | } |
| | | }, |
| | | |
| | | // 日期选择器点击 |
| | |
| | | opacity: 0.7; |
| | | } |
| | | |
| | | #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; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | ::v-deep .el-descriptions { |
| | | .el-descriptions__body { |
| | | background-color: transparent; |
| | |
| | | }) |
| | | } |
| | | |
| | | // 获取安全报告链接(模糊评价)- 返回可访问的 URL |
| | | // 获取安全报告文件流(模糊评价)- 直接返回 Blob |
| | | 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', |
| | |
| | | }) |
| | | } |
| | | |
| | | // 获取设备报告文件流(打分评价)- 用于下载 |
| | | export const getScoreReportFile = async (params) => { |
| | | // 获取设备报告文件流(打分评价)- 直接返回 Blob |
| | | export const getScoreReportUrl = async (params) => { |
| | | return reportRequest({ |
| | | url: '/admin/stat/report/scoreReport', |
| | | method: 'get', |
| | |
| | | </div> |
| | | </div> |
| | | <div class="item-trend" :class="{ 'down': item.rate < 0 }"> |
| | | <i v-if="item.rate > 0" class="trend-arrow trend-arrow-up"></i> |
| | | <i v-else-if="item.rate < 0" class="trend-arrow trend-arrow-down"></i> |
| | | <i v-if="item.rate < 0" class="el-icon-bottom trend-icon"></i> |
| | | <i v-else-if="item.rate > 0" class="el-icon-top trend-icon"></i> |
| | | <span>{{ Math.abs(item.rate) }}%</span> |
| | | </div> |
| | | </div> |
| | |
| | | line-height: 14px; |
| | | font-style: normal; |
| | | |
| | | .trend-arrow { |
| | | width: 0; |
| | | height: 0; |
| | | border-left: 4px solid transparent; |
| | | border-right: 4px solid transparent; |
| | | .trend-icon { |
| | | margin-right: 4px; |
| | | display: inline-block; |
| | | } |
| | | |
| | | .trend-arrow-up { |
| | | border-bottom: 6px solid #66ffff; |
| | | } |
| | | |
| | | .trend-arrow-down { |
| | | border-top: 6px solid rgba(253, 66, 62, 1); |
| | | font-size: 14px; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | &.down { |
| | | color: rgba(253, 66, 62, 1); |
| | | |
| | | .trend-icon { |
| | | color: rgba(253, 66, 62, 1); |
| | | } |
| | | } |
| | | } |
| | | } |