'use client';
import { useState, useMemo, useEffect, useRef } from 'react';
import { useI18n } from '../lib/useI18n';
// 分类的颜色和标识
const CATEGORY_STYLES = {
work: { color: 'var(--cat-work)', bg: 'var(--cat-work-bg)' },
bookInfo: { color: 'var(--cat-bookinfo)', bg: 'var(--cat-bookinfo-bg)' },
character: { color: 'var(--cat-character)', bg: 'var(--cat-character-bg)' },
location: { color: 'var(--cat-location)', bg: 'var(--cat-location-bg)' },
world: { color: 'var(--cat-world)', bg: 'var(--cat-world-bg)' },
object: { color: 'var(--cat-object)', bg: 'var(--cat-object-bg)' },
plot: { color: 'var(--cat-plot)', bg: 'var(--cat-plot-bg)' },
rules: { color: 'var(--cat-rules)', bg: 'var(--cat-rules-bg)' },
custom: { color: 'var(--cat-custom)', bg: 'var(--cat-custom-bg)' },
};
function getCategoryStyle(category) {
return CATEGORY_STYLES[category] || CATEGORY_STYLES.custom;
}
// 单个树节点
function TreeNode({ node, nodes, selectedId, onSelect, onAdd, onDelete, onRename, onToggleEnabled, collapsedIds, onToggleCollapse, level = 0 }) {
const { t } = useI18n();
const [isRenaming, setIsRenaming] = useState(false);
const [renameValue, setRenameValue] = useState('');
const children = nodes.filter(n => n.parentId === node.id);
const isFolder = node.type === 'folder' || node.type === 'special' || node.type === 'work';
const isWork = node.type === 'work';
const isRoot = node.parentId === null;
// 内置子分类(作品下的固定分类,如人物设定、空间/地点等)不可删除
const builtinSuffixes = ['bookinfo', 'characters', 'locations', 'world', 'objects', 'plot', 'rules'];
const parentNode = node.parentId ? nodes.find(n => n.id === node.parentId) : null;
const isBuiltinCategory = parentNode && parentNode.type === 'work' && builtinSuffixes.some(s => node.id.endsWith('-' + s));
const canDelete = !isRoot && !isBuiltinCategory;
const isCollapsed = collapsedIds.has(node.id);
const isSelected = selectedId === node.id;
const isDisabled = node.enabled === false;
const style = getCategoryStyle(node.category);
const nodeRef = useRef(null);
// 计算子项数目(递归)
const descendantCount = useMemo(() => {
if (!isFolder) return 0;
let count = 0;
const countChildren = (parentId) => {
nodes.filter(n => n.parentId === parentId).forEach(child => {
if (child.type === 'item') count++;
else countChildren(child.id);
});
};
countChildren(node.id);
return count;
}, [node.id, nodes, isFolder]);
const handleRename = () => {
if (renameValue.trim()) {
onRename(node.id, renameValue.trim());
}
setIsRenaming(false);
};
// 如果是被点击并展开的节点,自动滚入视图
useEffect(() => {
if (isSelected && isFolder && nodeRef.current) {
// 给一点延迟让折叠动画/渲染完成
setTimeout(() => {
nodeRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 100);
}
}, [isSelected, isFolder]);
return (
0 ? 12 : 0 }} ref={nodeRef}>
onSelect(node.id)}
style={isRoot ? { borderLeft: `3px solid ${style.color}`, marginBottom: 2 } : {}}
title={isDisabled ? t('settingsTree.disabledHint') : ''}
>
{/* 折叠箭头 */}
{isFolder && (
{ e.stopPropagation(); onToggleCollapse(node.id); }}
style={{ cursor: 'pointer', color: 'var(--text-muted)', fontSize: 10 }}
>
{isCollapsed ? '▶' : '▼'}
)}
{/* 图标 */}
{node.icon || (isFolder ? '📁' : '📄')}
{/* 名称 */}
{isRenaming ? (
setRenameValue(e.target.value)}
onBlur={handleRename}
onKeyDown={e => { if (e.key === 'Enter') handleRename(); if (e.key === 'Escape') setIsRenaming(false); }}
autoFocus
onClick={e => e.stopPropagation()}
style={{ border: '1px solid var(--accent)', borderRadius: 3, padding: '1px 4px', fontSize: 13, background: 'var(--bg-primary)', color: 'var(--text-primary)', outline: 'none' }}
/>
) : (
{node.name}
)}
{/* 启用/禁用开关(仅item节点) */}
{!isFolder && !isRoot && (
)}
{/* 子项计数徽标(folder节点) */}
{isFolder && descendantCount > 0 && (
{descendantCount}
)}
{/* 操作按钮 */}
{/* 添子项 */}
{isFolder && (
)}
{/* 重命名 */}
{!isRoot && (
)}
{/* 删除 */}
{canDelete && (
)}
{/* 子节点 */}
{isFolder && !isCollapsed && (
{children
.sort((a, b) => (a.type === 'folder' ? -1 : 1) - (b.type === 'folder' ? -1 : 1) || (a.sortOrder || 0) - (b.sortOrder || 0))
.map(child => (
))}
)}
);
}
// 设定树组件
export default function SettingsTree({
nodes,
selectedId,
onSelect,
onAdd,
onDelete,
onRename,
onToggleEnabled,
searchQuery = '',
expandedCategory = null,
onExpandComplete,
}) {
const [collapsedIds, setCollapsedIds] = useState(new Set());
const toggleCollapse = (id) => {
setCollapsedIds(prev => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
// 监听外部控制展开特定分类文件夹
useEffect(() => {
if (expandedCategory) {
const targetFolder = nodes.find(n => n.type === 'folder' && n.category === expandedCategory && n.parentId !== null);
if (targetFolder) {
// 确保父节点和该节点自己都没被折叠
setCollapsedIds(prev => {
const next = new Set(prev);
next.delete(targetFolder.id);
if (targetFolder.parentId) {
next.delete(targetFolder.parentId);
}
return next;
});
onSelect(targetFolder.id);
if (onExpandComplete) {
onExpandComplete();
}
}
}
}, [expandedCategory, nodes, onSelect, onExpandComplete]);
// 获取根节点
const rootNodes = nodes.filter(n => n.parentId === null);
// 搜索过滤
const filteredNodes = useMemo(() => {
if (!searchQuery.trim()) return nodes;
const q = searchQuery.toLowerCase();
// 找到匹配的节点和它们到根的路径
const matchIds = new Set();
nodes.forEach(n => {
if (n.name.toLowerCase().includes(q) ||
(n.content?.description || '').toLowerCase().includes(q) ||
(n.content?.personality || '').toLowerCase().includes(q) ||
(n.content?.background || '').toLowerCase().includes(q)) {
// 添加自己和所有祖先
let current = n;
while (current) {
matchIds.add(current.id);
current = current.parentId ? nodes.find(p => p.id === current.parentId) : null;
}
}
});
return nodes.filter(n => matchIds.has(n.id));
}, [nodes, searchQuery]);
const filteredRootNodes = rootNodes.filter(n => filteredNodes.some(fn => fn.id === n.id));
return (
{filteredRootNodes.map(root => (
))}
);
}