fix: 全局Telegram API配置+DB初始化+Razor修复
This commit is contained in:
1 parent
28546a614b
commit
b4fd6dde00
21 files changed
+1171
-272
No files matched your search
@@ -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/
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 user2 @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 user2 @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
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user