ThinkPHP开发博客系统笔记之二
1. 登陆验证码
当用户登陆的时候我们希望也弹出验证码,有两种方法可以实现:一是再增加一个弹出验证码的dialog,二是和注册共用一个验证码dialog。第一种方法有大量重复代码,所以我们使用第二种方法。首先,为了让javascript区分是登陆还是注册,我们给验证码表单增加一个自定义属性:form-click。
login.js <form id="verify_register" form-click=""> <ol class="ver_error"></ol> <p> <label for="verify">验证码:</label> <input type="text" name="verify" class="text" id="verify"> <span class="star">*</span> <a href="javascript:void(0)" class="changeimg">换一换</a> </p> <p> <img src='{:U("Login/verify",'','')}' class="changeimg verifyimg"> </p> </form>
现在login表单和register表单的验证中submitHandler只做两件事情:给verify_register的form-click属性赋值,打开verify_register窗口:
login.js $('#login').validate({ submitHandler: function(form){ $("#verify_register").attr('form-click', 'login'); $("#verify_register").dialog('open'); },
login.js $("#register").dialog({ width: 430, height: 370, modal: true, resizable: false, autoOpen: false, title: "注册新用户", closeText: "关闭", buttons: [{ text: "提交", click: function(e) { $(this).submit(); }, }], }).validate({ submitHandler: function() { $("#verify_register").attr('form-click', 'register'); $("#verify_register").dialog('open'); },
验证码的检测以及注册或登陆成功后的跳转都放到verify_register的submitHandler中进行处理:
login.js submitHandler: function(form) { if ($("#verify_register").attr('form-click') == 'register') { $('#register').ajaxSubmit({ url: ThinkPHP["MODULE"] + "/User/register", type: "POST", data: { verify: $('#verify').val(), }, beforeSubmit: function() { $('#loading').dialog('open'); }, success: function(responseText) { if (responseText) { $('#loading').css('background', 'url(' + ThinkPHP['IMG'] + '/reg_success.png) no-repeat 20px center').html('数据新增成功...'); setTimeout(function() { $('#register').dialog('close'); $('#verify_register').dialog('close'); //关闭注册界面 $('#loading').dialog('close'); //关闭提示界面 $('#verify_register').resetForm(); //还原注册表单 $('#span.star').html('*').removeClass('succ'); //恢复*去掉对号 }, 1000); } }, }); } else if ($("#verify_register").attr('form-click') == 'login') { $(form).ajaxSubmit({ url: ThinkPHP['MODULE'] + '/User/login', type: 'POST', beforeSubmit: function() { $('#loading').dialog('open'); }, success: function(responseText) { if (responseText == -9) { $('#loading').dialog('option', 'width', 200).css('background', 'url(' + ThinkPHP['IMG'] + '/warning.png) no-repeat 20px center').html('账号或密码不正确...'); setTimeout(function(){ $('#loading').dialog('close'); $('#loading').dialog('option', 'width', 180).css('background', 'url(' + ThinkPHP['IMG'] + '/loading.gif) no-repeat 20px center').html('数据交互中...'); }, 2000); } else { $('#loading').dialog('option', 'width', 220).css('background', 'url(' + ThinkPHP['IMG'] + '/reg_success.png) no-repeat 20px center').html('登录成功,跳转中...'); setTimeout(function(){ location.href = 'http://www.baidu.com'; }, 1000); } }, }); } },
2. 自动登录
如要要实现自动登录,那么必须保存用户的登陆信息,这有两种方式:cookie和session。前者保存在客户端,安全性较低;后者存储在服务器端,安全性高,但是会占用服务器资源。这里我们先采用session的方式。
保存方式已经决定了,接下来就要考虑保存哪些内容。我们保存用户的id,最后登录的IP和登录时间。为了存储这些信息,我们需要对原来的用户表进行调整,添加两个字段:last_login和last_ip。
login.js中的一个小错误导致在这里浪费了半个小时,特意做个记录,以免以后又犯同样的错误。
login.js
} else if ($("#verify_register").attr('form-click') == 'login') { $(form).ajaxSubmit({ //此处应该是$('#login') url: ThinkPHP['MODULE'] + '/User/login', type: 'POST', beforeSubmit: function() { $('#loading').dialog('open'); }, success: function(responseText) { if (responseText == -9) {
继续上面的话题,更新用户信息和写入session的动作都是在UserModel的login方法中完成的:
UserModel.class.php $user = $this->field('id, password, last_login, last_ip')->where($map)->find(); if ($user['password'] == $password) { //更新登陆信息 $update = array( 'id' => $user['id'], 'last_login' => NOW_TIME, 'last_ip' => get_client_ip(1), //参数1表示返回long型数字 ); $this->save($update); //登陆信息写入SESSION $auth = array( 'id' => $user['id'], 'last_login' => $user['last_login'], 'last_ip' => $user['last_ip'], ); session('user_auth', $auth); return $user['id']; } else { return -9; //用户密码错误 }
在一个需要登录的系统中,很多操作是只有登录用户才可以做的,所以在许多地方我们都需要验证用户是否已经登录,这是通过检测session是否存在实现的。我们这一部分提取出来建立一个新的HomeController,IndexController、UserController和LoginController都继承HomeController。
HomeController.class.php <?php namespace Home\Controller; use Think\Controller; class HomeController extends Controller { protected function login() { if (session('?user_auth')) { return 1; } else { $this->redirect('Login/index'); //redirect方法自带U方法 } } }
IndexController.class.php <?php namespace Home\Controller; class IndexController extends HomeController { public function index() { if ($this->login()) { echo "Login successfully"; } } }
接下来我们要把用户信息写入COOKIE,因为COOKIE是保存在客户端的,为了增强安全性,我们需要对其进行加密存储。流程如下:
1. 在配置文件里设置一个密钥COOKIE_KEY
2. 在加密函数中,首先对密钥值执行sha1,将结果与用户名进行异或,再将结果用base64加密
config.php <?php return array( 'TMPL_PARSE_STRING' => array( '__CSS__' => __ROOT__.'/Public/'.MODULE_NAME.'/css', '__JS__' => __ROOT__.'/Public/'.MODULE_NAME.'/js', '__IMG__' => __ROOT__.'/Public/'.MODULE_NAME.'/img', ), //cookie秘钥 'COOKIE_KEY' => 'www.juedi.com', );
function.php //COOKIE加解密,0加密,1解密 function encrypttion($username, $type = 0) { $key = sha1(C('COOKIE_KEY')); if (!$type) { $username = base64_encode($username ^ $key); } else { $username = base64_decode($username) ^ $key; } return $username; }
现在来添加自动登录功能。
首先需要在模板上添加一个checkbox:
index.tpl <span class="username"> <input type="text" name="username" placeholder="用户名/邮箱"> <label class="auto" for="auto"><input type="checkbox" id="auto" name="auto">自动登录</label> </span>
UserController和UserModel中的login方法也需要做相应的改动:
UserModel.class.php //用户名加密写入COOKIE if ($auto == 'on') { cookie('auto', encryption($user['username']), 3600 * 24 * 30); }
在HomeController的login方法中首先就需要判断是否自动登录:
HomeController.class.php class HomeController extends Controller { protected function login() { //处理自动登录,当cookie存在且session不存在的情况下,生成session if (!is_null(cookie('auto')) && !session('?user_auth')) { $username = encryption(cookie('auto'), 1); $map['username'] = $username; $db = D('user'); $user = $db->field('id, username, last_login, last_ip')->where($map)->find(); //登陆信息写入SESSION $auth = array( 'id' => $user['id'], 'username' => $user['username'], 'last_login' => $user['last_login'], 'last_ip' => $user['last_ip'], ); session('user_auth', $auth); } //判断session是否存在 if (session('?user_auth')) { return 1; } else { $this->redirect('Login/index'); //redirect方法自带U方法 } }
LoginController的login方法也要做相应的改动:
LoginController.class.php public function index() { if (!session('?user_auth')) { //只有当session不存在时才可以看到登录界面 $this->display(); } else { $this->redirect('Index/index'); } }
3. 绑定IP验证登录
如果有人恶意地将cookie复制到另外一台电脑上,那么他就可以实现自动登录。为了防止cookie被盗用,我们把IP地址也加密写入cookie,在自动登录的时候验证是不是用户登录时的IP地址。
UserModel.class.php //用户名和IP加密写入COOKIE if ($auto == 'on') { cookie('auto', encryption($user['username']. '|' .get_client_ip()), 3600 * 24 * 30); }
HomeController.class.php if (!is_null(cookie('auto')) && !session('?user_auth')) { $value = explode('|', encryption(cookie('auto'), 1)); list($username, $ip) = $value; if ($ip == get_client_ip()) { $map['username'] = $username; $db = D('user'); $user = $db->field('id, username, last_ip')->where($map)->find(); //自动登录更新登陆信息 $update = array( 'id' => $user['id'], 'last_login' => NOW_TIME, ); $db->save($update); //登陆信息写入SESSION $auth = array( 'id' => $user['id'], 'username' => $user['username'], 'last_login' => NOW_TIME, 'last_ip' => $user['last_ip'], ); session('user_auth', $auth); } }
4. 微博主页设计
我们把微博主页也分成header、main和footer三个部分。
在这里我们先复习一下CSS的定位position。position主要有四个属性:static(默认属性)、absolute、relative、fixed,区别如下:
- static:默认值。没有定位,元素出现在正常的流中(忽略 top, bottom, left, right 或者 z-index 声明)。
- absolute:脱离了文档流,生成绝对定位的元素,相对于 static 定位以外的第一个父元素进行定位。元素的位置通过 "left", "top", "right" 以及 "bottom" 属性进行规定。
- relative:不脱离文档流,生成相对定位的元素,相对于其正常位置进行定位。因此,"left:20" 会向元素的 LEFT 位置添加 20 像素。
- fixed:脱离了文档流,生成绝对定位的元素,相对于浏览器窗口进行定位。元素的位置通过 "left", "top", "right" 以及 "bottom" 属性进行规定。
对于脱离了文档流的元素(即使用了absolute或fixed定位的元素),它相邻元素的margin不再相对于此元素。还需注意left、top、right、bottom属性和margin的区别。根据position属性的不同,left等值相对的对象也不同,而margin一直是相对于相邻元素的。
此外这一部分我们还用到了CSS3的box-shadow属性,关于该属性的使用可以参考 http://www.w3cplus.com/content/css3-box-shadow。
在做导航的时候遇到了一个问题今天没能解决:
可以在上图看到每个导航项目都太靠下了。
接着昨天的问题往下说。
进过今天下午半个多小时的仔细研究,终于解决了昨天出现的问题,特意将解决过程记录在此,以备参考。
首先,从图中可以很明显地看出li元素超出了header的下边界,其实这里还有一个很容易被忽视的问题,那就是header_main有点儿靠右。
我给ul加上一个float:left:
#header ul { float: left; }
然后通过firebug选取ul元素如下图:
我突然发现ul居然有16px的上下margin,在index.css开头我明明已经把ul的margin都设为0了呀?!
//起始CSS body, h1, h2, h3, h4, h5, h6, ol, ul, p, form { margin: 0; padding: 0; }
再仔细一看,终于发现了问题所在:注释!
对,就是开头的”//起始CSS“使上面的CSS设置没有生效,因为在CSS中注释只能用/* */。改成如下样式就好了:
/*起始CSS*/ body, h1, h2, h3, h4, h5, h6, ol, ul, p, form { margin: 0; padding: 0; }
现在又出现了一个新问题:文字太靠上了。我们想让链接垂直居中,这就需要用到line-height属性了。line-height有一个特性,叫做垂直居中性,把line-height值设置为height一样大小的值可以实现单行文字的垂直居中。
又有一个问题:当鼠标放到导航链接上时,我们想让它的背景颜色有42px的高度。这就需要将a元素设置为display:block:
最终的CSS代码如下:
index.css #header .nav { float: left; height: 42px; margin: 0 0 0 30px; } #header ul li { float: left; margin-left: 5px; height: 42px; line-height: 42px; } #header ul li a { display: block; padding: 0 10px; font-size: 16px; color: #fff; text-decoration: none; } #header ul li a:hover { background: #333; } #header ul li a.selected { background: #333; }
现在我们来实现“消息”和“账号”的弹出菜单功能:
index.tpl <li class="app">消息 <dl class="list"> <dd><a href="#">@到我的</a></dd> <dd><a href="#">收到的评论</a></dd> <dd><a href="#">发出的评论</a></dd> <dd><a href="#">我的私信</a></dd> <dd><a href="#">系统消息</a></dd> <dd><a href="#" class="line">发私信>></a></dd> </dl> </li>
index.css #header .app { padding: 0 10px; position: relative; cursor: pointer; } #header dl.list { width: 100px; background: #fff; border: 1px solid #666; position: absolute; top: 42px; left: -50px; display: none; } #header dl.list a { color: #444; height: 30px; /*覆盖掉继承的height和line-height*/ line-height: 30px; } #header dl.list a.line { border-top: 1px solid #eee; }
index.js $(function() { $('.app').hover(function(){ $(this).css({ background: '#444', //注意,千万不要用分号,属性值一定加引号!!! color: '#666', }).find('.list').show(); }, function(){ $(this).css({ background: '#666', //注意,千万不要用分号,属性值一定加引号!!! color: '#fff', }).find('.list').hide(); }); });
现在主页的框架基本已经搭好了,我们现在把里面经常需要用到的东西分离出来,做成模板使用。
首先我们建立两个文件夹:Public和Base。Public里存放各个模板文件,Base中存放引用模板文件的文件,index.tpl文件只需要继承common.tpl即可。
5. 退出及跳转页
当用户退出登录的时候,我们应该删除用户的session,如果用户选择了自动登录,那么还应该删除用户的cookie,然后跳转到登录页面。
UserController.class.php
//退出登录 public function logout() { //清除session session(null); //清理自动登录生成的cookie cookie('auto', null); $this->success('退出成功', U('Login/index')); }
现在只要把链接加到微博首页就可以了。
index.tpl <li class="app">账号 <dl class="list"> <dd><a href="#">个人设置</a></dd> <dd><a href="#">排行榜</a></dd> <dd><a href="#">申请认证</a></dd> <dd><a href="{:U('User/logout')}" class="line">退出</a></dd> </dl> </li>
下面来设计错误和成功后的跳转页,首先在配置文件里定义两个变量:
config.php //错误跳转模板 'TMPL_ACTION_ERROR' => 'Public/jump', //成功跳转模板 'TMPL_ACTION_SUCCESS' => 'Public/jump',
在设计跳转页的过程中有几个问题需要解决:1. 文字居中显示;2. 文字前加图标,文字与图标在同一水平位置;
第一个问题的解决方法是给父元素增加padding,让文字到垂直居中,然后用text-align:center使文字水平居中:
jump.tpl .info { margin: 100px auto; padding: 200px 0 0 0; height: 300px; width: 1200px; background: #fafafa; text-align: center; }
第二个问题是通过设置背景图片的位置解决的:
jump.tpl
.error {
background: url(__PUBLIC__/{:MODULE_NAME}/img/jump_error.png) no-repeat left bottom;
}
.success {
background: url(__PUBLIC__/{:MODULE_NAME}/img/jump_success.png) no-repeat left bottom;
}
6. 微博发布区设计
现在可是设计微博的发布区。
这一部分主要是在main区域,我们把它分成main_left和main_right两个部分。其中main_right的css代码如下:
index.css #main .main_right { float: right; width: 300px; background: #d0d0d0; }
我们可以看到背景颜色只应用到了文字所在的一行,如果我们想让背景颜色充满整个区域,那就需要给区域加个height:
index.css #main .main_right { float: right; width: 300px; min-height: 800px; background: #d0d0d0; }
因为左侧区域是微博的发布区,所以会有很多内容,当内容超过了该区域的高度时就会出问题:
可以看到,超出的内容到了main区域的下方,致使footer的内容挤到了右边。
这个问题可以通过javascript解决,代码如下:
index.js //高度保持一致 if ($('.main_left').height() > 800) { $('.main_right').height($('.main_left').height()); $('#main').height($('.main_left').height()); }
这里还有一个问题,当我们向下拉滚动条时,提交按钮会到导航栏的上方,如下图所示:
这个可以通过z-index解决:
index.js #header { position: fixed; width: 100%; height: 42px; top: 0px; /*top定义了一个定位元素的上外边距边界与其包含块上边界之间的偏移*/ background: #666; z-index: 9999; }
界面设置基本做好了,接着要实现一些小功能。
第一个功能就是限制用户的输入在140个字以内,并且数字随着用户的输入变化,如下图:
这个是通过javascript实现的:
index.js //微博输入内容计算字个数 $('.weibo_text').on('keyup', weibo_num); function weibo_num() { var total = 280; var len = $(this).val().length; var temp = 0; if (len > 0) { for (var i = 0; i < len; i++) { if ($(this).val().charCodeAt(i) > 255) { temp += 2; } else { temp ++; } } var result = parseInt((total - temp) / 2); $('.weibo_num').html('您还可以输入<strong>' + result + '</strong>个字'); } }
7. 引入表情插件
这一节我们在微博发布区引入一款jquery表情插件,过程比较简单,就是把css、js文件放到相应目录,从原来的index.html文件里复制一些内容到我们的index.tpl就可以了。
不过,在这里还是出现了一个小问题。
当我们添加表情的时候,字数显示并没有减少,我们需要修改js文件:
index.js //微博输入内容得到光标计算字个数 $('.weibo_text').on('focus', weibo_num);
这里又出现了一个新问题,第一次添加表情的时候字数不会减少,直到再次添加字数才开始变化:
8. 微博发布及表分析
首先我们需要新建一个表用来存储用户发布的微博及其它一些信息
这里需要注意的是上面的content_over字段。由于varchar查询起来速度比较慢,所以这里我们用char类型存储微博内容。因为char最多只能存储255个字符,所以我们把280-255=25个字符存到content_over中。
今天终于解决了一个困扰我多日的问题。虽然最后发现是我的粗心造成的,但是还得记录一下。
错误还是firebug发现的:
点击上图中的绿色部分可以进入jquery.js查看:
从这儿可以开出问题出在val()方法上,elem没有nodeName。nodeName是HTML元素的一个属性,elem没有这个属性就说明它不是HTML元素,或者它是undefined或null。我们可以看一下:
index.js function weibo_num() { alert(this[0]); //查看this[0],也就是elem var total = 280; var len = $(this).val().length; var temp = 0; if (len > 0) { for (var i = 0; i < len; i++) { if ($(this).val().charCodeAt(i) > 255) { temp += 2; } else { temp ++; } }
结果如下图所示:
果然是undefined。为什么会是这个值呢?
仔细检查weibo_num函数,我发现了问题所在:$(this)!!!
这里应该用$('.weibo_text')!改过来之后一切恢复正常。