diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..aa6608e --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,22 @@ + + + + + + + + 1.1.0 + 1.1.0.0 + 1.1.0.0 + 1.1.0 + + + Telegram Panel Contributors + Telegram Panel + Telegram Panel + Copyright (c) 2024-2025 Telegram Panel Contributors + Telegram Multi-Account Management Panel + https://github.com/moeacgx/Telegram-Panel + git + + diff --git a/src/TelegramPanel.Core/Services/AccountManagementService.cs b/src/TelegramPanel.Core/Services/AccountManagementService.cs index f72c287..1f22983 100644 --- a/src/TelegramPanel.Core/Services/AccountManagementService.cs +++ b/src/TelegramPanel.Core/Services/AccountManagementService.cs @@ -234,7 +234,9 @@ public class AccountManagementService if (account == null) return; - account.Phone = PhoneNumberFormatter.FormatWithCountryCode(account.Phone); + // 只设置 DisplayPhone(非持久化字段),不修改 Phone(持久化字段) + // 这样可以避免 EF Core 追踪 Phone 的变化导致 UNIQUE 约束冲突 + account.DisplayPhone = PhoneNumberFormatter.FormatWithCountryCode(account.Phone); } private static string? TryFindRepoRoot() diff --git a/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs b/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs index 49032f2..7dbe927 100644 --- a/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs +++ b/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs @@ -214,7 +214,8 @@ public class AccountImportService /// public async Task> ImportFromZipAsync( IBrowserFile zipFile, - int? categoryId = null) + int? categoryId = null, + string? twoFactorPassword = null) { var results = new List(); @@ -251,7 +252,7 @@ public class AccountImportService var importedPhones = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var jsonPath in jsonFiles) { - var result = await ImportFromPackageEntryAsync(jsonPath, categoryId); + var result = await ImportFromPackageEntryAsync(jsonPath, categoryId, twoFactorPassword); if (result.Phone != null && !importedPhones.Add(result.Phone)) { results.Add(new ImportResult(false, result.Phone, result.UserId, result.Username, result.SessionPath, "重复账号已跳过")); @@ -331,7 +332,7 @@ public class AccountImportService } } - private async Task ImportFromPackageEntryAsync(string jsonPath, int? categoryId) + private async Task ImportFromPackageEntryAsync(string jsonPath, int? categoryId, string? twoFactorPassword) { try { @@ -375,6 +376,10 @@ public class AccountImportService return new ImportResult(false, phone, userId, username, null, "未找到对应的 .session 文件"); } + // 尝试从 2fa.txt 读取二级密码(优先于用户输入的统一密码) + var twoFaFromFile = await TryRead2faFileAsync(dir); + var effectiveTwoFactorPassword = !string.IsNullOrWhiteSpace(twoFaFromFile) ? twoFaFromFile : twoFactorPassword; + var sessionsPath = _configuration["Telegram:SessionsPath"] ?? "sessions"; Directory.CreateDirectory(sessionsPath); var targetSessionPath = Path.Combine(sessionsPath, $"{phone}.session"); @@ -432,6 +437,9 @@ public class AccountImportService existing.ApiHash = apiHash.Trim(); existing.IsActive = true; existing.LastSyncAt = DateTime.UtcNow; + // 仅在提供了二级密码时更新,避免覆盖已有密码 + if (!string.IsNullOrWhiteSpace(effectiveTwoFactorPassword)) + existing.TwoFactorPassword = effectiveTwoFactorPassword.Trim(); await _accountManagement.UpdateAccountAsync(existing); } else @@ -447,6 +455,7 @@ public class AccountImportService ApiHash = apiHash.Trim(), IsActive = true, CategoryId = categoryId, + TwoFactorPassword = string.IsNullOrWhiteSpace(effectiveTwoFactorPassword) ? null : effectiveTwoFactorPassword.Trim(), CreatedAt = DateTime.UtcNow, LastSyncAt = DateTime.UtcNow }; @@ -465,6 +474,35 @@ public class AccountImportService string extractDirFallback() => Path.GetTempPath(); } + /// + /// 尝试从目录中读取 2fa.txt 文件内容作为二级密码 + /// + private static async Task TryRead2faFileAsync(string directory) + { + try + { + // 支持多种常见文件名 + var possibleNames = new[] { "2fa.txt", "2FA.txt", "2fa", "2FA", "twofa.txt", "password.txt" }; + foreach (var name in possibleNames) + { + var filePath = Path.Combine(directory, name); + if (!File.Exists(filePath)) + continue; + + var content = await File.ReadAllTextAsync(filePath); + var password = content?.Trim(); + if (!string.IsNullOrWhiteSpace(password)) + return password; + } + } + catch + { + // 读取失败不影响导入流程 + } + + return null; + } + private static string? BuildNickname(string? firstName, string? lastName, string? username) { firstName = string.IsNullOrWhiteSpace(firstName) ? null : firstName.Trim(); diff --git a/src/TelegramPanel.Data/Entities/Account.cs b/src/TelegramPanel.Data/Entities/Account.cs index bbc2043..80b0691 100644 --- a/src/TelegramPanel.Data/Entities/Account.cs +++ b/src/TelegramPanel.Data/Entities/Account.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations.Schema; + namespace TelegramPanel.Data.Entities; /// @@ -7,6 +9,20 @@ public class Account { public int Id { get; set; } public string Phone { get; set; } = null!; + + private string? _displayPhone; + + /// + /// 格式化后的手机号(用于 UI 展示,不持久化),格式如:+86 13800138000 + /// 如果未通过 AccountManagementService 设置,则回退到 Phone 的值 + /// + [NotMapped] + public string DisplayPhone + { + get => _displayPhone ?? Phone; + set => _displayPhone = value; + } + public long UserId { get; set; } /// /// 账号昵称(Telegram 显示名称) @@ -46,6 +62,11 @@ public class Account /// public DateTime? TelegramStatusCheckedAtUtc { get; set; } + /// + /// 二级密码(两步验证密码),用于系统保存以便后续修改 + /// + public string? TwoFactorPassword { get; set; } + // 导航属性 public AccountCategory? Category { get; set; } public ICollection Channels { get; set; } = new List(); diff --git a/src/TelegramPanel.Data/Migrations/20251226000000_AddTwoFactorPassword.cs b/src/TelegramPanel.Data/Migrations/20251226000000_AddTwoFactorPassword.cs new file mode 100644 index 0000000..5ed803e --- /dev/null +++ b/src/TelegramPanel.Data/Migrations/20251226000000_AddTwoFactorPassword.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TelegramPanel.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20251226000000_AddTwoFactorPassword")] + /// + public partial class AddTwoFactorPassword : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "TwoFactorPassword", + table: "Accounts", + type: "TEXT", + maxLength: 200, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "TwoFactorPassword", + table: "Accounts"); + } + } +} diff --git a/src/TelegramPanel.Data/Migrations/AppDbContextModelSnapshot.cs b/src/TelegramPanel.Data/Migrations/AppDbContextModelSnapshot.cs index f24afea..6ad7565 100644 --- a/src/TelegramPanel.Data/Migrations/AppDbContextModelSnapshot.cs +++ b/src/TelegramPanel.Data/Migrations/AppDbContextModelSnapshot.cs @@ -72,6 +72,10 @@ namespace TelegramPanel.Data.Migrations b.Property("TelegramStatusSummary") .HasColumnType("TEXT"); + b.Property("TwoFactorPassword") + .HasMaxLength(200) + .HasColumnType("TEXT"); + b.Property("UserId") .HasColumnType("INTEGER"); diff --git a/src/TelegramPanel.Web/Components/Dialogs/AccountDetailsDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/AccountDetailsDialog.razor new file mode 100644 index 0000000..0ca3e70 --- /dev/null +++ b/src/TelegramPanel.Web/Components/Dialogs/AccountDetailsDialog.razor @@ -0,0 +1,132 @@ +@inject AccountManagementService AccountManagement +@inject PanelTimeZoneService TimeZone +@inject ISnackbar Snackbar + + + + + + 账号详情 + + + + @if (account == null) + { + + } + else + { + + + + + + + 二级密码 + + @if (isEditingPassword) + { + + + + 取消 + + @if (saving) + { + + } + 保存 + + + } + else + { + + + + + } + + } + + + 关闭 + + + +@code { + [CascadingParameter] + private MudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] + public int AccountId { get; set; } + + private TelegramPanel.Data.Entities.Account? account; + private bool isEditingPassword; + private string newPassword = ""; + private bool showPassword; + private bool saving; + + protected override async Task OnInitializedAsync() + { + account = await AccountManagement.GetAccountAsync(AccountId); + } + + private string FormatCreatedAt() + { + return account == null ? "-" : TimeZone.Format(account.CreatedAt, "yyyy-MM-dd HH:mm"); + } + + private string DisplayPassword() + { + if (account == null) + return "-"; + return string.IsNullOrEmpty(account.TwoFactorPassword) ? "(未保存)" : account.TwoFactorPassword; + } + + private void StartEdit() + { + newPassword = account?.TwoFactorPassword ?? ""; + isEditingPassword = true; + } + + private void CancelEdit() + { + isEditingPassword = false; + newPassword = ""; + } + + private async Task SavePassword() + { + if (account == null) + return; + + saving = true; + try + { + account.TwoFactorPassword = string.IsNullOrWhiteSpace(newPassword) ? null : newPassword.Trim(); + await AccountManagement.UpdateAccountAsync(account); + isEditingPassword = false; + Snackbar.Add("二级密码已保存", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"保存失败:{ex.Message}", Severity.Error); + } + finally + { + saving = false; + } + } + + private void Close() => MudDialog.Close(); +} diff --git a/src/TelegramPanel.Web/Components/Dialogs/BatchChangeBioDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/BatchChangeBioDialog.razor index 03e9099..ce1b2e9 100644 --- a/src/TelegramPanel.Web/Components/Dialogs/BatchChangeBioDialog.razor +++ b/src/TelegramPanel.Web/Components/Dialogs/BatchChangeBioDialog.razor @@ -160,7 +160,7 @@ processed++; if (ok) success++; else failed++; - results.Add(new ResultRow(a.Phone, ok, err)); + results.Add(new ResultRow(a.DisplayPhone, ok, err)); StateHasChanged(); } diff --git a/src/TelegramPanel.Web/Components/Dialogs/BatchChangeNicknameDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/BatchChangeNicknameDialog.razor index 2ea8e02..947a127 100644 --- a/src/TelegramPanel.Web/Components/Dialogs/BatchChangeNicknameDialog.razor +++ b/src/TelegramPanel.Web/Components/Dialogs/BatchChangeNicknameDialog.razor @@ -197,7 +197,7 @@ processed++; if (ok) success++; else failed++; - results.Add(new ResultRow(a.Phone, newNickname, ok, err)); + results.Add(new ResultRow(a.DisplayPhone, newNickname, ok, err)); if (ok) { diff --git a/src/TelegramPanel.Web/Components/Dialogs/BatchChangeUsernameDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/BatchChangeUsernameDialog.razor index 6f1a16d..1e93b99 100644 --- a/src/TelegramPanel.Web/Components/Dialogs/BatchChangeUsernameDialog.razor +++ b/src/TelegramPanel.Web/Components/Dialogs/BatchChangeUsernameDialog.razor @@ -188,7 +188,7 @@ { processed++; failed++; - results.Add(new ResultRow(a.Phone, "", false, genErr ?? "生成用户名失败")); + results.Add(new ResultRow(a.DisplayPhone, "", false, genErr ?? "生成用户名失败")); StateHasChanged(); continue; } @@ -200,7 +200,7 @@ processed++; if (ok) success++; else failed++; - results.Add(new ResultRow(a.Phone, newUsername, ok, err)); + results.Add(new ResultRow(a.DisplayPhone, newUsername, ok, err)); if (ok) { diff --git a/src/TelegramPanel.Web/Components/Dialogs/BatchChatMembershipDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/BatchChatMembershipDialog.razor index ac42ef9..74576e0 100644 --- a/src/TelegramPanel.Web/Components/Dialogs/BatchChatMembershipDialog.razor +++ b/src/TelegramPanel.Web/Components/Dialogs/BatchChatMembershipDialog.razor @@ -150,7 +150,7 @@ { _cts.Token.ThrowIfCancellationRequested(); - currentHint = $"{a.Phone} => {link}"; + currentHint = $"{a.DisplayPhone} => {link}"; StateHasChanged(); (bool Success, string? Error, string? Title) result; @@ -163,7 +163,7 @@ if (!result.Success) { failed++; - failures.Add($"{a.Phone} => {link}:{result.Error}"); + failures.Add($"{a.DisplayPhone} => {link}:{result.Error}"); } StateHasChanged(); diff --git a/src/TelegramPanel.Web/Components/Dialogs/ChangeTwoFactorPasswordDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/ChangeTwoFactorPasswordDialog.razor index 577cbe8..d51c040 100644 --- a/src/TelegramPanel.Web/Components/Dialogs/ChangeTwoFactorPasswordDialog.razor +++ b/src/TelegramPanel.Web/Components/Dialogs/ChangeTwoFactorPasswordDialog.razor @@ -8,17 +8,43 @@ - - 将对选中的账号批量修改 Telegram 两步验证(二级密码)。请确认你输入的“原二级密码”在这些账号上是一致的,否则对应账号会失败。 - + @if (IsBatchMode) + { + + 将对选中的账号批量修改 Telegram 两步验证(二级密码)。 + @if (!useStoredPasswords) + { + 请确认你输入的"原二级密码"在这些账号上是一致的,否则对应账号会失败。 + } + + } 账号数量:@accounts.Count - + @if (IsBatchMode) + { + + + @if (useStoredPasswords) + { + + 将为每个账号使用其在数据库中保存的二级密码作为原密码。若某账号未保存密码,则以空密码尝试。 + + } + else + { + + } + } + else + { + + } - 忘记原二级密码时,可发起“重置二级密码”申请(通常需要等待 7 天)。等待结束后,再回来设置新的二级密码即可。 + 忘记原二级密码时,可发起"重置二级密码"申请(通常需要等待 7 天)。等待结束后,再回来设置新的二级密码即可。 + + @if (running || results.Count > 0) { @@ -93,6 +121,8 @@ private string newPassword = ""; private string confirmPassword = ""; private string hint = ""; + private bool useStoredPasswords; + private bool saveNewPasswordToDb = true; private bool running; private int processed; @@ -101,6 +131,23 @@ private readonly List results = new(); + private bool IsBatchMode => accounts.Count > 1; + + private string SingleAccountPasswordHelperText + { + get + { + if (accounts.Count != 1) + return "如果账号未开启两步验证,可留空"; + + var stored = accounts[0].TwoFactorPassword; + if (string.IsNullOrEmpty(stored)) + return "如果账号未开启两步验证,可留空(数据库中未保存密码)"; + + return "已自动填充数据库中保存的密码"; + } + } + protected override async Task OnInitializedAsync() { var ids = AccountIds?.Distinct().Where(x => x > 0).ToList() ?? new List(); @@ -109,6 +156,12 @@ var all = await AccountManagement.GetAllAccountsAsync(); accounts = all.Where(a => ids.Contains(a.Id)).OrderBy(a => a.Id).ToList(); + + // 单个账号时,自动填充数据库中保存的原二级密码 + if (accounts.Count == 1 && !string.IsNullOrEmpty(accounts[0].TwoFactorPassword)) + { + currentPassword = accounts[0].TwoFactorPassword; + } } private async Task Start() @@ -152,16 +205,43 @@ { foreach (var a in accounts) { + // 确定使用的原密码:批量模式下可选择使用数据库中保存的密码 + var effectiveCurrentPassword = (IsBatchMode && useStoredPasswords) + ? (a.TwoFactorPassword ?? "") + : currentPassword; + var (ok, err) = await AccountTelegramTools.ChangeTwoFactorPasswordAsync( accountId: a.Id, - currentPassword: currentPassword, + currentPassword: effectiveCurrentPassword, newPassword: newPassword, hint: hint, cancellationToken: CancellationToken.None); processed++; - if (ok) success++; else failed++; - results.Add(new ResultRow(a.Phone, ok, ok ? null : err)); + if (ok) + { + success++; + + // 修改成功后,将新密码保存到数据库 + if (saveNewPasswordToDb) + { + try + { + a.TwoFactorPassword = newPassword; + await AccountManagement.UpdateAccountAsync(a); + } + catch + { + // 保存失败不影响主流程 + } + } + } + else + { + failed++; + } + + results.Add(new ResultRow(a.DisplayPhone, ok, ok ? null : err)); StateHasChanged(); } @@ -190,7 +270,7 @@ bool? confirm = await DialogService.ShowMessageBox( "确认申请重置", - $"将对 {accounts.Count} 个账号向 Telegram 发起“重置二级密码”申请(通常需要等待 7 天)。是否继续?", + $"将对 {accounts.Count} 个账号向 Telegram 发起【重置二级密码】申请(通常需要等待 7 天)。是否继续?", yesText: "继续", cancelText: "取消"); if (confirm != true) @@ -212,7 +292,7 @@ processed++; if (ok) success++; else failed++; - results.Add(new ResultRow(a.Phone, ok, msg)); + results.Add(new ResultRow(a.DisplayPhone, ok, msg)); StateHasChanged(); } diff --git a/src/TelegramPanel.Web/Components/Dialogs/ChangeTwoFactorRecoveryEmailDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/ChangeTwoFactorRecoveryEmailDialog.razor index 067f7ab..0dc8123 100644 --- a/src/TelegramPanel.Web/Components/Dialogs/ChangeTwoFactorRecoveryEmailDialog.razor +++ b/src/TelegramPanel.Web/Components/Dialogs/ChangeTwoFactorRecoveryEmailDialog.razor @@ -22,7 +22,7 @@ } else { - 账号:@account.Phone + 账号:@account.DisplayPhone diff --git a/src/TelegramPanel.Web/Components/Dialogs/ChannelDetailsDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/ChannelDetailsDialog.razor index 7f07564..3559018 100644 --- a/src/TelegramPanel.Web/Components/Dialogs/ChannelDetailsDialog.razor +++ b/src/TelegramPanel.Web/Components/Dialogs/ChannelDetailsDialog.razor @@ -155,7 +155,7 @@ var creator = await AccountManagement.GetAccountAsync(channel.CreatorAccountId.Value); creatorDisplay = creator == null ? $"账号 {channel.CreatorAccountId.Value}" - : $"{creator.Phone}({creator.Nickname ?? creator.Username ?? creator.UserId.ToString()})"; + : $"{creator.DisplayPhone}({creator.Nickname ?? creator.Username ?? creator.UserId.ToString()})"; } else { diff --git a/src/TelegramPanel.Web/Components/Dialogs/EditUserProfileDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/EditUserProfileDialog.razor index 09da547..51f7e76 100644 --- a/src/TelegramPanel.Web/Components/Dialogs/EditUserProfileDialog.razor +++ b/src/TelegramPanel.Web/Components/Dialogs/EditUserProfileDialog.razor @@ -24,7 +24,7 @@ 可修改:昵称、Bio、用户名、头像。用户名暂时仅支持单账号设置。 - 账号:@account.Phone + 账号:@account.DisplayPhone - 执行账号:@account.Phone @(string.IsNullOrWhiteSpace(account.Nickname) ? "" : $"({account.Nickname})") + 执行账号:@account.DisplayPhone @(string.IsNullOrWhiteSpace(account.Nickname) ? "" : $"({account.Nickname})") } @@ -103,7 +103,7 @@ bool? confirm = await DialogService.ShowMessageBox( "确认执行", - $"将使用账号 {account.Phone} 对 {links.Count} 个链接执行订阅/加群操作。是否继续?", + $"将使用账号 {account.DisplayPhone} 对 {links.Count} 个链接执行订阅/加群操作。是否继续?", yesText: "继续", cancelText: "取消"); if (confirm != true) diff --git a/src/TelegramPanel.Web/Components/Dialogs/RiskWarningDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/RiskWarningDialog.razor index e1db957..cdc3fdc 100644 --- a/src/TelegramPanel.Web/Components/Dialogs/RiskWarningDialog.razor +++ b/src/TelegramPanel.Web/Components/Dialogs/RiskWarningDialog.razor @@ -48,7 +48,7 @@ @foreach (var account in RiskyAccounts) { - @account.Phone + @account.DisplayPhone @if (account.LastLoginAt != null) { (登录 @account.GetLoginHoursFormatted() 小时) diff --git a/src/TelegramPanel.Web/Components/Dialogs/VersionInfoDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/VersionInfoDialog.razor new file mode 100644 index 0000000..e5757a4 --- /dev/null +++ b/src/TelegramPanel.Web/Components/Dialogs/VersionInfoDialog.razor @@ -0,0 +1,34 @@ +@namespace TelegramPanel.Web.Components.Dialogs +@using TelegramPanel.Web.Services + + + + + Telegram Panel + + 版本:v@VersionService.Version + + + 完整版本:@VersionService.FullVersion + + + GitHub 仓库 + + + + + 关闭 + + + +@code +{ + [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!; + + private void Close() + { + MudDialog.Close(); + } +} diff --git a/src/TelegramPanel.Web/Components/Layout/MainLayout.razor b/src/TelegramPanel.Web/Components/Layout/MainLayout.razor index 4182808..a142579 100644 --- a/src/TelegramPanel.Web/Components/Layout/MainLayout.razor +++ b/src/TelegramPanel.Web/Components/Layout/MainLayout.razor @@ -9,6 +9,10 @@ Telegram Panel + + v@(VersionService.Version) + ("版本信息", new DialogParameters(), options); + await Task.CompletedTask; + } } diff --git a/src/TelegramPanel.Web/Components/Pages/AccountImport.razor b/src/TelegramPanel.Web/Components/Pages/AccountImport.razor index bffd662..7082651 100644 --- a/src/TelegramPanel.Web/Components/Pages/AccountImport.razor +++ b/src/TelegramPanel.Web/Components/Pages/AccountImport.razor @@ -20,12 +20,14 @@
  • 单账号:Zip 根目录直接包含一个 .json + 一个 .session
  • 批量导入:每个账号一个独立子文件夹,文件夹内包含一个 .json + 一个 .session
  • +
  • 二级密码:自动解析账号目录下的 2fa.txt 文件作为二级密码保存到数据库
批量结构示例:
xx.zip
   ├─ 8613111111111
   │   ├─ 8613111111111.json
-  │   └─ 8613111111111.session
+  │   ├─ 8613111111111.session
+  │   └─ 2fa.txt (可选,内容为二级密码)
   └─ 8615119714541
       ├─ 8615119714541.json
       └─ 8615119714541.session
@@ -45,6 +47,10 @@ 已选择:@zipFile.Name(@zipFile.Size bytes) } + + @@ -168,6 +174,7 @@ private IBrowserFile? zipFile; private bool importingZip = false; + private string zipTwoFactorPassword = ""; private string sessionString = ""; private bool importingString = false; @@ -205,7 +212,8 @@ try { - var results = await AccountImportService.ImportFromZipAsync(zipFile); + var twoFactorPwd = string.IsNullOrWhiteSpace(zipTwoFactorPassword) ? null : zipTwoFactorPassword.Trim(); + var results = await AccountImportService.ImportFromZipAsync(zipFile, twoFactorPassword: twoFactorPwd); importResults.AddRange(results); var successCount = results.Count(r => r.Success); @@ -217,6 +225,7 @@ Snackbar.Add($"{failureCount} 个账号导入失败", Severity.Warning); zipFile = null; + zipTwoFactorPassword = ""; } catch (Exception ex) { diff --git a/src/TelegramPanel.Web/Components/Pages/Accounts.razor b/src/TelegramPanel.Web/Components/Pages/Accounts.razor index b882ced..5ca7d7f 100644 --- a/src/TelegramPanel.Web/Components/Pages/Accounts.razor +++ b/src/TelegramPanel.Web/Components/Pages/Accounts.razor @@ -88,7 +88,7 @@ 操作 - @context.Phone + @context.DisplayPhone @(context.Nickname ?? "-") @(context.Username ?? "-") @context.UserId @@ -248,11 +248,15 @@ private async Task ShowDetails(Account account) { - var (channels, groups) = await AccountManagement.GetAccountStatisticsAsync(account.Id); - var createdAt = TimeZone.Format(account.CreatedAt, "yyyy-MM-dd HH:mm"); - var lastSyncAt = TimeZone.Format(account.LastSyncAt, "yyyy-MM-dd HH:mm"); - var message = $"账号详情:\n手机号:{account.Phone}\n用户名:{account.Username ?? "无"}\n用户ID:{account.UserId}\n分类:{account.Category?.Name ?? "未分类"}\n创建的频道:{channels} 个\n创建的群组:{groups} 个\n创建时间:{createdAt}\n最后数据同步:{lastSyncAt}\nSession路径:{account.SessionPath}"; - await DialogService.ShowMessageBox("账号详情", message, "关闭"); + var parameters = new DialogParameters + { + ["AccountId"] = account.Id + }; + var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }; + var dialog = DialogService.Show("账号详情", parameters, options)!; + var result = await dialog.Result; + if (result is { Canceled: false }) + await LoadAccounts(); } private async Task OpenChangeTwoFactorPassword(Account account) @@ -569,7 +573,7 @@ // 资料刷新后重新加载(昵称/用户名/用户ID 可能更新) await LoadAccounts(); - Snackbar.Add($"账号 {account.Phone} 状态:{result.Summary}", result.Ok ? Severity.Success : Severity.Warning); + Snackbar.Add($"账号 {account.DisplayPhone} 状态:{result.Summary}", result.Ok ? Severity.Success : Severity.Warning); } catch (Exception ex) { @@ -857,7 +861,7 @@ var parameters = new DialogParameters { ["AccountId"] = account.Id, - ["Phone"] = account.Phone + ["Phone"] = account.DisplayPhone }; var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true }; @@ -870,7 +874,7 @@ var parameters = new DialogParameters { ["AccountId"] = account.Id, - ["Phone"] = account.Phone + ["Phone"] = account.DisplayPhone }; var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true }; @@ -900,7 +904,7 @@ { bool? result = await DialogService.ShowMessageBox( "确认删除", - $"确定要删除账号 {account.Phone} 吗?此操作不可撤销!", + $"确定要删除账号 {account.DisplayPhone} 吗?此操作不可撤销!", yesText: "删除", cancelText: "取消"); if (result == true) diff --git a/src/TelegramPanel.Web/Components/Pages/Channels.razor b/src/TelegramPanel.Web/Components/Pages/Channels.razor index a64343a..cac0acf 100644 --- a/src/TelegramPanel.Web/Components/Pages/Channels.razor +++ b/src/TelegramPanel.Web/Components/Pages/Channels.razor @@ -115,7 +115,7 @@ @if (context.CreatorAccount != null) { - @context.CreatorAccount.Phone + @context.CreatorAccount.DisplayPhone } else if (context.CreatorAccountId != null) { diff --git a/src/TelegramPanel.Web/Components/Pages/Groups.razor b/src/TelegramPanel.Web/Components/Pages/Groups.razor index 13a77ce..d654bb6 100644 --- a/src/TelegramPanel.Web/Components/Pages/Groups.razor +++ b/src/TelegramPanel.Web/Components/Pages/Groups.razor @@ -23,7 +23,7 @@ 全部账号 @foreach (var account in accounts) { - @account.Phone (@account.Username) + @account.DisplayPhone (@account.Username) } @if (context.CreatorAccount != null) { - @context.CreatorAccount.Phone + @context.CreatorAccount.DisplayPhone } else { diff --git a/src/TelegramPanel.Web/Services/VersionService.cs b/src/TelegramPanel.Web/Services/VersionService.cs new file mode 100644 index 0000000..e107170 --- /dev/null +++ b/src/TelegramPanel.Web/Services/VersionService.cs @@ -0,0 +1,58 @@ +using System.Reflection; + +namespace TelegramPanel.Web.Services; + +/// +/// 版本信息服务(静态类),用于获取应用程序版本号 +/// +public static class VersionService +{ + private static readonly Lazy _version = new(GetVersionFromAssembly); + private static readonly Lazy _fullVersion = new(GetFullVersionFromAssembly); + + /// + /// 获取简化版本号(如:1.0.0) + /// + public static string Version => _version.Value; + + /// + /// 获取完整版本信息(包含 InformationalVersion,如:1.0.0+commitHash) + /// + public static string FullVersion => _fullVersion.Value; + + private static string GetVersionFromAssembly() + { + var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); + + // 优先使用 InformationalVersion(更友好的版本号) + var infoVersion = assembly + .GetCustomAttribute()? + .InformationalVersion; + + if (!string.IsNullOrWhiteSpace(infoVersion)) + { + // 移除可能的 +commitHash 后缀,只保留版本号部分 + var plusIndex = infoVersion.IndexOf('+'); + return plusIndex > 0 ? infoVersion[..plusIndex] : infoVersion; + } + + // 回退到 AssemblyVersion + var version = assembly.GetName().Version; + return version != null + ? $"{version.Major}.{version.Minor}.{version.Build}" + : "Unknown"; + } + + private static string GetFullVersionFromAssembly() + { + var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); + + var infoVersion = assembly + .GetCustomAttribute()? + .InformationalVersion; + + return !string.IsNullOrWhiteSpace(infoVersion) + ? infoVersion + : GetVersionFromAssembly(); + } +}