From d6e535aa986e4b76d971a53b872bc08d545dc3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Wed, 27 May 2026 13:56:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20UI=20=E7=BB=84=E4=BB=B6=E9=80=82?= =?UTF-8?q?=E9=85=8D=20PathEntry=20=E2=80=94=20=E5=A4=8D=E9=80=89=E6=A1=86?= =?UTF-8?q?=E5=88=97=E3=80=81=E7=A6=81=E7=94=A8=E8=A1=8C=E7=81=B0=E6=98=BE?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/components/path-list/MergePreview.tsx | 64 ++++++++++++++++------- src/components/path-list/PathTable.tsx | 50 +++++++++++++----- src/hooks/use-app-actions.ts | 15 +++--- 3 files changed, 89 insertions(+), 40 deletions(-) diff --git a/src/components/path-list/MergePreview.tsx b/src/components/path-list/MergePreview.tsx index f576f0c..d57d32a 100644 --- a/src/components/path-list/MergePreview.tsx +++ b/src/components/path-list/MergePreview.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import { useAppStore } from '@/store/app-store'; import { useTranslation } from 'react-i18next'; +import type { PathEntry } from '@/core/path-entry'; export function MergePreview() { const sysPaths = useAppStore((s) => s.sysPaths); @@ -9,13 +10,27 @@ export function MergePreview() { const { t } = useTranslation(); const allPaths = useMemo(() => { - const result: { path: string; source: string; index: number }[] = []; - sysPaths.forEach((p, i) => result.push({ path: p, source: t('merge.system'), index: i })); - userPaths.forEach((p, i) => result.push({ path: p, source: t('merge.user'), index: i })); + const seen = new Set(); + const merged: (PathEntry & { source: string; displayIndex: number })[] = []; - if (!searchQuery) return result; + for (const entry of sysPaths) { + const lower = entry.path.toLowerCase(); + if (!seen.has(lower)) { + seen.add(lower); + merged.push({ ...entry, source: t('merge.system'), displayIndex: merged.length }); + } + } + for (const entry of userPaths) { + const lower = entry.path.toLowerCase(); + if (!seen.has(lower)) { + seen.add(lower); + merged.push({ ...entry, source: t('merge.user'), displayIndex: merged.length }); + } + } + + if (!searchQuery) return merged; const q = searchQuery.toLowerCase(); - return result.filter((r) => r.path.toLowerCase().includes(q)); + return merged.filter((r) => r.path.toLowerCase().includes(q)); }, [sysPaths, userPaths, searchQuery, t]); return ( @@ -32,20 +47,31 @@ export function MergePreview() { - {allPaths.map(({ path, source, index }, rowIdx) => ( - - {rowIdx + 1} - {path} - {source} - - ))} + {allPaths.map(({ path, enabled, source, displayIndex }, rowIdx) => { + const textColor = enabled ? 'var(--app-fg)' : '#6b7280'; + const textDecoration = enabled ? 'none' : 'line-through'; + const opacity = enabled ? 1 : 0.6; + + return ( + + {rowIdx + 1} + + {path} + + {source} + + ); + })} diff --git a/src/components/path-list/PathTable.tsx b/src/components/path-list/PathTable.tsx index 5c96184..9303409 100644 --- a/src/components/path-list/PathTable.tsx +++ b/src/components/path-list/PathTable.tsx @@ -1,6 +1,7 @@ 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'; @@ -9,6 +10,7 @@ interface PathTableProps { interface PathRow { path: string; index: number; + enabled: boolean; } type ValidationState = 'valid' | 'invalid' | 'unknown'; @@ -35,12 +37,12 @@ export function PathTable({ tabId }: PathTableProps) { // 过滤搜索 const filtered = useMemo(() => { - if (!searchQuery) return paths.map((p, i) => ({ path: p, index: i })); + 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.toLowerCase().includes(q)) result.push({ path: p, index: i }); + if (p.path.toLowerCase().includes(q)) result.push({ path: p.path, index: i, enabled: p.enabled }); } return result; }, [paths, searchQuery]); @@ -48,18 +50,18 @@ export function PathTable({ tabId }: PathTableProps) { // 异步验证未缓存的路径 useEffect(() => { let cancelled = false; - const toValidate = paths.filter((p) => !validatedRef.current.has(p)); + 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.includes('%')) return [p, 'valid']; - const valid: boolean = await invoke('validate_path', { path: p }); - return [p, valid ? 'valid' : 'invalid']; + 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, 'unknown']; + return [p.path, 'unknown']; } }), ).then((results) => { @@ -79,7 +81,7 @@ export function PathTable({ tabId }: PathTableProps) { useEffect(() => { let cancelled = false; const toExpand = paths.filter( - (p) => p.includes('%') && !expandedRef.current.has(p), + (p) => p.path.includes('%') && !expandedRef.current.has(p.path), ); if (toExpand.length === 0) return; @@ -87,10 +89,10 @@ export function PathTable({ tabId }: PathTableProps) { Promise.all( batch.map(async (p): Promise<[string, string]> => { try { - const expanded: string = await invoke('expand_env_vars', { path: p }); - return [p, expanded !== p ? expanded : '']; + const expanded: string = await invoke('expand_env_vars', { path: p.path }); + return [p.path, expanded !== p.path ? expanded : '']; } catch { - return [p, '']; + return [p.path, '']; } }), ).then((results) => { @@ -141,7 +143,7 @@ export function PathTable({ tabId }: PathTableProps) { if (!isActive) return; window.dispatchEvent( new CustomEvent('path-dblclick', { - detail: { index: realIndex, path: paths[realIndex] }, + detail: { index: realIndex, path: paths[realIndex].path }, }), ); }, @@ -157,11 +159,12 @@ export function PathTable({ tabId }: PathTableProps) { style={{ backgroundColor: 'var(--app-list-alt)', color: 'var(--app-fg)' }} > # + 路径 - {filtered.map(({ path, index }, rowIdx) => { + {filtered.map(({ path, index, enabled }, rowIdx) => { const v = validations[rowIdx]; const isSelected = selectedIndices.includes(index); let textColor = 'var(--app-fg)'; @@ -169,6 +172,14 @@ export function PathTable({ tabId }: PathTableProps) { 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 ( {index + 1} + + { + const target = tabId === 'system' ? TargetType.SYSTEM : TargetType.USER; + useAppStore.getState().togglePath(index, target); + }} + className="cursor-pointer" + /> + {path} diff --git a/src/hooks/use-app-actions.ts b/src/hooks/use-app-actions.ts index 1541912..5330797 100644 --- a/src/hooks/use-app-actions.ts +++ b/src/hooks/use-app-actions.ts @@ -4,6 +4,7 @@ import { TargetType } from '@/core/undo-redo'; import { open } from '@tauri-apps/plugin-dialog'; import { invoke } from '@tauri-apps/api/core'; import { importFromContent, exportToJson, exportToCsv, flattenImportResult } from '@/core/import-export'; +import type { PathEntry } from '@/core/path-entry'; import { is_valid_path_format } from '@/core/validation'; import { useKeyboard } from './use-keyboard'; import i18n from '@/i18n'; @@ -13,7 +14,7 @@ export interface DialogState { editDialog: { open: boolean; index: number; value: string; target: TargetType }; newDialog: boolean; helpOpen: boolean; - importDialog: { open: boolean; system: string[]; user: string[] }; + importDialog: { open: boolean; system: PathEntry[]; user: PathEntry[] }; setEditDialog: (v: DialogState['editDialog']) => void; setNewDialog: (v: boolean) => void; setHelpOpen: (v: boolean) => void; @@ -38,8 +39,8 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) { const list = target === TargetType.SYSTEM ? useAppStore.getState().sysPaths : useAppStore.getState().userPaths; - const value = list[idx]; - if (value) setEditDialog({ open: true, index: idx, value, target }); + const entry = list[idx]; + if (entry) setEditDialog({ open: true, index: idx, value: entry.path, target }); }, [activeTab, setEditDialog]); const handleBrowse = useCallback(async () => { @@ -92,9 +93,9 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) { if (result.system.length > 0 && result.user.length > 0) { setImportDialog({ open: true, system: result.system, user: result.user }); } else if (result.system.length > 0) { - useAppStore.getState().replacePaths(TargetType.SYSTEM, result.system); + useAppStore.getState().replacePaths(TargetType.SYSTEM, result.system.map(e => e.path)); } else if (result.user.length > 0) { - useAppStore.getState().replacePaths(TargetType.USER, result.user); + useAppStore.getState().replacePaths(TargetType.USER, result.user.map(e => e.path)); } }, [setImportDialog]); @@ -159,8 +160,8 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) { const handleImportSelect = useCallback((target: 'system' | 'user' | 'both') => { const { system, user } = dialogs.importDialog; const flat = flattenImportResult({ system, user }, target); - if (flat.system.length > 0) useAppStore.getState().replacePaths(TargetType.SYSTEM, flat.system); - if (flat.user.length > 0) useAppStore.getState().replacePaths(TargetType.USER, flat.user); + if (flat.system.length > 0) useAppStore.getState().replacePaths(TargetType.SYSTEM, flat.system.map(e => e.path)); + if (flat.user.length > 0) useAppStore.getState().replacePaths(TargetType.USER, flat.user.map(e => e.path)); setImportDialog({ open: false, system: [], user: [] }); }, [dialogs.importDialog, setImportDialog]);