学生成绩预测与分析可视化平台

一.创作思路

在平时办公中,我们往往需要对数据进行各种数据分析与图形可视化成图表,这些操作我们可以采用wps,word等等办公软件,于是我想自己尝试写一个线上的平台,专门实现上传文件,勾选相应的数据,采用Echarts生成图表,将Echars图表生成图表的全部步骤让用户自行选择生成图表,用户可以将生成的图表保存下来进行使用。目前开发中主要分析与预测学生数据为主,同时搭配上自主选择生成图表的工作台模块,以便后续的升级。

二.技术思路

1.技术路线:使用Vue3+pinia+router+Echarts+datav

2.功能模块:

1、 数据管理模块: 用于数据的收集、存储和管理,其中包括学生成绩数据、个人信息
等。
2、 成绩可视化模块: 能够根据用户需求,将成绩数据以柱状图、折线图、饼状图等形
式进行可视化展示。
3、 个性化分析模块: 根据用户的选择和参数设定,对成绩数据进行个性化分析。
4、 用户界面: 提供给用户的操作界面,包括数据输入、参数设置、图表展示等功能。

三.界面展示

1、登录注册页面:用户通过邮箱号和密码登录进入学生成绩可视化平台

1.1.实现思路:背景的动态粒子效果采用tsparticles,大家如果感兴趣的话,可以点击连接去官网查看。邮箱登录以及验证码方面,我主要是采用node里面的一个邮箱模块nodemailer,采用mysql存储用户信息,其实我感觉用mongdb可能更方便一些,但是当时没考虑这么多。

2.数据分析界面用户可以在操作台勾选数据,选择自己想要生成的图标,定制化设计图表,提供各种可视化图表,如折线图柱状图,饼状图等。

2.1.生成饼状图:用户可以点击鼠标左键,在平台上勾选相应的数据进行分析,用户可以自己定义图表的样式,布局,实时更改。然后图片中展示数据是采用mockjs随机生成。

2.2.勾选数据实现原理:使用xlsx模块将用户上传的文件数据解析出来存储在pinia,使用表格渲染到界面给每一个格子添加一个点击,移入,移出事件,方便用户勾选,主要数据代码如下

<template>
    <!-- 表格模板,使用 v-for 指令遍历 datatables.tables 中的数据 -->
    <table class="csv-table" ref="tablesbox">
        <tr v-for="(row, index) in datatables.tables" :key="index">
            <!-- 为每个单元格绑定点击和鼠标悬停事件,传递行列信息 -->
            <td v-for="(value, key) in row" :key="key" @click="draw" @mouseover="xuanran" :data-coloum="index"
                :data-row="key" :data-postion="index * 10 + key">{{ value }}</td>
        </tr>
    </table>
</template>

<script setup>
import { ref, onMounted, watch, toRefs } from "vue";
import { useCounterStore } from "@/stores/counter"

// 使用 Pinia store
const datatables = useCounterStore()

// 将 store 中的属性转为 refs
const { Mouseselected, tables } = toRefs(datatables)

// 定义 ref 变量
let isMouseDown = false;
const tablesbox = ref()
let num = []

// 在组件挂载时添加事件监听器
onMounted(() => {
    document.addEventListener('mousedown', () => {
        isMouseDown = true;
        tablesbox.value.classList.add('cursor')
    });
    document.addEventListener('mouseup', () => {
        isMouseDown = false;
        tablesbox.value.classList.remove('cursor')
    });
})

// 定义起点和父节点坐标
let start = { x: 0, y: 0 }
let parent = { x: 0, y: 0 }

// 绘制选中的单元格
const draw = (e) => {
    guiling()  // 清除所有单元格的样式
    e.target.style.backgroundColor = 'rgb(180, 178, 178,0.7)';
    e.target.style.border = '2px dotted rgba(205, 208, 20)';
    start.y = e.target.getAttribute('data-coloum')
    start.x = e.target.getAttribute('data-row')
    datatables.dataprencet = [e.target.innerHTML]
}

// 渲染鼠标悬停时的效果
const xuanran = (e) => {
    if (isMouseDown) {
        guiling()  // 清除所有单元格的样式
        parent.y = e.target.getAttribute('data-coloum')
        parent.x = e.target.getAttribute('data-row')
        num = getSurroundedCoordinates(start.x, start.y, parent.x, parent.y)
        for (let index = 0; index < num.length; index++) {
            let data = document.querySelector(`[data-postion="${num[index][1]}${num[index][0]}"]`)
            data.style.backgroundColor = 'rgb(180, 178, 178,0.2)';
            data.style.border = '2px dotted rgba(0, 0, 0)';
        }
        datatables.dataprencet = caiji(num)
    }
}

// 清空全部样式
function guiling() {
    const childNodes = document.querySelectorAll('td')
    childNodes.forEach(element => {
        element.style = 'border: 2px solid #dddddd;width: 50px;text-align: center;'
    });
}

// 获取被矩形包围的所有坐标
function getSurroundedCoordinates(x1, y1, x2, y2) {
    const grid = Array.from({ length: datatables.tables.length }, () => Array.from({ length: datatables.tables[0].length }, () => 0));
    grid[x1][y1] = 1;
    grid[x2][y2] = 1;

    const minX = Math.min(x1, x2);
    const maxX = Math.max(x1, x2);
    const minY = Math.min(y1, y2);
    const maxY = Math.max(y1, y2);

    const surroundedCoordinates = [];
    for (let i = minX; i <= maxX; i++) {
        for (let j = minY; j <= maxY; j++) {
            surroundedCoordinates.push([i, j]);
        }
    }
    return surroundedCoordinates;
}

// 采集选中区域的数据
function caiji(params) {
    let numdata = []
    for (let index = 0; index < params.length; index++) {
        let data = document.querySelector(`[data-postion="${params[index][1]}${params[index][0]}"]`)
        numdata.push(data.innerHTML)
    }
    num = []
    console.log(numdata);
    return numdata
}

// 监听 Mouseselected 的变化,清空样式并重置 dataprencet
watch(Mouseselected, (newValue) => {
    guiling()
    datatables.dataprencet = []
}, { deep: true });

// 监听 tables 的变化,更新 tables 的值
watch(tables, (newValue) => {
    tables.value = newValue
}, { deep: true });

</script>

<style scoped>
.csv-table {
    border-collapse: collapse;
    width: 100%;
    user-select: none;
    color: #ffffff;
}

.csv-table td {
    border: 2px solid #90b8ce;
    width: 50px;
    text-align: center;
    text-wrap: wrap;
}


.csv-table th {
    background-color: #f2f2f2;
}

.cursor {
    cursor: crosshair;
}

</style>

2.3生成图表的原理: 根据勾选的数据生成图表,灵感来自于Echars官方网站有在线运行生成图表的一个功能,以我这个饼状图举例,连接如下:点击跳转官方给我们提供的代码样式如下:

option = {
  title: {
    text: 'Referer of a Website',
    subtext: 'Fake Data',
    left: 'center'
  },
  tooltip: {
    trigger: 'item'
  },
  legend: {
    orient: 'vertical',
    left: 'left'
  },
  series: [
    {
      name: 'Access From',
      type: 'pie',
      radius: '50%',
      data: [
        { value: 1048, name: 'Search Engine' },
        { value: 735, name: 'Direct' },
        { value: 580, name: 'Email' },
        { value: 484, name: 'Union Ads' },
        { value: 300, name: 'Video Ads' }
      ],
      emphasis: {
        itemStyle: {
          shadowBlur: 10,
          shadowOffsetX: 0,
          shadowColor: 'rgba(0, 0, 0, 0.5)'
        }
      }
    }
  ]
};

可以看出来是一个对象,那么我们只需将对象里面每一个属性对应的值,使用v-model绑定或者使用watch监听,实现更新,在生成图表时,将对象传入进行更新即可。折线图与饼状图原理与其相同,这里不做过多介绍,感兴趣的朋友可以下载文件亲自上手使用。

2.4.  mockjs随机生成数据功能部分代码如下:

// npm install
import Mock from 'mockjs'

const dataTemplate = {
    'list|100': [
        ['@natural(1, 10)', '@id', '@cname', '@integer(0, 100)', '@integer(0, 100)', '@integer(0, 100)', '@integer(0, 100)', '@integer(0, 100)', '@integer(0, 100)']
    ]
};

// 生成模拟数据
const mockData = Mock.mock(dataTemplate).list;

// 添加表头
const we = [
    ['班级', '学号', '姓名', '语文', '数学', '英语', '物理', '化学', '生物'],
    ...mockData
];



// 输出模拟数据

export default we

3.上传与下载模板:这里的模板主要是为学生成绩可视化分析做准备,这里上传与下载使用了el-upload也就是element plus官方的组件库,下载模板主要是使用XLSX库.

3.1. 上传与下载模板文件的代码展示:

// 文件上传前的处理函数
const beforeUpload = (file) => {
    if (!file) return false;
    const fileName = file.name;
    const fileExtension = fileName.split('.').pop().toLowerCase();
    if (fileExtension !== 'csv' && fileExtension !== 'xlsx') {
        alert('只能上传 CSV 文件和 SQL 文件');
        return;
    }
    const reader = new FileReader();
    reader.onload = (e) => {
        const data = new Uint8Array(e.target.result);
        const workbook = XLSX.read(data, { type: 'array' });
        const sheet = workbook.Sheets[workbook.SheetNames[0]];
        const csvDataArray = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' }); // 添加 defval: '' 选项来保留空值
        csvData.value = csvDataArray;
        let a = JSON.stringify(csvData.value)
        student.value = JSON.parse(a)
        datawe.jiazai()
        router.push("/Maininterface/PersonalCenter/2")
    };
    reader.readAsArrayBuffer(file);

    // 返回 false 阻止默认上传行为
    return false;
};

// 准备 CSV 数据
const csvDatas = ref('班级,学号,姓名,语文,英语,数学,生物,化学,物理');
// 下载文件名
const filename = ref('可视化模板.csv');

// 下载 CSV 文件的函数
const downloadCSV = () => {
    // 创建 CSV 数据的 Blob 对象
    const blob = new Blob([csvDatas.value], { type: 'text/csv;charset=utf-8;' });
    // 创建下载链接
    const downloadLink = document.createElement('a');
    const url = URL.createObjectURL(blob);
    // 设置下载链接的属性
    downloadLink.setAttribute('href', url);
    downloadLink.setAttribute('download', filename.value);

    // 添加下载链接到文档中
    document.body.appendChild(downloadLink);

    // 模拟用户点击下载链接来触发下载
    downloadLink.click();

    // 清理创建的对象和链接
    URL.revokeObjectURL(url);
    document.body.removeChild(downloadLink);
};

4.学生成绩的可视化分析展示:

4.1. 在用户上传了模板文件后,我们使用遍历的方式将数据提取出来,由于我们模板文件前列的标题固定为 班级,学号,姓名  先使用XLSX库将数据提取为二维数组,让对学生数据进行求和,取平均值,排名,将对应的数据更新完毕后,传入对应的图表组件中,更新,代码如下。

import { ref } from 'vue'
import { defineStore } from 'pinia'
import { echarsdata } from '@/api/user.js'
export const usenamePoechars = defineStore('Poechars', () => {
   //存储学生信息
   const student = ref([])
   //存储当前选中查看的学生的基本信息
   const studentchoose = ref([])
   //存储学生成绩的排名
   const studentpaiming = ref([])
   //存储表头列
   const header = ref([])
   //存储排名信息(只有姓名与总分)
   const studenpaiming = ref([])
   //存储排名信息(只有学号与总分)
   const studenpaiming2 = ref([])
   //成绩平均值
   const pingjun = ref([])
   //获取用户上传的表单数据
   async function jiazai() {
      //模拟用户上传的数据
      // student.value = await echarsdata();
      // console.log(student.value);
      // const arrayToCSV = (arr) => {
      //    return arr.map(row => row.join(',')).join('\n');
      // };

      // // 准备 CSV 数据
      // const csvData = arrayToCSV(student.value);

      // // 下载文件名
      // const filename = 'student_data.csv';

      // // 下载 CSV 文件的函数
      // const downloadCSV = () => {
      //    // 创建 CSV 数据的 Blob 对象
      //    const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' });
      //    // 创建下载链接
      //    const downloadLink = document.createElement('a');
      //    const url = URL.createObjectURL(blob);
      //    // 设置下载链接的属性
      //    downloadLink.setAttribute('href', url);
      //    downloadLink.setAttribute('download', filename);

      //    // 添加下载链接到文档中
      //    document.body.appendChild(downloadLink);

      //    // 模拟用户点击下载链接来触发下载
      //    downloadLink.click();

      //    // 清理创建的对象和链接
      //    URL.revokeObjectURL(url);
      //    document.body.removeChild(downloadLink);
      // };

      // // 调用下载函数
      // downloadCSV();
      header.value = student.value[0]
      //存储学生数据信息
      const studentnum = student.value.slice(1)
      for (let index = 3; index < studentnum[0].length; index++) {
         pingjun.value.push(parseInt(studentnum.reduce((totil, per) => totil + per[index], 0) / studentnum.length))
      }
      studentchoose.value = student.value[1]
      studentnum.forEach(item => {
         studenpaiming.value.push({ name: item[2], value: item.slice(3).reduce((accumulator, currentValue) => accumulator + currentValue, 0) })
         studenpaiming2.value.push({ id: item[1], value: item.slice(3).reduce((accumulator, currentValue) => accumulator + currentValue, 0) })
      })
      studenpaiming.value.sort((a, b) => b.value - a.value);
      studenpaiming2.value.sort((a, b) => b.value - a.value);
   }
   //判断用户是否上传了标准文件

   //存储用户上传的需要预测的数据
   const updatawenjian = ref([])
   //记录需要预测的数据数量
   const numwenjian = ref([])

   //存储当前需要预测的学生的总分
   const numscore = ref([])
   //存储预测文件的开头标题
   let headeryuecei = ref([])
   const updatafangfa = (value) => {
      updatawenjian.value.push(value.splice(1))
      headeryuecei.value = value
      console.log(headeryuecei.value);
      numwenjian.value.push(numwenjian.value.length + 1)
   }
   //存储文件的预测名
   const wenjianname = ref([])
   function appname(value) {
      wenjianname.value.push(value)
   }
   function yuecei(qwedata) {
      //存储数量
      let numty = []
      //存储第一科的分数
      let frist = []
      //存储第2科的分数
      let frist2 = []
      //存储第3科的分数
      let frist3 = []
      //存储第4科的分数
      let frist4 = []
      //存储第5科的分数
      let frist5 = []
      //存储第6科的分数
      let frist6 = []
      //存储总科科分数
      let scores = []
      let num = []
      numscore.value = []
      let studentname = ""
      for (let index = 0; index < updatawenjian.value.length; index++) {
         let number = updatawenjian.value[index].filter(item => item[1] === qwedata)
         num.push(number)
         numty.push(index)
      }
      console.log(num);
      num.forEach(item => {
         studentname = item[0][2]
         let score = item[0].slice(3).reduce((pr, cr) => pr + cr, 0);
         frist.push(item[0].slice(3)[0])
         frist2.push(item[0].slice(3)[1])
         frist3.push(item[0].slice(3)[2])
         frist4.push(item[0].slice(3)[3])
         frist5.push(item[0].slice(3)[4])
         frist6.push(item[0].slice(3)[5])
         scores.push(score)
         numscore.value.push(score)
      })
      console.log(frist2);
      numty.push(numty.length + 1)
      // 计算总分平均分
      const averageScore = scores.reduce((acc, score) => acc + score, 0) / scores.length;
      //第一科平均分
      const fristScore = frist.reduce((acc, score) => acc + score, 0) / frist.length
      //第二科平均分
      const frist2Score = frist2.reduce((acc, score) => acc + score, 0) / frist2.length
      //第三科平均分
      const frist3Score = frist3.reduce((acc, score) => acc + score, 0) / frist3.length
      //第四科平均分
      const frist4Score = frist4.reduce((acc, score) => acc + score, 0) / frist4.length
      //第五科平均分
      const frist5Score = frist5.reduce((acc, score) => acc + score, 0) / frist5.length
      //第六科平均分
      const frist6Score = frist6.reduce((acc, score) => acc + score, 0) / frist6.length
      scores.push(huigui(numscore.value, numwenjian.value, averageScore))
      frist.push(huigui(frist, numwenjian.value, fristScore))
      frist2.push(huigui(frist2, numwenjian.value, frist2Score))
      frist3.push(huigui(frist3, numwenjian.value, frist3Score))
      frist4.push(huigui(frist4, numwenjian.value, frist4Score))
      frist5.push(huigui(frist5, numwenjian.value, frist5Score))
      frist6.push(huigui(frist6, numwenjian.value, frist6Score))
      return [{ score: scores, value: numty, name: "总分" },
      { score: frist, value: numty, name: headeryuecei.value[0][3] },
      { score: frist2, value: numty, name: headeryuecei.value[0][4] },
      { score: frist3, value: numty, name: headeryuecei.value[0][5] },
      { score: frist4, value: numty, name: headeryuecei.value[0][6] },
      { score: frist5, value: numty, name: headeryuecei.value[0][7] },
      { score: frist6, value: numty, name: headeryuecei.value[0][8] }]
   }
   function huigui(scores, exams, averageScore) {
      // 使用线性回归拟合模型
      function linearRegression(x, y) {
         const n = x.length;
         let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;

         for (let i = 0; i < n; i++) {
            sumX += x[i];
            sumY += y[i];
            sumXY += x[i] * y[i];
            sumX2 += x[i] * x[i];
         }
         //计算斜率和截距
         const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
         const intercept = (sumY - slope * sumX) / n;

         return { slope, intercept };
      }
      // 计算回归方程
      const regressionEquation = linearRegression(exams, scores);
      // 预测下次考试的分数
      const nextExamNumber = exams.length + 1;
      let predictedScore = regressionEquation.slope * nextExamNumber + regressionEquation.intercept;

      // 确保预测到的分数不为负数,如果为负数,统一返回0
      predictedScore = predictedScore < 0 ? 0 : predictedScore;

      return predictedScore;
   }
   return { student, studentchoose, pingjun, jiazai, header, studenpaiming, studenpaiming2, updatawenjian, numwenjian, yuecei, updatafangfa, numscore, appname, wenjianname }
}, {
   persist: true
})

4.2:Echar图表更新步骤,以上图学生个人可视化分析的里面的雷达图举例。

<template>
  <div ref="myEchartss" :style="{ width: '100%', height: '100%' }"></div>
</template>

<script setup>
import { usenamePoechars } from '@/stores/Poechars.js'
import * as echarts from 'echarts';
import { ref, onMounted, watch, toRefs } from 'vue';

const datawe = usenamePoechars();
const { student, pingjun, studentchoose, header } = toRefs(datawe);

let datas = [
  { Name: '语文', TotalScore: 150, Score: 76, AvgScore: 72 },
  { Name: '数学', TotalScore: 150, Score: 71, AvgScore: 63 },
  { Name: '英语', TotalScore: 150, Score: 56, AvgScore: 58 },
  { Name: '物理', TotalScore: 100, Score: 81, AvgScore: 68 },
  { Name: '化学', TotalScore: 100, Score: 77, AvgScore: 65 },
  { Name: '生物', TotalScore: 100, Score: 77, AvgScore: 65 }
];

let colorList = ['#36A87A', '#3f76f2'];
let aveList = datas.map((n) => { return n.AvgScore; });
let uList = datas.map((n) => { return n.Score; });
let nameList = [];

datas.forEach((item) => {
  nameList.push({
    name: item.Name,
    max: 150,
    AvgScore: item.AvgScore,
    Score: item.Score
  });
});

let option = {
  title: {
    text: `综合得分:${datas.reduce((er, per) => er + per.Score, 0)}分`,
    left: 'center',
    textStyle: {
      // 图例文字的样式
      fontSize: 18,
      color: '#fff'
    }
  },
  legend: {
    data: ['你的得分', '平均得分'],
    left: 'center',
    top: 'bottom',
    itemGap: 50,
    textStyle: {
      // 图例文字的样式
      fontSize: 14,
      color: '#fff'
    }
  },
  radar: {
    center: ['50%', '55%'], // 图表位置
    radius: '50%', // 图表大小
    // 设置雷达图中间射线的颜色
    axisLine: {
      lineStyle: {
        color: '#999',
        fontSize: 30
      }
    },
    indicator: nameList,
    // 雷达图背景的颜色,在这儿随便设置了一个颜色,完全不透明度为0,就实现了透明背景
    splitArea: {
      areaStyle: {
        color: '#a0cfff' // 图表背景的颜色
      }
    },
    name: {
      lineHeight: 18,
      formatter: (labelName, raw) => {
        const { AvgScore, Score } = raw;
        return (
          labelName + '\n' + `{score|${Score}}` + '/' + `{avg|${AvgScore}}`
        );
      },
      rich: {
        score: {
          color: colorList[0],
          fontSize: 16
        },
        avg: {
          color: colorList[1],
          fontSize: 16
        }
      }
    }
  },
  series: [
    {
      type: 'radar',
      data: [
        {
          value: uList,
          name: '你的得分',
          // 设置区域边框和区域的颜色
          itemStyle: {
            color: colorList[0]
          },
          label: {
            show: false,
            fontSize: 16,
            position: 'right',
            color: colorList[0],
            formatter: function (params) {
              return params.value;
            }
          },
          areaStyle: {
            color: colorList[0],
            opacity: 0.2
          }
        },
        {
          value: aveList,
          name: '平均得分',
          // 设置区域边框和区域的颜色
          itemStyle: {
            color: colorList[1]
          },
          label: {
            show: false,
            fontSize: 16,
            position: 'left',
            color: colorList[1],
            formatter: function (params) {
              return params.value;
            }
          },
          areaStyle: {
            color: colorList[1],
            opacity: 0.2
          }
        }
      ]
    }
  ]
}

const myEchartss = ref(null);

onMounted(() => {
  initChart(option);
});

function initChart(options) {
  const chartDom = myEchartss.value;
  if (!chartDom) return;

  const chart = echarts.init(chartDom);
  chart.setOption(options);

  window.onresize = () => {
    chart.resize();
  };
}

watch(studentchoose, (newvalue) => {
  datas = []
  for (let index = 3; index < header.value.length; index++) {
    datas.push({ Name: header.value[index], TotalScore: 150, Score: studentchoose.value[index], AvgScore: pingjun.value[index - 3] })
  }
  aveList = datas.map((n) => { return n.AvgScore; });
  uList = datas.map((n) => { return n.Score; });
  nameList = [];
  datas.forEach((item) => {
    nameList.push({
      name: item.Name,
      max: 150,
      AvgScore: item.AvgScore,
      Score: item.Score
    });
  });
  option = {
    title: {
      text: `综合得分:${datas.reduce((er, per) => er + per.Score, 0)}分`,
      left: 'center'
    },
    legend: {
      data: ['你的得分', '平均得分'],
      left: 'center',
      top: 'bottom',
      itemGap: 50,
      textStyle: {
        // 图例文字的样式
        fontSize: 14
      }
    },
    radar: {
      center: ['50%', '55%'], // 图表位置
      radius: '50%', // 图表大小
      // 设置雷达图中间射线的颜色
      axisLine: {
        lineStyle: {
          color: '#999',
          fontSize: 30
        }
      },
      indicator: nameList,
      // 雷达图背景的颜色,在这儿随便设置了一个颜色,完全不透明度为0,就实现了透明背景
      splitArea: {
        areaStyle: {
          color: '#d7cece' // 图表背景的颜色
        }
      },
      name: {
        lineHeight: 18,
        formatter: (labelName, raw) => {
          const { AvgScore, Score } = raw;
          return (
            labelName + '\n' + `{score|${Score}}` + '/' + `{avg|${AvgScore}}`
          );
        },
        rich: {
          score: {
            color: colorList[0],
            fontSize: 16
          },
          avg: {
            color: colorList[1],
            fontSize: 16
          }
        }
      }
    },
    series: [
      {
        type: 'radar',
        data: [
          {
            value: uList,
            name: '你的得分',
            // 设置区域边框和区域的颜色
            itemStyle: {
              color: colorList[0]
            },
            label: {
              show: false,
              fontSize: 16,
              position: 'right',
              color: colorList[0],
              formatter: function (params) {
                return params.value;
              }
            },
            areaStyle: {
              color: colorList[0],
              opacity: 0.2
            }
          },
          {
            value: aveList,
            name: '平均得分',
            // 设置区域边框和区域的颜色
            itemStyle: {
              color: colorList[1]
            },
            label: {
              show: false,
              fontSize: 16,
              position: 'left',
              color: colorList[1],
              formatter: function (params) {
                return params.value;
              }
            },
            areaStyle: {
              color: colorList[1],
              opacity: 0.2
            }
          }
        ]
      }
    ]
  }
  initChart(option);
}, {
  deep: true
})

</script>

<style></style>

及通过watch监听studentchoose的数据变化,来刷新界面。

5.学生成绩预测界面:

将学生每一期的数据上传后(至少上传两个文件)然后基于最基础的公式y=kx+b来进行预测,后续会考虑采用更先进的方式进行综合的预测。

四.总结:

1 .这个项目整体上来说只能算的上勉强实现基础功能,由于是一个人开发,在界面美化,以及功能的完善还有很长的一段路需要走,有更好的想法欢迎各位探讨,也请各位,如果觉得不错请点赞加收藏,谢谢了。

  • 27
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值