import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { useAppStore } from '@/store/app-store'; import { invoke } from '@tauri-apps/api/core'; import { TargetType } from '@/core/undo-redo'; interface PathTableProps { tabId: 'system' | 'user'; } interface PathRow { path: string; index: number; enabled: boolean; } type ValidationState = 'valid' | 'invalid' | 'unknown'; const DEFAULT_VALIDATION_STATE: ValidationState = 'valid'; export function PathTable({ tabId }: PathTableProps) { const sysPaths = useAppStore((s) => s.sysPaths); const userPaths = useAppStore((s) => s.userPaths); const searchQuery = useAppStore((s) => s.searchQuery); const selectedIndices = useAppStore((s) => s.selectedIndices); const setSelectedIndices = useAppStore((s) => s.setSelectedIndices); const activeTab = useAppStore((s) => s.activeTab); const paths = tabId === 'system' ? sysPaths : userPaths; const isActive = activeTab === tabId; // 本次会话中已验证过的路径缓存(key=path, value=ValidationState) const [validationCache, setValidationCache] = useState>(new Map()); // 环境变量展开结果缓存(key=path, value=expanded) const [expandedCache, setExpandedCache] = useState>(new Map()); const validatedRef = useRef>(new Set()); const expandedRef = useRef>(new Set()); // 过滤搜索 const filtered = useMemo(() => { if (!searchQuery) return paths.map((p, i) => ({ path: p.path, index: i, enabled: p.enabled })); const q = searchQuery.toLowerCase(); const result: PathRow[] = []; for (let i = 0; i < paths.length; i++) { const p = paths[i]; if (p.path.toLowerCase().includes(q)) result.push({ path: p.path, index: i, enabled: p.enabled }); } return result; }, [paths, searchQuery]); // 异步验证未缓存的路径 useEffect(() => { let cancelled = false; const toValidate = paths.filter((p) => !validatedRef.current.has(p.path)); if (toValidate.length === 0) return; const batch = toValidate.slice(0, 20); Promise.all( batch.map(async (p): Promise<[string, ValidationState]> => { try { if (p.path.includes('%')) return [p.path, 'valid']; const valid: boolean = await invoke('validate_path', { path: p.path }); return [p.path, valid ? 'valid' : 'invalid']; } catch { return [p.path, 'unknown']; } }), ).then((results) => { if (cancelled) return; for (const [p] of results) validatedRef.current.add(p); setValidationCache((prev) => { const next = new Map(prev); for (const [p, v] of results) next.set(p, v); return next; }); }); return () => { cancelled = true; }; }, [paths]); // 异步展开环境变量(用于 tooltip) useEffect(() => { let cancelled = false; const toExpand = paths.filter( (p) => p.path.includes('%') && !expandedRef.current.has(p.path), ); if (toExpand.length === 0) return; const batch = toExpand.slice(0, 20); Promise.all( batch.map(async (p): Promise<[string, string]> => { try { const expanded: string = await invoke('expand_env_vars', { path: p.path }); return [p.path, expanded !== p.path ? expanded : '']; } catch { return [p.path, '']; } }), ).then((results) => { if (cancelled) return; for (const [p] of results) expandedRef.current.add(p); setExpandedCache((prev) => { const next = new Map(prev); for (const [p, v] of results) next.set(p, v); return next; }); }); return () => { cancelled = true; }; }, [paths]); // 所有路径默认有效(异步验证结果回来后再精确染色) const validations = useMemo(() => { const seen = new Set(); return filtered.map(({ path }) => { const lower = path.toLowerCase(); const isDuplicate = seen.has(lower); seen.add(lower); return { state: validationCache.get(path) ?? DEFAULT_VALIDATION_STATE, isDuplicate, isEnvVar: path.includes('%'), }; }); }, [filtered, validationCache]); const handleClick = useCallback( (realIndex: number, e: React.MouseEvent) => { if (!isActive) return; if (e.ctrlKey) { const next = selectedIndices.includes(realIndex) ? selectedIndices.filter((i) => i !== realIndex) : [...selectedIndices, realIndex]; setSelectedIndices(next); } else { setSelectedIndices([realIndex]); } }, [isActive, selectedIndices, setSelectedIndices], ); const handleDoubleClick = useCallback( (realIndex: number) => { if (!isActive) return; window.dispatchEvent( new CustomEvent('path-dblclick', { detail: { index: realIndex, path: paths[realIndex].path }, }), ); }, [isActive, paths], ); return (
{filtered.map(({ path, index, enabled }, rowIdx) => { const v = validations[rowIdx]; const isSelected = selectedIndices.includes(index); let textColor = 'var(--app-fg)'; if (v.state === 'invalid') textColor = '#dc3545'; else if (v.isDuplicate) textColor = '#fd7e14'; else if (v.state === 'unknown') textColor = 'var(--app-fg)'; let textDecoration = 'none'; let opacity = 1; if (!enabled) { textColor = '#6b7280'; textDecoration = 'line-through'; opacity = 0.6; } return ( handleClick(index, e)} onDoubleClick={() => handleDoubleClick(index)} className="cursor-pointer select-none" style={{ backgroundColor: isSelected ? 'rgba(59, 130, 246, 0.3)' : rowIdx % 2 === 0 ? 'var(--app-list-bg)' : 'var(--app-list-alt)', }} > ); })}
# 路径
{index + 1} { const target = tabId === 'system' ? TargetType.SYSTEM : TargetType.USER; useAppStore.getState().togglePath(index, target); }} className="cursor-pointer" /> {path}
); }