refactor(RightClickRestorer): 用状态配置表简化页面 UI 结构

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
雨霖铃
2026-06-19 10:41:22 +08:00
parent 06b9d29270
commit 228cadf526
5 changed files with 102 additions and 72 deletions
@@ -8,8 +8,7 @@ vi.mock('../useRightClickRestorer', () => ({
useRightClickRestorer: () => ({ useRightClickRestorer: () => ({
domain: 'example.com', domain: 'example.com',
isLoading: false, isLoading: false,
isUnlocked: false, status: 'locked',
isUnsupported: false,
unlock: mockUnlock, unlock: mockUnlock,
}), }),
})); }));
@@ -26,8 +26,7 @@ describe('useRightClickRestorer', () => {
await waitFor(() => expect(result.current.isLoading).toBe(false)); await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.domain).toBe('example.com'); expect(result.current.domain).toBe('example.com');
expect(result.current.isUnlocked).toBe(false); expect(result.current.status).toBe('locked');
expect(result.current.isUnsupported).toBe(false);
expect(sendMessageToContent).toHaveBeenCalledWith('queryRightClickStatus'); expect(sendMessageToContent).toHaveBeenCalledWith('queryRightClickStatus');
}); });
@@ -39,8 +38,7 @@ describe('useRightClickRestorer', () => {
await waitFor(() => expect(result.current.isLoading).toBe(false)); await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.isUnsupported).toBe(true); expect(result.current.status).toBe('unsupported');
expect(result.current.isUnlocked).toBe(false);
expect(sendMessageToContent).not.toHaveBeenCalled(); expect(sendMessageToContent).not.toHaveBeenCalled();
}); });
@@ -56,7 +54,7 @@ describe('useRightClickRestorer', () => {
await result.current.unlock(); await result.current.unlock();
}); });
expect(result.current.isUnlocked).toBe(true); expect(result.current.status).toBe('unlocked');
expect(sendMessageToContent).toHaveBeenLastCalledWith('restoreRightClick'); expect(sendMessageToContent).toHaveBeenLastCalledWith('restoreRightClick');
}); });
@@ -72,7 +70,7 @@ describe('useRightClickRestorer', () => {
await result.current.unlock(); await result.current.unlock();
}); });
expect(result.current.isUnlocked).toBe(false); expect(result.current.status).toBe('unsupported');
expect(sendMessageToContent).not.toHaveBeenCalled(); expect(sendMessageToContent).not.toHaveBeenCalled();
}); });
@@ -87,6 +85,6 @@ describe('useRightClickRestorer', () => {
await result.current.unlock(); await result.current.unlock();
}); });
expect(result.current.isUnlocked).toBe(false); expect(result.current.status).toBe('locked');
}); });
}); });
+59
View File
@@ -0,0 +1,59 @@
import type { LucideIcon } from 'lucide-react';
import { AlertTriangle, MousePointerClick, Shield, ShieldCheck } from 'lucide-react';
export type RestorerStatus = 'unsupported' | 'locked' | 'unlocked';
export const CARD_CLASS =
'w-full p-4 rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden space-y-3';
export const LOADING_TEXT = '正在加载...';
export const DOMAIN_LABEL = '当前域名';
interface StatusConfig {
badgeVariant: 'default' | 'secondary' | 'destructive';
badgeClassName: string;
badgeIcon: LucideIcon;
badgeLabel: string;
description: string;
buttonIcon: LucideIcon;
buttonLabel: string;
buttonDisabled: boolean;
buttonVariant: 'default' | 'secondary';
}
export const STATUS_CONFIG: Record<RestorerStatus, StatusConfig> = {
unsupported: {
badgeVariant: 'destructive',
badgeClassName: 'gap-1 shrink-0',
badgeIcon: AlertTriangle,
badgeLabel: '不支持',
description: '当前页面为浏览器内部页面或扩展页面,无法解锁右键功能。请切换到普通网页后重试。',
buttonIcon: AlertTriangle,
buttonLabel: '不支持',
buttonDisabled: true,
buttonVariant: 'secondary',
},
locked: {
badgeVariant: 'secondary',
badgeClassName: 'gap-1 shrink-0',
badgeIcon: Shield,
badgeLabel: '未解锁',
description: '点击下方按钮,为当前网站临时解锁右键菜单。刷新页面后需要重新解锁。',
buttonIcon: MousePointerClick,
buttonLabel: '解锁当前网站右键',
buttonDisabled: false,
buttonVariant: 'default',
},
unlocked: {
badgeVariant: 'default',
badgeClassName: 'gap-1 bg-green-600 hover:bg-green-700 shrink-0',
badgeIcon: ShieldCheck,
badgeLabel: '已解锁',
description: '点击下方按钮,为当前网站临时解锁右键菜单。刷新页面后需要重新解锁。',
buttonIcon: MousePointerClick,
buttonLabel: '右键已解锁',
buttonDisabled: true,
buttonVariant: 'secondary',
},
};
+21 -51
View File
@@ -1,82 +1,52 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Shield, ShieldCheck, MousePointerClick, AlertTriangle } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { CARD_CLASS, DOMAIN_LABEL, LOADING_TEXT, STATUS_CONFIG } from './constants';
import { useRightClickRestorer } from './useRightClickRestorer'; import { useRightClickRestorer } from './useRightClickRestorer';
export default function Index() { export default function Index() {
const { domain, isLoading, isUnlocked, isUnsupported, unlock } = useRightClickRestorer(); const { domain, isLoading, status, unlock } = useRightClickRestorer();
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex flex-col items-center justify-center py-12 min-h-[280px] w-full"> <div className="flex flex-col items-center justify-center py-12 min-h-[280px] w-full">
<Loader2 className="h-6 w-6 text-muted-foreground/80 animate-spin" />
<span className="text-xs text-muted-foreground mt-2 font-medium tracking-wide"> <span className="text-xs text-muted-foreground mt-2 font-medium tracking-wide">
{'正在加载...'} {LOADING_TEXT}
</span> </span>
</div> </div>
); );
} }
const config = STATUS_CONFIG[status];
const BadgeIcon = config.badgeIcon;
const ButtonIcon = config.buttonIcon;
return ( return (
<div className="p-4 w-full flex flex-col space-y-4"> <div className="p-4 w-full flex flex-col space-y-4">
{/* Current Domain */} <div className={CARD_CLASS}>
<div className="w-full rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden"> <Label className="text-sm font-medium">{DOMAIN_LABEL}</Label>
<div className="p-4"> <div className="flex items-center justify-between gap-2">
<Label className="text-sm font-medium">{'当前域名'}</Label>
<div className="mt-2 flex items-center justify-between gap-2">
<code className="text-sm bg-muted px-2 py-1 rounded truncate min-w-0 flex-1"> <code className="text-sm bg-muted px-2 py-1 rounded truncate min-w-0 flex-1">
{domain || '—'} {domain || '—'}
</code> </code>
{isUnsupported ? ( <Badge variant={config.badgeVariant} className={config.badgeClassName}>
<Badge variant="destructive" className="gap-1 shrink-0"> <BadgeIcon className="h-3 w-3" />
<AlertTriangle className="h-3 w-3" /> {config.badgeLabel}
{'不支持'}
</Badge> </Badge>
) : isUnlocked ? (
<Badge variant="default" className="gap-1 bg-green-600 hover:bg-green-700 shrink-0">
<ShieldCheck className="h-3 w-3" />
{'已解锁'}
</Badge>
) : (
<Badge variant="secondary" className="gap-1 shrink-0">
<Shield className="h-3 w-3" />
{'未解锁'}
</Badge>
)}
</div>
</div>
</div> </div>
{/* Unlock Action */} <p className="text-xs text-muted-foreground">{config.description}</p>
<div className="w-full rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden">
<div className="p-4 space-y-3">
{isUnsupported ? (
<>
<p className="text-xs text-muted-foreground">
{'当前页面为浏览器内部页面或扩展页面,无法解锁右键功能。请切换到普通网页后重试。'}
</p>
<Button className="w-full gap-2" disabled variant="secondary">
<AlertTriangle className="h-4 w-4" />
{'不支持'}
</Button>
</>
) : (
<>
<p className="text-xs text-muted-foreground">
{'点击下方按钮,为当前网站临时解锁右键菜单。刷新页面后需要重新解锁。'}
</p>
<Button <Button
className="w-full gap-2" className="w-full gap-2"
onClick={() => void unlock()} disabled={config.buttonDisabled}
disabled={isUnlocked} variant={config.buttonVariant}
variant={isUnlocked ? 'secondary' : 'default'} onClick={config.buttonDisabled ? undefined : () => void unlock()}
> >
<MousePointerClick className="h-4 w-4" /> <ButtonIcon className="h-4 w-4" />
{isUnlocked ? '右键已解锁' : '解锁当前网站右键'} {config.buttonLabel}
</Button> </Button>
</>
)}
</div>
</div> </div>
</div> </div>
); );
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { MessageAction, sendMessageToContent } from '@/utils/messages'; import { MessageAction, sendMessageToContent } from '@/utils/messages';
import type { RestorerStatus } from './constants';
const UNSUPPORTED_PROTOCOLS = new Set([ const UNSUPPORTED_PROTOCOLS = new Set([
'chrome:', 'chrome:',
@@ -19,11 +20,15 @@ function isUnsupportedPage(url: string | undefined): boolean {
} }
} }
function deriveStatus(isUnsupported: boolean, isUnlocked: boolean): RestorerStatus {
if (isUnsupported) return 'unsupported';
return isUnlocked ? 'unlocked' : 'locked';
}
export interface UseRightClickRestorerReturn { export interface UseRightClickRestorerReturn {
domain: string; domain: string;
isLoading: boolean; isLoading: boolean;
isUnlocked: boolean; status: RestorerStatus;
isUnsupported: boolean;
unlock: () => Promise<void>; unlock: () => Promise<void>;
} }
@@ -76,8 +81,7 @@ export function useRightClickRestorer(): UseRightClickRestorerReturn {
return { return {
domain, domain,
isLoading, isLoading,
isUnlocked, status: deriveStatus(isUnsupported, isUnlocked),
isUnsupported,
unlock, unlock,
}; };
} }