Просмотр исходного кода

统一套打字体与二维码居中,修复工单号字号偏小。

套打文字改用微软雅黑并保持整张单同一字号;二维码按模板格子居中打印,并改进流程卡二维码区域识别。

Co-authored-by: Cursor <cursoragent@cursor.com>
unknown 1 месяц назад
Родитель
Сommit
7b5dadc68b

+ 20 - 13
fastadmin/application/common/library/ps/LodopPrinter.php

@@ -85,6 +85,10 @@ class LodopPrinter
             ['SET_PRINT_PAGESIZE', 1, (int)round($w * 10), (int)round($h * 10), ''],
             ['SET_PRINT_MODE', 'POS_BASEON_PAPER', 1],
             ['SET_PRINT_COPIES', $copies],
+            ['SET_PRINT_STYLE', 'FontName', '微软雅黑'],
+            ['SET_PRINT_STYLE', 'FontSize', 10],
+            ['SET_PRINT_STYLE', 'Bold', 0],
+            ['SET_PRINT_STYLE', 'Italic', 0],
         ];
         $last = count($pages) - 1;
         foreach ($pages as $index => $item) {
@@ -98,23 +102,27 @@ class LodopPrinter
                 foreach ($texts as $row) {
                     if (($row['type'] ?? '') === 'qrcode') {
                         $img = trim((string)($row['image'] ?? ''));
+                        $qrTop = (string)($row['top'] ?? '0mm');
+                        $qrLeft = (string)($row['left'] ?? '0mm');
+                        $qrWidth = (string)($row['width'] ?? '24mm');
+                        $qrHeight = (string)($row['height'] ?? '24mm');
                         if ($img !== '') {
                             $commands[] = [
                                 'ADD_PRINT_IMAGE',
-                                $row['top'],
-                                $row['left'],
-                                $row['width'],
-                                $row['height'],
+                                $qrTop,
+                                $qrLeft,
+                                $qrWidth,
+                                $qrHeight,
                                 $img,
                             ];
                             $commands[] = ['SET_PRINT_STYLEA', 0, 'Stretch', 1];
                         } else {
                             $commands[] = [
                                 'ADD_PRINT_BARCODE',
-                                $row['top'],
-                                $row['left'],
-                                $row['width'],
-                                $row['height'],
+                                $qrTop,
+                                $qrLeft,
+                                $qrWidth,
+                                $qrHeight,
                                 'QRCode',
                                 (string)($row['text'] ?? ''),
                             ];
@@ -131,12 +139,11 @@ class LodopPrinter
                         $row['height'],
                         (string)($row['text'] ?? ''),
                     ];
-                    $commands[] = ['SET_PRINT_STYLEA', 0, 'FontName', $row['font'] ?? 'SimSun'];
+                    $commands[] = ['SET_PRINT_STYLEA', 0, 'FontName', $row['font'] ?? '微软雅黑'];
                     $commands[] = ['SET_PRINT_STYLEA', 0, 'FontSize', (int)($row['size'] ?? 10)];
+                    $commands[] = ['SET_PRINT_STYLEA', 0, 'Bold', !empty($row['bold']) ? 1 : 0];
+                    $commands[] = ['SET_PRINT_STYLEA', 0, 'Italic', 0];
                     $commands[] = ['SET_PRINT_STYLEA', 0, 'Alignment', (int)($row['align'] ?? 1)];
-                    if (!empty($row['bold'])) {
-                        $commands[] = ['SET_PRINT_STYLEA', 0, 'Bold', 1];
-                    }
                 }
             } else {
                 $html = self::stripCoverImage((string)($item['html'] ?? ''));
@@ -188,7 +195,7 @@ class LodopPrinter
     protected static function wrapHtml($html, $w, $h)
     {
         return '<!DOCTYPE html><html><head><meta charset="utf-8">'
-            . '<style>html,body{margin:0;padding:0;width:' . $w . 'mm;height:' . $h . 'mm;overflow:hidden;background:transparent;}</style>'
+            . '<style>html,body{margin:0;padding:0;width:' . $w . 'mm;height:' . $h . 'mm;overflow:hidden;background:transparent;font-family:\'Microsoft YaHei\',\'微软雅黑\',sans-serif;font-size:13px;font-weight:normal;}</style>'
             . '</head><body>' . $html . '</body></html>';
     }
 }

+ 166 - 40
fastadmin/application/common/library/ps/PrintPlanner.php

@@ -407,7 +407,7 @@ class PrintPlanner
                 self::makeElement('title', 'text', '生产流程单', '', '', 15, 4, 70, 6, 20, 'center', true, false),
             ];
         }
-        $elements = self::alignFlowCardFields(self::alignHeaderFields(self::bindPrintKeys($elements)));
+        $elements = self::resolveQrField(self::alignFlowCardFields(self::alignHeaderFields(self::bindPrintKeys($elements))));
         $fields = [];
         foreach ($elements as $el) {
             if (($el['type'] ?? '') === 'field' || ($el['key'] ?? '') !== '') {
@@ -488,8 +488,10 @@ class PrintPlanner
         if ($pageH <= 0) {
             $pageH = 297;
         }
+        $typeface = self::overlayTypeface($schema);
         $html = '<div style="position:relative;width:' . $pageW . 'mm;height:' . $pageH
-            . 'mm;margin:0;overflow:hidden;background:transparent;font-family:SimSun,serif;line-height:1.2;">';
+            . 'mm;margin:0;overflow:hidden;background:transparent;font-family:' . $typeface['css']
+            . ';font-size:' . $typeface['px'] . 'px;font-weight:normal;line-height:1.2;">';
         if ($includeImage && $src !== '') {
             $html .= '<img src="' . $src . '" style="position:absolute;left:0;top:0;width:100%;height:100%;border:0;display:block;" alt="流程单底图" />';
         }
@@ -503,17 +505,14 @@ class PrintPlanner
             $top = self::pct($field['y'] ?? 0);
             $width = self::pct($field['w'] ?? 10);
             $height = self::pct($field['h'] ?? 4);
-            $size = max(8, (int)($field['fontSize'] ?? 13));
             $align = in_array($field['align'] ?? 'left', ['left', 'center', 'right'], true) ? $field['align'] : 'left';
             $color = self::color($field['color'] ?? '#111111', false, '#111111');
-            $weight = !empty($field['bold']) ? 'bold' : 'normal';
-            $font = htmlspecialchars(self::fontFamily($field['fontFamily'] ?? 'SimSun'), ENT_QUOTES, 'UTF-8');
             $content = $key !== ''
                 ? '{' . $key . '}'
                 : htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
             $html .= '<div style="position:absolute;left:' . $left . '%;top:' . $top
-                . '%;width:' . $width . '%;height:' . $height . '%;font-size:' . $size
-                . 'px;font-weight:' . $weight . ';font-family:' . $font
+                . '%;width:' . $width . '%;height:' . $height . '%;font-size:' . $typeface['px']
+                . 'px;font-weight:normal;font-style:normal;font-family:' . $typeface['css']
                 . ';overflow:hidden;white-space:nowrap;text-align:' . $align
                 . ';color:' . $color . ';background:transparent;">' . $content . '</div>';
         }
@@ -539,8 +538,10 @@ class PrintPlanner
         if ($pageH <= 0) {
             $pageH = 297;
         }
+        $fields = self::fillElements($schema);
+        $typeface = self::overlayTypeface($schema, $fields);
         $items = [];
-        foreach (self::fillElements($schema) as $field) {
+        foreach ($fields as $field) {
             $key = trim((string)($field['key'] ?? ''));
             $text = $key !== ''
                 ? self::fillText($key, $fill, $processes)
@@ -555,18 +556,12 @@ class PrintPlanner
                 if ($payload === '') {
                     continue;
                 }
-                $size = round(max(18, min($wMm, $hMm) - 0.8), 2);
-                if ($size > min($wMm, $hMm)) {
-                    $size = round(min($wMm, $hMm), 2);
-                }
-                $items[] = [
-                    'type'   => 'qrcode',
-                    'top'    => round($yMm + max(0, ($hMm - $size) / 2), 2) . 'mm',
-                    'left'   => round($xMm + max(0, ($wMm - $size) / 2), 2) . 'mm',
-                    'width'  => $size . 'mm',
-                    'height' => $size . 'mm',
-                    'text'   => $payload,
-                ];
+                $qrW = max(8, self::pct($field['w'] ?? 10) / 100 * $pageW);
+                $qrH = max(8, self::pct($field['h'] ?? 4) / 100 * $pageH);
+                $items[] = array_merge(
+                    ['type' => 'qrcode', 'text' => $payload],
+                    self::centerQrInCell($xMm, $yMm, $qrW, $qrH)
+                );
                 continue;
             }
             if ($text === '') {
@@ -579,14 +574,6 @@ class PrintPlanner
             $xMm += $padX;
             $hMm = max(3.2, $hMm - $padY * 1.4);
             $wMm = max(6, $wMm - $padX);
-            $fontSize = (int)max(7, min(11, round($hMm * 2.1)));
-            $charCount = max(1, mb_strlen($text, 'UTF-8'));
-            $fitWidth = (int)floor($wMm / $charCount / 0.36);
-            if ($fitWidth >= 7) {
-                $fontSize = min($fontSize, $fitWidth);
-            } else {
-                $fontSize = min($fontSize, 8);
-            }
             $align = $field['align'] ?? 'left';
             $items[] = [
                 'top'    => round($yMm, 2) . 'mm',
@@ -594,15 +581,93 @@ class PrintPlanner
                 'width'  => round($wMm, 2) . 'mm',
                 'height' => round($hMm, 2) . 'mm',
                 'text'   => $text,
-                'font'   => self::fontFamily($field['fontFamily'] ?? 'SimSun'),
-                'size'   => $fontSize,
+                'font'   => $typeface['family'],
+                'size'   => $typeface['pt'],
                 'align'  => $align === 'center' ? 2 : ($align === 'right' ? 3 : 1),
-                'bold'   => !empty($field['bold']) ? 1 : 0,
+                'bold'   => $typeface['bold'],
             ];
         }
         return $items;
     }
 
+    /**
+     * 二维码居中于识版格子内(不做文字字段的内边距)。
+     * @param float $xMm
+     * @param float $yMm
+     * @param float $wMm
+     * @param float $hMm
+     * @return array{top:string,left:string,width:string,height:string}
+     */
+    protected static function centerQrInCell($xMm, $yMm, $wMm, $hMm)
+    {
+        $wMm = max(8, (float)$wMm);
+        $hMm = max(8, (float)$hMm);
+        $inset = min(1.0, $wMm * 0.04, $hMm * 0.04);
+        $innerW = max(8, $wMm - $inset * 2);
+        $innerH = max(8, $hMm - $inset * 2);
+        $size = min($innerW, $innerH);
+        $left = $xMm + ($wMm - $size) / 2;
+        $top = $yMm + ($hMm - $size) / 2;
+        return [
+            'top'    => round($top, 2) . 'mm',
+            'left'   => round($left, 2) . 'mm',
+            'width'  => round($size, 2) . 'mm',
+            'height' => round($size, 2) . 'mm',
+        ];
+    }
+
+    /**
+     * 同一张套打单上的填写文字用同一字体、同一字号、同一字重。
+     * @param array      $schema
+     * @param array|null $fields
+     * @return array{family:string,css:string,px:int,pt:int,bold:int}
+     */
+    protected static function overlayTypeface(array $schema, array $fields = null)
+    {
+        if ($fields === null) {
+            $fields = self::fillElements($schema);
+        }
+        $sizes = [];
+        foreach ($fields as $field) {
+            if (trim((string)($field['key'] ?? '')) === 'qr') {
+                continue;
+            }
+            $px = max(8, (int)($field['fontSize'] ?? 13));
+            $sizes[] = $px;
+        }
+        $px = 13;
+        if ($sizes) {
+            $freq = array_count_values($sizes);
+            arsort($freq);
+            $px = (int)key($freq);
+        }
+        $pt = (int)max(8, min(12, round($px * 72 / 96)));
+        return [
+            'family' => self::overlayFontFamily(),
+            'css'    => self::overlayFontCss(),
+            'px'     => $px,
+            'pt'     => $pt,
+            'bold'   => 0,
+        ];
+    }
+
+    /**
+     * 套打 overlay 专用字体(不影响 compose 排版)。
+     * @return string
+     */
+    protected static function overlayFontFamily()
+    {
+        return '微软雅黑';
+    }
+
+    /**
+     * @return string
+     */
+    protected static function overlayFontCss()
+    {
+        return "'Microsoft YaHei','微软雅黑',sans-serif";
+    }
+
     /**
      * 流程单二维码:写入可被微信打开的信息页地址(纯文本微信会提示无法识别)。
      * @param array $fill
@@ -1084,12 +1149,6 @@ class PrintPlanner
                 $wideW = $w;
                 $wideIdx = $idx;
             }
-            if ($y < 22 && $h > 8 && $w > 8 && $x > 50) {
-                $elements[$idx]['key'] = 'qr';
-                $elements[$idx]['label'] = '二维码区域';
-                $elements[$idx]['type'] = 'field';
-                $elements[$idx]['role'] = 'fill';
-            }
             if ($y < 22 && $w >= 10 && $w <= 28 && $h >= 2.5 && $h <= 8) {
                 $topSmall[] = $idx;
             }
@@ -1116,6 +1175,70 @@ class PrintPlanner
         return $elements;
     }
 
+    /**
+     * 流程卡右侧大块空白通常是二维码区;优先绑定面积最大的右侧格子。
+     * @param array $elements
+     * @return array
+     */
+    protected static function resolveQrField(array $elements)
+    {
+        $reserved = [
+            'product.name', 'product.code', 'product.spec', 'qty_plan',
+            'workorder.batch_no', 'workorder.no', 'workorder.pack_no', 'workorder.remark',
+            'sheet.qty', 'sheet.no_text', 'operator', 'print_time', 'process_names',
+        ];
+        $qrIndices = [];
+        $bestIdx = null;
+        $bestArea = 0;
+        foreach ($elements as $idx => $el) {
+            if (!is_array($el)) {
+                continue;
+            }
+            $key = trim((string)($el['key'] ?? ''));
+            $label = trim((string)($el['label'] ?? ''));
+            if ($key === 'qr' || preg_match('/二维码/u', $label)) {
+                $qrIndices[] = $idx;
+            }
+            $x = (float)($el['x'] ?? 0);
+            $y = (float)($el['y'] ?? 0);
+            $w = (float)($el['w'] ?? 0);
+            $h = (float)($el['h'] ?? 0);
+            if ($x < 30 || $w < 12 || $h < 12) {
+                continue;
+            }
+            if ($y < 6 && $h < 16) {
+                continue;
+            }
+            if ($key !== '' && ($key === 'qr' || preg_match('/^process\./', $key) || in_array($key, $reserved, true))) {
+                if ($key !== 'qr') {
+                    continue;
+                }
+            }
+            $area = $w * $h;
+            if ($area > $bestArea) {
+                $bestArea = $area;
+                $bestIdx = $idx;
+            }
+        }
+        if ($bestIdx === null) {
+            return $elements;
+        }
+        foreach ($qrIndices as $idx) {
+            if ($idx === $bestIdx) {
+                continue;
+            }
+            $area = (float)($elements[$idx]['w'] ?? 0) * (float)($elements[$idx]['h'] ?? 0);
+            if ($area < $bestArea * 0.75) {
+                $elements[$idx]['key'] = '';
+            }
+        }
+        $elements[$bestIdx]['key'] = 'qr';
+        $elements[$bestIdx]['label'] = '二维码区域';
+        $elements[$bestIdx]['type'] = 'field';
+        $elements[$bestIdx]['role'] = 'fill';
+        return $elements;
+    }
+
     /**
      * 产品工序流程卡:色块标签常把「产品名称 / 总投料数」和工序列认反。
      * 按位置纠正——工序名称列是左侧一叠格子,其上一行从左到右是名称、总投料数、规格。
@@ -1471,16 +1594,19 @@ class PrintPlanner
     protected static function fontFamily($name)
     {
         $name = trim((string)$name);
-        $allow = ['SimSun', 'SimHei', 'KaiTi', 'Microsoft YaHei', 'FangSong', 'Arial', 'serif', 'sans-serif'];
+        $allow = ['SimSun', 'SimHei', 'KaiTi', 'Microsoft YaHei', '微软雅黑', 'FangSong', 'Arial', 'serif', 'sans-serif'];
         foreach ($allow as $item) {
             if (strcasecmp($item, $name) === 0) {
-                return $item;
+                return $item === 'Microsoft YaHei' ? '微软雅黑' : $item;
             }
         }
+        if (preg_match('/微软雅黑|微软黑|yahei/i', $name)) {
+            return '微软雅黑';
+        }
         if (preg_match('/宋体|song/i', $name)) {
             return 'SimSun';
         }
-        if (preg_match('/黑|hei/i', $name)) {
+        if (preg_match('/黑体|^hei$/i', $name)) {
             return 'SimHei';
         }
         if (preg_match('/楷/i', $name)) {

+ 28 - 7
vue/src/utils/lodop.ts

@@ -126,6 +126,7 @@ export async function runLodop(lodopPack: any) {
   }
   LODOP.SET_PRINT_MODE("POS_BASEON_PAPER", 1);
   LODOP.SET_PRINT_COPIES(copies);
+  applyLodopBaseTypeface(LODOP);
 
   if (!pages.length) {
     throw new Error("没有可套打的页面数据");
@@ -156,12 +157,7 @@ export async function runLodop(lodopPack: any) {
         row.height || "4%",
         text
       );
-      LODOP.SET_PRINT_STYLEA(0, "FontName", (row && row.font) || "SimSun");
-      LODOP.SET_PRINT_STYLEA(0, "FontSize", Number(row && row.size) || 10);
-      LODOP.SET_PRINT_STYLEA(0, "Alignment", Number(row && row.align) || 1);
-      if (row && row.bold) {
-        LODOP.SET_PRINT_STYLEA(0, "Bold", 1);
-      }
+      applyLodopTextStyle(LODOP, row);
       added += 1;
     }
     if (!added && sheet && sheet.html) {
@@ -170,6 +166,7 @@ export async function runLodop(lodopPack: any) {
     }
     if (!added) {
       LODOP.ADD_PRINT_TEXT("8%", "8%", "80%", "10%", "套打数据为空");
+      applyLodopTextStyle(LODOP, { font: "微软雅黑", size: 10, bold: 0, align: 1 });
     }
     if (index < pages.length - 1) {
       LODOP.NEWPAGE();
@@ -195,7 +192,7 @@ async function addLodopQrImage(lodop: any, row: any, text: string) {
       left,
       width,
       height,
-      `<img src="${png}" />`
+      `<img border="0" src="${png}" />`
     );
     lodop.SET_PRINT_STYLEA(0, "Stretch", 1);
   } catch {
@@ -210,6 +207,30 @@ async function addLodopQrImage(lodop: any, row: any, text: string) {
   }
 }
 
+function applyLodopBaseTypeface(lodop: any) {
+  try {
+    lodop.SET_PRINT_STYLE("FontName", "微软雅黑");
+    lodop.SET_PRINT_STYLE("FontSize", 10);
+    lodop.SET_PRINT_STYLE("Bold", 0);
+    lodop.SET_PRINT_STYLE("Italic", 0);
+  } catch {
+    /* 旧版 CLodop 无全局样式 */
+  }
+}
+
+function applyLodopTextStyle(lodop: any, row: any) {
+  lodop.SET_PRINT_STYLEA(0, "FontName", (row && row.font) || "微软雅黑");
+  lodop.SET_PRINT_STYLEA(0, "FontSize", Number(row && row.size) || 10);
+  lodop.SET_PRINT_STYLEA(0, "Bold", row && row.bold ? 1 : 0);
+  lodop.SET_PRINT_STYLEA(0, "Alignment", Number(row && row.align) || 1);
+  try {
+    lodop.SET_PRINT_STYLEA(0, "Italic", 0);
+    lodop.SET_PRINT_STYLEA(0, "Underline", 0);
+  } catch {
+    /* 旧版 CLodop 无斜体/下划线样式 */
+  }
+}
+
 function paperNameOf(w: number, h: number, name?: string) {
   const n = String(name || "").toUpperCase();
   if (n === "A5" || (Math.abs(w - 148) < 1 && Math.abs(h - 210) < 1)) {