diff --git a/public/assets/tagTemplate.csv b/public/assets/tagTemplate.csv index eacaf688..a1e01355 100644 --- a/public/assets/tagTemplate.csv +++ b/public/assets/tagTemplate.csv @@ -4,3 +4,4 @@ IP,192.168.1.1-192.168.1.2 IP,103.3.138.0 Domain,$www.baidu.com Domain,*.email.baidu.com +Subscriber,41582263 diff --git a/src/assets/css/components/views/charts2/entityDetailSubscriberMap.scss b/src/assets/css/components/views/charts2/entityDetailSubscriberMap.scss index d9e79803..378d0615 100644 --- a/src/assets/css/components/views/charts2/entityDetailSubscriberMap.scss +++ b/src/assets/css/components/views/charts2/entityDetailSubscriberMap.scss @@ -426,7 +426,7 @@ $color-highlight: #CC4444; .hexagon-tooltip__body { .body__item .item__label { - width: 140px; + width: 80px; } } } diff --git a/src/assets/css/components/views/location/location.scss b/src/assets/css/components/views/location/location.scss index adbb3fe5..b2a83c3a 100644 --- a/src/assets/css/components/views/location/location.scss +++ b/src/assets/css/components/views/location/location.scss @@ -7,6 +7,7 @@ $color-primary: var(--el-color-primary); $color-human: #233447; // 以下是颜色暂无替代的 $color-circle: #DE3434; $color-base-station: #585B5F; +$color-cell: #586A7F; $color-search-follow: #6f6f6e; $color-highlight: #CC4444; @@ -36,7 +37,7 @@ $color-highlight: #CC4444; .location-tabs { .traceTracking-tabs_label { display: flex; - flex-display: row; + .traceTracking-num { margin-top: 5px; @@ -175,7 +176,7 @@ $color-highlight: #CC4444; width: 20px; height: 20px; border-radius: 50%; - cursor: default; + cursor: pointer; padding: 0; transition: height .1s linear, width .1s linear; @@ -186,7 +187,6 @@ $color-highlight: #CC4444; &.map-marker--human { background-color: $color-human; - cursor: pointer; svg { width: 14px; @@ -203,6 +203,16 @@ $color-highlight: #CC4444; } } + &.map-marker--cell { + border: 1px solid $color-white; + background-color: $color-cell; + + svg { + width: 12px; + height: 12px; + } + } + &.map-marker--hover { width: 30px; height: 30px; @@ -215,9 +225,11 @@ $color-highlight: #CC4444; height: 21px; } - &.map-marker--base-station svg { - width: 18px; - height: 18px; + &.map-marker--base-station, &.map-marker--cell { + svg { + width: 18px; + height: 18px; + } } } @@ -233,6 +245,14 @@ $color-highlight: #CC4444; width: 21px; height: 21px; } + &.map-marker--base-station { + background-color: $color-base-station; + + svg { + width: 18px; + height: 18px; + } + } } &.map-marker--hidden, &.map-marker--unfollowed { @@ -358,77 +378,24 @@ $color-highlight: #CC4444; align-items: center; font-size: 12px; } - } - /*.analysis-statistics__search { - margin-bottom: 10px; - padding-left: 10px; - .el-select { - .search-active { - float: left; - border-radius: 3px; - width: 6px; - height: 6px; - margin-right: 10px; - position: relative; - top: 50%; - transform: translateY(-50%); - text-align: center; + .input-to-select { + .el-input__wrapper { + display: none; } - .search-value { - font-size: 14px; - margin-right: 20px; - } + .el-input-group__append { + width: 100%; + padding: 0; - .search-id { - color: var(--el-color-info); - } - - .search-follow__icon { - position: absolute; - top: 50%; - right: 10px; - transform: translateY(-50%); - color: $color-search-follow; - margin-right: 10px; - text-align: center; - - i { - font-size: 12px; + .el-select { + .el-select__wrapper { + background-color: #FFFFFF; + box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset; + } } } - - .search-content { - display: flex; - flex-direction: column; - height: 100%; - justify-content: center; - } - - .active-icon { - background: $color-business; - } - - .inactive-icon { - background: $color-inactive; - } - - .search-select { - max-height: 250px; - } - - .search-select .el-scrollbar__wrap { - max-height: 250px; - overflow-y: auto; - } - - .el-select-dropdown .el-scrollbar .el-select-dropdown__wrap .el-scrollbar__view.el-select-dropdown__list .el-select-dropdown__item { - position: relative; - height: 48px; - line-height: 16px; - } } - }*/ + } .analysis-statistics__condition { display: flex; justify-content: space-between; @@ -549,12 +516,13 @@ $color-highlight: #CC4444; .analysis-statistics__title { position: relative; - margin-bottom: 10px; + margin-bottom: 8px; padding-left: 10px; font-size: 16px; color: $color-text-primary; display: flex; justify-content: space-between; + .el-checkbox__label { font-size: 14px; color: $color-text-primary !important; @@ -832,6 +800,12 @@ $color-highlight: #CC4444; margin-left: 2px; border-radius: 50%; background-color: $color-circle; + + &.circle-circle__highlight { + margin-left: 0; + width: 14px; + height: 14px; + } } .circle-line { @@ -879,7 +853,7 @@ $color-highlight: #CC4444; box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.5); border-radius: 2px; min-width: 185px; - z-index: 1; + z-index: 5; /*&.geo-analysis__hexagon-tooltip--hexagon { }*/ @@ -896,7 +870,18 @@ $color-highlight: #CC4444; .hexagon-tooltip__body { .body__item .item__label { - width: 140px; + width: 80px; + } + } + } + &.geo-analysis__hexagon-tooltip--cell { + .icon__box { + background-color: $color-cell; + } + + .hexagon-tooltip__body { + .body__item .item__label { + width: 100px; } } } diff --git a/src/assets/css/font/iconfont.css b/src/assets/css/font/iconfont.css index e3cee2be..02c32675 100644 --- a/src/assets/css/font/iconfont.css +++ b/src/assets/css/font/iconfont.css @@ -1,8 +1,8 @@ @font-face { font-family: "cn-icon"; /* Project id 2614877 */ - src: url('iconfont.woff2?t=1722997039116') format('woff2'), - url('iconfont.woff?t=1722997039116') format('woff'), - url('iconfont.ttf?t=1722997039116') format('truetype'); + src: url('iconfont.woff2?t=1730282688072') format('woff2'), + url('iconfont.woff?t=1730282688072') format('woff'), + url('iconfont.ttf?t=1730282688072') format('truetype'); } .cn-icon { @@ -13,6 +13,22 @@ -moz-osx-font-smoothing: grayscale; } +.cn-icon-breached-human-black:before { + content: "\e824"; +} + +.cn-icon-breached-human-white:before { + content: "\e822"; +} + +.cn-icon-base-station2:before { + content: "\e820"; +} + +.cn-icon-signal:before { + content: "\e823"; +} + .cn-icon-fuhe:before { content: "\e815"; } diff --git a/src/assets/css/font/iconfont.js b/src/assets/css/font/iconfont.js index 81ad7444..4c5b8177 100644 --- a/src/assets/css/font/iconfont.js +++ b/src/assets/css/font/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_2614877='',function(l){var a=(a=document.getElementsByTagName("script"))[a.length-1],c=a.getAttribute("data-injectcss"),a=a.getAttribute("data-disable-injectsvg");if(!a){var h,o,m,i,v,z=function(a,c){c.parentNode.insertBefore(a,c)};if(c&&!l.__iconfont__svg__cssinject__){l.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}h=function(){var a,c=document.createElement("div");c.innerHTML=l._iconfont_svg_string_2614877,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(a=document.body).firstChild?z(c,a.firstChild):a.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(h,0):(o=function(){document.removeEventListener("DOMContentLoaded",o,!1),h()},document.addEventListener("DOMContentLoaded",o,!1)):document.attachEvent&&(m=h,i=l.document,v=!1,s(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,t())})}function t(){v||(v=!0,m())}function s(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(s,50)}t()}}(window); +window._iconfont_svg_string_2614877='',(l=>{var a=(c=(c=document.getElementsByTagName("script"))[c.length-1]).getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var h,o,i,m,z,v=function(a,c){c.parentNode.insertBefore(a,c)};if(a&&!l.__iconfont__svg__cssinject__){l.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}h=function(){var a,c=document.createElement("div");c.innerHTML=l._iconfont_svg_string_2614877,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(a=document.body).firstChild?v(c,a.firstChild):a.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(h,0):(o=function(){document.removeEventListener("DOMContentLoaded",o,!1),h()},document.addEventListener("DOMContentLoaded",o,!1)):document.attachEvent&&(i=h,m=l.document,z=!1,s(),m.onreadystatechange=function(){"complete"==m.readyState&&(m.onreadystatechange=null,t())})}function t(){z||(z=!0,i())}function s(){try{m.documentElement.doScroll("left")}catch(a){return void setTimeout(s,50)}t()}})(window); \ No newline at end of file diff --git a/src/assets/css/font/iconfont.ttf b/src/assets/css/font/iconfont.ttf index f9b8ddb4..2f118a06 100644 Binary files a/src/assets/css/font/iconfont.ttf and b/src/assets/css/font/iconfont.ttf differ diff --git a/src/assets/css/font/iconfont.woff b/src/assets/css/font/iconfont.woff index f229b40c..d6733430 100644 Binary files a/src/assets/css/font/iconfont.woff and b/src/assets/css/font/iconfont.woff differ diff --git a/src/assets/css/font/iconfont.woff2 b/src/assets/css/font/iconfont.woff2 index e6a4f9a2..c3db6e86 100644 Binary files a/src/assets/css/font/iconfont.woff2 and b/src/assets/css/font/iconfont.woff2 differ diff --git a/src/utils/api.js b/src/utils/api.js index ba8ae2c4..874384be 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -379,7 +379,7 @@ export const api = { density: apiVersion + '/locationIntelligence/population/density', trend: apiVersion + '/locationIntelligence/active/trend', count: apiVersion + '/locationIntelligence/active/count', - baseStation: apiVersion + '/locationIntelligence/baseStation', + baseStation: apiVersion + '/cell/list', list: apiVersion + '/locationIntelligence/list', totalCount: apiVersion + '/locationIntelligence/list/count', followedSubscriber: apiVersion + '/locationIntelligence/followed/subscribers', diff --git a/src/views/location/Index.vue b/src/views/location/Index.vue index 0b30b80e..c1a30c54 100644 --- a/src/views/location/Index.vue +++ b/src/views/location/Index.vue @@ -106,20 +106,44 @@ >
- {{$t('location.subscribers')}}  {{subscribersTotalCount}} +
{{$t('location.subscribers')}}: {{subscribersTotalCount}}
@@ -140,7 +164,7 @@ - +
@@ -155,7 +179,8 @@
- + +
@@ -210,14 +235,6 @@
APN
{{item.apn || '-'}}
-
{{$t('location.location')}}
{{locationHandler(item)}}
@@ -232,6 +249,30 @@
{{$t('location.noTrackingYet')}}
+
+ + + + +
+
@@ -516,11 +592,14 @@ import MyFollowBox from '@/components/rightBox/location/MyFollowBox' import dataListMixin from '@/mixins/data-list' import * as turf from '@turf/turf' import { generateRectanglePolygon } from '@/utils/geo-utils' +import i18n from '@/i18n' // import testData from './test' const humanSvg = '' -const baseStationSvg = '' +const humanLeakedSvg = '' +const baseStationSvg = '' +const cellSvg = '' export default { name: 'Location', @@ -533,7 +612,8 @@ export default { tooltipType: { hexagon: 'hexagon', baseStation: 'base-station', - human: 'human' + human: 'human', + cell: 'cell' }, activeCount: '', activeCountChain: '', @@ -788,9 +868,34 @@ export default { async queryBaseStation () { this.loading.baseStationLoading = true try { - // const response = await axios.get(api.location.baseStation) - const response = [{ cid: '414-06-15261-3', latitude: 17.182549, longitude: 95.972018, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11755-1', latitude: 17.182845, longitude: 95.969483, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-12955-2', latitude: 17.181968, longitude: 95.96975, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-13698-6', latitude: 17.181413, longitude: 95.969861, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11553-2', latitude: 17.183569, longitude: 95.971931, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-15971-1', latitude: 17.183807, longitude: 95.972561, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-13287-2', latitude: 17.184677124023, longitude: 95.97038269043, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11755-3', latitude: 17.18879699707, longitude: 95.97038269043, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-13651-1', latitude: 17.180557250977, longitude: 95.977249145508, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11421-2', latitude: 17.179871, longitude: 95.979309, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-12504-1', latitude: 17.173573, longitude: 95.978783, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-13651-3', latitude: 17.173690795898, longitude: 95.989608764648, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-13518-1', latitude: 17.170944213867, longitude: 95.990982055664, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11646-1', latitude: 17.169570922852, longitude: 95.99235534668, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11643-2', latitude: 17.165451049805, longitude: 95.995101928711, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11709-1', latitude: 17.143478393555, longitude: 95.999221801758, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11553-3', latitude: 17.135239, longitude: 96.001511, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-12955-3', latitude: 17.126575, longitude: 96.00721, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-13287-1', latitude: 17.120132446289, longitude: 96.010208129883, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-12503-1', latitude: 17.120132446289, longitude: 96.007461547852, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11588-1', latitude: 17.120132446289, longitude: 96.008834838867, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11835-1', latitude: 17.118759155273, longitude: 96.008834838867, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11452-2', latitude: 17.117385864258, longitude: 96.010208129883, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-12962-2', latitude: 17.114639282227, longitude: 96.012954711914, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11769-4', latitude: 17.11051940918, longitude: 96.015701293945, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11758-3', latitude: 17.109146118164, longitude: 96.015701293945, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11253-2', latitude: 17.111206, longitude: 96.019822, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-12823-2', latitude: 17.106399536133, longitude: 96.019821166992, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-15277-4', latitude: 17.105026245117, longitude: 96.019821166992, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11704-2', latitude: 17.103652954102, longitude: 96.021194458008, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-13908-1', latitude: 17.103652954102, longitude: 96.022567749023, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11385-1', latitude: 17.103652954102, longitude: 96.023941040039, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-12496-1', latitude: 17.102279663086, longitude: 96.025314331055, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-15984-3', latitude: 17.102279663086, longitude: 96.02668762207, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11904-1', latitude: 17.10090637207, longitude: 96.029434204102, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-15984-2', latitude: 17.102279663086, longitude: 96.033554077148, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11385-2', latitude: 17.099533081055, longitude: 96.033554077148, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11714-1', latitude: 17.10090637207, longitude: 96.039047241211, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-12462-2', latitude: 17.10090637207, longitude: 96.040420532227, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-13908-4', latitude: 17.10090637207, longitude: 96.041793823242, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11103-5', latitude: 17.10090637207, longitude: 96.043167114258, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-13213-3', latitude: 17.10090637207, longitude: 96.044540405273, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-13213-1', latitude: 17.10090637207, longitude: 96.045913696289, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11253-1', latitude: 17.10090637207, longitude: 96.047286987305, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-12823-3', latitude: 17.103922, longitude: 96.053436, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11769-1', latitude: 17.106399536133, longitude: 96.055526733398, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-11758-2', latitude: 17.106399536133, longitude: 96.059646606445, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-15277-3', latitude: 17.106399536133, longitude: 96.063766479492, areaCode: 100027, networkCode: 1574, communicationType: '4G', location: 'Myanmar, Yangon, Yangon' }, { cid: '414-06-12962-3', latitude: 17.10604, longitude: 96.064735 }] - return response // response.data.data.list + /* 这里涉及如何将cell聚类成基站的问题,目前暂时在前端做聚类,根据规则,同一个运营商中,nodeBID是唯一的。因此以nodeBID+operator组合作为聚类依据。 + 理论上,同一个nodeBID下的cell的经纬度都是一样的,因此直接取第一个cell的经纬度和地名信息作为基站的经纬度。 + 同一个nodeBID下的cell除经纬度外,还有许多属性值是相同的,这里将cell的nodeBID、operator、technology、lacTac、mcc、mnc作为基站的属性来展示, + 将cell的id、cellId、azimuth、coverageRadius、vendor、fddSpectrum作为cell的属性来展示。 */ + const response = await axios.get(api.location.baseStation, { params: { pageSize: -1 } }) + const baseStations = [] + response.data.data.list.forEach(cell => { + const exist = baseStations.find(bs => bs.nodebId === cell.nodebId && bs.operator === cell.operator) + if (!exist) { + baseStations.push({ + nodebId: cell.nodebId, + operator: cell.operator, + technology: cell.technology, + lacTac: cell.lacTac, + mcc: cell.mcc, + mnc: cell.mnc, + latitude: cell.latitude, + longitude: cell.longitude, + superAdministrativeArea: cell.superAdministrativeArea, + administrativeArea: cell.administrativeArea, + country: cell.country, + cells: [cell] + }) + } else { + exist.cells.push(cell) + } + }) + return baseStations } catch (e) { this.$message.error(this.errorMsgHandler(e)) console.error(e) @@ -844,6 +949,9 @@ export default { subscriberIds: this.trackingSubscribers.map(item => `'${item.subscriberId}'`).join(','), level: this.mapLevel } + if (this.searchList[3].value) { + params.params = `data_source='${this.searchList[3].value}'` + } try { const response = await axios.get(api.location.tracking, { params }) const trackingSubscribers = _.cloneDeep(this.trackingSubscribers) @@ -979,51 +1087,83 @@ export default { } }, renderMarker (data, type) { - let svg - if (type === this.tooltipType.baseStation) { - svg = baseStationSvg - } else if (type === this.tooltipType.human) { - svg = humanSvg - } try { data.forEach(marker => { - if (type === this.tooltipType.human && marker.subscriberLatitude) { - const el = document.createElement('div') - el.className = `map-marker map-marker--${type}` - if (marker.subscriberId === this.highlightSubscriber.subscriberId) { - el.classList.add('map-marker--highlight') + if (type === this.tooltipType.human && (marker.sessionRecordPoint || marker.sdPoint)) { + if (marker.sessionRecordPoint) { + const el = document.createElement('div') + el.className = `map-marker map-marker--${type}` + if (marker.subscriberId === this.highlightSubscriber.subscriberId) { + el.classList.add('map-marker--highlight') + } + // 非关注的用户,默认隐藏图标,鼠标移到列表后才显示 + if (this.currentZoom && this.currentZoom < 6) { + el.classList.add('map-marker--hidden') + } + if (!marker.isFollowed && this.onlyShowFollowed) { + el.classList.add('map-marker--unfollowed') + } + el.innerHTML = humanLeakedSvg + // 鼠标事件,控制tooltip显示和marker尺寸 + this.bindMarkerEvent(el, marker, type) + const mapMarker = new maplibregl.Marker({ element: el }) + .setLngLat(marker.sessionRecordPoint.coordinate) + .addTo(this.mapChart) + mapMarker.subscriberId = marker.subscriberId + mapMarker.isFollowed = marker.isFollowed + mapMarker.active = marker.active + this.humanMarkers.push(mapMarker) } - // 非关注的用户,默认隐藏图标,鼠标移到列表后才显示 - if (this.currentZoom && this.currentZoom < 11) { - el.classList.add('map-marker--hidden') + if (marker.sdPoint) { + const el = document.createElement('div') + el.className = `map-marker map-marker--${type}` + if (marker.subscriberId === this.highlightSubscriber.subscriberId) { + el.classList.add('map-marker--highlight') + } + // 非关注的用户,默认隐藏图标,鼠标移到列表后才显示 + if (this.currentZoom && this.currentZoom < 6) { + el.classList.add('map-marker--hidden') + } + if (!marker.isFollowed && this.onlyShowFollowed) { + el.classList.add('map-marker--unfollowed') + } + el.innerHTML = humanSvg + // 鼠标事件,控制tooltip显示和marker尺寸 + this.bindMarkerEvent(el, marker, type) + const mapMarker = new maplibregl.Marker({ element: el }) + .setLngLat(marker.sdPoint.coordinate) + .addTo(this.mapChart) + mapMarker.subscriberId = marker.subscriberId + mapMarker.isFollowed = marker.isFollowed + mapMarker.active = marker.active + this.humanMarkers.push(mapMarker) } - if (!marker.isFollowed && this.onlyShowFollowed) { - el.classList.add('map-marker--unfollowed') - } - el.innerHTML = svg - // 鼠标事件,控制tooltip显示和marker尺寸 - this.bindMarkerEvent(el, marker, type) - const position = [marker.subscriberLongitude, marker.subscriberLatitude] - const mapMarker = new maplibregl.Marker({ element: el }) - .setLngLat(position) - .addTo(this.mapChart) - mapMarker.subscriberId = marker.subscriberId - mapMarker.isFollowed = marker.isFollowed - mapMarker.active = marker.active - this.humanMarkers.push(mapMarker) } else if (type === this.tooltipType.baseStation) { const el = document.createElement('div') el.className = `map-marker map-marker--${type}` - if (this.currentZoom && this.currentZoom < 11) { + if (this.currentZoom && this.currentZoom < 6) { el.classList.add('map-marker--hidden') } - el.innerHTML = svg + el.innerHTML = baseStationSvg // 鼠标事件,控制tooltip显示和marker尺寸 this.bindMarkerEvent(el, marker, type) const mapMarker = new maplibregl.Marker({ element: el }) .setLngLat([marker.longitude, marker.latitude]) .addTo(this.mapChart) this.baseStationMarkers.push(mapMarker) + } else if (type === this.tooltipType.cell) { + const el = document.createElement('div') + el.className = `map-marker map-marker--${type}` + if (this.currentZoom && this.currentZoom < 6) { + el.classList.add('map-marker--hidden') + } + el.innerHTML = cellSvg + // 鼠标事件,控制tooltip显示和marker尺寸 + this.bindMarkerEvent(el, marker, type) + const mapMarker = new maplibregl.Marker({ element: el }) + .setLngLat([marker.longitude, marker.latitude]) + .addTo(this.mapChart) + this.cellMarkers.push(mapMarker) } }) } catch (e) { @@ -1038,6 +1178,8 @@ export default { .setLngLat(coordinates) .addTo(this.mapChart) }, + updateHumanMarker () { + }, updateBoundaryBox () { const boundaryBox = this.mapChart.getBounds() this.boundaryBox = { @@ -1060,29 +1202,6 @@ export default { } } return needUpdateData - /* if (Object.keys(this.boundaryBoxExtreme).length === 0) { - this.boundaryBoxExtreme = _.cloneDeep(this.boundaryBox) - return true - } else { - let needUpdateData = false - if (this.boundaryBox.maxLongitude > this.boundaryBoxExtreme.maxLongitude) { - needUpdateData = true - this.boundaryBoxExtreme.maxLongitude = this.boundaryBox.maxLongitude - } - if (this.boundaryBox.maxLatitude > this.boundaryBoxExtreme.maxLatitude) { - needUpdateData = true - this.boundaryBoxExtreme.maxLatitude = this.boundaryBox.maxLatitude - } - if (this.boundaryBox.minLongitude < this.boundaryBoxExtreme.minLongitude) { - needUpdateData = true - this.boundaryBoxExtreme.minLongitude = this.boundaryBox.minLongitude - } - if (this.boundaryBox.minLatitude < this.boundaryBoxExtreme.minLatitude) { - needUpdateData = true - this.boundaryBoxExtreme.minLatitude = this.boundaryBox.minLatitude - } - return needUpdateData - } */ }, // 先使用min=0的等宽分组法,若后续出现特大或特小的异常值导致等宽分组效果不理想,考虑用分位数分组法 calculateValueRamp (data) { @@ -1251,7 +1370,7 @@ export default { console.info(`current zoom: ${this.currentZoom}`) if (this.activeTab === 'traceTracking') { - if (this.currentZoom && this.currentZoom < 11) { + if (this.currentZoom && this.currentZoom < 6) { this.hideBaseStation() } else { this.showBaseStation() @@ -1305,7 +1424,7 @@ export default { trackingHexagonZoomEnd () { this.currentZoom = this.mapChart.getZoom() console.info(`current zoom: ${this.currentZoom}`) - if (this.currentZoom && this.currentZoom < 11) { + if (this.currentZoom && this.currentZoom < 6) { this.hideBaseStation() } else { this.showBaseStation() @@ -1337,11 +1456,18 @@ export default { this.hoverTrigger('trackingHexGrid', this.currentPolygon.id, true) } }, + trackingSourceChange () { + this.timeRefreshChange() + }, bindMarkerEvent (el, markerData, type) { el.addEventListener('mouseenter', e => { this.currentMarkerDom = el if (type === this.tooltipType.human) { this.currentSubscriber = markerData + const toHoverMarkers = this.humanMarkers.filter(m => m.subscriberId === markerData.subscriberId) + toHoverMarkers.forEach(m => { + m.getElement().classList.add('map-marker--hover') + }) if (!this.tooltip.mouseInMarkerOrTooltip) { this.tooltip.x = e.clientX + 15 - e.offsetX this.tooltip.y = (e.clientY + 15 - e.offsetY + this.tooltipDomHeight.human) > this.mapDomHeight ? (this.mapDomHeight - this.tooltipDomHeight.human) : (e.clientY + 15 - e.offsetY) @@ -1352,6 +1478,12 @@ export default { this.tooltip.x = e.clientX + 15 - e.offsetX this.tooltip.y = (e.clientY + 15 - e.offsetY + this.tooltipDomHeight.baseStation) > this.mapDomHeight ? (this.mapDomHeight - this.tooltipDomHeight.baseStation) : (e.clientY + 15 - e.offsetY) } + } else if (type === this.tooltipType.cell) { + this.currentCell = markerData + if (!this.tooltip.mouseInMarkerOrTooltip) { + this.tooltip.x = e.clientX + 15 - e.offsetX + this.tooltip.y = (e.clientY + 15 - e.offsetY + this.tooltipDomHeight.baseStation) > this.mapDomHeight ? (this.mapDomHeight - this.tooltipDomHeight.baseStation) : (e.clientY + 15 - e.offsetY) + } } this.tooltip.mouseInMarkerOrTooltip = true @@ -1365,17 +1497,28 @@ export default { el.classList.remove('map-marker--hover') this.tooltip.mouseInMarkerOrTooltip = false this.tooltip.showMarkerTooltip = false + if (type === this.tooltipType.human) { + const toCancelHoverMarkers = this.humanMarkers.filter(m => m.subscriberId === markerData.subscriberId) + toCancelHoverMarkers.forEach(m => { + m.getElement().classList.remove('map-marker--hover') + }) + } } }) if (type === this.tooltipType.human) { el.addEventListener('click', e => { + // 先去掉所有高亮marker this.humanMarkers.forEach(m => { m.getElement().classList.remove('map-marker--highlight') }) + // 再判断点击前是否高亮。是则去掉highlightSubscriber的引用,否则查找两种dataSource的marker并高亮 if (this.highlightSubscriber.subscriberId !== markerData.subscriberId) { this.rightExpanded = true - el.classList.add('map-marker--highlight') this.highlightSubscriber = markerData + const toHighlightMarkers = this.humanMarkers.filter(m => m.subscriberId === markerData.subscriberId) + toHighlightMarkers.forEach(m => { + m.getElement().classList.add('map-marker--highlight') + }) // 将滚动条跳转到对应位置 document.querySelector(`#locationMap-subscriberId-${markerData.subscriberId}`).scrollIntoView({ behavior: 'smooth', block: 'center' }) } else { @@ -1383,6 +1526,54 @@ export default { } }) } + if (type === this.tooltipType.baseStation) { + // 点击基站后展开各个方向的cell + el.addEventListener('click', e => { + // 首先将基站图标高亮 + this.baseStationMarkers.forEach(m => { + m.getElement().classList.remove('map-marker--highlight') + }) + this.cleanCellsAndLines() + if (this.highlightBaseStation.nodebId !== markerData.nodebId || this.highlightBaseStation.operator !== markerData.operator) { + el.classList.add('map-marker--highlight') + this.highlightBaseStation = markerData + + // 然后根据cells的数据,生成各个方位角的cell + // 先将zoom调到12,然后以基站为中心点,计算指定角度指定距离的点的坐标,并绘制点以及连线 + this.mapChart.flyTo({ + center: [markerData.longitude, markerData.latitude], + zoom: 12.5, + speed: 0.6 + }) + const points = [] + // 先判断cell的方位角有没重合,重合的话在老cell的基础上往右偏移一些 + markerData.cells.forEach(cell => { + const sameAzimuthCells = points.filter(p => p.azimuth === cell.azimuth) + const sourceCoordinate = [] + let coordinate + if (sameAzimuthCells.length > 0) { + const lastSameAzimuthCell = sameAzimuthCells[sameAzimuthCells.length - 1] + coordinate = turf.destination([lastSameAzimuthCell.longitude, lastSameAzimuthCell.latitude], 0.15, 90).geometry.coordinates + sourceCoordinate.push(lastSameAzimuthCell.longitude) + sourceCoordinate.push(lastSameAzimuthCell.latitude) + } else { + coordinate = turf.destination([markerData.longitude, markerData.latitude], 0.8, cell.azimuth).geometry.coordinates + sourceCoordinate.push(markerData.longitude) + sourceCoordinate.push(markerData.latitude) + } + points.push({ + ...cell, + longitude: coordinate[0], + latitude: coordinate[1], + sourceCoordinate + }) + }) + this.drawCellsAndLines(points) + } else { + this.highlightBaseStation = {} + } + }) + } }, tooltipMouseEnter () { this.tooltip.mouseInMarkerOrTooltip = true @@ -1427,6 +1618,11 @@ export default { } } }, + timelineClick (subscriber, record) { + // 点击timeline时,将地图中心移动到该记录的位置,并高亮左侧小红圈 + this.mapChart.panTo([record.subscriberLongitude, record.subscriberLatitude], { duration: 500 }) + this.highlightTrackingTimeline = record + }, // subscriber列表点击后将地图上的人图标保持特殊高亮,并移动地图中心至图标点 subscriberListClick (subscriber) { // 先删除当前所有高亮的 @@ -1437,25 +1633,31 @@ export default { this.highlightSubscriber = {} } else { this.highlightSubscriber = subscriber - const target = this.humanMarkers.find(m => subscriber.subscriberId === m.subscriberId) - if (target) { - target.getElement().classList.add('map-marker--highlight') - this.mapChart.panTo(target.getLngLat(), { duration: 500 }) + const targets = this.humanMarkers.filter(m => subscriber.subscriberId === m.subscriberId) + if (targets.length > 0) { + targets.forEach(target => { + target.getElement().classList.add('map-marker--highlight') + }) + this.mapChart.panTo(targets[0].getLngLat(), { duration: 500 }) } } }, subscriberListMouseEnter (subscriber) { subscriber.showJumpToEntity = true - const target = this.humanMarkers.find(m => subscriber.subscriberId === m.subscriberId) - if (target) { - target.addClassName && target.addClassName('map-marker--hover') + const targets = this.humanMarkers.filter(m => subscriber.subscriberId === m.subscriberId) + if (targets.length > 0) { + targets.forEach(target => { + target.addClassName && target.addClassName('map-marker--hover') + }) } }, subscriberListMouseLeave (subscriber) { subscriber.showJumpToEntity = false - const target = this.humanMarkers.find(m => subscriber.subscriberId === m.subscriberId) - if (target) { - target.removeClassName && target.removeClassName('map-marker--hover') + const targets = this.humanMarkers.filter(m => subscriber.subscriberId === m.subscriberId) + if (targets.length > 0) { + targets.forEach(target => { + target.removeClassName && target.removeClassName('map-marker--hover') + }) } }, async scrollList (e) { @@ -1553,7 +1755,6 @@ export default { this.opacity = 1 } }, 16) - console.info(this.trackingSubscribers) }, // 追踪页删除追踪 removeTrackingSubscriber (subscriber) { @@ -1647,13 +1848,15 @@ export default { isFollowed: this.onlyShowFollowed ? 1 : 0 } const paramArray = [] - if (this.searchList[2].value) { + if (this.getSearchItem('HEX ID').value) { const levelField = this.mapLevelField.find(item => item.level === this.mapLevel) - paramArray.push(levelField ? levelField.field + "='" + this.searchList[2].value + "'" : '') + paramArray.push(levelField ? levelField.field + "='" + this.getSearchItem('HEX ID').value + "'" : '') } this.searchList.forEach((s, i) => { - if (s.value && i !== 2 && s.active === 1) { + if (s.value && s.label !== 'HEX ID' && s.label !== this.$t('overall.source') && s.active === 1) { paramArray.push(`${s.name} like '%${s.value}%'`) + } else if (s.value && s.label === this.$t('overall.source')) { + paramArray.push(`data_source='${s.value}'`) } }) if (paramArray.length > 0) { @@ -1693,8 +1896,6 @@ export default { this.subscribersList.forEach(s => { if (!s.tags) { s.tags = [] - /*s.tags = [{ value: 'testTag', color: intentColor.Info }, - { value: 'testTag', color: intentColor.Benign }, { value: 'testTagttt', color: intentColor.Malicious }]*/ axios.get(`${api.entity.tags}/subscriber`, { params: { resource: s.subscriberId } }).then(response => { if (response.status === 200) { if (response.data.data && response.data.data.tags) { @@ -1723,20 +1924,34 @@ export default { level: this.mapLevel, subscriberIds: subscriberIds.map(id => `'${id}'`).join(',') } + if (this.getSearchItem(this.$t('overall.source')).value) { + timelineParams.params = `data_source='${this.getSearchItem(this.$t('overall.source')).value}'` + } await axios.get(api.location.tracking, { params: timelineParams }).then(async response => { subscriberList.forEach(item => { const find = response.data.data.result.find(d => { if (d.subscriberId === item.subscriberId) { const hasHexIdArr = d.trackRecords.filter(t => t.hexId) if (hasHexIdArr.length > 0) { - item.subscriberLatitude = hasHexIdArr[0].subscriberLatitude - item.subscriberLongitude = hasHexIdArr[0].subscriberLongitude - item.hexId = hasHexIdArr[0].hexId + // 将session record和sd的坐标点区分开 + const sessionRecordPoint = hasHexIdArr.find(d => d.dataSource === 'Session Record') + const sdPoint = hasHexIdArr.find(d => d.dataSource === 'SD') + if (sessionRecordPoint) { + item.sessionRecordPoint = { + coordinate: [sessionRecordPoint.subscriberLongitude, sessionRecordPoint.subscriberLatitude], + hexId: sessionRecordPoint.hexId + } + } + if (sdPoint) { + item.sdPoint = { + coordinate: [sdPoint.subscriberLongitude, sdPoint.subscriberLatitude], + hexId: sdPoint.hexId + } + } return true } else { - item.subscriberLatitude = null - item.subscriberLongitude = null - item.hexId = null + item.sessionRecordPoint = null + item.sdPoint = null } } return false @@ -1760,7 +1975,7 @@ export default { find.removeClassName && find.removeClassName('map-marker--unfollowed') } // 变更地图上图标的class - if (this.currentZoom && this.currentZoom < 11) { + if (this.currentZoom && this.currentZoom < 6) { this.hideBaseStation() this.hideFollowed() } else { @@ -1775,7 +1990,7 @@ export default { find.addClassName && find.addClassName('map-marker--unfollowed') } // 变更地图上图标的class - if (this.currentZoom && this.currentZoom < 11) { + if (this.currentZoom && this.currentZoom < 6) { this.hideBaseStation() this.hideFollowed() } else { @@ -1817,7 +2032,7 @@ export default { if (find) { find.addClassName && find.addClassName('map-marker--unfollowed') } - if (this.currentZoom && this.currentZoom < 11) { + if (this.currentZoom && this.currentZoom < 6) { this.hideBaseStation() this.hideFollowed() } else { @@ -1841,7 +2056,7 @@ export default { if (find) { find.removeClassName && find.removeClassName('map-marker--unfollowed') } - if (this.currentZoom && this.currentZoom < 11) { + if (this.currentZoom && this.currentZoom < 6) { this.hideBaseStation() this.hideFollowed() } else { @@ -1856,13 +2071,6 @@ export default { }) } }, - async clearHexId () { - this.searchList[2].value = '' - this.curPageNum = 1 - this.subscribersList = [] - await this.initSubscriberList() - this.removeHighlightHexagon() - }, async showFollowedSubscribers () { this.curPageNum = 1 this.subscribersList = [] @@ -1909,6 +2117,42 @@ export default { find.scrollEndIndex = endIndex // 列表距离顶部距离 find.startOffset = scrollTop - (scrollTop % this.scrollInfo.itemSize) + }, + getSearchItem (label) { + return this.searchList.find(l => l.label === label) + }, + cleanCellsAndLines () { + this.cellMarkers.forEach(marker => { + marker.remove && marker.remove() + }) + this.cellMarkers = [] + this.mapChart.getLayer('baseStationToCellLines') && this.mapChart.removeLayer('baseStationToCellLines') + this.mapChart.getSource('baseStationToCellLines') && this.mapChart.removeSource('baseStationToCellLines') + }, + drawCellsAndLines (targets) { + // 画cell + this.renderMarker(targets, this.tooltipType.cell) + // 画线 + const points = targets.map(target => ([target.sourceCoordinate, [target.longitude, target.latitude]])) + const lines = turf.multiLineString(points) + this.mapChart.addSource('baseStationToCellLines', { + type: 'geojson', + data: lines + }) + this.mapChart.addLayer({ + id: 'baseStationToCellLines', + type: 'line', + source: 'baseStationToCellLines', + layout: { + 'line-join': 'round', + 'line-cap': 'round' + }, + paint: { + 'line-color': '#586A7F', + 'line-width': 2, + 'line-dasharray': [2, 2] + } + }) } }, watch: { @@ -1949,6 +2193,14 @@ export default { } } }, + 'searchList.3.active': { + deep: true, + handler (n, o) { + if (n === 0 && this.searchList[3].value) { + this.searchList[3].value = '' + } + } + }, 'searchList.2.value': { deep: true, async handler (n, o) { @@ -1960,6 +2212,16 @@ export default { this.debounceSearch() } }, + 'searchList.3.value': { + deep: true, + async handler (n, o) { + if (this.activeTab === 'locationMap') { + // 由于调整了source,需要重新调整地图上marker的图标 + this.updateHumanMarker() + this.debounceSearch() + } + } + }, async activeTab (n) { this.initFlag = true this.$store.state.headerMenuByTab = n @@ -2063,7 +2325,7 @@ export default { }, currentZoom (n, o) { // zoom 小于11隐藏 marker - if (o && n < 11) { + if (o && n < 6) { this.hideBaseStation() this.hideFollowed() } else { @@ -2103,6 +2365,8 @@ export default { return 'var(--el-color-business)' } else if (this.tooltip.type === this.tooltipType.baseStation) { return '#233447' + } else if (this.tooltip.type === this.tooltipType.cell) { + return '#274E85' } return '' }, @@ -2221,18 +2485,42 @@ export default { label: 'ID', name: 'subscriber_id', value: '', + type: 'input', active: 1 }, { label: 'MSISDN', name: 'phone_number', value: '', + type: 'input', active: 0 }, { label: 'HEX ID', name: 'second_location', value: '', + type: 'input', + active: 0 + }, + { + label: i18n.global.t('overall.source'), + name: 'data_source', + value: '', + type: 'select', + options: [ + { + label: i18n.global.t('overall.all'), + value: '' + }, + { + label: i18n.global.t('location.baseStation'), + value: 'SD' + }, + { + label: i18n.global.t('overall.leaked'), + value: 'Session Record' + } + ], active: 0 } ]) @@ -2242,6 +2530,7 @@ export default { const currentMarkerDom = shallowRef(null) const humanMarkers = shallowRef([]) const baseStationMarkers = shallowRef([]) + const cellMarkers = shallowRef([]) const trackingHumanMarker = shallowRef({}) const pieChart = shallowRef(null) const pieOption = ref({}) @@ -2250,7 +2539,10 @@ export default { const currentBaseStation = ref({}) const currentSubscriber = ref({}) const currentPolygon = ref({}) + const currentCell = ref({}) + const highlightTrackingTimeline = ref({}) const highlightSubscriber = ref({}) + const highlightBaseStation = ref({}) // 从localStorage中获取数据 const trackingSubscribers = ref([]) @@ -2290,7 +2582,7 @@ export default { minuteTimeFilter, // 底下时间轴的时间 searchList, // 搜索框下拉列表 leftExpanded: ref(false), // 左右侧栏是否展开 - rightExpanded: ref(false), + rightExpanded: ref(true), tooltip, // 控制鼠标悬浮框 pieColorRamp, // 六边形颜色坡度 pieValueRamp, // 饼图数值坡度,动态获取 @@ -2302,6 +2594,7 @@ export default { currentMarkerDom, // 记录当前鼠标悬停的marker的dom humanMarkers, // 储存人marker的引用 baseStationMarkers, // 储存基站marker的引用 + cellMarkers, // 储存cell marker的引用 trackingHumanMarker, // 追踪页的人marker pieChart, // 饼图对象 pieOption, @@ -2312,7 +2605,10 @@ export default { currentBaseStation, // 鼠标当前悬浮的基站 currentSubscriber, // 鼠标当前悬浮的Subscriber currentPolygon, // 鼠标当前悬浮的六边形 + currentCell, // 鼠标当前悬浮的cell + highlightTrackingTimeline, // tracking页高亮的时间线 highlightSubscriber, // locationMap页保持高亮的subscriber + highlightBaseStation, // 高亮的基站 trackingSubscribers, // 存放当前追踪的Subscriber列表 currentShowSubscriber, // 当前在地图上展示轨迹的Subscriber trackingSubscriberRecordMap: [], // record数据量大时,vue监听性能开销太大,所以单独用非监听的数组来维护subscriberId与record的关系