⭐ 作者简介:码上言
⭐ 代表教程:Spring Boot + vue-element 开发个人博客项目实战教程
⭐专栏内容:零基础学Java、个人博客系统
⭐我的文档网站:http://xyhwh-nav.cn/
👦 学习讨论群:530826149
后端代码gitee地址:https://gitee.com/whxyh/personal_blog
前端代码gitee地址:https://gitee.com/whxyh/personal_vue
https://www.bilibili.com/video/BV1sg4y1A7Kv/?vd_source=dc7bf298d3c608d281c16239b3f5167b
这一篇将是我们项目开发的最后一篇文章了,到这里该和大家说再见了,这个项目从开始写到现在刚好一年了,有时间就写写,中间断了好几个月的时间,看到好多人说对他们很有用,我写的也就变成了有意义的事情,希望大家都不忘初心,牢记使命,认真的学习技术和好好地生活。
正如士兵突击中的许三多说的,好好活,就是做有意义的事,做有意义的事情,就是好好活着。
这里给大家预告一下,这个教程已经接近尾声了,看到大家给我的好多反馈,说的学到了很多东西,我感觉到十分的欣慰,感觉自己的付出没有白费,有的小伙伴说比某些机构的课程学的还多,我感觉只要好好地学习,都是可以的,哪怕你只是拿这个项目去最毕设或者其他的之类的,反正技多不压身,这个也涵盖了好多的东西,基本的入门也可以了。
我在想要不要再搞一个大一点的项目,将知识点再扩大一些,比如现在没有权限的操作、我们登录再加上短信验证码、文件的上传和解析、redis的实际运用等操作、自动化部署代码、原型设计、日志记录、小程序学习等新的技术,会更加完善做项目的流程和规范,基本上达到全栈的技术
。
我再考虑是不是要开通付费的专栏,大家可以根据自身的需求来学习,感觉自己需要学习就来学习,不需要看这个教程可以了就不要订阅。我感觉肯定会比花上万的去培训要实在的多,大家自己衡量。
欢迎大家给我提改进意见或者要加入什么技术,我尽量用项目来整合这些技术加入实际的应用。感谢各位!
这是上一篇的bug,我在测试文章添加后,然后再点击编辑后,发现标签的值并没有,我查看了接口返回的数据为空,发现是后端添加文章存入缓存的问题,这里修改一下,只要加一行代码即可,大家可以提前想一下。
先分析一下我们调用的方法findById。
@Override
public ArticleVO findById(Integer articleId) {
Article article = articleMap.get(articleId);
。。。。。。
只看这一句即可,文章的数据来自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());
}
}
我们在文章的数据表中预留了一个文章的字数,一开始的时候我直接赋值的是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()));
查看页面效果:
我在测试的时候,发布文章没有选择从数据库查出来的分类,而是自己创建的一个分类,点击发布会报错。
首先定位到代码错误的信息。
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;
}
这里我把邮箱的配置进行了修改,原来配置的邮箱信息是在代码里配置的,维护不太方便,我把它提到了配置文件中了。以后修改邮箱信息直接修改配置文件,就不需要找代码了。打开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));
}
}
添加完之后,再去测试下。
我们现在的登录页面非常的原始,不太好看,俗话说人靠衣服马靠鞍,我们也将登录的入口进行改造,后端的逻辑不用动,我们只改前端代码即可。
<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>
这里只修改了一些样式和添加了一个背景图片。也算是有点样子了。
这一块也就是对应的我们的首页,刚一进来就能直观看到的,我们尽量做的美观一点,逼格高一点。
首先我们首页的顶部先放四个导航菜单,用来展示我们的一些重要的数据。
下面的开发我先写前端页面布局完成之后,再去写后端的代码。
这里使用了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
改造完之后是这样的页面
这里先介绍一下echarts
官网:https://echarts.apache.org/zh/index.html
什么是echarts?
它是一个基于 JavaScript 的开源可视化图表库,可以用于我们对数据分析的可视化展示,是我们的数据在图表中清晰可见,一般领导比较喜欢看这种分析的图表。
具体的如何使用这里不再一一讲述了,可以查看官方给的文档,有快速上手的教程可以学习。
这里我们先安装一下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样式
然后我们打开页面,查看一下图表有没有渲染出来
此时就渲染出来了,一个我们会了,我们再添加两个图表分别统计分类和访问量。
和之前的那个图表一样,我这里不再一一讲述,只把代码给大家展现出来。
在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>
然后我们看一下效果
是不是感觉还挺哇塞的,其实那种看着非常高大上的大屏展示就是这种画出来的,我后边应该会写一篇如何制作大屏的页面的文章,大家可以等待一下。
现在我们的首页是不是有点样子了,越来越完善了,是有点系统的样子了。
接下来我们再美化一下首页,再添加一点小功能。
大家可以参考这个文章添加日历,以下就是我参照实现的,稍微做了修改。
参考文章: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>
我们把这个去掉即可,想知道什么原因的可以去学习一下。
然后看一下我们的页面。
接下来我们来写一下这个词云,这个主要是美化我们的页面,做一些特效使用,也多学一些常用的小功能。
我这个是在网上找了一个词云的代码,我将它封装成了一个组件,直接在首页引用即可。
在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
},
然后运行一下项目,我们看一下页面
看着是不是还挺高大上的。这个功能模块添加完成了,最后一个我们写一下公告的展示
在首页添加了公告的信息,方便我们及时的查看,但只展示最近发的前四条公告,其余的还是要去公告列表中去查看,这个就比较简单了,我们直接引用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>
好啦,下一篇会写后端的接口,完成数据的渲染就结束了,我上次发起的投票关于上线发布的事情,我后边可能不会更新那一篇了,但是应该会放在下一个专栏里,但是有些好学的小伙伴想学习可以去下面的公众号找我,我可以给你提供思路和部署的方法,我这里就不再以文章的形式写出来了,一篇文章我要写好久的,熬好几夜才搞完,希望大家理解。多多给我点点赞,推荐一下。
最后最后,希望大家再评论区给我留点意见和要搞的技术,要不然我光写大家只看我写的,对提升帮助比较小。感谢大家!
上一篇:Spring Boot + vue-element 开发个人博客项目实战教程(二十四、文章管理页面开发(3))
下一篇:Spring Boot + vue-element 开发个人博客项目实战教程(二十六、项目完善及扩展(后端部分))