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/),
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
### 新功能
+11 -9
View File
@@ -1,11 +1,13 @@
import 'dart:io';
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.
// ignore: unused_element
RandomAccessFile? _raf;
// Keep the server socket alive for the lifetime of the process.
// The OS releases the port automatically when the process exits.
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 false if another instance is already running.
@@ -15,11 +17,11 @@ Future<bool> acquire() async {
if (!Platform.isWindows && !Platform.isMacOS) return true;
try {
final dir = await getApplicationSupportDirectory();
final lockFile = File('${dir.path}/jiu.lock');
final raf = await lockFile.open(mode: FileMode.write);
await raf.lock(FileLock.exclusive);
_raf = raf;
_server = await ServerSocket.bind(
InternetAddress.loopbackIPv4,
_kLockPort,
shared: false,
);
return true;
} catch (_) {
return false;
+5 -2
View File
@@ -501,7 +501,7 @@ Future<List<String>> listLabelPrintersImpl() async {
return (r.stdout as String)
.split('\n')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.where((s) => s.isNotEmpty && _isThermalName(s))
.toList();
}
} catch (e) {
@@ -511,7 +511,10 @@ Future<List<String>> listLabelPrintersImpl() async {
}
if (Platform.isWindows) {
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) {
debugPrint('[label] listPrinters 失败: $e');
}
@@ -668,27 +668,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
DataCell(
item.productId != null
? TextButton(
onPressed: () async {
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 ?? '',
));
}
},
onPressed: () => _printLabel(context, item),
child: const Text('打标签',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
)
+9 -3
View File
@@ -36,6 +36,7 @@ class _LabelPreviewDialogState extends State<LabelPreviewDialog> {
List<String> _printers = [];
String? _printer;
bool _printing = false;
bool _printersChecked = false;
String _status = '';
@override
@@ -63,6 +64,7 @@ class _LabelPreviewDialogState extends State<LabelPreviewDialog> {
setState(() {
_printers = printers;
_printer = selected;
_printersChecked = true;
});
}
@@ -147,6 +149,7 @@ class _LabelPreviewDialogState extends State<LabelPreviewDialog> {
_printing = false;
_status = '完成,共打印 $done';
});
Navigator.of(context).pop();
}
}
@@ -329,8 +332,9 @@ class _LabelPreviewDialogState extends State<LabelPreviewDialog> {
const SizedBox(width: 8),
Expanded(
child: _printers.isEmpty
? const Text('检测中或未检测到打印机...',
style: TextStyle(
? Text(
_printersChecked ? '无打印机可用' : '正在检测打印机...',
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary))
: DropdownButtonHideUnderline(
@@ -419,7 +423,9 @@ class _LabelPreviewDialogState extends State<LabelPreviewDialog> {
),
const SizedBox(width: 8),
FilledButton(
onPressed: (_printing || totalCount == 0) ? null : _doPrint,
onPressed: (_printing || totalCount == 0 || _printer == null)
? null
: _doPrint,
child: Text(
_printing ? '打印中...' : '打印 $totalCount'),
),