增加摄像头中控台项目

This commit is contained in:
2025-12-30 10:53:02 +08:00
parent 471b8c50b6
commit de3adf0339
31 changed files with 2736 additions and 0 deletions

View 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>

View 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 ? "▼" : "▲";
}
}
}

View 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>

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

View 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>

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

View 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>

View 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 ?? "");
}
}
}