一、项目背景
自己上学校论坛接的一个前端兼职,后面发现是后端师兄自己接的项目,然后不想做前端,再在学校论坛上发布帖子招募前端师弟(that‘s me),硬件是一个装载电线杆上的设备,可以重启光猫、重启摄像头,在UI界面上可以看到全国各个省市该设备的分布及维修情况,并对设备状况做简单的展示,且能对其做重启等操作。
以下就是后端提供的原型图:
二、最终实现
- 登录界面
- 主界面
- 各个省市界面 (以重庆市为例)
- 对设备进行操作
三、项目实现以及难点总结
技术栈采用vue + datav + echarts + vrouter +vuex +vueAmap(高德插件)+ Element-UI 等实现
- 自定义组件
这块在页面上出现多次,可以抽象封装为小组件提高代码复用度,实现上如下:
<template>
<div style="display: flex; justify-content: space-evenly">
<p v-for="number in nums" :key="number.id" class="showNumber">
{{ number.value }}
</p>
</div>
</template>
<script>
export default {
name: "SixNumber",
props: {
numbers: {
type: Number,
},
},
mounted() {
this.nums = convert2Six(this.numbers);
},
data() {
return {
nums: [],
};
},
method: {
convert2Six(number) {
//将一个数字转换为6位的数字
const arr = [];
let count = 0;
while (number) {
arr.push(number % 10);
number = parseInt(number / 10);
count++;
}
//不足6位则补0到6位
while (count < 6) {
arr.push(0);
count++;
}
//现在arr里面是逆序的
return arr.reverse();
},
},
};
</script>
<style lang="scss" scoped>
.showNumber {
@media screen and (max-width: 1440px) and (min-width: 1280px) {
width: 20px;
height: 40px;
font-size: 25px;
padding-top: 12px;
margin-top: 10px;
}
width: 30px;
height: 100px;
color: white;
background: rgb(15, 224, 250, 0.8);
padding-left: 3px;
padding-top: 40px;
margin-top: 20px;
margin-bottom: 30px;
font-size: 45px;
}
</style>
在外部引入该组件并使用,先定义请求后端的方法,mounted时候请求,将实际的站址数、工单数放入data里定义的相应变量,再传入子组件的props
...
<six-number :number = downCount/>
...
<script>
import SixNumber from "./SixNumber.vue"
export default {
data() {
return{
downCount: 0;
}
},
components: {
SixNumber
},
mounted() {
//向后端请求数据,放进downCount里,再作为props传入子组件
this.getData();
}
}
</script>
- 地图下钻以及数据联动
这里请求的高德地图的AMapUI.loadUI来获取地图json数据,再调用后端的数据接口获得对应区域的设备数量,在echarts里进行填充
遇到显示上一些问题,例如海南三沙群岛太琐碎,就暂时将其屏蔽了。且只有几个省市有数据,则只有那几个省市能触发点击事件
<template>
<div class="echarts">
<div style="width:100;height:100%" ref="sctterMap"></div>
<div class="mapChoose">
<span v-for="(item,index) in parentInfo" :key="item.cityName">
<span
class="title"
@click="chooseArea(item,index)"
>{{item.cityName=='全国'?'中国':item.cityName}}</span>
<span class="icon" v-show="index+1!=parentInfo.length">></span>
</span>
</div>
</div>
</template>
<script>
import echarts from "echarts";
import resize from "./mixins/resize";
import "echarts/map/js/china";
export default {
name: "sctterMap",
mixins: [resize],
data() {
return {
myCharts: null,
geoJson: {
features: []
},
parentInfo: [
{
cityName: "全国",
code: 100000
}
],
curMapData: []
};
},
//监听vuex里面内容需要通过计算属性套一层
computed: {
cityNameComputed() {
return this.$store.state.city;
}
},
watch: {
cityNameComputed: function(newCity, oldCity) {
const cityMap = new Map([
["全国", 100000],
["广东省", 440000],
["重庆市", 500000],
["海南省", 460000],
["河北省", 130000],
["四川省", 510000],
["山东省", 370000]
]);
let adCode = cityMap.get(newCity);
this.getGeoJson(adCode);
if (
adCode !== 100000 &&
this.parentInfo[this.parentInfo.length - 1].code !== adCode
) {
this.parentInfo.push({
cityName: newCity,
code: adCode
});
}
}
},
mounted() {
this.getGeoJson(100000);
},
methods: {
getGeoJson(adcode) {
//填充geoJson
const adCodeSet = new Set([100000, 440000, 460000, 500000, 510000,130000,370000]);
if (!adCodeSet.has(adcode)) {
return;
}
let that = this;
AMapUI.loadUI(["geo/DistrictExplorer"], DistrictExplorer => {
var districtExplorer = new DistrictExplorer();
districtExplorer.loadAreaNode(adcode, function(error, areaNode) {
if (error) {
console.error(error);
return;
}
let Json = areaNode
.getSubFeatures()
.filter(it => it.properties.name !== "三沙市");
if (Json.length > 0) {
that.geoJson.features = Json;
} else if (Json.length === 0) {
that.geoJson.features = that.geoJson.features.filter(
item => item.properties.adcode == adcode
);
if (that.geoJson.features.length === 0) return;
}
that.getMapData();
});
});
},
getParams() {
const cityMap = new Map([
["全国"],
["广东省", 440000],
["重庆市", 500000],
["海南省", 460000],
["河北省", 130000],
["四川省", 510000],
["山东省", 370000]
]);
let adCode = cityMap.get(this.cityNameComputed);
let params = {
adCode: adCode
};
return params;
},
//获取数据
getMapData() {
let params = this.getParams();
this.$axios.findMonitor(params).then(res => {
this.curMapData = [];
if (res.code === 200 && res.message === "success") {
this.curMapData = res.data.deviceMonitorVoList;
let mapData = this.geoJson.features.map(item => {
let value = 0;
let deviceCountObj = this.curMapData.filter(
it => it.adCode == item.properties.adcode
);
if (deviceCountObj.length) {
value = deviceCountObj[0].deviceCount;
}
return {
name: item.properties.name,
value: value,
cityCode: item.properties.adcode
};
});
mapData = mapData.sort(function(a, b) {
return b.value - a.value;
});
this.initEcharts(mapData);
}
});
},
initEcharts(mapData) {
var min = mapData[mapData.length - 1].value;
var max = mapData[0].value;
if (mapData.length === 1) {
min = 0;
}
this.myChart = echarts.init(this.$refs.sctterMap);
echarts.registerMap("Map", this.geoJson); //注册
let myOption = {
tooltip: {
trigger: "item",
formatter: "设备数量<br />{b}: {c}"
},
title: {
show: false,
left: "center",
top: "15",
text: this.parentInfo[this.parentInfo.length - 1].cityName,
textStyle: {
color: "rgb(179, 239, 255)",
fontSize: 16
}
},
toolbox: {
feature: {
restore: {
show: false
},
dataView: {
optionToContent: function(opt) {
let series = opt.series[0].data; //折线图数据
let tdHeads =
'<th style="padding: 0 20px">所在地区</th><th style="padding: 0 20px">IOT设备数量</th>'; //表头
let tdBodys = ""; //数据
let table = `<table border="1" style="margin-left:20px;border-collapse:collapse;font-size:14px;text-align:left;"><tbody><tr>${tdHeads} </tr>`;
for (let i = 0; i < series.length; i++) {
table += `<tr>
<td style="padding: 0 50px">${series[i].name}</td>
<td style="padding: 0 50px">${series[
i
].value.toFixed(0)}个</td>
</tr>`;
}
table += "</tbody></table>";
return table;
}
},
saveAsImage: {
name:
this.parentInfo[this.parentInfo.length - 1].cityName + "地图"
},
dataZoom: {
show: false
},
magicType: {
show: false
}
},
iconStyle: {
normal: {
borderColor: "#1990DA"
}
},
top: 15,
right: 35
},
visualMap: {
min: min,
max: max,
right: "3%",
bottom: "25%",
calculable: true,
seriesIndex: [0],
inRange: {
color: ["#26cfe4","#26cfe4", "#ec5845"]
},
textStyle: {
color: "#fff"
}
},
series: [
{
name: "地图",
type: "map",
map: "Map",
roam: true, //是否可缩放
zoom: 1.0, //缩放比例
data: mapData,
label: {
normal: {
show: true,
textStyle: { color: "#c71585" }, //省份标签字体颜色
color: "rgb(249, 249, 249)", //省份标签字体颜色
formatter: p => {
const arr = [
"新疆维吾尔自治区",
"西藏自治区",
"青海省",
"甘肃省",
"云南省",
"贵州省",
"湖南省",
"广西壮族自治区",
"山西省",
"安徽省",
"天津市",
"香港特别行政区",
"澳门特别行政区",
"宁夏回族自治区",
"内蒙古自治区",
"黑龙江省",
"台湾省",
"福建省",
"江西省",
"陕西省",
"湖北省",
"河南省",
"江苏省",
"上海市",
"浙江省",
"北京市",
"辽宁省",
"吉林省"
];
const set = new Set(arr);
if (set.has(p.name)) {
return "";
}
const filterMap = new Map([
["酉阳土家族苗族自治县", "酉阳县"],
["彭水苗族土家族自治县", "彭水县"],
["石柱土家族自治县", "石柱县"],
["秀山土家族苗族自治县", "秀山县"],
["渝中区", ""],
["沙坪坝区", ""],
["九龙坡区", ""],
["大渡口区", ""],
["南岸区", ""],
["渝北区", ""],
["江北区", ""],
["昌江黎族自治县", "昌江县"],
["保亭黎族苗族自治县", "保亭县"],
["陵水黎族自治县", "陵水县"],
["琼中黎族苗族自治县", "琼中县"],
["白沙黎族自治县", "白沙县"],
["乐东黎族自治县", "乐东县"]
]);
if (filterMap.has(p.name)) {
return filterMap.get(p.name);
}
return p.name;
}
},
emphasis: {
show: true,
textStyle: { color: "#800080" },
color: "#f75a00"
}
},
itemStyle: {
normal: {
borderWidth: 0.5, //区域边框宽度
borderColor: "#009fe8", //区域边框颜色
areaColor: "#ffffff" //区域颜色
},
emphasis: {
borderWidth: 0.5,
borderColor: "#4b0082",
areaColor: "#ffdead"
}
}
}
]
};
this.myChart.setOption(myOption, true);
let that = this;
this.myChart.off("click");
this.myChart.on("click", params => {
if (
that.parentInfo[that.parentInfo.length - 1].code ==
params.data.cityCode
) {
return;
}
let data = params.data;
const adCodeSet = new Set([100000, 440000, 460000, 500000, 510000,130000,370000]);
if (!adCodeSet.has(data.cityCode)) {
return;
}
that.parentInfo.push({
cityName: data.name,
code: data.cityCode
});
let newCity = data.name;
this.$store.commit("changeCity", newCity);
that.getGeoJson(data.cityCode);
});
},
//选择切换市县
chooseArea(val, index) {
if (this.parentInfo.length === index + 1) {
return;
}
this.parentInfo.splice(index + 1);
let newCity = this.parentInfo[this.parentInfo.length - 1].cityName;
this.$store.commit("changeCity", newCity);
this.getGeoJson(this.parentInfo[this.parentInfo.length - 1].code);
}
}
};
</script>
<style lang="scss" scoped>
.echarts {
width: 100%;
height: 82%;
position: relative;
}
.mapChoose {
position: absolute;
left: 20px;
top: 5px;
color: #eee;
font-size: 18px;
.title {
padding: 5px;
border-top: 1px solid rgba(147, 235, 248, 0.8);
border-bottom: 1px solid rgba(147, 235, 248, 0.8);
cursor: pointer;
}
.icon {
font-family: "simsun";
font-size: 25px;
margin: 0 11px;
}
}
</style>
数据联动解决: 这里将当前所在区域代码存入了vuex,当点击地图上省份时,或点击回到全国,都先更改vuex里的区域代码,然后在所有组件监听区域代码,发生了变化则重新请求后端数据更新到页面上
- 设备数据实时更新到页面
采用的是轮询解决,更优雅的做法应该是通过websocket
- 表格滚动 以及 表格点击事件
表格滚动组件采用的是DataV的轮播表,由于全国和省市的表格项有不一样的地方,且点击事件不同,为此在最外层需引入这两个轮播表组件,通过v-if判断当前区域来进行显示
<div class="jc-center body-box deviceTable" v-if="showCountry">
<dv-scroll-board @click="showProvince" :config="config_country" ref="scrollBoard1" />
</div>
<div class="jc-center body-box deviceTable" v-if="!showCountry">
<dv-scroll-board @click="showDetail" :config="config_province" ref="scrollBoard2" />
</div>
methods: {
showProvince(ri) {
this.$store.commit("changeCity", ri.row[1]);
},
showDetail(ri) {
if (ri.ceil === "设备详情") {
this.deviceDlg = true;
this.districtAdCode = ri.row[0];
console.log(ri.row[1] + "设备详情");
}
if (ri.ceil === "告警详情") {
this.alarmDlg = true;
this.districtAdCode = ri.row[0];
console.log(ri.row[1] + "告警详情");
}
}
}
- 多分辨率适配
老板的笔记本分辨率是720p的,为此采用css媒体查询进行了一套720p分辨率下的适配