<?php
namespace app\models\Excel;
use app\models\AliyunOssApi;
use app\models\Util;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use yii\db\ActiveRecord;
/**
* 基础导表类
*/
class BaseExcel extends ActiveRecord
{
/**
* 表格每列展示的字段名称,及对应读取的数据变量
* 示例
* array[
* 'A'=>[
* 'name'='字段名',
* 'field'='对应的数据变量名',
* 'value'='', 数据[field] 为空时 读取
* 'default'='', field = '' 时 读取
* ]
* ]
* @var array
*/
private array $field_list = []; //字段名称数组
private string $file_name = ''; //文件名
private string $title_name = '未命名'; //文件表格title名
private int $column_width = 25;// 列宽
private array $column_width_fields = [];// 单独设置列宽
private bool $is_set_border= true;//是否设置边框
private array $column_content_format= [];//设置列内容格式
private int $data_start_line = 2; //加载数据开始行数
private int $name_line = 1; //数据名称展示在哪一行
private array $configure_top_data = []; //定制头部内容展示
private array $configure_bottom_data = [];// 定制底部内容展示
public function __construct()
{
$this->file_name = date('YmdHis') .'-'. rand(10000, 99999);
}
/**
* 设置表格导出列,
* 及每列第一行的字段名称,以及字段对应的变量名
* @param array $field_list
* @return $this
* @throws \app\models\Exception
*/
public function setFieldlist(array $field_list = []): BaseExcel
{
/**
* $field_list 为数组格式 每个参数的定义
* name 每列第一行展示的字段名称
* field 每列对应的变量参数
* value 当 field参数 对应的变量的值为空时,默认读取 value 的值
* default 当每列对应得到 field为空时 ,默认读取 default的值
*/
if (empty($field_list)) {
throw new \app\models\Exception('SERVER' ,'表格字段名称,及对应展示变量必传');
}
$this->field_list = $field_list;
return $this;
}
/**
* 设置文件名称
* @param string $file_name
* @return $this
*/
public function setFileName(string $file_name = ''): BaseExcel
{
$this->file_name = $file_name ?: date('YmdHis') .'-'.rand(10000,99999);
return $this;
}
/**
* 设置表格名称
* @param string $title_name
* @return $this
*/
public function setTitleName(string $title_name = ''): BaseExcel
{
$this->title_name = $title_name ?: '表格内容列表';
return $this;
}
/**
* 设置统一列宽 默认 25
* @param int $column_width
* @return BaseExcel
*/
public function setColumnWidth(int $column_width = 0): BaseExcel
{
$this->column_width = $column_width > 0 ? $column_width : 25;
return $this;
}
/**
* 导出表格是否需要边框 默认 是
* @param bool $is_set_border
* @return BaseExcel
*/
public function setIsSetBorder(bool $is_set_border = true): BaseExcel
{
$this->is_set_border = $is_set_border;
return $this;
}
/**
* 设置字段内容展示格式
* @param string $set_column_content_format
* @param array $set_column_content_format_fields
* @return $this
* @throws \app\models\Exception
*/
public function setColumnContentFormat(string $set_column_content_format = '' ,
array $set_column_content_format_fields = []): BaseExcel
{
if(empty($set_column_content_format)){
return $this;
}
if(empty($set_column_content_format_fields) || !is_array($set_column_content_format_fields)){
return $this;
}
if(empty($this->field_list)){
throw new \app\models\Exception('SERVER','请先设置基础列字段。');
}
$fields_keys = array_keys($this->field_list);
$differences = array_diff($set_column_content_format_fields, $fields_keys);
if($differences){
throw new \app\models\Exception('SERVER','设置样式字段不在基础字段当中');
}
switch ($set_column_content_format) {
case 'FORMAT_NUMBER_00':
//设置字段内容格式为数字,并保留两位小数
$style = [
'numberFormat' => [
'formatCode' => NumberFormat::FORMAT_NUMBER_00,
],
];
$this->column_content_format[] = [
'style' => $style,
'fields'=> $set_column_content_format_fields,
];
break;
case 'FORMAT_NUMBER':
//设置列字段内容为纯数字,主要用来避免超长数字时被序列化问题
$style = [
'numberFormat' => [
'formatCode' => NumberFormat::FORMAT_NUMBER,
],
];
$this->column_content_format[] = [
'style' => $style,
'fields'=> $set_column_content_format_fields,
];
break;
}
return $this;
}
/**
* 设置字段内容展示格式
* @param int $width_value 列宽数值
* @param array $column_fields 列宽对应列
* @return $this
* @throws \app\models\Exception
*/
public function setColumnWidthFields(int $width_value = 25 ,
array $column_fields = []): BaseExcel
{
if(empty($width_value)){
return $this;
}
if(empty($column_fields) || !is_array($column_fields)){
return $this;
}
if(empty($this->field_list)){
throw new \app\models\Exception('SERVER','请先设置基础列字段。');
}
$fields_keys = array_keys($this->field_list);
$differences = array_diff($column_fields, $fields_keys);
if($differences){
throw new \app\models\Exception('SERVER','设置列宽字段不在基础字段当中');
}
$this->column_width_fields[]=[
'value' => $width_value,
'fields' => $column_fields,
];
return $this;
}
/**
* 设置展示数据的开始行数 默认 2
* @param int $start_line
* @return $this
*/
// public function setDataStartLine(int $start_line = 2): BaseExcel
// {
// if(empty($start_line)){
// return $this;
// }
// $this->name_line = $start_line;
// return $this;
// }
/**
* 设置表格字段名称所在行数 默认 1
* @param int $start_line
* @return $this
*/[object Object]
// public function setNameLine(int $start_line = 1): BaseExcel
// {
// if(empty($start_line)){
// return $this;
// }
// $this->name_line = $start_line;
// return $this;
// }
/**
* 自定义头部字段内容显示
* @param array $configure_top_data
* 'column' 展示自定义内容的单元格
* 'line' 内容展示在单元格的哪一行
* 'text' 单元格展示内容
* 'merge_fields' array 需要合并的单元格,默认为所有列,参数为数组 值为列标识[A,B,C,...]
* @return $this
* @throws \app\models\Exception
*/
public function setConfigureTopData(array $configure_top_data = []): BaseExcel
{
if(empty($configure_top_data)){
return $this;
}
if(!empty($configure_top_data['merge_fields'])){
if(!is_array($configure_top_data['merge_fields'])){
throw new \app\models\Exception('SERVER','merge_fields 参数必须为数组');
}
if(!in_array($configure_top_data['column'], $configure_top_data['merge_fields'])){
throw new \app\models\Exception('SERVER','column 参数的值,必须在merge_fields内。');
}
}
$this->configure_top_data[] = $configure_top_data;
$this ->data_start_line += 1;
$this ->name_line += 1;
return $this;
}
/**
* 自定义底部字段内容显示
* @param array $configure_bottom_data
* 'column' 展示自定义内容的单元格
* 'line' 内容展示在单元格的哪一行及内容展示结束后的第几行
* 'text' 单元格展示内容
* 'merge_fields' array 需要合并的单元格,默认为所有列,参数为数组 值为列标识[A,B,C,...]
* @return $this
* @throws \app\models\Exception
*/
public function setConfigureBottomData(array $configure_bottom_data = []): BaseExcel
{
if(empty($configure_bottom_data)){
return $this;
}
if(!empty($configure_bottom_data['merge_fields'])){
if(!is_array($configure_bottom_data['merge_fields'])){
throw new \app\models\Exception('SERVER','merge_fields 参数必须为数组');
}
if(!in_array($configure_bottom_data['column'], $configure_bottom_data['merge_fields'])){
throw new \app\models\Exception('SERVER','column 参数的值,必须在merge_fields内。');
}
}
$this->configure_bottom_data[] = $configure_bottom_data;
return $this;
}
/**
* 基础表格导出操作
* @param $list
* @return array
* @throws Exception
* @throws \app\models\Exception
*/
public function getExcel($list): array
{
if(empty($this->field_list) || empty($list)){
throw new \app\models\Exception('SERVER','导表必须设置字段基础信息,并且数据内容不能为空');
}
$fileName = $this ->title_name;
$spreadsheet = new Spreadsheet();
$spreadsheet->getActiveSheet()->setTitle($fileName);
$sheet = $spreadsheet->getActiveSheet();
//定义表格格式
$excel_row_data = $this ->field_list;
//获取全部
$letterList = $this->generateLetters("A", count($excel_row_data));
$styleArray = [
'alignment' => [
'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_LEFT,
],
];
foreach ($letterList as $v) {
//设置单元格宽带
$spreadsheet->getActiveSheet()->getColumnDimension($v)->setWidth($this->column_width);
//设置单元格内容居左显示
$sheet->getStyle($v)->applyFromArray($styleArray);
//设置单元格第一列标题内容
$sheet->setCellValue($v.$this->name_line, $excel_row_data[$v]['name']);
}
//单独设置列表字段
if($this ->column_width_fields){
$column_width_fields = $this ->column_width_fields;
foreach ($column_width_fields as $v){
foreach ($v['fields'] as $vv){
//设置单元格宽带
$spreadsheet->getActiveSheet()->getColumnDimension($vv)->setWidth($v['value']);
}
}
}
//定制头部内容展示
if(!empty($this ->configure_top_data)){
foreach ($this ->configure_top_data as $v){
//设置单元格内容
$sheet->setCellValue($v['column'] . $v['line'], $v['text'] );
//设置合并单元格,默认所有列合并成一个单元格
if(!empty($v['merge_fields']) && is_array($v['merge_fields'])){
sort($v['merge_fields']);
$sheet->mergeCells($v['merge_fields'][0] . $v['line'].':'.end($v['merge_fields']).$v['line']);
}else{
$endLetter = end($letterList);
$sheet->mergeCells($v['column'] . $v['line'].':'.$endLetter.$v['line']);
}
}
}
$count = count($list);
//设置列内容格式
if(!empty($this ->column_content_format)){
foreach ($this ->column_content_format as $v){
foreach ($v['fields'] as $k=>$vv){
$sheet->getStyle($vv.$this ->data_start_line.':'.$vv.($count+$this ->data_start_line-1))->applyFromArray($v['style']);
}
}
}
$i = $this ->data_start_line;
//表格赋值
foreach ($list as $k => $v) {
foreach ($excel_row_data as $k1 => $v1) {
$value = ($v1['field'] != '' ? $v[$v1['field']] : $v1['default']) ?:$v1['value'];
if(strpos($value, '==') !== false){
// $value = '\''.$value;
$sheet->setCellValueExplicit($k1 . ($i),$value ,DataType::TYPE_STRING);
}else{
$sheet->setCellValue($k1 . ($i),$value );
}
}
$i ++ ;
}
$end_line = $i-1;
//定制底部内容展示
if(!empty($this ->configure_bottom_data)){
foreach ($this ->configure_bottom_data as $v){
$current_line = $i+$v['line']-1;
$end_line = $current_line; // 同时标记表格最后一行
//设置单元格内容
$sheet->setCellValue($v['column'] . $current_line, $v['text'] );
//设置合并单元格,默认所有列合并成一个单元格
if(!empty($v['merge_fields']) && is_array($v['merge_fields'])){
sort($v['merge_fields']);
$sheet->mergeCells($v['merge_fields'][0] . $current_line.':'.end($v['merge_fields']).$current_line);
}else{
$endLetter = end($letterList);
$sheet->mergeCells($v['column'] . $current_line.':'.$endLetter.$current_line);
}
}
}
if($this ->is_set_border){
//表格边框
$styleArray1 = [
'borders' => [
'allBorders' => [
'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN //细边框
]
]
];
$endLetter = end($letterList);
$sheet->getStyle('A1' . ':'.$endLetter . $end_line)->applyFromArray($styleArray1);
}
$file_name = $this ->file_name. '.xlsx';
$oss_path = 'ticket_order_export_excel/' . date('y/m/');
$dir_path = 'uploads/' . $file_name;
$writer = new Xlsx($spreadsheet);
$writer->save($dir_path); //保存文件
return ['url' => $dir_path ];
}
/**
* 获取字母数组
*
* @param string $start
* @param int $count
* @return array
*/
public function generateLetters(string $start, int $count): array {
$startCode = ord(strtoupper($start)); // 转成 ASCII 码
$letters = [];
for ($i = 0; $i < $count; $i++) {
$letters[] = chr($startCode + $i);
}
return $letters;
}
}