⭐ 作者简介:码上言
⭐ 代表教程:Spring Boot + vue-element 开发个人博客项目实战教程
⭐我的文档网站:http://xyhwh-nav.cn/
后端代码gitee地址:https://gitee.com/whxyh/personal_blog
前端代码gitee地址:https://gitee.com/whxyh/personal_vue
项目部署视频
https://www.bilibili.com/video/BV1sg4y1A7Kv/?vd_source=dc7bf298d3c608d281c16239b3f5167b
文章目录
前言
这一篇将是我们项目开发的最后一篇文章了,到这里该和大家说再见了,这个项目从开始写到现在刚好一年了,有时间就写写,中间断了好几个月的时间,看到好多人说对他们很有用,我写的也就变成了有意义的事情,希望大家都不忘初心,牢记使命,认真的学习技术和好好地生活。正如士兵突击中的许三多说的,好好活,就是做有意义的事,做有意义的事情,就是好好活着。
预告
这里给大家预告一下,这个教程已经接近尾声了,看到大家给我的好多反馈,说的学到了很多东西,我感觉到十分的欣慰,感觉自己的付出没有白费,有的小伙伴说比某些机构的课程学的还多,我感觉只要好好地学习,都是可以的,哪怕你只是拿这个项目去最毕设或者其他的之类的,反正技多不压身,这个也涵盖了好多的东西,基本的入门也可以了。
我在想要不要再搞一个大一点的项目,将知识点再扩大一些,比如现在没有权限的操作、我们登录再加上短信验证码、文件的上传和解析、redis的实际运用等操作、自动化部署代码、原型设计、日志记录、小程序学习等新的技术,会更加完善做项目的流程和规范,基本上达到全栈的技术。
我再考虑是不是要开通付费的专栏,大家可以根据自身的需求来学习,感觉自己需要学习就来学习,不需要看这个教程可以了就不要订阅。我感觉肯定会比花上万的去培训要实在的多,大家自己衡量。
欢迎大家给我提改进意见或者要加入什么技术,我尽量用项目来整合这些技术加入实际的应用。感谢各位!
1、项目完善
1.1、文章创建完后在编辑标签没有展示
这是上一篇的bug,我在测试文章添加后,然后再点击编辑后,发现标签的值并没有,我查看了接口返回的数据为空,发现是后端添加文章存入缓存的问题,这里修改一下,只要加一行代码即可,大家可以提前想一下。
先分析一下我们调用的方法findById。
@Override public ArticleVO findById(Integer articleId) { Article article = articleMap.get(articleId); 。。。。。。
只看这一句即可,文章的数据来自map中,然而再添加文章的时候,map只存的是页面传来的数据,并没有将标签的数据给map,所以查出来的话肯定没有标签数据。
可以在文章添加的实现类中直接加上一下这行代码,在我们添加文章之后,在调用init重新加载一下缓存里的数据
this.init();
只看这一句即可,文章的数据来自map中,然而再添加文章的时候,map只存的是页面传来的数据,并没有将标签的数据给map,所以查出来的话肯定没有标签数据。
可以在文章添加的实现类中直接加上一下这行代码,在我们添加文章之后,在调用init重新加载一下缓存里的数据
this.init();
完整代码:
@Override public void insertOrUpdateArticle(ArticleInsertBO bo) { //分类添加 Category category = saveCategory(bo); Article article = BeanUtil.copyProperties(bo, Article.class); if (category != null) { article.setCategoryId(category.getCategoryId()); } String username = (String) SecurityUtils.getSubject().getPrincipal(); User user = userService.getUserByUserName(username); article.setUserId(user.getId()); article.setAuthor(user.getUserName()); article.setViews(0L); article.setTotalWords(WordCountUtil.wordCount(bo.getContent())); if (bo.getId() != null) { articleMapper.updateArticle(article); } else { articleMapper.createArticle(article); } articleMap.put(article.getId(), article); //添加文章标签 saveTags(bo, article.getId()); this.init(); //添加文章发送邮箱提醒 try { String content = "【{0}】您好:\n" + "您已成功发布了标题为: {1} 的文章 \n" + "请注意查收!\n"; MailInfo build = MailInfo.builder() .receiveMail(user.getEmail()) .content(MessageFormat.format(content, user.getUserName(), article.getTitle())) .title("文章发布") .build(); SendMailConfig.sendMail(build); } catch (Exception e) { log.error("邮件发送失败{}", e.getMessage()); } }
1.2、文章字数统计实现
我们在文章的数据表中预留了一个文章的字数,一开始的时候我直接赋值的是0,现在我们要把字数统计给加上,所以需要写一个字数统计的工具类。
package com.blog.personalblog.util; /** * @author: SuperMan * @create: 2022-10-14 **/ public class WordCountUtil { /** * 统计字数, 空格不统计 * @param string * @return */ public static long wordCount(String string) { if (string == null) { return 0; } long letterCount = 0L; long numCount = 0L; long otherCount = 0L; String str = string.trim(); char[] chr = str.toCharArray(); for(int i = 0; i < chr.length;i++){ if(Character.isLetter(chr[i])){ letterCount++; } else if(Character.isDigit(chr[i])){ numCount ++; } else{ otherCount ++; } } return letterCount + numCount + otherCount; } }
还是在添加的方法中来统计文章字数。将原来的**article.setTotalWords(0L)**改成以下代码:
article.setTotalWords(WordCountUtil.wordCount(bo.getContent()));
查看页面效果:
1.3、发布文章新建分类修改
我在测试的时候,发布文章没有选择从数据库查出来的分类,而是自己创建的一个分类,点击发布会报错。
首先定位到代码错误的信息。
private Category saveCategory(ArticleInsertBO bo) { if (StrUtil.isEmpty(bo.getCategoryName())) { return null; } Category category = categoryService.getCategoryByName(bo.getCategoryName()); if (category == null && !ArticleArtStatusEnum.DRAFT.getStatus().equals(bo.getArtStatus())) { category.setCategoryName(bo.getCategoryName()); categoryService.saveCategory(category); } return category; }
Category category = categoryService.getCategoryByName(bo.getCategoryName());
这一句我们拿前端传过来的分类名去查找,然后没有找到,我们又将前端的值赋给了它,就报错了。
我们拿到的category是一个null,而null对象在堆中会被java的垃圾回收机制回收。所以这里赋值直接报错了,所以我们再重新new一个分类对象即可。
private Category saveCategory(ArticleInsertBO bo) { if (StrUtil.isEmpty(bo.getCategoryName())) { return null; } Category category = categoryService.getCategoryByName(bo.getCategoryName()); Category newCategory = new Category(); if (category == null && !ArticleArtStatusEnum.DRAFT.getStatus().equals(bo.getArtStatus())) { newCategory.setCategoryName(bo.getCategoryName()); categoryService.saveCategory(newCategory); return newCategory; } return category; }
1.4、邮件发送配置修改
这里我把邮箱的配置进行了修改,原来配置的邮箱信息是在代码里配置的,维护不太方便,我把它提到了配置文件中了。以后修改邮箱信息直接修改配置文件,就不需要找代码了。打开application.yml
send: mail: host: # 邮件服务器的SMTP地址 port: # 邮件服务器的SMTP端口 from: # 发件人 pass: # 密码
然后修改代码,打开SendMailConfig.java,将配置信息引入进来。
@Value("${send.mail.host}") private String host; @Value("${send.mail.port}") private Integer port; @Value("${send.mail.from}") private String from; @Value("${send.mail.pass}") private String pass; public void sendMail(MailInfo mailInfo) { try { MailAccount account = new MailAccount(); //邮件服务器的SMTP地址 account.setHost(host); //邮件服务器的SMTP端口 account.setPort(port); //发件人 account.setFrom(from); //密码 account.setPass(pass); //使用SSL安全连接 account.setSslEnable(false); MailUtil.send(account, mailInfo.getReceiveMail(), mailInfo.getTitle(), mailInfo.getContent(), false); log.info("邮件发送成功!"); } catch (Exception e) { log.error("邮件发送失败" + JSONUtil.toJsonStr(mailInfo)); } }
添加完之后,再去测试下。
1.5、登录页改造升级
我们现在的登录页面非常的原始,不太好看,俗话说人靠衣服马靠鞍,我们也将登录的入口进行改造,后端的逻辑不用动,我们只改前端代码即可。
<style rel="stylesheet/scss" lang="scss"> $bg:#889aa4; $light_gray:#eaeaea; /* reset element-ui css */ .login-container { .el-input { display: inline-block; height: 47px; width: 85%; input { background: transparent; border: 0px; -webkit-appearance: none; border-radius: 0px; padding: 12px 5px 12px 15px; color: black; height: 47px; &:-webkit-autofill { -webkit-box-shadow: 0 0 0px 1000px $bg inset !important; -webkit-text-fill-color: black !important; } } } .el-form-item { border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(0, 0, 0, 0.1); border-radius: 5px; color: #454545; } } </style> <style rel="stylesheet/scss" lang="scss" scoped> $bg:#889aa4; $dark_gray:#889aa4; $light_gray:#eee; .login-container { position: fixed; height: 100%; width: 100%; background:url("../../assets/bj.jpg"); position:fixed; background-size:100% 100%; // background-image: "../../assets/bg.jpg"; .login-form { position: absolute; left: 0; right: 0; width: 520px; max-width: 100%; padding: 35px 35px 15px 35px; margin: 120px auto; } .tips { font-size: 14px; color: #fff; margin-bottom: 10px; span { &:first-of-type { margin-right: 16px; } } } .svg-container { padding: 6px 5px 6px 15px; color: $dark_gray; vertical-align: middle; width: 30px; display: inline-block; } .title { font-size: 28px; font-weight: 400; margin: 0px auto 40px auto; text-align: center; font-weight: bold; } .show-pwd { position: absolute; right: 10px; top: 7px; font-size: 16px; color: $dark_gray; cursor: pointer; user-select: none; } } </style>
这里只修改了一些样式和添加了一个背景图片。也算是有点样子了。
2、首页功能前端开发
这一块也就是对应的我们的首页,刚一进来就能直观看到的,我们尽量做的美观一点,逼格高一点。
首先我们首页的顶部先放四个导航菜单,用来展示我们的一些重要的数据。
下面的开发我先写前端页面布局完成之后,再去写后端的代码。
2.1、顶部导航
这里使用了vue-element-admin
的首页的功能。打开我们的前端项目,然后找到/views/dashboard
。
然后我们引入一个组件,这个导航菜单已经封装成了一个组件。在dashboard
文件下新建一个components
文件夹,然后创建一个文件PanelGroup.vue
<template> <el-row :gutter="40" class="panel-group"> <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col"> <div class="card-panel"> <div class="card-panel-icon-wrapper icon-people"> <svg-icon icon-class="peoples" class-name="card-panel-icon" /> </div> <div class="card-panel-description"> <div class="card-panel-text"> 文章数量 </div> <count-to :start-val="0" :end-val="102400" :duration="2600" class="card-panel-num" /> </div> </div> </el-col> <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col"> <div class="card-panel"> <div class="card-panel-icon-wrapper icon-message"> <svg-icon icon-class="message" class-name="card-panel-icon" /> </div> <div class="card-panel-description"> <div class="card-panel-text"> 分类数量 </div> <count-to :start-val="0" :end-val="81212" :duration="3000" class="card-panel-num" /> </div> </div> </el-col> <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col"> <div class="card-panel"> <div class="card-panel-icon-wrapper icon-money"> <svg-icon icon-class="money" class-name="card-panel-icon" /> </div> <div class="card-panel-description"> <div class="card-panel-text"> 标签数量 </div> <count-to :start-val="0" :end-val="9280" :duration="3200" class="card-panel-num" /> </div> </div> </el-col> <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col"> <div class="card-panel"> <div class="card-panel-icon-wrapper icon-shopping"> <svg-icon icon-class="shopping" class-name="card-panel-icon" /> </div> <div class="card-panel-description"> <div class="card-panel-text"> 用户数量 </div> <count-to :start-val="0" :end-val="13600" :duration="3600" class="card-panel-num" /> </div> </div> </el-col> </el-row> </template> <script> import CountTo from 'vue-count-to' export default { components: { CountTo }, methods: { handleSetLineChartData(type) { this.$emit('handleSetLineChartData', type) } } } </script> <style lang="scss" scoped> .panel-group { margin-top: 18px; .card-panel-col { margin-bottom: 32px; } .card-panel { height: 108px; cursor: pointer; font-size: 12px; position: relative; overflow: hidden; color: #666; background: #fff; box-shadow: 4px 4px 40px rgba(0, 0, 0, .05); border-color: rgba(0, 0, 0, .05); &:hover { .card-panel-icon-wrapper { color: #fff; } .icon-people { background: #40c9c6; } .icon-message { background: #36a3f7; } .icon-money { background: #f4516c; } .icon-shopping { background: #34bfa3 } } .icon-people { color: #40c9c6; } .icon-message { color: #36a3f7; } .icon-money { color: #f4516c; } .icon-shopping { color: #34bfa3 } .card-panel-icon-wrapper { float: left; margin: 14px 0 0 14px; padding: 16px; transition: all 0.38s ease-out; border-radius: 6px; } .card-panel-icon { float: left; font-size: 48px; } .card-panel-description { float: right; font-weight: bold; margin: 26px; margin-left: 0px; .card-panel-text { line-height: 18px; color: rgba(0, 0, 0, 0.45); font-size: 16px; margin-bottom: 12px; } .card-panel-num { font-size: 20px; } } } } @media (max-width:550px) { .card-panel-description { display: none; } .card-panel-icon-wrapper { float: none !important; width: 100%; height: 100%; margin: 0 !important; .svg-icon { display: block; margin: 14px auto !important; float: none !important; } } } </style>
然后去dashboard目录下的index.vue
中引入该组件。
import PanelGroup from './components/PanelGroup' export default { name: 'Dashboard', components: { PanelGroup }, computed: { ...mapGetters([ 'name', 'roles' ]) } }
这时控制台会报一个错误
此时我们要执行:npm install --save vue-count-to
即可。
执行完之后,我们在引入该组件。
<panel-group ></panel-group>
此时页面上就已经有数据了,我们进行改造一下页面。可以看到上边还缺少图标和描述之类的,这个是在组件里修改,打开PanelGroup.vue
修改
<template> <el-row :gutter="40" class="panel-group"> <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col"> <div class="card-panel"> <div class="card-panel-icon-wrapper icon-people"> <svg-icon icon-class="documentation" class-name="card-panel-icon" /> </div> <div class="card-panel-description"> <div class="card-panel-text"> 文章数量 </div> <count-to :start-val="0" :end-val="102400" :duration="2600" class="card-panel-num" /> </div> </div> </el-col> <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col"> <div class="card-panel"> <div class="card-panel-icon-wrapper icon-message"> <svg-icon icon-class="component" class-name="card-panel-icon"/> </div> <div class="card-panel-description"> <div class="card-panel-text"> 分类数量 </div> <count-to :start-val="0" :end-val="81212" :duration="3000" class="card-panel-num" /> </div> </div> </el-col> <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col"> <div class="card-panel"> <div class="card-panel-icon-wrapper icon-money"> <svg-icon icon-class="icon" class-name="card-panel-icon" /> </div> <div class="card-panel-description"> <div class="card-panel-text"> 标签数量 </div> <count-to :start-val="0" :end-val="9280" :duration="3200" class="card-panel-num" /> </div> </div> </el-col> <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col"> <div class="card-panel"> <div class="card-panel-icon-wrapper icon-shopping"> <svg-icon icon-class="people" class-name="card-panel-icon"/> </div> <div class="card-panel-description"> <div class="card-panel-text"> 用户数量 </div> <count-to :start-val="0" :end-val="13600" :duration="3600" class="card-panel-num" /> </div> </div> </el-col> </el-row> </template>
图标的话,可以去官网上查找或者从这里查找,然后将图标下载放到自己项目的svg目录下即可。https://panjiachen.github.io/vue-element-admin/#/icon/index
改造完之后是这样的页面
2.2、ECharts入门
这里先介绍一下echarts
官网:https://echarts.apache.org/zh/index.html
什么是echarts?
它是一个基于 JavaScript 的开源可视化图表库,可以用于我们对数据分析的可视化展示,是我们的数据在图表中清晰可见,一般领导比较喜欢看这种分析的图表。
具体的如何使用这里不再一一讲述了,可以查看官方给的文档,有快速上手的教程可以学习。
2.3、数据图表
这里我们先安装一下echarts图表库。使用以下命令
npm install echarts --save
之后我们在创建的components组件文件中新建一个放图表的文件,现在是一个图表对应一个文件,新建一个BarChart.vue
文件,这个放我们的柱状图,打开文件,先写一下存放图表的的容器,并设置一下高和宽。
<template> <div :class="className" :style="{height:height,width:width}" /> </template>
紧接着要去写一下图表的代码,先引入echarts文件
import * as echarts from 'echarts' require('echarts/theme/macarons') // echarts theme
然后设置一下容器的高和宽
export default { props: { className: { type: String, default: 'chart' }, width: { type: String, default: '100%' }, height: { type: String, default: '300px' } }, data() { return { chart: null } }, mounted() { this.$nextTick(() => { this.initChart() }) }, beforeDestroy() { if (!this.chart) { return } this.chart.dispose() this.chart = null }, }
然后就可以通过 echarts.init
方法初始化一个 echarts 实例并通过 setOption
方法生成一个简单的柱状图
methods: { initChart() { this.chart = echarts.init(this.$el, 'macarons') this.chart.setOption({ title: { text: '发文数量' }, tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: [ { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], axisTick: { alignWithLabel: true } } ], yAxis: [ { type: 'value' } ], series: [ { name: 'Direct', type: 'bar', barWidth: '60%', data: [10, 52, 200, 334, 390, 330, 220] } ] }) } }
这里的代码大家可以去echarts官网的实例中去查找。
然后再去我们的主页将这个组件引进来。
import BarChart from './components/BarChart' components: { PanelGroup, BarChart, },
页面代码:
<el-row :gutter="32" class="row-chart"> <el-col :xs="24" :sm="24" :lg="8"> <div class="chart-wrapper"> <bar-chart /> </div> </el-col> </el-row>
然后再设置一下css样式
<style rel="stylesheet/scss" lang="scss" scoped> .dashboard { &-container { padding: 32px; background-color: #f0f2f5; } } .chart-wrapper { background: #fff; padding: 16px 16px 0; margin-bottom: 32px; } .row-chart{ margin-top: 30px; } </style>
然后我们打开页面,查看一下图表有没有渲染出来
此时就渲染出来了,一个我们会了,我们再添加两个图表分别统计分类和访问量。
和之前的那个图表一样,我这里不再一一讲述,只把代码给大家展现出来。
在components文件夹中新建一个PieChart.vue
<template> <div :class="className" :style="{height:height,width:width}" /> </template> <script> import * as echarts from 'echarts' require('echarts/theme/macarons') // echarts theme export default { props: { className: { type: String, default: 'chart' }, width: { type: String, default: '100%' }, height: { type: String, default: '300px' } }, data() { return { chart: null } }, mounted() { this.$nextTick(() => { this.initChart() }) }, beforeDestroy() { if (!this.chart) { return } this.chart.dispose() this.chart = null }, methods: { initChart() { this.chart = echarts.init(this.$el, 'macarons') this.chart.setOption({ title: { text: '分类占比', left: 'left' }, tooltip: { trigger: 'item' }, 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)' } } } ] }) } } } </script>
再新建一个LineChart.vue
<template> <div :class="className" :style="{height:height,width:width}" /> </template> <script> import * as echarts from 'echarts' require('echarts/theme/macarons') // echarts theme export default { props: { className: { type: String, default: 'chart' }, width: { type: String, default: '100%' }, height: { type: String, default: '300px' } }, data() { return { chart: null } }, mounted() { this.$nextTick(() => { this.initChart() }) }, beforeDestroy() { if (!this.chart) { return } this.chart.dispose() this.chart = null }, methods: { initChart() { this.chart = echarts.init(this.$el, 'macarons') this.chart.setOption({ title: { text: '访问量' }, xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, yAxis: { type: 'value' }, series: [ { data: [820, 932, 901, 934, 1290, 1330, 1320], type: 'line', smooth: true } ] }) } } } </script>
再去主页将这两个引入。
index.vue
完整代码如下
<template> <div class="dashboard-container"> <panel-group ></panel-group> <!-- 数据分析 --> <el-row :gutter="32" class="row-chart"> <el-col :xs="24" :sm="24" :lg="8"> <div class="chart-wrapper"> <bar-chart /> </div> </el-col> <el-col :xs="24" :sm="24" :lg="8"> <div class="chart-wrapper"> <pie-chart /> </div> </el-col> <el-col :xs="24" :sm="24" :lg="8"> <div class="chart-wrapper"> <line-chart /> </div> </el-col> </el-row> </div> </template> <script> import PanelGroup from './components/PanelGroup' import BarChart from './components/BarChart' import PieChart from './components/PieChart' import LineChart from './components/LineChart' export default { name: 'Dashboard', components: { PanelGroup, BarChart, PieChart, LineChart }, data() { return { } }, methods: { }, } </script> <style rel="stylesheet/scss" lang="scss" scoped> .dashboard { &-container { padding: 32px; background-color: #f0f2f5; } } .chart-wrapper { background: #fff; padding: 16px 16px 0; margin-bottom: 32px; } .row-chart{ margin-top: 30px; } </style>
然后我们看一下效果是不是感觉还挺哇塞的,其实那种看着非常高大上的大屏展示就是这种画出来的,我后边应该会写一篇如何制作大屏的页面的文章,大家可以等待一下。
现在我们的首页是不是有点样子了,越来越完善了,是有点系统的样子了。
接下来我们再美化一下首页,再添加一点小功能。
2.4、添加日历
大家可以参考这个文章添加日历,以下就是我参照实现的,稍微做了修改。
参考文章:vue日历插件vue-calendar
- 首先安装一下日历的组件
npm i vue-calendar-component --save
如果安装失败,可以试试以下的命令
cnpm i vue-calendar-component --save
- 引入组件
在我们的首页引入一下日历的组件
import Calendar from 'vue-calendar-component';
然后写绘制日历的代码
<el-col :xs="24" :sm="24" :lg="8"> <div class="chart-wrapper"> <div class="con"> <div class="now-data-myself"> <div class="now-data-myself-time">{{ date }}</div> <div class="now-data-myself-week">{{ week }}</div> </div> <Calendar v-on:choseDay="clickDay" v-on:changeMonth="changeDate" v-on:isToday="clickToday" ></Calendar> </div> </div> </el-col>
components: { PanelGroup, BarChart, PieChart, LineChart, Calendar },
data() { return { date: "", week: "", } }, created() { var now = new Date(); this.date = now.getDate();//得到日期 var day = now.getDay();//得到周几 var arr_week = new Array("星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"); this.week = arr_week[day]; }, methods: { clickDay(data) {}, changeDate(data) {}, clickToday(data) {} },
CSS样式:
.now-data-myself { width: 40%; position: absolute; border-right: 1px solid rgba(227, 227, 227, 0.6); } .con { position: relative; max-width: 400px; margin: auto; } .con .wh_content_all { background: transparent !important; } .wh_top_changge li { color: #F56C6C !important; font-size: 15px !important; } .wh_content_item, .wh_content_item_tag { color: #303133 !important; } .wh_content_item .wh_isToday { background: #00d985 !important; color: #fff !important; } .wh_content_item .wh_chose_day { background: #409EFF !important; color: #ffff !important; } .wh_item_date:hover { background: rgb(217, 236, 255) !important; border-radius: 100px !important; color: rgb(102, 177, 255) !important; } .wh_jiantou1[data-v-2ebcbc83] { border-top: 2px solid #909399; border-left: 2px solid #909399; width: 7px; height: 7px; } .wh_jiantou2[data-v-2ebcbc83] { border-top: 2px solid #909399; border-right: 2px solid #909399; width: 7px; height: 7px; } .wh_top_tag[data-v-2ebcbc83] { color: #409EFF; border-top: 1px solid rgba(227, 227, 227, 0.6); border-bottom: 1px solid rgba(227, 227, 227, 0.6); } .wh_container[data-v-2ebcbc83] { max-width: 400px; } .wh_top_changge[data-v-2ebcbc83] { display: flex; width: 50%; margin-left: 43%; } .now-data-myself-time { color: #F56C6C; font-size: 28px; margin-left:60px; height: 33px; font-family: "Helvetica Neue"; } .now-data-myself-week { margin-left:60px; font-size: 10px; color: #909399; } .wh_top_changge .wh_content_li[data-v-2ebcbc83] { font-family: Helvetica; }
这里我遇到了一个坑,修改的日历的样式没有效果,最终看到css样式的地方加了scoped
<style rel="stylesheet/scss" lang="scss" scoped>
我们把这个去掉即可,想知道什么原因的可以去学习一下。
然后看一下我们的页面。
2.5、词云
接下来我们来写一下这个词云,这个主要是美化我们的页面,做一些特效使用,也多学一些常用的小功能。
我这个是在网上找了一个词云的代码,我将它封装成了一个组件,直接在首页引用即可。
在components文件夹下面新建一个WordCloud.vue
文件
<template> <section class="cloud-bed"> <div class="cloud-box"> <span v-for="(item, index) in dataList" :key="index" @click="getDataInfo(item)" :style="{color:item.color,background:item.bgColor}" > {{ item.name }} </span> </div> </section> </template> <script> export default { name: "word-cloud", data() { return { timer: 10, // 球体转动速率 radius: 0, // 词云球体面积大小 dtr: Math.PI/180, //鼠标滑过球体转动速度 active: false, // 默认加载是否开启转动 lasta: 0, // 上下转动 lastb: 0.5, // 左右转动 distr: true, tspeed: 1, // 鼠标移动上去时球体转动 mouseX: 0, mouseY: 0, tagAttrList: [], tagContent: null, cloudContent: null, sinA: '', cosA: '', sinB: '', cosB: '', sinC: '', cosC: '', dataList: [ { name: '页面卡顿\白屏', value: '1', bgColor:'rgb(57, 193, 207,0.12)', color:'#39c1cf', }, { name: '闪退', value: '8', bgColor:'rgb(66, 105, 245,0.12)', color:'#4269f5', }, { name: '登录问题', value: '9', bgColor:'rgb(184, 107, 215,0.12)', color:'#b86bd7', }, { name: '功能bug', value: '3', bgColor:'rgb(243, 84, 83,0.12)', color:'#f35453', }, { name: '无法收到短信', value: '6', bgColor:'rgb(250, 116, 20,0.12)', color:'#FA7414', }, { name: '人脸/指纹认证失败', value: '10', bgColor:'rgb(255, 171, 30,0.12)', color:'#FFAB1E', }, { name: '功能建议', value: '2', bgColor:'rgb(136, 104, 217,0.12)', color:'#8868D9', }, { name: 'UI/UX', value: '5', bgColor:'rgb(42, 184, 230,0.12)', color:'#2AB8E6', }, { name: '导航性', value: '7', bgColor:'rgb(117, 133, 162,0.12)', color:'#7585A2', }, ] } }, mounted () { this.$nextTick(() => { this.radius = document.querySelector('.cloud-box').offsetWidth / 2 this.initWordCloud() }) }, beforeDestroy () { clearInterval(this.timer) }, methods:{ // 获取点击文本信息 getDataInfo (item) { console.log(item, 'item') }, initWordCloud () { this.cloudContent = document.querySelector('.cloud-box'); this.tagContent = this.cloudContent.getElementsByTagName('span'); for (let i = 0; i < this.tagContent.length; i++) { let tagObj = {}; tagObj.offsetWidth = this.tagContent[i].offsetWidth; tagObj.offsetHeight = this.tagContent[i].offsetHeight; this.tagAttrList.push(tagObj); } this.sineCosine(0, 0, 0); this.positionAll(); this.cloudContent.onmouseover = () => { this.active=true; }; this.cloudContent.onmouseout = () => { this.active=false; }; this.cloudContent.onmousemove = (ev) => { let oEvent = window.event || ev; this.mouseX = oEvent.clientX - (this.cloudContent.offsetLeft + this.cloudContent.offsetWidth/2); this.mouseY = oEvent.clientY - (this.cloudContent.offsetTop + this.cloudContent.offsetHeight/2); this.mouseX/= 5; this.mouseY/= 5; }; setInterval(this.update, this.timer); }, positionAll () { let phi = 0; let theta = 0; let max = this.tagAttrList.length; let aTmp = []; let oFragment = document.createDocumentFragment(); //随机排序 for (let i=0; i < this.tagContent.length; i++) { aTmp.push(this.tagContent[i]); } aTmp.sort(() => { return Math.random() < 0.5 ? 1 : -1; }); for (let i = 0; i < aTmp.length; i++) { oFragment.appendChild(aTmp[i]); } this.cloudContent.appendChild(oFragment); for(let i = 1; i < max + 1; i++){ if (this.distr) { phi = Math.acos(-1 + (2 * i - 1) / max); theta = Math.sqrt(max * Math.PI) * phi; } else { phi = Math.random() * (Math.PI); theta = Math.random() * (2 * Math.PI); } //坐标变换 this.tagAttrList[i-1].cx = this.radius * Math.cos(theta) * Math.sin(phi); this.tagAttrList[i-1].cy = this.radius * Math.sin(theta) * Math.sin(phi); this.tagAttrList[i-1].cz = this.radius * Math.cos(phi); this.tagContent[i-1].style.left = this.tagAttrList[i-1].cx + this.cloudContent.offsetWidth / 2 - this.tagAttrList[i-1].offsetWidth / 2 + 'px'; this.tagContent[i-1].style.top = this.tagAttrList[i-1].cy + this.cloudContent.offsetHeight / 2 - this.tagAttrList[i-1].offsetHeight / 2 + 'px'; } }, update () { let angleBasicA; let angleBasicB; if (this.active) { angleBasicA = (-Math.min(Math.max(-this.mouseY, -200 ), 200) / this.radius) * this.tspeed; angleBasicB = (Math.min(Math.max(-this.mouseX, -200 ), 200) / this.radius) * this.tspeed; } else { angleBasicA = this.lasta * 0.98; angleBasicB = this.lastb * 0.98; } //默认转动是后是否需要停下 // lasta=a; // lastb=b; // if(Math.abs(a)<=0.01 && Math.abs(b)<=0.01) // { // return; // } this.sineCosine(angleBasicA, angleBasicB, 0); for(let j = 0; j < this.tagAttrList.length; j++) { let rx1 = this.tagAttrList[j].cx; let ry1 = this.tagAttrList[j].cy * this.cosA + this.tagAttrList[j].cz * (-this.sinA); let rz1 = this.tagAttrList[j].cy * this.sinA + this.tagAttrList[j].cz * this.cosA; let rx2 = rx1 * this.cosB + rz1 * this.sinB; let ry2 = ry1; let rz2 = rx1 * (-this.sinB) + rz1 * this.cosB; let rx3 = rx2 * this.cosC + ry2 * (-this.sinC); let ry3 = rx2 * this.sinC + ry2 * this.cosC; let rz3 = rz2; this.tagAttrList[j].cx = rx3; this.tagAttrList[j].cy = ry3; this.tagAttrList[j].cz = rz3; let per = 350 / (350 + rz3); this.tagAttrList[j].x = rx3 * per - 2; this.tagAttrList[j].y = ry3 * per; this.tagAttrList[j].scale = per; this.tagAttrList[j].alpha = per; this.tagAttrList[j].alpha = (this.tagAttrList[j].alpha - 0.6) * (10/6); } this.doPosition(); this.depthSort(); }, doPosition() { let len = this.cloudContent.offsetWidth/2; let height = this.cloudContent.offsetHeight/2; for (let i=0;i < this.tagAttrList.length;i++) { this.tagContent[i].style.left = this.tagAttrList[i].cx + len - this.tagAttrList[i].offsetWidth/2 + 'px'; this.tagContent[i].style.top = this.tagAttrList[i].cy + height - this.tagAttrList[i].offsetHeight/2 + 'px'; // this.tagContent[i].style.fontSize = Math.ceil(12 * this.tagAttrList[i].scale/2) + 8 + 'px'; this.tagContent[i].style.fontSize = Math.ceil(12 * this.tagAttrList[i].scale/2) +2 + 'px'; this.tagContent[i].style.filter = "alpha(opacity="+100 * this.tagAttrList[i].alpha+")"; this.tagContent[i].style.opacity = this.tagAttrList[i].alpha; } }, depthSort(){ let aTmp = []; for (let i = 0; i < this.tagContent.length; i++) { aTmp.push(this.tagContent[i]); } aTmp.sort((item1, item2) => item2.cz - item1.cz); for (let i = 0; i < aTmp.length; i++) { aTmp[i].style.zIndex=i; } }, sineCosine (a, b, c) { this.sinA = Math.sin(a * this.dtr); this.cosA = Math.cos(a * this.dtr); this.sinB = Math.sin(b * this.dtr); this.cosB = Math.cos(b * this.dtr); this.sinC = Math.sin(c * this.dtr); this.cosC = Math.cos(c * this.dtr); } } }; </script> <style scoped> .cloud-bed { width: 250px; height: 270px; margin: auto; } .cloud-box{ position:relative; margin:20px auto 0px; width: 100%; height: 100%; background: #00000000; } .cloud-box span{ position: absolute; padding: 3px 6px; top: 0px; font-weight: bold; text-decoration:none; left:0px; background-image: linear-gradient(to bottom, red, #fff); background-clip: text; color: transparent; width: 50px; height: 50px; border-radius: 50%; text-align: center; display: flex; align-items: center; justify-content: center; /* line-height: 50px; overflow:hidden; white-space: nowrap; text-overflow: ellipsis; */ } </style>
然后在首页中将组件引进来,和之前的图表引入一样。
<el-col :xs="24" :sm="24" :lg="8"> <div class="chart-wrapper"> <div class="e-title">文章标签统计</div> <word-cloud /> </div> </el-col>
引入组件
import WordCloud from './components/WordCloud.vue' components: { PanelGroup, BarChart, PieChart, LineChart, Calendar, WordCloud },
然后运行一下项目,我们看一下页面
看着是不是还挺高大上的。这个功能模块添加完成了,最后一个我们写一下公告的展示
2.6、公告
在首页添加了公告的信息,方便我们及时的查看,但只展示最近发的前四条公告,其余的还是要去公告列表中去查看,这个就比较简单了,我们直接引用element-ui的组件即可。我选择了折叠面板来实现,感觉还挺符合这个通知公告的功能实现。
<el-col :xs="24" :sm="24" :lg="8"> <div class="chart-wrapper"> <div class="e-title">最新公告</div> <el-collapse v-model="activeName" accordion> <el-collapse-item title="一致性 Consistency" name="1"> <div>与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;</div> <div>在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。</div> </el-collapse-item> <el-collapse-item title="反馈 Feedback" name="2"> <div>控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;</div> <div>页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。</div> </el-collapse-item> <el-collapse-item title="效率 Efficiency" name="3"> <div>简化流程:设计简洁直观的操作流程;</div> <div>清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;</div> <div>帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。</div> </el-collapse-item> <el-collapse-item title="可控 Controllability" name="4"> <div>用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;</div> <div>结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。</div> </el-collapse-item> </el-collapse>
里面的内容暂时先不修改,我们下一篇处理后端的接口,再统一对接后端接口数据。
好啦,首页的功能基本上都写完了,我让大家看一下完整的页面效果
这个是不是很哇塞,感觉很beautiful。
首页完整代码如下:
<template> <div class="dashboard-container"> <panel-group ></panel-group> <!-- 数据分析 --> <el-row :gutter="32" class="row-chart"> <el-col :xs="24" :sm="24" :lg="8"> <div class="chart-wrapper"> <bar-chart /> </div> </el-col> <el-col :xs="24" :sm="24" :lg="8"> <div class="chart-wrapper"> <pie-chart /> </div> </el-col> <el-col :xs="24" :sm="24" :lg="8"> <div class="chart-wrapper"> <line-chart /> </div> </el-col> </el-row> <!-- 功能 --> <el-row :gutter="32" class="row-chart"> <el-col :xs="24" :sm="24" :lg="8"> <div class="chart-wrapper"> <div class="e-title">文章标签统计</div> <word-cloud /> </div> </el-col> <el-col :xs="24" :sm="24" :lg="8"> <div class="chart-wrapper"> <div class="e-title">最新公告</div> <el-collapse v-model="activeName" accordion> <el-collapse-item title="一致性 Consistency" name="1"> <div>与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;</div> <div>在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。</div> </el-collapse-item> <el-collapse-item title="反馈 Feedback" name="2"> <div>控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;</div> <div>页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。</div> </el-collapse-item> <el-collapse-item title="效率 Efficiency" name="3"> <div>简化流程:设计简洁直观的操作流程;</div> <div>清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;</div> <div>帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。</div> </el-collapse-item> <el-collapse-item title="可控 Controllability" name="4"> <div>用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;</div> <div>结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。</div> </el-collapse-item> </el-collapse> </div> </el-col> <el-col :xs="24" :sm="24" :lg="8"> <div class="chart-wrapper"> <div class="con"> <div class="now-data-myself"> <div class="now-data-myself-time">{{ date }}</div> <div class="now-data-myself-week">{{ week }}</div> </div> <Calendar v-on:choseDay="clickDay" v-on:changeMonth="changeDate" v-on:isToday="clickToday" ></Calendar> </div> </div> </el-col> </el-row> </div> </template> <script> import PanelGroup from './components/PanelGroup' import BarChart from './components/BarChart' import PieChart from './components/PieChart' import LineChart from './components/LineChart' import Calendar from 'vue-calendar-component' import WordCloud from './components/WordCloud.vue' export default { name: 'Dashboard', components: { PanelGroup, BarChart, PieChart, LineChart, Calendar, WordCloud }, data() { return { date: "", week: "", activeName: '1' } }, created() { var now = new Date(); this.date = now.getDate();//得到日期 var day = now.getDay();//得到周几 var arr_week = new Array("星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"); this.week = arr_week[day]; }, methods: { clickDay(data) {}, changeDate(data) {}, clickToday(data) {} }, } </script> <style rel="stylesheet/scss" lang="scss"> .dashboard { &-container { padding: 32px; background-color: #f0f2f5; } } .chart-wrapper { background: #fff; padding: 16px 16px 0; margin-bottom: 32px; } .row-chart{ margin-top: 30px; } .now-data-myself { width: 40%; position: absolute; border-right: 1px solid rgba(227, 227, 227, 0.6); } .con { position: relative; max-width: 400px; margin: auto; } .con .wh_content_all { background: transparent !important; } .wh_top_changge li { color: #F56C6C !important; font-size: 15px !important; } .wh_content_item, .wh_content_item_tag { color: #303133 !important; } .wh_content_item .wh_isToday { background: #00d985 !important; color: #fff !important; } .wh_content_item .wh_chose_day { background: #409EFF !important; color: #ffff !important; } .wh_item_date:hover { background: rgb(217, 236, 255) !important; border-radius: 100px !important; color: rgb(102, 177, 255) !important; } .wh_jiantou1[data-v-2ebcbc83] { border-top: 2px solid #909399; border-left: 2px solid #909399; width: 7px; height: 7px; } .wh_jiantou2[data-v-2ebcbc83] { border-top: 2px solid #909399; border-right: 2px solid #909399; width: 7px; height: 7px; } .wh_top_tag[data-v-2ebcbc83] { color: #409EFF; border-top: 1px solid rgba(227, 227, 227, 0.6); border-bottom: 1px solid rgba(227, 227, 227, 0.6); } .wh_container[data-v-2ebcbc83] { max-width: 400px; } .wh_top_changge[data-v-2ebcbc83] { display: flex; width: 50%; margin-left: 43%; } .now-data-myself-time { color: #F56C6C; font-size: 28px; margin-left:60px; height: 33px; font-family: "Helvetica Neue"; } .now-data-myself-week { margin-left:60px; font-size: 10px; color: #909399; } .wh_top_changge .wh_content_li[data-v-2ebcbc83] { font-family: Helvetica; } </style>
好啦,下一篇会写后端的接口,完成数据的渲染就结束了,我上次发起的投票关于上线发布的事情,我后边可能不会更新那一篇了,但是应该会放在下一个专栏里,但是有些好学的小伙伴想学习可以去下面的公众号找我,我可以给你提供思路和部署的方法,我这里就不再以文章的形式写出来了,一篇文章我要写好久的,熬好几夜才搞完,希望大家理解。多多给我点点赞,推荐一下。
最后最后,希望大家再评论区给我留点意见和要搞的技术,要不然我光写大家只看我写的,对提升帮助比较小。感谢大家!