You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

383 lines
13 KiB
PHP

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use LightnCandy\LightnCandy;
use Illuminate\Support\Facades\Storage;
/**
* 打印模板服务
*/
class PrintTemplateService
{
/**
* 根据ID获取模板
*/
public function getTemplateById(int $id)
{
return DB::table('print_templates')
->where('id', $id)
->where('status', 1)
->first();
}
/**
* 获取模板
*/
public function getTemplate(string $templateCode)
{
return DB::table('print_templates')
->where('template_code', $templateCode)
->where('status', 1)
->first();
}
/**
* 获取默认模板
*/
public function getDefaultTemplate(string $templateType)
{
return DB::table('print_templates')
->where('template_type', $templateType)
->where('is_default', 1)
->where('status', 1)
->first();
}
/**
* 获取模板列表(仅返回基本字段)
*/
public function getTemplateList(?string $templateType = null)
{
$query = DB::table('print_templates')
->where('status', 1)
->select([
'id',
'template_code',
'template_name',
'template_type',
'is_default',
'status',
'created_at',
'updated_at'
]);
if ($templateType) {
$query->where('template_type', $templateType);
}
return $query
->orderBy('created_at', 'desc')
->get();
}
/**
* 渲染模板(使用 LightnCandy
*/
public function renderTemplate(string $templateCode, array $data): string
{
$template = $this->getTemplate($templateCode);
if (!$template) {
throw new \Exception("模板不存在:{$templateCode}");
}
// 编译 Handlebars 模板为 PHP 代码
$phpCode = LightnCandy::compile($template->html_template, [
'flags' => LightnCandy::FLAG_ERROR_EXCEPTION |
LightnCandy::FLAG_HANDLEBARSJS |
LightnCandy::FLAG_RUNTIMEPARTIAL |
LightnCandy::FLAG_NOESCAPE |
LightnCandy::FLAG_BESTPERFORMANCE |
LightnCandy::FLAG_SPVARS,
'helpers' => $this->getHelpers(),
]);
// 执行编译后的 PHP 代码
$renderer = eval($phpCode);
$html = $renderer($data);
// 注入 CSS 样式
if ($template->css_style) {
$html = $this->injectStyles($html, $template->css_style);
}
return $html;
}
/**
* 获取自定义 Helper
*/
private function getHelpers(): array
{
return [
// 日期格式化
'formatDate' => function($args) {
$value = $args[0] ?? '';
$format = $args[1] ?? 'Y-m-d H:i:s';
if (!$value) return '';
return date($format, strtotime($value));
},
// 金额格式化
'formatMoney' => function($args) {
return number_format($args[0] ?? 0, 2);
},
// 等于 - Block Helper支持 {{#eq}}...{{else}}...{{/eq}}
'eq' => function($a, $b, $options) {
$isEqual = strval($a) === strval($b);
if (is_array($options) && isset($options['fn'])) {
// Block helper 模式
return $isEqual ? $options['fn']() : (isset($options['inverse']) ? $options['inverse']() : '');
}
// 简单模式(子表达式)
return $isEqual ? '1' : '';
},
// 不等于 - Block Helper
'ne' => function($a, $b, $options) {
$notEqual = strval($a) !== strval($b);
if ($options && isset($options['fn'])) {
return $notEqual ? $options['fn']() : (isset($options['inverse']) ? $options['inverse']() : '');
}
return $notEqual ? '1' : '';
},
// 大于 - Block Helper
'gt' => function($a, $b, $options) {
$result = $a > $b;
if ($options && isset($options['fn'])) {
return $result ? $options['fn']() : (isset($options['inverse']) ? $options['inverse']() : '');
}
return $result ? '1' : '';
},
// 小于 - Block Helper
'lt' => function($a, $b, $options) {
$result = $a < $b;
if ($options && isset($options['fn'])) {
return $result ? $options['fn']() : (isset($options['inverse']) ? $options['inverse']() : '');
}
return $result ? '1' : '';
},
// 大于等于 - Block Helper
'gte' => function($a, $b, $options) {
$result = $a >= $b;
if ($options && isset($options['fn'])) {
return $result ? $options['fn']() : (isset($options['inverse']) ? $options['inverse']() : '');
}
return $result ? '1' : '';
},
// 小于等于 - Block Helper
'lte' => function($a, $b, $options) {
$result = $a <= $b;
if ($options && isset($options['fn'])) {
return $result ? $options['fn']() : (isset($options['inverse']) ? $options['inverse']() : '');
}
return $result ? '1' : '';
},
// 返回值的 helper用于子表达式 {{#if (eqVal ...)}}
'eqVal' => function($args) {
$a = $args[0] ?? null;
$b = $args[1] ?? null;
return strval($a) === strval($b);
},
'neVal' => function($args) {
$a = $args[0] ?? null;
$b = $args[1] ?? null;
return strval($a) !== strval($b);
},
'gtVal' => function($args) {
return ($args[0] ?? 0) > ($args[1] ?? 0);
},
'ltVal' => function($args) {
return ($args[0] ?? 0) < ($args[1] ?? 0);
},
'gteVal' => function($args) {
return ($args[0] ?? 0) >= ($args[1] ?? 0);
},
'lteVal' => function($args) {
return ($args[0] ?? 0) <= ($args[1] ?? 0);
},
// 数组长度
'length' => function($args) {
return is_array($args[0]) ? count($args[0]) : 0;
},
// 加
'add' => function($args) {
return ($args[0] ?? 0) + ($args[1] ?? 0);
},
// 减
'sub' => function($args) {
return ($args[0] ?? 0) - ($args[1] ?? 0);
},
// 默认值
'default' => function($args) {
return $args[0] ?? $args[1] ?? '';
},
];
}
/**
* 注入样式
*/
private function injectStyles(string $html, string $css): string
{
$styleTag = sprintf('<style type="text/css">%s</style>', $css);
// 尝试插入到 <head> 中
if (preg_match('/<head\b[^>]*>/i', $html, $matches)) {
return str_replace($matches[0], $matches[0] . $styleTag, $html);
}
// 如果没有 head 标签,插入到 <html> 之后
if (preg_match('/<html\b[^>]*>/i', $html, $matches)) {
return str_replace($matches[0], $matches[0] . $styleTag, $html);
}
// 都没有,插入到最前面
return $styleTag . $html;
}
/**
* 替换条形码占位符
*/
private function replaceBarcodes(string $html): string
{
$generator = new \Picqer\Barcode\BarcodeGeneratorHTML();
// 匹配 <svg data-barcode="..."> 格式的条形码占位符
return preg_replace_callback(
'/<svg\s+data-barcode=["\']([^"\']+)["\'](?:\s+data-format=["\']([^"\']+)["\'])?(?:\s+data-width=["\']([^"\']+)["\'])?(?:\s+data-height=["\']([^"\']+)["\'])?\s*><\/svg>/i',
function ($matches) use ($generator) {
$code = $matches[1];
$format = $matches[2] ?? 'CODE128';
$width = intval($matches[3] ?? 2);
$height = intval($matches[4] ?? 50);
// 映射格式名称
$formatMap = [
'CODE128' => $generator::TYPE_CODE_128,
'CODE39' => $generator::TYPE_CODE_39,
'EAN13' => $generator::TYPE_EAN_13,
'UPC_A' => $generator::TYPE_UPC_A,
];
$type = $formatMap[$format] ?? $generator::TYPE_CODE_128;
// 生成条形码 HTML
$barcodeHtml = $generator->getBarcode($code, $type, $width, $height);
// 包装条形码,在下方添加编号显示
return sprintf(
'<div style="display:inline-block;">%s<div style="text-align:center;font-size:11px;margin-top:2px;">%s</div></div>',
$barcodeHtml,
htmlspecialchars($code)
);
},
$html
);
}
/**
* 生成 PDF使用 SnappyPdf
*/
public function generatePDF(string $templateCode, array $data, ?string $filePath = null)
{
$html = $this->renderTemplate($templateCode, $data);
// 替换条形码占位符
$html = $this->replaceBarcodes($html);
// 确保 HTML 是有效的 UTF-8
$html = mb_convert_encoding($html, 'UTF-8', 'UTF-8,GBK,GB2312');
// 获取模板的页面设置
$template = $this->getTemplate($templateCode);
$pageSettings = $template->page_settings ? json_decode($template->page_settings, true) : [];
$pdf = \Barryvdh\Snappy\Facades\SnappyPdf::loadHTML($html)
->setOption('encoding', 'UTF-8')
->setOption('enable-local-file-access', true)
->setOption('no-outline', true)
->setOption('disable-smart-shrinking', true);
// 应用页面设置
if (!empty($pageSettings['paper_size'])) {
$pdf->setOption('page-size', $pageSettings['paper_size']);
}
if (!empty($pageSettings['orientation'])) {
$pdf->setOption('orientation', $pageSettings['orientation']);
}
if (!empty($pageSettings['margin_top'])) {
$pdf->setOption('margin-top', $pageSettings['margin_top']);
}
if (!empty($pageSettings['margin_bottom'])) {
$pdf->setOption('margin-bottom', $pageSettings['margin_bottom']);
}
if (!empty($pageSettings['margin_left'])) {
$pdf->setOption('margin-left', $pageSettings['margin_left']);
}
if (!empty($pageSettings['margin_right'])) {
$pdf->setOption('margin-right', $pageSettings['margin_right']);
}
if ($filePath) {
// 确保目录存在
$directory = dirname($filePath);
if (!file_exists($directory)) {
mkdir($directory, 0755, true);
}
return $pdf->save($filePath);
}
return $pdf;
}
/**
* 验证模板语法
*/
public function validateTemplate(string $htmlTemplate): array
{
try {
LightnCandy::compile($htmlTemplate, [
'flags' => LightnCandy::FLAG_ERROR_EXCEPTION,
]);
return ['valid' => true, 'message' => '模板语法正确'];
} catch (\Exception $e) {
return ['valid' => false, 'message' => $e->getMessage()];
}
}
/**
* 保存模板
*/
public function saveTemplate(array $data): int
{
$templateId = $data['id'] ?? null;
$templateData = [
'template_code' => $data['template_code'],
'template_name' => $data['template_name'],
'template_type' => $data['template_type'],
'html_template' => $data['html_template'],
'css_style' => $data['css_style'] ?? '',
'data_mapping' => isset($data['data_mapping']) ? json_encode($data['data_mapping']) : null,
'page_settings' => isset($data['page_settings']) ? json_encode($data['page_settings']) : null,
'example_json' => $data['example_json'] ?? '',
'is_default' => $data['is_default'] ?? false,
'status' => $data['status'] ?? true,
'updated_at' => now(),
];
if ($templateId) {
// 更新
DB::table('print_templates')->where('id', $templateId)->update($templateData);
return $templateId;
} else {
// 新增
$templateData['created_at'] = now();
return DB::table('print_templates')->insertGetId($templateData);
}
}
}