This commit is contained in:
lincube
2026-03-24 17:47:54 +08:00
parent af2e7b4f2f
commit a0bb83c743
23 changed files with 2041 additions and 104 deletions

View File

@@ -0,0 +1,144 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="400"
x:Class="VoiceHubLanDesktop.Views.VoiceHubScheduleControl"
x:DataType="VoiceHubLanDesktop.Views.VoiceHubScheduleControl">
<Design.DataContext>
<VoiceHubLanDesktop.Views.VoiceHubScheduleControl/>
</Design.DataContext>
<Grid RowDefinitions="Auto,*">
<!-- 标题栏 -->
<Border Grid.Row="0"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
Padding="12,8"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="0,0,0,1">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="&#xE7D1;"
FontFamily="Segoe MDL2 Assets"
FontSize="16"
VerticalAlignment="Center"
Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}"/>
<TextBlock Text="{Binding TitleText}"
FontWeight="SemiBold"
FontSize="14"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding DateText}"
FontSize="12"
VerticalAlignment="Center"
Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}"
Margin="8,0,0,0"/>
</StackPanel>
</Border>
<!-- 内容区域 -->
<Grid Grid.Row="1">
<!-- 加载状态 -->
<StackPanel x:Name="LoadingPanel"
Orientation="Vertical"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12"
IsVisible="{Binding IsLoading}">
<ProgressBar IsIndeterminate="True"
Width="100"
Height="4"/>
<TextBlock Text="正在加载排期..."
FontSize="13"
Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}"/>
</StackPanel>
<!-- 排期列表 -->
<ScrollViewer x:Name="SchedulePanel"
IsVisible="{Binding IsNormal}"
Padding="8,8,8,8">
<ItemsControl ItemsSource="{Binding Songs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}"
CornerRadius="8"
Padding="12,10"
Margin="0,0,0,8">
<Grid ColumnDefinitions="Auto,*">
<!-- 序号 -->
<Border Grid.Column="0"
Background="{DynamicResource SystemAccentColor}"
CornerRadius="12"
Width="24"
Height="24"
Margin="0,0,12,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding Sequence}"
FontSize="11"
FontWeight="Bold"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<!-- 歌曲信息 -->
<StackPanel Grid.Column="1" Spacing="4">
<TextBlock Text="{Binding Song.Title}"
FontSize="14"
FontWeight="Medium"
TextTrimming="CharacterEllipsis"
MaxLines="1"/>
<TextBlock FontSize="12"
Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}"
TextTrimming="CharacterEllipsis"
MaxLines="1">
<Run Text="{Binding Song.Artist}"/>
</TextBlock>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<!-- 空状态 -->
<StackPanel x:Name="EmptyPanel"
Orientation="Vertical"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8"
IsVisible="{Binding IsEmpty}">
<TextBlock Text="&#xE7E5;"
FontFamily="Segoe MDL2 Assets"
FontSize="48"
Foreground="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"/>
<TextBlock Text="{Binding EmptyMessage}"
FontSize="14"
Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}"/>
</StackPanel>
<!-- 错误状态 -->
<StackPanel x:Name="ErrorPanel"
Orientation="Vertical"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8"
IsVisible="{Binding IsError}">
<TextBlock Text="&#xE783;"
FontFamily="Segoe MDL2 Assets"
FontSize="48"
Foreground="#FFB00020"/>
<TextBlock Text="{Binding ErrorMessage}"
FontSize="14"
Foreground="#FFB00020"
TextWrapping="Wrap"
MaxWidth="200"
TextAlignment="Center"/>
<Button Content="重试"
Command="{Binding RetryCommand}"
Margin="0,8,0,0"
HorizontalAlignment="Center"/>
</StackPanel>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,168 @@
using System.Collections.ObjectModel;
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LanMountainDesktop.PluginSdk;
using VoiceHubLanDesktop.Models;
using VoiceHubLanDesktop.Services;
namespace VoiceHubLanDesktop.Views;
/// <summary>
/// 广播站排期显示组件
/// </summary>
public sealed partial class VoiceHubScheduleControl : UserControl
{
private readonly VoiceHubScheduleService _scheduleService;
private readonly VoiceHubSettingsService _settingsService;
private readonly DispatcherTimer? _refreshTimer;
private CancellationTokenSource? _loadCts;
public ObservableCollection<SongItem> Songs { get; } = [];
[ObservableProperty] private string _titleText = "广播站排期";
[ObservableProperty] private string _dateText = "";
[ObservableProperty] private string _emptyMessage = "暂无排期数据";
[ObservableProperty] private string _errorMessage = "";
[ObservableProperty] private bool _isLoading = true;
[ObservableProperty] private bool _isNormal = false;
[ObservableProperty] private bool _isEmpty = false;
[ObservableProperty] private bool _isError = false;
public VoiceHubScheduleControl(
VoiceHubScheduleService scheduleService,
VoiceHubSettingsService settingsService,
IPluginRuntimeContext runtimeContext)
{
InitializeComponent();
DataContext = this;
_scheduleService = scheduleService;
_settingsService = settingsService;
// 设置刷新定时器
var settings = _settingsService.GetSettings();
_refreshTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMinutes(settings.RefreshIntervalMinutes)
};
_refreshTimer.Tick += async (_, _) => await RefreshAsync();
_refreshTimer.Start();
// 监听设置变化
_settingsService.SettingsChanged += OnSettingsChanged;
// 初始加载
_ = LoadAsync();
}
private void OnSettingsChanged(object? sender, PluginSettings settings)
{
if (_refreshTimer != null)
{
_refreshTimer.Interval = TimeSpan.FromMinutes(settings.RefreshIntervalMinutes);
}
_scheduleService.ClearCache();
_ = RefreshAsync();
}
private async Task LoadAsync()
{
SetState(ComponentState.Loading);
try
{
_loadCts?.Cancel();
_loadCts = new CancellationTokenSource();
var displayData = await _scheduleService.GetTodayScheduleAsync(_loadCts.Token);
await Dispatcher.UIThread.InvokeAsync(() =>
{
ApplyDisplayData(displayData);
});
}
catch (OperationCanceledException)
{
// 忽略取消
}
catch (Exception ex)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
SetState(ComponentState.NetworkError, $"加载失败: {ex.Message}");
});
}
}
private void ApplyDisplayData(DisplayData data)
{
switch (data.State)
{
case ComponentState.Normal:
Songs.Clear();
foreach (var song in data.Songs)
{
Songs.Add(song);
}
DateText = data.DisplayDate?.ToString("MM月dd日") ?? "";
SetState(ComponentState.Normal);
break;
case ComponentState.NoSchedule:
EmptyMessage = data.ErrorMessage ?? "暂无排期数据";
SetState(ComponentState.NoSchedule);
break;
case ComponentState.NetworkError:
SetState(ComponentState.NetworkError, data.ErrorMessage ?? "网络错误");
break;
default:
SetState(ComponentState.Loading);
break;
}
}
private void SetState(ComponentState state, string? message = null)
{
IsLoading = state == ComponentState.Loading;
IsNormal = state == ComponentState.Normal;
IsEmpty = state == ComponentState.NoSchedule;
IsError = state == ComponentState.NetworkError;
if (!string.IsNullOrWhiteSpace(message))
{
if (state == ComponentState.NetworkError)
{
ErrorMessage = message;
}
else if (state == ComponentState.NoSchedule)
{
EmptyMessage = message;
}
}
}
[RelayCommand]
private async Task RetryAsync()
{
_scheduleService.ClearCache();
await LoadAsync();
}
public async Task RefreshAsync()
{
await LoadAsync();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_refreshTimer?.Stop();
_loadCts?.Cancel();
_settingsService.SettingsChanged -= OnSettingsChanged;
}
}