You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

220 lines
9.1 KiB

using Microsoft.Extensions.Logging;
using AuroraDesk.Presentation.ViewModels.Base;
using AuroraDesk.Core.Interfaces;
using AuroraDesk.Core.Entities;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Linq;
using Avalonia.Threading;
namespace AuroraDesk.Presentation.ViewModels.Pages;
/// <summary>
/// 图标导航页面的 ViewModel
/// 优化后:延迟加载数据,避免导航时阻塞主线程
/// </summary>
public class IconsPageViewModel : RoutableViewModel
{
private ObservableCollection<HeroIconItem> _heroIcons = new();
private HeroIconItem? _selectedIcon;
private bool _isLoading;
private bool _isDataLoaded;
private bool _isReStreaming; // 标记是否正在重新流式加载
private readonly ILogger<IconsPageViewModel>? _logger;
private readonly IIconService _iconService;
public ObservableCollection<HeroIconItem> HeroIcons
{
get
{
// 延迟初始化:只在数据为空时才加载
// 关键优化:只要数据已存在,就不重新加载,无论 Tab 是否切换
// 这样 Tab 切换时不会触发不必要的重新加载
if (!_isLoading && !_isReStreaming && _heroIcons.Count == 0)
{
// 只在数据为空时才加载
_isDataLoaded = true;
_ = LoadIconsAsync();
}
// 如果数据已存在,直接返回,不重新加载
// 这样可以避免 Tab 切换时的重新加载,提升性能
return _heroIcons;
}
set => this.RaiseAndSetIfChanged(ref _heroIcons, value);
}
public HeroIconItem? SelectedIcon
{
get => _selectedIcon;
set => this.RaiseAndSetIfChanged(ref _selectedIcon, value);
}
/// <summary>
/// 是否正在加载图标数据
/// </summary>
public bool IsLoading
{
get => _isLoading;
set => this.RaiseAndSetIfChanged(ref _isLoading, value);
}
// 响应式命令
public ReactiveCommand<HeroIconItem, Unit> CopyIconCommand { get; }
public ReactiveCommand<HeroIconItem, Unit> SelectIconCommand { get; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="hostScreen">宿主 Screen</param>
/// <param name="iconService">图标服务</param>
/// <param name="logger">日志记录器</param>
public IconsPageViewModel(
IScreen hostScreen,
IIconService iconService,
ILogger<IconsPageViewModel>? logger = null)
: base(hostScreen, "Icons")
{
_logger = logger;
_iconService = iconService ?? throw new ArgumentNullException(nameof(iconService));
_logger?.LogInformation("IconsPageViewModel 已创建");
// 创建命令
CopyIconCommand = ReactiveCommand.Create<HeroIconItem>(CopyIconToClipboard);
SelectIconCommand = ReactiveCommand.Create<HeroIconItem>(SelectIcon);
// 监听图标选择变化
this.WhenAnyValue(x => x.SelectedIcon)
.Where(icon => icon != null)
.Subscribe(icon => _logger?.LogInformation("选择图标: {Name}", icon!.DisplayName));
// 延迟加载:不在构造函数中立即加载,避免导航时阻塞主线程
// 数据将在页面显示时(HeroIcons 绑定触发时)加载
}
/// <summary>
/// 异步流式加载图标数据(优化:批量添加,避免阻塞 UI)
/// 在后台线程准备数据,然后在 UI 线程批量添加,减少 UI 更新频率
/// </summary>
private async Task LoadIconsAsync()
{
try
{
// 由于 getter 已经确保只在数据为空时才调用此方法,所以这里不需要检查数据是否已存在
IsLoading = true;
_logger?.LogInformation("开始加载图标数据");
// 在后台线程准备数据,避免阻塞 UI
var iconList = await Task.Run(() =>
{
var icons = _iconService.GetIcons();
return icons.ToList();
});
// 流式渲染策略:
// 1. 立即显示第一批(让用户看到内容)
// 2. 然后批量添加剩余数据,每次添加一批,减少 UI 更新频率
// 3. 使用后台线程准备数据,UI 线程批量添加,避免阻塞
const int initialBatchSize = 30; // 减少第一批数量,更快响应,减少初始渲染负担
const int incrementalBatchSize = 30; // 增加批次大小,减少 UI 更新频率
// 立即显示第一批(在 UI 线程执行,使用后台优先级)
// 关键优化:不要重新创建ObservableCollection,直接使用Add方法
// 这样可以避免触发PropertyChanged,防止tab切换时ItemsControl重新渲染所有项
var initialBatch = iconList.Take(initialBatchSize).ToList();
await Dispatcher.UIThread.InvokeAsync(() =>
{
// 直接添加到现有集合,而不是创建新集合
// 这样可以保持集合引用不变,避免ItemsControl重新绑定和重新渲染
// 关键优化:当tab切换时,如果数据已加载,不会重新创建集合,避免卡顿
var wasEmpty = _heroIcons.Count == 0;
foreach (var icon in initialBatch)
{
_heroIcons.Add(icon);
}
// 只在第一次加载时(集合从空变为有数据)触发PropertyChanged
// 后续流式加载和tab切换都不会触发PropertyChanged,避免重新渲染
if (wasEmpty)
{
// 第一次加载,触发一次PropertyChanged确保UI绑定
this.RaisePropertyChanged(nameof(HeroIcons));
}
IsLoading = false; // 立即显示,用户体验更好
}, DispatcherPriority.Background);
_logger?.LogInformation("初始加载 {Count} 个图标(共 {Total} 个),开始流式加载剩余 {Remaining} 个",
initialBatch.Count, iconList.Count, iconList.Count - initialBatch.Count);
// 如果有更多数据,在后台流式加载
if (iconList.Count > initialBatchSize)
{
// 使用 Task.Yield 让 UI 线程有机会渲染第一批
await Task.Yield();
// 批量添加剩余数据
for (int i = initialBatchSize; i < iconList.Count; i += incrementalBatchSize)
{
var batch = iconList.Skip(i).Take(incrementalBatchSize).ToList();
// 在 UI 线程批量添加,避免单个添加导致的频繁 UI 更新
await Dispatcher.UIThread.InvokeAsync(() =>
{
// 批量添加,而不是逐个添加
// 这样可以减少 UI 更新频率,每批只触发一次集合变更通知
foreach (var icon in batch)
{
_heroIcons.Add(icon);
}
}, DispatcherPriority.Background); // 使用后台优先级,不阻塞其他 UI 操作
// 每批之间延迟,让 UI 有时间渲染
// 根据剩余数量动态调整延迟:剩余越多,延迟稍长,避免过度消耗资源
if (i + incrementalBatchSize < iconList.Count)
{
var remainingCount = iconList.Count - (i + incrementalBatchSize);
// 动态延迟:剩余数量多时延迟稍长(最多15ms)
var delay = remainingCount > 100 ? 15 : 8;
await Task.Delay(delay);
}
}
_logger?.LogInformation("流式加载完成,已添加所有 {Total} 个图标", iconList.Count);
}
// 重置重新流式加载标志
_isReStreaming = false;
_isDataLoaded = true;
}
catch (Exception ex)
{
_logger?.LogError(ex, "加载图标数据时发生错误");
IsLoading = false;
_isReStreaming = false;
}
}
private void CopyIconToClipboard(HeroIconItem icon)
{
if (icon == null) return;
// 这里可以实现复制到剪贴板的逻辑
_logger?.LogInformation("复制图标到剪贴板: {Name}", icon.DisplayName);
// 可以添加通知用户复制成功的逻辑
}
private void SelectIcon(HeroIconItem icon)
{
if (icon == null) return;
SelectedIcon = icon;
_logger?.LogInformation("选择图标: {Name}", icon.DisplayName);
}
}