mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 01:45:54 +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 { useMemo } from 'react';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { PathEntry } from '@/core/path-entry';
|
||||||
|
|
||||||
export function MergePreview() {
|
export function MergePreview() {
|
||||||
const sysPaths = useAppStore((s) => s.sysPaths);
|
const sysPaths = useAppStore((s) => s.sysPaths);
|
||||||
@@ -9,13 +10,27 @@ export function MergePreview() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const allPaths = useMemo(() => {
|
const allPaths = useMemo(() => {
|
||||||
const result: { path: string; source: string; index: number }[] = [];
|
const seen = new Set<string>();
|
||||||
sysPaths.forEach((p, i) => result.push({ path: p, source: t('merge.system'), index: i }));
|
const merged: (PathEntry & { source: string; displayIndex: number })[] = [];
|
||||||
userPaths.forEach((p, i) => result.push({ path: p, source: t('merge.user'), index: i }));
|
|
||||||
|
|
||||||
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();
|
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]);
|
}, [sysPaths, userPaths, searchQuery, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -32,9 +47,14 @@ export function MergePreview() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{allPaths.map(({ path, source, index }, rowIdx) => (
|
{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
|
<tr
|
||||||
key={`${source}-${index}`}
|
key={`${source}-${displayIndex}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
rowIdx % 2 === 0 ? 'var(--app-list-bg)' : 'var(--app-list-alt)',
|
rowIdx % 2 === 0 ? 'var(--app-list-bg)' : 'var(--app-list-alt)',
|
||||||
@@ -42,10 +62,16 @@ export function MergePreview() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<td className="px-2 py-0.5 text-xs opacity-50">{rowIdx + 1}</td>
|
<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-sm"
|
||||||
|
style={{ color: textColor, textDecoration, opacity }}
|
||||||
|
>
|
||||||
|
{path}
|
||||||
|
</td>
|
||||||
<td className="px-2 py-0.5 text-xs opacity-60">{source}</td>
|
<td className="px-2 py-0.5 text-xs opacity-60">{source}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { TargetType } from '@/core/undo-redo';
|
||||||
|
|
||||||
interface PathTableProps {
|
interface PathTableProps {
|
||||||
tabId: 'system' | 'user';
|
tabId: 'system' | 'user';
|
||||||
@@ -9,6 +10,7 @@ interface PathTableProps {
|
|||||||
interface PathRow {
|
interface PathRow {
|
||||||
path: string;
|
path: string;
|
||||||
index: number;
|
index: number;
|
||||||
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ValidationState = 'valid' | 'invalid' | 'unknown';
|
type ValidationState = 'valid' | 'invalid' | 'unknown';
|
||||||
@@ -35,12 +37,12 @@ export function PathTable({ tabId }: PathTableProps) {
|
|||||||
|
|
||||||
// 过滤搜索
|
// 过滤搜索
|
||||||
const filtered = useMemo<PathRow[]>(() => {
|
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 q = searchQuery.toLowerCase();
|
||||||
const result: PathRow[] = [];
|
const result: PathRow[] = [];
|
||||||
for (let i = 0; i < paths.length; i++) {
|
for (let i = 0; i < paths.length; i++) {
|
||||||
const p = paths[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;
|
return result;
|
||||||
}, [paths, searchQuery]);
|
}, [paths, searchQuery]);
|
||||||
@@ -48,18 +50,18 @@ export function PathTable({ tabId }: PathTableProps) {
|
|||||||
// 异步验证未缓存的路径
|
// 异步验证未缓存的路径
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
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;
|
if (toValidate.length === 0) return;
|
||||||
|
|
||||||
const batch = toValidate.slice(0, 20);
|
const batch = toValidate.slice(0, 20);
|
||||||
Promise.all(
|
Promise.all(
|
||||||
batch.map(async (p): Promise<[string, ValidationState]> => {
|
batch.map(async (p): Promise<[string, ValidationState]> => {
|
||||||
try {
|
try {
|
||||||
if (p.includes('%')) return [p, 'valid'];
|
if (p.path.includes('%')) return [p.path, 'valid'];
|
||||||
const valid: boolean = await invoke('validate_path', { path: p });
|
const valid: boolean = await invoke('validate_path', { path: p.path });
|
||||||
return [p, valid ? 'valid' : 'invalid'];
|
return [p.path, valid ? 'valid' : 'invalid'];
|
||||||
} catch {
|
} catch {
|
||||||
return [p, 'unknown'];
|
return [p.path, 'unknown'];
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).then((results) => {
|
).then((results) => {
|
||||||
@@ -79,7 +81,7 @@ export function PathTable({ tabId }: PathTableProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const toExpand = paths.filter(
|
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;
|
if (toExpand.length === 0) return;
|
||||||
|
|
||||||
@@ -87,10 +89,10 @@ export function PathTable({ tabId }: PathTableProps) {
|
|||||||
Promise.all(
|
Promise.all(
|
||||||
batch.map(async (p): Promise<[string, string]> => {
|
batch.map(async (p): Promise<[string, string]> => {
|
||||||
try {
|
try {
|
||||||
const expanded: string = await invoke('expand_env_vars', { path: p });
|
const expanded: string = await invoke('expand_env_vars', { path: p.path });
|
||||||
return [p, expanded !== p ? expanded : ''];
|
return [p.path, expanded !== p.path ? expanded : ''];
|
||||||
} catch {
|
} catch {
|
||||||
return [p, ''];
|
return [p.path, ''];
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).then((results) => {
|
).then((results) => {
|
||||||
@@ -141,7 +143,7 @@ export function PathTable({ tabId }: PathTableProps) {
|
|||||||
if (!isActive) return;
|
if (!isActive) return;
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('path-dblclick', {
|
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)' }}
|
style={{ backgroundColor: 'var(--app-list-alt)', color: 'var(--app-fg)' }}
|
||||||
>
|
>
|
||||||
<th className="w-8 px-2 py-1">#</th>
|
<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>
|
<th className="px-2 py-1">路径</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filtered.map(({ path, index }, rowIdx) => {
|
{filtered.map(({ path, index, enabled }, rowIdx) => {
|
||||||
const v = validations[rowIdx];
|
const v = validations[rowIdx];
|
||||||
const isSelected = selectedIndices.includes(index);
|
const isSelected = selectedIndices.includes(index);
|
||||||
let textColor = 'var(--app-fg)';
|
let textColor = 'var(--app-fg)';
|
||||||
@@ -169,6 +172,14 @@ export function PathTable({ tabId }: PathTableProps) {
|
|||||||
else if (v.isDuplicate) textColor = '#fd7e14';
|
else if (v.isDuplicate) textColor = '#fd7e14';
|
||||||
else if (v.state === 'unknown') textColor = 'var(--app-fg)';
|
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 (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
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)' }}>
|
<td className="w-8 px-2 py-0.5 text-xs opacity-50" style={{ color: 'var(--app-fg)' }}>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</td>
|
</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
|
<td
|
||||||
className="px-2 py-0.5 text-sm truncate max-w-2xl"
|
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}
|
title={expandedCache.get(path) || undefined}
|
||||||
>
|
>
|
||||||
{path}
|
{path}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { TargetType } from '@/core/undo-redo';
|
|||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { importFromContent, exportToJson, exportToCsv, flattenImportResult } from '@/core/import-export';
|
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 { is_valid_path_format } from '@/core/validation';
|
||||||
import { useKeyboard } from './use-keyboard';
|
import { useKeyboard } from './use-keyboard';
|
||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
@@ -13,7 +14,7 @@ export interface DialogState {
|
|||||||
editDialog: { open: boolean; index: number; value: string; target: TargetType };
|
editDialog: { open: boolean; index: number; value: string; target: TargetType };
|
||||||
newDialog: boolean;
|
newDialog: boolean;
|
||||||
helpOpen: boolean;
|
helpOpen: boolean;
|
||||||
importDialog: { open: boolean; system: string[]; user: string[] };
|
importDialog: { open: boolean; system: PathEntry[]; user: PathEntry[] };
|
||||||
setEditDialog: (v: DialogState['editDialog']) => void;
|
setEditDialog: (v: DialogState['editDialog']) => void;
|
||||||
setNewDialog: (v: boolean) => void;
|
setNewDialog: (v: boolean) => void;
|
||||||
setHelpOpen: (v: boolean) => void;
|
setHelpOpen: (v: boolean) => void;
|
||||||
@@ -38,8 +39,8 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
|||||||
const list = target === TargetType.SYSTEM
|
const list = target === TargetType.SYSTEM
|
||||||
? useAppStore.getState().sysPaths
|
? useAppStore.getState().sysPaths
|
||||||
: useAppStore.getState().userPaths;
|
: useAppStore.getState().userPaths;
|
||||||
const value = list[idx];
|
const entry = list[idx];
|
||||||
if (value) setEditDialog({ open: true, index: idx, value, target });
|
if (entry) setEditDialog({ open: true, index: idx, value: entry.path, target });
|
||||||
}, [activeTab, setEditDialog]);
|
}, [activeTab, setEditDialog]);
|
||||||
|
|
||||||
const handleBrowse = useCallback(async () => {
|
const handleBrowse = useCallback(async () => {
|
||||||
@@ -92,9 +93,9 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
|||||||
if (result.system.length > 0 && result.user.length > 0) {
|
if (result.system.length > 0 && result.user.length > 0) {
|
||||||
setImportDialog({ open: true, system: result.system, user: result.user });
|
setImportDialog({ open: true, system: result.system, user: result.user });
|
||||||
} else if (result.system.length > 0) {
|
} 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) {
|
} 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]);
|
}, [setImportDialog]);
|
||||||
|
|
||||||
@@ -159,8 +160,8 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
|||||||
const handleImportSelect = useCallback((target: 'system' | 'user' | 'both') => {
|
const handleImportSelect = useCallback((target: 'system' | 'user' | 'both') => {
|
||||||
const { system, user } = dialogs.importDialog;
|
const { system, user } = dialogs.importDialog;
|
||||||
const flat = flattenImportResult({ system, user }, target);
|
const flat = flattenImportResult({ system, user }, target);
|
||||||
if (flat.system.length > 0) useAppStore.getState().replacePaths(TargetType.SYSTEM, flat.system);
|
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);
|
if (flat.user.length > 0) useAppStore.getState().replacePaths(TargetType.USER, flat.user.map(e => e.path));
|
||||||
setImportDialog({ open: false, system: [], user: [] });
|
setImportDialog({ open: false, system: [], user: [] });
|
||||||
}, [dialogs.importDialog, setImportDialog]);
|
}, [dialogs.importDialog, setImportDialog]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user