Skip to content

13.4 开发文章详情页面

文章详情页面是本项目中比较复杂的页面,包括 Markdown 渲染页面,点赞、收藏、评论,还有目录解析等其他功能。该页面整体是左中右三栏布局,分别是操作区域、内容区域、目录区域,我们定义模版结构如下:

html
<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 中定义获取文章详情、点赞/取消点赞、收藏/取消收藏的方法:

js
// 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 表示文章详情组件,操作区域的模版代码如下:

js
<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() 方法,如下:

vue
<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)编写操作区域的样式,基本代码如下:

less
.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 访问:

js
{
  path: '/article/:id',
  name: 'article',
  component: () => import('@/views/article/detail.vue'),
},

13.4.2 开发 Markdown 渲染组件

文章内容要通过渲染 Markdown 展示,因此我们需要一个名为 showdown 的第三方解析库,根据该库封装一个公共的 Markdown 渲染组件,步骤如下。

(1)安装 showdown 相关依赖。

sh
$ yarn add showdown showdown-highlight

(2)创建 components/mk-render/index.vue 文件表示渲染组件,模版代码如下:

html
<article className="cus-mk-render" v-html="content"></article>

模版代码只是一个解析内容的标签,但我们需要定义解析后的通用样式,样式代码如下:

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;
  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 代码,接收字符串文本并解析,代码如下:

vue
<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)添加组件样式。因为渲染的样式要和编辑器预览区域的样式一致,所以导入并复用编辑器的样式即可,如下:

vue
<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:

sh
$ yarn add dayjs

(2)编写内容区域的模版代码,如下:

html
<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>
        &nbsp;{{ 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 代码如下:

js
import dayjs from "dayjs";
import MkRender from "@/components/mk-render/index.vue";
const toEdit = () => {
  window.open("/operate/" + article.value._id);
};

(4)添加内容区域的样式如下:

less
.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)在获取文章详情数据后,通过正则表达式从文章内容中取出一级和二级标题:

js
const directs = ref([]);
directs.value = article.content.match(/#{1,2}.*/g);

(2)编写右侧区域的模版代码如下:

html
<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>获得点赞 &nbsp;{{ article.user.good_num }}</span>
      </div>
      <div class="row fx">
        <span class="icarea">
          <span class="iconfont icon-view2" />
        </span>
        <span>文章被阅读 &nbsp;{{ 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('##')">&nbsp;{{ item.trim().slice(2) }}</li>
        <li v-else>{{ item.trim().slice(1) }}</li>
      </template>
    </ul>
  </div>
</div>

(3)右侧区域的目录结构样式代码如下:

less
.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);
          }
        }
      }
    }
  }
}