更新时效密码
This commit is contained in:
@@ -19,14 +19,30 @@ public partial class App : Application
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IStandardRepository, StandardRepository>();
|
||||
services.AddSingleton<IPasswordAccessService, PasswordAccessService>();
|
||||
services.AddSingleton<IModbusTelemetryService, ModbusTelemetryService>();
|
||||
services.AddSingleton<IRs485PumpFlowService, Rs485PumpFlowService>();
|
||||
services.AddSingleton<IPumpActuationService, Rs485PumpActuationService>();
|
||||
services.AddTransient<StartupPasswordViewModel>();
|
||||
services.AddTransient<StartupPasswordWindow>();
|
||||
services.AddSingleton<MainViewModel>();
|
||||
services.AddSingleton<MainWindow>();
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
var passwordAccessService = _serviceProvider.GetRequiredService<IPasswordAccessService>();
|
||||
var passwordStatus = passwordAccessService.GetStatus();
|
||||
if (passwordStatus.ShouldPromptAtStartup)
|
||||
{
|
||||
var passwordWindow = _serviceProvider.GetRequiredService<StartupPasswordWindow>();
|
||||
var passwordResult = passwordWindow.ShowDialog();
|
||||
if (passwordResult != true || !passwordAccessService.GetStatus().CanLaunch)
|
||||
{
|
||||
Shutdown();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
||||
mainWindow.Show();
|
||||
}
|
||||
|
||||
16
Cardiopulmonarybypasssystems/Models/PasswordAccessStatus.cs
Normal file
16
Cardiopulmonarybypasssystems/Models/PasswordAccessStatus.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Cardiopulmonarybypasssystems.Models;
|
||||
|
||||
public sealed class PasswordAccessStatus
|
||||
{
|
||||
public int Stage { get; init; }
|
||||
public bool CanLaunch { get; init; }
|
||||
public bool IsPermanent { get; init; }
|
||||
public bool ShouldPromptAtStartup { get; init; }
|
||||
public bool CanSubmitPassword { get; init; }
|
||||
public bool RequiresPassword => !CanLaunch;
|
||||
public string StageText { get; init; } = string.Empty;
|
||||
public string StatusTitle { get; init; } = string.Empty;
|
||||
public string StatusDetail { get; init; } = string.Empty;
|
||||
public int RemainingDays { get; init; }
|
||||
public DateTime? ExpiresAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Cardiopulmonarybypasssystems.Models;
|
||||
|
||||
namespace Cardiopulmonarybypasssystems.Services;
|
||||
|
||||
public interface IPasswordAccessService
|
||||
{
|
||||
PasswordAccessStatus GetStatus();
|
||||
bool TryUnlock(string password, out PasswordAccessStatus status, out string message);
|
||||
}
|
||||
228
Cardiopulmonarybypasssystems/Services/PasswordAccessService.cs
Normal file
228
Cardiopulmonarybypasssystems/Services/PasswordAccessService.cs
Normal file
@@ -0,0 +1,228 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using Cardiopulmonarybypasssystems.Models;
|
||||
|
||||
namespace Cardiopulmonarybypasssystems.Services;
|
||||
|
||||
public sealed class PasswordAccessService : IPasswordAccessService
|
||||
{
|
||||
private const int ExpirationReminderDays = 3;
|
||||
private const string SettingsDirectoryName = "Cardiopulmonarybypasssystems";
|
||||
private const string StatusFileName = "password-access.json";
|
||||
public const string FirstStagePassword = "152026";
|
||||
public const string SecondStagePassword = "302026";
|
||||
private const int FirstStageDays = 15;
|
||||
private const int SecondStageDays = 30;
|
||||
|
||||
private static readonly string StatusFilePath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
SettingsDirectoryName,
|
||||
StatusFileName);
|
||||
|
||||
public PasswordAccessStatus GetStatus()
|
||||
{
|
||||
var state = LoadState();
|
||||
return BuildStatus(state, persistState: true);
|
||||
}
|
||||
|
||||
public bool TryUnlock(string password, out PasswordAccessStatus status, out string message)
|
||||
{
|
||||
var normalizedPassword = password?.Trim() ?? string.Empty;
|
||||
var state = LoadState();
|
||||
var currentStatus = BuildStatus(state, persistState: true);
|
||||
|
||||
if (currentStatus.IsPermanent)
|
||||
{
|
||||
status = currentStatus;
|
||||
message = "当前版本已永久有效,不需要再输入密码。";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentStatus.Stage == 1 && currentStatus.CanLaunch)
|
||||
{
|
||||
status = currentStatus;
|
||||
message = "第 1 阶段仍在有效期内,无需再次输入密码。";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentStatus.Stage == 2 && currentStatus.CanLaunch)
|
||||
{
|
||||
status = currentStatus;
|
||||
message = "第 2 阶段仍在有效期内,无需再次输入密码。";
|
||||
return true;
|
||||
}
|
||||
|
||||
var nextStage = currentStatus.Stage switch
|
||||
{
|
||||
<= 0 => 1,
|
||||
1 => 2,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
if (nextStage == 0)
|
||||
{
|
||||
status = BuildStatus(state, persistState: true);
|
||||
message = "当前版本已转为永久有效,不再需要密码。";
|
||||
return true;
|
||||
}
|
||||
|
||||
var expectedPassword = nextStage == 1 ? FirstStagePassword : SecondStagePassword;
|
||||
if (!string.Equals(normalizedPassword, expectedPassword, StringComparison.Ordinal))
|
||||
{
|
||||
status = currentStatus;
|
||||
message = $"密码错误,请输入第 {nextStage} 次时效密码。";
|
||||
return false;
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
var expiresAt = now.AddDays(nextStage == 1 ? FirstStageDays : SecondStageDays);
|
||||
var nextState = new PasswordAccessState
|
||||
{
|
||||
Stage = nextStage,
|
||||
ActivatedAt = now,
|
||||
ExpiresAt = expiresAt
|
||||
};
|
||||
|
||||
SaveState(nextState);
|
||||
status = BuildStatus(nextState, persistState: false);
|
||||
message = nextStage == 1
|
||||
? $"第 1 次时效已生效,有效期至 {expiresAt:yyyy-MM-dd HH:mm}。"
|
||||
: $"第 2 次时效已生效,有效期至 {expiresAt:yyyy-MM-dd HH:mm};到期后自动转为永久有效。";
|
||||
return true;
|
||||
}
|
||||
|
||||
private static PasswordAccessStatus BuildStatus(PasswordAccessState? state, bool persistState)
|
||||
{
|
||||
if (state is null || state.Stage <= 0)
|
||||
{
|
||||
return new PasswordAccessStatus
|
||||
{
|
||||
Stage = 0,
|
||||
CanLaunch = false,
|
||||
IsPermanent = false,
|
||||
ShouldPromptAtStartup = true,
|
||||
CanSubmitPassword = true,
|
||||
StageText = "当前阶段:未开始",
|
||||
StatusTitle = "需要输入第 1 次时效密码",
|
||||
StatusDetail = $"输入 15 天密码后进入系统。",
|
||||
RemainingDays = 0
|
||||
};
|
||||
}
|
||||
|
||||
if (state.Stage >= 3)
|
||||
{
|
||||
return new PasswordAccessStatus
|
||||
{
|
||||
Stage = 3,
|
||||
CanLaunch = true,
|
||||
IsPermanent = true,
|
||||
ShouldPromptAtStartup = false,
|
||||
CanSubmitPassword = false,
|
||||
StageText = "当前阶段:永久有效",
|
||||
StatusTitle = "系统已永久有效",
|
||||
StatusDetail = "当前可直接进入系统。",
|
||||
RemainingDays = int.MaxValue
|
||||
};
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
if (state.ExpiresAt > now)
|
||||
{
|
||||
var remainingDays = Math.Max(1, (int)Math.Ceiling((state.ExpiresAt.Value - now).TotalDays));
|
||||
return new PasswordAccessStatus
|
||||
{
|
||||
Stage = state.Stage,
|
||||
CanLaunch = true,
|
||||
IsPermanent = false,
|
||||
ShouldPromptAtStartup = state.Stage == 1 && remainingDays <= ExpirationReminderDays,
|
||||
CanSubmitPassword = state.Stage == 1 && remainingDays <= ExpirationReminderDays,
|
||||
StageText = state.Stage == 1 ? "当前阶段:第 1 次时效(15 天)" : "当前阶段:第 2 次时效(30 天)",
|
||||
StatusTitle = state.Stage == 1 && remainingDays <= ExpirationReminderDays ? "第 1 次时效即将到期" : "时效有效",
|
||||
StatusDetail = state.Stage == 1 && remainingDays <= ExpirationReminderDays
|
||||
? $"剩余 {remainingDays} 天,可提前输入 30 天密码。"
|
||||
: $"当前可直接进入系统。",
|
||||
RemainingDays = remainingDays,
|
||||
ExpiresAt = state.ExpiresAt
|
||||
};
|
||||
}
|
||||
|
||||
if (state.Stage == 1)
|
||||
{
|
||||
return new PasswordAccessStatus
|
||||
{
|
||||
Stage = 1,
|
||||
CanLaunch = false,
|
||||
IsPermanent = false,
|
||||
ShouldPromptAtStartup = true,
|
||||
CanSubmitPassword = true,
|
||||
StageText = "当前阶段:第 1 次时效已到期",
|
||||
StatusTitle = "需要输入第 2 次时效密码",
|
||||
StatusDetail = $"输入 30 天密码后继续使用。",
|
||||
RemainingDays = 0,
|
||||
ExpiresAt = state.ExpiresAt
|
||||
};
|
||||
}
|
||||
|
||||
var permanentState = new PasswordAccessState
|
||||
{
|
||||
Stage = 3,
|
||||
ActivatedAt = state.ActivatedAt,
|
||||
ExpiresAt = null
|
||||
};
|
||||
|
||||
if (persistState)
|
||||
{
|
||||
SaveState(permanentState);
|
||||
}
|
||||
|
||||
return new PasswordAccessStatus
|
||||
{
|
||||
Stage = 3,
|
||||
CanLaunch = true,
|
||||
IsPermanent = true,
|
||||
ShouldPromptAtStartup = false,
|
||||
CanSubmitPassword = false,
|
||||
StageText = "当前阶段:永久有效",
|
||||
StatusTitle = "系统已永久有效",
|
||||
StatusDetail = "当前可直接进入系统。",
|
||||
RemainingDays = int.MaxValue
|
||||
};
|
||||
}
|
||||
|
||||
private static PasswordAccessState? LoadState()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(StatusFilePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(StatusFilePath);
|
||||
return JsonSerializer.Deserialize<PasswordAccessState>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveState(PasswordAccessState state)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(StatusFilePath);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(state, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(StatusFilePath, json);
|
||||
}
|
||||
|
||||
private sealed class PasswordAccessState
|
||||
{
|
||||
public int Stage { get; set; }
|
||||
public DateTime ActivatedAt { get; set; }
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
}
|
||||
}
|
||||
137
Cardiopulmonarybypasssystems/StartupPasswordWindow.xaml
Normal file
137
Cardiopulmonarybypasssystems/StartupPasswordWindow.xaml
Normal file
@@ -0,0 +1,137 @@
|
||||
<Window x:Class="Cardiopulmonarybypasssystems.StartupPasswordWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="系统进入验证"
|
||||
Width="640"
|
||||
Height="500"
|
||||
Loaded="Window_Loaded"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="NoResize">
|
||||
<Grid Margin="18">
|
||||
<Border CornerRadius="22"
|
||||
Background="#FFF7FAFB"
|
||||
BorderBrush="#FFD7E2E6"
|
||||
BorderThickness="1">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="126" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0"
|
||||
CornerRadius="22,22,0,0"
|
||||
Padding="24,20">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0"
|
||||
EndPoint="1,1">
|
||||
<GradientStop Color="#FF123744"
|
||||
Offset="0" />
|
||||
<GradientStop Color="#FF0F5F75"
|
||||
Offset="0.55" />
|
||||
<GradientStop Color="#FF2F7D7A"
|
||||
Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock FontSize="26"
|
||||
FontWeight="Bold"
|
||||
Foreground="White"
|
||||
Text="系统进入验证" />
|
||||
<Border Margin="0,10,0,0"
|
||||
Padding="10,6"
|
||||
HorizontalAlignment="Left"
|
||||
Background="#1FFFFFFF"
|
||||
CornerRadius="12">
|
||||
<TextBlock FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="White"
|
||||
Text="{Binding StageText}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Margin="24,16,24,14">
|
||||
<Border
|
||||
Padding="14,10"
|
||||
Background="#FFEAF3F5"
|
||||
CornerRadius="14">
|
||||
<DockPanel LastChildFill="False">
|
||||
<StackPanel DockPanel.Dock="Left"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock FontSize="12"
|
||||
Foreground="#FF52636B"
|
||||
Text="到期:" />
|
||||
<TextBlock FontSize="13"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource HeaderBrush}"
|
||||
Text="{Binding ExpiresAtText}" />
|
||||
</StackPanel>
|
||||
<StackPanel DockPanel.Dock="Right"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock FontSize="12"
|
||||
Foreground="#FF52636B"
|
||||
Text="剩余:" />
|
||||
<TextBlock FontSize="13"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource HeaderBrush}"
|
||||
Text="{Binding RemainingText}" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<Border Margin="0,16,0,0"
|
||||
Padding="16"
|
||||
Background="White"
|
||||
BorderBrush="#FFDCE7EA"
|
||||
BorderThickness="1"
|
||||
CornerRadius="16">
|
||||
<StackPanel>
|
||||
<TextBlock FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource HeaderBrush}"
|
||||
Text="密码输入" />
|
||||
<TextBox x:Name="PasswordInput"
|
||||
Margin="0,10,0,0"
|
||||
MinHeight="38"
|
||||
FontSize="15"
|
||||
Text="{Binding PasswordText, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextChanged="PasswordInput_OnTextChanged" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Row="2"
|
||||
Padding="18,14"
|
||||
Background="White"
|
||||
BorderBrush="#FFE2EBEE"
|
||||
BorderThickness="1,1,0,0"
|
||||
CornerRadius="0,0,22,22">
|
||||
<UniformGrid Columns="3">
|
||||
<Button Width="112"
|
||||
Margin="0,0,10,0"
|
||||
Command="{Binding CloseCommand}"
|
||||
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
Background="#FF667E88"
|
||||
Content="退出" />
|
||||
<Button Width="112"
|
||||
Margin="0,0,10,0"
|
||||
Command="{Binding ContinueCommand}"
|
||||
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
Background="#FF2B8F6A"
|
||||
Content="继续进入"
|
||||
IsEnabled="{Binding CanContinue}" />
|
||||
<Button Width="112"
|
||||
Margin="0,0,10,0"
|
||||
Command="{Binding SubmitCommand}"
|
||||
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
Background="#FF0F5F75"
|
||||
Content="提交密码"
|
||||
IsEnabled="{Binding CanSubmit}" />
|
||||
</UniformGrid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
29
Cardiopulmonarybypasssystems/StartupPasswordWindow.xaml.cs
Normal file
29
Cardiopulmonarybypasssystems/StartupPasswordWindow.xaml.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Cardiopulmonarybypasssystems.ViewModels;
|
||||
|
||||
namespace Cardiopulmonarybypasssystems;
|
||||
|
||||
public partial class StartupPasswordWindow : Window
|
||||
{
|
||||
private readonly StartupPasswordViewModel _viewModel;
|
||||
|
||||
public StartupPasswordWindow(StartupPasswordViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
_viewModel = viewModel;
|
||||
DataContext = viewModel;
|
||||
}
|
||||
|
||||
private void Window_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
PasswordInput.Focus();
|
||||
Keyboard.Focus(PasswordInput);
|
||||
PasswordInput.SelectAll();
|
||||
}
|
||||
|
||||
private void PasswordInput_OnTextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||
{
|
||||
_viewModel.PasswordText = PasswordInput.Text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using Cardiopulmonarybypasssystems.Models;
|
||||
using Cardiopulmonarybypasssystems.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace Cardiopulmonarybypasssystems.ViewModels;
|
||||
|
||||
public partial class StartupPasswordViewModel : ObservableObject
|
||||
{
|
||||
private readonly IPasswordAccessService _passwordAccessService;
|
||||
|
||||
public StartupPasswordViewModel(IPasswordAccessService passwordAccessService)
|
||||
{
|
||||
_passwordAccessService = passwordAccessService;
|
||||
ProductName = "心肺转流检测系统";
|
||||
SoftwareVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "1.0.0";
|
||||
RefreshStatus();
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private string productName = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string softwareVersion = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string passwordText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string statusTitle = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string statusDetail = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string stageText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string expiresAtText = "--";
|
||||
|
||||
[ObservableProperty]
|
||||
private string remainingText = "--";
|
||||
|
||||
[ObservableProperty]
|
||||
private string footerHint = "规则:第 1 个密码 15 天,第 2 个密码 30 天,第二次到期后自动永久有效。";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool canSubmit;
|
||||
|
||||
public bool CanContinue => _passwordAccessService.GetStatus().CanLaunch;
|
||||
|
||||
[RelayCommand]
|
||||
private void Submit(Window? window)
|
||||
{
|
||||
if (!_passwordAccessService.TryUnlock(PasswordText, out _, out var message))
|
||||
{
|
||||
RefreshStatus();
|
||||
StatusDetail = message;
|
||||
return;
|
||||
}
|
||||
|
||||
PasswordText = string.Empty;
|
||||
RefreshStatus();
|
||||
StatusDetail = message;
|
||||
|
||||
if (_passwordAccessService.GetStatus().CanLaunch && window is not null)
|
||||
{
|
||||
window.DialogResult = true;
|
||||
window.Close();
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Continue(Window? window)
|
||||
{
|
||||
if (!_passwordAccessService.GetStatus().CanLaunch || window is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
window.DialogResult = true;
|
||||
window.Close();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Close(Window? window)
|
||||
{
|
||||
window?.Close();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AppendPasswordCharacter(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PasswordText += value;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void BackspacePassword()
|
||||
{
|
||||
if (string.IsNullOrEmpty(PasswordText))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PasswordText = PasswordText[..^1];
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearPassword()
|
||||
{
|
||||
PasswordText = string.Empty;
|
||||
}
|
||||
|
||||
private void RefreshStatus()
|
||||
{
|
||||
var status = _passwordAccessService.GetStatus();
|
||||
StatusTitle = status.StatusTitle;
|
||||
StatusDetail = status.StatusDetail;
|
||||
StageText = status.StageText;
|
||||
ExpiresAtText = status.ExpiresAt?.ToString("yyyy-MM-dd HH:mm") ?? (status.IsPermanent ? "永久有效" : "--");
|
||||
RemainingText = status.IsPermanent ? "永久" : status.RemainingDays > 0 ? $"{status.RemainingDays} 天" : "需输入密码";
|
||||
CanSubmit = status.CanSubmitPassword;
|
||||
OnPropertyChanged(nameof(CanContinue));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user