520 lines
14 KiB
PHP
520 lines
14 KiB
PHP
<?php
|
||
|
||
namespace app\common\arw\adjfut\src;
|
||
|
||
use app\common\arw\adjfut\src\Exception\ErrorMsg;
|
||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||
use think\helper\Arr;
|
||
use think\helper\Str;
|
||
use think\Validate;
|
||
use app\common\arw\adjfut\src\Traits\Event;
|
||
use think\facade\Env;
|
||
|
||
class Excel
|
||
{
|
||
use Event;
|
||
/**
|
||
* Spreadsheet
|
||
*
|
||
* @var Spreadsheet
|
||
*/
|
||
private $spreadsheet = null;
|
||
/**
|
||
* 工作表样式
|
||
*
|
||
* @var array
|
||
*/
|
||
private $worksheetStyle = [];
|
||
/**
|
||
* 文件路径
|
||
*
|
||
* @var string
|
||
* @date 2022-04-15
|
||
* @example
|
||
* @author arw
|
||
* @since 1.0.0
|
||
*/
|
||
private $path = '';
|
||
|
||
/**
|
||
* 初始化
|
||
*
|
||
* @param string|UploadFile $path
|
||
* @date 2022-04-15
|
||
* @example
|
||
* @author arw
|
||
* @since 1.0.0
|
||
*/
|
||
public function __construct($path = '')
|
||
{
|
||
if ($path) {
|
||
if ($path instanceof UploadFile) {
|
||
$path = $path->getRealPath();
|
||
}
|
||
$this->spreadsheet = self::loadExcel($path);
|
||
$this->path = $path;
|
||
} else {
|
||
$this->spreadsheet = new Spreadsheet();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设置工作表样式
|
||
*
|
||
* @param array $options
|
||
* @param boolean $options[autoBorder] 是否自动添加边框
|
||
* @param boolean $options[freezeLine] 是否自动冻结首行
|
||
* @return self
|
||
*/
|
||
public function setWorksheetStyle(array $options): self
|
||
{
|
||
$this->worksheetStyle = $options;
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* 获取当前Spreadsheet
|
||
*
|
||
* @return Spreadsheet
|
||
* @date 2020-11-23
|
||
* @example
|
||
* @author arw
|
||
* @since 1.0.0
|
||
*/
|
||
public function getSpreadsheet(): Spreadsheet
|
||
{
|
||
return $this->spreadsheet;
|
||
}
|
||
|
||
/**
|
||
* 读取excel
|
||
*
|
||
* @param string $path excel路径
|
||
* @return Spreadsheet
|
||
*/
|
||
public static function loadExcel(string $path): Spreadsheet
|
||
{
|
||
if (!is_file($path)) {
|
||
throw new ErrorMsg("文件不存在 $path", 1);
|
||
}
|
||
$ext = pathinfo($path, PATHINFO_EXTENSION);
|
||
return IOFactory::createReader(ucfirst($ext))->load($path);
|
||
}
|
||
|
||
/**
|
||
* 获取工作表
|
||
* 默认获取当前激活的
|
||
*
|
||
* @param string $sheet
|
||
* @return Worksheet
|
||
*/
|
||
public function getWorksheet(string $sheet = 'active'): Worksheet
|
||
{
|
||
$Spreadsheet = $this->getSpreadsheet();
|
||
if ($sheet === 'active') {
|
||
$Worksheet = $Spreadsheet->getActiveSheet();
|
||
} else {
|
||
$Worksheet = $Spreadsheet->getSheet($sheet);
|
||
}
|
||
return $Worksheet;
|
||
}
|
||
|
||
/**
|
||
* 获取excel数据
|
||
*
|
||
* @param string $sheet
|
||
* @return array
|
||
*/
|
||
public function getExcelData(string $sheet = 'active'): array
|
||
{
|
||
return $this->getWorksheet($sheet)->toArray();
|
||
}
|
||
|
||
/**
|
||
* 解析excel
|
||
*
|
||
* @param array $rules
|
||
* @param string $rules[][title] 表头
|
||
* @param string $rules[][validate] 验证器
|
||
* @param string $rules[][field] 字段名
|
||
* @param string $rules[][type] 数据类型 date:日期格式(Y-m-d H:i:s)
|
||
* @param array $options
|
||
* @param array $options[titleLine] 表头行
|
||
* @param boolean $options[autoRemove] 解析后移除文件
|
||
* @param boolean $options[ignoreNullRow] 忽略空行
|
||
* @return array
|
||
* @date 2022-04-11
|
||
* @example
|
||
* @author arw
|
||
* @since 1.0.0
|
||
*/
|
||
public function parseExcel(array $rules, array $options = []): array
|
||
{
|
||
$titleLine = Arr::get($options, 'titleLine', [1]);
|
||
$autoRemove = Arr::get($options, 'autoRemove', true);
|
||
$ignoreNullRow = Arr::get($options, 'ignoreNullRow', true);
|
||
|
||
if ($autoRemove) {
|
||
if (is_file($this->path)) {
|
||
unlink($this->path);
|
||
}
|
||
}
|
||
|
||
$title = [];
|
||
$_title = [];
|
||
$_field = [];
|
||
$_type = [];
|
||
$_validate = [];
|
||
foreach ($rules as $rule) {
|
||
$_title[] = $rule['title'];
|
||
if (isset($rule['field'])) {
|
||
$_field[] = $rule['field'];
|
||
if (isset($rule['validate'])) {
|
||
$_validate[join('|', [
|
||
$rule['field'],
|
||
$rule['title']
|
||
])] = $rule['validate'];
|
||
}
|
||
} else {
|
||
$_field[] = '';
|
||
}
|
||
if (isset($rule['type'])) {
|
||
$_type[] = $rule['type'];
|
||
} else {
|
||
$_type[] = '';
|
||
}
|
||
}
|
||
$Worksheet = $this->getWorksheet();
|
||
$data = $Worksheet->toArray();
|
||
// 获取表头 合并复杂表头
|
||
foreach ($titleLine as $line) {
|
||
$values = $data[$line - 1];
|
||
foreach ($values as $key => $value) {
|
||
if ($value) {
|
||
$title[$key] = $value;
|
||
}
|
||
}
|
||
}
|
||
// 验证表头
|
||
if ($title != $_title) {
|
||
throw new ErrorMsg("表头不一致 请确认excel是否正确", 1);
|
||
}
|
||
// 去除非数据字段
|
||
$i = max($titleLine) - 1;
|
||
do {
|
||
unset($data[$i]);
|
||
$i--;
|
||
} while ($i >= 0);
|
||
|
||
$validate = new Validate;
|
||
$validate->rule($_validate)->batch(true)->failException(true);
|
||
|
||
$result = [];
|
||
$error = [];
|
||
foreach ($data as $row => $value) {
|
||
$index = $row + 1;
|
||
$line = "第${index}行";
|
||
$_value = [];
|
||
if ($ignoreNullRow && self::isNullRow($value)) {
|
||
continue;
|
||
}
|
||
foreach ($_field as $column => $field) {
|
||
if ($field) {
|
||
if ($_type[$column]) {
|
||
$type = $_type[$column];
|
||
if (Str::startsWith($type, 'date') && $value[$column]) {
|
||
$format = 'Y-m-d H:i:s';
|
||
$types = explode(':', $type);
|
||
if (isset($types[1])) {
|
||
$format = $types[1];
|
||
}
|
||
$value[$column] = Tool::conversionDateTime($value[$column], $format);
|
||
}
|
||
}
|
||
$_value[$field] = $value[$column];
|
||
}
|
||
}
|
||
// 验证数据
|
||
if ($_validate) {
|
||
try {
|
||
$validate->check($_value);
|
||
} catch (\Throwable $th) {
|
||
$error[] = $line . ':' . $th->getMessage();
|
||
}
|
||
}
|
||
|
||
$result[$line] = $_value;
|
||
}
|
||
|
||
if ($error) {
|
||
throw new ErrorMsg(join(PHP_EOL, $error), 1);
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 导出excel
|
||
*
|
||
* @param array $data 数据
|
||
* @param array $options
|
||
* @param boolean $options[autoBorder] 是否自动添加边框
|
||
* @param string $options[freezeLine] 是否自动冻结首行
|
||
* @date 2022-04-11
|
||
* @example
|
||
* @author arw
|
||
* @since 1.0.0
|
||
*/
|
||
public static function exportSheet(array $data, array $options = [])
|
||
{
|
||
$excel = new self;
|
||
$excel->setWorksheetStyle($options);
|
||
$spreadsheet = $excel->getSpreadsheet();
|
||
$worksheet = $spreadsheet->getActiveSheet();
|
||
$worksheet->fromArray($data);
|
||
return $excel;
|
||
}
|
||
|
||
/**
|
||
* 导出处理图片字段
|
||
*/
|
||
public static function ExportImgFiled($value)
|
||
{
|
||
if(strpos($value,"http") !== false){
|
||
$base_url = "";
|
||
}else{
|
||
$base_url = Env::get('APP.DEFAULT_IMG_URL');
|
||
}
|
||
return $base_url . $value;
|
||
}
|
||
|
||
/**
|
||
* 导出excel 多sheet
|
||
*
|
||
* @param array $data 数据
|
||
* @param array $options
|
||
* @param boolean $options[autoBorder] 是否自动添加边框
|
||
* @param string $options[freezeLine] 是否自动冻结首行
|
||
* @date 2022-04-11
|
||
* @example
|
||
* @author arw
|
||
* @since 1.0.0
|
||
*/
|
||
public static function exportMoreSheet(array $data, array $options = [])
|
||
{
|
||
$excel = new self;
|
||
$excel->setWorksheetStyle($options);
|
||
$spreadsheet = $excel->getSpreadsheet();
|
||
$spreadsheet->removeSheetByIndex(0);
|
||
foreach ($data as $key => $value) {
|
||
$worksheet = new Worksheet($spreadsheet, $key);
|
||
$worksheet->fromArray($value);
|
||
$spreadsheet->addSheet($worksheet);
|
||
}
|
||
$spreadsheet->setActiveSheetIndex(0);
|
||
return $excel;
|
||
}
|
||
|
||
/**
|
||
* 保存excel
|
||
*
|
||
* @param string $name 文件名
|
||
* @param string $save 保存路径
|
||
* @date 2022-04-11
|
||
* @example
|
||
* @author arw
|
||
* @since 1.0.0
|
||
*/
|
||
public function save(string $name, string $save = 'php://output')
|
||
{
|
||
$spreadsheet = $this->getSpreadsheet();
|
||
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||
$_name = $name;
|
||
if (!preg_match("/\./", $name)) {
|
||
$_name .= '.Xlsx';
|
||
}
|
||
$worksheets = $spreadsheet->getAllSheets();
|
||
foreach ($worksheets as $worksheet) {
|
||
$this->_setWorksheetStyle($worksheet);
|
||
}
|
||
// 保存前
|
||
$this->trigger('beforeSave', [$spreadsheet]);
|
||
// 浏览器下载
|
||
if ($save === 'php://output') {
|
||
Tool::obEndClean();
|
||
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||
header('Cache-Control: max-age=0');
|
||
header('Content-Disposition:inline;filename="' . $_name . '"');
|
||
$writer->save($save);
|
||
} else {
|
||
Tool::mkdir($save);
|
||
// 保存到本地目录
|
||
$writer->save(join(DIRECTORY_SEPARATOR, [
|
||
$save, $_name,
|
||
]));
|
||
}
|
||
$spreadsheet->disconnectWorksheets();
|
||
unset($spreadsheet);
|
||
// 保存后
|
||
$this->trigger('AfterSave');
|
||
}
|
||
|
||
/**
|
||
* 保存前
|
||
*
|
||
* @param callable $cb
|
||
* @date 2022-04-11
|
||
* @example
|
||
* @author arw
|
||
* @since 1.0.0
|
||
*/
|
||
public function onBeforeSave(callable $cb): self
|
||
{
|
||
return $this->event('beforeSave', $cb);
|
||
}
|
||
|
||
/**
|
||
* 保存后
|
||
*
|
||
* @param callable $cb
|
||
* @date 2022-04-11
|
||
* @example
|
||
* @author arw
|
||
* @since 1.0.0
|
||
*/
|
||
public function onAfterSave(callable $cb): self
|
||
{
|
||
return $this->event('afterSave', $cb);
|
||
}
|
||
|
||
/**
|
||
* 判断是否空行
|
||
*
|
||
* @param array $rows
|
||
* @return boolean
|
||
* @date 2021-11-27
|
||
* @example
|
||
* @author arw
|
||
* @since 1.0.0
|
||
*/
|
||
public static function isNullRow(array $rows)
|
||
{
|
||
$rows = array_unique($rows);
|
||
$rows = array_filter($rows, function ($v) {
|
||
return !is_null($v);
|
||
});
|
||
return count($rows) == 0;
|
||
}
|
||
|
||
/**
|
||
* 快捷设置excel样式
|
||
* 首行冻结 自动添加边框
|
||
*
|
||
* @param Worksheet $worksheet
|
||
* @return Worksheet $worksheet
|
||
* @date 2021-01-25
|
||
* @author arw
|
||
* @since 1.0.0
|
||
*/
|
||
private function _setWorksheetStyle(Worksheet $worksheet): Worksheet
|
||
{
|
||
$options = $this->worksheetStyle;
|
||
$autoBorder = Arr::get($options, 'autoBorder', true);
|
||
$freezeLine = Arr::get($options, 'freezeLine', 'A2');
|
||
$autoBackground = Arr::get($options, 'autoBackground', true);
|
||
if ($freezeLine) {
|
||
$worksheet->freezePane($freezeLine);
|
||
}
|
||
if ($autoBorder) {
|
||
$worksheet->getStyle(join(':', [
|
||
'A1',
|
||
$worksheet->getHighestDataColumn() . $worksheet->getHighestDataRow()
|
||
]))->applyFromArray([
|
||
'borders' => [
|
||
'allBorders' => [
|
||
'borderStyle' => Border::BORDER_THIN, //细边框
|
||
],
|
||
],
|
||
]);
|
||
}
|
||
if($autoBackground){
|
||
$style = join(':', [
|
||
'A1',
|
||
$worksheet->getHighestDataColumn() . "1"
|
||
]);
|
||
|
||
// 列宽
|
||
$worksheet -> getDefaultColumnDimension() -> setWidth(25);
|
||
// $worksheet -> getColumnDimension('A') -> setWidth(30); //设置A列宽度为30
|
||
//行高
|
||
// $worksheet -> getRowDimension(1) -> setRowHeight(25);
|
||
$worksheet -> getDefaultRowDimension() -> setRowHeight(25);
|
||
|
||
//样式设置 - 字体
|
||
$worksheet -> getStyle($style) -> getFont()
|
||
// -> setBold(true)
|
||
->setName('微软雅黑')
|
||
-> setSize(13);
|
||
|
||
//样式设置 - 字体颜色
|
||
// $worksheet -> getStyle('A1') -> getFont()
|
||
// -> getColor() -> setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_RED); //设置单元格A1的字体颜色
|
||
|
||
// $font = $worksheet -> getCell('B1') -> getColor() -> setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_RED);
|
||
|
||
// if(substr($font,0,1) == "*"){
|
||
|
||
// }
|
||
|
||
// $worksheet -> setCellValue('A2', substr($font,0,1) );
|
||
|
||
//样式设置 - 水平、垂直居中
|
||
$styleArray = [
|
||
'alignment' => [
|
||
'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER,
|
||
'vertical' => \PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_CENTER
|
||
],
|
||
];
|
||
$worksheet -> getStyle($style) -> applyFromArray($styleArray);
|
||
|
||
//样式设置 - 单元格背景颜色
|
||
$worksheet->getStyle($style)->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)->getStartColor()->setARGB('FFFFFF00');
|
||
}
|
||
return $worksheet;
|
||
}
|
||
|
||
/**
|
||
* 数字转字母
|
||
* 1 => A
|
||
*
|
||
* @param integer $index
|
||
* @return string
|
||
* @date 2021-01-23
|
||
* @author arw
|
||
* @since 1.0.0
|
||
*/
|
||
public static function stringToIndex(int $index)
|
||
{
|
||
return Coordinate::stringFromColumnIndex($index);
|
||
}
|
||
|
||
/**
|
||
* 字母转数字
|
||
* A => 1
|
||
*
|
||
* @param string $string
|
||
* @return int
|
||
* @date 2021-01-23
|
||
* @author arw
|
||
* @since 1.0.0
|
||
*/
|
||
public static function indexToString(string $string)
|
||
{
|
||
return Coordinate::columnIndexFromString($string);
|
||
}
|
||
}
|