Compare commits

...

3 Commits

Author SHA1 Message Date
wangjia 68e0ca30ad chore: release v1.0.39
Deploy / build-linux-web (push) Successful in 47s
Deploy / build-windows (push) Successful in 1m42s
Deploy / build-macos (push) Successful in 1m23s
Deploy / build-android (push) Successful in 1m0s
Deploy / build-ios (push) Successful in 7s
Deploy / release-deploy (push) Successful in 1m50s
2026-06-15 12:16:52 +08:00
wangjia a8c684ad0c fix(client): 单实例锁改用 TCP loopback socket
文件锁在 Windows 上行为不一致,改用绑定本地端口(54317)作为互斥锁,
进程退出时 OS 自动释放,Windows/macOS 均可靠生效。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 12:09:34 +08:00
wangjia 0779bf57e3 fix(client): 修复标签打印预览三个问题
- 打印完成后自动关闭预览弹窗
- 打印机列表改为仅显示热敏打印机,无热敏打印机时显示"无打印机可用"并禁用打印按钮
- 库存列表表格行"打标签"按钮改为调用预览弹窗(之前仍走旧直接打印逻辑)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 12:04:03 +08:00
5 changed files with 34 additions and 35 deletions
+8
View File
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.39] - 2026-06-15
### 修复
- 打印预览弹窗:打印完成后自动关闭
- 打印预览弹窗:打印机列表改为仅显示热敏打印机,无热敏打印机时显示"无打印机可用"并禁用打印按钮
- 库存列表表格行"打标签"按钮点击无反应
- Windows/macOS 单实例限制:同一台电脑不允许重复打开多个客户端窗口
## [1.0.38] - 2026-06-15 ## [1.0.38] - 2026-06-15
### 新功能 ### 新功能
+11 -9
View File
@@ -1,11 +1,13 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
// Keep the RAF open so the OS lock stays alive for the lifetime of the process. // Keep the server socket alive for the lifetime of the process.
// ignore: unused_element // The OS releases the port automatically when the process exits.
RandomAccessFile? _raf; ServerSocket? _server;
// Port unique to this app; chosen from the private range 4915265535.
const _kLockPort = 54317;
/// Returns true if this process successfully acquired the single-instance lock. /// Returns true if this process successfully acquired the single-instance lock.
/// Returns false if another instance is already running. /// Returns false if another instance is already running.
@@ -15,11 +17,11 @@ Future<bool> acquire() async {
if (!Platform.isWindows && !Platform.isMacOS) return true; if (!Platform.isWindows && !Platform.isMacOS) return true;
try { try {
final dir = await getApplicationSupportDirectory(); _server = await ServerSocket.bind(
final lockFile = File('${dir.path}/jiu.lock'); InternetAddress.loopbackIPv4,
final raf = await lockFile.open(mode: FileMode.write); _kLockPort,
await raf.lock(FileLock.exclusive); shared: false,
_raf = raf; );
return true; return true;
} catch (_) { } catch (_) {
return false; return false;
+5 -2
View File
@@ -501,7 +501,7 @@ Future<List<String>> listLabelPrintersImpl() async {
return (r.stdout as String) return (r.stdout as String)
.split('\n') .split('\n')
.map((s) => s.trim()) .map((s) => s.trim())
.where((s) => s.isNotEmpty) .where((s) => s.isNotEmpty && _isThermalName(s))
.toList(); .toList();
} }
} catch (e) { } catch (e) {
@@ -511,7 +511,10 @@ Future<List<String>> listLabelPrintersImpl() async {
} }
if (Platform.isWindows) { if (Platform.isWindows) {
try { try {
return (await Printing.listPrinters()).map((p) => p.name).toList(); return (await Printing.listPrinters())
.map((p) => p.name)
.where((n) => _isThermalName(n))
.toList();
} catch (e) { } catch (e) {
debugPrint('[label] listPrinters 失败: $e'); debugPrint('[label] listPrinters 失败: $e');
} }
@@ -668,27 +668,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
DataCell( DataCell(
item.productId != null item.productId != null
? TextButton( ? TextButton(
onPressed: () async { onPressed: () => _printLabel(context, item),
final qrBytes = await ref
.read(productRepositoryProvider)
.getQRCodeBytes(item.productId!);
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
if (context.mounted) {
await safePrint(context, () => printProductLabel(
qrBytes: qrBytes,
name: item.productName,
code: item.productCode,
series: item.series.isEmpty ? null : item.series,
spec: item.spec.isEmpty ? null : item.spec,
batchNo: item.batchNo.isEmpty ? null : item.batchNo,
productionDate: item.productionDate,
remark: item.remark.isEmpty ? null : item.remark,
shopName: shopInfo?.name ?? '',
shopAddress: shopInfo?.address ?? '',
shopPhone: shopInfo?.phone ?? '',
));
}
},
child: const Text('打标签', child: const Text('打标签',
style: TextStyle(fontSize: 12, color: AppTheme.primary)), style: TextStyle(fontSize: 12, color: AppTheme.primary)),
) )
+9 -3
View File
@@ -36,6 +36,7 @@ class _LabelPreviewDialogState extends State<LabelPreviewDialog> {
List<String> _printers = []; List<String> _printers = [];
String? _printer; String? _printer;
bool _printing = false; bool _printing = false;
bool _printersChecked = false;
String _status = ''; String _status = '';
@override @override
@@ -63,6 +64,7 @@ class _LabelPreviewDialogState extends State<LabelPreviewDialog> {
setState(() { setState(() {
_printers = printers; _printers = printers;
_printer = selected; _printer = selected;
_printersChecked = true;
}); });
} }
@@ -147,6 +149,7 @@ class _LabelPreviewDialogState extends State<LabelPreviewDialog> {
_printing = false; _printing = false;
_status = '完成,共打印 $done'; _status = '完成,共打印 $done';
}); });
Navigator.of(context).pop();
} }
} }
@@ -329,8 +332,9 @@ class _LabelPreviewDialogState extends State<LabelPreviewDialog> {
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: _printers.isEmpty child: _printers.isEmpty
? const Text('检测中或未检测到打印机...', ? Text(
style: TextStyle( _printersChecked ? '无打印机可用' : '正在检测打印机...',
style: const TextStyle(
fontSize: 13, fontSize: 13,
color: AppTheme.textSecondary)) color: AppTheme.textSecondary))
: DropdownButtonHideUnderline( : DropdownButtonHideUnderline(
@@ -419,7 +423,9 @@ class _LabelPreviewDialogState extends State<LabelPreviewDialog> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
FilledButton( FilledButton(
onPressed: (_printing || totalCount == 0) ? null : _doPrint, onPressed: (_printing || totalCount == 0 || _printer == null)
? null
: _doPrint,
child: Text( child: Text(
_printing ? '打印中...' : '打印 $totalCount'), _printing ? '打印中...' : '打印 $totalCount'),
), ),