Files
xin-procurement/app/Exports/ProductExport.php
T
2026-08-20 14:45:06 +08:00

127 lines
4.3 KiB
PHP

<?php
namespace App\Exports;
use App\Models\ProductCategoryModel;
use App\Models\ProductModel;
use App\Models\SupplierModel;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithStrictNullComparison;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
/**
* 商品档案导出:列格式与导入模板完全一致(首行即列头,导出文件修改后可直接重新导入)。
* 分类列输出「父分类/子分类」完整路径;状态列输出中文。
* template 模式仅输出列头 + 一行示例数据(首次导入前下载参考)。
*/
class ProductExport implements FromCollection, WithStrictNullComparison, WithStyles
{
/** 列头(与 ProductImport 的解析顺序一一对应,改动需同步) */
public const array HEADERS = ['品名', '分类', '供应商', '规格/包规', '单位', '成本价', '排序', '库存', '保质期', '状态', '备注'];
private ?Collection $rows = null;
/**
* @param int $categoryId 末级分类筛选(0=全部商品)
* @param bool $template 仅输出列头 + 示例行(导入模板)
*/
public function __construct(
private readonly int $categoryId = 0,
private readonly bool $template = false,
) {
}
/**
* 行结构:第 1 行列头,其后每行一件商品
*/
public function collection(): Collection
{
if ($this->rows !== null) {
return $this->rows;
}
$rows = [self::HEADERS];
if ($this->template) {
$rows[] = ['西红柿', '蔬菜/茄果类', '示例供应商', '10斤/箱', '斤', 2.5, 0, 100, 3, '上架', '示例行,导入前请删除'];
return $this->rows = collect($rows);
}
$products = ProductModel::query()
->when($this->categoryId > 0, fn ($query) => $query->where('category_id', $this->categoryId))
->orderBy('sort')
->orderBy('id')
->get()
->makeVisible('cost_price');
$categoryPaths = $this->categoryPaths($products->pluck('category_id')->unique()->all());
$supplierNames = SupplierModel::withTrashed()->pluck('name', 'id');
foreach ($products as $product) {
$rows[] = [
$product->name,
$categoryPaths[(int) $product->category_id] ?? '',
$supplierNames[(int) $product->supplier_id] ?? '',
$product->spec,
$product->unit,
(float) $product->cost_price,
(int) $product->sort,
(int) $product->stock,
(int) $product->shelf_life,
(int) $product->status === ProductModel::STATUS_ON ? '上架' : '下架',
$product->remark,
];
}
return $this->rows = collect($rows);
}
/**
* 列头加粗、冻结首行、设置列宽
*/
public function styles(Worksheet $sheet): array
{
$this->collection();
$sheet->freezePane('A2');
$widths = [20, 16, 14, 14, 8, 10, 8, 8, 8, 8, 24];
foreach ($widths as $index => $width) {
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
}
return [
1 => ['font' => ['bold' => true]],
];
}
/**
* 分类ID => 「父分类/子分类」完整路径(沿 parent_id 上溯拼接)
*
* @param array<int, int> $categoryIds
* @return array<int, string>
*/
private function categoryPaths(array $categoryIds): array
{
$categories = ProductCategoryModel::all()->keyBy('id');
$paths = [];
foreach ($categoryIds as $categoryId) {
$names = [];
$cursor = (int) $categoryId;
$guard = 0;
while ($cursor > 0 && $guard++ < 20) {
$category = $categories->get($cursor);
if ($category === null) {
break;
}
array_unshift($names, $category->name);
$cursor = (int) $category->parent_id;
}
$paths[(int) $categoryId] = implode('/', $names);
}
return $paths;
}
}