Quellcode durchsuchen

新增 map 底图,界面调整

linqilong vor 8 Monaten
Ursprung
Commit
b918dc8efa

Datei-Diff unterdrückt, da er zu groß ist
+ 831 - 3
package-lock.json


+ 4 - 0
package.json

@@ -16,6 +16,10 @@
     "format": "prettier --write src/"
   },
   "dependencies": {
+    "@antv/l7": "^2.22.1",
+    "@antv/l7-maps": "^2.22.1",
+    "@turf/bbox": "^7.1.0",
+    "@turf/helpers": "^7.1.0",
     "@yzfe/svgicon": "^1.2.2",
     "@yzfe/vue-svgicon": "^5.0.3",
     "pinia": "^2.2.4",

+ 8 - 0
src/.env.development

@@ -0,0 +1,8 @@
+# 页面标题
+VUE_APP_TITLE = 太湖流域水政执法知识平台
+
+# 开发环境配置
+NODE_ENV = 'development'
+
+# 知识库后台管理系统/开发环境
+VUE_APP_BASE_API = '/base_api'

+ 2 - 2
src/App.vue

@@ -1,7 +1,7 @@
 <script lang="ts" setup>
-import { RouterView } from 'vue-router'
+import {RouterView} from 'vue-router'
 </script>
 
 <template>
-  <RouterView />
+  <RouterView/>
 </template>

BIN
src/assets/images/card/header01.png


+ 4 - 1
src/assets/images/logo.svg

@@ -1 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69">
+    <path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/>
+    <path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/>
+</svg>

+ 29 - 30
src/assets/styles/index.scss

@@ -8,14 +8,13 @@ body {
   -moz-osx-font-smoothing: grayscale;
   -webkit-font-smoothing: antialiased;
   text-rendering: optimizeLegibility;
-  font-family:
-    Helvetica Neue,
-    Helvetica,
-    PingFang SC,
-    Hiragino Sans GB,
-    Microsoft YaHei,
-    Arial,
-    sans-serif;
+  font-family: Helvetica Neue,
+  Helvetica,
+  PingFang SC,
+  Hiragino Sans GB,
+  Microsoft YaHei,
+  Arial,
+  sans-serif;
 }
 
 label {
@@ -27,6 +26,16 @@ html {
   box-sizing: border-box;
 }
 
+ul {
+  margin: 0;
+  padding: 0;
+
+  li {
+    list-style: none;
+  }
+
+}
+
 #app {
   height: 100%;
 }
@@ -47,10 +56,7 @@ html {
   --tw-skew-y: 0;
   --tw-scale-x: 1;
   --tw-scale-y: 1;
-  --tw-transform: translateX(var(--tw-translate-x))
-    translateY(var(--tw-translate-y)) rotate(var(--tw-rotate))
-    skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
-    scaleY(var(--tw-scale-y));
+  --tw-transform: translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
   --tw-border-opacity: 1;
   border-color: rgba(229, 231, 235, var(--tw-border-opacity));
   --tw-ring-offset-shadow: 0 0 #0000;
@@ -65,9 +71,7 @@ html {
   --tw-saturate: var(--tw-empty,);
   --tw-sepia: var(--tw-empty,);
   --tw-drop-shadow: var(--tw-empty,);
-  --tw-filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast)
-    var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate)
-    var(--tw-sepia) var(--tw-drop-shadow);
+  --tw-filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
   --tw-backdrop-blur: var(--tw-empty,);
   --tw-backdrop-brightness: var(--tw-empty,);
   --tw-backdrop-contrast: var(--tw-empty,);
@@ -77,11 +81,7 @@ html {
   --tw-backdrop-opacity: var(--tw-empty,);
   --tw-backdrop-saturate: var(--tw-empty,);
   --tw-backdrop-sepia: var(--tw-empty,);
-  --tw-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness)
-    var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale)
-    var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert)
-    var(--tw-backdrop-opacity) var(--tw-backdrop-saturate)
-    var(--tw-backdrop-sepia);
+  --tw-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
 }
 
 .shadowed-box {
@@ -166,7 +166,7 @@ aside {
   line-height: 32px;
   font-size: 1rem;
   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
-    Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
+  Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
   color: #2c3e50;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
@@ -222,10 +222,9 @@ h5:first-child {
   position: fixed;
   inset: 0;
   z-index: 130;
-  box-shadow:
-    rgba(52, 58, 67, 0.1) 0 0 2px,
-    rgba(52, 58, 67, 0.08) 0 1px 2px,
-    rgba(52, 58, 67, 0.08) 0 1px 4px;
+  box-shadow: rgba(52, 58, 67, 0.1) 0 0 2px,
+  rgba(52, 58, 67, 0.08) 0 1px 2px,
+  rgba(52, 58, 67, 0.08) 0 1px 4px;
   border-radius: 2px;
   padding-bottom: 3px;
 }
@@ -239,11 +238,11 @@ h5:first-child {
   padding-right: 20px;
   transition: 600ms ease position;
   background: linear-gradient(
-    90deg,
-    rgba(32, 182, 249, 1) 0%,
-    rgba(32, 182, 249, 1) 0%,
-    rgba(33, 120, 241, 1) 100%,
-    rgba(33, 120, 241, 1) 100%
+                  90deg,
+                  rgba(32, 182, 249, 1) 0%,
+                  rgba(32, 182, 249, 1) 0%,
+                  rgba(33, 120, 241, 1) 100%,
+                  rgba(33, 120, 241, 1) 100%
   );
 
   .subtitle {

+ 3 - 3
src/assets/svg/convert.svg

@@ -1,5 +1,5 @@
 <svg fill="" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg">
-  <path
-    d="M0 10.353c0-1.04.843-1.882 1.882-1.882h23.96l-4.86-5.318A1.883 1.883 0 0123.76.613l7.743 8.47c1.105 1.208.247 3.152-1.389 3.152H1.882A1.882 1.882 0 010 10.353zm32 11.294c0 1.04-.843 1.882-1.882 1.882H6.158l4.86 5.318a1.883 1.883 0 01-2.778 2.541l-7.743-8.47c-1.105-1.209-.248-3.153 1.39-3.153h28.23c1.04 0 1.883.842 1.883 1.882z"
-    fill-rule="evenodd" opacity="0.9"/>
+    <path
+            d="M0 10.353c0-1.04.843-1.882 1.882-1.882h23.96l-4.86-5.318A1.883 1.883 0 0123.76.613l7.743 8.47c1.105 1.208.247 3.152-1.389 3.152H1.882A1.882 1.882 0 010 10.353zm32 11.294c0 1.04-.843 1.882-1.882 1.882H6.158l4.86 5.318a1.883 1.883 0 01-2.778 2.541l-7.743-8.47c-1.105-1.209-.248-3.153 1.39-3.153h28.23c1.04 0 1.883.842 1.883 1.882z"
+            fill-rule="evenodd" opacity="0.9"/>
 </svg>

+ 819 - 0
src/components/AntvMap/index.vue

@@ -0,0 +1,819 @@
+<script lang="ts" setup>
+import {points} from "@turf/helpers";
+import {bbox} from "@turf/bbox";
+import {LayerPopup, LineLayer, PointLayer, PolygonLayer, Popup, RasterLayer, Scene} from '@antv/l7';
+import {Map} from '@antv/l7-maps';
+// import {getLegendData, loadMapData, setCenter} from "@/utils/mapConfig";
+import {formatStringByTemplate} from "@/utils/string";
+import {copyObj} from "@/utils/ruoyi";
+import {onMounted, reactive} from "vue";
+import {getImageSrc} from "@/utils/image";
+
+let scene;
+let pointLayer;
+let pointLayerText;
+let lineLayer;
+let lineTextLayer;
+let polygonLayer;
+let polygonTextLayer;
+const token = 'a4eab5a1b0d94268669e192b8799f626';
+
+/**
+ * 初始化地图
+ */
+function init() {
+  const scene = new Scene({
+    id: 'mapDiv',
+    map: new Map({
+      center: [120.543139, 31.243133],
+      zoom: 9,
+    }),
+  });
+
+  const url1 = 'https://t0.tianditu.gov.cn/img_w/wmts?tk=b72aa81ac2b3cae941d1eb213499e15e&';
+  const layer1 = new RasterLayer().source(url1, {
+    parser: {
+      type: 'rasterTile',
+      tileSize: 256,
+      wmtsOptions: {
+        layer: 'img',
+        tileMatrixset: 'w',
+        format: 'tiles',
+      },
+    },
+  });
+
+  scene.on('loaded', () => {
+    scene.addLayer(layer1);
+  });
+
+  // scene = new Scene({
+  //   id: 'mapDiv',
+  //   map: new Map({}),
+  // });
+  // scene.on('mapmove', () => {
+  //   console.log('中心点:', scene.getCenter().lng, scene.getCenter().lat, scene.getZoom(), scene.getPitch())
+  // }); // 地图平移时触发事件
+  //
+  // // 天地图卫片地图
+  // scene.on('loaded', () => {
+  //   // 底图服务
+  //   const baseLayer = new RasterLayer({zIndex: 1})
+  //     .source(
+  //       `https://t1.tianditu.gov.cn/DataServer?T=vec_w&X={x}&Y={y}&L={z}&tk=${this.token}`,
+  //       {
+  //         parser: {
+  //           type: 'rasterTile',
+  //           tileSize: 256,
+  //         }
+  //       }
+  //     );
+  //   // 注记服务
+  //   const annotionLayer = new RasterLayer({zIndex: 2})
+  //     .source(
+  //       `https://t1.tianditu.gov.cn/DataServer?T=cva_w&X={x}&Y={y}&L={z}&tk=${this.token}`,
+  //       {
+  //         parser: {
+  //           type: 'rasterTile',
+  //           tileSize: 256,
+  //         }
+  //       }
+  //     );
+  //   scene.addLayer(baseLayer);
+  //   scene.addLayer(annotionLayer);
+  //
+  //   setCenter(scene, [120.543139, 31.243133], 8.03)
+  // });
+}
+
+function getWaterQualityTextByLevel(val, hasUnit = false) {
+  let waterQualityText = ''
+  switch (val) {
+    case '1':
+      waterQualityText = 'Ⅰ'
+      break
+    case '2':
+      waterQualityText = 'Ⅱ'
+      break
+    case '3':
+      waterQualityText = 'Ⅲ'
+      break
+    case '4':
+      waterQualityText = 'Ⅳ'
+      break
+    case '5':
+      waterQualityText = 'Ⅴ'
+      break
+    case '6':
+      waterQualityText = 'Ⅵ'
+      break
+  }
+
+  if (hasUnit && waterQualityText) {
+    waterQualityText = waterQualityText + '类'
+  }
+
+  return waterQualityText
+}
+
+
+// 图例
+const legendList = reactive([{}])
+const legendStyle = reactive({})
+
+/** 添加点(多个) */
+function addPoint(data, dataOptions = {}, nameField) {
+  // 1. 删除之前的图层
+  scene.removeAllLayer();
+
+  // 2. 添加点
+  // 点位标注
+  pointLayer = new PointLayer({})
+    .source(data, dataOptions)
+    .shape('circle')
+    .size(16)
+    .color('#004ef8')
+    .active(true)
+  scene.addLayer(pointLayer);
+  // 名称标注
+  pointLayerText = new PointLayer()
+    .source(data, dataOptions)
+    .shape(nameField, 'text')
+    .size(14)
+    .color('#000')
+    .style({
+      textOffset: [0, -50],
+      spacing: 2, // 字符间距
+      stroke: '#ffffff', // 描边颜色
+      strokeWidth: 2, // 描边宽度
+      strokeOpacity: 1.0,
+    });
+  scene.addLayer(pointLayerText);
+  // 3. 添加悬浮窗口
+  let keys = Object.keys(data[0]);
+  keys = keys.filter(k => !['经度', '纬度'].includes(k));
+  const fields = keys.map(k => {
+    return {
+      field: k,
+      formatField: () => k,
+    }
+  });
+  const layerPopup = new LayerPopup({
+    items: [
+      {
+        layer: pointLayer,
+        fields: fields,
+      },
+    ],
+  });
+  scene.addPopup(layerPopup);
+}
+
+/** 添加线(多个) */
+function addLine(data, nameField) {
+  // 1. 删除之前的图层
+  scene.removeAllLayer();
+
+  lineLayer = new LineLayer()
+    .source(data)
+    .size(5)
+    .shape('line')
+    .active(true)
+    .color('ICON', (ICON) => {
+      switch (ICON) {
+        case '1':
+          return '#1d8add'
+        case '2':
+          return '#4af905'
+        case '3':
+          return '#fed202'
+        case '4':
+          return '#f91005'
+        case '5':
+          return '#fb0891'
+        case '6':
+          return '#252627'
+        default:
+          return '#909399'
+      }
+    })
+    .style({
+      borderWidth: 0.4,
+      borderColor: '#fff',
+    });
+  scene.addLayer(lineLayer);
+
+  // 名称标注
+  const texts = []
+  data.features.forEach(d => {
+    let index = Math.ceil(d.geometry.coordinates[0].length / 2)
+    let array = d.geometry.coordinates[0][index]
+    texts.push({name: d.properties[nameField], x: array[0], y: array[1]})
+  })
+  lineTextLayer = new PointLayer()
+    .source(texts, {
+      parser: {
+        type: 'json',
+        x: 'x',
+        y: 'y',
+      },
+    })
+    .shape('name', 'text')
+    .color('#fff')
+    .size(14)
+    .style({
+      textOffset: [0, -50],
+    });
+  scene.addLayer(lineTextLayer);
+
+  // 悬浮窗
+  const fields = [
+    {field: 'HLNM', formatField: () => '河流名称'},
+    {field: 'ICON', formatField: () => '水质', formatValue: (val) => getWaterQualityTextByLevel(val, true)},
+  ];
+  const layerPopup = new LayerPopup({items: [{layer: lineLayer, fields: fields,},],});
+  scene.addPopup(layerPopup);
+}
+
+function addPolygon(data, nameField) {
+  // 1. 删除之前的图层
+  scene.removeAllLayer();
+
+  const color = [
+    'rgb(255,255,217)',
+    'rgb(237,248,177)',
+    'rgb(199,233,180)',
+    'rgb(127,205,187)',
+    'rgb(65,182,196)',
+    'rgb(29,145,192)',
+    'rgb(34,94,168)',
+    'rgb(12,44,132)',
+  ];
+  /**
+   *         .scale('用水量', {
+   *           type: 'quantile',
+   *         })
+   *         .color('用水量', color)
+   */
+  const layer = new PolygonLayer({})
+    .source(data)
+    .color('rgba(65,182,196,0.4)')
+    .shape('fill')
+    .active(true);
+  const layer2 = new LineLayer({zIndex: 2})
+    .source(data)
+    .color('#fff')
+    .active(true)
+    .size(1)
+    .style({
+      lineType: 'dash',
+      dashArray: [2, 2],
+    });
+  scene.addLayer(layer);
+  scene.addLayer(layer2);
+
+  const polygonTextLayer = new PointLayer({})
+    .source(data)
+    .shape(nameField, 'text')
+    .size(12)
+    .color('#000')
+    .style({
+      textAnchor: 'center', // 文本相对锚点的位置 center|left|right|top|bottom|top-left
+      textOffset: [0, 0], // 文本相对锚点的偏移量 [水平, 垂直]
+      spacing: 2, // 字符间距
+      padding: [1, 1], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近
+      stroke: '#ffffff', // 描边颜色
+      strokeWidth: 2, // 描边宽度
+      strokeOpacity: 1.0,
+    });
+
+  scene.addLayer(polygonTextLayer);
+
+  // layer.on('mousemove', (e) => {
+  //   const popup = new Popup({
+  //     offsets: [0, 0],
+  //     closeButton: false,
+  //   })
+  //     .setLnglat(e.lngLat)
+  //     .setHTML(
+  //       `
+  //       <span>${e.feature.properties['市']}: ${e.feature.properties['用水量']}</span>
+  //       <br/>
+  //       <span>许可水量: ${e.feature.properties['许可水量']}</span>
+  //       `
+  //     );
+  //   scene.addPopup(popup);
+  // });
+}
+
+function add3DPoint(list) {
+  const pointLayer = new PointLayer({})
+    .source(list, {
+      parser: {
+        type: 'json',
+        x: 'j',
+        y: 'w',
+      },
+    })
+    .shape('cylinder')
+    .size('t', function (level) {
+      return [2, 2, level * 2 + 20];
+    })
+    .animate(true)
+    .active(true)
+    .color('t', [
+      '#094D4A',
+      '#146968',
+      '#1D7F7E',
+      '#289899',
+      '#34B6B7',
+      '#4AC5AF',
+      '#5FD3A6',
+      '#7BE39E',
+      '#A1EDB8',
+      '#CEF8D6',
+    ]);
+  scene.addLayer(pointLayer);
+}
+
+function getScene() {
+  return scene;
+}
+
+function loadMapData(mapConfig) {
+  if (['left', 'center', 'right'].includes(mapConfig.legend.left)) {
+    switch (mapConfig.legend.left) {
+      case "left":
+        this.legendStyle.left = '10px';
+        break
+      case "center":
+        this.legendStyle.left = `calc(50% - (${this.$refs.mapLegend.offsetWidth} / 2))`;
+        break
+      case "right":
+        this.legendStyle.right = '10px';
+        break
+    }
+  } else if (mapConfig.legend.left.startsWith('-')) {
+    this.legendStyle.right = mapConfig.legend.left.replace('-', '')
+  } else {
+    this.legendStyle.left = mapConfig.legend.left
+  }
+
+  if (['top', 'center', 'bottom'].includes(mapConfig.legend.top)) {
+    switch (mapConfig.legend.top) {
+      case "top":
+        this.legendStyle.top = '10px';
+        break
+      case "center":
+        this.legendStyle.top = `calc(50% - (${this.$refs.mapLegend.offsetHeight} / 2))`;
+        break
+      case "bottom":
+        this.legendStyle.bottom = '10px';
+        break
+    }
+  } else if (mapConfig.legend.top.startsWith('-')) {
+    this.legendStyle.bottom = mapConfig.legend.top.replace('-', '')
+  } else {
+    this.legendStyle.top = mapConfig.legend.top
+  }
+
+  // this.legendList = getLegendData(mapConfig)
+}
+
+function fitBounds(data) {
+  const features = points(data);
+  const bboxs = bbox(features);
+
+  let lttddd = (bboxs[3] - bboxs[1]) / 8 // 计算缩放级别
+  let lgtddd = (bboxs[2] - bboxs[0]) / 8 // 计算缩放级别
+  scene.fitBounds([[bboxs[0] - lgtddd, bboxs[1] - lttddd], [bboxs[2] + lgtddd, bboxs[3] + lttddd]]);
+}
+
+/** 设置事件节点 */
+function setEventData(events) {
+  const _self = this
+  // 1. 删除之前的图层
+  scene.removeAllLayer();
+
+  const sourceOptions = {
+    parser: {
+      type: 'json',
+      x: 'x',
+      y: 'y',
+    },
+  }
+
+  const imageSet = new Set()
+  events.forEach(event => {
+    if (event.image) {
+      imageSet.add(event.image)
+    }
+  })
+  imageSet.forEach(image => {
+    scene.addImage(
+      image,
+      getImageSrc(image)
+    );
+  })
+
+  const imageEvents = events.filter(event => !!event.image)
+  const imagePointLayer = new PointLayer()
+    .source(imageEvents, sourceOptions)
+    .size(16)
+    .shape('image', Array.of(imageSet))
+    .style({
+      opacity: 0.8,
+      strokeWidth: 4
+    })
+    .active(true);
+  scene.addLayer(imagePointLayer);
+
+  const shapeEvents = events.filter(event => !event.image)
+  const shapePointLayer = new PointLayer()
+    .source(shapeEvents, sourceOptions)
+    .size(16)
+    .shape('shape')
+    .color('color')
+    .style({
+      opacity: 0.8,
+      strokeWidth: 4
+    })
+    .active(true);
+  scene.addLayer(shapePointLayer);
+
+
+  // 设置标签
+  pointLayerText = new PointLayer()
+    .source(events, sourceOptions)
+    .shape('name', 'text')
+    .size(12)
+    .color('#000')
+    .style({
+      textAnchor: 'center', // 文本相对锚点的位置 center|left|right|top|bottom|top-left
+      textOffset: [0, -60], // 文本相对锚点的偏移量 [水平, 垂直]
+      spacing: 2, // 字符间距
+      padding: [1, 1], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近
+      stroke: '#fff', // 描边颜色
+      strokeWidth: 2, // 描边宽度
+      strokeOpacity: 1.0,
+    });
+  scene.addLayer(pointLayerText);
+
+  // 显示弹窗
+  const showPopup = (e) => {
+    let data = e.feature.data
+    const template =
+      '<div class="entry-card-component"><section class="entry-base-info">' +
+      '<section class="base-info"><div class="entry-avatar"><img alt="" src="{{imgUrl}}"></div><div class="entry-text-info"><div class="entry-name">{{name}}</div><div class="entry-desc">{{summary}}</div></div></section></section>' +
+      '<section class="entry-nodes"><div class="nodes-list"><div class="node-item" data-id="{{id}}">' +
+      '<div class="title">{{source.title}}</div>' +
+      '<div class="sub-title"><div class="time">{{source.dateString}}</div>' +
+      '<div class="address">{{source.locationDesc}}</div></div>' +
+      '<div class="desc">{{source.description}}</div></div></div></section></div>'
+    const content = formatStringByTemplate(data, template)
+    const popup = new Popup({maxWidth: '36vw', autoClose: true})
+      .setLnglat(e.lngLat)
+      .setHTML(content);
+    scene.addPopup(popup);
+
+    // 确保在 DOM 完全更新后执行
+    setTimeout(() => {
+      const eventItems = document.getElementsByClassName('node-item');
+      // 遍历所有 node-item 元素
+      Array.from(eventItems).forEach(item => {
+        item.addEventListener('click', (event) => {
+          // const id = event.currentTarget?.dataset?.id;
+          // _self.$router.push({path: `/geoTemporalSearch/detail/${id}`});
+        });
+      });
+    }, 0); // 使用 setTimeout 来确保 DOM 已经更新
+  };
+
+  imagePointLayer.on('click', showPopup);
+  shapePointLayer.on('click', showPopup);
+}
+
+function setEventPoint(events) {
+  const _self = this
+  // 1. 删除之前的图层
+  scene.removeAllLayer();
+
+  const sourceOptions = {
+    parser: {
+      type: 'json',
+      x: 'x',
+      y: 'y',
+    },
+  }
+  const pointLayer = new PointLayer()
+    .source(events, sourceOptions)
+    .size(12)
+    .shape('simple')
+    .color('#fe7331')
+    .style({
+      strokeWidth: 4
+    })
+    .active(false);
+  scene.addLayer(pointLayer);
+
+  // 设置标签
+  pointLayerText = new PointLayer()
+    .source(events, sourceOptions)
+    .shape('title', 'text')
+    .size(12)
+    .color('#000')
+    .style({
+      textAnchor: 'center', // 文本相对锚点的位置 center|left|right|top|bottom|top-left
+      textOffset: [0, -60], // 文本相对锚点的偏移量 [水平, 垂直]
+      spacing: 2, // 字符间距
+      padding: [1, 1], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近
+      stroke: '#fff', // 描边颜色
+      strokeWidth: 2, // 描边宽度
+      strokeOpacity: 1.0,
+    });
+  scene.addLayer(pointLayerText);
+}
+
+function setEventPointDetail(events) {
+  const allPoints = copyObj(events)
+  // 1. 删除之前的图层
+  scene.removeAllLayer();
+
+  let mainEvent;
+  let index = events.findIndex(item => item.pointType === 'main');
+  if (index > -1) {
+    mainEvent = events.splice(index, 1); // 移除找到的第一个元素  l
+  } else {
+    return
+  }
+
+  const sourceOptions = {
+    parser: {
+      type: 'json',
+      x: 'x',
+      y: 'y',
+    },
+  }
+  const pointLayer = new PointLayer()
+    .source(events, sourceOptions)
+    .size(12)
+    .shape('simple')
+    .color('#fe7331')
+    .style({
+      strokeWidth: 4
+    })
+    .active(false);
+  scene.addLayer(pointLayer);
+
+  const mainPointLayer = new PointLayer()
+    .source(mainEvent, sourceOptions)
+    .size(20)
+    .shape('simple')
+    .color('#fe7331')
+    .style({
+      strokeWidth: 4
+    })
+    .active(false);
+  scene.addLayer(mainPointLayer);
+
+  // 设置标签
+  pointLayerText = new PointLayer()
+    .source(allPoints, sourceOptions)
+    .shape('title', 'text')
+    .size(12)
+    .color('#000')
+    .style({
+      textAnchor: 'center', // 文本相对锚点的位置 center|left|right|top|bottom|top-left
+      textOffset: [0, -60], // 文本相对锚点的偏移量 [水平, 垂直]
+      spacing: 2, // 字符间距
+      padding: [1, 1], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近
+      stroke: '#fff', // 描边颜色
+      strokeWidth: 2, // 描边宽度
+      strokeOpacity: 1.0,
+    });
+  scene.addLayer(pointLayerText);
+
+  for (let e of events) {
+    let data = []
+    if (e.pointType === 'previous') {
+      data.push({lng: e.x, lat: e.y, lng1: mainEvent[0].x, lat1: mainEvent[0].y})
+    } else {
+      data.push({lng: mainEvent[0].x, lat: mainEvent[0].y, lng1: e.x, lat1: e.y})
+    }
+
+    const lineLayer = new LineLayer()
+      .source(data, {
+        parser: {
+          type: 'json',
+          x: 'lng',
+          y: 'lat',
+          x1: 'lng1',
+          y1: 'lat1',
+        },
+      })
+      .shape('line')
+      .size(2)
+      .color('#f00')
+      .animate({
+        duration: 2,
+        interval: 1,
+        trailLength: 1,
+      })
+      .style({
+        lineType: 'dash', // 设置虚线类型
+        // dashArray: [5, 5], // 设置虚线的间隔和长度
+      });
+    scene.addLayer(lineLayer);
+  }
+
+  pointLayer.on('click', e => {
+    if (e.feature?.id) {
+      this.$emit('updateEventId', e.feature?.id)
+    }
+  });
+}
+
+onMounted(() => {
+  init()
+})
+</script>
+
+<template>
+  <div class="gw-map">
+    <!-- 2D地图 -->
+    <div id="mapDiv"></div>
+    <!--    <div v-if="legendList && legendList.length > 0" ref="mapLegend" :style="legendStyle" class="map-legend">-->
+    <!--      <ul>-->
+    <!--        <li v-for="(item, index) in legendList" :key="index">-->
+    <!--          <span :title="item.label">{{ item.label }}</span>-->
+    <!--          <template v-if="item.imgType === 'svg'">-->
+    <!--            <svg-icon :icon-class="item.src" :style="item.imgStyle" class="image"></svg-icon>-->
+    <!--          </template>-->
+    <!--          <template v-if="item.imgType === 'img'">-->
+    <!--            <el-image :src="item.src" :style="item.imgStyle" class="image"></el-image>-->
+    <!--          </template>-->
+    <!--        </li>-->
+    <!--      </ul>-->
+    <!--    </div>-->
+  </div>
+</template>
+<style lang="scss" scoped>
+.gw-map {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  pointer-events: auto;
+}
+
+#mapDiv {
+  width: 100%;
+  height: 100%;
+  z-index: 1;
+  opacity: 1;
+  overflow: hidden;
+  filter: saturate(1) contrast(1) hue-rotate(0deg) brightness(1);
+  transform: rotateZ(0deg) rotateX(0deg) rotateY(0deg) skewX(0deg) skewY(0deg);
+}
+
+.map-legend {
+  position: absolute;
+  bottom: 0;
+  right: 0;
+  z-index: 100;
+  background-color: #f2f2f2;
+  border-radius: 8px;
+  padding: 5px 10px;
+
+  ul {
+    margin: 0;
+    padding: 0;
+
+    li {
+      margin: 5px 0;
+      list-style: none;
+      display: flex;
+      justify-content: flex-end;
+      align-items: center;
+
+      span {
+        text-align: right;
+        width: 7vw;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+
+      .image {
+        margin-left: 5px;
+        width: 1rem;
+        height: 1rem;
+      }
+
+    }
+  }
+
+}
+
+:deep(.l7-control-logo) {
+  display: none;
+}
+
+:deep(.esri-view-surface) {
+  &:after {
+    display: none;
+  }
+}
+
+</style>
+<style lang="scss">
+.entry-card-component {
+  .entry-base-info {
+
+    .base-info {
+      display: flex;
+      margin-bottom: 14px;
+
+      .entry-avatar {
+        width: 90px;
+        margin-right: 8px;
+        height: 108px;
+
+        img {
+          width: 100%;
+          height: 100%;
+          object-fit: cover;
+        }
+
+      }
+
+      .entry-text-info {
+        flex: 1;
+
+        .entry-name {
+          font-size: 16px;
+          line-height: 22px;
+          font-weight: 600;
+          margin-bottom: 10px;
+          margin-right: 24px;
+        }
+
+        .entry-desc {
+          line-height: 20px;
+          font-size: 12px;
+          word-break: break-word;
+          display: -webkit-box;
+          height: 80px;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          -webkit-line-clamp: 4;
+        }
+      }
+    }
+
+
+  }
+
+  .entry-nodes {
+    .nodes-list {
+      border-top: 1px solid #ebebeb;
+      padding-top: 7px;
+
+      .node-item {
+        cursor: pointer;
+
+        .title {
+          word-break: break-word;
+          display: -webkit-box;
+          -webkit-box-orient: vertical;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          -webkit-line-clamp: 2;
+        }
+
+        .sub-title {
+          display: flex;
+          margin-bottom: 8px;
+          color: #999;
+          line-height: 14px;
+
+          .time {
+            margin-right: 4px;
+          }
+
+          .address {
+            overflow: hidden;
+          }
+        }
+
+        .desc {
+          font-size: 12px;
+          color: #666;
+          line-height: 20px;
+          max-height: 40px;
+        }
+      }
+
+    }
+  }
+}
+</style>

+ 3 - 3
src/components/HelloWorld.vue

@@ -1,4 +1,4 @@
-<script setup lang="ts">
+<script lang="ts" setup>
 defineProps<{
   msg: string
 }>()
@@ -9,8 +9,8 @@ defineProps<{
     <h1 class="green">{{ msg }}</h1>
     <h3>
       You’ve successfully created a project with
-      <a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
-      <a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
+      <a href="https://vite.dev/" rel="noopener" target="_blank">Vite</a> +
+      <a href="https://vuejs.org/" rel="noopener" target="_blank">Vue 3</a>.
       What's next?
     </h3>
   </div>

+ 5 - 1
src/components/RightFrame.vue

@@ -115,6 +115,10 @@ watch(
     height: calc(100%);
     min-width: 300px;
 
+    &:hover .gw-sidebar-btn {
+      display: flex;
+    }
+
     .gw-sidebar-container {
       display: flex;
       flex-direction: column;
@@ -129,7 +133,7 @@ watch(
       background-color: #2e6ca6;
       height: 40px;
       width: 40px;
-      display: flex;
+      display: none;
       justify-content: center;
       align-items: center;
 

+ 36 - 36
src/components/TheWelcome.vue

@@ -1,4 +1,4 @@
-<script setup lang="ts">
+<script lang="ts" setup>
 import WelcomeItem from './WelcomeItem.vue'
 import DocumentationIcon from './icons/IconDocumentation.vue'
 import ToolingIcon from './icons/IconTooling.vue'
@@ -10,112 +10,112 @@ import SupportIcon from './icons/IconSupport.vue'
 <template>
   <WelcomeItem>
     <template #icon>
-      <DocumentationIcon />
+      <DocumentationIcon/>
     </template>
     <template #heading>Documentation</template>
 
     Vue’s
-    <a href="https://vuejs.org/" target="_blank" rel="noopener"
-      >official documentation</a
+    <a href="https://vuejs.org/" rel="noopener" target="_blank"
+    >official documentation</a
     >
     provides you with all information you need to get started.
   </WelcomeItem>
 
   <WelcomeItem>
     <template #icon>
-      <ToolingIcon />
+      <ToolingIcon/>
     </template>
     <template #heading>Tooling</template>
 
     This project is served and bundled with
     <a
       href="https://vite.dev/guide/features.html"
-      target="_blank"
       rel="noopener"
-      >Vite</a
+      target="_blank"
+    >Vite</a
     >. The recommended IDE setup is
-    <a href="https://code.visualstudio.com/" target="_blank" rel="noopener"
-      >VSCode</a
+    <a href="https://code.visualstudio.com/" rel="noopener" target="_blank"
+    >VSCode</a
     >
     +
     <a
       href="https://github.com/johnsoncodehk/volar"
-      target="_blank"
       rel="noopener"
-      >Volar</a
+      target="_blank"
+    >Volar</a
     >. If you need to test your components and web pages, check out
-    <a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
+    <a href="https://www.cypress.io/" rel="noopener" target="_blank">Cypress</a>
     and
-    <a href="https://on.cypress.io/component" target="_blank" rel="noopener"
-      >Cypress Component Testing</a
+    <a href="https://on.cypress.io/component" rel="noopener" target="_blank"
+    >Cypress Component Testing</a
     >.
 
-    <br />
+    <br/>
 
     More instructions are available in <code>README.md</code>.
   </WelcomeItem>
 
   <WelcomeItem>
     <template #icon>
-      <EcosystemIcon />
+      <EcosystemIcon/>
     </template>
     <template #heading>Ecosystem</template>
 
     Get official tools and libraries for your project:
-    <a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
-    <a href="https://router.vuejs.org/" target="_blank" rel="noopener"
-      >Vue Router</a
+    <a href="https://pinia.vuejs.org/" rel="noopener" target="_blank">Pinia</a>,
+    <a href="https://router.vuejs.org/" rel="noopener" target="_blank"
+    >Vue Router</a
     >,
-    <a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener"
-      >Vue Test Utils</a
+    <a href="https://test-utils.vuejs.org/" rel="noopener" target="_blank"
+    >Vue Test Utils</a
     >, and
-    <a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener"
-      >Vue Dev Tools</a
+    <a href="https://github.com/vuejs/devtools" rel="noopener" target="_blank"
+    >Vue Dev Tools</a
     >. If you need more resources, we suggest paying
     <a
       href="https://github.com/vuejs/awesome-vue"
-      target="_blank"
       rel="noopener"
-      >Awesome Vue</a
+      target="_blank"
+    >Awesome Vue</a
     >
     a visit.
   </WelcomeItem>
 
   <WelcomeItem>
     <template #icon>
-      <CommunityIcon />
+      <CommunityIcon/>
     </template>
     <template #heading>Community</template>
 
     Got stuck? Ask your question on
-    <a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a
+    <a href="https://chat.vuejs.org" rel="noopener" target="_blank">Vue Land</a
     >, our official Discord server, or
     <a
       href="https://stackoverflow.com/questions/tagged/vue.js"
-      target="_blank"
       rel="noopener"
-      >StackOverflow</a
+      target="_blank"
+    >StackOverflow</a
     >. You should also subscribe to
-    <a href="https://news.vuejs.org" target="_blank" rel="noopener"
-      >our mailing list</a
+    <a href="https://news.vuejs.org" rel="noopener" target="_blank"
+    >our mailing list</a
     >
     and follow the official
-    <a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
-      >@vuejs</a
+    <a href="https://twitter.com/vuejs" rel="noopener" target="_blank"
+    >@vuejs</a
     >
     twitter account for latest news in the Vue world.
   </WelcomeItem>
 
   <WelcomeItem>
     <template #icon>
-      <SupportIcon />
+      <SupportIcon/>
     </template>
     <template #heading>Support Vue</template>
 
     As an independent project, Vue relies on community backing for its
     sustainability. You can help us by
-    <a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener"
-      >becoming a sponsor</a
+    <a href="https://vuejs.org/sponsor/" rel="noopener" target="_blank"
+    >becoming a sponsor</a
     >.
   </WelcomeItem>
 </template>

+ 3 - 3
src/components/__tests__/HelloWorld.spec.ts

@@ -1,11 +1,11 @@
-import { describe, it, expect } from 'vitest'
+import {describe, expect, it} from 'vitest'
 
-import { mount } from '@vue/test-utils'
+import {mount} from '@vue/test-utils'
 import HelloWorld from '../HelloWorld.vue'
 
 describe('HelloWorld', () => {
   it('renders properly', () => {
-    const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
+    const wrapper = mount(HelloWorld, {props: {msg: 'Hello Vitest'}})
     expect(wrapper.text()).toContain('Hello Vitest')
   })
 })

+ 64 - 0
src/components/card/Card01.vue

@@ -0,0 +1,64 @@
+<script lang="ts" setup>
+defineProps<{
+  title: { type: string, required: false },
+  customClass: { type: string, required: false },
+}>()
+
+import header from "@/assets/images/card/header01.png";
+</script>
+
+<template>
+  <div :class="[customClass]" class="card-one">
+    <div class="card-one-header">
+      <img :src="header" alt=""/>
+      <span class="title" v-html="title"></span>
+      <slot name="headerTool"></slot>
+    </div>
+    <div class="card-one-body">
+      <slot></slot>
+    </div>
+  </div>
+</template>
+<style lang="scss" scoped>
+.card-one {
+  color: #fff;
+
+  .card-one-header {
+    position: relative;
+    height: 2.85rem;
+    line-height: 2.85rem;
+    font-size: 1.2rem;
+    font-weight: 600;
+
+    img {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      top: 0;
+      left: 0;
+    }
+
+    .title {
+      position: relative;
+      padding-left: 6%;
+    }
+  }
+
+  .card-one-body {
+    width: 100%;
+    padding: 10px;
+    position: relative;
+    background-color: rgba(3, 37, 77, 0.8);
+    border: 1px transparent solid;
+  }
+
+}
+
+.card-one + .card-one {
+  margin-top: 5px;
+}
+
+.card-one.merge + .card-one.merge {
+  margin-top: 0;
+}
+</style>

+ 132 - 0
src/components/card/Card02.vue

@@ -0,0 +1,132 @@
+<script lang="ts" setup>
+defineProps<{
+  title: { type: string, required: false },
+  customClass: { type: string, required: false },
+}>()
+</script>
+
+<template>
+  <div :class="[customClass]" class="card-one">
+    <div class="card-one-header">
+      <span class="title-box" v-html="title"></span>
+      <slot name="headerTool"></slot>
+    </div>
+    <div class="card-one-body">
+      <slot></slot>
+    </div>
+  </div>
+</template>
+<style lang="scss" scoped>
+.card-one {
+  background-image: linear-gradient(180deg,
+    rgba(0, 28, 53, 0.8) 0%,
+    rgba(11, 45, 85, 0.8) 100%);
+  border: 2px solid hsla(0, 0%, 89.8%, .31);
+  border-radius: 12px;
+  min-width: 418px;
+
+  .card-one-header {
+    border-radius: 12px 12px 0 0;
+    background-image: linear-gradient(180deg,
+      rgba(0, 28, 53, 1) 0%,
+      rgba(11, 45, 85, 1) 100%);
+    height: 2.85rem;
+    line-height: 2.85rem;
+    font-size: 1.2rem;
+    font-weight: 600;
+    color: #fff;
+    display: flex;
+    align-items: center;
+
+    .title-box {
+      flex: 1;
+    }
+
+    .title-box:before {
+      display: inline-block;
+      content: "";
+      width: .5rem;
+      height: 1.4rem;
+      background: #0cf;
+      vertical-align: top;
+      margin: .7rem;
+    }
+
+    .time {
+      position: absolute;
+      right: 1rem;
+      top: -4px;
+    }
+  }
+
+  .card-one-body {
+    width: 100%;
+    height: calc(100% - 2.85rem);
+    position: relative;
+
+    ul {
+      margin: 0;
+      padding: 0;
+
+      li {
+        list-style: none;
+        padding: 0 10px 10px 10px;
+      }
+
+    }
+
+  }
+
+  &.merge {
+    border-top: none;
+    border-bottom: none;
+    border-radius: 0;
+
+    .card-one-header {
+      border-radius: 0;
+    }
+
+    &:first-child {
+      border-radius: 12px 12px 0 0;
+      border-top: 2px solid hsla(0, 0%, 89.8%, .31);
+
+      .card-one-header {
+        border-radius: 12px 12px 0 0;
+      }
+    }
+
+  }
+
+  &:last-child {
+    &.merge {
+      border-radius: 0 0 12px 12px;
+      border-bottom: 2px solid hsla(0, 0%, 89.8%, .31);
+    }
+  }
+}
+
+.card-one + .card-one {
+  margin-top: 5px;
+}
+
+.card-one.merge + .card-one.merge {
+  margin-top: 0;
+}
+
+.card-one-body {
+  .dv-scroll-board {
+    .header {
+      color: #8ec8f8;
+      font-size: 1rem;
+    }
+
+    .rows {
+      .row-item {
+        color: #CEF0FF;
+        font-size: 1rem;
+      }
+    }
+  }
+}
+
+</style>

+ 3 - 3
src/components/icons/IconCommunity.vue

@@ -1,9 +1,9 @@
 <template>
   <svg
-    xmlns="http://www.w3.org/2000/svg"
-    width="20"
-    height="20"
     fill="currentColor"
+    height="20"
+    width="20"
+    xmlns="http://www.w3.org/2000/svg"
   >
     <path
       d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"

+ 3 - 3
src/components/icons/IconDocumentation.vue

@@ -1,9 +1,9 @@
 <template>
   <svg
-    xmlns="http://www.w3.org/2000/svg"
-    width="20"
-    height="17"
     fill="currentColor"
+    height="17"
+    width="20"
+    xmlns="http://www.w3.org/2000/svg"
   >
     <path
       d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"

+ 3 - 3
src/components/icons/IconEcosystem.vue

@@ -1,9 +1,9 @@
 <template>
   <svg
-    xmlns="http://www.w3.org/2000/svg"
-    width="18"
-    height="20"
     fill="currentColor"
+    height="20"
+    width="18"
+    xmlns="http://www.w3.org/2000/svg"
   >
     <path
       d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"

+ 3 - 3
src/components/icons/IconSupport.vue

@@ -1,9 +1,9 @@
 <template>
   <svg
-    xmlns="http://www.w3.org/2000/svg"
-    width="20"
-    height="20"
     fill="currentColor"
+    height="20"
+    width="20"
+    xmlns="http://www.w3.org/2000/svg"
   >
     <path
       d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"

+ 3 - 4
src/components/icons/IconTooling.vue

@@ -1,15 +1,14 @@
 <!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
 <template>
   <svg
-    xmlns="http://www.w3.org/2000/svg"
-    xmlns:xlink="http://www.w3.org/1999/xlink"
     aria-hidden="true"
-    role="img"
     class="iconify iconify--mdi"
-    width="24"
     height="24"
     preserveAspectRatio="xMidYMid meet"
+    role="img"
     viewBox="0 0 24 24"
+    width="24"
+    xmlns="http://www.w3.org/2000/svg"
   >
     <path
       d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"

+ 2 - 9
src/layout/components/AppMain.vue

@@ -1,18 +1,11 @@
 <template>
   <section ref="appMain" class="app-main">
-    <router-view :key="route.path" />
+    <router-view :key="route.path"/>
   </section>
 </template>
 
 <script lang="ts" setup>
-import { RouterView, useRoute } from 'vue-router'
+import {RouterView, useRoute} from 'vue-router'
 
 let route = useRoute()
 </script>
-
-<style lang="scss" scoped>
-.app-main {
-  width: 100%;
-  height: 100%;
-}
-</style>

+ 11 - 14
src/layout/components/Navbar.vue

@@ -1,7 +1,7 @@
 <script lang="ts" setup>
-import { onMounted } from 'vue'
-import { getToken } from '@/utils/auth'
-import { jumpPage } from '@/utils'
+import {onMounted} from 'vue'
+import {getToken} from '@/utils/auth'
+import {jumpPage} from '@/utils'
 
 let navbarBackgroundImage = new URL(
   '@/assets/images/layout/header-background.png',
@@ -86,7 +86,7 @@ function logout() {
     class="navbar"
   >
     <div class="index" @click="jumpPage('/')">
-      <img :alt="title" :src="navbarTitleImage" />
+      <img :alt="title" :src="navbarTitleImage"/>
     </div>
   </div>
 </template>
@@ -96,17 +96,14 @@ function logout() {
   background-size: 100%;
   position: relative;
   color: #fff;
+  pointer-events: auto;
 
-  .index {
-    width: 100%;
-    height: 70%;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-
-    img {
-      width: 20%;
-    }
+  img {
+    width: 20%;
+    position: absolute;
+    top: 2vh;
+    left: 50%;
+    transform: translateX(-50%);
   }
 
 }

+ 2 - 2
src/layout/components/index.js

@@ -1,2 +1,2 @@
-export { default as AppMain } from './AppMain.vue'
-export { default as Navbar } from './Navbar.vue'
+export {default as AppMain} from './AppMain.vue'
+export {default as Navbar} from './Navbar.vue'

+ 95 - 6
src/layout/index.vue

@@ -1,20 +1,33 @@
 <script lang="ts" setup>
-import { AppMain, Navbar } from '@/layout/components/index.js'
+import {AppMain, Navbar} from '@/layout/components/index.js'
+import AntvMap from '@/components/AntvMap/index.vue'
 
-let navbarHeight = '10vh'
+let navbarHeight = '12vh'
 
 let theme = ''
-let navbarStyle = { height: navbarHeight }
+let navbarStyle = {height: navbarHeight}
 </script>
 
 <template>
   <div :style="{ '--current-color': theme }" class="app-wrapper">
+
+    <antv-map class="floor-container"></antv-map>
+
+    <!-- 顶部阴影 -->
+    <div class="header-shade"></div>
+    <!-- 底部阴影 -->
+    <!--    <div class="bottom-shade"></div>-->
+    <!-- 左侧阴影 -->
+    <div class="left-shade"></div>
+    <!-- 右侧阴影 -->
+    <div class="right-shade"></div>
+
     <!-- 主页 -->
     <div class="main-container">
       <!-- 顶部 -->
-      <navbar :style="navbarStyle" />
+      <navbar :style="navbarStyle"/>
       <!-- 展示页 -->
-      <app-main :style="{ height: `calc(100% - ${navbarHeight})` }" />
+      <app-main/>
     </div>
   </div>
 </template>
@@ -23,10 +36,13 @@ let navbarStyle = { height: navbarHeight }
 @use '@/assets/styles/mixin.scss';
 @use '@/assets/styles/variables.scss';
 
+$base-background-color: rgba(10, 117, 195, 0.6);
+$base-background-color2: rgba(10, 117, 195, 1);
+
 .app-wrapper {
   height: 100%;
   width: 100%;
-  background: #000;
+  pointer-events: none;
 
   .main-container {
     // background: linear-gradient(120deg, #6236ff 0%, #6236ff 0%, #32c5ff 99%, #32c5ff 100%);
@@ -38,6 +54,79 @@ let navbarStyle = { height: navbarHeight }
       width: 100%;
       background-image: linear-gradient(132deg, #4791f8 0%, #2d3bdb 100%);
     }
+
+    .app-main {
+      position: absolute;
+      top: 8vh;
+      left: 0;
+      width: 100%;
+      height: 92vh;
+    }
+  }
+
+  .floor-container {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+  }
+
+  .header-shade {
+    width: 100%;
+    height: 8vh;
+    top: 0;
+    left: 0;
+    background-image: linear-gradient(180deg,
+      $base-background-color2 0%,
+      rgba(0, 0, 0, 0) 100%);
+
   }
+
+  .bottom-shade {
+    width: 100%;
+    height: 8vh;
+    bottom: 0;
+    left: 0;
+    background-image: linear-gradient(0deg,
+      $base-background-color 0%,
+      rgba(0, 0, 0, 0) 100%);
+
+  }
+
+  .left-shade {
+    top: 0;
+    left: 0;
+    background-image: linear-gradient(90deg,
+      $base-background-color 0%,
+      rgba(0, 0, 0, 0) 100%);
+  }
+
+  .right-shade {
+    top: 0;
+    right: 0;
+    background-image: linear-gradient(270deg,
+      $base-background-color 0%,
+      rgba(0, 0, 0, 0) 100%);
+  }
+
+  .left-shade,
+  .right-shade {
+    width: 25vw;
+    height: 100%;
+  }
+
+  .header-shade,
+  .bottom-shade,
+  .left-shade,
+  .right-shade {
+    pointer-events: none;
+    //z-index: 1;
+    position: absolute;
+    background-size: 100% 100%;
+    background-repeat: no-repeat;
+    opacity: 1;
+  }
+
 }
 </style>

+ 4 - 4
src/layout/mixin/ResizeHandler.js

@@ -1,13 +1,13 @@
 import store from '@/store'
 
-const { body } = document
+const {body} = document
 const WIDTH = 992 // refer to Bootstrap's responsive design
 
 export default {
   watch: {
     $route(route) {
       if (this.device === 'mobile' && this.sidebar.opened) {
-        store.dispatch('app/closeSideBar', { withoutAnimation: false })
+        store.dispatch('app/closeSideBar', {withoutAnimation: false})
       }
     },
   },
@@ -21,7 +21,7 @@ export default {
     const isMobile = this.$_isMobile()
     if (isMobile) {
       store.dispatch('app/toggleDevice', 'mobile')
-      store.dispatch('app/closeSideBar', { withoutAnimation: true })
+      store.dispatch('app/closeSideBar', {withoutAnimation: true})
     }
   },
   methods: {
@@ -37,7 +37,7 @@ export default {
         store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
 
         if (isMobile) {
-          store.dispatch('app/closeSideBar', { withoutAnimation: true })
+          store.dispatch('app/closeSideBar', {withoutAnimation: true})
         }
       }
     },

+ 4 - 4
src/main.ts

@@ -1,9 +1,9 @@
 import './assets/styles/index.scss'
 
-import { createApp } from 'vue'
-import { createPinia } from 'pinia'
+import {createApp} from 'vue'
+import {createPinia} from 'pinia'
 import VueCookies from 'vue3-cookies'
-import { VueSvgIconPlugin } from '@yzfe/vue-svgicon'
+import {VueSvgIconPlugin} from '@yzfe/vue-svgicon'
 import '@yzfe/svgicon/lib/svgicon.css'
 
 // @ts-ignore
@@ -15,7 +15,7 @@ const app = createApp(App)
 app.use(createPinia())
 app.use(router)
 app.use(VueCookies)
-app.use(VueSvgIconPlugin, { tagName: 'icon' })
+app.use(VueSvgIconPlugin, {tagName: 'icon'})
 
 // 添加到vue对象上,可以在全局通过 this.$cookies来调用
 app.config.globalProperties.$cookies = VueCookies

+ 2 - 2
src/router/index.ts

@@ -1,4 +1,4 @@
-import { createRouter, createWebHistory } from 'vue-router'
+import {createRouter, createWebHistory} from 'vue-router'
 import layout from '@/layout/index.vue'
 
 const router = createRouter({
@@ -12,7 +12,7 @@ const router = createRouter({
         {
           path: 'index',
           name: 'home',
-          component: () => import('@/views/HomeView.vue'),
+          component: () => import('@/views/Home.vue'),
         },
       ],
     },

+ 4 - 3
src/stores/counter.ts

@@ -1,12 +1,13 @@
-import { ref, computed } from 'vue'
-import { defineStore } from 'pinia'
+import {computed, ref} from 'vue'
+import {defineStore} from 'pinia'
 
 export const useCounterStore = defineStore('counter', () => {
   const count = ref(0)
   const doubleCount = computed(() => count.value * 2)
+
   function increment() {
     count.value++
   }
 
-  return { count, doubleCount, increment }
+  return {count, doubleCount, increment}
 })

+ 3 - 3
src/utils/auth.ts

@@ -3,16 +3,16 @@ import useCurrentInstance from '@/utils/useCurrentInstance'
 const TokenKey = 'Admin-Token'
 
 export function getToken() {
-  const { proxy } = useCurrentInstance()
+  const {proxy} = useCurrentInstance()
   return proxy.$cookies.get(TokenKey)
 }
 
 export function setToken(token: string) {
-  const { proxy } = useCurrentInstance()
+  const {proxy} = useCurrentInstance()
   return proxy.$cookies.set(TokenKey, token)
 }
 
 export function removeToken() {
-  const { proxy } = useCurrentInstance()
+  const {proxy} = useCurrentInstance()
   return proxy.$cookies.remove(TokenKey)
 }

+ 67 - 0
src/utils/image.ts

@@ -0,0 +1,67 @@
+import {validURL} from "@/utils/validate";
+
+/**
+ * 图片转圆形图片
+ * @param {图片地址} imageSrc
+ */
+export function circularPicture(imageSrc: string) {
+  const fun = (resolve: any) => {
+    const canvas = document.createElement("canvas");
+    const contex = canvas.getContext("2d") as CanvasRenderingContext2D;
+    const img = new Image();
+    img.crossOrigin = "";
+
+    img.onload = () => {
+      let center = {
+        x: img.width / 2,
+        y: img.height / 2.5,
+      };
+      let diameter = img.width;
+      canvas.width = diameter;
+      canvas.height = diameter;
+      contex.clearRect(0, 0, diameter, diameter);
+      contex.save();
+      contex.beginPath();
+      let radius = img.width / 2;
+      contex.arc(radius, radius, radius, 0, 2 * Math.PI); //画出圆
+      contex.clip(); //裁剪上面的圆形
+      contex.drawImage(
+        img,
+        center.x - radius,
+        center.y - radius,
+        diameter,
+        diameter,
+        0,
+        0,
+        diameter,
+        diameter
+      ); // 在刚刚裁剪的园上画图
+      contex.restore(); // 还原状态
+      resolve(canvas.toDataURL("image/png", 1));
+    };
+    img.src = imageSrc;
+  };
+  return new Promise(fun);
+}
+
+/**
+ * 查询图片地址
+ * @param imageSrc 图片地址
+ * @param baseApi 后台基础路径
+ * @returns {*|string}
+ */
+export function getImageSrc(imageSrc: string) {
+  const baseApi = process.env.VUE_APP_BASE_API as string
+
+  if (!imageSrc) {
+    return imageSrc
+  } else if (validURL(imageSrc)) {
+    return imageSrc
+  } else if (imageSrc.indexOf(baseApi) === 0) {
+    return imageSrc
+  } else if (imageSrc.startsWith('/static')) {
+    return imageSrc
+  } else {
+    return baseApi + imageSrc
+  }
+}

+ 28 - 3
src/utils/index.ts

@@ -1,4 +1,4 @@
-import { useRouter } from 'vue-router'
+import {useRouter} from 'vue-router'
 
 const router = useRouter()
 
@@ -15,14 +15,39 @@ export const getImgUrl = (name: string) => {
 export function jumpPage(path: string, query: any = null, isBlank = false) {
   if (path) {
     if (!isBlank) {
-      router.push({ path, query })
+      router.push({path, query})
     } else {
       if (path.indexOf('http') !== -1) {
         window.open(path, '_blank')
       } else {
-        let routeUrl = router.resolve({ path, query })
+        let routeUrl = router.resolve({path, query})
         window.open(routeUrl.href, '_blank')
       }
     }
   }
 }
+
+
+/**
+ * Merges two objects, giving the last one precedence
+ * @param {Object} target
+ * @param {(Object|Array)} source
+ * @returns {Object}
+ */
+export function objectMerge(target: any, source: any) {
+  if (typeof target !== 'object') {
+    target = {}
+  }
+  if (Array.isArray(source)) {
+    return source.slice()
+  }
+  Object.keys(source).forEach(property => {
+    const sourceProperty = source[property]
+    if (typeof sourceProperty === 'object' && sourceProperty !== null) {
+      target[property] = objectMerge(target[property], sourceProperty)
+    } else {
+      target[property] = sourceProperty
+    }
+  })
+  return target
+}

+ 341 - 0
src/utils/mapConfig.ts

@@ -0,0 +1,341 @@
+// import {LineLayer, PointLayer, PolygonLayer, Popup} from "@antv/l7";
+// import {formatStringByTemplate} from "@/utils/string";
+// import {getUUID} from "@/utils/ruoyi";
+import {Scene} from "@antv/l7-scene";
+import type {ICameraOptions, Point} from "@antv/l7-core";
+
+/**
+ * 设置地图视角
+ * @param scene
+ * @param center  中心点
+ * @param zoom    层级
+ * @param pitch   倾斜角度
+ * @param padding
+ */
+export function setCenter(scene: Scene, center: Point, zoom: number, pitch = 0, padding = {}) {
+  if (center && zoom) {
+    scene.setZoomAndCenter(zoom, center);
+  } else if (center) {
+    scene.setCenter(center, padding as ICameraOptions);
+  } else if (zoom) {
+    scene.setZoom(zoom);
+  }
+  scene.setPitch(pitch);
+}
+
+//
+// let _scene: any;
+//
+// const imageMap = new Map()
+//
+// function getValueByConditions(data: any, conditions: any, field: any) {
+//   let condition;
+//   for (const item of conditions) {
+//     if (compareByConditions(data, item)) {
+//       condition = item
+//       break
+//     }
+//   }
+//
+//   if (!condition) {
+//     return null;
+//   }
+//
+//   if (field === 'color') {
+//     return condition[field]
+//   }
+//
+//   if (condition[field] !== 'image') {
+//     return condition[field]
+//   }
+//
+//   if (condition[field] === 'image') {
+//     let id;
+//     if (imageMap.has(condition.imageSrc)) {
+//       id = imageMap.get(condition.imageSrc)
+//     } else {
+//       id = getUUID()
+//       imageMap.set(condition.imageSrc, id);
+//       _scene.addImage(id, condition.imageSrc);
+//     }
+//     return id
+//   }
+//
+//   return null;
+// }
+//
+// function compareByConditions(data: any, condition: any) {
+//   switch (condition.cond) {
+//     case 'lt':
+//       if (data < condition.value) {
+//         return true
+//       }
+//       break
+//     case 'lte':
+//       if (data <= condition.value) {
+//         return true
+//       }
+//       break
+//     case 'eq':
+//       if (data == condition.value) {
+//         return true
+//       }
+//       break
+//     case 'gt':
+//       if (data > condition.value) {
+//         return true
+//       }
+//       break
+//     case 'gte':
+//       if (data >= condition.value) {
+//         return true
+//       }
+//       break
+//     case 'ne':
+//       if (data != condition.value) {
+//         return true
+//       }
+//       break
+//   }
+//   return false;
+// }
+//
+// /**
+//  * 图层数据配置
+//  * @param dataType
+//  * @returns {{parser: {x: string, y: string, type: string}}|null}
+//  */
+// function getSourceOptions(dataType: any) {
+//   switch (dataType) {
+//     case 'geojson':
+//       return null;
+//     case 'list':
+//       return {
+//         parser: {
+//           type: 'json',
+//           x: '经度',
+//           y: '纬度',
+//         },
+//       }
+//     default:
+//   }
+// }
+//
+// /**
+//  * 图层形状配置
+//  * @param layer
+//  * @param config
+//  */
+// function setLayerShape(layer: any, config: any) {
+//   if (config.isConstant) {
+//     let shape = config.value
+//     if (config.value === 'image') {
+//       let id;
+//       if (imageMap.has(config.imageSrc)) {
+//         id = imageMap.get(config.imageSrc)
+//       } else {
+//         id = getUUID()
+//         imageMap.set(config.imageSrc, id);
+//         _scene.addImage(id, config.imageSrc);
+//       }
+//       shape = id
+//     }
+//     layer.shape(shape)
+//   } else {
+//     const field = config.field
+//     const conditions = config.conditions
+//     layer.shape(field, (field: any) => {
+//       return getValueByConditions(field, conditions, 'shape');
+//     });
+//   }
+// }
+//
+// /**
+//  * 图层颜色配置
+//  * @param layer
+//  * @param config
+//  */
+// function setLayerColor(layer: any, config: any) {
+//   if (config.isConstant) {
+//     layer.color(config.value)
+//   } else {
+//     const field = config.field
+//     const conditions = config.conditions
+//     layer.color(field, (field: any) => {
+//       return getValueByConditions(field, conditions, 'color');
+//     });
+//   }
+// }
+//
+// export function setMark({dataType, mapData}, config: any, scene: any) {
+//   if (scene) {
+//     _scene = scene
+//   }
+//
+//   const sourceOptions = getSourceOptions(dataType)
+//   if (!config.enabled) {
+//     return
+//   }
+//   // 标注
+//   const markLayer = new PointLayer({})
+//     .source(mapData)
+//     .shape(config.field, 'text')
+//     .size(12)
+//     .color('#000')
+//     .style({
+//       textAnchor: 'center', // 文本相对锚点的位置 center|left|right|top|bottom|top-left
+//       textOffset: [0, 0], // 文本相对锚点的偏移量 [水平, 垂直]
+//       spacing: 2, // 字符间距
+//       padding: [1, 1], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近
+//       stroke: '#ffffff', // 描边颜色
+//       strokeWidth: 2, // 描边宽度
+//       strokeOpacity: 1.0,
+//     });
+//   _scene.addLayer(markLayer);
+// }
+//
+// function setPopup(layer: any, config: any) {
+//   if (!config.enabled) {
+//     return
+//   }
+//   layer.on(config.trigger, (e) => {
+//     let properties = e.feature.properties || e.feature
+//     const content = formatStringByTemplate(properties, config.content)
+//
+//     // offsets: [0, 0], closeButton: false,
+//     const popup = new Popup({})
+//       .setLnglat(e.lngLat)
+//       .setHTML(content);
+//     _scene.addPopup(popup);
+//   });
+// }
+//
+// function createPointByCongfig(config: any, mapData, dataType) {
+//   const sourceOptions = getSourceOptions(dataType)
+//   const layer = new PointLayer({})
+//     .source(mapData, sourceOptions)
+//     .size(16)
+//     .active(config.active);
+//   setLayerShape(layer, config.shape);
+//   // 颜色配置
+//   setLayerColor(layer, config.color);
+//   _scene.addLayer(layer);
+//   setMark({dataType, mapData}, config.mark);
+//   setPopup(layer, config.popup);
+// }
+//
+// /**
+//  * 创建线图层
+//  * @param config 配置
+//  * @param dataType 图层数据类型
+//  * @param mapData 图层数据
+//  */
+// function createLineByCongfig(config, {dataType, mapData}) {
+//   const sourceOptions = getSourceOptions(dataType)
+//   const layer = new LineLayer({})
+//     .source(mapData, sourceOptions)
+//     .size(1)
+//     .style({
+//       lineType: 'dash',
+//       dashArray: [2, 2],
+//     })
+//     .active(config.active);
+//   _scene.addLayer(layer);
+//   setMark({dataType, mapData}, config.mark);
+//   setPopup(layer, config.popup);
+// }
+//
+// /**
+//  * 创建面图层
+//  * @param config 配置
+//  * @param dataType 图层数据类型
+//  * @param mapData 图层数据
+//  */
+// function createPolygonByCongfig(config, {dataType, mapData}) {
+//   const sourceOptions = getSourceOptions(dataType)
+//   const layer = new PolygonLayer({})
+//     .source(mapData, sourceOptions)
+//     .shape('fill')
+//     .active(config.active);
+//   // 颜色配置
+//   setLayerColor(layer, config.color);
+//   _scene.addLayer(layer);
+//   setMark({dataType, mapData}, config.mark);
+//   setPopup(layer, config.popup);
+// }
+//
+// function createMarkByCongfig(config, {dataType, mapData}) {
+//   setMark({dataType, mapData}, config.mark)
+// }
+//
+// /**
+//  * 根据配置加载图层
+//  * @param scene
+//  * @param mapConfig
+//  * @returns {Promise<void>}
+//  */
+// export async function loadMapData(scene: any, mapConfig: any) {
+//   _scene = scene
+//   console.log('mapConfig', mapConfig);
+//   // 清空图层
+//   _scene.removeAllLayer();
+//   if (!mapConfig.enabled) {
+//     return
+//   }
+//
+//   const layers = mapConfig.layers
+//   for (const layer of layers) {
+//     if (!layer.data?.mapDataId) {
+//       continue;
+//     }
+//
+//     // 1. 加载数据
+//     const mapJson:any = {}
+//     console.log('mapJson', mapJson);
+//     // switch (layer.type) {
+//     //   case 'point':
+//     //     createPointByCongfig(layer, mapJson)
+//     //     break
+//     //   case 'line':
+//     //     createLineByCongfig(layer, mapJson)
+//     //     break
+//     //   case 'polygon':
+//     //     createPolygonByCongfig(layer, mapJson)
+//     //     break
+//     //   case 'mark':
+//     //     createMarkByCongfig(layer, mapJson)
+//     //     break
+//     // }
+//   }
+//
+//   // 视角
+//   setCenter(scene, mapConfig.center, mapConfig.zoom, mapConfig.pitch);
+// }
+//
+
+//
+//
+// export function getLegendData(config: any) {
+//   const layers = config.layers
+//
+//   const layerLegend = layers.reduce((acc: any, item: any) => {
+//     if (item.legend && item.legend.data) {
+//       return acc.concat(item.legend.data);
+//     } else {
+//       return acc;
+//     }
+//   }, []);
+//
+//   return layerLegend.map((item: any) => {
+//     const data = {label: item.text}
+//     if (item.shape === 'image') {
+//       data.imgType = 'img'
+//       data.src = item.imgSrc
+//     } else {
+//       data.imgType = 'svg'
+//       data.src = item.shape
+//       data.imgStyle = {fill: item.color}
+//     }
+//     return data
+//   })
+// }

+ 238 - 0
src/utils/ruoyi.ts

@@ -0,0 +1,238 @@
+/**
+ * 通用js方法封装处理
+ * Copyright (c) 2019 ruoyi
+ */
+
+// 日期格式化
+export function parseTime(time: any, pattern: string) {
+  if (arguments.length === 0 || !time) {
+    return null;
+  }
+  const format = pattern || "{y}-{m}-{d} {h}:{i}:{s}";
+  let date;
+  if (typeof time === "object") {
+    date = time;
+  } else {
+    if (typeof time === "string" && /^[0-9]+$/.test(time)) {
+      time = parseInt(time);
+    } else if (typeof time === "string") {
+      time = time
+        .replace(new RegExp(/-/gm), "/")
+        .replace("T", " ")
+        .replace(new RegExp(/\.[\d]{3}/gm), "");
+    }
+    if (typeof time === "number" && time.toString().length === 10) {
+      time = time * 1000;
+    }
+    date = new Date(time);
+  }
+  const formatObj: { [key: string]: any } = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay(),
+  };
+  const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+    let value = formatObj[key];
+    // Note: getDay() returns 0 on Sunday
+    if (key === "a") {
+      return ["日", "一", "二", "三", "四", "五", "六"][value];
+    }
+    if (result.length > 0 && value < 10) {
+      value = "0" + value;
+    }
+    return value || 0;
+  });
+  return time_str;
+}
+
+// 字符串格式化(%s )
+export function sprintf(str: string) {
+  var args = arguments,
+    flag = true,
+    i = 1;
+  str = str.replace(/%s/g, function () {
+    var arg = args[i++];
+    if (typeof arg === "undefined") {
+      flag = false;
+      return "";
+    }
+    return arg;
+  });
+  return flag ? str : "";
+}
+
+// 转换字符串,undefined,null等转化为""
+export function praseStrEmpty(str: any) {
+  if (!str || str == "undefined" || str == "null") {
+    return "";
+  }
+  return str;
+}
+
+/**
+ * 构造树型结构数据
+ * @param {*} data 数据源
+ * @param {*} id id字段 默认 'id'
+ * @param {*} parentId 父节点字段 默认 'parentId'
+ * @param {*} children 孩子节点字段 默认 'children'
+ */
+export function handleTree(data: any[], id: string, parentId: string, children: string) {
+  let config = {
+    id: id || "id",
+    parentId: parentId || "parentId",
+    childrenList: children || "children",
+  };
+
+  var childrenListMap: { [key: string]: any } = {};
+  var nodeIds: { [key: string]: any } = {};
+  var tree = [];
+
+  for (let d of data) {
+    let parentId = d[config.parentId];
+    if (childrenListMap[parentId] == null) {
+      childrenListMap[parentId] = [];
+    }
+    nodeIds[d[config.id]] = d;
+    childrenListMap[parentId].push(d);
+  }
+
+  for (let d of data) {
+    let parentId = d[config.parentId];
+    if (nodeIds[parentId] == null) {
+      tree.push(d);
+    }
+  }
+
+  for (let t of tree) {
+    adaptToChildrenList(t);
+  }
+
+  function adaptToChildrenList(o: any) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]];
+    }
+    if (o[config.childrenList]) {
+      for (let c of o[config.childrenList]) {
+        adaptToChildrenList(c);
+      }
+    }
+  }
+
+  return tree;
+}
+
+export function treeToList(treeList: any[]) {
+  let list: any[] = [];
+
+  function traverse(node: { [key: string]: any }) {
+    if (node) {
+      list.push(node); // 将节点的值新建到列表中
+      if (node.children) {
+        for (let child of node.children) {
+          traverse(child); // 递归遍历子节点
+        }
+      }
+    }
+  }
+
+  treeList.forEach((tree) => traverse(tree))
+  return list;
+}
+
+export function createTree(list: any[], value: any, parentId: string, id: string) {
+  let tree = [];
+  for (let num = 0; num < list.length; num++) {
+    let data = list[num];
+    if (data[parentId] === value) {
+      let children = createTree(list, data[id], parentId, id);
+      if (children && children.length > 0) {
+        data["children"] = children;
+      }
+      tree.push(data);
+    }
+  }
+  return tree;
+}
+
+export function copyObj(obj: object) {
+  let newObjStr = JSON.stringify(obj);
+  return JSON.parse(newObjStr);
+}
+
+/**
+ * * 生成一个不重复的ID
+ * @param { Number } randomLength
+ */
+export const getUUID = (randomLength = 10) => {
+  return Number(
+    Math.random().toString().substr(2, randomLength) + Date.now()
+  ).toString(36);
+};
+
+export function dateFYMDHM(val: string) {
+  if (val) {
+    return (
+      val.substr(0, 4) +
+      "-" +
+      val.substr(4, 2) +
+      "-" +
+      val.substr(6, 2) +
+      " " +
+      val.substr(8, 2) +
+      ":" +
+      val.substr(10, 2)
+    );
+  }
+  return "";
+}
+
+/**
+ * 列表筛重
+ * @param list  列表
+ * @param key   筛选字段
+ * @returns {Set<any>[]|*|*[]}
+ */
+export function listFilterDuplicateData(list: any[], key: string) {
+  if (!list || list.length === 0) {
+    return list
+  }
+  if (!key) {
+    return Array.from(new Set(list))
+  }
+
+  const fdd = new Set(list.map(i => i[key]))
+
+  let retList: any[] = []
+  list.forEach(i => {
+    let param = i[key];
+    if (fdd.has(param)) {
+      retList.push(i);
+      fdd.delete(param)
+    }
+  })
+
+  return retList
+}
+
+export function getObjectSize(obj: object) {
+  let count = 0
+  for (let key in obj)
+    count++
+  return count
+}
+
+// a 数组是否包含 b 数组的值
+export function isHasByOtherArray(a = [], b = []) {
+  for (let i in a) {
+    for (let j in b) {
+      if (a[i] == b[j]) {
+        return true
+      }
+    }
+  }
+  return false
+}

+ 86 - 0
src/utils/string.ts

@@ -0,0 +1,86 @@
+import {isArray} from "@/utils/validate";
+
+/**
+ * 根据模板生成字符串
+ * @param data      数据
+ * @param template  模板
+ * @returns 字符串
+ */
+export function formatStringByTemplate(data: any, template: string) {
+  let result = template
+  if (data) {
+    if (isArray(data)) {
+      if (data.length === 0) {
+        return result;
+      }
+      // 数组
+      return formatStringByList(data, result);
+    } else {
+      return replaceText(result, data);
+    }
+  }
+  return result;
+}
+
+/**
+ * [[{{姓名}}任{{职务}},负责{{工作职责}}]]
+ * @param data
+ * @param template
+ * @returns {*}
+ */
+function formatStringByList(list: any[], template: string) {
+  let result = template
+  const regex = /\[\[.*?\]\]/g
+  let templates = result.match(regex)
+
+  const keys = Object.keys(list[0]);
+  const templatess: any[] = []
+  templates?.forEach(t => {
+    let temp = t.substring(2, t.length - 2);
+    let temps: any[] = []
+
+    list.forEach(d => temps.push(replaceText(temp, d)))
+
+    let ts = temps.join(';');
+    if (ts.length > 0 && !ts.endsWith('。')) {
+      ts = ts + '。'
+    }
+    templatess.push(ts)
+  })
+  templates?.forEach((ts, index) => {
+    result = result.replace(ts, templatess[index])
+  })
+
+  return result;
+}
+
+
+function replaceText2(template: string, data: { [key: string]: any }) {
+  const keys = Object.keys(data);
+  keys.forEach(key => {
+    template = template.replace(new RegExp('{{' + key + '}}', 'g'), data[key] || '');
+  });
+  return template;
+}
+
+function replaceText(template: string, data: any) {
+  // 正则表达式,用于匹配{{key}},包括可能存在的空格
+  const regex = /{{\s*([^}]+?)\s*}}/g;
+
+  // 递归函数,用于处理嵌套对象
+  function replaceValue(key: string) {
+    // 分割键名,以处理嵌套对象
+    const keys = key.split('.');
+    let value = data;
+    for (const k of keys) {
+      // 如果值为undefined或null,则停止查找
+      if (value === undefined || value === null) break;
+      value = value[k];
+    }
+    // 如果找到的值是undefined或null,则替换为空字符串
+    return value === undefined || value === null ? '' : value;
+  }
+
+  // 使用正则表达式替换模板中的所有匹配项
+  return template.replace(regex, (match, key) => replaceValue(key));
+}

+ 3 - 3
src/utils/useCurrentInstance.ts

@@ -1,8 +1,8 @@
-import type { ComponentInternalInstance } from '@vue/runtime-core'
-import { getCurrentInstance } from 'vue'
+import type {ComponentInternalInstance} from '@vue/runtime-core'
+import {getCurrentInstance} from 'vue'
 
 export default function useCurrentInstance() {
-  const { appContext } = getCurrentInstance() as ComponentInternalInstance
+  const {appContext} = getCurrentInstance() as ComponentInternalInstance
   const proxy = appContext?.config.globalProperties
   return {
     proxy,

+ 30 - 0
src/views/Home.vue

@@ -0,0 +1,30 @@
+<script lang="ts" setup>
+import RightFrame from "@/components/RightFrame.vue";
+import Card01 from "@/components/card/Card01.vue";
+</script>
+
+<template>
+  <right-frame use-left-sidebar="">
+    <template #leftModule>
+      <card01 custom-class="" title="ceshi">
+        <div>aaaaa</div>
+        <p>aaaaa</p>
+        <p>aaaaa</p>
+        <p>aaaaa</p>
+        <div>aaaaa</div>
+      </card01>
+      <card01 custom-class="" title="ceshi">
+        <div>aaaaa</div>
+        <p>aaaaa</p>
+        <p>aaaaa</p>
+        <p>aaaaa</p>
+        <div>aaaaa</div>
+      </card01>
+    </template>
+    <template #rightModule>
+      <card01>
+        aaaaa
+      </card01>
+    </template>
+  </right-frame>
+</template>

+ 0 - 14
src/views/HomeView.vue

@@ -1,14 +0,0 @@
-<script lang="ts" setup>
-import RightFrame from "@/components/RightFrame.vue";
-</script>
-
-<template>
-  <right-frame use-left-sidebar="">
-    <template #leftModule>
-      aaaa
-    </template>
-    <template #rightModule>
-      bbbb
-    </template>
-  </right-frame>
-</template>

+ 0 - 9
src/views/index.vue

@@ -1,9 +0,0 @@
-<script setup lang="ts">
-import TheWelcome from '../components/TheWelcome.vue'
-</script>
-
-<template>
-  <main>
-    <TheWelcome />
-  </main>
-</template>

+ 1 - 0
tsconfig.json

@@ -12,6 +12,7 @@
     }
   ],
   "compilerOptions": {
+    "strictNullChecks": false,
     "module": "NodeNext",
     "baseUrl": "src",
     "paths": {

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.