php_xlsxwriter替换php_execl导出excel导出复杂表头

最近公司业务有一些需要导出多级表头的业务需求,百度之后,没有很好的应用demo,然后接触了XLSXWriter,就想着一劳永逸,写个工具类,示例只有三级,但是不止于三级

应用举例

比如说我们需要这样一个表头


image

1.XLSXWriter应用分析

根据XLSXWriter工具类的示例

我们需要插入三行数据作为表头

image

然后再合并掉对应的单元格

image

2.工具类编写

调试的注释啥的,都在里面,有兴趣的自己可以看一下。需要放在和xlsxwriter.class.php同一目录下使用

<?php
require_once 'web/libraries/XLSXwriter.php';
class PHPExeclCore
{
    private $headerData = array();
    private $headerWidthSize = 0;
    private $headerHeightSize = 0;


    private $colDefalutFormat = 'string';// 列默认数据格式
    private $colDefalutWidth = '10';// 列默认宽度

    private $header = array();
    private $headerWidth = array();

    // 数据字段
    private $dataFields = array();

    // 标题行默认样式
    private $headerDefalutStyle = array(
        'halign' => 'center',//水平居中
        'valign' => 'center',//竖直居中
        'font-style' => 'bold',
        'font-size' => 12,
        'border' => 'top,bottom,left,right'
        //'fill'=>'#fff'// 背景色
        // 边框有点问题不能用
        // 高度也不行 用font-size撑开);
    );
    // 标题行处理数组
    /**
     * $headerRow = array(
     * array('content' =>['Merge Cells Example','','','',''],'style'=>[['halign'=>'center'],array(),array(),array(),array()]),
     * array('content' =>['姓名', '吃饭', '', '考勤', ''],'style'=>[['halign'=>'center','valign'=>'center'],['halign'=>'center'],[],['halign'=>'center'],[]]),
     * array('content' =>['', '上午', '下午', '上午', '下午'],'style'=>[[],['halign'=>'center'],[],[],[]]),
     * );
     */
    private $headerRow = array(0 => array('content' => array(), 'style' => array()));
    private $headerMargin = array();

    // 数据行
    private $dataRow = array();
    // 现在够用了,先不做了
    private $dataStyle = array();


    public function __construct()
    {
    }

    public function __get($name)
    {
        if (isset($this->$name)) {
            return $this->$name;
        } else {
            return null;
        }
        // TODO: Implement __get() method.
    }

    /**
     * 设置表头
     * @param array $head
     */
    public function setHeader($header = array())
    {
        $this->headerData = $header;
        // 设置表头尺寸
        $this->setHeaderSize($this->headerData);
        // 生成rows 和 margin 设置样式
        $this->dealHeader($this->headerData);
    }


    public function setData($data = array())
    {
        foreach ($data as $d) {
            $row = array();
            foreach ($this->dataFields as $field) {
                $value = isset($d[$field]) ? $d[$field] : '';
                $row[] = $value;
            }
            $this->dataRow[] = $row;
        }
    }


    /**
     * 导出
     * @param string $filename
     * @param string $sheet
     */
    public function writeToStdOut($filename = 'test', $sheet = 'Sheet1')
    {
        //$header = ['字段','标题','宽度','类型']
        $filename .= '.xlsx';
        $writer = new \XLSXWriter();
        $writer->writeSheetHeader($sheet, $this->header, $col_options = array('suppress_row' => true, 'widths' => $this->headerWidth));
        foreach ($this->headerRow as $row)
            $writer->writeSheetRow($sheet, $row['content'], $row['style']);
        foreach ($this->headerMargin as $margin) {
            $writer->markMergedCell($sheet, $margin['startRow'], $margin['startCol'], $margin['endRow'], $margin['endCol']);
        }
        foreach ($this->dataRow as $data)
            $writer->writeSheetRow($sheet, $data);
        header('Content-disposition: attachment; filename="' . \XLSXWriter::sanitize_filename($filename));
        header("Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        header('Content-Transfer-Encoding: binary');
        header('Cache-Control: must-revalidate');
        header('Pragma: public');
        $writer->writeToStdOut();
    }


    /**
     *  生成rows 和 margin 设置样式
     * @param $headerData
     */
    private function dealHeader($headerData)
    {

        $this->recursionSetHeaderRowsAndMargin($headerData, 0, 0, $this->headerDefalutStyle);
    }

    /**
     * 递归设置表头行数据,和合并表格数据源
     * @param $headerData
     * @param $row
     * @param $startCol
     * // 换个思路,填坑赋值
     */
    private function recursionSetHeaderRowsAndMargin($headerData, $row, $col, $defStyle = array())
    {
        $heightSize = $this->headerHeightSize;

        $startRow = $row;
        $startCol = $col;
        $content = array();//标题
        $style = array();//样式

        foreach ($headerData as $single) {
            $marginCol = isset($single['marginCol']) ? $single['marginCol'] : 1;
            // 合并
            // 是否有子节点
            $hasChildren = (isset($single['children']) && !empty($single['children'])) ? true : false;
            // 有子节点合并一行,没有子节点直接把剩下行都算上
            if ($hasChildren) {
                $marginRow = 1;
            } else {
                // 一共3行 当前第1行需要合并1,2两行
                $marginRow = $heightSize - $startRow;
            }


            $style = isset($single['style']) ? array_merge($defStyle, $single['style']) : $defStyle;
            // 如果不是合并一行一列就加入合并单元格的数组,同时每个合并的单元格都加样式(主要是方便边框)
            if ($marginCol != 1 || $marginRow != 1) {
                //0+2-1=1 合并了0,1两个单元格 end是1
                $endCol = $startCol + $marginCol - 1;
                // 当前第1行需要合并2两行,目标单元格行号1+2-1
                $endRow = $startRow + $marginRow - 1;
                $this->headerMargin[] = array(
                    'startRow' => $startRow,
                    'startCol' => $startCol,
                    'endRow' => $endRow,
                    'endCol' => $endCol);

                if (isset($style['border'])) {
                    $s = array('border' => $style['border']);
                    for ($r = 0; $r < $marginRow; $r++) {
                        $row = $startRow + $r;
                        for ($c = 0; $c < $marginCol; $c++) {
                            $col = $startCol + $c;
                            $this->setHeaderRowCell($row, $col, 'style', $s);
                        }
                    }
                }

            }
            $this->setHeaderRowCell($startRow, $startCol, 'style', $style);


            $content = isset($single['title']) ? $single['title'] : '';
            $this->setHeaderRowCell($startRow, $startCol, 'content', $content);


            //处理子节点
            if ($hasChildren) {
                $this->recursionSetHeaderRowsAndMargin($single['children'], $startRow + 1, $startCol, $style);
            }
            $startCol += $marginCol;

        }


    }

    //
    //
    /**
     * 设置表头单元格尺寸
     * 同事设置列的数据类型和宽度
     * @param $header
     */
    private function setHeaderSize(&$headerData)
    {

        list($w, $h) = $this->recursionCalSize($headerData);
        $this->headerWidthSize = $w;
        $this->headerHeightSize = $h;
        $this->setHeaderRow($w, $h);


    }

    /**
     * 初始化表头行,占坑
     * @param $w
     * @param $h
     */
    private function setHeaderRow($w, $h)
    {
        $data = array();
        $content = array();
        $style = array();
        while (true) {
            $w--;
            $content[] = '';
            $style[] = array();
            if ($w <= 0) break;
        }

        while (true) {
            $h--;
            $data[] = array('content' => $content, 'style' => $style);

            if ($h <= 0)
                break;
        }

        $this->headerRow = $data;
        unset($content);
        unset($style);
        unset($data);

    }

    /**
     * 修改表头行的数据
     * @param $col
     * @param $row
     * @param $filed
     * @param $value
     */
    private function setHeaderRowCell($row, $col, $key, $value)
    {

        if (isset($this->headerRow[$row][$key][$col])) {
            $this->headerRow[$row][$key][$col] = $value;
        } else {
            return false;
        }
    }

    /**
     *
     * @param $headerData
     * @return array
     */
    private function recursionCalSize(&$headerData)
    {
        $w = 0; // 根节点++
        $h = 0; // 是子类高度的最大值
        $childHeightArr = array();
        foreach ($headerData as &$single) {
            $singleH = 1;
            // 没有子节点设置为空数组

            if ((isset($single['children']) && !empty($single['children']))) {
                list($cw, $ch) = $this->recursionCalSize($single['children'], $h);
                $w += $cw;
                $singleH += $ch;
                $single['marginCol'] = $cw;

            } else {

                //设置表头需要要参数
                //字段类型
                $type = isset($single['format']) ? $single['format'] : $this->colDefalutFormat;
                // 字段宽度
                $width = isset($single['width']) ? $single['width'] : $this->colDefalutWidth;
                $this->header[] = $type;
                $this->headerWidth[] = $width;
                // data的键
                $field = isset($single['field']) ? $single['field'] : '';
                $this->dataFields[] = $field;

                $w++;
                $singleH = 1;
            }
            $childHeightArr[] = $singleH;
        }
        $h = max($childHeightArr);
        return array($w, $h);
    }
}

3.使用示例


<?php
/**
 * 表头配置为一个数组
 * 根节点参数
 * title  必填,表头的内容
 * field  根节点列的内容要显示的data字段,默认为空[则这一列不显示数据]
 * width  根节点的列宽,默认为10
 * format 根节点的列在excel中的显示类型,默认为string,其他的我没有测试,因为我实际的应用过程中,String就够用了,如果要使用其他类型,请参照xlsxwriter示例自行修改测试
 *      format和width的默认值可以再tool里面修改
 *      colDefalutFormat = 'string';// 列默认数据格式
 *      colDefalutWidth = '10';// 列默认宽度
 *
 * 一般节点的参数
 * title  必填,表头的内容
 * style=> 应用的样式,会应用到根节点
 *      默样式可以修改headerDefalutStyle的配置
 * children 必填,子节点
 */
$headerConfig =
                array(
                    array('title' => '存量项目', 'field' => 'project_name','format'=>'string'),//根节点
                    array('title' => '业态', 'field' => 'layout','format'=>'string'),//根节点
                    array('title' => '标签', 'field' => 'project_label','format'=>'string'),//根节点
                    array('title' => '委托管理公司', 'field' => 'business_area_company','format'=>'string','width'=>'20'),//根节点
//                    ['title' => '姓名', 'field' => 'name','width'=>'40'],//根节点
                    // 一般节点
                    array('title' => '建筑面积',
                        'children' => array(
                            array('title' => '6月30日', 'field' => 'build_area_sum_1'),
                            array('title' => '9月30日', 'field' => 'build_area_sum_2'),
                        )
                    ),
                    array(
                        'title' => '实际收入',
                        'children' => array(
                            array('title' => '6月30日', 'field' => 'rent_income_sum_1'),
                            array('title' => '9月30日', 'field' => 'rent_income_sum_2'),
                        )
                    ),
                    array('title' => '单价',
                        'children' => array(
                            array('title' => '6月30日', 'field' => 'avg_price_1'),
                            array('title' => '9月30日', 'field' => 'avg_price_2'),
                            array('title' => '差值', 'field' => 'avg_price_diff')
                        )
                    ),
                    array('title' => '空置率(%)',
                        'children' => array(
                            array('title' => '6月30日', 'field' => 'rent_rate_1'),
                            array('title' => '9月30日', 'field' => 'rent_rate_2'),
                            array('title' => '差值', 'field' => 'rent_rate_diff')
                        )
                    )
                );
$data = [
            ['project_name' => '张三', 'project_name' => '8:23', 'build_area_sum_1' => '9:23', 'avg_price_1' => '9:23'],
            ['project_name' => '李四', 'project_name' => '8:24', 'build_area_sum_1' => '9:24', 'avg_price_1' => '9:24'],
        ];

include_once("../extend/PHP_XLSXWriter-master/XLSXWriterMultiHeaderTool.php");
$tool = new \XLSXWriterMultiHeaderTool();
$tool->setHeader($headerConfig);
$tool->setData($data);
$filename = date('_YmdHis');
$tool->writeToStdOut($filename);

4.效果

因为data里面没有对应的字段,或者节点设置的时候没有指定的field,所以有几列是空的,表头的颜色是因为写了style的fill属性

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,064评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,606评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,011评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,550评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,465评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,919评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,428评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,075评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,208评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,185评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,191评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,914评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,482评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,585评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,825评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,194评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,703评论 2 339

推荐阅读更多精彩内容