Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68e0ca30ad | |||
| a8c684ad0c | |||
| 0779bf57e3 |
@@ -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
|
||||
|
||||
### 新功能
|
||||
|
||||
@@ -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 49152–65535.
|
||||
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;
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
|
||||
@@ -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 张'),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user