智慧学成数据展示项目
该项⽬是基于web的轻量级系统,数据展示平台,解决了数据展示的复杂性。采⽤完全前后端分离的开发模式,使⽤ Vue.js 技术栈构建的PC端 SPA 单⻚⾯应⽤程序,UI ⽅⾯使⽤了ElementUI。
该项目主要功能是统计线上课程的各方面信息,把用户,课程,热门学科,学习频次等数据展示出来,方便运营人员直观的了解用户的学习趋势,热门的产品,是当前web开发中,比较火热的数据展示项目。
项⽬来自黑马程序员前端项目库,
共包含业务模块:9 个,后台接⼝:16个,适合了解Vue基础语法的同学练习。
项目初始化
1. 使用@vue/cli创建项目的基本结构
vue create zhxc_pro
2. 使用交互式命令行选择需要引入的模块,安装项目依赖
zhxc_pro
├─ .browserslistrc
├─ .editorconfig
├─ .eslintrc.js
├─ .gitignore
├─ babel.config.js
├─ package-lock.json
├─ package.json
├─ public
│ ├─ favicon.ico
│ └─ index.html
├─ README.md
└─ src
├─ App.vue
├─ assets
│ └─ logo.png
├─ components
│ └─ HelloWorld.vue
├─ main.js
├─ router
│ └─ index.js
└─ views
├─ About.vue
└─ Home.vue
3.安装其他依赖
-
安装less less-loader
npm install less less-loader
-
安装axios
npm install axios
-
安装dayjs echarts element-ui
npm install dayjs
npm install echarts
npm install element-ui
4. 梳理项目结构
-
清空App.vue文件
<template> <div id="app"> </div> </template> <script> export default { } </script> <style lang="less" scoped> </style>
-
清空components和views文件夹
-
清空router文件夹下 index.js 中的routes 数组里的路由规则
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) const routes = [ ] const router = new VueRouter({ routes }) export default router
-
将准备好的图片,字体图标等静态资源复制到项目中
-
在src --> assets --> styles中创建全局样式文件 global.css
html, body, #app { height: 100%; margin: 0; padding: 0; }
-
在main.js中导入element和global.css
import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import './assets/styles/global.css'
Login页面
1. 创建Login.vue文件
-
在src–> views 文件夹下创建 Login.vue
-
在路由中添加对应的路由规则
const routes = [ { path: '/', redirect: '/login' }, { path: '/login', name: 'login', component: () => import('@/views/Login.vue') } ]
2. 完成页面基本结构
-
完成template区域的结构
<template> <div class="login-container"> <div class="login-box"> <h3>智慧学成统计系统</h3> <el-form ref="form"> <el-form-item> <el-input></el-input> </el-form-item> <el-form-item> <el-input></el-input> </el-form-item> <el-form-item> <el-input></el-input> </el-form-item> <el-form-item> <el-button type="primary">登录</el-button> </el-form-item> </el-form> </div> </div> </template>
-
添加相应的样式
<style lang="less" scoped> .login-container { height: 100%; background: url("../assets/images/background.png") no-repeat; .login-box { width: 650px; height: 600px; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); background-color: rgba(255, 255, 255, 0.89); border-radius: 10px; .el-form { margin: 0 auto; width: 385px; height: 40px; .el-form-item { margin-bottom: 22px; /deep/ .el-input__inner { background-color: transparent; border: 0; border-bottom: 1px solid #cdcdcd; } } .el-button { margin-top: 50px; width: 100%; height: 60px; background: linear-gradient(270deg, #5efce8, #736efe); } } > h3 { text-align: center; margin: 90px 0; font-size: 36px; color: #1b7bef; } } } </style>
-
为输入框添加前置图标,因为这里的图标使用的是图片文件所以我们需要使用el-input组件提供的具名插槽prefix
<el-form ref="form"> <el-form-item> <el-input placeholder="用户名"> <img class="prefix" slot="prefix" src="@/assets/images/username.png" alt=""> </el-input> </el-form-item> <el-form-item> <el-input placeholder="密码"> <img class="prefix" slot="prefix" src="@/assets/images/pwd.png" alt=""> </el-input> </el-form-item> <el-form-item> <el-input placeholder="验证码"> <img class="prefix" slot="prefix" src="@/assets/images/yzm.png" alt=""> </el-input> </el-form-item>
设置图片大小为30px
.el-form-item { margin-bottom: 22px; .prefix { width: 30px; } }
-
创建自定义指令,实现为prefix中图片添加边框,显示选中状态
directives: { act: { update: function (el, binding) { if (binding.value) { el.style.cssText = 'border:1px dashed #ccc;' } else { el.style.cssText = 'border:0' } } } }
-
给el-input输入框绑定focus事件 修改v-act指令的值
<el-form-item> <el-input @focus="focusHandle('username')" placeholder="用户名"> <img v-act="activeStr == 'username'" class="prefix" slot="prefix" src="@/assets/images/username.png" alt /> </el-input> </el-form-item> <script> export default { data () { return { activeStr: '' } }, methods: { focusHandle (val) { this.activeStr = val } }, } </script>
3. 为login表单绑定数据,并添加表单验证
<el-form ref="form" :model="loginForm" :rules="loginFormRules">
<el-form-item prop="username">
<el-input @focus="focusHandle('username')" v-model="loginForm.username" placeholder="用户名">
<img
v-act="activeStr == 'username'"
class="prefix"
slot="prefix"
src="@/assets/images/username.png"
alt
/>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input @focus="focusHandle('pwd')" v-model="loginForm.password" placeholder="密码">
<img
v-act="activeStr == 'pwd'"
class="prefix"
slot="prefix"
src="@/assets/images/pwd.png"
alt
/>
</el-input>
</el-form-item>
<el-form-item prop="verifycode">
<el-input @focus="focusHandle('yzm')" v-model="loginForm.verifycode" placeholder="验证码">
<img
v-act="activeStr == 'yzm'"
class="prefix"
slot="prefix"
src="@/assets/images/yzm.png"
alt
/>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary">登录</el-button>
</el-form-item>
</el-form>
<script>
export default {
data () {
return {
activeStr: '',
loginForm: {
username: '',
password: '',
verifycode: ''
},
loginFormRules: {
username: [
{ required: true, message: '请输入用户名称', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入用户密码', trigger: 'blur' }
],
verifycode: [
{ required: true, message: '请输入验证码', trigger: 'blur' }
]
}
}
}
}
</script>
4.添加验证码区域
-
在 src–> utils 目录下创建操作本地存储的工具函数 storage.js
/** * 封装本地存储操作模块 */ /** * 存储数据 */ export const setItem = (key, value) => { // 将数组、对象类型的数据转换为 JSON 格式字符串进行存储 if (typeof value === 'object') { value = JSON.stringify(value) } window.sessionStorage.setItem(key, value) } /** * 获取数据 */ export const getItem = key => { const data = window.sessionStorage.getItem(key) try { return JSON.parse(data) } catch (err) { return data } } /** * 删除数据 */ export const removeItem = key => { window.sessionStorage.removeItem(key) }
-
在src–>utils 目录下创建用于生成验证码的工具函数 verify.js
import { setItem } from './storage' /** * * @param {String} el 选择器 * @param {*} option 配置对象 { lineNum:干扰线数量 ,textLen:验证码长度 ,width:画布宽 ,height:画布高 } */ function Gcode(el, option) { this.el = typeof el === "string" ? document.querySelector(el) : el; this.option = option || {}; this.text = "" this.init(); } Gcode.prototype = { constructor: Gcode, init: function () { if (this.el.getContext) { var ctx = this.el.getContext("2d"), // 设置画布宽高 cw = this.el.width = this.option.width || 90, ch = this.el.height = this.option.height || 30, textLen = this.option.textLen || 4, lineNum = this.option.lineNum || 2; this.randomText(textLen); this.onClick(ctx, textLen, lineNum, cw, ch); this.drawLine(ctx, lineNum, cw, ch); this.drawText(ctx, this.text, ch); } }, onClick: function (ctx, textLen, lineNum, cw, ch) { var _ = this; this.el.addEventListener("click", function () { _.randomText(textLen); _.drawLine(ctx, lineNum, cw, ch); _.drawText(ctx, _.text, ch); }, false) }, // 画干扰线 drawLine: function (ctx, lineNum, maxW, maxH) { ctx.clearRect(0, 0, maxW, maxH); for (var i = 0; i < lineNum; i++) { var dx1 = Math.random() * maxW, dy1 = Math.random() * maxH, dx2 = Math.random() * maxW, dy2 = Math.random() * maxH; ctx.strokeStyle = " rgb(" + 255 * Math.random() + "," + 255 * Math.random() + "," + 255 * Math.random() + ")"; ctx.beginPath(); ctx.moveTo(dx1, dy1); ctx.lineTo(dx2, dy2); ctx.stroke(); } }, // 画文字 drawText: function (ctx, text, maxH) { var len = text.length; for (var i = 0; i < len; i++) { var dx = 20 * Math.random() + 20 * i, dy = Math.random() * 5 + maxH / 2; ctx.fillStyle = " rgb(" + 255 * Math.random() + "," + 255 * Math.random() + "," + 255 * Math.random() + ")"; ctx.font = " 22px Helvetica"; ctx.textBaseline = " middle"; ctx.fillText(text[i], dx, dy); } }, // 生成指定个数的随机文字 randomText: function (len) { var source = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']; var result = []; var sourceLen = source.length; for (var i = 0; i < len; i++) { var text = this.generateUniqueText(source, result, sourceLen); result.push(text) } this.text = result.join("") setItem('verify_code', this.text) }, // 生成唯一文字 generateUniqueText: function (source, hasList, limit) { var text = source[Math.floor(Math.random() * limit)]; if (hasList.indexOf(text) > -1) { return this.generateUniqueText(source, hasList, limit) } else { return text } } } export default Gcode
-
给el-input添加后缀图标 suffix
<el-form-item prop="verifycode"> <el-input @focus="focusHandle('yzm')" v-model="loginForm.verifycode" placeholder="验证码"> <img v-act="activeStr == 'yzm'" class="prefix" slot="prefix" src="@/assets/images/yzm.png" alt /> <canvas slot="suffix" id="verifybox"></canvas> </el-input> </el-form-item>
-
在mounted生命周期中调用utils里的工具方法生成验证码
mounted () { // 创建验证码 return new Gcode('#verifybox', 4) }
-
自定义校验验证码的校验规则
data () { // 自定义校验规则 const checkVerify = (rule, value, callback) => { const vfCode = getItem('verify_code') if (vfCode !== value) { return callback(new Error('请填写正确的验证码')) } callback() } return { activeStr: '', loginForm: { username: '', password: '', verifycode: '' }, loginFormRules: { username: [ { required: true, message: '请输入用户名称', trigger: 'blur' } ], password: [ { required: true, message: '请输入用户密码', trigger: 'blur' } ], verifycode: [ { required: true, message: '请输入验证码', trigger: 'blur' }, { validator: checkVerify, trigger: 'blur' } ] } } }
5. 全局配置axios
6. 给登录按钮绑定点击事件
7. 获取el-form实例调用表单预验证函数发出登录请求
methods: {
focusHandle (val) {
this.activeStr = val
},
async login () {
this.$refs.form.validate(async vali => {
if (!vali) return this.$message.error('请填写正确的登录信息')
const { data: res } = await this.$http.post('common/login', this.loginForm)
if (res.code !== 200) return this.$message.error('登录失败')
this.$router.push('/home')
})
}
}
Home 页面
页面初始化
-
初始化页面结构
<template> <el-container> <el-header height="80px"> <div class="title"> <span>智慧云课堂</span> </div> <div class="right_box"> <i class="el-icon-warning-outline help" ></i> <span class="help">帮助</span> <img src="@/assets/images/personal.jpg" alt /> <span>HolyCode</span> </div> </el-header> <el-container> <el-aside width="250px"></el-aside> <el-main> <router-view></router-view> </el-main> </el-container> </el-container> </template> <script> export default {} </script> <style lang="less" scoped> .el-container { height: 100%; .el-header { background-color: #1b7bef; box-shadow: 6px 3px 10px 0 rgba(0, 0, 0, 0.3); z-index: 99; display: flex; justify-content: space-between; align-items: center; padding-left: 65px; font-size: 24px; color:#fff; .right_box { display: flex; align-items: center; .help { font-size: 14px; color:#cdcdcd; margin-left: 10px; } img { width: 36px; margin-left: 30px; border-radius: 50%; } > span:nth-child(4){ margin-left: 10px; font-size: 18px; } } } .el-aside { background-color: #1b7bef; } } </style>
-
引入侧边栏的静态数据
<script> import asideList from '../utils/asideList' export default { data () { return { asideList } } } </script>
-
循环生成NavMenu
<el-menu class="el-menu-vertical-demo" background-color="#1b7bef" text-color="#a7c7ed" active-text-color="#e7eaec" :default-active="'/board'" > <el-menu-item v-for="item in asideList" :key="item.path" :index="'/'+item.path"> <template> <i :class="item.icon"></i> <span>{{ item.name }}</span> </template> </el-menu-item> </el-menu>
-
监听路由变化 记录navmenu的选中状态
课程购买量页面
1. 获取图表数据
-
添加对应路由规则
-
创建Buy.vue文件
-
初始化结构
<template> <div> <el-breadcrumb separator-class="el-icon-arrow-right"> <el-breadcrumb-item>当前位置</el-breadcrumb-item> <el-breadcrumb-item>课程购买量</el-breadcrumb-item> </el-breadcrumb> <el-card> <el-tabs> <el-tab-pane label="课程购买量统计" name="buy"> </el-tab-pane> <el-tab-pane label="课程访问量" name="visit"> </el-tab-pane> </el-tabs> </el-card> </div> </template>
-
获取课程访问量数据
- 引入dayjs
- 查询近一周的数据使用dayjs来格式化时间
- 发起请求
<script> import dayjs from 'dayjs' export default { data () { return { queryInfo: { startDate: '', endDate: '', courseld: '' }, buyList: [] } }, mounted () { this.queryInfo.startDate = dayjs().subtract(10, 'day').format('YYYY-MM-DD') this.queryInfo.endDate = dayjs().format('YYYY-MM-DD') this.getBuyList() }, methods: { async getBuyList () { const { data: res } = await this.$http.get('course/courseBuy', { params: { ...this.queryInfo } }) console.log(res) } } } </script>
2. 初步展示图表
-
引入echart import echarts from ‘echarts’
-
创建图表容器
<el-tabs v-model="tabsActive"> <el-tab-pane label="课程购买量统计" name="buy"> <div ref="chartbox" style="height:600px"></div> </el-tab-pane> <el-tab-pane label="课程访问量" name="visit"> </el-tab-pane> </el-tabs>
-
初始化图表容器对象
-
封装绘制函数
-
在获取数据的函数里 得到返回值之后调用绘制图表函数
3. 自定义折线图的样式
-
设置tooltip 提示框组件
-
设置 show 属性为 true
-
设置 trigger 属性为 ‘axis’ 坐标轴触发,主要在柱状图,折线图等会使用类目轴的图表中使用
-
设置 axisPointer 坐标轴指示器配置项中的 type 属性设置为
'none'
无指示器 -
设置 背景色 backgroundColor 为 rgba(245, 245, 245, 0.8)
-
设置 文字样式 textStyle 中的color 为 #000
tooltip: { show: true, trigger: 'axis', axisPointer: { type: 'none' }, backgroundColor: 'rgba(245, 245, 245, 0.8)', textStyle: { color: '#000' }, borderWidth: 1, borderColor: '#ccc', padding: 10 }
-
设置 formatter 中的模板字符串
formatter: function (params) { return `<div style="width:194px;height:96px;border-radius:4px;position:relative;"> <p>${params.name}</p> <div style="width:8px;height:8px; position:absolute;top:42px;left:10px;background-color:#6BCEF0;border-radius:50%;"></div> <ul> <li> <span style="margin-right:20px">${params.seriesName ? params.seriesName : params.name}</span> <span>${params.value}</span> </li> </ul> </div>` }
-
-
设置 xAxis 属性 boundaryGap 坐标轴两边留白策略 boundaryGap: false
-
设置 折线图 line 的 series
series: [ { data: this.buyNumList, smooth: true, // 是否为平滑曲线 name: '新增会员数', // tooltip中显示 symbol: 'circle', // 标记的图形 symbolSize: 6, // 标记大小 lineStyle: { // 折线样式 width: 2, shadowColor: 'rgba(0,0,0,0.4)', shadowBlur: 7, shadowOffsetY: 30 // 阴影y轴上的偏移值 }, itemStyle: { // 拐点样式 color: '#509BF2', borderColor: '#fff' }, type: 'line' } ]
4. 添加自定义工具栏
结构
<div class="toolbox">
<div class="download">下载数据</div>
<div class="chart_type">
<span>图标类型:</span>
<div class="type_box">
<i class="iconfont icon-zhexiantu"></i>
<i class="iconfont icon-zhuzhuangtu"></i>
<i class="iconfont icon-zhuzhuangtu1"></i>
</div>
</div>
</div>
样式
.toolbox {
font-size: 14px;
display: flex;
align-items: center;
justify-content: flex-end;
.download {
color: #1b7befff;
font-weight: 600;
margin-right: 30px;
cursor: pointer;
}
.chart_type {
display: flex;
align-items: center;
span {
margin-right: 20px;
}
.type_box {
border-radius: 4px;
border: 1px solid #d0d0d0;
display: flex;
justify-content: center;
i:nth-child(3) {
border: 0;
}
i {
display: block;
border-right: 1px solid #d0d0d0;
box-sizing: border-box;
width: 52px;
height: 36px;
color: #c1c1c1;
font-size: 26px;
padding-left: 13px;
padding-top: 5px;
}
.active {
color: #fff;
background-color: #1b7bef;
}
}
}
}
5. 为自定义工具栏 添加选中高亮
<div class="type_box">
<i @click="changeType('line')" :class="{'iconfont':true, 'icon-zhexiantu':true, 'active': 'line'==chartType?true:false}"></i>
<i @click="changeType('bar')" :class="{'iconfont':true, 'icon-zhuzhuangtu':true, 'active': 'bar'==chartType?true:false}"></i>
<i @click="changeType('hbar')" :class="{'iconfont':true, 'icon-zhuzhuangtu1':true, 'active': 'hbar'==chartType?true:false}"></i>
</div>
封装图表组件
1. 抽离结构代码
<template>
<div ref="chartbox" style="height:600px"></div>
</template>
2. 定义接受的数据 prop
props: {
chartType: {
type: String,
default: 'line'
},
valueData: {
type: Array,
required: true
},
cateData: {
type: Array,
required: true
},
tipName: {
type: String,
required: true
}
}
3. 封装工具文件 chart.js
// 公共的tooltip 配置
const tooltip = {
show: true,
trigger: 'axis',
axisPointer: {
type: 'none'
},
backgroundColor: 'rgba(245, 245, 245, 0.8)',
textStyle: {
color: '#000'
},
borderWidth: 1,
borderColor: '#ccc',
padding: 10,
formatter: function (params) {
return `<div style="width:194px;height:96px;border-radius:4px;position:relative;">
<p>${params[0].name}</p>
<div style="width:8px;height:8px; position:absolute;top:42px;left:10px;background-color:#6BCEF0;border-radius:50%;"></div>
<ul>
<li>
<span style="margin-right:20px">${
params[0].seriesName ? params[0].seriesName : params[0].name
}</span>
<span>${params[0].value}</span>
</li>
</ul>
</div>`
}
}
// line
const lineopt = {
tooltip,
xAxis: {
type: 'category',
data: [],
boundaryGap: false
},
yAxis: {
type: 'value'
},
series: [
{
data: [],
smooth: true, // 是否为平滑曲线
name: '新增会员数', // tooltip中显示
symbol: 'circle', // 标记的图形
symbolSize: 6, // 标记大小
lineStyle: {
// 折线样式
width: 2,
shadowColor: 'rgba(0,0,0,0.4)',
shadowBlur: 7,
shadowOffsetY: 30 // 阴影y轴上的偏移值
},
itemStyle: {
// 拐点样式
color: '#509BF2',
borderColor: '#fff'
},
type: 'line'
}
]
}
export const drawnLine = (chartbox, options) => {
lineopt.xAxis.data = options.cateData
lineopt.series[0].data = options.valueData
lineopt.series[0].name = options.tipName
return chartbox.setOption(lineopt)
}
// bar
// hbar
<li>
<span style="margin-right:20px">${
params[0].seriesName ? params[0].seriesName : params[0].name
}</span>
<span>${params[0].value}</span>
</li>
</ul>
</div>`
}
}
// line
const lineopt = {
tooltip,
xAxis: {
type: ‘category’,
data: [],
boundaryGap: false
},
yAxis: {
type: ‘value’
},
series: [
{
data: [],
smooth: true, // 是否为平滑曲线
name: ‘新增会员数’, // tooltip中显示
symbol: ‘circle’, // 标记的图形
symbolSize: 6, // 标记大小
lineStyle: {
// 折线样式
width: 2,
shadowColor: ‘rgba(0,0,0,0.4)’,
shadowBlur: 7,
shadowOffsetY: 30 // 阴影y轴上的偏移值
},
itemStyle: {
// 拐点样式
color: ‘#509BF2’,
borderColor: ‘#fff’
},
type: ‘line’
}
]
}
export const drawnLine = (chartbox, options) => {
lineopt.xAxis.data = options.cateData
lineopt.series[0].data = options.valueData
lineopt.series[0].name = options.tipName
return chartbox.setOption(lineopt)
}
// bar
// hbar