feat.PLONDS系统开发

This commit is contained in:
lincube
2026-07-31 11:43:50 +09:00
parent 728a8d4861
commit 557e426dde
39 changed files with 5498 additions and 568 deletions

View File

@@ -1,7 +1,10 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Security;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LanDesktopPLONDS.Installer.Localization;
using LanDesktopPLONDS.Installer.Models;
using LanDesktopPLONDS.Installer.Services;
using LanMountainDesktop.Shared.Contracts.Privacy;
@@ -14,6 +17,11 @@ public sealed partial class MainWindowViewModel : ObservableObject
private readonly IPrivacyDeviceIdentityProvider _privacyIdentity;
private readonly InstallerPrivacyConsentStore _privacyConsentStore;
private CancellationTokenSource? _installCts;
private CancellationTokenSource? _checkCts;
// 下载速度计算状态
private long _lastBytesDownloaded;
private DateTime _lastProgressTime = DateTime.UtcNow;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(NextCommand))]
@@ -58,6 +66,9 @@ public sealed partial class MainWindowViewModel : ObservableObject
[ObservableProperty]
private string _downloadBytesText = string.Empty;
[ObservableProperty]
private string _downloadSpeedText = string.Empty;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(StartInstallCommand))]
[NotifyCanExecuteChangedFor(nameof(BackCommand))]
@@ -70,6 +81,19 @@ public sealed partial class MainWindowViewModel : ObservableObject
[ObservableProperty]
private bool _createStartupShortcut;
// === Task 1: 可取消的版本检查 ===
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(NextCommand))]
[NotifyCanExecuteChangedFor(nameof(BackCommand))]
private bool _isCheckingUpdate;
// === Task 2: 自动提权 ===
[ObservableProperty]
private bool _isElevationRequired;
[ObservableProperty]
private string _elevationMessage = "所选安装路径需要管理员权限。";
public MainWindowViewModel(
IOnlineInstallService installService,
IPrivacyDeviceIdentityProvider privacyIdentity,
@@ -95,7 +119,8 @@ public sealed partial class MainWindowViewModel : ObservableObject
public Func<string, Task<string?>>? BrowseRequested { get; set; }
public string WindowTitle => "LanDesktopPLONDS Installer";
/// <summary>窗口标题,已本地化。</summary>
public string WindowTitle => "阑山桌面 安装程序";
public string DeviceIdPreview { get; }
@@ -111,13 +136,13 @@ public sealed partial class MainWindowViewModel : ObservableObject
public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);
public bool CanGoBack => CurrentStep > InstallerStepId.Welcome && !IsInstalling;
public bool CanGoBack => CurrentStep > InstallerStepId.Welcome && !IsInstalling && !IsCheckingUpdate;
public bool CanGoNext => CurrentStep switch
{
InstallerStepId.Welcome => !IsInstalling,
InstallerStepId.InstallLocation => !string.IsNullOrWhiteSpace(InstallPath) && !IsInstalling,
InstallerStepId.PrivacyConfirm => PrivacyConfirmed && !IsInstalling,
InstallerStepId.Welcome => !IsInstalling && !IsCheckingUpdate,
InstallerStepId.InstallLocation => !string.IsNullOrWhiteSpace(InstallPath) && !IsInstalling && !IsCheckingUpdate,
InstallerStepId.PrivacyConfirm => PrivacyConfirmed && !IsInstalling && !IsCheckingUpdate,
_ => false
};
@@ -167,25 +192,77 @@ public sealed partial class MainWindowViewModel : ObservableObject
OnPropertyChanged(nameof(CanStartInstall));
}
partial void OnIsCheckingUpdateChanged(bool value)
{
_ = value;
OnPropertyChanged(nameof(CanGoBack));
OnPropertyChanged(nameof(CanGoNext));
}
// =====================================================================
// Task 1: 可取消的版本检查 + Task 2: 自动提权
// =====================================================================
[RelayCommand(CanExecute = nameof(CanGoNext))]
private async Task NextAsync()
{
ErrorMessage = null;
IsElevationRequired = false;
if (CurrentStep == InstallerStepId.InstallLocation)
{
try
{
InstallerPathGuard.ValidateInstallPath(InstallPath);
var info = await _installService.CheckLatestAsync(CancellationToken.None);
TargetVersion = info.Version;
SourceId = info.SourceId;
StatusText = $"准备安装 {info.Version}";
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
return;
}
// Task 2: 自动提权检查
if (InstallerElevation.RequiresElevation(InstallPath) && !InstallerElevation.IsRunningElevated())
{
IsElevationRequired = true;
ElevationMessage = $"所选安装路径 {InstallPath} 需要管理员权限才能写入。";
return;
}
// Task 1: 带 CTS 和30秒超时的版本检查
_checkCts?.Dispose();
_checkCts = new CancellationTokenSource();
IsCheckingUpdate = true;
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
timeoutCts.Token, _checkCts.Token);
var info = await _installService.CheckLatestAsync(linkedCts.Token);
TargetVersion = info.Version;
SourceId = info.SourceId;
StatusText = $"准备安装 {info.Version}";
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
ErrorMessage = "检查更新超时30秒请检查网络连接后重试。";
return;
}
catch (OperationCanceledException)
{
// 用户主动取消
ErrorMessage = "版本检查已取消。";
return;
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
return;
}
finally
{
timeoutCts.Dispose();
IsCheckingUpdate = false;
}
}
else if (CurrentStep == InstallerStepId.PrivacyConfirm)
{
@@ -243,7 +320,49 @@ public sealed partial class MainWindowViewModel : ObservableObject
}
}
[RelayCommand(CanExecute = nameof(CanStartInstall))]
// =====================================================================
// Task 1: 取消版本检查
// =====================================================================
[RelayCommand]
private void CancelCheck()
{
_checkCts?.Cancel();
}
// =====================================================================
// Task 2: 自动提权 — 以管理员身份重新启动
// =====================================================================
[RelayCommand]
private void RelaunchElevated()
{
try
{
var exePath = Environment.ProcessPath ?? System.Reflection.Assembly.GetExecutingAssembly().Location;
var args = $"--install-path \"{InstallPath}\"";
Process.Start(new ProcessStartInfo
{
FileName = exePath,
Arguments = args,
UseShellExecute = true,
Verb = "runas"
});
Environment.Exit(0);
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
// 用户在 UAC 对话框中点击了"否"(拒绝提权)
ErrorMessage = "已拒绝管理员权限请求。请选择一个不需要管理员权限的安装路径,或手动以管理员身份运行安装程序。";
IsElevationRequired = false;
}
catch (Exception ex)
{
ErrorMessage = $"重新启动失败:{ex.Message}";
}
}
[RelayCommand]
private async Task StartInstallAsync()
{
ErrorMessage = null;
@@ -251,6 +370,12 @@ public sealed partial class MainWindowViewModel : ObservableObject
StartInstallCommand.NotifyCanExecuteChanged();
_installCts?.Dispose();
_installCts = new CancellationTokenSource();
// 重置下载速度状态
_lastBytesDownloaded = 0;
_lastProgressTime = DateTime.UtcNow;
DownloadSpeedText = string.Empty;
try
{
var progress = new Progress<InstallerDeployProgress>(ApplyProgress);
@@ -325,14 +450,29 @@ public sealed partial class MainWindowViewModel : ObservableObject
CurrentStep = step;
}
// =====================================================================
// Task 4: 本地化进度阶段 + Task 5: 下载速度显示
// =====================================================================
private void ApplyProgress(InstallerDeployProgress progress)
{
StatusText = progress.Stage;
// Task 4: 将英文阶段键翻译为中文
StatusText = InstallerStrings.TranslateStage(progress.Stage);
TargetVersion = progress.TargetVersion ?? TargetVersion;
DownloadProgress = progress.DownloadProgress;
InstallProgress = progress.InstallProgress;
CurrentFile = progress.CurrentFile;
DownloadBytesText = FormatBytes(progress.BytesDownloaded, progress.TotalBytes);
// Task 5: 计算下载速度
var now = DateTime.UtcNow;
var elapsed = (now - _lastProgressTime).TotalSeconds;
if (elapsed > 0.5 && progress.BytesDownloaded > _lastBytesDownloaded && progress.BytesDownloaded > 0)
{
var speedBytesPerSec = (progress.BytesDownloaded - _lastBytesDownloaded) / elapsed;
DownloadSpeedText = $"{ToSize((long)speedBytesPerSec)}/s";
_lastBytesDownloaded = progress.BytesDownloaded;
_lastProgressTime = now;
}
}
private void SyncSteps()
@@ -368,4 +508,31 @@ public sealed partial class MainWindowViewModel : ObservableObject
return $"{size:0.##} {suffixes[suffix]}";
}
// =====================================================================
// Task 2: 解析 --install-path 命令行参数
// =====================================================================
/// <summary>
/// 从命令行参数中解析 --install-path 值。
/// 由 App.axaml.cs 在启动时调用。
/// </summary>
public static string? ParseInstallPath(string[]? args)
{
if (args is null || args.Length == 0)
{
return null;
}
for (var i = 0; i < args.Length - 1; i++)
{
if (string.Equals(args[i], "--install-path", StringComparison.OrdinalIgnoreCase))
{
var value = args[i + 1].Trim('"');
return string.IsNullOrWhiteSpace(value) ? null : value;
}
}
return null;
}
}