Parcourir la source

浼樺寲娉电珯鐩戞帶鐣岄潰涓庨鍑烘竻鐞

DESKTOP-74CLTRG\Leol il y a 5 jours
Parent
commit
4e3a001863

+ 1 - 0
.gitignore

@@ -21,6 +21,7 @@ mono_crash.*
 [Dd]ebugPublic/
 [Rr]elease/
 [Rr]eleases/
+.buildcheck*/
 x64/
 x86/
 [Ww][Ii][Nn]32/

+ 17 - 18
src/YZWater.Avalonia/App.axaml.cs

@@ -3,6 +3,7 @@ using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Markup.Xaml;
 using Avalonia.Markup.Xaml.Styling;
+using System.Threading;
 using YZWater.Avalonia.Controls;
 using YZWater.Avalonia.Views;
 using YZWater.Core.Services;
@@ -18,6 +19,7 @@ public partial class App : Application
     private HubServer? _hubServer;
     private HubClient? _hubClient;
     private Action? _userLoggedOutHandler;
+    private int _cleanupStarted;
 
     public override void Initialize()
     {
@@ -71,11 +73,14 @@ public partial class App : Application
 
         if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
         {
+            desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
+
             // 娉ㄥ唽鍏ㄥ眬閫鍑烘竻鐞嗭紙鏃犺浠ヤ綍绉嶆柟寮忛鍑洪兘浼氭墽琛岋級
             desktop.ShutdownRequested += (_, _) =>
             {
                 Cleanup();
             };
+            desktop.Exit += (_, _) => Cleanup();
 
             ShowLoginWindow(desktop);
         }
@@ -301,26 +306,12 @@ public partial class App : Application
 
             _userLoggedOutHandler = () =>
             {
-                mainWindow.Close();
                 StopServices();
                 ShowLoginWindow(desktop);
+                mainWindow.Close();
             };
             AuthService.UserLoggedOut += _userLoggedOutHandler;
 
-            mainWindow.Closed += (_, _) =>
-            {
-                // 閲婃斁 MainViewModel 涓殑 Timer
-                if (mainWindow.DataContext is IDisposable disposable)
-                {
-                    disposable.Dispose();
-                }
-
-                if (AuthService.IsLoggedIn)
-                {
-                    desktop.Shutdown();
-                }
-            };
-
             mainWindow.Show();
         }
         catch (Exception ex)
@@ -343,15 +334,23 @@ public partial class App : Application
         _dataLogger?.Dispose();
         _dataLogger = null;
 
-        // 鍏堟柇寮 PLC锛屽啀閲嶇疆鍗曚緥锛堥伩鍏嶆柊瀹炰緥灏濊瘯杩炴帴宸叉柇寮鐨 PLC锛
-        PlcService.Dispose();
-        PlcPollingService.ResetInstance();
         AlarmService.ResetInstance();
+        PlcPollingService.ResetInstance();
+        PlcService.Dispose();
     }
 
     private void Cleanup()
     {
+        if (Interlocked.Exchange(ref _cleanupStarted, 1) != 0)
+            return;
+
         StopServices();
+        if (_userLoggedOutHandler != null)
+        {
+            AuthService.UserLoggedOut -= _userLoggedOutHandler;
+            _userLoggedOutHandler = null;
+        }
+        ThemeService.Instance.ThemeChanged -= ApplyTheme;
         DatabaseService.Dispose();
         LogService.Shutdown();
     }

+ 584 - 0
src/YZWater.Avalonia/Controls/PumpStation2DControl.cs

@@ -0,0 +1,584 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Threading;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using YZWater.Core.ViewModels;
+
+namespace YZWater.Avalonia.Controls;
+
+/// <summary>
+/// 閫氱敤娉电珯 2D 宸ヨ壓娴佺▼鍥炬帶浠 (SCADA 椋庢牸)銆
+/// 璁捐鐢诲竷 960脳540锛岀瓑姣旂缉鏀鹃傞厤瀹為檯灏哄銆
+/// </summary>
+public class PumpStation2DControl : Control
+{
+    // 鈺愨晲鈺 Styled Properties锛堟帴鍙d笉鍙橈紝淇濇寔 ViewAView.axaml 缁戝畾鍏煎锛 鈺愨晲鈺
+
+    public static readonly StyledProperty<double> Tank1LevelProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(Tank1Level));
+    public static readonly StyledProperty<double> Tank2LevelProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(Tank2Level));
+    public static readonly StyledProperty<double> Tank3LevelProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(Tank3Level));
+    public static readonly StyledProperty<double> Tank4LevelProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(Tank4Level));
+    public static readonly StyledProperty<double> InflowRateProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(InflowRate));
+    public static readonly StyledProperty<double> OutflowRateProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(OutflowRate));
+    public static readonly StyledProperty<bool> Pump1RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Pump1Running));
+    public static readonly StyledProperty<bool> Pump2RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Pump2Running));
+    public static readonly StyledProperty<bool> Pump3RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Pump3Running));
+    public static readonly StyledProperty<bool> Pump4RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Pump4Running));
+    public static readonly StyledProperty<bool> Pump5RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Pump5Running));
+    public static readonly StyledProperty<bool> Fan1RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Fan1Running));
+    public static readonly StyledProperty<bool> Fan2RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Fan2Running));
+    public static readonly StyledProperty<ValveStatus> Valve1StatusProperty = AvaloniaProperty.Register<PumpStation2DControl, ValveStatus>(nameof(Valve1Status));
+    public static readonly StyledProperty<ValveStatus> Valve2StatusProperty = AvaloniaProperty.Register<PumpStation2DControl, ValveStatus>(nameof(Valve2Status));
+    public static readonly StyledProperty<ValveStatus> Valve3StatusProperty = AvaloniaProperty.Register<PumpStation2DControl, ValveStatus>(nameof(Valve3Status));
+    public static readonly StyledProperty<ValveStatus> Valve4StatusProperty = AvaloniaProperty.Register<PumpStation2DControl, ValveStatus>(nameof(Valve4Status));
+    public static readonly StyledProperty<bool> IsInflowRunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(IsInflowRunning));
+    public static readonly StyledProperty<bool> IsOutflowRunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(IsOutflowRunning));
+    public static readonly StyledProperty<bool> HasAlarmProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(HasAlarm));
+    public static readonly StyledProperty<int> AlarmCountProperty = AvaloniaProperty.Register<PumpStation2DControl, int>(nameof(AlarmCount));
+    public static readonly StyledProperty<int> RunningDeviceCountProperty = AvaloniaProperty.Register<PumpStation2DControl, int>(nameof(RunningDeviceCount));
+    public static readonly StyledProperty<double> EfficiencyProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(Efficiency));
+
+    public double Tank1Level { get => GetValue(Tank1LevelProperty); set => SetValue(Tank1LevelProperty, value); }
+    public double Tank2Level { get => GetValue(Tank2LevelProperty); set => SetValue(Tank2LevelProperty, value); }
+    public double Tank3Level { get => GetValue(Tank3LevelProperty); set => SetValue(Tank3LevelProperty, value); }
+    public double Tank4Level { get => GetValue(Tank4LevelProperty); set => SetValue(Tank4LevelProperty, value); }
+    public double InflowRate { get => GetValue(InflowRateProperty); set => SetValue(InflowRateProperty, value); }
+    public double OutflowRate { get => GetValue(OutflowRateProperty); set => SetValue(OutflowRateProperty, value); }
+    public bool Pump1Running { get => GetValue(Pump1RunningProperty); set => SetValue(Pump1RunningProperty, value); }
+    public bool Pump2Running { get => GetValue(Pump2RunningProperty); set => SetValue(Pump2RunningProperty, value); }
+    public bool Pump3Running { get => GetValue(Pump3RunningProperty); set => SetValue(Pump3RunningProperty, value); }
+    public bool Pump4Running { get => GetValue(Pump4RunningProperty); set => SetValue(Pump4RunningProperty, value); }
+    public bool Pump5Running { get => GetValue(Pump5RunningProperty); set => SetValue(Pump5RunningProperty, value); }
+    public bool Fan1Running { get => GetValue(Fan1RunningProperty); set => SetValue(Fan1RunningProperty, value); }
+    public bool Fan2Running { get => GetValue(Fan2RunningProperty); set => SetValue(Fan2RunningProperty, value); }
+    public ValveStatus Valve1Status { get => GetValue(Valve1StatusProperty); set => SetValue(Valve1StatusProperty, value); }
+    public ValveStatus Valve2Status { get => GetValue(Valve2StatusProperty); set => SetValue(Valve2StatusProperty, value); }
+    public ValveStatus Valve3Status { get => GetValue(Valve3StatusProperty); set => SetValue(Valve3StatusProperty, value); }
+    public ValveStatus Valve4Status { get => GetValue(Valve4StatusProperty); set => SetValue(Valve4StatusProperty, value); }
+    public bool IsInflowRunning { get => GetValue(IsInflowRunningProperty); set => SetValue(IsInflowRunningProperty, value); }
+    public bool IsOutflowRunning { get => GetValue(IsOutflowRunningProperty); set => SetValue(IsOutflowRunningProperty, value); }
+    public bool HasAlarm { get => GetValue(HasAlarmProperty); set => SetValue(HasAlarmProperty, value); }
+    public int AlarmCount { get => GetValue(AlarmCountProperty); set => SetValue(AlarmCountProperty, value); }
+    public int RunningDeviceCount { get => GetValue(RunningDeviceCountProperty); set => SetValue(RunningDeviceCountProperty, value); }
+    public double Efficiency { get => GetValue(EfficiencyProperty); set => SetValue(EfficiencyProperty, value); }
+
+    // 鈺愨晲鈺 鍔ㄧ敾 鈺愨晲鈺
+
+    private double _phase;
+    private double _pumpAngle;
+    private double _fanAngle;
+    private IDisposable? _timer;
+
+    static PumpStation2DControl()
+    {
+        AffectsRender<PumpStation2DControl>(
+            Tank1LevelProperty, Tank2LevelProperty, Tank3LevelProperty, Tank4LevelProperty,
+            InflowRateProperty, OutflowRateProperty,
+            Pump1RunningProperty, Pump2RunningProperty, Pump3RunningProperty, Pump4RunningProperty, Pump5RunningProperty,
+            Fan1RunningProperty, Fan2RunningProperty,
+            Valve1StatusProperty, Valve2StatusProperty, Valve3StatusProperty, Valve4StatusProperty,
+            IsInflowRunningProperty, IsOutflowRunningProperty, HasAlarmProperty, AlarmCountProperty,
+            RunningDeviceCountProperty, EfficiencyProperty);
+    }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnAttachedToVisualTree(e);
+        ThemeHelper.ThemeChanged += OnThemeChanged;
+        _timer = DispatcherTimer.Run(() =>
+        {
+            _phase = (_phase + 1) % 360;
+            if (AnyPumpRunning) _pumpAngle = (_pumpAngle + 4) % 360;
+            if (Fan1Running || Fan2Running) _fanAngle = (_fanAngle + 6) % 360;
+            InvalidateVisual();
+            return true;
+        }, TimeSpan.FromMilliseconds(33));
+    }
+
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        ThemeHelper.ThemeChanged -= OnThemeChanged;
+        _timer?.Dispose();
+        _timer = null;
+        base.OnDetachedFromVisualTree(e);
+    }
+
+    private void OnThemeChanged() => InvalidateVisual();
+
+    private bool AnyPumpRunning => Pump1Running || Pump2Running || Pump3Running || Pump4Running || Pump5Running;
+
+    // 鈺愨晲鈺 璁捐鍧愭爣甯搁噺 (960 脳 540) 鈺愨晲鈺
+
+    private const double DW = 960, DH = 540;
+
+    // 姘寸浣嶇疆: (x, y, width, height)
+    private static readonly Rect DTank1 = new(55, 130, 120, 140);
+    private static readonly Rect DTank2 = new(275, 85, 195, 210);
+    private static readonly Rect DTank3 = new(275, 400, 155, 105);
+    private static readonly Rect DTank4 = new(720, 130, 120, 140);
+
+    // 娉典綅缃 (5 鍙版车鍦ㄩ泦姘翠簳涓嬫柟)
+    private static readonly Point[] DPumps = { new(300, 340), new(338, 340), new(376, 340), new(414, 340), new(452, 340) };
+
+    // 椋庢満浣嶇疆
+    private static readonly Point DFan1 = new(590, 450);
+    private static readonly Point DFan2 = new(660, 450);
+
+    // 闃闂ㄤ綅缃 (绠¢亾涓婄殑鍏抽敭鑺傜偣)
+    private static readonly Point DV1 = new(218, 200);   // Tank1 鈫 Tank2
+    private static readonly Point DV2 = new(555, 195);   // Header 鈫 Tank4
+    private static readonly Point DV3 = new(420, 395);   // Header 鈫 Tank3 鍒嗘敮
+    private static readonly Point DV4 = new(230, 450);   // Tank2 鈫 Tank3
+
+    // 娴侀噺璁′綅缃
+    private static readonly Point DFM1 = new(22, 195);
+    private static readonly Point DFM2 = new(870, 195);
+
+    // 鈺愨晲鈺 涓绘覆鏌 鈺愨晲鈺
+
+    public override void Render(DrawingContext context)
+    {
+        base.Render(context);
+
+        var bounds = new Rect(Bounds.Size);
+        if (bounds.Width < 120 || bounds.Height < 120) return;
+
+        // 鑳屾櫙
+        context.DrawRectangle(ThemeHelper.PanelBg, null, bounds);
+
+        // 璁$畻缂╂斁锛氳璁″潗鏍 鈫 灞忓箷鍧愭爣
+        var margin = 8.0;
+        var scaleX = (bounds.Width - 2 * margin) / DW;
+        var scaleY = (bounds.Height - 2 * margin) / DH;
+        var scale = Math.Min(scaleX, scaleY);
+        var offsetX = margin + (bounds.Width - 2 * margin - DW * scale) / 2;
+        var offsetY = margin + (bounds.Height - 2 * margin - DH * scale) / 2;
+
+        // 鍦伴潰绾
+        DrawLine(context, offsetX, offsetY, scale, 0, 520, 960, 520, ThemeHelper.Border, 1);
+
+        // 鈺愨晲鈺 绠¢亾锛堝厛鐢伙紝杩欐牱璁惧瑕嗙洊鍦ㄤ笂闈級鈺愨晲鈺
+
+        // 杩涙按绠 鈫 FM1 鈫 Tank1
+        DrawPipeH(context, offsetX, offsetY, scale, 0, 200, 55, IsInflowRunning, 10);
+        DrawFlowMeter(context, offsetX, offsetY, scale, DFM1, "杩涙按", InflowRate, IsInflowRunning);
+        DrawPipeH(context, offsetX, offsetY, scale, 48, 200, 55, IsInflowRunning, 10);
+
+        // Tank1 鈫 V1 鈫 Tank2
+        DrawPipeH(context, offsetX, offsetY, scale, 175, 200, 195, IsInflowRunning, 8);
+        DrawValve(context, offsetX, offsetY, scale, DV1, "V1", Valve1Status);
+        DrawPipeH(context, offsetX, offsetY, scale, 240, 200, 275, IsInflowRunning, 8);
+
+        // Tank2 搴曢儴 鈫 娉靛惛鍏ョ
+        DrawPipeV(context, offsetX, offsetY, scale, 372, 295, 325, AnyPumpRunning, 8);
+
+        // 娉垫帓鍑烘荤
+        DrawPipeH(context, offsetX, offsetY, scale, 285, 365, 470, AnyPumpRunning, 10);
+
+        // 鎬荤涓婂集 鈫 V2 鈫 Tank4
+        DrawPipeV(context, offsetX, offsetY, scale, 500, 365, 195, IsOutflowRunning, 10);
+        DrawPipeH(context, offsetX, offsetY, scale, 500, 195, 530, IsOutflowRunning, 10);
+        DrawValve(context, offsetX, offsetY, scale, DV2, "V2", Valve2Status);
+        DrawPipeH(context, offsetX, offsetY, scale, 580, 195, 720, IsOutflowRunning, 10);
+
+        // Tank4 鈫 FM2 鈫 鍑烘按
+        DrawPipeH(context, offsetX, offsetY, scale, 840, 200, 860, IsOutflowRunning, 10);
+        DrawFlowMeter(context, offsetX, offsetY, scale, DFM2, "鍑烘按", OutflowRate, IsOutflowRunning);
+        DrawPipeH(context, offsetX, offsetY, scale, 896, 200, 960, IsOutflowRunning, 10);
+
+        // V3 鍒嗘敮锛氭荤 鈫 Tank3
+        DrawPipeV(context, offsetX, offsetY, scale, 420, 365, 378, false, 8);
+        DrawValve(context, offsetX, offsetY, scale, DV3, "V3", Valve3Status);
+        DrawPipeV(context, offsetX, offsetY, scale, 420, 412, 400, false, 8);
+
+        // V4 鍒嗘敮锛歍ank2 鈫 Tank3
+        DrawPipeV(context, offsetX, offsetY, scale, 230, 295, 430, false, 8);
+        DrawPipeH(context, offsetX, offsetY, scale, 230, 450, 275, false, 8);
+        DrawValve(context, offsetX, offsetY, scale, DV4, "V4", Valve4Status);
+
+        // 鈺愨晲鈺 姘寸 鈺愨晲鈺
+
+        DrawTank(context, offsetX, offsetY, scale, DTank1, Tank1Level, "杩涙按姹", "IN",
+            Color.FromRgb(0, 137, 123));  // Teal
+        DrawTank(context, offsetX, offsetY, scale, DTank2, Tank2Level, "闆嗘按浜", "SUMP",
+            Color.FromRgb(33, 150, 243));  // Blue
+        DrawTank(context, offsetX, offsetY, scale, DTank3, Tank3Level, "璋冭搫姹", "BUF",
+            Color.FromRgb(126, 87, 194));  // Purple
+        DrawTank(context, offsetX, offsetY, scale, DTank4, Tank4Level, "鍑烘按姹", "OUT",
+            Color.FromRgb(0, 150, 136));   // Green
+
+        // 鈺愨晲鈺 娉电粍 鈺愨晲鈺
+
+        bool[] pumpRunning = { Pump1Running, Pump2Running, Pump3Running, Pump4Running, Pump5Running };
+        string[] pumpLabels = { "P1", "P2", "P3", "P4", "P5" };
+        for (int i = 0; i < 5; i++)
+            DrawPump(context, offsetX, offsetY, scale, DPumps[i], pumpLabels[i], pumpRunning[i]);
+
+        // 鈺愨晲鈺 椋庢満 鈺愨晲鈺
+
+        DrawFan(context, offsetX, offsetY, scale, DFan1, "F1", Fan1Running);
+        DrawFan(context, offsetX, offsetY, scale, DFan2, "F2", Fan2Running);
+
+        // 椋庢満鍖哄煙妗
+        var fanAreaX = S(offsetX, scale, 555);
+        var fanAreaY = S(offsetY, scale, 418);
+        var fanAreaW = W(scale, 180);
+        var fanAreaH = H(scale, 75);
+        context.DrawRectangle(null, new Pen(ThemeHelper.Border, 1),
+            new Rect(fanAreaX, fanAreaY, fanAreaW, fanAreaH), 3, 3);
+        DrawLabel(context, offsetX, offsetY, scale, 560, 422, "椋庢満鍖", 10, ThemeHelper.TextTertiary);
+
+        // 鈺愨晲鈺 鎶ヨ杈规 鈺愨晲鈺
+
+        if (HasAlarm)
+        {
+            var sceneRect = new Rect(margin, margin, bounds.Width - 2 * margin, bounds.Height - 2 * margin);
+            context.DrawRectangle(null, new Pen(ThemeHelper.Danger, 2), sceneRect, 4);
+        }
+
+        // 鈺愨晲鈺 HUD 鐘舵侀潰鏉 鈺愨晲鈺
+
+        DrawHud(context, bounds, scale);
+    }
+
+    // 鈺愨晲鈺 鍧愭爣杞崲杈呭姪 鈺愨晲鈺
+
+    private static double S(double offset, double scale, double designVal) => offset + designVal * scale;
+    private static double W(double scale, double designW) => designW * scale;
+    private static double H(double scale, double designH) => designH * scale;
+    private static double R(double scale, double designR) => designR * scale;
+
+    // 鈺愨晲鈺 姘寸缁樺埗 鈺愨晲鈺
+
+    private void DrawTank(DrawingContext context, double ox, double oy, double sc,
+        Rect designRect, double level, string name, string code, Color waterColor)
+    {
+        var x = S(ox, sc, designRect.X);
+        var y = S(oy, sc, designRect.Y);
+        var w = W(sc, designRect.Width);
+        var h = H(sc, designRect.Height);
+        var rect = new Rect(x, y, w, h);
+
+        // 绠变綋
+        context.DrawRectangle(ThemeHelper.SurfaceBg, new Pen(ThemeHelper.Border, 1.5), rect, 3, 3);
+
+        // 姘翠綅濉厖
+        var clampedLevel = Math.Clamp(level, 0, 100);
+        if (clampedLevel > 0.5)
+        {
+            var waterH = (h - 4) * clampedLevel / 100.0;
+            var waterRect = new Rect(x + 2, y + h - 2 - waterH, w - 4, waterH);
+            var waterBrush = new SolidColorBrush(waterColor, 0.65);
+            context.DrawRectangle(waterBrush, null, waterRect);
+
+            // 姘撮潰娉㈢汗
+            var waveY = waterRect.Y;
+            var waveBrush = new SolidColorBrush(waterColor, 0.35);
+            for (int i = 0; i < 3; i++)
+            {
+                var wx = waterRect.X + 4 + i * (waterRect.Width / 3.0);
+                context.DrawLine(new Pen(waveBrush, 1),
+                    new Point(wx, waveY), new Point(wx + waterRect.Width / 6.0, waveY + 2));
+            }
+        }
+
+        // 鍚嶇О鏍囩
+        var fontSize = Math.Max(8, 11 * sc);
+        DrawLabel(context, ox, oy, sc, designRect.Center.X - 20, designRect.Y + 6, name, 11, ThemeHelper.TextPrimary, FontWeight.Bold);
+        DrawLabel(context, ox, oy, sc, designRect.Center.X - 14, designRect.Y + 20, code, 9, ThemeHelper.TextTertiary);
+
+        // 娑蹭綅鐧惧垎姣
+        var pctText = $"{clampedLevel:F1}%";
+        var pctColor = clampedLevel > 80 ? ThemeHelper.Warning :
+                       clampedLevel < 20 ? ThemeHelper.Danger :
+                       new SolidColorBrush(waterColor);
+        DrawLabel(context, ox, oy, sc, designRect.Center.X - 18, designRect.Center.Y + 8, pctText, 13, pctColor, FontWeight.Bold);
+    }
+
+    // 鈺愨晲鈺 绠¢亾缁樺埗 鈺愨晲鈺
+
+    private void DrawPipeH(DrawingContext context, double ox, double oy, double sc,
+        double x1Design, double yDesign, double x2Design, bool isFlowing, double thicknessDesign)
+    {
+        var x1 = S(ox, sc, x1Design);
+        var y = S(oy, sc, yDesign);
+        var x2 = S(ox, sc, x2Design);
+        var thickness = R(sc, thicknessDesign);
+
+        // 绠″
+        var pipeColor = Color.FromRgb(55, 65, 81);
+        context.DrawLine(new Pen(new SolidColorBrush(pipeColor), thickness),
+            new Point(x1, y), new Point(x2, y));
+
+        // 鍐呯
+        var innerColor = Color.FromRgb(97, 111, 134);
+        context.DrawLine(new Pen(new SolidColorBrush(innerColor), thickness * 0.45),
+            new Point(x1, y), new Point(x2, y));
+
+        // 娴佸姩鍔ㄧ敾
+        if (isFlowing && x2 > x1 + 2)
+            DrawFlowDots(context, x1, y, x2, y);
+    }
+
+    private void DrawPipeV(DrawingContext context, double ox, double oy, double sc,
+        double xDesign, double y1Design, double y2Design, bool isFlowing, double thicknessDesign)
+    {
+        var x = S(ox, sc, xDesign);
+        var y1 = S(oy, sc, y1Design);
+        var y2 = S(oy, sc, y2Design);
+        var thickness = R(sc, thicknessDesign);
+
+        var pipeColor = Color.FromRgb(55, 65, 81);
+        context.DrawLine(new Pen(new SolidColorBrush(pipeColor), thickness),
+            new Point(x, y1), new Point(x, y2));
+
+        var innerColor = Color.FromRgb(97, 111, 134);
+        context.DrawLine(new Pen(new SolidColorBrush(innerColor), thickness * 0.45),
+            new Point(x, y1), new Point(x, y2));
+
+        if (isFlowing && Math.Abs(y2 - y1) > 2)
+            DrawFlowDots(context, x, y1, x, y2);
+    }
+
+    private void DrawFlowDots(DrawingContext context, double x1, double y1, double x2, double y2)
+    {
+        var flowBrush = new SolidColorBrush(Color.FromRgb(3, 169, 244), 0.85);
+        var dx = x2 - x1;
+        var dy = y2 - y1;
+        var len = Math.Sqrt(dx * dx + dy * dy);
+        if (len < 4) return;
+
+        var count = Math.Max(2, (int)(len / 18));
+        for (int i = 0; i < count; i++)
+        {
+            var t = ((_phase / 360.0) + (double)i / count) % 1.0;
+            var px = x1 + dx * t;
+            var py = y1 + dy * t;
+            var r = Math.Max(1.5, 3);
+            context.DrawEllipse(flowBrush, null, new Rect(px - r, py - r, r * 2, r * 2));
+        }
+    }
+
+    // 鈺愨晲鈺 娉电粯鍒 鈺愨晲鈺
+
+    private void DrawPump(DrawingContext context, double ox, double oy, double sc,
+        Point designPos, string label, bool running)
+    {
+        var cx = S(ox, sc, designPos.X);
+        var cy = S(oy, sc, designPos.Y);
+        var r = R(sc, 18);
+
+        var statusColor = running ? GetColor(ThemeHelper.Success) : GetColor(ThemeHelper.TextDisabled);
+        var statusBrush = new SolidColorBrush(statusColor);
+
+        // 杩愯鍏夋檿
+        if (running)
+            context.DrawEllipse(new SolidColorBrush(statusColor, 0.18), null,
+                new Rect(cx - r * 1.4, cy - r * 1.4, r * 2.8, r * 2.8));
+
+        // 娉典綋鍦
+        context.DrawEllipse(ThemeHelper.SurfaceBg, new Pen(statusBrush, running ? 2 : 1.2),
+            new Rect(cx - r, cy - r, r * 2, r * 2));
+
+        // 鏃嬭浆鍙剁墖
+        var bladeCount = 4;
+        for (int i = 0; i < bladeCount; i++)
+        {
+            var angle = (running ? _pumpAngle : 0) + i * (360.0 / bladeCount);
+            var rad = angle * Math.PI / 180.0;
+            var bx = cx + Math.Cos(rad) * r * 0.65;
+            var by = cy + Math.Sin(rad) * r * 0.65;
+            var bladeBrush = new SolidColorBrush(statusColor, running ? 0.9 : 0.45);
+            context.DrawLine(new Pen(bladeBrush, Math.Max(1.5, 2.5 * sc)),
+                new Point(cx, cy), new Point(bx, by));
+        }
+
+        // 涓績鍦
+        context.DrawEllipse(statusBrush, null,
+            new Rect(cx - r * 0.22, cy - r * 0.22, r * 0.44, r * 0.44));
+
+        // 鏍囩
+        DrawLabelAt(context, cx, cy + r + 3, label, Math.Max(8, 10 * sc), ThemeHelper.TextPrimary, FontWeight.Bold);
+    }
+
+    // 鈺愨晲鈺 椋庢満缁樺埗 鈺愨晲鈺
+
+    private void DrawFan(DrawingContext context, double ox, double oy, double sc,
+        Point designPos, string label, bool running)
+    {
+        var cx = S(ox, sc, designPos.X);
+        var cy = S(oy, sc, designPos.Y);
+        var r = R(sc, 20);
+
+        var statusColor = running ? GetColor(ThemeHelper.Info) : GetColor(ThemeHelper.TextDisabled);
+        var statusBrush = new SolidColorBrush(statusColor);
+
+        // 澶栧3
+        context.DrawEllipse(ThemeHelper.SurfaceBg, new Pen(statusBrush, running ? 2 : 1.2),
+            new Rect(cx - r, cy - r, r * 2, r * 2));
+
+        // 鏃嬭浆鍙剁墖 (5 鐗)
+        var bladeCount = 5;
+        for (int i = 0; i < bladeCount; i++)
+        {
+            var angle = (running ? _fanAngle : 0) + i * (360.0 / bladeCount);
+            var rad = angle * Math.PI / 180.0;
+            var bx = cx + Math.Cos(rad) * r * 0.72;
+            var by = cy + Math.Sin(rad) * r * 0.72;
+            var bladeBrush = new SolidColorBrush(statusColor, running ? 0.85 : 0.4);
+            context.DrawLine(new Pen(bladeBrush, Math.Max(2, 3 * sc)),
+                new Point(cx, cy), new Point(bx, by));
+        }
+
+        // 涓績
+        context.DrawEllipse(statusBrush, null,
+            new Rect(cx - r * 0.18, cy - r * 0.18, r * 0.36, r * 0.36));
+
+        // 鏍囩
+        DrawLabelAt(context, cx, cy + r + 4, label, Math.Max(8, 10 * sc), ThemeHelper.TextPrimary, FontWeight.Bold);
+    }
+
+    // 鈺愨晲鈺 闃闂ㄧ粯鍒讹紙铦磋澏闃绗﹀彿锛夆晲鈺愨晲
+
+    private void DrawValve(DrawingContext context, double ox, double oy, double sc,
+        Point designPos, string label, ValveStatus status)
+    {
+        var cx = S(ox, sc, designPos.X);
+        var cy = S(oy, sc, designPos.Y);
+        var s = R(sc, 12);
+
+        var color = status switch
+        {
+            ValveStatus.Open => GetColor(ThemeHelper.Success),
+            ValveStatus.Middle => GetColor(ThemeHelper.Warning),
+            ValveStatus.Closed => GetColor(ThemeHelper.Danger),
+            _ => GetColor(ThemeHelper.TextDisabled)
+        };
+        var colorBrush = new SolidColorBrush(color);
+
+        // 铦磋澏闃锛氫袱涓笁瑙掑舰
+        var top = new Point(cx, cy - s);
+        var bottom = new Point(cx, cy + s);
+        var left = new Point(cx - s * 0.7, cy);
+        var right = new Point(cx + s * 0.7, cy);
+
+        // 涓婁笁瑙
+        var tri1 = new StreamGeometry();
+        using (var ctx = tri1.Open())
+        {
+            ctx.BeginFigure(left, true);
+            ctx.LineTo(top);
+            ctx.LineTo(right);
+            ctx.EndFigure(true);
+        }
+        context.DrawGeometry(new SolidColorBrush(color, 0.7), null, tri1);
+
+        // 涓嬩笁瑙掞紙鍊掔疆锛
+        var tri2 = new StreamGeometry();
+        using (var ctx = tri2.Open())
+        {
+            ctx.BeginFigure(left, true);
+            ctx.LineTo(bottom);
+            ctx.LineTo(right);
+            ctx.EndFigure(true);
+        }
+        context.DrawGeometry(new SolidColorBrush(color, 0.7), null, tri2);
+
+        // 闃鏉
+        context.DrawLine(new Pen(colorBrush, Math.Max(1, 1.5 * sc)),
+            new Point(cx - s * 0.5, cy), new Point(cx + s * 0.5, cy));
+
+        // 鐘舵佹寚绀虹偣
+        var lampR = Math.Max(2, 3 * sc);
+        context.DrawEllipse(colorBrush, null,
+            new Rect(cx - lampR, cy - s - lampR * 2.5, lampR * 2, lampR * 2));
+
+        // 鏍囩
+        DrawLabelAt(context, cx, cy + s + 3, label, Math.Max(7, 9 * sc), ThemeHelper.TextSecondary, FontWeight.Bold);
+    }
+
+    // 鈺愨晲鈺 娴侀噺璁$粯鍒 鈺愨晲鈺
+
+    private void DrawFlowMeter(DrawingContext context, double ox, double oy, double sc,
+        Point designPos, string label, double value, bool active)
+    {
+        var cx = S(ox, sc, designPos.X);
+        var cy = S(oy, sc, designPos.Y);
+        var w = W(sc, 56);
+        var h = H(sc, 32);
+
+        var color = active ? GetColor(ThemeHelper.Success) : GetColor(ThemeHelper.TextDisabled);
+        var colorBrush = new SolidColorBrush(color);
+
+        // 澶栨
+        var rect = new Rect(cx - w / 2, cy - h / 2, w, h);
+        context.DrawRectangle(new SolidColorBrush(GetColor(ThemeHelper.SurfaceBg), 0.95),
+            new Pen(colorBrush, 1.2), rect, 2, 2);
+
+        // 鏍囩
+        DrawLabelAt(context, cx, cy - h * 0.18, label, Math.Max(7, 8 * sc), ThemeHelper.TextTertiary);
+
+        // 鏁板
+        DrawLabelAt(context, cx, cy + h * 0.12, $"{value:F1}", Math.Max(8, 10 * sc), colorBrush, FontWeight.Bold);
+    }
+
+    // 鈺愨晲鈺 HUD 鐘舵侀潰鏉 鈺愨晲鈺
+
+    private void DrawHud(DrawingContext context, Rect bounds, double scale)
+    {
+        var panelW = Math.Max(120, 185 * Math.Min(scale, 1.5));
+        var panelH = Math.Max(45, 58 * Math.Min(scale, 1.5));
+        var panelX = bounds.Right - panelW - 10;
+        var panelY = bounds.Top + 8;
+        var panelRect = new Rect(panelX, panelY, panelW, panelH);
+
+        context.DrawRectangle(new SolidColorBrush(GetColor(ThemeHelper.SurfaceBg), 0.92),
+            new Pen(ThemeHelper.Border, 1), panelRect, 3, 3);
+
+        var fontSize = Math.Max(8, 10 * Math.Min(scale, 1.5));
+        var smallFont = Math.Max(7, 9 * Math.Min(scale, 1.5));
+
+        DrawLabelAt(context, panelX + 8, panelY + 6, "娉电珯鎬昏", fontSize * 1.15, ThemeHelper.TextPrimary, FontWeight.Bold);
+        DrawLabelAt(context, panelX + 8, panelY + 6 + fontSize * 1.5,
+            $"杩愯 {RunningDeviceCount}  鏁堢巼 {Efficiency:F0}%", smallFont, ThemeHelper.TextSecondary);
+
+        var alarmColor = HasAlarm ? ThemeHelper.Danger : ThemeHelper.Success;
+        DrawLabelAt(context, panelX + 8, panelY + 6 + fontSize * 1.5 + smallFont * 1.5,
+            $"鎶ヨ {AlarmCount}", smallFont, alarmColor, FontWeight.Bold);
+    }
+
+    // 鈺愨晲鈺 鏂囧瓧缁樺埗杈呭姪 鈺愨晲鈺
+
+    private void DrawLabel(DrawingContext context, double ox, double oy, double sc,
+        double designX, double designY, string text, double designFontSize,
+        IBrush brush, FontWeight weight = FontWeight.Normal)
+    {
+        var x = S(ox, sc, designX);
+        var y = S(oy, sc, designY);
+        var size = Math.Max(8, designFontSize * sc);
+        DrawLabelAt(context, x, y, text, size, brush, weight);
+    }
+
+    private static void DrawLabelAt(DrawingContext context, double x, double y, string text,
+        double size, IBrush brush, FontWeight weight = FontWeight.Normal)
+    {
+        var formatted = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
+            new Typeface("Microsoft YaHei", FontStyle.Normal, weight), size, brush);
+        context.DrawText(formatted, new Point(x, y));
+    }
+
+    private static void DrawLine(DrawingContext context, double ox, double oy, double sc,
+        double x1Design, double y1Design, double x2Design, double y2Design, IBrush brush, double thickness)
+    {
+        context.DrawLine(new Pen(brush, thickness),
+            new Point(S(ox, sc, x1Design), S(oy, sc, y1Design)),
+            new Point(S(ox, sc, x2Design), S(oy, sc, y2Design)));
+    }
+
+    private static Color GetColor(IBrush brush) => brush is SolidColorBrush solid ? solid.Color : Colors.Gray;
+
+    protected override Size MeasureOverride(Size availableSize) => new(760, 420);
+}

+ 395 - 0
src/YZWater.Avalonia/Controls/PumpStation3DControl.cs

@@ -0,0 +1,395 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Threading;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using YZWater.Core.ViewModels;
+
+namespace YZWater.Avalonia.Controls;
+
+/// <summary>
+/// 閫氱敤姹℃按鎻愬崌娉电珯 2.5D 宸ヨ壓鎬昏鎺т欢銆
+/// </summary>
+public class PumpStation3DControl : Control
+{
+    public static readonly StyledProperty<double> Tank1LevelProperty = AvaloniaProperty.Register<PumpStation3DControl, double>(nameof(Tank1Level));
+    public static readonly StyledProperty<double> Tank2LevelProperty = AvaloniaProperty.Register<PumpStation3DControl, double>(nameof(Tank2Level));
+    public static readonly StyledProperty<double> Tank3LevelProperty = AvaloniaProperty.Register<PumpStation3DControl, double>(nameof(Tank3Level));
+    public static readonly StyledProperty<double> Tank4LevelProperty = AvaloniaProperty.Register<PumpStation3DControl, double>(nameof(Tank4Level));
+    public static readonly StyledProperty<double> InflowRateProperty = AvaloniaProperty.Register<PumpStation3DControl, double>(nameof(InflowRate));
+    public static readonly StyledProperty<double> OutflowRateProperty = AvaloniaProperty.Register<PumpStation3DControl, double>(nameof(OutflowRate));
+    public static readonly StyledProperty<bool> Pump1RunningProperty = AvaloniaProperty.Register<PumpStation3DControl, bool>(nameof(Pump1Running));
+    public static readonly StyledProperty<bool> Pump2RunningProperty = AvaloniaProperty.Register<PumpStation3DControl, bool>(nameof(Pump2Running));
+    public static readonly StyledProperty<bool> Pump3RunningProperty = AvaloniaProperty.Register<PumpStation3DControl, bool>(nameof(Pump3Running));
+    public static readonly StyledProperty<bool> Pump4RunningProperty = AvaloniaProperty.Register<PumpStation3DControl, bool>(nameof(Pump4Running));
+    public static readonly StyledProperty<bool> Pump5RunningProperty = AvaloniaProperty.Register<PumpStation3DControl, bool>(nameof(Pump5Running));
+    public static readonly StyledProperty<bool> Fan1RunningProperty = AvaloniaProperty.Register<PumpStation3DControl, bool>(nameof(Fan1Running));
+    public static readonly StyledProperty<bool> Fan2RunningProperty = AvaloniaProperty.Register<PumpStation3DControl, bool>(nameof(Fan2Running));
+    public static readonly StyledProperty<ValveStatus> Valve1StatusProperty = AvaloniaProperty.Register<PumpStation3DControl, ValveStatus>(nameof(Valve1Status));
+    public static readonly StyledProperty<ValveStatus> Valve2StatusProperty = AvaloniaProperty.Register<PumpStation3DControl, ValveStatus>(nameof(Valve2Status));
+    public static readonly StyledProperty<ValveStatus> Valve3StatusProperty = AvaloniaProperty.Register<PumpStation3DControl, ValveStatus>(nameof(Valve3Status));
+    public static readonly StyledProperty<ValveStatus> Valve4StatusProperty = AvaloniaProperty.Register<PumpStation3DControl, ValveStatus>(nameof(Valve4Status));
+    public static readonly StyledProperty<bool> IsInflowRunningProperty = AvaloniaProperty.Register<PumpStation3DControl, bool>(nameof(IsInflowRunning));
+    public static readonly StyledProperty<bool> IsOutflowRunningProperty = AvaloniaProperty.Register<PumpStation3DControl, bool>(nameof(IsOutflowRunning));
+    public static readonly StyledProperty<bool> HasAlarmProperty = AvaloniaProperty.Register<PumpStation3DControl, bool>(nameof(HasAlarm));
+    public static readonly StyledProperty<int> AlarmCountProperty = AvaloniaProperty.Register<PumpStation3DControl, int>(nameof(AlarmCount));
+    public static readonly StyledProperty<int> RunningDeviceCountProperty = AvaloniaProperty.Register<PumpStation3DControl, int>(nameof(RunningDeviceCount));
+    public static readonly StyledProperty<double> EfficiencyProperty = AvaloniaProperty.Register<PumpStation3DControl, double>(nameof(Efficiency));
+
+    public double Tank1Level { get => GetValue(Tank1LevelProperty); set => SetValue(Tank1LevelProperty, value); }
+    public double Tank2Level { get => GetValue(Tank2LevelProperty); set => SetValue(Tank2LevelProperty, value); }
+    public double Tank3Level { get => GetValue(Tank3LevelProperty); set => SetValue(Tank3LevelProperty, value); }
+    public double Tank4Level { get => GetValue(Tank4LevelProperty); set => SetValue(Tank4LevelProperty, value); }
+    public double InflowRate { get => GetValue(InflowRateProperty); set => SetValue(InflowRateProperty, value); }
+    public double OutflowRate { get => GetValue(OutflowRateProperty); set => SetValue(OutflowRateProperty, value); }
+    public bool Pump1Running { get => GetValue(Pump1RunningProperty); set => SetValue(Pump1RunningProperty, value); }
+    public bool Pump2Running { get => GetValue(Pump2RunningProperty); set => SetValue(Pump2RunningProperty, value); }
+    public bool Pump3Running { get => GetValue(Pump3RunningProperty); set => SetValue(Pump3RunningProperty, value); }
+    public bool Pump4Running { get => GetValue(Pump4RunningProperty); set => SetValue(Pump4RunningProperty, value); }
+    public bool Pump5Running { get => GetValue(Pump5RunningProperty); set => SetValue(Pump5RunningProperty, value); }
+    public bool Fan1Running { get => GetValue(Fan1RunningProperty); set => SetValue(Fan1RunningProperty, value); }
+    public bool Fan2Running { get => GetValue(Fan2RunningProperty); set => SetValue(Fan2RunningProperty, value); }
+    public ValveStatus Valve1Status { get => GetValue(Valve1StatusProperty); set => SetValue(Valve1StatusProperty, value); }
+    public ValveStatus Valve2Status { get => GetValue(Valve2StatusProperty); set => SetValue(Valve2StatusProperty, value); }
+    public ValveStatus Valve3Status { get => GetValue(Valve3StatusProperty); set => SetValue(Valve3StatusProperty, value); }
+    public ValveStatus Valve4Status { get => GetValue(Valve4StatusProperty); set => SetValue(Valve4StatusProperty, value); }
+    public bool IsInflowRunning { get => GetValue(IsInflowRunningProperty); set => SetValue(IsInflowRunningProperty, value); }
+    public bool IsOutflowRunning { get => GetValue(IsOutflowRunningProperty); set => SetValue(IsOutflowRunningProperty, value); }
+    public bool HasAlarm { get => GetValue(HasAlarmProperty); set => SetValue(HasAlarmProperty, value); }
+    public int AlarmCount { get => GetValue(AlarmCountProperty); set => SetValue(AlarmCountProperty, value); }
+    public int RunningDeviceCount { get => GetValue(RunningDeviceCountProperty); set => SetValue(RunningDeviceCountProperty, value); }
+    public double Efficiency { get => GetValue(EfficiencyProperty); set => SetValue(EfficiencyProperty, value); }
+
+    private double _phase;
+    private IDisposable? _timer;
+
+    static PumpStation3DControl()
+    {
+        AffectsRender<PumpStation3DControl>(
+            Tank1LevelProperty, Tank2LevelProperty, Tank3LevelProperty, Tank4LevelProperty,
+            InflowRateProperty, OutflowRateProperty,
+            Pump1RunningProperty, Pump2RunningProperty, Pump3RunningProperty, Pump4RunningProperty, Pump5RunningProperty,
+            Fan1RunningProperty, Fan2RunningProperty,
+            Valve1StatusProperty, Valve2StatusProperty, Valve3StatusProperty, Valve4StatusProperty,
+            IsInflowRunningProperty, IsOutflowRunningProperty, HasAlarmProperty, AlarmCountProperty,
+            RunningDeviceCountProperty, EfficiencyProperty);
+    }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnAttachedToVisualTree(e);
+        ThemeHelper.ThemeChanged += OnThemeChanged;
+        _timer = DispatcherTimer.Run(() =>
+        {
+            _phase = (_phase + 1) % 120;
+            InvalidateVisual();
+            return true;
+        }, TimeSpan.FromMilliseconds(40));
+    }
+
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        ThemeHelper.ThemeChanged -= OnThemeChanged;
+        _timer?.Dispose();
+        _timer = null;
+        base.OnDetachedFromVisualTree(e);
+    }
+
+    private void OnThemeChanged() => InvalidateVisual();
+
+    public override void Render(DrawingContext context)
+    {
+        base.Render(context);
+
+        var bounds = new Rect(Bounds.Size);
+        if (bounds.Width < 120 || bounds.Height < 120)
+            return;
+
+        context.DrawRectangle(ThemeHelper.PanelBg, null, bounds);
+
+        var scene = new Rect(bounds.X + 22, bounds.Y + 16, Math.Max(0, bounds.Width - 44), Math.Max(0, bounds.Height - 32));
+        var scale = Math.Min(scene.Width / 960.0, scene.Height / 540.0);
+        var origin = new Point(scene.Center.X - 132 * scale, scene.Y + scene.Height * 0.10);
+
+        DrawFloor(context, origin, scale);
+
+        // 宸ヨ壓涓荤嚎锛氳繘姘存笭閬 -> 鏍兼爡 -> 闆嗘按婀夸簳 -> 娼滄按娉 -> 闃闂ㄤ簳 -> 鍑烘按鎬荤銆
+        DrawChannel(context, origin, scale, new Point(40, 188), "杩涙按娓犻亾", Tank1Level);
+        DrawWetWell(context, origin, scale, new Point(255, 145), Tank2Level);
+        DrawValveChamber(context, origin, scale, new Point(520, 172), Tank4Level);
+        DrawCabinet(context, origin, scale, new Point(430, 64));
+
+        DrawPipe(context, origin, scale, new Point(32, 244), new Point(258, 244), IsInflowRunning, 20);
+        DrawPipe(context, origin, scale, new Point(300, 132), new Point(620, 132), IsOutflowRunning || AnyPumpRunning, 122);
+        DrawPipe(context, origin, scale, new Point(620, 132), new Point(720, 132), IsOutflowRunning, 122);
+
+        DrawRiser(context, origin, scale, new Point(322, 228), "P1", Pump1Running);
+        DrawRiser(context, origin, scale, new Point(382, 228), "P2", Pump2Running);
+        DrawRiser(context, origin, scale, new Point(442, 228), "P3", Pump3Running);
+        DrawRiser(context, origin, scale, new Point(352, 292), "P4", Pump4Running);
+        DrawRiser(context, origin, scale, new Point(412, 292), "P5", Pump5Running);
+
+        DrawValve(context, origin, scale, new Point(550, 132), "V1", Valve2Status);
+        DrawValve(context, origin, scale, new Point(610, 132), "V2", Valve3Status);
+        DrawValve(context, origin, scale, new Point(670, 132), "V3", Valve4Status);
+        DrawFlowMeter(context, origin, scale, new Point(116, 218), "杩涙按", InflowRate, IsInflowRunning);
+        DrawFlowMeter(context, origin, scale, new Point(748, 112), "鍑烘按", OutflowRate, IsOutflowRunning);
+        DrawFan(context, origin, scale, new Point(490, 300), "F1", Fan1Running);
+        DrawFan(context, origin, scale, new Point(550, 300), "F2", Fan2Running);
+        DrawHud(context, scene, scale);
+    }
+
+    private bool AnyPumpRunning => Pump1Running || Pump2Running || Pump3Running || Pump4Running || Pump5Running;
+
+    private Point Iso(Point p, double z, Point origin, double scale)
+    {
+        return new Point(origin.X + (p.X - p.Y) * 0.78 * scale, origin.Y + (p.X + p.Y) * 0.38 * scale - z * scale);
+    }
+
+    private void DrawFloor(DrawingContext context, Point origin, double scale)
+    {
+        var floor = new[] { Iso(new Point(0, 85), 0, origin, scale), Iso(new Point(875, 85), 0, origin, scale), Iso(new Point(875, 430), 0, origin, scale), Iso(new Point(0, 430), 0, origin, scale) };
+        context.DrawGeometry(new SolidColorBrush(GetColor(ThemeHelper.SurfaceBg), 0.72), new Pen(ThemeHelper.Border, 1), Polygon(floor));
+
+        var gridPen = new Pen(new SolidColorBrush(GetColor(ThemeHelper.Border), 0.38), 1);
+        for (var x = 80; x <= 840; x += 80)
+            context.DrawLine(gridPen, Iso(new Point(x, 85), 0, origin, scale), Iso(new Point(x, 430), 0, origin, scale));
+        for (var y = 125; y <= 410; y += 60)
+            context.DrawLine(gridPen, Iso(new Point(0, y), 0, origin, scale), Iso(new Point(875, y), 0, origin, scale));
+    }
+
+    private void DrawChannel(DrawingContext context, Point origin, double scale, Point p, string name, double level)
+    {
+        DrawBox(context, origin, scale, p, 180, 112, 44, StructureColor, ThemeHelper.Border);
+        DrawBox(context, origin, scale, new Point(p.X + 10, p.Y + 10), 160, 92, Math.Max(8, 34 * Math.Clamp(level, 0, 100) / 100), GetColor(ThemeHelper.Success), null, 0.66);
+        var gridPen = new Pen(ThemeHelper.TextTertiary, Math.Max(1, scale));
+        for (var x = p.X + 128; x < p.X + 168; x += 8)
+            context.DrawLine(gridPen, Iso(new Point(x, p.Y + 20), 46, origin, scale), Iso(new Point(x, p.Y + 92), 46, origin, scale));
+        DrawText(context, name, Iso(new Point(p.X + 25, p.Y + 14), 62, origin, scale), 12 * scale, ThemeHelper.TextPrimary, FontWeight.Bold);
+        DrawText(context, "绮楁牸鏍", Iso(new Point(p.X + 146, p.Y + 64), 57, origin, scale), 10 * scale, ThemeHelper.TextSecondary, FontWeight.Bold);
+    }
+
+    private void DrawWetWell(DrawingContext context, Point origin, double scale, Point p, double level)
+    {
+        const double width = 255;
+        const double depth = 214;
+        const double wallHeight = 82;
+        DrawCutawayWellWalls(context, origin, scale, p, width, depth, wallHeight);
+        var waterHeight = 14 + 74 * Math.Clamp(level, 0, 100) / 100;
+        var water = new[]
+        {
+            Iso(new Point(p.X + 16, p.Y + 16), waterHeight, origin, scale),
+            Iso(new Point(p.X + width - 16, p.Y + 16), waterHeight, origin, scale),
+            Iso(new Point(p.X + width - 16, p.Y + depth - 16), waterHeight, origin, scale),
+            Iso(new Point(p.X + 16, p.Y + depth - 16), waterHeight, origin, scale)
+        };
+        context.DrawGeometry(new SolidColorBrush(GetColor(ThemeHelper.Info), 0.60), new Pen(ThemeHelper.Info, Math.Max(1, scale)), Polygon(water));
+        var title = Iso(new Point(p.X + 70, p.Y + 14), wallHeight + 26, origin, scale);
+        DrawText(context, "闆嗘按婀夸簳", title, 14 * scale, ThemeHelper.TextPrimary, FontWeight.Bold);
+        DrawText(context, $"娑蹭綅 {level:F1}%", title + new Point(0, 15 * scale), 10 * scale, ThemeHelper.Info, FontWeight.Bold);
+        DrawText(context, "娼滄按娉电粍 (3 鐢 2 澶)", Iso(new Point(p.X + 55, p.Y + depth - 18), waterHeight + 6, origin, scale), 10 * scale, ThemeHelper.TextSecondary, FontWeight.Bold);
+    }
+
+    private void DrawCutawayWellWalls(DrawingContext context, Point origin, double scale, Point p, double width, double depth, double height)
+    {
+        var wallBrush = new SolidColorBrush(StructureColor, 0.96);
+        var rearWall = new[]
+        {
+            Iso(p, 0, origin, scale),
+            Iso(new Point(p.X + width, p.Y), 0, origin, scale),
+            Iso(new Point(p.X + width, p.Y), height, origin, scale),
+            Iso(p, height, origin, scale)
+        };
+        var sideWall = new[]
+        {
+            Iso(new Point(p.X + width, p.Y), 0, origin, scale),
+            Iso(new Point(p.X + width, p.Y + depth), 0, origin, scale),
+            Iso(new Point(p.X + width, p.Y + depth), height, origin, scale),
+            Iso(new Point(p.X + width, p.Y), height, origin, scale)
+        };
+        context.DrawGeometry(wallBrush, new Pen(ThemeHelper.Border, Math.Max(1, scale)), Polygon(rearWall));
+        context.DrawGeometry(new SolidColorBrush(Darken(StructureColor, 0.82), 0.98), new Pen(ThemeHelper.Border, Math.Max(1, scale)), Polygon(sideWall));
+
+        var rimPen = new Pen(ThemeHelper.TextSecondary, Math.Max(1.2, 1.5 * scale));
+        context.DrawLine(rimPen, Iso(p, height, origin, scale), Iso(new Point(p.X + width, p.Y), height, origin, scale));
+        context.DrawLine(rimPen, Iso(new Point(p.X + width, p.Y), height, origin, scale), Iso(new Point(p.X + width, p.Y + depth), height, origin, scale));
+    }
+
+    private void DrawValveChamber(DrawingContext context, Point origin, double scale, Point p, double level)
+    {
+        DrawBox(context, origin, scale, p, 168, 145, 58, StructureColor, ThemeHelper.Border);
+        DrawBox(context, origin, scale, new Point(p.X + 12, p.Y + 12), 144, 121, Math.Max(6, 22 * Math.Clamp(level, 0, 100) / 100), GetColor(ThemeHelper.Success), null, 0.48);
+        DrawText(context, "闃闂ㄤ簳", Iso(new Point(p.X + 55, p.Y + 12), 76, origin, scale), 12 * scale, ThemeHelper.TextPrimary, FontWeight.Bold);
+        DrawText(context, "姝㈠洖闃 / 闂搁榾", Iso(new Point(p.X + 35, p.Y + 124), 67, origin, scale), 10 * scale, ThemeHelper.TextSecondary, FontWeight.Bold);
+    }
+
+    private void DrawCabinet(DrawingContext context, Point origin, double scale, Point p)
+    {
+        DrawBox(context, origin, scale, p, 150, 82, 84, StructureColor, ThemeHelper.Border);
+        DrawText(context, "鐢垫帶鏌 / 鍙橀鏌", Iso(new Point(p.X + 20, p.Y + 22), 102, origin, scale), 11 * scale, ThemeHelper.TextPrimary, FontWeight.Bold);
+        DrawText(context, "骞插尯", Iso(new Point(p.X + 48, p.Y + 70), 95, origin, scale), 10 * scale, ThemeHelper.TextSecondary, FontWeight.Bold);
+    }
+
+    private void DrawRiser(DrawingContext context, Point origin, double scale, Point p, string label, bool running)
+    {
+        var basePoint = Iso(p, 20, origin, scale);
+        var topPoint = Iso(p, 122, origin, scale);
+        var status = running ? ThemeHelper.Success : ThemeHelper.TextDisabled;
+        context.DrawLine(new Pen(new SolidColorBrush(Darken(StructureColor, 0.70)), 12 * scale), basePoint, topPoint);
+        context.DrawLine(new Pen(status, 4 * scale), basePoint, topPoint);
+        var headerTap = new Point(p.X, 132);
+        DrawPipe(context, origin, scale, p, headerTap, running, 122);
+        DrawPump(context, origin, scale, p, label, running);
+    }
+
+    private void DrawTank(DrawingContext context, Point origin, double scale, string name, string code, Point p, double w, double d, double h, double level, Color waterColor)
+    {
+        DrawBox(context, origin, scale, p, w, d, h, Color.FromRgb(46, 59, 77), new SolidColorBrush(GetColor(ThemeHelper.Border)));
+
+        var waterHeight = h * Math.Clamp(level, 0, 100) / 100.0;
+        if (waterHeight > 2)
+            DrawBox(context, origin, scale, new Point(p.X + 8, p.Y + 7), w - 16, d - 14, waterHeight, waterColor, null, 0.62);
+
+        var top = Iso(new Point(p.X + w * 0.52, p.Y + d * 0.22), h + 12, origin, scale);
+        DrawText(context, name, top + new Point(-34 * scale, -30 * scale), 12 * scale, ThemeHelper.TextPrimary, FontWeight.Bold);
+        DrawText(context, $"{code}  {level:F1}%", top + new Point(-34 * scale, -14 * scale), 11 * scale, new SolidColorBrush(waterColor), FontWeight.Bold);
+    }
+
+    private void DrawStructure(DrawingContext context, Point origin, double scale, Point p, double w, double d, double h, string label)
+    {
+        DrawBox(context, origin, scale, p, w, d, h, Color.FromRgb(61, 72, 92), new SolidColorBrush(GetColor(ThemeHelper.Border)), 0.95);
+        var textPoint = Iso(new Point(p.X + w * 0.36, p.Y + d * 0.1), h + 8, origin, scale);
+        DrawText(context, label, textPoint + new Point(-24 * scale, -20 * scale), 11 * scale, ThemeHelper.TextSecondary, FontWeight.Bold);
+    }
+
+    private void DrawBox(DrawingContext context, Point origin, double scale, Point p, double w, double d, double h, Color color, IBrush? stroke, double opacity = 1)
+    {
+        var top = new[] { Iso(p, h, origin, scale), Iso(new Point(p.X + w, p.Y), h, origin, scale), Iso(new Point(p.X + w, p.Y + d), h, origin, scale), Iso(new Point(p.X, p.Y + d), h, origin, scale) };
+        var left = new[] { Iso(p, 0, origin, scale), Iso(new Point(p.X, p.Y + d), 0, origin, scale), Iso(new Point(p.X, p.Y + d), h, origin, scale), Iso(p, h, origin, scale) };
+        var right = new[] { Iso(new Point(p.X + w, p.Y), 0, origin, scale), Iso(new Point(p.X + w, p.Y + d), 0, origin, scale), Iso(new Point(p.X + w, p.Y + d), h, origin, scale), Iso(new Point(p.X + w, p.Y), h, origin, scale) };
+
+        var pen = stroke == null ? null : new Pen(stroke, 1);
+        context.DrawGeometry(new SolidColorBrush(Darken(color, 0.50), opacity), pen, Polygon(left));
+        context.DrawGeometry(new SolidColorBrush(Darken(color, 0.68), opacity), pen, Polygon(right));
+        context.DrawGeometry(new SolidColorBrush(Lighten(color, 0.20), opacity), pen, Polygon(top));
+    }
+
+    private void DrawPipe(DrawingContext context, Point origin, double scale, Point start, Point end, bool isFlowing, double z)
+    {
+        var a = Iso(start, z, origin, scale);
+        var b = Iso(end, z, origin, scale);
+        context.DrawLine(new Pen(new SolidColorBrush(Darken(StructureColor, 0.70)), 16 * scale), a, b);
+        context.DrawLine(new Pen(new SolidColorBrush(Lighten(StructureColor, 0.12)), 5 * scale), a, b);
+
+        if (!isFlowing)
+            return;
+
+        var flowBrush = new SolidColorBrush(Color.FromRgb(3, 169, 244));
+        for (var i = 0; i < 9; i++)
+        {
+            var t = ((_phase / 120.0) + i / 9.0) % 1.0;
+            var p = new Point(a.X + (b.X - a.X) * t, a.Y + (b.Y - a.Y) * t);
+            context.DrawEllipse(flowBrush, null, new Rect(p.X - 4 * scale, p.Y - 4 * scale, 8 * scale, 8 * scale));
+        }
+    }
+
+    private void DrawPump(DrawingContext context, Point origin, double scale, Point p, string label, bool running)
+    {
+        var c = Iso(p, 30, origin, scale);
+        var status = running ? GetColor(ThemeHelper.Success) : GetColor(ThemeHelper.TextDisabled);
+        var radius = 15 * scale;
+        var body = new Rect(c.X - radius, c.Y - radius * 1.4, radius * 2, radius * 2.25);
+        context.DrawRectangle(new SolidColorBrush(Darken(StructureColor, 0.78)), new Pen(new SolidColorBrush(status), Math.Max(1, 1.4 * scale)), body, radius * 0.42);
+        context.DrawEllipse(new SolidColorBrush(Lighten(StructureColor, 0.12)), new Pen(new SolidColorBrush(status), Math.Max(1, scale)), new Rect(c.X - radius, c.Y - radius * 1.7, radius * 2, radius * 0.78));
+        context.DrawEllipse(new SolidColorBrush(status), null, new Rect(c.X - 3 * scale, c.Y - radius * 0.55, 6 * scale, 6 * scale));
+        DrawText(context, label, c + new Point(-9 * scale, radius * 1.3), 10 * scale, ThemeHelper.TextPrimary, FontWeight.Bold);
+    }
+
+    private void DrawFan(DrawingContext context, Point origin, double scale, Point p, string label, bool running)
+    {
+        var c = Iso(p, 34, origin, scale);
+        var r = 24 * scale;
+        var status = running ? GetColor(ThemeHelper.Info) : GetColor(ThemeHelper.TextDisabled);
+
+        context.DrawEllipse(new SolidColorBrush(Color.FromRgb(31, 41, 55)), new Pen(new SolidColorBrush(status), 1.5 * scale), new Rect(c.X - r, c.Y - r, r * 2, r * 2));
+        for (var i = 0; i < 5; i++)
+        {
+            var a = (_phase * 7 + i * 72) * Math.PI / 180.0;
+            context.DrawLine(new Pen(new SolidColorBrush(status, running ? 0.9 : 0.45), 5 * scale), c, new Point(c.X + Math.Cos(a) * r * 0.7, c.Y + Math.Sin(a) * r * 0.7));
+        }
+        context.DrawEllipse(new SolidColorBrush(status), null, new Rect(c.X - 5 * scale, c.Y - 5 * scale, 10 * scale, 10 * scale));
+        DrawText(context, label, c + new Point(-10 * scale, r + 5 * scale), 11 * scale, ThemeHelper.TextPrimary, FontWeight.Bold);
+    }
+
+    private void DrawValve(DrawingContext context, Point origin, double scale, Point p, string label, ValveStatus status)
+    {
+        var c = Iso(p, 42, origin, scale);
+        var color = status switch
+        {
+            ValveStatus.Open => GetColor(ThemeHelper.Success),
+            ValveStatus.Closed => GetColor(ThemeHelper.Warning),
+            _ => GetColor(ThemeHelper.TextDisabled)
+        };
+        var s = 18 * scale;
+        var points = new[] { c + new Point(0, -s), c + new Point(s, 0), c + new Point(0, s), c + new Point(-s, 0) };
+        context.DrawGeometry(new SolidColorBrush(color, 0.85), new Pen(ThemeHelper.Border, 1), Polygon(points));
+        context.DrawLine(new Pen(ThemeHelper.TextPrimary, 2 * scale), c + new Point(-s * 0.55, 0), c + new Point(s * 0.55, 0));
+        DrawText(context, label, c + new Point(-10 * scale, s + 3 * scale), 10 * scale, ThemeHelper.TextSecondary, FontWeight.Bold);
+    }
+
+    private void DrawFlowMeter(DrawingContext context, Point origin, double scale, Point p, string label, double value, bool active)
+    {
+        var c = Iso(p, 52, origin, scale);
+        var color = active ? GetColor(ThemeHelper.Success) : GetColor(ThemeHelper.TextDisabled);
+        var rect = new Rect(c.X - 34 * scale, c.Y - 18 * scale, 68 * scale, 36 * scale);
+        context.DrawRectangle(new SolidColorBrush(GetColor(ThemeHelper.SurfaceBg), 0.95), new Pen(new SolidColorBrush(color), 1.2 * scale), rect, 4 * scale);
+        DrawText(context, label, rect.Position + new Point(8 * scale, 3 * scale), 10 * scale, ThemeHelper.TextTertiary, FontWeight.Bold);
+        DrawText(context, $"{value:F1}", rect.Position + new Point(28 * scale, 16 * scale), 12 * scale, new SolidColorBrush(color), FontWeight.Bold);
+    }
+
+    private void DrawHud(DrawingContext context, Rect scene, double scale)
+    {
+        var panel = new Rect(scene.X + 10, scene.Y + 10, 218 * scale, 78 * scale);
+        context.DrawRectangle(new SolidColorBrush(GetColor(ThemeHelper.SurfaceBg), 0.9), new Pen(ThemeHelper.Border, 1), panel, 4 * scale);
+        DrawText(context, "閫氱敤娉电珯 3D 鎬昏", panel.Position + new Point(12 * scale, 8 * scale), 13 * scale, ThemeHelper.TextPrimary, FontWeight.Bold);
+        DrawText(context, "杩涙按娓 -> 闆嗘按浜 -> 娉电粍 -> 鍑烘按鎬荤", panel.Position + new Point(12 * scale, 30 * scale), 10 * scale, ThemeHelper.TextTertiary, FontWeight.Bold);
+        DrawText(context, $"RUN {RunningDeviceCount}  EFF {Efficiency:F0}%  ALM {AlarmCount}", panel.Position + new Point(12 * scale, 50 * scale), 11 * scale, HasAlarm ? ThemeHelper.Danger : ThemeHelper.Success, FontWeight.Bold);
+        if (HasAlarm)
+            context.DrawRectangle(null, new Pen(ThemeHelper.Danger, 2), scene, 4);
+    }
+
+    private static StreamGeometry Polygon(IReadOnlyList<Point> points)
+    {
+        var geometry = new StreamGeometry();
+        using var ctx = geometry.Open();
+        ctx.BeginFigure(points[0], true);
+        for (var i = 1; i < points.Count; i++)
+            ctx.LineTo(points[i]);
+        ctx.EndFigure(true);
+        return geometry;
+    }
+
+    private static void DrawText(DrawingContext context, string text, Point point, double size, IBrush brush, FontWeight weight)
+    {
+        var formatted = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Microsoft YaHei", FontStyle.Normal, weight), Math.Max(8, size), brush);
+        context.DrawText(formatted, point);
+    }
+
+    private static Color GetColor(IBrush brush) => brush is SolidColorBrush solid ? solid.Color : Colors.Gray;
+
+    private Color StructureColor => Lighten(GetColor(ThemeHelper.Border), 0.18);
+
+    private static Color Darken(Color color, double factor) => Color.FromRgb((byte)(color.R * factor), (byte)(color.G * factor), (byte)(color.B * factor));
+
+    private static Color Lighten(Color color, double amount)
+    {
+        return Color.FromRgb(
+            (byte)Math.Min(255, color.R + (255 - color.R) * amount),
+            (byte)Math.Min(255, color.G + (255 - color.G) * amount),
+            (byte)Math.Min(255, color.B + (255 - color.B) * amount));
+    }
+
+    protected override Size MeasureOverride(Size availableSize) => new(760, 420);
+}

+ 27 - 27
src/YZWater.Avalonia/Controls/ThemeHelper.cs

@@ -11,22 +11,22 @@ internal static class ThemeHelper
     public static void NotifyThemeChanged() => ThemeChanged?.Invoke();
 
     // 鈹鈹鈹 褰撳墠涓婚鐢诲埛缂撳瓨 鈹鈹鈹
-    public static IBrush AppBg { get; private set; } = new SolidColorBrush(Color.Parse("#0A0E14"));
-    public static IBrush SurfaceBg { get; private set; } = new SolidColorBrush(Color.Parse("#111820"));
-    public static IBrush PanelBg { get; private set; } = new SolidColorBrush(Color.Parse("#0D1117"));
-    public static IBrush Border { get; private set; } = new SolidColorBrush(Color.Parse("#1C2333"));
+    public static IBrush AppBg { get; private set; } = new SolidColorBrush(Color.Parse("#08111B"));
+    public static IBrush SurfaceBg { get; private set; } = new SolidColorBrush(Color.Parse("#101D2A"));
+    public static IBrush PanelBg { get; private set; } = new SolidColorBrush(Color.Parse("#0B1621"));
+    public static IBrush Border { get; private set; } = new SolidColorBrush(Color.Parse("#274055"));
     public static IBrush TextPrimary { get; private set; } = new SolidColorBrush(Color.Parse("#E6EDF3"));
     public static IBrush TextSecondary { get; private set; } = new SolidColorBrush(Color.Parse("#9CA3AF"));
     public static IBrush TextTertiary { get; private set; } = new SolidColorBrush(Color.Parse("#6B7280"));
     public static IBrush TextDisabled { get; private set; } = new SolidColorBrush(Color.Parse("#4B5563"));
-    public static IBrush Success { get; private set; } = new SolidColorBrush(Color.Parse("#00897B"));
-    public static IBrush Warning { get; private set; } = new SolidColorBrush(Color.Parse("#F59E0B"));
-    public static IBrush Danger { get; private set; } = new SolidColorBrush(Color.Parse("#EF4444"));
-    public static IBrush Info { get; private set; } = new SolidColorBrush(Color.Parse("#3B82F6"));
-    public static IBrush HeaderBg { get; private set; } = new SolidColorBrush(Color.Parse("#111820"));
+    public static IBrush Success { get; private set; } = new SolidColorBrush(Color.Parse("#18B887"));
+    public static IBrush Warning { get; private set; } = new SolidColorBrush(Color.Parse("#F5A524"));
+    public static IBrush Danger { get; private set; } = new SolidColorBrush(Color.Parse("#F05D5E"));
+    public static IBrush Info { get; private set; } = new SolidColorBrush(Color.Parse("#32A7E8"));
+    public static IBrush HeaderBg { get; private set; } = new SolidColorBrush(Color.Parse("#0E1B28"));
     public static IBrush HeaderText { get; private set; } = new SolidColorBrush(Color.Parse("#E6EDF3"));
     public static IBrush HeaderSubtext { get; private set; } = new SolidColorBrush(Color.Parse("#9CA3AF"));
-    public static IBrush NavBg { get; private set; } = new SolidColorBrush(Color.Parse("#111820"));
+    public static IBrush NavBg { get; private set; } = new SolidColorBrush(Color.Parse("#0E1B28"));
 
     /// <summary>
     /// 璁剧疆褰撳墠涓婚锛堢敱 App.axaml.cs 璋冪敤锛
@@ -36,37 +36,37 @@ internal static class ThemeHelper
         Console.WriteLine($"[ThemeHelper] SetTheme: isDark={isDark}, current HeaderBg={((SolidColorBrush)HeaderBg).Color}");
         if (isDark)
         {
-            AppBg = new SolidColorBrush(Color.Parse("#0A0E14"));
-            SurfaceBg = new SolidColorBrush(Color.Parse("#111820"));
-            PanelBg = new SolidColorBrush(Color.Parse("#0D1117"));
-            Border = new SolidColorBrush(Color.Parse("#1C2333"));
+            AppBg = new SolidColorBrush(Color.Parse("#08111B"));
+            SurfaceBg = new SolidColorBrush(Color.Parse("#101D2A"));
+            PanelBg = new SolidColorBrush(Color.Parse("#0B1621"));
+            Border = new SolidColorBrush(Color.Parse("#274055"));
             TextPrimary = new SolidColorBrush(Color.Parse("#E6EDF3"));
             TextSecondary = new SolidColorBrush(Color.Parse("#9CA3AF"));
             TextTertiary = new SolidColorBrush(Color.Parse("#6B7280"));
             TextDisabled = new SolidColorBrush(Color.Parse("#4B5563"));
-            Success = new SolidColorBrush(Color.Parse("#00897B"));
-            Warning = new SolidColorBrush(Color.Parse("#F59E0B"));
-            Danger = new SolidColorBrush(Color.Parse("#EF4444"));
-            Info = new SolidColorBrush(Color.Parse("#3B82F6"));
-            HeaderBg = new SolidColorBrush(Color.Parse("#111820"));
+            Success = new SolidColorBrush(Color.Parse("#18B887"));
+            Warning = new SolidColorBrush(Color.Parse("#F5A524"));
+            Danger = new SolidColorBrush(Color.Parse("#F05D5E"));
+            Info = new SolidColorBrush(Color.Parse("#32A7E8"));
+            HeaderBg = new SolidColorBrush(Color.Parse("#0E1B28"));
             HeaderText = new SolidColorBrush(Color.Parse("#E6EDF3"));
             HeaderSubtext = new SolidColorBrush(Color.Parse("#9CA3AF"));
-            NavBg = new SolidColorBrush(Color.Parse("#111820"));
+            NavBg = new SolidColorBrush(Color.Parse("#0E1B28"));
         }
         else
         {
-            AppBg = new SolidColorBrush(Color.Parse("#E5E7EB"));
-            SurfaceBg = new SolidColorBrush(Color.Parse("#F3F4F6"));
+            AppBg = new SolidColorBrush(Color.Parse("#EAF1F5"));
+            SurfaceBg = new SolidColorBrush(Color.Parse("#F7FAFC"));
             PanelBg = new SolidColorBrush(Color.Parse("#FFFFFF"));
-            Border = new SolidColorBrush(Color.Parse("#9CA3AF"));
+            Border = new SolidColorBrush(Color.Parse("#C7D6E2"));
             TextPrimary = new SolidColorBrush(Color.Parse("#111827"));
             TextSecondary = new SolidColorBrush(Color.Parse("#4B5563"));
             TextTertiary = new SolidColorBrush(Color.Parse("#6B7280"));
             TextDisabled = new SolidColorBrush(Color.Parse("#9CA3AF"));
-            Success = new SolidColorBrush(Color.Parse("#047857"));
-            Warning = new SolidColorBrush(Color.Parse("#B45309"));
-            Danger = new SolidColorBrush(Color.Parse("#B91C1C"));
-            Info = new SolidColorBrush(Color.Parse("#1D4ED8"));
+            Success = new SolidColorBrush(Color.Parse("#087F5B"));
+            Warning = new SolidColorBrush(Color.Parse("#B96A07"));
+            Danger = new SolidColorBrush(Color.Parse("#C43D45"));
+            Info = new SolidColorBrush(Color.Parse("#1676B9"));
             HeaderBg = new SolidColorBrush(Color.Parse("#1F2937"));
             HeaderText = new SolidColorBrush(Color.Parse("#F9FAFB"));
             HeaderSubtext = new SolidColorBrush(Color.Parse("#9CA3AF"));

+ 34 - 3
src/YZWater.Avalonia/Themes/IndustrialStyles.axaml

@@ -8,6 +8,13 @@
     <!-- 鈹鈹鈹 鍏ㄥ眬鎸夐挳: 鎵嬪瀷鍏夋爣 鈹鈹鈹 -->
     <Style Selector="Button">
         <Setter Property="Cursor" Value="Hand"/>
+        <Setter Property="CornerRadius" Value="5"/>
+    </Style>
+
+    <Style Selector="Border.surface-card">
+        <Setter Property="CornerRadius" Value="6"/>
+        <Setter Property="BorderThickness" Value="1"/>
+        <Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/>
     </Style>
 
     <!-- 鈹鈹鈹 鎸夐挳: Success 鈹鈹鈹 -->
@@ -18,7 +25,7 @@
         <Setter Property="FontFamily" Value="{DynamicResource MonoFont}"/>
         <Setter Property="FontSize" Value="11"/>
         <Setter Property="Padding" Value="12,6"/>
-        <Setter Property="CornerRadius" Value="2"/>
+        <Setter Property="CornerRadius" Value="5"/>
         <Setter Property="MinHeight" Value="32"/>
     </Style>
     <Style Selector="Button.btn-success:pointerover /template/ ContentPresenter">
@@ -39,7 +46,7 @@
         <Setter Property="FontFamily" Value="{DynamicResource MonoFont}"/>
         <Setter Property="FontSize" Value="11"/>
         <Setter Property="Padding" Value="12,6"/>
-        <Setter Property="CornerRadius" Value="2"/>
+        <Setter Property="CornerRadius" Value="5"/>
         <Setter Property="MinHeight" Value="32"/>
     </Style>
     <Style Selector="Button.btn-danger:pointerover /template/ ContentPresenter">
@@ -60,7 +67,7 @@
         <Setter Property="FontFamily" Value="{DynamicResource MonoFont}"/>
         <Setter Property="FontSize" Value="11"/>
         <Setter Property="Padding" Value="12,6"/>
-        <Setter Property="CornerRadius" Value="2"/>
+        <Setter Property="CornerRadius" Value="5"/>
         <Setter Property="MinHeight" Value="32"/>
     </Style>
     <Style Selector="Button.btn-info:pointerover /template/ ContentPresenter">
@@ -115,4 +122,28 @@
         <Setter Property="Background" Value="{DynamicResource SuccessPressedBrush}"/>
     </Style>
 
+    <!-- 鈹鈹鈹 瑙嗗浘鍒囨崲锛氱鐢ㄩ」鍗冲綋鍓嶅凡閫夎鍥 鈹鈹鈹 -->
+    <Style Selector="Button.view-switch">
+        <Setter Property="Background" Value="{DynamicResource PanelBgBrush}"/>
+        <Setter Property="Foreground" Value="{DynamicResource TextTertiaryBrush}"/>
+        <Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/>
+        <Setter Property="BorderThickness" Value="1"/>
+        <Setter Property="CornerRadius" Value="4"/>
+        <Setter Property="Padding" Value="12,4"/>
+        <Setter Property="MinHeight" Value="26"/>
+        <Setter Property="FontFamily" Value="{DynamicResource MonoFont}"/>
+        <Setter Property="FontSize" Value="10"/>
+    </Style>
+    <Style Selector="Button.view-switch:pointerover /template/ ContentPresenter">
+        <Setter Property="Background" Value="{DynamicResource ToggleBgBrush}"/>
+        <Setter Property="Foreground" Value="{DynamicResource InfoBrush}"/>
+    </Style>
+    <Style Selector="Button.view-switch:disabled">
+        <Setter Property="Opacity" Value="1"/>
+        <Setter Property="Background" Value="{DynamicResource SuccessBrush}"/>
+        <Setter Property="Foreground" Value="White"/>
+        <Setter Property="BorderBrush" Value="{DynamicResource SuccessBrush}"/>
+        <Setter Property="FontWeight" Value="Bold"/>
+    </Style>
+
 </Styles>

+ 16 - 16
src/YZWater.Avalonia/Themes/IndustrialTheme.axaml

@@ -6,14 +6,14 @@
          鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺 -->
 
     <!-- 鑳屾櫙灞傜骇: AppBg 鈫 SurfaceBg 鈫 PanelBg 鈫 CardBg -->
-    <Color x:Key="AppBgColor">#0A0E14</Color>
-    <Color x:Key="SurfaceBgColor">#111820</Color>
-    <Color x:Key="PanelBgColor">#0D1117</Color>
-    <Color x:Key="CardBgColor">#161B22</Color>
+    <Color x:Key="AppBgColor">#08111B</Color>
+    <Color x:Key="SurfaceBgColor">#101D2A</Color>
+    <Color x:Key="PanelBgColor">#0B1621</Color>
+    <Color x:Key="CardBgColor">#142535</Color>
 
     <!-- 杈规/鍒嗛殧绾 -->
-    <Color x:Key="BorderColor">#1C2333</Color>
-    <Color x:Key="DividerColor">#21262D</Color>
+    <Color x:Key="BorderColor">#274055</Color>
+    <Color x:Key="DividerColor">#1D3447</Color>
 
     <!-- 鏂囧瓧灞傜骇 -->
     <Color x:Key="TextPrimaryColor">#E6EDF3</Color>
@@ -22,11 +22,11 @@
     <Color x:Key="TextDisabledColor">#4B5563</Color>
 
     <!-- 璇箟鐘舵佽壊 -->
-    <Color x:Key="SuccessColor">#00897B</Color>
-    <Color x:Key="WarningColor">#F59E0B</Color>
-    <Color x:Key="DangerColor">#EF4444</Color>
-    <Color x:Key="InfoColor">#3B82F6</Color>
-    <Color x:Key="WaterColor">#0288D1</Color>
+    <Color x:Key="SuccessColor">#18B887</Color>
+    <Color x:Key="WarningColor">#F5A524</Color>
+    <Color x:Key="DangerColor">#F05D5E</Color>
+    <Color x:Key="InfoColor">#32A7E8</Color>
+    <Color x:Key="WaterColor">#21B5D8</Color>
 
     <!-- 璁惧鐘舵佽壊 -->
     <Color x:Key="DeviceRunningColor">#00897B</Color>
@@ -42,12 +42,12 @@
     <Color x:Key="AlarmBgColor">#1C0000</Color>
 
     <!-- 鏍囬鏍忎笓鐢ㄨ壊 -->
-    <Color x:Key="HeaderBgColor">#111820</Color>
+    <Color x:Key="HeaderBgColor">#0E1B28</Color>
     <Color x:Key="HeaderTextColor">#E6EDF3</Color>
     <Color x:Key="HeaderSubtextColor">#9CA3AF</Color>
 
     <!-- 搴曢儴瀵艰埅鏍 -->
-    <Color x:Key="NavBgColor">#111820</Color>
+    <Color x:Key="NavBgColor">#0E1B28</Color>
 
     <!-- 鎸夐挳浜や簰鐘舵佽壊 -->
     <Color x:Key="SuccessHoverColor">#009688</Color>
@@ -60,9 +60,9 @@
     <Color x:Key="WarningPressedColor">#3D2A00</Color>
 
     <!-- 鍒囨崲鎸夐挳涓撶敤鑹 -->
-    <Color x:Key="ToggleBgColor">#1C2333</Color>
-    <Color x:Key="ToggleBorderColor">#37474F</Color>
-    <Color x:Key="ToggleActiveBgColor">#00897B</Color>
+    <Color x:Key="ToggleBgColor">#162A3C</Color>
+    <Color x:Key="ToggleBorderColor">#36536B</Color>
+    <Color x:Key="ToggleActiveBgColor">#117A61</Color>
 
     <!-- 鐢诲埛 -->
     <SolidColorBrush x:Key="AppBgBrush" Color="{StaticResource AppBgColor}"/>

+ 11 - 11
src/YZWater.Avalonia/Themes/IndustrialThemeLight.axaml

@@ -6,8 +6,8 @@
          鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺 -->
 
     <!-- 鑳屾櫙灞傜骇 (鏇存繁鐨勫眰娆″尯鍒) -->
-    <Color x:Key="AppBgColor">#E5E7EB</Color>
-    <Color x:Key="SurfaceBgColor">#F3F4F6</Color>
+    <Color x:Key="AppBgColor">#EAF1F5</Color>
+    <Color x:Key="SurfaceBgColor">#F7FAFC</Color>
     <Color x:Key="PanelBgColor">#FFFFFF</Color>
     <Color x:Key="CardBgColor">#FFFFFF</Color>
 
@@ -20,8 +20,8 @@
     <Color x:Key="NavBgColor">#F9FAFB</Color>
 
     <!-- 杈规/鍒嗛殧绾 (鏇存槑鏄) -->
-    <Color x:Key="BorderColor">#9CA3AF</Color>
-    <Color x:Key="DividerColor">#D1D5DB</Color>
+    <Color x:Key="BorderColor">#C7D6E2</Color>
+    <Color x:Key="DividerColor">#DCE6ED</Color>
 
     <!-- 鏂囧瓧灞傜骇 -->
     <Color x:Key="TextPrimaryColor">#111827</Color>
@@ -30,11 +30,11 @@
     <Color x:Key="TextDisabledColor">#9CA3AF</Color>
 
     <!-- 璇箟鐘舵佽壊 (娣辫壊鐗堬紝纭繚鍦ㄦ祬鑹茶儗鏅笂鍙) -->
-    <Color x:Key="SuccessColor">#047857</Color>
-    <Color x:Key="WarningColor">#B45309</Color>
-    <Color x:Key="DangerColor">#B91C1C</Color>
-    <Color x:Key="InfoColor">#1D4ED8</Color>
-    <Color x:Key="WaterColor">#0369A1</Color>
+    <Color x:Key="SuccessColor">#087F5B</Color>
+    <Color x:Key="WarningColor">#B96A07</Color>
+    <Color x:Key="DangerColor">#C43D45</Color>
+    <Color x:Key="InfoColor">#1676B9</Color>
+    <Color x:Key="WaterColor">#0C83B5</Color>
 
     <!-- 璁惧鐘舵佽壊 -->
     <Color x:Key="DeviceRunningColor">#047857</Color>
@@ -60,8 +60,8 @@
     <Color x:Key="WarningPressedColor">#FDE68A</Color>
 
     <!-- 鍒囨崲鎸夐挳涓撶敤鑹 -->
-    <Color x:Key="ToggleBgColor">#E5E7EB</Color>
-    <Color x:Key="ToggleBorderColor">#9CA3AF</Color>
+    <Color x:Key="ToggleBgColor">#E9F2F7</Color>
+    <Color x:Key="ToggleBorderColor">#AEC4D2</Color>
     <Color x:Key="ToggleActiveBgColor">#047857</Color>
 
     <!-- 鐢诲埛 -->

+ 6 - 1
src/YZWater.Avalonia/Views/LoginView.axaml.cs

@@ -1,4 +1,6 @@
+using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using YZWater.Core.ViewModels;
@@ -58,6 +60,9 @@ public partial class LoginView : Window
     // 鍏抽棴
     private void OnCloseClick(object? sender, global::Avalonia.Interactivity.RoutedEventArgs e)
     {
-        Close();
+        if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+            desktop.Shutdown();
+        else
+            Close();
     }
 }

+ 14 - 12
src/YZWater.Avalonia/Views/MainWindow.axaml

@@ -29,6 +29,8 @@
                 <StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
                     <Button Content="鈹" Click="OnMinimizeClick" Width="36" Height="32" BorderThickness="0"
                             Background="Transparent" FontSize="14" HorizontalContentAlignment="Center"/>
+                    <Button x:Name="MaximizeBtn" Content="鈽" Click="OnMaximizeClick" Width="36" Height="32" BorderThickness="0"
+                            Background="Transparent" FontSize="14" HorizontalContentAlignment="Center"/>
                     <Button Content="鉁" Click="OnCloseClick" Width="36" Height="32" BorderThickness="0"
                             Background="Transparent" FontSize="14" HorizontalContentAlignment="Center"
                             Foreground="#E74C3C"/>
@@ -146,23 +148,23 @@
         <!-- 鈺愨晲鈺 涓诲唴瀹瑰尯鍩 鈺愨晲鈺 -->
         <Panel>
         <TabControl SelectedIndex="{Binding SelectedTabIndex}" Background="{DynamicResource AppBgBrush}">
-            <TabItem Header="PROCESS" IsVisible="False">
-                <views:ViewAView DataContext="{Binding ViewA}"/>
+            <TabItem Header="PROCESS" IsVisible="False" DataContext="{Binding ViewA}">
+                <views:ViewAView/>
             </TabItem>
-            <TabItem Header="PARAMS" IsVisible="False">
-                <views:ViewBView DataContext="{Binding ViewB}"/>
+            <TabItem Header="PARAMS" IsVisible="False" DataContext="{Binding ViewB}">
+                <views:ViewBView/>
             </TabItem>
-            <TabItem Header="FLOW" IsVisible="False">
-                <views:ViewCView DataContext="{Binding ViewC}"/>
+            <TabItem Header="FLOW" IsVisible="False" DataContext="{Binding ViewC}">
+                <views:ViewCView/>
             </TabItem>
-            <TabItem Header="ALARM" IsVisible="False">
-                <views:ViewDView DataContext="{Binding ViewD}"/>
+            <TabItem Header="ALARM" IsVisible="False" DataContext="{Binding ViewD}">
+                <views:ViewDView/>
             </TabItem>
-            <TabItem Header="ABOUT" IsVisible="False">
-                <views:ViewEView DataContext="{Binding ViewE}"/>
+            <TabItem Header="ABOUT" IsVisible="False" DataContext="{Binding ViewE}">
+                <views:ViewEView/>
             </TabItem>
-            <TabItem Header="AUDIT" IsVisible="False">
-                <views:ViewFView DataContext="{Binding ViewF}"/>
+            <TabItem Header="AUDIT" IsVisible="False" DataContext="{Binding ViewF}">
+                <views:ViewFView/>
             </TabItem>
         </TabControl>
 

+ 59 - 4
src/YZWater.Avalonia/Views/MainWindow.axaml.cs

@@ -1,5 +1,8 @@
+using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.Media;
 using YZWater.Avalonia.Controls;
 using YZWater.Core.Services;
@@ -9,11 +12,13 @@ namespace YZWater.Avalonia.Views;
 public partial class MainWindow : Window
 {
     private Border? _navBar;
+    private Button? _maximizeBtn;
 
     public MainWindow()
     {
         InitializeComponent();
         _navBar = this.FindControl<Border>("NavBar");
+        _maximizeBtn = this.FindControl<Button>("MaximizeBtn");
         ThemeHelper.ThemeChanged += OnThemeChanged;
         OnThemeChanged();
     }
@@ -24,24 +29,74 @@ public partial class MainWindow : Window
         if (_navBar != null) _navBar.Background = ThemeHelper.NavBg;
     }
 
-    // 鑷畾涔夋爣棰樻爮锛氭嫋鎷界Щ鍔ㄧ獥鍙
+    // 鑷畾涔夋爣棰樻爮锛鍗曞嚮鎷栨嫿绉诲姩绐楀彛锛屽弻鍑诲垏鎹㈡渶澶у寲
     private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
     {
-        if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
+        var point = e.GetCurrentPoint(this);
+        if (!point.Properties.IsLeftButtonPressed) return;
+
+        if (e.ClickCount == 2)
+        {
+            ToggleWindowState();
+        }
+        else
         {
             BeginMoveDrag(e);
         }
     }
 
     // 鏈灏忓寲
-    private void OnMinimizeClick(object? sender, global::Avalonia.Interactivity.RoutedEventArgs e)
+    private void OnMinimizeClick(object? sender, RoutedEventArgs e)
     {
         WindowState = WindowState.Minimized;
     }
 
+    // 鏈澶у寲/杩樺師
+    private void OnMaximizeClick(object? sender, RoutedEventArgs e)
+    {
+        ToggleWindowState();
+    }
+
+    private void ToggleWindowState()
+    {
+        WindowState = WindowState == WindowState.Maximized
+            ? WindowState.Normal
+            : WindowState.Maximized;
+    }
+
+    protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+    {
+        base.OnPropertyChanged(change);
+        if (change.Property == WindowStateProperty)
+        {
+            UpdateMaximizeButtonIcon();
+        }
+    }
+
+    private void UpdateMaximizeButtonIcon()
+    {
+        if (_maximizeBtn != null)
+        {
+            _maximizeBtn.Content = WindowState == WindowState.Maximized ? "鉂" : "鈽";
+        }
+    }
+
+    protected override void OnClosed(EventArgs e)
+    {
+        ThemeHelper.ThemeChanged -= OnThemeChanged;
+
+        if (DataContext is IDisposable disposable)
+            disposable.Dispose();
+
+        base.OnClosed(e);
+    }
+
     // 鍏抽棴
     private void OnCloseClick(object? sender, global::Avalonia.Interactivity.RoutedEventArgs e)
     {
-        Close();
+        if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+            desktop.Shutdown();
+        else
+            Close();
     }
 }

+ 86 - 234
src/YZWater.Avalonia/Views/ViewAView.axaml

@@ -3,29 +3,30 @@
              xmlns:vm="using:YZWater.Core.ViewModels"
              xmlns:controls="using:YZWater.Avalonia.Controls"
              x:Class="YZWater.Avalonia.Views.ViewAView"
-            >
-
+             x:DataType="vm:ViewAViewModel">
 
     <Border x:Name="RootBorder" Background="{DynamicResource AppBgBrush}">
-        <Grid RowDefinitions="48,*,32">
+        <Grid RowDefinitions="56,*,36">
 
             <!-- 鈺愨晲鈺 椤堕儴鏍囬鏍 鈺愨晲鈺 -->
             <Border x:Name="TitleBar" Grid.Row="0" Background="{DynamicResource HeaderBgBrush}" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
-                <Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" Margin="16,0">
+                <Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" Margin="20,0">
                     <StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="12" VerticalAlignment="Center">
-                        <Border Background="{DynamicResource SuccessBrush}" Width="4" Height="24" CornerRadius="2"/>
-                        <TextBlock Text="{Binding TitleText}" FontSize="16" FontWeight="Bold"
-                                   Foreground="{DynamicResource TextPrimaryBrush}" FontFamily="Consolas, monospace"
+                        <Border Background="{DynamicResource SuccessBrush}" Width="4" Height="28" CornerRadius="2"/>
+                        <TextBlock Text="{Binding TitleText}" FontSize="17" FontWeight="Bold"
+                                   Foreground="{DynamicResource HeaderTextBrush}" FontFamily="{DynamicResource MonoFont}"
                                    VerticalAlignment="Center"/>
                         <TextBlock x:Name="SubtitleText" Text="{Binding SubtitleText}" FontSize="12"
                                    Foreground="{DynamicResource HeaderSubtextBrush}" VerticalAlignment="Center"/>
                     </StackPanel>
 
-                    <StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center" Margin="24,0">
+                    <Border Grid.Column="2" Background="{DynamicResource ToggleBgBrush}" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" CornerRadius="12" Padding="10,5" VerticalAlignment="Center" Margin="24,0">
+                    <StackPanel Orientation="Horizontal" Spacing="7">
                         <Border Width="10" Height="10" CornerRadius="5" Background="{DynamicResource SuccessBrush}"/>
-                        <TextBlock Text="{Binding PlcStatusText}" FontFamily="Consolas, monospace" FontSize="11"
+                        <TextBlock Text="{Binding PlcStatusText}" FontFamily="{DynamicResource MonoFont}" FontSize="10"
                                    Foreground="{DynamicResource SuccessBrush}" VerticalAlignment="Center"/>
                     </StackPanel>
+                    </Border>
 
                     <StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center" Margin="16,0">
                         <Button Content="{Binding ConnectText}" Command="{Binding ConnectPlcCommand}" Classes="btn-success" Padding="8,3" FontSize="10"/>
@@ -45,226 +46,79 @@
             </Border>
 
             <!-- 鈺愨晲鈺 涓诲唴瀹瑰尯 鈺愨晲鈺 -->
-            <Grid Grid.Row="1" ColumnDefinitions="*,260" Margin="8,4">
+            <Grid Grid.Row="1" ColumnDefinitions="*,300" Margin="14,12">
 
                 <!-- 宸︿晶锛氬伐鑹烘祦绋嬪浘 -->
-                <Border Grid.Column="0" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2" Margin="0,0,4,0"
-                        BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
-                    <Grid RowDefinitions="Auto,*,Auto" Margin="12,8">
+                <Border Grid.Column="0" Classes="surface-card" Background="{DynamicResource SurfaceBgBrush}" Margin="0,0,10,0">
+                    <Grid RowDefinitions="Auto,*,Auto" Margin="16,14">
 
-                        <StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8" Margin="0,0,0,8">
+                        <StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8" Margin="0,0,0,12">
                             <Border Background="{DynamicResource SuccessBrush}" Width="3" Height="16" CornerRadius="1"/>
                             <TextBlock Text="{Binding ProcessFlowText}" FontFamily="Consolas, monospace"
                                        FontSize="12" FontWeight="Bold" Foreground="{DynamicResource TextSecondaryBrush}"/>
+                            <Border Background="{DynamicResource BorderBrush}" Width="1" Height="16" Margin="8,0"/>
+                            <Button Content="2D" Command="{Binding Set2DViewCommand}" IsEnabled="{Binding Is3DView}" Classes="view-switch"/>
+                            <Button Content="3D" Command="{Binding Set3DViewCommand}" IsEnabled="{Binding Is2DView}" Classes="view-switch"/>
                         </StackPanel>
 
-                        <!-- 宸ヨ壓娴佺▼鍖哄煙 - 浣跨敤 Grid 鏇夸唬 Canvas 瀹炵幇鑷傚簲 -->
-                        <Border Grid.Row="1" Background="{DynamicResource PanelBgBrush}" CornerRadius="2" Padding="8">
-                            <Grid RowDefinitions="Auto,Auto,Auto,Auto" ColumnDefinitions="*,*,*,*,Auto">
-
-                                <!-- 绗竴琛岋細姘寸 -->
-                                <StackPanel Grid.Row="0" Grid.ColumnSpan="4" Orientation="Horizontal"
-                                            Spacing="20" HorizontalAlignment="Center" Margin="0,0,0,8">
-                                    <StackPanel>
-                                        <controls:WaterTankControl Width="110" Height="130"
-                                                                   WaterLevel="{Binding Tank1Level}" Text="{Binding Tank1Text}"
-                                                                   WaterColor="{DynamicResource TankInletBrush}" BorderColor="{DynamicResource BorderBrush}"/>
-                                        <Border Background="{DynamicResource BorderBrush}" Padding="6,2" Margin="0,2,0,0">
-                                            <TextBlock Text="{Binding Tank1Level, StringFormat='LV: {0:F1}%'}"
-                                                       FontFamily="Consolas, monospace" FontSize="11"
-                                                       Foreground="{DynamicResource SuccessBrush}" HorizontalAlignment="Center"/>
-                                        </Border>
-                                    </StackPanel>
-                                    <TextBlock Text="鈫" FontSize="24" Foreground="{DynamicResource SuccessBrush}" VerticalAlignment="Center"/>
-                                    <StackPanel>
-                                        <controls:WaterTankControl Width="110" Height="130"
-                                                                   WaterLevel="{Binding Tank2Level}" Text="{Binding Tank2Text}"
-                                                                   WaterColor="{DynamicResource TankBioBrush}" BorderColor="{DynamicResource BorderBrush}"/>
-                                        <Border Background="{DynamicResource BorderBrush}" Padding="6,2" Margin="0,2,0,0">
-                                            <TextBlock Text="{Binding Tank2Level, StringFormat='LV: {0:F1}%'}"
-                                                       FontFamily="Consolas, monospace" FontSize="11"
-                                                       Foreground="{DynamicResource TankBioBrush}" HorizontalAlignment="Center"/>
-                                        </Border>
-                                    </StackPanel>
-                                    <TextBlock Text="鈫" FontSize="24" Foreground="{DynamicResource SuccessBrush}" VerticalAlignment="Center"/>
-                                    <StackPanel>
-                                        <controls:WaterTankControl Width="110" Height="130"
-                                                                   WaterLevel="{Binding Tank3Level}" Text="{Binding Tank3Text}"
-                                                                   WaterColor="{DynamicResource TankSedimentBrush}" BorderColor="{DynamicResource BorderBrush}"/>
-                                        <Border Background="{DynamicResource BorderBrush}" Padding="6,2" Margin="0,2,0,0">
-                                            <TextBlock Text="{Binding Tank3Level, StringFormat='LV: {0:F1}%'}"
-                                                       FontFamily="Consolas, monospace" FontSize="11"
-                                                       Foreground="{DynamicResource TankSedimentBrush}" HorizontalAlignment="Center"/>
-                                        </Border>
-                                    </StackPanel>
-                                    <TextBlock Text="鈫" FontSize="24" Foreground="{DynamicResource SuccessBrush}" VerticalAlignment="Center"/>
-                                    <StackPanel>
-                                        <controls:WaterTankControl Width="110" Height="130"
-                                                                   WaterLevel="{Binding Tank4Level}" Text="{Binding Tank4Text}"
-                                                                   WaterColor="{DynamicResource TankOutletBrush}" BorderColor="{DynamicResource BorderBrush}"/>
-                                        <Border Background="{DynamicResource BorderBrush}" Padding="6,2" Margin="0,2,0,0">
-                                            <TextBlock Text="{Binding Tank4Level, StringFormat='LV: {0:F1}%'}"
-                                                       FontFamily="Consolas, monospace" FontSize="11"
-                                                       Foreground="{DynamicResource TankOutletBrush}" HorizontalAlignment="Center"/>
-                                        </Border>
-                                    </StackPanel>
-                                </StackPanel>
-
-                                <!-- 绗簩琛岋細绠¢亾 -->
-                                <Border Grid.Row="1" Grid.ColumnSpan="4" Margin="0,0,0,4">
-                                    <controls:PipeLineControl Height="18"
-                                                              IsFlow="{Binding IsInflowRunning}" IsHorizontal="True"
-                                                              PipeColor="{DynamicResource BorderBrush}" WaterColor="{DynamicResource TankInletBrush}"/>
-                                </Border>
-
-                                <!-- 绗笁琛岋細闃闂 -->
-                                <StackPanel Grid.Row="2" Grid.ColumnSpan="4" Orientation="Horizontal"
-                                            Spacing="40" HorizontalAlignment="Center" Margin="0,0,0,8">
-                                    <controls:ValveControl Width="36" Height="44"
-                                                           Status="{Binding Valve1Status}" Text="V1"
-                                                           ValveColor="{DynamicResource WarningBrush}" BackgroundColor="{DynamicResource BorderBrush}"/>
-                                    <controls:ValveControl Width="36" Height="44"
-                                                           Status="{Binding Valve2Status}" Text="V2"
-                                                           ValveColor="{DynamicResource WarningBrush}" BackgroundColor="{DynamicResource BorderBrush}"/>
-                                    <controls:ValveControl Width="36" Height="44"
-                                                           Status="{Binding Valve3Status}" Text="V3"
-                                                           ValveColor="{DynamicResource WarningBrush}" BackgroundColor="{DynamicResource BorderBrush}"/>
-                                    <controls:ValveControl Width="36" Height="44"
-                                                           Status="{Binding Valve4Status}" Text="V4"
-                                                           ValveColor="{DynamicResource WarningBrush}" BackgroundColor="{DynamicResource BorderBrush}"/>
-                                </StackPanel>
-
-                                <!-- 绗洓琛岋細娉+椋庢満 -->
-                                <Grid Grid.Row="3" Grid.ColumnSpan="4" ColumnDefinitions="*,Auto">
-                                    <!-- 娉电粍 -->
-                                    <StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="16">
-                                        <StackPanel>
-                                            <controls:PumpControl Width="60" Height="68"
-                                                                  IsRunning="{Binding Pump1Running}" Frequency="50" Text="P1"/>
-                                            <TextBlock Text="{Binding Pump1Text}" FontFamily="Consolas, monospace" FontSize="10"
-                                                       Foreground="{DynamicResource TextTertiaryBrush}" HorizontalAlignment="Center" Margin="0,2,0,0"/>
-                                        </StackPanel>
-                                        <StackPanel>
-                                            <controls:PumpControl Width="60" Height="68"
-                                                                  IsRunning="{Binding Pump2Running}" Frequency="45" Text="P2"/>
-                                            <TextBlock Text="{Binding Pump2Text}" FontFamily="Consolas, monospace" FontSize="10"
-                                                       Foreground="{DynamicResource TextTertiaryBrush}" HorizontalAlignment="Center" Margin="0,2,0,0"/>
-                                        </StackPanel>
-                                        <StackPanel>
-                                            <controls:PumpControl Width="60" Height="68"
-                                                                  IsRunning="{Binding Pump3Running}" Frequency="40" Text="P3"/>
-                                            <TextBlock Text="{Binding Pump3Text}" FontFamily="Consolas, monospace" FontSize="10"
-                                                       Foreground="{DynamicResource TextTertiaryBrush}" HorizontalAlignment="Center" Margin="0,2,0,0"/>
-                                        </StackPanel>
-                                        <StackPanel>
-                                            <controls:PumpControl Width="60" Height="68"
-                                                                  IsRunning="{Binding Pump4Running}" Frequency="35" Text="P4"/>
-                                            <TextBlock Text="{Binding Pump4Text}" FontFamily="Consolas, monospace" FontSize="10"
-                                                       Foreground="{DynamicResource TextTertiaryBrush}" HorizontalAlignment="Center" Margin="0,2,0,0"/>
-                                        </StackPanel>
-                                        <StackPanel>
-                                            <controls:PumpControl Width="60" Height="68"
-                                                                  IsRunning="{Binding Pump5Running}" Frequency="55" Text="P5"/>
-                                            <TextBlock Text="{Binding Pump5Text}" FontFamily="Consolas, monospace" FontSize="10"
-                                                       Foreground="{DynamicResource TextTertiaryBrush}" HorizontalAlignment="Center" Margin="0,2,0,0"/>
-                                        </StackPanel>
-                                    </StackPanel>
-
-                                    <!-- 椋庢満缁 -->
-                                    <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="20" Margin="20,0,0,0">
-                                        <StackPanel>
-                                            <controls:FanControl Width="44" Height="44"
-                                                                  IsRunning="{Binding Fan1Running}" Speed="1" Text="F1"/>
-                                            <TextBlock Text="{Binding Fan1Text}" FontFamily="Consolas, monospace" FontSize="10"
-                                                       Foreground="{DynamicResource TextTertiaryBrush}" HorizontalAlignment="Center" Margin="0,2,0,0"/>
-                                        </StackPanel>
-                                        <StackPanel>
-                                            <controls:FanControl Width="44" Height="44"
-                                                                  IsRunning="{Binding Fan2Running}" Speed="1.5" Text="F2"/>
-                                            <TextBlock Text="{Binding Fan2Text}" FontFamily="Consolas, monospace" FontSize="10"
-                                                       Foreground="{DynamicResource TextTertiaryBrush}" HorizontalAlignment="Center" Margin="0,2,0,0"/>
-                                        </StackPanel>
-                                    </StackPanel>
-                                </Grid>
-
-                                <!-- 鍙充晶锛氭暟鎹潰鏉 -->
-                                <Grid Grid.Row="0" Grid.RowSpan="4" Grid.Column="4" RowDefinitions="Auto,Auto,Auto" Margin="8,0,0,0">
-                                    <!-- 妯″紡 -->
-                                    <Border Grid.Row="0" Background="{DynamicResource SurfaceBgBrush}" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"
-                                            CornerRadius="2" Padding="8" Margin="0,0,0,6">
-                                        <StackPanel HorizontalAlignment="Center" Spacing="4">
-                                            <TextBlock Text="{Binding ModeText}" FontFamily="Consolas, monospace"
-                                                       FontSize="10" Foreground="{DynamicResource TextTertiaryBrush}"/>
-                                            <controls:ValveControl Width="36" Height="36"
-                                                                   Status="{Binding ModeStatus}" Text="{Binding ModeText}"
-                                                                   ValveColor="{DynamicResource SuccessBrush}" BackgroundColor="{DynamicResource BorderBrush}"/>
-                                        </StackPanel>
-                                    </Border>
-
-                                    <!-- 瀹炴椂鏁版嵁 -->
-                                    <Border Grid.Row="1" Background="{DynamicResource SurfaceBgBrush}" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"
-                                            CornerRadius="2" Padding="8" Margin="0,0,0,6">
-                                        <StackPanel Spacing="6">
-                                            <TextBlock Text="{Binding RealtimeText}" FontFamily="Consolas, monospace"
-                                                       FontSize="10" Foreground="{DynamicResource TextTertiaryBrush}"/>
-                                            <StackPanel Spacing="1">
-                                                <TextBlock Text="{Binding InflowText}" FontFamily="Consolas, monospace" FontSize="10" Foreground="{DynamicResource TextDisabledBrush}"/>
-                                                <TextBlock FontFamily="Consolas, monospace" FontSize="15" FontWeight="Bold" Foreground="{DynamicResource SuccessBrush}">
-                                                    <Run Text="{Binding InflowRate, StringFormat='{}{0:F1}'}"/>
-                                                    <Run Text=" m鲁/h" FontSize="10" Foreground="{DynamicResource TextTertiaryBrush}"/>
-                                                </TextBlock>
-                                            </StackPanel>
-                                            <Border Background="{DynamicResource BorderBrush}" Height="1"/>
-                                            <StackPanel Spacing="1">
-                                                <TextBlock Text="{Binding OutflowText}" FontFamily="Consolas, monospace" FontSize="10" Foreground="{DynamicResource TextDisabledBrush}"/>
-                                                <TextBlock FontFamily="Consolas, monospace" FontSize="15" FontWeight="Bold" Foreground="{DynamicResource TankOutletBrush}">
-                                                    <Run Text="{Binding OutflowRate, StringFormat='{}{0:F1}'}"/>
-                                                    <Run Text=" m鲁/h" FontSize="10" Foreground="{DynamicResource TextTertiaryBrush}"/>
-                                                </TextBlock>
-                                            </StackPanel>
-                                            <Border Background="{DynamicResource BorderBrush}" Height="1"/>
-                                            <StackPanel Spacing="1">
-                                                <TextBlock Text="{Binding DeltaText}" FontFamily="Consolas, monospace" FontSize="10" Foreground="{DynamicResource TextDisabledBrush}"/>
-                                                <TextBlock FontFamily="Consolas, monospace" FontSize="13" Foreground="{DynamicResource WarningBrush}">
-                                                    <Run Text="{Binding FlowDelta, StringFormat='{}{0:F1}'}"/>
-                                                    <Run Text=" m鲁/h" FontSize="10" Foreground="{DynamicResource TextTertiaryBrush}"/>
-                                                </TextBlock>
-                                            </StackPanel>
-                                        </StackPanel>
-                                    </Border>
-
-                                    <!-- 缁熻 -->
-                                    <Border Grid.Row="2" Background="{DynamicResource SurfaceBgBrush}" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"
-                                            CornerRadius="2" Padding="8">
-                                        <StackPanel Spacing="5">
-                                            <TextBlock Text="{Binding StatisticsText}" FontFamily="Consolas, monospace"
-                                                       FontSize="10" Foreground="{DynamicResource TextTertiaryBrush}"/>
-                                            <Grid ColumnDefinitions="*,Auto">
-                                                <TextBlock Grid.Column="0" Text="{Binding RuntimeText}" FontFamily="Consolas, monospace" FontSize="10" Foreground="{DynamicResource TextDisabledBrush}"/>
-                                                <TextBlock Grid.Column="1" Text="{Binding RunningTime}" FontFamily="Consolas, monospace" FontSize="10" Foreground="{DynamicResource TextSecondaryBrush}"/>
-                                            </Grid>
-                                            <Grid ColumnDefinitions="*,Auto">
-                                                <TextBlock Grid.Column="0" Text="{Binding DevicesText}" FontFamily="Consolas, monospace" FontSize="10" Foreground="{DynamicResource TextDisabledBrush}"/>
-                                                <TextBlock Grid.Column="1" Text="{Binding RunningDeviceCount, StringFormat='{}{0} ON'}" FontFamily="Consolas, monospace" FontSize="10" Foreground="{DynamicResource SuccessBrush}"/>
-                                            </Grid>
-                                            <Grid ColumnDefinitions="*,Auto">
-                                                <TextBlock Grid.Column="0" Text="{Binding AlarmText}" FontFamily="Consolas, monospace" FontSize="10" Foreground="{DynamicResource TextDisabledBrush}"/>
-                                                <TextBlock Grid.Column="1" Text="{Binding AlarmCount, StringFormat='{}{0}'}" FontFamily="Consolas, monospace" FontSize="10" Foreground="{DynamicResource DangerBrush}"/>
-                                            </Grid>
-                                            <Border Background="{DynamicResource BorderBrush}" Height="1"/>
-                                            <TextBlock Text="{Binding EfficiencyText}" FontFamily="Consolas, monospace" FontSize="10" Foreground="{DynamicResource TextDisabledBrush}"/>
-                                            <TextBlock FontFamily="Consolas, monospace" FontSize="16" FontWeight="Bold" Foreground="{DynamicResource SuccessBrush}">
-                                                <Run Text="{Binding Efficiency, StringFormat='{}{0:F0}'}"/>
-                                                <Run Text="%" FontSize="11" Foreground="{DynamicResource TextTertiaryBrush}"/>
-                                            </TextBlock>
-                                        </StackPanel>
-                                    </Border>
-                                </Grid>
+                        <!-- 3D 娉电珯鐩戞帶鍖哄煙 -->
+                        <Border Grid.Row="1" Background="{DynamicResource PanelBgBrush}" CornerRadius="5" Padding="12" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" ClipToBounds="True">
+                            <Grid>
+                                <controls:PumpStation2DControl
+                                    IsVisible="{Binding Is2DView}"
+                                    Tank1Level="{Binding Tank1Level}"
+                                    Tank2Level="{Binding Tank2Level}"
+                                    Tank3Level="{Binding Tank3Level}"
+                                    Tank4Level="{Binding Tank4Level}"
+                                    InflowRate="{Binding InflowRate}"
+                                    OutflowRate="{Binding OutflowRate}"
+                                    Pump1Running="{Binding Pump1Running}"
+                                    Pump2Running="{Binding Pump2Running}"
+                                    Pump3Running="{Binding Pump3Running}"
+                                    Pump4Running="{Binding Pump4Running}"
+                                    Pump5Running="{Binding Pump5Running}"
+                                    Fan1Running="{Binding Fan1Running}"
+                                    Fan2Running="{Binding Fan2Running}"
+                                    Valve1Status="{Binding Valve1Status}"
+                                    Valve2Status="{Binding Valve2Status}"
+                                    Valve3Status="{Binding Valve3Status}"
+                                    Valve4Status="{Binding Valve4Status}"
+                                    IsInflowRunning="{Binding IsInflowRunning}"
+                                    IsOutflowRunning="{Binding IsOutflowRunning}"
+                                    HasAlarm="{Binding HasAlarm}"
+                                    AlarmCount="{Binding AlarmCount}"
+                                    RunningDeviceCount="{Binding RunningDeviceCount}"
+                                    Efficiency="{Binding Efficiency}"/>
+                                <controls:PumpStation3DControl
+                                    IsVisible="{Binding Is3DView}"
+                                    Tank1Level="{Binding Tank1Level}"
+                                    Tank2Level="{Binding Tank2Level}"
+                                    Tank3Level="{Binding Tank3Level}"
+                                    Tank4Level="{Binding Tank4Level}"
+                                    InflowRate="{Binding InflowRate}"
+                                    OutflowRate="{Binding OutflowRate}"
+                                    Pump1Running="{Binding Pump1Running}"
+                                    Pump2Running="{Binding Pump2Running}"
+                                    Pump3Running="{Binding Pump3Running}"
+                                    Pump4Running="{Binding Pump4Running}"
+                                    Pump5Running="{Binding Pump5Running}"
+                                    Fan1Running="{Binding Fan1Running}"
+                                    Fan2Running="{Binding Fan2Running}"
+                                    Valve1Status="{Binding Valve1Status}"
+                                    Valve2Status="{Binding Valve2Status}"
+                                    Valve3Status="{Binding Valve3Status}"
+                                    Valve4Status="{Binding Valve4Status}"
+                                    IsInflowRunning="{Binding IsInflowRunning}"
+                                    IsOutflowRunning="{Binding IsOutflowRunning}"
+                                    HasAlarm="{Binding HasAlarm}"
+                                    AlarmCount="{Binding AlarmCount}"
+                                    RunningDeviceCount="{Binding RunningDeviceCount}"
+                                    Efficiency="{Binding Efficiency}"/>
                             </Grid>
                         </Border>
 
                         <!-- 搴曢儴璁惧鐘舵佹潯 -->
-                        <Border Grid.Row="2" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2" Margin="0,4,0,0" Padding="8,6">
+                        <Border Grid.Row="2" Background="{DynamicResource PanelBgBrush}" CornerRadius="5" Margin="0,10,0,0" Padding="12,8" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
                             <StackPanel Orientation="Horizontal" Spacing="12">
                                 <StackPanel Orientation="Horizontal" Spacing="4">
                                     <Border Width="8" Height="8" CornerRadius="4" Background="{Binding Pump1StatusColor}"/>
@@ -320,8 +174,7 @@
                 <!-- 鍙充晶闈㈡澘 -->
                 <Grid Grid.Column="1" RowDefinitions="Auto,Auto,*,Auto">
 
-                    <Border Grid.Row="0" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2" Padding="10,8" Margin="0,0,0,4"
-                            BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
+                    <Border Grid.Row="0" Classes="surface-card" Background="{DynamicResource SurfaceBgBrush}" Padding="14,12" Margin="0,0,0,8">
                         <StackPanel>
                             <StackPanel Orientation="Horizontal" Spacing="6" Margin="0,0,0,8">
                                 <Border Background="{DynamicResource SuccessBrush}" Width="3" Height="14" CornerRadius="1"/>
@@ -338,25 +191,25 @@
                         </StackPanel>
                     </Border>
 
-                    <Border Grid.Row="1" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2" Padding="10,8" Margin="0,0,0,4"
-                            BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
+                    <Border Grid.Row="1" Classes="surface-card" Background="{DynamicResource SurfaceBgBrush}" Padding="14,12" Margin="0,0,0,8">
                         <StackPanel>
                             <StackPanel Orientation="Horizontal" Spacing="6" Margin="0,0,0,8">
                                 <Border Background="{DynamicResource WarningBrush}" Width="3" Height="14" CornerRadius="1"/>
                                 <TextBlock Text="{Binding DeviceStatusText}" FontFamily="Consolas, monospace" FontSize="11" Foreground="{DynamicResource TextSecondaryBrush}"/>
                             </StackPanel>
                             <StackPanel Spacing="3">
-                                <controls:StatusCard Title="{Binding Pump1TitleText}" Status="{Binding Pump1Status}" Icon="P1" IsActive="{Binding Pump1Running}"/>
-                                <controls:StatusCard Title="{Binding Pump2TitleText}" Status="{Binding Pump2Status}" Icon="P2" IsActive="{Binding Pump2Running}"/>
-                                <controls:StatusCard Title="{Binding RefluxPumpText}" Status="{Binding Pump3Status}" Icon="P3" IsActive="{Binding Pump3Running}"/>
-                                <controls:StatusCard Title="{Binding Fan1TitleText}" Status="{Binding Fan1Status}" Icon="F1" IsActive="{Binding Fan1Running}"/>
-                                <controls:StatusCard Title="{Binding Fan2TitleText}" Status="{Binding Fan2Status}" Icon="F2" IsActive="{Binding Fan2Running}"/>
+                                <controls:StatusCard Title="1#鎻愬崌娉" Status="{Binding Pump1Status}" Icon="P1" IsActive="{Binding Pump1Running}"/>
+                                <controls:StatusCard Title="2#鎻愬崌娉" Status="{Binding Pump2Status}" Icon="P2" IsActive="{Binding Pump2Running}"/>
+                                <controls:StatusCard Title="3#鎻愬崌娉" Status="{Binding Pump3Status}" Icon="P3" IsActive="{Binding Pump3Running}"/>
+                                <controls:StatusCard Title="4#鎻愬崌娉" Status="{Binding Pump4Status}" Icon="P4" IsActive="{Binding Pump4Running}"/>
+                                <controls:StatusCard Title="5#鎻愬崌娉" Status="{Binding Pump5Status}" Icon="P5" IsActive="{Binding Pump5Running}"/>
+                                <controls:StatusCard Title="椋庢満1" Status="{Binding Fan1Status}" Icon="F1" IsActive="{Binding Fan1Running}"/>
+                                <controls:StatusCard Title="椋庢満2" Status="{Binding Fan2Status}" Icon="F2" IsActive="{Binding Fan2Running}"/>
                             </StackPanel>
                         </StackPanel>
                     </Border>
 
-                    <Border Grid.Row="2" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2" Padding="10,8" Margin="0,0,0,4"
-                            BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" IsVisible="{Binding HasAlarm}">
+                    <Border Grid.Row="2" Classes="surface-card" Background="{DynamicResource SurfaceBgBrush}" Padding="14,12" Margin="0,0,0,8" IsVisible="{Binding HasAlarm}">
                         <StackPanel>
                             <StackPanel Orientation="Horizontal" Spacing="6" Margin="0,0,0,8">
                                 <Border Background="{DynamicResource DangerBrush}" Width="3" Height="14" CornerRadius="1"/>
@@ -370,21 +223,20 @@
                         </StackPanel>
                     </Border>
 
-                    <Border Grid.Row="3" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2" Padding="10,8"
-                            BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
+                    <Border Grid.Row="3" Classes="surface-card" Background="{DynamicResource SurfaceBgBrush}" Padding="14,12">
                         <StackPanel>
                             <StackPanel Orientation="Horizontal" Spacing="6" Margin="0,0,0,8">
                                 <Border Background="{DynamicResource InfoBrush}" Width="3" Height="14" CornerRadius="1"/>
                                 <TextBlock Text="{Binding QuickOpsText}" FontFamily="Consolas, monospace" FontSize="11" Foreground="{DynamicResource TextSecondaryBrush}"/>
                             </StackPanel>
                             <WrapPanel HorizontalAlignment="Center">
-                                <Button Content="{Binding StartAllText}" Margin="3" Padding="10,5"
+                                <Button Content="{Binding StartAllText}" Command="{Binding StartAllCommand}" Margin="3" Padding="10,5"
                                         FontFamily="Consolas, monospace" FontSize="10"
                                         Background="{DynamicResource BorderBrush}" Foreground="{DynamicResource SuccessBrush}" BorderBrush="{DynamicResource SuccessBrush}"/>
-                                <Button Content="{Binding StopAllText}" Margin="3" Padding="10,5"
+                                <Button Content="{Binding StopAllText}" Command="{Binding StopAllCommand}" Margin="3" Padding="10,5"
                                         FontFamily="Consolas, monospace" FontSize="10"
                                         Background="{DynamicResource BorderBrush}" Foreground="{DynamicResource DangerBrush}" BorderBrush="{DynamicResource DangerBrush}"/>
-                                <Button Content="{Binding AckAlarmText}" Margin="3" Padding="10,5"
+                                <Button Content="{Binding AckAlarmText}" Command="{Binding AckAlarmCommand}" Margin="3" Padding="10,5"
                                         FontFamily="Consolas, monospace" FontSize="10"
                                         Background="{DynamicResource BorderBrush}" Foreground="{DynamicResource WarningBrush}" BorderBrush="{DynamicResource WarningBrush}"/>
                                 <Button Content="{Binding ExportText}" Margin="3" Padding="10,5"
@@ -398,7 +250,7 @@
 
             <!-- 鈺愨晲鈺 搴曢儴鐘舵佹爮 鈺愨晲鈺 -->
             <Border x:Name="StatusBar" Grid.Row="2" Background="{DynamicResource NavBgBrush}" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,1,0,0">
-                <Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto,Auto" Margin="16,0">
+                <Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto,Auto" Margin="20,0">
                     <TextBlock Grid.Column="0" VerticalAlignment="Center"
                                FontFamily="Consolas, monospace" FontSize="11" Foreground="{DynamicResource TextDisabledBrush}">
                         <Run Text="INFLOW: "/><Run Text="{Binding InflowRate, StringFormat='{}{0:F1}'}" Foreground="{DynamicResource SuccessBrush}"/><Run Text=" m鲁/h"/>

+ 4 - 4
src/YZWater.Avalonia/Views/ViewDView.axaml

@@ -29,7 +29,7 @@
                 <!-- 鏌ヨ鏉′欢 -->
                 <Border Grid.Row="0" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2"
                         BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" Padding="12" Margin="0,0,0,4">
-                    <StackPanel Orientation="Horizontal" Spacing="16">
+                    <WrapPanel Orientation="Horizontal" ItemSpacing="16" LineSpacing="8">
                         <StackPanel Orientation="Horizontal" Spacing="6">
                             <TextBlock Text="{Binding FromText}" FontFamily="{DynamicResource MonoFont}" FontSize="10" Foreground="{DynamicResource HeaderSubtextBrush}" VerticalAlignment="Center"/>
                             <DatePicker SelectedDate="{Binding StartDate}" Width="150"/>
@@ -42,11 +42,11 @@
                         <Button Content="{Binding ExportText}" Command="{Binding ExportAlarmsCommand}" Classes="btn-success"/>
                         <Button Content="{Binding AckAllText}" Command="{Binding ConfirmAllAlarmsCommand}" Classes="btn-warning"/>
                         <Button Content="{Binding PurgeText}" Command="{Binding ClearHistoryCommand}" Classes="btn-danger"/>
-                    </StackPanel>
+                    </WrapPanel>
                 </Border>
 
                 <!-- 鎶ヨ鍒楄〃 -->
-                <Border Grid.Row="0" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2"
+                <Border Grid.Row="1" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2"
                         BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" Padding="8" Margin="0,0,0,4">
                     <DataGrid ItemsSource="{Binding AlarmRecords}" AutoGenerateColumns="False" IsReadOnly="True"
                               SelectedItem="{Binding SelectedRecord}">
@@ -64,7 +64,7 @@
                 </Border>
 
                 <!-- 鎶ヨ璇︽儏 -->
-                <Border Grid.Row="1" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2"
+                <Border Grid.Row="2" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2"
                         BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" Padding="12"
                         IsVisible="{Binding SelectedRecord, Converter={x:Static ObjectConverters.IsNotNull}}">
                     <Grid ColumnDefinitions="80,*" RowDefinitions="Auto,Auto,Auto,Auto">

+ 1 - 1
src/YZWater.Avalonia/Views/ViewFView.axaml

@@ -29,7 +29,7 @@
                   GridLinesVisibility="Horizontal"
                   CanUserResizeColumns="True">
             <DataGrid.Columns>
-                <DataGridTextColumn Header="Time" Binding="{Binding Timestamp, StringFormat='yyyy-MM-dd HH:mm:ss'}" Width="160"/>
+                <DataGridTextColumn Header="Time" Binding="{Binding Timestamp, StringFormat='{}{0:yyyy-MM-dd HH:mm:ss}'}" Width="160"/>
                 <DataGridTextColumn Header="User" Binding="{Binding UserName}" Width="100"/>
                 <DataGridTextColumn Header="Action" Binding="{Binding Action}" Width="100"/>
                 <DataGridTextColumn Header="Detail" Binding="{Binding Detail}" Width="*"/>

+ 75 - 22
src/YZWater.Core/Services/AuthService.cs

@@ -1,4 +1,5 @@
 using Serilog;
+using System.Security.Cryptography;
 using YZWater.Core.Models;
 
 namespace YZWater.Core.Services;
@@ -8,6 +9,10 @@ namespace YZWater.Core.Services;
 /// </summary>
 public static class AuthService
 {
+    private const int Pbkdf2Iterations = 100_000;
+    private const int SaltSize = 16;
+    private const int HashSize = 32;
+
     private static User? _currentUser;
     private static DateTime _lastActivityTime = DateTime.Now;
 
@@ -59,13 +64,10 @@ public static class AuthService
                     CreatedTime = DateTime.Now
                 };
                 DatabaseService.Db.Insertable(defaultAdmin).ExecuteCommand();
-                Serilog.Log.Information("宸插垱寤洪粯璁ょ鐞嗗憳璐︽埛 (admin/admin123)");
+                Serilog.Log.Warning("宸插垱寤洪粯璁ょ鐞嗗憳璐︽埛 admin锛岃棣栨鐧诲綍鍚庣珛鍗充慨鏀归粯璁ゅ瘑鐮");
             }
             else
             {
-                // 纭繚绠$悊鍛樺瘑鐮佹纭紙闃叉鏃ф暟鎹簱鏍煎紡闂锛
-                ResetAdminPassword("admin123");
-
                 // 鏇存柊绠$悊鍛樻樉绀哄悕涓哄綋鍓嶈瑷
                 var currentDisplayName = LanguageService.Instance.Get("AdminName");
                 if (admin.DisplayName != currentDisplayName)
@@ -106,9 +108,6 @@ public static class AuthService
                 return (false, LanguageService.Instance.Get("AccountDisabled"));
             }
 
-            Serilog.Log.Debug("楠岃瘉瀵嗙爜: 鐢ㄦ埛={User}, 杈撳叆瀵嗙爜闀垮害={Len}, 瀛樺偍鍝堝笇闀垮害={HashLen}, 鍝堝笇鍓嶇紑={Prefix}",
-                userName, password.Length, user.PasswordHash?.Length ?? 0, user.PasswordHash?.Substring(0, Math.Min(10, user.PasswordHash?.Length ?? 0)));
-
             if (!VerifyPassword(password, user.PasswordHash))
             {
                 Serilog.Log.Warning("鐧诲綍澶辫触锛氱敤鎴 {UserName} 瀵嗙爜閿欒", userName);
@@ -119,6 +118,15 @@ public static class AuthService
             _currentUser = user;
             _lastActivityTime = DateTime.Now;
 
+            if (NeedsPasswordRehash(user.PasswordHash))
+            {
+                user.PasswordHash = HashPassword(password);
+                DatabaseService.Db.Updateable(user)
+                    .UpdateColumns(u => u.PasswordHash)
+                    .ExecuteCommand();
+                Log.Information("鐢ㄦ埛 {UserName} 瀵嗙爜鍝堝笇宸插崌绾", userName);
+            }
+
             // 鏇存柊鏈鍚庣櫥褰曟椂闂
             user.LastLoginTime = DateTime.Now;
             DatabaseService.Db.Updateable(user)
@@ -303,31 +311,77 @@ public static class AuthService
     }
 
     /// <summary>
-    /// 瀵嗙爜鍝堝笇锛圫HA256 + 闅忔満鐩锛屾牸寮: salt$hash锛
+    /// 瀵嗙爜鍝堝笇锛PBKDF2-SHA256锛屾牸寮: pbkdf2$iterations$salt$hash锛
     /// </summary>
     public static string HashPassword(string password)
     {
-        var saltBytes = new byte[16];
-        using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
-        {
-            rng.GetBytes(saltBytes);
-        }
+        var saltBytes = RandomNumberGenerator.GetBytes(SaltSize);
         var salt = Convert.ToBase64String(saltBytes);
-        var hash = ComputeSha256(password, salt);
-        return $"{salt}${hash}";
+        var hashBytes = Rfc2898DeriveBytes.Pbkdf2(
+            password,
+            saltBytes,
+            Pbkdf2Iterations,
+            HashAlgorithmName.SHA256,
+            HashSize);
+        var hash = Convert.ToBase64String(hashBytes);
+        return $"pbkdf2${Pbkdf2Iterations}${salt}${hash}";
     }
 
     /// <summary>
     /// 楠岃瘉瀵嗙爜
     /// </summary>
-    public static bool VerifyPassword(string password, string storedHash)
+    public static bool VerifyPassword(string password, string? storedHash)
     {
-        if (string.IsNullOrEmpty(storedHash) || !storedHash.Contains('$'))
+        if (string.IsNullOrWhiteSpace(storedHash) || !storedHash.Contains('$'))
         {
-            Serilog.Log.Warning("瀵嗙爜鍝堝笇鏍煎紡鏃犳晥: {Hash}", storedHash?.Substring(0, Math.Min(20, storedHash?.Length ?? 0)));
+            Serilog.Log.Warning("瀵嗙爜鍝堝笇鏍煎紡鏃犳晥");
             return false;
         }
 
+        if (storedHash.StartsWith("pbkdf2$", StringComparison.Ordinal))
+            return VerifyPbkdf2(password, storedHash);
+
+        return VerifyLegacySha256(password, storedHash);
+    }
+
+    private static bool NeedsPasswordRehash(string? storedHash)
+    {
+        if (string.IsNullOrWhiteSpace(storedHash)) return true;
+        if (!storedHash.StartsWith("pbkdf2$", StringComparison.Ordinal)) return true;
+
+        var parts = storedHash.Split('$');
+        return parts.Length != 4 ||
+               !int.TryParse(parts[1], out var iterations) ||
+               iterations < Pbkdf2Iterations;
+    }
+
+    private static bool VerifyPbkdf2(string password, string storedHash)
+    {
+        var parts = storedHash.Split('$');
+        if (parts.Length != 4 || !int.TryParse(parts[1], out var iterations)) return false;
+
+        try
+        {
+            var saltBytes = Convert.FromBase64String(parts[2]);
+            var expectedBytes = Convert.FromBase64String(parts[3]);
+            var actualBytes = Rfc2898DeriveBytes.Pbkdf2(
+                password,
+                saltBytes,
+                iterations,
+                HashAlgorithmName.SHA256,
+                expectedBytes.Length);
+
+            return CryptographicOperations.FixedTimeEquals(expectedBytes, actualBytes);
+        }
+        catch (FormatException)
+        {
+            Log.Warning("瀵嗙爜鍝堝笇鏍煎紡鏃犳晥");
+            return false;
+        }
+    }
+
+    private static bool VerifyLegacySha256(string password, string storedHash)
+    {
         var parts = storedHash.Split('$', 2);
         if (parts.Length != 2) return false;
 
@@ -335,10 +389,9 @@ public static class AuthService
         var expectedHash = parts[1];
         var actualHash = ComputeSha256(password, salt);
 
-        Serilog.Log.Debug("瀵嗙爜楠岃瘉: 鐩={Salt}, 鏈熸湜鍝堝笇={Expected}, 瀹為檯鍝堝笇={Actual}",
-            salt, expectedHash.Substring(0, Math.Min(10, expectedHash.Length)), actualHash.Substring(0, Math.Min(10, actualHash.Length)));
-
-        return expectedHash == actualHash;
+        return CryptographicOperations.FixedTimeEquals(
+            System.Text.Encoding.UTF8.GetBytes(expectedHash),
+            System.Text.Encoding.UTF8.GetBytes(actualHash));
     }
 
     private static string ComputeSha256(string password, string salt)

+ 23 - 3
src/YZWater.Core/Services/DataLoggingService.cs

@@ -10,6 +10,7 @@ public class DataLoggingService : IDisposable
 {
     private readonly PlcPollingService _pollingService;
     private CancellationTokenSource? _cts;
+    private Task? _loggingTask;
     private bool _disposed;
     private readonly object _flowLock = new();
 
@@ -86,8 +87,11 @@ public class DataLoggingService : IDisposable
     /// </summary>
     public void Start()
     {
+        if (_loggingTask is { IsCompleted: false })
+            return;
+
         _cts = new CancellationTokenSource();
-        Task.Run(() => LoggingLoopAsync(_cts.Token));
+        _loggingTask = Task.Run(() => LoggingLoopAsync(_cts.Token));
         Log.Information("鏁版嵁鏃ュ織鏈嶅姟宸插惎鍔");
     }
 
@@ -97,6 +101,16 @@ public class DataLoggingService : IDisposable
     public void Stop()
     {
         _cts?.Cancel();
+        try
+        {
+            _loggingTask?.Wait(TimeSpan.FromSeconds(2));
+        }
+        catch (AggregateException ex) when (ex.InnerExceptions.All(e => e is OperationCanceledException))
+        {
+            // 鍙栨秷鏄甯搁鍑鸿矾寰勩
+        }
+
+        _loggingTask = null;
         Log.Information("鏁版嵁鏃ュ織鏈嶅姟宸插仠姝");
     }
 
@@ -347,8 +361,12 @@ public class DataLoggingService : IDisposable
                 .Where(r => r.LastUpdateTime < cutoff)
                 .ExecuteCommandAsync();
 
-            Log.Information("鏁版嵁娓呯悊瀹屾垚: 鍒犻櫎 {Flow} 鏉℃祦閲忚褰, {Alarm} 鏉℃姤璀﹁褰, {Equip} 鏉¤澶囩姸鎬",
-                flowDeleted, alarmDeleted, equipDeleted);
+            var analogDeleted = await DatabaseService.Db.Deleteable<AnalogRecord>()
+                .Where(r => r.RecordTime < cutoff)
+                .ExecuteCommandAsync();
+
+            Log.Information("鏁版嵁娓呯悊瀹屾垚: 鍒犻櫎 {Flow} 鏉℃祦閲忚褰, {Alarm} 鏉℃姤璀﹁褰, {Equip} 鏉¤澶囩姸鎬, {Analog} 鏉℃ā鎷熼噺璁板綍",
+                flowDeleted, alarmDeleted, equipDeleted, analogDeleted);
         }
         catch (Exception ex)
         {
@@ -380,6 +398,8 @@ public class DataLoggingService : IDisposable
         if (_disposed) return;
         _disposed = true;
         Stop();
+        _cts?.Dispose();
+        _cts = null;
         Log.Information("鏁版嵁鏃ュ織鏈嶅姟宸查噴鏀");
     }
 }

+ 11 - 1
src/YZWater.Core/Services/HubClient.cs

@@ -13,6 +13,7 @@ public class HubClient : IDisposable
 {
     private ClientWebSocket? _ws;
     private CancellationTokenSource? _cts;
+    private Task? _connectionTask;
     private bool _disposed;
     private volatile bool _isConnected;
     private volatile bool _shouldReconnect = true;
@@ -62,7 +63,7 @@ public class HubClient : IDisposable
         await ConnectInternalAsync();
 
         // 鍚姩鎺ユ敹 + 閲嶈繛寰幆
-        _ = Task.Run(() => ConnectionLoopAsync(_cts.Token));
+        _connectionTask = Task.Run(() => ConnectionLoopAsync(_cts.Token));
     }
 
     /// <summary>
@@ -374,6 +375,15 @@ public class HubClient : IDisposable
         }
 
         _ws?.Dispose();
+        try
+        {
+            _connectionTask?.Wait(TimeSpan.FromSeconds(2));
+        }
+        catch (AggregateException ex) when (ex.InnerExceptions.All(e => e is OperationCanceledException))
+        {
+            // 鍙栨秷鏄甯搁鍑鸿矾寰勩
+        }
+        _connectionTask = null;
         _cts?.Dispose();
     }
 }

+ 19 - 3
src/YZWater.Core/Services/HubServer.cs

@@ -14,6 +14,7 @@ public class HubServer : IDisposable
 {
     private HttpListener? _listener;
     private CancellationTokenSource? _cts;
+    private Task? _acceptTask;
     private bool _disposed;
 
     // 宸茶繛鎺ョ殑瀹㈡埛绔
@@ -87,7 +88,7 @@ public class HubServer : IDisposable
         }
 
         IsRunning = true;
-        Task.Run(() => AcceptLoopAsync(_cts.Token));
+        _acceptTask = Task.Run(() => AcceptLoopAsync(_cts.Token));
         Log.Information("涓帶鏈嶅姟绔凡鍚姩锛岀鍙: {Port}", Port);
     }
 
@@ -110,6 +111,20 @@ public class HubServer : IDisposable
         }
         _clients.Clear();
 
+        try
+        {
+            _acceptTask?.Wait(TimeSpan.FromSeconds(2));
+        }
+        catch (AggregateException ex) when (ex.InnerExceptions.All(e => e is OperationCanceledException))
+        {
+            // 鍙栨秷鏄甯搁鍑鸿矾寰勩
+        }
+        _acceptTask = null;
+        _listener?.Close();
+        _listener = null;
+        _cts?.Dispose();
+        _cts = null;
+
         IsRunning = false;
         Log.Information("涓帶鏈嶅姟绔凡鍋滄");
     }
@@ -161,9 +176,10 @@ public class HubServer : IDisposable
                     context.Response.Close();
                 }
             }
-            catch (Exception ex) when (!ct.IsCancellationRequested)
+            catch (Exception ex)
             {
-                Log.Error(ex, "鎺ュ彈杩炴帴寮傚父");
+                if (!ct.IsCancellationRequested)
+                    Log.Error(ex, "鎺ュ彈杩炴帴寮傚父");
             }
         }
     }

+ 49 - 24
src/YZWater.Core/ViewModels/MainViewModel.cs

@@ -1,6 +1,7 @@
 using CommunityToolkit.Mvvm.ComponentModel;
 using CommunityToolkit.Mvvm.Input;
 using Serilog;
+using System.ComponentModel;
 using YZWater.Core.Models;
 using YZWater.Core.Services;
 
@@ -115,35 +116,58 @@ public partial class MainViewModel : ObservableObject, IDisposable
         RefreshUserInfo();
         LoadRunMode();
 
-        _langService.LanguageChanged += () =>
+        _langService.LanguageChanged += OnLanguageChanged;
+
+        _themeService.PropertyChanged += OnThemeServicePropertyChanged;
+
+        // PLC 杩炴帴鐘舵佺洃鍚
+        PlcPollingService.Instance.ConnectionStateChanged += OnConnectionStateChanged;
+
+        StartTimer();
+    }
+
+    private void OnLanguageChanged()
+    {
+        RunOnUi(() =>
         {
             IsChinese = _langService.IsChinese;
             UpdateAllTexts();
             RefreshUserInfo();
-        };
+        });
+    }
 
-        _themeService.PropertyChanged += (s, e) =>
+    private void OnThemeServicePropertyChanged(object? sender, PropertyChangedEventArgs e)
+    {
+        if (e.PropertyName != nameof(ThemeService.IsDarkTheme)) return;
+
+        RunOnUi(() =>
         {
-            if (e.PropertyName == nameof(ThemeService.IsDarkTheme))
-            {
-                IsDarkTheme = _themeService.IsDarkTheme;
-                ThemeIcon = IsDarkTheme ? "馃寵" : "鈽锔";
-            }
-        };
+            IsDarkTheme = _themeService.IsDarkTheme;
+            ThemeIcon = IsDarkTheme ? "馃寵" : "鈽锔";
+        });
+    }
 
-        // PLC 杩炴帴鐘舵佺洃鍚
-        PlcPollingService.Instance.ConnectionStateChanged += connected =>
+    private void OnConnectionStateChanged(bool connected)
+    {
+        RunOnUi(() =>
         {
-            if (_uiContext != null)
-                _uiContext.Post(_ =>
-                {
-                    PlcStatusText = connected ? _langService.Get("PlcConnectedStatus") : _langService.Get("PlcDisconnected");
-                    PlcStatusColor = connected ? "#27AE60" : "#E74C3C";
-                    IsPlcConnected = connected;
-                }, null);
-        };
+            PlcStatusText = connected ? _langService.Get("PlcConnectedStatus") : _langService.Get("PlcDisconnected");
+            PlcStatusColor = connected ? "#27AE60" : "#E74C3C";
+            IsPlcConnected = connected;
+        });
+    }
 
-        StartTimer();
+    private void RunOnUi(Action action)
+    {
+        if (_disposed) return;
+
+        if (_uiContext != null && SynchronizationContext.Current != _uiContext)
+            _uiContext.Post(_ =>
+            {
+                if (!_disposed) action();
+            }, null);
+        else
+            action();
     }
 
     private void UpdateAllTexts()
@@ -239,10 +263,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
         _clockTimer.Elapsed += (s, e) =>
         {
             var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
-            if (_uiContext != null)
-                _uiContext.Post(_ => CurrentTime = time, null);
-            else
-                CurrentTime = time;
+            RunOnUi(() => CurrentTime = time);
         };
         _clockTimer.Start();
     }
@@ -349,5 +370,9 @@ public partial class MainViewModel : ObservableObject, IDisposable
         if (_disposed) return;
         _disposed = true;
         StopTimer();
+        _langService.LanguageChanged -= OnLanguageChanged;
+        _themeService.PropertyChanged -= OnThemeServicePropertyChanged;
+        PlcPollingService.Instance.ConnectionStateChanged -= OnConnectionStateChanged;
+        _viewAViewModel.Dispose();
     }
 }

+ 110 - 13
src/YZWater.Core/ViewModels/ViewAViewModel.cs

@@ -9,10 +9,13 @@ namespace YZWater.Core.ViewModels;
 /// <summary>
 /// 涓诲伐鑹鸿鍥 ViewModel锛堟帴鍏ョ湡瀹 PLC 鏁版嵁锛
 /// </summary>
-public partial class ViewAViewModel : ObservableObject
+public partial class ViewAViewModel : ObservableObject, IDisposable
 {
     private readonly LanguageService _lang = LanguageService.Instance;
+    private readonly SynchronizationContext? _uiContext = SynchronizationContext.Current;
     private DateTime _startTime = DateTime.Now;
+    private System.Timers.Timer? _clockTimer;
+    private bool _disposed;
 
     // 鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺
     //  姘寸娑蹭綅
@@ -153,10 +156,16 @@ public partial class ViewAViewModel : ObservableObject
     [ObservableProperty] private string _fan1TitleText;
     [ObservableProperty] private string _fan2TitleText;
 
+    [ObservableProperty]
+    [NotifyPropertyChangedFor(nameof(Is2DView))]
+    private bool _is3DView = true;
+
+    public bool Is2DView => !Is3DView;
+
     public ViewAViewModel()
     {
         UpdateTexts();
-        _lang.LanguageChanged += UpdateTexts;
+        _lang.LanguageChanged += OnLanguageChanged;
 
         // 璁㈤槄 PLC 杞鏁版嵁
         PlcPollingService.Instance.DataUpdated += OnDataUpdated;
@@ -169,6 +178,21 @@ public partial class ViewAViewModel : ObservableObject
         StartClockTimer();
     }
 
+    private void OnLanguageChanged() => RunOnUi(UpdateTexts);
+
+    private void RunOnUi(Action action)
+    {
+        if (_disposed) return;
+
+        if (_uiContext != null && SynchronizationContext.Current != _uiContext)
+            _uiContext.Post(_ =>
+            {
+                if (!_disposed) action();
+            }, null);
+        else
+            action();
+    }
+
     private void UpdateTexts()
     {
         TitleText = _lang.Get("AppName");
@@ -219,20 +243,29 @@ public partial class ViewAViewModel : ObservableObject
 
     private void StartClockTimer()
     {
-        var timer = new System.Timers.Timer(1000);
-        timer.Elapsed += (s, e) =>
+        _clockTimer = new System.Timers.Timer(1000);
+        _clockTimer.Elapsed += (s, e) =>
         {
-            CurrentTime = DateTime.Now;
-            var elapsed = DateTime.Now - _startTime;
-            RunningTime = $"{elapsed.Days}d {elapsed.Hours}h {elapsed.Minutes}m";
+            RunOnUi(() =>
+            {
+                CurrentTime = DateTime.Now;
+                var elapsed = DateTime.Now - _startTime;
+                RunningTime = $"{elapsed.Days}d {elapsed.Hours}h {elapsed.Minutes}m";
+            });
         };
-        timer.Start();
+        _clockTimer.Start();
     }
 
     /// <summary>
     /// PLC 鏁版嵁鏇存柊鍥炶皟
     /// </summary>
     private void OnDataUpdated(PlcDataModel data)
+    {
+        var snapshot = CreateSnapshot(data);
+        RunOnUi(() => ApplyData(snapshot));
+    }
+
+    private void ApplyData(PlcDataModel data)
     {
         // 鏇存柊姘寸娑蹭綅
         Tank1Level = data.Tank1Level;
@@ -278,7 +311,9 @@ public partial class ViewAViewModel : ObservableObject
 
         // 鏇存柊缁熻
         RunningDeviceCount = data.RunningPumpCount + data.RunningFanCount;
-        Efficiency = 85 + (data.InflowRate > 0 ? (data.OutflowRate / data.InflowRate * 100) : 0);
+        Efficiency = data.InflowRate > 0
+            ? Math.Clamp(data.OutflowRate / data.InflowRate * 100, 0, 100)
+            : 0;
 
         // 妫鏌ユ姤璀
         CheckAlarms(data);
@@ -289,7 +324,7 @@ public partial class ViewAViewModel : ObservableObject
     /// </summary>
     private void OnConnectionStateChanged(bool connected)
     {
-        IsPlcConnected = connected;
+        RunOnUi(() => IsPlcConnected = connected);
     }
 
     /// <summary>
@@ -297,11 +332,52 @@ public partial class ViewAViewModel : ObservableObject
     /// </summary>
     private void OnAlarmStateChanged()
     {
-        HasAlarm = AlarmService.Instance.HasActiveAlarm;
-        AlarmMessage = AlarmService.Instance.ActiveAlarmMessage;
-        AlarmCount = AlarmService.Instance.ActiveAlarmCount;
+        RunOnUi(() =>
+        {
+            HasAlarm = AlarmService.Instance.HasActiveAlarm;
+            AlarmMessage = AlarmService.Instance.ActiveAlarmMessage;
+            AlarmCount = AlarmService.Instance.ActiveAlarmCount;
+        });
     }
 
+    private static PlcDataModel CreateSnapshot(PlcDataModel data) => new()
+    {
+        Tank1Level = data.Tank1Level,
+        Tank2Level = data.Tank2Level,
+        Tank3Level = data.Tank3Level,
+        Tank4Level = data.Tank4Level,
+        InflowRate = data.InflowRate,
+        OutflowRate = data.OutflowRate,
+        FlowDelta = data.FlowDelta,
+        Pump1Running = data.Pump1Running,
+        Pump2Running = data.Pump2Running,
+        Pump3Running = data.Pump3Running,
+        Pump4Running = data.Pump4Running,
+        Pump5Running = data.Pump5Running,
+        Pump1Fault = data.Pump1Fault,
+        Pump2Fault = data.Pump2Fault,
+        Pump3Fault = data.Pump3Fault,
+        Pump4Fault = data.Pump4Fault,
+        Pump5Fault = data.Pump5Fault,
+        Pump1Freq = data.Pump1Freq,
+        Pump2Freq = data.Pump2Freq,
+        Pump3Freq = data.Pump3Freq,
+        Pump4Freq = data.Pump4Freq,
+        Pump5Freq = data.Pump5Freq,
+        Fan1Running = data.Fan1Running,
+        Fan2Running = data.Fan2Running,
+        Fan1Fault = data.Fan1Fault,
+        Fan2Fault = data.Fan2Fault,
+        Valve1Position = data.Valve1Position,
+        Valve2Position = data.Valve2Position,
+        Valve3Position = data.Valve3Position,
+        Valve4Position = data.Valve4Position,
+        IsAutoMode = data.IsAutoMode,
+        LastReadTime = data.LastReadTime,
+        IsDataValid = data.IsDataValid,
+        IsPlcConnected = data.IsPlcConnected
+    };
+
     /// <summary>
     /// 鏇存柊璁惧鐘舵
     /// </summary>
@@ -384,6 +460,12 @@ public partial class ViewAViewModel : ObservableObject
     [RelayCommand]
     private async Task RefreshDataAsync() => await PlcPollingService.Instance.RefreshNowAsync();
 
+    [RelayCommand]
+    private void Set2DView() => Is3DView = false;
+
+    [RelayCommand]
+    private void Set3DView() => Is3DView = true;
+
     /// <summary>
     /// 鎺у埗娉靛惎鍋
     /// </summary>
@@ -474,6 +556,21 @@ public partial class ViewAViewModel : ObservableObject
     {
         await AlarmService.Instance.AcknowledgeAllAsync();
     }
+
+    public void Dispose()
+    {
+        if (_disposed) return;
+        _disposed = true;
+
+        _clockTimer?.Stop();
+        _clockTimer?.Dispose();
+        _clockTimer = null;
+
+        _lang.LanguageChanged -= OnLanguageChanged;
+        PlcPollingService.Instance.DataUpdated -= OnDataUpdated;
+        PlcPollingService.Instance.ConnectionStateChanged -= OnConnectionStateChanged;
+        AlarmService.Instance.AlarmStateChanged -= OnAlarmStateChanged;
+    }
 }
 
 /// <summary>