mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 01:37:22 +08:00
feat: UI 组件适配 PathEntry — 复选框列、禁用行灰显删除线
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string>();
|
||||
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() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allPaths.map(({ path, source, index }, rowIdx) => (
|
||||
<tr
|
||||
key={`${source}-${index}`}
|
||||
style={{
|
||||
backgroundColor:
|
||||
rowIdx % 2 === 0 ? 'var(--app-list-bg)' : 'var(--app-list-alt)',
|
||||
color: 'var(--app-fg)',
|
||||
}}
|
||||
>
|
||||
<td className="px-2 py-0.5 text-xs opacity-50">{rowIdx + 1}</td>
|
||||
<td className="px-2 py-0.5 text-sm">{path}</td>
|
||||
<td className="px-2 py-0.5 text-xs opacity-60">{source}</td>
|
||||
</tr>
|
||||
))}
|
||||
{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 (
|
||||
<tr
|
||||
key={`${source}-${displayIndex}`}
|
||||
style={{
|
||||
backgroundColor:
|
||||
rowIdx % 2 === 0 ? 'var(--app-list-bg)' : 'var(--app-list-alt)',
|
||||
color: 'var(--app-fg)',
|
||||
}}
|
||||
>
|
||||
<td className="px-2 py-0.5 text-xs opacity-50">{rowIdx + 1}</td>
|
||||
<td
|
||||
className="px-2 py-0.5 text-sm"
|
||||
style={{ color: textColor, textDecoration, opacity }}
|
||||
>
|
||||
{path}
|
||||
</td>
|
||||
<td className="px-2 py-0.5 text-xs opacity-60">{source}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -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<PathRow[]>(() => {
|
||||
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)' }}
|
||||
>
|
||||
<th className="w-8 px-2 py-1">#</th>
|
||||
<th className="w-6 px-1 py-1"></th>
|
||||
<th className="px-2 py-1">路径</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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 (
|
||||
<tr
|
||||
key={index}
|
||||
@@ -186,9 +197,20 @@ export function PathTable({ tabId }: PathTableProps) {
|
||||
<td className="w-8 px-2 py-0.5 text-xs opacity-50" style={{ color: 'var(--app-fg)' }}>
|
||||
{index + 1}
|
||||
</td>
|
||||
<td className="w-6 px-1 py-0.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
const target = tabId === 'system' ? TargetType.SYSTEM : TargetType.USER;
|
||||
useAppStore.getState().togglePath(index, target);
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
className="px-2 py-0.5 text-sm truncate max-w-2xl"
|
||||
style={{ color: textColor }}
|
||||
style={{ color: textColor, textDecoration, opacity }}
|
||||
title={expandedCache.get(path) || undefined}
|
||||
>
|
||||
{path}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user