644 lines
21 KiB
C#
644 lines
21 KiB
C#
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.Services.Service.Config;
|
||
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 用户操作
|
||
/// <summary>
|
||
/// 从系统配置应用登录状态检查间隔(分钟)
|
||
/// </summary>
|
||
private static void ApplyTokenCheckIntervalFromConfig()
|
||
{
|
||
var minutes = 1;
|
||
try
|
||
{
|
||
var cfg = Prism.Ioc.ContainerLocator.Current.Resolve<ISysConfigService>();
|
||
var v = cfg.GetConfigValue<int>(ConfigConst.SysTokenCheckIntervalMinutes).GetAwaiter().GetResult();
|
||
if (v >= 1 && v <= 120)
|
||
minutes = v;
|
||
}
|
||
catch
|
||
{
|
||
// 使用默认 1 分钟
|
||
}
|
||
|
||
if (_tokenCheckTimer != null)
|
||
_tokenCheckTimer.Interval = TimeSpan.FromMinutes(minutes);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否开启登录永不过期
|
||
/// </summary>
|
||
private static bool IsTokenNeverExpireEnabled()
|
||
{
|
||
try
|
||
{
|
||
var cfg = Prism.Ioc.ContainerLocator.Current.Resolve<ISysConfigService>();
|
||
return cfg.GetConfigValue<bool>(ConfigConst.SysTokenNeverExpire).GetAwaiter().GetResult();
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 登录设置保存后刷新检查间隔(已启动定时器时立即生效)
|
||
/// </summary>
|
||
public static void RefreshTokenCheckIntervalFromConfig()
|
||
{
|
||
ApplyTokenCheckIntervalFromConfig();
|
||
}
|
||
|
||
// 启动定时器的方法
|
||
public static void StartTokenCheckTimer()
|
||
{
|
||
if (_tokenCheckTimer == null)
|
||
{
|
||
_tokenCheckTimer = new DispatcherTimer();
|
||
_tokenCheckTimer.Tick += CheckTokenExpiration;
|
||
|
||
// 捕获全局用户输入事件
|
||
EventManager.RegisterClassHandler(typeof(Window),
|
||
UIElement.PreviewMouseDownEvent,
|
||
new MouseButtonEventHandler(OnUserActivity));
|
||
EventManager.RegisterClassHandler(typeof(Window),
|
||
UIElement.PreviewKeyDownEvent,
|
||
new KeyEventHandler(OnUserActivity));
|
||
}
|
||
|
||
ApplyTokenCheckIntervalFromConfig();
|
||
|
||
if (!_tokenCheckTimer.IsEnabled)
|
||
{
|
||
_tokenCheckTimer.Start();
|
||
}
|
||
}
|
||
|
||
// 停止定时器的方法
|
||
public static void StopTokenCheckTimer()
|
||
{
|
||
if (_tokenCheckTimer != null && _tokenCheckTimer.IsEnabled)
|
||
{
|
||
_tokenCheckTimer.Stop();
|
||
}
|
||
}
|
||
|
||
private static void OnUserActivity(object sender, EventArgs e)
|
||
{
|
||
if (IsTokenNeverExpireEnabled())
|
||
return;
|
||
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;
|
||
}
|
||
|
||
if (IsTokenNeverExpireEnabled())
|
||
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) { }
|
||
}
|
||
}
|