diff --git a/CHANGELOG.md b/CHANGELOG.md index 055c132..ebe20d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ 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.29] - 2026-06-09 + +### 新功能 +- Windows / macOS 桌面客户端现在限制只能运行一个实例;重复打开时弹窗提示「程序已在运行中」并自动退出 + ## [1.0.28] - 2026-06-09 ### 修复 diff --git a/client/lib/core/platform/single_instance.dart b/client/lib/core/platform/single_instance.dart new file mode 100644 index 0000000..0e20c7c --- /dev/null +++ b/client/lib/core/platform/single_instance.dart @@ -0,0 +1,27 @@ +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; + +/// Returns true if this process successfully acquired the single-instance lock. +/// Returns false if another instance is already running. +/// On Web / mobile / Linux the function always returns true (no restriction). +Future acquire() async { + if (kIsWeb) return true; + 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; + return true; + } catch (_) { + return false; + } +} diff --git a/client/lib/main.dart b/client/lib/main.dart index bb1690d..077ac52 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -6,6 +7,7 @@ import 'package:flutter_web_plugins/url_strategy.dart'; import 'core/auth/auth_state.dart'; import 'core/config/app_info.dart'; import 'core/errors/error_reporter.dart'; +import 'core/platform/single_instance.dart' as single_instance; import 'core/router/app_router.dart'; import 'core/theme/app_theme.dart'; import 'providers/connectivity_provider.dart'; @@ -27,6 +29,12 @@ void main() { runZonedGuarded( () async { WidgetsFlutterBinding.ensureInitialized(); + + if (!await single_instance.acquire()) { + runApp(const _AlreadyRunningApp()); + return; + } + await AppInfo.load(); // 从配置文件加载「关于我们」品牌信息 runApp(const ProviderScope(child: JiuApp())); }, @@ -43,6 +51,38 @@ void main() { ); } +class _AlreadyRunningApp extends StatelessWidget { + const _AlreadyRunningApp(); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Builder( + builder: (context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => AlertDialog( + title: const Text('岩美酒库已在运行'), + content: const Text('请勿重复打开,程序已在运行中。\n请在任务栏或 Dock 中找到已打开的窗口。'), + actions: [ + TextButton( + onPressed: () => exit(0), + child: const Text('确定'), + ), + ], + ), + ); + }); + return const Scaffold(); + }, + ), + ); + } +} + class JiuApp extends ConsumerStatefulWidget { const JiuApp({super.key}); diff --git a/todo/todo.html b/todo/todo.html index dbf59a7..1b44bcb 100644 --- a/todo/todo.html +++ b/todo/todo.html @@ -169,9 +169,9 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
生成于 2026-06-09 · 真相源 todo/todo.json
24全部
-
7待开始
+
6待开始
0开发中
-
3待验收
+
4待验收
14已验收
@@ -206,7 +206,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
- 📋 待开始 7 + 📋 待开始 6 ▴ 收起
@@ -284,30 +284,6 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
-
  • -
    - 桌面客户端单实例限制,禁止同时运行多个进程 -
    - 待开始 - 重要 - -
    -
    - -
    Windows 和 macOS 客户端启动时检测是否已有实例运行;若有,则激活已有窗口并退出新进程。Flutter desktop 可通过 window_single_instance 或 dart:io 锁文件实现。
    - -
  • -
  • - 🔍 待验收 3 + 🔍 待验收 4 ▴ 收起
    @@ -425,6 +401,30 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
  • +
  • +
    + 桌面客户端单实例限制,禁止同时运行多个进程 +
    + 待验收 + 重要 + +
    +
    + +
    Windows 和 macOS 客户端启动时检测是否已有实例运行;若有,则激活已有窗口并退出新进程。Flutter desktop 可通过 window_single_instance 或 dart:io 锁文件实现。
    + +
  • +