董国庆
2026-01-26 d30576bf8a9b6ef63c323cb5343518e07ba6ac44
提交代码
6个文件已修改
427 ■■■■■ 已修改文件
screen/package.json 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/router/index.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/header/index.vue 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/planOfAudit/index.vue 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/planOfAudit/service.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/projectHome/index.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/package.json
@@ -18,6 +18,7 @@
    "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",
@@ -35,6 +36,8 @@
    "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",
screen/src/router/index.js
@@ -127,4 +127,32 @@
  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
screen/src/view/header/index.vue
@@ -19,8 +19,34 @@
      <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>
@@ -38,6 +64,7 @@
      clientName: localStorage.getItem("clientName"),
      clientLogo: localStorage.getItem("clientLogo"),
      VUE_APP_IMAGE_BASE_URL: process.env.VUE_APP_IMAGE_BASE_URL,
      showLogoutDialog: false,
    };
  },
  mounted() {
@@ -80,6 +107,18 @@
        day: "numeric",
      });
    },
    handleLogout() {
      // 清除所有登录信息
      localStorage.clear();
      // 关闭弹窗
      this.showLogoutDialog = false;
      // 跳转到登录页
      this.$router.replace({
        path: "/",
      });
    },
  },
};
</script>
@@ -113,4 +152,181 @@
  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>
screen/src/view/planOfAudit/index.vue
@@ -257,10 +257,23 @@
      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>
    <!-- 检查资料详情弹窗 -->
@@ -370,6 +383,7 @@
<script>
import HeaderBox from "../header/index.vue";
import moment from "moment";
import * as docx from "docx-preview";
import {
  getPlanList,
  getDeviceDetailCheckList,
@@ -422,6 +436,7 @@
      // 报告相关
      reportUrl: "",
      loading: false,
      showPreview: false, // 是否显示预览
      // 枚举映射
      planTypeMap: planTypeMap, // 计划类型映射
@@ -646,6 +661,8 @@
      }
      this.currentRow = row;
      this.reportVisible = true;
      this.showPreview = false;
      this.loading = true;
      try {
@@ -664,7 +681,7 @@
        // 使用当前时间戳作为 uuid
        const uuid = Date.now().toString();
        let response;
        let blob;
        if (type === "safe") {
          // 安全报告(模糊评价)
@@ -673,7 +690,8 @@
            this.loading = false;
            return;
          }
          response = await getFuzzyReportUrl({
          // 直接获取文件流
          blob = await getFuzzyReportUrl({
            checkTaskId: checkTaskId,
            templateId: safetyId,
            uuid: uuid
@@ -685,28 +703,33 @@
            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);
@@ -722,8 +745,50 @@
        } 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;
      }
    },
@@ -753,6 +818,11 @@
    closeReportDialog() {
      this.reportVisible = false;
      this.reportUrl = "";
      this.showPreview = false;
      // 清空预览容器,释放内存
      if (this.$refs.docxContainer) {
        this.$refs.docxContainer.innerHTML = '';
      }
    },
    // 日期选择器点击
@@ -971,6 +1041,33 @@
  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;
screen/src/view/planOfAudit/service.js
@@ -62,28 +62,8 @@
  })
}
// 获取安全报告链接(模糊评价)- 返回可访问的 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',
@@ -92,8 +72,8 @@
  })
}
// 获取设备报告文件流(打分评价)- 用于下载
export const getScoreReportFile = async (params) => {
// 获取设备报告文件流(打分评价)- 直接返回 Blob
export const getScoreReportUrl = async (params) => {
  return reportRequest({
    url: '/admin/stat/report/scoreReport',
    method: 'get',
screen/src/view/projectHome/index.vue
@@ -151,8 +151,8 @@
                    </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>
@@ -1831,25 +1831,18 @@
        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);
          }
        }
      }
    }