具备界面基础功能

This commit is contained in:
2026-01-01 22:40:32 +08:00
parent 0c86b4dad3
commit d039559402
81 changed files with 8333 additions and 1905 deletions

View File

@@ -0,0 +1,113 @@
using System.Collections.ObjectModel;
namespace SHH.CameraDashboard
{
/// <summary>
/// 应用程序全局状态和事件总线。
/// 此类作为一个静态的中央枢纽,用于在应用程序的不同部分之间共享数据和通信。
/// </summary>
public static class AppGlobal
{
#region --- ---
/// <summary>
/// 获取一个可观察的集合,用于存储和显示所有已配置的服务节点。
/// 由于使用了 <see cref="ObservableCollection{T}"/>当集合内容发生变化时UI如 ListView会自动更新。
/// </summary>
public static ObservableCollection<ServiceNodeModel> ServiceNodes { get; }
= new ObservableCollection<ServiceNodeModel>();
/// <summary>
/// 获取或设置当前正在使用的服务节点。
/// 当用户从列表中选择一个节点时,应更新此属性。
/// </summary>
public static ServiceNodeModel? UseServiceNode { get; set; }
#endregion
#region --- 线 ---
#region CameraAdd
/// <summary>
/// 当应用程序的任何部分请求添加一个新摄像头时发生。
/// </summary>
public static event Action? OnRequestAddCamera;
/// <summary>
/// 触发 <see cref="OnRequestAddCamera"/> 事件,以请求打开摄像头添加界面。
/// </summary>
public static void RequestAdd()
=> OnRequestAddCamera?.Invoke();
#endregion
#region CameraEdit
/// <summary>
/// 当应用程序的任何部分请求编辑一个摄像头时发生。
/// 事件处理程序将接收到要编辑的 <see cref="WebApiCameraModel"/> 实例。
/// </summary>
public static event Action<WebApiCameraModel>? OnRequestEditCamera;
/// <summary>
/// 触发 <see cref="OnRequestEditCamera"/> 事件,以请求打开摄像头编辑界面。
/// </summary>
/// <param name="camera">要编辑的摄像头数据模型。</param>
public static void RequestEdit(WebApiCameraModel camera)
=> OnRequestEditCamera?.Invoke(camera);
#endregion
#region CameraDelete
/// <summary>
/// 当应用程序的任何部分请求删除一个摄像头时发生。
/// 事件处理程序将接收到要删除的 <see cref="WebApiCameraModel"/> 实例。
/// </summary>
public static event Action<WebApiCameraModel>? OnRequestDeleteCamera;
/// <summary>
/// 触发 <see cref="OnRequestDeleteCamera"/> 事件,以请求删除指定的摄像头。
/// </summary>
/// <param name="camera">要删除的摄像头数据模型。</param>
public static void RequestDelete(WebApiCameraModel camera)
=> OnRequestDeleteCamera?.Invoke(camera);
#endregion
#region CameraRefreshList
/// <summary>
/// 当应用程序的任何部分请求刷新摄像头列表时发生。
/// </summary>
public static event Action? OnRefreshListRequest;
/// <summary>
/// 触发 <see cref="OnRefreshListRequest"/> 事件,以请求刷新摄像头列表数据。
/// </summary>
public static void RequestRefresh()
=> OnRefreshListRequest?.Invoke();
#endregion
// [新增] 请求云台控制事件
public static event Action<WebApiCameraModel>? OnRequestPtzCamera;
// [新增] 触发方法
public static void RequestPtz(WebApiCameraModel camera) => OnRequestPtzCamera?.Invoke(camera);
// 图像处理
public static event Action<WebApiCameraModel>? OnRequestImgProc;
public static void RequestImgProc(WebApiCameraModel camera) => OnRequestImgProc?.Invoke(camera);
// 1. 定义事件委托:当 ViewModel 请求订阅时触发
public static event Action<WebApiCameraModel>? OnRequestSubscription;
// 2. 定义触发方法:供 CameraItemTopViewModel 调用
public static void RequestSubscription(WebApiCameraModel camera) => OnRequestSubscription?.Invoke(camera);
#endregion
}
}

View File

@@ -1,17 +0,0 @@
namespace SHH.CameraDashboard
{
// 2. 全局配置存储
public static class AppGlobalData
{
public static List<ServerNode> ActiveServerList { get; private set; } = new List<ServerNode>();
public static void SaveConfig(IEnumerable<ServerNode> nodes)
{
ActiveServerList.Clear();
foreach (var node in nodes)
{
ActiveServerList.Add(new ServerNode { Ip = node.Ip, Port = node.Port });
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -1,172 +0,0 @@
<UserControl
x:Class="SHH.CameraDashboard.WizardControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Width="800"
Height="550"
mc:Ignorable="d">
<Border
Background="{DynamicResource Brush.Bg.Window}"
BorderBrush="{DynamicResource Brush.Border.Focus}"
BorderThickness="1"
CornerRadius="{StaticResource Radius.Normal}">
<Grid Margin="25">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Margin="0,0,0,20">
<DockPanel LastChildFill="False">
<TextBlock
VerticalAlignment="Center"
FontSize="{StaticResource Size.Font.Huge}"
FontWeight="Bold"
Foreground="{DynamicResource Brush.Text.Primary}"
Text="📡 服务节点配置" />
<Button
Width="30"
Height="30"
Click="Cancel_Click"
Content="✕"
DockPanel.Dock="Right"
Style="{StaticResource Btn.Ghost}" />
</DockPanel>
<TextBlock
Margin="0,10,0,0"
Foreground="{DynamicResource Brush.Text.Secondary}"
Text="请添加或修改后端的 IP 地址与端口,配置将用于聚合监控数据。" />
</StackPanel>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border
Padding="10"
Background="{DynamicResource Brush.Bg.Panel}"
BorderBrush="{DynamicResource Brush.Border}"
BorderThickness="1,1,1,0">
<DockPanel LastChildFill="False">
<TextBlock
VerticalAlignment="Center"
FontWeight="Bold"
Text="节点清单" />
<Button
Height="26"
Padding="15,0"
Background="{DynamicResource Brush.Accent}"
BorderThickness="0"
Click="AddNode_Click"
Content="+ 新增一行"
DockPanel.Dock="Right"
Style="{StaticResource {x:Type Button}}" />
</DockPanel>
</Border>
<ListView
x:Name="NodeList"
Grid.Row="1"
Background="Transparent"
BorderBrush="{DynamicResource Brush.Border}"
BorderThickness="1"
ItemContainerStyle="{StaticResource Style.ListViewItem.Table}">
<ListView.View>
<GridView ColumnHeaderContainerStyle="{StaticResource Style.GridViewHeader.Flat}">
<GridViewColumn Width="180" Header="节点名称">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Style="{StaticResource Style.TextBox.InTable}" Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="180" Header="IP 地址">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Style="{StaticResource Style.TextBox.InTable}" Text="{Binding Ip, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="100" Header="端口">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Style="{StaticResource Style.TextBox.InTable}" Text="{Binding Port, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="180" Header="连通性状态">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Border
Padding="8,2"
HorizontalAlignment="Left"
Background="#11000000"
CornerRadius="4">
<TextBlock
VerticalAlignment="Center"
FontWeight="Bold"
Foreground="{Binding StatusColor}"
Text="{Binding Status}" />
</Border>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="80" Header="操作">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button
Width="45"
Height="24"
Padding="0"
Click="DeleteNode_Click"
Content="删除"
FontSize="11"
Style="{StaticResource Btn.Danger}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</Grid>
<StackPanel
Grid.Row="2"
Margin="0,20,0,0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<TextBlock
Margin="0,0,15,0"
VerticalAlignment="Center"
FontSize="12"
Foreground="{DynamicResource Brush.Text.Secondary}"
Text="修改后请点击检测 →" />
<Button
Width="110"
Height="32"
Margin="0,0,10,0"
Click="Check_Click"
Content="🔍 立即检测"
Style="{StaticResource Btn.Ghost}" />
<Button
Width="120"
Height="32"
Background="{DynamicResource Brush.Accent}"
BorderThickness="0"
Click="Apply_Click"
Content="保存并应用"
Foreground="White" />
</StackPanel>
</Grid>
</Border>
</UserControl>

View File

@@ -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<ServerNode> Nodes { get; set; } = new ObservableCollection<ServerNode>();
// 定义关闭事件,通知主窗体关闭模态框
public event EventHandler RequestClose;
public WizardControl()
{
InitializeComponent();
// 使用泛型加载,指定返回类型为 ObservableCollection<ServerNode>
// 这里的 LoadServers 逻辑变为了通用的 Load<T>
Nodes = StorageService.Load<ObservableCollection<ServerNode>>(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);
}
}
}