feat(instanceDetails): replace API logs with WebSocket for real-time updates

Implement WebSocket connection for instance logs to enable real-time updates instead of periodic API polling. Remove deprecated API endpoints and update log display styling with level-specific backgrounds. Add cleanup on component unmount to prevent memory leaks.
This commit is contained in:
2026-03-26 17:29:30 +08:00
parent 34f0d9f6d5
commit 198129b4c2
2 changed files with 71 additions and 10 deletions

View File

@@ -86,8 +86,6 @@ export const instanceApi = {
api.get(`/frpcAct/instanceMgr/getInfo?instanceID=${instanceID}`), api.get(`/frpcAct/instanceMgr/getInfo?instanceID=${instanceID}`),
getInstanceStatus: (instanceID) => getInstanceStatus: (instanceID) =>
api.get(`/frpcAct/instanceMgr/status?instanceID=${instanceID}`), api.get(`/frpcAct/instanceMgr/status?instanceID=${instanceID}`),
getInstanceLogs: (instanceID) =>
api.post('/frpcAct/instanceMgr/logs', { instanceID }),
getInstanceProxies: (instanceID) => getInstanceProxies: (instanceID) =>
api.get(`/frpcAct/proxyMgr/list?instanceID=${instanceID}`), api.get(`/frpcAct/proxyMgr/list?instanceID=${instanceID}`),
createProxy: (instanceID, proxyInfo) => createProxy: (instanceID, proxyInfo) =>
@@ -109,9 +107,6 @@ export const systemInfoApi = {
export default api; export default api;
export const TODO_API_NOTES = { export const TODO_API_NOTES = {
instanceLogs: 'TODO: /frpcAct/instanceMgr/logs - 获取实例日志 API 未在 api-backend.md 中定义',
instanceProxies: 'TODO: /frpcAct/instanceMgr/proxies - 获取实例代理列表 API 未在 api-backend.md 中定义',
logList: 'TODO: /logMgr/list - 获取系统日志 API 未在 api-backend.md 中定义',
monitorStats: 'TODO: /monitor/stats - 获取系统监控数据 API 未在 api-backend.md 中定义', monitorStats: 'TODO: /monitor/stats - 获取系统监控数据 API 未在 api-backend.md 中定义',
systemInfo: 'TODO: /system/info - 获取系统信息 API 未在 api-backend.md 中定义' systemInfo: 'TODO: /system/info - 获取系统信息 API 未在 api-backend.md 中定义'
}; };

View File

@@ -272,7 +272,7 @@
</template> </template>
<script> <script>
import { ref, onMounted } from 'vue'; import { ref, onMounted, onUnmounted } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { instanceApi } from '../api/index.js'; import { instanceApi } from '../api/index.js';
import { showNotification, formatDate, getCookie } from '../utils/functions.js'; import { showNotification, formatDate, getCookie } from '../utils/functions.js';
@@ -395,17 +395,64 @@ export default {
} }
}; };
let logSocket = null;
const connectLogWebSocket = () => {
const token = getCookie('token');
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const target = __APP_TARGET__;
const wsUrl = `${wsProtocol}//${target}/frpcAct/instanceMgr/logs?instanceID=${instanceID.value}&token=${token}`;
logSocket = new WebSocket(wsUrl);
logSocket.onopen = () => {
console.log(`Connected to instance ${instanceID.value} log server`);
};
logSocket.onmessage = (event) => {
try {
const log = JSON.parse(event.data);
logs.value.unshift({
time: log.timestamp,
level: log.level,
message: log.content
});
if (logs.value.length > 100) {
logs.value.pop();
}
} catch (error) {
console.error('Error parsing log message:', error);
}
};
logSocket.onclose = () => {
console.log(`Disconnected from instance ${instanceID.value} log server`);
};
logSocket.onerror = (error) => {
console.error('WebSocket error:', error);
};
};
const disconnectLogWebSocket = () => {
if (logSocket) {
logSocket.close();
logSocket = null;
}
};
const loadLogs = async () => { const loadLogs = async () => {
try { try {
const result = await instanceApi.getInstanceLogs(instanceID.value); const result = await instanceApi.getInstanceInfo(instanceID.value);
logs.value = result.data.map(log => ({ logs.value = result.data.logs ? result.data.logs.map(log => ({
time: formatDate(log.timestamp), time: formatDate(log.timestamp),
level: log.level, level: log.level,
message: log.message message: log.content
})); })) : [];
} catch (error) { } catch (error) {
showNotification('Load logs failed', 'error'); showNotification('Load logs failed', 'error');
} }
connectLogWebSocket();
}; };
const goBack = () => { const goBack = () => {
@@ -524,6 +571,10 @@ export default {
await loadLogs(); await loadLogs();
}); });
onUnmounted(() => {
disconnectLogWebSocket();
});
return { return {
instanceID, instanceID,
instanceName, instanceName,
@@ -647,18 +698,33 @@ export default {
.log-level { .log-level {
min-width: 60px; min-width: 60px;
font-weight: 500; font-weight: 500;
padding: 2px 8px;
border-radius: 4px;
}
.log-level.DEBUG {
color: #52c41a;
background: #f6ffed;
} }
.log-level.INFO { .log-level.INFO {
color: #1890ff; color: #1890ff;
background: #e6f7ff;
} }
.log-level.WARN { .log-level.WARN {
color: #faad14; color: #faad14;
background: #fffbe6;
} }
.log-level.ERROR { .log-level.ERROR {
color: #ff4d4f; color: #ff4d4f;
background: #fff1f0;
}
.log-level.FATAL {
color: #cf1322;
background: #fff1f0;
} }
.empty-state { .empty-state {