Browse Source
- 将首页重构为客户端管理表格,取代原有的状态卡片,界面更清晰。 - 在表格中为每个客户端提供独立的启动、停止、配置和查看消息的操作。 - 增强了测试客户端配置页面,支持所有日志层的完整配置(级别、大小、负载)。 - 修正了服务层,添加了 GetTestClient 方法以正确获取客户端实例。 - 根据要求,在配置界面移除了 EVENT 日志层。master
10 changed files with 1098 additions and 215 deletions
@ -0,0 +1,80 @@ |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace LTEMvcApp.Models |
|||
{ |
|||
/// <summary>
|
|||
/// 日志层配置
|
|||
/// </summary>
|
|||
public class LogLayerConfig |
|||
{ |
|||
/// <summary>
|
|||
/// 日志级别
|
|||
/// </summary>
|
|||
[Required] |
|||
public string Level { get; set; } = "warn"; |
|||
|
|||
/// <summary>
|
|||
/// 最大大小
|
|||
/// </summary>
|
|||
public int MaxSize { get; set; } = 1; |
|||
|
|||
/// <summary>
|
|||
/// 是否包含负载
|
|||
/// </summary>
|
|||
public bool Payload { get; set; } = false; |
|||
|
|||
/// <summary>
|
|||
/// 过滤器(用于兼容性)
|
|||
/// </summary>
|
|||
public string Filter { get; set; } = "warn"; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 日志层类型枚举
|
|||
/// </summary>
|
|||
public static class LogLayerTypes |
|||
{ |
|||
public const string PHY = "PHY"; |
|||
public const string MAC = "MAC"; |
|||
public const string RLC = "RLC"; |
|||
public const string PDCP = "PDCP"; |
|||
public const string RRC = "RRC"; |
|||
public const string NAS = "NAS"; |
|||
public const string S1AP = "S1AP"; |
|||
public const string NGAP = "NGAP"; |
|||
public const string GTPU = "GTPU"; |
|||
public const string X2AP = "X2AP"; |
|||
public const string XnAP = "XnAP"; |
|||
public const string M2AP = "M2AP"; |
|||
public const string EVENT = "EVENT"; |
|||
|
|||
/// <summary>
|
|||
/// 获取所有日志层类型
|
|||
/// </summary>
|
|||
public static readonly string[] AllLayers = new[] |
|||
{ |
|||
PHY, MAC, RLC, PDCP, RRC, NAS, S1AP, NGAP, GTPU, X2AP, XnAP, M2AP, EVENT |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// 获取日志级别选项
|
|||
/// </summary>
|
|||
public static readonly string[] LogLevels = new[] |
|||
{ |
|||
"debug", "info", "warn", "error" |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// 获取默认日志级别
|
|||
/// </summary>
|
|||
public static string GetDefaultLevel(string layer) |
|||
{ |
|||
return layer switch |
|||
{ |
|||
NAS or RRC or S1AP or NGAP or X2AP => "debug", |
|||
EVENT or M2AP => "info", |
|||
_ => "warn" |
|||
}; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,181 @@ |
|||
@{ |
|||
ViewData["Title"] = "客户端消息队列"; |
|||
var clientName = ViewBag.ClientName as string ?? "TestClient"; |
|||
} |
|||
|
|||
<div class="container-fluid"> |
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h3 class="card-title">客户端消息队列 - @clientName</h3> |
|||
<div class="card-tools"> |
|||
<button type="button" class="btn btn-primary btn-sm" onclick="refreshMessages()"> |
|||
<i class="fas fa-sync-alt"></i> 刷新 |
|||
</button> |
|||
<a href="@Url.Action("TestClientConfig", "Home")" class="btn btn-info btn-sm"> |
|||
<i class="fas fa-cog"></i> 配置 |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div class="row"> |
|||
<div class="col-md-6"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h5 class="card-title"> |
|||
<i class="fas fa-paper-plane text-success"></i> 发送消息 |
|||
<span class="badge badge-primary" id="sentCount">0</span> |
|||
</h5> |
|||
</div> |
|||
<div class="card-body" style="max-height: 600px; overflow-y: auto;"> |
|||
<div id="sentMessages"> |
|||
<div class="text-muted text-center"> |
|||
<i class="fas fa-info-circle"></i> 暂无发送消息 |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h5 class="card-title"> |
|||
<i class="fas fa-download text-info"></i> 接收消息 |
|||
<span class="badge badge-info" id="receivedCount">0</span> |
|||
</h5> |
|||
</div> |
|||
<div class="card-body" style="max-height: 600px; overflow-y: auto;"> |
|||
<div id="receivedMessages"> |
|||
<div class="text-muted text-center"> |
|||
<i class="fas fa-info-circle"></i> 暂无接收消息 |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
@section Scripts { |
|||
<script> |
|||
var clientName = '@clientName'; |
|||
var refreshInterval; |
|||
|
|||
$(document).ready(function() { |
|||
loadMessages(); |
|||
// 每5秒自动刷新一次 |
|||
refreshInterval = setInterval(loadMessages, 5000); |
|||
}); |
|||
|
|||
function loadMessages() { |
|||
$.ajax({ |
|||
url: '/api/websocket/clients/' + encodeURIComponent(clientName) + '/messages', |
|||
type: 'GET', |
|||
success: function(data) { |
|||
displayMessages(data); |
|||
}, |
|||
error: function(xhr) { |
|||
if (xhr.status === 404) { |
|||
$('#sentMessages').html('<div class="text-warning text-center"><i class="fas fa-exclamation-triangle"></i> 客户端未连接</div>'); |
|||
$('#receivedMessages').html('<div class="text-warning text-center"><i class="fas fa-exclamation-triangle"></i> 客户端未连接</div>'); |
|||
} else { |
|||
$('#sentMessages').html('<div class="text-danger text-center"><i class="fas fa-times-circle"></i> 加载失败</div>'); |
|||
$('#receivedMessages').html('<div class="text-danger text-center"><i class="fas fa-times-circle"></i> 加载失败</div>'); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
function displayMessages(data) { |
|||
// 更新计数 |
|||
$('#sentCount').text(data.sentCount || 0); |
|||
$('#receivedCount').text(data.receivedCount || 0); |
|||
|
|||
// 显示发送消息 |
|||
var sentHtml = ''; |
|||
if (data.sentMessages && data.sentMessages.length > 0) { |
|||
data.sentMessages.forEach(function(message, index) { |
|||
sentHtml += createMessageCard(message, index, 'sent'); |
|||
}); |
|||
} else { |
|||
sentHtml = '<div class="text-muted text-center"><i class="fas fa-info-circle"></i> 暂无发送消息</div>'; |
|||
} |
|||
$('#sentMessages').html(sentHtml); |
|||
|
|||
// 显示接收消息 |
|||
var receivedHtml = ''; |
|||
if (data.receivedMessages && data.receivedMessages.length > 0) { |
|||
data.receivedMessages.forEach(function(message, index) { |
|||
receivedHtml += createMessageCard(message, index, 'received'); |
|||
}); |
|||
} else { |
|||
receivedHtml = '<div class="text-muted text-center"><i class="fas fa-info-circle"></i> 暂无接收消息</div>'; |
|||
} |
|||
$('#receivedMessages').html(receivedHtml); |
|||
|
|||
// 高亮JSON语法 |
|||
$('pre code').each(function() { |
|||
if (typeof hljs !== 'undefined') { |
|||
hljs.highlightElement(this); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
function createMessageCard(message, index, type) { |
|||
var timestamp = new Date().toLocaleTimeString(); |
|||
var messageType = type === 'sent' ? '发送' : '接收'; |
|||
var bgClass = type === 'sent' ? 'border-success' : 'border-info'; |
|||
var iconClass = type === 'sent' ? 'fas fa-paper-plane text-success' : 'fas fa-download text-info'; |
|||
|
|||
return ` |
|||
<div class="card mb-2 ${bgClass}"> |
|||
<div class="card-header py-2"> |
|||
<small class="text-muted"> |
|||
<i class="${iconClass}"></i> ${messageType} #${index + 1} - ${timestamp} |
|||
</small> |
|||
<button class="btn btn-sm btn-outline-secondary float-right" onclick="toggleMessage('${type}-${index}')"> |
|||
<i class="fas fa-chevron-down"></i> |
|||
</button> |
|||
</div> |
|||
<div class="card-body py-2" id="${type}-${index}" style="display: none;"> |
|||
<pre><code class="json">${formatJson(message)}</code></pre> |
|||
</div> |
|||
</div> |
|||
`; |
|||
} |
|||
|
|||
function toggleMessage(id) { |
|||
$('#' + id).slideToggle(); |
|||
} |
|||
|
|||
function formatJson(jsonString) { |
|||
try { |
|||
var obj = JSON.parse(jsonString); |
|||
return JSON.stringify(obj, null, 2); |
|||
} catch (e) { |
|||
return jsonString; |
|||
} |
|||
} |
|||
|
|||
function refreshMessages() { |
|||
loadMessages(); |
|||
} |
|||
|
|||
// 页面卸载时清除定时器 |
|||
$(window).on('beforeunload', function() { |
|||
if (refreshInterval) { |
|||
clearInterval(refreshInterval); |
|||
} |
|||
}); |
|||
</script> |
|||
|
|||
<!-- 添加代码高亮支持 --> |
|||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css"> |
|||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script> |
|||
<script>hljs.highlightAll();</script> |
|||
} |
@ -0,0 +1,292 @@ |
|||
@{ |
|||
ViewData["Title"] = "测试客户端配置"; |
|||
var testConfig = ViewBag.TestConfig as LTEMvcApp.Models.ClientConfig; |
|||
// 只保留不含 EVENT 的日志层 |
|||
var allLayers = LTEMvcApp.Models.LogLayerTypes.AllLayers.Where(l => l != "EVENT").ToArray(); |
|||
var layerConfigs = new Dictionary<string, Dictionary<string, object>>(); |
|||
if (testConfig?.Logs?.ContainsKey("layers") == true && testConfig.Logs["layers"] is Dictionary<string, object> layers) |
|||
{ |
|||
foreach (var layer in allLayers) |
|||
{ |
|||
var config = new Dictionary<string, object>(); |
|||
if (layers.ContainsKey(layer) && layers[layer] is Dictionary<string, object> layerConfig) |
|||
{ |
|||
config["level"] = layerConfig.ContainsKey("level") ? layerConfig["level"]?.ToString() : LTEMvcApp.Models.LogLayerTypes.GetDefaultLevel(layer); |
|||
config["maxSize"] = layerConfig.ContainsKey("max_size") && layerConfig["max_size"] != null ? Convert.ToInt32(layerConfig["max_size"]) : 1; |
|||
config["payload"] = layerConfig.ContainsKey("payload") && layerConfig["payload"] != null ? Convert.ToBoolean(layerConfig["payload"]) : false; |
|||
} |
|||
else |
|||
{ |
|||
config["level"] = LTEMvcApp.Models.LogLayerTypes.GetDefaultLevel(layer); |
|||
config["maxSize"] = 1; |
|||
config["payload"] = false; |
|||
} |
|||
layerConfigs[layer] = config; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
foreach (var layer in allLayers) |
|||
{ |
|||
layerConfigs[layer] = new Dictionary<string, object> |
|||
{ |
|||
["level"] = LTEMvcApp.Models.LogLayerTypes.GetDefaultLevel(layer), |
|||
["maxSize"] = 1, |
|||
["payload"] = false |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
|
|||
<div class="container-fluid"> |
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h3 class="card-title">测试客户端配置</h3> |
|||
</div> |
|||
<div class="card-body"> |
|||
<form id="testClientConfigForm"> |
|||
<!-- 基本配置 --> |
|||
<div class="row"> |
|||
<div class="col-md-6"> |
|||
<div class="form-group"> |
|||
<label for="name">客户端名称</label> |
|||
<input type="text" class="form-control" id="name" name="name" value="@testConfig?.Name" required> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6"> |
|||
<div class="form-group"> |
|||
<label for="address">服务器地址</label> |
|||
<input type="text" class="form-control" id="address" name="address" value="@testConfig?.Address" placeholder="192.168.13.12:9001"> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-6"> |
|||
<div class="form-group"> |
|||
<label for="password">密码</label> |
|||
<input type="password" class="form-control" id="password" name="password" value="@testConfig?.Password"> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6"> |
|||
<div class="form-group"> |
|||
<label for="reconnectDelay">重连延迟 (毫秒)</label> |
|||
<input type="number" class="form-control" id="reconnectDelay" name="reconnectDelay" value="@testConfig?.ReconnectDelay"> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-12"> |
|||
<div class="form-check"> |
|||
<input type="checkbox" class="form-check-input" id="enabled" name="enabled" @(testConfig?.Enabled == true ? "checked" : "")> |
|||
<label class="form-check-label" for="enabled">启用</label> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-12"> |
|||
<div class="form-check"> |
|||
<input type="checkbox" class="form-check-input" id="ssl" name="ssl" @(testConfig?.Ssl == true ? "checked" : "")> |
|||
<label class="form-check-label" for="ssl">使用SSL</label> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-12"> |
|||
<div class="form-check"> |
|||
<input type="checkbox" class="form-check-input" id="readonly" name="readonly" @(testConfig?.Readonly == true ? "checked" : "")> |
|||
<label class="form-check-label" for="readonly">只读模式</label> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 日志层配置 --> |
|||
<div class="row mt-4"> |
|||
<div class="col-md-12"> |
|||
<h5>日志层配置</h5> |
|||
<div class="table-responsive"> |
|||
<table class="table table-bordered table-sm"> |
|||
<thead class="table-dark"> |
|||
<tr> |
|||
<th>日志层</th> |
|||
<th>级别</th> |
|||
<th>最大大小</th> |
|||
<th>包含负载</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
@foreach (var layer in allLayers) |
|||
{ |
|||
var config = layerConfigs[layer]; |
|||
var level = config["level"]?.ToString(); |
|||
var maxSize = Convert.ToInt32(config["maxSize"]); |
|||
var payload = Convert.ToBoolean(config["payload"]); |
|||
<tr> |
|||
<td><strong>@layer</strong></td> |
|||
<td> |
|||
<select class="form-control form-control-sm" name="layers[@layer][level]"> |
|||
@foreach (var logLevel in LTEMvcApp.Models.LogLayerTypes.LogLevels) |
|||
{ |
|||
if (logLevel == level) |
|||
{ |
|||
<option value="@logLevel" selected>@logLevel.ToUpper()</option> |
|||
} |
|||
else |
|||
{ |
|||
<option value="@logLevel">@logLevel.ToUpper()</option> |
|||
} |
|||
} |
|||
</select> |
|||
</td> |
|||
<td> |
|||
<input type="number" class="form-control form-control-sm" name="layers[@layer][max_size]" value="@maxSize" min="1" max="1000"> |
|||
</td> |
|||
<td> |
|||
<div class="form-check"> |
|||
<input type="checkbox" class="form-check-input" name="layers[@layer][payload]" @(payload ? "checked" : "")> |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 操作按钮 --> |
|||
<div class="row mt-4"> |
|||
<div class="col-md-12"> |
|||
<button type="submit" class="btn btn-primary"> |
|||
<i class="fas fa-save"></i> 保存配置 |
|||
</button> |
|||
<button type="button" class="btn btn-success" onclick="startTestClient()"> |
|||
<i class="fas fa-play"></i> 启动测试客户端 |
|||
</button> |
|||
<button type="button" class="btn btn-danger" onclick="stopTestClient()"> |
|||
<i class="fas fa-stop"></i> 停止测试客户端 |
|||
</button> |
|||
<a href="@Url.Action("ClientMessages", "Home", new { clientName = testConfig?.Name })" class="btn btn-info"> |
|||
<i class="fas fa-list"></i> 查看消息队列 |
|||
</a> |
|||
<button type="button" class="btn btn-secondary" onclick="resetToDefaults()"> |
|||
<i class="fas fa-undo"></i> 重置为默认值 |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
@section Scripts { |
|||
<script> |
|||
$(document).ready(function() { |
|||
$('#testClientConfigForm').on('submit', function(e) { |
|||
e.preventDefault(); |
|||
saveTestClientConfig(); |
|||
}); |
|||
}); |
|||
|
|||
function saveTestClientConfig() { |
|||
var formData = { |
|||
name: $('#name').val(), |
|||
address: $('#address').val(), |
|||
password: $('#password').val(), |
|||
reconnectDelay: parseInt($('#reconnectDelay').val()) || 15000, |
|||
enabled: $('#enabled').is(':checked'), |
|||
ssl: $('#ssl').is(':checked'), |
|||
readonly: $('#readonly').is(':checked'), |
|||
logs: { |
|||
layers: {} |
|||
} |
|||
}; |
|||
|
|||
// 构建日志层配置 |
|||
var layers = @Html.Raw(Json.Serialize(LTEMvcApp.Models.LogLayerTypes.AllLayers)); |
|||
layers.forEach(function(layer) { |
|||
var level = $(`select[name="layers.${layer}.level"]`).val(); |
|||
var maxSize = parseInt($(`input[name="layers.${layer}.max_size"]`).val()) || 1; |
|||
var payload = $(`input[name="layers.${layer}.payload"]`).is(':checked'); |
|||
|
|||
formData.logs.layers[layer] = { |
|||
level: level, |
|||
max_size: maxSize, |
|||
payload: payload, |
|||
filter: level |
|||
}; |
|||
}); |
|||
|
|||
$.ajax({ |
|||
url: '/api/websocket/test-client-config', |
|||
type: 'POST', |
|||
contentType: 'application/json', |
|||
data: JSON.stringify(formData), |
|||
success: function(response) { |
|||
alert('配置保存成功!'); |
|||
location.reload(); |
|||
}, |
|||
error: function(xhr) { |
|||
alert('保存配置失败:' + xhr.responseText); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
function startTestClient() { |
|||
$.ajax({ |
|||
url: '/api/websocket/test-client/start', |
|||
type: 'POST', |
|||
success: function(response) { |
|||
alert('测试客户端启动成功!'); |
|||
}, |
|||
error: function(xhr) { |
|||
alert('启动测试客户端失败:' + xhr.responseText); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
function stopTestClient() { |
|||
$.ajax({ |
|||
url: '/api/websocket/test-client/stop', |
|||
type: 'POST', |
|||
success: function(response) { |
|||
alert('测试客户端停止成功!'); |
|||
}, |
|||
error: function(xhr) { |
|||
alert('停止测试客户端失败:' + xhr.responseText); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
function resetToDefaults() { |
|||
if (confirm('确定要重置为默认配置吗?')) { |
|||
// 重置为默认值 |
|||
var defaultLevels = { |
|||
'PHY': 'info', 'MAC': 'info', 'RLC': 'info', 'PDCP': 'warn', |
|||
'RRC': 'debug', 'NAS': 'debug', 'S1AP': 'debug', 'NGAP': 'debug', |
|||
'GTPU': 'info', 'X2AP': 'debug', 'XnAP': 'info', 'M2AP': 'info' |
|||
}; |
|||
|
|||
Object.keys(defaultLevels).forEach(function(layer) { |
|||
$(`select[name="layers.${layer}.level"]`).val(defaultLevels[layer]); |
|||
$(`input[name="layers.${layer}.max_size"]`).val(1); |
|||
$(`input[name="layers.${layer}.payload"]`).prop('checked', false); |
|||
}); |
|||
|
|||
// 设置一些层的 payload 为 true |
|||
['PHY', 'MAC', 'RRC', 'NAS'].forEach(function(layer) { |
|||
$(`input[name="layers.${layer}.payload"]`).prop('checked', true); |
|||
}); |
|||
|
|||
alert('已重置为默认配置!'); |
|||
} |
|||
} |
|||
</script> |
|||
} |
@ -0,0 +1,189 @@ |
|||
@{ |
|||
ViewData["Title"] = "WebSocket测试"; |
|||
} |
|||
|
|||
<div class="container-fluid"> |
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h3 class="card-title">WebSocket测试</h3> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div class="row"> |
|||
<div class="col-md-6"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h5 class="card-title">连接状态</h5> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div class="form-group"> |
|||
<label for="serverUrl">服务器地址</label> |
|||
<input type="text" class="form-control" id="serverUrl" value="ws://192.168.13.12:9001" placeholder="ws://localhost:9001"> |
|||
</div> |
|||
<div class="form-group mt-3"> |
|||
<button type="button" class="btn btn-success" id="connectBtn" onclick="connect()">连接</button> |
|||
<button type="button" class="btn btn-danger" id="disconnectBtn" onclick="disconnect()" style="display:none;">断开</button> |
|||
</div> |
|||
<div class="mt-3"> |
|||
<span class="badge" id="statusBadge">未连接</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h5 class="card-title">消息发送</h5> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div class="form-group"> |
|||
<label for="messageInput">消息内容</label> |
|||
<textarea class="form-control" id="messageInput" rows="3" placeholder="输入要发送的消息"></textarea> |
|||
</div> |
|||
<div class="form-group mt-3"> |
|||
<button type="button" class="btn btn-primary" onclick="sendMessage()" id="sendBtn" disabled>发送</button> |
|||
<button type="button" class="btn btn-secondary" onclick="clearMessages()">清空消息</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="row mt-4"> |
|||
<div class="col-12"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h5 class="card-title">消息日志</h5> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div id="messageLog" style="height: 400px; overflow-y: auto; background-color: #f8f9fa; padding: 10px; border-radius: 5px;"> |
|||
<div class="text-muted">等待连接...</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
@section Scripts { |
|||
<script> |
|||
var websocket = null; |
|||
var messageLog = document.getElementById('messageLog'); |
|||
var statusBadge = document.getElementById('statusBadge'); |
|||
var connectBtn = document.getElementById('connectBtn'); |
|||
var disconnectBtn = document.getElementById('disconnectBtn'); |
|||
var sendBtn = document.getElementById('sendBtn'); |
|||
var messageInput = document.getElementById('messageInput'); |
|||
|
|||
function connect() { |
|||
var url = document.getElementById('serverUrl').value; |
|||
if (!url) { |
|||
alert('请输入服务器地址'); |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
websocket = new WebSocket(url); |
|||
|
|||
websocket.onopen = function(event) { |
|||
updateStatus('已连接', 'success'); |
|||
addMessage('系统', '连接已建立', 'success'); |
|||
connectBtn.style.display = 'none'; |
|||
disconnectBtn.style.display = 'inline-block'; |
|||
sendBtn.disabled = false; |
|||
}; |
|||
|
|||
websocket.onmessage = function(event) { |
|||
addMessage('接收', event.data, 'info'); |
|||
}; |
|||
|
|||
websocket.onclose = function(event) { |
|||
updateStatus('已断开', 'secondary'); |
|||
addMessage('系统', '连接已断开', 'warning'); |
|||
connectBtn.style.display = 'inline-block'; |
|||
disconnectBtn.style.display = 'none'; |
|||
sendBtn.disabled = true; |
|||
}; |
|||
|
|||
websocket.onerror = function(error) { |
|||
updateStatus('连接错误', 'danger'); |
|||
addMessage('系统', '连接错误: ' + error, 'danger'); |
|||
}; |
|||
|
|||
} catch (error) { |
|||
alert('连接失败: ' + error.message); |
|||
} |
|||
} |
|||
|
|||
function disconnect() { |
|||
if (websocket) { |
|||
websocket.close(); |
|||
websocket = null; |
|||
} |
|||
} |
|||
|
|||
function sendMessage() { |
|||
var message = messageInput.value.trim(); |
|||
if (!message) { |
|||
alert('请输入要发送的消息'); |
|||
return; |
|||
} |
|||
|
|||
if (websocket && websocket.readyState === WebSocket.OPEN) { |
|||
websocket.send(message); |
|||
addMessage('发送', message, 'primary'); |
|||
messageInput.value = ''; |
|||
} else { |
|||
alert('WebSocket未连接'); |
|||
} |
|||
} |
|||
|
|||
function addMessage(type, content, color) { |
|||
var timestamp = new Date().toLocaleTimeString(); |
|||
var messageDiv = document.createElement('div'); |
|||
messageDiv.className = 'mb-2'; |
|||
messageDiv.innerHTML = ` |
|||
<small class="text-muted">[${timestamp}]</small> |
|||
<span class="badge badge-${color}">${type}</span> |
|||
<span class="ml-2">${escapeHtml(content)}</span> |
|||
`; |
|||
messageLog.appendChild(messageDiv); |
|||
messageLog.scrollTop = messageLog.scrollHeight; |
|||
} |
|||
|
|||
function updateStatus(status, color) { |
|||
statusBadge.textContent = status; |
|||
statusBadge.className = 'badge badge-' + color; |
|||
} |
|||
|
|||
function clearMessages() { |
|||
messageLog.innerHTML = '<div class="text-muted">消息已清空</div>'; |
|||
} |
|||
|
|||
function escapeHtml(text) { |
|||
var div = document.createElement('div'); |
|||
div.textContent = text; |
|||
return div.innerHTML; |
|||
} |
|||
|
|||
// 回车键发送消息 |
|||
messageInput.addEventListener('keypress', function(e) { |
|||
if (e.key === 'Enter' && !e.shiftKey) { |
|||
e.preventDefault(); |
|||
sendMessage(); |
|||
} |
|||
}); |
|||
|
|||
// 页面卸载时断开连接 |
|||
window.addEventListener('beforeunload', function() { |
|||
if (websocket) { |
|||
websocket.close(); |
|||
} |
|||
}); |
|||
</script> |
|||
} |
Loading…
Reference in new issue