首次提交:本地项目同步到Gitea

This commit is contained in:
zhusenlin
2026-03-02 09:08:20 +08:00
commit 1fb681fb34
371 changed files with 31868 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
using Cowain.Base.Models;
using Ke.Bee.Localization.Providers.Abstractions;
using Microsoft.Extensions.Options;
using System.Globalization;
using System.Text.Json;
namespace Cowain.Base.Abstractions.Localization;
public abstract class LocalizationResourceContributorBase: ILocalizationResourceContributor
{
/// <summary>
/// 插件根目录
/// </summary>
private readonly string _pluginRoot;
/// <summary>
///
/// </summary>
/// <param name="appSettings">全局配置</param>
/// <param name="pluginName">插件名称</param>
public LocalizationResourceContributorBase(IOptions<AppSettings> appSettings, string pluginName)
{
_pluginRoot = Path.Combine(appSettings.Value.PluginPath, pluginName);
}
/// <summary>
/// 获取本地化资源的默认实现
/// </summary>
/// <param name="culture"></param>
/// <returns></returns>
public virtual Dictionary<string, string>? GetResource(CultureInfo culture)
{
var resource = JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllBytes(
Path.Combine(_pluginRoot, "i18n", $"{culture.IetfLanguageTag}.json")
));
return resource;
}
}

View File

@@ -0,0 +1,119 @@
using Cowain.Base.Helpers;
using Cowain.Base.Models;
using Cowain.Base.Models.Navigation;
using Cowain.Base.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System.ComponentModel;
using System.Reflection;
namespace Cowain.Base.Abstractions.Navigation;
/// <summary>
/// 默认视图导航器
/// </summary>
public class DefaultViewNavigator : IViewNavigator
{
/// <summary>
/// 当前页视图模型
/// </summary>
public PageViewModelBase? CurrentPage { get; private set; }
/// <summary>
/// 属性变化通知事件
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// 导航命令集合
/// </summary>
//private readonly IEnumerable<INavigationCommand> _commands;
private readonly IServiceProvider _serviceProvider;
public DefaultViewNavigator(IServiceProvider serviceProvider)
{
//_commands = commands;
_serviceProvider = serviceProvider;
}
/// <summary>
/// 导航到指定视图页面
/// </summary>
/// <param name="key"></param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public void NavigateTo(string key)
{
ArgumentExceptionHelper.ThrowIfNullOrEmpty(key);
string vmName = key + "ViewModel";
//var assemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();
//// 加载插件程序集
//var appSettings = _serviceProvider.GetService<IOptions<AppSettings>>();
//var pluginPath = appSettings?.Value.PluginPath;
//if (Directory.Exists(pluginPath))
//{
// var pluginFiles = Directory.GetFiles(pluginPath, "*.dll", SearchOption.AllDirectories);
// foreach (var file in pluginFiles)
// {
// var assembly = Assembly.LoadFrom(file);
// assemblies.Add(assembly);
// }
//}
List<Assembly>? assemblies = GlobalData.Instance["Assemblies"] as List<Assembly>;
if (assemblies == null)
{
throw new ArgumentOutOfRangeException(nameof(key), $"assemblies is null.");
}
var vmType = assemblies.SelectMany(a => a.GetTypes()).FirstOrDefault(t => t.Name == vmName);
if (vmType == null)
{
throw new ArgumentOutOfRangeException(nameof(key), $"ViewModel type '{vmName}' not found.");
}
if (_serviceProvider.GetService(vmType) is PageViewModelBase vm)
{
SetCurrentPage(vm);
}
else
{
throw new InvalidOperationException($"Unable to resolve ViewModel '{vmName}' from the service provider.");
}
}
/// <summary>
/// 设置当前视图模型
/// </summary>
/// <param name="page"></param>
public void SetCurrentPage(PageViewModelBase page)
{
CurrentPage = page;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentPage)));
}
public void ClearCurrentPage()
{
CurrentPage = null;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentPage)));
}
/// <summary>
/// 重载视图
/// </summary>
public void ReloadCurrentPage()
{
if (CurrentPage == null)
{
return;
}
// 使用 DI 容器创建实例 (因为视图模型都是以瞬态模式注入,所以获取的实例都是新实例)
if (_serviceProvider.GetService(CurrentPage.GetType()) is PageViewModelBase newPage)
{
SetCurrentPage(newPage);
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.Abstractions.Navigation
{
internal class IPageViewMenuActions
{
}
}

View File

@@ -0,0 +1,36 @@
using Cowain.Base.ViewModels;
using System.ComponentModel;
namespace Cowain.Base.Abstractions.Navigation;
/// <summary>
/// 视图导航器接口
/// </summary>
public interface IViewNavigator
{
/// <summary>
/// 属性变化通知事件
/// </summary>
event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// 当前页视图模型对象
/// </summary>
PageViewModelBase? CurrentPage { get; }
/// <summary>
/// 设置当前页
/// </summary>
/// <param name="page"></param>
void SetCurrentPage(PageViewModelBase page);
/// <summary>
/// 导航到视图
/// </summary>
/// <param name="key"></param>
void NavigateTo(string key);
/// <summary>
/// 重载视图
/// </summary>
void ReloadCurrentPage();
void ClearCurrentPage();
}

View File

@@ -0,0 +1,45 @@
using Cowain.Base.Models;
using Cowain.Base.Models.Menu;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace Cowain.Base.Abstractions.Plugin;
/// <summary>
/// 插件接口
/// </summary>
public interface IPlugin
{
/// <summary>
/// 插件名称
/// </summary>
string PluginName { get; }
/// <summary>
/// 执行方法
/// </summary>
/// <param name="methodName">方法名称</param>
/// <param name="parameters">参数</param>
/// <returns>执行结果对象</returns>
R? Execute<T, R>(string methodName, T? parameters) where R : ResultModel<R>;
/// <summary>
/// 初始化插件
/// </summary>
/// <param name="serviceProvider"></param>
void Initialize(IServiceProvider serviceProvider);
/// <summary>
/// 注册服务
/// </summary>
/// <param name="services"></param>
void RegisterServices(IServiceCollection services, List<Assembly>? _assemblies);
/// <summary>
/// 配置菜单
/// </summary>
/// <param name="menuContext"></param>
void ConfigureMenu(MenuConfigurationContext? menuContext);
/// <summary>
/// 关闭插件
/// </summary>
void Shutdown();
}

View File

@@ -0,0 +1,34 @@
using Cowain.Base.Models;
namespace Cowain.Base.Abstractions.Plugin;
/// <summary>
/// 执行插件方法的处理接口
/// </summary>
/// <typeparam name="T">方法请求参数数据类型</typeparam>
public interface IPluginMethodHandler<T>
{
/// <summary>
/// 执行方法,并返回结果对象
/// </summary>
/// <param name="parameters"></param>
/// <returns></returns>
Task<ResultModel> ExecuteAsync(T? parameters);
}
/// <summary>
/// 执行插件方法的处理接口
/// </summary>
/// <typeparam name="T">方法请求参数数据类型</typeparam>
/// <typeparam name="R">方法返回对象的 Data 属性数据类型</typeparam>
public interface IPluginMethodHandler<T, R>
{
/// <summary>
/// 执行方法,并返回带 R 类型数据的结果对象
/// </summary>
/// <typeparam name="R"></typeparam>
/// <param name="parameters"></param>
/// <returns></returns>
Task<ResultModel<R>> ExecuteAsync(T? parameters);
}

View File

@@ -0,0 +1,105 @@
using Cowain.Base.Models;
using Cowain.Base.Models.Menu;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Reflection;
using System.Text.Json;
namespace Cowain.Base.Abstractions.Plugin;
/// <summary>
/// 插件基类
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class PluginBase : IPlugin
{
private ILogger<PluginBase>? _logger;
/// <summary>
/// 插件名称
/// </summary>
public abstract string PluginName { get; }
public string? PluginPath { get; private set; }
/// <summary>
/// 配置插件菜单
/// </summary>
/// <param name="menuContext"></param>
public virtual void ConfigureMenu(MenuConfigurationContext? menuContext)
{
if (menuContext == null)
{
return;
}
if (string.IsNullOrEmpty(PluginPath))
{
return;
}
var menuFile = Path.Combine(PluginPath, PluginName, "Configs", "menus.json");
if (!File.Exists(menuFile))
{
return;
}
// 从插件 Configs 目录下读取插件菜单配置
var menus = JsonSerializer.Deserialize<List<MenuItem>>(File.ReadAllBytes(menuFile));
if (menus == null)
{
return;
}
foreach (var menu in menus.Where(x => x.Group == "Toolbar").ToList())
{
var item = menuContext.Menus.FirstOrDefault(x => x.Key == menu.Key);
if (item != null)
{
//一级菜单已经存在
if (menu.Items.Count > 0)
{
//有二级菜单,合并二级菜单
foreach (var menuItem in menu.Items)
{
var existingMenu = item.Items.FirstOrDefault(x => x.Key == menuItem.Key);
if (existingMenu == null)
{
item.Items.Add(menuItem);
}
}
}
else
{
//无二级菜单,更新一级菜单属性
item.CommandType = menu.CommandType;
item.CommandParameter = menu.CommandParameter;
item.PageActions = menu.PageActions;
}
}
else
{
//新的一级菜单
menuContext.Menus.Add(menu);
}
}
}
public abstract R? Execute<T, R>(string methodName, T? parameters) where R : ResultModel<R>;
public abstract void RegisterServices(IServiceCollection services, List<Assembly>? _assemblies);
public virtual void Initialize(IServiceProvider serviceProvider)
{
var appSettings = serviceProvider.GetRequiredService<IOptions<AppSettings>>().Value;
var menuConfigurationContext = serviceProvider.GetRequiredService<MenuConfigurationContext>();
PluginPath = appSettings.PluginPath;
ConfigureMenu(menuConfigurationContext);
_logger = serviceProvider.GetRequiredService<ILogger<PluginBase>>();
_logger?.LogInformation($"插件 {PluginName} 初始化");
}
public virtual void Shutdown()
{
_logger?.LogInformation($"插件 {PluginName} 关闭");
}
}

View File

@@ -0,0 +1,18 @@
using Microsoft.Extensions.DependencyInjection;
namespace Cowain.Base.Attributes;
/// <summary>
/// 服务注册特性 - 支持自定义生命周期
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class AutoRegisterAttribute : Attribute
{
public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Scoped;
public AutoRegisterAttribute(ServiceLifetime lifetime = ServiceLifetime.Scoped)
{
Lifetime = lifetime;
}
}

View File

@@ -0,0 +1,41 @@
using Cowain.Base.Helpers;
using Ke.Bee.Localization.Localizer.Abstractions;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.Attributes
{
public class MinLengthAttribute : ValidationAttribute
{
private readonly ILocalizer _l;
public MinLengthAttribute(int length)
{
Length = length;
_l = ServiceLocator.GetRequiredService<ILocalizer>();
}
public MinLengthAttribute(int length, string messageKey) : this(length)
{
MessageKey = messageKey;
}
public int Length { get; }
public string? MessageKey { get; }
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (value is string stringValue && stringValue.Length < Length)
{
var errorMessage = string.IsNullOrEmpty(MessageKey) ? $"The length of the string should be at least {Length} characters" : _l[MessageKey] + Length;
return new ValidationResult(errorMessage);
}
return ValidationResult.Success;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Cowain.Base.Attributes;
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class SingletonAttribute : Attribute
{
}

View File

@@ -0,0 +1,8 @@
namespace Cowain.Base.Attributes;
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class TransientAttribute : Attribute
{
}

View File

@@ -0,0 +1,32 @@
using Avalonia.Data.Converters;
using Avalonia.Markup.Xaml;
using System.Globalization;
namespace Cowain.Base.Converters;
public class BooleanToVisibilityConverter : MarkupExtension, IValueConverter
{
public bool FalseVisibility { get; set; } = false;
public bool Negate { get; set; }
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string? stringValue = value?.ToString();
if (bool.TryParse(stringValue, out var b))
{
return (Negate ? !b : b) ? true : FalseVisibility;
}
else if (double.TryParse(stringValue, out var d))
{
return (Negate ? !(d > 0) : (d > 0)) ? true : FalseVisibility;
}
bool result = value != null;
return (Negate ? !result : result) ? true : FalseVisibility;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> value is bool v && v;
public override object ProvideValue(IServiceProvider serviceProvider) => this;
}

View File

@@ -0,0 +1,38 @@
using Avalonia.Data.Converters;
using Cowain.Base.Helpers;
using Ke.Bee.Localization.Localizer.Abstractions;
using System.Globalization;
namespace Cowain.Base.Converters;
/// <summary>
/// 枚举值本地化转换器
/// </summary>
public class EnumLocalizeConverter : IValueConverter
{
private readonly ILocalizer _l;
public EnumLocalizeConverter()
{
_l = ServiceLocator.GetRequiredService<ILocalizer>();
}
/// <summary>
/// 枚举值转换为本地化文本
/// </summary>
/// <param name="value">枚举值</param>
/// <param name="targetType"></param>
/// <param name="prefix">对应的本地化前缀</param>
/// <param name="culture"></param>
/// <returns></returns>
public object? Convert(object? value, Type targetType, object? prefix, CultureInfo culture)
{
if (value != null)
return _l[$"{prefix}.{value}"];
return null;
}
public object? ConvertBack(object? value, Type targetType, object? prefix, CultureInfo culture)
{
return null;
}
}

View File

@@ -0,0 +1,22 @@
using Avalonia.Data.Converters;
using System;
using System.Globalization;
namespace Cowain.Base.Converters;
public class FirstLetterConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is string str && !string.IsNullOrEmpty(str))
{
return str[0].ToString();
}
return null;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,43 @@
using Avalonia.Data.Converters;
using Cowain.Base.Helpers;
using Ke.Bee.Localization.Localizer.Abstractions;
using System.Globalization;
namespace Cowain.Base.Converters;
/// <summary>
/// 枚举值本地化转换器
/// </summary>
public class I18nLocalizeConverter : IValueConverter
{
private readonly ILocalizer _l;
public I18nLocalizeConverter()
{
_l = ServiceLocator.GetRequiredService<ILocalizer>();
}
/// <summary>
/// 字符串转换为本地化文本
/// </summary>
/// <param name="value">枚举值</param>
/// <param name="targetType"></param>
/// <param name="prefix">对应的本地化前缀</param>
/// <param name="culture"></param>
/// <returns></returns>
public object? Convert(object? value, Type targetType, object? prefix, CultureInfo culture)
{
if (value == null)
return "未配置";
var key = $"{prefix}.{value}";
var result = _l[key];
// 如果本地化内容为空或与 key 相同,则返回原始 value 字符串
if (string.IsNullOrEmpty(result) || result == key)
return value.ToString();
return result;
}
public object? ConvertBack(object? value, Type targetType, object? prefix, CultureInfo culture)
{
return null;
}
}

View File

@@ -0,0 +1,49 @@
using Avalonia.Data.Converters;
using System.Collections;
using System.Globalization;
namespace Cowain.Base.Converters;
public class ListCountConverter : IValueConverter
{
public ListCountConverter()
{
}
public object? Convert(object? value, Type targetType, object? prefix, CultureInfo culture)
{
if (value == null)
return 0;
// 处理集合类型
if (value is IEnumerable collection)
{
// 优化处理实现了ICollection的集合
if (collection is ICollection collectionWithCount)
return collectionWithCount.Count;
// 否则遍历计数
int count = 0;
foreach (var item in collection)
count++;
return count;
}
// 处理字符串
if (value is string str)
return str.Length;
// 处理数组
if (value.GetType().IsArray)
return ((Array)value).Length;
// 未知类型返回1表示单个对象
return 1;
}
public object? ConvertBack(object? value, Type targetType, object? prefix, CultureInfo culture)
{
throw new NotSupportedException("ConvertBack is not supported.");
}
}

View File

@@ -0,0 +1,39 @@
using Avalonia.Data.Converters;
using Cowain.Base.Helpers;
using Ke.Bee.Localization.Localizer.Abstractions;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Reflection.Metadata;
using System.Text.Json;
namespace Cowain.Base.Converters;
/// <summary>
/// List和string转换器
/// </summary>
public class ListStringConverter : IValueConverter
{
public ListStringConverter()
{
}
public object? Convert(object? value, Type targetType, object? prefix, CultureInfo culture)
{
if (value is ObservableCollection<string> lists)
{
return JsonSerializer.Serialize(lists);
//return string.Join(',', menuActions);
}
return null;
}
public object? ConvertBack(object? value, Type targetType, object? prefix, CultureInfo culture)
{
if (value is string jsonString)
{
return JsonSerializer.Deserialize<ObservableCollection<string>>(jsonString);
}
return null;
}
}

View File

@@ -0,0 +1,41 @@
using Avalonia.Data.Converters;
using Cowain.Base.Helpers;
using Ke.Bee.Localization.Localizer.Abstractions;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Reflection.Metadata;
using System.Text.Json;
namespace Cowain.Base.Converters;
/// <summary>
/// 菜单权限转换格式显示
/// </summary>
public class MenuActionsConverter : IValueConverter
{
private readonly ILocalizer _l;
public MenuActionsConverter()
{
_l = ServiceLocator.GetRequiredService<ILocalizer>();
}
public object? Convert(object? value, Type targetType, object? prefix, CultureInfo culture)
{
if (value is ObservableCollection<string> menuActions)
{
return JsonSerializer.Serialize(menuActions);
//return string.Join(',', menuActions);
}
return null;
}
public object? ConvertBack(object? value, Type targetType, object? prefix, CultureInfo culture)
{
if (value is string jsonString)
{
return JsonSerializer.Deserialize<ObservableCollection<string>>(jsonString);
}
return null;
}
}

View File

@@ -0,0 +1,39 @@
using Avalonia.Data;
using Avalonia.Data.Converters;
using Cowain.Base.Helpers;
using Ke.Bee.Localization.Localizer.Abstractions;
using System.Globalization;
namespace Cowain.Base.Converters;
public class MultiI18nLocalizeConverter : IMultiValueConverter
{
private readonly ILocalizer _l;
public MultiI18nLocalizeConverter()
{
_l = ServiceLocator.GetRequiredService<ILocalizer>();
}
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
{
try
{
// 参数说明values[0]=项, values[1]=集合对象
if (values.Count < 2 || values[0] is null || values[1] is null)
return BindingOperations.DoNothing;
string? val = values[0] is string ss ? ss : "Null";
IEnumerable<string>? collection = values[1] as IEnumerable<string>;
// 从集合中查找项
object? item = collection?.FirstOrDefault(val);
if (item is null) return "Not Found";
return _l[$"{parameter}.{item}"];
}
catch
{
return BindingOperations.DoNothing;
}
}
}

View File

@@ -0,0 +1,65 @@
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Markup.Xaml;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.Converters;
/// <summary>
/// 通用ID到对象属性值转换器
/// 支持通过ID从集合中查找对象并返回该对象的指定属性值
/// </summary>
public class MultiIdToPropertyConverter : MarkupExtension, IMultiValueConverter
{
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
{
try
{
// 参数说明values[0]=ID值, values[1]=集合对象, values[2]=目标属性名(可选)
if (values.Count < 2 || values[0] is null || values[1] is null)
return BindingOperations.DoNothing;
object? id = values[0];
object? collection = values[1];
string targetProperty = (values.Count >= 3 && values[2] is string s && !string.IsNullOrEmpty(s))
? s
: "Name"; // 默认属性名为 "Name"
// 从集合中查找项
object? item = (id is not null && collection is not null) ? FindItemById(collection, id) : null;
if (item is null) return "Null";
var propertyInfo = item.GetType().GetProperty(targetProperty);
return propertyInfo?.GetValue(item) ?? "Null";
}
catch
{
return BindingOperations.DoNothing;
}
}
private object? FindItemById(object collection, object id)
{
switch (collection)
{
case IDictionary dict:
return id is not null && dict.Contains(id) ? dict[id] : null;
case IEnumerable enumerable:
foreach (var item in enumerable)
{
var itemId = item?.GetType().GetProperty("Id")?.GetValue(item);
if (itemId is not null && itemId.Equals(id))
return item;
}
break;
}
return null;
}
public override object ProvideValue(IServiceProvider serviceProvider) => this;
}

View File

@@ -0,0 +1,26 @@
using Avalonia.Data.Converters;
using System.Globalization;
namespace Cowain.Base.Converters;
public class StringToBoolConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value != null && bool.TryParse(value.ToString(), out bool v))
{
return v;
}
return false;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value != null && value is bool boolValue)
{
return boolValue.ToString();
}
return string.Empty;
}
}

View File

@@ -0,0 +1,55 @@
using Avalonia.Data.Converters;
using Avalonia.Media;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.Converters;
public class StringToBrushConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not string colorStr || string.IsNullOrWhiteSpace(colorStr))
{
// 默认颜色(透明/黑色)
return parameter?.ToString() == "Foreground"
? Brushes.Black
: Brushes.Transparent;
}
try
{
// 处理十六进制颜色(#FF0000 或 FF0000
if (colorStr.StartsWith("#"))
{
return new SolidColorBrush(Color.Parse(colorStr));
}
// 处理命名颜色Red/Green/Blue等
else if (Color.TryParse(colorStr, out var color))
{
return new SolidColorBrush(color);
}
}
catch
{
// 转换失败返回默认色
return parameter?.ToString() == "Foreground"
? Brushes.Black
: Brushes.Transparent;
}
// 兜底默认色
return parameter?.ToString() == "Foreground"
? Brushes.Black
: Brushes.Transparent;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,38 @@
using Avalonia.Data.Converters;
using System.Globalization;
namespace Cowain.Base.Converters;
public class StringToByteConverter : IValueConverter
{
/// <summary>
/// 将数据模型中的值转换为字符串,流向 UI 元素时
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value ?? string.Empty;
}
/// <summary>
/// 用户在 UI 中输入或修改了某些内容,并且这些更改需要反映到数据模型时
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value != null && byte.TryParse(value.ToString(), out var v))
{
return v;
}
return null;
}
}

View File

@@ -0,0 +1,38 @@
using Avalonia.Data.Converters;
using System.Globalization;
namespace Cowain.Base.Converters;
public class StringToUIntConverter : IValueConverter
{
/// <summary>
/// 将数据模型中的值转换为字符串,流向 UI 元素时
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value ?? string.Empty;
}
/// <summary>
/// 用户在 UI 中输入或修改了某些内容,并且这些更改需要反映到数据模型时
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value != null && uint.TryParse(value.ToString(), out var intValue))
{
return intValue;
}
return null;
}
}

View File

@@ -0,0 +1,17 @@
using Avalonia.Data.Converters;
namespace Cowain.Base.Converters;
public static class StringVisibilityConverter
{
public static readonly IValueConverter IsNullOrEmpty =
new FuncValueConverter<string?, bool>(x => string.IsNullOrEmpty(x));
/// <summary>
/// A value converter that returns true if the input object is not null.
/// </summary>
public static readonly IValueConverter IsNotNullOrEmpty =
new FuncValueConverter<string?, bool>(x => !string.IsNullOrEmpty(x));
}

View File

@@ -0,0 +1,27 @@
using Avalonia.Data.Converters;
using Avalonia.Media;
using Cowain.Base.Models.Controls;
using System.Globalization;
namespace Cowain.Base.Converters;
public class ToastrIconConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is ToastrType toastrType)
{
return toastrType switch
{
ToastrType.Success => StreamGeometry.Parse("M512 21.333333C782.976 21.333333 1002.666667 241.024 1002.666667 512S782.976 1002.666667 512 1002.666667 21.333333 782.976 21.333333 512 241.024 21.333333 512 21.333333z m211.925333 337.408a35.029333 35.029333 0 0 0-49.536 0l-222.805333 222.72-90.026667-90.026666a35.072 35.072 0 1 0-49.578666 49.621333l111.530666 111.573333a35.413333 35.413333 0 0 0 11.605334 9.770667 35.029333 35.029333 0 0 0 47.573333-12.8l241.237333-241.322667a35.029333 35.029333 0 0 0 0-49.578666z"),
_ => StreamGeometry.Parse("M512 128a384 384 0 1 1-0.042667 768.042667A384 384 0 0 1 512 128z m-19.2 553.258667l76.501333-248.362667-16-1.408-91.52 11.690667-4.693333 13.994666c3.754667 0.085333 6.954667 0.256 9.6 0.512a17.066667 17.066667 0 0 1 13.226667 7.253334c2.986667 4.266667 4.266667 9.088 3.84 14.293333-0.725333 8.277333-3.584 20.48-8.533334 36.565333l-51.754666 166.954667c-4.181333 13.312-6.656 24.277333-7.424 33.109333-1.152 13.226667 1.28 24.533333 7.338666 33.792a33.28 33.28 0 0 0 26.282667 15.402667c30.08 2.645333 61.098667-26.112 93.269333-86.229333l-8.362666-7.936c-13.141333 22.016-24.106667 37.077333-32.853334 45.184-3.285333 3.328-6.4 4.864-9.429333 4.565333-1.834667-0.128-3.413333-1.365333-4.778667-3.797333a12.714667 12.714667 0 0 1-1.706666-7.509334c0.384-4.437333 2.688-13.866667 6.954666-28.074666zM554.496 384a41.301333 41.301333 0 0 0 30.293333-12.458667A41.301333 41.301333 0 0 0 597.333333 341.333333a41.429333 41.429333 0 0 0-12.373333-30.208 41.344 41.344 0 0 0-30.421333-12.458666 40.96 40.96 0 0 0-30.165334 12.458666A41.429333 41.429333 0 0 0 512 341.333333c0 11.776 4.138667 21.845333 12.501333 30.208 8.405333 8.277333 18.346667 12.458667 30.037334 12.458667z")
};
}
return null;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return null;
}
}

View File

@@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Company>Cowain Zhusenlin</Company>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AspectInjector" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="Dapper" />
<PackageReference Include="Fody">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Irihi.Ursa" />
<PackageReference Include="Ke.Bee.Localization" />
<PackageReference Include="MethodBoundaryAspect.Fody" />
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
<PackageReference Include="NPOI" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" />
<PackageReference Include="Serilog" />
<PackageReference Include="Xaml.Behaviors.Avalonia" />
</ItemGroup>
<ItemGroup>
<Folder Include="Themes\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,13 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.DBContext;
public interface IDataSeeding
{
void DataSeeding(ModelBuilder modelBuilder);
}

View File

@@ -0,0 +1,107 @@
using Cowain.Base.Helpers;
using Cowain.Base.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using System.Reflection;
namespace Cowain.Base.DBContext;
public class SqlDbContext : DbContext
{
private List<Assembly>? _assemblies;
/// <summary>
/// dbcontext
/// </summary>
/// <param name="options">options</param>
public SqlDbContext(DbContextOptions options) : base(options)
{
_assemblies = [.. GetAssembliesFromPluginDirectory()];
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
base.OnConfiguring(options);
}
override protected void OnModelCreating(ModelBuilder modelBuilder)
{
MapModelEntities(modelBuilder);
base.OnModelCreating(modelBuilder);
}
/// <summary>
/// over write EnsureCreated
/// </summary>
/// <returns></returns>
public virtual bool EnsureCreated()
{
return Database.EnsureCreated();
}
/// <summary>
/// over write EnsureCreated
/// </summary>
/// <returns></returns>
public async Task<bool> EnsureCreatedAsync()
{
return await Database.EnsureCreatedAsync();
}
/// <summary>
/// Auto Mapping Entity
/// </summary>
/// <param name="modelBuilder">ModelBuilder</param>
private void MapModelEntities(ModelBuilder modelBuilder)
{
var baseModelType = typeof(BaseModel);
var entityTypes = _assemblies?
.SelectMany(assembly => assembly.GetTypes())
.Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(baseModelType));
if (entityTypes == null)
{
return;
}
foreach (var type in entityTypes)
{
modelBuilder.Model.AddEntityType(type);
}
}
/// <summary>
/// Get all assemblies including those in the plugin directory
/// </summary>
/// <returns>IEnumerable<Assembly></returns>
private IEnumerable<Assembly> GetAssembliesFromPluginDirectory()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();
var pluginPath = Path.Combine(AppContext.BaseDirectory, "Plugins");
if (Directory.Exists(pluginPath))
{
var pluginAssemblies = Directory.GetFiles(pluginPath, "Plugin.*.dll")
.Select(Assembly.LoadFrom);
assemblies.AddRange(pluginAssemblies);
}
return assemblies;
}
/// <summary>
/// create DbSet
/// </summary>
/// <typeparam name="T">Entity</typeparam>
/// <returns></returns>
public virtual DbSet<T> GetDbSet<T>() where T : class
{
if (Model.FindEntityType(typeof(T)) != null)
{
return Set<T>();
}
else
{
throw new Exception($"type {typeof(T).Name} is not add into DbContext ");
}
}
}

View File

@@ -0,0 +1,47 @@
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Cowain.Base.Helpers;
using System.Reflection;
namespace Cowain.Base.Extensions;
public class GlobalDataExtensions : MarkupExtension
{
private string _key;
private string? _path;
public GlobalDataExtensions(string key)
{
_key = key;
}
public GlobalDataExtensions(string key, string path) : this(key)
{
_path = path;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (string.IsNullOrEmpty(_path))
{
var binding = new ReflectionBindingExtension("[" + _key + "]")
{
Mode = BindingMode.OneWay,
Source = GlobalData.Instance
};
return binding.ProvideValue(serviceProvider);
}
else
{
//绑定复杂的对象表达式
var bindingPath = $"[{_key}].{_path}";
var binding = new ReflectionBindingExtension(bindingPath)
{
Mode = BindingMode.OneWay,
Source = GlobalData.Instance
};
return binding.ProvideValue(serviceProvider);
}
}
}

View File

@@ -0,0 +1,41 @@
using Avalonia.Markup.Xaml;
using Cowain.Base.Helpers;
using System.Collections.ObjectModel;
namespace Cowain.Base.Extensions;
public class MenuEnableExtension : MarkupExtension
{
private string _pageName;
private string _menuKey;
public MenuEnableExtension(string pageName, string menuKey)
{
_pageName = pageName;
_menuKey = menuKey;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
var _executes = GetMenuActions(_pageName);
if (_executes == null)
{
return false;
}
return _executes.Any(action => action.Equals(_menuKey, StringComparison.OrdinalIgnoreCase));
}
public ObservableCollection<string>? GetMenuActions(string? pageName)
{
if (string.IsNullOrWhiteSpace(pageName) || !pageName.EndsWith("View"))
{
return null;
}
var menuKey = pageName.Substring(0, pageName.Length - "View".Length);
var menu = GlobalData.Instance.CurrentUser?.Menus?.FirstOrDefault(menu => menu.MenuKey == menuKey);
if (menu == null)
{
return null;
}
return menu.MenuActions;
}
}

View File

@@ -0,0 +1,27 @@
using System.Buffers.Binary;
namespace Cowain.Base.Extensions;
public static class ShortArrayExtensions
{
/// <summary>
/// 将 short[] 转换为 byte[],并可选择大小端。
/// </summary>
/// <param name="src">源数组</param>
/// <param name="asLittleEndian">true 为小端false 为大端</param>
public static byte[] ToByteArray(this short[] src, bool asLittleEndian = true)
{
if (src == null) throw new ArgumentNullException(nameof(src));
var dst = new byte[src.Length * sizeof(short)];
for (int i = 0; i < src.Length; i++)
{
var span = dst.AsSpan(i * sizeof(short));
if (asLittleEndian)
BinaryPrimitives.WriteInt16LittleEndian(span, src[i]);
else
BinaryPrimitives.WriteInt16BigEndian(span, src[i]);
}
return dst;
}
}

View File

@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<MethodBoundaryAspect />
</Weavers>

View File

@@ -0,0 +1,26 @@
using Cowain.Base.Abstractions.Navigation;
using Cowain.Base.Models.Navigation;
using System.Runtime.CompilerServices;
namespace Cowain.Base.Helpers;
/// <summary>
/// 参数异常助手类
/// </summary>
public class ArgumentExceptionHelper
{
public static void ThrowIfNullOrEmpty(string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
{
if (string.IsNullOrEmpty(argument))
throw new ArgumentNullException(paramName);
}
public static void ThrowIfOutOfRange(bool argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
{
if (argument)
throw new ArgumentOutOfRangeException(paramName);
}
}

View File

@@ -0,0 +1,49 @@
using Microsoft.Extensions.Hosting;
namespace Cowain.Base.Helpers;
public abstract class BackgroundHostedService : IHostedService, IDisposable
{
private Task? _executeTask;
private CancellationTokenSource? _stoppingCts;
public virtual Task? ExecuteTask => _executeTask;
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_executeTask = ExecuteAsync(_stoppingCts.Token);
if (_executeTask.IsCompleted)
{
return _executeTask;
}
return Task.CompletedTask;
}
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
if (_executeTask != null)
{
var stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
try
{
stoppingCts.Cancel();
}
finally
{
await _executeTask.WaitAsync(stoppingCts.Token).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
}
}
}
public virtual void Dispose()
{
_stoppingCts?.Cancel();
}
}

View File

@@ -0,0 +1,15 @@
using Avalonia;
namespace Cowain.Base.Helpers;
public class BindingProxy : AvaloniaObject
{
public static readonly StyledProperty<object?> DataContextProperty =
AvaloniaProperty.Register<BindingProxy, object?>(nameof(DataContext));
public object? DataContext
{
get => GetValue(DataContextProperty);
set => SetValue(DataContextProperty, value);
}
}

View File

@@ -0,0 +1,75 @@
using System.Security.Cryptography;
using System.Text;
namespace Cowain.Base.Helpers;
public class DESHelper
{
public static string Encrypt(string password, string key)
{
if (key.Length != 8)
{
return "";
}
if (string.IsNullOrEmpty(password))
{
return "";
}
using (DES des = DES.Create())
{
byte[] bytes = Encoding.Default.GetBytes(password);
des.Key = Encoding.ASCII.GetBytes(key);
des.IV = Encoding.ASCII.GetBytes(key);
using (MemoryStream memoryStream = new MemoryStream())
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, des.CreateEncryptor(), CryptoStreamMode.Write))
{
cryptoStream.Write(bytes, 0, bytes.Length);
cryptoStream.FlushFinalBlock();
StringBuilder stringBuilder = new StringBuilder();
byte[] array = memoryStream.ToArray();
foreach (byte b in array)
{
stringBuilder.AppendFormat("{0:X2}", b);
}
return stringBuilder.ToString();
}
}
}
}
public static string Decrypt(string password, string key)
{
if (key.Length != 8)
{
return "";
}
if (string.IsNullOrEmpty(password))
{
return "";
}
using (DES des = DES.Create())
{
byte[] array = new byte[password.Length / 2];
for (int i = 0; i < password.Length / 2; i++)
{
int num = Convert.ToInt32(password.Substring(i * 2, 2), 16);
array[i] = (byte)num;
}
des.Key = Encoding.ASCII.GetBytes(key);
des.IV = Encoding.ASCII.GetBytes(key);
using (MemoryStream memoryStream = new MemoryStream())
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, des.CreateDecryptor(), CryptoStreamMode.Write))
{
cryptoStream.Write(array, 0, array.Length);
cryptoStream.FlushFinalBlock();
return Encoding.Default.GetString(memoryStream.ToArray());
}
}
}
}
}

View File

@@ -0,0 +1,25 @@
namespace Cowain.Base.Helpers;
/// <summary>
/// 防抖器
/// </summary>
public class Debounce
{
private readonly int _delay;
private Timer? _timer;
public Debounce(int delay)
{
_delay = delay;
}
public void Trigger(Action action)
{
// 清除之前的定时器
_timer?.Dispose();
_timer = null;
// 创建新的定时器
_timer = new Timer(_ => action(), null, _delay, Timeout.Infinite);
}
}

View File

@@ -0,0 +1,155 @@
using Cowain.Base.Models;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
namespace Cowain.Base.Helpers;
public class ExcelHelper<T> where T : class
{
public static ResultModel ExportExcel(List<T> data, string filePath)
{
if (data == null || data.Count == 0)
return ResultModel.Error("数据不能为空");
if (string.IsNullOrWhiteSpace(filePath))
return ResultModel.Error("路径不能为空");
try
{
// 创建Excel工作簿
IWorkbook workbook = new XSSFWorkbook();
ISheet sheet = workbook.CreateSheet("Sheet1");
// 获取对象属性作为表头
var properties = typeof(T).GetProperties();
// 创建表头行
IRow headerRow = sheet.CreateRow(0);
for (int i = 0; i < properties.Length; i++)
{
headerRow.CreateCell(i).SetCellValue(properties[i].Name);
}
// 填充数据
for (int i = 0; i < data.Count; i++)
{
IRow row = sheet.CreateRow(i + 1);
var item = data[i];
for (int j = 0; j < properties.Length; j++)
{
var value = properties[j].GetValue(item)?.ToString() ?? "";
row.CreateCell(j).SetCellValue(value);
}
}
// 保存文件
using (FileStream fs = new FileStream(filePath, FileMode.Create))
{
workbook.Write(fs);
}
return ResultModel.Success("导出成功");
}
catch (Exception ex)
{
return ResultModel.Error($"错误:{ex.Message}");
}
}
public static async Task<ResultModel> ExportExcelAsync(List<T> data, string filePath)
{
if (data == null || data.Count == 0)
return ResultModel.Error("数据不能为空");
if (string.IsNullOrWhiteSpace(filePath))
return ResultModel.Error("路径不能为空");
try
{
// 创建Excel工作簿
IWorkbook workbook = new XSSFWorkbook();
ISheet sheet = workbook.CreateSheet("Sheet1");
// 获取对象属性作为表头
var properties = typeof(T).GetProperties();
// 创建表头行
IRow headerRow = sheet.CreateRow(0);
for (int i = 0; i < properties.Length; i++)
{
headerRow.CreateCell(i).SetCellValue(properties[i].Name);
}
// 填充数据
for (int i = 0; i < data.Count; i++)
{
IRow row = sheet.CreateRow(i + 1);
var item = data[i];
for (int j = 0; j < properties.Length; j++)
{
var value = properties[j].GetValue(item)?.ToString() ?? "";
row.CreateCell(j).SetCellValue(value);
}
}
// 保存文件
using (FileStream fs = new FileStream(filePath, FileMode.Create))
{
await Task.Run(() => workbook.Write(fs));
}
return ResultModel.Success("导出成功");
}
catch (Exception ex)
{
return ResultModel.Error($"错误:{ex.Message}");
}
}
public static ResultModel<List<T>> ImportExcel(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
return ResultModel<List<T>>.Error("路径不能为空");
try
{
List<T> data = new List<T>();
IWorkbook workbook;
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
workbook = new XSSFWorkbook(fs);
}
ISheet sheet = workbook.GetSheetAt(0);
if (sheet == null)
return ResultModel<List<T>>.Error("未找到工作表");
var properties = typeof(T).GetProperties();
for (int i = 1; i <= sheet.LastRowNum; i++)
{
IRow row = sheet.GetRow(i);
if (row == null) continue;
T item = Activator.CreateInstance<T>();
for (int j = 0; j < properties.Length; j++)
{
var cell = row.GetCell(j);
if (cell != null)
{
var value = cell.ToString();
properties[j].SetValue(item, Convert.ChangeType(value, properties[j].PropertyType));
}
}
data.Add(item);
}
return ResultModel<List<T>>.Success(data);
}
catch (Exception ex)
{
return ResultModel<List<T>>.Error($"错误:{ex.Message}");
}
}
}

View File

@@ -0,0 +1,63 @@
using Avalonia.Platform.Storage;
using Cowain.Base.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.Helpers;
public static class FileDialogHelper
{
public static IStorageProvider? Provider { get; set; }
public static async Task<ResultModel<IStorageFile>> OpenFileDialogAsync()
{
var files = await OpenFileDialogAsync(new List<FilePickerFileType>() { FilePickerFileTypes.All }, false);
if (!files.IsSuccess || files.Data is null || !files.Data.Any())
return ResultModel<IStorageFile>.Error(files.ErrorMessage);
return ResultModel<IStorageFile>.Success(files.Data.First());
}
public static async Task<ResultModel<IStorageFile>> OpenFileDialogAsync(List<FilePickerFileType>? fileTypes)
{
var files = await OpenFileDialogAsync(fileTypes, false);
if (!files.IsSuccess || files.Data is null || !files.Data.Any())
return ResultModel<IStorageFile>.Error(files.ErrorMessage);
return ResultModel<IStorageFile>.Success(files.Data.First());
}
public static async Task<ResultModel<IReadOnlyList<IStorageFile>>> OpenFileDialogAsync(List<FilePickerFileType>? fileTypes, bool multiple = false)
{
if (Provider is null) return ResultModel<IReadOnlyList<IStorageFile>>.Error("Provider is null");
var result = await Provider.OpenFilePickerAsync(new FilePickerOpenOptions()
{
Title = "Open File",
FileTypeFilter = fileTypes,
AllowMultiple = multiple,
});
if (result is null) return ResultModel<IReadOnlyList<IStorageFile>>.Error("file is null");
return ResultModel<IReadOnlyList<IStorageFile>>.Success(result);
}
public static async Task<ResultModel<IStorageFile>> SaveFileDialogAsync()
{
return await SaveFileDialogAsync(new List<FilePickerFileType>() { FilePickerFileTypes.All });
}
public static async Task<ResultModel<IStorageFile>> SaveFileDialogAsync(List<FilePickerFileType>? fileTypes)
{
if (Provider is null) return ResultModel<IStorageFile>.Error("Provider is null");
var result = await Provider.SaveFilePickerAsync(new FilePickerSaveOptions()
{
Title = "Save File",
FileTypeChoices = fileTypes,
});
if (result is null) return ResultModel<IStorageFile>.Error("file is null");
return ResultModel<IStorageFile>.Success(result);
}
}

View File

@@ -0,0 +1,72 @@
using Cowain.Base.Models;
using Cowain.Base.ViewModels;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Cowain.Base.Helpers;
public class GlobalData : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private static GlobalData? _instance;
private static readonly object _lock = new object();
private GlobalData()
{
}
public static GlobalData Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new GlobalData();
}
}
}
return _instance;
}
}
private IDictionary<string, object> _activeDictionary = new Dictionary<string, object>();
public LoginUserViewModel? CurrentUser { get; set; }
public void AddOrUpdate(string key, object? value)
{
if (value is null)
{
return;
}
if (_activeDictionary.ContainsKey(key))
{
_activeDictionary[key] = value;
//字典中的值发生变化时触发PropertyChanged事件
RaisePropertyChanged("Item");
}
else
{
_activeDictionary.Add(key, value);
// 添加新键时通知整个索引器
RaisePropertyChanged("Item[]");
}
}
public object? this[string key]
{
get
{
return _activeDictionary.TryGetValue(key, out var value) ? value : null;
}
}
}

View File

@@ -0,0 +1,98 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
namespace Cowain.Base.Helpers;
public class GridDefinitionsHelper
{
// 定义Rows附加属性
public static readonly AttachedProperty<int> RowsProperty =
AvaloniaProperty.RegisterAttached<GridDefinitionsHelper, Grid, int>("Rows", 0);
// 定义Columns附加属性
public static readonly AttachedProperty<int> ColumnsProperty =
AvaloniaProperty.RegisterAttached<GridDefinitionsHelper, Grid, int>("Columns", 0);
// 定义RowHeight附加属性
public static readonly AttachedProperty<string> RowHeightProperty =
AvaloniaProperty.RegisterAttached<GridDefinitionsHelper, Grid, string>("RowHeight", "Auto");
// 定义ColumnWidth附加属性
public static readonly AttachedProperty<string> ColumnWidthProperty =
AvaloniaProperty.RegisterAttached<GridDefinitionsHelper, Grid, string>("ColumnWidth", "Auto");
// Getter和Setter方法
public static int GetRows(AvaloniaObject element) => element.GetValue(RowsProperty);
public static void SetRows(AvaloniaObject element, int value) => element.SetValue(RowsProperty, value);
public static int GetColumns(AvaloniaObject element) => element.GetValue(ColumnsProperty);
public static void SetColumns(AvaloniaObject element, int value) => element.SetValue(ColumnsProperty, value);
public static string GetRowHeight(AvaloniaObject element) => element.GetValue(RowHeightProperty);
public static void SetRowHeight(AvaloniaObject element, string value) => element.SetValue(RowHeightProperty, value);
public static string GetColumnWidth(AvaloniaObject element) => element.GetValue(ColumnWidthProperty);
public static void SetColumnWidth(AvaloniaObject element, string value) => element.SetValue(ColumnWidthProperty, value);
static GridDefinitionsHelper()
{
// 监听Rows属性变化
RowsProperty.Changed.AddClassHandler<Grid>((grid, e) => UpdateGridDefinitions(grid));
// 监听Columns属性变化
ColumnsProperty.Changed.AddClassHandler<Grid>((grid, e) => UpdateGridDefinitions(grid));
// 监听RowHeight属性变化
RowHeightProperty.Changed.AddClassHandler<Grid>((grid, e) => UpdateGridDefinitions(grid));
// 监听ColumnWidth属性变化
ColumnWidthProperty.Changed.AddClassHandler<Grid>((grid, e) => UpdateGridDefinitions(grid));
}
private static void UpdateGridDefinitions(Grid grid)
{
int rows = GetRows(grid);
int columns = GetColumns(grid);
string rowHeight = GetRowHeight(grid);
string columnWidth = GetColumnWidth(grid);
// 更新行定义
grid.RowDefinitions.Clear();
var height = ParseDimension(rowHeight);
for (int i = 0; i < rows; i++)
{
grid.RowDefinitions.Add(new RowDefinition(height));
}
// 更新列定义
grid.ColumnDefinitions.Clear();
var width = ParseDimension(columnWidth);
for (int i = 0; i < columns; i++)
{
grid.ColumnDefinitions.Add(new ColumnDefinition(width));
}
}
private static GridLength ParseDimension(string value)
{
if (string.IsNullOrEmpty(value))
return GridLength.Auto;
value = value.Trim();
if (value.EndsWith("*"))
{
if (value.Length == 1)
return new GridLength(1, GridUnitType.Star);
if (double.TryParse(value.Substring(0, value.Length - 1), out double starValue))
return new GridLength(starValue, GridUnitType.Star);
}
else if (double.TryParse(value, out double pixelValue))
{
return new GridLength(pixelValue, GridUnitType.Pixel);
}
return GridLength.Auto;
}
}

View File

@@ -0,0 +1,82 @@
using Avalonia;
using Avalonia.Controls;
using System.Collections.ObjectModel;
namespace Cowain.Base.Helpers;
public class GridHelper
{
// 定义RowDefinitions附加属性
public static readonly AttachedProperty<ObservableCollection<RowDefinition>> RowDefinitionsProperty =
AvaloniaProperty.RegisterAttached<Grid, ObservableCollection<RowDefinition>>(
"RowDefinitions", typeof(GridHelper));
// 定义ColumnDefinitions附加属性
public static readonly AttachedProperty<ObservableCollection<ColumnDefinition>> ColumnDefinitionsProperty =
AvaloniaProperty.RegisterAttached<Grid, ObservableCollection<ColumnDefinition>>(
"ColumnDefinitions", typeof(GridHelper));
// RowDefinitions属性的Get方法
public static ObservableCollection<RowDefinition> GetRowDefinitions(AvaloniaObject element)
{
return element.GetValue(RowDefinitionsProperty);
}
// RowDefinitions属性的Set方法
public static void SetRowDefinitions(AvaloniaObject element, ObservableCollection<RowDefinition> value)
{
element.SetValue(RowDefinitionsProperty, value);
// 当属性值变化时更新Grid的RowDefinitions
if (element is Grid grid && value != null)
{
grid.RowDefinitions.Clear();
foreach (var definition in value)
{
grid.RowDefinitions.Add(definition);
}
// 监听集合变化动态更新Grid
value.CollectionChanged += (sender, e) =>
{
grid.RowDefinitions.Clear();
foreach (var definition in value)
{
grid.RowDefinitions.Add(definition);
}
};
}
}
// ColumnDefinitions属性的Get方法
public static ObservableCollection<ColumnDefinition> GetColumnDefinitions(AvaloniaObject element)
{
return element.GetValue(ColumnDefinitionsProperty);
}
// ColumnDefinitions属性的Set方法
public static void SetColumnDefinitions(AvaloniaObject element, ObservableCollection<ColumnDefinition> value)
{
element.SetValue(ColumnDefinitionsProperty, value);
// 当属性值变化时更新Grid的ColumnDefinitions
if (element is Grid grid && value != null)
{
grid.ColumnDefinitions.Clear();
foreach (var definition in value)
{
grid.ColumnDefinitions.Add(definition);
}
// 监听集合变化动态更新Grid
value.CollectionChanged += (sender, e) =>
{
grid.ColumnDefinitions.Clear();
foreach (var definition in value)
{
grid.ColumnDefinitions.Add(definition);
}
};
}
}
}

View File

@@ -0,0 +1,105 @@
using Avalonia.Animation;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Styling;
using Avalonia.Xaml.Interactivity;
using Avalonia.Threading;
namespace Cowain.Base.Helpers;
public class GridPositionAnimator : Behavior<Control>
{
public static readonly StyledProperty<int> RowProperty =
AvaloniaProperty.Register<GridPositionAnimator, int>("Row");
public static readonly StyledProperty<int> ColumnProperty =
AvaloniaProperty.Register<GridPositionAnimator, int>("Column");
public int Row
{
get => GetValue(RowProperty);
set => SetValue(RowProperty, value);
}
public int Column
{
get => GetValue(ColumnProperty);
set => SetValue(ColumnProperty, value);
}
protected override void OnAttached()
{
base.OnAttached();
if (AssociatedObject != null)
{
Dispatcher.UIThread.Post(() =>
{
// 初始设置位置
Grid.SetRow(AssociatedObject, Row);
Grid.SetColumn(AssociatedObject, Column);
// 新的事件订阅方式
RowProperty.Changed.AddClassHandler<GridPositionAnimator>((b, e) => b.OnRowChanged(e));
ColumnProperty.Changed.AddClassHandler<GridPositionAnimator>((b, e) => b.OnColumnChanged(e));
});
}
}
private void OnRowChanged(AvaloniaPropertyChangedEventArgs e)
{
if (AssociatedObject != null && e.NewValue is int newRow)
{
// 创建行变化动画
var animation = new Animation
{
Duration = TimeSpan.FromSeconds(1),
FillMode = FillMode.Both,
Children =
{
new KeyFrame
{
Cue = new Cue(0d),
Setters = { new Setter { Property = Grid.RowProperty, Value = e.OldValue } }
},
new KeyFrame
{
Cue = new Cue(1d),
Setters = { new Setter { Property = Grid.RowProperty, Value = newRow } }
}
}
};
animation.RunAsync(AssociatedObject);
}
}
private void OnColumnChanged(AvaloniaPropertyChangedEventArgs e)
{
if (AssociatedObject != null && e.NewValue is int newColumn)
{
// 创建列变化动画
var animation = new Animation
{
Duration = TimeSpan.FromSeconds(1),
FillMode = FillMode.Both,
Children =
{
new KeyFrame
{
Cue = new Cue(0d),
Setters = { new Setter { Property = Grid.ColumnProperty, Value = e.OldValue } }
},
new KeyFrame
{
Cue = new Cue(1d),
Setters = { new Setter { Property = Grid.ColumnProperty, Value = newColumn } }
}
}
};
animation.RunAsync(AssociatedObject);
}
}
}

View File

@@ -0,0 +1,14 @@
using MethodBoundaryAspect.Fody.Attributes;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)]
public sealed class LogAndSwallowAttribute : OnMethodBoundaryAspect
{
// 可全局统一处理,也能自定义
public static Action<Exception>? OnExceptionAction { get; set; }
public override void OnException(MethodExecutionArgs args)
{
OnExceptionAction?.Invoke(args.Exception);
args.FlowBehavior = FlowBehavior.Return; // 吃掉异常
}
}

View File

@@ -0,0 +1,32 @@
using System.Security.Cryptography;
using System.Text;
namespace Cowain.Base.Helpers;
public static class Md5Helper
{
/// <summary>
/// 32bit UTF8 MD5 Encrypt
/// </summary>
/// <param name="plaintext">明文</param>
/// <returns></returns>
public static string Md5Encrypt32(string plaintext)
{
string pwd = string.Empty;
if (!string.IsNullOrEmpty(plaintext) && !string.IsNullOrWhiteSpace(plaintext))
{
using (MD5 md5 = MD5.Create())
{
byte[] s = md5.ComputeHash(Encoding.UTF8.GetBytes(plaintext));
foreach (var item in s)
{
pwd = string.Concat(pwd, item.ToString("x2"));
}
}
}
return pwd;
}
}

View File

@@ -0,0 +1,190 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace Cowain.Base.Helpers;
public interface INodifyObservableCollection<T>
{
/// <summary>
/// Called when a new item is added
/// </summary>
/// <param name="added">The callback to execute when an item is added</param>
/// <returns>Returns self</returns>
INodifyObservableCollection<T> WhenAdded(Action<T> added);
/// <summary>
/// Called when an existing item is removed
/// Note: It is not called when items are cleared if <see cref="WhenCleared(Action{IList{T}})"/> is used
/// </summary>
/// <param name="added">The callback to execute when an item is removed</param>
/// <returns>Returns self</returns>
INodifyObservableCollection<T> WhenRemoved(Action<T> removed);
/// <summary>
/// Called when the collection is cleared
/// NOTE: It does not call <see cref="WhenRemoved(Action{T})"/> on each item
/// </summary>
/// <param name="added">The callback to execute when the collection is cleared</param>
/// <returns>Returns self</returns>
INodifyObservableCollection<T> WhenCleared(Action<IList<T>> cleared);
}
public class NodifyObservableCollection<T> : Collection<T>, INodifyObservableCollection<T>, INotifyPropertyChanged, INotifyCollectionChanged
{
protected static readonly PropertyChangedEventArgs IndexerPropertyChanged = new PropertyChangedEventArgs("Item[]");
protected static readonly PropertyChangedEventArgs CountPropertyChanged = new PropertyChangedEventArgs("Count");
protected static readonly NotifyCollectionChangedEventArgs ResetCollectionChanged = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
private readonly List<Action<T>> _added = new List<Action<T>>();
private readonly List<Action<T>> _removed = new List<Action<T>>();
private readonly List<Action<IList<T>>> _cleared = new List<Action<IList<T>>>();
public event NotifyCollectionChangedEventHandler? CollectionChanged;
public event PropertyChangedEventHandler? PropertyChanged;
public NodifyObservableCollection()
{
}
public NodifyObservableCollection(IEnumerable<T> collection)
: base(new List<T>(collection))
{
}
#region Collection Events
public INodifyObservableCollection<T> WhenAdded(Action<T> added)
{
if (added != null)
{
_added.Add(added);
}
return this;
}
public INodifyObservableCollection<T> WhenRemoved(Action<T> removed)
{
if (removed != null)
{
_removed.Add(removed);
}
return this;
}
public INodifyObservableCollection<T> WhenCleared(Action<IList<T>> cleared)
{
if (cleared != null)
{
_cleared.Add(cleared);
}
return this;
}
protected virtual void NotifyOnItemAdded(T item)
{
for (int i = 0; i < _added.Count; i++)
{
_added[i](item);
}
}
protected virtual void NotifyOnItemRemoved(T item)
{
for (int i = 0; i < _removed.Count; i++)
{
_removed[i](item);
}
}
protected virtual void NotifyOnItemsCleared(IList<T> items)
{
for (int i = 0; i < _cleared.Count; i++)
{
_cleared[i](items);
}
}
#endregion
#region Collection Handlers
protected override void ClearItems()
{
var items = _cleared.Count > 0 || _removed.Count > 0 ? new List<T>(this) : new List<T>();
base.ClearItems();
if (_cleared.Count > 0)
{
NotifyOnItemsCleared(items);
}
else if (_removed.Count > 0)
{
for (int i = 0; i < items.Count; i++)
{
NotifyOnItemRemoved(items[i]);
}
}
OnPropertyChanged(CountPropertyChanged);
OnPropertyChanged(IndexerPropertyChanged);
OnCollectionChanged(ResetCollectionChanged);
}
protected override void InsertItem(int index, T item)
{
base.InsertItem(index, item);
OnPropertyChanged(CountPropertyChanged);
OnPropertyChanged(IndexerPropertyChanged);
OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index);
NotifyOnItemAdded(item);
}
protected override void RemoveItem(int index)
{
var item = base[index];
base.RemoveItem(index);
OnPropertyChanged(CountPropertyChanged);
OnPropertyChanged(IndexerPropertyChanged);
OnCollectionChanged(NotifyCollectionChangedAction.Remove, item, index);
NotifyOnItemRemoved(item);
}
protected override void SetItem(int index, T item)
{
T prev = base[index];
base.SetItem(index, item);
OnPropertyChanged(IndexerPropertyChanged);
OnCollectionChanged(NotifyCollectionChangedAction.Replace, prev, item, index);
NotifyOnItemRemoved(prev);
NotifyOnItemAdded(item);
}
public void Move(int oldIndex, int newIndex)
{
T prev = base[oldIndex];
base.RemoveItem(oldIndex);
base.InsertItem(newIndex, prev);
OnPropertyChanged(IndexerPropertyChanged);
OnCollectionChanged(NotifyCollectionChangedAction.Move, prev, newIndex, oldIndex);
}
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
=> CollectionChanged?.Invoke(this, e);
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
=> PropertyChanged?.Invoke(this, args);
private void OnCollectionChanged(NotifyCollectionChangedAction action, object? item, int index)
=> OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index));
private void OnCollectionChanged(NotifyCollectionChangedAction action, object? item, int index, int oldIndex)
=> OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index, oldIndex));
private void OnCollectionChanged(NotifyCollectionChangedAction action, object? oldItem, object? newItem, int index)
=> OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
#endregion
}

View File

@@ -0,0 +1,30 @@
using Avalonia.Controls.Notifications;
using Notification = Ursa.Controls.Notification;
using WindowNotificationManager = Ursa.Controls.WindowNotificationManager;
namespace Cowain.Base.Helpers;
public static class NotificationHelper
{
public static WindowNotificationManager? NotificationManager { get; set; }
public static void ShowNormal(NotificationType notificationType, string message)
{
NotificationManager?.Show(
new Notification(notificationType.ToString(), message),
showIcon: true,
showClose: true,
type: notificationType);
}
public static void ShowLight(NotificationType notificationType, string message)
{
NotificationManager?.Show(
new Notification(notificationType.ToString(), message),
showIcon: true,
showClose: true,
type: notificationType,
classes: ["Light"]);
}
}

View File

@@ -0,0 +1,171 @@
using Avalonia.Media;
using Cowain.Base.Attributes;
using Cowain.Base.Models.Menu;
using Cowain.Base.ViewModels;
using Ke.Bee.Localization.Localizer.Abstractions;
using System;
using System.Collections.ObjectModel;
namespace Cowain.Base.Helpers;
[Singleton]
public class PageViewMenuHelper
{
/// <summary>
/// 菜单数据
/// </summary>
private readonly List<MenuItem> _menuItems;
private readonly ILocalizer _l;
public PageViewMenuHelper(MenuConfigurationContext menuContext, ILocalizer localizer)
{
_menuItems = menuContext.Menus;
_l = localizer;
}
/// <summary>
/// MenuItem 到 MenuItemViewModel 的转换器
/// </summary>
public Func<MenuItem, MenuItemViewModel> MenuItemToViewModel => x => new MenuItemViewModel(_l[x.LocaleKey])
{
Key = x.Key,
IsActive = x.IsActive == true,
Icon = string.IsNullOrWhiteSpace(x.Icon) ? null : StreamGeometry.Parse(x.Icon),
CommandParameter = x.CommandParameter,
PageActions = x.PageActions,
Items = x.Items.Select(MenuItemToViewModel).ToList(),
};
/// <summary>
/// 返回组菜单列表
/// </summary>
public IEnumerable<MenuItemViewModel> GetGroupMenus(string group)
{
return _menuItems.Where(x => x.Group == group).Select(MenuItemToViewModel);
}
/// <summary>
/// 清除所有菜单的Active状态
/// </summary>
public void ClearMenuActives(IEnumerable<MenuItemViewModel>? menus)
{
if (menus is null)
{
return;
}
foreach (var menu in menus)
{
menu.IsActive = false;
if (menu.Items != null && menu.Items.Any())
{
ClearMenuActives(menu.Items);
}
}
}
/// <summary>
/// 根据菜单keys列表设置Active状态
/// </summary>
public void SetMenuActiveByKeys(IEnumerable<MenuItemViewModel>? menus, List<string>? menuKeys)
{
if (menuKeys is null)
{
return;
}
if (menus is null)
{
return;
}
foreach (var menu in menus)
{
if (menuKeys.Contains(menu.Key))
{
menu.IsActive = true;
}
if (menu.Items != null && menu.Items.Any())
{
SetMenuActiveByKeys(menu.Items, menuKeys);
}
}
}
/// <summary>
/// 返回所有激活的菜单
/// </summary>
public IEnumerable<UserRoleMenuViewModel>? GetActiveMenus(IEnumerable<MenuItemViewModel>? menus)
{
if (menus is null)
{
return null;
}
//递归返回所有选中的菜单
return menus.Where(menu => menu.IsActive)
.Select(menu => new UserRoleMenuViewModel
{
Id = 0, // 这里可以根据需要设置实际的 Id
RoleId = 0, // 这里可以根据需要设置实际的 RoleId
MenuKey = menu.Key,
MenuName = menu.Text,
MenuActions = new ObservableCollection<string>(menu.PageActions ?? Array.Empty<string>())
})
.Concat(menus.SelectMany(menu => GetActiveMenus(menu.Items) ?? Enumerable.Empty<UserRoleMenuViewModel>()));
}
/// <summary>
/// 根据菜单的key获取菜单页面权限
/// </summary>
/// <returns></returns>
public List<string>? GetMenuActionsByKey(string? key)
{
if (string.IsNullOrEmpty(key))
{
return null;
}
var menuItem = FindMenuItem(_menuItems, key);
return menuItem?.PageActions?.ToList();
}
public string? GetMenuName(string? menuKey)
{
if (string.IsNullOrWhiteSpace(menuKey))
{
return null;
}
var menuItem = FindMenuItem(_menuItems, menuKey);
return menuItem != null ? _l[menuItem.LocaleKey] : null;
}
/// <summary>
/// 在菜单集合中查找指定Key的菜单项
/// </summary>
/// <param name="menus">菜单集合</param>
/// <param name="key">菜单项的Key</param>
/// <returns>找到的菜单项</returns>
public MenuItem? FindMenuItem(IEnumerable<MenuItem>? menus, string? key)
{
if (menus == null || key == null)
{
return null;
}
foreach (var menu in menus)
{
if (menu.Key == key)
{
return menu;
}
var found = FindMenuItem(menu.Items, key);
if (found != null)
{
return found;
}
}
return null;
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.Helpers;
public class PathHelper
{
private static string _localFolder = string.Empty;
private static string LocalFolder
{
get
{
if (!string.IsNullOrEmpty(_localFolder))
{
return _localFolder;
}
_localFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
AppDomain.CurrentDomain.FriendlyName
);
if (!Directory.Exists(_localFolder))
{
Directory.CreateDirectory(_localFolder);
}
return _localFolder;
}
}
public static string GetLocalFilePath(string flieName)
{
return Path.Combine(LocalFolder, flieName);
}
}

View File

@@ -0,0 +1,39 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.Helpers;
/// <summary>
/// 服务定位器
/// </summary>
public class ServiceLocator
{
private static IServiceProvider? _current;
public static IServiceProvider Current
{
get => _current!;
set
{
if (_current != null)
{
throw new InvalidOperationException("The service locator has already been set.");
}
_current = value;
}
}
public static T GetRequiredService<T>() where T : class
{
return (T)Current.GetRequiredService(typeof(T));
}
public static object GetRequiredService(Type T)
{
return Current.GetRequiredService(T);
}
}

View File

@@ -0,0 +1,267 @@
namespace Cowain.Base.Helpers;
/// <summary>
/// convert utility
/// </summary>
public static class UtilConvert
{
#region object convert function
/// <summary>
/// object convert to int
/// </summary>
/// <param name="thisValue">value</param>
/// <returns></returns>
public static int ObjToInt(this object thisValue)
{
int reval = 0;
if (thisValue == null)
{
return 0;
}
if (string.IsNullOrEmpty(thisValue.ToString()))
{
return 0;
}
if (thisValue != DBNull.Value && int.TryParse(thisValue.ToString(), out reval))
{
return reval;
}
return reval;
}
/// <summary>
/// object convert to int
/// </summary>
/// <param name="thisValue">value</param>
/// <param name="errorValue">value when error occured</param>
/// <returns></returns>
public static int ObjToInt(this object thisValue, int errorValue)
{
if (thisValue == null)
{
return errorValue;
}
if (string.IsNullOrEmpty(thisValue.ToString()))
{
return 0;
}
if (thisValue != DBNull.Value && int.TryParse(thisValue.ToString(), out int reval))
{
return reval;
}
return errorValue;
}
/// <summary>
/// object convert to double
/// </summary>
/// <param name="thisValue">value</param>
/// <returns></returns>
public static double ObjToDouble(this object thisValue)
{
if (string.IsNullOrEmpty(thisValue.ToString()))
{
return 0;
}
if (thisValue != DBNull.Value && double.TryParse(thisValue.ToString(), out double reval))
{
return reval;
}
return 0;
}
/// <summary>
/// object convert to double
/// </summary>
/// <param name="thisValue">value</param>
/// <param name="errorValue">value when error occured</param>
/// <returns></returns>
public static double ObjToDouble(this object thisValue, double errorValue)
{
if (string.IsNullOrEmpty(thisValue.ToString()))
{
return 0;
}
if (thisValue != DBNull.Value && double.TryParse(thisValue.ToString(), out double reval))
{
return reval;
}
return errorValue;
}
/// <summary>
/// object convert to string
/// </summary>
/// <param name="thisValue">value</param>
/// <returns></returns>
public static string? ObjToString(this object thisValue)
{
return thisValue?.ToString()?.Trim();
}
/// <summary>
/// object convert to decimal
/// </summary>
/// <param name="thisValue">value</param>
/// <returns></returns>
public static decimal ObjToDecimal(this object thisValue)
{
if (string.IsNullOrEmpty(thisValue.ToString()))
{
return 0;
}
if (thisValue != DBNull.Value && decimal.TryParse(thisValue.ToString(), out decimal reval))
{
return reval;
}
return 0;
}
/// <summary>
/// object convert to decimal
/// </summary>
/// <param name="thisValue">value</param>
/// <param name="errorValue">value when error occured</param>
/// <returns></returns>
public static decimal ObjToDecimal(this object thisValue, decimal errorValue)
{
if (string.IsNullOrEmpty(thisValue.ToString()))
{
return 0;
}
if (thisValue != DBNull.Value && decimal.TryParse(thisValue.ToString(), out decimal reval))
{
return reval;
}
return errorValue;
}
/// <summary>
/// object convert to date
/// </summary>
/// <param name="thisValue">value</param>
/// <returns></returns>
public static DateTime ObjToDate(this object thisValue)
{
DateTime reval = Convert.ToDateTime("1900-01-01");
if (thisValue != null && thisValue != DBNull.Value && DateTime.TryParse(thisValue.ToString(), out reval))
{
reval = Convert.ToDateTime(thisValue);
}
return reval;
}
/// <summary>
/// object convert to date
/// </summary>
/// <param name="thisValue">value</param>
/// <param name="errorValue">value when error occured</param>
/// <returns></returns>
public static DateTime ObjToDate(this object thisValue, DateTime errorValue)
{
DateTime reval;
if (thisValue != null && thisValue != DBNull.Value && DateTime.TryParse(thisValue.ToString(), out reval))
{
return reval;
}
return errorValue;
}
/// <summary>
/// object convert to bool
/// </summary>
/// <param name="thisValue">value</param>
/// <returns></returns>
public static bool ObjToBool(this object thisValue)
{
bool reval = false;
if (thisValue != null && thisValue != DBNull.Value && bool.TryParse(thisValue.ToString(), out reval))
{
return reval;
}
return reval;
}
#endregion
#region compare function
/// <summary>
/// less than
/// </summary>
/// <param name="thisValue">thisValue</param>
/// <param name="compareValue">compareValue</param>
/// <returns></returns>
public static bool IsLessThan(this object thisValue, double compareValue)
{
return Convert.ToDouble(thisValue) < compareValue;
}
/// <summary>
/// less than or equal
/// </summary>
/// <param name="thisValue">thisValue</param>
/// <param name="compareValue">compareValue</param>
/// <returns></returns>
public static bool IsLessThanOrEqual(this object thisValue, double compareValue)
{
return Convert.ToDouble(thisValue) <= compareValue;
}
/// <summary>
/// greater than
/// </summary>
/// <param name="thisValue">当前值</param>
/// <param name="compareValue">比较值</param>
/// <returns></returns>
public static bool IsGreaterThan(this object thisValue, double compareValue)
{
return Convert.ToDouble(thisValue) > compareValue;
}
/// <summary>
/// greater than or equal
/// </summary>
/// <param name="thisValue">thisValue</param>
/// <param name="compareValue">compareValue</param>
/// <returns></returns>
public static bool IsGreaterThanOrEqual(this object thisValue, double compareValue)
{
return Convert.ToDouble(thisValue) >= compareValue;
}
/// <summary>
/// less then
/// </summary>
/// <param name="thisValue">thisValue</param>
/// <param name="compareValue">compareValue</param>
/// <returns></returns>
public static bool IsLessThan(this object thisValue, DateTime compareValue)
{
return Convert.ToDateTime(thisValue) < compareValue;
}
/// <summary>
/// less than or equal
/// </summary>
/// <param name="thisValue">thisValue</param>
/// <param name="compareValue">compareValue</param>
/// <returns></returns>
public static bool IsLessThanOrEqual(this object thisValue, DateTime compareValue)
{
return Convert.ToDateTime(thisValue) <= compareValue;
}
/// <summary>
/// less then
/// </summary>
/// <param name="thisValue">thisValue</param>
/// <param name="compareValue">compareValue</param>
/// <returns></returns>
public static bool IsGreaterThan(this object thisValue, DateTime compareValue)
{
return Convert.ToDateTime(thisValue) > compareValue;
}
/// <summary>
/// ess than or equal
/// </summary>
/// <param name="thisValue">thisValue</param>
/// <param name="compareValue">compareValue</param>
/// <returns></returns>
public static bool IsGreaterThanOrEqual(this object thisValue, DateTime compareValue)
{
return Convert.ToDateTime(thisValue) >= compareValue;
}
#endregion
/// <summary>
/// default min date
/// </summary>
public static DateTime MinDate => Convert.ToDateTime("1900-01-01");
}

View File

@@ -0,0 +1,27 @@
using Cowain.Base.Models;
using Cowain.Base.ViewModels;
namespace Cowain.Base.IServices;
public interface IAccountService : IBaseService
{
/// <summary>
/// 获取所有用户
/// </summary>
Task<List<UserViewModel>> GetAllAsync();
Task<(List<UserViewModel>, int totals)> GetAllAsync(int pageIndex, int pageSize);
/// <summary>
/// 登录
/// </summary>
/// <param name="userName">用户名</param>
/// <param name="password">密码</param>
/// <returns></returns>
Task<ResultModel<LoginUserViewModel>> LoginAsync(string userName, string password);
Task<ResultModel> CheckHealthAsync(CancellationToken cancellationToken = default);
Task<ResultModel> AddUserAsync(UserViewModel? user);
Task<ResultModel> EditUserAsync(UserViewModel? user);
Task<ResultModel> DelUserAsync(UserViewModel? user);
}

View File

@@ -0,0 +1,41 @@
using MySqlConnector;
using System.Data;
using System.Linq.Expressions;
namespace Cowain.Base.IServices
{
public interface IBaseService
{
int Delete<T>(int Id) where T : class;
int Delete<T>(T t) where T : class;
Task<int> DeleteAsync<T>(int Id, CancellationToken cancellationToken = default) where T : class;
Task<int> DeleteAsync<T>(T t, CancellationToken cancellationToken = default) where T : class;
int ExecuteByDapper(string sql, object? param = null);
Task<int> ExecuteByDapperAsync(string sql, object? param = null, CancellationToken cancellationToken = default);
List<T> Find<T>() where T : class;
T? Find<T>(int id) where T : class;
Task<List<T>> FindAsync<T>(CancellationToken cancellationToken = default) where T : class;
Task<T?> FindAsync<T>(int id, CancellationToken cancellationToken = default) where T : class;
Task<T?> FirstOrDefaultAsync<T>(Expression<Func<T, bool>> funcWhere, CancellationToken cancellationToken = default) where T : class;
DataTable GetDataTable(string sql, IEnumerable<MySqlParameter>? parameters = null);
Task<DataTable> GetDataTableAsync(string sql, IEnumerable<MySqlParameter>? parameters = null, CancellationToken cancellationToken = default);
int Insert<T>(IEnumerable<T> tList) where T : class;
int Insert<T>(T t) where T : class;
Task<int> InsertAsync<T>(IEnumerable<T> tList, CancellationToken cancellationToken = default) where T : class;
Task<int> InsertAsync<T>(T t, CancellationToken cancellationToken = default) where T : class;
List<T> Query<T>(Expression<Func<T, bool>>? funcWhere = null) where T : class;
Task<(List<TViewModel> Data, int TotalCount)> QueryAsync<T, TKey, TViewModel>(Expression<Func<T, TViewModel>> selectExpression, Expression<Func<T, bool>>? funcWhere = null, Expression<Func<T, TKey>>? orderBy = null, bool isAscending = true, int pageIndex = 1, int pageSize = 20, CancellationToken cancellationToken = default) where T : class;
Task<(List<T> Data, int TotalCount)> QueryAsync<T, TKey>(Expression<Func<T, bool>>? funcWhere = null, Expression<Func<T, TKey>>? orderBy = null, bool isAscending = true, int pageIndex = 1, int pageSize = 20, CancellationToken cancellationToken = default) where T : class;
Task<List<T>> QueryAsync<T>(Expression<Func<T, bool>>? funcWhere = null, CancellationToken cancellationToken = default) where T : class;
IEnumerable<T> QueryByDapper<T>(string sql, object? param = null);
Task<IEnumerable<T>> QueryByDapperAsync<T>(string sql, object? param = null, CancellationToken cancellationToken = default);
T? QueryFirstOrDefaultByDapper<T>(string sql, object? param = null);
Task<T?> QueryFirstOrDefaultByDapperAsync<T>(string sql, object? param = null, CancellationToken cancellationToken = default);
DataTable QueryToDataTableByDapper(string sql, object? param = null);
Task<DataTable> QueryToDataTableByDapperAsync(string sql, object? param = null, CancellationToken cancellationToken = default);
int Update<T>(IEnumerable<T> tList) where T : class;
int Update<T>(T t) where T : class;
Task<int> UpdateAsync<T>(IEnumerable<T> tList, CancellationToken cancellationToken = default) where T : class;
Task<int> UpdateAsync<T>(T t, CancellationToken cancellationToken = default) where T : class;
}
}

View File

@@ -0,0 +1,18 @@
using Cowain.Base.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.IServices;
public interface ILogService : IBaseService
{
/// <summary>
/// 获取所有用户
/// </summary>
Task<List<SerilogViewModel>> GetAllAsync();
Task<(List<SerilogViewModel>, int totals)> GetAllAsync(int pageIndex, int pageSize);
}

View File

@@ -0,0 +1,24 @@
using Cowain.Base.Models;
using Cowain.Base.ViewModels;
namespace Cowain.Base.IServices;
public interface IUserRoleMenuService : IBaseService
{
/// <summary>
/// 获取所有菜单
/// </summary>
/// <param name="currentUser"></param>
/// <returns></returns>
Task<List<UserRoleViewModel>> GetAllAsync();
/// <summary>
/// 修改角色菜单
/// </summary>
/// <returns></returns>
Task<ResultModel> EditUserRoleMenuAsync(UserRoleViewModel? userRole);
Task<ResultModel> EditMenuActionsAsync(UserRoleMenuViewModel? userRoleMenu);
Task<ResultModel> DeleteMenuActionsAsync(UserRoleMenuViewModel? userRoleMenu);
}

View File

@@ -0,0 +1,39 @@
using Cowain.Base.Models;
using Cowain.Base.ViewModels;
namespace Cowain.Base.IServices;
public interface IUserRoleService : IBaseService
{
/// <summary>
/// 获取所有用户角色
/// </summary>
/// <param name="currentUser"></param>
/// <returns></returns>
Task<List<UserRoleViewModel>> GetAllAsync();
/// <summary>
/// 获取所有用户角色,分页查询
/// </summary>
/// <param name="pageIndex"></param>
/// <param name="pageSize"></param>
/// <returns></returns>
Task<(List<UserRoleViewModel>, int totals)> GetAllAsync(int pageIndex, int pageSize);
/// <summary>
/// 增加角色
/// </summary>
/// <returns></returns>
Task<ResultModel> AddUserRoleAsync(UserRoleViewModel? userRole);
/// <summary>
/// 删除角色
/// </summary>
/// <returns></returns>
Task<ResultModel> DelUserRoleAsync(UserRoleViewModel? userRole);
/// <summary>
/// 修改角色
/// </summary>
/// <returns></returns>
Task<ResultModel> EditUserRoleAsync(UserRoleViewModel? userRole);
}

View File

@@ -0,0 +1,77 @@
using Cowain.Base.DBContext;
using Cowain.Base.Helpers;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Cowain.Base.Models.Admins;
[Table("user")]
public class UserDto : BaseModel
{
[Key]
public int Id { get; set; }
/// <summary>
/// 姓名
/// </summary>
[Required]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 员工编号
/// </summary>
[Required]
public string UserNumber { get; set; } = string.Empty;
/// <summary>
/// 电话
/// </summary>
public string Phone { get; set; } = string.Empty;
/// <summary>
/// 角色
/// </summary>
public int RoleId { get; set; }
/// <summary>
/// 性别
/// </summary>
public string Sex { get; set; } = string.Empty;
/// <summary>
/// 密码
/// </summary>
public string Password { get; set; } = string.Empty;
public bool IsValid { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; } = DateTime.Now;
/// <summary>
/// 最后一次更新时间
/// </summary>
public DateTime UpdateTime { get; set; } = DateTime.MinValue;
}
public class UserSeed : IDataSeeding
{
public void DataSeeding(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserDto>().HasData(
new UserDto
{
Id = 1,
Name = "admin",
Sex = SexMode.Male.ToString(),
RoleId = 1,
UserNumber = "CWA4483",
Phone = "17625760609",
IsValid = true,
Password = DESHelper.Encrypt("12345", "ZSL12345")
}
);
}
}

View File

@@ -0,0 +1,48 @@
using Cowain.Base.DBContext;
using Cowain.Base.Helpers;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Cowain.Base.Models.Admins;
[Table("user_role")]
public class UserRoleDto : BaseModel
{
[Key]
public int Id { get; set; }
/// <summary>
/// 角色名称
/// </summary>
[Required]
public string RoleName { get; set; } = string.Empty;
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; } = DateTime.Now;
/// <summary>
/// 最后一次更新时间
/// </summary>
public DateTime UpdateTime { get; set; } = DateTime.MinValue;
public bool IsValid { get; set; }
}
public class UserRoleSeed : IDataSeeding
{
public void DataSeeding(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserRoleDto>().HasData(
new UserRoleDto
{
Id = 1,
RoleName = "管理员",
IsValid = true
}
);
}
}

View File

@@ -0,0 +1,58 @@
using Cowain.Base.DBContext;
using Cowain.Base.Helpers;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Cowain.Base.Models.Admins;
[Table("user_role_menu")]
public class UserRoleMenuDto : BaseModel
{
[Key]
public int Id { get; set; }
public int RoleId { get; set; }
public string? MenuKey { get; set; }
public string? MenuActions { get; set; }
}
public class UserRoleMenuSeed : IDataSeeding
{
public void DataSeeding(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserRoleMenuDto>().HasData(
new UserRoleMenuDto
{
Id = 1,
RoleId = 1,
MenuKey = "Home",
MenuActions = "[]"
},
new UserRoleMenuDto
{
Id = 2,
RoleId = 1,
MenuKey = "UserRoleSetting",
MenuActions = "[ \"add\", \"edit\", \"delete\"]"
},
new UserRoleMenuDto
{
Id = 3,
RoleId = 1,
MenuKey = "RoleMenuSetting",
MenuActions = "[ \"edit\", \"delete\"]"
},
new UserRoleMenuDto
{
Id = 4,
RoleId = 1,
MenuKey = "UserManagement",
MenuActions = "[ \"add\", \"edit\", \"delete\"]"
}
);
}
}

View File

@@ -0,0 +1,14 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using Cowain.Base.ViewModels;
namespace Plugin.Cowain.Base.Models;
public class AlarmChangedMessage : ValueChangedMessage<List<AlarmViewModel>>
{
// 构造函数:传递变更类型+告警实例
public AlarmChangedMessage(List<AlarmViewModel> alarm)
: base(alarm)
{
}
}

View File

@@ -0,0 +1,15 @@
namespace Cowain.Base.Models;
public class AppSettings
{
/// <summary>
/// 全局输出目录
/// </summary>
public string OutputPath { get; set; } = string.Empty;
/// <summary>
/// 全局插件目录
/// </summary>
public string PluginPath { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,17 @@
namespace Cowain.Base.Models;
/// <summary>
/// 应用异常基类
/// </summary>
public class ApplicationException : Exception
{
public ApplicationException() { }
public ApplicationException(string message)
: base(message) { }
public ApplicationException(string message, Exception innerException)
: base(message, innerException) { }
}

View File

@@ -0,0 +1,18 @@
namespace Cowain.Base.Models;
public abstract class BaseModel
{
}
/// <summary>
/// 菜单按钮点击命令类型
/// </summary>
public enum DataBaseType
{
MYSQL,
SQLITE,
SQLSERVER,
POSTGRES
}

View File

@@ -0,0 +1,47 @@
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Cowain.Base.Models.Controls;
/// <summary>
/// 消息提示项
/// </summary>
public partial class ToastrItem : ObservableObject, IDisposable
{
[ObservableProperty]
private bool _isVisible = true;
/// <summary>
/// 消息内容
/// </summary>
public string Message { get; }
/// <summary>
/// 消息类型
/// </summary>
public ToastrType ToastrType { get; }
private DispatcherTimer? _timer;
public event EventHandler? RequestRemove;
public ToastrItem(string message, ToastrType toastrType = ToastrType.Danger, int duration = 3000)
{
IsVisible = true;
Message = message;
ToastrType = toastrType;
// 使用 DispatcherTimer 确保回调在主线程上执行
_timer = new DispatcherTimer(TimeSpan.FromMilliseconds(duration), DispatcherPriority.Normal, (s, e) =>
{
IsVisible = false;
RequestRemove?.Invoke(this, EventArgs.Empty);
Dispose(); // 自动调用 Dispose 释放资源
});
_timer.Start();
}
public void Dispose()
{
_timer?.Stop();
_timer = null;
}
}

View File

@@ -0,0 +1,9 @@
namespace Cowain.Base.Models.Controls;
public enum ToastrType
{
None = 0,
Success,
Danger
}

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.Models
{
public class CurrentUser
{
public int UserId { get; set; }
public string? UserName { get; set; }
public string? UserRole { get; set; }
public int UserRoleId { get; set; }
}
}

View File

@@ -0,0 +1,33 @@
namespace Cowain.Base.Models.Menu;
/// <summary>
/// 菜单按钮点击命令类型
/// </summary>
public enum MenuClickCommandType
{
/// <summary>
/// 无效
/// </summary>
None = 0,
/// <summary>
/// 激活菜单命令
/// </summary>
Active,
/// <summary>
/// 切换主题命令
/// </summary>
SwitchTheme,
/// <summary>
/// 超链接
/// </summary>
Link,
/// <summary>
/// 导航命令
/// </summary>
Navigate,
/// <summary>
/// 切换语言命令
/// </summary>
SwitchLanguage
}

View File

@@ -0,0 +1,15 @@
namespace Cowain.Base.Models.Menu;
/// <summary>
/// 菜单配置上下文对象
/// </summary>
public class MenuConfigurationContext
{
public List<MenuItem> Menus { get; set; }
public MenuConfigurationContext(List<MenuItem>? menuItems = null)
{
Menus = menuItems ?? [];
}
}

View File

@@ -0,0 +1,45 @@
namespace Cowain.Base.Models.Menu;
/// <summary>
/// 菜单项实体
/// </summary>
public class MenuItem
{
/// <summary>
/// 唯一标识
/// </summary>
public string Key { get; set; } = string.Empty;
/// <summary>
/// 图标 Path 值
/// </summary>
public string Icon { get; set; } = string.Empty;
/// <summary>
/// 菜单是否激活状态
/// </summary>
public bool? IsActive { get; set; }
/// <summary>
/// 本地化键
/// </summary>
public string LocaleKey { get; set; } = string.Empty;
/// <summary>
/// 子菜单
/// </summary>
public ICollection<MenuItem> Items { get; set; } = [];
/// <summary>
/// 菜单分组
/// </summary>
public string? Group { get; set; }
/// <summary>
/// 命令类型
/// </summary>
public string? CommandType { get; set; }
/// <summary>
/// 命令参数
/// </summary>
public string? CommandParameter { get; set; }
/// <summary>
/// 页面命令
/// </summary>
public string[]? PageActions { get; set; }
}

View File

@@ -0,0 +1,21 @@
using Cowain.Base.Abstractions.Navigation;
namespace Cowain.Base.Models.Navigation;
/// <summary>
/// 视图导航命令上下文对象
/// </summary>
public class NavigationCommandContext
{
/// <summary>
/// 视图导航器对象
/// </summary>
public IViewNavigator? Navigator { get; set; }
public NavigationCommandContext(IViewNavigator? navigator)
{
Navigator = navigator;
}
}

View File

@@ -0,0 +1,17 @@
namespace Cowain.Base.Models.Navigation;
/// <summary>
/// 导航命令不存在异常
/// </summary>
public class NavigationCommandNotFoundException : ApplicationException
{
public NavigationCommandNotFoundException() : base("Navigation command not found")
{
}
public NavigationCommandNotFoundException(string message)
: base(message) { }
public NavigationCommandNotFoundException(string message, Exception innerException)
: base(message, innerException) { }
}

View File

@@ -0,0 +1,11 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.Models;
public class NotificationMessage(NotificationModel model) : ValueChangedMessage<NotificationModel>(model);

View File

@@ -0,0 +1,33 @@
using Avalonia.Controls.Notifications;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.Models;
public class NotificationModel
{
public NotificationModel(string message)
{
Message = message;
}
public NotificationModel(NotificationType notificationType, string message)
{
NotificationType = notificationType;
Message = message;
}
public NotificationModel(NotificationType notificationType, string message, string classes)
{
NotificationType = notificationType;
Message = message;
Classes = classes;
}
public NotificationType NotificationType { get; set; } = NotificationType.Information;
public string Message { get; set; } = "This is message";
public string Classes { get; set; } = "Light";
}

View File

@@ -0,0 +1,18 @@
namespace Cowain.Base.Models.Plugin;
/// <summary>
/// 插件方法未找到异常
/// </summary>
public class PluginMethodNotFoundException : ApplicationException
{
public PluginMethodNotFoundException() : base("插件方法不存在")
{
}
public PluginMethodNotFoundException(string message)
: base(message) { }
public PluginMethodNotFoundException(string message, Exception innerException)
: base(message, innerException) { }
}

View File

@@ -0,0 +1,16 @@
namespace Cowain.Base.Models.Plugin;
/// <summary>
/// 插件不存在异常
/// </summary>
public class PluginNotFoundException : ApplicationException
{
public PluginNotFoundException() : base("插件不存在")
{
}
public PluginNotFoundException(string message)
: base(message) { }
public PluginNotFoundException(string message, Exception innerException)
: base(message, innerException) { }
}

View File

@@ -0,0 +1,73 @@
namespace Cowain.Base.Models;
public class ResultModel<T> where T : notnull
{
private ResultModel(T data, string errorMessage, bool isSuccess, int code)
{
Data = data;
ErrorMessage = errorMessage;
IsSuccess = isSuccess;
Code = code;
}
/// <summary>
/// is request success
/// </summary>
public bool IsSuccess { get; }
/// <summary>
/// status code
/// </summary>
public int Code { get; }
/// <summary>
/// error message
/// </summary>
public string ErrorMessage { get; } = "";
/// <summary>
/// data
/// </summary>
public T Data { get; }
/// <summary>
/// success
/// </summary>
public static ResultModel<T> Success(T data, string errMsg = "")
{
if (data is null)
throw new ArgumentNullException(nameof(data), "Success时data不能为null");
return new ResultModel<T>(data, errMsg, true, 200);
}
/// <summary>
/// faild
/// </summary>
public static ResultModel<T> Error(string str, int code = 400)
{
// 错误时Data用默认值
return new ResultModel<T>(default!, str, false, code);
}
}
public class ResultModel
{
/// <summary>
/// is request success
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// status code
/// </summary>
public int Code { get; set; }
/// <summary>
/// error message
/// </summary>
public string ErrorMessage { get; set; } = "";
public static ResultModel Success(string errMsg = "")
{
return new ResultModel() { ErrorMessage = errMsg, IsSuccess = true, Code = 200 };
}
public static ResultModel Error(string str, int code = 400)
{
var ret = new ResultModel { ErrorMessage = str, IsSuccess = false, Code = code };
return ret;
}
}

View File

@@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Cowain.Base.Models;
[Table("logs")]
public class SerilogDto : BaseModel
{
[Key]
public int id { get; set; }
[Required]
public string Timestamp { get; set; } = string.Empty;
[Required]
public DateTime _ts { get; set; }
[Required]
public string Level { get; set; } = string.Empty;
public string? Template { get; set; } = string.Empty;
public string? Message { get; set; }
public string? Exception { get; set; }
public string? Properties { get; set; }
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.Models;
public enum SexMode
{
Other,
Male,
Female
}

View File

@@ -0,0 +1,17 @@
namespace Cowain.Base.Models.Task;
/// <summary>
/// 错误的任务状态
/// </summary>
public class InvalidTaskStatusException : ApplicationException
{
public InvalidTaskStatusException() : base("错误的任务状态")
{
}
public InvalidTaskStatusException(string message)
: base(message) { }
public InvalidTaskStatusException(string message, Exception innerException)
: base(message, innerException) { }
}

View File

@@ -0,0 +1,22 @@
namespace Cowain.Base.Models.Task;
/// <summary>
/// 任务状态枚举对象
/// </summary>
public enum TaskStatusEnum
{
None = 0,
/// <summary>
/// 准备
/// </summary>
Pending,
/// <summary>
/// 执行中
/// </summary>
Running,
/// <summary>
/// 完成
/// </summary>
Completed,
}

View File

@@ -0,0 +1,235 @@
using Cowain.Base.DBContext;
using Cowain.Base.Helpers;
using Cowain.Base.IServices;
using Cowain.Base.Models;
using Cowain.Base.Models.Admins;
using Cowain.Base.ViewModels;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.ObjectModel;
using System.Text.Json;
namespace Cowain.Base.Services;
public class AccountService : BaseService, IAccountService
{
private readonly PageViewMenuHelper _pageMenuHelper;
public AccountService(IDbContextFactory<SqlDbContext> dbContextFactory, PageViewMenuHelper pageMenuHelper) : base(dbContextFactory)
{
_pageMenuHelper = pageMenuHelper;
}
public async Task<ResultModel<LoginUserViewModel>> LoginAsync(string userName, string password)
{
using var dbContext = _dbContextFactory.CreateDbContext();
var userDbSet = dbContext.GetDbSet<UserDto>();
var userRoleDbSet = dbContext.GetDbSet<UserRoleDto>();
var data = await (from us in userDbSet
join ur in userRoleDbSet on us.RoleId equals ur.Id
where us.Name == userName && us.Password == DESHelper.Encrypt(password, "ZSL12345")
select new UserViewModel
{
Id = us.Id,
RoleId = us.RoleId,
IsValid = us.IsValid,
Name = us.Name,
Phone = us.Phone,
Sex = us.Sex,
UserNumber = us.UserNumber,
RoleName = ur.RoleName
}).FirstOrDefaultAsync();
//登录成功,需要获取用户菜单
if (data != null)
{
var jsonSerializerOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
LoginUserViewModel loginUser = new LoginUserViewModel();
loginUser.User = data;
var userRoleMenus = dbContext.GetDbSet<UserRoleMenuDto>();
var menus = userRoleMenus.Where(x => x.RoleId == data.RoleId);
loginUser.Menus = new ObservableCollection<UserRoleMenuViewModel>(menus.Select(x => new UserRoleMenuViewModel
{
Id = x.Id,
RoleId = x.RoleId,
MenuKey = x.MenuKey,
MenuName = _pageMenuHelper.GetMenuName(x.MenuKey),
MenuActions = new ObservableCollection<string>(JsonSerializer.Deserialize<List<string>>(x.MenuActions ?? "[]", jsonSerializerOptions) ?? new List<string>())
}));
return ResultModel<LoginUserViewModel>.Success(loginUser);
}
return ResultModel<LoginUserViewModel>.Error("login err");
}
public async Task<ResultModel> CheckHealthAsync(CancellationToken cancellationToken = default)
{
using var dbContext = _dbContextFactory.CreateDbContext();
try
{
await dbContext.EnsureCreatedAsync();
var DbSet = dbContext.GetDbSet<UserDto>();
var isHealthy = await DbSet.ToListAsync(cancellationToken);
return ResultModel.Success("An healthy result.");
}
catch
{
return ResultModel.Error("An unhealthy result.");
}
}
public async Task<List<UserViewModel>> GetAllAsync()
{
using var dbContext = _dbContextFactory.CreateDbContext();
var userDbSet = dbContext.GetDbSet<UserDto>();
var userRoleDbSet = dbContext.GetDbSet<UserRoleDto>();
var data = await (from us in userDbSet
join ur in userRoleDbSet on us.RoleId equals ur.Id
select new UserViewModel
{
Id = us.Id,
RoleId = us.RoleId,
IsValid = us.IsValid,
Name = us.Name,
Password = DESHelper.Decrypt(us.Password, "ZSL12345"),
Phone = us.Phone,
Sex = us.Sex,
UserNumber = us.UserNumber,
RoleName = ur.RoleName
}).ToListAsync();
return data;
}
public async Task<(List<UserViewModel>, int totals)> GetAllAsync(int pageIndex, int pageSize)
{
using var dbContext = _dbContextFactory.CreateDbContext();
var userDbSet = dbContext.GetDbSet<UserDto>();
var userRoleDbSet = dbContext.GetDbSet<UserRoleDto>();
var data = (from us in userDbSet
join ur in userRoleDbSet on us.RoleId equals ur.Id
select new UserViewModel
{
Id = us.Id,
RoleId = us.RoleId,
IsValid = us.IsValid,
Name = us.Name,
Password = DESHelper.Decrypt(us.Password, "ZSL12345"),
Phone = us.Phone,
Sex = us.Sex,
UserNumber = us.UserNumber,
RoleName = ur.RoleName
});
var list = await data.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
return (list, data.Count());
}
public async Task<ResultModel> AddUserAsync(UserViewModel? user)
{
if (user == null)
{
return ResultModel.Error("User cannot be null");
}
if (string.IsNullOrEmpty(user.Name))
{
return ResultModel.Error("UserName cannot be null");
}
if (user.RoleId == 0)
{
return ResultModel.Error("UserRole cannot be null");
}
if (string.IsNullOrEmpty(user.UserNumber))
{
return ResultModel.Error("UserNumber cannot be null");
}
using var dbContext = _dbContextFactory.CreateDbContext();
var users = dbContext.GetDbSet<UserDto>();
var existingUser = await users.FirstOrDefaultAsync(x => x.UserNumber == user.UserNumber);
if (existingUser != null)
{
return ResultModel.Error("UserNumber already exists");
}
var entity = new UserDto
{
RoleId = user.RoleId,
Name = user.Name,
Phone = user.Phone ?? string.Empty,
UserNumber = user.UserNumber,
Sex = user.Sex ?? SexMode.Other.ToString(),
Password = DESHelper.Encrypt(user.Password ?? "12345", "ZSL12345"),
IsValid = user.IsValid
};
await users.AddAsync(entity);
await dbContext.SaveChangesAsync();
return ResultModel.Success("User added successfully");
}
public async Task<ResultModel> EditUserAsync(UserViewModel? user)
{
if (user == null)
{
return ResultModel.Error("User cannot be null");
}
if (string.IsNullOrEmpty(user.Name))
{
return ResultModel.Error("UserName cannot be null");
}
if (user.RoleId == 0)
{
return ResultModel.Error("UserRole cannot be null");
}
if (string.IsNullOrEmpty(user.UserNumber))
{
return ResultModel.Error("UserNumber cannot be null");
}
using var dbContext = _dbContextFactory.CreateDbContext();
var users = dbContext.GetDbSet<UserDto>();
var existingUser = await users.FirstOrDefaultAsync(x => x.Id == user.Id);
if (existingUser == null)
{
return ResultModel.Error("User-id not find");
}
var duplicateUser = await users.FirstOrDefaultAsync(x => x.UserNumber == user.UserNumber && x.Id != user.Id);
if (duplicateUser != null)
{
return ResultModel.Error("UserNumber already exists");
}
existingUser.RoleId = user.RoleId;
existingUser.Name = user.Name;
existingUser.Phone = user.Phone ?? string.Empty;
existingUser.UserNumber = user.UserNumber;
existingUser.Sex = user.Sex ?? SexMode.Other.ToString();
existingUser.Password = DESHelper.Encrypt(user.Password ?? "12345", "ZSL12345");
existingUser.IsValid = user.IsValid;
existingUser.UpdateTime = DateTime.Now;
await dbContext.SaveChangesAsync();
return ResultModel.Success("User edit successfully");
}
public async Task<ResultModel> DelUserAsync(UserViewModel? user)
{
if (user == null)
{
return ResultModel.Error("User cannot be null");
}
if (string.IsNullOrEmpty(user.Name))
{
return ResultModel.Error("UserName cannot be null");
}
if (user.Id == 0)
{
return ResultModel.Error("User-id cannot be null");
}
using var dbContext = _dbContextFactory.CreateDbContext();
var users = dbContext.GetDbSet<UserDto>();
var existingUser = await users.FirstOrDefaultAsync(x => x.Id == user.Id);
if (existingUser == null)
{
return ResultModel.Error("User-id not find");
}
users.Remove(existingUser);
await dbContext.SaveChangesAsync();
return ResultModel.Success("User deleted successfully");
}
}

View File

@@ -0,0 +1,492 @@
using Cowain.Base.DBContext;
using Microsoft.EntityFrameworkCore;
using MySqlConnector;
using System.Data;
using System.Linq.Expressions;
using Dapper;
using Cowain.Base.IServices;
namespace Cowain.Base.Services;
public class BaseService : IBaseService
{
// 替换为 IDbContextFactory<SqlDbContext>
protected readonly IDbContextFactory<SqlDbContext> _dbContextFactory;
public BaseService(IDbContextFactory<SqlDbContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
/// <summary>
/// 根据ID删除实体
/// </summary>
public int Delete<T>(int Id) where T : class
{
// 每次操作创建独立上下文using 自动释放
using var dbContext = _dbContextFactory.CreateDbContext();
T? t = dbContext.Set<T>().Find(Id);
if (t == null) throw new KeyNotFoundException($"未找到类型 {typeof(T).Name}Id={Id}");
dbContext.Set<T>().Remove(t);
return dbContext.SaveChanges();
}
/// <summary>
/// 根据ID异步删除实体支持取消
/// </summary>
public async Task<int> DeleteAsync<T>(int Id, CancellationToken cancellationToken = default) where T : class
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
var t = await dbContext.Set<T>().FindAsync(Id, cancellationToken).ConfigureAwait(false);
if (t == null) throw new KeyNotFoundException($"未找到类型 {typeof(T).Name}Id={Id}");
dbContext.Set<T>().Remove(t);
return await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 删除实体
/// </summary>
public int Delete<T>(T t) where T : class
{
ArgumentNullException.ThrowIfNull(t);
using var dbContext = _dbContextFactory.CreateDbContext();
var entry = dbContext.Entry(t);
if (entry.State == EntityState.Detached)
{
dbContext.Set<T>().Attach(t);
}
dbContext.Set<T>().Remove(t);
return dbContext.SaveChanges();
}
/// <summary>
/// 删除实体(异步,支持取消)
/// </summary>
public async Task<int> DeleteAsync<T>(T t, CancellationToken cancellationToken = default) where T : class
{
ArgumentNullException.ThrowIfNull(t);
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
var entry = dbContext.Entry(t);
if (entry.State == EntityState.Detached)
{
dbContext.Set<T>().Attach(t);
}
dbContext.Set<T>().Remove(t);
return await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 批量读取实体
/// </summary>
public List<T> Find<T>() where T : class
{
using var dbContext = _dbContextFactory.CreateDbContext();
return dbContext.Set<T>().ToList();
}
/// <summary>
/// 异步、安全的批量读取(支持取消)
/// </summary>
public async Task<List<T>> FindAsync<T>(CancellationToken cancellationToken = default) where T : class
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
return await dbContext.Set<T>().ToListAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 根据ID查询实体
/// </summary>
public T? Find<T>(int id) where T : class
{
using var dbContext = _dbContextFactory.CreateDbContext();
return dbContext.Set<T>().Find(id);
}
/// <summary>
/// 根据ID异步查询实体支持取消
/// </summary>
public async Task<T?> FindAsync<T>(int id, CancellationToken cancellationToken = default) where T : class
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
return await dbContext.Set<T>().FindAsync(new object[] { id }, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 带表达式的异步查询(支持取消)
/// </summary>
public async Task<T?> FirstOrDefaultAsync<T>(Expression<Func<T, bool>> funcWhere, CancellationToken cancellationToken = default) where T : class
{
using var dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Set<T>().Where(funcWhere).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
/// 插入实体
/// </summary>
public int Insert<T>(T t) where T : class
{
ArgumentNullException.ThrowIfNull(t);
using var dbContext = _dbContextFactory.CreateDbContext();
dbContext.Set<T>().Add(t);
return dbContext.SaveChanges();
}
/// <summary>
/// 异步插入实体
/// </summary>
public async Task<int> InsertAsync<T>(T t, CancellationToken cancellationToken = default) where T : class
{
ArgumentNullException.ThrowIfNull(t);
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
dbContext.Set<T>().Add(t);
return await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 批量插入实体
/// </summary>
public int Insert<T>(IEnumerable<T> tList) where T : class
{
using var dbContext = _dbContextFactory.CreateDbContext();
dbContext.Set<T>().AddRange(tList);
return dbContext.SaveChanges();
}
/// <summary>
/// 批量异步插入实体
/// </summary>
public async Task<int> InsertAsync<T>(IEnumerable<T> tList, CancellationToken cancellationToken = default) where T : class
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
dbContext.Set<T>().AddRange(tList);
return await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 带表达式的查询
/// </summary>
public List<T> Query<T>(Expression<Func<T, bool>>? funcWhere = null) where T : class
{
// 注意Query 返回 IQueryable 时上下文生命周期需由调用方管理或改为立即执行ToList/First 等)
var dbContext = _dbContextFactory.CreateDbContext();
IQueryable<T> query = dbContext.Set<T>();
if (funcWhere is not null)
{
query = query.Where(funcWhere);
}
return query.ToList();
}
/// <summary>
/// 带表达式的异步查询(支持取消、空条件查全部)
/// </summary>
public async Task<List<T>> QueryAsync<T>(
Expression<Func<T, bool>>? funcWhere = null, // 设置默认值,调用更便捷
CancellationToken cancellationToken = default)
where T : class
{
using var dbContext = _dbContextFactory.CreateDbContext();
IQueryable<T> query = dbContext.Set<T>();
if (funcWhere is not null)
{
query = query.Where(funcWhere);
}
return await query.ToListAsync(cancellationToken);
}
/// <summary>
/// 基础版:返回实体列表+总条数(无投影)
/// </summary>
public async Task<(List<T> Data, int TotalCount)> QueryAsync<T, TKey>(
Expression<Func<T, bool>>? funcWhere = null,
Expression<Func<T, TKey>>? orderBy = null,
bool isAscending = true,
int pageIndex = 1,
int pageSize = 20,
CancellationToken cancellationToken = default)
where T : class
{
pageIndex = pageIndex < 1 ? 1 : pageIndex;
pageSize = pageSize < 0 ? 0 : pageSize;
using var dbContext = _dbContextFactory.CreateDbContext();
IQueryable<T> query = dbContext.Set<T>();
if (funcWhere is not null) query = query.Where(funcWhere);
if (orderBy is not null) query = isAscending ? query.OrderBy(orderBy) : query.OrderByDescending(orderBy);
if (pageSize > 0)
{
long totalCountLong = await query.LongCountAsync(cancellationToken);
int totalCount = (int)Math.Min(totalCountLong, int.MaxValue);
var data = await query.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken);
return (data, totalCount);
}
else
{
var data = await query.ToListAsync(cancellationToken);
return (data, data.Count);
}
}
/// <summary>
/// 增强版支持投影Select映射直接返回ViewModel列表+总条数
/// </summary>
/// <typeparam name="T">原实体类型</typeparam>
/// <typeparam name="TKey">排序字段类型</typeparam>
/// <typeparam name="TViewModel">目标ViewModel类型</typeparam>
/// <param name="selectExpression">投影表达式EF会解析为SQL的SELECT字段</param>
public async Task<(List<TViewModel> Data, int TotalCount)> QueryAsync<T, TKey, TViewModel>(
Expression<Func<T, TViewModel>> selectExpression, // 新增:投影映射表达式
Expression<Func<T, bool>>? funcWhere = null,
Expression<Func<T, TKey>>? orderBy = null,
bool isAscending = true,
int pageIndex = 1,
int pageSize = 20,
CancellationToken cancellationToken = default)
where T : class
{
pageIndex = pageIndex < 1 ? 1 : pageIndex;
pageSize = pageSize < 0 ? 0 : pageSize;
using var dbContext = _dbContextFactory.CreateDbContext();
IQueryable<T> query = dbContext.Set<T>();
// 1. 过滤
if (funcWhere is not null) query = query.Where(funcWhere);
// 2. 排序
if (orderBy is not null) query = isAscending ? query.OrderBy(orderBy) : query.OrderByDescending(orderBy);
// 3. 先查总条数(注意:总条数要在投影前查,避免投影影响计数)
long totalCountLong = await query.LongCountAsync(cancellationToken);
int totalCount = (int)Math.Min(totalCountLong, int.MaxValue);
// 4. 分页+投影EF会转换为SQLSELECT Id, Name, LayOutX, LayOutY FROM ... LIMIT ...
IQueryable<TViewModel> viewModelQuery = query
.Skip((pageIndex - 1) * pageSize)
.Take(pageSize)
.Select(selectExpression); // 内置投影替代手动Select
// 5. 转为List
var data = await viewModelQuery.ToListAsync(cancellationToken);
return (data, totalCount);
}
/// <summary>
/// 更新实体
/// </summary>
public int Update<T>(T t) where T : class
{
ArgumentNullException.ThrowIfNull(t);
using var dbContext = _dbContextFactory.CreateDbContext();
var entry = dbContext.Entry(t);
if (entry.State == EntityState.Detached)
{
dbContext.Set<T>().Attach(t);
}
dbContext.Entry(t).State = EntityState.Modified;
return dbContext.SaveChanges();
}
/// <summary>
/// 异步更新实体(支持取消)
/// </summary>
public async Task<int> UpdateAsync<T>(T t, CancellationToken cancellationToken = default) where T : class
{
ArgumentNullException.ThrowIfNull(t);
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
var entry = dbContext.Entry(t);
if (entry.State == EntityState.Detached)
{
dbContext.Set<T>().Attach(t);
}
dbContext.Entry(t).State = EntityState.Modified;
return await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 批量更新实体
/// </summary>
public int Update<T>(IEnumerable<T> tList) where T : class
{
ArgumentNullException.ThrowIfNull(tList);
var list = tList as IList<T> ?? tList.ToList();
if (list.Count == 0) return 0;
using var dbContext = _dbContextFactory.CreateDbContext();
dbContext.Set<T>().UpdateRange(list);
return dbContext.SaveChanges();
}
/// <summary>
/// 异步批量更新实体(支持取消)
/// </summary>
public async Task<int> UpdateAsync<T>(IEnumerable<T> tList, CancellationToken cancellationToken = default) where T : class
{
ArgumentNullException.ThrowIfNull(tList);
var list = tList as IList<T> ?? tList.ToList();
if (list.Count == 0) return 0;
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
dbContext.Set<T>().UpdateRange(list);
return await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 使用sql语句获取DataTable
/// </summary>
public DataTable GetDataTable(string sql, IEnumerable<MySqlParameter>? parameters = null)
{
if (string.IsNullOrWhiteSpace(sql)) throw new ArgumentException("sql 不能为空", nameof(sql));
// 从工厂创建上下文获取连接字符串
using var dbContext = _dbContextFactory.CreateDbContext();
using var conn = new MySqlConnection(dbContext.Database.GetConnectionString());
using var cmd = new MySqlCommand(sql, conn)
{
CommandType = CommandType.Text
};
if (parameters != null)
{
foreach (var p in parameters) cmd.Parameters.Add(p);
}
using var adapter = new MySqlDataAdapter(cmd);
var table = new DataTable();
adapter.Fill(table);
return table;
}
/// <summary>
/// 异步使用sql语句获取DataTable支持取消
/// </summary>
public async Task<DataTable> GetDataTableAsync(string sql, IEnumerable<MySqlParameter>? parameters = null, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(sql)) throw new ArgumentException("sql 不能为空", nameof(sql));
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using var conn = new MySqlConnection(dbContext.Database.GetConnectionString());
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
using var cmd = new MySqlCommand(sql, conn) { CommandType = CommandType.Text };
if (parameters != null) cmd.Parameters.AddRange(parameters.ToArray());
using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
var table = new DataTable();
table.Load(reader);
return table;
}
// ================= Dapper 辅助方法 =================
/// <summary>
/// 使用 Dapper 查询并映射到 POCO同步
/// </summary>
public IEnumerable<T> QueryByDapper<T>(string sql, object? param = null)
{
if (string.IsNullOrWhiteSpace(sql)) throw new ArgumentException("sql 不能为空", nameof(sql));
using var dbContext = _dbContextFactory.CreateDbContext();
using var conn = new MySqlConnection(dbContext.Database.GetConnectionString());
conn.Open();
return conn.Query<T>(sql, param);
}
/// <summary>
/// 使用 Dapper 查询并映射到 POCO异步支持 CancellationToken
/// </summary>
public async Task<IEnumerable<T>> QueryByDapperAsync<T>(string sql, object? param = null, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(sql)) throw new ArgumentException("sql 不能为空", nameof(sql));
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using var conn = new MySqlConnection(dbContext.Database.GetConnectionString());
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
var command = new CommandDefinition(sql, param, cancellationToken: cancellationToken);
var result = await conn.QueryAsync<T>(command).ConfigureAwait(false);
return result;
}
/// <summary>
/// 使用 Dapper 查询单条记录(同步)
/// </summary>
public T? QueryFirstOrDefaultByDapper<T>(string sql, object? param = null)
{
if (string.IsNullOrWhiteSpace(sql)) throw new ArgumentException("sql 不能为空", nameof(sql));
using var dbContext = _dbContextFactory.CreateDbContext();
using var conn = new MySqlConnection(dbContext.Database.GetConnectionString());
conn.Open();
return conn.QueryFirstOrDefault<T>(sql, param);
}
/// <summary>
/// 使用 Dapper 查询单条记录(异步)
/// </summary>
public async Task<T?> QueryFirstOrDefaultByDapperAsync<T>(string sql, object? param = null, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(sql)) throw new ArgumentException("sql 不能为空", nameof(sql));
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using var conn = new MySqlConnection(dbContext.Database.GetConnectionString());
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
var command = new CommandDefinition(sql, param, cancellationToken: cancellationToken);
return await conn.QueryFirstOrDefaultAsync<T>(command).ConfigureAwait(false);
}
/// <summary>
/// 使用 Dapper 执行非查询 SQL同步返回受影响行数
/// </summary>
public int ExecuteByDapper(string sql, object? param = null)
{
if (string.IsNullOrWhiteSpace(sql)) throw new ArgumentException("sql 不能为空", nameof(sql));
using var dbContext = _dbContextFactory.CreateDbContext();
using var conn = new MySqlConnection(dbContext.Database.GetConnectionString());
conn.Open();
return conn.Execute(sql, param);
}
/// <summary>
/// 使用 Dapper 执行非查询 SQL异步返回受影响行数
/// </summary>
public async Task<int> ExecuteByDapperAsync(string sql, object? param = null, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(sql)) throw new ArgumentException("sql 不能为空", nameof(sql));
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using var conn = new MySqlConnection(dbContext.Database.GetConnectionString());
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
var command = new CommandDefinition(sql, param, cancellationToken: cancellationToken);
return await conn.ExecuteAsync(command).ConfigureAwait(false);
}
/// <summary>
/// 使用 Dapper 执行 SQL 并返回 DataTable异步支持 CancellationToken
/// </summary>
public async Task<DataTable> QueryToDataTableByDapperAsync(string sql, object? param = null, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(sql)) throw new ArgumentException("sql 不能为空", nameof(sql));
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using var conn = new MySqlConnection(dbContext.Database.GetConnectionString());
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
var command = new CommandDefinition(sql, param, cancellationToken: cancellationToken, flags: CommandFlags.Buffered);
await using var reader = await conn.ExecuteReaderAsync(command).ConfigureAwait(false);
var table = new DataTable();
table.Load(reader);
return table;
}
/// <summary>
/// 使用 Dapper 执行 SQL 并返回 DataTable同步
/// </summary>
public DataTable QueryToDataTableByDapper(string sql, object? param = null)
{
if (string.IsNullOrWhiteSpace(sql)) throw new ArgumentException("sql 不能为空", nameof(sql));
using var dbContext = _dbContextFactory.CreateDbContext();
using var conn = new MySqlConnection(dbContext.Database.GetConnectionString());
conn.Open();
using var reader = conn.ExecuteReader(sql, param);
var table = new DataTable();
table.Load(reader);
return table;
}
}

View File

@@ -0,0 +1,57 @@
using Cowain.Base.DBContext;
using Cowain.Base.IServices;
using Cowain.Base.Models;
using Cowain.Base.ViewModels;
using Microsoft.EntityFrameworkCore;
namespace Cowain.Base.Services;
public class LogService : BaseService, ILogService
{
public LogService(IDbContextFactory<SqlDbContext> dbContextFactory) : base(dbContextFactory)
{
}
public async Task<List<SerilogViewModel>> GetAllAsync()
{
var data = await FindAsync<SerilogDto>();
return new List<SerilogViewModel>(data.Select(x => new SerilogViewModel
{
Id = x.id,
Template = x.Template,
Message = x.Message,
Exception = x.Exception,
Properties = x.Properties,
Level = x.Level,
Timestamp = x.Timestamp
}));
}
/// <summary>
/// 分页获取日志复用BaseService的FindAsync+内存分页)
/// </summary>
public async Task<(List<SerilogViewModel>, int totals)> GetAllAsync(int pageIndex, int pageSize)
{
// 复用BaseService的异步查询方法
var allData = await FindAsync<SerilogDto>();
// 分页处理
var paginatedData = allData
.OrderByDescending(x => x.id)
.Skip((pageIndex - 1) * pageSize)
.Take(pageSize)
.Select(x => new SerilogViewModel
{
Id = x.id,
Template = x.Template,
Message = x.Message,
Exception = x.Exception,
Properties = x.Properties,
Level = x.Level,
Timestamp = x.Timestamp
})
.ToList();
return (paginatedData, allData.Count);
}
}

View File

@@ -0,0 +1,152 @@
using Avalonia;
using Cowain.Base.DBContext;
using Cowain.Base.Helpers;
using Cowain.Base.IServices;
using Cowain.Base.Models;
using Cowain.Base.Models.Admins;
using Cowain.Base.Models.Menu;
using Cowain.Base.ViewModels;
using Ke.Bee.Localization.Localizer.Abstractions;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.Json;
using MenuItem = Cowain.Base.Models.Menu.MenuItem;
namespace Cowain.Base.Services;
public class UserRoleMenuService : BaseService, IUserRoleMenuService
{
/// <summary>
/// 菜单数据
/// </summary>
private readonly List<MenuItem> _menuItems;
private readonly PageViewMenuHelper _pageMenuHelper;
public UserRoleMenuService(IDbContextFactory<SqlDbContext> dbContextFactory, PageViewMenuHelper pageMenuHelper, MenuConfigurationContext menuContext) : base(dbContextFactory)
{
_menuItems = menuContext.Menus;
_pageMenuHelper = pageMenuHelper;
}
public async Task<List<UserRoleViewModel>> GetAllAsync()
{
using var dbContext = _dbContextFactory.CreateDbContext();
var userRoles = dbContext.GetDbSet<UserRoleDto>();
var userRoleMenus = dbContext.GetDbSet<UserRoleMenuDto>();
var jsonSerializerOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
var result = await (from ur in userRoles
join urm in userRoleMenus on ur.Id equals urm.RoleId into urms
select new UserRoleViewModel
{
RoleId = ur.Id,
RoleName = ur.RoleName,
IsValid = ur.IsValid,
Menus = new ObservableCollection<UserRoleMenuViewModel>(urms.Select(urm => new UserRoleMenuViewModel
{
Id = urm.Id,
RoleId = urm.RoleId,
MenuKey = urm.MenuKey,
MenuName = _pageMenuHelper.GetMenuName(urm.MenuKey),
MenuActions = new ObservableCollection<string>(JsonSerializer.Deserialize<List<string>>(urm.MenuActions ?? "[]", jsonSerializerOptions) ?? new List<string>())
}).ToList())
}).ToListAsync();
return result ?? new List<UserRoleViewModel>();
}
public async Task<ResultModel> EditUserRoleMenuAsync(UserRoleViewModel? userRole)
{
if (userRole == null)
{
return ResultModel.Error("UserRole cannot be null");
}
if (string.IsNullOrEmpty(userRole.RoleName))
{
return ResultModel.Error("UserRoleName cannot be null");
}
using var dbContext = _dbContextFactory.CreateDbContext();
var userRoleMenus = dbContext.GetDbSet<UserRoleMenuDto>();
// 删除旧的菜单
var oldMenus = userRoleMenus.Where(urm => urm.RoleId == userRole.RoleId);
dbContext.RemoveRange(oldMenus);
// 添加新的菜单
var newMenus = userRole.Menus?.Select(menu => new UserRoleMenuDto
{
RoleId = userRole.RoleId,
MenuKey = menu.MenuKey,
MenuActions = JsonSerializer.Serialize(menu.MenuActions)
}).ToList();
if (newMenus != null)
{
await userRoleMenus.AddRangeAsync(newMenus);
var ret = await dbContext.SaveChangesAsync();
// 添加完新的数据后id需要告诉userRole的Menus
var addedMenus = await userRoleMenus.Where(urm => urm.RoleId == userRole.RoleId).ToListAsync();
if (userRole.Menus != null)
{
for (int i = 0; i < userRole.Menus.Count; i++)
{
userRole.Menus[i].Id = addedMenus[i].Id;
}
}
return ResultModel.Success($"UserRoleMenu updated successfullycount is {ret}");
}
else
{
var ret = await dbContext.SaveChangesAsync();
return ResultModel.Success($"UserRoleMenu updated successfullycount is {ret}");
}
}
public async Task<ResultModel> EditMenuActionsAsync(UserRoleMenuViewModel? userRoleMenu)
{
if (userRoleMenu == null)
{
return ResultModel.Error("UserRoleMenu cannot be null");
}
if (string.IsNullOrEmpty(userRoleMenu.MenuKey))
{
return ResultModel.Error("MenuKey cannot be null");
}
using var dbContext = _dbContextFactory.CreateDbContext();
var userRoleMenus = dbContext.GetDbSet<UserRoleMenuDto>();
var menu = await userRoleMenus.FirstOrDefaultAsync(urm => urm.Id == userRoleMenu.Id);
if (menu == null)
{
return ResultModel.Error("UserRoleMenu Actions not found");
}
menu.MenuActions = JsonSerializer.Serialize(userRoleMenu.MenuActions);
dbContext.Update(menu);
var ret = await dbContext.SaveChangesAsync();
return ResultModel.Success($"UserRoleMenu Actions updated successfullycount is {ret}");
}
public async Task<ResultModel> DeleteMenuActionsAsync(UserRoleMenuViewModel? userRoleMenu)
{
if (userRoleMenu == null)
{
return ResultModel.Error("UserRoleMenu cannot be null");
}
if (string.IsNullOrEmpty(userRoleMenu.MenuKey))
{
return ResultModel.Error("MenuKey cannot be null");
}
using var dbContext = _dbContextFactory.CreateDbContext();
var userRoleMenus = dbContext.GetDbSet<UserRoleMenuDto>();
var menu = await userRoleMenus.FirstOrDefaultAsync(urm => urm.Id == userRoleMenu.Id);
if (menu == null)
{
return ResultModel.Error("UserRoleMenu Actions not found");
}
var ret = await DeleteAsync<UserRoleMenuDto>(menu);
return ResultModel.Success($"UserRoleMenu Actions remove successfullycount is {ret}");
}
}

View File

@@ -0,0 +1,133 @@
using Cowain.Base.DBContext;
using Cowain.Base.IServices;
using Cowain.Base.Models;
using Cowain.Base.Models.Admins;
using Cowain.Base.ViewModels;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.Services;
public class UserRoleService : BaseService, IUserRoleService
{
public UserRoleService(IDbContextFactory<SqlDbContext> dbContextFactory) : base(dbContextFactory)
{
}
public async Task<ResultModel> AddUserRoleAsync(UserRoleViewModel? userRole)
{
if (userRole == null)
{
return ResultModel.Error("UserRole cannot be null");
}
if (string.IsNullOrEmpty(userRole.RoleName))
{
return ResultModel.Error("UserRoleName cannot be null");
}
using var dbContext = _dbContextFactory.CreateDbContext();
var DbSet = dbContext.GetDbSet<UserRoleDto>();
var existingRole = await DbSet.FirstOrDefaultAsync(x => x.RoleName == userRole.RoleName);
if (existingRole != null)
{
return ResultModel.Error("UserRoleName already exists");
}
var entity = new UserRoleDto
{
Id = userRole.RoleId,
RoleName = userRole.RoleName,
IsValid = userRole.IsValid
};
await DbSet.AddAsync(entity);
await dbContext.SaveChangesAsync();
return ResultModel.Success("UserRole added successfully");
}
public async Task<ResultModel> EditUserRoleAsync(UserRoleViewModel? userRole)
{
if (userRole == null)
{
return ResultModel.Error("UserRole cannot be null");
}
if (string.IsNullOrEmpty(userRole.RoleName))
{
return ResultModel.Error("UserRoleName cannot be null");
}
using var dbContext = _dbContextFactory.CreateDbContext();
var DbSet = dbContext.GetDbSet<UserRoleDto>();
var existingRole = await DbSet.FirstOrDefaultAsync(x => x.Id == userRole.RoleId);
if (existingRole == null)
{
return ResultModel.Error("UserRoleName no exists");
}
var duplicateRole = await DbSet.FirstOrDefaultAsync(x => x.RoleName == userRole.RoleName && x.Id != userRole.RoleId);
if (duplicateRole != null)
{
return ResultModel.Error("UserRoleName already exists");
}
existingRole.IsValid = userRole.IsValid;
existingRole.RoleName = userRole.RoleName;
existingRole.UpdateTime = DateTime.Now;
await dbContext.SaveChangesAsync();
return ResultModel.Success("UserRole updated successfully");
}
public async Task<ResultModel> DelUserRoleAsync(UserRoleViewModel? userRole)
{
if (userRole == null)
{
return ResultModel.Error("UserRole cannot be null");
}
if (string.IsNullOrEmpty(userRole.RoleName))
{
return ResultModel.Error("UserRoleName cannot be null");
}
using var dbContext = _dbContextFactory.CreateDbContext();
var DbSet = dbContext.GetDbSet<UserRoleDto>();
var entity = await DbSet.FirstOrDefaultAsync(x => x.Id == userRole.RoleId);
if (entity == null)
{
return ResultModel.Error("UserRole not found");
}
// 删除UserRole还要将UserRoleMenu表中RoleId相同的项也同时删除
var userRoleMenuDbSet = dbContext.GetDbSet<UserRoleMenuDto>();
var userRoleMenus = await userRoleMenuDbSet.Where(x => x.RoleId == userRole.RoleId).ToListAsync();
userRoleMenuDbSet.RemoveRange(userRoleMenus);
DbSet.Remove(entity);
await dbContext.SaveChangesAsync();
return ResultModel.Success("UserRole and related UserRoleMenus deleted successfully");
}
public async Task<List<UserRoleViewModel>> GetAllAsync()
{
using var dbContext = _dbContextFactory.CreateDbContext();
var DbSet = dbContext.GetDbSet<UserRoleDto>();
var data = await DbSet.ToListAsync();
return new List<UserRoleViewModel>(data.Select(x => new UserRoleViewModel
{
RoleId = x.Id,
RoleName = x.RoleName,
IsValid = x.IsValid
}));
}
public async Task<(List<UserRoleViewModel>, int totals)> GetAllAsync(int pageIndex, int pageSize)
{
using var dbContext = _dbContextFactory.CreateDbContext();
var DbSet = dbContext.GetDbSet<UserRoleDto>();
var data = await DbSet.ToListAsync();
var list = data.Skip((pageIndex - 1) * pageSize).Take(pageSize);
return (new List<UserRoleViewModel>(list.Select(x => new UserRoleViewModel
{
RoleId = x.Id,
RoleName = x.RoleName,
IsValid = x.IsValid
})), data.Count());
}
}

3
Cowain.Base/UPGRADING.md Normal file
View File

@@ -0,0 +1,3 @@
## 首次发布 1.0.0
* 第一版

View File

@@ -0,0 +1,217 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.UndoRedo;
public interface IActionsHistory : INotifyPropertyChanged
{
int MaxSize { get; set; }
bool CanUndo { get; }
bool CanRedo { get; }
bool IsEnabled { get; set; }
IAction? Current { get; }
void Undo();
void Redo();
void Clear();
/// <summary>
/// All future modifications will be merged together to create a single history item until batch is disposed.
/// </summary>
IDisposable Batch(string? label = default);
void Record(IAction action);
/// <summary>
/// All future modifications will be merged together to create a single history item until history is resumed.
/// </summary>
void Pause(string? label = default);
/// <summary>Each future modifications will create a new history item.</summary>
void Resume();
}
public interface IAction
{
string? Label { get; }
void Execute();
void Undo();
}
public static class ActionsHistoryExtensions
{
public static void Record(this IActionsHistory history, Action execute, Action unexecute, string? label = default)
=> history.Record(new DelegateAction(execute, unexecute, label));
public static void ExecuteAction(this IActionsHistory history, IAction action)
{
history.Record(action);
action.Execute();
}
}
public class ActionsHistory : IActionsHistory
{
private readonly List<IAction> _history = new List<IAction>();
private readonly List<IAction> _batchHistory = new List<IAction>();
private int _position = -1;
private bool _isApplyingOperation = false;
private string? _batchLabel;
private int _batchDepth;
private static readonly PropertyChangedEventArgs _canRedoArgs = new PropertyChangedEventArgs(nameof(CanRedo));
private static readonly PropertyChangedEventArgs _canUndoArgs = new PropertyChangedEventArgs(nameof(CanUndo));
public event PropertyChangedEventHandler? PropertyChanged;
public static readonly ActionsHistory Global = new ActionsHistory();
public bool IsBatching { get; private set; }
public int MaxSize { get; set; } = 50;
public bool CanRedo => _history.Count > 0 && _position < _history.Count - 1;
public bool CanUndo => _position > -1;
public bool IsEnabled { get; set; } = true;
public IAction? Current => CanUndo ? _history[_position] : null;
public IDisposable Batch(string? label = default)
=> new BatchOperation(label, this);
public void Record(IAction op)
{
// Prevent recording the undo or redo operation
if (_isApplyingOperation || !IsEnabled)
{
return;
}
if (IsBatching)
{
_batchHistory.Add(op);
}
else
{
AddToUndoStack(op);
}
}
private void AddToUndoStack(IAction op)
{
if (_position < _history.Count - 1)
{
_history.RemoveRange(_position + 1, _history.Count - _position - 1);
}
if (_history.Count >= MaxSize)
{
_history.RemoveAt(0);
_position--;
}
_history.Add(op);
_position++;
PropertyChanged?.Invoke(this, _canRedoArgs);
PropertyChanged?.Invoke(this, _canUndoArgs);
}
public void Undo()
{
if (IsBatching)
{
throw new InvalidOperationException($"{nameof(Undo)} is not allowed during a batch.");
}
if (CanUndo)
{
var op = _history[_position];
_isApplyingOperation = true;
op.Undo();
_isApplyingOperation = false;
_position--;
}
}
public void Redo()
{
if (IsBatching)
{
throw new InvalidOperationException($"{nameof(Redo)} is not allowed during a batch.");
}
if (CanRedo)
{
_position++;
var op = _history[_position];
_isApplyingOperation = true;
op.Execute();
_isApplyingOperation = false;
}
}
public void Clear()
{
_history.Clear();
_batchHistory.Clear();
}
public void Pause(string? label = default)
{
if (_batchDepth > 0)
{
return;
}
_batchLabel = label;
IsBatching = true;
}
public void Resume()
{
if (_batchDepth > 0)
{
return;
}
if (_batchHistory.Count > 0)
{
AddToUndoStack(new BatchAction(_batchLabel, _batchHistory));
_batchHistory.Clear();
}
_batchLabel = null;
IsBatching = false;
}
private class BatchOperation : IDisposable
{
private readonly ActionsHistory _history;
private bool _disposed;
public BatchOperation(string? label, ActionsHistory history)
{
_history = history;
_history.Pause(label);
_history._batchDepth++;
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
_history._batchDepth--;
_history.Resume();
}
}
}
}

View File

@@ -0,0 +1,33 @@
namespace Cowain.Base.UndoRedo;
public class BatchAction : IAction
{
public BatchAction(string? label, IEnumerable<IAction> history)
{
History = history.Reverse().ToList();
Label = label;
}
public IReadOnlyList<IAction> History { get; }
public string? Label { get; }
public void Execute()
{
for (int i = History.Count - 1; i >= 0; i--)
{
History[i].Execute();
}
}
public void Undo()
{
for (int i = 0; i < History.Count; i++)
{
History[i].Undo();
}
}
public override string? ToString()
=> Label;
}

View File

@@ -0,0 +1,22 @@
namespace Cowain.Base.UndoRedo;
public class DelegateAction : IAction
{
private readonly Action _execute;
private readonly Action _undo;
public string? Label { get; }
public DelegateAction(Action apply, Action unapply, string? label)
{
_execute = apply;
_undo = unapply;
Label = label;
}
public void Execute() => _execute();
public void Undo() => _undo();
public override string? ToString()
=> Label;
}

View File

@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.UndoRedo;
public interface IPropertyAccessor
{
object? GetValue(object instance);
void SetValue(object instance, object? value);
bool CanRead { get; }
bool CanWrite { get; }
}
public sealed class PropertyAccessor<TInstanceType, TPropertyType> : IPropertyAccessor where TInstanceType : class
{
private readonly Func<TInstanceType, TPropertyType> _getter;
private readonly Action<TInstanceType, TPropertyType> _setter;
public bool CanRead { get; }
public bool CanWrite { get; }
public PropertyAccessor(Func<TInstanceType, TPropertyType> getter, Action<TInstanceType, TPropertyType> setter)
{
_getter = getter;
_setter = setter;
CanRead = getter != null;
CanWrite = setter != null;
}
public object? GetValue(object instance)
=> _getter((TInstanceType)instance);
public void SetValue(object instance, object? value)
=> _setter((TInstanceType)instance, (TPropertyType)value!);
}
public class PropertyCache
{
private static readonly Dictionary<string, IPropertyAccessor> _properties = new Dictionary<string, IPropertyAccessor>();
public static IPropertyAccessor Get(Type type, string name)
{
string propKey = $"{type.FullName}.{name}";
if (!_properties.TryGetValue(propKey, out var result))
{
var prop = type.GetProperty(name);
result = Create(type, prop!);
_properties.Add(propKey, result);
}
return result;
}
public static IPropertyAccessor Get<T>(string name)
=> Get(typeof(T), name);
private static IPropertyAccessor Create(Type type, PropertyInfo property)
{
Delegate? getterInvocation = default;
Delegate? setterInvocation = default;
if (property.CanRead)
{
MethodInfo getMethod = property.GetGetMethod(true)!;
Type getterType = typeof(Func<,>).MakeGenericType(type, property.PropertyType);
getterInvocation = Delegate.CreateDelegate(getterType, getMethod);
}
if (property.CanWrite)
{
MethodInfo setMethod = property.GetSetMethod(true)!;
Type setterType = typeof(Action<,>).MakeGenericType(type, property.PropertyType);
setterInvocation = Delegate.CreateDelegate(setterType, setMethod);
}
Type adapterType = typeof(PropertyAccessor<,>).MakeGenericType(type, property.PropertyType);
return (IPropertyAccessor)Activator.CreateInstance(adapterType, getterInvocation, setterInvocation)!;
}
}

View File

@@ -0,0 +1,86 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.UndoRedo;
[Flags]
public enum PropertyFlags
{
Disable = 0,
Enable = 1
}
public abstract class Undoable : ObservableObject
{
private readonly HashSet<string> _trackedProperties = new HashSet<string>();
public IActionsHistory History { get; }
private void RecordHistory<TPropType>(string propName, TPropType previous, TPropType current)
{
if (_trackedProperties.Contains(propName))
{
var prop = PropertyCache.Get(GetType(), propName);
History.Record(() => prop.SetValue(this, current), () => prop.SetValue(this, previous), propName);
}
}
protected void RecordProperty(string propName, PropertyFlags flags = PropertyFlags.Enable)
{
if (flags == PropertyFlags.Disable)
{
_trackedProperties.Remove(propName);
}
else if (flags.HasFlag(PropertyFlags.Enable))
{
_trackedProperties.Add(propName);
}
}
protected void RecordProperty<TType>(Expression<Func<TType, object?>> selector, PropertyFlags flags = PropertyFlags.Enable)
{
if (!RuntimeFeature.IsDynamicCodeSupported)
return;
string name = GetPropertyName(selector);
RecordProperty(name, flags);
}
private static string GetPropertyName(Expression memberAccess)
=> memberAccess switch
{
LambdaExpression lambda => GetPropertyName(lambda.Body),
MemberExpression mbr => mbr.Member.Name,
UnaryExpression unary => GetPropertyName(unary.Operand),
_ => throw new Exception($"Member name could not be extracted from {memberAccess}.")
};
//protected override bool SetProperty<TPropType>(ref TPropType field, TPropType value, [CallerMemberName] string propertyName = "")
//{
// TPropType prev = field;
// if (base.SetProperty(ref field, value, propertyName))
// {
// RecordHistory(propertyName, prev, value);
// return true;
// }
// return false;
//}
public Undoable()
{
History = ActionsHistory.Global;
}
public Undoable(IActionsHistory history)
{
History = history;
}
}

View File

@@ -0,0 +1,38 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Cowain.Base.ViewModels;
public partial class AlarmViewModel : ObservableObject
{
[ObservableProperty]
private int _id;
[ObservableProperty]
private int _tagId;
[ObservableProperty]
private bool _status;
[ObservableProperty]
private string _desc = string.Empty;
[ObservableProperty]
private int _group;
[ObservableProperty]
private int _level;
[ObservableProperty]
private string _groupName = string.Empty;
[ObservableProperty]
private string _levelName = string.Empty;
[ObservableProperty]
private string _color = string.Empty;
[ObservableProperty]
private DateTime _startTime = DateTime.MinValue;
[ObservableProperty]
private DateTime? _stopTime;
}

View File

@@ -0,0 +1,20 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.ViewModels;
public partial class LoginUserViewModel : ObservableObject
{
[ObservableProperty]
private UserViewModel? _user;
/// <summary>
/// 菜单列表
/// </summary>
[ObservableProperty]
private ObservableCollection<UserRoleMenuViewModel>? _menus;
}

View File

@@ -0,0 +1,56 @@
using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.Generic;
using System.Windows.Input;
namespace Cowain.Base.ViewModels;
public partial class MenuItemViewModel : ObservableObject
{
/// <summary>
/// 唯一标识
/// </summary>
public string Key { get; set; } = string.Empty;
/// <summary>
/// 按钮文本
/// </summary>
[ObservableProperty]
public string _text;
/// <summary>
/// 图标
/// </summary>
public StreamGeometry? Icon { get; set; }
/// <summary>
/// 子菜单
/// </summary>
public ICollection<MenuItemViewModel>? Items { get; set; }
/// <summary>
/// 菜单是否选中
/// </summary>
[ObservableProperty]
private bool _isActive;
/// <summary>
/// 菜单点击命令
/// </summary>
public ICommand? MenuClickCommand { get; set; }
/// <summary>
/// 命令参数
/// </summary>
public string? CommandParameter { get; set; }
/// <summary>
/// 页面命令
/// </summary>
public string[]? PageActions { get; set; }
public MenuItemViewModel(string text)
{
Text = text;
}
public void AddItem(MenuItemViewModel item)
{
Items ??= [];
Items?.Add(item);
}
}

View File

@@ -0,0 +1,44 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Cowain.Base.ViewModels;
/// <summary>
/// 视图页面模型基类
/// </summary>
public partial class PageViewModelBase : ObservableObject, IDisposable
{
/// <summary>
/// 页面标题
/// </summary>
public string? Title { get; set; }
public bool Disposed => _disposed;
private bool _disposed;
// 公开的 Dispose 方法IDisposable 接口要求)
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// 虚方法:允许子类重写,清理自身资源
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// 基类自身的托管资源清理(如命令、定时器等)
}
_disposed = true;
}
}
// 析构函数(非托管资源兜底)
~PageViewModelBase()
{
Dispose(false);
}
}

View File

@@ -0,0 +1,27 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.ViewModels;
public partial class SerilogViewModel : ObservableObject
{
[ObservableProperty]
private int _id;
[ObservableProperty]
private string? _template;
[ObservableProperty]
private string? _message;
[ObservableProperty]
private string? _exception;
[ObservableProperty]
private string? _properties;
[ObservableProperty]
private string? _level;
[ObservableProperty]
private string? _timestamp;
}

View File

@@ -0,0 +1,65 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Cowain.Base.Models.Task;
using Ke.Bee.Localization.Localizer.Abstractions;
namespace Cowain.Base.ViewModels;
/// <summary>
/// 任务类页面视图模型基类
/// </summary>
public partial class TaskPageViewModelBase : PageViewModelBase
{
/// <summary>
/// 本地化资源
/// </summary>
protected ILocalizer L { get; }
/// <summary>
/// 任务状态文本
/// </summary>
[ObservableProperty]
private string? _taskStatusText;
public TaskPageViewModelBase(ILocalizer localizer)
{
L = localizer;
}
/// <summary>
/// 设置任务准备状态
/// </summary>
/// <param name="tasksTotalCount">任务总数</param>
protected void SetPendingStatus(int tasksTotalCount)
{
SetTaskStatusText(TaskStatusEnum.Pending, tasksTotalCount);
}
/// <summary>
/// 设置运行中状态
/// </summary>
/// <param name="currentTaskIndex"></param>
/// <param name="tasksTotalCount"></param>
protected void SetRunningStatus(int currentTaskIndex, int tasksTotalCount)
{
SetTaskStatusText(TaskStatusEnum.Running, currentTaskIndex, tasksTotalCount);
}
/// <summary>
/// 设置任务完成状态
/// </summary>
protected void SetCompletedStatus()
{
SetTaskStatusText(TaskStatusEnum.Completed);
}
private void SetTaskStatusText(TaskStatusEnum taskStatusEnum, params object[] argments)
{
TaskStatusText = taskStatusEnum switch
{
TaskStatusEnum.Pending => string.Format(L["Task.TaskPendingStatusText"], argments),
TaskStatusEnum.Running => string.Format(L["Task.TaskRunningStatusText"], argments),
TaskStatusEnum.Completed => L["Task.TaskCompletedStatusText"],
_ => throw new InvalidTaskStatusException(nameof(taskStatusEnum))
};
}
}

View File

@@ -0,0 +1,37 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Cowain.Base.ViewModels
{
public partial class UserRoleMenuViewModel : ObservableValidator
{
public int Id { get; set; }
/// <summary>
/// 角色id
/// </summary>
[ObservableProperty]
private int _roleId;
/// <summary>
/// 菜单key
/// </summary>
[ObservableProperty]
private string? _menuKey;
/// <summary>
/// 菜单名称
/// </summary>
[ObservableProperty]
private string? _menuName;
/// <summary>
/// 菜单命令
/// </summary>
[ObservableProperty]
private ObservableCollection<string>? _menuActions;
}
}

View File

@@ -0,0 +1,36 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Cowain.Base.Attributes;
using System.Collections.ObjectModel;
namespace Cowain.Base.ViewModels
{
public partial class UserRoleViewModel : ObservableValidator
{
/// <summary>
/// 角色id
/// </summary>
[ObservableProperty]
private int _roleId;
/// <summary>
/// 角色名称
/// </summary>
[ObservableProperty]
[NotifyDataErrorInfo]
[MinLength(2, "Errors.MinLength")]
private string? _roleName;
/// <summary>
/// 是否有效
/// </summary>
[ObservableProperty]
private bool _isValid;
/// <summary>
/// 菜单列表
/// </summary>
[ObservableProperty]
private ObservableCollection<UserRoleMenuViewModel>? _menus;
}
}

Some files were not shown because too many files have changed in this diff Show More