Files
lgp-admin-plus-api/app/Service/common/ExcelCsvService.php

143 lines
4.8 KiB
PHP
Raw Normal View History

2026-08-20 19:16:49 +08:00
<?php
namespace App\Service\common;
/**
* 表格导出辅助:报价给结构化列/行(前端 ExcelJS xlsx小程序继续用 text。
* 仍保留 toCsv / parseUpload避免旧调用立刻断掉。
*/
class ExcelCsvService
{
private static mixed $_instance;
public static function getInstance(): null|static
{
$name = get_called_class();
if (!isset(self::$_instance[$name])) {
self::$_instance[$name] = new static();
}
return self::$_instance[$name];
}
/**
* 行数组转带 BOM CSV 文本
*/
public function toCsv(array $header, array $rows): string
{
$fp = fopen('php://temp', 'r+');
fprintf($fp, chr(0xEF) . chr(0xBB) . chr(0xBF));
fputcsv($fp, $header);
foreach ($rows as $row) {
$line = [];
foreach ($header as $key) {
$line[] = $row[$key] ?? '';
}
fputcsv($fp, $line);
}
rewind($fp);
$csv = stream_get_contents($fp);
fclose($fp);
return $csv === false ? '' : $csv;
}
/**
* 解析上传文件csv / 文本)
*/
public function parseUpload($file): array
{
if (empty($file)) {
UtilsService::getInstance()->errorThrow('请上传表格文件');
}
$path = is_string($file) ? $file : $file->getRealPath();
$raw = file_get_contents($path);
if ($raw === false) {
UtilsService::getInstance()->errorThrow('文件读取失败');
}
if (str_starts_with($raw, "\xEF\xBB\xBF")) {
$raw = substr($raw, 3);
}
$fp = fopen('php://temp', 'r+');
fwrite($fp, $raw);
rewind($fp);
$header = fgetcsv($fp);
if (empty($header)) {
fclose($fp);
UtilsService::getInstance()->errorThrow('表格没有表头');
}
$header = array_map(static fn ($v) => trim((string) $v), $header);
$rows = [];
while (($cols = fgetcsv($fp)) !== false) {
if ($this->rowEmpty($cols)) {
continue;
}
$item = [];
foreach ($header as $i => $key) {
$item[$key] = isset($cols[$i]) ? trim((string) $cols[$i]) : '';
}
$rows[] = $item;
}
fclose($fp);
return $rows;
}
/**
* 清单报价:后台用 ExcelJS xlsx小程序继续用 text 分享
*/
public function quoteFromList(array $info): array
{
$rows = [];
$lines = [];
$name = (string) ($info['name'] ?? $info['list_no'] ?? '报价');
$lines[] = $name;
foreach ($info['items'] ?? [] as $item) {
$title = (string) ($item['title'] ?? $item['catalogue']['title'] ?? $item['product']['title'] ?? '');
$spec = (string) ($item['specification'] ?? $item['price_sheet']['specification'] ?? '');
$qty = (int) ($item['quantity'] ?? 1);
$unit = (string) ($item['unit_price_text'] ?? '');
$total = (string) ($item['total_price_text'] ?? '');
$rows[] = [
'title' => $title,
'specification' => $spec,
'quantity' => $qty,
'unit_price' => $unit,
'total_price' => $total,
];
$lines[] = trim($title . ' ' . $spec . ' x' . $qty . ($unit !== '' ? ' ¥' . $unit : ''));
}
$payable = (string) ($info['payable_amount_text'] ?? $info['total_amount_text'] ?? '');
if ($payable !== '') {
$lines[] = '合计 ¥' . $payable;
}
$no = (string) ($info['list_no'] ?? 'quote');
return [
'filename' => 'quote-' . $no . '.xlsx',
'title' => $name,
'sheet' => '报价',
'subtitle' => '清单号 ' . $no,
'columns' => [
['key' => 'title', 'header' => '商品', 'width' => 28],
['key' => 'specification', 'header' => '规格', 'width' => 22],
['key' => 'quantity', 'header' => '数量', 'width' => 10, 'align' => 'center'],
['key' => 'unit_price', 'header' => '单价', 'width' => 14, 'align' => 'right'],
['key' => 'total_price', 'header' => '小计', 'width' => 14, 'align' => 'right'],
],
'rows' => $rows,
'summary' => [
'title' => '合计',
'total_price' => $payable === '' ? '' : ('¥' . $payable),
],
'text' => implode("\n", $lines),
];
}
private function rowEmpty(array $cols): bool
{
foreach ($cols as $col) {
if (trim((string) $col) !== '') {
return false;
}
}
return true;
}
}