废话写在最前面
做angular开发已经有很长一段时间了,它的优势已经不用赘述了,尤其是双向绑定和高度模块化,真是装逼利器,甚是好用...
至于KendoUi,接触时间不长,名字看上去是UI框架,但个人觉得更偏js。特别是自定义控件和Grid,是在客户端解析,节约了不少的内存和数据交互的开销。
两大框架各有所长,但稍有不慎就会陷入无尽的苦恼。特别是在没有装js开发插件的IDE中,没有任何错误提示,只能通过浏览器的调式一步一步修改。
做这个小例子,我就是这么过来的。做的时间不长,很多地方值得商榷,抽这个时间把整个过程详细记录下来,仅为以后的开发以此为参考和教训吧...
这里综合做了一个例子:包括Grid的显示、导出Excel、Echart查看走势图...
敲黑板了
先放个最后的效果图,就只做了一个Grid报表而已,【大神可以出门溜达了】很简单...真的很简单的功能...
导出Excel没做,结合NPOI后续再更新吧。
用到的另外两个框架:
主要是样式的优化,H-UI还是咱国人自己写的呢...
后台
1.构建model
1.1用户实体
public class Customer { public int Id_int { get; set; } public string Code { get; set; } public string Name { get; set; } public int Age { get; set; } public decimal SalaryDecimal { get; set; } public string Province { get; set; } public string City { get; set; } public string Area { get; set; } public DateTime CreateDateTime { get; set; } }
1.2传递参数DTO
查询条件。我把所有的查询条件封装成了一个对象,虽然也可以直接用拼接参数的方式传值...
/// <summary> /// 查询参数对象 /// </summary> public class CustomerParam { public string name { get; set; } public string stime { get; set; } public string etime { get; set; } public string area { get; set; } public string code { get;set; } public int page { get; set; } public int pageSize { get; set; } public string sort { get; set; } }
1.3统计结果集DTO
我做了两个汇总(总工资、总年龄),也封装成了对象。
/// <summary> /// 统计DTO /// </summary> public class SumDto { /// <summary> /// 总工资 /// </summary> public decimal SumMoney { get; set; } /// <summary> /// 总年龄 /// </summary> public long TotalAge { get; set; }
2.查询用户列表方法
这里模拟了55条记录,如果需要用到数据库,EF操作表就ok了。
/// <summary> /// 客户分页列表 /// </summary> /// <returns></returns> public PagedList<Customer> GetCustomers(CustomerParam param,out SumDto sumDto) { var list = new List<Customer>(); for (int i = 0; i < 55; i++) { var costomer = new Customer() { CreateDateTime = DateTime.Now.AddMinutes(i),SalaryDecimal = 100 + i,Age = 20 + i,Area = "渝北区" + i,City = "成都" + i,Code = "Customer" + i,Id_int = i,Name = "我叫张三" + i,Province = "重庆" + i }; list.Add(costomer); } #region 搜索条件 var i_list = list.ToList(); if (!string.IsNullOrEmpty(param.stime) && !string.IsNullOrEmpty(param.etime)) { //time var s_time = Convert.ToDateTime(param.stime); var e_time = Convert.ToDateTime(param.etime); i_list = i_list.FindAll(c => c.CreateDateTime >= s_time && c.CreateDateTime <= e_time); } //code or name if (!string.IsNullOrEmpty(param.name)) { i_list = i_list.FindAll(c => c.Code.Contains(param.name) || c.Name.Contains(param.name)); } #endregion sumDto = new SumDto() { SumMoney = i_list.Sum(o => o.SalaryDecimal),TotalAge = i_list.Sum(o => o.Age) }; return new PagedList<Customer>(i_list,param.page,param.pageSize);//自己写的扩展方法 }PagedList的重写构造方法, 我在另一篇博客里有记载:
3.控制器层
3.1Action指向视图
public ActionResult KendoView_() { return View(); }
3.2查询数据源
/// <summary> /// 获取数据源 /// </summary> /// <returns></returns> [HttpPost] public JsonResult GetUsers(CustomerParam query) { var sumDto = new SumDto();//用来做统计的对象 query.page -= 1;//视图传过来的值是1,索引是从0开始,所以-1 UserService _userService = new UserService(); var userlist = _userService.GetCustomers(query,out sumDto); return Json(new { data = userlist,total = userlist.TotalCount,TongJi = sumDto }); }注意:控制器方法必须注明为HttpPost方式
前台
1.布局页面(引入样式和js)
包括:KendoUI、JQ、Angular、H-ui、BootStrap...相关文件,要去官网或者官方qq群里下载。
<!DOCTYPE html> <html> <head> <Meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> <!--css--> <link href="~/static/h-ui/css/H-ui.min.css" rel="stylesheet" type="text/css" /> <link href="~/static/h-ui.admin/css/H-ui.admin.css" rel="stylesheet" type="text/css" /> <link href="~/lib/Hui-iconfont/1.0.7/iconfont.css" rel="stylesheet" type="text/css" /> <link href="~/lib/icheck/icheck.css" rel="stylesheet" type="text/css" /> <link href="~/static/h-ui.admin/skin/default/skin.css" rel="stylesheet" type="text/css" id="skin" /> <link href="~/static/h-ui.admin/css/style.css" rel="stylesheet" type="text/css" /> <link href="~/Scripts/toastr.css" rel="stylesheet" /> <link href="~/Scripts/bootstrap.min.css" rel="stylesheet" /> <link href="~/Scripts/KendoUI/styles/kendo.common-bootstrap.min.css" rel="stylesheet" /> <link href="~/Scripts/KendoUI/styles/kendo.bootstrap.min.css" rel="stylesheet" /> <!--JS--> <script src="~/lib/jquery/1.9.1/jquery.min.js" type="text/javascript"></script> <script src="~/lib/layer/2.1/layer.js" type="text/javascript"></script> <script src="~/static/h-ui/js/H-ui.js" type="text/javascript"></script> <script src="~/static/h-ui.admin/js/H-ui.admin.js" type="text/javascript"></script> <script src="~/Scripts/toastr.js"></script> <script src="~/Scripts/toastr.setting.js"></script> <!--angular--> <script src="~/Scripts/angular-1.4.2/angular.min.js"></script> <script src="~/lib/My97DatePicker/WdatePicker.js" type="text/javascript"></script> <!--bootstrap--> <script src="~/Scripts/bootstrap.min.js"></script> <!--kendo--> <script src="~/Scripts/KendoUI/kendo.all.min.js"></script> <script src="~/Scripts/KendoUI/kendo.angular.min.js"></script> <script src="~/Scripts/KendoUI/js/messages/kendo.messages.zh-CN.min.js"></script> <script src="~/Scripts/KendoUI/js/cultures/kendo.culture.zh-CN.min.js"></script> <!--自己重写的样式--> <style> body { color: #797979; background: #fff; padding: 0px !important; margin: 0 !important; font-size: 12px; color: black; } .k-grid .k-grid-header table thead tr th { color: deepskyblue; text-align: center; } .k-grid-header th.k-header > .k-link { color: deepskyblue; } .k-grid tr td { padding: 3px; padding-left: 5px; padding-right: 5px; } .btn { font-size: 12px; } .color-green { background-color: aquamarine; } .widget .widget-content { background-color: #fff; } .widget .widget-header { margin-bottom: 15px; border-bottom: 1px solid #ececec; *zoom: 1; } .widget.Box { border: 1px solid #d9d9d9; } .widget.Box .widget-header { background: #f1f2f7; border-bottom-color: #d9d9d9; line-height: 32px; padding-left: 12px; margin-bottom: 0; } .widget.Box .widget-header .toolbar.no-padding { margin: -1px; } .widget.Box .widget-header .toolbar.no-padding .btn { font-size: 13px; line-height: 23px; margin-top: 0; } </style> </head> <body> <div> @RenderBody() </div> </body> </html>
自己重写了一部分样式,也只是为了好看而已...
2.视图
2.1静态页面
@{ ViewBag.Title = "KendoGrid Test"; Layout = "~/Views/Shared/_LayoutKendo.cshtml"; ViewBag.stime = DateTime.Now.AddDays(-7).ToString("yyyy-MM-dd");//查询开始时间 ViewBag.etime = DateTime.Now.AddDays(1).ToString("yyyy-MM-dd");//结束时间 } <section ng-app="MyApp" ng-controller="myController" class="wrapper" style="padding-top: 15px" ng-init="ChangeTableHeight()"> <nav class="breadcrumb"> <i class="Hui-iconfont"></i> 首页 <span class="c-gray en">></span> @ViewBag.Title <a title="刷新" href="javascript:location.replace(location.href);" style="line-height: 1.6em; margin-top: 3px" class="btn btn-success radius r"> <i class="Hui-iconfont"></i> </a> </nav> <div class="page-container"> <!--搜索条件--> <div class="text-c"> 员工姓名: <input type="text" class="input-text" style="width: 250px" placeholder="请输入员工姓名" data-toggle="tooltip" data-placement="top" title="员工姓名" ng-model="query.name"> 创建时间: <input type="text" name="stime" style="width: 120px;" class="input-text Wdate" id="stime" ng-model="query.stime" onfocus="WdatePicker({ maxDate: '#F{$dp.$D(\'logmax\')||\'%y-%M-%d\'}' })"> - <input type="text" name="etime" style="width: 120px;" class="input-text Wdate" id="logmax" ng-model="query.etime" onfocus="WdatePicker({ minDate: '#F{$dp.$D(\'stime\')}',maxDate: '%y/%M/%d' })"> <button class="btn btn-success" ng-click="SearchList()"><i class="Hui-iconfont"></i> 搜索</button> </div> <!--操作--> <div class="cl pd-5 bg-1 bk-gray mt-20"> <span class="l"> <a class="btn btn-danger radius" id="delmany" href="javascript:;"> <i class="Hui-iconfont"></i> 批量删除 </a> <a href="javascript:;" data-title="添加员工" class="btn btn-primary radius" id="user_add"> <i class="Hui-iconfont"></i> 添加员工 </a> </span> </div> <!--Grid--> <div kendo-grid="Grid" options="GridOptions"></div> </div> </section>
2.2js
<script type="text/javascript"> var app = angular.module("MyApp",["kendo.directives"]); var myController = app.controller("myController",["$scope","$http",function ($scope,$http) { //查询条件 $scope.query = { stime: '@ViewBag.stime',etime: '@ViewBag.etime',area: '',code: "",name: '' } //统计结果集 $scope.TongJi = { SumMoney: 0.00,TotalAge: 0 } //数据源 $scope.GridDataSoruce = function () { return { pageSize: 10,page: 1,serverPaging: true,serverSorting: true,serverFiltering: true,serverGroupable: false,sort: { field: "SalaryDecimal",dir: "DESC" },transport: { read: function (options) { //JQuery时间选择器在angular中传不了值到后台 if ($("#stime").val() && $("#etime").val()) { $scope.query.stime = $("#stime").val(); $scope.query.etime = $("#etime").val(); } $http.post("GetUsers",$.extend({},options.data,$scope.query),{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },transformRequest: function (data) { //排序 if (data.sort) { var rtStr = ""; //var sortStr = "{0}-{1}#"; for (var i = 0,len = data.sort.length; i < len; i++) { var nowS = data.sort[i]; rtStr += (nowS.field + '-' + nowS.dir + '#'); //sortStr.format(nowS.field,nowS.dir); } if (rtStr.length > 0) { rtStr = rtStr.substring(0,rtStr.length - 1); } data.sort = rtStr; } return $.param(data);//参数 } }).then(function (response) { var dt = response.data; options.success(dt); //统计结果集 if (dt.TongJi) $scope.TongJi = dt.TongJi; },function (error) { console.log(error); options.error(error.statusText); }); console.log($scope.query); } },schema: { type: "json",data: "data",total: "total",errors: "errors",model: { fields: { CreateDateTime: { type: "date" } } } } } } //绑定数据源&表头 $scope.GridOptions = { dataSource: $scope.GridDataSoruce(),groupable: false,sortable: true,pageable: { refresh: false,pageSizes: [5,10],buttonCount: 5,numeric: true //如果不设true,会提示‘拖拽表头...’ },columnMenu: true,scrollable: true,selectable: false,dataBound: function () { //设置序号 var rows = this.items(); var page = this.pager.page() - 1; var pagesize = this.pager.pageSize(); $(rows).each(function () { if (rows.length > 0) { var index = $(this).index(); var XuHao = index + 1 + page * pagesize; var rowLable = $(this).find(".row-number").html(XuHao); } }); },columns: [ { field: "",title: "序号",width: 60,attributes: { style: "text-align:center" },template: "<span class='row-number'></span>",footerTemplate:"<span>汇总:</span>" },{ field: "SalaryDecimal",title: "工资",width: 100,format: "{0:0.00}",template: "<p style='text-align:center;color:pink'>¥{{dataItem.SalaryDecimal}}</p>",footerTemplate: "<div style='text-align:right;color:red'>¥<span ng-bind='TongJi.SumMoney | number:2'></span></div>" },{ field: "Name",title: "员工姓名",width: 150 },{ field: "Code",title: "员工编码",{ field: "Age",title: "年龄",template: "<p style='text-align:right;color:pink'>{{dataItem.Age}}岁</p>",footerTemplate: "<div style='text-align:right;color:red'>共<span ng-bind='TongJi.TotalAge'></span>岁</div>" },{ field: "CreateDateTime",title: "创建时间",format: "{0:yyyy-MM-dd HH:mm:ss}",width: 180 },//格式化时间 { title: "地址",columns: [ { field: "Province",title: "省",template: "<p style=\"text-align:right;color:green\">{{dataItem.Province}}</p>" },{ field: "City",title: "市",template: "<p style=\"text-align:right;color:red\">{{dataItem.City}}</p>" },{ field: "Area",title: "区",template: "<p style=\"text-align:right;color:blue\">{{dataItem.Area}}</p>" } ] },{ title: "操作",width: 80,sortable: false,template: function (item) { var showItem = '<a href="#" ng-click=\'showSelect(' + item.Id_int + ')\' title="得到用户标记" class="ml-5" style="text-decoration: none"><i class="Hui-iconfont"></i></a>'; //var res = showItem.format(item.Code,""); return showItem; } } ] } $scope.SearchList = function () { $scope.Grid.setDataSource(new kendo.data.DataSource($scope.GridDataSoruce())); } $scope.showSelect = function (id) { alert(id); //操作... } //改变table高度 $scope.ChangeTableHeight = function () { var prrGrid = $("div[options='GridOptions']"); var dataArea = prrGrid.find(".k-grid-content"); var bottomArea = prrGrid.find(".k-grid-pager"); var sxHeight = window.innerHeight - prrGrid.offset().top - 20; var diif = prrGrid.height() - sxHeight; prrGrid.height(sxHeight); dataArea.height(dataArea.height() - diif); } }]); </script>
注意: 1.页面初始化方法ChangeTableHeight(),该方法会填充table的空白,即使没有这么多数据。如果不加此方法,table的行高会依赖实际数据量..
2.不知道是不是JQ和angular有冲突,用JQ的时间选择器传值,控制器始终获取不到值。所以,在传值之前, 手动将两个时间控件的值赋值给参数对象.
3.post请求的时候,参数名必须和控制器方法的参数名一样,大小写也必须一样。
我两边的名字都叫query
后记:
1.这篇博文仅仅是给自己还有更多初学者作为参考笔记,主要是Kendo-Grid和Angular结合的使用,诸多查询效率、代码可读性、代码注释的问题没有解决,还请诸位看官高抬贵手。
2.今天是自己在这家公司的最后一天了,所以才有时间和闲心来总结过去的开发,写这篇文章。来这里2年左右了,细细回想起当初一起加班到凌晨、一起做活动的时光,心中也是各种滋味...看看现在的时间,2017.4.21 下午2.09分,还有不到4个小时就下班了。
很多同事都是不打不相识呢,特别是分公司的几位哥,当初做你们需求的时候真是想把你们拉过来当面打一顿,哈哈...现在我们的关系非常好,私底下也成为了朋友。
抬头望望窗外,阳光明媚,工位上的几盆绿萝,只有托付给以后的同事了...
兄弟们,保重,加油!
-----------------------------------2017.4.27更新-------------------------------
3.NPOI导出Excel
关于NPOI,我之前用一个系列详细做过记录
3.1DataTable导出Excel方法
/// <summary> /// 导出Excel /// </summary> /// <param name="dt">数据源</param> public static void ExportExcel(DataTable dt,string fileName) { HSSFWorkbook hssfworkbook = new HSSFWorkbook(); //创建Excel工作表 var sheet1 = hssfworkbook.CreateSheet("用户列表"); var row0 = sheet1.CreateRow(0); for (int i = 0; i < dt.Rows.Count; i++) { var row1 = sheet1.CreateRow(i + 1); for (int j = 0; j < dt.Columns.Count; j++) { var cell0 = row0.CreateCell(j); cell0.SetCellValue(dt.Columns[j].ToString()); var cell1 = row1.CreateCell(j); cell1.SetCellValue(dt.Rows[i][j].ToString()); } } //绝对路径-文件名 string excelName = string.Format(@"D:/{0}_{1}.xlsx",fileName,DateTime.Now.ToString("yyyy_MM_dd")); //保存 FileStream file = new FileStream(excelName,FileMode.Create,FileAccess.Write); hssfworkbook.Write(file); file.Close(); }因为NPOI是基于DataTable操作的,所以写了一个IEnumerable转DataTable的扩展方法:
public static DataTable ToTable<T>(this IEnumerable<T> list) { //属性集合 List<PropertyInfo> userList = new List<PropertyInfo>(); DataTable dt = new DataTable("MyTable"); Type type = typeof(T); Array.ForEach<PropertyInfo>(type.GetProperties(),p => { userList.Add(p); dt.Columns.Add(p.Name,p.PropertyType); }); foreach (var item in list) { var row = dt.NewRow(); userList.ForEach(u => row[u.Name] = u.GetValue(item,null)); dt.Rows.Add(row); } return dt; }
3.2前端
页面上一个按钮控件就不记录了;
Angular方法:
//导出excel $scope.Excel = function () { $.post('ExportExcel',{},function (data) { if (!data.success) { layer.alert('出错了:'+data.message,{ icon: 2 }); } }); }控制器:(注意: 我是将导出的数据源保存到Session)
public JsonResult ExportExcel() { if (Session["userlist"] != null) { var dataSoruce = Session["userlist"] as DataTable; Helper.ExportExcel(dataSoruce,"用户列表"); return Json(new { success = true }); } else { return Json(new { success = false,message = "获取用户数据源session失败" }); } }大功告成:在本地已经生成了相应的Excel
Echart生成折线图
祭出echart官网:
多说两句,echart是百度开发的客户端生成的图形报表第三方组件。想起前几年做折线图报表,不知道有第三方框架,硬生生用reportviewer和rdlc来做的,同样实现了这功能。可,人都是会变得,有了框架,谁还走那么多冤枉路呢?
这里我选取前十条记录的年龄和薪水来做折线图。
(才吃晚饭,先出去带幺儿散散步再回来写....)
查看折线图的按钮
页面上放了一个按钮,弹出一个新视图来show折线图。(说明一下:静态的样式都是用的h-ui的样式)
按钮标签:
@H_404_206@ <button class="btn btn-secondary radius " ng-click="Zhexian()"><i class="Hui-iconfont Hui-iconfont-tongji-xian"> </i>走势图</button>//查看折线图(单独打开一个视图) $scope.Zhexian = function () { layer.open({ type: 2,title: '薪水年龄折线图',shadeClose: true,shade: 0.4,area: ['60%','60%'],content: "/kendo/Zhexian" //控制器方法 }); }
控制器Zhexian方法:
public ActionResult Zhexian() { return View(); }
折线图View
静态页面很简单,只有一个div,用来做画板。因为要用到angular,所以外层加了一个section。
@{ ViewBag.Title = "Zhexian"; Layout = "~/Views/Shared/_Layout.cshtml"; } <section ng-app="MyApp" ng-controller="myController" class="wrapper" style="padding-top: 15px" ng-init="init()"> <div id="main" style="width: 100%;height:500px;"></div> </section>
注意这个angular的初始化方法,init。这个方法里面就去控制器里查询需要画折线图的数据。
该页面完整的脚本:
var app = angular.module("MyApp",$http) { $scope.xAxis_data = [];//X轴数据 $scope.series = [];//折线的对象数组 //我要统计两条折现,所以创建两个对象 //折线对象1 $scope.series_obj_a = { name: '年龄',type: 'line',stack: '总量' }; //折线对象2 $scope.series_obj_b = { name: '工资',stack: '总量' }; $scope.series_obj_a_data = [];//折线对象1上的值 $scope.series_obj_b_data = [];//折线对象2上的值 //初始化方法,得到待折线图的数据 $scope.init = function () { $.post('/kendo/GetZhexianData',function (data) { for (var i = 0; i < data.length; i++) { $scope.xAxis_data.push(data[i].UserName);//X轴-用户名 $scope.series_obj_a_data.push(data[i].Age);//年龄 $scope.series_obj_b_data.push(data[i].Salary);//薪水 } $scope.series_obj_a.data = $scope.series_obj_a_data; $scope.series_obj_b.data = $scope.series_obj_b_data; $scope.series.push($scope.series_obj_a); $scope.series.push($scope.series_obj_b); console.log($scope.series); $scope.MakeZhexian(); }); } //画折线图 $scope.MakeZhexian = function () { var myChart = echarts.init(document.getElementById('main')); var option = { title: { text: '选择前十组数据' },tooltip: { trigger: 'axis' },legend: { data: ['工资','年龄'] },grid: { left: '3%',right: '4%',bottom: '3%',containLabel: true },toolBox: { feature: { saveAsImage: {} } },xAxis: { type: 'category',boundaryGap: false,data: $scope.xAxis_data },yAxis: { type: 'value' },series: $scope.series }; myChart.setOption(option); } }]);说明:
1.初始化加载时post到控制器里,得到数据源
/// <summary> /// 画折线图的数据源 /// </summary> /// <returns></returns> public JsonResult GetZhexianData() { var userlist = Session["userlist"] as List<UserDto>;//这里可以用读取数据库的之,为了方便,我就只取了session return Json(userlist.Select(u => new { UserName = u.UserName,Salary = u.Salary,Age = u.Age }).Take(10));//只模拟前10条记录 }
2.画板上的看到的线条,本质是一个对象数组,每个折线图就是一个对象。
series: $scope.series
3.最终生成两条折线图(工资、年龄),所以我创建了两个折线图对象。
最后的效果