You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
583 lines
25 KiB
583 lines
25 KiB
@{
|
|
ViewData["Title"] = "主页";
|
|
var clients = ViewBag.Clients as List<dynamic>;
|
|
var ipGroups = ViewBag.IpGroups as List<dynamic>;
|
|
}
|
|
|
|
<style>
|
|
.client-table {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.client-table th {
|
|
background-color: #f8f9fa !important;
|
|
color: #212529 !important;
|
|
border-top: none;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.client-table td {
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.project-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
justify-content: flex-end;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.project-actions .btn {
|
|
margin: 0;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.badge {
|
|
font-size: 0.75rem;
|
|
padding: 0.375rem 0.75rem;
|
|
}
|
|
|
|
.badge-red, .badge.badge-red {
|
|
background-color: #ffd6d6 !important;
|
|
color: #111 !important;
|
|
font-weight: bold !important;
|
|
font-size: 1.1rem !important;
|
|
letter-spacing: 1px !important;
|
|
box-shadow: none !important;
|
|
padding: 0.375rem 0.75rem !important;
|
|
border-radius: 0.375rem !important;
|
|
display: inline-block !important;
|
|
text-shadow: none !important;
|
|
}
|
|
|
|
/* 页面容器 */
|
|
.card-body {
|
|
max-height: calc(100vh - 280px);
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.table-responsive {
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* 自定义滚动条样式 */
|
|
.card-body::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.card-body::-webkit-scrollbar-track {
|
|
background: #f1f1f1;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.card-body::-webkit-scrollbar-thumb {
|
|
background: #c1c1c1;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.card-body::-webkit-scrollbar-thumb:hover {
|
|
background: #a8a8a8;
|
|
}
|
|
|
|
.card-header {
|
|
background-color: #f8f9fa;
|
|
border-bottom: 1px solid #dee2e6;
|
|
}
|
|
|
|
.card-title {
|
|
margin-bottom: 0;
|
|
color: #495057;
|
|
}
|
|
|
|
.card-title i {
|
|
margin-right: 0.5rem;
|
|
}
|
|
|
|
/* 响应式调整 */
|
|
@@media (max-width: 768px) {
|
|
.card-body {
|
|
max-height: calc(100vh - 240px);
|
|
}
|
|
}
|
|
|
|
@@media (max-width: 576px) {
|
|
.card-body {
|
|
max-height: calc(100vh - 220px);
|
|
}
|
|
}
|
|
|
|
.status-dot {
|
|
display: inline-block;
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
margin-right: 6px;
|
|
vertical-align: middle;
|
|
}
|
|
.status-running {
|
|
background: #28a745;
|
|
}
|
|
.status-stopped {
|
|
background: #dc3545;
|
|
}
|
|
.status-idle {
|
|
background: #adb5bd;
|
|
}
|
|
.status-text {
|
|
font-weight: bold;
|
|
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: 20%">AgentIP</th>
|
|
<th style="width: 15%">AgentPort</th>
|
|
<th style="width: 15%">Key</th>
|
|
<th style="width: 10%">apn</th>
|
|
<th style="width: 10%">band</th>
|
|
<th style="width: 10%">comment</th>
|
|
<th style="width: 10%">状态</th>
|
|
<th style="width: 10%" 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>
|
|
<select class="form-control form-control-sm network-key-select" data-ip="@group.Ip" data-port="@group.Port" onchange="onKeyChange(this, '@group.Ip')">
|
|
<option value="">加载中...</option>
|
|
</select>
|
|
</td>
|
|
<td class="apn-cell" data-ip="@group.Ip"></td>
|
|
<td class="band-cell" data-ip="@group.Ip"></td>
|
|
<td class="comment-cell" data-ip="@group.Ip"></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">
|
|
<div class="btn-group d-inline-flex" role="group">
|
|
<a class="btn btn-outline-success btn-sm" href="#" title="启动网络" onclick="startIpGroup('@group.Ip')">
|
|
<i class="fas fa-play"></i>
|
|
</a>
|
|
<a class="btn btn-outline-danger btn-sm" href="#" title="停止网络" onclick="stopIpGroup('@group.Ip')">
|
|
<i class="fas fa-stop"></i>
|
|
</a>
|
|
<a class="btn btn-outline-primary btn-sm" href="@Url.Action("NetworkConfig", "Home", new { ip = group.Ip, port = group.Port })" title="添加/管理网络配置">
|
|
<i class="fas fa-plus"></i>
|
|
</a>
|
|
</div>
|
|
</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">
|
|
<h3 class="card-title">
|
|
<i class="fas fa-server"></i>客户端管理
|
|
</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: 20%">客户端名称</th>
|
|
<th style="width: 25%">地址</th>
|
|
<th style="width: 10%">状态</th>
|
|
<th style="width: 8%" class="text-center">SSL</th>
|
|
<th style="width: 8%" class="text-center">启用</th>
|
|
<th style="width: 29%" class="text-center">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (clients != null)
|
|
{
|
|
foreach (var client in clients)
|
|
{
|
|
var config = client.Config as LTEMvcApp.Models.ClientConfig;
|
|
var state = (LTEMvcApp.Models.ClientState)client.State;
|
|
|
|
<tr>
|
|
<td>@client.Config.Name</td>
|
|
<td>@client.Config.Address</td>
|
|
<td>
|
|
@if (state == LTEMvcApp.Models.ClientState.Connected)
|
|
{
|
|
<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>
|
|
}
|
|
else
|
|
{
|
|
<span class="status-dot status-stopped"></span>
|
|
|
|
<span class="status-text">停止</span>
|
|
}
|
|
</td>
|
|
<td class="text-center">
|
|
@if (config.Ssl)
|
|
{
|
|
<i class="fas fa-check-circle text-success"></i>
|
|
}
|
|
else
|
|
{
|
|
<i class="fas fa-times-circle text-muted"></i>
|
|
}
|
|
</td>
|
|
<td class="text-center">
|
|
@if (config.Enabled)
|
|
{
|
|
<i class="fas fa-check-circle text-success"></i>
|
|
}
|
|
else
|
|
{
|
|
<i class="fas fa-times-circle text-muted"></i>
|
|
}
|
|
</td>
|
|
<td class="project-actions text-right">
|
|
<a class="btn btn-primary btn-sm @(state == LTEMvcApp.Models.ClientState.Connected ? "disabled" : "")" href="#" onclick="startTestClient('@config.Address')">
|
|
<i class="fas fa-play"></i> 启动
|
|
</a>
|
|
<a class="btn btn-danger btn-sm @(state != LTEMvcApp.Models.ClientState.Connected ? "disabled" : "")" href="#" onclick="stopTestClient('@config.Address')">
|
|
<i class="fas fa-stop"></i> 停止
|
|
</a>
|
|
<a class="btn btn-info btn-sm @(state != LTEMvcApp.Models.ClientState.Connected ? "disabled" : "")" href="@Url.Action("ClientMessages", "Home", new { address = config.Address })">
|
|
<i class="fas fa-list"></i> 消息
|
|
</a>
|
|
<a href="@Url.Action("TestClientConfig", "Home", new { address = config.Address })" class="btn btn-sm btn-warning @(state == LTEMvcApp.Models.ClientState.Connected ? "disabled" : "")">
|
|
<i class="fas fa-cog"></i> 配置
|
|
</a>
|
|
<a class="btn btn-sm btn-outline-danger @(state == LTEMvcApp.Models.ClientState.Connected ? "disabled" : "")" href="#" onclick="deleteTestClient('@config.Address')">
|
|
<i class="fas fa-trash"></i> 删除
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
}
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@section Scripts {
|
|
<script>
|
|
// 页面加载完成后异步初始化网络配置下拉框
|
|
$(document).ready(function() {
|
|
// 延迟加载,避免阻塞页面渲染
|
|
setTimeout(function() {
|
|
loadAllNetworkConfigs();
|
|
}, 100);
|
|
});
|
|
|
|
function loadAllNetworkConfigs() {
|
|
$('.network-key-select').each(function() {
|
|
const select = $(this);
|
|
const ip = select.data('ip');
|
|
const port = select.data('port');
|
|
$.ajax({
|
|
url: '/api/ipgroup/network-config',
|
|
type: 'GET',
|
|
data: { ip, port },
|
|
success: function(res) {
|
|
select.empty();
|
|
if (res.isSuccess && res.data && res.data.length > 0) {
|
|
select.append('<option value="">请选择Key</option>');
|
|
res.data.forEach(function(cfg) {
|
|
select.append('<option value="' + cfg.configKey + '">' + cfg.configKey + '</option>');
|
|
});
|
|
} else {
|
|
select.append('<option value="">无可用Key</option>');
|
|
}
|
|
},
|
|
error: function() {
|
|
select.empty();
|
|
select.append('<option value="">加载失败</option>');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function onKeyChange(select, ip) {
|
|
const key = $(select).val();
|
|
const port = $(select).data('port');
|
|
if (!key) {
|
|
$('.apn-cell[data-ip="' + ip + '"]').text('');
|
|
$('.band-cell[data-ip="' + ip + '"]').text('');
|
|
$('.comment-cell[data-ip="' + ip + '"]').text('');
|
|
return;
|
|
}
|
|
// 联动显示apn/band/comment
|
|
$.ajax({
|
|
url: '/api/ipgroup/network-config',
|
|
type: 'GET',
|
|
data: { ip: ip, port: port },
|
|
success: function(res) {
|
|
if (res.isSuccess && res.data) {
|
|
const cfg = res.data.find(x => x.configKey === key);
|
|
$('.apn-cell[data-ip="' + ip + '"]').text(cfg ? (cfg.apn || '') : '');
|
|
$('.band-cell[data-ip="' + ip + '"]').text(cfg && cfg.band ? cfg.band.join(',') : '');
|
|
$('.comment-cell[data-ip="' + ip + '"]').text(cfg ? (cfg.comment || '') : '');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function startTestClient(address) {
|
|
var row = $('tr').filter(function() {
|
|
return $(this).find('td:eq(1)').text().trim() === address;
|
|
});
|
|
$.ajax({
|
|
url: '/api/testconfig/start',
|
|
type: 'POST',
|
|
contentType: 'application/json',
|
|
data: JSON.stringify({ address: address }),
|
|
success: function() {
|
|
showToast('启动请求已发送!', 'success');
|
|
// 更新当前行状态
|
|
row.find('td').eq(2).html('<span class="status-dot status-running"></span><span class="status-text">运行</span>');
|
|
// 更新按钮状态
|
|
row.find('.btn-primary').addClass('disabled');
|
|
row.find('.btn-danger').removeClass('disabled');
|
|
row.find('.btn-info').removeClass('disabled');
|
|
row.find('.btn-warning').addClass('disabled');
|
|
row.find('.btn-outline-danger').addClass('disabled');
|
|
},
|
|
error: function(xhr) {
|
|
showToast('启动失败:' + xhr.responseText, 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
function stopTestClient(address) {
|
|
var row = $('tr').filter(function() {
|
|
return $(this).find('td:eq(1)').text().trim() === address;
|
|
});
|
|
$.ajax({
|
|
url: '/api/testconfig/stop',
|
|
type: 'POST',
|
|
contentType: 'application/json',
|
|
data: JSON.stringify({ address: address }),
|
|
success: function() {
|
|
showToast('停止请求已发送!', 'success');
|
|
// 更新当前行状态
|
|
row.find('td').eq(2).html('<span class="status-dot status-stopped"></span><span class="status-text">停止</span>');
|
|
// 更新按钮状态
|
|
row.find('.btn-primary').removeClass('disabled');
|
|
row.find('.btn-danger').addClass('disabled');
|
|
row.find('.btn-info').addClass('disabled');
|
|
row.find('.btn-warning').removeClass('disabled');
|
|
row.find('.btn-outline-danger').removeClass('disabled');
|
|
},
|
|
error: function(xhr) {
|
|
showToast('停止失败:' + xhr.responseText, 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
function deleteTestClient(address) {
|
|
if (!confirm('确定要删除该测试客户端吗?')) return;
|
|
$.ajax({
|
|
url: '/api/testconfig/address/' + encodeURIComponent(address),
|
|
type: 'DELETE',
|
|
success: function() {
|
|
alert('删除成功!');
|
|
setTimeout(() => location.reload(), 1000);
|
|
},
|
|
error: function(xhr) {
|
|
alert('删除失败:' + xhr.responseText);
|
|
}
|
|
});
|
|
}
|
|
|
|
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 key = row.find('.network-key-select').val();
|
|
if (!key) {
|
|
alert('请先选择网络Key!');
|
|
return;
|
|
}
|
|
if (!confirm('确定要启动该网络吗?')) return;
|
|
$.ajax({
|
|
url: '/api/ipgroup/start',
|
|
type: 'POST',
|
|
contentType: 'application/json',
|
|
data: JSON.stringify({ ip: ip, port: port, key: key }),
|
|
success: function(response) {
|
|
showToast('网络启动成功', 'success');
|
|
// 解析嵌套的response字段
|
|
if (response && response.response) {
|
|
try {
|
|
var innerResponse = JSON.parse(response.response);
|
|
if (innerResponse && innerResponse.data === 1) {
|
|
row.find('td').eq(6).html('<span class="status-dot status-running"></span><span class="status-text">运行</span>');
|
|
}
|
|
} catch (e) {
|
|
console.log('解析响应数据失败:', e);
|
|
}
|
|
}
|
|
loadAllNetworkConfigs();
|
|
},
|
|
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();
|
|
var key = row.find('.network-key-select').val();
|
|
if (!key) {
|
|
alert('请先选择网络Key!');
|
|
return;
|
|
}
|
|
if (!confirm('确定要停止该网络吗?')) return;
|
|
$.ajax({
|
|
url: '/api/ipgroup/stop',
|
|
type: 'POST',
|
|
contentType: 'application/json',
|
|
data: JSON.stringify({ ip: ip, port: port, key: key }),
|
|
success: function(response) {
|
|
showToast('网络停止成功', 'success');
|
|
// 解析嵌套的response字段
|
|
if (response && response.response) {
|
|
try {
|
|
var innerResponse = JSON.parse(response.response);
|
|
if (innerResponse && innerResponse.isSuccess) {
|
|
row.find('td').eq(6).html('<span class="status-dot status-stopped"></span><span class="status-text">停止</span>');
|
|
}
|
|
} catch (e) {
|
|
console.log('解析响应数据失败:', e);
|
|
}
|
|
}
|
|
loadAllNetworkConfigs();
|
|
},
|
|
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);
|
|
}
|
|
|
|
$(function () {
|
|
$('[title]').tooltip({trigger: 'hover'});
|
|
});
|
|
</script>
|
|
}
|
|
|