目录
- 前言
- 判断浏览器是否支持websocket的方法
- Vue项目里使用websocket的实例
- 总结
前言
由于项目需求有要使用长链接,我们普通的http请求如果用轮询的方式与服务端通讯就很消耗资源。我们一起来学习一下在vue项目里如何使用websocket,本文纯属个人观点,如果有不正确的地方请大家批评指正,技术无高低,谦虚学习的心态我认为很重要,天外有天人外有人。
判断浏览器是否支持websocket的方法
比较直观的方式是直接判断全局对象中是否包含WebSocket对象即可:
if( typeof(WebSocket) != "function" ) {
alert("您的浏览器不支持Websocket通信协议,请更换浏览器为Chrome或者Firefox再次使用!")
}
但是这种方式不严谨,在 Android 中,即使浏览器不支持 WebSocket ,但是它还是存在这个属性。所以可以使用下面的方法:
if (typeof WebSocket != 'undefined') {
console.log("您的浏览器支持Websocket通信协议")
}else{
alert("您的浏览器不支持Websocket通信协议,请使用Chrome或者Firefox浏览器!")
}
或者是这种方法:
if (!!window.WebSocket && window.WebSocket.prototype.send) {
console.log("您的浏览器支持Websocket通信协议")
}else{
alert("您的浏览器不支持Websocket通信协议,请使用Chrome或者Firefox浏览器!")
}
Vue项目里使用websocket的实例
上面这个页面是我所做项目里的某个页面,由于硬件资源监测和网络性能监测两个板块需要传递给服务端不同的参数来获取数据,传参分别如下:
{ message: "sys_info" }
{ message: "net_info" }
我们需要建立2个不同的长链接。我们项目都是统一在public文件夹下的config.js里统一配置,不管是http请求还是websocket,代码如下:
然后在一个js文件里将其封装成一个函数并暴露出去,代码如下:
然后在我们要使用到的组件里引入,并在data里定义两个websock实例,然后再在mounted里初始化两个websock实例,最后在destroyed销毁页面时一并销毁两个websock实例,代码如下:
然后我们在methods里看看初始化websocket的两个方法怎么写的,代码如下:
这里为了考虑各大浏览器是否兼容websocket,所以加了一个判断语句:
if (typeof WebSocket === "undefined")
return console.log("您的浏览器不支持websocket");
最后附上完整代码,如有不正确之处望同行前辈批评指正:
<template>
<div class="homePage">
<div class="topArea">
<el-card class="el-card">
<div slot="header" class="clearfix">
<div class="headerBox">
<span class="arrow"
><img src="../../assets/homePage/arrow.png"
/></span>
<span class="title">硬件资源监测</span>
<span class="topRight"
><img src="../../assets/homePage/topRight.png"
/></span>
</div>
</div>
<div class="myCont">
<div class="itemBox">
<div class="titleBox">
<span>
<img src="../../assets/homePage/cpuTitle.png" alt="" />
</span>
<span class="title">CPU资源监测</span>
</div>
<div class="board">
<div class="left" id="cpuCharts"></div>
<div class="right">
<div class="tipBox">
<span>CPU利用率</span>
<span>{{ cpu_info.cpu_use }}%</span>
</div>
<div class="detailBox">
<div>
<span></span>
<span>CPU颗数</span>
<span>{{ cpu_info.cpu_physical_count }}</span>
</div>
<div>
<span></span>
<span>CPU核数</span>
<span>{{ cpu_info.cpu_kernel_count }}</span>
</div>
<div>
<span></span>
<span>CPU负载</span>
<span>{{ cpu_info.cpu_average }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="itemBox">
<div class="titleBox">
<span>
<img src="../../assets/homePage/Memory.png" alt="" />
</span>
<span class="title">内存资源监测</span>
</div>
<div class="board">
<div class="left" id="memoryCharts"></div>
<div class="right">
<div class="tipBox">
<span>内存利用率</span>
<span>{{ mem_info.memory_percent }}%</span>
</div>
<div class="detailBox">
<div>
<span style="background: none"></span>
<span></span>
<span></span>
</div>
<div>
<span style="background: none"></span>
<span></span>
<span></span>
</div>
<div>
<span></span>
<span>内存总量</span>
<span>{{
mem_info.memory_total | FilterBps(mem_info.memory_total)
}}</span>
</div>
</div>
</div>
</div>
</div>
<div class="itemBox">
<div class="titleBox">
<span>
<img src="../../assets/homePage/Hard.png" alt="" />
</span>
<span class="title">硬盘资源监测</span>
</div>
<div class="board">
<div class="left" id="hardCharts"></div>
<div class="right">
<div class="otherBox">
<div class="tipBox2">
<span>硬盘读速率</span>
<span>{{
disk_info.read_speed | FilterSpeed(disk_info.read_speed)
}}</span>
<span>MB/s</span>
</div>
<div class="tipBox2">
<span>硬盘写速率</span>
<span>{{
disk_info.write_speed | FilterSpeed(disk_info.write_speed)
}}</span>
<span>MB/s</span>
</div>
</div>
<div class="detailBox">
<div>
<span style="background: none"></span>
<span></span>
<span></span>
</div>
<div>
<span style="background: none"></span>
<span></span>
<span></span>
</div>
<div>
<span></span>
<span>硬盘大小</span>
<span
>{{
disk_info.disk_total
| FilterDiskTotal(disk_info.disk_total)
}}T</span
>
</div>
</div>
</div>
</div>
</div>
</div>
</el-card>
</div>
<div class="topArea bottomArea">
<el-card class="el-card">
<div slot="header" class="clearfix">
<div class="headerBox">
<span class="arrow"
><img src="../../assets/homePage/arrow.png"
/></span>
<span class="title">网络性能监测</span>
<span class="topRight"
><img src="../../assets/homePage/topRight.png"
/></span>
</div>
</div>
<div class="myCont">
<div class="Throughput">
<div class="titleBox">
<span
><img src="../../assets/homePage/Throughput.png" alt=""
/></span>
<span class="title">通道吞吐量</span>
</div>
<div class="lineBox" id="lineCharts"></div>
<div class="lineTips">
<span
><img src="../../assets/homePage/lineTips.png" alt=""
/></span>
<span>吞吐量</span>
<span>{{ inOutWay }}</span>
<span>{{ inoutUnit }}</span>
</div>
</div>
<div class="rightArea">
<div class="Item" v-for="(item, i) in card_info" :key="i">
<div class="topBox">
<div class="imgBox">
<img src="../../assets/homePage/Frame.png" alt="" />
</div>
<div class="portTip">{{ Object.keys(item)[0] }}</div>
<div class="transferBox transferBoxT">
<span>
<img src="../../assets/homePage/transferTop.png" alt="" />
</span>
<span class="Num">{{ WayMethods(item, "incoming") }}</span>
</div>
<div class="transferBox transferBoxB">
<span>
<img
src="../../assets/homePage/transferBottom.png"
alt=""
/>
</span>
<span class="Num">{{ WayMethods(item, "outgoing") }}</span>
</div>
</div>
<div class="bottomBox">
<div>
<span>工作速率:</span>
<span>{{ WayMethods(item, "工作速率") }} </span>
</div>
<div>
<span>双工模式:</span>
<span>{{ WayMethods(item, "双工模式") }}</span>
</div>
<div>
<span>自协商:</span>
<span>{{ WayMethods(item, "自协商") }}</span>
</div>
<div>
<span>接口类型:</span>
<span>{{ WayMethods(item, "接口类型") }}</span>
</div>
<div>
<span>链路状态:</span>
<span>{{ WayMethods(item, "链路状态") }}</span>
</div>
</div>
</div>
</div>
</div>
</el-card>
</div>
</div>
</template>
<script>
import { homeWsUrl } from "@/api/websocket.js";
export default {
data() {
return {
wsUrl: homeWsUrl(),
websock: null, //ws实例
websockNet: null, //ws实例
cpu_info: {
cpu_use: 0,
},
mem_info: {
memory_percent: 0,
},
disk_info: {
disk_percent: 0,
},
inoutArr: [], //吞吐量集合
card_info: [],
in_out_total: 0, //吞吐量
inoutUnit: "",
};
},
filters: {
FilterBps(bps) {
if (bps) {
let Grate = 1024 * 1024 * 1024;
return (Number(bps) / Grate).toFixed(2) + "G";
}
},
FilterSpeed(bps) {
if (bps) {
let Grate = 1024 * 1024;
return (Number(bps) / Grate).toFixed(2);
} else {
return 0;
}
},
FilterDiskTotal(bps) {
if (bps) {
let Grate = 1024 * 1024 * 1024 * 1024;
return (Number(bps) / Grate).toFixed(2);
}
},
},
mounted() {
this.cpuCharts();
this.memoryCharts();
this.hardCharts();
this.lineCharts();
//初始化websocket,此页面建立了2个长链接
this.initWebSocket();
this.initWebSocketNet();
},
destroyed() {
//离开路由之后断开websocket连接
this.websock.close();
this.websockNet.close();
},
computed: {
inOutWay() {
// console.log("inOutWay", this.in_out_total);
if (this.in_out_total <= 1000) {
return (
(this.inoutUnit = "bit/s"),
(this.in_out_total = Number(this.in_out_total))
);
} else if (this.in_out_total > 1000 && this.in_out_total <= 1000 * 1000) {
return (
(this.inoutUnit = "Kb/s"),
(this.in_out_total = (Number(this.in_out_total) / 1000).toFixed(2))
);
} else if (
this.in_out_total > 1000 * 1000 &&
this.in_out_total <= 1000 * 1000 * 1000
) {
return (
(this.inoutUnit = "Mb/s"),
(this.in_out_total = (
Number(this.in_out_total) /
(1000 * 1000)
).toFixed(2))
);
} else if (this.in_out_total > 1000 * 1000 * 1000) {
return (
(this.inoutUnit = "Gb/s"),
(this.in_out_total = (
Number(this.in_out_total) /
(1000 * 1000 * 1000)
).toFixed(2))
);
}
},
},
methods: {
cpuCharts() {
let chartDom = document.getElementById("cpuCharts");
let myChart = this.$echarts.init(chartDom);
let option = {
// tooltip: {
// formatter: "{a} <br/>{b} : {c}%",
// },
series: [
{
name: "Pressure",
title: {
show: false,
},
type: "gauge",
progress: {
show: true,
},
radius: "100%",
detail: {
valueAnimation: true,
formatter: "{value}%",
fontSize: 14,
},
data: [
{
value: this.cpu_info.cpu_use,
name: "SCORE",
},
],
},
],
};
option && myChart.setOption(option);
},
memoryCharts() {
let chartDom = document.getElementById("memoryCharts");
let myChart = this.$echarts.init(chartDom);
let option = {
// tooltip: {
// formatter: "{a} <br/>{b} : {c}%",
// },
series: [
{
name: "Pressure",
title: {
show: false,
},
type: "gauge",
progress: {
show: true,
},
radius: "100%",
detail: {
valueAnimation: true,
formatter: "{value}%",
fontSize: 14,
},
data: [
{
value: this.mem_info.memory_percent,
name: "SCORE",
},
],
},
],
};
option && myChart.setOption(option);
},
hardCharts() {
let chartDom = document.getElementById("hardCharts");
let myChart = this.$echarts.init(chartDom);
let option = {
// tooltip: {
// formatter: "{a} <br/>{b} : {c}%",
// },
series: [
{
name: "Pressure",
title: {
show: false,
},
type: "gauge",
progress: {
show: true,
},
radius: "100%",
detail: {
valueAnimation: true,
formatter: "{value}%",
fontSize: 14,
},
data: [
{
value: this.disk_info.disk_percent,
name: "SCORE",
},
],
},
],
};
option && myChart.setOption(option);
},
lineCharts() {
let chartDom = document.getElementById("lineCharts");
let myChart = this.$echarts.init(chartDom);
let option;
// prettier-ignore
// const data = [["2000-06-05", 1160], ["2000-06-06", 1209], ["2000-06-07", 1035], ["2000-06-08", 86], ["2000-06-09", 703],
// ["2000-06-10", 805], ["2000-06-11", 73], ["2000-06-12", 68], ["2000-06-13", 92], ["2000-06-14", 130], ["2000-06-15", 245],
// ];
// const dateList = data.map(function (item) {
// return item[0];
// });
// const valueList = data.map(function (item) {
// return item[1];
// });
// console.log("this.inoutArr",this.inoutArr)
const data = this.inoutArr;
const dateList = this.inoutArr;
const valueList = this.inoutArr;
option = {
// Make gradient line here
visualMap: [
{
show: false,
type: "continuous",
seriesIndex: 0,
min: 0,
},
{
show: false,
type: "continuous",
seriesIndex: 1,
dimension: 0,
min: 0,
},
],
title: [
{
left: "left",
text: `吞吐量Gb/s`,
textStyle: {
fontSize: 12,
color: "#7C818D",
},
},
],
// tooltip: {
// trigger: "axis",
// },
xAxis:
// {
// data: dateList,
// },
{
show: false,
data: dateList,
gridIndex: 1,
},
yAxis: {
gridIndex: 1,
min: 0, //取0为最小刻度
// max: 1000000, //取100000为最大刻度
},
grid: [
{
bottom: "0%",
},
{
top: "20%",
},
],
series: [
{
type: "line",
showSymbol: false,
data: valueList,
},
],
};
option && myChart.setOption(option);
},
//初始化Websocket--sys_info
initWebSocket() {
if (typeof WebSocket === "undefined")
return console.log("您的浏览器不支持websocket");
this.websock = new WebSocket(this.wsUrl);
this.websock.onmessage = this.websocketonmessage;
this.websock.onopen = this.websocketonopen;
this.websock.onerror = this.websocketonerror;
this.websock.onclose = this.websocketclose;
},
websocketonopen() {
// console.log("链接建立之后执行send方法发送数据");
let action = { message: "sys_info" };
this.websocketsend(JSON.stringify(action));
},
websocketonerror() {
//链接建立失败重连
this.initWebSocket();
},
websocketonmessage(e) {
//数据接收
const redata = JSON.parse(e.data);
// console.log("接收的数据", redata);
this.cpu_info = redata.cpu_info;
this.cpuCharts();
this.mem_info = redata.mem_info;
this.memoryCharts();
this.disk_info = redata.disk_info;
this.hardCharts();
},
websocketsend(Data) {
//数据发送
// console.log("数据发送", Data);
this.websock.send(Data);
},
websocketclose(e) {
//关闭
// console.log("断开链接", e);
},
//初始化Websocket--net_info
initWebSocketNet() {
if (typeof WebSocket === "undefined")
return console.log("您的浏览器不支持websocket");
this.websockNet = new WebSocket(this.wsUrl);
this.websockNet.onmessage = this.websocketonmessageNet;
this.websockNet.onopen = this.websocketonopenNet;
this.websockNet.onerror = this.websocketonerrorNet;
this.websockNet.onclose = this.websocketcloseNet;
},
websocketonopenNet() {
// console.log("链接建立之后执行send方法发送数据");
let action = { message: "net_info" };
this.websocketsendNet(JSON.stringify(action));
},
websocketonerrorNet() {
//链接建立失败重连
this.initWebSocketNet();
},
websocketonmessageNet(e) {
//数据接收
const redata = JSON.parse(e.data);
// console.log("net_info接收的数据", redata);
this.in_out_total = redata.in_out_total;
let allRate = 1000 * 1000 * 1000;
this.inoutArr = this.inoutArr.concat(
(redata.in_out_total / allRate).toFixed(2)
);
this.card_info = redata.card_info;
this.lineCharts();
},
websocketsendNet(Data) {
//数据发送
// console.log("数据发送", Data);
this.websockNet.send(Data);
},
websocketcloseNet(e) {
//关闭
// console.log("断开链接", e);
},
WayMethods(item, type) {
let arr = Object.keys(item);
// console.log("arr", item, arr);
if (type == "incoming") {
if (item[arr[0]].incoming <= 1000) {
return item[arr[0]].incoming + "bit/s";
} else if (
item[arr[0]].incoming > 1000 &&
item[arr[0]].incoming <= 1000 * 1000
) {
return (item[arr[0]].incoming / 1000).toFixed(2) + "Kb/s";
} else if (
item[arr[0]].incoming > 1000 * 1000 &&
item[arr[0]].incoming <= 1000 * 1000 * 1000
) {
return (item[arr[0]].incoming / (1000 * 1000)).toFixed(2) + "Mb/s";
} else if (item[arr[0]].incoming > 1000 * 1000 * 1000) {
return (
(item[arr[0]].incoming / (1000 * 1000 * 1000)).toFixed(2) + "Gb/s"
);
}
} else if (type == "outgoing") {
if (item[arr[0]].outgoing <= 1000) {
return item[arr[0]].outgoing + "bit/s";
} else if (
item[arr[0]].outgoing > 1000 &&
item[arr[0]].outgoing <= 1000 * 1000
) {
return (item[arr[0]].outgoing / 1000).toFixed(2) + "Kb/s";
} else if (
item[arr[0]].outgoing > 1000 * 1000 &&
item[arr[0]].outgoing <= 1000 * 1000 * 1000
) {
return (item[arr[0]].outgoing / (1000 * 1000)).toFixed(2) + "Mb/s";
} else if (item[arr[0]].outgoing > 1000 * 1000 * 1000) {
return (
(item[arr[0]].outgoing / (1000 * 1000 * 1000)).toFixed(2) + "Gb/s"
);
}
} else {
return item[arr[0]][type];
}
},
},
};
</script>
<style lang="scss" scoped>
.homePage {
display: flex;
flex-direction: column;
.topArea {
display: flex;
justify-content: space-between;
.el-card {
width: 100%;
background: #fbfdff;
::v-deep .el-card__header {
padding: 0 !important;
z-index: 9;
position: relative;
}
::v-deep .el-card__body {
padding: 0 !important;
}
.headerBox {
position: relative;
width: 100%;
height: 39px;
z-index: 9;
background: #fff !important;
.arrow {
position: absolute;
top: 0.6875rem;
left: 0.5rem;
img {
width: 1rem;
height: 1rem;
}
}
.title {
position: absolute;
top: 0.687rem;
left: 1.625rem;
font-weight: bolder;
color: #2d3d59;
font-size: 1rem;
font-family: SourceHanSansCN;
}
.topRight {
position: absolute;
bottom: -0.3rem;
right: 0;
img {
width: 2.1875rem;
height: 0.3125rem;
}
}
}
.myCont {
// border: 1px solid blue;
width: 100%;
display: flex;
background: #fbfdff;
margin-top: 1rem;
margin-bottom: 1rem;
.itemBox {
width: 32.5625rem;
height: 21.75rem;
// border: 1px solid red;
display: flex;
flex-direction: column;
.titleBox {
height: 2rem;
display: flex;
span img {
margin-top: 0.125rem;
width: 1rem;
height: 1rem;
}
.title {
margin-left: 0.625rem;
font-size: 1rem;
font-family: SourceHanSansCN;
font-weight: bolder;
color: #2d3d59;
}
}
.board {
width: 29.5625rem;
height: 18.75rem;
background: #f3f7ff;
box-shadow: inset 0px 0.25rem 0.25rem 0px rgba(92, 127, 252, 0.12);
border-radius: 0.25rem;
opacity: 1;
display: flex;
.left {
width: 15rem;
height: 12.5rem;
margin-top: 3.125rem;
margin-left: 0.875rem;
}
.right {
width: 15.1875rem;
// border: 1px solid blue;
margin-top: 3.125rem;
position: relative;
.tipBox {
width: 11.9375rem;
height: 4.875rem;
display: flex;
justify-content: space-between;
// border: 1px solid red;
position: absolute;
top: 1.125rem;
left: 1.375rem;
span {
&:nth-child(1) {
font-size: 0.875rem;
font-family: SourceHanSansCN;
font-weight: bold;
color: #413f3f;
}
&:nth-child(2) {
font-size: 1.5rem;
font-family: D-DIN-DIN-Bold, D-DIN-DIN;
font-weight: bold;
color: #0082fa;
margin-top: -0.5rem;
}
}
}
.detailBox {
position: absolute;
top: 5rem;
left: 1.375rem;
display: flex;
flex-direction: column;
// border: 1px solid salmon;
width: 11.9375rem;
div {
height: 1.75rem;
display: flex;
position: relative;
span {
// border: 1px solid red;
&:nth-child(1) {
width: 0.375rem;
height: 0.375rem;
border-radius: 0.1875rem;
background: #00b42a;
opacity: 1;
position: absolute;
top: 0.5rem;
left: 0;
}
&:nth-child(2) {
font-size: 0.8125rem;
font-family: SourceHanSansCN;
font-weight: 400;
color: #413f3f;
position: absolute;
top: 0.25rem;
left: 0.625rem;
}
&:nth-child(3) {
font-size: 0.8125rem;
font-family: SourceHanSansCN;
font-weight: 400;
color: #413f3f;
position: absolute;
top: 0.25rem;
right: 0rem;
}
}
}
}
.otherBox {
display: flex;
flex-direction: column;
position: absolute;
top: 0;
left: 1.375rem;
height: 4.875rem;
width: 100%;
// border: 1px solid blue;
.tipBox2 {
width: 11.9375rem;
height: 2.4375rem;
display: flex;
justify-content: space-between;
// border: 1px solid red;
span {
&:nth-child(1) {
font-size: 0.875rem;
font-family: SourceHanSansCN;
font-weight: bold;
color: #413f3f;
display: block;
}
&:nth-child(2) {
font-size: 1.5rem;
font-family: D-DIN-DIN-Bold, D-DIN-DIN;
font-weight: bold;
color: #0082fa;
margin-top: -0.35rem;
}
&:nth-child(3) {
font-size: 1rem;
font-family: D-DIN-DIN-Bold, D-DIN-DIN;
font-weight: bold;
color: #0082fa;
margin-top: -0.05rem;
}
}
}
}
}
}
&:nth-child(1) {
margin-left: 1.6875rem;
}
&:nth-child(2) {
margin-left: 2.625rem;
}
&:nth-child(3) {
margin-left: 2.625rem;
}
}
}
}
}
.bottomArea {
margin-top: 1rem;
.el-card {
.myCont {
height: 19rem;
display: flex;
.Throughput {
width: 36.875rem;
display: flex;
flex-direction: column;
.titleBox {
margin-top: 2.5rem;
margin-left: 1.5rem;
position: relative;
height: 2rem;
span {
position: absolute;
top: 0;
left: 0;
img {
width: 1rem;
height: 1rem;
}
}
.title {
position: absolute;
top: 0;
left: 1.5rem;
font-size: 0.875rem;
font-family: SourceHanSansCN;
font-weight: 400;
color: #2d3d59;
}
}
.lineBox {
width: 31.25rem;
height: 12.5rem;
margin-left: 1.6875rem;
// border: 1px solid red;
background: #fefbff;
}
.lineTips {
margin-left: 1.6875rem;
background: #fefbff;
width: 31.25rem;
span {
img {
width: 1rem;
height: 1rem;
}
&:nth-child(1) {
margin-top: 0.25rem;
}
&:nth-child(2) {
font-size: 0.875rem;
font-family: SourceHanSansCN;
font-weight: 400;
color: #413f3f;
margin: 0rem 0.5rem;
}
&:nth-child(3) {
font-size: 1rem;
font-family: D-DIN-DIN-Bold, D-DIN-DIN;
font-weight: bold;
color: #413f3f;
}
&:nth-child(4) {
font-size: 1rem;
font-family: D-DIN-DIN-Bold, D-DIN-DIN;
font-weight: bold;
color: #413f3f;
}
}
}
}
.rightArea {
// border: 1px solid blue;
width: 64.8125rem;
display: flex;
.Item {
width: 14rem;
height: 16.125rem;
margin-top: 2.5rem;
margin-right: 3rem;
// border: 1px solid red;
opacity: 1;
display: flex;
flex-direction: column;
&:nth-child(4) {
margin-right: 0rem;
}
.topBox {
position: relative;
background: #f2f3fb !important;
height: 5.875rem;
z-index: 9;
border-radius: 0.25rem;
.imgBox {
position: absolute;
top: 0.875rem;
left: 1rem;
img {
width: 4.125rem;
height: 4.125rem;
}
}
.portTip {
position: absolute;
top: 0.75rem;
left: 5.875rem;
font-size: 1rem;
font-family: SourceHanSansCN;
font-weight: 500;
color: #413f3f;
}
.transferBox {
span {
img {
width: 1rem;
height: 1rem;
}
}
.Num {
font-size: 0.875rem;
font-family: SourceHanSansCN;
font-weight: 500;
color: #7c818d;
}
}
.transferBoxT {
position: absolute;
top: 2.5rem;
left: 5.875rem;
}
.transferBoxB {
position: absolute;
top: 3.8125rem;
left: 5.875rem;
}
}
.bottomBox {
background: #fefbff;
// border: 1px solid orange;
margin-top: 1.5rem;
display: flex;
flex-direction: column;
div {
display: flex;
font-size: 0.875rem;
font-family: SourceHanSansCN;
font-weight: 400;
color: #413f3f;
line-height: 1.75rem;
span {
&:nth-child(1) {
margin-right: 0.5rem;
}
}
}
}
}
}
}
}
}
}
</style>