Feature/physics and tilemap enhancement (#247)
* feat(behavior-tree,tilemap): 修复编辑器连线缩放问题并增强插件系统 * feat(node-editor,blueprint): 新增通用节点编辑器和蓝图可视化脚本系统 * feat(editor,tilemap): 优化编辑器UI样式和Tilemap编辑器功能 * fix: 修复CodeQL安全警告和CI类型检查错误 * fix: 修复CodeQL安全警告和CI类型检查错误 * fix: 修复CodeQL安全警告和CI类型检查错误
This commit is contained in:
@@ -1,5 +1,16 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, Settings as SettingsIcon, ChevronRight } from 'lucide-react';
|
||||
/**
|
||||
* Settings Window - 设置窗口
|
||||
* 重新设计以匹配编辑器设计稿
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
X,
|
||||
Search,
|
||||
Settings as SettingsIcon,
|
||||
ChevronDown,
|
||||
ChevronRight
|
||||
} from 'lucide-react';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { SettingsService } from '../services/SettingsService';
|
||||
import { SettingsRegistry, SettingCategory, SettingDescriptor, ProjectService, PluginManager, IPluginManager } from '@esengine/editor-core';
|
||||
@@ -7,9 +18,16 @@ import { PluginListSetting } from './PluginListSetting';
|
||||
import '../styles/SettingsWindow.css';
|
||||
|
||||
interface SettingsWindowProps {
|
||||
onClose: () => void;
|
||||
settingsRegistry: SettingsRegistry;
|
||||
initialCategoryId?: string;
|
||||
onClose: () => void;
|
||||
settingsRegistry: SettingsRegistry;
|
||||
initialCategoryId?: string;
|
||||
}
|
||||
|
||||
// 主分类结构
|
||||
interface MainCategory {
|
||||
id: string;
|
||||
title: string;
|
||||
subCategories: SettingCategory[];
|
||||
}
|
||||
|
||||
export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }: SettingsWindowProps) {
|
||||
@@ -17,14 +35,88 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(initialCategoryId || null);
|
||||
const [values, setValues] = useState<Map<string, any>>(new Map());
|
||||
const [errors, setErrors] = useState<Map<string, string>>(new Map());
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
|
||||
const [expandedMainCategories, setExpandedMainCategories] = useState<Set<string>>(new Set(['通用']));
|
||||
|
||||
// 将分类组织成主分类和子分类
|
||||
const mainCategories = useMemo((): MainCategory[] => {
|
||||
const categoryMap = new Map<string, SettingCategory[]>();
|
||||
|
||||
// 定义主分类映射
|
||||
const mainCategoryMapping: Record<string, string> = {
|
||||
'appearance': '通用',
|
||||
'general': '通用',
|
||||
'project': '通用',
|
||||
'plugins': '通用',
|
||||
'editor': '通用',
|
||||
'physics': '全局',
|
||||
'rendering': '全局',
|
||||
'audio': '全局',
|
||||
'world': '世界分区',
|
||||
'local': '世界分区(本地)',
|
||||
'performance': '性能'
|
||||
};
|
||||
|
||||
categories.forEach((cat) => {
|
||||
const mainCatName = mainCategoryMapping[cat.id] || '其他';
|
||||
if (!categoryMap.has(mainCatName)) {
|
||||
categoryMap.set(mainCatName, []);
|
||||
}
|
||||
categoryMap.get(mainCatName)!.push(cat);
|
||||
});
|
||||
|
||||
// 定义固定的主分类顺序
|
||||
const orderedMainCategories = [
|
||||
'通用',
|
||||
'全局',
|
||||
'世界分区',
|
||||
'世界分区(本地)',
|
||||
'性能',
|
||||
'其他'
|
||||
];
|
||||
|
||||
return orderedMainCategories
|
||||
.filter((name) => categoryMap.has(name))
|
||||
.map((name) => ({
|
||||
id: name,
|
||||
title: name,
|
||||
subCategories: categoryMap.get(name)!
|
||||
}));
|
||||
}, [categories]);
|
||||
|
||||
// 获取显示的子分类标题
|
||||
const subCategoryTitle = useMemo(() => {
|
||||
if (!selectedCategoryId) return '';
|
||||
const cat = categories.find((c) => c.id === selectedCategoryId);
|
||||
return cat?.title || '';
|
||||
}, [categories, selectedCategoryId]);
|
||||
|
||||
// 获取主分类标题
|
||||
const mainCategoryTitle = useMemo(() => {
|
||||
for (const main of mainCategories) {
|
||||
if (main.subCategories.some((sub) => sub.id === selectedCategoryId)) {
|
||||
return main.title;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}, [mainCategories, selectedCategoryId]);
|
||||
|
||||
useEffect(() => {
|
||||
const allCategories = settingsRegistry.getAllCategories();
|
||||
setCategories(allCategories);
|
||||
|
||||
// 默认展开所有section
|
||||
const allSectionIds = new Set<string>();
|
||||
allCategories.forEach((cat) => {
|
||||
cat.sections.forEach((section) => {
|
||||
allSectionIds.add(`${cat.id}-${section.id}`);
|
||||
});
|
||||
});
|
||||
setExpandedSections(allSectionIds);
|
||||
|
||||
if (allCategories.length > 0 && !selectedCategoryId) {
|
||||
// 如果有 initialCategoryId,尝试使用它
|
||||
if (initialCategoryId && allCategories.some(c => c.id === initialCategoryId)) {
|
||||
if (initialCategoryId && allCategories.some((c) => c.id === initialCategoryId)) {
|
||||
setSelectedCategoryId(initialCategoryId);
|
||||
} else {
|
||||
const firstCategory = allCategories[0];
|
||||
@@ -40,7 +132,6 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
const initialValues = new Map<string, any>();
|
||||
|
||||
for (const [key, descriptor] of allSettings.entries()) {
|
||||
// Project-scoped settings are loaded from ProjectService
|
||||
if (key.startsWith('project.') && projectService) {
|
||||
if (key === 'project.uiDesignResolution.width') {
|
||||
const resolution = projectService.getUIDesignResolution();
|
||||
@@ -52,7 +143,6 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
const resolution = projectService.getUIDesignResolution();
|
||||
initialValues.set(key, `${resolution.width}x${resolution.height}`);
|
||||
} else {
|
||||
// For other project settings, use default
|
||||
initialValues.set(key, descriptor.defaultValue);
|
||||
}
|
||||
} else {
|
||||
@@ -62,7 +152,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
}
|
||||
|
||||
setValues(initialValues);
|
||||
}, [settingsRegistry, selectedCategoryId]);
|
||||
}, [settingsRegistry, initialCategoryId]);
|
||||
|
||||
const handleValueChange = (key: string, value: any, descriptor: SettingDescriptor) => {
|
||||
const newValues = new Map(values);
|
||||
@@ -71,7 +161,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
|
||||
const newErrors = new Map(errors);
|
||||
if (!settingsRegistry.validateSetting(descriptor, value)) {
|
||||
newErrors.set(key, descriptor.validator?.errorMessage || 'Invalid value');
|
||||
newErrors.set(key, descriptor.validator?.errorMessage || '无效值');
|
||||
} else {
|
||||
newErrors.delete(key);
|
||||
}
|
||||
@@ -87,13 +177,11 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
const projectService = Core.services.tryResolve<ProjectService>(ProjectService);
|
||||
const changedSettings: Record<string, any> = {};
|
||||
|
||||
// Track UI resolution changes for batch saving
|
||||
let uiResolutionChanged = false;
|
||||
let newWidth = 1920;
|
||||
let newHeight = 1080;
|
||||
|
||||
for (const [key, value] of values.entries()) {
|
||||
// Project-scoped settings are saved to ProjectService
|
||||
if (key.startsWith('project.') && projectService) {
|
||||
if (key === 'project.uiDesignResolution.width') {
|
||||
newWidth = value;
|
||||
@@ -102,7 +190,6 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
newHeight = value;
|
||||
uiResolutionChanged = true;
|
||||
} else if (key === 'project.uiDesignResolution.preset') {
|
||||
// Preset changes width and height together
|
||||
const [w, h] = value.split('x').map(Number);
|
||||
if (w && h) {
|
||||
newWidth = w;
|
||||
@@ -117,7 +204,6 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
}
|
||||
}
|
||||
|
||||
// Save UI resolution if changed
|
||||
if (uiResolutionChanged && projectService) {
|
||||
await projectService.setUIDesignResolution({ width: newWidth, height: newHeight });
|
||||
}
|
||||
@@ -133,6 +219,76 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleResetToDefault = () => {
|
||||
const allSettings = settingsRegistry.getAllSettings();
|
||||
const defaultValues = new Map<string, any>();
|
||||
for (const [key, descriptor] of allSettings.entries()) {
|
||||
defaultValues.set(key, descriptor.defaultValue);
|
||||
}
|
||||
setValues(defaultValues);
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
const exportData: Record<string, any> = {};
|
||||
for (const [key, value] of values.entries()) {
|
||||
exportData[key] = value;
|
||||
}
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'editor-settings.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
const importData = JSON.parse(text);
|
||||
const newValues = new Map(values);
|
||||
for (const [key, value] of Object.entries(importData)) {
|
||||
newValues.set(key, value);
|
||||
}
|
||||
setValues(newValues);
|
||||
} catch (err) {
|
||||
console.error('Failed to import settings:', err);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
const toggleSection = (sectionId: string) => {
|
||||
setExpandedSections((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(sectionId)) {
|
||||
newSet.delete(sectionId);
|
||||
} else {
|
||||
newSet.add(sectionId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleMainCategory = (categoryId: string) => {
|
||||
setExpandedMainCategories((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(categoryId)) {
|
||||
newSet.delete(categoryId);
|
||||
} else {
|
||||
newSet.add(categoryId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const renderSettingInput = (setting: SettingDescriptor) => {
|
||||
const value = values.get(setting.key) ?? setting.defaultValue;
|
||||
const error = errors.get(setting.key);
|
||||
@@ -140,105 +296,109 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
switch (setting.type) {
|
||||
case 'boolean':
|
||||
return (
|
||||
<div className="settings-field">
|
||||
<label className="settings-label settings-label-checkbox">
|
||||
<div className="settings-row">
|
||||
<div className="settings-row-label">
|
||||
{setting.description && (
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
<span>{setting.label}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'number':
|
||||
return (
|
||||
<div className="settings-field">
|
||||
<label className="settings-label">
|
||||
{setting.label}
|
||||
<div className="settings-row">
|
||||
<div className="settings-row-label">
|
||||
{setting.description && (
|
||||
<span className="settings-hint">{setting.description}</span>
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
</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>}
|
||||
<span>{setting.label}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<input
|
||||
type="number"
|
||||
className={`settings-number-input ${error ? '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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'string':
|
||||
return (
|
||||
<div className="settings-field">
|
||||
<label className="settings-label">
|
||||
{setting.label}
|
||||
<div className="settings-row">
|
||||
<div className="settings-row-label">
|
||||
{setting.description && (
|
||||
<span className="settings-hint">{setting.description}</span>
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
</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>}
|
||||
<span>{setting.label}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<input
|
||||
type="text"
|
||||
className={`settings-text-input ${error ? 'error' : ''}`}
|
||||
value={value}
|
||||
onChange={(e) => handleValueChange(setting.key, e.target.value, setting)}
|
||||
placeholder={setting.placeholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<div className="settings-field">
|
||||
<label className="settings-label">
|
||||
{setting.label}
|
||||
<div className="settings-row">
|
||||
<div className="settings-row-label">
|
||||
{setting.description && (
|
||||
<span className="settings-hint">{setting.description}</span>
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
</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>}
|
||||
<span>{setting.label}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<select
|
||||
className={`settings-select ${error ? '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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'range':
|
||||
return (
|
||||
<div className="settings-field">
|
||||
<label className="settings-label">
|
||||
{setting.label}
|
||||
<div className="settings-row">
|
||||
<div className="settings-row-label">
|
||||
{setting.description && (
|
||||
<span className="settings-hint">{setting.description}</span>
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
</label>
|
||||
<div className="settings-range-wrapper">
|
||||
<span>{setting.label}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<input
|
||||
type="range"
|
||||
className="settings-range"
|
||||
@@ -250,26 +410,28 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
/>
|
||||
<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}
|
||||
<div className="settings-row">
|
||||
<div className="settings-row-label">
|
||||
{setting.description && (
|
||||
<span className="settings-hint">{setting.description}</span>
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
</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>}
|
||||
<span>{setting.label}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<div className="settings-color-bar" style={{ backgroundColor: value }}>
|
||||
<input
|
||||
type="color"
|
||||
className="settings-color-input"
|
||||
value={value}
|
||||
onChange={(e) => handleValueChange(setting.key, e.target.value, setting)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -277,15 +439,30 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
const pluginManager = Core.services.tryResolve<PluginManager>(IPluginManager);
|
||||
if (!pluginManager) {
|
||||
return (
|
||||
<div className="settings-field settings-field-full">
|
||||
<div className="settings-row">
|
||||
<p className="settings-error">PluginManager 不可用</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="settings-field settings-field-full">
|
||||
<div className="settings-plugin-list">
|
||||
<PluginListSetting pluginManager={pluginManager} />
|
||||
{error && <span className="settings-error">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'collisionMatrix': {
|
||||
const CustomRenderer = setting.customRenderer as React.ComponentType<any> | undefined;
|
||||
if (CustomRenderer) {
|
||||
return (
|
||||
<div className="settings-custom-renderer">
|
||||
<CustomRenderer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="settings-row">
|
||||
<p className="settings-hint">碰撞矩阵编辑器未配置</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -298,71 +475,149 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
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 className="settings-overlay" onClick={handleCancel}>
|
||||
<div className="settings-window-new" onClick={(e) => e.stopPropagation()}>
|
||||
{/* Left Sidebar */}
|
||||
<div className="settings-sidebar-new">
|
||||
<div className="settings-sidebar-header">
|
||||
<SettingsIcon size={16} />
|
||||
<span>编辑器偏好设置</span>
|
||||
<button className="settings-sidebar-close" onClick={handleCancel}>
|
||||
<X size={14} />
|
||||
</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 className="settings-sidebar-search">
|
||||
<span>所有设置</span>
|
||||
</div>
|
||||
|
||||
<div className="settings-sidebar-categories">
|
||||
{mainCategories.map((mainCat) => (
|
||||
<div key={mainCat.id} className="settings-main-category">
|
||||
<div
|
||||
className="settings-main-category-header"
|
||||
onClick={() => toggleMainCategory(mainCat.id)}
|
||||
>
|
||||
{expandedMainCategories.has(mainCat.id) ? (
|
||||
<ChevronDown size={12} />
|
||||
) : (
|
||||
<ChevronRight size={12} />
|
||||
)}
|
||||
<span>{mainCat.title}</span>
|
||||
</div>
|
||||
|
||||
{expandedMainCategories.has(mainCat.id) && (
|
||||
<div className="settings-sub-categories">
|
||||
{mainCat.subCategories.map((subCat) => (
|
||||
<button
|
||||
key={subCat.id}
|
||||
className={`settings-sub-category ${selectedCategoryId === subCat.id ? 'active' : ''}`}
|
||||
onClick={() => setSelectedCategoryId(subCat.id)}
|
||||
>
|
||||
{subCat.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Content */}
|
||||
<div className="settings-content-new">
|
||||
{/* Top Header */}
|
||||
<div className="settings-content-header">
|
||||
<div className="settings-search-bar">
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-header-actions">
|
||||
<button className="settings-icon-btn" title="设置">
|
||||
<SettingsIcon size={14} />
|
||||
</button>
|
||||
<button className="settings-action-btn" onClick={handleExport}>
|
||||
导出......
|
||||
</button>
|
||||
<button className="settings-action-btn" onClick={handleImport}>
|
||||
导入......
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Title */}
|
||||
<div className="settings-category-title-bar">
|
||||
<div className="settings-category-breadcrumb">
|
||||
<ChevronDown size={14} />
|
||||
<span className="settings-breadcrumb-main">{mainCategoryTitle}</span>
|
||||
<span className="settings-breadcrumb-separator">-</span>
|
||||
<span className="settings-breadcrumb-sub">{subCategoryTitle}</span>
|
||||
</div>
|
||||
{selectedCategory?.description && (
|
||||
<p className="settings-category-desc">{selectedCategory.description}</p>
|
||||
)}
|
||||
<div className="settings-category-actions">
|
||||
<button className="settings-category-action-btn" onClick={handleResetToDefault}>
|
||||
设置为默认值
|
||||
</button>
|
||||
<button className="settings-category-action-btn" onClick={handleExport}>
|
||||
导出......
|
||||
</button>
|
||||
<button className="settings-category-action-btn" onClick={handleImport}>
|
||||
导入......
|
||||
</button>
|
||||
<button className="settings-category-action-btn" onClick={handleResetToDefault}>
|
||||
重置为默认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings Content */}
|
||||
<div className="settings-sections-container">
|
||||
{selectedCategory && selectedCategory.sections.map((section) => {
|
||||
const sectionKey = `${selectedCategory.id}-${section.id}`;
|
||||
const isExpanded = expandedSections.has(sectionKey);
|
||||
|
||||
return (
|
||||
<div key={section.id} className="settings-section-new">
|
||||
<div
|
||||
className="settings-section-header-new"
|
||||
onClick={() => toggleSection(sectionKey)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={12} />
|
||||
) : (
|
||||
<ChevronRight size={12} />
|
||||
)}
|
||||
<span>{section.title}</span>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="settings-section-content-new">
|
||||
{section.settings.map((setting) => (
|
||||
<div key={setting.key}>
|
||||
{renderSettingInput(setting)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{!selectedCategory && (
|
||||
<div className="settings-empty">
|
||||
<div className="settings-empty-new">
|
||||
<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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user