Vue3+TypeScript+Django Rest Framework 搭建个人博客(四):博客页面

博客网站最重要的是有一个给用户浏览文章的页面,也就是博客网站的前台,用户通过这个页面可以查找文章,浏览文章详情,评论,点赞等。

大家好,我是落霞孤鹜,上一篇我们已经博客的管理后台功能,这一章我们开始搭建博客的前台,实现对博客网站文章的查看,浏览,评论,点赞等功能。我同样按照一个完整的功能,从需求分析到代码编写来阐述如何实现。

一、需求分析

作为一个完整的博客网站,前台是内容呈现的核心部分,大部分的博客搭建文章着重介绍的也是这一部分。这里我们从实际需要出发,整理了如下需求要点:

  1. 首页: 主要展示整个博客网站的文章,一般按照发布时间倒序呈现,展示标题,摘要,浏览量,点赞量,评论量,留言量等内容,提供标签筛选
  2. 文章详情:主要用来展示文章的详情,涵盖文章的所有细节,同时提供文章的章节目录导航、点赞、评论功能。
  3. 文章分类:通过分类呈现文章列表,方便用户通过类型快速查找感兴趣的文章。
  4. 归档:按照年份倒序呈现博客网站的文章列表。
  5. 关于:一般介绍博客的博主情况和博客网站的主题信息。

以上功能也算是一套简单的个人博客网站的核心功能框架。

二、后端接口部分

后端接口部分在上一篇的管理后端中,已经全部实现,这里就不再重复介绍。

三、前端界面部分

前端按照需求,我们从首页,文章详情,文章分类,归档,关于五个部分呈现。这一部分的页面全部放在src/views/client文件中。

首页的功能,实际上是一个文章列表展示,而分类页面是增加了一个分类树的文章列表展示,因此在设计页面的时候,将文章展示列表作为一个组件,这样分类展示可以通过列表和分类两个组件拼装而成。

关于页面可以类比为一篇介绍博客和博主的文章详情页面,因此,可以将文章详情展示作为一个组件,这样可以完成对详情页和关于页的支撑。

3.1 首页

3.1.1 Type

在处理管理后台时,已经在src/types/index.ts文件中定义好了文章相关的interfaceArticle, ArticleArray, ArticleParams

3.1.2 API

在处理管理后台时,已经在src/api/service.ts文件中定义好了方法getArticleList

3.1.3 Component

依据上面的分析,我们需要将文章列表封装成一个组件,因此在src/components下新增文件ArticleList.vue,通过点击文章,在一个新的页面中查看文章详情,代码如下:




处理页面在请求后端时的加载状态组件和结束加载状态后的组件。

src/components下新增文件Loading.vue,代码如下:




src/components下新增文件EndLoading.vue,代码如下:




3.1.4 Utils层

在文章列表中,我们为了更好的体验,对图片展示提供了限流处理和无极滚动加载。工具方法有:getDocumentHeightgetQueryStringByNamegetScrollTopgetWindowHeightthrottle,在src/utils/index.ts增加代码:

// fn是我们需要包装的事件回调, delay是时间间隔的阈值
export function throttle(fn: Function, delay: number) {
    let last = 0,
        timer: any = null;
    return function (this: any) {
        let context = (this as any);
        let args = arguments;
        let now = +new Date();

        if (now - last < delay) {
            clearTimeout(timer);
            timer = setTimeout(function () {
                last = now;
                fn.apply(context, args);
            }, delay);
        } else {
            last = now;
            fn.apply(context, args);
        }
    };
}

//根据 QueryString 参数名称获取值
export function getQueryStringByName(name: string) {
    let result = window.location.search.match(
        new RegExp("[?&]" + name + "=([^&]+)", "i")
    );
    if (result == null || result.length < 1) {
        return "";
    }
    return result[1];
}

//获取页面顶部被卷起来的高度
export function getScrollTop() {
    return Math.max(
        //chrome
        document.body.scrollTop,
        //firefox/IE
        document.documentElement.scrollTop
    );
}

//获取页面文档的总高度
export function getDocumentHeight() {
    //现代浏览器(IE9+和其他浏览器)和IE8的document.body.scrollHeight和document.documentElement.scrollHeight都可以
    return Math.max(
        document.body.scrollHeight,
        document.documentElement.scrollHeight
    );
}

//页面浏览器视口的高度
export function getWindowHeight() {
    return document.compatMode === "CSS1Compat"
        ? document.documentElement.clientHeight
        : document.body.clientHeight;
}

3.1.5 View

修改src/views/client/Home.vue文件,编写如下代码:







3.1.5 Router

由于主页的路由已经在src/route/index.ts 文件中配置,这里增加Articles的路由。

{
        path: "/articles",
        name: "Articles",
        component: () =>
            import("../views/admin/Home.vue")
},

3.1.6 Less

src下新增文件夹less,新增index.less 代码如下:

body {
  padding: 10px;
  margin: 0;
}

a {
  text-decoration: none;
}

.clearfix:before,
.clearfix:after {
  display: table;
  content: '';
}

.layout {
  display: flex;
}

.right {
  width: 350px;
}

.left {

  flex: 1;
  padding-right: 20px !important;
}

.clearfix:after {
  clear: both;
}

.fl {
  float: left;
}

.fr {
  float: right;
}


h1,
h2,
h3,
h4,
h5,
h6 {
  margin-top: 1em;
}

strong {
  font-weight: bold;
}

p > code:not([class]) {
  padding: 2px 4px;
  font-size: 90%;
  color: #c7254e;
  background-color: #f9f2f4;
  border-radius: 4px;
}

img {
  max-width: 100%;
}

.container {
  width: 1200px;
  margin: 0 auto;
}

.article-detail {
  img {
    /* 图片居中 */
    display: flex;
    max-width: 100%;
    margin: 0 auto;
  }

  table {
    text-align: center;
    border: 1px solid #eee;
    margin-bottom: 1.5em;
  }

  th,
  td {
    // text-align: center;
    padding: 0.5em;
  }

  tr:nth-child(2n) {
    background: #f7f7f7;
  }
}

.article-detail {
  font-size: 16px;
  line-height: 30px;
}

.anchor-fix {
  position: relative !important;
  top: -80px !important;
  display: block !important;
  height: 0 !important;
  overflow: hidden !important;
}

.article-detail .desc ul,
.article-detail .desc ol {
  color: #333333;
  margin: 1.5em 0 0 25px;
}

.article-detail .desc h1,
.article-detail .desc h2 {
  border-bottom: 1px solid #eee;
  padding-bottom: 10px;
}

.article-detail .desc a {
  color: #009a61;
}

.article-detail blockquote {
  margin: 0 0 1em;
  background-color: rgb(220, 230, 240);
  padding: 1em 0 0.5em 0.5em;
  border-left: 6px solid rgb(181, 204, 226);
}

src/App.vuestyle部分,增加导入index.less的代码

@import url("./less/index.less");

3.2 文章详情

文章详情通过路径中的query参数传递文章id的方式区别不同的文章,这样的好处是方便文章可以通过url实现分享,比如想发表在公众号中,原文链接就可以直接用该URL

3.2.1 Type

文章详情中涉及到文章, 分类,标签,点赞、评论,结合之前已经定义的内容,在src/types/index.ts文件中增加代码如下:

export interface Like {
    article: number,
    user: number,
}

3.2.2 API

src/api/service.ts编写如下代码:

export function postLikeArticle(data: Like) {
    return request({
        url: '/like/',
        method: 'post',
        data,
    })
}

export function getArticleComments(articleId: number) {
    return request({
        url: '/comment/',
        method: 'get',
        params: {
            article: articleId,
        },
    }) as unknown as ResponseData
}

export function addComment(data: CommentPara) {
    return request({
        url: '/comment/',
        method: 'post',
        data
    })
}

3.2.3 Component

这里需要用到评论列表,评论组件。

src/components下新增文件Comment.vue,负责新增评论,代码如下:





src/components下新增文件CommentList.vue,负责展示文章的评论列表,代码如下:





3.2.4 Util层

由于我们编写的文章正文是用markdown的方式记录的,而在博客网站上需要展示的是HTML,因此我们需要在展示之前将markdown转换成html,同时能展示文章的章节目录,所以先安装依赖

yarn add [email protected]

src/utils下新增文件markdown.ts,编写代码如下:

import highlight from 'highlight.js'
// @ts-ignore
import marked from 'marked'

const tocObj = {
    add: function (text: any, level: any) {
        let anchor = `#toc${level}${++this.index}`;
        this.toc.push({anchor: anchor, level: level, text: text});
        return anchor;
    },

    toHTML: function () {
        let levelStack: any = [];
        let result = "";
        const addStartUL = () => {
            result += '
    '; }; const addEndUL = () => { result += "
\n"; }; const addLI = (anchor: any, text: any) => { result += '
  • ' + text + "
  • \n"; }; this.toc.forEach(function (item: any) { let levelIndex = levelStack.indexOf(item.level); // 没有找到相应level的ul标签,则将li放入新增的ul中 if (levelIndex === -1) { levelStack.unshift(item.level); addStartUL(); addLI(item.anchor, item.text); } // 找到了相应level的ul标签,并且在栈顶的位置则直接将li放在此ul下 else if (levelIndex === 0) { addLI(item.anchor, item.text); } // 找到了相应level的ul标签,但是不在栈顶位置,需要将之前的所有level出栈并且打上闭合标签,最后新增li else { while (levelIndex--) { levelStack.shift(); addEndUL(); } addLI(item.anchor, item.text); } }); // 如果栈中还有level,全部出栈打上闭合标签 while (levelStack.length) { levelStack.shift(); addEndUL(); } // 清理先前数据供下次使用 this.toc = []; this.index = 0; return result; }, toc: [] as any, index: 0 }; class MarkUtils { private readonly rendererMD: any; constructor() { this.rendererMD = new marked.Renderer() as any; this.rendererMD.heading = function (text: any, level: any, raw: any) { let anchor = tocObj.add(text, level); return `${text}\n`; }; this.rendererMD.table = function (header: any, body: any) { return '' + header + body + '
    ' } highlight.configure({useBR: true}); marked.setOptions({ renderer: this.rendererMD, headerIds: false, gfm: true, // tables: true, breaks: false, pedantic: false, sanitize: false, smartLists: true, smartypants: false, highlight: function (code: any) { return highlight.highlightAuto(code).value; } }); } async marked(data: any) { if (data) { let content = await marked(data); let toc = tocObj.toHTML(); return {content: content, toc: toc}; } else { return null; } } } const markdown: any = new MarkUtils(); export default markdown;

    3.2.5 View

    src/views/client下新增文件ArticleDetail.vue文件,编写如下代码:

    
    
    
    
    

    3.2.6 Router

    定义route来完成路由跳转。在src/route/index.ts 文件中新增代码:

    {
            path: "/article/",
            name: "ArticleDetail",
            component: () =>
                import("../views/client/ArticleDetail.vue")
    },
    

    3.3 分类

    3.3.1 Store

    为了实现文章列表组件的复用,我们在Store保存当前的文章的检索条件,调整后的src/store/index.ts

    import {InjectionKey} from 'vue'
    import {createStore, Store} from 'vuex'
    import { Nav, User, ArticleParams} from "../types";
    
    export interface State {
        user: User,
        navIndex: string,
        navs: Array

    3.3.2 View

    通过表格查看评论,在src/views/client下新增文件Catalog.vue文件,编写如下代码:

    
    
    
    
    
    

    3.3.5 Router

    定义route来完成路由跳转。在src/route/index.ts 文件中新增代码:

     {
            path: '/catalog',
            name: 'Catalog',
            component: () =>
                import("../views/client/Catalog.vue")
        },
    

    3.4 归档

    3.4.1 Type

    src/types/index.ts文件中增加代码如下:

    export interface PageInfo {
        page: number,
        page_size: number
    }
    
    export interface ArticleArchiveList {
        year: number,
        list: Array
    | any }

    3.4.2 API

    列表查询,在src/api/service.ts编写如下代码:

    export function getArchiveList(params: PageInfo) {
        return request({
            url: '/archive/',
            method: 'get',
            params
        })
    }
    

    3.4.3 Store

    src/store/index.ts 中增加如下代码:

    export const SET_NAV_INDEX_BY_ROUTE = 'setNavIndexByRoute'
    

    src/store/index.ts 中的store中增加如下代码:

    actions: {
            setNavIndexByRoute({commit, state}, route: string) {
                const index = state.navs.findIndex(r => r.path === route)
                if (state.navIndex === state.navs[index].index)
                    return
                if (index > -1) {
                    commit(SET_NAV_INDEX, state.navs[index].index)
                }
            }
        }
    

    3.4.4 View

    src/views/client下新增文件Archive.vue文件,编写如下代码:

    
    
    
    
    

    3.4.5 Router

    定义route来完成路由跳转。在src/route/index.ts 文件中新增代码:

    {
            path: "/archive/",
            name: "Archive",
            component: () =>
                import("../views/client/Archive.vue")
        },
    

    3.5 关于

    3.5.1 改造index.html

    
    
    
        
        
        
        微谈小智
    
        
        
        
        
    
    
    

    3.5.2 View

    通过表格查看评论,在src/views/client/ArticleDetail.vue文件的 180~185 行增加了对about的处理文件,如下代码:

    const route = useRoute()
        if (route.path === '/about') {
          state.params = 1
        } else {
          state.params = Number(route.query.id)
        }
    

    3.5.2 Router

    定义route来完成路由跳转。在src/route/index.ts 文件中新增代码:

    {
            path: '/about',
            name: 'About',
            component: () =>
                import("../views/client/ArticleDetail.vue")
        },
    

    至此,博客针对用户的页面全部开发完成。

    四、界面效果

    4.1 首页

    image-20210823235213587

    4.2 分类

    image-20210823235241587

    4.3 归档

    image-20210823235400251

    4.4 文章详情和关于

    image-20210823235337357

    五、项目代码

    项目的代码按照章节的代码进行的提交,能从提交记录中看到每一个模块是如何添加进去的。

    个人博客地址:微谈小智 (longair.cn)

    前端代码地址:https://gitee.com/Zhou_Jimmy/blog-frontend.git

    后端代码地址:https://gitee.com/Zhou_Jimmy/blog-backend.git

    你可能感兴趣的:(Vue3+TypeScript+Django Rest Framework 搭建个人博客(四):博客页面)