可视化项目(学员后台管理系统)

192 阅读11分钟

1、git管理

gitee.com/klein06/vis…

  • 在gitee创建了远程仓库
  • 在本地提交文件到本地仓库
  • 将本地仓库提交到远程仓库
  • 具体操作请查看git
1.实现远程仓库的别名设置
git remote add 别名 地址  
​
2。查看设置是否成功
git remote -v
​
3.第一次提交需要建立 关联
git push -u 别名 master
​
4.后面的提交就只需要
git push

2、axios根路径的全局配置

  • 在utils目录下创建request.js文件
  • 在request文件中创建根路径的全局配置(axios.defaults.baseURL)

utils/request.js

axios.defaults.baseURL = 'http://www.itcbc.com:8000'

3、提示插件的使用

toastr插件的使用:utils/mytoastr.js

1.引入插件的资源

2.添加必须要结构和样式

3.初始化

4.按文档说明使用

3.1 引入插件的资源

<link rel="stylesheet" href="./assets/lib/toastr/toastr.min.css" />
<script src="./assets/lib/toastr/toastr.min.js"></script>

3.2 初始化

toastr.options = {
  positionClass: 'toast-top-right', // 提示框位置,这里填类名
  showDuration: '300', // 提示框渐显所用时间
  hideDuration: '300', // 提示框隐藏渐隐时间
  timeOut: '2000' // 提示框持续时间
}

3.3 调用方法显示提示

toastr.info('提示信息');                // 普通提示
toastr.success('提示信息');             // 成功提示
toastr.warning('提示信息');             // 警告提示
toastr.error('提示信息');               // 错误提示

4、用户信息校验插件

  • 表单验证插件的下载路径

bootstrapValidator

4.1 引入插件的资源

<link rel="stylesheet" href="./assets/lib/validator/bootstrapValidator.min.css" /> 
<script src="./assets/lib/validator/bootstrapValidator.min.js"></script>

4.2 初始化-配置校验规则

// 添加表单的校验规则
  // 比如,验证一个用户名和密码
  function test() {
    return {
      // 指定需要校验的字段名称
      fields: {
        username: {
          // 这里username是 input 的name属性值,表示对这个输入框进行验证
          validators: {
            // 添加真正的校验规则
            notEmpty: {
              //不能为空
              message: '用户名不能为空.' // 如果不满足校验规则,则给出这句提示
            },
            stringLength: {
              //检测长度
              min: 2, // 最少2位
              max: 15, // 最多15位
              message: '用户名需要2~15个字符'
            }
          }
        },
        password: {
          validators: {
            notEmpty: {
              message: '密码不能为空'
            },
            stringLength: {
              //检测长度
              min: 6,
              max: 15,
              message: '密码需要6~15个字符'
            }
          }
        }
      }
    }
  }

4.3 使用插件语法绑定提交事件

$('.register form')
    .bootstrapValidator(test())
    .on('success.form.bv', function(e) {
      e.preventDefault()
    // 接下来收集数据发起ajax请求
    })

5、注册

一:实现登陆和注册面板的显示和隐藏

二:实现注册功能

5.1 实现注册和登陆面板的显示和隐藏

  • 获取去注册和去登录按钮元素,以及注册面板和登录面板元素
  • 给去登录按钮和去注册按钮绑定事件
    • 点击去注册,让登录面板隐藏,同时让注册面板显示
    • 反过来,点击去登录,让注册面板隐藏,同时让登录面板显示
// 1.获取元素
// 1.1 去注册
let goRegister = document.querySelector('.goRegister')
// 1.2 去登陆
let goLogin = document.querySelector('.goLogin')
// 1.3 注册面板
let register = document.querySelector('.register')
// 1.4 登陆面板
let login = document.querySelector('.login')
​
// 2.实现登陆和注册面板的显示和隐藏
goRegister.addEventListener('click', function() {
    login.style.display = 'none'
    register.style.display = 'block'
})
goLogin.addEventListener('click', function() {
    register.style.display = 'none'
    login.style.display = 'block'
})

5.2 实现注册功能

  • 收集表单数据
  • 发起ajax请求
    • 请求成功后判断code状态值
    • 判断code状态值如果是0,表示账号还没注册,提示注册成功
    • 判断code状态值如果是1,表示账号已注册,提示账号已存在
// 3.实现注册
// 比如,注册
// $('.register form'):指定监听那个表单的submit默认提交行为
// bootstrapValidator:调用插件的验证方法
// test():校验规则 ,要自己定义
// success.form.bv:submit的默认提交行为
$('.register form')
    .bootstrapValidator(test())
    .on('success.form.bv', function(e) {
    e.preventDefault()
    // 通过验证,这里的代码将会执行。我们将Ajax请求的代码放到这里即可
    // 1.收集数据
    let username = rusername.value
    let password = rpassword.value
​
    // 2.发起ajax请求
    axios({
        method: 'post',
        url: '/api/register',
        data: { username, password }
    }).then(res => {
        if (res.data.code == 0) {
            toastr.success(res.data.message)
        } else if (res.data.code == 1) {
            toastr.warning(res.data.message)
        }
    })
})

6、登陆

  • 也要使用提示框插件
  • 也要使用用户信息校验插件
  • 按插件的请求绑定事件
  • 发起ajax请求
    • 请求成功后判断code状态值
    • 判断code状态值如果是0,将token存储到本地,提示登录成功,跳转到首页
    • 否则提示账号或密码错误
// 4.实现登陆 --  /api/login
$('.login form')
    .bootstrapValidator(test())
    .on('success.form.bv', function(e) {
    e.preventDefault()
    // 1.收集数据
    let username = lusername.value
    let password = lpassword.value
    // 2.发起ajax请求
    axios({
        method: 'post',
        url: '/api/login',
        data: { username, password }
    }).then(res => {
        if (res.data.code == 0) {
            localStorage.setItem('token_79', res.data.token)
            toastr.success(res.data.message)
            // 跳转到首页
            location.href = './index.html'
        } else {
            toastr.error(res.data.message)
        }
    })
})

7、状态保持

HTTP协议是无状态协议,并不会记忆用户之前的操作,对于它而言每个请求都是单独的,崭新的请求。所以我们需要实现 状态保持

  • 1.在可视化项目中,除了登陆和注册接口,其它所有接口的请求都需要身份认证
  • 2.这种身份认证的场景在后台数据管理项目中必定存在
  • 3.如果发生401错误,这种错误说明 在请求接口数据的时候,你得告诉服务器你是谁,得证明你的权限进行接口的请求

image-20220714151751232.png

7.1 登陆成功之后接收token并存储(实现状态保持的第一种方法)

在login.js文件登陆成功代码中进行添加,登录成功后进行状态保持的本地存储

if (res.data.code == 0) {
    // 将后台响应的token存储到本地存储
    localStorage.setItem('token_79', res.data.token)
    toastr.success(res.data.message)
    // 跳转到首页
    location.href = './index.html'
}

7.2 在后续请求时传递token

  • 必须须请求头中传递token
  • 请求头的键名必须是 Authorization
  • 具体的请求头配置信息可以到axios文档中进行查看
// 实现数据渲染
function init() {
    axios({
        url: '/student/list',
        // 添加axios请求头的配置
        headers: { Authorization: localStorage.getItem('token_79') }
    }).then(res => {
        console.log(res)
    })
}

8、拦截器

8.1 请求拦截器(实现状态保持的第二种方法)

// 添加请求拦截器:所有请求都会经过
// config:请求报文对象
axios.interceptors.request.use(
  function(config) {
    // 在发送请求之前做些什么:以请求头的方式传递token
    let token = localStorage.getItem('token_79')
    // 判断是否有token,如果有token才进行传递
    if (token) {
      // 进行token的请求头的设置
      config.headers.Authorization = token
    }
    return config
  },
  function(error) {
    // 对请求错误做些什么
    return Promise.reject(error)
  }
)

9、学员信息

9.1 数据渲染

一:渲染之前要准备好 结构和数据

二:渲染的实现方式就是遍历拼接

  • 发起ajax请求
    • 请求成功后对结构和数据进行遍历拼接
// 实现数据渲染
function init() {
    axios({
        url: '/student/list'
    }).then(res => {
        console.log(res)
        let htmlStr = ''
        res.data.data.forEach(function(value, index) {
            htmlStr += `<tr>
                      <th scope="row">${index + 1}</th>
                      <td>${value.name}</td>
                      <td>${value.age}</td>
                      <td>${value.sex}</td>
                      <td>${value.group}</td>
                      <td>${value.phone}</td>
                      <td>${value.salary}</td>
                      <td>${value.truesalary}</td>
                      <td>${value.province + value.city + value.county}</td>
                      <td>
                        <button type="button" class="btn btn-primary btn-sm">修改</button>
                        <button type="button" class="btn btn-danger btn-sm">删除</button>
                      </td>
                    </tr>`
        })
        tbody.innerHTML = htmlStr
    })
}
​
init()

9.2 数据添加

使用js方式弹出模态框

  • 点击添加学员打开模态框
  • 让模态框的标题改成录入新学员
  • 模态框按钮的文本内容改成确定添加
  • 调用重置按钮的click事件,让表单内容清空
// 获取添加学员按钮
let btnAddDialog = document.querySelector('.btnAddDialog')
// 获取模态框确定按钮
let determine = document.querySelector('.determine')
// 获取重置按钮
let btnreset = document.querySelector('.btnreset')
// 打开学员添加模态框
btnAddDialog.addEventListener('click', function() {
    $('#addModal').modal('show')
    addModalLabel.innerText = '录入新学员'
    determine.innerHTML = '确定添加'
    btnreset.click()
})

加载省的数据

页面一打开就去加载省的数据:只加载一次就可以了

  • 发起ajax请求
  • 请求成功后做省的数据动态渲染
// 获取省级菜单
let province = document.querySelector('[name="province"]')
// 加载省的数据
axios({
    url: '/geo/province'
}).then(res => {
    let htmlStr = '<option selected value="">--省--</option>'
    res.data.forEach(function(value, i) {
        htmlStr += `<option value="${value}">${value}</option>`
    })
    province.innerHTML = htmlStr
})

加载当前所选择的省的市

  • 监听省的选项的变化(change事件),加载这个省所对应的市(看文档)
    • 用户选择省的默认值的时候,就让市和县同时也跳回默认值
    • 获取省的value值,因为想要加载市,前提是需要获取到省的值
    • 发起ajax请求
      • 请求成功后做市的数据动态渲染
// 获取市级菜单
let city = document.querySelector('[name="city"]')
// 获取县级菜单
let county = document.querySelector('[name="county"]')
// 加载市的数据
province.addEventListener('change', function() {
    // 用户选择省的默认值,就让市和县也跳回默认值
    city.innerHTML = '<option selected value="">--市--</option>'
    county.innerHTML = '<option selected value="">--县--</option>'
    // 获取省的value值
    // 因为加载市的前提,是需要获取到省的值
    let pname = province.value
    // 如果用户选择的是默认值(---省---)
    if (!pname) {
        return
    }
    // 发起ajax请求
    axios({
        url: '/geo/city',
        params: {pname}
    }).then(res => {
        let htmlStr = '<option selected value="">--市--</option>'
        res.data.forEach(value => {
            htmlStr += `<option value="${value}">${value}</option>`
        })
        city.innerHTML = htmlStr
    })
})

加载市所对应的区县

  • 实现市县级联
  • 监听市下拉列表的change事件,加载这个市所对应的区县
    • 用户如果选择了市的默认值,就让县同时也跳回默认值
    • 获取省和市的value值,因为想要加载县,前提是需要加载出省和市的值
    • 发起ajax请求
      • 请求成功后做县的数据动态渲染
// 加载县的数据
city.addEventListener('change', function() {
    // 用户选择市的默认值,就让县也跳回默认值
    county.innerHTML = '<option selected value="">--县--</option>'
    // 获取省和市的value值
    // 因为加载县的前提,是需要加载省和市的值
    let pname = province.value
    let cname = city.value
    // 如果用户选择的是默认值(---市---)
    if (!cname) {
        return
    }
    // 发起ajax的请求
    axios({
        url: '/geo/county',
        params: {pname, cname}
    }).then(res => {
        let htmlStr = '<option selected value="">--县--</option>'
        res.data.forEach(value => {
            htmlStr += `<option value="${value}">${value}</option>`
        })
        county.innerHTML = htmlStr
    })
})

添加数据校验

// 添加数据校验
function student() {
    return {
        fields: {
            name: {
                validators: {
                    notEmpty: {
                        message: '姓名不能为空'
                    },
                    stringLength: {
                        min: 2,
                        max: 10,
                        message: '姓名长度2~10位'
                    }
                }
            },
            age: {
                validators: {
                    notEmpty: {
                        message: '年龄不能为空'
                    },
                    greaterThan: {
                        value: 18,
                        message: '年龄不能小于18岁'
                    },
                    lessThan: {
                        value: 60,
                        message: '年龄不能超过60岁'
                    }
                }
            },
            sex: {
                validators: {
                    choice: {
                        min: 1,
                        max: 1,
                        message: '请选择性别'
                    }
                }
            },
            group: {
                validators: {
                    notEmpty: {
                        message: '组号不能为空'
                    },
                    regexp: {
                        regexp: /^\d{1,2}$/,
                        message: '请选择有效的组号'
                    }
                }
            },
            phone: {
                validators: {
                    notEmpty: {
                        message: '手机号不能为空'
                    },
                    regexp: {
                        regexp: /^1\d{10}$/,
                        message: '请填写有效的手机号'
                    }
                }
            },
            salary: {
                validators: {
                    notEmpty: {
                        message: '实际薪资不能为空'
                    },
                    greaterThan: {
                        value: 800,
                        message: '期望薪资不能低于800'
                    },
                    lessThan: {
                        value: 100000,
                        message: '期望薪资不能高于100000'
                    }
                }
            },
            truesalary: {
                validators: {
                    notEmpty: {
                        message: '实际薪资不能为空'
                    },
                    greaterThan: {
                        value: 800,
                        message: '实际薪资不能低于800'
                    },
                    lessThan: {
                        value: 100000,
                        message: '实际薪资不能高于100000'
                    }
                }
            },
            province: {
                validators: {
                    notEmpty: {
                        message: '省份必填'
                    }
                }
            },
            city: {
                validators: {
                    notEmpty: {
                        message: '市必填'
                    }
                }
            },
            county: {
                validators: {
                    notEmpty: {
                        message: '县必填'
                    }
                }
            }
        }
    }
}

实现新增

  • 做数据校验
  • 使用formdata收集数据
  • 发起ajax请求
    • 请求成功后判断code如果是0,表示数据添加成功
    • 给出提示信息
    • 我们自己要调用重置元素的事件
    • 让模态框隐藏
    • 页面重新渲染
// 获取form表单
let addForm = document.querySelector('.add-form')
// 实现学员信息的添加
$('.add-form')
    .bootstrapValidator(student())
    .on('success.form.bv', function(e) {
    e.preventDefault()
    // 收集数据
    let formdata = new FormData(addForm)
    let data = {}
    formdata.forEach(function(value, key) {
        data[key] = value
    })

    // 发起ajax请求
    axios({
        method: 'post',
        url: '/student/add',
        data
    }).then(res => {
        // 请求成功后判断code如果是0,表示添加成功
        if(res.data.code == 0) {
            // 给出提示信息
            toastr.success(res.data.message)
            // 我们自己要调用重置元素的事件
            btnreset.click()
            // 让模态框隐藏
            $('#addModal').modal('hide')
            // 页面重新渲染
            init()
        }
    })
})

9.3 数据删除

  • 先存储自定义属性id
  • 再添加委托事件
  • 获取自定义属性id,因为删除是需要通过获取用户id进行删除的
  • 发起ajax请求
    • 判断code值如果是0
      • 给出删除成功提示信息
      • 做页面的动态渲染
// 实现学员信息的删除
tbody.addEventListener('click', function(e) {
    if(e.target.classList.contains('btn-danger')) {
        // 获取自定义属性id,因为删除是需要通过获取用户id进行删除的
        let id = e.target.dataset.id
        // 发起ajax请求
        axios({
            method: 'DELETE',
            url: '/student/delete',
            params: {id}
        }).then(res => {
            if(res.data.code == 0) {
                toastr.success(res.data.message)
                init()
            }
        })
    }
})

9.4 数据编辑

下拉列表的基本特性
1、取值
- 如果option选项有value属性设置,则优先获取value属性
- 如果没有value属性则获取option标签之间的内容

2、赋值:优先将选项的value匹配你所赋的值,将匹配项显示
- 如果有选项的value与赋的值匹配则进行展示 
- 如果没有,则没有任何的内容

数据回填

编辑和新增共用一个模态框

  • 打开模态框

    • 使用js方式弹出模态框,因为不仅仅是弹出模态框,而且还要实现数据的回填
  • 获取你想回填的数据

    • 自定义属性存储所有值再获取所有值

    • 存储数据id,根据id查询数据,实现数据回填 -- 现在这个业务的实现方式

      • 后台得有这样的接口
  • 发起ajax请求

    • 实现数据的回填
      • 常规的表单元素,可以直接赋值给value属性
      • 单选按钮需要做特殊处理,判断如果是男选中男的单选按钮,否则选中女的单选按钮
      • 省市联级的市和县也需要做特殊的处理
      • 如果没有value选项,也需要进行赋值,则可以设置它的innerHTML
// 获取学员姓名表单元素
let name = document.querySelector('[name="name"]')
// 获取学员年龄表单元素
let age = document.querySelector('[name="age"]')
// 获取学员组号表单元素
let group = document.querySelector('[name="group"]')
// 获取学员性别表单元素
let sexs = document.querySelectorAll('[name="sex"]')
// 获取学员手机号表单元素
let phone = document.querySelector('[name="phone"]')
// 获取学员期望薪资表单元素
let salary = document.querySelector('[name="salary"]')
// 获取学员实际薪资表单元素
let truesalary = document.querySelector('[name="truesalary"]')
// 单击表格中的编辑按钮,弹出模态框,实现数据的回填
tbody.addEventListener('click', function(e) {
    if (e.target.classList.contains('btnedit')) {
        addModalLabel.innerText = '编辑学员信息'
        determine.innerHTML = '确定编辑'
        // 弹出模态框
        $('#addModal').modal('show')
        // 获取当前数据的id
        let id = e.target.dataset.id
        // 根据id查询对应的数据
        axios({
            url: '/student/one',
            params: { id }
        }).then(res => {
            let data = res.data.data
            // 常规表单元素(输入框),可以直接赋值value属性的
            name.value = data.name
            age.value = data.age
            phone.value = data.phone
            salary.value = data.salary
            truesalary.value = data.truesalary
            // 单选按钮
            data.sex == '男' ? (sexs[0].checked = true) : (sexs[1].checked = true)
            // 下拉列表
            group.value = data.group
            province.value = data.province
            // 如果没有value选项,也需要进行赋值,则可以设置它的innerHTML
            city.innerHTML = `<option selected value="${data.city}">--${data.city}--</option>`
            county.innerHTML = `<option selected value="${data.county}">--${data.county}--</option>`
        })
    }
})

实现编辑功能

  • 提升体验:修改模态框的标题和按钮的文本内容

    • 单击新增打开模态框的时候,设置为 “录入新学员”
    • 单击编辑打开模态框的时候,设置为“ 编辑学员”
  • 在确认按钮的处理事件中,判断标题的文件内容,决定当前操作是新增还是编辑

  • 编辑操作同样需要收集用户数据,不同的是,编辑还需要传入数据id

    • 在编辑弹框的时候可以获取到数据id
    • 将id存储到全局变量
    • 在满足编辑条件的时候,在参数中追加id参数
let editid
// 实现学员信息的添加  或 编辑
$('.add-form')
    .bootstrapValidator(student())
    .on('success.form.bv', function(e) {
    e.preventDefault()
    // 收集数据
    let formdata = new FormData(addForm)
    let data = {}
    formdata.forEach(function(value, key) {
        data[key] = value
    })

    if (dialogTitle.innerText == '录入新学员') {
        // 发起ajax请求
        axios({
            method: 'post',
            url: '/student/add',
            data
        }).then(res => {
            console.log(res)
            if (res.data.code == 0) {
                toastr.success(res.data.message)
                // 我们自己要调用重置元素的事件
                btnreset.click()
                $('#addModal').modal('hide')
                init()
            }
        })
    } else {
        // 实现编辑提交
        // 追加数据id
        data.id = editid
        axios({
            url: '/student/update',
            method: 'put',
            data
        }).then(res => {
            if (res.data.code == 0) {
                toastr.success(res.data.message)
                // 我们自己要调用重置元素的事件
            	btnreset.click()
                $('#addModal').modal('hide')
                init()
            }
        })
    }
})

10、成绩录入

10.1 成绩数据的渲染

let tbody = this.document.querySelector('tbody')
// 成绩渲染
function init() {
    axios({
        url: '/score/list'
    }).then(res => {
        let data = res.data.data
        let htmlStr = ''
        for (let key in data) {
            htmlStr += `<tr>
                      <th scope="row">${key}</th>
                      <td>${data[key].name}</td>
                      <td class="score">${data[key].score[0]}</td>
                      <td class="score">${data[key].score[1]}</td>
                      <td class="score">${data[key].score[2]}</td>
                      <td class="score">${data[key].score[3]}</td>
                      <td class="score">${data[key].score[4]}</td>
                    </tr>`
        }
        tbody.innerHTML = htmlStr
    })
}
init()

10.2 成绩的录入和修改

  • 为成绩单元格添加委托事件

  • 在单元格中添加一个输入框

    • 创建一个input输入框

    • 判断 当前td中是否有输入框

      • 如果没有:追加到当前td中
      • 如果有:不创建也不追加了
  • 先将td的原始值要存储起来

  • 为输入框赋值内容

  • 为输入框添加必要的样式

  • 让输入框聚焦-选中

  • 为输入框添加一个失焦事件,在事件处理函数中让td的内容还原

  • 为输入框添加一个按键事件,当按下enter键的时候,实现成绩的录入和修改

    • 获取td的自定义属性
    • 学员是存储在td的父容器tr中,通过parentNode获取数据
    • 先将td.innerHTML值清空。再设置td的innerText
// 实现成绩的录入和修改
// 1.页面效果
// 2.功能实现
tbody.addEventListener('click', function(e) {
    if (e.target.className == 'score') {
        let td = e.target
        let score = td.innerText
        if (!td.querySelector('input')) {
            // 清空Td的原始内容
            td.innerText = ''
            // 创建一个input输入框
            let input = document.createElement('input')
            // 追加到当前td中
            td.appendChild(input)
            // 为input赋值,值就是td中的原始的内容
            input.value = score
            // 为输入框设置样式:去除边框
            input.style.border = 'none'
            input.style.background = 'transparent'
            input.style.outline = 'none'
            input.style.textAlign = 'center'
            input.select()

            // 为输入框添加一个失焦事件
            input.addEventListener('blur', function() {
                td.innerText = score
            })

            // 实现录入或修改
            input.addEventListener('keyup', function(e) {
                if (e.key == 'Enter') {
                    // 发起ajax请求
                    axios({
                        url: '/score/entry',
                        method: 'post',
                        data: {
                            // 当前学员id
                            stu_id: td.parentNode.dataset.id,
                            // 第几组
                            batch: td.dataset.batch,
                            // 成绩
                            score: input.value
                        }
                    }).then(res => {
                        console.log(res)
                        toastr.success('成绩录入或修改成功')
                        td.innerHTML = ''
                        td.innerText = input.value
                    })
                }
            })
        }
    }
})

11、图表页

11.1 网站概览

班级概况:

let total = document.querySelector('.total')
let avgSalary = document.querySelector('.avgSalary')
let avgAge = document.querySelector('.avgAge')
let proportion = document.querySelector('.proportion')
// 获取班级概况:只需要渲染一次
;(function() {
    axios({
        url: '/student/overview'
    }).then(res => {
        console.log(res)
        let data = res.data.data
        total.innerText = data.total
        avgSalary.innerText = data.avgSalary
        avgAge.innerText = data.avgAge
        proportion.innerText = data.proportion
    })
})()

11.2籍贯

需要使用到echarts插件

添加静态图表

  • 引入插件的资源:

  • 添加必要的结构或样式: 准备一个定义了高宽的 DOM 容器

  • 初始化

    • 通过 echarts.init 方法初始化一个 echarts 实例
    • 通过 setOption 方法生成一个简单的图表
// 生成静态饼图
function createPie(data) {
    // 基于准备好的dom,初始化echarts实例
    let myChart = echarts.init(document.querySelector('.pie'));

    // 指定图表的配置项和数据
    let option = {
        title: {
            text: '籍贯分析',
        },
        tooltip: {
            trigger: 'item',
            formatter: '{a} <br/>{b} : {c} ({d}%)'
        },
        series: [
            {
                name: 'Nightingale Chart',
                type: 'pie',
                radius: [10, 100],
                center: ['50%', '50%'],
                roseType: 'area',
                itemStyle: {
                    borderRadius: 8
                },
                data
            }
        ]
    };

    // 使用刚指定的配置项和数据显示图表。
    myChart.setOption(option);
}

渲染动态图表

所谓动态饼图就是准备好饼图所需要渲染的数据

它要求的数据格式是对象数组

1.value是这个省的学员的数量

2.name是数据中的省份名称

实现方式

1.判断一下当前数组中有没有当前遍历到的省份,如果有,则数量+1

2.如果没有,则添加一条新的记录,name是当前省份名称,value值为1

// 获取所有学员信息
function init() {
    axios({
        url: '/student/list'
    }).then(res => {
        console.log(res)
        let data = res.data.data
        // 1.从源数据中提取出饼图数据
        let pieArr = []
        // 2.提取饼图数据....
        data.forEach(function(value) {
            // 判断一个pieArr中是否已经有了当前省份的记录,如果有,数量+1
            // let obj = pieArr.filter(function(v){
            //   return v.name == value.province
            // })[0]
            let obj = null
            for (let i = 0; i < pieArr.length; i++) {
                if (pieArr[i].name == value.province) {
                    // 将数组中的对象赋值给obj,造成obj和数组中的对象指向同一个地址,后期修改其中一个另外一个也会变化
                    obj = pieArr[i]
                }
            }
            // 如果当前遍历到的省份已存在,则将数量+1
            if (obj) {
                obj.value++
            }
            // 否则,添加一条新的数据
            else {
                pieArr.push({ value: 1, name: value.province })
            }
        })

        // 调用方法生成动态饼图
        createPie(pieArr)
    })
}
init()

11.3 薪资

生成静态折线图

// 生成静态折线图
function createLine(names, salarys, truesalarys) {
    // 基于准备好的dom,初始化echarts实例
    let myChart = echarts.init(document.querySelector('.line'));

    // 指定图表的配置项和数据
    let option = {
        title: {
            text: '薪资 Salary'
        },
        dataZoom: [
            {
                start: 10,
                end: 30
            }
        ],
        tooltip: {
            trigger: 'axis'
        },
        legend: {
            data: ['期望薪资', '实际薪资']
        },
        xAxis: {
            type: 'category',
            data: names
        },
        yAxis: {
            type: 'value'
        },
        series: [
            {
                name: '期望薪资',
                data: salarys,
                type: 'line',
                smooth: true,
                lineStyle: {
                    color: 'red'
                }
            },
            {
                name: '实际薪资',
                data: truesalarys,
                type: 'line',
                smooth: true,
                lineStyle: {
                    color: 'blue'
                }
            }
        ]
    };

    // 使用刚指定的配置项和数据显示图表。
    myChart.setOption(option);
}

生成动态折线图

// 获取所有学员信息
function init() {
    axios({
        url: '/student/list'
    }).then(res => {
        console.log(res)
        let data = res.data.data
        // 从源数据中提取出饼图数据--这是最终的饼图的数据
        let pieArr = []
        // 最终的折线图数据
        let names = [],
            salarys = [],
            trueSalarys = []
        // 1.遍历从接口获取的原始数据
        data.forEach(function (value) {
            // 2.提取饼图数据.... 开始
            // 判断一个pieArr中是否已经有了当前省份的记录,如果有,数量+1,如果没有则在pieArr中添加一条新记录
            // value就是当前所遍历到的原始用户信息对象
            // 拿当前遍历到的value的province属性值,去pieArr数组中查找,看看能不能找到
            let obj = null
            for (let i = 0; i < pieArr.length; i++) {
                // 说明这个省真的存在过
                if (pieArr[i].name == value.province) {
                    obj = pieArr[i]
                    break
                }
            }
            // 如果存在过,之前也找到了这个对象,那么就修改数量
            if (obj) {
                obj.value++
            }
            // 没有存在过
            else {
                pieArr.push({ value: 1, name: value.province })
            }
            // 2.提取饼图数据.... 结束

            // 3.提取折线图数据 ----开始
            names.push(value.name)
            salarys.push(value.salary)
            trueSalarys.push(value.truesalary)
            // 3.提取折线图数据 ----结束
        })

        // 调用方法生成动态饼图
        createPie(pieArr)
        // 调用方法生成折线图
        createLine(names, salarys, trueSalarys)
    })
}
init()

11.4 成绩

生成静态的柱状图

// 生成静态柱状图
function createBar() {
    let mychart = echarts.init(document.querySelector('.barChart'))
    let option = {
        tooltip: {
            trigger: 'axis'
        },
        legend: {
            data: ['平均分', '低于60分人数', '60-80分之间', '高于80分人数']
        },
        grid: {
            top: '10%',
            left: '2%',
            right: '2%',
            bottom: '2%',
            containLabel: true
        },
        xAxis: [
            {
                type: 'category',
                axisTick: { show: false },
                data: ['2012', '2013', '2014', '2015', '2016']
            }
        ],
        yAxis: [
            {
                type: 'value',
                min: 0,
                max: 100,
                interval: 10,
                axisLabel: {
                    formatter: '{value} 分'
                }
            },
            {
                type: 'value',
                min: 0,
                max: 10,
                interval: 1,
                axisLabel: {
                    formatter: '{value} 人'
                }
            }
        ],
        series: [
            {
                name: '平均分',
                type: 'bar',
                barWidth: 20,
                data: [55, 66, 77, 88, 44]
            },
            {
                name: '低于60分人数',
                type: 'bar',
                barWidth: 20,
                data: [2, 3, 4, 5, 6],
                yAxisIndex: 1
            },
            {
                name: '60-80分之间',
                type: 'bar',
                barWidth: 20,
                data: [3, 4, 5, 6, 7],
                yAxisIndex: 1 // 设置与那个轴的数据对应
            },
            {
                name: '高于80分人数',
                type: 'bar',
                barWidth: 20,
                data: [1, 6, 5, 3, 2],
                yAxisIndex: 1
            }
        ]
    }
    mychart.setOption(option)
}

渲染动态柱状图

// 按组查询成绩
function searchScore(batch) {
    axios({
        url: `/score/batch?batch=${batch}`
    }).then(res => {
        createBar(res.data.data)
    })
}
searchScore(4)

显示不同组的成绩

let btn = document.querySelector('.btn')
let batch = document.querySelector('#batch')
// 显示和隐藏右侧菜单项
btn.addEventListener('click', function() {
    if (batch.style.display == 'block') {
        batch.style.display = 'none'
    } else {
        batch.style.display = 'block'
    }
})
batch.addEventListener('click', function(e) {
    searchScore(e.target.dataset.index)
})

11.5 地图

生成静态地图

function mapChart(chinaGeoCoordMap, chinaDatas) {
    let myChart = echarts.init(document.querySelector('.map'))

    var convertData = function (data) {
        var res = []
        for (var i = 0; i < data.length; i++) {
            var dataItem = data[i]
            var fromCoord = chinaGeoCoordMap[dataItem[0].name]
            var toCoord = [113.434345, 23.135522]
            if (fromCoord && toCoord) {
                res.push([
                    {
                        coord: fromCoord,
                        value: dataItem[0].value
                    },
                    {
                        coord: toCoord
                    }
                ])
            }
        }
        return res
    }
    var series = []
        ;[['广东省', chinaDatas]].forEach(function (item, i) {
            console.log(item)
            series.push(
                {
                    type: 'lines',
                    zlevel: 2,
                    effect: {
                        show: true,
                        period: 4, //箭头指向速度,值越小速度越快
                        trailLength: 0.02, //特效尾迹长度[0,1]值越大,尾迹越长重
                        symbol: 'arrow', //箭头图标
                        symbolSize: 5 //图标大小
                    },
                    lineStyle: {
                        normal: {
                            width: 1, //尾迹线条宽度
                            opacity: 1, //尾迹线条透明度
                            curveness: 0.3 //尾迹线条曲直度
                        }
                    },
                    data: convertData(item[1])
                },
                {
                    type: 'effectScatter',
                    coordinateSystem: 'geo',
                    zlevel: 2,
                    rippleEffect: {
                        //涟漪特效
                        period: 4, //动画时间,值越小速度越快
                        brushType: 'stroke', //波纹绘制方式 stroke, fill
                        scale: 4 //波纹圆环最大限制,值越大波纹越大
                    },
                    label: {
                        normal: {
                            show: true,
                            position: 'right', //显示位置
                            offset: [5, 0], //偏移设置
                            formatter: function (params) {
                                //圆环显示文字
                                return params.data.name
                            },
                            fontSize: 13
                        },
                        emphasis: {
                            show: true
                        }
                    },
                    symbol: 'circle',
                    symbolSize: function (val) {
                        return 5 + val[2] * 5 //圆环大小
                    },
                    itemStyle: {
                        normal: {
                            show: false,
                            color: '#f00'
                        }
                    },
                    data: item[1].map(function (dataItem) {
                        return {
                            name: dataItem[0].name,
                            value: chinaGeoCoordMap[dataItem[0].name].concat([
                                dataItem[0].value
                            ])
                        }
                    })
                },
                //被攻击点
                {
                    type: 'scatter',
                    coordinateSystem: 'geo',
                    zlevel: 2,
                    rippleEffect: {
                        period: 4,
                        brushType: 'stroke',
                        scale: 4
                    },
                    label: {
                        normal: {
                            show: true,
                            position: 'right',
                            //offset:[5, 0],
                            color: '#0f0',
                            formatter: '{b}',
                            textStyle: {
                                color: '#0f0'
                            }
                        },
                        emphasis: {
                            show: true,
                            color: '#f60'
                        }
                    },
                    symbol: 'pin',
                    symbolSize: 50,
                    data: [
                        {
                            name: item[0],
                            value: chinaGeoCoordMap[item[0]].concat([10])
                        }
                    ]
                }
            )
        })

    //
    let option = {
        // 标题
        title: {
            // left: 'center',
            text: '来广路线 From',
            textStyle: {
                color: '#6d767e'
            }
        },
        tooltip: {
            trigger: 'item',
            backgroundColor: 'rgba(166, 200, 76, 0.82)',
            borderColor: '#FFFFCC',
            showDelay: 0,
            hideDelay: 0,
            enterable: true,
            transitionDuration: 0,
            extraCssText: 'z-index:100',
            formatter: function (params, ticket, callback) {
                //根据业务自己拓展要显示的内容
                var res = ''
                var name = params.name
                var value = params.value[params.seriesIndex + 1]
                res =
                    "<span style='color:#fff;'>" + name + '</span><br/>数据:' + value
                return res
            }
        },
        backgroundColor: '#fff',
        visualMap: {
            // 图例值控制(官方叫做视觉映射组件)
            min: 0,
            max: 1,
            calculable: true,
            show: false,
            color: ['#f44336', '#fc9700', '#ffde00', '#ffde00', '#00eaff'],
            textStyle: {
                color: '#fff'
            }
        },
        geo: {
            map: 'china',
            zoom: 1.2,
            label: {
                emphasis: {
                    show: false
                }
            },
            roam: true, //是否允许缩放
            itemStyle: {
                normal: {
                    color: 'rgba(51, 69, 89, .5)', //地图背景色
                    borderColor: '#516a89', //省市边界线00fcff 516a89
                    borderWidth: 1
                },
                emphasis: {
                    color: 'rgba(37, 43, 61, .5)' //悬浮背景
                }
            }
        },
        series: series
    }

    myChart.setOption(option)
}

渲染动态地图

// 获取所有学员信息
function init() {
    axios({
        url: '/student/list'
    }).then(res => {
        console.log(res)
        let data = res.data.data
        // 从源数据中提取出饼图数据--这是最终的饼图的数据
        let pieArr = []
        // 最终的折线图数据
        let names = [],
            salarys = [],
            trueSalarys = []
        // 地图数据
        let chinaGeoCoordMap = {},
            chinaDatas = []
        // 1.遍历从接口获取的原始数据
        data.forEach(function (value) {
            // 2.提取饼图数据.... 开始
            // 判断一个pieArr中是否已经有了当前省份的记录,如果有,数量+1,如果没有则在pieArr中添加一条新记录
            // value就是当前所遍历到的原始用户信息对象
            // 拿当前遍历到的value的province属性值,去pieArr数组中查找,看看能不能找到
            let obj = null
            for (let i = 0; i < pieArr.length; i++) {
                // 说明这个省真的存在过
                if (pieArr[i].name == value.province) {
                    obj = pieArr[i]
                    break
                }
            }
            // 如果存在过,之前也找到了这个对象,那么就修改数量
            if (obj) {
                obj.value++
            }
            // 没有存在过
            else {
                pieArr.push({ value: 1, name: value.province })
            }
            // 2.提取饼图数据.... 结束

            // 3.提取折线图数据 ----开始
            names.push(value.name)
            salarys.push(value.salary)
            trueSalarys.push(value.truesalary)
            // 3.提取折线图数据 ----结束
            // 4。提取地图数据----开始
            chinaGeoCoordMap[value.province] = [value.jing, value.wei]
            // chinaDatas.push([{ name: value.province, value: 0 }])
            // { value: 1, name: value.province } >> [{ value: 1, name: value.province }]
            chinaDatas = pieArr.map(function (v) {
                return [v]
            })
            // 4。提取地图数据----结束
        })

        // 调用方法生成动态饼图
        createPie(pieArr)
        // 调用方法生成折线图
        createLine(names, salarys, trueSalarys)
        // 调用方法生成地图
        mapChart(chinaGeoCoordMap, chinaDatas)
    })
}
init()

12、退出

let tuichu = this.document.querySelector('.logout')
// 退出
tuichu.addEventListener('click', function() {
    // 清除之前的token值
    localStorage.removeItem('token_79')

    // 重定向到登陆页
    location.href = './login.html'
})

13、响应拦截统一处理401

// 添加响应拦截器,拦截所有的服务器响应,对401进行统一的处理
// 添加响应拦截器
axios.interceptors.response.use(
    function(response) {
        // 响应没有报错
        // 对响应数据做点什么
        return response
    },
    function(error) {
        // 响应报错
        console.dir(error)
        if (error.response.status == 401) {
            // toastr.error('用户信息验证失败,请重新登陆')
            // setTimeout(() => {
            window.parent.location.href = './login.html'
            // }, 1000)
        }

        // 对响应错误做点什么
        return Promise.reject(error)
    }
)