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");
+}