更新密码数据123
This commit is contained in:
@@ -5,6 +5,7 @@ using Avalonia.Data.Core.Plugins;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModels;
|
||||
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Views;
|
||||
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.Linq;
|
||||
@@ -15,6 +16,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance
|
||||
public partial class App : Application
|
||||
{
|
||||
private static bool exceptionHandlersRegistered;
|
||||
private readonly MachineLicenseService licenseService = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -27,15 +29,51 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow
|
||||
var check = licenseService.Check(updateHeartbeat: true);
|
||||
if (check.CanUseSoftware)
|
||||
{
|
||||
DataContext = new MainWindowViewModel(),
|
||||
};
|
||||
ShowMainWindow(desktop, showImmediately: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var mode = check.State switch
|
||||
{
|
||||
Models.LicenseCheckState.NotInitialized => LicenseWindowMode.Initialization,
|
||||
Models.LicenseCheckState.Expired => LicenseWindowMode.Unlock,
|
||||
_ => LicenseWindowMode.Blocked
|
||||
};
|
||||
var licenseWindow = new LicenseWindow(licenseService, mode, check.Message);
|
||||
desktop.MainWindow = licenseWindow;
|
||||
licenseWindow.Closed += (_, _) =>
|
||||
{
|
||||
if (licenseWindow.Succeeded)
|
||||
{
|
||||
ShowMainWindow(desktop, showImmediately: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
desktop.Shutdown();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
private void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop, bool showImmediately)
|
||||
{
|
||||
var viewModel = new MainWindowViewModel(licenseService);
|
||||
desktop.MainWindow = new MainWindow(licenseService)
|
||||
{
|
||||
DataContext = viewModel,
|
||||
};
|
||||
if (showImmediately)
|
||||
{
|
||||
desktop.MainWindow.Show();
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterExceptionHandlers()
|
||||
{
|
||||
if (exceptionHandlersRegistered)
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="SukiUI" Version="6.0.2" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.0" />
|
||||
<PackageReference Include="System.IO.Ports" Version="10.0.8" />
|
||||
<PackageReference Include="Tmds.DBus.Protocol" Version="0.94.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
|
||||
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models
|
||||
{
|
||||
public enum LicenseStage
|
||||
{
|
||||
FirstPeriod,
|
||||
SecondPeriod,
|
||||
Permanent
|
||||
}
|
||||
|
||||
public enum LicenseCheckState
|
||||
{
|
||||
NotInitialized,
|
||||
Valid,
|
||||
Expired,
|
||||
Tampered
|
||||
}
|
||||
|
||||
public sealed record PasswordSecret(string Salt, string Hash);
|
||||
|
||||
public sealed record LicenseData(
|
||||
string InstallId,
|
||||
LicenseStage Stage,
|
||||
DateTime StageStartedUtc,
|
||||
DateTime LastTrustedUtc,
|
||||
int FirstPeriodMonths,
|
||||
int SecondPeriodMonths,
|
||||
PasswordSecret AdminPassword,
|
||||
PasswordSecret FirstUnlockPassword,
|
||||
PasswordSecret SecondUnlockPassword);
|
||||
|
||||
public sealed record LicenseCheckResult(
|
||||
LicenseCheckState State,
|
||||
LicenseStage Stage,
|
||||
DateTime? ExpiresUtc,
|
||||
string Message)
|
||||
{
|
||||
public bool CanUseSoftware => State == LicenseCheckState.Valid;
|
||||
public bool RequiresUnlock => State == LicenseCheckState.Expired;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Avalonia;
|
||||
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.IO;
|
||||
@@ -14,6 +15,11 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance
|
||||
|
||||
try
|
||||
{
|
||||
if (args.Length == 2 && string.Equals(args[0], MachineLicenseService.InstallArgument, StringComparison.Ordinal))
|
||||
{
|
||||
return MachineLicenseService.RunElevatedInstall(args[1]);
|
||||
}
|
||||
|
||||
Log.Information("鞋类整鞋防滑性能试验程序启动");
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models;
|
||||
using Microsoft.Win32;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Principal;
|
||||
using System.Text.Json;
|
||||
|
||||
#pragma warning disable CA1416
|
||||
|
||||
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
|
||||
{
|
||||
public sealed class MachineLicenseService
|
||||
{
|
||||
public const string InstallArgument = "--install-license";
|
||||
private const string RegistryPath = @"SOFTWARE\FootwearSlipResistance\License";
|
||||
private const string RegistryInstallIdName = "InstallId";
|
||||
private static readonly TimeSpan ClockRollbackTolerance = TimeSpan.FromMinutes(5);
|
||||
private static readonly TimeSpan HeartbeatInterval = TimeSpan.FromMinutes(10);
|
||||
private static readonly byte[] AdditionalEntropy = "FootwearSlipResistance-License-v1"u8.ToArray();
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
|
||||
private LicenseData? current;
|
||||
|
||||
public LicenseData? Current => current;
|
||||
|
||||
public static string LicenseFilePath =>
|
||||
Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
"FootwearSlipResistance",
|
||||
"license.dat");
|
||||
|
||||
public LicenseCheckResult Check(bool updateHeartbeat = false)
|
||||
{
|
||||
var loadResult = TryLoad(out var data);
|
||||
if (loadResult is not null)
|
||||
{
|
||||
current = null;
|
||||
return loadResult;
|
||||
}
|
||||
|
||||
current = data;
|
||||
var now = DateTime.UtcNow;
|
||||
if (now + ClockRollbackTolerance < data!.LastTrustedUtc)
|
||||
{
|
||||
return new LicenseCheckResult(
|
||||
LicenseCheckState.Tampered,
|
||||
data.Stage,
|
||||
GetExpiry(data),
|
||||
"检测到系统时间回退,软件已锁定,请联系管理员处理。");
|
||||
}
|
||||
|
||||
if (updateHeartbeat && now - data.LastTrustedUtc >= HeartbeatInterval)
|
||||
{
|
||||
try
|
||||
{
|
||||
current = data with { LastTrustedUtc = now };
|
||||
SaveCurrent();
|
||||
data = current;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "更新授权可信时间失败");
|
||||
return new LicenseCheckResult(
|
||||
LicenseCheckState.Tampered,
|
||||
data.Stage,
|
||||
GetExpiry(data),
|
||||
"授权状态无法安全更新,软件已锁定,请联系管理员处理。");
|
||||
}
|
||||
}
|
||||
|
||||
var expiry = GetExpiry(data);
|
||||
if (expiry.HasValue && now >= expiry.Value)
|
||||
{
|
||||
return new LicenseCheckResult(
|
||||
LicenseCheckState.Expired,
|
||||
data.Stage,
|
||||
expiry,
|
||||
data.Stage == LicenseStage.FirstPeriod
|
||||
? "第一阶段使用时效已到,请输入第一次时效密码。"
|
||||
: "第二阶段使用时效已到,请输入第二次时效密码。");
|
||||
}
|
||||
|
||||
return new LicenseCheckResult(
|
||||
LicenseCheckState.Valid,
|
||||
data.Stage,
|
||||
expiry,
|
||||
data.Stage == LicenseStage.Permanent ? "软件已永久授权。" : "软件授权有效。");
|
||||
}
|
||||
|
||||
public void Initialize(
|
||||
string adminPassword,
|
||||
string firstUnlockPassword,
|
||||
string secondUnlockPassword,
|
||||
int firstPeriodMonths,
|
||||
int secondPeriodMonths)
|
||||
{
|
||||
ValidateSettings(adminPassword, firstUnlockPassword, secondUnlockPassword, firstPeriodMonths, secondPeriodMonths);
|
||||
var now = DateTime.UtcNow;
|
||||
var data = new LicenseData(
|
||||
Guid.NewGuid().ToString("N"),
|
||||
LicenseStage.FirstPeriod,
|
||||
now,
|
||||
now,
|
||||
firstPeriodMonths,
|
||||
secondPeriodMonths,
|
||||
CreateSecret(adminPassword),
|
||||
CreateSecret(firstUnlockPassword),
|
||||
CreateSecret(secondUnlockPassword));
|
||||
|
||||
if (IsAdministrator())
|
||||
{
|
||||
InstallInitialLicense(data);
|
||||
current = data;
|
||||
return;
|
||||
}
|
||||
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), $"footwear-license-{Guid.NewGuid():N}.dat");
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(tempPath, Protect(data));
|
||||
var executable = Environment.ProcessPath ?? throw new InvalidOperationException("无法确定当前程序路径。");
|
||||
using var process = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = executable,
|
||||
Arguments = $"{InstallArgument} \"{tempPath}\"",
|
||||
UseShellExecute = true,
|
||||
Verb = "runas"
|
||||
}) ?? throw new InvalidOperationException("无法启动授权初始化程序。");
|
||||
process.WaitForExit();
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException("授权初始化未完成或管理员权限确认被取消。");
|
||||
}
|
||||
|
||||
var result = Check();
|
||||
if (!result.CanUseSoftware)
|
||||
{
|
||||
throw new InvalidOperationException(result.Message);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDelete(tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
public static int RunElevatedInstall(string packagePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsAdministrator())
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
var bytes = File.ReadAllBytes(packagePath);
|
||||
var data = Unprotect(bytes);
|
||||
InstallInitialLicense(data);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "整机授权初始化失败");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public bool VerifyAdminPassword(string password) =>
|
||||
EnsureCurrent() && VerifySecret(password, current!.AdminPassword);
|
||||
|
||||
public bool UnlockCurrentStage(string password)
|
||||
{
|
||||
if (!EnsureCurrent() || current!.Stage == LicenseStage.Permanent)
|
||||
{
|
||||
return current?.Stage == LicenseStage.Permanent;
|
||||
}
|
||||
|
||||
var secret = current.Stage == LicenseStage.FirstPeriod
|
||||
? current.FirstUnlockPassword
|
||||
: current.SecondUnlockPassword;
|
||||
if (!VerifySecret(password, secret))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
current = current.Stage == LicenseStage.FirstPeriod
|
||||
? current with { Stage = LicenseStage.SecondPeriod, StageStartedUtc = now, LastTrustedUtc = now }
|
||||
: current with { Stage = LicenseStage.Permanent, StageStartedUtc = now, LastTrustedUtc = now };
|
||||
SaveCurrent();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void UpdateSettings(
|
||||
string? newAdminPassword,
|
||||
string? newFirstUnlockPassword,
|
||||
string? newSecondUnlockPassword,
|
||||
int firstPeriodMonths,
|
||||
int secondPeriodMonths)
|
||||
{
|
||||
if (!EnsureCurrent())
|
||||
{
|
||||
throw new InvalidOperationException("授权状态未加载。");
|
||||
}
|
||||
|
||||
ValidateMonths(firstPeriodMonths, secondPeriodMonths);
|
||||
current = current! with
|
||||
{
|
||||
FirstPeriodMonths = firstPeriodMonths,
|
||||
SecondPeriodMonths = secondPeriodMonths,
|
||||
AdminPassword = string.IsNullOrWhiteSpace(newAdminPassword) ? current.AdminPassword : CreateSecret(newAdminPassword),
|
||||
FirstUnlockPassword = string.IsNullOrWhiteSpace(newFirstUnlockPassword) ? current.FirstUnlockPassword : CreateSecret(newFirstUnlockPassword),
|
||||
SecondUnlockPassword = string.IsNullOrWhiteSpace(newSecondUnlockPassword) ? current.SecondUnlockPassword : CreateSecret(newSecondUnlockPassword),
|
||||
LastTrustedUtc = DateTime.UtcNow
|
||||
};
|
||||
SaveCurrent();
|
||||
}
|
||||
|
||||
public void RestartTiming()
|
||||
{
|
||||
if (!EnsureCurrent())
|
||||
{
|
||||
throw new InvalidOperationException("授权状态未加载。");
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
current = current! with
|
||||
{
|
||||
Stage = LicenseStage.FirstPeriod,
|
||||
StageStartedUtc = now,
|
||||
LastTrustedUtc = now
|
||||
};
|
||||
SaveCurrent();
|
||||
}
|
||||
|
||||
public string DescribeCurrent()
|
||||
{
|
||||
if (!EnsureCurrent())
|
||||
{
|
||||
return "授权状态不可用";
|
||||
}
|
||||
|
||||
var expiry = GetExpiry(current!);
|
||||
var stageText = current!.Stage switch
|
||||
{
|
||||
LicenseStage.FirstPeriod => "第一阶段",
|
||||
LicenseStage.SecondPeriod => "第二阶段",
|
||||
_ => "永久授权"
|
||||
};
|
||||
return expiry.HasValue
|
||||
? $"{stageText};开始:{current.StageStartedUtc.ToLocalTime():yyyy-MM-dd HH:mm};到期:{expiry.Value.ToLocalTime():yyyy-MM-dd HH:mm}"
|
||||
: stageText;
|
||||
}
|
||||
|
||||
private LicenseCheckResult? TryLoad(out LicenseData? data)
|
||||
{
|
||||
data = null;
|
||||
var registryInstallId = ReadRegistryInstallId();
|
||||
var fileExists = File.Exists(LicenseFilePath);
|
||||
if (!fileExists && string.IsNullOrWhiteSpace(registryInstallId))
|
||||
{
|
||||
return new LicenseCheckResult(LicenseCheckState.NotInitialized, LicenseStage.FirstPeriod, null, "首次使用,请先完成时效授权初始化。");
|
||||
}
|
||||
|
||||
if (!fileExists || string.IsNullOrWhiteSpace(registryInstallId))
|
||||
{
|
||||
return new LicenseCheckResult(LicenseCheckState.Tampered, LicenseStage.FirstPeriod, null, "授权文件或整机安装标记缺失,软件已锁定。");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
data = Unprotect(File.ReadAllBytes(LicenseFilePath));
|
||||
if (!string.Equals(data.InstallId, registryInstallId, StringComparison.Ordinal))
|
||||
{
|
||||
data = null;
|
||||
return new LicenseCheckResult(LicenseCheckState.Tampered, LicenseStage.FirstPeriod, null, "授权文件与整机安装标记不匹配,软件已锁定。");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "读取整机授权失败:Path={Path}", LicenseFilePath);
|
||||
return new LicenseCheckResult(LicenseCheckState.Tampered, LicenseStage.FirstPeriod, null, "授权文件损坏或无法读取,软件已锁定。");
|
||||
}
|
||||
}
|
||||
|
||||
private bool EnsureCurrent()
|
||||
{
|
||||
if (current is not null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var result = Check();
|
||||
return result.State is LicenseCheckState.Valid or LicenseCheckState.Expired;
|
||||
}
|
||||
|
||||
private void SaveCurrent()
|
||||
{
|
||||
if (current is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(LicenseFilePath)!;
|
||||
Directory.CreateDirectory(directory);
|
||||
var tempPath = Path.Combine(directory, $"license-{Guid.NewGuid():N}.tmp");
|
||||
File.WriteAllBytes(tempPath, Protect(current));
|
||||
File.Move(tempPath, LicenseFilePath, true);
|
||||
}
|
||||
|
||||
private static void InstallInitialLicense(LicenseData data)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(LicenseFilePath)!;
|
||||
Directory.CreateDirectory(directory);
|
||||
File.WriteAllBytes(LicenseFilePath, Protect(data));
|
||||
using var key = Registry.LocalMachine.CreateSubKey(RegistryPath, true)
|
||||
?? throw new InvalidOperationException("无法创建整机授权注册表标记。");
|
||||
key.SetValue(RegistryInstallIdName, data.InstallId, RegistryValueKind.String);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
using var permissions = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "icacls.exe",
|
||||
Arguments = $"\"{directory}\" /grant *S-1-5-32-545:(OI)(CI)M /T /C",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
});
|
||||
permissions?.WaitForExit();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] Protect(LicenseData data) =>
|
||||
ProtectedData.Protect(
|
||||
JsonSerializer.SerializeToUtf8Bytes(data, JsonOptions),
|
||||
AdditionalEntropy,
|
||||
DataProtectionScope.LocalMachine);
|
||||
|
||||
private static LicenseData Unprotect(byte[] protectedBytes) =>
|
||||
JsonSerializer.Deserialize<LicenseData>(
|
||||
ProtectedData.Unprotect(protectedBytes, AdditionalEntropy, DataProtectionScope.LocalMachine))
|
||||
?? throw new InvalidDataException("授权数据为空。");
|
||||
|
||||
private static string? ReadRegistryInstallId()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.LocalMachine.OpenSubKey(RegistryPath, false);
|
||||
return key?.GetValue(RegistryInstallIdName) as string;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "读取整机授权注册表标记失败");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static PasswordSecret CreateSecret(string password)
|
||||
{
|
||||
ValidatePassword(password, "密码");
|
||||
var salt = RandomNumberGenerator.GetBytes(16);
|
||||
var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, 100_000, HashAlgorithmName.SHA256, 32);
|
||||
return new PasswordSecret(Convert.ToBase64String(salt), Convert.ToBase64String(hash));
|
||||
}
|
||||
|
||||
private static bool VerifySecret(string password, PasswordSecret secret)
|
||||
{
|
||||
try
|
||||
{
|
||||
var salt = Convert.FromBase64String(secret.Salt);
|
||||
var expected = Convert.FromBase64String(secret.Hash);
|
||||
var actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, 100_000, HashAlgorithmName.SHA256, expected.Length);
|
||||
return CryptographicOperations.FixedTimeEquals(actual, expected);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static DateTime? GetExpiry(LicenseData data) =>
|
||||
data.Stage switch
|
||||
{
|
||||
LicenseStage.FirstPeriod => data.StageStartedUtc.AddMonths(data.FirstPeriodMonths),
|
||||
LicenseStage.SecondPeriod => data.StageStartedUtc.AddMonths(data.SecondPeriodMonths),
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static void ValidateSettings(string admin, string first, string second, int firstMonths, int secondMonths)
|
||||
{
|
||||
ValidatePassword(admin, "管理密码");
|
||||
ValidatePassword(first, "第一次时效密码");
|
||||
ValidatePassword(second, "第二次时效密码");
|
||||
ValidateMonths(firstMonths, secondMonths);
|
||||
}
|
||||
|
||||
private static void ValidatePassword(string password, string label)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(password) || password.Length is < 4 or > 64)
|
||||
{
|
||||
throw new ArgumentException($"{label}长度必须为 4-64 个字符。");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateMonths(int firstMonths, int secondMonths)
|
||||
{
|
||||
if (firstMonths is < 1 or > 120 || secondMonths is < 1 or > 120)
|
||||
{
|
||||
throw new ArgumentException("两段时效必须为 1-120 个自然月。");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAdministrator()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using var identity = WindowsIdentity.GetCurrent();
|
||||
return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
|
||||
private static void TryDelete(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning restore CA1416
|
||||
@@ -44,9 +44,11 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
|
||||
private static readonly TimeSpan ResetButtonPendingTimeout = TimeSpan.FromMilliseconds(800);
|
||||
private static readonly TimeSpan TestButtonPendingTimeout = TimeSpan.FromSeconds(5);
|
||||
private static readonly TimeSpan RealtimeCurveTraceInterval = TimeSpan.FromSeconds(1);
|
||||
private static readonly TimeSpan LicenseCheckInterval = TimeSpan.FromMinutes(1);
|
||||
|
||||
private readonly SlipResistanceDeviceService deviceService = new();
|
||||
private readonly SlipExcelExportService excelExportService = new();
|
||||
private readonly MachineLicenseService licenseService;
|
||||
private readonly DispatcherTimer refreshTimer;
|
||||
private readonly Stopwatch runStopwatch = new();
|
||||
private readonly List<SlipDataPoint> currentRun = [];
|
||||
@@ -71,6 +73,12 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
|
||||
private double slidingStartDisplacementMm;
|
||||
private double? slidingStartTimeSeconds;
|
||||
private int activeRunLubricantIndex;
|
||||
private DateTime lastLicenseCheckAt = DateTime.MinValue;
|
||||
private bool isLicenseLockPending;
|
||||
private bool licenseLockRequestedRaised;
|
||||
private string licenseLockMessage = string.Empty;
|
||||
|
||||
public event Action<string>? LicenseLockRequested;
|
||||
|
||||
[ObservableProperty]
|
||||
private string testNumber = $"SLIP-{DateTime.Now:yyyyMMdd-HHmm}";
|
||||
@@ -275,8 +283,13 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
|
||||
}
|
||||
];
|
||||
|
||||
public MainWindowViewModel()
|
||||
public MainWindowViewModel() : this(new MachineLicenseService())
|
||||
{
|
||||
}
|
||||
|
||||
public MainWindowViewModel(MachineLicenseService licenseService)
|
||||
{
|
||||
this.licenseService = licenseService;
|
||||
Log.Information("初始化主页面 ViewModel");
|
||||
Series =
|
||||
[
|
||||
@@ -294,6 +307,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
|
||||
refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(20) };
|
||||
refreshTimer.Tick += (_, _) => RefreshFromDevice();
|
||||
refreshTimer.Start();
|
||||
RefreshLicenseState();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -325,7 +339,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
|
||||
[RelayCommand]
|
||||
private async Task StartTest()
|
||||
{
|
||||
if (!IsTestButtonEnabled)
|
||||
if (!IsTestButtonEnabled || isLicenseLockPending)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -487,16 +501,16 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private Task LiftMotion() => RunDeviceCommand(deviceService.ApplyOldLiftAsync(), "提升按老代码逻辑执行:M4=0, M5=1");
|
||||
private Task LiftMotion() => RunLicensedDeviceCommand(deviceService.ApplyOldLiftAsync, "提升按老代码逻辑执行:M4=0, M5=1");
|
||||
|
||||
[RelayCommand]
|
||||
private Task LowerMotion() => RunDeviceCommand(deviceService.ApplyOldLowerAsync(), "下降按老代码逻辑执行:M5=0, M4=1");
|
||||
private Task LowerMotion() => RunLicensedDeviceCommand(deviceService.ApplyOldLowerAsync, "下降按老代码逻辑执行:M5=0, M4=1");
|
||||
|
||||
[RelayCommand]
|
||||
private Task MoveLeftMotion() => RunDeviceCommand(deviceService.ToggleOldMoveLeftAsync(), "左移按老代码逻辑切换 M1");
|
||||
private Task MoveLeftMotion() => RunLicensedDeviceCommand(deviceService.ToggleOldMoveLeftAsync, "左移按老代码逻辑切换 M1");
|
||||
|
||||
[RelayCommand]
|
||||
private Task MoveRightMotion() => RunDeviceCommand(deviceService.ToggleOldMoveRightAsync(), "右移按老代码逻辑切换 M2");
|
||||
private Task MoveRightMotion() => RunLicensedDeviceCommand(deviceService.ToggleOldMoveRightAsync, "右移按老代码逻辑切换 M2");
|
||||
|
||||
public Task StopAllMotionAsync() => RunDeviceCommand(deviceService.StopAllMotionAsync(), "全部运动已停止");
|
||||
|
||||
@@ -624,9 +638,24 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
|
||||
deviceService.Dispose();
|
||||
}
|
||||
|
||||
public void RefreshLicenseState()
|
||||
{
|
||||
var check = licenseService.Check(updateHeartbeat: true);
|
||||
lastLicenseCheckAt = DateTime.UtcNow;
|
||||
isLicenseLockPending = !check.CanUseSoftware;
|
||||
licenseLockMessage = check.Message;
|
||||
if (!isLicenseLockPending)
|
||||
{
|
||||
licenseLockRequestedRaised = false;
|
||||
}
|
||||
|
||||
UpdateTestButtonState(deviceService.CurrentSnapshot);
|
||||
}
|
||||
|
||||
private void RefreshFromDevice()
|
||||
{
|
||||
var device = deviceService.CurrentSnapshot;
|
||||
CheckRuntimeLicense(device);
|
||||
if (device.IsConnected)
|
||||
{
|
||||
DeviceStatus = device.IsTestRunning ? "联机 / 测试中" : device.IsResetting ? "联机 / 复位中" : "联机 / 待机";
|
||||
@@ -699,6 +728,14 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
|
||||
|
||||
private void UpdateTestButtonState(SlipDeviceSnapshot device)
|
||||
{
|
||||
if (isLicenseLockPending)
|
||||
{
|
||||
ClearTestButtonFeedback();
|
||||
TestButtonText = "授权到期";
|
||||
IsTestButtonEnabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (device.IsTestRunning)
|
||||
{
|
||||
ClearTestButtonFeedback();
|
||||
@@ -720,6 +757,26 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
|
||||
IsTestButtonEnabled = true;
|
||||
}
|
||||
|
||||
private void CheckRuntimeLicense(SlipDeviceSnapshot device)
|
||||
{
|
||||
if (DateTime.UtcNow - lastLicenseCheckAt >= LicenseCheckInterval)
|
||||
{
|
||||
var check = licenseService.Check(updateHeartbeat: true);
|
||||
lastLicenseCheckAt = DateTime.UtcNow;
|
||||
isLicenseLockPending = !check.CanUseSoftware;
|
||||
licenseLockMessage = check.Message;
|
||||
}
|
||||
|
||||
if (isLicenseLockPending
|
||||
&& !device.IsTestRunning
|
||||
&& !device.IsResetting
|
||||
&& !licenseLockRequestedRaised)
|
||||
{
|
||||
licenseLockRequestedRaised = true;
|
||||
LicenseLockRequested?.Invoke(licenseLockMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginResetButtonFeedback()
|
||||
{
|
||||
isResetButtonPending = true;
|
||||
@@ -1313,6 +1370,17 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
|
||||
}
|
||||
}
|
||||
|
||||
private Task RunLicensedDeviceCommand(Func<Task> command, string successMessage)
|
||||
{
|
||||
if (isLicenseLockPending)
|
||||
{
|
||||
CurrentStatus = "软件使用时效已到,仅允许停止或复位设备。";
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return RunDeviceCommand(command(), successMessage);
|
||||
}
|
||||
|
||||
private void WriteNumericSetting(string value, Func<double, Task> writer, string label)
|
||||
{
|
||||
if (isLoadingDeviceSettings || !TryParseDouble(value, out var numericValue))
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Views.LicenseWindow"
|
||||
Width="560"
|
||||
SizeToContent="Height"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="#F4F7FB"
|
||||
Title="软件使用授权">
|
||||
<Border Margin="18"
|
||||
Padding="20"
|
||||
Background="White"
|
||||
BorderBrush="#DCE4EF"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock x:Name="TitleText"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#172033"/>
|
||||
<TextBlock x:Name="MessageText"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="#67738A"/>
|
||||
|
||||
<StackPanel x:Name="InitializationPanel" Spacing="10" IsVisible="False">
|
||||
<TextBlock Text="首次运行需要管理员权限创建整机授权。设置完成后开始第一阶段计时。"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="#D97706"/>
|
||||
<Grid ColumnDefinitions="170,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"
|
||||
RowSpacing="8" ColumnSpacing="12">
|
||||
<TextBlock Text="管理密码" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="InitialAdminPassword" Grid.Column="1" PasswordChar="●"/>
|
||||
<TextBlock Grid.Row="1" Text="确认管理密码" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="InitialAdminConfirm" Grid.Row="1" Grid.Column="1" PasswordChar="●"/>
|
||||
<TextBlock Grid.Row="2" Text="第一次时效密码" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="InitialFirstPassword" Grid.Row="2" Grid.Column="1" PasswordChar="●"/>
|
||||
<TextBlock Grid.Row="3" Text="确认第一次密码" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="InitialFirstConfirm" Grid.Row="3" Grid.Column="1" PasswordChar="●"/>
|
||||
<TextBlock Grid.Row="4" Text="第二次时效密码" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="InitialSecondPassword" Grid.Row="4" Grid.Column="1" PasswordChar="●"/>
|
||||
<TextBlock Grid.Row="5" Text="确认第二次密码" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="InitialSecondConfirm" Grid.Row="5" Grid.Column="1" PasswordChar="●"/>
|
||||
<TextBlock Grid.Row="6" Text="第一阶段(月)" VerticalAlignment="Center"/>
|
||||
<NumericUpDown x:Name="InitialFirstMonths" Grid.Row="6" Grid.Column="1" Minimum="1" Maximum="120" Value="2"/>
|
||||
<TextBlock Grid.Row="7" Text="第二阶段(月)" VerticalAlignment="Center"/>
|
||||
<NumericUpDown x:Name="InitialSecondMonths" Grid.Row="7" Grid.Column="1" Minimum="1" Maximum="120" Value="3"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="UnlockPanel" Spacing="10" IsVisible="False">
|
||||
<TextBlock Text="时效密码" Foreground="#172033"/>
|
||||
<TextBox x:Name="UnlockPassword" PasswordChar="●"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="AdminLoginPanel" Spacing="10" IsVisible="False">
|
||||
<TextBlock Text="管理密码" Foreground="#172033"/>
|
||||
<TextBox x:Name="AdminLoginPassword" PasswordChar="●"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="AdminSettingsPanel" Spacing="10" IsVisible="False">
|
||||
<TextBlock x:Name="LicenseStatusText"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="#2563EB"/>
|
||||
<TextBlock Text="密码留空表示保持不变。修改时长将按当前阶段开始时间重新计算到期日。"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="#67738A"/>
|
||||
<Grid ColumnDefinitions="170,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"
|
||||
RowSpacing="8" ColumnSpacing="12">
|
||||
<TextBlock Text="新管理密码" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="NewAdminPassword" Grid.Column="1" PasswordChar="●"/>
|
||||
<TextBlock Grid.Row="1" Text="确认新管理密码" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="NewAdminConfirm" Grid.Row="1" Grid.Column="1" PasswordChar="●"/>
|
||||
<TextBlock Grid.Row="2" Text="新第一次时效密码" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="NewFirstPassword" Grid.Row="2" Grid.Column="1" PasswordChar="●"/>
|
||||
<TextBlock Grid.Row="3" Text="确认新第一次密码" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="NewFirstConfirm" Grid.Row="3" Grid.Column="1" PasswordChar="●"/>
|
||||
<TextBlock Grid.Row="4" Text="新第二次时效密码" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="NewSecondPassword" Grid.Row="4" Grid.Column="1" PasswordChar="●"/>
|
||||
<TextBlock Grid.Row="5" Text="确认新第二次密码" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="NewSecondConfirm" Grid.Row="5" Grid.Column="1" PasswordChar="●"/>
|
||||
<TextBlock Grid.Row="6" Text="第一阶段(月)" VerticalAlignment="Center"/>
|
||||
<NumericUpDown x:Name="AdminFirstMonths" Grid.Row="6" Grid.Column="1" Minimum="1" Maximum="120"/>
|
||||
<TextBlock Grid.Row="7" Text="第二阶段(月)" VerticalAlignment="Center"/>
|
||||
<NumericUpDown x:Name="AdminSecondMonths" Grid.Row="7" Grid.Column="1" Minimum="1" Maximum="120"/>
|
||||
</Grid>
|
||||
<Button x:Name="RestartTimingButton"
|
||||
Content="重新开始第一阶段计时"
|
||||
HorizontalAlignment="Left"
|
||||
Background="#FEF3C7"
|
||||
Click="OnRestartTimingClick"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border x:Name="ConfirmationPanel"
|
||||
IsVisible="False"
|
||||
Padding="12"
|
||||
Background="#FEF3C7"
|
||||
BorderBrush="#F59E0B"
|
||||
BorderThickness="1"
|
||||
CornerRadius="6">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="确认重新开始计时?这会取消当前阶段或永久授权,从现在重新计算第一阶段。"
|
||||
TextWrapping="Wrap"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="确认重新计时" Click="OnConfirmRestartClick"/>
|
||||
<Button Content="取消" Click="OnCancelRestartClick"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="ErrorText"
|
||||
Foreground="#DC2626"
|
||||
TextWrapping="Wrap"/>
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Spacing="8">
|
||||
<Button x:Name="CancelButton" Content="退出" Click="OnCancelClick"/>
|
||||
<Button x:Name="PrimaryButton" Content="确定" Click="OnPrimaryClick"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -0,0 +1,214 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models;
|
||||
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services;
|
||||
using Serilog;
|
||||
using System;
|
||||
|
||||
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Views
|
||||
{
|
||||
public enum LicenseWindowMode
|
||||
{
|
||||
Initialization,
|
||||
Unlock,
|
||||
Administration,
|
||||
Blocked
|
||||
}
|
||||
|
||||
public partial class LicenseWindow : Window
|
||||
{
|
||||
private readonly MachineLicenseService licenseService;
|
||||
private readonly LicenseWindowMode mode;
|
||||
private bool adminAuthenticated;
|
||||
|
||||
public bool Succeeded { get; private set; }
|
||||
|
||||
public LicenseWindow() : this(new MachineLicenseService(), LicenseWindowMode.Blocked, "授权窗口需要由软件启动流程打开。")
|
||||
{
|
||||
}
|
||||
|
||||
public LicenseWindow(MachineLicenseService licenseService, LicenseWindowMode mode, string message = "")
|
||||
{
|
||||
this.licenseService = licenseService;
|
||||
this.mode = mode;
|
||||
InitializeComponent();
|
||||
|
||||
MessageText.Text = message;
|
||||
InitializationPanel.IsVisible = mode == LicenseWindowMode.Initialization;
|
||||
UnlockPanel.IsVisible = mode == LicenseWindowMode.Unlock;
|
||||
AdminLoginPanel.IsVisible = mode == LicenseWindowMode.Administration;
|
||||
CancelButton.Content = mode == LicenseWindowMode.Administration ? "关闭" : "退出";
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case LicenseWindowMode.Initialization:
|
||||
TitleText.Text = "首次使用授权设置";
|
||||
PrimaryButton.Content = "保存并开始使用";
|
||||
break;
|
||||
case LicenseWindowMode.Unlock:
|
||||
TitleText.Text = "软件使用时效已到";
|
||||
PrimaryButton.Content = "校验并继续使用";
|
||||
break;
|
||||
case LicenseWindowMode.Administration:
|
||||
TitleText.Text = "时效授权管理";
|
||||
PrimaryButton.Content = "验证管理密码";
|
||||
break;
|
||||
default:
|
||||
TitleText.Text = "软件授权已锁定";
|
||||
PrimaryButton.IsVisible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPrimaryClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
ErrorText.Foreground = Brushes.Crimson;
|
||||
ErrorText.Text = string.Empty;
|
||||
try
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case LicenseWindowMode.Initialization:
|
||||
InitializeLicense();
|
||||
break;
|
||||
case LicenseWindowMode.Unlock:
|
||||
UnlockLicense();
|
||||
break;
|
||||
case LicenseWindowMode.Administration:
|
||||
HandleAdministration();
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "授权窗口操作失败:Mode={Mode}", mode);
|
||||
ErrorText.Text = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeLicense()
|
||||
{
|
||||
RequireMatch(InitialAdminPassword.Text, InitialAdminConfirm.Text, "管理密码");
|
||||
RequireMatch(InitialFirstPassword.Text, InitialFirstConfirm.Text, "第一次时效密码");
|
||||
RequireMatch(InitialSecondPassword.Text, InitialSecondConfirm.Text, "第二次时效密码");
|
||||
licenseService.Initialize(
|
||||
InitialAdminPassword.Text ?? string.Empty,
|
||||
InitialFirstPassword.Text ?? string.Empty,
|
||||
InitialSecondPassword.Text ?? string.Empty,
|
||||
ReadMonths(InitialFirstMonths),
|
||||
ReadMonths(InitialSecondMonths));
|
||||
Succeeded = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void UnlockLicense()
|
||||
{
|
||||
if (!licenseService.UnlockCurrentStage(UnlockPassword.Text ?? string.Empty))
|
||||
{
|
||||
ErrorText.Text = "时效密码不正确。";
|
||||
UnlockPassword.Text = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
Succeeded = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void HandleAdministration()
|
||||
{
|
||||
if (!adminAuthenticated)
|
||||
{
|
||||
if (!licenseService.VerifyAdminPassword(AdminLoginPassword.Text ?? string.Empty))
|
||||
{
|
||||
ErrorText.Text = "管理密码不正确。";
|
||||
AdminLoginPassword.Text = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
adminAuthenticated = true;
|
||||
AdminLoginPanel.IsVisible = false;
|
||||
AdminSettingsPanel.IsVisible = true;
|
||||
PrimaryButton.Content = "保存设置";
|
||||
LoadAdminValues();
|
||||
return;
|
||||
}
|
||||
|
||||
var newAdminPassword = ReadOptionalMatchingPassword(NewAdminPassword.Text, NewAdminConfirm.Text, "新管理密码");
|
||||
var newFirstPassword = ReadOptionalMatchingPassword(NewFirstPassword.Text, NewFirstConfirm.Text, "新第一次时效密码");
|
||||
var newSecondPassword = ReadOptionalMatchingPassword(NewSecondPassword.Text, NewSecondConfirm.Text, "新第二次时效密码");
|
||||
licenseService.UpdateSettings(
|
||||
newAdminPassword,
|
||||
newFirstPassword,
|
||||
newSecondPassword,
|
||||
ReadMonths(AdminFirstMonths),
|
||||
ReadMonths(AdminSecondMonths));
|
||||
LicenseStatusText.Text = licenseService.DescribeCurrent();
|
||||
ErrorText.Foreground = Avalonia.Media.Brushes.ForestGreen;
|
||||
ErrorText.Text = "时效授权设置已保存。";
|
||||
NewAdminPassword.Text = string.Empty;
|
||||
NewAdminConfirm.Text = string.Empty;
|
||||
NewFirstPassword.Text = string.Empty;
|
||||
NewFirstConfirm.Text = string.Empty;
|
||||
NewSecondPassword.Text = string.Empty;
|
||||
NewSecondConfirm.Text = string.Empty;
|
||||
Succeeded = true;
|
||||
}
|
||||
|
||||
private void LoadAdminValues()
|
||||
{
|
||||
var data = licenseService.Current ?? throw new InvalidOperationException("授权状态未加载。");
|
||||
AdminFirstMonths.Value = data.FirstPeriodMonths;
|
||||
AdminSecondMonths.Value = data.SecondPeriodMonths;
|
||||
LicenseStatusText.Text = licenseService.DescribeCurrent();
|
||||
}
|
||||
|
||||
private void OnRestartTimingClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
ConfirmationPanel.IsVisible = true;
|
||||
}
|
||||
|
||||
private void OnConfirmRestartClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
licenseService.RestartTiming();
|
||||
ConfirmationPanel.IsVisible = false;
|
||||
LicenseStatusText.Text = licenseService.DescribeCurrent();
|
||||
ErrorText.Foreground = Avalonia.Media.Brushes.ForestGreen;
|
||||
ErrorText.Text = "已从当前时间重新开始第一阶段计时。";
|
||||
Succeeded = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorText.Text = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCancelRestartClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) =>
|
||||
ConfirmationPanel.IsVisible = false;
|
||||
|
||||
private void OnCancelClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) => Close();
|
||||
|
||||
private static int ReadMonths(NumericUpDown input) =>
|
||||
decimal.ToInt32(input.Value ?? throw new ArgumentException("请输入有效的时效月数。"));
|
||||
|
||||
private static void RequireMatch(string? password, string? confirmation, string label)
|
||||
{
|
||||
if (!string.Equals(password, confirmation, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException($"{label}与确认输入不一致。");
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ReadOptionalMatchingPassword(string? password, string? confirmation, string label)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(password) && string.IsNullOrWhiteSpace(confirmation))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
RequireMatch(password, confirmation, label);
|
||||
return password;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,29 @@
|
||||
using Avalonia.Input;
|
||||
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModels;
|
||||
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services;
|
||||
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models;
|
||||
using SukiUI.Controls;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Views
|
||||
{
|
||||
public partial class MainWindow : SukiWindow
|
||||
{
|
||||
public MainWindow()
|
||||
private readonly MachineLicenseService licenseService;
|
||||
private bool isLicenseWindowOpen;
|
||||
|
||||
public MainWindow() : this(new MachineLicenseService())
|
||||
{
|
||||
}
|
||||
|
||||
public MainWindow(MachineLicenseService licenseService)
|
||||
{
|
||||
this.licenseService = licenseService;
|
||||
InitializeComponent();
|
||||
Closed += OnClosed;
|
||||
KeyDown += OnKeyDown;
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
|
||||
private void OnClosed(object? sender, EventArgs e)
|
||||
@@ -19,5 +33,83 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Views
|
||||
viewModel.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (DataContext is MainWindowViewModel viewModel)
|
||||
{
|
||||
viewModel.LicenseLockRequested -= OnLicenseLockRequested;
|
||||
viewModel.LicenseLockRequested += OnLicenseLockRequested;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.F12
|
||||
&& e.KeyModifiers.HasFlag(KeyModifiers.Control)
|
||||
&& e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
||||
{
|
||||
e.Handled = true;
|
||||
_ = ShowAdministrationAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLicenseLockRequested(string message) => _ = ShowLicenseLockAsync(message);
|
||||
|
||||
private async Task ShowAdministrationAsync()
|
||||
{
|
||||
if (isLicenseWindowOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isLicenseWindowOpen = true;
|
||||
try
|
||||
{
|
||||
var window = new LicenseWindow(licenseService, LicenseWindowMode.Administration, "验证管理密码后可修改两次密码和时效。");
|
||||
await window.ShowDialog(this);
|
||||
if (DataContext is MainWindowViewModel viewModel)
|
||||
{
|
||||
viewModel.RefreshLicenseState();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLicenseWindowOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowLicenseLockAsync(string message)
|
||||
{
|
||||
if (isLicenseWindowOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isLicenseWindowOpen = true;
|
||||
try
|
||||
{
|
||||
var check = licenseService.Check();
|
||||
var mode = check.State == LicenseCheckState.Expired
|
||||
? LicenseWindowMode.Unlock
|
||||
: LicenseWindowMode.Blocked;
|
||||
var window = new LicenseWindow(licenseService, mode, message);
|
||||
await window.ShowDialog(this);
|
||||
if (!window.Succeeded)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (DataContext is MainWindowViewModel viewModel)
|
||||
{
|
||||
viewModel.RefreshLicenseState();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLicenseWindowOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user