【MVC】AngularJs+KendoUI开发报表Demo(导出Excel和折线图)

来源:互联网 发布:linux执行sql文件 编辑:程序博客网 时间:2024/04/20 07:43

废话写在最前面

做angular开发已经有很长一段时间了,它的优势已经不用赘述了,尤其是双向绑定和高度模块化,真是装逼利器,甚是好用...

至于KendoUi,接触时间不长,名字看上去是UI框架,但个人觉得更偏js。特别是自定义控件和Grid,是在客户端解析,节约了不少的内存和数据交互的开销。

两大框架各有所长,但稍有不慎就会陷入无尽的苦恼。特别是在没有装js开发插件的IDE中,没有任何错误提示,只能通过浏览器的调式一步一步修改。

做这个小例子,我就是这么过来的。做的时间不长,很多地方值得商榷,抽这个时间把整个过程详细记录下来,仅为以后的开发以此为参考和教训吧...

这里综合做了一个例子:包括Grid的显示、导出Excel、Echart查看走势图...

敲黑板了

先放个最后的效果图,就只做了一个Grid报表而已,【大神可以出门溜达了】很简单...真的很简单的功能...

导出Excel没做,结合NPOI后续再更新吧。

用到的另外两个框架:

H-UI、BootStrap

主要是样式的优化,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的重写构造方法, 我在另一篇博客里有记载:

LINQ扩展

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: "员工编码", width: 150 },                {                    field: "Age", title: "年龄", width: 100,                    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: "省", width: 100,                            template: "<p style=\"text-align:right;color:green\">{{dataItem.Province}}</p>"                        },                         {                             field: "City", title: "市", width: 100,                             template: "<p style=\"text-align:right;color:red\">{{dataItem.City}}</p>"                         },                          {                              field: "Area", title: "区", width: 100,                              template: "<p style=\"text-align:right;color:blue\">{{dataItem.Area}}</p>"                          }                    ]                },                {                    title: "操作", width: 80, sortable: false, attributes: { style: "text-align:center" },                    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,我之前用一个系列详细做过记录

【一步一步学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的样式

按钮标签:

 <button class="btn btn-secondary radius " ng-click="Zhexian()"><i class="Hui-iconfont Hui-iconfont-tongji-xian"> </i>走势图</button>

click方法:用了layer,单独弹出一个新页面。

  //查看折线图(单独打开一个视图)        $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", ["kendo.directives"]);    var myController = app.controller("myController", ["$scope", "$http", function ($scope, $http) {        $scope.xAxis_data = [];//X轴数据        $scope.series = [];//折线的对象数组        //我要统计两条折现,所以创建两个对象        //折线对象1        $scope.series_obj_a = {            name: '年龄',            type: 'line',            stack: '总量'        };        //折线对象2        $scope.series_obj_b = {            name: '工资',            type: 'line',            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.最终生成两条折线图(工资、年龄),所以我创建了两个折线图对象。


最后的效果










1 0
原创粉丝点击