diff --git a/SHH.CameraDashboard/App.xaml b/SHH.CameraDashboard/App.xaml index 6998269..2957226 100644 --- a/SHH.CameraDashboard/App.xaml +++ b/SHH.CameraDashboard/App.xaml @@ -1,25 +1,22 @@ - + - - - - - - - - - - - - - + + + + + + + + + - + \ No newline at end of file diff --git a/SHH.CameraDashboard/App.xaml.cs b/SHH.CameraDashboard/App.xaml.cs index f60e5be..613b685 100644 --- a/SHH.CameraDashboard/App.xaml.cs +++ b/SHH.CameraDashboard/App.xaml.cs @@ -1,5 +1,4 @@ -using System.Configuration; -using System.Data; +using System.Collections.ObjectModel; using System.Windows; namespace SHH.CameraDashboard @@ -9,6 +8,35 @@ namespace SHH.CameraDashboard /// public partial class App : Application { - } + protected override async void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); -} + // 1. 【核心代码】程序启动时,异步读取配置文件 + var savedNodes = await LocalStorageService.LoadAsync>(AppPaths.ServiceNodesConfig); + if (savedNodes != null) + { + foreach (var node in savedNodes) + AppGlobal.ServiceNodes.Add(node); + } + + // 3. 启动主窗口 + // 注意:如果 LoadAsync 耗时较长,这里可能会导致启动画面停留, + // 实际项目中可以搞一个 Splash Screen (启动屏) 来做这件事。 + var mainWin = new MainWindow(); + mainWin.Show(); + } + + /// + /// 全局统一退出入口 + /// + public static void ShutdownApp() + { + // 1. 这里可以处理统一的资源清理逻辑 (如停止摄像头推流、关闭数据库连接) + // 2. 保存用户配置 + // 3. 彻底退出 + Current.Shutdown(); + } + + } +} \ No newline at end of file diff --git a/SHH.CameraDashboard/App/AppGlobal.cs b/SHH.CameraDashboard/App/AppGlobal.cs new file mode 100644 index 0000000..94d491d --- /dev/null +++ b/SHH.CameraDashboard/App/AppGlobal.cs @@ -0,0 +1,113 @@ +using System.Collections.ObjectModel; + +namespace SHH.CameraDashboard +{ + /// + /// 应用程序全局状态和事件总线。 + /// 此类作为一个静态的中央枢纽,用于在应用程序的不同部分之间共享数据和通信。 + /// + public static class AppGlobal + { + #region --- 全局数据存储 --- + + /// + /// 获取一个可观察的集合,用于存储和显示所有已配置的服务节点。 + /// 由于使用了 ,当集合内容发生变化时,UI(如 ListView)会自动更新。 + /// + public static ObservableCollection ServiceNodes { get; } + = new ObservableCollection(); + + /// + /// 获取或设置当前正在使用的服务节点。 + /// 当用户从列表中选择一个节点时,应更新此属性。 + /// + public static ServiceNodeModel? UseServiceNode { get; set; } + + #endregion + + #region --- 全局事件总线 --- + + #region CameraAdd + + /// + /// 当应用程序的任何部分请求添加一个新摄像头时发生。 + /// + public static event Action? OnRequestAddCamera; + + /// + /// 触发 事件,以请求打开摄像头添加界面。 + /// + public static void RequestAdd() + => OnRequestAddCamera?.Invoke(); + + #endregion + + #region CameraEdit + + /// + /// 当应用程序的任何部分请求编辑一个摄像头时发生。 + /// 事件处理程序将接收到要编辑的 实例。 + /// + public static event Action? OnRequestEditCamera; + + /// + /// 触发 事件,以请求打开摄像头编辑界面。 + /// + /// 要编辑的摄像头数据模型。 + public static void RequestEdit(WebApiCameraModel camera) + => OnRequestEditCamera?.Invoke(camera); + + #endregion + + #region CameraDelete + + /// + /// 当应用程序的任何部分请求删除一个摄像头时发生。 + /// 事件处理程序将接收到要删除的 实例。 + /// + public static event Action? OnRequestDeleteCamera; + + /// + /// 触发 事件,以请求删除指定的摄像头。 + /// + /// 要删除的摄像头数据模型。 + public static void RequestDelete(WebApiCameraModel camera) + => OnRequestDeleteCamera?.Invoke(camera); + + #endregion + + #region CameraRefreshList + + /// + /// 当应用程序的任何部分请求刷新摄像头列表时发生。 + /// + public static event Action? OnRefreshListRequest; + + /// + /// 触发 事件,以请求刷新摄像头列表数据。 + /// + public static void RequestRefresh() + => OnRefreshListRequest?.Invoke(); + + #endregion + + // [新增] 请求云台控制事件 + public static event Action? OnRequestPtzCamera; + + // [新增] 触发方法 + public static void RequestPtz(WebApiCameraModel camera) => OnRequestPtzCamera?.Invoke(camera); + + // 图像处理 + public static event Action? OnRequestImgProc; + + public static void RequestImgProc(WebApiCameraModel camera) => OnRequestImgProc?.Invoke(camera); + + // 1. 定义事件委托:当 ViewModel 请求订阅时触发 + public static event Action? OnRequestSubscription; + + // 2. 定义触发方法:供 CameraItemTopViewModel 调用 + public static void RequestSubscription(WebApiCameraModel camera) => OnRequestSubscription?.Invoke(camera); + + #endregion + } +} \ No newline at end of file diff --git a/SHH.CameraDashboard/App/AppGlobalData.cs b/SHH.CameraDashboard/App/AppGlobalData.cs deleted file mode 100644 index ca201f8..0000000 --- a/SHH.CameraDashboard/App/AppGlobalData.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace SHH.CameraDashboard -{ - // 2. 全局配置存储 - public static class AppGlobalData - { - public static List ActiveServerList { get; private set; } = new List(); - - public static void SaveConfig(IEnumerable nodes) - { - ActiveServerList.Clear(); - foreach (var node in nodes) - { - ActiveServerList.Add(new ServerNode { Ip = node.Ip, Port = node.Port }); - } - } - } -} \ No newline at end of file diff --git a/SHH.CameraDashboard/App/ThemeManager.cs b/SHH.CameraDashboard/App/ThemeManager.cs deleted file mode 100644 index b0c24ef..0000000 --- a/SHH.CameraDashboard/App/ThemeManager.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; - -namespace SHH.CameraDashboard -{ - public enum ThemeType { Dark, Light } - - public static class ThemeManager - { - public static void ChangeTheme(ThemeType theme) - { - var appResources = Application.Current.Resources; - - // 1. 找到旧的颜色字典并移除 - // 我们通过检查 Source 路径来识别它 - ResourceDictionary oldDict = null; - foreach (var dict in appResources.MergedDictionaries) - { - // 只要路径里包含 "Colors." 说明它是我们的皮肤文件 - if (dict.Source != null && dict.Source.OriginalString.Contains("Themes/Colors.")) - { - oldDict = dict; - break; - } - } - - if (oldDict != null) - { - appResources.MergedDictionaries.Remove(oldDict); - } - - // 2. 加载新字典 - string dictName = theme switch - { - ThemeType.Light => "/Style/Themes/Colors.Light.xaml", - ThemeType.Dark => "/Style/Themes/Colors.Dark.xaml", - _ => "/Style/Themes/Colors.Dark.xaml" // 默认 - }; - - var newDict = new ResourceDictionary - { - Source = new Uri(dictName, UriKind.Relative) - }; - - // 3. 添加到集合中 (建议加在最前面,或者根据索引位置) - appResources.MergedDictionaries.Add(newDict); - } - } -} diff --git a/SHH.CameraDashboard/App/WizardControl.xaml.cs b/SHH.CameraDashboard/App/WizardControl.xaml.cs deleted file mode 100644 index b96bcb8..0000000 --- a/SHH.CameraDashboard/App/WizardControl.xaml.cs +++ /dev/null @@ -1,94 +0,0 @@ -using SHH.CameraDashboard.Services; -using System.Collections.ObjectModel; -using System.Windows; -using System.Windows.Controls; -using System.Xml.Linq; - -namespace SHH.CameraDashboard -{ - public partial class WizardControl : UserControl - { - private const string ServerConfigFile = "servers.config.json"; - - // 绑定源 - public ObservableCollection Nodes { get; set; } = new ObservableCollection(); - - // 定义关闭事件,通知主窗体关闭模态框 - public event EventHandler RequestClose; - - public WizardControl() - { - InitializeComponent(); - - // 使用泛型加载,指定返回类型为 ObservableCollection - // 这里的 LoadServers 逻辑变为了通用的 Load - Nodes = StorageService.Load>(ServerConfigFile); - - NodeList.ItemsSource = Nodes; - } - - private void AddNode_Click(object sender, RoutedEventArgs e) - { - Nodes.Add(new ServerNode()); - } - - private void DeleteNode_Click(object sender, RoutedEventArgs e) - { - if (sender is Button btn && btn.DataContext is ServerNode node) - { - Nodes.Remove(node); - } - } - - private async void Check_Click(object sender, RoutedEventArgs e) - { - // 禁用按钮防止重复点击 - var btn = sender as Button; - if (btn != null) btn.IsEnabled = false; - - foreach (var node in Nodes) - { - node.SetResult(false, "⏳ 检测中..."); - - // 构造 URL - string url = $"http://{node.Ip}:{node.Port}/api/Cameras"; - // 或者是 /api/health,看你的后端提供什么接口 - - try - { - // 【修改】这里调用封装好的 HttpService - // 我们使用 TestConnectionAsync,它内部会触发 OnApiLog 事件记录日志 - bool isConnected = await HttpService.TestConnectionAsync(url); - - if (isConnected) - node.SetResult(true, "✅ 连接成功"); - else - node.SetResult(false, "❌ 状态码异常"); - } - catch (Exception ex) - { - // 异常也被 HttpService 记录了,这里只负责更新 UI 状态 - node.SetResult(false, "❌ 无法连接"); - } - } - - if (btn != null) btn.IsEnabled = true; - } - - private void Apply_Click(object sender, RoutedEventArgs e) - { - // 将当前的 Nodes 集合保存到指定文件 - StorageService.Save(Nodes, ServerConfigFile); - - // 同步到全局单例内存中 - AppGlobalData.SaveConfig(Nodes); - - RequestClose?.Invoke(this, EventArgs.Empty); - } - - private void Cancel_Click(object sender, RoutedEventArgs e) - { - RequestClose?.Invoke(this, EventArgs.Empty); - } - } -} \ No newline at end of file diff --git a/SHH.CameraDashboard/Controls/BottomDockControl.xaml b/SHH.CameraDashboard/Controls/BottomDockControl.xaml deleted file mode 100644 index f8ef694..0000000 --- a/SHH.CameraDashboard/Controls/BottomDockControl.xaml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/SHH.CameraDashboard/Controls/BottomDockControl.xaml.cs b/SHH.CameraDashboard/Controls/BottomDockControl.xaml.cs deleted file mode 100644 index bef341b..0000000 --- a/SHH.CameraDashboard/Controls/BottomDockControl.xaml.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; - -namespace SHH.CameraDashboard -{ - public partial class BottomDockControl : UserControl - { - private bool _isExpanded = false; - - public BottomDockControl() - { - InitializeComponent(); - - // 核心修正:这里必须使用 WebApiDiag,因为它对应你 XAML 里的 x:Name - if (this.WebApiDiag != null) - { - // 订阅 DiagnosticControl 抛出的关闭事件 - this.WebApiDiag.RequestCollapse += (s, e) => - { - // 当子页面点击“关闭”按钮时,执行收回面板的方法 - HideExpandedPanel(); - }; - } - } - - // 逻辑:隐藏上方的大面板 - private void HideExpandedPanel() - { - ExpandedPanel.Visibility = Visibility.Collapsed; - ArrowIcon.Text = "▲"; // 箭头恢复向上 - } - - // 接收全局日志,分发给内部控件,并更新状态栏摘要 - public void PushLog(ApiLogEntry log) - { - // 1. 推送给内部的诊断控件 (详细列表) - WebApiDiag.PushLog(log); - - // 2. 更新底部状态栏 (摘要) - string statusIcon = log.IsSuccess ? "✅" : "❌"; - LatestLogText.Text = $"{statusIcon} [{log.Time:HH:mm:ss}] {log.Method} {log.Url} ({log.StatusCode})"; - LatencyText.Text = $"{log.DurationMs}ms"; - - // 如果失败,可以将状态栏背景变红一下(可选) - if (!log.IsSuccess) - { - // 这里简单处理,如果想要复杂的动画可以使用 Storyboard - LatestLogText.Foreground = new SolidColorBrush(Colors.Yellow); - } - else - { - LatestLogText.Foreground = Brushes.White; - } - } - - private void TogglePanel_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) - { - _isExpanded = !_isExpanded; - ExpandedPanel.Visibility = _isExpanded ? Visibility.Visible : Visibility.Collapsed; - ArrowIcon.Text = _isExpanded ? "▼" : "▲"; - } - } -} \ No newline at end of file diff --git a/SHH.CameraDashboard/Controls/CameraListControl.xaml b/SHH.CameraDashboard/Controls/CameraListControl.xaml deleted file mode 100644 index 94f8ddc..0000000 --- a/SHH.CameraDashboard/Controls/CameraListControl.xaml +++ /dev/null @@ -1,247 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/SHH.CameraDashboard/Controls/CameraListControl.xaml.cs b/SHH.CameraDashboard/Controls/CameraListControl.xaml.cs deleted file mode 100644 index bb10155..0000000 --- a/SHH.CameraDashboard/Controls/CameraListControl.xaml.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System.Collections.ObjectModel; -using System.Windows; -using System.Windows.Controls; - -namespace SHH.CameraDashboard -{ - // 简单的包装类,用于 ComboBox 显示 - public class ServerOption - { - public string Name { get; set; } - - public string Ip { get; set; } - public int Port { get; set; } - // 修改显示属性:如果有名字就显示 名字(IP),没有就显示 IP - public string DisplayText => string.IsNullOrEmpty(Name) ? $"{Ip}:{Port}" : $"{Name} ({Ip})"; - } - - public partial class CameraListControl : UserControl - { - // 所有摄像头数据(原始全集) - private List _allCameras = new List(); - - // 绑定到界面的数据(过滤后) - public ObservableCollection DisplayCameras { get; set; } = new ObservableCollection(); - - public CameraListControl() - { - InitializeComponent(); - CameraList.ItemsSource = DisplayCameras; - - // 初始加载服务器列表 - ReloadServers(); - } - - /// - /// 公开方法:供主窗体在向导结束后调用,刷新下拉框 - /// - public void ReloadServers() - { - var savedSelection = ServerCombo.SelectedItem as ServerOption; - - // 1. 转换全局配置到 ComboBox 选项 - var options = AppGlobalData.ActiveServerList - .Select(n => new ServerOption { Ip = n.Ip, Port = n.Port }) - .ToList(); - - ServerCombo.ItemsSource = options; - - // 2. 尝试恢复之前的选中项,或者默认选中第一个 - if (options.Count > 0) - { - if (savedSelection != null) - { - var match = options.FirstOrDefault(o => o.Ip == savedSelection.Ip && o.Port == savedSelection.Port); - ServerCombo.SelectedItem = match ?? options[0]; - } - else - { - ServerCombo.SelectedItem = options[0]; - } - } - else - { - // 如果没有配置,清空列表 - _allCameras.Clear(); - DisplayCameras.Clear(); - UpdateEmptyState(); - } - } - - private async void ServerCombo_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (ServerCombo.SelectedItem is ServerOption server) - { - // 切换服务器,加载数据 - LoadingMask.Visibility = Visibility.Visible; - EmptyText.Visibility = Visibility.Collapsed; - - string url = $"http://{server.Ip}:{server.Port}/api/Cameras"; - - try - { - // 使用 HttpService 获取列表 - var list = await HttpService.GetAsync>(url); - - _allCameras = list ?? new List(); - - // 应用当前的搜索词 - FilterList(SearchBox.Text); - } - catch - { - // 失败清空 - _allCameras.Clear(); - DisplayCameras.Clear(); - } - finally - { - LoadingMask.Visibility = Visibility.Collapsed; - UpdateEmptyState(); - } - } - } - - private void SearchBox_TextChanged(object sender, TextChangedEventArgs e) - { - FilterList(SearchBox.Text); - } - - private void FilterList(string keyword) - { - DisplayCameras.Clear(); - - if (string.IsNullOrWhiteSpace(keyword)) - { - foreach (var c in _allCameras) DisplayCameras.Add(c); - } - else - { - var lowerKw = keyword.ToLower(); - var results = _allCameras.Where(c => - (c.Name != null && c.Name.ToLower().Contains(lowerKw)) || - (c.IpAddress != null && c.IpAddress.Contains(lowerKw)) - ); - - foreach (var c in results) DisplayCameras.Add(c); - } - } - - private void UpdateEmptyState() - { - EmptyText.Visibility = DisplayCameras.Count == 0 ? Visibility.Visible : Visibility.Collapsed; - } - - // 1. 定义一个事件:当设备被选中时触发 - public event System.Action OnDeviceSelected; - - // 2. 实现 ListView 的选中事件处理 - private void CameraList_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (CameraList.SelectedItem is CameraInfo selectedCam) - { - // 触发事件,把选中的相机传出去 - OnDeviceSelected?.Invoke(selectedCam); - } - } - } -} \ No newline at end of file diff --git a/SHH.CameraDashboard/Controls/DeviceHomeControl.xaml b/SHH.CameraDashboard/Controls/DeviceHomeControl.xaml deleted file mode 100644 index 5abdfdc..0000000 --- a/SHH.CameraDashboard/Controls/DeviceHomeControl.xaml +++ /dev/null @@ -1,199 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -