hejianhao
2026-01-23 6dce1b10c1249593df40759b3855cb990050adc3
feat(home): 添加项目地图悬浮窗并优化滚动逻辑

- 新增项目详情悬浮窗组件,支持鼠标悬停显示项目详细信息
- 修复 getprojectTooltip API 调用参数传递问题
- 优化滚动数据初始化逻辑,避免空数据时触发滚动
- 添加缺失的图片资源(s4.png、f4.png、z4.png、znull.png)
- 更新搜索按钮点击事件,支持项目定位和悬浮窗显示
- 调整开发环境 API 基础地址配置
4个文件已添加
3个文件已修改
424 ■■■■■ 已修改文件
screen/.env.development 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/assets/img/home/f4.png 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/assets/img/home/s4.png 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/assets/img/home/z4.png 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/assets/img/home/znull.png 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/home/index.vue 415 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/home/service.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/.env.development
@@ -1,3 +1,3 @@
VUE_APP_BASE_URL=http://192.168.110.111:9908
VUE_APP_BASE_URL=http://192.168.110.188:9908
# VUE_APP_BASE_URL=http://221.182.45.100:8180
VUE_APP_IMAGE_BASE_URL=http://221.182.45.100:11225/images/files/
screen/src/assets/img/home/f4.png
screen/src/assets/img/home/s4.png
screen/src/assets/img/home/z4.png
screen/src/assets/img/home/znull.png
screen/src/view/home/index.vue
@@ -107,7 +107,7 @@
          />
          <img
            v-else
            src="../../assets/img/home/assets.png"
            src="../../assets/img/home/znull.png"
            class="w--18 h--16 shrink0"
          />
          <div :class="!categoryFilter.assets && 'gray-text'">资产</div>
@@ -201,7 +201,7 @@
            class="el-icon-circle-close c-fff cursor-pointer"
            v-show="projectId"
          />
          <div class="btn">搜索</div>
          <div class="btn" @click="handleSearch">搜索</div>
        </div>
      </div>
      <div v-if="permissionClient.includes('2')" class="mt--33 relative">
@@ -578,6 +578,112 @@
        </TitleBox>
      </div>
    </div>
    <!-- 自定义悬浮窗 -->
    <div
      v-if="tooltipVisible"
      class="map-tooltip-wrapper"
      :style="{ left: tooltipPos.left + 'px', top: tooltipPos.top + 'px' }"
    >
      <div class="custom-tooltip">
        <div v-if="tooltipLoading" class="loading">
          <i class="el-icon-loading"></i> 数据加载中...
        </div>
        <div v-else-if="tooltipError" class="error">数据加载失败</div>
        <div v-else>
          <div class="title">{{ tooltipData.name || "-" }}</div>
          <div class="row">
            <span class="label">项目地址:</span>
            <span class="value">{{ tooltipData.address || "-" }}</span>
          </div>
          <div class="flex gap--20">
            <div class="row">
              <span class="label">施工单位:</span>
              <span class="value">
                {{ tooltipData.constructionCompanyInfo.name || "-" }}
              </span>
            </div>
            <div class="row">
              <span class="label">项目经理:</span>
              <span class="value">
                {{ tooltipData.constructionCompanyInfo.leader || "-" }}
              </span>
            </div>
          </div>
          <div class="flex gap--20">
            <div class="row">
              <span class="label">监理单位:</span>
              <span class="value">
                {{ tooltipData.supervisorCompanyInfo.name || "-" }}
              </span>
            </div>
            <div class="row">
              <span class="label">总监:</span>
              <span class="value">
                {{ tooltipData.supervisorCompanyInfo.leader || "-" }}
              </span>
            </div>
          </div>
          <div class="flex gap--20">
            <div class="row">
              <span class="label">建设单位:</span>
              <span class="value">
                {{ tooltipData.developmentCompanyInfo.name || "-" }}
              </span>
            </div>
            <div class="row">
              <span class="label">项目负责人:</span>
              <span class="value">
                {{ tooltipData.developmentCompanyInfo.leader || "-" }}
              </span>
            </div>
          </div>
          <div class="flex gap--20">
            <div class="row">
              <span class="label">建设单位:</span>
              <span class="value">
                {{ tooltipData.developmentCompanyInfo.name || "-" }}
              </span>
            </div>
            <div class="row">
              <span class="label">项目负责人:</span>
              <span class="value">
                {{ tooltipData.developmentCompanyInfo.leader || "-" }}
              </span>
            </div>
          </div>
          <div
            class="flex gap--20"
            v-for="(item, index) in tooltipData.properties"
            :key="index"
          >
            <div class="row">
              <span class="label">设备租赁单位:</span>
              <span class="value">
                {{ JSON.parse(item)["工程起重机械租赁单位"] || "-" }}
              </span>
            </div>
            <div class="row">
              <span class="label">联系人:</span>
              <span class="value">
                {{ JSON.parse(item)["联系电话"] || "-" }}
              </span>
            </div>
          </div>
          <div
            class="flex flex-warp gap--20"
            v-for="(item, index) in tooltipData.deviceCountVos"
            :key="index"
          >
            <div class="row">
              <span class="label">{{ item.name }}:</span>
              <span class="value">
                {{ item.count || 0 }}
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
@@ -593,6 +699,9 @@
import z1 from "../../assets/img/home/assets1.png";
import z2 from "../../assets/img/home/assets2.png";
import z3 from "../../assets/img/home/assets3.png";
import s4 from "../../assets/img/home/s4.png";
import f4 from "../../assets/img/home/f4.png";
import z4 from "../../assets/img/home/z4.png";
import {
  getStatistics,
  getStoryList,
@@ -638,6 +747,11 @@
      workListData: [],
      VUE_APP_IMAGE_BASE_URL: process.env.VUE_APP_IMAGE_BASE_URL,
      projectList: [],
      tooltipVisible: false,
      tooltipPos: { left: 0, top: 0 },
      tooltipData: {},
      tooltipLoading: false,
      tooltipError: false,
    };
  },
  watch: {
@@ -680,7 +794,6 @@
  mounted() {
    this.$nextTick(() => {
      this.scrollContainer = this.$refs.scrollContainer;
      this.initScrollData();
    });
  },
  beforeDestroy() {
@@ -718,6 +831,9 @@
        time: time,
      }).then((res) => {
        this.scrollData = res;
        if (this.screenData.length > 4) {
          this.initScrollData();
        }
      });
    },
    getScreenData() {
@@ -778,83 +894,79 @@
    handleClick() {
      this.$refs.datePicker.focus();
    },
    handleSearch() {
      if (!this.projectId) {
        return;
      }
      const target = this.projectList.find(
        (item) => item.id === this.projectId
      );
      if (!target) return;
      let position = null;
      if (target.addressInfo) {
        try {
          const info =
            typeof target.addressInfo === "string"
              ? JSON.parse(target.addressInfo)
              : target.addressInfo;
          if (info.gps && info.gps.length) {
            position = [Number(info.gps[0]), Number(info.gps[1])];
          }
        } catch (e) {
          console.error("解析项目地址信息失败", e);
        }
      }
      if (position) {
        // 定位地图
        this.mapView.setZoomAndCenter(15, position);
        // 显示悬浮窗
        this.showTooltip(null, {
          id: target.id,
          position: position,
        });
      } else {
        this.$message.warning("该项目暂无位置信息");
      }
    },
    // 初始化滚动数据
    initScrollData() {
      // 创建示例数据,模拟真实的事故简讯数据
      const sampleData = [
        {
          id: 1,
          type: "塔吊倒塌",
          title: "秦皇岛市钢铁厂发生塔吊倒塌事故",
          location: "河北省秦皇岛市昌黎县",
          date: "2025年11月10日",
          casualties: "造成 1 人死亡",
        },
        {
          id: 2,
          type: "脚手架坍塌",
          title: "广州市某工地脚手架坍塌事故",
          location: "广东省广州市白云区",
          date: "2025年11月9日",
          casualties: "造成 2 人受伤",
        },
        {
          id: 3,
          type: "高处坠落",
          title: "深圳市某建筑工地工人高处坠落",
          location: "广东省深圳市南山区",
          date: "2025年11月8日",
          casualties: "造成 1 人死亡,1 人重伤",
        },
        {
          id: 4,
          type: "物体打击",
          title: "成都市某工地发生物体打击事故",
          location: "四川省成都市武侯区",
          date: "2025年11月7日",
          casualties: "造成 1 人轻伤",
        },
        {
          id: 5,
          type: "触电事故",
          title: "杭州市某工地发生触电事故",
          location: "浙江省杭州市西湖区",
          date: "2025年11月6日",
          casualties: "造成 1 人死亡",
        },
        {
          id: 6,
          type: "机械伤害",
          title: "南京市某工地发生机械伤害事故",
          location: "江苏省南京市江宁区",
          date: "2025年11月5日",
          casualties: "造成 1 人重伤",
        },
        {
          id: 7,
          type: "坍塌事故",
          title: "武汉市某工地发生局部坍塌事故",
          location: "湖北省武汉市汉阳区",
          date: "2025年11月4日",
          casualties: "造成 1 人死亡,2 人受伤",
        },
      ];
      this.stopAutoScroll();
      if (!this.scrollData || this.scrollData.length === 0) return;
      // 为了实现无缝滚动,复制2份数据
      this.scrollData = [...sampleData, ...sampleData];
      this.scrollData = [...this.scrollData, ...this.scrollData];
      // 开始滚动
      this.startAutoScroll();
      this.$nextTick(() => {
        this.startAutoScroll();
      });
    },
    // 开始自动滚动
    startAutoScroll() {
      this.stopAutoScroll();
      if (!this.scrollContainer) return;
      if (!this.scrollContainer || !this.scrollData.length) return;
      const originalDataLength = this.scrollData.length / 2;
      const itemHeight = 102; // 每个项目高度
      // 动态计算高度
      let firstHalfHeight = 0;
      const wrapper = this.scrollContainer.children[0];
      if (wrapper && wrapper.children.length) {
        for (let i = 0; i < originalDataLength; i++) {
          if (wrapper.children[i]) {
            firstHalfHeight += wrapper.children[i].offsetHeight;
          }
        }
      }
      // 兜底
      if (firstHalfHeight === 0) firstHalfHeight = originalDataLength * 102;
      // 判断是否需要跳转的标志
      let shouldJump = false;
@@ -863,9 +975,9 @@
        let currentScrollTop = this.scrollContainer.scrollTop;
        // 当滚动到第二份的末尾时
        if (currentScrollTop >= originalDataLength * itemHeight) {
        if (currentScrollTop >= firstHalfHeight) {
          // 计算当前滚动位置相对于第二份数据开始的偏移量
          const offset = currentScrollTop - originalDataLength * itemHeight;
          const offset = currentScrollTop - firstHalfHeight;
          // 直接跳到第一份数据相同偏移量的位置
          this.scrollContainer.scrollTop = offset;
          // 设置标志表示刚刚进行过跳转
@@ -927,6 +1039,10 @@
            center: [104.065617, 30.656952], // 初始化地图中心点
            mapStyle: "amap://styles/darkblue",
          });
          // this.infoWindow = new AMap.InfoWindow({
          //   isCustom: true,
          //   offset: new AMap.Pixel(0, -30),
          // });
          this.initCategoryMarkers(AMap);
        })
        .catch((e) => {
@@ -938,10 +1054,16 @@
        return;
      }
      const points = this.screenData.projectMapPoints.map((item) => {
        let category;
        if (this.activeTab === 2) {
          category = "assets";
        } else {
          category = ["house", "municipal", "assets"][item.projectType];
        }
        return {
          ...item,
          position: [Number(item.longitude), Number(item.latitude)],
          category: ["house", "municipal", "assets"][item.projectType],
          category: category,
          level: item.category ? item.category.splice(0, 1) : "",
        };
      });
@@ -962,15 +1084,15 @@
      }
      let image = null;
      let level = item.level;
      if (!level) {
        image = z3;
      } else if (item.category === "municipal") {
      if (item.category === "municipal") {
        if (item.level === "A") {
          image = s1;
        } else if (item.level === "B") {
          image = s2;
        } else if (item.level === "C") {
          image = s3;
        } else {
          image = s4;
        }
      } else if (item.category === "house") {
        if (item.level === "A") {
@@ -979,6 +1101,8 @@
          image = f2;
        } else if (item.level === "C") {
          image = f3;
        } else {
          image = f4;
        }
      } else if (item.category === "assets") {
        if (item.level === "A") {
@@ -987,6 +1111,8 @@
          image = z2;
        } else if (item.level === "C") {
          image = z3;
        } else {
          image = z4;
        }
      }
      if (!image) {
@@ -1006,8 +1132,11 @@
          level: level,
        },
      });
      marker.on("click", (item) => {
        console.log(item);
      marker.on("mouseover", (e) => {
        this.showTooltip(e, item);
      });
      marker.on("mouseout", () => {
        this.hideTooltip();
      });
      return marker;
    },
@@ -1045,6 +1174,62 @@
      }
      this.levelFilter[level] = !this.levelFilter[level];
      this.updateMarkerVisibility();
    },
    showTooltip(e, item) {
      if (!this.mapView) return;
      const pixel = this.mapView.lngLatToContainer(item.position);
      this.tooltipPos = { left: pixel.x, top: pixel.y };
      this.tooltipVisible = true;
      this.tooltipLoading = true;
      this.tooltipError = false;
      this.tooltipData = {};
      getprojectTooltip(item.projectId)
        .then((res) => {
          let address = "-";
          let constructionCompanyInfo = "-";
          let supervisorCompanyInfo = "-";
          let developmentCompanyInfo = "-";
          try {
            if (res.addressInfo) {
              const addressInfo = JSON.parse(res.addressInfo);
              address =
                (addressInfo.province || "") +
                (addressInfo.city || "") +
                (addressInfo.district || "") +
                (addressInfo.detail || "");
            }
            if (res.constructionCompanyInfo) {
              constructionCompanyInfo = JSON.parse(res.constructionCompanyInfo);
            }
            if (res.supervisorCompanyInfo) {
              supervisorCompanyInfo = JSON.parse(res.supervisorCompanyInfo);
            }
            if (res.developmentCompanyInfo) {
              developmentCompanyInfo = JSON.parse(res.developmentCompanyInfo);
            }
          } catch (error) {
            console.error("Parse error", error);
          }
          this.tooltipData = {
            ...res,
            address,
            constructionCompanyInfo,
            supervisorCompanyInfo,
            developmentCompanyInfo,
          };
          this.tooltipLoading = false;
        })
        .catch((err) => {
          this.tooltipLoading = false;
          this.tooltipError = true;
        });
    },
    hideTooltip() {
      this.tooltipVisible = false;
    },
  },
};
@@ -1428,3 +1613,85 @@
  }
}
</style>
<style lang="less">
.map-tooltip-wrapper {
  position: absolute;
  z-index: 9999;
  pointer-events: none;
  transform: translate(-50%, -50%);
  margin-top: -40px;
}
.custom-tooltip {
  position: relative;
  background: rgba(0, 22, 53, 0.9);
  border: 1px solid #204fb3;
  border-radius: 4px;
  padding: 10px;
  color: #fff;
  min-width: 250px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
  &::after {
    content: "";
    position: absolute;
    bottom: -6px;
    left: 50%;
    transform: translateX(-50%);
    border-left: 6px solid transparent;
    border-right: 6px solid transparent;
    border-top: 6px solid #204fb3;
  }
  &::before {
    content: "";
    position: absolute;
    bottom: -5px;
    left: 50%;
    transform: translateX(-50%);
    border-left: 6px solid transparent;
    border-right: 6px solid transparent;
    border-top: 6px solid rgba(0, 22, 53, 0.9);
    z-index: 1;
  }
  .title {
    font-size: 16px;
    font-weight: bold;
    margin-bottom: 8px;
    color: #fff;
  }
  .row {
    display: flex;
    font-size: 12px;
    line-height: 1.5;
    margin-bottom: 4px;
    flex-shrink: 0;
    .label {
      color: rgba(255, 255, 255, 0.7);
      min-width: 70px;
      flex-shrink: 0;
    }
    .value {
      color: #fff;
      flex: 1;
    }
  }
  .loading {
    text-align: center;
    padding: 10px;
    color: #00ffd2;
  }
  .error {
    color: #ff4d4f;
    text-align: center;
    padding: 10px;
  }
}
</style>
screen/src/view/home/service.js
@@ -28,9 +28,8 @@
  })
}
export const getprojectTooltip = async (params) => {
  return request(`/project/getProjectDetail/{id}`, {
    method: 'get',
    params
export const getprojectTooltip = async (id) => {
  return request(`/project/getProjectDetail/${id}`, {
    method: 'get'
  })
}