13.4 开发文章详情页面
文章详情页面是本项目中比较复杂的页面,包括 Markdown 渲染页面,点赞、收藏、评论,还有目录解析等其他功能。该页面整体是左中右三栏布局,分别是操作区域、内容区域、目录区域,我们定义模版结构如下:
<div class="article-detail-page">
<div class="handle-box">
<!-- 操作区域 -->
</div>
<div class="main-box fx">
<div class="content-panel">
<!-- 内容区域 -->
</div>
<div class="other-panel">
<!-- 目录区域 -->
</div>
</div>
</div>
13.4.1 开发左侧操作区域功能
左侧区域有点赞、收藏、评论三个按钮,点击后调用接口并根据执行结果计算状态(比如:计算点赞数量、计算我是否已点赞),因此先从获取数据开始。
(1)在已有的文章 Store 中定义获取文章详情、点赞/取消点赞、收藏/取消收藏的方法:
// stores/article/index.ts
{
// 文章详情
async getArtDetail(id: string, fun: (data: ArticleType) => void) {
try {
let res: any = await request.get('/arts/detail/' + id)
fun(res)
} catch (error) {
console.log(error)
}
},
// 操作点赞/收藏
async togglePraise(data: any, fun: (bool: boolean) => void) {
try {
data.target_type = 1
let res: any = await request.post('/praises/toggle', data)
fun(res.action == 'create' ? true : false)
} catch (error) {
console.log(error)
}
},
}
(2)创建文件 views/article/detail.vue 表示文章详情组件,操作区域的模版代码如下:
<div class="handle-box" v-if="article">
<div
:class="['icon-act fx-c', { active: article && article.is_praise }]"
@click="toPraiseOrStart(1)"
>
<el-badge :value="article.praises" :hidden="article.praises == 0">
<span class="iconfont icon-zan2"></span>
</el-badge>
</div>
<div class="icon-act fx-c">
<el-badge :value="article.comments" :hidden="article.comments == 0">
<span class="iconfont icon-wenda2"></span>
</el-badge>
</div>
<div
:class="['icon-act fx-c', { active: article && article.is_start }]"
@click="toPraiseOrStart(2)"
>
<el-badge :value="article.stars" :hidden="article.stars == 0">
<span class="iconfont icon-xing"></span>
</el-badge>
</div>
</div>
上方代码中,文章的点赞数量、收藏数量、评论数量等都已经通过接口返回,我们取字段展现即可。当点击按钮时,触发 toPraiseOrStart() 方法,在这里执行操作逻辑。
(3)编写操作区域的 JS 代码,包括获取文章数据和定义 toPraiseOrStart() 方法,如下:
<script setup lang="ts">
import { useRoute } from "vue-router";
import { onMounted, ref } from "vue";
const route = useRoute();
const toPraiseOrStart = (type: 1 | 2) => {
let { _id, created_by } = article.value;
let form = {
target_id: _id,
target_user: created_by,
type,
};
store.togglePraise(form, (bool) => {
if (type == 1) {
article.value.is_praise = bool;
article.value.praises += bool ? 1 : -1;
} else {
article.value.is_start = bool;
article.value.stars += bool ? 1 : -1;
}
});
};
onMounted(() => {
let { id } = route.params;
store.getArtDetail(id as string, (data) => {
article.value = data;
});
});
</script>
因为在接口中我们将点赞和收藏存在了一个集合中,所以只需要调用一个接口并传入不同的 type 参数,即可实现点赞和收藏的功能。
(4)编写操作区域的样式,基本代码如下:
.handle-box {
position: fixed;
top: 140px;
left: 5.5rem;
.icon-act {
width: 48px;
height: 48px;
border-radius: 50%;
background: #fff;
margin-bottom: 20px;
.iconfont {
font-size: 20px;
padding: 0 12px;
}
&:hover {
color: var(--font-color2);
}
&.active {
color: var(--el-color-primary);
.el-badge__content {
background: var(--el-color-primary);
}
}
}
}
(5)为文章详情页面添加一条路由配置,使其可以通过 URL 访问:
{
path: '/article/:id',
name: 'article',
component: () => import('@/views/article/detail.vue'),
},
13.4.2 开发 Markdown 渲染组件
文章内容要通过渲染 Markdown 展示,因此我们需要一个名为 showdown 的第三方解析库,根据该库封装一个公共的 Markdown 渲染组件,步骤如下。
(1)安装 showdown 相关依赖。
$ yarn add showdown showdown-highlight
(2)创建 components/mk-render/index.vue 文件表示渲染组件,模版代码如下:
<article className="cus-mk-render" v-html="content"></article>
模版代码只是一个解析内容的标签,但我们需要定义解析后的通用样式,样式代码如下:
.cus-mk-render {
padding: 0 30px;
word-break: break-word;
line-height: 1.75;
font-weight: 400;
font-size: 15px;
overflow-x: hidden;
color: #333;
h1 {
font-size: 30px;
margin-top: 35px;
margin-bottom: 5px;
line-height: 1.5;
padding-bottom: 5px;
font-weight: 700;
}
h2 {
margin-top: 30px;
padding-bottom: 12px;
font-size: 24px;
border-bottom: 1px solid #ececec;
}
h3 {
margin-top: 35px;
margin-bottom: 10px;
}
ul,
ol {
padding-left: 28px;
}
code {
word-break: break-word;
border-radius: 2px;
overflow-x: auto;
background-color: #fff5f5;
color: #ff502c;
font-size: 0.87em;
padding: 0.065em 0.4em;
font-family: Menlo, Monaco, Consolas, Courier New, monospace;
}
p {
line-height: inherit;
margin-top: 22px;
margin-bottom: 22px;
color: #252933;
font-size: 16px;
}
pre {
position: relative;
font-size: 85%;
line-height: 1.75;
background: #f8f8f8;
code.hljs {
padding: 15px 12px;
color: #333;
margin: 0;
word-break: normal;
display: block;
overflow-x: auto;
background: transparent;
font-size: 13px;
font-family: Menlo, Monaco, Consolas, Courier New, monospace;
}
}
img {
max-width: 80%;
}
}
(3)编写组件 JS 代码,接收字符串文本并解析,代码如下:
<script lang="ts" setup>
import showdown from "showdown";
import showdownHighlight from "showdown-highlight";
import "highlight.js/lib/languages/yaml";
import { onMounted, ref } from "vue";
showdown.setOption("tables", true);
showdown.setOption("tasklists", true);
showdown.setFlavor("github");
const content = ref("");
const props = defineProps<{
content: string;
}>();
onMounted(() => {
let converter = new showdown.Converter({
extensions: [
showdownHighlight({
pre: true,
}),
],
});
content.value = converter.makeHtml(props.content);
});
</script>
(4)添加组件样式。因为渲染的样式要和编辑器预览区域的样式一致,所以导入并复用编辑器的样式即可,如下:
<style lang="less">
@import "../cus-editior/index.less";
.cus-mk-render {
padding: 0 30px;
word-break: break-word;
line-height: 1.75;
font-weight: 400;
font-size: 15px;
overflow-x: hidden;
color: #333;
.cus-markdown-style();
}
</style>
13.4.3 开发中间内容区域功能
内容区域主要是解析 Markdown 内容。引入上一步创建的渲染组件,并添加一些如标题、时间等其他元素。时间解析需要第三方模块 dayjs。
(1)安装 dayjs:
$ yarn add dayjs
(2)编写内容区域的模版代码,如下:
<div class="content-panel">
<div class="content" v-if="article">
<h1 className="art-title">{{ article.title }}</h1>
<div className="options">
<span className="uname">{{ article.user.username }}</span>
<span className="time">
{{ dayjs(article.created_at).format('YYYY-MM-DD HH:mm') }}
</span>
<span class="fx">
<span class="iconfont icon-liulan"></span>
{{ article.page_view }}
</span>
<a
className="edit"
v-if="ustore.user_info?._id == article.user._id"
@click="toEdit"
>编辑</a
>
</div>
<MkRender :content="article.content" />
</div>
</div>
(3)添加内容区域的 JS 代码如下:
import dayjs from "dayjs";
import MkRender from "@/components/mk-render/index.vue";
const toEdit = () => {
window.open("/operate/" + article.value._id);
};
(4)添加内容区域的样式如下:
.content-panel {
flex: 1;
.content {
background: #fff;
overflow: hidden;
border-radius: 4px;
.art-title {
font-size: 32px;
margin: 30px 30px 20px 30px;
font-weight: 700;
}
.options {
display: flex;
align-items: center;
margin: 0 30px;
font-size: 14px;
color: var(--font-color3);
.uname {
font-weight: 500;
}
.time {
margin: 0 16px;
}
.edit {
cursor: pointer;
margin-left: 16px;
}
}
}
}
13.4.4 开发右侧目录区域功能
右侧目录区域主要展示作者基本信息和文章目录结构,比较简单,步骤如下。
(1)在获取文章详情数据后,通过正则表达式从文章内容中取出一级和二级标题:
const directs = ref([]);
directs.value = article.content.match(/#{1,2}.*/g);
(2)编写右侧区域的模版代码如下:
<div class="other-panel">
<div class="user-pan pan" v-if="article">
<div class="fx" @click="toUser">
<el-avatar :size="48">
<img src="@/assets/avatar.png" />
</el-avatar>
<div class="rcolum">
<div class="name">{{ article.user.username }}</div>
<div class="oinfo">
<span>{{ article.user.position }}</span>
</div>
</div>
</div>
<div class="count-info">
<div class="row fx">
<span class="icarea">
<span class="iconfont icon-zan2 izan" />
</span>
<span>获得点赞 {{ article.user.good_num }}</span>
</div>
<div class="row fx">
<span class="icarea">
<span class="iconfont icon-view2" />
</span>
<span>文章被阅读 {{ article.user.read_num }}</span>
</div>
</div>
</div>
<div class="direct-pan pan">
<div class="title">目录</div>
<ul>
<template v-for="item in directs">
<li v-if="item.includes('##')"> {{ item.trim().slice(2) }}</li>
<li v-else>{{ item.trim().slice(1) }}</li>
</template>
</ul>
</div>
</div>
(3)右侧区域的目录结构样式代码如下:
.other-panel {
margin-left: 20px;
width: 300px;
.pan {
background: #fff;
border-radius: 4px;
padding: 16px 20px;
margin-bottom: 20px;
}
.direct-pan {
.title {
font-size: 16px;
border-bottom: 1px solid #e4e6eb;
}
ul {
margin-top: 12px;
li {
font-size: 14px;
padding: 6px 0px;
cursor: pointer;
position: relative;
&:hover {
color: var(--el-color-primary);
&::after {
background: var(--el-color-primary);
}
}
}
}
}
}