fix: 全局Telegram API配置+DB初始化+Razor修复

This commit is contained in:
meoacgx committed 2025-12-19 10:21:24 +08:00
1 parent 28546a614b
commit b4fd6dde00
21 files changed
+1171 -272

No files matched your search

+7
View File
@@ -18,6 +18,10 @@ obj/
logs/
*.log
# Local config overrides (secrets)
appsettings.local.json
src/TelegramPanel.Web/appsettings.local.json
# Sessions
sessions/
@@ -25,6 +29,9 @@ sessions/
.DS_Store
Thumbs.db
# SQLite
*.db
# NuGet
*.nupkg
packages/
+6
View File
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "8.0.416",
"rollForward": "latestPatch"
}
}
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using TelegramPanel.Core.Interfaces;
using TelegramPanel.Core.Models;
using AccountStatus = TelegramPanel.Core.Interfaces.AccountStatus;
@@ -12,23 +13,30 @@ public class AccountService : IAccountService
{
private readonly ITelegramClientPool _clientPool;
private readonly ILogger<AccountService> _logger;
private readonly IConfiguration _configuration;
// 临时存储登录状态(实际项目应该使用数据库或缓存)
private readonly Dictionary<int, string> _pendingLogins = new();
public AccountService(ITelegramClientPool clientPool, ILogger<AccountService> logger)
public AccountService(ITelegramClientPool clientPool, ILogger<AccountService> logger, IConfiguration configuration)
{
_clientPool = clientPool;
_logger = logger;
_configuration = configuration;
}
public async Task<LoginResult> StartLoginAsync(int accountId, string phone)
{
// 这里需要从数据库获取 apiId, apiHash, sessionPath
// 暂时使用示例值
var apiId = 0; // TODO: 从配置或数据库获取
var apiHash = ""; // TODO: 从配置或数据库获取
var sessionPath = $"sessions/{phone}.session";
if (!int.TryParse(_configuration["Telegram:ApiId"], out var apiId) || apiId <= 0
|| string.IsNullOrWhiteSpace(_configuration["Telegram:ApiHash"]))
{
return new LoginResult(false, null, "请先在【系统设置】中配置全局 Telegram API(ApiId/ApiHash)");
}
var apiHash = _configuration["Telegram:ApiHash"]!.Trim();
var sessionsPath = _configuration["Telegram:SessionsPath"] ?? "sessions";
Directory.CreateDirectory(sessionsPath);
var sessionPath = Path.Combine(sessionsPath, $"{phone}.session");
var client = await _clientPool.GetOrCreateClientAsync(accountId, apiId, apiHash, sessionPath);
@@ -9,8 +9,14 @@
<ItemGroup>
<PackageReference Include="WTelegramClient" Version="4.*" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.*" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.*" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.*" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TelegramPanel.Data\TelegramPanel.Data.csproj" />
</ItemGroup>
</Project>
@@ -1,4 +1,5 @@
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@@ -6,6 +7,8 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace TelegramPanel.Data.Migrations
{
/// <inheritdoc />
[DbContext(typeof(AppDbContext))]
[Migration("20250119000000_InitialCreate")]
public partial class InitialCreate : Migration
{
/// <inheritdoc />
+2 -2
View File
@@ -7,11 +7,11 @@
<base href="/" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<HeadOutlet />
<HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
<Routes />
<Routes @rendermode="InteractiveServer" />
<script src="_framework/blazor.web.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
</body>
@@ -1,5 +1,8 @@
@page "/accounts/categories"
@inject AccountCategoryManagementService CategoryManagement
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@using TelegramPanel.Data.Entities
<PageTitle>账号分类 - Telegram Panel</PageTitle>
@@ -13,7 +16,7 @@
</MudCardHeader>
<MudCardContent>
<MudTextField @bind-Value="newCategoryName" Label="分类名称" Variant="Variant.Outlined" />
<MudColorPicker @bind-Value="newCategoryColor" Label="分类颜色" Variant="Variant.Outlined" Class="mt-3" />
<MudColorPicker @bind-Value="newCategoryColor" Label="分类颜色" Class="mt-3" />
<MudTextField @bind-Value="newCategoryDesc" Label="描述" Variant="Variant.Outlined" Lines="3" Class="mt-3" />
</MudCardContent>
<MudCardActions>
@@ -31,7 +34,7 @@
<MudText Typo="Typo.h6">分类列表</MudText>
</MudCardHeader>
<MudCardContent>
<MudTable Items="@categories" Hover="true">
<MudTable Items="@categories" Hover="true" Loading="@loading">
<HeaderContent>
<MudTh>分类名称</MudTh>
<MudTh>颜色</MudTh>
@@ -41,18 +44,16 @@
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="分类名称">
<MudChip Style="@($"background-color: {context.Color}")">@context.Name</MudChip>
<MudChip T="string" Style="@($"background-color: {context.Color ?? "#9E9E9E"}")">@context.Name</MudChip>
</MudTd>
<MudTd DataLabel="颜色">
<div style="@($"width: 30px; height: 30px; background-color: {context.Color}; border-radius: 4px;")"></div>
<div style="@($"width: 30px; height: 30px; background-color: {context.Color ?? "#9E9E9E"}; border-radius: 4px;")"></div>
</MudTd>
<MudTd DataLabel="描述">@context.Description</MudTd>
<MudTd DataLabel="账号数量">@context.AccountCount</MudTd>
<MudTd DataLabel="描述">@(context.Description ?? "-")</MudTd>
<MudTd DataLabel="账号数量">@context.Accounts.Count</MudTd>
<MudTd DataLabel="操作">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small" Color="Color.Info"
OnClick="@(() => EditCategory(context))" />
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error"
OnClick="@(() => DeleteCategory(context.Id))" />
OnClick="@(() => DeleteCategory(context))" />
</MudTd>
</RowTemplate>
<NoRecordsContent>
@@ -68,56 +69,77 @@
private string newCategoryName = "";
private MudColor newCategoryColor = "#1976d2";
private string newCategoryDesc = "";
private List<AccountCategory> categories = new();
private bool loading = true;
private List<CategoryModel> categories = new()
protected override async Task OnInitializedAsync()
{
new CategoryModel { Id = 1, Name = "工作账号", Color = "#1976d2", Description = "用于工作的账号", AccountCount = 5 },
new CategoryModel { Id = 2, Name = "推广账号", Color = "#f44336", Description = "用于推广的账号", AccountCount = 10 },
new CategoryModel { Id = 3, Name = "测试账号", Color = "#4caf50", Description = "测试用账号", AccountCount = 3 }
};
private void AddCategory()
{
var newCategory = new CategoryModel
{
Id = categories.Max(c => c.Id) + 1,
Name = newCategoryName,
Color = newCategoryColor.ToString(),
Description = newCategoryDesc,
AccountCount = 0
};
categories.Add(newCategory);
Snackbar.Add($"分类 \"{newCategoryName}\" 添加成功", Severity.Success);
// 清空表单
newCategoryName = "";
newCategoryColor = "#1976d2";
newCategoryDesc = "";
await LoadCategories();
}
private void EditCategory(CategoryModel category)
private async Task LoadCategories()
{
Snackbar.Add($"编辑分类:{category.Name}", Severity.Info);
// TODO: 实现编辑对话框
}
private void DeleteCategory(int id)
{
var category = categories.FirstOrDefault(c => c.Id == id);
if (category != null)
loading = true;
try
{
categories.Remove(category);
Snackbar.Add($"分类已删除", Severity.Success);
var result = await CategoryManagement.GetAllCategoriesAsync();
categories = result.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载失败:{ex.Message}", Severity.Error);
}
finally
{
loading = false;
}
}
private class CategoryModel
private async Task AddCategory()
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Color { get; set; } = "";
public string Description { get; set; } = "";
public int AccountCount { get; set; }
try
{
var category = new AccountCategory
{
Name = newCategoryName,
Color = newCategoryColor.Value,
Description = newCategoryDesc,
CreatedAt = DateTime.UtcNow
};
await CategoryManagement.CreateCategoryAsync(category);
await LoadCategories();
Snackbar.Add($"分类 \"{newCategoryName}\" 添加成功", Severity.Success);
newCategoryName = "";
newCategoryColor = "#1976d2";
newCategoryDesc = "";
}
catch (Exception ex)
{
Snackbar.Add($"添加失败:{ex.Message}", Severity.Error);
}
}
private async Task DeleteCategory(AccountCategory category)
{
bool? result = await DialogService.ShowMessageBox(
"确认删除",
$"确定要删除分类 {category.Name} 吗?关联的账号将变为未分类。",
yesText: "删除", cancelText: "取消");
if (result == true)
{
try
{
await CategoryManagement.DeleteCategoryAsync(category.Id);
categories.Remove(category);
Snackbar.Add("删除成功", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"删除失败:{ex.Message}", Severity.Error);
}
}
}
}
@@ -1,5 +1,6 @@
@page "/accounts/import"
@inject AccountImportService AccountImportService
@inject IConfiguration Configuration
@inject ISnackbar Snackbar
<PageTitle>导入账号 - Telegram Panel</PageTitle>
@@ -13,9 +14,6 @@
<MudText Typo="Typo.h6">Session 文件导入</MudText>
</MudCardHeader>
<MudCardContent>
<MudTextField @bind-Value="apiId" Label="API ID" Variant="Variant.Outlined" Required="true" />
<MudTextField @bind-Value="apiHash" Label="API Hash" Variant="Variant.Outlined" Required="true" Class="mt-3" />
<MudFileUpload T="IReadOnlyList<IBrowserFile>" FilesChanged="OnFilesChanged" Accept=".session" MaximumFileCount="100">
<ActivatorContent>
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.CloudUpload" Class="mt-4">
@@ -27,10 +25,10 @@
@if (selectedFiles.Any())
{
<MudText Typo="Typo.body2" Class="mt-3">已选择 @selectedFiles.Count 个文件</MudText>
<MudList Dense="true">
<MudList T="string" Dense="true">
@foreach (var file in selectedFiles)
{
<MudListItem Icon="@Icons.Material.Filled.Description">@file.Name</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Description">@file.Name</MudListItem>
}
</MudList>
}
@@ -58,8 +56,6 @@
<MudText Typo="Typo.h6">StringSession 导入</MudText>
</MudCardHeader>
<MudCardContent>
<MudTextField @bind-Value="stringApiId" Label="API ID" Variant="Variant.Outlined" Required="true" />
<MudTextField @bind-Value="stringApiHash" Label="API Hash" Variant="Variant.Outlined" Required="true" Class="mt-3" />
<MudTextField @bind-Value="sessionString" Label="Session String (Base64)" Variant="Variant.Outlined"
Lines="5" Required="true" Class="mt-3" />
@@ -101,11 +97,11 @@
<MudTd DataLabel="用户ID">@(context.UserId?.ToString() ?? "-")</MudTd>
<MudTd DataLabel="用户名">@(context.Username ?? "-")</MudTd>
<MudTd DataLabel="状态">
<MudChip Size="Size.Small" Color="@(context.Success ? Color.Success : Color.Error)">
<MudChip T="string" Size="Size.Small" Color="@(context.Success ? Color.Success : Color.Error)">
@(context.Success ? "成功" : "失败")
</MudChip>
</MudTd>
<MudTd DataLabel="消息">@(context.Message ?? "-")</MudTd>
<MudTd DataLabel="消息">@(context.Success ? "成功" : (context.Error ?? "失败"))</MudTd>
</RowTemplate>
</MudTable>
</MudCardContent>
@@ -113,18 +109,22 @@
}
@code {
private string apiId = "";
private string apiHash = "";
private List<IBrowserFile> selectedFiles = new();
private bool importing = false;
private string stringApiId = "";
private string stringApiHash = "";
private string sessionString = "";
private bool importingString = false;
private List<ImportResult> importResults = new();
private bool TryGetTelegramApi(out int apiId, out string apiHash)
{
apiHash = Configuration["Telegram:ApiHash"] ?? "";
return int.TryParse(Configuration["Telegram:ApiId"], out apiId)
&& apiId > 0
&& !string.IsNullOrWhiteSpace(apiHash);
}
private void OnFilesChanged(IReadOnlyList<IBrowserFile> files)
{
selectedFiles = files.ToList();
@@ -132,9 +132,9 @@
private async Task ImportSessionFiles()
{
if (!int.TryParse(apiId, out int parsedApiId))
if (!TryGetTelegramApi(out var apiId, out var apiHash))
{
Snackbar.Add("API ID 格式错误", Severity.Error);
Snackbar.Add("请先在【系统设置】中配置全局 Telegram API(ApiId/ApiHash)", Severity.Error);
return;
}
@@ -145,7 +145,7 @@
{
var results = await AccountImportService.ImportFromBrowserFilesAsync(
selectedFiles,
parsedApiId,
apiId,
apiHash
);
@@ -179,9 +179,9 @@
private async Task ImportStringSession()
{
if (!int.TryParse(stringApiId, out int parsedApiId))
if (!TryGetTelegramApi(out var apiId, out var apiHash))
{
Snackbar.Add("API ID 格式错误", Severity.Error);
Snackbar.Add("请先在【系统设置】中配置全局 Telegram API(ApiId/ApiHash)", Severity.Error);
return;
}
@@ -191,8 +191,8 @@
{
var result = await AccountImportService.ImportFromStringSessionAsync(
sessionString,
parsedApiId,
stringApiHash
apiId,
apiHash
);
importResults.Add(result);
@@ -204,7 +204,7 @@
}
else
{
Snackbar.Add($"导入失败:{result.Message}", Severity.Error);
Snackbar.Add($"导入失败:{result.Error ?? "未知错误"}", Severity.Error);
}
}
catch (Exception ex)
@@ -1,6 +1,7 @@
@page "/accounts/login"
@inject IAccountService AccountService
@inject AccountManagementService AccountManagement
@inject IConfiguration Configuration
@inject ISnackbar Snackbar
@inject NavigationManager Navigation
@using TelegramPanel.Data.Entities
@@ -15,8 +16,21 @@
<MudCardContent>
<MudStepper @ref="stepper" Color="Color.Primary">
<MudStep Title="API 配置">
<MudTextField @bind-Value="apiId" Label="API ID" Variant="Variant.Outlined" Required="true" />
<MudTextField @bind-Value="apiHash" Label="API Hash" Variant="Variant.Outlined" Required="true" Class="mt-3" />
@if (!IsApiConfigured)
{
<MudAlert Severity="Severity.Warning" Class="mb-3">
<MudText Typo="Typo.body2">未配置全局 Telegram API(ApiId/ApiHash),无法登录。</MudText>
</MudAlert>
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Href="/settings">
去系统设置配置
</MudButton>
}
else
{
<MudAlert Severity="Severity.Info" Class="mb-3">
<MudText Typo="Typo.body2">使用全局 Telegram API:ApiId=@(Configuration["Telegram:ApiId"] ?? "(未配置)")</MudText>
</MudAlert>
}
<MudTextField @bind-Value="phoneNumber" Label="手机号" Variant="Variant.Outlined" Required="true"
Placeholder="+86xxxxxxxxxx" Class="mt-3" />
<MudText Typo="Typo.caption" Class="mt-2" Color="Color.Info">
@@ -57,20 +71,23 @@
</MudStepper>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="@(() => stepper.Reset())">
<MudButton Variant="Variant.Text" Color="Color.Primary"
OnClick="@(async (MouseEventArgs _) => await ResetStepperAsync())">
重置
</MudButton>
<MudSpacer />
@if (stepper.GetActiveIndex() > 0 && !stepper.IsAllStepsCompleted())
@if ((stepper?.ActiveIndex ?? 0) > 0 && !IsFinished)
{
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="@(() => stepper.Previous())">
<MudButton Variant="Variant.Text" Color="Color.Primary"
OnClick="@(async (MouseEventArgs _) => await PreviousStepAsync())">
上一步
</MudButton>
}
@if (!stepper.IsAllStepsCompleted())
@if (!IsFinished)
{
<MudButton Variant="Variant.Filled" Color="Color.Primary"
Disabled="@logging" OnClick="NextStep">
Disabled="@logging"
OnClick="@(async (MouseEventArgs _) => await NextStep())">
@if (logging)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
@@ -78,7 +95,7 @@
}
else
{
<span>@(stepper.GetActiveIndex() == stepper.Steps.Count - 2 ? "登录" : "下一步")</span>
<span>@(IsLoginActionStep ? "登录" : "下一步")</span>
}
</MudButton>
}
@@ -88,11 +105,12 @@
</MudGrid>
@code {
private MudStepper stepper;
private MudStepper? stepper;
private bool logging = false;
private string apiId = "";
private string apiHash = "";
private int telegramApiId;
private string telegramApiHash = "";
private string sessionsPath = "sessions";
private string phoneNumber = "";
private string verificationCode = "";
private string password = "";
@@ -101,23 +119,54 @@
private long userId = 0;
private int accountId = 0;
private bool TryGetTelegramApi(out int apiId, out string apiHash, out string configuredSessionsPath)
{
apiHash = Configuration["Telegram:ApiHash"] ?? "";
configuredSessionsPath = Configuration["Telegram:SessionsPath"] ?? "sessions";
return int.TryParse(Configuration["Telegram:ApiId"], out apiId)
&& apiId > 0
&& !string.IsNullOrWhiteSpace(apiHash);
}
private bool IsApiConfigured => TryGetTelegramApi(out _, out _, out _);
private bool IsFinished =>
stepper != null && stepper.ActiveIndex >= stepper.Steps.Count - 1;
private bool IsLoginActionStep =>
stepper != null && stepper.ActiveIndex == stepper.Steps.Count - 2;
private async Task ResetStepperAsync()
{
if (stepper == null) return;
await stepper.ResetAsync(true);
}
private async Task PreviousStepAsync()
{
if (stepper == null) return;
await stepper.PreviousStepAsync();
}
private async Task NextStep()
{
var currentStep = stepper.GetActiveIndex();
if (stepper == null) return;
var currentStep = stepper.ActiveIndex;
logging = true;
try
{
if (currentStep == 0)
{
// 步骤1:验证 API 配置并发送验证码
if (!int.TryParse(apiId, out int parsedApiId))
// 步骤1:检查全局配置并发送验证码
if (!TryGetTelegramApi(out telegramApiId, out telegramApiHash, out sessionsPath))
{
Snackbar.Add("API ID 格式错误", Severity.Error);
Snackbar.Add("请先在【系统设置】中配置全局 Telegram API(ApiId/ApiHash)", Severity.Error);
return;
}
if (string.IsNullOrWhiteSpace(apiHash) || string.IsNullOrWhiteSpace(phoneNumber))
if (string.IsNullOrWhiteSpace(phoneNumber))
{
Snackbar.Add("请填写完整信息", Severity.Error);
return;
@@ -131,7 +180,7 @@
if (result.NextStep == "code")
{
Snackbar.Add($"验证码已发送到 {phoneNumber}", Severity.Info);
stepper.Next();
await stepper.NextStepAsync();
}
else if (result.NextStep == "signup")
{
@@ -161,7 +210,7 @@
else if (result.NextStep == "password")
{
Snackbar.Add("需要两步验证密码", Severity.Info);
stepper.Next();
await stepper.NextStepAsync();
}
else
{
@@ -211,6 +260,8 @@
// 更新现有账号
existingAccount.Username = accountInfo.Username;
existingAccount.IsActive = true;
existingAccount.ApiId = telegramApiId;
existingAccount.ApiHash = telegramApiHash;
existingAccount.LastSyncAt = DateTime.UtcNow;
await AccountManagement.UpdateAccountAsync(existingAccount);
@@ -224,9 +275,9 @@
Phone = accountInfo.Phone!,
UserId = accountInfo.TelegramUserId,
Username = accountInfo.Username,
SessionPath = $"sessions/{accountInfo.Phone}.session",
ApiId = int.Parse(apiId),
ApiHash = apiHash,
SessionPath = System.IO.Path.Combine(sessionsPath, $"{accountInfo.Phone}.session"),
ApiId = telegramApiId,
ApiHash = telegramApiHash,
IsActive = true,
CreatedAt = DateTime.UtcNow,
LastSyncAt = DateTime.UtcNow
@@ -241,7 +292,10 @@
userId = accountInfo.TelegramUserId;
// 跳转到完成步骤
stepper.Next();
if (stepper != null)
{
await stepper.NextStepAsync();
}
}
catch (Exception ex)
{
@@ -37,7 +37,7 @@
<MudTd DataLabel="分类">
@if (context.Category != null)
{
<MudChip Size="Size.Small" Style="@($"background-color: {context.Category.Color ?? "#9E9E9E"}")">
<MudChip T="string" Size="Size.Small" Style="@($"background-color: {context.Category.Color ?? "#9E9E9E"}")">
@context.Category.Name
</MudChip>
}
@@ -50,7 +50,7 @@
<MudText Typo="Typo.body2">@context.Channels.Count / @context.Groups.Count</MudText>
</MudTd>
<MudTd DataLabel="状态">
<MudChip Size="Size.Small" Color="@(context.IsActive ? Color.Success : Color.Default)">
<MudChip T="string" Size="Size.Small" Color="@(context.IsActive ? Color.Success : Color.Default)">
@(context.IsActive ? "活跃" : "停用")
</MudChip>
</MudTd>
@@ -115,18 +115,7 @@
private async Task ShowDetails(Account account)
{
var (channels, groups) = await AccountManagement.GetAccountStatisticsAsync(account.Id);
var message = $@"
账号详情:
手机号:{account.Phone}
用户名:{account.Username ?? "无"}
用户ID:{account.UserId}
分类:{account.Category?.Name ?? "未分类"}
创建的频道:{channels} 个
创建的群组:{groups} 个
创建时间:{account.CreatedAt:yyyy-MM-dd HH:mm}
最后同步:{account.LastSyncAt:yyyy-MM-dd HH:mm}
Session路径:{account.SessionPath}
";
var message = $"账号详情:\n手机号:{account.Phone}\n用户名:{account.Username ?? "无"}\n用户ID:{account.UserId}\n分类:{account.Category?.Name ?? "未分类"}\n创建的频道:{channels} 个\n创建的群组:{groups} 个\n创建时间:{account.CreatedAt:yyyy-MM-dd HH:mm}\n最后同步:{account.LastSyncAt:yyyy-MM-dd HH:mm}\nSession路径:{account.SessionPath}";
await DialogService.ShowMessageBox("账号详情", message, "关闭");
}
@@ -1,6 +1,11 @@
@page "/batch/admins"
@inject IChannelService ChannelService
@inject AccountManagementService AccountManagement
@inject ChannelManagementService ChannelManagement
@inject BatchTaskManagementService TaskManagement
@inject ISnackbar Snackbar
@using TelegramPanel.Data.Entities
@using TelegramPanel.Core.Interfaces
<PageTitle>批量设管理 - Telegram Panel</PageTitle>
@@ -15,14 +20,18 @@
<MudCardContent>
<MudSelect @bind-Value="selectedAccountId" Label="执行账号" Variant="Variant.Outlined" Required="true">
<MudSelectItem Value="0">-- 请选择账号 --</MudSelectItem>
<MudSelectItem Value="1">账号1 (+86xxxxxxxx)</MudSelectItem>
<MudSelectItem Value="2">账号2 (+86yyyyyyyy)</MudSelectItem>
@foreach (var account in accounts)
{
<MudSelectItem Value="@account.Id">@account.Phone (@account.Username)</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="selectedChannelId" Label="目标频道" Variant="Variant.Outlined" Required="true" Class="mt-4">
<MudSelectItem Value="0L">-- 请选择频道 --</MudSelectItem>
<MudSelectItem Value="1L">频道1</MudSelectItem>
<MudSelectItem Value="2L">频道2</MudSelectItem>
<MudSelectItem Value="0">-- 请选择频道 --</MudSelectItem>
@foreach (var channel in channels)
{
<MudSelectItem Value="@channel.Id">@channel.Title (@channel.Username)</MudSelectItem>
}
</MudSelect>
<MudTextField @bind-Value="adminTitle" Label="管理员头衔" Variant="Variant.Outlined" Class="mt-4"
@@ -40,8 +49,7 @@
<MudCheckBox @bind-Value="permissions.AddAdmins" Label="添加管理员" />
<MudTextField @bind-Value="usernames" Label="用户名列表" Variant="Variant.Outlined" Lines="8" Class="mt-4"
HelperText="每行一个用户名"
Placeholder="@user1&#10;user2&#10;@user3" />
HelperText="每行一个用户名" />
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Text" Color="Color.Secondary" OnClick="ClearForm">清空</MudButton>
@@ -69,16 +77,16 @@
<MudText Typo="Typo.h6">设置结果</MudText>
</MudCardHeader>
<MudCardContent>
<MudList Dense="true">
<MudList T="string" Dense="true">
@foreach (var result in results)
{
<MudListItem Icon="@(result.Success ? Icons.Material.Filled.CheckCircle : Icons.Material.Filled.Error)"
<MudListItem T="string" Icon="@(result.Success ? Icons.Material.Filled.CheckCircle : Icons.Material.Filled.Error)"
IconColor="@(result.Success ? Color.Success : Color.Error)">
<div>
<MudText Typo="Typo.body2">@("@" + result.Username)</MudText>
@if (!result.Success)
@if (!result.Success && result.Error != null)
{
<MudText Typo="Typo.caption" Color="Color.Error">@result.ErrorMessage</MudText>
<MudText Typo="Typo.caption" Color="Color.Error">@result.Error</MudText>
}
</div>
</MudListItem>
@@ -97,8 +105,10 @@
</MudGrid>
@code {
private List<Account> accounts = new();
private List<Channel> channels = new();
private int selectedAccountId = 0;
private long selectedChannelId = 0;
private int selectedChannelId = 0;
private string adminTitle = "Admin";
private string usernames = "";
private bool processing = false;
@@ -106,6 +116,38 @@
private AdminPermissions permissions = new();
private List<SetAdminResult> results = new();
protected override async Task OnInitializedAsync()
{
await LoadAccounts();
await LoadChannels();
}
private async Task LoadAccounts()
{
try
{
var accountList = await AccountManagement.GetActiveAccountsAsync();
accounts = accountList.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载账号列表失败:{ex.Message}", Severity.Error);
}
}
private async Task LoadChannels()
{
try
{
var channelList = await ChannelManagement.GetAllChannelsAsync();
channels = channelList.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载频道列表失败:{ex.Message}", Severity.Error);
}
}
private async Task StartSetAdmins()
{
processing = true;
@@ -113,13 +155,69 @@
try
{
// 解析用户名列表
var usernameList = usernames.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(u => u.Trim().TrimStart('@'))
.Where(u => !string.IsNullOrEmpty(u))
.ToList();
// TODO: 转换权限并调用 API
Snackbar.Add("批量设置管理员功能开发中", Severity.Info);
if (usernameList.Count == 0)
{
Snackbar.Add("请输入至少一个用户名", Severity.Warning);
return;
}
// 获取选中的频道
var channel = channels.FirstOrDefault(c => c.Id == selectedChannelId);
if (channel == null)
{
Snackbar.Add("未找到选中的频道", Severity.Error);
return;
}
// 转换权限
var rights = AdminRights.None;
if (permissions.ChangeInfo) rights |= AdminRights.ChangeInfo;
if (permissions.PostMessages) rights |= AdminRights.PostMessages;
if (permissions.EditMessages) rights |= AdminRights.EditMessages;
if (permissions.DeleteMessages) rights |= AdminRights.DeleteMessages;
if (permissions.BanUsers) rights |= AdminRights.BanUsers;
if (permissions.InviteUsers) rights |= AdminRights.InviteUsers;
if (permissions.PinMessages) rights |= AdminRights.PinMessages;
if (permissions.AddAdmins) rights |= AdminRights.AddAdmins;
// 创建批量任务记录
var task = new BatchTask
{
TaskType = "set_admin",
Total = usernameList.Count,
Completed = 0,
Failed = 0,
Config = System.Text.Json.JsonSerializer.Serialize(new
{
ChannelId = channel.TelegramId,
ChannelTitle = channel.Title,
Rights = rights.ToString(),
Title = adminTitle
})
};
var createdTask = await TaskManagement.CreateTaskAsync(task);
await TaskManagement.StartTaskAsync(createdTask.Id);
// 创建管理员请求列表
var adminRequests = usernameList.Select(u => new AdminRequest(u, rights, adminTitle)).ToList();
// 执行批量设置管理员
var setResults = await ChannelService.BatchSetAdminsAsync(selectedAccountId, channel.TelegramId, adminRequests);
results.AddRange(setResults);
// 更新任务状态
var successCount = setResults.Count(r => r.Success);
var failedCount = setResults.Count(r => !r.Success);
await TaskManagement.UpdateTaskProgressAsync(createdTask.Id, successCount, failedCount);
await TaskManagement.CompleteTaskAsync(createdTask.Id, true);
Snackbar.Add($"设置完成:{successCount}/{setResults.Count} 成功", successCount == setResults.Count ? Severity.Success : Severity.Warning);
}
catch (Exception ex)
{
@@ -1,6 +1,10 @@
@page "/batch/invite"
@inject IChannelService ChannelService
@inject AccountManagementService AccountManagement
@inject ChannelManagementService ChannelManagement
@inject BatchTaskManagementService TaskManagement
@inject ISnackbar Snackbar
@using TelegramPanel.Data.Entities
<PageTitle>批量邀请 - Telegram Panel</PageTitle>
@@ -15,19 +19,22 @@
<MudCardContent>
<MudSelect @bind-Value="selectedAccountId" Label="执行账号" Variant="Variant.Outlined" Required="true">
<MudSelectItem Value="0">-- 请选择账号 --</MudSelectItem>
<MudSelectItem Value="1">账号1 (+86xxxxxxxx)</MudSelectItem>
<MudSelectItem Value="2">账号2 (+86yyyyyyyy)</MudSelectItem>
@foreach (var account in accounts)
{
<MudSelectItem Value="@account.Id">@account.Phone (@account.Username)</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="selectedChannelId" Label="目标频道" Variant="Variant.Outlined" Required="true" Class="mt-4">
<MudSelectItem Value="0L">-- 请选择频道 --</MudSelectItem>
<MudSelectItem Value="1L">频道1</MudSelectItem>
<MudSelectItem Value="2L">频道2</MudSelectItem>
<MudSelectItem Value="0">-- 请选择频道 --</MudSelectItem>
@foreach (var channel in channels)
{
<MudSelectItem Value="@channel.Id">@channel.Title (@channel.Username)</MudSelectItem>
}
</MudSelect>
<MudTextField @bind-Value="usernames" Label="用户名列表" Variant="Variant.Outlined" Lines="10" Class="mt-4"
HelperText="每行一个用户名,支持 @username 或直接 username 格式"
Placeholder="@user1&#10;user2&#10;@user3" />
HelperText="每行一个用户名" />
<MudNumericField @bind-Value="delayMs" Label="邀请间隔 (毫秒)" Variant="Variant.Outlined" Min="1000" Max="10000" Class="mt-4"
HelperText="建议设置2000-5000ms,避免触发风控" />
@@ -58,16 +65,16 @@
<MudText Typo="Typo.h6">邀请结果</MudText>
</MudCardHeader>
<MudCardContent>
<MudList Dense="true">
<MudList T="string" Dense="true">
@foreach (var result in inviteResults)
{
<MudListItem Icon="@(result.Success ? Icons.Material.Filled.CheckCircle : Icons.Material.Filled.Error)"
<MudListItem T="string" Icon="@(result.Success ? Icons.Material.Filled.CheckCircle : Icons.Material.Filled.Error)"
IconColor="@(result.Success ? Color.Success : Color.Error)">
<div>
<MudText Typo="Typo.body2">@("@" + result.Username)</MudText>
@if (!result.Success)
@if (!result.Success && result.Error != null)
{
<MudText Typo="Typo.caption" Color="Color.Error">@result.ErrorMessage</MudText>
<MudText Typo="Typo.caption" Color="Color.Error">@result.Error</MudText>
}
</div>
</MudListItem>
@@ -86,14 +93,48 @@
</MudGrid>
@code {
private List<Account> accounts = new();
private List<Channel> channels = new();
private int selectedAccountId = 0;
private long selectedChannelId = 0;
private int selectedChannelId = 0;
private string usernames = "";
private int delayMs = 2000;
private bool inviting = false;
private List<InviteResult> inviteResults = new();
protected override async Task OnInitializedAsync()
{
await LoadAccounts();
await LoadChannels();
}
private async Task LoadAccounts()
{
try
{
var accountList = await AccountManagement.GetActiveAccountsAsync();
accounts = accountList.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载账号列表失败:{ex.Message}", Severity.Error);
}
}
private async Task LoadChannels()
{
try
{
var channelList = await ChannelManagement.GetAllChannelsAsync();
channels = channelList.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载频道列表失败:{ex.Message}", Severity.Error);
}
}
private async Task StartInvite()
{
inviting = true;
@@ -101,15 +142,53 @@
try
{
// 解析用户名列表
var usernameList = usernames.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(u => u.Trim().TrimStart('@'))
.Where(u => !string.IsNullOrEmpty(u))
.ToList();
var results = await ChannelService.BatchInviteUsersAsync(selectedAccountId, selectedChannelId, usernameList, delayMs);
if (usernameList.Count == 0)
{
Snackbar.Add("请输入至少一个用户名", Severity.Warning);
return;
}
// 获取选中的频道
var channel = channels.FirstOrDefault(c => c.Id == selectedChannelId);
if (channel == null)
{
Snackbar.Add("未找到选中的频道", Severity.Error);
return;
}
// 创建批量任务记录
var task = new BatchTask
{
TaskType = "invite",
Total = usernameList.Count,
Completed = 0,
Failed = 0,
Config = System.Text.Json.JsonSerializer.Serialize(new
{
ChannelId = channel.TelegramId,
ChannelTitle = channel.Title,
DelayMs = delayMs
})
};
var createdTask = await TaskManagement.CreateTaskAsync(task);
await TaskManagement.StartTaskAsync(createdTask.Id);
// 执行批量邀请
var results = await ChannelService.BatchInviteUsersAsync(selectedAccountId, channel.TelegramId, usernameList, delayMs);
inviteResults.AddRange(results);
// 更新任务状态
var successCount = results.Count(r => r.Success);
var failedCount = results.Count(r => !r.Success);
await TaskManagement.UpdateTaskProgressAsync(createdTask.Id, successCount, failedCount);
await TaskManagement.CompleteTaskAsync(createdTask.Id, true);
Snackbar.Add($"邀请完成:{successCount}/{results.Count} 成功", successCount == results.Count ? Severity.Success : Severity.Warning);
}
catch (Exception ex)
@@ -1,7 +1,10 @@
@page "/channels/create"
@inject IChannelService ChannelService
@inject AccountManagementService AccountManagement
@inject ChannelManagementService ChannelManagement
@inject ISnackbar Snackbar
@inject NavigationManager Navigation
@using TelegramPanel.Data.Entities
<PageTitle>创建频道 - Telegram Panel</PageTitle>
@@ -13,8 +16,10 @@
<MudCardContent>
<MudSelect @bind-Value="selectedAccountId" Label="选择账号" Variant="Variant.Outlined" Required="true">
<MudSelectItem Value="0">-- 请选择账号 --</MudSelectItem>
<MudSelectItem Value="1">账号1 (+86xxxxxxxx)</MudSelectItem>
<MudSelectItem Value="2">账号2 (+86yyyyyyyy)</MudSelectItem>
@foreach (var account in accounts)
{
<MudSelectItem Value="@account.Id">@account.Phone (@account.Username)</MudSelectItem>
}
</MudSelect>
<MudTextField @bind-Value="channelTitle" Label="频道名称" Variant="Variant.Outlined" Required="true" Class="mt-4"
@@ -28,8 +33,8 @@
@if (isPublic)
{
<MudTextField @bind-Value="username" Label="频道用户名" Variant="Variant.Outlined" Class="mt-3"
Adornment="Adornment.Start" AdornmentText="@" Required="true"
HelperText="5-32个字符,只能包含a-z、0-9和下划线" />
Adornment="Adornment.Start" AdornmentText='@("@")' Required
HelperText="5-32个字符,只能包含a-z、0-9和下划线" />
}
<MudDivider Class="my-4" />
@@ -63,6 +68,7 @@
</MudGrid>
@code {
private List<Account> accounts = new();
private int selectedAccountId = 0;
private string channelTitle = "";
private string channelAbout = "";
@@ -73,6 +79,24 @@
private bool signMessages = false;
private bool creating = false;
protected override async Task OnInitializedAsync()
{
await LoadAccounts();
}
private async Task LoadAccounts()
{
try
{
var accountList = await AccountManagement.GetActiveAccountsAsync();
accounts = accountList.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载账号列表失败:{ex.Message}", Severity.Error);
}
}
private async Task CreateChannel()
{
if (selectedAccountId == 0)
@@ -97,13 +121,32 @@
try
{
// 调用 Telegram API 创建频道
var channelInfo = await ChannelService.CreateChannelAsync(selectedAccountId, channelTitle, channelAbout, isPublic);
// 如果是公开频道,设置用户名
if (isPublic && !string.IsNullOrEmpty(username))
{
await ChannelService.SetChannelVisibilityAsync(selectedAccountId, channelInfo.TelegramId, true, username);
}
// 保存到数据库
var channel = new Channel
{
TelegramId = channelInfo.TelegramId,
AccessHash = channelInfo.AccessHash,
Title = channelTitle,
Username = isPublic ? username : null,
IsBroadcast = channelInfo.IsBroadcast,
MemberCount = channelInfo.MemberCount,
About = channelAbout,
CreatorAccountId = selectedAccountId,
CreatedAt = DateTime.UtcNow,
SyncedAt = DateTime.UtcNow
};
await ChannelManagement.CreateOrUpdateChannelAsync(channel);
Snackbar.Add($"频道 \"{channelTitle}\" 创建成功", Severity.Success);
Navigation.NavigateTo("/channels");
}
@@ -1,5 +1,8 @@
@page "/channels/groups"
@inject ChannelGroupManagementService GroupManagement
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@using TelegramPanel.Data.Entities
<PageTitle>频道分组 - Telegram Panel</PageTitle>
@@ -30,7 +33,7 @@
<MudText Typo="Typo.h6">分组列表</MudText>
</MudCardHeader>
<MudCardContent>
<MudTable Items="@groups" Hover="true">
<MudTable Items="@groups" Hover="true" Loading="@loading">
<HeaderContent>
<MudTh>分组名称</MudTh>
<MudTh>描述</MudTh>
@@ -39,14 +42,16 @@
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="分组名称">@context.Name</MudTd>
<MudTd DataLabel="描述">@context.Description</MudTd>
<MudTd DataLabel="频道数量">@context.ChannelCount</MudTd>
<MudTd DataLabel="描述">@(context.Description ?? "-")</MudTd>
<MudTd DataLabel="频道数量">@context.Channels.Count</MudTd>
<MudTd DataLabel="操作">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small" Color="Color.Info" />
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error"
OnClick="@(() => DeleteGroup(context.Id))" />
OnClick="@(() => DeleteGroup(context))" />
</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>暂无分组,请添加分组</MudText>
</NoRecordsContent>
</MudTable>
</MudCardContent>
</MudCard>
@@ -56,45 +61,75 @@
@code {
private string newGroupName = "";
private string newGroupDesc = "";
private List<ChannelGroup> groups = new();
private bool loading = true;
private List<GroupModel> groups = new()
protected override async Task OnInitializedAsync()
{
new GroupModel { Id = 1, Name = "推广频道", Description = "用于推广的频道", ChannelCount = 5 },
new GroupModel { Id = 2, Name = "资讯频道", Description = "资讯类频道", ChannelCount = 3 }
};
private void AddGroup()
{
var newGroup = new GroupModel
{
Id = groups.Max(g => g.Id) + 1,
Name = newGroupName,
Description = newGroupDesc,
ChannelCount = 0
};
groups.Add(newGroup);
Snackbar.Add($"分组 \"{newGroupName}\" 添加成功", Severity.Success);
newGroupName = "";
newGroupDesc = "";
await LoadGroups();
}
private void DeleteGroup(int id)
private async Task LoadGroups()
{
var group = groups.FirstOrDefault(g => g.Id == id);
if (group != null)
loading = true;
try
{
groups.Remove(group);
Snackbar.Add("分组已删除", Severity.Success);
var result = await GroupManagement.GetAllGroupsAsync();
groups = result.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载失败:{ex.Message}", Severity.Error);
}
finally
{
loading = false;
}
}
private class GroupModel
private async Task AddGroup()
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Description { get; set; } = "";
public int ChannelCount { get; set; }
try
{
var group = new ChannelGroup
{
Name = newGroupName,
Description = newGroupDesc,
CreatedAt = DateTime.UtcNow
};
await GroupManagement.CreateGroupAsync(group);
await LoadGroups();
Snackbar.Add($"分组 \"{newGroupName}\" 添加成功", Severity.Success);
newGroupName = "";
newGroupDesc = "";
}
catch (Exception ex)
{
Snackbar.Add($"添加失败:{ex.Message}", Severity.Error);
}
}
private async Task DeleteGroup(ChannelGroup group)
{
bool? result = await DialogService.ShowMessageBox(
"确认删除",
$"确定要删除分组 {group.Name} 吗?关联的频道将变为未分组。",
yesText: "删除", cancelText: "取消");
if (result == true)
{
try
{
await GroupManagement.DeleteGroupAsync(group.Id);
groups.Remove(group);
Snackbar.Add("删除成功", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"删除失败:{ex.Message}", Severity.Error);
}
}
}
}
@@ -1,7 +1,10 @@
@page "/channels"
@inject IChannelService ChannelService
@inject ChannelManagementService ChannelManagement
@inject AccountManagementService AccountManagement
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager Navigation
@using TelegramPanel.Data.Entities
<PageTitle>频道列表 - Telegram Panel</PageTitle>
@@ -9,19 +12,21 @@
<MudCard>
<MudCardContent>
<MudTable Items="@channels" Hover="true" Breakpoint="Breakpoint.Sm" Loading="@loading" Filter="new Func<ChannelInfo,bool>(FilterFunc)">
<MudTable Items="@channels" Hover="true" Breakpoint="Breakpoint.Sm" Loading="@loading" Filter="FilterFunc">
<ToolBarContent>
<MudText Typo="Typo.h6">我的频道</MudText>
<MudText Typo="Typo.h6">我的频道 (@channels.Count)</MudText>
<MudSpacer />
<MudSelect @bind-Value="filterAccount" Label="筛选账号" Variant="Variant.Outlined" Dense="true" Class="mr-2" Style="min-width: 200px;">
<MudSelectItem Value="0">全部账号</MudSelectItem>
<MudSelectItem Value="1">账号1 (+86xxx)</MudSelectItem>
<MudSelectItem Value="2">账号2 (+86yyy)</MudSelectItem>
@foreach (var account in accounts)
{
<MudSelectItem Value="@account.Id">@account.Phone (@account.Username)</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="filterType" Label="频道类型" Variant="Variant.Outlined" Dense="true" Class="mr-2" Style="min-width: 120px;">
<MudSelectItem Value="all">全部</MudSelectItem>
<MudSelectItem Value="public">公开</MudSelectItem>
<MudSelectItem Value="private">私密</MudSelectItem>
<MudSelectItem Value='@("all")'>全部</MudSelectItem>
<MudSelectItem Value='@("public")'>公开</MudSelectItem>
<MudSelectItem Value='@("private")'>私密</MudSelectItem>
</MudSelect>
<MudTextField @bind-Value="searchString" Placeholder="搜索频道..." Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0" />
@@ -43,7 +48,7 @@
<MudText Typo="Typo.body2"><strong>@context.Title</strong></MudText>
@if (!string.IsNullOrEmpty(context.About))
{
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.About</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@(context.About.Length > 50 ? context.About.Substring(0, 50) + "..." : context.About)</MudText>
}
</div>
</div>
@@ -51,7 +56,7 @@
<MudTd DataLabel="用户名">
@if (!string.IsNullOrEmpty(context.Username))
{
<MudChip Size="Size.Small" Icon="@Icons.Material.Filled.AlternateEmail">@context.Username</MudChip>
<MudChip T="string" Size="Size.Small" Icon="@Icons.Material.Filled.AlternateEmail">@context.Username</MudChip>
}
else
{
@@ -59,12 +64,21 @@
}
</MudTd>
<MudTd DataLabel="类型">
<MudChip Size="Size.Small" Color="@(string.IsNullOrEmpty(context.Username) ? Color.Warning : Color.Success)">
<MudChip T="string" Size="Size.Small" Color="@(string.IsNullOrEmpty(context.Username) ? Color.Warning : Color.Success)">
@(string.IsNullOrEmpty(context.Username) ? "私密" : "公开")
</MudChip>
</MudTd>
<MudTd DataLabel="成员数">@context.MemberCount</MudTd>
<MudTd DataLabel="创建账号">账号 @context.CreatorAccountId</MudTd>
<MudTd DataLabel="创建账号">
@if (context.CreatorAccount != null)
{
<span>@context.CreatorAccount.Phone</span>
}
else
{
<span>账号 @context.CreatorAccountId</span>
}
</MudTd>
<MudTd DataLabel="最后同步">@context.SyncedAt.ToString("yyyy-MM-dd HH:mm")</MudTd>
<MudTd DataLabel="操作">
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Size="Size.Small">
@@ -88,7 +102,8 @@
</MudCard>
@code {
private List<ChannelInfo> channels = new();
private List<Channel> channels = new();
private List<Account> accounts = new();
private bool loading = true;
private string searchString = "";
private int filterAccount = 0;
@@ -96,17 +111,30 @@
protected override async Task OnInitializedAsync()
{
await LoadAccounts();
await LoadChannels();
}
private async Task LoadAccounts()
{
try
{
var accountList = await AccountManagement.GetAllAccountsAsync();
accounts = accountList.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载账号列表失败:{ex.Message}", Severity.Error);
}
}
private async Task LoadChannels()
{
loading = true;
try
{
// TODO: 从数据库加载频道列表
channels = new List<ChannelInfo>();
Snackbar.Add("频道列表加载成功", Severity.Success);
var channelList = await ChannelManagement.GetAllChannelsAsync();
channels = channelList.ToList();
}
catch (Exception ex)
{
@@ -118,7 +146,7 @@
}
}
private bool FilterFunc(ChannelInfo channel)
private bool FilterFunc(Channel channel)
{
if (filterAccount > 0 && channel.CreatorAccountId != filterAccount)
return false;
@@ -141,22 +169,22 @@
return false;
}
private void ShowDetails(ChannelInfo channel)
private void ShowDetails(Channel channel)
{
Snackbar.Add($"查看频道详情:{channel.Title}", Severity.Info);
}
private void EditChannel(ChannelInfo channel)
private void EditChannel(Channel channel)
{
Snackbar.Add($"编辑频道:{channel.Title}", Severity.Info);
}
private void InviteUsers(ChannelInfo channel)
private void InviteUsers(Channel channel)
{
Navigation.NavigateTo($"/batch/invite?channelId={channel.Id}");
}
private void CopyLink(ChannelInfo channel)
private void CopyLink(Channel channel)
{
var link = string.IsNullOrEmpty(channel.Username)
? $"私密频道(邀请链接待实现)"
@@ -166,6 +194,26 @@
private async Task DeleteChannel(int id)
{
Snackbar.Add("删除功能开发中", Severity.Warning);
var channel = channels.FirstOrDefault(c => c.Id == id);
if (channel == null) return;
bool? result = await DialogService.ShowMessageBox(
"确认删除",
$"确定要删除频道 {channel.Title} 吗?此操作不可恢复。",
yesText: "删除", cancelText: "取消");
if (result == true)
{
try
{
await ChannelManagement.DeleteChannelAsync(id);
channels.Remove(channel);
Snackbar.Add("删除成功", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"删除失败:{ex.Message}", Severity.Error);
}
}
}
}
@@ -1,6 +1,10 @@
@page "/groups"
@inject IGroupService GroupService
@inject GroupManagementService GroupManagement
@inject AccountManagementService AccountManagement
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager Navigation
@using TelegramPanel.Data.Entities
<PageTitle>群组列表 - Telegram Panel</PageTitle>
@@ -8,10 +12,17 @@
<MudCard>
<MudCardContent>
<MudTable Items="@groups" Hover="true" Breakpoint="Breakpoint.Sm" Loading="@loading">
<MudTable Items="@groups" Hover="true" Breakpoint="Breakpoint.Sm" Loading="@loading" Filter="new Func<Group,bool>(FilterFunc)">
<ToolBarContent>
<MudText Typo="Typo.h6">我的群组</MudText>
<MudText Typo="Typo.h6">我的群组 (@groups.Count)</MudText>
<MudSpacer />
<MudSelect @bind-Value="filterAccount" Label="筛选账号" Variant="Variant.Outlined" Dense="true" Class="mr-2" Style="min-width: 200px;">
<MudSelectItem Value="0">全部账号</MudSelectItem>
@foreach (var account in accounts)
{
<MudSelectItem Value="@account.Id">@account.Phone (@account.Username)</MudSelectItem>
}
</MudSelect>
<MudTextField @bind-Value="searchString" Placeholder="搜索群组..." Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0" />
</ToolBarContent>
@@ -27,13 +38,19 @@
<MudTd DataLabel="群组名称">
<div class="d-flex align-center">
<MudAvatar Color="Color.Secondary" Size="Size.Small" Class="mr-2">@context.Title[0]</MudAvatar>
<MudText Typo="Typo.body2"><strong>@context.Title</strong></MudText>
<div>
<MudText Typo="Typo.body2"><strong>@context.Title</strong></MudText>
@if (!string.IsNullOrEmpty(context.About))
{
<MudText Typo="Typo.caption" Color="Color.Secondary">@(context.About.Length > 50 ? context.About.Substring(0, 50) + "..." : context.About)</MudText>
}
</div>
</div>
</MudTd>
<MudTd DataLabel="用户名">
@if (!string.IsNullOrEmpty(context.Username))
{
<MudChip Size="Size.Small" Icon="@Icons.Material.Filled.AlternateEmail">@context.Username</MudChip>
<MudChip T="string" Size="Size.Small" Icon="@Icons.Material.Filled.AlternateEmail">@context.Username</MudChip>
}
else
{
@@ -41,15 +58,24 @@
}
</MudTd>
<MudTd DataLabel="成员数">@context.MemberCount</MudTd>
<MudTd DataLabel="创建账号">账号 @context.CreatorAccountId</MudTd>
<MudTd DataLabel="创建账号">
@if (context.CreatorAccount != null)
{
<span>@context.CreatorAccount.Phone</span>
}
else
{
<span>账号 @context.CreatorAccountId</span>
}
</MudTd>
<MudTd DataLabel="最后同步">@context.SyncedAt.ToString("yyyy-MM-dd HH:mm")</MudTd>
<MudTd DataLabel="操作">
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Size="Size.Small">
<MudMenuItem Icon="@Icons.Material.Filled.Info">查看详情</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.PersonAdd">邀请成员</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Link">复制链接</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Info" OnClick="@(() => ShowDetails(context))">查看详情</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.PersonAdd" OnClick="@(() => InviteMembers(context))">邀请成员</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(() => CopyLink(context))">复制链接</MudMenuItem>
<MudDivider />
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error">删除群组</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="@(() => DeleteGroup(context.Id))">删除群组</MudMenuItem>
</MudMenu>
</MudTd>
</RowTemplate>
@@ -64,23 +90,38 @@
</MudCard>
@code {
private List<GroupInfo> groups = new();
private List<Group> groups = new();
private List<Account> accounts = new();
private bool loading = true;
private string searchString = "";
private int filterAccount = 0;
protected override async Task OnInitializedAsync()
{
await LoadAccounts();
await LoadGroups();
}
private async Task LoadAccounts()
{
try
{
var accountList = await AccountManagement.GetAllAccountsAsync();
accounts = accountList.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载账号列表失败:{ex.Message}", Severity.Error);
}
}
private async Task LoadGroups()
{
loading = true;
try
{
// TODO: 从数据库加载群组列表
groups = new List<GroupInfo>();
Snackbar.Add("群组列表加载成功", Severity.Success);
var groupList = await GroupManagement.GetAllGroupsAsync();
groups = groupList.ToList();
}
catch (Exception ex)
{
@@ -91,4 +132,64 @@
loading = false;
}
}
private bool FilterFunc(Group group)
{
if (filterAccount > 0 && group.CreatorAccountId != filterAccount)
return false;
if (string.IsNullOrWhiteSpace(searchString))
return true;
if (group.Title.Contains(searchString, StringComparison.OrdinalIgnoreCase))
return true;
if (group.Username?.Contains(searchString, StringComparison.OrdinalIgnoreCase) == true)
return true;
return false;
}
private void ShowDetails(Group group)
{
Snackbar.Add($"查看群组详情:{group.Title}", Severity.Info);
}
private void InviteMembers(Group group)
{
Navigation.NavigateTo($"/batch/invite?groupId={group.Id}");
}
private void CopyLink(Group group)
{
var link = string.IsNullOrEmpty(group.Username)
? $"私密群组(邀请链接待实现)"
: $"https://t.me/{group.Username}";
Snackbar.Add($"链接:{link}", Severity.Info);
}
private async Task DeleteGroup(int id)
{
var group = groups.FirstOrDefault(g => g.Id == id);
if (group == null) return;
bool? result = await DialogService.ShowMessageBox(
"确认删除",
$"确定要删除群组 {group.Title} 吗?此操作不可恢复。",
yesText: "删除", cancelText: "取消");
if (result == true)
{
try
{
await GroupManagement.DeleteGroupAsync(id);
groups.Remove(group);
Snackbar.Add("删除成功", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"删除失败:{ex.Message}", Severity.Error);
}
}
}
}
@@ -1,5 +1,13 @@
@page "/settings"
@inject IConfiguration Configuration
@inject AccountManagementService AccountManagement
@inject ChannelManagementService ChannelManagement
@inject GroupManagementService GroupManagement
@inject IChannelService ChannelService
@inject IGroupService GroupService
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject IWebHostEnvironment Environment
<PageTitle>系统设置 - Telegram Panel</PageTitle>
@@ -16,9 +24,15 @@
HelperText="从 https://my.telegram.org 获取" />
<MudTextField @bind-Value="apiHash" Label="默认 API Hash" Variant="Variant.Outlined" Class="mt-3"
HelperText="用于新账号登录和导入" />
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true" Class="mt-3">
<MudText Typo="Typo.body2"><strong>写入位置:</strong>@LocalConfigPath</MudText>
<MudText Typo="Typo.body2" Class="mt-1"><strong>文件存在:</strong>@(LocalConfigExists ? "是" : "否")</MudText>
<MudText Typo="Typo.body2" Class="mt-1"><strong>当前生效 ApiId:</strong>@(Configuration["Telegram:ApiId"] ?? "(未配置)")</MudText>
</MudAlert>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveApiConfig">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="@(async (MouseEventArgs _) => await SaveApiConfig())">
保存配置
</MudButton>
</MudCardActions>
@@ -41,7 +55,8 @@
}
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveBatchConfig">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="@(async (MouseEventArgs _) => await SaveBatchConfig())">
保存配置
</MudButton>
</MudCardActions>
@@ -61,7 +76,7 @@
Min="1" Max="24" />
}
<MudButton Variant="Variant.Outlined" Color="Color.Info" FullWidth="true" Class="mt-4"
OnClick="SyncNow">
OnClick="@(async (MouseEventArgs _) => await SyncNow())">
立即同步
</MudButton>
</MudCardContent>
@@ -73,16 +88,17 @@
</MudCardHeader>
<MudCardContent>
<MudSelect @bind-Value="logLevel" Label="日志级别" Variant="Variant.Outlined">
<MudSelectItem Value="Debug">Debug</MudSelectItem>
<MudSelectItem Value="Information">Information</MudSelectItem>
<MudSelectItem Value="Warning">Warning</MudSelectItem>
<MudSelectItem Value="Error">Error</MudSelectItem>
<MudSelectItem Value='@("Debug")'>Debug</MudSelectItem>
<MudSelectItem Value='@("Information")'>Information</MudSelectItem>
<MudSelectItem Value='@("Warning")'>Warning</MudSelectItem>
<MudSelectItem Value='@("Error")'>Error</MudSelectItem>
</MudSelect>
<MudNumericField @bind-Value="logRetentionDays" Label="日志保留天数" Variant="Variant.Outlined" Class="mt-3"
Min="1" Max="90" />
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveLogConfig">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="@(async (MouseEventArgs _) => await SaveLogConfig())">
保存配置
</MudButton>
</MudCardActions>
@@ -98,7 +114,8 @@
<MudText Typo="Typo.body2" Class="mt-2"><strong>数据库:</strong> SQLite</MudText>
<MudDivider Class="my-3" />
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" FullWidth="true"
StartIcon="@Icons.Material.Filled.Delete" OnClick="ClearCache">
StartIcon="@Icons.Material.Filled.Delete"
OnClick="@(async (MouseEventArgs _) => await ClearCache())">
清除缓存
</MudButton>
</MudCardContent>
@@ -117,31 +134,168 @@
private int syncInterval = 6;
private string logLevel = "Information";
private int logRetentionDays = 30;
private bool syncing = false;
private void SaveApiConfig()
private string LocalConfigPath => System.IO.Path.Combine(Environment.ContentRootPath, "appsettings.local.json");
private bool LocalConfigExists => System.IO.File.Exists(LocalConfigPath);
protected override void OnInitialized()
{
Snackbar.Add("API 配置已保存", Severity.Success);
// 从配置文件加载默认设置
apiId = Configuration["Telegram:ApiId"] ?? "";
apiHash = Configuration["Telegram:ApiHash"] ?? "";
}
private void SaveBatchConfig()
private async Task SaveApiConfig()
{
if (!int.TryParse(apiId, out int parsedApiId) || parsedApiId <= 0)
{
Snackbar.Add("默认 API ID 格式错误", Severity.Error);
return;
}
if (string.IsNullOrWhiteSpace(apiHash))
{
Snackbar.Add("默认 API Hash 不能为空", Severity.Error);
return;
}
var localSettings = new
{
Telegram = new
{
ApiId = parsedApiId,
ApiHash = apiHash.Trim()
}
};
try
{
var localConfigPath = LocalConfigPath;
var json = System.Text.Json.JsonSerializer.Serialize(
localSettings,
new System.Text.Json.JsonSerializerOptions { WriteIndented = true }
);
await System.IO.File.WriteAllTextAsync(
localConfigPath,
json,
new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false)
);
var fileInfo = new System.IO.FileInfo(localConfigPath);
Snackbar.Add($"API 配置已保存:{localConfigPath}({fileInfo.Length} bytes)。若仍提示未配置,请刷新页面或重启应用。", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"保存失败:{ex.Message}(路径:{LocalConfigPath})", Severity.Error);
}
}
private Task SaveBatchConfig()
{
// TODO: 实现配置保存到文件
Snackbar.Add("批量操作配置已保存", Severity.Success);
return Task.CompletedTask;
}
private void SaveLogConfig()
private Task SaveLogConfig()
{
// TODO: 实现配置保存到文件
Snackbar.Add("日志配置已保存", Severity.Success);
return Task.CompletedTask;
}
private async Task SyncNow()
{
Snackbar.Add("开始同步...", Severity.Info);
await Task.Delay(2000);
Snackbar.Add("同步完成", Severity.Success);
if (syncing)
{
Snackbar.Add("正在同步中,请稍候...", Severity.Warning);
return;
}
syncing = true;
Snackbar.Add("开始同步频道和群组信息...", Severity.Info);
try
{
var accounts = await AccountManagement.GetActiveAccountsAsync();
int totalChannelsSynced = 0;
int totalGroupsSynced = 0;
foreach (var account in accounts)
{
try
{
// 同步频道
var channels = await ChannelService.GetOwnedChannelsAsync(account.Id);
foreach (var channelInfo in channels)
{
var channel = new Data.Entities.Channel
{
TelegramId = channelInfo.TelegramId,
AccessHash = channelInfo.AccessHash,
Title = channelInfo.Title,
Username = channelInfo.Username,
IsBroadcast = channelInfo.IsBroadcast,
MemberCount = channelInfo.MemberCount,
About = channelInfo.About,
CreatorAccountId = account.Id
};
await ChannelManagement.CreateOrUpdateChannelAsync(channel);
totalChannelsSynced++;
}
// 同步群组
var groups = await GroupService.GetOwnedGroupsAsync(account.Id);
foreach (var groupInfo in groups)
{
var group = new Data.Entities.Group
{
TelegramId = groupInfo.TelegramId,
AccessHash = groupInfo.AccessHash,
Title = groupInfo.Title,
Username = groupInfo.Username,
MemberCount = groupInfo.MemberCount,
About = null,
CreatorAccountId = account.Id
};
await GroupManagement.CreateOrUpdateGroupAsync(group);
totalGroupsSynced++;
}
// 更新账号的最后同步时间
await AccountManagement.UpdateLastSyncTimeAsync(account.Id);
}
catch (Exception ex)
{
Snackbar.Add($"账号 {account.Phone} 同步失败:{ex.Message}", Severity.Warning);
}
}
Snackbar.Add($"同步完成:{totalChannelsSynced} 个频道,{totalGroupsSynced} 个群组", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"同步失败:{ex.Message}", Severity.Error);
}
finally
{
syncing = false;
}
}
private void ClearCache()
private async Task ClearCache()
{
Snackbar.Add("缓存已清除", Severity.Success);
bool? result = await DialogService.ShowMessageBox(
"确认清除",
"确定要清除缓存吗?这将清除所有临时数据。",
yesText: "清除", cancelText: "取消");
if (result == true)
{
// TODO: 实现缓存清除逻辑
Snackbar.Add("缓存已清除", Severity.Success);
}
}
}
@@ -1,5 +1,8 @@
@page "/tasks"
@inject BatchTaskManagementService TaskManagement
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@using TelegramPanel.Data.Entities
<PageTitle>任务中心 - Telegram Panel</PageTitle>
@@ -7,49 +10,63 @@
<MudCard>
<MudCardContent>
<MudTable Items="@tasks" Hover="true" Breakpoint="Breakpoint.Sm">
<MudTable Items="@filteredTasks" Hover="true" Breakpoint="Breakpoint.Sm" Loading="@loading">
<ToolBarContent>
<MudText Typo="Typo.h6">任务列表</MudText>
<MudText Typo="Typo.h6">任务列表 (@tasks.Count)</MudText>
<MudSpacer />
<MudSelect @bind-Value="filterStatus" Label="状态筛选" Variant="Variant.Outlined" Dense="true" Style="min-width: 120px;">
<MudSelectItem Value="all">全部</MudSelectItem>
<MudSelectItem Value="pending">待执行</MudSelectItem>
<MudSelectItem Value="running">执行中</MudSelectItem>
<MudSelectItem Value="completed">已完成</MudSelectItem>
<MudSelectItem Value="failed">失败</MudSelectItem>
<MudSelectItem Value='@("all")'>全部</MudSelectItem>
<MudSelectItem Value='@("pending")'>待执行</MudSelectItem>
<MudSelectItem Value='@("running")'>执行中</MudSelectItem>
<MudSelectItem Value='@("completed")'>已完成</MudSelectItem>
<MudSelectItem Value='@("failed")'>失败</MudSelectItem>
</MudSelect>
</ToolBarContent>
<HeaderContent>
<MudTh>任务ID</MudTh>
<MudTh>任务类型</MudTh>
<MudTh>目标</MudTh>
<MudTh>状态</MudTh>
<MudTh>进度</MudTh>
<MudTh>创建时间</MudTh>
<MudTh>完成时间</MudTh>
<MudTh>操作</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="任务ID">@context.Id</MudTd>
<MudTd DataLabel="任务类型">
<MudChip Size="Size.Small" Icon="@GetTaskIcon(context.Type)">@context.Type</MudChip>
<MudChip T="string" Size="Size.Small" Icon="@GetTaskIcon(context.TaskType)">@GetTaskTypeName(context.TaskType)</MudChip>
</MudTd>
<MudTd DataLabel="目标">@context.Target</MudTd>
<MudTd DataLabel="状态">
<MudChip Size="Size.Small" Color="@GetStatusColor(context.Status)">@context.Status</MudChip>
<MudChip T="string" Size="Size.Small" Color="@GetStatusColor(context.Status)">@GetStatusName(context.Status)</MudChip>
</MudTd>
<MudTd DataLabel="进度">
<MudProgressLinear Value="@context.Progress" Color="Color.Primary" />
<MudText Typo="Typo.caption">@context.Progress%</MudText>
<MudProgressLinear Value="@GetProgress(context)" Color="@GetProgressColor(context.Status)" />
<MudText Typo="Typo.caption">@context.Completed / @context.Total (@context.Failed 失败)</MudText>
</MudTd>
<MudTd DataLabel="创建时间">@context.CreatedAt.ToString("yyyy-MM-dd HH:mm")</MudTd>
<MudTd DataLabel="完成时间">
@if (context.CompletedAt.HasValue)
{
@context.CompletedAt.Value.ToString("yyyy-MM-dd HH:mm")
}
else
{
<span>-</span>
}
</MudTd>
<MudTd DataLabel="操作">
<MudIconButton Icon="@Icons.Material.Filled.Info" Size="Size.Small" Color="Color.Info"
OnClick="@(() => ShowTaskDetails(context))" />
@if (context.Status == "待执行" || context.Status == "执行中")
@if (context.Status == "pending" || context.Status == "running")
{
<MudIconButton Icon="@Icons.Material.Filled.Cancel" Size="Size.Small" Color="Color.Warning"
OnClick="@(() => CancelTask(context.Id))" />
}
@if (context.Status == "completed" || context.Status == "failed")
{
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error"
OnClick="@(() => DeleteTask(context.Id))" />
}
</MudTd>
</RowTemplate>
<NoRecordsContent>
@@ -60,48 +77,144 @@
</MudCard>
@code {
private List<BatchTask> tasks = new();
private bool loading = true;
private string filterStatus = "all";
private List<TaskModel> tasks = new()
private IEnumerable<BatchTask> filteredTasks =>
filterStatus == "all" ? tasks : tasks.Where(t => t.Status == filterStatus);
protected override async Task OnInitializedAsync()
{
new TaskModel { Id = 1, Type = "批量邀请", Target = "频道1", Status = "已完成", Progress = 100, CreatedAt = DateTime.Now.AddHours(-2) },
new TaskModel { Id = 2, Type = "设置管理", Target = "频道2", Status = "执行中", Progress = 60, CreatedAt = DateTime.Now.AddMinutes(-30) },
new TaskModel { Id = 3, Type = "批量邀请", Target = "频道3", Status = "待执行", Progress = 0, CreatedAt = DateTime.Now.AddMinutes(-5) }
};
await LoadTasks();
}
private async Task LoadTasks()
{
loading = true;
try
{
var taskList = await TaskManagement.GetRecentTasksAsync(50);
tasks = taskList.OrderByDescending(t => t.CreatedAt).ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载失败:{ex.Message}", Severity.Error);
}
finally
{
loading = false;
}
}
private double GetProgress(BatchTask task)
{
if (task.Total == 0) return 0;
return (double)task.Completed / task.Total * 100;
}
private string GetTaskIcon(string type) => type switch
{
"批量邀请" => Icons.Material.Filled.PersonAdd,
"设置管理" => Icons.Material.Filled.AdminPanelSettings,
"invite" => Icons.Material.Filled.PersonAdd,
"set_admin" => Icons.Material.Filled.AdminPanelSettings,
_ => Icons.Material.Filled.Task
};
private string GetTaskTypeName(string type) => type switch
{
"invite" => "批量邀请",
"set_admin" => "设置管理员",
_ => type
};
private string GetStatusName(string status) => status switch
{
"pending" => "待执行",
"running" => "执行中",
"completed" => "已完成",
"failed" => "失败",
_ => status
};
private Color GetStatusColor(string status) => status switch
{
"已完成" => Color.Success,
"执行中" => Color.Info,
"待执行" => Color.Default,
"失败" => Color.Error,
"completed" => Color.Success,
"running" => Color.Info,
"pending" => Color.Default,
"failed" => Color.Error,
_ => Color.Default
};
private void ShowTaskDetails(TaskModel task)
private Color GetProgressColor(string status) => status switch
{
Snackbar.Add($"查看任务详情:{task.Type} - {task.Target}", Severity.Info);
"completed" => Color.Success,
"running" => Color.Primary,
"failed" => Color.Error,
_ => Color.Default
};
private void ShowTaskDetails(BatchTask task)
{
var details = $"任务类型: {GetTaskTypeName(task.TaskType)}\n" +
$"状态: {GetStatusName(task.Status)}\n" +
$"总数: {task.Total}\n" +
$"已完成: {task.Completed}\n" +
$"失败: {task.Failed}\n" +
$"创建时间: {task.CreatedAt:yyyy-MM-dd HH:mm}\n";
if (task.StartedAt.HasValue)
details += $"开始时间: {task.StartedAt.Value:yyyy-MM-dd HH:mm}\n";
if (task.CompletedAt.HasValue)
details += $"完成时间: {task.CompletedAt.Value:yyyy-MM-dd HH:mm}\n";
if (!string.IsNullOrEmpty(task.Config))
details += $"\n配置信息:\n{task.Config}";
DialogService.ShowMessageBox("任务详情", details);
}
private void CancelTask(int id)
private async Task CancelTask(int id)
{
Snackbar.Add($"取消任务 #{id}", Severity.Warning);
bool? result = await DialogService.ShowMessageBox(
"确认取消",
"确定要取消这个任务吗?",
yesText: "取消任务", cancelText: "返回");
if (result == true)
{
try
{
await TaskManagement.CompleteTaskAsync(id, false);
await LoadTasks();
Snackbar.Add("任务已取消", Severity.Warning);
}
catch (Exception ex)
{
Snackbar.Add($"取消失败:{ex.Message}", Severity.Error);
}
}
}
private class TaskModel
private async Task DeleteTask(int id)
{
public int Id { get; set; }
public string Type { get; set; } = "";
public string Target { get; set; } = "";
public string Status { get; set; } = "";
public int Progress { get; set; }
public DateTime CreatedAt { get; set; }
bool? result = await DialogService.ShowMessageBox(
"确认删除",
"确定要删除这个任务记录吗?此操作不可恢复。",
yesText: "删除", cancelText: "取消");
if (result == true)
{
try
{
await TaskManagement.DeleteTaskAsync(id);
tasks.RemoveAll(t => t.Id == id);
Snackbar.Add("任务已删除", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"删除失败:{ex.Message}", Severity.Error);
}
}
}
}
@@ -0,0 +1,17 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Hosting
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using MudBlazor
@using MudBlazor.Utilities
@using TelegramPanel.Core.Interfaces
@using TelegramPanel.Core.Services
@using TelegramPanel.Core.Services.Telegram
@using TelegramPanel.Web.Components
@using TelegramPanel.Web.Components.Layout
+112 -1
View File
@@ -1,10 +1,14 @@
using MudBlazor.Services;
using Microsoft.EntityFrameworkCore;
using Serilog;
using TelegramPanel.Core;
using TelegramPanel.Data;
var builder = WebApplication.CreateBuilder(args);
// 可选的本地覆盖配置(不要提交到仓库)
builder.Configuration.AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true);
// 配置 Serilog
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
@@ -23,8 +27,30 @@ builder.Services.AddRazorComponents()
builder.Services.AddMudServices();
// 数据库上下文
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
var configuredConnectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? "Data Source=telegram-panel.db";
// 统一 SQLite 数据库路径为 ContentRoot 下的文件,避免因工作目录不同导致连到错误的 db(从而出现 no such table)
var connectionString = configuredConnectionString;
try
{
var dataSourcePrefix = "Data Source=";
var trimmed = configuredConnectionString.Trim();
if (trimmed.StartsWith(dataSourcePrefix, StringComparison.OrdinalIgnoreCase))
{
var dataSource = trimmed.Substring(dataSourcePrefix.Length).Trim().Trim('"');
if (!Path.IsPathRooted(dataSource))
{
var absolute = Path.Combine(builder.Environment.ContentRootPath, dataSource);
connectionString = $"{dataSourcePrefix}{absolute}";
}
}
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to normalize sqlite connection string, using configured value");
connectionString = configuredConnectionString;
}
builder.Services.AddTelegramPanelData(connectionString);
// Telegram Panel 核心服务
@@ -36,6 +62,91 @@ builder.Services.AddTelegramPanelCore();
var app = builder.Build();
// 确保数据库已创建并应用最新迁移
// 注意:SQLite 在首次连接时只会创建空文件,不会自动建表,因此需要显式执行迁移。
using (var scope = app.Services.CreateScope())
{
try
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
Log.Information("Using sqlite connection string: {ConnectionString}", connectionString);
var migrations = db.Database.GetMigrations().ToList();
Log.Information("EF migrations discovered: {Count}", migrations.Count);
var conn = db.Database.GetDbConnection();
if (conn.State != System.Data.ConnectionState.Open)
conn.Open();
List<string> tables;
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;";
using var reader = cmd.ExecuteReader();
tables = new List<string>();
while (reader.Read())
tables.Add(reader.GetString(0));
}
var hasHistory = tables.Contains("__EFMigrationsHistory", StringComparer.Ordinal);
var hasAnyUserTables = tables.Any(t =>
!string.Equals(t, "__EFMigrationsHistory", StringComparison.Ordinal) &&
!t.StartsWith("sqlite_", StringComparison.OrdinalIgnoreCase));
if (migrations.Count > 0)
{
// 只在「新库」或「已有迁移历史」时执行 Migrate;避免对已有表但无历史的库误执行迁移导致冲突
if (!hasAnyUserTables || hasHistory)
{
db.Database.Migrate();
}
else
{
Log.Warning("Database has schema tables but no __EFMigrationsHistory; skipping Migrate() to avoid conflicts");
}
}
else
{
db.Database.EnsureCreated();
}
// 刷新表清单(上面可能已创建/迁移)
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;";
using var reader = cmd.ExecuteReader();
tables = new List<string>();
while (reader.Read())
tables.Add(reader.GetString(0));
}
// 兜底:若 Accounts 仍不存在(常见于库被创建为空文件/仅有历史表),给出可恢复的自愈
if (!tables.Contains("Accounts", StringComparer.Ordinal))
{
hasHistory = tables.Contains("__EFMigrationsHistory", StringComparer.Ordinal);
if (tables.Count == 0)
{
Log.Warning("Database has no tables; calling EnsureCreated()");
db.Database.EnsureCreated();
}
else if (tables.Count == 1 && hasHistory)
{
Log.Warning("Database only contains __EFMigrationsHistory but no schema tables; recreating schema via EnsureCreated()");
db.Database.ExecuteSqlRaw("DROP TABLE IF EXISTS __EFMigrationsHistory;");
db.Database.EnsureCreated();
}
else
{
Log.Error("Accounts table missing. Existing tables: {Tables}", string.Join(", ", tables));
}
}
}
catch (Exception ex)
{
Log.Error(ex, "Database migration failed");
throw;
}
}
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
+5
View File
@@ -1,5 +1,6 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@@ -7,5 +8,9 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using MudBlazor
@using MudBlazor.Utilities
@using TelegramPanel.Core.Interfaces
@using TelegramPanel.Core.Services
@using TelegramPanel.Core.Services.Telegram
@using TelegramPanel.Web.Components
@using TelegramPanel.Web.Components.Layout