基于layui2和thinkphp5项目开发心得记录(一)

写这系列的文章目的无他,仅仅是用以记录自己写项目的一些思路和心得,在开发过程中代码是以实现为首要目的,写得并不会非常美观实用,代码会在后期不断调试更改,文章也会进行不定期更新。未经作者允许请勿转载。

最新更新:2018/7/26

功能需求:选取页面表格信息,批量生成word文件 

相关技术点

  • 前端layui表格的数据显示与后台json接口
  • 页面与页面之间的参数传递,页面与后台之间的参数传递
  • php批量生成word文件(数组循环遍历、字符串与数组转化、js中window.location.search的用法和作用。)
  • html样式对word影响
  • 浏览器文件下载

首先是用一个layui表格对数据进行展示,html页面写个table标签,给个id

<table class="layui-table"  id="projtb"  lay-filter="filtertb"></table>

 按照layui框架的规则,在JS中对表格的表头值进行规定,field是与数据库对应的字段值,title是表格的表头

layui.use('table', function(){
  var table = layui.table;
  
  //表格列表
  var show_col=[[
            {type:'checkbox', fixed: 'left'}
            ,{field:'id',width:85, sort: 'true',  title: 'ID',style:"font-size:10px;"}
            ,{field:'name',width:180, sort: 'true',  title: '名称',style:"font-size:12px"}
            ,{field:'desc', width:180, sort: 'true' , title: '备注',style:"font-size:12px"}
            ,{field:'is_on_duty' ,width:140,sort: 'true', title: '是否管理',style:"font-size:12px"}
            ,{field:'status', width:94, sort: 'true' , title: '状态',style:"font-size:12px"}
            ,{fixed:'right', width:114, align:'center', toolbar: '#op-bar' , title: '操作',style:"font-size:12px"}
        ]];

  //表格属性
  var options={
    elem: '#projtb'
    ,height: 315
    ,url: ' '     //数据接口
    ,page: true     //开启分页
    ,cols: show_col
  }
 
  //渲染
  table.render(options);
  
});

请求后台接口,对表格数据进行渲染。数据来源都是从服务器上的数据库获取,感觉刚上手thinkphp5框架,对数据库的操作还是蛮顺手的。接口以json形式返回。注意layui表格的接口数据有它自己的默认规则(除非是自己定义返回的数据格式),如果只按照自己后台数据去定义json,会导致页面报错说接口异常,或者表格一片空白。默认接口数据格式以及自定义数据格式的官方文档传送门

/*数据接口*/
    public function getDeptList($keyword = '', $page = 1,$limit=20)
    {
        $map = [];
        if ($keyword) {
            $map['name'] = ['like', "%{$keyword}%"];
        }
        $dept_list_query = $this->dept_model->where($map)->order('id Asc')->paginate($limit, false, ['page' => $page]);
        $dept_list_arr = $dept_list_query->toArray();
        //json数组
        $arr = array();
        $arr['code'] = 0;
        $arr['msg'] = "";
        $arr['count'] = $dept_list_arr['total'];
        $arr['per_page'] = $dept_list_arr['per_page'];
        $arr['current_page'] = $dept_list_arr['current_page'];
        $arr['data'] = $dept_list_arr['data'];
        $arr_json = json($arr);
        return $arr_json;

    }

/* 默认接收的数据格式
    {
      code: 0,
      msg: "",
      count: 1000,
      data: []
    } 
*/

功能的流程大概思路是:选取需要生成word的项目,勾选第一列的复选框,然后点击页面的生成按钮,弹出一个窗口,进行模板选择(因为需求中有三个word模板),选择了模板之后,跳转新的页面,将生成的doc的链接展示出来,供用户点击下载。

流程清楚之后,接下来就是数据问题了,首先要获取到所选取的那些项目的对应id,还有模板选择那个页面的模板id,一起传进后台控制里的生成word的函数。因为在经过了两个前端html页面这些参数才传到后台,所以打算把第一个页面的项目id以数组的形式,利用url传参方式get到第二个模板选择页面,然后在第二个页面利用js中window.location.search获取到第一个页面传过来的项目id,再将项目id和模板id一起传入后台。

那么第一个页面如何获取到项目id呢?利用layui的表格监听,将用户勾选的每一条项目对应的数据存起来。再利用循环,将每条项目的项目id取出来存到一个新的数组,这个数组就是需要传到下一个页面的参数,里面包含了用户勾选到的项目的id,不说那么多,直接上代码。

<!--第一个页面的生成按钮,点击后将打开弹窗(模板选择),并且将项目id传到下弹窗页面-->
<button class="layui-btn layui-btn-mini layui-bg-red" data-type="exportWords">生成文档</button>
    //JS代码
    layui.use(['form', 'layer'], function(){
        var $ = layui.jquery,
                layer = layui.layer,
                table = layui.table;

        //监听表格复选框选择
        table.on('checkbox(filtertb)', function(obj){
            var checkStatus = table.checkStatus('projtb');
            //将用户勾选的项目对应数据存起来
            clicked_project = checkStatus.data;
            //console.log(clicked_project);
        });

        var active = {
            //点击事件绑定
            exportWords:function () {
                //获取勾选的项目id,存入一个新数组,传入生成文档的子窗口
                var selected_project=new Array();
                for(var i=0;i<clicked_project.length;i++){
                    selected_project.push(clicked_project[i].id);
                }
                //检查是否有选择项目,没有选择的话要弹窗提示用户选择
                if(selected_project.length>0)
                {
                    layer.open({
                        shade: [0.8,'#000'],
                        shadeClose: true,
                        title: '请选择您要导出的模板文件',
                        type: 2,
                        area: ['460px', '500px'],
                        //向下个页面传入参数(已勾选的项目id)
                        content: '/chooseWordTemplate?pid='+selected_project,
                        btn: ['确定','取消']
                    });
                }else
                    {
                        layer.alert("您尚未选择需要导出word的项目!");
                    }

            }
        };

        //按钮触发
        $('.layui-btn').on('click', function(){
            var type = $(this).data('type');
            active[type] ? active[type].call(this) : '';
        });

    });
    /*
     * 控制器
     * 导出word模板选择
     * */
    public function chooseWordTemplate()
    {
        return $this->fetch('chooseWordTemplate');
    }

点击按钮之后,打开如下弹窗,进行模板选择

这是一个新的页面,就三个按钮,对应三个模板,利用data-type属性对每个按钮进行事件绑定

<body>
    <div class="layui-layer-content">
        <ul class="site-dir layui-layer-wrap" style="display:block;overflow:hidden">
            <li> <button class="layui-btn layui-btn-radius" data-type="template1">****表</button></li>
            <li> <button class="layui-btn layui-btn-radius layui-btn-normal" data-type="template2">****表</button></li>
            <li> <button class="layui-btn layui-btn-radius layui-btn-warm" data-type="template3">****表</button></li>
        </ul>
    </div>

    <script>
        layui.use(['form', 'layer'], function(){
            var $ = layui.jquery;
            //获取上一个页面传过来的pid参数,与当前页面的tep参数一起传到后台
            var url_pid = window.location.search;
            //console.log(url_pid);    //?pid=400,401,370
            var active= {
                template1:function ()
                {
                    layer.load(1);    //跳转空隙时加载lodaing
                    window.location.href='__ROOT__/admin/project/project2word'+url_pid+'&tep=1';
                },
                template2:function ()
                {
                    layer.load(1);
                    window.location.href='__ROOT__/admin/project/project2word'+url_pid+'&tep=2';
                },
                template3:function ()
                {
                    layer.load(1);
                    window.location.href='__ROOT__/admin/project/project2word'+url_pid+'&tep=3';
                }
            };

            $('.layui-btn').on('click', function(){
                var type = $(this).data('type');
                active[type] ? active[type].call(this) : '';
            });


        });
    </script>

</body>

到此前台部分就完成了。选择模板之后,此时会往后台传入项目id的数组和模板id。控制器方法获取到项目id和模板id之后,便可以着手开始生成word模板了,在这所谓的生成word,其实说白了就是先写好模板的html表格代码,赋值到一个变量中,然后写入文件中,以doc为后缀进行保存。接下来就是批量生成的问题,因为前台传过来的项目id有很多个,而且是字符串,因此要先取出来,将每一个id存进数组里面,再利用foreach循环数组,查询每一个id对应的数据,然后赋值到模板,写入文件,保存,最后echo出链接,大概就是这么一个思路。下面是控制器代码:

    /*
     * 将项目导出至word,文件存放位置public\uploads\Word文件夹
     * $pid勾选项目的id值,$tep选择需要导出的Word模板id
     * */
    public function project2word($pid,$tep){
        $word = new word;
        //取出pid将其转成数组,进行循环遍历
        $pidArray = explode(",",$pid);
        echo
        "<p style='text-align: center'>生成完毕!请点击下载</p> ";
        foreach ($pidArray as $value)
        {
            $item = $this->getProject($value)->toArray();
            /*表一*/
            $data1 = '<html xmlns:o="urn:schemas-microsoft-com:office:office"
//xmlns:w="urn:schemas-microsoft-com:office:word"
//xmlns="http://www.w3.org/TR/REC-html40">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
	<xml><w:WordDocument><w:View>Print</w:View></xml>
	<title>表一</title>
</head>
<body>
	<center><h2>****表</h2></center>
	<center>
	<table border="1" cellspacing="0" width="660">
		<tr height="40">
			<td align="center" width="150">项目名称</td>
			<td colspan="3" align="center">'.$item['name'].'</td>
		</tr>
		<tr height="40">
			<td align="center" width="150">预算金额(元)</td>
			<td align="center">'.$item['budget_total'].'</td>
			<td align="center" width="150">申请部门</td>
			<td align="center">'.$item['dept_name'].'</td>
		</tr>
		<tr height="40">
			<td align="center">备注</td>
			<td colspan="3"></td>
		</tr>
	</table>
	</center>
</body>
</html>';

        /*表二类似,代码省略*/
        $data2 = '';

        /*表三类似,代码省略*/
        $data3 = '';

            //以每天的日期为一个文件夹,当天生成的word会全部存在一个以日期命名的文件夹中
            $str = date('Ymd');
            //查询文件夹是否已经存在,如果不存在则创建
            $targetFolder = iconv("UTF-8", "GBK", ROOT_PATH."public/uploads/Word/".$str);
            if (!file_exists($targetFolder)){
                mkdir ($targetFolder,0777,true);
            }
            
            //取出tep,判断需要生成哪个模板
            switch ($tep)
            {
                case 1:
                    $name=ROOT_PATH."public/uploads/Word/".$str."/".$item['code']."tep1.doc";
                    $word->wirtefile($name,$data1);    //保存word并且结束
                    //echo出的链接供给用户点击下载,链接href是word文件所在位置
                    echo "<a href='/public/uploads/Word/".$str."/".$item['code']. "tep1.doc' target='_blank' style='color:rgba(30,173,160,0.72);'>【" .$item['name']."】***表</a><br>";
                    break;
                case 2:
                    $name=ROOT_PATH."public/uploads/Word/".$str."/".$item['code']."tep2.doc";
                    $word->wirtefile($name,$data2);    //保存word并且结束.
                    echo "<a href='/public/uploads/Word/".$str."/".$item['code']."tep2.doc' target='_blank' style='color:rgba(30,173,160,0.72);'>【".$item['name']."】****表</a><br>";
                    break;
                case 3:
                    $name=ROOT_PATH."public/uploads/Word/".$str."/".$item['code']."tep3.doc";
                    echo "<a href='/public/uploads/Word/".$str."/".$item['code']."tep3.doc' target='_blank' style='color:rgba(30,173,160,0.72);'>【".$item['name']."】***表</a><br>";
                    $word->wirtefile($name,$data3);    //保存word并且结束.
                    break;
            }
        }


    }

上面控制器代码中引用了word类中的writefile方法,Word.php代码如下:

<?php
namespace org;
class Word
{
    function wirtefile ($fn,$data)
    {
        $fp=fopen($fn,"w+");
        fwrite($fp,$data);
        fclose($fp);
    }
}
?>

至此功能就已经大体实现了。但是这样做其实性能方面会没有那么好,而且给用户的体验不太友好,用户的整个操作流程下来会感觉不太方便,接下来会对此功能进行评测以及代码的改进。文章会不定期更新。

2018/7/25更新

出于对使用这一功能的人群考虑,实际使用中用户并不需要一个一个去修改,而是仅仅负责分发给其他人,因此将生成的word打包成压缩包供用户下载。所以决定在原来的基础上,将每一次的批量生成都压缩一下,将压缩包放在存放word的同级目录下,zip名以日期加上时分秒命名,实现起来也很简单,php有专门的ZipArchive类,代码参考:koastal的博客,里面写的很详细,下面是项目修改后的代码

    /*
     * 将项目导出至word,文件存放位置public\uploads\Word文件夹
     * $pid勾选项目的id值,$tep选择需要导出的Word模板id
     * */
    public function project2word($pid,$tep){
        $word = new word;
        //取出pid将其转成数组,进行循环遍历
        $pidArray = explode(",",$pid);
        echo
        "<p style='text-align: center'>生成完毕!请点击下载</p> ";
        //创建以当天日期为名的文件夹,用来存放当天所有生成的word
        $str = date('Ymd');
        $targetFolder = iconv("UTF-8", "GBK", ROOT_PATH."public/uploads/Word/".$str);
        if (!file_exists($targetFolder)){
            mkdir ($targetFolder,0777,true);
        }
        //定义zip文件名及路径,创建并打开zip通道
        $zipStr = date('Ymdhis');
        $zipFilename = ROOT_PATH.'public/uploads/Word/'.$str.'/'.$zipStr.'.zip';
        $zip = new \ZipArchive();
        $zip->open($zipFilename,\ZipArchive::CREATE);

        foreach ($pidArray as $value)
        {
            $item = $this->getProject($value)->toArray();
            /*表一、二、三同上,代码省略*/
            $data1 = '';
            $data2 = '';
            $data3 = '';

            switch ($tep)
            {
                case 1:
                    $name=ROOT_PATH."public/uploads/Word/".$str."/".$item['code']."tep1.doc";
                    $word->wirtefile($name,$data1);    //保存word并且结束.
                    $zip->addFile($name,basename($name));    //添加到zip
                    echo "<a href='/public/uploads/Word/".$str."/".$item['code']. "tep1.doc' target='_blank' style='color:rgba(30,173,160,0.72);'>【" .$item['name']."】***表</a><br>";
                    break;
                case 2:
                    $name=ROOT_PATH."public/uploads/Word/".$str."/".$item['code']."tep2.doc";
                    $word->wirtefile($name,$data2);    //保存word并且结束.
                    $zip->addFile($name,basename($name));
                    echo "<a href='/public/uploads/Word/".$str."/".$item['code']."tep2.doc' target='_blank' style='color:rgba(30,173,160,0.72);'>【".$item['name']."】****表</a><br>";
                    break;
                case 3:
                    $name=ROOT_PATH."public/uploads/Word/".$str."/".$item['code']."tep3.doc";
                    echo "<a href='/public/uploads/Word/".$str."/".$item['code']."tep3.doc' target='_blank' style='color:rgba(30,173,160,0.72);'>【".$item['name']."】***表</a><br>";
                    $word->wirtefile($name,$data3);    //保存word并且结束.
                    $zip->addFile($name,basename($name));
                    break;
            }
        }
        //关闭压缩包
        $zip->close();
        echo "<a href='/public/uploads/Word/".$str."/".$zipStr.".zip' target='_blank' style='color:rgba(30,173,160,0.72);'>压缩包</a><br>";
    }

压缩功能完成。

2018/7/26更新

之前在生成模板之前选择的项目id,是通过url参数get到下一个页面的,和下个页面的模板id一起get到后台。这么做虽然简单,但是也有一个致命的问题,就是当用户一次性选择项目过多时,比如几百条几千条,url就会变得很长,但是实际上浏览器对url长度是有上限规定的,因此利用get请求把参数传到下一个页面是不可能了。那不能用get请求,又不能把项目id先传到后台,因为后台函数需要同时接受项目id和模板id,怎么办? 一开始是想着能不能把模板id通过session存起来,在第二个页面访问。但是后来一想,用post不是更简单吗?可是转眼一想,不可能把项目id直接post到下一个页面吧?查了一下js是没办法获取post过来的数据的。好,那既然post不到下一个页面,那就看看layui有没有提供类似的功能。结果翻了一下官方文档,发现还真的有,就是在主页面利用layer.open打开的子窗口之间,是可以实现数据互通的,主页面可以访问子页面参数、方法,反之子页面也能访问主页面参数、方法。那既然如此,我何不在子页面去获取主页面的项目id,然后利用表单直接和模板id一起post到后台呢?锦上添花的是,layui提供了form表单的初始化赋值的功能,这么以来就省事多了,也不用去考虑主页面子页面谁去访问谁的东西了,直接在主页面的layer.open里面的success回调对子页面的表单进行赋值,完美解决了之前的各种问题,下面贴上部分修改后的代码。

主页面的layer.open以及对子窗口的表单初始化赋值:

        exportWords:function () {
                //获取勾选的项目信息,提取其中的项目id为一个新数组,方便之后传入生成文档的子窗口
                var selected_project=new Array();
                for(var i=0;i<clicked_project.length;i++){
                    selected_project.push(clicked_project[i].id);
                }
                //检查是否有选择项目,没有选择的话要弹窗提示用户选择
                if(selected_project.length>0)
                {
                    layer.open({
                        shade: [0.8,'#000'],
                        shadeClose: true,
                        title: '请选择您要导出的模板文件',
                        type: 2,
                        area: ['460px', '500px'],
                        //向下个页面传入参数(已勾选的项目id)
                        content: '__ROOT__/admin/project/chooseWordTemplate',
                        btn: '取消',
                        success: function (layero, index) {
                            //找到子窗口的DOM
                            var body = layui.layer.getChildFrame('body',index);
                            //利用对表单的值的初始化,将项目id传到子页面
                            body.find(".pid").val(selected_project);
                        }
                    });
                }else
                    {
                        layer.alert("您尚未选择需要导出word的项目!");
                    }

            }

子页面即chooseWordTemplate.html原来是三个按钮,现在直接重新写个表单,原来的三个按钮的模板选择变成了下拉框的模板选择,还有一个输入框,输入框里面的值是从父页面就已经初始化好的。chooseWordTemplate.html代码如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>选择word模板类型</title>
    <meta name="renderer" content="webkit">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <link rel="stylesheet" href="__JS__/layui/css/layui.css" media="all">
    <link rel="stylesheet" href="__CSS__/font-awesome.min.css">
    <link rel="stylesheet" href="__CSS__/admin.css">
    <script src="__JS__/layui2/layui.all.js"></script>
    <!--[if lt IE 9]>
    <script src="__CSS__/html5shiv.min.js"></script>
    <script src="__CSS__/respond.min.js"></script>
    <![endif]-->
    <style>
        *{
            margin: 0;
            padding: 0;
        }
        body{
            background-color: #f8f8f8;
        }
        .content{
            width: 440px;
            background-color: #f8f8f8;
            margin-top: 100px;
        }
    </style>
</head>
<body>
<div class="content">
<form class="layui-form" method="post" action="__ROOT__/admin/project/project2word">
    <div class="layui-form-item">
        <label class="layui-form-label">所选项目</label>
        <div class="layui-input-block">
            <input type="text" name="projid" autocomplete="off" required  lay-verify="required" class="layui-input pid layui-disabled">
        </div>
    </div>
    <!--下拉选择模板-->
    <div class="layui-form-item">
        <label class="layui-form-label">模板选择</label>
        <div class="layui-input-block">
            <select name="template" lay-filter="demo" lay-verify="selectTemplate">
                <option value="0">请选择</option>
                <option value="1">****表</option>
                <option value="2">****表</option>
                <option value="3">****表</option>
            </select>
        </div>
    </div>
    <!--提交表单-->
    <div class="layui-form-item">
        <div class="layui-input-block">
            <button class="layui-btn layui-btn-normal" lay-submit lay-filter="*">立即生成</button>
        </div>
    </div>
</form>
</div>

<script>
    layui.use('form', function(){
        var form = layui.form; //只有执行了这一步,部分表单元素才会自动修饰成功
        //但是,如果你的HTML是动态生成的,自动渲染就会失效
        //因此你需要在相应的地方,执行下述方法来手动渲染,跟这类似的还有 element.init();
        form.render();       
        //监听下拉框
        form.on('select(demo)', function(data){
            var tep = data.value;
//            console.log(tep); //得到被选中的值
        });
        //验证下拉框
        form.verify({
            selectTemplate:function (value) {
                if(value == 0)
                {
                    return '请先选择模板!';
                }
            }
        });     
        //监听表单提交
        form.on('submit(*)', function(){
            layer.load(1);
        });
    });
</script>

</body>
</html>

页面效果如下(为了防止用户误操作,我把项目id的文本框禁用了,这里注意一下,layui的禁用是有两种,一种只是样式上的禁用,另一种是表单里禁用,即不会提交):

 然后是后台控制器代码修改:

    /*
     * 将项目导出至word,文件存放位置public\uploads\Word文件夹
     * $pid勾选项目的id值,$tep选择需要导出的Word模板id
     * */
    public function project2word(){
        $word = new word;
        $request = Request::instance();
/*      //取出pid
        $pidParam = $request->only('projid');   //获取参数是一个数组,注意是整体为一个value,所以要把相应value转成String类型,方便后面拆分再行转成数组
        $pid = implode($pidParam);      //数组$pidParam转成String类型的$pid    */
        //取出pid第二种方法
        $pidParam = $request->post('projid');       //这种获取方法直接就是string类型了
        $pidArray = explode(",",$pidParam);     //将其转成数组,进行循环遍历
        //取出tep,方便后面判断需要生成哪个模板
        $tepParam = $request->post('template');
        
        //后续操作不变...
    }

url过长问题到这就解决了。

猜你喜欢

转载自blog.csdn.net/bibifeng/article/details/81139416