Web实战之文章阅览与编辑


完成的功能

  • 阅览文章
  • 有作者信息栏
  • 根据当前用户判断是否可修改文章
  • 修改并保存文章

依赖的第三方工具

  • vue.js
  • SuMarkdown
  • jquery
  • bootstrap-taginput

前言

我们先来分析一下这个功能的实现,其实这个过程中是需要很多数据的——作者的数据,文章的数据,当前用户的数据。如果分两个页面来做,显然许多异步数据会被重复加载,所以我决定把这一部分做成一个极小的单页面应用。

同时,在这一次的博客里,用到了许多Vue的重要特性,比如component,directive,life-cycle,是Vue一个不错的示例。

页面代码

主页面代码

 <!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">

    <title>
        <%= title %>
    </title>
    <link rel="stylesheet" href="css/style.css">
    <link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="lib/suMarkdown/lib/highlight/styles/default.css">
    <link rel="stylesheet" href="lib/bootstrap/bootstrap-tag/bootstrap-tagsinput.css">
    <script src="lib/jquery/dist/jquery.min.js"></script>
    <script src="lib/bootstrap/dist/js/bootstrap.min.js"></script>
    <script src="lib/suMarkdown/lib/highlight/highlight.pack.js"></script>
    <script src="lib/suMarkdown/lib/marked/marked.js"></script>
    <script src="lib/bootstrap/bootstrap-tag/bootstrap-tagsinput.min.js"></script>
    <script src="lib/suMarkdown/js/suMarkdown.js"></script>
</head>
<body>
    <%include layout/left-bar.ejs%>
    <div v-component="{{currentView}}" v-ref='center'></div>
    <%include layout/alert.ejs%>
    <script src="js/blog.js"></script>
</body>

阅览模板

<style>

    .log-bar{
        position:fixed;
        top:10px;
        right:0;
        width:100%;
        z-index:999;
        padding-right:10px;
    }
    .log-bar *{
        float:right;
        margin-right:10px;
    }
    .header-bar{
        position:fixed;
        top:10px;
        right:0;
        width:100%;
        z-index:999;
        padding-right:10px;
    }
    .header-bar .btn-group{
        top:15px;
        float:right;
        margin-right:6px;
        vertical-align:middle;
    }
    .header-bar .dropdown-toggle{
        float:right;
        top:10px;
        margin-right:10px;
        position:relative;
        width:42px;
        height:42px;
        display:inline-block;
    }
    .header-bar .dropdown-toggle img{
        display:block;
        width:100%;
        height:100%;            
        border: 2px solid white;
    }
    .header-bar .dropdown-toggle .caret{
        position:absolute;
        right:-8px;
        top:20px;
    }
    .header-bar .dropdown-menu{
        left:auto;
        right:0;
        top:50px;
        position:absolute;
        z-index:1000;
        min-width:160px;
        padding:5px 5px;
        margin:2px 0 0;
        border: 1px solid #ccc;
    }
    .header-bar li{
        line-height:20px;
        margin-bottom:5px;
    }
    .header-bar li a{
        padding:3px 20px;
        clear:both;
        font-weight:normal;
    }
    .header-bar li a:hover{
        background-color:#333333;
        color:white;
    }
    .header-bar li a span{
        margin-right:8px;
    }


    .people {
        position: absolute;
        right: 70%;
        top: 80px;
        width: 160px;
        padding: 0 20px 20px 0;
        text-align: right;
    } 

    .people .author {
    margin: 0;
    font-size: 16px;
    font-weight: bold;
    line-height: 24px;
    }
    .people img{
        width: 60px;
        height: 60px;
        margin: 0 0 10px 0;
        display: inline-block;
        border: 2px solid white;
    }
    .people .about{
        color: #999999;
        font-size: 12px;
        margin:0 0 10
    }
    .people  .sns{
        margin-bottom:10px;
    }
    .sns a{
        width:20px;
        margin-left:7px;
        display:inline-block;
        color:#555555;
    }
    .sns a img{
        opacity:0.8;
        border-radius:3px;
        height:auto;
        max-width:100%;
        vertical-align:middle;
        border:0;
    }

    .container .article{
        position:relative;
        line-height:30px;
        left:25%;
        width:60%;
        padding:0 40px 30px;
    }
    .article #blogHead h1{
        font-size:50px;
        font-weight:700;
        line-height:70px;
    }
    .article #blogHead p{
        font-size:14px;
        color:#999999;
    }
    .article #blogHead span{
        margin:0 2%;
    }
    .article #blogBody{
        font-size:17px;
    }


</style>






<div class="header-bar" v-show="login" >
    <a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">
        <img class="img-circle" v-attr="src: user.avatar">
        <span class="caret"></span>
    </a>
    <ul class="dropdown-menu" role="menu">
        <li>
            <a href="#">
                <span class="glyphicon glyphicon-user"></span>
                我的主页
            </a>
            <a href="#">
                <span class="glyphicon glyphicon-hand-right"></span>
                我的小组
            </a>
            <a href="#">
                <span class="glyphicon glyphicon-cog"></span>
                我的设置
            </a>
            <a href="#">
                <span class="glyphicon glyphicon-inbox"></span>
                消息
            </a>
            <a href="#">
                <span class="glyphicon glyphicon-info-sign"></span>
                帮助
            </a>
            <a href="#">
                <span class="glyphicon glyphicon-log-out"></span>
                登出
            </a>
        </li>
    </ul>
    <div class="btn-group">
        <a href="javascript:void(0)" class="btn" v-on="click: edit()" v-show="editable">
            <span class="glyphicon glyphicon-edit"></span>
        </a> 
    </div>
</div>

<div class="log-bar" v-show="!login">
    <a href='#'>登陆</a>
    <a href='#'>注册</a>
</div>

<div class="container">
    <div class="people">
        <a class="author" href="#">
            <img class="img-circle" v-attr="src: author.avatar">
            <br>
            {{blog.author}}
        </a>
        <div class="about">
            <p>{{author.description}}</p>
        </div>
        <div class="sns">
            <a href="#">
                <img src="img/home.png">
            </a>
            <a href="#">
                <img src="img/home.png">
            </a>
            <a v-href="author.page">
                <img src="img/home.png">
            </a>
        </div>
    </div>
    <div class="article">
        <div id="blogHead">
            <h1>{{blog.title}}</h1>
            <p>
            <span>发表时间     {{blog.date | toDate }}</span>
            <span>字数:{{blog.body.length}}</span>
                <span>阅读量:30</span>
            </p>
        </div>
        <div id="blogBody">
        </div>
    </div>
</div>

编辑模板


<style>


    body{
        background-color: #f5f5f5;
    }
    .right-bar{
        float:left;
        height: 100%;
        width:100px;
        padding: 2% 5%;

    }
    .center-page{
        float: left;
        padding: 0 7% 0 7%;
        width:86%;
        vertical-align: middle;
        border-right: solid 1px;
        border-color: #cccccc;
    }
    .form-group h4{
        position: relative;
        top:-12px;
    }
    form:first-child input{
        font-size: 16px;
    }



    .alert {
        margin: 3% 5% 0;
        text-align: center;
        position: fixed;
        top: 80%;
        width:60%;
    }

</style>
<div class="container">
    <div class="center-page" >
       <form class="form-horizontal" id="writePage">
           <div class="form-group">
               <div class="col-sm-1"></div>
               <h4 class="col-sm-2 control-label">标题</h4>
               <div class="col-sm-6">
                   <input class="form-control" v-model="myBlog.title">
               </div>
           </div>
           <br>
           <div class="form-group">
               <div class="col-sm-1"></div>
               <h4 class="col-sm-2 control-label">标签</h4>
               <div class="col-sm-6">
                   <select multiple class="form-control" data-role="tagsinput" v-tags="myBlog.tags"></select>
               </div>
           </div>
           <br>
           <div class="form-group">
               <div class="col-sm-12 suMarkdown">
                <style>
                    .su-toolbar{
                        width:100%;
                        height:45px;
                        display: block;
                        background: #ffffff;
                        padding: 5px;
                        border: solid 1px;
                        border-color: #cccccc;
                    }
                    .su-toolbar .tool-block{
                        cursor: pointer;
                        display: block;
                        width:35px;
                        margin:0 1%;
                        height:35px;
                        float:left;
                        padding: 5px;
                    }
                    .su-toolbar .tool-block *{
                        left:20%;
                        top:20%;
                    }
                    .su-toolbar .tool-block:hover{
                        background: #00ffff;
                    }
                    .su-toolbar button{
                        margin-top:5px;
                    }
                    .suEditor{
                        width:50%;
                        float:left;
                        display: block;
                    }
                    .suEditor textarea{
                        width:100%;
                        height: 400px;
                        background: #ffffff;
                        tab-size: 4;
                        border:solid 1px;
                        border-top: none;
                        border-color: #cccccc;
                        padding: 20px;
                        resize: none;
                    }
                    .suEditor textarea:focus{
                        background: #fff;
                        border-color:#cccccc ;
                        outline: none;
                    }
                    .suPreview{
                        width:50%;
                        left:50%;
                        float:left;
                        background: #ffffff;
                        height: 400px;
                        display: block;
                        overflow: auto;
                        padding: 0 20px;
                        border-right: solid 1px;
                        border-bottom: solid 1px;
                        border-color: #cccccc;
                    }
                    .suProgress{
                        width: 100%;
                    }
                </style>
                <div class="suProgress progress">
                    <div class="progress-bar su-progress-bar" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100">
                        0%
                    </div>
                </div>
                <div class="su-toolbar">
                    <div class="tool-block su-tool-bold" title="加粗(Ctrl+B)" data-placement="top" data-toggle="tooltip">
                        <span class="glyphicon glyphicon-bold"></span>
                    </div>
                    <div class="tool-block su-tool-italic" title="斜体(Ctrl+I)" data-placement="top" data-toggle="tooltip">
                        <span class="glyphicon glyphicon-italic"></span>
                    </div>
                    <div class="tool-block su-tool-head" title="标题(Ctrl+H)" data-placement="top" data-toggle="tooltip">
                        <span class="glyphicon glyphicon-header"></span>
                    </div>
                    <div class="tool-block su-tool-link" title="链接(Ctrl+L)" data-placement="top" data-toggle="tooltip">
                        <span class="glyphicon glyphicon-link"></span>
                    </div>
                    <div class="tool-block su-tool-img" title="图片(Ctrl+G)" data-placement="top" data-toggle="tooltip">
                        <span class="glyphicon glyphicon-picture"></span>
                    </div>
                    <div class="tool-block su-tool-list" title="无序列表(Ctrl+U)" data-placement="top" data-toggle="tooltip">
                        <span class="glyphicon glyphicon-list"></span>
                    </div>
                    <div class="tool-block su-tool-orderlist" title="有序列表(Ctrl+O)" data-placement="top" data-toggle="tooltip">
                        <span class="glyphicon glyphicon-th-list"></span>
                    </div>
                    <div class="tool-block su-tool-code" title="单行代码(Ctrl+K)" data-placement="top" data-toggle="tooltip">
                        <span class="glyphicon glyphicon-asterisk"></span>
                    </div>
                    <div class="tool-block su-tool-quote" title="引用(Ctrl+Q)" data-placement="top" data-toggle="tooltip">
                        <span class="glyphicon glyphicon-comment"></span>
                    </div>
                    <div class="tool-block su-tool-plus" title="文件上传" data-placement="top" data-toggle="tooltip">
                        <span class="glyphicon glyphicon-upload"></span>
                    </div>
                    <input type="file" style="display: none" class="su-tool-upload" multiple>
                    <a class="tool-block su-tool-help" href="http://lab.lepture.com/editor/markdown" title="帮助" data-placement="top" data-toggle="tooltip">
                        <span class="glyphicon glyphicon-info-sign"></span>
                    </a>
                    <div class="tool-block pull-right su-tool-fullscreen" title="全屏" data-placement="top" data-toggle="tooltip">
                        <span class="glyphicon glyphicon-fullscreen"></span>
                    </div>

                    <div class="tool-block pull-right su-tool-preview" title="预览" data-placement="top" data-toggle="tooltip">
                        <span class="glyphicon glyphicon-eye-open"></span>
                    </div>
                </div>
                <div class="suEditor">
                    <textarea v-model="myBlog.body"></textarea>
                </div>
                <div class="suPreview">
                </div>
               </div>
           </div>
        </form>
        <button class="btn btn-primary btn-lg pull-left" id="submit" v-on="click: submit()">
            发表
        </button>
        <button class="btn btn-primary btn-lg pull-right" id="submit" v-on="click: cancel()">
            取消
        </button>
    </div>
    <div class="right-bar" >
        <img class="img-circle" style="width: 90px;height: 90px;" v-attr="src: author.avatar">
    </div>
</div>

前端代码

Directive

/**
 * Created by suemi on 14-12-13.
 */
module.exports={
  tag:function(Vue){
       Vue.directive('tags',{
          twoWay:true,
          bind: function () {
              var self=this;
              console.log(self);
              $(self.el).on('itemAdded',function(){
                  scope.blog.tags=$(this).val();
              });
              $(self.el).on('itemRemoved',function(){
                  scope.blog.tags=$(this).val();
              });
          },
          update:function(){},
          unbind:function(){
              $(this.el).off();
          }
      });
      return module.exports;
  },
  all:function(Vue){
      for(var i in module.exports){
          if(i==='all') return module.exports;
          else module.exports[i](Vue);
      }
      return module.exports;
  }
};

让所有函数返回module.exports可以支持漂亮的链调的写法,同时还写了all函数以实现像angular一样(如下)

angular.module('demo',['app.directives']);

一次加载全部directive

Components

/**
 * Created by suemi on 14-12-13.
 */
module.exports={
    readBlog:function(Vue){
        var scope=Vue.component('readBlog',{
            inherit:true,
            template:require('../templates/readBlog.html'),
            ready:function(){
                $('#blogBody').html(marked(this.blog.body));
                $('pre code').each(function(i,block){
                       hljs.highlightBlock(block);
                });
            },
            methods:{
                edit:function(){
                    console.log(this);
                    this.currentView='editBlog';
                }
            },
            filters:{
                toDate:function(date){
                    var tmp=new Date(date);
                    return tmp.getFullYear()+'-'+tmp.getMonth()+'-'+tmp.getDate()+' '+tmp.toTimeString().split(' ')[0];
                }
            }
        });
        return module.exports;
    },
    editBlog:function(Vue){
      var scope=Vue.component('editBlog',{
          inherit:true,
          template:require('../templates/editBlog.html'),
          ready:function(){
            console.log(this.$el);
            $.extend(this.myBlog,this.blog);
            //复制原文标签
            $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]",$(this.$el)).tagsinput();
            var tmp=[];
            $.extend(tmp,this.blog.tags);
            for(var i=0;i<tmp.length;i++) $('select').tagsinput('add',tmp[i]);

            SuMarkdown({
                preview:true,
                upload:'/upload'
            });
            $('.suPreview').html(marked(this.myBlog.body));
            $('pre code').each(function(){hljs.highlightBlock(this);});
          },
          data:function(){
              return {
                  myBlog:{
                    title:'',
                    body:'',
                    tags:[],
                    author:''
                  }
              };
          },
          methods:{
            save:function(){
                var tmp=window.location.href.split('/');
                $.post(window.location.href,scope.myBlog).done(function(data){
                    if(data.success) {
                        $.extend(scope.blog,scope.myBlog);
                        scope.content=marked(scope.blog.body);
                    }
                    else{
                        if(!data.err.message) return;
                        scope.msg=data.err.messgae;
                        scope.display=true;
                    }
                }).fail(function(){
                    scope.msg='未知错误,请重试';
                    scope.display=true;
                });
            },
            cancel:function(){
                this.currentView='readBlog';
            }
          }
      });
    },
    all:function(Vue){
        for(var i in module.exports){
            if(i==='all') return module.exports;
            else module.exports[i](Vue);
        }
        return module.exports;
    }
};

main.js

/**
 * Created by suemi on 14-12-13.
 */
hljs.initHighlightingOnLoad();
var Vue=require('vue');
//Vue.config.debug=true;
require('./directives.js').tag(Vue);
require('./components.js').readBlog(Vue).editBlog(Vue);



scope=new Vue({
    el:'body',
    data:{
        user:{
            name:'',
            avatar:''
        },
        author:{
            avatar:''
            description:'',
            page:''
        },
        blog:{
            title:'',
            tags:[],
            body:'',
            author:'',
            date:undefined,
            content:''//html of the blog
        },
        msg:'',
        display:false,
        currentView:'readBlog',
        editable:true,
        login:false
    },
    methods:{
        getAuthor:function(){
            var tmp=window.location.href.split('/');
            $.get('/getBlog/'+tmp.pop()).done(function(data){
                if(data.success){
                    $.extend(scope.user,data.user);
                    $.extend(scope.author,data.author);
                    $.extend(scope.blog,data.blog);
                    $('#blogBody').html(marked(scope.blog.body));
                    $('pre code').each(function(){hljs.highlightBlock(this);});
                    //scope.blog.content=marked(scope.blog.body);
                    if(scope.user.name!=='') scope.login=true;
                    if(scope.user.name!==scope.blog.author) scope.editable=false;
                }
                else window.location.href='/404';
            }).fail(function(){
                window.location.href='/404';
            });
        },
    }
});


scope.getAuthor();

后端代码

/**
 *
 * Created by suemi on 14-12-4.
 */
var Err=usf.module.tool.Err,
    EndHanler=usf.module.tool.EndHandler,
    msg=usf.module.msg,
    Then=usf.lib.then,
    User=usf.db.def.User,
    Blog=usf.db.def.Blog;

function writeBlog(req,res){
    req.session.uname='suemi';
    if(req.body.author!==req.session.uname)
        return res.json({
            success:false,
            err:new Err('用户名不一致')
        });
    (new Blog(req.body)).save(function(err){
       if(!err) res.json({
           success:true,
           err:null
       });
        else res.json({
           success:false,
           err: new Err('后台错误,稍后再试')
       });
    });
}

function editBlog(req,res){
    Then(function(cont){
        if(!req.session.uname) return new Err('未登录');
        Blog.findById(req.params.blogID,cont);
    }).then(function(cont,doc){
        if(!doc) return new Err(msg.BLOG.blogNone);
        if(doc.author!==req.session.uname) return new Err('权限不足');
        delete req.body._id;
        doc=tool.union(doc,req.body);
        doc.save(cont);
    }).then(function(cont){
        res.json({
            success:true,
            err:null
        });
    }).fail(EndHandler);
}

function getBlog(req,res){
    var tmp={};
    tmp.user={};
    tmp.author={};
    Then(function(cont){
        Blog.findById(req.params.blogID,cont);
    }).then(function(cont,doc){
        if(!doc) return new Err(msg.BLOG.blogNone);
        tmp.blog=doc;
        User.findOne({username:doc.author},cont);
    }).then(function(cont,doc){
        if(!doc) return new Err('数据错误');
        tmp.author.name=doc.username;
        tmp.author=tool.union(tmp.author,doc.profile);
        if(req.session.uname) User.findOne({username:req.session.uname},cont);
        else cont();
    }).then(function(cont,doc){
        if(doc){
            tmp.user.name=doc.username;
            tmp.user=tool.union(tmp.user,doc.profile);
        }
        res.json({
            success:true,
            user:tmp.user,
            author:tmp.author,
            blog:tmp.blog,
            err:null
        });
    }).fail(EndHandler);
}
module.exports= {
    writeBlog: writeBlog,
    editBlog:  editBlog,
    getBlog:   getBlog
};

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

推荐阅读更多精彩内容