Pārlūkot izejas kodu

feat: 多项目适配架构 - 支持通过 JSON 项目档案切换不同泵站配置

- 新增 ProjectProfile 模型和 ProjectService 项目管理服务
- PlcDataModel 增加动态寄存器支持项目化数据
- PlcPollingService 支持根据项目配置动态读取 PLC
- 新增 ProjectViewModel + ProjectView 混合方案视图
- 扬杰二厂泵站点位配置 (projects/yangjie2.json)
- 向后兼容: ActiveProject 为空时走原有默认布局
DESKTOP-74CLTRG\Leol 1 mēnesi atpakaļ
vecāks
revīzija
b4976547fd

+ 17 - 0
YZWater3.sln

@@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YZWater.Core", "src\YZWater
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YZWater.Avalonia", "src\YZWater.Avalonia\YZWater.Avalonia.csproj", "{A8DAD3B1-F889-43F1-814B-C56911ED84D4}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YZWater.Core.Tests", "tests\YZWater.Core.Tests\YZWater.Core.Tests.csproj", "{68FC1731-B0C7-4D1C-81EA-F8A86E5010CE}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -43,6 +47,18 @@ Global
 		{A8DAD3B1-F889-43F1-814B-C56911ED84D4}.Release|x64.Build.0 = Release|Any CPU
 		{A8DAD3B1-F889-43F1-814B-C56911ED84D4}.Release|x86.ActiveCfg = Release|Any CPU
 		{A8DAD3B1-F889-43F1-814B-C56911ED84D4}.Release|x86.Build.0 = Release|Any CPU
+		{68FC1731-B0C7-4D1C-81EA-F8A86E5010CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{68FC1731-B0C7-4D1C-81EA-F8A86E5010CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{68FC1731-B0C7-4D1C-81EA-F8A86E5010CE}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{68FC1731-B0C7-4D1C-81EA-F8A86E5010CE}.Debug|x64.Build.0 = Debug|Any CPU
+		{68FC1731-B0C7-4D1C-81EA-F8A86E5010CE}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{68FC1731-B0C7-4D1C-81EA-F8A86E5010CE}.Debug|x86.Build.0 = Debug|Any CPU
+		{68FC1731-B0C7-4D1C-81EA-F8A86E5010CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{68FC1731-B0C7-4D1C-81EA-F8A86E5010CE}.Release|Any CPU.Build.0 = Release|Any CPU
+		{68FC1731-B0C7-4D1C-81EA-F8A86E5010CE}.Release|x64.ActiveCfg = Release|Any CPU
+		{68FC1731-B0C7-4D1C-81EA-F8A86E5010CE}.Release|x64.Build.0 = Release|Any CPU
+		{68FC1731-B0C7-4D1C-81EA-F8A86E5010CE}.Release|x86.ActiveCfg = Release|Any CPU
+		{68FC1731-B0C7-4D1C-81EA-F8A86E5010CE}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -50,5 +66,6 @@ Global
 	GlobalSection(NestedProjects) = preSolution
 		{57C35C51-105D-4D70-8AF0-E3FA6FD89BB0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
 		{A8DAD3B1-F889-43F1-814B-C56911ED84D4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+		{68FC1731-B0C7-4D1C-81EA-F8A86E5010CE} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
 	EndGlobalSection
 EndGlobal

BIN
docs/old.xlsx


BIN
docs/扬杰二厂泵站点位表(1).xlsx


+ 122 - 0
projects/yangjie2.json

@@ -0,0 +1,122 @@
+{
+  "name": "扬杰二厂泵站",
+  "description": "3台排污泵 + 液位/压力/流量监控",
+  "viewTemplate": "PumpStation",
+  "pumps": [
+    {
+      "name": "1#泵",
+      "runAddress": "Q0.1",
+      "faultAddress": "I0.1",
+      "currentAddress": "VD50",
+      "autoStateAddress": "M2.6",
+      "dailyHoursAddress": "VD950",
+      "dailyMinutesAddress": "VD10",
+      "modeSelectAddress": "M1.0",
+      "startAddress": "M1.3",
+      "stopAddress": "M1.4",
+      "autoCommandAddress": "M2.2",
+      "manualCommandAddress": "M2.1",
+      "dailyResetAddress": "M19.4"
+    },
+    {
+      "name": "2#泵",
+      "runAddress": "Q0.2",
+      "faultAddress": "I0.2",
+      "currentAddress": "VD54",
+      "autoStateAddress": "M5.6",
+      "dailyHoursAddress": "VD954",
+      "dailyMinutesAddress": "VD18",
+      "modeSelectAddress": "M4.0",
+      "startAddress": "M4.3",
+      "stopAddress": "M4.4",
+      "autoCommandAddress": "M5.4",
+      "manualCommandAddress": "M5.3",
+      "dailyResetAddress": "M19.4"
+    },
+    {
+      "name": "3#泵",
+      "runAddress": "Q0.3",
+      "faultAddress": "I0.3",
+      "currentAddress": "VD58",
+      "autoStateAddress": "M8.6",
+      "dailyHoursAddress": "VD958",
+      "dailyMinutesAddress": "VD28",
+      "modeSelectAddress": "M7.0",
+      "startAddress": "M7.3",
+      "stopAddress": "M7.4",
+      "autoCommandAddress": "M8.4",
+      "manualCommandAddress": "M8.3",
+      "dailyResetAddress": "M19.4"
+    }
+  ],
+  "tanks": [
+    {
+      "name": "集水池",
+      "levelAddress": "VD62",
+      "startPump1LevelAddress": "VD500",
+      "startPump2LevelAddress": "VD504",
+      "startPump3LevelAddress": "VD508",
+      "stopLevelAddress": "VD528",
+      "protectionLevelAddress": "VD962"
+    }
+  ],
+  "gauges": [
+    {
+      "name": "压力",
+      "unit": "MPa",
+      "valueAddress": "VD6600",
+      "highSetAddress": "VD6704",
+      "lowSetAddress": "VD6700",
+      "minValue": 0,
+      "maxValue": 1.6
+    },
+    {
+      "name": "流量",
+      "unit": "m³/h",
+      "valueAddress": "VD6604",
+      "minValue": 0,
+      "maxValue": 200
+    }
+  ],
+  "sensors": [
+    {
+      "name": "日流量",
+      "unit": "m³",
+      "address": "VD884",
+      "dataType": "REAL",
+      "access": "R"
+    }
+  ],
+  "digitalInputs": [
+    {
+      "name": "泵1变频器故障",
+      "address": "I0.1",
+      "remark": "1-故障 0-正常"
+    },
+    {
+      "name": "泵2变频器故障",
+      "address": "I0.2",
+      "remark": "1-故障 0-正常"
+    },
+    {
+      "name": "泵3变频器故障",
+      "address": "I0.3",
+      "remark": "1-故障 0-正常"
+    }
+  ],
+  "digitalOutputs": [
+    {
+      "name": "日流量复位",
+      "address": "M19.3",
+      "remark": "点动复位"
+    },
+    {
+      "name": "复位按钮",
+      "address": "M30.0"
+    }
+  ],
+  "alarmThresholds": {
+    "pressureHigh": 1.2,
+    "pressureLow": 0.1
+  }
+}

+ 8 - 0
src/YZWater.Avalonia/App.axaml.cs

@@ -71,6 +71,14 @@ public partial class App : Application
         PlcConfig.LoadFromConfig();
         ThemeHelper.SetTheme(ThemeService.Instance.IsDarkTheme);
 
+        // 加载项目档案(如果配置了 ActiveProject)
+        ProjectService.EnsureProjectsDir();
+        var config = ConfigService.GetConfig();
+        if (!string.IsNullOrWhiteSpace(config?.ActiveProject))
+        {
+            ProjectService.LoadProject(config.ActiveProject);
+        }
+
         if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
         {
             desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;

+ 4 - 0
src/YZWater.Avalonia/Views/MainWindow.axaml

@@ -148,6 +148,10 @@
         <!-- ═══ 主内容区域 ═══ -->
         <Panel>
         <TabControl SelectedIndex="{Binding SelectedTabIndex}" Background="{DynamicResource AppBgBrush}">
+            <!-- 项目视图(当加载了项目档案时使用) -->
+            <TabItem Header="PROJECT" IsVisible="False" DataContext="{Binding ProjectView}">
+                <views:ProjectView/>
+            </TabItem>
             <TabItem Header="PROCESS" IsVisible="False" DataContext="{Binding ViewA}">
                 <views:ViewAView/>
             </TabItem>

+ 262 - 0
src/YZWater.Avalonia/Views/ProjectView.axaml

@@ -0,0 +1,262 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:vm="using:YZWater.Core.ViewModels"
+             xmlns:controls="using:YZWater.Avalonia.Controls"
+             xmlns:conv="using:YZWater.Avalonia.Converters"
+             x:Class="YZWater.Avalonia.Views.ProjectView"
+             x:DataType="vm:ProjectViewModel">
+
+    <Border x:Name="RootBorder" Background="{DynamicResource AppBgBrush}">
+        <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" Margin="20,0">
+                    <StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="12" VerticalAlignment="Center">
+                        <Border Background="{DynamicResource SuccessBrush}" Width="4" Height="28" CornerRadius="2"/>
+                        <TextBlock Text="{Binding Title}" FontSize="17" FontWeight="Bold"
+                                   Foreground="{DynamicResource HeaderTextBrush}" FontFamily="{DynamicResource MonoFont}"
+                                   VerticalAlignment="Center"/>
+                        <TextBlock Text="{Binding SubtitleText}" FontSize="12"
+                                   Foreground="{DynamicResource HeaderSubtextBrush}" VerticalAlignment="Center"/>
+                    </StackPanel>
+
+                    <StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
+                        <Border Background="{DynamicResource ToggleBgBrush}" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" CornerRadius="12" Padding="10,5">
+                            <StackPanel Orientation="Horizontal" Spacing="7">
+                                <Border Width="10" Height="10" CornerRadius="5" Background="{DynamicResource SuccessBrush}"/>
+                                <TextBlock Text="{Binding PlcStatusText}" FontFamily="{DynamicResource MonoFont}" FontSize="10"
+                                           Foreground="{DynamicResource SuccessBrush}" VerticalAlignment="Center"/>
+                            </StackPanel>
+                        </Border>
+                        <TextBlock Text="{Binding CurrentTime, StringFormat='{}{0:HH:mm:ss}'}"
+                                   FontFamily="Consolas, monospace" FontSize="16" FontWeight="Bold"
+                                   Foreground="{DynamicResource SuccessBrush}" VerticalAlignment="Center"/>
+                    </StackPanel>
+                </Grid>
+            </Border>
+
+            <!-- ═══ 主内容区 ═══ -->
+            <Grid Grid.Row="1" ColumnDefinitions="*,300" Margin="14,12">
+
+                <!-- 左侧:设备布局区 -->
+                <ScrollViewer Grid.Column="0" Margin="0,0,10,0">
+                    <StackPanel Spacing="10">
+
+                        <!-- 水箱/水池 -->
+                        <Border Classes="surface-card" Background="{DynamicResource SurfaceBgBrush}" Padding="14">
+                            <StackPanel>
+                                <StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,0,10">
+                                    <Border Background="{DynamicResource InfoBrush}" Width="3" Height="16" CornerRadius="1"/>
+                                    <TextBlock Text="液位监控" FontFamily="Consolas, monospace" FontSize="12"
+                                               FontWeight="Bold" Foreground="{DynamicResource TextSecondaryBrush}"/>
+                                </StackPanel>
+                                <ItemsControl ItemsSource="{Binding Tanks}">
+                                    <ItemsControl.ItemTemplate>
+                                        <DataTemplate x:DataType="vm:TankItemViewModel">
+                                            <Border Background="{DynamicResource PanelBgBrush}" CornerRadius="5"
+                                                    Padding="12" Margin="0,0,0,8" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
+                                                <Grid ColumnDefinitions="140,*">
+                                                    <controls:WaterTankControl Grid.Column="0" Width="120" Height="140"
+                                                        WaterLevel="{Binding Level}" Text="{Binding Name}"/>
+                                                    <StackPanel Grid.Column="1" Margin="14,0,0,0" Spacing="4" VerticalAlignment="Center">
+                                                        <Grid ColumnDefinitions="*,Auto">
+                                                            <TextBlock Text="即时液位" FontSize="11" Foreground="{DynamicResource TextTertiaryBrush}"/>
+                                                            <TextBlock Grid.Column="1" Text="{Binding Level, StringFormat='{}{0:F2} m'}"
+                                                                       FontFamily="Consolas, monospace" FontSize="14" FontWeight="Bold"
+                                                                       Foreground="{DynamicResource SuccessBrush}"/>
+                                                        </Grid>
+                                                        <Grid ColumnDefinitions="*,Auto">
+                                                            <TextBlock Text="启泵1液位" FontSize="10" Foreground="{DynamicResource TextTertiaryBrush}"/>
+                                                            <TextBlock Grid.Column="1" Text="{Binding StartPump1Level, StringFormat='{}{0:F2}'}"
+                                                                       FontFamily="Consolas, monospace" FontSize="11" Foreground="{DynamicResource TextSecondaryBrush}"/>
+                                                        </Grid>
+                                                        <Grid ColumnDefinitions="*,Auto">
+                                                            <TextBlock Text="启泵2液位" FontSize="10" Foreground="{DynamicResource TextTertiaryBrush}"/>
+                                                            <TextBlock Grid.Column="1" Text="{Binding StartPump2Level, StringFormat='{}{0:F2}'}"
+                                                                       FontFamily="Consolas, monospace" FontSize="11" Foreground="{DynamicResource TextSecondaryBrush}"/>
+                                                        </Grid>
+                                                        <Grid ColumnDefinitions="*,Auto">
+                                                            <TextBlock Text="启泵3液位" FontSize="10" Foreground="{DynamicResource TextTertiaryBrush}"/>
+                                                            <TextBlock Grid.Column="1" Text="{Binding StartPump3Level, StringFormat='{}{0:F2}'}"
+                                                                       FontFamily="Consolas, monospace" FontSize="11" Foreground="{DynamicResource TextSecondaryBrush}"/>
+                                                        </Grid>
+                                                        <Grid ColumnDefinitions="*,Auto">
+                                                            <TextBlock Text="停泵液位" FontSize="10" Foreground="{DynamicResource TextTertiaryBrush}"/>
+                                                            <TextBlock Grid.Column="1" Text="{Binding StopLevel, StringFormat='{}{0:F2}'}"
+                                                                       FontFamily="Consolas, monospace" FontSize="11" Foreground="{DynamicResource WarningBrush}"/>
+                                                        </Grid>
+                                                        <Grid ColumnDefinitions="*,Auto">
+                                                            <TextBlock Text="保护液位" FontSize="10" Foreground="{DynamicResource TextTertiaryBrush}"/>
+                                                            <TextBlock Grid.Column="1" Text="{Binding ProtectionLevel, StringFormat='{}{0:F2}'}"
+                                                                       FontFamily="Consolas, monospace" FontSize="11" Foreground="{DynamicResource DangerBrush}"/>
+                                                        </Grid>
+                                                    </StackPanel>
+                                                </Grid>
+                                            </Border>
+                                        </DataTemplate>
+                                    </ItemsControl.ItemTemplate>
+                                </ItemsControl>
+                            </StackPanel>
+                        </Border>
+
+                        <!-- 泵组 -->
+                        <Border Classes="surface-card" Background="{DynamicResource SurfaceBgBrush}" Padding="14">
+                            <StackPanel>
+                                <StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,0,10">
+                                    <Border Background="{DynamicResource SuccessBrush}" Width="3" Height="16" CornerRadius="1"/>
+                                    <TextBlock Text="排污泵组" FontFamily="Consolas, monospace" FontSize="12"
+                                               FontWeight="Bold" Foreground="{DynamicResource TextSecondaryBrush}"/>
+                                </StackPanel>
+                                <ItemsControl ItemsSource="{Binding Pumps}">
+                                    <ItemsControl.ItemsPanel>
+                                        <ItemsPanelTemplate>
+                                            <WrapPanel/>
+                                        </ItemsPanelTemplate>
+                                    </ItemsControl.ItemsPanel>
+                                    <ItemsControl.ItemTemplate>
+                                        <DataTemplate x:DataType="vm:PumpItemViewModel">
+                                            <Border Background="{DynamicResource PanelBgBrush}" CornerRadius="5"
+                                                    Width="200" Margin="4" Padding="12"
+                                                    BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
+                                                <StackPanel Spacing="6">
+                                                    <!-- 泵名称 + 状态指示灯 -->
+                                                    <Grid ColumnDefinitions="*,Auto">
+                                                        <TextBlock Text="{Binding Name}" FontSize="13" FontWeight="Bold"
+                                                                   Foreground="{DynamicResource TextPrimaryBrush}" VerticalAlignment="Center"/>
+                                                        <Border Grid.Column="1" Width="12" Height="12" CornerRadius="6"
+                                                                Background="{Binding IsRunning, Converter={x:Static conv:BoolConverters.ToBrush}}"/>
+                                                    </Grid>
+
+                                                    <!-- 泵信息 -->
+                                                    <Grid ColumnDefinitions="*,Auto">
+                                                        <TextBlock Text="电流" FontSize="10" Foreground="{DynamicResource TextTertiaryBrush}"/>
+                                                        <TextBlock Grid.Column="1" Text="{Binding Current, StringFormat='{}{0:F1} A'}"
+                                                                   FontFamily="Consolas, monospace" FontSize="12"
+                                                                   Foreground="{DynamicResource SuccessBrush}"/>
+                                                    </Grid>
+                                                    <Grid ColumnDefinitions="*,Auto">
+                                                        <TextBlock Text="故障" FontSize="10" Foreground="{DynamicResource TextTertiaryBrush}"/>
+                                                        <TextBlock Grid.Column="1" FontFamily="Consolas, monospace" FontSize="11"
+                                                                   Foreground="{DynamicResource DangerBrush}"
+                                                                   IsVisible="{Binding HasFault}" Text="故障!"/>
+                                                    </Grid>
+                                                    <Grid ColumnDefinitions="*,Auto">
+                                                        <TextBlock Text="日台时" FontSize="10" Foreground="{DynamicResource TextTertiaryBrush}"/>
+                                                        <TextBlock Grid.Column="1" Text="{Binding DailyHours, StringFormat='{}{0:F0} h'}"
+                                                                   FontFamily="Consolas, monospace" FontSize="11"
+                                                                   Foreground="{DynamicResource TextSecondaryBrush}"/>
+                                                    </Grid>
+
+                                                    <!-- 操作按钮 -->
+                                                    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6" Margin="0,4,0,0">
+                                                        <Button Content="启动" Command="{Binding StartCommand}" Padding="10,4" FontSize="10"
+                                                                Classes="btn-success"/>
+                                                        <Button Content="停止" Command="{Binding StopCommand}" Padding="10,4" FontSize="10"
+                                                                Classes="btn-danger"/>
+                                                    </StackPanel>
+                                                </StackPanel>
+                                            </Border>
+                                        </DataTemplate>
+                                    </ItemsControl.ItemTemplate>
+                                </ItemsControl>
+                            </StackPanel>
+                        </Border>
+                    </StackPanel>
+                </ScrollViewer>
+
+                <!-- 右侧面板 -->
+                <Grid Grid.Column="1" RowDefinitions="Auto,Auto,*">
+
+                    <!-- 仪表 -->
+                    <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 WarningBrush}" Width="3" Height="14" CornerRadius="1"/>
+                                <TextBlock Text="仪表监控" FontFamily="Consolas, monospace" FontSize="11" Foreground="{DynamicResource TextSecondaryBrush}"/>
+                            </StackPanel>
+                            <ItemsControl ItemsSource="{Binding Gauges}">
+                                <ItemsControl.ItemTemplate>
+                                    <DataTemplate x:DataType="vm:GaugeItemViewModel">
+                                        <controls:GaugeControl Width="130" Height="150" Margin="2"
+                                            Value="{Binding Value}" Title="{Binding Name}" Unit="{Binding Unit}"
+                                            MinValue="{Binding MinValue}" MaxValue="{Binding MaxValue}"
+                                            ValueColor="{DynamicResource SuccessBrush}"/>
+                                    </DataTemplate>
+                                </ItemsControl.ItemTemplate>
+                                <ItemsControl.ItemsPanel>
+                                    <ItemsPanelTemplate>
+                                        <WrapPanel HorizontalAlignment="Center"/>
+                                    </ItemsPanelTemplate>
+                                </ItemsControl.ItemsPanel>
+                            </ItemsControl>
+                        </StackPanel>
+                    </Border>
+
+                    <!-- 传感器 -->
+                    <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 InfoBrush}" Width="3" Height="14" CornerRadius="1"/>
+                                <TextBlock Text="累计数据" FontFamily="Consolas, monospace" FontSize="11" Foreground="{DynamicResource TextSecondaryBrush}"/>
+                            </StackPanel>
+                            <ItemsControl ItemsSource="{Binding Sensors}">
+                                <ItemsControl.ItemTemplate>
+                                    <DataTemplate x:DataType="vm:SensorItemViewModel">
+                                        <Grid ColumnDefinitions="*,Auto" Margin="0,4">
+                                            <TextBlock Text="{Binding Name}" FontSize="11" Foreground="{DynamicResource TextTertiaryBrush}" VerticalAlignment="Center"/>
+                                            <TextBlock Grid.Column="1" FontFamily="Consolas, monospace" FontSize="14" FontWeight="Bold"
+                                                       Foreground="{DynamicResource SuccessBrush}" VerticalAlignment="Center"
+                                                       Text="{Binding Value, StringFormat='{}{0:F1}'}"/>
+                                        </Grid>
+                                    </DataTemplate>
+                                </ItemsControl.ItemTemplate>
+                            </ItemsControl>
+                        </StackPanel>
+                    </Border>
+
+                    <!-- 故障信息 -->
+                    <Border Grid.Row="2" Classes="surface-card" Background="{DynamicResource SurfaceBgBrush}" Padding="14,12">
+                        <StackPanel>
+                            <StackPanel Orientation="Horizontal" Spacing="6" Margin="0,0,0,8">
+                                <Border Background="{DynamicResource DangerBrush}" Width="3" Height="14" CornerRadius="1"/>
+                                <TextBlock Text="故障状态" FontFamily="Consolas, monospace" FontSize="11" Foreground="{DynamicResource DangerBrush}"/>
+                            </StackPanel>
+                            <ItemsControl ItemsSource="{Binding Pumps}">
+                                <ItemsControl.ItemTemplate>
+                                    <DataTemplate x:DataType="vm:PumpItemViewModel">
+                                        <Grid ColumnDefinitions="*,Auto" Margin="0,3">
+                                            <TextBlock Text="{Binding Name}" FontSize="11" Foreground="{DynamicResource TextTertiaryBrush}"/>
+                                            <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4">
+                                                <Border Width="8" Height="8" CornerRadius="4" Background="{DynamicResource DangerBrush}"
+                                                        IsVisible="{Binding HasFault}"/>
+                                                <Border Width="8" Height="8" CornerRadius="4" Background="{DynamicResource SuccessBrush}"
+                                                        IsVisible="{Binding !HasFault}"/>
+                                            </StackPanel>
+                                        </Grid>
+                                    </DataTemplate>
+                                </ItemsControl.ItemTemplate>
+                            </ItemsControl>
+                        </StackPanel>
+                    </Border>
+                </Grid>
+            </Grid>
+
+            <!-- ═══ 底部状态栏 ═══ -->
+            <Border x:Name="StatusBar" Grid.Row="2" Background="{DynamicResource NavBgBrush}" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,1,0,0">
+                <Grid ColumnDefinitions="*,Auto,Auto" Margin="20,0">
+                    <TextBlock Grid.Column="0" VerticalAlignment="Center"
+                               FontFamily="Consolas, monospace" FontSize="11" Foreground="{DynamicResource TextDisabledBrush}">
+                        <Run Text="PROJECT: "/><Run Text="{Binding Title}" Foreground="{DynamicResource SuccessBrush}"/>
+                    </TextBlock>
+                    <TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="24,0"
+                               FontFamily="Consolas, monospace" FontSize="11" Foreground="{DynamicResource TextDisabledBrush}">
+                        <Run Text="PUMPS: "/><Run Text="{Binding Pumps.Count}" Foreground="{DynamicResource SuccessBrush}"/>
+                    </TextBlock>
+                    <TextBlock Grid.Column="2" VerticalAlignment="Center"
+                               FontFamily="Consolas, monospace" FontSize="11" Foreground="{DynamicResource TextDisabledBrush}" Text="v3.0.0"/>
+                </Grid>
+            </Border>
+        </Grid>
+    </Border>
+</UserControl>

+ 28 - 0
src/YZWater.Avalonia/Views/ProjectView.axaml.cs

@@ -0,0 +1,28 @@
+using Avalonia.Controls;
+using YZWater.Avalonia.Controls;
+
+namespace YZWater.Avalonia.Views;
+
+public partial class ProjectView : UserControl
+{
+    public ProjectView()
+    {
+        InitializeComponent();
+        Loaded += (_, _) =>
+        {
+            ApplyTheme();
+            ThemeHelper.ThemeChanged += ApplyTheme;
+        };
+        Unloaded += (_, _) => ThemeHelper.ThemeChanged -= ApplyTheme;
+    }
+
+    private void ApplyTheme()
+    {
+        if (this.FindControl<Border>("TitleBar") is Border titleBar)
+            titleBar.Background = ThemeHelper.HeaderBg;
+        if (this.FindControl<Border>("StatusBar") is Border statusBar)
+            statusBar.Background = ThemeHelper.NavBg;
+        if (this.FindControl<Border>("RootBorder") is Border root)
+            root.Background = ThemeHelper.AppBg;
+    }
+}

+ 6 - 6
src/YZWater.Avalonia/YZWater.Avalonia.csproj

@@ -13,14 +13,14 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Avalonia" Version="12.0.4" />
-    <PackageReference Include="Avalonia.Desktop" Version="12.0.4" />
-    <PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.4" />
-    <PackageReference Include="Avalonia.Fonts.Inter" Version="12.0.4" />
-    <PackageReference Include="Avalonia.Controls.DataGrid" Version="12.0.0" />
+    <PackageReference Include="Avalonia" Version="12.1.1" />
+    <PackageReference Include="Avalonia.Desktop" Version="12.1.1" />
+    <PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.1" />
+    <PackageReference Include="Avalonia.Fonts.Inter" Version="12.1.1" />
+    <PackageReference Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
     <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
     <PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.1.0-dev-798" />
-    <PackageReference Include="Semi.Avalonia" Version="12.0.3" />
+    <PackageReference Include="Semi.Avalonia" Version="12.1.0.1" />
   </ItemGroup>
 
   <ItemGroup>

+ 79 - 0
src/YZWater.Core/Models/PlcDataModel.cs

@@ -81,6 +81,85 @@ public partial class PlcDataModel : ObservableObject
     [ObservableProperty] private bool _isDataValid;
     [ObservableProperty] private bool _isPlcConnected;
 
+    // ═══════════════════════════════════════════════
+    //  动态寄存器(由 ProjectProfile 驱动)
+    // ═══════════════════════════════════════════════
+
+    /// <summary>模拟量动态寄存器(名称 → 值),由项目档案驱动</summary>
+    public Dictionary<string, float> AnalogRegisters { get; } = new();
+
+    /// <summary>数字量动态寄存器(名称 → 值),由项目档案驱动</summary>
+    public Dictionary<string, bool> DigitalRegisters { get; } = new();
+
+    /// <summary>
+    /// 初始化动态寄存器(根据项目档案创建空条目)
+    /// </summary>
+    public void InitializeDynamicRegisters(ProjectProfile profile)
+    {
+        AnalogRegisters.Clear();
+        DigitalRegisters.Clear();
+
+        // 泵相关
+        foreach (var pump in profile.Pumps)
+        {
+            if (!string.IsNullOrEmpty(pump.CurrentAddress))
+                AnalogRegisters[$"{pump.Name}.Current"] = 0;
+            if (!string.IsNullOrEmpty(pump.RunAddress))
+                DigitalRegisters[$"{pump.Name}.Run"] = false;
+            if (!string.IsNullOrEmpty(pump.FaultAddress))
+                DigitalRegisters[$"{pump.Name}.Fault"] = false;
+            if (!string.IsNullOrEmpty(pump.AutoStateAddress))
+                DigitalRegisters[$"{pump.Name}.AutoMode"] = false;
+            if (!string.IsNullOrEmpty(pump.DailyHoursAddress))
+                AnalogRegisters[$"{pump.Name}.DailyHours"] = 0;
+            if (!string.IsNullOrEmpty(pump.DailyMinutesAddress))
+                AnalogRegisters[$"{pump.Name}.DailyMinutes"] = 0;
+        }
+
+        // 水箱/水池
+        foreach (var tank in profile.Tanks)
+        {
+            if (!string.IsNullOrEmpty(tank.LevelAddress))
+                AnalogRegisters[$"{tank.Name}.Level"] = 0;
+            if (!string.IsNullOrEmpty(tank.StartPump1LevelAddress))
+                AnalogRegisters[$"{tank.Name}.StartPump1Level"] = 0;
+            if (!string.IsNullOrEmpty(tank.StartPump2LevelAddress))
+                AnalogRegisters[$"{tank.Name}.StartPump2Level"] = 0;
+            if (!string.IsNullOrEmpty(tank.StartPump3LevelAddress))
+                AnalogRegisters[$"{tank.Name}.StartPump3Level"] = 0;
+            if (!string.IsNullOrEmpty(tank.StopLevelAddress))
+                AnalogRegisters[$"{tank.Name}.StopLevel"] = 0;
+            if (!string.IsNullOrEmpty(tank.ProtectionLevelAddress))
+                AnalogRegisters[$"{tank.Name}.ProtectionLevel"] = 0;
+        }
+
+        // 仪表
+        foreach (var gauge in profile.Gauges)
+        {
+            if (!string.IsNullOrEmpty(gauge.ValueAddress))
+                AnalogRegisters[$"{gauge.Name}.Value"] = 0;
+            if (!string.IsNullOrEmpty(gauge.HighSetAddress))
+                AnalogRegisters[$"{gauge.Name}.HighSet"] = 0;
+            if (!string.IsNullOrEmpty(gauge.LowSetAddress))
+                AnalogRegisters[$"{gauge.Name}.LowSet"] = 0;
+        }
+
+        // 通用传感器
+        foreach (var sensor in profile.Sensors)
+        {
+            if (!string.IsNullOrEmpty(sensor.Address))
+                AnalogRegisters[$"{sensor.Name}.Value"] = 0;
+        }
+    }
+
+    /// <summary>获取动态模拟量寄存器值</summary>
+    public float GetAnalogRegister(string key, float defaultValue = 0)
+        => AnalogRegisters.TryGetValue(key, out var value) ? value : defaultValue;
+
+    /// <summary>获取动态数字量寄存器值</summary>
+    public bool GetDigitalRegister(string key, bool defaultValue = false)
+        => DigitalRegisters.TryGetValue(key, out var value) ? value : defaultValue;
+
     // ═══════════════════════════════════════════════
     //  计算属性
     // ═══════════════════════════════════════════════

+ 223 - 0
src/YZWater.Core/Models/ProjectProfile.cs

@@ -0,0 +1,223 @@
+namespace YZWater.Core.Models;
+
+/// <summary>
+/// 项目档案 - 定义一个项目的设备清单、PLC 地址映射和报警阈值
+/// 从 projects/ 目录下的 JSON 文件加载
+/// </summary>
+public class ProjectProfile
+{
+    /// <summary>项目名称(如 "扬杰二厂泵站")</summary>
+    public string Name { get; set; } = "";
+
+    /// <summary>项目描述</summary>
+    public string Description { get; set; } = "";
+
+    /// <summary>
+    /// 视图模板名称(决定使用哪种 View 布局)
+    /// 如 "PumpStation" = 泵站布局, "Default" = 默认4水箱布局
+    /// </summary>
+    public string ViewTemplate { get; set; } = "Default";
+
+    /// <summary>泵设备列表</summary>
+    public List<PumpConfig> Pumps { get; set; } = new();
+
+    /// <summary>水箱/水池列表</summary>
+    public List<TankConfig> Tanks { get; set; } = new();
+
+    /// <summary>仪表列表(压力、流量等带刻度的模拟量)</summary>
+    public List<GaugeConfig> Gauges { get; set; } = new();
+
+    /// <summary>通用传感器(日流量、温度等简单模拟量)</summary>
+    public List<SensorConfig> Sensors { get; set; } = new();
+
+    /// <summary>通用数字量输入(变频器故障等)</summary>
+    public List<DigitalInputConfig> DigitalInputs { get; set; } = new();
+
+    /// <summary>通用数字量输出(复位按钮等)</summary>
+    public List<DigitalOutputConfig> DigitalOutputs { get; set; } = new();
+
+    /// <summary>报警阈值配置</summary>
+    public AlarmThresholds AlarmThresholds { get; set; } = new();
+}
+
+/// <summary>
+/// 泵设备配置 - 涵盖泵的读/写/状态点位
+/// </summary>
+public class PumpConfig
+{
+    /// <summary>泵名称(如 "1#泵")</summary>
+    public string Name { get; set; } = "";
+
+    // ─── 读取地址 ───
+
+    /// <summary>运行状态地址 (DO 区,如 Q0.1) - 起停共用地址时与 StartAddress 相同</summary>
+    public string RunAddress { get; set; } = "";
+
+    /// <summary>变频器故障地址 (DI 区,如 I0.1)</summary>
+    public string FaultAddress { get; set; } = "";
+
+    /// <summary>电流地址 (AI 区,如 AIW18 或对应的 VD 地址)</summary>
+    public string CurrentAddress { get; set; } = "";
+
+    /// <summary>手自动状态地址 (读,如 M2.6, 1=自动 0=手动)</summary>
+    public string AutoStateAddress { get; set; } = "";
+
+    /// <summary>日台时-小时地址 (读,如 VD950)</summary>
+    public string DailyHoursAddress { get; set; } = "";
+
+    /// <summary>日台时-分钟地址 (读,如 VD10)</summary>
+    public string DailyMinutesAddress { get; set; } = "";
+
+    /// <summary>中控/屏控选择地址 (读,如 M1.0, 0=中控 1=屏控)</summary>
+    public string ModeSelectAddress { get; set; } = "";
+
+    // ─── 写入地址 ───
+
+    /// <summary>手动启动地址 (写,如 M1.3, 点动)</summary>
+    public string StartAddress { get; set; } = "";
+
+    /// <summary>手动停止地址 (写,如 M1.4, 点动)</summary>
+    public string StopAddress { get; set; } = "";
+
+    /// <summary>自动命令地址 (写,如 M2.2, 点动)</summary>
+    public string AutoCommandAddress { get; set; } = "";
+
+    /// <summary>手动命令地址 (写,如 M2.1, 点动)</summary>
+    public string ManualCommandAddress { get; set; } = "";
+
+    /// <summary>日台时清零地址 (写,如 M19.4)</summary>
+    public string DailyResetAddress { get; set; } = "";
+}
+
+/// <summary>
+/// 水箱/水池配置
+/// </summary>
+public class TankConfig
+{
+    /// <summary>名称(如 "集水池")</summary>
+    public string Name { get; set; } = "";
+
+    /// <summary>即时液位地址 (读,如 VD62)</summary>
+    public string LevelAddress { get; set; } = "";
+
+    /// <summary>启动 1 台泵的液位设定值</summary>
+    public float? StartPump1Level { get; set; }
+
+    /// <summary>启动 2 台泵的液位设定值</summary>
+    public float? StartPump2Level { get; set; }
+
+    /// <summary>启动 3 台泵的液位设定值</summary>
+    public float? StartPump3Level { get; set; }
+
+    /// <summary>停泵液位设定值</summary>
+    public float? StopLevel { get; set; }
+
+    /// <summary>保护液位设定值</summary>
+    public float? ProtectionLevel { get; set; }
+
+    /// <summary>启动 1 台泵液位地址 (读写,如 VD500)</summary>
+    public string StartPump1LevelAddress { get; set; } = "";
+
+    /// <summary>启动 2 台泵液位地址 (读写,如 VD504)</summary>
+    public string StartPump2LevelAddress { get; set; } = "";
+
+    /// <summary>启动 3 台泵液位地址 (读写,如 VD508)</summary>
+    public string StartPump3LevelAddress { get; set; } = "";
+
+    /// <summary>停泵液位地址 (读写,如 VD528)</summary>
+    public string StopLevelAddress { get; set; } = "";
+
+    /// <summary>保护液位地址 (读写,如 VD962)</summary>
+    public string ProtectionLevelAddress { get; set; } = "";
+}
+
+/// <summary>
+/// 仪表配置(压力、流量等带刻度/上下限的模拟量)
+/// </summary>
+public class GaugeConfig
+{
+    /// <summary>名称(如 "压力"、"流量")</summary>
+    public string Name { get; set; } = "";
+
+    /// <summary>单位(如 "MPa"、"m³/h")</summary>
+    public string Unit { get; set; } = "";
+
+    /// <summary>即时值地址 (读,如 VD6600)</summary>
+    public string ValueAddress { get; set; } = "";
+
+    /// <summary>高压/上限设置地址 (读写,如 VD6704)</summary>
+    public string HighSetAddress { get; set; } = "";
+
+    /// <summary>低压/下限设置地址 (读写,如 VD6700)</summary>
+    public string LowSetAddress { get; set; } = "";
+
+    /// <summary>仪表最小刻度值</summary>
+    public double MinValue { get; set; }
+
+    /// <summary>仪表最大刻度值</summary>
+    public double MaxValue { get; set; } = 100;
+}
+
+/// <summary>
+/// 通用传感器配置(简单模拟量,如日流量)
+/// </summary>
+public class SensorConfig
+{
+    /// <summary>名称(如 "日流量")</summary>
+    public string Name { get; set; } = "";
+
+    /// <summary>单位(如 "m³")</summary>
+    public string Unit { get; set; } = "";
+
+    /// <summary>数据地址(如 VD884)</summary>
+    public string Address { get; set; } = "";
+
+    /// <summary>数据类型: "REAL" / "INT" / "DWORD"</summary>
+    public string DataType { get; set; } = "REAL";
+
+    /// <summary>访问权限: "R" (只读) / "RW" (读写)</summary>
+    public string Access { get; set; } = "R";
+}
+
+/// <summary>
+/// 通用数字量输入配置
+/// </summary>
+public class DigitalInputConfig
+{
+    /// <summary>名称(如 "泵1变频器故障")</summary>
+    public string Name { get; set; } = "";
+
+    /// <summary>地址(如 I0.1)</summary>
+    public string Address { get; set; } = "";
+
+    /// <summary>备注</summary>
+    public string Remark { get; set; } = "";
+}
+
+/// <summary>
+/// 通用数字量输出配置
+/// </summary>
+public class DigitalOutputConfig
+{
+    /// <summary>名称(如 "复位按钮")</summary>
+    public string Name { get; set; } = "";
+
+    /// <summary>地址(如 M30.0)</summary>
+    public string Address { get; set; } = "";
+
+    /// <summary>备注</summary>
+    public string Remark { get; set; } = "";
+}
+
+/// <summary>
+/// 报警阈值
+/// </summary>
+public class AlarmThresholds
+{
+    public float? LevelHigh { get; set; }
+    public float? LevelLow { get; set; }
+    public float? FlowHigh { get; set; }
+    public float? FlowLow { get; set; }
+    public float? PressureHigh { get; set; }
+    public float? PressureLow { get; set; }
+}

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

@@ -127,6 +127,13 @@ public partial class SystemConfig : ObservableObject
     [ObservableProperty]
     private string _savedUserName = string.Empty;
 
+    /// <summary>
+    /// 当前激活的项目名称(对应 projects/ 目录下的 JSON 文件名,不含后缀)
+    /// 空字符串表示使用内置默认项目配置
+    /// </summary>
+    [ObservableProperty]
+    private string _activeProject = string.Empty;
+
     /// <summary>
     /// PLC 地址覆盖(key=标签名如 Tank1Level, value=地址如 VD100)
     /// 空则使用默认地址

+ 111 - 1
src/YZWater.Core/Services/PlcPollingService.cs

@@ -169,9 +169,21 @@ public partial class PlcPollingService : ObservableObject, IDisposable
     }
 
     /// <summary>
-    /// 批量读取所有 PLC 数据(6 次批量读取替代 25 次单独读取)
+    /// 批量读取所有 PLC 数据
+    /// 根据是否加载了项目档案选择读取模式
     /// </summary>
     private async Task ReadAllDataAsync(CancellationToken ct)
+    {
+        if (ProjectService.Current != null)
+            await ReadAllDataFromProfile(ct);
+        else
+            await ReadAllDataDefault(ct);
+    }
+
+    /// <summary>
+    /// 默认模式:硬编码地址批量读取(向后兼容)
+    /// </summary>
+    private async Task ReadAllDataDefault(CancellationToken ct)
     {
         if (ct.IsCancellationRequested) return;
 
@@ -281,6 +293,104 @@ public partial class PlcPollingService : ObservableObject, IDisposable
         OnPropertyChanged(nameof(PlcDataModel.FlowDelta));
     }
 
+    /// <summary>
+    /// 项目档案模式:根据 ProjectProfile 动态读取 PLC 数据
+    /// </summary>
+    private async Task ReadAllDataFromProfile(CancellationToken ct)
+    {
+        if (ct.IsCancellationRequested) return;
+        var profile = ProjectService.Current;
+        if (profile == null) return;
+
+        // 收集所有需要读取的模拟量地址 (REAL/float)
+        var analogReads = new List<(string key, string addr)>();
+
+        foreach (var pump in profile.Pumps)
+        {
+            if (!string.IsNullOrEmpty(pump.CurrentAddress))
+                analogReads.Add(($"{pump.Name}.Current", pump.CurrentAddress));
+            if (!string.IsNullOrEmpty(pump.DailyHoursAddress))
+                analogReads.Add(($"{pump.Name}.DailyHours", pump.DailyHoursAddress));
+            if (!string.IsNullOrEmpty(pump.DailyMinutesAddress))
+                analogReads.Add(($"{pump.Name}.DailyMinutes", pump.DailyMinutesAddress));
+        }
+        foreach (var tank in profile.Tanks)
+        {
+            if (!string.IsNullOrEmpty(tank.LevelAddress))
+                analogReads.Add(($"{tank.Name}.Level", tank.LevelAddress));
+            if (!string.IsNullOrEmpty(tank.StartPump1LevelAddress))
+                analogReads.Add(($"{tank.Name}.StartPump1Level", tank.StartPump1LevelAddress));
+            if (!string.IsNullOrEmpty(tank.StartPump2LevelAddress))
+                analogReads.Add(($"{tank.Name}.StartPump2Level", tank.StartPump2LevelAddress));
+            if (!string.IsNullOrEmpty(tank.StartPump3LevelAddress))
+                analogReads.Add(($"{tank.Name}.StartPump3Level", tank.StartPump3LevelAddress));
+            if (!string.IsNullOrEmpty(tank.StopLevelAddress))
+                analogReads.Add(($"{tank.Name}.StopLevel", tank.StopLevelAddress));
+            if (!string.IsNullOrEmpty(tank.ProtectionLevelAddress))
+                analogReads.Add(($"{tank.Name}.ProtectionLevel", tank.ProtectionLevelAddress));
+        }
+        foreach (var gauge in profile.Gauges)
+        {
+            if (!string.IsNullOrEmpty(gauge.ValueAddress))
+                analogReads.Add(($"{gauge.Name}.Value", gauge.ValueAddress));
+            if (!string.IsNullOrEmpty(gauge.HighSetAddress))
+                analogReads.Add(($"{gauge.Name}.HighSet", gauge.HighSetAddress));
+            if (!string.IsNullOrEmpty(gauge.LowSetAddress))
+                analogReads.Add(($"{gauge.Name}.LowSet", gauge.LowSetAddress));
+        }
+        foreach (var sensor in profile.Sensors)
+        {
+            if (!string.IsNullOrEmpty(sensor.Address) && sensor.DataType == "REAL")
+                analogReads.Add(($"{sensor.Name}.Value", sensor.Address));
+        }
+
+        // 收集所有需要读取的数字量地址 (bool/bit)
+        var digitalReads = new List<(string key, string addr)>();
+
+        foreach (var pump in profile.Pumps)
+        {
+            if (!string.IsNullOrEmpty(pump.RunAddress))
+                digitalReads.Add(($"{pump.Name}.Run", pump.RunAddress));
+            if (!string.IsNullOrEmpty(pump.FaultAddress))
+                digitalReads.Add(($"{pump.Name}.Fault", pump.FaultAddress));
+            if (!string.IsNullOrEmpty(pump.AutoStateAddress))
+                digitalReads.Add(($"{pump.Name}.AutoMode", pump.AutoStateAddress));
+        }
+
+        // 并行发起所有读取
+        var analogTasks = analogReads.Select(r =>
+            PlcService.ReadFloatAsync(r.addr).ContinueWith(t => (r.key, Value: t.Result)));
+        var digitalTasks = digitalReads.Select(r =>
+            PlcService.ReadBoolAsync(r.addr).ContinueWith(t => (r.key, Value: t.Result)));
+
+        var allTasks = analogTasks.Cast<Task>().Concat(digitalTasks.Cast<Task>()).ToArray();
+        if (allTasks.Length > 0)
+            await Task.WhenAll(allTasks);
+
+        if (ct.IsCancellationRequested) return;
+
+        // 更新模拟量寄存器
+        foreach (var task in analogTasks)
+        {
+            var (key, value) = task.Result;
+            if (_data.AnalogRegisters.ContainsKey(key))
+            {
+                ApplyWithDeadband(_data.AnalogRegisters[key], value, v => _data.AnalogRegisters[key] = v);
+            }
+        }
+
+        // 更新数字量寄存器
+        foreach (var task in digitalTasks)
+        {
+            var (key, value) = task.Result;
+            _data.DigitalRegisters[key] = value;
+        }
+
+        _data.LastReadTime = DateTime.Now;
+        _data.IsDataValid = true;
+        _data.IsPlcConnected = PlcService.IsConnected;
+    }
+
     /// <summary>
     /// 死区过滤:只有变化超过阈值才更新
     /// </summary>

+ 127 - 0
src/YZWater.Core/Services/ProjectService.cs

@@ -0,0 +1,127 @@
+using System.Text.Json;
+using Serilog;
+using YZWater.Core.Models;
+
+namespace YZWater.Core.Services;
+
+/// <summary>
+/// 项目管理服务 - 加载和管理项目档案(ProjectProfile)
+/// 项目 JSON 文件存放在 projects/ 目录下
+/// </summary>
+public static class ProjectService
+{
+    private static readonly string ProjectsDir = "projects";
+    private static readonly JsonSerializerOptions JsonOptions = new()
+    {
+        WriteIndented = true,
+        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+        ReadCommentHandling = JsonCommentHandling.Skip,
+        AllowTrailingCommas = true
+    };
+
+    private static ProjectProfile? _current;
+
+    /// <summary>当前激活的项目档案(null = 使用内置默认项目)</summary>
+    public static ProjectProfile? Current => _current;
+
+    /// <summary>当前项目名称(空字符串表示使用默认项目)</summary>
+    public static string CurrentProjectName { get; private set; } = "";
+
+    /// <summary>
+    /// 加载指定项目
+    /// </summary>
+    /// <param name="projectName">项目名称(不含 .json 后缀)</param>
+    /// <returns>是否加载成功</returns>
+    public static bool LoadProject(string projectName)
+    {
+        if (string.IsNullOrWhiteSpace(projectName))
+        {
+            _current = null;
+            CurrentProjectName = "";
+            Log.Information("已清除项目档案,使用内置默认配置");
+            return true;
+        }
+
+        try
+        {
+            var path = GetProjectPath(projectName);
+            if (!File.Exists(path))
+            {
+                Log.Error("项目文件不存在: {Path}", path);
+                return false;
+            }
+
+            var json = File.ReadAllText(path);
+            var profile = JsonSerializer.Deserialize<ProjectProfile>(json, JsonOptions);
+            if (profile == null)
+            {
+                Log.Error("项目文件格式错误: {Path}", path);
+                return false;
+            }
+
+            _current = profile;
+            CurrentProjectName = projectName;
+            Log.Information("已加载项目档案: {Name} ({Desc}), 泵:{PumpCount} 水箱:{TankCount} 仪表:{GaugeCount}",
+                profile.Name, profile.Description,
+                profile.Pumps?.Count ?? 0,
+                profile.Tanks?.Count ?? 0,
+                profile.Gauges?.Count ?? 0);
+            return true;
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "加载项目档案失败: {Name}", projectName);
+            _current = null;
+            return false;
+        }
+    }
+
+    /// <summary>
+    /// 列出所有可用项目(扫描 projects/*.json)
+    /// </summary>
+    public static List<string> ListProjects()
+    {
+        try
+        {
+            if (!Directory.Exists(ProjectsDir))
+                return new List<string>();
+
+            return Directory.GetFiles(ProjectsDir, "*.json")
+                .Select(f => Path.GetFileNameWithoutExtension(f))
+                .OrderBy(n => n)
+                .ToList();
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "扫描项目目录失败");
+            return new List<string>();
+        }
+    }
+
+    /// <summary>
+    /// 获取项目 JSON 文件路径
+    /// </summary>
+    public static string GetProjectPath(string projectName)
+    {
+        return Path.Combine(ProjectsDir, $"{projectName}.json");
+    }
+
+    /// <summary>
+    /// 确保 projects/ 目录存在
+    /// </summary>
+    public static void EnsureProjectsDir()
+    {
+        try
+        {
+            if (!Directory.Exists(ProjectsDir))
+            {
+                Directory.CreateDirectory(ProjectsDir);
+                Log.Information("已创建项目目录: {Path}", ProjectsDir);
+            }
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "创建项目目录失败");
+        }
+    }
+}

+ 38 - 2
src/YZWater.Core/ViewModels/MainViewModel.cs

@@ -18,6 +18,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
     private readonly ViewDViewModel _viewDViewModel;
     private readonly ViewEViewModel _viewEViewModel;
     private readonly ViewFViewModel _viewFViewModel;
+    private ProjectViewModel? _projectViewModel;
 
     private readonly ThemeService _themeService = ThemeService.Instance;
     private readonly LanguageService _langService = LanguageService.Instance;
@@ -93,6 +94,12 @@ public partial class MainViewModel : ObservableObject, IDisposable
     public ViewEViewModel ViewE => _viewEViewModel;
     public ViewFViewModel ViewF => _viewFViewModel;
 
+    /// <summary>项目化 ViewModel(当加载了项目档案时可用)</summary>
+    public ProjectViewModel? ProjectView => _projectViewModel;
+
+    /// <summary>是否加载了项目档案</summary>
+    [ObservableProperty] private bool _hasProject;
+
     // ═══ 用户与权限 ═══
     [ObservableProperty] private string _currentUserName = string.Empty;
     [ObservableProperty] private string _currentUserRole = string.Empty;
@@ -124,6 +131,24 @@ public partial class MainViewModel : ObservableObject, IDisposable
         PlcPollingService.Instance.ConnectionStateChanged += OnConnectionStateChanged;
 
         StartTimer();
+
+        // 项目档案初始化
+        InitializeProject();
+    }
+
+    /// <summary>
+    /// 根据当前配置初始化项目 ViewModel
+    /// </summary>
+    private void InitializeProject()
+    {
+        var profile = ProjectService.Current;
+        if (profile != null)
+        {
+            _projectViewModel = new ProjectViewModel(profile);
+            CurrentView = _projectViewModel;
+            HasProject = true;
+            Log.Information("已初始化项目视图: {Name}", profile.Name);
+        }
     }
 
     private void OnLanguageChanged()
@@ -240,7 +265,10 @@ public partial class MainViewModel : ObservableObject, IDisposable
 
     private void SetActiveTab(int index)
     {
-        SelectedTabIndex = index;
+        // 当加载了项目档案时,Tab 0 = ProjectView,原有 Tab 偏移 +1
+        var offset = HasProject ? 1 : 0;
+        var actualIndex = index + offset;
+        SelectedTabIndex = actualIndex;
         IsTabAActive = index == 0;
         IsTabBActive = index == 1;
         IsTabCActive = index == 2;
@@ -250,7 +278,14 @@ public partial class MainViewModel : ObservableObject, IDisposable
         TabTitle = _langService.Get(_tabTitleKeys[index]);
     }
 
-    [RelayCommand] private void ShowViewA() { CurrentView = _viewAViewModel; SetActiveTab(0); }
+    [RelayCommand] private void ShowViewA()
+    {
+        // 当加载了项目档案时,第一个 Tab 显示项目视图
+        CurrentView = HasProject && _projectViewModel != null
+            ? _projectViewModel
+            : _viewAViewModel;
+        SetActiveTab(0);
+    }
     [RelayCommand] private void ShowViewB() { CurrentView = _viewBViewModel; SetActiveTab(1); }
     [RelayCommand] private void ShowViewC() { CurrentView = _viewCViewModel; SetActiveTab(2); }
     [RelayCommand] private void ShowViewD() { CurrentView = _viewDViewModel; SetActiveTab(3); }
@@ -374,5 +409,6 @@ public partial class MainViewModel : ObservableObject, IDisposable
         _themeService.PropertyChanged -= OnThemeServicePropertyChanged;
         PlcPollingService.Instance.ConnectionStateChanged -= OnConnectionStateChanged;
         _viewAViewModel.Dispose();
+        _projectViewModel?.Dispose();
     }
 }

+ 230 - 0
src/YZWater.Core/ViewModels/ProjectViewModel.cs

@@ -0,0 +1,230 @@
+using System.Collections.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Serilog;
+using YZWater.Core.Models;
+using YZWater.Core.Services;
+
+namespace YZWater.Core.ViewModels;
+
+/// <summary>
+/// 项目化 ViewModel - 根据 ProjectProfile 动态生成设备列表
+/// </summary>
+public partial class ProjectViewModel : ObservableObject, IDisposable
+{
+    private readonly ProjectProfile _profile;
+    private readonly PlcPollingService _pollingService;
+    private bool _disposed;
+
+    [ObservableProperty] private string _title = "";
+    [ObservableProperty] private string _subtitleText = "";
+    [ObservableProperty] private bool _isPlcConnected;
+    [ObservableProperty] private string _plcStatusText = "PLC 未连接";
+    [ObservableProperty] private DateTime _currentTime = DateTime.Now;
+
+    // 设备列表
+    public ObservableCollection<PumpItemViewModel> Pumps { get; } = new();
+    public ObservableCollection<TankItemViewModel> Tanks { get; } = new();
+    public ObservableCollection<GaugeItemViewModel> Gauges { get; } = new();
+    public ObservableCollection<SensorItemViewModel> Sensors { get; } = new();
+
+    public ProjectViewModel(ProjectProfile profile)
+    {
+        _profile = profile;
+        _pollingService = PlcPollingService.Instance;
+
+        Title = profile.Name;
+        SubtitleText = profile.Description;
+
+        // 根据项目档案初始化设备列表
+        foreach (var pump in profile.Pumps)
+            Pumps.Add(new PumpItemViewModel(pump));
+
+        foreach (var tank in profile.Tanks)
+            Tanks.Add(new TankItemViewModel(tank));
+
+        foreach (var gauge in profile.Gauges)
+            Gauges.Add(new GaugeItemViewModel(gauge));
+
+        foreach (var sensor in profile.Sensors)
+            Sensors.Add(new SensorItemViewModel(sensor));
+
+        // 初始化动态寄存器
+        _pollingService.Data.InitializeDynamicRegisters(profile);
+
+        // 订阅数据更新
+        _pollingService.DataUpdated += OnDataUpdated;
+        _pollingService.ConnectionStateChanged += OnConnectionStateChanged;
+    }
+
+    private void OnDataUpdated(PlcDataModel data)
+    {
+        // 更新泵数据
+        foreach (var pump in Pumps)
+            pump.UpdateFromData(data);
+
+        // 更新水箱数据
+        foreach (var tank in Tanks)
+            tank.UpdateFromData(data);
+
+        // 更新仪表数据
+        foreach (var gauge in Gauges)
+            gauge.UpdateFromData(data);
+
+        // 更新传感器数据
+        foreach (var sensor in Sensors)
+            sensor.UpdateFromData(data);
+    }
+
+    private void OnConnectionStateChanged(bool connected)
+    {
+        IsPlcConnected = connected;
+        PlcStatusText = connected ? "PLC 已连接" : "PLC 未连接";
+    }
+
+    public void Dispose()
+    {
+        if (_disposed) return;
+        _disposed = true;
+        _pollingService.DataUpdated -= OnDataUpdated;
+        _pollingService.ConnectionStateChanged -= OnConnectionStateChanged;
+    }
+}
+
+/// <summary>
+/// 泵设备 ViewModel
+/// </summary>
+public partial class PumpItemViewModel : ObservableObject
+{
+    private readonly PumpConfig _config;
+
+    public string Name => _config.Name;
+
+    [ObservableProperty] private bool _isRunning;
+    [ObservableProperty] private bool _hasFault;
+    [ObservableProperty] private float _current;
+    [ObservableProperty] private bool _isAutoMode;
+    [ObservableProperty] private float _dailyHours;
+    [ObservableProperty] private float _dailyMinutes;
+
+    public PumpItemViewModel(PumpConfig config)
+    {
+        _config = config;
+    }
+
+    public void UpdateFromData(PlcDataModel data)
+    {
+        IsRunning = data.GetDigitalRegister($"{Name}.Run");
+        HasFault = data.GetDigitalRegister($"{Name}.Fault");
+        Current = data.GetAnalogRegister($"{Name}.Current");
+        IsAutoMode = data.GetDigitalRegister($"{Name}.AutoMode");
+        DailyHours = data.GetAnalogRegister($"{Name}.DailyHours");
+        DailyMinutes = data.GetAnalogRegister($"{Name}.DailyMinutes");
+    }
+
+    /// <summary>手动启动</summary>
+    [RelayCommand]
+    private async Task StartAsync()
+    {
+        if (string.IsNullOrEmpty(_config.StartAddress)) return;
+        Log.Information("泵启动: {Name} → {Addr}", Name, _config.StartAddress);
+        await PlcService.WriteBoolAsync(_config.StartAddress, true);
+        await Task.Delay(200); // 点动:短暂置位后复位
+        await PlcService.WriteBoolAsync(_config.StartAddress, false);
+    }
+
+    /// <summary>手动停止</summary>
+    [RelayCommand]
+    private async Task StopAsync()
+    {
+        if (string.IsNullOrEmpty(_config.StopAddress)) return;
+        Log.Information("泵停止: {Name} → {Addr}", Name, _config.StopAddress);
+        await PlcService.WriteBoolAsync(_config.StopAddress, true);
+        await Task.Delay(200);
+        await PlcService.WriteBoolAsync(_config.StopAddress, false);
+    }
+}
+
+/// <summary>
+/// 水箱/水池 ViewModel
+/// </summary>
+public partial class TankItemViewModel : ObservableObject
+{
+    private readonly TankConfig _config;
+
+    public string Name => _config.Name;
+
+    [ObservableProperty] private float _level;
+    [ObservableProperty] private float _startPump1Level;
+    [ObservableProperty] private float _startPump2Level;
+    [ObservableProperty] private float _startPump3Level;
+    [ObservableProperty] private float _stopLevel;
+    [ObservableProperty] private float _protectionLevel;
+
+    public TankItemViewModel(TankConfig config)
+    {
+        _config = config;
+    }
+
+    public void UpdateFromData(PlcDataModel data)
+    {
+        Level = data.GetAnalogRegister($"{Name}.Level");
+        StartPump1Level = data.GetAnalogRegister($"{Name}.StartPump1Level");
+        StartPump2Level = data.GetAnalogRegister($"{Name}.StartPump2Level");
+        StartPump3Level = data.GetAnalogRegister($"{Name}.StartPump3Level");
+        StopLevel = data.GetAnalogRegister($"{Name}.StopLevel");
+        ProtectionLevel = data.GetAnalogRegister($"{Name}.ProtectionLevel");
+    }
+}
+
+/// <summary>
+/// 仪表 ViewModel(压力、流量等)
+/// </summary>
+public partial class GaugeItemViewModel : ObservableObject
+{
+    private readonly GaugeConfig _config;
+
+    public string Name => _config.Name;
+    public string Unit => _config.Unit;
+    public double MinValue => _config.MinValue;
+    public double MaxValue => _config.MaxValue;
+
+    [ObservableProperty] private double _value;
+    [ObservableProperty] private float _highSet;
+    [ObservableProperty] private float _lowSet;
+
+    public GaugeItemViewModel(GaugeConfig config)
+    {
+        _config = config;
+    }
+
+    public void UpdateFromData(PlcDataModel data)
+    {
+        Value = data.GetAnalogRegister($"{Name}.Value");
+        HighSet = data.GetAnalogRegister($"{Name}.HighSet");
+        LowSet = data.GetAnalogRegister($"{Name}.LowSet");
+    }
+}
+
+/// <summary>
+/// 通用传感器 ViewModel(日流量等)
+/// </summary>
+public partial class SensorItemViewModel : ObservableObject
+{
+    private readonly SensorConfig _config;
+
+    public string Name => _config.Name;
+    public string Unit => _config.Unit;
+
+    [ObservableProperty] private float _value;
+
+    public SensorItemViewModel(SensorConfig config)
+    {
+        _config = config;
+    }
+
+    public void UpdateFromData(PlcDataModel data)
+    {
+        Value = data.GetAnalogRegister($"{Name}.Value");
+    }
+}

+ 4 - 4
src/YZWater.Core/YZWater.Core.csproj

@@ -9,14 +9,14 @@
 
   <ItemGroup>
     <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
-    <PackageReference Include="SqlSugarCore" Version="5.1.4.214" />
-    <PackageReference Include="HslCommunication" Version="12.8.3" />
+    <PackageReference Include="SqlSugarCore" Version="5.1.4.217" />
+    <PackageReference Include="HslCommunication" Version="12.9.2" />
     <PackageReference Include="LiveChartsCore" Version="2.1.0-dev-798" />
     <PackageReference Include="LiveChartsCore.SkiaSharpView" Version="2.1.0-dev-798" />
-    <PackageReference Include="Serilog" Version="4.3.1" />
+    <PackageReference Include="Serilog" Version="4.4.0" />
     <PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
     <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
-    <PackageReference Include="SkiaSharp" Version="3.119.4" />
+    <PackageReference Include="SkiaSharp" Version="4.151.1" />
   </ItemGroup>
 
 </Project>