Explorar el Código

feat: 添加开机自启动功能(跨平台支持)

DESKTOP-74CLTRG\Leol hace 1 mes
padre
commit
e31c07cda4

+ 20 - 4
src/YZWater.Avalonia/Views/ViewBView.axaml

@@ -21,7 +21,7 @@
 
             <!-- 内容 -->
             <ScrollViewer Grid.Row="1" Margin="8,4">
-                <Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto">
+                <Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,Auto">
 
                     <!-- PLC 连接设置 -->
                     <Border Grid.Row="0" Grid.Column="0" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2"
@@ -79,7 +79,7 @@
 
                     <!-- 泵频率设置 -->
                     <Border Grid.Row="1" Grid.Column="0" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2"
-                            BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" Padding="12" Margin="0,4,4,0">
+                            BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" Padding="12" Margin="0,4,4,4">
                         <StackPanel>
                             <StackPanel Orientation="Horizontal" Spacing="6" Margin="0,0,0,12">
                                 <Border Background="{DynamicResource SuccessBrush}" Width="3" Height="14" CornerRadius="1"/>
@@ -94,9 +94,25 @@
                         </StackPanel>
                     </Border>
 
-                    <!-- 操作按钮 -->
+                    <!-- 系统设置 -->
                     <Border Grid.Row="1" Grid.Column="1" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2"
-                            BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" Padding="12" Margin="4,4,0,0">
+                            BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" Padding="12" Margin="4,4,0,4">
+                        <StackPanel>
+                            <StackPanel Orientation="Horizontal" Spacing="6" Margin="0,0,0,12">
+                                <Border Background="{DynamicResource PrimaryBrush}" Width="3" Height="14" CornerRadius="1"/>
+                                <TextBlock Text="{Binding SystemSettingsText}" FontFamily="{DynamicResource MonoFont}" FontSize="12" FontWeight="Bold"
+                                           Foreground="{DynamicResource TextSecondaryBrush}"/>
+                            </StackPanel>
+                            <Grid ColumnDefinitions="120,*" Margin="4,0">
+                                <TextBlock Grid.Column="0" Text="{Binding RunAtStartupText}" FontFamily="{DynamicResource MonoFont}" FontSize="11" Foreground="{DynamicResource HeaderSubtextBrush}" VerticalAlignment="Center"/>
+                                <ToggleSwitch Grid.Column="1" IsChecked="{Binding RunAtStartup}" Margin="0,2"/>
+                            </Grid>
+                        </StackPanel>
+                    </Border>
+
+                    <!-- 操作按钮 -->
+                    <Border Grid.Row="2" Grid.Column="1" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2"
+                            BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" Padding="12" Margin="4,4,0,4">
                         <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="12">
                             <TextBlock Text="ACTIONS" FontFamily="{DynamicResource MonoFont}" FontSize="12" FontWeight="Bold"
                                        Foreground="{DynamicResource TextSecondaryBrush}" HorizontalAlignment="Center"/>

+ 6 - 0
src/YZWater.Core/Models/SystemConfig.cs

@@ -140,6 +140,12 @@ public partial class SystemConfig : ObservableObject
     [ObservableProperty]
     private int _plcPollingIntervalMs = 1000;
 
+    /// <summary>
+    /// 是否开机自启动
+    /// </summary>
+    [ObservableProperty]
+    private bool _runAtStartup;
+
     /// <summary>
     /// PLC 地址覆盖(key=标签名如 Tank1Level, value=地址如 VD100)
     /// 空则使用默认地址

+ 4 - 0
src/YZWater.Core/Services/LanguageService.cs

@@ -143,6 +143,8 @@ public partial class LanguageService : ObservableObject
         ["Connected"] = "已连接",
         ["Disconnected"] = "未连接",
         ["Connecting"] = "连接中...",
+        ["RunAtStartup"] = "开机自启动",
+        ["SystemSettings"] = "系统设置",
         // ViewC
         ["FlowRecords"] = "流量记录",
         ["FlowHistory"] = "流量历史数据",
@@ -391,6 +393,8 @@ public partial class LanguageService : ObservableObject
         ["Connected"] = "Connected",
         ["Disconnected"] = "Disconnected",
         ["Connecting"] = "Connecting...",
+        ["RunAtStartup"] = "START WITH WINDOWS",
+        ["SystemSettings"] = "SYSTEM SETTINGS",
         ["FlowRecords"] = "FLOW RECORDS",
         ["FlowHistory"] = "Flow History Data",
         ["From"] = "FROM",

+ 202 - 0
src/YZWater.Core/Services/StartupService.cs

@@ -0,0 +1,202 @@
+using System.Runtime.InteropServices;
+using Microsoft.Win32;
+using Serilog;
+
+namespace YZWater.Core.Services;
+
+/// <summary>
+/// 跨平台开机自启动管理服务
+/// </summary>
+public static class StartupService
+{
+    private const string AppName = "YZWater";
+    private const string WindowsRegistryPath = @"Software\Microsoft\Windows\CurrentVersion\Run";
+    private const string LinuxDesktopFileName = "yzwater.desktop";
+    private const string MacPlistFileName = "com.yxwater.yzwater.plist";
+
+    /// <summary>
+    /// 设置开机自启动
+    /// </summary>
+    public static bool SetAutoStart(bool enabled)
+    {
+        try
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                return SetWindowsAutoStart(enabled);
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+                return SetLinuxAutoStart(enabled);
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+                return SetMacAutoStart(enabled);
+
+            Log.Warning("StartupService: 不支持的操作系统");
+            return false;
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "StartupService: 设置开机自启动失败 (enabled={Enabled})", enabled);
+            return false;
+        }
+    }
+
+    /// <summary>
+    /// 检查是否已启用开机自启动
+    /// </summary>
+    public static bool IsAutoStartEnabled()
+    {
+        try
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                return IsWindowsAutoStartEnabled();
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+                return IsLinuxAutoStartEnabled();
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+                return IsMacAutoStartEnabled();
+
+            return false;
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "StartupService: 检查开机自启动状态失败");
+            return false;
+        }
+    }
+
+    /// <summary>
+    /// 获取当前可执行文件路径
+    /// </summary>
+    private static string GetExecutablePath()
+    {
+        return Environment.ProcessPath ?? Environment.GetCommandLineArgs()[0];
+    }
+
+    #region Windows
+
+    private static bool SetWindowsAutoStart(bool enabled)
+    {
+        using var key = Registry.CurrentUser.OpenSubKey(WindowsRegistryPath, true);
+        if (key == null)
+        {
+            Log.Error("StartupService: 无法打开注册表路径 {Path}", WindowsRegistryPath);
+            return false;
+        }
+
+        if (enabled)
+        {
+            key.SetValue(AppName, GetExecutablePath());
+            Log.Information("StartupService: Windows 开机自启动已启用");
+        }
+        else
+        {
+            key.DeleteValue(AppName, false);
+            Log.Information("StartupService: Windows 开机自启动已禁用");
+        }
+        return true;
+    }
+
+    private static bool IsWindowsAutoStartEnabled()
+    {
+        using var key = Registry.CurrentUser.OpenSubKey(WindowsRegistryPath);
+        return key?.GetValue(AppName) != null;
+    }
+
+    #endregion
+
+    #region Linux
+
+    private static string GetLinuxDesktopFilePath()
+    {
+        var configDir = Path.Combine(
+            Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+            ".config", "autostart");
+        return Path.Combine(configDir, LinuxDesktopFileName);
+    }
+
+    private static bool SetLinuxAutoStart(bool enabled)
+    {
+        var filePath = GetLinuxDesktopFilePath();
+
+        if (enabled)
+        {
+            var dir = Path.GetDirectoryName(filePath)!;
+            Directory.CreateDirectory(dir);
+
+            var content = $"""
+                [Desktop Entry]
+                Type=Application
+                Name={AppName}
+                Exec={GetExecutablePath()}
+                X-GNOME-Autostart-enabled=true
+                """;
+            File.WriteAllText(filePath, content);
+            Log.Information("StartupService: Linux 开机自启动已启用 ({Path})", filePath);
+        }
+        else
+        {
+            if (File.Exists(filePath))
+                File.Delete(filePath);
+            Log.Information("StartupService: Linux 开机自启动已禁用");
+        }
+        return true;
+    }
+
+    private static bool IsLinuxAutoStartEnabled()
+    {
+        return File.Exists(GetLinuxDesktopFilePath());
+    }
+
+    #endregion
+
+    #region macOS
+
+    private static string GetMacPlistPath()
+    {
+        var libraryDir = Path.Combine(
+            Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+            "Library", "LaunchAgents");
+        return Path.Combine(libraryDir, MacPlistFileName);
+    }
+
+    private static bool SetMacAutoStart(bool enabled)
+    {
+        var filePath = GetMacPlistPath();
+
+        if (enabled)
+        {
+            var dir = Path.GetDirectoryName(filePath)!;
+            Directory.CreateDirectory(dir);
+
+            var content = $"""
+                <?xml version="1.0" encoding="UTF-8"?>
+                <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+                <plist version="1.0">
+                <dict>
+                    <key>Label</key>
+                    <string>com.yxwater.yzwater</string>
+                    <key>ProgramArguments</key>
+                    <array>
+                        <string>{GetExecutablePath()}</string>
+                    </array>
+                    <key>RunAtLoad</key>
+                    <true/>
+                </dict>
+                </plist>
+                """;
+            File.WriteAllText(filePath, content);
+            Log.Information("StartupService: macOS 开机自启动已启用 ({Path})", filePath);
+        }
+        else
+        {
+            if (File.Exists(filePath))
+                File.Delete(filePath);
+            Log.Information("StartupService: macOS 开机自启动已禁用");
+        }
+        return true;
+    }
+
+    private static bool IsMacAutoStartEnabled()
+    {
+        return File.Exists(GetMacPlistPath());
+    }
+
+    #endregion
+}

+ 44 - 11
src/YZWater.Core/ViewModels/ViewBViewModel.cs

@@ -39,6 +39,9 @@ public partial class ViewBViewModel : ObservableObject, IDisposable
     [ObservableProperty]
     private bool _isConnecting;
 
+    [ObservableProperty]
+    private bool _runAtStartup;
+
     // ─── 翻译文字 ───
     private readonly LanguageService _lang = LanguageService.Instance;
     [ObservableProperty] private string _titleText;
@@ -60,6 +63,8 @@ public partial class ViewBViewModel : ObservableObject, IDisposable
     [ObservableProperty] private string _connectedText;
     [ObservableProperty] private string _disconnectedText;
     [ObservableProperty] private string _connectingText;
+    [ObservableProperty] private string _runAtStartupText;
+    [ObservableProperty] private string _systemSettingsText;
 
     public ViewBViewModel()
     {
@@ -90,6 +95,29 @@ public partial class ViewBViewModel : ObservableObject, IDisposable
         ConnectedText = _lang.Get("Connected");
         DisconnectedText = _lang.Get("Disconnected");
         ConnectingText = _lang.Get("Connecting");
+        RunAtStartupText = _lang.Get("RunAtStartup");
+        SystemSettingsText = _lang.Get("SystemSettings");
+    }
+
+    partial void OnRunAtStartupChanged(bool value)
+    {
+        var success = StartupService.SetAutoStart(value);
+        if (!success)
+        {
+            _runAtStartup = !value;
+            OnPropertyChanged(nameof(RunAtStartup));
+            Log.Warning("开机自启动设置失败,已回滚 UI 状态");
+        }
+        else
+        {
+            var config = ConfigService.GetConfig();
+            if (config != null)
+            {
+                config.RunAtStartup = value;
+                ConfigService.SaveConfig();
+                Log.Information("开机自启动已{Status}", value ? "启用" : "禁用");
+            }
+        }
     }
 
     /// <summary>
@@ -108,6 +136,7 @@ public partial class ViewBViewModel : ObservableObject, IDisposable
             LevelLowAlarm = config.LevelLowAlarm;
             FlowHighAlarm = config.FlowHighAlarm;
             PumpFrequency = config.PumpFrequency;
+            RunAtStartup = config.RunAtStartup;
         }
         Log.Debug("配置已加载");
     }
@@ -118,18 +147,22 @@ public partial class ViewBViewModel : ObservableObject, IDisposable
     [RelayCommand]
     private void SaveConfig()
     {
-        var config = new SystemConfig
+        var config = ConfigService.GetConfig();
+        if (config == null)
         {
-            PlcIp = PlcIp,
-            PlcPort = PlcPort,
-            AutoConnect = AutoConnect,
-            LevelHighAlarm = LevelHighAlarm,
-            LevelLowAlarm = LevelLowAlarm,
-            FlowHighAlarm = FlowHighAlarm,
-            PumpFrequency = PumpFrequency
-        };
-
-        ConfigService.UpdateConfig(config);
+            Log.Warning("配置为空,无法保存");
+            return;
+        }
+
+        config.PlcIp = PlcIp;
+        config.PlcPort = PlcPort;
+        config.AutoConnect = AutoConnect;
+        config.LevelHighAlarm = LevelHighAlarm;
+        config.LevelLowAlarm = LevelLowAlarm;
+        config.FlowHighAlarm = FlowHighAlarm;
+        config.PumpFrequency = PumpFrequency;
+
+        ConfigService.SaveConfig();
         Log.Information("配置已保存");
     }
 

+ 1 - 0
src/YZWater.Core/YZWater.Core.csproj

@@ -18,6 +18,7 @@
     <PackageReference Include="Serilog" Version="4.4.0" />
     <PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
     <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
+    <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
     <PackageReference Include="SkiaSharp" Version="4.151.1" />
     <PackageReference Include="Stateless" Version="5.20.1" />
   </ItemGroup>