特殊行索引(1 起)=> 类型(title/header/summary) */ private array $specialRows = []; /** @var array 明细行索引 => 填色判断数据 */ private array $rowData = []; /** @var array{quantity: int, amount: float} 合计行填色判断数据 */ private array $summaryTotals = ['quantity' => 0, 'amount' => 0.0]; /** 列头所在行索引 */ private int $headerRow = 1; /** 数据末行索引 */ private int $lastRow = 1; public function __construct( private readonly PurchaseOrderModel $purchase, private readonly StoreModel $store, private readonly string $sheetTitle, ) { } /** * 导出行:标题/空行/列头/明细/合计 */ public function collection(): Collection { if ($this->rows !== null) { return $this->rows; } $items = app(PurchaseItemService::class)->storeSummaryRows($this->purchase->id, $this->store->id); $rows = []; $rowIndex = 0; // 标题行 $rows[] = [$this->store->name . ' · 采购单 ' . $this->purchase->purchase_no]; $this->specialRows[++$rowIndex] = 'title'; // 空行 $rows[] = ['']; $rowIndex++; // 列头 $rows[] = ['序号', '品名', '市场', '包规', '单位', '单价', '数量', '重量(斤)', '预计金额']; $this->specialRows[++$rowIndex] = 'header'; $this->headerRow = $rowIndex; // 明细行 $totalQuantity = 0; $totalWeight = '0'; $totalAmount = '0'; foreach (array_values($items) as $sort => $item) { $this->rowData[++$rowIndex] = [ 'quantity' => (int) $item['quantity'], 'amount' => (float) $item['amount'], ]; $rows[] = [ $sort + 1, $item['product_name'], $item['market'], $item['product_spec'], $item['unit'], (float) $item['price'], (int) $item['quantity'], (float) $item['weight'], (float) $item['amount'], ]; $totalQuantity += (int) $item['quantity']; $totalWeight = bcadd($totalWeight, (string) $item['weight'], 3); $totalAmount = bcadd($totalAmount, (string) $item['amount'], 2); } // 合计行 $rows[] = ['', '合计', '', '', '', '', $totalQuantity, (float) $totalWeight, (float) $totalAmount]; $this->specialRows[++$rowIndex] = 'summary'; $this->lastRow = $rowIndex; $this->summaryTotals = ['quantity' => $totalQuantity, 'amount' => (float) $totalAmount]; return $this->rows = collect($rows); } public function title(): string { return $this->sheetTitle; } /** * 标题/列头/合计加粗;全表居中 + 全边框;金额类列货币格式; * 数量(浅黄)/金额(浅红)按值条件填色(0 不填、列头固定填色),冻结列头 */ public function styles(Worksheet $sheet): array { $this->collection(); $sheet->freezePane('A' . ($this->headerRow + 1)); $widths = [6, 24, 12, 14, 8, 10, 10, 12, 12]; foreach ($widths as $index => $width) { $sheet->getColumnDimension(\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($index + 1)) ->setWidth($width); } // 全部单元格水平/垂直居中 $sheet->getStyle('A1:I' . $this->lastRow) ->getAlignment() ->setHorizontal(Alignment::HORIZONTAL_CENTER) ->setVertical(Alignment::VERTICAL_CENTER); // 表格区域(列头 → 合计行)添加所有边框 $sheet->getStyle('A' . $this->headerRow . ':I' . $this->lastRow) ->getBorders() ->getAllBorders() ->setBorderStyle(Border::BORDER_THIN); // 金额类列(单价/预计金额)货币格式:¥ + 两位小数 foreach (['F', 'I'] as $column) { $sheet->getStyle($column . ($this->headerRow + 1) . ':' . $column . $this->lastRow) ->getNumberFormat() ->setFormatCode(self::CURRENCY_FORMAT); } // 数量列(G):有数据浅黄,0 无背景,列头固定浅黄 $this->fillCell($sheet, 'G' . $this->headerRow, self::QUANTITY_FILL); foreach ($this->rowData as $row => $data) { if ($data['quantity'] > 0) { $this->fillCell($sheet, 'G' . $row, self::QUANTITY_FILL); } } if ($this->summaryTotals['quantity'] > 0) { $this->fillCell($sheet, 'G' . $this->lastRow, self::QUANTITY_FILL); } // 金额列(I):有数据浅红,0 无背景,列头固定浅红 $this->fillCell($sheet, 'I' . $this->headerRow, self::AMOUNT_FILL); foreach ($this->rowData as $row => $data) { if ($data['amount'] > 0) { $this->fillCell($sheet, 'I' . $row, self::AMOUNT_FILL); } } if ($this->summaryTotals['amount'] > 0) { $this->fillCell($sheet, 'I' . $this->lastRow, self::AMOUNT_FILL); } $styles = []; foreach ($this->specialRows as $row => $type) { $style = match ($type) { 'title' => ['font' => ['bold' => true, 'size' => 14]], 'header', 'summary' => ['font' => ['bold' => true]], default => [], }; if ($style !== []) { $styles[$row] = $style; } } return $styles; } /** * 单元格纯色填充 */ private function fillCell(Worksheet $sheet, string $coordinate, string $argb): void { $sheet->getStyle($coordinate) ->getFill() ->setFillType(Fill::FILL_SOLID) ->getStartColor() ->setARGB($argb); } }