修复 #389 ;并优化了引导页面

This commit is contained in:
cc
2026-03-14 22:23:10 +08:00
parent 0a23ed6ef4
commit 641abc57b9
8 changed files with 274 additions and 67 deletions

View File

@@ -606,34 +606,14 @@ export class KeyService {
const logs: string[] = [] const logs: string[] = []
onStatus?.('正在定位微信安装路径...', 0) onStatus?.('正在查找微信进程...', 0)
let wechatPath = await this.findWeChatInstallPath() const pid = await this.findWeChatPid()
if (!wechatPath) { if (!pid) {
const err = '未找到微信安装路径请确认已安装PC微信' const err = '未找到微信进程,请先启动微信'
onStatus?.(err, 2) onStatus?.(err, 2)
return { success: false, error: err } 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?.(`检测到微信窗口 (PID: ${pid}),正在获取...`, 0)
onStatus?.('正在检测微信界面组件...', 0) onStatus?.('正在检测微信界面组件...', 0)
await this.waitForWeChatWindowComponents(pid, 15000) await this.waitForWeChatWindowComponents(pid, 15000)

View File

@@ -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;
}
}

View File

@@ -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 (
<div className="confirm-dialog-overlay" onClick={onCancel}>
<div className="confirm-dialog" onClick={e => e.stopPropagation()}>
<button className="close-btn" onClick={onCancel}>
<X size={20} />
</button>
{title && <div className="dialog-title">{title}</div>}
<div className="dialog-content">
<p style={{ whiteSpace: 'pre-line' }}>{message}</p>
</div>
<div className="dialog-actions">
<button className="btn-cancel" onClick={onCancel}></button>
<button className="btn-confirm" onClick={onConfirm}></button>
</div>
</div>
</div>
)
}

View File

@@ -5202,6 +5202,24 @@ function MessageBubble({
const [emojiError, setEmojiError] = useState(false) const [emojiError, setEmojiError] = useState(false)
const [emojiLoading, setEmojiLoading] = useState(false) const [emojiLoading, setEmojiLoading] = useState(false)
// 缓存相关的 state 必须在所有 Hooks 之前声明
const cacheKey = message.emojiMd5 || message.emojiCdnUrl || ''
const [emojiLocalPath, setEmojiLocalPath] = useState<string | undefined>(
() => emojiDataUrlCache.get(cacheKey) || message.emojiLocalPath
)
const imageCacheKey = message.imageMd5 || message.imageDatName || `local:${message.localId}`
const [imageLocalPath, setImageLocalPath] = useState<string | undefined>(
() => imageDataUrlCache.get(imageCacheKey)
)
const voiceCacheKey = `voice:${message.localId}`
const [voiceDataUrl, setVoiceDataUrl] = useState<string | undefined>(
() => voiceDataUrlCache.get(voiceCacheKey)
)
const voiceTranscriptCacheKey = `voice-transcript:${message.localId}`
const [voiceTranscript, setVoiceTranscript] = useState<string | undefined>(
() => voiceTranscriptCache.get(voiceTranscriptCacheKey)
)
// State variables... // State variables...
const [imageError, setImageError] = useState(false) const [imageError, setImageError] = useState(false)
const [imageLoading, setImageLoading] = useState(false) const [imageLoading, setImageLoading] = useState(false)
@@ -5282,24 +5300,6 @@ function MessageBubble({
loadConfig() loadConfig()
}, []) }, [])
// 从缓存获取表情包 data URL
const cacheKey = message.emojiMd5 || message.emojiCdnUrl || ''
const [emojiLocalPath, setEmojiLocalPath] = useState<string | undefined>(
() => emojiDataUrlCache.get(cacheKey) || message.emojiLocalPath
)
const imageCacheKey = message.imageMd5 || message.imageDatName || `local:${message.localId}`
const [imageLocalPath, setImageLocalPath] = useState<string | undefined>(
() => imageDataUrlCache.get(imageCacheKey)
)
const voiceCacheKey = `voice:${message.localId}`
const [voiceDataUrl, setVoiceDataUrl] = useState<string | undefined>(
() => voiceDataUrlCache.get(voiceCacheKey)
)
const voiceTranscriptCacheKey = `voice-transcript:${message.localId}`
const [voiceTranscript, setVoiceTranscript] = useState<string | undefined>(
() => voiceTranscriptCache.get(voiceTranscriptCacheKey)
)
const formatTime = (timestamp: number): string => { const formatTime = (timestamp: number): string => {
if (!Number.isFinite(timestamp) || timestamp <= 0) return '未知时间' if (!Number.isFinite(timestamp) || timestamp <= 0) return '未知时间'
const date = new Date(timestamp * 1000) const date = new Date(timestamp * 1000)

View File

@@ -41,6 +41,7 @@
} }
.settings-page { .settings-page {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: min(1160px, calc(100vw - 96px)); width: min(1160px, calc(100vw - 96px));

View File

@@ -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 () => { const handleAutoDetectPath = async () => {
if (isDetectingPath) return if (isDetectingPath) return
setIsDetectingPath(true) setIsDetectingPath(true)
try { try {
const result = await window.electronAPI.dbPath.autoDetect() const result = await window.electronAPI.dbPath.autoDetect()
if (result.success && result.path) { if (result.success && result.path) {
setDbPath(result.path) const validationError = validatePath(result.path)
await configService.setDbPath(result.path) if (validationError) {
showMessage(`自动检测成功:${result.path}`, true) 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) const wxids = await window.electronAPI.dbPath.scanWxids(result.path)
setWxidOptions(wxids) setWxidOptions(wxids)
if (wxids.length === 1) { if (wxids.length === 1) {
await applyWxidSelection(wxids[0].wxid, { await applyWxidSelection(wxids[0].wxid, {
toastText: `已检测到账号:${wxids[0].wxid}` toastText: `已检测到账号:${wxids[0].wxid}`
}) })
} else if (wxids.length > 1) { } else if (wxids.length > 1) {
setShowWxidSelect(true) setShowWxidSelect(true)
}
} }
} else { } else {
showMessage(result.error || '未能自动检测到数据库目录', false) showMessage(result.error || '未能自动检测到数据库目录', false)
@@ -591,9 +604,14 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const result = await dialog.openFile({ title: '选择微信数据库根目录', properties: ['openDirectory'] }) const result = await dialog.openFile({ title: '选择微信数据库根目录', properties: ['openDirectory'] })
if (!result.canceled && result.filePaths.length > 0) { if (!result.canceled && result.filePaths.length > 0) {
const selectedPath = result.filePaths[0] const selectedPath = result.filePaths[0]
setDbPath(selectedPath) const validationError = validatePath(selectedPath)
await configService.setDbPath(selectedPath) if (validationError) {
showMessage('已选择数据库目录', true) showMessage(validationError, false)
} else {
setDbPath(selectedPath)
await configService.setDbPath(selectedPath)
showMessage('已选择数据库目录', true)
}
} }
} catch (e: any) { } catch (e: any) {
showMessage('选择目录失败', false) showMessage('选择目录失败', false)
@@ -1287,7 +1305,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<div className="form-group"> <div className="form-group">
<label></label> <label></label>
<span className="form-hint">xwechat_files </span> <span className="form-hint">xwechat_files </span>
<span className="form-hint" style={{ color: '#ff6b6b' }}> --</span>
<input <input
type="text" type="text"
placeholder="例如: C:\Users\xxx\Documents\xwechat_files" placeholder="例如: C:\Users\xxx\Documents\xwechat_files"

View File

@@ -77,6 +77,7 @@
/* Unified Card Container */ /* Unified Card Container */
.welcome-container { .welcome-container {
position: relative;
width: 900px; width: 900px;
max-width: 100vw; max-width: 100vw;
height: 620px; height: 620px;
@@ -543,6 +544,18 @@
font-size: 13px; font-size: 13px;
margin-top: 8px; margin-top: 8px;
border: 1px solid rgba(0, 0, 0, 0.04); border: 1px solid rgba(0, 0, 0, 0.04);
&.is-success {
background: rgba(34, 197, 94, 0.15);
color: rgb(22, 163, 74);
border-color: rgba(34, 197, 94, 0.3);
@media (prefers-color-scheme: dark) {
background: rgba(34, 197, 94, 0.2);
color: rgb(134, 239, 172);
border-color: rgba(34, 197, 94, 0.4);
}
}
} }
.error-message { .error-message {
@@ -878,4 +891,4 @@
@keyframes progress-shimmer { @keyframes progress-shimmer {
0% { transform: translateX(-100%); } 0% { transform: translateX(-100%); }
100% { transform: translateX(100%); } 100% { transform: translateX(100%); }
} }

View File

@@ -8,6 +8,7 @@ import {
FolderOpen, FolderSearch, KeyRound, ShieldCheck, Sparkles, FolderOpen, FolderSearch, KeyRound, ShieldCheck, Sparkles,
UserRound, Wand2, Minus, X, HardDrive, RotateCcw UserRound, Wand2, Minus, X, HardDrive, RotateCcw
} from 'lucide-react' } from 'lucide-react'
import ConfirmDialog from '../components/ConfirmDialog'
import './WelcomePage.scss' import './WelcomePage.scss'
const steps = [ const steps = [
@@ -61,6 +62,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
const [imageKeyStatus, setImageKeyStatus] = useState('') const [imageKeyStatus, setImageKeyStatus] = useState('')
const [isManualStartPrompt, setIsManualStartPrompt] = useState(false) const [isManualStartPrompt, setIsManualStartPrompt] = useState(false)
const [imageKeyPercent, setImageKeyPercent] = useState<number | null>(null) const [imageKeyPercent, setImageKeyPercent] = useState<number | null>(null)
const [showDbKeyConfirm, setShowDbKeyConfirm] = useState(false)
// 安全相关 state // 安全相关 state
const [enableAuth, setEnableAuth] = useState(false) const [enableAuth, setEnableAuth] = useState(false)
@@ -123,6 +125,14 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
useEffect(() => { useEffect(() => {
const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => { const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => {
setDbKeyStatus(payload.message) 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 }) => { const removeImage = window.electronAPI.key.onImageKeyStatus((payload: { message: string, percent?: number }) => {
let msg = payload.message; let msg = payload.message;
@@ -187,6 +197,15 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
window.electronAPI.window.close() 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 () => { const handleSelectPath = async () => {
try { try {
const result = await dialog.openFile({ const result = await dialog.openFile({
@@ -195,8 +214,14 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
}) })
if (!result.canceled && result.filePaths.length > 0) { if (!result.canceled && result.filePaths.length > 0) {
setDbPath(result.filePaths[0]) const selectedPath = result.filePaths[0]
setError('') const validationError = validatePath(selectedPath)
if (validationError) {
setError(validationError)
} else {
setDbPath(selectedPath)
setError('')
}
} }
} catch (e) { } catch (e) {
setError('选择目录失败') setError('选择目录失败')
@@ -210,8 +235,13 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
try { try {
const result = await window.electronAPI.dbPath.autoDetect() const result = await window.electronAPI.dbPath.autoDetect()
if (result.success && result.path) { if (result.success && result.path) {
setDbPath(result.path) const validationError = validatePath(result.path)
setError('') if (validationError) {
setError(validationError)
} else {
setDbPath(result.path)
setError('')
}
} else { } else {
setError(result.error || '未能检测到数据库目录') setError(result.error || '未能检测到数据库目录')
} }
@@ -287,6 +317,11 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
const handleAutoGetDbKey = async () => { const handleAutoGetDbKey = async () => {
if (isFetchingDbKey) return if (isFetchingDbKey) return
setShowDbKeyConfirm(true)
}
const handleDbKeyConfirm = async () => {
setShowDbKeyConfirm(false)
setIsFetchingDbKey(true) setIsFetchingDbKey(true)
setError('') setError('')
setIsManualStartPrompt(false) setIsManualStartPrompt(false)
@@ -297,7 +332,6 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
setDecryptKey(result.key) setDecryptKey(result.key)
setDbKeyStatus('密钥获取成功') setDbKeyStatus('密钥获取成功')
setError('') setError('')
// 获取成功后自动扫描并填入 wxid
await handleScanWxid(true) await handleScanWxid(true)
} else { } else {
if (result.error?.includes('未找到微信安装路径') || result.error?.includes('启动微信失败')) { if (result.error?.includes('未找到微信安装路径') || result.error?.includes('启动微信失败')) {
@@ -613,9 +647,6 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
</div> </div>
<div className="field-hint">--</div> <div className="field-hint">--</div>
<div className="field-hint warning">
</div>
</div> </div>
)} )}
@@ -705,7 +736,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
)} )}
</div> </div>
{dbKeyStatus && <div className="status-message">{dbKeyStatus}</div>} {dbKeyStatus && <div className={`status-message ${dbKeyStatus.includes('现在可以登录') || dbKeyStatus.includes('Hook安装成功') ? 'is-success' : ''}`}>{dbKeyStatus}</div>}
<div className="field-hint"></div> <div className="field-hint"></div>
</div> </div>
)} )}
@@ -840,6 +871,16 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
)} )}
</div> </div>
</div> </div>
<ConfirmDialog
open={showDbKeyConfirm}
title="开始获取数据库密钥"
message={`当开始获取后 WeFlow 将会执行准备操作
当 WeFlow 内的提示条变为绿色显示允许登录或看到来自WeFlow的登录通知时登录你的微信或退出当前登录并重新登录。`}
onConfirm={handleDbKeyConfirm}
onCancel={() => setShowDbKeyConfirm(false)}
/>
</div> </div>
</div> </div>
) )