增加摄像头中控台项目
This commit is contained in:
67
SHH.CameraDashboard/Controls/BottomDockControl.xaml
Normal file
67
SHH.CameraDashboard/Controls/BottomDockControl.xaml
Normal file
@@ -0,0 +1,67 @@
|
||||
<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>
|
||||
64
SHH.CameraDashboard/Controls/BottomDockControl.xaml.cs
Normal file
64
SHH.CameraDashboard/Controls/BottomDockControl.xaml.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
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 ? "▼" : "▲";
|
||||
}
|
||||
}
|
||||
}
|
||||
247
SHH.CameraDashboard/Controls/CameraListControl.xaml
Normal file
247
SHH.CameraDashboard/Controls/CameraListControl.xaml
Normal file
@@ -0,0 +1,247 @@
|
||||
<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>
|
||||
148
SHH.CameraDashboard/Controls/CameraListControl.xaml.cs
Normal file
148
SHH.CameraDashboard/Controls/CameraListControl.xaml.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
199
SHH.CameraDashboard/Controls/DeviceHomeControl.xaml
Normal file
199
SHH.CameraDashboard/Controls/DeviceHomeControl.xaml
Normal file
@@ -0,0 +1,199 @@
|
||||
<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>
|
||||
52
SHH.CameraDashboard/Controls/DeviceHomeControl.xaml.cs
Normal file
52
SHH.CameraDashboard/Controls/DeviceHomeControl.xaml.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
83
SHH.CameraDashboard/Controls/DiagnosticControl.xaml
Normal file
83
SHH.CameraDashboard/Controls/DiagnosticControl.xaml
Normal file
@@ -0,0 +1,83 @@
|
||||
<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>
|
||||
106
SHH.CameraDashboard/Controls/DiagnosticControl.xaml.cs
Normal file
106
SHH.CameraDashboard/Controls/DiagnosticControl.xaml.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
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 ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user