Web实战之文章阅览与编辑

完成的功能

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

依赖的第三方工具

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

前言

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

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

页面代码

主页面代码

html <!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>

阅览模板

html<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>



编辑模板

html
<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

javascript/**
 * 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一样(如下)

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

一次加载全部directive

Components

javascript/**
 * 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

javascript/**
 * 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();

后端代码

javascript/**
 *
 * 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
};

    原文作者:suemi
    原文地址: https://segmentfault.com/a/1190000002433453
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞