Files
WeFlow/src/pages/WelcomePage.tsx

852 lines
32 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAppStore } from '../stores/appStore'
import { dialog } from '../services/ipc'
import * as configService from '../services/config'
import {
ArrowLeft, ArrowRight, CheckCircle2, Database, Eye, EyeOff,
FolderOpen, FolderSearch, KeyRound, ShieldCheck, Sparkles,
UserRound, Wand2, Minus, X, HardDrive, RotateCcw
} from 'lucide-react'
import './WelcomePage.scss'
const steps = [
{ id: 'intro', title: '欢迎', desc: '准备开始你的本地数据探索' },
{ id: 'db', title: '数据库目录', desc: '定位 xwechat_files 目录' },
{ id: 'cache', title: '缓存目录', desc: '设置本地缓存存储位置(可选)' },
{ id: 'key', title: '解密密钥', desc: '获取密钥与自动识别账号' },
{ id: 'image', title: '图片密钥', desc: '获取 XOR 与 AES 密钥' },
{ id: 'security', title: '安全防护', desc: '保护你的数据' }
]
interface WelcomePageProps {
standalone?: boolean
}
const formatDbKeyFailureMessage = (error?: string, logs?: string[]): string => {
const base = String(error || '自动获取密钥失败').trim()
const tailLogs = Array.isArray(logs)
? logs
.map(item => String(item || '').trim())
.filter(Boolean)
.slice(-6)
: []
if (tailLogs.length === 0) return base
return `${base};最近状态:${tailLogs.join(' | ')}`
}
function WelcomePage({ standalone = false }: WelcomePageProps) {
const navigate = useNavigate()
const { isDbConnected, setDbConnected, setLoading } = useAppStore()
const [stepIndex, setStepIndex] = useState(0)
const [dbPath, setDbPath] = useState('')
const [decryptKey, setDecryptKey] = useState('')
const [imageXorKey, setImageXorKey] = useState('')
const [imageAesKey, setImageAesKey] = useState('')
const [cachePath, setCachePath] = useState('')
const [wxid, setWxid] = useState('')
const [wxidOptions, setWxidOptions] = useState<Array<{ wxid: string; modifiedTime: number }>>([])
const [showWxidSelect, setShowWxidSelect] = useState(false)
const wxidSelectRef = useRef<HTMLDivElement>(null)
const [error, setError] = useState('')
const [isConnecting, setIsConnecting] = useState(false)
const [isDetectingPath, setIsDetectingPath] = useState(false)
const [isScanningWxid, setIsScanningWxid] = useState(false)
const [isFetchingDbKey, setIsFetchingDbKey] = useState(false)
const [isFetchingImageKey, setIsFetchingImageKey] = useState(false)
const [showDecryptKey, setShowDecryptKey] = useState(false)
const [isClosing, setIsClosing] = useState(false)
const [dbKeyStatus, setDbKeyStatus] = useState('')
const [imageKeyStatus, setImageKeyStatus] = useState('')
const [isManualStartPrompt, setIsManualStartPrompt] = useState(false)
const [imageKeyPercent, setImageKeyPercent] = useState<number | null>(null)
// 安全相关 state
const [enableAuth, setEnableAuth] = useState(false)
const [authPassword, setAuthPassword] = useState('')
const [authConfirmPassword, setAuthConfirmPassword] = useState('')
const [enableHello, setEnableHello] = useState(false)
const [helloAvailable, setHelloAvailable] = useState(false)
const [isSettingHello, setIsSettingHello] = useState(false)
// 检查 Hello 可用性
useEffect(() => {
if (window.PublicKeyCredential) {
void PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().then(setHelloAvailable)
}
}, [])
async function sha256(message: string) {
const msgBuffer = new TextEncoder().encode(message)
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
return hashHex
}
const handleSetupHello = async () => {
setIsSettingHello(true)
try {
// 注册凭证 (WebAuthn)
const challenge = new Uint8Array(32)
window.crypto.getRandomValues(challenge)
const credential = await navigator.credentials.create({
publicKey: {
challenge,
rp: { name: 'WeFlow', id: 'localhost' },
user: {
id: new Uint8Array([1]),
name: 'user',
displayName: 'User'
},
pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
authenticatorSelection: { userVerification: 'required' },
timeout: 60000
}
})
if (credential) {
setEnableHello(true)
// 成功提示?
}
} catch (e: any) {
if (e.name !== 'NotAllowedError') {
setError('Windows Hello 设置失败: ' + e.message)
}
} finally {
setIsSettingHello(false)
}
}
useEffect(() => {
const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => {
setDbKeyStatus(payload.message)
})
const removeImage = window.electronAPI.key.onImageKeyStatus((payload: { message: string, percent?: number }) => {
let msg = payload.message;
let pct = payload.percent;
// 解析文本中的百分比
if (pct === undefined) {
const match = msg.match(/\(([\d.]+)%\)/);
if (match) {
pct = parseFloat(match[1]);
msg = msg.replace(/\s*\([\d.]+%\)/, '');
}
}
setImageKeyStatus(msg);
if (pct !== undefined) {
setImageKeyPercent(pct);
} else if (msg.includes('启动多核') || msg.includes('定位') || msg.includes('准备')) {
setImageKeyPercent(0);
}
})
return () => {
removeDb?.()
removeImage?.()
}
}, [])
useEffect(() => {
if (isDbConnected && !standalone) {
navigate('/home')
}
}, [isDbConnected, standalone, navigate])
useEffect(() => {
setWxidOptions([])
setWxid('')
setShowWxidSelect(false)
}, [dbPath])
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (!showWxidSelect) return
const target = event.target as Node
if (wxidSelectRef.current && !wxidSelectRef.current.contains(target)) {
setShowWxidSelect(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [showWxidSelect])
const currentStep = steps[stepIndex]
const rootClassName = `welcome-page${isClosing ? ' is-closing' : ''}${standalone ? ' is-standalone' : ''}`
const showWindowControls = standalone
const handleMinimize = () => {
window.electronAPI.window.minimize()
}
const handleCloseWindow = () => {
window.electronAPI.window.close()
}
const handleSelectPath = async () => {
try {
const result = await dialog.openFile({
title: '选择微信数据库目录',
properties: ['openDirectory']
})
if (!result.canceled && result.filePaths.length > 0) {
setDbPath(result.filePaths[0])
setError('')
}
} catch (e) {
setError('选择目录失败')
}
}
const handleAutoDetectPath = async () => {
if (isDetectingPath) return
setIsDetectingPath(true)
setError('')
try {
const result = await window.electronAPI.dbPath.autoDetect()
if (result.success && result.path) {
setDbPath(result.path)
setError('')
} else {
setError(result.error || '未能检测到数据库目录')
}
} catch (e) {
setError(`自动检测失败: ${e}`)
} finally {
setIsDetectingPath(false)
}
}
const handleSelectCachePath = async () => {
try {
const result = await dialog.openFile({
title: '选择缓存目录',
properties: ['openDirectory']
})
if (!result.canceled && result.filePaths.length > 0) {
setCachePath(result.filePaths[0])
setError('')
}
} catch (e) {
setError('选择缓存目录失败')
}
}
const handleScanWxid = async (silent = false) => {
if (!dbPath) {
if (!silent) setError('请先选择数据库目录')
return
}
if (isScanningWxid) return
setIsScanningWxid(true)
if (!silent) setError('')
try {
const wxids = await window.electronAPI.dbPath.scanWxids(dbPath)
setWxidOptions(wxids)
if (wxids.length > 0) {
// scanWxids 已经按时间排过序了,直接取第一个
setWxid(wxids[0].wxid)
if (!silent) setError('')
} else {
if (!silent) setError('未检测到账号目录,请检查路径')
}
} catch (e) {
if (!silent) setError(`扫描失败: ${e}`)
} finally {
setIsScanningWxid(false)
}
}
const handleScanWxidCandidates = async () => {
if (!dbPath) {
setError('请先选择数据库目录')
return
}
if (isScanningWxid) return
setIsScanningWxid(true)
setError('')
try {
const wxids = await window.electronAPI.dbPath.scanWxidCandidates(dbPath)
setWxidOptions(wxids)
setShowWxidSelect(true)
if (!wxids.length) {
setError('未检测到可用的账号目录,请检查路径')
}
} catch (e) {
setError(`扫描失败: ${e}`)
} finally {
setIsScanningWxid(false)
}
}
const handleAutoGetDbKey = async () => {
if (isFetchingDbKey) return
setIsFetchingDbKey(true)
setError('')
setIsManualStartPrompt(false)
setDbKeyStatus('正在连接微信进程...')
try {
const result = await window.electronAPI.key.autoGetDbKey()
if (result.success && result.key) {
setDecryptKey(result.key)
setDbKeyStatus('密钥获取成功')
setError('')
// 获取成功后自动扫描并填入 wxid
await handleScanWxid(true)
} else {
if (result.error?.includes('未找到微信安装路径') || result.error?.includes('启动微信失败')) {
setIsManualStartPrompt(true)
setDbKeyStatus('需要手动启动微信')
} else {
if (result.error?.includes('尚未完成登录')) {
setDbKeyStatus('请先在微信完成登录后重试')
}
setError(formatDbKeyFailureMessage(result.error, result.logs))
}
}
} catch (e) {
setError(`自动获取密钥失败: ${e}`)
} finally {
setIsFetchingDbKey(false)
}
}
const handleManualConfirm = async () => {
setIsManualStartPrompt(false)
handleAutoGetDbKey()
}
const handleAutoGetImageKey = async () => {
if (isFetchingImageKey) return
if (!dbPath) { setError('请先选择数据库目录'); return }
setIsFetchingImageKey(true)
setError('')
setImageKeyPercent(0)
setImageKeyStatus('正在准备获取图片密钥...')
try {
const accountPath = wxid ? `${dbPath}/${wxid}` : dbPath
const result = await window.electronAPI.key.autoGetImageKey(accountPath, wxid)
if (result.success && result.aesKey) {
if (typeof result.xorKey === 'number') setImageXorKey(`0x${result.xorKey.toString(16).toUpperCase().padStart(2, '0')}`)
setImageAesKey(result.aesKey)
setImageKeyStatus('已获取图片密钥')
} else {
setError(result.error || '自动获取图片密钥失败')
}
} catch (e) {
setError(`自动获取图片密钥失败: ${e}`)
} finally {
setIsFetchingImageKey(false)
}
}
const handleScanImageKeyFromMemory = async () => {
if (isFetchingImageKey) return
if (!dbPath) { setError('请先选择数据库目录'); return }
setIsFetchingImageKey(true)
setError('')
setImageKeyPercent(0)
setImageKeyStatus('正在扫描内存...')
try {
const accountPath = wxid ? `${dbPath}/${wxid}` : dbPath
const result = await window.electronAPI.key.scanImageKeyFromMemory(accountPath)
if (result.success && result.aesKey) {
if (typeof result.xorKey === 'number') setImageXorKey(`0x${result.xorKey.toString(16).toUpperCase().padStart(2, '0')}`)
setImageAesKey(result.aesKey)
setImageKeyStatus('内存扫描成功,已获取图片密钥')
} else {
setError(result.error || '内存扫描获取图片密钥失败')
}
} catch (e) {
setError(`内存扫描失败: ${e}`)
} finally {
setIsFetchingImageKey(false)
}
}
const canGoNext = () => {
if (currentStep.id === 'intro') return true
if (currentStep.id === 'db') return Boolean(dbPath)
if (currentStep.id === 'cache') return true
if (currentStep.id === 'key') return decryptKey.length === 64 && Boolean(wxid)
if (currentStep.id === 'image') return true
if (currentStep.id === 'security') {
if (enableAuth) {
return authPassword.length > 0 && authPassword === authConfirmPassword
}
return true
}
return false
}
const handleNext = () => {
if (!canGoNext()) {
if (currentStep.id === 'db' && !dbPath) setError('请先选择数据库目录')
if (currentStep.id === 'key') {
if (decryptKey.length !== 64) setError('密钥长度必须为 64 个字符')
else if (!wxid) setError('未能自动识别 wxid请尝试重新获取或检查目录')
}
return
}
setError('')
setStepIndex((prev) => Math.min(prev + 1, steps.length - 1))
}
const handleBack = () => {
setError('')
setStepIndex((prev) => Math.max(prev - 1, 0))
}
const handleConnect = async () => {
if (!dbPath) { setError('请先选择数据库目录'); return }
if (!wxid) { setError('请填写微信ID'); return }
if (!decryptKey || decryptKey.length !== 64) { setError('请填写 64 位解密密钥'); return }
setIsConnecting(true)
setError('')
setLoading(true, '正在连接数据库...')
try {
const result = await window.electronAPI.wcdb.testConnection(dbPath, decryptKey, wxid)
if (!result.success) {
setError(result.error || 'WCDB 连接失败')
setLoading(false)
return
}
await configService.setDbPath(dbPath)
await configService.setDecryptKey(decryptKey)
await configService.setMyWxid(wxid)
await configService.setCachePath(cachePath)
const parsedXorKey = imageXorKey ? parseInt(imageXorKey.replace(/^0x/i, ''), 16) : null
await configService.setImageXorKey(typeof parsedXorKey === 'number' && !Number.isNaN(parsedXorKey) ? parsedXorKey : 0)
await configService.setImageAesKey(imageAesKey || '')
await configService.setWxidConfig(wxid, {
decryptKey,
imageXorKey: typeof parsedXorKey === 'number' && !Number.isNaN(parsedXorKey) ? parsedXorKey : 0,
imageAesKey
})
// 保存安全配置
if (enableAuth && authPassword) {
const hash = await sha256(authPassword)
await configService.setAuthEnabled(true)
await configService.setAuthPassword(hash)
await configService.setAuthUseHello(enableHello)
}
await configService.setOnboardingDone(true)
setDbConnected(true, dbPath)
setLoading(false)
if (standalone) {
setIsClosing(true)
setTimeout(() => {
window.electronAPI.window.completeOnboarding()
}, 450)
} else {
navigate('/home')
}
} catch (e) {
setError(`连接失败: ${e}`)
setLoading(false)
} finally {
setIsConnecting(false)
}
}
const formatModifiedTime = (time: number) => {
if (!time) return '未知时间'
const date = new Date(time)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
if (isDbConnected) {
return (
<div className={rootClassName}>
<div className="welcome-container">
{showWindowControls && (
<div className="window-controls">
<button type="button" className="window-btn" onClick={handleMinimize} aria-label="最小化">
<Minus size={14} />
</button>
<button type="button" className="window-btn is-close" onClick={handleCloseWindow} aria-label="关闭">
<X size={14} />
</button>
</div>
)}
<div className="welcome-sidebar">
<div className="sidebar-header">
<img src="./logo.png" alt="WeFlow" className="sidebar-logo" />
<div className="sidebar-brand">
<span className="brand-name">WeFlow</span>
<span className="brand-tag">Connected</span>
</div>
</div>
<div className="sidebar-spacer" style={{ flex: 1 }} />
<div className="sidebar-footer">
<ShieldCheck size={14} />
<span></span>
</div>
</div>
<div className="welcome-content success-content">
<div className="success-body">
<div className="success-icon">
<CheckCircle2 size={48} />
</div>
<h1 className="success-title"></h1>
<p className="success-desc">使</p>
<button
className="btn btn-primary btn-large"
onClick={() => {
if (standalone) {
setIsClosing(true)
setTimeout(() => {
window.electronAPI.window.completeOnboarding()
}, 450)
} else {
navigate('/home')
}
}}
>
<ArrowRight size={18} />
</button>
</div>
</div>
</div>
</div>
)
}
return (
<div className={rootClassName}>
<div className="welcome-container">
{showWindowControls && (
<div className="window-controls">
<button type="button" className="window-btn" onClick={handleMinimize} aria-label="最小化">
<Minus size={14} />
</button>
<button type="button" className="window-btn is-close" onClick={handleCloseWindow} aria-label="关闭">
<X size={14} />
</button>
</div>
)}
<div className="welcome-sidebar">
<div className="sidebar-header">
<img src="./logo.png" alt="WeFlow" className="sidebar-logo" />
<div className="sidebar-brand">
<span className="brand-name">WeFlow</span>
<span className="brand-tag">Setup</span>
</div>
</div>
<div className="sidebar-nav">
{steps.map((step, index) => (
<div key={step.id} className={`nav-item ${index === stepIndex ? 'active' : ''} ${index < stepIndex ? 'completed' : ''}`}>
<div className="nav-indicator">
{index < stepIndex ? <CheckCircle2 size={14} /> : <div className="dot" />}
</div>
<div className="nav-info">
<div className="nav-title">{step.title}</div>
<div className="nav-desc">{step.desc}</div>
</div>
</div>
))}
</div>
<div className="sidebar-footer">
<ShieldCheck size={14} />
<span></span>
</div>
</div>
<div className="welcome-content">
<div className="content-header">
<div>
<h2>{currentStep.title}</h2>
<p className="header-desc">{currentStep.desc}</p>
</div>
</div>
<div className="content-body">
{currentStep.id === 'intro' && (
<div className="intro-block">
{/* 内容移至底部 */}
</div>
)}
{currentStep.id === 'db' && (
<div className="form-group">
<label className="field-label"></label>
<div className="input-group">
<input
type="text"
className="field-input"
placeholder="例如C:\\Users\\xxx\\Documents\\xwechat_files"
value={dbPath}
onChange={(e) => setDbPath(e.target.value)}
/>
</div>
<div className="action-row">
<button className="btn btn-secondary" onClick={handleAutoDetectPath} disabled={isDetectingPath}>
<FolderSearch size={16} /> {isDetectingPath ? '检测中...' : '自动检测'}
</button>
<button className="btn btn-secondary" onClick={handleSelectPath}>
<FolderOpen size={16} /> ...
</button>
</div>
<div className="field-hint">--</div>
<div className="field-hint warning">
</div>
</div>
)}
{currentStep.id === 'cache' && (
<div className="form-group">
<label className="field-label"></label>
<div className="input-group">
<input
type="text"
className="field-input"
placeholder="留空即使用默认目录"
value={cachePath}
onChange={(e) => setCachePath(e.target.value)}
/>
</div>
<div className="action-row">
<button className="btn btn-secondary" onClick={handleSelectCachePath}>
<FolderOpen size={16} />
</button>
<button className="btn btn-secondary" onClick={() => setCachePath('')}>
<RotateCcw size={16} />
</button>
</div>
<div className="field-hint"></div>
</div>
)}
{currentStep.id === 'key' && (
<div className="form-group">
<label className="field-label"> (Wxid)</label>
<div className="wxid-select" ref={wxidSelectRef}>
<input
type="text"
className="field-input"
placeholder="点击选择..."
value={wxid}
readOnly
onClick={handleScanWxidCandidates}
onChange={(e) => setWxid(e.target.value)}
/>
{showWxidSelect && wxidOptions.length > 0 && (
<div className="wxid-dropdown">
{wxidOptions.map((opt) => (
<button
key={opt.wxid}
type="button"
className={`wxid-option ${opt.wxid === wxid ? 'active' : ''}`}
onClick={() => {
setWxid(opt.wxid)
setShowWxidSelect(false)
}}
>
<span className="wxid-name">{opt.wxid}</span>
<span className="wxid-time">{formatModifiedTime(opt.modifiedTime)}</span>
</button>
))}
</div>
)}
</div>
<label className="field-label mt-4"></label>
<div className="field-with-toggle">
<input
type={showDecryptKey ? 'text' : 'password'}
className="field-input"
placeholder="64 位十六进制密钥"
value={decryptKey}
onChange={(e) => setDecryptKey(e.target.value.trim())}
/>
<button type="button" className="toggle-btn" onClick={() => setShowDecryptKey(!showDecryptKey)}>
{showDecryptKey ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
<div className="key-actions">
{isManualStartPrompt ? (
<div className="manual-prompt">
<p></p>
<button className="btn btn-primary" onClick={handleManualConfirm}>
</button>
</div>
) : (
<button className="btn btn-secondary btn-block" onClick={handleAutoGetDbKey} disabled={isFetchingDbKey}>
{isFetchingDbKey ? '正在获取...' : '自动获取密钥'}
</button>
)}
</div>
{dbKeyStatus && <div className="status-message">{dbKeyStatus}</div>}
<div className="field-hint"></div>
</div>
)}
{currentStep.id === 'security' && (
<div className="form-group">
<div className="security-toggle-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<div className="toggle-info">
<label className="field-label" style={{ marginBottom: 0 }}></label>
<div className="field-hint"></div>
</div>
<label className="switch">
<input type="checkbox" checked={enableAuth} onChange={e => setEnableAuth(e.target.checked)} />
<span className="switch-slider" />
</label>
</div>
{enableAuth && (
<div className="security-settings" style={{ marginTop: 20, padding: 16, backgroundColor: 'var(--bg-secondary)', borderRadius: 8 }}>
<div className="form-group">
<label className="field-label"></label>
<input
type="password"
className="field-input"
placeholder="请输入密码"
value={authPassword}
onChange={e => setAuthPassword(e.target.value)}
/>
</div>
<div className="form-group">
<label className="field-label"></label>
<input
type="password"
className="field-input"
placeholder="请再次输入密码"
value={authConfirmPassword}
onChange={e => setAuthConfirmPassword(e.target.value)}
/>
{authPassword && authConfirmPassword && authPassword !== authConfirmPassword && (
<div className="error-text" style={{ color: '#ff4d4f', fontSize: 12, marginTop: 4 }}></div>
)}
</div>
<div className="divider" style={{ margin: '20px 0', borderTop: '1px solid var(--border-color)' }}></div>
<div className="security-toggle-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="toggle-info">
<label className="field-label" style={{ marginBottom: 0 }}>Windows Hello</label>
<div className="field-hint">使 PIN </div>
</div>
{enableHello ? (
<div style={{ color: '#52c41a', display: 'flex', alignItems: 'center', gap: 6 }}>
<CheckCircle2 size={16} />
<button className="btn btn-ghost btn-sm" onClick={() => setEnableHello(false)} style={{ padding: '2px 8px', height: 24, fontSize: 12 }}></button>
</div>
) : (
<button
className="btn btn-secondary btn-sm"
disabled={!helloAvailable || isSettingHello}
onClick={handleSetupHello}
>
{isSettingHello ? '设置中...' : (helloAvailable ? '点击开启' : '不可用')}
</button>
)}
</div>
{!helloAvailable && <div className="field-hint warning"> Windows Hello PIN </div>}
</div>
)}
</div>
)}
{currentStep.id === 'image' && (
<div className="form-group">
<div className="field-hint" style={{ color: '#f59e0b', marginBottom: '12px' }}>
使
</div>
<div className="grid-2">
<div>
<label className="field-label"> XOR </label>
<input type="text" className="field-input" placeholder="0x..." value={imageXorKey} onChange={(e) => setImageXorKey(e.target.value)} />
</div>
<div>
<label className="field-label"> AES </label>
<input type="text" className="field-input" placeholder="16位密钥" value={imageAesKey} onChange={(e) => setImageAesKey(e.target.value)} />
</div>
</div>
<div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
<button className="btn btn-secondary btn-block" onClick={handleAutoGetImageKey} disabled={isFetchingImageKey} title="从本地缓存快速计算(可能不准确)">
{isFetchingImageKey ? '获取中...' : '快速获取(缓存计算)'}
</button>
<button className="btn btn-primary btn-block" onClick={handleScanImageKeyFromMemory} disabled={isFetchingImageKey} title="扫描微信进程内存,准确率更高,需要微信正在运行">
{isFetchingImageKey ? '扫描中...' : '内存扫描(推荐)'}
</button>
</div>
{isFetchingImageKey ? (
<div className="brute-force-progress">
<div className="status-header">
<span className="status-text">{imageKeyStatus || '正在启动...'}</span>
</div>
</div>
) : (
imageKeyStatus && <div className="status-message" style={{ marginTop: '12px' }}>{imageKeyStatus}</div>
)}
<div className="field-hint" style={{ marginTop: '8px' }}> 2-3 </div>
</div>
)}
</div>
{error && <div className="error-message">{error}</div>}
{currentStep.id === 'intro' && (
<div className="intro-footer">
<p></p>
<p>WeFlow 访</p>
</div>
)}
<div className="content-actions">
<button className="btn btn-ghost" onClick={handleBack} disabled={stepIndex === 0}>
<ArrowLeft size={16} />
</button>
{stepIndex < steps.length - 1 ? (
<button className="btn btn-primary" onClick={handleNext} disabled={!canGoNext()}>
<ArrowRight size={16} />
</button>
) : (
<button className="btn btn-primary" onClick={handleConnect} disabled={isConnecting || !canGoNext()}>
{isConnecting ? '连接中...' : '完成配置'} <ArrowRight size={16} />
</button>
)}
</div>
</div>
</div>
</div>
)
}
export default WelcomePage