【Django+mdeditor】实现markdown编辑内容

一、目标:

在django项目中,部分字段支持markdown格式进行编辑,预览、文件上传等功能。

二、配置:

  • 1、官网链接:https://pandao.github.io/editor.md/,下载并解压到都django的静态目录下;
  • 2、引入css和js:
<link rel="stylesheet" href="{% static 'plugins/editor_md/css/editormd.css' %}">
<script src="{% static 'plugins/editor_md/editormd.min.js' %}"></script>

三、实现插件转换:

  • 1、定义一个div,包裹待转换的textarea,插件会自动转换为markdown编辑器
<div id="editor"> 
   		<textarea>.....</textarea>
</div>
  • 2、编写js代码,初始化markdown编辑器
<script>
        $(function(){
            initEditorMd();
        })
        /*
		 初始化markdown编辑器(textarea转换为编辑器)
	     */
        function initEditorMd(){
            editormd('editor',{
                placeholder: "请输入内容",
                height:500,
                path: "{% static 'plugins/editor_md/lib/' %}", //依赖的其他js/css文件路径
         
            })
        }
</script>
 

四、实现markdown页面预览:

用markdown编辑的内容,在详情页展示时也需要保持markdown格式进行预览,需要额外进行以下操作:

说明:以下路径是我创建的django项目的静态路径,需要根据自己项目的实际路径进行修改。

1、引入css和js文件:

<link rel="stylesheet" href="{% static 'plugins/editor_md/css/editormd.preview.css' %}">

<script src="{% static 'plugins/editor_md/lib/marked.min.js' %}"></script>
    <script src="{% static 'plugins/editor_md/lib/prettify.min.js' %}"></script>
    <script src="{% static 'plugins/editor_md/lib/raphael.min.js' %}"></script>
    <script src="{% static 'plugins/editor_md/lib/underscore.min.js' %}"></script>
    <script src="{% static 'plugins/editor_md/lib/sequence-diagram.min.js' %}"></script>
    <script src="{% static 'plugins/editor_md/lib/flowchart.min.js' %}"></script>
    <script src="{% static 'plugins/editor_md/lib/jquery.flowchart.min.js' %}"></script>

2、编写js:

<script>
        $(function(){
            initEditorMd();
            initPreivewMarkdown();//预览的页面
        })
function initPreivewMarkdown(){
            editormd.markdownToHTML("previewMarkdown",{
                htmlDebode: "style,script,iframe"
            })
        }
</script>

五、实现本地文件上传

第三步中的配置默认是不支持本地文件上传的,需要新增imageUpload、imageFormats和imageUploadURL 3个参数

 /*
		初始化markdown编辑器(textarea转换为编辑器)
	*/
        function initEditorMd(){
            editormd('editor',{
                placeholder: "请输入内容",
                height:500,
                path: "{% static 'plugins/editor_md/lib/' %}", //依赖的其他js/css文件路径
                imageUpload: true,//支持本地上传文件
                imageFormats:['jpg','jpeg','png','gif'],//支持的文件格式
                imageUploadURL: "{% url 'upload_url' %}"//请求的url
            })
        }

六、整体代码:

项目整体今结构:
在这里插入图片描述

  • urls.py:
"""bugProject URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from app01 import views


urlpatterns = [
    path('comment', views.comment, name='comment'),
    path('comment/add', views.comment_add, name='comment_add'),
    path('comment/catalog', views.comment_catalog, name='comment_catalog'),
    path('comment/upload', views.upload_url, name='upload_url'),
]

  • models.py
from django.db import models


class Comment(models.Model):
    """评论表"""
    title = models.CharField(max_length=32, verbose_name="标题")
    content = models.TextField(verbose_name="内容", blank=True, null=True)
    parent = models.ForeignKey(to="Comment", verbose_name="父目录",
                               blank=True, null=True, on_delete=models.CASCADE)
    depth = models.SmallIntegerField(verbose_name="目录层级")

    def __str__(self):
        return self.title

  • views.py
import os
from django.shortcuts import render, redirect
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
# XFrameOption 有三种,DENY:不允许嵌入IFrame,SAME_ORIGIN:运行显示同源iframe,ALLOW_FROM指定地址的iframe
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.clickjacking import xframe_options_deny
from django.views.decorators.clickjacking import xframe_options_sameorigin

from app01.forms.project import ProjectForm, CommentForm
from app01 import models


def comment(request):
    """
    测试多级目录
    :param request:
    :return:
    """
    cid = request.GET.get('cid')
    form = CommentForm(request)
    if not cid or not cid.isdecimal():
        return render(request, 'comment.html', {'form': form})

    comment_obj = models.Comment.objects.filter(id=cid).first()
    return render(request, 'comment.html',
                  {'form': form, 'comment_obj': comment_obj})


def comment_add(request):
    """
    添加目录
    :param request:
    :return:
    """
    form = CommentForm(request, data=request.POST)
    if form.is_valid():
        # 判断用户是否已经选择父目录
        if form.instance.parent:
            form.instance.depth = form.instance.parent.depth + 1
        else:
            form.instance.depth = 1
        form.save()
        return JsonResponse({'status': True})
    return JsonResponse({'status': False, 'error': form.errors})


def comment_catalog(requset):
    """
    展示所有的评论
    :param requset:
    :return:
    """
    query_set = models.Comment.objects.all().values('id', 'title', 'parent_id') \
        .order_by('depth', 'id')

    return JsonResponse({'status': True, 'data': list(query_set)})


@xframe_options_exempt
@csrf_exempt
def upload_url(request):
    """
    md上传文件
    :param request:
    :return:
    """
    # 返回固定格式数据,通知markdown插件是否上传成功
    # http://editor.md.ipandao.com/examples/image-upload.html
    result = {
        'success': 0, # 0 表示上传失败,1 表示上传成功
        'message': None,
        'url': None  # 上传成功时才返回给插件
    }

    image_obj = request.FILES.get("editormd-image-file")
    print(image_obj, request.get_host())
    if not image_obj:
        result['message'] = "文件不存在"
        return JsonResponse(result)
    path = os.path.join('static', 'upload', image_obj.name)
    file_path = os.path.join(settings.BASE_DIR, path)
    if not os.path.exists(file_path):
        with open(file_path, 'wb+') as f:
            # 文件分块写入
            for chunk in image_obj.chunks():
                f.write(chunk)

    path = "http://%s/%s" % (request.get_host(), path.replace("\\", '/'))
    print(path)
    result['success'] = 1
    result['url'] = path
    return JsonResponse(result)
  • forms/project.py
from django import forms
from django.core.exceptions import ValidationError
from app01 import models


class BaseForm(object):
    """自定义form基类"""
    # 不需要加form-control属性的字段
    bootstrap_class_exclude = []

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for name, field in self.fields.items():
            if name in self.bootstrap_class_exclude:
                continue
            old_class = field.widget.attrs.get('class', '')
            field.widget.attrs['class'] = '{} form-control'.format(old_class)
            field.widget.attrs['placeholder'] = '请输入%s' % field.label


class CommentForm(BaseForm, forms.ModelForm):
    """评论类的form"""
    class Meta:
        model = models.Comment
        exclude = ['depth']
        error_messages = {
            'title': {'required': "项目名不能为空"}
        }

    def __init__(self, request, *args, **kwargs):
        """初始化函数"""
        super().__init__(*args, **kwargs)
        self.request = request

  • comment.html
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>多级评论</title>
     <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
	<link rel="stylesheet" href="{% static 'plugins/editor_md/css/editormd.css' %}">
	<link rel="stylesheet" href="{% static 'plugins/editor_md/css/editormd.preview.css' %}">
    <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
	<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
    <style>
        .panel-default{
            margin-top: 10px;
        }
        .panel-body{
            padding: 0;
        }
        .panel-default .panel-heading{
            display: flex;
            flex-direction: row;
            justify-content: space-between;
            text-align: left;
        }
        .comment-list{
            border-right: 1px solid #dddddd;
            min-height: 500px;
        }
        .comment-list ul{
            padding-left: 15px;
        }
        .comment-list ul a{
            display: block;
            padding: 5px 0;
        }
        .content{
            border-left: 1px solid #dddddd;
            min-height: 600px;
            margin-left: -1px;
        }
        .editormd-fullscreen{
            z-index: 1002;
        }
    </style>

</head>
<body>
<div class="container-fluid">

    <div class="panel panel-default">
      <div class="panel-heading">
          <div><i class="fa fa-book" aria-hidden="true"></i> 目录</div>
        <div class="operation">
            <a type="button" class="btn btn-xs btn-success"data-toggle="modal" data-target="#myModal">
                <i class="fa fa-plus-circle"></i>新建
            </a>
        </div>
      </div>
      <div class="panel-body">
        <div class="col-sm-3 comment-list">
            <ul id="catalog">

            </ul>
        </div>
          <div class="col-sm-9 content">
              {% if comment_obj %}
                  <div id="previewMarkdown">
                      <textarea >{{ comment_obj.content }}</textarea>
                  </div>
              {% else %}
                   文本内容
                  <a href="{% url 'comment_add' %}"><i class="fa fa-plus-circle" aria-hidden="true"></i>新建文章</a>
              {% endif %}

        </div>

      </div>
    </div>

</div>


<div id="myModal" class="modal fade" tabindex="-1" role="dialog">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        <h4 class="modal-title">新增项目</h4>
      </div>
      <div class="modal-body">
          <form id="addForm">
              {% csrf_token %}
              {% for field in form %}
                  <div class="form-group">
                      <label for="{{ field.id_for_label }}">{{ field.label }}</label>
                         {% if field.name == 'content' %}
                            <div id="editor">
                                {{ field }}
                            </div>
                          {% else %}
                            {{ field }}
                          {% endif %}
                    <span class="error-msg">{{ field.errors.0 }}</span>
                  </div>
              {% endfor %}
          </form>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
        <button id='btnSubmit' type="button" class="btn btn-primary">提交</button>
      </div>
    </div><!-- /.modal-content -->
  </div><!-- /.modal-dialog -->
</div><!-- /.modal -->

    <script src="{% static 'plugins/editor_md/editormd.min.js' %}"></script>

    <script src="{% static 'plugins/editor_md/lib/marked.min.js' %}"></script>
    <script src="{% static 'plugins/editor_md/lib/prettify.min.js' %}"></script>
    <script src="{% static 'plugins/editor_md/lib/raphael.min.js' %}"></script>
    <script src="{% static 'plugins/editor_md/lib/underscore.min.js' %}"></script>
    <script src="{% static 'plugins/editor_md/lib/sequence-diagram.min.js' %}"></script>
    <script src="{% static 'plugins/editor_md/lib/flowchart.min.js' %}"></script>
    <script src="{% static 'plugins/editor_md/lib/jquery.flowchart.min.js' %}"></script>
    <script>
        $(function(){
            bindSubmit();
            initCatalog();
            initEditorMd();
            initPreivewMarkdown();//预览的页面
        })

        function bindSubmit() {
            $("#btnSubmit").click(function () {
                $.ajax({
                    url: "{% url 'comment_add' %}",
                    type: "POST",
                    data: $("#addForm").serialize(),
                    dataType: "JSON",
                    success: function (res) {
                        console.log(res);
                        if (res.status) {
                            location.href = location.href
                            //location.reload();
                        } else {
                            $.each(res.error, function (key, value) {
                                $("#id_" + key).next().text(value[0]);
                            })
                        }
                    }
                })
            })
        }

        function initCatalog() {
        $.ajax({
            url:"{% url 'comment_catalog' %}",
            type: 'GET',
            dataType: "JSON",
            success:function (res) {
                if (res.status){
                    preUrl = "{% url 'comment' %}" + "?cid="
                   $.each(res.data, function (index, item) {
                       var li=$("<li>").attr('id', 'id_'+item.id).append($("<a>").text(item.title).attr("href",preUrl+ item.id)).append($('<ul>'))
                       if (!item.parent_id){
                           // 直接添加到catalog中
                           $("#catalog").append(li);
                       }else{
                           $("#id_"+item.parent_id).children('ul').append(li);
                       }
                   })
                }else{
                    alert("目录初始化失败");
                }
            }
        })
    }
         /*
        初始化markdown编辑器(textarea转换为编辑器)
         */
        function initEditorMd(){
            editormd('editor',{
                placeholder: "请输入内容",
                height:500,
                path: "{% static 'plugins/editor_md/lib/' %}",
                imageUpload: true,
                imageFormats:['jpg','jpeg','png','gif'],
                imageUploadURL: "{% url 'upload_url' %}"
            })
        }

        function initPreivewMarkdown(){
            editormd.markdownToHTML("previewMarkdown",{
                htmlDebode: "style,script,iframe"
            })
        }
</script>
</body>
</html>

说明:
1、mkeditor关于图片上传的官方示例链接:http://editor.md.ipandao.com/examples/image-upload.html
在这里插入图片描述

2、upload_url视图函数需要加上装饰器xframe_options_exempt,否则会提示以下错误:
在这里插入图片描述

参考链接:https://gitee.com/wupeiqi/s25/tree/master

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值