fix: 修复打印中文乱码、XLS导入100条截断、Nginx图片路由
- 打印改为 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 <noreply@anthropic.com>
This commit is contained in:
@@ -522,12 +522,31 @@ func parseUploadedExcel(c *gin.Context) ([][]string, error) {
|
|||||||
if numCols == 0 {
|
if numCols == 0 {
|
||||||
numCols = 20
|
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)
|
row := sheet.Row(r)
|
||||||
cells := make([]string, numCols)
|
cells := make([]string, numCols)
|
||||||
|
isEmpty := true
|
||||||
for c := 0; c < numCols; c++ {
|
for c := 0; c < numCols; c++ {
|
||||||
cells[c] = strings.TrimSpace(row.Col(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)
|
rows = append(rows, cells)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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<void> printProductLabel({
|
||||||
|
required Uint8List qrBytes,
|
||||||
|
required String name,
|
||||||
|
required String code,
|
||||||
|
String? spec,
|
||||||
|
}) => printProductLabelImpl(qrBytes: qrBytes, name: name, code: code, spec: spec);
|
||||||
@@ -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<void> 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());
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:js_interop';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:web/web.dart' as web;
|
||||||
|
|
||||||
|
Future<void> printProductLabelImpl({
|
||||||
|
required Uint8List qrBytes,
|
||||||
|
required String name,
|
||||||
|
required String code,
|
||||||
|
String? spec,
|
||||||
|
}) async {
|
||||||
|
final base64Img = base64Encode(qrBytes);
|
||||||
|
final specHtml = (spec != null && spec.isNotEmpty)
|
||||||
|
? '<p style="margin:4px 0;font-size:13px;color:#555;">规格:$spec</p>'
|
||||||
|
: '';
|
||||||
|
final html = '''<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0;}
|
||||||
|
body{display:flex;justify-content:center;align-items:center;min-height:100vh;
|
||||||
|
font-family:"PingFang SC","Microsoft YaHei",sans-serif;}
|
||||||
|
.label{text-align:center;padding:24px;}
|
||||||
|
img{width:200px;height:200px;display:block;margin:0 auto;}
|
||||||
|
h2{margin:12px 0 4px;font-size:18px;font-weight:bold;}
|
||||||
|
p{margin:4px 0;font-size:13px;color:#444;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="label">
|
||||||
|
<img src="data:image/png;base64,$base64Img"/>
|
||||||
|
<h2>$name</h2>
|
||||||
|
<p>编号:$code</p>
|
||||||
|
$specHtml
|
||||||
|
</div>
|
||||||
|
<script>window.onload=function(){window.print();};</script>
|
||||||
|
</body>
|
||||||
|
</html>''';
|
||||||
|
|
||||||
|
final win = web.window.open('', '_blank');
|
||||||
|
if (win != null) {
|
||||||
|
win.document.write(html.toJS);
|
||||||
|
win.document.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/config/app_config.dart';
|
||||||
|
import '../../core/utils/print_util.dart';
|
||||||
import '../../core/theme/app_theme.dart';
|
import '../../core/theme/app_theme.dart';
|
||||||
import '../../models/product.dart';
|
import '../../models/product.dart';
|
||||||
import '../../models/product_image.dart';
|
import '../../models/product_image.dart';
|
||||||
@@ -527,36 +525,15 @@ class _QRCodeDialogState extends ConsumerState<_QRCodeDialog> {
|
|||||||
|
|
||||||
Future<void> _print() async {
|
Future<void> _print() async {
|
||||||
if (_bytes == null || _printing) return;
|
if (_bytes == null || _printing) return;
|
||||||
setState(() { _printing = true; _printStatus = '正在准备字体...'; });
|
setState(() { _printing = true; _printStatus = '正在打印...'; });
|
||||||
try {
|
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 p = widget.product;
|
||||||
final doc = pw.Document();
|
await printProductLabel(
|
||||||
final qrImage = pw.MemoryImage(_bytes!);
|
qrBytes: _bytes!,
|
||||||
doc.addPage(pw.Page(
|
name: p.name,
|
||||||
pageFormat: PdfPageFormat.a4,
|
code: p.code,
|
||||||
build: (_) => pw.Center(
|
spec: p.spec,
|
||||||
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());
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import Foundation
|
|||||||
|
|
||||||
import file_picker
|
import file_picker
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
|
import printing
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
|
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -758,7 +758,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.2"
|
version: "15.0.2"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: web
|
name: web
|
||||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ dependencies:
|
|||||||
file_picker: ^8.1.6
|
file_picker: ^8.1.6
|
||||||
printing: ^5.13.0
|
printing: ^5.13.0
|
||||||
pdf: ^3.10.8
|
pdf: ^3.10.8
|
||||||
|
web: ^1.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ server {
|
|||||||
root /opt/jiu/web;
|
root /opt/jiu/web;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
# 商品图片静态文件
|
# 商品图片静态文件(^~ 阻止正则 location 拦截 .jpg 等后缀请求)
|
||||||
location /images/ {
|
location ^~ /images/ {
|
||||||
alias /opt/jiu/images/;
|
alias /opt/jiu/images/;
|
||||||
expires 30d;
|
expires 30d;
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
|
|||||||
Reference in New Issue
Block a user