CzRger 1 éve%!(EXTRA string=óta)
szülő
commit
328e5dff1f

+ 33 - 0
src/api/modules/ship-test/hover.ts

@@ -0,0 +1,33 @@
+import { handle } from '../../index'
+
+const suffix = 'ax-node-api'
+
+export const shipTestHoverList = (params: any) => handle({
+  url: `/${suffix}/ship-test/hover/list`,
+  method: 'post',
+  params
+})
+export const shipTestHoverPage = (params: any) => handle({
+  url: `/${suffix}/ship-test/hover/page`,
+  method: 'post',
+  params
+})
+export const shipTestHoverInfo = (id: any) => handle({
+  url: `/${suffix}/ship-test/hover/info/${id}`,
+  method: 'get',
+})
+export const shipTestHoverAdd = (params: any) => handle({
+  url: `/${suffix}/ship-test/hover/add`,
+  method: 'post',
+  params
+})
+export const shipTestHoverEdit = (params: any) => handle({
+  url: `/${suffix}/ship-test/hover/edit`,
+  method: 'put',
+  params
+})
+export const shipTestHoverDel = (params: any) => handle({
+  url: `/${suffix}/ship-test/hover/del/`,
+  method: 'delete',
+  params
+})

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 18 - 0
src/assets/images/ship-test/ship-track-delete.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 18 - 0
src/assets/images/ship-test/ship-track-invisible.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 18 - 0
src/assets/images/ship-test/ship-track-speed.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 18 - 0
src/assets/images/ship-test/ship-track-visible.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 30 - 0
src/assets/images/ship-test/tools-location.svg


+ 1 - 0
src/components/easyMap/func/mark-draw.ts

@@ -10,6 +10,7 @@ import {unByKey} from "ol/Observable";
 import {isValue} from "@/utils/util";
 // @ts-ignore
 import PointIcon from "../images/gis-layout-tools_tool-bz_icon.png"
+// @ts-ignore
 import CloseIcon from "../images/close.png"
 import {v4} from "uuid";
 import store from "@/store";

+ 8 - 0
src/router/ship-test.ts

@@ -33,6 +33,14 @@ const aisTestRouter = [
           title: '轨迹信息'
         }
       },
+      {
+        name: '4e53c650-a86c-4fde-83b2-eb316dssd5e66a2222',
+        path: 'hover',
+        component: () => import('@/views/ship-test/manage/hover/index.vue'),
+        meta: {
+          title: '详情框'
+        }
+      },
     ]
   },
 ]

+ 349 - 126
src/views/ship-test/business/index.vue

@@ -1,19 +1,59 @@
 <template>
   <div class="business">
     <ShipFilterCom class="ship-filter" :map="map" :mapFunc="mapFunc" @getShipFilter="handleShipFilter"/>
-    <TrackListCom class="track-list" :map="map" :mapFunc="mapFunc"/>
+    <div class="track-list">
+      <div class="track-item track-item-head">
+        <div class="source">目标来源</div>
+        <div class="key">轨迹标识</div>
+        <div class="time">轨迹持续时间</div>
+        <div class="do">操作</div>
+      </div>
+      <div class="track-item-body">
+        <template v-for="([key, item], index) in trackMap">
+          <div class="track-item">
+            <div class="source" :style="`color: ${item.config?.name};`">{{item.config?.name}}</div>
+            <div class="key" :style="`color: ${item.color};background: #ffffff;`">
+              <CusEllipsis :value="key"/>
+            </div>
+            <div class="time">{{item.trackTime}}</div>
+            <div class="do">
+              <el-tooltip :enterable="false" placement="top" content="隐藏" v-if="item.show">
+                <img class="__hover" src="@/assets/images/ship-test/ship-track-visible.svg" @click="handleTrackHide(key)"/>
+              </el-tooltip>
+              <el-tooltip :enterable="false" placement="top" content="显示" v-else>
+                <img class="__hover" src="@/assets/images/ship-test/ship-track-invisible.svg" @click="handleTrackShow(key)"/>
+              </el-tooltip>
+              <el-tooltip :enterable="false" placement="top" content="定位">
+                <img class="__hover" src="@/assets/images/ship-test/tools-location.svg" @click="handleTrackLocation(key)"/>
+              </el-tooltip>
+              <el-tooltip :enterable="false" placement="top" content="调色盘">
+                <div class="color">
+                  <img class="__hover" src="@/assets/images/ship-test/ship-track-speed.svg"/>
+                  <el-color-picker v-model="item.color" @change="color => handleTrackColor(key, color)"/>
+                </div>
+              </el-tooltip>
+              <el-tooltip :enterable="false" placement="top" content="删除">
+                <img class="__hover" src="@/assets/images/ship-test/ship-track-delete.svg" @click="handleTrackDel(key)"/>
+              </el-tooltip>
+            </div>
+          </div>
+        </template>
+      </div>
+    </div>
     <div class="hover-info" ref="ref_shipHover">
       <div class="hover-info-head">
-        <span>{{ shipHoverInfo?.isHistory ? '历史' : '实时'}}</span>
+        <span>{{shipHoverInfo?.config?.name}}_{{ shipHoverInfo?.isHistory ? '历史' : '实时'}}</span>
       </div>
       <div class="hover-info-close __hover" @click="shipHover.setPosition(undefined)">
         <img src="@/components/easyMap/images/close.png" alt=""/>
       </div>
       <div class="hover-main" v-if="shipHoverInfo?.data">
-        <div class="hover-item">
-          <div class="hover-item-label">名称:</div>
-          <div class="hover-item-value">{{shipHoverInfo.data}}</div>
-        </div>
+        <template v-for="item in shipHoverInfoCpt">
+          <div class="hover-item">
+            <div class="hover-item-label">{{item.label}}:</div>
+            <div class="hover-item-value">{{item.value}}</div>
+          </div>
+        </template>
       </div>
     </div>
   </div>
@@ -36,7 +76,6 @@ import {useStore} from 'vuex'
 import {useRouter, useRoute} from 'vue-router'
 import {ElMessage, ElMessageBox} from "element-plus";
 import ShipFilterCom from "./ship-filter.vue";
-import TrackListCom from "./track-list.vue";
 import * as source from "ol/source";
 import * as layer from "ol/layer";
 import ShipStyle from "@/views/ship-test/business/shipStyle";
@@ -47,7 +86,6 @@ export default defineComponent({
   name: '',
   components: {
     ShipFilterCom,
-    TrackListCom
   },
   props: {
     map: <any>{},
@@ -60,7 +98,7 @@ export default defineComponent({
     const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
     const state = reactive({
       zoomWMS: true,
-      wsMap: new Map(),
+      currentWS: <any>null,
       wsLayer: <any>null,
       shipFilter: {
         cql: null,
@@ -68,7 +106,8 @@ export default defineComponent({
       },
       trackMap: new Map(),
       shipHover: <any>null,
-      shipHoverInfo: <any>{}
+      shipHoverInfo: <any>{},
+      currentTime: 1
     })
     const ref_shipHover = ref()
     const initMap = () => {
@@ -129,7 +168,7 @@ export default defineComponent({
     }
     const handleShipClick = (clickFeat) => {
       const color = `rgb(${that.$util.randomNum(0, 255)}, ${that.$util.randomNum(0, 255)}, ${that.$util.randomNum(0, 255)})`
-      const trackLayer: any = new layer.VectorImage({
+      const trackLayer: any = new layer.Vector({
         zIndex: 10
       })
       props.map.addLayer(trackLayer)
@@ -141,7 +180,11 @@ export default defineComponent({
         layer: trackLayer,
         historyData: [],
         realData: [],
-        sourceId: clickFeat.get('_sourceId')
+        sourceId: clickFeat.get('_sourceId'),
+        config: clickFeat.get('_config'),
+        currentTime: 0,
+        show: true,
+        trackTime: '?'
       })
       initWebSocketShip()
       const hWs = new WebSocket(clickFeat.get('_historyWsUrl'))
@@ -159,7 +202,9 @@ export default defineComponent({
           const json = JSON.parse(e.data)
           const s = 'json' + clickFeat.get('_historyWsDataFlag')
           const data = eval(s)
-          state.trackMap.get(trackId).historyData = data
+          const _historyData = [...state.trackMap.get(trackId).historyData, ...data]
+          state.trackMap.set(trackId, Object.assign(state.trackMap.get(trackId), {historyData: _historyData}))
+          draw()
         } catch (e) {
         }
       }
@@ -174,53 +219,103 @@ export default defineComponent({
           const json = JSON.parse(e.data)
           const s = 'json' + clickFeat.get('_wsDataFlag')
           const data = eval(s)
-          const features: any = []
           // 实时船舶
           const DATA = data?.[0]
+          const getKeyData = (key) => {
+            if (clickFeat.get(key)) {
+              return eval(clickFeat.get(key))
+            } else {
+              return null
+            }
+          }
           if (DATA) {
-            try {
-              const getKeyData = (key) => {
-                if (clickFeat.get(key)) {
-                  return eval(clickFeat.get(key))
-                } else {
-                  return null
-                }
-              }
-              const feat: any = new format.WKT().readFeature(getKeyData('trackWktKey'))
-              feat.set('featureType', 'ship')
-              feat.setId(getKeyData('trackPointKey'))
-              feat.set('_hover', {
-                data: DATA,
-                config: clickFeat.get('_config'),
-                isHistory: false
-              })
-              feat.setStyle(ShipStyle.ShipNormalStyle({
-                course: clickFeat.get('_course'),
-                speed: clickFeat.get('_speed'),
-                head: clickFeat.get('_head'),
-                color: color
-              }))
-              features.push(feat)
+            if (state.trackMap.get(trackId).realData.length === 0 || state.trackMap.get(trackId).realData[state.trackMap.get(trackId).realData.length - 1]._trackPointId != getKeyData('trackPointKey')) {
+              DATA._trackPointId = getKeyData('trackPointKey')
               state.trackMap.get(trackId).realData.push(DATA)
-            } catch (e) {
-              console.log(e)
+              if (state.currentTime != state.trackMap.get(trackId).currentTime) {
+                draw()
+              }
             }
           }
-          // 轨迹线
-          const lineArr: any = []
-          state.trackMap.get(trackId).historyData.forEach(DATA => {
-            DATA.isHistory = true
-            const getKeyData = (key) => {
-              if (clickFeat.get(key)) {
-                return eval(clickFeat.get(key))
-              } else {
-                return null
-              }
+        } catch (e) {
+        }
+      }
+      const draw = () => {
+        const timeArr: any = []
+        state.trackMap.get(trackId).currentTime = JSON.parse(JSON.stringify(state.currentTime))
+        const features: any = []
+        // 轨迹线
+        const lineArr: any = []
+        state.trackMap.get(trackId).historyData.forEach((DATA, i) => {
+          DATA.isHistory = true
+          const getKeyData = (key) => {
+            if (clickFeat.get(key)) {
+              return eval(clickFeat.get(key))
+            } else {
+              return null
             }
-            lineArr.push(getKeyData('historyTrackWktKey'))
-          })
-          state.trackMap.get(trackId).realData.forEach(DATA => {
-            DATA.isHistory = false
+          }
+          lineArr.push(getKeyData('historyTrackWktKey'))
+          if (i === 0) {
+            timeArr.push(getKeyData('historyTrackTimeKey'))
+          } else if (i === state.trackMap.get(trackId).historyData.length - 1) {
+            timeArr.push(getKeyData('historyTrackTimeKey'))
+          }
+        })
+        state.trackMap.get(trackId).realData.forEach((DATA, i) => {
+          DATA.isHistory = false
+          const getKeyData = (key) => {
+            if (clickFeat.get(key)) {
+              return eval(clickFeat.get(key))
+            } else {
+              return null
+            }
+          }
+          lineArr.push(getKeyData('trackWktKey'))
+          if (i === 0) {
+            timeArr.push(getKeyData('trackTimeKey'))
+          } else if (i === state.trackMap.get(trackId).historyData.length - 1) {
+            timeArr.push(getKeyData('trackTimeKey'))
+          }
+        })
+        if (lineArr.length > 0) {
+          const lineWkt = that.$easyMap.formatPosition.wptTwl(lineArr)
+          const feat: any = new format.WKT().readFeature(lineWkt)
+          feat.set('trackPointList', [...state.trackMap.get(trackId).historyData, ...state.trackMap.get(trackId).realData])
+          feat.set('featureType', 'shipTrack')
+          feat.setStyle((f, r) => ShipStyle.trackStyle(f, r, props.map, state.trackMap.get(trackId).color, (s, pointList) => {
+            const pointFeatures: any = []
+            pointList.forEach(DATA => {
+              try {
+                const getKeyData = (key) => {
+                  if (clickFeat.get(key)) {
+                    return eval(clickFeat.get(key))
+                  } else {
+                    return null
+                  }
+                }
+                const feat: any = new format.WKT().readFeature(DATA.isHistory ? getKeyData('historyTrackWktKey') : getKeyData('trackWktKey'))
+                feat.set('featureType', 'shipPoint')
+                feat.set('_hover', {
+                  data: DATA,
+                  config: clickFeat.get('_config'),
+                  isHistory: DATA.isHistory
+                })
+                feat.setStyle(ShipStyle.trackPointNormalStyle(state.trackMap.get(trackId).color))
+                pointFeatures.push(feat)
+              } catch (e) {
+                console.log(e)
+              }
+            })
+            trackLayer.getSource().addFeatures(pointFeatures)
+            return s
+          }))
+          features.push(feat)
+        }
+        // 船舶
+        if (state.trackMap.get(trackId).realData.length > 0) {
+          try {
+            const DATA = state.trackMap.get(trackId).realData[state.trackMap.get(trackId).realData.length - 1]
             const getKeyData = (key) => {
               if (clickFeat.get(key)) {
                 return eval(clickFeat.get(key))
@@ -228,52 +323,46 @@ export default defineComponent({
                 return null
               }
             }
-            lineArr.push(getKeyData('trackWktKey'))
-          })
-          if (lineArr.length > 0) {
-            const lineWkt = that.$easyMap.formatPosition.wptTwl(lineArr)
-            const feat: any = new format.WKT().readFeature(lineWkt)
-            feat.set('trackPointList', [...state.trackMap.get(trackId).historyData, ...state.trackMap.get(trackId).realData])
-            feat.set('featureType', 'shipTrack')
-            feat.setStyle((f, r) => ShipStyle.trackStyle(f, r, props.map, color, (s, pointList) => {
-              const pointFeatures: any = []
-              pointList.forEach(DATA => {
-                try {
-                  const getKeyData = (key) => {
-                    if (clickFeat.get(key)) {
-                      return eval(clickFeat.get(key))
-                    } else {
-                      return null
-                    }
-                  }
-                  const feat: any = new format.WKT().readFeature(DATA.isHistory ? getKeyData('historyTrackWktKey') : getKeyData('trackWktKey'))
-                  feat.set('featureType', 'shipPoint')
-                  feat.set('_hover', {
-                    data: DATA,
-                    config: clickFeat.get('_config'),
-                    isHistory: DATA.isHistory
-                  })
-                  feat.setStyle(ShipStyle.trackPointNormalStyle(color))
-                  pointFeatures.push(feat)
-                } catch (e) {
-                  console.log(e)
-                }
-              })
-              trackLayer.getSource().addFeatures(pointFeatures)
-              return s
+            const feat: any = new format.WKT().readFeature(getKeyData('trackWktKey'))
+            feat.set('featureType', 'ship')
+            feat.setId(getKeyData('trackPointKey'))
+            feat.set('_hover', {
+              data: DATA,
+              config: clickFeat.get('_config'),
+              isHistory: false
+            })
+            feat.setStyle(ShipStyle.ShipNormalStyle({
+              course: clickFeat.get('_course'),
+              speed: clickFeat.get('_speed'),
+              head: clickFeat.get('_head'),
+              color: state.trackMap.get(trackId).color
             }))
             features.push(feat)
+          } catch (e) {
+            console.log(e)
           }
-          const vectorSource = new source.Vector({
-            features: features,
-            wrapX: false
-          });
-          trackLayer.setSource(vectorSource)
-        } catch (e) {
+        }
+        const vectorSource = new source.Vector({
+          features: features,
+          wrapX: false
+        });
+        trackLayer.setSource(vectorSource)
+        if (timeArr.length > 1) {
+          state.trackMap.set(trackId, Object.assign(state.trackMap.get(trackId), {trackTime: that.$util.comTimeByArea(timeArr[0], timeArr[timeArr.length - 1], true)}))
         }
       }
     }
     const handleShipFilter = ({cql, shipParams}) => {
+      state.currentWS?.close()
+      state.currentWS = null
+      const tileLayer = props.map.getLayers().getArray().filter(v => v.get('layerName') === 'WMSShip')?.[0]
+      if (tileLayer) {
+        props.map.removeLayer(tileLayer)
+      }
+      if (state.wsLayer) {
+        props.map.removeLayer(state.wsLayer)
+        state.wsLayer = null
+      }
       state.shipFilter.cql = cql
       state.shipFilter.config = shipParams
       console.log(state.shipFilter.config)
@@ -296,10 +385,8 @@ export default defineComponent({
       if (state.wsLayer) {
         state.wsLayer.getSource().clear()
       }
-      for (let key of state.wsMap.keys()) {
-        state.wsMap.get(key).close()
-        state.wsMap.delete(key)
-      }
+      state.currentWS?.close()
+      state.currentWS = null
       const hasLayer = props.map.getLayers().getArray().filter(v => v.get('layerName') === 'WMSShip')?.[0]
       if (hasLayer) {
         props.map.removeLayer(hasLayer)
@@ -327,9 +414,12 @@ export default defineComponent({
     }
     const initWebSocketShip = () => {
       const str = {
-        cql: <any>null
+        cql: `(BBOX(location, ${props.mapFunc.getBBOX().join(',')})`,
       }
-      str.cql = `(BBOX(location, ${props.mapFunc.getBBOX().join(',')}) and ${state.shipFilter.cql})`
+      if (state.shipFilter.cql) {
+        str.cql += ` and ${state.shipFilter.cql}`
+      }
+      str.cql += ')'
       let ids: any = []
       state.trackMap.forEach((value, key) => {
         if (value.sourceId === state.shipFilter.config.id) {
@@ -339,13 +429,13 @@ export default defineComponent({
       if (ids.length > 0) {
         str.cql += ` and (${state.shipFilter.config.track.trackKey.split('DATA.')[1]} not in (${ids.join(',')}))`
       }
-      if (state.wsMap.has(state.shipFilter.config.id)) {
-        if (state.wsMap.get(state.shipFilter.config.id).readyState === 1) {
-          state.wsMap.get(state.shipFilter.config.id).send(JSON.stringify(str))
+      if (state.currentWS) {
+        if (state.currentWS.readyState === 1) {
+          state.currentWS.send(JSON.stringify(str))
         }
       } else {
         const ws = new WebSocket(state.shipFilter.config.wsUrl)
-        state.wsMap.set(state.shipFilter.config.id, ws)
+        state.currentWS = ws
         ws.onopen = (e) => {
           ws.send(JSON.stringify(str))
         }
@@ -362,13 +452,14 @@ export default defineComponent({
     }
     const initShip = (data) => {
       if (!state.wsLayer) {
-        state.wsLayer = new layer.VectorImage({
+        state.wsLayer = new layer.Vector({
           zIndex: 10,
           style: (f) => {
             return ShipStyle.ShipNormalStyle({
               course: f.get('_course'),
               speed: f.get('_speed'),
-              head: f.get('_head')
+              head: f.get('_head'),
+              color: state.shipFilter.config.color
             })
           }
         })
@@ -423,13 +514,95 @@ export default defineComponent({
     watch(() => state.zoomWMS, () => {
       refreshMap()
     })
+    const shipHoverInfoCpt = computed(() => {
+      const arr: any = []
+      const DATA = state.shipHoverInfo.data
+      if (DATA.isHistory) {
+        state.shipHoverInfo.config.hover.forEach(h => {
+          if (h.historyValue) {
+            let value = eval(h.historyValue)
+            h.options?.forEach(v => {
+              if (String(value) == v.value) {
+                value = v.label
+              }
+            })
+            arr.push({
+              label: h.name,
+              value: value
+            })
+          }
+        })
+      } else {
+        state.shipHoverInfo.config.hover.forEach(h => {
+          let value = eval(h.value)
+          h.options?.forEach(v => {
+            if (String(value) == v.value) {
+              value = v.label
+            }
+          })
+          arr.push({
+            label: h.name,
+            value: value
+          })
+        })
+      }
+
+      return arr
+    })
+    const handleTrackDel = (id) => {
+      const l = state.trackMap.get(id)?.layer
+      const ws = state.trackMap.get(id)?.webSocket
+      if (l) {
+        props.map.removeLayer(l)
+      }
+      ws.close()
+      state.trackMap.delete(id)
+      initWebSocketShip()
+    }
+    const handleTrackShow = (id) => {
+      const l = state.trackMap.get(id)?.layer
+      l?.setVisible(true)
+      state.trackMap.set(id, Object.assign(state.trackMap.get(id), {show: true}))
+    }
+    const handleTrackHide = (id) => {
+      const l = state.trackMap.get(id)?.layer
+      l?.setVisible(false)
+      state.trackMap.set(id, Object.assign(state.trackMap.get(id), {show: false}))
+    }
+    const handleTrackColor = (id, color) => {
+      state.trackMap.set(id, Object.assign(state.trackMap.get(id), {color: color}))
+    }
+    const handleTrackLocation = (id) => {
+      const config = state.trackMap.get(id).config
+      const arr: any = []
+      state.trackMap.get(id).realData.forEach(DATA => {
+        try {
+          const feat: any = new format.WKT().readFeature(eval(config.track['trackWktKey']))
+          arr.push(feat.getGeometry().getCoordinates())
+        } catch (e) {
+          console.log(e)
+        }
+      })
+      state.trackMap.get(id).historyData.forEach(DATA => {
+        try {
+          const feat: any = new format.WKT().readFeature(eval(config.track['historyTrackWktKey']))
+          arr.push(feat.getGeometry().getCoordinates())
+        } catch (e) {
+          console.log(e)
+        }
+      })
+      if (arr.length > 0) {
+        that.$easyMap.getShapeView(props.map, arr)
+      } else {
+        ElMessage.warning('没有轨迹点可定位')
+      }
+    }
     onMounted(() => {
       initMap()
+      setInterval(() => state.currentTime++, 5000)
     })
     onUnmounted(() => {
-      for (let key of state.wsMap.keys()) {
-        state.wsMap.get(key).close()
-      }
+      state.currentWS?.close()
       for (let key of state.trackMap.keys()) {
         state.trackMap.get(key).webSocket.close()
       }
@@ -437,7 +610,13 @@ export default defineComponent({
     return {
       ...toRefs(state),
       ref_shipHover,
-      handleShipFilter
+      handleShipFilter,
+      shipHoverInfoCpt,
+      handleTrackDel,
+      handleTrackShow,
+      handleTrackHide,
+      handleTrackColor,
+      handleTrackLocation
     }
   },
 })
@@ -454,6 +633,64 @@ export default defineComponent({
     position: fixed;
     top: 0;
     right: 0;
+    max-height: 600px;
+    background: linear-gradient(180deg, #3874C9 0%, #0043C4 100%);
+    padding: 10px 0;
+    color: #FFFFFF;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+    .track-item-head {
+      padding: 0 20px;
+      text-align: left;
+    }
+    .track-item-body {
+      padding: 0 20px;
+      flex: 1;
+      overflow-y: auto;
+      display: flex;
+      flex-direction: column;
+    }
+    .track-item {
+      display: flex;
+      font-size: 14px;
+      .key {
+        width: 200px;
+      }
+      .source {
+        width: 100px;
+      }
+      .time {
+        width: 100px;
+      }
+      .do {
+        display: flex;
+        >* {
+          width: 20px;
+          height: 20px;
+        }
+        .color {
+          position: relative;
+          width: 16px;
+          height: 16px;
+          display: flex;
+          >img {
+            width: 100%;
+            height: 100%;
+          }
+          :deep(.el-color-picker) {
+            width: 100%;
+            height: 100%;
+            position: absolute;
+            opacity: 0;
+            .el-color-picker__trigger {
+              width: 100%;
+              height: 100%;
+            }
+          }
+        }
+      }
+    }
   }
 }
 .hover-info {
@@ -473,7 +710,7 @@ export default defineComponent({
     border-right: $footH solid transparent;
   }
   .hover-info-head {
-    min-width: 68px;
+    padding: 0 4px;
     height: 18px;
     position: absolute;
     top: -18px;
@@ -485,22 +722,8 @@ export default defineComponent({
     display: flex;
     align-items: center;
     line-height: 8px;
-    &:before {
-      z-index: -1;
-      content: '';
-      position: absolute;
-      width: 100%;
-      height: 100%;
-      background: linear-gradient(180deg, #3874C9 0%, #0043C4 100%);
-      border-radius: 2px 2px 0 0;/* 设置圆角 */
-      transform: perspective(20px)rotateX(4deg);
-      /* 镜头距离元素表面的位置为8px,x轴为1.1倍y轴为1.3倍,绕x轴旋转5度 */
-      transform-origin: bottom left;
-      /* bottom left = left bottom = 0 100% 中心点偏移量*/
-    }
-    >span {
-      margin-left: 10px;
-    }
+    background: linear-gradient(180deg, #3874C9 0%, #0043C4 100%);
+    border-radius: 2px 2px 0 0;/* 设置圆角 */
     >img {
       margin: 0 4px 0 6px;
     }
@@ -517,7 +740,7 @@ export default defineComponent({
     .hover-item {
       display: flex;
       .hover-item-label {
-        width: 42px;
+        min-width: 42px;
         font-size: 14px;
         font-family: PingFang SC;
         font-weight: 600;

+ 1 - 1
src/views/ship-test/business/ship-filter.vue

@@ -21,7 +21,7 @@
               </div>
               <div class="filters">
                 <template v-for="item in shipSource">
-                  <div class="f-checkbox __hover" :class="{active: item.active}" v-html="item.name" @click="handleSource(item)"/>
+                  <div class="f-checkbox __hover" :style="`color: ${item.color};`" :class="{active: item.active}" v-html="item.name" @click="handleSource(item)"/>
                 </template>
               </div>
               <template v-for="p in shipSource.filter(v => v.active)?.[0]?.params">

+ 0 - 44
src/views/ship-test/business/ship-info.vue

@@ -1,44 +0,0 @@
-<template>
-  <div>
-    船档
-  </div>
-</template>
-
-<script lang="ts">
-import {
-  defineComponent,
-  computed,
-  onMounted,
-  ref,
-  reactive,
-  watch,
-  getCurrentInstance,
-  ComponentInternalInstance,
-  toRefs,
-  nextTick
-} from 'vue'
-import {useStore} from 'vuex'
-import {useRouter, useRoute} from 'vue-router'
-import {ElMessage, ElMessageBox} from "element-plus";
-
-export default defineComponent({
-  name: '',
-  components: {},
-  props: {},
-  setup(props, {emit}) {
-    const store = useStore();
-    const router = useRouter();
-    const route = useRoute();
-    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
-    const state = reactive({})
-    onMounted(() => {
-    })
-    return {
-      ...toRefs(state),
-    }
-  },
-})
-</script>
-
-<style scoped lang="scss">
-</style>

+ 3 - 3
src/views/ship-test/business/shipStyle.ts

@@ -73,7 +73,7 @@ const ShipNormalStyle = ({
 }) => {
   const _style: any = []
   const allRotation = Math.PI / (180 / course)
-  const lineRadius = speed
+  const lineRadius = speed * 5
   // 船速
   _style.push(speedStyle(lineRadius, allRotation))
   if (head) {
@@ -86,7 +86,7 @@ const ShipNormalStyle = ({
       color: color,
       src: AIS,
       rotation: allRotation,
-      scale: 0.6
+      scale: 1
     }),
   }))
   return _style
@@ -181,7 +181,7 @@ const trackStyle = (feature, resolution, map, color, callback) => {
   let trackPointListIndex = 0
   let lC = 0
   pList.push(trackPointList[0])
-  if (map.getView().getZoom() < map.getView().getMaxZoom() - 2) {
+  if (map.getView().getZoom() < map.getView().getMaxZoom() - 1) {
     geometry.forEachSegment((start, end) => {
       trackPointListIndex++
       const l = new geom.LineString([start,end])

+ 27 - 3
src/views/ship-test/business/track-list.vue

@@ -2,7 +2,9 @@
   <div class="track-list">
     <template v-if="isActive">
       <div class="track-list-block">
-        轨迹列表
+        <template v-for="item in trackList">
+          <div>{{item.id}}</div>
+        </template>
       </div>
     </template>
     <template v-else>
@@ -33,19 +35,41 @@ import {ElMessage, ElMessageBox} from "element-plus";
 export default defineComponent({
   name: '',
   components: {},
-  props: {},
+  props: {
+    trackMap: <any>{}
+  },
   setup(props, {emit}) {
     const store = useStore();
     const router = useRouter();
     const route = useRoute();
     const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
     const state = reactive({
-      isActive: false
+      isActive: false,
+      trackList: <any>[]
+    })
+    const trackListCpt = computed(() => {
+      const arr: any = []
+      console.log(props.trackMap)
+      props.trackMap.forEach((v, k) => {
+        console.log(v)
+        console.log(k)
+      })
+      return arr
     })
+    watch(() => props.trackMap, (n) => {
+      state.trackList = []
+      n.forEach((v, k) => {
+        state.trackList.push({
+          id: k,
+          value: v
+        })
+      })
+    }, {deep: true})
     onMounted(() => {
     })
     return {
       ...toRefs(state),
+      trackListCpt
     }
   },
 })

+ 221 - 0
src/views/ship-test/manage/hover/detail.vue

@@ -0,0 +1,221 @@
+<template>
+  <CusDialog
+      :title="transfer.method === 'add' ? '新增数据源' : '数据源详情'"
+      :show="show"
+      @onClose="$emit('update:show', false)"
+      @onSubmit="onSubmit"
+      height="auto"
+      :closeConfirm="!isViewCpt"
+      :loading="loading"
+      :showSubmit="!isViewCpt"
+  >
+    <div class="__normal-form">
+      <CusForm labelWidth="120px" ref="ref_form" :formView="isViewCpt">
+        <CusFormColumn
+            :span="24"
+            required
+            label="名称"
+            v-model:param="cusDetail.name"
+            :maxLength="20"/>
+        <CusFormColumn
+            :span="24"
+            required
+            type="textarea"
+            v-model:param="cusDetail.value"
+            :rows="2">
+          <template #label>
+            <el-tooltip content="数据属性取值,采用DATA.xxx格式">
+              <div>
+                取值<SvgIcon name="tips" color="#333333"/>
+              </div>
+            </el-tooltip>
+          </template>
+        </CusFormColumn>
+        <CusFormColumn
+            :span="24"
+            type="textarea"
+            v-model:param="cusDetail.historyValue"
+            :rows="2">
+          <template #label>
+            <el-tooltip content="数据属性取值,采用DATA.xxx格式">
+              <div>
+                取值(历史)<SvgIcon name="tips" color="#333333"/>
+              </div>
+            </el-tooltip>
+          </template>
+        </CusFormColumn>
+        <CusFormColumn
+            :span="12"
+            label="排序"
+            link="input-number"
+            :min="0"
+            :max="999"
+            v-model:param="cusDetail.sort"/>
+        <CusFormColumn
+            :span="24"
+            label="备注"
+            type="textarea"
+            v-model:param="cusDetail.remark"
+            :rows="4"/>
+        <CusTable
+            class="table-form"
+            v-loading="loading"
+            ref="ref_cusTable"
+            :tableData="cusDetail.options"
+            :tableHead="tableHeadCpt"
+            :total="cusDetail.options.length"
+            :full="false"
+            noPage
+            noFoot
+        >
+          <template #label-column-value="{ scope }">
+            <CusFormColumn
+                :span="24"
+                required
+                label=""
+                v-model:param="scope.row.label"/>
+          </template>
+          <template #value-column-value="{ scope }">
+            <CusFormColumn
+                :span="24"
+                required
+                label=""
+                v-model:param="scope.row.value"/>
+          </template>
+          <template #caozuo-column-value="{ scope }">
+            <el-link
+                href="javascript:;"
+                type="danger"
+                @click="cusDetail.options.splice(scope.$index, 1)"
+            >删除</el-link>
+          </template>
+        </CusTable>
+        <btn-add v-if="!isViewCpt" title="新增" style="margin: 0 auto 10px;" @click="cusDetail.options.push({})"/>
+      </CusForm>
+    </div>
+  </CusDialog>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  toRefs,
+  nextTick
+} from 'vue'
+import {useStore} from 'vuex'
+import {useRouter, useRoute} from 'vue-router'
+import {ElMessage, ElMessageBox } from 'element-plus';
+
+export default defineComponent({
+  name: '',
+  components: {},
+  props: {
+    show: {},
+    transfer: <any>{}
+  },
+  setup(props, {emit}) {
+    const store = useStore();
+    const router = useRouter();
+    const route = useRoute();
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    const state = reactive({
+      cusDetail: {},
+      loading: false
+    })
+    watch(() => props.show, (n) => {
+      if (n) {
+        state.cusDetail = {
+          sourceId: props.transfer.sourceId,
+          options: []
+        }
+        if (props.transfer.method !== 'add') {
+          state.loading = true
+          that.$api.shipTestHoverInfo(props.transfer.id).then(res => {
+            if (res.code === 200) {
+              state.cusDetail = res.data
+              state.loading = false
+            } else {
+              ElMessage.error(res.msg)
+            }
+          })
+        }
+        nextTick(() => {
+          ref_form.value.reset()
+        })
+      }
+    })
+    const ref_form = ref()
+    const isViewCpt = computed(() => {
+      return props.transfer.method === 'view'
+    })
+    const tableHeadCpt = computed(() => {
+      const arr: any =[
+        { value: "label", label: "字典名称", show: true, popover: true },
+        { value: "value", label: "字典值", show: true, popover: true },
+      ]
+      if (!isViewCpt.value) {
+        arr.push({ value: "caozuo", label: "操作", show: true, width: 60 })
+      }
+      return arr
+    })
+    const onSubmit = () => {
+      ref_form.value.submit().then(() => {
+        ElMessageBox.confirm("是否提交?", "提示", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning",
+        }).then(() => {
+          state.loading = true
+          const apiHandle = props.transfer.method === 'edit' ? that.$api.shipTestHoverEdit(state.cusDetail) : that.$api.shipTestHoverAdd(state.cusDetail)
+          apiHandle.then(res => {
+            if (res.code === 200) {
+              ElMessage.success(props.transfer.method === 'edit' ? '编辑成功' : '新增成功')
+              emit('update:show', false)
+              emit('refresh')
+            } else {
+              ElMessage.error(res.msg)
+            }
+            state.loading = false
+          }).catch(() => {
+            state.loading = false
+          })
+        }).catch(() => {})
+      }).catch((e) => {
+        ElMessage({
+          message: e[0].message,
+          grouping: true,
+          type: 'warning',
+        })
+      })
+    }
+    return {
+      ...toRefs(state),
+      onSubmit,
+      isViewCpt,
+      ref_form,
+      tableHeadCpt
+    }
+  },
+})
+</script>
+
+<style scoped lang="scss">
+:deep(.table-form) {
+  max-height: 300px;
+  height: auto;
+  margin-bottom: 10px;
+  .cus-table-main {
+    position: unset;
+    .el-form-item {
+      margin-bottom: 0 !important;
+    }
+  }
+}
+</style>

+ 317 - 0
src/views/ship-test/manage/hover/index.vue

@@ -0,0 +1,317 @@
+<template>
+  <div class="__normal-page">
+    <div class="__normal-tree">
+      <div class="__normal-tree_filter">
+        <el-input v-model="treeName" placeholder="请输入关键词进行搜索" clearable>
+          <template #suffix>
+            <img src="@/assets/images/cus/tree-search.png" alt=""/>
+          </template>
+        </el-input>
+      </div>
+      <div class="__normal-tree_content">
+        <el-tree
+          ref="ref_tree"
+          :data="treeData"
+          node-key="id"
+          :props="{
+              children: 'children',
+              label: 'name',
+            }"
+          @node-click="handleNodeClick"
+          :expand-on-click-node="false"
+          highlight-current
+          :filter-node-method="treeFilter"
+        />
+      </div>
+    </div>
+    <div class="__normal-content" v-if="currentSource">
+      <CusContent
+          v-model:tableHead="tableHead"
+          @handleReset="handleReset"
+          @handleSearch="onSearch"
+          v-model:full="isFull"
+      >
+        <template #fieldOut>
+          <CusForm labelWidth="100px" @handleEnter="onSearch">
+            <CusFormColumn
+                filterSpan="1"
+                label="名称"
+                v-model:param="queryForm.name"/>
+            <CusSearchButtons
+                @handleReset="handleReset"
+                @handleSearch="onSearch"
+            />
+          </CusForm>
+        </template>
+        <template #buttons>
+          <btn-add @click="onAdd"/>
+          <btn-delete @click="onDel()"/>
+        </template>
+        <template #table>
+          <CusTable
+              v-loading="loading"
+              ref="ref_cusTable"
+              :tableData="queryResult.tableData"
+              :tableHead="tableHead"
+              :total="queryResult.total"
+              :page="queryPage.pageNum"
+              :pageSize="queryPage.pageSize"
+              @handlePage="handlePage"
+              @handleSort="handleSort"
+              v-model:selected="selected"
+              v-model:full="isFull"
+          >
+            <template #name-column-value="{ scope }">
+              <el-link
+                  class="__text-ellipsis"
+                  href="javascript:;"
+                  type="primary"
+                  @click="onView(scope.row)"
+              >{{scope.row.name}}</el-link>
+            </template>
+            <template #isDict-column-value="{ scope }">
+              <el-switch v-model="scope.row.isDict" disabled/>
+            </template>
+            <template #caozuo-column-value="{ scope }">
+              <el-link
+                  href="javascript:;"
+                  type="primary"
+                  @click="onEdit(scope.row)"
+              >编辑</el-link>
+              <el-link
+                  href="javascript:;"
+                  type="danger"
+                  @click="onDel(scope.row)"
+              >删除</el-link>
+            </template>
+          </CusTable>
+        </template>
+      </CusContent>
+    </div>
+    <DetailCom v-model:show="showDetail" :transfer="transfer" @refresh="refreshSearch"/>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  onMounted,
+  ref,
+  toRefs,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  computed,
+} from "vue";
+import { useStore } from "vuex";
+import { useRouter, useRoute } from "vue-router";
+import { ElMessage, ElMessageBox } from "element-plus";
+import DetailCom from './detail.vue'
+
+export default defineComponent({
+  name: "",
+  components: {
+    DetailCom,
+  },
+  setup(props, {emit}) {
+    const store = useStore();
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties;
+    const state = reactive({
+      //  加载中
+      loading: false,
+      //  查询分页参数
+      queryPage: {
+        pageNum: 1,
+        pageSize: 10
+      },
+      //  查询结果
+      queryResult: {
+        total: 0,
+        tableData: []
+      },
+      //  查询表单参数
+      queryForm: <any>{},
+      //  查询表单参数备份
+      back_queryForm: {},
+      //  查询表格排序
+      querySort: {},
+      //  表格可选的情况下,绑定的数组
+      selected: [],
+      //  表格表头
+      tableHead: [
+        {value: 'name', label: '名称', show:true,},
+        {value: 'value', label: '取值', show:true,},
+        {value: 'isDict', label: '字典项', show:true,},
+        {value: 'sort', label: '排序', show:true, sort: true},
+        {value: 'updateTime', label: '更新时间', show:true, sort: true},
+        {value: 'caozuo', label: '操作', show:true}
+      ],
+      isFull: false,
+      transfer: {},
+      showDetail: false,
+      treeData: [],
+      treeName: '',
+      currentSource: <any>null
+    });
+    const ref_cusTable = ref()
+    //  获取字典
+    const initDictionary = () => {
+    }
+    //  查询分页参数改变处理方法
+    const handlePage = ({page, pageSize}: any) => {
+      state.queryPage.pageNum = page
+      state.queryPage.pageSize = pageSize
+      handleSearch(page, pageSize)
+    }
+    //  查询排序参数改变处理方法
+    const handleSort = ({key, value}: any) => {
+      state.querySort = key ? {[key]: value} : {}
+      refreshSearch()
+    }
+    //  列表刷新方法
+    const refreshSearch = () => {
+      state.queryPage.pageNum = 1
+      handleSearch(state.queryPage.pageNum, state.queryPage.pageSize)
+    }
+    //  重置查询表单方法
+    const handleReset = () => {
+      ref_cusTable.value?.reset()
+      state.querySort = {}
+      state.queryForm = {}
+      state.back_queryForm = JSON.parse(JSON.stringify(state.queryForm))
+      refreshSearch()
+    }
+    //  查询方法
+    const handleSearch = (page = 1, pageSize = 10) => {
+      //  添加分页参数
+      const queryParams: any = {
+        pageNum: page,
+        pageSize: pageSize,
+        sourceId: state.currentSource.id
+      }
+      //  添加排序参数
+      for (const [k, v] of Object.entries(state.querySort)) {
+        if (that.$util.isValue(v)) {
+          queryParams['orderByColumn'] = k
+          queryParams['isAsc'] = v === "ascending"
+        }
+      }
+      //  添加表单参数
+      for (const [k, v] of Object.entries(state.back_queryForm)) {
+        if (that.$util.isValue(v)) {
+          queryParams[k] = v
+        }
+      }
+      state.loading = true
+      that.$api.shipTestHoverPage(queryParams).then((res) => {
+        if (res.code === 200) {
+          state.queryResult.tableData = res.data.data.map(v => {
+            v.isDict = v.options.length > 0
+            return v
+          })
+          state.queryResult.total = res.data.total
+        } else {
+          ElMessage.error(res.msg)
+        }
+        state.loading = false
+      }).catch(() => {
+        state.loading = false
+      })
+    }
+    //  点击查询按钮后
+    const onSearch = () => {
+      state.back_queryForm = JSON.parse(JSON.stringify(state.queryForm))
+      refreshSearch()
+    }
+    const onAdd = () => {
+      state.transfer = {
+        method: 'add',
+        sourceId: state.currentSource.id
+      }
+      state.showDetail = true
+    }
+    const onEdit = (row) => {
+      state.transfer = {
+        method: 'edit',
+        id: row.id
+      }
+      state.showDetail = true
+    }
+    const onView = (row) => {
+      state.transfer = {
+        method: 'view',
+        id: row.id
+      }
+      state.showDetail = true
+    }
+    const onDel = (row: any = null) => {
+      const arr = row ? [row] : state.selected
+      if (arr.length > 0) {
+        ElMessageBox.confirm(`是否删除${arr.length === 1 ? arr[0].name : `${arr.length}条记录`}?`, "提示", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning",
+        }).then(() => {
+          state.loading = true
+          that.$api.shipTestHoverDel(arr.map(v => v.id).join(',')).then(res => {
+            if (res.code === 200) {
+              ElMessage.success('删除成功')
+              refreshSearch()
+            } else {
+              ElMessage.error(res.msg)
+              state.loading = false
+            }
+          }).catch(() => {
+            state.loading = false
+          })
+        }).catch(() => {
+        })
+      } else {
+        ElMessage.warning('请至少选择一条记录!')
+      }
+    }
+    const ref_tree = ref()
+    const handleNodeClick = (data) => {
+      state.currentSource = data
+      handleReset()
+    }
+    const treeFilter = (value, data) => {
+      if (!value) return true
+      return data.name.includes(value)
+    }
+    watch(() => state.treeName, (n) => {
+      ref_tree.value.filter(n)
+    })
+    const initSource = () => {
+      that.$api.shipTestSourceList().then(res => {
+        state.treeData = res.data
+      })
+    }
+    onMounted(() => {
+      state.back_queryForm = JSON.parse(JSON.stringify(state.queryForm))
+      initDictionary()
+      initSource()
+    })
+    return {
+      ref_cusTable,
+      ...toRefs(state),
+      handleSearch,
+      refreshSearch,
+      handlePage,
+      handleSort,
+      handleReset,
+      onSearch,
+      onAdd,
+      onEdit,
+      onView,
+      onDel,
+      handleNodeClick,
+      treeFilter,
+      ref_tree,
+    }
+  },
+});
+</script>
+<style scoped lang="scss">
+</style>

+ 13 - 2
src/views/ship-test/manage/source/detail.vue

@@ -26,6 +26,17 @@
             :max="999"
             v-model:param="cusDetail.sort"/>
         <CusFormColumn
+            :span="12"
+            required
+            label="默认颜色"
+            v-model:param="cusDetail.color"
+            defaultErrorMsg="请选择默认颜色"
+        >
+          <template #cus="{handleValidate}">
+            <el-color-picker v-model="cusDetail.color" :disabled="isViewCpt"/>
+          </template>
+        </CusFormColumn>
+        <CusFormColumn
             :span="24"
             label="备注"
             type="textarea"
@@ -52,7 +63,7 @@
           :span="24"
           v-model:param="cusDetail.wsDataFlag">
           <template #label>
-            <el-tooltip content="实际返回数据的路径,空值的话直接取返回值,对象的话例如.data">
+            <el-tooltip content="实际返回数据的路径,空值的话直接取返回值,对象的话例如.xxx">
               <div>
                 数据标识<SvgIcon name="tips" color="#333333"/>
               </div>
@@ -69,7 +80,7 @@
             :span="24"
             v-model:param="cusDetail.historyWsDataFlag">
           <template #label>
-            <el-tooltip content="实际返回数据的路径,空值的话直接取返回值,对象的话例如.data">
+            <el-tooltip content="实际返回数据的路径,空值的话直接取返回值,对象的话例如.xxx">
               <div>
                 数据标识<SvgIcon name="tips" color="#333333"/>
               </div>

+ 2 - 2
src/views/ship-test/manage/track/index.vue

@@ -59,7 +59,7 @@
         <CusFormColumn
             :span="12"
             required
-            label="时间"
+            label="轨迹时间"
             v-model:param="form.trackTimeKey"/>
         <div class="__cus-title_2">历史轨迹唯一标识(DATA.xxx形式,可任意拼接或使用字符串模板`POINT(${DATA.xxx} ${DATA.xxx})`或DATA.xxx + DATA.xxx)</div>
         <CusFormColumn
@@ -94,7 +94,7 @@
         <CusFormColumn
             :span="12"
             required
-            label="时间"
+            label="轨迹时间"
             v-model:param="form.historyTrackTimeKey"/>
       </CusForm>
       <div class="buttons">