具备界面基础功能
This commit is contained in:
@@ -1,25 +1,22 @@
|
||||
<Application x:Class="SHH.CameraDashboard.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:SHH.CameraDashboard"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application
|
||||
x:Class="SHH.CameraDashboard.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:SHH.CameraDashboard"
|
||||
xmlns:vm="clr-namespace:SHH.CameraDashboard">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<!-- 颜色主题(深色 / 浅色) -->
|
||||
<ResourceDictionary Source="/Style/Themes/Colors.Dark.xaml"/>
|
||||
<!-- 如果需要支持浅色主题,可以在 ThemeManager 中切换 Colors.Light.xaml -->
|
||||
|
||||
<!-- 尺寸定义(字号、圆角、边距等) -->
|
||||
<ResourceDictionary Source="/Style/Themes/Sizes.xaml"/>
|
||||
|
||||
<!-- 通用控件样式(Button、TextBox、ListViewItem 等) -->
|
||||
<ResourceDictionary Source="/Style/Themes/Styles.xaml"/>
|
||||
|
||||
<!-- 如果有专用控件样式,可以单独放在这里 -->
|
||||
<!-- <ResourceDictionary Source="/Style/Themes/Wizard.xaml"/> -->
|
||||
<!-- <ResourceDictionary Source="/Style/Themes/Diagnostic.xaml"/> -->
|
||||
<ResourceDictionary Source="/Style/Themes/Colors.Dark.xaml" />
|
||||
<ResourceDictionary Source="/Style/Themes/Icons.xaml" />
|
||||
<ResourceDictionary Source="/Style/Themes/Sizes.xaml" />
|
||||
<ResourceDictionary Source="/Style/Themes/Styles.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<DataTemplate DataType="{x:Type vm:WizardClientsViewModel}">
|
||||
<local:WizardClientsControl />
|
||||
</DataTemplate>
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
</Application>
|
||||
@@ -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
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
protected override async void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
}
|
||||
// 1. 【核心代码】程序启动时,异步读取配置文件
|
||||
var savedNodes = await LocalStorageService.LoadAsync<ObservableCollection<ServiceNodeModel>>(AppPaths.ServiceNodesConfig);
|
||||
if (savedNodes != null)
|
||||
{
|
||||
foreach (var node in savedNodes)
|
||||
AppGlobal.ServiceNodes.Add(node);
|
||||
}
|
||||
|
||||
// 3. 启动主窗口
|
||||
// 注意:如果 LoadAsync 耗时较长,这里可能会导致启动画面停留,
|
||||
// 实际项目中可以搞一个 Splash Screen (启动屏) 来做这件事。
|
||||
var mainWin = new MainWindow();
|
||||
mainWin.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全局统一退出入口
|
||||
/// </summary>
|
||||
public static void ShutdownApp()
|
||||
{
|
||||
// 1. 这里可以处理统一的资源清理逻辑 (如停止摄像头推流、关闭数据库连接)
|
||||
// 2. 保存用户配置
|
||||
// 3. 彻底退出
|
||||
Current.Shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
113
SHH.CameraDashboard/App/AppGlobal.cs
Normal file
113
SHH.CameraDashboard/App/AppGlobal.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
<UserControl
|
||||
x:Class="SHH.CameraDashboard.BottomDockControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:SHH.CameraDashboard.Controls">
|
||||
|
||||
<Grid VerticalAlignment="Bottom">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="30" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border
|
||||
x:Name="ExpandedPanel"
|
||||
Grid.Row="0"
|
||||
Height="250"
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Visibility="Collapsed">
|
||||
<local:DiagnosticControl x:Name="WebApiDiag" />
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Background="{DynamicResource Brush.Brand}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Cursor="Hand"
|
||||
MouseLeftButtonDown="TogglePanel_Click">
|
||||
<DockPanel Margin="10,0" LastChildFill="False">
|
||||
<TextBlock
|
||||
x:Name="LatestLogText"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="11"
|
||||
Foreground="White"
|
||||
Text="准备就绪" />
|
||||
|
||||
<TextBlock
|
||||
x:Name="ArrowIcon"
|
||||
Margin="10,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
Foreground="White"
|
||||
Text="▲" />
|
||||
|
||||
<StackPanel
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,0,5,0"
|
||||
FontSize="10"
|
||||
Foreground="#EEE"
|
||||
Opacity="0.7"
|
||||
Text="API Latency:" />
|
||||
<TextBlock
|
||||
x:Name="LatencyText"
|
||||
FontSize="10"
|
||||
FontWeight="Bold"
|
||||
Foreground="White"
|
||||
Text="0ms" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -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 ? "▼" : "▲";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
<UserControl
|
||||
x:Class="SHH.CameraDashboard.CameraListControl"
|
||||
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"
|
||||
xmlns:models="clr-namespace:SHH.CameraDashboard"
|
||||
d:DesignHeight="600"
|
||||
d:DesignWidth="250"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
|
||||
|
||||
<Style x:Key="Style.OnlineLed" TargetType="Ellipse">
|
||||
<Setter Property="Width" Value="8" />
|
||||
<Setter Property="Height" Value="8" />
|
||||
<Setter Property="Fill" Value="#666666" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsPhysicalOnline}" Value="True">
|
||||
<Setter Property="Fill" Value="{DynamicResource Brush.Status.Success}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsPhysicalOnline}" Value="False">
|
||||
<Setter Property="Fill" Value="{DynamicResource Brush.Status.Danger}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Style.RunningStatusBox" TargetType="Border">
|
||||
<Setter Property="Width" Value="14" />
|
||||
<Setter Property="Height" Value="14" />
|
||||
<Setter Property="CornerRadius" Value="2" />
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Status.Warning}" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsRunning}" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" Margin="10,10,10,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="📡" />
|
||||
<ComboBox
|
||||
x:Name="ServerCombo"
|
||||
Grid.Column="1"
|
||||
Height="30"
|
||||
DisplayMemberPath="DisplayText"
|
||||
SelectionChanged="ServerCombo_SelectionChanged"
|
||||
Style="{StaticResource {x:Type ComboBox}}" />
|
||||
</Grid>
|
||||
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Margin="10,5,10,10"
|
||||
Background="{DynamicResource Brush.Bg.Input}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{StaticResource Radius.Small}">
|
||||
<Grid>
|
||||
<TextBox
|
||||
x:Name="SearchBox"
|
||||
Height="28"
|
||||
Padding="8,0"
|
||||
VerticalContentAlignment="Center"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
TextChanged="SearchBox_TextChanged" />
|
||||
<TextBlock
|
||||
Margin="10,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
IsHitTestVisible="False"
|
||||
Text="🔍 搜索摄像头...">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Text, ElementName=SearchBox}" Value="">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<ListView
|
||||
x:Name="CameraList"
|
||||
Grid.Row="2"
|
||||
Padding="0,0,0,10"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
SelectionChanged="CameraList_SelectionChanged">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type models:CameraInfo}">
|
||||
<Grid>
|
||||
<Grid Margin="0,6" Background="#01FFFFFF">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="22" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Ellipse
|
||||
Margin="0,5,0,0"
|
||||
VerticalAlignment="Top"
|
||||
Style="{StaticResource Style.OnlineLed}" />
|
||||
|
||||
<StackPanel Grid.Column="1">
|
||||
<StackPanel Margin="0,0,0,4" Orientation="Horizontal">
|
||||
|
||||
<TextBlock
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="{Binding DisplayName}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
|
||||
<DockPanel LastChildFill="False">
|
||||
<TextBlock
|
||||
FontFamily="Consolas"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="{Binding Name}" />
|
||||
|
||||
<Border DockPanel.Dock="Right" Style="{StaticResource Style.RunningStatusBox}">
|
||||
<Path
|
||||
x:Name="PlayIcon"
|
||||
Margin="3,2"
|
||||
Data="M3,2.5 L9,6 L3,9.5 Z"
|
||||
Fill="White"
|
||||
Stretch="Fill">
|
||||
<Path.Style>
|
||||
<Style TargetType="Path">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsRunning}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Path.Style>
|
||||
</Path>
|
||||
</Border>
|
||||
</DockPanel>
|
||||
|
||||
<TextBlock
|
||||
Margin="0,4,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="{Binding MediaDetail}"
|
||||
Visibility="{Binding IsRunning, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Border
|
||||
Margin="0,0,0,0"
|
||||
Padding="3,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Background="{DynamicResource Brush.Bg.L4}"
|
||||
CornerRadius="2">
|
||||
<TextBlock
|
||||
FontSize="9"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="{Binding Brand}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style TargetType="ListViewItem">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListViewItem">
|
||||
<Border
|
||||
x:Name="Bd"
|
||||
Margin="5,1"
|
||||
Padding="8,4"
|
||||
CornerRadius="4">
|
||||
<ContentPresenter />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Brush.Bg.Hover}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Brush.Bg.L4}" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Brush.Accent}" />
|
||||
<Setter TargetName="Bd" Property="BorderThickness" Value="0.5" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
</ListView>
|
||||
|
||||
<Grid
|
||||
x:Name="LoadingMask"
|
||||
Grid.Row="2"
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
Opacity="0.8"
|
||||
Visibility="Collapsed">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource Brush.Accent}"
|
||||
Text="加载中..." />
|
||||
</Grid>
|
||||
<TextBlock
|
||||
x:Name="EmptyText"
|
||||
Grid.Row="2"
|
||||
Margin="0,50,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="暂无数据"
|
||||
Visibility="Collapsed" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -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<CameraInfo> _allCameras = new List<CameraInfo>();
|
||||
|
||||
// 绑定到界面的数据(过滤后)
|
||||
public ObservableCollection<CameraInfo> DisplayCameras { get; set; } = new ObservableCollection<CameraInfo>();
|
||||
|
||||
public CameraListControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
CameraList.ItemsSource = DisplayCameras;
|
||||
|
||||
// 初始加载服务器列表
|
||||
ReloadServers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公开方法:供主窗体在向导结束后调用,刷新下拉框
|
||||
/// </summary>
|
||||
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<List<CameraInfo>>(url);
|
||||
|
||||
_allCameras = list ?? new List<CameraInfo>();
|
||||
|
||||
// 应用当前的搜索词
|
||||
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<CameraInfo> OnDeviceSelected;
|
||||
|
||||
// 2. 实现 ListView 的选中事件处理
|
||||
private void CameraList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (CameraList.SelectedItem is CameraInfo selectedCam)
|
||||
{
|
||||
// 触发事件,把选中的相机传出去
|
||||
OnDeviceSelected?.Invoke(selectedCam);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
<UserControl
|
||||
x:Class="SHH.CameraDashboard.DeviceHomeControl"
|
||||
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"
|
||||
d:DesignHeight="600"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Background="{DynamicResource Brush.Bg.Window}">
|
||||
|
||||
<Grid x:Name="EmptyView" Visibility="Visible">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="60"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Opacity="0.2"
|
||||
Text="📺" />
|
||||
<TextBlock
|
||||
Margin="0,20,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="请在左侧选择一台设备以查看详情" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid x:Name="DetailView" Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="60" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border
|
||||
Grid.Row="0"
|
||||
Padding="20,0"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<DockPanel>
|
||||
<StackPanel VerticalAlignment="Center" Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,0,15,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="24"
|
||||
Text="📷" />
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
x:Name="TxtName"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="Camera #001" />
|
||||
<StackPanel Margin="0,5,0,0" Orientation="Horizontal">
|
||||
<Border
|
||||
x:Name="BadgeStatus"
|
||||
Margin="0,0,10,0"
|
||||
Padding="6,2"
|
||||
Background="#224ec9b0"
|
||||
CornerRadius="4">
|
||||
<TextBlock
|
||||
x:Name="TxtStatus"
|
||||
FontSize="11"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Status.Success}"
|
||||
Text="在线" />
|
||||
</Border>
|
||||
<TextBlock
|
||||
x:Name="TxtIp"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="192.168.1.100" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Margin="5,0"
|
||||
Content="⚙️ 配置"
|
||||
Style="{StaticResource Btn.Ghost}" />
|
||||
<Button
|
||||
Margin="5,0"
|
||||
Content="🔄 重启"
|
||||
Style="{StaticResource Btn.Ghost}" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="1" Margin="20">
|
||||
<Border Background="Black" CornerRadius="{StaticResource Radius.Normal}">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="20"
|
||||
Foreground="#444"
|
||||
Text="Live Stream" />
|
||||
<TextBlock
|
||||
x:Name="TxtStreamUrl"
|
||||
Margin="0,10,0,0"
|
||||
Foreground="#333"
|
||||
Text="rtsp://..." />
|
||||
|
||||
<ProgressBar
|
||||
Width="150"
|
||||
Height="2"
|
||||
Margin="0,20,0,0"
|
||||
IsIndeterminate="True"
|
||||
Opacity="0.5" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Margin="15"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Foreground="White"
|
||||
Opacity="0.8"
|
||||
Text="{Binding ElementName=TxtName, Path=Text}">
|
||||
<TextBlock.Effect>
|
||||
<DropShadowEffect
|
||||
BlurRadius="3"
|
||||
Opacity="0.8"
|
||||
ShadowDepth="2"
|
||||
Color="Black" />
|
||||
</TextBlock.Effect>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="2" Margin="20,0,20,20">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border
|
||||
Margin="0,0,10,0"
|
||||
Padding="15"
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
CornerRadius="5">
|
||||
<StackPanel>
|
||||
<TextBlock Foreground="{DynamicResource Brush.Text.Secondary}" Text="实时帧率" />
|
||||
<TextBlock
|
||||
Margin="0,5,0,0"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="25 FPS" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
Grid.Column="1"
|
||||
Margin="5,0"
|
||||
Padding="15"
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
CornerRadius="5">
|
||||
<StackPanel>
|
||||
<TextBlock Foreground="{DynamicResource Brush.Text.Secondary}" Text="累计丢包" />
|
||||
<TextBlock
|
||||
Margin="0,5,0,0"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="0.02%" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
Grid.Column="2"
|
||||
Margin="10,0,0,0"
|
||||
Padding="15"
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
CornerRadius="5">
|
||||
<StackPanel>
|
||||
<TextBlock Foreground="{DynamicResource Brush.Text.Secondary}" Text="运行时间" />
|
||||
<TextBlock
|
||||
Margin="0,5,0,0"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="12d 4h 20m" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,52 +0,0 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public partial class DeviceHomeControl : UserControl
|
||||
{
|
||||
private CameraInfo _currentDevice;
|
||||
|
||||
public DeviceHomeControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
// 供外部调用,切换显示的设备
|
||||
public void UpdateDevice(CameraInfo device)
|
||||
{
|
||||
_currentDevice = device;
|
||||
|
||||
if (device == null)
|
||||
{
|
||||
EmptyView.Visibility = Visibility.Visible;
|
||||
DetailView.Visibility = Visibility.Collapsed;
|
||||
return;
|
||||
}
|
||||
|
||||
// 切换到详情视图
|
||||
EmptyView.Visibility = Visibility.Collapsed;
|
||||
DetailView.Visibility = Visibility.Visible;
|
||||
|
||||
// 绑定数据到界面控件
|
||||
TxtName.Text = device.DisplayName;
|
||||
TxtIp.Text = device.IpAddress;
|
||||
TxtStreamUrl.Text = $"rtsp://{device.IpAddress}:554/live/main";
|
||||
|
||||
// 根据状态切换颜色和文字
|
||||
if (device.Status == "Playing" || device.Status == "Connected")
|
||||
{
|
||||
TxtStatus.Text = "在线运行";
|
||||
TxtStatus.Foreground = new SolidColorBrush(Color.FromRgb(78, 201, 176)); // Green
|
||||
BadgeStatus.Background = new SolidColorBrush(Color.FromArgb(30, 78, 201, 176));
|
||||
}
|
||||
else
|
||||
{
|
||||
TxtStatus.Text = "离线/断开";
|
||||
TxtStatus.Foreground = new SolidColorBrush(Color.FromRgb(244, 71, 71)); // Red
|
||||
BadgeStatus.Background = new SolidColorBrush(Color.FromArgb(30, 244, 71, 71));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
<UserControl x:Class="SHH.CameraDashboard.Controls.DiagnosticControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<Grid Background="{DynamicResource Brush.Bg.Window}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="35"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" Background="{DynamicResource Brush.Bg.Panel}"
|
||||
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1" Padding="10,0">
|
||||
<DockPanel LastChildFill="False">
|
||||
<TextBlock Text="🌐 WebAPI 诊断日志" VerticalAlignment="Center" FontWeight="Bold" Foreground="{DynamicResource Brush.Text.Primary}"/>
|
||||
<TextBlock Text="{Binding Logs.Count, StringFormat='(共 {0} 条)'}" VerticalAlignment="Center" Margin="10,0,0,0" Foreground="{DynamicResource Brush.Text.Secondary}" FontSize="11"/>
|
||||
|
||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
|
||||
<Button Content="🗑 清空" Click="Clear_Click" Style="{StaticResource Btn.Ghost}" Height="24" FontSize="11" Padding="10,0"/>
|
||||
<Button Content="✕" Click="Close_Click" Style="{StaticResource Btn.Ghost}" Height="24" Width="30" Margin="5,0,0,0"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MinWidth="250"/>
|
||||
<ColumnDefinition Width="5"/>
|
||||
<ColumnDefinition Width="*" MinWidth="300"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ListView x:Name="LogList" Grid.Column="0" SelectionChanged="LogList_SelectionChanged"
|
||||
ItemContainerStyle="{StaticResource Style.ListViewItem.Table}" Background="Transparent" BorderThickness="0">
|
||||
<ListView.View>
|
||||
<GridView ColumnHeaderContainerStyle="{StaticResource Style.GridViewHeader.Flat}">
|
||||
<GridViewColumn Header="请求时间" Width="90">
|
||||
<GridViewColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Time}" Foreground="{DynamicResource Brush.Text.Secondary}" FontFamily="Consolas"/>
|
||||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
</GridViewColumn>
|
||||
<GridViewColumn Header="URL" Width="200">
|
||||
<GridViewColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Url}" TextTrimming="CharacterEllipsis"/>
|
||||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
</GridViewColumn>
|
||||
<GridViewColumn Header="状态码" Width="60">
|
||||
<GridViewColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding StatusCode}" Foreground="{Binding StatusColor}" FontWeight="Bold"/>
|
||||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
</GridViewColumn>
|
||||
</GridView>
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
|
||||
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" Background="{DynamicResource Brush.Bg.Panel}"/>
|
||||
|
||||
<Grid Grid.Column="2" Background="{DynamicResource Brush.Bg.Input}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Border Background="{DynamicResource Brush.Bg.Panel}" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border}">
|
||||
<DockPanel LastChildFill="False">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<RadioButton x:Name="BtnReq" Content="Request" IsChecked="True" GroupName="Logs" Style="{StaticResource TabRadioStyle}" Checked="Tab_Checked"/>
|
||||
<RadioButton x:Name="BtnResp" Content="Response" GroupName="Logs" Style="{StaticResource TabRadioStyle}" Checked="Tab_Checked"/>
|
||||
</StackPanel>
|
||||
<Button Content="📋 复制" Click="Copy_Click" DockPanel.Dock="Right" Style="{StaticResource Btn.Ghost}" Height="22" Margin="0,0,10,0"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<TextBox x:Name="TxtContent" Grid.Row="1" Style="{StaticResource Style.TextBox.CodeEditor}"/>
|
||||
<StackPanel x:Name="TxtEmpty" Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<TextBlock Text="请选择一条日志查看详情" Foreground="{DynamicResource Brush.Text.Secondary}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,106 +0,0 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SHH.CameraDashboard.Controls
|
||||
{
|
||||
public partial class DiagnosticControl : UserControl
|
||||
{
|
||||
public event EventHandler RequestCollapse;
|
||||
private ApiLogEntry _selectedItem;
|
||||
private bool _isInitialized = false;
|
||||
|
||||
public DiagnosticControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
// 外部(MainWindow或BottomDock)调用此方法推送日志
|
||||
public void PushLog(ApiLogEntry entry)
|
||||
{
|
||||
this.Dispatcher.Invoke(() => {
|
||||
// 确保 XAML 中的 ListView 名称是 LogList
|
||||
LogList.Items.Insert(0, entry);
|
||||
|
||||
// 限制日志数量,防止内存溢出(可选)
|
||||
if (LogList.Items.Count > 100) LogList.Items.RemoveAt(100);
|
||||
});
|
||||
}
|
||||
|
||||
private void Close_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RequestCollapse?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void LogList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (!_isInitialized) return;
|
||||
_selectedItem = LogList.SelectedItem as ApiLogEntry;
|
||||
UpdateDetailView();
|
||||
}
|
||||
|
||||
private void Tab_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!_isInitialized) return;
|
||||
UpdateDetailView();
|
||||
}
|
||||
|
||||
private void UpdateDetailView()
|
||||
{
|
||||
// 防御性编程:检查所有可能为 null 的 UI 元素
|
||||
if (TxtEmpty == null || TxtContent == null || BtnReq == null) return;
|
||||
|
||||
if (_selectedItem == null)
|
||||
{
|
||||
TxtEmpty.Visibility = Visibility.Visible;
|
||||
TxtContent.Visibility = Visibility.Collapsed; // 隐藏编辑框更美观
|
||||
TxtContent.Text = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
TxtEmpty.Visibility = Visibility.Collapsed;
|
||||
TxtContent.Visibility = Visibility.Visible;
|
||||
|
||||
// 根据切换按钮显示对应内容
|
||||
TxtContent.Text = (BtnReq.IsChecked == true)
|
||||
? _selectedItem.RequestBody
|
||||
: _selectedItem.ResponseBody;
|
||||
}
|
||||
|
||||
private void Clear_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 具体的清空逻辑
|
||||
LogList.Items.Clear();
|
||||
_selectedItem = null;
|
||||
UpdateDetailView();
|
||||
}
|
||||
|
||||
private void Copy_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 如果没选中或内容本身为空,不执行复制
|
||||
if (_selectedItem == null || TxtContent == null || string.IsNullOrEmpty(TxtContent.Text)) return;
|
||||
|
||||
// 使用之前定义的带重试机制的 Helper
|
||||
ClipboardHelper.SetText(TxtContent.Text);
|
||||
}
|
||||
|
||||
// 右键菜单复制逻辑
|
||||
private void CopySummary_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (LogList.SelectedItem is ApiLogEntry item)
|
||||
ClipboardHelper.SetText($"[{item.Time}] {item.Url} - {item.StatusCode}");
|
||||
}
|
||||
|
||||
private void CopyRequest_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (LogList.SelectedItem is ApiLogEntry item)
|
||||
ClipboardHelper.SetText(item.RequestBody ?? "");
|
||||
}
|
||||
|
||||
private void CopyResponse_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (LogList.SelectedItem is ApiLogEntry item)
|
||||
ClipboardHelper.SetText(item.ResponseBody ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
29
SHH.CameraDashboard/Converters/BoolToMaxScaleConverter.cs
Normal file
29
SHH.CameraDashboard/Converters/BoolToMaxScaleConverter.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
/// <summary>
|
||||
/// 布尔值转最大缩放比例转换器
|
||||
/// </summary>
|
||||
public class BoolToMaxScaleConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
// 如果 AllowExpand (value) 为 true
|
||||
if (value is bool allow && allow)
|
||||
{
|
||||
// 允许放大:最大支持 200% (即放大 2 倍)
|
||||
// 你也可以改成 300 或根据业务需求调整
|
||||
return 200.0;
|
||||
}
|
||||
|
||||
// 不允许放大:最大限制在 100% (原图大小)
|
||||
return 100.0;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
// 2. 对象为空时显示,不为空时隐藏 (用于显示提示文字)
|
||||
public class InverseNullToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value == null ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
19
SHH.CameraDashboard/Converters/NullToVisibilityConverter.cs
Normal file
19
SHH.CameraDashboard/Converters/NullToVisibilityConverter.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
// 1. 对象为空时隐藏,不为空时显示
|
||||
public class NullToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value == null ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
34
SHH.CameraDashboard/Converters/SubscriptionTypeConverter.cs
Normal file
34
SHH.CameraDashboard/Converters/SubscriptionTypeConverter.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// 将订阅类型的 int 值 (0,1,2...) 转换为中文描述
|
||||
/// </summary>
|
||||
public class SubscriptionTypeConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is int typeId)
|
||||
{
|
||||
return typeId switch
|
||||
{
|
||||
0 => "🖥️ 本地窗口预览",
|
||||
1 => "📼 本地录像存储",
|
||||
2 => "🪟 句柄绑定显示",
|
||||
3 => "📡 网络转发传输",
|
||||
4 => "🌐 网页端推流",
|
||||
_ => $"未知类型 ({typeId})"
|
||||
};
|
||||
}
|
||||
// 如果后端传回的是字符串枚举 (兼容性),也可以尝试转换
|
||||
return value?.ToString() ?? "未知";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
SHH.CameraDashboard/Core/AppPaths.cs
Normal file
29
SHH.CameraDashboard/Core/AppPaths.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.IO;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public static class AppPaths
|
||||
{
|
||||
// 1. 基础目录:运行目录下的 Configs 文件夹
|
||||
public static readonly string BaseDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configs");
|
||||
|
||||
// 2. 具体的配置文件路径
|
||||
// 服务节点配置
|
||||
public static string ServiceNodesConfig => Path.Combine(BaseDir, "service_nodes.json");
|
||||
|
||||
// 用户偏好设置 (预留)
|
||||
public static string UserSettingsConfig => Path.Combine(BaseDir, "user_settings.json");
|
||||
|
||||
// 布局缓存 (预留)
|
||||
public static string LayoutCache => Path.Combine(BaseDir, "layout_cache.json");
|
||||
|
||||
// 静态构造函数:确保目录存在
|
||||
static AppPaths()
|
||||
{
|
||||
if (!Directory.Exists(BaseDir))
|
||||
{
|
||||
Directory.CreateDirectory(BaseDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
SHH.CameraDashboard/Core/EnumHelper.cs
Normal file
21
SHH.CameraDashboard/Core/EnumHelper.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public static class EnumHelper
|
||||
{
|
||||
public static string GetDescription(Enum value)
|
||||
{
|
||||
FieldInfo fi = value.GetType().GetField(value.ToString());
|
||||
|
||||
DescriptionAttribute[] attributes =
|
||||
(DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
|
||||
|
||||
if (attributes != null && attributes.Length > 0)
|
||||
return attributes[0].Description;
|
||||
else
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
7
SHH.CameraDashboard/Core/IOverlayClosable.cs
Normal file
7
SHH.CameraDashboard/Core/IOverlayClosable.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
// 定义一个简单的接口,让子 ViewModel 具备关闭能力
|
||||
public interface IOverlayClosable
|
||||
{
|
||||
event Action? RequestClose;
|
||||
}
|
||||
109
SHH.CameraDashboard/Core/JsonHelper.cs
Normal file
109
SHH.CameraDashboard/Core/JsonHelper.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON 序列化与反序列化帮助类
|
||||
/// 职责:
|
||||
/// 1. 提供全局统一的 JSON 转换配置(如驼峰命名、日期格式)。
|
||||
/// 2. 封装常见的序列化和反序列化操作。
|
||||
/// 3. 增加对 null 输入和无效 JSON 的健壮性处理。
|
||||
/// </summary>
|
||||
public static class JsonHelper
|
||||
{
|
||||
#region --- 静态配置 ---
|
||||
|
||||
/// <summary>
|
||||
/// 全局共享的 JSON 序列化设置。
|
||||
/// 静态构造函数保证其只被初始化一次。
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerSettings _settings;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 静态构造函数 ---
|
||||
|
||||
/// <summary>
|
||||
/// 静态构造函数,用于初始化全局的 JSON 序列化设置。
|
||||
/// </summary>
|
||||
static JsonHelper()
|
||||
{
|
||||
_settings = new JsonSerializerSettings
|
||||
{
|
||||
// 1. 命名策略:将 C# 的 PascalCase 属性名序列化为 JSON 的 camelCase。
|
||||
// 这是与 JavaScript/TypeScript 前端交互的标准做法。
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
|
||||
// 2. 日期格式:统一使用 "yyyy-MM-dd HH:mm:ss" 格式,避免时区和格式差异导致的问题。
|
||||
DateFormatString = "yyyy-MM-dd HH:mm:ss",
|
||||
|
||||
// 3. Null 值处理:在序列化时忽略值为 null 的属性。
|
||||
// 这可以显著减小 JSON 字符串的大小,并使生成的 JSON 更干净。
|
||||
// 例如,`{ Name = "Alice", Age = null }` 会被序列化为 `{"name":"Alice"}`。
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
|
||||
// 4. 枚举转换:将枚举值序列化为其字符串表示,而不是数字。
|
||||
// 例如,`LogLevel.Info` 会被序列化为 `"info"`,而不是 `1`。
|
||||
_settings.Converters.Add(new StringEnumConverter());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 公共方法 ---
|
||||
|
||||
/// <summary>
|
||||
/// 将对象序列化为 JSON 字符串。
|
||||
/// </summary>
|
||||
/// <param name="obj">要序列化的对象。</param>
|
||||
/// <returns>序列化后的 JSON 字符串。如果输入为 null,则返回空字符串。</returns>
|
||||
public static string Serialize(object obj)
|
||||
{
|
||||
// [健壮性] 如果输入对象为 null,返回空字符串,而不是 "null"。
|
||||
// 这可以防止在创建 HTTP 请求内容时出现意外行为。
|
||||
if (obj == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(obj, _settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 JSON 字符串反序列化为指定类型的对象。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标对象的类型(必须是引用类型)。</typeparam>
|
||||
/// <param name="json">要反序列化的 JSON 字符串。</param>
|
||||
/// <returns>成功时返回反序列化后的对象;失败或输入无效时返回 null。</returns>
|
||||
public static T? Deserialize<T>(string json) where T : class
|
||||
{
|
||||
// [健壮性] 检查输入是否为 null、空字符串或仅包含空白字符。
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// [健壮性] 处理 JSON 字符串为 "null" 的特殊情况。
|
||||
if (json.Trim() == "null")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 尝试使用预配置的设置进行反序列化。
|
||||
return JsonConvert.DeserializeObject<T>(json, _settings);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// [健壮性] 如果 JSON 格式无效,捕获异常并返回 null。
|
||||
// 这可以防止程序因一个格式错误的 JSON 字符串而崩溃。
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
65
SHH.CameraDashboard/Core/RelayCommand.cs
Normal file
65
SHH.CameraDashboard/Core/RelayCommand.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
// ===========================================================================
|
||||
// 1. 新增:非泛型 RelayCommand (支持 new RelayCommand(Method, Check))
|
||||
// ===========================================================================
|
||||
public class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action<object> _execute;
|
||||
private readonly Predicate<object> _canExecute;
|
||||
|
||||
public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public bool CanExecute(object parameter)
|
||||
{
|
||||
return _canExecute == null || _canExecute(parameter);
|
||||
}
|
||||
|
||||
public void Execute(object parameter)
|
||||
{
|
||||
_execute(parameter);
|
||||
}
|
||||
|
||||
public event EventHandler CanExecuteChanged
|
||||
{
|
||||
add => CommandManager.RequerySuggested += value;
|
||||
remove => CommandManager.RequerySuggested -= value;
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 2. 保留:泛型 RelayCommand<T> (支持 new RelayCommand<string>(Method...))
|
||||
// ===========================================================================
|
||||
public class RelayCommand<T> : ICommand
|
||||
{
|
||||
private readonly Action<T> _execute;
|
||||
private readonly Predicate<T> _canExecute;
|
||||
|
||||
public RelayCommand(Action<T> execute, Predicate<T> canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public bool CanExecute(object parameter)
|
||||
{
|
||||
return _canExecute == null || _canExecute((T)parameter);
|
||||
}
|
||||
|
||||
public void Execute(object parameter)
|
||||
{
|
||||
_execute((T)parameter);
|
||||
}
|
||||
|
||||
public event EventHandler CanExecuteChanged
|
||||
{
|
||||
add => CommandManager.RequerySuggested += value;
|
||||
remove => CommandManager.RequerySuggested -= value;
|
||||
}
|
||||
}
|
||||
92
SHH.CameraDashboard/Core/ThemeManager.cs
Normal file
92
SHH.CameraDashboard/Core/ThemeManager.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public static class ThemeManager
|
||||
{
|
||||
// 使用字典:Key=主题名, Value=路径
|
||||
private static readonly Dictionary<string, string> _themeMap = new Dictionary<string, string>
|
||||
{
|
||||
{ "Dark", "/Style/Themes/Colors.Dark.xaml" },
|
||||
{ "Light", "/Style/Themes/Colors.Light.xaml" },
|
||||
// { "Blue", "/Style/Themes/Colors.Blue.xaml" }
|
||||
};
|
||||
|
||||
// 当前主题名称
|
||||
public static string CurrentThemeName { get; private set; } = "Dark";
|
||||
|
||||
/// <summary>
|
||||
/// 指定切换到某个主题
|
||||
/// </summary>
|
||||
/// <param name="themeName">主题名称 (Dark, Light...)</param>
|
||||
public static void SetTheme(string themeName)
|
||||
{
|
||||
if (!_themeMap.ContainsKey(themeName))
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"未找到主题: {themeName}");
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyTheme(_themeMap[themeName]);
|
||||
CurrentThemeName = themeName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 循环切换下一个主题 (保留旧功能)
|
||||
/// </summary>
|
||||
public static void SwitchToNextTheme()
|
||||
{
|
||||
// 获取所有 Key 的列表
|
||||
var keys = _themeMap.Keys.ToList();
|
||||
|
||||
// 找到当前 Key 的索引
|
||||
int index = keys.IndexOf(CurrentThemeName);
|
||||
|
||||
// 计算下一个索引
|
||||
index++;
|
||||
if (index >= keys.Count) index = 0;
|
||||
|
||||
// 切换
|
||||
SetTheme(keys[index]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 私有方法:执行具体的资源替换
|
||||
/// </summary>
|
||||
private static void ApplyTheme(string themePath)
|
||||
{
|
||||
var appResources = Application.Current.Resources;
|
||||
ResourceDictionary oldDict = null;
|
||||
|
||||
// 查找旧的皮肤字典
|
||||
foreach (var dict in appResources.MergedDictionaries)
|
||||
{
|
||||
if (dict.Source != null && dict.Source.OriginalString.Contains("/Themes/Colors."))
|
||||
{
|
||||
oldDict = dict;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除旧的
|
||||
if (oldDict != null)
|
||||
{
|
||||
appResources.MergedDictionaries.Remove(oldDict);
|
||||
}
|
||||
|
||||
// 加载新的
|
||||
try
|
||||
{
|
||||
var newDict = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri(themePath, UriKind.Relative)
|
||||
};
|
||||
appResources.MergedDictionaries.Add(newDict);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"加载主题文件失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
81
SHH.CameraDashboard/Core/TouchBehavior.cs
Normal file
81
SHH.CameraDashboard/Core/TouchBehavior.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
public static class TouchBehavior
|
||||
{
|
||||
//原有 Action 属性保持不变...
|
||||
public static readonly DependencyProperty ActionProperty =
|
||||
DependencyProperty.RegisterAttached("Action", typeof(PtzAction?), typeof(TouchBehavior), new PropertyMetadata(null, OnActionChanged));
|
||||
|
||||
public static PtzAction? GetAction(DependencyObject obj) => (PtzAction?)obj.GetValue(ActionProperty);
|
||||
public static void SetAction(DependencyObject obj, PtzAction? value) => obj.SetValue(ActionProperty, value);
|
||||
|
||||
// ★★★ [新增] 私有附加属性,用于记录“是否处于按下状态” ★★★
|
||||
private static readonly DependencyProperty IsActiveProperty =
|
||||
DependencyProperty.RegisterAttached("IsActive", typeof(bool), typeof(TouchBehavior), new PropertyMetadata(false));
|
||||
|
||||
private static bool GetIsActive(DependencyObject obj) => (bool)obj.GetValue(IsActiveProperty);
|
||||
private static void SetIsActive(DependencyObject obj, bool value) => obj.SetValue(IsActiveProperty, value);
|
||||
|
||||
|
||||
private static void OnActionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is Button btn)
|
||||
{
|
||||
btn.PreviewMouseDown -= Btn_PreviewMouseDown;
|
||||
btn.PreviewMouseUp -= Btn_PreviewMouseUp;
|
||||
btn.MouseLeave -= Btn_PreviewMouseUp;
|
||||
|
||||
if (e.NewValue != null)
|
||||
{
|
||||
btn.PreviewMouseDown += Btn_PreviewMouseDown;
|
||||
btn.PreviewMouseUp += Btn_PreviewMouseUp;
|
||||
btn.MouseLeave += Btn_PreviewMouseUp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Btn_PreviewMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is Button btn && btn.DataContext is CameraPtzViewModel vm)
|
||||
{
|
||||
var action = GetAction(btn);
|
||||
if (action.HasValue && vm.StartCommand.CanExecute(action.Value))
|
||||
{
|
||||
// 1. 标记为活跃状态
|
||||
SetIsActive(btn, true);
|
||||
|
||||
// 2. 执行开始命令
|
||||
vm.StartCommand.Execute(action.Value);
|
||||
|
||||
// (可选) 捕获鼠标,防止快速拖出窗口丢失 MouseUp
|
||||
btn.CaptureMouse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Btn_PreviewMouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (sender is Button btn && btn.DataContext is CameraPtzViewModel vm)
|
||||
{
|
||||
// ★★★ [核心修改] 只有之前标记为活跃,才执行停止 ★★★
|
||||
if (GetIsActive(btn))
|
||||
{
|
||||
var action = GetAction(btn);
|
||||
if (action.HasValue && vm.StopCommand.CanExecute(action.Value))
|
||||
{
|
||||
vm.StopCommand.Execute(action.Value);
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
SetIsActive(btn, false);
|
||||
|
||||
// 释放鼠标
|
||||
btn.ReleaseMouseCapture();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,15 @@
|
||||
x:Class="SHH.CameraDashboard.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ctrl="clr-namespace:SHH.CameraDashboard"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:SHH.CameraDashboard"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="SHH 视频中控"
|
||||
Width="1366"
|
||||
Height="768"
|
||||
Width="1900"
|
||||
Height="1030"
|
||||
Background="{DynamicResource Brush.Bg.Window}"
|
||||
FontFamily="{StaticResource Font.Normal}"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Topmost="{Binding IsTopMost}"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@@ -22,223 +22,341 @@
|
||||
ResizeBorderThickness="5" />
|
||||
</WindowChrome.WindowChrome>
|
||||
|
||||
<Grid x:Name="RootGrid">
|
||||
<Grid x:Name="AppLayout">
|
||||
<Window.DataContext>
|
||||
<local:MainWindowViewModel />
|
||||
</Window.DataContext>
|
||||
|
||||
<Border
|
||||
Background="{DynamicResource Brush.Bg.Window}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="1">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="45" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="5" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border
|
||||
x:Name="TitleBar"
|
||||
Grid.Row="0"
|
||||
Height="32"
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="0,0,0,1"
|
||||
MouseDown="OnTitleBarMouseDown">
|
||||
MouseLeftButtonDown="TitleBar_MouseDown">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="0"
|
||||
Margin="10,0"
|
||||
Margin="12,0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Width="40"
|
||||
Click="ToggleSidebar"
|
||||
Content="≡"
|
||||
FontSize="18"
|
||||
<TextBlock
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="📷" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
FontWeight="Bold"
|
||||
Style="{StaticResource Btn.Ghost}"
|
||||
ToolTip="展开/收起侧边栏" />
|
||||
Text="SHH 视频网关 - 中控台" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="{StaticResource Size.Font.Large}"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
IsHitTestVisible="False"
|
||||
Text="SHH 视频中控平台" />
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||
<Button
|
||||
Command="{Binding OpenWizardCommand}"
|
||||
Style="{StaticResource Btn.TitleBar}"
|
||||
ToolTip="向导">
|
||||
<Path
|
||||
Width="18"
|
||||
Height="18"
|
||||
Data="{StaticResource Icon.Wizard}"
|
||||
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
|
||||
Stretch="Uniform" />
|
||||
</Button>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
Margin="0,0,10,0"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Width="40"
|
||||
Margin="0,0,5,0"
|
||||
Click="OpenWizard"
|
||||
Content="🧙♂️"
|
||||
Style="{StaticResource Btn.Ghost}"
|
||||
ToolTip="打开配置向导" />
|
||||
Height="30"
|
||||
Click="BtnTheme_Click"
|
||||
Style="{StaticResource Btn.TitleBar}"
|
||||
ToolTip="切换皮肤">
|
||||
<StackPanel VerticalAlignment="Center" Orientation="Horizontal">
|
||||
<Path
|
||||
Width="14"
|
||||
Height="14"
|
||||
Data="{StaticResource Icon.Theme}"
|
||||
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
|
||||
Stretch="Uniform" />
|
||||
<Path
|
||||
Width="8"
|
||||
Height="8"
|
||||
Margin="4,2,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Data="M7,10L12,15L17,10H7Z"
|
||||
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
|
||||
Stretch="Uniform" />
|
||||
</StackPanel>
|
||||
<Button.ContextMenu>
|
||||
<ContextMenu Placement="Bottom">
|
||||
<MenuItem Command="{Binding SwitchThemeCommand}" CommandParameter="Dark">
|
||||
<MenuItem.Header>
|
||||
<TextBlock
|
||||
Margin="0,1,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="13"
|
||||
Text="🌚 深色模式 (Dark)" />
|
||||
</MenuItem.Header>
|
||||
</MenuItem>
|
||||
<MenuItem Command="{Binding SwitchThemeCommand}" CommandParameter="Light">
|
||||
<MenuItem.Header>
|
||||
<TextBlock
|
||||
Margin="0,1,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="13"
|
||||
Text="☀️ 浅色模式 (Light)" />
|
||||
</MenuItem.Header>
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Width="40"
|
||||
Margin="0,0,15,0"
|
||||
Click="ToggleTheme"
|
||||
Content="🎨"
|
||||
Style="{StaticResource Btn.Ghost}"
|
||||
ToolTip="切换皮肤 (Dark/Light)" />
|
||||
Command="{Binding ToggleTopMostCommand}"
|
||||
Style="{StaticResource Btn.TitleBar}"
|
||||
ToolTip="{Binding TopMostTooltip}">
|
||||
|
||||
<Path
|
||||
Width="16"
|
||||
Height="16"
|
||||
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
|
||||
Stretch="Uniform">
|
||||
<Path.Style>
|
||||
<Style TargetType="Path">
|
||||
<Setter Property="Data" Value="{StaticResource Icon.Pin.Outline}" />
|
||||
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsTopMost}" Value="True">
|
||||
<Setter Property="Data" Value="{StaticResource Icon.Pin.Fixed}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Path.Style>
|
||||
</Path>
|
||||
</Button>
|
||||
<Rectangle Width="10" Fill="Transparent" />
|
||||
|
||||
<Button
|
||||
Width="40"
|
||||
Click="OnMinimize"
|
||||
Command="{Binding MinimizeCommand}"
|
||||
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
Content="—"
|
||||
Style="{StaticResource Btn.Ghost}"
|
||||
ToolTip="最小化" />
|
||||
Style="{StaticResource Btn.TitleBar}" />
|
||||
<Button
|
||||
Width="40"
|
||||
Margin="5,0,0,0"
|
||||
Click="OnClose"
|
||||
Command="{Binding ToggleMaximizeCommand}"
|
||||
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
Content="{Binding MaxButtonContent}"
|
||||
Style="{StaticResource Btn.TitleBar}" />
|
||||
<Button
|
||||
Command="{Binding CloseCommand}"
|
||||
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
Content="✕"
|
||||
Style="{StaticResource Btn.Danger}"
|
||||
ToolTip="关闭" />
|
||||
Style="{StaticResource Btn.TitleBar.Close}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ctrl:CameraListControl
|
||||
x:Name="Sidebar"
|
||||
Grid.Column="0"
|
||||
Width="250" />
|
||||
|
||||
<DockPanel Grid.Row="1">
|
||||
<!-- 左侧摄像头列表 -->
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
Width="1"
|
||||
HorizontalAlignment="Right"
|
||||
Background="{DynamicResource Brush.Border}" />
|
||||
Width="320"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="0,0,1,0"
|
||||
DockPanel.Dock="Left">
|
||||
<local:CameraList x:Name="CameraList" />
|
||||
</Border>
|
||||
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
<!-- 右侧面板 -->
|
||||
<Border
|
||||
Width="500"
|
||||
Background="{DynamicResource Brush.Bg.Window}"
|
||||
ClipToBounds="True">
|
||||
<ctrl:DeviceHomeControl x:Name="DeviceHome" Margin="0" />
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="1,0,0,0"
|
||||
DockPanel.Dock="Right"
|
||||
Visibility="{Binding IsRightPanelVisible, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
|
||||
<Border
|
||||
x:Name="RightConfigPanel"
|
||||
Width="350"
|
||||
<Border.Resources>
|
||||
<DataTemplate DataType="{x:Type local:CameraEditViewModel}">
|
||||
<local:CameraEdit />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:CameraPtzViewModel}">
|
||||
<local:CameraPtz />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:CameraImgProcViewModel}">
|
||||
<local:CameraImgProc />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:CameraImageSubscriptionViewModels}">
|
||||
<local:CameraImageSubscription />
|
||||
</DataTemplate>
|
||||
</Border.Resources>
|
||||
<ContentControl Content="{Binding CurrentRightPanelViewModel}" />
|
||||
|
||||
</Border>
|
||||
|
||||
<DockPanel>
|
||||
<local:CameraItemTop DataSource="{Binding ElementName=CameraList, Path=DataContext.SelectedCamera}" DockPanel.Dock="Top" />
|
||||
|
||||
<Grid />
|
||||
</DockPanel>
|
||||
</DockPanel>
|
||||
|
||||
<GridSplitter
|
||||
Grid.Row="2"
|
||||
Height="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent"
|
||||
ResizeBehavior="PreviousAndNext"
|
||||
ResizeDirection="Rows"
|
||||
ShowsPreview="True" />
|
||||
|
||||
<Border
|
||||
x:Name="BottomLogPanel"
|
||||
Grid.Row="3"
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="0,1,0,0">
|
||||
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Height" Value="250" />
|
||||
<Setter Property="MinHeight" Value="100" />
|
||||
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsLogPanelExpanded}" Value="False">
|
||||
<Setter Property="Height" Value="35" />
|
||||
<Setter Property="MinHeight" Value="35" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
|
||||
<Grid>
|
||||
<TabControl
|
||||
Padding="0"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
ItemContainerStyle="{DynamicResource Style.TabItem.Modern}">
|
||||
|
||||
<TabItem Header="WebAPI 诊断">
|
||||
<local:ServiceNodesDiagnostic DataContext="{Binding DiagnosticVM}" />
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
<StackPanel
|
||||
Margin="0,2,5,0"
|
||||
HorizontalAlignment="Right"
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="1,0,0,0"
|
||||
Visibility="Collapsed">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect
|
||||
BlurRadius="20"
|
||||
Direction="180"
|
||||
Opacity="0.3"
|
||||
ShadowDepth="5"
|
||||
Color="Black" />
|
||||
</Border.Effect>
|
||||
VerticalAlignment="Top"
|
||||
Panel.ZIndex="1"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="50" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="60" />
|
||||
</Grid.RowDefinitions>
|
||||
<Button
|
||||
Width="30"
|
||||
Height="30"
|
||||
Command="{Binding DiagnosticVM.ClearCommand}"
|
||||
Style="{StaticResource Btn.TitleBar}"
|
||||
ToolTip="清空日志">
|
||||
<Path
|
||||
Width="14"
|
||||
Height="14"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Data="{StaticResource Icon.Trash}"
|
||||
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
|
||||
Stretch="Uniform" />
|
||||
</Button>
|
||||
|
||||
<Border BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
||||
<DockPanel Margin="15,0" LastChildFill="False">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="{StaticResource Size.Font.Large}"
|
||||
FontWeight="Bold"
|
||||
Text="⚙️ 设备参数配置" />
|
||||
<Button
|
||||
Width="30"
|
||||
Padding="0"
|
||||
Click="CloseRightPanel"
|
||||
Content="✕"
|
||||
DockPanel.Dock="Right"
|
||||
Style="{StaticResource Btn.Ghost}" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Row="1" Margin="20">
|
||||
<TextBlock
|
||||
Margin="0,10,0,5"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="设备名称" />
|
||||
<TextBox Padding="5" Text="Camera #01" />
|
||||
<TextBlock
|
||||
Margin="0,15,0,5"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="流媒体协议" />
|
||||
<ComboBox
|
||||
Height="32"
|
||||
Padding="5"
|
||||
SelectedIndex="0">
|
||||
<ComboBoxItem>RTSP (TCP)</ComboBoxItem>
|
||||
<ComboBoxItem>RTSP (UDP)</ComboBoxItem>
|
||||
<ComboBoxItem>WebRTC</ComboBoxItem>
|
||||
</ComboBox>
|
||||
<TextBlock
|
||||
Margin="0,15,0,5"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="主码流地址" />
|
||||
<TextBox
|
||||
Height="80"
|
||||
Padding="5"
|
||||
VerticalContentAlignment="Top"
|
||||
Text="rtsp://admin:123456@192.168.1.20:554/h264/ch1/main/av_stream"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
Background="{DynamicResource Brush.Bg.Window}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<StackPanel
|
||||
Margin="15,0"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Margin="0,0,10,0"
|
||||
Click="CloseRightPanel"
|
||||
Content="取消"
|
||||
Style="{StaticResource Btn.Ghost}" />
|
||||
<Button
|
||||
Width="100"
|
||||
Background="{DynamicResource Brush.Accent}"
|
||||
Click="CloseRightPanel"
|
||||
Content="保存配置" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Button
|
||||
Width="30"
|
||||
Height="30"
|
||||
Command="{Binding ToggleLogPanelCommand}"
|
||||
Style="{StaticResource Btn.TitleBar}"
|
||||
ToolTip="展开/收起面板">
|
||||
<Path
|
||||
Width="12"
|
||||
Height="12"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
|
||||
Stretch="Uniform">
|
||||
<Path.Style>
|
||||
<Style TargetType="Path">
|
||||
<Setter Property="Data" Value="{StaticResource Icon.ChevronDown}" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsLogPanelExpanded}" Value="False">
|
||||
<Setter Property="Data" Value="{StaticResource Icon.ChevronUp}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Path.Style>
|
||||
</Path>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid
|
||||
x:Name="GlobalMask"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="3"
|
||||
Background="#CC000000"
|
||||
Visibility="{Binding IsOverlayVisible, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<ContentControl
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Content="{Binding OverlayContent}">
|
||||
<ContentControl.Style>
|
||||
<Style TargetType="ContentControl">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsOverlayVisible}" Value="True">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From="0"
|
||||
To="1"
|
||||
Duration="0:0:0.2" />
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
|
||||
From="0.9"
|
||||
To="1"
|
||||
Duration="0:0:0.2" />
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"
|
||||
From="0.9"
|
||||
To="1"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
<Setter Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
<ScaleTransform CenterX="0.5" CenterY="0.5" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ContentControl.Style>
|
||||
</ContentControl>
|
||||
</Grid>
|
||||
|
||||
<ctrl:BottomDockControl x:Name="BottomDock" Grid.Row="2" />
|
||||
</Grid>
|
||||
|
||||
<Grid x:Name="ModalLayer" Visibility="Collapsed">
|
||||
<Border Background="#99000000" MouseDown="CloseModal_Click" />
|
||||
<ContentControl
|
||||
x:Name="ModalContainer"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<ContentControl.Effect>
|
||||
<DropShadowEffect
|
||||
BlurRadius="30"
|
||||
Opacity="0.5"
|
||||
ShadowDepth="10"
|
||||
Color="Black" />
|
||||
</ContentControl.Effect>
|
||||
</ContentControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -1,107 +1,37 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// MainWindow.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private bool _isDarkTheme = true;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// 1. 【日志联动】HttpService -> 底部报警栏 (精准匹配你提供的逻辑)
|
||||
// 确保你的 ApiLogEntry 字段已根据我之前的建议对齐
|
||||
HttpService.OnApiLog += (log) =>
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
BottomDock.PushLog(log);
|
||||
});
|
||||
};
|
||||
|
||||
// 2. 【设备联动】侧边栏选中 -> 设备首页详情
|
||||
Sidebar.OnDeviceSelected += (device) =>
|
||||
{
|
||||
// 更新右侧 主视图
|
||||
DeviceHome.UpdateDevice(device);
|
||||
|
||||
// 联动:向诊断日志推送一条“选中设备”的虚拟日志(可选)
|
||||
BottomDock.LatestLogText.Text = $"当前选中设备: {device.DisplayName} ({device.IpAddress})";
|
||||
|
||||
// 如果右侧配置面板开着,最好关掉它
|
||||
RightConfigPanel.Visibility = Visibility.Collapsed;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 1. 窗口基础操作 (拖拽/最小化/关闭)
|
||||
// ============================
|
||||
private void OnTitleBarMouseDown(object sender, MouseButtonEventArgs e)
|
||||
private void TitleBar_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ChangedButton == MouseButton.Left)
|
||||
this.DragMove();
|
||||
}
|
||||
|
||||
private void OnMinimize(object sender, RoutedEventArgs e) => this.WindowState = WindowState.Minimized;
|
||||
private void OnClose(object sender, RoutedEventArgs e) => this.Close();
|
||||
|
||||
// ============================
|
||||
// 2. 界面交互逻辑
|
||||
// ============================
|
||||
|
||||
// 切换侧边栏展开/收起
|
||||
private void ToggleSidebar(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Sidebar.Width > 0)
|
||||
Sidebar.Width = 0;
|
||||
else
|
||||
Sidebar.Width = 250;
|
||||
}
|
||||
|
||||
// 切换主题 (使用你定义的 ThemeManager)
|
||||
private void ToggleTheme(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_isDarkTheme = !_isDarkTheme;
|
||||
// 确保 ThemeManager.ChangeTheme 逻辑未变动
|
||||
ThemeManager.ChangeTheme(_isDarkTheme ? ThemeType.Dark : ThemeType.Light);
|
||||
}
|
||||
|
||||
// 打开/关闭右侧配置面板
|
||||
private void OpenRightPanel(object sender, RoutedEventArgs e) => RightConfigPanel.Visibility = Visibility.Visible;
|
||||
private void CloseRightPanel(object sender, RoutedEventArgs e) => RightConfigPanel.Visibility = Visibility.Collapsed;
|
||||
|
||||
// ============================
|
||||
// 3. 向导模态框逻辑 (完整保留)
|
||||
// ============================
|
||||
private void OpenWizard(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var wizard = new WizardControl();
|
||||
|
||||
// 当向导请求关闭时
|
||||
wizard.RequestClose += (s, args) =>
|
||||
{
|
||||
CloseModal();
|
||||
// 关键:向导可能修改了全局服务器列表,通知侧边栏刷新
|
||||
Sidebar.ReloadServers();
|
||||
};
|
||||
|
||||
ModalContainer.Content = wizard;
|
||||
ModalLayer.Visibility = Visibility.Visible;
|
||||
DragMove();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseModal_Click(object sender, MouseButtonEventArgs e)
|
||||
private void BtnTheme_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 点击遮罩层背景关闭
|
||||
CloseModal();
|
||||
}
|
||||
|
||||
private void CloseModal()
|
||||
{
|
||||
ModalLayer.Visibility = Visibility.Collapsed;
|
||||
ModalContainer.Content = null;
|
||||
var btn = sender as Button;
|
||||
if (btn != null && btn.ContextMenu != null)
|
||||
{
|
||||
// 设置菜单的目标为当前按钮(防止定位跑偏)
|
||||
btn.ContextMenu.PlacementTarget = btn;
|
||||
// 打开菜单
|
||||
btn.ContextMenu.IsOpen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
515
SHH.CameraDashboard/MainWindowViewModel.cs
Normal file
515
SHH.CameraDashboard/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,515 @@
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// 主窗口的视图模型 (ViewModel)。
|
||||
/// 它封装了主窗口的所有业务逻辑、状态和用户交互命令。
|
||||
/// 它还作为一个中心协调者,响应全局事件并管理子视图(如右侧编辑面板和蒙板)。
|
||||
/// </summary>
|
||||
public class MainWindowViewModel : INotifyPropertyChanged
|
||||
{
|
||||
#region --- 构造函数 ---
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 MainWindowViewModel 的新实例。
|
||||
/// </summary>
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
#region 窗体控制命令
|
||||
|
||||
// 最小化窗口控制命令
|
||||
MinimizeCommand = new RelayCommand<Window>(win =>
|
||||
{
|
||||
if (win != null)
|
||||
{
|
||||
win.WindowState = WindowState.Minimized;
|
||||
}
|
||||
});
|
||||
|
||||
// 最大化、还原命令
|
||||
ToggleMaximizeCommand = new RelayCommand<Window>(win =>
|
||||
{
|
||||
if (win == null) return;
|
||||
|
||||
if (win.WindowState == WindowState.Maximized)
|
||||
{
|
||||
win.WindowState = WindowState.Normal;
|
||||
MaxButtonContent = "⬜";
|
||||
}
|
||||
else
|
||||
{
|
||||
win.WindowState = WindowState.Maximized;
|
||||
MaxButtonContent = "❐";
|
||||
}
|
||||
});
|
||||
|
||||
// 关闭应用程序命令
|
||||
CloseCommand = new RelayCommand<Window>(win =>
|
||||
{
|
||||
App.ShutdownApp();
|
||||
});
|
||||
|
||||
// 窗体置顶
|
||||
ToggleTopMostCommand = new RelayCommand<object>(_ => IsTopMost = !IsTopMost);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 其它控制命令
|
||||
|
||||
// 向导功能命令
|
||||
OpenWizardCommand = new RelayCommand<object>(_ =>
|
||||
{
|
||||
var wizardVM = new WizardClientsViewModel();
|
||||
ShowOverlay(wizardVM);
|
||||
});
|
||||
|
||||
// 更换主题命令
|
||||
SwitchThemeCommand = new RelayCommand<string>(themeName =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(themeName))
|
||||
ThemeManager.SetTheme(themeName);
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
#region 订阅事件
|
||||
|
||||
// 订阅事件
|
||||
AppGlobal.OnRequestAddCamera += HandleAddRequest; // 新增
|
||||
AppGlobal.OnRequestEditCamera += HandleEditRequest; // 编辑
|
||||
AppGlobal.OnRequestDeleteCamera += HandleDeleteRequest; // 删除
|
||||
AppGlobal.OnRequestPtzCamera += HandlePtzRequest; // 云台
|
||||
AppGlobal.OnRequestImgProc += HandleImgProcRequest; //订阅图像处理请求
|
||||
AppGlobal.OnRequestSubscription += ShowSubscriptionPanel; // 订阅图像处理请求
|
||||
|
||||
#endregion
|
||||
|
||||
// 打开或折叠日志面板
|
||||
ToggleLogPanelCommand = new RelayCommand<object>(_ => IsLogPanelExpanded = !IsLogPanelExpanded);
|
||||
|
||||
// 初始化子 ViewModel
|
||||
DiagnosticVM = new ServiceNodesViewModel();
|
||||
|
||||
// 启动异步初始化流程
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 事件处理 ---
|
||||
|
||||
#region HandleAddRequest
|
||||
|
||||
/// <summary>
|
||||
/// 处理添加请求
|
||||
/// </summary>
|
||||
public void HandleAddRequest()
|
||||
{
|
||||
// 1. 创建一个新的、空白的 DTO,并提供一些合理的默认值以提升用户体验
|
||||
var newDto = new CameraEditInfo
|
||||
{
|
||||
Id = 0, // 0 表示新增
|
||||
Name = "新摄像头",
|
||||
IpAddress = "192.168.1.64",
|
||||
Brand = 1, // 默认为海康(1) 或你想要的默认值
|
||||
Port = 8000,
|
||||
Username = "admin",
|
||||
Password = "abcd1234",
|
||||
ChannelIndex = 1,
|
||||
StreamType = 0, // 主码流
|
||||
EnhanceImage = false,
|
||||
AllowCompress = true,
|
||||
};
|
||||
|
||||
// 2. 创建用于编辑的 ViewModel
|
||||
var editVm = new CameraEditViewModel(newDto);
|
||||
|
||||
// 3. 监听编辑窗口的关闭请求,以便在操作完成后进行清理和刷新
|
||||
editVm.RequestClose += (isSaved, data) =>
|
||||
{
|
||||
CurrentRightPanelViewModel = null;
|
||||
if (isSaved)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("新增成功,刷新列表...");
|
||||
AppGlobal.RequestRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 在右侧面板中显示编辑界面
|
||||
CurrentRightPanelViewModel = editVm;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region HandleEditRequest
|
||||
|
||||
/// <summary>
|
||||
/// 处理编辑请求:调用 Repository 获取数据
|
||||
/// </summary>
|
||||
private async void HandleEditRequest(WebApiCameraModel listModel)
|
||||
{
|
||||
if (listModel == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 调用 API 客户端获取该摄像头的完整详细信息
|
||||
var editModel = await ApiClient.Instance.Cameras.GetCameraDetailsAsync(listModel.Id);
|
||||
|
||||
if (editModel == null)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("获取设备详情失败 (Repository返回null)");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 创建用于编辑的 ViewModel
|
||||
var editVm = new CameraEditViewModel(editModel);
|
||||
|
||||
// 3. 监听编辑窗口的关闭请求
|
||||
editVm.RequestClose += (isSaved, newConfig) =>
|
||||
{
|
||||
CurrentRightPanelViewModel = null;
|
||||
|
||||
if (isSaved && newConfig != null)
|
||||
{
|
||||
// 这里以后可以调用 Update 方法
|
||||
System.Diagnostics.Debug.WriteLine($"保存成功: {newConfig.Name}");
|
||||
AppGlobal.RequestRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 在右侧面板中显示编辑界面
|
||||
CurrentRightPanelViewModel = editVm;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"HandleEditRequest 异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region HandleDeleteRequest
|
||||
|
||||
/// <summary>
|
||||
/// 处理删除请求
|
||||
/// </summary>
|
||||
private async void HandleDeleteRequest(WebApiCameraModel camera)
|
||||
{
|
||||
if (camera == null) return;
|
||||
|
||||
// 1. 弹窗确认 (防止误删)
|
||||
var result = MessageBox.Show(
|
||||
$"确定要删除摄像头 \"{camera.Name}\" 吗?\n此操作不可恢复。",
|
||||
"确认删除",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Warning);
|
||||
|
||||
if (result != MessageBoxResult.Yes) return;
|
||||
|
||||
bool success = await ApiClient.Instance.Cameras.DeleteCameraAsync(camera.Id);
|
||||
|
||||
if (success)
|
||||
{
|
||||
// 3. 成功后刷新列表
|
||||
System.Diagnostics.Debug.WriteLine($"删除 {camera.Name} 成功,刷新列表...");
|
||||
AppGlobal.RequestRefresh();
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show("删除失败,请检查网络或日志。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region HandlePtzRequest (新增的处理逻辑)
|
||||
|
||||
/// <summary>
|
||||
/// [新增] 处理云台控制请求
|
||||
/// </summary>
|
||||
private void HandlePtzRequest(WebApiCameraModel camera)
|
||||
{
|
||||
if (camera == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 创建云台控制的 ViewModel
|
||||
var ptzVm = new CameraPtzViewModel(camera);
|
||||
|
||||
// 2. 监听关闭事件 (以便点击面板上的关闭按钮时,能收起右侧面板)
|
||||
// 注意:我们需要在 CameraPtzViewModel 中定义这个 RequestClose 事件
|
||||
ptzVm.RequestClose += () =>
|
||||
{
|
||||
CurrentRightPanelViewModel = null;
|
||||
};
|
||||
|
||||
// 3. 将右侧面板的内容切换为 云台控制
|
||||
CurrentRightPanelViewModel = ptzVm;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"打开云台失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async void HandleImgProcRequest(WebApiCameraModel camera)
|
||||
{
|
||||
if (camera == null) return;
|
||||
try
|
||||
{
|
||||
// 获取详情 (因为列表模型可能不包含 TargetResolution 等深层字段)
|
||||
var detailDto = await ApiClient.Instance.Cameras.GetCameraDetailsAsync(camera.Id);
|
||||
if (detailDto == null) return;
|
||||
|
||||
// 创建 VM
|
||||
var vm = new CameraImgProcViewModel(camera, detailDto);
|
||||
|
||||
// 处理关闭回调
|
||||
vm.RequestClose += () => CurrentRightPanelViewModel = null;
|
||||
|
||||
CurrentRightPanelViewModel = vm;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"打开图像处理失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 添加处理方法 (仿照 HandlePtzRequest 写法)
|
||||
private void ShowSubscriptionPanel(WebApiCameraModel camera)
|
||||
{
|
||||
if (camera == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
// A. 复用模式:只创建 ViewModel
|
||||
var vm = new CameraImageSubscriptionViewModels(camera);
|
||||
|
||||
// B. 复用关闭逻辑:当子页面请求关闭时,清空当前面板对象
|
||||
vm.RequestClose += () => CurrentRightPanelViewModel = null;
|
||||
|
||||
// C. 复用显示通道:直接赋值,UI 会自动更新
|
||||
CurrentRightPanelViewModel = vm;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"打开订阅面板失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 通用右侧面板控制 ---
|
||||
|
||||
// 这里的类型是 object (或 INotifyPropertyChanged)
|
||||
// 它可以存放 CameraEditViewModel,也可以存放 LogViewModel, SettingsViewModel 等
|
||||
private object? _currentRightPanelViewModel;
|
||||
public object? CurrentRightPanelViewModel
|
||||
{
|
||||
get => _currentRightPanelViewModel;
|
||||
set
|
||||
{
|
||||
_currentRightPanelViewModel = value;
|
||||
OnPropertyChanged();
|
||||
// 每次内容变了,通知 Visibility 属性也更新一下
|
||||
OnPropertyChanged(nameof(IsRightPanelVisible));
|
||||
}
|
||||
}
|
||||
|
||||
// 界面绑定的可见性:只要 Content 不为空,就显示面板
|
||||
public bool IsRightPanelVisible => _currentRightPanelViewModel != null;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 命令 (Commands) ---
|
||||
|
||||
/// <summary>
|
||||
/// 获取最小化窗口的命令。
|
||||
/// </summary>
|
||||
public ICommand MinimizeCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取切换窗口最大化/还原状态的命令。
|
||||
/// </summary>
|
||||
public ICommand ToggleMaximizeCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取关闭应用程序的命令。
|
||||
/// </summary>
|
||||
public ICommand CloseCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取打开客户端配置向导的命令。
|
||||
/// </summary>
|
||||
public ICommand OpenWizardCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取根据名称切换主题的命令。
|
||||
/// </summary>
|
||||
public ICommand SwitchThemeCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取切换窗口置顶状态的命令。
|
||||
/// </summary>
|
||||
public ICommand ToggleTopMostCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取展开或折叠日志面板的命令。
|
||||
/// </summary>
|
||||
public ICommand ToggleLogPanelCommand { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 属性 (Properties) ---
|
||||
|
||||
private string _maxButtonContent = "⬜";
|
||||
/// <summary>
|
||||
/// 获取或设置最大化/还原按钮的显示内容。
|
||||
/// </summary>
|
||||
public string MaxButtonContent
|
||||
{
|
||||
get => _maxButtonContent;
|
||||
set { _maxButtonContent = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private bool _isTopMost;
|
||||
/// <summary>
|
||||
/// 获取或设置一个值,该值指示窗口是否应始终显示在其他窗口之上。
|
||||
/// </summary>
|
||||
public bool IsTopMost
|
||||
{
|
||||
get => _isTopMost;
|
||||
set
|
||||
{
|
||||
if (_isTopMost != value)
|
||||
{
|
||||
_isTopMost = value;
|
||||
OnPropertyChanged(nameof(IsTopMost));
|
||||
OnPropertyChanged(nameof(TopMostTooltip));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于窗口置顶按钮的动态工具提示文本。
|
||||
/// </summary>
|
||||
public string TopMostTooltip => IsTopMost ? "取消置顶" : "窗口置顶";
|
||||
|
||||
private bool _isLogPanelExpanded = true;
|
||||
/// <summary>
|
||||
/// 获取或设置一个值,该值指示日志面板是否展开。
|
||||
/// </summary>
|
||||
public bool IsLogPanelExpanded
|
||||
{
|
||||
get => _isLogPanelExpanded;
|
||||
set
|
||||
{
|
||||
if (_isLogPanelExpanded != value)
|
||||
{
|
||||
_isLogPanelExpanded = value;
|
||||
OnPropertyChanged(nameof(IsLogPanelExpanded));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于服务节点诊断面板的 ViewModel。
|
||||
/// </summary>
|
||||
public ServiceNodesViewModel DiagnosticVM { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 蒙板/覆盖层管理 ---
|
||||
|
||||
private bool _isOverlayVisible;
|
||||
/// <summary>
|
||||
/// 获取或设置全局蒙板的可见性。
|
||||
/// </summary>
|
||||
public bool IsOverlayVisible
|
||||
{
|
||||
get => _isOverlayVisible;
|
||||
set { _isOverlayVisible = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private object? _overlayContent;
|
||||
/// <summary>
|
||||
/// 获取或设置蒙板内显示的内容(一个子 ViewModel)。
|
||||
/// </summary>
|
||||
public object? OverlayContent
|
||||
{
|
||||
get => _overlayContent;
|
||||
set { _overlayContent = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示一个带有指定内容的全局蒙板。
|
||||
/// </summary>
|
||||
/// <param name="contentViewModel">要在蒙板中显示的子 ViewModel。</param>
|
||||
public void ShowOverlay(object contentViewModel)
|
||||
{
|
||||
if (contentViewModel == null) return;
|
||||
|
||||
if (contentViewModel is IOverlayClosable closable)
|
||||
{
|
||||
closable.RequestClose += () =>
|
||||
{
|
||||
IsOverlayVisible = false;
|
||||
OverlayContent = null;
|
||||
};
|
||||
}
|
||||
|
||||
OverlayContent = contentViewModel;
|
||||
IsOverlayVisible = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 辅助方法 ---
|
||||
|
||||
/// <summary>
|
||||
/// 执行应用程序的异步初始化逻辑。
|
||||
/// </summary>
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
await Task.Delay(500);
|
||||
|
||||
if (CheckIfNeedWizard())
|
||||
{
|
||||
var wizardVM = new WizardClientsViewModel();
|
||||
ShowOverlay(wizardVM);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否需要在启动时显示设置向导。
|
||||
/// </summary>
|
||||
private bool CheckIfNeedWizard()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- INotifyPropertyChanged 实现 ---
|
||||
|
||||
/// <summary>
|
||||
/// 当属性值更改时发生。
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 引发 PropertyChanged 事件,通知UI更新。
|
||||
/// </summary>
|
||||
protected void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string name = "")
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
// 日志实体,用于在事件中传递详细信息
|
||||
public class ApiLogEntry
|
||||
{
|
||||
public DateTime Time { get; set; } = DateTime.Now;
|
||||
public string Method { get; set; } // GET, POST
|
||||
public string Url { get; set; }
|
||||
public int StatusCode { get; set; }
|
||||
public long DurationMs { get; set; } // 耗时(毫秒)
|
||||
public string RequestBody { get; set; } // 发送的内容
|
||||
public string ResponseBody { get; set; } // 接收的内容
|
||||
public string ErrorMessage { get; set; } // 异常信息
|
||||
|
||||
// 辅助属性:是否成功
|
||||
public bool IsSuccess => StatusCode >= 200 && StatusCode < 300;
|
||||
|
||||
// 辅助属性:显示颜色
|
||||
public Brush StatusColor => IsSuccess
|
||||
? new SolidColorBrush(Color.FromRgb(78, 201, 176)) // 绿色
|
||||
: new SolidColorBrush(Color.FromRgb(244, 71, 71)); // 红色
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
public class CameraInfo
|
||||
{
|
||||
// --- 原始 JSON 属性 ---
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string IpAddress { get; set; }
|
||||
public DeviceBrand Brand { get; set; }
|
||||
public string Status { get; set; } // "Playing", "Disconnected" 等
|
||||
public bool IsPhysicalOnline { get; set; } // 物理在线 (网络)
|
||||
public bool IsOnline { get; set; } // 业务在线 (登录)
|
||||
public bool IsRunning { get; set; } // 正在运行 (拉流)
|
||||
public int RealFps { get; set; }
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
|
||||
// --- UI 分离状态逻辑 ---
|
||||
|
||||
// 状态 1: 登录状态 (在线/离线)
|
||||
public string OnlineStatusText => (IsPhysicalOnline && IsOnline) ? "在线" : "离线";
|
||||
|
||||
// 状态 2: 运行状态 (运行/停止)
|
||||
public string RunningStatusText => IsRunning ? "运行中" : "已停止";
|
||||
|
||||
// 品牌信息
|
||||
public string BrandName => Brand.ToString();
|
||||
|
||||
public string DisplayName => string.IsNullOrEmpty(Name) ? IpAddress : Name;
|
||||
public string MediaDetail => IsRunning && Width > 0 ? $"{Width}x{Height} | {RealFps}fps" : "无信号";
|
||||
}
|
||||
58
SHH.CameraDashboard/Models/LogWebApiModel.cs
Normal file
58
SHH.CameraDashboard/Models/LogWebApiModel.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// WebAPI 调用日志模型
|
||||
/// 用于记录 API 请求的详细信息,便于调试和性能分析
|
||||
/// </summary>
|
||||
public class LogWebApiModel
|
||||
{
|
||||
#region --- 核心属性 ---
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置请求发生的时间
|
||||
/// </summary>
|
||||
public DateTime Time { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 HTTP 请求方法 (GET, POST, PUT, DELETE 等)
|
||||
/// </summary>
|
||||
public string Method { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置请求的 URL 路径
|
||||
/// </summary>
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置响应的状态码 (200, 404, 500 等)
|
||||
/// </summary>
|
||||
public string StatusCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置请求的原始数据 (JSON/XML 等)
|
||||
/// </summary>
|
||||
public string RequestData { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置响应的原始数据 (JSON/XML 等)
|
||||
/// </summary>
|
||||
public string ResponseData { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置请求处理耗时(毫秒)
|
||||
/// </summary>
|
||||
public long ElapsedMilliseconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 调用 WebAPI 的应用模块
|
||||
/// </summary>
|
||||
public string AppModule { get; set; } = "System";
|
||||
|
||||
/// <summary>
|
||||
/// 是否自动发送
|
||||
/// </summary>
|
||||
public bool IsAutoPost { get; set; } = false;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
|
||||
// 1. 单个节点的数据模型
|
||||
public class ServerNode : INotifyPropertyChanged
|
||||
{
|
||||
private string _name = "新节点"; // 默认名称
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set { _name = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private string _ip = "127.0.0.1";
|
||||
public string Ip
|
||||
{
|
||||
get => _ip;
|
||||
set
|
||||
{
|
||||
if (_ip != value)
|
||||
{
|
||||
_ip = value;
|
||||
OnPropertyChanged(); // 通知界面更新
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int _port = 5000;
|
||||
public int Port
|
||||
{
|
||||
get => _port;
|
||||
set
|
||||
{
|
||||
if (_port != value)
|
||||
{
|
||||
_port = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _status = "未检测";
|
||||
public string Status
|
||||
{
|
||||
get => _status;
|
||||
set
|
||||
{
|
||||
_status = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(StatusColor)); // 状态变了,颜色也要跟着变
|
||||
}
|
||||
}
|
||||
public Brush StatusColor
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (Status)
|
||||
{
|
||||
case "✅ 连接成功":
|
||||
return new SolidColorBrush(Color.FromRgb(78, 201, 176)); // 绿色
|
||||
case "❌ 状态码异常":
|
||||
return new SolidColorBrush(Color.FromRgb(255, 140, 0)); // 橙色 (警告)
|
||||
case "❌ 无法连接":
|
||||
return new SolidColorBrush(Color.FromRgb(244, 71, 71)); // 红色 (故障)
|
||||
case "⏳ 检测中...":
|
||||
return new SolidColorBrush(Color.FromRgb(86, 156, 214)); // 蓝色
|
||||
default:
|
||||
return new SolidColorBrush(Color.FromRgb(153, 153, 153)); // 灰色
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetResult(bool success, string msg)
|
||||
{
|
||||
Status = msg;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
protected void OnPropertyChanged([CallerMemberName] string name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
}
|
||||
65
SHH.CameraDashboard/Models/ServiceNodeModel.cs
Normal file
65
SHH.CameraDashboard/Models/ServiceNodeModel.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// 服务节点模型,用于表示一个可连接的服务端点
|
||||
/// </summary>
|
||||
public class ServiceNodeModel : INotifyPropertyChanged
|
||||
{
|
||||
#region --- 公共属性 ---
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置服务节点的名称
|
||||
/// </summary>
|
||||
public string ServiceNodeName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置服务节点的IP地址
|
||||
/// </summary>
|
||||
public string ServiceNodeIp { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置服务节点的端口号
|
||||
/// </summary>
|
||||
public string ServiceNodePort { get; set; } = string.Empty;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 状态属性 ---
|
||||
|
||||
private string _status = "未检测";
|
||||
/// <summary>
|
||||
/// 获取或设置服务节点的连接状态(如:未检测、在线、离线)
|
||||
/// </summary>
|
||||
public string Status
|
||||
{
|
||||
get => _status;
|
||||
set
|
||||
{
|
||||
_status = value;
|
||||
OnPropertyChanged(nameof(Status));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- INotifyPropertyChanged 实现 ---
|
||||
|
||||
/// <summary>
|
||||
/// 当属性值更改时发生
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 引发 <see cref="PropertyChanged"/> 事件
|
||||
/// </summary>
|
||||
/// <param name="propertyName">已更改的属性名称</param>
|
||||
protected void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 视频源逻辑状态枚举
|
||||
/// 描述了从配置加载到视频流稳定输出的完整生命周期
|
||||
/// </summary>
|
||||
public enum VideoSourceStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 已断开/初始状态。
|
||||
/// 此时资源已释放,尚未执行 Login 或 Start 操作。
|
||||
/// </summary>
|
||||
Disconnected,
|
||||
|
||||
/// <summary>
|
||||
/// 正在尝试建立网络连接。
|
||||
/// 此时正在进行 Socket 握手或探测设备 IP 是否可达。
|
||||
/// </summary>
|
||||
Connecting,
|
||||
|
||||
/// <summary>
|
||||
/// 正在进行身份验证。
|
||||
/// 连接已建立,正在提交 UserName/Password 调用 SDK 的 Login 接口。
|
||||
/// </summary>
|
||||
Authorizing,
|
||||
|
||||
/// <summary>
|
||||
/// 已登录/待机。
|
||||
/// 登录成功并获取到了设备元数据(Metadata),但尚未启动预览(RealPlay)。
|
||||
/// 适用于“仅管理,不看画面”的场景。
|
||||
/// </summary>
|
||||
Connected,
|
||||
|
||||
/// <summary>
|
||||
/// 正常取流播放中
|
||||
/// </summary>
|
||||
Playing,
|
||||
|
||||
/// <summary>
|
||||
/// 正在取流/正常运行中。
|
||||
/// 预览句柄已开启,取流回调函数正在持续接收数据帧并进行解码。
|
||||
/// </summary>
|
||||
Streaming,
|
||||
|
||||
/// <summary>
|
||||
/// 自动重连中。
|
||||
/// 检测到网络抖动或心跳丢失,SDK 正在尝试内部恢复,此时视频流可能处于停滞状态。
|
||||
/// </summary>
|
||||
Reconnecting,
|
||||
|
||||
/// <summary>
|
||||
/// 故障/异常状态。
|
||||
/// 发生了不可恢复的错误(如密码错误、最大连接数限制、设备强制离线)。
|
||||
/// 进入此状态通常需要人工干预或调用 Stop 后重新 Start。
|
||||
/// </summary>
|
||||
Faulted
|
||||
}
|
||||
}
|
||||
321
SHH.CameraDashboard/Pages/CameraEdit.xaml
Normal file
321
SHH.CameraDashboard/Pages/CameraEdit.xaml
Normal file
@@ -0,0 +1,321 @@
|
||||
<UserControl
|
||||
x:Class="SHH.CameraDashboard.CameraEdit"
|
||||
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:local="clr-namespace:SHH.CameraDashboard"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DataContext="{d:DesignInstance Type=local:CameraEditViewModel}"
|
||||
d:DesignHeight="700"
|
||||
d:DesignWidth="500"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Margin="15" Background="{DynamicResource Brush.Bg.Window}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Margin="0,0,0,15"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="设备参数配置" />
|
||||
|
||||
<ScrollViewer
|
||||
Grid.Row="1"
|
||||
Padding="0,0,5,0"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="Height" Value="26" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Bg.Input}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border}" />
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
|
||||
<GroupBox
|
||||
Margin="0,0,0,15"
|
||||
Padding="10"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Header="基础身份 (Identity)">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="80" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<WrapPanel Grid.ColumnSpan="2">
|
||||
<TextBlock Margin="0,5" Text="设备 ID *" />
|
||||
<TextBox
|
||||
Width="80"
|
||||
Margin="30,5"
|
||||
Text="{Binding EditingDto.Id}" />
|
||||
|
||||
<TextBlock Margin="0,5" Text="品牌类型" />
|
||||
<ComboBox
|
||||
Width="180"
|
||||
Margin="5,5,0,5"
|
||||
DisplayMemberPath="Label"
|
||||
ItemsSource="{Binding BrandOptions}"
|
||||
SelectedValue="{Binding EditingDto.Brand}"
|
||||
SelectedValuePath="Value" />
|
||||
</WrapPanel>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="0,5"
|
||||
Text="设备名称" />
|
||||
<TextBox
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="0,5"
|
||||
Text="{Binding EditingDto.Name}" />
|
||||
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="3"
|
||||
Margin="0,5"
|
||||
Text="安装位置" />
|
||||
<TextBox
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Margin="0,5"
|
||||
Text="{Binding EditingDto.Location}" />
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox
|
||||
Margin="0,0,0,15"
|
||||
Padding="10"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Header="核心连接 (Connectivity)">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="80" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="60" />
|
||||
<ColumnDefinition Width="60" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Margin="0,5" Text="IP 地址 *" />
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Margin="0,5,10,5"
|
||||
Text="{Binding EditingDto.IpAddress}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Margin="0,5"
|
||||
HorizontalAlignment="Right"
|
||||
Text="端口 " />
|
||||
<TextBox
|
||||
Grid.Column="3"
|
||||
Margin="5,5,0,5"
|
||||
Text="{Binding EditingDto.Port}" />
|
||||
|
||||
<WrapPanel Grid.Row="1" Grid.ColumnSpan="4">
|
||||
<TextBlock
|
||||
Width="80"
|
||||
Margin="0,5,0,5"
|
||||
Text="用户名" />
|
||||
<TextBox
|
||||
Width="120"
|
||||
Margin="0,5,0,5"
|
||||
Text="{Binding EditingDto.Username}" />
|
||||
|
||||
<TextBlock
|
||||
Width="40"
|
||||
Margin="33,5,10,5"
|
||||
Text="密码" />
|
||||
<TextBox
|
||||
Width="140"
|
||||
Margin="0,5,0,5"
|
||||
Text="{Binding EditingDto.Password}" />
|
||||
</WrapPanel>
|
||||
|
||||
<WrapPanel Grid.Row="2" Grid.ColumnSpan="4">
|
||||
<TextBlock
|
||||
Width="80"
|
||||
Margin="0,5,0,5"
|
||||
Text="句柄" />
|
||||
<TextBox
|
||||
Width="70"
|
||||
Margin="0,5,0,5"
|
||||
Text="{Binding EditingDto.RenderHandle}" />
|
||||
|
||||
<TextBlock
|
||||
Width="40"
|
||||
Margin="23,5,0,5"
|
||||
Text="通道号" />
|
||||
<TextBox
|
||||
Width="40"
|
||||
Margin="10,5,0,5"
|
||||
Text="{Binding EditingDto.ChannelIndex}" />
|
||||
|
||||
<TextBlock
|
||||
Width="50"
|
||||
Margin="20,0,10,0"
|
||||
Text="码流类型" />
|
||||
<ComboBox
|
||||
Width="80"
|
||||
Margin="0,5,0,5"
|
||||
SelectedIndex="{Binding EditingDto.StreamType}">
|
||||
<ComboBoxItem Content="主码流" />
|
||||
<ComboBoxItem Content="子码流" />
|
||||
</ComboBox>
|
||||
</WrapPanel>
|
||||
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox
|
||||
Margin="0,0,0,15"
|
||||
Padding="10"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Header="关联主板 (Metadata)">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="80" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="60" />
|
||||
<ColumnDefinition Width="60" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Margin="0,5" Text="主板 IP" />
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Margin="0,5,10,5"
|
||||
Text="{Binding EditingDto.MainboardIp}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Margin="0,5"
|
||||
HorizontalAlignment="Right"
|
||||
Text="端口 " />
|
||||
<TextBox
|
||||
Grid.Column="3"
|
||||
Margin="5,5,0,5"
|
||||
Text="{Binding EditingDto.MainboardPort}" />
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox
|
||||
Margin="0,0,0,15"
|
||||
Padding="10"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Header="运行参数 (Runtime)">
|
||||
<WrapPanel>
|
||||
<TextBlock
|
||||
Width="30"
|
||||
Margin="0,5,0,5"
|
||||
Text="RTSP" />
|
||||
<TextBox
|
||||
Width="360"
|
||||
Height="60"
|
||||
Margin="20,5,0,5"
|
||||
VerticalContentAlignment="Top"
|
||||
Text="{Binding EditingDto.RtspPath}" />
|
||||
</WrapPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Margin="0,10,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Width="100"
|
||||
Height="32"
|
||||
Margin="0,0,10,0"
|
||||
Command="{Binding SaveCommand}"
|
||||
Content="保存配置"
|
||||
Cursor="Hand">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.State.Success}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="4">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Opacity" Value="0.9" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Width="100"
|
||||
Height="32"
|
||||
Command="{Binding CancelCommand}"
|
||||
Content="取消"
|
||||
Cursor="Hand">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="4">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Bg.Hover}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
15
SHH.CameraDashboard/Pages/CameraEdit.xaml.cs
Normal file
15
SHH.CameraDashboard/Pages/CameraEdit.xaml.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// CameraEdit.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class CameraEdit : UserControl
|
||||
{
|
||||
public CameraEdit()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
317
SHH.CameraDashboard/Pages/CameraImageSubscription.xaml
Normal file
317
SHH.CameraDashboard/Pages/CameraImageSubscription.xaml
Normal file
@@ -0,0 +1,317 @@
|
||||
<UserControl
|
||||
x:Class="SHH.CameraDashboard.CameraImageSubscription"
|
||||
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:local="clr-namespace:SHH.CameraDashboard"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="600"
|
||||
d:DesignWidth="900"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||
<local:SubscriptionTypeConverter x:Key="TypeConverter" />
|
||||
</UserControl.Resources>
|
||||
<Grid Margin="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="20" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Margin="0,0,0,15"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="18"
|
||||
Text="📡" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="订阅管理" />
|
||||
<TextBlock
|
||||
Margin="5,2,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text=" (实时分发控制)" />
|
||||
</StackPanel>
|
||||
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{StaticResource Radius.Normal}">
|
||||
<DataGrid
|
||||
BorderThickness="0"
|
||||
IsReadOnly="True"
|
||||
ItemsSource="{Binding Subscriptions}"
|
||||
SelectedItem="{Binding SelectedSubscription}"
|
||||
SelectionMode="Single">
|
||||
<DataGrid.Columns>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="80"
|
||||
Binding="{Binding AppId}"
|
||||
Header="App ID">
|
||||
<DataGridTextColumn.ElementStyle>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
</Style>
|
||||
</DataGridTextColumn.ElementStyle>
|
||||
</DataGridTextColumn>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="125"
|
||||
Binding="{Binding Type, Converter={StaticResource TypeConverter}}"
|
||||
Header="类型">
|
||||
<DataGridTextColumn.ElementStyle>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Margin" Value="5,0" />
|
||||
</Style>
|
||||
</DataGridTextColumn.ElementStyle>
|
||||
</DataGridTextColumn>
|
||||
|
||||
<DataGridTemplateColumn Width="70" Header="目标帧率">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Border
|
||||
Padding="5,1"
|
||||
HorizontalAlignment="Center"
|
||||
Background="{DynamicResource Brush.Bg.L3}"
|
||||
CornerRadius="3"
|
||||
ToolTip="配置的期望帧率">
|
||||
<TextBlock
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource Brush.Text.Inverse}"
|
||||
Text="{Binding DisplayFps, StringFormat={}{0} FPS}" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTemplateColumn Width="70" Header="实际帧率">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Border
|
||||
Padding="5,1"
|
||||
HorizontalAlignment="Center"
|
||||
CornerRadius="3">
|
||||
<TextBlock
|
||||
FontSize="11"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.State.Success}"
|
||||
Text="{Binding RealFps, StringFormat={}{0:F1} FPS}" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="2*"
|
||||
Binding="{Binding Memo}"
|
||||
Header="备注">
|
||||
<DataGridTextColumn.ElementStyle>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
|
||||
<Setter Property="ToolTip" Value="{Binding Memo}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Secondary}" />
|
||||
</Style>
|
||||
</DataGridTextColumn.ElementStyle>
|
||||
</DataGridTextColumn>
|
||||
|
||||
<DataGridTemplateColumn Width="50" Header="操作">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Button
|
||||
Height="30"
|
||||
Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
CommandParameter="{Binding AppId}"
|
||||
Content="注销"
|
||||
Style="{StaticResource Btn.Ghost.Danger}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
Grid.Row="3"
|
||||
Padding="20"
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{StaticResource Radius.Normal}">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="80" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="30" />
|
||||
<ColumnDefinition Width="80" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Grid.ColumnSpan="5"
|
||||
Margin="0,0,0,15"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,0,5,0"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="➕" />
|
||||
<TextBlock
|
||||
FontSize="14"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="新增 / 编辑订阅" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="App ID:" />
|
||||
<TextBox
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="0,5"
|
||||
Text="{Binding EditAppId}" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="3"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="类型:" />
|
||||
<ComboBox
|
||||
Grid.Row="1"
|
||||
Grid.Column="4"
|
||||
Height="{StaticResource Height.Input}"
|
||||
Margin="0,5"
|
||||
VerticalContentAlignment="Center"
|
||||
DisplayMemberPath="Value"
|
||||
ItemsSource="{Binding SubscriptionTypes}"
|
||||
SelectedValue="{Binding EditType}"
|
||||
SelectedValuePath="Key" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="目标帧率:" />
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Margin="0,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Slider
|
||||
VerticalAlignment="Center"
|
||||
IsSnapToTickEnabled="True"
|
||||
Maximum="25"
|
||||
Minimum="1"
|
||||
Value="{Binding EditFps}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="10,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Brand}"
|
||||
Text="{Binding EditFps, StringFormat={}{0} fps}" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="3"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="备注:" />
|
||||
<TextBox
|
||||
Grid.Row="2"
|
||||
Grid.Column="4"
|
||||
Margin="0,5"
|
||||
Text="{Binding EditMemo}" />
|
||||
|
||||
<Grid
|
||||
Grid.Row="3"
|
||||
Grid.ColumnSpan="5"
|
||||
Visibility="{Binding IsNetworkType, Converter={StaticResource BoolToVis}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="80" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="30" />
|
||||
<ColumnDefinition Width="80" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="目标 IP:" />
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Margin="0,5"
|
||||
Text="{Binding EditTargetIp}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="3"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="目标端口:" />
|
||||
<TextBox
|
||||
Grid.Column="4"
|
||||
Margin="0,5"
|
||||
Text="{Binding EditTargetPort}" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="4"
|
||||
Grid.ColumnSpan="5"
|
||||
Margin="0,15,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<Button
|
||||
Width="100"
|
||||
Margin="0,0,10,0"
|
||||
Command="{Binding ClearFormCommand}"
|
||||
Content="重置表单"
|
||||
Style="{StaticResource Btn.Ghost}" />
|
||||
|
||||
<Button
|
||||
Width="120"
|
||||
Command="{Binding SaveCommand}"
|
||||
Content="提交保存"
|
||||
Style="{StaticResource PrimaryBtnStyle}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
15
SHH.CameraDashboard/Pages/CameraImageSubscription.xaml.cs
Normal file
15
SHH.CameraDashboard/Pages/CameraImageSubscription.xaml.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// CameraImageSubscription.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class CameraImageSubscription : UserControl
|
||||
{
|
||||
public CameraImageSubscription()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
274
SHH.CameraDashboard/Pages/CameraImgProc.xaml
Normal file
274
SHH.CameraDashboard/Pages/CameraImgProc.xaml
Normal file
@@ -0,0 +1,274 @@
|
||||
<UserControl
|
||||
x:Class="SHH.CameraDashboard.CameraImgProc"
|
||||
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:local="clr-namespace:SHH.CameraDashboard"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DataContext="{d:DesignInstance local:CameraImgProcViewModel}"
|
||||
d:DesignHeight="650"
|
||||
d:DesignWidth="500"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<local:BoolToMaxScaleConverter x:Key="BoolToMaxScaleConverter" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid Margin="20" Background="{DynamicResource Brush.Bg.Window}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<DockPanel Margin="0,0,0,20" LastChildFill="False">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="{Binding Title}" />
|
||||
<Button
|
||||
Width="30"
|
||||
Height="30"
|
||||
Command="{Binding CancelCommand}"
|
||||
Content="✕"
|
||||
DockPanel.Dock="Right"
|
||||
Style="{DynamicResource Btn.Ghost}" />
|
||||
</DockPanel>
|
||||
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Margin="0,0,0,15"
|
||||
Padding="15"
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
CornerRadius="6">
|
||||
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
|
||||
<TextBlock Foreground="{DynamicResource Brush.Text.Secondary}" Text="源画面分辨率:" />
|
||||
<TextBlock
|
||||
Margin="10,0,0,0"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="{Binding SourceResolutionText}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<GroupBox
|
||||
Grid.Row="2"
|
||||
Margin="0,0,0,15"
|
||||
Padding="15"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Header="分辨率调整">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<DockPanel LastChildFill="False">
|
||||
<StackPanel>
|
||||
<TextBlock Foreground="{DynamicResource Brush.Text.Primary}" Text="允许缩小 (Allow Shrink)" />
|
||||
<TextBlock
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="可将画面缩小至任意尺寸" />
|
||||
</StackPanel>
|
||||
<CheckBox
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding AllowShrink}"
|
||||
Style="{DynamicResource Style.CheckBox.Switch}" />
|
||||
</DockPanel>
|
||||
|
||||
<DockPanel
|
||||
Grid.Column="1"
|
||||
Margin="20,0,0,0"
|
||||
LastChildFill="False">
|
||||
<StackPanel>
|
||||
<TextBlock Foreground="{DynamicResource Brush.Text.Primary}" Text="允许放大 (Allow Enlarge)" />
|
||||
<TextBlock
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="最大支持 1080P" />
|
||||
</StackPanel>
|
||||
<CheckBox
|
||||
Margin="5,0,0,0"
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding AllowEnlarge}"
|
||||
IsEnabled="{Binding CanCheckExpand}"
|
||||
Style="{DynamicResource Style.CheckBox.Switch}" />
|
||||
</DockPanel>
|
||||
|
||||
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Row="1" Margin="0,20,0,0">
|
||||
<TextBlock
|
||||
Margin="0,0,0,10"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource Brush.State.Warning}"
|
||||
Text="请先开启“允许缩小”或“允许放大”以调整分辨率">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSliderEnabled}" Value="False">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
|
||||
<Grid Margin="0,0,0,15" IsEnabled="{Binding IsSliderEnabled}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="50" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Margin="0,0,10,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="缩放比" />
|
||||
|
||||
<Slider
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
IsSnapToTickEnabled="True"
|
||||
Maximum="{Binding AllowEnlarge, Converter={StaticResource BoolToMaxScaleConverter}}"
|
||||
Minimum="10"
|
||||
TickFrequency="5"
|
||||
Value="{Binding ScalePercent}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource Brush.Brand}"
|
||||
Text="{Binding ScalePercent, StringFormat={}{0:F0}%}" />
|
||||
</Grid>
|
||||
|
||||
<Grid IsEnabled="{Binding IsSliderEnabled}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Margin="0,0,0,5"
|
||||
FontSize="12"
|
||||
Text="宽度" />
|
||||
<TextBox
|
||||
Height="32"
|
||||
Style="{DynamicResource Style.TextBox.Search}"
|
||||
Text="{Binding TargetWidth, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
<ToggleButton
|
||||
Grid.Column="1"
|
||||
Margin="15,18,15,0"
|
||||
IsChecked="{Binding IsRatioLocked}"
|
||||
ToolTip="锁定长宽比">
|
||||
<Path
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="M12,17C10.89,17 10,16.1 10,15C10,13.89 10.89,13 12,13A2,2 0 0,1 14,15A2,2 0 0,1 12,17M18,20V10H6V20H18M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V10C4,8.89 4.89,8 6,8H7V6A5,5 0 0,1 17,6V8H18M12,4A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,4Z"
|
||||
Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ToggleButton}}"
|
||||
Stretch="Uniform" />
|
||||
</ToggleButton>
|
||||
<StackPanel Grid.Column="2">
|
||||
<TextBlock
|
||||
Margin="0,0,0,5"
|
||||
FontSize="12"
|
||||
Text="高度" />
|
||||
<TextBox
|
||||
Height="32"
|
||||
Style="{DynamicResource Style.TextBox.Search}"
|
||||
Text="{Binding TargetHeight, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="2" Margin="0,0,0,20">
|
||||
<TextBlock
|
||||
Margin="0,20,0,10"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="常用尺寸预设" />
|
||||
<WrapPanel>
|
||||
<WrapPanel.Resources>
|
||||
<Style BasedOn="{StaticResource Btn.Ghost.Secondary}" TargetType="Button">
|
||||
<Setter Property="Margin" Value="0,0,8,8" />
|
||||
<Setter Property="Width" Value="Auto" />
|
||||
<Setter Property="Padding" Value="6,0" />
|
||||
<Setter Property="Height" Value="16" />
|
||||
<Setter Property="Command" Value="{Binding ApplyPresetCommand}" />
|
||||
</Style>
|
||||
</WrapPanel.Resources>
|
||||
<Button CommandParameter="1920x1080" Content="1080P (1920x1080)" />
|
||||
<Button CommandParameter="1280x720" Content="720P (1280x720)" />
|
||||
<Button CommandParameter="704x576" Content="D1 (704x576)" />
|
||||
<Button CommandParameter="640x480" Content="VGA (640x480)" />
|
||||
<Button CommandParameter="640x640" Content="Square (640x640)" />
|
||||
<Button CommandParameter="352x288" Content="CIF (352x288)" />
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<StackPanel Grid.Row="3">
|
||||
<TextBlock
|
||||
Margin="0,0,0,10"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="高级处理" />
|
||||
<Border
|
||||
Padding="10"
|
||||
Background="{DynamicResource Brush.Bg.Input}"
|
||||
CornerRadius="4">
|
||||
<DockPanel LastChildFill="False">
|
||||
<StackPanel>
|
||||
<TextBlock Foreground="{DynamicResource Brush.Text.Primary}" Text="图像画质增强 (Image Enhancement)" />
|
||||
<TextBlock
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="自动调节对比度与锐度" />
|
||||
</StackPanel>
|
||||
<CheckBox
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding EnhanceImage}"
|
||||
Style="{DynamicResource Style.CheckBox.Switch}" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="4"
|
||||
Margin="0,20,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Width="100"
|
||||
Margin="0,0,10,0"
|
||||
Command="{Binding CancelCommand}"
|
||||
Content="取消"
|
||||
Style="{DynamicResource Btn.Ghost}" />
|
||||
<Button
|
||||
Width="120"
|
||||
Command="{Binding SaveCommand}"
|
||||
Content="保存应用" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
14
SHH.CameraDashboard/Pages/CameraImgProc.xaml.cs
Normal file
14
SHH.CameraDashboard/Pages/CameraImgProc.xaml.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
/// <summary>
|
||||
/// CameraImgProc.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class CameraImgProc : UserControl
|
||||
{
|
||||
public CameraImgProc()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
364
SHH.CameraDashboard/Pages/CameraItemTop.xaml
Normal file
364
SHH.CameraDashboard/Pages/CameraItemTop.xaml
Normal file
@@ -0,0 +1,364 @@
|
||||
<UserControl
|
||||
x:Class="SHH.CameraDashboard.CameraItemTop"
|
||||
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:local="clr-namespace:SHH.CameraDashboard"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="100"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Border
|
||||
Padding="20,12,20,6"
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="0,0,0,1">
|
||||
|
||||
<Grid>
|
||||
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Grid.Style>
|
||||
<Style TargetType="Grid">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasSelection}" Value="False">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Grid.Style>
|
||||
<TextBlock
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Opacity="0.5"
|
||||
Text="请在左侧列表中选择一个摄像头以查看详情" />
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Grid.Style>
|
||||
<Style TargetType="Grid">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasSelection}" Value="False">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Grid.Style>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="120" />
|
||||
<ColumnDefinition Width="210" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<WrapPanel>
|
||||
<!-- 在线、离线小圆点 -->
|
||||
<Border
|
||||
Width="10"
|
||||
Height="10"
|
||||
Margin="0,0,10,0"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="5">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#FF5555" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Camera.IsPhysicalOnline}" Value="True">
|
||||
<Setter Property="Background" Value="#55FF55" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
|
||||
<!-- 播放状态、ID号 -->
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Border
|
||||
Margin="6,2,0,0"
|
||||
Padding="6,2"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="4">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#10808080" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Camera.Status}" Value="Playing">
|
||||
<Setter Property="Background" Value="#00CC6A" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Camera.Status}" Value="Faulted">
|
||||
<Setter Property="Background" Value="#FF5555" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="White"
|
||||
Text="{Binding Camera.Status}" />
|
||||
</Border>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="6,8,0,0"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Opacity="0.6"
|
||||
Text="ID:" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="26,8,0,0"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="{Binding Camera.Id}" />
|
||||
|
||||
</Grid>
|
||||
</WrapPanel>
|
||||
|
||||
<Grid Grid.Column="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<WrapPanel>
|
||||
<!-- 设备名称 -->
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
FontWeight="Bold">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="{Binding Camera.Name}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Camera.Name}" Value="">
|
||||
<Setter Property="Text" Value="未命名" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Secondary}" />
|
||||
<Setter Property="FontStyle" Value="Italic" />
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Camera.Name}" Value="{x:Null}">
|
||||
<Setter Property="Text" Value="未命名" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Secondary}" />
|
||||
<Setter Property="FontStyle" Value="Italic" />
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Opacity="0.5"
|
||||
Text="{Binding Camera.Brand}" />
|
||||
</WrapPanel>
|
||||
|
||||
<!-- IP 地址 -->
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Left"
|
||||
FontFamily="Consolas"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="{Binding Camera.IpAddress}" />
|
||||
</Grid>
|
||||
|
||||
<WrapPanel Grid.Column="2" VerticalAlignment="Center">
|
||||
<!-- 分辨率 -->
|
||||
<StackPanel Margin="20,0,20,0">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
FontSize="10"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Opacity="0.5"
|
||||
Text="RESOLUTION" />
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||
<TextBlock
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="{Binding Camera.Width}" />
|
||||
<TextBlock
|
||||
Margin="2,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text=" x " />
|
||||
<TextBlock
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="{Binding Camera.Height}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Border BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,1,0" />
|
||||
|
||||
<!-- 实际帧 -->
|
||||
<StackPanel Margin="20,0,20,0">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
FontSize="10"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Opacity="0.5"
|
||||
Text="REAL FPS" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="{Binding Camera.RealFps}" />
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="4"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="8,0,8,0"
|
||||
Command="{Binding DeleteCommand}"
|
||||
Style="{StaticResource Btn.Ghost.Danger}"
|
||||
ToolTip="删除此摄像头">
|
||||
<Path
|
||||
Width="24"
|
||||
Height="24"
|
||||
Data="{StaticResource Icon.Trash}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="8,0,8,0"
|
||||
Command="{Binding EditDeviceCommand}"
|
||||
Style="{StaticResource Btn.Ghost.Info}"
|
||||
ToolTip="编辑设备配置">
|
||||
<Path
|
||||
Width="18"
|
||||
Height="18"
|
||||
Data="{StaticResource Icon.Action.Edit}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="5,0,0,0"
|
||||
Command="{Binding ImageProcessCommand}"
|
||||
Style="{StaticResource Btn.Ghost.Info}"
|
||||
ToolTip="图像处理">
|
||||
<Path
|
||||
Width="24"
|
||||
Height="24"
|
||||
Data="{StaticResource Icon.Action.Process}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="5,0,0,0"
|
||||
Command="{Binding ImageSubscribeCommand}"
|
||||
Style="{StaticResource Btn.Ghost.Info}"
|
||||
ToolTip="图像订阅">
|
||||
<Path
|
||||
Width="18"
|
||||
Height="18"
|
||||
Data="{StaticResource Icon.Action.Subscribe}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
</Button>
|
||||
<Button
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="0,0,5,0"
|
||||
Command="{Binding PtzCommand}"
|
||||
Style="{StaticResource Btn.Ghost.Info}"
|
||||
ToolTip="云台控制 (PTZ)">
|
||||
|
||||
<Path
|
||||
Width="24"
|
||||
Height="24"
|
||||
Data="{StaticResource Icon.Ptz}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="8,0,8,0"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Command="{Binding TogglePlayCommand}"
|
||||
Cursor="Hand">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
x:Name="bd"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="8">
|
||||
|
||||
<Path
|
||||
Width="24"
|
||||
Height="24"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform">
|
||||
<Path.Style>
|
||||
<Style TargetType="Path">
|
||||
<Setter Property="Data" Value="{StaticResource Icon.Action.Play}" />
|
||||
<Setter Property="Fill" Value="{DynamicResource Brush.Text.Secondary}" />
|
||||
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Camera.Status}" Value="Playing">
|
||||
<Setter Property="Data" Value="{StaticResource Icon.Action.Pause}" />
|
||||
<Setter Property="Fill" Value="#00CC6A" />
|
||||
</DataTrigger>
|
||||
|
||||
<DataTrigger Binding="{Binding Camera.Status}" Value="Connecting">
|
||||
<Setter Property="Data" Value="{StaticResource Icon.Status.Loading}" />
|
||||
<Setter Property="Fill" Value="{DynamicResource Brush.State.Warning}" />
|
||||
</DataTrigger>
|
||||
|
||||
<DataTrigger Binding="{Binding Camera.IsPhysicalOnline}" Value="False">
|
||||
<Setter Property="Data" Value="{StaticResource Icon.Status.WifiOff}" />
|
||||
<Setter Property="Fill" Value="{DynamicResource Brush.State.Danger}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Path.Style>
|
||||
</Path>
|
||||
</Border>
|
||||
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="bd" Property="Background" Value="#1A808080" />
|
||||
</Trigger>
|
||||
<DataTrigger Binding="{Binding Camera.IsPhysicalOnline}" Value="False">
|
||||
<Setter Property="Cursor" Value="Arrow" />
|
||||
</DataTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
55
SHH.CameraDashboard/Pages/CameraItemTop.xaml.cs
Normal file
55
SHH.CameraDashboard/Pages/CameraItemTop.xaml.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// CameraItemTop.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class CameraItemTop : UserControl
|
||||
{
|
||||
// 持有 ViewModel 的实例
|
||||
private CameraItemTopViewModel _viewModel;
|
||||
|
||||
public CameraItemTop()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// 1. 初始化 ViewModel
|
||||
_viewModel = new CameraItemTopViewModel();
|
||||
|
||||
// 2. 设置 DataContext,这样 XAML 里的 {Binding Name} 就能找到 VM 里的属性
|
||||
this.DataContext = _viewModel;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 定义依赖属性 (DependencyProperty) - 这是接收绑定的关键
|
||||
// ============================================================
|
||||
|
||||
// 注册一个名为 "DataSource" 的依赖属性
|
||||
public static readonly DependencyProperty DataSourceProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(DataSource), // 属性名
|
||||
typeof(WebApiCameraModel), // 属性类型
|
||||
typeof(CameraItemTop), // 所属类型
|
||||
new PropertyMetadata(null, OnDataSourceChanged)); // 回调函数
|
||||
|
||||
// 这是一个普通的包装属性,方便代码访问
|
||||
public WebApiCameraModel DataSource
|
||||
{
|
||||
get => (WebApiCameraModel)GetValue(DataSourceProperty);
|
||||
set => SetValue(DataSourceProperty, value);
|
||||
}
|
||||
|
||||
// 当外部绑定的数据发生变化时(比如用户在左侧列表点选了新项)
|
||||
private static void OnDataSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = d as CameraItemTop;
|
||||
if (control != null && control._viewModel != null)
|
||||
{
|
||||
// 【核心逻辑】将外部传进来的数据,转发给 ViewModel
|
||||
control._viewModel.Camera = e.NewValue as WebApiCameraModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
374
SHH.CameraDashboard/Pages/CameraList.xaml
Normal file
374
SHH.CameraDashboard/Pages/CameraList.xaml
Normal file
@@ -0,0 +1,374 @@
|
||||
<UserControl
|
||||
x:Class="SHH.CameraDashboard.CameraList"
|
||||
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:local="clr-namespace:SHH.CameraDashboard"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="600"
|
||||
d:DesignWidth="300"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid Background="{DynamicResource Brush.Bg.Panel}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ComboBox
|
||||
Grid.Row="0"
|
||||
Height="32"
|
||||
Margin="10,10,10,0"
|
||||
VerticalContentAlignment="Center"
|
||||
ItemsSource="{Binding NodeOptions}"
|
||||
SelectedItem="{Binding SelectedNode}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock FontWeight="SemiBold" Text="{Binding ServiceNodeName}" />
|
||||
<TextBlock
|
||||
Margin="5,0,0,0"
|
||||
Foreground="Gray"
|
||||
Text="{Binding ServiceNodeIp, StringFormat=' ({0})'}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ServiceNodeIp}" Value="ALL">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<Grid Grid.Row="1" Margin="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border
|
||||
Padding="8,6"
|
||||
Background="{DynamicResource Brush.Bg.Input}"
|
||||
CornerRadius="4">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="🔍" />
|
||||
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
VerticalContentAlignment="Center"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
CaretBrush="{DynamicResource Brush.Text.Primary}"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<WrapPanel Grid.Column="1" Margin="0,5,0,0">
|
||||
<Button
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="5,0,0,0"
|
||||
Command="{Binding AddCameraCommand}"
|
||||
Style="{StaticResource Btn.Ghost.Info}"
|
||||
ToolTip="添加新设备">
|
||||
<Path
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource Icon.Plus}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="5,0,0,0"
|
||||
Command="{Binding RefreshCommand}"
|
||||
Style="{StaticResource Btn.Ghost.Info}"
|
||||
ToolTip="重新加载数据">
|
||||
<Path
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource Icon.Refresh}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
</Button>
|
||||
</WrapPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
<ListBox
|
||||
Grid.Row="2"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
ItemsSource="{Binding FilteredCameras}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
SelectedItem="{Binding SelectedCamera}">
|
||||
|
||||
<ListBox.ItemContainerStyle>
|
||||
<Style TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="10,8" />
|
||||
<Setter Property="Margin" Value="0,0,0,1" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Grid Margin="0,0,0,1">
|
||||
<Border
|
||||
x:Name="bd"
|
||||
Margin="5,0"
|
||||
Background="Transparent"
|
||||
CornerRadius="4"
|
||||
Opacity="0" />
|
||||
<Border Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter />
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="bd" Property="Background" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter TargetName="bd" Property="Opacity" Value="0.1" />
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="bd" Property="Background" Value="{DynamicResource Brush.Brand}" />
|
||||
<Setter TargetName="bd" Property="Opacity" Value="0.2" />
|
||||
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Brand}" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ListBox.ItemContainerStyle>
|
||||
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Margin="0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border
|
||||
Width="10"
|
||||
Height="10"
|
||||
Margin="0,0,10,0"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="5">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#FF5555" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsPhysicalOnline}" Value="True">
|
||||
<Setter Property="Background" Value="#55FF55" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<WrapPanel>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
FontWeight="Bold">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="{Binding Name}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Name}" Value="">
|
||||
<Setter Property="Text" Value="未命名" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Secondary}" />
|
||||
<Setter Property="FontStyle" Value="Italic" />
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Name}" Value="{x:Null}">
|
||||
<Setter Property="Text" Value="未命名" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Secondary}" />
|
||||
<Setter Property="FontStyle" Value="Italic" />
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock
|
||||
Margin="12,0,10,0"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Opacity="0.5"
|
||||
Text="{Binding Id}" />
|
||||
</WrapPanel>
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Width="24"
|
||||
Height="24"
|
||||
Margin="5,0,0,0"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
ToolTip="{Binding Status}">
|
||||
|
||||
<Path
|
||||
Width="14"
|
||||
Height="14"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform">
|
||||
<Path.Style>
|
||||
<Style TargetType="Path">
|
||||
<Setter Property="Data" Value="{StaticResource Icon.Action.Play}" />
|
||||
<Setter Property="Fill" Value="{DynamicResource Brush.Brand}" />
|
||||
|
||||
<Style.Triggers>
|
||||
|
||||
<DataTrigger Binding="{Binding IsPhysicalOnline}" Value="False">
|
||||
<Setter Property="Data" Value="{StaticResource Icon.Status.WifiOff}" />
|
||||
<Setter Property="Fill" Value="{DynamicResource Brush.State.Danger}" />
|
||||
</DataTrigger>
|
||||
|
||||
<DataTrigger Binding="{Binding Status}" Value="Connecting">
|
||||
<Setter Property="Data" Value="{StaticResource Icon.Status.Loading}" />
|
||||
<Setter Property="Fill" Value="{DynamicResource Brush.State.Warning}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Status}" Value="Authorizing">
|
||||
<Setter Property="Data" Value="{StaticResource Icon.Status.Loading}" />
|
||||
<Setter Property="Fill" Value="{DynamicResource Brush.State.Warning}" />
|
||||
</DataTrigger>
|
||||
|
||||
<DataTrigger Binding="{Binding Status}" Value="Playing">
|
||||
<Setter Property="Data" Value="{StaticResource Icon.Action.Pause}" />
|
||||
<Setter Property="Fill" Value="{DynamicResource Brush.Status.Warning}" />
|
||||
</DataTrigger>
|
||||
|
||||
<DataTrigger Binding="{Binding Status}" Value="Streaming">
|
||||
<Setter Property="Data" Value="{StaticResource Icon.Action.Pause}" />
|
||||
<Setter Property="Fill" Value="{DynamicResource Brush.Status.Warning}" />
|
||||
</DataTrigger>
|
||||
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Path.Style>
|
||||
</Path>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Margin="0,2,0,0" Orientation="Horizontal">
|
||||
<TextBlock
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="{Binding IpAddress}" />
|
||||
<TextBlock
|
||||
Margin="4,0"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Opacity="0.3"
|
||||
Text="|" />
|
||||
<TextBlock
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Opacity="0.7"
|
||||
Text="{Binding Brand}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,2,0,0" Orientation="Horizontal">
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Status}" Value="Playing">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Status}" Value="Streaming">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
|
||||
<TextBlock
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Opacity="0.6">
|
||||
<Run Foreground="{DynamicResource Brush.Text.Primary}" Text="{Binding Width}" />
|
||||
<Run Text="x" />
|
||||
<Run Foreground="{DynamicResource Brush.Text.Primary}" Text="{Binding Height}" />
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Text=" " />
|
||||
|
||||
<TextBlock
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Opacity="0.6">
|
||||
<Run Text="FPS:" />
|
||||
<Run
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="{Binding RealFps}" />
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVis}}">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<ProgressBar
|
||||
Width="100"
|
||||
Height="4"
|
||||
Margin="0,0,0,10"
|
||||
IsIndeterminate="True" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="正在同步数据..." />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
17
SHH.CameraDashboard/Pages/CameraList.xaml.cs
Normal file
17
SHH.CameraDashboard/Pages/CameraList.xaml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// CameraList.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class CameraList : UserControl
|
||||
{
|
||||
public CameraList()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
DataContext = new CameraListViewModel();
|
||||
}
|
||||
}
|
||||
}
|
||||
413
SHH.CameraDashboard/Pages/CameraPtz.xaml
Normal file
413
SHH.CameraDashboard/Pages/CameraPtz.xaml
Normal file
@@ -0,0 +1,413 @@
|
||||
<UserControl
|
||||
x:Class="SHH.CameraDashboard.CameraPtz"
|
||||
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:local="clr-namespace:SHH.CameraDashboard"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DataContext="{d:DesignInstance local:CameraPtzViewModel}"
|
||||
d:DesignHeight="600"
|
||||
d:DesignWidth="500"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Margin="20" Background="{DynamicResource Brush.Bg.Window}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<DockPanel Margin="0,0,0,20" LastChildFill="False">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="{Binding Title}" />
|
||||
</DockPanel>
|
||||
|
||||
<Grid Grid.Row="1" Margin="0,0,0,30">
|
||||
<Grid
|
||||
Width="220"
|
||||
Height="220"
|
||||
Margin="50,0,0,0"
|
||||
HorizontalAlignment="Left">
|
||||
<Ellipse
|
||||
Fill="{DynamicResource Brush.Bg.Input}"
|
||||
Stroke="{DynamicResource Brush.Border}"
|
||||
StrokeThickness="1" />
|
||||
|
||||
<Button
|
||||
Width="60"
|
||||
Height="60"
|
||||
Margin="0,10,0,0"
|
||||
VerticalAlignment="Top"
|
||||
local:TouchBehavior.Action="{x:Static local:PtzAction.Up}"
|
||||
Style="{StaticResource Btn.Ghost.Secondary}">
|
||||
<Path
|
||||
Width="24"
|
||||
Height="24"
|
||||
Data="{StaticResource Icon.ChevronUp}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Width="60"
|
||||
Height="60"
|
||||
Margin="0,0,0,10"
|
||||
VerticalAlignment="Bottom"
|
||||
local:TouchBehavior.Action="{x:Static local:PtzAction.Down}"
|
||||
Style="{StaticResource Btn.Ghost.Secondary}">
|
||||
<Path
|
||||
Width="24"
|
||||
Height="24"
|
||||
Data="{StaticResource Icon.ChevronUp}"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Style="{StaticResource Style.IconPath}">
|
||||
<Path.RenderTransform>
|
||||
<RotateTransform Angle="180" />
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Width="60"
|
||||
Height="60"
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
local:TouchBehavior.Action="{x:Static local:PtzAction.Left}"
|
||||
Style="{StaticResource Btn.Ghost.Secondary}">
|
||||
<Path
|
||||
Width="24"
|
||||
Height="24"
|
||||
Data="{StaticResource Icon.ChevronUp}"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Style="{StaticResource Style.IconPath}">
|
||||
<Path.RenderTransform>
|
||||
<RotateTransform Angle="-90" />
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Width="60"
|
||||
Height="60"
|
||||
Margin="0,0,10,0"
|
||||
HorizontalAlignment="Right"
|
||||
local:TouchBehavior.Action="{x:Static local:PtzAction.Right}"
|
||||
Style="{StaticResource Btn.Ghost.Secondary}">
|
||||
<Path
|
||||
Width="24"
|
||||
Height="24"
|
||||
Data="{StaticResource Icon.ChevronUp}"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Style="{StaticResource Style.IconPath}">
|
||||
<Path.RenderTransform>
|
||||
<RotateTransform Angle="90" />
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Width="60"
|
||||
Height="60"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="0.8"
|
||||
Style="{StaticResource Btn.Icon.Info}">
|
||||
<Path
|
||||
Width="28"
|
||||
Height="28"
|
||||
Data="{StaticResource Icon.Ptz}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<WrapPanel
|
||||
Margin="300,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center">
|
||||
<Button
|
||||
Width="150"
|
||||
Height="48"
|
||||
Margin="0,0,5,0"
|
||||
Command="{Binding SyncTimeCommand}"
|
||||
Style="{StaticResource Btn.Ghost.Info}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path
|
||||
Width="32"
|
||||
Height="32"
|
||||
Data="{StaticResource Icon.Action.Time}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Text="一键校时" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Width="150"
|
||||
Height="48"
|
||||
Margin="5,20,0,0"
|
||||
Command="{Binding RebootCommand}"
|
||||
Style="{StaticResource Btn.Ghost.Danger}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path
|
||||
Width="32"
|
||||
Height="32"
|
||||
Data="{StaticResource Icon.Action.Reboot}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Text="远程重启" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</WrapPanel>
|
||||
</Grid>
|
||||
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
Padding="15"
|
||||
Background="{DynamicResource Brush.Bg.Input}"
|
||||
CornerRadius="8">
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Margin="0,0,0,15"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="镜头与辅助" />
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="50" />
|
||||
<RowDefinition Height="50" />
|
||||
<RowDefinition Height="50" />
|
||||
<RowDefinition Height="15" />
|
||||
<RowDefinition Height="50" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="80" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel VerticalAlignment="Center" Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="变倍" />
|
||||
</StackPanel>
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Width="120"
|
||||
local:TouchBehavior.Action="{x:Static local:PtzAction.ZoomOut}"
|
||||
Style="{StaticResource Btn.Ghost.Secondary}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource Icon.Minus}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Text="缩小" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Width="120"
|
||||
local:TouchBehavior.Action="{x:Static local:PtzAction.ZoomIn}"
|
||||
Style="{StaticResource Btn.Ghost.Secondary}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource Icon.Plus}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Text="放大" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="聚焦" />
|
||||
</StackPanel>
|
||||
<Button
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Width="120"
|
||||
local:TouchBehavior.Action="{x:Static local:PtzAction.FocusNear}"
|
||||
Style="{StaticResource Btn.Ghost.Secondary}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource Icon.Focus.Near}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Text="拉近" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Width="120"
|
||||
local:TouchBehavior.Action="{x:Static local:PtzAction.FocusFar}"
|
||||
Style="{StaticResource Btn.Ghost.Secondary}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource Icon.Focus.Far}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Text="拉远" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="6,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="光圈" />
|
||||
</StackPanel>
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Width="120"
|
||||
local:TouchBehavior.Action="{x:Static local:PtzAction.IrisClose}"
|
||||
Style="{StaticResource Btn.Ghost.Secondary}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource Icon.Iris.Small}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Text="变小" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
Width="120"
|
||||
local:TouchBehavior.Action="{x:Static local:PtzAction.IrisOpen}"
|
||||
Style="{StaticResource Btn.Ghost.Secondary}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource Icon.Iris.Large}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Text="变大" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Border
|
||||
Grid.Row="3"
|
||||
Grid.ColumnSpan="3"
|
||||
Margin="0,5"
|
||||
VerticalAlignment="Center"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="0,0,0,1" />
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="4"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="6,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="雨刷" />
|
||||
</StackPanel>
|
||||
|
||||
<Button
|
||||
Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
Width="120"
|
||||
Command="{Binding StopCommand}"
|
||||
CommandParameter="{x:Static local:PtzAction.Wiper}"
|
||||
Style="{StaticResource Btn.Ghost.Secondary}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource Icon.Action.StopSmall}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Text="停止" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Grid.Row="4"
|
||||
Grid.Column="2"
|
||||
Width="120"
|
||||
Command="{Binding StartCommand}"
|
||||
CommandParameter="{x:Static local:PtzAction.Wiper}"
|
||||
Style="{StaticResource Btn.Ghost.Secondary}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource Icon.Action.PlaySmall}"
|
||||
Style="{StaticResource Style.IconPath}" />
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Text="启动" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
15
SHH.CameraDashboard/Pages/CameraPtz.xaml.cs
Normal file
15
SHH.CameraDashboard/Pages/CameraPtz.xaml.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// CameraPtz.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class CameraPtz : UserControl
|
||||
{
|
||||
public CameraPtz()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<UserControl
|
||||
x:Class="SHH.CameraDashboard.ServiceNodesDiagnostic"
|
||||
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:local="clr-namespace:SHH.CameraDashboard"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
|
||||
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||
<local:InverseNullToVisibilityConverter x:Key="InverseNullToVisibilityConverter" />
|
||||
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid Background="{DynamicResource Brush.Bg.Window}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MinWidth="250" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="*" MinWidth="300" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ListView
|
||||
x:Name="LogList"
|
||||
Grid.Column="0"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
ItemContainerStyle="{StaticResource Style.ListViewItem.Table}"
|
||||
ItemsSource="{Binding Logs}"
|
||||
SelectedItem="{Binding SelectedLog}">
|
||||
<ListView.View>
|
||||
<GridView ColumnHeaderContainerStyle="{StaticResource Style.GridViewHeader.Flat}">
|
||||
<GridViewColumn
|
||||
Width="150"
|
||||
DisplayMemberBinding="{Binding Time, StringFormat='MM-dd HH:mm:ss fff'}"
|
||||
Header="请求时间" />
|
||||
<GridViewColumn
|
||||
Width="60"
|
||||
DisplayMemberBinding="{Binding StatusCode}"
|
||||
Header="状态" />
|
||||
<GridViewColumn
|
||||
Width="70"
|
||||
DisplayMemberBinding="{Binding ElapsedMilliseconds}"
|
||||
Header="耗时 ms" />
|
||||
<GridViewColumn
|
||||
Width="60"
|
||||
DisplayMemberBinding="{Binding Method}"
|
||||
Header="动作" />
|
||||
<GridViewColumn
|
||||
Width="120"
|
||||
DisplayMemberBinding="{Binding AppModule}"
|
||||
Header="模块" />
|
||||
<GridViewColumn
|
||||
Width="300"
|
||||
DisplayMemberBinding="{Binding Url}"
|
||||
Header="URL" />
|
||||
</GridView>
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
|
||||
<GridSplitter
|
||||
Grid.Column="1"
|
||||
Width="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{DynamicResource Brush.Bg.Panel}" />
|
||||
|
||||
<Grid Grid.Column="2" Background="{DynamicResource Brush.Bg.Input}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<DockPanel LastChildFill="False">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<RadioButton
|
||||
Command="{Binding SwitchTabCommand}"
|
||||
CommandParameter="0"
|
||||
Content="Request"
|
||||
GroupName="Logs"
|
||||
IsChecked="True"
|
||||
Style="{StaticResource TabRadioStyle}" />
|
||||
<RadioButton
|
||||
Command="{Binding SwitchTabCommand}"
|
||||
CommandParameter="1"
|
||||
Content="Response"
|
||||
GroupName="Logs"
|
||||
Style="{StaticResource TabRadioStyle}" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<TextBox
|
||||
Grid.Row="1"
|
||||
IsReadOnly="True"
|
||||
Style="{StaticResource Style.TextBox.CodeEditor}"
|
||||
Text="{Binding DisplayContent}"
|
||||
Visibility="{Binding SelectedLog, Converter={StaticResource NullToVisibilityConverter}}" />
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding SelectedLog, Converter={StaticResource InverseNullToVisibilityConverter}}">
|
||||
<TextBlock Foreground="{DynamicResource Brush.Text.Secondary}" Text="请选择一条日志查看详情" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<CheckBox
|
||||
Margin="15,0,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Content="过滤自动发送"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
IsChecked="{Binding IsFilterAutoLogs}"
|
||||
Style="{DynamicResource Style.CheckBox.Switch}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// ServiceNodesDiagnostic.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class ServiceNodesDiagnostic : UserControl
|
||||
{
|
||||
public ServiceNodesDiagnostic()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
126
SHH.CameraDashboard/Pages/Diagnostics/ServiceNodesViewModel.cs
Normal file
126
SHH.CameraDashboard/Pages/Diagnostics/ServiceNodesViewModel.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
public class ServiceNodesViewModel : INotifyPropertyChanged, IOverlayClosable, IDisposable
|
||||
{
|
||||
// 接口要求的关闭事件
|
||||
public event Action? RequestClose;
|
||||
|
||||
// 日志数据集合
|
||||
public ObservableCollection<LogWebApiModel> Logs { get; } = new ObservableCollection<LogWebApiModel>();
|
||||
|
||||
private LogWebApiModel _selectedLog;
|
||||
public LogWebApiModel SelectedLog
|
||||
{
|
||||
get => _selectedLog;
|
||||
set
|
||||
{
|
||||
_selectedLog = value;
|
||||
OnPropertyChanged();
|
||||
UpdateDisplayContent(); // 选中项改变时更新右侧文本
|
||||
}
|
||||
}
|
||||
|
||||
private string _displayContent;
|
||||
public string DisplayContent
|
||||
{
|
||||
get => _displayContent;
|
||||
set { _displayContent = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private int _currentTabIndex = 0; // 0: Request, 1: Response
|
||||
// =========================================================
|
||||
// [新增] 1. 过滤开关属性 (绑定到界面 CheckBox)
|
||||
// =========================================================
|
||||
private bool _isFilterAutoLogs;
|
||||
public bool IsFilterAutoLogs
|
||||
{
|
||||
get => _isFilterAutoLogs;
|
||||
set
|
||||
{
|
||||
_isFilterAutoLogs = value;
|
||||
OnPropertyChanged();
|
||||
// 可选:切换时是否要清空现有日志?根据需求决定
|
||||
// if (value) Logs.Clear();
|
||||
}
|
||||
}
|
||||
public ICommand ClearCommand { get; }
|
||||
public ICommand CloseCommand { get; }
|
||||
public ICommand SwitchTabCommand { get; }
|
||||
|
||||
public ServiceNodesViewModel()
|
||||
{
|
||||
// 【关键步骤】订阅全局 API 服务的日志事件
|
||||
WebApiService.Instance.OnRequestCompleted += OnNewLogReceived;
|
||||
|
||||
// 初始化指令
|
||||
ClearCommand = new RelayCommand<object>(_ => Logs.Clear());
|
||||
CloseCommand = new RelayCommand<object>(_ => RequestClose?.Invoke());
|
||||
|
||||
SwitchTabCommand = new RelayCommand<string>(index =>
|
||||
{
|
||||
_currentTabIndex = int.Parse(index);
|
||||
UpdateDisplayContent();
|
||||
});
|
||||
}
|
||||
/// <summary>
|
||||
/// 当 WebApiService 产生新日志时的回调
|
||||
/// </summary>
|
||||
private void OnNewLogReceived(LogWebApiModel log)
|
||||
{
|
||||
// 检查程序是否正在退出
|
||||
if (Application.Current == null) return;
|
||||
|
||||
if (IsFilterAutoLogs && CheckIsAutoLog(log))
|
||||
{
|
||||
return; // 如果是自动日志且开启了过滤,直接丢弃,不进入 UI 线程
|
||||
}
|
||||
|
||||
// 【线程安全】必须将操作封送回 UI 线程
|
||||
Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
// 1. 将最新日志插入顶部
|
||||
Logs.Insert(0, log);
|
||||
|
||||
// 2. 限制日志数量 (例如只保留最近 500 条),防止内存溢出
|
||||
if (Logs.Count > 500)
|
||||
{
|
||||
Logs.RemoveAt(Logs.Count - 1);
|
||||
}
|
||||
|
||||
// 3. (可选) 如果你想让列表自动滚动,可以在 View 的 CodeBehind 里做,或者在这里处理选中项逻辑
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
private bool CheckIsAutoLog(LogWebApiModel log)
|
||||
{
|
||||
if (log.IsAutoPost) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void UpdateDisplayContent()
|
||||
{
|
||||
if (SelectedLog == null)
|
||||
{
|
||||
DisplayContent = string.Empty;
|
||||
return;
|
||||
}
|
||||
// 根据当前选中的 Tab 显示请求或响应内容
|
||||
DisplayContent = _currentTabIndex == 0 ? SelectedLog.RequestData : SelectedLog.ResponseData;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
protected void OnPropertyChanged([CallerMemberName] string name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
WebApiService.Instance.OnRequestCompleted -= OnNewLogReceived;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
<UserControl
|
||||
x:Class="SHH.CameraDashboard.WizardControl"
|
||||
x:Class="SHH.CameraDashboard.WizardClientsControl"
|
||||
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"
|
||||
Width="850"
|
||||
Height="550"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
Background="{DynamicResource Brush.Bg.Window}"
|
||||
BorderBrush="{DynamicResource Brush.Border.Focus}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{StaticResource Radius.Normal}">
|
||||
CornerRadius="{DynamicResource Radius.Normal}">
|
||||
<Grid Margin="25">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -24,17 +24,17 @@
|
||||
<DockPanel LastChildFill="False">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="{StaticResource Size.Font.Huge}"
|
||||
FontSize="{DynamicResource Size.Font.Huge}"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="📡 服务节点配置" />
|
||||
<Button
|
||||
Width="30"
|
||||
Height="30"
|
||||
Click="Cancel_Click"
|
||||
Command="{Binding CancelCommand}"
|
||||
Content="✕"
|
||||
DockPanel.Dock="Right"
|
||||
Style="{StaticResource Btn.Ghost}" />
|
||||
Style="{DynamicResource Btn.Ghost}" />
|
||||
</DockPanel>
|
||||
<TextBlock
|
||||
Margin="0,10,0,0"
|
||||
@@ -59,14 +59,15 @@
|
||||
FontWeight="Bold"
|
||||
Text="节点清单" />
|
||||
<Button
|
||||
Width="90"
|
||||
Height="26"
|
||||
Padding="15,0"
|
||||
Background="{DynamicResource Brush.Accent}"
|
||||
BorderThickness="0"
|
||||
Click="AddNode_Click"
|
||||
Command="{Binding AddNodeCommand}"
|
||||
Content="+ 新增一行"
|
||||
DockPanel.Dock="Right"
|
||||
Style="{StaticResource {x:Type Button}}" />
|
||||
Foreground="White" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
@@ -76,13 +77,14 @@
|
||||
Background="Transparent"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="1"
|
||||
ItemContainerStyle="{StaticResource Style.ListViewItem.Table}">
|
||||
ItemContainerStyle="{DynamicResource Style.ListViewItem.Table}"
|
||||
ItemsSource="{Binding ServiceNodes}">
|
||||
<ListView.View>
|
||||
<GridView ColumnHeaderContainerStyle="{StaticResource Style.GridViewHeader.Flat}">
|
||||
<GridView ColumnHeaderContainerStyle="{DynamicResource Style.GridViewHeader.Flat}">
|
||||
<GridViewColumn Width="180" Header="节点名称">
|
||||
<GridViewColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Style="{StaticResource Style.TextBox.InTable}" Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{DynamicResource Style.TextBox.InTable}" Text="{Binding ServiceNodeName, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
</GridViewColumn>
|
||||
@@ -90,7 +92,7 @@
|
||||
<GridViewColumn Width="180" Header="IP 地址">
|
||||
<GridViewColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Style="{StaticResource Style.TextBox.InTable}" Text="{Binding Ip, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{DynamicResource Style.TextBox.InTable}" Text="{Binding ServiceNodeIp, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
</GridViewColumn>
|
||||
@@ -98,11 +100,10 @@
|
||||
<GridViewColumn Width="100" Header="端口">
|
||||
<GridViewColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Style="{StaticResource Style.TextBox.InTable}" Text="{Binding Port, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{DynamicResource Style.TextBox.InTable}" Text="{Binding ServiceNodePort, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
</GridViewColumn>
|
||||
|
||||
<GridViewColumn Width="180" Header="连通性状态">
|
||||
<GridViewColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
@@ -114,8 +115,28 @@
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="Bold"
|
||||
Foreground="{Binding StatusColor}"
|
||||
Text="{Binding Status}" />
|
||||
Text="{Binding Status}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="Gray" />
|
||||
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Status}" Value="在线">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Status.Success}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Status}" Value="离线">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Status.Danger}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Status}" Value="正在检测...">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Accent}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Status}" Value="警告">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Status.Warning}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
@@ -128,10 +149,11 @@
|
||||
Width="45"
|
||||
Height="24"
|
||||
Padding="0"
|
||||
Click="DeleteNode_Click"
|
||||
Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=ListView}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="删除"
|
||||
FontSize="11"
|
||||
Style="{StaticResource Btn.Danger}" />
|
||||
Style="{DynamicResource Btn.Danger}" />
|
||||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
</GridViewColumn>
|
||||
@@ -155,15 +177,15 @@
|
||||
Width="110"
|
||||
Height="32"
|
||||
Margin="0,0,10,0"
|
||||
Click="Check_Click"
|
||||
Command="{Binding CheckCommand}"
|
||||
Content="🔍 立即检测"
|
||||
Style="{StaticResource Btn.Ghost}" />
|
||||
Style="{DynamicResource Btn.Ghost}" />
|
||||
<Button
|
||||
Width="120"
|
||||
Height="32"
|
||||
Background="{DynamicResource Brush.Accent}"
|
||||
BorderThickness="0"
|
||||
Click="Apply_Click"
|
||||
Command="{Binding ConfirmCommand}"
|
||||
Content="保存并应用"
|
||||
Foreground="White" />
|
||||
</StackPanel>
|
||||
19
SHH.CameraDashboard/Pages/WizardClientsControl.xaml.cs
Normal file
19
SHH.CameraDashboard/Pages/WizardClientsControl.xaml.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// WizardClientsControl.xaml 的交互逻辑
|
||||
/// <para>客户端配置向导的视图,通过数据绑定与 <see cref="ViewModels.WizardClientsViewModel"/> 交互</para>
|
||||
/// </summary>
|
||||
public partial class WizardClientsControl : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化 <see cref="WizardClientsControl"/> 类的新实例
|
||||
/// </summary>
|
||||
public WizardClientsControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,18 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Olds\**" />
|
||||
<EmbeddedResource Remove="Olds\**" />
|
||||
<None Remove="Olds\**" />
|
||||
<Page Remove="Olds\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="App\WizardControl.xaml.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Remove="App\WizardControl.xaml" />
|
||||
<Page Remove="Style\Themes\Colors.Dark.xaml" />
|
||||
<Page Remove="Style\Themes\Colors.Light.xaml" />
|
||||
<Page Remove="Style\Themes\Sizes.xaml" />
|
||||
@@ -31,8 +43,4 @@
|
||||
<Resource Include="Style\Themes\Styles.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Converters\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public static class HttpService
|
||||
{
|
||||
// 单例 HttpClient,避免端口耗尽
|
||||
private static readonly HttpClient _client;
|
||||
|
||||
// 【关键】日志事件,UI层订阅这个事件来显示日志
|
||||
public static event Action<ApiLogEntry> OnApiLog;
|
||||
|
||||
static HttpService()
|
||||
{
|
||||
_client = new HttpClient();
|
||||
// 设置一个合理的超时,避免界面卡死
|
||||
_client.Timeout = TimeSpan.FromSeconds(5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 泛型 GET 方法
|
||||
/// </summary>
|
||||
public static async Task<T> GetAsync<T>(string url)
|
||||
{
|
||||
return await ExecuteRequestAsync<T>(new HttpRequestMessage(HttpMethod.Get, url));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 泛型 POST 方法
|
||||
/// </summary>
|
||||
public static async Task<T> PostAsync<T>(string url, object data)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
string json = JsonConvert.SerializeObject(data);
|
||||
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
return await ExecuteRequestAsync<T>(request, json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 专门用于【连通性测试】的方法 (不关心返回值内容,只关心通不通)
|
||||
/// </summary>
|
||||
public static async Task<bool> TestConnectionAsync(string url)
|
||||
{
|
||||
// 复用核心逻辑,但泛型传 string (忽略结果) 或 object
|
||||
try
|
||||
{
|
||||
await ExecuteRequestAsync<string>(new HttpRequestMessage(HttpMethod.Get, url));
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 核心执行逻辑 ---
|
||||
private static async Task<T> ExecuteRequestAsync<T>(HttpRequestMessage request, string requestBody = "")
|
||||
{
|
||||
// 1. 准备日志对象
|
||||
var log = new ApiLogEntry
|
||||
{
|
||||
Method = request.Method.ToString(),
|
||||
Url = request.RequestUri.ToString(),
|
||||
RequestBody = requestBody
|
||||
};
|
||||
|
||||
var sw = Stopwatch.StartNew(); // 开始计时
|
||||
|
||||
try
|
||||
{
|
||||
// 2. 发起网络请求
|
||||
var response = await _client.SendAsync(request);
|
||||
|
||||
sw.Stop(); // 停止计时
|
||||
log.DurationMs = sw.ElapsedMilliseconds;
|
||||
log.StatusCode = (int)response.StatusCode;
|
||||
|
||||
// 3. 读取响应内容
|
||||
string content = await response.Content.ReadAsStringAsync();
|
||||
log.ResponseBody = content;
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
// 如果 T 是 string,直接返回内容,不反序列化
|
||||
if (typeof(T) == typeof(string))
|
||||
return (T)(object)content;
|
||||
|
||||
// 反序列化 JSON
|
||||
return JsonConvert.DeserializeObject<T>(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
log.ErrorMessage = $"HTTP {response.StatusCode}: {response.ReasonPhrase}";
|
||||
throw new HttpRequestException(log.ErrorMessage);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
if (log.DurationMs == 0) log.DurationMs = sw.ElapsedMilliseconds;
|
||||
|
||||
log.StatusCode = 0; // 0 代表网络层面的失败(如断网)
|
||||
log.ErrorMessage = ex.Message;
|
||||
log.ResponseBody = ex.ToString(); // 记录堆栈以便排查
|
||||
|
||||
throw; // 抛出异常供调用方 UI 处理
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 4. 【广播日志】无论成功失败,都触发事件
|
||||
// 使用 Invoke 确保 UI 订阅者能收到
|
||||
OnApiLog?.Invoke(log);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
SHH.CameraDashboard/Services/LocalStorageService.cs
Normal file
110
SHH.CameraDashboard/Services/LocalStorageService.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// 本地文件存储服务
|
||||
/// 职责:
|
||||
/// 1. 提供通用的、异步的对象序列化和反序列化功能。
|
||||
/// 2. 将对象保存为 UTF-8 编码的 JSON 文件。
|
||||
/// 3. 从 JSON 文件中读取并反序列化为对象。
|
||||
/// 4. 处理文件不存在、内容为空或格式错误等常见异常情况。
|
||||
/// </summary>
|
||||
public static class LocalStorageService
|
||||
{
|
||||
#region --- 保存方法 ---
|
||||
|
||||
/// <summary>
|
||||
/// 将指定对象异步保存到文件。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">要保存的对象的类型。</typeparam>
|
||||
/// <param name="filePath">目标文件的完整路径。</param>
|
||||
/// <param name="data">要保存的对象实例。</param>
|
||||
/// <exception cref="Exception">当保存过程中发生任何错误时抛出。</exception>
|
||||
public static async Task SaveAsync<T>(string filePath, T data)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 确保文件所在的目录存在,如果不存在则创建
|
||||
var directoryPath = Path.GetDirectoryName(filePath);
|
||||
if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath))
|
||||
{
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
}
|
||||
|
||||
// 2. 使用 JsonHelper 将对象序列化为 JSON 字符串
|
||||
string json = JsonHelper.Serialize(data);
|
||||
|
||||
// 3. 使用 StreamWriter 异步将 JSON 字符串写入文件
|
||||
// - `false` 表示覆盖现有文件。
|
||||
// - `Encoding.UTF8` 确保文件编码正确,特别是处理中文字符时。
|
||||
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8))
|
||||
{
|
||||
await writer.WriteAsync(json);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录错误到调试输出
|
||||
System.Diagnostics.Debug.WriteLine($"保存文件失败 [{filePath}]: {ex.Message}");
|
||||
// 重新抛出异常,因为保存失败通常是一个需要上层处理的严重问题(例如,提示用户)
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 加载方法 ---
|
||||
|
||||
/// <summary>
|
||||
/// 从文件异步加载并反序列化为指定类型的对象。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标对象的类型。必须是引用类型且具有公共无参数构造函数。</typeparam>
|
||||
/// <param name="filePath">源文件的完整路径。</param>
|
||||
/// <returns>
|
||||
/// 成功时返回反序列化后的对象。
|
||||
/// 如果文件不存在、内容为空、格式错误或发生任何异常,将返回一个新创建的默认对象实例。
|
||||
/// </returns>
|
||||
public static async Task<T> LoadAsync<T>(string filePath) where T : class, new()
|
||||
{
|
||||
// 1. 如果文件不存在,直接返回一个新的默认对象
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return new T();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 2. 使用 StreamReader 异步读取文件全部内容
|
||||
using (var reader = new StreamReader(filePath, Encoding.UTF8))
|
||||
{
|
||||
string json = await reader.ReadToEndAsync();
|
||||
|
||||
// 3. 如果文件内容为空或只包含空白字符,返回一个新的默认对象
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return new T();
|
||||
}
|
||||
|
||||
// 4. 使用 JsonHelper 尝试反序列化 JSON 字符串
|
||||
var result = JsonHelper.Deserialize<T>(json);
|
||||
|
||||
// 5. 如果反序列化结果为 null (例如,JSON 内容为 "null" 或格式不匹配),
|
||||
// 则返回一个新的默认对象作为兜底。
|
||||
return result ?? new T();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录错误到调试输出
|
||||
System.Diagnostics.Debug.WriteLine($"读取文件失败 [{filePath}]: {ex.Message}");
|
||||
// 发生任何异常(如文件被占用、权限问题等),都返回一个新的默认对象,
|
||||
// 以确保程序能够继续运行而不会崩溃。
|
||||
return new T();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace SHH.CameraDashboard.Services
|
||||
{
|
||||
public static class StorageService
|
||||
{
|
||||
// 基础目录:C:\Users\Name\AppData\Local\SHH_Dashboard\
|
||||
private static readonly string BaseDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"SHH_CameraDashboard");
|
||||
|
||||
/// <summary>
|
||||
/// 泛型保存:将数据 T 序列化为指定文件名的 JSON
|
||||
/// </summary>
|
||||
public static void Save<T>(T data, string fileName) where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(BaseDir)) Directory.CreateDirectory(BaseDir);
|
||||
|
||||
string filePath = Path.Combine(BaseDir, fileName);
|
||||
string json = JsonConvert.SerializeObject(data, Formatting.Indented);
|
||||
File.WriteAllText(filePath, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"存储失败 [{fileName}]: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 泛型读取:从指定 JSON 文件反序列化为数据 T
|
||||
/// </summary>
|
||||
public static T Load<T>(string fileName) where T : class, new()
|
||||
{
|
||||
try
|
||||
{
|
||||
string filePath = Path.Combine(BaseDir, fileName);
|
||||
if (!File.Exists(filePath)) return new T();
|
||||
|
||||
string json = File.ReadAllText(filePath);
|
||||
return JsonConvert.DeserializeObject<T>(json) ?? new T();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"读取失败 [{fileName}]: {ex.Message}");
|
||||
return new T();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
SHH.CameraDashboard/Services/WebApis/ApiClient.cs
Normal file
18
SHH.CameraDashboard/Services/WebApis/ApiClient.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public class ApiClient
|
||||
{
|
||||
// 单例模式
|
||||
public static ApiClient Instance { get; } = new ApiClient();
|
||||
|
||||
// 各个模块的访问入口
|
||||
public CameraRepository Cameras { get; }
|
||||
public MonitorRepository Monitor { get; }
|
||||
|
||||
private ApiClient()
|
||||
{
|
||||
Cameras = new CameraRepository();
|
||||
Monitor = new MonitorRepository();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
public partial class CameraRepository
|
||||
{
|
||||
#region GetListByAddressAsync 获取摄像头列表
|
||||
|
||||
/// <summary>
|
||||
/// [新增] 动态检测方法。
|
||||
/// 专门用于向导界面,根据临时输入的 IP 地址和端口号来获取摄像头列表。
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">目标服务器的 IP 地址。</param>
|
||||
/// <param name="port">目标服务器的端口号。</param>
|
||||
/// <param name="pageName">调用此方法的页面或步骤名称,用于日志记录。</param>
|
||||
/// <returns>
|
||||
/// 一个异步任务,其结果包含一个 <see cref="WebApiCameraModel"/> 对象的列表。
|
||||
/// - 如果成功获取到列表,返回该列表(可能为空)。
|
||||
/// - 如果获取过程中发生任何异常(如网络错误、服务器无响应等),返回 <c>null</c>。
|
||||
/// </returns>
|
||||
public async Task<List<WebApiCameraModel>?> GetListByAddressAsync(string ipAddress, string port, string pageName)
|
||||
{
|
||||
// 1. 统一管理并拼接请求 URL
|
||||
// 在这里将基础地址与 API 路径组合,ViewModel 无需关心具体的路由细节。
|
||||
// WebApiRoutes.Cameras.Root 是一个假设的常量,例如 "/api/Cameras"。
|
||||
string requestUrl = $"http://{ipAddress}:{port}{WebApiRoutes.Cameras.Root}";
|
||||
|
||||
try
|
||||
{
|
||||
// 2. 调用底层的 WebApiService 发送 GET 请求
|
||||
// 传入 "Wizard" 相关的模块名,便于在日志中区分请求来源。
|
||||
string jsonResponse = await WebApiService.Instance.GetAsync(requestUrl, moduleName: $"摄像头列表-{pageName}", default, true);
|
||||
|
||||
// 3. 使用 JsonHelper 进行反序列化
|
||||
// 利用 JsonHelper 的防御性实现,即使 JSON 格式错误或内容为 "null",也不会抛出异常。
|
||||
var cameraList = JsonHelper.Deserialize<List<WebApiCameraModel>>(jsonResponse);
|
||||
|
||||
// 4. 确保返回的列表不为 null
|
||||
// 如果反序列化结果为 null(例如,API 返回 "null"),则返回一个空列表,
|
||||
// 这样调用方可以安全地遍历,而无需进行 null 检查。
|
||||
return cameraList ?? new List<WebApiCameraModel>();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 5. 捕获所有异常
|
||||
// 在动态检测阶段,网络不通、服务器宕机等都是预期内的情况。
|
||||
// 返回 null 作为明确的失败信号,ViewModel 可以据此向用户显示“连接失败”等提示。
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ControlPowerAsync 摄像头启停
|
||||
|
||||
/// <summary>
|
||||
/// 调用服务端的接口来控制指定摄像头的启停状态(开机/关机)。
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">目标服务器的 IP 地址。</param>
|
||||
/// <param name="port">目标服务器的端口号。</param>
|
||||
/// <param name="cameraId">要控制的摄像头的唯一ID。</param>
|
||||
/// <param name="enable">true 表示开机,false 表示关机。</param>
|
||||
/// <param name="pageName">调用此方法的页面或功能名称,用于日志记录。</param>
|
||||
/// <returns>
|
||||
/// 一个异步任务,其结果为一个布尔值:
|
||||
/// - <c>true</c>:表示操作成功(HTTP 响应为 200 OK)。
|
||||
/// - <c>false</c>:表示操作失败(如网络错误、服务器返回 4xx/5xx 错误等)。
|
||||
/// </returns>
|
||||
public async Task<bool> ControlPowerAsync(long cameraId, bool enable, string pageName)
|
||||
{
|
||||
var useServiceNode = AppGlobal.UseServiceNode;
|
||||
if (useServiceNode == null)
|
||||
return false;
|
||||
|
||||
var ipAddress = useServiceNode.ServiceNodeIp;
|
||||
var port = useServiceNode.ServiceNodePort;
|
||||
|
||||
// 1. 拼接请求 URL
|
||||
// 将控制参数(cameraId 和 enable)作为 URL 的一部分发送。
|
||||
string requestUrl = $"http://{ipAddress}:{port}/api/Cameras/{cameraId}/power?enabled={enable}";
|
||||
|
||||
try
|
||||
{
|
||||
// 2. 调用底层服务发送 POST 请求
|
||||
// 关键点:
|
||||
// a. 即使服务端不关心请求体(Body),`PostAsync` 方法也要求一个非 null 的 JSON 字符串。
|
||||
// 因此,我们传入一个空的 JSON 对象 "{}" 作为占位符。
|
||||
// b. 我们不检查响应内容,因为如果请求失败(例如,服务器返回 404 Not Found 或 500 Internal Server Error),
|
||||
// `WebApiService` 会抛出异常。
|
||||
await WebApiService.Instance.PostAsync(requestUrl, "{}", moduleName: $"设备启停-{pageName}");
|
||||
|
||||
// 3. 如果代码执行到这里,说明请求成功且没有抛出异常。
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 4. 捕获所有可能发生的异常
|
||||
// 这包括网络连接问题、DNS解析失败、服务器无响应或返回错误状态码等。
|
||||
// 记录异常信息到调试输出,以便于排查问题。
|
||||
System.Diagnostics.Debug.WriteLine($"控制摄像头电源失败 [IP: {ipAddress}, CameraId: {cameraId}]: {ex.Message}");
|
||||
|
||||
// 返回 false 通知调用方操作已失败。
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public partial class CameraRepository
|
||||
{
|
||||
#region GetCameraDetailsAsync 取单个摄像头详情
|
||||
|
||||
/// <summary>
|
||||
/// 获取摄像头的详细配置信息。
|
||||
/// 此方法封装了完整的 API 调用和 JSON 到 DTO 的手动映射过程。
|
||||
/// </summary>
|
||||
/// <param name="cameraId">要查询的摄像头的唯一 ID。</param>
|
||||
/// <returns>
|
||||
/// 一个异步任务,其结果是一个 <see cref="CameraEditInfo"/> 对象,包含摄像头的详细配置。
|
||||
/// 如果配置节点信息不存在、API 请求失败或 JSON 解析失败,则返回 <c>null</c>。
|
||||
/// </returns>
|
||||
public async Task<CameraEditInfo?> GetCameraDetailsAsync(long cameraId)
|
||||
{
|
||||
// 1. 从全局数据中获取当前使用的服务节点信息
|
||||
var serviceNode = AppGlobal.UseServiceNode;
|
||||
if (serviceNode == null)
|
||||
{
|
||||
// 如果没有配置服务节点,则无法获取信息
|
||||
return null;
|
||||
}
|
||||
|
||||
var ipAddress = serviceNode.ServiceNodeIp;
|
||||
var port = serviceNode.ServiceNodePort;
|
||||
|
||||
// 2. 拼接 API 请求的 URL
|
||||
string requestUrl = $"http://{ipAddress}:{port}/api/Cameras/{cameraId}";
|
||||
|
||||
try
|
||||
{
|
||||
// 3. 调用 WebApiService 发送 GET 请求,获取原始 JSON 字符串
|
||||
string jsonResponse = await WebApiService.Instance.GetAsync(requestUrl, "GetDetail");
|
||||
|
||||
// 4. 检查返回的 JSON 是否为空
|
||||
if (string.IsNullOrEmpty(jsonResponse))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 5. 将原始 JSON 字符串手动映射到目标 DTO 对象
|
||||
// 这种方式可以精确控制每个字段的转换,处理类型不匹配等问题。
|
||||
return MapJsonToEditDto(jsonResponse);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 6. 捕获所有可能的异常(网络错误、服务器错误等)
|
||||
System.Diagnostics.Debug.WriteLine($"[Repository] 获取摄像头详情失败 (ID: {cameraId}): {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateCameraAsync 更新单个摄像头
|
||||
|
||||
/// <summary>
|
||||
/// [新增] 更新摄像头的配置信息。
|
||||
/// 此方法负责将 <see cref="CameraEditInfo"/> 对象转换为 API 所需的 JSON 格式,并发送 PUT 请求。
|
||||
/// </summary>
|
||||
/// <param name="dto">包含摄像头新配置信息的 DTO 对象。</param>
|
||||
/// <returns>
|
||||
/// 一个异步任务,其结果为一个布尔值:
|
||||
/// - <c>true</c>:表示更新成功(HTTP 响应为 200 OK 或其他成功状态码)。
|
||||
/// - <c>false</c>:表示更新失败(如 DTO 为 null、网络错误、服务器返回错误等)。
|
||||
/// </returns>
|
||||
public async Task<bool> UpdateCameraAsync(CameraEditInfo dto, string pageName)
|
||||
{
|
||||
// 1. 防御性检查:确保传入的 DTO 对象不为 null
|
||||
if (dto == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. 从全局数据中获取当前使用的服务节点信息
|
||||
var serviceNode = AppGlobal.UseServiceNode;
|
||||
if (serviceNode == null)
|
||||
{
|
||||
// 如果没有配置服务节点,则无法获取信息
|
||||
return false;
|
||||
}
|
||||
|
||||
var ipAddress = serviceNode.ServiceNodeIp;
|
||||
var port = serviceNode.ServiceNodePort;
|
||||
|
||||
|
||||
// 2. 拼接 PUT 请求的 URL
|
||||
// URL 格式: http://{ip}:{port}/api/Cameras/{id}
|
||||
string requestUrl = $"http://{ipAddress}:{port}/api/Cameras/{dto.Id}";
|
||||
|
||||
try
|
||||
{
|
||||
// 3. 将 DTO 对象手动映射为 API 所需的 JSON 字符串
|
||||
// 这一步至关重要,因为它处理了特殊的字段转换,例如:
|
||||
// 将 DTO 中的 `Brand` (int) 转换为 JSON 中的 `"brand": "HikVision"` (string)。
|
||||
string jsonPayload = MapDtoToEditJson(dto);
|
||||
|
||||
// 4. 调用 WebApiService 发送 PUT 请求
|
||||
// 注意:请确保您的 `WebApiService` 类中存在 `PutAsync` 方法。
|
||||
// 如果不存在,可以考虑添加一个,或者在某些 RESTful 设计中,也可以使用 `PostAsync` 代替。
|
||||
await WebApiService.Instance.PutAsync(requestUrl, jsonPayload, pageName);
|
||||
|
||||
// 5. 如果代码执行到这里,说明请求成功且没有抛出异常
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 6. 捕获所有可能的异常(网络错误、服务器错误、序列化错误等)
|
||||
System.Diagnostics.Debug.WriteLine($"[Repository] 更新摄像头配置失败 (ID: {dto.Id}): {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateCameraAsync 新增单个摄像头
|
||||
|
||||
/// <summary>
|
||||
/// [新增] 创建一个新的摄像头配置。
|
||||
/// 此方法负责将 <see cref="CameraEditInfo"/> 对象转换为 API 所需的 JSON 格式,并发送 POST 请求到摄像头资源集合的根路径。
|
||||
/// </summary>
|
||||
/// <param name="nodeIp">目标服务器节点的 IP 地址。</param>
|
||||
/// <param name="nodePort">目标服务器节点的端口号。</param>
|
||||
/// <param name="dto">包含新摄像头配置信息的 DTO 对象。</param>
|
||||
/// <returns>
|
||||
/// 一个异步任务,其结果为一个布尔值:
|
||||
/// - <c>true</c>:表示创建成功(HTTP 响应为 201 Created 或其他成功状态码)。
|
||||
/// - <c>false</c>:表示创建失败(如 DTO 为 null、网络错误、服务器返回错误等)。
|
||||
/// </returns>
|
||||
public async Task<bool> CreateCameraAsync(CameraEditInfo dto, string pageName)
|
||||
{
|
||||
// 1. 防御性检查:确保传入的 DTO 对象不为 null
|
||||
if (dto == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. 从全局数据中获取当前使用的服务节点信息
|
||||
var serviceNode = AppGlobal.UseServiceNode;
|
||||
if (serviceNode == null)
|
||||
{
|
||||
// 如果没有配置服务节点,则无法获取信息
|
||||
return false;
|
||||
}
|
||||
|
||||
var ipAddress = serviceNode.ServiceNodeIp;
|
||||
var port = serviceNode.ServiceNodePort;
|
||||
|
||||
// 2. 拼接 POST 请求的 URL
|
||||
// URL 格式: http://{ip}:{port}/api/Cameras
|
||||
// 注意:创建新资源通常是 POST 到资源集合的根路径,而不是单个资源的路径。
|
||||
// 此时 DTO 中的 `Id` 字段通常应为 0 或默认值,由服务器在创建时生成新的唯一 ID。
|
||||
string requestUrl = $"http://{ipAddress}:{port}/api/Cameras";
|
||||
|
||||
try
|
||||
{
|
||||
// 3. 将 DTO 对象手动映射为 API 所需的 JSON 字符串
|
||||
// 复用与更新操作相同的映射逻辑,确保数据格式的一致性。
|
||||
string jsonPayload = MapDtoToEditJson(dto);
|
||||
|
||||
// 4. 调用 WebApiService 发送 POST 请求
|
||||
await WebApiService.Instance.PostAsync(requestUrl, jsonPayload, pageName);
|
||||
|
||||
// 5. 如果代码执行到这里,说明请求成功且没有抛出异常
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 6. 捕获所有可能的异常(网络错误、服务器错误、序列化错误等)
|
||||
System.Diagnostics.Debug.WriteLine($"[Repository] 创建新摄像头失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// [新增] 删除摄像头
|
||||
/// </summary>
|
||||
public async Task<bool> DeleteCameraAsync(long cameraId)
|
||||
{
|
||||
// 1. 从全局数据中获取当前使用的服务节点信息
|
||||
var serviceNode = AppGlobal.UseServiceNode;
|
||||
if (serviceNode == null)
|
||||
{
|
||||
// 如果没有配置服务节点,则无法获取信息
|
||||
return false;
|
||||
}
|
||||
|
||||
var ipAddress = serviceNode.ServiceNodeIp;
|
||||
var port = serviceNode.ServiceNodePort;
|
||||
|
||||
// URL: http://{ip}:{port}/api/Cameras/{id}
|
||||
string url = $"http://{ipAddress}:{port}/api/Cameras/{cameraId}";
|
||||
|
||||
try
|
||||
{
|
||||
// 发送 DELETE 请求
|
||||
await WebApiService.Instance.DeleteAsync(url, "DeleteCamera");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[Repository] 删除失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#region MapJsonToEditDto
|
||||
|
||||
/// <summary>
|
||||
/// 私有辅助方法:手动解析 JSON 字符串并映射到 <see cref="CameraEditInfo"/> DTO。
|
||||
/// 此方法能精确处理类型转换和提供默认值,避免自动反序列化时因类型不匹配而失败。
|
||||
/// </summary>
|
||||
/// <param name="json">从 API 获取的原始 JSON 字符串。</param>
|
||||
/// <returns>一个填充了数据的 <see cref="CameraEditInfo"/> 对象。即使解析失败,也会返回一个对象实例。</returns>
|
||||
private CameraEditInfo MapJsonToEditDto(string json)
|
||||
{
|
||||
// 初始化一个 DTO 对象,用于存储映射后的数据
|
||||
var cameraDto = new CameraEditInfo();
|
||||
|
||||
try
|
||||
{
|
||||
// 使用 System.Text.Json 的 JsonDocument 进行高性能的只读解析
|
||||
using (JsonDocument doc = JsonDocument.Parse(json))
|
||||
{
|
||||
JsonElement root = doc.RootElement;
|
||||
|
||||
// --- 基础字段映射 ---
|
||||
// 使用 TryGetProperty 安全地获取属性,避免因字段不存在而抛出异常
|
||||
if (root.TryGetProperty("id", out var idElement)) cameraDto.Id = idElement.GetInt64();
|
||||
if (root.TryGetProperty("name", out var nameElement)) cameraDto.Name = nameElement.GetString() ?? string.Empty;
|
||||
if (root.TryGetProperty("ipAddress", out var ipElement)) cameraDto.IpAddress = ipElement.GetString();
|
||||
if (root.TryGetProperty("username", out var userElement)) cameraDto.Username = userElement.GetString();
|
||||
if (root.TryGetProperty("password", out var passElement)) cameraDto.Password = passElement.GetString();
|
||||
if (root.TryGetProperty("channelIndex", out var chElement)) cameraDto.ChannelIndex = chElement.GetInt32();
|
||||
if (root.TryGetProperty("rtspPath", out var rtspElement)) cameraDto.RtspPath = rtspElement.GetString();
|
||||
if (root.TryGetProperty("mainboardIp", out var mainIpElement)) cameraDto.MainboardIp = mainIpElement.GetString();
|
||||
if (root.TryGetProperty("mainboardPort", out var mainPortElement)) cameraDto.MainboardPort = mainPortElement.GetInt32();
|
||||
if (root.TryGetProperty("streamType", out var streamElement)) cameraDto.StreamType = streamElement.GetInt32();
|
||||
|
||||
// --- 类型转换 ---
|
||||
// 将 JSON 中的 int 类型端口号转换为 DTO 中的 ushort 类型
|
||||
if (root.TryGetProperty("port", out var portElement)) cameraDto.Port = (ushort)portElement.GetInt32();
|
||||
|
||||
// --- 特殊逻辑处理 ---
|
||||
// 将 JSON 中的品牌字符串(如 "HikVision")转换为 DTO 中的整数枚举
|
||||
if (root.TryGetProperty("brand", out var brandElement))
|
||||
{
|
||||
cameraDto.Brand = ParseBrandToEditInt(brandElement.GetString());
|
||||
}
|
||||
|
||||
// --- 设置默认值 ---
|
||||
// 为未从 API 获取或有特定默认值的字段设置初始值
|
||||
cameraDto.Location = string.Empty;
|
||||
cameraDto.UseGrayscale = false;
|
||||
cameraDto.EnhanceImage = true;
|
||||
cameraDto.AllowCompress = true;
|
||||
cameraDto.TargetResolution = string.Empty;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 捕获 JSON 解析或映射过程中的任何异常
|
||||
System.Diagnostics.Debug.WriteLine($"[Repository] JSON 解析或映射到 DTO 时发生异常: {ex.Message}");
|
||||
// 即使发生异常,也返回一个对象实例,而不是 null,以防止上层代码出现空引用异常
|
||||
}
|
||||
|
||||
return cameraDto;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ParseBrandToEditInt
|
||||
|
||||
/// <summary>
|
||||
/// [读] API 字符串 -> DTO 枚举值 (int)
|
||||
/// 将 WebAPI 返回的 "HikVision" 等字符串解析为 DeviceBrand 枚举对应的 int
|
||||
/// </summary>
|
||||
private int ParseBrandToEditInt(string brandApiString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(brandApiString))
|
||||
return (int)DeviceBrand.Unknown;
|
||||
|
||||
// 统一转小写进行匹配,防止大小写差异
|
||||
var lowerName = brandApiString.Trim().ToLower();
|
||||
|
||||
// 1. 尝试直接通过枚举名解析 (例如 API 返回 "HikVision", 枚举也是 HikVision)
|
||||
// 这样如果 API 返回 "Usb",且枚举也有 "Usb",就能自动匹配
|
||||
if (Enum.TryParse(brandApiString, true, out DeviceBrand result))
|
||||
{
|
||||
return (int)result;
|
||||
}
|
||||
|
||||
return (int)DeviceBrand.Unknown;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ConvertBrandToEditString
|
||||
|
||||
/// <summary>
|
||||
/// [写] DTO 枚举值 (int) -> API 字符串
|
||||
/// 保存时,将 int 转换为 WebAPI 需要的字符串标识
|
||||
/// </summary>
|
||||
private string ConvertBrandToEditString(int brandValue)
|
||||
{
|
||||
// 将 int 强转为枚举,方便 switch
|
||||
var brand = (DeviceBrand)brandValue;
|
||||
|
||||
switch (brand)
|
||||
{
|
||||
case DeviceBrand.HikVision:
|
||||
return "HikVision"; // API 期望的字符串
|
||||
|
||||
case DeviceBrand.Dahua:
|
||||
return "Dahua";
|
||||
|
||||
case DeviceBrand.RtspGeneral:
|
||||
return "RTSP"; // 假设 API 期望全大写
|
||||
|
||||
case DeviceBrand.Usb:
|
||||
return "Usb";
|
||||
|
||||
case DeviceBrand.WebSocketShine:
|
||||
return "WebSocket"; // 需确认 API 期望什么
|
||||
|
||||
case DeviceBrand.File:
|
||||
return "File";
|
||||
|
||||
case DeviceBrand.OnvifGeneral:
|
||||
return "Onvif";
|
||||
|
||||
case DeviceBrand.Unknown:
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region MapDtoToEditJson
|
||||
|
||||
/// <summary>
|
||||
/// 辅助方法:将 DTO 转换为 API 需要的 JSON 字符串
|
||||
/// </summary>
|
||||
private string MapDtoToEditJson(CameraEditInfo dto)
|
||||
{
|
||||
// 根据后端约定的最新格式构建匿名对象
|
||||
// 特点:
|
||||
// 1. brand 直接传 int
|
||||
// 2. 扩展参数(useGrayscale等) 直接放在根节点,不需要 vendorArguments 包裹
|
||||
var apiModel = new
|
||||
{
|
||||
id = dto.Id,
|
||||
name = dto.Name ?? "",
|
||||
|
||||
// ★ 修正1:直接使用 int 值,不再转换成 "HikVision" 字符串
|
||||
brand = dto.Brand,
|
||||
|
||||
location = dto.Location ?? "",
|
||||
ipAddress = dto.IpAddress,
|
||||
port = dto.Port,
|
||||
username = dto.Username ?? "",
|
||||
password = dto.Password ?? "",
|
||||
|
||||
// ★ 修正2:根据样例,renderHandle 传 0 (通常这是运行时句柄,保存时无意义)
|
||||
renderHandle = 0,
|
||||
|
||||
channelIndex = dto.ChannelIndex,
|
||||
rtspPath = dto.RtspPath ?? "",
|
||||
mainboardIp = dto.MainboardIp ?? "",
|
||||
mainboardPort = dto.MainboardPort,
|
||||
streamType = dto.StreamType,
|
||||
|
||||
// ★ 修正3:扁平化处理,直接放在根层级
|
||||
useGrayscale = dto.UseGrayscale,
|
||||
enhanceImage = dto.EnhanceImage,
|
||||
allowCompress = dto.AllowCompress,
|
||||
allowExpand = dto.AllowExpand,
|
||||
targetResolution = dto.TargetResolution ?? "",
|
||||
|
||||
allowShrink = dto.AllowShrink,
|
||||
allowEnlarge = dto.AllowEnlarge,
|
||||
};
|
||||
|
||||
return JsonHelper.Serialize(apiModel);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public partial class CameraRepository
|
||||
{
|
||||
public async Task<bool> UpdateImageProcessingAsync(CameraEditInfo dto)
|
||||
{
|
||||
if (dto == null) return false;
|
||||
|
||||
var serviceNode = AppGlobal.UseServiceNode;
|
||||
if (serviceNode == null) return false;
|
||||
|
||||
// 1. 使用专用的 Processing 路由
|
||||
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Processing(dto.Id.ToString())}";
|
||||
|
||||
// 2. 解析分辨率字符串 (例如 "1920x1080" -> 1920, 1080)
|
||||
int width = 1280; // 默认值
|
||||
int height = 720;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(dto.TargetResolution))
|
||||
{
|
||||
var parts = dto.TargetResolution.ToLower().Split('x');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
int.TryParse(parts[0], out width);
|
||||
int.TryParse(parts[1], out height);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 2. 构建精简的专用 JSON Payload
|
||||
// 只包含图像处理相关的字段,不发送其他干扰信息
|
||||
var payload = new
|
||||
{
|
||||
// --- 分辨率 (拆分成 int) ---
|
||||
targetWidth = width,
|
||||
targetHeight = height,
|
||||
|
||||
// 确保这些字段名与后端 Processing 接口的定义一致
|
||||
useGrayscale = dto.UseGrayscale,
|
||||
enhanceImage = dto.EnhanceImage,
|
||||
|
||||
// 缩放控制
|
||||
allowShrink = dto.AllowShrink,
|
||||
allowEnlarge = dto.AllowEnlarge, // 这里使用你确认过的 AllowEnlarge
|
||||
|
||||
// 目标分辨率
|
||||
targetResolution = dto.TargetResolution ?? ""
|
||||
};
|
||||
|
||||
string jsonBody = JsonHelper.Serialize(payload);
|
||||
|
||||
// 3. 发送请求
|
||||
// 通常专用功能接口使用 POST 或 PUT。根据你的路由命名(Processing 是名词/动词),
|
||||
// 且是对资源的部分修改,PUT 的可能性较大;但如果是“执行处理”,也可能是 POST。
|
||||
// 建议先试 PUT (因为是 Update 操作),如果报 405 则改 POST。
|
||||
await WebApiService.Instance.PostAsync(requestUrl, jsonBody, "图像处理配置");
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"更新图像处理配置失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [新增] 获取指定设备的图像处理配置
|
||||
/// 对应后端: GET /api/cameras/{id}/processing
|
||||
/// </summary>
|
||||
public async Task<CameraEditInfo> GetImageProcessingAsync(long id)
|
||||
{
|
||||
var serviceNode = AppGlobal.UseServiceNode;
|
||||
if (serviceNode == null) return null;
|
||||
|
||||
// 1. 拼接 URL: http://.../api/Cameras/1001/processing
|
||||
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Root}/{id}/processing";
|
||||
|
||||
try
|
||||
{
|
||||
// 2. 发送 GET 请求
|
||||
string jsonResponse = await WebApiService.Instance.GetAsync(requestUrl, "获取图像处理配置");
|
||||
if (string.IsNullOrEmpty(jsonResponse)) return null;
|
||||
|
||||
// 3. 解析后端返回的 ProcessingOptions JSON
|
||||
// 注意:这里定义一个临时类来匹配后端 JSON 结构,确保字段名一致
|
||||
var options = JsonHelper.Deserialize<ProcessingOptionsDto>(jsonResponse);
|
||||
if (options == null) return null;
|
||||
|
||||
// 4. 映射回 CameraEditInfo 给 ViewModel 使用
|
||||
return new CameraEditInfo
|
||||
{
|
||||
Id = id,
|
||||
// 将宽高拼回 "1920x1080" 格式
|
||||
TargetResolution = $"{options.targetWidth}x{options.targetHeight}",
|
||||
|
||||
// 映射开关 (注意字段名对应关系)
|
||||
AllowShrink = options.enableShrink,
|
||||
AllowEnlarge = options.enableExpand, // 后端叫 EnableExpand
|
||||
EnhanceImage = options.enableBrightness
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[Repository] 获取图像配置失败: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// [内部类] 用于接收后端 JSON 的 DTO
|
||||
// 字段名需与后端 ProcessingOptions 序列化出来的 JSON 字段一致 (通常是 camelCase)
|
||||
private class ProcessingOptionsDto
|
||||
{
|
||||
public int targetWidth { get; set; }
|
||||
public int targetHeight { get; set; }
|
||||
public bool enableShrink { get; set; }
|
||||
public bool enableExpand { get; set; } // 对应后端的 EnableExpand
|
||||
public bool enableBrightness { get; set; } // 对应后端的 EnableBrightness
|
||||
public int brightness { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public partial class CameraRepository
|
||||
{
|
||||
// 文件: Services\WebApis\CameraReps\CameraRepository.cs
|
||||
|
||||
public async Task<bool> UpdateSubscriptionAsync(long cameraId, SubscriptionDto dto)
|
||||
{
|
||||
var serviceNode = AppGlobal.UseServiceNode;
|
||||
if (serviceNode == null) return false;
|
||||
|
||||
// URL: POST /api/Cameras/{id}/subscriptions
|
||||
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Root}/{cameraId}/subscriptions";
|
||||
|
||||
try
|
||||
{
|
||||
string jsonBody = JsonHelper.Serialize(dto);
|
||||
// 发送 POST 请求
|
||||
await WebApiService.Instance.PostAsync(requestUrl, jsonBody, "更新订阅配置");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"更新订阅失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [新增] 获取订阅列表
|
||||
/// </summary>
|
||||
public async Task<List<SubscriptionDto>> GetSubscriptionsAsync(long cameraId)
|
||||
{
|
||||
var serviceNode = AppGlobal.UseServiceNode;
|
||||
if (serviceNode == null) return new List<SubscriptionDto>();
|
||||
|
||||
// URL: GET /api/Cameras/{id}/subscriptions
|
||||
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Root}/{cameraId}/subscriptions";
|
||||
|
||||
try
|
||||
{
|
||||
string json = await WebApiService.Instance.GetAsync(requestUrl, "获取订阅列表");
|
||||
|
||||
// 如果返回空或null,返回空列表
|
||||
if (string.IsNullOrEmpty(json)) return new List<SubscriptionDto>();
|
||||
|
||||
var list = JsonHelper.Deserialize<List<SubscriptionDto>>(json);
|
||||
return list ?? new List<SubscriptionDto>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"获取订阅列表失败: {ex.Message}");
|
||||
return new List<SubscriptionDto>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [修改] 删除/注销订阅
|
||||
/// 改为标准的 DELETE 请求
|
||||
/// </summary>
|
||||
public async Task<bool> DeleteSubscriptionAsync(long cameraId, string appId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(appId)) return false;
|
||||
|
||||
var serviceNode = AppGlobal.UseServiceNode;
|
||||
if (serviceNode == null) return false;
|
||||
|
||||
// 拼接 URL: DELETE /api/Cameras/1001/subscriptions/Client_01
|
||||
// 注意:AppId 如果包含特殊字符,建议 UrlEncode,但一般 ID 都是字母数字
|
||||
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Root}/{cameraId}/subscriptions/{appId}";
|
||||
|
||||
try
|
||||
{
|
||||
// 调用刚刚在 WebApiService 里加的 DeleteAsync
|
||||
await WebApiService.Instance.DeleteAsync(requestUrl, "注销订阅");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"注销订阅失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 视频流订阅配置请求对象
|
||||
/// 用于定义第三方应用或内部模块对指定相机流的消费需求
|
||||
/// </summary>
|
||||
public class SubscriptionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 进程唯一标识 (如 "AI_Process_01"、"Main_Display_02")
|
||||
/// </summary>
|
||||
public string AppId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅业务类型。
|
||||
/// 决定了后端流控引擎后续的资源分配(如是否开启录像机或渲染器)。
|
||||
/// </summary>
|
||||
public int Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示帧率需求 (单位: fps)
|
||||
/// <para>不需要显示则设为 0,控制器会自动注销该类型需求</para>
|
||||
/// </summary>
|
||||
public int DisplayFps
|
||||
{
|
||||
get => TargetFps;
|
||||
set => TargetFps = value;
|
||||
}
|
||||
|
||||
public int TargetFps { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// [新增] 实际显示/分发帧率
|
||||
/// JSON: "realFps"
|
||||
/// </summary>
|
||||
[JsonPropertyName("realFps")]
|
||||
public double RealFps { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注信息。
|
||||
/// 用于记录订阅的用途、申请人或关联业务系统。
|
||||
/// </summary>
|
||||
public string Memo { get; set; }
|
||||
= string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 窗口句柄(HWND)。
|
||||
/// 仅在 Type 为 HandleDisplay 时必填。格式通常为十六进制或十进制字符串。
|
||||
/// </summary>
|
||||
public string Handle { get; set; }
|
||||
= string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 录像持续时长(分钟,范围 1-60)。
|
||||
/// 仅在 Type 为 LocalRecord 时有效。
|
||||
/// </summary>
|
||||
public int RecordDuration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 录像文件存放绝对路径。
|
||||
/// 仅在 Type 为 LocalRecord 时有效,例如:C:\Recordings\Room01。
|
||||
/// </summary>
|
||||
public string SavePath { get; set; }
|
||||
= string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 通讯方式协议。
|
||||
/// 仅在 Type 为 NetworkTrans 或 WebPush 时有效,默认为 Network。
|
||||
/// </summary>
|
||||
public int Protocol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标接收端 IP 地址。
|
||||
/// 仅在 Type 为 NetworkTrans 或 WebPush 且 Protocol 为 Network 时必填。
|
||||
/// </summary>
|
||||
public string TargetIp { get; set; }
|
||||
= string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 目标接收端端口号。
|
||||
/// 仅在 Type 为 NetworkTrans 或 WebPush 时必填。
|
||||
/// </summary>
|
||||
public int TargetPort { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订阅业务类型枚举
|
||||
/// 描述视频流的最终去向和业务用途,用于帧分发策略的路由决策
|
||||
/// </summary>
|
||||
public enum SubscriptionType
|
||||
{
|
||||
/// <summary>
|
||||
/// 本地窗口渲染
|
||||
/// <para>直接在服务器端显示器绘制(如 OpenCV Window、WinForm 控件)</para>
|
||||
/// </summary>
|
||||
[Description("本地窗口显示")]
|
||||
LocalWindow = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 本地录像存储
|
||||
/// <para>写入磁盘文件(如 MP4/AVI 格式,支持定时切割、循环覆盖)</para>
|
||||
/// </summary>
|
||||
[Description("本地录像存储")]
|
||||
LocalRecord = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 句柄绑定显示
|
||||
/// <para>渲染到指定 HWND 窗口句柄(如 SDK 硬件解码渲染到客户端控件)</para>
|
||||
/// </summary>
|
||||
[Description("句柄绑定显示")]
|
||||
HandleDisplay = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 自定义网络传输
|
||||
/// <para>通过私有协议转发给第三方系统(如工控机、告警服务器)</para>
|
||||
/// </summary>
|
||||
[Description("自定义网络传输")]
|
||||
NetworkTrans = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 网页端推流
|
||||
/// <para>转码为 Web 标准协议(如 WebRTC、HLS、RTMP)供浏览器播放</para>
|
||||
/// </summary>
|
||||
[Description("网页端推流")]
|
||||
WebPush = 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 网络传输协议类型
|
||||
/// </summary>
|
||||
public enum TransportProtocol
|
||||
{
|
||||
/// <summary> 可靠传输 (默认) </summary>
|
||||
Tcp = 0,
|
||||
|
||||
/// <summary> 快速传输 (可能丢包/花屏) </summary>
|
||||
Udp = 1,
|
||||
|
||||
/// <summary> 组播 (节省带宽) </summary>
|
||||
Multicast = 2,
|
||||
|
||||
/// <summary> 内存交互 </summary>
|
||||
Memory = 99,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
public partial class CameraRepository
|
||||
{
|
||||
#region PtzControlAsync
|
||||
|
||||
/// <summary>
|
||||
/// 发送 PTZ 控制指令
|
||||
/// </summary>
|
||||
public async Task<bool> PtzControlAsync(long cameraId, PtzControlDto payload)
|
||||
{
|
||||
var serviceNode = AppGlobal.UseServiceNode;
|
||||
if (serviceNode == null) return false;
|
||||
|
||||
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Ptz(cameraId.ToString())}";
|
||||
|
||||
try
|
||||
{
|
||||
string json = JsonHelper.Serialize(payload);
|
||||
await WebApiService.Instance.PostAsync(requestUrl, json, "PTZ控制");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"PTZ控制失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region 运维指令 (Maintenance)
|
||||
|
||||
/// <summary>
|
||||
/// [修正] 发送校时指令
|
||||
/// 修正点:后端 [FromBody] DateTime 需要接收带双引号的标准 ISO 时间字符串
|
||||
/// </summary>
|
||||
public async Task<bool> SyncTimeAsync(long cameraId)
|
||||
{
|
||||
var serviceNode = AppGlobal.UseServiceNode;
|
||||
if (serviceNode == null) return false;
|
||||
|
||||
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Time(cameraId.ToString())}";
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 获取当前时间,格式化为标准 ISO 8601 (yyyy-MM-ddTHH:mm:ss)
|
||||
string timeStr = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss");
|
||||
|
||||
// 2. [关键] 手动构造 JSON Body,必须包含双引号
|
||||
// 例如发送的内容应该是: "2025-01-01T12:00:00"
|
||||
string jsonBody = $"\"{timeStr}\"";
|
||||
|
||||
// 3. 发送请求
|
||||
// 确保你的 WebApiService.PostAsync 会设置 Content-Type: application/json
|
||||
await WebApiService.Instance.PostAsync(requestUrl, jsonBody, "设备校时");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"校时失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送远程重启指令
|
||||
/// 接口定义: [HttpPost("{id}/reboot")] public async Task<IActionResult> RebootDevice(long id)
|
||||
/// 该接口没有 [FromBody] 参数,Body 可以为空或空 JSON
|
||||
/// </summary>
|
||||
public async Task<bool> RebootCameraAsync(long cameraId)
|
||||
{
|
||||
var serviceNode = AppGlobal.UseServiceNode;
|
||||
if (serviceNode == null) return false;
|
||||
|
||||
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Reboot(cameraId.ToString())}";
|
||||
|
||||
try
|
||||
{
|
||||
await WebApiService.Instance.PostAsync(requestUrl, "{}", "远程重启");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"重启失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
139
SHH.CameraDashboard/Services/WebApis/Models/CameraEditInfo.cs
Normal file
139
SHH.CameraDashboard/Services/WebApis/Models/CameraEditInfo.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
|
||||
// ==============================================================================
|
||||
// 1. 物理与运行配置 DTO (对应 CRUD 操作)
|
||||
// 用于设备新增/全量配置查询,包含基础身份、连接信息、运行参数等全量字段
|
||||
// ==============================================================================
|
||||
public class CameraEditInfo
|
||||
{
|
||||
// --- 基础身份 (Identity) ---
|
||||
/// <summary>
|
||||
/// 设备唯一标识
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "设备ID不能为空")]
|
||||
[Range(1, long.MaxValue, ErrorMessage = "设备ID必须为正整数")]
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 设备友好名称
|
||||
/// </summary>
|
||||
[MaxLength(64, ErrorMessage = "设备名称长度不能超过64个字符")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 摄像头品牌类型 (0:HikVision, 1:Dahua, 2:RTSP...)
|
||||
/// </summary>
|
||||
[Range(0, 10, ErrorMessage = "品牌类型值必须在0-10范围内")]
|
||||
public int Brand { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 设备安装位置描述
|
||||
/// </summary>
|
||||
[MaxLength(128, ErrorMessage = "安装位置长度不能超过128个字符")]
|
||||
public string Location { get; set; } = string.Empty;
|
||||
|
||||
// --- 核心连接 (Connectivity) - 修改此类参数触发冷重启 ---
|
||||
/// <summary>
|
||||
/// 摄像头IP地址
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "IP地址不能为空")]
|
||||
[RegularExpression(@"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$",
|
||||
ErrorMessage = "请输入合法的IPv4地址")]
|
||||
public string IpAddress { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// SDK端口 (如海康默认8000)
|
||||
/// </summary>
|
||||
[Range(1, 65535, ErrorMessage = "端口号必须在1-65535范围内")]
|
||||
public ushort Port { get; set; } = 8000;
|
||||
|
||||
/// <summary>
|
||||
/// 登录用户名
|
||||
/// </summary>
|
||||
[MaxLength(32, ErrorMessage = "用户名长度不能超过32个字符")]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 登录密码
|
||||
/// </summary>
|
||||
[MaxLength(64, ErrorMessage = "密码长度不能超过64个字符")]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
public long RenderHandle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 通道号 (通常为1)
|
||||
/// </summary>
|
||||
[Range(1, 32, ErrorMessage = "通道号必须在1-32范围内")]
|
||||
public int ChannelIndex { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// RTSP流路径 (备用或非SDK模式使用)
|
||||
/// </summary>
|
||||
[MaxLength(256, ErrorMessage = "RTSP地址长度不能超过256个字符")]
|
||||
public string RtspPath { get; set; } = string.Empty;
|
||||
|
||||
// --- 主板关联信息 (Metadata) ---
|
||||
/// <summary>
|
||||
/// 关联主板IP地址
|
||||
/// </summary>
|
||||
[RegularExpression(@"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)?$",
|
||||
ErrorMessage = "请输入合法的IPv4地址")]
|
||||
public string MainboardIp { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 关联主板端口
|
||||
/// </summary>
|
||||
[Range(1, 65535, ErrorMessage = "主板端口号必须在1-65535范围内")]
|
||||
public int MainboardPort { get; set; } = 80;
|
||||
|
||||
// --- 运行时参数 (Runtime Options) - 支持热更新 ---
|
||||
/// <summary>
|
||||
/// 码流类型 (0:主码流, 1:子码流)
|
||||
/// </summary>
|
||||
[Range(0, 1, ErrorMessage = "码流类型只能是0(主码流)或1(子码流)")]
|
||||
public int StreamType { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用灰度图 (用于AI分析场景加速)
|
||||
/// </summary>
|
||||
public bool UseGrayscale { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用图像增强 (去噪/锐化等)
|
||||
/// </summary>
|
||||
public bool EnhanceImage { get; set; } = true;
|
||||
|
||||
// --- 画面变换 (Transform) - 支持热更新 ---
|
||||
/// <summary>
|
||||
/// 是否允许图像压缩 (降低带宽占用)
|
||||
/// </summary>
|
||||
public bool AllowCompress { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许图像放大 (提升渲染质量)
|
||||
/// </summary>
|
||||
public bool AllowExpand { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许图像缩小
|
||||
/// </summary>
|
||||
public bool AllowShrink { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许放大
|
||||
/// </summary>
|
||||
public bool AllowEnlarge { get; set; } = false;
|
||||
|
||||
// 给个默认值,比如 0 或 20
|
||||
public int Brightness { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 目标分辨率 (格式如 1920x1080,空则保持原图)
|
||||
/// </summary>
|
||||
[RegularExpression(@"^\d+x\d+$", ErrorMessage = "分辨率格式必须为 宽度x高度 (如 1920x1080)")]
|
||||
public string TargetResolution { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace SHH.CameraDashboard
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// 视频源物理/逻辑品牌类型
|
||||
@@ -9,6 +11,7 @@
|
||||
/// <summary>
|
||||
/// 未知
|
||||
/// </summary>
|
||||
[Description("未知")]
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
@@ -16,48 +19,55 @@
|
||||
/// 技术路径:基于海康私有 SDK (HCNetSDK.dll / PlayCtrl.dll)。
|
||||
/// 特性:支持全功能控制(PTZ、对讲、配置、报警回传)。
|
||||
/// </summary>
|
||||
HikVision,
|
||||
[Description("海康威视")]
|
||||
HikVision = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 大华 (Dahua)
|
||||
/// 技术路径:基于大华私有 SDK (dhnetsdk.dll / dhplay.dll)。
|
||||
/// 特性:支持全功能控制,与海康私有协议不兼容。
|
||||
/// </summary>
|
||||
Dahua,
|
||||
[Description("大华")]
|
||||
Dahua = 2,
|
||||
|
||||
/// <summary>
|
||||
/// USB 摄像头 / 虚拟摄像头
|
||||
/// 技术路径:基于 DirectShow 或 Windows Media Foundation。
|
||||
/// 特性:通常通过 OpenCV (VideoCapture) 或 DirectShowLib 直接读取本地硬件引用。
|
||||
/// </summary>
|
||||
Usb,
|
||||
[Description("USB")]
|
||||
Usb = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 标准 RTSP 流媒体
|
||||
/// 技术路径:基于标准 RTSP/RTP 协议 (RFC 2326)。
|
||||
/// 特性:跨品牌兼容,通常使用 FFmpeg 或 GStreamer 库取流,仅支持音视频,不支持云台控制。
|
||||
/// </summary>
|
||||
RtspGeneral,
|
||||
[Description("RTSP")]
|
||||
RtspGeneral = 4,
|
||||
|
||||
/// <summary>
|
||||
/// 三恒自研 WebSocket 流
|
||||
/// 技术路径:基于 WebSocket 传输的自定义二进制或 Base64 帧。
|
||||
/// 特性:专用于 Web 或云端推送场景的私有流媒体格式。
|
||||
/// </summary>
|
||||
WebSocketShine,
|
||||
[Description("三恒WebSocket")]
|
||||
WebSocketShine = 5,
|
||||
|
||||
/// <summary>
|
||||
/// 本地视频文件
|
||||
/// 技术路径:基于文件 IO 的离线解码。
|
||||
/// 特性:常用于算法演示、回放模拟,支持 Mp4, Avi, Mkv 等容器格式。
|
||||
/// </summary>
|
||||
File,
|
||||
[Description("文件")]
|
||||
File = 6,
|
||||
|
||||
/// <summary>
|
||||
/// 未知/通用标准 (ONVIF)
|
||||
/// 技术路径:基于标准 ONVIF WebService。
|
||||
/// 特性:用于接入非主流厂商但符合 ONVIF 标准的设备,支持基础 PTZ。
|
||||
/// </summary>
|
||||
OnvifGeneral
|
||||
[Description("通用标准")]
|
||||
OnvifGeneral = 7
|
||||
}
|
||||
}
|
||||
20
SHH.CameraDashboard/Services/WebApis/Models/PtzAction.cs
Normal file
20
SHH.CameraDashboard/Services/WebApis/Models/PtzAction.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
/// <summary>
|
||||
/// PTZ 控制动作枚举
|
||||
/// </summary>
|
||||
public enum PtzAction
|
||||
{
|
||||
Stop = 0, // 停止动作
|
||||
Up, // 向上
|
||||
Down, // 向下
|
||||
Left, // 向左
|
||||
Right, // 向右
|
||||
ZoomIn, // 变倍+ (放大)
|
||||
ZoomOut, // 变倍- (缩小)
|
||||
FocusNear, // 聚焦近
|
||||
FocusFar, // 聚焦远
|
||||
IrisOpen, // 光圈大
|
||||
IrisClose, // 光圈小
|
||||
Wiper // 雨刷
|
||||
}
|
||||
20
SHH.CameraDashboard/Services/WebApis/Models/PtzControlDto.cs
Normal file
20
SHH.CameraDashboard/Services/WebApis/Models/PtzControlDto.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
public class PtzControlDto
|
||||
{
|
||||
// 确保将枚举序列化为字符串 (如 "Up") 而不是数字
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public PtzAction Action { get; set; }
|
||||
|
||||
// 是否是停止指令 (true=停止当前动作)
|
||||
public bool Stop { get; set; }
|
||||
|
||||
// 速度 (1-10)
|
||||
public int Speed { get; set; } = 5;
|
||||
|
||||
// 持续时间 (毫秒),用于点动模式 (如雨刷)
|
||||
public int Duration { get; set; } = 0;
|
||||
}
|
||||
181
SHH.CameraDashboard/Services/WebApis/Models/WebApiCameraModel.cs
Normal file
181
SHH.CameraDashboard/Services/WebApis/Models/WebApiCameraModel.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
/// <summary>
|
||||
/// 代表一个网络摄像头的模型,用于在 UI 上显示和监控其状态。
|
||||
/// 实现了 <see cref="INotifyPropertyChanged"/> 接口,当属性值改变时可以通知视图进行更新。
|
||||
/// </summary>
|
||||
public class WebApiCameraModel : INotifyPropertyChanged
|
||||
{
|
||||
#region --- INotifyPropertyChanged 实现 ---
|
||||
|
||||
/// <summary>
|
||||
/// 当属性值更改时发生。
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 引发 <see cref="PropertyChanged"/> 事件。
|
||||
/// </summary>
|
||||
/// <param name="propertyName">已更改的属性名称。如果未提供,则使用调用方成员的名称。</param>
|
||||
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置属性值的通用方法。只有当值发生变化时,才会更新字段并通知属性更改。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">属性的类型。</typeparam>
|
||||
/// <param name="field">对存储属性值的字段的引用。</param>
|
||||
/// <param name="value">属性的新值。</param>
|
||||
/// <param name="propertyName">属性的名称。</param>
|
||||
/// <returns>如果值已更改,则返回 <c>true</c>;否则返回 <c>false</c>。</returns>
|
||||
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (Equals(field, value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 摄像头属性 ---
|
||||
|
||||
private int _id;
|
||||
/// <summary>
|
||||
/// 获取或设置摄像头的唯一标识符。
|
||||
/// </summary>
|
||||
public int Id
|
||||
{
|
||||
get => _id;
|
||||
set => SetProperty(ref _id, value);
|
||||
}
|
||||
|
||||
private string _name = string.Empty;
|
||||
/// <summary>
|
||||
/// 获取或设置摄像头的自定义名称。
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value);
|
||||
}
|
||||
|
||||
private string _ipAddress = string.Empty;
|
||||
/// <summary>
|
||||
/// 获取或设置摄像头的 IP 地址。
|
||||
/// </summary>
|
||||
public string IpAddress
|
||||
{
|
||||
get => _ipAddress;
|
||||
set => SetProperty(ref _ipAddress, value);
|
||||
}
|
||||
|
||||
private string _brand = string.Empty;
|
||||
/// <summary>
|
||||
/// 获取或设置摄像头的品牌。
|
||||
/// </summary>
|
||||
public string Brand
|
||||
{
|
||||
get => _brand;
|
||||
set => SetProperty(ref _brand, value);
|
||||
}
|
||||
|
||||
private string _status = string.Empty;
|
||||
/// <summary>
|
||||
/// 获取或设置摄像头的综合状态描述(例如:"在线", "离线", "连接中")。
|
||||
/// </summary>
|
||||
public string Status
|
||||
{
|
||||
get => _status;
|
||||
set => SetProperty(ref _status, value);
|
||||
}
|
||||
|
||||
private bool _isOnline;
|
||||
/// <summary>
|
||||
/// 获取或设置一个值,该值指示摄像头的逻辑连接状态(API 是否可达)。
|
||||
/// </summary>
|
||||
public bool IsOnline
|
||||
{
|
||||
get => _isOnline;
|
||||
set => SetProperty(ref _isOnline, value);
|
||||
}
|
||||
|
||||
private bool _isPhysicalOnline;
|
||||
/// <summary>
|
||||
/// 获取或设置一个值,该值指示摄像头的物理连接状态(例如:通过 ICMP ping 检测)。
|
||||
/// </summary>
|
||||
public bool IsPhysicalOnline
|
||||
{
|
||||
get => _isPhysicalOnline;
|
||||
set => SetProperty(ref _isPhysicalOnline, value);
|
||||
}
|
||||
|
||||
private bool _isRunning;
|
||||
/// <summary>
|
||||
/// 获取或设置一个值,该值指示摄像头的流媒体服务是否正在运行。
|
||||
/// </summary>
|
||||
public bool IsRunning
|
||||
{
|
||||
get => _isRunning;
|
||||
set => SetProperty(ref _isRunning, value);
|
||||
}
|
||||
|
||||
private int _width;
|
||||
/// <summary>
|
||||
/// 获取或设置摄像头视频流的宽度(分辨率)。
|
||||
/// </summary>
|
||||
public int Width
|
||||
{
|
||||
get => _width;
|
||||
set => SetProperty(ref _width, value);
|
||||
}
|
||||
|
||||
private int _height;
|
||||
/// <summary>
|
||||
/// 获取或设置摄像头视频流的高度(分辨率)。
|
||||
/// </summary>
|
||||
public int Height
|
||||
{
|
||||
get => _height;
|
||||
set => SetProperty(ref _height, value);
|
||||
}
|
||||
|
||||
private int _realFps;
|
||||
/// <summary>
|
||||
/// 获取或设置摄像头当前的实际帧率(FPS)。
|
||||
/// </summary>
|
||||
public int RealFps
|
||||
{
|
||||
get => _realFps;
|
||||
set => SetProperty(ref _realFps, value);
|
||||
}
|
||||
|
||||
private long _totalFrames;
|
||||
/// <summary>
|
||||
/// 获取或设置自启动以来收到的总帧数。
|
||||
/// </summary>
|
||||
public long TotalFrames
|
||||
{
|
||||
get => _totalFrames;
|
||||
set => SetProperty(ref _totalFrames, value);
|
||||
}
|
||||
|
||||
private int _streamType;
|
||||
/// <summary>
|
||||
/// 获取或设置视频流的类型(例如:0 = 主码流, 1 = 子码流)。
|
||||
/// </summary>
|
||||
public int StreamType
|
||||
{
|
||||
get => _streamType;
|
||||
set => SetProperty(ref _streamType, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
38
SHH.CameraDashboard/Services/WebApis/MonitorRepository.cs
Normal file
38
SHH.CameraDashboard/Services/WebApis/MonitorRepository.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public class MonitorRepository
|
||||
{
|
||||
private const string MODULE = "MonitorAPI";
|
||||
public async Task<MonitorWebApiData> GetDashboardDataAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = await WebApiService.Instance.GetAsync(WebApiRoutes.Monitor.Dashboard, MODULE);
|
||||
|
||||
// 1. 反序列化
|
||||
var data = JsonHelper.Deserialize<MonitorWebApiData>(json);
|
||||
|
||||
// 2. 【核心修复】空合并运算符
|
||||
// 如果 data 为 null,立即 new 一个空对象返回
|
||||
return data ?? new MonitorWebApiData();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 3. 【兜底】发生任何异常(断网/解析错误),返回空对象
|
||||
// 保证 ViewModel 拿到永远是实例
|
||||
return new MonitorWebApiData();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetSnapshotUrlAsync(string id)
|
||||
{
|
||||
// 假设返回的是图片 URL 或者是 Base64
|
||||
return await WebApiService.Instance.GetAsync(WebApiRoutes.Monitor.Snapshot(id), MODULE);
|
||||
}
|
||||
}
|
||||
|
||||
public class MonitorWebApiData
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
34
SHH.CameraDashboard/Services/WebApis/Routes.cs
Normal file
34
SHH.CameraDashboard/Services/WebApis/Routes.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public static class WebApiRoutes
|
||||
{
|
||||
// 摄像头模块
|
||||
public static class Cameras
|
||||
{
|
||||
public const string Root = "/api/Cameras";
|
||||
public static string ById(string id) => $"{Root}/{id}";
|
||||
public static string BindHandle(string id) => $"{Root}/{id}/bind-handle";
|
||||
public static string Power(string id) => $"{Root}/{id}/power";
|
||||
public static string Options(string id) => $"{Root}/{id}/options";
|
||||
public static string Subscriptions(string id) => $"{Root}/{id}/subscriptions";
|
||||
public static string Logs(string id) => $"{Root}/{id}/logs";
|
||||
public static string Processing(string id) => $"{Root}/{id}/processing";
|
||||
public static string Time(string id) => $"{Root}/{id}/time";
|
||||
public static string Reboot(string id) => $"{Root}/{id}/reboot";
|
||||
public static string Ptz(string id) => $"{Root}/{id}/ptz";
|
||||
}
|
||||
|
||||
// 监控模块
|
||||
public static class Monitor
|
||||
{
|
||||
public const string Root = "/api/Monitor";
|
||||
public const string All = $"{Root}/all";
|
||||
public const string Dashboard = $"{Root}/dashboard";
|
||||
public const string SystemLogs = $"{Root}/system-logs";
|
||||
public const string UpdateProcessing = $"{Root}/update-processing";
|
||||
public static string ById(string id) => $"{Root}/{id}";
|
||||
public static string Snapshot(string id) => $"{Root}/snapshot/{id}";
|
||||
public static string Diagnose(string id) => $"{Root}/diagnose/{id}";
|
||||
}
|
||||
}
|
||||
}
|
||||
299
SHH.CameraDashboard/Services/WebApis/WebApiService.cs
Normal file
299
SHH.CameraDashboard/Services/WebApis/WebApiService.cs
Normal file
@@ -0,0 +1,299 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局 Web API 服务
|
||||
/// 职责:
|
||||
/// 1. 统一管理 HttpClient 实例,避免资源耗尽。
|
||||
/// 2. 封装标准的 CRUD (GET, POST, PUT, DELETE, PATCH) 操作。
|
||||
/// 3. 提供详细的请求日志(耗时、状态码、请求/响应内容)。
|
||||
/// 4. 处理超时和异常,提供一致的错误处理机制。
|
||||
/// </summary>
|
||||
public class WebApiService
|
||||
{
|
||||
#region --- 静态成员 ---
|
||||
|
||||
/// <summary>
|
||||
/// WebAPI 设为单例
|
||||
/// </summary>
|
||||
public static WebApiService Instance { get; } = new WebApiService();
|
||||
|
||||
/// <summary>
|
||||
/// 静态 HttpClient 实例。
|
||||
/// 最佳实践:应用程序应共享单个 HttpClient 实例以避免套接字耗尽。
|
||||
/// </summary>
|
||||
private static readonly HttpClient _client;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 实例成员 ---
|
||||
|
||||
/// <summary>
|
||||
/// 当一个请求完成(无论成功或失败)时发生。
|
||||
/// 可用于记录详细的 API 调用日志。
|
||||
/// </summary>
|
||||
public event Action<LogWebApiModel>? OnRequestCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// 日志内容的最大长度(5KB),防止过大的响应体撑爆内存。
|
||||
/// </summary>
|
||||
private const int MAX_LOG_LENGTH = 5120;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 构造函数 ---
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
private WebApiService()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 静态构造函数,用于初始化 HttpClient。
|
||||
/// </summary>
|
||||
static WebApiService()
|
||||
{
|
||||
// 配置 HttpClient 处理器
|
||||
var handler = new SocketsHttpHandler
|
||||
{
|
||||
// 设置连接池寿命为 15 分钟。
|
||||
// 这有助于解决 DNS 变更后,HttpClient 仍使用旧 IP 的问题。
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(15)
|
||||
};
|
||||
|
||||
_client = new HttpClient(handler)
|
||||
{
|
||||
// 设置默认请求超时时间为 15 秒。
|
||||
Timeout = TimeSpan.FromSeconds(15)
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 核心 CRUD 方法 ---
|
||||
|
||||
/// <summary>
|
||||
/// 发送一个 GET 请求。
|
||||
/// </summary>
|
||||
/// <param name="url">请求的完整 URL。</param>
|
||||
/// <param name="moduleName">发起请求的模块名称,用于日志分类。</param>
|
||||
/// <param name="token">用于取消操作的令牌。</param>
|
||||
/// <returns>成功时返回响应内容的字符串表示。</returns>
|
||||
public async Task<string> GetAsync(string url, string moduleName = "System", CancellationToken token = default, bool isAutoPost = false)
|
||||
{
|
||||
return await ExecuteRequestAsync(url, "GET", null, moduleName, token, async () =>
|
||||
{
|
||||
return await _client.GetAsync(url, token);
|
||||
}, isAutoPost);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送一个 POST 请求。
|
||||
/// </summary>
|
||||
/// <param name="url">请求的完整 URL。</param>
|
||||
/// <param name="jsonBody">请求体的 JSON 字符串。</param>
|
||||
/// <param name="moduleName">发起请求的模块名称,用于日志分类。</param>
|
||||
/// <param name="token">用于取消操作的令牌。</param>
|
||||
/// <returns>成功时返回响应内容的字符串表示。</returns>
|
||||
public async Task<string> PostAsync(string url, string jsonBody, string moduleName = "System", CancellationToken token = default)
|
||||
{
|
||||
return await ExecuteRequestAsync(url, "POST", jsonBody, moduleName, token, async () =>
|
||||
{
|
||||
using var content = CreateJsonContent(jsonBody);
|
||||
return await _client.PostAsync(url, content, token);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送一个 PUT 请求(通常用于替换整个资源)。
|
||||
/// </summary>
|
||||
/// <param name="url">请求的完整 URL。</param>
|
||||
/// <param name="jsonBody">请求体的 JSON 字符串。</param>
|
||||
/// <param name="moduleName">发起请求的模块名称,用于日志分类。</param>
|
||||
/// <param name="token">用于取消操作的令牌。</param>
|
||||
/// <returns>成功时返回响应内容的字符串表示。</returns>
|
||||
public async Task<string> PutAsync(string url, string jsonBody, string moduleName = "System", CancellationToken token = default)
|
||||
{
|
||||
return await ExecuteRequestAsync(url, "PUT", jsonBody, moduleName, token, async () =>
|
||||
{
|
||||
using var content = CreateJsonContent(jsonBody);
|
||||
return await _client.PutAsync(url, content, token);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送一个 PATCH 请求(通常用于更新资源的部分属性)。
|
||||
/// </summary>
|
||||
/// <param name="url">请求的完整 URL。</param>
|
||||
/// <param name="jsonBody">请求体的 JSON 字符串。</param>
|
||||
/// <param name="moduleName">发起请求的模块名称,用于日志分类。</param>
|
||||
/// <param name="token">用于取消操作的令牌。</param>
|
||||
/// <returns>成功时返回响应内容的字符串表示。</returns>
|
||||
public async Task<string> PatchAsync(string url, string jsonBody, string moduleName = "System", CancellationToken token = default)
|
||||
{
|
||||
return await ExecuteRequestAsync(url, "PATCH", jsonBody, moduleName, token, async () =>
|
||||
{
|
||||
using var content = CreateJsonContent(jsonBody);
|
||||
return await _client.PatchAsync(url, content, token);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送一个 DELETE 请求。
|
||||
/// </summary>
|
||||
/// <param name="url">请求的完整 URL。</param>
|
||||
/// <param name="moduleName">发起请求的模块名称,用于日志分类。</param>
|
||||
/// <param name="token">用于取消操作的令牌。</param>
|
||||
/// <returns>成功时返回响应内容的字符串表示。</returns>
|
||||
public async Task<string> DeleteAsync(string url, string moduleName = "System", CancellationToken token = default)
|
||||
{
|
||||
return await ExecuteRequestAsync(url, "DELETE", null, moduleName, token, async () =>
|
||||
{
|
||||
return await _client.DeleteAsync(url, token);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 私有辅助方法 ---
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个 UTF-8 编码的 JSON 内容对象。
|
||||
/// </summary>
|
||||
private StringContent CreateJsonContent(string json)
|
||||
{
|
||||
return new StringContent(json ?? "", Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 核心执行引擎,封装了所有 HTTP 请求的通用逻辑:计时、日志、异常处理。
|
||||
/// </summary>
|
||||
private async Task<string> ExecuteRequestAsync(
|
||||
string url,
|
||||
string method,
|
||||
string? requestData,
|
||||
string moduleName,
|
||||
CancellationToken token,
|
||||
Func<Task<HttpResponseMessage>> action,
|
||||
bool isAutoPost = false)
|
||||
{
|
||||
if (requestData == null)
|
||||
requestData = "(请求数据为空)";
|
||||
|
||||
var log = new LogWebApiModel
|
||||
{
|
||||
Time = DateTime.Now,
|
||||
Method = method,
|
||||
Url = url,
|
||||
RequestData = TruncateLog(requestData),
|
||||
AppModule = moduleName,
|
||||
IsAutoPost = isAutoPost,
|
||||
};
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// 执行具体的 HTTP 请求
|
||||
using var response = await action();
|
||||
|
||||
// 读取响应内容
|
||||
var responseString = await response.Content.ReadAsStringAsync(token);
|
||||
|
||||
// 更新日志
|
||||
log.StatusCode = ((int)response.StatusCode).ToString();
|
||||
log.ResponseData = TruncateLog(responseString);
|
||||
|
||||
// 如果 HTTP 状态码不是 2xx,则抛出异常
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return responseString;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 捕获所有异常,更新日志状态为 "Error"
|
||||
log.StatusCode = "Error";
|
||||
log.ResponseData = $"Client Exception: {ex.Message}";
|
||||
|
||||
// 重新抛出异常,让调用者知道请求失败
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 无论成功或失败,都记录耗时并触发日志事件
|
||||
sw.Stop();
|
||||
log.ElapsedMilliseconds = sw.ElapsedMilliseconds;
|
||||
|
||||
// 安全地触发事件,防止订阅者抛出异常影响主线程
|
||||
try
|
||||
{
|
||||
OnRequestCompleted?.Invoke(log);
|
||||
}
|
||||
catch (Exception logEx)
|
||||
{
|
||||
// 记录日志系统自身的错误,但不中断程序
|
||||
Debug.WriteLine($"Log System Error: {logEx.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 截断过长的日志内容,并尝试格式化 JSON 以便阅读。
|
||||
/// </summary>
|
||||
private string TruncateLog(string? content)
|
||||
{
|
||||
if (string.IsNullOrEmpty(content)) return string.Empty;
|
||||
|
||||
// 1. 【新增步骤】尝试将内容格式化为漂亮的 JSON
|
||||
// 注意:格式化会增加空格和换行,导致长度变长,这是符合预期的
|
||||
string finalContent = TryFormatJson(content);
|
||||
|
||||
// 2. 截断逻辑 (基于格式化后的长度)
|
||||
if (finalContent.Length > MAX_LOG_LENGTH)
|
||||
{
|
||||
// 保留前 N 个字符,并在末尾添加提示
|
||||
return finalContent.Substring(0, MAX_LOG_LENGTH) +
|
||||
$"\r\n\r\n... [日志过长已截断,显示前 {MAX_LOG_LENGTH} 字符]";
|
||||
}
|
||||
|
||||
return finalContent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试将字符串解析并格式化为缩进的 JSON。
|
||||
/// 如果解析失败(说明不是 JSON),则原样返回。
|
||||
/// </summary>
|
||||
private string TryFormatJson(string content)
|
||||
{
|
||||
// 简单的性能优化:如果不像 JSON(不以 { 或 [ 开头),直接跳过
|
||||
var trimmed = content.Trim();
|
||||
if ((trimmed.StartsWith("{") && trimmed.EndsWith("}")) ||
|
||||
(trimmed.StartsWith("[") && trimmed.EndsWith("]")))
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用 JToken 解析,它可以处理 Object {} 和 Array []
|
||||
var token = JToken.Parse(content);
|
||||
|
||||
// 格式化输出 (Indented = 缩进模式)
|
||||
return token.ToString(Formatting.Indented);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 解析失败(可能只是普通的文本消息,或者 HTML),忽略异常,返回原样
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,19 @@
|
||||
<Color x:Key="Color.Bg.L3">#2D2D30</Color>
|
||||
<Color x:Key="Color.Bg.L4">#3E3E42</Color>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Action.Time">
|
||||
M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Action.Reboot">
|
||||
M12,5V1L7,6L12,11V7A5,5 0 0,1 17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12H5A7,7 0 0,0 12,19A7,7 0 0,0 19,12A7,7 0 0,0 12,5Z
|
||||
</StreamGeometry>
|
||||
|
||||
<SolidColorBrush x:Key="Brush.State.Info" Color="#3B82F6" />
|
||||
<SolidColorBrush x:Key="Brush.State.Success" Color="#10B981" />
|
||||
<SolidColorBrush x:Key="Brush.State.Warning" Color="#FFA940" />
|
||||
<SolidColorBrush x:Key="Brush.State.Danger" Color="#FF7875" />
|
||||
|
||||
<SolidColorBrush x:Key="Brush.Bg.L1" Color="{StaticResource Color.Bg.L1}" />
|
||||
<SolidColorBrush x:Key="Brush.Bg.L2" Color="{StaticResource Color.Bg.L2}" />
|
||||
<SolidColorBrush x:Key="Brush.Bg.L3" Color="{StaticResource Color.Bg.L3}" />
|
||||
|
||||
133
SHH.CameraDashboard/Style/Themes/Icons.xaml
Normal file
133
SHH.CameraDashboard/Style/Themes/Icons.xaml
Normal file
@@ -0,0 +1,133 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<StreamGeometry x:Key="Icon.Wizard">
|
||||
M19.14,12.94c0.46-0.45,0.46-1.18,0-1.64l-4.24-4.24c-0.45-0.46-1.18-0.46-1.64,0c-0.46,0.45-0.46,1.18,0,1.64
|
||||
l1.8,1.8l-9.5,9.5l-2.73-2.73l9.5-9.5l1.8,1.8c0.45,0.46,1.18,0.46,1.64,0c0.46-0.45,0.46-1.18,0-1.64L11.53,3.69
|
||||
c-0.45-0.46-1.18-0.46-1.64,0s-0.46,1.18,0,1.64l1.8,1.8l-9.5,9.5c-0.63,0.63-0.63,1.65,0,2.28c0.63,0.63,1.65,0.63,2.28,0
|
||||
l9.5-9.5l1.8,1.8c0.45,0.46,1.18,0.46,1.64,0C17.87,10.74,17.87,10.01,17.41,9.55l-1.8-1.8l1.8-1.8
|
||||
C17.87,5.5,18.6,5.5,19.06,5.95l1.06,1.06l-1.64,1.64C18.02,9.11,18.02,9.84,18.48,10.3l1.97,1.97
|
||||
C20.91,12.73,21.64,12.73,22.1,12.27l1.64-1.64l1.06,1.06c0.45,0.46,0.45,1.18,0,1.64l-4.24,4.24
|
||||
c-0.45,0.46-1.18,0.46-1.64,0c-0.46-0.45-0.46-1.18,0-1.64L19.14,12.94z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Theme">
|
||||
M12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2C17.5,2 22,6.5 22,12A6,6 0 0,1 16,18C14.9,18 14,17.1 14,16V14.5C14,14.2 13.8,14 13.5,14C13.2,14 13,14.2 13,14.5V16A4,4 0 0,0 17,20C17.6,20 18.1,19.9 18.6,19.7C17.1,21.1 14.8,22 12,22M6.5,10A1.5,1.5 0 0,0 5,11.5A1.5,1.5 0 0,0 6.5,13A1.5,1.5 0 0,0 8,11.5A1.5,1.5 0 0,0 6.5,10M9.5,6A1.5,1.5 0 0,0 8,7.5A1.5,1.5 0 0,0 9.5,9A1.5,1.5 0 0,0 11,7.5A1.5,1.5 0 0,0 9.5,6M14.5,6A1.5,1.5 0 0,0 13,7.5A1.5,1.5 0 0,0 14.5,9A1.5,1.5 0 0,0 16,7.5A1.5,1.5 0 0,0 14.5,6M17.5,10A1.5,1.5 0 0,0 16,11.5A1.5,1.5 0 0,0 17.5,13A1.5,1.5 0 0,0 19,11.5A1.5,1.5 0 0,0 17.5,10Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.ChevronDown">
|
||||
M7.41,8.59L12,13.17L16.59,8.59L18,10L12,16L6,10L7.41,8.59Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.ChevronUp">
|
||||
M7.41,15.41L12,10.83L16.59,15.41L18,14L12,8L6,14L7.41,15.41Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Trash">
|
||||
M9,3L8,4H4V6H20V4H16L15,3H9M18,19A2,2 0 0,1 16,21H8A2,2 0 0,1 6,19V7H18V19M8,9V19H10V9H8M14,9V19H16V9H14Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Pin.Off">
|
||||
M16,9V19H8V9H16M14.5,3H9.5L8.5,4H5V6H19V4H15.5L14.5,3M18,7H6V19C6,20.1 6.9,21 8,21H16C17.1,21 18,20.1 18,19V7Z
|
||||
</StreamGeometry>
|
||||
<StreamGeometry x:Key="Icon.Pin.Unfixed">
|
||||
M2,5.27L3.28,4L20,20.72L18.73,22L15.65,18.92L14.5,17.77V22H12.9V16.17L10.5,13.77L10.25,14H9.5L5.75,17.75H4.25V16.25L8,12.5V11.75L2,5.75V5.27M16.5,9.75L13,6.25L13.5,5.75L12.5,4.75L11,6.25L6.25,1.5L13.5,1.5L14.5,2.5L15.5,1.5L22.5,8.5L21.5,9.5L22.5,10.5L16.5,9.75Z
|
||||
</StreamGeometry>
|
||||
<StreamGeometry x:Key="Icon.Pin.Outline">
|
||||
M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12M8.83,14H15.17L14,12.83V4H10V12.83L8.83,14Z
|
||||
</StreamGeometry>
|
||||
<StreamGeometry x:Key="Icon.Pin.Fixed">
|
||||
M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Action.Play">
|
||||
M8,5.14V19.14L19,12.14L8,5.14Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Action.Pause">
|
||||
M14,19H18V5H14M6,19H10V5H6V19Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Status.WifiOff">
|
||||
M23.64,7C23.14,6.6 18.72,3 12,3C5.28,3 0.86,6.6 0.36,7L12,21.5L18.78,13.06C19.81,13.56 20.61,14.47 20.95,15.58L23.64,7M19,16C17.34,16 16,17.34 16,19C16,20.66 17.34,22 19,22C20.66,22 22,20.66 22,19C22,17.34 20.66,16 19,16M19,20C18.45,20 18,19.55 18,19C18,18.45 18.45,18 19,18C19.55,18 20,18.45 20,19C20,19.55 19.55,20 19,20Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Status.Loading">
|
||||
M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Action.Edit">
|
||||
M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Refresh">
|
||||
M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Plus">
|
||||
M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Ptz">
|
||||
M12,2L16,6H13V10H17V7L21,11L17,15V12H13V16H16L12,20L8,16H11V12H7V15L3,11L7,7V10H11V6H8L12,2Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Lens.Zoom">
|
||||
M15.5,14L20.5,19L19,20.5L14,15.5V14.71L13.73,14.43C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.43,13.73L14.71,14H15.5M9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14M12,10H10V12H9V10H7V9H9V7H10V9H12V10Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Lens.Focus">
|
||||
M6,6H10V4H6A2,2 0 0,0 4,6V10H6V6M14,4V6H18V10H20V6A2,2 0 0,0 18,4H14M18,20H14V18H18V14H20V18A2,2 0 0,0 18,20M6,18H10V20H6A2,2 0 0,0 4,18V14H6V18M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Lens.Iris">
|
||||
M12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22M12,4A8,8 0 0,0 4,12C4,13.85 4.63,15.55 5.68,16.91L13.67,14.19L16.22,7.62C15.06,5.39 12.77,4 12,4M15.21,17.47L9.7,18.7L7.05,10.61C7.81,13.11 9.92,15.93 12.92,17.18C13.71,17.5 14.47,17.61 15.21,17.47M17.65,15.39L16.79,9.45L8.71,9.08C9.57,6.85 11.75,5.19 14.47,5.03C16.85,4.88 19.16,6.31 20.16,8.5C20.42,9.08 20.59,9.72 20.62,10.39C20.69,11.85 20.18,13.25 19.34,14.39C18.88,15.03 18.28,15.41 17.65,15.39Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Lens.Wiper">
|
||||
M15,11L9.65,16.35C8.82,17.18 7.5,17.23 6.62,16.5C5.74,15.77 5.74,14.43 6.5,13.5L13.13,5.65C13.88,4.77 15.22,4.72 16.1,5.5C16.97,6.23 16.97,7.57 16.21,8.5L15,11M2.38,17.58C1.21,18.75 1.21,20.65 2.38,21.82C3.55,23 5.45,23 6.62,21.82L13.5,14.94L9.26,10.7L2.38,17.58Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Minus">
|
||||
M19,13H5V11H19V13Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Focus.Far">
|
||||
M14,6L10.25,11L13.1,14.8L11.5,16C9.81,13.75 7,10 7,10L1,18H23L14,6Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Focus.Near">
|
||||
M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M15,12A3,3 0 1,1 12,9A3,3 0 0,1 15,12Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Iris.Small">
|
||||
M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Iris.Large">
|
||||
M12,6A6,6 0 0,1 18,12A6,6 0 0,1 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Action.StopSmall">
|
||||
M6,6H18V18H6V6Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Action.PlaySmall">
|
||||
M8,5.14V19.14L19,12.14L8,5.14Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Action.Process">
|
||||
M7.5,5.6L10,7L8.6,4.5L10,2L7.5,3.4L5,2L6.4,4.5L5,7L7.5,5.6M19.5,8L22,9.4L20.6,6.9L22,4.4L19.5,5.8L17,4.4L18.4,6.9L17,9.4L19.5,8M19.5,16L22,17.4L20.6,14.9L22,12.4L19.5,13.8L17,12.4L18.4,14.9L17,17.4L19.5,16M13,5.83L14.88,8.7L18.7,9.5L15.39,12.28L16.27,15.93L13,13.78L9.73,15.93L10.61,12.28L7.3,9.5L11.12,8.7L13,5.83Z
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="Icon.Action.Subscribe">
|
||||
M6.18,15.64A2.18,2.18 0 0,1 8.36,17.82C8.36,19 7.38,20 6.18,20C5,20 4,19 4,17.82A2.18,2.18 0 0,1 6.18,15.64M4,4.44A15.56,15.56 0 0,1 19.56,20H16.73A12.73,12.73 0 0,0 4,7.27V4.44M4,10.1A9.9,9.9 0 0,1 13.9,20H11.07A7.07,7.07 0 0,0 4,12.93V10.1Z
|
||||
</StreamGeometry>
|
||||
|
||||
<Style x:Key="Style.IconPath" TargetType="Path">
|
||||
<Setter Property="Stretch" Value="Uniform" />
|
||||
<Setter Property="Fill" Value="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Control}}" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -1,4 +1,458 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:SHH.CameraDashboard">
|
||||
|
||||
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
|
||||
|
||||
<!-- 主界面 - 系统按钮 -->
|
||||
<Style x:Key="Btn.TitleBar" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Primary}" />
|
||||
<Setter Property="Width" Value="46" />
|
||||
<Setter Property="Height" Value="32" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
RecognizesAccessKey="True"
|
||||
SnapsToDevicePixels="False" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource Brush.Bg.Hover}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource Brush.Bg.L3}" />
|
||||
<Setter Property="Opacity" Value="0.8" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- 主界面 - 系统按钮 -->
|
||||
<Style
|
||||
x:Key="Btn.TitleBar.Close"
|
||||
BasedOn="{StaticResource Btn.TitleBar}"
|
||||
TargetType="Button">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource Brush.Status.Danger}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Inverse}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#99F44747" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Inverse}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- 按钮颜色 -->
|
||||
<Style x:Key="Btn.Icon.Base" TargetType="Button">
|
||||
<Setter Property="Width" Value="32" />
|
||||
<Setter Property="Height" Value="32" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="4">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
RecognizesAccessKey="True" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Opacity" Value="0.9" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Opacity" Value="0.7" />
|
||||
<Setter TargetName="border" Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
<ScaleTransform CenterX="16" CenterY="16" ScaleX="0.95" ScaleY="0.95" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Background" Value="#E0E0E0" />
|
||||
<Setter Property="Foreground" Value="#A0A0A0" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="Btn.Icon.Success"
|
||||
BasedOn="{StaticResource Btn.Icon.Base}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.State.Success}" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="Btn.Icon.Danger"
|
||||
BasedOn="{StaticResource Btn.Icon.Base}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.State.Danger}" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="Btn.Icon.Warning"
|
||||
BasedOn="{StaticResource Btn.Icon.Base}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.State.Warning}" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="Btn.Icon.Info"
|
||||
BasedOn="{StaticResource Btn.Icon.Base}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.State.Info}" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Btn.Ghost.Base" TargetType="Button">
|
||||
<Setter Property="Width" Value="32" />
|
||||
<Setter Property="Height" Value="32" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="4">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
RecognizesAccessKey="True" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Bg.Hover}" />
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Opacity" Value="0.7" />
|
||||
<Setter TargetName="border" Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
<ScaleTransform CenterX="16" CenterY="16" ScaleX="0.95" ScaleY="0.95" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.3" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="Btn.Ghost.Secondary"
|
||||
BasedOn="{StaticResource Btn.Ghost.Base}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Secondary}" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="Btn.Ghost.Danger"
|
||||
BasedOn="{StaticResource Btn.Ghost.Base}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.State.Danger}" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="Btn.Ghost.Info"
|
||||
BasedOn="{StaticResource Btn.Ghost.Base}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.State.Info}" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="Btn.Ghost.Success"
|
||||
BasedOn="{StaticResource Btn.Ghost.Base}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.State.Success}" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DangerBtnStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="#DC3545" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Padding" Value="10,5" />
|
||||
<Setter Property="Margin" Value="2" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="4">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
RecognizesAccessKey="True" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="border" Property="Background" Value="#C82333" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="border" Property="Background" Value="#BD2130" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="border" Property="Background" Value="#E2E6EA" />
|
||||
<Setter Property="Foreground" Value="#6C757D" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- TabItem 样式 -->
|
||||
<Style x:Key="Style.TabItem.Modern" TargetType="TabItem">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Secondary}" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="Padding" Value="16,10" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TabItem">
|
||||
<Grid
|
||||
x:Name="Root"
|
||||
Background="{TemplateBinding Background}"
|
||||
SnapsToDevicePixels="True">
|
||||
|
||||
<ContentPresenter
|
||||
x:Name="ContentSite"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
ContentSource="Header"
|
||||
RecognizesAccessKey="True" />
|
||||
|
||||
<Border
|
||||
x:Name="SelectionUnderline"
|
||||
Height="2"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{DynamicResource Brush.Brand}"
|
||||
Visibility="Hidden" />
|
||||
</Grid>
|
||||
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter TargetName="Root" Property="Background" Value="#1AFFFFFF" />
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Brand}" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter TargetName="SelectionUnderline" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Disable}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 窄边滚动条 -->
|
||||
<Style TargetType="ScrollBar">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
|
||||
<Setter Property="Width" Value="8" />
|
||||
<Setter Property="MinWidth" Value="8" />
|
||||
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ScrollBar">
|
||||
<Grid Background="Transparent">
|
||||
<Track x:Name="PART_Track" IsDirectionReversed="true">
|
||||
<Track.Thumb>
|
||||
<Thumb>
|
||||
<Thumb.Template>
|
||||
<ControlTemplate TargetType="Thumb">
|
||||
<Border
|
||||
Margin="2,0"
|
||||
Background="{DynamicResource Brush.Text.Disabled}"
|
||||
CornerRadius="2" />
|
||||
</ControlTemplate>
|
||||
</Thumb.Template>
|
||||
</Thumb>
|
||||
</Track.Thumb>
|
||||
</Track>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Opacity" Value="1" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="False">
|
||||
<Setter Property="Opacity" Value="0.6" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 滑块样式 -->
|
||||
<Style x:Key="Style.CheckBox.Switch" TargetType="CheckBox">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Bg.Input}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Padding" Value="10,0,0,0" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="CheckBox">
|
||||
<Grid Background="Transparent">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border
|
||||
x:Name="Track"
|
||||
Width="44"
|
||||
Height="24"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.6" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
|
||||
<Grid
|
||||
Width="44"
|
||||
Height="24"
|
||||
HorizontalAlignment="Left">
|
||||
<Ellipse
|
||||
x:Name="Knob"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="2,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
Fill="White"
|
||||
Stroke="#DDDDDD"
|
||||
StrokeThickness="0.5">
|
||||
<Ellipse.Effect>
|
||||
<DropShadowEffect
|
||||
BlurRadius="5"
|
||||
Direction="270"
|
||||
Opacity="0.3"
|
||||
ShadowDepth="1.5"
|
||||
Color="Black" />
|
||||
</Ellipse.Effect>
|
||||
<Ellipse.RenderTransform>
|
||||
<TranslateTransform x:Name="KnobTranslate" X="0" />
|
||||
</Ellipse.RenderTransform>
|
||||
</Ellipse>
|
||||
</Grid>
|
||||
|
||||
<ContentPresenter
|
||||
Grid.Column="1"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Content}"
|
||||
TextBlock.Foreground="{TemplateBinding Foreground}" />
|
||||
</Grid>
|
||||
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Track" Property="Background" Value="{DynamicResource Brush.State.Success}" />
|
||||
<Setter TargetName="Track" Property="BorderBrush" Value="{DynamicResource Brush.State.Success}" />
|
||||
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="KnobTranslate"
|
||||
Storyboard.TargetProperty="X"
|
||||
To="20"
|
||||
Duration="0:0:0.2">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.EnterActions>
|
||||
|
||||
<Trigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="KnobTranslate"
|
||||
Storyboard.TargetProperty="X"
|
||||
To="0"
|
||||
Duration="0:0:0.2">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.ExitActions>
|
||||
</Trigger>
|
||||
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsMouseOver" Value="True" />
|
||||
<Condition Property="IsChecked" Value="False" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="Track" Property="BorderBrush" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
</MultiTrigger>
|
||||
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
|
||||
@@ -105,38 +559,6 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ScrollBar">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Width" Value="10" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ScrollBar">
|
||||
<Grid Background="Transparent">
|
||||
<Track x:Name="PART_Track" IsDirectionReversed="true">
|
||||
<Track.Thumb>
|
||||
<Thumb>
|
||||
<Thumb.Template>
|
||||
<ControlTemplate TargetType="Thumb">
|
||||
<Border
|
||||
Margin="2,0"
|
||||
Background="{DynamicResource Brush.Text.Disabled}"
|
||||
CornerRadius="4" />
|
||||
</ControlTemplate>
|
||||
</Thumb.Template>
|
||||
</Thumb>
|
||||
</Track.Thumb>
|
||||
</Track>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Opacity" Value="1" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ListView">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
@@ -173,17 +595,34 @@
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style x:Key="Style.GridViewHeader.Flat" TargetType="GridViewColumnHeader">
|
||||
<Style TargetType="DataGrid">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="RowHeaderWidth" Value="0" />
|
||||
<Setter Property="GridLinesVisibility" Value="Horizontal" />
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource Brush.Border}" />
|
||||
<Setter Property="VerticalGridLinesBrush" Value="Transparent" />
|
||||
<Setter Property="AutoGenerateColumns" Value="False" />
|
||||
<Setter Property="CanUserAddRows" Value="False" />
|
||||
<Setter Property="CanUserResizeRows" Value="False" />
|
||||
<Setter Property="HeadersVisibility" Value="Column" />
|
||||
<Setter Property="RowHeight" Value="36" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Bg.Header}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Secondary}" />
|
||||
<Setter Property="Padding" Value="10,0" />
|
||||
<Setter Property="Height" Value="40" />
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border}" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Bg.Panel}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Secondary}" />
|
||||
<Setter Property="Padding" Value="10,8" />
|
||||
<Setter Property="BorderThickness" Value="0,0,1,1" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GridViewColumnHeader">
|
||||
<ControlTemplate TargetType="DataGridColumnHeader">
|
||||
<Border
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
@@ -197,45 +636,144 @@
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style x:Key="Style.ListViewItem.Table" TargetType="ListViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="Padding" Value="10,0" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridCell">
|
||||
<Border Background="Transparent" BorderThickness="0">
|
||||
<ContentPresenter Margin="{TemplateBinding Padding}" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Brand}" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="DataGridRow">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Bg.Hover}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Bg.Selected}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style x:Key="PrimaryBtnStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Brand}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Inverse}" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Padding" Value="15,6" />
|
||||
<Setter Property="Height" Value="32" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="4">
|
||||
<ContentPresenter
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
RecognizesAccessKey="True" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="border" Property="Opacity" Value="0.9" />
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="border" Property="Opacity" Value="0.8" />
|
||||
<Setter TargetName="border" Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
<ScaleTransform CenterX="0.5" CenterY="0.5" ScaleX="0.98" ScaleY="0.98" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter TargetName="border" Property="RenderTransformOrigin" Value="0.5,0.5" />
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="border" Property="Background" Value="{DynamicResource Brush.Bg.L4}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Disabled}" />
|
||||
<Setter Property="Opacity" Value="0.6" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Style.GridViewHeader.Flat" TargetType="GridViewColumnHeader">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Secondary}" />
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Bg.Panel}" />
|
||||
<Setter Property="Padding" Value="10,8" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GridViewColumnHeader">
|
||||
<Border
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<ContentPresenter
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
RecognizesAccessKey="True"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Style.ListViewItem.Table" TargetType="ListViewItem">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Padding" Value="5,0" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border}" />
|
||||
<Setter Property="MinHeight" Value="35" />
|
||||
<Setter Property="BorderBrush" Value="#1AFFFFFF" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListViewItem">
|
||||
<Border
|
||||
x:Name="Bd"
|
||||
Padding="4,0"
|
||||
Height="30"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<Grid>
|
||||
<Rectangle
|
||||
x:Name="AccentBar"
|
||||
Width="3"
|
||||
Margin="-4,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
Fill="{DynamicResource Brush.Accent}"
|
||||
Visibility="Collapsed" />
|
||||
<GridViewRowPresenter VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
|
||||
</Grid>
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
SnapsToDevicePixels="true">
|
||||
<GridViewRowPresenter VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Brush.Bg.Hover}" />
|
||||
<Trigger Property="IsMouseOver" Value="true">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#1AFFFFFF" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Brush.Bg.L4}" />
|
||||
<Setter TargetName="AccentBar" Property="Visibility" Value="Visible" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Accent}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Trigger Property="IsSelected" Value="true">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#33007ACC" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Brush.Brand}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
@@ -252,7 +790,7 @@
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="8,4" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Height" Value="30" />
|
||||
<Setter Property="Height" Value="36" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Style.TextBox.CodeEditor" TargetType="TextBox">
|
||||
|
||||
177
SHH.CameraDashboard/ViewModels/CameraEditViewModel.cs
Normal file
177
SHH.CameraDashboard/ViewModels/CameraEditViewModel.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public class CameraEditViewModel : INotifyPropertyChanged
|
||||
{
|
||||
// 编辑中的数据副本
|
||||
private CameraEditInfo _editingDto;
|
||||
|
||||
// 定义一个事件,通知 View 关闭窗口(或者 UserControl 所在的宿主)
|
||||
public event Action<bool, CameraEditInfo> RequestClose;
|
||||
|
||||
public CameraEditInfo EditingDto
|
||||
{
|
||||
get => _editingDto;
|
||||
set { _editingDto = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public ICommand SaveCommand { get; }
|
||||
public ICommand CancelCommand { get; }
|
||||
|
||||
private bool _isAdd = false;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="sourceDto">如果是编辑模式,传入源对象;如果是新增,传入 null</param>
|
||||
public CameraEditViewModel(CameraEditInfo sourceDto = null)
|
||||
{
|
||||
// 初始化枚举列表
|
||||
InitBrandOptions();
|
||||
|
||||
if (sourceDto != null)
|
||||
{
|
||||
// 编辑模式:深拷贝数据,避免直接修改源对象
|
||||
EditingDto = CloneDto(sourceDto);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 新增模式:创建默认对象
|
||||
EditingDto = new CameraEditInfo
|
||||
{
|
||||
Name = "新设备",
|
||||
Port = 8000,
|
||||
ChannelIndex = 1
|
||||
};
|
||||
}
|
||||
|
||||
if (EditingDto.Id == 0)
|
||||
_isAdd = true;
|
||||
|
||||
SaveCommand = new RelayCommand(ExecuteSave, CanSave);
|
||||
CancelCommand = new RelayCommand(ExecuteCancel);
|
||||
}
|
||||
|
||||
private bool CanSave(object obj)
|
||||
{
|
||||
// 简单验证:ID必须大于0,IP不能为空
|
||||
if (EditingDto == null) return false;
|
||||
return EditingDto.Id > 0 && !string.IsNullOrWhiteSpace(EditingDto.IpAddress);
|
||||
}
|
||||
|
||||
private bool IsSaving;
|
||||
|
||||
private async void ExecuteSave(object obj)
|
||||
{
|
||||
if (IsSaving) return;
|
||||
|
||||
try
|
||||
{
|
||||
IsSaving = true; // 开启 Loading
|
||||
|
||||
bool isSuccess = false;
|
||||
|
||||
if (_isAdd)
|
||||
{
|
||||
isSuccess = await ApiClient.Instance.Cameras.CreateCameraAsync(EditingDto, "摄像头-新增");
|
||||
}
|
||||
else
|
||||
{
|
||||
isSuccess = await ApiClient.Instance.Cameras.UpdateCameraAsync(EditingDto, "摄像头-编辑");
|
||||
_isAdd = false;
|
||||
}
|
||||
|
||||
// 1. 调用 Repository (ApiClient)
|
||||
// 注意:这里调用的是我们刚才在 Repository 中定义的 UpdateCameraAsync
|
||||
if (isSuccess)
|
||||
{
|
||||
// 2. 成功:通知外部关闭面板,并刷新列表
|
||||
RequestClose?.Invoke(true, EditingDto);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 3. 失败:弹窗提示
|
||||
MessageBox.Show("保存失败,请检查服务节点日志。", "系统提示", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"保存过程中发生异常:\n{ex.Message}", "系统错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSaving = false; // 关闭 Loading
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteCancel(object obj)
|
||||
{
|
||||
// false 表示取消操作
|
||||
RequestClose?.Invoke(false, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动深拷贝 DTO,防止引用传递导致界面未点保存就修改了列表
|
||||
/// 生产环境建议用 AutoMapper 或 JSON 序列化实现
|
||||
/// </summary>
|
||||
private CameraEditInfo CloneDto(CameraEditInfo source)
|
||||
{
|
||||
return new CameraEditInfo
|
||||
{
|
||||
Id = source.Id,
|
||||
Name = source.Name,
|
||||
Brand = source.Brand,
|
||||
Location = source.Location,
|
||||
IpAddress = source.IpAddress,
|
||||
Port = source.Port,
|
||||
Username = source.Username,
|
||||
Password = source.Password,
|
||||
RenderHandle = source.RenderHandle,
|
||||
ChannelIndex = source.ChannelIndex,
|
||||
RtspPath = source.RtspPath,
|
||||
MainboardIp = source.MainboardIp,
|
||||
MainboardPort = source.MainboardPort,
|
||||
StreamType = source.StreamType,
|
||||
UseGrayscale = source.UseGrayscale,
|
||||
EnhanceImage = source.EnhanceImage,
|
||||
AllowCompress = source.AllowCompress,
|
||||
AllowExpand = source.AllowExpand,
|
||||
TargetResolution = source.TargetResolution
|
||||
};
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
protected void OnPropertyChanged([CallerMemberName] string name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
|
||||
// 1. 定义一个简单的内部类或结构,用于 ComboBox 显示
|
||||
public class BrandOption
|
||||
{
|
||||
public string Label { get; set; } // 显示的文本 (Description)
|
||||
public int Value { get; set; } // 实际的值 (int)
|
||||
}
|
||||
|
||||
// 2. 数据源属性
|
||||
public List<BrandOption> BrandOptions { get; private set; }
|
||||
|
||||
private void InitBrandOptions()
|
||||
{
|
||||
// 遍历 DeviceBrand 枚举的所有值
|
||||
BrandOptions = Enum.GetValues(typeof(DeviceBrand))
|
||||
.Cast<DeviceBrand>()
|
||||
.Select(e => new BrandOption
|
||||
{
|
||||
// 获取 Description ("海康威视")
|
||||
Label = EnumHelper.GetDescription(e),
|
||||
// 获取 int 值 (1)
|
||||
Value = (int)e
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public class CameraImageSubscriptionViewModels : INotifyPropertyChanged
|
||||
{
|
||||
private readonly WebApiCameraModel _camera;
|
||||
|
||||
public event Action RequestClose;
|
||||
|
||||
// 用于绑定 ComboBox 的类型列表
|
||||
public Dictionary<int, string> SubscriptionTypes { get; } = new Dictionary<int, string>
|
||||
{
|
||||
{ 0, "本地窗口预览" },
|
||||
{ 1, "本地录像" },
|
||||
{ 2, "句柄渲染 (嵌入)" },
|
||||
{ 3, "网络转发 (TCP/UDP)" },
|
||||
{ 4, "Web 推流" }
|
||||
};
|
||||
|
||||
// --- 数据源 ---
|
||||
public ObservableCollection<SubscriptionDto> Subscriptions { get; set; } = new ObservableCollection<SubscriptionDto>();
|
||||
|
||||
// --- 编辑表单属性 ---
|
||||
|
||||
private string _editAppId;
|
||||
public string EditAppId
|
||||
{
|
||||
get => _editAppId;
|
||||
set { _editAppId = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private int _editType;
|
||||
public int EditType
|
||||
{
|
||||
get => _editType;
|
||||
set
|
||||
{
|
||||
_editType = value;
|
||||
OnPropertyChanged();
|
||||
// 切换类型时,控制 UI 上 IP/Port 输入框的显示
|
||||
OnPropertyChanged(nameof(IsNetworkType));
|
||||
OnPropertyChanged(nameof(IsRecordType));
|
||||
}
|
||||
}
|
||||
|
||||
private int _editFps = 25;
|
||||
public int EditFps
|
||||
{
|
||||
get => _editFps;
|
||||
set { _editFps = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private string _editMemo;
|
||||
public string EditMemo
|
||||
{
|
||||
get => _editMemo;
|
||||
set { _editMemo = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
// 网络参数
|
||||
private string _editTargetIp = "127.0.0.1";
|
||||
public string EditTargetIp
|
||||
{
|
||||
get => _editTargetIp;
|
||||
set { _editTargetIp = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private int _editTargetPort = 8080;
|
||||
public int EditTargetPort
|
||||
{
|
||||
get => _editTargetPort;
|
||||
set { _editTargetPort = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
// --- 界面显隐控制 ---
|
||||
// 只有类型是 3或4 时显示网络设置
|
||||
public bool IsNetworkType => EditType == 3 || EditType == 4;
|
||||
// 只有类型是 1 时显示录像设置 (这里简化,暂时只演示网络显隐)
|
||||
public bool IsRecordType => EditType == 1;
|
||||
|
||||
|
||||
// --- 命令 ---
|
||||
public ICommand LoadCommand { get; }
|
||||
public ICommand SaveCommand { get; }
|
||||
public ICommand DeleteCommand { get; }
|
||||
public ICommand ClearFormCommand { get; }
|
||||
|
||||
public CameraImageSubscriptionViewModels(WebApiCameraModel camera)
|
||||
{
|
||||
_camera = camera;
|
||||
|
||||
LoadCommand = new RelayCommand(ExecuteLoad);
|
||||
SaveCommand = new RelayCommand(ExecuteSave);
|
||||
DeleteCommand = new RelayCommand(ExecuteDelete);
|
||||
ClearFormCommand = new RelayCommand(obj => ResetForm());
|
||||
|
||||
// 初始化加载
|
||||
ExecuteLoad(null);
|
||||
}
|
||||
|
||||
private async void ExecuteLoad(object obj)
|
||||
{
|
||||
var list = await ApiClient.Instance.Cameras.GetSubscriptionsAsync(_camera.Id);
|
||||
Subscriptions.Clear();
|
||||
foreach (var item in list)
|
||||
{
|
||||
Subscriptions.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExecuteSave(object obj)
|
||||
{
|
||||
// 1. 基础校验
|
||||
if (string.IsNullOrWhiteSpace(EditAppId))
|
||||
{
|
||||
MessageBox.Show("AppId 不能为空", "提示");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 构建 DTO
|
||||
// 注意:这里需要根据你 DTO 的定义进行类型转换和默认值处理
|
||||
var dto = new SubscriptionDto
|
||||
{
|
||||
AppId = EditAppId,
|
||||
|
||||
// ★★★ 修改:直接赋值 int,不需要强转枚举 ★★★
|
||||
Type = EditType,
|
||||
|
||||
DisplayFps = EditFps,
|
||||
Memo = EditMemo ?? string.Empty,
|
||||
|
||||
// ... 网络参数 ...
|
||||
TargetIp = IsNetworkType ? (EditTargetIp ?? string.Empty) : string.Empty,
|
||||
TargetPort = IsNetworkType ? EditTargetPort : 0,
|
||||
|
||||
// ★★★ 修改:直接赋值 int ★★★
|
||||
Protocol = 0, // 0 代表 TCP
|
||||
|
||||
// ... 其他参数 ...
|
||||
Handle = string.Empty,
|
||||
SavePath = ((SubscriptionType)EditType == SubscriptionType.LocalRecord) ? @"C:\Temp\Video" : string.Empty,
|
||||
RecordDuration = 0
|
||||
};
|
||||
|
||||
// 3. 发送请求
|
||||
bool success = await ApiClient.Instance.Cameras.UpdateSubscriptionAsync(_camera.Id, dto);
|
||||
|
||||
if (success)
|
||||
{
|
||||
MessageBox.Show("保存成功", "提示");
|
||||
ExecuteLoad(null); // 刷新列表
|
||||
// ResetForm(); // 可选:保存后清空表单
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show("保存失败,请检查网络或参数。", "错误");
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExecuteDelete(object parameter)
|
||||
{
|
||||
// 支持两种方式删除:
|
||||
// 1. 传入 parameter (点击列表中的删除按钮)
|
||||
// 2. 根据当前表单的 AppId (如果在编辑模式)
|
||||
|
||||
string appIdToDelete = parameter as string;
|
||||
|
||||
if (string.IsNullOrEmpty(appIdToDelete)) return;
|
||||
|
||||
if (MessageBox.Show($"确定要停止并注销 [{appIdToDelete}] 吗?", "确认", MessageBoxButton.YesNo) != MessageBoxResult.Yes)
|
||||
return;
|
||||
|
||||
// 构造删除请求:关键是 DisplayFps = 0
|
||||
var dto = new SubscriptionDto
|
||||
{
|
||||
AppId = appIdToDelete,
|
||||
Type = 0,
|
||||
DisplayFps = 0, // <--- 核心:0 代表注销
|
||||
Memo = "User Deleted"
|
||||
};
|
||||
|
||||
bool success = await ApiClient.Instance.Cameras.UpdateSubscriptionAsync(_camera.Id, dto);
|
||||
if (success)
|
||||
{
|
||||
ExecuteLoad(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetForm()
|
||||
{
|
||||
// 生成一个随机 AppId 方便测试
|
||||
EditAppId = $"Client_{DateTime.Now:HHmmss}";
|
||||
EditType = 0;
|
||||
EditFps = 25;
|
||||
EditMemo = "";
|
||||
EditTargetIp = "127.0.0.1";
|
||||
EditTargetPort = 8080;
|
||||
}
|
||||
|
||||
// --- INotifyPropertyChanged 实现 (略,使用你的基类或标准实现) ---
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
protected void OnPropertyChanged([CallerMemberName] string name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
// 1. 添加一个字段
|
||||
private SubscriptionDto _selectedSubscription;
|
||||
|
||||
// 2. 添加 SelectedSubscription 属性
|
||||
public SubscriptionDto SelectedSubscription
|
||||
{
|
||||
get => _selectedSubscription;
|
||||
set
|
||||
{
|
||||
// 如果两次选中同一个,不做处理
|
||||
if (_selectedSubscription == value) return;
|
||||
|
||||
_selectedSubscription = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
// ★★★ 核心逻辑:一旦选中项发生变化,就填充表单 ★★★
|
||||
if (_selectedSubscription != null)
|
||||
{
|
||||
FillForm(_selectedSubscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 编写填充表单的辅助方法
|
||||
private void FillForm(SubscriptionDto dto)
|
||||
{
|
||||
// 这里我们将 DTO 的值“复制”到编辑属性中
|
||||
// 这样做的好处是:修改下方文本框时,不会立即改变列表里的显示,只有点击“保存”后才刷新
|
||||
EditAppId = dto.AppId;
|
||||
EditType = dto.Type; // 之前改成了 int,直接赋值即可
|
||||
EditFps = dto.DisplayFps; // 注意:后端返回可能是 targetFps,前端 DTO 映射已修复
|
||||
EditMemo = dto.Memo;
|
||||
|
||||
// 网络参数回显
|
||||
EditTargetIp = dto.TargetIp;
|
||||
EditTargetPort = dto.TargetPort;
|
||||
|
||||
// 触发一下 UI 状态刷新(比如网络参数的显隐)
|
||||
OnPropertyChanged(nameof(IsNetworkType));
|
||||
}
|
||||
}
|
||||
}
|
||||
369
SHH.CameraDashboard/ViewModels/CameraImgProcViewModel.cs
Normal file
369
SHH.CameraDashboard/ViewModels/CameraImgProcViewModel.cs
Normal file
@@ -0,0 +1,369 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public class CameraImgProcViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private readonly WebApiCameraModel _sourceCamera;
|
||||
private readonly CameraEditInfo _editInfo;
|
||||
|
||||
public event Action RequestClose;
|
||||
|
||||
public string Title => $"{_sourceCamera.Name} - 图像处理";
|
||||
public string SourceResolutionText => $"{_sourceCamera.Width} x {_sourceCamera.Height}";
|
||||
|
||||
// --- 状态控制属性 ---
|
||||
|
||||
// 源是否是高清 (>= 1080P)
|
||||
public bool IsSourceHighRes => _sourceCamera.Width >= 1920 || _sourceCamera.Height >= 1080;
|
||||
|
||||
// 是否允许勾选“允许放大” (UI 绑定 IsEnabled)
|
||||
public bool CanCheckExpand => !IsSourceHighRes;
|
||||
|
||||
// 滑块是否可用 (只有开启了缩小 或 开启了放大,才允许拖动)
|
||||
// ★★★ 修正:使用 AllowEnlarge ★★★
|
||||
public bool IsSliderEnabled => AllowShrink || AllowEnlarge;
|
||||
|
||||
// 允许缩小
|
||||
public bool AllowShrink
|
||||
{
|
||||
get => _editInfo.AllowShrink;
|
||||
set
|
||||
{
|
||||
if (_editInfo.AllowShrink != value)
|
||||
{
|
||||
_editInfo.AllowShrink = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsSliderEnabled));
|
||||
ValidateAndSyncSlider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ★★★ 修正:属性名为 AllowEnlarge,对应 Model 中的 AllowEnlarge ★★★
|
||||
public bool AllowEnlarge
|
||||
{
|
||||
get => _editInfo.AllowEnlarge;
|
||||
set
|
||||
{
|
||||
if (_editInfo.AllowEnlarge != value)
|
||||
{
|
||||
_editInfo.AllowEnlarge = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsSliderEnabled));
|
||||
// 开关变化时,重新校验当前尺寸是否合法
|
||||
ValidateAndSyncSlider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图像增强
|
||||
public bool EnhanceImage
|
||||
{
|
||||
get => _editInfo.EnhanceImage;
|
||||
set
|
||||
{
|
||||
if (_editInfo.EnhanceImage != value)
|
||||
{
|
||||
_editInfo.EnhanceImage = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 分辨率与缩放属性 ---
|
||||
|
||||
private int _targetWidth;
|
||||
public int TargetWidth
|
||||
{
|
||||
get => _targetWidth;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _targetWidth, value))
|
||||
{
|
||||
if (IsRatioLocked && _sourceCamera.Width > 0)
|
||||
{
|
||||
_targetHeight = (int)((double)value / _sourceCamera.Width * _sourceCamera.Height);
|
||||
OnPropertyChanged(nameof(TargetHeight));
|
||||
}
|
||||
ValidateAndSyncSlider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int _targetHeight;
|
||||
public int TargetHeight
|
||||
{
|
||||
get => _targetHeight;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _targetHeight, value))
|
||||
{
|
||||
if (IsRatioLocked && _sourceCamera.Height > 0)
|
||||
{
|
||||
_targetWidth = (int)((double)value / _sourceCamera.Height * _sourceCamera.Width);
|
||||
OnPropertyChanged(nameof(TargetWidth));
|
||||
}
|
||||
ValidateAndSyncSlider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isRatioLocked = true;
|
||||
public bool IsRatioLocked
|
||||
{
|
||||
get => _isRatioLocked;
|
||||
set => SetProperty(ref _isRatioLocked, value);
|
||||
}
|
||||
|
||||
private double _scalePercent = 100;
|
||||
public double ScalePercent
|
||||
{
|
||||
get => _scalePercent;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _scalePercent, value))
|
||||
{
|
||||
UpdateResolutionByScale(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [新增] 亮度调节属性
|
||||
public int Brightness
|
||||
{
|
||||
get => _editInfo.Brightness;
|
||||
set
|
||||
{
|
||||
if (_editInfo.Brightness != value)
|
||||
{
|
||||
_editInfo.Brightness = value;
|
||||
OnPropertyChanged();
|
||||
// 如果需要,可以在这里限制范围,例如 0-100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public ICommand SaveCommand { get; }
|
||||
public ICommand CancelCommand { get; }
|
||||
public ICommand ApplyPresetCommand { get; }
|
||||
|
||||
public CameraImgProcViewModel(WebApiCameraModel source, CameraEditInfo detail)
|
||||
{
|
||||
_sourceCamera = source;
|
||||
|
||||
// 此时 detail 可能是不完整的,先赋值防止空引用
|
||||
_editInfo = detail ?? new CameraEditInfo { Id = source.Id };
|
||||
|
||||
SaveCommand = new RelayCommand(ExecuteSave);
|
||||
CancelCommand = new RelayCommand(o => RequestClose?.Invoke());
|
||||
ApplyPresetCommand = new RelayCommand(ExecutePreset);
|
||||
|
||||
InitializeState();
|
||||
|
||||
LoadDataAsync();
|
||||
}
|
||||
|
||||
// 2. 实现加载逻辑
|
||||
private async void LoadDataAsync()
|
||||
{
|
||||
// 调用 Repository
|
||||
var remoteInfo = await ApiClient.Instance.Cameras.GetImageProcessingAsync(_sourceCamera.Id);
|
||||
|
||||
if (remoteInfo != null)
|
||||
{
|
||||
// --- A. 更新 ViewModel 的开关属性 ---
|
||||
// 注意:Setter 会自动触发 OnPropertyChanged 和 ValidateAndSyncSlider
|
||||
AllowShrink = remoteInfo.AllowShrink;
|
||||
AllowEnlarge = remoteInfo.AllowEnlarge; // 这里会自动更新 IsSliderEnabled
|
||||
EnhanceImage = remoteInfo.EnhanceImage;
|
||||
|
||||
// --- B. 更新分辨率 (解析 "W x H") ---
|
||||
if (!string.IsNullOrWhiteSpace(remoteInfo.TargetResolution))
|
||||
{
|
||||
var parts = remoteInfo.TargetResolution.Split('x');
|
||||
if (parts.Length == 2 && int.TryParse(parts[0], out int w) && int.TryParse(parts[1], out int h))
|
||||
{
|
||||
// 赋值 TargetWidth/Height 会自动触发 ValidateAndSyncSlider
|
||||
// 从而更新 ScalePercent 和 滑块位置
|
||||
TargetWidth = w;
|
||||
TargetHeight = h;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeState()
|
||||
{
|
||||
// 1. 强制规则:如果源分辨率 >= 1080P,不允许放大
|
||||
if (IsSourceHighRes)
|
||||
{
|
||||
_editInfo.AllowEnlarge = false; // ★★★ 修正引用 ★★★
|
||||
}
|
||||
|
||||
// 2. 检查历史设置
|
||||
// ★★★ 修正引用 ★★★
|
||||
bool hasPreviousSettings = _editInfo.AllowShrink || _editInfo.AllowEnlarge;
|
||||
|
||||
if (hasPreviousSettings)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_editInfo.TargetResolution))
|
||||
{
|
||||
var parts = _editInfo.TargetResolution.ToLower().Split('x');
|
||||
if (parts.Length == 2 && int.TryParse(parts[0], out int w) && int.TryParse(parts[1], out int h))
|
||||
{
|
||||
_targetWidth = w;
|
||||
_targetHeight = h;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 默认逻辑
|
||||
if (_sourceCamera.Width > 1280)
|
||||
{
|
||||
_targetWidth = 1280;
|
||||
_targetHeight = _sourceCamera.Width > 0
|
||||
? (int)((double)1280 / _sourceCamera.Width * _sourceCamera.Height)
|
||||
: 720;
|
||||
}
|
||||
else
|
||||
{
|
||||
_targetWidth = _sourceCamera.Width;
|
||||
_targetHeight = _sourceCamera.Height;
|
||||
}
|
||||
}
|
||||
|
||||
ValidateAndSyncSlider();
|
||||
}
|
||||
|
||||
private void UpdateResolutionByScale(double percent)
|
||||
{
|
||||
if (_sourceCamera.Width <= 0 || _sourceCamera.Height <= 0) return;
|
||||
|
||||
double w = _sourceCamera.Width * (percent / 100.0);
|
||||
double h = _sourceCamera.Height * (percent / 100.0);
|
||||
|
||||
_targetWidth = (int)w;
|
||||
_targetHeight = (int)h;
|
||||
OnPropertyChanged(nameof(TargetWidth));
|
||||
OnPropertyChanged(nameof(TargetHeight));
|
||||
}
|
||||
|
||||
private void ValidateAndSyncSlider()
|
||||
{
|
||||
if (_sourceCamera.Width <= 0) return;
|
||||
|
||||
// ★★★ 修正引用:使用 AllowEnlarge ★★★
|
||||
int maxWidth = AllowEnlarge ? 1920 : _sourceCamera.Width;
|
||||
int maxHeight = AllowEnlarge ? 1080 : _sourceCamera.Height;
|
||||
int minWidth = 160;
|
||||
|
||||
bool changed = false;
|
||||
|
||||
// 规则A:如果未开启放大,且当前 > 源 -> 强制回退到源
|
||||
// ★★★ 修正引用 ★★★
|
||||
if (!AllowEnlarge && (_targetWidth > _sourceCamera.Width))
|
||||
{
|
||||
_targetWidth = _sourceCamera.Width;
|
||||
_targetHeight = _sourceCamera.Height;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!AllowShrink && (_targetWidth < _sourceCamera.Width))
|
||||
{
|
||||
_targetWidth = _sourceCamera.Width;
|
||||
_targetHeight = _sourceCamera.Height;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (_targetWidth > 1920) { _targetWidth = 1920; changed = true; }
|
||||
if (_targetHeight > 1080) { _targetHeight = 1080; changed = true; }
|
||||
if (_targetWidth < minWidth) { _targetWidth = minWidth; changed = true; }
|
||||
|
||||
if (changed)
|
||||
{
|
||||
OnPropertyChanged(nameof(TargetWidth));
|
||||
OnPropertyChanged(nameof(TargetHeight));
|
||||
}
|
||||
|
||||
double currentPercent = ((double)_targetWidth / _sourceCamera.Width) * 100.0;
|
||||
|
||||
if (Math.Abs(_scalePercent - currentPercent) > 0.1)
|
||||
{
|
||||
_scalePercent = currentPercent;
|
||||
OnPropertyChanged(nameof(ScalePercent));
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecutePreset(object parameter)
|
||||
{
|
||||
if (parameter is string preset)
|
||||
{
|
||||
var parts = preset.Split('x');
|
||||
if (parts.Length != 2) return;
|
||||
|
||||
if (int.TryParse(parts[0], out int w) && int.TryParse(parts[1], out int h))
|
||||
{
|
||||
if (w > _sourceCamera.Width)
|
||||
{
|
||||
if (IsSourceHighRes)
|
||||
{
|
||||
MessageBox.Show("当前源分辨率已超过或等于 1080P,不支持放大。", "提示");
|
||||
return;
|
||||
}
|
||||
AllowEnlarge = true; // ★★★ 修正引用 ★★★
|
||||
}
|
||||
else if (w < _sourceCamera.Width)
|
||||
{
|
||||
AllowShrink = true;
|
||||
}
|
||||
|
||||
TargetWidth = w;
|
||||
TargetHeight = h;
|
||||
IsRatioLocked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExecuteSave(object obj)
|
||||
{
|
||||
// 1. 准备数据 (拼接分辨率字符串)
|
||||
_editInfo.TargetResolution = $"{TargetWidth}x{TargetHeight}";
|
||||
|
||||
// 2. 调用专用的 Processing 接口
|
||||
bool success = await ApiClient.Instance.Cameras.UpdateImageProcessingAsync(_editInfo); // <--- 换成这就行
|
||||
|
||||
if (success)
|
||||
{
|
||||
AppGlobal.RequestRefresh();
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show("保存图像处理配置失败,请检查网络或后端日志。", "错误");
|
||||
}
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string name = null)
|
||||
{
|
||||
if (Equals(field, value)) return false;
|
||||
field = value;
|
||||
OnPropertyChanged(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
145
SHH.CameraDashboard/ViewModels/CameraItemTopViewModel.cs
Normal file
145
SHH.CameraDashboard/ViewModels/CameraItemTopViewModel.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
public class CameraItemTopViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private WebApiCameraModel _camera;
|
||||
|
||||
/// <summary>
|
||||
/// 当前选中的摄像头数据
|
||||
/// </summary>
|
||||
public WebApiCameraModel Camera
|
||||
{
|
||||
get => _camera;
|
||||
set
|
||||
{
|
||||
if (_camera != value)
|
||||
{
|
||||
_camera = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HasSelection)); // 通知界面是否有选中项
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand EditDeviceCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 辅助属性:用于界面判断是否显示内容
|
||||
/// </summary>
|
||||
public bool HasSelection => Camera != null;
|
||||
|
||||
public ICommand TogglePlayCommand { get; }
|
||||
|
||||
public ICommand PtzCommand { get; } // [新增]
|
||||
|
||||
// [新增] 图像处理命令
|
||||
public ICommand ImageProcessCommand { get; }
|
||||
|
||||
// [新增] 图像订阅命令
|
||||
public ICommand ImageSubscribeCommand { get; }
|
||||
|
||||
public CameraItemTopViewModel()
|
||||
{
|
||||
// 绑定命令到执行方法
|
||||
TogglePlayCommand = new RelayCommand<object>(async _ => await ExecuteTogglePlayAsync());
|
||||
|
||||
// 2. 初始化命令
|
||||
EditDeviceCommand = new RelayCommand<object>(ExecuteEditDevice);
|
||||
|
||||
PtzCommand = new RelayCommand(ExecutePtz);
|
||||
|
||||
DeleteCommand = new RelayCommand(ExecuteDelete);
|
||||
|
||||
// 初始化新命令
|
||||
ImageProcessCommand = new RelayCommand(ExecuteImageProcess);
|
||||
ImageSubscribeCommand = new RelayCommand(ExecuteImageSubscribe);
|
||||
}
|
||||
|
||||
// [新增] 执行图像处理
|
||||
private void ExecuteImageProcess(object obj)
|
||||
{
|
||||
if (Camera == null) return;
|
||||
|
||||
// 触发全局事件,打开右侧面板
|
||||
AppGlobal.RequestImgProc(this.Camera);
|
||||
}
|
||||
|
||||
// [新增] 执行图像订阅
|
||||
private void ExecuteImageSubscribe(object obj)
|
||||
{
|
||||
if (Camera == null) return;
|
||||
|
||||
// 不再直接 MessageBox,而是像 Edit/Ptz 一样发出全局请求
|
||||
// 这会将操作权交给 MainViewModel,由它在右侧面板加载 View
|
||||
AppGlobal.RequestSubscription(this.Camera);
|
||||
}
|
||||
|
||||
private void ExecutePtz(object obj)
|
||||
{
|
||||
// 这里可以加个判断,比如 Brand=RTSP 或 File 可能不支持云台
|
||||
// if (Model.Brand == (int)DeviceBrand.File) return;
|
||||
|
||||
AppGlobal.RequestPtz(this.Camera);
|
||||
}
|
||||
|
||||
private async Task ExecuteTogglePlayAsync()
|
||||
{
|
||||
if (Camera == null) return;
|
||||
|
||||
// 1. 物理离线检查
|
||||
if (!Camera.IsPhysicalOnline)
|
||||
{
|
||||
// 提示:设备断网,无法发送指令
|
||||
MessageBox.Show("设备物理离线,无法操作");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 发送控制指令
|
||||
var useServiceNode = AppGlobal.UseServiceNode;
|
||||
if (useServiceNode != null)
|
||||
{
|
||||
bool isPlaying = Camera.Status.Equals("Playing");
|
||||
bool isSuccess = await ApiClient.Instance.Cameras.ControlPowerAsync(
|
||||
Camera.Id,
|
||||
!isPlaying,
|
||||
"设备面板"
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行编辑逻辑
|
||||
/// </summary>
|
||||
private void ExecuteEditDevice(object obj)
|
||||
{
|
||||
if (Camera == null) return;
|
||||
|
||||
AppGlobal.RequestEdit(Camera);
|
||||
}
|
||||
|
||||
// 2. 自身的删除命令 (不需要参数,因为我手里有 Model)
|
||||
public ICommand DeleteCommand { get; }
|
||||
|
||||
private void ExecuteDelete(object obj)
|
||||
{
|
||||
// 直接把手里的数据发出去
|
||||
AppGlobal.RequestDelete(this.Camera);
|
||||
}
|
||||
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string name = "")
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
259
SHH.CameraDashboard/ViewModels/CameraListViewModel.cs
Normal file
259
SHH.CameraDashboard/ViewModels/CameraListViewModel.cs
Normal file
@@ -0,0 +1,259 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading; // 引入 DispatcherTimer 命名空间
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public class CameraListViewModel : INotifyPropertyChanged
|
||||
{
|
||||
// 存储从 API 获取的所有原始数据
|
||||
private readonly ObservableCollection<WebApiCameraModel> _allCameras;
|
||||
|
||||
// 提供给界面绑定的视图 (支持搜索过滤)
|
||||
public ICollectionView FilteredCameras { get; private set; }
|
||||
|
||||
#region --- 状态属性 ---
|
||||
|
||||
private bool _isLoading;
|
||||
public bool IsLoading
|
||||
{
|
||||
get => _isLoading;
|
||||
set { _isLoading = value; OnPropertyChanged(nameof(IsLoading)); }
|
||||
}
|
||||
|
||||
private WebApiCameraModel _selectedCamera;
|
||||
public WebApiCameraModel SelectedCamera
|
||||
{
|
||||
get => _selectedCamera;
|
||||
set { _selectedCamera = value; OnPropertyChanged(nameof(SelectedCamera)); }
|
||||
}
|
||||
|
||||
private string _searchText;
|
||||
public string SearchText
|
||||
{
|
||||
get => _searchText;
|
||||
set
|
||||
{
|
||||
_searchText = value;
|
||||
OnPropertyChanged(nameof(SearchText));
|
||||
// 输入文字时立即触发过滤
|
||||
FilteredCameras.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 下拉框节点选择 ---
|
||||
|
||||
// 下拉框的数据源
|
||||
public ObservableCollection<ServiceNodeModel> NodeOptions
|
||||
=> AppGlobal.ServiceNodes;
|
||||
|
||||
private ServiceNodeModel _selectedNode;
|
||||
public ServiceNodeModel SelectedNode
|
||||
{
|
||||
get => _selectedNode;
|
||||
set
|
||||
{
|
||||
if (_selectedNode != value)
|
||||
{
|
||||
_selectedNode = value;
|
||||
OnPropertyChanged(nameof(SelectedNode));
|
||||
|
||||
AppGlobal.UseServiceNode = value;
|
||||
|
||||
// 切换节点时,自动重新加载数据
|
||||
_ = LoadDataAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public ICommand RefreshCommand { get; }
|
||||
|
||||
public CameraListViewModel()
|
||||
{
|
||||
_allCameras = new ObservableCollection<WebApiCameraModel>();
|
||||
|
||||
// 初始化 CollectionView
|
||||
FilteredCameras = CollectionViewSource.GetDefaultView(_allCameras);
|
||||
FilteredCameras.Filter = FilterCameras;
|
||||
|
||||
AddCameraCommand = new RelayCommand(ExecuteAddCamera);
|
||||
DeleteCommand = new RelayCommand(ExecuteDelete);
|
||||
|
||||
RefreshCommand = new RelayCommand<object>(async _ => await LoadDataAsync());
|
||||
|
||||
// --- 新增:定时器 ---
|
||||
// 使用 DispatcherTimer 确保在 UI 线程执行,避免跨线程操作集合报错
|
||||
var timer = new DispatcherTimer();
|
||||
timer.Interval = TimeSpan.FromSeconds(2); // 这里设置间隔,例如 2 秒
|
||||
timer.Tick += async (s, e) => await LoadDataAsync();
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 核心:加载数据逻辑 (无刷版本)
|
||||
/// </summary>
|
||||
public async Task LoadDataAsync()
|
||||
{
|
||||
// 如果正在加载中,跳过本次(防止并发)
|
||||
if (IsLoading) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 【改动1】只有第一次加载(列表为空)时才显示 Loading 转圈
|
||||
// 这样定时刷新时,界面就不会弹出遮罩层
|
||||
if (_allCameras.Count == 0) IsLoading = true;
|
||||
|
||||
// 确定要查询的目标节点列表
|
||||
var targetNodes = new List<ServiceNodeModel>();
|
||||
|
||||
if (SelectedNode == null || SelectedNode.ServiceNodeIp == "ALL")
|
||||
{
|
||||
if (AppGlobal.ServiceNodes != null)
|
||||
targetNodes.AddRange(AppGlobal.ServiceNodes);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetNodes.Add(SelectedNode);
|
||||
}
|
||||
|
||||
if (targetNodes.Count == 0)
|
||||
{
|
||||
// 如果没有节点,清空列表
|
||||
if (_allCameras.Count > 0) _allCameras.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建并发任务列表
|
||||
var tasks = new List<Task<List<WebApiCameraModel>?>>();
|
||||
|
||||
foreach (var node in targetNodes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(node.ServiceNodeIp) || string.IsNullOrWhiteSpace(node.ServiceNodePort))
|
||||
continue;
|
||||
|
||||
tasks.Add(ApiClient.Instance.Cameras.GetListByAddressAsync(node.ServiceNodeIp, node.ServiceNodePort, "左侧列表刷新"));
|
||||
}
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
// 【改动2】先将所有新数据收集到一个临时列表中,不要直接操作界面集合
|
||||
var newLatestData = new List<WebApiCameraModel>();
|
||||
foreach (var list in results)
|
||||
{
|
||||
if (list != null)
|
||||
{
|
||||
newLatestData.AddRange(list);
|
||||
}
|
||||
}
|
||||
|
||||
// 【改动3】执行无刷更新 (智能合并)
|
||||
if (newLatestData.Count > 0)
|
||||
UpdateCollectionNoFlash(newLatestData);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 辅助方法:比对新旧数据,只更新变化的,实现“无刷”
|
||||
/// </summary>
|
||||
private void UpdateCollectionNoFlash(List<WebApiCameraModel> newData)
|
||||
{
|
||||
// 1. 删除:界面上有,但新数据里没有的 (说明设备断开或移除了)
|
||||
// 使用 ToList() 避免在遍历时修改集合报错
|
||||
var itemsToRemove = _allCameras.Where(old => !newData.Any(n => n.Id == old.Id)).ToList();
|
||||
foreach (var item in itemsToRemove)
|
||||
{
|
||||
_allCameras.Remove(item);
|
||||
}
|
||||
|
||||
// 2. 更新或新增
|
||||
foreach (var newCam in newData)
|
||||
{
|
||||
// 尝试在界面列表中找这个 ID
|
||||
var oldCam = _allCameras.FirstOrDefault(x => x.Id == newCam.Id);
|
||||
|
||||
if (oldCam != null)
|
||||
{
|
||||
// --- [更新] ---
|
||||
// 找到老对象,手动更新它的属性。
|
||||
// 注意:你的 WebApiCameraModel 必须实现了 INotifyPropertyChanged
|
||||
// 这里的赋值才会让界面文字发生变化,否则界面不动。
|
||||
oldCam.Name = newCam.Name;
|
||||
oldCam.IpAddress = newCam.IpAddress;
|
||||
oldCam.Brand = newCam.Brand;
|
||||
|
||||
oldCam.Status = newCam.Status;
|
||||
oldCam.IsOnline = newCam.IsOnline;
|
||||
oldCam.IsPhysicalOnline = newCam.IsPhysicalOnline;
|
||||
oldCam.IsRunning = newCam.IsRunning;
|
||||
|
||||
oldCam.Width = newCam.Width;
|
||||
oldCam.Height = newCam.Height;
|
||||
oldCam.RealFps = newCam.RealFps;
|
||||
oldCam.TotalFrames = newCam.TotalFrames;
|
||||
oldCam.StreamType = newCam.StreamType;
|
||||
|
||||
// 如果有其他变化的字段(如帧率),继续在这里赋值...
|
||||
}
|
||||
else
|
||||
{
|
||||
// --- [新增] ---
|
||||
// 没找到,说明是新上线的设备,添加到列表
|
||||
_allCameras.Add(newCam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 本地搜索过滤逻辑
|
||||
/// </summary>
|
||||
private bool FilterCameras(object obj)
|
||||
{
|
||||
if (obj is WebApiCameraModel camera)
|
||||
{
|
||||
// 搜索框为空显示所有
|
||||
if (string.IsNullOrWhiteSpace(SearchText)) return true;
|
||||
|
||||
string lowerSearch = SearchText.ToLower();
|
||||
|
||||
// 匹配 名称、IP 或 品牌
|
||||
return (camera.Name?.ToLower().Contains(lowerSearch) == true) ||
|
||||
(camera.IpAddress?.Contains(lowerSearch) == true) ||
|
||||
(camera.Brand?.ToLower().Contains(lowerSearch) == true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public ICommand AddCameraCommand { get; }
|
||||
|
||||
private void ExecuteAddCamera(object obj)
|
||||
{
|
||||
// 简单直接:通知全世界“我要添加摄像头!”
|
||||
AppGlobal.RequestAdd();
|
||||
}
|
||||
|
||||
// [新增] 删除命令
|
||||
public ICommand DeleteCommand { get; }
|
||||
|
||||
private void ExecuteDelete(object obj)
|
||||
{
|
||||
// 这里的 obj 就是从界面传过来的 WebApiCameraModel
|
||||
if (obj is WebApiCameraModel camera)
|
||||
{
|
||||
AppGlobal.RequestDelete(camera);
|
||||
}
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
protected void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
}
|
||||
133
SHH.CameraDashboard/ViewModels/CameraPtzViewModel.cs
Normal file
133
SHH.CameraDashboard/ViewModels/CameraPtzViewModel.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public class CameraPtzViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private readonly WebApiCameraModel _camera;
|
||||
|
||||
public string Title => $"{_camera.Name} - 云台控制";
|
||||
|
||||
public ICommand StartCommand { get; }
|
||||
public ICommand StopCommand { get; }
|
||||
|
||||
// 用于关闭面板
|
||||
public ICommand CloseCommand { get; }
|
||||
|
||||
public ICommand SyncTimeCommand { get; }
|
||||
public ICommand RebootCommand { get; }
|
||||
|
||||
// ★★★ [新增] 关闭请求事件,通知 MainViewModel 关闭面板
|
||||
public event Action? RequestClose;
|
||||
|
||||
public CameraPtzViewModel(WebApiCameraModel camera)
|
||||
{
|
||||
_camera = camera;
|
||||
|
||||
// 使用非泛型 RelayCommand,但在 Execute 中强转参数
|
||||
StartCommand = new RelayCommand(ExecuteStart);
|
||||
StopCommand = new RelayCommand(ExecuteStop);
|
||||
|
||||
// 关闭逻辑:可以简单地请求刷新或置空 MainVM 的属性(这里暂用刷新模拟)
|
||||
CloseCommand = new RelayCommand(o => RequestClose?.Invoke());
|
||||
|
||||
// [新增] 初始化运维命令
|
||||
SyncTimeCommand = new RelayCommand(ExecuteSyncTime);
|
||||
RebootCommand = new RelayCommand(ExecuteReboot);
|
||||
}
|
||||
// [新增] 执行校时
|
||||
private async void ExecuteSyncTime(object obj)
|
||||
{
|
||||
if (_camera == null) return;
|
||||
|
||||
// 可以加个 IsBusy 状态防止连点
|
||||
bool success = await ApiClient.Instance.Cameras.SyncTimeAsync(_camera.Id);
|
||||
|
||||
if (success)
|
||||
MessageBox.Show("校时指令已发送成功!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
else
|
||||
MessageBox.Show("校时失败,请检查网络或日志。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
|
||||
// [新增] 执行重启
|
||||
private async void ExecuteReboot(object obj)
|
||||
{
|
||||
if (_camera == null) return;
|
||||
|
||||
// 重启是高风险操作,必须弹窗确认
|
||||
var result = MessageBox.Show(
|
||||
$"确定要重启设备 \"{_camera.Name}\" 吗?\n\n注意:\n1. 设备将断开连接约 60 秒。\n2. 录像可能会中断。",
|
||||
"确认重启",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Warning);
|
||||
|
||||
if (result != MessageBoxResult.Yes) return;
|
||||
|
||||
bool success = await ApiClient.Instance.Cameras.RebootCameraAsync(_camera.Id);
|
||||
|
||||
if (success)
|
||||
MessageBox.Show("重启指令已发送,设备正在重启中...", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
else
|
||||
MessageBox.Show("重启指令发送失败。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
|
||||
private async void ExecuteStart(object parameter)
|
||||
{
|
||||
if (parameter is PtzAction action)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"PTZ START: {action}");
|
||||
|
||||
// 模式 A: 雨刷 (Wiper) -> 点动模式 (持续 5秒)
|
||||
if (action == PtzAction.Wiper)
|
||||
{
|
||||
await SendPtzRequest(new PtzControlDto
|
||||
{
|
||||
Action = action,
|
||||
Duration = 5000
|
||||
});
|
||||
}
|
||||
// 模式 B: 持续动作 (Up/Down/Zoom...) -> Start
|
||||
else
|
||||
{
|
||||
await SendPtzRequest(new PtzControlDto
|
||||
{
|
||||
Action = action,
|
||||
Stop = false,
|
||||
Speed = 5
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExecuteStop(object parameter)
|
||||
{
|
||||
if (parameter is PtzAction action)
|
||||
{
|
||||
// 雨刷是自动复位的,不需要发停止
|
||||
if (action == PtzAction.Wiper) return;
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"PTZ STOP: {action}");
|
||||
|
||||
// 发送停止指令:Action=动作名, Stop=true
|
||||
await SendPtzRequest(new PtzControlDto
|
||||
{
|
||||
Action = action,
|
||||
Stop = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendPtzRequest(PtzControlDto payload)
|
||||
{
|
||||
if (_camera == null) return;
|
||||
await ApiClient.Instance.Cameras.PtzControlAsync(_camera.Id, payload);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
protected void OnPropertyChanged([CallerMemberName] string name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
}
|
||||
188
SHH.CameraDashboard/ViewModels/WizardClientsVewModel.cs
Normal file
188
SHH.CameraDashboard/ViewModels/WizardClientsVewModel.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// 客户端配置向导的 ViewModel
|
||||
/// 实现了 <see cref="IOverlayClosable"/> 接口,用于与父窗口的蒙板交互。
|
||||
/// </summary>
|
||||
public class WizardClientsViewModel : INotifyPropertyChanged, IOverlayClosable
|
||||
{
|
||||
/// <summary>
|
||||
/// 绑定到 ListView 的数据源
|
||||
/// </summary>
|
||||
public static ObservableCollection<ServiceNodeModel> ServiceNodes
|
||||
=> AppGlobal.ServiceNodes;
|
||||
|
||||
#region --- 业务数据属性 ---
|
||||
|
||||
private string _statusText = "准备就绪";
|
||||
/// <summary>
|
||||
/// 获取或设置向导的当前状态文本。
|
||||
/// </summary>
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
set { _statusText = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 命令属性 ---
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于确认操作的命令。
|
||||
/// </summary>
|
||||
public ICommand ConfirmCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于取消操作的命令。
|
||||
/// </summary>
|
||||
public ICommand CancelCommand { get; }
|
||||
|
||||
// 新增一行的命令
|
||||
public ICommand AddNodeCommand { get; }
|
||||
|
||||
public ICommand DeleteNodeCommand { get; }
|
||||
|
||||
public ICommand CheckCommand { get; }
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 构造函数 ---
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 <see cref="WizardClientsViewModel"/> 类的新实例。
|
||||
/// </summary>
|
||||
public WizardClientsViewModel()
|
||||
{
|
||||
// 实现新增逻辑
|
||||
AddNodeCommand = new RelayCommand<object>(_ =>
|
||||
{
|
||||
// 创建新行对象并添加到集合
|
||||
var newNode = new ServiceNodeModel
|
||||
{
|
||||
ServiceNodeName = "新节点",
|
||||
ServiceNodeIp = "0.0.0.0",
|
||||
ServiceNodePort = "5000",
|
||||
Status = "未检测"
|
||||
};
|
||||
ServiceNodes.Add(newNode);
|
||||
});
|
||||
|
||||
// 删除逻辑实现
|
||||
DeleteNodeCommand = new RelayCommand<ServiceNodeModel>(node =>
|
||||
{
|
||||
if (node != null && ServiceNodes.Contains(node))
|
||||
{
|
||||
ServiceNodes.Remove(node);
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化确认命令
|
||||
ConfirmCommand = new RelayCommand<object>(async _ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 2. 【核心代码】调用通用存储服务
|
||||
// 泛型 T 自动推断为 ObservableCollection<ServiceNodeModel>
|
||||
await LocalStorageService.SaveAsync(
|
||||
AppPaths.ServiceNodesConfig, // 路径:Configs/service_nodes.json
|
||||
ServiceNodes // 数据:当前的列表对象
|
||||
);
|
||||
|
||||
// 4. 关闭当前弹窗
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"保存失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
});
|
||||
|
||||
CheckCommand = new RelayCommand<object>(async _ => await ExecuteCheckAsync());
|
||||
|
||||
// 初始化取消命令
|
||||
CancelCommand = new RelayCommand<object>(_ =>
|
||||
{
|
||||
// 直接触发关闭请求事件,取消并关闭向导
|
||||
RequestClose?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- IOverlayClosable 接口实现 ---
|
||||
|
||||
/// <summary>
|
||||
/// 当需要关闭蒙板时发生。
|
||||
/// </summary>
|
||||
public event Action? RequestClose;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- INotifyPropertyChanged 接口实现 ---
|
||||
|
||||
/// <summary>
|
||||
/// 当属性值更改时发生。
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 引发 <see cref="PropertyChanged"/> 事件。
|
||||
/// </summary>
|
||||
/// <param name="name">更改的属性名称。</param>
|
||||
protected void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task ExecuteCheckAsync()
|
||||
{
|
||||
foreach (var node in ServiceNodes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(node.ServiceNodeIp) || string.IsNullOrWhiteSpace(node.ServiceNodePort))
|
||||
continue;
|
||||
|
||||
node.Status = "正在获取信息...";
|
||||
|
||||
// 1. 调用 ApiClient 获取列表 (代码不需要变,因为 Repository 已经封装好了)
|
||||
var cameras = await ApiClient.Instance.Cameras.GetListByAddressAsync(
|
||||
node.ServiceNodeIp,
|
||||
node.ServiceNodePort,
|
||||
"向导页面"
|
||||
);
|
||||
|
||||
// 2. 根据返回的详细数据,生成更智能的状态描述
|
||||
if (cameras.Count > 0)
|
||||
{
|
||||
// 统计在线的摄像头数量
|
||||
int onlineCount = 0;
|
||||
// 统计主要品牌 (例如: HikVision)
|
||||
string firstBrand = cameras[0].Brand;
|
||||
|
||||
foreach (var cam in cameras)
|
||||
{
|
||||
if (cam.IsOnline) onlineCount++;
|
||||
}
|
||||
|
||||
// 状态显示示例:只能是在线,其它的影响界面上色
|
||||
node.Status = $"在线";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 列表为空,或者是网络不通导致 Repository 返回了空列表
|
||||
// 为了区分是“无数据”还是“网络不通”,其实 Repository 可以优化返回 null 或抛异常,
|
||||
// 但目前的架构返回空列表最安全。
|
||||
node.Status = "离线或无设备";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace SHH.CameraSdk;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 视频源物理/逻辑品牌类型
|
||||
@@ -9,6 +11,7 @@ public enum DeviceBrand
|
||||
/// <summary>
|
||||
/// 未知
|
||||
/// </summary>
|
||||
[Description("未知")]
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
@@ -16,47 +19,54 @@ public enum DeviceBrand
|
||||
/// 技术路径:基于海康私有 SDK (HCNetSDK.dll / PlayCtrl.dll)。
|
||||
/// 特性:支持全功能控制(PTZ、对讲、配置、报警回传)。
|
||||
/// </summary>
|
||||
HikVision,
|
||||
[Description("海康威视")]
|
||||
HikVision = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 大华 (Dahua)
|
||||
/// 技术路径:基于大华私有 SDK (dhnetsdk.dll / dhplay.dll)。
|
||||
/// 特性:支持全功能控制,与海康私有协议不兼容。
|
||||
/// </summary>
|
||||
Dahua,
|
||||
[Description("大华")]
|
||||
Dahua = 2,
|
||||
|
||||
/// <summary>
|
||||
/// USB 摄像头 / 虚拟摄像头
|
||||
/// 技术路径:基于 DirectShow 或 Windows Media Foundation。
|
||||
/// 特性:通常通过 OpenCV (VideoCapture) 或 DirectShowLib 直接读取本地硬件引用。
|
||||
/// </summary>
|
||||
Usb,
|
||||
[Description("USB")]
|
||||
Usb = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 标准 RTSP 流媒体
|
||||
/// 技术路径:基于标准 RTSP/RTP 协议 (RFC 2326)。
|
||||
/// 特性:跨品牌兼容,通常使用 FFmpeg 或 GStreamer 库取流,仅支持音视频,不支持云台控制。
|
||||
/// </summary>
|
||||
RtspGeneral,
|
||||
[Description("RTSP")]
|
||||
RtspGeneral = 4,
|
||||
|
||||
/// <summary>
|
||||
/// 三恒自研 WebSocket 流
|
||||
/// 技术路径:基于 WebSocket 传输的自定义二进制或 Base64 帧。
|
||||
/// 特性:专用于 Web 或云端推送场景的私有流媒体格式。
|
||||
/// </summary>
|
||||
WebSocketShine,
|
||||
[Description("三恒WebSocket")]
|
||||
WebSocketShine = 5,
|
||||
|
||||
/// <summary>
|
||||
/// 本地视频文件
|
||||
/// 技术路径:基于文件 IO 的离线解码。
|
||||
/// 特性:常用于算法演示、回放模拟,支持 Mp4, Avi, Mkv 等容器格式。
|
||||
/// </summary>
|
||||
File,
|
||||
[Description("文件")]
|
||||
File = 6,
|
||||
|
||||
/// <summary>
|
||||
/// 未知/通用标准 (ONVIF)
|
||||
/// 技术路径:基于标准 ONVIF WebService。
|
||||
/// 特性:用于接入非主流厂商但符合 ONVIF 标准的设备,支持基础 PTZ。
|
||||
/// </summary>
|
||||
OnvifGeneral
|
||||
[Description("通用标准")]
|
||||
OnvifGeneral = 7
|
||||
}
|
||||
@@ -326,6 +326,64 @@ public class CamerasController : ControllerBase
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{id}/subscriptions")]
|
||||
public IActionResult GetSubscriptions(long id)
|
||||
{
|
||||
// 1. 检查设备是否存在
|
||||
var camera = _manager.GetDevice(id);
|
||||
if (camera == null)
|
||||
{
|
||||
return NotFound(new { error = $"Camera {id} not found." });
|
||||
}
|
||||
|
||||
// 2. 从设备的 FrameController 获取当前活跃的订阅
|
||||
// 注意:FrameController.GetCurrentRequirements() 返回的是 List<FrameRequirement>
|
||||
// 它可以直接被序列化为 JSON
|
||||
var subs = camera.Controller.GetCurrentRequirements();
|
||||
|
||||
return Ok(subs);
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// 6. 新增:彻底删除/注销订阅 (RESTful DELETE)
|
||||
// URL: DELETE /api/cameras/{id}/subscriptions/{appId}
|
||||
// =============================================================
|
||||
[HttpDelete("{id}/subscriptions/{appId}")]
|
||||
public IActionResult RemoveSubscription(long id, string appId)
|
||||
{
|
||||
// 1. 检查设备是否存在
|
||||
var device = _manager.GetDevice(id);
|
||||
if (device == null)
|
||||
{
|
||||
return NotFound(new { error = $"Camera {id} not found." });
|
||||
}
|
||||
|
||||
// 2. 获取流控控制器
|
||||
var controller = device.Controller;
|
||||
if (controller == null)
|
||||
{
|
||||
// 如果设备本身没有控制器(比如离线或不支持),也算删除成功
|
||||
return Ok(new { success = true, message = "Device has no controller, nothing to remove." });
|
||||
}
|
||||
|
||||
// 3. 执行注销逻辑 (核心)
|
||||
// 从 FrameController 的分发列表中移除
|
||||
controller.Unregister(appId);
|
||||
|
||||
// 4. 清理显示资源 (重要!)
|
||||
// 参考您 UpdateSubscription 中的逻辑,必须同时停止 DisplayManager,否则窗口关不掉
|
||||
_displayManager.StopDisplay(appId);
|
||||
|
||||
// 5. 记录审计日志
|
||||
device.AddAuditLog($"用户指令:彻底注销订阅 [{appId}]");
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = $"Subscription {appId} has been removed and display stopped."
|
||||
});
|
||||
}
|
||||
|
||||
// 1. 获取单个设备详情(用于编辑回填)
|
||||
[HttpGet("{id}")]
|
||||
public IActionResult GetDevice(int id)
|
||||
|
||||
Reference in New Issue
Block a user