Django-Vue搭建个人博客:发表文章

5912 views, 2021/03/28 updated   Go to Comments

把用户管理做的七七八八后,就可以继续愉快的开发博客文章有关的功能了,这才是博客的核心啊。

本章完成博客文章的发表

准备工作

一般来说,博客是只允许博主自己发表文章的,因此之前设计的接口就有点缺陷了,它没有返回用户的权限信息。不过没关系,改起来也容易。

修改后端文件 user_info/serializers.py ,增加返回当前用户是否为超级用户的信息:

# user_info/serializers.py

...
class UserRegisterSerializer(serializers.ModelSerializer):
    ...
    class Meta:
        ...
        fields = [
            ...
            'is_superuser'
        ]
        extra_kwargs = {
            ...
            'is_superuser': {'read_only': True}
        }

由于博客文章的分类、标签通常不会太多,因此对这两个接口,为了方便起见我并不想翻页而是希望一次请求直接返回所有的数据。

所以修改后端文件 article/views.py

# article/views.py
...
class TagViewSet(viewsets.ModelViewSet):
    ...
    pagination_class = None


class CategoryViewSet(viewsets.ModelViewSet):
    ...
    pagination_class = None

...

这样就可以了,并且不影响其他接口。

回到前端编写。

发表文章需要一个新的页面,因此新建 frontend/src/views/ArticleCreate.vue

<!-- frontend/src/views/ArticleCreate.vue -->

<template>
  <BlogHeader/>

  <BlogFooter/>
</template>

<script>
  import BlogHeader from '@/components/BlogHeader.vue'
  import BlogFooter from '@/components/BlogFooter.vue'

  export default {
    name: 'ArticleCreate',
    components: {BlogHeader, BlogFooter}
  }
</script>

暂时有个空壳子就行了,后面再来填补内容。

接着,在用户登录时追加记录用户是否为超级管理员

<!-- frontend/src/views/Login.vue -->

...
<script>
  ...
  methods: {
    ...
    signin() {
      ...
      axios
        .post(...)
              .then(function (response) {
        ...
        // 是否为管理员
        axios
          .get('/api/user/' + that.signinName + '/')
          .then(function (response) {
          storage.setItem('isSuperuser.myblog', response.data.is_superuser);
          // 路由跳转修改到这里
          that.$router.push({name: 'Home'});
        });
        // .catch(...)
      })
    },
  }
</script>
....

将新页面的路由注册好:

// frontend/src/router/index.js

...
import ArticleCreate from "@/views/ArticleCreate.vue";

const routes = [
  ...
  {
    path: "/article/create",
    name: "ArticleCreate",
    component: ArticleCreate
  },
];

最后,在页眉的欢迎词下拉框用 v-if 仅对超级用户显示入口,普通用户不显示:

<!-- frontend/src/components/BlogHeader.vue -->

<template>
  ...
  <div class="dropdown-content">
    ...
    <router-link 
                 :to="{ name: 'ArticleCreate' }" 
                 v-if="isSuperuser"
                 >
      发表文章
    </router-link>
  </div>
  ...
</template>

<script>
  ...
  data: function () {
    return {
      ...
      isSuperuser: JSON.parse(localStorage.getItem('isSuperuser.myblog')),
    }
  },
    ...
</script>

准备工作就完成了。现在把鼠标悬停在页眉欢迎词上,如果是超级用户,下拉框中会出现“发表文章”的链接。点击链接,就能前往发表文章页面了(暂时空空如也):

发表页面

最后就是 ArticleCreate.vue 的实际内容了。

代码量比较大,这里贴出完整内容:

<!-- frontend/src/views/ArticleCreate.vue -->

<template>
  <BlogHeader/>
  <div id="article-create">
    <h3>发表文章</h3>
    <form>
      <div class="form-elem">
        <span>标题:</span>
        <input v-model="title" type="text" placeholder="输入标题">
      </div>

      <div class="form-elem">
        <span>分类:</span>
        <span
              v-for="category in categories"
              :key="category.id"
              >
          <!--样式也可以通过 :style 绑定-->
          <button
                  class="category-btn"
                  :style="categoryStyle(category)"
                  @click.prevent="chooseCategory(category)"
                  >
            {{category.title}}
          </button>
        </span>
      </div>

      <div class="form-elem">
        <span>标签:</span>
        <input v-model="tags" type="text" placeholder="输入标签,用逗号分隔">
      </div>

      <div class="form-elem">
        <span>正文:</span>
        <textarea v-model="body" placeholder="输入正文" rows="20" cols="80"></textarea>
      </div>

      <div class="form-elem">
        <button v-on:click.prevent="submit">提交</button>
      </div>
    </form>
  </div>
  <BlogFooter/>
</template>

<script>
  import BlogHeader from '@/components/BlogHeader.vue'
  import BlogFooter from '@/components/BlogFooter.vue'
  import axios from 'axios';
  import authorization from '@/utils/authorization';

  export default {
    name: 'ArticleCreate',
    components: {BlogHeader, BlogFooter},
    data: function () {
      return {
        // 文章标题
        title: '',
        // 文章正文
        body: '',
        // 数据库中所有的分类
        categories: [],
        // 选定的分类
        selectedCategory: null,
        // 标签
        tags: '',
      }
    },
    mounted() {
      // 页面初始化时获取所有分类
      axios
        .get('/api/category/')
        .then(response => this.categories = response.data)
    },
    methods: {
      // 根据分类是否被选中,按钮的颜色发生变化
      // 这里可以看出 css 也是可以被 vue 绑定的,很方便
      categoryStyle(category) {
        if (this.selectedCategory !== null && category.id === this.selectedCategory.id) {
          return {
            backgroundColor: 'black',
          }
        }
        return {
          backgroundColor: 'lightgrey',
          color: 'black',
        }
      },
      // 选取分类的方法
      chooseCategory(category) {
        // 如果点击已选取的分类,则将 selectedCategory 置空
        if (this.selectedCategory !== null && this.selectedCategory.id === category.id) {
          this.selectedCategory = null
        }
        // 如果没选中当前分类,则选中它
        else {
          this.selectedCategory = category;
        }
      },
      // 点击提交按钮
      submit() {
        const that = this;
        // 前面封装的验证函数又用上了
        authorization()
          .then(function (response) {
            if (response[0]) {
              // 需要传给后端的数据字典
              let data = {
                title: that.title,
                body: that.body,
              };
              // 添加分类
              if (that.selectedCategory) {
                data.category_id = that.selectedCategory.id
              }
              // 标签预处理
              data.tags = that.tags
                // 用逗号分隔标签
                .split(/[,,]/)
                // 剔除标签首尾空格
                .map(x => x.trim())
                // 剔除长度为零的无效标签
                .filter(x => x.charAt(0) !== '');

              // 将发表文章请求发送至接口
              // 成功后前往详情页面
              const token = localStorage.getItem('access.myblog');
              axios
                .post('/api/article/',
                  data,
                  {
                        headers: {Authorization: 'Bearer ' + token}
                    })
                .then(function (response) {
                    that.$router.push({name: 'ArticleDetail', params: {id: response.data.id}});
              })
            }
            else {
              alert('令牌过期,请重新登录。')
            }
          }
        )
      }
    }
  }
</script>

<style scoped>
  .category-btn {
    margin-right: 10px;
  }
  #article-create {
    text-align: center;
    font-size: large;
  }
  form {
    text-align: left;
    padding-left: 100px;
    padding-right: 10px;
  }
  .form-elem {
    padding: 10px;
  }
  input {
    height: 25px;
    padding-left: 10px;
    width: 50%;
  }
  button {
    height: 35px;
    cursor: pointer;
    border: none;
    outline: none;
    background: steelblue;
    color: whitesmoke;
    border-radius: 5px;
    width: 60px;
  }
</style>

细节就由读者自己去慢慢啃了,把新出现的知识点和主要逻辑理出来讲讲:

  • 基本思路和用户注册、登录等实现很像,核心就是围绕 Vue 的 data;把需要的数据全部绑定到 data 中,点击提交按钮就将这些数据处理得妥妥当当,发送到接口。
  • v-bind 很强大,它甚至可以把样式也绑定成为数据。比如这里为了让分类按钮被选中后具有不同的外观,就把所有分类按钮的样式绑定到 categoryStyle() 方法上。样式绑定看起来很像 CSS,但实际上它是个 Javascript 对象。(注意这里也是驼峰式命名)
  • 提交按钮的 submit() 篇幅很长,但是仔细看看也很简单:把 data 里的数据进行预处理,转换为接口所需要的数据类型并发送请求。

还记得吗,我们的后端对标签的处理非常优秀:可以在创建文章的接口里添加标签的列表,并且很好的处理了列表中已有标签和未有标签的混合。

新内容就这么多。

为了让列表页面也能显示分类信息,稍微改一改 ArticleList.vue :

<!-- frontend/src/components/ArticleList.vue -->

<template>
  <div v-for="article in info.results" ...>
    <div>
      <!-- 增加了这个 span -->
      <span
            v-if="article.category !== null"
            class="category"
            >
        {{article.category.title}}
      </span>
      <span v-for="tag in article.tags" ...>{{ tag }}</span>
    </div>
    ...
  </div>

  ...

</template>

...

<style scoped>
  .category {
    padding: 5px 10px 5px 10px;
    margin: 5px 5px 5px 0;
    font-family: Georgia, Arial, sans-serif;
    font-size: small;
    background-color: darkred;
    color: whitesmoke;
    border-radius: 15px;
  }

  ...
</style>

大功告成了,看看效果。

发表文章页面:

写好文章后,点击提交就会进入到这篇文章的详情页。

再看看文章列表页:

添加了分类信息显示。

核心功能都较完整的实现了,可歌可泣。至于外观,读者慢慢摸索着优化吧。




本文作者: 杜赛
发布时间: 2021年03月28日 - 18:57
最后更新: 2021年03月28日 - 18:57
转载请保留原文链接及作者