Files
esengine/packages/editor-app/src/components/FileTree.tsx

773 lines
28 KiB
TypeScript
Raw Normal View History

import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { Folder, ChevronRight, ChevronDown, File, Edit3, Trash2, FolderOpen, Copy, FileText, FolderPlus, ChevronsDown, ChevronsUp } from 'lucide-react';
2025-10-15 10:08:15 +08:00
import { TauriAPI, DirectoryEntry } from '../api/tauri';
import { MessageHub, FileActionRegistry } from '@esengine/editor-core';
import { Core } from '@esengine/ecs-framework';
import { ContextMenu, ContextMenuItem } from './ContextMenu';
import { ConfirmDialog } from './ConfirmDialog';
import { PromptDialog } from './PromptDialog';
2025-10-15 10:08:15 +08:00
import '../styles/FileTree.css';
interface TreeNode {
name: string;
path: string;
type: 'folder' | 'file';
size?: number;
modified?: number;
2025-10-15 10:08:15 +08:00
children?: TreeNode[];
expanded?: boolean;
loaded?: boolean;
}
interface FileTreeProps {
rootPath: string | null;
onSelectFile?: (path: string) => void;
selectedPath?: string | null;
messageHub?: MessageHub;
searchQuery?: string;
showFiles?: boolean;
2025-10-15 10:08:15 +08:00
}
export interface FileTreeHandle {
collapseAll: () => void;
refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 (#216) * refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 * feat(editor): 添加插件市场功能 * feat(editor): 重构插件市场以支持版本管理和ZIP打包 * feat(editor): 重构插件发布流程并修复React渲染警告 * fix(plugin): 修复插件发布和市场的路径不一致问题 * feat: 重构插件发布流程并添加插件删除功能 * fix(editor): 完善插件删除功能并修复多个关键问题 * fix(auth): 修复自动登录与手动登录的竞态条件问题 * feat(editor): 重构插件管理流程 * feat(editor): 支持 ZIP 文件直接发布插件 - 新增 PluginSourceParser 解析插件源 - 重构发布流程支持文件夹和 ZIP 两种方式 - 优化发布向导 UI * feat(editor): 插件市场支持多版本安装 - 插件解压到项目 plugins 目录 - 新增 Tauri 后端安装/卸载命令 - 支持选择任意版本安装 - 修复打包逻辑,保留完整 dist 目录结构 * feat(editor): 个人中心支持多版本管理 - 合并同一插件的不同版本 - 添加版本历史展开/折叠功能 - 禁止有待审核 PR 时更新插件 * fix(editor): 修复 InspectorRegistry 服务注册 - InspectorRegistry 实现 IService 接口 - 注册到 Core.services 供插件使用 * feat(behavior-tree-editor): 完善插件注册和文件操作 - 添加文件创建模板和操作处理器 - 实现右键菜单创建行为树功能 - 修复文件读取权限问题(使用 Tauri 命令) - 添加 BehaviorTreeEditorPanel 组件 - 修复 rollup 配置支持动态导入 * feat(plugin): 完善插件构建和发布流程 * fix(behavior-tree-editor): 完整恢复编辑器并修复 Toast 集成 * fix(behavior-tree-editor): 修复节点选中、连线跟随和文件加载问题并优化性能 * fix(behavior-tree-editor): 修复端口连接失败问题并优化连线样式 * refactor(behavior-tree-editor): 移除调试面板功能简化代码结构 * refactor(behavior-tree-editor): 清理冗余代码合并重复逻辑 * feat(behavior-tree-editor): 完善编辑器核心功能增强扩展性 * fix(lint): 修复ESLint错误确保CI通过 * refactor(behavior-tree-editor): 优化编辑器工具栏和编译器功能 * refactor(behavior-tree-editor): 清理技术债务,优化代码质量 * fix(editor-app): 修复字符串替换安全问题
2025-11-18 14:46:51 +08:00
refresh: () => void;
}
export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, onSelectFile, selectedPath, messageHub, searchQuery, showFiles = true }, ref) => {
const [tree, setTree] = useState<TreeNode[]>([]);
const [loading, setLoading] = useState(false);
const [internalSelectedPath, setInternalSelectedPath] = useState<string | null>(null);
const [contextMenu, setContextMenu] = useState<{
position: { x: number; y: number };
node: TreeNode | null;
} | null>(null);
const [renamingNode, setRenamingNode] = useState<string | null>(null);
const [newName, setNewName] = useState('');
const [deleteDialog, setDeleteDialog] = useState<{ node: TreeNode } | null>(null);
const [promptDialog, setPromptDialog] = useState<{
type: 'create-file' | 'create-folder' | 'create-template';
parentPath: string;
templateExtension?: string;
templateContent?: (fileName: string) => Promise<string>;
} | null>(null);
const [filteredTree, setFilteredTree] = useState<TreeNode[]>([]);
const fileActionRegistry = Core.services.resolve(FileActionRegistry);
2025-10-15 10:08:15 +08:00
const collapseAll = () => {
const collapseNode = (node: TreeNode): TreeNode => {
if (node.type === 'folder') {
return {
...node,
expanded: false,
children: node.children ? node.children.map(collapseNode) : node.children
};
}
return node;
};
const collapsedTree = tree.map(node => collapseNode(node));
setTree(collapsedTree);
};
useImperativeHandle(ref, () => ({
refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 (#216) * refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 * feat(editor): 添加插件市场功能 * feat(editor): 重构插件市场以支持版本管理和ZIP打包 * feat(editor): 重构插件发布流程并修复React渲染警告 * fix(plugin): 修复插件发布和市场的路径不一致问题 * feat: 重构插件发布流程并添加插件删除功能 * fix(editor): 完善插件删除功能并修复多个关键问题 * fix(auth): 修复自动登录与手动登录的竞态条件问题 * feat(editor): 重构插件管理流程 * feat(editor): 支持 ZIP 文件直接发布插件 - 新增 PluginSourceParser 解析插件源 - 重构发布流程支持文件夹和 ZIP 两种方式 - 优化发布向导 UI * feat(editor): 插件市场支持多版本安装 - 插件解压到项目 plugins 目录 - 新增 Tauri 后端安装/卸载命令 - 支持选择任意版本安装 - 修复打包逻辑,保留完整 dist 目录结构 * feat(editor): 个人中心支持多版本管理 - 合并同一插件的不同版本 - 添加版本历史展开/折叠功能 - 禁止有待审核 PR 时更新插件 * fix(editor): 修复 InspectorRegistry 服务注册 - InspectorRegistry 实现 IService 接口 - 注册到 Core.services 供插件使用 * feat(behavior-tree-editor): 完善插件注册和文件操作 - 添加文件创建模板和操作处理器 - 实现右键菜单创建行为树功能 - 修复文件读取权限问题(使用 Tauri 命令) - 添加 BehaviorTreeEditorPanel 组件 - 修复 rollup 配置支持动态导入 * feat(plugin): 完善插件构建和发布流程 * fix(behavior-tree-editor): 完整恢复编辑器并修复 Toast 集成 * fix(behavior-tree-editor): 修复节点选中、连线跟随和文件加载问题并优化性能 * fix(behavior-tree-editor): 修复端口连接失败问题并优化连线样式 * refactor(behavior-tree-editor): 移除调试面板功能简化代码结构 * refactor(behavior-tree-editor): 清理冗余代码合并重复逻辑 * feat(behavior-tree-editor): 完善编辑器核心功能增强扩展性 * fix(lint): 修复ESLint错误确保CI通过 * refactor(behavior-tree-editor): 优化编辑器工具栏和编译器功能 * refactor(behavior-tree-editor): 清理技术债务,优化代码质量 * fix(editor-app): 修复字符串替换安全问题
2025-11-18 14:46:51 +08:00
collapseAll,
refresh: refreshTree
}));
useEffect(() => {
if (rootPath) {
loadRootDirectory(rootPath);
2025-10-15 10:08:15 +08:00
} else {
setTree([]);
}
}, [rootPath]);
useEffect(() => {
if (selectedPath) {
setInternalSelectedPath(selectedPath);
}
}, [selectedPath]);
useEffect(() => {
const performSearch = async () => {
const filterByFileType = (nodes: TreeNode[]): TreeNode[] => {
return nodes
.filter(node => showFiles || node.type === 'folder')
.map(node => ({
...node,
children: node.children ? filterByFileType(node.children) : node.children
}));
};
let result = filterByFileType(tree);
if (searchQuery && searchQuery.trim()) {
const query = searchQuery.toLowerCase();
const loadAndFilterTree = async (nodes: TreeNode[]): Promise<TreeNode[]> => {
const filtered: TreeNode[] = [];
for (const node of nodes) {
const nameMatches = node.name.toLowerCase().includes(query);
let filteredChildren: TreeNode[] = [];
if (node.type === 'folder') {
let childrenToSearch = node.children || [];
if (!node.loaded) {
try {
const entries = await TauriAPI.listDirectory(node.path);
childrenToSearch = entriesToNodes(entries);
} catch (error) {
console.error('Failed to load children for search:', error);
}
}
if (childrenToSearch.length > 0) {
filteredChildren = await loadAndFilterTree(childrenToSearch);
}
}
if (nameMatches || filteredChildren.length > 0) {
filtered.push({
...node,
expanded: filteredChildren.length > 0,
loaded: true,
children: filteredChildren.length > 0 ? filteredChildren : (node.type === 'folder' ? [] : undefined)
});
}
}
return filtered;
};
result = await loadAndFilterTree(result);
}
setFilteredTree(result);
};
performSearch();
}, [searchQuery, tree, showFiles]);
const loadRootDirectory = async (path: string) => {
setLoading(true);
try {
const entries = await TauriAPI.listDirectory(path);
const children = entriesToNodes(entries);
// 创建根节点
const rootName = path.split(/[/\\]/).filter((p) => p).pop() || 'Project';
const rootNode: TreeNode = {
name: rootName,
path: path,
type: 'folder',
children: children,
expanded: true,
loaded: true
};
setTree([rootNode]);
} catch (error) {
console.error('Failed to load directory:', error);
setTree([]);
} finally {
setLoading(false);
2025-10-15 10:08:15 +08:00
}
};
const entriesToNodes = (entries: DirectoryEntry[]): TreeNode[] => {
return entries
.sort((a, b) => {
if (a.is_dir === b.is_dir) return a.name.localeCompare(b.name);
return a.is_dir ? -1 : 1;
})
.map((entry) => ({
name: entry.name,
path: entry.path,
type: entry.is_dir ? 'folder' as const : 'file' as const,
size: entry.size,
modified: entry.modified,
children: entry.is_dir ? [] : undefined,
expanded: false,
loaded: entry.is_dir ? false : undefined
}));
};
2025-10-15 10:08:15 +08:00
const loadChildren = async (node: TreeNode): Promise<TreeNode[]> => {
try {
const entries = await TauriAPI.listDirectory(node.path);
return entriesToNodes(entries);
} catch (error) {
console.error('Failed to load children:', error);
return [];
}
};
2025-10-15 10:08:15 +08:00
const toggleNode = async (nodePath: string) => {
const updateTree = async (nodes: TreeNode[]): Promise<TreeNode[]> => {
const newNodes: TreeNode[] = [];
for (const node of nodes) {
if (node.path === nodePath) {
if (!node.loaded) {
const children = await loadChildren(node);
newNodes.push({
...node,
expanded: true,
loaded: true,
children
});
} else {
newNodes.push({
...node,
expanded: !node.expanded
});
}
} else if (node.children) {
newNodes.push({
...node,
children: await updateTree(node.children)
});
} else {
newNodes.push(node);
}
}
return newNodes;
};
const newTree = await updateTree(tree);
setTree(newTree);
};
2025-10-15 10:08:15 +08:00
const refreshTree = async () => {
if (!rootPath) return;
// 保存当前展开状态
const expandedPaths = new Set<string>();
const collectExpandedPaths = (nodes: TreeNode[]) => {
for (const node of nodes) {
if (node.type === 'folder' && node.expanded) {
expandedPaths.add(node.path);
if (node.children) {
collectExpandedPaths(node.children);
}
}
}
};
collectExpandedPaths(tree);
// 重新加载根目录,获取最新的文件结构
try {
const entries = await TauriAPI.listDirectory(rootPath);
const children = entriesToNodes(entries);
const rootName = rootPath.split(/[/\\]/).filter((p) => p).pop() || 'Project';
let rootNode: TreeNode = {
name: rootName,
path: rootPath,
type: 'folder',
children: children,
expanded: true,
loaded: true
};
// 恢复展开状态
if (expandedPaths.size > 0) {
const restoreExpandedState = async (node: TreeNode): Promise<TreeNode> => {
if (node.type === 'folder' && expandedPaths.has(node.path)) {
let children = node.children || [];
if (!node.loaded && node.children) {
children = await loadChildren(node);
}
const restoredChildren = await Promise.all(
children.map(child => restoreExpandedState(child))
);
return {
...node,
expanded: true,
loaded: true,
children: restoredChildren
};
} else if (node.type === 'folder' && node.children) {
const restoredChildren = await Promise.all(
node.children.map(child => restoreExpandedState(child))
);
return {
...node,
children: restoredChildren
};
}
return node;
};
rootNode = await restoreExpandedState(rootNode);
}
setTree([rootNode]);
} catch (error) {
console.error('Failed to refresh directory:', error);
}
};
const expandAll = async () => {
const expandNode = async (node: TreeNode): Promise<TreeNode> => {
if (node.type === 'folder') {
let children = node.children || [];
if (!node.loaded) {
try {
const entries = await TauriAPI.listDirectory(node.path);
children = entriesToNodes(entries);
} catch (error) {
console.error('Failed to load children:', error);
children = [];
}
}
const expandedChildren = await Promise.all(
children.map(child => expandNode(child))
);
return {
...node,
expanded: true,
loaded: true,
children: expandedChildren
};
}
return node;
};
const expandedTree = await Promise.all(tree.map(node => expandNode(node)));
setTree(expandedTree);
};
const handleRename = async (node: TreeNode) => {
if (!newName || newName === node.name) {
setRenamingNode(null);
return;
}
const pathParts = node.path.split(/[/\\]/);
pathParts[pathParts.length - 1] = newName;
const newPath = pathParts.join('/');
try {
await TauriAPI.renameFileOrFolder(node.path, newPath);
await refreshTree();
setRenamingNode(null);
setNewName('');
} catch (error) {
console.error('Failed to rename:', error);
alert(`重命名失败: ${error}`);
}
};
const handleDeleteClick = (node: TreeNode) => {
setContextMenu(null);
setDeleteDialog({ node });
};
const handleDeleteConfirm = async () => {
if (!deleteDialog) return;
const node = deleteDialog.node;
setDeleteDialog(null);
try {
if (node.type === 'folder') {
await TauriAPI.deleteFolder(node.path);
} else {
await TauriAPI.deleteFile(node.path);
}
await refreshTree();
} catch (error) {
console.error('Failed to delete:', error);
alert(`删除失败: ${error}`);
}
};
const handleCreateFileClick = (parentPath: string) => {
setContextMenu(null);
setPromptDialog({ type: 'create-file', parentPath });
};
const handleCreateFolderClick = (parentPath: string) => {
setContextMenu(null);
setPromptDialog({ type: 'create-folder', parentPath });
};
const handleCreateTemplateFileClick = (parentPath: string, template: any) => {
setContextMenu(null);
setPromptDialog({
type: 'create-template',
parentPath,
templateExtension: template.extension,
templateContent: template.createContent
});
};
const handlePromptConfirm = async (value: string) => {
if (!promptDialog) return;
const { type, parentPath, templateExtension, templateContent } = promptDialog;
setPromptDialog(null);
let fileName = value;
let targetPath = `${parentPath}/${value}`;
try {
if (type === 'create-file') {
await TauriAPI.createFile(targetPath);
} else if (type === 'create-folder') {
await TauriAPI.createDirectory(targetPath);
} else if (type === 'create-template' && templateExtension && templateContent) {
if (!fileName.endsWith(`.${templateExtension}`)) {
fileName = `${fileName}.${templateExtension}`;
targetPath = `${parentPath}/${fileName}`;
}
const content = await templateContent(fileName);
await TauriAPI.writeFileContent(targetPath, content);
}
await refreshTree();
} catch (error) {
console.error(`Failed to ${type}:`, error);
alert(`${type === 'create-file' ? '创建文件' : type === 'create-folder' ? '创建文件夹' : '创建模板文件'}失败: ${error}`);
}
};
const getContextMenuItems = (node: TreeNode | null): ContextMenuItem[] => {
if (!node) {
const baseItems: ContextMenuItem[] = [
{
label: '新建文件',
icon: <FileText size={16} />,
onClick: () => rootPath && handleCreateFileClick(rootPath)
},
{
label: '新建文件夹',
icon: <FolderPlus size={16} />,
onClick: () => rootPath && handleCreateFolderClick(rootPath)
}
];
if (fileActionRegistry && rootPath) {
const templates = fileActionRegistry.getCreationTemplates();
if (templates.length > 0) {
baseItems.push({ label: '', separator: true, onClick: () => {} });
for (const template of templates) {
baseItems.push({
label: template.label,
icon: template.icon,
onClick: () => handleCreateTemplateFileClick(rootPath, template)
});
}
}
}
return baseItems;
}
const items: ContextMenuItem[] = [];
if (node.type === 'file') {
items.push({
label: '打开文件',
icon: <File size={16} />,
onClick: async () => {
try {
await TauriAPI.openFileWithSystemApp(node.path);
} catch (error) {
console.error('Failed to open file:', error);
}
}
});
if (fileActionRegistry) {
const handlers = fileActionRegistry.getHandlersForFile(node.path);
for (const handler of handlers) {
if (handler.getContextMenuItems) {
const parentPath = node.path.substring(0, node.path.lastIndexOf('/'));
const pluginItems = handler.getContextMenuItems(node.path, parentPath);
for (const pluginItem of pluginItems) {
items.push({
label: pluginItem.label,
icon: pluginItem.icon,
onClick: () => pluginItem.onClick(node.path, parentPath),
disabled: pluginItem.disabled,
separator: pluginItem.separator
});
}
}
}
}
}
items.push({
label: '重命名',
icon: <Edit3 size={16} />,
onClick: () => {
setRenamingNode(node.path);
setNewName(node.name);
}
});
items.push({
label: '删除',
icon: <Trash2 size={16} />,
onClick: () => handleDeleteClick(node)
});
items.push({ label: '', separator: true, onClick: () => {} });
if (node.type === 'folder') {
items.push({
label: '新建文件',
icon: <FileText size={16} />,
onClick: () => handleCreateFileClick(node.path)
});
items.push({
label: '新建文件夹',
icon: <FolderPlus size={16} />,
onClick: () => handleCreateFolderClick(node.path)
});
if (fileActionRegistry) {
const templates = fileActionRegistry.getCreationTemplates();
if (templates.length > 0) {
items.push({ label: '', separator: true, onClick: () => {} });
for (const template of templates) {
items.push({
label: template.label,
icon: template.icon,
onClick: () => handleCreateTemplateFileClick(node.path, template)
});
}
}
}
items.push({ label: '', separator: true, onClick: () => {} });
}
items.push({
label: '在文件管理器中显示',
icon: <FolderOpen size={16} />,
onClick: async () => {
try {
await TauriAPI.showInFolder(node.path);
} catch (error) {
console.error('Failed to show in folder:', error);
}
}
});
items.push({
label: '复制路径',
icon: <Copy size={16} />,
onClick: () => {
navigator.clipboard.writeText(node.path);
}
});
return items;
};
const handleNodeClick = (node: TreeNode) => {
if (node.type === 'folder') {
setInternalSelectedPath(node.path);
onSelectFile?.(node.path);
toggleNode(node.path);
} else {
setInternalSelectedPath(node.path);
const extension = node.name.includes('.') ? node.name.split('.').pop() : undefined;
messageHub?.publish('asset-file:selected', {
fileInfo: {
name: node.name,
path: node.path,
extension,
size: node.size,
modified: node.modified,
isDirectory: false
}
});
}
};
const handleNodeDoubleClick = async (node: TreeNode) => {
if (node.type === 'file') {
if (fileActionRegistry) {
const handled = await fileActionRegistry.handleDoubleClick(node.path);
if (handled) {
return;
}
}
try {
await TauriAPI.openFileWithSystemApp(node.path);
} catch (error) {
console.error('Failed to open file:', error);
}
}
};
const handleContextMenu = (e: React.MouseEvent, node: TreeNode | null) => {
e.preventDefault();
e.stopPropagation();
setContextMenu({
position: { x: e.clientX, y: e.clientY },
node
});
};
const renderNode = (node: TreeNode, level: number = 0) => {
const isSelected = (internalSelectedPath || selectedPath) === node.path;
const isRenaming = renamingNode === node.path;
const indent = level * 16;
return (
<div key={node.path}>
<div
className={`tree-node ${isSelected ? 'selected' : ''}`}
style={{ paddingLeft: `${indent}px`, cursor: node.type === 'file' ? 'grab' : 'pointer' }}
onClick={() => !isRenaming && handleNodeClick(node)}
onDoubleClick={() => !isRenaming && handleNodeDoubleClick(node)}
onContextMenu={(e) => handleContextMenu(e, node)}
draggable={node.type === 'file' && !isRenaming}
onDragStart={(e) => {
if (node.type === 'file' && !isRenaming) {
e.dataTransfer.effectAllowed = 'copy';
// 设置拖拽的数据
e.dataTransfer.setData('asset-path', node.path);
e.dataTransfer.setData('asset-name', node.name);
const ext = node.name.includes('.') ? node.name.split('.').pop() : '';
e.dataTransfer.setData('asset-extension', ext || '');
e.dataTransfer.setData('text/plain', node.path);
// 添加视觉反馈
e.currentTarget.style.opacity = '0.5';
}
}}
onDragEnd={(e) => {
// 恢复透明度
e.currentTarget.style.opacity = '1';
}}
>
<span className="tree-arrow">
{node.type === 'folder' ? (
node.expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />
) : (
<span style={{ width: '14px', display: 'inline-block' }} />
)}
</span>
<span className="tree-icon">
{node.type === 'folder' ? (
node.name.toLowerCase() === 'plugins' || node.name.toLowerCase() === '.ecs' ? (
<Folder size={16} className="system-folder-icon" style={{ color: '#42a5f5' }} />
) : (
<Folder size={16} style={{ color: '#ffa726' }} />
)
) : (
<File size={16} style={{ color: '#90caf9' }} />
)}
</span>
{isRenaming ? (
<input
type="text"
className="tree-rename-input"
value={newName}
onChange={(e) => setNewName(e.target.value)}
onBlur={() => handleRename(node)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleRename(node);
} else if (e.key === 'Escape') {
setRenamingNode(null);
}
}}
autoFocus
onClick={(e) => e.stopPropagation()}
/>
) : (
<span className="tree-label">{node.name}</span>
)}
</div>
{node.type === 'folder' && node.expanded && node.children && (
<div className="tree-children">
{node.children.map((child) => renderNode(child, level + 1))}
</div>
)}
</div>
);
};
2025-10-15 10:08:15 +08:00
if (loading) {
return <div className="file-tree loading">Loading...</div>;
}
2025-10-15 10:08:15 +08:00
if (!rootPath || tree.length === 0) {
return <div className="file-tree empty">No folders</div>;
}
2025-10-15 10:08:15 +08:00
return (
<>
<div
className="file-tree"
onContextMenu={(e) => {
const target = e.target as HTMLElement;
if (target.classList.contains('file-tree')) {
handleContextMenu(e, null);
}
}}
>
{filteredTree.map((node) => renderNode(node))}
</div>
{contextMenu && (
<ContextMenu
items={getContextMenuItems(contextMenu.node)}
position={contextMenu.position}
onClose={() => setContextMenu(null)}
/>
)}
{deleteDialog && (
<ConfirmDialog
title="确认删除"
message={
deleteDialog.node.type === 'folder'
? `确定要删除文件夹 "${deleteDialog.node.name}" 及其所有内容吗?\n此操作无法撤销。`
: `确定要删除文件 "${deleteDialog.node.name}" 吗?\n此操作无法撤销。`
}
confirmText="删除"
cancelText="取消"
onConfirm={handleDeleteConfirm}
onCancel={() => setDeleteDialog(null)}
/>
)}
{promptDialog && (
<PromptDialog
title={
promptDialog.type === 'create-file' ? '新建文件' :
promptDialog.type === 'create-folder' ? '新建文件夹' :
'新建文件'
}
message={
promptDialog.type === 'create-file' ? '请输入文件名:' :
promptDialog.type === 'create-folder' ? '请输入文件夹名:' :
`请输入文件名 (将自动添加 .${promptDialog.templateExtension} 扩展名):`
}
placeholder={
promptDialog.type === 'create-file' ? '例如: config.json' :
promptDialog.type === 'create-folder' ? '例如: assets' :
'例如: MyFile'
}
confirmText="创建"
cancelText="取消"
onConfirm={handlePromptConfirm}
onCancel={() => setPromptDialog(null)}
/>
)}
</>
);
});