前端工程的地址:UserManagerFront: 数据可视化前端 (gitee.com)
效果展示,可以展现出来了,样式可能还有一些丑。
后端代码
后端主要是拿到数据并对数据进行处理,按照前端需要的格式进行返回即可。import com.njitzx.entity.Student;
import com.njitzx.entity.vo.*;
import com.njitzx.mapper.StudentMapper;
import com.njitzx.mapper.TeacherMapper;
import com.njitzx.serivce.StudentService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import java.util.LinkedHashMap;
@Service
@RequiredArgsConstructor
public class StudentServiceImpl implements StudentService {
private final StudentMapper studentMapper;
private final TeacherMapper teacherMapper;
@Override
public CollegeVO getCollege() {
List<Student> students = studentMapper.getAll();
// 分组并计算每个学院的学生数量
Map<String, Long> collect = students.stream() //收集成map集合
.collect(Collectors.groupingBy(Student::getCollege, Collectors.counting()));
// 根据值进行排序(降序),然后将结果收集到一个新的LinkedHashMap中
Map<String, Long> sortedCollect = collect.entrySet().stream()
.sorted(Entry.<String, Long>comparingByValue().reversed())
.collect(Collectors.toMap(
Entry::getKey,
Entry::getValue,
(e1, e2) -> e1,
LinkedHashMap::new
));
// 将排序后的键和值拼接成字符串
String college = String.join(",", sortedCollect.keySet());
String countList = sortedCollect.values().stream()
.map(String::valueOf)
.collect(Collectors.joining(","));
return CollegeVO.builder().nameList(college).numberList(countList).build();
}
@Override
public List<GenderVO> getSex() {
List<Student> students = studentMapper.getAll();
Map<String, Long> collect = students.stream()
.collect(Collectors.groupingBy(Student::getGender, Collectors.counting()));
List<GenderVO> genderVOList = collect.entrySet().stream()
.map(entry -> {
GenderVO vo = new GenderVO();
vo.setName(entry.getKey());
vo.setValue(entry.getValue());
return vo;
})
.collect(Collectors.toList());
return genderVOList;
}
@Override
public List<ProvinceVO> getProvince() {
List<Student> students = studentMapper.getAll();
Map<String, Long> collect = students.stream()
.collect(Collectors.groupingBy(Student::getProvinces, Collectors.counting()));
List<ProvinceVO> genderVOList = collect.entrySet().stream()
.map(entry -> {
ProvinceVO vo = new ProvinceVO();
vo.setName(entry.getKey());
vo.setValue(entry.getValue());
return vo;
})
.collect(Collectors.toList());
return genderVOList;
}
@Override
public NumberVO getnumbwer() {
Long l = studentMapper.selectCount();
Long r = teacherMapper.selectCount();
return NumberVO.builder().numberStudent(l).numberTeacher(r).build();
}
@Override
public HobbyVO getHobby() {
List<Student> students = studentMapper.getAll();
// 分组并计算每个学院的学生数量
Map<String, Long> collect = students.stream()
.collect(Collectors.groupingBy(Student::getHobby, Collectors.counting()));
// 将排序后的键和值拼接成字符串
String name = String.join(",", collect.keySet());
String count = collect.values().stream()
.map(String::valueOf)
.collect(Collectors.joining(","));
return HobbyVO.builder().name(name).count(count).build();
}
}
前端
主要是学习前端如何编写的。vue3编写echarts代码
1.创建一个盒子<div class="panel bar" ref="chart1"> //通过ref 拿到这个盒子
2 导入echarts import * as echarts from 'echarts';
3const chart1 = ref(null); 创建dom对象
4.创建option配置
5 初始化 let instance = echarts.init(chart1.value);
6挂载配置到instance上面 instance.setOption(chartOptions);
也可以通过document来获取对象,进行初始化和挂载。
let initMap = echarts.init(document.querySelector('#hobbyRef'))
initMap.setOption(option)
柱状图的配置
```javascript const chartOptions = { //设置距离边框的样式 grid: { left: '0%', right: '0%', top: "20%", bottom: '4%', containLabel: true }, // 设置标题 title: { // 标题 text: '学院人数排名前五', //居中位置 left: 'center', //设置标题的样式 textStyle: { color: '#2f89cf', // 设置标题颜色为红色 fontSize: 18, // 设置字体大小 fontWeight: 'bold' // 设置字体粗细 } }, color: ['#2f89cf'], tooltip: { trigger: 'axis', //坐标轴点上去触发 axisPointer: {type: 'shadow'} }, xAxis: { type: 'category', data: collegeNameList.value, axisTick: { alignWithLabel: true }, //修改 axisLabel: { color: "rgba(255,255,255,.6)", fontSize: 12, // 调大字体大小 interval: 0, // 强制显示所有标签 rotate: 30, // 旋转标签以避免重叠 formatter: function (value) { // 如果名称超过10个字符,显示省略号 return value.length > 10 ? value.slice(0, 10) + '...' : value; } }, axisLine: { show: false, // 如果想要设置单独的线条样式 lineStyle: { color: "rgba(255,255,255,.1)", width: 1, type: "solid" } } }, yAxis: { type: 'value', axisLabel: { color: "rgba(255,255,255,.6)", fontSize: "12" }, // y轴线条样式 axisLine: { lineStyle: { color: "rgba(255,255,255,.1)", // width: 1, // type: "solid" } }, // y 轴分隔线样式 splitLine: { lineStyle: { color: "rgba(255,255,255,.1)" } } }, //配置数据的 series: [{ name: '学生数量', type: 'bar', barWidth: '35%', data: collegeNumberList.value, itemStyle: { barBorderRadius: 5 } }] }; ```
<h4 id="hA9LP">中国地图</h4>
``javascript
import china from '@/json/china.json' //导入地图的json数据
const mockData = ref([])
//从后端拿到地图的数据 [{'nane':'北京市','value':500}]
const getmokcData = async () => {
const res = await getStudentProvince();
mockData.value = res.data.data
await nextTick(() => {
getEcharts3();
});
}
let initMap = echarts.init(document.querySelector('#mapDom')); //初始化
//注册中国地图
echarts.registerMap('china', china);
//拿到前五的数据
let topFiveData = mockData.value.sort((a, b) => b.value - a.value).slice(0, 5);
// 将筛选后的数据转换为 ECharts 需要的格式
let data = topFiveData.map(i => {
let cityPosition = getCityPositionByName(i.name);
return {
name: i.name,
value: cityPosition ? [...cityPosition.value.map(Number), i.value] : [0, 0, i.value]
};
});
// 创建一个方便查找的字典对象
let mockDataMap = mockData.value.reduce((acc, item) => {
acc[item.name] = item.value;
return acc;
}, {});
let options = {
title: {
text: '学生家乡分布',
left: 'center',
textStyle: {
color: '#fa4c27',
fontSize: 18,
fontWeight: 'bold'
}
},
tooltip: {
trigger: 'item',
formatter: (params) => {
// 从 mockDataMap 中获取对应省份的数据值
let count = mockDataMap[params.name] || 0; // 确保显示为0而不是NaN
return `${params.name}<br/>${count} (学生总数)`;
}
},
//又下角的工具
toolbox: {
show: true,
orient: 'vertical',
left: 'right',
top: 'center',
feature: {
dataView: {readOnly: false, title: '数据视图'},
restore: {title: '还原'},
saveAsImage: {title: '保存为图片', pixelRatio: 2}
}
},
visualMap: {
min: 0,
max: Math.max(...topFiveData.map(d => d.value)),
text: ['High', 'Low'],
realtime: false,
calculable: true,
inRange: {
color: ['#e0f7fa', '#80deea', '#0288d1']
}
},
//地图坐标
geo: {
map: 'china',
roam: false,
// zoom: 1, // 调整这个值来放大地图
label: {
show: false
},
emphasis: {
label: {
show: false
}
}
},
series: [
{
name: '中国',
type: 'map',
map: 'china',
label: {
show: false
},
data: mockData.value,
},
{
type: 'scatter',
coordinateSystem: 'geo',
symbol: 'pin',
symbolSize: [50, 50],
label: {
show: true,
color: '#fff',
formatter(value) {
return value.name + value.data.value[2]; // 显示人数
},
fontSize: 10
},
itemStyle: {
color: '#e30707' // 标记颜色
},
data: data,
}
]
};
initMap.setOption(options);
背景样式
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
li {
list-style: none;
}
@font-face {
font-family: electronicFont;
src: url("@/assets/font/DS-DIGIT.TTF");
}
.header {
position: relative;
height: 1.25rem;
background: url('@/assets/images/head_bg.png') no-repeat top center;
background-color: #0b1341;
background-size: 100% 100%;
h1 {
font-size: 0.475rem;
color: #fff;
text-align: center;
line-height: 1rem;
}
.showTime {
position: absolute;
top: 0;
right: 0.375rem;
line-height: 0.9375rem;
font-size: 0.25rem;
color: rgba(255, 255, 255, 0.7);
}
}
.mainbox {
font-family: Arial, Helvetica, sans-serif;
margin: 0;
padding: 0;
/* 背景图定位 / 背景图尺寸 cover 完全铺满容器 contain 完整显示在容器内 */
background: url('@/assets/images/bg.jpg') no-repeat #000;
background-size: cover;
/* 行高是字体1.15倍 */
line-height: 1.15;
}
.mainbox {
min-width: 1024px;
max-width: 1920px;
padding: 0.125rem 0.125rem 0;
display: flex;
.column {
flex: 3;
&:nth-child(2) {
flex: 5;
margin: 0 0.125rem 0.1875rem;
overflow: hidden;
}
}
}
.panel {
position: relative;
height: 3.875rem;
border: 1px solid rgba(25, 186, 139, 0.17);
background: rgba(255, 255, 255, 0.04) url('@/assets/images/line.png');
padding: 0 0.1875rem 0.5rem;
margin-bottom: 0.1875rem;
&::before {
position: absolute;
top: 0;
left: 0;
content: "";
width: 10px;
height: 10px;
border-top: 2px solid #02a6b5;
border-left: 2px solid #02a6b5;
}
&::after {
position: absolute;
top: 0;
right: 0;
content: "";
width: 10px;
height: 10px;
border-top: 2px solid #02a6b5;
border-right: 2px solid #02a6b5;
}
.panel-footer {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
//z-index: 0;
&::before {
position: absolute;
bottom: 0;
left: 0;
content: "";
width: 10px;
height: 10px;
border-bottom: 2px solid #02a6b5;
border-left: 2px solid #02a6b5;
}
&::after {
position: absolute;
bottom: 0;
right: 0;
content: "";
width: 10px;
height: 10px;
border-bottom: 2px solid #02a6b5;
border-right: 2px solid #02a6b5;
}
}
//.panel-footer::before, .panel-footer::after {
// z-index: 0; /* 确保伪元素在 panel-footer 下层 */
//}
h2 {
//z-index: 1; /* 确保 panel 在最上层 */
height: 0.6rem;
line-height: 0.6rem;
text-align: center;
color: #fff;
font-size: 0.25rem;
font-weight: 400;
a {
margin: 0 0.1875rem;
color: #fff;
text-decoration: underline;
}
}
.chart {
height: 3rem;
}
}
.no {
background: rgba(101, 132, 226, 0.1);
padding: 0.1875rem;
.no-hd {
position: relative;
border: 1px solid rgba(25, 186, 139, 0.17);
&::before {
content: "";
position: absolute;
width: 30px;
height: 10px;
border-top: 2px solid #02a6b5;
border-left: 2px solid #02a6b5;
top: 0;
left: 0;
}
&::after {
content: "";
position: absolute;
width: 30px;
height: 10px;
border-bottom: 2px solid #02a6b5;
border-right: 2px solid #02a6b5;
right: 0;
bottom: 0;
}
ul {
display: flex;
li {
position: relative;
flex: 1;
text-align: center;
height: 1rem;
line-height: 1rem;
font-size: 0.875rem;
color: #ffeb7b;
padding: 0.05rem 0;
font-family: electronicFont;
font-weight: bold;
&:first-child::after {
content: "";
position: absolute;
height: 50%;
width: 1px;
background: rgba(255, 255, 255, 0.2);
right: 0;
top: 25%;
}
}
}
}
.no-bd ul {
display: flex;
li {
flex: 1;
height: 0.5rem;
line-height: 0.5rem;
text-align: center;
font-size: 0.225rem;
color: rgba(255, 255, 255, 0.7);
padding-top: 0.125rem;
}
}
}
.map {
position: relative;
height: 10.125rem;
.chart {
position: absolute;
top: 0;
left: 0;
z-index: 5;
height: 10.125rem;
width: 100%;
}
.map1,
.map2,
.map3 {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 6.475rem;
height: 6.475rem;
background: url('@/assets/images/map.png') no-repeat;
background-size: 100% 100%;
opacity: 0.3;
}
.map2 {
width: 8.0375rem;
height: 8.0375rem;
background-image: url('@/assets/images/lbx.png');
opacity: 0.6;
animation: rotate 15s linear infinite;
z-index: 2;
}
.map3 {
width: 7.075rem;
height: 7.075rem;
background-image: url('@/assets/images/jt.png');
animation: rotate1 10s linear infinite;
}
@keyframes rotate {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes rotate1 {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(-360deg);
}
}
}
@media screen and (max-width: 1024px) {
html {
font-size: 42px !important;
}
}
@media screen and (min-width: 1920px) {
html {
font-size: 80px !important;
}
}
template
整体结构是 flex布局 3 5 3 布局样式。
<template>
<div class="header">
<h1>工程数据分析</h1>
<div class="showTime"></div>
</div>
<!-- 页面主体 一个大的盒子 划分 -->
<div class="mainbox">
<div class="column">
<div class="panel bar" ref="chart1">
<div class="chart"></div>
<!-- <h2 style="color: red">学院人数排名前五</h2>-->
<div class="panel-footer"></div>
</div>
<div class="panel line" ref="chart2">
<div class="chart"></div>
<h2>标题</h2>
<div class="panel-footer"></div>
</div>
<div class="panel pie">
<h2></h2>
<div class="panel-footer"></div>
</div>
</div>
<div class="column">
<div class="no">
<div class="no-hd">
<ul>
<li>{{ stNumber.numberTeacher }}</li>
<li>{{ stNumber.numberStudent }}</li>
</ul>
</div>
<div class="no-bd">
<ul>
<li>学生人数</li>
<li>老师人数</li>
</ul>
</div>
</div>
<!-- 地图模块 -->
<div class="map">
<!-- 放到map中间 -->
<div class="chart" id="mapDom"></div>
<div class="map1"></div>
<div class="map2"></div>
<div class="map3"></div>
</div>
</div>
<div class="column">
<div class="panel bar" >
<h2></h2>
<div class="chart" id="hobbyRef"></div>
<div class="panel-footer"></div>
</div>
<div class="panel line">
<h2></h2>
<div class="panel-footer"></div>
</div>
<div class="panel pie">
<h2></h2>
<div class="panel-footer"></div>
</div>
</div>
</div>
</template>