From 08e2b883f2be1d1bcdc17957863968b2fa8e89ab Mon Sep 17 00:00:00 2001 From: vartrue <1175398345@qq.com> Date: Fri, 12 Jul 2024 12:00:22 +0800 Subject: [PATCH] feat:v1.0 --- .editorconfig | 20 ++ .gitattributes | 13 + .gitignore | 11 + .php-cs-fixer.php | 49 +++ LICENSE | 21 ++ README.md | 53 +++ composer.json | 67 ++++ phpunit.xml.dist | 21 ++ src/ExcelAbstract.php | 536 +++++++++++++++++++++++++++++ src/ExcelManager.php | 48 +++ src/ProgressTrait.php | 316 +++++++++++++++++ src/behaviors/ExcelLogBehavior.php | 89 +++++ src/data/export/Column.php | 69 ++++ src/data/export/ExportConfig.php | 194 +++++++++++ src/data/export/ExportData.php | 52 +++ src/data/export/Sheet.php | 150 ++++++++ src/data/import/ImportConfig.php | 190 ++++++++++ src/data/import/ImportData.php | 60 ++++ src/data/import/Sheet.php | 145 ++++++++ src/drivers/csv/Excel.php | 29 ++ src/drivers/xlswriter/Excel.php | 189 ++++++++++ src/events/ErrorEvent.php | 29 ++ src/events/ExportEvent.php | 20 ++ src/events/ImportEvent.php | 18 + src/events/ProgressEvent.php | 21 ++ src/exceptions/ExcelException.php | 10 + src/jobs/ExportJob.php | 34 ++ src/jobs/ImportJob.php | 36 ++ src/jobs/base/BaseJob.php | 28 ++ src/utils/Helper.php | 72 ++++ tests/ExportTest.php | 79 +++++ tests/ImportTest.php | 41 +++ tests/config/TestExportConfig.php | 99 ++++++ tests/config/TestImportConfig.php | 38 ++ tests/files/ceshi.xlsx | Bin 0 -> 8697 bytes 35 files changed, 2847 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .php-cs-fixer.php create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 src/ExcelAbstract.php create mode 100644 src/ExcelManager.php create mode 100644 src/ProgressTrait.php create mode 100644 src/behaviors/ExcelLogBehavior.php create mode 100644 src/data/export/Column.php create mode 100644 src/data/export/ExportConfig.php create mode 100644 src/data/export/ExportData.php create mode 100644 src/data/export/Sheet.php create mode 100644 src/data/import/ImportConfig.php create mode 100644 src/data/import/ImportData.php create mode 100644 src/data/import/Sheet.php create mode 100644 src/drivers/csv/Excel.php create mode 100644 src/drivers/xlswriter/Excel.php create mode 100644 src/events/ErrorEvent.php create mode 100644 src/events/ExportEvent.php create mode 100644 src/events/ImportEvent.php create mode 100644 src/events/ProgressEvent.php create mode 100644 src/exceptions/ExcelException.php create mode 100644 src/jobs/ExportJob.php create mode 100644 src/jobs/ImportJob.php create mode 100644 src/jobs/base/BaseJob.php create mode 100644 src/utils/Helper.php create mode 100644 tests/ExportTest.php create mode 100644 tests/ImportTest.php create mode 100644 tests/config/TestExportConfig.php create mode 100644 tests/config/TestImportConfig.php create mode 100644 tests/files/ceshi.xlsx diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..df55cd7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = false + +[*.{vue,js,scss}] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9e631da --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +* text=auto + +/tests export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.scrutinizer.yml export-ignore +.travis.yml export-ignore +phpunit.php export-ignore +phpunit.xml.dist export-ignore +phpunit.xml export-ignore +.php_cs export-ignore +.php-cs-fixer.cache +.phpunit.result.cache diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a68851 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.idea +*.DS_Store +/vendor +/coverage +/hooks +sftp-config.json +composer.lock +.subsplit +.php_cs.cache +cghooks.lock + diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..8695228 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,49 @@ +setRules([ + '@PSR12' => true, + 'binary_operator_spaces' => true, + 'blank_line_after_opening_tag' => true, + 'compact_nullable_typehint' => true, + 'declare_equal_normalize' => true, + 'lowercase_cast' => true, + 'lowercase_static_reference' => true, + 'new_with_braces' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_leading_import_slash' => true, + 'no_whitespace_in_blank_line' => true, + 'no_unused_imports' => true, + 'ordered_class_elements' => [ + 'order' => [ + 'use_trait', + ], + ], + 'ordered_imports' => [ + 'imports_order' => [ + 'class', + 'function', + 'const', + ], + 'sort_algorithm' => 'none', + ], + 'return_type_declaration' => true, + 'short_scalar_cast' => true, + 'single_blank_line_before_namespace' => true, + 'single_trait_insert_per_statement' => true, + 'ternary_operator_spaces' => true, + 'unary_operator_spaces' => true, + 'visibility_required' => [ + 'elements' => [ + 'const', + 'method', + 'property', + ], + ], + ]) + ->setFinder( + PhpCsFixer\Finder::create() + ->exclude('vendor') + ->in([__DIR__.'/src/', __DIR__.'/tests/']) + ) +; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a022a3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 vartruexuan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f564a93 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# yii-excel + +[![php](https://img.shields.io/badge/php-%3E=8.2-brightgreen.svg?maxAge=2592000)](https://github.com/php/php-src) +[![Latest Stable Version](https://img.shields.io/packagist/v/vartruexuan/yii-excel)](https://packagist.org/packages/vartruexuan/yii-excel) +[![Total Downloads](https://img.shields.io/packagist/dt/vartruexuan/yii-excel)](https://packagist.org/packages/vartruexuan/yii-excel) +[![License](https://img.shields.io/packagist/l/vartruexuan/yii-excel)](https://github.com/vartruexuan/yii-excel) + +# 概述 +excel 导入导出,相关文档处理组件 + +## 组件能力 + +- [x] 导入、导出excel +- [x] 支持异步操作,进度构建,信息构建 +- [x] 支持 `xlswriter` +- [ ] 支持 `csv` +- [ ] ... +# 安装 +```shell +composer require vartruexuan/yii-excel +``` +## 配置 + +## 使用 +### 配置 +- 配置组件 `components` +```php +[ + 'components' => [ + // excel组件 + 'excel' => [ + 'class' => \vartruexuan\excel\drivers\xlswriter\Excel::class, + 'fileSystem' => 'filesystem', // 文件管理组件 + 'redis' => 'redis', // redis + 'queue' => 'queue', // 队列组件 + // 日志行为类 + 'as log' => \vartruexuan\excel\behaviors\ExcelLogBehavior::class + ] + ] +] +``` +### 导出 +```php + +``` +### 导入 + +```php + +``` +## License + +MIT diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a97ce0a --- /dev/null +++ b/composer.json @@ -0,0 +1,67 @@ +{ + "name": "vartruexuan/yii-excel", + "description": "excel导入导出组件", + "homepage": "https://github.com/vartruexuan/yii-excel", + "license": "MIT", + "authors": [ + { + "name": "guozhaoxuan", + "email": "guozhaoxuanx@163.com" + } + ], + "require": { + "php": ">=8.2", + "overtrue/http": "^1.2", + "creocoder/yii2-flysystem": "^1.1", + "ramsey/uuid": "*" + }, + "require-dev": { + "brainmaestro/composer-git-hooks": "^2.8", + "friendsofphp/php-cs-fixer": "^3.0", + "mockery/mockery": "^1.2", + "phpunit/phpunit": "^9.0", + "vimeo/psalm": "^4.10", + "jetbrains/phpstorm-attributes": "^1.0" + }, + "autoload": { + "files": [ + ], + "psr-4": { + "vartruexuan\\excel\\": "src" + } + }, + "extra": { + "hooks": { + "pre-commit": [ + "composer check-style", + "composer test" + ], + "pre-push": [ + "composer test", + "composer check-style" + ] + }, + "vartruexuan": { + "config": "vartruexuan\\excel\\ConfigProvider" + } + }, + "scripts": { + "post-update-cmd": [ + ], + "post-merge": "", + "post-install-cmd": [ + ], + "cghooks": "vendor/bin/cghooks", + "check-style": "php-cs-fixer fix --using-cache=no --diff --dry-run --ansi", + "fix-style": "php-cs-fixer fix --using-cache=no --ansi", + "test": "phpunit --colors", + "psalm": "psalm --show-info=true --no-cache", + "psalm-fix": "psalm --no-cache --alter --issues=MissingReturnType,MissingParamType" + }, + "scripts-descriptions": { + "test": "Run all tests.", + "check-style": "Run style checks (only dry run - no fixing!).", + "fix-style": "Run style checks and fix violations." + } + +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..e47284c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,21 @@ + + + + + ./tests/ + + + + + src/ + + + diff --git a/src/ExcelAbstract.php b/src/ExcelAbstract.php new file mode 100644 index 0000000..72697f9 --- /dev/null +++ b/src/ExcelAbstract.php @@ -0,0 +1,536 @@ +redis instanceof Connection) { + $this->redis = Instance::ensure($this->redis, '\yii\redis\Connection'); + } + + if (!$this->queue instanceof Queue) { + $this->queue = Instance::ensure($this->queue, '\yii\queue\Queue'); + } + + if (!$this->fileSystem instanceof Filesystem) { + $this->fileSystem = Instance::ensure($this->fileSystem, '\creocoder\flysystem\Filesystem'); + } + } + + /** + * 导出 + * + * @param ExportConfig $config 导出配置 + * + * @return string 返回文件地址 + */ + abstract protected function exportExcel(ExportConfig $config): string; + + /** + * 导入操作 + * + * @param ImportConfig $config 导入配置 + * @return ImportData 导入数据 + */ + abstract protected function importExcel(ImportConfig $config): ImportData; + + + /** + * 导出 + * + * @param ExportConfig $config + * @return void + */ + public function export(ExportConfig $config) + { + $config = $this->formatExportConfig($config); + try { + $token = $config->getToken(); + $exportData = new ExportData([ + 'token' => $token, + ]); + + $event = new ExportEvent([ + 'exportConfig' => $config, + ]); + $this->trigger(self::ENVENT_BEFORE_EXPORT, $event); + + $this->initProgressInfo($token); + // 异步 + if ($config->getIsAsync()) { + // 异步不支持直接输出 + if ($config->getOutPutType() == ExportConfig::OUT_PUT_TYPE_OUT) { + throw new ExcelException('Async does not support output type ExportConfig::OUT_PUT_TYPE_OUT'); + } + $exportData->queueId = $this->pushExportQueue($config->setToken($token)); + return $exportData; + } + $filePath = $this->exportExcel($config); + + // 文件输出 + if ($config->outPutType == ExportConfig::OUT_PUT_TYPE_UPLOAD) { + // 上传文件 + if (!$this->fileSystem->writeStream($config->path, fopen($filePath, 'r+'))) { + throw new ExcelException('upload file fail'); + } + if (method_exists($config, 'getUrl')) { + $url = $config->getUrl($config->path); + } else { + $url = $this->getFileSystemUrl($config->path); + } + $exportData->setPath($url); + $this->setProgressInfo($token, null, self::PROGRESS_STATUS_END, [ + 'url' => $url, + ]); + } else if ($config->outPutType == ExportConfig::OUT_PUT_TYPE_LOCAL) { + if (copy($filePath, $config->getPath()) === false) { + throw new ExcelException('copy file fail'); + } + $exportData->setPath($config->getPath()); + $this->setProgressInfo($token, null, self::PROGRESS_STATUS_END); + } else { + // Set Header + header("Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + header('Content-Disposition: attachment;filename="' . urlencode($config->getFileName()) . '"'); + header('Content-Length: ' . filesize($filePath)); + header('Content-Transfer-Encoding: binary'); + header('Cache-Control: must-revalidate'); + header('Cache-Control: max-age=0'); + header('Pragma: public'); + + ob_clean(); + flush(); + // 直接输出 + if (copy($filePath, 'php://output') === false) { + throw new ExcelException('copy file fail'); + } + } + $this->trigger(self::ENVENT_AFTER_EXPORT, $event); + // 删除临时文件 + @$this->deleteFile($filePath); + $this->setProgressInfo($token, null, self::PROGRESS_STATUS_END); + } catch (ExcelException $excelException) { + // 失败 + $this->setProgressInfo($token, null, self::PROGRESS_STATUS_FAIL); + $this->setProgressMessage($token, $excelException->getMessage()); + $this->trigger(self::EVENT_ERROR, new ErrorEvent([ + 'config' => $config, + 'exception' => $excelException, + ])); + throw $excelException; + } catch (\Throwable $exception) { + $this->setProgressInfo($token, null, self::PROGRESS_STATUS_FAIL); + $this->setProgressMessage($token, '导出异常'); + $this->trigger(self::EVENT_ERROR, new ErrorEvent([ + 'config' => $config, + 'exception' => $exception, + ])); + \Yii::error($exception->getMessage(), 'vartruexuan/excel/export'); + throw $exception; + } + + return $exportData; + } + + /** + * 导入 + * + * @param ImportConfig $config + * @return void + */ + public function import(ImportConfig $config) + { + $config = $this->formatImportConfig($config); + try { + $token = $config->getToken(); + $importData = new ImportData([ + 'token' => $token, + ]); + + $event = new ImportEvent([ + 'importConfig' => $config, + ]); + $this->trigger(self::ENVENT_BEFORE_IMPORT, $event); + + $this->initProgressInfo($token); + // 异步 + if ($config->getIsAsync()) { + $importData->queueId = $this->pushImportQueue($config->setToken($token)); + return $importData; + } + + // 构建临时文件 + $filePath = $this->getTempFileName(); + $config->setTempPath($filePath); + if (!$this->isRemoteUrl($config->getPath())) { + // 本地文件 + if (!is_file($config->getPath())) { + throw new ExcelException('path not exists'); + } + if (!FileHelper::copy($config->getPath(), $filePath)) { + throw new ExcelException('file copy error'); + } + } else { + // 远程文件 + if (!$this->downloadFile($config->getPath(), $filePath)) { + throw new ExcelException('file download error'); + } + } + + // 执行导入 + $importData = $this->importExcel($config); + + $this->trigger(self::ENVENT_AFTER_IMPORT, $event); + + $this->setProgressInfo($token, null, self::PROGRESS_STATUS_END); + + } catch (ExcelException $excelException) { + // 失败 + $this->setProgressInfo($token, null, self::PROGRESS_STATUS_FAIL); + $this->setProgressMessage($token, $excelException->getMessage()); + $this->trigger(self::EVENT_ERROR, new ErrorEvent([ + 'config' => $config, + 'exception' => $excelException, + ])); + throw $excelException; + } catch (\Throwable $exception) { + $this->setProgressInfo($token, null, self::PROGRESS_STATUS_FAIL); + $this->setProgressMessage($token, '导入异常'); + $this->trigger(self::EVENT_ERROR, new ErrorEvent([ + 'config' => $config, + 'exception' => $exception, + ])); + \Yii::error($exception->getMessage(), 'vartruexuan/excel/import'); + throw $exception; + } + + return $importData; + } + + + /** + * 导入异步队列 + * + * @param ImportConfig $config + * @return string|null + */ + protected function pushImportQueue(ImportConfig $config) + { + return $this->queue->push(new ImportJob([ + 'importConfig' => $config, + 'componentId' => $this->getCommandId(), + ])); + } + + /** + * 导出异步队列 + * + * @param ExportConfig $config + * @return string|null + */ + protected function pushExportQueue(ExportConfig $config) + { + return $this->queue->push(new ExportJob([ + 'exportConfig' => $config, + 'componentId' => $this->getCommandId(), + ])); + } + + + /** + * 格式化导入配置数据 + * + * @param ImportConfig $config + * @return ImportConfig + */ + protected function formatImportConfig(ImportConfig $config) + { + if (!$config->getToken()) { + $config->setToken($this->buildToken()); + } + // 格式化页码 + $sheets = $config->getSheets(); + foreach ($sheets as &$sheet) { + if (!$sheet instanceof Sheet) { + $sheet = new Sheet($sheet); + } + } + $config->setSheets($sheets); + return $config; + } + + /** + * 格式化导出配置 + * + * @param ExportConfig $config + * @return ExportConfig + */ + protected function formatExportConfig(ExportConfig $config) + { + + if (!$config->getToken()) { + $config->setToken($this->buildToken()); + } + return $config; + } + + + /** + * 获取临时目录 + * + * @return string + */ + protected function getTempDir() + { + return sys_get_temp_dir(); + } + + /** + * 构建一个临时文件 + * + * @return false|string + */ + protected function getTempFileName($prefix = 'ex_') + { + return tempnam($this->getTempDir(), $prefix); + } + + + /** + * 删除文件 + * + * @param $filepath + * @return bool + */ + protected function deleteFile($filepath) + { + return Helper::instance()->deleteFile($filepath); + } + + /** + * 远程文件地址 + * + * @param $remotePath + * @param $filepath + * @return void + */ + protected function downloadFile($remotePath, $filePath) + { + return Helper::instance()->downloadFile($remotePath, $filePath); + } + + /** + * 是否是url地址 + * + * @param $url + * @return void + */ + protected function isRemoteUrl($url) + { + return Helper::instance()->isUrl($url); + } + + /** + * 构建token + * + * @return string + * @throws \yii\base\Exception + */ + protected function buildToken() + { + // return \Yii::$app->security->generateRandomString(); + return Helper::instance()->uuid4(); + } + + /** + * 行回调 + * + * @param callable $callback + * @param array $row + * @param ExcelAbstract $excel + * @param ImportConfig $config + * @param string $sheetName + * @param bool $isSetProgress + * @param int $status + * @return mixed + */ + protected function importRowCallback(callable $callback, array $row, ExcelAbstract $excel, ImportConfig $config, string $sheetName, $isSetProgress = true, $status = self::PROGRESS_STATUS_PROCESS) + { + $errorMessage = null; + try { + $result = call_user_func($callback, $row, $excel, $config); + } catch (\Throwable $exception) { + $errorMessage = $exception->getMessage(); + } + // 设置进度信息 + if ($isSetProgress) { + $prgress = 1; + if (!$errorMessage) { + $success = 1; + } else { + $fail = 1; + } + $this->setSheetProgress($config->getToken(), $sheetName, $status, $prgress ?? 0, $success ?? 0, $fail ?? 0); + } + return $result ?? null; + } + + + /** + * 导出数据回调 + * + * @param callable $callback 回调 + * @param int $page 页码 + * @param int $pageCount 限制每页数量 + * @param ?int $count 总数 + * @param $param 额外参数 + * @param string $token + * @param string $sheetName + * @return mixed + */ + protected function exportDataFunc(callable $callback, ExportConfig $config, int $page, int $pageCount, ?int $count, string $sheetName) + { + return call_user_func($callback, $config, $page, $pageCount, $count, $sheetName, $this); + } + + + /** + * 获取命令ID(组件ID) + * + * @return string command id + * @throws + */ + protected function getCommandId() + { + foreach (\Yii::$app->getComponents(false) as $id => $component) { + if ($component === $this) { + return Inflector::camel2id($id); + } + } + throw new InvalidConfigException('excel must be an application component.'); + } + + /** + * 获取文件地址 + * + * @param $url + * @return void + */ + protected function getFileSystemUrl(string $path, bool $isSign = false) + { + // cdn + if ($this->fileSystem->cdn) { + return $this->fileSystem->getAdapter()->applyPathPrefix($path); + } + + // 签名地址 + if ($isSign) { + return $this->fileSystem->getAdapter()->getUrl($path); + } + // 原始地址 + $scheme = $this->fileSystem->scheme ? $this->fileSystem->scheme : 'http'; + $sourcePath = $this->fileSystem->getAdapter()->getSourcePath($path); + return "{$scheme}://{$sourcePath}"; + } + +} \ No newline at end of file diff --git a/src/ExcelManager.php b/src/ExcelManager.php new file mode 100644 index 0000000..1bd8979 --- /dev/null +++ b/src/ExcelManager.php @@ -0,0 +1,48 @@ +has($componentId)) { + throw new ExcelException("component id error {$this->componentId}"); + } + $component = \Yii::$app->{$componentId}; + if (!$component instanceof ExcelAbstract) { + throw new ExcelException("component is not ExcelAbstract instance"); + } + return $component; + } + $defaultDriverClass = $defaultDriverClass ?: $this->getDefaultDriverClass(); + return $defaultDriverClass::instance(); + } + + /** + * 获取默认驱动 + * + * @return string + */ + public function getDefaultDriverClass() + { + return '\vartruexuan\excel\drivers\xlswriter\Excel'; + } + +} \ No newline at end of file diff --git a/src/ProgressTrait.php b/src/ProgressTrait.php new file mode 100644 index 0000000..44558ab --- /dev/null +++ b/src/ProgressTrait.php @@ -0,0 +1,316 @@ +setProgressInfo($token, [], self::PROGRESS_STATUS_AWAIT); + } + + /** + * 设置进度信息 + * + * @param string $token token + * @param array|null $sheetList 页信息 + * @param bool $isAwait 是否在等待处理 + * @param bool|null $status 进度状态 + * @param array|null $data 数据 + * @return mixed + */ + public function setProgressInfo(string $token, ?array $sheetList = null, ?int $status = self::PROGRESS_STATUS_PROCESS, ?array $data = null) + { + // 初始化进度信息 + $info = $this->getProgressInfo($token); + $info = $info ?? []; + $info['status'] = $status; + if ($sheetList !== null) { + $info['sheetList'] = $sheetList; + } + if ($data) { + $info['data'] = $data; + } + $this->redis->set($this->getProgressCacheKeyByInfo($token), Json::encode($info)); + $this->resetProgressTime($token); + } + + /** + * 获取进度信息 + * + * @param string $token + * @return mixed|null + */ + public function getProgressInfo(string $token) + { + $info = $this->redis->get($this->getProgressCacheKeyByInfo($token)); + return Json::decode($info, true); + } + + /** + * 查询进度 + * + * @param $token + * @return void + */ + public function getProgress($token) + { + $info = $this->getProgressInfo($token); + if (!$info) { + return false; + } + $progressInfo = [ + 'sheetList' => [], + 'all' => [ + 'total' => 0, + 'progress' => 0, + 'success' => 0, + 'fail' => 0, + 'status' => (int)($info['status'] ?? self::PROGRESS_STATUS_AWAIT), + ], + 'data' => $info['data'] ?? null, + ]; + foreach ($info['sheetList'] ?? [] as $key => $sheetName) { + $sheetProgress = $this->getSheetProgress($token, $sheetName); + $progressInfo['sheetList'][$sheetName] = $sheetProgress; + $progressInfo['all']['total'] += $sheetProgress['total'] ?? 0; + $progressInfo['all']['progress'] += $sheetProgress['progress'] ?? 0; + $progressInfo['all']['success'] += $sheetProgress['success'] ?? 0; + $progressInfo['all']['fail'] += $sheetProgress['fail'] ?? 0; + } + + + return $progressInfo; + } + + + /** + * 初始化进度信息 + * + * @param string $token + * @param string $sheetName + * @param int $total + * @return void + */ + public function initSheetProgress(string $token, string $sheetName, int $total = 0) + { + $key = $this->getProgressCacheKeyBySheet($token, $sheetName); + $this->redis->hmset($key, [ + 'total' => $total, + 'progress' => 0, + 'success' => 0, + 'fail' => 0, + 'status' => self::PROGRESS_STATUS_AWAIT, + ]); + $this->resetProgressTime($token); + + } + + /** + * 设置页码进度 + * + * @param string $token + * @param string $sheetName + * @param bool $status + * @param int $progress + * @param int $success + * @param int $fail + * @return void + */ + public function setSheetProgress(string $token, string $sheetName, ?int $status = null, int $progress = 0, int $success = 0, int $fail = 0) + { + $key = $this->getProgressCacheKeyBySheet($token, $sheetName); + if ($progress > 0) { + $this->redis->hincrby($key, 'progress', $progress); + } + if ($success > 0) { + $this->redis->hincrby($key, 'success', $success); + } + if ($fail > 0) { + $this->redis->hincrby($key, 'fail', $fail); + } + if ($status !== null) { + $this->redis->hmset($key, 'status', $status); + } + $this->resetProgressTime($token); + + $progressInfo = $this->getProgressInfo($token); + // 触发事件 + $this->trigger(ExcelAbstract::EVENT_AFTER_PROGRESS, new ProgressEvent([ + 'progressInfo' => $progressInfo, + 'token' => $token, + ])); + } + + + /** + * 设置消息(队列方式) + * + * @return void + */ + public function setProgressMessage($token, $message) + { + $key = $this->getProgressCacheKeyByMessage($token); + $this->redis->lpush($key, $message); + $this->resetProgressTime($token); + } + + /** + * 获取进度消息 + * + * @param $token + * @param int $limit 限制数量 + * @return array + */ + public function getProgressMessage($token, int $limit = 30) + { + $key = $this->getProgressCacheKeyByMessage($token); + $messages = []; + for ($i = 0; $i < $limit; $i++) { + if ($message = $this->redis->rpop($key)) { + $messages[] = $message; + } + } + return $messages; + } + + /** + * 重置key时间 + * + * @param $key + * @param $time + * @return mixed + */ + public function resetProgressTime($token, $time = null) + { + $time = $time ? $time : $this->progressExpireTime; + $this->redis->expire($this->getProgressCacheKeyByInfo($token), $time); + $this->redis->expire($this->getProgressCacheKeyByMessage($token), $time); + $progressInfo = $this->getProgressInfo($token); + + foreach ($progressInfo['sheetList'] ?? [] as $sheetName) { + $this->redis->expire($this->getProgressCacheKeyBySheet($token, $sheetName), $time); + } + + } + + + /** + * 获取页码进度信息 + * + * @param string $token + * @param string $sheetName + * @return mixed + */ + public function getSheetProgress(string $token, string $sheetName) + { + $result = $this->redis->hgetall($this->getProgressCacheKeyBySheet($token, $sheetName)); + return [ + 'total' => intval($result['total'] ?? 0), + 'progress' => intval($result['progress'] ?? 0), + 'success' => intval($result['success'] ?? 0), + 'fail' => intval($result['fail'] ?? 0), + 'status' => intval($result['status'] ?? self::PROGRESS_STATUS_AWAIT), + ]; + } + + + /** + * 获取进度信息key + * + * @param $token + * @return string + */ + protected function getProgressCacheKeyByInfo($token) + { + return sprintf(self::PROGRESS_KEY_INFO, $token); + } + + /** + * 获取页码进度key + * + * @param $token + * @return string + */ + protected function getProgressCacheKeyBySheet($token, $sheetName) + { + return sprintf(self::PROGRESS_KEY_SHEET, $token, $sheetName); + } + + /** + * 错误信息 + * + * @param $token + * @return string + */ + protected function getProgressCacheKeyByMessage($token) + { + return sprintf(self::PROGRESS_KEY_MESSAGE, $token); + } + + /** + * 计算状态 + * + * @param $sheetStatusList + * @return void + */ + protected function getAllStatus($sheetStatusList) + { + $allStatus = self::PROGRESS_STATUS_AWAIT; + $sheetStatusList = array_unique($sheetStatusList ?? []); + $sheetStatusCount = count($sheetStatusList); + if ($sheetStatusCount == 1) { + $allStatus = $sheetStatusList[0]; + } else if ($sheetStatusCount > 1) { + if (!in_array(self::PROGRESS_STATUS_FAIL, $sheetStatusList)) { + $allStatus = self::PROGRESS_STATUS_FAIL; + } else { + $allStatus = self::PROGRESS_STATUS_PROCESS; + } + } else { + $allStatus = self::PROGRESS_STATUS_AWAIT; + } + return $allStatus; + } + +} \ No newline at end of file diff --git a/src/behaviors/ExcelLogBehavior.php b/src/behaviors/ExcelLogBehavior.php new file mode 100644 index 0000000..8ad4ee9 --- /dev/null +++ b/src/behaviors/ExcelLogBehavior.php @@ -0,0 +1,89 @@ + 'afterExport', + ExcelAbstract::ENVENT_BEFORE_EXPORT => 'beforeExport', + ExcelAbstract::ENVENT_AFTER_IMPORT => 'afterImport', + ExcelAbstract::ENVENT_BEFORE_IMPORT => 'beforeImport', + ExcelAbstract::EVENT_AFTER_PROGRESS => 'afterProgress', + ExcelAbstract::EVENT_ERROR => 'error', + ]; + } + + /** + * 导出之前 + * + * @param ExportEvent $event + * @return void + */ + public function beforeExport(ExportEvent $event) + { + + } + + /** + * 导出之后 + * + * @param ExportEvent $event + * @return void + * @throws ServiceException + */ + public function afterExport(ExportEvent $event) + { + + } + + /** + * 导入之前 + * + * @param ImportEvent $event + * @return void + */ + public function beforeImport(ImportEvent $event) + { + } + + /** + * 导入之后 + * + * @param ImportEvent $event + * @return void + */ + public function afterImport(ImportEvent $event) + { + } + + /** + * 设置进度 + * + * @param ProgressEvent $event + * @return void + */ + public function afterProgress(ProgressEvent $event) + { + + } + + /** + * 异常 + * + * @param ErrorEvent $event + * @return void + */ + public function error(ErrorEvent $event) + { + } + + +} \ No newline at end of file diff --git a/src/data/export/Column.php b/src/data/export/Column.php new file mode 100644 index 0000000..8cdbec7 --- /dev/null +++ b/src/data/export/Column.php @@ -0,0 +1,69 @@ +path; + } + + /** + * 获取页码配置 + * + * @return Sheet[] + */ + public function getSheets() + { + return $this->sheets; + } + + /** + * 设置页 + * + * @param $sheets + * @return $this + */ + public function setSheets($sheets) + { + $this->sheets = $sheets; + return $this; + } + + /** + * 获取输出类型 + * + * @return string + */ + public function getOutPutType() + { + return $this->outPutType; + } + + /** + * 获取文件名 + * + * @return string + */ + public function getFileName() + { + return basename($this->getPath()); + } + + + /** + * 获取导出参数 + * + * @return null + */ + public function getParam() + { + return $this->param; + } + + /** + * 是否异步 + * + * @return bool + */ + public function getIsAsync() + { + return $this->isAsync; + } + + /** + * 获取token + * + * @return void + */ + public function getToken() + { + return $this->token; + } + + + /** + * 设置token + * + * @param $token + * @return $this + */ + public function setToken($token) + { + $this->token = $token; + return $this; + } + + /** + * 获取所有页名 + * + * @return void + */ + public function getSheetNames() + { + return array_map(function (Sheet $n) { + return $n->name; + }, $this->getSheets()); + } + + + /** + * 序列化 + * + * @return array + */ + public function __serialize(): array + { + return [ + 'token' => $this->getToken(), + 'path' => $this->getPath(), + 'isAsync' => $this->getIsAsync(), + 'outPutType' => $this->getOutPutType(), + 'param' => $this->getParam(), + ]; + } + +} \ No newline at end of file diff --git a/src/data/export/ExportData.php b/src/data/export/ExportData.php new file mode 100644 index 0000000..339837e --- /dev/null +++ b/src/data/export/ExportData.php @@ -0,0 +1,52 @@ +path = $path; + return $this; + } + + + public function setToken($token) + { + $this->token = $token; + return $this; + } + + + public function setQueueId($queueId) + { + $this->queueId = $queueId; + return $this; + } + +} \ No newline at end of file diff --git a/src/data/export/Sheet.php b/src/data/export/Sheet.php new file mode 100644 index 0000000..216861b --- /dev/null +++ b/src/data/export/Sheet.php @@ -0,0 +1,150 @@ +columns; + } + + /** + * 获取页码名 + * + * @return void + */ + public function getName() + { + return $this->name; + } + + + /** + * 获取数据 + * + * @return void + */ + public function getData() + { + return $this->data; + } + + /** + * 获取数据数量 + * + * @return int + */ + public function getCount() + { + return $this->count; + } + + /** + * 获取每页导出数量 + * + * @return int + */ + public function getPageCount() + { + return $this->pageCount; + + } + + /** + * 获取头部信息 + * + * @return array|string[] + */ + public function getHeaders() + { + return array_map(function (Column $col) { + return $col->title; + }, $this->getColumns()); + } + + + /** + * 格式行数据 + * + * @param $row + * @return array + */ + public function formatRow($row) + { + $newRow = []; + foreach ($this->columns as $column) { + $newRow[$column->field] = $row[$column->field] ?? ''; + if (is_callable($column->callback)) { + $newRow[$column->field] = call_user_func($column->callback, $row); + } + } + return $newRow; + } + + /** + * 格式化多行数据 + * + * @param $list + * @return array + */ + public function formatList($list) + { + return array_map([$this, 'formatRow'], $list ?? []); + } +} \ No newline at end of file diff --git a/src/data/import/ImportConfig.php b/src/data/import/ImportConfig.php new file mode 100644 index 0000000..ee1b391 --- /dev/null +++ b/src/data/import/ImportConfig.php @@ -0,0 +1,190 @@ +sheets; + } + + /** + * 获取地址 + * + * @return string + */ + public function getPath() + { + return $this->path; + } + + + /** + * 获取token + * + * @return void + */ + public function getToken() + { + return $this->token; + } + + + /** + * 是否异步 + * + * @return bool + */ + public function getIsAsync(): bool + { + return $this->isAsync; + } + + /** + * 设置页码信息 + * + * @param array $sheets + * @return $this + */ + public function setSheets(array $sheets) + { + $this->sheets = $sheets; + return $this; + } + + + /** + * 添加读取页 + * + * @param Sheet $sheet + * @return void + */ + public function addSheet(Sheet $sheet) + { + $this->sheets[] = $sheet; + return $this; + } + + /** + * 设置导入地址 + * + * @param string $path + * @return $this + */ + public function setPath(string $path) + { + $this->path = $path; + return $this; + } + + /** + * 设置token + * + * @return void + */ + public function setToken($token) + { + $this->token = $token; + return $this; + } + + /** + * 设置临时文件地址 + * + * @param string $tempPath + * @return $this + */ + final public function setTempPath(string $tempPath) + { + $this->tempPath = $tempPath; + return $this; + } + + /** + * 获取临时文件地址 + * + * @return string + */ + final public function getTempPath(): string + { + return $this->tempPath; + } + + + /** + * 序列化 + * + * @return array + */ + public function __serialize(): array + { + return [ + 'path' => $this->getPath(), + 'isAsync' => $this->isAsync, + 'token' => $this->getToken(), + ]; + } + +} \ No newline at end of file diff --git a/src/data/import/ImportData.php b/src/data/import/ImportData.php new file mode 100644 index 0000000..2c27b8e --- /dev/null +++ b/src/data/import/ImportData.php @@ -0,0 +1,60 @@ +sheetDatas[strtolower($sheetName)] = $sheetData; + return $this; + } + + /** + * 获取页数据 + * + * @param $sheetName + * @return mixed + */ + public function getSheetData($sheetName = 'sheet1') + { + return $this->getSheetData(strtolower($sheetName)); + } + +} \ No newline at end of file diff --git a/src/data/import/Sheet.php b/src/data/import/Sheet.php new file mode 100644 index 0000000..e225f43 --- /dev/null +++ b/src/data/import/Sheet.php @@ -0,0 +1,145 @@ +类型) + * + * @var array + */ + public array $columnTypes = []; + + + /** + * 游标读取回调 + * ` + * function($row){ + * // 执行业务代码 + * } + * ` + * @var + */ + public $callback; + + + /** + * 获取最终列头信息 + * + * @return void + */ + public function getHeader(array $cols) + { + return array_map(function ($n) { + return $this->headerMap[$n] ?? $n; + }, $cols); + } + + /** + * 格式化数据 + * + * @param $sheetData + * @param $header + * @return void + */ + public function formatSheetDataByHeader($sheetData, $header) + { + return array_map(function ($n) use ($header) { + return $this->formatRowByHeader($n, $header); + }, $sheetData); + } + + /** + * 格式化行数据 + * + * @param $row + * @param $header + * @return array + */ + public function formatRowByHeader($row, $header) + { + return array_combine($header, $row); + } + + +} \ No newline at end of file diff --git a/src/drivers/csv/Excel.php b/src/drivers/csv/Excel.php new file mode 100644 index 0000000..c339598 --- /dev/null +++ b/src/drivers/csv/Excel.php @@ -0,0 +1,29 @@ +excel = new \Vtiful\Kernel\Excel([ + 'path' => $this->getTempDir(), + ]); + parent::init(); + } + + + /** + * 导出 + * + * @param ExportConfig $config + * @return void + */ + protected function exportExcel(ExportConfig $config): string + { + $token = $config->getToken(); + + $filePath = $this->getTempFileName('ex_'); + $fileName = basename($filePath); + $this->excel->fileName($fileName, ($config->sheets[0])->name ?? 'sheet1'); + + $this->setProgressInfo($token, $config->getSheetNames(), self::PROGRESS_STATUS_PROCESS); + /** + * 写入页码数据 + * + * @var \vartruexuan\excel\data\export\Sheet $sheet + */ + foreach ($config->getSheets() as $key => $sheet) { + if ($key > 0) { + $this->excel->addSheet($sheet->getName()); + } + $this->excel->header($sheet->getHeaders()); + $count = $sheet->getCount(); + $pageCount = $sheet->getPageCount(); + $data = $sheet->getData(); + $this->initSheetProgress($token, $sheet->getName(), $sheet->getCount()); + if (is_callable($data)) { + $pageNum = ceil($count / $pageCount); + $page = 1; + do { + $list = $this->exportDataFunc($data, $config, $page, $pageCount, $count, $sheet->getName()); + $listCount = count($list ?? []); + if ($list) { + $this->excel->data($sheet->formatList($list)); + } + $progressStatus = ($count <= 0 || ($listCount < $pageCount || $pageNum <= $page)) ? self::PROGRESS_STATUS_END : self::PROGRESS_STATUS_PROCESS; + $this->setSheetProgress($token, $sheet->getName(), $progressStatus, $listCount, $listCount); + $page++; + } while ($count > 0 && $listCount >= $pageCount && ($pageNum >= $page)); + } else { + $this->excel->data($sheet->formatList($data)); + $listCount = count($data ?? []); + $this->setSheetProgress($token, $sheet->getName(), self::PROGRESS_STATUS_END, $listCount, $listCount); + } + } + + // 输出对应文件 + $this->excel->output(); + + return $filePath; + } + + /** + * 导入 + * + * @param ImportConfig $config + * @return ImportData + */ + protected function importExcel(ImportConfig $config): ImportData + { + $token = $config->getToken(); + $importData = new ImportData([ + 'token' => $token, + ]); + $filePath = $config->getTempPath(); + $fileName = basename($filePath); + $this->checkFile($filePath); + // 打开文件 + $this->excel->openFile($fileName); + $sheetList = $this->excel->sheetList(); + + // 初始化进度信息 + $this->setProgressInfo($token, array_map('strtolower', $sheetList), self::PROGRESS_STATUS_PROCESS); + /** + * 页配置 + * + * @var Sheet $sheetConfig + */ + foreach ($config->getSheets() as $sheetConfig) { + $sheetName = $sheetConfig->name; + if ($sheetConfig->readType == Sheet::SHEET_READ_TYPE_INDEX) { + $sheetName = $sheetList[$sheetConfig->index]; + } + + $this->excel->openSheet($sheetName); + + $header = []; + if ($sheetConfig->isSetHeader) { + // 跳过指定行 + $header = $this->excel->nextRow(); + $header = $sheetConfig->getHeader($header); + } + // 返回全量数据 + if ($sheetConfig->isReturnSheetData) { + $sheetData = $this->excel->getSheetData(); + $sheetDataCount = count($sheetData ?? []); + } + $this->initSheetProgress($token, $sheetName, $sheetDataCount ?? 0); + if ($sheetConfig->callback || $header) { + if ($sheetConfig->isReturnSheetData) { + foreach ($sheetData as $key => &$row) { + if ($header) { + $row = $sheetConfig->formatRowByHeader($row, $header); + } + // 执行回调 + if ($sheetConfig->callback && is_callable($sheetConfig->callback)) { + $this->importRowCallback($sheetConfig->callback, $row, $this, $config, $sheetName); + } + } + $importData->addSheetData($sheetData, $sheetName); + } else { + // 执行回调 + if ($sheetConfig->callback) { + while (null !== $row = $this->excel->nextRow()) { + $this->importRowCallback($sheetConfig->callback, $sheetConfig->formatRowByHeader($row, $header), $this, $sheetConfig, $sheetName); + } + } + } + } + $this->setSheetProgress($token, $sheetName, self::PROGRESS_STATUS_END); + } + + // 删除临时文件 + @$this->deleteFile($filePath); + + $this->excel->close(); + return $importData; + } + + + /** + * 校验文件mimeType类型 + * + * @param $filePath + * @return void + */ + protected function checkFile($filePath) + { + // 本地地址 + if (!file_exists($filePath)) { + throw new ExcelException('File does not exist'); + } + // 校验mime type + $mimeType = FileHelper::getMimeType($filePath); + if (!in_array($mimeType, [ + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-excel', + 'application/octet-stream', + ])) { + throw new ExcelException('File mime type error'); + } + } + +} \ No newline at end of file diff --git a/src/events/ErrorEvent.php b/src/events/ErrorEvent.php new file mode 100644 index 0000000..5c864c6 --- /dev/null +++ b/src/events/ErrorEvent.php @@ -0,0 +1,29 @@ +exportConfig->isAsync = false; + $this->getExcelInstance()->export($this->exportConfig); + } catch (\Throwable $exception) { + \Yii::error($exception->getMessage(), 'vartruexuan/excel/ExportJob'); + throw $exception; + } + } +} \ No newline at end of file diff --git a/src/jobs/ImportJob.php b/src/jobs/ImportJob.php new file mode 100644 index 0000000..8f3a18d --- /dev/null +++ b/src/jobs/ImportJob.php @@ -0,0 +1,36 @@ +importConfig->isAsync = false; + $this->getExcelInstance()->import($this->importConfig); + } catch (\Throwable $exception) { + \Yii::error($exception->getMessage(), 'vartruexuan/excel/ImportJob'); + throw $exception; + } + + } +} \ No newline at end of file diff --git a/src/jobs/base/BaseJob.php b/src/jobs/base/BaseJob.php new file mode 100644 index 0000000..3c564e4 --- /dev/null +++ b/src/jobs/base/BaseJob.php @@ -0,0 +1,28 @@ +getExcelInstance($this->componentId); + } + +} \ No newline at end of file diff --git a/src/utils/Helper.php b/src/utils/Helper.php new file mode 100644 index 0000000..99a4d97 --- /dev/null +++ b/src/utils/Helper.php @@ -0,0 +1,72 @@ + 'raw', + ])->request($remotePath, 'GET', [ + 'verify' => false, + 'http_errors' => false, + ]); + + if (@file_put_contents($filePath, $response->getBody()->getContents())) { + return $filePath; + } + return false; + } + + + /** + * 是否是远程地址 + * + * @param $url + * @return false|int + */ + public function isUrl($url) + { + return preg_match('/^http[s]?:\/\//', $url); + } + + + /** + * 获取uuid4 + * + * @return void + */ + public function uuid4() + { + return Uuid::uuid4()->getHex()->toString(); + } + + /** + * 删除文件 + * + * @param string $filePath + * @return bool + */ + public function deleteFile(string $filePath) + { + return FileHelper::unlink($filepath); + } + +} \ No newline at end of file diff --git a/tests/ExportTest.php b/tests/ExportTest.php new file mode 100644 index 0000000..7fe8c2b --- /dev/null +++ b/tests/ExportTest.php @@ -0,0 +1,79 @@ +export(new ExportConfig([ + 'path' => '/www/export_'.Carbon::now()->format('Y_m_d_H_i_s_u').'.xlsx', + 'outPutType' => ExportConfig::OUT_PUT_TYPE_LOCAL, + 'sheets' => [ + new \vartruexuan\excel\data\export\Sheet([ + 'name' => 'sheet1', + 'columns' => [ + new Column([ + 'title' => 'uniqId', + 'key' => 'uniqId', + 'field' => 'uniqId', + ]), + new Column([ + 'title' => '标题', + 'key' => 'title', + 'field' => 'title', + ]), + new Column([ + 'title' => 'component', + 'key' => 'component', + 'field' => 'component', + ]), + ], + 'count' => PlatformMenuService::instance()->getCount(), + 'data' => function ($page, $pageCount, $total) { + $pageInfo = Helper::getPageInfo($total, $pageCount, $page); + return PlatformMenuService::instance()->getList(null,null,$pageInfo['limit'],$pageInfo['offset'],true); + }, + ]), + new \vartruexuan\excel\data\export\Sheet([ + 'name' => 'sheet2', + 'columns' => [ + new Column([ + 'title' => 'uniqId', + 'key' => 'uniqId', + 'field' => 'uniqId', + ]), + new Column([ + 'title' => '标题', + 'key' => 'title', + 'field' => 'title', + ]), + new Column([ + 'title' => 'component', + 'key' => 'component', + 'field' => 'component', + ]), + ], + 'count' => PlatformMenuService::instance()->getCount(), + 'data' => function ($page, $pageCount, $total) { + $pageInfo = Helper::getPageInfo($total, $pageCount, $page); + return PlatformMenuService::instance()->getList(null,null,$pageInfo['limit'],$pageInfo['offset'],true); + }, + ]) + ] + ])); + + } + +} \ No newline at end of file diff --git a/tests/ImportTest.php b/tests/ImportTest.php new file mode 100644 index 0000000..f339004 --- /dev/null +++ b/tests/ImportTest.php @@ -0,0 +1,41 @@ +import(new \vartruexuan\excel\data\import\ImportConfig([ + 'path' => '../files/ceshi.xlsx', + // 'path' => 'https://pubbucket-1259747003.cos.ap-guangzhou.myqcloud.com/ceshi.xlsx', + + 'sheets' => [ + new Sheet([ + 'name' => 'sheet1', + 'isReturnSheetData' => false, + 'isSetHeader' => true, + 'headerMap' => [ + '年龄' => 'age', + '姓名' => 'name', + '身高' => 'height', + ], + 'callback' => function ($row) { + // 执行行回调 + var_dump($row); + } + ]), + ], + ])); + + } + +} \ No newline at end of file diff --git a/tests/config/TestExportConfig.php b/tests/config/TestExportConfig.php new file mode 100644 index 0000000..fa1cd18 --- /dev/null +++ b/tests/config/TestExportConfig.php @@ -0,0 +1,99 @@ +path; + } + + /** + * 获取页码配置 + * + * @return Sheet[] + */ + public function getSheets() + { + return [ + new \vartruexuan\excel\data\export\Sheet([ + 'name' => 'sheet1', + 'pageCount' => 5000, + 'columns' => [ + new Column([ + 'title' => 'uniqId', + 'key' => 'uniqId', + 'field' => 'uniqId', + ]), + new Column([ + 'title' => '标题', + 'key' => 'title', + 'field' => 'title', + ]), + new Column([ + 'title' => 'component', + 'key' => 'component', + 'field' => 'component', + ]), + ], + 'count' => PlatformMenuService::instance()->getCount(), + 'data' => function (ExportConfig $config, $page, $pageSize, $total, $sheetName, ExcelAbstract $excel) { + $pageInfo = Helper::getPageInfo($total, $pageCount, $page, null, [1, $pageCount]); + return PlatformMenuService::instance()->getList(null, null, $pageInfo['limit'], $pageInfo['offset'], true); + }, + ]), + new \vartruexuan\excel\data\export\Sheet([ + 'name' => 'sheet2', + 'pageCount' => 5000, + 'columns' => [ + new Column([ + 'title' => 'uniqId', + 'key' => 'uniqId', + 'field' => 'uniqId', + ]), + new Column([ + 'title' => '标题', + 'key' => 'title', + 'field' => 'title', + ]), + new Column([ + 'title' => 'component', + 'key' => 'component', + 'field' => 'component', + ]), + ], + 'count' => PlatformMenuService::instance()->getCount(), + 'data' => function ($page, $pageCount, $total) { + var_dump($page, $pageCount, $total); + $pageInfo = Helper::getPageInfo($total, $pageCount, $page, null, [1, $pageCount]); + return PlatformMenuService::instance()->getList(null, null, $pageInfo['limit'], $pageInfo['offset'], true); + }, + ]) + ]; + } + + /** + * 获取输出类型 + * + * @return string + */ + public function getOutPutType() + { + return $this->outPutType; + } + + +} \ No newline at end of file diff --git a/tests/config/TestImportConfig.php b/tests/config/TestImportConfig.php new file mode 100644 index 0000000..a016537 --- /dev/null +++ b/tests/config/TestImportConfig.php @@ -0,0 +1,38 @@ +setSheets([ + new Sheet([ + 'name' => 'sheet1', + 'isReturnSheetData' => true, + 'isSetHeader' => true, + 'headerMap' => [ + '年龄' => 'age', + '姓名' => 'name', + '身高' => 'height', + ], + 'callback' => function ($row, ExcelAbstract $excel, ImportConfig $config) { + // 执行行回调 + var_dump($row); + + sleep(10); + + return true; + } + ]) + + ]); + return parent::getSheets(); + + } + + +} \ No newline at end of file diff --git a/tests/files/ceshi.xlsx b/tests/files/ceshi.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6aa4a4b8d8e8233365ea60007d4b8f165a3275ec GIT binary patch literal 8697 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 literal 0 HcmV?d00001