Browse Source

初始化

CzRger 1 year ago
parent
commit
9d640ef6d5

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 7 - 0
README.md

@@ -0,0 +1,7 @@
+# Vue 3 + Vite
+
+This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+## Recommended IDE Setup
+
+- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Vite + Vue</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 23 - 0
package.json

@@ -0,0 +1,23 @@
+{
+  "name": "yyy",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "vue": "^3.2.47",
+    "sass": "^1.60.0",
+    "axios": "^1.3.4",
+    "vuex": "^4.1.0",
+    "vue-router": "^4.1.6",
+    "element-plus": "^2.3.1"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^4.1.0",
+    "vite": "^4.3.9"
+  }
+}

File diff suppressed because it is too large
+ 1 - 0
public/vite.svg


+ 38 - 0
src/App.vue

@@ -0,0 +1,38 @@
+<template>
+  <div style="overflow: hidden">
+    <router-view/>
+  </div>
+</template>
+<script>
+import {
+  defineComponent,
+  ref,
+  getCurrentInstance
+} from 'vue'
+import {useStore} from 'vuex'
+import {ElConfigProvider} from 'element-plus'
+import zhLocale from 'element-plus/lib/locale/lang/zh-cn'
+export default defineComponent({
+  name: 'App',
+  components: {
+    [ElConfigProvider.name]: ElConfigProvider //添加组件
+  },
+  setup() {
+    const store = useStore()
+    const locale = ref(zhLocale)
+    const that = getCurrentInstance().appContext.config.globalProperties
+    return {
+      locale
+    }
+  }
+})
+</script>
+<style scope lang="scss">
+html, body {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 1 - 0
src/assets/vue.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 40 - 0
src/components/HelloWorld.vue

@@ -0,0 +1,40 @@
+<script setup>
+import { ref } from 'vue'
+
+defineProps({
+  msg: String,
+})
+
+const count = ref(0)
+</script>
+
+<template>
+  <h1>{{ msg }}</h1>
+
+  <div class="card">
+    <button type="button" @click="count++">count is {{ count }}</button>
+    <p>
+      Edit
+      <code>components/HelloWorld.vue</code> to test HMR
+    </p>
+  </div>
+
+  <p>
+    Check out
+    <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
+      >create-vue</a
+    >, the official Vue + Vite starter
+  </p>
+  <p>
+    Install
+    <a href="https://github.com/vuejs/language-tools" target="_blank">Volar</a>
+    in your IDE for a better DX
+  </p>
+  <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
+</template>
+
+<style scoped>
+.read-the-docs {
+  color: #888;
+}
+</style>

+ 55 - 0
src/components/easyMap/func/location.ts

@@ -0,0 +1,55 @@
+import * as ol from 'ol'
+import * as style from 'ol/style'
+import * as layer from 'ol/layer'
+import * as source from 'ol/source'
+import * as geom from 'ol/geom'
+import * as proj from 'ol/proj'
+import * as interaction from 'ol/interaction'
+import * as coordinate from 'ol/coordinate'
+import * as format from "ol/format";
+
+const layerFlag = ['layerName', 'positionLayer']
+export default function Location ({map, position = null, wkt = null, zoom = null, color = '#039ff3'}) {
+    let _source = null
+    const realLayer = map.getLayers().getArray().filter(v => v.get(layerFlag[0]) === layerFlag[1])
+    if (realLayer[0]) {
+        _source = realLayer[0].getSource()
+        _source.clear()
+    } else {
+        _source = new source.Vector(); //图层数据源
+        const _vector = new layer.Vector({
+            zIndex: 9999,
+            source: _source,
+        });
+        _vector.set(layerFlag[0], layerFlag[1])
+        map.addLayer(_vector);
+    }
+    const feat = new format.WKT().readFeature(position ? `POINT(${position[0]} ${position[1]})` : wkt)
+    const radius = 25
+    const longRadius = radius * Math.SQRT2
+    feat.setStyle([new style.Style({ //图层样式
+        image: new style.RegularShape({
+            stroke: new style.Stroke({
+                color: color,
+                width: 2,
+                lineDash: [
+                    (longRadius * 3) / 10,
+                    (longRadius * 4) / 10,
+                    (longRadius * 3) / 10,
+                    0
+                ]
+            }),
+            radius1: radius,
+            rotation: Math.PI / (180 / 45),
+            points: 4
+        })
+    })])
+    _source.addFeature(feat)
+    setTimeout(() => {
+        feat?.getGeometry().setCoordinates([0, 0])
+    }, 3000)
+    map.getView().setCenter(feat?.getGeometry().getCoordinates())
+    if (zoom) {
+        map.getView().setZoom(zoom)
+    }
+}

+ 45 - 0
src/components/easyMap/func/measure.scss

@@ -0,0 +1,45 @@
+.tooltip {
+  position: relative;
+  background: rgba(0, 0, 0, 0.5);
+  border-radius: 4px;
+  color: white;
+  padding: 4px 8px;
+  white-space: nowrap;
+}
+
+.tooltip-measure {
+  opacity: 1;
+  font-weight: bold;
+}
+
+.tooltip-static {
+  background-color: #ffcc33;
+  color: black;
+  border: 1px solid white;
+}
+
+.tooltip-measure:before,
+.tooltip-static:before {
+  border-top: 6px solid rgba(0, 0, 0, 0.5);
+  border-right: 6px solid transparent;
+  border-left: 6px solid transparent;
+  content: "";
+  position: absolute;
+  bottom: -6px;
+  margin-left: -7px;
+  left: 50%;
+}
+
+.tooltip-static:before {
+  border-top-color: #ffcc33;
+}
+.lineDel {
+  width: 16px;
+  height: 16px;
+  display: inline-block;
+  vertical-align: middle;
+  margin-left: 10px;
+  cursor: pointer;
+  //background: url('@/assets/images/map/lineDel.png') no-repeat;
+  background-size: 100% 100%;
+}

+ 262 - 0
src/components/easyMap/func/measure.ts

@@ -0,0 +1,262 @@
+import * as ol from 'ol'
+import * as style from 'ol/style'
+import * as layer from 'ol/layer'
+import * as source from 'ol/source'
+import * as geom from 'ol/geom'
+import * as proj from 'ol/proj'
+import * as interaction from 'ol/interaction'
+import * as coordinate from 'ol/coordinate'
+import * as control from 'ol/control'
+import * as sphere from 'ol/sphere'
+import { unByKey } from 'ol/Observable'
+import './measure.scss'
+import {createBox} from "ol/interaction/Draw";
+import {Circle, LineString, Polygon} from "ol/geom";
+
+const layerFlag = ['layerName', 'measureLayer']
+let measureTooltipElement;
+let helpTooltipElement;
+const typeMapper = new Map([
+    ['line', 'LineString'],
+    ['rectangle', 'LineString'],
+    ['polygon', 'Polygon'],
+    ['circle', 'Circle'],
+])
+/**
+ *
+ * @param map
+ * @param typeSelect    line线,rectangle矩形,polygon多边形,circle圆形
+ */
+export default function Measure (map, typeSelect) {
+    let _source = null
+    const realLayer = map.getLayers().getArray().filter(v => v.get(layerFlag[0]) === layerFlag[1])
+    if (realLayer[0]) {
+        _source = realLayer[0].getSource()
+    } else {
+        _source = new source.Vector(); //图层数据源
+        const _vector = new layer.Vector({
+            zIndex: 9999,
+            source: _source,
+            style: new style.Style({ //图层样式
+                fill: new style.Fill({
+                    color: 'rgba(255, 255, 255, 0.2)' //填充颜色
+                }),
+                stroke: new style.Stroke({
+                    color: '#f31a4a',  //边框颜色
+                    width: 2   // 边框宽度
+                }),
+                image: new style.Circle({
+                    radius: 7,
+                    fill: new style.Fill({
+                        color: '#ffcc33'
+                    })
+                })
+            })
+        });
+        _vector.set(layerFlag[0], layerFlag[1])
+        map.addLayer(_vector);
+    }
+    let sketch;
+    let helpTooltip;
+    let measureTooltip;
+    let continueMsg = '双击结束测量';
+    const geodesicCheckbox = true;//测地学方式对象
+    const createMeasureTooltip = () => {
+        const id = 'measureTooltipElementId'
+        if (measureTooltipElement) {
+            map.removeOverlay(map.getOverlayById(id))
+            measureTooltipElement.parentNode.removeChild(measureTooltipElement);
+        }
+        measureTooltipElement = document.createElement('div');
+        measureTooltipElement.className = 'tooltip tooltip-measure';
+        measureTooltip = new ol.Overlay({
+            id,
+            element: measureTooltipElement,
+            offset: [0, -15],
+            positioning: 'bottom-center'
+        });
+        map.addOverlay(measureTooltip);
+    }
+    const createHelpTooltip = () => {
+        const id = 'helpTooltipElementId'
+        if (helpTooltipElement) {
+            map.removeOverlay(map.getOverlayById(id))
+            helpTooltipElement.parentNode.removeChild(helpTooltipElement);
+        }
+        helpTooltipElement = document.createElement('div');
+        helpTooltipElement.className = 'tooltip hidden';
+        helpTooltip = new ol.Overlay({
+            id,
+            element: helpTooltipElement,
+            offset: [15, 0],
+            positioning: 'center-left'
+        });
+        map.addOverlay(helpTooltip);
+    }
+    const formatLength = (line) => {
+        // 获取投影坐标系
+        const sourceProj = map.getView().getProjection();
+        // ol/sphere里有getLength()和getArea()用来测量距离和区域面积,默认的投影坐标系是EPSG:3857, 其中有个options的参数,可以设置投影坐标系
+        const length = sphere.getLength(line, {projection: sourceProj});
+        // const length = getLength(line);
+        let output;
+        if (length > 100) {
+            const km = Math.round((length / 1000) * 100) / 100;
+            output = `${km} 千米 <br>${parseFloat(String(km * 0.53995)).toFixed(2)} 海里`;
+        } else {
+            output = `${Math.round(length * 100) / 100} m`;
+        }
+        return output;
+    };
+    //获取圆的面积
+    const getCircleArea = (circle, projection) => {
+        const P = 3.14
+        const radius = getCircleRadius(circle, projection)
+        return P * radius * radius
+    }
+//获取圆的半径
+    const getCircleRadius = (circle, projection) => {
+        return circle.getRadius() * projection.getMetersPerUnit()
+    }
+    const formatArea = (polygon, type= 'polygon') => {
+        let area
+        const sourceProj = map.getView().getProjection();
+        // 获取投影坐标系
+        if (type === 'polygon') {
+            area = sphere.getArea(polygon, {
+                projection: sourceProj,
+            });
+        } else if (type === 'circle') {
+            area = getCircleArea(polygon, sourceProj)
+        }
+        let output;
+        if (area > 10000) {
+            const km = Math.round((area / 1000000) * 100) / 100;
+            output = `${km} 平方公里<br>${parseFloat(String(km * 0.38610)).toFixed(
+                2
+            )} 平方英里`;
+        } else {
+            output = `${Math.round(area * 100) / 100} ` + " m<sup>2</sup>";
+        }
+        return output;
+    };
+    const addInteraction = () => {
+        const id = 'drawName'
+        const draw = new interaction.Draw({
+            source: _source,//测量绘制层数据源
+            type: typeMapper.get(typeSelect),  //几何图形类型
+            geometryFunction: typeSelect === 'rectangle' ? createBox() : null,
+            style: new style.Style({
+                fill: new style.Fill({
+                    color: "rgba(255, 255, 255, 0.2)",
+                }),
+                stroke: new style.Stroke({
+                    color: "#f3584a",
+                    width: 2,
+                }),
+                image: new style.Circle({
+                    radius: 5,
+                    stroke: new style.Stroke({
+                        color: "rgba(0, 0, 0, 0.7)",
+                    }),
+                    fill: new style.Fill({
+                        color: "rgba(255, 255, 255, 0.2)",
+                    }),
+                }),
+            }),
+        });
+        draw.set(id, id)
+        createMeasureTooltip(); //创建测量工具提示框
+        createHelpTooltip(); //创建帮助提示框
+        map.addInteraction(draw);
+        let listener;
+        //绑定交互绘制工具开始绘制的事件
+        const drawstartHandle = (evt) => {
+            sketch = evt.feature; //绘制的要素
+            let tooltipCoord = evt.coordinate;// 绘制的坐标
+            //绑定change事件,根据绘制几何类型得到测量长度值或面积值,并将其设置到测量工具提示框中显示
+            listener = sketch.getGeometry().on('change', function (evt) {
+                const geom = evt.target
+                let output;
+                if (geom.getType() === 'LineString') {
+                    output = formatLength(geom);//长度值
+                    tooltipCoord = geom.getLastCoordinate();//坐标
+                } else if (geom.getType() === 'Polygon') {
+                    output = formatArea(geom);//面积值
+                    tooltipCoord = geom.getInteriorPoint().getCoordinates();//坐标
+                } else if (geom.getType() === 'Circle') {
+                    output = formatArea(geom, 'circle');//面积值
+                    tooltipCoord = geom.getCenter()
+                }
+                measureTooltipElement.innerHTML = output;//将测量值设置到测量工具提示框中显示
+                measureTooltip.setPosition(tooltipCoord);//设置测量工具提示框的显示位置
+            });
+        }
+        draw.on('drawstart', drawstartHandle);
+        //绑定交互绘制工具结束绘制的事件
+        const copy = (value) => {
+            const str = document.createElement('input')
+            str.setAttribute('value', value)
+            document.body.appendChild(str)
+            str.select()
+            document.execCommand('copy')
+            document.body.removeChild(str)
+        }
+        const drawendHandle = (evt) => {
+            map.removeInteraction(map.getInteractions().getArray().filter(v => v.get(id) === id)[0]);
+            const del = document.createElement("div");
+            del.className = "lineDel";
+            measureTooltipElement.append(del);
+            del.onclick = () => {
+                _source.removeFeature(evt.feature)
+                const b = del.parentElement.parentElement
+                b.parentElement.removeChild(b);
+                const g = evt.feature.getGeometry()
+                if (g.getType() === 'LineString') {
+                    const w = `LINESTRING(${g.getCoordinates().map(v => v[0] + ' ' + v[1]).join(',')})`
+                    copy(w)
+                } else if (g.getType() === 'Polygon') {
+                    const w = `POLYGON(${g.getCoordinates().map(v => '(' + v.map(c => c[0] + ' ' + c[1]) + ')').join(',')})`
+                    copy(w)
+                }
+            };
+            measureTooltipElement.className = 'tooltip tooltip-static'; //设置测量提示框的样式
+            measureTooltip.setOffset([0, -7]);
+            sketch = null; //置空当前绘制的要素对象
+            measureTooltipElement = null; //置空测量工具提示框对象
+            helpTooltipElement.parentNode.removeChild(helpTooltipElement);
+            helpTooltipElement = null; //置空测量工具提示框对象
+            unByKey(listener);
+            draw.un('drawstart', drawstartHandle);
+            draw.un('drawend', drawendHandle);
+            map.removeInteraction(map.getInteractions().getArray().filter(v => v.get(id) === id)[0]);
+            map.un('pointermove', pointerMoveHandler)
+        }
+        draw.on('drawend', drawendHandle);
+    }
+    addInteraction(); //调用加载绘制交互控件方法,添加绘图进行测量
+    const pointerMoveHandler = (evt) => {
+        if (evt.dragging) {
+            return;
+        }
+        let helpMsg = '单击开始测量';//当前默认提示信息
+        //判断绘制几何类型设置相应的帮助提示信息
+        if (sketch) {
+            const geom = sketch.getGeometry()
+            helpMsg = continueMsg;
+            // if (geom.getType() === 'Polygon') {
+            //     helpMsg = continueMsg; //绘制多边形时提示相应内容
+            // } else if (geom.getType() === 'LineString') {
+            //     helpMsg = continueMsg; //绘制线时提示相应内容
+            // }
+        }
+        helpTooltipElement.innerHTML = helpMsg; //将提示信息设置到对话框中显示
+        helpTooltip.setPosition(evt.coordinate);//设置帮助提示框的位置
+        helpTooltipElement.classList.remove('hidden');//移除帮助提示框的隐藏样式进行显示
+    };
+    map.on('pointermove', pointerMoveHandler); //地图容器绑定鼠标移动事件,动态显示帮助提示框内容
+    //地图绑定鼠标移出事件,鼠标移出时为帮助提示框设置隐藏样式
+    map.getViewport().on('mouseout', () => {
+        helpTooltipElement.addClass('hidden');
+    });
+}

BIN
src/components/easyMap/images/bg-land.png


BIN
src/components/easyMap/images/bg-ocean.png


BIN
src/components/easyMap/images/bg-sky.png


File diff suppressed because it is too large
+ 12 - 0
src/components/easyMap/images/bg-switch.svg


+ 165 - 0
src/components/easyMap/index.vue

@@ -0,0 +1,165 @@
+<template>
+  <div class="easy-map">
+    <OlMap
+      ref="ref_olMap"
+      :baseMapLayers="_baseMapLayers"
+      :baseMapView="_baseMapView"
+      @olZoomChange="(zoom) => $emit('zoomChange', zoom)"
+      @olMapLoad="(map) => handleOlMapLoad(map)"
+    />
+    <template v-if="showBaseSwitch">
+      <div class="base-switch">
+        <el-popover
+          placement="left"
+          trigger="hover"
+          popper-class="easy-map-base-switch"
+        >
+          <template #reference>
+            <img class="__hover" src="./images/bg-switch.svg" />
+          </template>
+          <div class="base-switch-item">
+            <template v-for="item in _baseMapLayers">
+              <div
+                class="base-item __hover"
+                :class="{
+                  active: judgeBaseLayerActive(
+                    item.get('_easyMapOl_layerName')
+                  ),
+                }"
+                @click="
+                  ref_olMap.switchBaseLayer(item.get('_easyMapOl_layerName'))
+                "
+              >
+                <div class="label">{{ item.get("_label") }}</div>
+                <img :src="item.get('_img')" />
+              </div>
+            </template>
+          </div>
+        </el-popover>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  onMounted,
+  ref,
+  toRefs,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  computed,
+  nextTick,
+} from "vue";
+import { useStore } from "vuex";
+import OlMap from "./ol-map.vue";
+import InitMapInfoClass from "./initMapInfo";
+import MeasureFunc from "./func/measure";
+import LocationFunc from "./func/location";
+
+export default defineComponent({
+  name: "EasyMap",
+  components: {
+    OlMap,
+  },
+  props: {
+    baseMapLayers: {},
+    baseMapView: {},
+    showBaseSwitch: {
+      default: false,
+    },
+  },
+  setup(props, { emit }) {
+    const store = useStore();
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext
+      .config.globalProperties;
+    const state = reactive({});
+    const ref_olMap = ref();
+    const _baseMapLayers = computed(
+      () => props.baseMapLayers || InitMapInfoClass.baseMapLayers
+    );
+    const _baseMapView = computed(
+      () => props.baseMapView || InitMapInfoClass.baseMapView
+    );
+    const easyMap = computed(() => ref_olMap.value?.easyMapOl);
+    const handleOlMapLoad = (map) => {
+      emit("easyMapLoad", map, {
+        getBBOX,
+        measure,
+        toLocation,
+        resetCenter,
+        baseLayer: {
+          switchMapper: _baseMapLayers.value,
+          switchLayer: ref_olMap.value.switchBaseLayer,
+          judgeActive: judgeBaseLayerActive,
+        },
+      });
+    };
+    const getBBOX = () => {
+      return easyMap.value.getView().calculateExtent(easyMap.value.getSize());
+    };
+    const judgeBaseLayerActive = (layerName) => {
+      return easyMap.value
+        ?.getLayers()
+        .getArray()
+        .filter((v) => v.get("_easyMapOl_layerGroupType") === "base")[0]
+        .getLayers()
+        .getArray()
+        .filter((v) => v.get("_easyMapOl_layerName") === layerName)[0]
+        ?.getVisible();
+    };
+    const measure = (type) => {
+      MeasureFunc(easyMap.value, type);
+    };
+    const toLocation = ({ position = null, zoom = null, wkt = null }) => {
+      LocationFunc({
+        map: easyMap.value,
+        position,
+        wkt,
+        zoom,
+      });
+    };
+    const resetCenter = () => {
+      easyMap.value.getView().setCenter(_baseMapView.value.center);
+      easyMap.value.getView().setZoom(_baseMapView.value.zoom);
+    };
+    onMounted(() => {
+      nextTick(() => {});
+    });
+    return {
+      ...toRefs(state),
+      ref_olMap,
+      _baseMapLayers,
+      _baseMapView,
+      easyMap,
+      getBBOX,
+      measure,
+      toLocation,
+      handleOlMapLoad,
+      judgeBaseLayerActive,
+    };
+  },
+});
+</script>
+<style scoped lang="scss">
+.easy-map {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  .base-switch {
+    position: absolute;
+    z-index: 2;
+    right: 40px;
+    bottom: 70px;
+    width: 26px;
+    height: 26px;
+    background-color: #f2f8fd;
+    border-radius: 4px;
+    display: grid;
+    place-items: center;
+  }
+}
+</style>

+ 88 - 0
src/components/easyMap/initMapInfo.ts

@@ -0,0 +1,88 @@
+import * as layer from 'ol/layer'
+import * as source from 'ol/source'
+import HaituImg from './images/bg-ocean.png'
+import LutuImg from './images/bg-land.png'
+import WeixingImg from './images/bg-sky.png'
+// @ts-ignore
+import store from '@/store/index'
+
+const baseMapView = {
+  center: [109.6915958479584, 19.111636735969318],
+  projection: "EPSG:4326",
+  zoom: 9
+  // extent: [120.8953306326286,31.3667480047968,121.37735577911297,31.692561298253832]
+}
+const initBaseLayer = (obj: { key: any; name: any; label: any; maxZoom: any; minZoom: any; visible: any; img: any }) => {
+  const _layer = new layer.Tile({
+    source: new source.XYZ({
+      projection: "EPSG:4326",
+      url: `/${store.state.app.apiProxy.EzServer6Api}/EzServer6/Maps/${obj.key}/EzMap?Service=getImage&Type=RGB&ZoomOffset=0&Col={x}&Row={y}&Zoom={z}&V=0.3`,
+    }),
+    visible: obj.visible,
+  })
+  _layer.set('_maxZoom', obj.maxZoom)
+  _layer.set('_minZoom', obj.minZoom)
+  _layer.set('_easyMapOl_layerName', obj.name)
+  _layer.set('_label', obj.label)
+  _layer.set('_img', obj.img)
+  return _layer
+}
+const initLocalhost = (obj: { key?: string; name: any; label: any; maxZoom: any; minZoom: any; visible: any; img: any }) => {
+  const _layer = new layer.Tile({
+    source: new source.XYZ({
+      url: 'http://wprd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}'
+    }),
+    visible: obj.visible,
+  })
+  _layer.set('_maxZoom', obj.maxZoom)
+  _layer.set('_minZoom', obj.minZoom)
+  _layer.set('_easyMapOl_layerName', obj.name)
+  _layer.set('_label', obj.label)
+  _layer.set('_img', obj.img)
+  return _layer
+}
+
+const baseMapLayers = [
+  initBaseLayer({
+    key: 'sea',
+    name: 'base_haitu',
+    label: '海图',
+    maxZoom: 14,
+    minZoom: 5,
+    visible: false,
+    img: HaituImg
+  }),
+  !window.location.origin.includes('localhost')
+    ? initBaseLayer({
+      key: 'tdtsl',
+      name: 'base_tianditu',
+      label: '陆图',
+      maxZoom: 20,
+      minZoom: 8,
+      visible: true,
+      img: LutuImg
+    })
+  : initLocalhost({
+      key: 'tdtsl',
+      name: 'base_tianditu',
+      label: '陆图',
+      maxZoom: 20,
+      minZoom: 8,
+      visible: true,
+      img: LutuImg
+    }),
+  initBaseLayer({
+    key: 'hnimg',
+    name: 'base_weixingtu',
+    label: '卫星遥感图',
+    maxZoom: 19,
+    minZoom: 8,
+    visible: false,
+    img: WeixingImg
+  }),
+]
+
+export default {
+  baseMapView,
+  baseMapLayers,
+}

+ 296 - 0
src/components/easyMap/ol-map.vue

@@ -0,0 +1,296 @@
+<template>
+  <div class="easy-map-ol">
+    <div class="map" ref="ref_easyMapOl"/>
+    <div class="easy-map_ol-mouse-position" ref="ref_easyMap_olMousePosition" @click="controlMousePosition.format = !controlMousePosition.format">
+      <template v-if="controlMousePosition.format">
+        {{controlMousePosition.formatLongitude}}<br/>
+        {{controlMousePosition.formatLatitude}}
+      </template>
+      <template v-else>
+        {{controlMousePosition.longitude}}<br/>
+        {{controlMousePosition.latitude}}
+      </template>
+    </div>
+    <div class="easy-map_ol-zoom" ref="ref_easyMap_olZoom">
+      <div class="easy-map_ol-zoom-button" @click="zoomChange(true)">+</div>
+      <div class="easy-map_ol-zoom-num">{{Math.floor(interactionZoom)}}</div>
+      <div class="easy-map_ol-zoom-button" @click="zoomChange(false)">-</div>
+    </div>
+    <div class="easy-map_ol-scaleLine" ref="ref_easyMap_scaleLine"></div>
+  </div>
+</template>
+
+<script lang="ts">
+  import {
+    defineComponent,
+    onMounted,
+    ref,
+    toRefs,
+    reactive,
+    watch,
+    getCurrentInstance,
+    ComponentInternalInstance,
+    computed,
+    nextTick,
+  } from "vue";
+  import { useStore } from "vuex";
+  import * as ol from 'ol'
+  import * as style from 'ol/style'
+  import * as layer from 'ol/layer'
+  import * as source from 'ol/source'
+  import * as geom from 'ol/geom'
+  import * as proj from 'ol/proj'
+  import * as interaction from 'ol/interaction'
+  import * as coordinate from 'ol/coordinate'
+  import * as control from 'ol/control'
+
+  export default defineComponent({
+    name: "",
+    components: {
+    },
+    props: {
+      baseMapLayers: { default: () => [] },
+      baseMapView: {},
+    },
+    setup(props, { emit }) {
+      const store = useStore();
+      const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties;
+      const state = reactive({
+        controlMousePosition: {
+          longitude: <any>null,
+          latitude: <any>null,
+          format: false,
+          formatLongitude: <any>null,
+          formatLatitude: <any>null,
+        },
+        interactionZoom: props.baseMapView.zoom,
+      });
+      const easyMapOl = ref()
+      const ref_easyMapOl = ref()
+      const ref_easyMap_olMousePosition = ref()
+      const ref_easyMap_olZoom = ref()
+      const ref_easyMap_scaleLine = ref()
+      const initMap = () => {
+        easyMapOl.value = new ol.Map({
+          target: ref_easyMapOl.value,
+          layers: [
+            new layer.Group({
+              _easyMapOl_layerGroupType: 'base',
+              layers: props.baseMapLayers,
+              zIndex: 1
+            }),
+          ],
+          view: new ol.View(props.baseMapView),
+          controls: control.defaults({
+            attribution: false,
+            rotate: false,
+            zoom: false,
+          }).extend([
+            new control.MousePosition({
+              target: ref_easyMap_olMousePosition.value,
+              coordinateFormat: (e) => {
+                state.controlMousePosition.longitude = e[0]
+                state.controlMousePosition.latitude = e[1]
+                const f = coordinate.toStringHDMS(e, 0).split(' ')
+                state.controlMousePosition.formatLatitude = `${f[0]} ${f[1]} ${f[2]} ${f[3]}`
+                state.controlMousePosition.formatLongitude = `${f[4]} ${f[5]} ${f[6]} ${f[7]}`
+                return null
+              },
+              placeholder: ''
+            }),
+            new control.Zoom({
+              target: ref_easyMap_olZoom.value,
+            }),
+            new control.ScaleLine({
+              target: ref_easyMap_scaleLine.value,
+              bar: true
+            })
+          ]),
+          interactions: interaction.defaults({
+            doubleClickZoom: false
+          })
+        })
+        easyMapOl.value.getView().on('change:resolution', e => {
+          state.interactionZoom = e.target.getZoom()
+          emit('olZoomChange', state.interactionZoom)
+        })
+        const defaultBaseLayer = props.baseMapLayers.filter(v => v.getVisible())[0]
+        setLayerView(defaultBaseLayer)
+        emit('olMapLoad', easyMapOl.value)
+        easyMapOl.value.on('contextmenu', e => {
+          window.event.returnValue = false
+          if (window?.event?.shiftKey) {
+            const str = document.createElement('input')
+            str.setAttribute('value', `POINT(${e.coordinate[0]} ${e.coordinate[1]})`)
+            document.body.appendChild(str)
+            str.select()
+            document.execCommand('copy')
+            document.body.removeChild(str)
+          }
+        })
+      }
+      const zoomChange = (flag) => {
+        state.interactionZoom = flag ? state.interactionZoom + 1 : state.interactionZoom - 1
+        if (state.interactionZoom > easyMapOl.value.getView().getMaxZoom()) {
+          state.interactionZoom = easyMapOl.value.getView().getMaxZoom()
+        } else if (state.interactionZoom < easyMapOl.value.getView().getMinZoom()) {
+          state.interactionZoom = easyMapOl.value.getView().getMinZoom()
+        }
+        easyMapOl.value.getView().setZoom(state.interactionZoom)
+      }
+      const baseLayersMap = computed(() => {
+        const map = new Map()
+        easyMapOl.value?.getLayers().getArray().filter(v => v.get('_easyMapOl_layerGroupType') === 'base')[0].getLayers().getArray().forEach(v => {
+          map.set(v.get('_easyMapOl_layerName'), v)
+        })
+        return map
+      })
+      const switchBaseLayer = (layerName) => {
+        baseLayersMap.value.forEach(v => {
+          if (layerName === v.get('_easyMapOl_layerName')) {
+            setLayerView(v)
+            v.setVisible(true)
+          } else {
+            v.setVisible(false)
+          }
+        })
+      }
+      const setLayerView = (_layer) => {
+        easyMapOl.value.getView().setMaxZoom(_layer.get('_maxZoom'))
+        easyMapOl.value.getView().setMinZoom(_layer.get('_minZoom'))
+      }
+      onMounted(() => {
+        nextTick(() => {
+          initMap()
+          setTimeout(() => {
+            easyMapOl.value.updateSize();
+            easyMapOl.value.render()
+          })
+        })
+      })
+      return {
+        ...toRefs(state),
+        ref_easyMapOl,
+        ref_easyMap_olMousePosition,
+        ref_easyMap_olZoom,
+        ref_easyMap_scaleLine,
+        easyMapOl,
+        zoomChange,
+        switchBaseLayer,
+        baseLayersMap,
+      }
+    },
+  });
+</script>
+<style scoped lang="scss">
+.easy-map-ol {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  .map {
+    width: 100%;
+    height: 100%;
+    background-color: #bfdbf3;
+  }
+  .easy-map_ol-mouse-position {
+    cursor: pointer;
+    position: absolute;
+    z-index: 1;
+    color: #000;
+    top: unset;
+    font-size: 14px;
+    right: 10px;
+    bottom: 10px;
+    height: 50px;
+    padding: 5px 10px;
+    background-color: #fff;
+    border-radius: 2px;
+    line-height: 22px;
+    opacity: .8;
+  }
+  .easy-map_ol-zoom {
+    position: absolute;
+    bottom: 70px;
+    right: 10px;
+    >div {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin: 0;
+      padding: 0;
+      color: #000;
+      font-size: 12px;
+      font-weight: 700;
+      height: 20px;
+      width: 20px;
+      background-color: #fff;
+    }
+    .easy-map_ol-zoom-num {
+      border-top: 1px solid rgba(153, 153, 153, 0.32);
+      border-bottom: 1px solid rgba(153, 153, 153, 0.32);
+    }
+    .easy-map_ol-zoom-button {
+      cursor: pointer;
+      &:hover {
+        opacity: 0.7;
+      }
+    }
+  }
+  .easy-map_ol-scaleLine {
+    position: absolute;
+    bottom: 20px;
+    left: 20px;
+  }
+  .easy-map_switchBaseLayer {
+    position: absolute;
+    left: 20px;
+    bottom: 40px;
+  }
+  ::v-deep(.ol-zoom) {
+    display: none;
+  }
+  ::v-deep(.ol-scale-bar-inner) {
+    position: absolute;
+    bottom: 1%;
+    left: 1%;
+
+    &>div>div:nth-child(2) {
+      .ol-scale-singlebar {
+        border-left: 2px solid #807A7A;
+      }
+    }
+
+    &>div>div:nth-child(5) {
+      .ol-scale-singlebar {
+        border-right: 2px solid #807A7A;
+      }
+    }
+
+    .ol-scale-step-text {
+      padding-bottom: 20px;
+      font-size: 12px;
+      transform: scale(0.8);
+      position: absolute;
+      bottom: -5px;
+      font-size: 12px;
+      z-index: 11;
+      color: #000000;
+      text-shadow: -2px 0 #ffffff,
+      0 2px #ffffff,
+      2px 0 #ffffff,
+      0 -2px #ffffff;
+    }
+
+    .ol-scale-singlebar {
+      border: 0;
+      background-color: transparent !important;
+      border-bottom: 2px solid #807A7A;
+      height: 10px;
+    }
+
+    .ol-scale-step-marker {
+      display: none;
+    }
+  }
+}
+</style>

+ 14 - 0
src/main.js

@@ -0,0 +1,14 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+import store from "./store"
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import './style/cus-element.scss'
+import * as util from '@/utils/util.ts'
+const app = createApp(App)
+app.use(router)
+app.use(store)
+app.use(ElementPlus)
+app.config.globalProperties.$util = util
+app.mount('#app')

+ 20 - 0
src/out/config.js

@@ -0,0 +1,20 @@
+window.cusConfig = {
+  trackSource: [
+    ['TIANAO_RADAR', {
+      label: '小目标雷达',
+      color: '#f0a461'
+    }],
+    ['BEIDOU', {
+      label: '北斗',
+      color: '#f755f3'
+    }],
+    ['GLOBAL_AIS', {
+      label: '全球AIS',
+      color: '#409eff'
+    }],
+  ],
+  trackParams: [
+    {value: 'delay', label: '延时', init: 0},
+    {value: 'batchNum', label: '批次号', init: 0},
+  ]
+}

+ 14 - 0
src/router/index.ts

@@ -0,0 +1,14 @@
+import { RouteRecordRaw, createRouter, createWebHistory } from 'vue-router'
+const routes = [
+    {
+        path: '/',
+        component: () => import('@/views/init-speed-track/index.vue'),
+    }
+]
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes,
+});
+
+export default router;

+ 13 - 0
src/store/index.ts

@@ -0,0 +1,13 @@
+import { createStore } from "vuex";
+import app from "./modules/app";
+import easyMap from "./modules/easy-map";
+
+export default createStore({
+  state: {},
+  mutations: {},
+  actions: {},
+  modules: {
+    app,
+    easyMap
+  },
+});

+ 18 - 0
src/store/modules/app.ts

@@ -0,0 +1,18 @@
+
+const state = {
+	apiProxy: {
+		EzServer6Api: 'EzServer6-api',	// 地图底图代理
+	},
+}
+const mutations = {
+}
+
+const actions = {
+}
+
+export default {
+	namespaced: true,
+	state,
+	mutations,
+	actions
+}

+ 38 - 0
src/store/modules/easy-map.ts

@@ -0,0 +1,38 @@
+const state = {
+	clickInfo: {
+		layerName: null,
+		value: null,
+		feature: null,
+		isClick: false,
+		e: null
+	},
+	hoverInfo: {
+		layerName: null,
+		value: null,
+		feature: null
+	},
+}
+
+const mutations = {
+	SET_INFO (state: { clickInfo: any; hoverInfo: any }, {data, type}: any) {
+		if (type === 'click') {
+			state.clickInfo = data
+		} else if (type === 'hover') {
+			state.hoverInfo = data
+		}
+	},
+}
+
+const actions = {
+	// @ts-ignore
+	LOAD_INFO ({ commit }, {data, type}) {
+		commit('SET_INFO', {data, type})
+	},
+}
+
+export default {
+	namespaced: true,
+	state,
+	mutations,
+	actions
+}

+ 89 - 0
src/style.css

@@ -0,0 +1,89 @@
+:root {
+  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+  line-height: 1.5;
+  font-weight: 400;
+
+  color-scheme: light dark;
+  color: rgba(255, 255, 255, 0.87);
+  background-color: #242424;
+
+  font-synthesis: none;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-text-size-adjust: 100%;
+}
+
+a {
+  font-weight: 500;
+  color: #646cff;
+  text-decoration: inherit;
+}
+a:hover {
+  color: #535bf2;
+}
+
+a {
+  font-weight: 500;
+  color: #646cff;
+  text-decoration: inherit;
+}
+a:hover {
+  color: #535bf2;
+}
+
+body {
+  margin: 0;
+  display: flex;
+  place-items: center;
+  min-width: 320px;
+  min-height: 100vh;
+}
+
+h1 {
+  font-size: 3.2em;
+  line-height: 1.1;
+}
+
+button {
+  border-radius: 8px;
+  border: 1px solid transparent;
+  padding: 0.6em 1.2em;
+  font-size: 1em;
+  font-weight: 500;
+  font-family: inherit;
+  background-color: #1a1a1a;
+  cursor: pointer;
+  transition: border-color 0.25s;
+}
+button:hover {
+  border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+  outline: 4px auto -webkit-focus-ring-color;
+}
+
+.card {
+  padding: 2em;
+}
+
+#app {
+  max-width: 1280px;
+  margin: 0 auto;
+  padding: 2rem;
+  text-align: center;
+}
+
+@media (prefers-color-scheme: light) {
+  :root {
+    color: #213547;
+    background-color: #ffffff;
+  }
+  a:hover {
+    color: #747bff;
+  }
+  button {
+    background-color: #f9f9f9;
+  }
+}

+ 54 - 0
src/style/cus-element.scss

@@ -0,0 +1,54 @@
+::-webkit-scrollbar { width: 6px; height: 10px; }
+::-webkit-scrollbar-track { width: 6px; background: rgba(#101F1C, 0.1);-webkit-border-radius: 2em; -moz-border-radius: 2em; border-radius: 2em; }
+::-webkit-scrollbar-thumb { background-color: rgba(#101F1C, 0.5); background-clip: padding-box; min-height: 28px; -webkit-border-radius: 2em; -moz-border-radius: 2em; border-radius: 2em; }
+::-webkit-scrollbar-thumb:hover { background-color: rgba(#101F1C, 1); }
+
+.easy-map-base-switch {
+  width: auto !important;
+  min-width: auto !important;
+  padding: 0 !important;
+  border: none !important;
+
+  .base-switch-item {
+    background: #f2f8fd;
+    border-radius: 6px;
+    opacity: .9;
+    display: flex;
+
+    .base-item {
+      flex: 0 0 auto;
+      width: 115px;
+      height: 70px;
+      position: relative;
+      margin: 10px 0 10px 10px;
+
+      &:last-child {
+        margin-right: 10px;
+      }
+
+      .label {
+        position: absolute;
+        right: 0;
+        bottom: 0;
+        padding: 0 3px;
+        font-size: 14px;
+        background: rgba(0, 0, 0, .3);
+        color: #fff;
+      }
+
+      >img {
+        width: 100%;
+        height: 100%;
+      }
+
+      &:hover,
+      &.active {
+        border: 2px solid #255fef;
+
+        .label {
+          background-color: #255fef;
+        }
+      }
+    }
+  }
+}

+ 391 - 0
src/utils/easyMap.ts

@@ -0,0 +1,391 @@
+import * as ol from 'ol'
+import * as style from 'ol/style'
+import * as layer from 'ol/layer'
+import * as source from 'ol/source'
+import * as geom from 'ol/geom'
+import * as proj from 'ol/proj'
+import * as format from 'ol/format'
+import * as extent from 'ol/extent'
+import store from '@/store/index'
+
+let hasPointClick = false
+let zIndex = 999
+let ctrlHideListGlobal = []
+const activeFeatureMap = new Map()
+const hoverFeatureMap = new Map()
+let moveFlag = false
+
+export const initShape = ({map, layerName, list, clickEl, hoverEl, hoverClick = false, clickHandle = (feature: any) => {}, layerZIndex}) => {
+  const _layers = map.getLayers().getArray().filter(v => v.get('easyMapLayerName') === layerName)
+  let realLayer = _layers.length > 0 ? _layers[0] : null
+  const features = []
+  const featuresMap = new Map()
+  list.forEach((v, i) => {
+    try {
+      const feat = new format.WKT().readFeature(v.easyMapParams.position)
+      feat.set('layerName', layerName)
+      feat.set('easyMap', v)
+      feat.set('_geom', feat.getGeometry())
+      feat.setStyle(v.easyMapParams.normalStyle)
+      feat.set('normalStyle', v.easyMapParams.normalStyle)
+      feat.set('activeStyle', v.easyMapParams.activeStyle ? v.easyMapParams.activeStyle : v.easyMapParams.normalStyle)
+      feat.set('hoverStyle', v.easyMapParams.hoverStyle ? v.easyMapParams.hoverStyle : (v.easyMapParams.activeStyle ? v.easyMapParams.activeStyle : v.easyMapParams.normalStyle))
+      feat.set('backStyle', v.easyMapParams.normalStyle)
+      feat.set('layerZ', realLayer ? realLayer.getZIndex() : layerZIndex ? layerZIndex : zIndex)
+      feat.set('featureZ', i)
+      feat.set('value', v.value)
+      feat.setId(v.easyMapParams.id)
+      v.easyMapParams.featureSetHandle?.(feat)
+      features.push(feat)
+      featuresMap.set(v.easyMapParams.id, feat)
+    } catch (e) {
+      console.log('v:\n%o  e:\n%o', v, e)
+    }
+  })
+  const vectorSource = new source.Vector({
+    features: features,
+    wrapX: false
+  });
+  let clickDialog = clickEl ? map.getOverlayById(clickEl.id) : null
+  let hoverDialog = hoverEl ? map.getOverlayById(hoverEl.id) : null
+  const clickClose = () => {
+    clickDialog?.setPosition(undefined)
+  }
+  const hoverClose = () => {
+    hoverDialog?.setPosition(undefined)
+  }
+  const setActive = (feature, isClick = false, e = null) => {
+    if (feature) {
+      feature.set('backStyle', feature.getStyle())
+      feature.setStyle(hoverFeatureMap.get(layerName)?.getId() === feature.getId() ? feature.get('hoverStyle') : feature.get('activeStyle'))
+      activeFeatureMap.set(layerName, feature)
+      store.dispatch('easyMap/LOAD_INFO', {
+        type: 'click',
+        data: {
+          layerName: layerName,
+          value: feature.get('easyMap'),
+          feature: feature,
+          isClick,
+          e
+        }
+      })
+    }
+  }
+  const setHover = (feature) => {
+    if (feature) {
+      if (hoverDialog && !(hoverClick || activeFeatureMap.get(layerName)?.getId() !== hoverFeatureMap.get(layerName)?.getId())) {
+        hoverClose()
+      }
+      feature.set('backStyle', feature.getStyle())
+      feature.setStyle(feature.get('hoverStyle'))
+      hoverFeatureMap.set(layerName, feature)
+      store.dispatch('easyMap/LOAD_INFO', {
+        type: 'hover',
+        data: {
+          layerName: layerName,
+          value: feature.get('easyMap'),
+          feature: feature
+        }
+      })
+    }
+  }
+  if (hoverFeatureMap.get(layerName)) {
+    hoverFeatureMap.set(layerName, vectorSource.getFeatureById(hoverFeatureMap.get(layerName).getId()))
+    setHover(hoverFeatureMap.get(layerName))
+  } else {
+    hoverFeatureMap.set(layerName, null)
+  }
+  if (activeFeatureMap.get(layerName)) {
+    activeFeatureMap.set(layerName, vectorSource.getFeatureById(activeFeatureMap.get(layerName).getId()))
+    setActive(activeFeatureMap.get(layerName))
+  } else {
+    activeFeatureMap.set(layerName, null)
+  }
+    if (realLayer) {
+    realLayer.setSource(vectorSource)
+  } else {
+    realLayer = new layer.VectorImage({
+      source: vectorSource,
+      easyMapLayerName: layerName,
+      layerType: 'easyMap',
+      zIndex: layerZIndex ? layerZIndex : zIndex--
+    })
+    map.addLayer(realLayer)
+    map.on('movestart', e => {
+      moveFlag = true
+      map.un('pointermove', mapPointerMove)
+      map.un('singleclick', mapSingleClick)
+      map.un('contextmenu', mapContextmenu)
+    })
+    map.on('moveend', e => {
+      map.on('singleclick', mapSingleClick)
+      map.on('pointermove', mapPointerMove)
+      map.on('contextmenu', mapContextmenu)
+      moveFlag = false
+    })
+    if (clickEl) {
+      clickDialog = setOverlay(clickEl)
+      map.addOverlay(clickDialog)
+    }
+    const mapSingleClick = e => {
+      let pointFlag = true
+      clickDialog?.setPosition(undefined)
+      // activeFeatureMap.get(layerName)?.setStyle(activeFeatureMap.get(layerName).get('normalStyle'))
+      let activeLayer = {
+        zIndex: 0,
+        layerName: null,
+      }
+      map.forEachFeatureAtPixel(e.pixel, (feature) => {
+        if (feature.get('layerZ') >= activeLayer.zIndex) {
+          activeLayer = {
+            zIndex: feature.get('layerZ'),
+            layerName: feature.get('layerName'),
+          }
+        }
+      }, {
+        hitTolerance: 0,
+      });
+      map.forEachFeatureAtPixel(e.pixel, (feature) => {
+        if (activeLayer.layerName === layerName && feature.get('layerName') === layerName && !hasPointClick) {
+          if (pointFlag) {
+            hasPointClick = true
+            if (activeFeatureMap.get(layerName)) {
+              activeFeatureMap.get(layerName).set('backStyle', activeFeatureMap.get(layerName).getStyle())
+              activeFeatureMap.get(layerName).setStyle(feature.get('normalStyle'))
+            }
+            pointFlag = false
+            if (!hoverClick) {
+              hoverClose()
+              hoverFeatureMap.delete(layerName)
+            }
+            setActive(feature, true, e)
+            clickHandle(feature)
+            if (clickDialog) {
+              if (feature.getGeometry().getType() !== 'Point') {
+                clickDialog.setPosition(e.coordinate)
+              } else {
+                clickDialog.setPosition(feature.getGeometry().getCoordinates())
+              }
+            }
+            setTimeout(() => {
+              hasPointClick = false
+            }, 100)
+          }
+        }
+      }, {
+        hitTolerance: 0,
+      });
+    }
+    map.on('singleclick', mapSingleClick)
+    if (hoverEl) {
+      hoverDialog = setOverlay(hoverEl)
+      map.addOverlay(hoverDialog)
+    }
+    let pointerMoveTime = null
+    const mapPointerMove = e => {
+      clearTimeout(pointerMoveTime)
+      pointerMoveTime = setTimeout(() => {
+        if (!moveFlag) {
+          let isFeature = false
+          let hoverLayer = {
+            zIndex: 0,
+            layerName: null
+          }
+          map.forEachFeatureAtPixel(e.pixel, (feature) => {
+            if (feature.get('layerZ') > hoverLayer.zIndex) {
+              hoverLayer = {
+                zIndex: feature.get('layerZ'),
+                layerName: feature.get('layerName')
+              }
+            }
+            isFeature = true
+          }, {
+            hitTolerance: 0,
+          });
+          const reset = () => {
+            hoverDialog?.setPosition(undefined)
+            hoverFeatureMap.get(layerName)?.setStyle(hoverFeatureMap.get(layerName).get('backStyle'))
+            hoverFeatureMap.delete(layerName)
+          }
+          if (layerName !== hoverLayer.layerName) {
+            reset()
+          }
+          if (!isFeature) {
+            reset()
+          }
+          let pointFlag = true
+          map.forEachFeatureAtPixel(e.pixel, (feature) => {
+            if (feature.get('layerName') === layerName) {
+              if (pointFlag) {
+                if (layerName !== hoverLayer.layerName) {
+                  hoverDialog?.setPosition(undefined)
+                } else {
+                  pointFlag = false
+                  if (hoverFeatureMap.get(layerName)?.getId() !== feature.getId()) {
+                    if (feature.getId() !== hoverFeatureMap.get(layerName)?.getId()) {
+                      reset()
+                    }
+                    setHover(feature)
+                    if (hoverDialog && (hoverClick || activeFeatureMap.get(layerName)?.getId() !== hoverFeatureMap.get(layerName)?.getId())) {
+                      if (feature.getGeometry().getType() !== 'Point') {
+                        hoverDialog.setPosition(e.coordinate)
+                      } else {
+                        hoverDialog.setPosition(feature.getGeometry().getCoordinates())
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }, {
+            hitTolerance: 0,
+          });
+        }
+      }, 10)
+    }
+    map.on('pointermove', mapPointerMove)
+    let ctrlHideList = []
+    const mapContextmenu = e => {
+      let isFeature = false
+      let hoverLayer = {
+        zIndex: 0,
+        layerName: null
+      }
+      map.forEachFeatureAtPixel(e.pixel, (feature) => {
+        if (feature.get('layerZ') > hoverLayer.zIndex) {
+          hoverLayer = {
+            zIndex: feature.get('layerZ'),
+            layerName: feature.get('layerName')
+          }
+        }
+        isFeature = true
+      }, {
+        hitTolerance: 0,
+      });
+      if (ctrlHideList.length > 0 && !window?.event?.ctrlKey && !window?.event.altKey) {
+        if (ctrlHideListGlobal[ctrlHideListGlobal.length - 1] === ctrlHideList[ctrlHideList.length - 1]) {
+          setTimeout(() => {
+            switchVisible([ctrlHideList.pop()], true)
+            ctrlHideListGlobal.pop()
+          }, 100)
+        }
+      } else if (ctrlHideList.length > 0 && window?.event.altKey) {
+        switchVisible(ctrlHideList, true)
+        ctrlHideList = []
+        ctrlHideListGlobal = []
+      }
+      let pointFlag = true
+      map.forEachFeatureAtPixel(e.pixel, (feature) => {
+        if (feature.get('layerName') === layerName) {
+          if (pointFlag) {
+            if (feature.get('layerName') === hoverLayer.layerName) {
+              pointFlag = false
+              if (window?.event?.ctrlKey) {
+                switchVisible([feature.get('easyMap').easyMapParams.id], false)
+                ctrlHideList.push(feature.get('easyMap').easyMapParams.id)
+                ctrlHideListGlobal.push(feature.get('easyMap').easyMapParams.id)
+              }
+            }
+          }
+        }
+      }, {
+        hitTolerance: 0,
+      });
+    }
+    map.on('contextmenu', mapContextmenu)
+  }
+  const switchVisible = (arr, show) => {
+    arr.forEach(v => {
+      const feat = featuresMap.get(v)
+      if (show) {
+        feat.setGeometry(feat.get('_geom'))
+      } else {
+        if (feat.getGeometry().getCoordinates().toString() !== [0, 0].toString()) {
+          feat.setGeometry(new geom.Point([0, 0]))
+        }
+      }
+    })
+  }
+  const removeRealLayer = () => {
+    map.removeOverlay(clickDialog)
+    map.removeOverlay(hoverDialog)
+    map.removeLayer(realLayer)
+  }
+  return {
+    clickDialog, hoverDialog, clickClose, hoverClose, switchVisible, features, realLayer, removeRealLayer, setActive
+  }
+}
+
+const setOverlay = (el) => {
+  const over = new ol.Overlay({
+    id: el.id,
+    element: el.element,
+    autoPan: false,
+    offset: el.offset,
+    positioning: el.positioning,
+    stopEvent: true,
+    autoPanAnimation: {
+      duration: 250
+    }
+  })
+  return over
+}
+
+/**
+ *
+ * @param map         地图实例,传了直接定位到中心点最大可视范围,不传返回中心点和最大分辨率
+ * @param position    坐标数组
+ * @param L           缩放比例系数,数值越小,要素边界越靠近中心
+ */
+export const getShapeView = (map, position, L = 600) => {
+  const center = extent.getCenter(extent.boundingExtent(position))
+  let x = 0
+  let y = 0
+  position.forEach(v => {
+    if (Math.abs(v[0] - center[0]) > x) {
+      x = Math.abs(v[0] - center[0])
+    }
+    if (Math.abs(v[1] - center[1]) > y) {
+      y = Math.abs(v[1] - center[1])
+    }
+  })
+  const resolution = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) / (L / document.body.clientWidth * document.body.clientHeight)
+  if (map) {
+    map.getView().animate({
+      center, resolution
+    })
+  }
+  return {
+    center, resolution
+  }
+}
+
+export const formatPosition = {
+  wptTwl: (arr) => {  // WKT POINT ARR TO WKT LINESTRING
+    const temp = arr.map(v => new format.WKT().readFeature(v).getGeometry().getCoordinates())
+    return new format.WKT().writeGeometry(new geom.LineString(proj.fromLonLat(temp, 'EPSG:4326')), {
+      dataProjection: 'EPSG:4326'
+    })
+  },
+  cptTwpt: (cpt) => {  // coordinates POINT TO WKT POINT
+    return `POINT(${cpt[0]} ${cpt[1]})`
+  },
+  wptTcpt: (wpt) => {  // WKT POINT TO coordinates POINT
+    return new format.WKT().readFeature(wpt).getGeometry().getCoordinates()
+  },
+  wpnTcpn: (wpn) => {  // WKT POLYGON TO coordinates POLYGON
+    return new format.WKT().readFeature(wpn).getGeometry().getCoordinates()
+  },
+  cpnTwpn: (cpn) => {  // coordinates POLYGON TO WKT POLYGON
+    return `POLYGON(${cpn.map(v => '(' + v.map(c => `${c[0]} ${c[1]}`).join(',') + ')')})`
+  },
+  cmpnTwmpn: (cmpt) => {  // coordinates MULTIPOLYGON TO WKT MULTIPOLYGON
+    return `MULTIPOLYGON(${cmpt.map(v1 => `(${v1.map(v => '(' + v.map(c => `${c[0]} ${c[1]}`).join(',') + ')')})`)})`
+  },
+  wmpnTcmpn: (wmpn) => {  // WKT MULTIPOLYGON TO coordinates MULTIPOLYGON
+    return new format.WKT().readFeature(wmpn).getGeometry().getCoordinates()
+  },
+  wlTcl: (wl) => {  // WKT LINESTRING TO coordinates LINESTRING
+    return new format.WKT().readFeature(wl).getGeometry().getCoordinates()
+  }
+}

+ 281 - 0
src/utils/util.ts

@@ -0,0 +1,281 @@
+export const isValue = (val: any) => {
+  if (val === null || val === undefined || val === '') {
+    return false
+  }
+  return true
+}
+
+export const structureParams = (array: Array<any>, attribute: string) => {
+  const endArray: any[] = []
+  array.forEach(v => {
+    endArray.push(v[attribute])
+  })
+  return endArray
+}
+
+export const replaceParams = (array: Array<any>, reArray: Array<any>, attribute: string, reAttribute: string) => {
+  const endArray: any[] = []
+  const endAllArray: any[] = []
+  array.forEach(v => {
+    reArray.forEach(rv => {
+      if (v === rv[attribute]) {
+        endArray.push(rv[reAttribute])
+        endAllArray.push(rv)
+      }
+    })
+  })
+  return {
+    replace: endArray,
+    all: endAllArray
+  }
+}
+
+export const copyObject = (ob: Object) => {
+  return JSON.parse(JSON.stringify(ob))
+}
+
+export const arrayToMap = (array: Array<any>, key: any) => {
+  const map = new Map()
+  array.forEach((v: any) => {
+    map.set(v[key], v)
+  })
+  return map
+}
+
+/**
+ * 通过某个字段在一个多级数组中查找数据
+ * @param data 目标数组,不能包含函数
+ * @param current 目标数据
+ * @param key 查找的字段
+ * @param children 子集集合字段
+ */
+export const findInListData = (data: Array<any>, current:any, key = "id", children = 'children') => {
+
+  for(let item of data){
+
+    if(item[key] && JSON.parse(JSON.stringify(item[key])) == JSON.parse(JSON.stringify(current))) return item
+
+    if(!!item[children] && Array.isArray(item[children]) && item[children].length > 0){
+
+      const findChildData = findInListData(item[children], current, key, children)
+
+      if(findChildData) return findChildData
+
+    }
+
+  }
+
+  return null
+}
+
+export const formatGetParam = (params: Object) => {
+  let paramUrl = ''
+  Object.keys(params).forEach((v, i) => {
+    paramUrl += i === 0 ? `${v}=${encodeURIComponent(params[v])}` : `&${v}=${encodeURIComponent(params[v])}`
+  })
+  return paramUrl
+}
+
+export const formatTableHeadFilters = (arr, text = 'dictLabel', value = 'dictValue') => {
+  return arr.map(v => {
+    v.value = v[value]
+    v.text = v[text]
+    return v
+  })
+}
+
+export const YMDHms = (date) => {
+  const _date = new Date(date)
+  const Y = `${_date.getFullYear()}`;
+  const M = `${_date.getMonth() + 1 < 10 ? `0${_date.getMonth() + 1}` : _date.getMonth() + 1}`;
+  const D = `${_date.getDate() + 1 < 10 ? `0${_date.getDate()}` : _date.getDate()}`;
+  const H = `${_date.getHours() < 10 ? `0${_date.getHours()}` : _date.getHours()}`;
+  const m = `${_date.getMinutes() < 10 ? `0${_date.getMinutes()}` : _date.getMinutes()}`;
+  const s = _date.getSeconds() < 10 ? `0${_date.getSeconds()}` : _date.getSeconds();
+  return `${Y}-${M}-${D} ${H}:${m}:${s}`;
+}
+
+export const YMD = (date) => {
+  const _date = new Date(date)
+  const Y = `${_date.getFullYear()}`;
+  const M = `${_date.getMonth() + 1 < 10 ? `0${_date.getMonth() + 1}` : _date.getMonth() + 1}`;
+  const D = `${_date.getDate() + 1 < 10 ? `0${_date.getDate()}` : _date.getDate()}`;
+  return `${Y}-${M}-${D}`;
+}
+
+//防抖
+export const debounce = function (cb, ms = 0) {
+  let timer = null
+  return function () {
+    if (timer) clearTimeout(timer)
+    timer = setTimeout(() => {
+      cb.apply(this, arguments)
+      timer = null
+    }, ms)
+  }
+}
+
+export const comTime = (time) => {
+  const sAll = time
+  const d = Math.floor(sAll / (1000 * 60 * 60 * 24))
+  const h = Math.floor((sAll - d * (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
+  const m = Math.floor((sAll - d * (1000 * 60 * 60 * 24) - h * (1000 * 60 * 60)) / (1000 * 60))
+  const s = Math.floor((sAll - d * (1000 * 60 * 60 * 24) - h * (1000 * 60 * 60) - m * (1000 * 60)) / 1000)
+  return{
+    d, h, m ,s
+  }
+}
+
+export const comTimeByArea = (start, end) => {
+  const sAll = new Date(end).getTime() - new Date(start).getTime()
+  const d = Math.floor(sAll / (1000 * 60 * 60 * 24))
+  const h = Math.floor((sAll - d * (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
+  const m = Math.floor((sAll - d * (1000 * 60 * 60 * 24) - h * (1000 * 60 * 60)) / (1000 * 60))
+  const s = Math.floor((sAll - d * (1000 * 60 * 60 * 24) - h * (1000 * 60 * 60) - m * (1000 * 60)) / 1000)
+  return{
+    d, h, m ,s
+  }
+}
+
+export const deepAssign = (...obj) => {
+  const result = Object.assign({}, ...obj)
+  for (let item of obj) {
+    for (let [idx, val] of Object.entries(item)) {
+      if (val instanceof Array) {
+        result[idx] = val
+      } else if (val instanceof Object) {
+        result[idx] = deepAssign(result[idx], val)
+      }
+    }
+  }
+  return result
+}
+
+export const copy = (value) => {
+  const str = document.createElement('input')
+  str.setAttribute('value', value)
+  document.body.appendChild(str)
+  str.select()
+  document.execCommand('copy')
+  document.body.removeChild(str)
+  console.log(value)
+}
+
+/**
+ *
+ * @param precision 精度  10.10.01……
+ * @param colorArr
+ * [
+ *    [20.1, '#111111'],
+ *    [20.3, '#dddddd'],
+ *    [20.7, '#eeeeee'],
+ * ]
+ * @return colorMap
+ * new Map([
+ *    [20.1, '#111111']
+ *    ……
+ *    [20.3, '#dddddd']
+ *    ……
+ *    [20.7, '#eeeeee']
+ * ])
+ */
+export const getGradientColorArray = (precision, colorArr) => {
+  // 将hex表示方式转换为rgb表示方式(这里返回rgb数组模式)
+  const colorRgb = (sColor) => {
+    const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
+    let _sColor = sColor.toLowerCase();
+    if (_sColor && reg.test(_sColor)) {
+      if (_sColor.length === 4) {
+        let sColorNew = "#";
+        for (let i = 1; i < 4; i += 1) {
+          sColorNew += _sColor.slice(i, i + 1).concat(_sColor.slice(i, i + 1));
+        }
+        _sColor = sColorNew;
+      }
+      //处理六位的颜色值
+      const sColorChange = [];
+      for (let i = 1; i < 7; i += 2) {
+        sColorChange.push(parseInt("0x" + _sColor.slice(i, i + 2)));
+      }
+      return sColorChange;
+    } else {
+      return _sColor;
+    }
+  };
+  // 将rgb表示方式转换为hex表示方式
+  const colorHex = (rgb) => {
+    const _this = rgb;
+    const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
+    if (/^(rgb|RGB)/.test(_this)) {
+      const aColor = _this.replace(/(?:(|)|rgb|RGB)*/g, "").split(",");
+      let strHex = "#";
+      for (let i = 0; i < aColor.length; i++) {
+        let hex = Number(aColor[i]).toString(16);
+        hex = hex < 10 ? 0 + '' + hex : hex;// 保证每个rgb的值为2位
+        if (hex === "0") {
+          hex += hex;
+        }
+        strHex += hex;
+      }
+      if (strHex.length !== 7) {
+        strHex = _this;
+      }
+      return strHex;
+    } else if (reg.test(_this)) {
+      const aNum = _this.replace(/#/, "").split("");
+      if (aNum.length === 6) {
+        return _this;
+      } else if (aNum.length === 3) {
+        let numHex = "#";
+        for (let i = 0; i < aNum.length; i += 1) {
+          numHex += (aNum[i] + aNum[i]);
+        }
+        return numHex;
+      }
+    } else {
+      return _this;
+    }
+  }
+  const rgb2hex = (sRGB) => {
+    const reg = /^(RGB|rgb)\((\d+),\s*(\d+),\s*(\d+)\)$/
+    if (!reg.test(sRGB)) return sRGB
+    const rgbArr = sRGB.match(/\d+/g)
+    const resultRgbArr = rgbArr.map(v => {
+      if (+v > 16) return (+v).toString(16)
+      return '0' + (+v).toString(16)
+    })
+    return '#' + resultRgbArr.join('')
+  }
+  const gradientColor = (startColor, endColor, step) => {
+    const startRGB = colorRgb(startColor);//转换为rgb数组模式
+    const startR = startRGB[0];
+    const startG = startRGB[1];
+    const startB = startRGB[2];
+    const endRGB = colorRgb(endColor);
+    const endR = endRGB[0];
+    const endG = endRGB[1];
+    const endB = endRGB[2];
+    const sR = (endR - startR) / step;//总差值
+    const sG = (endG - startG) / step;
+    const sB = (endB - startB) / step;
+    const colorArr = [];
+    for (let i = 0; i <= step; i++) {
+      //计算每一步的hex值
+      const hex = colorHex('rgb(' + parseInt((sR * i + startR)) + ',' + parseInt((sG * i + startG)) + ',' + parseInt((sB * i + startB)) + ')');
+      colorArr.push(rgb2hex(hex));
+    }
+    return colorArr;
+  }
+  const colorMap = new Map()
+  colorArr.forEach((v, i) => {
+    if (i < colorArr.length - 1) {
+      const _arr = gradientColor(v[1], colorArr[i + 1][1], (Number(colorArr[i + 1][0]) - Number(v[0])) / precision)
+      _arr.forEach((cV, cI) => {
+        colorMap.set((Number(v[0]) + cI * precision).toFixed(String(precision).split('').filter(p => p === '0').length), cV)
+      })
+    } else {
+      colorMap.set(Number(v[0]).toFixed(String(precision).split('').filter(p => p === '0').length), v[1])
+    }
+  })
+  return colorMap
+}

BIN
src/views/init-speed-track/button-del.png


+ 256 - 0
src/views/init-speed-track/drawTrack.ts

@@ -0,0 +1,256 @@
+import * as ol from 'ol'
+import * as style from 'ol/style'
+import * as layer from 'ol/layer'
+import * as source from 'ol/source'
+import * as geom from 'ol/geom'
+import * as proj from 'ol/proj'
+import * as interaction from 'ol/interaction'
+import * as coordinate from 'ol/coordinate'
+import * as control from 'ol/control'
+import * as sphere from 'ol/sphere'
+import { unByKey } from 'ol/Observable'
+import './measure.scss'
+import {createBox} from "ol/interaction/Draw";
+import {Circle, LineString, Polygon} from "ol/geom";
+
+const layerFlag = ['layerName', 'measureLayer']
+let measureTooltipElement;
+let helpTooltipElement;
+/**
+ *
+ * @param map
+ * @param typeSelect    line线,rectangle矩形,polygon多边形,circle圆形
+ */
+export default function Measure (map, typeSelect) {
+    let _source = null
+    const realLayer = map.getLayers().getArray().filter(v => v.get(layerFlag[0]) === layerFlag[1])
+    if (realLayer[0]) {
+        _source = realLayer[0].getSource()
+    } else {
+        _source = new source.Vector(); //图层数据源
+        const _vector = new layer.Vector({
+            zIndex: 9999,
+            source: _source,
+            style: new style.Style({ //图层样式
+                fill: new style.Fill({
+                    color: 'rgba(255, 255, 255, 0.2)' //填充颜色
+                }),
+                stroke: new style.Stroke({
+                    color: '#f31a4a',  //边框颜色
+                    width: 2   // 边框宽度
+                }),
+                image: new style.Circle({
+                    radius: 7,
+                    fill: new style.Fill({
+                        color: '#ffcc33'
+                    })
+                })
+            })
+        });
+        _vector.set(layerFlag[0], layerFlag[1])
+        map.addLayer(_vector);
+    }
+    let sketch;
+    let helpTooltip;
+    let measureTooltip;
+    let continueMsg = '双击结束测量';
+    const geodesicCheckbox = true;//测地学方式对象
+    const createMeasureTooltip = () => {
+        const id = 'measureTooltipElementId'
+        if (measureTooltipElement) {
+            map.removeOverlay(map.getOverlayById(id))
+            measureTooltipElement.parentNode.removeChild(measureTooltipElement);
+        }
+        measureTooltipElement = document.createElement('div');
+        measureTooltipElement.className = 'tooltip tooltip-measure';
+        measureTooltip = new ol.Overlay({
+            id,
+            element: measureTooltipElement,
+            offset: [0, -15],
+            positioning: 'bottom-center'
+        });
+        map.addOverlay(measureTooltip);
+    }
+    const createHelpTooltip = () => {
+        const id = 'helpTooltipElementId'
+        if (helpTooltipElement) {
+            map.removeOverlay(map.getOverlayById(id))
+            helpTooltipElement.parentNode.removeChild(helpTooltipElement);
+        }
+        helpTooltipElement = document.createElement('div');
+        helpTooltipElement.className = 'tooltip hidden';
+        helpTooltip = new ol.Overlay({
+            id,
+            element: helpTooltipElement,
+            offset: [15, 0],
+            positioning: 'center-left'
+        });
+        map.addOverlay(helpTooltip);
+    }
+    const formatLength = (line) => {
+        // 获取投影坐标系
+        const sourceProj = map.getView().getProjection();
+        // ol/sphere里有getLength()和getArea()用来测量距离和区域面积,默认的投影坐标系是EPSG:3857, 其中有个options的参数,可以设置投影坐标系
+        const length = sphere.getLength(line, {projection: sourceProj});
+        // const length = getLength(line);
+        let output;
+        if (length > 100) {
+            const km = Math.round((length / 1000) * 100) / 100;
+            output = `${km} 千米 <br>${parseFloat(String(km * 0.53995)).toFixed(2)} 海里`;
+        } else {
+            output = `${Math.round(length * 100) / 100} m`;
+        }
+        return output;
+    };
+    //获取圆的面积
+    const getCircleArea = (circle, projection) => {
+        const P = 3.14
+        const radius = getCircleRadius(circle, projection)
+        return P * radius * radius
+    }
+//获取圆的半径
+    const getCircleRadius = (circle, projection) => {
+        return circle.getRadius() * projection.getMetersPerUnit()
+    }
+    const formatArea = (polygon, type= 'polygon') => {
+        let area
+        const sourceProj = map.getView().getProjection();
+        // 获取投影坐标系
+        if (type === 'polygon') {
+            area = sphere.getArea(polygon, {
+                projection: sourceProj,
+            });
+        } else if (type === 'circle') {
+            area = getCircleArea(polygon, sourceProj)
+        }
+        let output;
+        if (area > 10000) {
+            const km = Math.round((area / 1000000) * 100) / 100;
+            output = `${km} 平方公里<br>${parseFloat(String(km * 0.38610)).toFixed(
+                2
+            )} 平方英里`;
+        } else {
+            output = `${Math.round(area * 100) / 100} ` + " m<sup>2</sup>";
+        }
+        return output;
+    };
+    const addInteraction = () => {
+        const id = 'drawName'
+        const draw = new interaction.Draw({
+            source: _source,//测量绘制层数据源
+            type: 'LineString',  //几何图形类型
+            geometryFunction: typeSelect === 'rectangle' ? createBox() : null,
+            style: new style.Style({
+                fill: new style.Fill({
+                    color: "rgba(255, 255, 255, 0.2)",
+                }),
+                stroke: new style.Stroke({
+                    color: "#f3584a",
+                    width: 2,
+                }),
+                image: new style.Circle({
+                    radius: 5,
+                    stroke: new style.Stroke({
+                        color: "rgba(0, 0, 0, 0.7)",
+                    }),
+                    fill: new style.Fill({
+                        color: "rgba(255, 255, 255, 0.2)",
+                    }),
+                }),
+            }),
+        });
+        draw.set(id, id)
+        createMeasureTooltip(); //创建测量工具提示框
+        createHelpTooltip(); //创建帮助提示框
+        map.addInteraction(draw);
+        let listener;
+        //绑定交互绘制工具开始绘制的事件
+        const drawstartHandle = (evt) => {
+            sketch = evt.feature; //绘制的要素
+            let tooltipCoord = evt.coordinate;// 绘制的坐标
+            //绑定change事件,根据绘制几何类型得到测量长度值或面积值,并将其设置到测量工具提示框中显示
+            listener = sketch.getGeometry().on('change', function (evt) {
+                const geom = evt.target
+                let output;
+                if (geom.getType() === 'LineString') {
+                    output = formatLength(geom);//长度值
+                    tooltipCoord = geom.getLastCoordinate();//坐标
+                } else if (geom.getType() === 'Polygon') {
+                    output = formatArea(geom);//面积值
+                    tooltipCoord = geom.getInteriorPoint().getCoordinates();//坐标
+                } else if (geom.getType() === 'Circle') {
+                    output = formatArea(geom, 'circle');//面积值
+                    tooltipCoord = geom.getCenter()
+                }
+                measureTooltipElement.innerHTML = output;//将测量值设置到测量工具提示框中显示
+                measureTooltip.setPosition(tooltipCoord);//设置测量工具提示框的显示位置
+            });
+        }
+        draw.on('drawstart', drawstartHandle);
+        //绑定交互绘制工具结束绘制的事件
+        const copy = (value) => {
+            const str = document.createElement('input')
+            str.setAttribute('value', value)
+            document.body.appendChild(str)
+            str.select()
+            document.execCommand('copy')
+            document.body.removeChild(str)
+        }
+        const drawendHandle = (evt) => {
+            map.removeInteraction(map.getInteractions().getArray().filter(v => v.get(id) === id)[0]);
+            const del = document.createElement("div");
+            del.className = "lineDel";
+            measureTooltipElement.append(del);
+            del.onclick = () => {
+                _source.removeFeature(evt.feature)
+                const b = del.parentElement.parentElement
+                b.parentElement.removeChild(b);
+                const g = evt.feature.getGeometry()
+                if (g.getType() === 'LineString') {
+                    const w = `LINESTRING(${g.getCoordinates().map(v => v[0] + ' ' + v[1]).join(',')})`
+                    copy(w)
+                } else if (g.getType() === 'Polygon') {
+                    const w = `POLYGON(${g.getCoordinates().map(v => '(' + v.map(c => c[0] + ' ' + c[1]) + ')').join(',')})`
+                    copy(w)
+                }
+            };
+            measureTooltipElement.className = 'tooltip tooltip-static'; //设置测量提示框的样式
+            measureTooltip.setOffset([0, -7]);
+            sketch = null; //置空当前绘制的要素对象
+            measureTooltipElement = null; //置空测量工具提示框对象
+            helpTooltipElement.parentNode.removeChild(helpTooltipElement);
+            helpTooltipElement = null; //置空测量工具提示框对象
+            unByKey(listener);
+            draw.un('drawstart', drawstartHandle);
+            draw.un('drawend', drawendHandle);
+            map.removeInteraction(map.getInteractions().getArray().filter(v => v.get(id) === id)[0]);
+            map.un('pointermove', pointerMoveHandler)
+        }
+        draw.on('drawend', drawendHandle);
+    }
+    addInteraction(); //调用加载绘制交互控件方法,添加绘图进行测量
+    const pointerMoveHandler = (evt) => {
+        if (evt.dragging) {
+            return;
+        }
+        let helpMsg = '单击开始测量';//当前默认提示信息
+        //判断绘制几何类型设置相应的帮助提示信息
+        if (sketch) {
+            const geom = sketch.getGeometry()
+            helpMsg = continueMsg;
+            // if (geom.getType() === 'Polygon') {
+            //     helpMsg = continueMsg; //绘制多边形时提示相应内容
+            // } else if (geom.getType() === 'LineString') {
+            //     helpMsg = continueMsg; //绘制线时提示相应内容
+            // }
+        }
+        helpTooltipElement.innerHTML = helpMsg; //将提示信息设置到对话框中显示
+        helpTooltip.setPosition(evt.coordinate);//设置帮助提示框的位置
+        helpTooltipElement.classList.remove('hidden');//移除帮助提示框的隐藏样式进行显示
+    };
+    map.on('pointermove', pointerMoveHandler); //地图容器绑定鼠标移动事件,动态显示帮助提示框内容
+    //地图绑定鼠标移出事件,鼠标移出时为帮助提示框设置隐藏样式
+    map.getViewport().on('mouseout', () => {
+        helpTooltipElement.addClass('hidden');
+    });
+}

BIN
src/views/init-speed-track/edit.png


+ 916 - 0
src/views/init-speed-track/index.vue

@@ -0,0 +1,916 @@
+<template>
+  <div class="init-speed-track" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.5)">
+    <EasyMapComponent
+        class="map"
+        :showBaseSwitch="true"
+        @easyMapLoad="mapLoad"
+    />
+    <div class="track">
+      <el-card shadow="always">
+        <template #header>
+          <div class="card-header">
+            <span>轨迹列表</span>
+            <el-button-group>
+              <template v-for="[key, value] in SourceMap">
+                <el-button :color="value.color" size="small" @click="drawTrack(key)" style="color: white">{{value.label}}</el-button>
+              </template>
+            </el-button-group>
+            <el-cascader
+                v-model="ddd"
+                :options="options"
+                :props="{
+                  // props.
+                  multiple: true,
+                  checkStrictly: true,
+                  emitPath: false
+                }"
+            />
+          </div>
+        </template>
+        <div class="track-line">
+          <el-button v-if="trackList.length > 0" type="primary" size="small" @click="onSubmit" style="color: white">保存</el-button>
+          <template v-for="(item, index) in trackList">
+            <div class="line">
+              <div class="label" :style="`color: ${SourceMap.get(item.type).color};`">{{item.ID}}、{{SourceMap.get(item.type).label}}</div>
+              <el-tooltip :enterable="false" placement="top" content="隐藏" v-if="item.show">
+                <img class="__hover" src="./ship-track-visible.svg" @click="handleShow(false, item)"/>
+              </el-tooltip>
+              <el-tooltip :enterable="false" placement="top" content="显示" v-else>
+                <img class="__hover" src="./ship-track-invisible.svg" @click="handleShow(true, item)"/>
+              </el-tooltip>
+              <el-tooltip :enterable="false" placement="top" content="编辑">
+                <img class="__hover" src="./edit.png" @click="handleEdit(item)"/>
+              </el-tooltip>
+              <el-tooltip :enterable="false" placement="top" content="删除">
+                <img class="__hover" src="./button-del.png" @click="handleDelete(item, index)"/>
+              </el-tooltip>
+            </div>
+          </template>
+        </div>
+      </el-card>
+      <el-card shadow="always" v-if="isForm">
+        <template #header>
+          <div class="card-header">
+            <span>轨迹点列表</span>
+            <el-button v-if="trackPointList.length > 0" type="primary" size="small" @click="onTemp" style="color: white">暂存</el-button>
+          </div>
+        </template>
+        <div class="track-point">
+          <div v-if="form.ID" class="label" :style="`color: ${SourceMap.get(form.type).color};`">{{form.ID}}、{{SourceMap.get(form.type).label}}</div>
+          <template v-for="item in formParams">
+            <div class="item">
+              <span>{{item.label}}:</span><el-input v-model="form[item.value]" clearable/>
+            </div>
+          </template>
+          <template v-for="(item, index) in trackPointList">
+            <div class="point">
+              <div class="position">
+                <span>{{item.position[0]}}</span><br/>
+                <span>{{item.position[1]}}</span>
+              </div>
+              <div class="speed">
+                <el-input-number v-model="item.speed" :precision="2" :step="0.1" :max="100" :min="0" @focus="onPointFocus(trackPointList[index - 1], item, trackPointList[index + 1])"/>
+              </div>
+            </div>
+          </template>
+        </div>
+      </el-card>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  ref,
+  nextTick,
+  onMounted,
+  watch,
+  computed,
+  ComponentInternalInstance,
+  reactive,
+  toRefs,
+  getCurrentInstance
+} from 'vue'
+import {useStore} from 'vuex'
+import * as source from "ol/source";
+import * as layer from "ol/layer";
+import * as style from "ol/style";
+import * as ol from "ol";
+import * as sphere from "ol/sphere";
+import * as interaction from "ol/interaction";
+import {createBox} from "ol/interaction/Draw";
+import {unByKey} from "ol/Observable";
+import { Geometry } from 'ol/geom';
+import { EventsKey } from 'ol/events';
+import { Coordinate } from 'ol/coordinate';
+import TrackStyle from './track-style'
+import axios from "axios";
+import {ElMessage, ElMessageBox} from "element-plus";
+
+export default defineComponent({
+  name: 'App',
+  components: {},
+  setup() {
+    const store = useStore()
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    const SourceMap = new Map(window.cusConfig.trackSource)
+    const formParams = window.cusConfig.trackParams
+    const options = [
+      {
+        value: 'guide',
+        label: 'Guide',
+        children: [
+          {
+            value: 'disciplines',
+            label: 'Disciplines',
+            children: [
+              {
+                value: 'consistency',
+                label: 'Consistency',
+              },
+              {
+                value: 'feedback',
+                label: 'Feedback',
+              },
+              {
+                value: 'efficiency',
+                label: 'Efficiency',
+              },
+              {
+                value: 'controllability',
+                label: 'Controllability',
+              },
+            ],
+          },
+          {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [
+              {
+                value: 'side nav',
+                label: 'Side Navigation',
+              },
+              {
+                value: 'top nav',
+                label: 'Top Navigation',
+              },
+            ],
+          },
+        ],
+      },
+      {
+        value: 'component',
+        label: 'Component',
+        children: [
+          {
+            value: 'basic',
+            label: 'Basic',
+            children: [
+              {
+                value: 'layout',
+                label: 'Layout',
+              },
+              {
+                value: 'color',
+                label: 'Color',
+              },
+              {
+                value: 'typography',
+                label: 'Typography',
+              },
+              {
+                value: 'icon',
+                label: 'Icon',
+              },
+              {
+                value: 'button',
+                label: 'Button',
+              },
+            ],
+          },
+          {
+            value: 'form',
+            label: 'Form',
+            children: [
+              {
+                value: 'radio',
+                label: 'Radio',
+              },
+              {
+                value: 'checkbox',
+                label: 'Checkbox',
+              },
+              {
+                value: 'input',
+                label: 'Input',
+              },
+              {
+                value: 'input-number',
+                label: 'InputNumber',
+              },
+              {
+                value: 'select',
+                label: 'Select',
+              },
+              {
+                value: 'cascader',
+                label: 'Cascader',
+              },
+              {
+                value: 'switch',
+                label: 'Switch',
+              },
+              {
+                value: 'slider',
+                label: 'Slider',
+              },
+              {
+                value: 'time-picker',
+                label: 'TimePicker',
+              },
+              {
+                value: 'date-picker',
+                label: 'DatePicker',
+              },
+              {
+                value: 'datetime-picker',
+                label: 'DateTimePicker',
+              },
+              {
+                value: 'upload',
+                label: 'Upload',
+              },
+              {
+                value: 'rate',
+                label: 'Rate',
+              },
+              {
+                value: 'form',
+                label: 'Form',
+              },
+            ],
+          },
+          {
+            value: 'data',
+            label: 'Data',
+            children: [
+              {
+                value: 'table',
+                label: 'Table',
+              },
+              {
+                value: 'tag',
+                label: 'Tag',
+              },
+              {
+                value: 'progress',
+                label: 'Progress',
+              },
+              {
+                value: 'tree',
+                label: 'Tree',
+              },
+              {
+                value: 'pagination',
+                label: 'Pagination',
+              },
+              {
+                value: 'badge',
+                label: 'Badge',
+              },
+            ],
+          },
+          {
+            value: 'notice',
+            label: 'Notice',
+            children: [
+              {
+                value: 'alert',
+                label: 'Alert',
+              },
+              {
+                value: 'loading',
+                label: 'Loading',
+              },
+              {
+                value: 'message',
+                label: 'Message',
+              },
+              {
+                value: 'message-box',
+                label: 'MessageBox',
+              },
+              {
+                value: 'notification',
+                label: 'Notification',
+              },
+            ],
+          },
+          {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [
+              {
+                value: 'menu',
+                label: 'Menu',
+              },
+              {
+                value: 'tabs',
+                label: 'Tabs',
+              },
+              {
+                value: 'breadcrumb',
+                label: 'Breadcrumb',
+              },
+              {
+                value: 'dropdown',
+                label: 'Dropdown',
+              },
+              {
+                value: 'steps',
+                label: 'Steps',
+              },
+            ],
+          },
+          {
+            value: 'others',
+            label: 'Others',
+            children: [
+              {
+                value: 'dialog',
+                label: 'Dialog',
+              },
+              {
+                value: 'tooltip',
+                label: 'Tooltip',
+              },
+              {
+                value: 'popover',
+                label: 'Popover',
+              },
+              {
+                value: 'card',
+                label: 'Card',
+              },
+              {
+                value: 'carousel',
+                label: 'Carousel',
+              },
+              {
+                value: 'collapse',
+                label: 'Collapse',
+              },
+            ],
+          },
+        ],
+      },
+      {
+        value: 'resource',
+        label: 'Resource',
+        children: [
+          {
+            value: 'axure',
+            label: 'Axure Components',
+          },
+          {
+            value: 'sketch',
+            label: 'Sketch Templates',
+          },
+          {
+            value: 'docs',
+            label: 'Design Documentation',
+          },
+        ],
+      },
+    ]
+    const state = reactive({
+      map: <any>null,
+      mapFunc: null,
+      trackPointList: [],
+      trackList: <any>[],
+      formTrackPointStartCount: 0,
+      formTrackPointEndCount: 0,
+      formTrackPointList: [],
+      initTrackPointStartCount: 0,
+      initTrackPointEndCount: 0,
+      initTrackPointList: [],
+      loading: false,
+      form: <any>{},
+      idNum: 1,
+      isForm: false,
+      ddd: null
+    });
+    formParams.forEach((v: { value: string | number; init: any; }) => {
+      state.form[v.value] = v.init
+    })
+    const mapLoad = (map: null, func: null) => {
+      state.map = map
+      state.mapFunc = func
+    }
+    const startDraw = (cb: { (evt: any): void; (arg0: { feature: { getGeometry: () => any; }; }): void; }) => {
+      let measureTooltipElement: HTMLDivElement | null;
+      let helpTooltipElement: HTMLDivElement | null;
+      const realLayer = state.map.getLayers().getArray().filter((v: { get: (arg0: string) => string; }) => v.get('layerName') === 'measureLayer')
+      let sketch: { getGeometry: () => { (): any; new(): any; on: { (arg0: string, arg1: (evt: any) => void): any; new(): any; }; }; } | null;
+      let helpTooltip: ol.Overlay;
+      let measureTooltip: ol.Overlay;
+      let continueMsg = '双击结束标绘';
+      const createMeasureTooltip = () => {
+        const id = 'measureTooltipElementId'
+        if (measureTooltipElement) {
+          state.map.removeOverlay(state.map.getOverlayById(id))
+          measureTooltipElement.parentNode?.removeChild(measureTooltipElement);
+        }
+        measureTooltipElement = document.createElement('div');
+        measureTooltipElement.className = 'tooltip tooltip-measure';
+        measureTooltip = new ol.Overlay({
+          id,
+          element: measureTooltipElement,
+          offset: [0, -15],
+          positioning: 'bottom-center'
+        });
+        state.map.addOverlay(measureTooltip);
+      }
+      const createHelpTooltip = () => {
+        const id = 'helpTooltipElementId'
+        if (helpTooltipElement) {
+          state.map.removeOverlay(state.map.getOverlayById(id))
+          helpTooltipElement.parentNode?.removeChild(helpTooltipElement);
+        }
+        helpTooltipElement = document.createElement('div');
+        helpTooltipElement.className = 'tooltip hidden';
+        helpTooltip = new ol.Overlay({
+          id,
+          element: helpTooltipElement,
+          offset: [15, 0],
+          positioning: 'center-left'
+        });
+        state.map.addOverlay(helpTooltip);
+      }
+      const formatLength = (line: Geometry) => {
+        // 获取投影坐标系
+        const sourceProj = state.map.getView().getProjection();
+        // ol/sphere里有getLength()和getArea()用来测量距离和区域面积,默认的投影坐标系是EPSG:3857, 其中有个options的参数,可以设置投影坐标系
+        const length = sphere.getLength(line, {projection: sourceProj});
+        // const length = getLength(line);
+        let output;
+        if (length > 100) {
+          const km = Math.round((length / 1000) * 100) / 100;
+          output = `${km} 千米 <br>${parseFloat(String(km * 0.53995)).toFixed(2)} 海里`;
+        } else {
+          output = `${Math.round(length * 100) / 100} m`;
+        }
+        return output;
+      };
+      const addInteraction = () => {
+        const id = 'drawName'
+        const draw = new interaction.Draw({
+          type: 'LineString',  //几何图形类型
+          style: new style.Style({
+            fill: new style.Fill({
+              color: "rgba(255, 255, 255, 0.2)",
+            }),
+            stroke: new style.Stroke({
+              color: "#f3584a",
+              width: 2,
+            }),
+            image: new style.Circle({
+              radius: 5,
+              stroke: new style.Stroke({
+                color: "rgba(0, 0, 0, 0.7)",
+              }),
+              fill: new style.Fill({
+                color: "rgba(255, 255, 255, 0.2)",
+              }),
+            }),
+          }),
+        });
+        draw.set(id, id)
+        createMeasureTooltip(); //创建测量工具提示框
+        createHelpTooltip(); //创建帮助提示框
+        state.map.addInteraction(draw);
+        let listener: EventsKey | EventsKey[];
+        //绑定交互绘制工具开始绘制的事件
+        const drawstartHandle = (evt: { feature: { getGeometry: () => { (): any; new(): any; on: { (arg0: string, arg1: (evt: any) => void): any; new(): any; }; }; } | null; coordinate: any; }) => {
+          sketch = evt.feature; //绘制的要素
+          let tooltipCoord = evt.coordinate;// 绘制的坐标
+          //绑定change事件,根据绘制几何类型得到测量长度值或面积值,并将其设置到测量工具提示框中显示
+          listener = sketch?.getGeometry().on('change', function (evt) {
+            const geom = evt.target
+            let output;
+            output = formatLength(geom);//长度值
+            tooltipCoord = geom.getLastCoordinate();//坐标
+            if (measureTooltipElement) measureTooltipElement.innerHTML = output;//将测量值设置到测量工具提示框中显示
+            measureTooltip.setPosition(tooltipCoord);//设置测量工具提示框的显示位置
+          });
+        }
+        draw.on('drawstart', drawstartHandle);
+        //绑定交互绘制工具结束绘制的事件
+        const copy = (value: string) => {
+          const str = document.createElement('input')
+          str.setAttribute('value', value)
+          document.body.appendChild(str)
+          str.select()
+          document.execCommand('copy')
+          document.body.removeChild(str)
+        }
+        const drawendHandle = (evt: { feature: { getGeometry: () => any; }; }) => {
+          state.map.removeInteraction(state.map.getInteractions().getArray().filter((v: { get: (arg0: string) => string; }) => v.get(id) === id)[0]);
+          sketch = null; //置空当前绘制的要素对象
+          measureTooltipElement?.parentNode?.removeChild(measureTooltipElement);
+          measureTooltipElement = null; //置空测量工具提示框对象
+          helpTooltipElement?.parentNode?.removeChild(helpTooltipElement);
+          helpTooltipElement = null; //置空测量工具提示框对象
+          unByKey(listener);
+          draw.un('drawstart', drawstartHandle);
+          draw.un('drawend', drawendHandle);
+          state.map.removeInteraction(state.map.getInteractions().getArray().filter((v: { get: (arg0: string) => string; }) => v.get(id) === id)[0]);
+          state.map.un('pointermove', pointerMoveHandler)
+          cb(evt)
+        }
+        draw.on('drawend', drawendHandle);
+      }
+      addInteraction(); //调用加载绘制交互控件方法,添加绘图进行测量
+      const pointerMoveHandler = (evt: { dragging: any; coordinate: Coordinate | undefined; }) => {
+        if (evt.dragging) {
+          return;
+        }
+        let helpMsg = '单击开始标绘';//当前默认提示信息
+        //判断绘制几何类型设置相应的帮助提示信息
+        if (sketch) {
+          const geom = sketch.getGeometry()
+          helpMsg = continueMsg;
+          // if (geom.getType() === 'Polygon') {
+          //     helpMsg = continueMsg; //绘制多边形时提示相应内容
+          // } else if (geom.getType() === 'LineString') {
+          //     helpMsg = continueMsg; //绘制线时提示相应内容
+          // }
+        }
+        if (helpTooltipElement)helpTooltipElement.innerHTML = helpMsg; //将提示信息设置到对话框中显示
+        helpTooltip.setPosition(evt.coordinate);//设置帮助提示框的位置
+        helpTooltipElement?.classList.remove('hidden');//移除帮助提示框的隐藏样式进行显示
+      };
+      state.map.on('pointermove', pointerMoveHandler); //地图容器绑定鼠标移动事件,动态显示帮助提示框内容
+      //地图绑定鼠标移出事件,鼠标移出时为帮助提示框设置隐藏样式
+      // state.map.getViewport().on('mouseout', () => {
+      //   helpTooltipElement?.addClass('hidden');
+      // });
+    }
+    const initDraw = (pMap, type) => {
+      that.$easyMap.initShape({
+        map: state.map,
+        layerName: "form-track-point-line",
+        layerZIndex: 9,
+        list: [
+          {
+            easyMapParams: {
+              id: new Date().getTime(),
+              position: that.$easyMap.formatPosition.wptTwl(state.trackPointList.map(v => that.$easyMap.formatPosition.cptTwpt(v.position))),
+              normalStyle: (f: any, r: any) => TrackStyle.trackLineStyle(f, r, state.map, SourceMap.get(type)?.color, pMap, (s, p) => {
+                state.formTrackPointStartCount++
+                setTimeout(() => {
+                  state.formTrackPointEndCount++
+                  state.formTrackPointList.push(...p)
+                  if (state.formTrackPointStartCount === state.formTrackPointEndCount) {
+                    that.$easyMap.initShape({
+                      map: state.map,
+                      layerName: 'form-track-point',
+                      layerZIndex: 10,
+                      list: state.formTrackPointList.map((v, i) => {
+                        return {
+                          easyMapParams: {
+                            id: `form-track-point-${i}`,
+                            position: that.$easyMap.formatPosition.cptTwpt(v.current.position),
+                            normalStyle: TrackStyle.trackPointStyle(SourceMap.get(v.current.source).color, v.current, v.next)
+                          }
+                        }
+                      })
+                    })
+                    state.formTrackPointStartCount = 0
+                    state.formTrackPointEndCount = 0
+                    state.formTrackPointList = []
+                  }
+                }, 10)
+                return s
+              }),
+            }
+          }
+        ]
+      });
+    }
+    const drawTrack = (trackSource: string) => {
+      state.isForm = false
+      state.form = {}
+      formParams.forEach((v: { value: string | number; init: any; }) => {
+        state.form[v.value] = v.init
+      })
+      startDraw((evt) => {
+        const geom = evt.feature.getGeometry()
+        const pMap = new Map()
+        state.trackPointList = geom.getCoordinates().map((v: any) => {
+          const obj = {
+            source: trackSource,
+            position: v,
+            speed: 0
+          }
+          pMap.set(`${v[0]}-${v[1]}`, obj)
+          return obj
+        })
+        initDraw(pMap, trackSource)
+        state.isForm = true
+      })
+    }
+    const onPointFocus = (p1: any, p2: any, p3: any) => {
+      that.$easyMap.getShapeView(state.map, [p1?.position, p2.position, p3?.position].filter(v => v))
+      const radius = 25
+      const longRadius = radius * Math.SQRT2
+      that.$easyMap.initShape({
+        map: state.map,
+        layerName: "focus",
+        layerZIndex: 20,
+        list: [
+          {
+            easyMapParams: {
+              id: 'focus',
+              position: that.$easyMap.formatPosition.cptTwpt(p2.position),
+              normalStyle: [new style.Style({ //图层样式
+                image: new style.RegularShape({
+                  stroke: new style.Stroke({
+                    color: '#9F2EFF',
+                    width: 2,
+                    lineDash: [
+                      (longRadius * 3) / 10,
+                      (longRadius * 4) / 10,
+                      (longRadius * 3) / 10,
+                      0
+                    ]
+                  }),
+                  radius1: radius,
+                  rotation: Math.PI / (180 / 45),
+                  points: 4
+                })
+              })]
+            }
+          }
+        ]
+      });
+    }
+    const onTemp = () => {
+      let index = 0
+      if (state.trackList.some((v: { ID: any; }, i: number) => {
+        index = i
+        return v.ID === state.form.ID
+      })) {
+        state.trackList[index] = Object.assign(state.form, {show: true, lines: state.trackPointList.map(v => {
+            return {
+              lon: v.position[0],
+              lat: v.position[1],
+              speed: v.speed
+            }
+          })})
+      } else {
+        const obj = {
+          type: state.trackPointList[0].source,
+          lines: state.trackPointList.map(v => {
+            return {
+              lon: v.position[0],
+              lat: v.position[1],
+              speed: v.speed
+            }
+          })
+        }
+        Object.assign(obj, state.form)
+        const result = JSON.parse(JSON.stringify(obj))
+        state.trackList.push(Object.assign(result, {show: true, ID: state.idNum.toString()}))
+      }
+      state.idNum++
+      state.trackPointList = []
+      that.$easyMap.initShape({
+        map: state.map,
+        layerName: "form-track-point-line",
+        layerZIndex: 9,
+        list: []
+      });
+      that.$easyMap.initShape({
+        map: state.map,
+        layerName: "form-track-point",
+        layerZIndex: 10,
+        list: []
+      });
+      initTrack()
+      state.isForm = false
+    }
+    const trackShowListCom = computed(() => {
+      return state.trackList.filter((v: { show: any; }) => v.show)
+    })
+    const initTrack = () => {
+      that.$easyMap.initShape({
+        map: state.map,
+        layerName: "track-point-line",
+        layerZIndex: 7,
+        list: []
+      });
+      that.$easyMap.initShape({
+        map: state.map,
+        layerName: "track-point",
+        layerZIndex: 8,
+        list: []
+      });
+      that.$easyMap.initShape({
+        map: state.map,
+        layerName: "track-point-line",
+        layerZIndex: 7,
+        list: trackShowListCom.value.map((v: any) => {
+          const pMap = new Map()
+          v.lines.forEach((p: {
+              speed: any; type: any; lon: any; lat: any;
+          }) => {
+            const obj = {
+              source: v.type,
+              position: [p.lon, p.lat],
+              speed: p.speed
+            }
+            pMap.set(`${p.lon}-${p.lat}`, obj)
+          })
+          return {
+            easyMapParams: {
+              id: v.ID,
+              position: that.$easyMap.formatPosition.wptTwl(v.lines.map((c: { lon: any; lat: any; }) => that.$easyMap.formatPosition.cptTwpt([c.lon, c.lat]))),
+              normalStyle: (f: any, r: any) => TrackStyle.trackLineStyle(f, r, state.map, SourceMap.get(v.type)?.color, pMap, (s, p) => {
+                state.initTrackPointStartCount++
+                setTimeout(() => {
+                  state.initTrackPointEndCount++
+                  state.initTrackPointList.push(...p)
+                  if (state.initTrackPointStartCount === state.initTrackPointEndCount) {
+                    that.$easyMap.initShape({
+                      map: state.map,
+                      layerName: 'track-point',
+                      layerZIndex: 8,
+                      list: state.initTrackPointList.map((c, i) => {
+                        return {
+                          easyMapParams: {
+                            id: `init-track-point-${v.ID}-${i}`,
+                            position: that.$easyMap.formatPosition.cptTwpt(c.current.position),
+                            normalStyle: TrackStyle.trackPointStyle(SourceMap.get(c.current.source).color, c.current, c.next)
+                          }
+                        }
+                      })
+                    })
+                    state.initTrackPointStartCount = 0
+                    state.initTrackPointEndCount = 0
+                    state.initTrackPointList = []
+                  }
+                }, 10)
+                return s
+              }),
+            }
+          }
+        })
+      });
+    }
+    const handleShow = (show: any, item: any) => {
+      item.show = show
+      initTrack()
+    }
+    const handleEdit = (item: any) => {
+      item.show = false
+      state.form = item
+      const pMap = new Map()
+      state.trackPointList = state.form.lines.map((v: { speed: any; lon: any; lat: any; }) => {
+        const obj = {
+          source: state.form.type,
+          position: [v.lon, v.lat],
+          speed: v.speed
+        }
+        pMap.set(`${v.lon}-${v.lat}`, obj)
+        return obj
+      })
+      initDraw(pMap, state.form.type)
+      state.isForm = true
+      initTrack()
+    }
+    const handleDelete = (item: any, index) => {
+      ElMessageBox.confirm(`是否删除:${item.ID}、${SourceMap.get(item.type).label}?`, "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning",
+      }).then(() => {
+        if (state.form.ID === item.ID) {
+          state.isForm = false
+          state.form = {}
+          formParams.forEach((v: { value: string | number; init: any; }) => {
+            state.form[v.value] = v.init
+          })
+          state.trackPointList = []
+          that.$easyMap.initShape({
+            map: state.map,
+            layerName: "form-track-point-line",
+            layerZIndex: 9,
+            list: []
+          });
+          that.$easyMap.initShape({
+            map: state.map,
+            layerName: "form-track-point",
+            layerZIndex: 10,
+            list: []
+          });
+        }
+        state.trackList.splice(index, 1)
+        initTrack()
+      }).catch(() => {})
+    }
+    const onSubmit = () => {
+      ElMessageBox.confirm(`是否保存?`, "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "success",
+      }).then(() => {
+        state.loading = true
+        const result = JSON.parse(JSON.stringify(state.trackList)).map(v => {
+          delete v.ID
+          delete v.show
+          return v
+        })
+        axios.post("/init-speed-track-api/hujie-track-server/mock", result, {
+          contentType: "application/json"
+        }).then(res => {
+          if (res.status === 200 && res.data) {
+            ElMessage.success('保存成功!')
+          } else {
+            ElMessage.error('保存失败!')
+          }
+          state.loading = false
+        })
+      }).catch(() => {})
+    }
+    return {
+      ...toRefs(state),
+      mapLoad,
+      drawTrack,
+      onPointFocus,
+      SourceMap,
+      onTemp,
+      handleShow,
+      formParams,
+      handleEdit,
+      handleDelete,
+      onSubmit,
+      options
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.init-speed-track {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  .map {
+    width: 100%;
+    height: 100vh;
+  }
+  .track {
+    position: absolute;
+    z-index: 20;
+    top: 0;
+    left: 0;
+    .track-line {
+      overflow-y: auto;
+      max-height: 180px;
+      .line {
+        height: 22px;
+        display: flex;
+        align-items: center;
+        .label {
+          width: 160px;
+          white-space: nowrap;
+        }
+        img {
+          width: 16px;
+          height: 16px;
+          cursor: pointer;
+          margin-left: 6px;
+        }
+      }
+    }
+    .track-point {
+      overflow-y: auto;
+      max-height: 600px;
+      .item {
+        display: flex;
+        align-items: center;
+        white-space: nowrap;
+      }
+      .point {
+        display: flex;
+        .position {
+          font-size: 12px;
+        }
+        border-bottom: 1px solid black;
+      }
+    }
+  }
+}
+</style>

File diff suppressed because it is too large
+ 18 - 0
src/views/init-speed-track/ship-track-invisible.svg


File diff suppressed because it is too large
+ 18 - 0
src/views/init-speed-track/ship-track-visible.svg


+ 162 - 0
src/views/init-speed-track/track-style.ts

@@ -0,0 +1,162 @@
+import * as ol from 'ol'
+import * as style from 'ol/style'
+import * as layer from 'ol/layer'
+import * as source from 'ol/source'
+import * as geom from 'ol/geom'
+import * as proj from 'ol/proj'
+import * as interaction from 'ol/interaction'
+import * as extent from "ol/extent";
+import * as format from "ol/format";
+import { Coordinate } from 'ol/coordinate'
+import * as turf from '@turf/turf'
+const trackLineStyle = (feature: any, resolution: any, map: any, color: any, pMap: { get: (arg0: string) => any }, callback: (arg0: style.Style[], arg1: any[]) => void) => {
+    const _style = []
+    _style.push(new style.Style({
+        stroke: new style.Stroke({
+            color: color,
+            width: 2
+        })
+    }))
+    const geometry = feature.getGeometry();
+    const length = geometry.getLength();//获取线段长度
+    const radio = (200 * resolution) / length;
+    const dradio = 1;//投影坐标系,如3857等,在EPSG:4326下可以设置dradio=10000
+    const radius = 10
+    const longRadius = radius * Math.SQRT2;
+    const judgeIs = (p1: any[], p2: any[], p3: any[]) => {
+        const E = 0.00000001
+        const k1 = (ps: any[], pe: number[]) => {
+            return (pe[1] - ps[1]) / (pe[0] - ps[0])
+        }
+        const k2 = (ps: any[], pe: number[]) => {
+            return (pe[0] - ps[0]) / (pe[1] - ps[0])
+        }
+        const a = (ps: any[], pe: any[]) => {
+            return Math.abs(k1(p1, ps) - k1(p1, pe)) <= E && Math.abs(k1(p1, ps) - k1(p1, pe)) >= -E
+        }
+        const d = (ps: any[], pe: any[]) => {
+            return Math.abs(k2(p1, ps) - k2(p1, pe)) <= E && Math.abs(k2(p1, ps) - k2(p1, pe)) >= -E
+        }
+        const s = (p: any[]) => {
+            return p3[0] === p[0] && p3[1] === p[1]
+        }
+        return s(p1) || s(p2) || a(p2, p3) || d(p2, p3)
+    }
+    for (let i = 0; i <= 1; i += radio) {
+        const arrowLocation = geometry.getCoordinateAt(i);
+        if (extent.containsCoordinate(map.getView().calculateExtent(map.getSize()), arrowLocation)) {
+            geometry.forEachSegment((start: any[], end: any[]) => {
+                if (!judgeIs(start, end, arrowLocation)) {
+                    return
+                }
+                let rotation = 0;
+                const dx = end[0] - start[0];
+                const dy = end[1] - start[1];
+                rotation = Math.atan2(dy, dx);
+                const pushStyle = (position: Coordinate) => {
+                    _style.push(new style.Style({
+                        geometry: new geom.Point(position),
+                        image: new style.RegularShape({
+                            stroke: new style.Stroke({
+                                color,
+                                width: 2,
+                                lineDash: [
+                                    longRadius - (4 * (radius / 10)),
+                                    longRadius + (5.5 * (radius / 10)),
+                                    longRadius,
+                                    0
+                                ]
+                            }),
+                            radius: radius / Math.SQRT2,
+                            rotation: -rotation,
+                            angle: Math.PI / (180 / 90),
+                            points: 4
+                        })
+                    }));
+                }
+                if (map.getView().getZoom() < map.getView().getMaxZoom()) {
+                    const dx1 = end[0] - arrowLocation[0];
+                    const dy1 = end[1] - arrowLocation[1];
+                    const dx2 = arrowLocation[0] - start[0];
+                    const dy2 = arrowLocation[1] - start[1];
+                    if (dx1 != dx2 && dy1 != dy2) {
+                        if (Math.abs(dradio * dx1 * dy2 - dradio * dx2 * dy1) < 0.001) {
+                            pushStyle(arrowLocation)
+                        }
+                    }
+                } else {
+                    if (Math.sqrt(Math.pow(start[0] - end[0], 2) + Math.pow(start[1] - end[1], 2)) > resolution * 100) {
+                        pushStyle([(start[0] + end[0]) / 2, (start[1] + end[1]) / 2])
+                    }
+                }
+            });
+        }
+    }
+    const pList = []
+    let lC = 0
+    // pList.push(pMap.get(geometry.getFirstCoordinate().join('-')))
+    if (map.getView().getZoom() < map.getView().getMaxZoom()) {
+        geometry.forEachSegment((start: number | Coordinate, end: number | any[]) => {
+            // @ts-ignore
+            const l = new geom.LineString([start, end])
+            lC += l.getLength()
+            if (extent.containsCoordinate(map.getView().calculateExtent(map.getSize()), <any>start) && lC > 200 * resolution) {
+                // @ts-ignore
+                const current = pMap.get(`${start[0]}-${start[1]}`)
+                // @ts-ignore
+                const next = pMap.get(`${end[0]}-${end[1]}`)
+                pList.push({current, next})
+                lC = 0
+            }
+        });
+    } else {
+        geometry.forEachSegment((start: number | Coordinate, end: number | any[]) => {
+            // @ts-ignore
+            const l = new geom.LineString([start, end])
+            if (extent.containsCoordinate(map.getView().calculateExtent(map.getSize()), <any>start)) {
+                // @ts-ignore
+                const current = pMap.get(`${start[0]}-${start[1]}`)
+                // @ts-ignore
+                const next = pMap.get(`${end[0]}-${end[1]}`)
+                pList.push({current, next})
+            }
+        });
+    }
+    pList.push({
+        current: pMap.get(geometry.getLastCoordinate().join('-')),
+        next: null
+    })
+    callback(_style, pList)
+    return _style
+}
+
+const trackPointStyle = (color: any, current: any, next: any) => {
+    const _style = []
+    let _text = String(current.speed) + '节'
+    if (next) {
+        const dis = turf.distance(turf.point(current.position), turf.point(next.position), {units: 'meters'})
+        _text += dis > 1000 ? `\n${(dis / 1000).toFixed(1)}千米` : `\n${dis.toFixed(0)}米`
+        _text += `\n${(dis / 0.51444444).toFixed(0)}秒`
+    }
+    _style.push(new style.Style({
+        image: new style.Circle({
+            radius: 10,
+            fill: new style.Fill({
+                color: color,
+            }),
+        }),
+        text: new style.Text({
+            text: _text,
+            font: "12px Microsoft YaHei", // 设置字体大小
+            fill: new style.Fill({
+                // 设置字体颜色
+                color: "#000",
+            }),
+        })
+    }),)
+    return _style
+}
+export default {
+    trackLineStyle,
+    trackPointStyle
+}

+ 47 - 0
vite.config.js

@@ -0,0 +1,47 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import {resolve} from "path";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+  base: '/',
+  resolve: {
+    alias: {
+      '@': resolve(__dirname, 'src'),
+    },
+  },
+  server: {
+    port: 1006,
+    // open: true,
+    https: false,
+    base: '/',
+    host: '0.0.0.0',
+    strictPort: false,
+    proxy: {
+      '/api': {
+        // target: 'http://localhost:8080/',
+        target: 'http://120.25.74.229:8000/',
+        // target: 'http://192.168.1.110:8080/',
+        changeOrigin: true,
+        rewrite: path => {
+          return path.replace(/^\/api/, '')
+        }
+      },
+      '/EzServer6-api': {
+        target: 'http://74.10.28.116:8090/',
+        changeOrigin: true,
+        rewrite: path => {
+          return path
+        }
+      },
+    }
+  },
+  build: {
+    outDir: "seat-tools",
+  },
+  publicDir: 'src/out',
+  optimizeDeps: {
+    include: []
+  }
+})

+ 640 - 0
yarn.lock

@@ -0,0 +1,640 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/parser@^7.20.15", "@babel/parser@^7.21.3":
+  version "7.22.4"
+  resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.22.4.tgz#a770e98fd785c231af9d93f6459d36770993fb32"
+  integrity sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA==
+
+"@ctrl/tinycolor@^3.4.1":
+  version "3.6.0"
+  resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz#53fa5fe9c34faee89469e48f91d51a3766108bc8"
+  integrity sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==
+
+"@element-plus/icons-vue@^2.0.6":
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.1.0.tgz#7ad90d08a8c0d5fd3af31c4f73264ca89614397a"
+  integrity sha512-PSBn3elNoanENc1vnCfh+3WA9fimRC7n+fWkf3rE5jvv+aBohNHABC/KAR5KWPecxWxDTVT1ERpRbOMRcOV/vA==
+
+"@esbuild/android-arm64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd"
+  integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==
+
+"@esbuild/android-arm@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d"
+  integrity sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==
+
+"@esbuild/android-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1"
+  integrity sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==
+
+"@esbuild/darwin-arm64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276"
+  integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==
+
+"@esbuild/darwin-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb"
+  integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==
+
+"@esbuild/freebsd-arm64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2"
+  integrity sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==
+
+"@esbuild/freebsd-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4"
+  integrity sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==
+
+"@esbuild/linux-arm64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb"
+  integrity sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==
+
+"@esbuild/linux-arm@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a"
+  integrity sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==
+
+"@esbuild/linux-ia32@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a"
+  integrity sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==
+
+"@esbuild/linux-loong64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72"
+  integrity sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==
+
+"@esbuild/linux-mips64el@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289"
+  integrity sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==
+
+"@esbuild/linux-ppc64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7"
+  integrity sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==
+
+"@esbuild/linux-riscv64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09"
+  integrity sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==
+
+"@esbuild/linux-s390x@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829"
+  integrity sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==
+
+"@esbuild/linux-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4"
+  integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==
+
+"@esbuild/netbsd-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462"
+  integrity sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==
+
+"@esbuild/openbsd-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691"
+  integrity sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==
+
+"@esbuild/sunos-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273"
+  integrity sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==
+
+"@esbuild/win32-arm64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f"
+  integrity sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==
+
+"@esbuild/win32-ia32@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03"
+  integrity sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==
+
+"@esbuild/win32-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061"
+  integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==
+
+"@floating-ui/core@^1.2.6":
+  version "1.2.6"
+  resolved "https://registry.npmmirror.com/@floating-ui/core/-/core-1.2.6.tgz#d21ace437cc919cdd8f1640302fa8851e65e75c0"
+  integrity sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==
+
+"@floating-ui/dom@^1.0.1":
+  version "1.2.9"
+  resolved "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.2.9.tgz#b9ed1c15d30963419a6736f1b7feb350dd49c603"
+  integrity sha512-sosQxsqgxMNkV3C+3UqTS6LxP7isRLwX8WMepp843Rb3/b0Wz8+MdUkxJksByip3C2WwLugLHN1b4ibn//zKwQ==
+  dependencies:
+    "@floating-ui/core" "^1.2.6"
+
+"@jridgewell/sourcemap-codec@^1.4.13":
+  version "1.4.15"
+  resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+  integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
+"@popperjs/core@npm:@sxzz/popperjs-es@^2.11.7":
+  version "2.11.7"
+  resolved "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz#a7f69e3665d3da9b115f9e71671dae1b97e13671"
+  integrity sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==
+
+"@types/lodash-es@^4.17.6":
+  version "4.17.7"
+  resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.7.tgz#22edcae9f44aff08546e71db8925f05b33c7cc40"
+  integrity sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ==
+  dependencies:
+    "@types/lodash" "*"
+
+"@types/lodash@*", "@types/lodash@^4.14.182":
+  version "4.14.195"
+  resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632"
+  integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==
+
+"@types/web-bluetooth@^0.0.16":
+  version "0.0.16"
+  resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz#1d12873a8e49567371f2a75fe3e7f7edca6662d8"
+  integrity sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==
+
+"@vitejs/plugin-vue@^4.1.0":
+  version "4.2.3"
+  resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz#ee0b6dfcc62fe65364e6395bf38fa2ba10bb44b6"
+  integrity sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==
+
+"@vue/compiler-core@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.4.tgz#7fbf591c1c19e1acd28ffd284526e98b4f581128"
+  integrity sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==
+  dependencies:
+    "@babel/parser" "^7.21.3"
+    "@vue/shared" "3.3.4"
+    estree-walker "^2.0.2"
+    source-map-js "^1.0.2"
+
+"@vue/compiler-dom@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz#f56e09b5f4d7dc350f981784de9713d823341151"
+  integrity sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==
+  dependencies:
+    "@vue/compiler-core" "3.3.4"
+    "@vue/shared" "3.3.4"
+
+"@vue/compiler-sfc@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz#b19d942c71938893535b46226d602720593001df"
+  integrity sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==
+  dependencies:
+    "@babel/parser" "^7.20.15"
+    "@vue/compiler-core" "3.3.4"
+    "@vue/compiler-dom" "3.3.4"
+    "@vue/compiler-ssr" "3.3.4"
+    "@vue/reactivity-transform" "3.3.4"
+    "@vue/shared" "3.3.4"
+    estree-walker "^2.0.2"
+    magic-string "^0.30.0"
+    postcss "^8.1.10"
+    source-map-js "^1.0.2"
+
+"@vue/compiler-ssr@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz#9d1379abffa4f2b0cd844174ceec4a9721138777"
+  integrity sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==
+  dependencies:
+    "@vue/compiler-dom" "3.3.4"
+    "@vue/shared" "3.3.4"
+
+"@vue/devtools-api@^6.0.0-beta.11", "@vue/devtools-api@^6.5.0":
+  version "6.5.0"
+  resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07"
+  integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==
+
+"@vue/reactivity-transform@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz#52908476e34d6a65c6c21cd2722d41ed8ae51929"
+  integrity sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==
+  dependencies:
+    "@babel/parser" "^7.20.15"
+    "@vue/compiler-core" "3.3.4"
+    "@vue/shared" "3.3.4"
+    estree-walker "^2.0.2"
+    magic-string "^0.30.0"
+
+"@vue/reactivity@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.4.tgz#a27a29c6cd17faba5a0e99fbb86ee951653e2253"
+  integrity sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==
+  dependencies:
+    "@vue/shared" "3.3.4"
+
+"@vue/runtime-core@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.4.tgz#4bb33872bbb583721b340f3088888394195967d1"
+  integrity sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==
+  dependencies:
+    "@vue/reactivity" "3.3.4"
+    "@vue/shared" "3.3.4"
+
+"@vue/runtime-dom@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz#992f2579d0ed6ce961f47bbe9bfe4b6791251566"
+  integrity sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==
+  dependencies:
+    "@vue/runtime-core" "3.3.4"
+    "@vue/shared" "3.3.4"
+    csstype "^3.1.1"
+
+"@vue/server-renderer@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.4.tgz#ea46594b795d1536f29bc592dd0f6655f7ea4c4c"
+  integrity sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==
+  dependencies:
+    "@vue/compiler-ssr" "3.3.4"
+    "@vue/shared" "3.3.4"
+
+"@vue/shared@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.4.tgz#06e83c5027f464eef861c329be81454bc8b70780"
+  integrity sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==
+
+"@vueuse/core@^9.1.0":
+  version "9.13.0"
+  resolved "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz#2f69e66d1905c1e4eebc249a01759cf88ea00cf4"
+  integrity sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==
+  dependencies:
+    "@types/web-bluetooth" "^0.0.16"
+    "@vueuse/metadata" "9.13.0"
+    "@vueuse/shared" "9.13.0"
+    vue-demi "*"
+
+"@vueuse/metadata@9.13.0":
+  version "9.13.0"
+  resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz#bc25a6cdad1b1a93c36ce30191124da6520539ff"
+  integrity sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==
+
+"@vueuse/shared@9.13.0":
+  version "9.13.0"
+  resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz#089ff4cc4e2e7a4015e57a8f32e4b39d096353b9"
+  integrity sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==
+  dependencies:
+    vue-demi "*"
+
+anymatch@~3.1.2:
+  version "3.1.3"
+  resolved "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
+  integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
+async-validator@^4.2.5:
+  version "4.2.5"
+  resolved "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz#c96ea3332a521699d0afaaceed510a54656c6339"
+  integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==
+
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
+axios@^1.3.4:
+  version "1.4.0"
+  resolved "https://registry.npmmirror.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
+  integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
+  dependencies:
+    follow-redirects "^1.15.0"
+    form-data "^4.0.0"
+    proxy-from-env "^1.1.0"
+
+binary-extensions@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+"chokidar@>=3.0.0 <4.0.0":
+  version "3.5.3"
+  resolved "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+  dependencies:
+    anymatch "~3.1.2"
+    braces "~3.0.2"
+    glob-parent "~5.1.2"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.6.0"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+combined-stream@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
+csstype@^3.1.1:
+  version "3.1.2"
+  resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
+  integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
+
+dayjs@^1.11.3:
+  version "1.11.7"
+  resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2"
+  integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
+
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
+element-plus@^2.3.1:
+  version "2.3.6"
+  resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.3.6.tgz#848f8834ed70adfbae8f4dec5303a9126d472d28"
+  integrity sha512-GLz0pXUYI2zRfIgyI6W7SWmHk6dSEikP9yR++hsQUyy63+WjutoiGpA3SZD4cGPSXUzRFeKfVr8CnYhK5LqXZw==
+  dependencies:
+    "@ctrl/tinycolor" "^3.4.1"
+    "@element-plus/icons-vue" "^2.0.6"
+    "@floating-ui/dom" "^1.0.1"
+    "@popperjs/core" "npm:@sxzz/popperjs-es@^2.11.7"
+    "@types/lodash" "^4.14.182"
+    "@types/lodash-es" "^4.17.6"
+    "@vueuse/core" "^9.1.0"
+    async-validator "^4.2.5"
+    dayjs "^1.11.3"
+    escape-html "^1.0.3"
+    lodash "^4.17.21"
+    lodash-es "^4.17.21"
+    lodash-unified "^1.0.2"
+    memoize-one "^6.0.0"
+    normalize-wheel-es "^1.2.0"
+
+esbuild@^0.17.5:
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.17.19.tgz#087a727e98299f0462a3d0bcdd9cd7ff100bd955"
+  integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==
+  optionalDependencies:
+    "@esbuild/android-arm" "0.17.19"
+    "@esbuild/android-arm64" "0.17.19"
+    "@esbuild/android-x64" "0.17.19"
+    "@esbuild/darwin-arm64" "0.17.19"
+    "@esbuild/darwin-x64" "0.17.19"
+    "@esbuild/freebsd-arm64" "0.17.19"
+    "@esbuild/freebsd-x64" "0.17.19"
+    "@esbuild/linux-arm" "0.17.19"
+    "@esbuild/linux-arm64" "0.17.19"
+    "@esbuild/linux-ia32" "0.17.19"
+    "@esbuild/linux-loong64" "0.17.19"
+    "@esbuild/linux-mips64el" "0.17.19"
+    "@esbuild/linux-ppc64" "0.17.19"
+    "@esbuild/linux-riscv64" "0.17.19"
+    "@esbuild/linux-s390x" "0.17.19"
+    "@esbuild/linux-x64" "0.17.19"
+    "@esbuild/netbsd-x64" "0.17.19"
+    "@esbuild/openbsd-x64" "0.17.19"
+    "@esbuild/sunos-x64" "0.17.19"
+    "@esbuild/win32-arm64" "0.17.19"
+    "@esbuild/win32-ia32" "0.17.19"
+    "@esbuild/win32-x64" "0.17.19"
+
+escape-html@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+  integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
+
+estree-walker@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
+  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+follow-redirects@^1.15.0:
+  version "1.15.2"
+  resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
+  integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
+
+form-data@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
+fsevents@~2.3.2:
+  version "2.3.2"
+  resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+glob-parent@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
+immutable@^4.0.0:
+  version "4.3.0"
+  resolved "https://registry.npmmirror.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be"
+  integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==
+
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+  version "4.0.3"
+  resolved "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+lodash-es@^4.17.21:
+  version "4.17.21"
+  resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
+  integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
+lodash-unified@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz#80b1eac10ed2eb02ed189f08614a29c27d07c894"
+  integrity sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==
+
+lodash@^4.17.21:
+  version "4.17.21"
+  resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+magic-string@^0.30.0:
+  version "0.30.0"
+  resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.0.tgz#fd58a4748c5c4547338a424e90fa5dd17f4de529"
+  integrity sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==
+  dependencies:
+    "@jridgewell/sourcemap-codec" "^1.4.13"
+
+memoize-one@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
+  integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
+
+mime-db@1.52.0:
+  version "1.52.0"
+  resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+  integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+  version "2.1.35"
+  resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+  integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+  dependencies:
+    mime-db "1.52.0"
+
+nanoid@^3.3.6:
+  version "3.3.6"
+  resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
+  integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+normalize-wheel-es@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz#0fa2593d619f7245a541652619105ab076acf09e"
+  integrity sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==
+
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.0.4, picomatch@^2.2.1:
+  version "2.3.1"
+  resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+postcss@^8.1.10, postcss@^8.4.23:
+  version "8.4.24"
+  resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.24.tgz#f714dba9b2284be3cc07dbd2fc57ee4dc972d2df"
+  integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==
+  dependencies:
+    nanoid "^3.3.6"
+    picocolors "^1.0.0"
+    source-map-js "^1.0.2"
+
+proxy-from-env@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+  integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
+readdirp@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+  dependencies:
+    picomatch "^2.2.1"
+
+rollup@^3.21.0:
+  version "3.23.0"
+  resolved "https://registry.npmmirror.com/rollup/-/rollup-3.23.0.tgz#b8d6146dac4bf058ee817f92820988e9b358b564"
+  integrity sha512-h31UlwEi7FHihLe1zbk+3Q7z1k/84rb9BSwmBSr/XjOCEaBJ2YyedQDuM0t/kfOS0IxM+vk1/zI9XxYj9V+NJQ==
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+sass@^1.60.0:
+  version "1.62.1"
+  resolved "https://registry.npmmirror.com/sass/-/sass-1.62.1.tgz#caa8d6bf098935bc92fc73fa169fb3790cacd029"
+  integrity sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==
+  dependencies:
+    chokidar ">=3.0.0 <4.0.0"
+    immutable "^4.0.0"
+    source-map-js ">=0.6.2 <2.0.0"
+
+"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+  integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+vite@^4.3.9:
+  version "4.3.9"
+  resolved "https://registry.npmmirror.com/vite/-/vite-4.3.9.tgz#db896200c0b1aa13b37cdc35c9e99ee2fdd5f96d"
+  integrity sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==
+  dependencies:
+    esbuild "^0.17.5"
+    postcss "^8.4.23"
+    rollup "^3.21.0"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+vue-demi@*:
+  version "0.14.5"
+  resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.5.tgz#676d0463d1a1266d5ab5cba932e043d8f5f2fbd9"
+  integrity sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==
+
+vue-router@^4.1.6:
+  version "4.2.2"
+  resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.2.2.tgz#b0097b66d89ca81c0986be03da244c7b32a4fd81"
+  integrity sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==
+  dependencies:
+    "@vue/devtools-api" "^6.5.0"
+
+vue@^3.2.47:
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/vue/-/vue-3.3.4.tgz#8ed945d3873667df1d0fcf3b2463ada028f88bd6"
+  integrity sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==
+  dependencies:
+    "@vue/compiler-dom" "3.3.4"
+    "@vue/compiler-sfc" "3.3.4"
+    "@vue/runtime-dom" "3.3.4"
+    "@vue/server-renderer" "3.3.4"
+    "@vue/shared" "3.3.4"
+
+vuex@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.npmmirror.com/vuex/-/vuex-4.1.0.tgz#aa1b3ea5c7385812b074c86faeeec2217872e36c"
+  integrity sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==
+  dependencies:
+    "@vue/devtools-api" "^6.0.0-beta.11"