具备界面基础功能
This commit is contained in:
177
SHH.CameraDashboard/ViewModels/CameraEditViewModel.cs
Normal file
177
SHH.CameraDashboard/ViewModels/CameraEditViewModel.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public class CameraEditViewModel : INotifyPropertyChanged
|
||||
{
|
||||
// 编辑中的数据副本
|
||||
private CameraEditInfo _editingDto;
|
||||
|
||||
// 定义一个事件,通知 View 关闭窗口(或者 UserControl 所在的宿主)
|
||||
public event Action<bool, CameraEditInfo> RequestClose;
|
||||
|
||||
public CameraEditInfo EditingDto
|
||||
{
|
||||
get => _editingDto;
|
||||
set { _editingDto = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public ICommand SaveCommand { get; }
|
||||
public ICommand CancelCommand { get; }
|
||||
|
||||
private bool _isAdd = false;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="sourceDto">如果是编辑模式,传入源对象;如果是新增,传入 null</param>
|
||||
public CameraEditViewModel(CameraEditInfo sourceDto = null)
|
||||
{
|
||||
// 初始化枚举列表
|
||||
InitBrandOptions();
|
||||
|
||||
if (sourceDto != null)
|
||||
{
|
||||
// 编辑模式:深拷贝数据,避免直接修改源对象
|
||||
EditingDto = CloneDto(sourceDto);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 新增模式:创建默认对象
|
||||
EditingDto = new CameraEditInfo
|
||||
{
|
||||
Name = "新设备",
|
||||
Port = 8000,
|
||||
ChannelIndex = 1
|
||||
};
|
||||
}
|
||||
|
||||
if (EditingDto.Id == 0)
|
||||
_isAdd = true;
|
||||
|
||||
SaveCommand = new RelayCommand(ExecuteSave, CanSave);
|
||||
CancelCommand = new RelayCommand(ExecuteCancel);
|
||||
}
|
||||
|
||||
private bool CanSave(object obj)
|
||||
{
|
||||
// 简单验证:ID必须大于0,IP不能为空
|
||||
if (EditingDto == null) return false;
|
||||
return EditingDto.Id > 0 && !string.IsNullOrWhiteSpace(EditingDto.IpAddress);
|
||||
}
|
||||
|
||||
private bool IsSaving;
|
||||
|
||||
private async void ExecuteSave(object obj)
|
||||
{
|
||||
if (IsSaving) return;
|
||||
|
||||
try
|
||||
{
|
||||
IsSaving = true; // 开启 Loading
|
||||
|
||||
bool isSuccess = false;
|
||||
|
||||
if (_isAdd)
|
||||
{
|
||||
isSuccess = await ApiClient.Instance.Cameras.CreateCameraAsync(EditingDto, "摄像头-新增");
|
||||
}
|
||||
else
|
||||
{
|
||||
isSuccess = await ApiClient.Instance.Cameras.UpdateCameraAsync(EditingDto, "摄像头-编辑");
|
||||
_isAdd = false;
|
||||
}
|
||||
|
||||
// 1. 调用 Repository (ApiClient)
|
||||
// 注意:这里调用的是我们刚才在 Repository 中定义的 UpdateCameraAsync
|
||||
if (isSuccess)
|
||||
{
|
||||
// 2. 成功:通知外部关闭面板,并刷新列表
|
||||
RequestClose?.Invoke(true, EditingDto);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 3. 失败:弹窗提示
|
||||
MessageBox.Show("保存失败,请检查服务节点日志。", "系统提示", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"保存过程中发生异常:\n{ex.Message}", "系统错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSaving = false; // 关闭 Loading
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteCancel(object obj)
|
||||
{
|
||||
// false 表示取消操作
|
||||
RequestClose?.Invoke(false, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动深拷贝 DTO,防止引用传递导致界面未点保存就修改了列表
|
||||
/// 生产环境建议用 AutoMapper 或 JSON 序列化实现
|
||||
/// </summary>
|
||||
private CameraEditInfo CloneDto(CameraEditInfo source)
|
||||
{
|
||||
return new CameraEditInfo
|
||||
{
|
||||
Id = source.Id,
|
||||
Name = source.Name,
|
||||
Brand = source.Brand,
|
||||
Location = source.Location,
|
||||
IpAddress = source.IpAddress,
|
||||
Port = source.Port,
|
||||
Username = source.Username,
|
||||
Password = source.Password,
|
||||
RenderHandle = source.RenderHandle,
|
||||
ChannelIndex = source.ChannelIndex,
|
||||
RtspPath = source.RtspPath,
|
||||
MainboardIp = source.MainboardIp,
|
||||
MainboardPort = source.MainboardPort,
|
||||
StreamType = source.StreamType,
|
||||
UseGrayscale = source.UseGrayscale,
|
||||
EnhanceImage = source.EnhanceImage,
|
||||
AllowCompress = source.AllowCompress,
|
||||
AllowExpand = source.AllowExpand,
|
||||
TargetResolution = source.TargetResolution
|
||||
};
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
protected void OnPropertyChanged([CallerMemberName] string name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
|
||||
// 1. 定义一个简单的内部类或结构,用于 ComboBox 显示
|
||||
public class BrandOption
|
||||
{
|
||||
public string Label { get; set; } // 显示的文本 (Description)
|
||||
public int Value { get; set; } // 实际的值 (int)
|
||||
}
|
||||
|
||||
// 2. 数据源属性
|
||||
public List<BrandOption> BrandOptions { get; private set; }
|
||||
|
||||
private void InitBrandOptions()
|
||||
{
|
||||
// 遍历 DeviceBrand 枚举的所有值
|
||||
BrandOptions = Enum.GetValues(typeof(DeviceBrand))
|
||||
.Cast<DeviceBrand>()
|
||||
.Select(e => new BrandOption
|
||||
{
|
||||
// 获取 Description ("海康威视")
|
||||
Label = EnumHelper.GetDescription(e),
|
||||
// 获取 int 值 (1)
|
||||
Value = (int)e
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public class CameraImageSubscriptionViewModels : INotifyPropertyChanged
|
||||
{
|
||||
private readonly WebApiCameraModel _camera;
|
||||
|
||||
public event Action RequestClose;
|
||||
|
||||
// 用于绑定 ComboBox 的类型列表
|
||||
public Dictionary<int, string> SubscriptionTypes { get; } = new Dictionary<int, string>
|
||||
{
|
||||
{ 0, "本地窗口预览" },
|
||||
{ 1, "本地录像" },
|
||||
{ 2, "句柄渲染 (嵌入)" },
|
||||
{ 3, "网络转发 (TCP/UDP)" },
|
||||
{ 4, "Web 推流" }
|
||||
};
|
||||
|
||||
// --- 数据源 ---
|
||||
public ObservableCollection<SubscriptionDto> Subscriptions { get; set; } = new ObservableCollection<SubscriptionDto>();
|
||||
|
||||
// --- 编辑表单属性 ---
|
||||
|
||||
private string _editAppId;
|
||||
public string EditAppId
|
||||
{
|
||||
get => _editAppId;
|
||||
set { _editAppId = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private int _editType;
|
||||
public int EditType
|
||||
{
|
||||
get => _editType;
|
||||
set
|
||||
{
|
||||
_editType = value;
|
||||
OnPropertyChanged();
|
||||
// 切换类型时,控制 UI 上 IP/Port 输入框的显示
|
||||
OnPropertyChanged(nameof(IsNetworkType));
|
||||
OnPropertyChanged(nameof(IsRecordType));
|
||||
}
|
||||
}
|
||||
|
||||
private int _editFps = 25;
|
||||
public int EditFps
|
||||
{
|
||||
get => _editFps;
|
||||
set { _editFps = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private string _editMemo;
|
||||
public string EditMemo
|
||||
{
|
||||
get => _editMemo;
|
||||
set { _editMemo = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
// 网络参数
|
||||
private string _editTargetIp = "127.0.0.1";
|
||||
public string EditTargetIp
|
||||
{
|
||||
get => _editTargetIp;
|
||||
set { _editTargetIp = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private int _editTargetPort = 8080;
|
||||
public int EditTargetPort
|
||||
{
|
||||
get => _editTargetPort;
|
||||
set { _editTargetPort = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
// --- 界面显隐控制 ---
|
||||
// 只有类型是 3或4 时显示网络设置
|
||||
public bool IsNetworkType => EditType == 3 || EditType == 4;
|
||||
// 只有类型是 1 时显示录像设置 (这里简化,暂时只演示网络显隐)
|
||||
public bool IsRecordType => EditType == 1;
|
||||
|
||||
|
||||
// --- 命令 ---
|
||||
public ICommand LoadCommand { get; }
|
||||
public ICommand SaveCommand { get; }
|
||||
public ICommand DeleteCommand { get; }
|
||||
public ICommand ClearFormCommand { get; }
|
||||
|
||||
public CameraImageSubscriptionViewModels(WebApiCameraModel camera)
|
||||
{
|
||||
_camera = camera;
|
||||
|
||||
LoadCommand = new RelayCommand(ExecuteLoad);
|
||||
SaveCommand = new RelayCommand(ExecuteSave);
|
||||
DeleteCommand = new RelayCommand(ExecuteDelete);
|
||||
ClearFormCommand = new RelayCommand(obj => ResetForm());
|
||||
|
||||
// 初始化加载
|
||||
ExecuteLoad(null);
|
||||
}
|
||||
|
||||
private async void ExecuteLoad(object obj)
|
||||
{
|
||||
var list = await ApiClient.Instance.Cameras.GetSubscriptionsAsync(_camera.Id);
|
||||
Subscriptions.Clear();
|
||||
foreach (var item in list)
|
||||
{
|
||||
Subscriptions.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExecuteSave(object obj)
|
||||
{
|
||||
// 1. 基础校验
|
||||
if (string.IsNullOrWhiteSpace(EditAppId))
|
||||
{
|
||||
MessageBox.Show("AppId 不能为空", "提示");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 构建 DTO
|
||||
// 注意:这里需要根据你 DTO 的定义进行类型转换和默认值处理
|
||||
var dto = new SubscriptionDto
|
||||
{
|
||||
AppId = EditAppId,
|
||||
|
||||
// ★★★ 修改:直接赋值 int,不需要强转枚举 ★★★
|
||||
Type = EditType,
|
||||
|
||||
DisplayFps = EditFps,
|
||||
Memo = EditMemo ?? string.Empty,
|
||||
|
||||
// ... 网络参数 ...
|
||||
TargetIp = IsNetworkType ? (EditTargetIp ?? string.Empty) : string.Empty,
|
||||
TargetPort = IsNetworkType ? EditTargetPort : 0,
|
||||
|
||||
// ★★★ 修改:直接赋值 int ★★★
|
||||
Protocol = 0, // 0 代表 TCP
|
||||
|
||||
// ... 其他参数 ...
|
||||
Handle = string.Empty,
|
||||
SavePath = ((SubscriptionType)EditType == SubscriptionType.LocalRecord) ? @"C:\Temp\Video" : string.Empty,
|
||||
RecordDuration = 0
|
||||
};
|
||||
|
||||
// 3. 发送请求
|
||||
bool success = await ApiClient.Instance.Cameras.UpdateSubscriptionAsync(_camera.Id, dto);
|
||||
|
||||
if (success)
|
||||
{
|
||||
MessageBox.Show("保存成功", "提示");
|
||||
ExecuteLoad(null); // 刷新列表
|
||||
// ResetForm(); // 可选:保存后清空表单
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show("保存失败,请检查网络或参数。", "错误");
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExecuteDelete(object parameter)
|
||||
{
|
||||
// 支持两种方式删除:
|
||||
// 1. 传入 parameter (点击列表中的删除按钮)
|
||||
// 2. 根据当前表单的 AppId (如果在编辑模式)
|
||||
|
||||
string appIdToDelete = parameter as string;
|
||||
|
||||
if (string.IsNullOrEmpty(appIdToDelete)) return;
|
||||
|
||||
if (MessageBox.Show($"确定要停止并注销 [{appIdToDelete}] 吗?", "确认", MessageBoxButton.YesNo) != MessageBoxResult.Yes)
|
||||
return;
|
||||
|
||||
// 构造删除请求:关键是 DisplayFps = 0
|
||||
var dto = new SubscriptionDto
|
||||
{
|
||||
AppId = appIdToDelete,
|
||||
Type = 0,
|
||||
DisplayFps = 0, // <--- 核心:0 代表注销
|
||||
Memo = "User Deleted"
|
||||
};
|
||||
|
||||
bool success = await ApiClient.Instance.Cameras.UpdateSubscriptionAsync(_camera.Id, dto);
|
||||
if (success)
|
||||
{
|
||||
ExecuteLoad(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetForm()
|
||||
{
|
||||
// 生成一个随机 AppId 方便测试
|
||||
EditAppId = $"Client_{DateTime.Now:HHmmss}";
|
||||
EditType = 0;
|
||||
EditFps = 25;
|
||||
EditMemo = "";
|
||||
EditTargetIp = "127.0.0.1";
|
||||
EditTargetPort = 8080;
|
||||
}
|
||||
|
||||
// --- INotifyPropertyChanged 实现 (略,使用你的基类或标准实现) ---
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
protected void OnPropertyChanged([CallerMemberName] string name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
// 1. 添加一个字段
|
||||
private SubscriptionDto _selectedSubscription;
|
||||
|
||||
// 2. 添加 SelectedSubscription 属性
|
||||
public SubscriptionDto SelectedSubscription
|
||||
{
|
||||
get => _selectedSubscription;
|
||||
set
|
||||
{
|
||||
// 如果两次选中同一个,不做处理
|
||||
if (_selectedSubscription == value) return;
|
||||
|
||||
_selectedSubscription = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
// ★★★ 核心逻辑:一旦选中项发生变化,就填充表单 ★★★
|
||||
if (_selectedSubscription != null)
|
||||
{
|
||||
FillForm(_selectedSubscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 编写填充表单的辅助方法
|
||||
private void FillForm(SubscriptionDto dto)
|
||||
{
|
||||
// 这里我们将 DTO 的值“复制”到编辑属性中
|
||||
// 这样做的好处是:修改下方文本框时,不会立即改变列表里的显示,只有点击“保存”后才刷新
|
||||
EditAppId = dto.AppId;
|
||||
EditType = dto.Type; // 之前改成了 int,直接赋值即可
|
||||
EditFps = dto.DisplayFps; // 注意:后端返回可能是 targetFps,前端 DTO 映射已修复
|
||||
EditMemo = dto.Memo;
|
||||
|
||||
// 网络参数回显
|
||||
EditTargetIp = dto.TargetIp;
|
||||
EditTargetPort = dto.TargetPort;
|
||||
|
||||
// 触发一下 UI 状态刷新(比如网络参数的显隐)
|
||||
OnPropertyChanged(nameof(IsNetworkType));
|
||||
}
|
||||
}
|
||||
}
|
||||
369
SHH.CameraDashboard/ViewModels/CameraImgProcViewModel.cs
Normal file
369
SHH.CameraDashboard/ViewModels/CameraImgProcViewModel.cs
Normal file
@@ -0,0 +1,369 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public class CameraImgProcViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private readonly WebApiCameraModel _sourceCamera;
|
||||
private readonly CameraEditInfo _editInfo;
|
||||
|
||||
public event Action RequestClose;
|
||||
|
||||
public string Title => $"{_sourceCamera.Name} - 图像处理";
|
||||
public string SourceResolutionText => $"{_sourceCamera.Width} x {_sourceCamera.Height}";
|
||||
|
||||
// --- 状态控制属性 ---
|
||||
|
||||
// 源是否是高清 (>= 1080P)
|
||||
public bool IsSourceHighRes => _sourceCamera.Width >= 1920 || _sourceCamera.Height >= 1080;
|
||||
|
||||
// 是否允许勾选“允许放大” (UI 绑定 IsEnabled)
|
||||
public bool CanCheckExpand => !IsSourceHighRes;
|
||||
|
||||
// 滑块是否可用 (只有开启了缩小 或 开启了放大,才允许拖动)
|
||||
// ★★★ 修正:使用 AllowEnlarge ★★★
|
||||
public bool IsSliderEnabled => AllowShrink || AllowEnlarge;
|
||||
|
||||
// 允许缩小
|
||||
public bool AllowShrink
|
||||
{
|
||||
get => _editInfo.AllowShrink;
|
||||
set
|
||||
{
|
||||
if (_editInfo.AllowShrink != value)
|
||||
{
|
||||
_editInfo.AllowShrink = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsSliderEnabled));
|
||||
ValidateAndSyncSlider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ★★★ 修正:属性名为 AllowEnlarge,对应 Model 中的 AllowEnlarge ★★★
|
||||
public bool AllowEnlarge
|
||||
{
|
||||
get => _editInfo.AllowEnlarge;
|
||||
set
|
||||
{
|
||||
if (_editInfo.AllowEnlarge != value)
|
||||
{
|
||||
_editInfo.AllowEnlarge = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsSliderEnabled));
|
||||
// 开关变化时,重新校验当前尺寸是否合法
|
||||
ValidateAndSyncSlider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图像增强
|
||||
public bool EnhanceImage
|
||||
{
|
||||
get => _editInfo.EnhanceImage;
|
||||
set
|
||||
{
|
||||
if (_editInfo.EnhanceImage != value)
|
||||
{
|
||||
_editInfo.EnhanceImage = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 分辨率与缩放属性 ---
|
||||
|
||||
private int _targetWidth;
|
||||
public int TargetWidth
|
||||
{
|
||||
get => _targetWidth;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _targetWidth, value))
|
||||
{
|
||||
if (IsRatioLocked && _sourceCamera.Width > 0)
|
||||
{
|
||||
_targetHeight = (int)((double)value / _sourceCamera.Width * _sourceCamera.Height);
|
||||
OnPropertyChanged(nameof(TargetHeight));
|
||||
}
|
||||
ValidateAndSyncSlider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int _targetHeight;
|
||||
public int TargetHeight
|
||||
{
|
||||
get => _targetHeight;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _targetHeight, value))
|
||||
{
|
||||
if (IsRatioLocked && _sourceCamera.Height > 0)
|
||||
{
|
||||
_targetWidth = (int)((double)value / _sourceCamera.Height * _sourceCamera.Width);
|
||||
OnPropertyChanged(nameof(TargetWidth));
|
||||
}
|
||||
ValidateAndSyncSlider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isRatioLocked = true;
|
||||
public bool IsRatioLocked
|
||||
{
|
||||
get => _isRatioLocked;
|
||||
set => SetProperty(ref _isRatioLocked, value);
|
||||
}
|
||||
|
||||
private double _scalePercent = 100;
|
||||
public double ScalePercent
|
||||
{
|
||||
get => _scalePercent;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _scalePercent, value))
|
||||
{
|
||||
UpdateResolutionByScale(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [新增] 亮度调节属性
|
||||
public int Brightness
|
||||
{
|
||||
get => _editInfo.Brightness;
|
||||
set
|
||||
{
|
||||
if (_editInfo.Brightness != value)
|
||||
{
|
||||
_editInfo.Brightness = value;
|
||||
OnPropertyChanged();
|
||||
// 如果需要,可以在这里限制范围,例如 0-100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public ICommand SaveCommand { get; }
|
||||
public ICommand CancelCommand { get; }
|
||||
public ICommand ApplyPresetCommand { get; }
|
||||
|
||||
public CameraImgProcViewModel(WebApiCameraModel source, CameraEditInfo detail)
|
||||
{
|
||||
_sourceCamera = source;
|
||||
|
||||
// 此时 detail 可能是不完整的,先赋值防止空引用
|
||||
_editInfo = detail ?? new CameraEditInfo { Id = source.Id };
|
||||
|
||||
SaveCommand = new RelayCommand(ExecuteSave);
|
||||
CancelCommand = new RelayCommand(o => RequestClose?.Invoke());
|
||||
ApplyPresetCommand = new RelayCommand(ExecutePreset);
|
||||
|
||||
InitializeState();
|
||||
|
||||
LoadDataAsync();
|
||||
}
|
||||
|
||||
// 2. 实现加载逻辑
|
||||
private async void LoadDataAsync()
|
||||
{
|
||||
// 调用 Repository
|
||||
var remoteInfo = await ApiClient.Instance.Cameras.GetImageProcessingAsync(_sourceCamera.Id);
|
||||
|
||||
if (remoteInfo != null)
|
||||
{
|
||||
// --- A. 更新 ViewModel 的开关属性 ---
|
||||
// 注意:Setter 会自动触发 OnPropertyChanged 和 ValidateAndSyncSlider
|
||||
AllowShrink = remoteInfo.AllowShrink;
|
||||
AllowEnlarge = remoteInfo.AllowEnlarge; // 这里会自动更新 IsSliderEnabled
|
||||
EnhanceImage = remoteInfo.EnhanceImage;
|
||||
|
||||
// --- B. 更新分辨率 (解析 "W x H") ---
|
||||
if (!string.IsNullOrWhiteSpace(remoteInfo.TargetResolution))
|
||||
{
|
||||
var parts = remoteInfo.TargetResolution.Split('x');
|
||||
if (parts.Length == 2 && int.TryParse(parts[0], out int w) && int.TryParse(parts[1], out int h))
|
||||
{
|
||||
// 赋值 TargetWidth/Height 会自动触发 ValidateAndSyncSlider
|
||||
// 从而更新 ScalePercent 和 滑块位置
|
||||
TargetWidth = w;
|
||||
TargetHeight = h;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeState()
|
||||
{
|
||||
// 1. 强制规则:如果源分辨率 >= 1080P,不允许放大
|
||||
if (IsSourceHighRes)
|
||||
{
|
||||
_editInfo.AllowEnlarge = false; // ★★★ 修正引用 ★★★
|
||||
}
|
||||
|
||||
// 2. 检查历史设置
|
||||
// ★★★ 修正引用 ★★★
|
||||
bool hasPreviousSettings = _editInfo.AllowShrink || _editInfo.AllowEnlarge;
|
||||
|
||||
if (hasPreviousSettings)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_editInfo.TargetResolution))
|
||||
{
|
||||
var parts = _editInfo.TargetResolution.ToLower().Split('x');
|
||||
if (parts.Length == 2 && int.TryParse(parts[0], out int w) && int.TryParse(parts[1], out int h))
|
||||
{
|
||||
_targetWidth = w;
|
||||
_targetHeight = h;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 默认逻辑
|
||||
if (_sourceCamera.Width > 1280)
|
||||
{
|
||||
_targetWidth = 1280;
|
||||
_targetHeight = _sourceCamera.Width > 0
|
||||
? (int)((double)1280 / _sourceCamera.Width * _sourceCamera.Height)
|
||||
: 720;
|
||||
}
|
||||
else
|
||||
{
|
||||
_targetWidth = _sourceCamera.Width;
|
||||
_targetHeight = _sourceCamera.Height;
|
||||
}
|
||||
}
|
||||
|
||||
ValidateAndSyncSlider();
|
||||
}
|
||||
|
||||
private void UpdateResolutionByScale(double percent)
|
||||
{
|
||||
if (_sourceCamera.Width <= 0 || _sourceCamera.Height <= 0) return;
|
||||
|
||||
double w = _sourceCamera.Width * (percent / 100.0);
|
||||
double h = _sourceCamera.Height * (percent / 100.0);
|
||||
|
||||
_targetWidth = (int)w;
|
||||
_targetHeight = (int)h;
|
||||
OnPropertyChanged(nameof(TargetWidth));
|
||||
OnPropertyChanged(nameof(TargetHeight));
|
||||
}
|
||||
|
||||
private void ValidateAndSyncSlider()
|
||||
{
|
||||
if (_sourceCamera.Width <= 0) return;
|
||||
|
||||
// ★★★ 修正引用:使用 AllowEnlarge ★★★
|
||||
int maxWidth = AllowEnlarge ? 1920 : _sourceCamera.Width;
|
||||
int maxHeight = AllowEnlarge ? 1080 : _sourceCamera.Height;
|
||||
int minWidth = 160;
|
||||
|
||||
bool changed = false;
|
||||
|
||||
// 规则A:如果未开启放大,且当前 > 源 -> 强制回退到源
|
||||
// ★★★ 修正引用 ★★★
|
||||
if (!AllowEnlarge && (_targetWidth > _sourceCamera.Width))
|
||||
{
|
||||
_targetWidth = _sourceCamera.Width;
|
||||
_targetHeight = _sourceCamera.Height;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!AllowShrink && (_targetWidth < _sourceCamera.Width))
|
||||
{
|
||||
_targetWidth = _sourceCamera.Width;
|
||||
_targetHeight = _sourceCamera.Height;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (_targetWidth > 1920) { _targetWidth = 1920; changed = true; }
|
||||
if (_targetHeight > 1080) { _targetHeight = 1080; changed = true; }
|
||||
if (_targetWidth < minWidth) { _targetWidth = minWidth; changed = true; }
|
||||
|
||||
if (changed)
|
||||
{
|
||||
OnPropertyChanged(nameof(TargetWidth));
|
||||
OnPropertyChanged(nameof(TargetHeight));
|
||||
}
|
||||
|
||||
double currentPercent = ((double)_targetWidth / _sourceCamera.Width) * 100.0;
|
||||
|
||||
if (Math.Abs(_scalePercent - currentPercent) > 0.1)
|
||||
{
|
||||
_scalePercent = currentPercent;
|
||||
OnPropertyChanged(nameof(ScalePercent));
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecutePreset(object parameter)
|
||||
{
|
||||
if (parameter is string preset)
|
||||
{
|
||||
var parts = preset.Split('x');
|
||||
if (parts.Length != 2) return;
|
||||
|
||||
if (int.TryParse(parts[0], out int w) && int.TryParse(parts[1], out int h))
|
||||
{
|
||||
if (w > _sourceCamera.Width)
|
||||
{
|
||||
if (IsSourceHighRes)
|
||||
{
|
||||
MessageBox.Show("当前源分辨率已超过或等于 1080P,不支持放大。", "提示");
|
||||
return;
|
||||
}
|
||||
AllowEnlarge = true; // ★★★ 修正引用 ★★★
|
||||
}
|
||||
else if (w < _sourceCamera.Width)
|
||||
{
|
||||
AllowShrink = true;
|
||||
}
|
||||
|
||||
TargetWidth = w;
|
||||
TargetHeight = h;
|
||||
IsRatioLocked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExecuteSave(object obj)
|
||||
{
|
||||
// 1. 准备数据 (拼接分辨率字符串)
|
||||
_editInfo.TargetResolution = $"{TargetWidth}x{TargetHeight}";
|
||||
|
||||
// 2. 调用专用的 Processing 接口
|
||||
bool success = await ApiClient.Instance.Cameras.UpdateImageProcessingAsync(_editInfo); // <--- 换成这就行
|
||||
|
||||
if (success)
|
||||
{
|
||||
AppGlobal.RequestRefresh();
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show("保存图像处理配置失败,请检查网络或后端日志。", "错误");
|
||||
}
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string name = null)
|
||||
{
|
||||
if (Equals(field, value)) return false;
|
||||
field = value;
|
||||
OnPropertyChanged(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
145
SHH.CameraDashboard/ViewModels/CameraItemTopViewModel.cs
Normal file
145
SHH.CameraDashboard/ViewModels/CameraItemTopViewModel.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard;
|
||||
|
||||
public class CameraItemTopViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private WebApiCameraModel _camera;
|
||||
|
||||
/// <summary>
|
||||
/// 当前选中的摄像头数据
|
||||
/// </summary>
|
||||
public WebApiCameraModel Camera
|
||||
{
|
||||
get => _camera;
|
||||
set
|
||||
{
|
||||
if (_camera != value)
|
||||
{
|
||||
_camera = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HasSelection)); // 通知界面是否有选中项
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand EditDeviceCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 辅助属性:用于界面判断是否显示内容
|
||||
/// </summary>
|
||||
public bool HasSelection => Camera != null;
|
||||
|
||||
public ICommand TogglePlayCommand { get; }
|
||||
|
||||
public ICommand PtzCommand { get; } // [新增]
|
||||
|
||||
// [新增] 图像处理命令
|
||||
public ICommand ImageProcessCommand { get; }
|
||||
|
||||
// [新增] 图像订阅命令
|
||||
public ICommand ImageSubscribeCommand { get; }
|
||||
|
||||
public CameraItemTopViewModel()
|
||||
{
|
||||
// 绑定命令到执行方法
|
||||
TogglePlayCommand = new RelayCommand<object>(async _ => await ExecuteTogglePlayAsync());
|
||||
|
||||
// 2. 初始化命令
|
||||
EditDeviceCommand = new RelayCommand<object>(ExecuteEditDevice);
|
||||
|
||||
PtzCommand = new RelayCommand(ExecutePtz);
|
||||
|
||||
DeleteCommand = new RelayCommand(ExecuteDelete);
|
||||
|
||||
// 初始化新命令
|
||||
ImageProcessCommand = new RelayCommand(ExecuteImageProcess);
|
||||
ImageSubscribeCommand = new RelayCommand(ExecuteImageSubscribe);
|
||||
}
|
||||
|
||||
// [新增] 执行图像处理
|
||||
private void ExecuteImageProcess(object obj)
|
||||
{
|
||||
if (Camera == null) return;
|
||||
|
||||
// 触发全局事件,打开右侧面板
|
||||
AppGlobal.RequestImgProc(this.Camera);
|
||||
}
|
||||
|
||||
// [新增] 执行图像订阅
|
||||
private void ExecuteImageSubscribe(object obj)
|
||||
{
|
||||
if (Camera == null) return;
|
||||
|
||||
// 不再直接 MessageBox,而是像 Edit/Ptz 一样发出全局请求
|
||||
// 这会将操作权交给 MainViewModel,由它在右侧面板加载 View
|
||||
AppGlobal.RequestSubscription(this.Camera);
|
||||
}
|
||||
|
||||
private void ExecutePtz(object obj)
|
||||
{
|
||||
// 这里可以加个判断,比如 Brand=RTSP 或 File 可能不支持云台
|
||||
// if (Model.Brand == (int)DeviceBrand.File) return;
|
||||
|
||||
AppGlobal.RequestPtz(this.Camera);
|
||||
}
|
||||
|
||||
private async Task ExecuteTogglePlayAsync()
|
||||
{
|
||||
if (Camera == null) return;
|
||||
|
||||
// 1. 物理离线检查
|
||||
if (!Camera.IsPhysicalOnline)
|
||||
{
|
||||
// 提示:设备断网,无法发送指令
|
||||
MessageBox.Show("设备物理离线,无法操作");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 发送控制指令
|
||||
var useServiceNode = AppGlobal.UseServiceNode;
|
||||
if (useServiceNode != null)
|
||||
{
|
||||
bool isPlaying = Camera.Status.Equals("Playing");
|
||||
bool isSuccess = await ApiClient.Instance.Cameras.ControlPowerAsync(
|
||||
Camera.Id,
|
||||
!isPlaying,
|
||||
"设备面板"
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行编辑逻辑
|
||||
/// </summary>
|
||||
private void ExecuteEditDevice(object obj)
|
||||
{
|
||||
if (Camera == null) return;
|
||||
|
||||
AppGlobal.RequestEdit(Camera);
|
||||
}
|
||||
|
||||
// 2. 自身的删除命令 (不需要参数,因为我手里有 Model)
|
||||
public ICommand DeleteCommand { get; }
|
||||
|
||||
private void ExecuteDelete(object obj)
|
||||
{
|
||||
// 直接把手里的数据发出去
|
||||
AppGlobal.RequestDelete(this.Camera);
|
||||
}
|
||||
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string name = "")
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
259
SHH.CameraDashboard/ViewModels/CameraListViewModel.cs
Normal file
259
SHH.CameraDashboard/ViewModels/CameraListViewModel.cs
Normal file
@@ -0,0 +1,259 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading; // 引入 DispatcherTimer 命名空间
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public class CameraListViewModel : INotifyPropertyChanged
|
||||
{
|
||||
// 存储从 API 获取的所有原始数据
|
||||
private readonly ObservableCollection<WebApiCameraModel> _allCameras;
|
||||
|
||||
// 提供给界面绑定的视图 (支持搜索过滤)
|
||||
public ICollectionView FilteredCameras { get; private set; }
|
||||
|
||||
#region --- 状态属性 ---
|
||||
|
||||
private bool _isLoading;
|
||||
public bool IsLoading
|
||||
{
|
||||
get => _isLoading;
|
||||
set { _isLoading = value; OnPropertyChanged(nameof(IsLoading)); }
|
||||
}
|
||||
|
||||
private WebApiCameraModel _selectedCamera;
|
||||
public WebApiCameraModel SelectedCamera
|
||||
{
|
||||
get => _selectedCamera;
|
||||
set { _selectedCamera = value; OnPropertyChanged(nameof(SelectedCamera)); }
|
||||
}
|
||||
|
||||
private string _searchText;
|
||||
public string SearchText
|
||||
{
|
||||
get => _searchText;
|
||||
set
|
||||
{
|
||||
_searchText = value;
|
||||
OnPropertyChanged(nameof(SearchText));
|
||||
// 输入文字时立即触发过滤
|
||||
FilteredCameras.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 下拉框节点选择 ---
|
||||
|
||||
// 下拉框的数据源
|
||||
public ObservableCollection<ServiceNodeModel> NodeOptions
|
||||
=> AppGlobal.ServiceNodes;
|
||||
|
||||
private ServiceNodeModel _selectedNode;
|
||||
public ServiceNodeModel SelectedNode
|
||||
{
|
||||
get => _selectedNode;
|
||||
set
|
||||
{
|
||||
if (_selectedNode != value)
|
||||
{
|
||||
_selectedNode = value;
|
||||
OnPropertyChanged(nameof(SelectedNode));
|
||||
|
||||
AppGlobal.UseServiceNode = value;
|
||||
|
||||
// 切换节点时,自动重新加载数据
|
||||
_ = LoadDataAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public ICommand RefreshCommand { get; }
|
||||
|
||||
public CameraListViewModel()
|
||||
{
|
||||
_allCameras = new ObservableCollection<WebApiCameraModel>();
|
||||
|
||||
// 初始化 CollectionView
|
||||
FilteredCameras = CollectionViewSource.GetDefaultView(_allCameras);
|
||||
FilteredCameras.Filter = FilterCameras;
|
||||
|
||||
AddCameraCommand = new RelayCommand(ExecuteAddCamera);
|
||||
DeleteCommand = new RelayCommand(ExecuteDelete);
|
||||
|
||||
RefreshCommand = new RelayCommand<object>(async _ => await LoadDataAsync());
|
||||
|
||||
// --- 新增:定时器 ---
|
||||
// 使用 DispatcherTimer 确保在 UI 线程执行,避免跨线程操作集合报错
|
||||
var timer = new DispatcherTimer();
|
||||
timer.Interval = TimeSpan.FromSeconds(2); // 这里设置间隔,例如 2 秒
|
||||
timer.Tick += async (s, e) => await LoadDataAsync();
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 核心:加载数据逻辑 (无刷版本)
|
||||
/// </summary>
|
||||
public async Task LoadDataAsync()
|
||||
{
|
||||
// 如果正在加载中,跳过本次(防止并发)
|
||||
if (IsLoading) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 【改动1】只有第一次加载(列表为空)时才显示 Loading 转圈
|
||||
// 这样定时刷新时,界面就不会弹出遮罩层
|
||||
if (_allCameras.Count == 0) IsLoading = true;
|
||||
|
||||
// 确定要查询的目标节点列表
|
||||
var targetNodes = new List<ServiceNodeModel>();
|
||||
|
||||
if (SelectedNode == null || SelectedNode.ServiceNodeIp == "ALL")
|
||||
{
|
||||
if (AppGlobal.ServiceNodes != null)
|
||||
targetNodes.AddRange(AppGlobal.ServiceNodes);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetNodes.Add(SelectedNode);
|
||||
}
|
||||
|
||||
if (targetNodes.Count == 0)
|
||||
{
|
||||
// 如果没有节点,清空列表
|
||||
if (_allCameras.Count > 0) _allCameras.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建并发任务列表
|
||||
var tasks = new List<Task<List<WebApiCameraModel>?>>();
|
||||
|
||||
foreach (var node in targetNodes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(node.ServiceNodeIp) || string.IsNullOrWhiteSpace(node.ServiceNodePort))
|
||||
continue;
|
||||
|
||||
tasks.Add(ApiClient.Instance.Cameras.GetListByAddressAsync(node.ServiceNodeIp, node.ServiceNodePort, "左侧列表刷新"));
|
||||
}
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
// 【改动2】先将所有新数据收集到一个临时列表中,不要直接操作界面集合
|
||||
var newLatestData = new List<WebApiCameraModel>();
|
||||
foreach (var list in results)
|
||||
{
|
||||
if (list != null)
|
||||
{
|
||||
newLatestData.AddRange(list);
|
||||
}
|
||||
}
|
||||
|
||||
// 【改动3】执行无刷更新 (智能合并)
|
||||
if (newLatestData.Count > 0)
|
||||
UpdateCollectionNoFlash(newLatestData);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 辅助方法:比对新旧数据,只更新变化的,实现“无刷”
|
||||
/// </summary>
|
||||
private void UpdateCollectionNoFlash(List<WebApiCameraModel> newData)
|
||||
{
|
||||
// 1. 删除:界面上有,但新数据里没有的 (说明设备断开或移除了)
|
||||
// 使用 ToList() 避免在遍历时修改集合报错
|
||||
var itemsToRemove = _allCameras.Where(old => !newData.Any(n => n.Id == old.Id)).ToList();
|
||||
foreach (var item in itemsToRemove)
|
||||
{
|
||||
_allCameras.Remove(item);
|
||||
}
|
||||
|
||||
// 2. 更新或新增
|
||||
foreach (var newCam in newData)
|
||||
{
|
||||
// 尝试在界面列表中找这个 ID
|
||||
var oldCam = _allCameras.FirstOrDefault(x => x.Id == newCam.Id);
|
||||
|
||||
if (oldCam != null)
|
||||
{
|
||||
// --- [更新] ---
|
||||
// 找到老对象,手动更新它的属性。
|
||||
// 注意:你的 WebApiCameraModel 必须实现了 INotifyPropertyChanged
|
||||
// 这里的赋值才会让界面文字发生变化,否则界面不动。
|
||||
oldCam.Name = newCam.Name;
|
||||
oldCam.IpAddress = newCam.IpAddress;
|
||||
oldCam.Brand = newCam.Brand;
|
||||
|
||||
oldCam.Status = newCam.Status;
|
||||
oldCam.IsOnline = newCam.IsOnline;
|
||||
oldCam.IsPhysicalOnline = newCam.IsPhysicalOnline;
|
||||
oldCam.IsRunning = newCam.IsRunning;
|
||||
|
||||
oldCam.Width = newCam.Width;
|
||||
oldCam.Height = newCam.Height;
|
||||
oldCam.RealFps = newCam.RealFps;
|
||||
oldCam.TotalFrames = newCam.TotalFrames;
|
||||
oldCam.StreamType = newCam.StreamType;
|
||||
|
||||
// 如果有其他变化的字段(如帧率),继续在这里赋值...
|
||||
}
|
||||
else
|
||||
{
|
||||
// --- [新增] ---
|
||||
// 没找到,说明是新上线的设备,添加到列表
|
||||
_allCameras.Add(newCam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 本地搜索过滤逻辑
|
||||
/// </summary>
|
||||
private bool FilterCameras(object obj)
|
||||
{
|
||||
if (obj is WebApiCameraModel camera)
|
||||
{
|
||||
// 搜索框为空显示所有
|
||||
if (string.IsNullOrWhiteSpace(SearchText)) return true;
|
||||
|
||||
string lowerSearch = SearchText.ToLower();
|
||||
|
||||
// 匹配 名称、IP 或 品牌
|
||||
return (camera.Name?.ToLower().Contains(lowerSearch) == true) ||
|
||||
(camera.IpAddress?.Contains(lowerSearch) == true) ||
|
||||
(camera.Brand?.ToLower().Contains(lowerSearch) == true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public ICommand AddCameraCommand { get; }
|
||||
|
||||
private void ExecuteAddCamera(object obj)
|
||||
{
|
||||
// 简单直接:通知全世界“我要添加摄像头!”
|
||||
AppGlobal.RequestAdd();
|
||||
}
|
||||
|
||||
// [新增] 删除命令
|
||||
public ICommand DeleteCommand { get; }
|
||||
|
||||
private void ExecuteDelete(object obj)
|
||||
{
|
||||
// 这里的 obj 就是从界面传过来的 WebApiCameraModel
|
||||
if (obj is WebApiCameraModel camera)
|
||||
{
|
||||
AppGlobal.RequestDelete(camera);
|
||||
}
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
protected void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
}
|
||||
133
SHH.CameraDashboard/ViewModels/CameraPtzViewModel.cs
Normal file
133
SHH.CameraDashboard/ViewModels/CameraPtzViewModel.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
public class CameraPtzViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private readonly WebApiCameraModel _camera;
|
||||
|
||||
public string Title => $"{_camera.Name} - 云台控制";
|
||||
|
||||
public ICommand StartCommand { get; }
|
||||
public ICommand StopCommand { get; }
|
||||
|
||||
// 用于关闭面板
|
||||
public ICommand CloseCommand { get; }
|
||||
|
||||
public ICommand SyncTimeCommand { get; }
|
||||
public ICommand RebootCommand { get; }
|
||||
|
||||
// ★★★ [新增] 关闭请求事件,通知 MainViewModel 关闭面板
|
||||
public event Action? RequestClose;
|
||||
|
||||
public CameraPtzViewModel(WebApiCameraModel camera)
|
||||
{
|
||||
_camera = camera;
|
||||
|
||||
// 使用非泛型 RelayCommand,但在 Execute 中强转参数
|
||||
StartCommand = new RelayCommand(ExecuteStart);
|
||||
StopCommand = new RelayCommand(ExecuteStop);
|
||||
|
||||
// 关闭逻辑:可以简单地请求刷新或置空 MainVM 的属性(这里暂用刷新模拟)
|
||||
CloseCommand = new RelayCommand(o => RequestClose?.Invoke());
|
||||
|
||||
// [新增] 初始化运维命令
|
||||
SyncTimeCommand = new RelayCommand(ExecuteSyncTime);
|
||||
RebootCommand = new RelayCommand(ExecuteReboot);
|
||||
}
|
||||
// [新增] 执行校时
|
||||
private async void ExecuteSyncTime(object obj)
|
||||
{
|
||||
if (_camera == null) return;
|
||||
|
||||
// 可以加个 IsBusy 状态防止连点
|
||||
bool success = await ApiClient.Instance.Cameras.SyncTimeAsync(_camera.Id);
|
||||
|
||||
if (success)
|
||||
MessageBox.Show("校时指令已发送成功!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
else
|
||||
MessageBox.Show("校时失败,请检查网络或日志。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
|
||||
// [新增] 执行重启
|
||||
private async void ExecuteReboot(object obj)
|
||||
{
|
||||
if (_camera == null) return;
|
||||
|
||||
// 重启是高风险操作,必须弹窗确认
|
||||
var result = MessageBox.Show(
|
||||
$"确定要重启设备 \"{_camera.Name}\" 吗?\n\n注意:\n1. 设备将断开连接约 60 秒。\n2. 录像可能会中断。",
|
||||
"确认重启",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Warning);
|
||||
|
||||
if (result != MessageBoxResult.Yes) return;
|
||||
|
||||
bool success = await ApiClient.Instance.Cameras.RebootCameraAsync(_camera.Id);
|
||||
|
||||
if (success)
|
||||
MessageBox.Show("重启指令已发送,设备正在重启中...", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
else
|
||||
MessageBox.Show("重启指令发送失败。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
|
||||
private async void ExecuteStart(object parameter)
|
||||
{
|
||||
if (parameter is PtzAction action)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"PTZ START: {action}");
|
||||
|
||||
// 模式 A: 雨刷 (Wiper) -> 点动模式 (持续 5秒)
|
||||
if (action == PtzAction.Wiper)
|
||||
{
|
||||
await SendPtzRequest(new PtzControlDto
|
||||
{
|
||||
Action = action,
|
||||
Duration = 5000
|
||||
});
|
||||
}
|
||||
// 模式 B: 持续动作 (Up/Down/Zoom...) -> Start
|
||||
else
|
||||
{
|
||||
await SendPtzRequest(new PtzControlDto
|
||||
{
|
||||
Action = action,
|
||||
Stop = false,
|
||||
Speed = 5
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExecuteStop(object parameter)
|
||||
{
|
||||
if (parameter is PtzAction action)
|
||||
{
|
||||
// 雨刷是自动复位的,不需要发停止
|
||||
if (action == PtzAction.Wiper) return;
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"PTZ STOP: {action}");
|
||||
|
||||
// 发送停止指令:Action=动作名, Stop=true
|
||||
await SendPtzRequest(new PtzControlDto
|
||||
{
|
||||
Action = action,
|
||||
Stop = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendPtzRequest(PtzControlDto payload)
|
||||
{
|
||||
if (_camera == null) return;
|
||||
await ApiClient.Instance.Cameras.PtzControlAsync(_camera.Id, payload);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
protected void OnPropertyChanged([CallerMemberName] string name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
}
|
||||
188
SHH.CameraDashboard/ViewModels/WizardClientsVewModel.cs
Normal file
188
SHH.CameraDashboard/ViewModels/WizardClientsVewModel.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// 客户端配置向导的 ViewModel
|
||||
/// 实现了 <see cref="IOverlayClosable"/> 接口,用于与父窗口的蒙板交互。
|
||||
/// </summary>
|
||||
public class WizardClientsViewModel : INotifyPropertyChanged, IOverlayClosable
|
||||
{
|
||||
/// <summary>
|
||||
/// 绑定到 ListView 的数据源
|
||||
/// </summary>
|
||||
public static ObservableCollection<ServiceNodeModel> ServiceNodes
|
||||
=> AppGlobal.ServiceNodes;
|
||||
|
||||
#region --- 业务数据属性 ---
|
||||
|
||||
private string _statusText = "准备就绪";
|
||||
/// <summary>
|
||||
/// 获取或设置向导的当前状态文本。
|
||||
/// </summary>
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
set { _statusText = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 命令属性 ---
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于确认操作的命令。
|
||||
/// </summary>
|
||||
public ICommand ConfirmCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于取消操作的命令。
|
||||
/// </summary>
|
||||
public ICommand CancelCommand { get; }
|
||||
|
||||
// 新增一行的命令
|
||||
public ICommand AddNodeCommand { get; }
|
||||
|
||||
public ICommand DeleteNodeCommand { get; }
|
||||
|
||||
public ICommand CheckCommand { get; }
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 构造函数 ---
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 <see cref="WizardClientsViewModel"/> 类的新实例。
|
||||
/// </summary>
|
||||
public WizardClientsViewModel()
|
||||
{
|
||||
// 实现新增逻辑
|
||||
AddNodeCommand = new RelayCommand<object>(_ =>
|
||||
{
|
||||
// 创建新行对象并添加到集合
|
||||
var newNode = new ServiceNodeModel
|
||||
{
|
||||
ServiceNodeName = "新节点",
|
||||
ServiceNodeIp = "0.0.0.0",
|
||||
ServiceNodePort = "5000",
|
||||
Status = "未检测"
|
||||
};
|
||||
ServiceNodes.Add(newNode);
|
||||
});
|
||||
|
||||
// 删除逻辑实现
|
||||
DeleteNodeCommand = new RelayCommand<ServiceNodeModel>(node =>
|
||||
{
|
||||
if (node != null && ServiceNodes.Contains(node))
|
||||
{
|
||||
ServiceNodes.Remove(node);
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化确认命令
|
||||
ConfirmCommand = new RelayCommand<object>(async _ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 2. 【核心代码】调用通用存储服务
|
||||
// 泛型 T 自动推断为 ObservableCollection<ServiceNodeModel>
|
||||
await LocalStorageService.SaveAsync(
|
||||
AppPaths.ServiceNodesConfig, // 路径:Configs/service_nodes.json
|
||||
ServiceNodes // 数据:当前的列表对象
|
||||
);
|
||||
|
||||
// 4. 关闭当前弹窗
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"保存失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
});
|
||||
|
||||
CheckCommand = new RelayCommand<object>(async _ => await ExecuteCheckAsync());
|
||||
|
||||
// 初始化取消命令
|
||||
CancelCommand = new RelayCommand<object>(_ =>
|
||||
{
|
||||
// 直接触发关闭请求事件,取消并关闭向导
|
||||
RequestClose?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- IOverlayClosable 接口实现 ---
|
||||
|
||||
/// <summary>
|
||||
/// 当需要关闭蒙板时发生。
|
||||
/// </summary>
|
||||
public event Action? RequestClose;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- INotifyPropertyChanged 接口实现 ---
|
||||
|
||||
/// <summary>
|
||||
/// 当属性值更改时发生。
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 引发 <see cref="PropertyChanged"/> 事件。
|
||||
/// </summary>
|
||||
/// <param name="name">更改的属性名称。</param>
|
||||
protected void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task ExecuteCheckAsync()
|
||||
{
|
||||
foreach (var node in ServiceNodes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(node.ServiceNodeIp) || string.IsNullOrWhiteSpace(node.ServiceNodePort))
|
||||
continue;
|
||||
|
||||
node.Status = "正在获取信息...";
|
||||
|
||||
// 1. 调用 ApiClient 获取列表 (代码不需要变,因为 Repository 已经封装好了)
|
||||
var cameras = await ApiClient.Instance.Cameras.GetListByAddressAsync(
|
||||
node.ServiceNodeIp,
|
||||
node.ServiceNodePort,
|
||||
"向导页面"
|
||||
);
|
||||
|
||||
// 2. 根据返回的详细数据,生成更智能的状态描述
|
||||
if (cameras.Count > 0)
|
||||
{
|
||||
// 统计在线的摄像头数量
|
||||
int onlineCount = 0;
|
||||
// 统计主要品牌 (例如: HikVision)
|
||||
string firstBrand = cameras[0].Brand;
|
||||
|
||||
foreach (var cam in cameras)
|
||||
{
|
||||
if (cam.IsOnline) onlineCount++;
|
||||
}
|
||||
|
||||
// 状态显示示例:只能是在线,其它的影响界面上色
|
||||
node.Status = $"在线";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 列表为空,或者是网络不通导致 Repository 返回了空列表
|
||||
// 为了区分是“无数据”还是“网络不通”,其实 Repository 可以优化返回 null 或抛异常,
|
||||
// 但目前的架构返回空列表最安全。
|
||||
node.Status = "离线或无设备";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user