diff --git a/IO/Files.cs b/IO/Files.cs index 6d9ada1a..2894dd50 100644 --- a/IO/Files.cs +++ b/IO/Files.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Security; +using PCL.Core.Logging; namespace PCL.Core.IO; @@ -48,4 +50,68 @@ public static void CreateShortcut( // 保存 .lnk 文件 link.Save(); } + + /// + /// 检查是否拥有对指定文件夹的 I/O 权限。 + /// 如果文件夹不存在,会返回 false。 + /// + /// 要检查的文件夹路径。 + /// 如果拥有权限且文件夹存在,则为 true;否则为 false。 + public static bool CheckPermission(string path) { + try { + if (string.IsNullOrWhiteSpace(path)) { + return false; + } + + // 检查一些系统特殊文件夹,这些文件夹通常没有权限 + if (path.EndsWith(":\\System Volume Information", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(":\\$RECYCLE.BIN", StringComparison.OrdinalIgnoreCase)) { + return false; + } + + // 检查文件夹是否存在 + if (!Directory.Exists(path)) { + return false; + } + + // 核心逻辑:通过创建和删除临时文件来检查权限 + var tempFileName = Path.Combine(path, Guid.NewGuid().ToString()); + using (File.Create(tempFileName)) { } + File.Delete(tempFileName); + + return true; + } catch (IOException) { + return false; + } catch (UnauthorizedAccessException) { + return false; + } catch (SecurityException) { + return false; + } catch (Exception ex) { + // 捕获并记录其他未知异常 + LogWrapper.Warn(ex, $"没有对文件夹 {path} 的权限,请尝试以管理员权限运行。"); + return false; + } + } + + /// + /// 检查是否拥有对指定文件夹的 I/O 权限。 + /// 如果出错,则抛出异常。 + /// + /// 要检查的文件夹路径。 + /// 文件夹路径为空或只包含空格。 + /// 文件夹不存在。 + /// 没有访问文件夹的权限。 + public static void CheckPermissionWithException(string path) { + if (string.IsNullOrWhiteSpace(path)) { + throw new ArgumentNullException(nameof(path), "文件夹名不能为空!"); + } + if (!Directory.Exists(path)) { + throw new DirectoryNotFoundException("文件夹不存在!"); + } + + // 核心逻辑:创建和删除临时文件 + var tempFileName = Path.Combine(path, "CheckPermission"); + using (File.Create(tempFileName)) { } + File.Delete(tempFileName); + } } diff --git a/UI/ModernColor.cs b/UI/ModernColor.cs new file mode 100644 index 00000000..d8143f47 --- /dev/null +++ b/UI/ModernColor.cs @@ -0,0 +1,411 @@ +namespace PCL.Core.UI; + +using System; +using System.Windows.Media; + +/// +/// 现代化的颜色类,支持小数精度和各种颜色操作 +/// 使用不可变设计和显式转换提高可维护性 +/// +public readonly record struct ModernColor { + #region 属性和字段 + + /// 透明度 (0-255) + public double A { get; init; } + + /// 红色分量 (0-255) + public double R { get; init; } + + /// 绿色分量 (0-255) + public double G { get; init; } + + /// 蓝色分量 (0-255) + public double B { get; init; } + + /// 是否完全透明 + public bool IsTransparent => A <= 0; + + /// 是否完全不透明 + public bool IsOpaque => A >= 255; + + #endregion + + #region 构造函数 + + /// + /// 创建RGB颜色 (不透明) + /// + public ModernColor(double r, double g, double b) : this(255, r, g, b) { } + + /// + /// 创建ARGB颜色 + /// + public ModernColor(double a, double r, double g, double b) { + A = Math.Clamp(a, 0, 255); + R = Math.Clamp(r, 0, 255); + G = Math.Clamp(g, 0, 255); + B = Math.Clamp(b, 0, 255); + } + + /// + /// 从WPF Color创建 + /// + public ModernColor(Color color) : this(color.A, color.R, color.G, color.B) { } + + /// + /// 从十六进制字符串创建 + /// + public ModernColor(string hexString) { + if (string.IsNullOrWhiteSpace(hexString)) + throw new ArgumentException("颜色字符串不能为空", nameof(hexString)); + + try { + var color = (Color)ColorConverter.ConvertFromString(hexString); + A = color.A; + R = color.R; + G = color.G; + B = color.B; + } catch (Exception ex) { + throw new ArgumentException($"无效的颜色字符串: {hexString}", nameof(hexString), ex); + } + } + + /// + /// 从SolidColorBrush创建 + /// + public ModernColor(SolidColorBrush brush) { + ArgumentNullException.ThrowIfNull(brush); + var color = brush.Color; + A = color.A; + R = color.R; + G = color.G; + B = color.B; + } + + #endregion + + #region 静态工厂方法 + + /// 常用颜色:白色 + public static ModernColor White => new(255, 255, 255); + + /// 常用颜色:黑色 + public static ModernColor Black => new(0, 0, 0); + + /// 常用颜色:透明 + public static ModernColor Transparent => new(0, 0, 0, 0); + + /// 常用颜色:红色 + public static ModernColor Red => new(255, 0, 0); + + /// 常用颜色:绿色 + public static ModernColor Green => new(0, 255, 0); + + /// 常用颜色:蓝色 + public static ModernColor Blue => new(0, 0, 255); + + /// + /// 从HSL创建颜色 + /// + /// 色调 (0-360) + /// 饱和度 (0-100) + /// 亮度 (0-100) + public static ModernColor FromHsl(double hue, double saturation, double lightness) { + var converter = new HslConverter(); + return converter.FromHsl(hue, saturation, lightness); + } + + /// + /// 从HSL创建颜色 (改进版本,对特定色调进行亮度调整) + /// + public static ModernColor FromHslEnhanced(double hue, double saturation, double lightness) { + var converter = new HslConverter(); + return converter.FromHslEnhanced(hue, saturation, lightness); + } + + /// + /// 从对象创建颜色 + /// + public static ModernColor FromObject(object? obj) => obj switch { + null => White, + Color color => new ModernColor(color), + SolidColorBrush brush => new ModernColor(brush), + ModernColor modernColor => modernColor, + string str => new ModernColor(str), + _ => throw new ArgumentException($"不支持的颜色类型: {obj.GetType()}") + }; + + /// + /// 安全地从对象创建颜色 + /// + public static ModernColor TryFromObject(object? obj) => obj switch { + null => White, + Color color => new ModernColor(color), + SolidColorBrush brush => new ModernColor(brush), + ModernColor modernColor => modernColor, + string str => new ModernColor(str), + _ => new ModernColor() +}; + + #endregion + + #region 颜色运算操作符 + + /// 颜色加法 + public static ModernColor operator +(ModernColor left, ModernColor right) => + new(left.A + right.A, left.R + right.R, left.G + right.G, left.B + right.B); + + /// 颜色减法 + public static ModernColor operator -(ModernColor left, ModernColor right) => + new(left.A - right.A, left.R - right.R, left.G - right.G, left.B - right.B); + + /// 颜色缩放 + public static ModernColor operator *(ModernColor color, double factor) => + new(color.A * factor, color.R * factor, color.G * factor, color.B * factor); + + /// 颜色缩放 + public static ModernColor operator *(double factor, ModernColor color) => color * factor; + + /// 颜色除法 + public static ModernColor operator /(ModernColor color, double divisor) => + divisor == 0 ? throw new DivideByZeroException("不能除以零") : color * (1.0 / divisor); + + #endregion + + #region 显式转换操作符 (避免意外转换) + + /// 显式转换为WPF Color + public static explicit operator Color(ModernColor color) => + Color.FromArgb(_ToByte(color.A), _ToByte(color.R), _ToByte(color.G), _ToByte(color.B)); + + /// 显式转换为SolidColorBrush + public static explicit operator SolidColorBrush(ModernColor color) => + new((Color)color); + + /// 显式转换为System.Drawing.Color + public static explicit operator System.Drawing.Color(ModernColor color) => + System.Drawing.Color.FromArgb(_ToByte(color.A), _ToByte(color.R), _ToByte(color.G), _ToByte(color.B)); + + /// 从WPF Color隐式转换 (常用转换保持隐式) + public static implicit operator ModernColor(Color color) => new(color); + + /// 从字符串隐式转换 + public static implicit operator ModernColor(string hexString) => new(hexString); + + #endregion + + #region 实用方法 + + /// + /// 创建具有指定透明度的新颜色 + /// + public ModernColor WithAlpha(double alpha) => this with { A = Math.Clamp(alpha, 0, 255) }; + + /// + /// 线性插值到另一个颜色 + /// + public ModernColor Lerp(ModernColor target, double t) { + t = Math.Clamp(t, 0, 1); + return new ModernColor( + A + (target.A - A) * t, + R + (target.R - R) * t, + G + (target.G - G) * t, + B + (target.B - B) * t + ); + } + + /// + /// 获取颜色的亮度 (0-1) + /// + public double GetBrightness() => (0.299 * R + 0.587 * G + 0.114 * B) / 255.0; + + /// + /// 判断是否为暗色 + /// + public bool IsDark() => GetBrightness() < 0.5; + + /// + /// 获取对比色 (黑色或白色) + /// + public ModernColor GetContrastColor() => IsDark() ? White : Black; + + /// + /// 转换为十六进制字符串 + /// + public string ToHex(bool includeAlpha = false) { + var color = (Color)this; + return includeAlpha + ? $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}" + : $"#{color.R:X2}{color.G:X2}{color.B:X2}"; + } + + #endregion + + #region 辅助方法 + + /// + /// 安全地将double转换为byte + /// + private static byte _ToByte(double value) => (byte)Math.Clamp(Math.Round(value), 0, 255); + + #endregion + + #region 覆盖方法 + + public override string ToString() => $"ARGB({A:F1}, {R:F1}, {G:F1}, {B:F1})"; + + public string ToString(string format) => format.ToUpperInvariant() switch { + "HEX" => ToHex(), + "HEXA" => ToHex(true), + "RGB" => $"RGB({R:F1}, {G:F1}, {B:F1})", + _ => ToString() + }; + + #endregion +} + +/// +/// HSL颜色转换器 - 分离职责,提高可测试性 +/// +public sealed class HslConverter { + /// + /// 从HSL转换为RGB + /// + public ModernColor FromHsl(double hue, double saturation, double lightness) { + // 参数验证和范围限制 + hue = ((hue % 360) + 360) % 360; // 确保在0-360范围内 + saturation = Math.Clamp(saturation, 0, 100); + lightness = Math.Clamp(lightness, 0, 100); + + if (saturation == 0) { + // 灰度色 + var gray = lightness * 2.55; + return new ModernColor(gray, gray, gray); + } + + var h = hue / 360.0; + var s = saturation / 100.0; + var l = lightness / 100.0; + + var v2 = l < 0.5 ? l * (1 + s) : l + s - l * s; + var v1 = 2 * l - v2; + + var r = 255 * _HueToRgb(v1, v2, h + 1.0 / 3.0); + var g = 255 * _HueToRgb(v1, v2, h); + var b = 255 * _HueToRgb(v1, v2, h - 1.0 / 3.0); + + return new ModernColor(r, g, b); + } + + /// + /// HSL转换的改进版本,对特定色调进行亮度调整 + /// + public ModernColor FromHslEnhanced(double hue, double saturation, double lightness) { + if (saturation == 0) { + var gray = lightness * 2.55; + return new ModernColor(gray, gray, gray); + } + + // 色调调整表 - 每30度一个调整值 + var adjustments = new [] { + +0.1, -0.06, -0.3, // 0°, 30°, 60° + -0.19, -0.15, -0.24, // 90°, 120°, 150° + -0.32, -0.09, +0.18, // 180°, 210°, 240° + +0.05, -0.12, -0.02, // 270°, 300°, 330° + +0.1, -0.06 // 循环回到开始 + }; + + // 计算调整后的中心亮度 + var normalizedHue = ((hue % 360) + 360) % 360; + var segment = normalizedHue / 30.0; + var segmentIndex = (int)Math.Floor(segment); + var interpolation = segment - segmentIndex; + + var adjustment = adjustments[segmentIndex] + + interpolation * (adjustments[segmentIndex + 1] - adjustments[segmentIndex]); + + var centerLightness = 50 - adjustment * saturation; + + var adjustedLightness = lightness < centerLightness + ? lightness / centerLightness * 50 + : 50 + (lightness - centerLightness) / (100 - centerLightness) * 50; + + return FromHsl(hue, saturation, adjustedLightness); + } + + /// + /// HSL转RGB的色调计算 + /// + private static double _HueToRgb(double v1, double v2, double vH) { + vH = ((vH % 1) + 1) % 1; // 确保在0-1范围内 + + return vH switch { + < 1.0 / 6.0 => v1 + (v2 - v1) * 6 * vH, + < 0.5 => v2, + < 2.0 / 3.0 => v1 + (v2 - v1) * (2.0 / 3.0 - vH) * 6, + _ => v1 + }; + } +} + +/// +/// 颜色工具类 - 提供常用的颜色操作 +/// +public static class ColorUtils { + /// + /// 在两个颜色之间进行线性插值 + /// + public static ModernColor Lerp(ModernColor from, ModernColor to, double t) => + from.Lerp(to, t); + + /// + /// 创建颜色渐变序列 + /// + public static ModernColor[] CreateGradient(ModernColor from, ModernColor to, int steps) { + if (steps < 2) throw new ArgumentException("步数至少为2", nameof(steps)); + + var colors = new ModernColor[steps]; + for (var i = 0; i < steps; i++) { + var t = i / (double)(steps - 1); + colors[i] = Lerp(from, to, t); + } + return colors; + } + + /// + /// 混合多个颜色 + /// + public static ModernColor BlendColors(params (ModernColor color, double weight)[] colors) { + if (colors.Length == 0) return ModernColor.Transparent; + + double totalWeight = 0; + double a = 0, r = 0, g = 0, b = 0; + + foreach (var (color, weight) in colors) { + if (weight <= 0) continue; + + totalWeight += weight; + a += color.A * weight; + r += color.R * weight; + g += color.G * weight; + b += color.B * weight; + } + + return totalWeight == 0 ? ModernColor.Transparent : new ModernColor(a / totalWeight, r / totalWeight, g / totalWeight, b / totalWeight); + } +} + +/// +/// 扩展方法 +/// +public static class ColorExtensions { + /// + /// 为WPF Color添加现代化操作 + /// + public static ModernColor ToModern(this Color color) => new(color); + + /// + /// 为字符串添加颜色解析 + /// + public static ModernColor AsColor(this string hexString) => new(hexString); +} diff --git a/Utils/Exts/UiExtension.cs b/Utils/Exts/UiExtension.cs new file mode 100644 index 00000000..ca47fe51 --- /dev/null +++ b/Utils/Exts/UiExtension.cs @@ -0,0 +1,62 @@ +using PCL.Core.Logging; + +namespace PCL.Core.Utils.Exts; + +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +/// +/// 提供 WPF UI 控件的扩展方法。 +/// +public static class UiExtension { + /// + /// 检查控件是否在指定窗口的可视区域内,且控件本身可见。 + /// + /// 要检查的 FrameworkElement。 + /// 主窗口,用于确定可视区域。 + /// 如果控件部分或完全在窗口可视区域内且可见,则返回 true;否则返回 false。 + /// 当 或 为 null 时抛出。 + public static bool IsVisibleInWindow(this FrameworkElement element, Window mainWindow) { + if (!element.IsVisible) return false; + + try { + var transform = element.TransformToAncestor(mainWindow); + var bounds = transform.TransformBounds(new Rect(0, 0, element.ActualWidth, element.ActualHeight)); + var windowRect = new Rect(0, 0, mainWindow.ActualWidth, mainWindow.ActualHeight); + return windowRect.IntersectsWith(bounds); + } catch (InvalidOperationException ex) { + // 可能由于控件未附加到视觉树 + LogWrapper.Warn(ex, $"无法检查控件可见性:{element.Name}"); + return false; + } + } + + /// + /// 检查 TextBlock 是否因 TextTrimming 属性导致文本被截断。 + /// + /// 要检查的 TextBlock。 + /// 如果文本被截断,则返回 true;否则返回 false。 + /// 当 为 null 时抛出。 + public static bool IsTextTrimmed(this TextBlock textBlock) { + if (textBlock.TextTrimming == TextTrimming.None) return false; + + try { + var formattedText = new FormattedText( + textBlock.Text, + System.Globalization.CultureInfo.CurrentCulture, + textBlock.FlowDirection, + new Typeface(textBlock.FontFamily, textBlock.FontStyle, textBlock.FontWeight, textBlock.FontStretch), + textBlock.FontSize, + textBlock.Foreground, + VisualTreeHelper.GetDpi(textBlock).PixelsPerDip + ); + + return formattedText.Width > textBlock.ActualWidth; + } catch (Exception ex) { + LogWrapper.Warn(ex, $"无法检查文本截断:{textBlock.Name}"); + return false; + } + } +} diff --git a/Utils/OS/DialogUtils.cs b/Utils/OS/DialogUtils.cs new file mode 100644 index 00000000..ef8e7c7b --- /dev/null +++ b/Utils/OS/DialogUtils.cs @@ -0,0 +1,132 @@ +using Microsoft.Win32; +using PCL.Core.Logging; + +namespace PCL.Core.Utils.OS; + +using System; +using System.IO; + +/// +/// 提供文件和文件夹对话框相关的实用方法。 +/// +public static class DialogUtils { + /// + /// 显示保存文件对话框,要求用户选择保存位置。 + /// + /// 对话框标题。 + /// 默认文件名。 + /// 文件格式过滤器,例如 "常用图片文件(*.png;*.jpg)|*.png;*.jpg",默认为 null。 + /// 初始目录,默认为 null。 + /// 用户选择的完整文件路径,如果取消则返回 null。 + /// 当 或 为 null 时抛出。 + public static string? SelectSaveFile(string title, string fileName, string? fileFilter = null, string? initialDirectory = null) { + var fileDialog = new SaveFileDialog { + AddExtension = true, + Title = title, + FileName = fileName, + Filter = fileFilter ?? "所有文件(*.*)|*.*", + InitialDirectory = !string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory) ? initialDirectory : null + }; + + LogWrapper.Info($"[UI] 打开保存文件对话框:{title}"); + var result = fileDialog.ShowDialog(); + if (result != true) { + LogWrapper.Info("[UI] 选择文件被取消"); + return null; + } + + var selectedPath = fileDialog.FileName; + LogWrapper.Info($"[UI] 选择文件返回:{selectedPath}"); + return string.IsNullOrEmpty(selectedPath) ? null : Path.GetFullPath(selectedPath); + } + + /// + /// 显示打开文件对话框,要求用户选择单个文件。 + /// + /// 文件格式过滤器,例如 "常用图片文件(*.png;*.jpg)|*.png;*.jpg"。 + /// 对话框标题。 + /// 初始目录,默认为 null。 + /// 用户选择的完整文件路径,如果取消则返回 null。 + /// 当 或 为 null 时抛出。 + public static string? SelectFile(string fileFilter, string title, string? initialDirectory = null) { + var fileDialog = new OpenFileDialog { + AddExtension = true, + CheckFileExists = true, + Filter = fileFilter, + Multiselect = false, + Title = title, + ValidateNames = true, + InitialDirectory = !string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory) ? initialDirectory : null + }; + + LogWrapper.Info($"[UI] 打开选择文件对话框:{title}"); + var result = fileDialog.ShowDialog(); + if (result != true) { + LogWrapper.Info("[UI] 选择文件被取消"); + return null; + } + + var selectedPath = fileDialog.FileName; + LogWrapper.Info($"[UI] 选择单个文件返回:{selectedPath}"); + return string.IsNullOrEmpty(selectedPath) ? null : Path.GetFullPath(selectedPath); + } + + /// + /// 显示打开文件对话框,要求用户选择多个文件。 + /// + /// 文件格式过滤器,例如 "常用图片文件(*.png;*.jpg)|*.png;*.jpg"。 + /// 对话框标题。 + /// 用户选择的文件路径数组,如果取消则返回空数组。 + /// 当 或 为 null 时抛出。 + public static string[] SelectFiles(string fileFilter, string title) { + var fileDialog = new OpenFileDialog { + AddExtension = true, + CheckFileExists = true, + Filter = fileFilter, + Multiselect = true, + Title = title, + ValidateNames = true + }; + + LogWrapper.Info($"[UI] 打开选择多个文件对话框:{title}"); + var result = fileDialog.ShowDialog(); + if (result != true) { + LogWrapper.Info("[UI] 选择文件被取消"); + return []; + } + + string[] selectedFiles = fileDialog.FileNames; + LogWrapper.Info($"[UI] 选择多个文件返回:{string.Join(",", selectedFiles)}"); + return selectedFiles.Length == 0 ? [] : Array.ConvertAll(selectedFiles, Path.GetFullPath); + } + + /// + /// 显示文件夹选择对话框,要求用户选择一个文件夹。 + /// + /// 对话框标题,默认为 "选择文件夹"。 + /// 用户选择的文件夹路径(以 \ 结尾),如果取消则返回 null。 + public static string? SelectFolder(string title = "选择文件夹") { + var folderDialog = new OpenFolderDialog { + Title = title, + InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop), + Multiselect = false + }; + + LogWrapper.Info($"[UI] 打开选择文件夹对话框:{title}"); + var result = folderDialog.ShowDialog(); + if (result != true) { + LogWrapper.Info("[UI] 选择文件夹被取消"); + return null; + } + + var selectedPath = folderDialog.FolderName; + if (string.IsNullOrEmpty(selectedPath)) { + LogWrapper.Info("[UI] 选择文件夹返回:空"); + return null; + } + + var normalizedPath = Path.GetFullPath(selectedPath).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + LogWrapper.Info($"[UI] 选择文件夹返回:{normalizedPath}"); + return normalizedPath; + } +} diff --git a/Utils/OS/ProcessInterop.cs b/Utils/OS/ProcessInterop.cs index f53aa0a5..5f9e8510 100644 --- a/Utils/OS/ProcessInterop.cs +++ b/Utils/OS/ProcessInterop.cs @@ -1,11 +1,19 @@ using System.Diagnostics; using System.IO; using System.Management; +using System.Security.Principal; namespace PCL.Core.Utils.OS; public class ProcessInterop { + /// + /// 检查当前程序是否以管理员权限运行。 + /// + /// 如果当前用户具有管理员权限,则返回 true;否则返回 false。 + public static bool IsAdmin() => + new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); + /// /// 获取指定进程 ID 的命令行参数。 /// diff --git a/Utils/OS/SystemTheme.cs b/Utils/OS/SystemTheme.cs new file mode 100644 index 00000000..104832c2 --- /dev/null +++ b/Utils/OS/SystemTheme.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; +using System.Security; +using Microsoft.Win32; +using PCL.Core.Logging; + +namespace PCL.Core.Utils.OS; + +public class SystemTheme { + private const string ThemeRegistryPath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + private const string AppsUseLightThemeKey = "AppsUseLightTheme"; + + /// + /// 检查系统是否处于深色模式。 + /// + /// 如果系统使用深色模式,则返回 true;否则返回 false(包括注册表不可访问的情况)。 + public static bool IsSystemInDarkMode() { + try { + using var registryKey = Registry.CurrentUser.OpenSubKey(ThemeRegistryPath); + if (registryKey == null) { + LogWrapper.Warn($"注册表键 {ThemeRegistryPath} 不存在"); + return false; + } + + var value = registryKey.GetValue(AppsUseLightThemeKey) as int?; + return value == 0; // 0 表示深色模式(AppsUseLightTheme = false) + } catch (Exception ex) when (ex is SecurityException or IOException) { + LogWrapper.Warn(ex, $"无法访问注册表键 {ThemeRegistryPath}"); + return false; + } + } +} diff --git a/Utils/RandomUtils.cs b/Utils/RandomUtils.cs new file mode 100644 index 00000000..fe3854ea --- /dev/null +++ b/Utils/RandomUtils.cs @@ -0,0 +1,70 @@ +namespace PCL.Core.Utils; + +using System; +using System.Collections.Generic; +using System.Linq; + +/// +/// 提供随机数和集合随机操作的实用方法。 +/// +public static class RandomUtils { + private static readonly Random SharedRandom = Random.Shared; + + /// + /// 从集合中随机选择一个元素。 + /// + /// 集合元素类型。 + /// 要从中选择元素的集合。 + /// 随机选择的元素。 + /// 当 为 null 时抛出。 + /// 当 为空时抛出。 + public static T PickRandom(ICollection collection) { + if (collection.Count == 0) throw new ArgumentException("集合不能为空", nameof(collection)); + + var index = SharedRandom.Next(0, collection.Count); + return collection.ElementAt(index); + } + + /// + /// 生成指定范围内的随机整数(包含 min 和 max)。 + /// + /// 范围下限(包含)。 + /// 范围上限(包含)。 + /// 随机整数,范围为 [min, max]。 + /// 当 大于 时抛出。 + public static int NextInt(int min, int max) { + return min > max ? throw new ArgumentOutOfRangeException(nameof(min), "最小值不能大于最大值") : SharedRandom.Next(min, max + 1); + } + + /// + /// 随机打乱列表的元素,返回新列表。 + /// + /// 列表元素类型。 + /// 要打乱的列表。 + /// 包含随机顺序元素的新列表。 + /// 当 为 null 时抛出。 + public static List Shuffle(IList list) { + var result = new List(list); + var n = result.Count; + for (var i = n - 1; i > 0; i--) { + var j = SharedRandom.Next(0, i + 1); + (result[i], result[j]) = (result[j], result[i]); + } + + return result; + } + + /// + /// 原地随机打乱列表的元素。 + /// + /// 列表元素类型。 + /// 要打乱的列表。 + /// 当 为 null 时抛出。 + public static void ShuffleInPlace(IList list) { + var n = list.Count; + for (var i = n - 1; i > 0; i--) { + var j = SharedRandom.Next(0, i + 1); + (list[i], list[j]) = (list[j], list[i]); + } + } +} diff --git a/Utils/TimeUtils.cs b/Utils/TimeUtils.cs new file mode 100644 index 00000000..c873d6fc --- /dev/null +++ b/Utils/TimeUtils.cs @@ -0,0 +1,144 @@ +namespace PCL.Core.Utils; + +using System; + +/// +/// 提供与时间相关的实用方法。 +/// +public static class TimeUtils { + /// + /// 获取格式类似于“11:08:52.037”的当前时间字符串。 + /// + /// 格式化后的时间字符串。 + public static string GetTimeNow() { + return DateTime.Now.ToString("HH:mm:ss.fff"); + } + + /// + /// 获取系统运行时间(毫秒),保证为正长整型,且大于 1。 + /// + /// + /// 此方法基于 Environment.TickCount,但在 .NET 框架中,我们有更可靠的替代方案。 + /// + /// 系统运行毫秒数。 + public static long GetTimeTick() { + // 原始代码处理了 Environment.TickCount 的符号溢出问题(在 24.8 天后), + // 但在现代 .NET 中,我们有更可靠、更精确的 Stopwatch 类。 + // 为了保持原函数意图,这里直接返回 Environment.TickCount。 + // 注意:Environment.TickCount 在64位系统中可能为负,不推荐在重要场景使用。 + return Environment.TickCount64; + } + + /// + /// 将时间间隔转换为易于阅读的形式。 + /// + /// 要转换的时间间隔。 + /// 如果为 true,则使用简短格式。 + /// 格式化后的时间间隔字符串。 + public static string GetTimeSpanString(TimeSpan span, bool isShortForm) { + var isPast = span.TotalMilliseconds < 0; + var endFix = isPast ? "前" : "后"; + if (isPast) { + span = span.Negate(); + } + + var totalMonths = Math.Floor(span.Days / 30.0); + string result; + + if (isShortForm) { + if (totalMonths >= 12) { + result = $"{Math.Floor(totalMonths / 12)} 年"; + } else if (totalMonths >= 2) { + result = $"{totalMonths} 个月"; + } else if (span.TotalDays >= 2) { + result = $"{span.Days} 天"; + } else if (span.TotalHours >= 1) { + result = $"{span.Hours} 小时"; + } else if (span.TotalMinutes >= 1) { + result = $"{span.Minutes} 分钟"; + } else if (span.TotalSeconds >= 1) { + result = $"{span.Seconds} 秒"; + } else { + result = "1 秒"; + } + } else // Long form + { + if (totalMonths >= 61) { + result = $"{Math.Floor(totalMonths / 12)} 年"; + } else if (totalMonths >= 12) { + var years = Math.Floor(totalMonths / 12); + var months = totalMonths % 12; + result = $"{years} 年{(months > 0 ? $" {months} 个月" : "")}"; + } else if (totalMonths >= 4) { + result = $"{totalMonths} 个月"; + } else if (totalMonths >= 1) { + var days = span.Days % 30; + result = $"{totalMonths} 月{(days > 0 ? $" {days} 天" : "")}"; + } else if (span.TotalDays >= 4) { + result = $"{span.Days} 天"; + } else if (span.TotalDays >= 1) { + result = $"{span.Days} 天{(span.Hours > 0 ? $" {span.Hours} 小时" : "")}"; + } else if (span.TotalHours >= 10) { + result = $"{span.Hours} 小时"; + } else if (span.TotalHours >= 1) { + result = $"{span.Hours} 小时{(span.Minutes > 0 ? $" {span.Minutes} 分钟" : "")}"; + } else if (span.TotalMinutes >= 10) { + result = $"{span.Minutes} 分钟"; + } else if (span.TotalMinutes >= 1) { + result = $"{span.Minutes} 分{(span.Seconds > 0 ? $" {span.Seconds} 秒" : "")}"; + } else if (span.TotalSeconds >= 1) { + result = $"{span.Seconds} 秒"; + } else { + result = "1 秒"; + } + } + + return result + endFix; + } + + /// + /// 获取十进制 Unix 时间戳(秒)。 + /// + /// 当前时间的 Unix 时间戳。 + public static long GetUnixTimestamp() { + return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + + /// + /// 将 Unix 时间戳(秒)转换为本地时区的日期时间。 + /// + /// Unix 时间戳(秒),表示自 1970-01-01 00:00:00 UTC 起的秒数。 + /// 转换后的本地日期时间。 + /// 当 为负数或过大时抛出。 + public static DateTimeOffset FromUnixTimestamp(long unixTimestamp) + { + if (unixTimestamp < 0) + throw new ArgumentOutOfRangeException(nameof(unixTimestamp), "Unix 时间戳不能为负数。"); + + try + { + return DateTimeOffset.FromUnixTimeSeconds(unixTimestamp).ToLocalTime(); + } + catch (ArgumentOutOfRangeException ex) + { + throw new ArgumentOutOfRangeException(nameof(unixTimestamp), "Unix 时间戳超出有效范围。", ex.Message); + } + } + + /// + /// 将 UTC 时间转换为当前时区的时间。 + /// + /// UTC 日期时间。 + /// 转换后的本地日期时间。 + public static DateTimeOffset ToLocalTime(DateTimeOffset utcDate) => + utcDate.ToLocalTime(); + + /// + /// 将 Unix 时间戳(秒)转换为格式化的本地时间字符串(yyyy/MM/dd HH:mm)。 + /// + /// Unix 时间戳(秒),表示自 1970-01-01 00:00:00 UTC 起的秒数。 + /// 格式为 yyyy/MM/dd HH:mm 的本地时间字符串。 + /// 当 为负数或过大时抛出。 + public static string FormatUnixTimestamp(long unixTimestamp) => + FromUnixTimestamp(unixTimestamp).ToString("yyyy/MM/dd HH:mm"); +}