Files
Ayay/SHH.CameraDashboard/ViewModels/CameraListViewModel.cs
2026-01-01 22:40:32 +08:00

259 lines
9.1 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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));
}
}