Browse Source

大苏打

feature/MultiClientLog
root 1 month ago
parent
commit
398874bfa5
  1. 87
      LTEMvcApp/Controllers/HomeController.cs
  2. 213
      LTEMvcApp/Controllers/IpGroupController.cs
  3. 13
      LTEMvcApp/Models/StartCellularNetworkCommand.cs
  4. 13
      LTEMvcApp/Models/StopCellularNetworkCommand.cs
  5. 3
      LTEMvcApp/Program.cs
  6. 301
      LTEMvcApp/Services/HttpClientService.cs
  7. 218
      LTEMvcApp/Views/Home/Index.cshtml

87
LTEMvcApp/Controllers/HomeController.cs

@ -2,6 +2,7 @@ using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using LTEMvcApp.Models;
using LTEMvcApp.Services;
using System.Reflection;
namespace LTEMvcApp.Controllers;
@ -31,9 +32,95 @@ public class HomeController : Controller
var allTestClients = _webSocketManager.GetAllTestClientsWithState();
ViewBag.Clients = allTestClients;
// 处理IP分组数据
var ipGroups = ProcessIpGroups(allTestClients);
ViewBag.IpGroups = ipGroups;
return View();
}
/// <summary>
/// 处理IP分组数据
/// </summary>
private List<dynamic> ProcessIpGroups(List<dynamic> allTestClients)
{
var ipGroups = new List<dynamic>();
var ipDict = new Dictionary<string, List<dynamic>>();
// 按IP地址分组
foreach (var client in allTestClients)
{
var address = client.Config.Address;
var ip = address.Split(':')[0]; // 根据:分割获取IP
if (!ipDict.ContainsKey(ip))
{
ipDict[ip] = new List<dynamic>();
}
ipDict[ip].Add(client);
}
// 转换为动态对象列表
foreach (var kvp in ipDict)
{
var ip = kvp.Key;
var clients = kvp.Value;
// 计算该IP下客户端的状态
var connectedCount = clients.Count(c => (LTEMvcApp.Models.ClientState)c.State == LTEMvcApp.Models.ClientState.Connected);
var totalCount = clients.Count;
var groupState = connectedCount == 0 ? "停止" :
connectedCount == totalCount ? "运行" : "部分运行";
// 从TestConfigController获取已保存的Key
var savedKey = GetSavedIpGroupKey(ip);
ipGroups.Add(new
{
Ip = ip,
Clients = clients,
Port = 11003,
ConnectedCount = connectedCount,
State = groupState,
Key = savedKey
});
}
return ipGroups;
}
/// <summary>
/// 获取已保存的IP组Key
/// </summary>
/// <param name="ip">IP地址</param>
/// <returns>保存的Key值</returns>
private string GetSavedIpGroupKey(string ip)
{
try
{
// 通过反射获取IpGroupController中的静态字典
var ipGroupControllerType = typeof(IpGroupController);
var ipGroupKeysField = ipGroupControllerType.GetField("_ipGroupKeys",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
if (ipGroupKeysField != null)
{
var ipGroupKeys = ipGroupKeysField.GetValue(null) as Dictionary<string, string>;
if (ipGroupKeys != null && ipGroupKeys.TryGetValue(ip, out var key))
{
return key;
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "获取IP组Key失败: {Ip}", ip);
}
return string.Empty;
}
public IActionResult Privacy()
{
return View();

213
LTEMvcApp/Controllers/IpGroupController.cs

@ -0,0 +1,213 @@
using Microsoft.AspNetCore.Mvc;
using LTEMvcApp.Models;
using LTEMvcApp.Services;
using Microsoft.Extensions.Logging;
namespace LTEMvcApp.Controllers
{
/// <summary>
/// IP组管理控制器 - 负责IP组网络启动、停止和Key管理
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class IpGroupController : ControllerBase
{
private readonly HttpClientService _httpClientService;
private readonly ILogger<IpGroupController> _logger;
// 存储IP组的Key配置
private static readonly Dictionary<string, string> _ipGroupKeys = new();
public IpGroupController(HttpClientService httpClientService, ILogger<IpGroupController> logger)
{
_httpClientService = httpClientService;
_logger = logger;
}
/// <summary>
/// 保存IP组Key配置
/// </summary>
/// <param name="request">IP组Key请求</param>
/// <returns>操作结果</returns>
[HttpPost("key")]
public ActionResult SaveIpGroupKey([FromBody] IpGroupKeyRequest request)
{
if (string.IsNullOrEmpty(request.Ip))
return BadRequest("IP地址不能为空");
_ipGroupKeys[request.Ip] = request.Key ?? string.Empty;
_logger.LogInformation("保存IP组Key: {Ip} -> {Key}", request.Ip, request.Key);
return Ok(new { message = "IP组Key保存成功" });
}
/// <summary>
/// 启动IP组网络
/// </summary>
/// <param name="request">启动网络请求</param>
/// <returns>操作结果</returns>
[HttpPost("start")]
public async Task<ActionResult> StartIpGroup([FromBody] StartIpGroupRequest request)
{
if (string.IsNullOrEmpty(request.Ip))
return BadRequest("IP地址不能为空");
if (string.IsNullOrEmpty(request.Port))
return BadRequest("端口不能为空");
// 检查Key是否已配置
if (!_ipGroupKeys.TryGetValue(request.Ip, out var key) || string.IsNullOrEmpty(key))
return BadRequest("请先配置网络Key");
try
{
var apiUrl = $"http://{request.Ip}:{request.Port}/api/v1/CellularNetwork/start";
var command = new StartCellularNetworkCommand { Key = key };
_logger.LogInformation("启动IP组网络: {Ip}:{Port}, Key: {Key}", request.Ip, request.Port, key);
var response = await _httpClientService.PostJsonAsync(apiUrl, command);
_logger.LogInformation("IP组网络启动成功: {Response}", response);
return Ok(new { message = "网络启动成功", response });
}
catch (Exception ex)
{
_logger.LogError(ex, "启动IP组网络失败: {Ip}:{Port}", request.Ip, request.Port);
return BadRequest($"启动网络失败: {ex.Message}");
}
}
/// <summary>
/// 停止IP组网络
/// </summary>
/// <param name="request">停止网络请求</param>
/// <returns>操作结果</returns>
[HttpPost("stop")]
public async Task<ActionResult> StopIpGroup([FromBody] StopIpGroupRequest request)
{
if (string.IsNullOrEmpty(request.Ip))
return BadRequest("IP地址不能为空");
if (string.IsNullOrEmpty(request.Port))
return BadRequest("端口不能为空");
// 检查Key是否已配置
if (!_ipGroupKeys.TryGetValue(request.Ip, out var key) || string.IsNullOrEmpty(key))
return BadRequest("请先配置网络Key");
try
{
var apiUrl = $"http://{request.Ip}:{request.Port}/api/v1/CellularNetwork/stop";
var command = new StopCellularNetworkCommand { Key = key };
_logger.LogInformation("停止IP组网络: {Ip}:{Port}, Key: {Key}", request.Ip, request.Port, key);
var response = await _httpClientService.PostJsonAsync(apiUrl, command);
_logger.LogInformation("IP组网络停止成功: {Response}", response);
return Ok(new { message = "网络停止成功", response });
}
catch (Exception ex)
{
_logger.LogError(ex, "停止IP组网络失败: {Ip}:{Port}", request.Ip, request.Port);
return BadRequest($"停止网络失败: {ex.Message}");
}
}
/// <summary>
/// 获取IP组Key配置
/// </summary>
/// <param name="ip">IP地址</param>
/// <returns>Key配置</returns>
[HttpGet("key/{ip}")]
public ActionResult GetIpGroupKey(string ip)
{
if (string.IsNullOrEmpty(ip))
return BadRequest("IP地址不能为空");
var key = _ipGroupKeys.TryGetValue(ip, out var value) ? value : string.Empty;
return Ok(new { ip, key });
}
/// <summary>
/// 获取所有IP组Key配置
/// </summary>
/// <returns>所有IP组Key配置</returns>
[HttpGet("keys")]
public ActionResult GetAllIpGroupKeys()
{
return Ok(_ipGroupKeys);
}
/// <summary>
/// 删除IP组Key配置
/// </summary>
/// <param name="ip">IP地址</param>
/// <returns>操作结果</returns>
[HttpDelete("key/{ip}")]
public ActionResult DeleteIpGroupKey(string ip)
{
if (string.IsNullOrEmpty(ip))
return BadRequest("IP地址不能为空");
if (_ipGroupKeys.Remove(ip))
{
_logger.LogInformation("删除IP组Key: {Ip}", ip);
return Ok(new { message = "IP组Key删除成功" });
}
else
{
return NotFound($"未找到IP地址为 {ip} 的Key配置");
}
}
}
/// <summary>
/// IP组Key请求
/// </summary>
public class IpGroupKeyRequest
{
/// <summary>
/// IP地址
/// </summary>
public string Ip { get; set; } = string.Empty;
/// <summary>
/// Key值
/// </summary>
public string? Key { get; set; }
}
/// <summary>
/// 启动IP组请求
/// </summary>
public class StartIpGroupRequest
{
/// <summary>
/// IP地址
/// </summary>
public string Ip { get; set; } = string.Empty;
/// <summary>
/// 端口
/// </summary>
public string Port { get; set; } = string.Empty;
}
/// <summary>
/// 停止IP组请求
/// </summary>
public class StopIpGroupRequest
{
/// <summary>
/// IP地址
/// </summary>
public string Ip { get; set; } = string.Empty;
/// <summary>
/// 端口
/// </summary>
public string Port { get; set; } = string.Empty;
}
}

13
LTEMvcApp/Models/StartCellularNetworkCommand.cs

@ -0,0 +1,13 @@
namespace LTEMvcApp.Models
{
/// <summary>
/// 启动蜂窝网络命令
/// </summary>
public class StartCellularNetworkCommand
{
/// <summary>
/// 网络配置键
/// </summary>
public string Key { get; set; } = string.Empty;
}
}

13
LTEMvcApp/Models/StopCellularNetworkCommand.cs

@ -0,0 +1,13 @@
namespace LTEMvcApp.Models
{
/// <summary>
/// 停止蜂窝网络命令
/// </summary>
public class StopCellularNetworkCommand
{
/// <summary>
/// 网络接口名称
/// </summary>
public string Key { get; set; } = string.Empty;
}
}

3
LTEMvcApp/Program.cs

@ -11,6 +11,9 @@ builder.WebHost.ConfigureKestrel(serverOptions =>
// Add services to the container.
builder.Services.AddControllersWithViews();
// 注册HttpClient服务
builder.Services.AddHttpClient<HttpClientService>();
// 注册WebSocket管理服务
builder.Services.AddSingleton<WebSocketManagerService>(sp =>
new WebSocketManagerService(

301
LTEMvcApp/Services/HttpClientService.cs

@ -0,0 +1,301 @@
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Collections.Generic;
namespace LTEMvcApp.Services
{
/// <summary>
/// HTTP客户端服务 - 提供HTTP请求功能
/// </summary>
public class HttpClientService
{
private readonly HttpClient _httpClient;
private readonly ILogger<HttpClientService> _logger;
public HttpClientService(HttpClient httpClient, ILogger<HttpClientService> logger)
{
_httpClient = httpClient;
_logger = logger;
// 设置默认请求头
_httpClient.DefaultRequestHeaders.Add("User-Agent", "LTEMvcApp/1.0");
_httpClient.Timeout = TimeSpan.FromSeconds(30);
_logger.LogInformation("HttpClientService 初始化完成");
}
#region GET请求方法
/// <summary>
/// 发送GET请求
/// </summary>
/// <param name="url">请求URL</param>
/// <param name="headers">自定义请求头</param>
/// <returns>响应内容</returns>
public async Task<string> GetAsync(string url, Dictionary<string, string>? headers = null)
{
try
{
_logger.LogInformation("发送GET请求到: {Url}", url);
using var request = new HttpRequestMessage(HttpMethod.Get, url);
if (headers != null)
{
foreach (var header in headers)
{
request.Headers.Add(header.Key, header.Value);
}
}
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
_logger.LogInformation("GET请求成功,响应长度: {Length}", content.Length);
return content;
}
catch (Exception ex)
{
_logger.LogError(ex, "GET请求失败: {Url}", url);
throw;
}
}
/// <summary>
/// 发送GET请求并反序列化为指定类型
/// </summary>
/// <typeparam name="T">目标类型</typeparam>
/// <param name="url">请求URL</param>
/// <param name="headers">自定义请求头</param>
/// <returns>反序列化的对象</returns>
public async Task<T> GetAsync<T>(string url, Dictionary<string, string>? headers = null)
{
var json = await GetAsync(url, headers);
return JsonConvert.DeserializeObject<T>(json) ?? throw new InvalidOperationException("反序列化失败");
}
#endregion
#region POST请求方法
/// <summary>
/// 发送POST请求
/// </summary>
/// <param name="url">请求URL</param>
/// <param name="content">请求内容</param>
/// <param name="contentType">内容类型</param>
/// <param name="headers">自定义请求头</param>
/// <returns>响应内容</returns>
public async Task<string> PostAsync(string url, string content, string contentType = "application/json", Dictionary<string, string>? headers = null)
{
try
{
_logger.LogInformation("发送POST请求到: {Url}", url);
using var request = new HttpRequestMessage(HttpMethod.Post, url);
if (headers != null)
{
foreach (var header in headers)
{
request.Headers.Add(header.Key, header.Value);
}
}
request.Content = new StringContent(content, Encoding.UTF8, contentType);
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
_logger.LogInformation("POST请求成功,响应长度: {Length}", responseContent.Length);
return responseContent;
}
catch (Exception ex)
{
_logger.LogError(ex, "POST请求失败: {Url}", url);
throw;
}
}
/// <summary>
/// 发送POST请求(JSON格式)
/// </summary>
/// <param name="url">请求URL</param>
/// <param name="data">要发送的数据对象</param>
/// <param name="headers">自定义请求头</param>
/// <returns>响应内容</returns>
public async Task<string> PostJsonAsync(string url, object data, Dictionary<string, string>? headers = null)
{
var json = JsonConvert.SerializeObject(data);
return await PostAsync(url, json, "application/json", headers);
}
/// <summary>
/// 发送POST请求并反序列化响应
/// </summary>
/// <typeparam name="T">响应类型</typeparam>
/// <param name="url">请求URL</param>
/// <param name="data">要发送的数据对象</param>
/// <param name="headers">自定义请求头</param>
/// <returns>反序列化的响应对象</returns>
public async Task<T> PostJsonAsync<T>(string url, object data, Dictionary<string, string>? headers = null)
{
var json = await PostJsonAsync(url, data, headers);
return JsonConvert.DeserializeObject<T>(json) ?? throw new InvalidOperationException("反序列化失败");
}
#endregion
#region PUT请求方法
/// <summary>
/// 发送PUT请求
/// </summary>
/// <param name="url">请求URL</param>
/// <param name="content">请求内容</param>
/// <param name="contentType">内容类型</param>
/// <param name="headers">自定义请求头</param>
/// <returns>响应内容</returns>
public async Task<string> PutAsync(string url, string content, string contentType = "application/json", Dictionary<string, string>? headers = null)
{
try
{
_logger.LogInformation("发送PUT请求到: {Url}", url);
using var request = new HttpRequestMessage(HttpMethod.Put, url);
if (headers != null)
{
foreach (var header in headers)
{
request.Headers.Add(header.Key, header.Value);
}
}
request.Content = new StringContent(content, Encoding.UTF8, contentType);
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
_logger.LogInformation("PUT请求成功,响应长度: {Length}", responseContent.Length);
return responseContent;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "PUT请求失败: {Url}", url);
throw;
}
catch (TaskCanceledException ex)
{
_logger.LogError(ex, "PUT请求超时: {Url}", url);
throw;
}
}
/// <summary>
/// 发送PUT请求(JSON格式)
/// </summary>
/// <param name="url">请求URL</param>
/// <param name="data">要发送的数据对象</param>
/// <param name="headers">自定义请求头</param>
/// <returns>响应内容</returns>
public async Task<string> PutJsonAsync(string url, object data, Dictionary<string, string>? headers = null)
{
var json = JsonConvert.SerializeObject(data);
return await PutAsync(url, json, "application/json", headers);
}
#endregion
#region DELETE请求方法
/// <summary>
/// 发送DELETE请求
/// </summary>
/// <param name="url">请求URL</param>
/// <param name="headers">自定义请求头</param>
/// <returns>响应内容</returns>
public async Task<string> DeleteAsync(string url, Dictionary<string, string>? headers = null)
{
try
{
_logger.LogInformation("发送DELETE请求到: {Url}", url);
using var request = new HttpRequestMessage(HttpMethod.Delete, url);
if (headers != null)
{
foreach (var header in headers)
{
request.Headers.Add(header.Key, header.Value);
}
}
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
_logger.LogInformation("DELETE请求成功,响应长度: {Length}", content.Length);
return content;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "DELETE请求失败: {Url}", url);
throw;
}
catch (TaskCanceledException ex)
{
_logger.LogError(ex, "DELETE请求超时: {Url}", url);
throw;
}
}
#endregion
#region 工具方法
/// <summary>
/// 设置默认请求头
/// </summary>
/// <param name="name">头部名称</param>
/// <param name="value">头部值</param>
public void SetDefaultHeader(string name, string value)
{
_httpClient.DefaultRequestHeaders.Remove(name);
_httpClient.DefaultRequestHeaders.Add(name, value);
_logger.LogInformation("设置默认请求头: {Name} = {Value}", name, value);
}
/// <summary>
/// 设置超时时间
/// </summary>
/// <param name="timeout">超时时间</param>
public void SetTimeout(TimeSpan timeout)
{
_httpClient.Timeout = timeout;
_logger.LogInformation("设置超时时间: {Timeout}", timeout);
}
/// <summary>
/// 获取当前HttpClient实例
/// </summary>
/// <returns>HttpClient实例</returns>
public HttpClient GetHttpClient()
{
return _httpClient;
}
#endregion
}
}

218
LTEMvcApp/Views/Home/Index.cshtml

@ -1,6 +1,7 @@
@{
ViewData["Title"] = "主页";
var clients = ViewBag.Clients as List<dynamic>;
var ipGroups = ViewBag.IpGroups as List<dynamic>;
}
<style>
@ -129,10 +130,109 @@
color: #333;
vertical-align: middle;
}
/* IP分组表格样式 */
.ip-group-table {
margin-top: 2rem;
}
.ip-group-table .form-control-sm {
height: 30px;
font-size: 0.875rem;
padding: 0.25rem 0.5rem;
}
.ip-group-table .badge-info {
background-color: #17a2b8;
color: white;
font-size: 0.75rem;
padding: 0.375rem 0.75rem;
}
.ip-group-table .card-header {
background-color: #e3f2fd;
border-bottom: 1px solid #bbdefb;
}
.ip-group-table .card-title {
color: #1976d2;
}
.ip-group-table .card-title i {
color: #1976d2;
}
</style>
<div class="container">
<!-- IP分组管理表格 -->
<div class="row">
<div class="col-12">
<div class="card ip-group-table">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-network-wired"></i>AgentService
</h3>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-striped projects client-table">
<thead>
<tr>
<th style="width: 25%">AgentIP</th>
<th style="width: 25%">AgentPort</th>
<th style="width: 10%">Key</th>
<th style="width: 10%">状态</th>
<th style="width: 16%" class="text-center">操作</th>
</tr>
</thead>
<tbody>
@if (ipGroups != null)
{
foreach (var group in ipGroups)
{
<tr>
<td>@group.Ip</td>
<td>@group.Port</td>
<td>
<input type="text" class="form-control form-control-sm"
value="@group.Key"
placeholder="输入Key"
onchange="updateGroupKey('@group.Ip', this.value)">
</td>
<td>
@if (group.State == "运行")
{
<span class="status-dot status-running"></span><span class="status-text">运行</span>
}
else if (group.State == "部分运行")
{
<span class="status-dot status-running"></span><span class="status-text">部分运行</span>
}
else
{
<span class="status-dot status-stopped"></span><span class="status-text">停止</span>
}
</td>
<td class="project-actions text-right">
<a class="btn btn-primary btn-sm" href="#" onclick="startIpGroup('@group.Ip')">
<i class="fas fa-play"></i> 启动网络
</a>
<a class="btn btn-danger btn-sm" href="#" onclick="stopIpGroup('@group.Ip')">
<i class="fas fa-stop"></i> 停止网络
</a>
</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
@ -167,15 +267,21 @@
<td>
@if (state == LTEMvcApp.Models.ClientState.Connected)
{
<span class="status-dot status-running"></span><span class="status-text">运行</span>
<span class="status-dot status-running"></span>
<span class="status-text">运行</span>
}
else if (state == LTEMvcApp.Models.ClientState.Stop)
{
<span class="status-dot status-idle"></span><span class="status-text">未启动</span>
<span class="status-dot status-idle"></span>
<span class="status-text">未启动</span>
}
else
{
<span class="status-dot status-stopped"></span><span class="status-text">停止</span>
<span class="status-dot status-stopped"></span>
<span class="status-text">停止</span>
}
</td>
<td class="text-center">
@ -275,5 +381,111 @@
}
});
}
function updateGroupKey(ip, key) {
// 发送AJAX请求保存到服务器
$.ajax({
url: '/api/ipgroup/key',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ ip: ip, key: key }),
success: function(response) {
console.log('IP组Key保存成功:', response);
// 可以显示一个小的成功提示
showToast('Key保存成功', 'success');
},
error: function(xhr) {
console.error('IP组Key保存失败:', xhr.responseText);
showToast('Key保存失败', 'error');
}
});
}
function startIpGroup(ip) {
// 获取当前行的端口信息
var row = $('tr').filter(function() {
return $(this).find('td:first').text().trim() === ip;
});
var port = row.find('td:eq(1)').text().trim(); // 第二列是端口
var keyInput = row.find('input[type="text"]');
var key = keyInput.val().trim();
if (!key) {
alert('请先填写网络Key!');
keyInput.focus();
return;
}
if (!confirm('确定要启动该网络吗?')) return;
$.ajax({
url: '/api/ipgroup/start',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ ip: ip, port: port }),
success: function(response) {
showToast('网络启动成功', 'success');
setTimeout(() => location.reload(), 2000);
},
error: function(xhr) {
var errorMsg = '启动失败';
try {
var errorResponse = JSON.parse(xhr.responseText);
errorMsg = errorResponse.message || errorResponse;
} catch (e) {
errorMsg = xhr.responseText || '启动失败';
}
showToast(errorMsg, 'error');
}
});
}
function stopIpGroup(ip) {
// 获取当前行的端口信息
var row = $('tr').filter(function() {
return $(this).find('td:first').text().trim() === ip;
});
var port = row.find('td:eq(1)').text().trim(); // 第二列是端口
if (!confirm('确定要停止该网络吗?')) return;
$.ajax({
url: '/api/ipgroup/stop',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ ip: ip, port: port }),
success: function(response) {
showToast('网络停止成功', 'success');
setTimeout(() => location.reload(), 2000);
},
error: function(xhr) {
var errorMsg = '停止失败';
try {
var errorResponse = JSON.parse(xhr.responseText);
errorMsg = errorResponse.message || errorResponse;
} catch (e) {
errorMsg = xhr.responseText || '停止失败';
}
showToast(errorMsg, 'error');
}
});
}
function showToast(message, type) {
// 简单的提示函数,可以替换为更美观的toast组件
var alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
var alertHtml = '<div class="alert ' + alertClass + ' alert-dismissible fade show position-fixed" style="top: 20px; right: 20px; z-index: 9999;" role="alert">' +
message +
'<button type="button" class="btn-close" data-bs-dismiss="alert"></button>' +
'</div>';
$('body').append(alertHtml);
// 3秒后自动移除
setTimeout(function() {
$('.alert').fadeOut();
}, 3000);
}
</script>
}

Loading…
Cancel
Save