功能更新

This commit is contained in:
GG Z
2026-02-12 21:29:00 +08:00
parent a9faf251be
commit b3479d1f39
342 changed files with 4671 additions and 2223 deletions

View File

@@ -0,0 +1,112 @@
namespace Melskin.Controls;
/// <summary>
/// 通知管理器类,提供显示系统通知的方法。
/// 该类主要用于在应用程序中方便地创建和显示不同类型的通知。
/// </summary>
public static class Notification
{
// 数据结构现在存储 Notification 模型,而不是 Window
private static readonly Dictionary<NotificationPlacement, List<Tuple<NotificationModel, NotificationView>>> ActiveNotifications =
new()
{
{ NotificationPlacement.TopRight, [] },
{ NotificationPlacement.TopLeft, [] },
{ NotificationPlacement.BottomRight, [] },
{ NotificationPlacement.BottomLeft, [] }
};
private static readonly object Lock = new();
/// <summary>
/// 显示系统通知。
/// </summary>
/// <param name="title">通知的标题。</param>
/// <param name="message">通知的内容消息。</param>
/// <param name="type">通知的类型,默认为信息类型。</param>
/// <param name="placement">通知在屏幕上的位置,默认为右上角。</param>
/// <param name="durationSeconds">通知显示的持续时间默认为5秒。</param>
public static void Show(string title, string message, NotificationType type = NotificationType.Info,
NotificationPlacement placement = NotificationPlacement.TopRight, int durationSeconds = 5)
{
Application.Current.Dispatcher.Invoke(() =>
{
var model = new NotificationModel(title, message, type, TimeSpan.FromSeconds(durationSeconds));
// 修改这里:在创建 view 时传入 placement
var view = new NotificationView(model, placement);
// 后续代码完全不变
view.Ready += (v) => OnNotificationReady(v, placement);
view.Closed += (_, _) => OnNotificationClosed(view, placement);
view.Show();
});
}
private static void OnNotificationReady(NotificationView view, NotificationPlacement placement)
{
lock (Lock)
{
ActiveNotifications[placement].Add(new Tuple<NotificationModel, NotificationView>(view.Model, view));
Reposition(placement);
}
}
private static void OnNotificationClosed(NotificationView view, NotificationPlacement placement)
{
lock (Lock)
{
var itemToRemove = ActiveNotifications[placement].FirstOrDefault(t => t.Item2 == view);
if (itemToRemove != null)
{
ActiveNotifications[placement].Remove(itemToRemove);
Reposition(placement);
}
}
}
private static void Reposition(NotificationPlacement placement)
{
var notifications = ActiveNotifications[placement];
if (!notifications.Any()) return;
var workArea = SystemParameters.WorkArea;
double currentTop;
var isTop = placement == NotificationPlacement.TopLeft || placement == NotificationPlacement.TopRight;
if (isTop)
{
currentTop = workArea.Top + 10;
}
else // Bottom
{
// 对于底部,我们需要先计算总高度
var totalHeight = notifications.Sum(t => t.Item2.ActualHeight + 10);
currentTop = workArea.Bottom - totalHeight;
}
foreach (var tuple in notifications)
{
var view = tuple.Item2;
// 在 `Ready` 事件之后ActualWidth/Height 已经是准确的了
var finalLeft = (placement == NotificationPlacement.TopRight || placement == NotificationPlacement.BottomRight)
? workArea.Right - view.ActualWidth - 10
: workArea.Left + 10;
// 检查这个窗口是否是新来的(通过透明度判断)
if (view.Opacity == 0)
{
// 如果是新的,命令它播放入场动画
view.AnimateIn(finalLeft, currentTop);
}
else
{
// 如果是已经存在的,命令它平滑移动到新位置
view.AnimateMove(currentTop);
}
currentTop += view.ActualHeight + 10; // 为下一个窗口计算Y坐标
}
}
}

View File

@@ -0,0 +1,43 @@
namespace Melskin.Controls;
/// <summary>
/// 代表系统通知的模型,包含标题、消息内容、类型和显示时长等信息。此类用于在应用程序中创建并配置通知。
/// </summary>
public class NotificationModel
{
/// <summary>
/// 获取此通知的唯一标识符。每个通知实例生成时都会自动分配一个全局唯一的ID。
/// </summary>
public Guid Id { get; } = Guid.NewGuid();
/// <summary>
/// 获取或设置通知的标题。此属性用于存储和显示通知的标题文本。
/// </summary>
public string Title { get; set; }
/// <summary>
/// 获取或设置通知的消息内容。此属性用于存储和显示通知的具体信息。
/// </summary>
public string Message { get; set; }
/// <summary>
/// 代表通知的类型,用于区分不同的通知类别,如信息、成功、警告或错误。
/// </summary>
public NotificationType Type { get; set; }
/// <summary>
/// 获取或设置通知显示的持续时间。此属性用于定义从通知显示到自动关闭的时间间隔。
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// 代表系统通知的模型,包含标题、消息内容、类型和显示时长等信息。
/// </summary>
public NotificationModel(string title, string message, NotificationType type, TimeSpan duration)
{
Title = title;
Message = message;
Type = type;
Duration = duration;
}
}

View File

@@ -0,0 +1,28 @@
namespace Melskin.Controls
{
/// <summary>
/// 通知位置枚举,用于指定通知在屏幕上的显示位置。
/// </summary>
public enum NotificationPlacement
{
/// <summary>
/// 通知显示位置为屏幕的右上角。此枚举值用于指定通知在用户界面中的具体展示位置。
/// </summary>
TopRight,
/// <summary>
///
/// </summary>
TopLeft,
/// <summary>
/// 通知显示位置为屏幕的右下角。此枚举值用于指定通知在用户界面中的具体展示位置。
/// </summary>
BottomRight,
/// <summary>
/// 通知显示位置为屏幕的左下角。此枚举值用于指定通知在用户界面中的具体展示位置。
/// </summary>
BottomLeft
}
}

View File

@@ -0,0 +1,31 @@
namespace Melskin.Controls;
/// <summary>
/// 通知类型枚举,用于定义不同类型的系统通知。
/// </summary>
public enum NotificationType
{
/// <summary>
/// 表示通知类型为信息。此枚举成员用于标识那些传达一般信息或非关键性消息的通知。
/// 通常,使用这种类型的通知来向用户提供一般性的提示或更新,比如操作的状态、系统的一般信息等场景。
/// </summary>
Info,
/// <summary>
/// 表示通知类型为成功。此枚举成员用于标识那些传达操作已成功完成或达到预期结果的通知。
/// 通常,使用这种类型的通知来向用户反馈积极的结果,比如数据保存成功、操作执行无误等场景。
/// </summary>
Success,
/// <summary>
/// 表示通知类型为警告。此枚举成员用于标识那些传达需要注意或潜在问题的通知。
/// 通常,使用这种类型的通知来提醒用户存在一些需要注意的情况,比如输入数据格式不正确、即将进行的操作可能带来风险等场景。
/// </summary>
Warning,
/// <summary>
/// 表示通知类型为错误。此枚举成员用于标识那些传达操作失败或出现异常情况的通知。
/// 通常,使用这种类型的通知来向用户反馈负面的结果,比如无法连接到服务器、数据保存失败等场景。
/// </summary>
Error
}

View File

@@ -0,0 +1,107 @@
<Window
AllowsTransparency="True"
Background="Transparent"
Height="Auto"
ShowInTaskbar="False"
SizeToContent="Height"
Title="NotificationWindow"
Topmost="True"
Width="384"
WindowStyle="None"
mc:Ignorable="d"
x:Class="Melskin.Controls.NotificationView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:controls="clr-namespace:Melskin.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Border
Background="{DynamicResource BackgroundFloatingBrush}"
BorderThickness="1"
CornerRadius="4"
Margin="8"
x:Name="RootBorder">
<!--<controls:Card.Effect>
<DropShadowEffect ShadowDepth="0" BlurRadius="10" Color="#000000" Opacity="0.1"/>
</controls:Card.Effect>-->
<!--<controls:Alert Message="{Binding Title}" Description="{Binding Message}" Type="{Binding Type}"/>-->
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Icon Section (CORRECTED) -->
<controls:IconElement
Cursor=""
Grid.Column="0"
Height="20"
Margin="0,0,16,0"
VerticalAlignment="Top"
Width="20"
x:Name="IconElement">
<controls:IconElement.Style>
<Style BasedOn="{StaticResource IconElementStyle}" TargetType="controls:IconElement">
<Style.Triggers>
<DataTrigger Binding="{Binding Type}" Value="Success">
<Setter Property="Symbol" Value="CheckCircle" />
<Setter Property="Foreground" Value="{DynamicResource SuccessBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="Info">
<Setter Property="Symbol" Value="Info" />
<Setter Property="Foreground" Value="{DynamicResource InfoBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="Warning">
<Setter Property="Symbol" Value="Warning" />
<Setter Property="Foreground" Value="{DynamicResource WarningBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="Error">
<Setter Property="Symbol" Value="Error" />
<Setter Property="Foreground" Value="{DynamicResource ErrorBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</controls:IconElement.Style>
</controls:IconElement>
<!-- Content Section -->
<StackPanel Grid.Column="1" d:DataContext="{d:DesignInstance Type=controls:NotificationModel}">
<TextBlock
FontSize="16"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimaryBrush}"
Margin="0,0,0,4"
Name="TitleTextBlock"
Text="{Binding Title}" />
<TextBlock
FontSize="14"
Foreground="{DynamicResource TextPrimaryBrush}"
Name="MessageTextBlock"
Text="{Binding Message}"
TextWrapping="Wrap" />
</StackPanel>
<!-- Close Button -->
<Button
Click="CloseButton_Click"
Cursor="Hand"
Grid.Column="2"
Margin="8,-4,-4,-4"
Name="CloseButton"
VerticalAlignment="Top">
<Button.Template>
<ControlTemplate TargetType="Button">
<controls:IconElement Foreground="{DynamicResource TextSecondaryBrush}" Symbol="Close" />
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,119 @@
using System.Windows.Media.Animation;
using System.Windows.Threading;
namespace Melskin.Controls;
/// <summary>
/// 代表一个通知视图用于在应用程序中显示系统通知。此类继承自Window并根据提供的Notification模型和位置信息来初始化自身。支持抽屉式滑入和滑出动画效果。
/// </summary>
public partial class NotificationView
{
private readonly DispatcherTimer? closeTimer;
private readonly NotificationPlacement placement; // 新增一个字段来存储位置
/// <summary>
/// 当通知视图加载完毕并准备好显示时触发的事件。此事件允许外部代码在通知视图完全初始化后执行特定操作,例如调整其位置或开始动画。
/// </summary>
public event Action<NotificationView>? Ready;
/// <summary>
/// 获取与此视图关联的通知模型。该模型包含了通知的标题、消息内容、类型以及显示时长等信息。
/// </summary>
public NotificationModel Model { get; }
/// <summary>
/// 代表一个通知视图用于在应用程序中显示系统通知。该类继承自Window并根据提供的Notification模型和位置信息来初始化自身。
/// </summary>
public NotificationView(NotificationModel model, NotificationPlacement placement) // 构造函数接收placement
{
InitializeComponent();
this.Model = model;
this.DataContext = this.Model;
this.placement = placement; // 保存placement
this.Opacity = 0; // 初始时依然保持透明,防止在动画开始前闪烁
this.Loaded += (_, _) => Ready?.Invoke(this);
if (model.Duration <= TimeSpan.Zero) return;
closeTimer = new DispatcherTimer { Interval = model.Duration };
closeTimer.Tick += (_, _) => AnimateOut();
}
/// <summary>
/// 命令视图执行抽屉式滑入动画
/// </summary>
public void AnimateIn(double finalLeft, double finalTop)
{
// Manager已经计算好了最终位置我们只需执行动画
this.Top = finalTop; // Y坐标直接设定
// 根据位置判断是从左侧滑入还是右侧滑入
var isRightSide = placement == NotificationPlacement.TopRight ||
placement == NotificationPlacement.BottomRight;
var startLeft = isRightSide
? finalLeft + this.ActualWidth + 10 // 从右侧屏幕外开始
: finalLeft - this.ActualWidth - 10; // 从左侧屏幕外开始
// 在动画开始前,立即将窗口移动到屏幕外的起始位置
this.Left = startLeft;
// 创建滑入动画
var slideAnim = new DoubleAnimation(startLeft, finalLeft, TimeSpan.FromMilliseconds(400))
{
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
};
// 同时,让窗口从透明变为不透明,这是一个很好的辅助效果
var fadeAnim = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200));
// 应用动画
this.BeginAnimation(Window.LeftProperty, slideAnim);
this.BeginAnimation(Window.OpacityProperty, fadeAnim);
closeTimer?.Start();
}
/// <summary>
/// 命令视图执行抽屉式滑出动画
/// </summary>
public void AnimateOut()
{
closeTimer?.Stop();
// 计算滑出到屏幕外的目标位置
var isRightSide = placement is NotificationPlacement.TopRight or NotificationPlacement.BottomRight;
var endLeft = isRightSide ? this.Left + this.ActualWidth + 10 : this.Left - this.ActualWidth - 10;
// 创建滑出动画
var slideAnim = new DoubleAnimation(this.Left, endLeft, TimeSpan.FromMilliseconds(300))
{
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseIn }
};
// 同时淡出
var fadeAnim = new DoubleAnimation(this.Opacity, 0, TimeSpan.FromMilliseconds(250));
// 当动画完成时,关闭窗口
slideAnim.Completed += (_, _) => this.Close();
this.BeginAnimation(Window.LeftProperty, slideAnim);
this.BeginAnimation(Window.OpacityProperty, fadeAnim);
}
/// <summary>
/// 当其他通知关闭时,平滑地移动到新位置 (这个方法保持不变)
/// </summary>
public void AnimateMove(double newTop)
{
var moveAnim = new DoubleAnimation(this.Top, newTop, TimeSpan.FromMilliseconds(350))
{
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
};
this.BeginAnimation(Window.TopProperty, moveAnim);
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
AnimateOut();
}
}