import { useState, useEffect } from 'react'; import { X, Folder, File, Search, ArrowLeft, Grid, List, FileCode } from 'lucide-react'; import { TauriAPI, DirectoryEntry } from '../api/tauri'; import '../styles/AssetPickerDialog.css'; interface AssetPickerDialogProps { projectPath: string; fileExtension: string; onSelect: (assetId: string) => void; onClose: () => void; locale: string; /** 资产基础路径(相对于项目根目录),用于计算 assetId */ assetBasePath?: string; } interface AssetItem { name: string; path: string; isDir: boolean; extension?: string; size?: number; modified?: number; } type ViewMode = 'list' | 'grid'; export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClose, locale, assetBasePath }: AssetPickerDialogProps) { // 计算实际的资产目录路径 const actualAssetPath = assetBasePath ? `${projectPath}/${assetBasePath}`.replace(/\\/g, '/').replace(/\/+/g, '/') : projectPath; const [currentPath, setCurrentPath] = useState(actualAssetPath); const [assets, setAssets] = useState([]); const [selectedPath, setSelectedPath] = useState(null); const [loading, setLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [viewMode, setViewMode] = useState('list'); const translations = { en: { title: 'Select Asset', loading: 'Loading...', empty: 'No assets found', select: 'Select', cancel: 'Cancel', search: 'Search...', back: 'Back', listView: 'List View', gridView: 'Grid View' }, zh: { title: '选择资产', loading: '加载中...', empty: '没有找到资产', select: '选择', cancel: '取消', search: '搜索...', back: '返回上级', listView: '列表视图', gridView: '网格视图' } }; const t = translations[locale as keyof typeof translations] || translations.en; useEffect(() => { loadAssets(currentPath); }, [currentPath]); const loadAssets = async (path: string) => { setLoading(true); try { const entries = await TauriAPI.listDirectory(path); const assetItems: AssetItem[] = entries .map((entry: DirectoryEntry) => { const extension = entry.is_dir ? undefined : (entry.name.includes('.') ? entry.name.split('.').pop() : undefined); return { name: entry.name, path: entry.path, isDir: entry.is_dir, extension, size: entry.size, modified: entry.modified }; }) .filter(item => item.isDir || item.extension === fileExtension) .sort((a, b) => { if (a.isDir === b.isDir) return a.name.localeCompare(b.name); return a.isDir ? -1 : 1; }); setAssets(assetItems); } catch (error) { console.error('Failed to load assets:', error); setAssets([]); } finally { setLoading(false); } }; // 过滤搜索结果 const filteredAssets = assets.filter(item => item.name.toLowerCase().includes(searchQuery.toLowerCase()) ); // 格式化文件大小 const formatFileSize = (bytes?: number): string => { if (!bytes) return ''; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; // 格式化修改时间 const formatDate = (timestamp?: number): string => { if (!timestamp) return ''; const date = new Date(timestamp * 1000); return date.toLocaleDateString(locale === 'zh' ? 'zh-CN' : 'en-US', { year: 'numeric', month: 'short', day: 'numeric' }); }; // 返回上级目录 const handleGoBack = () => { const parentPath = currentPath.split(/[/\\]/).slice(0, -1).join('/'); const minPath = actualAssetPath.replace(/[/\\]$/, ''); if (parentPath && parentPath !== minPath) { setCurrentPath(parentPath); } else if (currentPath !== actualAssetPath) { setCurrentPath(actualAssetPath); } }; // 只能返回到资产基础目录,不能再往上 const canGoBack = currentPath !== actualAssetPath; const handleItemClick = (item: AssetItem) => { if (item.isDir) { setCurrentPath(item.path); } else { setSelectedPath(item.path); } }; const handleItemDoubleClick = (item: AssetItem) => { if (!item.isDir) { const assetId = calculateAssetId(item.path); onSelect(assetId); } }; const handleSelect = () => { if (selectedPath) { const assetId = calculateAssetId(selectedPath); onSelect(assetId); } }; /** * 计算资产ID * 将绝对路径转换为相对于资产基础目录的assetId(不含扩展名) */ const calculateAssetId = (absolutePath: string): string => { const normalized = absolutePath.replace(/\\/g, '/'); const baseNormalized = actualAssetPath.replace(/\\/g, '/'); // 获取相对于资产基础目录的路径 let relativePath = normalized; if (normalized.startsWith(baseNormalized)) { relativePath = normalized.substring(baseNormalized.length); } // 移除开头的斜杠 relativePath = relativePath.replace(/^\/+/, ''); // 移除文件扩展名 const assetId = relativePath.replace(new RegExp(`\\.${fileExtension}$`), ''); return assetId; }; const getBreadcrumbs = () => { const basePathNormalized = actualAssetPath.replace(/\\/g, '/'); const currentPathNormalized = currentPath.replace(/\\/g, '/'); const relative = currentPathNormalized.replace(basePathNormalized, ''); const parts = relative.split('/').filter(p => p); // 根路径名称(显示"行为树"或"Assets") const rootName = assetBasePath ? assetBasePath.split('/').pop() || 'Assets' : 'Content'; const crumbs = [{ name: rootName, path: actualAssetPath }]; let accPath = actualAssetPath; for (const part of parts) { accPath = `${accPath}/${part}`; crumbs.push({ name: part, path: accPath }); } return crumbs; }; const breadcrumbs = getBreadcrumbs(); return (
e.stopPropagation()}>

{t.title}

{breadcrumbs.map((crumb, index) => ( setCurrentPath(crumb.path)} > {crumb.name} {index < breadcrumbs.length - 1 && / } ))}
setSearchQuery(e.target.value)} className="search-input" /> {searchQuery && ( )}
{loading ? (
{t.loading}
) : filteredAssets.length === 0 ? (
{t.empty}
) : (
{filteredAssets.map((item, index) => (
handleItemClick(item)} onDoubleClick={() => handleItemDoubleClick(item)} >
{item.isDir ? ( ) : ( )}
{item.name} {viewMode === 'list' && !item.isDir && (
{item.size && {formatFileSize(item.size)}} {item.modified && {formatDate(item.modified)}}
)}
))}
)}
{filteredAssets.length} {locale === 'zh' ? '项' : 'items'}
); }