具备界面基础功能

This commit is contained in:
2026-01-01 22:40:32 +08:00
parent 0c86b4dad3
commit d039559402
81 changed files with 8333 additions and 1905 deletions

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