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>
大功告成了,看看效果。
发表文章页面:
写好文章后,点击提交就会进入到这篇文章的详情页。
再看看文章列表页:
添加了分类信息显示。
核心功能都较完整的实现了,可歌可泣。至于外观,读者慢慢摸索着优化吧。