董国庆
2026-06-11 0aa66dcedffba1073dc51b1a3ab558cd1ffbf197
screen/src/view/projectHome/index.vue
@@ -1,97 +1,205 @@
<template>
  <div class="relative">
    <HeaderBox :title="projectName" />
    <HeaderBox :title="projectName" :name="clientName" :logo="clientLogo" />
    <!-- 地图 -->
    <div id="container" style="width: 1920px; height: 995px; background-color: #001635"></div>
    <div
      id="projectContainer"
      style="
        position: fixed;
        top: 0;
        left: 0;
        z-index: 0;
        width: 100%;
        height: 100%;
        background-color: #001635;
      "
    ></div>
    <!-- 左边 内容 -->
    <div class="left-card">
      <div class="mt--16">
        <TitleBox>
          <template #title> 项目信息 </template>
          <template #right>
            <div @click="toProjectDetail()" class="card-more">
              <span>项目进度</span>
              <img src="../../assets/img/project/more.png" />
            </div>
          </template>
          <template #content>
            <div class="project-info">
              <div class="info-item">
              <div class="info-item11">
                <div class="flex2 flex">
                  <div class="label">项目编号</div>
                  <div class="value">{{ projectInfo.code || '-' }}</div>
                  <div class="label">项目规模</div>
                  <div class="value">
                    {{ `${projectInfo.scale}平方米` || "-" }}
                  </div>
                </div>
                <div class="flex1 flex">
                  <div class="label">工程类型</div>
                  <div class="value">{{ projectInfo.type || '-' }}</div>
                  <div class="value">
                    {{ ["房建工程", "市政工程", "固定资产"][projectInfo.type] }}
                  </div>
                </div>
              </div>
              <div class="info-item">
              <div class="info-item11">
                <div class="flex2 flex">
                  <div class="label">项目地址</div>
                  <div class="value">{{ projectInfo.address || '-' }}</div>
                  <div
                    class="value address-value"
                    :title="projectInfo.address || '-'"
                  >
                    {{ projectInfo.address || "-" }}
                  </div>
                </div>
                <div class="flex1 flex">
                <div class="flex1 flex a-center">
                  <div class="label">项目阶段</div>
                  <div class="value">{{ projectInfo.progress || '-' }}</div>
                  <div class="value">{{ projectInfo.progress || "-" }}</div>
                </div>
              </div>
              <div class="info-item">
              <div class="info-item11">
                <div class="flex2 flex">
                  <div class="label">施工单位</div>
                  <div class="value">{{ projectInfo.developmentCompanyInfo.name || '-' }}</div>
                  <div class="value">
                    {{ projectInfo.developmentCompanyInfo.name || "-" }}
                  </div>
                </div>
                <div class="flex1 flex">
                  <div class="label">项目经理</div>
                  <div class="value">{{ projectInfo.developmentCompanyInfo.leader || '-' }}</div>
                  <div class="value">
                    {{ projectInfo.developmentCompanyInfo.leader || "-" }}
                  </div>
                </div>
              </div>
              <div class="info-item">
              <div class="info-item11">
                <div class="flex2 flex">
                  <div class="label">监理单位</div>
                  <div class="value">{{ projectInfo.supervisorCompanyInfo.name || '-' }}</div>
                  <div class="value">
                    {{ projectInfo.supervisorCompanyInfo.name || "-" }}
                  </div>
                </div>
                <div class="flex1 flex">
                  <div class="label">总监</div>
                  <div class="value">{{ projectInfo.supervisorCompanyInfo.leader || '-' }}</div>
                  <div class="value">
                    {{ projectInfo.supervisorCompanyInfo.leader || "-" }}
                  </div>
                </div>
              </div>
              <div class="info-item">
              <div class="info-item11">
                <div class="flex2 flex">
                  <div class="label">建设单位</div>
                  <div class="value">{{ projectInfo.constructionCompanyInfo.name || '-' }}</div>
                  <div class="value">
                    {{ projectInfo.constructionCompanyInfo.name || "-" }}
                  </div>
                </div>
                <div class="flex1 flex">
                  <div class="label">项目负责人</div>
                  <div class="value">{{ projectInfo.constructionCompanyInfo.leader || '-' }}</div>
                  <div class="value">
                    {{ projectInfo.constructionCompanyInfo.leader || "-" }}
                  </div>
                </div>
              </div>
              <div class="info-item" v-for="item in projectInfo.properties">
              <div
                class="info-item11"
                v-if="
                  (filteredRentalUnits && filteredRentalUnits.length > 0) ||
                  (filteredContacts && filteredContacts.length > 0)
                "
              >
                <div class="flex2 flex">
                  <div class="label dept">设备租赁单位</div>
                  <div class="value">{{ JSON.parse(item)['工程起重机械租赁单位'] || '-' }}</div>
                  <div class="value" v-if="filteredRentalUnits.length >= 1">
                    {{ filteredRentalUnits[0]["工程起重机械租赁单位"] || "-" }}
                  </div>
                  <el-tooltip
                    style="margin-left: 10px"
                    v-if="filteredRentalUnits.length > 1"
                    placement="top"
                    effect="dark"
                  >
                    <div slot="content" class="tooltip-content">
                      <div
                        v-for="(item, index) in filteredRentalUnits"
                        :key="index"
                        class="tooltip-item"
                      >
                        <div class="tooltip-label">单位{{ index + 1 }}:</div>
                        <div class="tooltip-value">
                          {{ item["工程起重机械租赁单位"] || "-" }}
                        </div>
                      </div>
                    </div>
                    <span class="count-badge">{{
                      filteredRentalUnits.length
                    }}</span>
                  </el-tooltip>
                </div>
                <div class="flex1 flex">
                  <div class="label">联系人</div>
                  <div class="value">{{ JSON.parse(item)['联系电话'] || '-' }}</div>
                  <div class="value" v-if="filteredContacts.length >= 1">
                    {{ filteredContacts[0]["联系人"] || "-" }}
                  </div>
                  <el-tooltip
                    style="margin-left: 10px"
                    v-if="filteredContacts.length > 1"
                    placement="top"
                    effect="dark"
                  >
                    <div slot="content" class="tooltip-content">
                      <div
                        v-for="(item, index) in filteredContacts"
                        :key="index"
                        class="tooltip-item"
                      >
                        <div class="tooltip-label">联系人{{ index + 1 }}:</div>
                        <div class="tooltip-value">
                          {{ item["联系人"] || "-" }}
                        </div>
                      </div>
                    </div>
                    <span class="count-badge">{{
                      filteredContacts.length
                    }}</span>
                  </el-tooltip>
                </div>
              </div>
              <div class="info-item">
              <div class="info-item11">
                <div class="flex">
                  <div class="label time">最近一次检查时间</div>
                  <div class="value">{{ projectInfo.checkTime || '-' }}</div>
                  <div class="value">{{ projectInfo.checkTime || "-" }}</div>
                </div>
              </div>
            </div>
          </template>
        </TitleBox>
      </div>
      <div class="mt--30" @click="toEquipmentInfo" v-if="permissionProject.includes('3')">
      <div
        class="mt--30"
        @click="toEquipmentInfo"
        v-if="permissionProject.includes('11')"
      >
        <TitleBox>
          <template #title> 设备信息 </template>
          <template #content>
            <div class="equipment-info">
              <img src="../../assets//img/project/chartBg.png" class="chart-bg" alt="" />
              <img
                src="../../assets//img/project/chartBg.png"
                class="chart-bg"
                alt=""
              />
              <div class="pie-canvas" ref="pieChart"></div>
              <div class="legend">
                <div class="legend-item" v-for="(it, idx) in legendData" :key="idx">
                  <div class="dot" :style="{ backgroundColor: it.itemStyle.color }"></div>
                <div
                  class="legend-item"
                  v-for="(it, idx) in legendData"
                  :key="idx"
                >
                  <div
                    class="dot"
                    :style="{ backgroundColor: it.itemStyle.color }"
                  ></div>
                  <div class="legend-info">
                    <div class="legend-name" :title="it.name">{{ it.name }}
                    <div class="legend-name" :title="it.name">
                      {{ it.name }}
                    </div>
                    <div class="legend-number">{{ it.value }}</div>
                  </div>
@@ -105,7 +213,7 @@
          </template>
        </TitleBox>
      </div>
      <div class="mt--30" v-if="permissionProject.includes('1')">
      <div class="mt--30" v-if="permissionProject.includes('9')">
        <TitleBox>
          <template #title>各分项隐患趋势 </template>
          <template #right>
@@ -120,39 +228,81 @@
                <span>隐患类型</span>
                <div class="header-line" @click="changeDangerSort(1)">
                  <span>隐患数量</span>
                  <img v-if="dangerSortType === 1 && dangerSortDesc" src="../../assets/img/project/up.png" />
                  <img v-else-if="dangerSortType === 1 && !dangerSortDesc" src="../../assets/img/project/down.png" />
                  <img v-else src="../../assets/img/project/down.png" style="opacity: 0.3;" />
                  <img
                    v-if="dangerSortType === 1 && dangerSortDesc"
                    src="../../assets/img/project/up.png"
                  />
                  <img
                    v-else-if="dangerSortType === 1 && !dangerSortDesc"
                    src="../../assets/img/project/down.png"
                  />
                  <img
                    v-else
                    src="../../assets/img/project/down.png"
                    style="opacity: 0.3"
                  />
                </div>
                <div class="header-line" @click="changeDangerSort(2)">
                  <span>趋势</span>
                  <img v-if="dangerSortType === 2 && dangerSortDesc" src="../../assets/img/project/up.png" />
                  <img v-else-if="dangerSortType === 2 && !dangerSortDesc" src="../../assets/img/project/down.png" />
                  <img v-else src="../../assets/img/project/down.png" style="opacity: 0.3;" />
                  <img
                    v-if="dangerSortType === 2 && dangerSortDesc"
                    src="../../assets/img/project/up.png"
                  />
                  <img
                    v-else-if="dangerSortType === 2 && !dangerSortDesc"
                    src="../../assets/img/project/down.png"
                  />
                  <img
                    v-else
                    src="../../assets/img/project/down.png"
                    style="opacity: 0.3"
                  />
                </div>
              </div>
              <div class="info-body">
                <div class="info-item" v-for="(item, index) in dangerTrendList" :key="index">
                  <div class="item-type" :title="item.name || '-'">{{ item.name || '-' }}</div>
                <div
                  class="info-item"
                  v-for="(item, index) in dangerTrendList"
                  :key="index"
                >
                  <div class="item-type" :title="item.name || '-'">
                    {{ item.name || "-" }}
                  </div>
                  <div class="item-number">
                    <div class="flex a--center">
                      <div class="flex a--center">
                        <span>本月</span>
                        <span>本轮</span>
                        <div class="number">{{ item.thisMonth || 0 }}</div>
                      </div>
                      <div class="now" :style="{ width: getBarWidth(item.thisMonth, item.lastMonth) }"></div>
                      <div
                        class="now"
                        :style="{
                          width: getBarWidth(item.thisMonth, item.lastMonth),
                        }"
                      ></div>
                    </div>
                    <div class="flex a--center">
                      <div class="flex a--center">
                        <span>上月</span>
                        <span>上轮</span>
                        <div class="number">{{ item.lastMonth || 0 }}</div>
                      </div>
                      <div class="before" :style="{ width: getBarWidth(item.lastMonth, item.thisMonth) }"></div>
                      <div
                        class="before"
                        :style="{
                          width: getBarWidth(item.lastMonth, item.thisMonth),
                        }"
                      ></div>
                    </div>
                  </div>
                  <div class="item-trend" :class="{ 'down': item.rate < 0 }">
                    <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>
                  <div class="item-trend" :class="{ down: item.rate < 0 }">
                    <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>
@@ -166,18 +316,37 @@
    </div>
    <!-- 中间内容 -->
    <div class="mid-card">
      <div @click="toHiddenDanger(1)" class="mid-info" v-if="permissionProject.includes('2')">
      <div
        @click="toHiddenDanger(1)"
        class="mid-info"
        v-if="permissionProject.includes('10')"
      >
        <div class="flex a--center">
          <div @click.stop="toHiddenDanger(2)" class="mid-title">隐患数量</div>
          <div class="flex a--center tab-border">
            <div class="mid-tab">所有隐患数</div>
            <div class="mid-tab active-tab">重大隐患数</div>
            <div
              class="mid-tab"
              :class="{ 'active-tab': dangerCountTab === 'all' }"
              @click.stop="handleDangerTabChange('all')"
            >
              所有隐患数
            </div>
            <div
              class="mid-tab"
              :class="{ 'active-tab': dangerCountTab === 'serious' }"
              @click.stop="handleDangerTabChange('serious')"
            >
              重大隐患数
            </div>
          </div>
        </div>
        <div class="mid-content">
        <div
          class="mid-content"
          :class="{ 'mid-content-switching': dangerCountLoading }"
        >
          <div class="mid-img">
            <div class="number">{{ dangerCount.serious || 0 }}</div>
            <div class="name">所有隐患数</div>
            <div class="name">{{ dangerCountTabName }}</div>
          </div>
          <div class="mid-total">
            <div class="mid-item">
@@ -191,11 +360,17 @@
          </div>
        </div>
      </div>
      <div v-if="permissionProject.includes('4')" @click="toPlanOfAudit" class="mid-more">查看报告</div>
      <div
        v-if="permissionProject.includes('12')"
        @click="toPlanOfAudit"
        class="mid-more"
      >
        查看报告
      </div>
    </div>
    <!-- 右边 -->
    <div class="right-card">
      <div class="mt--16" v-if="permissionProject.includes('5')">
      <div class="mt--16" v-if="permissionProject.includes('13')">
        <TitleBox>
          <template #title>人员在岗率 </template>
          <template #right>
@@ -207,37 +382,36 @@
          <template #content>
            <div class="people-info">
              <div class="people-card">
                <div class="people-item" v-for="(item, index) in peopleList.slice(0, 3)" :key="index">
                <div
                  class="people-item"
                  v-for="(item, index) in peopleList"
                  :key="item.id || item.postName || item.name || index"
                >
                  <div class="people-chart">
                    <el-progress :width="74" type="circle" :show-text="false" :percentage="parseFloat(item.rate) || 0"
                      define-back-color="#000" color="#32C5FF" stroke-linecap="square"></el-progress>
                    <el-progress
                      :width="74"
                      type="circle"
                      :show-text="false"
                      :percentage="parseFloat(item.rate) || 0"
                      define-back-color="#000"
                      color="#32C5FF"
                      stroke-linecap="square"
                    ></el-progress>
                    <div class="progress-card">
                      <div class="progress-num">{{ item.rate }}</div>
                      <div class="progress-unit">%</div>
                    </div>
                  </div>
                  <div class="people-name">{{ item.postName || item.name }}</div>
                </div>
              </div>
              <div class="people-card2">
                <div class="people-item" :class="{ 'mr--67': index === 0 }"
                  v-for="(item, index) in peopleList.slice(3, 5)" :key="index + 3">
                  <div class="people-chart">
                    <el-progress :width="74" type="circle" :show-text="false" :percentage="parseFloat(item.rate) || 0"
                      define-back-color="#000" color="#32C5FF" stroke-linecap="square"></el-progress>
                    <div class="progress-card">
                      <div class="progress-num">{{ item.rate }}</div>
                      <div class="progress-unit">%</div>
                    </div>
                  <div class="people-name">
                    {{ item.postName || item.name }}
                  </div>
                  <div class="people-name">{{ item.name }}</div>
                </div>
              </div>
            </div>
          </template>
        </TitleBox>
      </div>
      <div class="mt--30" v-if="permissionProject.includes('6')">
      <div class="mt--30" v-if="permissionProject.includes('14')">
        <TitleBox>
          <template #title>重大风险源 </template>
          <template #right>
@@ -255,16 +429,27 @@
                <div class="label card4">实施情况</div>
              </div>
              <div class="risk-body">
                <div class="risk-item" v-for="(item, index) in riskSourceList" :key="index">
                <div
                  class="risk-item"
                  v-for="(item, index) in riskSourceList"
                  :key="index"
                >
                  <div class="label card1">{{ index + 1 }}</div>
                  <div class="label card2" :class="{
                    'merged-cell': item.showRiskScale && item.rowspan > 1,
                    'hidden-cell': !item.showRiskScale
                  }">
                    <div v-if="item.showRiskScale" class="merged-content" :style="{
                      height: `${item.rowspan * 36}px`,
                      lineHeight: `${item.rowspan * 36}px`
                    }">
                  <div
                    class="label card2"
                    :class="{
                      'merged-cell': item.showRiskScale && item.rowspan > 1,
                      'hidden-cell': !item.showRiskScale,
                    }"
                  >
                    <div
                      v-if="item.showRiskScale"
                      class="merged-content"
                      :style="{
                        height: `${item.rowspan * 36}px`,
                        lineHeight: `${item.rowspan * 36}px`,
                      }"
                    >
                      {{ item.riskScale }}
                    </div>
                  </div>
@@ -273,18 +458,28 @@
                </div>
              </div>
            </div>
          </template>
        </TitleBox>
      </div>
      <div class="mt--30" v-if="permissionProject.includes('7')">
      <div class="mt--30" v-if="permissionProject.includes('15')">
        <TitleBox>
          <template #title>项目风险趋势 </template>
          <template #right>
            <div class="risk-picker">
            <div @click="handleRiskPickerClick" class="risk-picker">
              <el-date-picker
                v-model="dateRange"
                ref="riskDatePicker"
                type="monthrange"
                range-separator=""
                start-placeholder=""
                end-placeholder=""
              >
              </el-date-picker>
              <div class="risk-ab">
                <img src="../../assets/img/home/Shape@2x (6).png" class="pick-img" />
                <img
                  src="../../assets/img/home/Shape@2x (6).png"
                  class="pick-img"
                />
                <div class="pick-text">
                  {{ dateTxt }}
                </div>
@@ -299,36 +494,148 @@
        </TitleBox>
      </div>
    </div>
    <!-- 项目进度详情弹窗 -->
    <CustomDialog
      title="项目进度"
      :visible.sync="progressVisible"
      width="1200px"
      @close="closeProgressDialog"
      class="progress-dialog"
    >
      <!-- 历史进度按钮 - 使用绝对定位放在关闭按钮旁边 -->
      <div class="history-btn-fixed" @click="toProgressHistory">
        <span>历史进度</span>
      </div>
      <div class="progress-dialog-content" v-loading="progressLoading">
        <div v-if="latestProgress && latestProgress.id">
          <!-- 进度说明 -->
          <div class="progress-description">
            {{ latestProgress.detail || "暂无进度说明" }}
          </div>
          <!-- 进度图片和视频 - 缩略图网格 -->
          <div class="progress-media-grid" v-if="progressMediaList.length > 0">
            <div
              v-for="(item, index) in progressMediaList"
              :key="index"
              class="media-thumbnail"
              :class="{ 'video-thumbnail': item.type === 'video' }"
              @click="previewProgressMedia(item)"
            >
              <!-- 图片缩略图 -->
              <img
                v-if="item.type === 'image'"
                :src="item.url"
                class="thumbnail-img"
                @error="handleImageError"
              />
              <!-- 视频缩略图 -->
              <video
                v-else-if="item.type === 'video'"
                :src="item.url"
                class="thumbnail-video"
                @error="handleVideoError"
              ></video>
              <!-- 悬浮遮罩 -->
              <div class="thumbnail-overlay">
                <i v-if="item.type === 'image'" class="el-icon-zoom-in"></i>
                <i
                  v-else-if="item.type === 'video'"
                  class="el-icon-video-play"
                ></i>
              </div>
              <!-- 视频角标 -->
              <div v-if="item.type === 'video'" class="video-badge">
                <i class="el-icon-video-camera"></i>
              </div>
            </div>
          </div>
          <!-- 无内容提示 -->
          <div v-else class="no-images">
            <div class="no-images-text">暂无进度内容</div>
          </div>
        </div>
        <div v-else class="no-data">
          <div class="no-data-icon">
            <i class="el-icon-document"></i>
          </div>
          <div class="no-data-text">暂无进度信息</div>
        </div>
      </div>
    </CustomDialog>
    <!-- 媒体预览弹窗 -->
    <el-dialog
      :visible.sync="previewVisible"
      width="80%"
      :show-close="true"
      @close="closePreview"
      class="media-preview-dialog"
      append-to-body
    >
      <div class="preview-content">
        <!-- 图片预览 -->
        <img
          v-if="previewType === 'image'"
          :src="previewUrl"
          class="preview-image"
          @error="handleImageError"
        />
        <!-- 视频预览 -->
        <video
          v-else-if="previewType === 'video'"
          :src="previewUrl"
          class="preview-video"
          controls
          autoplay
          @error="handleVideoError"
        ></video>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import HeaderBox from "../header/index.vue";
import HeaderBox from '../header/index.vue'
import moment from 'moment'
import {
  getLoginInfo,
  getProjectDetail,
  getProjectDeviceCount,
  getDangerCount,
  getDangerCountAll,
  deviceTypes,
  getProjectPersonInfo,
  getProjectSource,
  getDangerTrend,
  getProjectDangerousTrend
  getProjectDangerousTrend,
  getProjectLatestProgress
} from './service.js'
import {
  transformRiskSourceData
} from '@/utils/riskSourceMap.js'
import moment from "moment";
import * as echarts from "echarts";
import { transformRiskSourceData } from '@/utils/riskSourceMap.js'
import * as echarts from 'echarts'
export default {
  components: {
    HeaderBox,
    HeaderBox
  },
  data() {
  data () {
    return {
      mapView: null,
      mapPromise: null,
      dateRange: [moment().subtract(5, 'months').startOf("month"), moment().endOf("month")], //默认近6个月
      _AMap: null,
      _mapInitPromise: null,
      _mapResizeHandler: null,
      _lastMarkerPosKey: '',
      currentMarker: null, // 保存当前的 marker 实例
      dateRange: [
        moment().subtract(5, 'months').startOf('month'),
        moment().endOf('month')
      ], // 默认近6个月
      markers: [],
      projectId: '',
      projectName: '',
@@ -340,20 +647,27 @@
        developmentCompanyInfo: {},
        supervisorCompanyInfo: {},
        properties: []
      },//项目信息
      }, // 项目信息
      dangerCount: {
        serious: 0,
        rectified: 0,
        unRectified: 0
      },//危险源统计
      deviceTypeList: [],//设备类型列表
      equipment: 0,//设备数量
      peopleList: [],//人员列表
      }, // 危险源统计
      dangerCountTab: 'serious',
      dangerCountLoading: false,
      dangerCountCache: {
        all: null,
        serious: null
      },
      deviceTypeList: [], // 设备类型列表
      equipment: 0, // 设备数量
      peopleList: [], // 人员列表
      // pie数据
      legendData: [],
      dateTxt: '',
      dangerTrendList: [], //分项隐患趋势数据
      dangerTrendList: [], // 分项隐患趋势数据
      dangerSortType: 1, // 排序类型:1-隐患数量,2-趋势
      dangerSortDesc: true, // 是否倒序
      // 风险源数据
@@ -361,32 +675,51 @@
      // 项目风险趋势数据
      riskTrendData: {
        xAxisData: [],
        seriesData: []
        seriesData: [],
        statusData: []
      },
      // 9各分项隐患趋势 10隐患数量 11设备数量 12查看报告入口 13人员在岗率 14重大风险源 15 项目风险趋势
      permissionProject: JSON.parse(localStorage.getItem("permissionProject")) || [],
    };
      permissionProject:
        JSON.parse(sessionStorage.getItem('permissionProject')) || [],
      // 项目进度相关
      progressVisible: false, // 进度弹窗显示状态
      progressLoading: false, // 进度加载状态
      latestProgress: {}, // 最新进度数据
      VUE_APP_BASE_URL: process.env.VUE_APP_IMAGE_BASE_URL, // 基础URL
      progressMediaList: [], // 处理后的媒体列表(包含类型信息)
      // 预览相关
      previewVisible: false, // 预览弹窗显示状态
      previewUrl: '', // 预览的URL
      previewType: 'image', // 预览类型:'image' 或 'video'
      clientName: '', // 客户名称
      clientLogo: '' // 客户logo
    }
  },
  computed: {
    dangerCountTabName () {
      return this.dangerCountTab === 'all' ? '所有隐患数' : '重大隐患数'
    },
    // 处理风险源数据,添加合并单元格的逻辑
    riskSourceList() {
    riskSourceList () {
      if (!this.riskSourceData || this.riskSourceData.length === 0) {
        return [];
        return []
      }
      const result = [];
      let i = 0;
      const result = []
      let i = 0
      while (i < this.riskSourceData.length) {
        const currentRiskScale = this.riskSourceData[i].riskScale;
        let rowspan = 1;
        const currentRiskScale = this.riskSourceData[i].riskScale
        let rowspan = 1
        // 计算相同风险规模的行数
        for (let j = i + 1; j < this.riskSourceData.length; j++) {
          if (this.riskSourceData[j].riskScale === currentRiskScale) {
            rowspan++;
            rowspan++
          } else {
            break;
            break
          }
        }
@@ -395,7 +728,7 @@
          ...this.riskSourceData[i],
          showRiskScale: true,
          rowspan: rowspan
        });
        })
        // 添加后续相同风险规模的行,不显示风险规模
        for (let k = 1; k < rowspan; k++) {
@@ -403,208 +736,563 @@
            ...this.riskSourceData[i + k],
            showRiskScale: false,
            rowspan: 0
          });
          })
        }
        i += rowspan;
        i += rowspan
      }
      return result;
      return result
    },
    filteredProperties () {
      if (
        !this.projectInfo.properties ||
        this.projectInfo.properties.length === 0
      ) {
        return []
      }
      const uniqueProperties = []
      const seen = new Set()
      this.projectInfo.properties.forEach((itemStr) => {
        try {
          const item = JSON.parse(itemStr)
          const rentalUnit = item['工程起重机械租赁单位']
          const contact = item['联系电话']
          if (rentalUnit || contact) {
            const key = `${rentalUnit}-${contact}`
            if (!seen.has(key)) {
              seen.add(key)
              uniqueProperties.push(item)
            }
          }
        } catch (e) {
          console.error('Parsing project properties failed:', e)
        }
      })
      console.log('uniqueProperties uniqueProperties', uniqueProperties)
      return uniqueProperties
    },
    filteredRentalUnits () {
      if (
        !this.projectInfo.properties ||
        this.projectInfo.properties.length === 0
      ) {
        return []
      }
      const list = []
      const seen = new Set()
      this.projectInfo.properties.forEach((itemStr) => {
        try {
          const item = JSON.parse(itemStr)
          const rentalUnit = item['工程起重机械租赁单位']
          if (!rentalUnit) return
          const key = String(rentalUnit).trim()
          if (!key) return
          if (seen.has(key)) return
          seen.add(key)
          list.push({ 工程起重机械租赁单位: rentalUnit })
        } catch (e) {}
      })
      return list
    },
    filteredContacts () {
      if (
        !this.projectInfo.properties ||
        this.projectInfo.properties.length === 0
      ) {
        return []
      }
      const list = []
      const seen = new Set()
      this.projectInfo.properties.forEach((itemStr) => {
        try {
          const item = JSON.parse(itemStr)
          const name = item['联系人']
          if (!name) return
          const key = String(name).trim()
          if (!key) return
          if (seen.has(key)) return
          seen.add(key)
          list.push({ 联系人: name })
        } catch (e) {}
      })
      return list
    }
  },
  watch: {
    dateRange: {
      handler(newVal, oldVal) {
      handler (newVal, oldVal) {
        if (newVal.length > 0) {
          this.dateTxt =
            moment(newVal[0]).format("YYYY -MM") +
            " - " +
            moment(newVal[1]).format("YYYY -MM");
            moment(newVal[0]).format('YYYY -MM') +
            ' - ' +
            moment(newVal[1]).format('YYYY -MM')
          // 当日期范围改变时,重新获取风险趋势数据
          if (this.projectId) {
            this.getProjectRiskTrend();
            this.getProjectRiskTrend()
          }
        }
      },
      immediate: true,
    },
      immediate: true
    }
  },
  created() {
    this.mapPromise = AMapLoader.load({
      key: "232929db1cb31f26d42b3645a7456154",
      version: "2.0",
    });
  created () {
    const loader =
      window.AMapLoader ||
      (typeof AMapLoader !== 'undefined' ? AMapLoader : null)
    if (!loader) return
    this.mapPromise = loader.load({
      key: '6edc6798d22e5a46660ff46560b1e612',
      version: '2.0',
      plugins: ['AMap.Geocoder']
    })
    if (this.$route.query.projectId) {
      this.projectId = this.$route.query.projectId || ''
      // 添加错误处理,确保登录信息获取失败不影响其他模块
      getLoginInfo({ id: this.projectId, type: 2 }).then(res => {
        console.log('res', res)
        this.projectName = res.name
        const permissions = res.permissionProject ? res.permissionProject.split(",") : []
        this.permissionProject = ['1', '2', '3', '4', '5', '6', '7'] || permissions
        localStorage.setItem("permissionProject", JSON.stringify(permissions));
        localStorage.setItem('commentType', res.commentType)
        localStorage.setItem("deviceTemplateId", res.deviceTemplateId);//设备模板id
        localStorage.setItem("safetyId", res.safetyId);//安全报告模板id
      }).catch(err => {
        console.error('获取登录信息失败:', err)
        // 设置默认值,确保页面可以继续渲染
        this.projectName = '项目详情'
        this.permissionProject = []
      })
      getLoginInfo({ id: this.projectId, type: 2 })
        .then((res) => {
          this.projectName = res.projectName
          if (sessionStorage.getItem('userInfo')) {
            this.permissionProject =
              JSON.parse(sessionStorage.getItem('permissionProject')) || []
          } else {
            const permissions = res.permissionProject
              ? res.permissionProject.split(',')
              : []
            this.permissionProject = permissions
            sessionStorage.setItem(
              'permissionProject',
              JSON.stringify(permissions)
            )
          }
          sessionStorage.setItem('deviceTemplateId', res.deviceTemplateId) // 设备模板id
          sessionStorage.setItem('deviceTemplateName', res.deviceTemplateName) // 设备模板id
          sessionStorage.setItem('safetyId', res.safetyId) // 安全报告模板id
          sessionStorage.setItem('safetyName', res.safetyName)
          sessionStorage.setItem('bigHiddenDanger', res.bigHiddenDanger) // 1开启 2关闭 重大隐患
          sessionStorage.setItem(
            'generalHiddenDanger',
            res.generalHiddenDanger
          ) // 1开启 2关闭 一般隐患
          sessionStorage.setItem('commentType', res.commentType) // 1模糊评价 2打分评价
          sessionStorage.setItem('clientName', res.name)
          sessionStorage.setItem('clientLogo', res.logo)
          sessionStorage.setItem('clientId', res.clientId)
          this.clientName = res.name
          this.clientLogo = res.logo
        })
        .catch((err) => {
          console.error('获取登录信息失败:', err)
          // 设置默认值,确保页面可以继续渲染
          this.projectName = '项目详情'
          // this.permissionProject = []
        })
      this.getDetail()
      this.getDangerTrendList()
      this.getProjectRiskTrend()
    }
  },
  mounted() {
    this.setMapload()
  mounted () {
    setTimeout(() => {
      this.initRiskChart()
    }, 1000)
  },
  beforeDestroy() { },
  activated () {
    // 当从其他页面返回时,重新设置地图 marker
    if (this.mapView && this.projectInfo.type !== undefined) {
      this.updateMapMarker()
    }
  },
  beforeDestroy () {
    this._mapDestroyed = true
    if (this._mapResizeHandler) {
      window.removeEventListener('resize', this._mapResizeHandler)
      this._mapResizeHandler = null
    }
    if (this.mapView) {
      this.mapView.destroy()
      this.mapView = null
    }
    this.currentMarker = null
    this._lastMarkerPosKey = ''
  },
  methods: {
    // 获取项目详情
    getDetail() {
      // 设备类型 - 添加错误处理
      deviceTypes().then(res => {
        this.deviceTypeList = res
      }).catch(err => {
        console.error('获取设备类型失败:', err)
        this.deviceTypeList = []
      })
      // 获取项目详情 - 添加错误处理
      getProjectDetail(this.projectId).then(res => {
        const addressInfo = res.addressInfo ? JSON.parse(res.addressInfo) : {}
        const constructionCompanyInfo = res.constructionCompanyInfo ? JSON.parse(res.constructionCompanyInfo) : {}
        const developmentCompanyInfo = res.developmentCompanyInfo ? JSON.parse(res.developmentCompanyInfo) : {}
        const supervisorCompanyInfo = res.supervisorCompanyInfo ? JSON.parse(res.supervisorCompanyInfo) : {}
        this.projectInfo = {
          ...res,
          addressInfo: addressInfo,
          address: (addressInfo.province || '') + (addressInfo.city || '') + (addressInfo.district || '') + (addressInfo.detail || ''),
          constructionCompanyInfo: constructionCompanyInfo,
          developmentCompanyInfo: developmentCompanyInfo,
          supervisorCompanyInfo: supervisorCompanyInfo,
    // 打开项目进度弹窗
    toProjectDetail () {
      this.progressVisible = true
      this.getLatestProgress()
    },
    // 获取最新进度
    async getLatestProgress () {
      this.progressLoading = true
      try {
        const res = await getProjectLatestProgress(this.projectId)
        if (res) {
          this.latestProgress = res
          // 解析并检测媒体文件类型
          await this.detectMediaTypes(res.url)
        } else {
          this.latestProgress = {}
          this.progressMediaList = []
        }
        console.log('projectInfo projectInfo', this.projectInfo)
      }).catch(err => {
        console.error('获取项目详情失败:', err)
        // 设置默认值
        this.projectInfo = {
          addressInfo: {},
          address: '',
          constructionCompanyInfo: {},
          developmentCompanyInfo: {},
          supervisorCompanyInfo: {},
          properties: []
      } catch (error) {
        console.error('获取最新进度失败:', error)
        this.$message.error('获取最新进度失败')
        this.latestProgress = {}
        this.progressMediaList = []
      } finally {
        this.progressLoading = false
      }
    },
    // 关闭进度弹窗
    closeProgressDialog () {
      this.progressVisible = false
      this.latestProgress = {}
    },
    // 跳转到历史进度页面
    toProgressHistory () {
      this.progressVisible = false
      this.$router.push({
        path: '/projectProgress',
        query: {
          projectId: this.projectId,
          projectName: this.projectName
        }
      })
    },
    // 格式化进度时间
    formatProgressTime (time) {
      return time ? moment(time).format('YYYY-MM-DD HH:mm:ss') : '-'
    },
    // 通过 Content-Type 检测媒体文件类型(加载时检测所有文件)
    async detectMediaTypes (urlStr) {
      if (!urlStr) {
        this.progressMediaList = []
        return
      }
      // 设备数量 - 添加错误处理
      getProjectDeviceCount(this.projectId).then(res => {
        this.equipment = 0
        this.legendData = res.map(item => {
          this.equipment += item.count
          const deviceType = this.deviceTypeList[item.type]
      try {
        let fileIds = []
        // 如果是数组,直接使用
        if (Array.isArray(urlStr)) {
          fileIds = urlStr
        }
        // 如果是逗号分隔的字符串
        else if (typeof urlStr === 'string' && urlStr.includes(',')) {
          fileIds = urlStr.split(',').filter((item) => item.trim())
        }
        // 如果是单个字符串
        else if (typeof urlStr === 'string' && urlStr.trim()) {
          fileIds = [urlStr.trim()]
        }
        // 并发检测所有文件的类型
        const mediaPromises = fileIds.map(async (id) => {
          let url = ''
          // 如果已经是完整的URL(http或https开头),直接使用
          if (id.startsWith('http://') || id.startsWith('https://')) {
            url = id
          } else {
            // 否则拼接为下载URL
            url = `${this.VUE_APP_BASE_URL}/public/file/download/${id}`
          }
          // 先尝试从扩展名判断
          let type = 'image'
          if (url.match(/\.(mp4|webm|ogg|avi|mov)$/i)) {
            type = 'video'
          } else {
            // 如果没有扩展名,通过 Content-Type 检测
            type = await this.getFileTypeByContentType(url)
          }
          return {
            ...item,
            value: item.count || 0,
            name: deviceType && deviceType.name ? deviceType.name : '未知设备',
            itemStyle: { color: this.generateRandomColor() }
            url: url,
            type: type, // 'image' 或 'video'
            id: id
          }
        })
        this.initPie3D()
      }).catch(err => {
        console.error('获取设备数量失败:', err)
        this.equipment = 0
        this.legendData = []
      })
      // 隐患数量 - 添加错误处理
      getDangerCount(this.projectId).then(res => {
        this.dangerCount = res
        console.log('getDangerCount getDangerCount', res)
      }).catch(err => {
        console.error('获取隐患数量失败:', err)
        this.dangerCount = {
          serious: 0,
          rectified: 0,
          unRectified: 0
        this.progressMediaList = await Promise.all(mediaPromises)
      } catch (error) {
        console.error('检测媒体文件类型失败:', error)
        this.progressMediaList = []
      }
    },
    // 通过 Content-Type 判断文件类型(使用 GET 请求,和后台管理保持一致)
    async getFileTypeByContentType (url) {
      try {
        // 使用 GET 请求获取响应头(和 React 后台管理一致)
        const response = await fetch(url, {
          method: 'GET',
          mode: 'cors'
        })
        const contentType = response.headers.get('Content-Type') || ''
        // 判断是否为视频
        if (contentType.startsWith('video/')) {
          return 'video'
        }
      })
        // 判断是否为图片
        else if (contentType.startsWith('image/')) {
          return 'image'
        }
        // 默认当作图片处理
        else {
          return 'image'
        }
      } catch (error) {
        console.error('获取文件 Content-Type 失败:', url, error)
        // 如果请求失败,默认当作图片处理
        return 'image'
      }
    },
    // 预览进度媒体(图片或视频)- 弹窗预览
    previewProgressMedia (item) {
      if (item && item.url) {
        this.previewUrl = item.url
        this.previewType = item.type || 'image'
        this.previewVisible = true
      }
    },
    // 关闭预览弹窗
    closePreview () {
      this.previewVisible = false
      this.previewUrl = ''
      this.previewType = 'image'
    },
    // 图片加载失败处理
    handleImageError (e) {
      e.target.src = require('../../assets/img/logo.png')
    },
    // 视频加载失败处理
    handleVideoError (e) {
      console.error('视频加载失败:', e)
      // 可以在这里添加视频加载失败的处理逻辑
    },
    normalizeDangerCount (res) {
      return {
        serious: res && res.serious !== undefined ? res.serious : 0,
        rectified: res && res.rectified !== undefined ? res.rectified : 0,
        unRectified: res && res.unRectified !== undefined ? res.unRectified : 0
      }
    },
    async loadDangerCount (tab = this.dangerCountTab, force = false) {
      const cacheData = this.dangerCountCache[tab]
      if (!force && cacheData) {
        this.dangerCount = { ...cacheData }
        return
      }
      this.dangerCountLoading = true
      try {
        const requestApi = tab === 'all' ? getDangerCountAll : getDangerCount
        const res = await requestApi(this.projectId)
        const countData = this.normalizeDangerCount(res)
        this.dangerCount = countData
        this.dangerCountCache[tab] = { ...countData }
      } catch (err) {
        console.error(
          `获取${tab === 'all' ? '所有隐患数量' : '重大隐患数量'}失败:`,
          err
        )
        this.dangerCount = this.normalizeDangerCount()
      } finally {
        this.dangerCountLoading = false
      }
    },
    handleDangerTabChange (tab) {
      if (this.dangerCountTab === tab) return
      this.dangerCountTab = tab
      this.loadDangerCount(tab)
    },
    // 获取项目详情
    getDetail () {
      this.dangerCountCache = {
        all: null,
        serious: null
      }
      // 设备类型 - 添加错误处理
      deviceTypes()
        .then((res) => {
          this.deviceTypeList = res
        })
        .catch((err) => {
          console.error('获取设备类型失败:', err)
          this.deviceTypeList = []
        })
      // 获取项目详情 - 添加错误处理
      getProjectDetail(this.projectId)
        .then((res) => {
          const addressInfo = res.addressInfo
            ? JSON.parse(res.addressInfo)
            : {}
          const constructionCompanyInfo = res.constructionCompanyInfo
            ? JSON.parse(res.constructionCompanyInfo)
            : {}
          const developmentCompanyInfo = res.developmentCompanyInfo
            ? JSON.parse(res.developmentCompanyInfo)
            : {}
          const supervisorCompanyInfo = res.supervisorCompanyInfo
            ? JSON.parse(res.supervisorCompanyInfo)
            : {}
          this.projectInfo = {
            ...res,
            addressInfo: addressInfo,
            address:
              (addressInfo.province || '') +
              (addressInfo.city || '') +
              (addressInfo.district || '') +
              (addressInfo.detail || ''),
            constructionCompanyInfo: constructionCompanyInfo,
            developmentCompanyInfo: developmentCompanyInfo,
            supervisorCompanyInfo: supervisorCompanyInfo
          }
          this.$nextTick(() => {
            this.setMapload().then(() => {
              this.updateMapMarker()
            })
          })
        })
        .catch((err) => {
          console.error('获取项目详情失败:', err)
          // 设置默认值
          this.projectInfo = {
            addressInfo: {},
            address: '',
            constructionCompanyInfo: {},
            developmentCompanyInfo: {},
            supervisorCompanyInfo: {},
            properties: []
          }
        })
      // 设备数量 - 添加错误处理
      getProjectDeviceCount(this.projectId)
        .then((res) => {
          this.equipment = 0
          this.legendData = res.map((item) => {
            this.equipment += item.count
            const deviceType = this.deviceTypeList[item.type]
            return {
              ...item,
              value: item.count || 0,
              name:
                deviceType && deviceType.name ? deviceType.name : '未知设备',
              itemStyle: { color: this.generateRandomColor() }
            }
          })
          this.initPie3D()
        })
        .catch((err) => {
          console.error('获取设备数量失败:', err)
          this.equipment = 0
          this.legendData = []
        })
      this.loadDangerCount(this.dangerCountTab, true)
      // 项目人员信息 - 添加错误处理
      getProjectPersonInfo({ projectId: this.projectId }).then(res => {
        this.peopleList = res || []
      }).catch(err => {
        console.error('获取人员信息失败:', err)
        this.peopleList = []
      })
      getProjectPersonInfo({ projectId: this.projectId })
        .then((res) => {
          this.peopleList = res || []
        })
        .catch((err) => {
          console.error('获取人员信息失败:', err)
          this.peopleList = []
        })
      // 获取项目风险源 - 添加错误处理
      getProjectSource({ projectId: this.projectId }).then(res => {
        const riskSourceList = []
      getProjectSource({ projectId: this.projectId })
        .then((res) => {
          const riskSourceList = []
        // 处理超危大工程
        if (res.superDangerousOptions) {
          try {
            const superDangerousArray = JSON.parse(res.superDangerousOptions)
            const transformedData = transformRiskSourceData(superDangerousArray, 'superDangerous', '超危大工程')
            riskSourceList.push(...transformedData)
          } catch (e) {
            console.error('解析超危大工程数据失败:', e)
          // 处理超危大工程
          if (res.superDangerousOptions) {
            try {
              const superDangerousArray = JSON.parse(res.superDangerousOptions)
              const transformedData = transformRiskSourceData(
                superDangerousArray,
                'superDangerous',
                '超危大工程'
              )
              riskSourceList.push(...transformedData)
            } catch (e) {
              console.error('解析超危大工程数据失败:', e)
            }
          }
        }
        // 处理危大工程
        if (res.dangerousOptions) {
          try {
            const dangerousArray = JSON.parse(res.dangerousOptions)
            const transformedData = transformRiskSourceData(dangerousArray, 'dangerous', '危大工程')
            riskSourceList.push(...transformedData)
          } catch (e) {
            console.error('解析危大工程数据失败:', e)
          // 处理危大工程
          if (res.dangerousOptions) {
            try {
              const dangerousArray = JSON.parse(res.dangerousOptions)
              const transformedData = transformRiskSourceData(
                dangerousArray,
                'dangerous',
                '危大工程'
              )
              riskSourceList.push(...transformedData)
            } catch (e) {
              console.error('解析危大工程数据失败:', e)
            }
          }
        }
        // 处理风险源
        if (res.hazardSource) {
          try {
            const hazardSourceArray = JSON.parse(res.hazardSource)
            const transformedData = transformRiskSourceData(hazardSourceArray, 'hazardSource', '风险源')
            riskSourceList.push(...transformedData)
          } catch (e) {
            console.error('解析风险源数据失败:', e)
          // 处理风险源
          if (res.hazardSource) {
            try {
              const hazardSourceArray = JSON.parse(res.hazardSource)
              const transformedData = transformRiskSourceData(
                hazardSourceArray,
                'hazardSource',
                '风险源'
              )
              riskSourceList.push(...transformedData)
            } catch (e) {
              console.error('解析风险源数据失败:', e)
            }
          }
        }
        this.riskSourceData = riskSourceList
      }).catch(err => {
        console.error('获取项目风险源失败:', err)
        this.riskSourceData = []
      })
          this.riskSourceData = riskSourceList
        })
        .catch((err) => {
          console.error('获取项目风险源失败:', err)
          this.riskSourceData = []
        })
    },
    // 获取隐患趋势
    getDangerTrendList() {
    getDangerTrendList () {
      let params = {
        projectId: this.projectId,
        type: this.dangerSortType,// 1数量 2 趋势
        desc: this.dangerSortDesc, // 是否倒序
        type: this.dangerSortType, // 1数量 2 趋势
        desc: this.dangerSortDesc // 是否倒序
      }
      getDangerTrend(params).then(res => {
        this.dangerTrendList = res || []
      }).catch(err => {
        console.error('获取隐患趋势失败:', err)
        this.dangerTrendList = []
      })
      getDangerTrend(params)
        .then((res) => {
          this.dangerTrendList = res || []
        })
        .catch((err) => {
          console.error('获取隐患趋势失败:', err)
          this.dangerTrendList = []
        })
    },
    // 切换隐患排序方式
    changeDangerSort(type) {
    changeDangerSort (type) {
      if (this.dangerSortType === type) {
        // 如果点击的是当前排序类型,则切换升序/降序
        this.dangerSortDesc = !this.dangerSortDesc
@@ -617,7 +1305,7 @@
      this.getDangerTrendList()
    },
    // 计算进度条宽度
    getBarWidth(current, compare) {
    getBarWidth (current, compare) {
      const max = Math.max(current, compare)
      if (max === 0) return '0px'
      const percentage = (current / max) * 100
@@ -625,199 +1313,380 @@
      return `${(percentage / 100) * maxWidth}px`
    },
    // 人员在岗率
    toPersonnelOnDuty() {
    toPersonnelOnDuty () {
      this.$router.push({
        path: "/personnelOnDuty",
        path: '/personnelOnDuty',
        query: {
          projectId: this.projectId,
          projectName: this.projectName,
        },
      });
          projectName: this.projectName
        }
      })
    },
    // 跳转隐患列表
    toHiddenDanger(type) {
    toHiddenDanger (type) {
      this.$router.push({
        path: "/hiddenDanger",
        path: '/hiddenDanger',
        query: {
          projectId: this.projectId,
          projectName: this.projectName,
          activeTab: type,
        },
      });
          type: 2
        }
      })
    },
    // 跳转设备列表
    toEquipmentInfo() {
    toEquipmentInfo () {
      this.$router.push({
        path: "/equipmentList",
        path: '/equipmentList',
        query: {
          projectId: this.projectId,
          projectName: this.projectName,
        },
      });
          projectName: this.projectName
        }
      })
    },
    // 跳转重大危险源
    toMajorRiskList() {
    toMajorRiskList () {
      this.$router.push({
        path: "/majorRisk",
        path: '/majorRisk',
        query: {
          projectId: this.projectId,
          projectName: this.projectName,
        },
      });
          projectName: this.projectName
        }
      })
    },
    toPlanOfAudit() {
    toPlanOfAudit () {
      this.$router.push({
        path: "/planOfAudit",
        path: '/planOfAudit',
        query: {
          projectId: this.projectId,
          projectName: this.projectName,
          type: 2
        },
      });
        }
      })
    },
    setMapload() {
    setMapload () {
      if (!this.mapPromise) {
        this.mapPromise = AMapLoader.load({
          key: "232929db1cb31f26d42b3645a7456154",
          version: "2.0",
        });
        const loader =
          window.AMapLoader ||
          (typeof AMapLoader !== 'undefined' ? AMapLoader : null)
        if (!loader) {
          return Promise.reject(new Error('AMapLoader is not available'))
        }
        this.mapPromise = loader.load({
          key: '6edc6798d22e5a46660ff46560b1e612',
          version: '2.0',
          plugins: ['AMap.Geocoder']
        })
      }
      this.mapPromise
        .then((AMap) => {
          // 微信建筑图层
          let satellite = new AMap.TileLayer.Satellite();
          //创建路网图层
          let roadNet = new AMap.TileLayer.RoadNet();
          this.mapView = new AMap.Map("container", {
            zoom: 13,
            center: [104.065617, 30.65695], // 初始化地图中心点
            // mapStyle: "amap://styles/darkblue",
            layers: [satellite],
          });
      if (this._mapInitPromise) {
        return this._mapInitPromise
      }
          // 在地图中心添加marker
          const marker = new AMap.Marker({
            position: [104.065617, 30.65695], // 使用地图中心点坐标
      this._mapInitPromise = this.mapPromise
        .then((AMap) => {
          if (this._mapDestroyed) return AMap
          this._AMap = AMap
          if (this.mapView) {
            this.mapView.resize()
            if (!this._mapResizeHandler) {
              this._mapResizeHandler = () => {
                if (this.mapView) this.mapView.resize()
              }
              window.addEventListener('resize', this._mapResizeHandler)
            }
            return AMap
          }
          const satellite = new AMap.TileLayer.Satellite()
          const roadNet = new AMap.TileLayer.RoadNet()
          // 获取经纬度
          let center = [104.065617, 30.65695] // 默认中心点
          let longitude = null
          let latitude = null
          if (this.projectInfo && this.projectInfo.addressInfo) {
            longitude = Number(this.projectInfo.addressInfo.longitude)
            latitude = Number(this.projectInfo.addressInfo.latitude)
            if (
              !isNaN(longitude) &&
              !isNaN(latitude) &&
              longitude !== 0 &&
              latitude !== 0
            ) {
              center = [longitude, latitude]
            }
          }
          this.mapView = new AMap.Map('projectContainer', {
            zoom: 16,
            center: center,
            layers: [satellite, roadNet]
          })
          this.mapView.resize()
          if (!this._mapResizeHandler) {
            this._mapResizeHandler = () => {
              if (this.mapView) this.mapView.resize()
            }
            window.addEventListener('resize', this._mapResizeHandler)
          }
          return AMap
        })
        .catch((e) => {
          console.error(e)
          throw e
        })
        .finally(() => {
          this._mapInitPromise = null
        })
      return this._mapInitPromise
    },
    // 更新地图 marker
    updateMapMarker () {
      if (!this.mapView) return
      const AMap = this._AMap || window.AMap
      if (!AMap) return
      // 根据项目类型选择图标
      const iconImages = [
        require('../../assets/img/home/new-mark.png'),
        require('../../assets/img/home/s4.png'),
        require('../../assets/img/home/z4.png')
      ]
      // const iconImage = iconImages[this.projectInfo.type] || iconImages[0];
      const iconImage = iconImages[0]
      const tryRenderMarker = (lng, lat) => {
        if (
          typeof lng !== 'number' ||
          typeof lat !== 'number' ||
          isNaN(lng) ||
          isNaN(lat)
        ) {
          return false
        }
        const nextKey = `${lng},${lat}`
        if (this.currentMarker && this._lastMarkerPosKey === nextKey) {
          return true
        }
        if (!this.currentMarker) {
          this.currentMarker = new AMap.Marker({
            position: [lng, lat],
            icon: new AMap.Icon({
              image: require('../../assets/img/project/mark.png'),
              image: iconImage,
              size: new AMap.Size(60, 60),
              imageSize: new AMap.Size(60, 60)
            }),
            offset: new AMap.Pixel(-30, -30),
            zoom: 16
          })
          this.mapView.add(this.currentMarker)
        } else if (typeof this.currentMarker.setPosition === 'function') {
          this.currentMarker.setPosition([lng, lat])
        } else {
          this.mapView.remove(this.currentMarker)
          this.currentMarker = new AMap.Marker({
            position: [lng, lat],
            icon: new AMap.Icon({
              image: iconImage,
              size: new AMap.Size(30, 30),
              imageSize: new AMap.Size(30, 30)
            }),
            offset: new AMap.Pixel(-15, -15) // 设置偏移量,使图标中心对准坐标点
          });
            offset: new AMap.Pixel(-15, -15),
            zoom: 16
          })
          this.mapView.add(this.currentMarker)
        }
          // 将marker添加到地图
          this.mapView.add(marker);
        if (typeof this.mapView.setZoomAndCenter === 'function') {
          if (!this._lastMarkerPosKey) {
            this.mapView.setZoomAndCenter(16, [lng, lat])
          } else if (typeof this.mapView.setCenter === 'function') {
            this.mapView.setCenter([lng, lat])
          } else {
            this.mapView.setZoomAndCenter(16, [lng, lat])
          }
        }
        this._lastMarkerPosKey = nextKey
        return true
      }
          // this.initCategoryMarkers(AMap);
        })
        .catch((e) => {
          console.error(e); //加载错误提示
        });
      // 1) 优先使用后端返回的经纬度(如果有)
      const info = this.projectInfo.addressInfo || {}
      let lng = null
      let lat = null
      if (info.longitude && info.latitude) {
        lng = Number(info.longitude)
        lat = Number(info.latitude)
      } else {
        const lonLatStr = typeof info.lonLat === 'string' ? info.lonLat : ''
        const lonLatParts = lonLatStr.split(',')
        lng = lonLatParts[0] ? Number(lonLatParts[0]) : null
        lat = lonLatParts[1] ? Number(lonLatParts[1]) : null
      }
      if (tryRenderMarker(lng, lat)) {
        return
      }
      // 2) 无经纬度时,使用地址进行地理编码
      const address =
        this.projectInfo.address ||
        [
          info && info.province,
          info && info.city,
          info && info.district,
          info && info.detail
        ]
          .filter(Boolean)
          .join('')
      if (address && this.mapPromise) {
        this.mapPromise
          .then(() => {
            const Geocoder =
              (window.AMap && window.AMap.Geocoder) ||
              (typeof AMap !== 'undefined' && AMap.Geocoder)
            if (!Geocoder) return
            const geocoder = new Geocoder({
              city: (info && (info.city || info.province)) || ''
            })
            geocoder.getLocation(address, (status, result) => {
              if (
                status === 'complete' &&
                result &&
                result.geocodes &&
                result.geocodes.length > 0
              ) {
                const loc = result.geocodes[0].location
                const lnglat = [loc.lng, loc.lat]
                tryRenderMarker(lnglat[0], lnglat[1])
              } else {
                // 回退到默认中心点
                tryRenderMarker(104.065617, 30.65695)
              }
            })
          })
          .catch(() => {
            // 加载失败时回退到默认中心点
            tryRenderMarker(104.065617, 30.65695)
          })
      } else {
        // 3) 地址不可用时,回退到默认中心点
        tryRenderMarker(104.065617, 30.65695)
      }
    },
    // 初始化 3D 环饼图(echarts-gl)
    initPie3D() {
      const el = this.$refs.pieChart;
      if (!el) return;
      this._pie = echarts.init(el);
    initPie3D () {
      const el = this.$refs.pieChart
      if (!el) return
      this._pie = echarts.init(el)
      // 使用动态数据或默认数据
      const data = this.legendData.length > 0 ? this.legendData : [];
      const data = this.legendData.length > 0 ? this.legendData : []
      // 生成 3D 饼图 option(internalDiameterRatio 控制环形内径比)
      this.legendData = data; // 用于右侧自定义 legend
      const series = this.getPie3D(data, 0.7);
      this.legendData = data // 用于右侧自定义 legend
      const series = this.getPie3D(data, 0.7)
      const option = {
        legend: {
          tooltip: {
            show: true,
            show: true
          },
          data: data.map((item) => item.name),
          right: "2%",
          top: "center",
          orient: "vertical",
          right: '2%',
          top: 'center',
          orient: 'vertical',
          itemGap: 20,
          textStyle: {
            color: "#fff",
            fontSize: 12,
            color: '#fff',
            fontSize: 12
          },
          show: false,
          show: false
        },
        graphic: {
          elements: [
            {
              type: "circle",
              type: 'circle',
              shape: {
                cx: "50%",
                cy: "50%",
                r: "35%",
                cx: '50%',
                cy: '50%',
                r: '35%'
              },
              style: {
                fill: "transparent",
                fill: 'transparent',
                shadowBlur: 20,
                shadowColor: "rgba(0, 191, 255, 0.4)",
                opacity: 0.6,
                shadowColor: 'rgba(0, 191, 255, 0.4)',
                opacity: 0.6
              },
              z: -1,
            },
          ],
              z: -1
            }
          ]
        },
        animation: true,
        tooltip: {
          formatter: (params) => {
            if (
              params.seriesName !== "mouseoutSeries" &&
              params.seriesName !== "pie2d"
              params.seriesName !== 'mouseoutSeries' &&
              params.seriesName !== 'pie2d'
            ) {
              const series = option.series[params.seriesIndex];
              const series = option.series[params.seriesIndex]
              if (series && series.pieData) {
                return `${params.seriesName}<br/><span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${params.color};"></span>${series.pieData.value}`;
                return `${params.seriesName}<br/><span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${params.color};"></span>${series.pieData.value}`
              }
            }
          },
          textStyle: {
            fontSize: 12,
          },
            fontSize: 12
          }
        },
        title: {
          x: "center",
          top: "20",
          x: 'center',
          top: '20',
          textStyle: {
            color: "#fff",
            fontSize: 12,
          },
            color: '#fff',
            fontSize: 12
          }
        },
        backgroundColor: "transparent",
        backgroundColor: 'transparent',
        labelLine: {
          show: false,
          lineStyle: {
            color: "transparent",
          },
            color: 'transparent'
          }
        },
        label: {
          show: false,
          color: "transparent",
          position: "outside",
          formatter: "{b} \n{c} {d}%",
          color: 'transparent',
          position: 'outside',
          formatter: '{b} \n{c} {d}%'
        },
        xAxis3D: {
          min: -1,
          max: 1,
          max: 1
        },
        yAxis3D: {
          min: -1,
          max: 1,
          max: 1
        },
        zAxis3D: {
          min: -1,
          max: 1,
          max: 1
        },
        grid3D: {
          show: false,
          boxHeight: 2,
          bottom: "-10%",
          left: "-15%",
          bottom: '-10%',
          left: '-15%',
          viewControl: {
            alpha: 18,
            beta: 50,
@@ -825,32 +1694,32 @@
            autoRotate: true,
            rotateSensitivity: 0,
            zoomSensitivity: 0,
            panSensitivity: 0,
          },
            panSensitivity: 0
          }
        },
        series: series,
      };
        series: series
      }
      this._pie.clear();
      this._pie.setOption(option);
      this._pie.clear()
      this._pie.setOption(option)
      // 鼠标事件交互
      let selectedIndex = "";
      let hoveredIndex = "";
      let selectedIndex = ''
      let hoveredIndex = ''
      this._pie.on("click", (params) => {
        if (params.seriesName === "mouseoutSeries") return;
        const s = option.series[params.seriesIndex];
        if (!s || !s.pieData || !s.pieStatus) return;
      this._pie.on('click', (params) => {
        if (params.seriesName === 'mouseoutSeries') return
        const s = option.series[params.seriesIndex]
        if (!s || !s.pieData || !s.pieStatus) return
        const isSelected = !s.pieStatus.selected;
        const isHovered = s.pieStatus.hovered;
        const k = s.pieStatus.k;
        const startRatio = s.pieData.startRatio;
        const endRatio = s.pieData.endRatio;
        const isSelected = !s.pieStatus.selected
        const isHovered = s.pieStatus.hovered
        const k = s.pieStatus.k
        const startRatio = s.pieData.startRatio
        const endRatio = s.pieData.endRatio
        if (selectedIndex !== "" && selectedIndex !== params.seriesIndex) {
          const prev = option.series[selectedIndex];
        if (selectedIndex !== '' && selectedIndex !== params.seriesIndex) {
          const prev = option.series[selectedIndex]
          if (prev && prev.pieData && prev.pieStatus) {
            prev.parametricEquation = this.getParametricEquation(
              prev.pieData.startRatio,
@@ -859,8 +1728,8 @@
              false,
              k,
              prev.pieData.value
            );
            prev.pieStatus.selected = false;
            )
            prev.pieStatus.selected = false
          }
        }
@@ -871,21 +1740,21 @@
          isHovered,
          k,
          s.pieData.value
        );
        s.pieStatus.selected = isSelected;
        if (isSelected) selectedIndex = params.seriesIndex;
        )
        s.pieStatus.selected = isSelected
        if (isSelected) selectedIndex = params.seriesIndex
        this._pie.setOption(option);
      });
        this._pie.setOption(option)
      })
      this._pie.on("mouseover", (params) => {
        if (hoveredIndex === params.seriesIndex) return;
      this._pie.on('mouseover', (params) => {
        if (hoveredIndex === params.seriesIndex) return
        // 取消之前的高亮
        if (hoveredIndex !== "") {
          let prev = option.series[hoveredIndex];
        if (hoveredIndex !== '') {
          let prev = option.series[hoveredIndex]
          if (prev && prev.pieData && prev.pieStatus) {
            const k = prev.pieStatus.k;
            const k = prev.pieStatus.k
            prev.parametricEquation = this.getParametricEquation(
              prev.pieData.startRatio,
              prev.pieData.endRatio,
@@ -893,17 +1762,17 @@
              false,
              k,
              prev.pieData.value
            );
            prev.pieStatus.hovered = false;
            hoveredIndex = "";
            )
            prev.pieStatus.hovered = false
            hoveredIndex = ''
          }
        }
        // 当前高亮(排除透明圆环)
        if (params.seriesName !== "mouseoutSeries") {
          let cur = option.series[params.seriesIndex];
        if (params.seriesName !== 'mouseoutSeries') {
          let cur = option.series[params.seriesIndex]
          if (cur && cur.pieData && cur.pieStatus) {
            const k = cur.pieStatus.k;
            const k = cur.pieStatus.k
            cur.parametricEquation = this.getParametricEquation(
              cur.pieData.startRatio,
              cur.pieData.endRatio,
@@ -911,20 +1780,20 @@
              true,
              k,
              cur.pieData.value
            );
            cur.pieStatus.hovered = true;
            hoveredIndex = params.seriesIndex;
            )
            cur.pieStatus.hovered = true
            hoveredIndex = params.seriesIndex
          }
        }
        this._pie.setOption(option);
      });
        this._pie.setOption(option)
      })
      this._pie.on("globalout", () => {
        if (hoveredIndex !== "") {
          let prev = option.series[hoveredIndex];
      this._pie.on('globalout', () => {
        if (hoveredIndex !== '') {
          let prev = option.series[hoveredIndex]
          if (prev && prev.pieData && prev.pieStatus) {
            const k = prev.pieStatus.k;
            const k = prev.pieStatus.k
            prev.parametricEquation = this.getParametricEquation(
              prev.pieData.startRatio,
              prev.pieData.endRatio,
@@ -932,23 +1801,23 @@
              false,
              k,
              prev.pieData.value
            );
            prev.pieStatus.hovered = false;
            hoveredIndex = "";
            this._pie.setOption(option);
            )
            prev.pieStatus.hovered = false
            hoveredIndex = ''
            this._pie.setOption(option)
          }
        }
      });
      })
    },
    // 生成随机颜色
    generateRandomColor() {
      const h = Math.floor(Math.random() * 360);
      const s = Math.floor(Math.random() * 30) + 60; // 60-90%
      const l = Math.floor(Math.random() * 20) + 50; // 50-70%
      return `hsl(${h}, ${s}%, ${l}%)`;
    generateRandomColor () {
      const h = Math.floor(Math.random() * 360)
      const s = Math.floor(Math.random() * 30) + 60 // 60-90%
      const l = Math.floor(Math.random() * 20) + 50 // 50-70%
      return `hsl(${h}, ${s}%, ${l}%)`
    },
    // 生成扇形的曲面参数方程
    getParametricEquation(
    getParametricEquation (
      startRatio,
      endRatio,
      isSelected,
@@ -957,135 +1826,135 @@
      height
    ) {
      // 计算
      let midRatio = (startRatio + endRatio) / 2;
      let midRatio = (startRatio + endRatio) / 2
      let startRadian = startRatio * Math.PI * 2;
      let endRadian = endRatio * Math.PI * 2;
      let midRadian = midRatio * Math.PI * 2;
      let startRadian = startRatio * Math.PI * 2
      let endRadian = endRatio * Math.PI * 2
      let midRadian = midRatio * Math.PI * 2
      // 如果只有一个扇形,则不实现选中效果。
      if (startRatio === 0 && endRatio === 1) {
        isSelected = false;
        isSelected = false
      }
      // 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3)
      k = typeof k !== "undefined" ? k : 1 / 3;
      k = typeof k !== 'undefined' ? k : 1 / 3
      // 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0)
      let offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0;
      let offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0;
      let offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0
      let offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0
      // 计算高亮效果的放大比例(未高亮,则比例为 1)
      let hoverRate = isHovered ? 1.05 : 1;
      let hoverRate = isHovered ? 1.05 : 1
      // 返回曲面参数方程
      return {
        u: {
          min: -Math.PI,
          max: Math.PI * 3,
          step: Math.PI / 32,
          step: Math.PI / 32
        },
        v: {
          min: 0,
          max: Math.PI * 2,
          step: Math.PI / 20,
          step: Math.PI / 20
        },
        x: function (u, v) {
          if (u < startRadian) {
            return (
              offsetX +
              Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate
            );
            )
          }
          if (u > endRadian) {
            return (
              offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate
            );
            )
          }
          return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate;
          return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate
        },
        y: function (u, v) {
          if (u < startRadian) {
            return (
              offsetY +
              Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate
            );
            )
          }
          if (u > endRadian) {
            return (
              offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate
            );
            )
          }
          return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate;
          return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate
        },
        z: function (u, v) {
          if (u < -Math.PI * 0.5) {
            return Math.sin(u);
            return Math.sin(u)
          }
          if (u > Math.PI * 2.5) {
            return Math.sin(u);
            return Math.sin(u)
          }
          return Math.sin(v) > 0 ? 0.6 * height : -0.6;
        },
      };
          return Math.sin(v) > 0 ? 0.6 * height : -0.6
        }
      }
    },
    // 生成模拟 3D 饼图的配置项
    getPie3D(pieData, internalDiameterRatio) {
      let series = [];
      let sumValue = 0;
      let startValue = 0;
      let endValue = 0;
      let legendData = [];
    getPie3D (pieData, internalDiameterRatio) {
      let series = []
      let sumValue = 0
      let startValue = 0
      let endValue = 0
      let legendData = []
      let k =
        typeof internalDiameterRatio !== "undefined"
        typeof internalDiameterRatio !== 'undefined'
          ? (1 - internalDiameterRatio) / (1 + internalDiameterRatio)
          : 1 / 3;
          : 1 / 3
      // 为每一个饼图数据,生成一个 series-surface 配置
      for (let i = 0; i < pieData.length; i++) {
        sumValue += pieData[i].value;
        sumValue += pieData[i].value
        let seriesItem = {
          name:
            typeof pieData[i].name === "undefined"
            typeof pieData[i].name === 'undefined'
              ? `series${i}`
              : pieData[i].name,
          type: "surface",
          type: 'surface',
          parametric: true,
          wireframe: {
            show: false,
            show: false
          },
          pieData: pieData[i],
          pieStatus: {
            selected: false,
            hovered: false,
            k: k,
          },
        };
        if (typeof pieData[i].itemStyle != "undefined") {
          let itemStyle = {};
          typeof pieData[i].itemStyle.color != "undefined"
            ? (itemStyle.color = pieData[i].itemStyle.color)
            : null;
          typeof pieData[i].itemStyle.opacity != "undefined"
            ? (itemStyle.opacity = pieData[i].itemStyle.opacity)
            : null;
          seriesItem.itemStyle = itemStyle;
            k: k
          }
        }
        series.push(seriesItem);
        if (typeof pieData[i].itemStyle !== 'undefined') {
          let itemStyle = {}
          typeof pieData[i].itemStyle.color !== 'undefined'
            ? (itemStyle.color = pieData[i].itemStyle.color)
            : null
          typeof pieData[i].itemStyle.opacity !== 'undefined'
            ? (itemStyle.opacity = pieData[i].itemStyle.opacity)
            : null
          seriesItem.itemStyle = itemStyle
        }
        series.push(seriesItem)
      }
      // 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数,
      // 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。
      for (let i = 0; i < series.length; i++) {
        endValue = startValue + series[i].pieData.value;
        console.log(series[i]);
        series[i].pieData.startRatio = startValue / sumValue;
        series[i].pieData.endRatio = endValue / sumValue;
        endValue = startValue + series[i].pieData.value
        series[i].pieData.startRatio = startValue / sumValue
        series[i].pieData.endRatio = endValue / sumValue
        series[i].parametricEquation = this.getParametricEquation(
          series[i].pieData.startRatio,
          series[i].pieData.endRatio,
@@ -1093,11 +1962,11 @@
          false,
          k,
          series[i].pieData.value
        );
        )
        startValue = endValue;
        startValue = endValue
        legendData.push(series[i].name);
        legendData.push(series[i].name)
      }
      // series.push({
      //   name: 'mouseoutSeries',
@@ -1132,79 +2001,75 @@
      //     },
      //   },
      // });
      return series;
      return series
    },
    // 风险等级转换为数值 (A->3, B->2, C->1)
    levelToValue(level) {
      if (!level) return null;
      const levelStr = String(level).toUpperCase().trim();
    levelToValue (level) {
      if (!level) return null
      const levelStr = String(level).toUpperCase().trim()
      // 处理 A、B、C 或 A类、B类、C类
      if (levelStr === 'A' || levelStr === 'A类') return 3;
      if (levelStr === 'B' || levelStr === 'B类') return 2;
      if (levelStr === 'C' || levelStr === 'C类') return 1;
      if (levelStr === 'A' || levelStr === 'A类') return 3
      if (levelStr === 'B' || levelStr === 'B类') return 2
      if (levelStr === 'C' || levelStr === 'C类') return 1
      return null;
      return null
    },
    // 数值转换为风险等级显示 (3->A类, 2->B类, 1->C类)
    valueToLevel(value) {
      if (value === 3) return 'A类';
      if (value === 2) return 'B类';
      if (value === 1) return 'C类';
      return '';
    valueToLevel (value) {
      if (value === 3) return 'A类'
      if (value === 2) return 'B类'
      if (value === 1) return 'C类'
      return ''
    },
    // 获取项目风险趋势数据
    async getProjectRiskTrend() {
    async getProjectRiskTrend () {
      if (!this.dateRange || this.dateRange.length !== 2) {
        return;
        return
      }
      const params = {
        projectId: this.projectId,
        startTime: moment(this.dateRange[0]).format('YYYY-MM'),
        endTime: moment(this.dateRange[1]).format('YYYY-MM')
      };
      }
      try {
        const res = await getProjectDangerousTrend(params);
        console.log('项目风险趋势数据:', res);
        const res = await getProjectDangerousTrend(params)
        if (res && Array.isArray(res)) {
          // 处理接口返回的数据
          this.riskTrendData.xAxisData = res.map(item => item.time || '');
          this.riskTrendData.xAxisData = res.map((item) => item.time || '')
          // 将A、B、C转换为数值 3、2、1
          this.riskTrendData.seriesData = res.map(item => {
            const value = this.levelToValue(item.level);
            console.log(`level: ${item.level} -> value: ${value}`);
            return value;
          });
          this.riskTrendData.seriesData = res.map((item) => {
            const value = this.levelToValue(item.level)
          console.log('转换后的数据:', {
            xAxisData: this.riskTrendData.xAxisData,
            seriesData: this.riskTrendData.seriesData
          });
            return value
          })
          this.riskTrendData.statusData = res.map((item) => item.status)
          // 更新图表
          this.updateRiskChart();
          this.updateRiskChart()
        }
      } catch (error) {
        console.error('获取项目风险趋势失败:', error)
        // 设置默认值,确保图表可以正常显示
        this.riskTrendData.xAxisData = []
        this.riskTrendData.seriesData = []
        this.riskTrendData.statusData = []
      }
    },
    // 更新风险趋势图表
    updateRiskChart() {
      const chartDom = document.getElementById('riskTrendChart');
    updateRiskChart () {
      const chartDom = document.getElementById('riskTrendChart')
      if (!chartDom) {
        console.warn('风险趋势图表DOM不存在,可能是权限限制导致该模块未渲染');
        return;
        console.warn('风险趋势图表DOM不存在,可能是权限限制导致该模块未渲染')
        return
      }
      const riskChart = echarts.getInstanceByDom(chartDom);
      const riskChart = echarts.getInstanceByDom(chartDom)
      if (!riskChart) {
        console.warn('风险趋势图表实例不存在');
        return;
        console.warn('风险趋势图表实例不存在')
        return
      }
      // 文本换行处理函数
@@ -1222,7 +2087,7 @@
        return textStr
      }
      const self = this;
      const self = this
      const option = {
        tooltip: {
@@ -1237,11 +2102,11 @@
          },
          formatter: function (params) {
            if (params && params.length > 0) {
              const param = params[0];
              const levelText = self.valueToLevel(param.value);
              return `${param.name}<br/>${param.seriesName}: ${levelText}`;
              const param = params[0]
              const levelText = self.valueToLevel(param.value)
              return `${param.name}<br/>${param.seriesName}: ${levelText}`
            }
            return '';
            return ''
          }
        },
        legend: {
@@ -1263,7 +2128,14 @@
            }
          },
          axisLabel: {
            color: '#fff',
            color: (value, index) => {
              const status =
                Array.isArray(this.riskTrendData.statusData) &&
                typeof index === 'number'
                  ? this.riskTrendData.statusData[index]
                  : undefined
              return Number(status) === 2 ? '#999' : '#fff'
            },
            fontSize: 12,
            interval: 0,
            rotate: 0,
@@ -1300,10 +2172,10 @@
            fontSize: 12,
            formatter: function (value) {
              // Y轴自定义显示:1->C类, 2->B类, 3->A类
              if (value === 1) return 'C类';
              if (value === 2) return 'B类';
              if (value === 3) return 'A类';
              return '';
              if (value === 1) return 'C类'
              if (value === 2) return 'B类'
              if (value === 3) return 'A类'
              return ''
            },
            align: 'right',
            verticalAlign: 'middle',
@@ -1342,16 +2214,16 @@
            }
          }
        ]
      };
      }
      riskChart.setOption(option);
      riskChart.setOption(option)
    },
    // 初始化折线图
    initRiskChart() {
      const chartDom = document.getElementById('riskTrendChart');
    initRiskChart () {
      const chartDom = document.getElementById('riskTrendChart')
      if (!chartDom) {
        console.warn('风险趋势图表DOM不存在,可能是权限限制导致该模块未渲染');
        return;
        console.warn('风险趋势图表DOM不存在,可能是权限限制导致该模块未渲染')
        return
      }
      const riskChart = echarts.init(chartDom)
@@ -1369,7 +2241,6 @@
        }
        return textStr
      }
      let option = {
        tooltip: {
          trigger: 'axis',
@@ -1386,7 +2257,16 @@
          containLabel: true
        },
        xAxis: {
          data: this.riskTrendData.xAxisData.length > 0 ? this.riskTrendData.xAxisData : ['2025年1月检查', '2025年2月检查', '2025年3月检查', '2025年4月检查', '2025年5月检查'],
          data:
            this.riskTrendData.xAxisData.length > 0
              ? this.riskTrendData.xAxisData
              : [
                '2025年1月检查',
                '2025年2月检查',
                '2025年3月检查',
                '2025年4月检查',
                '2025年5月检查'
              ],
          axisTick: { show: true, alignWithLabel: true },
          axisLine: {
            lineStyle: {
@@ -1394,7 +2274,14 @@
            }
          },
          axisLabel: {
            color: '#fff',
            color: (value, index) => {
              const status =
                Array.isArray(this.riskTrendData.statusData) &&
                typeof index === 'number'
                  ? this.riskTrendData.statusData[index]
                  : undefined
              return Number(status) === 2 ? '#999' : '#fff'
            },
            fontSize: 12,
            interval: 0, // 强制显示所有标签
            rotate: 0, // 不旋转
@@ -1434,10 +2321,10 @@
            fontSize: 12,
            formatter: function (value) {
              // Y轴自定义显示:1->C类, 2->B类, 3->A类
              if (value === 1) return 'C类';
              if (value === 2) return 'B类';
              if (value === 3) return 'A类';
              return '';
              if (value === 1) return 'C类'
              if (value === 2) return 'B类'
              if (value === 3) return 'A类'
              return ''
            },
            // 文本对齐方式
            align: 'right',
@@ -1451,7 +2338,10 @@
          {
            name: '风险等级',
            type: 'line',
            data: this.riskTrendData.seriesData.length > 0 ? this.riskTrendData.seriesData : [10, 52, 20, 33, 39],
            data:
              this.riskTrendData.seriesData.length > 0
                ? this.riskTrendData.seriesData
                : [10, 52, 20, 33, 39],
            smooth: false, // 不使用圆滑过渡
            showSymbol: false, // 不显示转折点
            lineStyle: {
@@ -1474,8 +2364,15 @@
      }
      riskChart.setOption(option)
    },
    toRiskTrend() {
      this.$router.push({ path: '/subentry', query: { projectId: this.projectId, projectName: this.projectName } })
    // 处理风险趋势时间选择器点击
    handleRiskPickerClick () {
      this.$refs.riskDatePicker.focus()
    },
    toRiskTrend () {
      this.$router.push({
        path: '/subentry',
        query: { projectId: this.projectId, projectName: this.projectName }
      })
    }
  }
}
@@ -1491,7 +2388,7 @@
}
.card-more {
  width: 62px;
  min-width: 62px;
  height: 24px;
  background: rgba(14, 93, 251, 0.5);
  border-radius: 4px;
@@ -1506,6 +2403,7 @@
    font-size: 12px;
    color: #ffffff;
    line-height: 12px;
    margin-right: 10px;
  }
  img {
@@ -1557,7 +2455,7 @@
      flex: 1.5;
    }
    .info-item {
    .info-item11 {
      display: flex;
      gap: 10px;
      flex-shrink: 0;
@@ -1569,14 +2467,40 @@
      color: #dcdfe6;
      flex-shrink: 0;
      width: 64px;
      position: relative;
      display: flex;
      // align-items: center;
      gap: 4px;
    }
    .dept {
      width: 76px;
      width: 75px;
    }
    .time {
      width: 100px;
    }
    .count-badge {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-width: 18px;
      height: 18px;
      padding: 0 5px;
      background: linear-gradient(135deg, #32c5ff 0%, #0e5dfb 100%);
      border-radius: 9px;
      font-weight: bold;
      font-size: 11px;
      color: #ffffff;
      cursor: pointer;
      transition: all 0.3s ease;
      box-shadow: 0 2px 6px rgba(50, 197, 255, 0.4);
      &:hover {
        transform: scale(1.1);
        box-shadow: 0 3px 10px rgba(50, 197, 255, 0.6);
      }
    }
    .value {
@@ -1584,6 +2508,24 @@
      font-size: 12px;
      color: #4cefe6;
      max-width: 144px;
      // 项目地址特殊处理:最多显示两行,超出省略
      &.address-value {
        display: -webkit-box;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 2;
        overflow: hidden;
        text-overflow: ellipsis;
        word-break: break-all;
        line-height: 18px;
        max-height: 36px; // 2行的高度:18px * 2
        cursor: pointer;
        transition: color 0.3s ease;
        &:hover {
          color: #66ffff;
        }
      }
    }
  }
@@ -1642,7 +2584,7 @@
        width: 180px;
        height: 32px;
        background-image: url('../../../src/assets/img/project/equipmentBg.png');
        background-image: url("../../../src/assets/img/project/equipmentBg.png");
        background-size: 100% 100%;
        position: relative;
@@ -1668,7 +2610,7 @@
            max-width: 70px;
            font-weight: 400;
            font-size: 14px;
            color: #CCE0F8;
            color: #cce0f8;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap; // 确保单行显示
@@ -1676,14 +2618,14 @@
            transition: color 0.3s ease;
            &:hover {
              color: #FFFFFF; // 悬停时文字变亮
              color: #ffffff; // 悬停时文字变亮
            }
          }
          .legend-number {
            font-weight: bold;
            font-size: 18px;
            color: #1BDDFF;
            color: #1bddff;
            flex-shrink: 0;
          }
        }
@@ -1703,15 +2645,15 @@
      .total {
        font-weight: 400;
        font-size: 34px;
        color: #66FFFF;
        color: #66ffff;
        line-height: 39px;
        text-shadow: 0px 0px 12px #1068F7;
        text-shadow: 0px 0px 12px #1068f7;
      }
      .text {
        font-weight: bold;
        font-size: 16px;
        color: #FFFFFF;
        color: #ffffff;
        line-height: 24px;
      }
    }
@@ -1827,7 +2769,7 @@
        font-weight: 800;
        font-size: 12px;
        color: #66ffff;
        color: rgba(253, 66, 62, 1);
        line-height: 14px;
        font-style: normal;
@@ -1838,10 +2780,10 @@
        }
        &.down {
          color: rgba(253, 66, 62, 1);
          color: #66ffff;
          .trend-icon {
            color: rgba(253, 66, 62, 1);
            color: #66ffff;
          }
        }
      }
@@ -1895,14 +2837,24 @@
    .tab-border {
      border: 1px solid rgba(50, 197, 255, 0.22);
      border-radius: 4px;
      overflow: hidden;
    }
    .mid-tab {
      cursor: pointer;
      user-select: none;
      border-radius: 4px;
      padding: 6px 5px;
      padding: 6px 8px;
      font-weight: 400;
      font-size: 12px;
      color: #ffffff;
      transition: background 0.25s ease, box-shadow 0.25s ease,
        transform 0.25s ease;
      &:hover {
        background: rgba(6, 0, 228, 0.2);
      }
    }
    .active-tab {
@@ -1915,6 +2867,12 @@
      display: flex;
      align-items: center;
      justify-content: center;
      transition: opacity 0.25s ease, transform 0.25s ease;
      &.mid-content-switching {
        opacity: 0.68;
        transform: translateY(4px);
      }
      .mid-img {
        width: 90px;
@@ -1932,10 +2890,12 @@
          font-size: 24px;
          // color: linear-gradient(90deg, #66FFFF 0%, #F7B500 100%, #F7B500 100%);
          line-height: 28px;
          background: linear-gradient(90deg,
              #66ffff 0%,
              #f7b500 100%,
              #f7b500 100%);
          background: linear-gradient(
            90deg,
            #66ffff 0%,
            #f7b500 100%,
            #f7b500 100%
          );
          /*设置渐变的方向从左到右 颜色从ff0000到ffff00*/
          -webkit-background-clip: text;
          /*将设置的背景颜色限制在文字中*/
@@ -1985,18 +2945,23 @@
    width: 42px;
    height: 150px;
    box-sizing: border-box;
    background: linear-gradient(90deg,
        #040622 0%,
        #0952cf 22.29%,
        #155edf 53.43%,
        #0952cf 81.31%,
        #011845 100%);
    background: linear-gradient(
      90deg,
      #040622 0%,
      #0952cf 22.29%,
      #155edf 53.43%,
      #0952cf 81.31%,
      #011845 100%
    );
    border-radius: 96px;
    border: 2px solid;
    border-image: linear-gradient(90deg,
    border-image: linear-gradient(
        90deg,
        rgba(159, 255, 255, 0),
        rgba(133, 255, 255, 1),
        rgba(102, 255, 255, 0)) 3 1;
        rgba(102, 255, 255, 0)
      )
      3 1;
    padding: 24px 12px;
    font-weight: bold;
    font-size: 17px;
@@ -2025,6 +2990,8 @@
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
      overflow: auto;
    }
    .people-card2 {
@@ -2034,9 +3001,11 @@
    }
    .people-item {
      width: 33%;
      display: flex;
      flex-direction: column;
      align-items: center;
      margin-bottom: 25px;
      .people-chart {
        width: 82px;
@@ -2062,13 +3031,13 @@
        .progress-num {
          font-weight: 800;
          font-size: 18px;
          font-size: 14px;
          color: #66ffff;
        }
        .progress-unit {
          font-weight: 800;
          font-size: 12px;
          font-size: 10px;
          color: #66ffff;
          margin-left: 2px;
        }
@@ -2098,7 +3067,7 @@
    .label {
      font-weight: 400;
      font-size: 12px;
      color: #FFFFFF;
      color: #ffffff;
      text-align: center;
    }
@@ -2146,24 +3115,24 @@
          // border-right: 1px solid #337AB6;
          .label {
            border-bottom: 1px solid #337AB6;
            border-bottom: 1px solid #337ab6;
          }
        }
        .label {
          &:nth-last-child(1) {
            border-right: 1px solid #337AB6;
            border-right: 1px solid #337ab6;
            border-left: unset !important;
            padding-left: 6px;
            font-weight: bold;
            font-size: 12px;
            color: #66FFFF;
            color: #66ffff;
            font-style: normal;
          }
          line-height: 36px;
          border-top: 1px solid #337AB6;
          border-left: 1px solid #337AB6;
          border-top: 1px solid #337ab6;
          border-left: 1px solid #337ab6;
          position: relative;
          &.merged-cell {
@@ -2187,9 +3156,7 @@
          }
        }
      }
    }
  }
  .risk-trend {
@@ -2204,8 +3171,13 @@
    position: relative;
    display: flex;
    align-items: center;
    cursor: pointer;
    ::v-deep .el-input__icon {
      display: none;
    }
    ::v-deep .el-range-separator {
      display: none;
    }
@@ -2231,14 +3203,12 @@
      display: flex;
      align-items: center;
      justify-content: space-between;
    }
    .pick-text {
      font-weight: 400;
      font-size: 12px;
      color: #FFFFFF;
      color: #ffffff;
      line-height: 18px;
    }
@@ -2248,6 +3218,296 @@
      margin-right: 12px;
    }
  }
}
// 项目进度弹窗 - 历史进度按钮固定定位
.progress-dialog {
  ::v-deep .el-dialog__header {
    position: relative;
  }
}
.history-btn-fixed {
  position: absolute;
  right: 70px;
  top: 20px;
  z-index: 10;
  width: 120px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(270deg, #09b9ff 0%, #4d68ff 100%);
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s ease;
  &:hover {
    opacity: 0.9;
    transform: translateY(-1px);
    box-shadow: 0 4px 8px rgba(9, 185, 255, 0.4);
  }
  span {
    font-family: SourceHanSansCN, SourceHanSansCN;
    font-weight: 500;
    font-size: 16px;
    color: #ffffff;
    line-height: 24px;
    text-align: center;
    font-style: normal;
    margin-right: 8px;
  }
  .arrow-icon {
    width: 12px;
    height: 12px;
  }
}
// 项目进度弹窗样式
.progress-dialog-content {
  min-height: 500px;
  padding: 30px 40px;
  background: transparent;
  .progress-description {
    font-size: 16px;
    color: rgba(255, 255, 255, 0.9);
    line-height: 1.8;
    margin-bottom: 30px;
    text-align: justify;
    word-break: break-word;
  }
  // 进度媒体网格 - 缩略图展示
  .progress-media-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 16px;
    margin-top: 20px;
    .media-thumbnail {
      position: relative;
      width: 100%;
      height: 150px;
      border-radius: 8px;
      overflow: hidden;
      cursor: pointer;
      border: 2px solid rgba(50, 197, 255, 0.3);
      background: rgba(0, 22, 53, 0.6);
      transition: all 0.3s ease;
      &:hover {
        border-color: rgba(50, 197, 255, 0.8);
        transform: translateY(-4px);
        box-shadow: 0 8px 20px rgba(14, 93, 251, 0.5);
        .thumbnail-overlay {
          opacity: 1;
        }
      }
      .thumbnail-img,
      .thumbnail-video {
        width: 100%;
        height: 100%;
        object-fit: cover;
        display: block;
      }
      .thumbnail-overlay {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.6);
        display: flex;
        align-items: center;
        justify-content: center;
        opacity: 0;
        transition: opacity 0.3s ease;
        i {
          font-size: 32px;
          color: #ffffff;
        }
      }
      .video-badge {
        position: absolute;
        top: 8px;
        right: 8px;
        width: 28px;
        height: 28px;
        background: rgba(0, 0, 0, 0.7);
        border-radius: 50%;
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 1;
        i {
          font-size: 14px;
          color: #32c5ff;
        }
      }
    }
  }
  .no-images {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 200px;
    .no-images-text {
      font-size: 16px;
      color: rgba(255, 255, 255, 0.5);
    }
  }
  .no-data {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 400px;
    padding: 60px 24px;
    .no-data-icon {
      width: 80px;
      height: 80px;
      background: rgba(24, 83, 200, 0.2);
      border: 2px solid rgba(50, 197, 255, 0.3);
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-bottom: 20px;
      i {
        font-size: 40px;
        color: rgba(50, 197, 255, 0.6);
      }
    }
    .no-data-text {
      font-size: 16px;
      color: rgba(255, 255, 255, 0.6);
    }
  }
}
// 媒体预览弹窗样式
.media-preview-dialog {
  ::v-deep .el-dialog {
    background: rgba(0, 0, 0, 0.95);
    border: 1px solid rgba(50, 197, 255, 0.3);
    box-shadow: 0 0 30px rgba(14, 93, 251, 0.5);
  }
  ::v-deep .el-dialog__header {
    display: none;
  }
  ::v-deep .el-dialog__body {
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 400px;
  }
  ::v-deep .el-dialog__close {
    color: #ffffff;
    font-size: 24px;
    &:hover {
      color: #32c5ff;
    }
  }
  .preview-content {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    .preview-image,
    .preview-video {
      max-width: 100%;
      max-height: 80vh;
      object-fit: contain;
      border-radius: 8px;
    }
    .preview-video {
      background: #000000;
    }
  }
}
// Tooltip 提示框样式
::v-deep .el-tooltip__popper {
  max-width: 400px;
  &.is-dark {
    background: rgba(0, 22, 53, 0.95);
  }
}
.tooltip-content {
  min-width: 200px;
  max-height: 300px;
  overflow-y: auto;
  padding: 4px 0;
  // 自定义滚动条
  &::-webkit-scrollbar {
    width: 4px;
  }
  &::-webkit-scrollbar-track {
    background: rgba(255, 255, 255, 0.1);
    border-radius: 2px;
  }
  &::-webkit-scrollbar-thumb {
    background: rgba(50, 197, 255, 0.6);
    border-radius: 2px;
    &:hover {
      background: rgba(50, 197, 255, 0.8);
    }
  }
  .tooltip-item {
    padding: 8px 0;
    display: flex;
    align-items: center;
    min-width: 200px;
    &:last-child {
      border-bottom: none;
    }
    .tooltip-label {
      font-weight: 500;
      font-size: 12px;
      color: #32c5ff;
      margin-bottom: 4px;
    }
    .tooltip-value {
      font-weight: 400;
      font-size: 13px;
      color: #ffffff;
      line-height: 1.5;
      word-break: break-all;
    }
  }
}
</style>