Forr谩sk贸d B枚ng茅sz茅se

娣诲姞瑙f瀽搴﹁嚜閫傚簲鍔熻兘

纾 鏇 1 hete
commit
845655b463
46 m贸dos铆tott f谩jl, 4707 hozz谩ad谩s 茅s 0 t枚rl茅s
  1. 432 0
      .gitignore
  2. 354 0
      README.md
  3. 54 0
      YZWater3.sln
  4. 11 0
      src/YZWater.Avalonia/App.axaml
  5. 31 0
      src/YZWater.Avalonia/App.axaml.cs
  6. BIN
      src/YZWater.Avalonia/Assets/姹℃按澶勭悊鍘.ico
  7. 146 0
      src/YZWater.Avalonia/Controls/AdaptiveContainer.cs
  8. 173 0
      src/YZWater.Avalonia/Controls/FanControl.cs
  9. 167 0
      src/YZWater.Avalonia/Controls/PipeLineControl.cs
  10. 191 0
      src/YZWater.Avalonia/Controls/PumpControl.cs
  11. 147 0
      src/YZWater.Avalonia/Controls/ValveControl.cs
  12. 113 0
      src/YZWater.Avalonia/Controls/WaterTankControl.cs
  13. 85 0
      src/YZWater.Avalonia/Converters/BoolToBrushConverter.cs
  14. 21 0
      src/YZWater.Avalonia/Program.cs
  15. 86 0
      src/YZWater.Avalonia/Views/MainWindow.axaml
  16. 11 0
      src/YZWater.Avalonia/Views/MainWindow.axaml.cs
  17. 175 0
      src/YZWater.Avalonia/Views/ViewAView.axaml
  18. 11 0
      src/YZWater.Avalonia/Views/ViewAView.axaml.cs
  19. 77 0
      src/YZWater.Avalonia/Views/ViewBView.axaml
  20. 11 0
      src/YZWater.Avalonia/Views/ViewBView.axaml.cs
  21. 72 0
      src/YZWater.Avalonia/Views/ViewCView.axaml
  22. 11 0
      src/YZWater.Avalonia/Views/ViewCView.axaml.cs
  23. 93 0
      src/YZWater.Avalonia/Views/ViewDView.axaml
  24. 11 0
      src/YZWater.Avalonia/Views/ViewDView.axaml.cs
  25. 65 0
      src/YZWater.Avalonia/Views/ViewEView.axaml
  26. 11 0
      src/YZWater.Avalonia/Views/ViewEView.axaml.cs
  27. 35 0
      src/YZWater.Avalonia/YZWater.Avalonia.csproj
  28. 25 0
      src/YZWater.Avalonia/app.manifest
  29. 63 0
      src/YZWater.Core/Models/AlarmRecord.cs
  30. 83 0
      src/YZWater.Core/Models/EquipmentStatus.cs
  31. 52 0
      src/YZWater.Core/Models/FlowRecord.cs
  32. 39 0
      src/YZWater.Core/Models/Person.cs
  33. 75 0
      src/YZWater.Core/Models/SystemConfig.cs
  34. 94 0
      src/YZWater.Core/Services/ConfigService.cs
  35. 58 0
      src/YZWater.Core/Services/DatabaseService.cs
  36. 193 0
      src/YZWater.Core/Services/PlcService.cs
  37. 65 0
      src/YZWater.Core/Utils/Nlogger.cs
  38. 127 0
      src/YZWater.Core/Utils/ResolutionManager.cs
  39. 90 0
      src/YZWater.Core/Utils/WinSize.cs
  40. 134 0
      src/YZWater.Core/ViewModels/MainViewModel.cs
  41. 416 0
      src/YZWater.Core/ViewModels/ViewAViewModel.cs
  42. 140 0
      src/YZWater.Core/ViewModels/ViewBViewModel.cs
  43. 186 0
      src/YZWater.Core/ViewModels/ViewCViewModel.cs
  44. 171 0
      src/YZWater.Core/ViewModels/ViewDViewModel.cs
  45. 78 0
      src/YZWater.Core/ViewModels/ViewEViewModel.cs
  46. 24 0
      src/YZWater.Core/YZWater.Core.csproj

+ 432 - 0
.gitignore

@@ -0,0 +1,432 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog/
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (Sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure settings, but sensitive information contained in these scripts
+# will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server Files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Pair with TFS gI
+tools/**
+!tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU Debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (often used with SSDT)
+.vshistory/
+
+# BeatPulse healthance liveness check folder
+health/
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+
+# Common IDE files
+*.swp
+*.swo
+*~
+
+# macOS
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.Spotlight-V100
+.Trashes
+
+# Directories potentially created on remote AFP share
+.Network
+.TemporaryItems
+.fseventsd
+
+# Auto-generanted files
+*.g.cs
+*.g.i.cs
+
+# Avalonia
+*.axaml.cs.g.cs
+
+# Database files
+*.db
+*.db-journal
+*.db-shm
+*.db-wal
+
+# Log files
+Logs/
+*.log
+
+# Config files with sensitive data
+yzwater-config.json

+ 354 - 0
README.md

@@ -0,0 +1,354 @@
+# YZWater3 - 璺ㄥ钩鍙版薄姘村鐞嗙洃鎺х郴缁
+
+鍩轰簬 Avalonia UI 鐨勮法骞冲彴姹℃按澶勭悊鍘傜洃鎺х郴缁燂紝鏀寔 Windows銆丩inux 鍜 macOS銆
+
+## 馃殌 鎶鏈爤
+
+| 缁勪欢 | 鎶鏈柟妗 |
+|------|---------|
+| **UI 妗嗘灦** | Avalonia UI 11.2.3 |
+| **杩愯鏃** | .NET 8.0 |
+| **MVVM 妗嗘灦** | CommunityToolkit.Mvvm |
+| **PLC 閫氫俊** | HslCommunication |
+| **鏁版嵁搴** | FreeSql + SQLite |
+| **鍥捐〃** | LiveCharts2 |
+| **鏃ュ織** | Serilog |
+
+## 馃搧 椤圭洰缁撴瀯
+
+```
+YZWater3/
+鈹溾攢鈹 src/
+鈹   鈹溾攢鈹 YZWater.Core/              # 鏍稿績涓氬姟閫昏緫锛堝叡浜級
+鈹   鈹   鈹溾攢鈹 Models/                # 鏁版嵁妯″瀷
+鈹   鈹   鈹   鈹溾攢鈹 Person.cs         # 浜哄憳妯″瀷
+鈹   鈹   鈹   鈹溾攢鈹 AlarmRecord.cs    # 鎶ヨ璁板綍
+鈹   鈹   鈹   鈹溾攢鈹 FlowRecord.cs     # 娴侀噺璁板綍
+鈹   鈹   鈹   鈹溾攢鈹 EquipmentStatus.cs # 璁惧鐘舵
+鈹   鈹   鈹   鈹斺攢鈹 SystemConfig.cs   # 绯荤粺閰嶇疆
+鈹   鈹   鈹溾攢鈹 Services/              # 鏈嶅姟灞
+鈹   鈹   鈹   鈹溾攢鈹 DatabaseService.cs # 鏁版嵁搴撴湇鍔
+鈹   鈹   鈹   鈹溾攢鈹 PlcService.cs     # PLC 閫氫俊鏈嶅姟
+鈹   鈹   鈹   鈹斺攢鈹 ConfigService.cs  # 閰嶇疆鏈嶅姟
+鈹   鈹   鈹溾攢鈹 ViewModels/            # ViewModel 灞
+鈹   鈹   鈹   鈹溾攢鈹 MainViewModel.cs  # 涓荤獥鍙
+鈹   鈹   鈹   鈹溾攢鈹 ViewAViewModel.cs # 涓诲伐鑹
+鈹   鈹   鈹   鈹溾攢鈹 ViewBViewModel.cs # 鍙傛暟璁剧疆
+鈹   鈹   鈹   鈹溾攢鈹 ViewCViewModel.cs # 娴侀噺璁板綍
+鈹   鈹   鈹   鈹溾攢鈹 ViewDViewModel.cs # 鎶ヨ璁板綍
+鈹   鈹   鈹   鈹斺攢鈹 ViewEViewModel.cs # 鍏充簬
+鈹   鈹   鈹斺攢鈹 Utils/                 # 宸ュ叿绫
+鈹   鈹       鈹溾攢鈹 Nlogger.cs        # 鏃ュ織宸ュ叿
+鈹   鈹       鈹斺攢鈹 WinSize.cs        # 绐楀彛灏哄宸ュ叿
+鈹   鈹
+鈹   鈹斺攢鈹 YZWater.Avalonia/          # Avalonia UI 椤圭洰
+鈹       鈹溾攢鈹 Views/                 # XAML 瑙嗗浘
+鈹       鈹   鈹溾攢鈹 MainWindow.axaml  # 涓荤獥鍙
+鈹       鈹   鈹溾攢鈹 ViewAView.axaml   # 涓诲伐鑹鸿鍥撅紙宸ヤ笟鎺т欢锛
+鈹       鈹   鈹溾攢鈹 ViewBView.axaml   # 鍙傛暟璁剧疆瑙嗗浘
+鈹       鈹   鈹溾攢鈹 ViewCView.axaml   # 娴侀噺璁板綍瑙嗗浘
+鈹       鈹   鈹溾攢鈹 ViewDView.axaml   # 鎶ヨ璁板綍瑙嗗浘
+鈹       鈹   鈹斺攢鈹 ViewEView.axaml   # 鍏充簬瑙嗗浘
+鈹       鈹溾攢鈹 Controls/              # 鑷畾涔夊伐涓氭帶浠
+鈹       鈹   鈹溾攢鈹 WaterTankControl.cs # 姘寸鎺т欢
+鈹       鈹   鈹溾攢鈹 PumpControl.cs    # 娉垫帶浠
+鈹       鈹   鈹溾攢鈹 FanControl.cs     # 椋庢墖鎺т欢
+鈹       鈹   鈹溾攢鈹 PipeLineControl.cs # 绠¢亾鎺т欢
+鈹       鈹   鈹斺攢鈹 ValveControl.cs   # 闃闂ㄦ帶浠
+鈹       鈹溾攢鈹 Converters/            # 鍊艰浆鎹㈠櫒
+鈹       鈹斺攢鈹 Assets/                # 璧勬簮鏂囦欢
+鈹
+鈹斺攢鈹 YZWater3.sln                   # 瑙e喅鏂规鏂囦欢
+```
+
+## 馃洜锔 寮鍙戠幆澧
+
+### 鍓嶇疆瑕佹眰
+
+- .NET 8.0 SDK 鎴栨洿楂樼増鏈
+- Visual Studio 2022 鎴 JetBrains Rider锛堟帹鑽愶級
+- Git
+
+### 瀹夎姝ラ
+
+1. **鍏嬮殕浠撳簱**
+   ```bash
+   git clone <repository-url>
+   cd YZWater3
+   ```
+
+2. **杩樺師渚濊禆**
+   ```bash
+   dotnet restore
+   ```
+
+3. **鏋勫缓椤圭洰**
+   ```bash
+   dotnet build
+   ```
+
+4. **杩愯搴旂敤**
+   ```bash
+   dotnet run --project src/YZWater.Avalonia
+   ```
+
+## 馃幆 鍔熻兘鐗规
+
+### 涓诲伐鑹鸿鍥 (ViewA)
+- **宸ヤ笟鎺т欢灞曠ず** - 浣跨敤鑷畾涔夋帶浠舵ā鎷熺湡瀹炲伐涓氱晫闈
+  - WaterTankControl - 姘寸娑蹭綅鏄剧ず
+  - PumpControl - 娉佃繍琛岀姸鎬佸姩鐢
+  - FanControl - 椋庢墖杩愯鐘舵佸姩鐢
+  - PipeLineControl - 绠¢亾娴佸姩鏁堟灉
+  - ValveControl - 闃闂ㄧ姸鎬佹樉绀
+- 瀹炴椂鏄剧ず 4 涓按姹犳恫浣
+- 杩涙按/鍑烘按娴侀噺鐩戞祴
+- 娉点侀鏈恒侀榾闂ㄦ帶鍒
+- 鑷姩/鎵嬪姩妯″紡鍒囨崲
+- 鎶ヨ鐘舵佹樉绀
+- 璁惧鐘舵侀潰鏉
+
+### 鍙傛暟璁剧疆 (ViewB)
+- PLC 杩炴帴閰嶇疆锛圛P銆佺鍙o級
+- 鎶ヨ闃堝艰缃紙娑蹭綅銆佹祦閲忥級
+- 娉甸鐜囪缃
+- 閰嶇疆淇濆瓨/鍔犺浇
+
+### 娴侀噺璁板綍 (ViewC)
+- 杩涙按/鍑烘按娴侀噺鍥捐〃
+- 鍘嗗彶鏁版嵁鏌ヨ
+- 鏁版嵁瀵煎嚭 CSV
+- 娓呴櫎鏃ф暟鎹
+
+### 鎶ヨ璁板綍 (ViewD)
+- 瀹炴椂鎶ヨ鍒楄〃
+- 鎶ヨ纭鍔熻兘
+- 鎵归噺纭
+- 鎶ヨ璇︽儏鏌ョ湅
+
+### 鍏充簬 (ViewE)
+- 绯荤粺淇℃伅
+- 鍏徃淇℃伅
+- 鐗堟湰淇℃伅
+
+## 馃敡 閰嶇疆璇存槑
+
+### PLC 閰嶇疆
+
+榛樿閰嶇疆鏂囦欢锛歚yzwater-config.json`
+
+```json
+{
+  "PlcIp": "192.168.0.150",
+  "PlcPort": 5000,
+  "AutoConnect": true,
+  "LevelHighAlarm": 80.0,
+  "LevelLowAlarm": 20.0,
+  "FlowHighAlarm": 100.0,
+  "PumpFrequency": 50.0,
+  "SystemName": "姹℃按澶勭悊鐩戞帶绯荤粺",
+  "CompanyName": "鎵窞鏃僵绉戞妧鏈夐檺鍏徃",
+  "ContactPerson": "璧电粡鐞",
+  "ContactPhone": "18115099090"
+}
+```
+
+### 鏁版嵁搴撻厤缃
+
+鏁版嵁搴撴枃浠讹細`yzwater.db`锛圫QLite锛
+
+鑷姩鍒涘缓浠ヤ笅琛細
+- `persons` - 浜哄憳琛
+- `alarm_records` - 鎶ヨ璁板綍琛
+- `flow_records` - 娴侀噺璁板綍琛
+- `system_config` - 绯荤粺閰嶇疆琛
+
+## 馃摝 渚濊禆鍖
+
+### Core 椤圭洰
+- CommunityToolkit.Mvvm 8.2.2
+- FreeSql.Provider.Sqlite 3.2.800
+- FreeSql.Repository 3.2.800
+- HslCommunication 12.0.1
+- Serilog 4.0.2
+- Serilog.Sinks.Console 6.0.0
+- Serilog.Sinks.File 6.0.0
+
+### Avalonia 椤圭洰
+- Avalonia 11.2.3
+- Avalonia.Desktop 11.2.3
+- Avalonia.Themes.Fluent 11.2.3
+- Avalonia.Fonts.Inter 11.2.3
+- LiveChartsCore.SkiaSharpView.Avalonia 2.0.0-rc4.5
+- Semi.Avalonia 11.2.1.4
+
+## 馃搻 瑙f瀽搴﹁嚜閫傚簲
+
+### 鑷傚簲鏈哄埗
+
+椤圭洰浣跨敤 `AdaptiveContainer` 鎺т欢瀹炵幇瑙f瀽搴﹁嚜閫傚簲锛
+
+1. **璁捐鍩哄噯**: 1280 x 800
+2. **缂╂斁鏂瑰紡**: Uniform锛堢瓑姣旂缉鏀撅紝淇濇寔瀹介珮姣旓級
+3. **缂╂斁鑼冨洿**: 0.6x - 1.5x
+4. **鑷姩灞呬腑**: 鍐呭鍦ㄥ彲鐢ㄧ┖闂村唴鑷姩灞呬腑
+
+### 宸ヤ綔鍘熺悊
+
+```
+鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
+鈹  绐楀彛瀹為檯澶у皬 (1920 x 1080)                                  鈹
+鈹  鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹   鈹
+鈹  鈹  缂╂斁鍚庣殑鍐呭鍖哄煙 (1280 x 800 * 1.25)                鈹   鈹
+鈹  鈹  鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹   鈹   鈹
+鈹  鈹  鈹  璁捐鍐呭 (1280 x 800)                       鈹   鈹   鈹
+鈹  鈹  鈹  - 鎵鏈夋帶浠朵娇鐢ㄥ浐瀹氬潗鏍                       鈹   鈹   鈹
+鈹  鈹  鈹  - 鐢卞鍣ㄧ粺涓缂╂斁                            鈹   鈹   鈹
+鈹  鈹  鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹   鈹   鈹
+鈹  鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹   鈹
+鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
+```
+
+### 浣跨敤鏂瑰紡
+
+```xml
+<!-- 鍦 MainWindow.axaml 涓 -->
+<controls:AdaptiveContainer DesignWidth="1280" DesignHeight="800"
+                             Stretch="Uniform" MinScale="0.6" MaxScale="1.5">
+    <!-- 鎵鏈夊唴瀹规斁鍦ㄨ繖閲 -->
+</controls:AdaptiveContainer>
+```
+
+### 鍒嗚鲸鐜囩鐞嗗櫒
+
+`ResolutionManager` 鎻愪緵鍏ㄥ眬鍒嗚鲸鐜囦俊鎭細
+
+```csharp
+// 鑾峰彇褰撳墠缂╂斁姣斾緥
+var scale = ResolutionManager.Instance.UniformScale;
+
+// 璁$畻缂╂斁鍚庣殑灏哄
+var scaledWidth = ResolutionManager.Instance.Scale(100);
+
+// 璁$畻缂╂斁鍚庣殑瀛椾綋澶у皬
+var fontSize = ResolutionManager.Instance.ScaleFontSize(14);
+```
+
+## 馃帹 鑷畾涔夊伐涓氭帶浠
+
+### WaterTankControl - 姘寸鎺т欢
+- 鏄剧ず娑蹭綅鐧惧垎姣
+- 鍙厤缃按棰滆壊鍜岃竟妗嗛鑹
+- 鏀寔鏂囧瓧鏍囩
+
+```xml
+<controls:WaterTankControl WaterLevel="75" Text="鍏ュ彛姹" WaterColor="#178187"/>
+```
+
+### PumpControl - 娉垫帶浠
+- 杩愯鏃舵樉绀烘棆杞姩鐢
+- 鍙厤缃棆杞熷害
+- 鏀寔澶氶鑹查厤缃
+
+```xml
+<controls:PumpControl IsRunning="{Binding Pump1Running}" Speed="2" Text="娉1"/>
+```
+
+### FanControl - 椋庢墖鎺т欢
+- 杩愯鏃舵樉绀洪鎵囨棆杞姩鐢
+- 鍙厤缃棆杞熷害
+- 鏀寔鏂囧瓧鏍囩
+
+```xml
+<controls:FanControl IsRunning="{Binding Fan1Running}" Speed="1" Text="椋庢墖1"/>
+```
+
+### PipeLineControl - 绠¢亾鎺т欢
+- 鏄剧ず娴佸姩鏁堟灉锛堣櫄绾垮姩鐢伙級
+- 鏀寔姘村钩/鍨傜洿鏂瑰悜
+- 鍙厤缃閬撻鑹插拰姘存祦棰滆壊
+
+```xml
+<controls:PipeLineControl IsFlow="True" IsHorizontal="True"/>
+```
+
+### ValveControl - 闃闂ㄦ帶浠
+- 涓夌鐘舵侊細鍏抽棴/涓棿/鎵撳紑
+- 鐘舵侀鑹叉寚绀猴紙绾/榛/缁匡級
+- 鏀寔鏂囧瓧鏍囩
+
+```xml
+<controls:ValveControl Status="{Binding Valve1Status}" Text="杩涙按闃"/>
+```
+
+## 馃攧 涓 YZWater2 鐨勫姣
+
+| 鐗规 | YZWater2 (WPF) | YZWater3 (Avalonia) |
+|------|---------------|---------------------|
+| **璺ㄥ钩鍙** | 鉂 浠 Windows | 鉁 Windows/Linux/macOS |
+| **UI 妗嗘灦** | WPF | Avalonia UI 11 |
+| **杩愯鏃** | .NET Framework 4.8 | .NET 8.0 |
+| **MVVM** | Caliburn.Micro | CommunityToolkit.Mvvm |
+| **鍥捐〃** | LiveCharts.Wpf | LiveCharts2 |
+| **鎺т欢搴** | HandyControl | Semi.Avalonia |
+| **鏃ュ織** | NLog | Serilog |
+| **宸ヤ笟鎺т欢** | HslControls | 鑷畾涔夋帶浠 |
+
+## 馃摑 寮鍙戣鏄
+
+### 娣诲姞鏂拌鍥
+
+1. 鍦 `YZWater.Core/ViewModels` 涓垱寤 ViewModel
+2. 鍦 `YZWater.Avalonia/Views` 涓垱寤哄搴旂殑 View
+3. 鍦 `MainViewModel` 涓坊鍔犺鍥惧垏鎹㈤昏緫
+4. 鍦 `MainWindow.axaml` 涓坊鍔 TabItem
+
+### 鑷畾涔夋帶浠
+
+鍦 `YZWater.Avalonia/Controls` 鐩綍涓垱寤鸿嚜瀹氫箟鎺т欢锛屼緥濡傦細
+- 姘寸鎺т欢 (WaterTankControl)
+- 绠¢亾鎺т欢 (PipeLineControl)
+- 闃闂ㄦ帶浠 (ValveControl)
+
+### 鏁版嵁缁戝畾
+
+浣跨敤 CommunityToolkit.Mvvm 鐨勬簮鐢熸垚鍣細
+
+```csharp
+public partial class MyViewModel : ObservableObject
+{
+    [ObservableProperty]
+    private string _name = string.Empty;
+
+    [RelayCommand]
+    private async Task SaveAsync()
+    {
+        // 淇濆瓨閫昏緫
+    }
+}
+```
+
+## 馃悰 甯歌闂
+
+### 1. PLC 杩炴帴澶辫触
+- 妫鏌 PLC IP 鍦板潃鏄惁姝g‘
+- 纭缃戠粶杩炴帴姝e父
+- 妫鏌ラ槻鐏璁剧疆
+
+### 2. 鏁版嵁搴撻敊璇
+- 鍒犻櫎 `yzwater.db` 鏂囦欢閲嶆柊鍚姩
+- 妫鏌ョ鐩樼┖闂
+
+### 3. UI 鏄剧ず寮傚父
+- 鏇存柊鏄惧崱椹卞姩
+- 妫鏌 .NET 8.0 杩愯鏃舵槸鍚﹀畨瑁
+
+## 馃搫 璁稿彲璇
+
+漏 2024 鎵窞鏃僵绉戞妧鏈夐檺鍏徃
+
+## 馃懃 鑱旂郴鏂瑰紡
+
+- **鍏徃**: 鎵窞鏃僵绉戞妧鏈夐檺鍏徃
+- **鑱旂郴浜**: 璧电粡鐞
+- **鐢佃瘽**: 18115099090

+ 54 - 0
YZWater3.sln

@@ -0,0 +1,54 @@
+锘
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YZWater.Core", "src\YZWater.Core\YZWater.Core.csproj", "{57C35C51-105D-4D70-8AF0-E3FA6FD89BB0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YZWater.Avalonia", "src\YZWater.Avalonia\YZWater.Avalonia.csproj", "{A8DAD3B1-F889-43F1-814B-C56911ED84D4}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Debug|x64 = Debug|x64
+		Debug|x86 = Debug|x86
+		Release|Any CPU = Release|Any CPU
+		Release|x64 = Release|x64
+		Release|x86 = Release|x86
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{57C35C51-105D-4D70-8AF0-E3FA6FD89BB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{57C35C51-105D-4D70-8AF0-E3FA6FD89BB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{57C35C51-105D-4D70-8AF0-E3FA6FD89BB0}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{57C35C51-105D-4D70-8AF0-E3FA6FD89BB0}.Debug|x64.Build.0 = Debug|Any CPU
+		{57C35C51-105D-4D70-8AF0-E3FA6FD89BB0}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{57C35C51-105D-4D70-8AF0-E3FA6FD89BB0}.Debug|x86.Build.0 = Debug|Any CPU
+		{57C35C51-105D-4D70-8AF0-E3FA6FD89BB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{57C35C51-105D-4D70-8AF0-E3FA6FD89BB0}.Release|Any CPU.Build.0 = Release|Any CPU
+		{57C35C51-105D-4D70-8AF0-E3FA6FD89BB0}.Release|x64.ActiveCfg = Release|Any CPU
+		{57C35C51-105D-4D70-8AF0-E3FA6FD89BB0}.Release|x64.Build.0 = Release|Any CPU
+		{57C35C51-105D-4D70-8AF0-E3FA6FD89BB0}.Release|x86.ActiveCfg = Release|Any CPU
+		{57C35C51-105D-4D70-8AF0-E3FA6FD89BB0}.Release|x86.Build.0 = Release|Any CPU
+		{A8DAD3B1-F889-43F1-814B-C56911ED84D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{A8DAD3B1-F889-43F1-814B-C56911ED84D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{A8DAD3B1-F889-43F1-814B-C56911ED84D4}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{A8DAD3B1-F889-43F1-814B-C56911ED84D4}.Debug|x64.Build.0 = Debug|Any CPU
+		{A8DAD3B1-F889-43F1-814B-C56911ED84D4}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{A8DAD3B1-F889-43F1-814B-C56911ED84D4}.Debug|x86.Build.0 = Debug|Any CPU
+		{A8DAD3B1-F889-43F1-814B-C56911ED84D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{A8DAD3B1-F889-43F1-814B-C56911ED84D4}.Release|Any CPU.Build.0 = Release|Any CPU
+		{A8DAD3B1-F889-43F1-814B-C56911ED84D4}.Release|x64.ActiveCfg = Release|Any CPU
+		{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
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(NestedProjects) = preSolution
+		{57C35C51-105D-4D70-8AF0-E3FA6FD89BB0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+		{A8DAD3B1-F889-43F1-814B-C56911ED84D4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+	EndGlobalSection
+EndGlobal

+ 11 - 0
src/YZWater.Avalonia/App.axaml

@@ -0,0 +1,11 @@
+<Application xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             x:Class="YZWater.Avalonia.App"
+             RequestedThemeVariant="Default">
+    <!-- "Default" ThemeVariant follows system theme, "Light" or "Dark" are other options -->
+
+    <Application.Styles>
+        <FluentTheme />
+    </Application.Styles>
+
+</Application>

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

@@ -0,0 +1,31 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using YZWater.Avalonia.Views;
+using YZWater.Core.Services;
+
+namespace YZWater.Avalonia;
+
+public partial class App : Application
+{
+    public override void Initialize()
+    {
+        AvaloniaXamlLoader.Load(this);
+    }
+
+    public override void OnFrameworkInitializationCompleted()
+    {
+        // 鍒濆鍖栨暟鎹簱鏈嶅姟
+        DatabaseService.Initialize();
+
+        // 鍒濆鍖 PLC 鏈嶅姟
+        PlcService.Initialize();
+
+        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+        {
+            desktop.MainWindow = new MainWindow();
+        }
+
+        base.OnFrameworkInitializationCompleted();
+    }
+}

BIN
src/YZWater.Avalonia/Assets/姹℃按澶勭悊鍘.ico


+ 146 - 0
src/YZWater.Avalonia/Controls/AdaptiveContainer.cs

@@ -0,0 +1,146 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using YZWater.Core.Utils;
+
+namespace YZWater.Avalonia.Controls;
+
+/// <summary>
+/// 鑷傚簲瀹瑰櫒鎺т欢 - 鏍规嵁绐楀彛澶у皬鑷姩缂╂斁鍐呭
+/// </summary>
+public class AdaptiveContainer : Decorator
+{
+    public static readonly StyledProperty<double> DesignWidthProperty =
+        AvaloniaProperty.Register<AdaptiveContainer, double>(nameof(DesignWidth), ResolutionManager.DesignWidth);
+
+    public static readonly StyledProperty<double> DesignHeightProperty =
+        AvaloniaProperty.Register<AdaptiveContainer, double>(nameof(DesignHeight), ResolutionManager.DesignHeight);
+
+    public static readonly StyledProperty<bool> MaintainAspectRatioProperty =
+        AvaloniaProperty.Register<AdaptiveContainer, bool>(nameof(MaintainAspectRatio), true);
+
+    public static readonly StyledProperty<Stretch> StretchProperty =
+        AvaloniaProperty.Register<AdaptiveContainer, Stretch>(nameof(Stretch), Stretch.Uniform);
+
+    public static readonly StyledProperty<double> MinScaleProperty =
+        AvaloniaProperty.Register<AdaptiveContainer, double>(nameof(MinScale), 0.5);
+
+    public static readonly StyledProperty<double> MaxScaleProperty =
+        AvaloniaProperty.Register<AdaptiveContainer, double>(nameof(MaxScale), 2.0);
+
+    public double DesignWidth
+    {
+        get => GetValue(DesignWidthProperty);
+        set => SetValue(DesignWidthProperty, value);
+    }
+
+    public double DesignHeight
+    {
+        get => GetValue(DesignHeightProperty);
+        set => SetValue(DesignHeightProperty, value);
+    }
+
+    public bool MaintainAspectRatio
+    {
+        get => GetValue(MaintainAspectRatioProperty);
+        set => SetValue(MaintainAspectRatioProperty, value);
+    }
+
+    public Stretch Stretch
+    {
+        get => GetValue(StretchProperty);
+        set => SetValue(StretchProperty, value);
+    }
+
+    public double MinScale
+    {
+        get => GetValue(MinScaleProperty);
+        set => SetValue(MinScaleProperty, value);
+    }
+
+    public double MaxScale
+    {
+        get => GetValue(MaxScaleProperty);
+        set => SetValue(MaxScaleProperty, value);
+    }
+
+    static AdaptiveContainer()
+    {
+        AffectsMeasure<AdaptiveContainer>(DesignWidthProperty, DesignHeightProperty, MaintainAspectRatioProperty, StretchProperty, MinScaleProperty, MaxScaleProperty);
+        AffectsArrange<AdaptiveContainer>(DesignWidthProperty, DesignHeightProperty, MaintainAspectRatioProperty, StretchProperty, MinScaleProperty, MaxScaleProperty);
+    }
+
+    protected override Size MeasureOverride(Size availableSize)
+    {
+        var child = Child;
+        if (child == null)
+        {
+            return new Size(0, 0);
+        }
+
+        // 璁╁瓙鍏冪礌鎸夌収璁捐灏哄娴嬮噺
+        var designSize = new Size(DesignWidth, DesignHeight);
+        child.Measure(designSize);
+
+        // 璁$畻鍙敤绌洪棿
+        var availableWidth = availableSize.Width;
+        var availableHeight = availableSize.Height;
+
+        // 濡傛灉鍙敤绌洪棿鏄棤闄愮殑锛屼娇鐢ㄨ璁″昂瀵
+        if (double.IsInfinity(availableWidth))
+        {
+            availableWidth = DesignWidth;
+        }
+        if (double.IsInfinity(availableHeight))
+        {
+            availableHeight = DesignHeight;
+        }
+
+        // 杩斿洖鍙敤绌洪棿锛堝疄闄呯缉鏀惧湪 Arrange 闃舵澶勭悊锛
+        return new Size(availableWidth, availableHeight);
+    }
+
+    protected override Size ArrangeOverride(Size finalSize)
+    {
+        var child = Child;
+        if (child == null)
+        {
+            return finalSize;
+        }
+
+        // 璁$畻缂╂斁姣斾緥
+        var scaleX = finalSize.Width / DesignWidth;
+        var scaleY = finalSize.Height / DesignHeight;
+
+        var scale = Stretch switch
+        {
+            Stretch.Fill => new ScaleTransform(scaleX, scaleY),
+            Stretch.Uniform => new ScaleTransform(Math.Min(scaleX, scaleY), Math.Min(scaleX, scaleY)),
+            Stretch.UniformToFill => new ScaleTransform(Math.Max(scaleX, scaleY), Math.Max(scaleX, scaleY)),
+            _ => new ScaleTransform(1, 1)
+        };
+
+        // 闄愬埗缂╂斁鑼冨洿
+        var uniformScale = Math.Max(MinScale, Math.Min(MaxScale, scale.ScaleX));
+
+        // 鏇存柊鍒嗚鲸鐜囩鐞嗗櫒
+        ResolutionManager.Instance.UpdateResolution(finalSize.Width, finalSize.Height);
+
+        // 璁$畻瀹為檯灏哄
+        var actualWidth = DesignWidth * uniformScale;
+        var actualHeight = DesignHeight * uniformScale;
+
+        // 璁$畻灞呬腑鍋忕Щ
+        var offsetX = (finalSize.Width - actualWidth) / 2;
+        var offsetY = (finalSize.Height - actualHeight) / 2;
+
+        // 搴旂敤鍙樻崲
+        child.RenderTransform = new ScaleTransform(uniformScale, uniformScale);
+        child.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute);
+
+        // 鎺掑垪瀛愬厓绱
+        child.Arrange(new Rect(offsetX, offsetY, DesignWidth, DesignHeight));
+
+        return finalSize;
+    }
+}

+ 173 - 0
src/YZWater.Avalonia/Controls/FanControl.cs

@@ -0,0 +1,173 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Threading;
+using System;
+using System.Globalization;
+
+namespace YZWater.Avalonia.Controls;
+
+/// <summary>
+/// 椋庢墖鎺т欢 - 妯℃嫙 HslCoolFan
+/// </summary>
+public class FanControl : Control
+{
+    public static readonly StyledProperty<bool> IsRunningProperty =
+        AvaloniaProperty.Register<FanControl, bool>(nameof(IsRunning), false);
+
+    public static readonly StyledProperty<double> SpeedProperty =
+        AvaloniaProperty.Register<FanControl, double>(nameof(Speed), 1.0);
+
+    public static readonly StyledProperty<IBrush> FanColorProperty =
+        AvaloniaProperty.Register<FanControl, IBrush>(nameof(FanColor), new SolidColorBrush(Color.Parse("#607D8B")));
+
+    public static readonly StyledProperty<string> TextProperty =
+        AvaloniaProperty.Register<FanControl, string>(nameof(Text), "椋庢墖");
+
+    public bool IsRunning
+    {
+        get => GetValue(IsRunningProperty);
+        set => SetValue(IsRunningProperty, value);
+    }
+
+    public double Speed
+    {
+        get => GetValue(SpeedProperty);
+        set => SetValue(SpeedProperty, value);
+    }
+
+    public IBrush FanColor
+    {
+        get => GetValue(FanColorProperty);
+        set => SetValue(FanColorProperty, value);
+    }
+
+    public string Text
+    {
+        get => GetValue(TextProperty);
+        set => SetValue(TextProperty, value);
+    }
+
+    private double _rotationAngle = 0;
+    private IDisposable? _timerSubscription;
+
+    static FanControl()
+    {
+        AffectsRender<FanControl>(IsRunningProperty, SpeedProperty, FanColorProperty, TextProperty);
+    }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnAttachedToVisualTree(e);
+        if (IsRunning)
+        {
+            StartAnimation();
+        }
+    }
+
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnDetachedFromVisualTree(e);
+        StopAnimation();
+    }
+
+    protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+    {
+        base.OnPropertyChanged(change);
+        if (change.Property == IsRunningProperty)
+        {
+            if (IsRunning)
+            {
+                StartAnimation();
+            }
+            else
+            {
+                StopAnimation();
+                _rotationAngle = 0;
+            }
+        }
+    }
+
+    private void StartAnimation()
+    {
+        StopAnimation();
+        _timerSubscription = DispatcherTimer.Run(() =>
+        {
+            _rotationAngle += Speed * 2;
+            if (_rotationAngle >= 360) _rotationAngle -= 360;
+            InvalidateVisual();
+            return true;
+        }, TimeSpan.FromMilliseconds(50));
+    }
+
+    private void StopAnimation()
+    {
+        _timerSubscription?.Dispose();
+        _timerSubscription = null;
+    }
+
+    public override void Render(DrawingContext context)
+    {
+        base.Render(context);
+
+        var bounds = new Rect(Bounds.Size);
+        var centerX = bounds.Width / 2;
+        var centerY = bounds.Height / 2;
+        var radius = Math.Min(bounds.Width, bounds.Height) / 2 - 5;
+        var typeface = new Typeface("Microsoft YaHei");
+
+        // 缁樺埗澶栨
+        var outerPen = new Pen(FanColor, 2);
+        context.DrawRectangle(null, outerPen, new Rect(
+            centerX - radius,
+            centerY - radius,
+            radius * 2,
+            radius * 2));
+
+        // 缁樺埗椋庢墖鍙剁墖
+        var bladeCount = 4;
+        var bladeLength = radius * 0.8;
+        var bladeWidth = radius * 0.25;
+
+        for (int i = 0; i < bladeCount; i++)
+        {
+            var angle = _rotationAngle + (360.0 / bladeCount) * i;
+            var radians = angle * Math.PI / 180;
+
+            var bladeX = centerX + Math.Cos(radians) * bladeLength * 0.4;
+            var bladeY = centerY + Math.Sin(radians) * bladeLength * 0.4;
+
+            var bladeRect = new Rect(
+                bladeX - bladeWidth / 2,
+                bladeY - bladeLength / 2,
+                bladeWidth,
+                bladeLength);
+
+            context.DrawRectangle(FanColor, null, bladeRect);
+        }
+
+        // 缁樺埗涓績鍦
+        var centerRadius = radius * 0.15;
+        context.DrawEllipse(FanColor, null, new Rect(
+            centerX - centerRadius,
+            centerY - centerRadius,
+            centerRadius * 2,
+            centerRadius * 2));
+
+        // 缁樺埗鏂囧瓧
+        if (!string.IsNullOrEmpty(Text))
+        {
+            var foreground = new SolidColorBrush(Colors.Black);
+            var text = new FormattedText(Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 10, foreground);
+            var textPoint = new Point(
+                centerX - text.Width / 2,
+                bounds.Bottom - text.Height - 2);
+            context.DrawText(text, textPoint);
+        }
+    }
+
+    protected override Size MeasureOverride(Size availableSize)
+    {
+        return new Size(50, 50);
+    }
+}

+ 167 - 0
src/YZWater.Avalonia/Controls/PipeLineControl.cs

@@ -0,0 +1,167 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Threading;
+using System;
+
+namespace YZWater.Avalonia.Controls;
+
+/// <summary>
+/// 绠¢亾鎺т欢 - 妯℃嫙 PipeLine
+/// </summary>
+public class PipeLineControl : Control
+{
+    public static readonly StyledProperty<bool> IsFlowProperty =
+        AvaloniaProperty.Register<PipeLineControl, bool>(nameof(IsFlow), false);
+
+    public static readonly StyledProperty<double> FlowSpeedProperty =
+        AvaloniaProperty.Register<PipeLineControl, double>(nameof(FlowSpeed), 1.0);
+
+    public static readonly StyledProperty<IBrush> PipeColorProperty =
+        AvaloniaProperty.Register<PipeLineControl, IBrush>(nameof(PipeColor), new SolidColorBrush(Color.Parse("#4CAF50")));
+
+    public static readonly StyledProperty<IBrush> WaterColorProperty =
+        AvaloniaProperty.Register<PipeLineControl, IBrush>(nameof(WaterColor), new SolidColorBrush(Color.Parse("#2196F3")));
+
+    public static readonly StyledProperty<double> PipeWidthProperty =
+        AvaloniaProperty.Register<PipeLineControl, double>(nameof(PipeWidth), 20.0);
+
+    public static readonly StyledProperty<bool> IsHorizontalProperty =
+        AvaloniaProperty.Register<PipeLineControl, bool>(nameof(IsHorizontal), true);
+
+    public bool IsFlow
+    {
+        get => GetValue(IsFlowProperty);
+        set => SetValue(IsFlowProperty, value);
+    }
+
+    public double FlowSpeed
+    {
+        get => GetValue(FlowSpeedProperty);
+        set => SetValue(FlowSpeedProperty, value);
+    }
+
+    public IBrush PipeColor
+    {
+        get => GetValue(PipeColorProperty);
+        set => SetValue(PipeColorProperty, value);
+    }
+
+    public IBrush WaterColor
+    {
+        get => GetValue(WaterColorProperty);
+        set => SetValue(WaterColorProperty, value);
+    }
+
+    public double PipeWidth
+    {
+        get => GetValue(PipeWidthProperty);
+        set => SetValue(PipeWidthProperty, value);
+    }
+
+    public bool IsHorizontal
+    {
+        get => GetValue(IsHorizontalProperty);
+        set => SetValue(IsHorizontalProperty, value);
+    }
+
+    private double _flowOffset = 0;
+    private IDisposable? _timerSubscription;
+
+    static PipeLineControl()
+    {
+        AffectsRender<PipeLineControl>(IsFlowProperty, FlowSpeedProperty, PipeColorProperty, WaterColorProperty, PipeWidthProperty, IsHorizontalProperty);
+    }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnAttachedToVisualTree(e);
+        if (IsFlow)
+        {
+            StartAnimation();
+        }
+    }
+
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnDetachedFromVisualTree(e);
+        StopAnimation();
+    }
+
+    protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+    {
+        base.OnPropertyChanged(change);
+        if (change.Property == IsFlowProperty)
+        {
+            if (IsFlow)
+            {
+                StartAnimation();
+            }
+            else
+            {
+                StopAnimation();
+                _flowOffset = 0;
+            }
+        }
+    }
+
+    private void StartAnimation()
+    {
+        StopAnimation();
+        _timerSubscription = DispatcherTimer.Run(() =>
+        {
+            _flowOffset += FlowSpeed;
+            if (_flowOffset >= 20) _flowOffset -= 20;
+            InvalidateVisual();
+            return true; // 缁х画杩愯
+        }, TimeSpan.FromMilliseconds(50));
+    }
+
+    private void StopAnimation()
+    {
+        _timerSubscription?.Dispose();
+        _timerSubscription = null;
+    }
+
+    public override void Render(DrawingContext context)
+    {
+        base.Render(context);
+
+        var bounds = new Rect(Bounds.Size);
+        var pipeWidth = PipeWidth;
+
+        if (IsHorizontal)
+        {
+            // 缁樺埗姘村钩绠¢亾
+            var pipeRect = new Rect(0, (bounds.Height - pipeWidth) / 2, bounds.Width, pipeWidth);
+            context.DrawRectangle(PipeColor, null, pipeRect);
+
+            // 缁樺埗娴佸姩鏁堟灉
+            if (IsFlow)
+            {
+                var dashPen = new Pen(WaterColor, pipeWidth * 0.6, new DashStyle(new double[] { 8, 12 }, _flowOffset));
+                var y = bounds.Height / 2;
+                context.DrawLine(dashPen, new Point(0, y), new Point(bounds.Width, y));
+            }
+        }
+        else
+        {
+            // 缁樺埗鍨傜洿绠¢亾
+            var pipeRect = new Rect((bounds.Width - pipeWidth) / 2, 0, pipeWidth, bounds.Height);
+            context.DrawRectangle(PipeColor, null, pipeRect);
+
+            // 缁樺埗娴佸姩鏁堟灉
+            if (IsFlow)
+            {
+                var dashPen = new Pen(WaterColor, pipeWidth * 0.6, new DashStyle(new double[] { 8, 12 }, _flowOffset));
+                var x = bounds.Width / 2;
+                context.DrawLine(dashPen, new Point(x, 0), new Point(x, bounds.Height));
+            }
+        }
+    }
+
+    protected override Size MeasureOverride(Size availableSize)
+    {
+        return IsHorizontal ? new Size(80, 30) : new Size(30, 80);
+    }
+}

+ 191 - 0
src/YZWater.Avalonia/Controls/PumpControl.cs

@@ -0,0 +1,191 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Threading;
+using System;
+using System.Globalization;
+
+namespace YZWater.Avalonia.Controls;
+
+/// <summary>
+/// 娉垫帶浠 - 妯℃嫙 HslPumpOne
+/// </summary>
+public class PumpControl : Control
+{
+    public static readonly StyledProperty<bool> IsRunningProperty =
+        AvaloniaProperty.Register<PumpControl, bool>(nameof(IsRunning), false);
+
+    public static readonly StyledProperty<double> SpeedProperty =
+        AvaloniaProperty.Register<PumpControl, double>(nameof(Speed), 0.0);
+
+    public static readonly StyledProperty<IBrush> Color1Property =
+        AvaloniaProperty.Register<PumpControl, IBrush>(nameof(Color1), new SolidColorBrush(Color.Parse("#9b9b9b")));
+
+    public static readonly StyledProperty<IBrush> Color2Property =
+        AvaloniaProperty.Register<PumpControl, IBrush>(nameof(Color2), new SolidColorBrush(Color.Parse("#9b9b9b")));
+
+    public static readonly StyledProperty<IBrush> Color3Property =
+        AvaloniaProperty.Register<PumpControl, IBrush>(nameof(Color3), new SolidColorBrush(Color.Parse("#9b9b9b")));
+
+    public static readonly StyledProperty<string> TextProperty =
+        AvaloniaProperty.Register<PumpControl, string>(nameof(Text), string.Empty);
+
+    public bool IsRunning
+    {
+        get => GetValue(IsRunningProperty);
+        set => SetValue(IsRunningProperty, value);
+    }
+
+    public double Speed
+    {
+        get => GetValue(SpeedProperty);
+        set => SetValue(SpeedProperty, value);
+    }
+
+    public IBrush Color1
+    {
+        get => GetValue(Color1Property);
+        set => SetValue(Color1Property, value);
+    }
+
+    public IBrush Color2
+    {
+        get => GetValue(Color2Property);
+        set => SetValue(Color2Property, value);
+    }
+
+    public IBrush Color3
+    {
+        get => GetValue(Color3Property);
+        set => SetValue(Color3Property, value);
+    }
+
+    public string Text
+    {
+        get => GetValue(TextProperty);
+        set => SetValue(TextProperty, value);
+    }
+
+    private double _rotationAngle = 0;
+    private IDisposable? _timerSubscription;
+
+    static PumpControl()
+    {
+        AffectsRender<PumpControl>(IsRunningProperty, SpeedProperty, Color1Property, Color2Property, Color3Property, TextProperty);
+    }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnAttachedToVisualTree(e);
+        if (IsRunning)
+        {
+            StartAnimation();
+        }
+    }
+
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnDetachedFromVisualTree(e);
+        StopAnimation();
+    }
+
+    protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+    {
+        base.OnPropertyChanged(change);
+        if (change.Property == IsRunningProperty)
+        {
+            if (IsRunning)
+            {
+                StartAnimation();
+            }
+            else
+            {
+                StopAnimation();
+                _rotationAngle = 0;
+            }
+        }
+    }
+
+    private void StartAnimation()
+    {
+        StopAnimation();
+        _timerSubscription = DispatcherTimer.Run(() =>
+        {
+            _rotationAngle += Speed;
+            if (_rotationAngle >= 360) _rotationAngle -= 360;
+            InvalidateVisual();
+            return true;
+        }, TimeSpan.FromMilliseconds(50));
+    }
+
+    private void StopAnimation()
+    {
+        _timerSubscription?.Dispose();
+        _timerSubscription = null;
+    }
+
+    public override void Render(DrawingContext context)
+    {
+        base.Render(context);
+
+        var bounds = new Rect(Bounds.Size);
+        var centerX = bounds.Width / 2;
+        var centerY = bounds.Height / 2;
+        var radius = Math.Min(bounds.Width, bounds.Height) / 2 - 5;
+        var typeface = new Typeface("Microsoft YaHei");
+
+        // 缁樺埗娉靛澹
+        var outerPen = new Pen(Color1, 3);
+        context.DrawEllipse(null, outerPen, new Rect(
+            centerX - radius,
+            centerY - radius,
+            radius * 2,
+            radius * 2));
+
+        // 缁樺埗娉靛彾鐗
+        var bladeCount = 3;
+        var bladeLength = radius * 0.7;
+        var bladeWidth = radius * 0.3;
+
+        for (int i = 0; i < bladeCount; i++)
+        {
+            var angle = _rotationAngle + (360.0 / bladeCount) * i;
+            var radians = angle * Math.PI / 180;
+
+            var bladeX = centerX + Math.Cos(radians) * bladeLength * 0.5;
+            var bladeY = centerY + Math.Sin(radians) * bladeLength * 0.5;
+
+            var bladeRect = new Rect(
+                bladeX - bladeWidth / 2,
+                bladeY - bladeLength / 2,
+                bladeWidth,
+                bladeLength);
+
+            context.DrawRectangle(Color2, null, bladeRect);
+        }
+
+        // 缁樺埗涓績鍦
+        var centerRadius = radius * 0.2;
+        context.DrawEllipse(Color3, null, new Rect(
+            centerX - centerRadius,
+            centerY - centerRadius,
+            centerRadius * 2,
+            centerRadius * 2));
+
+        // 缁樺埗鏂囧瓧
+        if (!string.IsNullOrEmpty(Text))
+        {
+            var foreground = new SolidColorBrush(Colors.Black);
+            var text = new FormattedText(Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 10, foreground);
+            var textPoint = new Point(
+                centerX - text.Width / 2,
+                bounds.Bottom - text.Height - 2);
+            context.DrawText(text, textPoint);
+        }
+    }
+
+    protected override Size MeasureOverride(Size availableSize)
+    {
+        return new Size(60, 60);
+    }
+}

+ 147 - 0
src/YZWater.Avalonia/Controls/ValveControl.cs

@@ -0,0 +1,147 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using System;
+using System.Globalization;
+
+namespace YZWater.Avalonia.Controls;
+
+/// <summary>
+/// 闃闂ㄧ姸鎬佹灇涓
+/// </summary>
+public enum ValveStatus
+{
+    /// <summary>
+    /// 鍏抽棴
+    /// </summary>
+    Closed,
+
+    /// <summary>
+    /// 涓棿浣嶇疆
+    /// </summary>
+    Middle,
+
+    /// <summary>
+    /// 鎵撳紑
+    /// </summary>
+    Open
+}
+
+/// <summary>
+/// 闃闂ㄦ帶浠 - 妯℃嫙 HslSwitch
+/// </summary>
+public class ValveControl : Control
+{
+    public static readonly StyledProperty<ValveStatus> StatusProperty =
+        AvaloniaProperty.Register<ValveControl, ValveStatus>(nameof(Status), ValveStatus.Middle);
+
+    public static readonly StyledProperty<string> TextProperty =
+        AvaloniaProperty.Register<ValveControl, string>(nameof(Text), "闃闂");
+
+    public static readonly StyledProperty<IBrush> ValveColorProperty =
+        AvaloniaProperty.Register<ValveControl, IBrush>(nameof(ValveColor), new SolidColorBrush(Color.Parse("#FF5722")));
+
+    public static readonly StyledProperty<IBrush> BackgroundColorProperty =
+        AvaloniaProperty.Register<ValveControl, IBrush>(nameof(BackgroundColor), new SolidColorBrush(Color.Parse("#333333")));
+
+    public ValveStatus Status
+    {
+        get => GetValue(StatusProperty);
+        set => SetValue(StatusProperty, value);
+    }
+
+    public string Text
+    {
+        get => GetValue(TextProperty);
+        set => SetValue(TextProperty, value);
+    }
+
+    public IBrush ValveColor
+    {
+        get => GetValue(ValveColorProperty);
+        set => SetValue(ValveColorProperty, value);
+    }
+
+    public IBrush BackgroundColor
+    {
+        get => GetValue(BackgroundColorProperty);
+        set => SetValue(BackgroundColorProperty, value);
+    }
+
+    static ValveControl()
+    {
+        AffectsRender<ValveControl>(StatusProperty, TextProperty, ValveColorProperty, BackgroundColorProperty);
+    }
+
+    public override void Render(DrawingContext context)
+    {
+        base.Render(context);
+
+        var bounds = new Rect(Bounds.Size);
+        var centerX = bounds.Width / 2;
+        var centerY = bounds.Height / 2;
+        var size = Math.Min(bounds.Width, bounds.Height) * 0.8;
+        var typeface = new Typeface("Microsoft YaHei");
+
+        // 缁樺埗鑳屾櫙鍦
+        var backgroundRadius = size / 2;
+        context.DrawEllipse(BackgroundColor, null, new Rect(
+            centerX - backgroundRadius,
+            centerY - backgroundRadius,
+            backgroundRadius * 2,
+            backgroundRadius * 2));
+
+        // 缁樺埗闃闂ㄦ墜鏌
+        var handleLength = size * 0.35;
+        var handleWidth = size * 0.15;
+
+        var angle = Status switch
+        {
+            ValveStatus.Open => 0,
+            ValveStatus.Middle => 45,
+            ValveStatus.Closed => 90,
+            _ => 45
+        };
+
+        // 绠鍖栫粯鍒 - 涓嶄娇鐢ㄦ棆杞紝鐩存帴缁樺埗鐭╁舰
+        var handleRect = new Rect(
+            centerX - handleWidth / 2,
+            centerY - handleLength,
+            handleWidth,
+            handleLength * 2);
+
+        context.DrawRectangle(ValveColor, null, handleRect);
+
+        // 缁樺埗鐘舵佹寚绀哄櫒
+        var indicatorSize = size * 0.15;
+        var indicatorColor = Status switch
+        {
+            ValveStatus.Open => new SolidColorBrush(Colors.Green),
+            ValveStatus.Middle => new SolidColorBrush(Colors.Yellow),
+            ValveStatus.Closed => new SolidColorBrush(Colors.Red),
+            _ => new SolidColorBrush(Colors.Gray)
+        };
+
+        context.DrawEllipse(indicatorColor, null, new Rect(
+            centerX - indicatorSize / 2,
+            centerY - backgroundRadius - indicatorSize - 5,
+            indicatorSize,
+            indicatorSize));
+
+        // 缁樺埗鏂囧瓧
+        if (!string.IsNullOrEmpty(Text))
+        {
+            var foreground = new SolidColorBrush(Colors.White);
+            var text = new FormattedText(Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 10, foreground);
+            var textPoint = new Point(
+                centerX - text.Width / 2,
+                bounds.Bottom - text.Height - 2);
+            context.DrawText(text, textPoint);
+        }
+    }
+
+    protected override Size MeasureOverride(Size availableSize)
+    {
+        return new Size(50, 70);
+    }
+}

+ 113 - 0
src/YZWater.Avalonia/Controls/WaterTankControl.cs

@@ -0,0 +1,113 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using System.Globalization;
+
+namespace YZWater.Avalonia.Controls;
+
+/// <summary>
+/// 姘寸鎺т欢 - 妯℃嫙 HslWaterBox
+/// </summary>
+public class WaterTankControl : Control
+{
+    public static readonly StyledProperty<double> WaterLevelProperty =
+        AvaloniaProperty.Register<WaterTankControl, double>(nameof(WaterLevel), 50.0);
+
+    public static readonly StyledProperty<string> TextProperty =
+        AvaloniaProperty.Register<WaterTankControl, string>(nameof(Text), string.Empty);
+
+    public static readonly StyledProperty<IBrush> WaterColorProperty =
+        AvaloniaProperty.Register<WaterTankControl, IBrush>(nameof(WaterColor), new SolidColorBrush(Color.Parse("#178187")));
+
+    public static readonly StyledProperty<IBrush> BorderColorProperty =
+        AvaloniaProperty.Register<WaterTankControl, IBrush>(nameof(BorderColor), new SolidColorBrush(Colors.LightGray));
+
+    public static readonly StyledProperty<double> EdgeWidthProperty =
+        AvaloniaProperty.Register<WaterTankControl, double>(nameof(EdgeWidth), 2.0);
+
+    public double WaterLevel
+    {
+        get => GetValue(WaterLevelProperty);
+        set => SetValue(WaterLevelProperty, value);
+    }
+
+    public string Text
+    {
+        get => GetValue(TextProperty);
+        set => SetValue(TextProperty, value);
+    }
+
+    public IBrush WaterColor
+    {
+        get => GetValue(WaterColorProperty);
+        set => SetValue(WaterColorProperty, value);
+    }
+
+    public IBrush BorderColor
+    {
+        get => GetValue(BorderColorProperty);
+        set => SetValue(BorderColorProperty, value);
+    }
+
+    public double EdgeWidth
+    {
+        get => GetValue(EdgeWidthProperty);
+        set => SetValue(EdgeWidthProperty, value);
+    }
+
+    static WaterTankControl()
+    {
+        AffectsRender<WaterTankControl>(WaterLevelProperty, TextProperty, WaterColorProperty, BorderColorProperty, EdgeWidthProperty);
+        AffectsMeasure<WaterTankControl>(WaterLevelProperty, TextProperty, WaterColorProperty, BorderColorProperty, EdgeWidthProperty);
+    }
+
+    public override void Render(DrawingContext context)
+    {
+        base.Render(context);
+
+        var bounds = new Rect(Bounds.Size);
+        var edgeWidth = EdgeWidth;
+        var typeface = new Typeface("Microsoft YaHei");
+        var foreground = new SolidColorBrush(Colors.White);
+
+        // 缁樺埗杈规
+        var borderPen = new Pen(BorderColor, edgeWidth);
+        context.DrawRectangle(null, borderPen, bounds);
+
+        // 璁$畻姘翠綅楂樺害
+        var waterHeight = bounds.Height * (WaterLevel / 100.0);
+        var waterRect = new Rect(
+            bounds.X + edgeWidth,
+            bounds.Y + bounds.Height - waterHeight,
+            bounds.Width - edgeWidth * 2,
+            waterHeight - edgeWidth);
+
+        // 缁樺埗姘
+        if (waterHeight > edgeWidth)
+        {
+            context.DrawRectangle(WaterColor, null, waterRect);
+        }
+
+        // 缁樺埗鏂囧瓧
+        if (!string.IsNullOrEmpty(Text))
+        {
+            var text = new FormattedText(Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 14, foreground);
+            var textPoint = new Point(
+                bounds.X + (bounds.Width - text.Width) / 2,
+                bounds.Y + (bounds.Height - text.Height) / 2);
+            context.DrawText(text, textPoint);
+        }
+
+        // 缁樺埗姘翠綅鐧惧垎姣
+        var levelText = new FormattedText($"{WaterLevel:F0}%", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 12, foreground);
+        var levelPoint = new Point(
+            bounds.X + (bounds.Width - levelText.Width) / 2,
+            bounds.Y + 5);
+        context.DrawText(levelText, levelPoint);
+    }
+
+    protected override Size MeasureOverride(Size availableSize)
+    {
+        return new Size(100, 120);
+    }
+}

+ 85 - 0
src/YZWater.Avalonia/Converters/BoolToBrushConverter.cs

@@ -0,0 +1,85 @@
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using System;
+using System.Globalization;
+
+namespace YZWater.Avalonia.Converters;
+
+/// <summary>
+/// 甯冨皵鍊艰浆鐢诲埛杞崲鍣
+/// </summary>
+public class BoolToBrushConverter : IValueConverter
+{
+    /// <summary>
+    /// 鐪熷兼椂鐨勭敾鍒
+    /// </summary>
+    public IBrush TrueBrush { get; set; } = new SolidColorBrush(Colors.Green);
+
+    /// <summary>
+    /// 鍋囧兼椂鐨勭敾鍒
+    /// </summary>
+    public IBrush FalseBrush { get; set; } = new SolidColorBrush(Colors.Red);
+
+    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (value is bool boolValue)
+        {
+            return boolValue ? TrueBrush : FalseBrush;
+        }
+        return FalseBrush;
+    }
+
+    public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        throw new NotImplementedException();
+    }
+}
+
+/// <summary>
+/// 鎶ヨ鐘舵佽浆鐢诲埛杞崲鍣
+/// </summary>
+public class AlarmToBrushConverter : IValueConverter
+{
+    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (value is bool hasAlarm)
+        {
+            return hasAlarm
+                ? new SolidColorBrush(Color.Parse("#F44336"))
+                : new SolidColorBrush(Color.Parse("#4CAF50"));
+        }
+        return new SolidColorBrush(Colors.Gray);
+    }
+
+    public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        throw new NotImplementedException();
+    }
+}
+
+/// <summary>
+/// 璁惧鐘舵佽浆鐢诲埛杞崲鍣
+/// </summary>
+public class DeviceStatusToBrushConverter : IValueConverter
+{
+    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (value is Core.Models.DeviceStatus status)
+        {
+            return status switch
+            {
+                Core.Models.DeviceStatus.Running => new SolidColorBrush(Color.Parse("#4CAF50")),
+                Core.Models.DeviceStatus.Stopped => new SolidColorBrush(Color.Parse("#9E9E9E")),
+                Core.Models.DeviceStatus.Fault => new SolidColorBrush(Color.Parse("#F44336")),
+                Core.Models.DeviceStatus.Maintenance => new SolidColorBrush(Color.Parse("#FF9800")),
+                _ => new SolidColorBrush(Colors.Gray)
+            };
+        }
+        return new SolidColorBrush(Colors.Gray);
+    }
+
+    public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        throw new NotImplementedException();
+    }
+}

+ 21 - 0
src/YZWater.Avalonia/Program.cs

@@ -0,0 +1,21 @@
+using Avalonia;
+using System;
+
+namespace YZWater.Avalonia;
+
+class Program
+{
+    // Initialization code. Don't use any Avalonia, third-party APIs or any
+    // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+    // yet and stuff might break.
+    [STAThread]
+    public static void Main(string[] args) => BuildAvaloniaApp()
+        .StartWithClassicDesktopLifetime(args);
+
+    // Avalonia configuration, don't remove; also used by visual designer.
+    public static AppBuilder BuildAvaloniaApp()
+        => AppBuilder.Configure<App>()
+            .UsePlatformDetect()
+            .WithInterFont()
+            .LogToTrace();
+}

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

@@ -0,0 +1,86 @@
+<Window xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:vm="using:YZWater.Core.ViewModels"
+        xmlns:views="using:YZWater.Avalonia.Views"
+        xmlns:controls="using:YZWater.Avalonia.Controls"
+        x:Class="YZWater.Avalonia.Views.MainWindow"
+        x:DataType="vm:MainViewModel"
+        Title="{Binding Title}"
+        Width="1280"
+        Height="800"
+        MinWidth="800"
+        MinHeight="600"
+        WindowStartupLocation="CenterScreen"
+        Background="#1a1a2e">
+
+    <Window.DataContext>
+        <vm:MainViewModel/>
+    </Window.DataContext>
+
+    <!-- 浣跨敤鑷傚簲瀹瑰櫒鍖呰9鏁翠釜鍐呭 -->
+    <controls:AdaptiveContainer DesignWidth="1280" DesignHeight="800"
+                                 Stretch="Uniform" MinScale="0.6" MaxScale="1.5">
+        <DockPanel>
+            <!-- 椤堕儴鏍囬鏍 -->
+            <Border DockPanel.Dock="Top" Background="#1E3A5F" Padding="10">
+                <Grid ColumnDefinitions="Auto,*,Auto,Auto">
+                    <!-- Logo 鍜屾爣棰 -->
+                    <StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="10">
+                        <TextBlock Text="馃彮" FontSize="24" VerticalAlignment="Center"/>
+                        <TextBlock Text="{Binding Title}"
+                                   FontSize="20"
+                                   FontWeight="Bold"
+                                   Foreground="White"
+                                   VerticalAlignment="Center"/>
+                    </StackPanel>
+
+                    <!-- PLC 杩炴帴鐘舵 -->
+                    <StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="10" Margin="20,0">
+                        <Ellipse Width="12" Height="12" Fill="Green"/>
+                        <TextBlock Text="{Binding IsPlcConnected, StringFormat='PLC: {0}'}"
+                                   Foreground="White"
+                                   VerticalAlignment="Center"/>
+                    </StackPanel>
+
+                    <!-- 鏃堕棿鏄剧ず -->
+                    <TextBlock Grid.Column="3"
+                               Text="{Binding CurrentTime}"
+                               Foreground="White"
+                               FontSize="16"
+                               VerticalAlignment="Center"/>
+                </Grid>
+            </Border>
+
+            <!-- 搴曢儴瀵艰埅鏍 -->
+            <Border DockPanel.Dock="Bottom" Background="#2C5F8A" Padding="5">
+                <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="20">
+                    <Button Content="馃搳 涓诲伐鑹" Command="{Binding ShowViewACommand}" Padding="15,8"/>
+                    <Button Content="鈿欙笍 鍙傛暟璁剧疆" Command="{Binding ShowViewBCommand}" Padding="15,8"/>
+                    <Button Content="馃搱 娴侀噺璁板綍" Command="{Binding ShowViewCCommand}" Padding="15,8"/>
+                    <Button Content="馃敂 鎶ヨ璁板綍" Command="{Binding ShowViewDCommand}" Padding="15,8"/>
+                    <Button Content="鈩癸笍 鍏充簬" Command="{Binding ShowViewECommand}" Padding="15,8"/>
+                </StackPanel>
+            </Border>
+
+            <!-- 涓诲唴瀹瑰尯鍩 -->
+            <TabControl SelectedIndex="{Binding SelectedTabIndex}" Padding="10"
+                        Background="Transparent">
+                <TabItem Header="涓诲伐鑹" IsVisible="False">
+                    <views:ViewAView/>
+                </TabItem>
+                <TabItem Header="鍙傛暟璁剧疆" IsVisible="False">
+                    <views:ViewBView/>
+                </TabItem>
+                <TabItem Header="娴侀噺璁板綍" IsVisible="False">
+                    <views:ViewCView/>
+                </TabItem>
+                <TabItem Header="鎶ヨ璁板綍" IsVisible="False">
+                    <views:ViewDView/>
+                </TabItem>
+                <TabItem Header="鍏充簬" IsVisible="False">
+                    <views:ViewEView/>
+                </TabItem>
+            </TabControl>
+        </DockPanel>
+    </controls:AdaptiveContainer>
+</Window>

+ 11 - 0
src/YZWater.Avalonia/Views/MainWindow.axaml.cs

@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace YZWater.Avalonia.Views;
+
+public partial class MainWindow : Window
+{
+    public MainWindow()
+    {
+        InitializeComponent();
+    }
+}

+ 175 - 0
src/YZWater.Avalonia/Views/ViewAView.axaml

@@ -0,0 +1,175 @@
+<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"
+             x:Class="YZWater.Avalonia.Views.ViewAView"
+             x:DataType="vm:ViewAViewModel">
+
+    <UserControl.DataContext>
+        <vm:ViewAViewModel/>
+    </UserControl.DataContext>
+
+    <!-- 浣跨敤鍥哄畾灏哄璁捐锛岀敱澶栧眰瀹瑰櫒璐熻矗缂╂斁 -->
+    <Border Background="#1a1a2e" Width="1024" Height="613">
+        <Canvas Width="1024" Height="613">
+
+            <!-- 妯″紡鍒囨崲寮鍏 -->
+            <controls:ValveControl Canvas.Left="893" Canvas.Top="9" Width="129" Height="108"
+                                   Status="{Binding ModeStatus}" Text="杩滅▼/鏈湴"/>
+
+            <!-- 鍏ュ彛姘寸 -->
+            <controls:WaterTankControl Canvas.Left="44" Canvas.Top="113" Width="134" Height="100"
+                                       WaterLevel="{Binding Tank1Level}" Text="鍏ュ彛姹"
+                                       WaterColor="#178187" BorderColor="#cfcfcf"/>
+
+            <!-- 姹1 -->
+            <Border BorderBrush="#cfcfcf" BorderThickness="8" Height="135" Canvas.Left="248" Canvas.Top="113" Width="138"/>
+            <controls:WaterTankControl Canvas.Left="250" Canvas.Top="121" Width="134" Height="127"
+                                       WaterLevel="{Binding Tank1Level}" Text=""
+                                       WaterColor="#178187" BorderColor="#cfcfcf"/>
+
+            <!-- 姹2 -->
+            <controls:WaterTankControl Canvas.Left="250" Canvas.Top="248" Width="134" Height="87"
+                                       WaterLevel="{Binding Tank2Level}" Text=""
+                                       WaterColor="#178187" BorderColor="#cfcfcf"/>
+
+            <!-- 澶ф按姹 -->
+            <controls:WaterTankControl Canvas.Left="250" Canvas.Top="335" Width="748" Height="183"
+                                       WaterLevel="{Binding Tank3Level}" Text=""
+                                       WaterColor="#178187" BorderColor="#cfcfcf"/>
+            <Border BorderBrush="#cfcfcf" BorderThickness="8" Height="191" Canvas.Left="248" Canvas.Top="327" Width="138"/>
+
+            <!-- 鍑哄彛姹 -->
+            <controls:WaterTankControl Canvas.Left="44" Canvas.Top="267" Width="134" Height="100"
+                                       WaterLevel="{Binding Tank4Level}" Text="鍑哄彛姹"
+                                       WaterColor="#178187" BorderColor="#cfcfcf"/>
+
+            <!-- 绠¢亾杩炴帴绾 -->
+            <Rectangle Fill="#178187" Height="12" Canvas.Left="358" Stroke="Transparent" Canvas.Top="480" Width="48"
+                       RenderTransformOrigin="0.5,0.5">
+                <Rectangle.RenderTransform>
+                    <RotateTransform Angle="-90"/>
+                </Rectangle.RenderTransform>
+            </Rectangle>
+
+            <!-- 娑蹭綅鏄剧ず -->
+            <TextBlock Canvas.Left="676" Canvas.Top="414" Width="70" Foreground="White" FontSize="12">
+                <Run Text="娑蹭綅3:"/><Run Text="{Binding Tank3Level, StringFormat='{}{0:F1}'}"/><Run Text="绫"/>
+            </TextBlock>
+            <TextBlock Canvas.Left="288" Canvas.Top="147" Width="70" Foreground="White" FontSize="12">
+                <Run Text="姹1"/><LineBreak/><Run Text="娑蹭綅1:"/><Run Text="{Binding Tank1Level, StringFormat='{}{0:F1}'}"/><Run Text="绫"/>
+            </TextBlock>
+            <TextBlock Canvas.Left="286" Canvas.Top="415" Width="78" Foreground="White" FontSize="12">
+                <Run Text="姹2"/><LineBreak/><Run Text="娑蹭綅2:"/><Run Text="{Binding Tank2Level, StringFormat='{}{0:F1}'}"/><Run Text="绫"/>
+            </TextBlock>
+
+            <!-- 闃闂ㄦ帶浠 -->
+            <controls:ValveControl Canvas.Left="147" Canvas.Top="168" Width="22" Height="22"
+                                   Status="{Binding Valve1Status}" Text=""/>
+            <controls:ValveControl Canvas.Left="279" Canvas.Top="205" Width="22" Height="22"
+                                   Status="{Binding Valve2Status}" Text=""/>
+            <controls:ValveControl Canvas.Left="336" Canvas.Top="206" Width="22" Height="22"
+                                   Status="{Binding Valve3Status}" Text=""/>
+            <controls:ValveControl Canvas.Left="278" Canvas.Top="298" Width="22" Height="22"
+                                   Status="{Binding Valve4Status}" Text=""/>
+            <controls:ValveControl Canvas.Left="337" Canvas.Top="298" Width="22" Height="22"
+                                   Status="{Binding Valve5Status}" Text=""/>
+
+            <!-- 绠¢亾 -->
+            <controls:PipeLineControl Canvas.Left="0" Canvas.Top="306" Width="52" Height="29"
+                                      IsFlow="{Binding IsInflowRunning}" IsHorizontal="True"/>
+            <controls:PipeLineControl Canvas.Left="0" Canvas.Top="164" Width="52" Height="29"
+                                      IsFlow="{Binding IsOutflowRunning}" IsHorizontal="True"
+                                      RenderTransformOrigin="0.5,0.5">
+                <controls:PipeLineControl.RenderTransform>
+                    <RotateTransform Angle="180"/>
+                </controls:PipeLineControl.RenderTransform>
+            </controls:PipeLineControl>
+
+            <!-- 娉1-5 -->
+            <controls:PumpControl Canvas.Left="394" Canvas.Top="427" Width="60" Height="63"
+                                  IsRunning="{Binding Pump1Running}" Speed="2" Text="娉1"/>
+            <controls:PumpControl Canvas.Left="504" Canvas.Top="426" Width="60" Height="63"
+                                  IsRunning="{Binding Pump2Running}" Speed="2" Text="娉2"/>
+            <controls:PumpControl Canvas.Left="617" Canvas.Top="426" Width="60" Height="63"
+                                  IsRunning="{Binding Pump3Running}" Speed="2" Text="娉3"/>
+            <controls:PumpControl Canvas.Left="730" Canvas.Top="427" Width="60" Height="63"
+                                  IsRunning="{Binding Pump4Running}" Speed="2" Text="娉4"/>
+            <controls:PumpControl Canvas.Left="843" Canvas.Top="426" Width="60" Height="63"
+                                  IsRunning="{Binding Pump5Running}" Speed="2" Text="娉5"/>
+
+            <!-- 椋庢墖 -->
+            <controls:FanControl Canvas.Left="412" Canvas.Top="441" Width="34" Height="32"
+                                 IsRunning="{Binding Fan1Running}" Speed="1" Text="椋庢墖1"/>
+            <controls:FanControl Canvas.Left="522" Canvas.Top="441" Width="34" Height="32"
+                                 IsRunning="{Binding Fan2Running}" Speed="1" Text="椋庢墖2"/>
+
+            <!-- 娴侀噺鏄剧ず -->
+            <Border Background="#2d2d44" CornerRadius="5" Canvas.Left="10" Canvas.Top="550" Padding="10">
+                <StackPanel Spacing="10">
+                    <TextBlock Foreground="#4CAF50" FontWeight="Bold" FontSize="14">
+                        <Run Text="杩涙按娴侀噺: "/>
+                        <Run Text="{Binding InflowRate, StringFormat='{}{0:F1}'}"/>
+                        <Run Text=" m鲁/h"/>
+                    </TextBlock>
+                    <TextBlock Foreground="#2196F3" FontWeight="Bold" FontSize="14">
+                        <Run Text="鍑烘按娴侀噺: "/>
+                        <Run Text="{Binding OutflowRate, StringFormat='{}{0:F1}'}"/>
+                        <Run Text=" m鲁/h"/>
+                    </TextBlock>
+                </StackPanel>
+            </Border>
+
+            <!-- 鎶ヨ淇℃伅 -->
+            <Border Background="#F44336" CornerRadius="5" Canvas.Left="259" Canvas.Top="544" Width="685" Height="43"
+                    IsVisible="{Binding HasAlarm}">
+                <TextBlock Text="{Binding AlarmMessage}" FontSize="20" Foreground="White"
+                           HorizontalAlignment="Center" VerticalAlignment="Center"/>
+            </Border>
+
+            <!-- 鎿嶄綔鎸夐挳鍖哄煙 -->
+            <StackPanel Canvas.Left="10" Canvas.Top="10" Spacing="5">
+                <Button Content="杩炴帴PLC" Command="{Binding ConnectPlcCommand}" Width="80" Height="30"/>
+                <Button Content="鏂紑PLC" Command="{Binding DisconnectPlcCommand}" Width="80" Height="30"/>
+                <Button Content="鍒锋柊鏁版嵁" Command="{Binding RefreshDataCommand}" Width="80" Height="30"/>
+            </StackPanel>
+
+            <!-- 璁惧鐘舵侀潰鏉 -->
+            <Border Background="#2d2d44" CornerRadius="5" Canvas.Left="850" Canvas.Top="130" Width="160" Height="300" Padding="10">
+                <StackPanel Spacing="5">
+                    <TextBlock Text="璁惧鐘舵" FontWeight="Bold" Foreground="White" FontSize="14"/>
+                    <Separator Background="#444"/>
+                    <StackPanel Orientation="Horizontal" Spacing="5">
+                        <Ellipse Width="10" Height="10" Fill="{Binding Pump1StatusColor}"/>
+                        <TextBlock Text="娉1" Foreground="White"/>
+                    </StackPanel>
+                    <StackPanel Orientation="Horizontal" Spacing="5">
+                        <Ellipse Width="10" Height="10" Fill="{Binding Pump2StatusColor}"/>
+                        <TextBlock Text="娉2" Foreground="White"/>
+                    </StackPanel>
+                    <StackPanel Orientation="Horizontal" Spacing="5">
+                        <Ellipse Width="10" Height="10" Fill="{Binding Pump3StatusColor}"/>
+                        <TextBlock Text="娉3" Foreground="White"/>
+                    </StackPanel>
+                    <StackPanel Orientation="Horizontal" Spacing="5">
+                        <Ellipse Width="10" Height="10" Fill="{Binding Pump4StatusColor}"/>
+                        <TextBlock Text="娉4" Foreground="White"/>
+                    </StackPanel>
+                    <StackPanel Orientation="Horizontal" Spacing="5">
+                        <Ellipse Width="10" Height="10" Fill="{Binding Pump5StatusColor}"/>
+                        <TextBlock Text="娉5" Foreground="White"/>
+                    </StackPanel>
+                    <Separator Background="#444"/>
+                    <StackPanel Orientation="Horizontal" Spacing="5">
+                        <Ellipse Width="10" Height="10" Fill="{Binding Fan1StatusColor}"/>
+                        <TextBlock Text="椋庢墖1" Foreground="White"/>
+                    </StackPanel>
+                    <StackPanel Orientation="Horizontal" Spacing="5">
+                        <Ellipse Width="10" Height="10" Fill="{Binding Fan2StatusColor}"/>
+                        <TextBlock Text="椋庢墖2" Foreground="White"/>
+                    </StackPanel>
+                </StackPanel>
+            </Border>
+        </Canvas>
+    </Border>
+</UserControl>

+ 11 - 0
src/YZWater.Avalonia/Views/ViewAView.axaml.cs

@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace YZWater.Avalonia.Views;
+
+public partial class ViewAView : UserControl
+{
+    public ViewAView()
+    {
+        InitializeComponent();
+    }
+}

+ 77 - 0
src/YZWater.Avalonia/Views/ViewBView.axaml

@@ -0,0 +1,77 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:vm="using:YZWater.Core.ViewModels"
+             x:Class="YZWater.Avalonia.Views.ViewBView"
+             x:DataType="vm:ViewBViewModel">
+
+    <UserControl.DataContext>
+        <vm:ViewBViewModel/>
+    </UserControl.DataContext>
+
+    <Border Background="#1a1a2e" Width="1024" Height="613">
+        <ScrollViewer>
+            <Grid RowDefinitions="Auto,Auto,Auto" Margin="20">
+                <!-- PLC 杩炴帴璁剧疆 -->
+                <Border Grid.Row="0" Background="#2d2d44" CornerRadius="8" Padding="15" Margin="0,0,0,15">
+                    <StackPanel>
+                        <TextBlock Text="PLC 杩炴帴璁剧疆" FontWeight="Bold" FontSize="18" Foreground="White" Margin="0,0,0,15"/>
+                        <Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,Auto" Margin="10">
+                            <!-- IP 鍦板潃 -->
+                            <TextBlock Grid.Row="0" Grid.Column="0" Text="IP 鍦板潃:" VerticalAlignment="Center" Margin="5" Foreground="White"/>
+                            <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding PlcIp}" Margin="5"/>
+
+                            <!-- 绔彛 -->
+                            <TextBlock Grid.Row="1" Grid.Column="0" Text="绔彛:" VerticalAlignment="Center" Margin="5" Foreground="White"/>
+                            <NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding PlcPort}"
+                                           Minimum="1" Maximum="65535" Margin="5"/>
+
+                            <!-- 鑷姩杩炴帴 -->
+                            <TextBlock Grid.Row="2" Grid.Column="0" Text="鑷姩杩炴帴:" VerticalAlignment="Center" Margin="5" Foreground="White"/>
+                            <CheckBox Grid.Row="2" Grid.Column="1" IsChecked="{Binding AutoConnect}" Margin="5"/>
+
+                            <!-- 杩炴帴鐘舵 -->
+                            <TextBlock Grid.Row="3" Grid.Column="0" Text="鐘舵:" VerticalAlignment="Center" Margin="5" Foreground="White"/>
+                            <StackPanel Grid.Row="3" Grid.Column="1" Orientation="Horizontal" Spacing="10" Margin="5">
+                                <TextBlock Text="{Binding ConnectionStatus}" VerticalAlignment="Center" Foreground="White"/>
+                                <Button Content="娴嬭瘯杩炴帴" Command="{Binding TestConnectionCommand}"
+                                        IsEnabled="{Binding !IsConnecting}"/>
+                            </StackPanel>
+                        </Grid>
+                    </StackPanel>
+                </Border>
+
+                <!-- 鎶ヨ闃堝艰缃 -->
+                <Border Grid.Row="1" Background="#2d2d44" CornerRadius="8" Padding="15" Margin="0,0,0,15">
+                    <StackPanel>
+                        <TextBlock Text="鎶ヨ闃堝艰缃" FontWeight="Bold" FontSize="18" Foreground="White" Margin="0,0,0,15"/>
+                        <Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto,Auto" Margin="10">
+                            <!-- 娑蹭綅楂樻姤璀 -->
+                            <TextBlock Grid.Row="0" Grid.Column="0" Text="娑蹭綅楂樻姤璀:" VerticalAlignment="Center" Margin="5" Foreground="White"/>
+                            <NumericUpDown Grid.Row="0" Grid.Column="1" Value="{Binding LevelHighAlarm}"
+                                           Minimum="0" Maximum="100" FormatString="F1" Margin="5"/>
+                            <TextBlock Grid.Row="0" Grid.Column="2" Text="%" VerticalAlignment="Center" Margin="5" Foreground="White"/>
+
+                            <!-- 娑蹭綅浣庢姤璀 -->
+                            <TextBlock Grid.Row="1" Grid.Column="0" Text="娑蹭綅浣庢姤璀:" VerticalAlignment="Center" Margin="5" Foreground="White"/>
+                            <NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding LevelLowAlarm}"
+                                           Minimum="0" Maximum="100" FormatString="F1" Margin="5"/>
+                            <TextBlock Grid.Row="1" Grid.Column="2" Text="%" VerticalAlignment="Center" Margin="5" Foreground="White"/>
+
+                            <!-- 娴侀噺楂樻姤璀 -->
+                            <TextBlock Grid.Row="2" Grid.Column="0" Text="娴侀噺楂樻姤璀:" VerticalAlignment="Center" Margin="5" Foreground="White"/>
+                            <NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding FlowHighAlarm}"
+                                           Minimum="0" Maximum="1000" FormatString="F1" Margin="5"/>
+                            <TextBlock Grid.Row="2" Grid.Column="2" Text="m鲁/h" VerticalAlignment="Center" Margin="5" Foreground="White"/>
+                        </Grid>
+                    </StackPanel>
+                </Border>
+
+                <!-- 鎿嶄綔鎸夐挳 -->
+                <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Spacing="20">
+                    <Button Content="淇濆瓨璁剧疆" Command="{Binding SaveConfigCommand}" Width="120" Height="35"/>
+                    <Button Content="鎭㈠榛樿" Command="{Binding ResetToDefaultCommand}" Width="120" Height="35"/>
+                </StackPanel>
+            </Grid>
+        </ScrollViewer>
+    </Border>
+</UserControl>

+ 11 - 0
src/YZWater.Avalonia/Views/ViewBView.axaml.cs

@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace YZWater.Avalonia.Views;
+
+public partial class ViewBView : UserControl
+{
+    public ViewBView()
+    {
+        InitializeComponent();
+    }
+}

+ 72 - 0
src/YZWater.Avalonia/Views/ViewCView.axaml

@@ -0,0 +1,72 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:vm="using:YZWater.Core.ViewModels"
+             xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
+             x:Class="YZWater.Avalonia.Views.ViewCView"
+             x:DataType="vm:ViewCViewModel">
+
+    <UserControl.DataContext>
+        <vm:ViewCViewModel/>
+    </UserControl.DataContext>
+
+    <Border Background="#1a1a2e" Width="1024" Height="613">
+        <Grid RowDefinitions="Auto,*,Auto" Margin="20">
+            <!-- 鏃ユ湡绛涢 -->
+            <Border Grid.Row="0" Background="#2d2d44" CornerRadius="8" Padding="15" Margin="0,0,0,15">
+                <StackPanel>
+                    <TextBlock Text="娴侀噺璁板綍鏌ヨ" FontWeight="Bold" FontSize="18" Foreground="White" Margin="0,0,0,15"/>
+                    <StackPanel Orientation="Horizontal" Spacing="20">
+                        <StackPanel Orientation="Horizontal" Spacing="5">
+                            <TextBlock Text="寮濮嬫棩鏈:" VerticalAlignment="Center" Foreground="White"/>
+                            <DatePicker SelectedDate="{Binding StartDate}" Width="150"/>
+                        </StackPanel>
+                        <StackPanel Orientation="Horizontal" Spacing="5">
+                            <TextBlock Text="缁撴潫鏃ユ湡:" VerticalAlignment="Center" Foreground="White"/>
+                            <DatePicker SelectedDate="{Binding EndDate}" Width="150"/>
+                        </StackPanel>
+                        <Button Content="鏌ヨ" Command="{Binding LoadFlowRecordsCommand}"/>
+                        <Button Content="瀵煎嚭鏁版嵁" Command="{Binding ExportDataCommand}"/>
+                        <Button Content="娓呴櫎鏃ф暟鎹" Command="{Binding ClearOldDataCommand}"/>
+                    </StackPanel>
+                </StackPanel>
+            </Border>
+
+            <!-- 鍥捐〃鍖哄煙 -->
+            <Border Grid.Row="1" Background="#2d2d44" CornerRadius="8" Padding="15" Margin="0,0,0,15">
+                <ScrollViewer>
+                    <StackPanel>
+                        <TextBlock Text="杩涙按娴侀噺瓒嬪娍" FontWeight="Bold" Foreground="White" Margin="0,0,0,10"/>
+                        <lvc:CartesianChart Series="{Binding InflowSeries}"
+                                            XAxes="{Binding XAxes}"
+                                            YAxes="{Binding YAxes}"
+                                            Height="200"/>
+
+                        <TextBlock Text="鍑烘按娴侀噺瓒嬪娍" FontWeight="Bold" Foreground="White" Margin="0,15,0,10"/>
+                        <lvc:CartesianChart Series="{Binding OutflowSeries}"
+                                            XAxes="{Binding XAxes}"
+                                            YAxes="{Binding YAxes}"
+                                            Height="200"/>
+                    </StackPanel>
+                </ScrollViewer>
+            </Border>
+
+            <!-- 鏁版嵁琛ㄦ牸 -->
+            <Border Grid.Row="2" Background="#2d2d44" CornerRadius="8" Padding="15">
+                <DataGrid ItemsSource="{Binding FlowRecords}"
+                          AutoGenerateColumns="False"
+                          IsReadOnly="True"
+                          Height="150"
+                          Background="#1a1a2e"
+                          Foreground="White">
+                    <DataGrid.Columns>
+                        <DataGridTextColumn Header="鏃堕棿" Binding="{Binding RecordTime, StringFormat='{}{0:yyyy-MM-dd HH:mm:ss}'}" Width="150"/>
+                        <DataGridTextColumn Header="杩涙按娴侀噺(m鲁/h)" Binding="{Binding InflowRate, StringFormat='{}{0:F1}'}" Width="120"/>
+                        <DataGridTextColumn Header="鍑烘按娴侀噺(m鲁/h)" Binding="{Binding OutflowRate, StringFormat='{}{0:F1}'}" Width="120"/>
+                        <DataGridTextColumn Header="绱杩涙按閲(m鲁)" Binding="{Binding TotalInflow, StringFormat='{}{0:F1}'}" Width="120"/>
+                        <DataGridTextColumn Header="绱鍑烘按閲(m鲁)" Binding="{Binding TotalOutflow, StringFormat='{}{0:F1}'}" Width="120"/>
+                    </DataGrid.Columns>
+                </DataGrid>
+            </Border>
+        </Grid>
+    </Border>
+</UserControl>

+ 11 - 0
src/YZWater.Avalonia/Views/ViewCView.axaml.cs

@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace YZWater.Avalonia.Views;
+
+public partial class ViewCView : UserControl
+{
+    public ViewCView()
+    {
+        InitializeComponent();
+    }
+}

+ 93 - 0
src/YZWater.Avalonia/Views/ViewDView.axaml

@@ -0,0 +1,93 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:vm="using:YZWater.Core.ViewModels"
+             x:Class="YZWater.Avalonia.Views.ViewDView"
+             x:DataType="vm:ViewDViewModel">
+
+    <UserControl.DataContext>
+        <vm:ViewDViewModel/>
+    </UserControl.DataContext>
+
+    <Border Background="#1a1a2e" Width="1024" Height="613">
+        <Grid RowDefinitions="Auto,*,Auto" Margin="20">
+            <!-- 鏌ヨ鏉′欢 -->
+            <Border Grid.Row="0" Background="#2d2d44" CornerRadius="8" Padding="15" Margin="0,0,0,15">
+                <StackPanel>
+                    <TextBlock Text="鎶ヨ璁板綍鏌ヨ" FontWeight="Bold" FontSize="18" Foreground="White" Margin="0,0,0,15"/>
+                    <StackPanel Orientation="Horizontal" Spacing="20">
+                        <StackPanel Orientation="Horizontal" Spacing="5">
+                            <TextBlock Text="寮濮嬫棩鏈:" VerticalAlignment="Center" Foreground="White"/>
+                            <DatePicker SelectedDate="{Binding StartDate}" Width="150"/>
+                        </StackPanel>
+                        <StackPanel Orientation="Horizontal" Spacing="5">
+                            <TextBlock Text="缁撴潫鏃ユ湡:" VerticalAlignment="Center" Foreground="White"/>
+                            <DatePicker SelectedDate="{Binding EndDate}" Width="150"/>
+                        </StackPanel>
+                        <Button Content="鏌ヨ" Command="{Binding LoadAlarmRecordsCommand}"/>
+                        <Button Content="瀵煎嚭璁板綍" Command="{Binding ExportAlarmsCommand}"/>
+                        <Button Content="娓呴櫎鍘嗗彶" Command="{Binding ClearHistoryCommand}"/>
+                    </StackPanel>
+
+                    <!-- 鏈‘璁ゆ姤璀︽暟 -->
+                    <Border Background="#F44336" CornerRadius="5" Padding="10" Margin="0,10,0,0"
+                            IsVisible="{Binding UnconfirmedCount}">
+                        <StackPanel Orientation="Horizontal" Spacing="10">
+                            <TextBlock Text="鈿狅笍" FontSize="16"/>
+                            <TextBlock Text="{Binding UnconfirmedCount, StringFormat='鏈‘璁ゆ姤璀: {0} 鏉'}"
+                                       Foreground="White" FontWeight="Bold"/>
+                            <Button Content="鍏ㄩ儴纭" Command="{Binding ConfirmAllAlarmsCommand}"
+                                    Margin="20,0,0,0"/>
+                        </StackPanel>
+                    </Border>
+                </StackPanel>
+            </Border>
+
+            <!-- 鎶ヨ鍒楄〃 -->
+            <Border Grid.Row="1" Background="#2d2d44" CornerRadius="8" Padding="15" Margin="0,0,0,15">
+                <DataGrid ItemsSource="{Binding AlarmRecords}"
+                          AutoGenerateColumns="False"
+                          IsReadOnly="True"
+                          SelectedItem="{Binding SelectedRecord}"
+                          Background="#1a1a2e"
+                          Foreground="White">
+                    <DataGrid.Columns>
+                        <DataGridTextColumn Header="鏃堕棿" Binding="{Binding AlarmTime, StringFormat='{}{0:yyyy-MM-dd HH:mm:ss}'}" Width="150"/>
+                        <DataGridTextColumn Header="绫诲瀷" Binding="{Binding AlarmType}" Width="100"/>
+                        <DataGridTextColumn Header="鍐呭" Binding="{Binding AlarmMessage}" Width="200"/>
+                        <DataGridTextColumn Header="鎶ヨ鍊" Binding="{Binding AlarmValue, StringFormat='{}{0:F1}'}" Width="80"/>
+                        <DataGridTextColumn Header="绾у埆" Binding="{Binding AlarmLevel}" Width="60"/>
+                        <DataGridCheckBoxColumn Header="宸茬‘璁" Binding="{Binding IsConfirmed}" Width="80"/>
+                        <DataGridTextColumn Header="纭鏃堕棿" Binding="{Binding ConfirmedTime, StringFormat='{}{0:yyyy-MM-dd HH:mm:ss}'}" Width="150"/>
+                        <DataGridTextColumn Header="纭浜" Binding="{Binding ConfirmedBy}" Width="80"/>
+                    </DataGrid.Columns>
+                </DataGrid>
+            </Border>
+
+            <!-- 鎶ヨ璇︽儏 -->
+            <Border Grid.Row="2" Background="#2d2d44" CornerRadius="8" Padding="15"
+                    IsVisible="{Binding SelectedRecord, Converter={x:Static ObjectConverters.IsNotNull}}">
+                <StackPanel>
+                    <TextBlock Text="鎶ヨ璇︽儏" FontWeight="Bold" FontSize="16" Foreground="White" Margin="0,0,0,10"/>
+                    <Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,Auto">
+                        <TextBlock Grid.Row="0" Grid.Column="0" Text="鏃堕棿:" Margin="5" Foreground="White"/>
+                        <TextBlock Grid.Row="0" Grid.Column="1"
+                                   Text="{Binding SelectedRecord.AlarmTime, StringFormat='{}{0:yyyy-MM-dd HH:mm:ss}'}"
+                                   Margin="5" Foreground="White"/>
+
+                        <TextBlock Grid.Row="1" Grid.Column="0" Text="绫诲瀷:" Margin="5" Foreground="White"/>
+                        <TextBlock Grid.Row="1" Grid.Column="1"
+                                   Text="{Binding SelectedRecord.AlarmType}" Margin="5" Foreground="White"/>
+
+                        <TextBlock Grid.Row="2" Grid.Column="0" Text="鍐呭:" Margin="5" Foreground="White"/>
+                        <TextBlock Grid.Row="2" Grid.Column="1"
+                                   Text="{Binding SelectedRecord.AlarmMessage}" Margin="5" Foreground="White"/>
+
+                        <TextBlock Grid.Row="3" Grid.Column="0" Text="绾у埆:" Margin="5" Foreground="White"/>
+                        <TextBlock Grid.Row="3" Grid.Column="1"
+                                   Text="{Binding SelectedRecord.AlarmLevel, StringFormat='{}Level {0}'}" Margin="5" Foreground="White"/>
+                    </Grid>
+                </StackPanel>
+            </Border>
+        </Grid>
+    </Border>
+</UserControl>

+ 11 - 0
src/YZWater.Avalonia/Views/ViewDView.axaml.cs

@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace YZWater.Avalonia.Views;
+
+public partial class ViewDView : UserControl
+{
+    public ViewDView()
+    {
+        InitializeComponent();
+    }
+}

+ 65 - 0
src/YZWater.Avalonia/Views/ViewEView.axaml

@@ -0,0 +1,65 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:vm="using:YZWater.Core.ViewModels"
+             x:Class="YZWater.Avalonia.Views.ViewEView"
+             x:DataType="vm:ViewEViewModel">
+
+    <UserControl.DataContext>
+        <vm:ViewEViewModel/>
+    </UserControl.DataContext>
+
+    <Border Background="#1a1a2e" Width="1024" Height="613">
+        <ScrollViewer>
+            <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="30" Margin="50">
+                <!-- Logo 鍜岀郴缁熷悕绉 -->
+                <StackPanel HorizontalAlignment="Center" Spacing="15">
+                    <TextBlock Text="馃彮" FontSize="64" HorizontalAlignment="Center"/>
+                    <TextBlock Text="{Binding SystemName}"
+                               FontSize="32" FontWeight="Bold"
+                               HorizontalAlignment="Center"
+                               Foreground="White"/>
+                    <TextBlock Text="{Binding Version, StringFormat='Version {0}'}"
+                               FontSize="16" Foreground="Gray"
+                               HorizontalAlignment="Center"/>
+                </StackPanel>
+
+                <!-- 绯荤粺鎻忚堪 -->
+                <Border Background="#2d2d44" CornerRadius="10" Padding="20" MaxWidth="500">
+                    <StackPanel Spacing="10">
+                        <TextBlock Text="绯荤粺绠浠" FontWeight="Bold" FontSize="16" Foreground="White"/>
+                        <TextBlock Text="{Binding Description}" TextWrapping="Wrap" Foreground="#cccccc"/>
+                        <TextBlock Text="{Binding TechStack}" Foreground="Gray"/>
+                    </StackPanel>
+                </Border>
+
+                <!-- 鍏徃淇℃伅 -->
+                <Border Background="#2d2d44" CornerRadius="10" Padding="20" MaxWidth="500">
+                    <StackPanel Spacing="15">
+                        <TextBlock Text="鍏徃淇℃伅" FontWeight="Bold" FontSize="16" Foreground="White"/>
+                        <Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto">
+                            <TextBlock Grid.Row="0" Grid.Column="0" Text="鍏徃鍚嶇О:" Margin="0,0,10,0" Foreground="White"/>
+                            <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding CompanyName}" Foreground="#cccccc"/>
+
+                            <TextBlock Grid.Row="1" Grid.Column="0" Text="鑱旂郴浜:" Margin="0,0,10,0" Foreground="White"/>
+                            <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding ContactPerson}" Foreground="#cccccc"/>
+
+                            <TextBlock Grid.Row="2" Grid.Column="0" Text="鑱旂郴鐢佃瘽:" Margin="0,0,10,0" Foreground="White"/>
+                            <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding ContactPhone}" Foreground="#cccccc"/>
+                        </Grid>
+                    </StackPanel>
+                </Border>
+
+                <!-- 鎿嶄綔鎸夐挳 -->
+                <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="20">
+                    <Button Content="璁块棶瀹樼綉" Command="{Binding OpenWebsiteCommand}" Width="120" Height="35"/>
+                    <Button Content="妫鏌ユ洿鏂" Command="{Binding CheckUpdateCommand}" Width="120" Height="35"/>
+                </StackPanel>
+
+                <!-- 鐗堟潈淇℃伅 -->
+                <TextBlock Text="{Binding Copyright}"
+                           Foreground="Gray" FontSize="12"
+                           HorizontalAlignment="Center" Margin="0,20,0,0"/>
+            </StackPanel>
+        </ScrollViewer>
+    </Border>
+</UserControl>

+ 11 - 0
src/YZWater.Avalonia/Views/ViewEView.axaml.cs

@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace YZWater.Avalonia.Views;
+
+public partial class ViewEView : UserControl
+{
+    public ViewEView()
+    {
+        InitializeComponent();
+    }
+}

+ 35 - 0
src/YZWater.Avalonia/YZWater.Avalonia.csproj

@@ -0,0 +1,35 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>WinExe</OutputType>
+    <TargetFramework>net8.0</TargetFramework>
+    <Nullable>enable</Nullable>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
+    <ApplicationManifest>app.manifest</ApplicationManifest>
+    <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
+    <ApplicationIcon>Assets\姹℃按澶勭悊鍘.ico</ApplicationIcon>
+    <RootNamespace>YZWater.Avalonia</RootNamespace>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Avalonia" Version="11.2.3" />
+    <PackageReference Include="Avalonia.Desktop" Version="11.2.3" />
+    <PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.3" />
+    <PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.3" />
+    <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
+    <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.3" />
+    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
+    <PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.0-rc4.5" />
+    <PackageReference Include="Semi.Avalonia" Version="11.2.1.4" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\YZWater.Core\YZWater.Core.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Resource Include="Assets\姹℃按澶勭悊鍘.ico" />
+  </ItemGroup>
+
+</Project>

+ 25 - 0
src/YZWater.Avalonia/app.manifest

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
+  <assemblyIdentity version="1.0.0.0" name="YZWater.Avalonia.Desktop"/>
+  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
+    <security>
+      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
+        <requestedExecutionLevel level="asInvoker" uiAccess="false" />
+      </requestedPrivileges>
+    </security>
+  </trustInfo>
+
+  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+    <application>
+      <!-- Windows 10 and Windows 11 -->
+      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
+    </application>
+  </compatibility>
+
+  <application xmlns="urn:schemas-microsoft-com:asm.v3">
+    <windowsSettings>
+      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
+      <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
+    </windowsSettings>
+  </application>
+</assembly>

+ 63 - 0
src/YZWater.Core/Models/AlarmRecord.cs

@@ -0,0 +1,63 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using FreeSql.DataAnnotations;
+
+namespace YZWater.Core.Models;
+
+/// <summary>
+/// 鎶ヨ璁板綍妯″瀷
+/// </summary>
+[Table(Name = "alarm_records")]
+public partial class AlarmRecord : ObservableObject
+{
+    /// <summary>
+    /// 涓婚敭 ID
+    /// </summary>
+    [Column(IsIdentity = true, IsPrimary = true)]
+    public int Id { get; set; }
+
+    /// <summary>
+    /// 鎶ヨ鏃堕棿
+    /// </summary>
+    public DateTime AlarmTime { get; set; } = DateTime.Now;
+
+    /// <summary>
+    /// 鎶ヨ绫诲瀷
+    /// </summary>
+    [ObservableProperty]
+    private string _alarmType = string.Empty;
+
+    /// <summary>
+    /// 鎶ヨ鍐呭
+    /// </summary>
+    [ObservableProperty]
+    private string _alarmMessage = string.Empty;
+
+    /// <summary>
+    /// 鎶ヨ鍊
+    /// </summary>
+    [ObservableProperty]
+    private float _alarmValue;
+
+    /// <summary>
+    /// 鎶ヨ绾у埆 (1-5)
+    /// </summary>
+    [ObservableProperty]
+    private int _alarmLevel = 1;
+
+    /// <summary>
+    /// 鏄惁宸茬‘璁
+    /// </summary>
+    [ObservableProperty]
+    private bool _isConfirmed;
+
+    /// <summary>
+    /// 纭鏃堕棿
+    /// </summary>
+    public DateTime? ConfirmedTime { get; set; }
+
+    /// <summary>
+    /// 纭浜
+    /// </summary>
+    [ObservableProperty]
+    private string? _confirmedBy;
+}

+ 83 - 0
src/YZWater.Core/Models/EquipmentStatus.cs

@@ -0,0 +1,83 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace YZWater.Core.Models;
+
+/// <summary>
+/// 璁惧鐘舵佹灇涓
+/// </summary>
+public enum DeviceStatus
+{
+    /// <summary>
+    /// 鍋滄
+    /// </summary>
+    Stopped,
+
+    /// <summary>
+    /// 杩愯
+    /// </summary>
+    Running,
+
+    /// <summary>
+    /// 鏁呴殰
+    /// </summary>
+    Fault,
+
+    /// <summary>
+    /// 缁存姢
+    /// </summary>
+    Maintenance
+}
+
+/// <summary>
+/// 璁惧鐘舵佹ā鍨
+/// </summary>
+public partial class EquipmentStatus : ObservableObject
+{
+    /// <summary>
+    /// 璁惧 ID
+    /// </summary>
+    [ObservableProperty]
+    private string _deviceId = string.Empty;
+
+    /// <summary>
+    /// 璁惧鍚嶇О
+    /// </summary>
+    [ObservableProperty]
+    private string _deviceName = string.Empty;
+
+    /// <summary>
+    /// 璁惧鐘舵
+    /// </summary>
+    [ObservableProperty]
+    private DeviceStatus _status = DeviceStatus.Stopped;
+
+    /// <summary>
+    /// 杩愯鏃堕棿 (灏忔椂)
+    /// </summary>
+    [ObservableProperty]
+    private float _runningHours;
+
+    /// <summary>
+    /// 鏄惁鍦ㄧ嚎
+    /// </summary>
+    [ObservableProperty]
+    private bool _isOnline;
+
+    /// <summary>
+    /// 鏈鍚庢洿鏂版椂闂
+    /// </summary>
+    [ObservableProperty]
+    private DateTime _lastUpdateTime = DateTime.Now;
+
+    /// <summary>
+    /// 璇绘暟/鏁板
+    /// </summary>
+    [ObservableProperty]
+    private float _value;
+
+    /// <summary>
+    /// 鍗曚綅
+    /// </summary>
+    [ObservableProperty]
+    private string _unit = string.Empty;
+}

+ 52 - 0
src/YZWater.Core/Models/FlowRecord.cs

@@ -0,0 +1,52 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using FreeSql.DataAnnotations;
+
+namespace YZWater.Core.Models;
+
+/// <summary>
+/// 娴侀噺璁板綍妯″瀷
+/// </summary>
+[Table(Name = "flow_records")]
+public partial class FlowRecord : ObservableObject
+{
+    /// <summary>
+    /// 涓婚敭 ID
+    /// </summary>
+    [Column(IsIdentity = true, IsPrimary = true)]
+    public int Id { get; set; }
+
+    /// <summary>
+    /// 璁板綍鏃堕棿
+    /// </summary>
+    public DateTime RecordTime { get; set; } = DateTime.Now;
+
+    /// <summary>
+    /// 杩涙按娴侀噺 (m鲁/h)
+    /// </summary>
+    [ObservableProperty]
+    private float _inflowRate;
+
+    /// <summary>
+    /// 鍑烘按娴侀噺 (m鲁/h)
+    /// </summary>
+    [ObservableProperty]
+    private float _outflowRate;
+
+    /// <summary>
+    /// 绱杩涙按閲 (m鲁)
+    /// </summary>
+    [ObservableProperty]
+    private float _totalInflow;
+
+    /// <summary>
+    /// 绱鍑烘按閲 (m鲁)
+    /// </summary>
+    [ObservableProperty]
+    private float _totalOutflow;
+
+    /// <summary>
+    /// 澶囨敞
+    /// </summary>
+    [ObservableProperty]
+    private string? _remark;
+}

+ 39 - 0
src/YZWater.Core/Models/Person.cs

@@ -0,0 +1,39 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using FreeSql.DataAnnotations;
+
+namespace YZWater.Core.Models;
+
+/// <summary>
+/// 浜哄憳妯″瀷
+/// </summary>
+[Table(Name = "persons")]
+public partial class Person : ObservableObject
+{
+    /// <summary>
+    /// 涓婚敭 ID
+    /// </summary>
+    [Column(IsIdentity = true, IsPrimary = true)]
+    public int Id { get; set; }
+
+    /// <summary>
+    /// 濮
+    /// </summary>
+    [ObservableProperty]
+    private string _firstName = string.Empty;
+
+    /// <summary>
+    /// 鍚
+    /// </summary>
+    [ObservableProperty]
+    private string _lastName = string.Empty;
+
+    /// <summary>
+    /// 鍒涘缓鏃堕棿
+    /// </summary>
+    public DateTime CreatedTime { get; set; } = DateTime.Now;
+
+    /// <summary>
+    /// 鍏ㄥ悕
+    /// </summary>
+    public string FullName => $"{FirstName} {LastName}";
+}

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

@@ -0,0 +1,75 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace YZWater.Core.Models;
+
+/// <summary>
+/// 绯荤粺閰嶇疆妯″瀷
+/// </summary>
+public partial class SystemConfig : ObservableObject
+{
+    /// <summary>
+    /// PLC IP 鍦板潃
+    /// </summary>
+    [ObservableProperty]
+    private string _plcIp = "192.168.0.150";
+
+    /// <summary>
+    /// PLC 绔彛
+    /// </summary>
+    [ObservableProperty]
+    private int _plcPort = 5000;
+
+    /// <summary>
+    /// 鏄惁鑷姩杩炴帴
+    /// </summary>
+    [ObservableProperty]
+    private bool _autoConnect = true;
+
+    /// <summary>
+    /// 娑蹭綅楂樻姤璀﹀
+    /// </summary>
+    [ObservableProperty]
+    private float _levelHighAlarm = 80f;
+
+    /// <summary>
+    /// 娑蹭綅浣庢姤璀﹀
+    /// </summary>
+    [ObservableProperty]
+    private float _levelLowAlarm = 20f;
+
+    /// <summary>
+    /// 娴侀噺楂樻姤璀﹀
+    /// </summary>
+    [ObservableProperty]
+    private float _flowHighAlarm = 100f;
+
+    /// <summary>
+    /// 娉甸鐜囪缃
+    /// </summary>
+    [ObservableProperty]
+    private float _pumpFrequency = 50f;
+
+    /// <summary>
+    /// 绯荤粺鍚嶇О
+    /// </summary>
+    [ObservableProperty]
+    private string _systemName = "姹℃按澶勭悊鐩戞帶绯荤粺";
+
+    /// <summary>
+    /// 鍏徃鍚嶇О
+    /// </summary>
+    [ObservableProperty]
+    private string _companyName = "鎵窞鏃僵绉戞妧鏈夐檺鍏徃";
+
+    /// <summary>
+    /// 鑱旂郴浜
+    /// </summary>
+    [ObservableProperty]
+    private string _contactPerson = "璧电粡鐞";
+
+    /// <summary>
+    /// 鑱旂郴鐢佃瘽
+    /// </summary>
+    [ObservableProperty]
+    private string _contactPhone = "18115099090";
+}

+ 94 - 0
src/YZWater.Core/Services/ConfigService.cs

@@ -0,0 +1,94 @@
+using System.Text.Json;
+using Serilog;
+using YZWater.Core.Models;
+
+namespace YZWater.Core.Services;
+
+/// <summary>
+/// 閰嶇疆鏈嶅姟 - 绠$悊搴旂敤绋嬪簭閰嶇疆
+/// </summary>
+public static class ConfigService
+{
+    private static readonly string ConfigFilePath = "yzwater-config.json";
+    private static SystemConfig? _config;
+
+    /// <summary>
+    /// 鑾峰彇閰嶇疆
+    /// </summary>
+    public static SystemConfig? GetConfig()
+    {
+        if (_config == null)
+        {
+            LoadConfig();
+        }
+        return _config;
+    }
+
+    /// <summary>
+    /// 鍔犺浇閰嶇疆鏂囦欢
+    /// </summary>
+    public static void LoadConfig()
+    {
+        try
+        {
+            if (File.Exists(ConfigFilePath))
+            {
+                var json = File.ReadAllText(ConfigFilePath);
+                _config = JsonSerializer.Deserialize<SystemConfig>(json);
+                Log.Information("閰嶇疆鏂囦欢鍔犺浇鎴愬姛");
+            }
+            else
+            {
+                // 鍒涘缓榛樿閰嶇疆
+                _config = new SystemConfig
+                {
+                    PlcIp = "192.168.0.150",
+                    PlcPort = 5000,
+                    AutoConnect = true,
+                    LevelHighAlarm = 80f,
+                    LevelLowAlarm = 20f,
+                    FlowHighAlarm = 100f
+                };
+                SaveConfig();
+                Log.Information("宸插垱寤洪粯璁ら厤缃枃浠");
+            }
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "鍔犺浇閰嶇疆鏂囦欢澶辫触");
+            _config = new SystemConfig();
+        }
+    }
+
+    /// <summary>
+    /// 淇濆瓨閰嶇疆鏂囦欢
+    /// </summary>
+    public static void SaveConfig()
+    {
+        try
+        {
+            if (_config == null) return;
+
+            var options = new JsonSerializerOptions
+            {
+                WriteIndented = true
+            };
+            var json = JsonSerializer.Serialize(_config, options);
+            File.WriteAllText(ConfigFilePath, json);
+            Log.Information("閰嶇疆鏂囦欢淇濆瓨鎴愬姛");
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "淇濆瓨閰嶇疆鏂囦欢澶辫触");
+        }
+    }
+
+    /// <summary>
+    /// 鏇存柊閰嶇疆
+    /// </summary>
+    public static void UpdateConfig(SystemConfig config)
+    {
+        _config = config;
+        SaveConfig();
+    }
+}

+ 58 - 0
src/YZWater.Core/Services/DatabaseService.cs

@@ -0,0 +1,58 @@
+using FreeSql;
+using Serilog;
+using YZWater.Core.Models;
+
+namespace YZWater.Core.Services;
+
+/// <summary>
+/// 鏁版嵁搴撴湇鍔 - 浣跨敤 FreeSql + SQLite
+/// </summary>
+public static class DatabaseService
+{
+    private static IFreeSql? _db;
+
+    /// <summary>
+    /// 鏁版嵁搴撳疄渚
+    /// </summary>
+    public static IFreeSql Db => _db ?? throw new InvalidOperationException("鏁版嵁搴撴湭鍒濆鍖栵紝璇峰厛璋冪敤 Initialize()");
+
+    /// <summary>
+    /// 鍒濆鍖栨暟鎹簱
+    /// </summary>
+    public static void Initialize()
+    {
+        // 閰嶇疆 Serilog
+        Log.Logger = new LoggerConfiguration()
+            .MinimumLevel.Debug()
+            .WriteTo.Console()
+            .WriteTo.File("Logs/log-.txt", rollingInterval: RollingInterval.Day)
+            .CreateLogger();
+
+        // 鍒濆鍖 FreeSql
+        _db = new FreeSqlBuilder()
+            .UseConnectionString(DataType.Sqlite, "Data Source=yzwater.db")
+            .UseAutoSyncStructure(true)
+            .UseMonitorCommand(cmd =>
+            {
+                Log.Debug("SQL: {Sql}", cmd.CommandText);
+            })
+            .Build();
+
+        // 纭繚鏁版嵁搴撳凡鍒涘缓
+        _db.CodeFirst.SyncStructure(typeof(Person));
+        _db.CodeFirst.SyncStructure(typeof(AlarmRecord));
+        _db.CodeFirst.SyncStructure(typeof(FlowRecord));
+        _db.CodeFirst.SyncStructure(typeof(SystemConfig));
+
+        Log.Information("鏁版嵁搴撳垵濮嬪寲瀹屾垚");
+    }
+
+    /// <summary>
+    /// 閲婃斁璧勬簮
+    /// </summary>
+    public static void Dispose()
+    {
+        _db?.Dispose();
+        Log.CloseAndFlush();
+    }
+}

+ 193 - 0
src/YZWater.Core/Services/PlcService.cs

@@ -0,0 +1,193 @@
+using HslCommunication;
+using HslCommunication.Profinet.Siemens;
+using Serilog;
+
+namespace YZWater.Core.Services;
+
+/// <summary>
+/// PLC 閫氫俊鏈嶅姟 - 浣跨敤 HslCommunication
+/// </summary>
+public static class PlcService
+{
+    private static SiemensS7Net? _plc;
+    private static bool _isConnected;
+
+    /// <summary>
+    /// PLC 杩炴帴鐘舵
+    /// </summary>
+    public static bool IsConnected => _isConnected;
+
+    /// <summary>
+    /// PLC 瀹炰緥
+    /// </summary>
+    public static SiemensS7Net Plc => _plc ?? throw new InvalidOperationException("PLC 鏈垵濮嬪寲锛岃鍏堣皟鐢 Initialize()");
+
+    /// <summary>
+    /// 鍒濆鍖 PLC 杩炴帴
+    /// </summary>
+    public static void Initialize()
+    {
+        try
+        {
+            // 浠庨厤缃鍙 PLC IP锛岄粯璁や负 192.168.0.150
+            var config = ConfigService.GetConfig();
+            var plcIp = config?.PlcIp ?? "192.168.0.150";
+
+            _plc = new SiemensS7Net(SiemensPLCS.S200Smart, plcIp)
+            {
+                ConnectTimeOut = 3000,
+                ReceiveTimeOut = 3000
+            };
+
+            Log.Information("PLC 鏈嶅姟鍒濆鍖栧畬鎴愶紝IP: {Ip}", plcIp);
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "PLC 鏈嶅姟鍒濆鍖栧け璐");
+        }
+    }
+
+    /// <summary>
+    /// 杩炴帴 PLC
+    /// </summary>
+    public static async Task<bool> ConnectAsync()
+    {
+        if (_plc == null)
+        {
+            Log.Warning("PLC 鏈垵濮嬪寲");
+            return false;
+        }
+
+        try
+        {
+            var result = await _plc.ConnectServerAsync();
+            _isConnected = result.IsSuccess;
+            if (_isConnected)
+            {
+                Log.Information("PLC 杩炴帴鎴愬姛");
+            }
+            else
+            {
+                Log.Warning("PLC 杩炴帴澶辫触: {Message}", result.Message);
+            }
+            return _isConnected;
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "PLC 杩炴帴寮傚父");
+            _isConnected = false;
+            return false;
+        }
+    }
+
+    /// <summary>
+    /// 鏂紑 PLC 杩炴帴
+    /// </summary>
+    public static void Disconnect()
+    {
+        _plc?.ConnectClose();
+        _isConnected = false;
+        Log.Information("PLC 宸叉柇寮杩炴帴");
+    }
+
+    /// <summary>
+    /// 璇诲彇娴偣鏁
+    /// </summary>
+    public static async Task<float> ReadFloatAsync(string address)
+    {
+        if (_plc == null || !_isConnected)
+        {
+            Log.Warning("PLC 鏈繛鎺ワ紝鏃犳硶璇诲彇鏁版嵁");
+            return 0f;
+        }
+
+        try
+        {
+            var result = await _plc.ReadFloatAsync(address);
+            return result.IsSuccess ? result.Content : 0f;
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "璇诲彇 PLC 鏁版嵁澶辫触锛屽湴鍧: {Address}", address);
+            return 0f;
+        }
+    }
+
+    /// <summary>
+    /// 鍐欏叆娴偣鏁
+    /// </summary>
+    public static async Task<bool> WriteFloatAsync(string address, float value)
+    {
+        if (_plc == null || !_isConnected)
+        {
+            Log.Warning("PLC 鏈繛鎺ワ紝鏃犳硶鍐欏叆鏁版嵁");
+            return false;
+        }
+
+        try
+        {
+            var result = await _plc.WriteAsync(address, value);
+            return result.IsSuccess;
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "鍐欏叆 PLC 鏁版嵁澶辫触锛屽湴鍧: {Address}, 鍊: {Value}", address, value);
+            return false;
+        }
+    }
+
+    /// <summary>
+    /// 璇诲彇甯冨皵鍊
+    /// </summary>
+    public static async Task<bool> ReadBoolAsync(string address)
+    {
+        if (_plc == null || !_isConnected)
+        {
+            Log.Warning("PLC 鏈繛鎺ワ紝鏃犳硶璇诲彇鏁版嵁");
+            return false;
+        }
+
+        try
+        {
+            var result = await _plc.ReadBoolAsync(address);
+            return result.IsSuccess && result.Content;
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "璇诲彇 PLC 鏁版嵁澶辫触锛屽湴鍧: {Address}", address);
+            return false;
+        }
+    }
+
+    /// <summary>
+    /// 鍐欏叆甯冨皵鍊
+    /// </summary>
+    public static async Task<bool> WriteBoolAsync(string address, bool value)
+    {
+        if (_plc == null || !_isConnected)
+        {
+            Log.Warning("PLC 鏈繛鎺ワ紝鏃犳硶鍐欏叆鏁版嵁");
+            return false;
+        }
+
+        try
+        {
+            var result = await _plc.WriteAsync(address, value);
+            return result.IsSuccess;
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "鍐欏叆 PLC 鏁版嵁澶辫触锛屽湴鍧: {Address}, 鍊: {Value}", address, value);
+            return false;
+        }
+    }
+
+    /// <summary>
+    /// 閲婃斁璧勬簮
+    /// </summary>
+    public static void Dispose()
+    {
+        Disconnect();
+        _plc = null;
+    }
+}

+ 65 - 0
src/YZWater.Core/Utils/Nlogger.cs

@@ -0,0 +1,65 @@
+using Serilog;
+
+namespace YZWater.Core.Utils;
+
+/// <summary>
+/// 鏃ュ織宸ュ叿绫 - 灏佽 Serilog
+/// </summary>
+public static class Nlogger
+{
+    /// <summary>
+    /// 璁板綍璋冭瘯淇℃伅
+    /// </summary>
+    public static void Debug(string message, params object[] args)
+    {
+        Log.Debug(message, args);
+    }
+
+    /// <summary>
+    /// 璁板綍鏅氫俊鎭
+    /// </summary>
+    public static void Info(string message, params object[] args)
+    {
+        Log.Information(message, args);
+    }
+
+    /// <summary>
+    /// 璁板綍璀﹀憡淇℃伅
+    /// </summary>
+    public static void Warn(string message, params object[] args)
+    {
+        Log.Warning(message, args);
+    }
+
+    /// <summary>
+    /// 璁板綍閿欒淇℃伅
+    /// </summary>
+    public static void Error(string message, params object[] args)
+    {
+        Log.Error(message, args);
+    }
+
+    /// <summary>
+    /// 璁板綍閿欒淇℃伅锛堝甫寮傚父锛
+    /// </summary>
+    public static void Error(Exception ex, string message, params object[] args)
+    {
+        Log.Error(ex, message, args);
+    }
+
+    /// <summary>
+    /// 璁板綍鑷村懡閿欒
+    /// </summary>
+    public static void Fatal(string message, params object[] args)
+    {
+        Log.Fatal(message, args);
+    }
+
+    /// <summary>
+    /// 璁板綍鑷村懡閿欒锛堝甫寮傚父锛
+    /// </summary>
+    public static void Fatal(Exception ex, string message, params object[] args)
+    {
+        Log.Fatal(ex, message, args);
+    }
+}

+ 127 - 0
src/YZWater.Core/Utils/ResolutionManager.cs

@@ -0,0 +1,127 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using System;
+
+namespace YZWater.Core.Utils;
+
+/// <summary>
+/// 鍒嗚鲸鐜囩鐞嗗櫒 - 鎻愪緵瑙f瀽搴﹁嚜閫傚簲鏀寔
+/// </summary>
+public partial class ResolutionManager : ObservableObject
+{
+    private static ResolutionManager? _instance;
+
+    /// <summary>
+    /// 鍗曚緥瀹炰緥
+    /// </summary>
+    public static ResolutionManager Instance => _instance ??= new ResolutionManager();
+
+    /// <summary>
+    /// 璁捐鍩哄噯瀹藉害
+    /// </summary>
+    public const double DesignWidth = 1024;
+
+    /// <summary>
+    /// 璁捐鍩哄噯楂樺害
+    /// </summary>
+    public const double DesignHeight = 768;
+
+    /// <summary>
+    /// 鏈灏忕缉鏀炬瘮渚
+    /// </summary>
+    public const double MinScale = 0.5;
+
+    /// <summary>
+    /// 鏈澶х缉鏀炬瘮渚
+    /// </summary>
+    public const double MaxScale = 2.0;
+
+    [ObservableProperty]
+    private double _currentWidth = DesignWidth;
+
+    [ObservableProperty]
+    private double _currentHeight = DesignHeight;
+
+    [ObservableProperty]
+    private double _scaleX = 1.0;
+
+    [ObservableProperty]
+    private double _scaleY = 1.0;
+
+    [ObservableProperty]
+    private double _uniformScale = 1.0;
+
+    [ObservableProperty]
+    private double _fontSizeScale = 1.0;
+
+    [ObservableProperty]
+    private double _marginScale = 1.0;
+
+    /// <summary>
+    /// 鏇存柊鍒嗚鲸鐜
+    /// </summary>
+    public void UpdateResolution(double width, double height)
+    {
+        CurrentWidth = width;
+        CurrentHeight = height;
+
+        // 璁$畻缂╂斁姣斾緥
+        ScaleX = width / DesignWidth;
+        ScaleY = height / DesignHeight;
+
+        // 浣跨敤缁熶竴缂╂斁姣斾緥锛堝彇杈冨皬鍊硷紝淇濊瘉鍐呭瀹屽叏鏄剧ず锛
+        UniformScale = Math.Min(ScaleX, ScaleY);
+        UniformScale = Math.Max(MinScale, Math.Min(MaxScale, UniformScale));
+
+        // 瀛椾綋缂╂斁姣斾緥锛堢◢寰繚瀹堜竴鐐癸紝閬垮厤鏂囧瓧杩囧ぇ锛
+        FontSizeScale = Math.Max(0.8, Math.Min(1.3, UniformScale));
+
+        // 杈硅窛缂╂斁姣斾緥
+        MarginScale = Math.Max(0.7, Math.Min(1.5, UniformScale));
+    }
+
+    /// <summary>
+    /// 鏍规嵁鍩哄噯灏哄璁$畻瀹為檯灏哄
+    /// </summary>
+    public double Scale(double baseSize)
+    {
+        return baseSize * UniformScale;
+    }
+
+    /// <summary>
+    /// 璁$畻瀛椾綋澶у皬
+    /// </summary>
+    public double ScaleFontSize(double baseFontSize)
+    {
+        return baseFontSize * FontSizeScale;
+    }
+
+    /// <summary>
+    /// 璁$畻杈硅窛
+    /// </summary>
+    public double ScaleMargin(double baseMargin)
+    {
+        return baseMargin * MarginScale;
+    }
+
+    /// <summary>
+    /// 鑾峰彇缂╂斁鍚庣殑灏哄
+    /// </summary>
+    public (double Width, double Height) GetScaledSize(double baseWidth, double baseHeight)
+    {
+        return (baseWidth * UniformScale, baseHeight * UniformScale);
+    }
+
+    /// <summary>
+    /// 鑾峰彇閫傞厤鍚庣殑绐楀彛灏哄
+    /// </summary>
+    public (double Width, double Height) GetFittedWindowSize(double screenWidth, double screenHeight, double padding = 50)
+    {
+        var availableWidth = screenWidth - padding * 2;
+        var availableHeight = screenHeight - padding * 2;
+
+        var scale = Math.Min(availableWidth / DesignWidth, availableHeight / DesignHeight);
+        scale = Math.Max(MinScale, Math.Min(MaxScale, scale));
+
+        return (DesignWidth * scale, DesignHeight * scale);
+    }
+}

+ 90 - 0
src/YZWater.Core/Utils/WinSize.cs

@@ -0,0 +1,90 @@
+namespace YZWater.Core.Utils;
+
+/// <summary>
+/// 绐楀彛灏哄宸ュ叿绫
+/// </summary>
+public static class WinSize
+{
+    /// <summary>
+    /// 鍩哄噯瀹藉害
+    /// </summary>
+    public const double BaseWidth = 1024;
+
+    /// <summary>
+    /// 鍩哄噯楂樺害
+    /// </summary>
+    public const double BaseHeight = 768;
+
+    /// <summary>
+    /// 璁$畻缂╂斁姣斾緥
+    /// </summary>
+    public static double GetScaleFactor(double actualWidth, double actualHeight)
+    {
+        var scaleX = actualWidth / BaseWidth;
+        var scaleY = actualHeight / BaseHeight;
+        return Math.Min(scaleX, scaleY);
+    }
+
+    /// <summary>
+    /// 鏍规嵁鍩哄噯灏哄璁$畻瀹為檯灏哄
+    /// </summary>
+    public static double CalculateSize(double baseSize, double scaleFactor)
+    {
+        return baseSize * scaleFactor;
+    }
+
+    /// <summary>
+    /// 璁$畻瀛椾綋澶у皬
+    /// </summary>
+    public static double CalculateFontSize(double baseFontSize, double scaleFactor)
+    {
+        var fontSize = baseFontSize * scaleFactor;
+        return Math.Max(12, Math.Min(24, fontSize)); // 闄愬埗鍦 12-24 涔嬮棿
+    }
+
+    /// <summary>
+    /// 璁$畻杈硅窛
+    /// </summary>
+    public static Thickness CalculateMargin(double baseMargin, double scaleFactor)
+    {
+        var margin = baseMargin * scaleFactor;
+        return new Thickness(margin);
+    }
+
+    /// <summary>
+    /// 璁$畻杈硅窛锛堝垎鍒寚瀹氬洓涓柟鍚戯級
+    /// </summary>
+    public static Thickness CalculateMargin(double left, double top, double right, double bottom, double scaleFactor)
+    {
+        return new Thickness(
+            left * scaleFactor,
+            top * scaleFactor,
+            right * scaleFactor,
+            bottom * scaleFactor
+        );
+    }
+}
+
+/// <summary>
+/// Thickness 缁撴瀯浣擄紙绠鍖栫増锛
+/// </summary>
+public struct Thickness
+{
+    public double Left { get; }
+    public double Top { get; }
+    public double Right { get; }
+    public double Bottom { get; }
+
+    public Thickness(double uniformLength)
+    {
+        Left = Top = Right = Bottom = uniformLength;
+    }
+
+    public Thickness(double left, double top, double right, double bottom)
+    {
+        Left = left;
+        Top = top;
+        Right = right;
+        Bottom = bottom;
+    }
+}

+ 134 - 0
src/YZWater.Core/ViewModels/MainViewModel.cs

@@ -0,0 +1,134 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Serilog;
+using YZWater.Core.Services;
+
+namespace YZWater.Core.ViewModels;
+
+/// <summary>
+/// 涓荤獥鍙 ViewModel
+/// </summary>
+public partial class MainViewModel : ObservableObject
+{
+    private readonly ViewAViewModel _viewAViewModel;
+    private readonly ViewBViewModel _viewBViewModel;
+    private readonly ViewCViewModel _viewCViewModel;
+    private readonly ViewDViewModel _viewDViewModel;
+    private readonly ViewEViewModel _viewEViewModel;
+
+    [ObservableProperty]
+    private ObservableObject _currentView;
+
+    [ObservableProperty]
+    private string _title = "姹℃按澶勭悊鐩戞帶绯荤粺";
+
+    [ObservableProperty]
+    private string _currentTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+
+    [ObservableProperty]
+    private bool _isPlcConnected;
+
+    [ObservableProperty]
+    private int _selectedTabIndex;
+
+    public MainViewModel()
+    {
+        _viewAViewModel = new ViewAViewModel();
+        _viewBViewModel = new ViewBViewModel();
+        _viewCViewModel = new ViewCViewModel();
+        _viewDViewModel = new ViewDViewModel();
+        _viewEViewModel = new ViewEViewModel();
+
+        _currentView = _viewAViewModel;
+
+        // 鍚姩瀹氭椂鍣ㄦ洿鏂版椂闂
+        StartTimer();
+    }
+
+    /// <summary>
+    /// 鍒囨崲鍒拌鍥 A (涓诲伐鑹)
+    /// </summary>
+    [RelayCommand]
+    private void ShowViewA()
+    {
+        CurrentView = _viewAViewModel;
+        SelectedTabIndex = 0;
+        Log.Debug("鍒囨崲鍒颁富宸ヨ壓瑙嗗浘");
+    }
+
+    /// <summary>
+    /// 鍒囨崲鍒拌鍥 B (鍙傛暟璁剧疆)
+    /// </summary>
+    [RelayCommand]
+    private void ShowViewB()
+    {
+        CurrentView = _viewBViewModel;
+        SelectedTabIndex = 1;
+        Log.Debug("鍒囨崲鍒板弬鏁拌缃鍥");
+    }
+
+    /// <summary>
+    /// 鍒囨崲鍒拌鍥 C (娴侀噺璁板綍)
+    /// </summary>
+    [RelayCommand]
+    private void ShowViewC()
+    {
+        CurrentView = _viewCViewModel;
+        SelectedTabIndex = 2;
+        Log.Debug("鍒囨崲鍒版祦閲忚褰曡鍥");
+    }
+
+    /// <summary>
+    /// 鍒囨崲鍒拌鍥 D (鎶ヨ璁板綍)
+    /// </summary>
+    [RelayCommand]
+    private void ShowViewD()
+    {
+        CurrentView = _viewDViewModel;
+        SelectedTabIndex = 3;
+        Log.Debug("鍒囨崲鍒版姤璀﹁褰曡鍥");
+    }
+
+    /// <summary>
+    /// 鍒囨崲鍒拌鍥 E (鍏充簬)
+    /// </summary>
+    [RelayCommand]
+    private void ShowViewE()
+    {
+        CurrentView = _viewEViewModel;
+        SelectedTabIndex = 4;
+        Log.Debug("鍒囨崲鍒板叧浜庤鍥");
+    }
+
+    /// <summary>
+    /// 杩炴帴 PLC
+    /// </summary>
+    [RelayCommand]
+    private async Task ConnectPlcAsync()
+    {
+        IsPlcConnected = await PlcService.ConnectAsync();
+    }
+
+    /// <summary>
+    /// 鏂紑 PLC
+    /// </summary>
+    [RelayCommand]
+    private void DisconnectPlc()
+    {
+        PlcService.Disconnect();
+        IsPlcConnected = false;
+    }
+
+    /// <summary>
+    /// 鍚姩瀹氭椂鍣
+    /// </summary>
+    private void StartTimer()
+    {
+        var timer = new System.Timers.Timer(1000);
+        timer.Elapsed += (s, e) =>
+        {
+            CurrentTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+        };
+        timer.Start();
+    }
+}

+ 416 - 0
src/YZWater.Core/ViewModels/ViewAViewModel.cs

@@ -0,0 +1,416 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Serilog;
+using YZWater.Core.Models;
+using YZWater.Core.Services;
+
+namespace YZWater.Core.ViewModels;
+
+/// <summary>
+/// 涓诲伐鑹鸿鍥 ViewModel
+/// </summary>
+public partial class ViewAViewModel : ObservableObject
+{
+    // 娑蹭綅鏁版嵁
+    [ObservableProperty]
+    private float _tank1Level = 50f;
+
+    [ObservableProperty]
+    private float _tank2Level = 60f;
+
+    [ObservableProperty]
+    private float _tank3Level = 70f;
+
+    [ObservableProperty]
+    private float _tank4Level = 45f;
+
+    // 娴侀噺鏁版嵁
+    [ObservableProperty]
+    private float _inflowRate = 25.5f;
+
+    [ObservableProperty]
+    private float _outflowRate = 22.3f;
+
+    // 娉电姸鎬
+    [ObservableProperty]
+    private bool _pump1Running;
+
+    [ObservableProperty]
+    private bool _pump2Running;
+
+    [ObservableProperty]
+    private bool _pump3Running;
+
+    [ObservableProperty]
+    private bool _pump4Running;
+
+    [ObservableProperty]
+    private bool _pump5Running;
+
+    // 椋庢墖鐘舵
+    [ObservableProperty]
+    private bool _fan1Running;
+
+    [ObservableProperty]
+    private bool _fan2Running;
+
+    // 闃闂ㄧ姸鎬
+    [ObservableProperty]
+    private ValveStatus _valve1Status = ValveStatus.Middle;
+
+    [ObservableProperty]
+    private ValveStatus _valve2Status = ValveStatus.Middle;
+
+    [ObservableProperty]
+    private ValveStatus _valve3Status = ValveStatus.Middle;
+
+    [ObservableProperty]
+    private ValveStatus _valve4Status = ValveStatus.Middle;
+
+    [ObservableProperty]
+    private ValveStatus _valve5Status = ValveStatus.Middle;
+
+    // 妯″紡鐘舵
+    [ObservableProperty]
+    private ValveStatus _modeStatus = ValveStatus.Middle;
+
+    // 娴佸姩鐘舵
+    [ObservableProperty]
+    private bool _isInflowRunning = true;
+
+    [ObservableProperty]
+    private bool _isOutflowRunning = true;
+
+    // 璁惧鐘舵侀鑹
+    [ObservableProperty]
+    private string _pump1StatusColor = "#4CAF50";
+
+    [ObservableProperty]
+    private string _pump2StatusColor = "#4CAF50";
+
+    [ObservableProperty]
+    private string _pump3StatusColor = "#4CAF50";
+
+    [ObservableProperty]
+    private string _pump4StatusColor = "#4CAF50";
+
+    [ObservableProperty]
+    private string _pump5StatusColor = "#4CAF50";
+
+    [ObservableProperty]
+    private string _fan1StatusColor = "#4CAF50";
+
+    [ObservableProperty]
+    private string _fan2StatusColor = "#4CAF50";
+
+    // 鎶ヨ鐘舵
+    [ObservableProperty]
+    private bool _hasAlarm;
+
+    [ObservableProperty]
+    private string _alarmMessage = string.Empty;
+
+    // 杩炴帴鐘舵
+    [ObservableProperty]
+    private bool _isPlcConnected;
+
+    public ViewAViewModel()
+    {
+        StartDataRefresh();
+    }
+
+    /// <summary>
+    /// 鍚姩鏁版嵁鍒锋柊
+    /// </summary>
+    private void StartDataRefresh()
+    {
+        var timer = new System.Timers.Timer(1000);
+        timer.Elapsed += async (s, e) =>
+        {
+            await RefreshDataAsync();
+        };
+        timer.Start();
+    }
+
+    /// <summary>
+    /// 鍒锋柊 PLC 鏁版嵁
+    /// </summary>
+    [RelayCommand]
+    private async Task RefreshDataAsync()
+    {
+        if (!PlcService.IsConnected) return;
+
+        try
+        {
+            // 璇诲彇娑蹭綅鏁版嵁
+            Tank1Level = await PlcService.ReadFloatAsync("VD100");
+            Tank2Level = await PlcService.ReadFloatAsync("VD104");
+            Tank3Level = await PlcService.ReadFloatAsync("VD108");
+            Tank4Level = await PlcService.ReadFloatAsync("VD112");
+
+            // 璇诲彇娴侀噺鏁版嵁
+            InflowRate = await PlcService.ReadFloatAsync("VD200");
+            OutflowRate = await PlcService.ReadFloatAsync("VD204");
+
+            // 璇诲彇娉电姸鎬
+            Pump1Running = await PlcService.ReadBoolAsync("Q0.0");
+            Pump2Running = await PlcService.ReadBoolAsync("Q0.1");
+            Pump3Running = await PlcService.ReadBoolAsync("Q0.2");
+            Pump4Running = await PlcService.ReadBoolAsync("Q0.3");
+            Pump5Running = await PlcService.ReadBoolAsync("Q0.4");
+
+            // 璇诲彇椋庢墖鐘舵
+            Fan1Running = await PlcService.ReadBoolAsync("Q0.5");
+            Fan2Running = await PlcService.ReadBoolAsync("Q0.6");
+
+            // 鏇存柊璁惧鐘舵侀鑹
+            UpdateDeviceStatusColors();
+
+            // 妫鏌ユ姤璀
+            CheckAlarms();
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "鍒锋柊 PLC 鏁版嵁澶辫触");
+        }
+    }
+
+    /// <summary>
+    /// 鏇存柊璁惧鐘舵侀鑹
+    /// </summary>
+    private void UpdateDeviceStatusColors()
+    {
+        Pump1StatusColor = Pump1Running ? "#4CAF50" : "#F44336";
+        Pump2StatusColor = Pump2Running ? "#4CAF50" : "#F44336";
+        Pump3StatusColor = Pump3Running ? "#4CAF50" : "#F44336";
+        Pump4StatusColor = Pump4Running ? "#4CAF50" : "#F44336";
+        Pump5StatusColor = Pump5Running ? "#4CAF50" : "#F44336";
+        Fan1StatusColor = Fan1Running ? "#4CAF50" : "#F44336";
+        Fan2StatusColor = Fan2Running ? "#4CAF50" : "#F44336";
+    }
+
+    /// <summary>
+    /// 妫鏌ユ姤璀︽潯浠
+    /// </summary>
+    private void CheckAlarms()
+    {
+        var config = ConfigService.GetConfig();
+        if (config == null) return;
+
+        HasAlarm = false;
+        AlarmMessage = string.Empty;
+
+        if (Tank1Level > config.LevelHighAlarm)
+        {
+            HasAlarm = true;
+            AlarmMessage = "1鍙锋按姹犳恫浣嶈繃楂";
+        }
+        else if (Tank1Level < config.LevelLowAlarm)
+        {
+            HasAlarm = true;
+            AlarmMessage = "1鍙锋按姹犳恫浣嶈繃浣";
+        }
+
+        if (InflowRate > config.FlowHighAlarm)
+        {
+            HasAlarm = true;
+            AlarmMessage = "杩涙按娴侀噺杩囬珮";
+        }
+    }
+
+    /// <summary>
+    /// 杩炴帴 PLC
+    /// </summary>
+    [RelayCommand]
+    private async Task ConnectPlcAsync()
+    {
+        IsPlcConnected = await PlcService.ConnectAsync();
+    }
+
+    /// <summary>
+    /// 鏂紑 PLC
+    /// </summary>
+    [RelayCommand]
+    private void DisconnectPlc()
+    {
+        PlcService.Disconnect();
+        IsPlcConnected = false;
+    }
+
+    /// <summary>
+    /// 鎺у埗娉
+    /// </summary>
+    [RelayCommand]
+    private async Task TogglePumpAsync(string pumpId)
+    {
+        if (!PlcService.IsConnected)
+        {
+            Log.Warning("PLC 鏈繛鎺ワ紝鏃犳硶鎺у埗娉");
+            return;
+        }
+
+        var address = pumpId switch
+        {
+            "P001" => "Q0.0",
+            "P002" => "Q0.1",
+            "P003" => "Q0.2",
+            "P004" => "Q0.3",
+            "P005" => "Q0.4",
+            _ => throw new ArgumentException($"鏈煡鐨勬车 ID: {pumpId}")
+        };
+
+        var currentState = pumpId switch
+        {
+            "P001" => Pump1Running,
+            "P002" => Pump2Running,
+            "P003" => Pump3Running,
+            "P004" => Pump4Running,
+            "P005" => Pump5Running,
+            _ => false
+        };
+
+        var success = await PlcService.WriteBoolAsync(address, !currentState);
+        if (success)
+        {
+            Log.Information("娉 {PumpId} 鐘舵佸凡鍒囨崲", pumpId);
+        }
+    }
+
+    /// <summary>
+    /// 鎺у埗椋庢墖
+    /// </summary>
+    [RelayCommand]
+    private async Task ToggleFanAsync(string fanId)
+    {
+        if (!PlcService.IsConnected)
+        {
+            Log.Warning("PLC 鏈繛鎺ワ紝鏃犳硶鎺у埗椋庢墖");
+            return;
+        }
+
+        var address = fanId switch
+        {
+            "F001" => "Q0.5",
+            "F002" => "Q0.6",
+            _ => throw new ArgumentException($"鏈煡鐨勯鎵 ID: {fanId}")
+        };
+
+        var currentState = fanId switch
+        {
+            "F001" => Fan1Running,
+            "F002" => Fan2Running,
+            _ => false
+        };
+
+        var success = await PlcService.WriteBoolAsync(address, !currentState);
+        if (success)
+        {
+            Log.Information("椋庢墖 {FanId} 鐘舵佸凡鍒囨崲", fanId);
+        }
+    }
+
+    /// <summary>
+    /// 鎺у埗闃闂
+    /// </summary>
+    [RelayCommand]
+    private async Task ToggleValveAsync(string valveId)
+    {
+        if (!PlcService.IsConnected)
+        {
+            Log.Warning("PLC 鏈繛鎺ワ紝鏃犳硶鎺у埗闃闂");
+            return;
+        }
+
+        var address = valveId switch
+        {
+            "V001" => "Q1.0",
+            "V002" => "Q1.1",
+            "V003" => "Q1.2",
+            "V004" => "Q1.3",
+            "V005" => "Q1.4",
+            _ => throw new ArgumentException($"鏈煡鐨勯榾闂 ID: {valveId}")
+        };
+
+        var currentStatus = valveId switch
+        {
+            "V001" => Valve1Status,
+            "V002" => Valve2Status,
+            "V003" => Valve3Status,
+            "V004" => Valve4Status,
+            "V005" => Valve5Status,
+            _ => ValveStatus.Middle
+        };
+
+        // 寰幆鍒囨崲鐘舵: 鍏抽棴 -> 涓棿 -> 鎵撳紑 -> 鍏抽棴
+        var newStatus = currentStatus switch
+        {
+            ValveStatus.Closed => ValveStatus.Middle,
+            ValveStatus.Middle => ValveStatus.Open,
+            ValveStatus.Open => ValveStatus.Closed,
+            _ => ValveStatus.Middle
+        };
+
+        var success = await PlcService.WriteBoolAsync(address, newStatus == ValveStatus.Open);
+        if (success)
+        {
+            switch (valveId)
+            {
+                case "V001": Valve1Status = newStatus; break;
+                case "V002": Valve2Status = newStatus; break;
+                case "V003": Valve3Status = newStatus; break;
+                case "V004": Valve4Status = newStatus; break;
+                case "V005": Valve5Status = newStatus; break;
+            }
+            Log.Information("闃闂 {ValveId} 鐘舵佸凡鍒囨崲涓 {Status}", valveId, newStatus);
+        }
+    }
+
+    /// <summary>
+    /// 鍒囨崲妯″紡
+    /// </summary>
+    [RelayCommand]
+    private async Task ToggleModeAsync()
+    {
+        if (!PlcService.IsConnected)
+        {
+            Log.Warning("PLC 鏈繛鎺ワ紝鏃犳硶鍒囨崲妯″紡");
+            return;
+        }
+
+        var newStatus = ModeStatus switch
+        {
+            ValveStatus.Closed => ValveStatus.Middle,
+            ValveStatus.Middle => ValveStatus.Open,
+            ValveStatus.Open => ValveStatus.Closed,
+            _ => ValveStatus.Middle
+        };
+
+        var success = await PlcService.WriteBoolAsync("M0.0", newStatus == ValveStatus.Open);
+        if (success)
+        {
+            ModeStatus = newStatus;
+            Log.Information("妯″紡宸插垏鎹负: {Status}", newStatus);
+        }
+    }
+}
+
+/// <summary>
+/// 闃闂ㄧ姸鎬佹灇涓
+/// </summary>
+public enum ValveStatus
+{
+    /// <summary>
+    /// 鍏抽棴
+    /// </summary>
+    Closed,
+
+    /// <summary>
+    /// 涓棿浣嶇疆
+    /// </summary>
+    Middle,
+
+    /// <summary>
+    /// 鎵撳紑
+    /// </summary>
+    Open
+}

+ 140 - 0
src/YZWater.Core/ViewModels/ViewBViewModel.cs

@@ -0,0 +1,140 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Serilog;
+using YZWater.Core.Models;
+using YZWater.Core.Services;
+
+namespace YZWater.Core.ViewModels;
+
+/// <summary>
+/// 鍙傛暟璁剧疆瑙嗗浘 ViewModel
+/// </summary>
+public partial class ViewBViewModel : ObservableObject
+{
+    [ObservableProperty]
+    private string _plcIp = "192.168.0.150";
+
+    [ObservableProperty]
+    private int _plcPort = 5000;
+
+    [ObservableProperty]
+    private bool _autoConnect = true;
+
+    [ObservableProperty]
+    private float _levelHighAlarm = 80f;
+
+    [ObservableProperty]
+    private float _levelLowAlarm = 20f;
+
+    [ObservableProperty]
+    private float _flowHighAlarm = 100f;
+
+    [ObservableProperty]
+    private float _pumpFrequency = 50f;
+
+    [ObservableProperty]
+    private string _connectionStatus = "鏈繛鎺";
+
+    [ObservableProperty]
+    private bool _isConnecting;
+
+    public ViewBViewModel()
+    {
+        LoadConfig();
+    }
+
+    /// <summary>
+    /// 鍔犺浇閰嶇疆
+    /// </summary>
+    [RelayCommand]
+    private void LoadConfig()
+    {
+        var config = ConfigService.GetConfig();
+        if (config != null)
+        {
+            PlcIp = config.PlcIp;
+            PlcPort = config.PlcPort;
+            AutoConnect = config.AutoConnect;
+            LevelHighAlarm = config.LevelHighAlarm;
+            LevelLowAlarm = config.LevelLowAlarm;
+            FlowHighAlarm = config.FlowHighAlarm;
+            PumpFrequency = config.PumpFrequency;
+        }
+        Log.Debug("閰嶇疆宸插姞杞");
+    }
+
+    /// <summary>
+    /// 淇濆瓨閰嶇疆
+    /// </summary>
+    [RelayCommand]
+    private void SaveConfig()
+    {
+        var config = new SystemConfig
+        {
+            PlcIp = PlcIp,
+            PlcPort = PlcPort,
+            AutoConnect = AutoConnect,
+            LevelHighAlarm = LevelHighAlarm,
+            LevelLowAlarm = LevelLowAlarm,
+            FlowHighAlarm = FlowHighAlarm,
+            PumpFrequency = PumpFrequency
+        };
+
+        ConfigService.UpdateConfig(config);
+        Log.Information("閰嶇疆宸蹭繚瀛");
+    }
+
+    /// <summary>
+    /// 娴嬭瘯杩炴帴
+    /// </summary>
+    [RelayCommand]
+    private async Task TestConnectionAsync()
+    {
+        IsConnecting = true;
+        ConnectionStatus = "杩炴帴涓...";
+
+        try
+        {
+            // 鏇存柊 PLC IP
+            PlcService.Initialize();
+
+            var success = await PlcService.ConnectAsync();
+            ConnectionStatus = success ? "杩炴帴鎴愬姛" : "杩炴帴澶辫触";
+
+            if (success)
+            {
+                Log.Information("PLC 杩炴帴娴嬭瘯鎴愬姛");
+            }
+            else
+            {
+                Log.Warning("PLC 杩炴帴娴嬭瘯澶辫触");
+            }
+        }
+        catch (Exception ex)
+        {
+            ConnectionStatus = "杩炴帴寮傚父";
+            Log.Error(ex, "PLC 杩炴帴娴嬭瘯寮傚父");
+        }
+        finally
+        {
+            IsConnecting = false;
+        }
+    }
+
+    /// <summary>
+    /// 鎭㈠榛樿璁剧疆
+    /// </summary>
+    [RelayCommand]
+    private void ResetToDefault()
+    {
+        PlcIp = "192.168.0.150";
+        PlcPort = 5000;
+        AutoConnect = true;
+        LevelHighAlarm = 80f;
+        LevelLowAlarm = 20f;
+        FlowHighAlarm = 100f;
+        PumpFrequency = 50f;
+
+        Log.Information("宸叉仮澶嶉粯璁よ缃");
+    }
+}

+ 186 - 0
src/YZWater.Core/ViewModels/ViewCViewModel.cs

@@ -0,0 +1,186 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using LiveChartsCore;
+using LiveChartsCore.Defaults;
+using LiveChartsCore.SkiaSharpView;
+using LiveChartsCore.SkiaSharpView.Painting;
+using SkiaSharp;
+using Serilog;
+using YZWater.Core.Models;
+using YZWater.Core.Services;
+
+namespace YZWater.Core.ViewModels;
+
+/// <summary>
+/// 娴侀噺璁板綍瑙嗗浘 ViewModel
+/// </summary>
+public partial class ViewCViewModel : ObservableObject
+{
+    [ObservableProperty]
+    private ISeries[] _inflowSeries;
+
+    [ObservableProperty]
+    private ISeries[] _outflowSeries;
+
+    [ObservableProperty]
+    private Axis[] _xAxes;
+
+    [ObservableProperty]
+    private Axis[] _yAxes;
+
+    [ObservableProperty]
+    private DateTime _startDate = DateTime.Now.AddDays(-7);
+
+    [ObservableProperty]
+    private DateTime _endDate = DateTime.Now;
+
+    [ObservableProperty]
+    private List<FlowRecord> _flowRecords = new();
+
+    public ViewCViewModel()
+    {
+        InitializeChart();
+        _ = LoadFlowRecordsAsync();
+    }
+
+    /// <summary>
+    /// 鍒濆鍖栧浘琛
+    /// </summary>
+    private void InitializeChart()
+    {
+        InflowSeries = new ISeries[]
+        {
+            new ColumnSeries<DateTimePoint>
+            {
+                Name = "杩涙按娴侀噺",
+                Values = new List<DateTimePoint>(),
+                Fill = new SolidColorPaint(SKColors.CornflowerBlue),
+                Stroke = null
+            }
+        };
+
+        OutflowSeries = new ISeries[]
+        {
+            new ColumnSeries<DateTimePoint>
+            {
+                Name = "鍑烘按娴侀噺",
+                Values = new List<DateTimePoint>(),
+                Fill = new SolidColorPaint(SKColors.MediumSeaGreen),
+                Stroke = null
+            }
+        };
+
+        XAxes = new Axis[]
+        {
+            new Axis
+            {
+                Labeler = value => new DateTime((long)value).ToString("MM-dd"),
+                UnitWidth = TimeSpan.FromDays(1).Ticks,
+                LabelsRotation = 45
+            }
+        };
+
+        YAxes = new Axis[]
+        {
+            new Axis
+            {
+                Name = "娴侀噺 (m鲁/h)",
+                MinLimit = 0
+            }
+        };
+    }
+
+    /// <summary>
+    /// 鍔犺浇娴侀噺璁板綍
+    /// </summary>
+    [RelayCommand]
+    private async Task LoadFlowRecordsAsync()
+    {
+        try
+        {
+            var records = await DatabaseService.Db.Select<FlowRecord>()
+                .Where(r => r.RecordTime >= StartDate && r.RecordTime <= EndDate)
+                .OrderByDescending(r => r.RecordTime)
+                .ToListAsync();
+
+            FlowRecords = records;
+            UpdateChart();
+            Log.Debug("鍔犺浇浜 {Count} 鏉℃祦閲忚褰", records.Count);
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "鍔犺浇娴侀噺璁板綍澶辫触");
+        }
+    }
+
+    /// <summary>
+    /// 鏇存柊鍥捐〃鏁版嵁
+    /// </summary>
+    private void UpdateChart()
+    {
+        var inflowData = FlowRecords
+            .GroupBy(r => r.RecordTime.Date)
+            .Select(g => new DateTimePoint(g.Key, g.Average(r => r.InflowRate)))
+            .OrderBy(p => p.DateTime)
+            .ToList();
+
+        var outflowData = FlowRecords
+            .GroupBy(r => r.RecordTime.Date)
+            .Select(g => new DateTimePoint(g.Key, g.Average(r => r.OutflowRate)))
+            .OrderBy(p => p.DateTime)
+            .ToList();
+
+        InflowSeries[0].Values = inflowData;
+        OutflowSeries[0].Values = outflowData;
+    }
+
+    /// <summary>
+    /// 瀵煎嚭鏁版嵁
+    /// </summary>
+    [RelayCommand]
+    private async Task ExportDataAsync()
+    {
+        try
+        {
+            var filePath = $"FlowRecords_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
+            var lines = new List<string>
+            {
+                "鏃堕棿,杩涙按娴侀噺(m鲁/h),鍑烘按娴侀噺(m鲁/h),绱杩涙按閲(m鲁),绱鍑烘按閲(m鲁),澶囨敞"
+            };
+
+            foreach (var record in FlowRecords)
+            {
+                lines.Add($"{record.RecordTime:yyyy-MM-dd HH:mm:ss},{record.InflowRate},{record.OutflowRate},{record.TotalInflow},{record.TotalOutflow},{record.Remark}");
+            }
+
+            await File.WriteAllLinesAsync(filePath, lines);
+            Log.Information("娴侀噺璁板綍宸插鍑哄埌: {FilePath}", filePath);
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "瀵煎嚭娴侀噺璁板綍澶辫触");
+        }
+    }
+
+    /// <summary>
+    /// 娓呴櫎鏃ф暟鎹
+    /// </summary>
+    [RelayCommand]
+    private async Task ClearOldDataAsync()
+    {
+        try
+        {
+            var cutoffDate = DateTime.Now.AddDays(-30);
+            await DatabaseService.Db.Delete<FlowRecord>()
+                .Where(r => r.RecordTime < cutoffDate)
+                .ExecuteAffrowsAsync();
+
+            await LoadFlowRecordsAsync();
+            Log.Information("宸叉竻闄 30 澶╁墠鐨勬祦閲忚褰");
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "娓呴櫎鏃ф暟鎹け璐");
+        }
+    }
+}

+ 171 - 0
src/YZWater.Core/ViewModels/ViewDViewModel.cs

@@ -0,0 +1,171 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Serilog;
+using YZWater.Core.Models;
+using YZWater.Core.Services;
+
+namespace YZWater.Core.ViewModels;
+
+/// <summary>
+/// 鎶ヨ璁板綍瑙嗗浘 ViewModel
+/// </summary>
+public partial class ViewDViewModel : ObservableObject
+{
+    [ObservableProperty]
+    private List<AlarmRecord> _alarmRecords = new();
+
+    [ObservableProperty]
+    private AlarmRecord? _selectedRecord;
+
+    [ObservableProperty]
+    private int _unconfirmedCount;
+
+    [ObservableProperty]
+    private DateTime _startDate = DateTime.Now.AddDays(-7);
+
+    [ObservableProperty]
+    private DateTime _endDate = DateTime.Now;
+
+    [ObservableProperty]
+    private string _filterType = "鍏ㄩ儴";
+
+    public ViewDViewModel()
+    {
+        _ = LoadAlarmRecordsAsync();
+    }
+
+    /// <summary>
+    /// 鍔犺浇鎶ヨ璁板綍
+    /// </summary>
+    [RelayCommand]
+    private async Task LoadAlarmRecordsAsync()
+    {
+        try
+        {
+            var query = DatabaseService.Db.Select<AlarmRecord>()
+                .Where(r => r.AlarmTime >= StartDate && r.AlarmTime <= EndDate);
+
+            if (FilterType != "鍏ㄩ儴")
+            {
+                query = query.Where(r => r.AlarmType == FilterType);
+            }
+
+            var records = await query
+                .OrderByDescending(r => r.AlarmTime)
+                .ToListAsync();
+
+            AlarmRecords = records;
+            UnconfirmedCount = records.Count(r => !r.IsConfirmed);
+
+            Log.Debug("鍔犺浇浜 {Count} 鏉℃姤璀﹁褰", records.Count);
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "鍔犺浇鎶ヨ璁板綍澶辫触");
+        }
+    }
+
+    /// <summary>
+    /// 纭鎶ヨ
+    /// </summary>
+    [RelayCommand]
+    private async Task ConfirmAlarmAsync(AlarmRecord? record)
+    {
+        if (record == null) return;
+
+        try
+        {
+            record.IsConfirmed = true;
+            record.ConfirmedTime = DateTime.Now;
+            record.ConfirmedBy = "鎿嶄綔鍛"; // 瀹為檯搴斾粠鐧诲綍鐢ㄦ埛鑾峰彇
+
+            await DatabaseService.Db.Update<AlarmRecord>()
+                .Set(r => r.IsConfirmed, true)
+                .Set(r => r.ConfirmedTime, record.ConfirmedTime)
+                .Set(r => r.ConfirmedBy, record.ConfirmedBy)
+                .Where(r => r.Id == record.Id)
+                .ExecuteAffrowsAsync();
+
+            await LoadAlarmRecordsAsync();
+            Log.Information("鎶ヨ {Id} 宸茬‘璁", record.Id);
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "纭鎶ヨ澶辫触");
+        }
+    }
+
+    /// <summary>
+    /// 鎵归噺纭鎵鏈夋姤璀
+    /// </summary>
+    [RelayCommand]
+    private async Task ConfirmAllAlarmsAsync()
+    {
+        try
+        {
+            await DatabaseService.Db.Update<AlarmRecord>()
+                .Set(r => r.IsConfirmed, true)
+                .Set(r => r.ConfirmedTime, DateTime.Now)
+                .Set(r => r.ConfirmedBy, "鎿嶄綔鍛")
+                .Where(r => !r.IsConfirmed)
+                .ExecuteAffrowsAsync();
+
+            await LoadAlarmRecordsAsync();
+            Log.Information("鎵鏈夋姤璀﹀凡纭");
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "鎵归噺纭鎶ヨ澶辫触");
+        }
+    }
+
+    /// <summary>
+    /// 瀵煎嚭鎶ヨ璁板綍
+    /// </summary>
+    [RelayCommand]
+    private async Task ExportAlarmsAsync()
+    {
+        try
+        {
+            var filePath = $"AlarmRecords_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
+            var lines = new List<string>
+            {
+                "鏃堕棿,绫诲瀷,鍐呭,鎶ヨ鍊,绾у埆,鏄惁宸茬‘璁,纭鏃堕棿,纭浜"
+            };
+
+            foreach (var record in AlarmRecords)
+            {
+                lines.Add($"{record.AlarmTime:yyyy-MM-dd HH:mm:ss},{record.AlarmType},{record.AlarmMessage},{record.AlarmValue},{record.AlarmLevel},{(record.IsConfirmed ? "鏄" : "鍚")},{record.ConfirmedTime:yyyy-MM-dd HH:mm:ss},{record.ConfirmedBy}");
+            }
+
+            await File.WriteAllLinesAsync(filePath, lines);
+            Log.Information("鎶ヨ璁板綍宸插鍑哄埌: {FilePath}", filePath);
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "瀵煎嚭鎶ヨ璁板綍澶辫触");
+        }
+    }
+
+    /// <summary>
+    /// 娓呴櫎鍘嗗彶璁板綍
+    /// </summary>
+    [RelayCommand]
+    private async Task ClearHistoryAsync()
+    {
+        try
+        {
+            var cutoffDate = DateTime.Now.AddDays(-90);
+            await DatabaseService.Db.Delete<AlarmRecord>()
+                .Where(r => r.AlarmTime < cutoffDate && r.IsConfirmed)
+                .ExecuteAffrowsAsync();
+
+            await LoadAlarmRecordsAsync();
+            Log.Information("宸叉竻闄 90 澶╁墠鐨勫凡纭鎶ヨ璁板綍");
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "娓呴櫎鍘嗗彶璁板綍澶辫触");
+        }
+    }
+}

+ 78 - 0
src/YZWater.Core/ViewModels/ViewEViewModel.cs

@@ -0,0 +1,78 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using YZWater.Core.Services;
+
+namespace YZWater.Core.ViewModels;
+
+/// <summary>
+/// 鍏充簬瑙嗗浘 ViewModel
+/// </summary>
+public partial class ViewEViewModel : ObservableObject
+{
+    [ObservableProperty]
+    private string _systemName = "姹℃按澶勭悊鐩戞帶绯荤粺";
+
+    [ObservableProperty]
+    private string _version = "3.0.0";
+
+    [ObservableProperty]
+    private string _companyName = "鎵窞鏃僵绉戞妧鏈夐檺鍏徃";
+
+    [ObservableProperty]
+    private string _contactPerson = "璧电粡鐞";
+
+    [ObservableProperty]
+    private string _contactPhone = "18115099090";
+
+    [ObservableProperty]
+    private string _copyright = $"漏 {DateTime.Now.Year} 鎵窞鏃僵绉戞妧鏈夐檺鍏徃";
+
+    [ObservableProperty]
+    private string _description = "鍩轰簬 Avalonia UI 鐨勮法骞冲彴姹℃按澶勭悊鍘傜洃鎺х郴缁";
+
+    [ObservableProperty]
+    private string _techStack = "Avalonia UI 11 | .NET 8 | FreeSql | HslCommunication";
+
+    public ViewEViewModel()
+    {
+        LoadCompanyInfo();
+    }
+
+    /// <summary>
+    /// 鍔犺浇鍏徃淇℃伅
+    /// </summary>
+    private void LoadCompanyInfo()
+    {
+        var config = ConfigService.GetConfig();
+        if (config != null)
+        {
+            CompanyName = config.CompanyName;
+            ContactPerson = config.ContactPerson;
+            ContactPhone = config.ContactPhone;
+        }
+    }
+
+    /// <summary>
+    /// 鎵撳紑瀹樼綉
+    /// </summary>
+    [RelayCommand]
+    private void OpenWebsite()
+    {
+        // 瀹為檯瀹炵幇涓簲璇ユ墦寮娴忚鍣
+        // System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
+        // {
+        //     FileName = "https://www.example.com",
+        //     UseShellExecute = true
+        // });
+    }
+
+    /// <summary>
+    /// 妫鏌ユ洿鏂
+    /// </summary>
+    [RelayCommand]
+    private async Task CheckUpdateAsync()
+    {
+        // 瀹為檯瀹炵幇涓簲璇ユ鏌ユ湇鍔″櫒鏄惁鏈夋柊鐗堟湰
+        await Task.Delay(1000); // 妯℃嫙缃戠粶璇锋眰
+    }
+}

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

@@ -0,0 +1,24 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <RootNamespace>YZWater.Core</RootNamespace>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
+    <PackageReference Include="FreeSql.Provider.Sqlite" Version="3.2.800" />
+    <PackageReference Include="FreeSql.Repository" Version="3.2.800" />
+    <PackageReference Include="HslCommunication" Version="12.0.1" />
+    <PackageReference Include="LiveChartsCore" Version="2.0.0-rc4.5" />
+    <PackageReference Include="LiveChartsCore.SkiaSharpView" Version="2.0.0-rc4.5" />
+    <PackageReference Include="NLog" Version="5.3.4" />
+    <PackageReference Include="Serilog" Version="4.0.2" />
+    <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
+    <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
+    <PackageReference Include="SkiaSharp" Version="2.88.9" />
+  </ItemGroup>
+
+</Project>