HomeViewModel.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. using Prism.Commands;
  2. using System;
  3. using System.Collections.ObjectModel;
  4. using System.Linq;
  5. using System.Windows.Media;
  6. using TeamAAS.FlowEditor;
  7. using TeamAAS.FlowEditor.Execution;
  8. using TeamAAS.FlowEngine.Execution;
  9. using TeamAAS.Theme;
  10. using TeamAAS.Vision;
  11. namespace TeamAAS.ViewModels
  12. {
  13. public class HomeViewModel : TeamAAS.BindableBase
  14. {
  15. public ObservableCollection<string> Products { get; } = new ObservableCollection<string>();
  16. private string _selectedProduct = "";
  17. public string SelectedProduct
  18. {
  19. get => _selectedProduct;
  20. set => SetProperty(ref _selectedProduct, value);
  21. }
  22. public DelegateCommand LoadProductCommand { get; }
  23. public DelegateCommand ResetStatisticsCommand { get; }
  24. public DelegateCommand RunCommand { get; }
  25. public DelegateCommand StopCommand { get; }
  26. #region 运行状态(总运行/停止按钮)
  27. public bool IsRunning => FlowRunner.Instance.IsRunning;
  28. public bool CanRun => !IsRunning && !string.IsNullOrEmpty(FlowRunner.Instance.RunProductName);
  29. public bool CanStop => IsRunning;
  30. /// <summary>FlowRunner 运行状态变化后刷新按钮状态</summary>
  31. public void RefreshRunState()
  32. {
  33. RaisePropertyChanged(nameof(IsRunning));
  34. RaisePropertyChanged(nameof(CanRun));
  35. RaisePropertyChanged(nameof(CanStop));
  36. RaisePropertyChanged(nameof(RunProductName));
  37. }
  38. private async void RunAll()
  39. {
  40. if (string.IsNullOrEmpty(FlowRunner.Instance.RunProductName))
  41. {
  42. DialogHelper.ShowWarning("请先加载运行产品");
  43. return;
  44. }
  45. // 循环运行当前产品所有主流程,直到点击停止
  46. await FlowRunner.Instance.RunLoopAsync();
  47. }
  48. private void StopAll()
  49. {
  50. FlowRunner.Instance.Stop();
  51. }
  52. #endregion
  53. /// <summary>首页统计数据(真实运行数据,不再是写死的假数据)</summary>
  54. public RunStatistics Stats => RunStatistics.Instance;
  55. /// <summary>当前运行产品名(空=未加载)</summary>
  56. public string RunProductName => FlowRunner.Instance.RunProductName;
  57. #region 主题与主色(单一数据源:App.SystemConfig,切换后写入 SystemConfiguration.json)
  58. public bool IsDarkMode =>
  59. string.Equals(App.SystemConfig?.Theme, "Dark", StringComparison.OrdinalIgnoreCase);
  60. public string ThemeIcon => IsDarkMode ? "☀️" : "🌙";
  61. /// <summary>预设主色</summary>
  62. public ObservableCollection<AccentColorOption> AccentColors { get; }
  63. = new ObservableCollection<AccentColorOption>();
  64. /// <summary>当前主色画刷(ColorPicker 与色板联动)</summary>
  65. public Brush AccentBrush
  66. {
  67. get
  68. {
  69. try
  70. {
  71. var color = (Color)ColorConverter.ConvertFromString(App.SystemConfig?.AccentColor);
  72. // 规范化为主色不透明版本(配置里可能残留带 alpha 的历史值)
  73. var brush = new SolidColorBrush(Color.FromRgb(color.R, color.G, color.B));
  74. brush.Freeze();
  75. return brush;
  76. }
  77. catch { return Brushes.DodgerBlue; }
  78. }
  79. set => ApplyAccent(value); // ColorPicker TwoWay 回写:应用并持久化
  80. }
  81. public DelegateCommand ToggleThemeCommand { get; }
  82. public DelegateCommand<string> SetAccentCommand { get; }
  83. /// <summary>应用主色(色板按钮 / ColorPicker 回调共用)</summary>
  84. public void ApplyAccent(Brush brush)
  85. {
  86. if (!(brush is SolidColorBrush sb)) return;
  87. // 规范化:只取 RGB、强制不透明。ColorPicker 样式重算时会回写带任意 alpha 的画刷
  88. // (实测出现过 A=0x0A 的 96% 透明色被存进配置,表现为主色"特别淡、朦了一层"),
  89. // 统一丢弃 alpha——色相保真,主色永远纯色。
  90. var color = Color.FromRgb(sb.Color.R, sb.Color.G, sb.Color.B);
  91. // 按 RGB 去重:同色不同 alpha 的回写(切主题时样式重算触发)完全静默忽略
  92. var current = TryParseAccentColor();
  93. if (current.HasValue &&
  94. current.Value.R == color.R && current.Value.G == color.G && current.Value.B == color.B)
  95. {
  96. return;
  97. }
  98. App.SystemConfig.AccentColor = color.ToString();
  99. // 主色系画刷与文字对比色由主题覆盖层派生重建(毫秒级生效,不碰合并字典)
  100. ThemeManager.UpdateAccent(color);
  101. App.SaveSystemConfig();
  102. RaisePropertyChanged(nameof(AccentBrush));
  103. }
  104. private static Color? TryParseAccentColor()
  105. {
  106. try
  107. {
  108. var hex = App.SystemConfig?.AccentColor;
  109. if (string.IsNullOrWhiteSpace(hex)) return null;
  110. return (Color)ColorConverter.ConvertFromString(hex);
  111. }
  112. catch { return null; }
  113. }
  114. /// <summary>主题/主色在别处(顶部栏、设置页)变化后刷新本地状态</summary>
  115. public void RefreshThemeState()
  116. {
  117. RaisePropertyChanged(nameof(IsDarkMode));
  118. RaisePropertyChanged(nameof(ThemeIcon));
  119. RaisePropertyChanged(nameof(AccentBrush));
  120. }
  121. #endregion
  122. public HomeViewModel()
  123. {
  124. LoadProductCommand = new DelegateCommand(LoadProduct);
  125. ResetStatisticsCommand = new DelegateCommand(() => RunStatistics.Instance.Reset());
  126. RunCommand = new DelegateCommand(RunAll);
  127. StopCommand = new DelegateCommand(StopAll);
  128. ToggleThemeCommand = new DelegateCommand(() =>
  129. {
  130. App.SystemConfig.Theme = IsDarkMode ? "Light" : "Dark";
  131. ThemeManager.ApplyTheme(!IsDarkMode, App.SystemConfig.AccentColor);
  132. App.SaveSystemConfig();
  133. RaisePropertyChanged(nameof(IsDarkMode));
  134. RaisePropertyChanged(nameof(ThemeIcon));
  135. });
  136. SetAccentCommand = new DelegateCommand<string>(hex =>
  137. {
  138. if (string.IsNullOrWhiteSpace(hex)) return;
  139. App.SystemConfig.AccentColor = hex;
  140. // 主色系画刷与文字对比色由主题覆盖层派生重建(毫秒级生效)
  141. ThemeManager.UpdateAccent(hex);
  142. App.SaveSystemConfig();
  143. RaisePropertyChanged(nameof(AccentBrush));
  144. });
  145. InitAccentColors();
  146. RefreshProducts();
  147. // 产品管理页新增/删除/重命名产品后,同步刷新首页产品型号下拉
  148. ProductManager.Instance.ProductListChanged += OnProductListChanged;
  149. // 产品画面配置(画面数/排列/名称,跟产品走)变化后重建布局
  150. ProductManager.Instance.HomeFrameConfigChanged += RefreshEngineView;
  151. RefreshEngineView();
  152. }
  153. #region 主页视觉画面(引擎多画面视图 + 画面框注册)
  154. private object _engineMainView;
  155. /// <summary>当前视觉引擎的主页多画面视图(无可用引擎时为 null,显示占位提示)</summary>
  156. public object EngineMainView
  157. {
  158. get { return _engineMainView; }
  159. private set { SetProperty(ref _engineMainView, value); }
  160. }
  161. /// <summary>
  162. /// 按当前产品的画面配置刷新主页画面:应用当前视觉引擎的布局,
  163. /// 并把各画面框注册到 VisualFrameManager(唯一 ID + 用户可见名称),
  164. /// 其他页面/插件通过 ID 即可选择往哪个框显示结果。
  165. /// </summary>
  166. public void RefreshEngineView()
  167. {
  168. var manager = VisualEngineManager.Instance;
  169. var engine = manager.GetOrLoad(App.SystemConfig?.Vision?.EngineName);
  170. if (engine == null)
  171. {
  172. EngineMainView = null;
  173. return;
  174. }
  175. manager.SetCurrent(engine.EngineName);
  176. // 画面配置跟产品走(ProductMeta.HomeFrame*)
  177. // 注意:必须读"运行产品"(主页面刚加载的),而不是编辑态产品——
  178. // 主页面下拉直接加载某产品时编辑态(CurrentProductName)并未跟随,
  179. // 若读编辑态产品会导致首次加载的画面数/排列用的是别的产品的配置
  180. var pm = ProductManager.Instance;
  181. var configProduct = string.IsNullOrEmpty(pm.RunProductName) ? pm.CurrentProductName : pm.RunProductName;
  182. var cfg = pm.GetHomeFrameConfig(configProduct);
  183. // 画面列表(可空):0 个画面 = 主页面不显示画面框
  184. var count = cfg.HomeFrames?.Count ?? 0;
  185. engine.ApplyHomeLayout(count, (int)cfg.HomeFrameArrange);
  186. TeamAAS.Vision.VisualFrameManager.Clear();
  187. for (int i = 0; i < count; i++)
  188. {
  189. var frameId = $"home:{i + 1}";
  190. var frameName = cfg.GetFrameName(i);
  191. var showDot = cfg.GetFrameShowDot(i);
  192. TeamAAS.Vision.VisualFrameManager.Register(frameId, frameName, "Home");
  193. // 通过引擎抽象接口设置画面框标题与状态圆点显隐
  194. //(不直接引用引擎插件内部类型——引擎可替换)
  195. engine.SetFrameCaption(i, frameName);
  196. engine.SetFrameDotVisible(i, showDot);
  197. }
  198. EngineMainView = engine.GetMainView();
  199. }
  200. #endregion
  201. private void OnProductListChanged()
  202. {
  203. RefreshProducts();
  204. }
  205. private void InitAccentColors()
  206. {
  207. var presets = new[]
  208. {
  209. ("默认蓝", "#2D6CDF"),
  210. ("成功绿", "#4CAF50"),
  211. ("活力橙", "#FF9800"),
  212. ("警示红", "#F44336"),
  213. ("优雅紫", "#7B1FA2"),
  214. ("清新青", "#00BCD4"),
  215. ("少女粉", "#E91E63"),
  216. ("中性灰", "#607D8B"),
  217. };
  218. foreach (var (name, hex) in presets)
  219. {
  220. AccentColors.Add(new AccentColorOption
  221. {
  222. Name = name,
  223. Hex = hex,
  224. Brush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex))
  225. });
  226. }
  227. }
  228. public void RefreshProducts()
  229. {
  230. Products.Clear();
  231. var list = ProductManager.Instance.GetProductList();
  232. foreach (var name in list)
  233. Products.Add(name);
  234. // 当前选中的产品已被删除 → 清空或切到第一个;否则保留选中状态
  235. if (!Products.Contains(SelectedProduct))
  236. SelectedProduct = Products.Count > 0 ? Products.First() : "";
  237. }
  238. private void LoadProduct()
  239. {
  240. if (string.IsNullOrEmpty(SelectedProduct)) return;
  241. // 先停掉正在运行的流程(Stop 内部已完全释放 ResultRegistry.Main 的流程数据),再切换运行产品
  242. FlowRunner.Instance.Stop();
  243. // 清空视觉画面框注册
  244. TeamAAS.Vision.VisualFrameManager.Clear();
  245. // 加载为"运行产品"并复制运行态流程(真正接上运行引擎)
  246. if (FlowRunner.Instance.LoadRunProduct(SelectedProduct))
  247. {
  248. // 加载成功后刷新主页画面布局(应用新产品的画面配置,会重建视觉引擎布局)
  249. RefreshEngineView();
  250. RefreshRunState();
  251. DialogHelper.Success($"运行产品已切换为: {SelectedProduct}");
  252. }
  253. else
  254. {
  255. DialogHelper.ShowError($"加载运行产品失败: {SelectedProduct}");
  256. }
  257. }
  258. }
  259. /// <summary>预设主色选项</summary>
  260. public class AccentColorOption
  261. {
  262. public string Name { get; set; }
  263. public string Hex { get; set; }
  264. public Brush Brush { get; set; }
  265. }
  266. }