9 文章模块
文章模块
创建子引用
cd renranapi/apps
python ../../manage.py startapp article
注册子应用
settings/dev.py
INSTALLED_APPS = [
# ....
'article',
]
总路由:
urls.py
path('article/', include('article.urls')),
新建子路由文件:
article/urls.py
from django.urls import path,re_path
from . import views
urlpatterns = [
# path('collection/', views.ArticleCollectionView.as_view()),
]
模型代码
aricle./models.py,代码:
from django.db import models
from renranapi.utils.models import BaseModel
from users.models import User
# create your models here.
class ArticleCollection(BaseModel):
"""文集模型"""
name = models.CharField(max_length=200, verbose_name="文章标题")
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="用户")
class Meta:
db_table = "rr_article_collection"
verbose_name = "文集"
verbose_name_plural = verbose_name
class Special(BaseModel):
"""专题模型"""
name = models.CharField(max_length=200, verbose_name="文章标题")
image = models.ImageField(null=True, blank=True, verbose_name="封面图片")
notice = models.TextField(null=True, blank=True, verbose_name="专题公告")
article_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="文章总数")
follow_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="关注量量")
collect_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="收藏量")
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="创建人")
class Meta:
db_table = "rr_special"
verbose_name = "专题"
verbose_name_plural = verbose_name
class Article(BaseModel):
"""文章模型"""
title = models.CharField(max_length=200, verbose_name="文章标题")
content = models.TextField(null=True, blank=True, verbose_name="文章内容")
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="用户")
collection = models.ForeignKey(ArticleCollection, on_delete=models.CASCADE, verbose_name="文集")
pub_date = models.DateTimeField(null=True, default=None, verbose_name="发布时间")
access_pwd = models.CharField(max_length=15,null=True, blank=True, verbose_name="访问密码")
read_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="阅读量")
like_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="点赞量")
collect_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="收藏量")
comment_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="评论量")
reward_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="赞赏量")
is_public = models.BooleanField(default=False, verbose_name="是否公开")
class Meta:
db_table = "rr_article"
verbose_name = "文章"
verbose_name_plural = verbose_name
class SpecialArticle(BaseModel):
"""文章和专题的绑定关系"""
article = models.ForeignKey(Article, on_delete=models.CASCADE, verbose_name="文章")
special = models.ForeignKey(Special, on_delete=models.CASCADE, verbose_name="专题")
class Meta:
db_table = "rr_special_article"
verbose_name = "专题的文章"
verbose_name_plural = verbose_name
class SpecialManager(BaseModel):
"""专题管理员"""
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="管理员")
special = models.ForeignKey(Special, on_delete=models.CASCADE, verbose_name="专题")
class Meta:
db_table = "rr_special_manager"
verbose_name = "专题的管理员"
verbose_name_plural = verbose_name
class SpecialFocus(BaseModel):
"""专题关注"""
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="管理员")
special = models.ForeignKey(Special, on_delete=models.CASCADE, verbose_name="专题")
class Meta:
db_table = "rr_special_focus"
verbose_name = "专题的关注"
verbose_name_plural = verbose_name
class SpecialCollection(BaseModel):
"""专题收藏"""
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="管理员")
special = models.ForeignKey(Special, on_delete=models.CASCADE, verbose_name="专题")
class Meta:
db_table = "rr_special_collection"
verbose_name = "专题收藏"
verbose_name_plural = verbose_name
class ArticleImage(BaseModel):
"""文章图片,一些公共图片,暂不和文章关联"""
group = models.CharField(max_length=15,null=True, blank=True, verbose_name="组名")
image = models.ImageField(null=True, blank=True, verbose_name="图片地址")
# user = models.IntegerField(null=True, blank=True, verbose_name="上传图片的用户") #可以设置这么一个字段先,表示哪个用户上传的图片
class Meta:
db_table = "rr_article_image"
verbose_name = "文章图片"
verbose_name_plural = verbose_name
数据迁移
python manage.py makemigrations
python manage.py migrate
在xadmin中注册一下这些表
import xadmin
from xadmin import views
from .models import *
# 注册文集模型
class ArticleCollectionModelAdmin(object):
list_display = ["id"] # 后台显示字段
xadmin.site.register(ArticleCollection,ArticleCollectionModelAdmin)
# 注册专题模型
class SpecialModelAdmin(object):
list_display=['id'] # 后台显示字段
xadmin.site.register(Special,SpecialModelAdmin)
# 注册文章模型
class ArticleModelAdmin(object):
list_display = ["id"] # 后台显示字段
xadmin.site.register(Article,ArticleModelAdmin)
# 注册文章和专题的绑定关系
class SpecialArticleModelAdmin(object):
list_display=['id'] # 后台显示字段
xadmin.site.register(SpecialArticle,SpecialArticleModelAdmin)
# 注册专题管理员
class SpecialManagerModelAdmin(object):
list_display = ["id"] # 后台显示字段
xadmin.site.register(SpecialManager,SpecialManagerModelAdmin)
# 注册专题关注
class SpecialFocusModelAdmin(object):
list_display=['id'] # 后台显示字段
xadmin.site.register(SpecialFocus,SpecialFocusModelAdmin)
# 注册专题收藏
class SpecialCollectionModelAdmin(object):
list_display = ["id"] # 后台显示字段
xadmin.site.register(SpecialCollection,SpecialCollectionModelAdmin)
# 注册文章图片
class ArticleImageModelAdmin(object):
list_display=['id'] # 后台显示字段
xadmin.site.register(ArticleImage,ArticleImageModelAdmin)
# 注册文章投稿记录
class ArticlePostLogModelAdmin(object):
list_display=['id'] # 后台显示字段
xadmin.site.register(ArticlePostLog,ArticlePostLogModelAdmin)
相关依赖
在vue中引入集成markdown富文本编辑器
这里我们使用 mavonEditor,链接:https://github.com/hinesboy/mavonEditor
安装
cd renran_pc
npm install mavon-editor --save
在main.js中注册编辑器组件
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
// 注册mavon-editor组件
Vue.use(mavonEditor);
new Vue({
'el': '#main'
})
写文章页面
引入font-awesome文件,到static目录下
文章页面引入富文本编辑器
创建Write.vue组件,提供给用户编写文章
<template>
<div class="write">
<div class="_2v5v5">
<div class="_3zibT"><a href="/">回首页</a></div>
<div class="_1iZMb">
<div class="_33Zlg" @click="collection_form=true"><i class="fa fa-plus"></i><span>新建文集</span></div>
<div class="_2G97m">
<form class="M8J6Q" :class="collection_form?'_2a1Rp':'_1mU5v'">
<input type="text" placeholder="请输入文集名..." name="name" class="_1CtV4">
<button type="submit" class="dwU8Q _3zXcJ _3QfkW"><span>提 交</span></button>
<button type="button" class="vIzwB _3zXcJ" @click="collection_form=false"><span>取 消</span></button>
</form>
</div>
</div>
<ul class="_3MbJ4 _3t059">
<li class="_3DM7w _31PCv" title="日记本">
<div class="_3P4JX _2VLy-">
<i class="fa fa-gear"></i>
<span>
<ul class="_2V8zt _3FcHm _2w9pn" :class="true?'':'NvfK4'">
<li class="_2po2r cRfUr" title="">
<span class=""><i class="fa fa-pencil-square-o _22XWG"></i>修改文集</span>
</li>
<li class="_2po2r cRfUr" title="">
<span class=""><i class="fa fa-trash-o _22XWG"></i>删除文集</span>
</li>
</ul>
</span>
</div>
<span>日记本</span>
</li>
<li class="_3DM7w" title="随笔"><span>随笔</span></li>
</ul>
<div style="height: 50px;"></div>
<div role="button" class="h-5Am">
<span class="ant-dropdown-trigger"><i class="fa fa-bars"></i><span>设置</span></span>
<span class="Yv5Zx">遇到问题<i class="fa fa-question-circle-o"></i></span>
</div>
</div>
<div class="rQQG7">
<div class="_3revO _2mnPN">
<div class="_3br9T">
<div>
<div class="_1GsW5"><i class="fa fa-plus-circle"></i><span> 新建文章</span></div>
<ul class="_2TxA-">
<li class="_25Ilv _33nt7" title="ABC">
<i class="_13kgp _2m93u"></i>
<div class="_3P4JX poOXI">
<i class="fa fa-gear"></i>
<span>
<ul class="_2V8zt _3FcHm _2w9pn">
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-share _22XWG"></i>直接发布</span></li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-clock-o _22XWG"></i>定时发布</span></li>
<li class="_2po2r cRfUr" title=""><span class="_20tIi"><i class="iconfont ic-paid _22XWG"></i>发布为付费文章</span></li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="iconfont ic-set _22XWG"></i>设置发布样式</span></li>
<li class="_3nZXj _2_WAp _3df2u _2po2r cRfUr" title=""><span class=""><i class="fa fa-folder-open _22XWG"></i>移动文章
<div class="_3x4X_">
<ul class="_2KzJx oGKRI _3DXDE _2w9pn">
<li class="_2po2r cRfUr" title="随笔"><span class="">随笔</span></li>
</ul>
</div>
</span>
</li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-history _22XWG"></i>历史版本</span></li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-trash-o _22XWG"></i>删除文章</span></li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-ban _22XWG"></i>设置禁止转载</span></li>
</ul>
</span>
</div>
<span class="NariC">ABC</span>
<span class="hLzJv">题目:有四个数字:1、2、3、4,能组成多少个互不相同且无重复数字的三位数?各是多少?
题目:企业发放的奖金根据利润提成</span>
<span class="_29C-V">字数:905</span>
</li>
<li class="_25Ilv" title="2020-01-12">
<i class="_13kgp"></i>
<span class="NariC">2020-01-12</span>
<span class="hLzJv">题目:有四个数字:1、2、3、4,能组成多少个互不相同且无重复数字的三位数?各是多少?
题目:企业发放的奖金根据利润提成</span>
</li>
</ul>
<div class="_2cVn3"><i class="fa fa-plus"></i><span> 在下方新建文章</span></div>
</div>
</div>
</div>
<input type="text" class="_24i7u" value="2020-01-12">
<!--引入文本编辑器-->
<div id="editor">
<mavon-editor
style="height: 100%"
v-model="editorContent"
:ishljs="true"
ref=md
@imgAdd="imgAdd"
@imgDel="imgDel"
></mavon-editor>
</div>
</div>
</div>
</template>
<script>
// 引入富文本编辑器
import { mavonEditor } from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
export default {
name: "Write",
data(){
return {
editorContent:"",
img_file:[],
collection_form:false,
}
},
watch:{
editorContent(){
console.log(this.editorContent)
}
},
mounted(){
document.querySelector("#editor").style.height = document.documentElement.clientHeight-document.querySelector("._24i7u").clientHeight+"px";
},
components: {
mavonEditor
},
methods:{
// 绑定@imgAdd event
imgAdd(pos, $file){
// 添加文件
},
imgDel(pos) {
// 删除文件
},
}
}
</script>
<style scoped>
body *{
box-sizing: border-box;
}
.write{
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
margin: 0;
}
._2v5v5 {
position: relative;
height: 100%;
overflow-y: auto;
background-color: #404040;
color: #f2f2f2;
z-index: 100;
width: 16.66666667%;
display: block;
flex: 0 0 auto;
float: left;
padding-right: 0;
padding-left: 0;
min-height: 1px;
}
._3zibT {
padding: 30px 18px 5px;
text-align: center;
font-size: 14px;
}
._3zibT a {
display: block;
font-size: 15px;
padding: 9px 0;
color: #ec7259;
border: 1px solid rgba(236,114,89,.8);
border-radius: 20px;
-webkit-transition: border-color .2s ease-in;
-o-transition: border-color .2s ease-in;
transition: border-color .2s ease-in;
}
._1iZMb {
padding: 0 15px;
margin-top: 20px;
margin-bottom: 10px;
font-size: 14px;
line-height: 1.5;
}
._1iZMb ._33Zlg {
cursor: pointer;
color: #f2f2f2;
transition: color .2s cubic-bezier(.645,.045,.355,1);
font-size: 14px;
}
._1iZMb ._33Zlg .fa+span {
margin-left: 4px;
}
._1iZMb ._2G97m {
overflow: hidden;
}
._1iZMb ._2a1Rp {
height: 85px;
opacity: 1;
margin-top: 10px;
transition: all .2s ease-out;
overflow: hidden;
}
._1CtV4 {
width: 100%;
height: 35px;
color: #ccc;
background-color: #595959;
border: 1px solid #333;
padding: 4px 6px;
font-size: 14px;
line-height: 20px;
outline: 0;
overflow: visible;
margin: 10px 0 0;
margin-bottom: 10px;
}
._3zXcJ {
position: relative;
display: inline-block;
text-align: center;
height: 30px;
line-height: 20px;
padding: 4px 12px;
border: 1px solid transparent;
border-radius: 15px;
font-size: 14px;
font-weight: 500;
-ms-touch-action: manipulation;
touch-action: manipulation;
cursor: pointer;
background-image: none;
white-space: nowrap;
user-select: none;
transition: all .2s cubic-bezier(.645,.045,.355,1);
text-transform: none;
color: #42c02e;
border-color: #42c02e;
margin-left: 4px;
background-color: #404040;
}
.vIzwB {
color: #999;
outline: 0;
}
._1iZMb ._1mU5v {
height: 0;
opacity: 0;
margin-top: 0;
}
._1iZMb ._2a1Rp {
height: 85px;
opacity: 1;
margin-top: 10px;
}
._1iZMb ._1mU5v, ._1iZMb ._2a1Rp {
transition: all .2s ease-out;
}
.vIzwB, .vIzwB:focus, .vIzwB:hover {
background-color: #404040;
border-color: transparent;
}
.dwU8Q {
margin-left: 4px;
background-color: #404040;
}
._3t059 {
position: relative;
z-index: 0;
background-color: #8c8c8c;
}
._3MbJ4 {
margin-bottom: 0;
}
._3DM7w {
position: relative;
line-height: 40px;
list-style: none;
font-size: 15px;
color: #f2f2f2;
background-color: #404040;
padding: 0 15px;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
._31PCv {
background-color: #666;
border-left: 3px solid #ec7259;
padding-left: 12px;
}
._3DM7w ._2VLy- {
float: right;
}
._3P4JX {
font-size: 16px;
width: 40px;
text-align: center;
position: relative;
min-height: 30px;
max-height: 50px;
}
._3DM7w span {
display: block;
margin-right: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
._2w9pn {
font-size: 14px;
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
box-shadow: 0 5px 10px rgba(0,0,0,.2);
list-style: none;
background-color: #fff;
color: #595959;
border-radius: 6px;
}
._3P4JX ul._2V8zt {
display: none;
position: absolute;
z-index: 99;
right: 0;
}
._3P4JX ul._3FcHm {
top: 100%;
}
._2po2r {
padding: 10px 20px;
line-height: 20px;
white-space: nowrap;
text-align: left;
position: relative;
border-bottom: 1px solid #d9d9d9;
}
._3DM7w:hover, .JUBSP {
background-color: #666;
}
.h-5Am {
display: block;
width: 16.66666667%;
position: fixed;
bottom: 0;
height: 50px;
line-height: 50px;
font-size: 15px;
padding-left: 15px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
z-index: 150;
background-color: #404040;
}
.cRfUr {
border-bottom: 1px solid #d9d9d9;
}
._2po2r:last-child {
border-radius: 0 0 4px 4px;
border-bottom: 0;
}
._2po2r:first-child {
border-radius: 4px 4px 0 0;
}
._2po2r ._22XWG {
margin-right: 5px;
}
._2po2r:hover {
background-color: #666;
color: #fff;
}
._3DM7w span {
display: block;
margin-right: 20px;
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
white-space: nowrap;
}
._3P4JX ul.NvfK4 {
display: block;
}
._3P4JX ul._2V8zt:before {
position: absolute;
right: 12px;
content: "";
display: inline-block;
}
._3P4JX ul._3FcHm:before {
border-left: 9px solid transparent;
border-right: 9px solid transparent;
border-bottom: 9px solid #fff;
top: -9px;
}
.h-5Am .ant-dropdown-trigger {
display: inline-block;
color: #999;
cursor: pointer;
-webkit-transition: color .2s cubic-bezier(.645,.045,.355,1);
-o-transition: color .2s cubic-bezier(.645,.045,.355,1);
transition: color .2s cubic-bezier(.645,.045,.355,1);
}
.h-5Am .fa+span {
margin-left: 4px;
}
.h-5Am .Yv5Zx {
float: right;
margin-right: 15px;
color: #999;
cursor: pointer;
}
.h-5Am .Yv5Zx i {
margin-left: 5px;
}
.rQQG7{
height: 100%;
display: block;
width: 33.33333%;
border-right: 1px solid #d9d9d9;
}
._3revO {
overflow-y: scroll;
height: 100%;
position: relative;
}
._3br9T {
position: relative;
transition: opacity .3s cubic-bezier(.645,.045,.355,1);
opacity: 1;
}
._1GsW5 {
line-height: 20px;
font-size: 15px;
font-weight: 400;
padding: 20px 0 20px 25px;
cursor: pointer;
color: #595959;
}
._1GsW5:hover {
color: #262626;
}
._2TxA- {
position: relative;
margin-bottom: 0;
background-color: #efe9d9;
border-top: 1px solid #d9d9d9;
}
._25Ilv {
position: relative;
height: 90px;
color: #595959;
background-color: #fff;
margin-bottom: 0;
padding: 15px 10px 15px 60px;
box-shadow: 0 0 0 1px #d9d9d9;
border-left: 5px solid transparent;
list-style: none;
line-height: 60px;
cursor: pointer;
user-select: none;
}
._25Ilv ._2m93u {
background: url(/static/image/sprite.9d24217.png) no-repeat -50px -25px;
background-size: 250px;
position: absolute;
top: 30px;
left: 20px;
width: 22px;
height: 30px;
}
._1tqbw, ._25Ilv:hover, ._33nt7 {
background-color: #e6e6e6;
}
._25Ilv ._2m93u {
background: url(/static/image/sprite.9d24217.png) no-repeat -50px -25px;
background-size: 250px;
position: absolute;
top: 30px;
left: 20px;
width: 22px;
height: 30px;
}
._3P4JX {
font-size: 16px;
width: 40px;
text-align: center;
position: relative;
min-height: 30px;
max-height: 50px;
}
._25Ilv .poOXI {
float: right;
}
._33nt7 {
border-left-color: #ec7259;
}
._25Ilv .hLzJv, ._25Ilv .NariC {
display: block;
height: 30px;
line-height: 30px;
margin-right: 40px;
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 18px;
font-family: sans-serif;
}
._2TxA- {
position: relative;
margin-bottom: 0;
background-color: #efe9d9;
border-top: 1px solid #d9d9d9;
}
._3P4JX ul._2V8zt {
display: none;
position: absolute;
z-index: 99;
right: 0;
}
._3P4JX ul._3FcHm {
top: 100%;
}
._2w9pn {
font-size: 14px;
box-shadow: 0 5px 10px rgba(0,0,0,.2);
list-style: none;
background-color: #fff;
color: #595959;
border-radius: 6px;
}
._3P4JX ul.NvfK4 {
display: block;
}
._3P4JX ul._3FcHm:before {
border-left: 9px solid transparent;
border-right: 9px solid transparent;
border-bottom: 9px solid #fff;
top: -9px;
}
._3P4JX ul._2V8zt:before {
position: absolute;
right: 12px;
content: "";
display: inline-block;
}
._25Ilv ._13kgp {
position: absolute;
top: 30px;
left: 20px;
width: 22px;
height: 30px;
background: url(/static/image/sprite.9d24217.png) no-repeat 0 -25px;
background-size: 250px;
}
._25Ilv ._13kgp {
position: absolute;
top: 30px;
left: 20px;
width: 22px;
height: 30px;
background: url(/static/image/sprite.9d24217.png) no-repeat 0 -25px;
background-size: 250px;
}
._25Ilv ._2m93u {
background: url(/static/image/sprite.9d24217.png) no-repeat -50px -25px;
background-size: 250px;
}
._25Ilv ._29C-V {
position: absolute;
bottom: 2px;
left: 5px;
font-size: 9px;
line-height: 16px;
color: #595959;
}
._2cVn3 {
line-height: 30px;
padding: 20px 0 20px 25px;
cursor: pointer;
color: #999;
margin-bottom: 80px;
}
._24i7u {
flex-shrink: 0;
padding: 0 80px 10px 40px;
margin-bottom: 0;
border: none;
font-size: 30px;
font-weight: 400;
line-height: 30px;
box-shadow: none;
color: #595959;
background-color: transparent;
outline: none;
border-radius: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
position: absolute;
top: 0;
right: 0;
width: 66.666666%;
}
#editor {
margin: auto;
width: 66.666666%;
position: absolute;
right: 0;
top: 44px;
height: 580px;
}
</style>
路由代码:routers.index.js
// ....
import Write from "@/components/Write"
export default new Router({
mode: "history",
routes: [
// ....
{
name:"Write",
path:"/write",
component: Write,
},
]
})
在 Header.vue提供跳转链接
<router-link class="btn write-btn" target="_blank" to="/writer"><img class="icon-write" src="/static/image/write.svg">写文章</router-link>
文集
- 判断用户是否登录了
- 查询当前用户的所有文集
- 如果用户没有文集,那么给用户添加两个默认文件,随笔和日记本
- 用户新建文集
- 查询文集中的所有文章
- 当前默认显示的文章的标题和内容
客户端: settings.js 定义公共方法获取token值
check_user_login(vm){
// 判断用户是否已经登陆
let token = localStorage.user_token || sessionStorage.user_token;
if(!token){
// 跳转到登陆页面
this.jump_page(vm, "尊敬的游客, 您尚未登陆!请登陆后再进行操作!", "警告","去登陆", "/user/login");
}
return token;
}
展示文集 - 添加文集 - 修改文集
服务端
article/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('collection/', views.ArticleCollectionView.as_view()), # 展示文集
]
Articel/views.py
from rest_framework.generics import ListAPIView
from rest_framework.permissions import IsAuthenticated
from .serializers import ArticleCollectionSerializer
# 文集初始化
class ArticleCollectionView(ListAPIView):
# 必须是登陆用户才能访问过来 -- 用户验证
permission_classes = [IsAuthenticated,]
# self.request.user -- 拿到当前登录用户
serializer_class = ArticleCollectionSerializer
# 重写queryset数据
def get_queryset(self):
# 获取文集数据
queryset = ArticleCollection.objects.filter(
user_id=self.request.user.id,is_show=True,is_delete=False).order_by('orders')
# 如果用户没有文集,默认添加两个文集
if not queryset.exists():
obj1 = ArticleCollection.objects.create(**{
'name':'日记本',
'user_id':self.request.user.id,
})
obj2 = ArticleCollection.objects.create(**{
'name':'随笔',
'user_id':self.request.user.id,
})
data = [
{'name':obj1.name,'id':obj1.user_id},
{'name':obj2.name,'id':obj2.user_id},
]
return data
return queryset
# 文集操作 -- 增,删,改
class ArticleCollectionOtherView(ModelViewSet):
# 登录验证
permission_classes = [IsAuthenticated, ]
serializer_class = ArticleCollectionSerializer
queryset = ArticleCollection.objects.filter(is_show=True, is_delete=False).order_by('orders')
serializers.py
from rest_framework import serializers
from . import models
import datetime
from users.models import User
# 获取文集数据
class ArticleCollectionSerializer(serializers.ModelSerializer):
class Meta:
model =models.ArticleCollection
fields = ['id','name']
extra_kwargs = {
'id':{'read_only':True}
}
# 局部钩子校验name字段
def validate_name(self, name):
# 同一用户文集名称不能重复
# self.context -- 额外参数有request请求方法
res = models.ArticleCollection.objects.filter(user_id=self.context['request'].user.id ,name=name)
if res.exists():
raise serializers.ValidationError('文集名称不能重复!')
return name
# 重写添加文集方法
def create(self, validated_data):
# validated_data 中只有校验的name字段,少数据
collection_obj = models.ArticleCollection.objects.create(
name = validated_data.get('name'),
user_id = self.context['request'].user.id,
)
return collection_obj
客户端
write.vue
<template>
<div class="write" @click="boss">
<div class="_2v5v5">
<div class="_3zibT"><a href="/">回首页</a></div>
<div class="_1iZMb">
<div class="_33Zlg" @click="collection_form=true"><i class="fa fa-plus"></i><span>新建文集</span></div>
<div class="_2G97m">
<form class="M8J6Q" :class="collection_form?'_2a1Rp':'_1mU5v'">
<input type="text" placeholder="请输入文集名..." name="name" class="_1CtV4" v-model="collection_name">
<button @click="add_collection" class="dwU8Q _3zXcJ _3QfkW"><span>提 交</span></button>
<button type="button" class="vIzwB _3zXcJ" @click="collection_form=false"><span>取 消</span></button>
</form>
</div>
</div>
<ul class="_3MbJ4 _3t059">
<li class="_3DM7w " @click="change_collection_current_index(collection_index,collection_value.id)" :class="{_31PCv:collection_current_index===collection_index}" :title="collection_value.name" v-for="(collection_value,collection_index) in collection_list" :key="collection_index">
<div @click.stop.prevent="change_edit_event" class="_3P4JX _2VLy-" v-if="collection_current_index===collection_index">
<i class="fa fa-gear"></i>
<span>
<ul class="_2V8zt _3FcHm _2w9pn" :class="edit_event_status?'':'NvfK4'">
<li @click="edit_collection(collection_value.name,collection_value.id,collection_index)" class="_2po2r cRfUr" title="">
<span class=""><i class="fa fa-pencil-square-o _22XWG"></i>修改文集</span>
</li>
<li @click="delete_collection(collection_value.id,collection_index)" class="_2po2r cRfUr" title="">
<span class=""><i class="fa fa-trash-o _22XWG"></i>删除文集</span>
</li>
</ul>
</span>
</div>
<span>{{ collection_value.name }}</span>
</li>
</ul>
<div style="height: 50px;"></div>
<div role="button" class="h-5Am">
<span class="ant-dropdown-trigger"><i class="fa fa-bars"></i><span>设置</span></span>
<span class="Yv5Zx">遇到问题<i class="fa fa-question-circle-o"></i></span>
</div>
</div>
<div class="rQQG7">
<div class="_3revO _2mnPN">
<div class="_3br9T">
<div>
<div class="_1GsW5" @click="add_article(0)"><i class="fa fa-plus-circle"></i><span> 新建文章</span></div>
<ul class="_2TxA-">
<li @click="change_article_index(article_index)" class="_25Ilv" :class="{_33nt7:article_current_index===article_index}" title="ABC" v-for="(article_value,article_index) in article_list" :key="article_index">
<i class="_13kgp" :class="{_2m93u:article_value.is_public}"></i>
<div @click.stop.prevent="change_article_toolbar_status" class="_3P4JX poOXI" v-if="article_current_index===article_index">
<i class="fa fa-gear"></i>
<span>
<ul class="_2V8zt _3FcHm _2w9pn" :class="{toolvar:article_toolbar_status}">
<li class="_2po2r cRfUr" title="">
<span class="" v-if="article_value.is_public" @click="article_not_public(article_value.id,article_index)"><i class="fa fa-share _22XWG" ></i>取消发布</span>
<span class="" v-else @click="article_public(article_value.id,article_index)"><i class="fa fa-share _22XWG"></i>直接发布</span>
</li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-clock-o _22XWG"></i>定时发布</span></li>
<li class="_2po2r cRfUr" title=""><span class="_20tIi"><i class="iconfont ic-paid _22XWG"></i>发布为付费文章</span></li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="iconfont ic-set _22XWG"></i>设置发布样式</span></li>
<li class="_3nZXj _2_WAp _3df2u _2po2r cRfUr" title=""><span class=""><i class="fa fa-folder-open _22XWG"></i>移动文章
<div class="_3x4X_">
<ul class="_2KzJx oGKRI _3DXDE _2w9pn">
<li @click="remove_article(cvalue.id)" class="_2po2r cRfUr" title="随笔" v-for="(cvalue,cindex) in collection_list" :key="cindex" v-if="cindex!=collection_current_index"><span class="">{{cvalue.name}}</span></li>
</ul>
</div>
</span>
</li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-history _22XWG"></i>历史版本</span></li>
<li @click="delete_article" class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-trash-o _22XWG"></i>删除文章</span></li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-ban _22XWG"></i>设置禁止转载</span></li>
</ul>
</span>
</div>
<span class="NariC">{{ article_value.title }}</span>
<span class="hLzJv">{{ article_value.content }}</span>
<span class="_29C-V">字数:{{article_value.content ?article_value.content.length:0}}</span>
</li>
</ul>
<div class="_2cVn3" @click="add_article(1)"><i class="fa fa-plus"></i><span> 在下方新建文章</span></div>
</div>
</div>
</div>
<input type="text" class="_24i7u" v-model="editorTitle">
<div id="editor"> <!--引入文本编辑器-->
<mavon-editor
style="height: 100%"
v-model="editorContent"
:ishljs="true"
ref=md
@change="content_change"
@imgAdd="imgAdd"
@imgDel="imgDel"
></mavon-editor>
</div>
</div>
</div>
</template>
<script>
import { mavonEditor } from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
export default {
name: "Write",
data(){
return {
img_file:[], // 缓存图片列表
collection_form:false,
collection_list:[], // 文集列表
collection_current_index:0, // 默认选中文集索引
edit_event_status:true, //文集编辑状态
collection_name:'', //文集名称
collection_id:0, //文集id
}
},
watch:{
editorContent(){
console.log(this.editorContent)
}
},
mounted(){
document.querySelector("#editor").style.height = document.documentElement.clientHeight-document.querySelector("._24i7u").clientHeight+"px";
//
this.get_collection_data();
},
components: {
mavonEditor
},
methods:{
// 改变文章编辑状态
change_article_toolbar_status(){
this.article_toolbar_status = !this.article_toolbar_status
},
// 最外层点击事件
boss(){
// 不显示文集设置按钮
this.edit_event_status = true
this.article_toolbar_status = false
},
// 删除文集
delete_collection(id,index){
this.$confirm('此操作将永久删除该文集, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then((value) => {
let token = this.$settings.check_user_login(this)
//
this.$axios.delete(`${this.$settings.host}/article/collection/operation/${id}/`,{
headers:{
'Authorization':`jwt ${token}`,}
}).then((res)=>{
this.collection_list.splice(index,1)
this.$message({
type: 'success',
message: '删除成功!'
})
}).catch((error)=>{
this.$message.error('删除失败!')
});
}).catch((error) => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
// 编辑文集
edit_collection(old_name,id,index){
this.$prompt('请输入文集名称', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputValue:old_name,
}).then(({ value }) => {
let token = this.$settings.check_user_login(this)
// 传递修改数据
this.$axios.put(`${this.$settings.host}/article/collection/operation/${id}/`,{
name:value,
},{
headers:{
'Authorization':`jwt ${token}`,}
}).then((res)=>{
// 修改文集列表
this.collection_list[index].name = res.data.name
}).catch((error)=>{
this.$message.error('修改文集名失败!')
})
}).catch(() => {
this.$message({
type: 'info',
message: '取消输入'
});
});
},
// 增加文集
add_collection(){
let token = this.$settings.check_user_login(this)
this.$axios.post(`${this.$settings.host}/article/collection/operation/`,{
name:this.collection_name
},{
headers:{
'Authorization':`jwt ${token}`,}
}).then((res)=>{
// 向列表中添加数据
this.collection_list.push(res.data)
}).catch((error)=>{
this.$message.error('添加文集失败!')
})
},
// 编辑删除文集状态
change_edit_event(){
this.edit_event_status = !this.edit_event_status
},
// 切换文集获取文章数据
change_collection_current_index(index,collection_id){
this.collection_id = collection_id
this.collection_current_index = index
// 选中文集的同时,获取文章数据
let token = this.$settings.check_user_login(this)
this.$axios.get(`${this.$settings.host}/article/articles/`,{
params:{
collection_id:collection_id,
},
headers:{
'Authorization':`jwt ${token}`,
},
}).then((res)=>{
this.article_list = res.data
// 显示文章内容和标题(编辑)
this.editorTitle = this.article_list[this.article_current_index].title
this.editorContent = this.article_list[this.article_current_index].content
}).catch((error)=>{
this.$message.error('获取文章失败!')
})
},
// 刷新页面获取文集信息 -->成功获取文章信息
get_collection_data(){
let token = this.$settings.check_user_login(this)
this.$axios.get(`${this.$settings.host}/article/collection/`,{
// params -- 写主体内容
// headers -- 写请求头键值对
headers:{
'Authorization':`jwt ${token}`,
}
}).then((res)=>{
console.log('>>>res',res)
this.collection_list = res.data
let collection_id = this.collection_list[0].id // 地一个文集id
// 调用方法显示地一个文集的文章
this.change_collection_current_index(0,collection_id)
}).catch((error)=>{
this.$message.error('获取文集失败!')
})
},
// 绑定@imgAdd event
imgAdd(pos, $file){
// 添加文件
console.log(' $file', $file);
},
imgDel(pos) {
// 删除文件
},
}
}
</script>
文章 -- 数据展示和添加,发布,删除
服务端:
路由: article/urls.py
path('articles/', views.ArticlesView.as_view({'get': 'list','post':'create'})), # 获取-添加文章
re_path('change_public/(?P<pk>\d+)/', views.ChangeArticlePublicView.as_view()), # 文章发布
re_path('remove_article/(?P<pk>\d+)/',views.RemoveArticleView.as_view()), # 移动文章
re_path('delete_article/(?P<pk>\d+)/',views.DeleteArticleView.as_view()), # 逻辑删除文章
article/views.py
# 文章操作
class ArticlesView(ModelViewSet):
permission_classes = [IsAuthenticated,]
# self.request.user -- 拿到当前登录用户
serializer_class = ArticlesSerializer
def get_queryset(self):
collection_id = self.request.query_params.get('collection_id')
queryset = Article.objects.filter(
user_id=self.request.user.id,
collection_id=collection_id,
is_show=True,
is_delete=False
).order_by('orders')
return queryset
# 改变文章发布状态
class ChangeArticlePublicView(APIView):
permission_classes = [IsAuthenticated, ]
# 发布文章
def put(self,request,pk):
is_public = request.data.get('is_public')
# 推送feed,给粉丝推送文章
# 获取当前作者的粉丝
ts = TableStore()
# 获取粉丝类表
fans_list = ts.get_author_fans(request.user.id)
if len(fans_list)>0:
# 给每一个粉丝推送feed
ts.push_feed(request.user.id,pk, fans_list)
try:
Article.objects.filter(pk=pk).update(
is_public=is_public
)
return Response({'msg':'ok'})
except:
logger.error(f'id为{pk}的文章,发布失败!')
return Response({'msg':'not ok'},status=507)
# 取消发布
def post(self,request,pk):
is_public = request.data.get('is_public')
try:
Article.objects.filter(pk=pk).update(
is_public=is_public
)
return Response({'msg':'ok'})
except:
logger.error(f'id为{pk}的文章,取消发布失败!')
return Response({'msg':'not ok'},status=507)
# 移动文章
class RemoveArticleView(APIView):
permission_classes = [IsAuthenticated, ]
# 发布文章
def put(self,request,pk):
collection_id = request.data.get('collection_id')
try:
Article.objects.filter(pk=pk).update(
collection_id=collection_id
)
return Response({'msg':'ok'})
except:
logger.error(f'id为{pk}的文章,移动失败!')
return Response({'msg':'not ok'},status=507)
# 删除文章
class DeleteArticleView(APIView):
permission_classes = [IsAuthenticated, ]
# 发布文章
def put(self,request,pk):
is_delete = request.data.get('is_delete')
try:
Article.objects.filter(pk=pk).update(
is_delete=is_delete
)
return Response({'msg':'ok'})
except:
logger.error(f'id为{pk}的文章,删除失败!')
return Response({'msg':'not ok'},status=507)
article/serialziers.py
# 文章操作
class ArticlesSerializer(serializers.ModelSerializer):
class Meta:
model = models.Article
fields = ['id','title','content','is_public']
extra_kwargs = {
'id':{'read_only':True},
'title':{'read_only':True},
'content':{'read_only':True},
'is_public':{'read_only':True},
}
# 添加文章
def create(self, validated_data):
position = self.context['request'].data.get('position')
if position == 0:
orders = models.Article.objects.all().order_by('orders')[0].orders - 1
else:
orders = models.Article.objects.all().order_by('-orders')[0].orders + 1
title = datetime.datetime.now().strftime('%Y-%m-%d')
article_obj = models.Article.objects.create(**{
'title':title,
'content':'',
'user_id':self.context['request'].user.id,
'collection_id':self.context['request'].query_params.get('collection_id'),
'orders':orders,
})
return article_obj
客户端
write.vue
<template>
<div class="write" @click="boss">
<div class="_2v5v5">
<div class="_3zibT"><a href="/">回首页</a></div>
<div class="_1iZMb">
<div class="_33Zlg" @click="collection_form=true"><i class="fa fa-plus"></i><span>新建文集</span></div>
<div class="_2G97m">
<form class="M8J6Q" :class="collection_form?'_2a1Rp':'_1mU5v'">
<input type="text" placeholder="请输入文集名..." name="name" class="_1CtV4" v-model="collection_name">
<button @click="add_collection" class="dwU8Q _3zXcJ _3QfkW"><span>提 交</span></button>
<button type="button" class="vIzwB _3zXcJ" @click="collection_form=false"><span>取 消</span></button>
</form>
</div>
</div>
<ul class="_3MbJ4 _3t059">
<li class="_3DM7w " @click="change_collection_current_index(collection_index,collection_value.id)" :class="{_31PCv:collection_current_index===collection_index}" :title="collection_value.name" v-for="(collection_value,collection_index) in collection_list" :key="collection_index">
<div @click.stop.prevent="change_edit_event" class="_3P4JX _2VLy-" v-if="collection_current_index===collection_index">
<i class="fa fa-gear"></i>
<span>
<ul class="_2V8zt _3FcHm _2w9pn" :class="edit_event_status?'':'NvfK4'">
<li @click="edit_collection(collection_value.name,collection_value.id,collection_index)" class="_2po2r cRfUr" title="">
<span class=""><i class="fa fa-pencil-square-o _22XWG"></i>修改文集</span>
</li>
<li @click="delete_collection(collection_value.id,collection_index)" class="_2po2r cRfUr" title="">
<span class=""><i class="fa fa-trash-o _22XWG"></i>删除文集</span>
</li>
</ul>
</span>
</div>
<span>{{ collection_value.name }}</span>
</li>
</ul>
<div style="height: 50px;"></div>
<div role="button" class="h-5Am">
<span class="ant-dropdown-trigger"><i class="fa fa-bars"></i><span>设置</span></span>
<span class="Yv5Zx">遇到问题<i class="fa fa-question-circle-o"></i></span>
</div>
</div>
<div class="rQQG7">
<div class="_3revO _2mnPN">
<div class="_3br9T">
<div>
<div class="_1GsW5" @click="add_article(0)"><i class="fa fa-plus-circle"></i><span> 新建文章</span></div>
<ul class="_2TxA-">
<li @click="change_article_index(article_index)" class="_25Ilv" :class="{_33nt7:article_current_index===article_index}" title="ABC" v-for="(article_value,article_index) in article_list" :key="article_index">
<i class="_13kgp" :class="{_2m93u:article_value.is_public}"></i>
<div @click.stop.prevent="change_article_toolbar_status" class="_3P4JX poOXI" v-if="article_current_index===article_index">
<i class="fa fa-gear"></i>
<span>
<ul class="_2V8zt _3FcHm _2w9pn" :class="{toolvar:article_toolbar_status}">
<li class="_2po2r cRfUr" title="">
<span class="" v-if="article_value.is_public" @click="article_not_public(article_value.id,article_index)"><i class="fa fa-share _22XWG" ></i>取消发布</span>
<span class="" v-else @click="article_public(article_value.id,article_index)"><i class="fa fa-share _22XWG"></i>直接发布</span>
</li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-clock-o _22XWG"></i>定时发布</span></li>
<li class="_2po2r cRfUr" title=""><span class="_20tIi"><i class="iconfont ic-paid _22XWG"></i>发布为付费文章</span></li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="iconfont ic-set _22XWG"></i>设置发布样式</span></li>
<li class="_3nZXj _2_WAp _3df2u _2po2r cRfUr" title=""><span class=""><i class="fa fa-folder-open _22XWG"></i>移动文章
<div class="_3x4X_">
<ul class="_2KzJx oGKRI _3DXDE _2w9pn">
<li @click="remove_article(cvalue.id)" class="_2po2r cRfUr" title="随笔" v-for="(cvalue,cindex) in collection_list" :key="cindex" v-if="cindex!=collection_current_index"><span class="">{{cvalue.name}}</span></li>
</ul>
</div>
</span>
</li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-history _22XWG"></i>历史版本</span></li>
<li @click="delete_article" class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-trash-o _22XWG"></i>删除文章</span></li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-ban _22XWG"></i>设置禁止转载</span></li>
</ul>
</span>
</div>
<span class="NariC">{{ article_value.title }}</span>
<span class="hLzJv">{{ article_value.content }}</span>
<span class="_29C-V">字数:{{article_value.content ?article_value.content.length:0}}</span>
</li>
</ul>
<div class="_2cVn3" @click="add_article(1)"><i class="fa fa-plus"></i><span> 在下方新建文章</span></div>
</div>
</div>
</div>
<input type="text" class="_24i7u" v-model="editorTitle">
<div id="editor"> <!--引入文本编辑器-->
<mavon-editor
style="height: 100%"
v-model="editorContent"
:ishljs="true"
ref=md
@change="content_change"
@imgAdd="imgAdd"
@imgDel="imgDel"
></mavon-editor>
</div>
</div>
</div>
</template>
<script>
// 引入富文本编辑器
import { mavonEditor } from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
export default {
name: "Writer",
data(){
return {
editorTitle:'', // 展示文章标题
editorContent:"", // 展示文章内容
editorContentRender:"", // 展示文章内容(解析后)
img_file:[], // 缓存图片列表
collection_form:false,
collection_list:[], // 文集列表
collection_current_index:0, // 默认选中文集索引
edit_event_status:true, //文集编辑状态
collection_name:'', //文集名称
collection_id:0, //文集id
article_list:[], // 文章列表
article_current_index:0, // 默认选中文章索引
article_toolbar_status:false, //文章编辑状态
position:0, //添加文章记录位置
timer:0, // 定时器
}
},
watch:{
editorContent(){
console.log(this.editorContent)
this.article_list[this.article_current_index].content = this.editorContent
this.save_content_ajax()
},
editorTitle(){
this.article_list[this.article_current_index].title = this.editorTitle
this.save_content_ajax()
}
},
// 数据加载到试图,自动触发方法
mounted(){
document.querySelector("#editor").style.height = document.documentElement.clientHeight-document.querySelector("._24i7u").clientHeight+"px";
this.get_collection_data()
},
components: {
mavonEditor // 挂载文本编辑器组件
},
methods:{
// 切换文章触发事件
change_article_index(article_index){
// 绑定文章
this.article_current_index=article_index;
// 切换文章显示内容
this.editorTitle = this.article_list[this.article_current_index].title
this.editorContent = this.article_list[this.article_current_index].content
},
// 移动文章 collection_id目标文集id
remove_article(collection_id){
// 获取当前文章id
let article_id = this.article_list[this.article_current_index].id
let token = this.$settings.check_user_login(this)
this.$axios.put(`${this.$settings.host}/article/remove_article/${article_id}/`,{
collection_id:collection_id
},{
headers:{
'Authorization':`jwt ${token}`,
},
}).then((res)=>{
this.$message.success('文章移动成功')
// 删除当前文章(在当前文集)
this.article_list.splice(this.article_current_index,1)
}).catch((error)=>{
this.$message.error('移动文章失败!')
})
},
// 逻辑删除文章
delete_article(){
// 获取当前文章id
let article_id = this.article_list[this.article_current_index].id
let token = this.$settings.check_user_login(this)
let is_delete = true
this.$axios.put(`${this.$settings.host}/article/delete_article/${article_id}/`,{
is_delete:is_delete
},{
headers:{
'Authorization':`jwt ${token}`,
},
}).then((res)=>{
this.$message.success('文章删除成功')
// 删除当前文章(在当前文集)
this.article_list.splice(this.article_current_index,1)
}).catch((error)=>{
this.$message.error('文章删除失败!')
})
},
// 文章发布
article_public(id,index){
let token = this.$settings.check_user_login(this)
this.$axios.put(`${this.$settings.host}/article/change_public/${id}/`,{
is_public:true
},{
headers:{
'Authorization':`jwt ${token}`,
},
}).then((res)=>{
this.article_list[index].is_public = true;
// 保存文章id和文章标题到本地
sessionStorage.article_id = id;
sessionStorage.article_title = this.article_list[index].title;
// 发布成功跳转到专题页面
this.$router.push('/postarticle')
}).catch((error)=>{
this.$message.error('文章发布失败!')
})
},
// 文章取消发布
article_not_public(id,index){
let token = this.$settings.check_user_login(this)
this.$axios.post(`${this.$settings.host}/article/change_public/${id}/`,{
is_public:false
},{
headers:{
'Authorization':`jwt ${token}`,
},
}).then((res)=>{
this.article_list[index].is_public = false;
}).catch((error)=>{
this.$message.error('文章取消发布失败!')
})
},
// 添加文章
add_article(position){
// position 0:上方 1:下方
let token = this.$settings.check_user_login(this)
this.$axios.post(`${this.$settings.host}/article/articles/`,{
position:position,
},{
params:{
collection_id:this.collection_id,
},
headers:{
'Authorization':`jwt ${token}`,
},
}).then((res)=>{
// position 0:上方 1:下方
if (position===0){
this.article_list.unshift(res.data)
}else {
this.article_list.push(res.data)
}
}).catch((error)=>{
this.$message.error('添加文章失败!')
})
},
// 改变文章编辑状态
change_article_toolbar_status(){
this.article_toolbar_status = !this.article_toolbar_status
},
// 最外层点击事件
boss(){
// 不显示文集设置按钮
this.edit_event_status = true
this.article_toolbar_status = false
},
}
}
</script>
定时发布[扩展知识点]
原理:使用celery完成定时任务!
步骤:
1. 当用户点选了定时发布, 页面中弹出一个选择时间的窗口。
2. 当用户设置完成发布时间以后,点击“确认”以后,把这个时间和文章id发送到服务端。
3. 服务端中文章模型的pub_date记录这个定时发布时间。
4. 在celery中创建一个定时任务,在每个固定时间段,检查文章表中,对应时间段的pub_date把对应的文章进行发布。
1.前端增加一个选择时间的弹窗,
代码;article.vue
<template>
<div class="write" @click="boss">
<div class="_2v5v5">
<div class="_3zibT"><a href="/">回首页</a></div>
<div class="_1iZMb">
<div class="_33Zlg" @click="collection_form=true"><i class="fa fa-plus"></i><span>新建文集</span></div>
<div class="_2G97m">
<form class="M8J6Q" :class="collection_form?'_2a1Rp':'_1mU5v'">
<input type="text" placeholder="请输入文集名..." name="name" class="_1CtV4" v-model="collection_name">
<button @click="add_collection" class="dwU8Q _3zXcJ _3QfkW"><span>提 交</span></button>
<button type="button" class="vIzwB _3zXcJ" @click="collection_form=false"><span>取 消</span></button>
</form>
</div>
</div>
<ul class="_3MbJ4 _3t059">
<li class="_3DM7w " @click="change_collection_current_index(collection_index,collection_value.id)" :class="{_31PCv:collection_current_index===collection_index}" :title="collection_value.name" v-for="(collection_value,collection_index) in collection_list" :key="collection_index">
<div @click.stop.prevent="change_edit_event" class="_3P4JX _2VLy-" v-if="collection_current_index===collection_index">
<i class="fa fa-gear"></i>
<span>
<ul class="_2V8zt _3FcHm _2w9pn" :class="edit_event_status?'':'NvfK4'">
<li @click="edit_collection(collection_value.name,collection_value.id,collection_index)" class="_2po2r cRfUr" title="">
<span class=""><i class="fa fa-pencil-square-o _22XWG"></i>修改文集</span>
</li>
<li @click="delete_collection(collection_value.id,collection_index)" class="_2po2r cRfUr" title="">
<span class=""><i class="fa fa-trash-o _22XWG"></i>删除文集</span>
</li>
</ul>
</span>
</div>
<span>{{ collection_value.name }}</span>
</li>
</ul>
<div style="height: 50px;"></div>
<div role="button" class="h-5Am">
<span class="ant-dropdown-trigger"><i class="fa fa-bars"></i><span>设置</span></span>
<span class="Yv5Zx">遇到问题<i class="fa fa-question-circle-o"></i></span>
</div>
</div>
<div class="rQQG7">
<div class="_3revO _2mnPN">
<div class="_3br9T">
<div>
<div class="_1GsW5" @click="add_article(0)"><i class="fa fa-plus-circle"></i><span> 新建文章</span></div>
<ul class="_2TxA-">
<li @click="change_article_index(article_index)" class="_25Ilv" :class="{_33nt7:article_current_index===article_index}" title="ABC" v-for="(article_value,article_index) in article_list" :key="article_index">
<i class="_13kgp" :class="{_2m93u:article_value.is_public}"></i>
<div @click.stop.prevent="change_article_toolbar_status" class="_3P4JX poOXI" v-if="article_current_index===article_index">
<i class="fa fa-gear"></i>
<span>
<ul class="_2V8zt _3FcHm _2w9pn" :class="{toolvar:article_toolbar_status}">
<li class="_2po2r cRfUr" title="">
<span class="" v-if="article_value.is_public" @click="article_not_public(article_value.id,article_index)"><i class="fa fa-share _22XWG" ></i>取消发布</span>
<span class="" v-else @click="article_public(article_value.id,article_index)"><i class="fa fa-share _22XWG"></i>直接发布</span>
</li>
<li v-if="!article_value.is_public" class="_2po2r cRfUr" title="" @click="dialogVisible = true"><span class=""><i class="fa fa-clock-o _22XWG"></i>定时发布</span></li>
<li class="_2po2r cRfUr" title=""><span class="_20tIi"><i class="iconfont ic-paid _22XWG"></i>发布为付费文章</span></li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="iconfont ic-set _22XWG"></i>设置发布样式</span></li>
<li class="_3nZXj _2_WAp _3df2u _2po2r cRfUr" title=""><span class=""><i class="fa fa-folder-open _22XWG"></i>移动文章
<div class="_3x4X_">
<ul class="_2KzJx oGKRI _3DXDE _2w9pn">
<li @click="remove_article(cvalue.id)" class="_2po2r cRfUr" title="随笔" v-for="(cvalue,cindex) in collection_list" :key="cindex" v-if="cindex!=collection_current_index"><span class="">{{cvalue.name}}</span></li>
</ul>
</div>
</span>
</li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-history _22XWG"></i>历史版本</span></li>
<li @click="delete_article" class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-trash-o _22XWG"></i>删除文章</span></li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-ban _22XWG"></i>设置禁止转载</span></li>
</ul>
</span>
</div>
<span class="NariC">{{ article_value.title }}</span>
<span class="hLzJv">{{ article_value.content }}</span>
<span class="_29C-V">字数:{{article_value.content ?article_value.content.length:0}}</span>
</li>
</ul>
<div class="_2cVn3" @click="add_article(1)"><i class="fa fa-plus"></i><span> 在下方新建文章</span></div>
</div>
</div>
</div>
<input type="text" class="_24i7u" v-model="editorTitle">
<div id="editor"> <!--引入文本编辑器-->
<mavon-editor
style="height: 100%"
v-model="editorContent"
:ishljs="true"
ref=md
@change="content_change"
@imgAdd="imgAdd"
@imgDel="imgDel"
></mavon-editor>
</div>
<!--定时发布时间选择器-->
<el-dialog
title="提示"
:visible.sync="dialogVisible"
width="30%"
>
<span>请选择发布时间</span>
<div class="block">
<span class="demonstration">默认</span>
<el-date-picker
v-model="pub_date"
type="datetime"
format="yyyy-MM-dd HH:mm"
placeholder="选择日期时间">
</el-date-picker>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="time_public">确 定</el-button>
</span>
</el-dialog>
</div>
</div>
</template>
<script>
// 引入富文本编辑器
import { mavonEditor } from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
export default {
name: "Writer",
data(){
return {
editorTitle:'', // 展示文章标题
editorContent:"", // 展示文章内容
editorContentRender:"", // 展示文章内容(解析后)
img_file:[], // 缓存图片列表
collection_form:false,
collection_list:[], // 文集列表
collection_current_index:0, // 默认选中文集索引
edit_event_status:true, //文集编辑状态
collection_name:'', //文集名称
collection_id:0, //文集id
article_list:[], // 文章列表
article_current_index:0, // 默认选中文章索引
article_toolbar_status:false, //文章编辑状态
position:0, //添加文章记录位置
timer:0, // 定时器
dialogVisible:false, // 时间选择弹窗
pub_date:null, // 发布时间
}
},
watch:{
// 文章内容发生变化
editorContent(){
console.log(this.editorContent)
this.article_list[this.article_current_index].content = this.editorContent
this.save_content_ajax()
},
// 文章标题发生变化
editorTitle(){
this.article_list[this.article_current_index].title = this.editorTitle
this.save_content_ajax()
}
},
// 数据加载到试图,自动触发方法
mounted(){
document.querySelector("#editor").style.height = document.documentElement.clientHeight-document.querySelector("._24i7u").clientHeight+"px";
this.get_collection_data()
},
components: {
mavonEditor // 挂载文本编辑器组件
},
methods:{
// 定时发布
time_public(){
let token = this.$settings.check_user_login(this)
let article_id = this.article_list[this.article_current_index].id
// this.pub_data 获取时间对象 -- 转化成时间字符串 -- 年月日时分
let pub_date = `${this.pub_date.getFullYear()}-${this.pub_date.getMonth()+1}-${this.pub_date.getDate()} ${this.pub_date.getHours()}:${this.pub_date.getMinutes()}`
console.log(pub_date)
this.$axios.put(`${this.$settings.host}/article/time_public/${article_id}/`,{
pub_date:pub_date,
},{
headers:{
'Authorization':`jwt ${token}`,
},
}).then((res)=>{
this.$message.success('定时发布成功')
this.dialogVisible = false
}).catch((error)=>{
this.$message.error('文章定时发布失败!')
})
},
}
}
</script>
2.服务端提供修改pub_date发布时间的api接口
路由article/urls.py:
from django.urls import path,re_path
from . import views
urlpatterns = [
re_path('time_public/(?P<pk>\d+)/', views.TimeArticlePublicView.as_view()), # 定时发布
]
视图代码article/views.py:
# 文章定时发布
class TimeArticlePublicView(APIView):
permission_classes = [IsAuthenticated, ]
# 发布文章
def put(self,request,pk):
pub_date = request.data.get('pub_date')
# 将字符串转换成时间戳,发布时间戳
pub_date_timestamp = datetime.timestamp(datetime.strptime(pub_date,'%Y-%m-%d %H:%M'))
# 当前时间戳
now_timestamp = datetime.now().timestamp()
if pub_date_timestamp < now_timestamp:
return Response({'error':'发布时间不能小于当前时间'},status=400)
# 推送feed,给粉丝推送文章
# 获取当前作者的粉丝
ts = TableStore()
# 获取粉丝类表
fans_list = ts.get_author_fans(request.user.id)
if len(fans_list)>0:
# 给每一个粉丝推送feed
ts.push_feed(request.user.id,pk, fans_list)
try:
Article.objects.filter(pk=pk).update(
pub_date=pub_date
)
return Response({'msg':'ok'})
except:
logger.error(f'id为{pk}的文章,发布失败!')
return Response({'msg':'not ok'},status=507)
3.使用celery的定时任务
每分钟执行一次定时发布操作,让pub_date时间到了,则更新对应文章的发布状态。
在mycelery中创建time_article任务目录,在目录下创建任务文件tasks.py,编写异步任务:
from mycelery.main import app
from article.models import Article
from datetime import datetime
@app.task(name='time_public')
def time_public():
# 查询pub_date不为空的,发布时间小于周期任务执行时间的文章
article_list = Article.objects.filter(pub_date__lte=datetime.now()).exclude(pub_date=None)
print(article_list)
for article in article_list:
article.is_public = True
article.save()
注册异步任务到myselery/main.py中。使用celery的定时任务调度器,让celery定时执行异步任务,代码:
from celery import Celery
import os
import django
from django.conf import settings
# 把celery和django进行组合,识别和加载django的配置文件
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'renranapi.settings.dev')
# 对django框架执行初始化
django.setup()
# 实例化对象
app = Celery('renran')
# 加载配置
app.config_from_object('mycelery.config')
# 自动搜索并加载任务
app.autodiscover_tasks(['mycelery.mytasks','mycelery.time_article'])
# 定时任务调度器
app.conf.beat_schedule={
'every_10_seconds':{
'task':'time_public',
# 'schedule': crontab(), # 时间间隔
'schedule':10,
# 'args':(16,16) # 参数
},
}
# 修改时区,和django框架同步时区
app.conf.timezone = settings.TIME_ZONE
Celery官方文档中关于定时任务使用的说明:http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html
接下来,我们就可以重启Celery并启用Celery的定时任务调度器
先在终端下,运行celery的定时任务程序,以下命令:
celery -A mycelery.main beat # mycelery.main 是celery的主应用文件
然后再新建一个终端,运行以下命令,上面的命令必须先指定:
celery -A mycelery.main worker --loglevel=info
注意,使用的时候,如果有时区必须先配置好系统时区。
文章内容保存
前端ajax节流
现在用户每一次粒度很小的操纵都会导致前端发送一次ajax请求,所以我们可以通过定时器setTimeout来让ajax延时发送请求,例如,当用户进行修改操作时,我们可以调用setTimeout来让ajax2秒发送请求,如果2秒内,用户有继续操作了文章,则重新计算2秒。 这种解决问题的思路,在前端里面叫函数节流/ajax节流。
客户端,代码:writer.vue
<template>
<div class="write" @click="boss">
<div class="_2v5v5">
<div class="_3zibT"><a href="/">回首页</a></div>
<div class="_1iZMb">
<div class="_33Zlg" @click="collection_form=true"><i class="fa fa-plus"></i><span>新建文集</span></div>
<div class="_2G97m">
<form class="M8J6Q" :class="collection_form?'_2a1Rp':'_1mU5v'">
<input type="text" placeholder="请输入文集名..." name="name" class="_1CtV4" v-model="collection_name">
<button @click="add_collection" class="dwU8Q _3zXcJ _3QfkW"><span>提 交</span></button>
<button type="button" class="vIzwB _3zXcJ" @click="collection_form=false"><span>取 消</span></button>
</form>
</div>
</div>
<ul class="_3MbJ4 _3t059">
<li class="_3DM7w " @click="change_collection_current_index(collection_index,collection_value.id)" :class="{_31PCv:collection_current_index===collection_index}" :title="collection_value.name" v-for="(collection_value,collection_index) in collection_list" :key="collection_index">
<div @click.stop.prevent="change_edit_event" class="_3P4JX _2VLy-" v-if="collection_current_index===collection_index">
<i class="fa fa-gear"></i>
<span>
<ul class="_2V8zt _3FcHm _2w9pn" :class="edit_event_status?'':'NvfK4'">
<li @click="edit_collection(collection_value.name,collection_value.id,collection_index)" class="_2po2r cRfUr" title="">
<span class=""><i class="fa fa-pencil-square-o _22XWG"></i>修改文集</span>
</li>
<li @click="delete_collection(collection_value.id,collection_index)" class="_2po2r cRfUr" title="">
<span class=""><i class="fa fa-trash-o _22XWG"></i>删除文集</span>
</li>
</ul>
</span>
</div>
<span>{{ collection_value.name }}</span>
</li>
</ul>
<div style="height: 50px;"></div>
<div role="button" class="h-5Am">
<span class="ant-dropdown-trigger"><i class="fa fa-bars"></i><span>设置</span></span>
<span class="Yv5Zx">遇到问题<i class="fa fa-question-circle-o"></i></span>
</div>
</div>
<div class="rQQG7">
<div class="_3revO _2mnPN">
<div class="_3br9T">
<div>
<div class="_1GsW5" @click="add_article(0)"><i class="fa fa-plus-circle"></i><span> 新建文章</span></div>
<ul class="_2TxA-">
<li @click="change_article_index(article_index)" class="_25Ilv" :class="{_33nt7:article_current_index===article_index}" title="ABC" v-for="(article_value,article_index) in article_list" :key="article_index">
<i class="_13kgp" :class="{_2m93u:article_value.is_public}"></i>
<div @click.stop.prevent="change_article_toolbar_status" class="_3P4JX poOXI" v-if="article_current_index===article_index">
<i class="fa fa-gear"></i>
<span>
<ul class="_2V8zt _3FcHm _2w9pn" :class="{toolvar:article_toolbar_status}">
<li class="_2po2r cRfUr" title="">
<span class="" v-if="article_value.is_public" @click="article_not_public(article_value.id,article_index)"><i class="fa fa-share _22XWG" ></i>取消发布</span>
<span class="" v-else @click="article_public(article_value.id,article_index)"><i class="fa fa-share _22XWG"></i>直接发布</span>
</li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-clock-o _22XWG"></i>定时发布</span></li>
<li class="_2po2r cRfUr" title=""><span class="_20tIi"><i class="iconfont ic-paid _22XWG"></i>发布为付费文章</span></li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="iconfont ic-set _22XWG"></i>设置发布样式</span></li>
<li class="_3nZXj _2_WAp _3df2u _2po2r cRfUr" title=""><span class=""><i class="fa fa-folder-open _22XWG"></i>移动文章
<div class="_3x4X_">
<ul class="_2KzJx oGKRI _3DXDE _2w9pn">
<li @click="remove_article(cvalue.id)" class="_2po2r cRfUr" title="随笔" v-for="(cvalue,cindex) in collection_list" :key="cindex" v-if="cindex!=collection_current_index"><span class="">{{cvalue.name}}</span></li>
</ul>
</div>
</span>
</li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-history _22XWG"></i>历史版本</span></li>
<li @click="delete_article" class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-trash-o _22XWG"></i>删除文章</span></li>
<li class="_2po2r cRfUr" title=""><span class=""><i class="fa fa-ban _22XWG"></i>设置禁止转载</span></li>
</ul>
</span>
</div>
<span class="NariC">{{ article_value.title }}</span>
<span class="hLzJv">{{ article_value.content }}</span>
<span class="_29C-V">字数:{{article_value.content ?article_value.content.length:0}}</span>
</li>
</ul>
<div class="_2cVn3" @click="add_article(1)"><i class="fa fa-plus"></i><span> 在下方新建文章</span></div>
</div>
</div>
</div>
<input type="text" class="_24i7u" v-model="editorTitle">
<div id="editor"> <!--引入文本编辑器-->
<mavon-editor
style="height: 100%"
v-model="editorContent"
:ishljs="true"
ref=md
@change="content_change"
@imgAdd="imgAdd"
@imgDel="imgDel"
></mavon-editor>
</div>
</div>
</div>
</template>
<script>
import { mavonEditor } from 'mavon-editor'
import 'mavon-editor/dist/css/index.css';
import "../../static/font-awesome/css/font-awesome.css";
export default {
name: "Write",
data(){
return {
editorTitle:'', // 展示文章标题
editorContent:"", // 展示文章内容
editorContentRender:"", // 展示文章内容(解析后)
img_file:[], // 缓存图片列表
collection_form:false,
collection_list:[], // 文集列表
collection_current_index:0, // 默认选中文集索引
edit_event_status:true, //文集编辑状态
collection_name:'', //文集名称
collection_id:0, //文集id
article_list:[], // 文章列表
article_current_index:0, // 默认选中文章索引
article_toolbar_status:false, //文章编辑状态
position:0, //添加文章记录位置
timer:0, // 定时器
}
},
watch:{
// 文章内容发生变化
editorContent(){
console.log(this.editorContent)
this.article_list[this.article_current_index].content = this.editorContent
this.save_content_ajax()
},
// 文章标题发生变化
editorTitle(){
this.article_list[this.article_current_index].title = this.editorTitle
this.save_content_ajax()
}
},
// 数据加载到试图,自动触发方法
mounted(){
document.querySelector("#editor").style.height = document.documentElement.clientHeight-document.querySelector("._24i7u").clientHeight+"px";
this.get_collection_data()
},
// 挂载文本编辑器组件
components: {
mavonEditor
},
methods:{
// ajax截流,保存文章内容和标题
save_content_ajax(){
// 定义定时器,每两秒保存文章没人
clearInterval(this.timer)
this.timer = setInterval(this.save_content,2000)
},
// 保存文章内容和标题
save_content(){
// 判断内容是否发生变化,如果没有变化,关闭定时器
if (this.article_list[this.article_current_index].content === this.editorContent){
clearInterval(this.timer)
}
let token = this.$settings.check_user_login(this)
let id = this.article_list[this.article_current_index].id
this.$axios.put(`${this.$settings.host}/article/content/${id}/`,{
title:this.editorTitle,
content:this.editorContent,
render:this.editorContentRender,
},{
headers:{
'Authorization':`jwt ${token}`,
},
}).then((res)=>{
this.$message.success('保存成功')
}).catch((error)=>{
this.$message.error('文章内容保存失败')
})
},
// 编辑文章内容时触发事件
content_change(content,render){
this.editorContentRender = render
},
// 切换文章触发事件
change_article_index(article_index){
// 绑定文章
this.article_current_index=article_index;
// 切换文章显示内容
this.editorTitle = this.article_list[this.article_current_index].title
this.editorContent = this.article_list[this.article_current_index].content
},
}
}
</script>
服务端
服务端提供修改文章内容和标题的api接口。
模型新增一个保存文章内容显示效果的字段render。
class Article(BaseModel):
"""文章模型"""
content = models.TextField(null=True, blank=True, verbose_name="文章内容")
render = models.TextField(null=True, blank=True, verbose_name="文章内容[解析后]")
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="作者")
collection = models.ForeignKey(ArticleCollection, on_delete=models.CASCADE, verbose_name="文集")
pub_date = models.DateTimeField(null=True, default=None, verbose_name="发布时间")
access_pwd = models.CharField(max_length=15, null=True, blank=True, verbose_name="访问密码")
read_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="阅读量")
like_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="点赞量")
collect_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="收藏量")
comment_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="评论量")
reward_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="赞赏量")
is_public = models.BooleanField(default=False, verbose_name="是否发布")
class Meta:
db_table = "rr_article"
verbose_name = "文章"
verbose_name_plural = verbose_name
数据迁移,
python manage.py makemigrations
python manage.py migrate
视图代码:article/views.py
# 保存标题和文章内容
class SaveContentView(APIView):
permission_classes = [IsAuthenticated, ]
def put(self,request,pk):
try:
article_obj = Article.objects.get(pk=pk)
except:
logger.error(f'id为{pk}的文章,不存在!')
return Response({'msg':'文章不存在!'},status=507)
article_obj.title = request.data.get('title')
article_obj.content = request.data.get('content')
article_obj.render = request.data.get('render')
article_obj.save()
return Response({'msg':'文章保存成功'})
# 保存图片
class SaveImageView(CreateAPIView):
queryset = ArticleImage.objects.all()
serializer_class = ImageModelserializer
路由代码:article/urls.py
from django.urls import path,re_path
from . import views
urlpatterns = [
# ....
# 保存文章内容
re_path('content/(?P<pk>\d+)/',views.SaveContentView.as_view()),
# 保存图片
path('image/',views.SaveImageView.as_view()),
]
序列化器: article/serializer.py
# 保存图片
class ImageModelserializer(serializers.ModelSerializer):
class Meta:
model = models.ArticleImage
fields = ['image']
def create(self, validated_data):
image_obj = models.ArticleImage.objects.create(
image = self.context['request'].data.get('image')
)
return image_obj