关于vue,ts,echart整合显示地图的实现方式
2024-11-04 15:02:25 # 技术笔记

这两天需要做一个大屏的东西,看着网上那个地图的效果很酷很炫,所以想整一个这样的东西,话不多说,开始整。因为项目本体是人人开源项目,我是在这个基础上做的一个二次开发吧。后端代码就省略了。其实很简单。在此就不会做太多的赘述了。

后端

不过有一个点是可以关注的,就是当你的数据库过于庞大的话,我们可以建一个缓存。也就是一下的示例代码。

2023年5月25日:针对这个数据,因为map里面是放到缓存中了,一直没有释放,所以改用redis,又因为这个是二次开发的项目,所以人家对redis进行了一系列的改造,我在自己新建redis配置类的时候一直报错有冲突,所以在看了一下结构之后,对后端的代码进行了一系列的重构。

redis工具类

用于从 Redis 中获取缓存数据或者将数据存入 Redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

public List<Object> getListFromRedis(String key) {
Object cachedData = get(key);
if (cachedData != null) {
// 如果缓存中存在数据,则将其转换为 List 对象并返回
return (List) cachedData;
} else {
// 如果缓存中不存在数据,则返回空列表
return new ArrayList<>();
}
}
public void setListToRedis(String key, List<Object> data, long expire) {
set(key, data, expire);
}

控制层

修改 getEnterpriseNums 方法,以先从 Redis 中获取数据,如果缓存中存在数据,则直接返回;如果缓存中不存在数据,则从数据库中获取数据,并将其存入 Redis 缓存中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    @Autowired
private RedisUtils redisUtils;
@GetMapping("/enterpriseNums")
@ApiOperation("返回地图上的区域数据")
public Result getEnterpriseNums(){
// List<ShowEnterpriseDetailsNumsDTO> enterpriseDetailsList = enterpriseDetailsService.getEnterpriseNums();
// return new Result().ok(enterpriseDetailsList);
String redisKey = "enterpriseNums"; // 定义 Redis 缓存的键名
List enterpriseDetailsList = redisUtils.getListFromRedis(redisKey);
if (enterpriseDetailsList.isEmpty()) {
// 从数据库中获取数据
enterpriseDetailsList = enterpriseDetailsService.getEnterpriseNums();
// 将数据存入 Redis 缓存,设置过期时间为一小时
redisUtils.setListToRedis(redisKey, enterpriseDetailsList, RedisUtils.HOUR_ONE_EXPIRE);
}
return new Result().ok(enterpriseDetailsList);
}

首先从 showEnterpriseDetailsServicesCache 缓存中获取工厂信息列表数据。如果列表数据为空(即缓存中没有数据),则调用 enterpriseDetailsService.getEnterpriseNums() 方法获取工厂信息数据,并将其存入缓存中。在下次调用 getEnterpriseNums() 方法时,将直接从缓存中获取数据,避免了重复查询的开销。

通过以上优化,当第一次请求 getEnterpriseNums 接口时,数据将从数据库中获取,并存入 Redis 缓存中。之后的请求将直接从 Redis 缓存中获取数据,从而提高响应速度和性能。请确保 Redis 配置正确,并已经启动了 Redis 服务。

前端

echarts示例:https://echarts.apache.org/examples/zh/editor.html?c=map-HK

他给的示例我们可以分析出来,我觉得是通过ajax请求来把地图搞到,然后绑定map最后,把数据显示出来。$.get(ROOT_PATH + '/data/asset/geo/HK.json', function (geoJson)用于异步加载地图数据。这段代码的作用是通过 AJAX 请求获取名为 'HK.json' 的地图数据文件,然后在成功获取数据后,使用echarts.registerMap(‘HK’, geoJson)将地图数据注册到 ECharts 中,并命名为 ‘HK’。这样,之后在配置地图的 series中就可以通过指定map: ‘HK’` 来使用该地图数据。

再者我们需要提前的把地图数据导入到项目当中来。可以通过这个平台下载地图的数据

http://datav.aliyun.com/portal/school/atlas/area_selector#&lat=30.332329214580188&lng=106.72278672066881&zoom=3.5

然后的话导入方式,因为项目的原因我们需要这样做,

先是import

1
2
3
import jiNanMapJson from "src/views/enterprise/assets/json/济南市.json";

import * as echarts from "echarts";

再然后注册地图数据到 ECharts:

1
echarts.registerMap('jiNan', jiNanMapJson);

更新 mapOption 中的 series 配置,将 map: 'HK' 添加到相关的地图系列中,以便使用注册的地图数据:

1
2
3
4
5
6
7
8
9
10
const mapOption = ref({
// ...其他配置项
series: [
{
// ...其他系列配置
map: 'jiNan', // 使用注册的地图数据
// ...其他系列配置
}
]
});

通过这种方式,我们可以注册到其中,完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
<template>
<div class="box">
<el-form-item>
<el-button type="warning" @click="importHandle()">{{ $t("excel.import") }}</el-button>
</el-form-item>
<div style="width: 100%;margin-left: 6em;margin-bottom: 2em;">
<div class="topShow">
<div style="color:brown; font-size: 1.875rem /* 30/16 */;text-align: center;padding-top: 1.4375rem /* 23/16 */;">
信用金桥大数据屏幕展示平台</div>
<div style="color:brown; font-size: 1.25rem /* 20/16 */;text-align: right;padding-right: 2.1875rem /* 35/16 */;;">
{{ dataForm.currentTime }}</div>
</div>
</div>


<div class="line_01">
<figure class="figure_left">
<div v-for="(item, index) in dataForm.enterpriseNums" :key="index" style="width: 23em;text-align: center; ">
<div class="card">
<span style="color: red;">{{ item.name }}区</span>
在营企业总数 :
<span style="color: sienna; font-size: 2em;">{{ item.value }} 家</span>
</div>
<br />
</div>
</figure>

<figure class="figure_center">

</figure>
<figure class="figure_right">
<v-chart :option="mapOption" :autoresize="true" />
</figure>
</div>
<div class="line_02">
<figure class="figure_left">
<v-chart :option="pie" :autoresize="true" />
</figure>
</div>



<import ref="importRef"></import>
</div>
</template>

<script lang="ts" setup>
import { ref, provide, reactive } from "vue";
import { use } from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";
import { BarChart, LineChart, PieChart, MapChart, RadarChart, ScatterChart, EffectScatterChart, LinesChart } from "echarts/charts";
import { GridComponent, PolarComponent, GeoComponent, TooltipComponent, LegendComponent, TitleComponent, VisualMapComponent, DatasetComponent, ToolboxComponent, DataZoomComponent } from "echarts/components";
import VChart, { THEME_KEY } from "vue-echarts";
import app from "@/constants/app";
import { getToken } from "@/utils/cache";
import Import from '@/views/enterprise/enterprise-library-import.vue';
import baseService from "@/service/baseService";
import jiNanMapJson from "src/views/enterprise/assets/json/济南市.json";
import zhangQiuMapJson from "src/views/enterprise/assets/json/章丘区.json";
import * as echarts from "echarts";

use([BarChart, LineChart, PieChart, MapChart, RadarChart, ScatterChart, EffectScatterChart, LinesChart, GridComponent, PolarComponent, GeoComponent, TooltipComponent, LegendComponent, TitleComponent, VisualMapComponent, DatasetComponent, CanvasRenderer, ToolboxComponent, DataZoomComponent]);

provide(THEME_KEY, "westeros");

const dataForm = reactive({
currentTime: '',
enterpriseNums: [],
});


const importRef = ref();
const importHandle = () => {
importRef.value.init(`${app.api}/enterprise/details/import?token=${getToken()}`);
};

const list = ref([]);
const industryNames = ref([]);
const getIndustryInfo = async () => {
let result = await (await baseService.get("/enterprise/details/industryInfo")).data;
list.value = result;
industryNames.value = result.map((item: any) => item.name);
};
getIndustryInfo();

const updateTime = () => {
const currentDate = new Date();
const year = String(currentDate.getFullYear());
const month = String(currentDate.getMonth() + 1).padStart(2, '0');
const date = String(currentDate.getDate()).padStart(2, '0');
const hours = String(currentDate.getHours()).padStart(2, '0');
const minutes = String(currentDate.getMinutes()).padStart(2, '0');
const seconds = String(currentDate.getSeconds()).padStart(2, '0');
dataForm.currentTime = `${year}年${month}月${date}日 ${hours}:${minutes}:${seconds}`;
};
setInterval(() => {
updateTime();
}, 1000);

const mapMsg = ref([]);
const getEnterpriseNums = async () => {
let result = await (await baseService.get("/enterprise/details/enterpriseNums")).data;
dataForm.enterpriseNums = result;
mapMsg.value = result;
//console.log(mapMsg)
};
getEnterpriseNums();

echarts.registerMap('jiNan', jiNanMapJson);
const mapOption = ref({
title: {
text: '济南市各区在营企业统计',
subtext: '数据来源XXXX',
},
tooltip: {
trigger: 'item',
formatter: '{b}<br/>{c} (家)'
},
toolbox: {
show: false,
orient: 'vertical',
left: 'right',
top: 'center',
feature: {
dataView: { readOnly: false },
restore: {},
saveAsImage: {}
}
},
visualMap: {
min: 800,
max: 500000,
text: ['High', 'Low'],
realtime: false,
calculable: true,
inRange: {
color: ['lightskyblue', 'yellow', 'orangered']
}
},
series: [
{
name: '济南市各区注册企业数量',
type: 'map',
map: 'jiNan',
label: {
show: true
},
data: mapMsg,
}
]
});


const pie = ref({
title: {
text: "行业占比",
left: "center"
},
tooltip: {
trigger: "item",
formatter: "{a} <br/>{b} : {c} ({d}%)"
},
legend: {
type: 'scroll',
orient: "vertical",
right: 10,
top: 20,
bottom: 20,
left: "left",
data: industryNames
},
series: [
{
name: "行业占比",
type: "pie",
label: {
show: false
},
radius: "55%",
center: ["50%", "60%"],
data: list,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: "rgba(0, 0, 0, 0.5)"
}
}
}
]
});
</script>

<style lang="less" scoped>
.topShow {
width: 90%;
height: 5rem
/* 80/16 */
;
font-family: "得意黑";
border-radius: 3.125rem
/* 50/16 */
;
border-radius: 3.4375rem
/* 55/16 */
;
border-radius: 2.5rem
/* 40/16 */
;
background: #f0f0ff;
box-shadow: .4375rem
/* 7/16 */
.4375rem
/* 7/16 */
.875rem
/* 14/16 */
#ccccd9,
-.4375rem
/* 7/16 */
-.4375rem
/* 7/16 */
.875rem
/* 14/16 */
#ffffff;
}

@font-face {
font-family: '得意黑';
src: url('./assets/font/SmileySans-Oblique-2.ttf');
font-weight: normal;
font-style: normal;
}

.card {
border-radius: .5rem
/* 8/16 */
;
font-family: "得意黑";
font-size: large;
border-radius: 1.125rem
/* 18/16 */
;
background: #f0f0ff;
box-shadow: inset .4375rem
/* 7/16 */
.4375rem
/* 7/16 */
.875rem
/* 14/16 */
#ccccd9,
inset -.4375rem
/* 7/16 */
-.4375rem
/* 7/16 */
.875rem
/* 14/16 */
#ffffff;
}

.box {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;

figure {
display: inline-block;
// position: relative;
//margin: 2em auto;
border: .0625rem
/* 1/16 */
solid rgba(0, 0, 0, 0.1);
border-radius: .5rem
/* 8/16 */
;
box-shadow: 0 0 2.8125rem
/* 45/16 */
rgba(0, 0, 0, 0.2);
padding: 1.875rem
/* 30/16 */
;

.echarts {
width: 40vw;
min-width: 25rem
/* 400/16 */
;
height: 18.75rem
/* 300/16 */
;
}
}
}

.line_01 {
width: 100%;
height: auto;

// 左对齐
.figure_left {
float: left;
width: 27em;
border-radius: 1.125rem
/* 18/16 */
;
background: #f0f0ff;
box-shadow: .4375rem
/* 7/16 */
.4375rem
/* 7/16 */
.875rem
/* 14/16 */
#ccccd9,
-.4375rem
/* 7/16 */
-.4375rem
/* 7/16 */
.875rem
/* 14/16 */
#ffffff;
}

//右对齐
.figure_right {
float: right;
width: 50em;

.echarts {
width: auto;
height: 43.75rem
/* 700/16 */
;
}

border-radius: 1.125rem
/* 18/16 */
;
background: #f0f0ff;
box-shadow: .4375rem
/* 7/16 */
.4375rem
/* 7/16 */
.875rem
/* 14/16 */
#ccccd9,
-.4375rem
/* 7/16 */
-.4375rem
/* 7/16 */
.875rem
/* 14/16 */
#ffffff;
}

.figure_center {
//float: right;
//position: absolute;
width: 21em;
border-radius: 1.125rem
/* 18/16 */
;
background: #f0f0ff;
box-shadow: .4375rem
/* 7/16 */
.4375rem
/* 7/16 */
.875rem
/* 14/16 */
#ccccd9,
-.4375rem
/* 7/16 */
-.4375rem
/* 7/16 */
.875rem
/* 14/16 */
#ffffff;
}
}

.line_02 {
width: 100%;
height: auto;

// 左对齐
.figure_left {
float: left;
width: 50em;
border-radius: 1.125rem
/* 18/16 */
;
background: #f0f0ff;
box-shadow: .4375rem
/* 7/16 */
.4375rem
/* 7/16 */
.875rem
/* 14/16 */
#ccccd9,
-.4375rem
/* 7/16 */
-.4375rem
/* 7/16 */
.875rem
/* 14/16 */
#ffffff;
}
}</style>

最终实现效果可参考echarts的案例。