diff --git a/electron/services/keyService.ts b/electron/services/keyService.ts index 66345c2..25b0aa1 100644 --- a/electron/services/keyService.ts +++ b/electron/services/keyService.ts @@ -606,34 +606,14 @@ export class KeyService { const logs: string[] = [] - onStatus?.('正在定位微信安装路径...', 0) - let wechatPath = await this.findWeChatInstallPath() - if (!wechatPath) { - const err = '未找到微信安装路径,请确认已安装PC微信' + onStatus?.('正在查找微信进程...', 0) + const pid = await this.findWeChatPid() + if (!pid) { + const err = '未找到微信进程,请先启动微信' onStatus?.(err, 2) return { success: false, error: err } } - onStatus?.('正在关闭微信以进行获取...', 0) - const closed = await this.killWeChatProcesses() - if (!closed) { - const err = '无法自动关闭微信,请手动退出后重试' - onStatus?.(err, 2) - return { success: false, error: err } - } - - onStatus?.('正在启动微信...', 0) - const sub = spawn(wechatPath, { - detached: true, - stdio: 'ignore', - cwd: dirname(wechatPath) - }) - sub.unref() - - onStatus?.('等待微信界面就绪...', 0) - const pid = await this.waitForWeChatWindow() - if (!pid) return { success: false, error: '启动微信失败或等待界面就绪超时' } - onStatus?.(`检测到微信窗口 (PID: ${pid}),正在获取...`, 0) onStatus?.('正在检测微信界面组件...', 0) await this.waitForWeChatWindowComponents(pid, 15000) diff --git a/src/components/ConfirmDialog.scss b/src/components/ConfirmDialog.scss new file mode 100644 index 0000000..5acd32d --- /dev/null +++ b/src/components/ConfirmDialog.scss @@ -0,0 +1,123 @@ +.confirm-dialog-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + animation: fadeIn 0.2s ease-out; + + .confirm-dialog { + width: 480px; + background: var(--bg-primary); + border-radius: 20px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + position: relative; + animation: slideUp 0.2s ease-out; + overflow: hidden; + + .close-btn { + position: absolute; + top: 16px; + right: 16px; + background: rgba(0, 0, 0, 0.05); + border: none; + color: var(--text-secondary); + cursor: pointer; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + + &:hover { + background: rgba(0, 0, 0, 0.1); + color: var(--text-primary); + } + } + + .dialog-title { + padding: 40px 40px 16px; + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + } + + .dialog-content { + padding: 0 40px 24px; + + p { + font-size: 15px; + color: var(--text-primary); + line-height: 1.6; + margin: 0 0 16px 0; + + &:last-child { + margin-bottom: 0; + } + } + } + + .dialog-actions { + padding: 0 40px 40px; + display: flex; + justify-content: flex-end; + gap: 12px; + + button { + padding: 12px 24px; + border-radius: 12px; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + border: none; + + &.btn-cancel { + background: var(--bg-tertiary); + color: var(--text-secondary); + + &:hover { + background: var(--bg-hover); + } + } + + &.btn-confirm { + background: var(--primary); + color: var(--on-primary); + + &:hover { + background: var(--primary-hover); + } + + &:active { + transform: scale(0.98); + } + } + } + } + } +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx new file mode 100644 index 0000000..7126edf --- /dev/null +++ b/src/components/ConfirmDialog.tsx @@ -0,0 +1,32 @@ +import { X } from 'lucide-react' +import './ConfirmDialog.scss' + +interface ConfirmDialogProps { + open: boolean + title?: string + message: string + onConfirm: () => void + onCancel: () => void +} + +export default function ConfirmDialog({ open, title, message, onConfirm, onCancel }: ConfirmDialogProps) { + if (!open) return null + + return ( +
+
e.stopPropagation()}> + + {title &&
{title}
} +
+

{message}

+
+
+ + +
+
+
+ ) +} diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 559fc03..50e1534 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -5202,6 +5202,24 @@ function MessageBubble({ const [emojiError, setEmojiError] = useState(false) const [emojiLoading, setEmojiLoading] = useState(false) + // 缓存相关的 state 必须在所有 Hooks 之前声明 + const cacheKey = message.emojiMd5 || message.emojiCdnUrl || '' + const [emojiLocalPath, setEmojiLocalPath] = useState( + () => emojiDataUrlCache.get(cacheKey) || message.emojiLocalPath + ) + const imageCacheKey = message.imageMd5 || message.imageDatName || `local:${message.localId}` + const [imageLocalPath, setImageLocalPath] = useState( + () => imageDataUrlCache.get(imageCacheKey) + ) + const voiceCacheKey = `voice:${message.localId}` + const [voiceDataUrl, setVoiceDataUrl] = useState( + () => voiceDataUrlCache.get(voiceCacheKey) + ) + const voiceTranscriptCacheKey = `voice-transcript:${message.localId}` + const [voiceTranscript, setVoiceTranscript] = useState( + () => voiceTranscriptCache.get(voiceTranscriptCacheKey) + ) + // State variables... const [imageError, setImageError] = useState(false) const [imageLoading, setImageLoading] = useState(false) @@ -5282,24 +5300,6 @@ function MessageBubble({ loadConfig() }, []) - // 从缓存获取表情包 data URL - const cacheKey = message.emojiMd5 || message.emojiCdnUrl || '' - const [emojiLocalPath, setEmojiLocalPath] = useState( - () => emojiDataUrlCache.get(cacheKey) || message.emojiLocalPath - ) - const imageCacheKey = message.imageMd5 || message.imageDatName || `local:${message.localId}` - const [imageLocalPath, setImageLocalPath] = useState( - () => imageDataUrlCache.get(imageCacheKey) - ) - const voiceCacheKey = `voice:${message.localId}` - const [voiceDataUrl, setVoiceDataUrl] = useState( - () => voiceDataUrlCache.get(voiceCacheKey) - ) - const voiceTranscriptCacheKey = `voice-transcript:${message.localId}` - const [voiceTranscript, setVoiceTranscript] = useState( - () => voiceTranscriptCache.get(voiceTranscriptCacheKey) - ) - const formatTime = (timestamp: number): string => { if (!Number.isFinite(timestamp) || timestamp <= 0) return '未知时间' const date = new Date(timestamp * 1000) diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index d1608eb..04a5751 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -41,6 +41,7 @@ } .settings-page { + position: relative; display: flex; flex-direction: column; width: min(1160px, calc(100vw - 96px)); diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index ae49e8e..d963b14 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -557,24 +557,37 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { } } + const validatePath = (path: string): string | null => { + if (!path) return null + if (/[\u4e00-\u9fa5]/.test(path)) { + return '路径包含中文字符,请迁移至全英文目录' + } + return null + } + const handleAutoDetectPath = async () => { if (isDetectingPath) return setIsDetectingPath(true) try { const result = await window.electronAPI.dbPath.autoDetect() if (result.success && result.path) { - setDbPath(result.path) - await configService.setDbPath(result.path) - showMessage(`自动检测成功:${result.path}`, true) + const validationError = validatePath(result.path) + if (validationError) { + showMessage(validationError, false) + } else { + setDbPath(result.path) + await configService.setDbPath(result.path) + showMessage(`自动检测成功:${result.path}`, true) - const wxids = await window.electronAPI.dbPath.scanWxids(result.path) - setWxidOptions(wxids) - if (wxids.length === 1) { - await applyWxidSelection(wxids[0].wxid, { - toastText: `已检测到账号:${wxids[0].wxid}` - }) - } else if (wxids.length > 1) { - setShowWxidSelect(true) + const wxids = await window.electronAPI.dbPath.scanWxids(result.path) + setWxidOptions(wxids) + if (wxids.length === 1) { + await applyWxidSelection(wxids[0].wxid, { + toastText: `已检测到账号:${wxids[0].wxid}` + }) + } else if (wxids.length > 1) { + setShowWxidSelect(true) + } } } else { showMessage(result.error || '未能自动检测到数据库目录', false) @@ -591,9 +604,14 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const result = await dialog.openFile({ title: '选择微信数据库根目录', properties: ['openDirectory'] }) if (!result.canceled && result.filePaths.length > 0) { const selectedPath = result.filePaths[0] - setDbPath(selectedPath) - await configService.setDbPath(selectedPath) - showMessage('已选择数据库目录', true) + const validationError = validatePath(selectedPath) + if (validationError) { + showMessage(validationError, false) + } else { + setDbPath(selectedPath) + await configService.setDbPath(selectedPath) + showMessage('已选择数据库目录', true) + } } } catch (e: any) { showMessage('选择目录失败', false) @@ -1287,7 +1305,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
xwechat_files 目录 - 目录路径不可包含中文,如有中文请去微信-设置-存储位置点击更改,迁移至全英文目录 (null) + const [showDbKeyConfirm, setShowDbKeyConfirm] = useState(false) // 安全相关 state const [enableAuth, setEnableAuth] = useState(false) @@ -123,6 +125,14 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { useEffect(() => { const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => { setDbKeyStatus(payload.message) + if (payload.message.includes('现在可以登录') || payload.message.includes('Hook安装成功')) { + window.electronAPI.notification?.show({ + title: 'WeFlow 准备就绪', + content: '现在可以登录微信了', + avatarUrl: './logo.png', + sessionId: 'weflow-system' + }) + } }) const removeImage = window.electronAPI.key.onImageKeyStatus((payload: { message: string, percent?: number }) => { let msg = payload.message; @@ -187,6 +197,15 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { window.electronAPI.window.close() } + const validatePath = (path: string): string | null => { + if (!path) return null + // 检测中文字符和其他可能有问题的特殊字符 + if (/[\u4e00-\u9fa5]/.test(path)) { + return '路径包含中文字符,请迁移至全英文目录' + } + return null + } + const handleSelectPath = async () => { try { const result = await dialog.openFile({ @@ -195,8 +214,14 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { }) if (!result.canceled && result.filePaths.length > 0) { - setDbPath(result.filePaths[0]) - setError('') + const selectedPath = result.filePaths[0] + const validationError = validatePath(selectedPath) + if (validationError) { + setError(validationError) + } else { + setDbPath(selectedPath) + setError('') + } } } catch (e) { setError('选择目录失败') @@ -210,8 +235,13 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { try { const result = await window.electronAPI.dbPath.autoDetect() if (result.success && result.path) { - setDbPath(result.path) - setError('') + const validationError = validatePath(result.path) + if (validationError) { + setError(validationError) + } else { + setDbPath(result.path) + setError('') + } } else { setError(result.error || '未能检测到数据库目录') } @@ -287,6 +317,11 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { const handleAutoGetDbKey = async () => { if (isFetchingDbKey) return + setShowDbKeyConfirm(true) + } + + const handleDbKeyConfirm = async () => { + setShowDbKeyConfirm(false) setIsFetchingDbKey(true) setError('') setIsManualStartPrompt(false) @@ -297,7 +332,6 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { setDecryptKey(result.key) setDbKeyStatus('密钥获取成功') setError('') - // 获取成功后自动扫描并填入 wxid await handleScanWxid(true) } else { if (result.error?.includes('未找到微信安装路径') || result.error?.includes('启动微信失败')) { @@ -613,9 +647,6 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
请选择微信-设置-存储位置对应的目录
-
- 目录路径不可包含中文,如有中文请先在微信中迁移至全英文目录 -
)} @@ -705,7 +736,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { )} - {dbKeyStatus &&
{dbKeyStatus}
} + {dbKeyStatus &&
{dbKeyStatus}
}
点击自动获取后微信将重启,请留意弹窗提示
)} @@ -840,6 +871,16 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { )} + + setShowDbKeyConfirm(false)} + /> )