更新项目配置,新增设备同步模块,优化WebSocket和Swagger配置,增强SCADA系统的免登录接口,支持数据字典项和登录日志的免登录查询与记录。调整Java编译设置,确保更好的开发体验。
This commit is contained in:
BIN
yy-admin-master/YY.Admin/Admin.NET.db
Normal file
BIN
yy-admin-master/YY.Admin/Admin.NET.db
Normal file
Binary file not shown.
18
yy-admin-master/YY.Admin/App.config
Normal file
18
yy-admin-master/YY.Admin/App.config
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<configSections>
|
||||
<sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
|
||||
<section name="YY.Admin.Settings1" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
|
||||
<section name="YY.Admin.Properties.AppSettings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
<userSettings>
|
||||
<YY.Admin.Properties.AppSettings>
|
||||
<setting name="SkinType" serializeAs="String">
|
||||
<value>0</value>
|
||||
</setting>
|
||||
</YY.Admin.Properties.AppSettings>
|
||||
</userSettings>
|
||||
</configuration>
|
||||
27
yy-admin-master/YY.Admin/App.xaml
Normal file
27
yy-admin-master/YY.Admin/App.xaml
Normal file
@@ -0,0 +1,27 @@
|
||||
<prism:PrismApplication x:Class="YY.Admin.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:YY.Admin"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:core="clr-namespace:YY.Admin.Core;assembly=YY.Admin.Core"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes">
|
||||
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<!-- HandyControl资源 -->
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
|
||||
<!--<hc:Theme/>-->
|
||||
|
||||
<!-- 通用资源 -->
|
||||
<ResourceDictionary Source="/Resources/Styles/YY.xaml"/>
|
||||
<!-- 图标资源 -->
|
||||
<ResourceDictionary Source="/Resources/Styles/Icons.xaml"/>
|
||||
<!-- 自定义HandyControl资源 -->
|
||||
<ResourceDictionary Source="/Resources/Styles/HandyControl/Styles.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</prism:PrismApplication>
|
||||
109
yy-admin-master/YY.Admin/App.xaml.cs
Normal file
109
yy-admin-master/YY.Admin/App.xaml.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
|
||||
using FluentValidation;
|
||||
using Mapster;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NewLife;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.EventBus;
|
||||
using YY.Admin.Filter;
|
||||
using YY.Admin.Module;
|
||||
using YY.Admin.Properties;
|
||||
using YY.Admin.Setup;
|
||||
using YY.Admin.ViewModels;
|
||||
using YY.Admin.Views;
|
||||
namespace YY.Admin
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : PrismApplication
|
||||
{
|
||||
private IConfiguration? _configuration;
|
||||
private ILoggerService? _logger;
|
||||
private readonly SyncModule _syncModule = new();
|
||||
protected override Window CreateShell()
|
||||
{
|
||||
return Container.Resolve<LoginWindow>();
|
||||
}
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
// 构建配置
|
||||
_configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(baseDirectory)
|
||||
.AddJsonFile("Configuration/appsettings.json", optional: false, reloadOnChange: true)
|
||||
.Build();
|
||||
// 全局配置
|
||||
TypeAdapterConfig.GlobalSettings.Default
|
||||
.IgnoreNullValues(true)
|
||||
.NameMatchingStrategy(NameMatchingStrategy.IgnoreCase);
|
||||
|
||||
// FluentValidation 全局规则级别配置
|
||||
ValidatorOptions.Global.DefaultRuleLevelCascadeMode = CascadeMode.Stop;
|
||||
|
||||
// Mapster 全局配置
|
||||
#if DEBUG
|
||||
//TypeAdapterConfig.GlobalSettings.RequireExplicitMapping = true;
|
||||
#endif
|
||||
|
||||
base.OnStartup(e);
|
||||
}
|
||||
//注册
|
||||
protected override void RegisterTypes(IContainerRegistry containerRegistry)
|
||||
{
|
||||
//后续调整为配置依赖注入(Prism Module)
|
||||
// 注册错误处理
|
||||
containerRegistry.RegisterSingleton<IErrorHandler, ErrorHandler>();
|
||||
//全局错误处理
|
||||
containerRegistry.RegisterSingleton<GlobalExceptionHandler>();
|
||||
|
||||
// 注册事件聚合器(Prism自带)
|
||||
containerRegistry.RegisterSingleton<IEventAggregator, EventAggregator>();
|
||||
|
||||
//项目配置选项
|
||||
containerRegistry.AddProjectOptions(_configuration!);
|
||||
// 注册原始配置,供业务服务读取第三方对接参数
|
||||
containerRegistry.RegisterInstance(_configuration!);
|
||||
// 注册缓存服务
|
||||
containerRegistry.AddNewLifeCache(_configuration!);
|
||||
//注册数据库服务
|
||||
containerRegistry.AddDbContext(_configuration!);
|
||||
//注册通知事件服务
|
||||
containerRegistry.AddNotificationEventBus();
|
||||
// 服务注册
|
||||
containerRegistry.AddService(_configuration!);
|
||||
// 注册HttpClient连接池
|
||||
containerRegistry.AddHttpClient();
|
||||
|
||||
// 注册断联续传同步模块
|
||||
_syncModule.RegisterTypes(containerRegistry);
|
||||
|
||||
// 注册所有需要导航的视图
|
||||
containerRegistry.AddNavigation();
|
||||
}
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
// 获取日志服务
|
||||
_logger = Container.Resolve<ILoggerService>();
|
||||
// 初始化全局异常处理(通过解析触发构造函数注册)
|
||||
Container.Resolve<GlobalExceptionHandler>();
|
||||
// 保存默认主题
|
||||
AppSettings.Default.SkinType = AppSettingsViewModel.GetSkinType().ToInt();
|
||||
AppSettings.Default.Save();
|
||||
|
||||
_logger.Information("应用程序已启动");
|
||||
|
||||
// 启动断联续传同步模块
|
||||
_syncModule.OnInitialized(Container);
|
||||
}
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
BaseViewModel.StopTokenCheckTimer();
|
||||
base.OnExit(e);
|
||||
_logger?.Information("应用程序已退出");
|
||||
}
|
||||
}
|
||||
}
|
||||
10
yy-admin-master/YY.Admin/AssemblyInfo.cs
Normal file
10
yy-admin-master/YY.Admin/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
11
yy-admin-master/YY.Admin/Event/TabClosedEvent.cs
Normal file
11
yy-admin-master/YY.Admin/Event/TabClosedEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using YY.Admin.Module;
|
||||
|
||||
namespace YY.Admin.Event
|
||||
{
|
||||
/// <summary>
|
||||
/// Tab关闭事件
|
||||
/// </summary>
|
||||
public class TabClosedEvent : PubSubEvent<TabItemModel>
|
||||
{
|
||||
}
|
||||
}
|
||||
11
yy-admin-master/YY.Admin/Event/TabRefreshEvent.cs
Normal file
11
yy-admin-master/YY.Admin/Event/TabRefreshEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using YY.Admin.Module;
|
||||
|
||||
namespace YY.Admin.Event
|
||||
{
|
||||
/// <summary>
|
||||
/// Tab刷新事件
|
||||
/// </summary>
|
||||
public class TabRefreshEvent : PubSubEvent<TabItemModel>
|
||||
{
|
||||
}
|
||||
}
|
||||
11
yy-admin-master/YY.Admin/Event/TabSelectedEvent.cs
Normal file
11
yy-admin-master/YY.Admin/Event/TabSelectedEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using YY.Admin.Module;
|
||||
|
||||
namespace YY.Admin.Event
|
||||
{
|
||||
/// <summary>
|
||||
/// Tab选中事件
|
||||
/// </summary>
|
||||
public class TabSelectedEvent : PubSubEvent<TabItemModel>
|
||||
{
|
||||
}
|
||||
}
|
||||
12
yy-admin-master/YY.Admin/Event/TabSourceSelectedEvent.cs
Normal file
12
yy-admin-master/YY.Admin/Event/TabSourceSelectedEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
using YY.Admin.Module;
|
||||
|
||||
namespace YY.Admin.Event
|
||||
{
|
||||
/// <summary>
|
||||
/// Tab源选中事件
|
||||
/// </summary>
|
||||
public class TabSourceSelectedEvent : PubSubEvent<TabSource>
|
||||
{
|
||||
}
|
||||
}
|
||||
46
yy-admin-master/YY.Admin/Filter/GlobalExceptionHandler.cs
Normal file
46
yy-admin-master/YY.Admin/Filter/GlobalExceptionHandler.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Windows.Threading;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.EventBus;
|
||||
|
||||
namespace YY.Admin.Filter
|
||||
{
|
||||
public class GlobalExceptionHandler
|
||||
{
|
||||
private readonly IErrorHandler _errorHandler;
|
||||
private ILoggerService _logger;
|
||||
public GlobalExceptionHandler(IErrorHandler errorHandler,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_errorHandler = errorHandler;
|
||||
_logger= logger;
|
||||
|
||||
// 注册全局异常处理
|
||||
System.Windows.Application.Current.DispatcherUnhandledException += OnDispatcherUnhandledException;
|
||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException!;
|
||||
}
|
||||
|
||||
private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
_errorHandler.HandleError(e.Exception);
|
||||
_logger.Error("未处理的异常", e.Exception);
|
||||
e.Handled = true; // 标记为已处理
|
||||
}
|
||||
|
||||
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
if (e.ExceptionObject is Exception ex)
|
||||
{
|
||||
_errorHandler.HandleError(ex);
|
||||
_logger.Error("UI线程未处理异常", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
|
||||
{
|
||||
_errorHandler.HandleError(e.Exception);
|
||||
_logger.Error("未观察到的任务异常", e.Exception);
|
||||
e.SetObserved(); // 标记为已观察
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using FluentValidation;
|
||||
using YY.Admin.Services;
|
||||
|
||||
namespace YY.Admin.FluentValidation
|
||||
{
|
||||
public class LoginInputValidator : AbstractValidator<LoginInput>
|
||||
{
|
||||
public LoginInputValidator()
|
||||
{
|
||||
RuleFor(x => x.Username)
|
||||
.NotEmpty().WithMessage("用户名不能为空");
|
||||
|
||||
RuleFor(x => x.Password)
|
||||
.NotEmpty().WithMessage("密码不能为空");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using FluentValidation;
|
||||
using NewLife;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Services.Service.User;
|
||||
|
||||
namespace YY.Admin.FluentValidation
|
||||
{
|
||||
public class SysUserValidator : AbstractValidator<SysUser>
|
||||
{
|
||||
private readonly ISysUserService _userService;
|
||||
public SysUserValidator(ISysUserService userService)
|
||||
{
|
||||
_userService = userService;
|
||||
|
||||
RuleFor(x => x.Account)
|
||||
.NotEmpty().WithMessage("账号不能为空")
|
||||
.Length(4, 20).WithMessage("账号长度需在4~20个字符之间")
|
||||
.Matches(@"^[a-zA-Z0-9_]+$").WithMessage("账号仅能包含字母、数字和下划线")
|
||||
.MustAsync(async (user, account, cancellation) =>
|
||||
{
|
||||
var exists = await _userService.AccountExistsAsync(account, user.Id);
|
||||
return !exists;
|
||||
}).WithMessage("账号已存在")
|
||||
.When(x => x.Id == 0);
|
||||
|
||||
RuleFor(x => x.RealName)
|
||||
.NotEmpty().WithMessage("姓名不能为空")
|
||||
.Length(2, 10).WithMessage("姓名长度需在2~10个字符之间");
|
||||
|
||||
RuleFor(x => x.Password)
|
||||
.NotEmpty().WithMessage("密码不能为空")
|
||||
.MinimumLength(6).WithMessage("密码长度不能少于6个字符")
|
||||
.When(x => x.Id == 0);
|
||||
|
||||
RuleFor(x => x.NickName)
|
||||
.Length(2, 10).WithMessage("昵称长度需在2~10个字符之间")
|
||||
.When(x => !x.NickName.IsNullOrEmpty());
|
||||
|
||||
RuleFor(x => x.Sex)
|
||||
.NotNull().WithMessage("性别不能为空");
|
||||
|
||||
RuleFor(x => x.Age)
|
||||
.NotNull().WithMessage("年龄不能为空")
|
||||
.GreaterThanOrEqualTo(0).WithMessage("年龄不能小于0")
|
||||
.LessThanOrEqualTo(150).WithMessage("年龄不能大于150");
|
||||
|
||||
//RuleFor(x => x.Birthday)
|
||||
// .NotNull().WithMessage("出生日期不能为空");
|
||||
|
||||
//RuleFor(x => x.Phone)
|
||||
// .Matches(@"^1[3456789]\d{9}$").WithMessage("手机号码格式不正确!")
|
||||
// .When(x => !string.IsNullOrWhiteSpace(x.Phone));
|
||||
}
|
||||
}
|
||||
}
|
||||
117
yy-admin-master/YY.Admin/Helper/ServerSettingsStore.cs
Normal file
117
yy-admin-master/YY.Admin/Helper/ServerSettingsStore.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.IO;
|
||||
|
||||
namespace YY.Admin.Helper
|
||||
{
|
||||
/// <summary>
|
||||
/// 服务器连接配置读写工具。
|
||||
/// </summary>
|
||||
public static class ServerSettingsStore
|
||||
{
|
||||
private const string DefaultWebSocketPath = "/websocket/scada-sync";
|
||||
|
||||
public class ServerSettingsModel
|
||||
{
|
||||
public string Ip { get; set; } = "127.0.0.1";
|
||||
public int Port { get; set; } = 8080;
|
||||
public string BaseScheme { get; set; } = "http";
|
||||
public string BasePath { get; set; } = "/jeecg-boot";
|
||||
public string WebSocketUrl { get; set; } = string.Empty;
|
||||
public string WebSocketPath { get; set; } = DefaultWebSocketPath;
|
||||
}
|
||||
|
||||
public static string GetConfigPath()
|
||||
{
|
||||
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configuration", "appsettings.json");
|
||||
}
|
||||
|
||||
public static ServerSettingsModel Load()
|
||||
{
|
||||
var model = new ServerSettingsModel();
|
||||
var path = GetConfigPath();
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return model;
|
||||
}
|
||||
|
||||
var content = File.ReadAllText(path);
|
||||
var root = JObject.Parse(content);
|
||||
var jeecg = root["JeecgIntegration"] as JObject;
|
||||
if (jeecg == null)
|
||||
{
|
||||
return model;
|
||||
}
|
||||
|
||||
var baseUrl = jeecg.Value<string>("BaseUrl") ?? string.Empty;
|
||||
if (Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri))
|
||||
{
|
||||
model.BaseScheme = uri.Scheme;
|
||||
model.Ip = uri.Host;
|
||||
model.Port = uri.Port;
|
||||
model.BasePath = string.IsNullOrWhiteSpace(uri.AbsolutePath) ? string.Empty : uri.AbsolutePath.TrimEnd('/');
|
||||
}
|
||||
|
||||
model.WebSocketUrl = jeecg.Value<string>("WebSocketUrl") ?? string.Empty;
|
||||
model.WebSocketPath = NormalizeWebSocketPath(jeecg.Value<string>("WebSocketPath"));
|
||||
return model;
|
||||
}
|
||||
|
||||
public static void Save(ServerSettingsModel model)
|
||||
{
|
||||
var path = GetConfigPath();
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException("未找到配置文件 appsettings.json", path);
|
||||
}
|
||||
|
||||
var content = File.ReadAllText(path);
|
||||
var root = JObject.Parse(content);
|
||||
var jeecg = root["JeecgIntegration"] as JObject;
|
||||
if (jeecg == null)
|
||||
{
|
||||
jeecg = new JObject();
|
||||
root["JeecgIntegration"] = jeecg;
|
||||
}
|
||||
|
||||
var basePath = string.IsNullOrWhiteSpace(model.BasePath) ? string.Empty : model.BasePath.TrimEnd('/');
|
||||
if (!string.IsNullOrWhiteSpace(basePath) && !basePath.StartsWith('/'))
|
||||
{
|
||||
basePath = "/" + basePath;
|
||||
}
|
||||
|
||||
var baseUrl = $"{model.BaseScheme}://{model.Ip}:{model.Port}{basePath}";
|
||||
var webSocketPath = NormalizeWebSocketPath(model.WebSocketPath);
|
||||
var webSocketUrl = string.IsNullOrWhiteSpace(model.WebSocketUrl)
|
||||
? BuildDefaultWebSocketUrl(model.BaseScheme, model.Ip, model.Port, basePath, webSocketPath)
|
||||
: model.WebSocketUrl.Trim();
|
||||
jeecg["BaseUrl"] = baseUrl;
|
||||
jeecg["WebSocketUrl"] = webSocketUrl;
|
||||
jeecg["WebSocketPath"] = webSocketPath;
|
||||
|
||||
File.WriteAllText(path, root.ToString(Formatting.Indented));
|
||||
}
|
||||
|
||||
public static string BuildDefaultWebSocketUrl(string baseScheme, string ip, int port, string basePath, string webSocketPath = DefaultWebSocketPath)
|
||||
{
|
||||
var safeScheme = string.Equals(baseScheme, "https", StringComparison.OrdinalIgnoreCase) ? "wss" : "ws";
|
||||
var safeBasePath = string.IsNullOrWhiteSpace(basePath) ? string.Empty : basePath.TrimEnd('/');
|
||||
if (!string.IsNullOrWhiteSpace(safeBasePath) && !safeBasePath.StartsWith('/'))
|
||||
{
|
||||
safeBasePath = "/" + safeBasePath;
|
||||
}
|
||||
var safeWsPath = NormalizeWebSocketPath(webSocketPath);
|
||||
return $"{safeScheme}://{ip}:{port}{safeBasePath}{safeWsPath}";
|
||||
}
|
||||
|
||||
private static string NormalizeWebSocketPath(string? webSocketPath)
|
||||
{
|
||||
var value = string.IsNullOrWhiteSpace(webSocketPath) ? DefaultWebSocketPath : webSocketPath.Trim();
|
||||
if (!value.StartsWith('/'))
|
||||
{
|
||||
value = "/" + value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Prism.Events;
|
||||
using System.IO;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Infrastructure.Storage;
|
||||
|
||||
namespace YY.Admin.Infrastructure.Hubs;
|
||||
|
||||
public class StompWebSocketService : ISignalRService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly TokenStore _tokenStore;
|
||||
private ClientWebSocket? _socket;
|
||||
private string _deviceId = "default-device";
|
||||
private string _token = string.Empty;
|
||||
|
||||
public StompWebSocketService(
|
||||
IConfiguration configuration,
|
||||
IEventAggregator eventAggregator,
|
||||
TokenStore tokenStore)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_eventAggregator = eventAggregator;
|
||||
_tokenStore = tokenStore;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ConnectAsync(string token, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_token = token ?? string.Empty;
|
||||
await ConnectUnifiedDeviceChannelAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ConnectUnifiedDeviceChannelAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var anonymous = _configuration.GetValue("JeecgIntegration:AnonymousMode", true);
|
||||
if (anonymous)
|
||||
{
|
||||
_token = string.Empty;
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(_token))
|
||||
{
|
||||
_token = await _tokenStore.GetTokenAsync(cancellationToken).ConfigureAwait(false) ?? string.Empty;
|
||||
}
|
||||
|
||||
_deviceId = ResolveDeviceId(_token);
|
||||
var wsUrl = ResolveWsUrl();
|
||||
var retryDelays = new[] { 0, 2, 5, 10, 30 };
|
||||
|
||||
foreach (var delay in retryDelays)
|
||||
{
|
||||
if (delay > 0)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_socket?.Dispose();
|
||||
_socket = new ClientWebSocket();
|
||||
_socket.Options.AddSubProtocol("v12.stomp");
|
||||
await _socket.ConnectAsync(new Uri(wsUrl), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var connectFrame = anonymous || string.IsNullOrWhiteSpace(_token)
|
||||
? "CONNECT\naccept-version:1.2\nheart-beat:10000,10000\n\n\0"
|
||||
: BuildConnectFrame(_token);
|
||||
await SendFrameAsync(connectFrame, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 用户镜像变更:与后端 /topic/sync/jeecg-users 对齐(设备同步统一线路)
|
||||
await SendFrameAsync(BuildSubscribeFrame("sub-jeecg-users", "/topic/sync/jeecg-users"), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 非免密时同时订阅设备点对点指令队列
|
||||
if (!anonymous && !string.IsNullOrWhiteSpace(_token))
|
||||
{
|
||||
await SendFrameAsync(BuildSubscribeFrame("sub-device-command", $"/user/{_deviceId}/queue/command"), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_eventAggregator.GetEvent<NetworkStatusChangedEvent>().Publish(new NetworkStatusChangedPayload
|
||||
{
|
||||
IsOnline = true,
|
||||
ChangedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
_ = Task.Run(() => ReceiveLoopAsync(cancellationToken), cancellationToken);
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_eventAggregator.GetEvent<NetworkStatusChangedEvent>().Publish(new NetworkStatusChangedPayload
|
||||
{
|
||||
IsOnline = false,
|
||||
ChangedAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SendDeviceStatusAsync(object status, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_socket == null || _socket.State != WebSocketState.Open)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(status);
|
||||
var frame = $"SEND\n" +
|
||||
$"destination:/app/device/status\n" +
|
||||
$"content-type:application/json\n" +
|
||||
$"content-length:{Encoding.UTF8.GetByteCount(json)}\n\n" +
|
||||
$"{json}\0";
|
||||
await SendFrameAsync(frame, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ReceiveLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_socket == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var buffer = new byte[8192];
|
||||
while (_socket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
WebSocketReceiveResult result;
|
||||
do
|
||||
{
|
||||
result = await _socket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken).ConfigureAwait(false);
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
_eventAggregator.GetEvent<NetworkStatusChangedEvent>().Publish(new NetworkStatusChangedPayload
|
||||
{
|
||||
IsOnline = false,
|
||||
ChangedAt = DateTime.UtcNow
|
||||
});
|
||||
await ConnectUnifiedDeviceChannelAsync(cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
ms.Write(buffer, 0, result.Count);
|
||||
} while (!result.EndOfMessage);
|
||||
|
||||
var text = Encoding.UTF8.GetString(ms.ToArray());
|
||||
if (!text.StartsWith("MESSAGE", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var idx = text.IndexOf("\n\n", StringComparison.Ordinal);
|
||||
if (idx < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var body = text[(idx + 2)..].TrimEnd('\0');
|
||||
_eventAggregator.GetEvent<RemoteCommandReceivedEvent>().Publish(new RemoteCommandPayload
|
||||
{
|
||||
DeviceId = _deviceId,
|
||||
CommandJson = body
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendFrameAsync(string frame, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_socket == null || _socket.State != WebSocketState.Open)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var data = Encoding.UTF8.GetBytes(frame);
|
||||
await _socket.SendAsync(new ArraySegment<byte>(data), WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private string ResolveWsUrl()
|
||||
{
|
||||
var baseUrl = _configuration.GetValue<string>("JeecgIntegration:BaseUrl")?.TrimEnd('/');
|
||||
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||
{
|
||||
return "ws://127.0.0.1:8080/jeecg-boot/ws/device/websocket";
|
||||
}
|
||||
if (baseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "wss://" + baseUrl["https://".Length..] + "/ws/device/websocket";
|
||||
}
|
||||
return "ws://" + baseUrl["http://".Length..] + "/ws/device/websocket";
|
||||
}
|
||||
|
||||
private static string ResolveDeviceId(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parts = token.Split('.');
|
||||
if (parts.Length < 2)
|
||||
{
|
||||
return "default-device";
|
||||
}
|
||||
var payload = parts[1].Replace('-', '+').Replace('_', '/');
|
||||
payload = payload.PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');
|
||||
var json = Encoding.UTF8.GetString(Convert.FromBase64String(payload));
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(json);
|
||||
if (doc.RootElement.TryGetProperty("deviceId", out var deviceId))
|
||||
{
|
||||
return deviceId.GetString() ?? "default-device";
|
||||
}
|
||||
if (doc.RootElement.TryGetProperty("username", out var username))
|
||||
{
|
||||
return username.GetString() ?? "default-device";
|
||||
}
|
||||
return "default-device";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "default-device";
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildConnectFrame(string token)
|
||||
{
|
||||
return "CONNECT\n" +
|
||||
"accept-version:1.2\n" +
|
||||
"heart-beat:10000,10000\n" +
|
||||
$"Authorization:Bearer {token}\n\n\0";
|
||||
}
|
||||
|
||||
private static string BuildSubscribeFrame(string subscriptionId, string destination)
|
||||
{
|
||||
return "SUBSCRIBE\n" +
|
||||
$"id:{subscriptionId}\n" +
|
||||
$"destination:{destination}\n" +
|
||||
"ack:auto\n\n\0";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Prism.Events;
|
||||
using System.Net.Http;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Services;
|
||||
|
||||
namespace YY.Admin.Infrastructure.Network;
|
||||
|
||||
public class NetworkMonitor : INetworkMonitor
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly SemaphoreSlim _startLock = new(1, 1);
|
||||
private Task? _loopTask;
|
||||
private CancellationTokenSource? _cts;
|
||||
private volatile bool _isOnline;
|
||||
|
||||
public NetworkMonitor(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IConfiguration configuration,
|
||||
IEventAggregator eventAggregator)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_configuration = configuration;
|
||||
_eventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
public bool IsOnline => _isOnline;
|
||||
|
||||
public event Action<bool>? StatusChanged;
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _startLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (_cts != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_loopTask = Task.Run(() => MonitorLoopAsync(_cts.Token), _cts.Token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_startLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MonitorLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));
|
||||
while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var online = await ProbeAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (online == _isOnline)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_isOnline = online;
|
||||
StatusChanged?.Invoke(online);
|
||||
_eventAggregator.GetEvent<NetworkStatusChangedEvent>().Publish(new NetworkStatusChangedPayload
|
||||
{
|
||||
IsOnline = online,
|
||||
ChangedAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ProbeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var baseUrl = _configuration.GetValue<string>("JeecgIntegration:BaseUrl")?.TrimEnd('/');
|
||||
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 探活策略:优先调用免登录接口,失败后再降级到健康检查接口
|
||||
var probeUrls = new[]
|
||||
{
|
||||
$"{baseUrl}/sys/user/scada/queryUser?current=1&pageSize=1",
|
||||
$"{baseUrl}/sys/dict/scada/queryDictItem?pageNo=1&pageSize=1",
|
||||
$"{baseUrl}/actuator/health"
|
||||
};
|
||||
|
||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(3));
|
||||
var client = _httpClientFactory.CreateClient("JeecgApi");
|
||||
foreach (var url in probeUrls)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
using var resp = await client.SendAsync(req, timeoutCts.Token).ConfigureAwait(false);
|
||||
if (resp.IsSuccessStatusCode)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 当前探活地址失败后继续尝试下一个降级地址
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using SqlSugar;
|
||||
|
||||
namespace YY.Admin.Infrastructure.Storage;
|
||||
|
||||
public class TokenStore
|
||||
{
|
||||
private const string TokenKey = "DeviceToken";
|
||||
private readonly ISqlSugarClient _db;
|
||||
|
||||
public TokenStore(ISqlSugarClient db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task UpdateTokenAsync(string token, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var value = token ?? string.Empty;
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
var sql = "INSERT INTO app_config(config_key,config_value,updated_at) VALUES(@key,@val,@ts) " +
|
||||
"ON CONFLICT(config_key) DO UPDATE SET config_value=@val, updated_at=@ts;";
|
||||
var now = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
await _db.Ado.ExecuteCommandAsync(sql, new[]
|
||||
{
|
||||
new SugarParameter("@key", TokenKey),
|
||||
new SugarParameter("@val", value),
|
||||
new SugarParameter("@ts", now)
|
||||
}).ConfigureAwait(false);
|
||||
_ = cancellationToken;
|
||||
}
|
||||
|
||||
public async Task<string> GetTokenAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
var sql = "SELECT config_value FROM app_config WHERE config_key=@key LIMIT 1;";
|
||||
var token = await _db.Ado.GetStringAsync(sql, new[] { new SugarParameter("@key", TokenKey) }).ConfigureAwait(false);
|
||||
return token ?? string.Empty;
|
||||
}
|
||||
|
||||
private async Task EnsureTableAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = "CREATE TABLE IF NOT EXISTS app_config(" +
|
||||
"config_key TEXT PRIMARY KEY," +
|
||||
"config_value TEXT NULL," +
|
||||
"updated_at TEXT NULL);";
|
||||
await _db.Ado.ExecuteCommandAsync(sql).ConfigureAwait(false);
|
||||
_ = cancellationToken;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Net.Http;
|
||||
using YY.Admin.Core.Models;
|
||||
using YY.Admin.Infrastructure.Storage;
|
||||
|
||||
namespace YY.Admin.Infrastructure.Sync;
|
||||
|
||||
public class HttpSyncClient
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly TokenStore _tokenStore;
|
||||
|
||||
private sealed class SyncBatchItem
|
||||
{
|
||||
public string MessageId { get; set; } = string.Empty;
|
||||
public string AggregateType { get; set; } = string.Empty;
|
||||
public string AggregateId { get; set; } = string.Empty;
|
||||
public string EventType { get; set; } = string.Empty;
|
||||
public string Payload { get; set; } = string.Empty;
|
||||
public DateTime OccurredAt { get; set; }
|
||||
}
|
||||
|
||||
public HttpSyncClient(IHttpClientFactory httpClientFactory, TokenStore tokenStore)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_tokenStore = tokenStore;
|
||||
}
|
||||
|
||||
public async Task<bool> SendBatchAsync(IReadOnlyCollection<OutboxMessage> messages, CancellationToken cancellationToken)
|
||||
{
|
||||
if (messages.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var client = _httpClientFactory.CreateClient("JeecgApi");
|
||||
var body = messages.Select(m => new SyncBatchItem
|
||||
{
|
||||
MessageId = m.Id,
|
||||
AggregateType = m.AggregateType,
|
||||
AggregateId = m.AggregateId,
|
||||
EventType = m.EventType,
|
||||
Payload = m.Payload,
|
||||
OccurredAt = m.CreatedAt
|
||||
}).ToList();
|
||||
|
||||
using var response = await client.PostAsJsonAsync("/sys/sync/batch", body, cancellationToken).ConfigureAwait(false);
|
||||
if (response.Headers.TryGetValues("X-Refresh-Token", out var values))
|
||||
{
|
||||
var refreshed = values.FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(refreshed))
|
||||
{
|
||||
await _tokenStore.UpdateTokenAsync(refreshed, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Core.Sync;
|
||||
|
||||
namespace YY.Admin.Infrastructure.Sync;
|
||||
|
||||
/// <summary>
|
||||
/// 用户镜像拉取入队:统一走 Outbox(断网续传),与设备同步模块同一条基础设施线路。
|
||||
/// </summary>
|
||||
public sealed class JeecgUserMirrorPullOutbox : IJeecgUserMirrorPullOutbox
|
||||
{
|
||||
private readonly OutboxProcessor _outboxProcessor;
|
||||
|
||||
public JeecgUserMirrorPullOutbox(OutboxProcessor outboxProcessor)
|
||||
{
|
||||
_outboxProcessor = outboxProcessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task EnqueuePullAsync(string eventType, string? payloadJson, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var payload = string.IsNullOrWhiteSpace(payloadJson) ? "{}" : payloadJson;
|
||||
return _outboxProcessor.EnqueueAsync(
|
||||
JeecgUserMirrorOutbox.AggregateType,
|
||||
"mirror",
|
||||
eventType,
|
||||
new { source = "unified-device-channel", detail = payload },
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
232
yy-admin-master/YY.Admin/Infrastructure/Sync/OutboxProcessor.cs
Normal file
232
yy-admin-master/YY.Admin/Infrastructure/Sync/OutboxProcessor.cs
Normal file
@@ -0,0 +1,232 @@
|
||||
using SqlSugar;
|
||||
using System.Threading.Channels;
|
||||
using Prism.Events;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Models;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Core.Sync;
|
||||
|
||||
namespace YY.Admin.Infrastructure.Sync;
|
||||
|
||||
public class OutboxProcessor
|
||||
{
|
||||
private readonly INetworkMonitor _networkMonitor;
|
||||
private readonly HttpSyncClient _httpSyncClient;
|
||||
private readonly IJeecgUserMirrorPullHandler _mirrorPullHandler;
|
||||
private readonly ISqlSugarClient _db;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly Channel<OutboxMessage> _channel = Channel.CreateBounded<OutboxMessage>(new BoundedChannelOptions(1000)
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
FullMode = BoundedChannelFullMode.Wait
|
||||
});
|
||||
private readonly SemaphoreSlim _flushLock = new(1, 1);
|
||||
|
||||
public OutboxProcessor(
|
||||
INetworkMonitor networkMonitor,
|
||||
HttpSyncClient httpSyncClient,
|
||||
IJeecgUserMirrorPullHandler mirrorPullHandler,
|
||||
ISqlSugarClient db,
|
||||
IEventAggregator eventAggregator)
|
||||
{
|
||||
_networkMonitor = networkMonitor;
|
||||
_httpSyncClient = httpSyncClient;
|
||||
_mirrorPullHandler = mirrorPullHandler;
|
||||
_db = db.AsTenant().GetConnectionScope("Slave");
|
||||
_eventAggregator = eventAggregator;
|
||||
_networkMonitor.StatusChanged += OnNetworkStatusChanged;
|
||||
}
|
||||
|
||||
private static bool IsJeecgUserMirrorMessage(OutboxMessage m) =>
|
||||
string.Equals(m.AggregateType, JeecgUserMirrorOutbox.AggregateType, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public async Task EnqueueAsync<T>(
|
||||
string aggregateType,
|
||||
string aggregateId,
|
||||
string eventType,
|
||||
T payload,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var message = new OutboxMessage
|
||||
{
|
||||
AggregateType = aggregateType,
|
||||
AggregateId = aggregateId,
|
||||
EventType = eventType,
|
||||
Payload = System.Text.Json.JsonSerializer.Serialize(payload),
|
||||
Status = 0,
|
||||
RetryCount = 0,
|
||||
CreatedAt = now
|
||||
};
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
await _db.Insertable(message).ExecuteCommandAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
await _channel.Writer.WriteAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartConsumerAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
_ = Task.Run(() => ConsumeLoopAsync(cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
public async Task FlushPendingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await _flushLock.WaitAsync(0, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
var pending = await _db.Queryable<OutboxMessage>()
|
||||
.Where(x => x.Status == 0 && x.RetryCount < 5)
|
||||
.OrderBy(x => x.CreatedAt)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (pending.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mirror = pending.Where(IsJeecgUserMirrorMessage).ToList();
|
||||
var serverBatch = pending.Where(m => !IsJeecgUserMirrorMessage(m)).ToList();
|
||||
|
||||
foreach (var item in mirror)
|
||||
{
|
||||
var ok = await _mirrorPullHandler.ExecutePullAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (ok)
|
||||
{
|
||||
await MarkSentAsync(item, cancellationToken).ConfigureAwait(false);
|
||||
_eventAggregator.GetEvent<SyncCompletedEvent>().Publish(item.AggregateId);
|
||||
}
|
||||
else
|
||||
{
|
||||
await MarkFailedAsync(item, "Jeecg用户镜像拉取失败", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (serverBatch.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var success = await _httpSyncClient.SendBatchAsync(serverBatch, cancellationToken).ConfigureAwait(false);
|
||||
if (success)
|
||||
{
|
||||
var ids = serverBatch.Select(x => x.Id).ToArray();
|
||||
await _db.Updateable<OutboxMessage>()
|
||||
.SetColumns(x => new OutboxMessage
|
||||
{
|
||||
Status = 1,
|
||||
SentAt = DateTime.UtcNow,
|
||||
LastTriedAt = DateTime.UtcNow,
|
||||
ErrorMessage = null
|
||||
})
|
||||
.Where(x => ids.Contains(x.Id))
|
||||
.ExecuteCommandAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
foreach (var item in serverBatch)
|
||||
{
|
||||
_eventAggregator.GetEvent<SyncCompletedEvent>().Publish(item.AggregateId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in serverBatch)
|
||||
{
|
||||
await MarkFailedAsync(item, "批量同步失败", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_flushLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConsumeLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (await _channel.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
while (_channel.Reader.TryRead(out var message))
|
||||
{
|
||||
var success = IsJeecgUserMirrorMessage(message)
|
||||
? await _mirrorPullHandler.ExecutePullAsync(cancellationToken).ConfigureAwait(false)
|
||||
: await _httpSyncClient.SendBatchAsync(new[] { message }, cancellationToken).ConfigureAwait(false);
|
||||
if (success)
|
||||
{
|
||||
await MarkSentAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
_eventAggregator.GetEvent<SyncCompletedEvent>().Publish(message.AggregateId);
|
||||
}
|
||||
else
|
||||
{
|
||||
await MarkFailedAsync(message, "实时同步失败", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MarkSentAsync(OutboxMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
await _db.Updateable<OutboxMessage>()
|
||||
.SetColumns(x => new OutboxMessage
|
||||
{
|
||||
Status = 1,
|
||||
SentAt = DateTime.UtcNow,
|
||||
LastTriedAt = DateTime.UtcNow,
|
||||
ErrorMessage = null
|
||||
})
|
||||
.Where(x => x.Id == message.Id)
|
||||
.ExecuteCommandAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task MarkFailedAsync(OutboxMessage message, string error, CancellationToken cancellationToken)
|
||||
{
|
||||
var nextRetry = message.RetryCount + 1;
|
||||
var backoff = (int)Math.Pow(2, Math.Min(nextRetry, 5));
|
||||
await _db.Updateable<OutboxMessage>()
|
||||
.SetColumns(x => new OutboxMessage
|
||||
{
|
||||
RetryCount = nextRetry,
|
||||
Status = nextRetry >= 5 ? 2 : 0,
|
||||
ErrorMessage = error,
|
||||
LastTriedAt = DateTime.UtcNow
|
||||
})
|
||||
.Where(x => x.Id == message.Id)
|
||||
.ExecuteCommandAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (nextRetry < 5)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(backoff), cancellationToken).ConfigureAwait(false);
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
var retryMessage = await _db.Queryable<OutboxMessage>()
|
||||
.FirstAsync(x => x.Id == message.Id, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (retryMessage != null && retryMessage.Status == 0)
|
||||
{
|
||||
await _channel.Writer.WriteAsync(retryMessage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnsureTableAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_ = cancellationToken;
|
||||
await Task.Run(() => _db.CodeFirst.InitTables<OutboxMessage>(), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnNetworkStatusChanged(bool isOnline)
|
||||
{
|
||||
if (!isOnline)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_ = FlushPendingAsync(default);
|
||||
}
|
||||
}
|
||||
78
yy-admin-master/YY.Admin/Module/CacheExtensions.cs
Normal file
78
yy-admin-master/YY.Admin/Module/CacheExtensions.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Identity.Client;
|
||||
using NewLife.Caching.Services;
|
||||
using NewLife.Caching;
|
||||
using NewLife.Log;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Option;
|
||||
using NewLife.Configuration;
|
||||
|
||||
public static class CacheExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册缓存服务
|
||||
/// </summary>
|
||||
public static void AddNewLifeCache(this IContainerRegistry containerRegistry,IConfiguration configuration)
|
||||
{
|
||||
// 读取配置
|
||||
var BaseCacheOptions = configuration.GetSection("Cache").Get<BaseCacheOptions>();
|
||||
|
||||
// 注册配置选项
|
||||
containerRegistry.RegisterInstance(BaseCacheOptions);
|
||||
|
||||
// 注册缓存提供器
|
||||
if (BaseCacheOptions.CacheType == CacheTypeEnum.Redis.ToString())
|
||||
{
|
||||
containerRegistry.RegisterSingleton<ICacheProvider>(() =>
|
||||
{
|
||||
return CreateRedisCacheProvider(BaseCacheOptions);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// 默认使用内存缓存
|
||||
containerRegistry.RegisterSingleton<ICacheProvider, CacheProvider>();
|
||||
}
|
||||
|
||||
// 注册缓存服务
|
||||
containerRegistry.RegisterSingleton<ISysCacheService, SysCacheService>();
|
||||
}
|
||||
private static ICacheProvider CreateRedisCacheProvider(BaseCacheOptions options)
|
||||
{
|
||||
var redis = new FullRedis
|
||||
{
|
||||
Name = "RedisCache",
|
||||
Tracer = null, // 禁用跟踪器
|
||||
Log = XTrace.Log // 使用NewLife日志
|
||||
};
|
||||
|
||||
// 初始化Redis
|
||||
redis.Init(options.Redis.Configuration!);
|
||||
|
||||
// 设置前缀
|
||||
if (!string.IsNullOrEmpty(options.Redis.Prefix))
|
||||
{
|
||||
redis.Prefix = options.Redis.Prefix;
|
||||
}
|
||||
|
||||
// 设置最大消息大小
|
||||
if (options.Redis.MaxMessageSize > 0)
|
||||
{
|
||||
redis.MaxMessageSize = options.Redis.MaxMessageSize;
|
||||
}
|
||||
|
||||
//// 测试连接
|
||||
//if (!redis.Ping())
|
||||
//{
|
||||
// throw new Exception("Redis连接失败,请检查配置");
|
||||
//}
|
||||
|
||||
return new RedisCacheProvider { Cache = redis };
|
||||
}
|
||||
}
|
||||
27
yy-admin-master/YY.Admin/Module/DbExtensions.cs
Normal file
27
yy-admin-master/YY.Admin/Module/DbExtensions.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Identity.Client;
|
||||
using NewLife.Caching.Services;
|
||||
using NewLife.Caching;
|
||||
using NewLife.Log;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Option;
|
||||
using NewLife.Configuration;
|
||||
using YY.Admin.Core.SqlSugar;
|
||||
|
||||
public static class DbExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册缓存服务
|
||||
/// </summary>
|
||||
public static void AddDbContext(this IContainerRegistry containerRegistry,IConfiguration configuration)
|
||||
{
|
||||
containerRegistry.AddSqlSugar(configuration);
|
||||
}
|
||||
|
||||
}
|
||||
41
yy-admin-master/YY.Admin/Module/HttpClientExtensions.cs
Normal file
41
yy-admin-master/YY.Admin/Module/HttpClientExtensions.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Net.Http;
|
||||
|
||||
namespace YY.Admin.Module
|
||||
{
|
||||
public static class HttpClientExtensions
|
||||
{
|
||||
|
||||
public static void AddHttpClient(this IContainerRegistry containerRegistry)
|
||||
{
|
||||
containerRegistry.RegisterSingleton<HttpClient>(() =>
|
||||
{
|
||||
var handler = new SocketsHttpHandler
|
||||
{
|
||||
// 连接池大小
|
||||
MaxConnectionsPerServer = 50,
|
||||
|
||||
// 最大存活时间(绝对时间)
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
|
||||
|
||||
// 空闲连接超时时间,超时后自动回收
|
||||
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
|
||||
|
||||
// 是否自动重试
|
||||
AutomaticDecompression = System.Net.DecompressionMethods.GZip |
|
||||
System.Net.DecompressionMethods.Deflate
|
||||
};
|
||||
|
||||
var client = new HttpClient(handler)
|
||||
{
|
||||
// 默认超时时间 30秒
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("YY.Admin");
|
||||
|
||||
return client;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
29
yy-admin-master/YY.Admin/Module/NavItem.cs
Normal file
29
yy-admin-master/YY.Admin/Module/NavItem.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using YY.Admin.Core;
|
||||
|
||||
namespace YY.Admin.Module
|
||||
{
|
||||
/// <summary>
|
||||
/// SideBar选项
|
||||
/// </summary>
|
||||
public class NavItem : TabSource
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否放到底部
|
||||
/// </summary>
|
||||
public bool AlignBottom { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 是否支持高亮选中
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 图标类型
|
||||
/// </summary>
|
||||
public override IconTypeEnum IconType { get => base.IconType; set => base.IconType = value; }
|
||||
public NavItem()
|
||||
{
|
||||
IconType = IconTypeEnum.MaterialDesign;
|
||||
}
|
||||
}
|
||||
}
|
||||
82
yy-admin-master/YY.Admin/Module/NavigationExtensions.cs
Normal file
82
yy-admin-master/YY.Admin/Module/NavigationExtensions.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using YY.Admin.ViewModels.Control;
|
||||
using YY.Admin.ViewModels.Dialogs;
|
||||
using YY.Admin.Views;
|
||||
using YY.Admin.Views.Control;
|
||||
using YY.Admin.Views.Dialogs;
|
||||
using YY.Admin.Views.SysManage;
|
||||
|
||||
namespace YY.Admin
|
||||
{
|
||||
public static class NavigationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册导航
|
||||
/// </summary>
|
||||
public static void AddNavigation(this IContainerRegistry containerRegistry)
|
||||
{
|
||||
// 注册对话框
|
||||
containerRegistry.RegisterDialog<AlertDialogView, AlertDialogViewModel>("AlertDialog");
|
||||
containerRegistry.RegisterDialog<SuccessDialogView, SuccessDialogViewModel>("SuccessDialog");
|
||||
containerRegistry.RegisterDialog<ErrorDialogView, ErrorDialogViewModel>("ErrorDialog");
|
||||
containerRegistry.RegisterDialog<WarningDialogView, WarningDialogViewModel>("WarningDialog");
|
||||
containerRegistry.RegisterDialog<ConfirmDialogView, ConfirmDialogViewModel>("ConfirmDialog");
|
||||
containerRegistry.RegisterDialog<ServerSettingsDialogView, ServerSettingsDialogViewModel>("ServerSettingsDialog");
|
||||
|
||||
// 设置对话框样式
|
||||
containerRegistry.RegisterDialogWindow<DialogWindow>();
|
||||
|
||||
// 注册导航
|
||||
containerRegistry.RegisterForNavigation<DashboardView>("DashboardView");
|
||||
// 404视图
|
||||
containerRegistry.RegisterForNavigation<NotFoundView>("NotFoundView");
|
||||
//containerRegistry.RegisterForNavigation<RoleManagementView>("RoleManagementView");
|
||||
//containerRegistry.RegisterForNavigation<PermissionManagementView>("PermissionManagementView");
|
||||
//containerRegistry.RegisterForNavigation<OrderManagementView>("OrderManagementView");
|
||||
//containerRegistry.RegisterForNavigation<ProductManagementView>("ProductManagementView");
|
||||
//containerRegistry.RegisterForNavigation<ReportView>("ReportView");
|
||||
//containerRegistry.RegisterForNavigation<MonitorView>("MonitorView");
|
||||
|
||||
// 窗口注册
|
||||
containerRegistry.Register<LoginWindow>();
|
||||
containerRegistry.Register<MainWindow>();
|
||||
|
||||
// 注册视图(页面)
|
||||
containerRegistry.RegisterForNavigation<MenuTreeView>();
|
||||
containerRegistry.RegisterForNavigation<UserManagementView>();
|
||||
containerRegistry.RegisterForNavigation<DataDictionaryManagementView>();
|
||||
containerRegistry.RegisterForNavigation<RoleManagementView>();
|
||||
containerRegistry.RegisterForNavigation<TenantManagementView>();
|
||||
|
||||
}
|
||||
}
|
||||
public class DialogWindow : Window, IDialogWindow
|
||||
{
|
||||
public DialogWindow()
|
||||
{
|
||||
WindowStyle = WindowStyle.None;
|
||||
AllowsTransparency = true;
|
||||
Background = Brushes.Transparent; // 背景透明
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
SizeToContent = SizeToContent.WidthAndHeight;
|
||||
ResizeMode = ResizeMode.NoResize;
|
||||
}
|
||||
public IDialogResult? Result { get; set; }
|
||||
}
|
||||
//public class DialogWindow : Window, IDialogWindow
|
||||
//{
|
||||
// public DialogWindow()
|
||||
// {
|
||||
// //InitializeComponent();
|
||||
|
||||
// // 去掉最大化最小化
|
||||
// ResizeMode = ResizeMode.NoResize;
|
||||
|
||||
// // 去掉右上角系统按钮
|
||||
// WindowStyle = WindowStyle.None;
|
||||
// }
|
||||
// public IDialogResult Result { get; set; }
|
||||
//}
|
||||
}
|
||||
24
yy-admin-master/YY.Admin/Module/NotificationExtensions.cs
Normal file
24
yy-admin-master/YY.Admin/Module/NotificationExtensions.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using YY.Admin.EventBus;
|
||||
using static YY.Admin.Core.SysUserEvents;
|
||||
using System.Reflection;
|
||||
using YY.Admin.Core;
|
||||
using Prism.Ioc;
|
||||
|
||||
namespace YY.Admin.Module
|
||||
{
|
||||
public static class NotificationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册通知事件
|
||||
/// </summary>
|
||||
public static void AddNotificationEventBus(this IContainerRegistry containerRegistry)
|
||||
{
|
||||
// 注册EventAggregator为单例
|
||||
containerRegistry.RegisterSingleton<IEventAggregator, EventAggregator>();
|
||||
}
|
||||
}
|
||||
}
|
||||
31
yy-admin-master/YY.Admin/Module/ProjectOptions.cs
Normal file
31
yy-admin-master/YY.Admin/Module/ProjectOptions.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using YY.Admin.Core.Option;
|
||||
using YY.Admin.Core.SqlSugar;
|
||||
|
||||
namespace YY.Admin.Setup
|
||||
{
|
||||
public static class ProjectOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册项目配置选项到Prism容器
|
||||
/// </summary>
|
||||
public static IContainerRegistry AddProjectOptions(
|
||||
this IContainerRegistry containerRegistry,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// 绑定数据库连接配置到对象
|
||||
var dbOptions = configuration.GetSection("DbConnection").Get<DbConnectionOptions>();
|
||||
|
||||
// 注册配置实例
|
||||
containerRegistry.RegisterInstance(dbOptions);
|
||||
|
||||
return containerRegistry;
|
||||
}
|
||||
}
|
||||
}
|
||||
109
yy-admin-master/YY.Admin/Module/ServiceExtensions.cs
Normal file
109
yy-admin-master/YY.Admin/Module/ServiceExtensions.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Reflection;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Services.Service.Auth;
|
||||
|
||||
namespace YY.Admin
|
||||
{
|
||||
public static class ServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册服务
|
||||
/// </summary>
|
||||
public static void AddService(this IContainerRegistry containerRegistry, IConfiguration configuration)
|
||||
{
|
||||
// 注册配置
|
||||
containerRegistry.RegisterInstance<IConfiguration>(configuration);
|
||||
//// 注册日志服务为单例
|
||||
containerRegistry.RegisterSingleton<ILoggerService, SerilogLoggerService>();
|
||||
// 自动扫描并注册应用层服务
|
||||
RegisterServicesByAssembly(containerRegistry, typeof(ISysAuthService).Assembly);
|
||||
}
|
||||
/// <summary>
|
||||
/// 自动注册程序集中所有服务
|
||||
/// </summary>
|
||||
private static void RegisterServicesByAssembly(IContainerRegistry containerRegistry, Assembly assembly)
|
||||
{
|
||||
// 获取所有公开的非抽象类
|
||||
var serviceTypes = assembly.GetExportedTypes()
|
||||
.Where(t => t.IsClass && !t.IsAbstract);
|
||||
|
||||
foreach (var implementationType in serviceTypes)
|
||||
{
|
||||
// 查找服务接口(名称以"I"开头,去掉首字母后匹配)
|
||||
var serviceInterface = implementationType.GetInterfaces()
|
||||
.FirstOrDefault(i => i.Name == $"I{implementationType.Name}");
|
||||
|
||||
// 如果没有直接匹配的接口,尝试查找其他接口
|
||||
if (serviceInterface == null)
|
||||
{
|
||||
// 备选方案1:注册所有实现的接口(适合多接口实现)
|
||||
RegisterAllInterfaces(containerRegistry, implementationType);
|
||||
|
||||
// 备选方案2:只注册主接口
|
||||
// serviceInterface = implementationType.GetInterface($"I{implementationType.Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 注册找到的接口
|
||||
RegisterService(containerRegistry, serviceInterface, implementationType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册服务到容器
|
||||
/// </summary>
|
||||
private static void RegisterService(
|
||||
IContainerRegistry containerRegistry,
|
||||
Type serviceType,
|
||||
Type implementationType)
|
||||
{
|
||||
// 根据命名约定判断生命周期
|
||||
bool isSingleton = implementationType.Name.EndsWith("Service") ||
|
||||
implementationType.Name.EndsWith("Repository");
|
||||
|
||||
// 根据基类判断生命周期
|
||||
bool isSingletonByBase = typeof(ISingletonDependency).IsAssignableFrom(implementationType);
|
||||
|
||||
// 根据特性判断生命周期
|
||||
var attribute = implementationType.GetCustomAttribute<LifecycleAttribute>();
|
||||
var lifecycle = attribute?.Lifecycle ?? Lifecycle.Transient;
|
||||
|
||||
// 确定注册方式
|
||||
if (isSingleton || isSingletonByBase || lifecycle == Lifecycle.Singleton)
|
||||
{
|
||||
containerRegistry.RegisterSingleton(serviceType, implementationType);
|
||||
Console.WriteLine($"注册单例服务: {serviceType.Name} -> {implementationType.Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
containerRegistry.Register(serviceType, implementationType);
|
||||
Console.WriteLine($"注册瞬时服务: {serviceType.Name} -> {implementationType.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册所有实现的接口
|
||||
/// </summary>
|
||||
private static void RegisterAllInterfaces(IContainerRegistry containerRegistry, Type implementationType)
|
||||
{
|
||||
var interfaces = implementationType.GetInterfaces()
|
||||
.Where(i => i != typeof(IDisposable) &&
|
||||
!i.Name.StartsWith("_") && // 排除某些特殊接口
|
||||
i.Assembly == implementationType.Assembly); // 只注册同程序集接口
|
||||
|
||||
foreach (var serviceType in interfaces)
|
||||
{
|
||||
RegisterService(containerRegistry, serviceType, implementationType);
|
||||
}
|
||||
|
||||
if (!interfaces.Any())
|
||||
{
|
||||
// 如果没有接口,直接注册具体类型
|
||||
containerRegistry.Register(implementationType);
|
||||
Console.WriteLine($"注册具体类型: {implementationType.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
yy-admin-master/YY.Admin/Module/SyncModule.cs
Normal file
69
yy-admin-master/YY.Admin/Module/SyncModule.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Polly;
|
||||
using Polly.Extensions.Http;
|
||||
using Prism.Ioc;
|
||||
using Prism.Modularity;
|
||||
using System.Net.Http;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Infrastructure.Hubs;
|
||||
using YY.Admin.Infrastructure.Network;
|
||||
using YY.Admin.Infrastructure.Storage;
|
||||
using YY.Admin.Infrastructure.Sync;
|
||||
|
||||
namespace YY.Admin.Module;
|
||||
|
||||
public class SyncModule : IModule
|
||||
{
|
||||
public void RegisterTypes(IContainerRegistry containerRegistry)
|
||||
{
|
||||
containerRegistry.RegisterSingleton<TokenStore>();
|
||||
containerRegistry.RegisterSingleton<HttpSyncClient>();
|
||||
containerRegistry.RegisterSingleton<OutboxProcessor>();
|
||||
containerRegistry.RegisterSingleton<IJeecgUserMirrorPullOutbox, JeecgUserMirrorPullOutbox>();
|
||||
containerRegistry.RegisterSingleton<INetworkMonitor, NetworkMonitor>();
|
||||
containerRegistry.RegisterSingleton<ISignalRService, StompWebSocketService>();
|
||||
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddHttpClient("JeecgApi", (sp, client) =>
|
||||
{
|
||||
var config = containerRegistry.GetContainer().Resolve<IConfiguration>();
|
||||
var baseUrl = config.GetValue<string>("JeecgIntegration:BaseUrl")?.TrimEnd('/');
|
||||
if (!string.IsNullOrWhiteSpace(baseUrl))
|
||||
{
|
||||
client.BaseAddress = new Uri(baseUrl);
|
||||
}
|
||||
|
||||
var tokenStore = containerRegistry.GetContainer().Resolve<TokenStore>();
|
||||
var token = tokenStore.GetTokenAsync(default).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (!string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
}).AddPolicyHandler(GetRetryPolicy());
|
||||
|
||||
var provider = serviceCollection.BuildServiceProvider();
|
||||
var httpClientFactory = provider.GetRequiredService<IHttpClientFactory>();
|
||||
containerRegistry.RegisterInstance(httpClientFactory);
|
||||
}
|
||||
|
||||
public void OnInitialized(IContainerProvider containerProvider)
|
||||
{
|
||||
var networkMonitor = containerProvider.Resolve<INetworkMonitor>();
|
||||
var outboxProcessor = containerProvider.Resolve<OutboxProcessor>();
|
||||
var signalService = containerProvider.Resolve<ISignalRService>();
|
||||
|
||||
_ = networkMonitor.StartAsync(CancellationToken.None);
|
||||
_ = outboxProcessor.StartConsumerAsync(CancellationToken.None);
|
||||
// 用户镜像 + 设备指令:统一 STOMP(/ws/device),免密与设备 Token 模式均启动
|
||||
_ = Task.Run(() => signalService.ConnectUnifiedDeviceChannelAsync(CancellationToken.None));
|
||||
}
|
||||
|
||||
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
|
||||
{
|
||||
return HttpPolicyExtensions
|
||||
.HandleTransientHttpError()
|
||||
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
|
||||
}
|
||||
|
||||
}
|
||||
246
yy-admin-master/YY.Admin/Module/TabItemModel.cs
Normal file
246
yy-admin-master/YY.Admin/Module/TabItemModel.cs
Normal file
@@ -0,0 +1,246 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Event;
|
||||
|
||||
namespace YY.Admin.Module
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个标签的数据模型
|
||||
/// </summary>
|
||||
public class TabItemModel : BindableBase
|
||||
{
|
||||
public ObservableCollection<TabItemModel>? OpenTabs { get; set; }
|
||||
|
||||
public IEventAggregator? EventAggregator { get; set; }
|
||||
|
||||
private string? _id;
|
||||
public string? Id
|
||||
{
|
||||
get => _id;
|
||||
set => SetProperty(ref _id, value);
|
||||
}
|
||||
|
||||
private string? _header;
|
||||
public string? Header
|
||||
{
|
||||
get => _header;
|
||||
set => SetProperty(ref _header, value);
|
||||
}
|
||||
|
||||
private string? _icon = string.Empty;
|
||||
public string? Icon
|
||||
{
|
||||
get => _icon;
|
||||
set => SetProperty(ref _icon, value);
|
||||
}
|
||||
|
||||
private IconTypeEnum? _iconType;
|
||||
public IconTypeEnum IconType {
|
||||
get {
|
||||
return _iconType ?? IconTypeEnum.AntDesign;
|
||||
}
|
||||
set => SetProperty(ref _iconType, value);
|
||||
}
|
||||
|
||||
private string? _viewName = string.Empty;
|
||||
public string? ViewName
|
||||
{
|
||||
get => _viewName;
|
||||
set => SetProperty(ref _viewName, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tab是否允许关闭
|
||||
/// </summary>
|
||||
private bool _isClosable = true;
|
||||
public bool IsClosable
|
||||
{
|
||||
get => _isClosable;
|
||||
set => SetProperty(ref _isClosable, value);
|
||||
}
|
||||
|
||||
private bool _isSelected;
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => SetProperty(ref _isSelected, value);
|
||||
}
|
||||
|
||||
// 添加对Tab源的引用
|
||||
public TabSource? TabSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 左侧是否有可关闭Tab
|
||||
/// </summary>
|
||||
public bool HasClosableLeft => OpenTabs != null && OpenTabs
|
||||
.Take(OpenTabs.IndexOf(this))
|
||||
.Any(t => t.IsClosable);
|
||||
|
||||
/// <summary>
|
||||
/// 右侧是否有可关闭Tab
|
||||
/// </summary>
|
||||
public bool HasClosableRight => OpenTabs != null && OpenTabs
|
||||
.Skip(OpenTabs.IndexOf(this) + 1)
|
||||
.Any(t => t.IsClosable);
|
||||
|
||||
/// <summary>
|
||||
/// 其他Tab(除当前)是否有可关闭Tab
|
||||
/// </summary>
|
||||
public bool HasClosableOther => OpenTabs != null && OpenTabs
|
||||
.Where(t => t != this)
|
||||
.Any(t => t.IsClosable);
|
||||
|
||||
/// <summary>
|
||||
/// 是否有任意可关闭Tab(全部)
|
||||
///</summary>
|
||||
public bool HasClosableAny => OpenTabs != null && OpenTabs
|
||||
.Any(t => t.IsClosable);
|
||||
|
||||
public DelegateCommand<TabItemModel> RefreshTabCommand { get; }
|
||||
public DelegateCommand<TabItemModel> CloseTabCommand { get; }
|
||||
public DelegateCommand<TabItemModel> CloseLeftTabsCommand { get; }
|
||||
public DelegateCommand<TabItemModel> CloseRightTabsCommand { get; }
|
||||
public DelegateCommand<TabItemModel> CloseOtherTabsCommand { get; }
|
||||
public DelegateCommand<TabItemModel> CloseAllTabsCommand { get; }
|
||||
|
||||
public TabItemModel()
|
||||
{
|
||||
Id = $"TabRegion_{Guid.NewGuid():N}"; // 保证唯一
|
||||
|
||||
RefreshTabCommand = new DelegateCommand<TabItemModel>(RefreshTab);
|
||||
CloseTabCommand = new DelegateCommand<TabItemModel>(CloseTab);
|
||||
CloseLeftTabsCommand = new DelegateCommand<TabItemModel>(CloseLeftTabs);
|
||||
CloseRightTabsCommand = new DelegateCommand<TabItemModel>(CloseRightTabs);
|
||||
CloseOtherTabsCommand = new DelegateCommand<TabItemModel>(CloseOtherTabs);
|
||||
CloseAllTabsCommand = new DelegateCommand<TabItemModel>(CloseAllTabs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新当前Tab
|
||||
/// </summary>
|
||||
/// <param name="tabItemModel"></param>
|
||||
private void RefreshTab(TabItemModel tabItemModel)
|
||||
{
|
||||
if (tabItemModel?.TabSource == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 发布事件
|
||||
EventAggregator?.GetEvent<TabRefreshEvent>().Publish(tabItemModel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭当前Tab
|
||||
/// </summary>
|
||||
/// <param name="tabItemModel"></param>
|
||||
private void CloseTab(TabItemModel tabItemModel)
|
||||
{
|
||||
if (tabItemModel?.IsClosable != true || OpenTabs?.Any() != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 发布事件
|
||||
EventAggregator?.GetEvent<TabClosedEvent>().Publish(tabItemModel);
|
||||
|
||||
// 从集合中移除标签
|
||||
OpenTabs.Remove(tabItemModel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭左侧Tab
|
||||
/// </summary>
|
||||
private void CloseLeftTabs(TabItemModel tabItemModel)
|
||||
{
|
||||
if (OpenTabs?.Any() != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 找到当前 Tab 的索引
|
||||
int currentIndex = OpenTabs.IndexOf(tabItemModel);
|
||||
// 左侧没有 Tab
|
||||
if (currentIndex <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 找出左侧所有可关闭的 Tab
|
||||
var leftTabs = OpenTabs
|
||||
.Take(currentIndex) // 取前面所有 Tab
|
||||
.Where(t => t.IsClosable) // 只关闭可关闭的
|
||||
.ToList(); // 先 ToList 避免集合修改时报错
|
||||
|
||||
foreach (var tab in leftTabs)
|
||||
{
|
||||
CloseTab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭右侧Tab
|
||||
/// </summary>
|
||||
private void CloseRightTabs(TabItemModel tabItemModel)
|
||||
{
|
||||
if (OpenTabs?.Any() != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int currentIndex = OpenTabs.IndexOf(tabItemModel);
|
||||
if (currentIndex < 0 || currentIndex >= OpenTabs.Count - 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var rightTabs = OpenTabs
|
||||
.Skip(currentIndex + 1)
|
||||
.Where(t => t.IsClosable)
|
||||
.ToList();
|
||||
|
||||
foreach (var tab in rightTabs)
|
||||
{
|
||||
CloseTab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭其他Tab(仅保留当前)
|
||||
/// </summary>
|
||||
private void CloseOtherTabs(TabItemModel tabItemModel)
|
||||
{
|
||||
if (OpenTabs?.Any() != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var otherTabs = OpenTabs
|
||||
.Where(t => t != tabItemModel && t.IsClosable)
|
||||
.ToList();
|
||||
|
||||
foreach (var tab in otherTabs)
|
||||
{
|
||||
CloseTab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭全部(包括当前)
|
||||
/// </summary>
|
||||
private void CloseAllTabs(TabItemModel tabItemModel)
|
||||
{
|
||||
if (OpenTabs?.Any() != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var allClosableTabs = OpenTabs
|
||||
.Where(t => t.IsClosable)
|
||||
.ToList();
|
||||
|
||||
foreach (var tab in allClosableTabs)
|
||||
{
|
||||
CloseTab(tab);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
yy-admin-master/YY.Admin/Module/TabSource.cs
Normal file
56
yy-admin-master/YY.Admin/Module/TabSource.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Windows.Input;
|
||||
using YY.Admin.Core;
|
||||
|
||||
namespace YY.Admin.Module
|
||||
{
|
||||
public class TabSource : BindableBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public virtual string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图标
|
||||
/// </summary>
|
||||
public virtual string? Icon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图标类型
|
||||
/// </summary>
|
||||
public virtual IconTypeEnum IconType { get; set; } = IconTypeEnum.AntDesign;
|
||||
|
||||
/// <summary>
|
||||
/// 视图
|
||||
/// </summary>
|
||||
public virtual string? ViewName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 视图的导航参数
|
||||
/// </summary>
|
||||
public INavigationParameters? NavigationParameter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否选中
|
||||
/// </summary>
|
||||
private bool _isSelected;
|
||||
|
||||
public virtual bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => SetProperty(ref _isSelected, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tab是否允许关闭
|
||||
/// </summary>
|
||||
private bool _isClosable = true;
|
||||
public bool IsClosable
|
||||
{
|
||||
get => _isClosable;
|
||||
set => SetProperty(ref _isClosable, value);
|
||||
}
|
||||
|
||||
public ICommand? Command { get; set; }
|
||||
}
|
||||
}
|
||||
38
yy-admin-master/YY.Admin/Properties/AppSettings.Designer.cs
generated
Normal file
38
yy-admin-master/YY.Admin/Properties/AppSettings.Designer.cs
generated
Normal file
@@ -0,0 +1,38 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码由工具生成。
|
||||
// 运行时版本:4.0.30319.42000
|
||||
//
|
||||
// 对此文件的更改可能会导致不正确的行为,并且如果
|
||||
// 重新生成代码,这些更改将会丢失。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YY.Admin.Properties {
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.14.0.0")]
|
||||
internal sealed partial class AppSettings : global::System.Configuration.ApplicationSettingsBase {
|
||||
|
||||
private static AppSettings defaultInstance = ((AppSettings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new AppSettings())));
|
||||
|
||||
public static AppSettings Default {
|
||||
get {
|
||||
return defaultInstance;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("0")]
|
||||
public int SkinType {
|
||||
get {
|
||||
return ((int)(this["SkinType"]));
|
||||
}
|
||||
set {
|
||||
this["SkinType"] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
yy-admin-master/YY.Admin/Properties/AppSettings.settings
Normal file
9
yy-admin-master/YY.Admin/Properties/AppSettings.settings
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="YY.Admin.Properties" GeneratedClassName="AppSettings">
|
||||
<Profiles />
|
||||
<Settings>
|
||||
<Setting Name="SkinType" Type="System.Int32" Scope="User">
|
||||
<Value Profile="(Default)">0</Value>
|
||||
</Setting>
|
||||
</Settings>
|
||||
</SettingsFile>
|
||||
BIN
yy-admin-master/YY.Admin/Resources/Icon/404.png
Normal file
BIN
yy-admin-master/YY.Admin/Resources/Icon/404.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
yy-admin-master/YY.Admin/Resources/Icon/avatar.png
Normal file
BIN
yy-admin-master/YY.Admin/Resources/Icon/avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
BIN
yy-admin-master/YY.Admin/Resources/Icon/iconfont.ttf
Normal file
BIN
yy-admin-master/YY.Admin/Resources/Icon/iconfont.ttf
Normal file
Binary file not shown.
BIN
yy-admin-master/YY.Admin/Resources/Icon/login.png
Normal file
BIN
yy-admin-master/YY.Admin/Resources/Icon/login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
BIN
yy-admin-master/YY.Admin/Resources/Icon/logo.ico
Normal file
BIN
yy-admin-master/YY.Admin/Resources/Icon/logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
BIN
yy-admin-master/YY.Admin/Resources/Icon/logo.png
Normal file
BIN
yy-admin-master/YY.Admin/Resources/Icon/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
@@ -0,0 +1,7 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:o="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options">
|
||||
|
||||
<SolidColorBrush o:Freeze="True" x:Key="ThirdlyBorderBrush" Color="{DynamicResource ThirdlyBorderColor}"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,6 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<Color x:Key="ThirdlyBorderColor">#e0e0e0</Color>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,11 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<Color x:Key="ThirdlyTextColor">#8d9095</Color>
|
||||
<Color x:Key="PrimaryTextColor">#cfd3dc</Color>
|
||||
<Color x:Key="ThirdlyBorderColor">#616161</Color>
|
||||
|
||||
<!--<Color x:Key="HoverColor">#404040</Color>-->
|
||||
<!--<SolidColorBrush x:Key="HoverBrush" Color="{DynamicResource HoverColor}"/>-->
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,6 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<Color x:Key="ThirdlyBorderColor">#e0e0e0</Color>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,117 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:system="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol">
|
||||
|
||||
<!-- DataGrid -->
|
||||
<Style x:Key="CusDataGridStyle" TargetType="DataGrid" BasedOn="{StaticResource DataGridBaseStyle}">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Margin" Value="0"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
|
||||
<!-- 行样式:去掉分隔线、显示底线 -->
|
||||
<Setter Property="RowStyle">
|
||||
<Setter.Value>
|
||||
<Style TargetType="DataGridRow" BasedOn="{StaticResource DataGridRowStyle}">
|
||||
<Setter Property="BorderThickness" Value="0,1,0,0"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/>
|
||||
<Setter Property="hc:BorderElement.CornerRadius" Value="0"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Margin" Value="0"/>
|
||||
</Style>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="CusDataGridColumnHeaderStyle"
|
||||
TargetType="DataGridColumnHeader"
|
||||
BasedOn="{StaticResource DataGridColumnHeaderStyle}">
|
||||
<Setter Property="FontSize" Value="{StaticResource FontSize}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Height" Value="40"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Margin" Value="0"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="CusDataGridCellStyle"
|
||||
TargetType="DataGridCell"
|
||||
BasedOn="{StaticResource DataGridCellStyle}">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="FontSize" Value="{StaticResource FontSize}"/>
|
||||
<Setter Property="hc:BorderElement.CornerRadius" Value="0"/>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="CusOperDataGridCellStyle"
|
||||
TargetType="DataGridCell"
|
||||
BasedOn="{StaticResource CusDataGridCellStyle}">
|
||||
<Setter Property="Foreground" Value="#1890ff"/>
|
||||
<Setter Property="Focusable" Value="False"/>
|
||||
<!-- 禁止选中改变 Foreground -->
|
||||
<Style.Triggers>
|
||||
<!-- 选中单元格 -->
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Foreground" Value="#1890ff"/>
|
||||
</Trigger>
|
||||
<!-- 聚焦单元格 -->
|
||||
<Trigger Property="IsKeyboardFocusWithin" Value="True">
|
||||
<Setter Property="Foreground" Value="#1890ff"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- TreeView -->
|
||||
<Style x:Key="CusTreeViewItemBaseStyle" BasedOn="{StaticResource TreeViewItemBaseStyle}" TargetType="TreeViewItem">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TreeViewItem">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition MinHeight="{TemplateBinding MinHeight}"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Border x:Name="Bd" CornerRadius="{Binding Path=(hc:BorderElement.CornerRadius),RelativeSource={RelativeSource TemplatedParent}}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
|
||||
<DockPanel LastChildFill="True" Margin="{Binding Converter={StaticResource TreeViewItemMarginConverter}, RelativeSource={RelativeSource TemplatedParent}}">
|
||||
<ToggleButton x:Name="Expander" ClickMode="Press" IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ExpandCollapseToggleStyle}"/>
|
||||
<ContentPresenter VerticalAlignment="Center" x:Name="PART_Header" ContentSource="Header" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<ItemsPresenter x:Name="ItemsHost" Grid.Row="1"/>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="true" SourceName="Bd">
|
||||
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource SecondaryRegionBrush}"/>
|
||||
<Setter Property="Cursor" TargetName="Bd" Value="Arrow"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsExpanded" Value="false">
|
||||
<Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/>
|
||||
</Trigger>
|
||||
<Trigger Property="HasItems" Value="false">
|
||||
<Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="true">
|
||||
<!--<Setter Property="Background" TargetName="Bd" Value="{DynamicResource PrimaryBrush}"/>-->
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="false">
|
||||
<Setter Property="Opacity" Value=".4" />
|
||||
</Trigger>
|
||||
|
||||
<!--<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="HasItems" Value="True"/>
|
||||
<Condition Property="IsSelected" Value="True"/>
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="Transparent"/>
|
||||
</MultiTrigger>-->
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
43
yy-admin-master/YY.Admin/Resources/Styles/Icons.xaml
Normal file
43
yy-admin-master/YY.Admin/Resources/Styles/Icons.xaml
Normal file
@@ -0,0 +1,43 @@
|
||||
<!-- Icons.xaml -->
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<!-- 仪表盘图标 -->
|
||||
<Geometry x:Key="DashboardOutlined">M549.61981 133.022476l319.683047 203.605334A70.851048 70.851048 0 0 1 902.095238 396.361143v434.883047A70.89981 70.89981 0 0 1 831.146667 902.095238h-282.819048l0.024381-218.112h-71.826286v218.087619L192.853333 902.095238A70.89981 70.89981 0 0 1 121.904762 831.24419V390.241524c0-24.527238 12.678095-47.299048 33.54819-60.220953l318.659048-197.485714a70.972952 70.972952 0 0 1 75.50781 0.487619zM828.952381 828.952381V397.214476L511.488 195.047619 195.047619 391.119238V828.952381h211.309714v-216.551619h212.187429v216.527238L828.952381 828.952381z</Geometry>
|
||||
|
||||
<!-- 系统管理图标 -->
|
||||
<Geometry x:Key="HomeIcon">M782.208 883.968q-27.712 48-83.2 48H324.992q-55.424 0-83.136-48L54.784 560q-27.712-48 0-96l187.008-323.968q27.712-48 83.2-48h374.08q55.424 0 83.136 48l187.008 323.968q27.712 48 0 96l-187.008 323.968z m-55.424-32l187.008-323.968q9.28-16 0-32l-187.008-323.968q-9.28-16-27.712-16H324.928q-18.432 0-27.712 16L110.208 496q-9.28 16 0 32l187.008 323.968q9.28 16 27.712 16h374.144q18.432 0 27.712-16zM672 512a160 160 0 1 1-320 0 160 160 0 0 1 320 0z m-64 0a96 96 0 1 0-192 0 96 96 0 0 0 192 0z</Geometry>
|
||||
|
||||
<!-- 用户管理图标 -->
|
||||
<Geometry x:Key="UserOutlined">M858.5 763.6a374 374 0 0 0-80.6-119.5 375.63 375.63 0 0 0-119.5-80.6c-54.3-26.7-114.5-40.3-175.8-40.3-61.3 0-121.5 13.6-175.8 40.3a373.6 373.6 0 0 0-119.5 80.6 375.57 375.57 0 0 0-80.6 119.5A375.55 375.55 0 0 0 136 901.8a8 8 0 0 0 8 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 0 0 8-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-79.5 0-144-64.5-144-144s64.5-144 144-144 144 64.5 144 144-64.5 144-144 144z</Geometry>
|
||||
|
||||
<!-- 角色管理图标 -->
|
||||
<Geometry x:Key="TeamOutlined">M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zm0-620c-48.6 0-88 39.4-88 88s39.4 88 88 88 88-39.4 88-88-39.4-88-88-88zm0 288c-66.2 0-120 53.8-120 120 0 4.4 3.6 8 8 8h224c4.4 0 8-3.6 8-8 0-66.2-53.8-120-120-120z</Geometry>
|
||||
|
||||
<!-- 向下 -->
|
||||
<Geometry x:Key="ArrowDown">M832 340.992l-320 312-320-312a28.8 28.8 0 0 0-20.992-8.96 28.8 28.8 0 0 0-20.992 8.96 28.8 28.8 0 0 0-8.96 20.992c0 8 2.624 14.72 8 19.968l340.992 332.032c5.952 6.016 13.312 8.96 22.016 8.96 8.64 0 16-2.944 22.016-8.96l340.992-331.008a31.232 31.232 0 0 0 8-21.504c0-8.32-3.008-15.36-8.96-20.992a29.76 29.76 0 0 0-42.048 0.448v0.064z</Geometry>
|
||||
<!-- 通知 -->
|
||||
<Geometry x:Key="Notice">M509.824 874.624c-51.3536 0-94.336-21.76-112.9984-53.632h225.9712c-18.6624 31.8464-61.6448 53.632-112.9728 53.632zM507.5968 142.464c53.2992 0 98.3296 33.7152 110.0288 80.128 97.3312 47.4624 153.088 142.976 153.088 263.936l-0.0768 22.3232c-0.3072 34.432-1.9712 72.3968-11.0592 104.2688 13.5424 8.704 26.6496 20.8128 37.5552 34.944 17.5872 22.4 27.264 46.4384 27.264 67.6352v2.0224c0 60.7744-66.6368 88.448-128.6144 88.448H319.4112c-30.8992 0-59.776-6.8608-81.3824-19.3792-27.648-16.1536-42.8032-41.0112-42.8032-70.0672v-2.048c0-36.3264 26.6496-79.36 60.5696-101.5552-11.3152-40.3712-11.3152-92.8768-11.3152-132.6592 0-118.912 54.3232-209.9968 153.088-257.8432 11.6992-46.4384 56.704-80.1536 110.0288-80.1536z m0 64c-22.7584 0-41.1136 12.416-46.976 28.544l-1.024 3.2512-7.296 28.928-26.8544 13.0304c-76.2368 36.9408-116.9664 105.4464-116.9664 200.2432l0.1024 24.576c0.128 14.08 0.4352 24.32 1.0496 35.072l0.256 4.0704c1.1776 18.5344 3.2512 34.1248 6.2208 46.5664l1.3056 5.12 12.6464 45.1328-39.1936 25.6768c-16.0768 10.496-29.5936 31.744-31.4368 45.0816l-0.2048 2.944v2.0224c0 5.632 2.56 9.8048 10.88 14.6688 9.984 5.8112 25.216 9.8048 42.6496 10.624l6.656 0.1536h376.3712c21.632 0 42.0608-4.864 54.8352-12.4416 6.9888-4.1472 9.216-6.8096 9.7024-10.3936l0.0768-1.6128v-2.0224c0-5.5808-4.3008-16.256-13.9264-28.544-5.12-6.656-11.0592-12.544-16.9984-17.0496l-4.4544-3.1232-40.0128-25.6768 13.056-45.7216c3.328-11.6992 5.6064-26.24 6.9632-43.8528l0.512-7.7312c0.9472-15.7696 1.152-29.0816 1.152-57.472 0-94.08-39.8336-165.504-110.3872-202.9824l-6.7072-3.4048-26.752-13.056-7.2704-28.8256c-4.4544-17.664-23.7056-31.7952-47.9744-31.7952z</Geometry>
|
||||
|
||||
<GeometryGroup x:Key="AdminSys">
|
||||
<PathGeometry>M1243.428571 1024h-219.428571a73.142857 73.142857 0 0 0-146.285714 0H512a73.142857 73.142857 0 0 0-146.285714 0H146.285714a146.285714 146.285714 0 0 1-146.285714-146.285714V146.285714a146.285714 146.285714 0 0 1 146.285714-146.285714h512a146.285714 146.285714 0 0 1 146.285715 146.285714h438.857142a146.285714 146.285714 0 0 1 146.285715 146.285715v585.142857a146.285714 146.285714 0 0 1-146.285715 146.285714zM658.285714 707.072a203.922286 203.922286 0 0 0-139.629714-97.572571 142.482286 142.482286 0 0 1-45.348571-48.713143h-1.170286A92.891429 92.891429 0 0 1 425.545143 512a21.942857 21.942857 0 0 1 0.658286-3.145143A142.336 142.336 0 0 0 518.656 365.714286a119.442286 119.442286 0 1 0-232.740571 0 142.336 142.336 0 0 0 92.452571 143.140571 21.942857 21.942857 0 0 1 0.658286 3.145143 93.769143 93.769143 0 0 1-46.592 48.786286 141.092571 141.092571 0 0 1-47.323429 48.713143 204.8 204.8 0 0 0-138.971428 97.572571V804.571429h512V707.072zM1243.428571 292.571429h-438.857142v73.142857h438.857142V292.571429z m0 219.428571h-438.857142v73.142857h438.857142V512z m0 219.428571h-438.857142v73.142858h438.857142v-73.142858z</PathGeometry>
|
||||
</GeometryGroup>
|
||||
<Geometry x:Key="SkinAntDesign">M870 126H663.8c-17.4 0-32.9 11.9-37 29.3C614.3 208.1 567 246 512 246s-102.3-37.9-114.8-90.7c-4.1-17.4-19.5-29.3-37-29.3H154c-24.3 0-44 19.7-44 44v252c0 24.3 19.7 44 44 44h75v388c0 24.3 19.7 44 44 44h478c24.3 0 44-19.7 44-44V466h75c24.3 0 44-19.7 44-44V170c0-24.3-19.7-44-44-44z m-28 268H723v432H301V394H182V198h153.3c28.2 71.2 97.5 120 176.7 120s148.5-48.8 176.7-120H842v196z</Geometry>
|
||||
|
||||
<Geometry x:Key="Delete">M880 240H704v-64c0-52.8-43.2-96-96-96H416c-52.8 0-96 43.2-96 96v64H144c-17.6 0-32 14.4-32 32s14.4 32 32 32h48v512c0 70.4 57.6 128 128 128h384c70.4 0 128-57.6 128-128V304h48c17.6 0 32-14.4 32-32s-14.4-32-32-32z m-496-64c0-17.6 14.4-32 32-32h192c17.6 0 32 14.4 32 32v64H384v-64z m384 640c0 35.2-28.8 64-64 64H320c-35.2 0-64-28.8-64-64V304h512v512zM416 432c-17.6 0-32 14.4-32 32v256c0 17.6 14.4 32 32 32s32-14.4 32-32V464c0-17.6-14.4-32-32-32z m192 0c-17.6 0-32 14.4-32 32v256c0 17.6 14.4 32 32 32s32-14.4 32-32V464c0-17.6-14.4-32-32-32z</Geometry>
|
||||
|
||||
<FontFamily x:Key="AntDesignIcon">
|
||||
Pack://application:,,,/YY.Admin;component/Resources/Icon/#iconfont
|
||||
</FontFamily>
|
||||
|
||||
<FontFamily x:Key="FontAwesomeRegular">
|
||||
pack://application:,,,/YY.Admin;component/Resources/Icon/FontAwesome/#Font Awesome 7 Free Regular
|
||||
</FontFamily>
|
||||
<FontFamily x:Key="FontAwesomeSolid">
|
||||
pack://application:,,,/YY.Admin;component/Resources/Icon/FontAwesome/#Font Awesome 7 Free Solid
|
||||
</FontFamily>
|
||||
<FontFamily x:Key="FontAwesomeBrands">
|
||||
pack://application:,,,/YY.Admin;component/Resources/Icon/FontAwesome/#Font Awesome 7 Brands
|
||||
</FontFamily>
|
||||
|
||||
</ResourceDictionary>
|
||||
<!--图标来源地址:https://www.iconfont.cn/collections/detail?spm=a313x.collections_index.i1.d9df05512.2c083a81ELVBqs&cid=19238&page=1-->
|
||||
75
yy-admin-master/YY.Admin/Resources/Styles/YY.xaml
Normal file
75
yy-admin-master/YY.Admin/Resources/Styles/YY.xaml
Normal file
@@ -0,0 +1,75 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:system="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:converter="clr-namespace:YY.Admin.Core.Converter;assembly=YY.Admin.Core">
|
||||
|
||||
<!-- 转换器 -->
|
||||
<converter:EnumDescriptionConverter x:Key="EnumDescriptionConverter"/>
|
||||
<converter:EnumToTagTypeConverter x:Key="EnumToTagTypeConverter"/>
|
||||
<converter:EnumToIntConverter x:Key="EnumToIntConverter"/>
|
||||
<converter:EnumToBoolConverter x:Key="EnumToBoolConverter"/>
|
||||
<converter:EnumToVisibilityConverter x:Key="EnumToVisibilityConverter"/>
|
||||
<converter:RadioButtonEnumMultiConverter x:Key="RadioButtonEnumMultiConverter"/>
|
||||
<converter:NegativeLeftThicknessConverter x:Key="NegativeLeftThicknessConverter" />
|
||||
<converter:BrushToSolidColorPaintConverter x:Key="BrushToSolidColorPaintConverter" />
|
||||
<converter:MaxWidthConverter x:Key="MaxWidthConverter" />
|
||||
<converter:PercentageConverter x:Key="PercentageConverter" />
|
||||
|
||||
<!-- 字体大小 -->
|
||||
<system:Double x:Key="FontSize">14</system:Double>
|
||||
|
||||
<!-- 控件校验错误信息模板 -->
|
||||
<ControlTemplate x:Key="BottomLeftErrorTemplate_ForInfoElement">
|
||||
<Grid>
|
||||
<!-- 占位原控件 -->
|
||||
<AdornedElementPlaceholder x:Name="adorner" />
|
||||
|
||||
<!-- 错误提示框:左对齐,相对于 AdornedElement 的 TitleWidth 做左移 -->
|
||||
<Border
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Left">
|
||||
<Border.Margin>
|
||||
<MultiBinding Converter="{StaticResource NegativeLeftThicknessConverter}" ConverterParameter="-18">
|
||||
<!-- 通过 adorner 访问 adorned element 的附加属性 -->
|
||||
<Binding Path="AdornedElement.(hc:InfoElement.TitleWidth)"
|
||||
ElementName="adorner"/>
|
||||
<Binding Path="AdornedElement.(hc:InfoElement.TitlePlacement)"
|
||||
ElementName="adorner"/>
|
||||
</MultiBinding>
|
||||
</Border.Margin>
|
||||
<TextBlock Foreground="#E74C3C" FontSize="12" Text="{Binding [0].ErrorContent}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
|
||||
<Style x:Key="IconButtonStyle" TargetType="TextBlock">
|
||||
<Setter Property="Margin" Value="5,0,0,0"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="BaseViewStyle" TargetType="FrameworkElement">
|
||||
<Setter Property="Margin" Value="20"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DataGridOpeButtonStyle" TargetType="Border">
|
||||
<!--<Setter Property="Margin" Value="5 0 0 0"/>-->
|
||||
<Setter Property="Padding" Value="0,7"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="GridSplitterPreviewStyle" TargetType="Control">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate>
|
||||
<Border
|
||||
Background="{DynamicResource PrimaryBrush}"
|
||||
Opacity="0.6"
|
||||
Margin="-1,0"/>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
BIN
yy-admin-master/YY.Admin/Slave.db
Normal file
BIN
yy-admin-master/YY.Admin/Slave.db
Normal file
Binary file not shown.
46
yy-admin-master/YY.Admin/Subscriber/ErrorNotification.cs
Normal file
46
yy-admin-master/YY.Admin/Subscriber/ErrorNotification.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using HandyControl.Controls;
|
||||
using YY.Admin.Core.BusinessException;
|
||||
|
||||
namespace YY.Admin.EventBus
|
||||
{
|
||||
// 错误事件
|
||||
public class ErrorEvent : PubSubEvent<string>
|
||||
{
|
||||
}
|
||||
|
||||
// 错误处理服务
|
||||
public interface IErrorHandler
|
||||
{
|
||||
void HandleError(Exception ex);
|
||||
}
|
||||
|
||||
public class ErrorHandler : IErrorHandler
|
||||
{
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
|
||||
public ErrorHandler(
|
||||
IEventAggregator eventAggregator)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
public void HandleError(Exception ex)
|
||||
{
|
||||
if (ex is BusinessException bex)
|
||||
{
|
||||
// _logger.LogWarning($"业务错误: {bex.ErrorCode} - {bex.Message}");
|
||||
|
||||
// 在UI线程显示HandyControl提示
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
Growl.Error(bex.Message);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// _logger.LogError(ex, "未处理的异常");
|
||||
_eventAggregator.GetEvent<ErrorEvent>().Publish($"系统错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
163
yy-admin-master/YY.Admin/Subscriber/SysUserEventSubscriber.cs
Normal file
163
yy-admin-master/YY.Admin/Subscriber/SysUserEventSubscriber.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using YY.Admin.Core;
|
||||
using static YY.Admin.Core.SysUserEvents;
|
||||
using Prism.Events;
|
||||
namespace YY.Admin.EventBus
|
||||
{
|
||||
public class SysUserEventSubscriber : IDisposable
|
||||
{
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly List<SubscriptionToken> _subscriptions = new();
|
||||
public SysUserEventSubscriber(
|
||||
IEventAggregator eventAggregator,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
SubscribeEvents();
|
||||
}
|
||||
public void SubscribeEvents()
|
||||
{
|
||||
_eventAggregator.GetEvent<AddUserEvent>().Subscribe(OnAddUser, ThreadOption.BackgroundThread);
|
||||
_eventAggregator.GetEvent<UpdateUserEvent>().Subscribe(OnUpdateUser, ThreadOption.BackgroundThread);
|
||||
_eventAggregator.GetEvent<DeleteUserEvent>().Subscribe(OnDeleteUser, ThreadOption.BackgroundThread);
|
||||
_eventAggregator.GetEvent<SetUserStatusEvent>().Subscribe(OnSetUserStatus, ThreadOption.BackgroundThread);
|
||||
_eventAggregator.GetEvent<ChangePwdEvent>().Subscribe(OnChangePwd, ThreadOption.BackgroundThread);
|
||||
_eventAggregator.GetEvent<ResetPwdEvent>().Subscribe(OnResetPwd, ThreadOption.BackgroundThread);
|
||||
_eventAggregator.GetEvent<UnlockUserLoginEvent>().Subscribe(OnUnlockUserLogin, ThreadOption.BackgroundThread);
|
||||
_eventAggregator.GetEvent<RegisterUserEvent>().Subscribe(OnRegisterUser, ThreadOption.BackgroundThread);
|
||||
_eventAggregator.GetEvent<LoginUserEvent>().Subscribe(OnLoginUser, ThreadOption.BackgroundThread);
|
||||
_eventAggregator.GetEvent<LoginOutEvent>().Subscribe(OnLoginOut, ThreadOption.BackgroundThread);
|
||||
_eventAggregator.GetEvent<UpdateUserRoleEvent>().Subscribe(OnUpdateUserRole, ThreadOption.BackgroundThread);
|
||||
}
|
||||
|
||||
public void OnAddUser(SysUser payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Information($"添加新用户: {payload.Account}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"添加用户事件处理失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnRegisterUser(SysUser payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
Task.Run(() => {
|
||||
|
||||
|
||||
});
|
||||
_logger.Information($"用户注册");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"注册用户事件处理失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnUpdateUser((SysUser Original, SysUser Updated) payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Information($"更新用户");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"更新用户事件处理失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDeleteUser(SysUser payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"删除用户事件处理失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnSetUserStatus((SysUser User, StatusEnum NewStatus) payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"设置用户状态事件处理失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
public void OnChangePwd(SysUser payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"设置用户状态事件处理失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
public void OnUpdateUserRole((SysUser User, List<long> RoleIds) payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"更新用户角色事件处理失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnUnlockUserLogin(SysUser payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"解除登录锁定事件处理失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnResetPwd(SysUser payload)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void OnLoginUser(SysUser payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Information($"登录成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"登录处理失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnLoginOut(SysUser payload)
|
||||
{
|
||||
// 勿抛异常:Prism 事件总线上若此处抛错,可能影响同事件其它订阅者(如主窗口释放与 WS 停止)
|
||||
_logger.Information($"用户登出事件: {payload?.Account ?? "<null>"}");
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
// 显式取消所有订阅
|
||||
foreach (var token in _subscriptions)
|
||||
{
|
||||
token.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
yy-admin-master/YY.Admin/Updates/version.xml
Normal file
13
yy-admin-master/YY.Admin/Updates/version.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<item>
|
||||
<version>1.2.0.0</version>
|
||||
<url>http://your-update-server.com/YourAppSetup.exe</url>
|
||||
<changelog>
|
||||
• 新增用户管理功能
|
||||
• 优化系统性能
|
||||
• 修复已知问题
|
||||
• 改进用户体验
|
||||
</changelog>
|
||||
<publishDate>2025-11-26</publishDate>
|
||||
<mandatory>true</mandatory>
|
||||
</item>
|
||||
257
yy-admin-master/YY.Admin/ViewModels/AppSettingsViewModel.cs
Normal file
257
yy-admin-master/YY.Admin/ViewModels/AppSettingsViewModel.cs
Normal file
@@ -0,0 +1,257 @@
|
||||
using HandyControl.Themes;
|
||||
using HandyControl.Tools;
|
||||
using Mapster;
|
||||
using Newtonsoft.Json;
|
||||
using SqlSugar;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Const;
|
||||
using YY.Admin.Core.EventBus;
|
||||
using YY.Admin.Core.Helper;
|
||||
using YY.Admin.Core.Model;
|
||||
using YY.Admin.Core.Session;
|
||||
using HcSkinType = HandyControl.Data.SkinType;
|
||||
|
||||
namespace YY.Admin.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 系统设置
|
||||
/// </summary>
|
||||
public class AppSettingsViewModel : BindableBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 皮肤类型
|
||||
/// </summary>
|
||||
private HcSkinType? _skinType;
|
||||
public HcSkinType? SkinType
|
||||
{
|
||||
get => _skinType ??= GetDefaultSkinType();
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _skinType, value) && value != null)
|
||||
{
|
||||
if (!SyncWithSystem)
|
||||
{
|
||||
UpdateSkin(value.Value);
|
||||
UpdateAppSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 皮肤是否与系统同步
|
||||
/// </summary>
|
||||
private bool _syncWithSystem;
|
||||
public bool SyncWithSystem
|
||||
{
|
||||
get => _syncWithSystem;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _syncWithSystem, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(IsNotSyncWithSystem));
|
||||
if (value)
|
||||
{
|
||||
SkinType = GetSkinTypeBySystem();
|
||||
UpdateSkin(SkinType.Value);
|
||||
}
|
||||
UpdateAppSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsNotSyncWithSystem => !SyncWithSystem;
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示TabControl
|
||||
/// </summary>
|
||||
private bool _isTabControlVisible = true;
|
||||
|
||||
public bool IsTabControlVisible
|
||||
{
|
||||
get => _isTabControlVisible;
|
||||
set {
|
||||
if (SetProperty(ref _isTabControlVisible, value))
|
||||
{
|
||||
UpdateAppSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取默认皮肤类型
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private HcSkinType GetDefaultSkinType()
|
||||
{
|
||||
return SyncWithSystem ? GetSkinTypeBySystem() : (HcSkinType)Properties.AppSettings.Default.SkinType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据系统主题配置获取对应皮肤类型
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static HcSkinType GetSkinTypeBySystem()
|
||||
{
|
||||
return SystemHelper.DetermineIfInLightThemeMode()
|
||||
? HcSkinType.Default
|
||||
: HcSkinType.Dark;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从ResourceDictionary中获取皮肤类型
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static HcSkinType GetSkinType()
|
||||
{
|
||||
var _theme = Application.Current.Resources.MergedDictionaries.OfType<Theme>().FirstOrDefault();
|
||||
if (_theme != null)
|
||||
{
|
||||
return _theme?.Skin ?? HcSkinType.Default;
|
||||
}
|
||||
var skin = Application.Current.Resources.MergedDictionaries
|
||||
.Where(it => it.Source.OriginalString.StartsWith("pack://application:,,,/HandyControl;component/Themes/Skin"))
|
||||
.FirstOrDefault();
|
||||
if (skin != null)
|
||||
{
|
||||
string OriginalString = skin.Source.OriginalString;
|
||||
// 4:Skin的长度
|
||||
int skinStart = OriginalString.IndexOf("Skin") + 4;
|
||||
int dotIndex = OriginalString.LastIndexOf('.');
|
||||
string skinName = OriginalString.Substring(skinStart, dotIndex - skinStart);
|
||||
return (HcSkinType)Enum.Parse(typeof(HcSkinType), skinName);
|
||||
}
|
||||
return HcSkinType.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新皮肤
|
||||
/// </summary>
|
||||
/// <param name="skinType"></param>
|
||||
public static void UpdateSkin(HcSkinType skinType)
|
||||
{
|
||||
// 配置方式:<hc:Theme />
|
||||
var theme = Application.Current.Resources.MergedDictionaries.OfType<Theme>().FirstOrDefault();
|
||||
if (theme != null)
|
||||
{
|
||||
//theme.Skin = skin;
|
||||
theme.MergedDictionaries[0].Source = ResourceHelper.GetSkin(skinType).Source;
|
||||
theme.MergedDictionaries[1].Source = ResourceHelper.GetStandaloneTheme().Source;
|
||||
AddResourceDictionary(skinType);
|
||||
return;
|
||||
}
|
||||
|
||||
// 配置方式:<ResourceDictionary />
|
||||
var skins = Application.Current.Resources.MergedDictionaries
|
||||
.Where(it => it.Source.OriginalString.StartsWith("pack://application:,,,/HandyControl;component/Themes/"))
|
||||
.ToList();
|
||||
if (skins == null || skins.Count < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
skins[0].Source = ResourceHelper.GetSkin(skinType).Source;
|
||||
skins[1].Source = ResourceHelper.GetStandaloneTheme().Source;
|
||||
|
||||
// 添加除HandyControl之外的资源字典
|
||||
AddResourceDictionary(skinType);
|
||||
|
||||
ContainerLocator.Container.Resolve<IEventAggregator>().GetEvent<ThemeChangedEvent>().Publish(skinType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件完整路径
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string GetFilePath()
|
||||
{
|
||||
string filePathSuffix = string.Format(CommonConst.AppSettingsFilePath, AppSession.CurrentUser!.Account);
|
||||
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, filePathSuffix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新系统设置
|
||||
/// 默认配置由MainWindowViewModel保存
|
||||
/// </summary>
|
||||
/// <param name="appSettingsViewModel">系统设置VM</param>
|
||||
public void UpdateAppSettings()
|
||||
{
|
||||
var filePath = GetFilePath();
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
SaveAppSettings(filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存系统设置
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
public void SaveAppSettings(string? filePath = null)
|
||||
{
|
||||
filePath ??= GetFilePath();
|
||||
var directory = Path.GetDirectoryName(filePath);
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory!);
|
||||
}
|
||||
var appSettings = this.Adapt<AppSettings>();
|
||||
var json = JsonConvert.SerializeObject(appSettings, Formatting.Indented);
|
||||
// 保存到文件
|
||||
File.WriteAllText(filePath, json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加资源字典
|
||||
/// </summary>
|
||||
/// <param name="skinType"></param>
|
||||
private static void AddResourceDictionary(HcSkinType skinType)
|
||||
{
|
||||
// 先移除上一次主题添加的资源字典,否则会影响当前主题
|
||||
RemoveResourceDictionaryBySource("/Resources/Styles/HandyControl/Colors.xaml");
|
||||
RemoveResourceDictionaryBySource("/Resources/Styles/HandyControl/ColorsDark.xaml");
|
||||
RemoveResourceDictionaryBySource("/Resources/Styles/HandyControl/ColorsViolet.xaml");
|
||||
RemoveResourceDictionaryBySource("/Resources/Styles/HandyControl/Brushes.xaml");
|
||||
if (skinType == HcSkinType.Default)
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Add(new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("/Resources/Styles/HandyControl/Colors.xaml", UriKind.Relative)
|
||||
});
|
||||
} else if (skinType == HcSkinType.Dark)
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Add(new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("/Resources/Styles/HandyControl/ColorsDark.xaml", UriKind.Relative)
|
||||
});
|
||||
} else if (skinType == HcSkinType.Violet)
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Add(new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("/Resources/Styles/HandyControl/ColorsViolet.xaml", UriKind.Relative)
|
||||
});
|
||||
}
|
||||
Application.Current.Resources.MergedDictionaries.Add(new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("/Resources/Styles/HandyControl/Brushes.xaml", UriKind.Relative)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除资源字典
|
||||
/// </summary>
|
||||
/// <param name="sourceEndsWith"></param>
|
||||
private static void RemoveResourceDictionaryBySource(string sourceEndsWith)
|
||||
{
|
||||
var dictToRemove = Application.Current.Resources.MergedDictionaries
|
||||
.FirstOrDefault(d => d.Source?.OriginalString?.EndsWith(sourceEndsWith) == true);
|
||||
|
||||
if (dictToRemove != null)
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Remove(dictToRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
592
yy-admin-master/YY.Admin/ViewModels/Base/BaseViewModel.cs
Normal file
592
yy-admin-master/YY.Admin/ViewModels/Base/BaseViewModel.cs
Normal file
@@ -0,0 +1,592 @@
|
||||
using HandyControl.Data;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Const;
|
||||
using YY.Admin.Core.EventBus;
|
||||
using YY.Admin.Core.Session;
|
||||
using YY.Admin.Services.Service.Auth;
|
||||
using YY.Admin.Views;
|
||||
|
||||
namespace YY.Admin.ViewModels
|
||||
{
|
||||
public class BaseViewModel : BindableBase, IDestructible
|
||||
{
|
||||
protected bool _isLoading;
|
||||
private string _title = string.Empty;
|
||||
|
||||
// 添加Token检查定时器
|
||||
private static DispatcherTimer? _tokenCheckTimer;
|
||||
/// <summary>
|
||||
///加载
|
||||
/// </summary>
|
||||
public bool IsLoading
|
||||
{
|
||||
get => _isLoading;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isLoading, value))
|
||||
{
|
||||
// 触发一个虚拟方法,让派生类可以响应
|
||||
OnIsLoadingChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsNotLoading => !IsLoading;
|
||||
|
||||
/// <summary>
|
||||
///标题
|
||||
/// </summary>
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
set => SetProperty(ref _title, value);
|
||||
}
|
||||
private readonly IDialogService _dialogService;
|
||||
protected readonly IRegionManager _regionManager;
|
||||
/// <summary>
|
||||
/// 日志对象
|
||||
/// </summary>
|
||||
|
||||
protected ILoggerService _logger;
|
||||
/// <summary>
|
||||
/// 依赖注入容器
|
||||
/// </summary>
|
||||
protected IContainerExtension _container { get; }
|
||||
/// <summary>
|
||||
/// 事件汇总器,用于发布或订阅事件
|
||||
/// </summary>
|
||||
protected IEventAggregator _eventAggregator;
|
||||
|
||||
/// <summary>
|
||||
/// 当前已登录用户信息
|
||||
/// </summary>
|
||||
protected static UserContext? _userContext { get; set; }
|
||||
|
||||
private SkinType _skinType;
|
||||
|
||||
public SkinType SkinType { get => _skinType; set => SetProperty(ref _skinType, value); }
|
||||
|
||||
private SubscriptionToken? _themeChangedEventToken;
|
||||
|
||||
protected BaseViewModel(IContainerExtension container, IRegionManager regionManager)
|
||||
{
|
||||
_container = container;
|
||||
_logger = container.Resolve<ILoggerService>();
|
||||
_eventAggregator = container.Resolve<IEventAggregator>();
|
||||
this._dialogService = container.Resolve<IDialogService>();
|
||||
this._regionManager = regionManager;
|
||||
|
||||
_themeChangedEventToken = _eventAggregator.GetEvent<ThemeChangedEvent>().Subscribe(skinType => SkinType = skinType);
|
||||
|
||||
}
|
||||
|
||||
#region 用户操作
|
||||
// 启动定时器的方法
|
||||
public static void StartTokenCheckTimer()
|
||||
{
|
||||
if (_tokenCheckTimer == null)
|
||||
{
|
||||
_tokenCheckTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMinutes(1)
|
||||
};
|
||||
_tokenCheckTimer.Tick += CheckTokenExpiration;
|
||||
|
||||
// 捕获全局用户输入事件
|
||||
EventManager.RegisterClassHandler(typeof(Window),
|
||||
UIElement.PreviewMouseDownEvent,
|
||||
new MouseButtonEventHandler(OnUserActivity));
|
||||
EventManager.RegisterClassHandler(typeof(Window),
|
||||
UIElement.PreviewKeyDownEvent,
|
||||
new KeyEventHandler(OnUserActivity));
|
||||
}
|
||||
|
||||
if (!_tokenCheckTimer.IsEnabled)
|
||||
{
|
||||
_tokenCheckTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
// 停止定时器的方法
|
||||
public static void StopTokenCheckTimer()
|
||||
{
|
||||
if (_tokenCheckTimer != null && _tokenCheckTimer.IsEnabled)
|
||||
{
|
||||
_tokenCheckTimer.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnUserActivity(object sender, EventArgs e)
|
||||
{
|
||||
var authService = ContainerLocator.Current.Resolve<ISysAuthService>();
|
||||
authService.RefreshToken(UserContext?.Token?.AccessToken);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
///定时器检查方法
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private static void CheckTokenExpiration(object? sender, EventArgs e)
|
||||
{
|
||||
// 确保在主线程执行
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (UserContext == null || UserContext.Token == null)
|
||||
{
|
||||
// 如果没有用户信息,停止定时器
|
||||
StopTokenCheckTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
var authService = ContainerLocator.Current.Resolve<ISysAuthService>();
|
||||
if (!authService.ValidateToken(UserContext.Token.AccessToken))
|
||||
{
|
||||
// 停止定时器防止重复触发
|
||||
StopTokenCheckTimer();
|
||||
|
||||
// 显示过期提示
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var currentWindow = Application.Current.MainWindow;
|
||||
if (currentWindow != null)
|
||||
{
|
||||
var viewModel = currentWindow.DataContext as BaseViewModel;
|
||||
viewModel?.ForceLogout("您的登录已过期,请重新登录");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
/// <summary>
|
||||
/// 强制退出方法
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public async void ForceLogout(string message)
|
||||
{
|
||||
// 先显示对话框
|
||||
await ShowAlertAsync(message);
|
||||
|
||||
// 发布登出事件
|
||||
_eventAggregator.GetEvent<SysUserEvents.LoginOutEvent>().Publish(AppSession.CurrentUser!);
|
||||
|
||||
Logout();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 登出
|
||||
/// </summary>
|
||||
public void Logout()
|
||||
{
|
||||
// 停止定时器
|
||||
StopTokenCheckTimer();
|
||||
// 清除用户上下文
|
||||
ClearUserContext();
|
||||
// 再执行退出操作
|
||||
var authService = _container.Resolve<ISysAuthService>();
|
||||
authService.LogoutAsync();
|
||||
|
||||
// 当前窗口
|
||||
var mainWindow = Application.Current.MainWindow;
|
||||
// 跳转到登录页
|
||||
var loginWindow = _container.Resolve<LoginWindow>();
|
||||
Application.Current.MainWindow = loginWindow;
|
||||
loginWindow.Show();
|
||||
mainWindow.Close();
|
||||
|
||||
// 移除所有区域
|
||||
RemoveAllRegion();
|
||||
}
|
||||
|
||||
public static UserContext? UserContext
|
||||
{
|
||||
get => _userContext;
|
||||
private set
|
||||
{
|
||||
if (_userContext != value)
|
||||
{
|
||||
_userContext = value;
|
||||
// 通知静态属性变化(需要额外实现)
|
||||
StaticPropertyChanged?.Invoke(null, new PropertyChangedEventArgs(nameof(UserContext)));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 静态属性变更通知事件
|
||||
public static event EventHandler<PropertyChangedEventArgs>? StaticPropertyChanged;
|
||||
/// <summary>
|
||||
/// 设置用户上下文
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="token"></param>
|
||||
public static void SetUserContext(SysUser user, UserToken token)
|
||||
{
|
||||
UserContext = new UserContext
|
||||
{
|
||||
UserId = user.Id,
|
||||
TenantId = user.TenantId!.Value,
|
||||
Account = user.Account,
|
||||
AccountType = user.AccountType,
|
||||
RealName = user.RealName,
|
||||
IsSuperAdmin = user.AccountType == AccountTypeEnum.SuperAdmin,
|
||||
OrgId = user.OrgId,
|
||||
Token = token
|
||||
};
|
||||
}
|
||||
/// <summary>
|
||||
/// 清除用户上下文
|
||||
/// </summary>
|
||||
public static void ClearUserContext()
|
||||
{
|
||||
UserContext = null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 导航
|
||||
/// <summary>
|
||||
/// 导航到指定Page
|
||||
/// </summary>
|
||||
/// <param name="regionName">区域名称</param>
|
||||
/// <param name="target">目标Page名称</param>
|
||||
/// <param name="navigationCallback">导航回调函数</param>
|
||||
protected void RequestNavigate(string regionName, string target, Action<NavigationResult> navigationCallback = null)
|
||||
{
|
||||
IRegion region = _regionManager.Regions[regionName];
|
||||
if (region == null) return;
|
||||
region.RemoveAll();
|
||||
if (navigationCallback != null)
|
||||
region.RequestNavigate(target, navigationCallback);
|
||||
else
|
||||
region.RequestNavigate(target);
|
||||
}
|
||||
|
||||
protected void SetRegionManager(DependencyObject target)
|
||||
{
|
||||
RegionManager.SetRegionManager(target, _regionManager);
|
||||
}
|
||||
/// <summary>
|
||||
/// 安全导航到指定视图
|
||||
/// </summary>
|
||||
protected void SafeNavigate(string regionName, string targetView, NavigationParameters parameters = null)
|
||||
{
|
||||
ExecuteWithExceptionHandling(() =>
|
||||
{
|
||||
// 导航页面
|
||||
_regionManager.RequestNavigate(regionName, targetView, parameters);
|
||||
});
|
||||
}
|
||||
protected void ExecuteWithExceptionHandling(Action action)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleException(Exception ex)
|
||||
{
|
||||
LogError("操作发生异常", ex);
|
||||
// ShowErrorDialog($"操作异常: {ex.Message}");
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 弹出框
|
||||
/// <summary>
|
||||
/// 弹框提示
|
||||
/// </summary>
|
||||
/// <param name="message">消息内容</param>
|
||||
/// <param name="callback">回调函数</param>
|
||||
protected void Alert(string message, Action<IDialogResult>? callback = null)
|
||||
{
|
||||
_dialogService.ShowDialog("AlertDialog", new DialogParameters($"message={message}"), callback);
|
||||
}
|
||||
protected Task ShowAlertAsync(string message)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
Alert(message,result => tcs.SetResult(true));
|
||||
return tcs.Task;
|
||||
}
|
||||
/// <summary>
|
||||
/// 弹出消息提示框,1秒钟自动关闭
|
||||
/// </summary>
|
||||
/// <param name="message">消息内容</param>
|
||||
/// <param name="messageType">消息类型</param>
|
||||
/// <param name="callback">回调函数</param>
|
||||
protected void AlertPopup(string message, MessageTypeEnum messageType = MessageTypeEnum.Success, Action<IDialogResult> callback = null)
|
||||
{
|
||||
switch (messageType)
|
||||
{
|
||||
case MessageTypeEnum.Success:
|
||||
_dialogService.ShowDialog("SuccessDialog", new DialogParameters($"message={message}"), callback);
|
||||
break;
|
||||
case MessageTypeEnum.Error:
|
||||
_dialogService.ShowDialog("ErrorDialog", new DialogParameters($"message={message}"), callback);
|
||||
break;
|
||||
case MessageTypeEnum.Warning:
|
||||
_dialogService.ShowDialog("WarningDialog", new DialogParameters($"message={message}"), callback);
|
||||
break;
|
||||
default:
|
||||
_dialogService.ShowDialog("SuccessDialog", new DialogParameters($"message={message}"), callback);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确认框提示
|
||||
/// </summary>
|
||||
/// <param name="message">确认框消息</param>
|
||||
/// <param name="callback">回调函数</param>
|
||||
protected void Confirm(string message, Action<IDialogResult>? callback = null)
|
||||
{
|
||||
_dialogService.ShowDialog("ConfirmDialog", new DialogParameters($"message={message}"), callback);
|
||||
}
|
||||
/// <summary>
|
||||
/// 异步确认框
|
||||
/// </summary>
|
||||
/// <param name="message">确认消息</param>
|
||||
/// <returns>用户是否确认</returns>
|
||||
protected Task<bool> ConfirmAsync(string message)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
Confirm(message, result =>
|
||||
{
|
||||
// 从 IDialogResult 中提取用户选择
|
||||
var userConfirmed = result.Result == ButtonResult.Yes;
|
||||
tcs.SetResult(userConfirmed);
|
||||
});
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 日志功能封装
|
||||
protected void LogInfo(string message, [CallerMemberName] string caller = "")
|
||||
=> _logger.Information($"[{GetType().Name}.{caller}] {message}");
|
||||
|
||||
protected void LogWarning(string message, [CallerMemberName] string caller = "")
|
||||
=> _logger.Warning($"[{GetType().Name}.{caller}] {message}");
|
||||
|
||||
protected void LogError(string message, Exception ex = null, [CallerMemberName] string caller = "")
|
||||
=> _logger.Error($"[{GetType().Name}.{caller}] {message}", ex);
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 异常处理封装
|
||||
/// </summary>
|
||||
/// <param name="asyncAction"></param>
|
||||
/// <param name="onFinally"></param>
|
||||
/// <returns></returns>
|
||||
protected async Task ExecuteAsync(Func<Task> asyncAction, Action? onFinally = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
await asyncAction();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
onFinally?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口管理
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
protected void SwitchMainWindow<T>() where T : Window
|
||||
{
|
||||
ExecuteWithExceptionHandling(() =>
|
||||
{
|
||||
var currentWindow = Application.Current.MainWindow;
|
||||
var newWindow = _container.Resolve<T>();
|
||||
|
||||
Application.Current.MainWindow?.Hide();
|
||||
Application.Current.MainWindow = newWindow;
|
||||
newWindow.Show();
|
||||
|
||||
currentWindow?.Close();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 资源管理
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="resourceKey"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ResourceNotFoundException"></exception>
|
||||
protected T GetResource<T>(string resourceKey)
|
||||
{
|
||||
if (Application.Current.TryFindResource(resourceKey) is T resource)
|
||||
{
|
||||
return resource;
|
||||
}
|
||||
throw new ResourceNotFoundException($"资源未找到: {resourceKey}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除区域,包括清空区域内的内容,导航日志
|
||||
/// </summary>
|
||||
/// <param name="regionName">区域名称</param>
|
||||
protected void RemoveRegion(string regionName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(regionName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!_regionManager.Regions.ContainsRegionWithName(regionName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var region = _regionManager.Regions[regionName];
|
||||
RemoveRegion(region);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除区域,包括清空区域内的内容,导航日志
|
||||
/// </summary>
|
||||
/// <param name="region"></param>
|
||||
protected void RemoveRegion(IRegion region)
|
||||
{
|
||||
if (region == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 清空区域内容
|
||||
region.RemoveAll();
|
||||
|
||||
// 清空导航历史
|
||||
region.NavigationService?.Journal?.Clear();
|
||||
|
||||
// 从区域管理器中移除区域
|
||||
_regionManager.Regions.Remove(region.Name);
|
||||
|
||||
_logger.Debug($"移除区域{region.Name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除所有区域
|
||||
/// </summary>
|
||||
protected void RemoveAllRegion()
|
||||
{
|
||||
foreach (var region in _regionManager.Regions.ToList())
|
||||
{
|
||||
RemoveRegion(region);
|
||||
}
|
||||
_logger.Debug($"移除所有区域");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除区域视图
|
||||
/// </summary>
|
||||
/// <param name="regionName">区域名称</param>
|
||||
/// <param name="viewName">视图名称</param>
|
||||
protected void RemoveView(string regionName, string? viewName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(regionName) || string.IsNullOrEmpty(viewName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!_regionManager.Regions.ContainsRegionWithName(regionName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var region = _regionManager.Regions[regionName];
|
||||
// 查找并移除指定的视图
|
||||
var viewToRemove = region.Views.FirstOrDefault(v =>
|
||||
{
|
||||
// 根据视图名称或类型来匹配
|
||||
var viewType = v.GetType();
|
||||
return viewType.Name == viewName || viewType.Name == viewName + "View";
|
||||
});
|
||||
|
||||
if (viewToRemove != null)
|
||||
{
|
||||
region.Remove(viewToRemove);
|
||||
_logger.Debug($"从区域{regionName}移除视图{viewName}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除区域ContentRegion视图
|
||||
/// </summary>
|
||||
/// <param name="viewName"></param>
|
||||
protected void RemoveContentRegionView(string? viewName)
|
||||
{
|
||||
RemoveView(CommonConst.ContentRegion, viewName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除区域所有视图
|
||||
/// </summary>
|
||||
/// <param name="regionName">区域名称</param>
|
||||
protected void RemoveAllView(string regionName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(regionName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!_regionManager.Regions.ContainsRegionWithName(regionName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var region = _regionManager.Regions[regionName];
|
||||
region.RemoveAll();
|
||||
_logger.Debug($"移除区域{regionName}所有视图");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当 IsLoading 变化时的回调方法
|
||||
/// </summary>
|
||||
protected virtual void OnIsLoadingChanged()
|
||||
{
|
||||
RaisePropertyChanged(nameof(IsNotLoading));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理资源
|
||||
/// </summary>
|
||||
protected virtual void CleanUp()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空资源
|
||||
/// 执行时机:从Region移除View 或 导航到另一个View时
|
||||
/// </summary>
|
||||
public void Destroy()
|
||||
{
|
||||
if (_themeChangedEventToken != null)
|
||||
{
|
||||
_eventAggregator.GetEvent<ThemeChangedEvent>().Unsubscribe(_themeChangedEventToken);
|
||||
_themeChangedEventToken = null;
|
||||
}
|
||||
// 清理派生类的资源
|
||||
CleanUp();
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义异常类型
|
||||
public class ResourceNotFoundException : Exception
|
||||
{
|
||||
public ResourceNotFoundException(string message) : base(message) { }
|
||||
}
|
||||
}
|
||||
376
yy-admin-master/YY.Admin/ViewModels/Control/MenuTreeViewModel.cs
Normal file
376
yy-admin-master/YY.Admin/ViewModels/Control/MenuTreeViewModel.cs
Normal file
@@ -0,0 +1,376 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Util;
|
||||
using YY.Admin.Event;
|
||||
using YY.Admin.Module;
|
||||
using YY.Admin.Services;
|
||||
using YY.Admin.Services.Service.Menu;
|
||||
|
||||
namespace YY.Admin.ViewModels.Control
|
||||
{
|
||||
/// <summary>
|
||||
/// 菜单选项
|
||||
/// </summary>
|
||||
public class MenuItem : TabSource
|
||||
{
|
||||
public MenuItem? Parent { get; set; } // 父节点引用
|
||||
public ObservableCollection<MenuItem> Children { get; set; } = [];
|
||||
|
||||
|
||||
private bool _isExpanded;
|
||||
|
||||
public bool IsExpanded
|
||||
{
|
||||
get => _isExpanded;
|
||||
set => SetProperty(ref _isExpanded, value);
|
||||
}
|
||||
}
|
||||
|
||||
public class MenuTreeViewModel : BaseViewModel
|
||||
{
|
||||
private static readonly Dictionary<string, string> RouteToViewMap = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// 已实现页面:仪表盘
|
||||
["DashboardView"] = "DashboardView",
|
||||
["/dashboard"] = "DashboardView",
|
||||
["/dashboard/index"] = "DashboardView",
|
||||
["/home/index"] = "DashboardView",
|
||||
["dashboard"] = "DashboardView",
|
||||
["home"] = "DashboardView",
|
||||
|
||||
// 已实现页面:账号管理
|
||||
["UserManagementView"] = "UserManagementView",
|
||||
["/system/user"] = "UserManagementView",
|
||||
["/system/user/index"] = "UserManagementView",
|
||||
["sysUser"] = "UserManagementView",
|
||||
|
||||
// 已实现页面:数据字典
|
||||
["DataDictionaryManagementView"] = "DataDictionaryManagementView",
|
||||
["/system/dict"] = "DataDictionaryManagementView",
|
||||
["/system/dict/index"] = "DataDictionaryManagementView",
|
||||
["/platform/dict"] = "DataDictionaryManagementView",
|
||||
["sysDict"] = "DataDictionaryManagementView",
|
||||
|
||||
// 已实现页面:角色管理
|
||||
["RoleManagementView"] = "RoleManagementView",
|
||||
["/system/role"] = "RoleManagementView",
|
||||
["/system/role/index"] = "RoleManagementView",
|
||||
["sysRole"] = "RoleManagementView",
|
||||
|
||||
// 已实现页面:租户管理
|
||||
["TenantManagementView"] = "TenantManagementView",
|
||||
["/system/tenant"] = "TenantManagementView",
|
||||
["/system/tenant/index"] = "TenantManagementView",
|
||||
["/platform/tenant"] = "TenantManagementView",
|
||||
["sysTenant"] = "TenantManagementView"
|
||||
};
|
||||
|
||||
private MenuItem? _selectedMenuItem;
|
||||
|
||||
public MenuItem? SelectedMenuItem
|
||||
{
|
||||
get => _selectedMenuItem;
|
||||
set => SetProperty(ref _selectedMenuItem, value);
|
||||
}
|
||||
|
||||
private readonly ISysMenuService _sysMenuService;
|
||||
|
||||
public ObservableCollection<MenuItem> MenuItems { get; } = [];
|
||||
|
||||
public DelegateCommand<MenuItem> NavigateCommand { get; }
|
||||
|
||||
private SubscriptionToken? tabSelectedToken;
|
||||
private SubscriptionToken? tabClosedToken;
|
||||
|
||||
public MenuTreeViewModel(
|
||||
ISysMenuService sysMenuService,
|
||||
IContainerExtension _container,
|
||||
IRegionManager regionManager) : base(_container, regionManager)
|
||||
{
|
||||
_sysMenuService = sysMenuService;
|
||||
|
||||
// 异步初始化菜单
|
||||
LoadMenuAsync();
|
||||
|
||||
NavigateCommand = new DelegateCommand<MenuItem>(OpenOrActivateTab);
|
||||
|
||||
// 订阅事件
|
||||
tabSelectedToken = _eventAggregator.GetEvent<TabSelectedEvent>().Subscribe(OnTabSelected);
|
||||
tabClosedToken = _eventAggregator.GetEvent<TabClosedEvent>().Subscribe(OnTabClosed);
|
||||
}
|
||||
|
||||
public void OpenOrActivateTab(MenuItem menuItem)
|
||||
{
|
||||
// 发布事件
|
||||
_eventAggregator.GetEvent<TabSourceSelectedEvent>().Publish(menuItem);
|
||||
}
|
||||
|
||||
private void OnTabSelected(TabItemModel tab)
|
||||
{
|
||||
if (tab.TabSource is MenuItem menuItem)
|
||||
{
|
||||
// 取消上一个选中菜单选中状态
|
||||
SelectedMenuItem?.IsSelected = false;
|
||||
|
||||
SelectedMenuItem = menuItem;
|
||||
|
||||
// 设置菜单选中
|
||||
SelectedMenuItem.IsSelected = true;
|
||||
|
||||
// 展开父节点
|
||||
ToggleParents(SelectedMenuItem, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTabClosed(TabItemModel tab)
|
||||
{
|
||||
if (tab.TabSource is MenuItem menuItem)
|
||||
{
|
||||
// 折叠父节点
|
||||
ToggleParents(menuItem, false);
|
||||
|
||||
// 取消菜单选中
|
||||
menuItem?.IsSelected = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步加载菜单【后续使用缓存以及权限管理】
|
||||
/// </summary>
|
||||
private async void LoadMenuAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 异步获取菜单数据
|
||||
var menuTree = await _sysMenuService.GetLoginMenuTree();
|
||||
// 转换菜单数据
|
||||
ConvertMenuTreeToViewModel(menuTree);
|
||||
// 默认导航
|
||||
ScheduleDefaultNavigation();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"菜单加载失败: {ex.Message}", ex);
|
||||
// 显示错误菜单项
|
||||
MenuItems.Add(new MenuItem
|
||||
{
|
||||
Name = "菜单加载失败",
|
||||
Icon = "ErrorOutline",
|
||||
ViewName = "ErrorView",
|
||||
Children = { new MenuItem { Name = "点击重试", Icon = "Refresh" } }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将服务层菜单树转换为视图模型
|
||||
/// </summary>
|
||||
private void ConvertMenuTreeToViewModel(List<MenuOutput> menuTree)
|
||||
{
|
||||
// 过滤并排序菜单项:只包含目录和菜单类型,排除按钮类型,并按排序号排序
|
||||
var rootMenus = menuTree
|
||||
.Where(m => m.Type == MenuTypeEnum.Dir || m.Type == MenuTypeEnum.Menu)
|
||||
.Where(m => m.Status == StatusEnum.Enable) // 只包含启用状态的菜单
|
||||
.Where(m => !(m?.IsHide ?? false)) // 排除隐藏的菜单
|
||||
.OrderBy(m => m.OrderNo)
|
||||
.ToList();
|
||||
// 递归转换菜单项
|
||||
void ConvertMenu(MenuOutput source, MenuItem target, MenuItem? parent = null)
|
||||
{
|
||||
target.Name = source?.Title ?? source?.Name;
|
||||
target.Icon = ConvertHtmlEntityToUnicode(source?.Icon ?? "");
|
||||
target.ViewName = ResolveViewName(source); // 将菜单路由映射到已注册的WPF视图
|
||||
target.Parent = parent; // 设置父节点
|
||||
|
||||
// 添加子菜单(如果有)
|
||||
if (source.Children != null && source.Children.Any())
|
||||
{
|
||||
// 过滤并排序子菜单
|
||||
var childMenus = source.Children
|
||||
.Where(c => c.Type == MenuTypeEnum.Dir || c.Type == MenuTypeEnum.Menu) // 子菜单支持目录和菜单两种类型
|
||||
.Where(c => c.Status == StatusEnum.Enable)
|
||||
.Where(c => !(c?.IsHide ?? false))
|
||||
.OrderBy(c => c.OrderNo)
|
||||
.ToList();
|
||||
|
||||
foreach (var child in childMenus)
|
||||
{
|
||||
var childItem = new MenuItem();
|
||||
ConvertMenu(child, childItem, target);
|
||||
target.Children.Add(childItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 处理每个根菜单
|
||||
foreach (var root in rootMenus)
|
||||
{
|
||||
var rootItem = new MenuItem();
|
||||
ConvertMenu(root, rootItem);
|
||||
|
||||
// 如果根菜单是目录但没有子菜单,则不显示
|
||||
if (root.Type == MenuTypeEnum.Dir && !rootItem.Children.Any())
|
||||
continue;
|
||||
|
||||
MenuItems.Add(rootItem);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析菜单对应的视图名称
|
||||
/// </summary>
|
||||
private string? ResolveViewName(MenuOutput? menu)
|
||||
{
|
||||
if (menu == null)
|
||||
return null;
|
||||
|
||||
// 目录节点不参与内容区导航
|
||||
if (menu.Type == MenuTypeEnum.Dir)
|
||||
return null;
|
||||
|
||||
// 依次尝试 Path / Component / Name / Title,兼容不同来源的菜单数据
|
||||
var candidates = new[]
|
||||
{
|
||||
menu.Path,
|
||||
menu.Component,
|
||||
menu.Name,
|
||||
menu.Title
|
||||
};
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(candidate))
|
||||
continue;
|
||||
|
||||
if (RouteToViewMap.TryGetValue(candidate.Trim(), out var viewName))
|
||||
return viewName;
|
||||
}
|
||||
|
||||
// 保留原始Path,若未注册将统一展示NotFoundView
|
||||
return menu.Path;
|
||||
}
|
||||
|
||||
private string ConvertHtmlEntityToUnicode(string htmlEntity)
|
||||
{
|
||||
if (string.IsNullOrEmpty(htmlEntity))
|
||||
return "\ue7c6"; // 默认图标
|
||||
|
||||
return StringUtil.ConvertHtmlEntityToUnicode(htmlEntity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安排默认导航
|
||||
/// </summary>
|
||||
private void ScheduleDefaultNavigation()
|
||||
{
|
||||
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 默认菜单
|
||||
var defaultMenuItem = GetFirstLeaf(MenuItems.FirstOrDefault());
|
||||
if (defaultMenuItem != null)
|
||||
{
|
||||
// Tab不允许关闭
|
||||
defaultMenuItem.IsClosable = false;
|
||||
|
||||
// 导航菜单
|
||||
OpenOrActivateTab(defaultMenuItem);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"默认导航失败: {ex.Message}", ex);
|
||||
}
|
||||
}), DispatcherPriority.ApplicationIdle);
|
||||
}
|
||||
|
||||
private MenuItem? GetFirstLeaf(MenuItem? menu)
|
||||
{
|
||||
if (menu == null)
|
||||
return null;
|
||||
|
||||
if (menu.Children == null || menu.Children.Count == 0)
|
||||
return menu; // 自己就是叶子节点
|
||||
|
||||
// 递归向下找第一个叶子
|
||||
return GetFirstLeaf(menu.Children.FirstOrDefault());
|
||||
}
|
||||
|
||||
public void ToggleParents(MenuItem? item, bool IsExpanded)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var parent = item.Parent;
|
||||
while (parent != null)
|
||||
{
|
||||
parent.IsExpanded = IsExpanded; // 展开父节点
|
||||
if (!IsExpanded)
|
||||
{
|
||||
parent.IsSelected = IsExpanded;
|
||||
}
|
||||
parent = parent.Parent;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空资源
|
||||
/// </summary>
|
||||
protected override void CleanUp()
|
||||
{
|
||||
if (tabSelectedToken != null)
|
||||
{
|
||||
_eventAggregator
|
||||
.GetEvent<TabSelectedEvent>()
|
||||
.Unsubscribe(tabSelectedToken);
|
||||
tabSelectedToken = null;
|
||||
}
|
||||
|
||||
if (tabClosedToken != null)
|
||||
{
|
||||
_eventAggregator
|
||||
.GetEvent<TabClosedEvent>()
|
||||
.Unsubscribe(tabClosedToken);
|
||||
tabClosedToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据ViewName同步选中对应的菜单项
|
||||
/// </summary>
|
||||
//private MenuItem? GetSelectedMenuItem(string? viewName)
|
||||
//{
|
||||
// if (string.IsNullOrEmpty(viewName))
|
||||
// return null;
|
||||
|
||||
// // 递归查找匹配的菜单项
|
||||
// return FindMenuItemByViewName(MenuItems, viewName);
|
||||
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// 递归查找匹配ViewName的菜单项
|
||||
/// </summary>
|
||||
//private MenuItem? FindMenuItemByViewName(ObservableCollection<MenuItem> menuItems, string viewName)
|
||||
//{
|
||||
// foreach (var menuItem in menuItems)
|
||||
// {
|
||||
// // 如果当前菜单项匹配
|
||||
// if (menuItem.ViewName == viewName)
|
||||
// return menuItem;
|
||||
|
||||
// // 递归查找子菜单
|
||||
// if (menuItem.Children?.Count > 0)
|
||||
// {
|
||||
// var found = FindMenuItemByViewName(menuItem.Children, viewName);
|
||||
// if (found != null)
|
||||
// return found;
|
||||
// }
|
||||
// }
|
||||
// return null;
|
||||
//}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.ViewModels.Control
|
||||
{
|
||||
public class PaginationDataGridViewModel<T> : BindableBase
|
||||
{
|
||||
private int _pageIndex = 1;
|
||||
private int _dataCountPerPage = 10;
|
||||
private int _totalCount;
|
||||
private int _maxPageCount;
|
||||
private ObservableCollection<T> _data;
|
||||
//private string _pageInfo;
|
||||
|
||||
public int MyProperty { get; private set; }
|
||||
|
||||
public int PageIndex
|
||||
{
|
||||
get => _pageIndex;
|
||||
set => SetProperty(ref _pageIndex, value);
|
||||
}
|
||||
|
||||
public int DataCountPerPage
|
||||
{
|
||||
get => _dataCountPerPage;
|
||||
set => SetProperty(ref _dataCountPerPage, value);
|
||||
}
|
||||
|
||||
public int TotalCount
|
||||
{
|
||||
get => _totalCount;
|
||||
set => SetProperty(ref _totalCount, value);
|
||||
}
|
||||
|
||||
public int MaxPageCount
|
||||
{
|
||||
get => _maxPageCount;
|
||||
set => SetProperty(ref _maxPageCount, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<T> Data
|
||||
{
|
||||
get => _data;
|
||||
set => SetProperty(ref _data, value);
|
||||
}
|
||||
|
||||
//public string PageInfo
|
||||
//{
|
||||
// get => _pageInfo;
|
||||
// set => SetProperty(ref _pageInfo, value);
|
||||
//}
|
||||
|
||||
// 页面大小选项
|
||||
public Dictionary<int, string> PageSizes { get; } = new Dictionary<int, string>
|
||||
{
|
||||
{ 10, "10条/页" },
|
||||
{ 20, "20条/页" },
|
||||
{ 30, "30条/页" },
|
||||
{ 40, "40条/页" },
|
||||
{ 50, "50条/页" },
|
||||
{ 100, "100条/页" }
|
||||
};
|
||||
|
||||
public DelegateCommand PageUpdatedCmd { get; private set; }
|
||||
|
||||
public DelegateCommand PageSizeUpdatedCmd { get; private set; }
|
||||
|
||||
private Func<Task<(IEnumerable<T> data, int totalCount)>> _fetchData;
|
||||
|
||||
public PaginationDataGridViewModel(Func<Task<(IEnumerable<T> data, int totalCount)>> fetchData)
|
||||
{
|
||||
_fetchData = fetchData;
|
||||
PageUpdatedCmd = new DelegateCommand(async () => await LoadDataAsync());
|
||||
PageSizeUpdatedCmd = new DelegateCommand(async () => await LoadDataAsync());
|
||||
|
||||
// 初始化时加载数据
|
||||
//_ = LoadData();
|
||||
}
|
||||
|
||||
public async Task LoadDataAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var (data, totalCount) = await _fetchData();
|
||||
Data = new ObservableCollection<T>(data);
|
||||
TotalCount = totalCount;
|
||||
// 通知分页总数变化
|
||||
//RaisePropertyChanged(nameof(MaxPageCount));
|
||||
MaxPageCount = totalCount == 0 ? 1 : (int)Math.Ceiling((double)totalCount / DataCountPerPage);
|
||||
// 更新分页信息
|
||||
//PageInfo = $"共 {_totalCount} 条";
|
||||
} catch (OperationCanceledException)
|
||||
{
|
||||
// 查询被取消,保持表格不变
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
172
yy-admin-master/YY.Admin/ViewModels/DashboardViewModel.cs
Normal file
172
yy-admin-master/YY.Admin/ViewModels/DashboardViewModel.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using LiveChartsCore;
|
||||
using LiveChartsCore.Drawing;
|
||||
using LiveChartsCore.Kernel;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Input;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Services.Service.User;
|
||||
|
||||
namespace YY.Admin.ViewModels
|
||||
{
|
||||
public class StatisticCard
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
public string Color { get; set; } = string.Empty;
|
||||
public string Trend { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class RecentActivity
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public DateTime Time { get; set; }
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class DashboardViewModel : BaseViewModel
|
||||
{
|
||||
private readonly ISysUserService _userService;
|
||||
|
||||
public float[] Values1 { get; set; } = FetchVales(0);
|
||||
public float[] Values2 { get; set; } = FetchVales(-0.15f);
|
||||
|
||||
public ICommand PointMeasuredCommand { get; }
|
||||
|
||||
public DashboardViewModel(
|
||||
ISysUserService userService,
|
||||
IContainerExtension _container,
|
||||
IRegionManager regionManager
|
||||
) : base(_container, regionManager)
|
||||
{
|
||||
// _eventAggregator.GetEvent<ThemeChangedEvent>().Subscribe(ApplyTheme);
|
||||
_userService = userService;
|
||||
|
||||
PointMeasuredCommand = new DelegateCommand<ChartPoint>(OnPointMeasured);
|
||||
|
||||
_ = LoadDashboardDataAsync();
|
||||
|
||||
}
|
||||
|
||||
public ObservableCollection<StatisticCard> StatisticCards { get; } = new();
|
||||
public ObservableCollection<RecentActivity> RecentActivities { get; } = new();
|
||||
|
||||
private async Task LoadDashboardDataAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
|
||||
try
|
||||
{
|
||||
// 加载统计数据
|
||||
var users = await _userService.GetUsersAsync();
|
||||
|
||||
StatisticCards.Clear();
|
||||
StatisticCards.Add(new StatisticCard
|
||||
{
|
||||
Title = "总用户数",
|
||||
Value = users.Count.ToString(),
|
||||
Icon = "UserOutlined",
|
||||
Color = "#1890ff",
|
||||
Trend = "+12%"
|
||||
});
|
||||
StatisticCards.Add(new StatisticCard
|
||||
{
|
||||
Title = "活跃用户",
|
||||
Value = users.Count(u => u.Status== StatusEnum.Enable).ToString(),
|
||||
Icon = "UserCheckOutlined",
|
||||
Color = "#52c41a",
|
||||
Trend = "+8%"
|
||||
});
|
||||
StatisticCards.Add(new StatisticCard
|
||||
{
|
||||
Title = "今日访问",
|
||||
Value = "1,234",
|
||||
Icon = "EyeOutlined",
|
||||
Color = "#faad14",
|
||||
Trend = "+5%"
|
||||
});
|
||||
StatisticCards.Add(new StatisticCard
|
||||
{
|
||||
Title = "系统消息",
|
||||
Value = "56",
|
||||
Icon = "MessageOutlined",
|
||||
Color = "#f5222d",
|
||||
Trend = "+2"
|
||||
});
|
||||
|
||||
// 加载最近活动
|
||||
RecentActivities.Clear();
|
||||
RecentActivities.Add(new RecentActivity
|
||||
{
|
||||
Title = "用户登录",
|
||||
Description = "管理员 admin 登录系统",
|
||||
Time = DateTime.Now.AddMinutes(-5),
|
||||
Icon = "LoginOutlined"
|
||||
});
|
||||
RecentActivities.Add(new RecentActivity
|
||||
{
|
||||
Title = "数据更新",
|
||||
Description = "用户数据已同步更新",
|
||||
Time = DateTime.Now.AddMinutes(-15),
|
||||
Icon = "SyncOutlined"
|
||||
});
|
||||
RecentActivities.Add(new RecentActivity
|
||||
{
|
||||
Title = "系统备份",
|
||||
Description = "数据库备份已完成",
|
||||
Time = DateTime.Now.AddHours(-2),
|
||||
Icon = "DatabaseOutlined"
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static float[] FetchVales(float offset)
|
||||
{
|
||||
var values = new List<float>();
|
||||
|
||||
// the EasingFunctions.BounceInOut, is just
|
||||
// a function that looks nice!
|
||||
|
||||
var fx = EasingFunctions.BounceInOut;
|
||||
var x = 0f;
|
||||
|
||||
while (x <= 1)
|
||||
{
|
||||
values.Add(fx(x + offset));
|
||||
x += 0.025f;
|
||||
}
|
||||
|
||||
return [.. values];
|
||||
}
|
||||
|
||||
private void OnPointMeasured(ChartPoint point)
|
||||
{
|
||||
// each point will have a different delay depending on its index
|
||||
var index = point.Context.Entity.MetaData!.EntityIndex; // the index of the point in the data source
|
||||
var delay = index / (float)Values1.Length;
|
||||
|
||||
// the animation takes a function, that represents the normalized progress of the animation
|
||||
// the parameter is the normalized time of the animation, it goes from 0 to 1
|
||||
// the function must return a value from 0 to 1, where 0 is the initial state
|
||||
// and 1 is the final state
|
||||
var duration = TimeSpan.FromSeconds(3);
|
||||
var animation = new Animation(t => DelayedEase(t, delay), duration);
|
||||
|
||||
point.Context.Visual?.SetTransition(animation);
|
||||
}
|
||||
|
||||
private static float DelayedEase(float t, float delay)
|
||||
{
|
||||
if (t <= delay) return 0f;
|
||||
|
||||
var remappedT = (t - delay) / (1f - delay);
|
||||
var baseEasing = EasingFunctions.BuildCustomElasticOut(1.5f, 0.60f);
|
||||
return baseEasing(Math.Clamp(remappedT, 0f, 1f));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Prism.Dialogs;
|
||||
using Prism.Mvvm;
|
||||
using Prism.Commands;
|
||||
using System;
|
||||
|
||||
namespace YY.Admin.ViewModels.Dialogs
|
||||
{
|
||||
public class AlertDialogViewModel : BindableBase, IDialogAware
|
||||
{
|
||||
private string _message;
|
||||
public string Message
|
||||
{
|
||||
get => _message;
|
||||
set => SetProperty(ref _message, value);
|
||||
}
|
||||
|
||||
public DelegateCommand CloseCommand { get; }
|
||||
|
||||
public AlertDialogViewModel()
|
||||
{
|
||||
CloseCommand = new DelegateCommand(() =>
|
||||
{
|
||||
// 调用 RequestClose 属性触发关闭
|
||||
RequestClose.Invoke(new DialogResult(ButtonResult.OK));
|
||||
});
|
||||
}
|
||||
|
||||
public bool CanCloseDialog() => true;
|
||||
|
||||
public void OnDialogOpened(IDialogParameters parameters)
|
||||
{
|
||||
Message = parameters.GetValue<string>("message");
|
||||
}
|
||||
|
||||
public void OnDialogClosed() { }
|
||||
|
||||
public string Title => "提示";
|
||||
|
||||
// 这里实现的是属性,不是 event
|
||||
public DialogCloseListener RequestClose { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.ViewModels.Dialogs
|
||||
{
|
||||
public class ConfirmDialogViewModel : BindableBase, IDialogAware
|
||||
{
|
||||
public string Title => "确认";
|
||||
|
||||
private string _message;
|
||||
public string Message
|
||||
{
|
||||
get => _message;
|
||||
set => SetProperty(ref _message, value);
|
||||
}
|
||||
|
||||
// 这是 Prism.Dialogs 版本的 RequestClose
|
||||
public DialogCloseListener RequestClose { get; private set; }
|
||||
|
||||
public DelegateCommand YesCommand { get; }
|
||||
public DelegateCommand NoCommand { get; }
|
||||
|
||||
public ConfirmDialogViewModel()
|
||||
{
|
||||
YesCommand = new DelegateCommand(() => CloseDialog(ButtonResult.Yes));
|
||||
NoCommand = new DelegateCommand(() => CloseDialog(ButtonResult.No));
|
||||
}
|
||||
|
||||
private void CloseDialog(ButtonResult result)
|
||||
{
|
||||
// 触发关闭
|
||||
RequestClose.Invoke(new DialogResult(result));
|
||||
}
|
||||
|
||||
public bool CanCloseDialog() => true;
|
||||
|
||||
public void OnDialogOpened(IDialogParameters parameters)
|
||||
{
|
||||
Message = parameters.GetValue<string>("message");
|
||||
}
|
||||
|
||||
public void OnDialogClosed() { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Prism.Dialogs;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace YY.Admin.ViewModels.Dialogs
|
||||
{
|
||||
public class ErrorDialogViewModel : BindableBase, IDialogAware
|
||||
{
|
||||
public string Title => "错误";
|
||||
|
||||
private string _message;
|
||||
public string Message
|
||||
{
|
||||
get => _message;
|
||||
set => SetProperty(ref _message, value);
|
||||
}
|
||||
|
||||
// 用属性来实现,而不是 event
|
||||
public DialogCloseListener RequestClose { get; private set; }
|
||||
|
||||
public ErrorDialogViewModel()
|
||||
{
|
||||
// 自动关闭定时器(2秒)
|
||||
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
|
||||
timer.Tick += (s, e) =>
|
||||
{
|
||||
timer.Stop();
|
||||
RequestClose.Invoke(new DialogResult(ButtonResult.OK));
|
||||
};
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
public bool CanCloseDialog() => true;
|
||||
|
||||
public void OnDialogOpened(IDialogParameters parameters)
|
||||
{
|
||||
Message = parameters.GetValue<string>("message");
|
||||
}
|
||||
|
||||
public void OnDialogClosed()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using YY.Admin.Helper;
|
||||
|
||||
namespace YY.Admin.ViewModels.Dialogs
|
||||
{
|
||||
public class ServerSettingsDialogViewModel : BindableBase, IDialogAware
|
||||
{
|
||||
private const string DefaultWebSocketPath = "/websocket/scada-sync";
|
||||
private string _loadedWebSocketUrl = string.Empty;
|
||||
private string _loadedAutoWebSocketUrl = string.Empty;
|
||||
public string Title => "服务器设置";
|
||||
|
||||
private string _ip = "127.0.0.1";
|
||||
public string Ip
|
||||
{
|
||||
get => _ip;
|
||||
set => SetProperty(ref _ip, value);
|
||||
}
|
||||
|
||||
private int _port = 8080;
|
||||
public int Port
|
||||
{
|
||||
get => _port;
|
||||
set => SetProperty(ref _port, value);
|
||||
}
|
||||
|
||||
private string _webSocketUrl = string.Empty;
|
||||
public string WebSocketUrl
|
||||
{
|
||||
get => _webSocketUrl;
|
||||
set => SetProperty(ref _webSocketUrl, value);
|
||||
}
|
||||
|
||||
private string _basePath = "/jeecg-boot";
|
||||
public string BasePath
|
||||
{
|
||||
get => _basePath;
|
||||
set => SetProperty(ref _basePath, value);
|
||||
}
|
||||
|
||||
private string _errorMessage = string.Empty;
|
||||
public string ErrorMessage
|
||||
{
|
||||
get => _errorMessage;
|
||||
set => SetProperty(ref _errorMessage, value);
|
||||
}
|
||||
|
||||
public DelegateCommand SaveCommand { get; }
|
||||
public DelegateCommand CancelCommand { get; }
|
||||
public DialogCloseListener RequestClose { get; private set; }
|
||||
|
||||
public ServerSettingsDialogViewModel()
|
||||
{
|
||||
SaveCommand = new DelegateCommand(Save);
|
||||
CancelCommand = new DelegateCommand(() => RequestClose.Invoke(new DialogResult(ButtonResult.Cancel)));
|
||||
}
|
||||
|
||||
public bool CanCloseDialog() => true;
|
||||
public void OnDialogClosed() { }
|
||||
|
||||
public void OnDialogOpened(IDialogParameters parameters)
|
||||
{
|
||||
var settings = ServerSettingsStore.Load();
|
||||
_loadedAutoWebSocketUrl = ServerSettingsStore.BuildDefaultWebSocketUrl(settings.BaseScheme, settings.Ip, settings.Port, settings.BasePath, settings.WebSocketPath);
|
||||
Ip = settings.Ip;
|
||||
Port = settings.Port;
|
||||
WebSocketUrl = string.IsNullOrWhiteSpace(settings.WebSocketUrl)
|
||||
? _loadedAutoWebSocketUrl
|
||||
: settings.WebSocketUrl;
|
||||
_loadedWebSocketUrl = WebSocketUrl;
|
||||
BasePath = string.IsNullOrWhiteSpace(settings.BasePath) ? "/jeecg-boot" : settings.BasePath;
|
||||
ErrorMessage = string.Empty;
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
ErrorMessage = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(Ip))
|
||||
{
|
||||
ErrorMessage = "IP 不能为空";
|
||||
return;
|
||||
}
|
||||
if (Port <= 0 || Port > 65535)
|
||||
{
|
||||
ErrorMessage = "端口号必须在 1-65535 之间";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var basePath = BasePath?.Trim() ?? "/jeecg-boot";
|
||||
var shouldAutoRebuildWs = string.IsNullOrWhiteSpace(WebSocketUrl)
|
||||
|| string.Equals(WebSocketUrl.Trim(), _loadedAutoWebSocketUrl, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(WebSocketUrl.Trim(), _loadedWebSocketUrl, StringComparison.OrdinalIgnoreCase);
|
||||
var finalWsUrl = shouldAutoRebuildWs
|
||||
? ServerSettingsStore.BuildDefaultWebSocketUrl("http", Ip.Trim(), Port, basePath, DefaultWebSocketPath)
|
||||
: WebSocketUrl.Trim();
|
||||
|
||||
ServerSettingsStore.Save(new ServerSettingsStore.ServerSettingsModel
|
||||
{
|
||||
Ip = Ip.Trim(),
|
||||
Port = Port,
|
||||
BaseScheme = "http",
|
||||
BasePath = basePath,
|
||||
WebSocketUrl = finalWsUrl,
|
||||
WebSocketPath = DefaultWebSocketPath
|
||||
});
|
||||
RequestClose.Invoke(new DialogResult(ButtonResult.OK));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = $"保存失败:{ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Prism.Dialogs;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace YY.Admin.ViewModels.Dialogs
|
||||
{
|
||||
public class SuccessDialogViewModel : BindableBase, IDialogAware
|
||||
{
|
||||
public string Title => "成功";
|
||||
|
||||
private string _message;
|
||||
public string Message
|
||||
{
|
||||
get => _message;
|
||||
set => SetProperty(ref _message, value);
|
||||
}
|
||||
|
||||
// Prism.Dialogs 版本的 RequestClose 是属性,不是 event
|
||||
public DialogCloseListener RequestClose { get; private set; }
|
||||
|
||||
public SuccessDialogViewModel()
|
||||
{
|
||||
// 自动关闭定时器
|
||||
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1.5) };
|
||||
timer.Tick += (s, e) =>
|
||||
{
|
||||
timer.Stop();
|
||||
RequestClose.Invoke(new DialogResult(ButtonResult.OK));
|
||||
};
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
public bool CanCloseDialog() => true;
|
||||
|
||||
public void OnDialogOpened(IDialogParameters parameters)
|
||||
{
|
||||
Message = parameters.GetValue<string>("message");
|
||||
}
|
||||
|
||||
public void OnDialogClosed()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Prism.Dialogs;
|
||||
using Prism.Mvvm;
|
||||
using Prism.Commands;
|
||||
using System;
|
||||
|
||||
namespace YY.Admin.ViewModels.Dialogs
|
||||
{
|
||||
public class WarningDialogViewModel : BindableBase, IDialogAware
|
||||
{
|
||||
public string Title => "警告";
|
||||
|
||||
private string _message;
|
||||
public string Message
|
||||
{
|
||||
get => _message;
|
||||
set => SetProperty(ref _message, value);
|
||||
}
|
||||
|
||||
// Prism.Dialogs 版本的 RequestClose 是属性
|
||||
public DialogCloseListener RequestClose { get; private set; }
|
||||
|
||||
public DelegateCommand CloseCommand { get; }
|
||||
|
||||
public WarningDialogViewModel()
|
||||
{
|
||||
CloseCommand = new DelegateCommand(CloseDialog);
|
||||
}
|
||||
|
||||
private void CloseDialog()
|
||||
{
|
||||
RequestClose.Invoke(new DialogResult(ButtonResult.OK));
|
||||
}
|
||||
|
||||
public bool CanCloseDialog() => true;
|
||||
|
||||
public void OnDialogOpened(IDialogParameters parameters)
|
||||
{
|
||||
Message = parameters.GetValue<string>("message");
|
||||
}
|
||||
|
||||
public void OnDialogClosed()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
277
yy-admin-master/YY.Admin/ViewModels/LoginWindowViewModel.cs
Normal file
277
yy-admin-master/YY.Admin/ViewModels/LoginWindowViewModel.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Net.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using YY.Admin.Core.Helper;
|
||||
using YY.Admin.Core.Session;
|
||||
using YY.Admin.FluentValidation;
|
||||
using YY.Admin.Services;
|
||||
using YY.Admin.Services.Service.Auth;
|
||||
using YY.Admin.Services.Service.Jeecg;
|
||||
using YY.Admin.Views;
|
||||
|
||||
namespace YY.Admin.ViewModels
|
||||
{
|
||||
// ViewModels/LoginViewModel.cs
|
||||
public class LoginWindowViewModel : BaseViewModel
|
||||
{
|
||||
private readonly ISysAuthService _authService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IJeecgLoginLogReportService _loginLogReportService;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly HttpClient _httpClient = new();
|
||||
private readonly CancellationTokenSource _connectivityCts = new();
|
||||
private const int ConnectivityCheckIntervalSeconds = 5;
|
||||
public LoginInput LoginInput { get; set; }
|
||||
private string _loginMessage = string.Empty;
|
||||
public DelegateCommand LoginCommand { get; }
|
||||
public DelegateCommand SyncJeecgUsersCommand { get; }
|
||||
public DelegateCommand OpenServerSettingsCommand { get; }
|
||||
public LoginInputValidator LoginInputValidator { get; private set; }
|
||||
|
||||
private bool _isSyncingJeecgUsers;
|
||||
/// <summary>
|
||||
/// 正在从 Jeecg 同步用户到本地库
|
||||
/// </summary>
|
||||
public bool IsSyncingJeecgUsers
|
||||
{
|
||||
get => _isSyncingJeecgUsers;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isSyncingJeecgUsers, value))
|
||||
{
|
||||
SyncJeecgUsersCommand.RaiseCanExecuteChanged();
|
||||
LoginCommand.RaiseCanExecuteChanged();
|
||||
RaisePropertyChanged(nameof(SyncJeecgUsersButtonText));
|
||||
RaisePropertyChanged(nameof(CanInteractWithLogin));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string SyncJeecgUsersButtonText => IsSyncingJeecgUsers ? "同步中..." : "同步 Jeecg 用户";
|
||||
|
||||
/// <summary>登录与同步互斥,用于禁用登录按钮</summary>
|
||||
public bool CanInteractWithLogin => !IsLoading && !IsSyncingJeecgUsers;
|
||||
|
||||
private bool _isBackendConnected;
|
||||
/// <summary>
|
||||
/// 后端连接状态:true=连接中,false=已断开
|
||||
/// </summary>
|
||||
public bool IsBackendConnected
|
||||
{
|
||||
get => _isBackendConnected;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isBackendConnected, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(BackendConnectionStatusText));
|
||||
RaisePropertyChanged(nameof(BackendConnectionStatusBrush));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string BackendConnectionStatusText => IsBackendConnected ? "后端连接中" : "后端已断开";
|
||||
public Brush BackendConnectionStatusBrush => IsBackendConnected ? Brushes.LimeGreen : Brushes.Red;
|
||||
|
||||
public LoginWindowViewModel(
|
||||
ISysAuthService authService,
|
||||
IDialogService dialogService,
|
||||
IJeecgLoginLogReportService loginLogReportService,
|
||||
IContainerExtension _container,
|
||||
IRegionManager regionManager
|
||||
) : base(_container, regionManager)
|
||||
{
|
||||
_authService = authService;
|
||||
_dialogService = dialogService;
|
||||
_loginLogReportService = loginLogReportService;
|
||||
_configuration = _container.Resolve<IConfiguration>();
|
||||
_loginLogReportService.StartBackgroundSync();
|
||||
Title = "系统登录";
|
||||
|
||||
LoginInput = new LoginInput()
|
||||
{
|
||||
Username = "admin",
|
||||
Password = "123456"
|
||||
};
|
||||
|
||||
LoginInputValidator = new LoginInputValidator();
|
||||
|
||||
LoginCommand = new DelegateCommand(async () => await LoginAsync(), CanLogin)
|
||||
.ObservesProperty(() => IsLoading)
|
||||
.ObservesProperty(() => IsSyncingJeecgUsers);
|
||||
|
||||
SyncJeecgUsersCommand = new DelegateCommand(async () => await SyncJeecgUsersAsync(), CanSyncJeecgUsers)
|
||||
.ObservesProperty(() => IsLoading)
|
||||
.ObservesProperty(() => IsSyncingJeecgUsers);
|
||||
|
||||
OpenServerSettingsCommand = new DelegateCommand(OpenServerSettings);
|
||||
|
||||
_ = StartBackendConnectivityLoopAsync(_connectivityCts.Token);
|
||||
}
|
||||
|
||||
public string LoginMessage
|
||||
{
|
||||
get => _loginMessage;
|
||||
set => SetProperty(ref _loginMessage, value);
|
||||
}
|
||||
|
||||
public string LoginButtonText => IsLoading ? "登录中..." : "登录";
|
||||
|
||||
protected override void OnIsLoadingChanged()
|
||||
{
|
||||
base.OnIsLoadingChanged();
|
||||
RaisePropertyChanged(nameof(LoginButtonText));
|
||||
RaisePropertyChanged(nameof(CanInteractWithLogin));
|
||||
LoginCommand.RaiseCanExecuteChanged();
|
||||
SyncJeecgUsersCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
private bool CanLogin()
|
||||
{
|
||||
return !IsLoading && !IsSyncingJeecgUsers;
|
||||
}
|
||||
|
||||
private bool CanSyncJeecgUsers()
|
||||
{
|
||||
return !IsLoading && !IsSyncingJeecgUsers;
|
||||
}
|
||||
|
||||
private async Task SyncJeecgUsersAsync()
|
||||
{
|
||||
LoginMessage = string.Empty;
|
||||
IsSyncingJeecgUsers = true;
|
||||
try
|
||||
{
|
||||
await UIHelper.WaitForRenderAsync();
|
||||
var (success, message) = await _authService.SyncJeecgUsersToLocalFromLoginScreenAsync();
|
||||
LoginMessage = message;
|
||||
if (success)
|
||||
{
|
||||
_logger?.Information("登录页一键同步 Jeecg 用户成功");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger?.Warning($"登录页一键同步 Jeecg 用户:{message}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoginMessage = $"同步出错:{ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSyncingJeecgUsers = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoginAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
//LoginMessage = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
// 让出线程,让UI先渲染
|
||||
await UIHelper.WaitForRenderAsync();
|
||||
|
||||
var response = await _authService.LoginAsync(LoginInput);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_ = _loginLogReportService.ReportLoginAsync(LoginInput.Username, true, "登录成功");
|
||||
_connectivityCts.Cancel();
|
||||
//设置会话
|
||||
AppSession.CurrentUser = response.User;
|
||||
// 设置用户上下文
|
||||
SetUserContext(response.User!, response.Token);
|
||||
// 启动定时器
|
||||
StartTokenCheckTimer();
|
||||
// 登录成功,打开主窗口
|
||||
var loginWindow = Application.Current.MainWindow;
|
||||
var mainWindow = _container.Resolve<MainWindow>();
|
||||
Application.Current.MainWindow = mainWindow;
|
||||
//把窗口和RegionManager 绑定一下即可
|
||||
SetRegionManager(mainWindow);
|
||||
mainWindow.Show();
|
||||
loginWindow.Close();
|
||||
|
||||
// 不等待异步更新用户登录信息,不阻塞主窗口打开
|
||||
_ = _authService.UpdateUserLoginInfoAsync(response.User!);
|
||||
}
|
||||
else
|
||||
{
|
||||
LoginMessage = response.Message;
|
||||
_ = _loginLogReportService.ReportLoginAsync(LoginInput.Username, false, response.Message);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoginMessage = $"登录出错:{ex.Message}";
|
||||
_ = _loginLogReportService.ReportLoginAsync(LoginInput.Username, false, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartBackendConnectivityLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
bool connected = false;
|
||||
try
|
||||
{
|
||||
// 每轮都重新读取配置,保存服务器设置后可即时生效
|
||||
var baseUrl = _configuration.GetValue<string>("JeecgIntegration:BaseUrl")?.TrimEnd('/');
|
||||
var userListPath = _configuration.GetValue<string>("JeecgIntegration:UserListPath") ?? "/sys/user/scada/queryUser";
|
||||
var probeUrl = string.IsNullOrWhiteSpace(baseUrl)
|
||||
? string.Empty
|
||||
: $"{baseUrl}{userListPath}?pageNo=1&pageSize=1&includeDetail=false";
|
||||
if (!string.IsNullOrWhiteSpace(probeUrl))
|
||||
{
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, probeUrl);
|
||||
using var resp = await _httpClient.SendAsync(req, cancellationToken);
|
||||
connected = resp.IsSuccessStatusCode;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
connected = false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
IsBackendConnected = connected;
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略窗口关闭后的调度异常
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(ConnectivityCheckIntervalSeconds), cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenServerSettings()
|
||||
{
|
||||
_dialogService.ShowDialog("ServerSettingsDialog", r =>
|
||||
{
|
||||
if (r.Result == ButtonResult.OK)
|
||||
{
|
||||
LoginMessage = "服务器配置已保存";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
691
yy-admin-master/YY.Admin/ViewModels/MainWindowViewModel.cs
Normal file
691
yy-admin-master/YY.Admin/ViewModels/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,691 @@
|
||||
using Mapster;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
using SqlSugar;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Const;
|
||||
using YY.Admin.Core.Model;
|
||||
using YY.Admin.Core.Session;
|
||||
using YY.Admin.Core.Util;
|
||||
using YY.Admin.Event;
|
||||
using YY.Admin.Module;
|
||||
using YY.Admin.Services.Service.Auth;
|
||||
using YY.Admin.Services.Service.Jeecg;
|
||||
using YY.Admin.Services.Service.Menu;
|
||||
using YY.Admin.ViewModels.Control;
|
||||
|
||||
namespace YY.Admin.ViewModels
|
||||
{
|
||||
public class MainWindowViewModel : BaseViewModel
|
||||
{
|
||||
private readonly ISysAuthService _authService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IJeecgLoginLogReportService _loginLogReportService;
|
||||
private readonly IJeecgUserSyncCoordinator _jeecgUserSyncCoordinator;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly HttpClient _httpClient = new();
|
||||
private readonly CancellationTokenSource _backendConnectivityCts = new();
|
||||
private const int ConnectivityCheckIntervalSeconds = 5;
|
||||
private SysUser? _currentUser;
|
||||
private bool _isBackendConnected;
|
||||
|
||||
#region 标签集合
|
||||
public ObservableCollection<TabItemModel> OpenTabs { get; } = new ObservableCollection<TabItemModel>();
|
||||
private CancellationTokenSource? _navigateCts;
|
||||
private bool _isTabClosing;
|
||||
private TabItemModel? _selectedTab;
|
||||
public TabItemModel? SelectedTab
|
||||
{
|
||||
get => _selectedTab;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedTab, value) && value != null)
|
||||
{
|
||||
// 发布事件
|
||||
_eventAggregator.GetEvent<TabSelectedEvent>().Publish(value);
|
||||
|
||||
bool isNotMenuTreeView = _regionManager.Regions[CommonConst.MenuRegion].ActiveViews.Where(it => it.GetType().Name != "MenuTreeView").Any();
|
||||
if (value.TabSource is MenuItem menuItem && !isNotMenuTreeView)
|
||||
{
|
||||
var navItem = NavItems?.Where(it => it.ViewName == "MenuTreeView").FirstOrDefault();
|
||||
if (navItem != null)
|
||||
{
|
||||
if (navItem.AlignBottom)
|
||||
{
|
||||
SelectedBottomNavItem = navItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedTopNavItem = navItem;
|
||||
}
|
||||
}
|
||||
} else if (value.TabSource is NavItem navItem)
|
||||
{
|
||||
if (navItem.AlignBottom)
|
||||
{
|
||||
SelectedBottomNavItem = navItem;
|
||||
} else
|
||||
{
|
||||
SelectedTopNavItem = navItem;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消上一次延迟导航
|
||||
// 防止拖拽Tab时由于两次导航出现视觉上闪烁问题
|
||||
// TabA拖动到TabB过程:TabA -> TabB -> TabA
|
||||
_navigateCts?.Cancel();
|
||||
_navigateCts = new CancellationTokenSource();
|
||||
|
||||
// 延迟 100ms 执行导航(过滤临时选中)
|
||||
_ = Task.Delay(100, _navigateCts.Token)
|
||||
.ContinueWith(_ =>
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
_ = NavigateToViewAsync(CommonConst.ContentRegion, value.ViewName, value.TabSource?.NavigationParameter);
|
||||
});
|
||||
},
|
||||
TaskContinuationOptions.OnlyOnRanToCompletion
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
# region 侧边栏
|
||||
public ObservableCollection<NavItem>? NavItems { get; set; }
|
||||
public ObservableCollection<NavItem>? TopNavItems { get; set; }
|
||||
public ObservableCollection<NavItem>? BottomNavItems { get; set; }
|
||||
|
||||
private NavItem? _selectedNavItem;
|
||||
public NavItem? SelectedNavItem
|
||||
{
|
||||
get => _selectedNavItem;
|
||||
set {
|
||||
if (SetProperty(ref _selectedNavItem, value) && value != null)
|
||||
{
|
||||
// 执行命令(如果有)
|
||||
if (value.Command?.CanExecute(value) == true)
|
||||
{
|
||||
value.Command.Execute(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private NavItem? _selectedTopNavItem;
|
||||
public NavItem? SelectedTopNavItem
|
||||
{
|
||||
get => _selectedTopNavItem;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedTopNavItem, value) && value != null)
|
||||
{
|
||||
|
||||
// 取消底部选中 互斥
|
||||
SelectedBottomNavItem = null;
|
||||
|
||||
SelectedNavItem = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private NavItem? _selectedBottomNavItem;
|
||||
public NavItem? SelectedBottomNavItem
|
||||
{
|
||||
get => _selectedBottomNavItem;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedBottomNavItem, value) && value != null)
|
||||
{
|
||||
|
||||
// 取消顶部选中 互斥
|
||||
SelectedTopNavItem = null;
|
||||
|
||||
SelectedNavItem = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 系统设置
|
||||
private bool _isAppSettingsOpen;
|
||||
public bool IsAppSettingsOpen
|
||||
{
|
||||
get => _isAppSettingsOpen;
|
||||
set => SetProperty(ref _isAppSettingsOpen, value);
|
||||
}
|
||||
|
||||
private AppSettingsViewModel? _appSettingsViewModel;
|
||||
public AppSettingsViewModel? AppSettingsViewModel {
|
||||
get => _appSettingsViewModel;
|
||||
set => SetProperty(ref _appSettingsViewModel, value);
|
||||
}
|
||||
|
||||
// 系统设置命令
|
||||
public ICommand ResetAppSettingsCommand { get; }
|
||||
public ICommand OpenAppSettingsCommand { get; }
|
||||
public ICommand OpenServerSettingsCommand { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
private SubscriptionToken? _openOrActivateTabToken;
|
||||
private SubscriptionToken? _tabClosedToken;
|
||||
private SubscriptionToken? _refreshTabToken;
|
||||
private SubscriptionToken? _loginOutToken;
|
||||
|
||||
public MainWindowViewModel(
|
||||
ISysAuthService authService,
|
||||
IDialogService dialogService,
|
||||
IJeecgLoginLogReportService loginLogReportService,
|
||||
IJeecgUserSyncCoordinator jeecgUserSyncCoordinator,
|
||||
ISysMenuService sysMenuService,
|
||||
IContainerExtension _container,
|
||||
IRegionManager regionManager
|
||||
) : base(_container, regionManager)
|
||||
{
|
||||
// 加载系统设置
|
||||
LoadAppSettings();
|
||||
|
||||
_authService = authService;
|
||||
_dialogService = dialogService;
|
||||
_loginLogReportService = loginLogReportService;
|
||||
_jeecgUserSyncCoordinator = jeecgUserSyncCoordinator;
|
||||
_configuration = _container.Resolve<IConfiguration>();
|
||||
_loginLogReportService.StartBackgroundSync();
|
||||
// 订阅用户变更事件
|
||||
_currentUser = _authService.CurrentUser;
|
||||
_authService.UserChanged += OnUserChanged;
|
||||
|
||||
// 登出命令
|
||||
LogoutCommand = new DelegateCommand(LogoutAsync);
|
||||
// 系统设置命令
|
||||
OpenAppSettingsCommand = new DelegateCommand(OpenAppSettings);
|
||||
ResetAppSettingsCommand = new DelegateCommand(ResetAppSettings);
|
||||
OpenServerSettingsCommand = new DelegateCommand(OpenServerSettings);
|
||||
|
||||
// 初始化Sidebar数据
|
||||
InitNavItems();
|
||||
|
||||
// 订阅事件
|
||||
_openOrActivateTabToken = _eventAggregator.GetEvent<TabSourceSelectedEvent>().Subscribe(OnOpenOrActivateTab);
|
||||
_tabClosedToken = _eventAggregator.GetEvent<TabClosedEvent>().Subscribe(OnTabClosed);
|
||||
_refreshTabToken = _eventAggregator.GetEvent<TabRefreshEvent>().Subscribe(OnRefreshTab);
|
||||
_loginOutToken = _eventAggregator.GetEvent<SysUserEvents.LoginOutEvent>().Subscribe(Destroy);
|
||||
|
||||
// Jeecg 用户增量同步:定时 + 可选 WebSocket(工控机断网续传)
|
||||
_jeecgUserSyncCoordinator.Start();
|
||||
|
||||
// 主窗口底部连接状态圆点
|
||||
_ = StartBackendConnectivityLoopAsync(_backendConnectivityCts.Token);
|
||||
}
|
||||
|
||||
|
||||
public SysUser? CurrentUser
|
||||
{
|
||||
get => _currentUser;
|
||||
set => SetProperty(ref _currentUser, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后端连接状态:true=连接中,false=已断开
|
||||
/// </summary>
|
||||
public bool IsBackendConnected
|
||||
{
|
||||
get => _isBackendConnected;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isBackendConnected, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(BackendConnectionStatusBrush));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Brush BackendConnectionStatusBrush => IsBackendConnected ? Brushes.LimeGreen : Brushes.Red;
|
||||
|
||||
public DelegateCommand LogoutCommand { get; }
|
||||
|
||||
private void InitNavItems()
|
||||
{
|
||||
NavItems = new ObservableCollection<NavItem>
|
||||
{
|
||||
new NavItem {
|
||||
Icon = "FileTreeOutline",
|
||||
Name = "功能菜单",
|
||||
ViewName = "MenuTreeView",
|
||||
Command = new DelegateCommand<NavItem>(it => _ = NavigateToViewAsync(CommonConst.MenuRegion, it.ViewName))
|
||||
},
|
||||
new NavItem {
|
||||
Icon = "GamepadVariantOutline",
|
||||
Name = "菜单区域",
|
||||
Command = new DelegateCommand<NavItem>(it => _ = NavigateToViewAsync(CommonConst.MenuRegion, it.ViewName))
|
||||
},
|
||||
new NavItem {
|
||||
Icon = "FoodAppleOutline",
|
||||
Name = "Tab区域",
|
||||
Command = new DelegateCommand<NavItem>(OnOpenOrActivateTab)
|
||||
},
|
||||
new NavItem {
|
||||
Icon = "Server",
|
||||
Name = "服务器设置",
|
||||
AlignBottom = true,
|
||||
IsActive = false,
|
||||
Command = OpenServerSettingsCommand
|
||||
},
|
||||
new NavItem {
|
||||
Icon = "AccountCircleOutline",
|
||||
Name = "个人中心",
|
||||
AlignBottom = true,
|
||||
Command = new DelegateCommand<NavItem>(OnOpenOrActivateTab)
|
||||
},
|
||||
new NavItem {
|
||||
Icon = StringUtil.ConvertHtmlEntityToUnicode(""),
|
||||
IconType = IconTypeEnum.AntDesign,
|
||||
Name = "系统设置",
|
||||
AlignBottom = true,
|
||||
IsActive = false,
|
||||
Command = OpenAppSettingsCommand
|
||||
},
|
||||
new NavItem {
|
||||
Icon = "Power",
|
||||
Name = "退出登录",
|
||||
AlignBottom = true,
|
||||
IsActive = false,
|
||||
Command = LogoutCommand
|
||||
}
|
||||
};
|
||||
TopNavItems = new(NavItems.Where(t => !t.AlignBottom));
|
||||
BottomNavItems = new(NavItems.Where(t => t.AlignBottom));
|
||||
|
||||
SelectedTopNavItem = TopNavItems.FirstOrDefault();
|
||||
}
|
||||
|
||||
private void OnUserChanged(object? sender, SysUser? user)
|
||||
{
|
||||
CurrentUser = user;
|
||||
//// 用户变更时刷新菜单
|
||||
//LoadMenuAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导航
|
||||
/// </summary>
|
||||
/// <param name="regionName">区域名称</param>
|
||||
/// <param name="viewName">视图名称</param>
|
||||
/// <returns></returns>
|
||||
private async Task NavigateToViewAsync(string regionName, string? viewName, INavigationParameters? parameters = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(regionName))
|
||||
return;
|
||||
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
// 视图为空 或者 视图未在容器中注册
|
||||
if (string.IsNullOrEmpty(viewName) || !(_container as IContainerProvider).IsRegistered<object>(viewName))
|
||||
{
|
||||
_logger.Error($"视图未注册: {viewName}");
|
||||
_regionManager.RequestNavigate(regionName, "NotFoundView");
|
||||
tcs.SetResult(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Debug($"开始后台异步导航到: {viewName}");
|
||||
|
||||
|
||||
// 在UI线程执行导航(但不在属性设置的调用栈中)
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_regionManager.RequestNavigate(regionName, viewName,
|
||||
result =>
|
||||
{
|
||||
if (result.Success)
|
||||
{
|
||||
_logger.Debug($"导航成功: {viewName}");
|
||||
tcs.SetResult(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Error($"导航失败: {viewName}");
|
||||
tcs.SetResult(false);
|
||||
}
|
||||
}, parameters);
|
||||
}, DispatcherPriority.Background); // 使用较低优先级
|
||||
|
||||
await tcs.Task;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"导航异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开或激活一个标签页(若已打开则激活,否则新建)
|
||||
/// </summary>
|
||||
private void OnOpenOrActivateTab(TabSource tabSource)
|
||||
{
|
||||
if (tabSource == null) {
|
||||
_logger.Debug("tabSource为空");
|
||||
return;
|
||||
}
|
||||
|
||||
if (tabSource is MenuItem menuItem)
|
||||
{
|
||||
// 检查是否是父级菜单(有子菜单)
|
||||
if (menuItem.Children?.Count > 0)
|
||||
{
|
||||
_logger.Debug($"跳过父级菜单: {menuItem.Name},它有 {menuItem.Children.Count} 个子菜单");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有有效的 ViewName
|
||||
//if (string.IsNullOrEmpty(menuItem.ViewName))
|
||||
//{
|
||||
// _logger.Debug($"菜单没有 ViewName: {menuItem.Name}");
|
||||
// return;
|
||||
//}
|
||||
|
||||
_logger.Debug($"点击菜单: {menuItem.Name}, ViewName: {menuItem.ViewName}");
|
||||
}
|
||||
|
||||
// 检查标签是否已打开
|
||||
var existingTab = OpenTabs.FirstOrDefault(t => t.TabSource == tabSource);
|
||||
if (existingTab != null)
|
||||
{
|
||||
_logger.Debug($"切换到已存在标签: {tabSource.ViewName}");
|
||||
SelectedTab = existingTab;
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建新标签
|
||||
var newTab = new TabItemModel
|
||||
{
|
||||
Header = tabSource.Name,
|
||||
Icon = tabSource.Icon,
|
||||
IconType = tabSource.IconType,
|
||||
ViewName = tabSource.ViewName,
|
||||
IsClosable = tabSource.IsClosable,
|
||||
TabSource = tabSource,
|
||||
OpenTabs = OpenTabs,
|
||||
EventAggregator = _eventAggregator
|
||||
};
|
||||
|
||||
_logger.Debug($"创建新标签: {tabSource.ViewName}");
|
||||
OpenTabs.Add(newTab);
|
||||
SelectedTab = newTab;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新Tab
|
||||
/// </summary>
|
||||
/// <param name="tabItemModel"></param>
|
||||
private void OnRefreshTab(TabItemModel tabItemModel)
|
||||
{
|
||||
if (tabItemModel?.TabSource == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 当前Tab选中
|
||||
SelectedTab = tabItemModel;
|
||||
|
||||
// 从Region中移除现有视图(避免缓存问题)
|
||||
RemoveContentRegionView(tabItemModel.ViewName);
|
||||
|
||||
// 重新导航,确保加载新的视图
|
||||
_ = NavigateToViewAsync(CommonConst.ContentRegion, tabItemModel.ViewName, tabItemModel.TabSource.NavigationParameter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TabItem关闭回调
|
||||
/// </summary>
|
||||
private void OnTabClosed(TabItemModel tabModel)
|
||||
{
|
||||
if (tabModel != null)
|
||||
{
|
||||
_logger.Debug($"标签已关闭: {tabModel.Header}");
|
||||
|
||||
//var viewName = OpenTabs.Count <= 1 ? null : tabModel.ViewName;
|
||||
var viewName = tabModel.ViewName;
|
||||
|
||||
RemoveContentRegionView(viewName);
|
||||
|
||||
// 当前Tab是最后一个
|
||||
if (OpenTabs.Count <= 1)
|
||||
{
|
||||
var region = _regionManager.Regions[CommonConst.MenuRegion];
|
||||
var activeView = region.ActiveViews.FirstOrDefault();
|
||||
if (activeView != null)
|
||||
{
|
||||
var navItem = NavItems?.Where(it => it.ViewName == activeView.GetType().Name).FirstOrDefault();
|
||||
if (navItem != null)
|
||||
{
|
||||
if (navItem.AlignBottom)
|
||||
{
|
||||
SelectedBottomNavItem = navItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedTopNavItem = navItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (tabModel.TabSource is NavItem navItem)
|
||||
{
|
||||
if (navItem == SelectedTopNavItem)
|
||||
{
|
||||
SelectedTopNavItem = null;
|
||||
SelectedNavItem = null;
|
||||
}
|
||||
else if (navItem == SelectedBottomNavItem)
|
||||
{
|
||||
SelectedBottomNavItem = null;
|
||||
SelectedNavItem = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果关闭的不是当前选中的Tab
|
||||
if (SelectedTab != null && SelectedTab != tabModel)
|
||||
{
|
||||
// 发布事件:强制刷新当前选中(因为右键关闭未切换,不会触发SelectedTab属性setter方法)
|
||||
_eventAggregator.GetEvent<TabSelectedEvent>().Publish(SelectedTab);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void LogoutAsync()
|
||||
{
|
||||
// 使用异步版本
|
||||
var confirmed = await ConfirmAsync("确定退出登录吗?");
|
||||
if (confirmed)
|
||||
{
|
||||
var account = AppSession.CurrentUser?.Account ?? CurrentUser?.Account ?? string.Empty;
|
||||
_ = _loginLogReportService.ReportLogAsync("LOGIN", "退出登录", account, true);
|
||||
// 发布登出事件
|
||||
_eventAggregator.GetEvent<SysUserEvents.LoginOutEvent>().Publish(AppSession.CurrentUser!);
|
||||
Logout();
|
||||
}
|
||||
// var messageBoxResult = HandyControl.Controls.MessageBox.Show(
|
||||
// $"确定退出登录吗?",
|
||||
// "确认登出",
|
||||
//MessageBoxButton.OKCancel,
|
||||
//MessageBoxImage.Warning);
|
||||
|
||||
// if (messageBoxResult != MessageBoxResult.OK) return;
|
||||
|
||||
// // 发布登出事件
|
||||
// _eventAggregator.GetEvent<SysUserEvents.LoginOutEvent>().Publish(AppSession.CurrentUser!);
|
||||
|
||||
// Logout();
|
||||
}
|
||||
|
||||
#region 主题
|
||||
/// <summary>
|
||||
/// 打开系统设置
|
||||
/// </summary>
|
||||
private void OpenAppSettings()
|
||||
{
|
||||
IsAppSettingsOpen = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置系统设置
|
||||
/// </summary>
|
||||
private void ResetAppSettings()
|
||||
{
|
||||
// 重置为默认值
|
||||
// new AppSettingsViewModel().Adapt(AppSettingsViewModel);
|
||||
|
||||
AppSettingsViewModel = new AppSettingsViewModel();
|
||||
AppSettingsViewModel.UpdateSkin(AppSettingsViewModel.SkinType!.Value);
|
||||
AppSettingsViewModel.UpdateAppSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载系统设置
|
||||
/// </summary>
|
||||
private void LoadAppSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = AppSettingsViewModel.GetFilePath();
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
AppSettingsViewModel = new AppSettingsViewModel();
|
||||
AppSettingsViewModel.UpdateSkin(AppSettingsViewModel.SkinType!.Value);
|
||||
AppSettingsViewModel.SaveAppSettings();
|
||||
return;
|
||||
}
|
||||
var json = File.ReadAllText(filePath);
|
||||
var appSettings = JsonConvert.DeserializeObject<AppSettings>(json);
|
||||
AppSettingsViewModel = appSettings.Adapt<AppSettingsViewModel>();
|
||||
AppSettingsViewModel.SkinType = AppSettingsViewModel.SyncWithSystem ? AppSettingsViewModel.GetSkinTypeBySystem() : AppSettingsViewModel.SkinType;
|
||||
AppSettingsViewModel.UpdateSkin(AppSettingsViewModel.SkinType!.Value);
|
||||
AppSettingsViewModel.UpdateAppSettings();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"加载系统设置失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task StartBackendConnectivityLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
bool connected = false;
|
||||
try
|
||||
{
|
||||
// 每轮都重新读取配置,保存服务器设置后可即时生效
|
||||
var baseUrl = _configuration.GetValue<string>("JeecgIntegration:BaseUrl")?.TrimEnd('/');
|
||||
var userListPath = _configuration.GetValue<string>("JeecgIntegration:UserListPath") ?? "/sys/user/scada/queryUser";
|
||||
var probeUrl = string.IsNullOrWhiteSpace(baseUrl)
|
||||
? string.Empty
|
||||
: $"{baseUrl}{userListPath}?pageNo=1&pageSize=1&includeDetail=false";
|
||||
if (!string.IsNullOrWhiteSpace(probeUrl))
|
||||
{
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, probeUrl);
|
||||
using var resp = await _httpClient.SendAsync(req, cancellationToken);
|
||||
connected = resp.IsSuccessStatusCode;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
connected = false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
IsBackendConnected = connected;
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略窗口关闭后的调度异常
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(ConnectivityCheckIntervalSeconds), cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenServerSettings()
|
||||
{
|
||||
_dialogService.ShowDialog("ServerSettingsDialog", r => { });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空资源
|
||||
/// </summary>
|
||||
private void Destroy(SysUser? sysUser = null)
|
||||
{
|
||||
if (_openOrActivateTabToken != null)
|
||||
{
|
||||
_eventAggregator
|
||||
.GetEvent<TabSourceSelectedEvent>()
|
||||
.Unsubscribe(_openOrActivateTabToken);
|
||||
|
||||
_openOrActivateTabToken = null;
|
||||
}
|
||||
|
||||
if (_tabClosedToken != null)
|
||||
{
|
||||
_eventAggregator
|
||||
.GetEvent<TabClosedEvent>()
|
||||
.Unsubscribe(_tabClosedToken);
|
||||
|
||||
_tabClosedToken = null;
|
||||
}
|
||||
|
||||
if (_refreshTabToken != null)
|
||||
{
|
||||
_eventAggregator
|
||||
.GetEvent<TabRefreshEvent>()
|
||||
.Unsubscribe(_refreshTabToken);
|
||||
|
||||
_refreshTabToken = null;
|
||||
}
|
||||
|
||||
if (_loginOutToken != null)
|
||||
{
|
||||
_eventAggregator
|
||||
.GetEvent<SysUserEvents.LoginOutEvent>()
|
||||
.Unsubscribe(_loginOutToken);
|
||||
|
||||
_loginOutToken = null;
|
||||
}
|
||||
|
||||
if (_authService != null)
|
||||
{
|
||||
_authService.UserChanged -= OnUserChanged;
|
||||
}
|
||||
|
||||
if (!_backendConnectivityCts.IsCancellationRequested)
|
||||
{
|
||||
_backendConnectivityCts.Cancel();
|
||||
}
|
||||
|
||||
_jeecgUserSyncCoordinator.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
27
yy-admin-master/YY.Admin/ViewModels/NotFoundViewModel.cs
Normal file
27
yy-admin-master/YY.Admin/ViewModels/NotFoundViewModel.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using YY.Admin.Core.Const;
|
||||
|
||||
namespace YY.Admin.ViewModels
|
||||
{
|
||||
public class NotFoundViewModel : BindableBase
|
||||
{
|
||||
private readonly IRegionManager _regionManager;
|
||||
|
||||
public NotFoundViewModel(IRegionManager regionManager)
|
||||
{
|
||||
_regionManager = regionManager;
|
||||
GoHomeCommand = new DelegateCommand(GoHome);
|
||||
}
|
||||
|
||||
public DelegateCommand GoHomeCommand { get; }
|
||||
|
||||
private void GoHome()
|
||||
{
|
||||
_regionManager.RequestNavigate(CommonConst.ContentRegion, "DashboardView");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using HandyControl.Controls;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Extension;
|
||||
using YY.Admin.Core.Helper;
|
||||
using YY.Admin.Services.Service;
|
||||
using YY.Admin.ViewModels.Control;
|
||||
|
||||
namespace YY.Admin.ViewModels.SysManage;
|
||||
|
||||
public class DataDictionaryManagementViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IJeecgDictSyncService _dictSyncService;
|
||||
|
||||
private PaginationDataGridViewModel<JeecgDictItemOutput> _paginationDataGridViewModel;
|
||||
public PaginationDataGridViewModel<JeecgDictItemOutput> PaginationDataGridViewModel
|
||||
{
|
||||
get => _paginationDataGridViewModel;
|
||||
set => SetProperty(ref _paginationDataGridViewModel, value);
|
||||
}
|
||||
|
||||
private PageJeecgDictItemInput _input;
|
||||
public PageJeecgDictItemInput Input
|
||||
{
|
||||
get => _input;
|
||||
set => SetProperty(ref _input, value);
|
||||
}
|
||||
|
||||
public List<KeyValuePair<string, int>> StatusList =>
|
||||
Enum.GetValues(typeof(StatusEnum))
|
||||
.Cast<StatusEnum>()
|
||||
.Select(e => new KeyValuePair<string, int>(e.GetDescription(), (int)e))
|
||||
.ToList();
|
||||
|
||||
public DelegateCommand SearchCommand { get; }
|
||||
public DelegateCommand ResetCommand { get; }
|
||||
public DelegateCommand SyncCommand { get; }
|
||||
|
||||
public DataDictionaryManagementViewModel(
|
||||
IJeecgDictSyncService dictSyncService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_dictSyncService = dictSyncService;
|
||||
_paginationDataGridViewModel = new PaginationDataGridViewModel<JeecgDictItemOutput>(FetchAsync);
|
||||
_input = new PageJeecgDictItemInput();
|
||||
|
||||
SearchCommand = new DelegateCommand(async () => await SearchAsync());
|
||||
ResetCommand = new DelegateCommand(async () => await ResetAsync());
|
||||
SyncCommand = new DelegateCommand(async () => await SyncAsync());
|
||||
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
await UIHelper.WaitForRenderAsync();
|
||||
await PaginationDataGridViewModel.LoadDataAsync();
|
||||
}
|
||||
|
||||
private async Task<(IEnumerable<JeecgDictItemOutput> data, int totalCount)> FetchAsync()
|
||||
{
|
||||
Input.Page = PaginationDataGridViewModel.PageIndex;
|
||||
Input.PageSize = PaginationDataGridViewModel.DataCountPerPage;
|
||||
var result = await _dictSyncService.PageAsync(Input);
|
||||
return (result.Items, result.Total);
|
||||
}
|
||||
|
||||
private async Task SearchAsync()
|
||||
{
|
||||
PaginationDataGridViewModel.PageIndex = 1;
|
||||
await PaginationDataGridViewModel.LoadDataAsync();
|
||||
}
|
||||
|
||||
private async Task ResetAsync()
|
||||
{
|
||||
Input = new PageJeecgDictItemInput();
|
||||
await UIHelper.WaitForRenderAsync();
|
||||
await SearchAsync();
|
||||
}
|
||||
|
||||
private async Task SyncAsync()
|
||||
{
|
||||
var count = await _dictSyncService.SyncFromJeecgAsync();
|
||||
if (count > 0)
|
||||
{
|
||||
Growl.Success($"同步完成,共处理 {count} 条数据字典项");
|
||||
}
|
||||
else
|
||||
{
|
||||
Growl.Warning("未同步到数据字典,请确认后端可访问");
|
||||
}
|
||||
await SearchAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using YY.Admin.Services.Service.User;
|
||||
using YY.Admin.Services.Service;
|
||||
using YY.Admin.ViewModels.Control;
|
||||
|
||||
namespace YY.Admin.ViewModels.SysManage
|
||||
{
|
||||
public class RoleManagementViewModel : BaseViewModel
|
||||
{
|
||||
public DelegateCommand AlertDialogCommand { get; }
|
||||
public DelegateCommand ConfirmDialogCommand { get; }
|
||||
public DelegateCommand ErrorDialogCommand { get; }
|
||||
public DelegateCommand SuccessDialogCommand { get; }
|
||||
public DelegateCommand WarningDialogCommand { get; }
|
||||
|
||||
public RoleManagementViewModel(
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager
|
||||
) : base(container, regionManager)
|
||||
{
|
||||
AlertDialogCommand = new DelegateCommand( () => AlertDialogAsync());
|
||||
ConfirmDialogCommand = new DelegateCommand( () => ConfirmDialogAsync());
|
||||
ErrorDialogCommand = new DelegateCommand( () => ErrorDialogAsync());
|
||||
SuccessDialogCommand = new DelegateCommand( () => SuccessDialogAsync());
|
||||
WarningDialogCommand = new DelegateCommand( () => WarningDialogAsync());
|
||||
}
|
||||
|
||||
private void WarningDialogAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private void SuccessDialogAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private void ErrorDialogAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private void ConfirmDialogAsync()
|
||||
{
|
||||
base.Confirm("测试Confirm");
|
||||
}
|
||||
|
||||
private void AlertDialogAsync()
|
||||
{
|
||||
base.Alert("测试Alert");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using HandyControl.Controls;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Extension;
|
||||
using YY.Admin.Core.Helper;
|
||||
using YY.Admin.Services.Service;
|
||||
using YY.Admin.Services.Service.Tenant;
|
||||
using YY.Admin.ViewModels.Control;
|
||||
|
||||
namespace YY.Admin.ViewModels.SysManage
|
||||
{
|
||||
public class TenantManagementViewModel : BaseViewModel
|
||||
{
|
||||
private readonly ISysTenantSyncService _tenantSyncService;
|
||||
|
||||
private PaginationDataGridViewModel<TenantOutput> _paginationDataGridViewModel;
|
||||
public PaginationDataGridViewModel<TenantOutput> PaginationDataGridViewModel
|
||||
{
|
||||
get => _paginationDataGridViewModel;
|
||||
set => SetProperty(ref _paginationDataGridViewModel, value);
|
||||
}
|
||||
|
||||
private PageTenantInput _input;
|
||||
public PageTenantInput Input
|
||||
{
|
||||
get => _input;
|
||||
set => SetProperty(ref _input, value);
|
||||
}
|
||||
|
||||
public List<KeyValuePair<string, int>> StatusList =>
|
||||
Enum.GetValues(typeof(StatusEnum))
|
||||
.Cast<StatusEnum>()
|
||||
.Select(e => new KeyValuePair<string, int>(e.GetDescription(), (int)e))
|
||||
.ToList();
|
||||
|
||||
public DelegateCommand SearchCommand { get; }
|
||||
public DelegateCommand ResetCommand { get; }
|
||||
public DelegateCommand SyncCommand { get; }
|
||||
|
||||
public TenantManagementViewModel(
|
||||
ISysTenantSyncService tenantSyncService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_tenantSyncService = tenantSyncService;
|
||||
_paginationDataGridViewModel = new PaginationDataGridViewModel<TenantOutput>(FetchTenantsAsync);
|
||||
_input = new PageTenantInput();
|
||||
|
||||
SearchCommand = new DelegateCommand(async () => await SearchAsync());
|
||||
ResetCommand = new DelegateCommand(async () => await ResetAsync());
|
||||
SyncCommand = new DelegateCommand(async () => await SyncAsync());
|
||||
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
await UIHelper.WaitForRenderAsync();
|
||||
await PaginationDataGridViewModel.LoadDataAsync();
|
||||
}
|
||||
|
||||
private async Task<(IEnumerable<TenantOutput> data, int totalCount)> FetchTenantsAsync()
|
||||
{
|
||||
Input.Page = PaginationDataGridViewModel.PageIndex;
|
||||
Input.PageSize = PaginationDataGridViewModel.DataCountPerPage;
|
||||
var result = await _tenantSyncService.PageAsync(Input);
|
||||
return (result.Items, result.Total);
|
||||
}
|
||||
|
||||
private async Task SearchAsync()
|
||||
{
|
||||
PaginationDataGridViewModel.PageIndex = 1;
|
||||
await PaginationDataGridViewModel.LoadDataAsync();
|
||||
}
|
||||
|
||||
private async Task ResetAsync()
|
||||
{
|
||||
Input = new PageTenantInput();
|
||||
await UIHelper.WaitForRenderAsync();
|
||||
await SearchAsync();
|
||||
}
|
||||
|
||||
private async Task SyncAsync()
|
||||
{
|
||||
var count = await _tenantSyncService.SyncFromJeecgAsync();
|
||||
if (count > 0)
|
||||
{
|
||||
Growl.Success($"同步完成,共处理 {count} 条租户数据");
|
||||
}
|
||||
else
|
||||
{
|
||||
Growl.Warning("未同步到租户数据,请确认已使用Jeecg账号登录且后端可访问");
|
||||
}
|
||||
await SearchAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
using HandyControl.Controls;
|
||||
using HandyControl.Data;
|
||||
using HandyControl.Tools.Extension;
|
||||
using System.Collections.ObjectModel;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.FluentValidation;
|
||||
using YY.Admin.Services.Service;
|
||||
using YY.Admin.Services.Service.User;
|
||||
|
||||
namespace YY.Admin.ViewModels.SysManage
|
||||
{
|
||||
public class UserEditDialogViewModel : BaseViewModel, IDialogResultable<bool>
|
||||
{
|
||||
private SysUser? _sysUser;
|
||||
private readonly ISysUserService _sysUserService;
|
||||
|
||||
public SysUserValidator SysUserValidator { get; private set; }
|
||||
|
||||
public SysUser? SysUser
|
||||
{
|
||||
get => _sysUser;
|
||||
set {
|
||||
SetProperty(ref _sysUser, value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAddMode => SysUser?.Id == 0;
|
||||
|
||||
public string DialogTitle => IsAddMode ? "新增用户" : "编辑用户";
|
||||
|
||||
// 性别枚举列表
|
||||
//public List<KeyValuePair<string, int>> GenderList =>
|
||||
// Enum.GetValues(typeof(GenderEnum))
|
||||
// .Cast<GenderEnum>()
|
||||
// .Select(e => new KeyValuePair<string, int>(e.GetDescription(), (int)e))
|
||||
// .ToList();
|
||||
|
||||
public ObservableCollection<GenderEnum> GenderList { get; }
|
||||
= new ObservableCollection<GenderEnum>(Enum.GetValues(typeof(GenderEnum)).Cast<GenderEnum>().ToArray());
|
||||
|
||||
public ObservableCollection<StatusEnum> StatusOptions { get; }
|
||||
= new ObservableCollection<StatusEnum>(Enum.GetValues(typeof(StatusEnum)).Cast<StatusEnum>().ToArray());
|
||||
|
||||
//// 状态枚举列表
|
||||
//public List<KeyValuePair<string, int>> StatusList =>
|
||||
// Enum.GetValues(typeof(StatusEnum))
|
||||
// .Cast<StatusEnum>()
|
||||
// .Select(e => new KeyValuePair<string, int>(e.GetDescription(), (int)e))
|
||||
// .ToList();
|
||||
|
||||
public DelegateCommand SaveCommand { get; private set; }
|
||||
public DelegateCommand CancelCommand { get; private set; }
|
||||
|
||||
public DelegateCommand<object> StatusSelectedCommand { get; }
|
||||
|
||||
private bool _result;
|
||||
public bool Result { get => _result; set => SetProperty(ref _result, value); }
|
||||
public Action? CloseAction { get; set; }
|
||||
|
||||
public UserEditDialogViewModel(
|
||||
ISysUserService sysUserService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_sysUserService = sysUserService;
|
||||
SysUserValidator = new SysUserValidator(sysUserService);
|
||||
//SysUser = new SysUser();
|
||||
|
||||
StatusSelectedCommand = new DelegateCommand<object>(OnStatusSelected);
|
||||
|
||||
SaveCommand = new DelegateCommand(async () => await SaveUserAsync());
|
||||
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
|
||||
}
|
||||
|
||||
private void OnStatusSelected(object statusObj)
|
||||
{
|
||||
if (statusObj is StatusEnum status)
|
||||
{
|
||||
SysUser?.Status = status;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SaveUserAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsAddMode)
|
||||
{
|
||||
var result = await _sysUserService.CreateAsync(SysUser!);
|
||||
Result = result > 0;
|
||||
if (Result)
|
||||
{
|
||||
Growl.Success(new GrowlInfo
|
||||
{
|
||||
Message = "新增用户成功!",
|
||||
ShowDateTime = false,
|
||||
WaitTime = 1
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Growl.Error("新增用户失败!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await _sysUserService.UpdateAsync(SysUser!);
|
||||
Result = result > 0;
|
||||
if (Result)
|
||||
{
|
||||
Growl.Success("修改用户成功!");
|
||||
}
|
||||
else
|
||||
{
|
||||
Growl.Error("修改用户失败!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
CloseAction?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Growl.Error($"操作失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化编辑数据
|
||||
public void InitializeForEdit(UserOutput user)
|
||||
{
|
||||
if (user != null)
|
||||
{
|
||||
SysUser = new SysUser
|
||||
{
|
||||
Id = user.Id,
|
||||
//Account = user.Account,
|
||||
RealName = user.RealName,
|
||||
NickName = user.NickName,
|
||||
//Phone = user.Phone,
|
||||
Sex = user.Sex,
|
||||
Birthday = user.Birthday,
|
||||
Age = user.Age,
|
||||
//AccountType = user.AccountType,
|
||||
Status = user.Status
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化新增数据
|
||||
public void InitializeForAdd()
|
||||
{
|
||||
SysUser = new SysUser();
|
||||
//IsAddMode = true;
|
||||
RaisePropertyChanged(nameof(IsAddMode));
|
||||
RaisePropertyChanged(nameof(DialogTitle));
|
||||
}
|
||||
|
||||
//private readonly SysUserValidator _addValidator = new SysUserValidator(true);
|
||||
//private readonly SysUserValidator _editValidator = new SysUserValidator(false);
|
||||
|
||||
//public string this[string columnName]
|
||||
//{
|
||||
// get
|
||||
// {
|
||||
// if (SysUser == null) return string.Empty;
|
||||
|
||||
// var validator = IsAddMode ? _addValidator : _editValidator;
|
||||
// var result = validator.Validate(SysUser);
|
||||
|
||||
// var error = result.Errors.FirstOrDefault(e => e.PropertyName == columnName);
|
||||
// return error?.ErrorMessage;
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,607 @@
|
||||
using HandyControl.Controls;
|
||||
using HandyControl.Tools.Extension;
|
||||
using SqlSugar;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Extension;
|
||||
using YY.Admin.Core.Helper;
|
||||
using YY.Admin.Services.Service;
|
||||
using YY.Admin.Services.Service.User;
|
||||
using YY.Admin.ViewModels.Control;
|
||||
using YY.Admin.Views.SysManage;
|
||||
|
||||
namespace YY.Admin.ViewModels.SysManage
|
||||
{
|
||||
public class UserManagementViewModel : BaseViewModel
|
||||
{
|
||||
private PaginationDataGridViewModel<UserOutput> _paginationDataGridViewModel;
|
||||
|
||||
private PageUserInput _userInput;
|
||||
|
||||
private readonly ISysUserService _sysUserService;
|
||||
|
||||
private readonly IDialogService _dialogService;
|
||||
private SubscriptionToken? _jeecgSyncToken;
|
||||
public PaginationDataGridViewModel<UserOutput> PaginationDataGridViewModel
|
||||
{
|
||||
get => _paginationDataGridViewModel;
|
||||
set => SetProperty(ref _paginationDataGridViewModel, value);
|
||||
}
|
||||
|
||||
|
||||
public PageUserInput UserInput
|
||||
{ get => _userInput;
|
||||
set => SetProperty(ref _userInput, value);
|
||||
}
|
||||
|
||||
// 性别枚举列表属性
|
||||
public List<GenderEnum> GenderList =>
|
||||
[.. Enum.GetValues(typeof(GenderEnum)).Cast<GenderEnum>()];
|
||||
|
||||
// 状态枚举列表属性
|
||||
public List<KeyValuePair<string, int>> StatusList =>
|
||||
Enum.GetValues(typeof(StatusEnum))
|
||||
.Cast<StatusEnum>()
|
||||
.Select(e => new KeyValuePair<string, int>(e.GetDescription(), (int)e))
|
||||
.ToList();
|
||||
|
||||
// 全选
|
||||
//private bool? _isAllSelected = false;
|
||||
//public bool? IsAllSelected
|
||||
//{
|
||||
// get => _isAllSelected;
|
||||
// set
|
||||
// {
|
||||
// if (SetProperty(ref _isAllSelected, value) && value.HasValue)
|
||||
// {
|
||||
// // 只有当值真正改变时才执行全选逻辑
|
||||
// SelectAll(value.Value);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// 选中用户数量
|
||||
private int _selectedCount;
|
||||
public int SelectedCount
|
||||
{
|
||||
get => _selectedCount;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedCount, value))
|
||||
{
|
||||
// 当 SelectedCount 变化时,触发 HasSelectedItems 通知
|
||||
RaisePropertyChanged(nameof(HasSelectedItems));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 是否有选中项
|
||||
public bool HasSelectedItems => SelectedCount > 0;
|
||||
|
||||
// 批量删除
|
||||
public DelegateCommand BatchDeleteCommand { get; private set; }
|
||||
|
||||
// 单个删除
|
||||
public DelegateCommand<long?> DeleteCommand { get; private set; }
|
||||
|
||||
// 查询
|
||||
public DelegateCommand SearchCommand { get; private set; }
|
||||
|
||||
public DelegateCommand ResetCommand { get; private set; }
|
||||
|
||||
public DelegateCommand AddCommand { get; private set; }
|
||||
public DelegateCommand<UserOutput> EditCommand { get; private set; }
|
||||
|
||||
public DelegateCommand<UserOutput> StatusToggleCommand { get; private set; }
|
||||
|
||||
|
||||
// 行选择改变命令
|
||||
//public DelegateCommand<UserOutput> RowSelectionChangedCommand { get; private set; }
|
||||
|
||||
public UserManagementViewModel(
|
||||
ISysUserService sysUserService,
|
||||
IContainerExtension container,
|
||||
IDialogService dialogService,
|
||||
IRegionManager regionManager
|
||||
) : base(container, regionManager)
|
||||
{
|
||||
_sysUserService= sysUserService;
|
||||
_dialogService = dialogService;
|
||||
// 创建分页控件的 ViewModel,传递一个获取数据的委托
|
||||
_paginationDataGridViewModel = new PaginationDataGridViewModel<UserOutput>(FetchUsersAsync);
|
||||
_userInput = new PageUserInput();
|
||||
|
||||
// 初始化批量删除命令
|
||||
BatchDeleteCommand = new DelegateCommand(async () => await BatchDelete(),
|
||||
() => HasSelectedItems);
|
||||
|
||||
DeleteCommand = new DelegateCommand<long?>(async (id) => {
|
||||
if (id.HasValue)
|
||||
{
|
||||
await Delete(id.Value);
|
||||
}
|
||||
});
|
||||
|
||||
SearchCommand = new DelegateCommand(async () => await ReadAsync());
|
||||
|
||||
ResetCommand = new DelegateCommand(async () => await ResetFormAsync());
|
||||
|
||||
AddCommand = new DelegateCommand(async () => await ShowAddDialog());
|
||||
|
||||
EditCommand = new DelegateCommand<UserOutput>(async (user) => await ShowEditDialog(user));
|
||||
|
||||
StatusToggleCommand = new DelegateCommand<UserOutput>(async (user) => await ToggleStatus(user));
|
||||
|
||||
//RowSelectionChangedCommand = new DelegateCommand<UserOutput>(OnRowSelectionChanged);
|
||||
|
||||
// 监听数据变化
|
||||
//PaginationDataGridViewModel.PropertyChanged += OnPaginationDataChanged;
|
||||
//_ = PaginationDataGridViewModel.LoadDataAsync(); // 默认加载第一页数据
|
||||
|
||||
//_dataList = GetDemoDataList(10);
|
||||
|
||||
// 启动异步初始化,但不等待
|
||||
_ = InitializeAsync();
|
||||
|
||||
// Jeecg 同构用户表同步完成后,自动刷新当前列表
|
||||
_jeecgSyncToken = _eventAggregator
|
||||
.GetEvent<SysUserEvents.JeecgMirrorUsersSyncedEvent>()
|
||||
.Subscribe(async _ =>
|
||||
{
|
||||
await PaginationDataGridViewModel.LoadDataAsync();
|
||||
}, ThreadOption.UIThread);
|
||||
}
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 不阻塞UI线程,让UI先渲染(效果就是先看到界面,然后再加载表格数据,提升用户体验)
|
||||
await UIHelper.WaitForRenderAsync();
|
||||
|
||||
// 然后异步加载数据
|
||||
await PaginationDataGridViewModel.LoadDataAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"初始化失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void CleanUp()
|
||||
{
|
||||
base.CleanUp();
|
||||
if (_jeecgSyncToken != null)
|
||||
{
|
||||
_eventAggregator.GetEvent<SysUserEvents.JeecgMirrorUsersSyncedEvent>().Unsubscribe(_jeecgSyncToken);
|
||||
_jeecgSyncToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 行选择改变事件处理
|
||||
//private void OnRowSelectionChanged(UserOutput user)
|
||||
//{
|
||||
// if (user == null) return;
|
||||
|
||||
// user.IsSelected = !user.IsSelected;
|
||||
|
||||
// // 更新选中数量
|
||||
// UpdateSelectedCount();
|
||||
|
||||
// // 更新全选状态
|
||||
// UpdateSelectAllStatus();
|
||||
|
||||
// // 更新批量删除命令状态
|
||||
// BatchDeleteCommand.RaiseCanExecuteChanged();
|
||||
|
||||
// // 调试输出
|
||||
// //System.Diagnostics.Debug.WriteLine($"用户 {user.UserName} 选中状态: {user.IsSelected}");
|
||||
//}
|
||||
|
||||
|
||||
//private void OnPaginationDataChanged(object sender, PropertyChangedEventArgs e)
|
||||
//{
|
||||
// if (e.PropertyName == nameof(PaginationDataGridViewModel.Data))
|
||||
// {
|
||||
// RegisterSelectionEvents();
|
||||
// UpdateSelectedCount();
|
||||
// }
|
||||
//}
|
||||
|
||||
// 注册选择事件监听
|
||||
//private void RegisterSelectionEvents()
|
||||
//{
|
||||
// if (PaginationDataGridViewModel.Data == null) return;
|
||||
|
||||
// foreach (var user in PaginationDataGridViewModel.Data.OfType<UserOutput>())
|
||||
// {
|
||||
// user.SelectionChanged -= OnUserSelectionChanged;
|
||||
// user.SelectionChanged += OnUserSelectionChanged;
|
||||
// }
|
||||
//}
|
||||
|
||||
//private void OnUserSelectionChanged(object sender, EventArgs e)
|
||||
//{
|
||||
// UpdateSelectedCount();
|
||||
// // 更新全选状态
|
||||
// UpdateSelectAllStatus();
|
||||
// BatchDeleteCommand.RaiseCanExecuteChanged();
|
||||
//}
|
||||
|
||||
|
||||
public void UpdateSelectionState()
|
||||
{
|
||||
UpdateSelectedCount();
|
||||
//UpdateSelectAllStatus();
|
||||
BatchDeleteCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
// 更新选中数量
|
||||
private void UpdateSelectedCount()
|
||||
{
|
||||
if (PaginationDataGridViewModel.Data == null)
|
||||
{
|
||||
SelectedCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedCount = PaginationDataGridViewModel.Data.Count(user =>
|
||||
user is UserOutput output && output.IsSelected);
|
||||
}
|
||||
|
||||
// 获取选中的用户ID列表
|
||||
private List<long> GetSelectedUserIds()
|
||||
{
|
||||
if (PaginationDataGridViewModel.Data == null)
|
||||
return new List<long>();
|
||||
|
||||
return PaginationDataGridViewModel.Data
|
||||
.Where(user => user is UserOutput output && output.IsSelected)
|
||||
.Select(user => (user as UserOutput).Id)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// 批量删除执行方法
|
||||
private async Task BatchDelete()
|
||||
{
|
||||
var selectedUserIds = GetSelectedUserIds();
|
||||
if (selectedUserIds.Count == 0) return;
|
||||
|
||||
var messageBoxResult = HandyControl.Controls.MessageBox.Show(
|
||||
$"确定要删除选中的 {selectedUserIds.Count} 个用户吗?此操作不可恢复!",
|
||||
"确认删除",
|
||||
MessageBoxButton.OKCancel,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (messageBoxResult != MessageBoxResult.OK) return;
|
||||
|
||||
int count = await _sysUserService.BatchDeleteAsync(selectedUserIds);
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
PaginationDataGridViewModel.PageIndex = 1;
|
||||
await PaginationDataGridViewModel.LoadDataAsync();
|
||||
}
|
||||
|
||||
// 1. 准备传递给对话框的参数
|
||||
//var parameters = new DialogParameters();
|
||||
//parameters.Add("Title", "操作确认");
|
||||
//parameters.Add("Message", "确定要执行此操作吗?");
|
||||
//parameters.Add("ConfirmButtonText", "确认"); // 可选:自定义确认按钮文本
|
||||
//parameters.Add("CancelButtonText", "取消"); // 可选:自定义取消按钮文本
|
||||
|
||||
// 2. 显示对话框并处理返回结果
|
||||
//_dialogService.ShowDialog(
|
||||
// "ConfirmDialog", // 注册时使用的对话框名称
|
||||
// parameters,
|
||||
// result =>
|
||||
// {
|
||||
// // 3. 根据对话框返回结果执行后续逻辑
|
||||
// if (result.Result == ButtonResult.OK)
|
||||
// {
|
||||
// // 用户点击了确认按钮
|
||||
// //ExecuteConfirmedAction();
|
||||
// }
|
||||
// else if (result.Result == ButtonResult.Cancel)
|
||||
// {
|
||||
// // 用户点击了取消按钮
|
||||
// //ExecuteCancelledAction();
|
||||
// }
|
||||
// }
|
||||
//);
|
||||
}
|
||||
|
||||
// 删除执行方法
|
||||
private async Task Delete(long id)
|
||||
{
|
||||
var messageBoxResult = HandyControl.Controls.MessageBox.Show(
|
||||
$"确定删除ID为 {id} 的用户吗?此操作不可恢复!",
|
||||
"确认删除",
|
||||
MessageBoxButton.OKCancel,
|
||||
MessageBoxImage.Question,
|
||||
MessageBoxResult.No);
|
||||
|
||||
if (messageBoxResult != MessageBoxResult.OK) return;
|
||||
|
||||
int count = await _sysUserService.DeleteAsync(id);
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
PaginationDataGridViewModel.PageIndex = 1;
|
||||
await PaginationDataGridViewModel.LoadDataAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(IEnumerable<UserOutput> data, int totalCount)> FetchUsersAsync()
|
||||
{
|
||||
if (UserInput.EndTime.HasValue && UserInput.EndTime.HasValue && UserInput.EndTime < UserInput.BeginTime)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Error("开始时间不能大于结束时间!");
|
||||
//return (PaginationDataGridViewModel.Data, PaginationDataGridViewModel.TotalCount);
|
||||
throw new OperationCanceledException("时间条件不合法,取消查询");
|
||||
}
|
||||
UserInput.Page = PaginationDataGridViewModel.PageIndex;
|
||||
UserInput.PageSize = PaginationDataGridViewModel.DataCountPerPage;
|
||||
|
||||
// 延迟10秒
|
||||
//await Task.Delay(10000);
|
||||
|
||||
var rlt = await _sysUserService.PageAsync(UserInput);
|
||||
return (rlt.Items, rlt.Total); // 返回分页数据和总条目数
|
||||
}
|
||||
|
||||
private async Task ReadAsync()
|
||||
{
|
||||
PaginationDataGridViewModel.PageIndex = 1;
|
||||
await PaginationDataGridViewModel.LoadDataAsync();
|
||||
|
||||
}
|
||||
|
||||
public async Task ResetFormAsync()
|
||||
{
|
||||
UserInput = new PageUserInput();
|
||||
|
||||
// 立即更新UI
|
||||
await UIHelper.WaitForRenderAsync();
|
||||
|
||||
await ReadAsync();
|
||||
}
|
||||
|
||||
// 显示新增对话框
|
||||
private async Task ShowAddDialog()
|
||||
{
|
||||
try
|
||||
{
|
||||
//var vm = new UserEditDialogViewModel(_sysUserService, _container);
|
||||
//var view = new UserEditDialogView { DataContext = vm };
|
||||
|
||||
var result = await HandyControl.Controls.Dialog.Show<UserEditDialogView>()
|
||||
.Initialize<UserEditDialogViewModel>(v => v.InitializeForAdd())
|
||||
.GetResultAsync<bool>();
|
||||
// 使用泛型方式,通过 Initialize 方法设置数据
|
||||
//var result = await HandyControl.Controls.Dialog.Show<UserEditDialogView>()
|
||||
// .Initialize<UserEditDialogViewModel>(vm =>
|
||||
// {
|
||||
// vm.InitializeForAdd();
|
||||
// })
|
||||
// .GetResultAsync<bool?>();
|
||||
|
||||
if (result)
|
||||
{
|
||||
await PaginationDataGridViewModel.LoadDataAsync();
|
||||
//Growl.Success("用户新增成功!");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Growl.Error($"打开对话框失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 显示编辑对话框
|
||||
private async Task ShowEditDialog(UserOutput user)
|
||||
{
|
||||
if (user == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await HandyControl.Controls.Dialog.Show<UserEditDialogView>()
|
||||
.Initialize<UserEditDialogViewModel>(vm =>
|
||||
{
|
||||
vm.InitializeForEdit(user);
|
||||
})
|
||||
.GetResultAsync<bool>();
|
||||
|
||||
if (result)
|
||||
{
|
||||
await PaginationDataGridViewModel.LoadDataAsync();
|
||||
//Growl.Success("用户修改成功!");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Growl.Error($"打开对话框失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ToggleStatus(UserOutput user)
|
||||
{
|
||||
if (user == null) return;
|
||||
|
||||
// 保存原始状态以便回滚
|
||||
var originalStatus = user.Status;
|
||||
try
|
||||
{
|
||||
// 先切换本地状态(提供即时反馈)
|
||||
user.Status = originalStatus == StatusEnum.Enable
|
||||
? StatusEnum.Disable
|
||||
: StatusEnum.Enable;
|
||||
|
||||
SysUser sysUser = new SysUser();
|
||||
sysUser.Id = user.Id;
|
||||
sysUser.Status = user.Status;
|
||||
|
||||
int count = await _sysUserService.ToggleStatus(sysUser);
|
||||
|
||||
if (count <= 0)
|
||||
{
|
||||
// 如果服务调用失败,回滚状态
|
||||
user.Status = originalStatus;
|
||||
Growl.Warning("状态切换失败");
|
||||
}
|
||||
} catch (Exception)
|
||||
{
|
||||
user.Status = originalStatus;
|
||||
Growl.Warning("状态切换失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 全选/取消全选方法
|
||||
//private void SelectAll(bool isSelected)
|
||||
//{
|
||||
// if (PaginationDataGridViewModel?.Data == null) return;
|
||||
|
||||
// foreach (var user in PaginationDataGridViewModel.Data.OfType<UserOutput>())
|
||||
// {
|
||||
// user.IsSelected = isSelected;
|
||||
// }
|
||||
|
||||
// // 更新选中数量
|
||||
// UpdateSelectedCount();
|
||||
//}
|
||||
|
||||
// 更新全选状态(根据当前选中情况)
|
||||
//public void UpdateSelectAllStatus()
|
||||
//{
|
||||
// if (PaginationDataGridViewModel?.Data == null || !PaginationDataGridViewModel.Data.Any())
|
||||
// {
|
||||
// IsAllSelected = false;
|
||||
// return;
|
||||
// }
|
||||
|
||||
// var selectedCount = PaginationDataGridViewModel.Data.Count(user =>
|
||||
// user is UserOutput output && output.IsSelected);
|
||||
// var totalCount = PaginationDataGridViewModel.Data.Count;
|
||||
|
||||
// if (selectedCount == 0)
|
||||
// {
|
||||
// IsAllSelected = false;
|
||||
// }
|
||||
// else if (selectedCount == totalCount)
|
||||
// {
|
||||
// IsAllSelected = true;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// IsAllSelected = null; // 部分选中状态
|
||||
// }
|
||||
|
||||
//}
|
||||
|
||||
//private string _searchText;
|
||||
//public string SearchText
|
||||
//{
|
||||
// get => _searchText;
|
||||
// set
|
||||
// {
|
||||
// if (SetProperty(ref _searchText, value))
|
||||
// {
|
||||
// Debug.WriteLine($"SearchText Updated: {value}");
|
||||
// FilterItems(value);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
//private DemoDataModel? _selectedItem;
|
||||
//public DemoDataModel? SelectedItem
|
||||
//{
|
||||
// get => _selectedItem;
|
||||
// set
|
||||
// {
|
||||
// if (SetProperty(ref _selectedItem, value))
|
||||
// {
|
||||
// Debug.WriteLine($"SelectedItem Updated: {value}");
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
//// 修改 Items 属性为 ManualObservableCollection
|
||||
//private ManualObservableCollection<DemoDataModel> _items = new();
|
||||
//public ManualObservableCollection<DemoDataModel> Items
|
||||
//{
|
||||
// get => _items;
|
||||
// set => SetProperty(ref _items, value);
|
||||
//}
|
||||
|
||||
//private readonly List<DemoDataModel> _dataList;
|
||||
|
||||
//private void FilterItems(string key)
|
||||
//{
|
||||
// //Items.CanNotify = false;
|
||||
|
||||
// Items.Clear();
|
||||
|
||||
// foreach (var data in _dataList)
|
||||
// {
|
||||
// if (data.Name.ToLower().Contains(key.ToLower()))
|
||||
// {
|
||||
// Items.Add(data);
|
||||
// }
|
||||
// }
|
||||
|
||||
// //RaisePropertyChanged(nameof(Items));
|
||||
// //Items.CanNotify = true;
|
||||
//}
|
||||
//List<DemoDataModel> GetDemoDataList(int count)
|
||||
//{
|
||||
// var list = new List<DemoDataModel>();
|
||||
// for (var i = 1; i <= count; i++)
|
||||
// {
|
||||
// var index = i % 6 + 1;
|
||||
// var model = new DemoDataModel
|
||||
// {
|
||||
// Index = i,
|
||||
// IsSelected = i % 2 == 0,
|
||||
// Name = $"Name{i}",
|
||||
// Type = (DemoType)index,
|
||||
// ImgPath = $"/HandyControlDemo;component/Resources/Img/Avatar/avatar{index}.png",
|
||||
// Remark = new string(i.ToString()[0], 10)
|
||||
// };
|
||||
// list.Add(model);
|
||||
// }
|
||||
|
||||
// return list;
|
||||
//}
|
||||
}
|
||||
|
||||
|
||||
// public class DemoDataModel
|
||||
//{
|
||||
// public int Index { get; set; }
|
||||
|
||||
// public string Name { get; set; } = string.Empty;
|
||||
|
||||
// public bool IsSelected { get; set; }
|
||||
|
||||
// public string Remark { get; set; } = string.Empty;
|
||||
|
||||
// public DemoType Type { get; set; }
|
||||
|
||||
// public string ImgPath { get; set; } = string.Empty;
|
||||
|
||||
// public List<DemoDataModel> DataList { get; set; } = [];
|
||||
//}
|
||||
|
||||
//public enum DemoType
|
||||
//{
|
||||
// Type1 = 1,
|
||||
// Type2,
|
||||
// Type3,
|
||||
// Type4,
|
||||
// Type5,
|
||||
// Type6
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
50
yy-admin-master/YY.Admin/Views/Control/MenuTreeView.xaml
Normal file
50
yy-admin-master/YY.Admin/Views/Control/MenuTreeView.xaml
Normal file
@@ -0,0 +1,50 @@
|
||||
<UserControl x:Class="YY.Admin.Views.Control.MenuTreeView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:YY.Admin.Views"
|
||||
xmlns:beh="clr-namespace:YY.Admin.Core.Behavior;assembly=YY.Admin.Core"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<TreeView
|
||||
x:Name="MenuTree"
|
||||
ItemsSource="{Binding MenuItems}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0">
|
||||
<TreeView.ItemContainerStyle>
|
||||
<Style TargetType="TreeViewItem" BasedOn="{StaticResource CusTreeViewItemBaseStyle}">
|
||||
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
|
||||
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
||||
<Setter Property="FontSize" Value="{StaticResource FontSize}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
|
||||
<!-- 禁止双击自动展开 -->
|
||||
<EventSetter Event="MouseDoubleClick" Handler="TreeViewItem_MouseDoubleClick"/>
|
||||
<!--<Setter Property="Margin" Value="0"/>-->
|
||||
<!-- 绑定命令到 TreeView 的 DataContext 下的 NavigateCommand -->
|
||||
<Setter
|
||||
Property="beh:TreeViewItemClickBehavior.Command"
|
||||
Value="{Binding DataContext.NavigateCommand,
|
||||
RelativeSource={RelativeSource AncestorType=TreeView}}"/>
|
||||
<!-- 把命令参数设为当前数据上下文(菜单项本身) -->
|
||||
<Setter Property="beh:TreeViewItemClickBehavior.CommandParameter" Value="{Binding}"/>
|
||||
</Style>
|
||||
</TreeView.ItemContainerStyle>
|
||||
<TreeView.ItemTemplate>
|
||||
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
|
||||
<StackPanel Orientation="Horizontal" Height="50">
|
||||
<!--菜单图标-->
|
||||
<TextBlock
|
||||
Text="{Binding Icon}"
|
||||
FontFamily="{StaticResource AntDesignIcon}"
|
||||
FontSize="16"
|
||||
VerticalAlignment="Center"
|
||||
Margin="20,0,12,0"/>
|
||||
<!--菜单名称-->
|
||||
<TextBlock Text="{Binding Name}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</HierarchicalDataTemplate>
|
||||
</TreeView.ItemTemplate>
|
||||
</TreeView>
|
||||
</UserControl>
|
||||
22
yy-admin-master/YY.Admin/Views/Control/MenuTreeView.xaml.cs
Normal file
22
yy-admin-master/YY.Admin/Views/Control/MenuTreeView.xaml.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace YY.Admin.Views.Control
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MenuTreeView.xaml
|
||||
/// </summary>
|
||||
public partial class MenuTreeView : UserControl
|
||||
{
|
||||
public MenuTreeView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void TreeViewItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 阻止双击默认展开行为
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<UserControl x:Class="YY.Admin.Views.Control.PaginationDataGridControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<!-- 底部分页区 -->
|
||||
<DockPanel LastChildFill="False" Margin="0 10 0 0">
|
||||
|
||||
<!-- 分页条(靠右) -->
|
||||
<hc:Pagination
|
||||
MaxPageCount="{Binding MaxPageCount}"
|
||||
PageIndex="{Binding PageIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
DataCountPerPage="{Binding DataCountPerPage}"
|
||||
IsJumpEnabled="True"
|
||||
DockPanel.Dock="Right"
|
||||
Margin="0,0,4,0">
|
||||
<hc:Interaction.Triggers>
|
||||
<hc:EventTrigger EventName="PageUpdated">
|
||||
<hc:EventToCommand Command="{Binding PageUpdatedCmd}" PassEventArgsToCommand="True" />
|
||||
</hc:EventTrigger>
|
||||
</hc:Interaction.Triggers>
|
||||
</hc:Pagination>
|
||||
|
||||
<hc:ComboBox
|
||||
Width="100"
|
||||
ItemsSource="{Binding PageSizes}"
|
||||
SelectedIndex="0"
|
||||
SelectedValue="{Binding DataCountPerPage, Mode=TwoWay}"
|
||||
DisplayMemberPath="Value"
|
||||
SelectedValuePath="Key"
|
||||
Margin="5,0 10 0"
|
||||
DockPanel.Dock="Right">
|
||||
<hc:Interaction.Triggers>
|
||||
<hc:EventTrigger EventName="SelectionChanged">
|
||||
<hc:EventToCommand Command="{Binding PageSizeUpdatedCmd}"
|
||||
PassEventArgsToCommand="True"/>
|
||||
</hc:EventTrigger>
|
||||
</hc:Interaction.Triggers>
|
||||
</hc:ComboBox>
|
||||
|
||||
<!-- 分页信息(靠左) -->
|
||||
<TextBlock
|
||||
Text="{Binding TotalCount, StringFormat='共 {0} 条'}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="10,0 5 0"
|
||||
DockPanel.Dock="Right"/>
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Threading;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Services.Service;
|
||||
|
||||
namespace YY.Admin.Views.Control
|
||||
{
|
||||
/// <summary>
|
||||
/// PaginationDataGridControl.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class PaginationDataGridControl : UserControl
|
||||
{
|
||||
public PaginationDataGridControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
//this.Loaded += PaginationDataGridControl_Loaded;
|
||||
}
|
||||
|
||||
// 确保用户在控制加载时触发列生成
|
||||
//private void PaginationDataGridControl_Loaded(object sender, RoutedEventArgs e)
|
||||
//{
|
||||
// // 确保 DataGrid 已完全加载
|
||||
// Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
|
||||
// {
|
||||
// // 确保 DataGrid 控件已初始化并且 ItemsSource 不为 null
|
||||
// var data = DataGrid.ItemsSource;
|
||||
// if (data == null)
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // 获取数据模型的类型
|
||||
// var type = data.GetType().GetGenericArguments().FirstOrDefault();
|
||||
// if (type == null) return;
|
||||
|
||||
// // 获取所有带有 BindDescriptionAttribute 的属性
|
||||
// var properties = type.GetProperties()
|
||||
// .Where(prop => Attribute.IsDefined(prop, typeof(BindDescriptionAttribute)))
|
||||
// .OrderBy(prop => ((BindDescriptionAttribute)prop.GetCustomAttribute(typeof(BindDescriptionAttribute))).DisplayIndex)
|
||||
// .ToList();
|
||||
|
||||
// foreach (var prop in properties)
|
||||
// {
|
||||
// var attribute = prop.GetCustomAttribute<BindDescriptionAttribute>();
|
||||
// if (attribute != null)
|
||||
// {
|
||||
// // 检查列是否已经存在
|
||||
// if (!ColumnExists(attribute.HeaderName))
|
||||
// {
|
||||
// var column = CreateDataGridColumn(attribute, prop);
|
||||
// if (column != null)
|
||||
// {
|
||||
// // 通过绑定添加列
|
||||
// DataGrid.Columns.Add(column);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }));
|
||||
//}
|
||||
|
||||
//// 检查列是否已经存在
|
||||
//private bool ColumnExists(string headerName)
|
||||
//{
|
||||
// return DataGrid.Columns.Any(c => c.Header.ToString() == headerName);
|
||||
//}
|
||||
|
||||
//private DataGridColumn CreateDataGridColumn(BindDescriptionAttribute attribute, PropertyInfo property)
|
||||
//{
|
||||
// DataGridColumn column = null;
|
||||
|
||||
// // 根据属性的显示方式来创建不同的列
|
||||
// switch (attribute.ShowAs)
|
||||
// {
|
||||
// case ShowScheme.普通文本:
|
||||
// column = new DataGridTextColumn
|
||||
// {
|
||||
// Header = attribute.HeaderName,
|
||||
// Binding = new Binding(property.Name),
|
||||
// Width = attribute.Width,
|
||||
// DisplayIndex = attribute.DisplayIndex
|
||||
// };
|
||||
// break;
|
||||
|
||||
// case ShowScheme.自定义:
|
||||
// // 如果需要自定义列,您可以在这里扩展并处理自定义类型的列
|
||||
// break;
|
||||
// }
|
||||
|
||||
// return column;
|
||||
//}
|
||||
}
|
||||
}
|
||||
193
yy-admin-master/YY.Admin/Views/Control/SidebarControl.xaml
Normal file
193
yy-admin-master/YY.Admin/Views/Control/SidebarControl.xaml
Normal file
@@ -0,0 +1,193 @@
|
||||
<UserControl x:Class="YY.Admin.Views.Control.SidebarControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:core="clr-namespace:YY.Admin.Core;assembly=YY.Admin.Core"
|
||||
xmlns:ctls="clr-namespace:YY.Admin.Core.Controls;assembly=YY.Admin.Core"
|
||||
xmlns:local="clr-namespace:YY.Admin.Views.Control">
|
||||
|
||||
<UserControl.Resources>
|
||||
|
||||
<!-- AntDesign 模板 -->
|
||||
<DataTemplate x:Key="AntDesignIconTemplate">
|
||||
<TextBlock
|
||||
FontFamily="{StaticResource AntDesignIcon}"
|
||||
FontSize="22"
|
||||
Text="{Binding Icon}"
|
||||
Margin="0,0,0,5"
|
||||
HorizontalAlignment="Center"/>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- MaterialDesign 模板 -->
|
||||
<DataTemplate x:Key="MaterialDesignIconTemplate">
|
||||
<md:PackIcon
|
||||
Kind="{Binding Icon}"
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="0,0,0,5"
|
||||
HorizontalAlignment="Center"/>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- FontAwesome 模板 -->
|
||||
<DataTemplate x:Key="FontawesomeIconTemplate">
|
||||
<ctls:FontAwesomeIcon
|
||||
Icon="{Binding Icon}"
|
||||
FontSize="22"
|
||||
Margin="0,0,0,5"
|
||||
HorizontalAlignment="Center"/>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="NavItemTemplate">
|
||||
<StackPanel Orientation="Vertical" Margin="5" HorizontalAlignment="Center">
|
||||
<ContentControl>
|
||||
<ContentControl.Style>
|
||||
<Style TargetType="ContentControl">
|
||||
<!-- 默认模板(AntDesign) -->
|
||||
<Setter Property="ContentTemplate" Value="{StaticResource AntDesignIconTemplate}"/>
|
||||
<!-- 绑定数据上下文 -->
|
||||
<Setter Property="Content" Value="{Binding}"/>
|
||||
|
||||
<!-- 触发器根据 IconType 切换模板 -->
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IconType}" Value="{x:Static core:IconTypeEnum.MaterialDesign}">
|
||||
<Setter Property="ContentTemplate" Value="{StaticResource MaterialDesignIconTemplate}"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IconType}" Value="{x:Static core:IconTypeEnum.FontAwesome}">
|
||||
<Setter Property="ContentTemplate" Value="{StaticResource FontawesomeIconTemplate}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ContentControl.Style>
|
||||
</ContentControl>
|
||||
<TextBlock Text="{Binding Name}" FontSize="12" TextTrimming="CharacterEllipsis" TextWrapping="NoWrap" TextAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<Style x:Key="NavItemStyle" TargetType="ListBoxItem" BasedOn="{StaticResource ListBoxItemBaseStyle}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Grid x:Name="templateRoot" Margin="0,0,0,5">
|
||||
<!-- 左侧指示器 -->
|
||||
<Border
|
||||
x:Name="indicator"
|
||||
Width="4"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Margin="6,0,0,0"
|
||||
CornerRadius="2"
|
||||
Background="{DynamicResource PrimaryBrush}"
|
||||
Opacity="0"
|
||||
Panel.ZIndex="1"
|
||||
Height="{Binding ActualHeight, ElementName=templateRoot, Converter={StaticResource PercentageConverter}, ConverterParameter=0.4}">
|
||||
<Border.RenderTransform>
|
||||
<TranslateTransform Y="10"/>
|
||||
</Border.RenderTransform>
|
||||
</Border>
|
||||
|
||||
<!-- 内容边框 -->
|
||||
<Border x:Name="border" CornerRadius="6" Background="Transparent" Margin="5,0">
|
||||
<ContentPresenter x:Name="content" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<ControlTemplate.Triggers>
|
||||
<!-- 选中动画 -->
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="border" Property="Background" Value="{DynamicResource SecondaryRegionBrush}"/>
|
||||
<Setter TargetName="content" Property="TextBlock.Foreground" Value="{DynamicResource PrimaryBrush}"/>
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="indicator"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="0.8"
|
||||
Duration="0:0:0.25"/>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="indicator"
|
||||
Storyboard.TargetProperty="RenderTransform.(TranslateTransform.Y)"
|
||||
To="0"
|
||||
Duration="0:0:0.25"/>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.EnterActions>
|
||||
<Trigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="indicator"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="0"
|
||||
Duration="0:0:0.25"/>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="indicator"
|
||||
Storyboard.TargetProperty="RenderTransform.(TranslateTransform.Y)"
|
||||
To="10"
|
||||
Duration="0:0:0.25"/>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.ExitActions>
|
||||
</Trigger>
|
||||
|
||||
<!-- 鼠标悬停 -->
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="border" Property="Background" Value="{DynamicResource SecondaryRegionBrush}"/>
|
||||
<Setter TargetName="content" Property="TextBlock.Foreground" Value="{DynamicResource PrimaryBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<!-- 鼠标点击触发命令 -->
|
||||
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="OnNavItemMouseDown"/>
|
||||
</Style>
|
||||
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,1,0">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<!-- 上部占满剩余空间 -->
|
||||
<RowDefinition Height="*"/>
|
||||
<!-- 底部自适应 -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 上半部分:主导航 -->
|
||||
<ListBox
|
||||
Grid.Row="0"
|
||||
Padding="0,5,0,0"
|
||||
BorderThickness="0"
|
||||
hc:BorderElement.CornerRadius="0"
|
||||
ItemsSource="{Binding TopNavItems}"
|
||||
SelectedItem="{Binding SelectedTopNavItem, Mode=TwoWay}"
|
||||
ItemTemplate="{StaticResource NavItemTemplate}"
|
||||
ItemContainerStyle="{StaticResource NavItemStyle}">
|
||||
<!--<i:Interaction.Triggers>
|
||||
<i:EventTrigger EventName="SelectionChanged">
|
||||
<prism:InvokeCommandAction
|
||||
Command="{Binding NavItemClickCommand}"
|
||||
CommandParameter="{Binding SelectedItem, RelativeSource={RelativeSource AncestorType=ListBox}}"/>
|
||||
</i:EventTrigger>
|
||||
</i:Interaction.Triggers>-->
|
||||
</ListBox>
|
||||
|
||||
<!-- 底部部分:固定操作 -->
|
||||
<ListBox
|
||||
Grid.Row="1"
|
||||
Padding="0"
|
||||
BorderThickness="0"
|
||||
hc:BorderElement.CornerRadius="0"
|
||||
ItemsSource="{Binding BottomNavItems}"
|
||||
SelectedItem="{Binding SelectedBottomNavItem, Mode=TwoWay}"
|
||||
ItemTemplate="{StaticResource NavItemTemplate}"
|
||||
ItemContainerStyle="{StaticResource NavItemStyle}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using YY.Admin.Module;
|
||||
|
||||
namespace YY.Admin.Views.Control
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for Sidebar.xaml
|
||||
/// </summary>
|
||||
public partial class SidebarControl : UserControl
|
||||
{
|
||||
public SidebarControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
private void OnNavItemMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is ListBoxItem item && item.DataContext is NavItem navItem)
|
||||
{
|
||||
// 执行命令(如果有)
|
||||
if (navItem.Command?.CanExecute(navItem) == true)
|
||||
{
|
||||
navItem.Command.Execute(navItem);
|
||||
}
|
||||
|
||||
// 如果不可选,阻止选中
|
||||
if (!navItem.IsActive)
|
||||
{
|
||||
// 阻止 ListBox 自动选择该项
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<UserControl x:Class="YY.Admin.Views.Control.TabContentView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:prism="http://prismlibrary.com/">
|
||||
<Grid>
|
||||
<!-- 这个 ContentControl 将被注册为 Prism 区域 -->
|
||||
<ContentControl x:Name="PART_ContentHost" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
371
yy-admin-master/YY.Admin/Views/Control/TabContentView.xaml.cs
Normal file
371
yy-admin-master/YY.Admin/Views/Control/TabContentView.xaml.cs
Normal file
@@ -0,0 +1,371 @@
|
||||
//using Prism.Ioc;
|
||||
//using System;
|
||||
//using System.Windows;
|
||||
//using System.Windows.Controls;
|
||||
//using System.Windows.Threading;
|
||||
|
||||
//namespace YY.Admin.Views.Control
|
||||
//{
|
||||
// /// <summary>
|
||||
// /// TabContentView.xaml 的交互逻辑
|
||||
// /// </summary>
|
||||
// public partial class TabContentView : UserControl
|
||||
// {
|
||||
// public TabContentView()
|
||||
// {
|
||||
// InitializeComponent();
|
||||
// this.Loaded += TabContentView_Loaded;
|
||||
// this.Unloaded += TabContentView_Unloaded;
|
||||
// }
|
||||
|
||||
// private bool _navigated = false;
|
||||
|
||||
// private void TabContentView_Loaded(object sender, RoutedEventArgs e)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// if (string.IsNullOrWhiteSpace(RegionName))
|
||||
// return;
|
||||
|
||||
// var regionManager = ContainerLocator.Current.Resolve<IRegionManager>();
|
||||
|
||||
// // 把内部 ContentControl 注册为 region(基于 RegionName)
|
||||
// RegionManager.SetRegionName(PART_ContentHost, RegionName);
|
||||
// RegionManager.SetRegionManager(PART_ContentHost, regionManager);
|
||||
|
||||
// // 延迟导航,确保 region 真正注册到 RegionManager 并且模板生成为止
|
||||
// if (!_navigated && !string.IsNullOrEmpty(ViewName))
|
||||
// {
|
||||
// Dispatcher.BeginInvoke(new Action(() =>
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// // 再次检查 region 是否存在并执行导航
|
||||
// if (regionManager.Regions.ContainsRegionWithName(RegionName))
|
||||
// {
|
||||
// regionManager.RequestNavigate(RegionName, ViewName);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // 如果 region 仍然不存在,尝试 RequestNavigate(Prism 通常会创建 region)
|
||||
// regionManager.RequestNavigate(RegionName, ViewName);
|
||||
// }
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// System.Diagnostics.Debug.WriteLine($"TabContentView 导航异常: {ex.Message}");
|
||||
// }
|
||||
// }), DispatcherPriority.Background);
|
||||
|
||||
// _navigated = true;
|
||||
// }
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// System.Diagnostics.Debug.WriteLine($"TabContentView_Loaded 出错: {ex.Message}");
|
||||
// }
|
||||
// }
|
||||
|
||||
// private void TabContentView_Unloaded(object sender, RoutedEventArgs e)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// if (!string.IsNullOrWhiteSpace(RegionName))
|
||||
// {
|
||||
// var regionManager = ContainerLocator.Current.Resolve<IRegionManager>();
|
||||
// if (regionManager.Regions.ContainsRegionWithName(RegionName))
|
||||
// {
|
||||
// var region = regionManager.Regions[RegionName];
|
||||
// region.RemoveAll();
|
||||
// regionManager.Regions.Remove(RegionName);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// System.Diagnostics.Debug.WriteLine($"TabContentView_Unloaded 清理异常: {ex.Message}");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #region RegionName DP
|
||||
// public static readonly DependencyProperty RegionNameProperty =
|
||||
// DependencyProperty.Register(nameof(RegionName), typeof(string), typeof(TabContentView), new PropertyMetadata(null));
|
||||
|
||||
// public string RegionName
|
||||
// {
|
||||
// get => (string)GetValue(RegionNameProperty);
|
||||
// set => SetValue(RegionNameProperty, value);
|
||||
// }
|
||||
// #endregion
|
||||
|
||||
// #region ViewName DP
|
||||
// public static readonly DependencyProperty ViewNameProperty =
|
||||
// DependencyProperty.Register(nameof(ViewName), typeof(string), typeof(TabContentView), new PropertyMetadata(null));
|
||||
|
||||
// public string ViewName
|
||||
// {
|
||||
// get => (string)GetValue(ViewNameProperty);
|
||||
// set => SetValue(ViewNameProperty, value);
|
||||
// }
|
||||
// #endregion
|
||||
// }
|
||||
//}
|
||||
using Prism.Ioc;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace YY.Admin.Views.Control
|
||||
{
|
||||
public partial class TabContentView : UserControl
|
||||
{
|
||||
public TabContentView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
this.Loaded += TabContentView_Loaded;
|
||||
this.Unloaded += TabContentView_Unloaded;
|
||||
this.IsVisibleChanged += TabContentView_IsVisibleChanged;
|
||||
this.DataContextChanged += TabContentView_DataContextChanged;
|
||||
}
|
||||
|
||||
private bool _navigated = false;
|
||||
private IRegionManager _regionManager;
|
||||
private DispatcherOperation _pendingNavigationOp;
|
||||
|
||||
#region DPs
|
||||
public static readonly DependencyProperty RegionNameProperty =
|
||||
DependencyProperty.Register(nameof(RegionName), typeof(string), typeof(TabContentView), new PropertyMetadata(null, OnRegionOrViewNameChanged));
|
||||
|
||||
public string RegionName
|
||||
{
|
||||
get => (string)GetValue(RegionNameProperty);
|
||||
set => SetValue(RegionNameProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ViewNameProperty =
|
||||
DependencyProperty.Register(nameof(ViewName), typeof(string), typeof(TabContentView), new PropertyMetadata(null, OnRegionOrViewNameChanged));
|
||||
|
||||
public string ViewName
|
||||
{
|
||||
get => (string)GetValue(ViewNameProperty);
|
||||
set => SetValue(ViewNameProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void TabContentView_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(RegionName))
|
||||
return;
|
||||
|
||||
_regionManager = ContainerLocator.Current.Resolve<IRegionManager>();
|
||||
|
||||
// 确保 region/manager 设置在后续导航前准备好(但不要重复注册 region 这里)
|
||||
// We will call PrepareRegionForRegistration inside EnsureNavigateIfNeeded before actual registration.
|
||||
EnsureNavigateIfNeeded();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"TabContentView_Loaded 出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void TabContentView_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
=> EnsureNavigateIfNeeded();
|
||||
|
||||
private void TabContentView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
=> EnsureNavigateIfNeeded();
|
||||
|
||||
private void EnsureNavigateIfNeeded()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_navigated)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(RegionName) || string.IsNullOrWhiteSpace(ViewName))
|
||||
return;
|
||||
|
||||
if (!IsLoaded || !IsVisible)
|
||||
return;
|
||||
|
||||
if (_regionManager == null)
|
||||
_regionManager = ContainerLocator.Current.Resolve<IRegionManager>();
|
||||
|
||||
// 在注册 region 之前准备 host(清理旧 region 与清空 Content),避免 Prism 抛 Content 非空异常
|
||||
PrepareRegionForRegistration();
|
||||
|
||||
// 取消之前挂起的导航(保证最后一次胜出)
|
||||
if (_pendingNavigationOp != null && _pendingNavigationOp.Status == DispatcherOperationStatus.Pending)
|
||||
{
|
||||
Debug.WriteLine($"取消挂起导航: region={RegionName}, view={ViewName}");
|
||||
_pendingNavigationOp.Abort();
|
||||
_pendingNavigationOp = null;
|
||||
}
|
||||
|
||||
var desiredView = ViewName;
|
||||
var desiredRegion = RegionName;
|
||||
|
||||
_pendingNavigationOp = Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_regionManager == null)
|
||||
_regionManager = ContainerLocator.Current.Resolve<IRegionManager>();
|
||||
|
||||
// 现在把 PART_ContentHost 注册为一个 region(设置 RegionName/RegionManager)
|
||||
// 注意:如果前面清理正确,这里不会再触发 ContentControl already has content 错误
|
||||
RegionManager.SetRegionName(PART_ContentHost, desiredRegion);
|
||||
RegionManager.SetRegionManager(PART_ContentHost, _regionManager);
|
||||
|
||||
Debug.WriteLine($"开始 RequestNavigate -> region={desiredRegion}, view={desiredView}");
|
||||
|
||||
_regionManager.RequestNavigate(desiredRegion, desiredView, result =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (result.Success == true)
|
||||
{
|
||||
_navigated = true;
|
||||
Debug.WriteLine($"导航成功: region={desiredRegion}, view={desiredView}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_navigated = false;
|
||||
Debug.WriteLine($"导航失败: region={desiredRegion}, view={desiredView}, error={result.Exception?.Message}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_navigated = false;
|
||||
Debug.WriteLine($"导航回调异常: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"调度导航异常: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pendingNavigationOp = null;
|
||||
}
|
||||
}), DispatcherPriority.Background);
|
||||
|
||||
Debug.WriteLine($"调度导航: Region={RegionName}, View={ViewName}, Loaded={IsLoaded}, Visible={IsVisible}, navigated={_navigated}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"EnsureNavigateIfNeeded 异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在注册 region 之前清理宿主控件与 RegionManager 中可能残留的 region。
|
||||
/// 这一步是防止 Prism 在 adapt 时因为 ContentControl.Content 非空而抛异常。
|
||||
/// </summary>
|
||||
private void PrepareRegionForRegistration()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 如果 RegionManager 中已存在同名 region,则先移除它(并 RemoveAll 内容)
|
||||
if (_regionManager != null && _regionManager.Regions.ContainsRegionWithName(RegionName))
|
||||
{
|
||||
try
|
||||
{
|
||||
var existing = _regionManager.Regions[RegionName];
|
||||
existing.RemoveAll();
|
||||
_regionManager.Regions.Remove(RegionName);
|
||||
Debug.WriteLine($"已移除旧 region: {RegionName}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"移除旧 region 发生异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果宿主 ContentControl 已经有内容,清空它(很关键)
|
||||
if (PART_ContentHost != null && PART_ContentHost.Content != null)
|
||||
{
|
||||
Debug.WriteLine($"清空 PART_ContentHost.Content (原内容类型: {PART_ContentHost.Content.GetType().Name})");
|
||||
PART_ContentHost.Content = null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"PrepareRegionForRegistration 异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void TabContentView_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 取消挂起导航
|
||||
if (_pendingNavigationOp != null && _pendingNavigationOp.Status == DispatcherOperationStatus.Pending)
|
||||
{
|
||||
_pendingNavigationOp.Abort();
|
||||
_pendingNavigationOp = null;
|
||||
}
|
||||
|
||||
// 清理 region 与宿主内容
|
||||
if (!string.IsNullOrWhiteSpace(RegionName))
|
||||
{
|
||||
var rm = _regionManager ?? ContainerLocator.Current.Resolve<IRegionManager>();
|
||||
try
|
||||
{
|
||||
if (rm != null && rm.Regions.ContainsRegionWithName(RegionName))
|
||||
{
|
||||
var region = rm.Regions[RegionName];
|
||||
region.RemoveAll();
|
||||
rm.Regions.Remove(RegionName);
|
||||
Debug.WriteLine($"卸载并移除 region: {RegionName}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"TabContentView_Unloaded 清理 region 异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (PART_ContentHost != null && PART_ContentHost.Content != null)
|
||||
{
|
||||
PART_ContentHost.Content = null;
|
||||
Debug.WriteLine($"Unloaded: 清空 PART_ContentHost.Content");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"TabContentView_Unloaded 清理异常: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_navigated = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnRegionOrViewNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is TabContentView tc)
|
||||
{
|
||||
// 新参数到来,允许再次导航
|
||||
tc._navigated = false;
|
||||
|
||||
// 取消旧挂起导航
|
||||
if (tc._pendingNavigationOp != null && tc._pendingNavigationOp.Status == DispatcherOperationStatus.Pending)
|
||||
{
|
||||
tc._pendingNavigationOp.Abort();
|
||||
tc._pendingNavigationOp = null;
|
||||
}
|
||||
|
||||
// 尝试导航(这一调用会在 Loaded/可见时真正执行)
|
||||
tc.EnsureNavigateIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
361
yy-admin-master/YY.Admin/Views/DashboardView.xaml
Normal file
361
yy-admin-master/YY.Admin/Views/DashboardView.xaml
Normal file
@@ -0,0 +1,361 @@
|
||||
<UserControl x:Class="YY.Admin.Views.DashboardView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:ext="clr-namespace:YY.Admin.Core.Extension;assembly=YY.Admin.Core"
|
||||
xmlns:ctls="clr-namespace:YY.Admin.Core.Controls;assembly=YY.Admin.Core"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF"
|
||||
xmlns:cuslvc="clr-namespace:YY.Admin.Core.LiveCharts2;assembly=YY.Admin.Core"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True">
|
||||
|
||||
<hc:ScrollViewer IsInertiaEnabled="True">
|
||||
<StackPanel Style="{StaticResource BaseViewStyle}">
|
||||
<ItemsControl ItemsSource="{Binding StatisticCards}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<ctls:GridPanel MinWidth="180" Gap="20"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border
|
||||
Background="{DynamicResource ThirdlyRegionBrush}"
|
||||
CornerRadius="8"
|
||||
Padding="24"
|
||||
Effect="{StaticResource EffectShadow1}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock
|
||||
Text="{Binding Title}"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
Margin="0,0,0,8"/>
|
||||
<TextBlock
|
||||
Text="{Binding Value}"
|
||||
FontSize="28"
|
||||
FontWeight="Bold"
|
||||
Margin="0,0,0,4"/>
|
||||
<TextBlock
|
||||
Text="{Binding Trend}"
|
||||
FontSize="12"
|
||||
Foreground="#52c41a"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border
|
||||
Grid.Column="1"
|
||||
Background="{Binding Color}"
|
||||
Width="48"
|
||||
Height="48"
|
||||
CornerRadius="24"
|
||||
Opacity="0.1"/>
|
||||
<TextBlock Grid.Column="1"
|
||||
Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="24"
|
||||
Foreground="{Binding Color}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<Grid Margin="0,20,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
<ColumnDefinition Width="20"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 访问趋势图 -->
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
Background="{DynamicResource ThirdlyRegionBrush}"
|
||||
CornerRadius="8"
|
||||
Padding="24"
|
||||
Effect="{StaticResource EffectShadow1}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Text="访问趋势"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Margin="0,0,0,16"/>
|
||||
<Border Grid.Row="1" CornerRadius="4">
|
||||
<lvc:CartesianChart Background="Transparent">
|
||||
<lvc:CartesianChart.XAxes>
|
||||
<lvc:AxesCollection>
|
||||
<lvc:XamlAxis>
|
||||
<lvc:XamlAxis.LabelsPaint>
|
||||
<Binding Path="SkinType" Converter="{StaticResource BrushToSolidColorPaintConverter}">
|
||||
<Binding.ConverterParameter>
|
||||
<cuslvc:CusSolidColorPaint CusColor="PrimaryTextColor"/>
|
||||
</Binding.ConverterParameter>
|
||||
</Binding>
|
||||
</lvc:XamlAxis.LabelsPaint>
|
||||
</lvc:XamlAxis>
|
||||
</lvc:AxesCollection>
|
||||
</lvc:CartesianChart.XAxes>
|
||||
|
||||
<lvc:CartesianChart.YAxes>
|
||||
<lvc:AxesCollection>
|
||||
<lvc:XamlAxis>
|
||||
<lvc:XamlAxis.LabelsPaint>
|
||||
<Binding Path="SkinType" Converter="{StaticResource BrushToSolidColorPaintConverter}">
|
||||
<Binding.ConverterParameter>
|
||||
<cuslvc:CusSolidColorPaint CusColor="PrimaryTextColor"/>
|
||||
</Binding.ConverterParameter>
|
||||
</Binding>
|
||||
</lvc:XamlAxis.LabelsPaint>
|
||||
<lvc:XamlAxis.SeparatorsPaint>
|
||||
<Binding Path="SkinType" Converter="{StaticResource BrushToSolidColorPaintConverter}">
|
||||
<Binding.ConverterParameter>
|
||||
<cuslvc:CusSolidColorPaint CusColor="ThirdlyBorderBrush"/>
|
||||
</Binding.ConverterParameter>
|
||||
</Binding>
|
||||
</lvc:XamlAxis.SeparatorsPaint>
|
||||
</lvc:XamlAxis>
|
||||
</lvc:AxesCollection>
|
||||
</lvc:CartesianChart.YAxes>
|
||||
|
||||
<lvc:CartesianChart.Series>
|
||||
<lvc:SeriesCollection>
|
||||
<lvc:XamlColumnSeries
|
||||
Values="{Binding Values1}"
|
||||
Padding="2"
|
||||
PointMeasuredCommand="{Binding PointMeasuredCommand}"
|
||||
/>
|
||||
<lvc:XamlColumnSeries
|
||||
Values="{Binding Values2}"
|
||||
Padding="0"
|
||||
PointMeasuredCommand="{Binding PointMeasuredCommand}"/>
|
||||
</lvc:SeriesCollection>
|
||||
</lvc:CartesianChart.Series>
|
||||
</lvc:CartesianChart>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<Border
|
||||
Grid.Column="2"
|
||||
Background="{DynamicResource ThirdlyRegionBrush}"
|
||||
CornerRadius="8"
|
||||
Padding="24"
|
||||
Effect="{StaticResource EffectShadow1}">
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Text="最近活动"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Margin="0,0,0,16"/>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding RecentActivities}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border BorderBrush="{DynamicResource ThirdlyBorderBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="0,12">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
Background="#1890ff"
|
||||
Width="32"
|
||||
Height="32"
|
||||
CornerRadius="16"
|
||||
Margin="0,0,12,0">
|
||||
<TextBlock
|
||||
Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="12"
|
||||
Foreground="White"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1">
|
||||
<TextBlock
|
||||
Text="{Binding Title}"
|
||||
FontWeight="Bold"
|
||||
FontSize="14"
|
||||
Margin="0,0,0,4"/>
|
||||
<TextBlock
|
||||
Text="{Binding Description}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,0,0,4"/>
|
||||
<TextBlock
|
||||
Text="{Binding Time, StringFormat='{}{0:MM-dd HH:mm}'}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource ThirdlyTextBrush}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<Border
|
||||
Background="{DynamicResource ThirdlyRegionBrush}"
|
||||
CornerRadius="8"
|
||||
Padding="24"
|
||||
Margin="0,20,0,0"
|
||||
Effect="{StaticResource EffectShadow1}">
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Text="快捷操作"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Margin="0,0,0,16"/>
|
||||
|
||||
<UniformGrid Rows="1" Columns="4">
|
||||
<Button
|
||||
Style="{StaticResource ButtonCustom}"
|
||||
Padding="0,8">
|
||||
<StackPanel>
|
||||
<Grid>
|
||||
<Border
|
||||
Background="#52c41a"
|
||||
Width="48"
|
||||
Height="48"
|
||||
CornerRadius="24"
|
||||
Opacity="0.1"/>
|
||||
<TextBlock
|
||||
Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="24"
|
||||
Foreground="#52c41a"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock
|
||||
Text="添加用户"
|
||||
FontSize="14"
|
||||
Margin="0,5,0,0"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Style="{StaticResource ButtonCustom}"
|
||||
Padding="0,8">
|
||||
<StackPanel>
|
||||
<Grid>
|
||||
<Border
|
||||
Background="#faad14"
|
||||
Width="48"
|
||||
Height="48"
|
||||
CornerRadius="24"
|
||||
Opacity="0.1"/>
|
||||
<TextBlock
|
||||
Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="24"
|
||||
Foreground="#faad14"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<TextBlock
|
||||
Text="系统设置"
|
||||
FontSize="14"
|
||||
Margin="0,5,0,0"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Style="{StaticResource ButtonCustom}"
|
||||
Padding="0,8">
|
||||
<StackPanel>
|
||||
<Grid>
|
||||
<Border
|
||||
Background="#1890ff"
|
||||
Width="48"
|
||||
Height="48"
|
||||
CornerRadius="24"
|
||||
Opacity="0.1"/>
|
||||
<TextBlock
|
||||
Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="24"
|
||||
Foreground="#1890ff"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<TextBlock
|
||||
Text="数据备份"
|
||||
FontSize="14"
|
||||
Margin="0,5,0,0"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Style="{StaticResource ButtonCustom}"
|
||||
Padding="0,8">
|
||||
<StackPanel>
|
||||
<Grid>
|
||||
<Border
|
||||
Background="#1890ff"
|
||||
Width="48"
|
||||
Height="48"
|
||||
CornerRadius="24"
|
||||
Opacity="0.1"/>
|
||||
<TextBlock
|
||||
Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="24"
|
||||
Foreground="#1890ff"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<TextBlock
|
||||
Text="查看日志"
|
||||
FontSize="14"
|
||||
Margin="0,5,0,0"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 加载指示器 -->
|
||||
<!--<Border Visibility="{Binding IsLoading, Converter={StaticResource Boolean2VisibilityConverter}}"
|
||||
Background="White"
|
||||
CornerRadius="8"
|
||||
Padding="24"
|
||||
Margin="0,16,0,0">
|
||||
<StackPanel HorizontalAlignment="Center">
|
||||
<hc:LoadingCircle Width="50" Height="50"/>
|
||||
<TextBlock Text="数据加载中..."
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,10,0,0"
|
||||
Foreground="#666"/>
|
||||
</StackPanel>
|
||||
</Border>-->
|
||||
</StackPanel>
|
||||
</hc:ScrollViewer>
|
||||
</UserControl>
|
||||
17
yy-admin-master/YY.Admin/Views/DashboardView.xaml.cs
Normal file
17
yy-admin-master/YY.Admin/Views/DashboardView.xaml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace YY.Admin.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// DashboardView.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class DashboardView : UserControl
|
||||
{
|
||||
public DashboardView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
101
yy-admin-master/YY.Admin/Views/Dialogs/AlertDialogView.xaml
Normal file
101
yy-admin-master/YY.Admin/Views/Dialogs/AlertDialogView.xaml
Normal file
@@ -0,0 +1,101 @@
|
||||
<UserControl x:Class="YY.Admin.Views.Dialogs.AlertDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:ctls="clr-namespace:YY.Admin.Core.Controls;assembly=YY.Admin.Core"
|
||||
Width="400" Height="260">
|
||||
|
||||
<UserControl.Resources>
|
||||
<Style x:Key="DialogButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="#1890ff"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="Width" Value="80"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border Background="White"
|
||||
CornerRadius="8"
|
||||
BorderBrush="#f0f0f0"
|
||||
BorderThickness="1"
|
||||
>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<Border Grid.Row="0"
|
||||
Background="#1890ff"
|
||||
CornerRadius="8,8,0,0"
|
||||
Height="50">
|
||||
<Grid Margin="16,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 图标和标题 -->
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<ctls:FontAwesomeIcon
|
||||
Icon=""
|
||||
Foreground="White"
|
||||
FontSize="16"
|
||||
Margin="0,0,8,0"/>
|
||||
<TextBlock Text="{Binding Title}"
|
||||
Foreground="White"
|
||||
FontWeight="Bold"
|
||||
FontSize="14"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<Button Grid.Column="2"
|
||||
Command="{Binding CloseCommand}"
|
||||
Style="{StaticResource ButtonIcon}"
|
||||
Foreground="White"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Width="24" Height="24"
|
||||
hc:IconElement.Geometry="{StaticResource ErrorGeometry}"
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<Border Grid.Row="1" Padding="20">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<TextBlock Text="{Binding Message}"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="14"
|
||||
Foreground="#666666"
|
||||
LineHeight="20"/>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
<Border Grid.Row="2"
|
||||
Background="#fafafa"
|
||||
CornerRadius="0,0,8,8"
|
||||
Height="60">
|
||||
<Button Content="确定"
|
||||
Command="{Binding CloseCommand}"
|
||||
Style="{StaticResource DialogButton}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace YY.Admin.Views.Dialogs
|
||||
{
|
||||
/// <summary>
|
||||
/// AlertDialogView.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class AlertDialogView : UserControl
|
||||
{
|
||||
public AlertDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
116
yy-admin-master/YY.Admin/Views/Dialogs/ConfirmDialogView.xaml
Normal file
116
yy-admin-master/YY.Admin/Views/Dialogs/ConfirmDialogView.xaml
Normal file
@@ -0,0 +1,116 @@
|
||||
<UserControl x:Class="YY.Admin.Views.Dialogs.ConfirmDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:YY.Admin.Views.Dialogs"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
mc:Ignorable="d"
|
||||
Width="420" Height="280">
|
||||
|
||||
<UserControl.Resources>
|
||||
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="#1890ff"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="Width" Value="80"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#40a9ff"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#096dd9"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SecondaryButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="#666666"/>
|
||||
<Setter Property="BorderBrush" Value="#d9d9d9"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="Width" Value="80"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#f5f5f5"/>
|
||||
<Setter Property="BorderBrush" Value="#40a9ff"/>
|
||||
<Setter Property="Foreground" Value="#40a9ff"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#e6f7ff"/>
|
||||
<Setter Property="BorderBrush" Value="#096dd9"/>
|
||||
<Setter Property="Foreground" Value="#096dd9"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border Background="White"
|
||||
BorderBrush="#f0f0f0"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<Grid Margin="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<Border Grid.Row="0"
|
||||
Background="#fafafa"
|
||||
Height="40"
|
||||
CornerRadius="4,4,0,0"
|
||||
BorderThickness="0,0,0,1"
|
||||
BorderBrush="#f0f0f0">
|
||||
<StackPanel Orientation="Horizontal" Margin="16,0">
|
||||
<TextBlock Text="提示"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#333333"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Padding="16">
|
||||
<TextBlock Text="{Binding Message}"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="14"
|
||||
Foreground="#666666"
|
||||
LineHeight="20"/>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
<Border Grid.Row="2"
|
||||
Background="#fafafa"
|
||||
Height="60"
|
||||
CornerRadius="0,0,4,4"
|
||||
BorderThickness="0,1,0,0"
|
||||
BorderBrush="#f0f0f0">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Button Content="确定"
|
||||
Command="{Binding YesCommand}"
|
||||
Style="{StaticResource PrimaryButtonStyle}"
|
||||
Margin="0,0,12,0"/>
|
||||
<Button Content="取消"
|
||||
Command="{Binding NoCommand}"
|
||||
Style="{StaticResource SecondaryButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace YY.Admin.Views.Dialogs
|
||||
{
|
||||
/// <summary>
|
||||
/// ConfirmDialogView.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class ConfirmDialogView : UserControl
|
||||
{
|
||||
public ConfirmDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
78
yy-admin-master/YY.Admin/Views/Dialogs/ErrorDialogView.xaml
Normal file
78
yy-admin-master/YY.Admin/Views/Dialogs/ErrorDialogView.xaml
Normal file
@@ -0,0 +1,78 @@
|
||||
<UserControl x:Class="YY.Admin.Views.Dialogs.ErrorDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:ctls="clr-namespace:YY.Admin.Core.Controls;assembly=YY.Admin.Core"
|
||||
Width="420" Height="280">
|
||||
<Border Background="White"
|
||||
CornerRadius="8"
|
||||
BorderBrush="#f0f0f0"
|
||||
BorderThickness="1">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<Border Grid.Row="0"
|
||||
Background="#ff4d4f"
|
||||
CornerRadius="8,8,0,0"
|
||||
Height="50">
|
||||
<Grid Margin="16,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<ctls:FontAwesomeIcon
|
||||
Icon=""
|
||||
Foreground="White"
|
||||
FontSize="16"
|
||||
Margin="0,0,8,0"/>
|
||||
<TextBlock Text="错误提示"
|
||||
Foreground="White"
|
||||
FontWeight="Bold"
|
||||
FontSize="14"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="2"
|
||||
Command="{Binding CloseCommand}"
|
||||
Style="{StaticResource ButtonIcon}"
|
||||
Foreground="White"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Width="24" Height="24"
|
||||
hc:IconElement.Geometry="{StaticResource ErrorGeometry}"
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<Border Grid.Row="1" Padding="20">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- 错误消息 -->
|
||||
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto">
|
||||
<TextBlock Text="{Binding Message}"
|
||||
TextWrapping="Wrap"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Foreground="#666666"
|
||||
LineHeight="20"/>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace YY.Admin.Views.Dialogs
|
||||
{
|
||||
/// <summary>
|
||||
/// ErrorDialogView.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class ErrorDialogView : UserControl
|
||||
{
|
||||
public ErrorDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<UserControl x:Class="YY.Admin.Views.Dialogs.ServerSettingsDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ctls="clr-namespace:YY.Admin.Core.Controls;assembly=YY.Admin.Core"
|
||||
Width="460" Height="360">
|
||||
<Border Background="White" BorderBrush="#f0f0f0" BorderThickness="1" CornerRadius="4">
|
||||
<Grid Margin="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" Background="#fafafa" Height="42" CornerRadius="4,4,0,0" BorderThickness="0,0,0,1" BorderBrush="#f0f0f0">
|
||||
<StackPanel Orientation="Horizontal" Margin="16,0">
|
||||
<TextBlock Text="服务器设置" VerticalAlignment="Center" FontWeight="SemiBold" Foreground="#333333"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Row="1" Margin="20,16,20,0">
|
||||
<TextBlock Text="IP" Margin="0,0,0,6"/>
|
||||
<TextBox Text="{Binding Ip, UpdateSourceTrigger=PropertyChanged}" Height="32"/>
|
||||
|
||||
<TextBlock Text="端口" Margin="0,12,0,6"/>
|
||||
<TextBox Text="{Binding Port, UpdateSourceTrigger=PropertyChanged}" Height="32"/>
|
||||
|
||||
<TextBlock Text="WebSocket 地址(可选)" Margin="0,12,0,6"/>
|
||||
<TextBox Text="{Binding WebSocketUrl, UpdateSourceTrigger=PropertyChanged}" Height="32"/>
|
||||
|
||||
<TextBlock Text="后端上下文路径" Margin="0,12,0,6"/>
|
||||
<TextBox Text="{Binding BasePath, UpdateSourceTrigger=PropertyChanged}" Height="32"/>
|
||||
|
||||
<TextBlock Text="{Binding ErrorMessage}" Foreground="Red" Margin="0,12,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Row="2" Background="#fafafa" Height="62" CornerRadius="0,0,4,4" BorderThickness="0,1,0,0" BorderBrush="#f0f0f0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Button Command="{Binding SaveCommand}" Width="90" Height="34" Margin="0,0,12,0" Background="#1890ff" Foreground="White" BorderThickness="0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<ctls:FontAwesomeIcon Icon="" IconFamily="Solid" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="保存"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Command="{Binding CancelCommand}" Width="90" Height="34" Background="Transparent" BorderBrush="#d9d9d9">
|
||||
<TextBlock Text="取消"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace YY.Admin.Views.Dialogs
|
||||
{
|
||||
/// <summary>
|
||||
/// ServerSettingsDialogView.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class ServerSettingsDialogView : UserControl
|
||||
{
|
||||
public ServerSettingsDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
110
yy-admin-master/YY.Admin/Views/Dialogs/SuccessDialogView.xaml
Normal file
110
yy-admin-master/YY.Admin/Views/Dialogs/SuccessDialogView.xaml
Normal file
@@ -0,0 +1,110 @@
|
||||
<UserControl x:Class="YY.Admin.Views.Dialogs.SuccessDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:ctls="clr-namespace:YY.Admin.Core.Controls;assembly=YY.Admin.Core"
|
||||
Width="420" Height="280">
|
||||
|
||||
<UserControl.Resources>
|
||||
<Style x:Key="SuccessButton" TargetType="Button" BasedOn="{StaticResource DialogButton}">
|
||||
<Setter Property="Background" Value="#52c41a"/>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border Background="White"
|
||||
CornerRadius="8"
|
||||
BorderBrush="#f0f0f0"
|
||||
BorderThickness="1">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<Border Grid.Row="0"
|
||||
Background="#52c41a"
|
||||
CornerRadius="8,8,0,0"
|
||||
Height="50">
|
||||
<Grid Margin="16,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<ctls:FontAwesomeIcon
|
||||
Icon=""
|
||||
Foreground="White"
|
||||
FontSize="16"
|
||||
Margin="0,0,8,0"/>
|
||||
<TextBlock Text="操作成功"
|
||||
Foreground="White"
|
||||
FontWeight="Bold"
|
||||
FontSize="14"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="2"
|
||||
Command="{Binding CloseCommand}"
|
||||
Style="{StaticResource ButtonIcon}"
|
||||
Foreground="White"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Width="24" Height="24"
|
||||
hc:IconElement.Geometry="{StaticResource ErrorGeometry}"
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<Border Grid.Row="1" Padding="20">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 成功图标 -->
|
||||
<Border Grid.Column="0"
|
||||
Background="#f6ffed"
|
||||
Width="48" Height="48"
|
||||
CornerRadius="24"
|
||||
Margin="0,0,16,0">
|
||||
<ctls:FontAwesomeIcon
|
||||
Icon=""
|
||||
Foreground="#52c41a"
|
||||
FontSize="20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<!-- 成功消息 -->
|
||||
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto">
|
||||
<TextBlock Text="{Binding Message}"
|
||||
TextWrapping="Wrap"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Foreground="#666666"
|
||||
LineHeight="20"/>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
<Border Grid.Row="2"
|
||||
Background="#fafafa"
|
||||
CornerRadius="0,0,8,8"
|
||||
Height="60">
|
||||
<Button Content="确定"
|
||||
Command="{Binding CloseCommand}"
|
||||
Style="{StaticResource SuccessButton}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace YY.Admin.Views.Dialogs
|
||||
{
|
||||
/// <summary>
|
||||
/// SuccessDialogView.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class SuccessDialogView : UserControl
|
||||
{
|
||||
public SuccessDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
110
yy-admin-master/YY.Admin/Views/Dialogs/WarningDialogView.xaml
Normal file
110
yy-admin-master/YY.Admin/Views/Dialogs/WarningDialogView.xaml
Normal file
@@ -0,0 +1,110 @@
|
||||
<UserControl x:Class="YY.Admin.Views.Dialogs.WarningDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:ctls="clr-namespace:YY.Admin.Core.Controls;assembly=YY.Admin.Core"
|
||||
Width="420" Height="280">
|
||||
|
||||
<UserControl.Resources>
|
||||
<Style x:Key="WarningButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="#faad14"/>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border Background="White"
|
||||
CornerRadius="8"
|
||||
BorderBrush="#f0f0f0"
|
||||
BorderThickness="1">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<Border Grid.Row="0"
|
||||
Background="#faad14"
|
||||
CornerRadius="8,8,0,0"
|
||||
Height="50">
|
||||
<Grid Margin="16,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<ctls:FontAwesomeIcon
|
||||
Icon=""
|
||||
Foreground="White"
|
||||
FontSize="16"
|
||||
Margin="0,0,8,0"/>
|
||||
<TextBlock Text="警告提示"
|
||||
Foreground="White"
|
||||
FontWeight="Bold"
|
||||
FontSize="14"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="2"
|
||||
Command="{Binding CloseCommand}"
|
||||
Style="{StaticResource ButtonIcon}"
|
||||
Foreground="White"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Width="24" Height="24"
|
||||
hc:IconElement.Geometry="{StaticResource ErrorGeometry}"
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<Border Grid.Row="1" Padding="20">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 警告图标 -->
|
||||
<Border Grid.Column="0"
|
||||
Background="#fffbe6"
|
||||
Width="48" Height="48"
|
||||
CornerRadius="24"
|
||||
Margin="0,0,16,0">
|
||||
<ctls:FontAwesomeIcon
|
||||
Icon=""
|
||||
Foreground="#faad14"
|
||||
FontSize="20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<!-- 警告消息 -->
|
||||
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto">
|
||||
<TextBlock Text="{Binding Message}"
|
||||
TextWrapping="Wrap"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Foreground="#666666"
|
||||
LineHeight="20"/>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
<Border Grid.Row="2"
|
||||
Background="#fafafa"
|
||||
CornerRadius="0,0,8,8"
|
||||
Height="60">
|
||||
<Button Content="确定"
|
||||
Command="{Binding CloseCommand}"
|
||||
Style="{StaticResource WarningButton}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace YY.Admin.Views.Dialogs
|
||||
{
|
||||
/// <summary>
|
||||
/// WarningDialogView.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class WarningDialogView : UserControl
|
||||
{
|
||||
public WarningDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
168
yy-admin-master/YY.Admin/Views/LoginWindow.xaml
Normal file
168
yy-admin-master/YY.Admin/Views/LoginWindow.xaml
Normal file
@@ -0,0 +1,168 @@
|
||||
<hc:Window x:Class="YY.Admin.Views.LoginWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:consts="clr-namespace:YY.Admin.Core.Const;assembly=YY.Admin.Core"
|
||||
xmlns:ctls="clr-namespace:YY.Admin.Core.Controls;assembly=YY.Admin.Core"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
Icon="/Resources/Icon/logo.ico"
|
||||
Title="{Binding Title}"
|
||||
Width="650"
|
||||
Height="500"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="CanMinimize"
|
||||
ShowInTaskbar="True"
|
||||
KeyDown="Window_KeyDown"
|
||||
FontSize="{StaticResource FontSize }">
|
||||
|
||||
<Window.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<!-- 使用独立的HandyControl主题资源,不受主题切换影响 -->
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Window.Resources>
|
||||
<Grid Background="{DynamicResource RegionBrush}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="260"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Image
|
||||
Source="/Resources/Icon/login.png"
|
||||
Stretch="Uniform"/>
|
||||
|
||||
<Border Grid.Column="1">
|
||||
<Grid>
|
||||
<Button
|
||||
Command="{Binding OpenServerSettingsCommand}"
|
||||
Width="34"
|
||||
Height="34"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,8,8,0"
|
||||
ToolTip="服务器设置">
|
||||
<ctls:FontAwesomeIcon Icon="" IconFamily="Solid"/>
|
||||
</Button>
|
||||
<StackPanel Margin="20 0 20 20">
|
||||
<!-- Logo和标题 -->
|
||||
<StackPanel HorizontalAlignment="Center" Margin="0,20,0,40">
|
||||
<TextBlock
|
||||
Text="{x:Static consts:CommonConst.SystemName}"
|
||||
FontSize="24"
|
||||
FontWeight="Bold"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,15,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<StackPanel x:Name="LoginForm">
|
||||
<!-- 用户名 -->
|
||||
<Grid VerticalAlignment="Center">
|
||||
<hc:TextBox
|
||||
Text="{Binding LoginInput.Username, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Placeholder="请输入用户名"
|
||||
hc:InfoElement.Title="用户名"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
hc:InfoElement.ContentHeight="32"
|
||||
Margin="0,0,0,20"
|
||||
Padding="30 0 8 0 "/>
|
||||
|
||||
<ctls:FontAwesomeIcon
|
||||
Icon=""
|
||||
IconFamily="Solid"
|
||||
Foreground="#c0c4cc"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="8,4,0,0"/>
|
||||
</Grid>
|
||||
|
||||
<Grid VerticalAlignment="Center">
|
||||
<hc:PasswordBox
|
||||
UnsafePassword="{Binding LoginInput.Password, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Placeholder="请输入密码"
|
||||
hc:InfoElement.Title="密码"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
hc:InfoElement.ContentHeight="32"
|
||||
IsSafeEnabled="False"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,0,20"
|
||||
Padding="30,0,8,0"/>
|
||||
|
||||
<ctls:FontAwesomeIcon
|
||||
Icon=""
|
||||
IconFamily="Solid"
|
||||
Foreground="#c0c4cc"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="8,4,0,0"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 记住我 -->
|
||||
<CheckBox
|
||||
Content="记住我"
|
||||
IsChecked="{Binding LoginInput.RememberMe}"
|
||||
Margin="0,0,0,20"
|
||||
HorizontalAlignment="Left"
|
||||
FontSize="{StaticResource FontSize}"/>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<TextBlock
|
||||
Text="{Binding LoginMessage}"
|
||||
Foreground="Red"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,0,0,10"
|
||||
Visibility="{Binding LoginMessage, Converter={StaticResource String2VisibilityConverter}}"/>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<Button
|
||||
x:Name="LoginButton"
|
||||
Click="Submit_Click"
|
||||
Style="{StaticResource ButtonPrimary}"
|
||||
Height="32"
|
||||
FontSize="{StaticResource FontSize}"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsEnabled="{Binding CanInteractWithLogin}"
|
||||
Margin="0,10,0,8">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<ctls:FontAwesomeIcon Icon="" Spin="True" Margin="0 0 10 0" Visibility="{Binding IsLoading, Converter={StaticResource Boolean2VisibilityConverter}}"/>
|
||||
<TextBlock Text="{Binding LoginButtonText}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- 一键同步 Jeecg 用户到本地库(SCADA 免登录接口) -->
|
||||
<Button
|
||||
Command="{Binding SyncJeecgUsersCommand}"
|
||||
Style="{StaticResource ButtonDefault}"
|
||||
Height="32"
|
||||
FontSize="{StaticResource FontSize}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="0,0,0,20"
|
||||
ToolTip="无需登录,从 Jeecg SCADA 接口拉取用户写入 SQLite(需在配置中启用 Jeecg 且 UserListPath 为 scada/queryUser)">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<ctls:FontAwesomeIcon Icon="" Margin="0 0 8 0" Spin="{Binding IsSyncingJeecgUsers}"/>
|
||||
<TextBlock Text="{Binding SyncJeecgUsersButtonText}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- 后端连接状态指示器 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,0,0,8">
|
||||
<Ellipse Width="10" Height="10" Fill="{Binding BackendConnectionStatusBrush}" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="{Binding BackendConnectionStatusText}" Foreground="#666666" FontSize="12" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 其他链接 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<TextBlock Text="忘记密码?" Foreground="#1890ff" Cursor="Hand" Margin="0,0,20,0"/>
|
||||
<TextBlock Text="注册账号" Foreground="#1890ff" Cursor="Hand"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</hc:Window>
|
||||
124
yy-admin-master/YY.Admin/Views/LoginWindow.xaml.cs
Normal file
124
yy-admin-master/YY.Admin/Views/LoginWindow.xaml.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using Npgsql.Replication.PgOutput.Messages;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using YY.Admin.Core.FluentValidation;
|
||||
using YY.Admin.Services.Service.AutoUpdate;
|
||||
using YY.Admin.ViewModels;
|
||||
using Window = HandyControl.Controls.Window;
|
||||
namespace YY.Admin.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// LoginWindow.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class LoginWindow : Window
|
||||
{
|
||||
private readonly IAutoUpdateService _autoUpdateService;
|
||||
public LoginWindow(IAutoUpdateService autoUpdateService)
|
||||
{
|
||||
InitializeComponent();
|
||||
_autoUpdateService = autoUpdateService;
|
||||
// 窗口加载完成后检查更新
|
||||
Loaded += async (s, e) => await CheckForUpdatesAsync();
|
||||
// 当 DataContext 设置后附加验证
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.NewValue != null)
|
||||
{
|
||||
// 附加验证到 UI 控件
|
||||
FluentValidationHelper.Attach(LoginForm, typeof(LoginWindowViewModel));
|
||||
}
|
||||
}
|
||||
private async Task CheckForUpdatesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 延迟一点时间,确保UI已经加载完成
|
||||
await Task.Delay(1000);
|
||||
|
||||
var hasUpdate = await _autoUpdateService.CheckForUpdatesAsync();
|
||||
if (hasUpdate)
|
||||
{
|
||||
await ShowUpdateDialogAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 更新检查失败不影响登录
|
||||
System.Diagnostics.Debug.WriteLine($"更新检查异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
private async Task ShowUpdateDialogAsync()
|
||||
{
|
||||
await Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
// 检查窗口是否仍然打开
|
||||
if (!IsLoaded || !IsVisible || Visibility == Visibility.Collapsed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var versionInfo =await _autoUpdateService.GetVersionInfo();
|
||||
if (versionInfo == null) return;
|
||||
|
||||
var currentVersion = _autoUpdateService.GetCurrentVersion();
|
||||
|
||||
var updateWindow = new UpdateWindow
|
||||
{
|
||||
CurrentVersion = currentVersion,
|
||||
LatestVersion = versionInfo.LatestVersion,
|
||||
PublishDate = versionInfo.PublishDate,
|
||||
Changelog = versionInfo.Changelog,
|
||||
DownloadUrl = versionInfo.DownloadUrl,
|
||||
ApplicationName = versionInfo.ApplicationName,
|
||||
IsMandatory = versionInfo.Mandatory,
|
||||
Owner = this
|
||||
};
|
||||
|
||||
updateWindow.UpdateRequested += (downloadUrl) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_autoUpdateService.StartUpdate(downloadUrl);
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"更新启动失败: {ex.Message}", "更新错误",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
};
|
||||
|
||||
updateWindow.ShowDialog();
|
||||
});
|
||||
}
|
||||
private void LoginWindow_Closed(object? sender, EventArgs e)
|
||||
{
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
|
||||
private void Window_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
Submit_Click(sender, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void Submit_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var vm = (LoginWindowViewModel)DataContext;
|
||||
vm.LoginMessage = string.Empty;
|
||||
bool valid = FluentValidationHelper.ValidateAll(LoginForm, vm.LoginInput!, vm.LoginInputValidator);
|
||||
if (valid)
|
||||
{
|
||||
if (vm.LoginCommand.CanExecute())
|
||||
{
|
||||
vm.LoginCommand.Execute();
|
||||
}
|
||||
}
|
||||
// 阻止事件继续冒泡
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
597
yy-admin-master/YY.Admin/Views/MainWindow.xaml
Normal file
597
yy-admin-master/YY.Admin/Views/MainWindow.xaml
Normal file
@@ -0,0 +1,597 @@
|
||||
<hc:Window x:Class="YY.Admin.Views.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
|
||||
xmlns:views="clr-namespace:YY.Admin.Views.Control"
|
||||
xmlns:helper="clr-namespace:YY.Admin.Core.Helper;assembly=YY.Admin.Core"
|
||||
xmlns:beh="clr-namespace:YY.Admin.Core.Behavior;assembly=YY.Admin.Core"
|
||||
xmlns:consts="clr-namespace:YY.Admin.Core.Const;assembly=YY.Admin.Core"
|
||||
xmlns:core="clr-namespace:YY.Admin.Core;assembly=YY.Admin.Core"
|
||||
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:ctls="clr-namespace:YY.Admin.Core.Controls;assembly=YY.Admin.Core"
|
||||
xmlns:converter="clr-namespace:YY.Admin.Core.Converter;assembly=YY.Admin.Core"
|
||||
Icon="/Resources/Icon/logo.ico"
|
||||
Title="{x:Static consts:CommonConst.SystemName}"
|
||||
Width="1200"
|
||||
Height="800"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
WindowState="Maximized"
|
||||
FontSize="{StaticResource FontSize}">
|
||||
|
||||
<Grid>
|
||||
<DockPanel>
|
||||
<!-- 顶部导航栏 -->
|
||||
<Border
|
||||
DockPanel.Dock="Top"
|
||||
Height="60"
|
||||
BorderThickness="0 0 0 1"
|
||||
BorderBrush="{DynamicResource BorderBrush}"
|
||||
Background="{DynamicResource RegionBrush}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧标题 -->
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center"
|
||||
Margin="20,0,0,0">
|
||||
<Image
|
||||
Source="/Resources/Icon/logo.png"
|
||||
Width="60"
|
||||
Height="60"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock
|
||||
Text="{x:Static consts:CommonConst.SystemName}"
|
||||
FontSize="24"
|
||||
FontWeight="Bold"
|
||||
VerticalAlignment="Center"
|
||||
Margin="5,0,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 右侧用户信息 -->
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center"
|
||||
Margin="20,0">
|
||||
|
||||
<!-- 服务器设置 -->
|
||||
<Button BorderThickness="0" Height="60" Padding="12,0" ToolTip="服务器设置" Command="{Binding OpenServerSettingsCommand}">
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
|
||||
<ctls:FontAwesomeIcon FontSize="16" Icon="" Margin="0,0,0,2"/>
|
||||
<TextBlock Text="服务器设置" FontSize="11" HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- 消息通知 -->
|
||||
<hc:Badge
|
||||
Value="100"
|
||||
BadgeMargin="0,4,-10,0"
|
||||
FontSize="12"
|
||||
Style="{StaticResource BadgeDanger}"
|
||||
Margin="10,0">
|
||||
<Button BorderThickness="0" Height="60" Padding="20,0" ToolTip="消息通知">
|
||||
<ctls:FontAwesomeIcon FontSize="20" Icon=""/>
|
||||
</Button>
|
||||
</hc:Badge>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center"
|
||||
Margin="10,0">
|
||||
<hc:Gravatar Width="40" Height="40" Style="{StaticResource GravatarCircleImg}" Source="/Resources/Icon/avatar.png"/>
|
||||
<StackPanel Margin="8,0,0,0" VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
Text="{Binding CurrentUser.RealName, FallbackValue=管理员}"
|
||||
FontWeight="Bold"
|
||||
FontSize="14"
|
||||
Margin="0,0,0,3"/>
|
||||
<TextBlock
|
||||
Text="{Binding CurrentUser.AccountType, FallbackValue=Administrator}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 底部版权栏 -->
|
||||
<Border
|
||||
DockPanel.Dock="Bottom"
|
||||
Height="30"
|
||||
Background="{DynamicResource RegionBrush}"
|
||||
BorderThickness="0,1,0,0"
|
||||
BorderBrush="{DynamicResource BorderBrush}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧系统名称 -->
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="10,0">
|
||||
<Ellipse Width="8"
|
||||
Height="8"
|
||||
Fill="{Binding BackendConnectionStatusBrush}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,6,0"/>
|
||||
<TextBlock Text="星数连科技科技有限公司"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 右侧版权信息 -->
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Text="Copyright © 2026 XSL All rights reserved."
|
||||
VerticalAlignment="Center"
|
||||
Margin="10,0"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Sidebar侧边栏 -->
|
||||
<views:SidebarControl x:Name="LeftSidebar" DockPanel.Dock="Left" Width="80"/>
|
||||
|
||||
<Grid x:Name="MainContentGrid">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition
|
||||
x:Name="LeftMenuTreeCol"
|
||||
Width="250"
|
||||
MinWidth="50"
|
||||
MaxWidth="{Binding ActualWidth, ElementName=MainContentGrid, Converter={StaticResource MaxWidthConverter}, ConverterParameter=50}"/>
|
||||
<ColumnDefinition Width="4"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- Sidebar侧边栏区域 -->
|
||||
<Border
|
||||
x:Name="LeftMenuTree"
|
||||
BorderThickness="0,0,1,0"
|
||||
BorderBrush="{DynamicResource BorderBrush}"
|
||||
Background="{DynamicResource RegionBrush}">
|
||||
<ContentControl prism:RegionManager.RegionName="{x:Static consts:CommonConst.MenuRegion}"/>
|
||||
</Border>
|
||||
<!-- 拖拽分隔条 -->
|
||||
<GridSplitter
|
||||
Grid.Column="1"
|
||||
x:Name="GridSplitter"
|
||||
Width="4"
|
||||
ResizeBehavior="PreviousAndNext"
|
||||
ShowsPreview="True"
|
||||
Cursor="SizeWE"
|
||||
Background="{DynamicResource RegionBrush}"
|
||||
PreviewStyle="{StaticResource GridSplitterPreviewStyle}"/>
|
||||
<!-- 主内容区域 -->
|
||||
<Border
|
||||
Grid.Column="2"
|
||||
Background="{DynamicResource RegionBrush}"
|
||||
BorderBrush="{DynamicResource BorderBrush}"
|
||||
BorderThickness="1,0,0,0">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<hc:TabControl
|
||||
ItemsSource="{Binding OpenTabs}"
|
||||
SelectedItem="{Binding SelectedTab, Mode=TwoWay}"
|
||||
OverflowMenuDisplayMemberPath="Header"
|
||||
IsAnimationEnabled="True"
|
||||
IsDraggable="True"
|
||||
ShowCloseButton="True"
|
||||
TabItemWidth="120"
|
||||
FontSize="12"
|
||||
BorderThickness="0"
|
||||
Background="{DynamicResource RegionBrush}"
|
||||
Visibility="{Binding AppSettingsViewModel.IsTabControlVisible, Converter={StaticResource Boolean2VisibilityConverter}}"
|
||||
PreviewMouseRightButtonDown="TabControl_PreviewMouseRightButtonDown">
|
||||
|
||||
<!-- 定义资源 -->
|
||||
<hc:TabControl.Resources>
|
||||
<!-- AntDesign 模板 -->
|
||||
<DataTemplate x:Key="AntDesignIconTemplate">
|
||||
<TextBlock
|
||||
FontFamily="{StaticResource AntDesignIcon}"
|
||||
FontSize="14"
|
||||
Text="{Binding Icon}"
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"/>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- MaterialDesign 模板 -->
|
||||
<DataTemplate x:Key="MaterialDesignIconTemplate">
|
||||
<md:PackIcon
|
||||
Kind="{Binding Icon}"
|
||||
Width="16"
|
||||
Height="16"
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"/>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- FontAwesome 模板 -->
|
||||
<DataTemplate x:Key="FontawesomeIconTemplate">
|
||||
<ctls:FontAwesomeIcon
|
||||
Icon="{Binding Icon}"
|
||||
FontSize="14"
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"/>
|
||||
</DataTemplate>
|
||||
|
||||
<Style x:Key="CusTabItemPlusBaseStyle" TargetType="hc:TabItem" BasedOn="{StaticResource TabItemPlusBaseStyle}">
|
||||
<Setter Property="BorderThickness" Value="0,0,1,1"/>
|
||||
<Setter Property="Background" Value="{DynamicResource RegionBrush}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="hc:TabItem">
|
||||
<Grid x:Name="templateRoot" SnapsToDevicePixels="true" ContextMenu="{TemplateBinding Menu}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 外层边框 -->
|
||||
<Border Grid.ColumnSpan="3" BorderThickness="{TemplateBinding BorderThickness}" x:Name="mainBorder" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" Margin="0">
|
||||
<Border Margin="0,0,0,-1" x:Name="innerBorder" Background="{DynamicResource RegionBrush}" Visibility="Collapsed" />
|
||||
</Border>
|
||||
|
||||
<!-- 图标 -->
|
||||
<Path x:Name="PathMain" Margin="10,0,0,0" Grid.Column="0" Width="{TemplateBinding hc:IconElement.Width}" Height="{TemplateBinding hc:IconElement.Height}" Fill="{TemplateBinding Foreground}" SnapsToDevicePixels="True" Stretch="Uniform" Data="{TemplateBinding hc:IconElement.Geometry}" />
|
||||
|
||||
<!-- Header 内容 -->
|
||||
<ContentPresenter Grid.Column="1" x:Name="contentPresenter" ContentSource="Header" Focusable="False" HorizontalAlignment="Stretch" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center" />
|
||||
|
||||
<!-- 遮罩 -->
|
||||
<Border Name="BorderMask" Grid.Column="1" HorizontalAlignment="Right" Width="20" Background="{TemplateBinding Background}" Margin="0,0,1,1">
|
||||
<Border.OpacityMask>
|
||||
<LinearGradientBrush EndPoint="1,0" StartPoint="0,0">
|
||||
<GradientStop Color="White" Offset="1" />
|
||||
<GradientStop Offset="0" />
|
||||
</LinearGradientBrush>
|
||||
</Border.OpacityMask>
|
||||
</Border>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Focusable="False"
|
||||
Command="{Binding CloseTabCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
Background="Transparent"
|
||||
Width="28"
|
||||
Visibility="{Binding IsClosable, Converter={StaticResource Boolean2VisibilityConverter}}"
|
||||
OverridesDefaultStyle="True"
|
||||
Cursor="Hand">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<!-- 使用稍大的容器保证圆形和图标居中 -->
|
||||
<Grid Width="28" Height="28" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="1">
|
||||
<!-- 圆形 hover 背景(默认透明) -->
|
||||
<Ellipse x:Name="bg"
|
||||
Width="16"
|
||||
Height="16"
|
||||
Fill="Transparent"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
<Path x:Name="icon"
|
||||
Style="{StaticResource ClosePathStyle}"
|
||||
Width="8"
|
||||
Height="8"
|
||||
Fill="{DynamicResource PrimaryTextBrush}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
SnapsToDevicePixels="True"/>
|
||||
</Grid>
|
||||
|
||||
<ControlTemplate.Triggers>
|
||||
<!-- Hover:显示圆形背景并加深图标颜色 -->
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="bg" Property="Fill" Value="#E5E5E5"/>
|
||||
<Setter TargetName="icon" Property="Fill" Value="#333"/>
|
||||
</Trigger>
|
||||
|
||||
<!-- Pressed:背景更深 -->
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="bg" Property="Fill" Value="#CCCCCC"/>
|
||||
</Trigger>
|
||||
|
||||
<!-- Disabled 状态下淡化(可选) -->
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="icon" Property="Opacity" Value="0.4"/>
|
||||
<Setter TargetName="bg" Property="Opacity" Value="0.4"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="true">
|
||||
<Setter Property="Panel.ZIndex" Value="1"/>
|
||||
<Setter Property="Visibility" TargetName="innerBorder" Value="Visible"/>
|
||||
<Setter Property="TextElement.Foreground" Value="{DynamicResource PrimaryBrush}" TargetName="contentPresenter"/>
|
||||
<Setter Property="Background" TargetName="BorderMask" Value="{DynamicResource RegionBrush}"/>
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="hc:IconElement.Geometry" Value="{x:Null}">
|
||||
<Setter Property="Visibility" Value="Collapsed" TargetName="PathMain"/>
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.4" TargetName="contentPresenter"/>
|
||||
</Trigger>
|
||||
|
||||
<!-- Hover 效果 -->
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<!--<Setter Property="Background" TargetName="mainBorder" Value="{DynamicResource SecondaryRegionBrush}" />-->
|
||||
<Setter Property="TextElement.Foreground" Value="{DynamicResource PrimaryBrush}" TargetName="contentPresenter"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
</hc:TabControl.Resources>
|
||||
|
||||
<hc:TabControl.ItemContainerStyle>
|
||||
<Style TargetType="hc:TabItem" BasedOn="{StaticResource CusTabItemPlusBaseStyle}">
|
||||
<Setter Property="Menu">
|
||||
<Setter.Value>
|
||||
<ContextMenu MinWidth="140" Padding="3,5" Background="{DynamicResource ThirdlyRegionBrush}">
|
||||
<ContextMenu.ItemContainerStyle>
|
||||
<Style TargetType="MenuItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="MenuItem">
|
||||
<Border x:Name="Border" Background="{TemplateBinding Background}" CornerRadius="2" Padding="0,5">
|
||||
<ContentPresenter ContentSource="Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsHighlighted" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="{DynamicResource PrimaryBrush}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextIconBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.4"/>
|
||||
<Setter Property="Cursor" Value="No"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ContextMenu.ItemContainerStyle>
|
||||
<MenuItem
|
||||
Header="刷新页面"
|
||||
Command="{Binding PlacementTarget.DataContext.RefreshTabCommand,
|
||||
RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
CommandParameter="{Binding PlacementTarget.DataContext,
|
||||
RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
|
||||
<MenuItem
|
||||
Header="关闭当前"
|
||||
Command="{Binding PlacementTarget.DataContext.CloseTabCommand,
|
||||
RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
CommandParameter="{Binding PlacementTarget.DataContext,
|
||||
RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
IsEnabled="{Binding PlacementTarget.DataContext.IsClosable, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
|
||||
<MenuItem
|
||||
Header="关闭左侧"
|
||||
Command="{Binding PlacementTarget.DataContext.CloseLeftTabsCommand,
|
||||
RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
CommandParameter="{Binding PlacementTarget.DataContext,
|
||||
RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
IsEnabled="{Binding PlacementTarget.DataContext.HasClosableLeft, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
|
||||
<MenuItem
|
||||
Header="关闭右侧"
|
||||
Command="{Binding PlacementTarget.DataContext.CloseRightTabsCommand,
|
||||
RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
CommandParameter="{Binding PlacementTarget.DataContext,
|
||||
RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
IsEnabled="{Binding PlacementTarget.DataContext.HasClosableRight, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
|
||||
<MenuItem
|
||||
Header="关闭其他"
|
||||
Command="{Binding PlacementTarget.DataContext.CloseOtherTabsCommand,
|
||||
RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
CommandParameter="{Binding PlacementTarget.DataContext,
|
||||
RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
IsEnabled="{Binding PlacementTarget.DataContext.HasClosableOther, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
|
||||
<MenuItem
|
||||
Header="关闭全部"
|
||||
Command="{Binding PlacementTarget.DataContext.CloseAllTabsCommand,
|
||||
RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
CommandParameter="{Binding PlacementTarget.DataContext,
|
||||
RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
IsEnabled="{Binding PlacementTarget.DataContext.HasClosableAny, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
|
||||
</ContextMenu>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</hc:TabControl.ItemContainerStyle>
|
||||
|
||||
<hc:TabControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
|
||||
<!-- ContentControl:动态选择图标模板 -->
|
||||
<ContentControl>
|
||||
<ContentControl.Style>
|
||||
<Style TargetType="ContentControl">
|
||||
<!-- 默认模板(AntDesign) -->
|
||||
<Setter Property="ContentTemplate" Value="{StaticResource AntDesignIconTemplate}" />
|
||||
<!-- 绑定数据上下文 -->
|
||||
<Setter Property="Content" Value="{Binding}" />
|
||||
|
||||
<!-- 触发器根据 IconType 切换模板 -->
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IconType}" Value="{x:Static core:IconTypeEnum.MaterialDesign}">
|
||||
<Setter Property="ContentTemplate" Value="{StaticResource MaterialDesignIconTemplate}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IconType}" Value="{x:Static core:IconTypeEnum.FontAwesome}">
|
||||
<Setter Property="ContentTemplate" Value="{StaticResource FontawesomeIconTemplate}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ContentControl.Style>
|
||||
</ContentControl>
|
||||
|
||||
<!-- 文本标题 -->
|
||||
<TextBlock Text="{Binding Header}" VerticalAlignment="Center"/>
|
||||
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</hc:TabControl.ItemTemplate>
|
||||
|
||||
<!-- 内容模板设为空(因为内容在外部显示),不能省略,不然会显示SelectedTab类型的全路径名称 -->
|
||||
<hc:TabControl.ContentTemplate>
|
||||
<DataTemplate/>
|
||||
</hc:TabControl.ContentTemplate>
|
||||
</hc:TabControl>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<hc:TransitioningContentControl Grid.Row="1" prism:RegionManager.RegionName="{x:Static consts:CommonConst.ContentRegion}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
|
||||
<hc:Drawer
|
||||
IsOpen="{Binding IsAppSettingsOpen}"
|
||||
Dock="Left"
|
||||
ShowMask="True">
|
||||
<Border
|
||||
Background="{DynamicResource ThirdlyRegionBrush}"
|
||||
Effect="{StaticResource EffectShadow1}"
|
||||
Width="220">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<!-- 内容区域 -->
|
||||
<RowDefinition Height="*"/>
|
||||
<!-- 按钮区域 -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 可滚动的内容区域 -->
|
||||
<hc:ScrollViewer
|
||||
x:Name="ContentScrollViewer"
|
||||
IsInertiaEnabled="True"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Grid.Row="0">
|
||||
<StackPanel Margin="20" x:Name="ContentPanel">
|
||||
<TextBlock HorizontalAlignment="Left" Text="主题设置" FontSize="16" Style="{StaticResource TextBlockTitle}"/>
|
||||
<Border BorderThickness="0,0,0,1" BorderBrush="{DynamicResource ThirdlyBorderBrush}" Margin="0,10,0,20"/>
|
||||
|
||||
<DockPanel LastChildFill="False" Margin="0,0,0,20">
|
||||
<TextBlock Text="与系统同步" DockPanel.Dock="Left"/>
|
||||
<ToggleButton
|
||||
IsChecked="{Binding AppSettingsViewModel.SyncWithSystem}"
|
||||
Style="{StaticResource ToggleButtonSwitch}"
|
||||
hc:VisualElement.HighlightBrush="{DynamicResource PrimaryBrush}"
|
||||
DockPanel.Dock="Right"/>
|
||||
</DockPanel>
|
||||
<WrapPanel
|
||||
Orientation="Horizontal"
|
||||
Button.Click="OnSkinTypeChanged"
|
||||
IsEnabled="{Binding AppSettingsViewModel.IsNotSyncWithSystem}">
|
||||
<Button Tag="{x:Static hc:SkinType.Default}" Style="{StaticResource ButtonCustom}" Margin="0,0,8,8" ToolTip="默认主题">
|
||||
<Grid>
|
||||
<Border Background="White" Width="48" Height="48" CornerRadius="2" BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}"/>
|
||||
<ctls:FontAwesomeIcon
|
||||
Icon=""
|
||||
IconFamily="Solid"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource SuccessBrush}"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Visibility="{Binding AppSettingsViewModel.SkinType, Converter={StaticResource EnumToVisibilityConverter}, ConverterParameter={x:Static hc:SkinType.Default}}"/>
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button Tag="{x:Static hc:SkinType.Dark}" Style="{StaticResource ButtonCustom}" Margin="0,0,8,8" ToolTip="暗黑主题">
|
||||
<Grid>
|
||||
<Border Background="Black" Width="48" Height="48" CornerRadius="2" BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}"/>
|
||||
<ctls:FontAwesomeIcon
|
||||
Icon=""
|
||||
IconFamily="Solid"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource SuccessBrush}"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Visibility="{Binding AppSettingsViewModel.SkinType, Converter={StaticResource EnumToVisibilityConverter}, ConverterParameter={x:Static hc:SkinType.Dark}}"/>
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button Tag="{x:Static hc:SkinType.Violet}" Style="{StaticResource ButtonCustom}" Margin="0,0,8,8" ToolTip="紫色主题">
|
||||
<Grid>
|
||||
<Border Background="DarkViolet" Width="48" Height="48" CornerRadius="2" BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}"/>
|
||||
<ctls:FontAwesomeIcon
|
||||
Icon=""
|
||||
IconFamily="Solid"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource SuccessBrush}"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Visibility="{Binding AppSettingsViewModel.SkinType, Converter={StaticResource EnumToVisibilityConverter}, ConverterParameter={x:Static hc:SkinType.Violet}}"/>
|
||||
</Grid>
|
||||
</Button>
|
||||
</WrapPanel>
|
||||
|
||||
<TextBlock HorizontalAlignment="Left" Text="布局设置" FontSize="16" Margin="0,40,0,0" Style="{StaticResource TextBlockTitle}"/>
|
||||
<Border BorderThickness="0,0,0,1" BorderBrush="{DynamicResource ThirdlyBorderBrush}" Margin="0,10,0,20"/>
|
||||
<DockPanel LastChildFill="False">
|
||||
<TextBlock Text="显示选项卡" DockPanel.Dock="Left"/>
|
||||
<ToggleButton
|
||||
IsChecked="{Binding AppSettingsViewModel.IsTabControlVisible}"
|
||||
Style="{StaticResource ToggleButtonSwitch}"
|
||||
hc:VisualElement.HighlightBrush="{DynamicResource PrimaryBrush}"
|
||||
DockPanel.Dock="Right"/>
|
||||
</DockPanel>
|
||||
|
||||
<!-- 无滚动条时,按钮放在内容内部 -->
|
||||
<Button
|
||||
x:Name="InnerButton"
|
||||
Grid.Row="1"
|
||||
Style="{StaticResource ButtonPrimary}"
|
||||
Command="{Binding ResetAppSettingsCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="0,40,0,0"
|
||||
Visibility="Collapsed">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<md:PackIcon Kind="Refresh" Width="18" Height="18" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="重置" FontSize="14" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</hc:ScrollViewer>
|
||||
|
||||
<!-- 有滚动条时,按钮固定在底部 -->
|
||||
<Button
|
||||
x:Name="FixedButton"
|
||||
Grid.Row="1"
|
||||
Style="{StaticResource ButtonPrimary}"
|
||||
Command="{Binding ResetAppSettingsCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="20"
|
||||
Visibility="Visible">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<md:PackIcon Kind="Refresh" Width="18" Height="18" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="重置" FontSize="14" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</hc:Drawer>
|
||||
</Grid>
|
||||
</hc:Window>
|
||||
108
yy-admin-master/YY.Admin/Views/MainWindow.xaml.cs
Normal file
108
yy-admin-master/YY.Admin/Views/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using HandyControl.Data;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using YY.Admin.ViewModels;
|
||||
using Window = HandyControl.Controls.Window;
|
||||
|
||||
namespace YY.Admin.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
//this.Closed += MainWindow_Closed;
|
||||
ContentScrollViewer.SizeChanged += OnScrollViewerSizeChanged;
|
||||
}
|
||||
|
||||
private void MainWindow_Closed(object? sender, EventArgs e)
|
||||
{
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
|
||||
private void UserMenuButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button && button.ContextMenu != null)
|
||||
{
|
||||
button.ContextMenu.PlacementTarget = button;
|
||||
button.ContextMenu.IsOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void Splitter_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
|
||||
{
|
||||
double newWidth = LeftMenuTree.Width + e.HorizontalChange;
|
||||
|
||||
// 获取主窗口宽度
|
||||
double windowWidth = this.ActualWidth;
|
||||
|
||||
// 最小宽度
|
||||
double minWidth = 50;
|
||||
|
||||
// 最大宽度
|
||||
double maxWidth = windowWidth - LeftSidebar.Width - GridSplitter.Width - minWidth;
|
||||
|
||||
if (newWidth < minWidth)
|
||||
newWidth = minWidth;
|
||||
else if (newWidth > maxWidth)
|
||||
newWidth = maxWidth;
|
||||
|
||||
LeftMenuTree.Width = newWidth;
|
||||
}
|
||||
|
||||
private void TabControl_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 查找点击点是否在 TabItem Header 上
|
||||
var dep = e.OriginalSource as DependencyObject;
|
||||
|
||||
while (dep != null && dep is not TabItem)
|
||||
{
|
||||
dep = VisualTreeHelper.GetParent(dep);
|
||||
}
|
||||
|
||||
// 如果右键点在 TabItem 上
|
||||
if (dep is TabItem)
|
||||
{
|
||||
// 阻止 TabControl 切换 SelectedItem
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScrollViewerSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
// 检查是否需要显示滚动条
|
||||
//bool needsScroll = ContentPanel.ActualHeight > ContentScrollViewer.ActualHeight;
|
||||
|
||||
//bool needsScroll = ContentScrollViewer.ScrollableHeight > 0;
|
||||
|
||||
bool needsScroll = ContentScrollViewer.ComputedVerticalScrollBarVisibility == Visibility.Visible;
|
||||
|
||||
if (needsScroll)
|
||||
{
|
||||
// 有滚动条:显示固定按钮,隐藏内部按钮
|
||||
FixedButton.Visibility = Visibility.Visible;
|
||||
InnerButton.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无滚动条:隐藏固定按钮,显示内部按钮
|
||||
FixedButton.Visibility = Visibility.Collapsed;
|
||||
InnerButton.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSkinTypeChanged(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is Button { Tag: SkinType skinType })
|
||||
{
|
||||
var vm = DataContext as MainWindowViewModel;
|
||||
vm?.AppSettingsViewModel?.SkinType = skinType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
yy-admin-master/YY.Admin/Views/NotFoundView.xaml
Normal file
25
yy-admin-master/YY.Admin/Views/NotFoundView.xaml
Normal file
@@ -0,0 +1,25 @@
|
||||
<UserControl x:Class="YY.Admin.Views.NotFoundView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol">
|
||||
<Grid>
|
||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<Image
|
||||
Source="/Resources/Icon/404.png"
|
||||
Width="200"
|
||||
Height="200"/>
|
||||
<TextBlock Text="404 - 页面未找到"
|
||||
FontSize="22"
|
||||
FontWeight="Bold"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="抱歉,您访问的页面不存在"
|
||||
FontSize="16"
|
||||
Margin="0,10,0,20"
|
||||
HorizontalAlignment="Center"/>
|
||||
<!--<Button Content="返回首页"
|
||||
Command="{Binding GoHomeCommand}"
|
||||
Style="{StaticResource ButtonPrimary}"
|
||||
Width="120"/>-->
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
28
yy-admin-master/YY.Admin/Views/NotFoundView.xaml.cs
Normal file
28
yy-admin-master/YY.Admin/Views/NotFoundView.xaml.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace YY.Admin.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// NotFoundView.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class NotFoundView : UserControl
|
||||
{
|
||||
public NotFoundView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<UserControl x:Class="YY.Admin.Views.SysManage.DataDictionaryManagementView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:core="clr-namespace:YY.Admin.Core;assembly=YY.Admin.Core"
|
||||
xmlns:components="clr-namespace:YY.Admin.Views.Control"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Style="{StaticResource BaseViewStyle}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" CornerRadius="4" Margin="0 0 -10 0">
|
||||
<hc:Row>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:TextBox
|
||||
Text="{Binding Input.DictCode, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Placeholder="请输入字典编码"
|
||||
hc:InfoElement.Title="字典编码"
|
||||
hc:InfoElement.TitleWidth="70"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0 0 10 10"/>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:TextBox
|
||||
Text="{Binding Input.DictName, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Placeholder="请输入字典名称"
|
||||
hc:InfoElement.Title="字典名称"
|
||||
hc:InfoElement.TitleWidth="70"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0 0 10 10"/>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:TextBox
|
||||
Text="{Binding Input.ItemText, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Placeholder="请输入字典项文本"
|
||||
hc:InfoElement.Title="字典项文本"
|
||||
hc:InfoElement.TitleWidth="70"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0 0 10 10"/>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:TextBox
|
||||
Text="{Binding Input.ItemValue, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Placeholder="请输入字典项值"
|
||||
hc:InfoElement.Title="字典项值"
|
||||
hc:InfoElement.TitleWidth="70"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0 0 10 10"/>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:ComboBox
|
||||
SelectedValue="{Binding Input.Status, Converter={StaticResource EnumToIntConverter}, ConverterParameter={x:Type core:StatusEnum}}"
|
||||
ItemsSource="{Binding StatusList}"
|
||||
DisplayMemberPath="Key"
|
||||
SelectedValuePath="Value"
|
||||
hc:InfoElement.Placeholder="请选择状态"
|
||||
hc:InfoElement.Title="状态"
|
||||
hc:InfoElement.TitleWidth="70"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0 0 10 10"/>
|
||||
</hc:Col>
|
||||
</hc:Row>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="1" Margin="0,10">
|
||||
<hc:UniformSpacingPanel Spacing="10">
|
||||
<Button Style="{StaticResource ButtonPrimary}" Command="{Binding SearchCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Search"/>
|
||||
<TextBlock Text="搜索" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonDefault}" Command="{Binding ResetCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Refresh"/>
|
||||
<TextBlock Text="重置" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonSuccess}" Command="{Binding SyncCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="CloudSyncOutline"/>
|
||||
<TextBlock Text="同步数据字典" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</hc:UniformSpacingPanel>
|
||||
</Border>
|
||||
|
||||
<DataGrid
|
||||
Grid.Row="2"
|
||||
AutoGenerateColumns="False"
|
||||
HeadersVisibility="All"
|
||||
ItemsSource="{Binding PaginationDataGridViewModel.Data}"
|
||||
ColumnHeaderStyle="{StaticResource CusDataGridColumnHeaderStyle}"
|
||||
Style="{StaticResource CusDataGridStyle}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn IsReadOnly="True" Binding="{Binding DictCode}" Header="字典编码" Width="150" CellStyle="{StaticResource CusDataGridCellStyle}"/>
|
||||
<DataGridTextColumn IsReadOnly="True" Binding="{Binding DictName}" Header="字典名称" Width="170" CellStyle="{StaticResource CusDataGridCellStyle}"/>
|
||||
<DataGridTextColumn IsReadOnly="True" Binding="{Binding ItemText}" Header="字典项文本" Width="170" CellStyle="{StaticResource CusDataGridCellStyle}"/>
|
||||
<DataGridTextColumn IsReadOnly="True" Binding="{Binding ItemValue}" Header="字典项值" Width="140" CellStyle="{StaticResource CusDataGridCellStyle}"/>
|
||||
<DataGridTextColumn IsReadOnly="True" Binding="{Binding ItemDescription}" Header="描述" Width="220" CellStyle="{StaticResource CusDataGridCellStyle}"/>
|
||||
<DataGridTextColumn IsReadOnly="True" Binding="{Binding SortOrder}" Header="排序" Width="80" CellStyle="{StaticResource CusDataGridCellStyle}"/>
|
||||
<DataGridTextColumn IsReadOnly="True" Binding="{Binding Status, Converter={StaticResource EnumDescriptionConverter}}" Header="状态" Width="80" CellStyle="{StaticResource CusDataGridCellStyle}"/>
|
||||
<DataGridTextColumn IsReadOnly="True" Binding="{Binding CreateTime, StringFormat='yyyy-MM-dd HH:mm:ss'}" Header="创建时间" Width="170" CellStyle="{StaticResource CusDataGridCellStyle}"/>
|
||||
<DataGridTextColumn IsReadOnly="True" Binding="{Binding UpdateTime, StringFormat='yyyy-MM-dd HH:mm:ss'}" Header="更新时间" Width="170" CellStyle="{StaticResource CusDataGridCellStyle}"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<components:PaginationDataGridControl Grid.Row="3" DataContext="{Binding PaginationDataGridViewModel}"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user