feat: 一些更新

This commit is contained in:
cc
2026-01-29 20:41:12 +08:00
parent ff1bc279f2
commit b9fa0cc215
17 changed files with 875 additions and 232 deletions

View File

@@ -9,40 +9,40 @@ function AgreementPage() {
<div className="agreement-content">
{/* 协议内容 - 请替换为完整的协议文本 */}
<h2></h2>
<h3></h3>
<p>使WeFlowWeFlow使使</p>
<p>使WeFlowWeFlow使使</p>
<h3></h3>
<p>WeFlow是一款本地化的微信聊天记录查看与分析工具</p>
<h3>使</h3>
<p>1. 使</p>
<p>2. </p>
<p>3. </p>
<h3></h3>
<p>1. "现状"</p>
<p>2. 使使</p>
<p>3. </p>
<h3></h3>
<p></p>
<h2></h2>
<h3></h3>
<p></p>
<h3></h3>
<p></p>
<p></p>
<h3></h3>
<p>访</p>
<h3></h3>
<p>广</p>
<p className="agreement-footer-text">20251</p>
</div>
</div>

View File

@@ -34,8 +34,8 @@ function AnalyticsWelcomePage() {
</div>
<h1></h1>
<p>
WeFlow <br />
WeFlow <br />
</p>
<div className="action-cards">

View File

@@ -338,61 +338,33 @@
}
}
.time-options {
display: flex;
flex-direction: column;
gap: 12px;
}
.checkbox-item {
.time-range-picker-item {
display: flex;
align-items: center;
gap: 10px;
justify-content: space-between;
padding: 14px 16px;
cursor: pointer;
font-size: 14px;
color: var(--text-primary);
input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--primary);
cursor: pointer;
}
svg {
color: var(--text-secondary);
}
&.main-toggle {
padding: 12px 16px;
background: var(--bg-secondary);
border-radius: 10px;
}
}
.date-range {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: var(--bg-secondary);
border-radius: 10px;
font-size: 14px;
color: var(--text-primary);
cursor: pointer;
transition: all 0.2s;
transition: background 0.2s;
background: transparent;
&:hover {
background: var(--bg-hover);
}
svg {
color: var(--text-tertiary);
flex-shrink: 0;
.time-picker-info {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
color: var(--text-primary);
svg {
color: var(--primary);
}
}
span {
flex: 1;
svg {
color: var(--text-tertiary);
}
}
@@ -1184,50 +1156,4 @@
color: var(--text-tertiary);
}
// Switch 开关样式
.switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
flex-shrink: 0;
input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--bg-tertiary);
transition: 0.3s;
border-radius: 24px;
&::before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
}
input:checked+.slider {
background-color: var(--primary);
}
input:checked+.slider::before {
transform: translateX(20px);
}
}
// 全局样式已在 main.scss 中定义

View File

@@ -537,21 +537,34 @@ function ExportPage() {
<div className="setting-section">
<h3></h3>
<div className="time-options">
<label className="checkbox-item">
<input
type="checkbox"
checked={options.useAllTime}
onChange={e => setOptions({ ...options, useAllTime: e.target.checked })}
/>
<span></span>
</label>
{!options.useAllTime && options.dateRange && (
<div className="date-range" onClick={() => setShowDatePicker(true)}>
<Calendar size={16} />
<span>{formatDate(options.dateRange.start)} - {formatDate(options.dateRange.end)}</span>
<ChevronDown size={14} />
<p className="setting-subtitle"></p>
<div className="media-options-card">
<div className="media-switch-row">
<div className="media-switch-info">
<span className="media-switch-title"></span>
<span className="media-switch-desc"></span>
</div>
<label className="switch">
<input
type="checkbox"
checked={options.useAllTime}
onChange={e => setOptions({ ...options, useAllTime: e.target.checked })}
/>
<span className="switch-slider"></span>
</label>
</div>
{!options.useAllTime && options.dateRange && (
<>
<div className="media-option-divider"></div>
<div className="time-range-picker-item" onClick={() => setShowDatePicker(true)}>
<div className="time-picker-info">
<Calendar size={16} />
<span>{formatDate(options.dateRange.start)} - {formatDate(options.dateRange.end)}</span>
</div>
<ChevronDown size={14} />
</div>
</>
)}
</div>
</div>
@@ -609,7 +622,7 @@ function ExportPage() {
checked={options.exportMedia}
onChange={e => setOptions({ ...options, exportMedia: e.target.checked })}
/>
<span className="slider"></span>
<span className="switch-slider"></span>
</label>
</div>
@@ -689,7 +702,7 @@ function ExportPage() {
checked={options.exportAvatars}
onChange={e => setOptions({ ...options, exportAvatars: e.target.checked })}
/>
<span className="slider"></span>
<span className="switch-slider"></span>
</label>
</div>
</div>

View File

@@ -603,54 +603,7 @@
}
}
.switch {
position: relative;
width: 46px;
height: 24px;
display: inline-block;
user-select: none;
}
.switch-input {
opacity: 0;
width: 0;
height: 0;
}
.switch-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 999px;
transition: all 0.2s ease;
}
.switch-slider::before {
content: '';
position: absolute;
height: 18px;
width: 18px;
left: 3px;
top: 2px;
background: var(--text-tertiary);
border-radius: 50%;
transition: all 0.2s ease;
}
.switch-input:checked+.switch-slider {
background: var(--primary);
border-color: var(--primary);
}
.switch-input:checked+.switch-slider::before {
transform: translateX(22px);
background: #ffffff;
}
// 全局样式已在 main.scss 中定义
.log-actions {
display: flex;
@@ -1311,4 +1264,4 @@
border-top: 1px solid var(--border-primary);
display: flex;
justify-content: flex-end;
}
}

View File

@@ -8,11 +8,12 @@ import * as configService from '../services/config'
import {
Eye, EyeOff, FolderSearch, FolderOpen, Search, Copy,
RotateCcw, Trash2, Save, Plug, Check, Sun, Moon,
Palette, Database, Download, HardDrive, Info, RefreshCw, ChevronDown, Mic
Palette, Database, Download, HardDrive, Info, RefreshCw, ChevronDown, Mic,
ShieldCheck, Fingerprint, Lock, KeyRound
} from 'lucide-react'
import './SettingsPage.scss'
type SettingsTab = 'appearance' | 'database' | 'whisper' | 'export' | 'cache' | 'about'
type SettingsTab = 'appearance' | 'database' | 'whisper' | 'export' | 'cache' | 'security' | 'about'
const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
{ id: 'appearance', label: '外观', icon: Palette },
@@ -20,6 +21,7 @@ const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
{ id: 'whisper', label: '语音识别模型', icon: Mic },
{ id: 'export', label: '导出', icon: Download },
{ id: 'cache', label: '缓存', icon: HardDrive },
{ id: 'security', label: '安全', icon: ShieldCheck },
{ id: 'about', label: '关于', icon: Info }
]
@@ -95,8 +97,31 @@ function SettingsPage() {
const [isClearingImageCache, setIsClearingImageCache] = useState(false)
const [isClearingAllCache, setIsClearingAllCache] = useState(false)
// 安全设置 state
const [authEnabled, setAuthEnabled] = useState(false)
const [authUseHello, setAuthUseHello] = useState(false)
const [helloAvailable, setHelloAvailable] = useState(false)
const [newPassword, setNewPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [isSettingHello, setIsSettingHello] = useState(false)
const isClearingCache = isClearingAnalyticsCache || isClearingImageCache || isClearingAllCache
// 检查 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
}
useEffect(() => {
loadConfig()
loadAppVersion()
@@ -154,6 +179,11 @@ function SettingsPage() {
const savedExportDefaultExcelCompactColumns = await configService.getExportDefaultExcelCompactColumns()
const savedExportDefaultConcurrency = await configService.getExportDefaultConcurrency()
const savedAuthEnabled = await configService.getAuthEnabled()
const savedAuthUseHello = await configService.getAuthUseHello()
setAuthEnabled(savedAuthEnabled)
setAuthUseHello(savedAuthUseHello)
if (savedPath) setDbPath(savedPath)
if (savedWxid) setWxid(savedWxid)
if (savedCachePath) setCachePath(savedCachePath)
@@ -191,7 +221,7 @@ function SettingsPage() {
if (savedWhisperModelDir) setWhisperModelDir(savedWhisperModelDir)
} catch (e) {
} catch (e: any) {
console.error('加载配置失败:', e)
}
}
@@ -217,7 +247,7 @@ function SettingsPage() {
try {
const version = await window.electronAPI.app.getVersion()
setAppVersion(version)
} catch (e) {
} catch (e: any) {
console.error('获取版本号失败:', e)
}
}
@@ -256,7 +286,7 @@ function SettingsPage() {
} else {
showMessage('当前已是最新版', true)
}
} catch (e) {
} catch (e: any) {
showMessage(`检查更新失败: ${e}`, false)
} finally {
setIsCheckingUpdate(false)
@@ -271,7 +301,7 @@ function SettingsPage() {
try {
showMessage('正在下载更新...', true)
await window.electronAPI.app.downloadAndInstall()
} catch (e) {
} catch (e: any) {
showMessage(`更新失败: ${e}`, false)
setIsDownloading(false)
}
@@ -366,7 +396,7 @@ function SettingsPage() {
if (!result.success && result.error) {
showMessage(result.error, false)
}
} catch (e) {
} catch (e: any) {
showMessage(`切换账号后重新连接失败: ${e}`, false)
setDbConnected(false)
}
@@ -403,7 +433,7 @@ function SettingsPage() {
} else {
showMessage(result.error || '未能自动检测到数据库目录', false)
}
} catch (e) {
} catch (e: any) {
showMessage(`自动检测失败: ${e}`, false)
} finally {
setIsDetectingPath(false)
@@ -417,7 +447,7 @@ function SettingsPage() {
setDbPath(result.filePaths[0])
showMessage('已选择数据库目录', true)
}
} catch (e) {
} catch (e: any) {
showMessage('选择目录失败', false)
}
}
@@ -445,7 +475,7 @@ function SettingsPage() {
} else {
if (!silent) showMessage('未检测到账号目录,请检查路径', false)
}
} catch (e) {
} catch (e: any) {
if (!silent) showMessage(`扫描失败: ${e}`, false)
}
}
@@ -461,7 +491,7 @@ function SettingsPage() {
setCachePath(result.filePaths[0])
showMessage('已选择缓存目录', true)
}
} catch (e) {
} catch (e: any) {
showMessage('选择目录失败', false)
}
}
@@ -477,7 +507,7 @@ function SettingsPage() {
await configService.setWhisperModelDir(dir)
showMessage('已选择 Whisper 模型目录', true)
}
} catch (e) {
} catch (e: any) {
showMessage('选择目录失败', false)
}
}
@@ -501,7 +531,7 @@ function SettingsPage() {
} else {
showMessage(result.error || '模型下载失败', false)
}
} catch (e) {
} catch (e: any) {
showMessage(`模型下载失败: ${e}`, false)
} finally {
setIsWhisperDownloading(false)
@@ -533,7 +563,7 @@ function SettingsPage() {
showMessage(result.error || '自动获取密钥失败', false)
}
}
} catch (e) {
} catch (e: any) {
showMessage(`自动获取密钥失败: ${e}`, false)
} finally {
setIsFetchingDbKey(false)
@@ -566,7 +596,7 @@ function SettingsPage() {
} else {
showMessage(result.error || '自动获取图片密钥失败', false)
}
} catch (e) {
} catch (e: any) {
showMessage(`自动获取图片密钥失败: ${e}`, false)
} finally {
setIsFetchingImageKey(false)
@@ -589,7 +619,7 @@ function SettingsPage() {
} else {
showMessage(result.error || '连接测试失败', false)
}
} catch (e) {
} catch (e: any) {
showMessage(`连接测试失败: ${e}`, false)
} finally {
setIsTesting(false)
@@ -624,8 +654,14 @@ function SettingsPage() {
await configService.setOnboardingDone(true)
// 保存按钮只负责持久化配置,不做连接测试/重连,避免影响聊天页的活动连接
// 保存安全配置
// 注意:这里只处理开关,密码修改是实时生效的(在 renderSecurityTab 里处理)
await configService.setAuthEnabled(authEnabled)
await configService.setAuthUseHello(authUseHello)
showMessage('配置保存成功', true)
} catch (e) {
} catch (e: any) {
showMessage(`保存配置失败: ${e}`, false)
} finally {
setIsLoadingState(false)
@@ -657,7 +693,7 @@ function SettingsPage() {
setIsWhisperDownloading(false)
setDbConnected(false)
await window.electronAPI.window.openOnboardingWindow()
} catch (e) {
} catch (e: any) {
showMessage(`清除配置失败: ${e}`, false)
} finally {
setIsLoadingState(false)
@@ -669,7 +705,7 @@ function SettingsPage() {
try {
const logPath = await window.electronAPI.log.getPath()
await window.electronAPI.shell.openPath(logPath)
} catch (e) {
} catch (e: any) {
showMessage(`打开日志失败: ${e}`, false)
}
}
@@ -683,7 +719,7 @@ function SettingsPage() {
}
await navigator.clipboard.writeText(result.content || '')
showMessage('日志已复制到剪贴板', true)
} catch (e) {
} catch (e: any) {
showMessage(`复制日志失败: ${e}`, false)
}
}
@@ -699,7 +735,7 @@ function SettingsPage() {
} else {
showMessage(`清除分析缓存失败: ${result.error || '未知错误'}`, false)
}
} catch (e) {
} catch (e: any) {
showMessage(`清除分析缓存失败: ${e}`, false)
} finally {
setIsClearingAnalyticsCache(false)
@@ -716,7 +752,7 @@ function SettingsPage() {
} else {
showMessage(`清除图片缓存失败: ${result.error || '未知错误'}`, false)
}
} catch (e) {
} catch (e: any) {
showMessage(`清除图片缓存失败: ${e}`, false)
} finally {
setIsClearingImageCache(false)
@@ -734,7 +770,7 @@ function SettingsPage() {
} else {
showMessage(`清除所有缓存失败: ${result.error || '未知错误'}`, false)
}
} catch (e) {
} catch (e: any) {
showMessage(`清除所有缓存失败: ${e}`, false)
} finally {
setIsClearingAllCache(false)
@@ -797,7 +833,7 @@ function SettingsPage() {
<div className="form-group">
<label></label>
<span className="form-hint">xwechat_files </span>
<span className="form-hint" style={{ color: '#ff6b6b' }}> --</span>
<span className="form-hint" style={{ color: '#ff6b6b' }}> --</span>
<input type="text" placeholder="例如: C:\Users\xxx\Documents\xwechat_files" value={dbPath} onChange={(e) => setDbPath(e.target.value)} />
<div className="btn-row">
<button className="btn btn-primary" onClick={handleAutoDetectPath} disabled={isDetectingPath}>
@@ -1210,6 +1246,129 @@ function SettingsPage() {
</div>
)
const handleSetupHello = async () => {
setIsSettingHello(true)
try {
const challenge = new Uint8Array(32)
window.crypto.getRandomValues(challenge)
const credential = await navigator.credentials.create({
publicKey: {
challenge,
rp: { name: 'WeFlow', id: window.location.hostname },
user: { id: new Uint8Array([1]), name: 'user', displayName: 'User' },
pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
authenticatorSelection: { userVerification: 'required' },
timeout: 60000
}
})
if (credential) {
setAuthUseHello(true)
await configService.setAuthUseHello(true)
showMessage('Windows Hello 设置成功', true)
}
} catch (e: any) {
if (e.name !== 'NotAllowedError') {
showMessage(`Windows Hello 设置失败: ${e.message}`, false)
}
} finally {
setIsSettingHello(false)
}
}
const handleUpdatePassword = async () => {
if (!newPassword || newPassword !== confirmPassword) {
showMessage('两次密码不一致', false)
return
}
// 简单的保存逻辑,实际上应该先验证旧密码,但为了简化流程,这里直接允许覆盖
// 因为能进入设置页面说明已经解锁了
try {
const hash = await sha256(newPassword)
await configService.setAuthPassword(hash)
await configService.setAuthEnabled(true)
setAuthEnabled(true)
setNewPassword('')
setConfirmPassword('')
showMessage('密码已更新', true)
} catch (e: any) {
showMessage('密码更新失败', false)
}
}
const renderSecurityTab = () => (
<div className="tab-content">
<div className="form-group">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<label></label>
<span className="form-hint"></span>
</div>
<label className="switch">
<input type="checkbox" checked={authEnabled} onChange={(e) => setAuthEnabled(e.target.checked)} />
<span className="switch-slider" />
</label>
</div>
</div>
<div className="divider" />
<div className="form-group">
<label></label>
<span className="form-hint"></span>
<div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 10 }}>
<input
type="password"
className="field-input"
placeholder="新密码"
value={newPassword}
onChange={e => setNewPassword(e.target.value)}
/>
<div style={{ display: 'flex', gap: 10 }}>
<input
type="password"
className="field-input"
placeholder="确认新密码"
value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)}
style={{ flex: 1 }}
/>
<button className="btn btn-primary" onClick={handleUpdatePassword} disabled={!newPassword}></button>
</div>
</div>
</div>
<div className="divider" />
<div className="form-group">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<label>Windows Hello</label>
<span className="form-hint">使</span>
{!helloAvailable && <div className="form-hint warning" style={{ color: '#ff4d4f' }}> Windows Hello</div>}
</div>
<div>
{authUseHello ? (
<button className="btn btn-secondary btn-sm" onClick={() => setAuthUseHello(false)}></button>
) : (
<button
className="btn btn-secondary btn-sm"
onClick={handleSetupHello}
disabled={!helloAvailable || isSettingHello}
>
{isSettingHello ? '设置中...' : '开启与设置'}
</button>
)}
</div>
</div>
</div>
</div>
)
const renderAboutTab = () => (
<div className="tab-content about-tab">
<div className="about-card">
@@ -1321,6 +1480,7 @@ function SettingsPage() {
{activeTab === 'whisper' && renderWhisperTab()}
{activeTab === 'export' && renderExportTab()}
{activeTab === 'cache' && renderCacheTab()}
{activeTab === 'security' && renderSecurityTab()}
{activeTab === 'about' && renderAboutTab()}
</div>

View File

@@ -112,7 +112,6 @@
-webkit-app-region: drag;
[data-mode="dark"] & {
background: #18181b;
border-right-color: rgba(255, 255, 255, 0.08);
}
}
@@ -152,7 +151,7 @@
margin-top: 2px;
[data-mode="dark"] .welcome-sidebar & {
color: rgba(255, 255, 255, 0.45);
color: rgba(255, 255, 255, 0.6); // 稍微调亮一点
}
}
@@ -188,7 +187,7 @@
border-radius: 12px;
[data-mode="dark"] .welcome-sidebar & {
opacity: 0.7;
opacity: 0.75; // 整体调亮一点原来是0.7
}
&.active,
@@ -236,8 +235,8 @@
transition: all 0.3s ease;
[data-mode="dark"] .welcome-sidebar & {
border-color: rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.03);
border-color: rgba(255, 255, 255, 0.2); // 稍微调亮边框
background: rgba(255, 255, 255, 0.05);
}
.nav-item.active & {
@@ -281,7 +280,7 @@
color: #1a1a1a;
[data-mode="dark"] .welcome-sidebar & {
color: #ffffff;
color: rgba(255, 255, 255, 0.9); // 提高非活动标题亮度
}
.nav-item.active & {
@@ -299,7 +298,8 @@
}
.nav-item.active & {
color: rgba(255, 255, 255, 0.85);
color: #ffffff; // 活动描述使用纯白
font-weight: 500;
}
}
@@ -315,7 +315,7 @@
border-top: 1px dashed var(--border-color);
[data-mode="dark"] .welcome-sidebar & {
color: rgba(255, 255, 255, 0.5);
color: rgba(255, 255, 255, 0.65); // 提高底部文字亮度
border-top-color: rgba(255, 255, 255, 0.1);
}

View File

@@ -15,7 +15,8 @@ const steps = [
{ id: 'db', title: '数据库目录', desc: '定位 xwechat_files 目录' },
{ id: 'cache', title: '缓存目录', desc: '设置本地缓存存储位置(可选)' },
{ id: 'key', title: '解密密钥', desc: '获取密钥与自动识别账号' },
{ id: 'image', title: '图片密钥', desc: '获取 XOR 与 AES 密钥' }
{ id: 'image', title: '图片密钥', desc: '获取 XOR 与 AES 密钥' },
{ id: 'security', title: '安全防护', desc: '保护你的数据' }
]
interface WelcomePageProps {
@@ -46,6 +47,64 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
const [imageKeyStatus, setImageKeyStatus] = useState('')
const [isManualStartPrompt, setIsManualStartPrompt] = useState(false)
// 安全相关 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: window.location.hostname },
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) => {
setDbKeyStatus(payload.message)
@@ -227,6 +286,12 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
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
}
@@ -277,6 +342,15 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
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)
@@ -450,7 +524,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
<div className="field-hint">--</div>
<div className="field-hint warning">
</div>
</div>
)}
@@ -525,6 +599,74 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
</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="grid-2">
@@ -564,8 +706,8 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
{currentStep.id === 'intro' && (
<div className="intro-footer">
<p></p>
<p>WeFlow 访</p>
<p></p>
<p>WeFlow 访</p>
</div>
)}