写这系列的文章目的无他,仅仅是用以记录自己写项目的一些思路和心得,在开发过程中代码是以实现为首要目的,写得并不会非常美观实用,代码会在后期不断调试更改,文章也会进行不定期更新。未经作者允许请勿转载。
最新更新:2018/7/26
功能需求:选取页面表格信息,批量生成word文件
相关技术点
首先是用一个layui表格对数据进行展示,html页面写个table标签,给个id
按照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,不说那么多,直接上代码。
//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;i0)
{
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属性对每个按钮进行事件绑定
到此前台部分就完成了。选择模板之后,此时会往后台传入项目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
"生成完毕!请点击下载
";
foreach ($pidArray as $value)
{
$item = $this->getProject($value)->toArray();
/*表一*/
$data1 = '
Print
表一
****表
项目名称
'.$item['name'].'
预算金额(元)
'.$item['budget_total'].'
申请部门
'.$item['dept_name'].'
备注
';
/*表二类似,代码省略*/
$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 "【" .$item['name']."】***表
";
break;
case 2:
$name=ROOT_PATH."public/uploads/Word/".$str."/".$item['code']."tep2.doc";
$word->wirtefile($name,$data2); //保存word并且结束.
echo "【".$item['name']."】****表
";
break;
case 3:
$name=ROOT_PATH."public/uploads/Word/".$str."/".$item['code']."tep3.doc";
echo "【".$item['name']."】***表
";
$word->wirtefile($name,$data3); //保存word并且结束.
break;
}
}
}
上面控制器代码中引用了word类中的writefile方法,Word.php代码如下:
至此功能就已经大体实现了。但是这样做其实性能方面会没有那么好,而且给用户的体验不太友好,用户的整个操作流程下来会感觉不太方便,接下来会对此功能进行评测以及代码的改进。文章会不定期更新。
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
"生成完毕!请点击下载
";
//创建以当天日期为名的文件夹,用来存放当天所有生成的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 "【" .$item['name']."】***表
";
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 "【".$item['name']."】****表
";
break;
case 3:
$name=ROOT_PATH."public/uploads/Word/".$str."/".$item['code']."tep3.doc";
echo "【".$item['name']."】***表
";
$word->wirtefile($name,$data3); //保存word并且结束.
$zip->addFile($name,basename($name));
break;
}
}
//关闭压缩包
$zip->close();
echo "压缩包
";
}
压缩功能完成。
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;i0)
{
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代码如下:
选择word模板类型
页面效果如下(为了防止用户误操作,我把项目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过长问题到这就解决了。