This commit is contained in:
ryan
2026-07-13 15:17:45 +08:00
parent 08e61ce833
commit e0eb4e5975
334 changed files with 17212 additions and 12321 deletions
+59 -46
View File
@@ -1,20 +1,26 @@
'use client'
'use client';
import {useEffect, useRef, useState} from 'react'
import {CheckCircle2, Loader2, ShieldAlert, ShieldCheck, ShieldQuestion} from 'lucide-react'
import {getCapToken} from '@/lib/cap-solver'
import { useEffect, useRef, useState } from 'react';
import {
CheckCircle2,
Loader2,
ShieldAlert,
ShieldCheck,
ShieldQuestion,
} from 'lucide-react';
import { getCapToken } from '@/lib/cap-solver';
type CapStatus = 'idle' | 'solving' | 'solved' | 'error'
type CapStatus = 'idle' | 'solving' | 'solved' | 'error';
interface CapWidgetProps {
/** Called with the one-time token once the challenge is solved */
onToken: (token: string) => void
onToken: (token: string) => void;
/** Called when the challenge fails or encounters an error */
onError?: (err: Error) => void
onError?: (err: Error) => void;
/** Whether to auto-start solving when the component mounts. Defaults to true. */
autoStart?: boolean
autoStart?: boolean;
/** PoW scope sent to the backend */
scope?: string
scope?: string;
}
/**
@@ -23,88 +29,95 @@ interface CapWidgetProps {
* autoStart=true(默认):挂载后立即在后台运行 PoW 求解,完成后回调 onToken。
* autoStart=false:显示「点击开始验证」按钮,用户手动触发后才开始求解。
*/
export function CapWidget({ onToken, onError, autoStart = true, scope = 'login' }: CapWidgetProps) {
const [status, setStatus] = useState<CapStatus>('idle')
const [errorMsg, setErrorMsg] = useState('')
const solving = useRef(false)
export function CapWidget({
onToken,
onError,
autoStart = true,
scope = 'login',
}: CapWidgetProps) {
const [status, setStatus] = useState<CapStatus>('idle');
const [errorMsg, setErrorMsg] = useState('');
const solving = useRef(false);
const solve = async () => {
if (solving.current) return
solving.current = true
setStatus('solving')
setErrorMsg('')
if (solving.current) return;
solving.current = true;
setStatus('solving');
setErrorMsg('');
try {
const token = await getCapToken(scope)
setStatus('solved')
onToken(token)
const token = await getCapToken(scope);
setStatus('solved');
onToken(token);
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err))
setStatus('error')
setErrorMsg(error.message)
onError?.(error)
const error = err instanceof Error ? err : new Error(String(err));
setStatus('error');
setErrorMsg(error.message);
onError?.(error);
} finally {
solving.current = false
solving.current = false;
}
}
};
// Auto-start on mount (only when autoStart is enabled)
useEffect(() => {
if (autoStart) {
void solve()
void solve();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}, []);
return (
<div className="flex items-center gap-2 rounded-lg border border-border/60 bg-muted/30 px-3 py-2 text-sm">
<div className='flex items-center gap-2 rounded-lg border border-border/60 bg-muted/30 px-3 py-2 text-sm'>
{/* idle + autoStart=false: 手动触发按钮 */}
{status === 'idle' && !autoStart && (
<button
type="button"
className="flex w-full items-center gap-2 text-left"
type='button'
className='flex w-full items-center gap-2 text-left'
onClick={() => void solve()}
>
<ShieldQuestion className="size-4 shrink-0 text-muted-foreground" />
<span className="flex-1 text-muted-foreground">点击开始人机验证</span>
<ShieldQuestion className='size-4 shrink-0 text-muted-foreground' />
<span className='flex-1 text-muted-foreground'>点击开始人机验证</span>
</button>
)}
{/* idle + autoStart=true: 等待自动开始(极短暂状态) */}
{status === 'idle' && autoStart && (
<>
<ShieldAlert className="size-4 shrink-0 text-muted-foreground" />
<span className="text-muted-foreground">等待人机验证…</span>
<ShieldAlert className='size-4 shrink-0 text-muted-foreground' />
<span className='text-muted-foreground'>等待人机验证…</span>
</>
)}
{status === 'solving' && (
<>
<Loader2 className="size-4 shrink-0 animate-spin text-primary" />
<span className="text-muted-foreground">正在完成人机验证…</span>
<Loader2 className='size-4 shrink-0 animate-spin text-primary' />
<span className='text-muted-foreground'>正在完成人机验证…</span>
</>
)}
{status === 'solved' && (
<>
<CheckCircle2 className="size-4 shrink-0 text-green-500" />
<span className="text-green-600 dark:text-green-400">人机验证通过</span>
<ShieldCheck className="ml-auto size-4 shrink-0 text-green-500" />
<CheckCircle2 className='size-4 shrink-0 text-green-500' />
<span className='text-green-600 dark:text-green-400'>
人机验证通过
</span>
<ShieldCheck className='ml-auto size-4 shrink-0 text-green-500' />
</>
)}
{status === 'error' && (
<button
type="button"
className="flex w-full items-center gap-2 text-left"
type='button'
className='flex w-full items-center gap-2 text-left'
onClick={() => void solve()}
>
<ShieldAlert className="size-4 shrink-0 text-destructive" />
<span className="flex-1 text-destructive">
<ShieldAlert className='size-4 shrink-0 text-destructive' />
<span className='flex-1 text-destructive'>
{errorMsg || '人机验证失败'}
</span>
<span className="text-xs text-muted-foreground underline">重试</span>
<span className='text-xs text-muted-foreground underline'>重试</span>
</button>
)}
</div>
)
);
}