具备界面基础功能
This commit is contained in:
29
SHH.CameraDashboard/Core/AppPaths.cs
Normal file
29
SHH.CameraDashboard/Core/AppPaths.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.IO;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public static class AppPaths
|
||||
{
|
||||
// 1. 基础目录:运行目录下的 Configs 文件夹
|
||||
public static readonly string BaseDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configs");
|
||||
|
||||
// 2. 具体的配置文件路径
|
||||
// 服务节点配置
|
||||
public static string ServiceNodesConfig => Path.Combine(BaseDir, "service_nodes.json");
|
||||
|
||||
// 用户偏好设置 (预留)
|
||||
public static string UserSettingsConfig => Path.Combine(BaseDir, "user_settings.json");
|
||||
|
||||
// 布局缓存 (预留)
|
||||
public static string LayoutCache => Path.Combine(BaseDir, "layout_cache.json");
|
||||
|
||||
// 静态构造函数:确保目录存在
|
||||
static AppPaths()
|
||||
{
|
||||
if (!Directory.Exists(BaseDir))
|
||||
{
|
||||
Directory.CreateDirectory(BaseDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
SHH.CameraDashboard/Core/EnumHelper.cs
Normal file
21
SHH.CameraDashboard/Core/EnumHelper.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public static class EnumHelper
|
||||
{
|
||||
public static string GetDescription(Enum value)
|
||||
{
|
||||
FieldInfo fi = value.GetType().GetField(value.ToString());
|
||||
|
||||
DescriptionAttribute[] attributes =
|
||||
(DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
|
||||
|
||||
if (attributes != null && attributes.Length > 0)
|
||||
return attributes[0].Description;
|
||||
else
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
7
SHH.CameraDashboard/Core/IOverlayClosable.cs
Normal file
7
SHH.CameraDashboard/Core/IOverlayClosable.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
// 定义一个简单的接口,让子 ViewModel 具备关闭能力
|
||||
public interface IOverlayClosable
|
||||
{
|
||||
event Action? RequestClose;
|
||||
}
|
||||
109
SHH.CameraDashboard/Core/JsonHelper.cs
Normal file
109
SHH.CameraDashboard/Core/JsonHelper.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON 序列化与反序列化帮助类
|
||||
/// 职责:
|
||||
/// 1. 提供全局统一的 JSON 转换配置(如驼峰命名、日期格式)。
|
||||
/// 2. 封装常见的序列化和反序列化操作。
|
||||
/// 3. 增加对 null 输入和无效 JSON 的健壮性处理。
|
||||
/// </summary>
|
||||
public static class JsonHelper
|
||||
{
|
||||
#region --- 静态配置 ---
|
||||
|
||||
/// <summary>
|
||||
/// 全局共享的 JSON 序列化设置。
|
||||
/// 静态构造函数保证其只被初始化一次。
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerSettings _settings;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 静态构造函数 ---
|
||||
|
||||
/// <summary>
|
||||
/// 静态构造函数,用于初始化全局的 JSON 序列化设置。
|
||||
/// </summary>
|
||||
static JsonHelper()
|
||||
{
|
||||
_settings = new JsonSerializerSettings
|
||||
{
|
||||
// 1. 命名策略:将 C# 的 PascalCase 属性名序列化为 JSON 的 camelCase。
|
||||
// 这是与 JavaScript/TypeScript 前端交互的标准做法。
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
|
||||
// 2. 日期格式:统一使用 "yyyy-MM-dd HH:mm:ss" 格式,避免时区和格式差异导致的问题。
|
||||
DateFormatString = "yyyy-MM-dd HH:mm:ss",
|
||||
|
||||
// 3. Null 值处理:在序列化时忽略值为 null 的属性。
|
||||
// 这可以显著减小 JSON 字符串的大小,并使生成的 JSON 更干净。
|
||||
// 例如,`{ Name = "Alice", Age = null }` 会被序列化为 `{"name":"Alice"}`。
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
|
||||
// 4. 枚举转换:将枚举值序列化为其字符串表示,而不是数字。
|
||||
// 例如,`LogLevel.Info` 会被序列化为 `"info"`,而不是 `1`。
|
||||
_settings.Converters.Add(new StringEnumConverter());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 公共方法 ---
|
||||
|
||||
/// <summary>
|
||||
/// 将对象序列化为 JSON 字符串。
|
||||
/// </summary>
|
||||
/// <param name="obj">要序列化的对象。</param>
|
||||
/// <returns>序列化后的 JSON 字符串。如果输入为 null,则返回空字符串。</returns>
|
||||
public static string Serialize(object obj)
|
||||
{
|
||||
// [健壮性] 如果输入对象为 null,返回空字符串,而不是 "null"。
|
||||
// 这可以防止在创建 HTTP 请求内容时出现意外行为。
|
||||
if (obj == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(obj, _settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 JSON 字符串反序列化为指定类型的对象。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标对象的类型(必须是引用类型)。</typeparam>
|
||||
/// <param name="json">要反序列化的 JSON 字符串。</param>
|
||||
/// <returns>成功时返回反序列化后的对象;失败或输入无效时返回 null。</returns>
|
||||
public static T? Deserialize<T>(string json) where T : class
|
||||
{
|
||||
// [健壮性] 检查输入是否为 null、空字符串或仅包含空白字符。
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// [健壮性] 处理 JSON 字符串为 "null" 的特殊情况。
|
||||
if (json.Trim() == "null")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 尝试使用预配置的设置进行反序列化。
|
||||
return JsonConvert.DeserializeObject<T>(json, _settings);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// [健壮性] 如果 JSON 格式无效,捕获异常并返回 null。
|
||||
// 这可以防止程序因一个格式错误的 JSON 字符串而崩溃。
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
65
SHH.CameraDashboard/Core/RelayCommand.cs
Normal file
65
SHH.CameraDashboard/Core/RelayCommand.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
// ===========================================================================
|
||||
// 1. 新增:非泛型 RelayCommand (支持 new RelayCommand(Method, Check))
|
||||
// ===========================================================================
|
||||
public class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action<object> _execute;
|
||||
private readonly Predicate<object> _canExecute;
|
||||
|
||||
public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public bool CanExecute(object parameter)
|
||||
{
|
||||
return _canExecute == null || _canExecute(parameter);
|
||||
}
|
||||
|
||||
public void Execute(object parameter)
|
||||
{
|
||||
_execute(parameter);
|
||||
}
|
||||
|
||||
public event EventHandler CanExecuteChanged
|
||||
{
|
||||
add => CommandManager.RequerySuggested += value;
|
||||
remove => CommandManager.RequerySuggested -= value;
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 2. 保留:泛型 RelayCommand<T> (支持 new RelayCommand<string>(Method...))
|
||||
// ===========================================================================
|
||||
public class RelayCommand<T> : ICommand
|
||||
{
|
||||
private readonly Action<T> _execute;
|
||||
private readonly Predicate<T> _canExecute;
|
||||
|
||||
public RelayCommand(Action<T> execute, Predicate<T> canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public bool CanExecute(object parameter)
|
||||
{
|
||||
return _canExecute == null || _canExecute((T)parameter);
|
||||
}
|
||||
|
||||
public void Execute(object parameter)
|
||||
{
|
||||
_execute((T)parameter);
|
||||
}
|
||||
|
||||
public event EventHandler CanExecuteChanged
|
||||
{
|
||||
add => CommandManager.RequerySuggested += value;
|
||||
remove => CommandManager.RequerySuggested -= value;
|
||||
}
|
||||
}
|
||||
92
SHH.CameraDashboard/Core/ThemeManager.cs
Normal file
92
SHH.CameraDashboard/Core/ThemeManager.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public static class ThemeManager
|
||||
{
|
||||
// 使用字典:Key=主题名, Value=路径
|
||||
private static readonly Dictionary<string, string> _themeMap = new Dictionary<string, string>
|
||||
{
|
||||
{ "Dark", "/Style/Themes/Colors.Dark.xaml" },
|
||||
{ "Light", "/Style/Themes/Colors.Light.xaml" },
|
||||
// { "Blue", "/Style/Themes/Colors.Blue.xaml" }
|
||||
};
|
||||
|
||||
// 当前主题名称
|
||||
public static string CurrentThemeName { get; private set; } = "Dark";
|
||||
|
||||
/// <summary>
|
||||
/// 指定切换到某个主题
|
||||
/// </summary>
|
||||
/// <param name="themeName">主题名称 (Dark, Light...)</param>
|
||||
public static void SetTheme(string themeName)
|
||||
{
|
||||
if (!_themeMap.ContainsKey(themeName))
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"未找到主题: {themeName}");
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyTheme(_themeMap[themeName]);
|
||||
CurrentThemeName = themeName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 循环切换下一个主题 (保留旧功能)
|
||||
/// </summary>
|
||||
public static void SwitchToNextTheme()
|
||||
{
|
||||
// 获取所有 Key 的列表
|
||||
var keys = _themeMap.Keys.ToList();
|
||||
|
||||
// 找到当前 Key 的索引
|
||||
int index = keys.IndexOf(CurrentThemeName);
|
||||
|
||||
// 计算下一个索引
|
||||
index++;
|
||||
if (index >= keys.Count) index = 0;
|
||||
|
||||
// 切换
|
||||
SetTheme(keys[index]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 私有方法:执行具体的资源替换
|
||||
/// </summary>
|
||||
private static void ApplyTheme(string themePath)
|
||||
{
|
||||
var appResources = Application.Current.Resources;
|
||||
ResourceDictionary oldDict = null;
|
||||
|
||||
// 查找旧的皮肤字典
|
||||
foreach (var dict in appResources.MergedDictionaries)
|
||||
{
|
||||
if (dict.Source != null && dict.Source.OriginalString.Contains("/Themes/Colors."))
|
||||
{
|
||||
oldDict = dict;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除旧的
|
||||
if (oldDict != null)
|
||||
{
|
||||
appResources.MergedDictionaries.Remove(oldDict);
|
||||
}
|
||||
|
||||
// 加载新的
|
||||
try
|
||||
{
|
||||
var newDict = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri(themePath, UriKind.Relative)
|
||||
};
|
||||
appResources.MergedDictionaries.Add(newDict);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"加载主题文件失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
81
SHH.CameraDashboard/Core/TouchBehavior.cs
Normal file
81
SHH.CameraDashboard/Core/TouchBehavior.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
public static class TouchBehavior
|
||||
{
|
||||
//原有 Action 属性保持不变...
|
||||
public static readonly DependencyProperty ActionProperty =
|
||||
DependencyProperty.RegisterAttached("Action", typeof(PtzAction?), typeof(TouchBehavior), new PropertyMetadata(null, OnActionChanged));
|
||||
|
||||
public static PtzAction? GetAction(DependencyObject obj) => (PtzAction?)obj.GetValue(ActionProperty);
|
||||
public static void SetAction(DependencyObject obj, PtzAction? value) => obj.SetValue(ActionProperty, value);
|
||||
|
||||
// ★★★ [新增] 私有附加属性,用于记录“是否处于按下状态” ★★★
|
||||
private static readonly DependencyProperty IsActiveProperty =
|
||||
DependencyProperty.RegisterAttached("IsActive", typeof(bool), typeof(TouchBehavior), new PropertyMetadata(false));
|
||||
|
||||
private static bool GetIsActive(DependencyObject obj) => (bool)obj.GetValue(IsActiveProperty);
|
||||
private static void SetIsActive(DependencyObject obj, bool value) => obj.SetValue(IsActiveProperty, value);
|
||||
|
||||
|
||||
private static void OnActionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is Button btn)
|
||||
{
|
||||
btn.PreviewMouseDown -= Btn_PreviewMouseDown;
|
||||
btn.PreviewMouseUp -= Btn_PreviewMouseUp;
|
||||
btn.MouseLeave -= Btn_PreviewMouseUp;
|
||||
|
||||
if (e.NewValue != null)
|
||||
{
|
||||
btn.PreviewMouseDown += Btn_PreviewMouseDown;
|
||||
btn.PreviewMouseUp += Btn_PreviewMouseUp;
|
||||
btn.MouseLeave += Btn_PreviewMouseUp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Btn_PreviewMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is Button btn && btn.DataContext is CameraPtzViewModel vm)
|
||||
{
|
||||
var action = GetAction(btn);
|
||||
if (action.HasValue && vm.StartCommand.CanExecute(action.Value))
|
||||
{
|
||||
// 1. 标记为活跃状态
|
||||
SetIsActive(btn, true);
|
||||
|
||||
// 2. 执行开始命令
|
||||
vm.StartCommand.Execute(action.Value);
|
||||
|
||||
// (可选) 捕获鼠标,防止快速拖出窗口丢失 MouseUp
|
||||
btn.CaptureMouse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Btn_PreviewMouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (sender is Button btn && btn.DataContext is CameraPtzViewModel vm)
|
||||
{
|
||||
// ★★★ [核心修改] 只有之前标记为活跃,才执行停止 ★★★
|
||||
if (GetIsActive(btn))
|
||||
{
|
||||
var action = GetAction(btn);
|
||||
if (action.HasValue && vm.StopCommand.CanExecute(action.Value))
|
||||
{
|
||||
vm.StopCommand.Execute(action.Value);
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
SetIsActive(btn, false);
|
||||
|
||||
// 释放鼠标
|
||||
btn.ReleaseMouseCapture();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user