zoukankan      html  css  js  c++  java
  • 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;
         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 {
         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;
         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;
         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;
         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;
         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;
         22px;
        height: 30px;
      }
      ._3P4JX {
        font-size: 16px;
         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;
         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;
         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;
         66.666666%;
    }
      #editor {
        margin: auto;
         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
    
  • 相关阅读:
    用户态切换到内核态的3种方式
    vim_action
    import date
    __sizeof__()
    classmethod staticmethod
    Java对对象的引用 不是 引用调用 而是按值引用 Java不存在引用调用
    多线程同步
    Does Hadoop require SSH?
    hdfs namenode出错
    软件项目的一致性语义描述
  • 原文地址:https://www.cnblogs.com/jia-shu/p/14677171.html
Copyright © 2011-2022 走看看