欢迎来到十九分快乐的博客

生死看淡,不服就干。

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>

文集

  1. 判断用户是否登录了
  2. 查询当前用户的所有文集
  3. 如果用户没有文集,那么给用户添加两个默认文件,随笔和日记本
  4. 用户新建文集
  5. 查询文集中的所有文章
  6. 当前默认显示的文章的标题和内容

客户端: 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
posted @ 2021-04-19 15:51  十九分快乐  阅读(88)  评论(0编辑  收藏  举报