显示客户端链接的ip:port
This commit is contained in:
@@ -132,11 +132,13 @@ async fn handle_connection(
|
||||
match msg {
|
||||
Ok(Message::Text(text)) => {
|
||||
// Parse incoming messages
|
||||
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||
if json_value.get("type").and_then(|t| t.as_str()) == Some("debug_data") {
|
||||
if let Ok(mut json_value) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||
let msg_type = json_value.get("type").and_then(|t| t.as_str());
|
||||
|
||||
if msg_type == Some("debug_data") {
|
||||
// Broadcast debug data from game client to all clients (including frontend)
|
||||
tx.send(text).ok();
|
||||
} else if json_value.get("type").and_then(|t| t.as_str()) == Some("ping") {
|
||||
} else if msg_type == Some("ping") {
|
||||
// Respond to ping
|
||||
let _ = tx.send(
|
||||
serde_json::json!({
|
||||
@@ -145,6 +147,12 @@ async fn handle_connection(
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
} else if msg_type == Some("log") {
|
||||
// Inject clientId into log messages
|
||||
if let Some(data) = json_value.get_mut("data").and_then(|d| d.as_object_mut()) {
|
||||
data.insert("clientId".to_string(), serde_json::Value::String(peer_addr.to_string()));
|
||||
}
|
||||
tx.send(json_value.to_string()).ok();
|
||||
} else {
|
||||
// Forward all other messages (like get_raw_entity_list, get_entity_details, etc.)
|
||||
// to all connected clients (this enables frontend -> game client communication)
|
||||
|
||||
@@ -148,8 +148,8 @@ function App() {
|
||||
|
||||
// 监听远程日志事件
|
||||
window.addEventListener('profiler:remote-log', ((event: CustomEvent) => {
|
||||
const { level, message, timestamp } = event.detail;
|
||||
logService.addRemoteLog(level, message, timestamp);
|
||||
const { level, message, timestamp, clientId } = event.detail;
|
||||
logService.addRemoteLog(level, message, timestamp, clientId);
|
||||
}) as EventListener);
|
||||
|
||||
Core.services.registerInstance(UIRegistry, uiRegistry);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { LogService, LogEntry } from '@esengine/editor-core';
|
||||
import { LogLevel } from '@esengine/ecs-framework';
|
||||
import { Trash2, AlertCircle, Info, AlertTriangle, XCircle, Bug, Search, Maximize2, ChevronRight, ChevronDown } from 'lucide-react';
|
||||
import { Trash2, AlertCircle, Info, AlertTriangle, XCircle, Bug, Search, Maximize2, ChevronRight, ChevronDown, Wifi } from 'lucide-react';
|
||||
import { JsonViewer } from './JsonViewer';
|
||||
import '../styles/ConsolePanel.css';
|
||||
|
||||
@@ -19,6 +19,7 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
||||
LogLevel.Error,
|
||||
LogLevel.Fatal
|
||||
]));
|
||||
const [showRemoteOnly, setShowRemoteOnly] = useState(false);
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
const [expandedLogs, setExpandedLogs] = useState<Set<number>>(new Set());
|
||||
const [jsonViewerData, setJsonViewerData] = useState<any>(null);
|
||||
@@ -65,6 +66,7 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
||||
|
||||
const filteredLogs = logs.filter(log => {
|
||||
if (!levelFilter.has(log.level)) return false;
|
||||
if (showRemoteOnly && log.source !== 'remote') return false;
|
||||
if (filter && !log.message.toLowerCase().includes(filter.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
@@ -224,6 +226,8 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
||||
[LogLevel.Error]: logs.filter(l => l.level === LogLevel.Error || l.level === LogLevel.Fatal).length
|
||||
};
|
||||
|
||||
const remoteLogCount = logs.filter(l => l.source === 'remote').length;
|
||||
|
||||
return (
|
||||
<div className="console-panel">
|
||||
<div className="console-toolbar">
|
||||
@@ -246,6 +250,14 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="console-toolbar-right">
|
||||
<button
|
||||
className={`console-filter-btn ${showRemoteOnly ? 'active' : ''}`}
|
||||
onClick={() => setShowRemoteOnly(!showRemoteOnly)}
|
||||
title="Show Remote Logs Only"
|
||||
>
|
||||
<Wifi size={14} />
|
||||
{remoteLogCount > 0 && <span>{remoteLogCount}</span>}
|
||||
</button>
|
||||
<button
|
||||
className={`console-filter-btn ${levelFilter.has(LogLevel.Debug) ? 'active' : ''}`}
|
||||
onClick={() => toggleLevelFilter(LogLevel.Debug)}
|
||||
@@ -317,6 +329,11 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
||||
<div className={`log-entry-source ${log.source === 'remote' ? 'source-remote' : ''}`}>
|
||||
[{log.source === 'remote' ? '🌐 Remote' : log.source}]
|
||||
</div>
|
||||
{log.clientId && (
|
||||
<div className="log-entry-client" title={`Client: ${log.clientId}`}>
|
||||
{log.clientId}
|
||||
</div>
|
||||
)}
|
||||
<div className="log-entry-message">
|
||||
{formatMessage(log.message, isExpanded)}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import { Entity } from '@esengine/ecs-framework';
|
||||
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import { Box, Layers, Wifi } from 'lucide-react';
|
||||
import { Box, Layers, Wifi, Search } from 'lucide-react';
|
||||
import { ProfilerService, RemoteEntity } from '../services/ProfilerService';
|
||||
import '../styles/SceneHierarchy.css';
|
||||
|
||||
@@ -16,6 +16,7 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
||||
const [remoteEntities, setRemoteEntities] = useState<RemoteEntity[]>([]);
|
||||
const [isRemoteConnected, setIsRemoteConnected] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { t } = useLocale();
|
||||
|
||||
// Subscribe to local entity changes
|
||||
@@ -106,8 +107,33 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
||||
});
|
||||
};
|
||||
|
||||
// Filter entities based on search query
|
||||
const filterEntities = <T extends Entity | RemoteEntity>(entityList: T[]): T[] => {
|
||||
if (!searchQuery.trim()) return entityList;
|
||||
|
||||
const query = searchQuery.toLowerCase();
|
||||
return entityList.filter(entity => {
|
||||
const name = 'name' in entity ? entity.name : `Entity ${entity.id}`;
|
||||
const id = entity.id.toString();
|
||||
|
||||
// Search by name or ID
|
||||
if (name.toLowerCase().includes(query) || id.includes(query)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Search by component types (for remote entities)
|
||||
if ('componentTypes' in entity && Array.isArray(entity.componentTypes)) {
|
||||
return entity.componentTypes.some(type =>
|
||||
type.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
// Determine which entities to display
|
||||
const displayEntities = isRemoteConnected ? remoteEntities : entities;
|
||||
const displayEntities = filterEntities(isRemoteConnected ? remoteEntities : entities);
|
||||
const showRemoteIndicator = isRemoteConnected && remoteEntities.length > 0;
|
||||
|
||||
return (
|
||||
@@ -121,6 +147,15 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="hierarchy-search">
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('hierarchy.search') || 'Search entities...'}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="hierarchy-content scrollable">
|
||||
{displayEntities.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
@@ -134,7 +169,7 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
||||
</div>
|
||||
) : isRemoteConnected ? (
|
||||
<ul className="entity-list">
|
||||
{remoteEntities.map(entity => (
|
||||
{displayEntities.map(entity => (
|
||||
<li
|
||||
key={entity.id}
|
||||
className={`entity-item remote-entity ${selectedId === entity.id ? 'selected' : ''} ${!entity.enabled ? 'disabled' : ''}`}
|
||||
@@ -143,6 +178,11 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
||||
>
|
||||
<Box size={14} className="entity-icon" />
|
||||
<span className="entity-name">{entity.name}</span>
|
||||
{entity.tag !== 0 && (
|
||||
<span className="entity-tag" title={`Tag: ${entity.tag}`}>
|
||||
#{entity.tag}
|
||||
</span>
|
||||
)}
|
||||
{entity.componentCount > 0 && (
|
||||
<span className="component-count">{entity.componentCount}</span>
|
||||
)}
|
||||
|
||||
@@ -64,6 +64,7 @@ export class ProfilerService {
|
||||
private currentData: ProfilerData | null = null;
|
||||
private checkServerInterval: NodeJS.Timeout | null = null;
|
||||
private reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
private clientIdMap: Map<string, string> = new Map(); // 客户端地址 -> 客户端ID映射
|
||||
|
||||
constructor() {
|
||||
const settings = SettingsService.getInstance();
|
||||
@@ -370,11 +371,14 @@ export class ProfilerService {
|
||||
}
|
||||
}
|
||||
|
||||
const clientId = data.clientId || data.client_id || 'unknown';
|
||||
|
||||
window.dispatchEvent(new CustomEvent('profiler:remote-log', {
|
||||
detail: {
|
||||
level,
|
||||
message,
|
||||
timestamp: data.timestamp ? new Date(data.timestamp) : new Date()
|
||||
timestamp: data.timestamp ? new Date(data.timestamp) : new Date(),
|
||||
clientId
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -107,37 +107,46 @@
|
||||
}
|
||||
|
||||
.console-filter-btn:nth-child(1) {
|
||||
color: #858585;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.console-filter-btn:nth-child(1).active {
|
||||
color: #34d399;
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
.console-filter-btn:nth-child(2) {
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
.console-filter-btn:nth-child(2).active {
|
||||
color: #a0a0a0;
|
||||
border-color: #858585;
|
||||
}
|
||||
|
||||
.console-filter-btn:nth-child(2) {
|
||||
.console-filter-btn:nth-child(3) {
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.console-filter-btn:nth-child(2).active {
|
||||
.console-filter-btn:nth-child(3).active {
|
||||
color: #6eb3ff;
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
.console-filter-btn:nth-child(3) {
|
||||
.console-filter-btn:nth-child(4) {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.console-filter-btn:nth-child(3).active {
|
||||
.console-filter-btn:nth-child(4).active {
|
||||
color: #ffd54f;
|
||||
border-color: #ffc107;
|
||||
}
|
||||
|
||||
.console-filter-btn:nth-child(4) {
|
||||
.console-filter-btn:nth-child(5) {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.console-filter-btn:nth-child(4).active {
|
||||
.console-filter-btn:nth-child(5).active {
|
||||
color: #ef5350;
|
||||
border-color: #f44336;
|
||||
}
|
||||
@@ -215,6 +224,19 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log-entry-client {
|
||||
color: #10b981;
|
||||
font-size: 9px;
|
||||
white-space: nowrap;
|
||||
padding: 1px 6px;
|
||||
flex-shrink: 0;
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
border: 1px solid rgba(16, 185, 129, 0.4);
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
|
||||
.log-entry-remote {
|
||||
border-left: 2px solid #4a9eff;
|
||||
background: rgba(74, 158, 255, 0.05);
|
||||
|
||||
@@ -45,6 +45,35 @@
|
||||
animation: pulse-green 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.hierarchy-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-bottom: 1px solid var(--color-border-default);
|
||||
background-color: var(--color-bg-base);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hierarchy-search svg {
|
||||
color: var(--color-text-tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hierarchy-search input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.hierarchy-search input::placeholder {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.hierarchy-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -171,6 +200,21 @@
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.entity-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
background-color: rgba(139, 92, 246, 0.15);
|
||||
border: 1px solid rgba(139, 92, 246, 0.4);
|
||||
border-radius: var(--radius-sm);
|
||||
color: rgb(167, 139, 250);
|
||||
font-size: 10px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-family: var(--font-family-mono);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.component-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface LogEntry {
|
||||
source: string;
|
||||
message: string;
|
||||
args: unknown[];
|
||||
clientId?: string; // 远程客户端ID
|
||||
}
|
||||
|
||||
export type LogListener = (entry: LogEntry) => void;
|
||||
@@ -124,14 +125,15 @@ export class LogService implements IService {
|
||||
/**
|
||||
* 添加远程日志(从远程游戏接收)
|
||||
*/
|
||||
public addRemoteLog(level: LogLevel, message: string, timestamp?: Date): void {
|
||||
public addRemoteLog(level: LogLevel, message: string, timestamp?: Date, clientId?: string): void {
|
||||
const entry: LogEntry = {
|
||||
id: this.nextId++,
|
||||
timestamp: timestamp || new Date(),
|
||||
level,
|
||||
source: 'remote',
|
||||
message,
|
||||
args: []
|
||||
args: [],
|
||||
clientId
|
||||
};
|
||||
|
||||
this.logs.push(entry);
|
||||
|
||||
Reference in New Issue
Block a user