ソースを参照

依据华电修改首页布局排版

liujiejie 2 週間 前
コミット
49d8ad91d6

+ 26 - 26
.env.hd

@@ -5,32 +5,32 @@ VUE_APP_Helath='hd'
 VUE_APP_PROJECT='hd'
 #外网
 # #http://106.120.102.238:18997/
-# # VUE_APP_MAPVIEW="http://106.120.102.238:18000/tiles/{z}/{x}/{y}.png" # 地图废除
-# VUE_APP_MAPVIEW=/tiles/{z}/{x}/{y}.png
-# VUE_APP_UPLOAD="http://106.120.102.238:16700/energy-manage-service/api/check/upload"
-# VUE_APP_APIPROXY='http://106.120.102.238:18997'
-# VUE_APP_MAP=http://106.120.102.238:18080
-# VUE_APP_WZLAPIPROXY='http://106.120.102.238:18080/WindTransDev'
-# VUE_APP_ETLAPIPROXY='http://106.120.102.238:18080/WindTransDev'
-# VUE_APP_AnalysisMultiAPIPROXY='http://106.120.102.238:28999/AnalysisMulti'
-# #自定义算法文佳 目前无法使用,可能是服务未启动
-# VUE_APP_sAlgorithmAPIPROXY='http://106.120.102.238:58880'
-# VUE_APP_databaseApiAPIPROXY='http://106.120.102.238:58880'
-# # #暂时不知下载报告内网dev 环境地址
-# VUE_APP_downLoadChartAPIPROXY='http://106.120.102.238:58880'
+# VUE_APP_MAPVIEW="http://106.120.102.238:18000/tiles/{z}/{x}/{y}.png" # 地图废除
+VUE_APP_MAPVIEW=/tiles/{z}/{x}/{y}.png
+VUE_APP_UPLOAD="http://106.120.102.238:16700/energy-manage-service/api/check/upload"
+VUE_APP_APIPROXY='http://106.120.102.238:16700'
+VUE_APP_MAP=http://106.120.102.238:18080
+VUE_APP_WZLAPIPROXY='http://106.120.102.238:18080/WindTransDev'
+VUE_APP_ETLAPIPROXY='http://106.120.102.238:18080/WindTransDev'
+VUE_APP_AnalysisMultiAPIPROXY='http://106.120.102.238:28999/AnalysisMulti'
+#自定义算法文佳 目前无法使用,可能是服务未启动
+VUE_APP_sAlgorithmAPIPROXY='http://106.120.102.238:58880'
+VUE_APP_databaseApiAPIPROXY='http://106.120.102.238:58880'
+# #暂时不知下载报告内网dev 环境地址
+VUE_APP_downLoadChartAPIPROXY='http://106.120.102.238:58880'
 
 #内网
 # VUE_APP_UPLOAD="http://192.168.0.1/energy-manage-service/api/check/upload"
-VUE_APP_UPLOAD="http://192.168.50.235/energy-manage-service/api/check/upload"
-VUE_APP_MAPVIEW=/tiles/{z}/{x}/{y}.png
-VUE_APP_MAP=http://192.168.50.235
-VUE_APP_APIPROXY='http://192.168.50.235:16200'
-VUE_APP_WZLAPIPROXY='http://192.168.50.241:9001'
-VUE_APP_ETLAPIPROXY='http://192.168.50.241:9001'
-# #自定义算法文佳 目前无法使用,可能是服务未启动
-VUE_APP_sAlgorithmAPIPROXY='http://192.168.50.235:8998/AnalysisMulti'
-VUE_APP_databaseApiAPIPROXY='http://192.168.50.234:3002'
-#暂时不知健康评估内网dev 环境地址
-VUE_APP_AnalysisMultiAPIPROXY='http://192.168.50.235:8998/AnalysisMulti'
-#暂时不知下载报告内网dev 环境地址
-VUE_APP_downLoadChartAPIPROXY='http://192.168.50.235:8998/downLoadChart'
+# VUE_APP_UPLOAD="http://192.168.50.235/energy-manage-service/api/check/upload"
+# VUE_APP_MAPVIEW=/tiles/{z}/{x}/{y}.png
+# VUE_APP_MAP=http://192.168.50.235
+# VUE_APP_APIPROXY='http://192.168.50.235:16200'
+# VUE_APP_WZLAPIPROXY='http://192.168.50.241:9001'
+# VUE_APP_ETLAPIPROXY='http://192.168.50.241:9001'
+# # #自定义算法文佳 目前无法使用,可能是服务未启动
+# VUE_APP_sAlgorithmAPIPROXY='http://192.168.50.235:8998/AnalysisMulti'
+# VUE_APP_databaseApiAPIPROXY='http://192.168.50.234:3002'
+# #暂时不知健康评估内网dev 环境地址
+# VUE_APP_AnalysisMultiAPIPROXY='http://192.168.50.235:8998/AnalysisMulti'
+# #暂时不知下载报告内网dev 环境地址
+# VUE_APP_downLoadChartAPIPROXY='http://192.168.50.235:8998/downLoadChart'

+ 0 - 135
downLoadServer/src/server/utils/chartsCom/BarChart.js

@@ -1,135 +0,0 @@
-/*
- * 柱状图(服务化版本)
- */
-import { renderChart } from "../chartService/index.js";
-
-export const generateBarChart = async (data, bucketName, objectName) => {
-  /**
-   * =========================
-   * 1. 数据校验
-   * =========================
-   */
-  const chartDataset = data?.data?.[0];
-
-  if (!chartDataset) {
-    throw new Error("Invalid chart data");
-  }
-
-  const xData = chartDataset.xData || [];
-  const yData = chartDataset.yData || [];
-
-  /**
-   * =========================
-   * 2. traces
-   * =========================
-   */
-  const traces = [
-    {
-      x: xData,
-      y: yData,
-      type: "bar",
-      marker: {
-        color: "#588CF0",
-      },
-      name: chartDataset.title || "数据",
-      hovertemplate: `${data.xaixs}: %{x}<br>${data.yaixs}: %{y}<extra></extra>`,
-    },
-  ];
-
-  /**
-   * =========================
-   * 3. layout
-   * =========================
-   */
-  const minY = Math.min(...yData, -5);
-  const maxY = Math.max(...yData, 5);
-
-  const layout = {
-    title: {
-      text: chartDataset.title,
-      font: { size: 16 },
-    },
-    xaxis: {
-      title: data.xaixs || "X轴",
-      type:
-        data.xaixs === "机组" || data.xaixs === "机组名称"
-          ? "category"
-          : undefined,
-      gridcolor: "rgb(255,255,255)",
-      tickcolor: "rgb(255,255,255)",
-      backgroundcolor: "#e5ecf6",
-      showbackground: true,
-      showline: true, // ✅ 显示 X 轴轴线
-      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
-      zeroline: false,
-    },
-    yaxis: {
-      title: data.yaixs || "Y轴",
-      gridcolor: "rgb(255,255,255)",
-      tickcolor: "rgb(255,255,255)",
-      backgroundcolor: "#e5ecf6",
-      showbackground: true,
-      showline: true, // ✅ 显示 X 轴轴线
-      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
-      zeroline: false,
-      range:
-        chartDataset.title === "温度偏差" ? [minY - 1, maxY + 1] : undefined,
-    },
-    margin: { l: 50, r: 50, t: 50, b: 50 },
-    showlegend: true,
-  };
-
-  /**
-   * =========================
-   * 4. 温度偏差(上下限线)
-   * =========================
-   */
-  if (chartDataset.title === "温度偏差") {
-    layout.shapes = [
-      {
-        type: "line",
-        xref: "paper",
-        yref: "y",
-        x0: 0,
-        x1: 1,
-        y0: 5,
-        y1: 5,
-        line: { color: "red", width: 2, dash: "dash" },
-      },
-      {
-        type: "line",
-        xref: "paper",
-        yref: "y",
-        x0: 0,
-        x1: 1,
-        y0: -5,
-        y1: -5,
-        line: { color: "red", width: 2, dash: "dash" },
-      },
-    ];
-
-    layout.hovermode = "x unified";
-  }
-
-  /**
-   * =========================
-   * 5. 机组类目处理
-   * =========================
-   */
-  if (data.xaixs === "机组" || data.xaixs === "机组名称") {
-    layout.xaxis.tickvals = xData;
-    layout.xaxis.ticktext = xData;
-  }
-
-  /**
-   * =========================
-   * 6. 调用统一渲染
-   * =========================
-   */
-  return await renderChart({
-    traces,
-    layout,
-    bucketName,
-    objectName,
-  });
-};

+ 0 - 115
downLoadServer/src/server/utils/chartsCom/ColorbarInitTwoDmarkersChart.js

@@ -1,115 +0,0 @@
-/*
- * 2D 散点图(带 colorbar)- 终极稳定版
- */
-import { renderChart } from "../chartService/index.js";
-import { colorSchemes } from "../colors.js";
-
-export const generateColorbarInitTwoDmarkersChart = async (
-  data,
-  bucketName,
-  objectName,
-) => {
-  const colorsBar = colorSchemes[4].colors;
-
-  const scatterData = data.data?.[0];
-  if (!scatterData) throw new Error("scatterData missing");
-
-  // ✅ 构造颜色刻度(更合理)
-  // const colorStops = [colorsBar[0], colorsBar[4], colorsBar[8], colorsBar[12]];
-
-  // const colorscale = colorStops.map((color, index) => [
-  //   index / (colorStops.length - 1),
-  //   color,
-  // ]);
-  const colorscale = [
-    [0, "#e6f4f5"], // 0%
-    [0.1, "#74ABF7"], // 1%
-    // [0.1, "#3E7AF6"], // 1%
-    [0.2, "#0000E4"], // 30%
-    [1, "#020085"], // 100%
-  ];
-  // ✅ colorbar 数据
-  const hasColorbar =
-    scatterData.colorbar &&
-    scatterData.colorbar.length === scatterData.xData.length;
-
-  const colorValues = hasColorbar ? scatterData.colorbar : scatterData.yData;
-
-  // ✅ trace
-  const trace = {
-    x: scatterData.xData,
-    y: scatterData.yData,
-    mode: "markers",
-    type: "scattergl",
-    text: scatterData.engineName,
-
-    marker: {
-      color: colorValues,
-      colorscale,
-      size: 4,
-      line: {
-        color: "#fff",
-        width: 0.3,
-      },
-
-      // ✅ colorbar 显示
-      colorbar: hasColorbar
-        ? { title: { text: scatterData.colorbartitle || "" } }
-        : undefined,
-    },
-  };
-
-  // ✅ 特殊范围控制(修复版)
-  if (scatterData.colorbartitle === "密度") {
-    trace.marker.cmin = 0;
-    trace.marker.cmax = Math.max(...colorValues);
-  } else if (scatterData.colorbartitle === "百分比(%)") {
-    trace.marker.cmin = 0;
-    trace.marker.cmax = 100;
-  }
-
-  // ✅ layout
-  const layout = {
-    title: {
-      text: scatterData.title,
-      font: { size: 16, weight: "bold" },
-    },
-    xaxis: {
-      title: data.xaixs,
-      gridcolor: "rgb(255,255,255)",
-      tickcolor: "rgb(255,255,255)",
-      backgroundcolor: "#e5ecf6",
-      showbackground: true,
-      showline: true, // ✅ 显示 X 轴轴线
-      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
-      zeroline: false,
-    },
-    yaxis: {
-      title: data.yaixs,
-      gridcolor: "rgb(255,255,255)",
-      tickcolor: "rgb(255,255,255)",
-      backgroundcolor: "#e5ecf6",
-      showbackground: true,
-      showline: true, // ✅ 显示 X 轴轴线
-      linecolor: "#ffffff", // ✅ X 轴轴线颜色设为白色
-      zeroline: false,
-    },
-    showlegend: false,
-    plot_bgcolor: "#e5ecf6",
-    paper_bgcolor: "#e5ecf6",
-  };
-
-  // ✅ 时间轴处理
-  if (data.xaixs === "时间") {
-    layout.xaxis.type = "date";
-    layout.xaxis.tickformat = "%Y-%m-%d";
-  }
-
-  // ✅ 🚀 核心:统一走 chartService
-  return await renderChart({
-    traces: [trace],
-    layout,
-    bucketName,
-    objectName,
-  });
-};

+ 1284 - 0
src/components/map/TechBlueOfflineProvinceGeoMap.vue

@@ -0,0 +1,1284 @@
+<template>
+  <div class="tech-blue-province-map">
+    <div class="tech-blue-province-map__bg" aria-hidden="true"></div>
+    <div class="tech-blue-province-map__grid" aria-hidden="true"></div>
+    <div class="tech-blue-province-map__stage">
+      <div class="tech-blue-province-map__shadow" aria-hidden="true"></div>
+      <div ref="mapRoot" class="tech-blue-province-map__canvas"></div>
+    </div>
+    <div class="tech-blue-province-map__south-sea" aria-hidden="true">
+      <span class="tech-blue-province-map__south-sea-title">南海诸岛</span>
+      <i
+        v-for="n in 7"
+        :key="'dot-' + n"
+        class="tech-blue-province-map__south-sea-dot"
+        :style="southSeaDotStyle(n)"
+      />
+    </div>
+
+    <div
+      v-if="hoverInfo"
+      class="tech-blue-province-map__hover"
+      :style="hoverStyle"
+      @mouseenter="hovering = true"
+      @mouseleave="onHoverLeave"
+    >
+      <h3>{{ hoverInfo.fieldName || hoverInfo.companyName || "风场信息" }}</h3>
+      <div class="tech-blue-province-map__hover-body">
+        <p>
+          <span>状态</span>
+          <span>{{
+            hoverInfo.analysisState === 30 || hoverInfo.val === 30 || hoverInfo.val === "30"
+              ? "已完成分析"
+              : hoverInfo.analysisState === 0 || hoverInfo.val === 0 || hoverInfo.val === "0"
+                ? "分析中"
+                : "未完成分析"
+          }}</span>
+        </p>
+        <p>
+          <span>风场编号</span>
+          <span>{{ hoverInfo.codeNumber || "-" }}</span>
+        </p>
+        <p>
+          <span>风机数量</span>
+          <span>{{ newWind.engineTotalCount || 0 }} 台</span>
+        </p>
+        <p>
+          <span>总容量</span>
+          <span>{{ newWind.ratedCapacityNumber || 0 }}/KW</span>
+        </p>
+        <p>
+          <span>经度</span>
+          <span>{{ hoverInfo.longitude || hoverInfo.point?.[0] || "-" }}°</span>
+        </p>
+        <p>
+          <span>纬度</span>
+          <span>{{ hoverInfo.latitude || hoverInfo.point?.[1] || "-" }}°</span>
+        </p>
+        <p>
+          <span>分析模型数</span>
+          <span>{{ hoverInfo.analysisTypeCount || 0 }} 个</span>
+        </p>
+        <p>
+          <span>分析开始时间</span>
+          <span>{{
+            newWind.analysisStartTime
+              ? $formatDateTWO(newWind.analysisStartTime)
+              : "暂无数据"
+          }}</span>
+        </p>
+        <p>
+          <span>分析结束时间</span>
+          <span>{{
+            newWind.analysisEndTime
+              ? $formatDateTWO(newWind.analysisEndTime)
+              : "暂无数据"
+          }}</span>
+        </p>
+        <p>
+          <span>数据开始时间</span>
+          <span>{{
+            newWind.dataStartTime
+              ? $formatDateTWO(newWind.dataStartTime)
+              : "暂无数据"
+          }}</span>
+        </p>
+        <p>
+          <span>数据结束时间</span>
+          <span>{{
+            newWind.dataEndTime
+              ? $formatDateTWO(newWind.dataEndTime)
+              : "暂无数据"
+          }}</span>
+        </p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Map, View, Feature } from "ol";
+import GeoJSON from "ol/format/GeoJSON";
+import { defaults as defaultControls } from "ol/control";
+import { LineString, Point } from "ol/geom";
+import { Vector as VectorLayer } from "ol/layer";
+import { Vector as VectorSource } from "ol/source";
+import {
+  Fill,
+  Icon,
+  Stroke,
+  Style,
+  Text,
+} from "ol/style";
+import { ChinaData, ProvinceData } from "china-map-geojson";
+import { queryFloatingWindowInfo } from "@/api/ledger.js";
+import icon01 from "@/assets/img/iconFC.png";
+import icon02 from "@/assets/img/iconFC.png";
+import icon03 from "@/assets/img/iconFC.png";
+import icon04 from "@/assets/img/iconFJ.png";
+import icon05 from "@/assets/img/icon05.png";
+import icon06 from "@/assets/img/iconFJ.png";
+import defaultIcon from "@/assets/img/iconFJ.png";
+
+const DEFAULT_CENTER = [104.5, 35.5];
+const DEFAULT_ZOOM = 5.15;
+
+/** 分层挤出:仅向下偏移,避免横向错位看起来发斜 */
+const EXTRUSION_STEPS = [
+  {
+    dx: 0,
+    dy: -0.55,
+    fill: "rgba(1, 8, 26, 0.99)",
+    stroke: "rgba(8, 55, 130, 0.42)",
+    width: 1.35,
+  },
+  {
+    dx: 0,
+    dy: -0.42,
+    fill: "rgba(3, 14, 40, 0.98)",
+    stroke: "rgba(14, 80, 160, 0.48)",
+    width: 1.25,
+  },
+  {
+    dx: 0,
+    dy: -0.3,
+    fill: "rgba(6, 22, 55, 0.96)",
+    stroke: "rgba(22, 115, 195, 0.55)",
+    width: 1.15,
+  },
+  {
+    dx: 0,
+    dy: -0.18,
+    fill: "rgba(10, 34, 82, 0.94)",
+    stroke: "rgba(40, 150, 230, 0.62)",
+    width: 1.05,
+  },
+  {
+    dx: 0,
+    dy: -0.08,
+    fill: "rgba(26, 72, 155, 0.86)",
+    stroke: "rgba(110, 220, 255, 0.8)",
+    width: 0.85,
+  },
+];
+
+const CN_TO_PROVINCE_KEY = {
+  安徽: "Anhui",
+  澳门: "Aomen",
+  北京: "Beijing",
+  重庆: "Chongqing",
+  福建: "Fujian",
+  贵州: "Guizhou",
+  广西: "Guangxi",
+  广东: "Guangdong",
+  甘肃: "Gansu",
+  湖南: "Hunan",
+  湖北: "Hubei",
+  黑龙江: "Heilongjiang",
+  河北: "Hebei",
+  河南: "Henan",
+  海南: "Hainan",
+  江西: "Jiangxi",
+  江苏: "Jiangsu",
+  吉林: "Jilin",
+  辽宁: "Liaoning",
+  宁夏: "Ningxia",
+  内蒙古: "Neimenggu",
+  青海: "Qinghai",
+  四川: "Sichuan",
+  上海: "Shanghai",
+  山东: "Shandong",
+  山西: "Shanxi_1",
+  陕西: "Shanxi_3",
+  天津: "Tianjin",
+  台湾: "Taiwan",
+  新疆: "Xinjiang",
+  香港: "Xianggang",
+  西藏: "Xizang",
+  云南: "Yunnan",
+  浙江: "Zhejiang",
+};
+
+const RIVER_LINES = [
+  [
+    [96.5, 35.2],
+    [101.8, 34.6],
+    [106.4, 33.9],
+    [110.8, 31.6],
+    [114.9, 30.6],
+    [118.5, 31.0],
+    [121.7, 31.2],
+  ],
+  [
+    [103.6, 36.2],
+    [108.1, 38.2],
+    [111.5, 38.7],
+    [115.0, 37.9],
+    [118.4, 37.6],
+  ],
+];
+
+const RAIL_LINES = [
+  [
+    [87.6, 43.8],
+    [96.8, 40.2],
+    [103.8, 36.2],
+    [108.9, 34.3],
+    [113.6, 34.7],
+    [116.4, 39.9],
+    [121.5, 31.2],
+  ],
+  [
+    [111.7, 40.8],
+    [112.5, 37.9],
+    [113.3, 34.8],
+    [114.3, 30.6],
+    [113.3, 23.1],
+  ],
+];
+
+export default {
+  name: "TechBlueOfflineProvinceGeoMap",
+  props: {
+    markers: {
+      type: Array,
+      default: () => [],
+    },
+    maplist: {
+      type: [Array, Object],
+      default: () => [],
+    },
+    activeProvinceNames: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  data() {
+    return {
+      map: null,
+      markerSource: null,
+      markerLayer: null,
+      provinceLayer: null,
+      provinceGlowLayer: null,
+      countyLayer: null,
+      cityLabelLayer: null,
+      cityLabelSource: null,
+      provinceFeatures: [],
+      markerFeatures: [],
+      focusedProvinceName: "",
+      focusedCityName: "",
+      currentZoom: DEFAULT_ZOOM,
+      hoverInfo: null,
+      hovering: false,
+      hideTimer: null,
+      hoverStyle: {
+        position: "absolute",
+        left: "0px",
+        top: "0px",
+      },
+      newWind: {},
+      lastHoveredFeature: null,
+    };
+  },
+  mounted() {
+    this.initMap();
+    this.setMarkers(this.markers);
+    this.$nextTick(() => {
+      if (this.map) {
+        this.map.updateSize();
+        this.fitChina(false);
+      }
+    });
+    window.addEventListener("resize", this.handleResize);
+  },
+  beforeDestroy() {
+    window.removeEventListener("resize", this.handleResize);
+    if (this.map) {
+      this.map.setTarget(null);
+      this.map = null;
+    }
+  },
+  watch: {
+    markers: {
+      deep: true,
+      handler(val) {
+        this.setMarkers(val || []);
+      },
+    },
+    activeProvinceNames: {
+      deep: true,
+      handler() {
+        this.refreshProvinceStyle();
+      },
+    },
+  },
+  methods: {
+    handleResize() {
+      if (this.map) this.map.updateSize();
+    },
+    southSeaDotStyle(n) {
+      const positions = [
+        [22, 38],
+        [38, 52],
+        [52, 34],
+        [64, 58],
+        [48, 72],
+        [72, 46],
+        [34, 68],
+      ];
+      const [left, top] = positions[n - 1] || [40, 40];
+      return {
+        left: `${left}%`,
+        top: `${top}%`,
+      };
+    },
+    cloneTranslatedFeatures(features, dx, dy) {
+      return features.map((feature) => {
+        const cloned = feature.clone();
+        const geometry = cloned.getGeometry();
+        if (geometry) {
+          geometry.translate(dx, dy);
+        }
+        return cloned;
+      });
+    },
+    createExtrusionLayers(baseFeatures) {
+      return EXTRUSION_STEPS.map((step, index) => {
+        const source = new VectorSource({
+          features: this.cloneTranslatedFeatures(
+            baseFeatures,
+            step.dx,
+            step.dy,
+          ),
+        });
+        return new VectorLayer({
+          source,
+          zIndex: index + 1,
+          style: new Style({
+            fill: new Fill({ color: step.fill }),
+            stroke: new Stroke({
+              color: step.stroke,
+              width: step.width,
+            }),
+          }),
+        });
+      });
+    },
+    initMap() {
+      const format = new GeoJSON();
+      const provinceSource = new VectorSource({
+        features: format.readFeatures(ChinaData, {
+          dataProjection: "EPSG:4326",
+          featureProjection: "EPSG:4326",
+        }),
+      });
+      this.provinceFeatures = provinceSource.getFeatures();
+
+      const glowSource = new VectorSource({
+        features: this.provinceFeatures.map((feature) => feature.clone()),
+      });
+      const countySource = this.createCountySource(format);
+      const decorSource = this.createDecorSource();
+      const extrusionLayers = this.createExtrusionLayers(this.provinceFeatures);
+
+      const markerSource = new VectorSource();
+      this.markerSource = markerSource;
+
+      this.cityLabelSource = new VectorSource();
+      this.cityLabelLayer = new VectorLayer({
+        source: this.cityLabelSource,
+        zIndex: 45,
+        style: (feature) => this.createCityLabelStyle(feature),
+      });
+
+      const seaLayer = new VectorLayer({
+        source: decorSource,
+        zIndex: 0,
+        style: (feature) => this.createDecorStyle(feature),
+      });
+
+      this.provinceGlowLayer = new VectorLayer({
+        source: glowSource,
+        zIndex: 20,
+        style: (feature) => this.createProvinceGlowStyle(feature),
+      });
+
+      this.provinceLayer = new VectorLayer({
+        source: provinceSource,
+        zIndex: 30,
+        style: (feature) => this.createProvinceStyle(feature),
+      });
+
+      this.countyLayer = new VectorLayer({
+        source: countySource,
+        zIndex: 35,
+        style: (feature) => this.createCountyBoundaryStyle(feature),
+      });
+
+      const markerLayer = new VectorLayer({
+        source: markerSource,
+        zIndex: 50,
+        style: (feature) => this.createMarkerStyle(feature),
+      });
+      this.markerLayer = markerLayer;
+
+      this.map = new Map({
+        target: this.$refs.mapRoot,
+        layers: [
+          seaLayer,
+          ...extrusionLayers,
+          this.provinceGlowLayer,
+          this.provinceLayer,
+          this.countyLayer,
+          this.cityLabelLayer,
+          markerLayer,
+        ],
+        view: new View({
+          projection: "EPSG:4326",
+          center: DEFAULT_CENTER,
+          zoom: DEFAULT_ZOOM,
+          minZoom: 3.2,
+          maxZoom: 10,
+          constrainResolution: false,
+        }),
+        controls: defaultControls({
+          zoom: true,
+          attribution: false,
+          rotate: false,
+        }),
+      });
+
+      this.map.getView().on("change:resolution", () => {
+        this.currentZoom = this.map.getView().getZoom() || DEFAULT_ZOOM;
+        this.refreshProvinceStyle();
+        if (this.cityLabelLayer) this.cityLabelLayer.changed();
+      });
+
+      this.initMapClick();
+      this.initPointerMove();
+      this.fitChina(false);
+    },
+    createDecorSource() {
+      const source = new VectorSource();
+      RIVER_LINES.forEach((coords, index) => {
+        source.addFeature(
+          new Feature({
+            geometry: new LineString(coords),
+            decorType: "river",
+            decorIndex: index,
+          }),
+        );
+      });
+      RAIL_LINES.forEach((coords, index) => {
+        source.addFeature(
+          new Feature({
+            geometry: new LineString(coords),
+            decorType: "rail",
+            decorIndex: index,
+          }),
+        );
+      });
+      return source;
+    },
+    createCountySource(format) {
+      const featureCollections = Object.values(ProvinceData || {});
+      const featureList = [];
+      featureCollections.forEach((collection) => {
+        if (!collection) return;
+        const features = format.readFeatures(collection, {
+          dataProjection: "EPSG:4326",
+          featureProjection: "EPSG:4326",
+        });
+        featureList.push(...features);
+      });
+      return new VectorSource({
+        features: featureList,
+      });
+    },
+    createDecorStyle(feature) {
+      const decorType = feature.get("decorType");
+      if (decorType === "river") {
+        return [
+          new Style({
+            stroke: new Stroke({
+              color: "rgba(72, 171, 255, 0.16)",
+              width: 4,
+            }),
+          }),
+          new Style({
+            stroke: new Stroke({
+              color: "rgba(140, 220, 255, 0.42)",
+              width: 1.1,
+            }),
+          }),
+        ];
+      }
+      if (decorType === "rail") {
+        return new Style({
+          stroke: new Stroke({
+            color: "rgba(190, 220, 245, 0.16)",
+            width: 1,
+            lineDash: [4, 5],
+          }),
+        });
+      }
+      return null;
+    },
+    normalizeRegionName(name = "") {
+      return String(name)
+        .replace(/(特别行政区|自治区|壮族|回族|维吾尔|省|市)$/g, "")
+        .trim();
+    },
+    getProvinceName(feature) {
+      return feature?.getProperties?.().name || "";
+    },
+    findProvinceFeature(name) {
+      const target = this.normalizeRegionName(name);
+      if (!target) return null;
+      return (
+        this.provinceFeatures.find((feature) => {
+          const pname = this.normalizeRegionName(this.getProvinceName(feature));
+          return (
+            pname === target || pname.includes(target) || target.includes(pname)
+          );
+        }) || null
+      );
+    },
+    isProvinceActive(feature) {
+      const name = this.getProvinceName(feature);
+      const normalized = this.normalizeRegionName(name);
+      return (this.activeProvinceNames || []).some((item) => {
+        const n = this.normalizeRegionName(item);
+        return n === normalized || name === item;
+      });
+    },
+    isProvinceFocused(feature) {
+      const name = this.getProvinceName(feature);
+      if (!this.focusedProvinceName) return false;
+      return (
+        this.normalizeRegionName(name) ===
+        this.normalizeRegionName(this.focusedProvinceName)
+      );
+    },
+    shouldShowProvinceLabel(feature) {
+      // 默认全国各省都显示轻量省名;深缩放到市级时仅保留当前聚焦省
+      if (this.focusedProvinceName && this.currentZoom >= 6.4) {
+        return this.isProvinceFocused(feature);
+      }
+      return true;
+    },
+    /** MultiPolygon 只取最大面,避免海岛厚描边叠成光斑 */
+    getMainPolygonGeometry(feature) {
+      const geometry = feature.getGeometry();
+      if (!geometry) return null;
+      if (geometry.getType() !== "MultiPolygon") return geometry;
+      let largest = null;
+      let maxArea = 0;
+      geometry.getPolygons().forEach((polygon) => {
+        const area = Math.abs(polygon.getArea());
+        if (area > maxArea) {
+          maxArea = area;
+          largest = polygon;
+        }
+      });
+      return largest || geometry;
+    },
+    createProvinceGlowStyle(feature) {
+      const active =
+        this.isProvinceActive(feature) || this.isProvinceFocused(feature);
+      const mainGeom = this.getMainPolygonGeometry(feature);
+      return [
+        new Style({
+          geometry: mainGeom || undefined,
+          stroke: new Stroke({
+            color: active
+              ? "rgba(255, 180, 60, 0.3)"
+              : "rgba(40, 190, 255, 0.26)",
+            width: active ? 14 : 12,
+          }),
+        }),
+        new Style({
+          geometry: mainGeom || undefined,
+          stroke: new Stroke({
+            color: active
+              ? "rgba(255, 210, 110, 0.48)"
+              : "rgba(90, 225, 255, 0.45)",
+            width: active ? 7 : 6,
+          }),
+        }),
+      ];
+    },
+    /** MultiPolygon 省(如浙江多岛)只取一个标注点,避免每个面各画一次省名 */
+    getProvinceLabelPoint(feature) {
+      const props = feature.getProperties() || {};
+      if (Array.isArray(props.cp) && props.cp.length >= 2) {
+        return new Point(props.cp);
+      }
+      if (props.longitude != null && props.latitude != null) {
+        return new Point([Number(props.longitude), Number(props.latitude)]);
+      }
+      const geometry = feature.getGeometry();
+      if (!geometry) return null;
+      const type = geometry.getType();
+      if (type === "Polygon") {
+        return geometry.getInteriorPoint();
+      }
+      if (type === "MultiPolygon") {
+        const main = this.getMainPolygonGeometry(feature);
+        return main && main.getInteriorPoint ? main.getInteriorPoint() : null;
+      }
+      return null;
+    },
+    createProvinceStyle(feature) {
+      const props = feature.getProperties() || {};
+      const active =
+        this.isProvinceActive(feature) || this.isProvinceFocused(feature);
+      const showLabel = this.shouldShowProvinceLabel(feature);
+      const styles = [
+        new Style({
+          fill: new Fill({
+            color: active
+              ? "rgba(255, 168, 48, 0.58)"
+              : "rgba(52, 120, 220, 0.52)",
+          }),
+          stroke: new Stroke({
+            color: active ? "#ffe5a8" : "rgba(130, 235, 255, 0.98)",
+            width: active ? 2.4 : 1.35,
+          }),
+        }),
+      ];
+      if (showLabel) {
+        const labelPoint = this.getProvinceLabelPoint(feature);
+        styles.push(
+          new Style({
+            geometry: labelPoint || undefined,
+            text: new Text({
+              text: props.name || "",
+              font: active
+                ? "bold 13px Microsoft YaHei"
+                : "12px Microsoft YaHei",
+              overflow: true,
+              fill: new Fill({
+                color: active
+                  ? "rgba(255, 236, 190, 0.98)"
+                  : "rgba(214, 246, 255, 0.92)",
+              }),
+              stroke: new Stroke({
+                color: active
+                  ? "rgba(90, 45, 0, 0.45)"
+                  : "rgba(6, 36, 82, 0.55)",
+                width: 3,
+              }),
+            }),
+          }),
+        );
+      }
+      return styles;
+    },
+    createCountyBoundaryStyle() {
+      const zoom = this.currentZoom;
+      const alpha = zoom >= 5.5 ? 0.28 : zoom >= 4.5 ? 0.16 : 0.08;
+      return new Style({
+        stroke: new Stroke({
+          color: `rgba(150, 220, 255, ${alpha})`,
+          width: zoom >= 6 ? 0.7 : 0.5,
+        }),
+      });
+    },
+    createCityLabelStyle(feature) {
+      const name = feature.get("name") || "";
+      const focused = name && name === this.focusedCityName;
+      if (this.currentZoom < 5.6) return null;
+      return new Style({
+        text: new Text({
+          text: name,
+          font: focused ? "bold 12px Microsoft YaHei" : "11px Microsoft YaHei",
+          overflow: true,
+          fill: new Fill({
+            color: focused
+              ? "rgba(255, 236, 190, 0.98)"
+              : "rgba(210, 242, 255, 0.9)",
+          }),
+          stroke: new Stroke({
+            color: focused ? "rgba(90, 45, 0, 0.45)" : "rgba(6, 36, 82, 0.5)",
+            width: 2.5,
+          }),
+        }),
+      });
+    },
+    createMarkerStyle(feature) {
+      const data = feature.get("markerData") || feature.get("data") || {};
+      const val = data.val;
+      const iconSrc = this.getIconForValue(val);
+      const scale = val === "4" ? 0.12 : 0.08;
+      return new Style({
+        image: new Icon({
+          src: iconSrc,
+          scale,
+          anchor: [0.5, 1],
+          opacity: 1,
+        }),
+      });
+    },
+    getIconForValue(val) {
+      switch (val) {
+        case "-1":
+        case -1:
+        case "10":
+        case 10:
+          return icon01;
+        case 0:
+        case "0":
+        case 20:
+        case "20":
+          return icon02;
+        case 1:
+        case "1":
+        case 30:
+        case "30":
+          return icon03;
+        case "4":
+          return icon04;
+        case "5":
+          return icon05;
+        case "6":
+          return icon06;
+        default:
+          return defaultIcon;
+      }
+    },
+    onHoverLeave() {
+      this.hovering = false;
+      if (this.hideTimer) clearTimeout(this.hideTimer);
+      this.hideTimer = setTimeout(() => {
+        if (!this.hovering) {
+          this.hoverInfo = null;
+          this.lastHoveredFeature = null;
+        }
+      }, 120);
+    },
+    fetchWindHoverInfo() {
+      if (!this.hoverInfo) return;
+      const param = {
+        batchcode: this.hoverInfo.batchCode,
+        fieldCode: this.hoverInfo.codeNumber,
+      };
+      queryFloatingWindowInfo(param)
+        .then((res) => {
+          this.newWind = res.data || {};
+        })
+        .catch(() => {
+          this.newWind = {};
+        });
+    },
+    calcHoverPosition(evt, hoverHeight = 280, hoverWidth = 240) {
+      const mapEl = this.map.getTargetElement();
+      const mapRect = mapEl.getBoundingClientRect();
+      const rootRect = this.$el.getBoundingClientRect();
+      const offset = 12;
+      let left =
+        mapRect.left - rootRect.left + evt.pixel[0] + offset;
+      let top =
+        mapRect.top - rootRect.top + evt.pixel[1] - hoverHeight / 2;
+      if (top + hoverHeight > rootRect.height - offset) {
+        top = mapRect.top - rootRect.top + evt.pixel[1] - hoverHeight - offset;
+      }
+      if (top < offset) top = offset;
+      if (left + hoverWidth > rootRect.width - offset) {
+        left =
+          mapRect.left - rootRect.left + evt.pixel[0] - hoverWidth - offset;
+      }
+      if (left < offset) left = offset;
+      return { top, left };
+    },
+    initPointerMove() {
+      if (!this.map) return;
+      this.map.on("pointermove", (evt) => {
+        if (evt.dragging) return;
+        const markerFeature = this.map.forEachFeatureAtPixel(
+          evt.pixel,
+          (hit) => hit,
+          {
+            hitTolerance: 8,
+            layerFilter: (layer) => layer === this.markerLayer,
+          },
+        );
+        const targetEl = this.map.getTargetElement();
+        if (markerFeature) {
+          const data =
+            markerFeature.get("markerData") || markerFeature.get("data");
+          if (!data) return;
+          targetEl.style.cursor = "pointer";
+          if (this.lastHoveredFeature !== markerFeature) {
+            this.lastHoveredFeature = markerFeature;
+            const pos = this.calcHoverPosition(evt);
+            this.hoverInfo = data;
+            this.hoverStyle = {
+              position: "absolute",
+              left: `${pos.left}px`,
+              top: `${pos.top}px`,
+            };
+            this.fetchWindHoverInfo();
+          } else if (this.hoverInfo) {
+            const pos = this.calcHoverPosition(evt);
+            this.hoverStyle = {
+              position: "absolute",
+              left: `${pos.left}px`,
+              top: `${pos.top}px`,
+            };
+          }
+          return;
+        }
+        targetEl.style.cursor = "";
+        if (!this.hovering) {
+          this.hoverInfo = null;
+          this.lastHoveredFeature = null;
+        }
+      });
+    },
+    refreshProvinceStyle() {
+      if (this.provinceLayer) this.provinceLayer.changed();
+      if (this.provinceGlowLayer) this.provinceGlowLayer.changed();
+      if (this.countyLayer) this.countyLayer.changed();
+    },
+    loadCityLabels(provinceName) {
+      if (!this.cityLabelSource) return;
+      this.cityLabelSource.clear();
+      this.focusedCityName = "";
+      const key = CN_TO_PROVINCE_KEY[this.normalizeRegionName(provinceName)];
+      const collection = key ? ProvinceData[key] : null;
+      if (!collection || !collection.features) return;
+      const format = new GeoJSON();
+      const features = format.readFeatures(collection, {
+        dataProjection: "EPSG:4326",
+        featureProjection: "EPSG:4326",
+      });
+      features.forEach((feature) => {
+        const props = feature.getProperties() || {};
+        const cp = props.cp;
+        if (!Array.isArray(cp) || cp.length < 2) return;
+        const labelFeature = new Feature({
+          geometry: new Point(cp),
+          name: props.name || "",
+        });
+        this.cityLabelSource.addFeature(labelFeature);
+      });
+    },
+    clearCityLabels() {
+      if (this.cityLabelSource) this.cityLabelSource.clear();
+      this.focusedCityName = "";
+    },
+    fitFeature(feature, options = {}) {
+      if (!this.map || !feature) return;
+      const geometry = feature.getGeometry();
+      if (!geometry) return;
+      this.map.getView().fit(geometry.getExtent(), {
+        padding: options.padding || [56, 56, 56, 56],
+        duration: options.duration == null ? 700 : options.duration,
+        maxZoom: options.maxZoom || 7.4,
+      });
+    },
+    fitChina(animate = true) {
+      if (!this.map) {
+        return;
+      }
+      const view = this.map.getView();
+      const source = this.provinceLayer && this.provinceLayer.getSource();
+      if (source) {
+        const extent = source.getExtent();
+        view.fit(extent, {
+          padding: [12, 16, 28, 16],
+          duration: animate ? 500 : 0,
+          maxZoom: 6.4,
+          callback: () => {
+            // 宽屏 fit 后仍偏小,再略放大铺满中间区域
+            const z = view.getZoom() || DEFAULT_ZOOM;
+            const nextZoom = Math.min(z + 0.55, 6.4);
+            if (animate) {
+              view.animate({ zoom: nextZoom, duration: 280 });
+            } else {
+              view.setZoom(nextZoom);
+            }
+            this.currentZoom = nextZoom;
+          },
+        });
+        return;
+      }
+      if (animate) {
+        view.animate({
+          center: DEFAULT_CENTER,
+          zoom: DEFAULT_ZOOM,
+          duration: 600,
+        });
+      } else {
+        view.setCenter(DEFAULT_CENTER);
+        view.setZoom(DEFAULT_ZOOM);
+      }
+      this.currentZoom = DEFAULT_ZOOM;
+    },
+    fitMarkers(animate = true) {
+      if (!this.map || !this.markerSource) return;
+      const features = this.markerSource.getFeatures();
+      if (!features.length) {
+        this.fitChina(animate);
+        return;
+      }
+      if (features.length === 1) {
+        const coord = features[0].getGeometry().getCoordinates();
+        this.map.getView().animate({
+          center: coord,
+          zoom: 6.8,
+          duration: animate ? 700 : 0,
+        });
+        return;
+      }
+      const extent = this.markerSource.getExtent();
+      const spanLon = extent[2] - extent[0];
+      const spanLat = extent[3] - extent[1];
+      // 场站分布接近全国时,继续用放大后的全国视图,避免又缩回去
+      if (spanLon > 12 || spanLat > 10 || features.length > 6) {
+        this.fitChina(animate);
+        return;
+      }
+      this.map.getView().fit(extent, {
+        padding: [48, 56, 56, 56],
+        duration: animate ? 700 : 0,
+        maxZoom: 7,
+      });
+    },
+    focusProvince(name, options = {}) {
+      const feature = this.findProvinceFeature(name);
+      if (!feature) return false;
+      const provinceName = this.getProvinceName(feature);
+      this.focusedProvinceName = provinceName;
+      this.loadCityLabels(provinceName);
+      this.refreshProvinceStyle();
+      if (options.fit !== false) {
+        this.fitFeature(feature, {
+          maxZoom: options.maxZoom || 6.8,
+          duration: options.duration,
+        });
+      }
+      return true;
+    },
+    focusCity(cityName, provinceName) {
+      if (provinceName) {
+        this.focusProvince(provinceName, { fit: false });
+      }
+      this.focusedCityName = cityName || "";
+      if (this.cityLabelLayer) this.cityLabelLayer.changed();
+      if (!this.cityLabelSource || !cityName) return false;
+      const feature = this.cityLabelSource
+        .getFeatures()
+        .find((item) => item.get("name") === cityName);
+      if (!feature) return false;
+      this.map.getView().animate({
+        center: feature.getGeometry().getCoordinates(),
+        zoom: Math.max(this.currentZoom, 7.2),
+        duration: 700,
+      });
+      return true;
+    },
+    initMapClick() {
+      if (!this.map) return;
+      this.map.on("singleclick", (evt) => {
+        const markerFeature = this.map.forEachFeatureAtPixel(
+          evt.pixel,
+          (hit) => hit,
+          {
+            hitTolerance: 8,
+            layerFilter: (layer) => layer === this.markerLayer,
+          },
+        );
+        if (markerFeature) {
+          const markerData =
+            markerFeature.get("markerData") || markerFeature.get("data");
+          if (markerData) {
+            this.$emit("feature-click", markerData);
+            return;
+          }
+        }
+        const feature = this.map.forEachFeatureAtPixel(
+          evt.pixel,
+          (hit) => hit,
+          {
+            layerFilter: (layer) =>
+              layer === this.provinceLayer || layer === this.cityLabelLayer,
+          },
+        );
+        if (!feature) return;
+        const cityName = feature.get("name");
+        if (feature.getGeometry() instanceof Point && cityName) {
+          this.focusedCityName = cityName;
+          if (this.cityLabelLayer) this.cityLabelLayer.changed();
+          this.$emit("city-click", {
+            name: cityName,
+            province: this.focusedProvinceName,
+          });
+          return;
+        }
+        const provinceName = this.getProvinceName(feature);
+        if (provinceName) {
+          this.focusProvince(provinceName);
+          this.$emit("province-click", {
+            name: provinceName,
+            center: feature.getProperties()?.cp || null,
+          });
+        }
+      });
+    },
+    setMarkers(list = []) {
+      if (!this.markerSource) return;
+      this.markerSource.clear();
+      this.markerFeatures = [];
+      list.forEach((item) => this.addMarker(item));
+    },
+    addMarker(marker) {
+      if (!this.markerSource || !marker) return;
+      if (!Array.isArray(marker.point) || marker.point.length < 2) return;
+      const payload = {
+        ...marker,
+        val: marker.val != null ? marker.val : marker.analysisState,
+      };
+      const feature = new Feature({
+        geometry: new Point(marker.point),
+      });
+      feature.set("active", !!marker.active);
+      feature.set("markerData", payload);
+      feature.set("data", payload);
+      this.markerSource.addFeature(feature);
+      this.markerFeatures.push(feature);
+    },
+    clearMarkers() {
+      if (!this.markerSource) return;
+      this.markerSource.clear();
+      this.markerFeatures = [];
+      this.hoverInfo = null;
+      this.lastHoveredFeature = null;
+    },
+    moveAndZoom(data = { point: DEFAULT_CENTER, zoom: DEFAULT_ZOOM }) {
+      if (!this.map) return;
+      if (data.provinceName) {
+        this.focusProvince(data.provinceName, {
+          maxZoom: Number(data.zoom) || 6.8,
+        });
+        return;
+      }
+      if (data.cityName) {
+        this.focusCity(data.cityName, data.provinceName);
+        return;
+      }
+      const point = Array.isArray(data.point) ? data.point : DEFAULT_CENTER;
+      const zoom = Number(data.zoom) || DEFAULT_ZOOM;
+      this.map.getView().animate({
+        center: point,
+        zoom,
+        duration: 800,
+      });
+    },
+    zoomToChina() {
+      this.focusedProvinceName = "";
+      this.clearCityLabels();
+      this.refreshProvinceStyle();
+      this.fitChina(true);
+    },
+    zoomToProvince(name) {
+      return this.focusProvince(name);
+    },
+  },
+};
+</script>
+
+<style scoped lang="scss">
+.tech-blue-province-map {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  min-height: 420px;
+  overflow: hidden;
+  border-radius: 14px;
+  background: radial-gradient(
+      circle at 50% 42%,
+      rgba(36, 93, 188, 0.34) 0%,
+      rgba(8, 22, 54, 0.12) 32%,
+      rgba(2, 9, 24, 0.98) 86%
+    ),
+    linear-gradient(180deg, #091730 0%, #020814 100%);
+}
+
+.tech-blue-province-map__bg {
+  position: absolute;
+  inset: 0;
+  z-index: 0;
+  pointer-events: none;
+  background: radial-gradient(
+      circle at 50% 36%,
+      rgba(69, 131, 238, 0.2) 0%,
+      transparent 52%
+    ),
+    radial-gradient(
+      circle at 50% 72%,
+      rgba(10, 120, 255, 0.08) 0%,
+      transparent 40%
+    );
+  box-shadow: inset 0 0 180px rgba(0, 0, 0, 0.58),
+    inset 0 -90px 120px rgba(0, 0, 0, 0.62),
+    inset 0 24px 50px rgba(30, 137, 255, 0.08);
+}
+
+.tech-blue-province-map__grid {
+  position: absolute;
+  inset: 0;
+  z-index: 1;
+  pointer-events: none;
+  background: linear-gradient(rgba(90, 182, 255, 0.05) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(90, 182, 255, 0.04) 1px, transparent 1px),
+    linear-gradient(rgba(90, 182, 255, 0.02) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(90, 182, 255, 0.02) 1px, transparent 1px);
+  background-size: 24px 24px, 24px 24px, 96px 96px, 96px 96px;
+  mix-blend-mode: screen;
+}
+
+.tech-blue-province-map__stage {
+  position: absolute;
+  inset: 0;
+  z-index: 2;
+  transform: none;
+  transform-origin: 50% 50%;
+}
+
+.tech-blue-province-map__shadow {
+  position: absolute;
+  left: 12%;
+  right: 12%;
+  bottom: -2%;
+  height: 18%;
+  border-radius: 50%;
+  background: radial-gradient(
+    ellipse at center,
+    rgba(0, 180, 255, 0.28) 0%,
+    rgba(0, 50, 110, 0.2) 40%,
+    transparent 74%
+  );
+  filter: blur(12px);
+  pointer-events: none;
+  z-index: 0;
+}
+
+.tech-blue-province-map__canvas {
+  position: relative;
+  z-index: 1;
+  width: 100%;
+  height: 100%;
+  filter: drop-shadow(0 0 18px rgba(40, 190, 255, 0.35))
+    drop-shadow(0 16px 28px rgba(0, 0, 0, 0.5));
+}
+
+.tech-blue-province-map__canvas ::v-deep .ol-zoom {
+  top: 16px;
+  left: auto;
+  right: 16px;
+  background: transparent;
+}
+
+.tech-blue-province-map__canvas ::v-deep .ol-zoom button {
+  width: 28px;
+  height: 28px;
+  margin: 3px 0;
+  color: #9fe9ff;
+  background: rgba(6, 24, 52, 0.88);
+  border: 1px solid rgba(70, 190, 255, 0.55);
+  border-radius: 4px;
+  box-shadow: 0 0 10px rgba(40, 170, 255, 0.25);
+}
+
+.tech-blue-province-map__canvas ::v-deep .ol-zoom button:hover {
+  background: rgba(18, 56, 110, 0.95);
+  color: #fff;
+}
+
+.tech-blue-province-map__south-sea {
+  position: absolute;
+  right: 28px;
+  bottom: 24px;
+  z-index: 4;
+  width: 148px;
+  height: 108px;
+  border: 1px solid rgba(90, 220, 255, 0.75);
+  border-radius: 4px;
+  background: linear-gradient(
+    180deg,
+    rgba(8, 28, 62, 0.92) 0%,
+    rgba(3, 12, 32, 0.96) 100%
+  );
+  box-shadow: 0 0 16px rgba(40, 180, 255, 0.28),
+    inset 0 0 18px rgba(30, 140, 255, 0.16);
+  pointer-events: none;
+}
+
+.tech-blue-province-map__south-sea-title {
+  position: absolute;
+  left: 8px;
+  top: 6px;
+  font-size: 11px;
+  color: rgba(190, 240, 255, 0.92);
+  text-shadow: 0 0 6px rgba(60, 200, 255, 0.55);
+}
+
+.tech-blue-province-map__south-sea-dot {
+  position: absolute;
+  width: 4px;
+  height: 4px;
+  border-radius: 50%;
+  background: #8beeff;
+  box-shadow: 0 0 6px rgba(100, 220, 255, 0.9);
+}
+
+.tech-blue-province-map__hover {
+  position: absolute;
+  z-index: 20;
+  width: 248px;
+  pointer-events: auto;
+  border-radius: 8px;
+  overflow: hidden;
+  border: 1px solid rgba(90, 210, 255, 0.55);
+  box-shadow:
+    0 0 20px rgba(40, 160, 255, 0.28),
+    0 12px 28px rgba(0, 0, 0, 0.45);
+  backdrop-filter: blur(6px);
+
+  h3 {
+    margin: 0;
+    padding: 8px 12px;
+    font-size: 14px;
+    font-weight: 600;
+    color: #eaf9ff;
+    background: linear-gradient(
+      90deg,
+      rgba(30, 110, 190, 0.9) 0%,
+      rgba(12, 48, 100, 0.92) 100%
+    );
+    border-bottom: 1px solid rgba(90, 210, 255, 0.35);
+  }
+}
+
+.tech-blue-province-map__hover-body {
+  padding: 4px 0;
+  background: rgba(4, 18, 42, 0.94);
+
+  p {
+    margin: 0;
+    padding: 5px 12px;
+    display: flex;
+    justify-content: space-between;
+    gap: 12px;
+    font-size: 12px;
+    color: rgba(210, 240, 255, 0.88);
+
+    span:last-child {
+      color: #9fe9ff;
+      text-align: right;
+    }
+  }
+}
+</style>

+ 119 - 53
src/components/map/index.vue

@@ -176,10 +176,11 @@ import { fromLonLat } from "ol/proj";
 import { Vector } from "ol/source";
 import { Vector as VectorLayer } from "ol/layer";
 import { Point } from "ol/geom";
-import { Icon, Style, Stroke, Fill } from "ol/style";
+import { Icon, Style, Stroke, Fill, Text } from "ol/style";
 import ZoomSlider from "ol/control/ZoomSlider.js";
 import { defaults as defaultControls } from "ol/control.js";
 import GeoJSON from "ol/format/GeoJSON";
+import { ChinaData, ProvinceData } from "china-map-geojson";
 
 // 假设你已经有正确的山西省边界数据文件(GeoJSON 格式)
 // import shanxiBoundary from "./shanxi.json";
@@ -192,14 +193,6 @@ import icon05 from "../../assets/img/icon05.png";
 import icon06 from "../../assets/img/iconFJ.png";
 import defaultIcon from "../../assets/img/iconFJ.png";
 
-// import icon01 from "../../assets/img/icon01.png";
-// import icon02 from "../../assets/img/icon02.png";
-// import icon03 from "../../assets/img/icon03.png";
-// import icon04 from "../../assets/img/icon04.png";
-// import icon05 from "../../assets/img/icon05.png";
-// import icon06 from "../../assets/img/iconFJ.png";
-// import defaultIcon from "../../assets/img/iconFJ.png";
-
 import { queryErrDescByEngine, queryFloatingWindowInfo } from "@/api/ledger.js";
 export default {
   props: {
@@ -239,6 +232,9 @@ export default {
       fengjiTimer: null,
       taTimer: null,
       newWind: {},
+      provinceLabelLayer: null,
+      cityLabelLayer: null,
+      currentZoom: 3,
     };
   },
   mounted() {
@@ -255,9 +251,6 @@ export default {
       layers: [
         new TileLayer({
           source: new XYZ({
-            // url: "http://10.96.137.5:9080/tiles/{z}/{x}/{y}.png", //大~#@唐
-            // url: "http://192.168.0.1/tiles/{z}/{x}/{y}.png", //华电
-            // url: "http://192.168.50.235/tiles/{z}/{x}/{y}.png", //中广核
             url: process.env.VUE_APP_MAPVIEW,
           }),
         }),
@@ -269,50 +262,123 @@ export default {
       controls: defaultControls().extend([new ZoomSlider()]),
     });
 
-    // -------------【添加山西省真实边界线】------------- 定位全国的时候注释这个代码
-    // if (this.$route.path === "/home/cockpitManage") {
-    //   // 通过导入的 JSON 文件加载边界数据(GeoJSON 格式),并解析成矢量要素
-    //   const shanxiSource = new Vector({
-    //     features: new GeoJSON().readFeatures(shanxiBoundary, {
-    //       featureProjection: "EPSG:4326",
-    //     }),
-    //   });
-    //   // 创建矢量图层,仅显示边界线(填充颜色设为透明)
-    //   const shanxiLayer = new VectorLayer({
-    //     source: shanxiSource,
-    //     style: new Style({
-    //       stroke: new Stroke({
-    //         color: "rgba(59, 130, 246, 0.5)",
-    //         width: 3,
-    //       }),
-    //       fill: new Fill({
-    //         color: "rgba(59, 130, 246, 0.05)", // 透明填充
-    //       }),
-    //     }),
-    //   });
-    //   this.map.addLayer(shanxiLayer);
-
-    //   const markerLayer = this.map
-    //     .getLayers()
-    //     .getArray()
-    //     .find((layer) => {
-    //       return layer.get("id") === "marker";
-    //     });
-    //   if (markerLayer) {
-    //     markerLayer.setZIndex(10);
-    //   }
-
-    //   // 同时设置高亮图层 zIndex 较低
-    //   shanxiLayer.setZIndex(1);
-    //   // -------------【结束】-------------
-    //   // const targetExtent = [106.8, 34.3, 118.6, 41.2];//山西
-    //   const targetExtent = [119.65, 40.87, 131.31, 46.30];//吉林
-    //   this.map.getView().fit(targetExtent, { duration: 2000 });
-    // }
-    // 这个放在外面
+    this.initAdminNameLayers();
+    this.map.getView().on("change:resolution", () => {
+      this.currentZoom = this.map.getView().getZoom() || 3;
+      if (this.provinceLabelLayer) this.provinceLabelLayer.changed();
+      if (this.cityLabelLayer) this.cityLabelLayer.changed();
+    });
     this.initEvent();
   },
   methods: {
+    initAdminNameLayers() {
+      const format = new GeoJSON();
+      const provinceSource = new Vector();
+      const citySource = new Vector();
+
+      // 省级标注
+      format
+        .readFeatures(ChinaData, {
+          dataProjection: "EPSG:4326",
+          featureProjection: "EPSG:4326",
+        })
+        .forEach((feature) => {
+          const props = feature.getProperties() || {};
+          let coord = null;
+          if (Array.isArray(props.cp) && props.cp.length >= 2) {
+            coord = props.cp;
+          } else if (props.longitude != null && props.latitude != null) {
+            coord = [Number(props.longitude), Number(props.latitude)];
+          }
+          if (!coord || !props.name) return;
+          const pointFeature = new Feature({
+            geometry: new Point(coord),
+            name: props.name,
+            level: "province",
+          });
+          provinceSource.addFeature(pointFeature);
+        });
+
+      // 地市/州标注(china-map-geojson 最细到地市级;不含乡镇)
+      Object.values(ProvinceData || {}).forEach((collection) => {
+        if (!collection || !collection.features) return;
+        format
+          .readFeatures(collection, {
+            dataProjection: "EPSG:4326",
+            featureProjection: "EPSG:4326",
+          })
+          .forEach((feature) => {
+            const props = feature.getProperties() || {};
+            const cp = props.cp;
+            if (!Array.isArray(cp) || cp.length < 2 || !props.name) return;
+            const pointFeature = new Feature({
+              geometry: new Point(cp),
+              name: props.name,
+              level: "city",
+            });
+            citySource.addFeature(pointFeature);
+          });
+      });
+
+      this.provinceLabelLayer = new VectorLayer({
+        source: provinceSource,
+        zIndex: 20,
+        declutter: true,
+        style: (feature) => this.createAdminLabelStyle(feature, "province"),
+      });
+      this.cityLabelLayer = new VectorLayer({
+        source: citySource,
+        zIndex: 21,
+        declutter: true,
+        style: (feature) => this.createAdminLabelStyle(feature, "city"),
+      });
+      this.map.addLayer(this.provinceLabelLayer);
+      this.map.addLayer(this.cityLabelLayer);
+    },
+    createAdminLabelStyle(feature, level) {
+      const zoom = this.currentZoom;
+      const name = feature.get("name") || "";
+      if (level === "province") {
+        // 放大到地市级后隐藏省名,减少重叠
+        if (zoom >= 6.2) return null;
+        return new Style({
+          text: new Text({
+            text: name,
+            font: zoom >= 4.8 ? "bold 13px Microsoft YaHei" : "12px Microsoft YaHei",
+            fill: new Fill({ color: "rgba(20, 40, 70, 0.92)" }),
+            stroke: new Stroke({
+              color: "rgba(255, 255, 255, 0.92)",
+              width: 3,
+            }),
+            overflow: true,
+          }),
+        });
+      }
+      // 地市名称:中高缩放显示
+      if (zoom < 5.2) return null;
+      const isCountyLike = /[县市区旗]$/.test(name) || name.includes("自治");
+      // 更高缩放才尽量展示偏县级命名,减轻全国视图拥挤
+      if (zoom < 6.5 && isCountyLike && name.length > 4) {
+        // 仍显示,但字号略小
+      }
+      return new Style({
+        text: new Text({
+          text: name,
+          font:
+            zoom >= 7
+              ? "12px Microsoft YaHei"
+              : "11px Microsoft YaHei",
+          fill: new Fill({
+            color: "rgba(30, 55, 90, 0.9)",
+          }),
+          stroke: new Stroke({
+            color: "rgba(255, 255, 255, 0.9)",
+            width: 2.5,
+          }),
+          overflow: true,
+        }),
+      });
+    },
     onHoverLeave() {
       this.hovering = false;
       clearTimeout(this.hideTimer);

+ 0 - 13
src/themes/light.scss

@@ -1,13 +0,0 @@
-$primary-color: #82780ccf;
-$background-color: #82780ccf;
-$text-color: #ffffff;
-$bgopacity-color: #82780c27;
-
-:root {
-  --primary-color: #{$primary-color};
-  --background-color: #{$background-color};
-  --bgopacity-color: #{$bgopacity-color};
-  --text-color: #{$text-color};
-  --header-bg: #82780c;
-  --content-bg: #82780ca5;
-}

+ 82 - 150
src/views/admin/cockpitManage/HD.vue

@@ -1,33 +1,28 @@
 <template>
-  <div class="map-ditu">
-    <Tmap ref="map" :maplist="maplist" @feature-click="featureClick"></Tmap>
-    <bottom-data :maplistArr="maplistArr" :defaultdata="defaultdata" />
-    <div class="zuobian">
-      <selecttree
-        placeholder="请选择所属公司"
-        :list="parentOpt"
-        type="1"
-        v-model="companyCode"
-        @change="parentChange"
-        :defaultParentProps="{
-          children: 'children',
-          label: 'companyName',
-          value: 'codeNumber',
-        }"
-      >
-      </selecttree>
-    </div>
-
+  <div class="map-ditu cockpit-hd">
+    <bottom-data
+      class="cockpit-hd__sides"
+      layout="sides"
+      :maplistArr="maplistArr"
+      :defaultdata="defaultdata"
+    >
+      <template #map>
+        <div class="cockpit-hd__map">
+          <Tmap
+            ref="map"
+            :maplist="maplist"
+            @feature-click="featureClick"
+          ></Tmap>
+        </div>
+      </template>
+    </bottom-data>
     <router-view></router-view>
   </div>
 </template>
 
 <script>
-import selecttree from "../../../components/selecttree.vue";
-import {
-  getCompanyFieldNumberVo,
-  getSysOrganizationAuthTreeByRoleId,
-} from "@/api/ledger.js";
+import { eventBus } from "@/assets/js/eventBus.js";
+// import Tmap from "@/components/map/TechBlueOfflineProvinceGeoMap.vue";
 import Tmap from "@/components/map";
 import BottomData from "./component/bottomData.vue";
 
@@ -36,7 +31,6 @@ export default {
   components: {
     Tmap,
     BottomData,
-    selecttree,
   },
   props: {
     fieldCode: "",
@@ -45,127 +39,61 @@ export default {
   data() {
     return {
       ShowRi: true,
-      treeval: "",
-      value1: true,
-      treeval: "", // 绑定的输入框值
-      showTree: false, // 控制树列表显示
-      treeData: [],
       maplist: [],
       maplistArr: {},
-      totalList: [],
-      parentOpt: [],
-      companyCode: "",
       defaultdata: {},
     };
   },
-  created() {
-    this.GETtree();
-    this.GETtotal();
-  },
+
   mounted() {
-    //模拟地图上的点位
-    const data = this.maplist;
-    // data.forEach((element) => {
-    //   console.log(element);
-    //   // this.$refs.map.addMarker({ point: [116.40740,39.90420], val: "1" });
-    // });
+    eventBus.$on("hd-company-change", this.parentChange);
+    eventBus.$emit("hd-cockpit-ready");
+  },
+  beforeDestroy() {
+    eventBus.$off("hd-company-change", this.parentChange);
   },
   methods: {
-    // 获取总数
-    GETtotal() {
-      getCompanyFieldNumberVo().then((res) => {
-        this.totalList = res.data;
-      });
-    },
-    // 获取企业数
-    async GETtree() {
-      const res = await getSysOrganizationAuthTreeByRoleId();
-      const treedata = res.data;
-      const processedData = this.processTreeData(treedata);
-      this.parentOpt = processedData;
-      this.defaultdata = res.data[0];
-      this.parentChange(this.defaultdata);
-      // const firstLayer = this.findFirstFieldLayer(processedData);
-      // if (firstLayer) {
-      //   this.parentChange(  this.defaultdata);
-      // }
-    },
-    //过滤数据
-    processTreeData(treeData) {
-      const processedData = [];
-      function processNode(node) {
-        if (node.codeType === "field") {
-          node.companyName = node.fieldName;
-        }
-        if (node.children && node.children.length > 0) {
-          node.children.forEach((child) => {
-            processNode(child);
-          });
-        }
-      }
-      treeData.forEach((root) => {
-        processNode(root);
-        processedData.push(root);
-      });
-      return processedData;
-    },
-    handleTreeData() {
-      this.treeData = this.processTreeData(this.treeData);
-    },
     parseCoordinates(input) {
       if (input && typeof input === "string") {
         return input.split(",").map(Number);
       }
-      // debugger;
       return [];
     },
-    parentChange(data) {
-      //data为当前选中对象
-      this.$refs.map.clearMarkers();
-      this.maplist = data;
-      this.maplistArr = data;
-      if (data.codeType === "field") {
-        if (this.parseCoordinates(data.longitudeAndLatitudeString).length > 0) {
-          this.$refs.map.addMarker({
-            point: this.parseCoordinates(data.longitudeAndLatitudeString),
-            val: data.analysisState,
-            ...data,
+    collectFieldMarkers(node, result = []) {
+      if (!node) return result;
+      if (node.codeType === "field") {
+        const point = this.parseCoordinates(node.longitudeAndLatitudeString);
+        if (point.length >= 2) {
+          result.push({
+            point,
+            val: node.analysisState,
+            ...node,
           });
-          return;
         }
-        this.$refs.map.clearMarkers();
-      } else {
-        const dataMapList = data.children;
-        dataMapList.forEach((element) => {
-          if (
-            this.parseCoordinates(element.longitudeAndLatitudeString).length >
-              0 &&
-            element.codeType === "field"
-          ) {
-            this.$refs.map.addMarker({
-              point: this.parseCoordinates(element.longitudeAndLatitudeString),
-              val: element.analysisState,
-              ...element,
-            });
-            return;
-          }
-          this.$refs.map.clearMarkers();
-        });
       }
+      (node.children || []).forEach((child) => {
+        this.collectFieldMarkers(child, result);
+      });
+      return result;
     },
-    findFirstFieldLayer(data) {
-      for (const item of data) {
-        if (item.codeType === "field") {
-          return item;
+    parentChange(data) {
+      if (!data || !this.$refs.map) return;
+      this.$refs.map.clearMarkers();
+      this.maplist = data;
+      this.maplistArr = data;
+      this.defaultdata = data;
+      const markers = this.collectFieldMarkers(data);
+      markers.forEach((marker) => {
+        this.$refs.map.addMarker(marker);
+      });
+      this.$nextTick(() => {
+        if (this.$refs.map && this.$refs.map.fitMarkers) {
+          this.$refs.map.fitMarkers(true);
         }
-        if (item.children && item.children.length > 0) {
-          const childResult = this.findFirstFieldLayer(item.children);
-          if (childResult) {
-            return childResult;
-          }
+        if (this.$refs.map && this.$refs.map.map) {
+          this.$refs.map.map.updateSize();
         }
-      }
-      return null;
+      });
     },
     featureClick(data) {
       const val = data.val;
@@ -177,7 +105,6 @@ export default {
           type: "warning",
         });
       } else if (val === "1" || val === 1 || val === "30" || val === 30) {
-        console.log("打包成功");
         this.$router.push({
           path: "cockpitManage/electronic-map",
           query: {
@@ -195,39 +122,44 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-.map-ditu {
-  height: calc(100vh - 100px);
+.map-ditu.cockpit-hd {
+  height: calc(100vh - 88px);
   display: flex;
   flex-direction: column;
+  padding: 8px 10px 10px;
+  box-sizing: border-box;
+  overflow: hidden;
+  background: #061322;
 }
 
-// 动态计算 Tmap 组件高度
-Tmap {
-  height: calc(88.9vh - 230px);
-  min-height: 0; // 防止 flex 项溢出
+.cockpit-hd__sides {
+  flex: 1;
+  min-height: 0;
 }
 
-// 设置 bottom-data 组件高度为 230px
-bottom-data {
-  height: 230px;
-  flex-shrink: 0; // 防止组件缩小
+.cockpit-hd__map {
+  width: 100%;
+  height: 100%;
+  min-width: 0;
+  min-height: 0;
+  border-radius: 8px;
+  overflow: hidden;
+  border: 1px solid rgba(100, 170, 210, 0.2);
+  background: #0b1c30;
 }
 
-.zuobian {
-  width: 150px;
-  position: absolute;
-  top: 30px;
-  left: 10px;
+.cockpit-hd__map ::v-deep .map-container,
+.cockpit-hd__map ::v-deep .map,
+.cockpit-hd__map ::v-deep .tech-blue-province-map {
+  width: 100%;
+  height: 100%;
+  border-radius: 0;
 }
+
 @media screen and (max-width: 1650px) {
-  .zuobian {
-    width: 150px;
-    position: absolute;
-    top: 10px;
-    left: 10px;
-  }
-  .map-ditu {
-    height: calc(100vh - 60px);
+  .map-ditu.cockpit-hd {
+    height: calc(100vh - 64px);
+    padding: 6px 8px 8px;
   }
 }
 </style>

+ 548 - 376
src/views/admin/cockpitManage/component/bottomData.vue

@@ -1,245 +1,286 @@
 <template>
-  <div class="bottom-data">
-    <div class="Box1">
-      <h3 style="margin: 8px 0">风场分析完成率</h3>
-      <el-date-picker
-        size="mini"
-        v-model="value1"
-        type="datetimerange"
-        format="yyyy-MM-dd"
-        range-separator="至"
-        start-placeholder="开始日期"
-        end-placeholder="结束日期"
-        @change="onDateChange"
-      >
-      </el-date-picker>
-      <div id="main" style="width: 260px; height: 160px"></div>
-    </div>
-    <div class="Box2">
-      <div>
-        <h3 style="margin: 8px 0">任务统计</h3>
-        <div class="BOXsan">
-          <div class="YC02" @click="getwgl">
-            <strong class="texyellow">
-              {{ onrelevance ? onrelevance : 0 }}</strong
-            >
-            <span>待分析任务</span>
-          </div>
-          <div class="YC02" @click="getwfx">
-            <strong class="textred"> {{ abnormal ? abnormal : 0 }}</strong>
-            <span>异常任务</span>
-          </div>
-
-          <div class="YC02" @click="getpd">
-            <strong class="textred">
-              {{ queueNumber ? queueNumber : 0 }}</strong
-            >
-            <span>排队任务</span>
-          </div>
+  <div class="side-data" :class="'side-data--' + layout">
+    <div class="side-data__col side-data__col--left">
+      <div class="panel Box1">
+        <div class="panel__head">
+          <h3>风场分析完成率</h3>
         </div>
+        <el-date-picker
+          class="panel__date"
+          size="mini"
+          v-model="value1"
+          type="datetimerange"
+          format="yyyy-MM-dd"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          @change="onDateChange"
+        >
+        </el-date-picker>
+        <div id="main" class="panel__chart"></div>
       </div>
-      <div>
-        <h3 style="margin: 8px 0">异常设备概览</h3>
-        <div class="BOX">
-          <div style="display: flex">
-            <div class="YC02">
-              <strong>
-                {{ equipment?.errCounrt ? equipment.errCounrt : 0 }}</strong
+      <div class="panel Box2">
+        <div class="panel__block">
+          <div class="panel__head">
+            <h3>任务统计</h3>
+          </div>
+          <div class="BOXsan">
+            <div class="stat-card" @click="getwgl">
+              <strong class="texyellow">
+                {{ onrelevance ? onrelevance : 0 }}</strong
               >
-              <span>异常总数</span>
+              <span>待分析任务</span>
             </div>
-            <div class="YC02">
-              <strong>
-                {{
-                  equipment?.errCountInCurrentMonth
-                    ? equipment.errCountInCurrentMonth
-                    : 0
-                }}</strong
-              >
-              <span>本月异常</span>
+            <div class="stat-card" @click="getwfx">
+              <strong class="textred"> {{ abnormal ? abnormal : 0 }}</strong>
+              <span>异常任务</span>
             </div>
-            <div class="YC02">
-              <strong class="textred">
-                {{
-                  equipment?.errCountNotDeal ? equipment.errCountNotDeal : 0
-                }}</strong
+            <div class="stat-card" @click="getpd">
+              <strong class="textorange">
+                {{ queueNumber ? queueNumber : 0 }}</strong
               >
-              <span>未处理异常</span>
+              <span>排队任务</span>
             </div>
-            <div class="YC02">
-              <strong class="texyellow">
-                {{
-                  equipment?.errDeviceCount ? equipment.errDeviceCount : 0
-                }}</strong
-              >
-              <span>异常设备数</span>
+          </div>
+        </div>
+        <div class="panel__block">
+          <div class="panel__head">
+            <h3>异常设备概览</h3>
+          </div>
+          <div class="BOX">
+            <div class="stat-row">
+              <div class="stat-card">
+                <strong>
+                  {{ equipment?.errCounrt ? equipment.errCounrt : 0 }}</strong
+                >
+                <span>异常总数</span>
+              </div>
+              <div class="stat-card">
+                <strong>
+                  {{
+                    equipment?.errCountInCurrentMonth
+                      ? equipment.errCountInCurrentMonth
+                      : 0
+                  }}</strong
+                >
+                <span>本月异常</span>
+              </div>
+              <div class="stat-card">
+                <strong class="textred">
+                  {{
+                    equipment?.errCountNotDeal ? equipment.errCountNotDeal : 0
+                  }}</strong
+                >
+                <span>未处理异常</span>
+              </div>
+              <div class="stat-card">
+                <strong class="texyellow">
+                  {{
+                    equipment?.errDeviceCount ? equipment.errDeviceCount : 0
+                  }}</strong
+                >
+                <span>异常设备数</span>
+              </div>
             </div>
           </div>
         </div>
       </div>
+    </div>
 
-      <el-dialog title="待分析任务统计表" :visible.sync="glShow" width="45%">
-        <!-- 外部容器设置最大高度和滚动条 -->
-        <div style="max-height: 400px; overflow-y: auto">
-          <el-table :data="glData" style="width: 100%">
-            <el-table-column
-              prop="fieldName"
-              label="风场名称"
-              width="180"
-            ></el-table-column>
-            <el-table-column
-              prop="analysisName"
-              label="分析名称"
-            ></el-table-column>
-            <el-table-column
-              prop="createTime"
-              label="创建时间"
-              width="180"
-            ></el-table-column>
-
-            <el-table-column fixed="right" label="操作" width="100">
-              <template slot-scope="scope">
-                <el-button
-                  @click="handleClick(scope.row)"
-                  type="text"
-                  size="small"
-                  >编辑</el-button
-                >
-              </template>
-            </el-table-column>
-          </el-table>
+    <div class="side-data__map">
+      <slot name="map"></slot>
+    </div>
+
+    <div class="side-data__col side-data__col--right">
+      <div class="panel Box3">
+        <div class="BOX" v-hasPermi="['home:offlinedata']">
+          <div class="panel__head panel__head--actions">
+            <h3>入库数据</h3>
+            <span class="panel__actions">
+              <span class="panel__link" @click="importdata">导入数据</span>
+              <span class="panel__link" @click="more">更多</span>
+            </span>
+          </div>
+          <div class="panel__table">
+            <p class="table-head">
+              <span>风场名称</span><span>入库时间</span>
+            </p>
+            <tabrollHD
+              :table-data="RKtableData"
+              :shujuloading="shujuloading"
+              :columns="RKtableColumns"
+              :column-widths="RKcolumnWidths"
+              :max-height="tableMaxHeight"
+            ></tabrollHD>
+          </div>
         </div>
-      </el-dialog>
-
-      <el-dialog title="异常任务统计表" :visible.sync="YCShow" width="45%">
-        <div style="max-height: 400px; overflow-y: auto">
-          <el-table :data="FXData" style="width: 100%">
-            <el-table-column prop="fieldName" label="风场名称" width="180">
-            </el-table-column>
-            <el-table-column prop="analysisName" label="分析名称">
-            </el-table-column>
-            <el-table-column prop="createTime" label="创建时间" width="180">
-            </el-table-column>
-            <el-table-column fixed="right" label="操作" width="100">
-              <template slot-scope="scope">
-                <el-button
-                  @click="YChandleClick(scope.row)"
-                  type="text"
-                  size="small"
-                  >编辑</el-button
-                >
-              </template>
-            </el-table-column>
-          </el-table>
+      </div>
+      <div class="panel Box4">
+        <div class="panel__head">
+          <h3>性能分析操作记录</h3>
         </div>
-      </el-dialog>
-
-      <el-dialog title="排队任务统计表" :visible.sync="PdShow" width="45%">
-        <div style="max-height: 400px; overflow-y: auto">
-          <el-table :data="PdData" style="width: 100%">
-            <el-table-column prop="fieldName" label="风场名称" width="180">
-            </el-table-column>
-            <el-table-column prop="analysisName" label="分析名称">
-            </el-table-column>
-            <el-table-column prop="createTime" label="创建时间" width="180">
-            </el-table-column>
-            <el-table-column fixed="right" label="操作" width="100">
-              <template slot-scope="scope">
-                <el-button @click="PDClick(scope.row)" type="text" size="small"
-                  >编辑</el-button
-                >
-              </template>
-            </el-table-column>
-          </el-table>
+        <div class="panel__table">
+          <p class="table-head table-head--3">
+            <span>名字</span><span>日期</span><span>操作记录</span>
+          </p>
+          <tabrollHD
+            :table-data="tableData"
+            :shujuloading="shujuloading"
+            :columns="tableColumns"
+            :max-height="tableMaxHeight"
+            :column-widths="columnWidths"
+          ></tabrollHD>
+        </div>
+      </div>
+      <div class="panel Box5">
+        <div class="panel__head">
+          <h3>异常数据信息</h3>
         </div>
-      </el-dialog>
-
-      <el-dialog
-        title="入库信息"
-        width="70%"
-        :visible.sync="dialogTableVisible"
-      >
-        <el-button size="small" @click="importdata">导入数据</el-button>
-        <el-table :data="gridData" style="max-height: 400px; overflow: auto">
+        <div class="panel__table">
+          <p class="table-head">
+            <span>风场名称</span><span>风机信息</span>
+          </p>
+          <tabrollHD
+            :table-data="YCtableData"
+            :shujuloading="shujuloading"
+            :columns="YCtableColumns"
+            :column-widths="YCcolumnWidths"
+            :max-height="tableMaxHeight"
+          ></tabrollHD>
+        </div>
+      </div>
+    </div>
+
+    <el-dialog
+      title="待分析任务统计表"
+      :visible.sync="glShow"
+      width="45%"
+      append-to-body
+    >
+      <div style="max-height: 400px; overflow-y: auto">
+        <el-table :data="glData" style="width: 100%">
           <el-table-column
-            property="windFarmName"
+            prop="fieldName"
             label="风场名称"
-            show-overflow-tooltip
-            width="150px"
-          ></el-table-column>
-          <el-table-column
-            property="minuteTimeAreaList"
-            label="SCADA分钟级"
-            show-overflow-tooltip
-          ></el-table-column>
-          <el-table-column
-            property="secondTimeAreaList"
-            label="SCADA秒级"
-            show-overflow-tooltip
-          ></el-table-column>
-          <el-table-column
-            property="faultTimeAreaList"
-            label="故障数据"
-            show-overflow-tooltip
+            width="180"
           ></el-table-column>
           <el-table-column
-            property="warnTimeAreaList"
-            label="报警数据"
-            show-overflow-tooltip
+            prop="analysisName"
+            label="分析名称"
           ></el-table-column>
           <el-table-column
-            property="vibrationTimeAreaList"
-            label="振动数据"
-            show-overflow-tooltip
+            prop="createTime"
+            label="创建时间"
+            width="180"
           ></el-table-column>
+          <el-table-column fixed="right" label="操作" width="100">
+            <template slot-scope="scope">
+              <el-button
+                @click="handleClick(scope.row)"
+                type="text"
+                size="small"
+                >编辑</el-button
+              >
+            </template>
+          </el-table-column>
         </el-table>
-      </el-dialog>
-    </div>
-    <div class="Box3">
-      <div class="BOX" v-hasPermi="['home:offlinedata']">
-        <h3 class="centeredh3" style="margin: 8px 0">
-          入库数据
-          <span>
-            <span style="cursor: pointer" @click="importdata">导入数据</span>
-            <span style="cursor: pointer" @click="more">更多</span>
-          </span>
-        </h3>
-        <div>
-          <p class="titleYC"><span>风场名称</span><span>入库时间</span></p>
-          <tabrollHD
-            :table-data="RKtableData"
-            :shujuloading="shujuloading"
-            :columns="RKtableColumns"
-            :column-widths="RKcolumnWidths"
-            :max-height="140"
-          ></tabrollHD>
-        </div>
       </div>
-    </div>
-    <div class="Box4">
-      <h3 style="margin: 8px 0">性能分析操作记录</h3>
-      <p class="titleJQ">
-        <span>名字</span><span>日期</span><span>操作记录</span>
-      </p>
-      <tabrollHD
-        :table-data="tableData"
-        :shujuloading="shujuloading"
-        :columns="tableColumns"
-        :max-height="140"
-        :column-widths="columnWidths"
-      ></tabrollHD>
-    </div>
-    <div class="Box5">
-      <h3 style="margin: 8px 0">异常数据信息</h3>
-      <p class="titleYC"><span>风场名称</span><span>风机信息</span></p>
-      <tabrollHD
-        :table-data="YCtableData"
-        :shujuloading="shujuloading"
-        :columns="YCtableColumns"
-        :column-widths="YCcolumnWidths"
-        :max-height="140"
-      ></tabrollHD>
-    </div>
+    </el-dialog>
+
+    <el-dialog
+      title="异常任务统计表"
+      :visible.sync="YCShow"
+      width="45%"
+      append-to-body
+    >
+      <div style="max-height: 400px; overflow-y: auto">
+        <el-table :data="FXData" style="width: 100%">
+          <el-table-column prop="fieldName" label="风场名称" width="180">
+          </el-table-column>
+          <el-table-column prop="analysisName" label="分析名称">
+          </el-table-column>
+          <el-table-column prop="createTime" label="创建时间" width="180">
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" width="100">
+            <template slot-scope="scope">
+              <el-button
+                @click="YChandleClick(scope.row)"
+                type="text"
+                size="small"
+                >编辑</el-button
+              >
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-dialog>
+
+    <el-dialog
+      title="排队任务统计表"
+      :visible.sync="PdShow"
+      width="45%"
+      append-to-body
+    >
+      <div style="max-height: 400px; overflow-y: auto">
+        <el-table :data="PdData" style="width: 100%">
+          <el-table-column prop="fieldName" label="风场名称" width="180">
+          </el-table-column>
+          <el-table-column prop="analysisName" label="分析名称">
+          </el-table-column>
+          <el-table-column prop="createTime" label="创建时间" width="180">
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" width="100">
+            <template slot-scope="scope">
+              <el-button @click="PDClick(scope.row)" type="text" size="small"
+                >编辑</el-button
+              >
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-dialog>
+
+    <el-dialog
+      title="入库信息"
+      width="70%"
+      :visible.sync="dialogTableVisible"
+      append-to-body
+    >
+      <el-button size="small" @click="importdata">导入数据</el-button>
+      <el-table :data="gridData" style="max-height: 400px; overflow: auto">
+        <el-table-column
+          property="windFarmName"
+          label="风场名称"
+          show-overflow-tooltip
+          width="150px"
+        ></el-table-column>
+        <el-table-column
+          property="minuteTimeAreaList"
+          label="SCADA分钟级"
+          show-overflow-tooltip
+        ></el-table-column>
+        <el-table-column
+          property="secondTimeAreaList"
+          label="SCADA秒级"
+          show-overflow-tooltip
+        ></el-table-column>
+        <el-table-column
+          property="faultTimeAreaList"
+          label="故障数据"
+          show-overflow-tooltip
+        ></el-table-column>
+        <el-table-column
+          property="warnTimeAreaList"
+          label="报警数据"
+          show-overflow-tooltip
+        ></el-table-column>
+        <el-table-column
+          property="vibrationTimeAreaList"
+          label="振动数据"
+          show-overflow-tooltip
+        ></el-table-column>
+      </el-table>
+    </el-dialog>
   </div>
 </template>
 
@@ -264,10 +305,19 @@ export default {
       type: Object,
       default: () => ({}),
     },
+    layout: {
+      type: String,
+      default: "sides",
+    },
   },
   components: {
     tabrollHD,
   },
+  computed: {
+    tableMaxHeight() {
+      return this.layout === "sides" ? 160 : 140;
+    },
+  },
   watch: {
     maplistArr: {
       handler(newVal) {
@@ -340,15 +390,28 @@ export default {
     const halfYearAgo = new Date();
     halfYearAgo.setMonth(currentTime.getMonth() - 6);
     this.value1 = [halfYearAgo, currentTime];
-    // 同时更新 startTime 和 endTime
     this.startTime = this.$formatDate(halfYearAgo);
     this.endTime = this.$formatDate(currentTime);
+    this.$nextTick(() => {
+      if (this.myChart) this.myChart.resize();
+    });
+    window.addEventListener("resize", this.handleChartResize);
+  },
+  beforeDestroy() {
+    window.removeEventListener("resize", this.handleChartResize);
+    if (this.myChart) {
+      this.myChart.dispose();
+      this.myChart = null;
+    }
   },
   created() {
     this.getbacklog();
     this.ruku();
   },
   methods: {
+    handleChartResize() {
+      if (this.myChart) this.myChart.resize();
+    },
     initChart() {
       var chartDom = document.getElementById("main");
       this.myChart = echarts.init(chartDom);
@@ -364,42 +427,44 @@ export default {
           },
         },
         legend: {
-          orient: "vertical",
-          left: "left",
-          top: "30%",
+          orient: "horizontal",
+          left: "center",
+          bottom: 0,
+          itemWidth: 8,
+          itemHeight: 8,
+          itemGap: 12,
           textStyle: {
-            // Add this for white legend text
-            color: "#fff",
+            color: "rgba(220, 240, 255, 0.88)",
+            fontSize: 11,
           },
         },
         series: [
           {
             type: "pie",
-            radius: ["40%", "80%"],
+            radius: ["32%", "52%"],
             avoidLabelOverlap: false,
             label: {
               show: false,
-              position: "center",
             },
             labelLine: {
               show: false,
             },
-            center: ["60%", "50%"],
+            center: ["50%", "42%"],
             data: [
               {
                 value: this.equipment.analysisedCount,
                 name: "已完成",
-                itemStyle: { color: "#7ECF51" },
+                itemStyle: { color: "#3ddea0" },
               },
               {
                 value: this.equipment.analysisingCount,
                 name: "进行中",
-                itemStyle: { color: "#EECB5F" },
+                itemStyle: { color: "#4eb8ff" },
               },
               {
                 value: this.equipment.noAnalysisCount,
                 name: "未开始",
-                itemStyle: { color: "#E16757" },
+                itemStyle: { color: "#e8a06a" },
               },
             ],
           },
@@ -412,7 +477,7 @@ export default {
       const startTime =
         this.startTime ||
         this.$formatDate(
-          new Date(new Date().setMonth(new Date().getMonth() - 6))
+          new Date(new Date().setMonth(new Date().getMonth() - 6)),
         ); // 半年前的时间
       const endTime = this.endTime || this.$formatDate(new Date()); // 当前时间
       // halfYearAgo.setMonth(currentTime.getMonth() - 6)
@@ -498,7 +563,7 @@ export default {
       // 打开新窗口
       const newWindow = window.open(
         "/home/performance/assetssMag?id=195",
-        "_blank"
+        "_blank",
       );
       // 等待窗口加载完成后发送消息
       newWindow.onload = () => {
@@ -513,7 +578,7 @@ export default {
       // 打开新窗口
       const newWindow = window.open(
         "/home/performance/assetssMag?id=195",
-        "_blank"
+        "_blank",
       );
       // 等待窗口加载完成后发送消息
       newWindow.onload = () => {
@@ -592,190 +657,297 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-.bottom-data {
-  height: 230px;
-  background-color: #1b3b7f;
-  display: flex;
-  justify-content: space-around;
+.side-data {
+  height: 100%;
+  box-sizing: border-box;
 }
-.Box1,
-.Box2,
-.Box4,
-.Box5 {
-  H3 {
-    color: #fff;
-  }
+
+.side-data--sides {
+  display: grid;
+  grid-template-columns: 270px minmax(0, 1fr) 270px;
+  gap: 10px;
+  height: 100%;
+  width: 100%;
+  padding: 0;
+  background: transparent;
+  border: none;
+  box-shadow: none;
 }
 
-.Box2 {
-  .BOX {
-    display: flex;
-    padding-left: 10px;
-    flex-wrap: wrap;
-    padding-bottom: 15px;
-    .YC02 {
-      width: 70px;
-      height: 60px;
-      background-image: url("../../../../assets/003.png");
-      background-size: 70px 60px;
-      background-repeat: no-repeat;
-      background-position: center;
-      margin-right: 15px;
-      margin-top: 0px;
-      text-align: center;
-
-      .textred {
-        color: #d9001b;
-      }
+.side-data__map {
+  min-width: 0;
+  min-height: 0;
+  height: 100%;
+}
 
-      .texyellow {
-        color: #facd91;
-      }
+.side-data__col {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  min-width: 0;
+  height: 100%;
+  padding: 0;
+  box-sizing: border-box;
+  overflow: hidden;
+  background: transparent;
+  border: none;
+  box-shadow: none;
+}
 
-      strong {
-        display: block;
-        font-size: 26px;
-        color: #4bffff;
-      }
+.panel {
+  flex: 1;
+  min-height: 0;
+  min-width: 0;
+  padding: 10px 12px 12px;
+  border-radius: 8px;
+  background: rgba(8, 24, 48, 0.88);
+  border: 1px solid rgba(90, 160, 210, 0.2);
+  display: flex;
+  flex-direction: column;
+}
 
-      span {
-        display: block;
-        color: #fff;
-        font-size: 12px;
-      }
-    }
+.panel__head {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8px;
+  flex-shrink: 0;
+  padding-left: 8px;
+  border-left: 2px solid #3eb6ff;
+
+  h3 {
+    margin: 0;
+    font-size: 13px;
+    font-weight: 600;
+    letter-spacing: 0.5px;
+    color: rgba(230, 245, 255, 0.95);
   }
+}
 
-  .BOXsan {
-    width: 255px;
-    display: flex;
-    justify-content: space-around;
-
-    .YC02 {
-      width: 70px;
-      height: 60px;
-      background-image: url("../../../../assets/003.png");
-      background-size: 70px 60px;
-      background-repeat: no-repeat;
-      background-position: center;
-
-      margin-top: 15px;
-      text-align: center;
-      cursor: pointer;
-
-      .textred {
-        color: #d9001b;
-      }
+.panel__head--actions {
+  justify-content: space-between;
+  padding-right: 0;
+}
 
-      .texyellow {
-        color: #facd91;
-      }
+.panel__actions {
+  display: inline-flex;
+  gap: 10px;
+}
 
-      strong {
-        display: block;
-        font-size: 26px;
-        color: #4bffff;
-      }
+.panel__link {
+  cursor: pointer;
+  font-size: 12px;
+  color: rgba(120, 200, 245, 0.85);
 
-      span {
-        display: block;
-        font-size: 12px;
-        color: #fff;
-      }
-    }
+  &:hover {
+    color: #fff;
   }
 }
 
-.Box3 {
-  width: 290px;
-  .BOX {
-    display: flex;
-    padding-left: 10px;
-    flex-wrap: wrap;
-    padding-bottom: 15px;
-    color: #fff;
-    border-radius: 5px;
-    margin-bottom: 10px;
+.panel__date {
+  margin-bottom: 6px;
+  flex-shrink: 0;
+}
 
-    h3 {
-      margin-top: 5px;
-    }
+.panel__chart {
+  width: 100%;
+  flex: 1;
+  min-height: 140px;
+}
 
-    .centeredh3 {
-      width: 100%;
-      display: flex;
-      justify-content: space-between;
-      margin-bottom: 5px;
+.panel__table {
+  min-height: 0;
+  flex: 1;
+  overflow: hidden;
+}
 
-      span:nth-child(1) {
-        font-size: 12px;
-      }
-      span:nth-child(2) {
-        font-size: 12px;
-        padding-left: 10px;
-      }
-    }
-    .titleYC {
-      font-size: 14px;
-      background: #51a7e3;
-      padding: 5px 0;
-      color: #fff;
-
-      span:nth-child(1) {
-        margin: 0 70px 0 10px;
-      }
-    }
+.table-head {
+  display: flex;
+  margin: 0 0 4px;
+  padding: 6px 10px;
+  font-size: 12px;
+  color: rgba(180, 220, 245, 0.9);
+  background: rgba(30, 80, 140, 0.35);
+  border-radius: 4px;
+  flex-shrink: 0;
+
+  span:nth-child(1) {
+    flex: 1;
+  }
+
+  span:nth-child(2) {
+    flex: 1;
+    text-align: right;
   }
 }
-.Box4 {
-  width: 260px;
-  .titleJQ {
-    font-size: 14px;
-    background: #51a7e3;
-    padding: 5px 0;
-    color: #fff;
 
-    span:nth-child(1) {
-      margin: 0 15px 0 10px;
-    }
+.table-head--3 {
+  span {
+    flex: 1;
+    text-align: left;
+  }
 
-    span:nth-child(2) {
-      margin: 0 45px 0 30px;
-    }
+  span:nth-child(2) {
+    text-align: center;
+  }
+
+  span:nth-child(3) {
+    text-align: right;
   }
 }
+
+.Box1 {
+  flex: 1.2;
+}
+
+.Box2 {
+  flex: 1;
+  gap: 10px;
+}
+
+.Box3,
+.Box4,
 .Box5 {
-  width: 260px;
-  .titleYC {
-    font-size: 14px;
-    background: #51a7e3;
-    padding: 5px 0;
-    color: #fff;
+  flex: 1;
+}
 
-    span:nth-child(1) {
-      margin: 0 70px 0 10px;
-    }
+.panel__block {
+  flex: 1;
+  min-height: 0;
+}
+
+.stat-row,
+.BOXsan {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 8px;
+}
+
+.BOXsan {
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  margin-top: 2px;
+}
+
+.stat-card {
+  min-width: 0;
+  height: 56px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  cursor: default;
+  border-radius: 6px;
+  background: rgba(12, 40, 72, 0.7);
+  border: 1px solid rgba(80, 150, 200, 0.18);
+  transition: background 0.2s ease, border-color 0.2s ease;
+
+  &:hover {
+    background: rgba(18, 56, 96, 0.85);
+    border-color: rgba(90, 180, 230, 0.35);
+  }
+
+  strong {
+    display: block;
+    font-size: 18px;
+    line-height: 1.1;
+    color: #7edfff;
+    font-weight: 700;
   }
+
+  span {
+    display: block;
+    margin-top: 4px;
+    font-size: 11px;
+    color: rgba(190, 220, 240, 0.75);
+  }
+
+  .textred {
+    color: #ff8f8f;
+  }
+
+  .texyellow {
+    color: #ffd58a;
+  }
+
+  .textorange {
+    color: #ffb070;
+  }
+}
+
+.BOXsan .stat-card {
+  cursor: pointer;
 }
 
 .Tabtab {
   font-size: 12px;
 }
 
-::v-deep.el-table__cell {
+::v-deep .el-table__cell {
   padding: 2px 0 !important;
 }
 
-::v-deep.el-table .el-table__cell {
+::v-deep .el-table .el-table__cell {
   padding: 2px 0 !important;
 }
 
-::v-deep.el-table--scrollable-y .el-table__body-wrapper {
+::v-deep .el-table--scrollable-y .el-table__body-wrapper {
   overflow-y: clip !important;
 }
 
-.el-date-editor--datetimerange.el-input,
-.el-date-editor--datetimerange.el-input__inner {
-  width: 250px;
+::v-deep .el-date-editor--datetimerange.el-input,
+::v-deep .el-date-editor--datetimerange.el-input__inner {
+  width: 100%;
+  max-width: 100%;
+}
+
+::v-deep .el-range-editor.el-input__inner {
+  background: rgba(6, 22, 42, 0.95);
+  border-color: rgba(80, 150, 200, 0.3);
+  color: #d7f4ff;
+  width: 100% !important;
+  height: 28px;
+  line-height: 28px;
+}
+
+::v-deep .el-range-editor .el-range-input {
+  background: transparent;
+  color: #d7f4ff;
+  width: 72px;
+  font-size: 12px;
+}
+
+::v-deep .el-range-editor .el-range-separator {
+  color: rgba(170, 220, 245, 0.7);
+  padding: 0 2px;
+  width: 16px;
+  line-height: 20px;
+}
+
+::v-deep .el-range-editor .el-range__icon,
+::v-deep .el-range-editor .el-range__close-icon {
+  color: rgba(140, 210, 255, 0.8);
+  line-height: 22px;
+}
+
+@media screen and (max-width: 1650px) {
+  .side-data--sides {
+    grid-template-columns: 230px minmax(0, 1fr) 230px;
+    gap: 8px;
+  }
+
+  .stat-card {
+    height: 48px;
+
+    strong {
+      font-size: 16px;
+    }
+
+    span {
+      font-size: 10px;
+    }
+  }
+
+  .panel {
+    padding: 8px 10px 10px;
+  }
 }
 </style>

+ 46 - 30
src/views/admin/cockpitManage/component/tabrollHD.vue

@@ -1,5 +1,5 @@
 <template>
-  <div>
+  <div class="hd-scroll-table">
     <el-table
       :show-header="false"
       v-loading="shujuloading"
@@ -7,11 +7,9 @@
       :max-height="maxHeight"
       :row-class-name="tableRowClassName"
       ref="shujuscroll_Table"
-      
       @mouseenter.native="shujuautoScroll(true)"
       @mouseleave.native="shujuautoScroll(false)"
       id="Tabtab"
-      :header-cell-style="{ backgroundColor: 'red' }"
     >
       <el-table-column
         v-for="(column, index) in columns"
@@ -70,7 +68,9 @@ export default {
   methods: {
     shujuautoScroll(stop) {
       const table = this.$refs.shujuscroll_Table;
+      if (!table || !table.$refs) return;
       const divData = table.$refs.bodyWrapper;
+      if (!divData) return;
       if (stop) {
         window.clearInterval(this.scrolltimer);
       } else {
@@ -86,64 +86,80 @@ export default {
       }
     },
     getColumnWidth(prop) {
-      return this.columnWidths[prop] + "px"; // 根据传入的属性(prop)获取对应的列宽度
-      return this.YCcolumnWidths[prop] + "px"; // 根据传入的属性(prop)获取对应的列宽度
+      return this.columnWidths[prop] + "px";
     },
-    tableRowClassName({ row, rowIndex }) {
+    tableRowClassName({ rowIndex }) {
       if (rowIndex % 2 === 1) {
         return "warning-row";
-      } else {
-        return "success-row";
       }
+      return "success-row";
     },
   },
 };
 </script>
 
 <style lang="scss" scoped>
+.hd-scroll-table {
+  width: 100%;
+}
+
 #Tabtab {
   font-size: 12px;
 }
 
-/* 单元格 padding、去掉底部白线 */
+::v-deep .el-table,
+::v-deep .el-table__expanded-cell {
+  background-color: transparent !important;
+  color: rgba(220, 245, 255, 0.92);
+}
+
+::v-deep .el-table tr,
+::v-deep .el-table th.el-table__cell {
+  background-color: transparent !important;
+}
+
 ::v-deep .el-table .el-table__cell {
-  padding: 2px 0 !important;
+  padding: 4px 0 !important;
   border-bottom: none !important;
+  color: rgba(220, 245, 255, 0.9);
 }
 
-/* 去掉 el-table 自带底部边框 */
-::v-deep .el-table::before {
+::v-deep .el-table::before,
+::v-deep .el-table::after {
   height: 0 !important;
+  background: transparent !important;
 }
 
-/* 设置奇偶行背景色 */
-::v-deep .warning-row {
+::v-deep .el-table__empty-block {
+  background: transparent !important;
+  min-height: 80px;
+}
+
+::v-deep .el-table__empty-text {
+  color: rgba(150, 200, 230, 0.5) !important;
+}
 
-  background-color: #1b3b7f !important;
-  color: white !important;
+::v-deep .warning-row {
+  background-color: rgba(20, 70, 140, 0.28) !important;
+  color: rgba(230, 248, 255, 0.95) !important;
 }
+
 ::v-deep .success-row {
-  background-color: #0b255d !important;
-  color: white !important;
+  background-color: rgba(6, 28, 62, 0.35) !important;
+  color: rgba(230, 248, 255, 0.95) !important;
 }
 
-/* ✅ 解决 hover 行不变色问题 —— 统一高亮 hover 行为 */
 ::v-deep .el-table__body .el-table__row:hover > td {
-  background-color: #50a7e3 !important;
-  color: white !important;
+  background-color: rgba(50, 150, 230, 0.35) !important;
+  color: #fff !important;
 }
 
-
+::v-deep .el-loading-mask {
+  background-color: rgba(4, 18, 42, 0.55);
+}
 
 ::v-deep .el-table__body-wrapper::-webkit-scrollbar {
-  width: 0px;
+  width: 0;
   opacity: 0.5;
 }
-
-::v-deep .el-table__body-wrapper::-webkit-scrollbar-thumb {
-  border-radius: 15px;
-}
-
-
-
 </style>

+ 160 - 14
src/views/home/Index.vue

@@ -1,12 +1,26 @@
 <template>
   <div class="homeWrap" v-loading="loading">
     <!-- 华电 -->
-    <el-container v-if="menuShow === 'HD'">
+    <el-container v-if="menuShow === 'HD'" class="home-hd">
       <el-main>
         <el-header>
           <div class="headerHDBox">
-            <div class="headerLeft"></div>
-            <!-- <HeaderCom class="headerRight" @setLoding="setLoding"></HeaderCom> -->
+            <div class="headerLeft header-company">
+              <selecttree
+                size="small"
+                placeholder="请选择所属公司"
+                :list="parentOpt"
+                type="1"
+                v-model="companyCode"
+                @change="onCompanyChange"
+                :defaultParentProps="{
+                  children: 'children',
+                  label: 'companyName',
+                  value: 'codeNumber',
+                }"
+              >
+              </selecttree>
+            </div>
             <MenuHD class="menuBox"></MenuHD>
             <HeaderCom class="headerRight" @setLoding="setLoding"></HeaderCom>
           </div>
@@ -43,6 +57,9 @@ import HeaderCom from "@/components/HeaderCom.vue";
 import MenuHD from "./components/MenuHD.vue";
 import Menu from "./components/Menu.vue";
 import MenuTag from "@/components/MenuTag.vue";
+import selecttree from "@/components/selecttree.vue";
+import { getSysOrganizationAuthTreeByRoleId } from "@/api/ledger.js";
+import { eventBus } from "@/assets/js/eventBus.js";
 
 export default {
   components: {
@@ -50,17 +67,72 @@ export default {
     MenuHD,
     HeaderCom,
     MenuTag,
+    selecttree,
   },
   data() {
     return {
       menuShow: process.env.VUE_APP_ISSHOWHD,
       loading: false,
+      parentOpt: [],
+      companyCode: "",
+      currentCompany: null,
     };
   },
+  created() {
+    if (this.menuShow === "HD") {
+      this.loadCompanyTree();
+      eventBus.$on("hd-cockpit-ready", this.emitCurrentCompany);
+    }
+  },
+  beforeDestroy() {
+    eventBus.$off("hd-cockpit-ready", this.emitCurrentCompany);
+  },
   methods: {
     setLoding(state) {
       this.loading = state;
     },
+    processTreeData(treeData) {
+      const processedData = [];
+      function processNode(node) {
+        if (node.codeType === "field") {
+          node.companyName = node.fieldName;
+        }
+        if (node.children && node.children.length > 0) {
+          node.children.forEach((child) => {
+            processNode(child);
+          });
+        }
+      }
+      treeData.forEach((root) => {
+        processNode(root);
+        processedData.push(root);
+      });
+      return processedData;
+    },
+    async loadCompanyTree() {
+      try {
+        const res = await getSysOrganizationAuthTreeByRoleId();
+        const treedata = res.data || [];
+        this.parentOpt = this.processTreeData(treedata);
+        if (treedata[0]) {
+          this.currentCompany = treedata[0];
+          this.companyCode = treedata[0].codeNumber || "";
+          this.emitCurrentCompany();
+        }
+      } catch (e) {
+        // ignore
+      }
+    },
+    onCompanyChange(data) {
+      this.currentCompany = data;
+      this.companyCode = data && data.codeNumber ? data.codeNumber : "";
+      this.emitCurrentCompany();
+    },
+    emitCurrentCompany() {
+      if (this.currentCompany) {
+        eventBus.$emit("hd-company-change", this.currentCompany);
+      }
+    },
   },
 };
 </script>
@@ -72,12 +144,10 @@ export default {
 .homeWrap {
   height: 100vh;
   .el-container {
-    // height: 100%;
     min-height: 89vh;
   }
   .el-main {
     min-width: 800px;
-    // min-height: 88vh;
     height: 100%;
     background-color: #f7f7f8;
     padding: 0;
@@ -86,28 +156,78 @@ export default {
       height: 100%;
     }
   }
+
+  .home-hd .el-main {
+    background:
+      linear-gradient(180deg, #061428 0%, #030b1a 100%);
+  }
 }
 ::v-deep.el-header {
   padding: 0 !important;
-  background: #1b3b7f;
   color: #fff !important;
-  height: 100px !important;
+  height: 88px !important;
+  background: transparent;
+
   .headerHDBox {
+    position: relative;
     display: flex;
     justify-content: space-between;
     align-items: center;
-    background: url("../../assets/headerBorder.png");
-    background-size: 6%, 100%;
-    background-repeat: repeat-x; /* 水平方向重复 */
+    height: 100%;
+    padding: 0 16px 0 12px;
+    box-sizing: border-box;
+    background:
+      linear-gradient(
+        180deg,
+        rgba(10, 40, 88, 0.96) 0%,
+        rgba(4, 16, 40, 0.98) 100%
+      );
+    border-bottom: 1px solid rgba(70, 180, 255, 0.28);
+    box-shadow:
+      0 8px 24px rgba(0, 20, 60, 0.35),
+      inset 0 -1px 0 rgba(120, 210, 255, 0.12);
+
+    &::after {
+      content: "";
+      position: absolute;
+      left: 8%;
+      right: 8%;
+      bottom: 0;
+      height: 1px;
+      background: linear-gradient(
+        90deg,
+        transparent,
+        rgba(90, 210, 255, 0.7),
+        transparent
+      );
+      pointer-events: none;
+    }
   }
+
   .menuBox {
     flex: 1;
-    overflow-x: scroll;
+    overflow: hidden;
     align-items: center;
     justify-content: center;
     display: flex;
+    height: 100%;
   }
+
   .headerRight {
+    flex-shrink: 0;
+    z-index: 2;
+  }
+
+  .headerRight ::v-deep .header {
+    background: transparent;
+  }
+
+  .headerRight ::v-deep .personalCenter img {
+    width: 36px;
+    height: 36px;
+    border-radius: 50%;
+    border: 1px solid rgba(90, 210, 255, 0.45);
+    box-shadow: 0 0 12px rgba(60, 180, 255, 0.3);
   }
 }
 .headerBox {
@@ -128,13 +248,39 @@ export default {
 }
 
 .headerLeft {
-  // width: 170px;
-  width: 52px;
+  width: 12px;
+  flex-shrink: 0;
+}
+
+.header-company {
+  width: 200px;
+  margin-right: 8px;
+  z-index: 3;
+}
+
+.header-company ::v-deep .el-input__inner {
+  background: rgba(6, 28, 58, 0.9);
+  border-color: rgba(70, 190, 255, 0.4);
+  color: #d7f4ff;
+  height: 36px;
+  line-height: 36px;
+}
+
+.header-company ::v-deep .el-input__inner::placeholder {
+  color: rgba(170, 220, 245, 0.55);
+}
+
+.header-company ::v-deep .el-select .el-input .el-select__caret {
+  color: rgba(140, 210, 255, 0.85);
 }
 
 @media screen and (max-width: 1650px) {
   ::v-deep .el-header {
-    height: 60px !important;
+    height: 64px !important;
+  }
+
+  .header-company {
+    width: 160px;
   }
 }
 </style>

+ 302 - 313
src/views/home/components/MenuHD.vue

@@ -1,110 +1,110 @@
 <template>
-  <div class="menuBox">
-    <el-menu
-      collapse-transition
-      ref="menu"
-      mode="horizontal"
-      :class="`mt-3 el-menu-vertical-demo my-menu-wrapper ${isDefault}`"
-      @open="handleOpen"
-      @close="handleClose"
-      :router="true"
-      :default-active="defaultActive"
-      :default-openeds="openMenus"
-      background-color="#1b3b7f"
-      text-color="#fff"
-      active-text-color="#ffd04b"
-    >
-      <!--     :active-text-color="activeTextColor" -->
-      <template v-for="(item, indexMenu) in routerList">
-        <!-- 判断是否是中间位置 -->
-        <el-menu-item
-          class="hdMenuTitle"
-          v-if="indexMenu === Math.floor(routerList.length / 2) + 1"
+  <div class="hd-nav">
+    <div class="hd-nav__side hd-nav__side--left">
+      <template v-for="item in leftMenus">
+        <el-dropdown
+          v-if="hasVisibleChildren(item)"
+          :key="'l-drop-' + item.id"
+          trigger="hover"
+          placement="bottom"
+          @command="onDropdownCommand"
         >
-          <div class="titles">
-            <span class="headerText">风电机组健康度诊断平台</span>
-          </div>
-        </el-menu-item>
-        <el-submenu
-          class="menuParentItem"
-          :style="{
-            transform:
-              indexMenu < Math.floor(routerList.length / 2) + 1
-                ? 'scaleX(-1)'
-                : 'scaleX(1)',
-          }"
-          v-if="
-            item.children && item.children.length && item.meta?.hidden === false
-          "
-          :index="item.path"
-        >
-          <template slot="title">
-            <span
-              :style="{
-                transform:
-                  indexMenu < Math.floor(routerList.length / 2) + 1
-                    ? 'scaleX(-1)'
-                    : 'scaleX(1)',
-              }"
-              >{{ item.name }}</span
-            >
-          </template>
-          <el-menu-item
-            v-for="child in item.children"
-            v-if="child.meta?.hidden === false"
-            :key="child.id"
-            :index="`${child.path}?id=${child.id}`"
-            @click="handleChangeMenuUrl(child, `${child.path}?id=${child.id}`)"
+          <button
+            type="button"
+            class="hd-nav__item"
+            :class="{ 'is-active': isActiveParent(item) }"
           >
-            <span>{{ child.name }}</span>
-          </el-menu-item>
-        </el-submenu>
-        <el-menu-item
-          class="menuParentItem"
-          :style="{
-            transform:
-              indexMenu < Math.floor(routerList.length / 2) + 1
-                ? 'scaleX(-1)'
-                : 'scaleX(1)',
-            color: '#fff',
-          }"
-          v-else-if="item.meta?.hidden === false"
-          :key="item.id"
-          :index="`${item.path}?id=${item.id}`"
+            <span>{{ item.name }}</span>
+            <i class="el-icon-arrow-down hd-nav__caret"></i>
+          </button>
+          <el-dropdown-menu slot="dropdown" class="hd-nav-dropdown">
+            <el-dropdown-item
+              v-for="child in visibleChildren(item)"
+              :key="child.id"
+              :command="{ item: child, path: `${child.path}?id=${child.id}` }"
+            >
+              {{ child.name }}
+            </el-dropdown-item>
+          </el-dropdown-menu>
+        </el-dropdown>
+        <button
+          v-else
+          :key="'l-btn-' + item.id"
+          type="button"
+          class="hd-nav__item"
+          :class="{ 'is-active': isActiveItem(item) }"
           @click="handleChangeMenuUrl(item, `${item.path}?id=${item.id}`)"
         >
-          <span
-            :style="{
-              transform:
-                indexMenu < Math.floor(routerList.length / 2) + 1
-                  ? 'scaleX(-1)'
-                  : 'scaleX(1)',
-            }"
-            >{{ item.name }}</span
+          <span>{{ item.name }}</span>
+        </button>
+      </template>
+    </div>
+
+    <div class="hd-nav__brand">
+      <div class="hd-nav__brand-deco hd-nav__brand-deco--left" aria-hidden="true"></div>
+      <div class="hd-nav__brand-core">
+        <span class="hd-nav__brand-text">风电机组健康度诊断平台</span>
+      </div>
+      <div class="hd-nav__brand-deco hd-nav__brand-deco--right" aria-hidden="true"></div>
+    </div>
+
+    <div class="hd-nav__side hd-nav__side--right">
+      <template v-for="item in rightMenus">
+        <el-dropdown
+          v-if="hasVisibleChildren(item)"
+          :key="'r-drop-' + item.id"
+          trigger="hover"
+          placement="bottom"
+          @command="onDropdownCommand"
+        >
+          <button
+            type="button"
+            class="hd-nav__item"
+            :class="{ 'is-active': isActiveParent(item) }"
           >
-        </el-menu-item>
+            <span>{{ item.name }}</span>
+            <i class="el-icon-arrow-down hd-nav__caret"></i>
+          </button>
+          <el-dropdown-menu slot="dropdown" class="hd-nav-dropdown">
+            <el-dropdown-item
+              v-for="child in visibleChildren(item)"
+              :key="child.id"
+              :command="{ item: child, path: `${child.path}?id=${child.id}` }"
+            >
+              {{ child.name }}
+            </el-dropdown-item>
+          </el-dropdown-menu>
+        </el-dropdown>
+        <button
+          v-else
+          :key="'r-btn-' + item.id"
+          type="button"
+          class="hd-nav__item"
+          :class="{ 'is-active': isActiveItem(item) }"
+          @click="handleChangeMenuUrl(item, `${item.path}?id=${item.id}`)"
+        >
+          <span>{{ item.name }}</span>
+        </button>
       </template>
-    </el-menu>
+    </div>
   </div>
 </template>
 
 <script>
 import { mapActions, mapState } from "vuex";
 import { orgList } from "./mockData";
-import Vue from "vue";
 
 export default {
   data() {
     return {
-      isExpanded: false, // 控制是否展开所有子菜单
-      openMenus: [], // 用来存储展开的菜单
+      isExpanded: false,
+      openMenus: [],
       isCollapse: true,
       orgList: orgList,
       searchInputValue: "",
       defaultActive: this.$route.fullPath,
       activeIndex: false,
       keyObject: {},
-      activeTextColor: Vue.prototype.$backgroundColor,
       routerList: [
         {
           id: 1,
@@ -128,104 +128,53 @@ export default {
     ...mapState({
       currentMenuIndex: (state) => state.breadStore?.currentUrl?.routeUrl,
     }),
-    // 获取所有父级菜单的 index
-    allMenuIndexes() {
-      return this.routerList.map((item) => {
-        if (
-          item.children &&
-          item.children.length &&
-          item.meta?.hidden === false
-        ) {
-          return item.path;
-        } else {
-          return `${item.path}?id=${item.id}`;
-        }
-      });
+    visibleMenus() {
+      return (this.routerList || []).filter(
+        (item) => item && item.meta && item.meta.hidden === false,
+      );
+    },
+    splitIndex() {
+      return Math.ceil(this.visibleMenus.length / 2);
+    },
+    leftMenus() {
+      return this.visibleMenus.slice(0, this.splitIndex);
     },
-    isDefault() {
-      return process.env.VUE_APP_ISSHOWHD;
+    rightMenus() {
+      return this.visibleMenus.slice(this.splitIndex);
     },
   },
   watch: {
-    currentMenuIndex: {
-      deep: true,
-      handler(newVale, oldVal) {
-        if (newVale) {
-          this.$refs.menu.close(newVale);
-        }
-      },
-    },
-    keyObject: {
-      deep: true,
-      handler(newVale) {
-        if (newVale && newVale.key === this.defaultActive) {
-          this.getBreadcrumbList(newVale.keyPath);
-        }
-      },
+    "$route.fullPath"(val) {
+      this.defaultActive = val;
     },
   },
   methods: {
     ...mapActions("menuTag", ["addTag"]),
-    // 切换展开/收起所有菜单
-    toggleAllMenus() {
-      this.isExpanded = !this.isExpanded;
-
-      if (this.isExpanded) {
-        // 展开所有子菜单
-        this.openMenus = this.routerList
-          .filter((item) => item.children && item.children.length)
-          .map((item) => item.path);
-      } else {
-        // 收起所有子菜单
-        this.openMenus = [];
-      }
-    },
-    handleChangeMenuUrl(item, path) {
-      this.defaultActive = path;
-      this.$router.push(path); // 跳转到指定的路由
+    visibleChildren(item) {
+      return (item.children || []).filter(
+        (child) => child && child.meta && child.meta.hidden === false,
+      );
     },
-    isElPrefix(str) {
-      const regex = /^el-/;
-      return regex.test(str);
+    hasVisibleChildren(item) {
+      return this.visibleChildren(item).length > 0;
     },
-    getBreadcrumbList(keyPath) {
-      const urls = keyPath;
-      const result = urls.map((url, index) => {
-        const params = new URLSearchParams(url.split("?")[1]);
-        const id = params.get("id");
-        const name = params.get("name");
-        const routeUrl = url;
-        if (index === urls.length - 1) {
-          return { id, name, routeUrl, currentPage: true };
-        }
-        return { id, name, routeUrl };
-      });
-      this.$store.commit("breadStore/ADD_BREAD", result);
-      return result;
-    },
-    handleOpen(key, keyPath) {
-      this.activeIndex = false;
-      this.keyObject = {
-        key,
-        keyPath,
-      };
-    },
-    handleClose(key, keyPath) {
-      this.activeIndex = false;
-      this.keyObject = {
-        key,
-        keyPath,
-      };
+    isActiveItem(item) {
+      const path = `${item.path}?id=${item.id}`;
+      return (
+        this.defaultActive === path ||
+        this.defaultActive === item.path ||
+        (this.$route.path && this.$route.path.indexOf(item.path) === 0)
+      );
     },
-    shrinkTree() {
-      this.$refs.menu.close(`/orgsPage?id=${orgList.id}&name=${orgList.name}`);
+    isActiveParent(item) {
+      if (this.isActiveItem(item)) return true;
+      return this.visibleChildren(item).some((child) =>
+        this.isActiveItem(child),
+      );
     },
-    handleChangeRouter() {
-      this.activeIndex = true;
-      this.defaultActive = "";
-      this.$store.commit("breadStore/ADD_BREAD", []);
-      this.$refs.menu.close(`/orgsPage?id=${orgList.id}&name=${orgList.name}`);
-      this.$router.push("/");
+    onDropdownCommand(command) {
+      if (!command) return;
+      this.handleChangeMenuUrl(command.item, command.path);
     },
     handleChangeMenuUrl(item, key) {
       this.defaultActive = key;
@@ -247,194 +196,234 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-::v-deep.menuParentItem .el-submenu__title {
-  background: #eff1f300 !important;
-  color: #fff !important;
-}
-:v-deep.menuParentItem .el-submenu__title:hover {
-  color: #eee !important;
-}
-::v-deep .menuParentItem {
-  background-image: url("../../../assets/head-tab.png") !important;
-  background-repeat: no-repeat !important;
-  background-position: center !important;
-
-  background-size: 100% 60% !important;
-
-  height: 100px !important;
-  width: 160px !important;
-  display: flex !important;
-  align-items: center !important;
-  justify-content: center !important;
-  background-color: #eff1f300 !important;
-}
-::v-deep.el-menu {
-  background-color: #eff1f300 !important;
-}
-::v-deep.hdMenuTitle {
-  background-color: transparent !important;
-  padding: 0px !important;
-}
-
-::v-deep.titles {
-  width: 100% !important;
-  // background-image: url("../../../assets/headerBorder.png") !important;
-  background-size: 35%, 100% !important;
-  background-repeat: repeat-x !important;
-  padding: 0px !important;
-}
-::v-deep.headerText {
-  pointer-events: none;
-  background-image: url("../../../assets/headerText.png") !important;
-  background-size: 100% 100%;
-  color: #fff;
-  font-size: 26px;
-  font-weight: 600;
-  text-align: center;
-  display: block;
+.hd-nav {
   width: 100%;
-
-  height: 70px;
-  padding: 0px 50px;
+  height: 100%;
   display: flex;
   align-items: center;
   justify-content: center;
+  gap: 10px;
+  padding: 0 8px;
+  box-sizing: border-box;
 }
 
-::v-deep.mt-3 {
-  margin-top: 0px !important;
-  //padding-left: 20px;
+.hd-nav__side {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  min-width: 0;
 }
-.stop-animation,
-.active-animation {
-  font-size: 0px;
+
+.hd-nav__side--left {
+  justify-content: flex-end;
 }
 
-.flexCenter {
-  display: flex;
-  align-items: center !important;
-  justify-content: space-between !important;
-  width: 200px;
+.hd-nav__side--right {
+  justify-content: flex-start;
 }
 
-.foldBox {
-  position: fixed;
-  bottom: 10px;
-  left: 20px;
-  padding: 10px;
+.hd-nav__item {
+  position: relative;
+  min-width: 108px;
+  height: 40px;
+  padding: 0 18px;
+  border: 1px solid rgba(80, 190, 255, 0.35);
+  border-radius: 4px;
+  background: linear-gradient(
+    180deg,
+    rgba(24, 78, 150, 0.55) 0%,
+    rgba(8, 32, 72, 0.72) 100%
+  );
+  color: rgba(230, 248, 255, 0.92);
+  font-size: 15px;
+  letter-spacing: 1px;
+  cursor: pointer;
+  outline: none;
+  transition: all 0.2s ease;
+  clip-path: polygon(10px 0, calc(100% - 10px) 0, 100% 100%, 0 100%);
+  box-shadow:
+    inset 0 1px 0 rgba(140, 220, 255, 0.18),
+    0 0 10px rgba(40, 140, 255, 0.08);
 
-  .icon {
-    cursor: pointer;
-    color: #666;
-    font-size: 14px;
+  span {
+    position: relative;
+    z-index: 1;
   }
-}
-
-::v-deep .el-submenu__title {
-  display: flex;
-  align-items: center;
-  margin-left: 15px;
-}
 
-::v-deep .el-submenu__icon-arrow {
-  right: 30px;
-}
+  &::before {
+    content: "";
+    position: absolute;
+    left: 12%;
+    right: 12%;
+    bottom: 0;
+    height: 2px;
+    background: linear-gradient(
+      90deg,
+      transparent,
+      rgba(90, 220, 255, 0.85),
+      transparent
+    );
+    opacity: 0.65;
+  }
 
-::v-deep .svnIcon {
-  vertical-align: middle;
-  margin-right: 5px;
-  width: 24px;
-  text-align: center;
-  font-size: 18px;
-}
+  &:hover {
+    color: #fff;
+    border-color: rgba(110, 220, 255, 0.7);
+    background: linear-gradient(
+      180deg,
+      rgba(40, 120, 210, 0.7) 0%,
+      rgba(12, 48, 100, 0.85) 100%
+    );
+    box-shadow: 0 0 16px rgba(60, 180, 255, 0.28);
+  }
 
-::-webkit-scrollbar {
-  width: 6px;
-  display: none;
-}
+  &.is-active {
+    color: #fff;
+    border-color: rgba(120, 230, 255, 0.9);
+    background: linear-gradient(
+      180deg,
+      rgba(48, 150, 240, 0.78) 0%,
+      rgba(16, 70, 140, 0.92) 100%
+    );
+    box-shadow:
+      0 0 18px rgba(70, 200, 255, 0.4),
+      inset 0 0 12px rgba(100, 210, 255, 0.18);
+    text-shadow: 0 0 8px rgba(120, 220, 255, 0.55);
 
-::-webkit-scrollbar-thumb {
-  border-radius: 10px;
-  background-color: #aaabaa;
-  cursor: pointer;
+    &::before {
+      opacity: 1;
+      height: 3px;
+      background: linear-gradient(
+        90deg,
+        transparent,
+        #7ef0ff,
+        transparent
+      );
+    }
+  }
 }
 
-::-webkit-scrollbar-thumb:hover {
-  background-color: #efeff0;
+.hd-nav__caret {
+  margin-left: 4px;
+  font-size: 12px;
 }
 
-::-webkit-scrollbar-track-piece {
-  background-color: #efeff0;
-  border-radius: 10px;
+.hd-nav__brand {
+  flex: 0 0 auto;
+  display: flex;
+  align-items: center;
+  max-width: 46%;
+  min-width: 320px;
 }
 
-.asideBox:hover ::-webkit-scrollbar {
-  display: block !important;
+.hd-nav__brand-deco {
+  width: 36px;
+  height: 2px;
+  background: linear-gradient(
+    90deg,
+    transparent,
+    rgba(90, 210, 255, 0.8)
+  );
 }
 
-/* 当前选中项的颜色 */
-.el-menu--horizontal .el-menu-item.is-active {
-  color: #ffffff !important;
+.hd-nav__brand-deco--right {
+  background: linear-gradient(
+    90deg,
+    rgba(90, 210, 255, 0.8),
+    transparent
+  );
 }
 
-// 默认样式(大屏)
-::v-deep .menuParentItem {
-  width: 160px !important;
-  height: 100px !important;
-  // 其他样式
+.hd-nav__brand-core {
+  position: relative;
+  padding: 10px 28px;
+  margin: 0 6px;
+  border: 1px solid rgba(90, 210, 255, 0.45);
+  background:
+    linear-gradient(
+      180deg,
+      rgba(18, 70, 140, 0.55) 0%,
+      rgba(6, 24, 56, 0.85) 100%
+    );
+  box-shadow:
+    0 0 24px rgba(40, 160, 255, 0.22),
+    inset 0 0 18px rgba(40, 140, 255, 0.12);
+  clip-path: polygon(18px 0, calc(100% - 18px) 0, 100% 50%, calc(100% - 18px) 100%, 18px 100%, 0 50%);
 }
 
-::v-deep .headerText {
-  font-size: 26px;
-  height: 70px;
-  // 其他样式
+.hd-nav__brand-text {
+  display: block;
+  white-space: nowrap;
+  font-size: 22px;
+  font-weight: 700;
+  letter-spacing: 2px;
+  color: #eaf9ff;
+  text-shadow:
+    0 0 10px rgba(80, 200, 255, 0.55),
+    0 2px 8px rgba(0, 0, 0, 0.35);
 }
 
-// 平板设备(小于1200px)
 @media screen and (max-width: 1650px) {
-  ::v-deep .menuParentItem {
-    width: 140px !important;
-    height: 60px !important;
+  .hd-nav {
+    gap: 6px;
   }
 
-  ::v-deep .headerText {
-    font-size: 20px;
-    height: 50px;
+  .hd-nav__item {
+    min-width: 88px;
+    height: 34px;
+    padding: 0 12px;
+    font-size: 13px;
   }
 
-  .el-submenu__title * {
-    font-size: 16px;
+  .hd-nav__brand {
+    min-width: 260px;
   }
-  .el-menu-item * {
+
+  .hd-nav__brand-core {
+    padding: 6px 18px;
+  }
+
+  .hd-nav__brand-text {
     font-size: 16px;
+    letter-spacing: 1px;
   }
-}
 
-// 非华电公司放开这个注释下面的
-.default .el-submenu__title * {
-  font-size: 16px;
-}
-.default .el-menu-item * {
-  font-size: 16px;
+  .hd-nav__brand-deco {
+    width: 20px;
+  }
 }
 </style>
 
-<!-- /* 华电时的菜单打开这个 */ -->
 <style lang="scss">
-.HD .el-menu--popup {
-  width: 100% !important;
-  min-width: 120px !important;
-  .el-menu-item {
-    width: 100% !important;
-    min-width: unset !important;
-    text-align: center;
-    padding: 0 5px !important;
-  }
+.hd-nav-dropdown.el-dropdown-menu {
+  background: rgba(6, 24, 52, 0.98) !important;
+  border: 1px solid rgba(70, 190, 255, 0.45) !important;
+  box-shadow: 0 8px 24px rgba(0, 30, 80, 0.5);
+  padding: 6px 0;
+}
+
+.hd-nav-dropdown .el-dropdown-menu__item {
+  color: #e8f7ff !important;
+  font-size: 14px;
+  line-height: 34px;
+  padding: 0 18px;
 }
-.HD .el-submenu__title * {
-  font-size: 20px;
+
+.hd-nav-dropdown .el-dropdown-menu__item:not(.is-disabled):hover,
+.hd-nav-dropdown .el-dropdown-menu__item:focus,
+.hd-nav-dropdown .el-dropdown-menu__item:not(.is-disabled):hover span,
+.hd-nav-dropdown .el-dropdown-menu__item:focus span {
+  background: linear-gradient(
+    90deg,
+    rgba(40, 140, 230, 0.85) 0%,
+    rgba(20, 90, 180, 0.9) 100%
+  ) !important;
+  color: #ffffff !important;
+  text-shadow: 0 0 8px rgba(160, 230, 255, 0.55);
 }
-.HD .el-menu-item * {
-  font-size: 20px;
+
+.hd-nav-dropdown .el-dropdown-menu__item.is-disabled {
+  color: rgba(160, 190, 210, 0.45) !important;
 }
 </style>