更新项目配置,新增设备同步模块,优化WebSocket和Swagger配置,增强SCADA系统的免登录接口,支持数据字典项和登录日志的免登录查询与记录。调整Java编译设置,确保更好的开发体验。

This commit is contained in:
geht
2026-04-28 10:23:58 +08:00
parent bbe46dcf2d
commit 142a0bdaba
1013 changed files with 41858 additions and 28 deletions

Binary file not shown.

View 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>

View 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>

View 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("应用程序已退出");
}
}
}

View 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)
)]

View File

@@ -0,0 +1,11 @@
using YY.Admin.Module;
namespace YY.Admin.Event
{
/// <summary>
/// Tab关闭事件
/// </summary>
public class TabClosedEvent : PubSubEvent<TabItemModel>
{
}
}

View File

@@ -0,0 +1,11 @@
using YY.Admin.Module;
namespace YY.Admin.Event
{
/// <summary>
/// Tab刷新事件
/// </summary>
public class TabRefreshEvent : PubSubEvent<TabItemModel>
{
}
}

View File

@@ -0,0 +1,11 @@
using YY.Admin.Module;
namespace YY.Admin.Event
{
/// <summary>
/// Tab选中事件
/// </summary>
public class TabSelectedEvent : PubSubEvent<TabItemModel>
{
}
}

View File

@@ -0,0 +1,12 @@

using YY.Admin.Module;
namespace YY.Admin.Event
{
/// <summary>
/// Tab源选中事件
/// </summary>
public class TabSourceSelectedEvent : PubSubEvent<TabSource>
{
}
}

View 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(); // 标记为已观察
}
}
}

View File

@@ -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("密码不能为空");
}
}
}

View File

@@ -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));
}
}
}

View 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;
}
}
}

View File

@@ -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";
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View 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);
}
}

View 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 };
}
}

View 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);
}
}

View 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;
});
}
}
}

View 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;
}
}
}

View 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; }
//}
}

View 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>();
}
}
}

View 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;
}
}
}

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

View 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)));
}
}

View 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);
}
}
}
}

View 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; }
}
}

View 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;
}
}
}
}

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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-->

View 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>

Binary file not shown.

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

View 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();
}
}
}
}

View 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>

View 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;
// 4Skin的长度
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);
}
}
}
}

View 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) { }
}
}

View 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 ?? "&#xe810;");
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;
//}
}
}

View File

@@ -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;
}
}
}
}

View 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));
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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() { }
}
}

View File

@@ -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()
{
}
}
}

View File

@@ -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}";
}
}
}
}

View File

@@ -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()
{
}
}
}

View File

@@ -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()
{
}
}
}

View 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 = "服务器配置已保存";
}
});
}
}
}

View 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("&#xe7a3;"),
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();
}
}
}

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

View File

@@ -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();
}
}

View File

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

View File

@@ -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();
}
}
}

View File

@@ -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;
// }
//}
}
}

View File

@@ -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
//}
}

View 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>

View 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;
}
}
}

View File

@@ -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>

View File

@@ -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;
//}
}
}

View 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>

View File

@@ -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;
}
}
}
}
}

View File

@@ -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>

View 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 仍然不存在,尝试 RequestNavigatePrism 通常会创建 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();
}
}
}
}

View 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="&#xE7EE;"
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="&#xE7EF;"
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="&#xE8B7;"
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="&#xE8A5;"
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="&#xE8BC;"
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="&#xE946;"
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>

View 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();
}
}
}

View 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="&#xf05a;"
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>

View 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.Dialogs
{
/// <summary>
/// AlertDialogView.xaml 的交互逻辑
/// </summary>
public partial class AlertDialogView : UserControl
{
public AlertDialogView()
{
InitializeComponent();
}
}
}

View 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>

View 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.Dialogs
{
/// <summary>
/// ConfirmDialogView.xaml 的交互逻辑
/// </summary>
public partial class ConfirmDialogView : UserControl
{
public ConfirmDialogView()
{
InitializeComponent();
}
}
}

View 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="&#xf06a;"
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>

View 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.Dialogs
{
/// <summary>
/// ErrorDialogView.xaml 的交互逻辑
/// </summary>
public partial class ErrorDialogView : UserControl
{
public ErrorDialogView()
{
InitializeComponent();
}
}
}

View File

@@ -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="&#xf0c7;" 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>

View File

@@ -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();
}
}
}

View 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="&#xf058;"
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="&#xf058;"
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>

View 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.Dialogs
{
/// <summary>
/// SuccessDialogView.xaml 的交互逻辑
/// </summary>
public partial class SuccessDialogView : UserControl
{
public SuccessDialogView()
{
InitializeComponent();
}
}
}

View 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="&#xf071;"
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="&#xf071;"
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>

View 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.Dialogs
{
/// <summary>
/// WarningDialogView.xaml 的交互逻辑
/// </summary>
public partial class WarningDialogView : UserControl
{
public WarningDialogView()
{
InitializeComponent();
}
}
}

View 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="&#xf013;" 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="&#xf007;"
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="&#xf023;"
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="&#xf110;" 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="&#xf021;" 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>

View 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;
}
}
}

View 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="&#xf233;" 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="&#xf0f3;"/>
</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="&#xf058;"
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="&#xf058;"
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="&#xf058;"
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>

View 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;
}
}
}
}

View 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>

View 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();
}
}
}

View File

@@ -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