从本篇文章开始,我们就开始实战头条网后台管理。记得我写过webApp实战一,里面提到了头条网,我打算围绕这个头条网做一个后台管理系统,然后再实现头条网移动站点。因为现在公司也迟迟不能进行web项目的开发,一直都是银光,这样下去,恐怕我把web开发都忘光了。所以不管是J2EE还是ASP.NET总要持续学习,否则你就跟不上时代了。
废话不多说了,我们先看一下头条网后台管理Solution。
OK,三层架构,最简单的架构。我们现在看一下第一个页面
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>登录</title> <link rel="stylesheet" type="text/css" href="~/BootStrap/css/bootstrap.min.css" /> <link rel="stylesheet" type="text/css" href="~/BootStrap/css/bootstrap-theme.css" /> <link rel="stylesheet" type="text/css" href="~/Content/Login.css" /> @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/bundles/bootStrap") <script type="text/javascript" src="~/Scripts/angular.js"></script> </head> <body> <div id="main_layout" ng-app="loginModule" ng-controller="loginController"> <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title"> <img src="~/Images/Base/userlogin.png" style="width:25px;height:25px" /> <b>用户登录</b> </h3> </div> <form ng-submit="loginIn()" role="form"> <div class="div-panel-content"> <div class="row"> <div class="col-md-12 col-md-margin"> <div class="input-group"> <span class="input-group-addon bg_user"> <img src="~/Images/Base/usermng.jpg" /> </span> <input ng-model="userNo" type="text" class="form-control" autofocus="autofocus" placeholder="请输入用户名" maxlength="15" required> </div> </div> <div class="col-md-12 col-md-margin"> <div class="input-group"> <span class="input-group-addon bg_pwd"> <img src="~/Images/Base/pwd.jpg" /> </span> <input ng-model="pwd" type="password" class="form-control" placeholder="请输入密码" required maxlength="15"> </div> </div> <div class="col-md-12 col-md-margin form-inline"> <div class="input-group col-md-8"> <span class="input-group-addon bg_pwd"> <img src="~/Images/Base/validateCode.png" /> </span> <input ng-model="validateCode" type="text" class="form-control" placeholder="请输入验证码" required maxlength="5"> </div> <div class="col-md-4" style="float:right"> <img id="img_ValidateCode" src="../../Handler/ValidateCodeCreate.ashx" ng-click="getValidateCode()"> </div> </div> <div class="col-md-12 col-md-margin" style="text-align:center;margin-bottom:10px"> <button type="submit" class="btn btn-primary">登录</button> <button type="reset" class="btn btn-primary">取消</button> </div> <div id="div_login_process" class="col-md-12 col-md-margin" style="text-align:center;display:none"> <img src="~/Images/Base/loading.gif" /> <label style="color: #003399">正在登录,请稍候......</label> </div> </div> </div> </form> </div> </div> <script> var appModule = angular.module('loginModule', []); appModule.controller("loginController", function ($scope) { $scope.loginIn = function () { $.ajax({ url: "/Login/LoginIn", type: "POST", dataType: "json", data: { requestJson: JSON.stringify({ userNo: $.trim($scope.userNo), pwd: $.trim($scope.pwd), validateCode:$.trim($scope.validateCode) }) }, beforeSend: function () { $("#div_login_process").show(); }, complete: function () { $("#div_login_process").hide(); }, success: function (data) { if (data.suc == 1) { window.location.href = "/Home/Index"; } else { $("#div_login_process").hide(); alert(data.msg, "提示信息"); } }, error: function () { $("#div_login_process").hide(); } }); } $scope.getValidateCode = function () { $("#img_ValidateCode").attr("src", "../../Handler/ValidateCodeCreate.ashx?param=" + new Date().toTimeString()); } }); </script> </body> </html>
这个是登录界面,我们在这里引用了bootStrap,安哥拉杰斯(angularjs)。这个页面我们将一个div放在浏览器的正中间,然后在这个div中使用BootStrap布局。
<div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title"> <img src="~/Images/Base/userlogin.png" style="width:25px;height:25px" /> <b>用户登录</b> </h3> </div> <form ng-submit="loginIn()" role="form"> <div class="div-panel-content">
先看这个class panel这个其实是BootStrap提供的样式,在这个Panel中我们先设置panel-title,再设置panel-content。在Panel Content中我们布局我们的登录表单。关于UI布局我在这里就不多说了,给大家介绍两个网站,大家自己去看
BootStrap:
http://www.w3cschool.cc/bootstrap/bootstrap-tutorial.html
http://v3.bootcss.com/components/?#media
这两个网站就够你学BootStrap了,以后关于UI布局我不会再多说了,反正用到了就去这两个网站看就行了。不过在这里我只说一下这个div居中浏览器的css
#main_layout { position: absolute; top: 50%; left: 50%; margin: -130px 0 0 -165px; width: 330px; height: 260px; }
距离上左各50%,然后让左边距和上边距分别为宽度和高度的负一半就ok了。我们看一下效果,在看效果之前,先把特定页设置一下
再把默认的路由设置一下
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Login", action = "Login", id = UrlParameter.Optional } ); }
这样当部署以后,直接IP:port之后就是登录界面。
这就是用BootStrap布局出来的页面,OK,我们看一下是如何实现登录的。
首先我们在点击登录按钮的时候,会提交form到Login/LoginIn这个action。那么我们的提交是如何验证的,我们知道,如果单纯的使用ajax提交,意味着html5自带的验证就不会起作用,所以我们使用angularjs。在页面代码的div中,我们有如下设置
<div id="main_layout" ng-app="loginModule" ng-controller="loginController">
我们设置模块为loginModule,这个module其实是标示一个范围用的,假如你在一个页面有两部分想使用不同的$scope,那么你就可以设两个ng-app,而他们中的控制器和变量是互不影响的。在form标签中,我们看到有一个ng-submit
<form ng-submit="loginIn()" role="form">
这个意思是当提交的时候,去调用$scope中的loginIn方法。所以在页面底部的js中,在controller中有个方法叫loginIn。
$scope.loginIn = function () {},在这个方法中ajax请求发送了requestJson的请求数据。
requestJson: JSON.stringify({ userNo: $.trim($scope.userNo), pwd: $.trim($scope.pwd), validateCode:$.trim($scope.validateCode) })
在这里其实是取得$scope中的三个表单绑定的值,怎么绑定的呢,注意这三个input表单,都有一个ng-model的扩展标签,分别和$scope进行关联绑定。如果你引用了安哥拉杰斯,并且在html标签上标记了ng-model标签,那么在VS中当你$scope点的时候,是会有智能提示的。
正是这个方法去发出ajax请求,在登录失败后,弹出错误信息
在登录成功后,跳转至主页面。$("#div_login_process")这个div是用来展示登录进度的,效果如上图,就是这么简单。
那么我们在登录的时候,如果用户没有输入用户名或者密码怎么验证的呢,这个不需要你操心,这个是html5自带的,看一下效果,他会提示你请填写此字段,只要你设置了required。如果你给input type="text"设置了title属性,那么它的验证信息就会加上title的内容
<input ng-model="userNo" type="text" class="form-control" title="用户名不能为空" autofocus="autofocus" placeholder="请输入用户名" maxlength="15" required>
确实是这样,大家注意到这个用户名文本框我设置了一个autofocus,这个标签也是html5的新标签用来设置页面呈现以后,获得焦点的元素。
OK,上面的部分讲完了,到下面的部分,验证码。这个验证码需要在服务端生成,我们看前端代码是调用了一个httpHandler处理文件。
<img id="img_ValidateCode" src="../../Handler/ValidateCodeCreate.ashx" ng-click="getValidateCode()">
在这个img被click的时候调用了$scope中的getValidateCode方法。getValidateCode方法很简单,只不过是重新设置img的src属性。我们主要看这个httpHandler是如何向客户端输出img的。
public class ValidateCodeCreate : IHttpHandler, IRequiresSessionState { public void ProcessRequest(HttpContext context) { ValidateCode vCode = new ValidateCode(); string code = vCode.CreateValidateCode(5); context.Session["ValidateCode"] = code; byte[] bytes = vCode.CreateValidateGraphic(code); MemoryStream ms = new MemoryStream(bytes); Bitmap bitmap = new Bitmap(ms); bitmap.Save(context.Response.OutputStream, ImageFormat.Jpeg); context.Response.ContentType = "image/jpg"; context.Response.End(); } public bool IsReusable { get { return false; } } }
在这里我们通过调用一个ValidateCode类,去获取一个5位的验证码图片。并且把生成的验证码存放在session中,用户验证客户端提交的验证码。关于这个C#生成validateCode的方法一大堆,我在这里就不贴出来代码了。好的,到此界面上的所有逻辑就讲完了,接下来我们看一下控制器。
public class LoginController : BaseController { public ViewResult Login() { return View("~/Views/Home/Login.cshtml"); } [HttpGet] public RedirectToRouteResult LogOut() { Session.Clear(); return RedirectToAction("Login"); } [HttpPost] public ActionResult LoginIn(FormCollection fc) { string requestJson = fc["requestJson"]; JObject jObj = (JObject)JsonConvert.DeserializeObject(requestJson); string userNo = jObj.Value<string>("userNo"); string pwd = jObj.Value<string>("pwd"); string validateCode = jObj.Value<string>("validateCode"); if (string.IsNullOrWhiteSpace(userNo)) { return GetJsonMessage("LG_001"); } if (string.IsNullOrWhiteSpace(pwd)) { return GetJsonMessage("LG_002"); } if (string.IsNullOrWhiteSpace(pwd)) { return GetJsonMessage("LG_004"); } if (!validateCode.Trim().Equals(Session["ValidateCode"])) { return GetJsonMessage("LG_005"); } bool isLoginSuc = false; UserInfoEntity userInfoEntity = UserInfoBiz.GetInstance().GetLoginUser(userNo.Trim(), pwd.Trim(), out isLoginSuc); if (isLoginSuc) { Session["User"] = userInfoEntity; return Json(new { suc = 1 }); } return GetJsonMessage("LG_003"); } }
主要有三个action,一个是导向页面的Login,一个是处理登录的LoginIn,一个是处理注销的LoginOut。这里主要是LogIn。我们首先拿到客户端提交的requestJson,这里你既可以使用FormCollection来接收requestJson,也可以直接使用Request["requestJson"]。拿到客户端的请求数据以后,我们使用Newtonsoft.json将其反序列化然后得到每个表单值。验证后,调用Biz层方法去获取用户信息。这里的GetJsonMessage其实是BaseController中的方法
[LoginFilter] public class BaseController : Controller { protected string UserID { get { UserInfoEntity userEntity = (Session["User"] as UserInfoEntity); return userEntity != null ? userEntity.UserID : string.Empty; } } public JsonResult GetJsonMessage(string msg, JsonMsgType jsonMsgType = JsonMsgType.FAIL) { return Json(new { suc = (int)jsonMsgType, msg = MessageResourceBuilder.GetMessageResource(msg) }, JsonRequestBehavior.AllowGet); } public JavaScriptResult GetJSMessage(string msg) { return JavaScript("alert('" + MessageResourceBuilder.GetMessageResource(msg) + "')"); } } public enum JsonMsgType { SUCCESS = 1, FAIL = 0 }
用于根据MessageID获取Message。我们看一下这个MessageResourceBuilder。
public class MessageResourceBuilder { private static Dictionary<string, string> messages; public static string GetMessageResource(string resourceID) { if (messages == null || messages.Count == 0) { messages = GetAllMessageResource(); } if (!messages.ContainsKey(resourceID)) { throw new Exception(string.Concat("ResourceID:", resourceID, " doesn't exist in message resource file.")); } return messages[resourceID]; } private static Dictionary<string, string> GetAllMessageResource() { Dictionary<string, string> messageResources = new Dictionary<string, string>(); string baseFolder = AppDomain.CurrentDomain.BaseDirectory; string messageFolder = ConstValues.CONN_MessageResourceFolder; string messageResourceFolder = Path.Combine(baseFolder, messageFolder); if (!Directory.Exists(messageResourceFolder)) { LogHelper.WriteExceptionLog(string.Concat(MethodBase.GetCurrentMethod().Name, ":", "MessageResource Folder does'nt exist!")); return null; } string[] resourceFiles = Directory.GetFiles(messageResourceFolder, "*.xml", SearchOption.AllDirectories); if (resourceFiles.Length == 0) { LogHelper.WriteExceptionLog(string.Concat(MethodBase.GetCurrentMethod().Name, ":", "MessageResource files don't exist!")); return null; } foreach (var resourceFile in resourceFiles) { XmlDocument xmlDocument = new XmlDocument(); xmlDocument.Load(resourceFile); XmlElement root = xmlDocument.DocumentElement; XmlNodeList nodeList = root.SelectNodes("/MessageResources/Message"); foreach (XmlNode node in nodeList) { messageResources.Add(node.Attributes["ResourceID"].Value, node.InnerText); } } return messageResources; } }
其实是将所有的Message放到Dictionary中,用的时候根据ResourceID取出来。代码很简单,就是获取folder下面的文件,然后循环变化,进行xml解析,加入Dictionary中。我们看一下xml文件的定义,其实很简单。
<?xml version="1.0" encoding="utf-8" ?> <MessageResources> <Message ResourceID="LG_001"> 用户名不能为空! </Message> <Message ResourceID="LG_002"> 密码不能为空! </Message> <Message ResourceID="LG_003"> 用户名或者密码不正确! </Message> <Message ResourceID="LG_004"> 验证码不能为空! </Message> <Message ResourceID="LG_005"> 验证码不正确! </Message> </MessageResources>
ok,整个登录就说完了,登陆成功后,跳转至Home/Index界面。
success: function (data) { if (data.suc == 1) { window.location.href = "/Home/Index"; } else { $("#div_login_process").hide(); alert(data.msg, "提示信息"); $scope.getValidateCode(); } },
我们看一下登录成功后的效果。