flask博客首先支持在线编辑markdown编辑,作为技术人通常使用markdown写文档还是比较顺手的,因为格式统一,不需要花很多的时间在排版上,我选用了国内editor.md作为本博客的编辑器,下面就对editor.md的集成做下简单介绍

文章编辑界面

{% extends 'admin/common/base.html' %}
{% block css %}
{{ super() }}
<link rel="stylesheet" href="{{url_for('.static',filename='editor_md/editormd.min.css')}}">
<link rel="stylesheet" href="{{url_for('.static',filename='css/write.css')}}">
{% endblock %}
{% block js %}
{{ super() }}
<script src="{{url_for('.static',filename='editor_md/editormd.min.js')}}"></script>
<script>

    var imagehosting_for = "thumbnail-img";
    //图床
    function imagehosting(flag) {
        imagehosting_for = flag;
        layer.open({
            type: 2,
            title: '图片管理',
            shade: 0.8,
            maxmin: true,
            shadeClose: true,
            area: ['80%', '90%'],
            content: '{{url_for("admin.image_hosting")}}' //这里content是一个URL,如果你不想让iframe出现滚动条,你还可以content: ['http://sentsin.com', 'no']
        });
    }

    function imagehosting_callback(obj) {
        //缩略图
        if(imagehosting_for === 'thumbnail-img') {
            $('#thumbnail-img').attr('src', obj.img_url);
            $('#thumbnail').val(obj.img_url);
        }
        //编辑器 editor.md
        if(imagehosting_for === 'editor-img') {
            console.log(myEditor)
            myEditor.settings.imagehosting_callback(obj)
        }

    }

    var myEditor;
    $(function () {
        myEditor = editormd("write-editor", {
            // width: '100%',
            height: '450px',
            syncScrolling: "single",
            path: "{{url_for('.static',filename='editor_md/lib/')}}",
            emoji: true,//emoji表情,默认关闭
            watch: true, // 关闭实时预览
            taskList: true,
            tocm: true, // Using [TOCM]
            tex: true,// 开启科学公式TeX语言支持,默认关闭
            //flowChart: true,//开启流程图支持,默认关闭
            //sequenceDiagram: true,//开启时序/序列图支持,默认关闭,
            //启动本地图片上传功能
            imageUpload: true,
            // imageFormats: ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
            // imageUploadURL: "{{url_for('.upload')}}",
            toolbarIcons: function () {
                // Or return editormd.toolbarModes[name]; // full, simple, mini
                // Using "||" set icons align right.
                // ["undo", "redo", "|", "bold", "del", "italic", "quote", "|", "h1", "h2", "h3", "h4",
                //     "h5", "h6", "|", "list-ul", "list-ol", "hr", "|", "link", "reference-link", "image",
                //     "code", "code-block", "table", "datetime", "html-entities", "goto-line", "search", "||",
                //     "fullscreen", "watch", "preview", "info", "|", "publish"]
                return ["bold", "del", "italic", "quote", "|", "h2", "h3", "h4",
                    "|", "list-ul", "list-ol", "hr", "|", "link", "reference-link", "image",
                    "code", "code-block", "table", "|", "watch", "fullscreen"]
            },
            imagehosting: function () {
                imagehosting('editor-img');
            },
            imagehosting_callback:function(obj) {

            }
        });

        function save(state) {
            $.ajax({
                url: '{{url_for("admin.write")}}',
                type: "post",
                data: $("form").serialize(),
                dataType: 'json',
                success: function (res) {
                    if (res.code == 1) {
                        if (res.id) {
                            $('#id').val(res.id);
                        }
                        toastr.success(res.msg);
                    } else {
                        toastr.error(res.msg);
                    }
                },
                fail: function (res) {
                    toastr.error('网络错误');
                }
            })
        }

        //注册保存按钮点击事件
        $('#form').on('submit', function (ev) {
            ev.preventDefault();
            // var content = $('#hidden-body').val();
            // var summary = content.replace(/#*.*#/g, '').replace(/[^a-z0-9\u4e00-\u9fa5]/, '').substring(0, 200) // 除去标题部分,截取200个字用来显示
            // $('#summary').val(summary);
            save(1);
        });

        setInterval(save,1000 * 15,1);

        //注册草稿按钮
        $('#draft').click(function (e) {
            $('#state').val(0);
            $('#form').submit();
        });

        //注册发布按钮点击
        $('#publish').click(function (e) {
            $('#state').val(1);
            $('#form').submit();
        });

        //上传图片
        $('#thumbnail-upload-btn').click(function (e) {
            imagehosting('thumbnail-img');
        });

        //固定路径编辑
        $('#baseURL').html(window.location.protocol + "//" + window.location.host + '/');
    })
</script>
{% endblock %}
{% block content %}
<div class="content-wrapper" style="min-height: 600px;">
    <section class="content-header">
        <h3>撰写</h3>
    </section>
    <section class="content">
        <form id="form" method="POST">
            {{ form.hidden_tag() }}
            <div class="row">
                <div class="col-lg-9 col-md-9">
                    <div class="input-group mb-3">
                        <div class="input-group-prepend">
                            <span class="input-group-text" id="inputGroup-sizing-default">标题</span>
                        </div>
                        {{form.title(class='form-control')}}
                    </div>
                    <div class="mb-2 ml-2">
                        网址:<span
                            id="baseURL">http://www.h3blog.com/</span>article/{{form.name(class='border-top-0 border-left-0 border-right-0 input-small w-50')}}
                    </div>
                    <div class="write-article">
                        <div id="write-editor">
                            {{form.content(id='hidden-body',class='hidden')}}
                        </div>
                    </div>
                    <div class="mt-2">
                        <div class="form-group">
                            <label for="summary">文章简介</label>
                            {{form.summary(id='summary',class='form-control')}}
                        </div>
                    </div>
                </div>
                <div class="col-lg-3 col-md-3">
                    <div id="saveBtns" class="card">
                        <div class="card-body">
                            {{form.save(id='save',class="btn btn-primary btn-sm")}}
                            <button id='draft' type="button" class="btn btn-secondary btn-sm">草稿</button>
                            <button id='publish' type="button" class="btn btn-success btn-sm">发布</button>
                        </div>
                    </div>
                    <!--save btn end-->

                    <div id="category" class="card mt-3">
                        <h6 class="card-title p-2 border-bottom">分类</h6>
                        <div class="card-body">
                            {{form.category_id(id='category_id',class='form-control')}}
                        </div>
                    </div>
                    <!--category end-->
                    <div id="tags" class="card mt-3">
                        <h6 class="card-title p-2 border-bottom">标签</h6>
                        <div class="card-body">
                            {{form.tags(class='form-control')}}
                        </div>
                    </div>
                    <!--tags end-->
                    <div id="thumbnail-div" class="card mt-3">
                        <h6 class="card-title p-2 border-bottom">缩略图</h6>
                        <div class="card-body">
                            <img id="thumbnail-img"
                                src="{{form.thumbnail.data if form.thumbnail.data else url_for('static',filename='img/thumbnail.jpg')}}"
                                class="card-img-top" alt="缩略图">
                            <div class="mt-2">
                                <button id="thumbnail-upload-btn" type="button"
                                    class="btn btn-outline-secondary btn-sm">选择图片</button>
                            </div>
                        </div>
                    </div>
                    <!--thumbnail end-->
                    <div id="publish-time" class="card mt-3">
                        <h6 class="card-title p-2 border-bottom">发布时间</h6>
                        <div class="card-body">
                            {{form.timestamp(type='datetime',class='form-control')}}
                        </div>
                    </div><!--publish time end-->
                </div>
            </div>
        </form>

    </section>
</div>
{% endblock %}

后台代码业务处理

后台保存代码

@admin.route('/article/write', methods=['GET','POST'])
@login_required
@admin_required
def write():
    form = ArticleForm()
    if form.validate_on_submit():
        # --------以下功能是增加文章的分类
        cty = Category.query.get(int(form.category_id.data))
        a = None
        if form.id.data:
            a = Article.query.get(int(form.id.data))
        if a :
            a.title = form.title.data.strip()
            a.content = form.content.data
            a.content_html = a.content_to_html()
            a.summary = form.summary.data
            a.thumbnail = form.thumbnail.data
            a.category = cty
            a.name = form.name.data.strip()
            a.state = form.state.data
            a.timestamp = form.timestamp.data
            if not a.name and len(a.name) == 0 :
                a.name = a.id
            db.session.commit()
        else:
            # --------以下功能是将文章信息插入数据库
            a = Article(title=form.title.data.strip(), content=form.content.data,
                        thumbnail = form.thumbnail.data,name = form.name.data.strip(),
                        state = form.state.data,summary = form.summary.data,
                        category=cty, author=current_user._get_current_object())
            a.content_html = a.content_to_html()
            db.session.add(a)
            db.session.commit()
            if not a.name and len(a.name) == 0 :
                a.name = a.id
                db.session.commit()
        # --------以下功能是将文章标识插入数据库
        a.tags = []
        for tg in form.tags.data.split(','):
            if tg.strip() == '':
                continue
            t = Tag.query.filter_by(name=tg.strip()).first()
            if not t:
                t = Tag(name=tg.strip())
                db.session.add(t)
            if t not in a.tags :
                a.tags.append(t)
        if isAjax() :
            msg = '发布成功' if int(form.state.data) == 1 else '保存成功' 
            return jsonify({'code':1,'msg':msg,'id':a.id})

    return render_template('admin/write.html', form=form)

Article类的实现

class Article(db.Model):
    __tablename__ = 'article'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(120), index=True)
    name = db.Column(db.String(64),index=True,unique=True)
    content = db.Column(db.Text)
    content_html = db.Column(db.Text)
    summary = db.Column(db.String(300))
    thumbnail = db.Column(db.String(200))
    state = db.Column(db.Integer,default=0)
    vc = db.Column(db.Integer,default=0)
    timestamp = db.Column(db.DateTime, index=True, default=datetime.now)
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
    tags = db.relationship('Tag',secondary=article_tag,backref=db.backref('articles',lazy='dynamic'),lazy='dynamic')

    def content_to_html(self):
        return markdown.markdown(self.content, extensions=[
            'markdown.extensions.extra',
            'markdown.extensions.codehilite',
            ])

    @property
    def author(self):
        """返回作者对象"""
        return User.query.get(self.author_id)

    @property
    def category(self):
        """返回文章分类对象"""
        return Category.query.get(self.category_id)

    @property
    def category_name(self):
        """返回文章分类名称,主要是为了使用 flask-wtf 的 obj 返回对象的功能"""
        return Category.query.get(self.category_id).name

    @property
    def previous(self):
        """用于分页显示的上一页"""
        a = self.query.filter(Article.state==1,Article.id < self.id). \
            order_by(Article.timestamp.desc()).first()
        return a

    @property
    def next(self):
        """用于分页显示的下一页"""
        a = self.query.filter(Article.state==1,Article.id > self.id). \
            order_by(Article.timestamp.asc()).first()
        return a

    @property
    def tag_names(self):
        """返回文章的标签的字符串,用英文‘, ’分隔,主要用于修改文章功能"""
        tags = []
        for tag in self.tags:
            tags.append(tag.name)
        return ', '.join(tags)

    @property
    def thread_key(self): # 用于评论插件
        return hashlib.new(name='md5', string=str(self.id)).hexdigest()

    def __repr__(self):
        return '<Title %r>' % self.title