基于阿里egg框架搭建博客(6)——浏览、发表文章

相关文章

基于阿里egg框架搭建博客(1)——开发准备
基于阿里egg框架搭建博客(2)——Hello World
基于阿里egg框架搭建博客(3)——注册与登录
基于阿里egg框架搭建博客(4)——权限控制
基于阿里egg框架搭建博客(5)——置顶导航条
基于阿里egg框架搭建博客(6)——浏览、发表文章
基于阿里egg框架搭建博客(7)——编辑文章

git

https://github.com/ZzzSimon/egg-example
喜欢就点个赞吧!

正文

浏览、发表文章简单来讲就是对article表的读/写操作。

Article表设计

字段说明

名称 解释
id 主键id
title 文章标题
url 文章访问path
detail 文章内容
author 作者,对应username
invisible 是否保密,保密则不显示在文章列表
create_time 文章第一次发表时间
update_time 文章最后一次修改时间

sql脚本

DROP TABLE IF EXISTS `article`;
CREATE TABLE `article` (
  `id` varchar(20) NOT NULL,
  `title` varchar(255) NOT NULL,
  `url` varchar(255) NOT NULL,
  `detail` varchar(4096) NOT NULL,
  `author` varchar(255) NOT NULL,
  `invisible` int(1) NOT NULL DEFAULT '0',
  `create_time` datetime NOT NULL,
  `update_time` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

页面设计

浏览文章


发表文章

功能设计

浏览文章

  1. 点击文章标题查看文章详细内容

发表文章

  1. 输入文章标题
  2. 选择是否保密,保密则不显示在文章列表
  3. 保存文章
  4. 支持markdown

markdown支持

对于前端md编辑器,我们选择了Editor.md

官方文档:http://pandao.github.io/editor.md/

前端代码

浏览文章

list.tpl 文章列表

我们创建app/view/article/list.tpl文件:

{% extends "parent.tpl" %}

{% block head %}
<title>文章列表</title>
{% endblock %}


{% block content %}
<ul class="article-view view">
    {% for item in list %}
    <li class="item">
        <dl>
            <dt><a href="{{ item.url }}">{{ item.title }}</a></dt>
            <dd><small>{{item.author}}</small> 最后更新于 {{helper.formatTime(item.update_time)}}</dd>
        </dl>
    </li>
    {% endfor %}
</ul>
{% endblock %}

detail.tpl 文章详情

我们创建app/view/article/detail.tpl文件:

{% extends "parent.tpl" %}

{% block head %}
<title>{{article.title}}</title>
<link rel="stylesheet" href="/public/editormd/editormd.css">
<script src="/public/editormd/lib/marked.min.js"></script>
<script src="/public/editormd/lib/prettify.min.js"></script>
<script src="/public/editormd/lib/raphael.min.js"></script>
<script src="/public/editormd/lib/underscore.min.js"></script>
<script src="/public/editormd/lib/sequence-diagram.min.js"></script>
<script src="/public/editormd/lib/flowchart.min.js"></script>
<script src="/public/editormd/lib/jquery.flowchart.min.js"></script>
<script type="text/javascript" src="/public/editormd/editormd.js"></script>
{% endblock %}


{% block content %}
<div class="page-header">
    <h1>{{article.title}} <small style="font-size: small">{{article.author}} 最后更新于 {{helper.formatTime(article.update_time)}}</small></h1>
</div>

<div id="detail" style="visibility: hidden">{{article.detail}}</div>
<div id="layout">
    <div id="test-editormd-view">

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

{%block script%}
<script type="text/javascript">
    $(function () {
        const markdown = $('#detail').text();
        var testEditormdView = editormd.markdownToHTML("test-editormd-view", {
            markdown: markdown,//+ "\r\n" + $("#append-test").text(),
            //htmlDecode      : true,       // 开启 HTML 标签解析,为了安全性,默认不开启
            htmlDecode: "style,script,iframe",  // you can filter tags decode
            //toc             : false,
            tocm: true,    // Using [TOCM]
            //tocContainer    : "#custom-toc-container", // 自定义 ToC 容器层
            //gfm             : false,
            //tocDropdown     : true,
            // markdownSourceCode : true, // 是否保留 Markdown 源码,即是否删除保存源码的 Textarea 标签
            emoji: true,
            taskList: true,
            tex: true,  // 默认不解析
            flowChart: true,  // 默认不解析
            sequenceDiagram: true,  // 默认不解析
        });

    });
</script>
{% endblock %}

此处需要注意1点:
md的内容先通过模板,渲染在一个隐藏的div中。之后,通过editormd动态渲染出来。

发表文章

article.tpl 发表文章

我们创建app/view/article/article.tpl文件:

{% extends "parent.tpl" %}

{% block head %}
<title>Markdown Editor</title>
<link rel="stylesheet" href="/public/editormd/editormd.css">
<script type="text/javascript" src="/public/editormd/editormd.js"></script>
{% endblock %}

{% block content %}

<div class="row">
    <div class="form-group">
        <label for="title">文章标题:</label>
        <input id="title" type="text" class="form-control">
    </div>
    <div class="checkbox ">
        <label>
            <input id="invisible" type="checkbox">保密(勾选后将<strong style="color: red">不显示</strong>在文章列表)
        </label>
    </div>
    <div class="form-group pull-right">
        <button id="save" class="btn btn-success ">保存</button>
    </div>
</div>
<div class="row">
    <div id="layout">
        <div id="test-editormd"></div>
    </div>
</div>
{% endblock %}


{% block script %}
<script type="text/javascript">

    let testEditor = editormd("test-editormd", {
        width: "100%",
        height: 740,
        path: '/public/editormd/lib/',
        // theme: "dark",
        // previewTheme: "dark",
        // editorTheme: "pastel-on-dark",
        // markdown: md,
        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: "/edit/uploadPic?_csrf={{ ctx.csrf | safe }}",

   /*  后端需返回:   {
            success : 0 | 1, //0表示上传失败;1表示上传成功
            message : "提示的信息",
            url     : "图片地址" //上传成功时才返回
        }*/
        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);
        }
    });

    $('#save').bind('click', function () {
        data = {
            article: {
                title: $('#title').val(),
                detail: testEditor.getMarkdown(),
                invisible: $('#invisible').prop('checked')  ? 1:0
            }
        };

        $.post('/edit/save?_csrf={{ ctx.csrf | safe }}', data, function (resp) {
            if (resp.flag === '1') {
                window.location.href = resp.url;
            }
        })
    })


</script>
{% endblock %}

此处需要注意1点:
ajax默认是不重定向的,所以当保存成功,我们需要返回文章的访问url,在回调函数里重定向。

后端代码

ArticleController

我们创建app/controller/article.js文件:

const Controller = require('egg').Controller;

class ArticleController extends Controller {
    async list() {
        const ctx = this.ctx;
        const articleList = await ctx.service.article.list();
        await ctx.render('article/list.tpl', { list: articleList });
    }

    async detail(){
        const ctx = this.ctx;
        const queryRes = await ctx.service.article.detail(ctx.params.id);
        ctx.logger.info(queryRes);
        await ctx.render('article/detail.tpl', { article: queryRes[0] });
    }
}

module.exports = ArticleController;

EditController

我们创建app\controller\edit.js文件:

const Controller = require('egg').Controller;
const fs = require('mz/fs');


class EditController extends Controller{
    async editHtm(){
        await this.ctx.render('article/edit.tpl');
    }
    async save(){
        const ctx = this.ctx;
        const article = ctx.request.body.article;
        article.id = ctx.helper.uuid();
        article.url = '/article/'+article.id+'.htm';
        article.author = ctx.session.user.username;
        const nowTime = new Date();
        article.create_time = nowTime;
        article.update_time = nowTime;
        const result = await ctx.service.article.save(article);
        if (result) {
            ctx.body = {flag:'1',msg:'保存成功',url:article.url}
        }else {
            ctx.body = {flag:'0',msg:'保存失败'}
        }
    }

    async uploadPic(){
        const { ctx } = this;
        const file = ctx.request.files[0];
        let filenameNew = ctx.helper.uuid() +'.'+  file.filename.split('.').pop();
        let filepathNew = this.config.baseDir+'\\app\\public\\mdPic\\'+filenameNew;
        //把临时文件剪切到新目录去
        await fs.rename(file.filepath, filepathNew);
        //按editormd要求格式返回
        ctx.body = {
            success : 1, //0表示上传失败;1表示上传成功
            message : "上传成功",
            url     : filepathNew.split(this.config.baseDir+'\\app')[1] //上传成功时才返回
        }
    }
}

module.exports = EditController;

此处需要注意1点:

  1. uoloadPic方法主要用于md编辑器的图片上传。

ArticleService

我们创建app/service/article.js文件:

const Service = require('egg').Service;

class ArticleService extends Service {
    async list() {
        const sql = "SELECT url,title,author,update_time FROM article WHERE invisible = 0";
        const list =await this.app.mysql.query(sql);
        return list;
    }

    async detail(id = 1){
        const sql = "SELECT title,detail,author,update_time FROM article WHERE id = ?";
        return await this.app.mysql.query(sql,[id])
    }

    async save(article = {}){
        const res = await this.app.mysql.insert('article',article);
        return res.affectedRows === 1;
    }


}

module.exports = ArticleService;

router.js

我们往 app/router.js中添加一下内容:

router.get('/edit.htm',controller.edit.editHtm);
router.get('/article/:id.htm',controller.article.detail);
router.get('/articleList.htm', controller.article.list);

router.post('/edit/save',controller.edit.save);
router.post('/edit/uploadPic',controller.edit.uploadPic);

结尾

如果看完觉得有用,请给作者一个喜欢吧!谢谢啦!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342