1.1.0:二级密码存储/导入优化;版本信息弹窗改进
This commit is contained in:
1 parent
4875aefdd7
commit
ab0df75600
25 files changed
+498
-43
No files matched your search
@@ -0,0 +1,22 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- 集中管理项目版本号 -->
|
||||
<!-- 版本格式: 主版本.次版本.修订号 -->
|
||||
<!-- 主版本: 重大变更/不兼容更新 -->
|
||||
<!-- 次版本: 新功能/兼容性更新 -->
|
||||
<!-- 修订号: Bug修复/小改进 -->
|
||||
<Version>1.1.0</Version>
|
||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
||||
<FileVersion>1.1.0.0</FileVersion>
|
||||
<InformationalVersion>1.1.0</InformationalVersion>
|
||||
|
||||
<!-- 项目元信息 -->
|
||||
<Authors>Telegram Panel Contributors</Authors>
|
||||
<Company>Telegram Panel</Company>
|
||||
<Product>Telegram Panel</Product>
|
||||
<Copyright>Copyright (c) 2024-2025 Telegram Panel Contributors</Copyright>
|
||||
<Description>Telegram Multi-Account Management Panel</Description>
|
||||
<RepositoryUrl>https://github.com/moeacgx/Telegram-Panel</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -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()
|
||||
|
||||
@@ -214,7 +214,8 @@ public class AccountImportService
|
||||
/// </summary>
|
||||
public async Task<List<ImportResult>> ImportFromZipAsync(
|
||||
IBrowserFile zipFile,
|
||||
int? categoryId = null)
|
||||
int? categoryId = null,
|
||||
string? twoFactorPassword = null)
|
||||
{
|
||||
var results = new List<ImportResult>();
|
||||
|
||||
@@ -251,7 +252,7 @@ public class AccountImportService
|
||||
var importedPhones = new HashSet<string>(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<ImportResult> ImportFromPackageEntryAsync(string jsonPath, int? categoryId)
|
||||
private async Task<ImportResult> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试从目录中读取 2fa.txt 文件内容作为二级密码
|
||||
/// </summary>
|
||||
private static async Task<string?> 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();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace TelegramPanel.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
@@ -7,6 +9,20 @@ public class Account
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Phone { get; set; } = null!;
|
||||
|
||||
private string? _displayPhone;
|
||||
|
||||
/// <summary>
|
||||
/// 格式化后的手机号(用于 UI 展示,不持久化),格式如:+86 13800138000
|
||||
/// 如果未通过 AccountManagementService 设置,则回退到 Phone 的值
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public string DisplayPhone
|
||||
{
|
||||
get => _displayPhone ?? Phone;
|
||||
set => _displayPhone = value;
|
||||
}
|
||||
|
||||
public long UserId { get; set; }
|
||||
/// <summary>
|
||||
/// 账号昵称(Telegram 显示名称)
|
||||
@@ -46,6 +62,11 @@ public class Account
|
||||
/// </summary>
|
||||
public DateTime? TelegramStatusCheckedAtUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 二级密码(两步验证密码),用于系统保存以便后续修改
|
||||
/// </summary>
|
||||
public string? TwoFactorPassword { get; set; }
|
||||
|
||||
// 导航属性
|
||||
public AccountCategory? Category { get; set; }
|
||||
public ICollection<Channel> Channels { get; set; } = new List<Channel>();
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TelegramPanel.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20251226000000_AddTwoFactorPassword")]
|
||||
/// <inheritdoc />
|
||||
public partial class AddTwoFactorPassword : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TwoFactorPassword",
|
||||
table: "Accounts",
|
||||
type: "TEXT",
|
||||
maxLength: 200,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TwoFactorPassword",
|
||||
table: "Accounts");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,10 @@ namespace TelegramPanel.Data.Migrations
|
||||
b.Property<string>("TelegramStatusSummary")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TwoFactorPassword")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
@inject AccountManagementService AccountManagement
|
||||
@inject PanelTimeZoneService TimeZone
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Info" Class="mr-2" />
|
||||
账号详情
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
@if (account == null)
|
||||
{
|
||||
<MudProgressCircular Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudStack Spacing="3">
|
||||
<MudTextField Value="@FormatCreatedAt()" Label="创建时间" ReadOnly="true" Variant="Variant.Outlined" />
|
||||
<MudTextField Value="@account.SessionPath" Label="Session 路径" ReadOnly="true" Variant="Variant.Outlined" />
|
||||
|
||||
<MudDivider Class="my-2" />
|
||||
|
||||
<MudText Typo="Typo.subtitle1" Class="mb-1">二级密码</MudText>
|
||||
|
||||
@if (isEditingPassword)
|
||||
{
|
||||
<MudTextField @bind-Value="newPassword" Label="新密码" Variant="Variant.Outlined"
|
||||
InputType="@(showPassword ? InputType.Text : InputType.Password)"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentIcon="@(showPassword ? Icons.Material.Filled.VisibilityOff : Icons.Material.Filled.Visibility)"
|
||||
OnAdornmentClick="() => showPassword = !showPassword" />
|
||||
|
||||
<MudStack Row="true" Spacing="2" Justify="Justify.FlexEnd">
|
||||
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="CancelEdit">取消</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SavePassword" Disabled="@saving">
|
||||
@if (saving)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
||||
}
|
||||
保存
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudTextField Value="@DisplayPassword()" Label="当前保存的密码" ReadOnly="true" Variant="Variant.Outlined"
|
||||
InputType="@(showPassword ? InputType.Text : InputType.Password)"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentIcon="@(showPassword ? Icons.Material.Filled.VisibilityOff : Icons.Material.Filled.Visibility)"
|
||||
OnAdornmentClick="() => showPassword = !showPassword"
|
||||
Style="flex: 1;" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Primary" OnClick="StartEdit" Title="修改" />
|
||||
</MudStack>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Close">关闭</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -8,17 +8,43 @@
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="2">
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
|
||||
将对选中的账号批量修改 Telegram 两步验证(二级密码)。请确认你输入的“原二级密码”在这些账号上是一致的,否则对应账号会失败。
|
||||
</MudAlert>
|
||||
@if (IsBatchMode)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
|
||||
将对选中的账号批量修改 Telegram 两步验证(二级密码)。
|
||||
@if (!useStoredPasswords)
|
||||
{
|
||||
<span>请确认你输入的"原二级密码"在这些账号上是一致的,否则对应账号会失败。</span>
|
||||
}
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudText Typo="Typo.subtitle2">账号数量:@accounts.Count</MudText>
|
||||
|
||||
<MudTextField @bind-Value="currentPassword" Label="原二级密码" Variant="Variant.Outlined" InputType="InputType.Password"
|
||||
HelperText="如果账号未开启两步验证,可留空" />
|
||||
@if (IsBatchMode)
|
||||
{
|
||||
<MudSwitch @bind-Value="useStoredPasswords" Label="使用数据库中保存的原二级密码" Color="Color.Primary" />
|
||||
|
||||
@if (useStoredPasswords)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
|
||||
将为每个账号使用其在数据库中保存的二级密码作为原密码。若某账号未保存密码,则以空密码尝试。
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTextField @bind-Value="currentPassword" Label="原二级密码(统一)" Variant="Variant.Outlined" InputType="InputType.Password"
|
||||
HelperText="如果账号未开启两步验证,可留空" />
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTextField @bind-Value="currentPassword" Label="原二级密码" Variant="Variant.Outlined" InputType="InputType.Password"
|
||||
HelperText="@SingleAccountPasswordHelperText" />
|
||||
}
|
||||
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
|
||||
忘记原二级密码时,可发起“重置二级密码”申请(通常需要等待 7 天)。等待结束后,再回来设置新的二级密码即可。
|
||||
忘记原二级密码时,可发起"重置二级密码"申请(通常需要等待 7 天)。等待结束后,再回来设置新的二级密码即可。
|
||||
</MudAlert>
|
||||
|
||||
<MudTextField @bind-Value="newPassword" Label="新二级密码" Variant="Variant.Outlined" InputType="InputType.Password"
|
||||
@@ -30,6 +56,8 @@
|
||||
<MudTextField @bind-Value="hint" Label="密码提示(可选)" Variant="Variant.Outlined"
|
||||
HelperText="Telegram 会展示给你作为提示,建议不要写明文密码" />
|
||||
|
||||
<MudCheckBox @bind-Value="saveNewPasswordToDb" Label="修改成功后将新密码保存到数据库" Color="Color.Primary" />
|
||||
|
||||
@if (running || results.Count > 0)
|
||||
{
|
||||
<MudDivider Class="my-2" />
|
||||
@@ -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<ResultRow> 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<int>();
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.subtitle2">账号:@account.Phone</MudText>
|
||||
<MudText Typo="Typo.subtitle2">账号:@account.DisplayPhone</MudText>
|
||||
|
||||
<MudStack direction="Row" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<MudChip T="string" Size="Size.Small" Color="@(hasTwoFactorPassword ? Color.Success : Color.Warning)">
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
可修改:昵称、Bio、用户名、头像。用户名暂时仅支持单账号设置。
|
||||
</MudAlert>
|
||||
|
||||
<MudText Typo="Typo.subtitle2">账号:@account.Phone</MudText>
|
||||
<MudText Typo="Typo.subtitle2">账号:@account.DisplayPhone</MudText>
|
||||
|
||||
<MudSwitch @bind-Value="editNickname" Label="修改昵称" Color="Color.Primary" />
|
||||
<MudTextField @bind-Value="nickname" Label="昵称" Variant="Variant.Outlined" Disabled="@(!editNickname)"
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
var creator = await AccountManagement.GetAccountAsync(group.CreatorAccountId);
|
||||
creatorDisplay = creator == null
|
||||
? $"账号 {group.CreatorAccountId}"
|
||||
: $"{creator.Phone}({creator.Nickname ?? creator.Username ?? creator.UserId.ToString()})";
|
||||
: $"{creator.DisplayPhone}({creator.Nickname ?? creator.Username ?? creator.UserId.ToString()})";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.subtitle2">
|
||||
执行账号:@account.Phone @(string.IsNullOrWhiteSpace(account.Nickname) ? "" : $"({account.Nickname})")
|
||||
执行账号:@account.DisplayPhone @(string.IsNullOrWhiteSpace(account.Nickname) ? "" : $"({account.Nickname})")
|
||||
</MudText>
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
|
||||
bool? confirm = await DialogService.ShowMessageBox(
|
||||
"确认执行",
|
||||
$"将使用账号 {account.Phone} 对 {links.Count} 个链接执行订阅/加群操作。是否继续?",
|
||||
$"将使用账号 {account.DisplayPhone} 对 {links.Count} 个链接执行订阅/加群操作。是否继续?",
|
||||
yesText: "继续", cancelText: "取消");
|
||||
|
||||
if (confirm != true)
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
@foreach (var account in RiskyAccounts)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Warning" Class="ma-1">
|
||||
@account.Phone
|
||||
@account.DisplayPhone
|
||||
@if (account.LastLoginAt != null)
|
||||
{
|
||||
<text> (登录 @account.GetLoginHoursFormatted() 小时)</text>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
@namespace TelegramPanel.Web.Components.Dialogs
|
||||
@using TelegramPanel.Web.Services
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="2">
|
||||
<MudText Typo="Typo.h6">Telegram Panel</MudText>
|
||||
<MudText Typo="Typo.body2">
|
||||
<strong>版本:</strong>v@VersionService.Version
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2" Style="overflow-wrap: anywhere; word-break: break-word; user-select: text;">
|
||||
<strong>完整版本:</strong>@VersionService.FullVersion
|
||||
</MudText>
|
||||
<MudLink Href="https://github.com/moeacgx/Telegram-Panel"
|
||||
Target="_blank"
|
||||
Rel="noopener noreferrer">
|
||||
GitHub 仓库
|
||||
</MudLink>
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="Close">关闭</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
private void Close()
|
||||
{
|
||||
MudDialog.Close();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,10 @@
|
||||
<MudAppBar Elevation="1">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@ToggleDrawer" />
|
||||
<MudText Typo="Typo.h5" Class="ml-3">Telegram Panel</MudText>
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Default" Class="ml-2" Style="cursor: pointer;"
|
||||
title="点击查看版本信息" OnClick="@ShowVersionInfo">
|
||||
v@(VersionService.Version)
|
||||
</MudChip>
|
||||
<MudSpacer />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Settings"
|
||||
Color="Color.Inherit"
|
||||
@@ -38,6 +42,8 @@
|
||||
|
||||
@using TelegramPanel.Web.Services
|
||||
@inject UiPreferencesService UiPreferences
|
||||
@inject NavigationManager Navigation
|
||||
@inject IDialogService DialogService
|
||||
|
||||
@code {
|
||||
private bool _drawerOpen = true;
|
||||
@@ -82,4 +88,17 @@
|
||||
// 强制刷新组件状态,确保在异步操作后 UI 能及时更新
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task ShowVersionInfo()
|
||||
{
|
||||
var options = new DialogOptions
|
||||
{
|
||||
CloseOnEscapeKey = true,
|
||||
MaxWidth = MaxWidth.Medium,
|
||||
FullWidth = true
|
||||
};
|
||||
|
||||
DialogService.Show<TelegramPanel.Web.Components.Dialogs.VersionInfoDialog>("版本信息", new DialogParameters(), options);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,14 @@
|
||||
<ul style="margin: 6px 0 0 18px;">
|
||||
<li>单账号:Zip 根目录直接包含一个 <strong>.json</strong> + 一个 <strong>.session</strong></li>
|
||||
<li>批量导入:每个账号一个独立子文件夹,文件夹内包含一个 <strong>.json</strong> + 一个 <strong>.session</strong></li>
|
||||
<li><strong>二级密码</strong>:自动解析账号目录下的 <strong>2fa.txt</strong> 文件作为二级密码保存到数据库</li>
|
||||
</ul>
|
||||
<div style="margin-top: 6px;">批量结构示例:</div>
|
||||
<pre style="margin: 4px 0 0; white-space: pre; overflow: auto;">xx.zip
|
||||
├─ 8613111111111
|
||||
│ ├─ 8613111111111.json
|
||||
│ └─ 8613111111111.session
|
||||
│ ├─ 8613111111111.session
|
||||
│ └─ 2fa.txt (可选,内容为二级密码)
|
||||
└─ 8615119714541
|
||||
├─ 8615119714541.json
|
||||
└─ 8615119714541.session</pre>
|
||||
@@ -45,6 +47,10 @@
|
||||
<MudText Typo="Typo.body2" Class="mt-3">已选择:@zipFile.Name(@zipFile.Size bytes)</MudText>
|
||||
}
|
||||
|
||||
<MudTextField @bind-Value="zipTwoFactorPassword" Label="二级密码(可选)" Variant="Variant.Outlined"
|
||||
InputType="InputType.Password" Class="mt-3"
|
||||
HelperText="统一密码,用于没有 2fa.txt 的账号;若账号目录有 2fa.txt 则优先使用文件内容" />
|
||||
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Success" FullWidth="true"
|
||||
Disabled="@(zipFile == null || importingZip)" Class="mt-4"
|
||||
OnClick="@(async (MouseEventArgs _) => await ImportZip())">
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
<MudTh>操作</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="手机号">@context.Phone</MudTd>
|
||||
<MudTd DataLabel="手机号">@context.DisplayPhone</MudTd>
|
||||
<MudTd DataLabel="昵称">@(context.Nickname ?? "-")</MudTd>
|
||||
<MudTd DataLabel="用户名">@(context.Username ?? "-")</MudTd>
|
||||
<MudTd DataLabel="用户ID">@context.UserId</MudTd>
|
||||
@@ -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<TelegramPanel.Web.Components.Dialogs.AccountDetailsDialog>("账号详情", 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)
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
<MudTd DataLabel="创建账号">
|
||||
@if (context.CreatorAccount != null)
|
||||
{
|
||||
<span>@context.CreatorAccount.Phone</span>
|
||||
<span>@context.CreatorAccount.DisplayPhone</span>
|
||||
}
|
||||
else if (context.CreatorAccountId != null)
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<MudSelectItem Value="0">全部账号</MudSelectItem>
|
||||
@foreach (var account in accounts)
|
||||
{
|
||||
<MudSelectItem Value="@account.Id">@account.Phone (@account.Username)</MudSelectItem>
|
||||
<MudSelectItem Value="@account.Id">@account.DisplayPhone (@account.Username)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudTextField @bind-Value="searchString" Placeholder="搜索群组..." Adornment="Adornment.Start"
|
||||
@@ -64,7 +64,7 @@
|
||||
<MudTd DataLabel="创建账号">
|
||||
@if (context.CreatorAccount != null)
|
||||
{
|
||||
<span>@context.CreatorAccount.Phone</span>
|
||||
<span>@context.CreatorAccount.DisplayPhone</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace TelegramPanel.Web.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 版本信息服务(静态类),用于获取应用程序版本号
|
||||
/// </summary>
|
||||
public static class VersionService
|
||||
{
|
||||
private static readonly Lazy<string> _version = new(GetVersionFromAssembly);
|
||||
private static readonly Lazy<string> _fullVersion = new(GetFullVersionFromAssembly);
|
||||
|
||||
/// <summary>
|
||||
/// 获取简化版本号(如:1.0.0)
|
||||
/// </summary>
|
||||
public static string Version => _version.Value;
|
||||
|
||||
/// <summary>
|
||||
/// 获取完整版本信息(包含 InformationalVersion,如:1.0.0+commitHash)
|
||||
/// </summary>
|
||||
public static string FullVersion => _fullVersion.Value;
|
||||
|
||||
private static string GetVersionFromAssembly()
|
||||
{
|
||||
var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
|
||||
|
||||
// 优先使用 InformationalVersion(更友好的版本号)
|
||||
var infoVersion = assembly
|
||||
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
|
||||
.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<AssemblyInformationalVersionAttribute>()?
|
||||
.InformationalVersion;
|
||||
|
||||
return !string.IsNullOrWhiteSpace(infoVersion)
|
||||
? infoVersion
|
||||
: GetVersionFromAssembly();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user