2025-11-27 17:09:53 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace app\common\controller;
|
|
|
|
|
|
|
|
|
|
|
|
use PHPExcel;
|
|
|
|
|
|
use PHPExcel_IOFactory;
|
|
|
|
|
|
use PHPExcel_Worksheet_Drawing;
|
|
|
|
|
|
use think\Controller;
|
|
|
|
|
|
use think\Exception;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 通用导出控制器,提供 Excel 导出与图片插入能力
|
|
|
|
|
|
*/
|
|
|
|
|
|
class ExportController extends Controller
|
|
|
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @var array<string> 需要在请求结束时清理的临时文件
|
|
|
|
|
|
*/
|
|
|
|
|
|
protected static $tempFiles = [];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 导出 Excel(支持指定列插入图片)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param string $fileName 输出文件名(可不带扩展名)
|
|
|
|
|
|
* @param array $headers 列定义,例如 ['name' => '姓名', 'phone' => '电话']
|
|
|
|
|
|
* @param array $rows 数据行,需与 $headers 的 key 对应
|
|
|
|
|
|
* @param array $imageColumns 需要渲染为图片的列 key 列表
|
|
|
|
|
|
* @param string $sheetName 工作表名称
|
2025-11-28 16:03:56 +08:00
|
|
|
|
* @param array $options 额外选项:
|
|
|
|
|
|
* - imageWidth(图片宽度,默认100)
|
|
|
|
|
|
* - imageHeight(图片高度,默认100)
|
|
|
|
|
|
* - imageColumnWidth(图片列宽,默认15)
|
|
|
|
|
|
* - titleRow(标题行内容,支持多行文本数组)
|
2025-11-27 17:09:53 +08:00
|
|
|
|
*
|
|
|
|
|
|
* @throws Exception
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function exportExcelWithImages(
|
|
|
|
|
|
$fileName,
|
|
|
|
|
|
array $headers,
|
|
|
|
|
|
array $rows,
|
|
|
|
|
|
array $imageColumns = [],
|
2025-11-28 16:03:56 +08:00
|
|
|
|
$sheetName = 'Sheet1',
|
|
|
|
|
|
array $options = []
|
2025-11-27 17:09:53 +08:00
|
|
|
|
) {
|
|
|
|
|
|
if (empty($headers)) {
|
|
|
|
|
|
throw new Exception('导出列定义不能为空');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (empty($rows)) {
|
|
|
|
|
|
throw new Exception('导出数据不能为空');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-28 16:03:56 +08:00
|
|
|
|
// 默认选项
|
|
|
|
|
|
$imageWidth = isset($options['imageWidth']) ? (int)$options['imageWidth'] : 100;
|
|
|
|
|
|
$imageHeight = isset($options['imageHeight']) ? (int)$options['imageHeight'] : 100;
|
|
|
|
|
|
$imageColumnWidth = isset($options['imageColumnWidth']) ? (float)$options['imageColumnWidth'] : 15;
|
|
|
|
|
|
$rowHeight = isset($options['rowHeight']) ? (int)$options['rowHeight'] : ($imageHeight + 10);
|
|
|
|
|
|
|
2025-11-27 17:09:53 +08:00
|
|
|
|
$excel = new PHPExcel();
|
|
|
|
|
|
$sheet = $excel->getActiveSheet();
|
|
|
|
|
|
$sheet->setTitle($sheetName);
|
|
|
|
|
|
|
|
|
|
|
|
$columnKeys = array_keys($headers);
|
2025-11-28 16:03:56 +08:00
|
|
|
|
$totalColumns = count($columnKeys);
|
|
|
|
|
|
$lastColumnLetter = self::columnLetter($totalColumns - 1);
|
2025-11-27 17:09:53 +08:00
|
|
|
|
|
2025-11-28 16:03:56 +08:00
|
|
|
|
// 定义特定列的固定宽度(如果未指定则使用默认值)
|
|
|
|
|
|
$columnWidths = isset($options['columnWidths']) ? $options['columnWidths'] : [];
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有标题行
|
|
|
|
|
|
$titleRow = isset($options['titleRow']) ? $options['titleRow'] : null;
|
|
|
|
|
|
$dataStartRow = 1; // 数据开始行(表头行)
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有标题行,先写入标题行
|
|
|
|
|
|
if ($titleRow && is_array($titleRow) && !empty($titleRow)) {
|
|
|
|
|
|
$dataStartRow = 2; // 数据从第2行开始(第1行是标题,第2行是表头)
|
|
|
|
|
|
|
|
|
|
|
|
// 合并标题行单元格(从第一列到最后一列)
|
|
|
|
|
|
$titleRange = 'A1:' . $lastColumnLetter . '1';
|
|
|
|
|
|
$sheet->mergeCells($titleRange);
|
|
|
|
|
|
|
|
|
|
|
|
// 构建标题内容(支持多行)
|
|
|
|
|
|
$titleContent = '';
|
|
|
|
|
|
if (is_array($titleRow)) {
|
|
|
|
|
|
$titleContent = implode("\n", $titleRow);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$titleContent = (string)$titleRow;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 写入标题
|
|
|
|
|
|
$sheet->setCellValue('A1', $titleContent);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置标题行样式
|
|
|
|
|
|
$sheet->getStyle('A1')->applyFromArray([
|
|
|
|
|
|
'font' => ['bold' => true, 'size' => 16],
|
|
|
|
|
|
'alignment' => [
|
|
|
|
|
|
'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
|
|
|
|
|
|
'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
|
|
|
|
|
|
'wrap' => true
|
|
|
|
|
|
],
|
|
|
|
|
|
'fill' => [
|
|
|
|
|
|
'type' => \PHPExcel_Style_Fill::FILL_SOLID,
|
|
|
|
|
|
'color' => ['rgb' => 'FFF8DC'] // 浅黄色背景
|
|
|
|
|
|
],
|
|
|
|
|
|
'borders' => [
|
|
|
|
|
|
'allborders' => [
|
|
|
|
|
|
'style' => \PHPExcel_Style_Border::BORDER_THIN,
|
|
|
|
|
|
'color' => ['rgb' => '000000']
|
|
|
|
|
|
]
|
|
|
|
|
|
]
|
|
|
|
|
|
]);
|
|
|
|
|
|
$sheet->getRowDimension(1)->setRowHeight(80); // 标题行高度
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 写入表头并设置列宽
|
|
|
|
|
|
$headerRow = $dataStartRow;
|
2025-11-27 17:09:53 +08:00
|
|
|
|
foreach ($columnKeys as $index => $key) {
|
|
|
|
|
|
$columnLetter = self::columnLetter($index);
|
2025-11-28 16:03:56 +08:00
|
|
|
|
$sheet->setCellValue($columnLetter . $headerRow, $headers[$key]);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是图片列,设置固定列宽
|
|
|
|
|
|
if (in_array($key, $imageColumns, true)) {
|
|
|
|
|
|
$sheet->getColumnDimension($columnLetter)->setWidth($imageColumnWidth);
|
|
|
|
|
|
} elseif (isset($columnWidths[$key])) {
|
|
|
|
|
|
// 如果指定了该列的宽度,使用指定宽度
|
|
|
|
|
|
$sheet->getColumnDimension($columnLetter)->setWidth($columnWidths[$key]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 否则自动调整
|
|
|
|
|
|
$sheet->getColumnDimension($columnLetter)->setAutoSize(true);
|
|
|
|
|
|
}
|
2025-11-27 17:09:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-28 16:03:56 +08:00
|
|
|
|
// 设置表头样式
|
|
|
|
|
|
$headerRange = 'A' . $headerRow . ':' . $lastColumnLetter . $headerRow;
|
|
|
|
|
|
$sheet->getStyle($headerRange)->applyFromArray([
|
|
|
|
|
|
'font' => ['bold' => true, 'size' => 11],
|
|
|
|
|
|
'alignment' => [
|
|
|
|
|
|
'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
|
|
|
|
|
|
'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
|
|
|
|
|
|
'wrap' => true
|
|
|
|
|
|
],
|
|
|
|
|
|
'fill' => [
|
|
|
|
|
|
'type' => \PHPExcel_Style_Fill::FILL_SOLID,
|
|
|
|
|
|
'color' => ['rgb' => 'FFF8DC']
|
|
|
|
|
|
],
|
|
|
|
|
|
'borders' => [
|
|
|
|
|
|
'allborders' => [
|
|
|
|
|
|
'style' => \PHPExcel_Style_Border::BORDER_THIN,
|
|
|
|
|
|
'color' => ['rgb' => '000000']
|
|
|
|
|
|
]
|
|
|
|
|
|
]
|
|
|
|
|
|
]);
|
|
|
|
|
|
$sheet->getRowDimension($headerRow)->setRowHeight(30); // 增加表头行高以确保文本完整显示
|
|
|
|
|
|
|
2025-11-27 17:09:53 +08:00
|
|
|
|
// 写入数据与图片
|
2025-11-28 16:03:56 +08:00
|
|
|
|
$dataRowStart = $dataStartRow + 1; // 数据从表头行下一行开始
|
2025-11-27 17:09:53 +08:00
|
|
|
|
foreach ($rows as $rowIndex => $rowData) {
|
2025-11-28 16:03:56 +08:00
|
|
|
|
$excelRow = $dataRowStart + $rowIndex; // 数据行
|
|
|
|
|
|
$maxRowHeight = $rowHeight; // 记录当前行的最大高度
|
|
|
|
|
|
|
2025-11-27 17:09:53 +08:00
|
|
|
|
foreach ($columnKeys as $colIndex => $key) {
|
|
|
|
|
|
$columnLetter = self::columnLetter($colIndex);
|
|
|
|
|
|
$cell = $columnLetter . $excelRow;
|
|
|
|
|
|
$value = isset($rowData[$key]) ? $rowData[$key] : '';
|
|
|
|
|
|
|
|
|
|
|
|
if (in_array($key, $imageColumns, true) && !empty($value)) {
|
|
|
|
|
|
$imagePath = self::resolveImagePath($value);
|
|
|
|
|
|
if ($imagePath) {
|
2025-11-28 16:03:56 +08:00
|
|
|
|
// 获取图片实际尺寸并等比例缩放
|
|
|
|
|
|
$imageSize = @getimagesize($imagePath);
|
|
|
|
|
|
if ($imageSize) {
|
|
|
|
|
|
$originalWidth = $imageSize[0];
|
|
|
|
|
|
$originalHeight = $imageSize[1];
|
|
|
|
|
|
|
|
|
|
|
|
// 计算等比例缩放后的尺寸
|
|
|
|
|
|
$ratio = min($imageWidth / $originalWidth, $imageHeight / $originalHeight);
|
|
|
|
|
|
$scaledWidth = $originalWidth * $ratio;
|
|
|
|
|
|
$scaledHeight = $originalHeight * $ratio;
|
|
|
|
|
|
|
|
|
|
|
|
// 确保不超过最大尺寸
|
|
|
|
|
|
if ($scaledWidth > $imageWidth) {
|
|
|
|
|
|
$scaledWidth = $imageWidth;
|
|
|
|
|
|
$scaledHeight = $originalHeight * ($imageWidth / $originalWidth);
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($scaledHeight > $imageHeight) {
|
|
|
|
|
|
$scaledHeight = $imageHeight;
|
|
|
|
|
|
$scaledWidth = $originalWidth * ($imageHeight / $originalHeight);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$drawing = new PHPExcel_Worksheet_Drawing();
|
|
|
|
|
|
$drawing->setPath($imagePath);
|
|
|
|
|
|
$drawing->setCoordinates($cell);
|
|
|
|
|
|
|
|
|
|
|
|
// 居中显示图片(Excel列宽1单位≈7像素,行高1单位≈0.75像素)
|
|
|
|
|
|
$cellWidthPx = $imageColumnWidth * 7;
|
|
|
|
|
|
$cellHeightPx = $maxRowHeight * 0.75;
|
|
|
|
|
|
$offsetX = max(2, ($cellWidthPx - $scaledWidth) / 2);
|
|
|
|
|
|
$offsetY = max(2, ($cellHeightPx - $scaledHeight) / 2);
|
|
|
|
|
|
|
|
|
|
|
|
$drawing->setOffsetX((int)$offsetX);
|
|
|
|
|
|
$drawing->setOffsetY((int)$offsetY);
|
|
|
|
|
|
$drawing->setWidth((int)$scaledWidth);
|
|
|
|
|
|
$drawing->setHeight((int)$scaledHeight);
|
|
|
|
|
|
$drawing->setWorksheet($sheet);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新行高以适应图片(留出一些边距)
|
|
|
|
|
|
$neededHeight = (int)($scaledHeight / 0.75) + 10;
|
|
|
|
|
|
if ($neededHeight > $maxRowHeight) {
|
|
|
|
|
|
$maxRowHeight = $neededHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果无法获取图片尺寸,使用默认尺寸
|
|
|
|
|
|
$drawing = new PHPExcel_Worksheet_Drawing();
|
|
|
|
|
|
$drawing->setPath($imagePath);
|
|
|
|
|
|
$drawing->setCoordinates($cell);
|
|
|
|
|
|
$drawing->setOffsetX(5);
|
|
|
|
|
|
$drawing->setOffsetY(5);
|
|
|
|
|
|
$drawing->setWidth($imageWidth);
|
|
|
|
|
|
$drawing->setHeight($imageHeight);
|
|
|
|
|
|
$drawing->setWorksheet($sheet);
|
|
|
|
|
|
}
|
2025-11-27 17:09:53 +08:00
|
|
|
|
} else {
|
2025-11-28 16:03:56 +08:00
|
|
|
|
$sheet->setCellValue($cell, '');
|
2025-11-27 17:09:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$sheet->setCellValue($cell, $value);
|
2025-11-28 16:03:56 +08:00
|
|
|
|
// 设置文本对齐和换行
|
|
|
|
|
|
$style = $sheet->getStyle($cell);
|
|
|
|
|
|
$style->getAlignment()->setVertical(\PHPExcel_Style_Alignment::VERTICAL_CENTER);
|
|
|
|
|
|
$style->getAlignment()->setWrapText(true);
|
|
|
|
|
|
// 根据列类型设置水平对齐
|
|
|
|
|
|
if (in_array($key, ['date', 'postTime'])) {
|
|
|
|
|
|
$style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_CENTER);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_LEFT);
|
|
|
|
|
|
}
|
2025-11-27 17:09:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-28 16:03:56 +08:00
|
|
|
|
|
|
|
|
|
|
// 设置行高
|
|
|
|
|
|
$sheet->getRowDimension($excelRow)->setRowHeight($maxRowHeight);
|
2025-11-27 17:09:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$safeName = preg_replace('/[^\w\-]/', '_', $fileName ?: 'export_' . date('Ymd_His'));
|
|
|
|
|
|
if (stripos($safeName, '.xlsx') === false) {
|
|
|
|
|
|
$safeName .= '.xlsx';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (ob_get_length()) {
|
|
|
|
|
|
ob_end_clean();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
|
|
|
|
header('Cache-Control: max-age=0');
|
|
|
|
|
|
header('Content-Disposition: attachment;filename="' . $safeName . '"');
|
|
|
|
|
|
|
|
|
|
|
|
$writer = PHPExcel_IOFactory::createWriter($excel, 'Excel2007');
|
|
|
|
|
|
$writer->save('php://output');
|
|
|
|
|
|
|
|
|
|
|
|
self::cleanupTempFiles();
|
|
|
|
|
|
exit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据列序号生成 Excel 列字母
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param int $index
|
|
|
|
|
|
* @return string
|
|
|
|
|
|
*/
|
|
|
|
|
|
protected static function columnLetter($index)
|
|
|
|
|
|
{
|
|
|
|
|
|
$letters = '';
|
|
|
|
|
|
do {
|
|
|
|
|
|
$letters = chr($index % 26 + 65) . $letters;
|
|
|
|
|
|
$index = intval($index / 26) - 1;
|
|
|
|
|
|
} while ($index >= 0);
|
|
|
|
|
|
|
|
|
|
|
|
return $letters;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将远程或本地图片路径转换为可用的本地文件路径
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param string $path
|
|
|
|
|
|
* @return string|null
|
|
|
|
|
|
*/
|
|
|
|
|
|
protected static function resolveImagePath($path)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (empty($path)) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (preg_match('/^https?:\/\//i', $path)) {
|
|
|
|
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'export_img_');
|
|
|
|
|
|
$stream = @file_get_contents($path);
|
|
|
|
|
|
if ($stream === false) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
file_put_contents($tempFile, $stream);
|
|
|
|
|
|
self::$tempFiles[] = $tempFile;
|
|
|
|
|
|
return $tempFile;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (file_exists($path)) {
|
|
|
|
|
|
return $path;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 清理所有临时文件
|
|
|
|
|
|
*/
|
|
|
|
|
|
protected static function cleanupTempFiles()
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (self::$tempFiles as $file) {
|
|
|
|
|
|
if (file_exists($file)) {
|
|
|
|
|
|
@unlink($file);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self::$tempFiles = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|