一、目标:
在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">×</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