From 50b139c97a4e822c13617c6f407141fd40561876 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Thu, 30 Apr 2026 22:07:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=93=E5=8D=B0?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E4=B9=B1=E7=A0=81=E3=80=81XLS=E5=AF=BC?= =?UTF-8?q?=E5=85=A5100=E6=9D=A1=E6=88=AA=E6=96=AD=E3=80=81Nginx=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 打印改为 Web 平台用 HTML+CSS 方案,避免 pdf 包中文字体 bug - 新增 print_util.dart 条件导入层(web/stub),彻底隔离 platform channel - XLS 导入不再依赖 DIMENSIONS 记录的 MaxRow,改为读到连续5空行停止 - Nginx /images/ 加 ^~ 前缀,防止 .jpg 正则 location 拦截 Co-Authored-By: Claude Sonnet 4.6 --- backend/internal/handler/import.go | 21 ++++++++- client/lib/core/utils/print_util.dart | 13 ++++++ client/lib/core/utils/print_util_stub.dart | 38 +++++++++++++++ client/lib/core/utils/print_util_web.dart | 46 +++++++++++++++++++ .../products/product_detail_screen.dart | 39 ++++------------ .../Flutter/GeneratedPluginRegistrant.swift | 2 + client/pubspec.lock | 2 +- client/pubspec.yaml | 1 + deploy/nginx-jiu.conf | 4 +- 9 files changed, 131 insertions(+), 35 deletions(-) create mode 100644 client/lib/core/utils/print_util.dart create mode 100644 client/lib/core/utils/print_util_stub.dart create mode 100644 client/lib/core/utils/print_util_web.dart diff --git a/backend/internal/handler/import.go b/backend/internal/handler/import.go index 2453c83..07b30d2 100644 --- a/backend/internal/handler/import.go +++ b/backend/internal/handler/import.go @@ -522,12 +522,31 @@ func parseUploadedExcel(c *gin.Context) ([][]string, error) { if numCols == 0 { numCols = 20 } - for r := 0; r <= int(sheet.MaxRow); r++ { + // sheet.MaxRow 依赖 DIMENSIONS 记录,旧软件导出的 XLS 该值可能偏小。 + // 改为读到连续 5 行全空为止,最多 100000 行防止死循环。 + const maxRows = 100000 + const maxEmpty = 5 + emptyStreak := 0 + for r := 0; r < maxRows; r++ { row := sheet.Row(r) cells := make([]string, numCols) + isEmpty := true for c := 0; c < numCols; c++ { cells[c] = strings.TrimSpace(row.Col(c)) + if cells[c] != "" { + isEmpty = false + } } + if isEmpty { + emptyStreak++ + if emptyStreak >= maxEmpty { + break + } + // 保留空行,让调用方自行跳过 + rows = append(rows, cells) + continue + } + emptyStreak = 0 rows = append(rows, cells) } } else { diff --git a/client/lib/core/utils/print_util.dart b/client/lib/core/utils/print_util.dart new file mode 100644 index 0000000..8a02863 --- /dev/null +++ b/client/lib/core/utils/print_util.dart @@ -0,0 +1,13 @@ +import 'dart:typed_data'; +import 'print_util_stub.dart' + if (dart.library.js_interop) 'print_util_web.dart'; + +/// 打印商品标签(二维码 + 名称 + 编号)。 +/// Web:生成 HTML 弹出新窗口调用浏览器打印。 +/// 非 Web:生成 PDF 用 printing 包调起原生打印。 +Future printProductLabel({ + required Uint8List qrBytes, + required String name, + required String code, + String? spec, +}) => printProductLabelImpl(qrBytes: qrBytes, name: name, code: code, spec: spec); diff --git a/client/lib/core/utils/print_util_stub.dart b/client/lib/core/utils/print_util_stub.dart new file mode 100644 index 0000000..ab96bfd --- /dev/null +++ b/client/lib/core/utils/print_util_stub.dart @@ -0,0 +1,38 @@ +import 'package:flutter/services.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart' as pw; +import 'package:printing/printing.dart'; + +Future printProductLabelImpl({ + required Uint8List qrBytes, + required String name, + required String code, + String? spec, +}) async { + final fontData = await rootBundle.load('assets/fonts/NotoSansSC-Regular.ttf'); + final font = pw.Font.ttf(fontData); + final doc = pw.Document(); + final qrImage = pw.MemoryImage(qrBytes); + doc.addPage(pw.Page( + pageFormat: PdfPageFormat.a4, + build: (_) => pw.Center( + child: pw.Column( + mainAxisSize: pw.MainAxisSize.min, + children: [ + pw.Image(qrImage, width: 200, height: 200), + pw.SizedBox(height: 12), + pw.Text(name, + style: pw.TextStyle( + font: font, fontSize: 18, fontWeight: pw.FontWeight.bold)), + pw.SizedBox(height: 4), + pw.Text('编号:$code', + style: pw.TextStyle(font: font, fontSize: 13)), + if ((spec ?? '').isNotEmpty) + pw.Text('规格:$spec', + style: pw.TextStyle(font: font, fontSize: 13)), + ], + ), + ), + )); + await Printing.layoutPdf(onLayout: (_) async => doc.save()); +} diff --git a/client/lib/core/utils/print_util_web.dart b/client/lib/core/utils/print_util_web.dart new file mode 100644 index 0000000..4ce79de --- /dev/null +++ b/client/lib/core/utils/print_util_web.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; +import 'dart:js_interop'; +import 'dart:typed_data'; +import 'package:web/web.dart' as web; + +Future printProductLabelImpl({ + required Uint8List qrBytes, + required String name, + required String code, + String? spec, +}) async { + final base64Img = base64Encode(qrBytes); + final specHtml = (spec != null && spec.isNotEmpty) + ? '

规格:$spec

' + : ''; + final html = ''' + + + + + + +
+ +

$name

+

编号:$code

+ $specHtml +
+ + +'''; + + final win = web.window.open('', '_blank'); + if (win != null) { + win.document.write(html.toJS); + win.document.close(); + } +} diff --git a/client/lib/screens/products/product_detail_screen.dart b/client/lib/screens/products/product_detail_screen.dart index 4a6d61f..7e360a7 100644 --- a/client/lib/screens/products/product_detail_screen.dart +++ b/client/lib/screens/products/product_detail_screen.dart @@ -4,10 +4,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdf/pdf.dart'; -import 'package:pdf/widgets.dart' as pw; -import 'package:printing/printing.dart'; import '../../core/config/app_config.dart'; +import '../../core/utils/print_util.dart'; import '../../core/theme/app_theme.dart'; import '../../models/product.dart'; import '../../models/product_image.dart'; @@ -527,36 +525,15 @@ class _QRCodeDialogState extends ConsumerState<_QRCodeDialog> { Future _print() async { if (_bytes == null || _printing) return; - setState(() { _printing = true; _printStatus = '正在准备字体...'; }); + setState(() { _printing = true; _printStatus = '正在打印...'; }); try { - final fontData = await rootBundle.load('assets/fonts/NotoSansSC-Regular.ttf'); - if (mounted) setState(() => _printStatus = '正在生成 PDF...'); - final font = pw.Font.ttf(fontData); final p = widget.product; - final doc = pw.Document(); - final qrImage = pw.MemoryImage(_bytes!); - doc.addPage(pw.Page( - pageFormat: PdfPageFormat.a4, - build: (_) => pw.Center( - child: pw.Column( - mainAxisSize: pw.MainAxisSize.min, - children: [ - pw.Image(qrImage, width: 200, height: 200), - pw.SizedBox(height: 12), - pw.Text(p.name, - style: pw.TextStyle(font: font, fontSize: 18, fontWeight: pw.FontWeight.bold)), - pw.SizedBox(height: 4), - pw.Text('编号:${p.code}', - style: pw.TextStyle(font: font, fontSize: 13)), - if ((p.spec ?? '').isNotEmpty) - pw.Text('规格:${p.spec}', - style: pw.TextStyle(font: font, fontSize: 13)), - ], - ), - ), - )); - if (mounted) setState(() => _printStatus = '正在打开打印...'); - await Printing.layoutPdf(onLayout: (_) async => doc.save()); + await printProductLabel( + qrBytes: _bytes!, + name: p.name, + code: p.code, + spec: p.spec, + ); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/client/macos/Flutter/GeneratedPluginRegistrant.swift b/client/macos/Flutter/GeneratedPluginRegistrant.swift index c2314b3..5713c70 100644 --- a/client/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/client/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,12 +7,14 @@ import Foundation import file_picker import package_info_plus +import printing import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/client/pubspec.lock b/client/pubspec.lock index 386114f..59497d5 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -758,7 +758,7 @@ packages: source: hosted version: "15.0.2" web: - dependency: transitive + dependency: "direct main" description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 01e5fa8..ebda75a 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: file_picker: ^8.1.6 printing: ^5.13.0 pdf: ^3.10.8 + web: ^1.1.0 dev_dependencies: flutter_test: diff --git a/deploy/nginx-jiu.conf b/deploy/nginx-jiu.conf index 88c0325..e63429e 100644 --- a/deploy/nginx-jiu.conf +++ b/deploy/nginx-jiu.conf @@ -10,8 +10,8 @@ server { root /opt/jiu/web; index index.html; - # 商品图片静态文件 - location /images/ { + # 商品图片静态文件(^~ 阻止正则 location 拦截 .jpg 等后缀请求) + location ^~ /images/ { alias /opt/jiu/images/; expires 30d; add_header Cache-Control "public, immutable";