设置界面

This commit is contained in:
YHH
2025-10-16 13:07:19 +08:00
parent 6bcfd48a2f
commit 1ec7892338
11 changed files with 1141 additions and 6 deletions

View File

@@ -27,6 +27,7 @@ interface MenuBarProps {
onOpenPluginManager?: () => void;
onOpenProfiler?: () => void;
onOpenPortManager?: () => void;
onOpenSettings?: () => void;
onToggleDevtools?: () => void;
}
@@ -45,6 +46,7 @@ export function MenuBar({
onOpenPluginManager,
onOpenProfiler,
onOpenPortManager,
onOpenSettings,
onToggleDevtools
}: MenuBarProps) {
const [openMenu, setOpenMenu] = useState<string | null>(null);
@@ -139,6 +141,7 @@ export function MenuBar({
pluginManager: 'Plugin Manager',
tools: 'Tools',
portManager: 'Port Manager',
settings: 'Settings',
help: 'Help',
documentation: 'Documentation',
about: 'About',
@@ -170,6 +173,7 @@ export function MenuBar({
pluginManager: '插件管理器',
tools: '工具',
portManager: '端口管理器',
settings: '设置',
help: '帮助',
documentation: '文档',
about: '关于',
@@ -222,7 +226,9 @@ export function MenuBar({
{ label: t('devtools'), onClick: onToggleDevtools }
],
tools: [
{ label: t('portManager'), onClick: onOpenPortManager }
{ label: t('portManager'), onClick: onOpenPortManager },
{ separator: true },
{ label: t('settings'), onClick: onOpenSettings }
],
help: [
{ label: t('documentation'), disabled: true },

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2 } from 'lucide-react';
import { ProfilerService, ProfilerData } from '../services/ProfilerService';
import { SettingsService } from '../services/SettingsService';
import { Core } from '@esengine/ecs-framework';
import { MessageHub } from '@esengine/editor-core';
import '../styles/ProfilerDockPanel.css';
@@ -9,6 +10,25 @@ export function ProfilerDockPanel() {
const [profilerData, setProfilerData] = useState<ProfilerData | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [isServerRunning, setIsServerRunning] = useState(false);
const [port, setPort] = useState('8080');
useEffect(() => {
const settings = SettingsService.getInstance();
setPort(settings.get('profiler.port', '8080'));
const handleSettingsChange = ((event: CustomEvent) => {
const newPort = event.detail['profiler.port'];
if (newPort) {
setPort(newPort);
}
}) as EventListener;
window.addEventListener('settings:changed', handleSettingsChange);
return () => {
window.removeEventListener('settings:changed', handleSettingsChange);
};
}, []);
useEffect(() => {
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
@@ -99,7 +119,7 @@ export function ProfilerDockPanel() {
<div className="profiler-dock-empty">
<Activity size={32} />
<p>Waiting for game connection...</p>
<p className="hint">Connect your game to port 8080</p>
<p className="hint">Connect to: <code>ws://localhost:{port}</code></p>
</div>
) : (
<div className="profiler-dock-content">

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
import { Core } from '@esengine/ecs-framework';
import { Activity, BarChart3, Clock, Cpu, RefreshCw, Pause, Play, X, Wifi, WifiOff, Server, Search, Table2, TreePine } from 'lucide-react';
import { ProfilerService } from '../services/ProfilerService';
import { SettingsService } from '../services/SettingsService';
import '../styles/ProfilerWindow.css';
interface SystemPerformanceData {
@@ -33,8 +34,27 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
const [searchQuery, setSearchQuery] = useState('');
const [isConnected, setIsConnected] = useState(false);
const [isServerRunning, setIsServerRunning] = useState(false);
const [port, setPort] = useState('8080');
const animationRef = useRef<number>();
useEffect(() => {
const settings = SettingsService.getInstance();
setPort(settings.get('profiler.port', '8080'));
const handleSettingsChange = ((event: CustomEvent) => {
const newPort = event.detail['profiler.port'];
if (newPort) {
setPort(newPort);
}
}) as EventListener;
window.addEventListener('settings:changed', handleSettingsChange);
return () => {
window.removeEventListener('settings:changed', handleSettingsChange);
};
}, []);
// Check ProfilerService connection status
useEffect(() => {
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
@@ -361,7 +381,7 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
<div className="profiler-connection">
<div className="connection-port-display">
<Server size={14} />
<span>Port: 8080</span>
<span>ws://localhost:{port}</span>
</div>
{isConnected ? (
<div className="connection-status-indicator connected">

View File

@@ -0,0 +1,295 @@
import { useState, useEffect } from 'react';
import { X, Settings as SettingsIcon, ChevronRight } from 'lucide-react';
import { SettingsService } from '../services/SettingsService';
import { SettingsRegistry, SettingCategory, SettingDescriptor } from '@esengine/editor-core';
import '../styles/SettingsWindow.css';
interface SettingsWindowProps {
onClose: () => void;
settingsRegistry: SettingsRegistry;
}
export function SettingsWindow({ onClose, settingsRegistry }: SettingsWindowProps) {
const [categories, setCategories] = useState<SettingCategory[]>([]);
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null);
const [values, setValues] = useState<Map<string, any>>(new Map());
const [errors, setErrors] = useState<Map<string, string>>(new Map());
useEffect(() => {
const allCategories = settingsRegistry.getAllCategories();
setCategories(allCategories);
if (allCategories.length > 0 && !selectedCategoryId) {
const firstCategory = allCategories[0];
if (firstCategory) {
setSelectedCategoryId(firstCategory.id);
}
}
const settings = SettingsService.getInstance();
const allSettings = settingsRegistry.getAllSettings();
const initialValues = new Map<string, any>();
for (const [key, descriptor] of allSettings.entries()) {
const value = settings.get(key, descriptor.defaultValue);
initialValues.set(key, value);
}
setValues(initialValues);
}, [settingsRegistry, selectedCategoryId]);
const handleValueChange = (key: string, value: any, descriptor: SettingDescriptor) => {
const newValues = new Map(values);
newValues.set(key, value);
setValues(newValues);
const newErrors = new Map(errors);
if (!settingsRegistry.validateSetting(descriptor, value)) {
newErrors.set(key, descriptor.validator?.errorMessage || 'Invalid value');
} else {
newErrors.delete(key);
}
setErrors(newErrors);
};
const handleSave = () => {
if (errors.size > 0) {
return;
}
const settings = SettingsService.getInstance();
const changedSettings: Record<string, any> = {};
for (const [key, value] of values.entries()) {
settings.set(key, value);
changedSettings[key] = value;
}
window.dispatchEvent(new CustomEvent('settings:changed', {
detail: changedSettings
}));
onClose();
};
const handleCancel = () => {
onClose();
};
const renderSettingInput = (setting: SettingDescriptor) => {
const value = values.get(setting.key) ?? setting.defaultValue;
const error = errors.get(setting.key);
switch (setting.type) {
case 'boolean':
return (
<div className="settings-field">
<label className="settings-label settings-label-checkbox">
<input
type="checkbox"
className="settings-checkbox"
checked={value}
onChange={(e) => handleValueChange(setting.key, e.target.checked, setting)}
/>
<span>{setting.label}</span>
{setting.description && (
<span className="settings-hint">{setting.description}</span>
)}
</label>
{error && <span className="settings-error">{error}</span>}
</div>
);
case 'number':
return (
<div className="settings-field">
<label className="settings-label">
{setting.label}
{setting.description && (
<span className="settings-hint">{setting.description}</span>
)}
</label>
<input
type="number"
className={`settings-input ${error ? 'settings-input-error' : ''}`}
value={value}
onChange={(e) => handleValueChange(setting.key, parseInt(e.target.value) || 0, setting)}
placeholder={setting.placeholder}
min={setting.min}
max={setting.max}
step={setting.step}
/>
{error && <span className="settings-error">{error}</span>}
</div>
);
case 'string':
return (
<div className="settings-field">
<label className="settings-label">
{setting.label}
{setting.description && (
<span className="settings-hint">{setting.description}</span>
)}
</label>
<input
type="text"
className={`settings-input ${error ? 'settings-input-error' : ''}`}
value={value}
onChange={(e) => handleValueChange(setting.key, e.target.value, setting)}
placeholder={setting.placeholder}
/>
{error && <span className="settings-error">{error}</span>}
</div>
);
case 'select':
return (
<div className="settings-field">
<label className="settings-label">
{setting.label}
{setting.description && (
<span className="settings-hint">{setting.description}</span>
)}
</label>
<select
className={`settings-select ${error ? 'settings-input-error' : ''}`}
value={value}
onChange={(e) => {
const option = setting.options?.find(opt => String(opt.value) === e.target.value);
if (option) {
handleValueChange(setting.key, option.value, setting);
}
}}
>
{setting.options?.map((option) => (
<option key={String(option.value)} value={String(option.value)}>
{option.label}
</option>
))}
</select>
{error && <span className="settings-error">{error}</span>}
</div>
);
case 'range':
return (
<div className="settings-field">
<label className="settings-label">
{setting.label}
{setting.description && (
<span className="settings-hint">{setting.description}</span>
)}
</label>
<div className="settings-range-wrapper">
<input
type="range"
className="settings-range"
value={value}
onChange={(e) => handleValueChange(setting.key, parseFloat(e.target.value), setting)}
min={setting.min}
max={setting.max}
step={setting.step}
/>
<span className="settings-range-value">{value}</span>
</div>
{error && <span className="settings-error">{error}</span>}
</div>
);
case 'color':
return (
<div className="settings-field">
<label className="settings-label">
{setting.label}
{setting.description && (
<span className="settings-hint">{setting.description}</span>
)}
</label>
<input
type="color"
className="settings-color-input"
value={value}
onChange={(e) => handleValueChange(setting.key, e.target.value, setting)}
/>
{error && <span className="settings-error">{error}</span>}
</div>
);
default:
return null;
}
};
const selectedCategory = categories.find(c => c.id === selectedCategoryId);
return (
<div className="settings-overlay">
<div className="settings-window">
<div className="settings-header">
<div className="settings-title">
<SettingsIcon size={18} />
<h2></h2>
</div>
<button className="settings-close-btn" onClick={handleCancel}>
<X size={18} />
</button>
</div>
<div className="settings-body">
<div className="settings-sidebar">
{categories.map((category) => (
<button
key={category.id}
className={`settings-category-btn ${selectedCategoryId === category.id ? 'active' : ''}`}
onClick={() => setSelectedCategoryId(category.id)}
>
<span className="settings-category-title">{category.title}</span>
{category.description && (
<span className="settings-category-desc">{category.description}</span>
)}
<ChevronRight size={14} className="settings-category-arrow" />
</button>
))}
</div>
<div className="settings-content">
{selectedCategory && selectedCategory.sections.map((section) => (
<div key={section.id} className="settings-section">
<h3 className="settings-section-title">{section.title}</h3>
{section.description && (
<p className="settings-section-description">{section.description}</p>
)}
{section.settings.map((setting) => (
<div key={setting.key}>
{renderSettingInput(setting)}
</div>
))}
</div>
))}
{!selectedCategory && (
<div className="settings-empty">
<SettingsIcon size={48} />
<p></p>
</div>
)}
</div>
</div>
<div className="settings-footer">
<button className="settings-btn settings-btn-cancel" onClick={handleCancel}>
</button>
<button
className="settings-btn settings-btn-save"
onClick={handleSave}
disabled={errors.size > 0}
>
</button>
</div>
</div>
</div>
);
}