feat: bot channels batch ops + per-channel creator execution

This commit is contained in:
meoacgx
2026-01-02 21:11:02 +08:00
parent b941f49a40
commit 55e8fc25fa
15 changed files with 1289 additions and 39 deletions

View File

@@ -5,10 +5,10 @@
<!-- 主版本: 重大变更/不兼容更新 -->
<!-- 次版本: 新功能/兼容性更新 -->
<!-- 修订号: Bug修复/小改进 -->
<Version>1.2.6</Version>
<AssemblyVersion>1.2.6.0</AssemblyVersion>
<FileVersion>1.2.6.0</FileVersion>
<InformationalVersion>1.2.6</InformationalVersion>
<Version>1.2.7</Version>
<AssemblyVersion>1.2.7.0</AssemblyVersion>
<FileVersion>1.2.7.0</FileVersion>
<InformationalVersion>1.2.7</InformationalVersion>
<!-- 项目元信息 -->
<Authors>Telegram Panel Contributors</Authors>

View File

@@ -18,7 +18,8 @@
- 🔐 **二级密码2FA与找回邮箱**:支持单个/批量修改二级密码;支持绑定/换绑 2FA 找回邮箱(验证码确认)
- 🧯 **忘记二级密码可申请重置**:支持单个或批量向 Telegram 提交“忘记密码重置”申请(通常等待 7 天后可重新设置)
- 🪪 **账号资料管理**:支持单账号编辑昵称/Bio/用户名/头像;支持批量修改昵称(自动追加手机号后 4 位便于区分)与批量修改 Bio
- 🤖 **Bot 频道管理**:用于管理“频道创建人不在系统中”的频道(把 Bot 设为管理员即可纳入管理),支持批量导出链接、批量设置管理员;支持对每个 Bot 一键启用/停用(停用后相关后台不再轮询 getUpdates
- 🤖 **Bot 频道管理**:用于管理“频道创建人不在系统中”的频道(把 Bot 设为管理员即可纳入管理),支持批量导出链接、批量邀请成员/设置管理员(可按管理员列表匹配系统账号执行);支持对每个 Bot 一键启用/停用(停用后相关后台不再轮询 getUpdates
- 🏷️ **账号分类(排除操作)**:支持把“卖号/不参与运营”的账号分类标记为“排除操作”,不会出现在创建频道/批量任务的执行账号下拉里
## 🧊 防冻结指南(新号必看)

View File

@@ -60,6 +60,7 @@ public class AppDbContext : DbContext
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
entity.Property(e => e.Color).HasMaxLength(20);
entity.Property(e => e.Description).HasMaxLength(500);
entity.Property(e => e.ExcludeFromOperations).HasDefaultValue(false);
entity.HasIndex(e => e.Name).IsUnique();
});

View File

@@ -9,6 +9,10 @@ public class AccountCategory
public string Name { get; set; } = null!;
public string? Color { get; set; }
public string? Description { get; set; }
/// <summary>
/// 排除操作:该分类下的账号不出现在“创建频道/批量邀请/批量设置管理员”等操作的执行账号选择中
/// </summary>
public bool ExcludeFromOperations { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// 导航属性

View File

@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TelegramPanel.Data.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260102000000_AddAccountCategoryExcludeFromOperations")]
/// <inheritdoc />
public partial class AddAccountCategoryExcludeFromOperations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "ExcludeFromOperations",
table: "AccountCategories",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ExcludeFromOperations",
table: "AccountCategories");
}
}
}

View File

@@ -112,6 +112,10 @@ namespace TelegramPanel.Data.Migrations
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<bool>("ExcludeFromOperations")
.HasColumnType("INTEGER")
.HasDefaultValue(false);
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)

View File

@@ -9,6 +9,8 @@
<MudTextField T="string" @bind-Value="name" Label="分类名称" Variant="Variant.Outlined" Required="true" />
<MudColorPicker @bind-Value="color" Label="分类颜色" Class="mt-2" />
<MudTextField T="string" @bind-Value="description" Label="描述" Variant="Variant.Outlined" Lines="3" Class="mt-2" />
<MudSwitch @bind-Value="excludeFromOperations" Color="Color.Primary" Class="mt-2"
Label="排除操作(不出现在创建/批量任务中)" />
</MudStack>
</DialogContent>
<DialogActions>
@@ -28,12 +30,14 @@
private string name = "";
private MudColor color = "#9E9E9E";
private string? description;
private bool excludeFromOperations;
protected override void OnParametersSet()
{
name = (Category?.Name ?? "").Trim();
description = (Category?.Description ?? "").Trim();
color = string.IsNullOrWhiteSpace(Category?.Color) ? "#9E9E9E" : Category!.Color!;
excludeFromOperations = Category?.ExcludeFromOperations ?? false;
}
private void Cancel()
@@ -46,9 +50,9 @@
var model = new AccountCategoryEditModel(
Name: name.Trim(),
Color: string.IsNullOrWhiteSpace(color.Value) ? null : color.Value,
Description: string.IsNullOrWhiteSpace(description) ? null : description!.Trim());
Description: string.IsNullOrWhiteSpace(description) ? null : description!.Trim(),
ExcludeFromOperations: excludeFromOperations);
MudDialog.Close(DialogResult.Ok(model));
}
}

View File

@@ -1,4 +1,3 @@
namespace TelegramPanel.Web.Components.Dialogs;
public sealed record AccountCategoryEditModel(string Name, string? Color, string? Description);
public sealed record AccountCategoryEditModel(string Name, string? Color, string? Description, bool ExcludeFromOperations);

View File

@@ -0,0 +1,609 @@
@inject AccountManagementService AccountManagement
@inject IChannelService ChannelService
@inject BotTelegramService BotTelegram
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject TelegramPanel.Web.Services.ChannelAdminDefaultsService DefaultsService
@inject TelegramPanel.Web.Services.ChannelAdminPresetsService PresetsService
@using TelegramPanel.Data.Entities
<MudDialog>
<DialogContent>
<MudStack Spacing="2">
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
<span>将使用“执行账号”为所选 Bot 频道批量设置管理员。执行账号必须是该频道管理员(通过 Bot API 获取管理员列表并与系统账号匹配)。</span>
<MudText Typo="Typo.body2" Class="mt-1">
支持:<b>@@username</b>、<b>username</b>
</MudText>
</MudAlert>
<MudText Typo="Typo.subtitle2">目标频道:@channels.Count 个</MudText>
@if (loadingAdmins)
{
<MudProgressLinear Indeterminate="true" />
<MudText Typo="Typo.caption" Color="Color.Secondary">正在读取频道管理员列表Bot API...</MudText>
}
@if (!string.IsNullOrWhiteSpace(adminSummary))
{
<MudText Typo="Typo.caption" Color="Color.Secondary">@adminSummary</MudText>
}
<MudGrid Spacing="2">
<MudItem xs="12" sm="8">
<MudSelect T="string" Value="@selectedPresetName" ValueChanged="OnPresetChanged"
Label="管理员预设(用户名列表)" Variant="Variant.Outlined" Dense="true" Disabled="@(running || loadingPresets)">
<MudSelectItem Value="@string.Empty">(不使用预设)</MudSelectItem>
@foreach (var p in Presets)
{
<MudSelectItem Value="@p.Name">@p.Name</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" sm="4" Class="d-flex align-center">
<MudButton Variant="Variant.Outlined" Color="Color.Error" OnClick="DeleteSelectedPreset"
Disabled="@(running || loadingPresets || string.IsNullOrWhiteSpace(selectedPresetName))">
删除预设
</MudButton>
</MudItem>
</MudGrid>
<MudSelect @bind-Value="selectedAccountId" Label="执行账号" Variant="Variant.Outlined" Dense="true"
Disabled="@(running || loadingAdmins || eligibleAccounts.Count == 0)">
<MudSelectItem Value="0">自动选择(按频道)</MudSelectItem>
@foreach (var a in eligibleAccounts)
{
<MudSelectItem Value="@a.Id">@FormatAccountLabel(a)</MudSelectItem>
}
</MudSelect>
@if (eligibleAccounts.Count == 0 && !loadingAdmins && channels.Count > 0)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
当前所选频道中未发现“既是频道管理员又在系统中的账号”。你仍可继续执行,但这些频道会被跳过。
</MudAlert>
}
<MudTextField @bind-Value="usernamesText" Label="用户名列表(换行分隔)" Variant="Variant.Outlined" Lines="8"
HelperText="@UsernamesHelperText" />
<MudGrid Spacing="2">
<MudItem xs="12" sm="8">
<MudTextField @bind-Value="presetNameToSave" Label="保存当前用户名为预设组(名称)"
Variant="Variant.Outlined" Disabled="@(running || loadingPresets)" />
</MudItem>
<MudItem xs="12" sm="4" Class="d-flex align-center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SavePreset"
Disabled="@(running || loadingPresets || string.IsNullOrWhiteSpace(presetNameToSave))">
保存为预设
</MudButton>
</MudItem>
</MudGrid>
<MudTextField @bind-Value="adminTitle" Label="管理员头衔" Variant="Variant.Outlined"
HelperText="例如Admin/Editor。留空将使用默认值。" />
<MudText Typo="Typo.subtitle2">权限(可多选)</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
说明:这里选择的是“你希望目标账号拥有的权限”。如果设置后发现某些权限未生效,面板会提示缺失的权限名称。
</MudText>
<MudStack Row="true" Spacing="1" Style="flex-wrap: wrap;">
<MudButton Variant="Variant.Outlined" OnClick="@(() => ApplyRights(AdminRights.BasicAdmin))" Disabled="@running">常用权限</MudButton>
<MudButton Variant="Variant.Outlined" OnClick="@(() => ApplyRights(FullRights))" Disabled="@running">全选权限</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Warning" OnClick="@(() => ApplyRights(AdminRights.None))" Disabled="@running">清空</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveAsDefaultRightsAsync" Disabled="@running">保存为默认权限</MudButton>
</MudStack>
<MudPaper Outlined="true" Class="pa-3">
<MudGrid Spacing="2">
<MudItem xs="12" sm="6" md="4"><MudCheckBox T="bool" Dense="true" @bind-Value="rights.ChangeInfo" Label="修改信息" Disabled="@running" /></MudItem>
<MudItem xs="12" sm="6" md="4"><MudCheckBox T="bool" Dense="true" @bind-Value="rights.PostMessages" Label="发消息" Disabled="@running" /></MudItem>
<MudItem xs="12" sm="6" md="4"><MudCheckBox T="bool" Dense="true" @bind-Value="rights.EditMessages" Label="编辑消息" Disabled="@running" /></MudItem>
<MudItem xs="12" sm="6" md="4"><MudCheckBox T="bool" Dense="true" @bind-Value="rights.DeleteMessages" Label="删除消息" Disabled="@running" /></MudItem>
<MudItem xs="12" sm="6" md="4"><MudCheckBox T="bool" Dense="true" @bind-Value="rights.BanUsers" Label="封禁用户" Disabled="@running" /></MudItem>
<MudItem xs="12" sm="6" md="4"><MudCheckBox T="bool" Dense="true" @bind-Value="rights.InviteUsers" Label="邀请用户" Disabled="@running" /></MudItem>
<MudItem xs="12" sm="6" md="4"><MudCheckBox T="bool" Dense="true" @bind-Value="rights.PinMessages" Label="置顶消息" Disabled="@running" /></MudItem>
<MudItem xs="12" sm="6" md="4"><MudCheckBox T="bool" Dense="true" @bind-Value="rights.ManageCall" Label="语音/直播" Disabled="@running" /></MudItem>
<MudItem xs="12" sm="6" md="4"><MudCheckBox T="bool" Dense="true" @bind-Value="rights.AddAdmins" Label="添加管理员" Disabled="@running" /></MudItem>
<MudItem xs="12" sm="6" md="4"><MudCheckBox T="bool" Dense="true" @bind-Value="rights.Anonymous" Label="匿名" Disabled="@running" /></MudItem>
<MudItem xs="12" sm="6" md="4"><MudCheckBox T="bool" Dense="true" @bind-Value="rights.ManageTopics" Label="管理话题" Disabled="@running" /></MudItem>
</MudGrid>
</MudPaper>
<MudNumericField @bind-Value="delayMs" Label="操作间隔(毫秒)" Variant="Variant.Outlined" Min="0" Max="30000"
HelperText="建议设置 1000-2500ms避免触发风控会额外加少量随机抖动。" />
@if (running)
{
<MudDivider />
<MudText Typo="Typo.body2">进度:@done / @total失败@failed</MudText>
<MudProgressLinear Value="@Progress" Color="Color.Primary" />
@if (!string.IsNullOrWhiteSpace(currentHint))
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">@currentHint</MudText>
}
}
</MudStack>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="CancelOrClose">
@(running ? "停止" : "关闭")
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="Submit" Disabled="@(running || channels.Count == 0)">
开始设置
</MudButton>
</DialogActions>
</MudDialog>
@code
{
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
[Parameter] public int BotId { get; set; }
[Parameter] public List<BotChannel> Channels { get; set; } = new();
private readonly Random _rand = new();
private readonly List<Account> eligibleAccounts = new();
private readonly Dictionary<long, Account> accountsByUserId = new();
private readonly Dictionary<long, HashSet<long>> channelAdminUserIds = new();
private List<BotChannel> channels = new();
private int selectedAccountId;
private string usernamesText = "";
private string adminTitle = "Admin";
private int delayMs = 1500;
private const string UsernamesHelperText = "每行一个 username 或 @username将按“频道 × 用户”的顺序依次执行。";
private bool loadingAdmins;
private string? adminSummary;
private bool loadingPresets;
private List<TelegramPanel.Web.Services.ChannelAdminPreset> Presets { get; set; } = new();
private string selectedPresetName = "";
private string presetNameToSave = "";
private RightsModel rights = new();
private static readonly AdminRights FullRights =
AdminRights.ChangeInfo
| AdminRights.PostMessages
| AdminRights.EditMessages
| AdminRights.DeleteMessages
| AdminRights.BanUsers
| AdminRights.InviteUsers
| AdminRights.PinMessages
| AdminRights.ManageCall
| AdminRights.AddAdmins
| AdminRights.Anonymous
| AdminRights.ManageTopics;
private bool running;
private int total;
private int done;
private int failed;
private string? currentHint;
private CancellationTokenSource? _cts;
private int Progress => total <= 0 ? 0 : (int)Math.Round(done * 100d / total);
private AdminRights SelectedRights => rights.ToRights();
protected override async Task OnInitializedAsync()
{
channels = (Channels ?? new List<BotChannel>())
.Where(x => x != null)
.GroupBy(x => x.TelegramId)
.Select(x => x.First())
.ToList();
await LoadAccountsAsync();
// 默认权限:优先读取上次保存的默认值;否则使用“常用权限”
var defaults = await DefaultsService.GetAsync();
ApplyRights(defaults?.Rights ?? AdminRights.BasicAdmin);
await ReloadPresetsAsync();
if (BotId > 0 && channels.Count > 0)
await LoadAdminsAndEligibleAccountsAsync();
}
private async Task LoadAccountsAsync()
{
var all = (await AccountManagement.GetAllAccountsAsync()).ToList();
accountsByUserId.Clear();
foreach (var a in all.Where(x => x.IsActive && x.Category?.ExcludeFromOperations != true))
{
if (a.UserId <= 0)
continue;
if (!accountsByUserId.ContainsKey(a.UserId))
accountsByUserId[a.UserId] = a;
}
}
private async Task LoadAdminsAndEligibleAccountsAsync()
{
loadingAdmins = true;
adminSummary = null;
eligibleAccounts.Clear();
channelAdminUserIds.Clear();
try
{
var eligibleUserIds = new HashSet<long>();
var matchedChannels = 0;
foreach (var ch in channels)
{
var adminIds = new HashSet<long>();
try
{
var admins = await BotTelegram.GetChatAdminsAsync(BotId, ch.TelegramId, CancellationToken.None);
foreach (var a in admins)
{
adminIds.Add(a.UserId);
if (accountsByUserId.ContainsKey(a.UserId))
eligibleUserIds.Add(a.UserId);
}
if (admins.Any(a => accountsByUserId.ContainsKey(a.UserId)))
matchedChannels++;
}
catch
{
// ignore
}
channelAdminUserIds[ch.TelegramId] = adminIds;
}
foreach (var uid in eligibleUserIds)
{
if (accountsByUserId.TryGetValue(uid, out var acc))
eligibleAccounts.Add(acc);
}
eligibleAccounts.Sort((a, b) => string.Compare(FormatAccountLabel(a), FormatAccountLabel(b), StringComparison.OrdinalIgnoreCase));
adminSummary = $"可用执行账号:{eligibleAccounts.Count} 个;可执行频道:{matchedChannels}/{channels.Count}。";
}
finally
{
loadingAdmins = false;
}
}
private async Task ReloadPresetsAsync()
{
loadingPresets = true;
try
{
Presets = (await PresetsService.GetPresetsAsync()).ToList();
}
finally
{
loadingPresets = false;
}
}
private Task OnPresetChanged(string? name)
{
selectedPresetName = name ?? "";
if (string.IsNullOrWhiteSpace(selectedPresetName))
return Task.CompletedTask;
var p = Presets.FirstOrDefault(x => string.Equals(x.Name, selectedPresetName, StringComparison.OrdinalIgnoreCase));
if (p != null)
usernamesText = string.Join(Environment.NewLine, p.Usernames.Select(x => $"@{x}"));
return Task.CompletedTask;
}
private async Task SavePreset()
{
var name = (presetNameToSave ?? "").Trim();
if (string.IsNullOrWhiteSpace(name))
{
Snackbar.Add("请输入预设名称", Severity.Warning);
return;
}
var usernames = ParseUsernames(usernamesText);
if (usernames.Count == 0)
{
Snackbar.Add("请至少输入一个用户名", Severity.Warning);
return;
}
try
{
await PresetsService.SavePresetAsync(name, usernames);
Snackbar.Add("已保存预设", Severity.Success);
presetNameToSave = "";
await ReloadPresetsAsync();
}
catch (Exception ex)
{
Snackbar.Add($"保存预设失败:{ex.Message}", Severity.Error);
}
}
private async Task DeleteSelectedPreset()
{
if (string.IsNullOrWhiteSpace(selectedPresetName))
return;
bool? ok = await DialogService.ShowMessageBox(
title: "删除预设",
message: $"确定删除预设“{selectedPresetName}”吗?",
yesText: "删除",
cancelText: "取消");
if (ok != true)
return;
try
{
await PresetsService.DeletePresetAsync(selectedPresetName);
Snackbar.Add("已删除预设", Severity.Success);
selectedPresetName = "";
await ReloadPresetsAsync();
}
catch (Exception ex)
{
Snackbar.Add($"删除预设失败:{ex.Message}", Severity.Error);
}
}
private void ApplyRights(AdminRights selected)
{
rights = RightsModel.FromRights(selected);
}
private async Task SaveAsDefaultRightsAsync()
{
try
{
await DefaultsService.SaveAsync(new TelegramPanel.Web.Services.ChannelAdminDefaults(SelectedRights));
Snackbar.Add("已保存默认权限", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"保存失败:{ex.Message}", Severity.Error);
}
}
private async Task Submit()
{
if (running)
return;
if (BotId <= 0)
{
Snackbar.Add("BotId 无效", Severity.Error);
return;
}
var usernames = ParseUsernames(usernamesText);
if (usernames.Count == 0)
{
Snackbar.Add("请至少输入一个用户名", Severity.Warning);
return;
}
if (channels.Count == 0)
{
Snackbar.Add("未选择任何频道", Severity.Info);
return;
}
var title = string.IsNullOrWhiteSpace(adminTitle) ? "Admin" : adminTitle.Trim();
bool? confirm = await DialogService.ShowMessageBox(
"确认执行",
$"设置管理员:{channels.Count} 个频道 × {usernames.Count} 个用户(共 {channels.Count * usernames.Count} 次操作)。是否继续?",
yesText: "继续", cancelText: "取消");
if (confirm != true)
return;
running = true;
total = channels.Count * usernames.Count;
done = 0;
failed = 0;
currentHint = null;
_cts = new CancellationTokenSource();
var failures = new List<string>();
try
{
foreach (var ch in channels)
{
var executorId = ResolveExecutorAccountId(ch);
if (executorId == null)
{
foreach (var _ in usernames)
{
done++;
failed++;
}
failures.Add($"{ch.Title}:无可用执行账号(需要该频道管理员且存在于系统账号)");
StateHasChanged();
continue;
}
foreach (var username in usernames)
{
_cts.Token.ThrowIfCancellationRequested();
currentHint = $"{ch.Title} => {username}";
StateHasChanged();
try
{
var ok = await ChannelService.SetAdminAsync(executorId.Value, ch.TelegramId, username, SelectedRights, title);
if (!ok)
{
failed++;
failures.Add($"{ch.Title} => {username}:失败");
}
}
catch (Exception ex)
{
failed++;
failures.Add($"{ch.Title} => {username}{ex.Message}");
}
finally
{
done++;
}
StateHasChanged();
var wait = delayMs;
if (wait < 0) wait = 0;
if (wait > 30000) wait = 30000;
var jitter = _rand.Next(500, 1000);
await Task.Delay(TimeSpan.FromMilliseconds(wait + jitter), _cts.Token);
}
}
var summary = $"完成:{done}/{total}(失败:{failed}";
Snackbar.Add(summary, failed == 0 ? Severity.Success : Severity.Warning);
if (failures.Count > 0)
{
var details = string.Join(Environment.NewLine, failures.Take(80));
await DialogService.ShowMessageBox("失败明细(前 80 条)", details, "关闭");
}
MudDialog.Close(DialogResult.Ok(new { Done = done, Failed = failed }));
}
catch (OperationCanceledException)
{
Snackbar.Add("已停止执行", Severity.Info);
}
catch (Exception ex)
{
Snackbar.Add($"执行异常:{ex.Message}", Severity.Error);
}
finally
{
running = false;
currentHint = null;
_cts?.Dispose();
_cts = null;
StateHasChanged();
}
}
private int? ResolveExecutorAccountId(BotChannel channel)
{
if (!channelAdminUserIds.TryGetValue(channel.TelegramId, out var adminIds) || adminIds.Count == 0)
return null;
if (selectedAccountId > 0)
{
var selected = eligibleAccounts.FirstOrDefault(x => x.Id == selectedAccountId);
if (selected == null || selected.UserId <= 0)
return null;
return adminIds.Contains(selected.UserId) ? selected.Id : null;
}
foreach (var uid in adminIds)
{
if (accountsByUserId.TryGetValue(uid, out var acc))
return acc.Id;
}
return null;
}
private void CancelOrClose()
{
if (running)
{
_cts?.Cancel();
return;
}
MudDialog.Close();
}
private static List<string> ParseUsernames(string? text)
{
if (string.IsNullOrWhiteSpace(text))
return new List<string>();
return text
.Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => (x ?? string.Empty).Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => x.StartsWith("@", StringComparison.Ordinal) ? x.Substring(1) : x)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
private static string FormatAccountLabel(Account account)
{
var nickname = string.IsNullOrWhiteSpace(account.Nickname) ? null : account.Nickname.Trim();
var username = string.IsNullOrWhiteSpace(account.Username) ? null : account.Username.Trim();
var namePart = nickname ?? account.Phone;
if (!string.IsNullOrWhiteSpace(username))
return $"{namePart} (@{username})";
return namePart;
}
private sealed class RightsModel
{
public bool ChangeInfo { get; set; }
public bool PostMessages { get; set; }
public bool EditMessages { get; set; }
public bool DeleteMessages { get; set; }
public bool BanUsers { get; set; }
public bool InviteUsers { get; set; }
public bool PinMessages { get; set; }
public bool ManageCall { get; set; }
public bool AddAdmins { get; set; }
public bool Anonymous { get; set; }
public bool ManageTopics { get; set; }
public AdminRights ToRights()
{
var r = AdminRights.None;
if (ChangeInfo) r |= AdminRights.ChangeInfo;
if (PostMessages) r |= AdminRights.PostMessages;
if (EditMessages) r |= AdminRights.EditMessages;
if (DeleteMessages) r |= AdminRights.DeleteMessages;
if (BanUsers) r |= AdminRights.BanUsers;
if (InviteUsers) r |= AdminRights.InviteUsers;
if (PinMessages) r |= AdminRights.PinMessages;
if (ManageCall) r |= AdminRights.ManageCall;
if (AddAdmins) r |= AdminRights.AddAdmins;
if (Anonymous) r |= AdminRights.Anonymous;
if (ManageTopics) r |= AdminRights.ManageTopics;
return r;
}
public static RightsModel FromRights(AdminRights rights)
{
return new RightsModel
{
ChangeInfo = rights.HasFlag(AdminRights.ChangeInfo),
PostMessages = rights.HasFlag(AdminRights.PostMessages),
EditMessages = rights.HasFlag(AdminRights.EditMessages),
DeleteMessages = rights.HasFlag(AdminRights.DeleteMessages),
BanUsers = rights.HasFlag(AdminRights.BanUsers),
InviteUsers = rights.HasFlag(AdminRights.InviteUsers),
PinMessages = rights.HasFlag(AdminRights.PinMessages),
ManageCall = rights.HasFlag(AdminRights.ManageCall),
AddAdmins = rights.HasFlag(AdminRights.AddAdmins),
Anonymous = rights.HasFlag(AdminRights.Anonymous),
ManageTopics = rights.HasFlag(AdminRights.ManageTopics)
};
}
}
}

View File

@@ -0,0 +1,490 @@
@inject AccountManagementService AccountManagement
@inject IChannelService ChannelService
@inject BotTelegramService BotTelegram
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject TelegramPanel.Web.Services.ChannelInvitePresetsService PresetsService
@using TelegramPanel.Data.Entities
<MudDialog>
<DialogContent>
<MudStack Spacing="2">
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
<span>将使用“执行账号”邀请用户加入所选 Bot 频道。执行账号必须是该频道管理员(通过 Bot API 获取管理员列表并与系统账号匹配)。</span>
<MudText Typo="Typo.body2" Class="mt-1">
支持:<b>@@username</b>、<b>username</b>
</MudText>
</MudAlert>
<MudText Typo="Typo.subtitle2">目标频道:@channels.Count 个</MudText>
@if (loadingAdmins)
{
<MudProgressLinear Indeterminate="true" />
<MudText Typo="Typo.caption" Color="Color.Secondary">正在读取频道管理员列表Bot API...</MudText>
}
@if (!string.IsNullOrWhiteSpace(adminSummary))
{
<MudText Typo="Typo.caption" Color="Color.Secondary">@adminSummary</MudText>
}
<MudGrid Spacing="2">
<MudItem xs="12" sm="8">
<MudSelect T="string" Value="@selectedPresetName" ValueChanged="OnPresetChanged"
Label="邀请预设" Variant="Variant.Outlined" Dense="true" Disabled="@(running || loadingPresets)">
<MudSelectItem Value="@string.Empty">(不使用预设)</MudSelectItem>
@foreach (var p in Presets)
{
<MudSelectItem Value="@p.Name">@p.Name</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" sm="4" Class="d-flex align-center">
<MudButton Variant="Variant.Outlined" Color="Color.Error" OnClick="DeleteSelectedPreset"
Disabled="@(running || loadingPresets || string.IsNullOrWhiteSpace(selectedPresetName))">
删除预设
</MudButton>
</MudItem>
</MudGrid>
<MudSelect @bind-Value="selectedAccountId" Label="执行账号" Variant="Variant.Outlined" Dense="true"
Disabled="@(running || loadingAdmins || eligibleAccounts.Count == 0)">
<MudSelectItem Value="0">自动选择(按频道)</MudSelectItem>
@foreach (var a in eligibleAccounts)
{
<MudSelectItem Value="@a.Id">@FormatAccountLabel(a)</MudSelectItem>
}
</MudSelect>
@if (eligibleAccounts.Count == 0 && !loadingAdmins && channels.Count > 0)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
当前所选频道中未发现“既是频道管理员又在系统中的账号”。你仍可继续执行,但这些频道会被跳过。
</MudAlert>
}
<MudTextField @bind-Value="usernamesText" Label="用户名列表(换行分隔)" Variant="Variant.Outlined" Lines="8"
HelperText="@UsernamesHelperText" />
<MudGrid Spacing="2">
<MudItem xs="12" sm="8">
<MudTextField @bind-Value="presetNameToSave" Label="保存当前用户名为预设组(名称)"
Variant="Variant.Outlined" Disabled="@(running || loadingPresets)" />
</MudItem>
<MudItem xs="12" sm="4" Class="d-flex align-center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SavePreset"
Disabled="@(running || loadingPresets || string.IsNullOrWhiteSpace(presetNameToSave))">
保存为预设
</MudButton>
</MudItem>
</MudGrid>
<MudNumericField @bind-Value="delayMs" Label="邀请间隔(毫秒)" Variant="Variant.Outlined" Min="0" Max="30000"
HelperText="建议设置 1500-4000ms避免触发风控会额外加少量随机抖动。" />
@if (running)
{
<MudDivider />
<MudText Typo="Typo.body2">进度:@done / @total失败@failed</MudText>
<MudProgressLinear Value="@Progress" Color="Color.Primary" />
@if (!string.IsNullOrWhiteSpace(currentHint))
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">@currentHint</MudText>
}
}
</MudStack>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="CancelOrClose">
@(running ? "停止" : "关闭")
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="Submit" Disabled="@(running || channels.Count == 0)">
开始邀请
</MudButton>
</DialogActions>
</MudDialog>
@code
{
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
[Parameter] public int BotId { get; set; }
[Parameter] public List<BotChannel> Channels { get; set; } = new();
private readonly Random _rand = new();
private readonly List<Account> eligibleAccounts = new();
private readonly Dictionary<long, Account> accountsByUserId = new();
private readonly Dictionary<long, HashSet<long>> channelAdminUserIds = new();
private List<BotChannel> channels = new();
private int selectedAccountId;
private string usernamesText = "";
private int delayMs = 2000;
private const string UsernamesHelperText = "每行一个 username 或 @username将按“频道 × 用户”的顺序依次执行。";
private bool loadingAdmins;
private string? adminSummary;
private bool loadingPresets;
private List<TelegramPanel.Web.Services.ChannelInvitePreset> Presets { get; set; } = new();
private string selectedPresetName = "";
private string presetNameToSave = "";
private bool running;
private int total;
private int done;
private int failed;
private string? currentHint;
private CancellationTokenSource? _cts;
private int Progress => total <= 0 ? 0 : (int)Math.Round(done * 100d / total);
protected override async Task OnInitializedAsync()
{
channels = (Channels ?? new List<BotChannel>())
.Where(x => x != null)
.GroupBy(x => x.TelegramId)
.Select(x => x.First())
.ToList();
await LoadAccountsAsync();
await ReloadPresetsAsync();
if (BotId > 0 && channels.Count > 0)
await LoadAdminsAndEligibleAccountsAsync();
}
private async Task LoadAccountsAsync()
{
var all = (await AccountManagement.GetAllAccountsAsync()).ToList();
accountsByUserId.Clear();
foreach (var a in all)
{
if (!a.IsActive)
continue;
if (a.Category?.ExcludeFromOperations == true)
continue;
if (a.UserId <= 0)
continue;
if (!accountsByUserId.ContainsKey(a.UserId))
accountsByUserId[a.UserId] = a;
}
}
private async Task LoadAdminsAndEligibleAccountsAsync()
{
loadingAdmins = true;
adminSummary = null;
eligibleAccounts.Clear();
channelAdminUserIds.Clear();
try
{
var eligibleUserIds = new HashSet<long>();
var matchedChannels = 0;
foreach (var ch in channels)
{
var adminIds = new HashSet<long>();
try
{
var admins = await BotTelegram.GetChatAdminsAsync(BotId, ch.TelegramId, CancellationToken.None);
foreach (var a in admins)
{
adminIds.Add(a.UserId);
if (accountsByUserId.ContainsKey(a.UserId))
eligibleUserIds.Add(a.UserId);
}
if (admins.Any(a => accountsByUserId.ContainsKey(a.UserId)))
matchedChannels++;
}
catch
{
// ignore: 无权限/风控/频道已失效等,后续按“无可用执行账号”处理
}
channelAdminUserIds[ch.TelegramId] = adminIds;
}
foreach (var uid in eligibleUserIds)
{
if (accountsByUserId.TryGetValue(uid, out var acc))
eligibleAccounts.Add(acc);
}
eligibleAccounts.Sort((a, b) => string.Compare(FormatAccountLabel(a), FormatAccountLabel(b), StringComparison.OrdinalIgnoreCase));
adminSummary = $"可用执行账号:{eligibleAccounts.Count} 个;可执行频道:{matchedChannels}/{channels.Count}。";
}
finally
{
loadingAdmins = false;
}
}
private async Task ReloadPresetsAsync()
{
loadingPresets = true;
try
{
Presets = (await PresetsService.GetPresetsAsync()).ToList();
}
finally
{
loadingPresets = false;
}
}
private Task OnPresetChanged(string value)
{
selectedPresetName = value ?? "";
if (string.IsNullOrWhiteSpace(selectedPresetName))
return Task.CompletedTask;
var preset = Presets.FirstOrDefault(x => string.Equals(x.Name, selectedPresetName, StringComparison.OrdinalIgnoreCase));
if (preset != null)
usernamesText = string.Join(Environment.NewLine, preset.Usernames);
return Task.CompletedTask;
}
private async Task SavePreset()
{
var name = (presetNameToSave ?? "").Trim();
if (string.IsNullOrWhiteSpace(name))
{
Snackbar.Add("请输入预设名称", Severity.Warning);
return;
}
var usernames = ParseUsernames(usernamesText);
if (usernames.Count == 0)
{
Snackbar.Add("请至少输入一个用户名", Severity.Warning);
return;
}
try
{
await PresetsService.SavePresetAsync(name, usernames);
Snackbar.Add("已保存预设", Severity.Success);
presetNameToSave = "";
await ReloadPresetsAsync();
}
catch (Exception ex)
{
Snackbar.Add($"保存预设失败:{ex.Message}", Severity.Error);
}
}
private async Task DeleteSelectedPreset()
{
if (string.IsNullOrWhiteSpace(selectedPresetName))
return;
bool? ok = await DialogService.ShowMessageBox(
title: "删除预设",
message: $"确定删除预设“{selectedPresetName}”吗?",
yesText: "删除",
cancelText: "取消");
if (ok != true)
return;
try
{
await PresetsService.DeletePresetAsync(selectedPresetName);
Snackbar.Add("已删除预设", Severity.Success);
selectedPresetName = "";
await ReloadPresetsAsync();
}
catch (Exception ex)
{
Snackbar.Add($"删除预设失败:{ex.Message}", Severity.Error);
}
}
private async Task Submit()
{
if (running)
return;
if (BotId <= 0)
{
Snackbar.Add("BotId 无效", Severity.Error);
return;
}
var usernames = ParseUsernames(usernamesText);
if (usernames.Count == 0)
{
Snackbar.Add("请至少输入一个用户名", Severity.Warning);
return;
}
if (channels.Count == 0)
{
Snackbar.Add("未选择任何频道", Severity.Info);
return;
}
bool? confirm = await DialogService.ShowMessageBox(
"确认执行",
$"邀请:{channels.Count} 个频道 × {usernames.Count} 个用户(共 {channels.Count * usernames.Count} 次操作)。是否继续?",
yesText: "继续", cancelText: "取消");
if (confirm != true)
return;
running = true;
total = channels.Count * usernames.Count;
done = 0;
failed = 0;
currentHint = null;
_cts = new CancellationTokenSource();
var failures = new List<string>();
try
{
foreach (var ch in channels)
{
var executorId = ResolveExecutorAccountId(ch);
if (executorId == null)
{
foreach (var _ in usernames)
{
done++;
failed++;
}
failures.Add($"{ch.Title}:无可用执行账号(需要该频道管理员且存在于系统账号)");
StateHasChanged();
continue;
}
foreach (var username in usernames)
{
_cts.Token.ThrowIfCancellationRequested();
currentHint = $"{ch.Title} => {username}";
StateHasChanged();
try
{
var result = await ChannelService.InviteUserAsync(executorId.Value, ch.TelegramId, username);
if (!result.Success)
{
failed++;
failures.Add($"{ch.Title} => {username}{result.Error}");
}
}
catch (Exception ex)
{
failed++;
failures.Add($"{ch.Title} => {username}{ex.Message}");
}
finally
{
done++;
}
StateHasChanged();
var wait = delayMs;
if (wait < 0) wait = 0;
if (wait > 30000) wait = 30000;
var jitter = _rand.Next(500, 1500);
await Task.Delay(TimeSpan.FromMilliseconds(wait + jitter), _cts.Token);
}
}
var summary = $"完成:{done}/{total}(失败:{failed}";
Snackbar.Add(summary, failed == 0 ? Severity.Success : Severity.Warning);
if (failures.Count > 0)
{
var details = string.Join(Environment.NewLine, failures.Take(80));
await DialogService.ShowMessageBox("失败明细(前 80 条)", details, "关闭");
}
MudDialog.Close(DialogResult.Ok(new { Done = done, Failed = failed }));
}
catch (OperationCanceledException)
{
Snackbar.Add("已停止执行", Severity.Info);
}
catch (Exception ex)
{
Snackbar.Add($"执行异常:{ex.Message}", Severity.Error);
}
finally
{
running = false;
currentHint = null;
_cts?.Dispose();
_cts = null;
StateHasChanged();
}
}
private int? ResolveExecutorAccountId(BotChannel channel)
{
if (!channelAdminUserIds.TryGetValue(channel.TelegramId, out var adminIds) || adminIds.Count == 0)
return null;
if (selectedAccountId > 0)
{
var selected = eligibleAccounts.FirstOrDefault(x => x.Id == selectedAccountId);
if (selected == null || selected.UserId <= 0)
return null;
return adminIds.Contains(selected.UserId) ? selected.Id : null;
}
foreach (var uid in adminIds)
{
if (accountsByUserId.TryGetValue(uid, out var acc))
return acc.Id;
}
return null;
}
private void CancelOrClose()
{
if (running)
{
_cts?.Cancel();
return;
}
MudDialog.Close();
}
private static List<string> ParseUsernames(string? text)
{
if (string.IsNullOrWhiteSpace(text))
return new List<string>();
return text
.Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => (x ?? string.Empty).Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => x.StartsWith("@", StringComparison.Ordinal) ? x.Substring(1) : x)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
private static string FormatAccountLabel(Account account)
{
var nickname = string.IsNullOrWhiteSpace(account.Nickname) ? null : account.Nickname.Trim();
var username = string.IsNullOrWhiteSpace(account.Username) ? null : account.Username.Trim();
var namePart = nickname ?? account.Phone;
if (!string.IsNullOrWhiteSpace(username))
return $"{namePart} (@{username})";
return namePart;
}
}

View File

@@ -11,7 +11,7 @@
<DialogContent>
<MudStack Spacing="2">
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
<span>将使用“执行账号”为所选频道批量设置管理员。支持输入多个用户名,换行分隔。</span>
<span>将使用“执行账号”为所选频道批量设置管理员(默认:每个频道的创建账号)。支持输入多个用户名,换行分隔。</span>
<MudText Typo="Typo.body2" Class="mt-1">
支持:<b>@@username</b>、<b>username</b>
</MudText>
@@ -39,6 +39,7 @@
</MudGrid>
<MudSelect @bind-Value="selectedAccountId" Label="执行账号" Variant="Variant.Outlined" Dense="true">
<MudSelectItem Value="0">每个频道创建账号(默认)</MudSelectItem>
@foreach (var a in accounts)
{
<MudSelectItem Value="@a.Id">@FormatAccountLabel(a)</MudSelectItem>
@@ -173,12 +174,8 @@
.ToList();
var all = (await AccountManagement.GetAllAccountsAsync()).ToList();
accounts.AddRange(all.Where(a => a.IsActive));
if (DefaultAccountId > 0 && accounts.Any(a => a.Id == DefaultAccountId))
selectedAccountId = DefaultAccountId;
else
selectedAccountId = accounts.FirstOrDefault()?.Id ?? 0;
accounts.AddRange(all.Where(a => a.IsActive && a.Category?.ExcludeFromOperations != true));
selectedAccountId = 0;
// 默认权限:优先读取上次保存的默认值;否则使用“常用权限”
var defaults = await DefaultsService.GetAsync();
@@ -187,6 +184,21 @@
await ReloadPresetsAsync();
}
private int? ResolveExecuteAccountId(Channel channel)
{
if (channel == null)
return null;
if (selectedAccountId > 0)
return selectedAccountId;
var creatorId = channel.CreatorAccountId;
if (creatorId == null || creatorId.Value <= 0)
return null;
return accounts.Any(a => a.Id == creatorId.Value) ? creatorId.Value : null;
}
private async Task ReloadPresetsAsync()
{
loadingPresets = true;
@@ -291,12 +303,6 @@
if (running)
return;
if (selectedAccountId <= 0)
{
Snackbar.Add("请选择一个执行账号", Severity.Warning);
return;
}
var usernames = ParseUsernames(usernamesText);
if (usernames.Count == 0)
{
@@ -335,6 +341,16 @@
{
foreach (var ch in channels)
{
var executeAccountId = ResolveExecuteAccountId(ch);
if (executeAccountId == null)
{
done += usernames.Count;
failed += usernames.Count;
failures.Add($"{ch.Title}:无可用执行账号(将使用创建账号执行;但该频道无创建账号/创建账号不可用)");
StateHasChanged();
continue;
}
foreach (var username in usernames)
{
_cts.Token.ThrowIfCancellationRequested();
@@ -344,7 +360,7 @@
try
{
var ok = await ChannelService.SetAdminAsync(selectedAccountId, ch.TelegramId, username, SelectedRights, title);
var ok = await ChannelService.SetAdminAsync(executeAccountId.Value, ch.TelegramId, username, SelectedRights, title);
if (!ok)
{
failed++;

View File

@@ -10,7 +10,7 @@
<DialogContent>
<MudStack Spacing="2">
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
<span>将使用“执行账号”邀请用户加入所选频道。支持输入多个用户名,换行分隔。</span>
<span>将使用“执行账号”邀请用户加入所选频道(默认:每个频道的创建账号)。支持输入多个用户名,换行分隔。</span>
<MudText Typo="Typo.body2" Class="mt-1">
支持:<b>@@username</b>、<b>username</b>
</MudText>
@@ -38,6 +38,7 @@
</MudGrid>
<MudSelect @bind-Value="selectedAccountId" Label="执行账号" Variant="Variant.Outlined" Dense="true">
<MudSelectItem Value="0">每个频道创建账号(默认)</MudSelectItem>
@foreach (var a in accounts)
{
<MudSelectItem Value="@a.Id">@FormatAccountLabel(a)</MudSelectItem>
@@ -123,16 +124,27 @@
.ToList();
var all = (await AccountManagement.GetAllAccountsAsync()).ToList();
accounts.AddRange(all.Where(a => a.IsActive));
if (DefaultAccountId > 0 && accounts.Any(a => a.Id == DefaultAccountId))
selectedAccountId = DefaultAccountId;
else
selectedAccountId = accounts.FirstOrDefault()?.Id ?? 0;
accounts.AddRange(all.Where(a => a.IsActive && a.Category?.ExcludeFromOperations != true));
selectedAccountId = 0;
await ReloadPresetsAsync();
}
private int? ResolveExecuteAccountId(Channel channel)
{
if (channel == null)
return null;
if (selectedAccountId > 0)
return selectedAccountId;
var creatorId = channel.CreatorAccountId;
if (creatorId == null || creatorId.Value <= 0)
return null;
return accounts.Any(a => a.Id == creatorId.Value) ? creatorId.Value : null;
}
private async Task ReloadPresetsAsync()
{
loadingPresets = true;
@@ -218,12 +230,6 @@
if (running)
return;
if (selectedAccountId <= 0)
{
Snackbar.Add("请选择一个执行账号", Severity.Warning);
return;
}
var usernames = ParseUsernames(usernamesText);
if (usernames.Count == 0)
{
@@ -258,6 +264,16 @@
{
foreach (var ch in channels)
{
var executeAccountId = ResolveExecuteAccountId(ch);
if (executeAccountId == null)
{
done += usernames.Count;
failed += usernames.Count;
failures.Add($"{ch.Title}:无可用执行账号(将使用创建账号执行;但该频道无创建账号/创建账号不可用)");
StateHasChanged();
continue;
}
foreach (var username in usernames)
{
_cts.Token.ThrowIfCancellationRequested();
@@ -267,7 +283,7 @@
try
{
var result = await ChannelService.InviteUserAsync(selectedAccountId, ch.TelegramId, username);
var result = await ChannelService.InviteUserAsync(executeAccountId.Value, ch.TelegramId, username);
if (!result.Success)
{
failed++;

View File

@@ -19,6 +19,8 @@
<MudTextField @bind-Value="newCategoryName" Label="分类名称" Variant="Variant.Outlined" />
<MudColorPicker @bind-Value="newCategoryColor" Label="分类颜色" Class="mt-3" />
<MudTextField @bind-Value="newCategoryDesc" Label="描述" Variant="Variant.Outlined" Lines="3" Class="mt-3" />
<MudSwitch @bind-Value="newCategoryExcludeFromOperations" Color="Color.Primary" Class="mt-3"
Label="排除操作(不出现在创建/批量任务中)" />
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
@@ -40,6 +42,7 @@
<MudTh>分类名称</MudTh>
<MudTh>颜色</MudTh>
<MudTh>描述</MudTh>
<MudTh>排除操作</MudTh>
<MudTh>账号数量</MudTh>
<MudTh>操作</MudTh>
</HeaderContent>
@@ -51,6 +54,16 @@
<div style="@($"width: 30px; height: 30px; background-color: {context.Color ?? "#9E9E9E"}; border-radius: 4px;")"></div>
</MudTd>
<MudTd DataLabel="描述">@(context.Description ?? "-")</MudTd>
<MudTd DataLabel="排除操作">
@if (context.ExcludeFromOperations)
{
<MudChip T="string" Size="Size.Small" Color="Color.Warning">是</MudChip>
}
else
{
<span>-</span>
}
</MudTd>
<MudTd DataLabel="账号数量">@context.Accounts.Count</MudTd>
<MudTd DataLabel="操作">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small" Color="Color.Primary"
@@ -115,6 +128,7 @@
private string newCategoryName = "";
private MudColor newCategoryColor = "#1976d2";
private string newCategoryDesc = "";
private bool newCategoryExcludeFromOperations;
private List<AccountCategory> categories = new();
private bool loading = true;
@@ -215,6 +229,7 @@
Name = newCategoryName,
Color = newCategoryColor.Value,
Description = newCategoryDesc,
ExcludeFromOperations = newCategoryExcludeFromOperations,
CreatedAt = DateTime.UtcNow
};
@@ -226,6 +241,7 @@
newCategoryName = "";
newCategoryColor = "#1976d2";
newCategoryDesc = "";
newCategoryExcludeFromOperations = false;
}
catch (Exception ex)
{
@@ -290,6 +306,7 @@
category.Name = model.Name.Trim();
category.Color = model.Color;
category.Description = model.Description;
category.ExcludeFromOperations = model.ExcludeFromOperations;
await CategoryManagement.UpdateCategoryAsync(category);
await LoadCategories();

View File

@@ -65,7 +65,9 @@
<MudMenu Label="批量操作" Variant="Variant.Outlined" Color="Color.Default" StartIcon="@Icons.Material.Filled.MoreHoriz"
Size="Size.Small" Disabled="@(loading || selectedBotId <= 0)" Dense="true" EndIcon="@Icons.Material.Filled.KeyboardArrowDown">
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="ExportInvites">导出邀请(已选)</MudMenuItem>
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="OpenSetAdmins">批量设置管理员(已选)</MudMenuItem>
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="OpenBatchInviteMembers">批量邀请成员(已选)</MudMenuItem>
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="OpenBatchSetAdminsByAccount">批量设置管理员(账号执行)</MudMenuItem>
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="OpenSetAdmins">批量设置管理员(机器人/ID</MudMenuItem>
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="OpenBatchSetCategory">批量设置分类(已选)</MudMenuItem>
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="OpenBanMember">踢出用户(已选)</MudMenuItem>
</MudMenu>
@@ -450,6 +452,58 @@
}
}
private async Task OpenBatchInviteMembers()
{
if (selectedBotId <= 0)
return;
if (selected.Count == 0)
{
Snackbar.Add("请先选择频道", Severity.Info);
return;
}
var parameters = new DialogParameters
{
["BotId"] = selectedBotId,
["Channels"] = selected.ToList()
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
var dialog = DialogService.Show<BotChannelBatchInviteDialog>("批量邀请成员", parameters, options);
var result = await dialog.Result;
if (!result.Canceled)
{
// 邀请不影响本地数据,但用户可能期望刷新一下
await LoadBotData();
}
}
private async Task OpenBatchSetAdminsByAccount()
{
if (selectedBotId <= 0)
return;
if (selected.Count == 0)
{
Snackbar.Add("请先选择频道", Severity.Info);
return;
}
var parameters = new DialogParameters
{
["BotId"] = selectedBotId,
["Channels"] = selected.ToList()
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
var dialog = DialogService.Show<BotChannelBatchAdminsDialog>("批量设置管理员(账号执行)", parameters, options);
var result = await dialog.Result;
if (!result.Canceled)
{
// 管理员设置后不影响本地数据,但用户可能期望刷新一下
await LoadBotData();
}
}
private async Task OpenBatchSetCategory()
{
if (selectedBotId <= 0)

View File

@@ -101,7 +101,9 @@
try
{
var accountList = await AccountManagement.GetActiveAccountsAsync();
accounts = accountList.ToList();
accounts = accountList
.Where(a => a.Category?.ExcludeFromOperations != true)
.ToList();
}
catch (Exception ex)
{