Node.js 切近实战(七) 之Excel在线(文件&文件组)

最近西安的天气真他妈的热,感觉还是青海的天气美,最高温28度。上周逛了青海湖,感觉还是意犹未尽,其实我还是很喜欢去一趟西藏的,但是考虑到花费也没人陪我,我暂时放弃这个念头。计划去一下重庆或者甘南,也许是现实的。

OK,废话不多说,今天我们来看一下Excel在线部分的文件和文件组。首先我们来看一下页面,调一下胃口。俗话说无图无真相,先看图。

wKioL1d6hXWx-U3mAADpvT66v2g395.png

没错,还是Telerik Kendo UI,其实我面试的时候当听到别人说自己用的是EasyUI和ExtJs的时候,我就不那么上心,但是如果有人用的是Kendo UI for Html5&JS的话,我就会多关注一点。

先看一下页面整体代码。

wKiom1d6hn7TeaCTAAEYoILJIqw841.png

很简单,还是BootStrap布局,jade模板。注意下最底下的css样式,在jade模板中,如果想要在页面定义css,就要像上面这样写。注意这里我们引用了一个部分页,popup.jade,里面其实就是第一幅图里面New,Rename等按钮弹出的modal页。

wKiom1d6h7bT1hhFAABuLvwydac892.png

OK,首先我们看到的是左边的树,这个树叫kendoTreeView,我们来看一下这个树是怎么生成的。

var url = "/filegroup/list/" + userObj.UserID;
var dataSource = new kendo.data.HierarchicalDataSource({
        transport: {
            read: {
                url: url,
                dataType: "json"
            }
        },
        schema: {
            model: {
                id: "_id",
                children: "subgroup",
                expanded: true,
                hasChildren: function (node) {
                    return (node.subgroup && node.subgroup.length > 0);
                },
                spriteCssClass: "folder"
            }
        }
});
    
$("#treeview-filegroup").kendoTreeView({
        dataSource: dataSource,
        dataTextField: ["filecount"],
        dataValueField: ["_id"],
        change: function (e) {
            var tree = e.sender;
            selNode = tree.select();
            var data = tree.dataItem(selNode);
            
            if (data._id) {
                selGroupId = data._id;
                $("#chk_all").prop('checked', false);
                getFilelist(data._id);
            }
        }
    });

OK,这段代码就是生成树的代码,注意这里的dataSource,当页面加载以后,会请求url,filegroup/list/{0},我们来看一下后台这个api。

router.get('/filegroup/list/:userId', fileRoutes.fileGroupList);

再看一下fileGroupList方法。

exports.fileGroupList = function (req, res) {
    fileGroupModel.find({ 'userid': req.params.userId }, null, { sort: { name: 1 } } , function (error, fileGroup) {
        res.json(fileGroup);
    });
}

其实就是根据传入的userid查询了一下fileGroup Collection,查出来后,注意这里的schema,它的model定义树节点id是我们mongodb的主键,children是subgroup(上节讲过group和subgroup的model定义,不明白的去上节看),hasChildrenC返回是否有子节点。spriteCssClass设置父节点样式,注意我们第一幅图定义的页面样式就用在这里。OK,接下里我们看树的change事件,当有选中的节点时,将右边列表表头的全选复选框uncheck,并根据选中的_id去mongodb查询group下面的数据,我们来看一下getFilelist方法。

function getFilelist(groupId) {
    if (!groupId) return;
    
    $.get("/filegroup/" + groupId, function (result) {
        var grid = $("#file_list").data("kendoGrid");
        if (result) {
            var dataSource = new kendo.data.DataSource({
                pageSize: 10,
                data: result,
                schema: {
                    parse: function (response) {
                        $.each(response, function (idx, elem) {
                            if (elem.createdate && typeof elem.createdate === "string") {
                                elem.createdate = kendo.parseDate(elem.createdate, "yyyy-MM-ddTHH:mm:ss.fffZ");
                            }
                            
                            if (elem.lasteditdate && typeof elem.lasteditdate === "string") {
                                elem.lasteditdate = kendo.parseDate(elem.lasteditdate, "yyyy-MM-ddTHH:mm:ss.fffZ");
                            }
                        });
                        return response;
                    }
                }
            });
            
            grid.setDataSource(dataSource);
        }
        else {
            grid.dataSource.data([]);
        }
    });
}

直接调用rest api filegroup/{0}查询数据,得到结果以后,构造kendo Grid的dataSource,对日期进行格式化。

router.get('/filegroup/:id', fileRoutes.fileGroup);
exports.fileGroup = function (req, res) {
    var groupId = req.params.id;
    fileGroupModel.findById(groupId).populate('file').exec(function (error, doc) {
        if (!doc || doc.length == 0) {
            fileGroupModel.findOne({ 'subgroup._id': groupId })
            .populate('subgroup')
            .populate('subgroup.file')
            .exec(function (error, docs) {
                if (docs) {
                    var subGroupIndex = -1;
                    docs.subgroup.forEach(function (element, index, arra) {
                        if (subGroupIndex > -1) return;
                        
                        if (element._id == groupId) {
                            subGroupIndex = index;
                        }
                    });
                    
                    if (subGroupIndex > -1) {
                        res.json(docs.subgroup[subGroupIndex].file);
                    }
                }
            });
        }
        else {
            res.json(doc.file);
        }
    });
}

这里要注意,首先我们也不知道这里传入的是groupId还是subgroupId,所以我们先拿groupId查询,如果查到了,就返回数据,如果没查到,就拿_id去查subgroup,查询subgroup,注意这里要使用subgroup._id作为查询条件,因为subgroup是嵌入在group中的,是一个整体。查完之后注意这里的两个populate,如果没有populate的话,意味着这些嵌入的subgroup以及引用的file都不会被包含在查询结果中,我们来看一下查询的结果,在后台加一句console.log(docs)即可。有两个subgroup,两个下面都有文件。

wKiom1d6lw2D21nAAABTK7Q12dg097.png

我们用robomongo也许看的更清晰一些,两个group,一个下面有7个文件,一个有2个文件。

wKioL1d6lrDyyoUNAAB3v_zHWTc241.png

此时这个结果的话,我们还得找到我们页面传递的_id对应的subgroup下面的file。所以在代码中又有了循环匹配id确定subgroup索引下标的过程,找到后,直接根据索引拿出file。是不是很麻烦,如果你有什么好的办法,可以给我留言。

接下来我们来看一下kendo grid,首先看一下这个全选。

$("#chk_all").click(function () {
    var isChecked = $(this).prop("checked");
    $("#file_list table:eq(1)").find("tr").each(function () {
        $(this).children("td:first").find("input[type='checkbox']").first().prop('checked', isChecked);
    });
});

看起来很简单,找到第一个table找到所有tr,再找到第一个td,锁定checkbox让它选中或者不选中。

$("#file_list").kendoGrid({
        scrollable: true,
        selectable: true,
        allowCopy: true,
        resizable: false,
        sortable: true,
        pageable: {
            refresh: true,
            pageSizes: [10, 20, 50, 100],
            buttonCount: 5
        },
        toolbar: [
            { name: 'share', imageClass: 'glyphicon glyphicon-share-alt' },
            { name: 'unshare', imageClass: 'glyphicon glyphicon-lock' },
            { name: 'batch_delete', text: "Delete" , imageClass: 'glyphicon glyphicon-trash' }, 
            { name: 'export', imageClass: 'k-icon k-i-excel' }],
        columns: [{
                template: "<div class='center-align-text'>" +
                    "<input id='chkId_#=_id#' type='checkbox' class='k-checkbox' value='#=_id#' onclick='chkHeader_click'/>" 
                    + "<label class='k-checkbox-label' for='chkId_#=_id#'></label></div>",
                field: "",
                title: "<div class='center-align-text'>" +
                    "<input type='checkbox' class='k-checkbox' id='chk_all'/>" 
                    + "<label class='k-checkbox-label' for='chk_all'></label></div>",
                width: 40
            },
            {
                field: "fullname", 
                title: "File Name"
            }, {
                field: "isshared", 
                title: "Shared" ,
                width: 85, 
                template: "<div><input type='checkbox' class='k-checkbox' value='#=isshared#' #=isshared ? \"checked='checked'\":\"\" # />" 
                + "<label class='k-checkbox-label'></label></div>",
                sortable: false
            },
            {
                command: [
                    {
                        name: "preview",
                        text: "", 
                        imageClass: 'glyphicon glyphicon-search',
                        click: showDetails
                    }, {
                        name: "delete",
                        text: "", 
                        imageClass: 'glyphicon glyphicon-trash',
                        click: confirmFileDelete
                    }, {
                        name: "rename",
                        text: "",
                        imageClass: 'glyphicon glyphicon-list-alt',
                        click: fileRename
                    }, {
                        name: "edits", 
                        text: "", 
                        imageClass: 'glyphicon glyphicon-pencil',
                        click: viewfile
                    }
                ], 
                width: 310,
                title: "Operation"
            }]
    });

注意grid中的Shared列,使用的是kendo的模板#=#这种写法。最后我们再看一下command列,这个列的话其实就是定义一些按钮,每个按钮都定义好了click事件。就看第一个showDetails,看看js代码

function showDetails(e) {
    var wnd = $("#wd_details").kendoWindow({
        title: "File Detail Info",
        modal: true,
        visible: false,
        resizable: false,
        minWidth: 300
    }).data("kendoWindow");
    
    var detailsTemplate = kendo.template($("#popup_detail").html());
    e.preventDefault();
    
    var dataItem = this.dataItem($(e.currentTarget).closest("tr"));
    wnd.content(detailsTemplate(dataItem));
    wnd.center().open();
}

在这里其实就是弹出一个kendo window,这个弹出页中又加载了kendo 模板popup_detail,我们来看一下这个template。

script#popup_detail(type="text/x-kendo-template")
 dl
  dt
   label File Name:
  dd #=fullname#
  dt
   label Shared:
  dd #=isshared#
  dt
   label CreateDate:
  dd #=kendo.toString(createdate,'MM/dd/yyyy HH:mm tt')#
  dt
   label LastEditUser:
  dd #=lastedituser#
  dt
   label LastEditDate:
  dd #=kendo.toString(lasteditdate,'MM/dd/yyyy HH:mm tt')#

只要我们将一个对象给模板,模板就会自己替换对应的内容。在这里我们会先拿到点击行的对象,然后通过.content赋给模板,最后弹出modal页。

wKioL1d6k1_zdULEAAC8Y3_OHmw826.png

就是这么简单,点击重命名按钮,就会弹出重命名页面。

wKioL1d6k6WjdMwrAACmHiR005A529.png

rename功能其实很简单,看看前台和后台。

$("#btn_fileRename").click(function () {
    var fileName = $.trim($("#new_fileName").val());
    if (!fileName) {
        showMsg("info", "Please input new file name!");
        return;
    }
    
    var postData = {
        id: $("#hfd_fileId").val(),
        filename: fileName
    };
    
    $.ajax({
        url: '/file/rename', 
        type: 'PUT', 
        dataType: 'json',
        data: { postData: JSON.stringify(postData) },
        success: function (res) {
            if (!res.isSuc) {
                showMsg('error', res.msg);
                return;
            }
            
            $("#wd_fileRename").data("kendoWindow").close();
            getFilelist(selGroupId);
        }
    });
});
router.put('/file/rename', fileRoutes.fileRename);
exports.fileRename = function (req, res) {
    var data = JSON.parse(req.body.postData);
    fileModel.findByIdAndUpdate(data.id, { $set: { name: data.filename, lasteditdate: Date.now() } }
        , function (error, result) {
        if (error) {
            res.json({ isSuc: false, msg: error.message });
        }
        else {
            res.json({ isSuc: true });
        }
    });
}

nodejs,用起来就是这么爽,好了今天就到这里,明天我们继续会讲剩下的group&subgroup创建,文件删除,共享设置等功能,敬请期待。

本文出自 “技术创造价值” 博客,请务必保留此出处http://leelei.blog.51cto.com/856755/1795764

本页内容版权归属为原作者,如有侵犯您的权益,请通知我们删除。

shell快速迁移海量文件 - 2016-07-11 22:07:13

业务需求:需要把一个目录下的1000多万个文件迁移到远程机器 思路:用wget来把文件一个一个的迁移过去,因为文件数量比较大,如果一下在循环操作,会非常慢。所以分批操作,采用化整为零的方法。 #!/bin/shhome=/usr/local/www/skate/image63delbackcd$homeif[`pwd`==$home];thena="1100000020000003000000400000050000006000000700000080000009000000"forbin$adoc=`e

自动升级OPENSSH shell脚本 - 2016-07-11 19:07:23

由于管理着两百多台Linux服务器,一个人搞这么多机器的安全加固比较累,因此在学习了shell脚本之后果断的写了一些常用脚本做一些系统日常维护,本文OPENSSH的升级是博主本人几乎每两三个月就要做一次升级的,没办法绿盟安全扫描系统总是扫描到相关的高危漏洞,再就是OPENSSH版本更新的也比较频繁,因此不偷懒几乎没法活了。废话不说了,在这里贴下脚本,已经在线上服务器上执行了上百次了,应该不会有什么问题。 #!/bin/bash########################################

python for循环remove同一个list - 2016-07-11 19:07:23

下午在用python将Linux的conf配置文件转化成字典dict时遇到了一个奇怪的问题,原先conf配置文件中没有注释行(以#开头的行),后来为了避免这种情况,添加了一个对以#开头的行删除的操作。 实践结果颠覆了已有的认知,直接上代码示例。 代码片段1 #!/usr/bin/python#encoding:utf-8#-*-coding:utf8-*-importrelist_to_test=['#','#conf','NAME="Ubuntu"','VERSION="14.04.3LTS,Trust
豆子继续来看看PowerShell 如何管理 AWS的虚拟网络 VPC。 网上我没有找到快速上手的文档,相关的命令主要来自官方api文档和get-command的搜索。 http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Route_Tables.html#route-tables-api-cli VPC的配置向导里面默认给了4种场景的配置方案,下面我以第一种最简单的场景为例,用PowerShell配置一下。 场景如下,简单的说,我需要配置一

使用amoeba配置mysql读写分离 - 2016-07-11 19:07:12

一,背景介绍: Amoeba(变形虫)项目,专注 分布式数据库 proxy 开发。座落与Client、DB Server(s)之间。对客户端透明。具有负载均衡、高可用性、sql过滤、读写分离、可路由相关的query到目标数据库、可并发请求多台数据库合并结果。 要想搭建Amoeba读写分离,首先需要知道MySQL的主从配置。具体的架构图如下图: 二,配置所需的环境: Amoeba for mysql:192.168.1.28 Master : 192.168.1.247 Slave : 192.168.1.
本节我们来部署Exchange2016邮件服务器,如下图红框所示。 Exchange2016相对于Exchange2013来说,改变不大。最大的改变还是客户端访问服务器角色(CAS)和邮箱服务器角色(MBX)的角色合并。也就是说我们无法将CAS和MBX分开部署。这一更改反映了Exchange服务器角色归置(自 Exchange 2010 起的推荐最佳做法)的理念。 Exchange2016体系结构图: 多角色 Exchange 服务器体系结构带来了以下切实可见的好处: a. 环境中的所有 Exchange

Openvpn部署实施方案 - 2016-07-11 19:07:37

OpenVPN简介 VPN直译就是虚拟专用通道,是提供给企业之间或者个人与公司之间 安全数据传输 的隧道,OpenVPN无疑是Linux下开源VPN的先锋,提供了良好的性能和友好的用户GUI。[ OpenVPN允许参与建立VPN的单点使用共享金钥,电子证书,或者用户名/密码来进行身份验证。它大量使用了OpenSSL加密库中的SSLv3/TLSv1 协议函式库。OpenVPN能在Solaris、Linux、OpenBSD、FreeBSD、NetBSD、Mac OS X与Windows 上运行,并包含了许多安
1. OpenvSwitch简介 Open vSwitch(下面简称为 OVS)是由 Nicira Networks 主导的,运行在虚拟化平台(例如 KVM,Xen)上的虚拟交换机。在虚拟化平台上,OVS可以为动态变化的端点提供 2 层交换功能,很好的控制虚拟网络中的访问策略、网络隔离、流量监控等等。主要实现代码为可移植的C代码。 它的目的是让大规模网络自动化可以通过编程扩展,同时仍然支持标准的管理接口和协议(例如NetFlow,sFlow,SPAN,RSPAN,CLI,LACP,802.1ag)。此外,
Lync Server 2013就地升级Skype for Business Server 2015,So easy ,So do. 一切都按部就班进行,一路过关斩妖,安装必须的IIS KB补丁、SQL RtcLocal/LYNCLocal with sp1、删除原来的Lync Server 2013相关的组件和数据库,再安装Skype for Business Server 2015组件和数据库,一切看起来就是那么顺利。 规划不到位(安装系统和Lync,系统盘空间规划的比较小),必报错,今天讲一例报错,
一、前言 最近想测试一下RedHat 7 KVM的V2V功能,结果发现Redhat 7.0没有V2V的包,上RedHat官网才发现要RedHat 7.2才包含Virt-V2V的包。 官网说明如下: Virt-v2v became a supported product starting with Red Hat Enterprise Linux 7.2. Therefore, we recommend you update your virt-v2v conversion server to Red Ha