定制网站用Python基础知识实现了一个在线的markdown编辑工具、基于Editor.md、Flask、Flask_SQLAlchemy、sm.ms

在线markdown平台搭建

文章目录

前言

定制网站我打算把我的域名用于了,定制网站网站后面可能访问就不太行了

定制网站所谓天下代码一大抄,定制网站抄来抄去有提高,定制网站用来描述编程再合适不过了,定制网站今天我也抄了一波。定制网站我通过开源+定制网站借鉴的方式,定制网站自己搞了一个在线的markdown编辑器,定制网站没错这篇文章就是在上面写的。

话不多说,先上图,定制网站下面就是我抄的成果:


目的

定制网站我之前一直都是使用vscode定制网站敲各种代码的,定制网站我非常喜欢这个工具,定制网站主要是颜值把住了我,定制网站其次通过插件可以支持定制网站非常多的语言,定制网站通用性非常高,定制网站上一个被我这么宠幸的IDE还是eclipse

定制网站我写文章使用的是markdown,定制网站之前也用过富文本编辑器,相比于markdown,定制网站富文本编辑器更多样,定制网站这是优势也是劣势。定制网站主要的缺点是写出来的定制网站文章比较花哨(对我来说,定制网站有很多读者都喜欢这种),而且非常容易造成自己写的文章格式风格不统一。

我一直用vscode编写markdownMarkdown All in One这个插件非常的神器,基本上能用到的功能都有涉及。

问题在于代码的同步,最初都是用Gitee,因为GitHub老是打不开。我这人有一个毛病,不喜欢同步代码,这就导致家里和公司的代码出现了不匹配,很烦。

当然,代码同步只是一个方面,最主要的是,如果在公司打开一个黑乎乎的vscode很引人注意(我的岗位不需要敲代码),这就有了划水的嫌疑。

另外呢,我买的还有两台服务器,域名也收藏了好多,正好用上。其实用vscode连接远端服务器也蛮好的,但是问题还是在工位上打开vscode不合适~~
(把vscode改成light主题??哈哈)

话说我还买了好几个中文域名,太费钱了

需求

为了解决我遇到的困扰,我收集了一下我的主要矛盾:

  1. 代码自动同步;
  2. 界面简洁低调;
  3. 良好的markdown编辑体验

我目前了解到的、喜欢的开源在线编辑工具主要有两个:

Editor.md是一个网页版的markdown编辑器,界面风格非常简洁,Demo也非常丰富,也是本文的选择。遗憾的是代码库停止更新了。

CKEditor是一个富文本编辑器,就能力上来说,更强,但是是一个富文本编辑器,虽然支持markdown,对我来说有那么一奈奈的功能过剩。

这两款编辑工具都非常优秀,我非常喜欢,只恨自己不是开发者~~

设计

原计划只做一个页面,其他功能以弹窗的方式实现,但是Editorbootstrap等前端有冲突,自己前端水平有限,做不出好看的界面,就能简则简。

前端页面设计

页面包括三个:

  1. 登录/注册页,登录注册二合一;
  2. 文章列表页,展示编辑过的文章;
  3. 编辑页面,使用Editor.md实现;

后端框架选择

所谓,人生苦短,我用Python,顺理成章的就选择了Flask作为后端框架。

框架

简单介绍一下FlaskPython服务器开发的流行框架,非常的轻量,同时插件很丰富,文档也齐全,有兴趣的童鞋可以访问,或则访问我之前写的文章,文章写的比较粗,但是基本的注意事项都提到了。

数据库选择

是常用的单机数据库解决方案,完全能够满足我当前的需求,就不折腾MySQL了。也非常推荐简单玩玩的童鞋使用,MySQL如果不是老鸟,太难了~💔

我之前的文章使用的是MySQL,详细介绍了如何连接数据库,使用起来都差不多。

连接数据库的工具是,SQLAlchemy是一个ORMObject Relational Mapper)框架,简单来讲,就是可以在不写sql的情况下完成各种数据库操作。

图床sm.ms

因为贫穷,只能使用免费的图床平台,这里我用的是。

市面上有很多图床可以选择,一般都有免费空间赠送,sm.ms5GB的免费空间,支持API上传,不过访问速度一般,可能因为我是白嫖的。

关键是不需要注册就能使用,直接上传图片就可以获得链接。

实现

下面是抄袭教程:

数据库设计

数据库使用flask-sqlalchemy连接,详细操作在中都有讲解。

下面的代码涉及了flask-sqlalchemy的使用方法、flask-cli命令行的使用。

可以简单的使用flask db-initflask db-rebuild等命令操作数据库。

话不多说,上代码:

# db.pyfrom email.policy import defaultfrom flask_sqlalchemy import SQLAlchemyimport sqlite3import clickfrom flask.cli import with_appcontextfrom datetime import datetimefrom werkzeug.security import generate_password_hash, check_password_hashfrom flask_login import UserMixindb = SQLAlchemy()def addUser(u):    if isinstance(u, User):        db.session.add(u)        db.session.commit()def updatePost(p):    if isinstance(p, Post):        db.session.add(p)        db.session.commit()def init_app(app):    db.init_app(app)    app.cli.add_command(init_db_command)    app.cli.add_command(reb_db_command)    app.cli.add_command(del_db_command)def init_data():    admin = User(username='admin', password='996996', email='666@163.com')    db.session.add(admin)    db.session.flush()    db.session.commit()    post = Post(title='第一篇文章', html='# 第一篇文章', markdown='# 第一篇文章')    post.author_id = admin.id    db.session.add(post)    db.session.commit()    anonym = User(username='anonym', password='996996', email='666@666.com')    db.session.add(anonym)    db.session.commit()def init_db():    db.create_all()    init_data()def del_db():    db.drop_all()@click.command('db-rebuild')@with_appcontextdef reb_db_command():    del_db()    init_db()    click.echo('Rebuild the database.')@click.command('db-clean')@with_appcontextdef del_db_command():    del_db()    click.echo('Cleared the database.')@click.command('db-init')@with_appcontextdef init_db_command():    init_db()    click.echo('Initialized the database.')class ShareField(object):    created = db.Column(        db.DateTime, nullable=False, default=datetime.utcnow)    updated = db.Column(db.DateTime, onupdate=datetime.utcnow)    status = db.Column(db.Integer, default=0)class User(db.Model, ShareField, UserMixin):    __tablename__ = 't_users'    id = db.Column(db.Integer, primary_key=True)    username = db.Column(db.String(80),  nullable=False)    _password = db.Column(db.String(128),  nullable=False)    email = db.Column(db.String(120), unique=True, nullable=False)    posts = db.relationship('Post', backref='author', lazy=True)    def __init__(self, username, password, email):        self.username = username        self.password = password        self.email = email    # getter    @property    def password(self):        return self._password    # setter    @password.setter    def password(self, raw_password):        self._password = generate_password_hash(raw_password)  # 加密    # check    def check_password(self, raw_password):        result = check_password_hash(self.password, raw_password)        return resultclass Post(db.Model, ShareField):    __tablename__ = 't_posts'    id = db.Column(db.Integer, primary_key=True)    title = db.Column(db.String(64), nullable=False, default='')    html = db.Column(db.String(30000), nullable=False, default='')    markdown = db.Column(db.String(30000), nullable=False, default='')    author_id = db.Column(db.Integer, db.ForeignKey(        't_users.id'), nullable=False)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121

登录注册

首先,去csdn上搜个登录注册页面源代码!

我选择的是

稍微改改里面的form代码,以下仅供参考:

<div class="container__form container--signup">      <form action="{{url_for('auth.register')}}" method="post" class="form" id="form1">        <h2 class="form__title">Sign Up</h2>        <input type="text" name="username" placeholder="UserName" class="input" />        <input type="email" name="email" placeholder="Email" class="input" />        <input type="password" name="password" placeholder="Password" class="input" />        <input type="submit" class="btn" value="Sign Up"></input>      </form>    </div>    <!-- Sign In -->    <div class="container__form container--signin">      <form action="{{url_for('auth.login')}}" method="post" class="form" id="form2">        <h2 class="form__title">Sign In</h2>        <input type="email" name="email" placeholder="Email" class="input" />        <input type="password" name="password" placeholder="Password" class="input" />        <a href="#" class="link">Forgot your password?</a>        <input type="submit" class="btn" value="Sign In"></input>      </form>    </div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

后端使用flask-login插件完成登录,如果不会用这个插件的,可以访问我之前的文章。

@bp.route('/register', methods=['POST', 'GET'])def register():    if request.method == 'POST':        email = request.form.get('email', '')        username = request.form.get('username', '')        password = request.form.get('password', '')        if email == '' or username == '' or password == '':            flash('注册信息不完整')            return {'msg': '注册信息不完整'}, 201        user = User.query.filter_by(email=email).first()        if user:            flash('邮箱已注册')            return {'msg': '邮箱已注册'}, 201                user = User(email=email,username=username,password=password)        addUser(user) #插入数据库        return redirect(url_for('auth.login'))    return render_template('sigh.html')@bp.route('/login', methods=['POST', 'GET'])def login():    if request.method == 'POST':        email = request.form.get('email', '')        password = request.form.get('password', '')        print(email, password)        if email == '' or password == '':            flash('登录信息不完整')            return {'msg': '登录信息不完整'}, 201        user = User.query.filter_by(email=email).first()        if not user:            flash('用户不存在')            return {'msg': '用户不存在'}, 404        if not user.check_password(password):            flash('密码错误')            return {'msg': '密码错误'}, 201        login_user(user)        return redirect(url_for('post.all'))    return render_template('sign.html')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

以上代码写的非常粗糙,基本上没有异常的处理,而且登录失败没有页面跳转😫,可以稍微改下。

文章编辑

主要是Editor.md的引入,同样的,里面也都有,我这里直接上代码:

index.html{% extends 'base.html' %}{% block style %}{{super()}}<link rel="stylesheet" href="{{ url_for('static',filename='css/editormd.css')}}" />{% endblock %}{% block content %}<div class="main_content">    <div class="center_content">        <div class="btn-group">            <button id="show-btn">Show editor</button>            <button id="hide-btn">Hide editor</button>            <button id="get-md-btn">Get Markdown</button>            <button id="get-html-btn">Get HTML</button>            <button id="show-toolbar-btn">Show toolbar</button>            <button id="close-toolbar-btn">Hide toolbar</button>            {% if current_user.is_authenticated %}            <a class="link-btn" href="{{url_for('auth.logout')}}">Quit</a>            <a class="link-btn" href="{{url_for('post.all')}}">Post List</a>            {% else %}            <a class="link-btn" href="{{url_for('auth.register')}}">Sign Up</a>            <a class="link-btn" href="{{url_for('auth.login')}}">Sign In</a>            {%endif%}        </div>        <input id="title" name="title" type="text" value="{{target.title}}" style="width: 100%;" placeholder="请输入文章标题">    </div>    <div id="test-editormd">        <textarea style="display: none;">{% if target %}{{ target.markdown}}{% else %}{% endif %}</textarea>    </div></div>{% endblock %}{% block script %}{{super()}}<script src="{{ url_for('static',filename='js/editormd.js')}}"></script><script type="text/javascript">    function debounce(func, wait, immediate) {        let timeout        return function (...args) {            clearTimeout(timeout)            timeout = setTimeout(() => {                timeout = null                if (!immediate) func.apply(this, args)            }, wait)            if (immediate && !timeout) func.apply(this, [...args])        }    };    function update() {        title = $('#title').val();        html = testEditor.getHTML();        mark = testEditor.getMarkdown();        data = {            title: title,            html: html,            markdown: mark        }        $.ajax({            url: '{{url_for("post.edit",id=target.id)}}',            data: JSON.stringify(data),            method: 'post',            dataType: 'json',            contentType: 'application/json',            success: function (data) {                console.log(data.msg);            }        });    }    $('#title').on('input', debounce(update, 3000, false));    var testEditor;    $(function () {        // $.get('test.md', function (md) {        testEditor = editormd("test-editormd", {            width: "90%",            height: 740,            path: '{{url_for("static",filename="editor.md/lib/")}}',            // theme: "dark",            // previewTheme: "dark",            // editorTheme: "pastel-on-dark",            // markdown: "{% if target %}{{ target.markdown.replace('','\')}}{% else %}{% endif %}",            codeFold: true,            //syncScrolling : false,            saveHTMLToTextarea: true,    // 保存 HTML 到 Textarea            searchReplace: true,            //watch : false,                // 关闭实时预览            htmlDecode: "style,script,iframe|on*",            // 开启 HTML 标签解析,为了安全性,默认不开启                //toolbar  : false,             //关闭工具栏            //previewCodeHighlight : false, // 关闭预览 HTML 的代码块高亮,默认开启            emoji: true,            taskList: true,            tocm: true,         // Using [TOCM]            tex: true,                   // 开启科学公式TeX语言支持,默认关闭            flowChart: true,             // 开启流程图支持,默认关闭            sequenceDiagram: true,       // 开启时序/序列图支持,默认关闭,            //dialogLockScreen : false,   // 设置弹出层对话框不锁屏,全局通用,默认为true            //dialogShowMask : false,     // 设置弹出层对话框显示透明遮罩层,全局通用,默认为true            //dialogDraggable : false,    // 设置弹出层对话框不可拖动,全局通用,默认为true            //dialogMaskOpacity : 0.4,    // 设置透明遮罩层的透明度,全局通用,默认值为0.1            //dialogMaskBgColor : "#000", // 设置透明遮罩层的背景颜色,全局通用,默认为#fff            imageUpload: true,            imageFormats: ["jpg", "jpeg", "gif", "png", "bmp", "webp"],            imageUploadURL: "{{url_for('post.upload')}}",            onload: function () {                console.log('onload', this);                //this.fullscreen();                //this.unwatch();                //this.watch().fullscreen();                //this.setMarkdown("#PHP");                //this.width("100%");                //this.height(480);                //this.resize("100%", 640);            },            onchange: debounce(update, 3000, false),        });        // });        $("#goto-line-btn").bind("click", function () {            testEditor.gotoLine(90);        });        $("#show-btn").bind('click', function () {            testEditor.show();        });        $("#hide-btn").bind('click', function () {            testEditor.hide();        });        $("#get-md-btn").bind('click', function () {            alert(testEditor.getMarkdown());        });        $("#get-html-btn").bind('click', function () {            alert(testEditor.getHTML());        });        $("#watch-btn").bind('click', function () {            testEditor.watch();        });        $("#unwatch-btn").bind('click', function () {            testEditor.unwatch();        });        $("#preview-btn").bind('click', function () {            testEditor.previewing();        });        $("#fullscreen-btn").bind('click', function () {            testEditor.fullscreen();        });        $("#show-toolbar-btn").bind('click', function () {            testEditor.showToolbar();        });        $("#close-toolbar-btn").bind('click', function () {            testEditor.hideToolbar();        });        $("#toc-menu-btn").click(function () {            testEditor.config({                tocDropdown: true,                tocTitle: "目录 Table of Contents",            });        });        $("#toc-default-btn").click(function () {            testEditor.config("tocDropdown", false);        });    });</script>{% endblock %}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182

简单解释一下,下载Editor.md压缩包,解压后放在static文件夹下面,重命名为editor.md,页面中的cssjs文件都可以直接抄editor.md/expample/full.html的引用方式,然后换成jinja的格式就可以了。

需要注意的是:

  1. 自动保存
    自动保存功能使用onchange实现,Editor.md留的有接口,我在这里使用了一个防抖动的技术,说白了就是在文章修改后的第一时间不上传,而是等停止改动后3秒再上传,这样可以有效的降低服务器压力。

  2. 图片上传
    图片上传使用imageUploadURL指定上传路径。我这里没有把图片保存在自己的服务器,而是转手把图片上传到了sm.ms,下面会有详细的实现代码。

文章保存后端代码

@bp.route('/edit/<int:id>', methods=['POST', 'GET'])@login_requireddef edit(id=0):    target = Post.query.filter_by(id=id).first()    if not target:        return {'msg': '服务器没有查询到当前文章的信息!'}, 404    if request.method == 'POST':        data = request.json        target.title = data['title']        target.html = data['html']        target.markdown = data['markdown']        print(target.html, target.markdown)        updatePost(target)        return {'msg': 'success'}, 200    return render_template('index.html', target=target)@bp.route('/all')@login_requireddef all():    post_list = current_user.posts    return render_template('posts.html', post_list=post_list)@bp.route('/upload', methods=['POST'])@login_requireddef upload():    img = request.files.get('editormd-image-file')    if not img:        return {'success': 0, 'message': 'null'}    headers = {'Authorization': '这里需要写自己的授权码'}    files = {'smfile': img}    url = 'https://sm.ms/api/v2/upload'    res = requests.post(url, files=files, headers=headers).text    import json    js = json.loads(res)    if js.get('success') == True:        url = js.get('data').get('url')    else:        url = js.get('images')    msg = js.get('message')    return {'success': 1, 'message': msg, 'url': url}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

edit方法用于更新文章,upload方法用于图片上传。

可以看到代码写的非常脆弱,大佬不要嘲讽我~~

图片上传里面有一个需要注意的地方,就是headers变量中的Authorization值。
这个值需要自己注册sm.ms才能获得,获得方法如下图:

文章列表

列表页面也是我自己写的唯一一个页面,主要没得抄,当然也是非常简单的。

功能就是展示所有的文章,也没有使用分页功能。

后端代码就是上面代码的all()函数,这里不再重复,简单贴一下前端代码:

posts.html{% extends 'base.html' %}{% block style %}{{super()}}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">{% endblock %}{% block content %}<div class="main_content">    <div class="center_content">        <div>            <span style="font-size: 18px;font-weight: bold ;">文章列表</span>            {% if current_user.is_authenticated %}            <a class="link-btn" href="{{url_for('auth.logout')}}">Quit</a>            {% else %}            <a class="link-btn" href="{{url_for('auth.register')}}">Sign Up</a>            <a class="link-btn" href="{{url_for('auth.login')}}">Sign In</a>            {%endif%}        </div>        <br>        <ul class="post_list">            <li><a href="{{url_for('post.add')}}" style="color: rgb(45, 141, 128);"><i class="bi bi-plus-circle"></i>                    Create a new                    post</a></li>            {% if post_list %}            {% for post in post_list | reverse %}            <li>                <a href="{{url_for('post.edit',id=post.id)}}"> {{ post.created.strftime('%Y-%m-%d %H:%M:%S')}}《{{                    post.title}}》 </a>            </li>            {% endfor %}            {% endif %}        </ul>    </div></div>{% endblock %}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

这里没啥好解释的,还是那句话,如果有兴趣可以看我之前的文章。

欢迎大家留言讨论,这点我还算熟悉~~~

还有图标呢~~~
😍



网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发