增加摄像头中控台项目
This commit is contained in:
25
SHH.CameraDashboard/App.xaml
Normal file
25
SHH.CameraDashboard/App.xaml
Normal file
@@ -0,0 +1,25 @@
|
||||
<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.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.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
14
SHH.CameraDashboard/App.xaml.cs
Normal file
14
SHH.CameraDashboard/App.xaml.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Windows;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
17
SHH.CameraDashboard/App/AppGlobalData.cs
Normal file
17
SHH.CameraDashboard/App/AppGlobalData.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
SHH.CameraDashboard/App/ClipboardHelper.cs
Normal file
40
SHH.CameraDashboard/App/ClipboardHelper.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
|
||||
public static class ClipboardHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 安全复制文本,包含冲突重试机制
|
||||
/// </summary>
|
||||
public static void SetText(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
|
||||
// 最多尝试 5 次,每次间隔 100 毫秒
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
Clipboard.SetText(text);
|
||||
return; // 复制成功,退出方法
|
||||
}
|
||||
catch (COMException ex)
|
||||
{
|
||||
// 如果是剪贴板被占用错误,等待后重试
|
||||
if ((uint)ex.ErrorCode == 0x800401D0)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
continue;
|
||||
}
|
||||
throw; // 其他 COM 错误则抛出
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
if (i == 4) throw; // 最后一次尝试失败则抛出
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
SHH.CameraDashboard/App/ThemeManager.cs
Normal file
53
SHH.CameraDashboard/App/ThemeManager.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
172
SHH.CameraDashboard/App/WizardControl.xaml
Normal file
172
SHH.CameraDashboard/App/WizardControl.xaml
Normal file
@@ -0,0 +1,172 @@
|
||||
<UserControl
|
||||
x:Class="SHH.CameraDashboard.WizardControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Width="800"
|
||||
Height="550"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Border
|
||||
Background="{DynamicResource Brush.Bg.Window}"
|
||||
BorderBrush="{DynamicResource Brush.Border.Focus}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{StaticResource Radius.Normal}">
|
||||
<Grid Margin="25">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0" Margin="0,0,0,20">
|
||||
<DockPanel LastChildFill="False">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="{StaticResource Size.Font.Huge}"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
Text="📡 服务节点配置" />
|
||||
<Button
|
||||
Width="30"
|
||||
Height="30"
|
||||
Click="Cancel_Click"
|
||||
Content="✕"
|
||||
DockPanel.Dock="Right"
|
||||
Style="{StaticResource Btn.Ghost}" />
|
||||
</DockPanel>
|
||||
<TextBlock
|
||||
Margin="0,10,0,0"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="请添加或修改后端的 IP 地址与端口,配置将用于聚合监控数据。" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border
|
||||
Padding="10"
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="1,1,1,0">
|
||||
<DockPanel LastChildFill="False">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="Bold"
|
||||
Text="节点清单" />
|
||||
<Button
|
||||
Height="26"
|
||||
Padding="15,0"
|
||||
Background="{DynamicResource Brush.Accent}"
|
||||
BorderThickness="0"
|
||||
Click="AddNode_Click"
|
||||
Content="+ 新增一行"
|
||||
DockPanel.Dock="Right"
|
||||
Style="{StaticResource {x:Type Button}}" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<ListView
|
||||
x:Name="NodeList"
|
||||
Grid.Row="1"
|
||||
Background="Transparent"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="1"
|
||||
ItemContainerStyle="{StaticResource Style.ListViewItem.Table}">
|
||||
<ListView.View>
|
||||
<GridView ColumnHeaderContainerStyle="{StaticResource Style.GridViewHeader.Flat}">
|
||||
<GridViewColumn Width="180" Header="节点名称">
|
||||
<GridViewColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Style="{StaticResource Style.TextBox.InTable}" Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
</GridViewColumn>
|
||||
|
||||
<GridViewColumn Width="180" Header="IP 地址">
|
||||
<GridViewColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Style="{StaticResource Style.TextBox.InTable}" Text="{Binding Ip, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
</GridViewColumn>
|
||||
|
||||
<GridViewColumn Width="100" Header="端口">
|
||||
<GridViewColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Style="{StaticResource Style.TextBox.InTable}" Text="{Binding Port, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
</GridViewColumn>
|
||||
|
||||
<GridViewColumn Width="180" Header="连通性状态">
|
||||
<GridViewColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Border
|
||||
Padding="8,2"
|
||||
HorizontalAlignment="Left"
|
||||
Background="#11000000"
|
||||
CornerRadius="4">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="Bold"
|
||||
Foreground="{Binding StatusColor}"
|
||||
Text="{Binding Status}" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
</GridViewColumn>
|
||||
|
||||
<GridViewColumn Width="80" Header="操作">
|
||||
<GridViewColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Button
|
||||
Width="45"
|
||||
Height="24"
|
||||
Padding="0"
|
||||
Click="DeleteNode_Click"
|
||||
Content="删除"
|
||||
FontSize="11"
|
||||
Style="{StaticResource Btn.Danger}" />
|
||||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
</GridViewColumn>
|
||||
</GridView>
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
</Grid>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Margin="0,20,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,0,15,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
Text="修改后请点击检测 →" />
|
||||
<Button
|
||||
Width="110"
|
||||
Height="32"
|
||||
Margin="0,0,10,0"
|
||||
Click="Check_Click"
|
||||
Content="🔍 立即检测"
|
||||
Style="{StaticResource Btn.Ghost}" />
|
||||
<Button
|
||||
Width="120"
|
||||
Height="32"
|
||||
Background="{DynamicResource Brush.Accent}"
|
||||
BorderThickness="0"
|
||||
Click="Apply_Click"
|
||||
Content="保存并应用"
|
||||
Foreground="White" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
94
SHH.CameraDashboard/App/WizardControl.xaml.cs
Normal file
94
SHH.CameraDashboard/App/WizardControl.xaml.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
SHH.CameraDashboard/AssemblyInfo.cs
Normal file
10
SHH.CameraDashboard/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
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 ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
244
SHH.CameraDashboard/MainWindow.xaml
Normal file
244
SHH.CameraDashboard/MainWindow.xaml
Normal file
@@ -0,0 +1,244 @@
|
||||
<Window
|
||||
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="SHH 视频中控"
|
||||
Width="1366"
|
||||
Height="768"
|
||||
Background="{DynamicResource Brush.Bg.Window}"
|
||||
FontFamily="{StaticResource Font.Normal}"
|
||||
Foreground="{DynamicResource Brush.Text.Primary}"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<WindowChrome.WindowChrome>
|
||||
<WindowChrome
|
||||
CaptionHeight="0"
|
||||
CornerRadius="0"
|
||||
GlassFrameThickness="0"
|
||||
ResizeBorderThickness="5" />
|
||||
</WindowChrome.WindowChrome>
|
||||
|
||||
<Grid x:Name="RootGrid">
|
||||
<Grid x:Name="AppLayout">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="45" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border
|
||||
Grid.Row="0"
|
||||
Background="{DynamicResource Brush.Bg.Panel}"
|
||||
BorderBrush="{DynamicResource Brush.Border}"
|
||||
BorderThickness="0,0,0,1"
|
||||
MouseDown="OnTitleBarMouseDown">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="0"
|
||||
Margin="10,0"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Width="40"
|
||||
Click="ToggleSidebar"
|
||||
Content="≡"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Style="{StaticResource Btn.Ghost}"
|
||||
ToolTip="展开/收起侧边栏" />
|
||||
</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
|
||||
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="打开配置向导" />
|
||||
<Button
|
||||
Width="40"
|
||||
Margin="0,0,15,0"
|
||||
Click="ToggleTheme"
|
||||
Content="🎨"
|
||||
Style="{StaticResource Btn.Ghost}"
|
||||
ToolTip="切换皮肤 (Dark/Light)" />
|
||||
<Button
|
||||
Width="40"
|
||||
Click="OnMinimize"
|
||||
Content="—"
|
||||
Style="{StaticResource Btn.Ghost}"
|
||||
ToolTip="最小化" />
|
||||
<Button
|
||||
Width="40"
|
||||
Margin="5,0,0,0"
|
||||
Click="OnClose"
|
||||
Content="✕"
|
||||
Style="{StaticResource Btn.Danger}"
|
||||
ToolTip="关闭" />
|
||||
</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" />
|
||||
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
Width="1"
|
||||
HorizontalAlignment="Right"
|
||||
Background="{DynamicResource Brush.Border}" />
|
||||
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
Background="{DynamicResource Brush.Bg.Window}"
|
||||
ClipToBounds="True">
|
||||
<ctrl:DeviceHomeControl x:Name="DeviceHome" Margin="0" />
|
||||
|
||||
<Border
|
||||
x:Name="RightConfigPanel"
|
||||
Width="350"
|
||||
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>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="50" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="60" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<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>
|
||||
</Grid>
|
||||
</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>
|
||||
</Window>
|
||||
107
SHH.CameraDashboard/MainWindow.xaml.cs
Normal file
107
SHH.CameraDashboard/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
private void CloseModal_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 点击遮罩层背景关闭
|
||||
CloseModal();
|
||||
}
|
||||
|
||||
private void CloseModal()
|
||||
{
|
||||
ModalLayer.Visibility = Visibility.Collapsed;
|
||||
ModalContainer.Content = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
SHH.CameraDashboard/Models/ApiLogEntry.cs
Normal file
25
SHH.CameraDashboard/Models/ApiLogEntry.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
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)); // 红色
|
||||
}
|
||||
}
|
||||
31
SHH.CameraDashboard/Models/CameraInfo.cs
Normal file
31
SHH.CameraDashboard/Models/CameraInfo.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
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" : "无信号";
|
||||
}
|
||||
63
SHH.CameraDashboard/Models/DeviceBrand.cs
Normal file
63
SHH.CameraDashboard/Models/DeviceBrand.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// 视频源物理/逻辑品牌类型
|
||||
/// 职责:用于工厂模式匹配具体的 IVideoSource 实现类,并定义基础通信协议栈
|
||||
/// </summary>
|
||||
public enum DeviceBrand
|
||||
{
|
||||
/// <summary>
|
||||
/// 未知
|
||||
/// </summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 海康威视 (HikVision)
|
||||
/// 技术路径:基于海康私有 SDK (HCNetSDK.dll / PlayCtrl.dll)。
|
||||
/// 特性:支持全功能控制(PTZ、对讲、配置、报警回传)。
|
||||
/// </summary>
|
||||
HikVision,
|
||||
|
||||
/// <summary>
|
||||
/// 大华 (Dahua)
|
||||
/// 技术路径:基于大华私有 SDK (dhnetsdk.dll / dhplay.dll)。
|
||||
/// 特性:支持全功能控制,与海康私有协议不兼容。
|
||||
/// </summary>
|
||||
Dahua,
|
||||
|
||||
/// <summary>
|
||||
/// USB 摄像头 / 虚拟摄像头
|
||||
/// 技术路径:基于 DirectShow 或 Windows Media Foundation。
|
||||
/// 特性:通常通过 OpenCV (VideoCapture) 或 DirectShowLib 直接读取本地硬件引用。
|
||||
/// </summary>
|
||||
Usb,
|
||||
|
||||
/// <summary>
|
||||
/// 标准 RTSP 流媒体
|
||||
/// 技术路径:基于标准 RTSP/RTP 协议 (RFC 2326)。
|
||||
/// 特性:跨品牌兼容,通常使用 FFmpeg 或 GStreamer 库取流,仅支持音视频,不支持云台控制。
|
||||
/// </summary>
|
||||
RtspGeneral,
|
||||
|
||||
/// <summary>
|
||||
/// 三恒自研 WebSocket 流
|
||||
/// 技术路径:基于 WebSocket 传输的自定义二进制或 Base64 帧。
|
||||
/// 特性:专用于 Web 或云端推送场景的私有流媒体格式。
|
||||
/// </summary>
|
||||
WebSocketShine,
|
||||
|
||||
/// <summary>
|
||||
/// 本地视频文件
|
||||
/// 技术路径:基于文件 IO 的离线解码。
|
||||
/// 特性:常用于算法演示、回放模拟,支持 Mp4, Avi, Mkv 等容器格式。
|
||||
/// </summary>
|
||||
File,
|
||||
|
||||
/// <summary>
|
||||
/// 未知/通用标准 (ONVIF)
|
||||
/// 技术路径:基于标准 ONVIF WebService。
|
||||
/// 特性:用于接入非主流厂商但符合 ONVIF 标准的设备,支持基础 PTZ。
|
||||
/// </summary>
|
||||
OnvifGeneral
|
||||
}
|
||||
}
|
||||
87
SHH.CameraDashboard/Models/ServerNode.cs
Normal file
87
SHH.CameraDashboard/Models/ServerNode.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
60
SHH.CameraDashboard/Models/VideoSourceStatus.cs
Normal file
60
SHH.CameraDashboard/Models/VideoSourceStatus.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
38
SHH.CameraDashboard/SHH.CameraDashboard.csproj
Normal file
38
SHH.CameraDashboard/SHH.CameraDashboard.csproj
Normal file
@@ -0,0 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Remove="Style\Themes\Colors.Dark.xaml" />
|
||||
<Page Remove="Style\Themes\Colors.Light.xaml" />
|
||||
<Page Remove="Style\Themes\Sizes.xaml" />
|
||||
<Page Remove="Style\Themes\Styles.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="Style\Themes\Colors.Light.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Resource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="Style\Themes\Colors.Dark.xaml" />
|
||||
<Resource Include="Style\Themes\Sizes.xaml" />
|
||||
<Resource Include="Style\Themes\Styles.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Converters\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
119
SHH.CameraDashboard/Services/HttpService.cs
Normal file
119
SHH.CameraDashboard/Services/HttpService.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
SHH.CameraDashboard/Services/StorageService.cs
Normal file
52
SHH.CameraDashboard/Services/StorageService.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
SHH.CameraDashboard/Style/Themes/Colors.Dark.xaml
Normal file
56
SHH.CameraDashboard/Style/Themes/Colors.Dark.xaml
Normal file
@@ -0,0 +1,56 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<Color x:Key="C.Brand">#007ACC</Color>
|
||||
<Color x:Key="C.Brand.Hover">#1C97EA</Color>
|
||||
<Color x:Key="C.Brand.Pressed">#005A9E</Color>
|
||||
|
||||
<Color x:Key="C.Success">#4EC9B0</Color>
|
||||
<Color x:Key="C.Warning">#FFCA28</Color>
|
||||
<Color x:Key="C.Danger">#F44747</Color>
|
||||
|
||||
<Color x:Key="C.Bg.L1">#1E1E1E</Color>
|
||||
<Color x:Key="C.Bg.L2">#252526</Color>
|
||||
<Color x:Key="C.Bg.L3">#333333</Color>
|
||||
<Color x:Key="C.Bg.L4">#3E3E42</Color>
|
||||
|
||||
<Color x:Key="C.Text.Primary">#D4D4D4</Color>
|
||||
<Color x:Key="C.Text.Secondary">#858585</Color>
|
||||
<Color x:Key="C.Text.Disable">#555555</Color>
|
||||
<Color x:Key="C.Text.Inverse">#FFFFFF</Color>
|
||||
|
||||
<Color x:Key="C.Border">#3E3E42</Color>
|
||||
<Color x:Key="C.Border.Focus">#007ACC</Color>
|
||||
|
||||
<Color x:Key="Color.Bg.L1">#1E1E1E</Color>
|
||||
<Color x:Key="Color.Bg.L2">#252526</Color>
|
||||
<Color x:Key="Color.Bg.L3">#2D2D30</Color>
|
||||
<Color x:Key="Color.Bg.L4">#3E3E42</Color>
|
||||
|
||||
<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}" />
|
||||
<SolidColorBrush x:Key="Brush.Bg.L4" Color="{StaticResource Color.Bg.L4}" />
|
||||
|
||||
<SolidColorBrush x:Key="Brush.Bg.Window" Color="{StaticResource C.Bg.L1}" />
|
||||
<SolidColorBrush x:Key="Brush.Bg.Panel" Color="{StaticResource C.Bg.L2}" />
|
||||
<SolidColorBrush x:Key="Brush.Bg.Input" Color="{StaticResource C.Bg.L3}" />
|
||||
<SolidColorBrush x:Key="Brush.Bg.Hover" Color="{StaticResource C.Bg.L4}" />
|
||||
|
||||
<SolidColorBrush x:Key="Brush.Text.Primary" Color="{StaticResource C.Text.Primary}" />
|
||||
<SolidColorBrush x:Key="Brush.Text.Secondary" Color="{StaticResource C.Text.Secondary}" />
|
||||
<SolidColorBrush x:Key="Brush.Text.Disabled" Color="{StaticResource C.Text.Disable}" />
|
||||
<SolidColorBrush x:Key="Brush.Text.Inverse" Color="{StaticResource C.Text.Inverse}" />
|
||||
|
||||
<SolidColorBrush x:Key="Brush.Brand" Color="{StaticResource C.Brand}" />
|
||||
<SolidColorBrush x:Key="Brush.Accent" Color="{StaticResource C.Brand}" />
|
||||
<SolidColorBrush x:Key="Brush.Accent.Hover" Color="{StaticResource C.Brand.Hover}" />
|
||||
<SolidColorBrush x:Key="Brush.Accent.Pressed" Color="{StaticResource C.Brand.Pressed}" />
|
||||
|
||||
<SolidColorBrush x:Key="Brush.Status.Success" Color="{StaticResource C.Success}" />
|
||||
<SolidColorBrush x:Key="Brush.Status.Warning" Color="{StaticResource C.Warning}" />
|
||||
<SolidColorBrush x:Key="Brush.Status.Danger" Color="{StaticResource C.Danger}" />
|
||||
|
||||
<SolidColorBrush x:Key="Brush.Border" Color="{StaticResource C.Border}" />
|
||||
<SolidColorBrush x:Key="Brush.Border.Focus" Color="{StaticResource C.Border.Focus}" />
|
||||
|
||||
</ResourceDictionary>
|
||||
56
SHH.CameraDashboard/Style/Themes/Colors.Light.xaml
Normal file
56
SHH.CameraDashboard/Style/Themes/Colors.Light.xaml
Normal file
@@ -0,0 +1,56 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<Color x:Key="C.Brand">#005FB8</Color>
|
||||
<Color x:Key="C.Brand.Hover">#2B88D8</Color>
|
||||
<Color x:Key="C.Brand.Pressed">#004080</Color>
|
||||
|
||||
<Color x:Key="C.Success">#107C10</Color>
|
||||
<Color x:Key="C.Warning">#D13438</Color>
|
||||
<Color x:Key="C.Danger">#A80000</Color>
|
||||
|
||||
<Color x:Key="C.Bg.L1">#FFFFFF</Color>
|
||||
<Color x:Key="C.Bg.L2">#F3F3F3</Color>
|
||||
<Color x:Key="C.Bg.L3">#FFFFFF</Color>
|
||||
<Color x:Key="C.Bg.L4">#E1E1E1</Color>
|
||||
|
||||
<Color x:Key="C.Text.Primary">#201F1E</Color>
|
||||
<Color x:Key="C.Text.Secondary">#605E5C</Color>
|
||||
<Color x:Key="C.Text.Disable">#A19F9D</Color>
|
||||
<Color x:Key="C.Text.Inverse">#FFFFFF</Color>
|
||||
|
||||
<Color x:Key="C.Border">#D2D0CE</Color>
|
||||
<Color x:Key="C.Border.Focus">#005FB8</Color>
|
||||
|
||||
<Color x:Key="Color.Bg.L1">#F5F5F7</Color>
|
||||
<Color x:Key="Color.Bg.L2">#FFFFFF</Color>
|
||||
<Color x:Key="Color.Bg.L3">#F0F0F0</Color>
|
||||
<Color x:Key="Color.Bg.L4">#E5E5E5</Color>
|
||||
|
||||
<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}" />
|
||||
<SolidColorBrush x:Key="Brush.Bg.L4" Color="{StaticResource Color.Bg.L4}" />
|
||||
|
||||
<SolidColorBrush x:Key="Brush.Bg.Window" Color="{StaticResource C.Bg.L1}" />
|
||||
<SolidColorBrush x:Key="Brush.Bg.Panel" Color="{StaticResource C.Bg.L2}" />
|
||||
<SolidColorBrush x:Key="Brush.Bg.Input" Color="{StaticResource C.Bg.L3}" />
|
||||
<SolidColorBrush x:Key="Brush.Bg.Hover" Color="{StaticResource C.Bg.L4}" />
|
||||
|
||||
<SolidColorBrush x:Key="Brush.Text.Primary" Color="{StaticResource C.Text.Primary}" />
|
||||
<SolidColorBrush x:Key="Brush.Text.Secondary" Color="{StaticResource C.Text.Secondary}" />
|
||||
<SolidColorBrush x:Key="Brush.Text.Disabled" Color="{StaticResource C.Text.Disable}" />
|
||||
<SolidColorBrush x:Key="Brush.Text.Inverse" Color="{StaticResource C.Text.Inverse}" />
|
||||
|
||||
<SolidColorBrush x:Key="Brush.Brand" Color="{StaticResource C.Brand}" />
|
||||
<SolidColorBrush x:Key="Brush.Accent" Color="{StaticResource C.Brand}" />
|
||||
<SolidColorBrush x:Key="Brush.Accent.Hover" Color="{StaticResource C.Brand.Hover}" />
|
||||
<SolidColorBrush x:Key="Brush.Accent.Pressed" Color="{StaticResource C.Brand.Pressed}" />
|
||||
|
||||
<SolidColorBrush x:Key="Brush.Status.Success" Color="{StaticResource C.Success}" />
|
||||
<SolidColorBrush x:Key="Brush.Status.Warning" Color="{StaticResource C.Warning}" />
|
||||
<SolidColorBrush x:Key="Brush.Status.Danger" Color="{StaticResource C.Danger}" />
|
||||
|
||||
<SolidColorBrush x:Key="Brush.Border" Color="{StaticResource C.Border}" />
|
||||
<SolidColorBrush x:Key="Brush.Border.Focus" Color="{StaticResource C.Border.Focus}" />
|
||||
|
||||
</ResourceDictionary>
|
||||
23
SHH.CameraDashboard/Style/Themes/Sizes.xaml
Normal file
23
SHH.CameraDashboard/Style/Themes/Sizes.xaml
Normal file
@@ -0,0 +1,23 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||
|
||||
<FontFamily x:Key="Font.Normal">Microsoft YaHei, Segoe UI</FontFamily>
|
||||
<FontFamily x:Key="Font.Code">Consolas, Cascadia Code, Courier New</FontFamily>
|
||||
|
||||
<sys:Double x:Key="Size.Font.Small">12</sys:Double>
|
||||
<sys:Double x:Key="Size.Font.Normal">14</sys:Double>
|
||||
<sys:Double x:Key="Size.Font.Large">18</sys:Double>
|
||||
<sys:Double x:Key="Size.Font.Huge">24</sys:Double>
|
||||
|
||||
<CornerRadius x:Key="Radius.Small">3</CornerRadius>
|
||||
<CornerRadius x:Key="Radius.Normal">5</CornerRadius>
|
||||
<CornerRadius x:Key="Radius.Circle">100</CornerRadius>
|
||||
|
||||
<sys:Double x:Key="Height.Input">32</sys:Double>
|
||||
<sys:Double x:Key="Height.Button">32</sys:Double>
|
||||
|
||||
<Thickness x:Key="Thickness.Border">1</Thickness>
|
||||
<Thickness x:Key="Thickness.Focus">2</Thickness>
|
||||
|
||||
</ResourceDictionary>
|
||||
378
SHH.CameraDashboard/Style/Themes/Styles.xaml
Normal file
378
SHH.CameraDashboard/Style/Themes/Styles.xaml
Normal file
@@ -0,0 +1,378 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Inverse}" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Padding" Value="15,0" />
|
||||
<Setter Property="Height" Value="{StaticResource Height.Button}" />
|
||||
<Setter Property="FontSize" Value="{StaticResource Size.Font.Normal}" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource Font.Normal}" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{StaticResource Radius.Normal}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="border" Property="Background" Value="{DynamicResource Brush.Accent.Hover}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="border" Property="Background" Value="{DynamicResource Brush.Accent.Pressed}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="border" Property="Background" Value="{DynamicResource Brush.Bg.Panel}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Disabled}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="Btn.Ghost"
|
||||
BasedOn="{StaticResource {x:Type Button}}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border}" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Bg.Hover}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Accent}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="Btn.Danger"
|
||||
BasedOn="{StaticResource {x:Type Button}}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Status.Danger}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Inverse}" />
|
||||
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Opacity" Value="0.8" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Opacity" Value="0.6" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Bg.Input}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border}" />
|
||||
<Setter Property="BorderThickness" Value="{StaticResource Thickness.Border}" />
|
||||
<Setter Property="Padding" Value="8,0" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Height" Value="{StaticResource Height.Input}" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource Font.Code}" />
|
||||
<Setter Property="CaretBrush" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TextBox">
|
||||
<Border
|
||||
x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{StaticResource Radius.Small}">
|
||||
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="border" Property="BorderBrush" Value="{DynamicResource Brush.Text.Secondary}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||
<Setter TargetName="border" Property="BorderBrush" Value="{DynamicResource Brush.Border.Focus}" />
|
||||
<Setter TargetName="border" Property="BorderThickness" Value="{StaticResource Thickness.Focus}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</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" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ListViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListViewItem">
|
||||
<Border
|
||||
x:Name="Bd"
|
||||
Padding="8,4"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<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.Panel}" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Brush.Accent}" />
|
||||
</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="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">
|
||||
<Border
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<ContentPresenter
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style x:Key="Style.ListViewItem.Table" TargetType="ListViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border}" />
|
||||
<Setter Property="MinHeight" Value="35" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListViewItem">
|
||||
<Border
|
||||
x:Name="Bd"
|
||||
Padding="4,0"
|
||||
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>
|
||||
</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="AccentBar" Property="Visibility" Value="Visible" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Accent}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="Style.TextBox.InTable"
|
||||
BasedOn="{StaticResource {x:Type TextBox}}"
|
||||
TargetType="TextBox">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Bg.Input}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="8,4" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Height" Value="30" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Style.TextBox.CodeEditor" TargetType="TextBox">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="FontFamily" Value="Consolas, Cascadia Code, Courier New, monospace" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="Padding" Value="10" />
|
||||
<Setter Property="IsReadOnly" Value="True" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Top" />
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TextBox">
|
||||
<Border Background="{TemplateBinding Background}" BorderThickness="0">
|
||||
<ScrollViewer x:Name="PART_ContentHost" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="TabRadioStyle"
|
||||
BasedOn="{StaticResource {x:Type ToggleButton}}"
|
||||
TargetType="RadioButton">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Secondary}" />
|
||||
<Setter Property="BorderThickness" Value="0,0,0,2" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="Padding" Value="15,6" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="RadioButton">
|
||||
<Border
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<ContentPresenter
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Accent}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Bg.L4}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Bg.Hover}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style x:Key="Style.TextBox.Search" TargetType="TextBox">
|
||||
<Setter Property="Background" Value="{DynamicResource Brush.Bg.Input}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Brush.Text.Primary}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Height" Value="28" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Padding" Value="25,0,5,0" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TextBox">
|
||||
<Grid>
|
||||
<Border
|
||||
x:Name="Bd"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="4">
|
||||
<Grid>
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
IsHitTestVisible="False"
|
||||
Text="🔍" />
|
||||
|
||||
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}" />
|
||||
|
||||
<TextBlock
|
||||
x:Name="Watermark"
|
||||
Margin="28,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource Brush.Text.Secondary}"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="0.5"
|
||||
Text="{TemplateBinding Tag}"
|
||||
Visibility="Collapsed" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="Text" Value="">
|
||||
<Setter TargetName="Watermark" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Brush.Accent}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsFocused" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Brush.Accent}" />
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Brush.Bg.Panel}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
Reference in New Issue
Block a user