Commit f196007987c1b0d22c9f5e3c8306bf4f55aa283e

Authored by xp.Huang
2 parents eebc69c2 9cd8cd65

Merge branch 'ft' into 'main_dev'

perf(src/packages): 优化高德地图标点弹窗和数据展示

See merge request yunteng/thingskit-view!133
Showing 19 changed files with 232 additions and 41 deletions
... ... @@ -235,6 +235,7 @@ const updateVChart = async (newData: SocketReceiveMessageType) => {
235 235 }
236 236
237 237 const { vChartRef } = useChartDataFetch(props.chartConfig, useChartEditStore, (newData: any, targetComponent: any) => {
  238 + props.chartConfig.option.queryCondition.timeRange=[targetComponent.requestParams.Params.startTs,targetComponent.requestParams.Params.endTs]
238 239 //联动支持分组
239 240 /**
240 241 * 修复多个分组,然后下拉框联动,会影响另一个组件
... ...
... ... @@ -12,7 +12,7 @@ export type dataJsonType = typeof dataJson //data.json类型
12 12 export type dataJsonMarkersType = typeof dataJson.markers[number] //data.json markers类型
13 13
14 14 //标注数据格式
15   -export const fileterDevice = (items: any) => {
  15 +export const filterDevice = (items: any) => {
16 16 const values = items.reduce((acc: any, curr: any) => {
17 17 acc.push({
18 18 name: curr.alias,
... ... @@ -88,6 +88,10 @@ export const option = {
88 88 amapLon: 104.108689,
89 89 amapLat: 30.66176,
90 90 amapZindex: 11,
  91 + typeMarker: '',
  92 + mpBorderConfig: {
  93 + value: 'Border01',
  94 + },
91 95 marker: {
92 96 fillColor: '#E98984FF',
93 97 fillOpacity: 0.5,
... ...
... ... @@ -71,19 +71,64 @@
71 71 <setting-item name="类型">
72 72 <n-select size="small" v-model:value="optionData.mapOptions.mapMarkerType" :options="MarkerOptions" />
73 73 </setting-item>
74   - <setting-item name="颜色">
  74 + <setting-item v-if="optionData.mapOptions.mapMarkerType === MarkerEnum.CIRCLE_MARKER" name="颜色">
75 75 <n-color-picker v-model:value="optionData.mapOptions.marker.fillColor" size="small"></n-color-picker>
76 76 </setting-item>
77 77 </setting-item-box>
  78 + <setting-item-box name="图标选择" v-if="optionData.mapOptions.mapMarkerType === MarkerEnum.MARKER">
  79 + <setting-item name="图标选择">
  80 + <NSelect
  81 + size="small"
  82 + placeholder="请选择您要使用的图标"
  83 + style="width: 250px"
  84 + :value="typeMarkerValue"
  85 + :options="typeMarkerOptions"
  86 + :render-label="renderOption"
  87 + clearable
  88 + filterable
  89 + @update:value="selectHandle"
  90 + />
  91 + </setting-item>
  92 + </setting-item-box>
  93 + <setting-item-box name="弹窗选择" v-if="optionData.mapOptions.mapMarkerType === MarkerEnum.MARKER">
  94 + <setting-item name="弹窗选择">
  95 + <!-- <NSelect
  96 + size="small"
  97 + placeholder="请选择您要使用的弹窗"
  98 + style="width: 250px"
  99 + :value="mapSelectBorderValue"
  100 + :options="mapSelectBorderOption.option"
  101 + @update:value="mapSelectBorderHandle"
  102 + clearable
  103 + filterable
  104 + /> -->
  105 + <NSelect
  106 + size="small"
  107 + placeholder="请选择您要使用的弹窗"
  108 + style="width: 250px"
  109 + :value="mapSelectBorderValue"
  110 + :options="mapBorderOptions"
  111 + :render-label="renderMapBorderOption"
  112 + clearable
  113 + filterable
  114 + @update:value="selectMapBorderHandle"
  115 + />
  116 + </setting-item>
  117 + </setting-item-box>
78 118 </collapse-item>
79 119 </template>
80 120
81 121 <script setup lang="ts">
82   -import { PropType } from 'vue'
  122 +import { PropType, ref, computed, h, onMounted, reactive } from 'vue'
83 123 import { option, MarkerEnum, ThemeEnum, LangEnum, ViewModeEnum, FeaturesEnum } from './config'
84 124 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
  125 +import { NEllipsis, NImage, NSelect, NSpace, SelectOption } from 'naive-ui'
85 126
86   -defineProps({
  127 +interface BaseSelectBorderIF extends SelectOption {
  128 + config?: string
  129 +}
  130 +
  131 +const props = defineProps({
87 132 optionData: {
88 133 type: Object as PropType<typeof option>,
89 134 required: true
... ... @@ -187,10 +232,10 @@ const featuresOptions = [
187 232 ]
188 233
189 234 const MarkerOptions = [
190   - {
191   - value: MarkerEnum.CIRCLE_MARKER,
192   - label: '圆形标点'
193   - },
  235 + // {
  236 + // value: MarkerEnum.CIRCLE_MARKER,
  237 + // label: '圆形标点'
  238 + // },
194 239 {
195 240 value: MarkerEnum.MARKER,
196 241 label: '定位标点'
... ... @@ -200,4 +245,101 @@ const MarkerOptions = [
200 245 label: '隐藏标点'
201 246 }
202 247 ]
  248 +
  249 +onMounted(() => {
  250 + props.optionData.mapOptions.typeMarker = getMarkerImagePath(props.optionData.mapOptions.typeMarker || 'position1.png')
  251 + typeMarkerValue.value = props.optionData.mapOptions.typeMarker || 'position1.png'
  252 + mapSelectBorderValue.value =
  253 + `${props.optionData.mapOptions.mpBorderConfig.value.toLocaleLowerCase()}.png` || 'border01.png'
  254 +})
  255 +
  256 +const typeMarkerValue = ref<string | null>('position1.png')
  257 +
  258 +const mapSelectBorderValue = ref<string | null>('border01.png')
  259 +
  260 +const isHref = (url: string) => {
  261 + try {
  262 + new URL(url)
  263 + return true
  264 + } catch (error) {
  265 + return false
  266 + }
  267 +}
  268 +
  269 +// import.meta.glob 这个不能封装,必须是字符串,不能通过传值传进去
  270 +const typeMarkerOptions = computed(() => {
  271 + const pathList = import.meta.glob('./images/marker/*')
  272 + return Object.keys(pathList).map(item => {
  273 + const imgName = item.split('/').at(-1)
  274 + return {
  275 + label: imgName,
  276 + value: imgName
  277 + } as SelectOption
  278 + })
  279 +})
  280 +//
  281 +
  282 +const getMarkerImagePath = (name: string) => {
  283 + return isHref(name) ? name : new URL(`./images/marker/${name}`, import.meta.url).href
  284 +}
  285 +
  286 +const renderOption = (option: SelectOption) => {
  287 + return h(NSpace, { justify: 'space-between', style: 'padding: 0 15px; height: 28px; line-height: 28px;' }, () => [
  288 + h(NImage, {
  289 + width: 25,
  290 + src: getMarkerImagePath(option.value as string),
  291 + previewDisabled: true,
  292 + style: { height: '25px' }
  293 + } as Recordable),
  294 + h(NEllipsis, null, () => option.label)
  295 + ])
  296 +}
  297 +
  298 +const selectHandle = (value: string) => {
  299 + typeMarkerValue.value = value
  300 + props.optionData.mapOptions.typeMarker = getMarkerImagePath(value)
  301 +}
  302 +
  303 +const needBorder = ['border01.png', 'border02.png', 'border03.png', 'border05.png', 'border07.png']
  304 +
  305 +// import.meta.glob 这个不能封装,必须是字符串,不能通过传值传进去
  306 +const mapBorderOptions = computed(() => {
  307 + const pathList = import.meta.glob('../../../../../../assets/images/chart/decorates/*')
  308 + return Object.keys(pathList)
  309 + .map(item => {
  310 + const imgName = item.split('/').at(-1) as string
  311 + if (needBorder.includes(imgName)) {
  312 + return {
  313 + label: imgName,
  314 + value: imgName
  315 + } as SelectOption
  316 + }
  317 + })
  318 + .filter(Boolean) as SelectOption[]
  319 +})
  320 +//
  321 +
  322 +const getMapBorderImagePath = (name: string) => {
  323 + return isHref(name) ? name : new URL(`../../../../../../assets/images/chart/decorates/${name}`, import.meta.url).href
  324 +}
  325 +
  326 +const renderMapBorderOption = (option: SelectOption) => {
  327 + return h(NSpace, { justify: 'space-between', style: 'padding: 0 15px; height: 28px; line-height: 28px;' }, () => [
  328 + h(NImage, {
  329 + width: 25,
  330 + src: getMapBorderImagePath(option.value as string),
  331 + previewDisabled: true,
  332 + style: { height: '25px' }
  333 + } as Recordable),
  334 + h(NEllipsis, null, () => option.label)
  335 + ])
  336 +}
  337 +
  338 +const selectMapBorderHandle = (value: string) => {
  339 + mapSelectBorderValue.value = value
  340 + const toLowerValue = value.toLocaleLowerCase()
  341 + ;(props.optionData.mapOptions.mpBorderConfig as BaseSelectBorderIF) = {
  342 + value: toLowerValue[0]?.toUpperCase() + toLowerValue?.substr(1)?.split('.')[0]
  343 + }
  344 +}
203 345 </script>
... ...
1 1 {
2 2 "markers": [
3 3 {
4   - "name": "模拟11111111111",
  4 + "name": "模拟数据1",
5 5 "value": 20,
6 6 "position": [103.856504, 30.687278],
7 7 "extraInfo": {
8 8 "tbDeviceId": "@xxxxxxxxxxx",
9   - "name": "模拟11111111111",
10   - "alias": "模拟11111111111",
  9 + "name": "模拟数据1",
  10 + "alias": "模拟数据1",
11 11 "organizationDTO": {
12   - "name": "模拟11111111111"
  12 + "name": "模拟数据1"
13 13 },
14 14 "deviceState": "INACTIVE",
15 15 "deviceProfile": {
... ... @@ -23,15 +23,15 @@
23 23 }
24 24 },
25 25 {
26   - "name": "模拟22222222222",
  26 + "name": "模拟数据2",
27 27 "value": 30,
28 28 "position": [104.095368, 30.716787],
29 29 "extraInfo": {
30 30 "tbDeviceId": "@xxxxxxxxxxxxxxx",
31   - "name": "模拟22222222222",
32   - "alias": "模拟22222222222",
  31 + "name": "模拟数据2",
  32 + "alias": "模拟数据2",
33 33 "organizationDTO": {
34   - "name": "模拟22222222222"
  34 + "name": "模拟数据2"
35 35 },
36 36 "deviceState": "INACTIVE",
37 37 "deviceProfile": {
... ...
... ... @@ -6,16 +6,17 @@
6 6 </template>
7 7
8 8 <script setup lang="ts">
9   -import { ref, PropType, toRefs, watch } from 'vue'
  9 +import { ref, PropType, toRefs, watch, render, h } from 'vue'
10 10 import AMapLoader from '@amap/amap-jsapi-loader'
11 11 import { CreateComponentType } from '@/packages/index.d'
12 12 import { useChartDataFetch } from '@/hooks'
13 13 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
14   -import { MarkerEnum, ThemeEnum, dataExtraInfoType, dataJsonType, dataJsonMarkersType, fileterDevice } from './config'
  14 +import { MarkerEnum, ThemeEnum, dataExtraInfoType, dataJsonType, dataJsonMarkersType, filterDevice } from './config'
15 15 import { isArray } from '@/utils'
16 16 import djh from './images/djh.png'
17 17 import online from './images/online.png'
18 18 import lx1 from './images/lx1.png'
  19 +import positionImg from './images/marker/position1.png'
19 20 import { getDeviceActiveTime, getDeviceList } from '@/api/external/common/index'
20 21 import dayjs from 'dayjs'
21 22 import SearchBox from './components/SearchBox.vue'
... ... @@ -44,7 +45,9 @@ let {
44 45 viewMode,
45 46 pitch,
46 47 skyColor,
47   - marker
  48 + marker,
  49 + typeMarker,
  50 + mpBorderConfig
48 51 } = toRefs(props.chartConfig.option.mapOptions)
49 52
50 53 //官方没有高德地图api的ts,所以类型全用的any
... ... @@ -84,8 +87,7 @@ const initMap = (newData: any) => {
84 87 `amap://styles/${amapStyleKeyCustom.value !== '' ? amapStyleKeyCustom.value : amapStyleKey.value}`
85 88 )
86 89 }
87   - //点击地图任意地方关闭infoWindow窗体
88   - mapIns.on('click', () => {
  90 + mapIns.on('mouseout', () => {
89 91 mapIns.clearInfoWindow()
90 92 })
91 93 })
... ... @@ -102,26 +104,38 @@ const createInfoWindow = async (extraInfo: dataExtraInfoType) => {
102 104 const res = await getDeviceActiveTime(tbDeviceId) //查询设备最后离线时间
103 105 let { lastUpdateTs } = res[0]
104 106 const lastUpdateFormatTs = dayjs(lastUpdateTs).format('YYYY-MM-DD HH:mm:ss')
  107 + //render方式渲染小组件里的边框组件
  108 + const BorderInstance = await import(`../../../../Decorates/Borders/${mpBorderConfig.value.value}/index.vue`)
  109 + const config = await import(`../../../../Decorates/Borders/${mpBorderConfig.value.value}/config.ts`)
  110 + const BorderConfigInstance = new config.default()
  111 + const id = Date.now().toString()
  112 + setTimeout(() => {
  113 + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  114 + render(h(BorderInstance.default, { chartConfig: BorderConfigInstance }), document.getElementById(id)!)
  115 + }, 100)
  116 + //
105 117 return `
106   - <div style="width:15vw;height:18vh;background-color:white;">
107   - <div style="margin:0px 10px">
  118 + <div id="${id}" style="width:25vw;height:29vh;position:relative;top:30px;">
  119 + <div style="position: absolute;top: 52px;left: 105px;">
108 120 <div style="display:flex;justify-content:space-between; margin:20px 0px;">
109   - <div style="font-size:16px;font-weight:bold">${alias || name}</div>
  121 + <div style="width:12vw;text-overflow: ellipsis;overflow: hidden; word-break: break-all;white-space: nowrap;font-size:18px;font-weight:bold;color:white">${
  122 + alias || name
  123 + }</div>
110 124 ${
111 125 deviceState === 'INACTIVE'
112   - ? `<div style="display:flex;align-items:center;"><img style="width:15px;height:15px" src="${djh}" class="mr-1">待激活</div>`
  126 + ? `<div style="display:flex;align-items:center;color:#999999"><img style="width:15px;height:15px;margin-right:5px" src="${djh}">待激活</div>`
113 127 : deviceState === 'ONLINE'
114   - ? `<div style="display:flex;align-items:center; ">
115   - <img style="width:15px;height:15px" src="${online}" class="mr-1">在线</div>`
116   - : `<div style="display:flex;align-items:center;"><img style="width:15px;height:15px" src="${lx1}" class="mr-1">离线</div>`
  128 + ? `<div style="display:flex;align-items:center;color:green">
  129 + <img style="width:15px;height:15px;margin-right:5px" src="${online}" class="mr-1">在线</div>`
  130 + : `<div style="display:flex;align-items:center;color:#F5222D"><img style="width:15px;height:15px;margin-right:5px" src="${lx1}">离线</div>`
117 131 }
118 132 </div>
119   - <div>所属组织:${organizationDTO.name}</div>
120   - <div style="margin-top:6px;">接入协议:${deviceProfile.transportType}</div>
121   - <div style="margin-top:6px;">
  133 + <div style="color:white;">所属组织:${organizationDTO.name}</div>
  134 + <div style="margin-top:6px;color:white">接入协议:${deviceProfile.transportType}</div>
  135 + <div style="margin-top:6px;color:white">
122 136 设备位置:${!deviceInfo.address ? '该设备暂无地理位置' : deviceInfo.address}
123 137 </div>
124   - <div style="margin-top:6px;">
  138 + <div style="margin-top:6px;color:white">
125 139 ${
126 140 deviceState === 'ONLINE' ? '在线' : deviceState === 'INACTIVE' ? '创建' : '离线'
127 141 }时间:${lastUpdateFormatTs}
... ... @@ -145,7 +159,7 @@ const handleCloseDrawer = () => (modelShow.value = false)
145 159 const handleSearchParams = async (searchPage: any, params: any) => {
146 160 try {
147 161 const { items } = await getDeviceList(searchPage, params)
148   - const values = fileterDevice(items)
  162 + const values = filterDevice(items)
149 163 if (!values) return
150 164 dataHandle(values)
151 165 } finally {
... ... @@ -153,22 +167,23 @@ const handleSearchParams = async (searchPage: any, params: any) => {
153 167 }
154 168 }
155 169
156   -//地图点击
  170 +//地图鼠标hover
157 171 const mapClick = (markerInstance: any, markerItem: dataJsonMarkersType) => {
158 172 markerInstance.setExtData({
159 173 extraInfo: markerItem.extraInfo
160 174 })
161   - markerInstance.setLabel({
162   - content: markerItem.extraInfo.alias || markerItem.extraInfo.name
163   - })
164   - markerInstance.on('click', async (e: any) => {
  175 + markerInstance.on('mouseover', async (e: any) => {
165 176 const { extraInfo } = e.target.getExtData()
  177 + if (extraInfo.tbDeviceId.startsWith('@')) return //假的模拟数据则终止弹窗
166 178 let infoWindow = new AMapIns.InfoWindow({
167 179 content: await createInfoWindow(extraInfo),
168   - offset: new AMapIns.Pixel(0, -50)
  180 + offset: new AMapIns.Pixel(3, -30)
169 181 })
170 182 infoWindow.open(mapIns, e.target.getPosition())
171 183 })
  184 + markerInstance.on('mouseout', () => {
  185 + mapIns.clearInfoWindow()
  186 + })
172 187 }
173 188
174 189 const dataHandle = (newData: dataJsonType) => {
... ... @@ -185,7 +200,8 @@ const dataHandle = (newData: dataJsonType) => {
185 200 newData.markers.forEach((markerItem: dataJsonMarkersType) => {
186 201 const markerInstance = new AMapIns.Marker({
187 202 position: [markerItem.position[0], markerItem.position[1]],
188   - offset: new AMapIns.Pixel(-13, -30)
  203 + offset: new AMapIns.Pixel(-13, -30),
  204 + icon: typeMarker.value || positionImg
189 205 })
190 206 // markers.push(markerInstance) 原作者这种方式添加,属于JS API 1.4.8版本的
191 207 // markerInstance.setMap(mapIns)
... ... @@ -243,6 +259,29 @@ useChartDataFetch(props.chartConfig, useChartEditStore, (newData: any) => {
243 259 })
244 260 </script>
245 261
  262 +<style>
  263 +.amap-info-outer {
  264 + background-color: rgba(0, 0, 0, 0) !important;
  265 +}
  266 +.amap-info-close {
  267 + display: none !important;
  268 +}
  269 +.amap-info-content {
  270 + overflow: hidden !important;
  271 +}
  272 +.amap-info-content .go-border-box {
  273 + position: absolute;
  274 + transform: scale(0.7);
  275 + top: -10px;
  276 + /* opacity: 0,8; */
  277 + /* background-color: rgba(0, 0, 0, 0.1); */
  278 + background-color:rgba(255,255,255,0.1)
  279 +}
  280 +.amap-info-content .go-border-box svg {
  281 + /* background-color: rgba(0, 0, 0, 0.1); */
  282 + /* background-color: rgba(255, 255, 255, 0.8); */
  283 +}
  284 +</style>
246 285 <style lang="scss" scoped>
247 286 .chart-amap {
248 287 position: relative;
... ...
... ... @@ -82,7 +82,9 @@ import { fetchConfigComponent, fetchChartComponent } from '@/packages/index'
82 82 import { componentInstall, loadingStart, loadingFinish, loadingError } from '@/utils'
83 83 import { ChartGlobImage } from '@/components/Pages/ChartGlobImage'
84 84 import { Icon } from '@iconify/vue'
85   -
  85 +// THINGS_KIT 修改搜索未过滤隐藏的组件
  86 +import { hideAsideComponentsObj } from '../../external/components/ChartsOptionContent/config'
  87 +//
86 88 const props = defineProps({
87 89 menuOptions: {
88 90 type: Array,
... ... @@ -136,6 +138,9 @@ const searchHandle = (key: string | null) => {
136 138 searchRes.value = List.filter(
137 139 (e: ConfigType) => !e.disabled && (!key || e.title.toLowerCase().includes(key.toLowerCase()))
138 140 )
  141 + // THINGS_KIT 修改搜索未过滤隐藏的组件
  142 + searchRes.value = searchRes.value.filter((e: ConfigType) =>!hideAsideComponentsObj['all'].includes(e.chartKey))
  143 + //
139 144 setTimeout(() => {
140 145 loading.value = undefined
141 146 }, 500)
... ...