特殊行索引(1 起)=> 类型(title/header) */ private array $specialRows = []; /** 列头所在行索引 */ private int $headerRow = 1; /** 数据末行索引 */ private int $lastRow = 1; /** @var array}> 明细行索引 => 填色判断数据 */ private array $rowData = []; /** * @param PurchaseOrderModel $purchase 采购单 * @param SupplierModel $supplier 供应商 * @param string $marketLabel 市场名(空市场已转为「未设置」) * @param array> $productRows 商品行(品名/汇总数量/各门店数量) * @param array $stores 门店列(门店ID => 名称) * @param string $sheetTitle 工作表名 */ public function __construct( private readonly PurchaseOrderModel $purchase, private readonly SupplierModel $supplier, private readonly string $marketLabel, private readonly array $productRows, private readonly array $stores, private readonly string $sheetTitle, ) { } /** * 导出行:标题+备注(合并区)/空行(合并区)/列头(品名、汇总、市场、各门店)/明细 */ public function collection(): Collection { if ($this->rows !== null) { return $this->rows; } $storeIds = array_map('intval', array_keys($this->stores)); $rows = []; $rowIndex = 0; // 标题行(A1:C2 合并占两行),D1 起放采购单备注(D1:I2 合并,红色 22 号字) $rows[] = [ $this->supplier->name . ' · ' . $this->marketLabel . ' · 采购单 ' . $this->purchase->purchase_no, '', '', trim((string) $this->purchase->remark), ]; $this->specialRows[++$rowIndex] = 'title'; // 空行(被标题/备注合并区域覆盖) $rows[] = ['']; $rowIndex++; // 列头:品名、汇总、市场、各门店明细 $rows[] = array_merge(['品名', '汇总', '市场'], array_values($this->stores)); $this->specialRows[++$rowIndex] = 'header'; $this->headerRow = $rowIndex; // 明细行(0 数量的门店格留空不填色) foreach ($this->productRows as $productRow) { $line = [ $productRow['product_name'], (int) $productRow['quantity'], $this->marketLabel, ]; $storeQuantities = []; foreach ($storeIds as $storeId) { $quantity = (int) ($productRow['store_quantities'][$storeId] ?? 0); $line[] = $quantity > 0 ? $quantity : ''; $storeQuantities[$storeId] = $quantity; } $rows[] = $line; $this->rowData[++$rowIndex] = [ 'quantity' => (int) $productRow['quantity'], 'store_quantities' => $storeQuantities, ]; } $this->lastRow = $rowIndex; return $this->rows = collect($rows); } public function title(): string { return $this->sheetTitle; } /** * 标题合并 A1:C2、备注合并 D1:I2(红色 22 号字);标题/列头加粗;全表居中 + 全边框 + 列头行自动换行; * 汇总列(浅黄)/门店数量列(浅蓝)按值条件填色(0 不填、列头固定填色),冻结列头 */ public function styles(Worksheet $sheet): array { $this->collection(); $sheet->freezePane('A' . ($this->headerRow + 1)); // 标题合并前三列占两行(A1:C2),备注合并六列两行(D1:I2) $sheet->mergeCells('A1:C2'); $sheet->mergeCells('D1:I2'); $widths = [24, 10, 6]; foreach ($widths as $index => $width) { $sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width); } $storeIds = array_map('intval', array_keys($this->stores)); $lastColumn = Coordinate::stringFromColumnIndex(3 + max(count($storeIds), 1)); foreach ($storeIds as $i => $storeId) { $sheet->getColumnDimension(Coordinate::stringFromColumnIndex(4 + $i))->setWidth(6); } // 全部单元格水平/垂直居中 $sheet->getStyle('A1:' . $lastColumn . $this->lastRow) ->getAlignment() ->setHorizontal(Alignment::HORIZONTAL_CENTER) ->setVertical(Alignment::VERTICAL_CENTER); // 列头行自动换行(门店列窄,门店名换行显示) $sheet->getStyle('A' . $this->headerRow . ':' . $lastColumn . $this->headerRow) ->getAlignment() ->setWrapText(true); // 备注合并单元格自动换行(长备注多行显示) $sheet->getStyle('D1')->getAlignment()->setWrapText(true); // 表格区域(列头 → 数据末行)添加所有边框 $sheet->getStyle('A' . $this->headerRow . ':' . $lastColumn . $this->lastRow) ->getBorders() ->getAllBorders() ->setBorderStyle(Border::BORDER_THIN); // 汇总列(B):有数据浅黄,0 无背景,列头固定浅黄 $this->fillCell($sheet, 'B' . $this->headerRow, self::QUANTITY_FILL); foreach ($this->rowData as $row => $data) { if ($data['quantity'] > 0) { $this->fillCell($sheet, 'B' . $row, self::QUANTITY_FILL); } } // 门店数量列(D 起):有数据浅蓝,0 无背景,列头固定浅蓝 foreach ($storeIds as $i => $storeId) { $column = Coordinate::stringFromColumnIndex(4 + $i); $this->fillCell($sheet, $column . $this->headerRow, self::STORE_FILL); foreach ($this->rowData as $row => $data) { if (($data['store_quantities'][$storeId] ?? 0) > 0) { $this->fillCell($sheet, $column . $row, self::STORE_FILL); } } } $styles = []; foreach ($this->specialRows as $row => $type) { $style = match ($type) { 'title' => ['font' => ['bold' => true, 'size' => 14]], 'header' => ['font' => ['bold' => true]], default => [], }; if ($style !== []) { $styles[$row] = $style; } } // 备注单元格(D1):红色 22 号字,须放在行样式之后应用以覆盖标题行字号 $styles['D1'] = ['font' => ['bold' => true, 'size' => 22, 'color' => ['argb' => 'FFFF0000']]]; return $styles; } /** * 单元格纯色填充 */ private function fillCell(Worksheet $sheet, string $coordinate, string $argb): void { $sheet->getStyle($coordinate) ->getFill() ->setFillType(Fill::FILL_SOLID) ->getStartColor() ->setARGB($argb); } }