前言
此 nuxt-blog 项目是基于 nuxt 服务器渲染(SSR)构建
效果图
完整效果请看:sdjBlog.cn
功能描述
已经实现功能
- 注册登录
- 文章列表
- 友情链接
- 文章点赞评论
- 文章归档
- 项目列表
- 留言列表
- 看板娘背景
技术库依赖
- @nuxtjs/axios (API请求)
- @nuxtjs/style-resources (scss公共文件引入)
- element-ui (组件库)
- highlight.js (代码高亮)
- moment (时间格式处理)
- nprogress (进度条)
- nuxt (Vue框架)
项目结构
- assets 资源文件(图片、css和字体图标)
- components
- RaindropCanvas 下雨特效
- BackTop 返回顶部
- EmptyShow 空状态
- HeaderNav 头部导航
- MyForm 表单封装
- SideBar 侧边栏
- TagBox 标签展示
- layouts 布局入口页面
- pages
- article 嵌套路由
- page
- _num 文章列表
- archive 文章归档
- project 项目列表
- articleDetail
- _id 文章详情
- article 文章列表头部及侧边栏
- index 默认首页路由
- message 留言列表
- plugins 插件封装
- static 看板娘、背景资源等
- store Vuex状态管理
- utils 表单校验、标题目录导航和时间格式化等一些常用方法封装
- nuxt.config 个性化配置
侧边栏
-
根据星期获取对应励志语句展示
-
相关联系信息,如
Github、码云、后台登录页面和掘金等 -
通过文章标签以及文章排序来筛选展示文章列表
-
友情链接展示
文章归档
文章列表格式化,把列表数据中相同月份数据内容合并到新数组,重新生成月份列表数据
async asyncData({ $axios }) {
const res = await $axios.get('/blogPage/statistics/articleArchive')
let total = res.data.length
let data = res.data
let arr = []
let articleList = []
if(data.length > 0){
data.forEach(item=>{
let outerObj = { month: item._id.month, articleArr: [] }
let inObj = { articleId: item._id.id, title: item._id.title, createTime: item._id.createTime}
outerObj.articleArr.push(inObj);
arr.push(outerObj);
})
let newData = []; // 目标数组
let newObj = {};
arr.forEach((item, index) => {
if (!newObj[item.month]) {
newData.push(item);
newObj[item.month] = true;
} else {
newData.forEach(data => {
if (data.month === item.month) {
data.articleArr = [...data.articleArr, ...item.articleArr]
}
})
}
})
articleList = newData
}
return { articleList, total }
}
文章详情
给内容h标题标签添加class和抽离标题生成目录
//标题添加class
export function catalogList(content) {
// 去除marked解析生成h标题标签中的id
// content = content.replace(/(\sid\s*=[\s\'\"].*?[\s\'\"])/g,"")
const toc = content.match(/<[hH][1-6]>.*?<\/[hH][1-6]>/g)
let tocList = null
if (toc && toc.length > 0) {
toc.forEach((item, index) => {
let _toc = `<div class='rich-title' id='content-title${index}'>${item} </div>`
content = content.replace(item, _toc)
})
tocList = toToc(toc)
}else{
tocList = '<div class="catalog-title">目录(无)</div>\n'
}
let obj = {
tocList,
content
}
return obj
}
// 文章内容h标签生成标题目录
function toToc(data) {
let levelStack = []
let result = '<div class="catalog-title">目录</div>\n'
const addStartList = () => { result += `<div class="catalog-list">\n`; }
const addEndList = () => { result += '</div>\n'; }
const addLInk = (index, itemText) => { result += `<div class='catalog-link' title='${itemText}' id='title${index}'>${itemText}</div>\n`; }
data.forEach(function (item, index) {
let itemText = item.replace(/<[^>]+>/g, '') // 匹配h标签中的文字
let itemLabel = item.match(/<\w+?>/)[0] // 匹配h?标签<h?>
let levelIndex = levelStack.indexOf(itemLabel) // 判断数组里有无<h?>
// 没有找到相应<h?>标签,则将新增ul、li
if (levelIndex === -1) {
levelStack.unshift(itemLabel)
addStartList()
addLInk(index, itemText)
}
// 找到了相应<h?>标签,并且在栈顶的位置则直接将li放在此ul下
else if (levelIndex === 0) {
addLInk(index, itemText)
}
// 找到了相应<h?>标签,但是不在栈顶位置,需要将之前的所有<h?>出栈并且打上闭合标签,最后新增li
else {
while (levelIndex--) {
levelStack.shift()
addEndList()
}
addLInk(index, itemText)
}
})
// 如果栈中还有<h?>,全部出栈打上闭合标签
while (levelStack.length) {
levelStack.shift()
addEndList()
}
return result
}
点击标签目录跳转到对应位置以及滚动监听位置来高亮标题
//rich-title为内容标题添加的class
this.$nextTick(()=>{
let linkArr = document.querySelectorAll(".rich-title")
let linkTopArr = []
if(linkArr.length > 0){
linkArr.forEach(item=>{
linkTopArr.push(item.offsetTop - 130)
})
linkTopArr.push(2 * linkTopArr[linkTopArr.length-1])
this.linkTopArr = linkTopArr
}
window.addEventListener('scroll', this.handleScroll, true)
})
// 滚动监听
handleScroll(){
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
let {linkTopArr} = this
const linkArr = document.querySelectorAll(".catalog-link")
if(linkArr.length > 0){
for(let i = 0; i < linkTopArr.length; i++){
let start = linkTopArr[i]
let top = linkTopArr[i + 1]
if (scrollTop >= start && scrollTop <= top) {
// 获取文章滚动到目录的目标元素
linkArr.forEach((item) => {
item.classList.remove('link-active')
})
if(linkArr[i]){
linkArr[i].classList.add('link-active')
}
break;
}
}
}
}
留言列表
鼠标拖拽留言移动,当拖拽移动到最顶部时,出现下雨特效
//绑定鼠标按下事件
<div class="box-item" v-for='item in messageList' :key='item._id' :style="item.style" @mousedown.prevent="mousedown" v-show='initData'></div>
// 拖拽留言,按下鼠标左键时间
mousedown (e) {
this.isDrag = true
this.dragObj = e.currentTarget
this.dragMouseOffset = this.getCurrentOffset(e)
this.maxDragOffset = this.getMaxDragOffset(e)
this.LiftingZIndex()
document.addEventListener('mousemove', this.mousemove)
document.addEventListener('mouseup', this.mouseup)
},
// 鼠标距离元素的位置
getCurrentOffset (e) {
let target = e.target
let offset = {
x: 0,
y: 0
}
while (target.className.indexOf('box-item') < 0) {
offset = {
x: offset.x + target.offsetLeft + target.clientLeft,
y: offset.y + target.offsetTop + target.clientTop
}
target = target.offsetParent
}
offset = {
x: e.offsetX + offset.x,
y: e.offsetY + offset.y + 60,
}
return offset
},
// 获取
getMaxDragOffset (e) {
const target = e.currentTarget
let w = target.offsetWidth,
h = target.offsetHeight
return {
w: this.screen.width - w,
h: this.screen.height - h - 60
}
},
// 当前元素提升层级
LiftingZIndex () {
this.noteIndex += 1
this.dragObj.style.zIndex = this.noteIndex
},
mousemove (e) {
if (!this.isDrag) return
let {x, y} = this.getCurrentEleCoords(e)
let {w, h} = this.maxDragOffset
if (x < 0) x = 0
if (x > w) x = w
if (y < 0) y = 0
if (y > h) y = h
this.dragObj.style.left = `${x}px`
this.dragObj.style.top = `${y}px`
if(y <= 0){
this.showRaindropCanvas = true
}
},
// 鼠标当前距离
getCurrentEleCoords(e){
return {
x: e.clientX - this.dragMouseOffset.x,
y: e.clientY - this.dragMouseOffset.y
}
},
mouseup () {
this.isDrag = false
}
看板娘背景
通过在nuxt.config.js中引入资源
head: {
title: '个人博客',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'keywords', name: 'keywords', content: '前端,博客,vue,node'},
{ hid: 'description', name: 'description', content: '个人技术知识博客,vue,node' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
],
script:[
{
src:'/live2dw/lib/L2Dwidget.min.js' //看板娘
},
{
src:'/navigator.js' //设备类型判断
},
{
src:'/bgShow.js', //背景特效
body: true
}
]
},
//在layout中进行初始化渲染
created() {
if (process.browser) {
setTimeout(() => {
window.L2Dwidget.init({
pluginRootPath: "/live2dw/",
pluginJsPath: "lib/",
pluginModelPath: `live2d-widget-model-shizuku/assets/`,
tagMode: false,
debug: false,
model: {
jsonPath: `/live2dw/live2d-widget-model-shizuku/assets/shizuku.model.json`
},
display: {
position: "right",
width: 220,
height: 400,
hOffset: 30,
vOffset: -45
},
react: { opacity: 0.7 },
mobile: { show: true },
log: false
});
}, 400);
}
}
说明
线上使用pm2部署,把.nuxt、static、nuxt.config.js和package.json文件放到服务器上,执行npm install安装依赖,pm2 start npm --name "nuxtBlog" -- run start来启动服务,package.json中config配置端口
建立安装
# 安装依赖
$ npm install
# 开发环境
$ npm run dev
# 打包生产环境
$ npm run build
$ npm run start
# 静态打包
$ npm run generate
项目地址:
项目系列文章: