审批&PDF生成

This commit is contained in:
mzl
2025-09-08 11:34:19 +08:00
parent 8a4848bc75
commit 28368e27a7
30 changed files with 1269 additions and 980 deletions
@@ -0,0 +1,44 @@
<?php
namespace app\admin\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\admin\service\ApprovalFlowsService;
class ApprovalFlowsController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new ApprovalFlowsService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
protected function indexFormat($data)
{
if (isset($data['created_at']))
$data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
if (isset($data['updated_at']))
$data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
return $data;
}
public function desc()
{
// type sequential(顺序会签), parallel(并行会签), any(或签)
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace app\admin\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\admin\model\ApprovalFlows;
class ApprovalFlowsLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = ApprovalFlows::class;
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace app\admin\model;
use support\Model;
class ApprovalFlows extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'approval_flows';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'name', 'type', 'steps', 'is_active', 'timeout_hours', 'timeout_action', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
protected $casts = [
'steps' => 'array',
];
}
@@ -0,0 +1,62 @@
<?php
namespace app\admin\service;
use app\admin\validate\ApprovalFlowsValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\admin\model\ApprovalFlows;
use support\Request;
class ApprovalFlowsService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = ApprovalFlows::class;
/**
* 验证器
*/
protected $validator = ApprovalFlowsValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
protected function buildQueryConfig(Request $request, string $scene = 'index'): array
{
$p = $request->all();
$config = [];
$config['model'] = $this->modelClass;
switch ($scene) {
case 'index':
$config['order']['id'] = 'desc';
break;
case 'show':
$config['where']['id'] = $p['id'];
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
}
@@ -0,0 +1,29 @@
<?php
namespace app\admin\validate;
use think\Validate;
class ApprovalFlowsValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'store' => [
],
'update' => [
]
];
}
+141
View File
@@ -8,6 +8,7 @@ use app\common\model\StockOrderItems;
use app\common\validate\StockOrdersValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\StockOrders;
use app\common\model\SysCurrency;
use support\Request;
use app\common\model\User;
use support\exception\BusinessException;
@@ -64,6 +65,7 @@ class StockOrdersService
$config['order']['id'] = 'desc';
break;
case 'show':
case 'print':
$config['where']['id'] = $p['id'];
$config['with'][] = 'items';
@@ -174,4 +176,143 @@ class StockOrdersService
}
return $res;
}
public function servicePrint(Request $request)
{
$config = $this->buildQueryConfig($request, 'print');
$item = $this->logicShow($config);
$pdfData = $this->pdfData($item);
// 渲染模板为最终HTML
$tplPath = app_path('common/utils/pdf/template/stockOrder.stub');
$html = \app\common\utils\pdf\service\PdfHelper::render($tplPath, ['data' => $pdfData]);
// 生成PDF
$driver = new \app\common\utils\pdf\service\MpdfDriver();
$pdfBin = $driver->htmlToPdf($html);
// 存储
$dir = public_path('pdf');
$fileName = '/'.($pdfData['order_no'] ?? date('YmdHis')) . '.pdf';
// 本地保存
$filePath = \app\common\utils\pdf\service\PdfStorage::saveBinary(
$pdfBin,
$dir,
$fileName
);
// // 在线预览
// return new \support\Response(200, [
// 'Content-Type' => 'application/pdf',
// 'Content-Disposition' => 'inline; filename="stock-order.pdf"',
// ], $pdfBin);
return ['file_url' => $dir.$fileName];
}
public function pdfData($item)
{
$vehList = [];
$index = 0;
foreach ($item->items as $k => $v) {
// 存在则数量加1
if (count($vehList) > 0 && isset($vehList[$v->sku_id])) {
$vehList[$v->sku_id]['num'] += 1;
$vehList[$v->sku_id]['total_price'] += $vehList[$v->sku_id]['unit_price'];
$vehList[$v->sku_id]['total_price_text'] = $vehList[$v->sku_id]['total_price_symbol'].number_format($vehList[$v->sku_id]['total_price'], 2, '.', ',');
continue;
}
$index += 1;
// 处理金额
$mv = [];
foreach ($v->moneyValues as $money) {
$mv[$money->field] = [
'currency' => $money->currency,
'amount' => $money->amount,
'rate' => $money->rate,
'currency_symbol' => SysCurrency::where('currency_code', $money->currency)->value('currency_symbol'),
];
}
// 处理改装
$mfy = '';
foreach ($v->options as $my) {
$mfy .= $my->option_name_snapshot.' ';
}
// 处理结果集
$vehList[$v->sku_id] = [
'index' => $index,
'model_snapshot' => $v->model_snapshot,
'color_interior_snapshot' => $v->color_interior_snapshot,
'color_exterior_snapshot' => $v->color_exterior_snapshot,
'num' => 1,
'modify' => $mfy,
'unit_price' => $mv['unit_price']['amount'],
'total_price' => $mv['total_price']['amount'],
'official_price' => $mv['official_price']['amount'],
'discount_price' => $mv['discount_price']['amount'],
'unit_price_symbol' => $mv['unit_price']['currency_symbol'],
'total_price_symbol' => $mv['total_price']['currency_symbol'],
'official_price_symbol' => $mv['official_price']['currency_symbol'],
'discount_price_symbol' => $mv['discount_price']['currency_symbol'],
'unit_price_text' => $mv['unit_price']['currency_symbol'].number_format($mv['unit_price']['amount'], 2, '.', ','),
'total_price_text' => $mv['total_price']['currency_symbol'].number_format($mv['total_price']['amount'], 2, '.', ','),
'official_price_text' => $mv['official_price']['currency_symbol'].number_format($mv['official_price']['amount'], 2, '.', ','),
'discount_price_text' => $mv['discount_price']['currency_symbol'].number_format($mv['discount_price']['amount'], 2, '.', ','),
];
}
$accList = [];
$accIndex = 1;
foreach ($item->accounts as $k => $v) {
$accList[] = [
'index' => '收款'.$k + 1,
'remit_country_name_snapshot' => $v->remit_country_name_snapshot,
'remit_date' => date('Y-m-d', $v->remit_date),
'currency_name_snapshot' => $v->currency_name_snapshot,
'account_name_snapshot' => $v->account_name_snapshot,
'remitter_name' => $v->remitter_name,
'amount' => $v->amount,
];
$accIndex += 1;
}
$veh = [
'order_no' => $item->order_no,
'salesperson_snapshot' => $item->salesperson_snapshot,
'pi_no' => $item->pi_no,
'biz_date' => date('Y-m-d', $item->biz_date),
'clearance_country_snapshot' => $item->clearance_country_snapshot,
'customer_source_snapshot' => $item->customer_source_snapshot,
'customer_snapshot' => $item->customer_snapshot,
'customer_country_snapshot' => $item->customer_country_snapshot,
// 订单项
'veh_list' => $vehList,
'contract_exchange_rate' => $item->contract_exchange_rate,
'cif_total_amount' => $item->cif_total_amount,
'declarant_name' => $item->declarant_name,
'eta_ship_date' => date('Y-m-d', $item->eta_ship_date),
'shipping_method_snapshot' => $item->shipping_method_snapshot,
'domestic_port_snapshot' => $item->domestic_port_snapshot,
'trade_term_snapshot' => $item->trade_term_snapshot,
'clearance_port_snapshot' => $item->clearance_port_snapshot,
'latest_arrival_date' => date('Y-m-d', $item->latest_arrival_date),
// 账户
'account_list' => $accList,
'acc_index' => $accIndex
];
return $veh;
}
}
@@ -1,6 +1,6 @@
<?php
namespace app\common\approval\model;
namespace app\common\utils\approval\model;
use support\Model;
@@ -1,6 +1,6 @@
<?php
namespace app\common\approval\model;
namespace app\common\utils\approval\model;
use support\Model;
@@ -1,6 +1,6 @@
<?php
namespace app\common\approval\model;
namespace app\common\utils\approval\model;
use support\Model;
@@ -1,6 +1,6 @@
<?php
namespace app\common\approval\model;
namespace app\common\utils\approval\model;
use support\Model;
@@ -1,15 +1,9 @@
<?php
namespace app\common\approval\service;
namespace app\common\utils\approval\service;
use DateTime;
use support\exception\BusinessException;
use support\Db;
use app\common\approval\model\ApprovalCcs;
use app\common\approval\model\ApprovalFlows;
use app\common\approval\model\ApprovalInstances;
use app\common\approval\model\ApprovalRecords;
class ApprovalFlowService
{
// --- 公共方法 ---
@@ -1,5 +1,5 @@
<?php
namespace app\common\approval\service;
namespace app\common\utils\approval\service;
use DateTime;
use support\exception\BusinessException;
@@ -1,138 +0,0 @@
<?php
namespace app\common\utils\pdf\controller;
use support\Request;
use support\Response;
use app\common\utils\pdf\service\document\ReimbursementDocumentService;
use app\common\utils\pdf\service\PdfGeneratorService; // 用于通用 HTML 生成
use Exception;
class DocumentController
{
protected $noNeedLogin = ['downloadReimbursementPdf', 'viewReimbursementPdf'];
protected $noNeedHeaderData = ['downloadReimbursementPdf', 'viewReimbursementPdf'];
/**
* 下载报销单 PDF
* 访问路由: GET /document/reimbursement/{id}
* 示例: /document/reimbursement/123
*/
public function downloadReimbursementPdf(Request $request, int $id)
{
try {
$service = new ReimbursementDocumentService();
$pdfData = $service->generatePdfById($id);
$filename = 'reimbursement_' . $id . '_' . date('Ymd_His') . '.pdf';
return response($pdfData, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
]);
} catch (Exception $e) {
// 记录日志
// \support\Log::error("Failed to generate reimbursement PDF: " . $e->getMessage());
return json(['code' => 1, 'msg' => 'Failed to generate PDF: ' . $e->getMessage()]);
}
}
/**
* 在浏览器中查看报销单 PDF
* 访问路由: GET /document/reimbursement/view/{id}
* 示例: /document/reimbursement/view/123
*/
public function viewReimbursementPdf(Request $request, int $id)
{
try {
$service = new ReimbursementDocumentService();
$pdfData = $service->generatePdfById($id);
return response($pdfData, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="reimbursement_view_' . $id . '.pdf"', // 'inline' 关键
]);
} catch (Exception $e) {
return json(['code' => 1, 'msg' => 'Failed to view PDF: ' . $e->getMessage()]);
}
}
/**
* 保存报销单 PDF 到服务器
* 访问路由: POST /document/reimbursement/save
* 参数: id (POST)
*/
public function saveReimbursementPdf(Request $request)
{
$id = $request->post('id');
if (!$id || !is_numeric($id)) {
return json(['code' => 1, 'msg' => 'Invalid ID']);
}
try {
// 指定保存路径
$savePath = runtime_path() . '/generated_pdfs/reimbursements/';
$service = new ReimbursementDocumentService();
$service->savePdfToFile((int)$id, $savePath);
return json(['code' => 0, 'msg' => 'PDF saved successfully', 'path' => $savePath]);
} catch (Exception $e) {
return json(['code' => 1, 'msg' => 'Failed to save PDF: ' . $e->getMessage()]);
}
}
/**
* 通用 HTML 到 PDF 生成接口 (示例)
* 访问路由: POST /document/generate-from-html
* 参数: html_content (POST)
*/
public function generateFromHtml(Request $request)
{
$htmlContent = $request->post('html_content');
if (!$htmlContent) {
return json(['code' => 1, 'msg' => 'HTML content is required']);
}
try {
$generator = new PdfGeneratorService();
// 这里直接传入 HTML,不需要模板。可以创建一个简单的 'html_wrapper' 模板来应用样式。
// 或者直接处理 HTML 字符串。
// 为简化,我们假设传入的 HTML 已经包含所需样式。
// 注意:TCPDF 对复杂 CSS 支持有限,内联样式通常更可靠。
// 创建一个临时数据结构
$tempData = ['content' => $htmlContent];
// 创建一个简单的模板 app/view/pdf/html_wrapper.php
/* 内容示例: <?php echo $data['content']; ?> */
// 但更简单的是直接使用 TCPDF 的 writeHTMLCell 或类似方法处理原始 HTML 片段
// 这里我们直接调用 TCPDF (绕过服务的模板机制)
$pdf = new \TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
$pdf->SetCreator('Your System');
$pdf->SetTitle('Generated from HTML');
$pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED);
$pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT);
$pdf->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM);
$pdf->setImageScale(PDF_IMAGE_SCALE_RATIO);
$pdf->setFontSubsetting(true);
$pdf->SetFont('dejavusans', '', 10, '', true);
$pdf->AddPage();
// 直接写入 HTML
$pdf->writeHTML($htmlContent, true, false, true, false, '');
$pdf->endPage();
$pdfData = $pdf->Output('', 'S');
$filename = 'custom_html_' . date('Ymd_His') . '.pdf';
return response($pdfData, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
]);
} catch (Exception $e) {
return json(['code' => 1, 'msg' => 'Failed to generate PDF from HTML: ' . $e->getMessage()]);
}
}
}
@@ -0,0 +1,76 @@
<?php
// app/controller/PdfController.php
namespace app\controller;
use support\Request;
use support\Response;
use support\View;
use app\common\utils\pdf\service\MpdfDriver;
use app\common\utils\pdf\service\PdfHelper;
class PdfController
{
// ① 从“现有 HTML 文件 + 外链CSS”生成并在线预览
public function previewFromFile(Request $request): Response
{
// 你的模板路径(示例)
$tpl = base_path('resource/templates/stock-order.html');
// 可选:把 {{变量}} 替换
$html = PdfHelper::loadTemplate($tpl, [
'title' => '备货单',
'order_no' => 'SO' . date('YmdHis'),
'customer' => 'Magic Bridge Auto FZE',
]);
$driver = new MpdfDriver();
$pdf = $driver->htmlToPdf($html, [
// 若 HTML 里用到相对资源(img/css),给个基准目录
'asset_base' => public_path('assets/pdf'),
// 也可以单独传 CSS 文件(推荐把 CSS 放在 public/assets/pdf)
'css_files' => [
public_path('assets/pdf/stock-order.css'),
],
'header_html' => '<div style="text-align:right;color:#888;font-size:10px">Company • Stock Order</div>',
'footer_html' => '<div style="text-align:center;color:#888;font-size:10px">{PAGENO} / {nbpg}</div>',
'watermark_text' => '', // 'DRAFT'
]);
return new Response(200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="stock-order.pdf"',
], $pdf);
}
// ② 直接下载
public function download(Request $request): Response
{
$html = '<h1>发票</h1><p>客户:永生花</p>';
$pdf = (new MpdfDriver())->htmlToPdf($html);
return new Response(200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="invoice.pdf"',
], $pdf);
}
// ③ 保存到本地(或上传 OSS 前先落盘)
public function save(Request $request): Response
{
$tpl = base_path('resource/templates/invoice.html');
$html = PdfHelper::loadTemplate($tpl, [
'invoice_no' => 'INV' . date('YmdHis'),
'amount' => '12345.67',
]);
$pdf = (new MpdfDriver())->htmlToPdf($html, [
'css_files' => [ public_path('assets/pdf/invoice.css') ],
'asset_base'=> public_path('assets/pdf'),
]);
$dir = runtime_path('pdf');
if (!is_dir($dir)) mkdir($dir, 0777, true);
$path = $dir . '/invoice-' . date('YmdHis') . '.pdf';
file_put_contents($path, $pdf);
return json(['path' => $path]);
}
}
+118
View File
@@ -0,0 +1,118 @@
<?php
// app/service/pdf/MpdfDriver.php
namespace app\common\utils\pdf\service;
use Mpdf\Mpdf;
use Mpdf\Config\ConfigVariables;
use Mpdf\Config\FontVariables;
use Mpdf\HTMLParserMode;
class MpdfDriver
{
public function htmlToPdf(string $html, array $options = []): string
{
$opts = array_merge([
'format' => 'A4',
'orientation' => 'L',
'margin_top' => 12,
'margin_bottom' => 12,
'margin_left' => 10,
'margin_right' => 10,
'tempDir' => runtime_path('mpdf'),
'header_html' => '',
'footer_html' => '',
'watermark_text'=> '',
'css_files' => [],
'default_font' => 'notosanssc',
'font_dirs' => [public_path('fonts')],
'font_data' => [
'notosanssc' => ['R'=>'NotoSansSC-Regular.ttf','B'=>'NotoSansSC-Bold.ttf'],
],
'asset_base' => '',
'enable_html5_parser' => true,
], $options);
$defaultConfig = (new ConfigVariables())->getDefaults();
$defaultFontConfig = (new FontVariables())->getDefaults();
$fontDirs = array_merge($defaultConfig['fontDir'], $opts['font_dirs']);
$fontData = array_merge($defaultFontConfig['fontdata'], $opts['font_data']);
$mpdf = new Mpdf([
'format' => $opts['format'],
'orientation' => $opts['orientation'],
'margin_left' => $opts['margin_left'],
'margin_right' => $opts['margin_right'],
'margin_top' => $opts['margin_top'],
'margin_bottom' => $opts['margin_bottom'],
'tempDir' => $opts['tempDir'],
'fontDir' => $fontDirs,
'fontdata' => $fontData,
'default_font' => $opts['default_font'],
'enable_html5_parser' => $opts['enable_html5_parser'],
]);
if ($opts['header_html']) $mpdf->SetHTMLHeader($opts['header_html']);
if ($opts['footer_html']) $mpdf->SetHTMLFooter($opts['footer_html']);
if (!empty($opts['watermark_text'])) {
$mpdf->SetWatermarkText($opts['watermark_text']);
$mpdf->showWatermarkText = true;
$mpdf->watermarkTextAlpha = 0.08;
}
// 资源绝对化(如果你有相对路径的图片等)
$html = PdfHelper::absolutizeAssetUrls($html, $opts['asset_base']);
// === 关键改动:把 <style>…</style> 抽出来,作为 HEADER_CSS 写入 ===
[$inlineCss, $cleanHtml] = $this->extractCssAndCleanHtml($html);
// 先写外部 css_files
if (!empty($opts['css_files'])) {
foreach ($opts['css_files'] as $cssFile) {
if (is_file($cssFile)) {
$mpdf->WriteHTML(file_get_contents($cssFile), HTMLParserMode::HEADER_CSS);
}
}
}
// 再写内联 <style> 的 CSS
if ($inlineCss !== '') {
$mpdf->WriteHTML($inlineCss, HTMLParserMode::HEADER_CSS);
}
// 最后写 HTML 正文(已剔除 <style>,避免任何情况下把 CSS 当文字打印)
$mpdf->WriteHTML($cleanHtml, HTMLParserMode::HTML_BODY);
return $mpdf->Output('', \Mpdf\Output\Destination::STRING_RETURN);
}
/**
* 把所有 <style>...</style> 的内容拼成一段 CSS,并把这些 <style> 标签从 HTML 中移除
* @return array{0:string,1:string} [css, htmlWithoutStyle]
*/
private function extractCssAndCleanHtml(string $html): array
{
$cssParts = [];
// 抓取所有 <style ...>...</style>,DOTALL 支持跨行
if (preg_match_all('/<style\b[^>]*>(.*?)<\/style>/is', $html, $m)) {
foreach ($m[1] as $block) {
// 去掉 HTML 注释,以防注释把 CSS 包起来
$block = preg_replace('/<!--|-->/', '', $block);
$cssParts[] = trim($block);
}
// 从 HTML 删除所有 <style> 块
$html = preg_replace('/<style\b[^>]*>.*?<\/style>/is', '', $html);
}
$css = implode("\n\n", $cssParts);
// 额外保险:把 <head> 也清理掉,防止某些解析器把 head 里残余文本当正文
$html = preg_replace('/<head\b[^>]*>.*?<\/head>/is', '', $html);
// 确保剩余 HTML 有 <body>,没有就包一层
if (!preg_match('/<body\b/i', $html)) {
$html = '<body>' . $html . '</body>';
}
return [$css, $html];
}
}
@@ -1,179 +0,0 @@
<?php
namespace app\common\utils\pdf\service;
use TCPDF;
use Exception;
class PdfGeneratorService
{
private $templatePath;
private $defaultOptions;
public function __construct()
{
// 设置模板基础路径
$this->templatePath = app_path() . '/common/utils/pdf/template/';
// 设置 TCPDF 默认选项
$this->defaultOptions = [
'page_orientation' => 'P', // PDF_PAGE_ORIENTATION
'unit' => 'mm', // PDF_UNIT
'page_format' => 'A4', // PDF_PAGE_FORMAT
'unicode' => true,
'encoding' => 'UTF-8',
// --- 修改字体设置 ---
// 'font' => 'dejavusans', // DejaVu 字体有时可能需要额外配置
'font' => 'stsongstdlight', // 使用 TCPDF 内置的简体中文支持字体
'font_style' => '', // 字体样式 (e.g., 'B' for bold)
'font_size' => 10,
// --- 字体设置结束 ---
'creator' => 'Your Company Internal System',
'author' => 'Your Company',
'margin_left' => 15,
'margin_right' => 15,
'margin_top' => 20,
'margin_bottom' => 20,
'margin_header' => 5,
'margin_footer' => 10,
];
}
/**
* 渲染模板并返回 HTML 字符串
*
* @param string $templateName 模板文件名 (不含 .php)
* @param array $data 注入模板的数据
* @return string 渲染后的 HTML
* @throws Exception
*/
private function renderTemplate(string $templateName, array $data): string
{
$templateFile = $this->templatePath . $templateName . '.php';
if (!file_exists($templateFile)) {
throw new Exception("PDF template file not found: {$templateFile}");
}
// 提取数据变量到当前作用域,供模板使用
extract($data);
// 开启输出缓冲
ob_start();
// 包含模板文件
include $templateFile;
// 获取缓冲区内容并清理缓冲区
$htmlContent = ob_get_clean();
if ($htmlContent === false) {
throw new Exception("Failed to render PDF template: {$templateName}");
}
return $htmlContent;
}
/**
* 生成 PDF
*
* @param string $templateName 模板文件名 (不含 .php)
* @param array $data 注入模板的数据
* @param array $options (可选)覆盖默认的 TCPDF 选项
* @return string PDF 二进制数据
* @throws Exception
*/
public function generate(string $templateName, array $data, array $options = []): string
{
// 合并选项
$options = array_merge($this->defaultOptions, $options);
// 渲染 HTML
$html = $this->renderTemplate($templateName, $data);
// 创建 TCPDF 实例
$pdf = new TCPDF(
$options['page_orientation'],
$options['unit'],
$options['page_format'],
$options['unicode'],
$options['encoding']
);
// 设置文档信息
$pdf->SetCreator($options['creator']);
$pdf->SetAuthor($options['author']);
$pdf->SetTitle($templateName . ' - Generated PDF');
// --- 修改字体设置 ---
// 使用支持中文的字体
// 'stsongstdlight' 是 TCPDF 内置的简体中文 CJK 字体
// 'cid0cs' 也是支持中文的字体,可以尝试
$pdf->SetFont($options['font'], $options['font_style'], $options['font_size'], '', true);
// --- 字体设置结束 ---
// 设置页边距
$pdf->SetMargins($options['margin_left'], $options['margin_top'], $options['margin_right']);
$pdf->SetHeaderMargin($options['margin_header']);
$pdf->SetFooterMargin($options['margin_footer']);
// 设置自动分页
$pdf->SetAutoPageBreak(TRUE, $options['margin_bottom']);
// 设置图像比例因子
$pdf->setImageScale(PDF_IMAGE_SCALE_RATIO);
// 添加首页
$pdf->AddPage();
// 写入 HTML 内容
$pdf->writeHTML($html, true, false, true, false, '');
// 结束最后一页
$pdf->endPage();
// 返回 PDF 字符串
return $pdf->Output('', 'S'); // 'S' 返回字符串
}
/**
* 保存 PDF 到文件
*
* @param string $templateName 模板文件名
* @param array $data 注入模板的数据
* @param string $filePath 保存的完整文件路径
* @param array $options (可选)TCPDF 选项
* @throws Exception
*/
public function saveToFile(string $templateName, array $data, string $filePath, array $options = [])
{
$options = array_merge($this->defaultOptions, $options);
$html = $this->renderTemplate($templateName, $data);
$pdf = new TCPDF(
$options['page_orientation'],
$options['unit'],
$options['page_format'],
$options['unicode'],
$options['encoding']
);
$pdf->SetCreator($options['creator']);
$pdf->SetAuthor($options['author']);
$pdf->SetTitle($templateName . ' - Saved PDF');
// --- 修改字体设置 ---
$pdf->SetFont($options['font'], $options['font_style'], $options['font_size'], '', true);
// --- 字体设置结束 ---
$pdf->SetMargins($options['margin_left'], $options['margin_top'], $options['margin_right']);
$pdf->SetAutoPageBreak(TRUE, $options['margin_bottom']);
$pdf->setImageScale(PDF_IMAGE_SCALE_RATIO);
$pdf->AddPage();
$pdf->writeHTML($html, true, false, true, false, '');
$pdf->endPage();
// 确保目录存在
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$pdf->Output($filePath, 'F'); // 'F' 保存到文件
}
}
@@ -0,0 +1,80 @@
<?php
// app/service/pdf/PdfHelper.php
namespace app\common\utils\pdf\service;
class PdfHelper
{
/** 从文件加载 HTML(支持占位符 {{key}} 简单替换) */
public static function loadTemplate(string $file, array $vars = []): string
{
if (!is_file($file)) {
throw new \RuntimeException("Template not found: {$file}");
}
$html = file_get_contents($file);
if ($vars) {
$search = [];
$replace = [];
foreach ($vars as $k => $v) {
$search[] = '{{' . $k . '}}';
$replace[] = htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$html = str_replace($search, $replace, $html);
}
return $html;
}
/**
* 把 HTML 里的相对资源路径(img/link/script href/src)改成绝对路径
* 仅处理以 ./ ../ 或非 http(s) 的相对量;base 传 public/assets/pdf 等目录
*/
public static function absolutizeAssetUrls(string $html, string $baseDir): string
{
$baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR);
// img src
$html = preg_replace_callback('#(<img[^>]+src=["\'])([^"\']+)(["\'])#i', function($m) use ($baseDir){
$u = $m[2];
if (preg_match('#^https?://#i', $u) || strpos($u, 'data:') === 0) return $m[0];
$abs = realpath($baseDir . DIRECTORY_SEPARATOR . ltrim($u, '/')) ?: ($baseDir . '/' . ltrim($u, '/'));
return $m[1] . $abs . $m[3];
}, $html);
// link href(如果你在 HTML 里写了 <link>,通常建议走 css_files 方式,这里也兼容)
$html = preg_replace_callback('#(<link[^>]+href=["\'])([^"\']+)(["\'])#i', function($m) use ($baseDir){
$u = $m[2];
if (preg_match('#^https?://#i', $u)) return $m[0];
$abs = realpath($baseDir . DIRECTORY_SEPARATOR . ltrim($u, '/')) ?: ($baseDir . '/' . ltrim($u, '/'));
return $m[1] . $abs . $m[3];
}, $html);
return $html;
}
/**
* 渲染一个原生 PHP 模板为字符串
* @param string $file 模板路径(如 resource/templates/stock-order.php)
* @param array $vars 将在模板中以变量名存在(如 $data)
* @return string 完整 HTML
*/
public static function render(string $file, array $vars = []): string
{
if (!is_file($file)) {
throw new \RuntimeException("Template not found: {$file}");
}
// 将键名导出为同名变量,例如 'data' => [...] 变为 $data
extract($vars, EXTR_OVERWRITE);
// 开启缓冲,将 include 的输出抓取为字符串
ob_start();
ob_implicit_flush(false);
try {
/** @noinspection PhpIncludeInspection */
include $file;
} catch (\Throwable $e) {
// 清理缓冲并抛出更清晰的异常
ob_end_clean();
throw $e;
}
return ob_get_clean();
}
}
@@ -0,0 +1,38 @@
<?php
namespace app\common\utils\pdf\service;
final class PdfStorage
{
/**
* @param string $pdfBin PDF 二进制
* @param string|null $dir 保存目录,默认 runtime/pdf
* @param string|null $name 文件名(不含目录),默认:stock-order-YYYYmmdd-His-uniqid.pdf
* @return string 绝对路径
*/
public static function saveBinary(string $pdfBin, ?string $dir = null, ?string $name = null): string
{
$dir = $dir ?: runtime_path('pdf');
if (!is_dir($dir)) {
if (!@mkdir($dir, 0775, true) && !is_dir($dir)) {
throw new \RuntimeException("Failed to create dir: {$dir}");
}
}
// 生成安全文件名
$name = $name ?: ('stock-order-' . date('Ymd-His') . '-' . substr(uniqid('', true), -6) . '.pdf');
$name = preg_replace('/[^a-zA-Z0-9_\-.]/', '_', $name); // 简易净化
$path = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $name;
// 先写入临时文件,再原子重命名,避免并发/写入中被读取
$tmp = $path . '.part';
if (file_put_contents($tmp, $pdfBin, LOCK_EX) === false) {
throw new \RuntimeException("Failed to write temp file: {$tmp}");
}
if (!@rename($tmp, $path)) {
@unlink($tmp);
throw new \RuntimeException("Failed to move temp to: {$path}");
}
@chmod($path, 0664);
return $path;
}
}
@@ -1,82 +0,0 @@
<?php
namespace app\common\utils\pdf\service\document;
use app\common\utils\pdf\service\PdfGeneratorService;
use Exception;
class ReimbursementDocumentService
{
private $pdfGenerator;
public function __construct()
{
$this->pdfGenerator = new PdfGeneratorService();
}
/**
* 根据报销单 ID 生成 PDF 数据
* (这里模拟从数据库获取数据)
*
* @param int $reimbursementId
* @return string PDF 二进制数据
* @throws Exception
*/
public function generatePdfById(int $reimbursementId): string
{
// --- 模拟从数据库获取数据 ---
// 实际应用中,这里应该是查询数据库
$reimbursementData = $this->getMockReimbursementData($reimbursementId);
if (!$reimbursementData) {
throw new Exception("Reimbursement record not found for ID: {$reimbursementId}");
}
// --- 数据获取结束 ---
// 调用核心 PDF 服务生成 PDF
// 'reimbursement_template' 对应 app/view/pdf/reimbursement_template.php
return $this->pdfGenerator->generate('reimbursement', $reimbursementData);
}
/**
* 模拟获取报销单数据
* @param int $id
* @return array|null
*/
private function getMockReimbursementData(int $id)
{
if ($id <= 0) return null;
return [
'reimbursement_id' => 'REIM-' . str_pad($id, 5, '0', STR_PAD_LEFT),
'applicant_name' => '张三',
'department' => '技术部',
'application_date' => '2023-10-26',
'total_amount' => 1250.75,
'reason' => "参加北京技术研讨会的差旅费用。\n包括交通、住宿和餐饮。",
'items' => [
['date' => '2023-10-20', 'category' => '交通费', 'description' => '高铁二等座', 'amount' => 520.00, 'remark' => ''],
['date' => '2023-10-21', 'category' => '住宿费', 'description' => 'XX酒店 2 晚', 'amount' => 680.00, 'remark' => '含早餐'],
['date' => '2023-10-21', 'category' => '餐饮费', 'description' => '午餐晚餐', 'amount' => 50.75, 'remark' => '发票已附'],
]
];
}
/**
* 保存报销单 PDF 到服务器
* @param int $reimbursementId
* @param string $savePath 保存路径 (目录)
* @throws Exception
*/
public function savePdfToFile(int $reimbursementId, string $savePath)
{
$reimbursementData = $this->getMockReimbursementData($reimbursementId);
if (!$reimbursementData) {
throw new Exception("Reimbursement record not found for ID: {$reimbursementId}");
}
$filename = 'reimbursement_' . $reimbursementData['reimbursement_id'] . '_' . date('Ymd_His') . '.pdf';
$fullPath = rtrim($savePath, '/') . '/' . $filename;
$this->pdfGenerator->saveToFile('reimbursement_template', $reimbursementData, $fullPath);
}
}
-172
View File
@@ -1,172 +0,0 @@
<!-- app/view/pdf/delivery_template.php -->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>出货单 - <?php echo htmlspecialchars($data['delivery_id'] ?? 'N/A'); ?></title>
<style>
/* styles.css */
body {
font-family: "DejaVu Sans", sans-serif; /* TCPDF 支持的字体 */
font-size: 10pt;
color: #333;
margin: 20mm 15mm 20mm 15mm; /* 页边距 */
}
.header {
text-align: center;
border-bottom: 2px solid #000;
padding-bottom: 5mm;
margin-bottom: 8mm;
}
.header h1 {
margin: 0;
font-size: 16pt;
color: #000;
}
.header p {
margin: 2mm 0;
}
.company-info {
font-size: 9pt;
text-align: right;
margin-bottom: 10mm;
}
.document-details {
margin-bottom: 10mm;
}
.document-details table {
width: 100%;
border-collapse: collapse;
}
.document-details th, .document-details td {
border: 1px solid #999;
padding: 2mm;
text-align: left;
vertical-align: top;
}
.document-details th {
background-color: #f0f0f0;
font-weight: bold;
text-align: center;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10mm;
}
.items-table th, .items-table td {
border: 1px solid #999;
padding: 2mm;
text-align: left;
}
.items-table th {
background-color: #eaeaea;
font-weight: bold;
}
.items-table tfoot td {
font-weight: bold;
background-color: #f9f9f9;
}
.footer {
border-top: 1px solid #ccc;
padding-top: 5mm;
font-size: 9pt;
text-align: center;
}
/* 特定于出货单的样式 */
.delivery-address {
margin-bottom: 10mm;
}
</style>
</head>
<body>
<div class="header">
<h1>商品出货单</h1>
<p>出货编号: <?php echo htmlspecialchars($data['delivery_id'] ?? 'N/A'); ?></p>
</div>
<div class="company-info">
<p>发货方: Your Company Ltd.</p>
<p>地址: 123 Main Street, City</p>
<p>电话: (123) 456-7890</p>
</div>
<div class="document-details">
<table>
<tr>
<th width="20%">客户名称</th>
<td width="30%"><?php echo htmlspecialchars($data['customer_name'] ?? ''); ?></td>
<th width="20%">出货日期</th>
<td width="30%"><?php echo htmlspecialchars($data['delivery_date'] ?? ''); ?></td>
</tr>
<tr>
<th>订单编号</th>
<td><?php echo htmlspecialchars($data['order_id'] ?? ''); ?></td>
<th>运输方式</th>
<td><?php echo htmlspecialchars($data['shipping_method'] ?? ''); ?></td>
</tr>
</table>
</div>
<div class="delivery-address">
<h3>收货地址</h3>
<p><?php echo nl2br(htmlspecialchars($data['recipient_address'] ?? '')); ?></p>
</div>
<div>
<h3>出货明细</h3>
<table class="items-table">
<thead>
<tr>
<th width="5%">序号</th>
<th width="15%">产品编码</th>
<th width="30%">产品名称</th>
<th width="10%">规格</th>
<th width="10%">单位</th>
<th width="15%">数量</th>
<th width="15%">备注</th>
</tr>
</thead>
<tbody>
<?php if (!empty($data['items'])): ?>
<?php foreach ($data['items'] as $index => $item): ?>
<tr>
<td><?php echo $index + 1; ?></td>
<td><?php echo htmlspecialchars($item['product_code'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($item['product_name'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($item['specification'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($item['unit'] ?? ''); ?></td>
<td style="text-align: right;"><?php echo number_format($item['quantity'] ?? 0, 2); ?></td>
<td><?php echo htmlspecialchars($item['remark'] ?? ''); ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="7" style="text-align: center;">无出货明细</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="footer">
<p>仓库经办人: _________________ &nbsp;&nbsp;&nbsp;&nbsp; 司机签名: _________________ &nbsp;&nbsp;&nbsp;&nbsp; 客户签收: _________________</p>
<p>签收日期: _________</p>
</div>
</body>
</html>
@@ -1,178 +0,0 @@
<!-- app/view/pdf/reimbursement_template.php -->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>报销单 - <?php echo htmlspecialchars($data['reimbursement_id'] ?? 'N/A'); ?></title>
<style>
/* 明确指定支持中文的字体族 */
body {
font-family: "STSongStdLight", "Arial", sans-serif; /* STSongStdLight 对应 TCPDF 的 stsongstdlight */
font-size: 10pt;
color: #333;
margin: 20mm 15mm 20mm 15mm; /* 页边距 */
}
.header {
text-align: center;
border-bottom: 2px solid #000;
padding-bottom: 5mm;
margin-bottom: 8mm;
}
.header h1 {
margin: 0;
font-size: 16pt;
color: #000;
/* 确保标题也使用正确字体 */
font-family: "STSongStdLight", "Arial", sans-serif;
}
.header p {
margin: 2mm 0;
}
.company-info {
font-size: 9pt;
text-align: right;
margin-bottom: 10mm;
}
.document-details {
margin-bottom: 10mm;
}
.document-details table {
width: 100%;
border-collapse: collapse;
}
.document-details th, .document-details td {
border: 1px solid #999;
padding: 2mm;
text-align: left;
vertical-align: top;
/* 确保表格内容使用正确字体 */
font-family: "STSongStdLight", "Arial", sans-serif;
}
.document-details th {
background-color: #f0f0f0;
font-weight: bold;
text-align: center;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10mm;
}
.items-table th, .items-table td {
border: 1px solid #999;
padding: 2mm;
text-align: left;
/* 确保表格内容使用正确字体 */
font-family: "STSongStdLight", "Arial", sans-serif;
}
.items-table th {
background-color: #eaeaea;
font-weight: bold;
}
.items-table tfoot td {
font-weight: bold;
background-color: #f9f9f9;
}
.footer {
border-top: 1px solid #ccc;
padding-top: 5mm;
font-size: 9pt;
text-align: center;
}
</style>
</head>
<body>
<div class="header">
<h1>费用报销申请单</h1>
<p>报销编号: <?php echo htmlspecialchars($data['reimbursement_id'] ?? 'N/A'); ?></p>
</div>
<div class="company-info">
<!-- 公司信息可以从配置中获取 -->
<p>公司名称: Your Company Ltd.</p>
<p>地址: 123 Main Street, City</p>
<p>电话: (123) 456-7890</p>
</div>
<div class="document-details">
<table>
<tr>
<th width="20%">申请人</th>
<td width="30%"><?php echo htmlspecialchars($data['applicant_name'] ?? ''); ?></td>
<th width="20%">部门</th>
<td width="30%"><?php echo htmlspecialchars($data['department'] ?? ''); ?></td>
</tr>
<tr>
<th>申请日期</th>
<td><?php echo htmlspecialchars($data['application_date'] ?? ''); ?></td>
<th>报销总金额</th>
<td><?php echo number_format($data['total_amount'] ?? 0, 2); ?> 元</td>
</tr>
<tr>
<th>事由说明</th>
<td colspan="3"><?php echo nl2br(htmlspecialchars($data['reason'] ?? '')); ?></td>
</tr>
</table>
</div>
<div>
<h3>费用明细</h3>
<table class="items-table">
<thead>
<tr>
<th width="5%">序号</th>
<th width="15%">日期</th>
<th width="20%">费用类别</th>
<th width="30%">说明</th>
<th width="15%">金额 (元)</th>
<th width="15%">备注</th>
</tr>
</thead>
<tbody>
<?php if (!empty($data['items'])): ?>
<?php foreach ($data['items'] as $index => $item): ?>
<tr>
<td><?php echo $index + 1; ?></td>
<td><?php echo htmlspecialchars($item['date'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($item['category'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($item['description'] ?? ''); ?></td>
<td style="text-align: right;"><?php echo number_format($item['amount'] ?? 0, 2); ?></td>
<td><?php echo htmlspecialchars($item['remark'] ?? ''); ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="6" style="text-align: center;">无费用明细</td>
</tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr>
<td colspan="4" style="text-align: right;"><strong>合计:</strong></td>
<td style="text-align: right;"><strong><?php echo number_format($data['total_amount'] ?? 0, 2); ?></strong></td>
<td></td>
</tr>
</tfoot>
</table>
</div>
<div class="footer">
<p>申请人签名: _________________ &nbsp;&nbsp;&nbsp;&nbsp; 审批人签名: _________________ &nbsp;&nbsp;&nbsp;&nbsp; 日期: _________________</p>
<p>本单据由系统生成,仅供参考。</p>
</div>
</body>
</html>
@@ -0,0 +1,237 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>合塑实业-汽车事业部-备货单(客户下定)</title>
<style>
/* 横版:A4 297mm,左右各 12mm => 可用宽度 273mm */
@page { margin: 12mm; }
body {
font-family: 'notosanssc', 'DejaVu Sans', Arial, sans-serif;
font-size: 10pt;
line-height: 1.3;
color: #000;
background: #fff;
}
.container {
width: 273mm; /* 横向版心 */
margin: 0 auto;
background: #fff;
}
.header {
text-align: center;
margin-bottom: 10pt;
padding-bottom: 6pt;
}
.title {
font-size: 14pt;
font-weight: bold;
margin-bottom: 6pt;
}
.sub-title {
font-size: 9pt;
color: #000;
margin: 0 6pt;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10pt;
font-size: 9pt;
table-layout: fixed; /* 横向固定布局,配合 colgroup 更稳 */
}
th, td {
border: 0.5pt solid #000;
padding: 6pt 6pt;
text-align: center;
vertical-align: middle;
word-break: break-all;
}
th {
background-color: #f0f0f0;
font-weight: bold;
color: #000;
}
/* 跨页友好 */
thead { display: table-header-group; }
tfoot { display: table-row-group; }
tr, img { page-break-inside: avoid; }
/* 信息类表格左对齐 */
.info-table td,
.logistics-table td { text-align: left; }
.info-label { font-weight: bold; color: #000; }
.info-value { color: #000; }
.summary-row {
background-color: #f0f0f0;
font-weight: bold;
}
.payment-title {
background-color: #f0f0f0;
font-weight: bold;
text-align: center;
vertical-align: middle;
white-space: nowrap;
}
.nowrap { white-space: nowrap; }
</style>
</head>
<body>
<div class="container">
<!-- 头部 -->
<div class="header">
<h1 class="title">合塑实业-汽车事业部-备货单(客户下定)</h1>
<div>
<span class="sub-title">备货单号: <?php echo htmlspecialchars($data['order_no'] ?? 'N/A'); ?></span>
<span class="sub-title">业务员: <?php echo htmlspecialchars($data['salesperson_snapshot'] ?? ' - '); ?></span>
</div>
</div>
<!-- 基本信息 -->
<table class="info-table">
<colgroup>
<col style="width:18%">
<col style="width:15%">
<col style="width:14%">
<col style="width:14%">
<col style="width:19%">
<col style="width:20%">
</colgroup>
<tbody>
<tr>
<td><span class="info-label">阿里订单号/PI号:</span> <span class="info-value"><?php echo htmlspecialchars($data['pi_no'] ?? ' - '); ?></span></td>
<td><span class="info-label">日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['biz_date'] ?? ' - '); ?></span></td>
<td><span class="info-label">关目的国:</span> <span class="info-value"><?php echo htmlspecialchars($data['clearance_country_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户来源:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_source_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户名称:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户所在国:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_country_snapshot'] ?? ' - '); ?></span></td>
</tr>
</tbody>
</table>
<!-- 产品列表 -->
<table class="product-table">
<colgroup>
<col style="width:6%">
<col style="width:34%">
<col style="width:6%">
<col style="width:6%">
<col style="width:6%">
<col style="width:14%">
<col style="width:8%">
<col style="width:8%">
<col style="width:6%">
<col style="width:6%">
</colgroup>
<thead>
<tr>
<th>编号</th>
<th>车型</th>
<th>数量/台</th>
<th>颜色/外</th>
<th>内饰</th>
<th>改装</th>
<th class="nowrap">单价(美金)</th>
<th class="nowrap">总价(美金)</th>
<th class="nowrap">官方指导价</th>
<th class="nowrap">优惠</th>
</tr>
</thead>
<tbody>
<?php foreach (($data['veh_list'] ?? []) as $vehicle): ?>
<tr>
<td><?php echo htmlspecialchars($vehicle['index'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['model_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['num'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['color_exterior_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['color_interior_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['modify'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['unit_price_text'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['total_price_text'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['official_price_text'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['discount_price_text'] ?? ' - '); ?></td>
</tr>
<?php endforeach; ?>
<tr class="summary-row">
<td colspan="6" style="text-align:left;">
<span class="info-label">合同汇率:</span> <span class="info-value"><?php echo htmlspecialchars($data['contract_exchange_rate'] ?? ' - '); ?></span>
</td>
<td colspan="4" style="text-align:left;">
<span class="info-label">合计到岸价:</span> <span class="info-value"><?php echo htmlspecialchars($data['cif_total_amount'] ?? ' - '); ?></span>
</td>
</tr>
</tbody>
</table>
<!-- 物流信息 -->
<table class="logistics-table">
<colgroup>
<col style="width:16%">
<col style="width:18%">
<col style="width:12%">
<col style="width:18%">
<col style="width:22%">
<col style="width:14%">
</colgroup>
<tbody>
<tr>
<td><span class="info-label">清关人:</span> <span class="info-value"><?php echo htmlspecialchars($data['declarant_name'] ?? ' - '); ?></span></td>
<td><span class="info-label">预计发运日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['eta_ship_date'] ?? ' - '); ?></span></td>
<td><span class="info-label">出运方式:</span> <span class="info-value"><?php echo htmlspecialchars($data['shipping_method_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">国内收货港:</span> <span class="info-value"><?php echo htmlspecialchars($data['domestic_port_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">贸易条款/清关目的港:</span> <span class="info-value"><?php echo htmlspecialchars($data['trade_term_snapshot'] ?? ' - '); ?>/<?php echo htmlspecialchars($data['clearance_port_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">最迟到港时间:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['latest_arrival_date'] ?? ' - '); ?></span></td>
</tr>
</tbody>
</table>
<!-- 计划收款信息 -->
<table class="payment-table">
<colgroup>
<col style="width:12%">
<col style="width:10%">
<col style="width:14%">
<col style="width:12%">
<col style="width:12%">
<col style="width:20%">
<col style="width:10%">
</colgroup>
<thead>
<tr>
<th style="width:24mm" rowspan="<?php echo htmlspecialchars($data['acc_index']); ?>">计划收款信息</th>
<th>序号</th>
<th>汇款国</th>
<th>日期</th>
<th>币种</th>
<th>账户</th>
<th>汇款人</th>
<th>金额</th>
</tr>
</thead>
<tbody>
<?php foreach (($data['account_list'] ?? []) as $account): ?>
<tr>
<td class="nowrap"><?php echo htmlspecialchars($account['index'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($account['remit_country_name_snapshot'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['remit_date'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['currency_name_snapshot'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['account_name_snapshot'] ?? ' - '); ?></td>
<td style="text-align:left;"><?php echo htmlspecialchars($account['remitter_name'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['amount'] ?? ' - '); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</body>
</html>
-81
View File
@@ -1,81 +0,0 @@
/* styles.css */
body {
font-family: "DejaVu Sans", sans-serif; /* TCPDF 支持的字体 */
font-size: 10pt;
color: #333;
margin: 20mm 15mm 20mm 15mm; /* 页边距 */
}
.header {
text-align: center;
border-bottom: 2px solid #000;
padding-bottom: 5mm;
margin-bottom: 8mm;
}
.header h1 {
margin: 0;
font-size: 16pt;
color: #000;
}
.header p {
margin: 2mm 0;
}
.company-info {
font-size: 9pt;
text-align: right;
margin-bottom: 10mm;
}
.document-details {
margin-bottom: 10mm;
}
.document-details table {
width: 100%;
border-collapse: collapse;
}
.document-details th, .document-details td {
border: 1px solid #999;
padding: 2mm;
text-align: left;
vertical-align: top;
}
.document-details th {
background-color: #f0f0f0;
font-weight: bold;
text-align: center;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10mm;
}
.items-table th, .items-table td {
border: 1px solid #999;
padding: 2mm;
text-align: left;
}
.items-table th {
background-color: #eaeaea;
font-weight: bold;
}
.items-table tfoot td {
font-weight: bold;
background-color: #f9f9f9;
}
.footer {
border-top: 1px solid #ccc;
padding-top: 5mm;
font-size: 9pt;
text-align: center;
}
+28 -61
View File
@@ -77,27 +77,27 @@ class IndexController
public function createResource()
{
$options = [
// 'controller' => [
// 'module' => 'common',
// 'name' => 'stock_orders'
// ],
'model' => [
'module' => 'approval',
'name' => 'approval_records'
'controller' => [
'module' => 'admin',
'name' => 'approval_flows'
],
// 'service' => [
// 'module' => 'common',
// 'name' => 'stock_orders'
// ],
// 'logic' => [
// 'module' => 'common',
// 'name' => 'stock_orders'
// ],
// 'validate' => [
// 'module' => 'common',
// 'name' => 'stock_orders'
// ],
'table' => 'approval_records'
'model' => [
'module' => 'admin',
'name' => 'approval_flows'
],
'service' => [
'module' => 'admin',
'name' => 'approval_flows'
],
'logic' => [
'module' => 'admin',
'name' => 'approval_flows'
],
'validate' => [
'module' => 'admin',
'name' => 'approval_flows'
],
'table' => 'approval_flows'
];
$result = (new Generate($options))->doGenerate();
@@ -107,46 +107,13 @@ class IndexController
public function test()
{
$data = [
'id' => 1,
'user_id' => 1,
'leave_type_id' => 1,
'start_date' => '2019-01-01',
'end_date' => '2019-01-02',
'reason' => 'test',
'status' => 1
];
$code = $this->submitLeaveApplication(1, $data);
return $code;
}
// 假设这是你的业务逻辑
public function submitLeaveApplication(int $bizId, $leaveData) {
// 1. 保存业务数据
// $bizId = DB::table('leave_applications')->insertGetId($leaveData);
// 2. 启动审批流程
$service = new \app\common\approval\service\ApprovalFlowService();
try {
$result = $service->start(
'leave', // 流程类型,对应 approval_flows.type
'leave_application', // 业务类型标识
$bizId, // 业务ID
$leaveData, // 表单数据快照
$_SESSION['user_id'] ?? 1 // 发起人ID
);
// 3. 将审批实例ID关联回业务表
DB::table('leave_applications')
->where('id', $bizId)
->update(['approval_instance_id' => $result['instance_id']]);
return ['status' => 'success', 'message' => '申请已提交,等待审批'];
} catch (\Exception $e) {
// 处理错误,可能需要回滚业务数据插入
return ['status' => 'error', 'message' => $e->getMessage()];
}
return json([
'code' => 0,
'msg' => 'success',
'data' => [
'time' => time(),
'date' => date('Y-m-d H:i:s')
]
]);
}
}
@@ -8,6 +8,10 @@ use support\Request;
class StockOrdersController extends BaseController
{
protected $noNeedLogin = ['print'];
protected $noNeedHeaderData = ['print'];
use CrudControllerTrait;
protected $service;
@@ -41,6 +45,11 @@ class StockOrdersController extends BaseController
if (isset($data['eta_ship_date'])) $data['eta_ship_date'] = date('Y-m-d', $data['eta_ship_date']);
if (isset($data['biz_date'])) $data['biz_date'] = date('Y-m-d', $data['biz_date']);
if (isset($data['latest_arrival_date'])) $data['latest_arrival_date'] = date('Y-m-d', $data['latest_arrival_date']);
if (!empty($data['contract'])) {
$data['contract_url'] = (new \app\common\service\OssService)->generatePrivateUrl($data['contract']);
}
if (count($data['moneyValues']) > 0) {
foreach ($data['moneyValues'] as $mvs) {
$data['cif_total_amount'] = [
@@ -102,6 +111,16 @@ class StockOrdersController extends BaseController
}
public function print(Request $request)
{
try {
$raw = $this->service->servicePrint($request);
return $this->success($raw);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
+1 -1
View File
@@ -47,7 +47,7 @@
"phpoffice/phpspreadsheet": "^4.5",
"phpmailer/phpmailer": "^6.10",
"webman/gateway-worker": "^1.0",
"tecnickcom/tcpdf": "^6.10"
"mpdf/mpdf": "^8.2"
},
"suggest": {
"ext-event": "For better performance. "
Generated
+306 -72
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "475042c507f4dff165d21e0c64715771",
"content-hash": "68e74d2aef3a2f7f8163bfaf65431cb2",
"packages": [
{
"name": "aliyuncs/oss-sdk-php",
@@ -2890,6 +2890,239 @@
],
"time": "2024-11-12T12:43:37+00:00"
},
{
"name": "mpdf/mpdf",
"version": "v8.2.6",
"source": {
"type": "git",
"url": "https://github.com/mpdf/mpdf.git",
"reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mpdf/mpdf/zipball/dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44",
"reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44",
"shasum": ""
},
"require": {
"ext-gd": "*",
"ext-mbstring": "*",
"mpdf/psr-http-message-shim": "^1.0 || ^2.0",
"mpdf/psr-log-aware-trait": "^2.0 || ^3.0",
"myclabs/deep-copy": "^1.7",
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
"php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"psr/http-message": "^1.0 || ^2.0",
"psr/log": "^1.0 || ^2.0 || ^3.0",
"setasign/fpdi": "^2.1"
},
"require-dev": {
"mockery/mockery": "^1.3.0",
"mpdf/qrcode": "^1.1.0",
"squizlabs/php_codesniffer": "^3.5.0",
"tracy/tracy": "~2.5",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
"ext-bcmath": "Needed for generation of some types of barcodes",
"ext-xml": "Needed mainly for SVG manipulation",
"ext-zlib": "Needed for compression of embedded resources, such as fonts"
},
"type": "library",
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"Mpdf\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-only"
],
"authors": [
{
"name": "Matěj Humpál",
"role": "Developer, maintainer"
},
{
"name": "Ian Back",
"role": "Developer (retired)"
}
],
"description": "PHP library generating PDF files from UTF-8 encoded HTML",
"homepage": "https://mpdf.github.io",
"keywords": [
"pdf",
"php",
"utf-8"
],
"support": {
"docs": "https://mpdf.github.io",
"issues": "https://github.com/mpdf/mpdf/issues",
"source": "https://github.com/mpdf/mpdf"
},
"funding": [
{
"url": "https://www.paypal.me/mpdf",
"type": "custom"
}
],
"time": "2025-08-18T08:51:51+00:00"
},
{
"name": "mpdf/psr-http-message-shim",
"version": "v2.0.1",
"source": {
"type": "git",
"url": "https://github.com/mpdf/psr-http-message-shim.git",
"reference": "f25a0153d645e234f9db42e5433b16d9b113920f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/f25a0153d645e234f9db42e5433b16d9b113920f",
"reference": "f25a0153d645e234f9db42e5433b16d9b113920f",
"shasum": ""
},
"require": {
"psr/http-message": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Mpdf\\PsrHttpMessageShim\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Dorison",
"email": "mark@chromatichq.com"
},
{
"name": "Kristofer Widholm",
"email": "kristofer@chromatichq.com"
},
{
"name": "Nigel Cunningham",
"email": "nigel.cunningham@technocrat.com.au"
}
],
"description": "Shim to allow support of different psr/message versions.",
"support": {
"issues": "https://github.com/mpdf/psr-http-message-shim/issues",
"source": "https://github.com/mpdf/psr-http-message-shim/tree/v2.0.1"
},
"time": "2023-10-02T14:34:03+00:00"
},
{
"name": "mpdf/psr-log-aware-trait",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/mpdf/psr-log-aware-trait.git",
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/a633da6065e946cc491e1c962850344bb0bf3e78",
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78",
"shasum": ""
},
"require": {
"psr/log": "^3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Mpdf\\PsrLogAwareTrait\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Dorison",
"email": "mark@chromatichq.com"
},
{
"name": "Kristofer Widholm",
"email": "kristofer@chromatichq.com"
}
],
"description": "Trait to allow support of different psr/log versions.",
"support": {
"issues": "https://github.com/mpdf/psr-log-aware-trait/issues",
"source": "https://github.com/mpdf/psr-log-aware-trait/tree/v3.0.0"
},
"time": "2023-05-03T06:19:36+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpspec/prophecy": "^1.10",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
"files": [
"src/DeepCopy/deep_copy.php"
],
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Create deep copies (clones) of your objects",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2025-08-01T08:46:24+00:00"
},
{
"name": "nesbot/carbon",
"version": "3.10.0",
@@ -3975,6 +4208,78 @@
},
"time": "2019-03-08T08:55:37+00:00"
},
{
"name": "setasign/fpdi",
"version": "v2.6.4",
"source": {
"type": "git",
"url": "https://github.com/Setasign/FPDI.git",
"reference": "4b53852fde2734ec6a07e458a085db627c60eada"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada",
"reference": "4b53852fde2734ec6a07e458a085db627c60eada",
"shasum": ""
},
"require": {
"ext-zlib": "*",
"php": "^7.1 || ^8.0"
},
"conflict": {
"setasign/tfpdf": "<1.31"
},
"require-dev": {
"phpunit/phpunit": "^7",
"setasign/fpdf": "~1.8.6",
"setasign/tfpdf": "~1.33",
"squizlabs/php_codesniffer": "^3.5",
"tecnickcom/tcpdf": "^6.8"
},
"suggest": {
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
},
"type": "library",
"autoload": {
"psr-4": {
"setasign\\Fpdi\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jan Slabon",
"email": "jan.slabon@setasign.com",
"homepage": "https://www.setasign.com"
},
{
"name": "Maximilian Kresse",
"email": "maximilian.kresse@setasign.com",
"homepage": "https://www.setasign.com"
}
],
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
"homepage": "https://www.setasign.com/fpdi",
"keywords": [
"fpdf",
"fpdi",
"pdf"
],
"support": {
"issues": "https://github.com/Setasign/FPDI/issues",
"source": "https://github.com/Setasign/FPDI/tree/v2.6.4"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
"type": "tidelift"
}
],
"time": "2025-08-05T09:57:14+00:00"
},
{
"name": "shopwwi/filesystem-oss",
"version": "v1.0.3",
@@ -6211,77 +6516,6 @@
],
"time": "2025-05-15T09:04:05+00:00"
},
{
"name": "tecnickcom/tcpdf",
"version": "6.10.0",
"source": {
"type": "git",
"url": "https://github.com/tecnickcom/TCPDF.git",
"reference": "ca5b6de294512145db96bcbc94e61696599c391d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/ca5b6de294512145db96bcbc94e61696599c391d",
"reference": "ca5b6de294512145db96bcbc94e61696599c391d",
"shasum": ""
},
"require": {
"ext-curl": "*",
"php": ">=7.1.0"
},
"type": "library",
"autoload": {
"classmap": [
"config",
"include",
"tcpdf.php",
"tcpdf_barcodes_1d.php",
"tcpdf_barcodes_2d.php",
"include/tcpdf_colors.php",
"include/tcpdf_filters.php",
"include/tcpdf_font_data.php",
"include/tcpdf_fonts.php",
"include/tcpdf_images.php",
"include/tcpdf_static.php",
"include/barcodes/datamatrix.php",
"include/barcodes/pdf417.php",
"include/barcodes/qrcode.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-3.0-or-later"
],
"authors": [
{
"name": "Nicola Asuni",
"email": "info@tecnick.com",
"role": "lead"
}
],
"description": "TCPDF is a PHP class for generating PDF documents and barcodes.",
"homepage": "http://www.tcpdf.org/",
"keywords": [
"PDFD32000-2008",
"TCPDF",
"barcodes",
"datamatrix",
"pdf",
"pdf417",
"qrcode"
],
"support": {
"issues": "https://github.com/tecnickcom/TCPDF/issues",
"source": "https://github.com/tecnickcom/TCPDF/tree/6.10.0"
},
"funding": [
{
"url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ",
"type": "custom"
}
],
"time": "2025-05-27T18:02:28+00:00"
},
{
"name": "topthink/think-container",
"version": "v3.0.2",
+13 -4
View File
@@ -287,6 +287,14 @@ Route::group('/admin', function () {
Route::any('/destroy', [app\admin\controller\SysPortController::class, 'destroy']);
});
// 审批
Route::group('/approval', function () {
Route::any('/list', [app\admin\controller\ApprovalFlowsController::class, 'index']);
Route::any('/show', [app\admin\controller\ApprovalFlowsController::class, 'show']);
Route::any('/store', [app\admin\controller\ApprovalFlowsController::class, 'store']);
Route::any('/update', [app\admin\controller\ApprovalFlowsController::class, 'update']);
Route::any('/destroy', [app\admin\controller\ApprovalFlowsController::class, 'destroy']);
});
});
@@ -356,12 +364,13 @@ Route::group('/crm', function () {
Route::any('/list', [app\crm\controller\StockOrdersController::class, 'index']);
Route::any('/show', [app\crm\controller\StockOrdersController::class, 'show']);
Route::any('/store', [app\crm\controller\StockOrdersController::class, 'store']);
Route::any('/print', [app\crm\controller\StockOrdersController::class, 'print']);
// Route::any('/update', [app\crm\controller\StockOrdersController::class, 'update']);
// Route::any('/destroy', [app\crm\controller\StockOrdersController::class, 'destroy']);
});
Route::group('/pdf', function () {
Route::any('/bx/{id}', [app\common\utils\pdf\controller\DocumentController::class, 'downloadReimbursementPdf']);
Route::any('/ckbx/{id}', [app\common\utils\pdf\controller\DocumentController::class, 'viewReimbursementPdf']);
});
// Route::group('/pdf', function () {
// Route::any('/bx/{id}', [app\common\utils\pdf\controller\DocumentController::class, 'downloadReimbursementPdf']);
// Route::any('/ckbx/{id}', [app\common\utils\pdf\controller\DocumentController::class, 'viewReimbursementPdf']);
// });
});
Binary file not shown.
Binary file not shown.