完成的功能
- 阅览文章
- 有作者信息栏
- 根据当前用户判断是否可修改文章
- 修改并保存文章
依赖的第三方工具
- 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
};