MainWindowViewModel.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. using Prism.Commands;
  2. using Prism.Mvvm;
  3. using Prism.Regions;
  4. using System;
  5. using System.Collections.ObjectModel;
  6. using System.Diagnostics;
  7. using System.Windows;
  8. using System.Windows.Media;
  9. using System.Windows.Threading;
  10. using TeamAAS.Core;
  11. using TeamAAS.Models;
  12. using TeamAAS.Localization;
  13. using TeamAAS.Theme;
  14. namespace TeamAAS.ViewModels
  15. {
  16. public class MainWindowViewModel : TeamAAS.BindableBase
  17. {
  18. private readonly IRegionManager _regionManager;
  19. private DispatcherTimer _timer;
  20. public ObservableCollection<DeviceStatusItem> DeviceStatusItems { get; } = new ObservableCollection<DeviceStatusItem>();
  21. /// <summary>左侧导航(首页/产品/设置)</summary>
  22. public ObservableCollection<NavItem> LeftNavItems { get; } = new ObservableCollection<NavItem>();
  23. /// <summary>右侧导航(标定/统计/用户)</summary>
  24. public ObservableCollection<NavItem> RightNavItems { get; } = new ObservableCollection<NavItem>();
  25. private string _memoryUsage = "";
  26. public string MemoryUsage
  27. {
  28. get => _memoryUsage;
  29. set => SetProperty(ref _memoryUsage, value);
  30. }
  31. private string _currentTime = "";
  32. public string CurrentTime
  33. {
  34. get => _currentTime;
  35. set => SetProperty(ref _currentTime, value);
  36. }
  37. private string _currentView = "HomeView";
  38. public string CurrentView
  39. {
  40. get => _currentView;
  41. set => SetProperty(ref _currentView, value);
  42. }
  43. #region 主题与主色
  44. public bool IsDarkMode
  45. {
  46. get => string.Equals(App.SystemConfig?.Theme, "Dark", StringComparison.OrdinalIgnoreCase);
  47. set
  48. {
  49. if (IsDarkMode == value) return;
  50. App.SystemConfig.Theme = value ? "Dark" : "Light";
  51. ThemeManager.ApplyTheme(value, App.SystemConfig.AccentColor);
  52. App.SaveSystemConfig();
  53. RaisePropertyChanged(nameof(IsDarkMode));
  54. RaisePropertyChanged(nameof(ThemeIcon));
  55. RaisePropertyChanged(nameof(ThemeDisplayName));
  56. }
  57. }
  58. public string ThemeIcon => IsDarkMode ? "☀️" : "🌙";
  59. public string ThemeDisplayName => IsDarkMode ? "深色(点击切换)" : "浅色(点击切换)";
  60. public ObservableCollection<AccentColorOption> AccentColors { get; }
  61. = new ObservableCollection<AccentColorOption>();
  62. public Brush AccentBrush
  63. {
  64. get
  65. {
  66. try
  67. {
  68. var color = (Color)ColorConverter.ConvertFromString(App.SystemConfig?.AccentColor);
  69. // 规范化为主色不透明版本(配置里可能残留带 alpha 的历史值)
  70. var brush = new SolidColorBrush(Color.FromRgb(color.R, color.G, color.B));
  71. brush.Freeze();
  72. return brush;
  73. }
  74. catch { return Brushes.DodgerBlue; }
  75. }
  76. set => ApplyAccent(value);
  77. }
  78. public DelegateCommand<string> NavigateCommand { get; }
  79. public DelegateCommand ToggleThemeCommand { get; }
  80. public DelegateCommand<string> SetAccentCommand { get; }
  81. public MainWindowViewModel(IRegionManager regionManager)
  82. {
  83. _regionManager = regionManager;
  84. NavigateCommand = new DelegateCommand<string>(Navigate);
  85. ToggleThemeCommand = new DelegateCommand(() => IsDarkMode = !IsDarkMode);
  86. SetAccentCommand = new DelegateCommand<string>(hex =>
  87. {
  88. if (string.IsNullOrWhiteSpace(hex)) return;
  89. App.SystemConfig.AccentColor = hex;
  90. // 主色系画刷与文字对比色由主题覆盖层派生重建(毫秒级生效)
  91. ThemeManager.UpdateAccent(hex);
  92. App.SaveSystemConfig();
  93. RaisePropertyChanged(nameof(AccentBrush));
  94. });
  95. InitAccentColors();
  96. ThemeManager.ThemeChanged += OnAppThemeChanged;
  97. InitNavItems();
  98. }
  99. private void InitAccentColors()
  100. {
  101. var presets = new[]
  102. {
  103. ("默认蓝", "#2D6CDF"),
  104. ("成功绿", "#4CAF50"),
  105. ("活力橙", "#FF9800"),
  106. ("警示红", "#F44336"),
  107. ("优雅紫", "#7B1FA2"),
  108. ("清新青", "#00BCD4"),
  109. ("少女粉", "#E91E63"),
  110. ("中性灰", "#607D8B"),
  111. };
  112. foreach (var (name, hex) in presets)
  113. {
  114. AccentColors.Add(new AccentColorOption
  115. {
  116. Name = name,
  117. Hex = hex,
  118. Brush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex))
  119. });
  120. }
  121. }
  122. private void ApplyAccent(Brush brush)
  123. {
  124. if (!(brush is SolidColorBrush sb)) return;
  125. // 规范化:只取 RGB、强制不透明。ColorPicker 样式重算时会回写带任意 alpha 的画刷
  126. // (实测出现过 A=0x0A 的 96% 透明色被存进配置,表现为主色"特别淡、朦了一层"),
  127. // 统一丢弃 alpha——色相保真,主色永远纯色。
  128. var color = Color.FromRgb(sb.Color.R, sb.Color.G, sb.Color.B);
  129. // 按 RGB 去重:同色不同 alpha 的回写(切主题时样式重算触发)完全静默忽略
  130. var current = TryParseAccentColor();
  131. if (current.HasValue &&
  132. current.Value.R == color.R && current.Value.G == color.G && current.Value.B == color.B)
  133. {
  134. return;
  135. }
  136. App.SystemConfig.AccentColor = color.ToString();
  137. // 主色系画刷与文字对比色由主题覆盖层派生重建(毫秒级生效,不碰合并字典)
  138. ThemeManager.UpdateAccent(color);
  139. App.SaveSystemConfig();
  140. RaisePropertyChanged(nameof(AccentBrush));
  141. }
  142. private static Color? TryParseAccentColor()
  143. {
  144. try
  145. {
  146. var hex = App.SystemConfig?.AccentColor;
  147. if (string.IsNullOrWhiteSpace(hex)) return null;
  148. return (Color)ColorConverter.ConvertFromString(hex);
  149. }
  150. catch { return null; }
  151. }
  152. private void OnAppThemeChanged()
  153. {
  154. RaisePropertyChanged(nameof(IsDarkMode));
  155. RaisePropertyChanged(nameof(ThemeIcon));
  156. RaisePropertyChanged(nameof(ThemeDisplayName));
  157. RaisePropertyChanged(nameof(AccentBrush));
  158. }
  159. #endregion
  160. public void OnLoaded()
  161. {
  162. Navigate("HomeView");
  163. StartTimer();
  164. }
  165. #region 导航
  166. private void InitNavItems()
  167. {
  168. // 图标沿用 Material Design Path(与旧 XAML 资源一致)
  169. AddNav(LeftNavItems, "首页", "HomeView",
  170. "M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z");
  171. AddNav(LeftNavItems, "产品", "ProductView",
  172. "M12,2L2,7V17L12,22L22,17V7L12,2Z M12,2V12 M2,7L12,12L22,7");
  173. AddNav(LeftNavItems, "设置", "SettingView",
  174. "M19.14,12.94C19.18,12.64 19.2,12.33 19.2,12C19.2,11.68 19.18,11.36 19.13,11.06L21.16,9.48C21.34,9.34 21.39,9.07 21.27,8.87L19.35,5.55C19.23,5.33 18.96,5.25 18.73,5.33L16.38,6.28C15.88,5.89 15.35,5.56 14.76,5.32L14.4,2.81C14.36,2.57 14.16,2.4 13.92,2.4H10.08C9.83,2.4 9.63,2.57 9.59,2.81L9.24,5.32C8.65,5.56 8.11,5.89 7.62,6.28L5.27,5.33C5.04,5.25 4.77,5.33 4.65,5.55L2.73,8.87C2.61,9.07 2.66,9.34 2.84,9.48L4.87,11.06C4.82,11.36 4.8,11.68 4.8,12C4.8,12.33 4.82,12.64 4.87,12.94L2.84,14.52C2.66,14.66 2.61,14.93 2.73,15.13L4.65,18.45C4.77,18.67 5.04,18.75 5.27,18.67L7.62,17.72C8.11,18.11 8.65,18.44 9.24,18.68L9.6,21.19C9.64,21.43 9.84,21.6 10.08,21.6H13.92C14.17,21.6 14.37,21.43 14.41,21.19L14.76,18.68C15.35,18.44 15.89,18.11 16.38,17.72L18.73,18.67C18.96,18.75 19.23,18.67 19.35,18.45L21.27,15.13C21.39,14.93 21.34,14.66 21.16,14.52L19.14,12.94ZM12,15.6C10.02,15.6 8.4,13.98 8.4,12C8.4,10.02 10.02,8.4 12,8.4C13.98,8.4 15.6,10.02 15.6,12C15.6,13.98 13.98,15.6 12,15.6Z");
  175. AddNav(RightNavItems, "标定", "CalibrationView",
  176. "M14.06,9L15,9.94L5.92,19H5V18.08L14.06,9M17.66,3C17.41,3 17.15,3.1 16.96,3.29L15.13,5.12L18.88,8.87L20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18.17,3.09 17.92,3 17.66,3M14.06,6.19L3,17.25V21H6.75L17.81,9.94L14.06,6.19Z");
  177. AddNav(RightNavItems, "统计", "StatisticsView",
  178. "M3,3H21V21H3V3Z M7,17V11 M12,17V7 M17,17V13");
  179. AddNav(RightNavItems, "用户", "UserView",
  180. "M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z");
  181. }
  182. private void AddNav(ObservableCollection<NavItem> collection, string title, string viewName, string iconData)
  183. {
  184. // Path.Data 在运行时不会自动把字符串转成 Geometry,
  185. // 必须在代码里 Geometry.Parse 成对象(并 Freeze 提升渲染性能/线程安全)
  186. var geometry = Geometry.Parse(iconData);
  187. geometry.Freeze();
  188. var item = new NavItem { Title = title, ViewName = viewName, IconData = geometry };
  189. item.SelectRequested += nav =>
  190. {
  191. if (CurrentView != nav.ViewName)
  192. Navigate(nav.ViewName);
  193. };
  194. collection.Add(item);
  195. }
  196. private void Navigate(string viewName)
  197. {
  198. if (string.IsNullOrEmpty(viewName)) return;
  199. _regionManager.RequestNavigate("ContentRegion", viewName, r =>
  200. {
  201. if (r.Result != true)
  202. {
  203. // 解包内部异常链,找到根因(Prism 只抛 ContainerResolutionException 外壳)
  204. var root = r.Error;
  205. var depth = 0;
  206. while (root?.InnerException != null && depth++ < 10)
  207. root = root.InnerException;
  208. AppLogger.Error($"导航到 {viewName} 失败,内容区保持原页面 | 根因: {root?.Message}", r.Error, "Navigation");
  209. }
  210. });
  211. CurrentView = viewName;
  212. // 每次导航都同步选中状态(含首次启动:
  213. // 初始 CurrentView 与目标相同,不能依赖"是否变化"判断,否则首页不会被选中)
  214. SyncNavSelection();
  215. }
  216. private void SyncNavSelection()
  217. {
  218. foreach (var nav in LeftNavItems)
  219. nav.IsSelected = nav.ViewName == CurrentView;
  220. foreach (var nav in RightNavItems)
  221. nav.IsSelected = nav.ViewName == CurrentView;
  222. }
  223. #endregion
  224. #region 定时刷新
  225. private void StartTimer()
  226. {
  227. // Loaded 事件在 WPF 中可能触发多次,避免重复创建定时器导致刷新回调叠加。
  228. if (_timer != null)
  229. return;
  230. _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
  231. _timer.Tick += OnTimerTick;
  232. _timer.Start();
  233. }
  234. private void OnTimerTick(object sender, EventArgs e)
  235. {
  236. RefreshDeviceStatuses();
  237. UpdateSystemInfo();
  238. }
  239. private void RefreshDeviceStatuses()
  240. {
  241. var statuses = CoreManager.GetAllDeviceStatuses();
  242. if (DeviceStatusItems.Count != statuses.Count)
  243. {
  244. DeviceStatusItems.Clear();
  245. foreach (var s in statuses)
  246. DeviceStatusItems.Add(new DeviceStatusItem { Name = s.Name, Category = L10n.T(s.Category), IsConnected = s.IsConnected });
  247. return;
  248. }
  249. // 设备数量未变时也要同步名称/类别(用户在设备管理页改名后即时反映到状态栏)
  250. for (int i = 0; i < statuses.Count; i++)
  251. {
  252. DeviceStatusItems[i].Name = statuses[i].Name;
  253. DeviceStatusItems[i].Category = L10n.T(statuses[i].Category);
  254. DeviceStatusItems[i].IsConnected = statuses[i].IsConnected;
  255. }
  256. }
  257. private void UpdateSystemInfo()
  258. {
  259. var proc = Process.GetCurrentProcess();
  260. MemoryUsage = L10n.F("内存: {0} MB", proc.PrivateMemorySize64 / 1024 / 1024);
  261. CurrentTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
  262. }
  263. #endregion
  264. }
  265. /// <summary>
  266. /// 顶部导航项(数据驱动生成导航按钮,新增页面只需在 MainWindowViewModel.InitNavItems 加一行)
  267. /// </summary>
  268. public class NavItem : TeamAAS.BindableBase
  269. {
  270. public string Title { get; set; }
  271. public string ViewName { get; set; }
  272. /// <summary>图标 Geometry 对象(Path.Data 直接绑定,避免运行时字符串→Geometry 转换失败)</summary>
  273. public Geometry IconData { get; set; }
  274. private bool _isSelected;
  275. public bool IsSelected
  276. {
  277. get => _isSelected;
  278. set
  279. {
  280. if (SetProperty(ref _isSelected, value) && value)
  281. SelectRequested?.Invoke(this);
  282. }
  283. }
  284. public event Action<NavItem> SelectRequested;
  285. }
  286. public class DeviceStatusItem : TeamAAS.BindableBase
  287. {
  288. private string _name;
  289. private string _category;
  290. private bool _isConnected;
  291. public string Name
  292. {
  293. get => _name;
  294. set => SetProperty(ref _name, value);
  295. }
  296. public string Category
  297. {
  298. get => _category;
  299. set => SetProperty(ref _category, value);
  300. }
  301. public bool IsConnected
  302. {
  303. get => _isConnected;
  304. set => SetProperty(ref _isConnected, value);
  305. }
  306. }
  307. }