Commit db11048f57523bb8ce8e6919a7e6c66e0e0cd48c

Authored by 阿宝
1 parent 530a37c8

设备状态,学校管理

package.json
... ... @@ -15,6 +15,7 @@
15 15 "font-awesome": "^4.7.0",
16 16 "js-cookie": "^2.2.0",
17 17 "jsencrypt": "^3.2.0",
  18 + "lodash": "^4.17.21",
18 19 "nprogress": "^0.2.0",
19 20 "script-ext-html-webpack-plugin": "^2.1.5",
20 21 "vue": "^2.6.11",
... ...
src/api/apis/device.js 0 → 100644
  1 +
  2 +import service from "../axios"
  3 +import deviceUrls from "../urls/device"
  4 +
  5 +export default {
  6 + // 下载设备模板
  7 + downDevice(data) {
  8 + return service({
  9 + url: deviceUrls.downDevice,
  10 + method: 'POST',
  11 + data
  12 + })
  13 + },
  14 + // 设备列表
  15 + fetchDeviceList(data) {
  16 + return service({
  17 + url: deviceUrls.deviceList,
  18 + method: 'POST',
  19 + data
  20 + })
  21 + },
  22 + // autoUpDate
  23 + autoUpDate(data) {
  24 + return service({
  25 + url: deviceUrls.autoUpDate,
  26 + method: 'POST',
  27 + data
  28 + })
  29 + },
  30 + // 设备列表
  31 + stopUpdate(data) {
  32 + return service({
  33 + url: deviceUrls.stopUpdate,
  34 + method: 'POST',
  35 + data
  36 + })
  37 + },
  38 +
  39 +}
... ...
src/api/urls/device.js 0 → 100644
  1 +
  2 +export default {
  3 + // 设备文件上传
  4 + upLoadDevice: "/web/upLoadDevice",
  5 + // 设备模板下载
  6 + downDevice: "/web/downDevice",
  7 + // 设备列表
  8 + deviceList: "/web/deviceList",
  9 + // 自动更新
  10 + autoUpDate: "/web/autoUpDate",
  11 + // 关闭自动更新
  12 + stopUpdate: "/web/stopUpdate",
  13 +}
... ...
src/assets/css/index.scss
... ... @@ -22,7 +22,7 @@
22 22 .el-input__inner {
23 23 border-radius: 20px;
24 24 border: 1px solid #e2e2e2;
25   - height: 36px;
  25 + height: 36px!important;
26 26 line-height: 34px;
27 27 }
28 28  
... ... @@ -116,4 +116,24 @@
116 116 .pagination-box{
117 117 text-align:center;
118 118 margin:10px;
  119 +}
  120 +.down-txt{
  121 + display: flex;
  122 + align-items: center;
  123 + justify-content: center;
  124 +}
  125 +.h-title{
  126 + padding-left:12px;
  127 + position: relative;
  128 + font-size:16px;
  129 + &:after{
  130 + content:"";
  131 + position:absolute;
  132 + left:0;
  133 + top:50%;
  134 + margin-top:-8px;
  135 + width:3px;
  136 + height:16px;
  137 + background: #2e9afe;
  138 + }
119 139 }
120 140 \ No newline at end of file
... ...
src/components/ECharts/lineEcharts.vue deleted
1   -<template>
2   - <div>
3   - <div :id="id" :style="{width: width, height: height}"></div>
4   - </div>
5   -</template>
6   -
7   -<script>
8   -import * as echarts from "echarts"
9   -export default {
10   - name: "lineEcharts",
11   - props: {
12   - id: {
13   - type: String,
14   - default: "myChart"
15   - },
16   - width: {
17   - type: String,
18   - default: "100%"
19   - },
20   - height: {
21   - type: String,
22   - default: "100%"
23   - }
24   - },
25   - data () {
26   - return {
27   - chart: null
28   - }
29   - },
30   - mounted () {
31   - this.initChart()
32   - },
33   - methods: {
34   - initChart () {
35   - this.chart = echarts.init(document.getElementById(this.id))
36   -
37   - this.chart.setOption({
38   - title: {
39   - text: "折线图堆叠"
40   - },
41   - tooltip: {
42   - trigger: "axis"
43   - },
44   - legend: {
45   - data: ["邮件营销", "联盟广告", "视频广告", "直接访问", "搜索引擎"]
46   - },
47   - grid: {
48   - left: "3%",
49   - right: "4%",
50   - bottom: "3%",
51   - containLabel: true
52   - },
53   - toolbox: {
54   - feature: {
55   - saveAsImage: {}
56   - }
57   - },
58   - xAxis: {
59   - type: "category",
60   - boundaryGap: false,
61   - data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
62   - },
63   - yAxis: {
64   - type: "value"
65   - },
66   - series: [
67   - {
68   - name: "邮件营销",
69   - type: "line",
70   - stack: "总量",
71   - data: [8200, 6320, 5010, 4340, 3400, 2300, 1100]
72   - },
73   - {
74   - name: "联盟广告",
75   - type: "line",
76   - stack: "总量",
77   - data: [2200, 3820, 1910, 2340, 4900, 3300, 1100]
78   - },
79   - {
80   - name: "视频广告",
81   - type: "line",
82   - stack: "总量",
83   - data: [2500, 4302, 5010, 2540, 6900, 5300, 6410]
84   - },
85   - {
86   - name: "直接访问",
87   - type: "line",
88   - stack: "总量",
89   - data: [5320, 7332, 9301, 6334, 5390, 4330, 1320]
90   - },
91   - {
92   - name: "搜索引擎",
93   - type: "line",
94   - stack: "总量",
95   - data: [8820, 1932, 5901, 7304, 2900, 3300, 8200]
96   - }
97   - ]
98   - })
99   -
100   - this.selfAdaption()
101   - },
102   - // echart自适应
103   - selfAdaption() {
104   - let that = this;
105   - setTimeout(() => {
106   - window.onresize = function () {
107   - if (that.$refs.echarts) {
108   - that.$refs.echarts.chart.resize();
109   - }
110   - };
111   - }, 10);
112   - },
113   - }
114   -}
115   -</script>
116   -
117   -<style scoped>
118   -
119   -</style>
src/components/charts/pieChart.vue 0 → 100644
  1 +<template>
  2 + <div class="chart" :id="id"></div>
  3 +</template>
  4 +
  5 +<script>
  6 +export default {
  7 + name: "pieChart",
  8 + props: {
  9 + id: String,
  10 + params: Array,
  11 + },
  12 + watch: {
  13 + params: {
  14 + handler: function (val) {
  15 + if (val.length) {
  16 + this.initData();
  17 + }
  18 + },
  19 + deep: true,
  20 + },
  21 + },
  22 + data() {
  23 + return {
  24 + chart: null,
  25 + };
  26 + },
  27 + created() {},
  28 + mounted() {
  29 + this.initData();
  30 + },
  31 + methods: {
  32 + setOption() {
  33 + const that = this;
  34 + let barStyle = {
  35 + barGap: 0,
  36 + barWidth: 16,
  37 + emphasis: {
  38 + focus: "series",
  39 + },
  40 + itemStyle: {
  41 + borderRadius: [8, 8, 0, 0],
  42 + },
  43 + };
  44 + const options = {
  45 + color: this.colors || ["#ff80db", "#c8cc00", "#67c6b5"],
  46 + backgroundColor: "#f8f8f8",
  47 + tooltip: {
  48 + trigger: "item",
  49 + confine: true,
  50 + formatter(v) {
  51 + return `${v.marker} ${v.name}-${v.value}%`
  52 + },
  53 + },
  54 + legend: {
  55 + show: false,
  56 + bottom: 40,
  57 + left: 60,
  58 + tight: 60,
  59 + selectedMode: false,
  60 + formatter: function (name) {},
  61 + textStyle: {
  62 + rich: {
  63 + b: {
  64 + color: "#999",
  65 + },
  66 + },
  67 + },
  68 + },
  69 + series: {
  70 + type: "pie",
  71 + center: ["50%", "50%"], // 位置
  72 + radius: [0, "80%"],
  73 + data: that.params,
  74 + label: {
  75 + normal: {
  76 + formatter(v) {
  77 + return v.name + v.value + "%";
  78 + },
  79 + },
  80 + },
  81 + selectedMode: true,
  82 + hoverAnimation: true,
  83 + avoidLabelOverlap: true,
  84 + animationType: "scale",
  85 + animationEasing: "elasticOut",
  86 + animationDelay() {
  87 + return Math.random() * 100;
  88 + },
  89 + },
  90 + };
  91 + return options;
  92 + },
  93 + initData() {
  94 + if (!this.chart) {
  95 + const div = document.getElementById(this.id);
  96 + this.chart = this.$echarts.init(div);
  97 + }
  98 + const options = this.setOption();
  99 + this.chart?.clear();
  100 + this.chart.setOption(options, true);
  101 + this.chart.off("click");
  102 + this.chart.on("click", "series", (params) => {
  103 + this.$emit("clickPieChart", params);
  104 + });
  105 + },
  106 + },
  107 +};
  108 +</script>
  109 +
  110 +<style lang="scss" scoped>
  111 +.chart {
  112 + height: 100%;
  113 +}
  114 +</style>
... ...
src/components/charts/scatterChart.vue 0 → 100644
  1 +<template>
  2 + <div class="chart" :id="id"></div>
  3 +</template>
  4 +
  5 +<script>
  6 +export default {
  7 + name: "pieChart",
  8 + props: {
  9 + id: String,
  10 + params: Array,
  11 + },
  12 + watch: {
  13 + params: {
  14 + handler: function (val) {
  15 + if (val.length) {
  16 + this.initData();
  17 + }
  18 + },
  19 + deep: true,
  20 + },
  21 + },
  22 + data() {
  23 + return {
  24 + chart: null,
  25 + };
  26 + },
  27 + created() {},
  28 + mounted() {
  29 + this.initData();
  30 + },
  31 + methods: {
  32 + setOption() {
  33 + const that = this;
  34 + let xData = this.params.map((item) => {
  35 + return item.name;
  36 + });
  37 + const options = {
  38 + // color: this.colors || ["#ff80db", "#c8cc00", "#67c6b5"],
  39 + backgroundColor: "#f8f8f8",
  40 + tooltip: {
  41 + axisPointer: {
  42 + type: "cross",
  43 + },
  44 + formatter: function (params) {
  45 + return (
  46 + params.marker +
  47 + ` 占比:${params.data.value}% <br>数量:${params.data.count}`
  48 + );
  49 + },
  50 + },
  51 + xAxis: {
  52 + type: "category",
  53 + data: xData,
  54 + axisLine: {
  55 + show: true,
  56 + lineStyle: {
  57 + width: 0.5,
  58 + color: "#999",
  59 + },
  60 + },
  61 + axisTick: {
  62 + show: false,
  63 + },
  64 + },
  65 + yAxis: {
  66 + name:"活跃度占比",
  67 + type: "value",
  68 + nameTextStyle:{
  69 + align:"right"
  70 + },
  71 + axisLine: {
  72 + show: true,
  73 + lineStyle: {
  74 + width: 0.5,
  75 + color: "#999",
  76 + },
  77 + },
  78 + splitLine: {
  79 + show: false,
  80 + },
  81 + max: 100,
  82 + axisLabel: {
  83 + show: true,
  84 + interval: "auto",
  85 + formatter: "{value}%",
  86 + },
  87 + },
  88 + series: {
  89 + data: this.params,
  90 + colorBy: "data",
  91 + type: "scatter",
  92 + barMaxWidth: 40, //最大宽度
  93 + barMinHeight: 0, // 最小高度
  94 + symbolSize: function (data) {
  95 + return Math.sqrt(data) * 8 > 10 ? Math.sqrt(data) * 8 : 10;
  96 + },
  97 + itemStyle: {
  98 + shadowBlur: 10,
  99 + shadowColor: "rgba(25, 100, 150, 0.5)",
  100 + shadowOffsetY: 5,
  101 + },
  102 + label: {
  103 + show: true,
  104 + position: "top",
  105 + formatter: function (params) {
  106 + return params.data.count ? params.data.count : "";
  107 + },
  108 + },
  109 + selectedMode: true,
  110 + hoverAnimation: true,
  111 + avoidLabelOverlap: true,
  112 + animationType: "scale",
  113 + animationEasing: "elasticOut",
  114 + animationDelay() {
  115 + return Math.random() * 100;
  116 + },
  117 + },
  118 + grid: {
  119 + top: 40,
  120 + right: "5%",
  121 + bottom: 30,
  122 + left: "10%",
  123 + },
  124 + };
  125 + return options;
  126 + },
  127 + initData() {
  128 + if (!this.chart) {
  129 + const div = document.getElementById(this.id);
  130 + this.chart = this.$echarts.init(div);
  131 + }
  132 + const options = this.setOption();
  133 + this.chart?.clear();
  134 + this.chart.setOption(options, true);
  135 + this.chart.off("click");
  136 + this.chart.on("click", "series", (params) => {
  137 + this.$emit("clickScatterChart", params);
  138 + });
  139 + },
  140 + },
  141 +};
  142 +</script>
  143 +
  144 +<style lang="scss" scoped>
  145 +.chart {
  146 + height: 100%;
  147 +}
  148 +</style>
... ...
src/components/upload.vue
1 1 <template>
2 2 <div>
  3 + <slot name="down"></slot>
3 4 <div class="d1">
4   - 第一步:下载模板并编辑完成学生分数<el-link
5   - type="primary"
6   - icon="el-icon-download"
7   - @click="()=>downExcel()"
8   - >下载模版</el-link
9   - >
10   - </div>
11   - <div class="d1">
12   - 第二步:上传完成编辑的模板文件并导入
13 5 <el-upload
14 6 class="upload-demo"
15 7 ref="upload"
16   - action="/api/web/report/importSubjectiveScore"
  8 + :action="url"
17 9 :multiple="false"
18 10 :data="{ id: id }"
19 11 :with-credentials="true"
... ... @@ -23,17 +15,18 @@
23 15 :on-error="upError"
24 16 >
25 17 <!-- accept="application/vnd.ms-excel" -->
26   - <el-button class="btn" size="mini" type="primary"
27   - >选择文件并上传</el-button
28   - >
  18 + <div class="upload-btn">
  19 + <i class="el-icon-upload"></i>
  20 + <el-button class="btn" size="mini" type="primary"
  21 + >选择文件并上传</el-button
  22 + >
  23 + </div>
29 24 </el-upload>
30 25 </div>
31 26 </div>
32 27 </template>
33 28  
34 29 <script>
35   -import { downloadTemplate } from "@/api/report";
36   -import { downloadFile } from "@/utils";
37 30 export default {
38 31 name: "downUpData",
39 32 props: {
... ... @@ -41,12 +34,15 @@ export default {
41 34 type: String,
42 35 default: "",
43 36 },
44   - classObject: {
45   - type: Object,
46   - default: ()=> {
47   - return {}
48   - },
49   - }
  37 + url: {
  38 + type: String,
  39 + default: "",
  40 + },
  41 +
  42 + fileName: {
  43 + type: String,
  44 + default: "模板",
  45 + },
50 46 },
51 47 data() {
52 48 return {
... ... @@ -87,24 +83,6 @@ export default {
87 83 change(file) {
88 84 this.file = file;
89 85 },
90   - async downExcel() {
91   - let data = await downloadTemplate({
92   - id: this.id,
93   - });
94   - if (data && !data.code) {
95   - let filename = "模板.xlsx";
96   - if (this.classObject) {
97   - let className = this.classObject.classObject?.[0].label;
98   - filename = className + "_" + this.classObject.subjectName + "_" + this.classObject.examinationName + "_主观题分数导入.xlsx";
99   - }
100   - let blob = new Blob([data], {
101   - type: "application/vnd.ms-excel;charset=utf-8",
102   - });
103   - downloadFile(filename, blob);
104   - } else {
105   - this.$message.error(data.message);
106   - }
107   - },
108 86 },
109 87 };
110 88 </script>
... ... @@ -114,8 +92,22 @@ export default {
114 92 padding: 10px;
115 93 }
116 94 .btn {
117   - margin-top: 20px;
118 95 border-radius: 8px;
119 96 font-weight: normal;
120 97 }
  98 +.upload-demo {
  99 + display: flex;
  100 + flex-direction: column;
  101 + align-items: center;
  102 +}
  103 +.upload-btn {
  104 + display: flex;
  105 + flex-direction: column;
  106 + align-items: center;
  107 + .el-icon-upload {
  108 + font-size: 48px;
  109 + margin-bottom: 6px;
  110 + color: #667ffd;
  111 + }
  112 +}
121 113 </style>
... ...
src/router/index.js
... ... @@ -21,6 +21,7 @@ const Portrait = () =&gt; import(&quot;@/views/portrait/index&quot;)
21 21 const Card = () => import("@/views/card/index")
22 22 const Analysis = () => import("@/views/analysis/index")
23 23 const Device = () => import("@/views/device/index")
  24 +const DeviceLog = () => import("@/views/device/log")
24 25 const Down = () => import("@/views/down/index")
25 26 const DownClient = () => import("@/views/down/client")
26 27 const SetUpAccount = () => import("@/views/setUp/account")
... ... @@ -269,6 +270,13 @@ let addrouters = [ //测试用,后续后端获取
269 270 name: "",
270 271 component: Device,
271 272 children: []
  273 + },
  274 + {
  275 + path: "/deviceLog",
  276 + iconCls: "fa fa-list-alt", // 图标样式class
  277 + name: "",
  278 + component: DeviceLog,
  279 + children: []
272 280 }
273 281 ]
274 282 },
... ...
src/utils/index.js
1   -
2 1 // import CryptoJS from "crypto-js"
3   -import { JSEncrypt } from 'jsencrypt'
  2 +import { JSEncrypt } from "jsencrypt";
4 3  
5   -const encryptKey = "WfJTKO9S4eLkrPz2JKrAnzdb"
6   -const encryptIV = "D076D35C"
  4 +const encryptKey = "WfJTKO9S4eLkrPz2JKrAnzdb";
  5 +const encryptIV = "D076D35C";
7 6 /**
8 7 * 登录密码加密,公钥直接写死在方法里
9 8 * @param data: 待加密数据
10 9 * @returns 加密结果
11 10 */
12 11 export function encryptLoginPassword(data) {
13   - const secret = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjh2ei17z5k2r4VzbqoSCE6RmYzWySJTgVQYulgfVM+vqcDoUE4cFB4XCFA2lHWjjpsuJP1EtwKlvUgxo5okr3x/a88o8eERxBynnVQZbEYpKteW5aqSEb/g1yPLWnKV88b/ED445ITYbZZuInRo5lkCvd6QEjL6d2Fch6mEo5awYXC4/S4BJf9YlYRhGzR7wpiXCLvyBHQ4iSIIDNpmrPBPQzGP0rx09aDu54kz/42CR6SX2OqXSi4ZoieqkPFl/iuX4RoD/NKKR+haDn1UzoD3k1WzHSTBFFs27rxRpxfBUZzfXQeskgKyw/Slcl3jUFizczsY4CLgTRrfey48Q6QIDAQAB';
  12 + const secret =
  13 + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjh2ei17z5k2r4VzbqoSCE6RmYzWySJTgVQYulgfVM+vqcDoUE4cFB4XCFA2lHWjjpsuJP1EtwKlvUgxo5okr3x/a88o8eERxBynnVQZbEYpKteW5aqSEb/g1yPLWnKV88b/ED445ITYbZZuInRo5lkCvd6QEjL6d2Fch6mEo5awYXC4/S4BJf9YlYRhGzR7wpiXCLvyBHQ4iSIIDNpmrPBPQzGP0rx09aDu54kz/42CR6SX2OqXSi4ZoieqkPFl/iuX4RoD/NKKR+haDn1UzoD3k1WzHSTBFFs27rxRpxfBUZzfXQeskgKyw/Slcl3jUFizczsY4CLgTRrfey48Q6QIDAQAB";
14 14 // 新建JSEncrypt对象
15 15 let encryptor = new JSEncrypt();
16 16 // 设置公钥
... ... @@ -20,11 +20,11 @@ export function encryptLoginPassword(data) {
20 20 }
21 21  
22 22 /**
23   -* 对称加密
24   -* @param secret:加密公钥
25   -* @param data: 待加密数据
26   -* @returns 加密结果
27   -*/
  23 + * 对称加密
  24 + * @param secret:加密公钥
  25 + * @param data: 待加密数据
  26 + * @returns 加密结果
  27 + */
28 28 export function encryptData(secret, data) {
29 29 // 新建JSEncrypt对象
30 30 let encryptor = new JSEncrypt();
... ... @@ -36,17 +36,17 @@ export function encryptData(secret, data) {
36 36  
37 37 // 深度复制
38 38 export function deepClone(obj) {
39   - let result = Array.isArray(obj) ? [] : {}
  39 + let result = Array.isArray(obj) ? [] : {};
40 40 for (let key in obj) {
41 41 if (obj.hasOwnProperty(key)) {
42 42 if (typeof obj[key] === "object") {
43   - result[key] = deepClone(obj[key])
  43 + result[key] = deepClone(obj[key]);
44 44 } else {
45   - result[key] = obj[key]
  45 + result[key] = obj[key];
46 46 }
47 47 }
48 48 }
49   - return result
  49 + return result;
50 50 }
51 51  
52 52 // // 3DES加密
... ... @@ -78,45 +78,109 @@ export function randomWord(randomFlag, min, max) {
78 78 // randomFlag: Boolean 是否随机个数
79 79 // min 最少个数
80 80 // max 最大个数
81   - var str = ""
82   - var range = min
83   - var arr = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
  81 + var str = "";
  82 + var range = min;
  83 + var arr = [
  84 + "0",
  85 + "1",
  86 + "2",
  87 + "3",
  88 + "4",
  89 + "5",
  90 + "6",
  91 + "7",
  92 + "8",
  93 + "9",
  94 + "a",
  95 + "b",
  96 + "c",
  97 + "d",
  98 + "e",
  99 + "f",
  100 + "g",
  101 + "h",
  102 + "i",
  103 + "j",
  104 + "k",
  105 + "l",
  106 + "m",
  107 + "n",
  108 + "o",
  109 + "p",
  110 + "q",
  111 + "r",
  112 + "s",
  113 + "t",
  114 + "u",
  115 + "v",
  116 + "w",
  117 + "x",
  118 + "y",
  119 + "z",
  120 + "A",
  121 + "B",
  122 + "C",
  123 + "D",
  124 + "E",
  125 + "F",
  126 + "G",
  127 + "H",
  128 + "I",
  129 + "J",
  130 + "K",
  131 + "L",
  132 + "M",
  133 + "N",
  134 + "O",
  135 + "P",
  136 + "Q",
  137 + "R",
  138 + "S",
  139 + "T",
  140 + "U",
  141 + "V",
  142 + "W",
  143 + "X",
  144 + "Y",
  145 + "Z",
  146 + ];
84 147 // 随机产生
85 148 if (randomFlag) {
86   - range = Math.round(Math.random() * (max - min)) + min
  149 + range = Math.round(Math.random() * (max - min)) + min;
87 150 }
88   - var pos = ""
  151 + var pos = "";
89 152 for (var i = 0; i < range; i++) {
90   - pos = Math.round(Math.random() * (arr.length - 1))
91   - str += arr[pos]
  153 + pos = Math.round(Math.random() * (arr.length - 1));
  154 + str += arr[pos];
92 155 }
93   - return str
  156 + return str;
94 157 }
95 158  
96 159 // 判断数组中是否存在相同值
97 160 export function hasRepeatValue(arr, key = null) {
98   - if (key) arr = arr.map(d => d[key])
  161 + if (key) arr = arr.map((d) => d[key]);
99 162 if (arr.length) {
100 163 let nameNum = arr.reduce((pre, cur) => {
101 164 if (cur in pre) {
102   - pre[cur]++
  165 + pre[cur]++;
103 166 } else {
104   - pre[cur] = 1
  167 + pre[cur] = 1;
105 168 }
106   - return pre
107   - }, {})
108   - return Object.values(nameNum).findIndex(d => d > 1) < 0
  169 + return pre;
  170 + }, {});
  171 + return Object.values(nameNum).findIndex((d) => d > 1) < 0;
109 172 }
110   - return true
  173 + return true;
111 174 }
112 175  
113 176 // 获取cookie值
114 177 export function getCookie(name, defaultValue) {
115   - const result = new RegExp("(^| )" + name + "=([^;]*)(;|$)")
116   - return result[0] === document.cookie.match(result[1]) ? unescape(result[0][2]) : defaultValue
  178 + const result = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
  179 + return result[0] === document.cookie.match(result[1])
  180 + ? unescape(result[0][2])
  181 + : defaultValue;
117 182 }
118 183  
119   -
120 184 /**
121 185 * base64转化unicode
122 186 */
... ... @@ -124,64 +188,69 @@ export function b64DecodeUnicode(str) {
124 188 let uni;
125 189 try {
126 190 // atob 经过 base-64 编码的字符串进行解码
127   - uni = decodeURIComponent(atob(str).split('').map(function (c) {
128   - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
129   - }).join(''));
130   - } catch (e) { }
  191 + uni = decodeURIComponent(
  192 + atob(str)
  193 + .split("")
  194 + .map(function (c) {
  195 + return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
  196 + })
  197 + .join("")
  198 + );
  199 + } catch (e) {}
131 200 return uni;
132 201 }
133 202  
134 203 // base64ToFile
135 204 export function base64ToFile(base64Data, tempfilename, contentType) {
136   - contentType = contentType || ""
137   - var sliceSize = 1024
138   - var byteCharacters = atob(base64Data)
139   - var bytesLength = byteCharacters.length
140   - var slicesCount = Math.ceil(bytesLength / sliceSize)
141   - var byteArrays = new Array(slicesCount)
  205 + contentType = contentType || "";
  206 + var sliceSize = 1024;
  207 + var byteCharacters = atob(base64Data);
  208 + var bytesLength = byteCharacters.length;
  209 + var slicesCount = Math.ceil(bytesLength / sliceSize);
  210 + var byteArrays = new Array(slicesCount);
142 211  
143 212 for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
144   - var begin = sliceIndex * sliceSize
145   - var end = Math.min(begin + sliceSize, bytesLength)
  213 + var begin = sliceIndex * sliceSize;
  214 + var end = Math.min(begin + sliceSize, bytesLength);
146 215  
147   - var bytes = new Array(end - begin)
  216 + var bytes = new Array(end - begin);
148 217 for (var offset = begin, i = 0; offset < end; ++i, ++offset) {
149   - bytes[i] = byteCharacters[offset].charCodeAt(0)
  218 + bytes[i] = byteCharacters[offset].charCodeAt(0);
150 219 }
151   - byteArrays[sliceIndex] = new Uint8Array(bytes)
  220 + byteArrays[sliceIndex] = new Uint8Array(bytes);
152 221 }
153   - var file = new File(byteArrays, tempfilename, { type: contentType })
154   - return file
  222 + var file = new File(byteArrays, tempfilename, { type: contentType });
  223 + return file;
155 224 }
156 225  
157 226 // 将base64转换为文件
158 227 export function dataURLtoFile(dataurl, filename) {
159   - var arr = dataurl.split(",")
160   - var mime = arr[0].match(/:(.*?);/)[1]
161   - var bstr = atob(arr[1])
162   - var n = bstr.length
163   - var u8arr = new Uint8Array(n)
  228 + var arr = dataurl.split(",");
  229 + var mime = arr[0].match(/:(.*?);/)[1];
  230 + var bstr = atob(arr[1]);
  231 + var n = bstr.length;
  232 + var u8arr = new Uint8Array(n);
164 233 while (n--) {
165   - u8arr[n] = bstr.charCodeAt(n)
  234 + u8arr[n] = bstr.charCodeAt(n);
166 235 }
167   - return new File([u8arr], filename, { type: mime })
  236 + return new File([u8arr], filename, { type: mime });
168 237 }
169 238  
170 239 // 将图片转换为Base64
171 240 export function getImgToBase64(url, callback, outputFormat) {
172   - var canvas = document.createElement("canvas")
173   - var ctx = canvas.getContext("2d")
174   - var img = new Image()
175   - img.crossOrigin = "Anonymous"
  241 + var canvas = document.createElement("canvas");
  242 + var ctx = canvas.getContext("2d");
  243 + var img = new Image();
  244 + img.crossOrigin = "Anonymous";
176 245 img.onload = function () {
177   - canvas.height = img.height
178   - canvas.width = img.width
179   - ctx.drawImage(img, 0, 0)
180   - var dataURL = canvas.toDataURL(outputFormat || "image/png")
181   - callback(dataURL)
182   - canvas = null
183   - }
184   - img.src = url
  246 + canvas.height = img.height;
  247 + canvas.width = img.width;
  248 + ctx.drawImage(img, 0, 0);
  249 + var dataURL = canvas.toDataURL(outputFormat || "image/png");
  250 + callback(dataURL);
  251 + canvas = null;
  252 + };
  253 + img.src = url;
185 254 }
186 255  
187 256 // 转换级联下拉数据
... ... @@ -190,47 +259,49 @@ export function loopOptions(list, option = {}) {
190 259 value: "id",
191 260 label: "name",
192 261 children: "children",
193   - ...option
194   - }
  262 + ...option,
  263 + };
195 264 if (list instanceof Array && list.length) {
196 265 return list.map((d, i) => {
197   - d.value = d[option.value] || i + 1
198   - d.label = d[option.label]
  266 + d.value = d[option.value] || i + 1;
  267 + d.label = d[option.label];
199 268 if (d[option.children]) {
200   - d[option.children] = loopOptions(d[option.children], option)
  269 + d[option.children] = loopOptions(d[option.children], option);
201 270 }
202   - return d
203   - })
  271 + return d;
  272 + });
204 273 }
205   - return []
  274 + return [];
206 275 }
207 276  
208 277 // 通过Id获取级联数据id数组
209 278 export function getTreeIds(tree, currentId, key = "id") {
210   - let parent = {}
211   - let pid = {}
  279 + let parent = {};
  280 + let pid = {};
212 281 const loop = (list, level) => {
213 282 if (list instanceof Array && list.length) {
214 283 for (let index = 0; index < list.length; index++) {
215   - const d = list[index]
216   - parent[level] = d.id
  284 + const d = list[index];
  285 + parent[level] = d.id;
217 286 if (d[key] === currentId) {
218 287 for (let idx = 1; idx <= level; idx++) {
219   - pid[idx] = parent[idx]
  288 + pid[idx] = parent[idx];
220 289 }
221   - break
  290 + break;
222 291 } else if (d.children) {
223   - loop(d.children, level + 1)
  292 + loop(d.children, level + 1);
224 293 }
225 294 }
226 295 }
227   - }
228   - loop(tree, 1)
229   - let result = []
230   - Object.keys(pid).sort((a, b) => a - b).forEach(k => {
231   - result.push(pid[k])
232   - })
233   - return result
  296 + };
  297 + loop(tree, 1);
  298 + let result = [];
  299 + Object.keys(pid)
  300 + .sort((a, b) => a - b)
  301 + .forEach((k) => {
  302 + result.push(pid[k]);
  303 + });
  304 + return result;
234 305 }
235 306  
236 307 /*
... ... @@ -264,38 +335,47 @@ export function formatDate(date, fmt) {
264 335 return time_str;
265 336 }
266 337  
267   -
268 338 // 获取日期时间戳
269 339 export function getTime(dayNum) {
270   - var myDate = new Date()
271   - var lw = new Date(myDate - 1000 * 60 * 60 * 24 * dayNum)// 最后一个数字多少天前的意思
272   - var lastY = lw.getFullYear()
273   - var lastM = lw.getMonth() + 1
274   - var lastD = lw.getDate()
275   - var startdate = lastY + "-" + (lastM < 10 ? "0" + lastM : lastM) + "-" + (lastD < 10 ? "0" + lastD : lastD)
276   - var b = startdate.split(/\D/)
277   - var date = new Date(b[0], b[1] - 1, b[2])
278   - var time = date.getTime()
279   - return time
  340 + var myDate = new Date();
  341 + var lw = new Date(myDate - 1000 * 60 * 60 * 24 * dayNum); // 最后一个数字多少天前的意思
  342 + var lastY = lw.getFullYear();
  343 + var lastM = lw.getMonth() + 1;
  344 + var lastD = lw.getDate();
  345 + var startdate =
  346 + lastY +
  347 + "-" +
  348 + (lastM < 10 ? "0" + lastM : lastM) +
  349 + "-" +
  350 + (lastD < 10 ? "0" + lastD : lastD);
  351 + var b = startdate.split(/\D/);
  352 + var date = new Date(b[0], b[1] - 1, b[2]);
  353 + var time = date.getTime();
  354 + return time;
280 355 }
281 356  
282 357 // 获取几天之前日期
283 358 export function getData(dayNum) {
284   - var myDate = new Date()
285   - var lw = new Date(myDate - 1000 * 60 * 60 * 24 * dayNum)// 最后一个数字多少天前的意思
286   - var lastY = lw.getFullYear()
287   - var lastM = lw.getMonth() + 1
288   - var lastD = lw.getDate()
289   - var startdate = lastY + "-" + (lastM < 10 ? "0" + lastM : lastM) + "-" + (lastD < 10 ? "0" + lastD : lastD)
290   - return startdate
  359 + var myDate = new Date();
  360 + var lw = new Date(myDate - 1000 * 60 * 60 * 24 * dayNum); // 最后一个数字多少天前的意思
  361 + var lastY = lw.getFullYear();
  362 + var lastM = lw.getMonth() + 1;
  363 + var lastD = lw.getDate();
  364 + var startdate =
  365 + lastY +
  366 + "-" +
  367 + (lastM < 10 ? "0" + lastM : lastM) +
  368 + "-" +
  369 + (lastD < 10 ? "0" + lastD : lastD);
  370 + return startdate;
291 371 }
292 372  
293 373 // 日期转换时间戳
294 374 export function getNewTime(dayNum) {
295   - var b = dayNum.split(/\D/)
296   - var date = new Date(b[0], b[1] - 1, b[2])
297   - var time = date.getTime()
298   - return time
  375 + var b = dayNum.split(/\D/);
  376 + var date = new Date(b[0], b[1] - 1, b[2]);
  377 + var time = date.getTime();
  378 + return time;
299 379 }
300 380  
301 381 /*
... ... @@ -310,17 +390,17 @@ export function getURLParams(variable) {
310 390 let data;
311 391 for (var i = 0; i < ar.length; i++) {
312 392 var pair = ar[i].split("=");
313   - pair[1] = decodeURIComponent(pair[1])
  393 + pair[1] = decodeURIComponent(pair[1]);
314 394  
315 395 if (pair[0] == variable) {
316   - data = pair[1]
317   - return pair[1]
  396 + data = pair[1];
  397 + return pair[1];
318 398 }
319 399 if (pair[0]) {
320   - obj[pair[0]] = pair[1]
  400 + obj[pair[0]] = pair[1];
321 401 }
322 402 }
323   - return variable ? data : obj
  403 + return variable ? data : obj;
324 404 }
325 405  
326 406 /**
... ... @@ -335,13 +415,12 @@ function filtterChar(s, b, optionCount) {
335 415 let rs = "";
336 416 for (let i = 0; i < s.length; i++) {
337 417 let c = s[i];
338   - if (c == ',' || c == ' ' || c == ',') {
  418 + if (c == "," || c == " " || c == ",") {
339 419 if (!b) {
340 420 continue;
341 421 }
342   - c = ',';
343   - }
344   - else if (c < 'A' || c >= ms[optionCount]) {
  422 + c = ",";
  423 + } else if (c < "A" || c >= ms[optionCount]) {
345 424 continue;
346 425 }
347 426 rs += c;
... ... @@ -359,20 +438,26 @@ function removeDup(s) {
359 438 }
360 439 return rs;
361 440 }
362   -export function checkAnswer(s, questionType, optionCount = 4, questionCount = 1) {
  441 +export function checkAnswer(
  442 + s,
  443 + questionType,
  444 + optionCount = 4,
  445 + questionCount = 1
  446 +) {
363 447 if (optionCount > 10 || questionCount < 1) {
364 448 return null;
365 449 }
366 450 let pre = s;
367 451 s = s.toUpperCase();
368 452 s = filtterChar(s, questionType == 3 && questionCount > 1, optionCount);
369   - if (questionType == 2) {//单选
  453 + if (questionType == 2) {
  454 + //单选
370 455 console.log(s.length + " " + questionCount);
371 456 if (s.length > questionCount) {
372 457 s = s.substring(s.length - questionCount, s.length);
373 458 }
374   - }
375   - else if (questionType == 3) {//多选
  459 + } else if (questionType == 3) {
  460 + //多选
376 461 //允许逗号
377 462 let ss = s.split(",");
378 463 let len = questionCount;
... ... @@ -383,13 +468,213 @@ export function checkAnswer(s, questionType, optionCount = 4, questionCount = 1)
383 468 for (let i = 0; i < len; i++) {
384 469 rs += removeDup(ss[i]);
385 470 if (i < len - 1) {
386   - rs += ',';
  471 + rs += ",";
387 472 }
388 473 }
389 474 s = rs;
390   - }
391   - else {
  475 + } else {
392 476 return null;
393 477 }
394 478 return s;
395 479 }
  480 +
  481 +export function downloadFile(fileName, files) {
  482 + if (typeof window.navigator.msSaveBlob !== "undefined") {
  483 + window.navigator.msSaveBlob(files, fileName);
  484 + } else {
  485 + let URL = window.URL || window.webkitURL;
  486 + let objectUrl;
  487 + if (files instanceof Blob) {
  488 + objectUrl = URL.createObjectURL(files);
  489 + } else {
  490 + objectUrl = files;
  491 + }
  492 + const a = document.createElement("a");
  493 + if (typeof a.download === "undefined") {
  494 + window.location = objectUrl;
  495 + } else {
  496 + a.href = objectUrl;
  497 + a.download = fileName;
  498 + document.body.appendChild(a);
  499 + a.click();
  500 + a.remove();
  501 + }
  502 + }
  503 +}
  504 +// 获取网络URL的blob,返回一个promise
  505 +export function getBlob(url) {
  506 + return new Promise((resolve) => {
  507 + resolve(
  508 + service({
  509 + url: url,
  510 + withCredentials: true,
  511 + method: "get",
  512 + responseType: "blob",
  513 + })
  514 + );
  515 + });
  516 +}
  517 +/**
  518 + * 打包压缩下载
  519 + */
  520 +export function compressAndDown(arr, fileName) {
  521 + const zip = new JSZip();
  522 + const promiseArr = [];
  523 + arr.forEach((item) => {
  524 + const promise = getBlob(item.reportPath).then((res) => {
  525 + const fileName = item.testName;
  526 + let sIdx = item.reportPath.lastIndexOf(".");
  527 + const fileType = item.reportPath.substring(sIdx);
  528 + zip.file(`${fileName}${fileType}`, res, { binary: true });
  529 + });
  530 + promiseArr.push(promise);
  531 + });
  532 + Promise.all(promiseArr).then(() => {
  533 + zip
  534 + .generateAsync({
  535 + type: "blob",
  536 + compression: "DEFLATE",
  537 + compressionOptions: {
  538 + level: 9,
  539 + },
  540 + })
  541 + .then((res) => {
  542 + FileSave.saveAs(res, fileName ? fileName : "报表.zip");
  543 + });
  544 + });
  545 +}
  546 +
  547 +/**
  548 + * 班级格式化为三级列表 学段-年级-班级
  549 + * @param {*} data
  550 + * @returns
  551 + */
  552 +function setSectionName(num) {
  553 + let txt = "";
  554 + switch (num) {
  555 + case 1:
  556 + txt = "小学";
  557 + break;
  558 + case 2:
  559 + txt = "中学";
  560 + break;
  561 + case 3:
  562 + txt = "高中";
  563 + break;
  564 + case 4:
  565 + txt = "大学";
  566 + break;
  567 + }
  568 + return txt;
  569 +}
  570 +function setGradeName(num) {
  571 + let txt = "";
  572 + switch (num) {
  573 + case 1:
  574 + txt = "一年级";
  575 + break;
  576 + case 2:
  577 + txt = "二年级";
  578 + break;
  579 + case 3:
  580 + txt = "三年级";
  581 + break;
  582 + case 4:
  583 + txt = "四年级";
  584 + break;
  585 + case 5:
  586 + txt = "五年级";
  587 + break;
  588 + case 6:
  589 + txt = "六年级";
  590 + break;
  591 + case 7:
  592 + txt = "初一";
  593 + break;
  594 + case 8:
  595 + txt = "初二";
  596 + break;
  597 + case 9:
  598 + txt = "初三";
  599 + break;
  600 + case 10:
  601 + txt = "高一";
  602 + break;
  603 + case 11:
  604 + txt = "高二";
  605 + break;
  606 + case 12:
  607 + txt = "高三";
  608 + break;
  609 + case 13:
  610 + txt = "大一";
  611 + break;
  612 + case 14:
  613 + txt = "大二";
  614 + break;
  615 + case 15:
  616 + txt = "大三";
  617 + break;
  618 + case 16:
  619 + txt = "大四";
  620 + break;
  621 + }
  622 + return txt;
  623 +}
  624 +export function formatClass(data) {
  625 + let sectionName = [];
  626 + let sectionNameArr = [];
  627 + data.map((item) => {
  628 + if (!sectionName.includes(item.sectionName)) {
  629 + sectionName.push(item.sectionName);
  630 + sectionNameArr.push({
  631 + value: item.sectionName,
  632 + label: setSectionName(item.sectionName),
  633 + children: [
  634 + {
  635 + value: item.gradeName,
  636 + label: setGradeName(item.gradeName),
  637 + children: [
  638 + {
  639 + value: item.classId,
  640 + label: item.className,
  641 + },
  642 + ],
  643 + },
  644 + ],
  645 + });
  646 + } else {
  647 + let hasGrade = false;
  648 + let sectionIndex = 0;
  649 + let gradeIndex = 0;
  650 + sectionNameArr.map((items, index) => {
  651 + items.map((grade, gradeInx) => {
  652 + if (setGradeName(item.gradeName) == grade.value) {
  653 + hasGrade = true;
  654 + sectionIndex = index;
  655 + gradeIndex = gradeInx;
  656 + }
  657 + });
  658 + });
  659 + if (hasGrade) {
  660 + sectionNameArr[sectionIndex].children[gradeIndex].push({
  661 + value: item.classId,
  662 + label: item.className,
  663 + });
  664 + } else {
  665 + sectionNameArr[sectionIndex].children.push({
  666 + value: setGradeName(item.gradeName),
  667 + label: setGradeName(item.gradeName),
  668 + children: [
  669 + {
  670 + value: item.classId,
  671 + label: item.className,
  672 + },
  673 + ],
  674 + });
  675 + }
  676 + }
  677 + });
  678 + console.log(sectionNameArr);
  679 + return sectionNameArr;
  680 +}
... ...
src/views/device/index.vue
... ... @@ -4,7 +4,7 @@
4 4 <template slot="title">
5 5 <span>设备管理</span>
6 6 </template>
7   - <template slot="btns">
  7 + <template slot="btns" v-if="type == 1 && school !== '长水'">
8 8 <el-tooltip effect="dark" content="设备导入" placement="bottom">
9 9 <el-button
10 10 type="primary"
... ... @@ -12,15 +12,7 @@
12 12 size="mini"
13 13 plain
14 14 circle
15   - ></el-button>
16   - </el-tooltip>
17   - <el-tooltip effect="dark" content="添加答题器" placement="bottom">
18   - <el-button
19   - type="primary"
20   - icon="el-icon-mobile"
21   - size="mini"
22   - plain
23   - circle
  15 + @click="diaUp = true"
24 16 ></el-button>
25 17 </el-tooltip>
26 18 <el-tooltip effect="dark" content="添加基站" placement="bottom">
... ... @@ -30,19 +22,732 @@
30 22 size="mini"
31 23 plain
32 24 circle
  25 + @click="diaAnswerEqu = true"
33 26 ></el-button>
34 27 </el-tooltip>
35 28 </template>
36 29 </back-box>
37   - <el-dialog title="设备导入" :visible.sync="diaUp" width="400" center>
38   - <p>通过Excel名单导入设备,需要提供设备编码,点击<el-link>模板下载</el-link>。</p>
39   - </el-dialog>
  30 + <div class="page-content">
  31 + <div class="tab-box">
  32 + <el-radio-group v-model="type">
  33 + <el-radio-button :label="1">基站管理</el-radio-button>
  34 + <el-radio-button :label="2">答题器管理</el-radio-button>
  35 + <el-radio-button :label="3">授课端管理</el-radio-button>
  36 + </el-radio-group>
  37 + </div>
  38 + <div class="content">
  39 + <div v-if="type == 1">
  40 + <div class="chart-box">
  41 + <div class="device-num">
  42 + <p class="p1">{{ total }}</p>
  43 + <p class="p2">基站数量</p>
  44 + </div>
  45 + <div class="chart">
  46 + <pie-chart
  47 + id="pieChart"
  48 + :params="chartData"
  49 + @clickPieChart="clickPieChart"
  50 + ></pie-chart>
  51 + </div>
  52 + </div>
  53 + <div class="table-box">
  54 + <div class="answer-header">
  55 + <div class="sel-box">
  56 + <el-cascader
  57 + class="sel"
  58 + collapse-tags
  59 + clearable
  60 + placeholder="选择班级"
  61 + v-model="query.classId"
  62 + :options="classList"
  63 + :props="props"
  64 + :show-all-levels="false"
  65 + @change="_QueryData(false)"
  66 + ></el-cascader>
  67 + <el-select
  68 + class="sel"
  69 + v-model="query.status"
  70 + placeholder="选择状态"
  71 + @change="_QueryData(false)"
  72 + >
  73 + <el-option
  74 + v-for="item in statusList"
  75 + :key="item.value"
  76 + :label="item.label"
  77 + :value="item.value"
  78 + >
  79 + </el-option>
  80 + </el-select>
  81 + <el-input
  82 + type="number"
  83 + placeholder="请输入设备编码"
  84 + v-model="query.number"
  85 + class="input-with-select"
  86 + @keyup.enter.native="_QueryData(true)"
  87 + >
  88 + <el-button
  89 + slot="append"
  90 + icon="el-icon-search"
  91 + @click="_QueryData(true)"
  92 + ></el-button>
  93 + </el-input>
  94 + </div>
  95 + </div>
  96 + <el-table :data="tableData" border style="width: 100%">
  97 + <el-table-column
  98 + prop="number"
  99 + label="设备编码"
  100 + align="center"
  101 + ></el-table-column>
  102 + <el-table-column
  103 + prop="pindian"
  104 + label="频点"
  105 + align="center"
  106 + ></el-table-column>
  107 + <el-table-column
  108 + prop="peidui"
  109 + label="配对码"
  110 + align="center"
  111 + ></el-table-column>
  112 + <el-table-column
  113 + prop="jiaoshi"
  114 + label="所在教室"
  115 + align="center"
  116 + ></el-table-column>
  117 + <el-table-column
  118 + prop="class"
  119 + label="关联班级"
  120 + align="center"
  121 + ></el-table-column>
  122 + <el-table-column
  123 + prop="version"
  124 + label="固件版本号"
  125 + align="center"
  126 + ></el-table-column>
  127 + <el-table-column prop="datiqi" label="配对答题器" align="center"
  128 + ><template slot-scope="scope"
  129 + >{{ scope.row.datiqi }}个</template
  130 + ></el-table-column
  131 + >
  132 + <el-table-column label="状态" align="center"
  133 + ><template slot-scope="scope">
  134 + {{
  135 + scope.row.status == 1
  136 + ? "在线"
  137 + : scope.row.status == 2
  138 + ? "离线"
  139 + : "异常"
  140 + }}
  141 + </template></el-table-column
  142 + >
  143 + <el-table-column label="操作" align="center"
  144 + ><template slot-scope="scoped">
  145 + <el-tooltip effect="dark" content="修改基站" placement="top">
  146 + <el-button
  147 + type="primary"
  148 + circle
  149 + size="mini"
  150 + icon="fa fa-edit"
  151 + @click="edit(scoped.row)"
  152 + ></el-button>
  153 + </el-tooltip>
  154 + <el-tooltip effect="dark" content="日志" placement="top">
  155 + <el-button
  156 + type="warning"
  157 + circle
  158 + size="mini"
  159 + icon="fa fa-eye"
  160 + @click="linkTo(scoped.row, 1)"
  161 + ></el-button>
  162 + </el-tooltip> </template
  163 + ></el-table-column>
  164 + </el-table>
  165 + </div>
  166 + </div>
  167 + <div v-if="type == 2">
  168 + <div class="chart-box">
  169 + <div class="device-num">
  170 + <p class="p1">{{ total }}</p>
  171 + <p class="p2">答题器数量</p>
  172 + </div>
  173 + <div class="chart">
  174 + <scatter-chart
  175 + id="scatterChart"
  176 + :params="chartData2"
  177 + @clickScatterChart="clickScatterChart"
  178 + ></scatter-chart>
  179 + </div>
  180 + </div>
  181 + <div class="table-box">
  182 + <div class="answer-header">
  183 + <div class="sel-box">
  184 + <el-cascader
  185 + class="sel"
  186 + collapse-tags
  187 + clearable
  188 + placeholder="选择班级"
  189 + v-model="query.classId"
  190 + :options="classList"
  191 + :props="props"
  192 + :show-all-levels="false"
  193 + @change="_QueryData(false)"
  194 + ></el-cascader>
  195 + <el-select
  196 + class="sel"
  197 + v-model="query.status"
  198 + placeholder="选择状态"
  199 + @change="_QueryData(false)"
  200 + >
  201 + <el-option
  202 + v-for="item in statusList"
  203 + :key="item.value"
  204 + :label="item.label"
  205 + :value="item.value"
  206 + >
  207 + </el-option>
  208 + </el-select>
  209 + <el-input
  210 + type="number"
  211 + placeholder="请输入设备编码"
  212 + v-model="query.number"
  213 + class="input-with-select"
  214 + @keyup.enter.native="_QueryData(true)"
  215 + >
  216 + <el-button
  217 + slot="append"
  218 + icon="el-icon-search"
  219 + @click="_QueryData(true)"
  220 + ></el-button>
  221 + </el-input>
  222 + </div>
  223 + </div>
  224 + <el-table :data="tableData" border style="width: 100%">
  225 + <el-table-column
  226 + prop="number"
  227 + label="设备编码"
  228 + align="center"
  229 + ></el-table-column>
  230 + <el-table-column
  231 + prop="xuesheng"
  232 + label="学生信息"
  233 + align="center"
  234 + ></el-table-column>
  235 + <el-table-column
  236 + prop="dianliang"
  237 + label="电量"
  238 + align="center"
  239 + ></el-table-column>
  240 + <el-table-column
  241 + prop="class"
  242 + label="关联班级"
  243 + align="center"
  244 + ></el-table-column>
  245 + <el-table-column
  246 + prop="peidui"
  247 + label="配对码"
  248 + align="center"
  249 + ></el-table-column>
  250 + <el-table-column
  251 + prop="cishu"
  252 + label="答题次数"
  253 + align="center"
  254 + ></el-table-column>
  255 + <el-table-column
  256 + prop="time"
  257 + label="最后答题时间"
  258 + align="center"
  259 + ></el-table-column>
  260 + <el-table-column label="操作" align="center"
  261 + ><template slot-scope="scoped">
  262 + <el-tooltip effect="dark" content="日志" placement="top">
  263 + <el-button
  264 + type="warning"
  265 + circle
  266 + size="mini"
  267 + icon="fa fa-eye"
  268 + @click="linkTo(scoped.row, 2)"
  269 + ></el-button>
  270 + </el-tooltip> </template
  271 + ></el-table-column>
  272 + </el-table>
  273 + </div>
  274 + </div>
  275 + <div v-if="type == 3">
  276 + <div class="table-box">
  277 + <div class="answer-header">
  278 + <div class="sel-box">
  279 + <el-cascader
  280 + class="sel"
  281 + collapse-tags
  282 + clearable
  283 + placeholder="选择班级"
  284 + v-model="query.classId"
  285 + :options="classList"
  286 + :props="props"
  287 + :show-all-levels="false"
  288 + @change="_QueryData(false)"
  289 + ></el-cascader>
  290 + <span class="sel">共选择5个授课端</span>
  291 + <el-button plan round @click="autoUpDate"
  292 + >开启自动更新</el-button
  293 + >
  294 + <el-button plan round @click="stopUpdate"
  295 + >停止自动更新</el-button
  296 + >
  297 + </div>
  298 + </div>
  299 + <el-table
  300 + :data="tableData"
  301 + border
  302 + style="width: 100%"
  303 + @selection-change="handleSelectionChange"
  304 + >
  305 + <el-table-column type="selection" width="55"> </el-table-column>
  306 + <el-table-column
  307 + prop="class"
  308 + label="关联班级"
  309 + align="center"
  310 + ></el-table-column>
  311 + <el-table-column
  312 + prop="gengxin"
  313 + label="最近更新"
  314 + align="center"
  315 + ></el-table-column>
  316 + <el-table-column
  317 + prop="xitong"
  318 + label="软件系统"
  319 + align="center"
  320 + ></el-table-column>
  321 + <el-table-column
  322 + prop="yingjian"
  323 + label="硬件环境"
  324 + align="center"
  325 + ></el-table-column>
  326 + <el-table-column
  327 + prop="version"
  328 + label="版本号"
  329 + align="center"
  330 + ></el-table-column>
  331 + <el-table-column label="状态" align="center"
  332 + ><template slot-scope="scope">
  333 + {{
  334 + scope.row.status == 1
  335 + ? "在线"
  336 + : scope.row.status == 2
  337 + ? "离线"
  338 + : "异常"
  339 + }}
  340 + </template></el-table-column
  341 + >
  342 + <el-table-column label="自动更新" align="center"
  343 + ><template slot-scope="scoped">
  344 + <el-switch
  345 + v-model="scoped.row.isUp"
  346 + @change="changeUpdate($event, scoped.row, this)"
  347 + >
  348 + </el-switch> </template
  349 + ></el-table-column>
  350 + </el-table>
  351 + </div>
  352 + </div>
  353 + </div>
  354 + </div>
  355 + <el-dialog title="设备导入" :visible.sync="diaUp" width="400">
  356 + <up-load id="downDevice" :url="url" fileName="设备信息">
  357 + <p class="down-txt" slot="down">
  358 + 通过Excel名单导入设备,需要提供设备编码,点击
  359 + <el-link type="danger" @click="downExcel">模板下载</el-link> 。
  360 + </p>
  361 + </up-load>
  362 + <div class="dialog-footer" slot="footer">
  363 + <el-button @click="diaUp = false">取 消</el-button>
  364 + </div>
  365 + </el-dialog>
  366 + <el-dialog title="添加答题器" :visible.sync="diaAnswerEqu" width="400">
  367 + <el-form ref="forms" :model="form" :rules="formRules" label-width="140px">
  368 + <el-form-item label="设备编码:" prop="number">
  369 + <el-col :span="10"
  370 + ><el-input
  371 + type="text"
  372 + placeholder="输入设备编码"
  373 + v-model.trim="form.number"
  374 + maxlength="30"
  375 + size="45"
  376 + show-word-limit
  377 + >
  378 + </el-input
  379 + ></el-col>
  380 + </el-form-item>
  381 + <el-form-item label="频点:" prop="pindian">
  382 + <el-col :span="10"
  383 + ><el-input
  384 + type="text"
  385 + placeholder="输入频点"
  386 + v-model.trim="form.pindian"
  387 + maxlength="30"
  388 + size="45"
  389 + show-word-limit
  390 + >
  391 + </el-input
  392 + ></el-col>
  393 + </el-form-item>
  394 + <el-form-item label="配对码:" prop="peidui">
  395 + <el-col :span="10"
  396 + ><el-input
  397 + type="text"
  398 + placeholder="输入配对码"
  399 + v-model.trim="form.peidui"
  400 + maxlength="30"
  401 + size="45"
  402 + show-word-limit
  403 + >
  404 + </el-input
  405 + ></el-col>
  406 + </el-form-item>
  407 + <el-form-item label="选择班级:" prop="classId">
  408 + <el-col :span="10">
  409 + <el-cascader
  410 + clearable
  411 + v-model="form.classId"
  412 + :options="classList"
  413 + :props="{ expandTrigger: 'hover' }"
  414 + :show-all-levels="false"
  415 + ></el-cascader>
  416 + </el-col>
  417 + </el-form-item>
  418 + <el-form-item label="所在教室:">
  419 + <el-col :span="10"
  420 + ><el-input
  421 + type="text"
  422 + placeholder="输入所在教室"
  423 + v-model.trim="form.jiaoshi"
  424 + maxlength="30"
  425 + size="45"
  426 + show-word-limit
  427 + >
  428 + </el-input
  429 + ></el-col>
  430 + </el-form-item>
  431 + </el-form>
  432 + <div class="dialog-footer" slot="footer">
  433 + <el-button type="primary" @click="addAnswerEqu">确 定</el-button>
  434 + <el-button @click="diaAnswerEqu = false">取 消</el-button>
  435 + </div>
  436 + </el-dialog>
40 437 </div>
41 438 </template>
42 439  
43 440 <script>
44   -export default {};
  441 +import pieChart from "@/components/charts/pieChart";
  442 +import scatterChart from "@/components/charts/scatterChart";
  443 +import _ from "lodash";
  444 +import { downloadFile, formatClass } from "@/utils";
  445 +export default {
  446 + components: { pieChart, scatterChart },
  447 + data() {
  448 + return {
  449 + school: "",
  450 + loading: false,
  451 + url: "/web/upLoadDevice",
  452 + diaUp: false,
  453 + diaAnswerEqu: false,
  454 + classList: [],
  455 + classListAll: [],
  456 + props: { multiple: true, checkStrictly: false },
  457 + type: 1,
  458 + query: {
  459 + classId: [],
  460 + status: 0,
  461 + number: "",
  462 + },
  463 + statusList: [
  464 + { label: "全部", value: 0 },
  465 + { label: "在线", value: 1 },
  466 + { label: "离线", value: 2 },
  467 + { label: "异常", value: 3 },
  468 + ],
  469 + form: {
  470 + number: "",
  471 + pindian: "",
  472 + peidui: "",
  473 + classId: "",
  474 + jiaoshi: "",
  475 + },
  476 + formRules: {
  477 + number: [
  478 + { required: true, message: "请输入设备编码", trigger: "blur" },
  479 + ],
  480 + pindian: [{ required: true, message: "请输入频点", trigger: "blur" }],
  481 + peidui: [{ required: true, message: "请输入配对码", trigger: "blur" }],
  482 + classId: [{ required: true, message: "请选择班级", trigger: "blur" }],
  483 + },
  484 + tableData: [
  485 + {
  486 + number: 32456,
  487 + pindian: 12,
  488 + peidui: 100123,
  489 + jiaoshi: " 13号教室",
  490 + class: "21班",
  491 + version: "2.13",
  492 + datiqi: 50,
  493 + status: 1,
  494 + xuesheng: "sss",
  495 + dianliang: "25",
  496 + cishu: "10",
  497 + time: "10:00:00",
  498 + gengxin: "10:00:00",
  499 + xitong: "window7",
  500 + yingjian: "cpu:i7,4G内存,160G硬盘",
  501 + isUp: true,
  502 + },
  503 + ],
  504 + total: 35,
  505 + chartData: [
  506 + { name: "在线", value: "65" },
  507 + { name: "离线", value: "18" },
  508 + { name: "异常", value: "17" },
  509 + ],
  510 + chartData2: [
  511 + { name: "1日内", value: "8.6", count: 12 },
  512 + { name: "3日内", value: "18", count: 30 },
  513 + { name: "7日内", value: "36", count: 60 },
  514 + { name: "1月内", value: "80", count: 138 },
  515 + { name: "3月内", value: "20", count: 36 },
  516 + { name: "3月以上", value: "8", count: 9 },
  517 + ],
  518 + selectionTabIds: [],
  519 + };
  520 + },
  521 + created() {
  522 + this._QueryClassList();
  523 + },
  524 + methods: {
  525 + edit() {},
  526 + linkTo(obj, type) {
  527 + this.$router.push({
  528 + path: "/deviceLog",
  529 + query: {
  530 + id: obj.id,
  531 + type: type,
  532 + },
  533 + });
  534 + },
  535 + clickPieChart(obj) {
  536 + this.query.status = obj.name == "在线" ? 1 : obj.name == "离线" ? 2 : 3;
  537 + this._QueryData(false);
  538 + },
  539 + clickScatterChart(obj) {
  540 + console.log(obj);
  541 + // this._QueryData(false)
  542 + },
  543 + handleSelectionChange(val) {
  544 + console.log(val);
  545 + this.selectionTabIds = val.map((item) => {
  546 + return item.id;
  547 + });
  548 + },
  549 + changeUpdate: _.debounce(function (event, obj) {
  550 + console.log(this);
  551 + if (event) {
  552 + this.autoUpDate(obj.id);
  553 + } else {
  554 + this.stopUpdate(obj.id);
  555 + }
  556 + }, 800),
  557 + async autoUpDate(id) {
  558 + if (!this.selectionTabIds.length && !id) {
  559 + this.$message.warning("请选择授课端~");
  560 + return;
  561 + }
  562 + if (this.loadingUpDate) return;
  563 + this.loadingUpDate = true;
  564 + let data = await this.$request.autoUpDate({
  565 + id: id ? id : this.selectionTabIds,
  566 + });
  567 + this.loadingUpDate = false;
  568 + if (data && !data.code) {
  569 + this._QueryData(false);
  570 + this.$message.success("开启自动更新成功");
  571 + } else {
  572 + this.$message.error(data.message);
  573 + }
  574 + },
  575 + async stopUpdate(id) {
  576 + if (!this.selectionTabIds.length && !id) {
  577 + this.$message.warning("请选择授课端~");
  578 + return;
  579 + }
  580 + if (this.loadingUpDate) return;
  581 + this.loadingUpDate = true;
  582 + let data = await this.$request.stopUpdate({
  583 + id: id ? id : this.selectionTabIds,
  584 + });
  585 + this.loadingUpDate = false;
  586 + if (data && !data.code) {
  587 + this._QueryData(false);
  588 + this.$message.success("关闭自动更新成功");
  589 + } else {
  590 + this.$message.error(data.message);
  591 + }
  592 + },
  593 + async downExcel() {
  594 + let data = await this.$request.downDevice({
  595 + id: this.id,
  596 + });
  597 + if (data && !data.code) {
  598 + let blob = new Blob([data], {
  599 + type: "application/vnd.ms-excel;charset=utf-8",
  600 + });
  601 + downloadFile(`设备信息.xlsx`, blob);
  602 + } else {
  603 + this.$message.error(data.message);
  604 + }
  605 + },
  606 +
  607 + // 添加设备
  608 + async addAnswerEqu() {
  609 + // if(this.loadingAnswerEqu)return
  610 + // this.loadingAnswerEqu = true;
  611 + // const { data, status, info } = await this.$request.fetchClassList();
  612 + // this.loadingAnswerEqu = false;
  613 + // console.log(status);
  614 + // if (status === 0) {
  615 + // this._QueryData();
  616 + // } else {
  617 + // this.$message.error(info);
  618 + // }
  619 + },
  620 + // 查找班级
  621 + async _QueryClassList() {
  622 + // this.loading = true;
  623 + // const { data, status, info } = await this.$request.fetchClassList();
  624 + // console.log(status);
  625 + // if (status === 0) {
  626 + // if (!!data.list) {
  627 + // this.classList =
  628 + // data.list?.map((item) => {
  629 + // return {
  630 + // value: item.classId,
  631 + // label: item.className,
  632 + // };
  633 + // }) || [];
  634 + // }
  635 + // } else {
  636 + // this.$message.error(info);
  637 + // }
  638 + let data = [
  639 + {
  640 + classId: 1,
  641 + className: 1001,
  642 + sectionName: 1,
  643 + gradeName: 1,
  644 + },
  645 + {
  646 + classId: 1,
  647 + className: 1001,
  648 + sectionName: 2,
  649 + gradeName: 7,
  650 + },
  651 + {
  652 + classId: 1,
  653 + className: 1001,
  654 + sectionName: 3,
  655 + gradeName: 10,
  656 + },
  657 + {
  658 + classId: 1,
  659 + className: 1001,
  660 + sectionName: 4,
  661 + gradeName: 12,
  662 + },
  663 + ];
  664 + this.classList = formatClass(data);
  665 + },
  666 + // 设备列表信息
  667 + async _QueryData(type) {
  668 + console.log(1);
  669 + // this.loading = true;
  670 + // let query = {};
  671 + // if (!type) {
  672 + // this.query.title = "";
  673 + // query = { ...this.query };
  674 + // } else {
  675 + // query = { title: this.query.title };
  676 + // this.query.status = "";
  677 + // this.query.classId = [];
  678 + // }
  679 + // for (let key in query) {
  680 + // if (!query[key] || query[key].length == 0) {
  681 + // query[key] = null;
  682 + // }
  683 + // }
  684 + // this.loading = true;
  685 + // const { data, status, info } = await this.$request.fetchDeviceList();
  686 + // this.loading = false;
  687 + // console.log(status);
  688 + // if (status === 0) {
  689 + // this.tableData = data.list || [];
  690 + // } else {
  691 + // this.$message.error(info);
  692 + // }
  693 + },
  694 + },
  695 +};
45 696 </script>
46 697  
47   -<style>
  698 +<style lang="scss" scoped>
  699 +.page-content {
  700 + padding: 20px 20px 0;
  701 +}
  702 +.tab-box {
  703 + margin-bottom: 12px;
  704 +}
  705 +.content {
  706 + background: #f8f8f8;
  707 + border: 1px solid #e2e2e2;
  708 + border-radius: 10px;
  709 + overflow: hidden;
  710 + :deep(.fa-edit) {
  711 + width: 12px;
  712 + height: 12px;
  713 + &::before {
  714 + margin-left: 2px;
  715 + }
  716 + }
  717 + :deep(.fa-eye) {
  718 + width: 12px;
  719 + height: 12px;
  720 + &::before {
  721 + margin-left: 1px;
  722 + }
  723 + }
  724 + .chart-box {
  725 + display: flex;
  726 + overflow: hidden;
  727 + height: 240px;
  728 + border-bottom: 0.5px solid #e2e2e2;
  729 + .device-num {
  730 + width: 280px;
  731 + border-right: 0.5px solid #e2e2e2;
  732 + display: flex;
  733 + flex-direction: column;
  734 + justify-content: center;
  735 + align-items: center;
  736 + .p1 {
  737 + font-size: 28px;
  738 + }
  739 + }
  740 + .chart {
  741 + flex: 1;
  742 + height: 100%;
  743 + }
  744 + }
  745 + .table-box {
  746 + padding: 20px;
  747 + .answer-header {
  748 + padding: 0;
  749 + margin-bottom: 12px;
  750 + }
  751 + }
  752 +}
48 753 </style>
49 754 \ No newline at end of file
... ...
src/views/device/log.vue 0 → 100644
  1 +<template>
  2 + <div>
  3 + <back-box>
  4 + <template slot="title">
  5 + <span>设备日志</span>
  6 + </template>
  7 + <template slot="btns">
  8 + <el-tooltip effect="dark" content="基站配置信息导出" placement="bottom">
  9 + <el-button
  10 + type="primary"
  11 + icon="el-icon-download"
  12 + size="mini"
  13 + plain
  14 + circle
  15 + @click="down = true"
  16 + ></el-button>
  17 + </el-tooltip>
  18 + </template>
  19 + </back-box>
  20 + </div>
  21 +</template>
  22 +
  23 +<script>
  24 +export default {
  25 + data() {
  26 + return {
  27 + down: false,
  28 + };
  29 + },
  30 + methods: {},
  31 +};
  32 +</script>
  33 +
  34 +<style>
  35 +</style>
0 36 \ No newline at end of file
... ...
src/views/setUp/school.vue
1 1 <template>
2   - <div>学校管理</div>
  2 + <div>
  3 + <back-box>
  4 + <template slot="title">
  5 + <span>学校设置</span>
  6 + </template>
  7 + <template slot="btns">
  8 + <el-tooltip effect="dark" content="导入学校名单" placement="bottom">
  9 + <el-button
  10 + type="primary"
  11 + icon="el-icon-upload2"
  12 + size="mini"
  13 + plain
  14 + circle
  15 + @click="diaUp = true"
  16 + ></el-button>
  17 + </el-tooltip>
  18 + </template>
  19 + </back-box>
  20 + <div class="page-content">
  21 + <div class="content-box">
  22 + <i class="el-icon-edit"></i>
  23 + <ul class="school-info">
  24 + <li class="school-item">
  25 + <span class="s1">学校名称:</span>
  26 + <span class="s2">{{ school.title }}</span>
  27 + </li>
  28 + <li class="school-item">
  29 + <span class="s1">授课端管理密码:</span>
  30 + <span class="s2">{{ school.password }}</span>
  31 + </li>
  32 + <li class="school-item">
  33 + <span class="s1">联系人:</span>
  34 + <span class="s2">{{ school.lianxiren }}</span>
  35 + </li>
  36 + <li class="school-item">
  37 + <span class="s1">手机号码:</span>
  38 + <span class="s2">{{ school.phone }}</span>
  39 + </li>
  40 + <li class="school-item">
  41 + <span class="s1">学段:</span>
  42 + <span class="s2">{{ school.xueduan }}</span>
  43 + </li>
  44 + <li class="school-item">
  45 + <span class="s1">所属集团:</span>
  46 + <span class="s2">{{ school.jituan }}</span>
  47 + </li>
  48 + </ul>
  49 + <div class="grade-box">
  50 + <p class="h-title">年级管理</p>
  51 + <ul class="grade-info">
  52 + <li class="grade-item">
  53 + <p class="grade-name">一年级</p>
  54 + <div class="grade-class">
  55 + <p><i class="fa fa-building"></i>班级:10个</p>
  56 + <p><i class="fa fa-book"></i>科目:10个</p>
  57 + </div>
  58 + </li>
  59 + </ul>
  60 + </div>
  61 + </div>
  62 + </div>
  63 + <el-dialog title="导入学校名单" :visible.sync="diaUp" width="400">
  64 + <up-load id="downDevice" :url="url" fileName="学校名单">
  65 + <p class="down-txt" slot="down">
  66 + 通过Excel名单导入学校名单,需要提供设备编码,点击
  67 + <el-link type="danger" @click="downExcel">模板下载</el-link> 。
  68 + </p>
  69 + </up-load>
  70 + <div class="dialog-footer" slot="footer">
  71 + <el-button @click="diaUp = false">取 消</el-button>
  72 + </div>
  73 + </el-dialog>
  74 + </div>
3 75 </template>
4 76  
5 77 <script>
  78 +import { downloadFile } from "@/utils";
6 79 export default {
7   -
8   -}
  80 + data() {
  81 + return {
  82 + url: "xxx",
  83 + diaUp: false,
  84 + school: {
  85 + title: "长水实验中学",
  86 + password: "123456",
  87 + lianxiren: "张老师",
  88 + phone: "13548645321",
  89 + xueduan: "初中",
  90 + jituan: "长水集团",
  91 + },
  92 + };
  93 + },
  94 + methods: {
  95 + async downExcel() {
  96 + let data = await this.$request.downDevice({
  97 + id: this.id,
  98 + });
  99 + if (data && !data.code) {
  100 + let blob = new Blob([data], {
  101 + type: "application/vnd.ms-excel;charset=utf-8",
  102 + });
  103 + downloadFile(`设备信息.xlsx`, blob);
  104 + } else {
  105 + this.$message.error(data.message);
  106 + }
  107 + },
  108 + },
  109 +};
9 110 </script>
10 111  
11   -<style>
12   -
  112 +<style lang="scss" scoped>
  113 +.page-content {
  114 + padding: 20px;
  115 + .content-box {
  116 + background: #f8f8f8;
  117 + border-radius: 16px;
  118 + position: relative;
  119 + .el-icon-edit {
  120 + position: absolute;
  121 + top: 12px;
  122 + right: 12px;
  123 + padding:5px;
  124 + font-size:18px;
  125 + cursor: pointer;
  126 + &:hover{
  127 + color:#36f
  128 + }
  129 + }
  130 + }
  131 + .school-info {
  132 + display: flex;
  133 + flex-wrap: wrap;
  134 + padding: 16px 0;
  135 + border-bottom:.5px solid #f2f2f2;
  136 + .school-item {
  137 + width: 50%;
  138 + line-height: 48px;
  139 + padding-left: 100px;
  140 + display: flex;
  141 + box-sizing: border-box;
  142 + .s1 {
  143 + width: 160px;
  144 + font-size: 15px;
  145 + color: #888;
  146 + }
  147 + .s2 {
  148 + flex: 1;
  149 + }
  150 + }
  151 + }
  152 + .grade-box {
  153 + padding: 20px;
  154 + .grade-info{
  155 + display: flex;
  156 + flex-wrap:wrap;
  157 + padding:20px;
  158 + .grade-item{
  159 + width:300px;
  160 + margin-right:50px;
  161 + margin-bottom:40px;
  162 + box-sizing: border-box;
  163 + padding:12px 16px;
  164 + border-radius:10px;
  165 + box-shadow: 1px 1px 3px #888;
  166 + }
  167 + .grade-name{
  168 + font-size:16px;
  169 + font-weight: bold;
  170 + line-height: 18px;
  171 + padding-bottom:12px;
  172 + }
  173 + .grade-class{
  174 + display: flex;
  175 + justify-content: space-between;
  176 + font-size:15px;
  177 + padding-right:20px;
  178 + .fa{
  179 + font-size:18px;
  180 + margin-right:5px;
  181 + color:#a4a4a4
  182 + }
  183 + .fa-book{
  184 + font-size:20px;
  185 + }
  186 + }
  187 + }
  188 + }
  189 +}
13 190 </style>
14 191 \ No newline at end of file
... ...