<template>
|
<div class="message-display-container">
|
<!-- 控制面板 -->
|
<el-card class="control-panel" shadow="hover">
|
<div class="control-content">
|
<el-form :inline="true" :model="filterForm" class="filter-form">
|
<el-form-item :label="t('message.messageType')" prop="messageType" required>
|
<el-select
|
v-model="filterForm.messageType"
|
:placeholder="t('message.selectMessageType')"
|
clearable
|
:disabled="isReceiving"
|
style="width: 200px"
|
>
|
<el-option v-for="type in messageTypes" :key="type.value" :label="t(type.label)" :value="type.value" />
|
</el-select>
|
</el-form-item>
|
|
<el-form-item :label="t('message.deviceId')" prop="messageIdOption" required>
|
<el-select
|
v-model="filterForm.messageIdOption"
|
:placeholder="t('message.selectOption')"
|
style="width: 120px"
|
:disabled="isReceiving"
|
@change="handleIdOptionChange"
|
>
|
<el-option :label="t('message.all')" value="all" />
|
<el-option :label="t('message.specific')" value="specific" />
|
</el-select>
|
</el-form-item>
|
|
<el-form-item v-if="filterForm.messageIdOption === 'specific'" prop="specificId" required>
|
<el-input
|
v-model="filterForm.specificId"
|
:placeholder="t('message.inputDeviceId')"
|
:disabled="isReceiving"
|
style="width: 180px"
|
/>
|
</el-form-item>
|
</el-form>
|
|
<div class="control-buttons">
|
<el-button type="primary" @click="startReceiving" :disabled="isReceiving || !canStartReceiving" :icon="VideoPlay" round>
|
{{ t("message.startReceive") }}
|
</el-button>
|
<el-button type="danger" @click="stopReceiving" :disabled="!isReceiving" :icon="VideoPause" round>
|
{{ t("message.stopReceive") }}
|
</el-button>
|
<el-button @click="clearMessages" :icon="Delete" round>
|
{{ t("message.clear") }}
|
</el-button>
|
</div>
|
</div>
|
</el-card>
|
|
<!-- 报文展示区 -->
|
<el-card class="message-card" shadow="never">
|
<template #header>
|
<div class="message-header">
|
<span class="header-title">{{ t("message.messageContent") }}</span>
|
<div class="header-actions">
|
<el-input
|
v-model="searchText"
|
:placeholder="t('message.searchContent')"
|
clearable
|
style="width: 240px; margin-right: 16px"
|
@clear="clearSearch"
|
>
|
<template #prefix>
|
<el-icon><Search /></el-icon>
|
</template>
|
</el-input>
|
<el-tag type="info" effect="dark">
|
{{ t("message.displayCount", { current: filteredMessages.length, total: totalMessages }) }}
|
</el-tag>
|
</div>
|
</div>
|
</template>
|
|
<div class="message-container" ref="messageContainer">
|
<div
|
v-for="(msg, index) in filteredMessages"
|
:key="msg.id"
|
class="message-item"
|
:class="{ 'odd-row': index % 2 === 0, highlight: isHighlighted(msg) }"
|
@dblclick="copyMessage(msg.content)"
|
>
|
<span class="message-index">#{{ msg.id }}</span>
|
<el-tag class="message-time" effect="plain" size="small">
|
{{ msg.timestamp }}
|
</el-tag>
|
<span class="message-content" v-html="highlightSearchText(msg.content)"></span>
|
<el-icon class="copy-icon" @click="copyMessage(msg.content)">
|
<DocumentCopy />
|
</el-icon>
|
</div>
|
|
<div v-if="isReceiving && !searchText" class="loading-indicator">
|
<el-icon class="loading-icon" :size="20"><Loading /></el-icon>
|
<span>{{ t("message.receiving") }}</span>
|
</div>
|
|
<div v-if="filteredMessages.length === 0" class="no-data">
|
<el-empty :description="t('message.noData')" />
|
</div>
|
</div>
|
</el-card>
|
</div>
|
</template>
|
|
<script lang="ts">
|
import { defineComponent, ref, computed, onUnmounted, nextTick } from "vue";
|
import {} from "@vueuse/core";
|
import { ElMessage, ElNotification } from "element-plus";
|
import { VideoPlay, VideoPause, Delete, DocumentCopy, Loading, Search } from "@element-plus/icons-vue";
|
import { generateSimpleUUID } from "./uuid";
|
import { useI18n } from "vue-i18n";
|
|
interface Message {
|
id: number;
|
timestamp: string;
|
content: string;
|
type: string;
|
deviceId?: string; // Add deviceId to match SSE data
|
}
|
|
export default defineComponent({
|
name: "MessageDisplay",
|
components: {
|
DocumentCopy,
|
Loading,
|
Search
|
},
|
setup() {
|
const { t } = useI18n({ useScope: "global" });
|
|
let eventSource: EventSource | null = null;
|
const isReceiving = ref(false);
|
const messages = ref<Message[]>([]);
|
const lastMessageId = ref(0);
|
const searchText = ref("");
|
const messageContainer = ref<HTMLElement>();
|
|
// 过滤表单
|
const filterForm = ref({
|
messageType: "",
|
messageIdOption: "all",
|
specificId: ""
|
});
|
const uuid = ref<string>("");
|
const generateNewUUID = () => {
|
uuid.value = generateSimpleUUID();
|
};
|
|
// 报文类型选项
|
const messageTypes = ref([
|
{ value: "1", label: "message.rawData" },
|
{ value: "2", label: "message.parsedData" }
|
]);
|
|
// 计算是否可以开始接收
|
const canStartReceiving = computed(() => {
|
return (
|
filterForm.value.messageType &&
|
(filterForm.value.messageIdOption === "all" ||
|
(filterForm.value.messageIdOption === "specific" && filterForm.value.specificId))
|
);
|
});
|
|
// 处理ID选项变化
|
const handleIdOptionChange = (val: string) => {
|
if (val === "all") {
|
filterForm.value.specificId = "";
|
}
|
};
|
|
// 连接SSE
|
const connectSSE = () => {
|
generateNewUUID();
|
if (eventSource) {
|
eventSource.close();
|
}
|
|
const params = new URLSearchParams();
|
params.append("type", filterForm.value.messageType);
|
params.append("", filterForm.value.messageType);
|
if (filterForm.value.messageIdOption === "specific" && filterForm.value.specificId) {
|
params.append("deviceId", filterForm.value.specificId);
|
} else {
|
params.append("deviceId", "ALL");
|
}
|
eventSource = new EventSource(`http://192.168.5.101:6002/sse/connect?${params.toString()}`);
|
|
eventSource.onmessage = event => {
|
if (!isReceiving.value) return;
|
|
try {
|
const data = JSON.parse(event.data);
|
if (shouldDisplayMessage(data)) {
|
addMessage(data);
|
}
|
} catch (error) {
|
console.error("Error parsing SSE message:", error);
|
}
|
};
|
|
eventSource.addEventListener("YSSJ", event => {
|
if (!isReceiving.value) return;
|
|
try {
|
const data = JSON.parse(event.data);
|
if (shouldDisplayMessage(data)) {
|
addMessage(data);
|
}
|
} catch (error) {
|
console.error("Error parsing YSSJ message:", error);
|
}
|
});
|
|
eventSource.onerror = error => {
|
console.error("SSE Error:", error);
|
// 自动重连逻辑
|
if (isReceiving.value) {
|
setTimeout(connectSSE, 5000);
|
}
|
};
|
};
|
|
// 检查消息是否符合当前过滤条件
|
const shouldDisplayMessage = (data: any): boolean => {
|
// 检查报文类型
|
if (filterForm.value.messageType && data.type !== filterForm.value.messageType) {
|
return false;
|
}
|
|
// 检查设备ID
|
if (
|
filterForm.value.messageIdOption === "specific" &&
|
filterForm.value.specificId &&
|
data.deviceId !== filterForm.value.specificId
|
) {
|
return false;
|
}
|
|
return true;
|
};
|
|
// 添加消息到列表
|
const addMessage = (data: any) => {
|
lastMessageId.value++;
|
const newMessage: Message = {
|
id: lastMessageId.value,
|
type: data.type || "unknown",
|
deviceId: data.deviceId,
|
timestamp: new Date().toLocaleTimeString("zh-CN", {
|
hour12: false,
|
hour: "2-digit",
|
minute: "2-digit",
|
second: "2-digit",
|
fractionalSecondDigits: 3
|
}),
|
content: typeof data === "object" ? JSON.stringify(data, null, 2) : String(data)
|
};
|
|
messages.value = [...messages.value, newMessage];
|
scrollToBottom();
|
};
|
|
// 复制消息内容
|
const copyMessage = (content: string) => {
|
navigator.clipboard.writeText(content);
|
ElMessage.success(t("message.copySuccess"));
|
};
|
|
// 滚动到底部
|
const scrollToBottom = () => {
|
if (messageContainer.value) {
|
nextTick(() => {
|
messageContainer.value.scrollTop = messageContainer.value.scrollHeight;
|
});
|
}
|
};
|
|
// 开始接收
|
const startReceiving = () => {
|
if (isReceiving.value || !canStartReceiving.value) return;
|
|
isReceiving.value = true;
|
connectSSE();
|
|
ElNotification({
|
title: t("message.startReceive"),
|
message: t("message.startReceiveMessage", {
|
type: filterForm.value.messageType,
|
device: filterForm.value.messageIdOption === "all" ? t("message.all") : filterForm.value.specificId
|
}),
|
type: "success"
|
});
|
};
|
|
// 停止接收
|
const stopReceiving = () => {
|
isReceiving.value = false;
|
if (eventSource) {
|
eventSource.close();
|
eventSource = null;
|
}
|
ElNotification({
|
title: t("message.stopped"),
|
message: t("message.stopReceiveMessage"),
|
type: "warning"
|
});
|
};
|
|
// 清空消息
|
const clearMessages = () => {
|
messages.value = [];
|
lastMessageId.value = 0;
|
searchText.value = "";
|
ElNotification({
|
title: t("message.cleared"),
|
message: t("message.clearMessage"),
|
type: "info"
|
});
|
};
|
|
// 高亮搜索文本
|
const highlightSearchText = (text: string) => {
|
if (!searchText.value) return text;
|
const regex = new RegExp(`(${escapeRegExp(searchText.value)})`, "gi");
|
return text.replace(regex, '<span class="highlight-text">$1</span>');
|
};
|
|
// 判断是否包含搜索文本
|
const isHighlighted = (msg: Message) => {
|
if (!searchText.value) return false;
|
const search = searchText.value.toLowerCase();
|
return (
|
msg.content.toLowerCase().includes(search) ||
|
msg.id.toString().includes(search) ||
|
msg.timestamp.toLowerCase().includes(search)
|
);
|
};
|
|
// 清除搜索
|
const clearSearch = () => {
|
searchText.value = "";
|
};
|
|
// 转义正则表达式特殊字符
|
const escapeRegExp = (string: string) => {
|
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
};
|
|
// 计算属性
|
const totalMessages = computed(() => messages.value.length);
|
const visibleMessages = computed(() => messages.value);
|
|
// 过滤后的消息
|
const filteredMessages = computed(() => {
|
if (!searchText.value) {
|
return visibleMessages.value;
|
}
|
const search = searchText.value.toLowerCase();
|
return visibleMessages.value.filter(
|
msg =>
|
msg.content.toLowerCase().includes(search) ||
|
msg.id.toString().includes(search) ||
|
msg.timestamp.toLowerCase().includes(search)
|
);
|
});
|
|
// 组件卸载时停止接收
|
onUnmounted(() => {
|
if (eventSource) {
|
eventSource.close();
|
}
|
});
|
|
return {
|
t,
|
VideoPlay,
|
VideoPause,
|
Delete,
|
DocumentCopy,
|
Loading,
|
Search,
|
isReceiving,
|
messages,
|
visibleMessages,
|
filteredMessages,
|
totalMessages,
|
searchText,
|
messageContainer,
|
filterForm,
|
messageTypes,
|
canStartReceiving,
|
handleIdOptionChange,
|
startReceiving,
|
stopReceiving,
|
clearMessages,
|
copyMessage,
|
highlightSearchText,
|
isHighlighted,
|
clearSearch
|
};
|
}
|
});
|
</script>
|
|
<style scoped lang="scss">
|
.message-display-container {
|
box-sizing: border-box;
|
display: flex;
|
flex-direction: column;
|
gap: 16px;
|
height: 86vh;
|
padding: 20px;
|
background-color: #f5f7fa;
|
}
|
.control-panel {
|
border: 1px solid #ebeef5;
|
border-radius: 12px;
|
:deep(.el-card__body) {
|
padding: 16px 20px;
|
}
|
.control-content {
|
display: flex;
|
flex-direction: column;
|
gap: 16px;
|
.filter-section {
|
width: 100%;
|
.filter-form {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 16px;
|
align-items: center;
|
}
|
}
|
.control-buttons {
|
display: flex;
|
gap: 12px;
|
}
|
}
|
}
|
.message-card {
|
display: flex;
|
flex: 1;
|
flex-direction: column;
|
overflow: hidden;
|
border-radius: 12px;
|
:deep(.el-card__header) {
|
flex-shrink: 0;
|
padding: 12px 20px;
|
background-color: #f9fafc;
|
border-bottom: 1px solid #ebeef5;
|
}
|
:deep(.el-card__body) {
|
display: flex;
|
flex: 1;
|
padding: 0;
|
overflow: hidden;
|
}
|
.message-header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
width: 100%;
|
.header-title {
|
font-size: 16px;
|
font-weight: 500;
|
color: #303133;
|
}
|
.header-actions {
|
display: flex;
|
align-items: center;
|
}
|
}
|
}
|
.message-container {
|
flex: 1;
|
padding: 0;
|
overflow-y: auto;
|
.message-item {
|
display: flex;
|
align-items: center;
|
padding: 12px 16px;
|
font-family: "JetBrains Mono", monospace, "Courier New", Courier;
|
font-size: 14px;
|
cursor: pointer;
|
border-bottom: 1px solid #ebeef5;
|
transition: all 0.2s;
|
&:hover {
|
background-color: #f0f7ff;
|
}
|
&.highlight {
|
background-color: #52f52d84;
|
}
|
.message-index {
|
display: inline-block;
|
width: 80px;
|
font-weight: bold;
|
color: #909399;
|
}
|
.message-time {
|
flex-shrink: 0;
|
width: 120px;
|
margin-right: 20px;
|
}
|
.message-content {
|
flex: 1;
|
overflow: hidden;
|
color: #303133;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
:deep(.highlight-text) {
|
padding: 0 2px;
|
background-color: #fffb8f;
|
border-radius: 2px;
|
}
|
}
|
.copy-icon {
|
margin-left: 12px;
|
color: #909399;
|
cursor: pointer;
|
opacity: 0;
|
transition: opacity 0.2s;
|
&:hover {
|
color: #409eff;
|
}
|
}
|
&:hover .copy-icon {
|
opacity: 1;
|
}
|
}
|
.odd-row {
|
background-color: #fafafa;
|
}
|
.loading-indicator {
|
position: sticky;
|
bottom: 0;
|
display: flex;
|
gap: 8px;
|
align-items: center;
|
padding: 12px 16px;
|
font-size: 14px;
|
color: #909399;
|
background-color: #f5f7fa;
|
.loading-icon {
|
animation: rotate 2s linear infinite;
|
}
|
}
|
.no-data {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
height: 200px;
|
}
|
}
|
|
@keyframes rotate {
|
from {
|
transform: rotate(0deg);
|
}
|
to {
|
transform: rotate(360deg);
|
}
|
}
|
|
@media (width <= 992px) {
|
.control-content {
|
flex-direction: column;
|
align-items: flex-start !important;
|
}
|
.message-header {
|
flex-direction: column;
|
gap: 12px;
|
align-items: flex-start !important;
|
.header-actions {
|
justify-content: space-between;
|
width: 100%;
|
}
|
}
|
.filter-form {
|
flex-direction: column;
|
align-items: flex-start !important;
|
}
|
}
|
</style>
|