泽恩小站-教程区 Help

发表文章

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

本章完成博客文章的发表

准备工作

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

修改后端文件 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>

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

P280 1

发表页面

最后就是 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>

大功告成了,看看效果。

发表文章页面:

P280 2

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

再看看文章列表页:

P280 3

添加了分类信息显示。

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

Last modified: 06 January 2025