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.
219 lines
9.1 KiB
219 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);
|
|
}
|
|
}
|
|
|