feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
CI / Go Backend (push) Canceled after 0s

初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
xyou
2026-08-08 14:07:56 +08:00
parent 9500c4c80a
commit f70b061d1a
1972 changed files with 159760 additions and 6 deletions
@@ -0,0 +1,1112 @@
using System.Collections;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Shapes;
using PCL.Core.UI.Controls.SvgIcon;
namespace PCL;
[ContentProperty("Inlines")]
public partial class MyListItem : IMyRadio
{
public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e);
public delegate void LogoClickEventHandler(object sender, MouseButtonEventArgs e);
public bool isMouseOverAnimationEnabled = true;
private string stateLast;
public object tag { get; set; }
public event IMyRadio.CheckEventHandler? Check;
public event IMyRadio.ChangedEventHandler? Changed;
public event ClickEventHandler? Click;
public event LogoClickEventHandler? LogoClick;
public void RefreshColor(object sender, EventArgs e)
{
// 菜单虚拟化检测
if (ContentHandler is not null)
{
ContentHandler.Invoke(this, e);
ContentHandler = null;
}
// 判断当前颜色
string stateNew;
int time;
if (isMouseDown && !(Type == CheckType.RadioBox && Checked))
{
stateNew = "MouseDown";
time = 120;
}
else if (IsMouseOver && isMouseOverAnimationEnabled)
{
stateNew = "MouseOver";
time = 120;
}
else
{
stateNew = "Idle";
time = 180;
}
if ((stateLast ?? "") == (stateNew ?? ""))
return;
stateLast = stateNew;
// 触发颜色动画
if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
{
// 有动画
var ani = new List<ModAnimation.AniData>();
if (IsMouseOver && isMouseOverAnimationEnabled)
{
if (buttonStack is not null)
{
ani.Add(ModAnimation.AaOpacity(buttonStack, 1d - buttonStack.Opacity, (int)Math.Round(time * 0.7d),
(int)Math.Round(time * 0.3d)));
ani.Add(ModAnimation.AaDouble(
i => ColumnPaddingRight.Width =
new GridLength(Math.Max(0, ColumnPaddingRight.Width.Value + (double)i)),
Math.Max(MinPaddingRight, 5 + Buttons.Count() * 25) - ColumnPaddingRight.Width.Value,
(int)Math.Round(time * 0.3d), (int)Math.Round(time * 0.7d)));
}
ani.AddRange(new[]
{
ModAnimation.AaColor(RectBack, Border.BackgroundProperty,
isMouseDown ? "ColorBrush6" : "ColorBrushBg1", time),
ModAnimation.AaOpacity(RectBack, 1d - RectBack.Opacity, time,
ease: new ModAnimation.AniEaseOutFluent())
});
if (IsScaleAnimationEnabled)
{
ani.Add(ModAnimation.AaScaleTransform(RectBack,
1d - ((ScaleTransform)RectBack.RenderTransform).ScaleX, (int)Math.Round(time * 1.6d),
ease: new ModAnimation.AniEaseOutFluent()));
if (isMouseDown)
ani.Add(ModAnimation.AaScaleTransform(this, 0.98d - ((ScaleTransform)RenderTransform).ScaleX,
(int)Math.Round(time * 0.9d), ease: new ModAnimation.AniEaseOutFluent()));
else
ani.Add(ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX,
(int)Math.Round(time * 1.2d), ease: new ModAnimation.AniEaseOutFluent()));
}
}
else
{
if (buttonStack is not null)
{
ani.Add(ModAnimation.AaOpacity(buttonStack, -buttonStack.Opacity, (int)Math.Round(time * 0.4d)));
ani.Add(ModAnimation.AaDouble(
i => ColumnPaddingRight.Width =
new GridLength(Math.Max(0, ColumnPaddingRight.Width.Value + (double)i)),
MinPaddingRight - ColumnPaddingRight.Width.Value, (int)Math.Round(time * 0.4d)));
}
ani.Add(ModAnimation.AaOpacity(RectBack, -RectBack.Opacity, time));
if (IsScaleAnimationEnabled)
ani.AddRange(new[]
{
ModAnimation.AaColor(RectBack, Border.BackgroundProperty,
isMouseDown ? "ColorBrush6" : "ColorBrush7", time),
ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX, time * 3,
ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaScaleTransform(RectBack,
0.996d - ((ScaleTransform)RectBack.RenderTransform).ScaleX, time,
ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaScaleTransform(RectBack, -0.246d, 1, after: true)
});
}
ModAnimation.AniStart(ani, "ListItem Color " + Uuid);
}
else
{
// 无动画
if (IsMouseOver && isMouseOverAnimationEnabled)
{
if (buttonStack is not null)
{
buttonStack.Opacity = 1d;
ColumnPaddingRight.Width = new GridLength(Math.Max(MinPaddingRight, 5 + Buttons.Count() * 25));
}
// 由于鼠标已经移入,所以直接实例化 RectBack
RectBack.Background = (Brush)ThemeManager.AppResources["ColorBrushBg1"];
RectBack.Opacity = 1d;
RectBack.RenderTransform = new ScaleTransform(1d, 1d);
RenderTransform = new ScaleTransform(1d, 1d);
}
else
{
if (buttonStack is not null)
{
buttonStack.Opacity = 0d;
ColumnPaddingRight.Width = new GridLength(MinPaddingRight);
}
RenderTransform = new ScaleTransform(1d, 1d);
if (_RectBack is not null)
{
if (IsScaleAnimationEnabled)
RectBack.RenderTransform = new ScaleTransform(0.75d, 0.75d);
RectBack.Background = (Brush)ThemeManager.AppResources["ColorBrush7"];
RectBack.Opacity = 0d;
}
}
ModAnimation.AniStop("ListItem Color " + Uuid);
}
}
private void MyListItem_Loaded(object sender, RoutedEventArgs e)
{
if (Checked)
SetResourceReference(ForegroundProperty, Height < 40d ? "ColorBrush3" : "ColorBrush2");
else
SetResourceReference(ForegroundProperty, "ColorBrush1");
ColumnPaddingRight.Width = new GridLength(MinPaddingRight);
}
public override string ToString()
{
return Title;
}
#region
// 指向背景
private Border _RectBack;
public Border RectBack
{
get
{
if (_RectBack is null)
{
var rect = new Border
{
Name = "RectBack",
CornerRadius = new CornerRadius(IsScaleAnimationEnabled || Height > 40d ? 6 : 0),
RenderTransform = IsScaleAnimationEnabled ? new ScaleTransform(0.8d, 0.8d) : null,
RenderTransformOrigin = new Point(0.5d, 0.5d),
BorderThickness = new Thickness(ModBase.GetWPFSize(1d)),
SnapsToDevicePixels = true,
IsHitTestVisible = false,
Opacity = 0d
};
rect.SetResourceReference(Border.BackgroundProperty, "ColorBrush7");
rect.SetResourceReference(Border.BorderBrushProperty, "ColorBrush6");
SetColumnSpan(rect, 999);
SetRowSpan(rect, 999);
Children.Insert(0, rect);
_RectBack = rect;
// <!--<Border x:Name = "RectBack" CornerRadius="3" RenderTransformOrigin="0.5,0.5" SnapsToDevicePixels="True"
// IsHitTestVisible = "False" Opacity="0" BorderThickness="1"
// Grid.ColumnSpan = "4" Background="{DynamicResource ColorBrush7}" BorderBrush="{DynamicResource ColorBrush6}"/>-->
}
return _RectBack;
}
}
// 按钮
public FrameworkElement buttonStack;
// 图标
public FrameworkElement pathLogo;
// 勾选条
public Border rectCheck;
/// <summary>
/// Tags 的存放 StackPanel
/// </summary>
public StackPanel PanTags
{
get
{
if (field is not null)
return field;
var newStack = new StackPanel
{
IsHitTestVisible = false,
Orientation = Orientation.Horizontal,
VerticalAlignment = VerticalAlignment.Bottom,
Margin = new Thickness(3.5d, 0d, -3, 0d)
};
SetColumn(newStack, 3);
SetRow(newStack, 2);
PanBack.Children.Add(newStack);
field = newStack;
return field;
}
}
/// <summary>
/// 标签,可以传入 String 和 List(Of String)
/// </summary>
public object Tags
{
set
{
var list = new List<string>();
if (value is string str) list = str.Split("|").ToList();
if (value is List<string>) list = (List<string>)value;
PanTags.Children.Clear();
PanTags.Visibility = list.Any() ? Visibility.Visible : Visibility.Collapsed;
foreach (var TagText in list)
{
var newTag = new Border
{
Background = new SolidColorBrush(Color.FromArgb(17, 0, 0, 0)),
Padding = new Thickness(3d, 1d, 3d, 1d),
CornerRadius = new CornerRadius(3d),
Margin = new Thickness(0d, 0d, 3d, 0d),
SnapsToDevicePixels = true,
UseLayoutRounding = false
};
var tagTextBlock = new TextBlock
{
Text = TagText,
Foreground = new SolidColorBrush(Color.FromRgb(134, 134, 134)),
FontSize = 11d
};
newTag.Child = tagTextBlock;
PanTags.Children.Add(newTag);
}
}
}
// 副文本
public TextBlock LabInfo
{
get
{
if (field is null)
{
var lab = new TextBlock
{
Name = "LabInfo",
SnapsToDevicePixels = false,
UseLayoutRounding = false,
HorizontalAlignment = HorizontalAlignment.Left,
IsHitTestVisible = false,
TextTrimming = TextTrimming.CharacterEllipsis,
Visibility = Visibility.Collapsed,
FontSize = 12d,
Margin = new Thickness(4d, 0d, 0d, 0d),
Opacity = 0.6d
};
SetColumn(lab, 4);
SetRow(lab, 2);
PanBack.Children.Add(lab);
field = lab;
// <TextBlock Grid.Row="2" SnapsToDevicePixels="False" UseLayoutRounding="False" HorizontalAlignment="Left" x:Name = "LabInfo" IsHitTestVisible="False" Grid.Column="2"
// TextTrimming = "CharacterEllipsis" Visibility="Collapsed" FontSize="12" Foreground="{StaticResource ColorBrushGray2}" Margin="4,0,0,0" />
}
return field;
}
}
#endregion
#region
// Uuid
public int Uuid = ModBase.GetUuid();
/// <summary>
/// 是否启用缩放动画。
/// </summary>
public bool IsScaleAnimationEnabled
{
get => field;
set
{
field = value;
if (_RectBack is not null)
RectBack.CornerRadius = new CornerRadius(value ? 6 : 0);
}
} = true;
// 边距
public int PaddingLeft
{
get => (int)Math.Round(ColumnPaddingLeft.Width.Value);
set => ColumnPaddingLeft.Width = new GridLength(value);
}
/// <summary>
/// 右边距的最小值。
/// 在存在右侧按钮时,右边距会被自动设置为 5 + 按钮数 * 25。
/// </summary>
public int MinPaddingRight { get; set; } = 4;
// 按钮
public IEnumerable<MyIconButton> Buttons
{
get => field;
set
{
field = value;
// 没有特殊按钮,移除原 Stack
if (buttonStack is not null)
{
Children.Remove(buttonStack);
buttonStack = null;
}
// 添加新 Stack
switch (value.Count())
{
case 0:
{
break;
}
// 没有按钮,不添加新的
case 1:
{
// 只有一个按钮
foreach (var Btn in value)
{
if (Btn.Height.Equals(double.NaN))
Btn.Height = 25d;
if (Btn.Width.Equals(double.NaN))
Btn.Width = 25d;
Btn.Opacity = 0d;
Btn.Margin = new Thickness(0d, 0d, 5d, 0d);
Btn.SnapsToDevicePixels = false;
Btn.HorizontalAlignment = HorizontalAlignment.Right;
Btn.VerticalAlignment = VerticalAlignment.Center;
Btn.UseLayoutRounding = false;
SetColumnSpan(Btn, 10);
SetRowSpan(Btn, 10);
Children.Add(Btn);
buttonStack = Btn;
}
break;
}
default:
{
// 有复数按钮,使用 StackPanel
buttonStack = new StackPanel
{
Opacity = 0d, Margin = new Thickness(0d, 0d, 5d, 0d), SnapsToDevicePixels = false,
Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center, UseLayoutRounding = false
};
SetColumnSpan(buttonStack, 10);
SetRowSpan(buttonStack, 10);
// 构造按钮
foreach (var Btn in value)
{
if (Btn.Height.Equals(double.NaN))
Btn.Height = 25d;
if (Btn.Width.Equals(double.NaN))
Btn.Width = 25d;
((StackPanel)buttonStack).Children.Add(Btn);
}
Children.Add(buttonStack);
break;
}
}
}
}
// 标题
public InlineCollection Inlines => LabTitle.Inlines;
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value.Replace("\r", "").Replace("\n", ""));
}
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string), typeof(MyListItem));
// 字号
public double FontSize
{
get => (double)GetValue(FontSizeProperty);
set => SetValue(FontSizeProperty, value);
}
public static readonly DependencyProperty FontSizeProperty =
DependencyProperty.Register("FontSize", typeof(double), typeof(MyListItem), new PropertyMetadata(14d));
// 信息
public string Info
{
get => (string)GetValue(InfoProperty);
set
{
if (Info == value)
return;
value = value?.Replace("\r", "").Replace("\n", "");
SetValue(InfoProperty, value);
}
}
public static readonly DependencyProperty InfoProperty = DependencyProperty.Register("Info", typeof(string),
typeof(MyListItem), new PropertyMetadata("", OnInfoChanged));
public MyListItem()
{
InitializeComponent();
SizeChanged += (_, _) => OnSizeChanged();
PreviewMouseLeftButtonUp += Button_MouseUp;
PreviewMouseLeftButtonDown += Button_MouseDown;
MouseLeave += Button_MouseLeave;
PreviewMouseLeftButtonUp += Button_MouseLeave;
MouseEnter += RefreshColor;
MouseLeave += RefreshColor;
MouseLeftButtonDown += RefreshColor;
MouseLeftButtonUp += RefreshColor;
Loaded += MyListItem_Loaded;
}
private static void OnInfoChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (MyListItem)d;
var value = e.NewValue as string;
control.LabInfo.Text = value;
control.LabInfo.Visibility = string.IsNullOrEmpty(value) ? Visibility.Collapsed : Visibility.Visible;
}
// 图片
public string Logo
{
get => (string)GetValue(LogoProperty);
set
{
if (Logo == value)
return;
SetValue(LogoProperty, value);
}
}
public static readonly DependencyProperty LogoProperty = DependencyProperty.Register(
nameof(Logo),
typeof(string),
typeof(MyListItem),
new PropertyMetadata("", OnLogoChanged));
public string SvgIcon
{
get => (string)GetValue(SvgIconProperty);
set
{
if (SvgIcon == value)
return;
SetValue(SvgIconProperty, value);
}
}
public static readonly DependencyProperty SvgIconProperty = DependencyProperty.Register(
nameof(SvgIcon),
typeof(string),
typeof(MyListItem),
new PropertyMetadata("", OnSvgIconChanged));
private static void OnLogoChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (MyListItem)d;
control.UpdateLogo(e.NewValue as string);
}
private static void OnSvgIconChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (MyListItem)d;
control.UpdateLogo(control.Logo);
}
private bool IsUsingSvgIcon => SvgIconControlHelper.HasSvgIcon(SvgIcon);
private void UpdateLogo(string logo)
{
// 删除旧 Logo
if (pathLogo is not null)
Children.Remove(pathLogo);
pathLogo = null;
// 添加新 Logo
if (IsUsingSvgIcon)
{
pathLogo = new SvgIcon
{
Tag = this,
IsHitTestVisible = LogoClickable,
Icon = SvgIcon,
Stretch = Stretch.Uniform,
RenderTransformOrigin = new Point(0.5d, 0.5d),
RenderTransform = new ScaleTransform { ScaleX = 1D, ScaleY = 1D },
SnapsToDevicePixels = false,
UseLayoutRounding = false,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch
};
((SvgIcon)pathLogo).SetBinding(
Core.UI.Controls.SvgIcon.SvgIcon.IconBrushProperty,
new Binding("Foreground") { Source = this });
}
else if (!string.IsNullOrEmpty(logo))
{
if (logo.StartsWithF("http", true))
{
// 网络图片
pathLogo = new MyImage
{
Tag = this,
IsHitTestVisible = LogoClickable,
Source = logo,
RenderTransformOrigin = new Point(0.5d, 0.5d),
RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale },
SnapsToDevicePixels = true,
UseLayoutRounding = false
};
RenderOptions.SetBitmapScalingMode(pathLogo, BitmapScalingMode.Linear);
}
else if (logo.EndsWithF(".png", true) || logo.EndsWithF(".jpg", true) || logo.EndsWithF(".webp", true))
{
// 位图
pathLogo = new Canvas
{
Tag = this,
IsHitTestVisible = LogoClickable,
Background = new MyBitmap(logo),
RenderTransformOrigin = new Point(0.5d, 0.5d),
RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale },
SnapsToDevicePixels = true,
UseLayoutRounding = false,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch
};
if (logo.Contains(ModBase.pathTemp + @"Cache\Skin\Head") ||
logo.Contains(ModBase.pathTemp + @"Cache\Cape"))
RenderOptions.SetBitmapScalingMode(pathLogo, BitmapScalingMode.NearestNeighbor);
else
RenderOptions.SetBitmapScalingMode(pathLogo, BitmapScalingMode.Linear);
}
else
{
// 矢量图
pathLogo = new Path
{
Tag = this,
IsHitTestVisible = LogoClickable,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Stretch = Stretch.Uniform,
Data = (Geometry)new GeometryConverter().ConvertFromString(logo),
RenderTransformOrigin = new Point(0.5d, 0.5d),
RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale },
SnapsToDevicePixels = false,
UseLayoutRounding = false
};
pathLogo.SetBinding(Shape.FillProperty, new Binding("Foreground") { Source = this });
}
}
if (pathLogo is not null)
{
SetColumn(pathLogo, 2);
SetRowSpan(pathLogo, 4);
OnSizeChanged(); // 设置边距
Children.Add(pathLogo);
// 图标的点击事件
if (LogoClickable)
{
pathLogo.MouseLeave += (sender, e) => isLogoDown = false;
pathLogo.MouseLeftButtonDown += (sender, e) => isLogoDown = true;
pathLogo.MouseLeftButtonUp += (sender, e) =>
{
if (isLogoDown)
{
isLogoDown = false;
LogoClick?.Invoke(((FrameworkElement)sender).Tag, e);
}
};
}
}
// 改变行距
var hasLogo = IsUsingSvgIcon || !string.IsNullOrEmpty(logo);
ColumnLogo.Width = new GridLength((hasLogo ? 34 : 0) + (Height < 40d ? 0 : 4));
}
public double LogoScale
{
get => field;
set
{
field = value;
if (pathLogo is not null)
{
var scale = IsUsingSvgIcon ? 1D : LogoScale;
pathLogo.RenderTransform = new ScaleTransform { ScaleX = scale, ScaleY = scale };
}
}
} = 1d;
// 图标的点击
/// <summary>
/// 该 Logo 是否可用点击触发事件。需要在 Logo 属性之前设置。
/// </summary>
public bool LogoClickable { get; set; } = false;
private bool isLogoDown;
// 勾选选项
public enum CheckType
{
None,
Clickable,
RadioBox,
CheckBox
}
private CheckType _Type = CheckType.None;
public CheckType Type
{
get => _Type;
set
{
if (_Type == value)
return;
_Type = value;
// 切换左栏大小
ColumnCheck.Width =
new GridLength(_Type == CheckType.None || _Type == CheckType.Clickable ? Height < 40d ? 4 : 2 : 6);
// 切换竖条控件
if (_Type == CheckType.None || _Type == CheckType.Clickable)
{
// 移除竖条控件
if (rectCheck is not null)
{
Children.Remove(rectCheck);
rectCheck = null;
}
SetChecked(false, false, false);
}
// 添加竖条控件
else if (rectCheck is null)
{
rectCheck = new Border
{
Width = 5d,
Height = Checked ? double.NaN : 0d,
CornerRadius = new CornerRadius(2d, 2d, 2d, 2d),
VerticalAlignment = Checked ? VerticalAlignment.Stretch : VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Left,
UseLayoutRounding = false,
SnapsToDevicePixels = false,
Margin = Checked ? new Thickness(-1, 6d, 0d, 6d) : new Thickness(-1, 0d, 0d, 0d)
};
rectCheck.SetResourceReference(Border.BackgroundProperty, "ColorBrush3");
SetRowSpan(rectCheck, 4);
Children.Add(rectCheck);
}
}
}
// 适应尺寸
private void OnSizeChanged()
{
var logo = Logo;
var hasLogo = IsUsingSvgIcon || !string.IsNullOrEmpty(logo);
ColumnCheck.Width = new GridLength(_Type is CheckType.None or CheckType.Clickable
? Height < 40d
? 4
: 2
: 6);
ColumnLogo.Width = new GridLength((hasLogo ? 34 : 0) + (Height < 40d ? 0 : 4));
if (pathLogo is not null)
{
if (!IsUsingSvgIcon && (logo.EndsWithF(".png", true) ||
logo.EndsWithF(".jpg", true) ||
logo.EndsWithF(".webp", true)))
pathLogo.Margin = new Thickness(4d, 5d, 3d, 5d);
else
pathLogo.Margin = new Thickness(Height < 40d ? 6 : 8, 8d, Height < 40d ? 4 : 6, 8d);
}
LabTitle.Margin = new Thickness(4d, 0d, 0d, Height < 40d ? 0 : 2);
}
// 勾选状态
private bool _Checked;
public bool Checked
{
get => _Checked;
set => SetChecked(value, false, value != _Checked); // 仅在值发生变化时触发动画 (#4596)
}
/// <summary>
/// 手动设置 Checked 属性。
/// </summary>
/// <param name="value">新的 Checked 属性。</param>
/// <param name="user">是否由用户引发。</param>
/// <param name="anime">是否执行动画。</param>
public void SetChecked(bool value, bool user, bool anime)
{
try
{
// 自定义属性基础
var ChangedEventArgs = new ModBase.RouteEventArgs(user);
var rawValue = _Checked;
if (Type == CheckType.RadioBox)
{
if (IsInitialized && value != _Checked)
{
_Checked = value;
Changed?.Invoke(this, ChangedEventArgs);
if (ChangedEventArgs.handled)
{
_Checked = rawValue;
return;
}
}
_Checked = value;
}
else
{
if (value == _Checked)
return;
_Checked = value;
if (IsInitialized)
{
Changed?.Invoke(this, ChangedEventArgs);
if (ChangedEventArgs.handled)
{
_Checked = rawValue;
return;
}
}
}
if (value)
{
var checkEventArgs = new ModBase.RouteEventArgs(user);
Check?.Invoke(this, checkEventArgs);
if (checkEventArgs.handled)
return;
}
// 保证只有一个单选 ListItem 选中
if (Type == CheckType.RadioBox)
{
if (Parent is null)
return;
var radioboxList = new List<MyListItem>();
var checkedCount = 0;
// 收集控件列表与选中个数
foreach (var ControlRaw in ((Panel)Parent).Children)
{
var control = MyVirtualizingElement.TryInit((FrameworkElement)ControlRaw);
if (control is MyListItem listItem && listItem.Type == CheckType.RadioBox)
{
radioboxList.Add(listItem);
if (listItem.Checked)
checkedCount += 1;
}
}
// 判断选中情况
switch (checkedCount)
{
case 0:
{
// 没有任何单选框被选中,选择第一个
radioboxList[0].Checked = true;
break;
}
case var @case when @case > 1:
{
// 选中项目多于 1 个
if (Checked)
{
// 如果本控件选中,则取消其他所有控件的选中
foreach (var Control in radioboxList)
if (Control.Checked && !Control.Equals(this))
Control.Checked = false;
}
else
{
// 如果本控件未选中,则只保留第一个选中的控件
var firstChecked = false;
foreach (var Control in radioboxList)
if (Control.Checked)
{
if (firstChecked)
Control.Checked = false; // 修改 Checked 会自动触发 Change 事件,所以不用额外触发
else
firstChecked = true;
}
}
break;
}
}
}
var customHeight = 20d; // 修改左侧神秘动画条的高度
if (IsLoaded && ModAnimation.AniControlEnabled == 0 && anime) // 防止默认属性变更触发动画
{
var anim = new List<ModAnimation.AniData>();
if (Checked)
{
// 由无变有
if (rectCheck is not null)
{
// 统一静态属性:固定高度、居中对齐、无额外边距
rectCheck.Height = customHeight;
rectCheck.VerticalAlignment = VerticalAlignment.Center;
rectCheck.Margin = new Thickness(-1, 0d, 0d, 0d);
rectCheck.Opacity = 1d;
// 初始化缩放中心为正中心
var scale = new ScaleTransform(1d, 0d);
rectCheck.RenderTransformOrigin = new Point(0.5d, 0.5d);
rectCheck.RenderTransform = scale;
// 动画:让 ScaleY 从 0 弹性放大到 1
anim.Add(ModAnimation.AaDouble(
i => scale.ScaleY = Math.Max(0d, scale.ScaleY + (double)i),
1d - scale.ScaleY,
300,
ease: new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)
));
}
anim.Add(ModAnimation.AaColor(this, ForegroundProperty,
Height < 40d ? "ColorBrush3" : "ColorBrush2", 200));
}
else
{
// 由有变无
if (rectCheck is not null)
{
if (rectCheck.RenderTransform is not ScaleTransform)
{
rectCheck.RenderTransformOrigin = new Point(0.5d, 0.5d);
rectCheck.RenderTransform = new ScaleTransform(1d, 1d);
}
var scale = (ScaleTransform)rectCheck.RenderTransform;
// 动画:让 ScaleY 从当前值缩减到 0
anim.Add(ModAnimation.AaDouble(
i => scale.ScaleY = Math.Max(0d, scale.ScaleY + (double)i),
-scale.ScaleY,
120,
ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)
));
anim.Add(ModAnimation.AaOpacity(rectCheck, -rectCheck.Opacity, 70, 40));
}
anim.Add(ModAnimation.AaColor(this, ForegroundProperty, "ColorBrush1", 120));
}
ModAnimation.AniStart(anim, "MyListItem Checked " + Uuid);
}
else
{
// 不使用动画
ModAnimation.AniStop("MyListItem Checked " + Uuid);
if (Checked)
{
if (rectCheck is not null)
{
rectCheck.Height = customHeight; // 应用自定义固定高度
rectCheck.Margin = new Thickness(-1, 0d, 0d, 0d);
rectCheck.Opacity = 1d;
rectCheck.VerticalAlignment = VerticalAlignment.Center; // 居中
rectCheck.RenderTransform = null; // 清除缩放
}
SetResourceReference(ForegroundProperty, Height < 40d ? "ColorBrush3" : "ColorBrush2");
}
else
{
if (rectCheck is not null)
{
rectCheck.Height = 0d;
rectCheck.Margin = new Thickness(-1, 0d, 0d, 0d);
rectCheck.Opacity = 0d;
rectCheck.VerticalAlignment = VerticalAlignment.Center;
rectCheck.RenderTransform = null;
}
SetResourceReference(ForegroundProperty, "ColorBrush1");
}
}
}
catch (Exception ex)
{
ModBase.Log(ex, "设置 Checked 失败");
}
}
// 前景色绑定
public Brush Foreground
{
get => (Brush)GetValue(ForegroundProperty);
set => SetValue(ForegroundProperty, value);
}
public static readonly DependencyProperty ForegroundProperty = DependencyProperty.Register("Foreground",
typeof(Brush), typeof(MyListItem), new PropertyMetadata(ThemeManager.AppResources["ColorBrush1"]));
// 菜单与按钮绑定
public Action<MyListItem, EventArgs> ContentHandler { get; set; }
/// <summary>
/// 图标可选的圆角。仅当 Logo 为位图(MyImage 或 Canvas)时生效。
/// 当所有角的圆角半径均 ≥ 0 时,应用圆角效果;否则不应用。
/// </summary>
public CornerRadius LogoCornerRadius
{
get => field;
set
{
field = value;
if (pathLogo is Canvas) _canvasClipHandlerAdded = false;
ApplyLogoCornerRadius();
}
} = new CornerRadius(-1);
private bool IsLogoCornerRadiusEnabled() =>
LogoCornerRadius is { TopLeft: >= 0, TopRight: >= 0, BottomLeft: >= 0, BottomRight: >= 0 };
private void UpdateCanvasClip(Canvas c)
{
if (c is { ActualWidth: > 0, ActualHeight: > 0 })
{
var r = LogoCornerRadius;
double radius = Math.Max(Math.Max(r.TopLeft, r.TopRight), Math.Max(r.BottomLeft, r.BottomRight));
c.Clip = new RectangleGeometry(new Rect(0, 0, c.ActualWidth, c.ActualHeight), radius, radius);
}
}
private bool _canvasClipHandlerAdded = false;
private void ApplyLogoCornerRadius()
{
if (pathLogo is null || !IsLogoCornerRadiusEnabled()) return;
switch (pathLogo)
{
case MyImage myImage:
myImage.CornerRadius = LogoCornerRadius;
break;
case Canvas canvas when !_canvasClipHandlerAdded && canvas is { ActualWidth: 0, ActualHeight: 0 }:
UpdateCanvasClip(canvas);
canvas.SizeChanged += OnCanvasLogoSizeChanged;
_canvasClipHandlerAdded = true;
break;
case Canvas canvas:
UpdateCanvasClip(canvas);
break;
}
}
private void OnCanvasLogoSizeChanged(object? sender, SizeChangedEventArgs e)
{
if (sender is not Canvas { ActualWidth: > 0, ActualHeight: > 0 } canvas) return;
UpdateCanvasClip(canvas);
canvas.SizeChanged -= OnCanvasLogoSizeChanged;
_canvasClipHandlerAdded = false;
}
#endregion
#region
// 触发点击事件
private void Button_MouseUp(object sender, MouseButtonEventArgs e)
{
if (!isMouseDown)
return;
Click?.Invoke(sender, e);
if (e.Handled)
return;
// 触发自定义事件
var dependencyObject = (DependencyObject)sender;
if (CustomEventService.GetEventType(dependencyObject) != EventType.None)
{
ModMain.RaiseCustomEvent(this);
e.Handled = true;
}
if (e.Handled)
return;
// 实际的单击处理
switch (Type)
{
case CheckType.Clickable:
{
ModBase.Log("[Control] 按下单击列表项:" + Title);
break;
}
case CheckType.RadioBox:
{
ModBase.Log("[Control] 按下单选列表项:" + Title);
if (!Checked)
SetChecked(true, true, true);
break;
}
case CheckType.CheckBox:
{
ModBase.Log("[Control] 按下复选列表项(" + !Checked + "):" + Title);
SetChecked(!Checked, true, true);
break;
}
}
}
// 鼠标点击判定
private bool isMouseDown;
private void Button_MouseDown(object sender, MouseButtonEventArgs e)
{
if (IsMouseDirectlyOver && !(Type == CheckType.None))
{
isMouseDown = true;
if (buttonStack is not null)
buttonStack.IsHitTestVisible = false;
}
}
private void Button_MouseLeave(object sender, object e)
{
isMouseDown = false;
if (buttonStack is not null)
buttonStack.IsHitTestVisible = true;
}
#endregion
}