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