连接前端页面与后端服务(第一部分)

本次更新内容:
- 完善Accounts账号列表页面,连接AccountManagementService
  - 实现真实数据加载、搜索过滤、启用/停用、删除等功能
  - 显示账号分类、频道/群组统计信息
- 完善AccountLogin手机号登录页面,实现完整登录流程
  - 多步骤登录(API配置→验证码→两步验证密码)
  - 登录成功后自动保存到数据库
  - 支持已有账号更新和新账号创建
- 新增AccountCategory和ChannelGroup的Repository和Service
  - IAccountCategoryRepository / AccountCategoryRepository
  - IChannelGroupRepository / ChannelGroupRepository
  - AccountCategoryManagementService
  - ChannelGroupManagementService
- 更新服务注册,添加新的Repository和Service

页面功能:
- Accounts页面:列表展示、搜索、详情查看、启用/停用、删除
- AccountLogin页面:完整的三步登录流程,自动保存到数据库

下一步计划:
- 继续连接Channels、Groups、BatchTasks等页面
- 完善系统设置页面

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
meoacgxandClaude committed 2025-12-19 03:03:13 +08:00
1 parent 02c10cf2bb
commit 28546a614b
10 files changed
+392 -46

No files matched your search

@@ -26,7 +26,9 @@ public static class ServiceCollectionExtensions
// 注册数据管理服务
services.AddScoped<AccountManagementService>();
services.AddScoped<AccountCategoryManagementService>();
services.AddScoped<ChannelManagementService>();
services.AddScoped<ChannelGroupManagementService>();
services.AddScoped<GroupManagementService>();
services.AddScoped<BatchTaskManagementService>();
@@ -0,0 +1,51 @@
using TelegramPanel.Data.Entities;
using TelegramPanel.Data.Repositories;
namespace TelegramPanel.Core.Services;
/// <summary>
/// 账号分类管理服务
/// </summary>
public class AccountCategoryManagementService
{
private readonly IAccountCategoryRepository _categoryRepository;
public AccountCategoryManagementService(IAccountCategoryRepository categoryRepository)
{
_categoryRepository = categoryRepository;
}
public async Task<IEnumerable<AccountCategory>> GetAllCategoriesAsync()
{
return await _categoryRepository.GetAllAsync();
}
public async Task<AccountCategory?> GetCategoryAsync(int id)
{
return await _categoryRepository.GetByIdAsync(id);
}
public async Task<AccountCategory?> GetCategoryByNameAsync(string name)
{
return await _categoryRepository.GetByNameAsync(name);
}
public async Task<AccountCategory> CreateCategoryAsync(AccountCategory category)
{
return await _categoryRepository.AddAsync(category);
}
public async Task UpdateCategoryAsync(AccountCategory category)
{
await _categoryRepository.UpdateAsync(category);
}
public async Task DeleteCategoryAsync(int id)
{
var category = await _categoryRepository.GetByIdAsync(id);
if (category != null)
{
await _categoryRepository.DeleteAsync(category);
}
}
}
@@ -0,0 +1,51 @@
using TelegramPanel.Data.Entities;
using TelegramPanel.Data.Repositories;
namespace TelegramPanel.Core.Services;
/// <summary>
/// 频道分组管理服务
/// </summary>
public class ChannelGroupManagementService
{
private readonly IChannelGroupRepository _groupRepository;
public ChannelGroupManagementService(IChannelGroupRepository groupRepository)
{
_groupRepository = groupRepository;
}
public async Task<IEnumerable<ChannelGroup>> GetAllGroupsAsync()
{
return await _groupRepository.GetAllAsync();
}
public async Task<ChannelGroup?> GetGroupAsync(int id)
{
return await _groupRepository.GetByIdAsync(id);
}
public async Task<ChannelGroup?> GetGroupByNameAsync(string name)
{
return await _groupRepository.GetByNameAsync(name);
}
public async Task<ChannelGroup> CreateGroupAsync(ChannelGroup group)
{
return await _groupRepository.AddAsync(group);
}
public async Task UpdateGroupAsync(ChannelGroup group)
{
await _groupRepository.UpdateAsync(group);
}
public async Task DeleteGroupAsync(int id)
{
var group = await _groupRepository.GetByIdAsync(id);
if (group != null)
{
await _groupRepository.DeleteAsync(group);
}
}
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore;
using TelegramPanel.Data.Entities;
namespace TelegramPanel.Data.Repositories;
/// <summary>
/// 账号分类仓储实现
/// </summary>
public class AccountCategoryRepository : Repository<AccountCategory>, IAccountCategoryRepository
{
public AccountCategoryRepository(AppDbContext context) : base(context)
{
}
public override async Task<IEnumerable<AccountCategory>> GetAllAsync()
{
return await _dbSet
.Include(c => c.Accounts)
.OrderBy(c => c.Name)
.ToListAsync();
}
public async Task<AccountCategory?> GetByNameAsync(string name)
{
return await _dbSet
.Include(c => c.Accounts)
.FirstOrDefaultAsync(c => c.Name == name);
}
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore;
using TelegramPanel.Data.Entities;
namespace TelegramPanel.Data.Repositories;
/// <summary>
/// 频道分组仓储实现
/// </summary>
public class ChannelGroupRepository : Repository<ChannelGroup>, IChannelGroupRepository
{
public ChannelGroupRepository(AppDbContext context) : base(context)
{
}
public override async Task<IEnumerable<ChannelGroup>> GetAllAsync()
{
return await _dbSet
.Include(g => g.Channels)
.OrderBy(g => g.Name)
.ToListAsync();
}
public async Task<ChannelGroup?> GetByNameAsync(string name)
{
return await _dbSet
.Include(g => g.Channels)
.FirstOrDefaultAsync(g => g.Name == name);
}
}
@@ -0,0 +1,11 @@
using TelegramPanel.Data.Entities;
namespace TelegramPanel.Data.Repositories;
/// <summary>
/// 账号分类仓储接口
/// </summary>
public interface IAccountCategoryRepository : IRepository<AccountCategory>
{
Task<AccountCategory?> GetByNameAsync(string name);
}
@@ -0,0 +1,11 @@
using TelegramPanel.Data.Entities;
namespace TelegramPanel.Data.Repositories;
/// <summary>
/// 频道分组仓储接口
/// </summary>
public interface IChannelGroupRepository : IRepository<ChannelGroup>
{
Task<ChannelGroup?> GetByNameAsync(string name);
}
@@ -17,7 +17,9 @@ public static class ServiceCollectionExtensions
// 注册所有Repository
services.AddScoped<IAccountRepository, AccountRepository>();
services.AddScoped<IAccountCategoryRepository, AccountCategoryRepository>();
services.AddScoped<IChannelRepository, ChannelRepository>();
services.AddScoped<IChannelGroupRepository, ChannelGroupRepository>();
services.AddScoped<IGroupRepository, GroupRepository>();
services.AddScoped<IBatchTaskRepository, BatchTaskRepository>();
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
@@ -1,6 +1,9 @@
@page "/accounts/login"
@inject IAccountService AccountService
@inject AccountManagementService AccountManagement
@inject ISnackbar Snackbar
@inject NavigationManager Navigation
@using TelegramPanel.Data.Entities
<PageTitle>手机号登录 - Telegram Panel</PageTitle>
@@ -16,6 +19,9 @@
<MudTextField @bind-Value="apiHash" Label="API Hash" Variant="Variant.Outlined" Required="true" Class="mt-3" />
<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">
* 手机号需包含国家代码,例如:+8613800138000
</MudText>
</MudStep>
<MudStep Title="验证码" IsResultStep="false">
@@ -24,6 +30,9 @@
</MudText>
<MudTextField @bind-Value="verificationCode" Label="验证码" Variant="Variant.Outlined" Required="true"
Placeholder="12345" />
<MudText Typo="Typo.caption" Class="mt-2" Color="Color.Info">
请在Telegram客户端查收验证码
</MudText>
</MudStep>
<MudStep Title="密码验证" Optional="true">
@@ -36,10 +45,14 @@
<MudStep Title="完成" IsResultStep="true">
<MudAlert Severity="Severity.Success" Class="mb-3">
<MudText Typo="Typo.body1">登录成功!</MudText>
<MudText Typo="Typo.body1">登录成功并保存到数据库!</MudText>
</MudAlert>
<MudText Typo="Typo.body2">用户名:@username</MudText>
<MudText Typo="Typo.body2">用户ID:@userId</MudText>
<MudText Typo="Typo.body2"><strong>手机号:</strong>@phoneNumber</MudText>
<MudText Typo="Typo.body2"><strong>用户名:</strong>@(username ?? "无")</MudText>
<MudText Typo="Typo.body2"><strong>用户ID:</strong>@userId</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="/accounts" Class="mt-4">
查看账号列表
</MudButton>
</MudStep>
</MudStepper>
</MudCardContent>
@@ -48,7 +61,7 @@
重置
</MudButton>
<MudSpacer />
@if (stepper.GetActiveIndex() > 0)
@if (stepper.GetActiveIndex() > 0 && !stepper.IsAllStepsCompleted())
{
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="@(() => stepper.Previous())">
上一步
@@ -84,62 +97,96 @@
private string verificationCode = "";
private string password = "";
private string username = "";
private string? username = "";
private long userId = 0;
private int accountId = 0;
private async Task NextStep()
{
var currentStep = stepper.GetActiveIndex();
logging = true;
try
{
if (currentStep == 0)
{
// 验证 API 配置并发送验证码
if (!int.TryParse(apiId, out _))
// 步骤1:验证 API 配置并发送验证码
if (!int.TryParse(apiId, out int parsedApiId))
{
Snackbar.Add("API ID 格式错误", Severity.Error);
return;
}
// TODO: 调用 API 发送验证码
Snackbar.Add($"验证码已发送到 {phoneNumber}", Severity.Info);
stepper.Next();
if (string.IsNullOrWhiteSpace(apiHash) || string.IsNullOrWhiteSpace(phoneNumber))
{
Snackbar.Add("请填写完整信息", Severity.Error);
return;
}
// 生成临时账号ID(用于跟踪登录过程)
accountId = DateTime.UtcNow.GetHashCode();
var result = await AccountService.StartLoginAsync(accountId, phoneNumber);
if (result.NextStep == "code")
{
Snackbar.Add($"验证码已发送到 {phoneNumber}", Severity.Info);
stepper.Next();
}
else if (result.NextStep == "signup")
{
Snackbar.Add("此号码未注册Telegram,请先注册", Severity.Error);
}
else
{
Snackbar.Add(result.Message ?? "发送验证码失败", Severity.Error);
}
}
else if (currentStep == 1)
{
// 验证验证码
if (string.IsNullOrEmpty(verificationCode))
// 步骤2:验证验证码
if (string.IsNullOrWhiteSpace(verificationCode))
{
Snackbar.Add("请输入验证码", Severity.Error);
return;
}
// TODO: 验证验证码,检查是否需要密码
bool needsPassword = false; // 模拟
var result = await AccountService.SubmitCodeAsync(accountId, verificationCode);
if (needsPassword)
if (result.Success)
{
// 登录成功,保存到数据库
await SaveAccount(result.Account!);
}
else if (result.NextStep == "password")
{
Snackbar.Add("需要两步验证密码", Severity.Info);
stepper.Next();
}
else
{
// 直接完成登录
await CompleteLogin();
Snackbar.Add(result.Message ?? "验证码错误", Severity.Error);
}
}
else if (currentStep == 2)
{
// 验证密码并完成登录
if (string.IsNullOrEmpty(password))
// 步骤3:验证密码
if (string.IsNullOrWhiteSpace(password))
{
Snackbar.Add("请输入密码", Severity.Error);
return;
}
await CompleteLogin();
var result = await AccountService.SubmitPasswordAsync(accountId, password);
if (result.Success)
{
await SaveAccount(result.Account!);
}
else
{
Snackbar.Add(result.Message ?? "密码错误", Severity.Error);
}
}
}
catch (Exception ex)
@@ -152,13 +199,53 @@
}
}
private async Task CompleteLogin()
private async Task SaveAccount(TelegramPanel.Core.Models.AccountInfo accountInfo)
{
// TODO: 完成登录并保存 session
username = "TestUser";
userId = 123456789;
try
{
// 检查账号是否已存在
var existingAccount = await AccountManagement.GetAccountByPhoneAsync(accountInfo.Phone!);
Snackbar.Add("登录成功!", Severity.Success);
stepper.Next();
if (existingAccount != null)
{
// 更新现有账号
existingAccount.Username = accountInfo.Username;
existingAccount.IsActive = true;
existingAccount.LastSyncAt = DateTime.UtcNow;
await AccountManagement.UpdateAccountAsync(existingAccount);
Snackbar.Add("账号信息已更新", Severity.Success);
}
else
{
// 创建新账号
var account = new Account
{
Phone = accountInfo.Phone!,
UserId = accountInfo.TelegramUserId,
Username = accountInfo.Username,
SessionPath = $"sessions/{accountInfo.Phone}.session",
ApiId = int.Parse(apiId),
ApiHash = apiHash,
IsActive = true,
CreatedAt = DateTime.UtcNow,
LastSyncAt = DateTime.UtcNow
};
await AccountManagement.CreateAccountAsync(account);
Snackbar.Add("账号已保存到数据库", Severity.Success);
}
// 设置显示信息
username = accountInfo.Username;
userId = accountInfo.TelegramUserId;
// 跳转到完成步骤
stepper.Next();
}
catch (Exception ex)
{
Snackbar.Add($"保存账号失败:{ex.Message}", Severity.Error);
}
}
}
@@ -1,6 +1,8 @@
@page "/accounts"
@inject IAccountService AccountService
@inject AccountManagementService AccountManagement
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@using TelegramPanel.Data.Entities
<PageTitle>账号列表 - Telegram Panel</PageTitle>
@@ -8,18 +10,22 @@
<MudCard>
<MudCardContent>
<MudTable Items="@accounts" Hover="true" Breakpoint="Breakpoint.Sm" Loading="@loading">
<MudTable Items="@filteredAccounts" Hover="true" Breakpoint="Breakpoint.Sm" Loading="@loading">
<ToolBarContent>
<MudText Typo="Typo.h6">已导入账号</MudText>
<MudText Typo="Typo.h6">已导入账号 (@accounts.Count)</MudText>
<MudSpacer />
<MudTextField @bind-Value="searchString" Placeholder="搜索账号..." Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0" />
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"
Immediate="true" />
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Refresh"
OnClick="LoadAccounts" Class="ml-2">刷新</MudButton>
</ToolBarContent>
<HeaderContent>
<MudTh>手机号</MudTh>
<MudTh>用户名</MudTh>
<MudTh>用户ID</MudTh>
<MudTh>分类</MudTh>
<MudTh>频道/群组</MudTh>
<MudTh>状态</MudTh>
<MudTh>最后同步</MudTh>
<MudTh>操作</MudTh>
@@ -28,22 +34,40 @@
<MudTd DataLabel="手机号">@context.Phone</MudTd>
<MudTd DataLabel="用户名">@(context.Username ?? "-")</MudTd>
<MudTd DataLabel="用户ID">@context.UserId</MudTd>
<MudTd DataLabel="分类">@(context.Category ?? "未分类")</MudTd>
<MudTd DataLabel="分类">
@if (context.Category != null)
{
<MudChip Size="Size.Small" Style="@($"background-color: {context.Category.Color ?? "#9E9E9E"}")">
@context.Category.Name
</MudChip>
}
else
{
<MudText Typo="Typo.body2" Color="Color.Default">未分类</MudText>
}
</MudTd>
<MudTd DataLabel="频道/群组">
<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)">
@(context.IsActive ? "在线" : "离线")
@(context.IsActive ? "活跃" : "停用")
</MudChip>
</MudTd>
<MudTd DataLabel="最后同步">@context.LastSyncAt.ToString("yyyy-MM-dd HH:mm")</MudTd>
<MudTd DataLabel="操作">
<MudIconButton Icon="@Icons.Material.Filled.Info" Size="Size.Small" Color="Color.Info"
OnClick="@(() => ShowDetails(context))" />
OnClick="@(() => ShowDetails(context))" Title="查看详情" />
<MudIconButton Icon="@(context.IsActive ? Icons.Material.Filled.Stop : Icons.Material.Filled.PlayArrow)"
Size="Size.Small" Color="@(context.IsActive ? Color.Warning : Color.Success)"
OnClick="@(() => ToggleActive(context))"
Title="@(context.IsActive ? "停用" : "启用")" />
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error"
OnClick="@(() => DeleteAccount(context.Id))" />
OnClick="@(() => DeleteAccount(context))" Title="删除账号" />
</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>暂无账号数据,请先导入账号</MudText>
<MudText>暂无账号数据,请先<MudLink Href="/accounts/import">导入账号</MudLink></MudText>
</NoRecordsContent>
<LoadingContent>
<MudProgressCircular Indeterminate="true" />
@@ -53,10 +77,18 @@
</MudCard>
@code {
private List<AccountInfo> accounts = new();
private List<Account> accounts = new();
private bool loading = true;
private string searchString = "";
private IEnumerable<Account> filteredAccounts =>
string.IsNullOrWhiteSpace(searchString)
? accounts
: accounts.Where(a =>
a.Phone.Contains(searchString, StringComparison.OrdinalIgnoreCase) ||
(a.Username?.Contains(searchString, StringComparison.OrdinalIgnoreCase) ?? false) ||
a.UserId.ToString().Contains(searchString));
protected override async Task OnInitializedAsync()
{
await LoadAccounts();
@@ -67,9 +99,8 @@
loading = true;
try
{
// TODO: 从数据库加载账号列表
accounts = new List<AccountInfo>();
Snackbar.Add("账号列表加载成功", Severity.Success);
var accountList = await AccountManagement.GetAllAccountsAsync();
accounts = accountList.ToList();
}
catch (Exception ex)
{
@@ -81,15 +112,57 @@
}
}
private void ShowDetails(AccountInfo account)
private async Task ShowDetails(Account account)
{
Snackbar.Add($"查看账号详情:{account.Phone}", Severity.Info);
// TODO: 显示账号详情对话框
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}
";
await DialogService.ShowMessageBox("账号详情", message, "关闭");
}
private async Task DeleteAccount(int accountId)
private async Task ToggleActive(Account account)
{
// TODO: 确认对话框 + 删除账号
Snackbar.Add("删除功能开发中", Severity.Warning);
try
{
await AccountManagement.SetAccountActiveStatusAsync(account.Id, !account.IsActive);
account.IsActive = !account.IsActive;
Snackbar.Add($"账号已{(account.IsActive ? "启用" : "停用")}", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"操作失败:{ex.Message}", Severity.Error);
}
}
private async Task DeleteAccount(Account account)
{
bool? result = await DialogService.ShowMessageBox(
"确认删除",
$"确定要删除账号 {account.Phone} 吗?此操作不可撤销!",
yesText: "删除", cancelText: "取消");
if (result == true)
{
try
{
await AccountManagement.DeleteAccountAsync(account.Id);
accounts.Remove(account);
Snackbar.Add("账号已删除", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"删除失败:{ex.Message}", Severity.Error);
}
}
}
}