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.
213 lines
9.4 KiB
213 lines
9.4 KiB
@{
|
|
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">
|
|
<span id="connection-status" class="badge badge-secondary">正在连接...</span>
|
|
<a href="@Url.Action("TestClientConfig", "Home")" class="btn btn-info btn-sm ml-2">
|
|
<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 id="sentScrollArea" class="clusterize-scroll" style="height: 600px; overflow-y: auto;">
|
|
<div id="sentContentArea" class="clusterize-content">
|
|
<div class="clusterize-no-data">正在连接...</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 id="receivedScrollArea" class="clusterize-scroll" style="height: 600px; overflow-y: auto;">
|
|
<div id="receivedContentArea" class="clusterize-content">
|
|
<div class="clusterize-no-data">正在连接...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@section Scripts {
|
|
<!-- Clusterize.js for virtual scrolling -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.19.0/clusterize.min.css">
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.19.0/clusterize.min.js"></script>
|
|
|
|
<!-- highlight.js for syntax highlighting -->
|
|
<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>
|
|
const clientName = '@clientName';
|
|
const MAX_MESSAGES_IN_MEMORY = 10000; // JS内存中最多保留1万条数据
|
|
|
|
let sentMessagesData = [];
|
|
let receivedMessagesData = [];
|
|
let sentClusterize, receivedClusterize;
|
|
|
|
$(document).ready(function() {
|
|
// 初始化虚拟滚动列表
|
|
sentClusterize = new Clusterize({
|
|
rows: [],
|
|
scrollId: 'sentScrollArea',
|
|
contentId: 'sentContentArea',
|
|
tag: 'div',
|
|
rows_in_block: 20,
|
|
blocks_in_cluster: 4,
|
|
no_data_text: '暂无发送消息'
|
|
});
|
|
|
|
receivedClusterize = new Clusterize({
|
|
rows: [],
|
|
scrollId: 'receivedScrollArea',
|
|
contentId: 'receivedContentArea',
|
|
tag: 'div',
|
|
rows_in_block: 20,
|
|
blocks_in_cluster: 4,
|
|
no_data_text: '暂无接收消息'
|
|
});
|
|
|
|
// 启动SSE连接
|
|
initializeEventSource();
|
|
});
|
|
|
|
function initializeEventSource() {
|
|
const source = new EventSource(`/api/websocket/clients/${encodeURIComponent(clientName)}/messages/stream`);
|
|
const statusBadge = $('#connection-status');
|
|
|
|
source.addEventListener('open', function(e) {
|
|
console.log("SSE connection opened.");
|
|
statusBadge.removeClass('badge-secondary badge-danger').addClass('badge-success').text('已连接');
|
|
|
|
// 重置数据和视图
|
|
sentMessagesData = [];
|
|
receivedMessagesData = [];
|
|
sentClusterize.update([]);
|
|
receivedClusterize.update([]);
|
|
});
|
|
|
|
source.addEventListener('update', function(e) {
|
|
const data = JSON.parse(e.data);
|
|
const isSent = data.type === 'sent';
|
|
|
|
const clusterize = isSent ? sentClusterize : receivedClusterize;
|
|
const dataArray = isSent ? sentMessagesData : receivedMessagesData;
|
|
const scrollArea = isSent ? $('#sentScrollArea') : $('#receivedScrollArea');
|
|
|
|
// 检查用户是否已滚动到底部
|
|
const isScrolledToBottom = scrollArea[0].scrollHeight - scrollArea.scrollTop() < scrollArea.outerHeight() + 50;
|
|
|
|
const newRows = data.messages.map(function(msg, i) {
|
|
let totalIndex = data.totalCount - data.messages.length + i;
|
|
return createMessageCardHtml(msg, totalIndex, data.type);
|
|
});
|
|
|
|
// 使用 append 方法,性能更高
|
|
clusterize.append(newRows);
|
|
dataArray.push(...newRows); // 同步数据到内存数组
|
|
|
|
// 限制内存中的数据量
|
|
if (dataArray.length > MAX_MESSAGES_IN_MEMORY) {
|
|
const toRemoveCount = dataArray.length - MAX_MESSAGES_IN_MEMORY;
|
|
dataArray.splice(0, toRemoveCount);
|
|
// Clusterize.js 内部会自动处理DOM,我们只需更新数据数组即可
|
|
clusterize.update(dataArray);
|
|
}
|
|
|
|
// 如果用户之前在底部,则自动滚动到底部
|
|
if (isScrolledToBottom) {
|
|
scrollArea.scrollTop(scrollArea[0].scrollHeight);
|
|
}
|
|
|
|
$(`#${data.type}Count`).text(data.totalCount);
|
|
});
|
|
|
|
source.addEventListener('error', function(e) {
|
|
statusBadge.removeClass('badge-success').addClass('badge-danger').text('连接断开');
|
|
console.error("SSE connection error/closed.", e);
|
|
});
|
|
}
|
|
|
|
function createMessageCardHtml(message, index, type) {
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
const messageType = type === 'sent' ? '发送' : '接收';
|
|
const bgClass = type === 'sent' ? 'border-success' : 'border-info';
|
|
const 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 py-0" onclick="toggleMessage(this)">
|
|
<i class="fas fa-chevron-down"></i>
|
|
</button>
|
|
</div>
|
|
<div class="card-body py-2" style="display: none;">
|
|
<pre><code class="json">${escapeHtml(formatJson(message))}</code></pre>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function toggleMessage(button) {
|
|
const cardBody = $(button).closest('.card').find('.card-body');
|
|
cardBody.slideToggle('fast', function() {
|
|
// 当内容可见时,才进行语法高亮
|
|
if (cardBody.is(':visible')) {
|
|
const codeBlock = cardBody.find('pre code');
|
|
if (codeBlock.length && typeof hljs !== 'undefined' && !codeBlock.hasClass('hljs')) {
|
|
hljs.highlightElement(codeBlock[0]);
|
|
}
|
|
}
|
|
});
|
|
$(button).find('i').toggleClass('fa-chevron-down fa-chevron-up');
|
|
}
|
|
|
|
function formatJson(jsonString) {
|
|
try {
|
|
var obj = JSON.parse(jsonString);
|
|
return JSON.stringify(obj, null, 2);
|
|
} catch (e) {
|
|
return jsonString;
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
return text
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
</script>
|
|
}
|