Vue+Nuxt 博客展示

2,617 阅读3分钟

首页

前言

此 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

项目地址:

前台展示:https://gitee.com/sdj_work/blog-page(Vue/Nuxt/uni-app)

管理后台:https://gitee.com/sdj_work/blog-admin(Vue/React)

后端Node:https://gitee.com/sdj_work/blog-node(Express/Koa)

博客地址:https://sdjBlog.cn/

项目系列文章:

Vue+Nuxt 博客展示

Vue+uniapp 博客展示

Vue+ElementUI 后台博客管理

node + koa + mongodb 博客接口开发

node + express + mongodb 博客接口开发