没有不值得去解决的问题,也没有不值得去学习的技术!

在 Yii 2.0 中导出资源列表为 Excel 文件的实现流程

打开文件:我的任务-2019-09-20-19-39-11.xlsx,符合预期
1、我的任务的资源列表,计划在资源列表头部添加一个导出按钮,如图1
我的任务的资源列表,计划在资源列表头部添加一个导出按钮
图1
2、在 GitHub 中搜索:yii2 export excel,其结果:moonlandsoft/yii2-phpexcel、kartik-v/yii2-export,如图2
在 GitHub 中搜索:yii2 export excel,其结果:moonlandsoft/yii2-phpexcel、kartik-v/yii2-export
图2
3、经过对比分析,主要从支持的导出格式的丰富性、后续可能需要支持导入的扩展性上考虑,最终决定选择:moonlandsoft/yii2-phpexcel moonlandsoft/yii2-phpexcel的导出格式:Xls、Xlsx、Xml、Ods、Slk、Gnumeric、Csv、Html kartik-v/yii2-export的导出格式:Xls、Xlsx、Csv、Html、Txt、Pdf 4、基于 Composer 安装此扩展,执行如下命令:,注:由于网络问题,总计执行了 3 次,第 3 次为翻墙之后再执行,如图3
基于 Composer 安装此扩展,执行如下命令:,注:由于网络问题,总计执行了 3 次,第 3 次为翻墙之后再执行
图3


PS E:\wwwroot\pcs-api-feature-task-statistics-print> composer require --prefer-dist moonlandsoft/yii2-phpexcel "*"
Content-Length mismatch, received 16128 bytes out of the expected 3759703
https://asset-packagist.org could not be fully loaded, package information was loaded from the local cache and may be ou
t of date
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
https://asset-packagist.org could not be fully loaded, package information was loaded from the local cache and may be ou
t of date
Package operations: 5 installs, 0 updates, 0 removals
  - Installing markbaker/matrix (1.1.4): Downloading (100%)
  - Installing markbaker/complex (1.4.7): Downloading (100%)
  - Installing psr/simple-cache (1.0.1): Downloading (100%)
  - Installing phpoffice/phpspreadsheet (1.9.0): Downloading (100%)
PS E:\wwwroot\pcs-api-feature-task-statistics-print> composer require --prefer-dist moonlandsoft/yii2-phpexcel "*"
The "https://asset-packagist.org/p/provider-latest/49c795e8ff9b455adc45e73e45b8fa84fd39ce4761894526a7d05b455390a960.json
" file could not be downloaded: failed to open stream: HTTP request failed!
https://asset-packagist.org could not be fully loaded, package information was loaded from the local cache and may be ou
t of date
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Content-Length mismatch, received 16128 bytes out of the expected 3759943
https://asset-packagist.org could not be fully loaded, package information was loaded from the local cache and may be ou
t of date
Package operations: 2 installs, 0 updates, 0 removals
  - Installing phpoffice/phpspreadsheet (1.9.0):
PS E:\wwwroot\pcs-api-feature-task-statistics-print> composer require --prefer-dist moonlandsoft/yii2-phpexcel "*"
Content-Length mismatch, received 16128 bytes out of the expected 3759943
https://asset-packagist.org could not be fully loaded, package information was loaded from the local cache and may be ou
t of date
./composer.json has been updated
Loading composer repositories with package information
https://asset-packagist.org could not be fully loaded, package information was loaded from the local cache and may be ou
t of date
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing phpoffice/phpspreadsheet (1.9.0): Loading from cache
  - Installing moonlandsoft/yii2-phpexcel (2.0.0): Downloading (100%)
phpoffice/phpspreadsheet suggests installing mpdf/mpdf (Option for rendering PDF with PDF Writer)
phpoffice/phpspreadsheet suggests installing dompdf/dompdf (Option for rendering PDF with PDF Writer)
phpoffice/phpspreadsheet suggests installing tecnickcom/tcpdf (Option for rendering PDF with PDF Writer)
phpoffice/phpspreadsheet suggests installing jpgraph/jpgraph (Option for rendering charts, or including charts with PDF
or HTML Writers)
Writing lock file
Generating autoload files


5、用于导出数据的新功能,如果您熟悉 yii gridview,则可以使用此功能。这与 gridview 数据列相同。数组模式中的列有效参数是 ‘attribute’,’header’,’format’,’value’和footer(TODO)。字符串模式有效布局中的列是’attribute:format:header:footer(TODO)’。如图4
用于导出数据的新功能,如果您熟悉 yii gridview,则可以使用此功能。这与 gridview 数据列相同。数组模式中的列有效参数是 'attribute','header','format','value'和footer(TODO)。字符串模式有效布局中的列是'attribute:format:header:footer(TODO)'。
图4
6、复制 \api\rests\plan_task\IndexAction.php 至 \api\rests\plan_task\ExportAction.php,由于导出任务为全部记录,因此设置每页资源数量为资源总数


        // 设置每页资源数量为资源总数
        $count = $query->count();
        $requestParams['per-page'] = $count;


7、编辑 \api\rests\plan_task\Serializer.php,判断操作ID,如果为 export,则导出,新增请求参数,file_type:文件类型


use moonland\phpexcel\Excel;
use yii\helpers\ArrayHelper;

    /**
     * Serializes a data provider.
     * @param DataProviderInterface $dataProvider
     * @return array|null the array representation of the data provider.
     * @throws UnprocessableEntityHttpException
     */
    protected function serializeDataProvider($dataProvider)
    {
        // 导出文件
        $actionId = Yii::$app->controller->action->id;
        if ($actionId == 'export') {

            $fileType = isset($requestParams['file_type']) ? $requestParams['file_type'] : 'xlsx';
            $allowedFileTypes = ['xlsx'];
            // 判断文件类型范围
            if (!in_array($fileType, $allowedFileTypes)) {
                throw new UnprocessableEntityHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '226823'), ['file_types' => implode(",", $allowedFileTypes)])), 226823);
            }

            // 导出文件的名称
            $fileName = Yii::t('application', '326001') . '-' . date("Y-m-d-H-i-s");
            // 导出文件的格式,将字符串的首字母转换为大写
            $format = ucfirst($fileType);

            Excel::export([
                'models' => $result['items'],
                'asAttachment' => true,
                'fileName' => $fileName,
                'format' => $format,
                'columns' => [
                    [
                        'attribute' => 'id',
                        'header' => Yii::t('model/plan-task-export-param', 'ID'),
                    ],
                    [
                        'attribute' => 'is_deferred',
                        'header' => Yii::t('model/plan-task-export-param', 'Is Deferred'),
                        'value' => function($model) {
                            return $model['is_deferred'] == PlanTask::IS_DEFERRED_NO ? Yii::t('model/plan-task-export-param', 'No') : Yii::t('model/plan-task-export-param', 'Yes');
                        },
                    ],
                    [
                        'attribute' => 'title',
                        'header' => Yii::t('model/plan-task-export-param', 'Title'),
                    ],
                    [
                        'attribute' => 'task_info',
                        'header' => Yii::t('model/plan-task-export-param', 'Task Info'),
                    ],
                    [
                        'attribute' => 'config_task_name',
                        'header' => Yii::t('model/plan-task-export-param', 'Config Task Name'),
                    ],
                    [
                        'attribute' => 'config_column_name',
                        'header' => Yii::t('model/plan-task-export-param', 'Config Column Name'),
                    ],
                    [
                        'attribute' => 'create_name',
                        'header' => Yii::t('model/plan-task-export-param', 'Create Name'),
                    ],
                    [
                        'attribute' => 'exec_name',
                        'header' => Yii::t('model/plan-task-export-param', 'Exec Name'),
                    ],
                    [
                        'attribute' => 'plan_task_attended_users',
                        'header' => Yii::t('model/plan-task-export-param', 'Attended Names'),
                        'value' => function($model) {
                            $planTaskAttendedUserNames = ArrayHelper::getColumn($model['plan_task_attended_users'], 'relation_user_name');
                            return implode(",", $planTaskAttendedUserNames);
                        },
                    ],
                    [
                        'attribute' => 'sort_order',
                        'header' => Yii::t('model/plan-task-export-param', 'Sort Order'),
                    ],
                    [
                        'attribute' => 'place',
                        'header' => Yii::t('model/plan-task-export-param', 'Place'),
                    ],
                    [
                        'attribute' => 'occur_at',
                        'header' => Yii::t('model/plan-task-export-param', 'Occur At'),
                        'format' => ['date', 'php:Y-m-d H:i:s'],
                    ],
                    [
                        'attribute' => 'ended_at',
                        'header' => Yii::t('model/plan-task-export-param', 'Ended At'),
                        'format' => ['date', 'php:Y-m-d H:i:s'],
                    ],
                    [
                        'attribute' => 'task_location_status',
                        'header' => Yii::t('model/plan-task-export-param', 'Task Location Status'),
                        'value' => function($model) {
                            return $model['task_location_status'] == PlanTask::PLAN_TASK_LOCATION_STATUS_OFF ? Yii::t('model/plan-task-export-param', 'Off') : Yii::t('model/plan-task-export-param', 'On');
                        },
                    ],
                    [
                        'attribute' => 'is_united',
                        'header' => Yii::t('model/plan-task-export-param', 'Is United'),
                        'value' => function($model) {
                            return $model['task_location_status'] == PlanTask::IS_DELETED_NO ? Yii::t('model/plan-task-export-param', 'No') : Yii::t('model/plan-task-export-param', 'Yes');
                        },
                    ],
                    [
                        'attribute' => 'prev_status',
                        'header' => Yii::t('model/plan-task-export-param', 'Prev Status'),
                        'value' => function($model) {
                            if ($model['prev_status'] == PlanTask::STATUS_NOT_STARTED) {
                                $prevStatusValue = Yii::t('model/plan-task-export-param', 'Not Started');
                            } elseif ($model['prev_status'] == PlanTask::STATUS_STARTED) {
                                $prevStatusValue = Yii::t('model/plan-task-export-param', 'Started');
                            } elseif ($model['prev_status'] == PlanTask::STATUS_COMPLETED) {
                                $prevStatusValue = Yii::t('model/plan-task-export-param', 'Completed');
                            } else {
                                $prevStatusValue = Yii::t('model/plan-task-export-param', 'Disabled');
                            }
                            return $prevStatusValue;
                        },
                    ],
                    [
                        'attribute' => 'status',
                        'header' => Yii::t('model/plan-task-export-param', 'Status'),
                        'value' => function($model) {
                            if ($model['status'] == PlanTask::STATUS_NOT_STARTED) {
                                $statusValue = Yii::t('model/plan-task-export-param', 'Not Started');
                            } elseif ($model['status'] == PlanTask::STATUS_STARTED) {
                                $statusValue = Yii::t('model/plan-task-export-param', 'Started');
                            } elseif ($model['status'] == PlanTask::STATUS_COMPLETED) {
                                $statusValue = Yii::t('model/plan-task-export-param', 'Completed');
                            } else {
                                $statusValue = Yii::t('model/plan-task-export-param', 'Disabled');
                            }
                            return $statusValue;
                        },
                    ],
                    [
                        'attribute' => 'plan_task_steps',
                        'header' => Yii::t('model/plan-task-export-param', 'Task Steps'),
                        'value' => function($model) {
                            $planTaskStepNames = ArrayHelper::getColumn($model['plan_task_steps'], 'step_name');
                            return implode(",", $planTaskStepNames);
                        },
                    ],
                    [
                        'attribute' => 'is_not_isolated',
                        'header' => Yii::t('model/plan-task-export-param', 'Is Not Isolated'),
                        'value' => function($model) {
                            return $model['is_not_isolated'] == PlanTask::IS_NOT_ISOLATED_NO ? Yii::t('model/plan-task-export-param', 'No') : Yii::t('model/plan-task-export-param', 'Yes');
                        },
                    ],
                    [
                        'attribute' => 'created_at',
                        'header' => Yii::t('model/plan-task-export-param', 'Created At'),
                        'format' => ['date', 'php:Y-m-d H:i:s'],
                    ],
                    [
                        'attribute' => 'updated_at',
                        'header' => Yii::t('model/plan-task-export-param', 'Updated At'),
                        'format' => ['date', 'php:Y-m-d H:i:s'],
                    ],
                    [
                        'attribute' => 'deleted_at',
                        'header' => Yii::t('model/plan-task-export-param', 'Deleted At'),
                        'format' => ['date', 'php:Y-m-d H:i:s'],
                    ],
                ],
            ]);
        }

        if ($pagination !== false) {
            return ['code' => 10000, 'message' => Yii::t('success', '126007'), 'data' => array_merge($result, $this->serializePagination($pagination))];
        }

        return ['code' => 10000, 'message' => Yii::t('success', '126007'), 'data' => $result];
    }




8、新增模型的语言包文件 \common\messages\en-US\model\plan-task-export-param.php
<pre class="wp-block-syntaxhighlighter-code">

<?php /** * Created by PhpStorm. * User: Qiang Wang * Date: 2019/09/20 * Time: 17:31 */ return [ 'ID' => 'ID',
    'Is Deferred' => 'Is Deferred',
    'Title' => 'Title',
    'Task Info' => 'Task Info',
    'Config Task Name' => 'Config Task Name',
    'Config Column Name' => 'Config Column Name',
    'Create Name' => 'Create Name',
    'Exec Name' => 'Exec Name',
    'Attended Names' => 'Attended Names',
    'Sort Order' => 'Sort Order',
    'Place' => 'Place',
    'Occur At' => 'Occur At',
    'Ended At' => 'Ended At',
    'Task Location Status' => 'Task Location Status',
    'Off' => 'Off',
    'On' => 'On',
    'Is United' => 'Is United',
    'Prev Status' => 'Prev Status',
    'Status' => 'Status',
    'Disabled' => 'Disabled',
    'Not Started' => 'Not Started',
    'Started' => 'Started',
    'Completed' => 'Completed',
    'Task Steps' => 'Task Steps',
    'Is Not Isolated' => 'Is Not Isolated',
    'No' => 'No',
    'Yes' => 'Yes',
    'Created At' => 'Created At',
    'Updated At' => 'Updated At',
    'Deleted At' => 'Deleted At',
];


</pre>
\common\messages\zh-CN\model\plan-task-export-param.php
<pre class="wp-block-syntaxhighlighter-code">

<?php /** * Created by PhpStorm. * User: Qiang Wang * Date: 2019/09/20 * Time: 15:47 */ return [ 'ID' => '编号',
    'Is Deferred' => '是否已延期',
    'Title' => '任务名称',
    'Task Info' => '任务内容',
    'Config Task Name' => '任务类型',
    'Config Column Name' => '任务栏目',
    'Create Name' => '创建人',
    'Exec Name' => '负责人',
    'Attended Names' => '参与人',
    'Sort Order' => '排序',
    'Place' => '地点',
    'Occur At' => '开始时间',
    'Ended At' => '结束时间',
    'Task Location Status' => '任务定位状态',
    'Off' => '禁用',
    'On' => '启用',
    'Is United' => '是否联合',
    'Prev Status' => '上一状态',
    'Status' => '状态',
    'Disabled' => '禁用',
    'Not Started' => '未认领',
    'Started' => '已认领',
    'Completed' => '已完成',
    'Task Steps' => '任务步骤',
    'Is Not Isolated' => '是否跨租户',
    'No' => '否',
    'Yes' => '是',
    'Created At' => '创建时间',
    'Updated At' => '更新时间',
    'Deleted At' => '删除时间',
];


</pre>
9、在浏览器中打开网址:http://api.pcs-api.localhost/v1/plan-tasks/export ,成功下载文件:我的任务-2019-09-20-19-39-11.xlsx,如图5
在浏览器中打开网址:http://api.pcs-api.localhost/v1/plan-tasks/export ,成功下载文件:我的任务-2019-09-20-19-39-11.xlsx
图5
10、打开文件:我的任务-2019-09-20-19-39-11.xlsx,符合预期,如图6
打开文件:我的任务-2019-09-20-19-39-11.xlsx,符合预期
图6
11、在 Postman 中打开网址:http://api.pcs-api.localhost/v1/plan-tasks/export ,点击 “Send” 按钮,响应乱码,如图7
在 Postman 中打开网址:http://api.pcs-api.localhost/v1/plan-tasks/export ,点击 "Send" 按钮,响应乱码
图7
12、在 Postman 中打开网址:http://api.pcs-api.localhost/v1/plan-tasks/export ,点击 “Send and download” 按钮,下载名为:response.txt 的文件,且文件中无内容,如图8
在 Postman 中打开网址:http://api.pcs-api.localhost/v1/plan-tasks/export ,点击 "Send and download" 按钮,下载名为:response.txt 的文件,且文件中无内容
图8
13、在 Postman 中设置 Headers , Accept:application/xml; version=0.0,打开网址:http://api.pcs-api.localhost/v1/plan-tasks/export ,点击 “Send and download” 按钮,下载名为:*-2019-09-26-10-44-40.xlsx 的文件,文件名中的中文乱码,但文件中的内容符合预期,如图9
在 Postman 中设置 Headers , Accept:application/xml; version=0.0,打开网址:http://api.pcs-api.localhost/v1/plan-tasks/export ,点击 "Send and download" 按钮,下载名为:*-2019-09-26-10-44-40.xlsx 的文件,文件名中的中文乱码,但文件中的内容符合预期
图9
14、在前端(基于 Ant Design)页面中,请求网址:http://api.pcs-api.localhost/v1/plan-tasks/export ,响应乱码,并未下载文件,如图10
在前端(基于 Ant Design)页面中,请求网址:http://api.pcs-api.localhost/v1/plan-tasks/export ,响应乱码,并未下载文件
图10
15、初步分析原因,应该在于前端代码对于响应头并未实现完整地解析,因此,即使服务端响应要求前端下载文件,但是前端仍然基于 json 格式来解析数据,如图11
初步分析原因,应该在于前端代码对于响应头并未实现完整地解析,因此,即使服务端响应要求前端下载文件,但是前端仍然基于 json 格式来解析数据
图11
16、最终决定基于纯粹的链接来实现,使用 download 属性,此属性指示浏览器下载 URL 而不是导航到它,因此将提示用户将其保存为本地文件。,点击 “导出” 按钮,下载名为:我的任务-2019-09-26-10-44-40.xlsx 的文件,如图12
最终决定基于纯粹的链接来实现,使用 download 属性,此属性指示浏览器下载 URL 而不是导航到它,因此将提示用户将其保存为本地文件。,点击 "导出" 按钮,下载名为:我的任务-2019-09-26-10-44-40.xlsx 的文件
图12
<pre class="wp-block-syntaxhighlighter-code">

<a href='http://api.pcs-api.localhost/v1/plan-tasks/export' download>导出</a>

</pre>

PHP / Laravel / Yii2 老项目维护与长期技术支持

如果你的 PHP / Laravel / Yii2 项目已经上线,但遇到原开发离职、Bug 长期无人修复、接口不稳定、性能下降、代码难以接手等问题,可以联系我做一次远程技术排查。

适合以下情况:
✅ 老旧 PHP 系统无人维护
✅ Laravel / Yii2 项目 Bug 修复
✅ 后台管理系统小功能迭代
✅ RESTful API 接口排查
✅ MySQL / Redis / Nginx 性能问题
✅ 长期远程兼职维护

可先从一次小问题开始:
✅ 线上报错排查
✅ 接口异常分析
✅ 慢查询与性能瓶颈定位
✅ 代码结构初步评估
✅ 部署环境与日志检查

如需咨询,请联系我,并注明:PHP 维护咨询

联系方式:
Telegram:@shuijingwan
微信:13980074657
邮箱:shuijingwanwq@gmail.com

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理