mirror of
https://github.com/ILoveBingLu/CipherTalk.git
synced 2026-05-24 21:40:21 +08:00
feat: polish macos window and auth behavior
This commit is contained in:
@@ -9,6 +9,59 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
|
||||
&.is-mac {
|
||||
inset: auto 20px auto auto;
|
||||
top: 54px;
|
||||
left: auto;
|
||||
right: 20px;
|
||||
bottom: auto;
|
||||
background: transparent;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
pointer-events: none;
|
||||
|
||||
.decrypt-card {
|
||||
min-width: 280px;
|
||||
max-width: 360px;
|
||||
padding: 18px 20px;
|
||||
text-align: left;
|
||||
border-radius: 18px;
|
||||
border: 1px solid color-mix(in srgb, var(--primary) 20%, var(--border-color) 80%);
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.16);
|
||||
backdrop-filter: blur(18px);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.decrypt-title {
|
||||
font-size: 15px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.progress-ring-container {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.progress-percent {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.decrypt-database {
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.decrypt-detail {
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.decrypt-hint {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.decrypt-card {
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useAppStore } from '../stores/appStore'
|
||||
import './DecryptProgressOverlay.scss'
|
||||
|
||||
function DecryptProgressOverlay() {
|
||||
const { isDecrypting, decryptingDatabase, decryptProgress, decryptTotal } = useAppStore()
|
||||
const [platform, setPlatform] = useState<'win32' | 'darwin' | 'linux'>('win32')
|
||||
|
||||
useEffect(() => {
|
||||
void window.electronAPI.app.getPlatformInfo().then((info) => {
|
||||
setPlatform((info.platform as 'win32' | 'darwin' | 'linux') || 'win32')
|
||||
}).catch(() => {
|
||||
// ignore
|
||||
})
|
||||
}, [])
|
||||
|
||||
const percent = decryptTotal > 0 ? Math.round((decryptProgress / decryptTotal) * 100) : 0
|
||||
const isMac = platform === 'darwin'
|
||||
|
||||
if (!isDecrypting) return null
|
||||
|
||||
const percent = decryptTotal > 0 ? Math.round((decryptProgress / decryptTotal) * 100) : 0
|
||||
|
||||
return (
|
||||
<div className="decrypt-overlay">
|
||||
<div className={`decrypt-overlay ${isMac ? 'is-mac' : 'is-default'}`}>
|
||||
<div className="decrypt-card">
|
||||
<h2 className="decrypt-title">正在加载数据库</h2>
|
||||
|
||||
|
||||
@@ -1,20 +1,44 @@
|
||||
.title-bar {
|
||||
height: 41px;
|
||||
background: var(--bg-secondary);
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(140px, 1fr) auto minmax(140px, 1fr);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-left: 16px;
|
||||
padding-right: 144px; // 为窗口控件留空间
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
-webkit-app-region: drag;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.is-mac {
|
||||
padding-left: 84px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.title-bar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title-bar-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
|
||||
.titles {
|
||||
max-width: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
.title-bar-traffic-spacer {
|
||||
width: 68px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.update-status {
|
||||
@@ -53,6 +77,9 @@
|
||||
.title-bar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
@@ -72,6 +99,21 @@
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.title-bar.is-mac {
|
||||
.title-bar-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.title-bar-right {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.update-status {
|
||||
margin-left: 0;
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 导出页面标签切换
|
||||
.export-tabs {
|
||||
@@ -82,6 +124,26 @@
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.title-bar {
|
||||
grid-template-columns: minmax(90px, 1fr) auto minmax(90px, 1fr);
|
||||
}
|
||||
|
||||
.title-bar.is-mac {
|
||||
padding-left: 76px;
|
||||
}
|
||||
|
||||
.title-bar.is-mac .title-bar-center .titles {
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.update-status {
|
||||
.update-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.export-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -109,4 +171,4 @@
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+39
-16
@@ -1,4 +1,4 @@
|
||||
import { ReactNode, useEffect } from 'react'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { RefreshCw } from 'lucide-react'
|
||||
import { useTitleBarStore } from '../stores/titleBarStore'
|
||||
import { useUpdateStatusStore } from '../stores/updateStatusStore'
|
||||
@@ -15,6 +15,7 @@ function TitleBar({ rightContent, title }: TitleBarProps) {
|
||||
const displayContent = rightContent ?? storeRightContent
|
||||
const isUpdating = useUpdateStatusStore(state => state.isUpdating)
|
||||
const appIcon = useThemeStore(state => state.appIcon)
|
||||
const [platform, setPlatform] = useState<'win32' | 'darwin' | 'linux'>('win32')
|
||||
|
||||
// 调试:检查状态
|
||||
useEffect(() => {
|
||||
@@ -23,27 +24,49 @@ function TitleBar({ rightContent, title }: TitleBarProps) {
|
||||
}
|
||||
}, [isUpdating])
|
||||
|
||||
useEffect(() => {
|
||||
void window.electronAPI.app.getPlatformInfo().then((info) => {
|
||||
setPlatform((info.platform as 'win32' | 'darwin' | 'linux') || 'win32')
|
||||
}).catch(() => {
|
||||
// ignore
|
||||
})
|
||||
}, [])
|
||||
|
||||
const isMac = platform === 'darwin'
|
||||
const updateStatusNode = isUpdating ? (
|
||||
<div className="update-status">
|
||||
<RefreshCw
|
||||
className="update-indicator"
|
||||
size={16}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
<span className="update-text">正在同步数据...</span>
|
||||
</div>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div className="title-bar">
|
||||
<div className={`title-bar ${isMac ? 'is-mac' : 'is-win'}`}>
|
||||
<div className="title-bar-left">
|
||||
<img src={appIcon === 'xinnian' ? "./xinnian.png" : "./logo.png"} alt="密语" className="title-logo" />
|
||||
<span className="titles">{title || 'CipherTalk'}</span>
|
||||
{isUpdating && (
|
||||
<div className="update-status">
|
||||
<RefreshCw
|
||||
className="update-indicator"
|
||||
size={16}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
<span className="update-text">正在同步数据...</span>
|
||||
</div>
|
||||
{isMac ? (
|
||||
<div className="title-bar-traffic-spacer" aria-hidden="true" />
|
||||
) : (
|
||||
<>
|
||||
<img src={appIcon === 'xinnian' ? "./xinnian.png" : "./logo.png"} alt="密语" className="title-logo" />
|
||||
<span className="titles">{title || 'CipherTalk'}</span>
|
||||
{updateStatusNode}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{displayContent && (
|
||||
<div className="title-bar-right">
|
||||
{displayContent}
|
||||
{isMac && (
|
||||
<div className="title-bar-center">
|
||||
<img src={appIcon === 'xinnian' ? "./xinnian.png" : "./logo.png"} alt="密语" className="title-logo" />
|
||||
<span className="titles">{title || 'CipherTalk'}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="title-bar-right">
|
||||
{isMac && updateStatusNode}
|
||||
{displayContent}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -901,7 +901,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
z-index: 1400;
|
||||
}
|
||||
|
||||
.export-progress-modal {
|
||||
@@ -1118,4 +1118,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,15 @@ export default function LockScreen() {
|
||||
const { unlock, verifyPassword, authMethod } = useAuthStore()
|
||||
const [isVerifying, setIsVerifying] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [platformInfo, setPlatformInfo] = useState<{ platform: string; arch: string }>({ platform: 'win32', arch: 'x64' })
|
||||
const hasInvokedRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
void window.electronAPI.app.getPlatformInfo().then(setPlatformInfo).catch(() => {
|
||||
// ignore
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// 自动触发一次验证 (仅当生物识别时)
|
||||
if (authMethod === 'biometric' && !hasInvokedRef.current) {
|
||||
@@ -87,7 +94,7 @@ export default function LockScreen() {
|
||||
disabled={isVerifying}
|
||||
>
|
||||
<Fingerprint size={20} />
|
||||
{isVerifying ? '正在验证...' : '使用 Windows Hello 解锁'}
|
||||
{isVerifying ? '正在验证...' : platformInfo.platform === 'darwin' ? '使用 Touch ID 解锁' : '使用 Windows Hello 解锁'}
|
||||
</button>
|
||||
) : (
|
||||
<form className="password-form" onSubmit={handlePasswordUnlock}>
|
||||
|
||||
+33
-37
@@ -195,6 +195,7 @@ function SettingsPage() {
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
|
||||
const [initialConfig, setInitialConfig] = useState<any>(null)
|
||||
const isMac = platformInfo.platform === 'darwin'
|
||||
const biometricLabel = isMac ? 'Touch ID' : 'Windows Hello'
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig()
|
||||
@@ -2381,11 +2382,6 @@ function SettingsPage() {
|
||||
|
||||
|
||||
const handleSecurityMethodSelect = async (method: 'biometric' | 'password') => {
|
||||
if (method === 'biometric' && isMac) {
|
||||
showMessage('当前平台不支持 Windows Hello,请改用自定义密码。', false)
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 如果点击的是当前已激活的方法 -> 关闭
|
||||
if (isAuthEnabled && authMethod === method) {
|
||||
await disableAuth()
|
||||
@@ -2403,7 +2399,7 @@ function SettingsPage() {
|
||||
show: true,
|
||||
title: '切换认证方式',
|
||||
message: method === 'biometric'
|
||||
? '切换到 Windows Hello 将清除当前的密码设置,是否继续?'
|
||||
? `切换到${biometricLabel}将清除当前的密码设置,是否继续?`
|
||||
: '切换到密码认证将清除当前的生物识别设置,是否继续?',
|
||||
onConfirm: async () => {
|
||||
await disableAuth()
|
||||
@@ -2427,10 +2423,10 @@ function SettingsPage() {
|
||||
}
|
||||
|
||||
const activateBiometric = async () => {
|
||||
showMessage('正在等待 Windows Hello 验证...', true)
|
||||
showMessage(`正在等待${biometricLabel}验证...`, true)
|
||||
const result = await enableAuth()
|
||||
if (result.success) {
|
||||
showMessage('已启用 Windows Hello', true)
|
||||
showMessage(`已启用${biometricLabel}`, true)
|
||||
setShowPasswordInput(false)
|
||||
} else {
|
||||
showMessage(result.error || '启用失败', false)
|
||||
@@ -2441,42 +2437,42 @@ function SettingsPage() {
|
||||
<div className="tab-content">
|
||||
<h3 className="section-title">安全保护</h3>
|
||||
<div className="section-desc">
|
||||
{isMac ? 'macOS 当前仅支持自定义应用密码。Windows Hello 在此平台直接禁用。' : '配置应用启动时的安全验证方式,保护您的隐私数据。'}
|
||||
{isMac ? '配置应用启动时的安全验证方式。macOS 优先使用 Touch ID,设备不支持时可改用自定义密码。' : '配置应用启动时的安全验证方式,保护您的隐私数据。'}
|
||||
</div>
|
||||
|
||||
<div className="security-grid">
|
||||
{!isMac && (
|
||||
<div
|
||||
className={`security-card ${isAuthEnabled && authMethod === 'biometric' ? 'active' : ''}`}
|
||||
onClick={() => handleSecurityMethodSelect('biometric')}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div className="security-preview-area">
|
||||
<div className="preview-lock-screen">
|
||||
<div className="preview-avatar">
|
||||
<Lock size={20} />
|
||||
</div>
|
||||
<div className="preview-badge">
|
||||
<Fingerprint /> Windows Hello
|
||||
</div>
|
||||
<div className="preview-btn" />
|
||||
<div
|
||||
className={`security-card ${isAuthEnabled && authMethod === 'biometric' ? 'active' : ''}`}
|
||||
onClick={() => handleSecurityMethodSelect('biometric')}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div className="security-preview-area">
|
||||
<div className="preview-lock-screen">
|
||||
<div className="preview-avatar">
|
||||
<Lock size={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="security-content">
|
||||
<div className="security-header">
|
||||
<span className="security-title">Windows Hello</span>
|
||||
{isAuthEnabled && authMethod === 'biometric' && (
|
||||
<div className="theme-check" style={{ position: 'relative', top: 0, right: 0, transform: 'scale(1)', background: 'var(--primary)', boxShadow: 'none' }}>
|
||||
<Check size={12} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="security-desc">
|
||||
使用系统的面部识别、指纹或 PIN 码进行验证。体验最流畅,安全性高。
|
||||
<div className="preview-badge">
|
||||
<Fingerprint /> {biometricLabel}
|
||||
</div>
|
||||
<div className="preview-btn" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="security-content">
|
||||
<div className="security-header">
|
||||
<span className="security-title">{biometricLabel}</span>
|
||||
{isAuthEnabled && authMethod === 'biometric' && (
|
||||
<div className="theme-check" style={{ position: 'relative', top: 0, right: 0, transform: 'scale(1)', background: 'var(--primary)', boxShadow: 'none' }}>
|
||||
<Check size={12} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="security-desc">
|
||||
{isMac
|
||||
? '使用 macOS 系统 Touch ID 进行验证。设备未启用或不支持时,请改用自定义密码。'
|
||||
: '使用系统的面部识别、指纹或 PIN 码进行验证。体验最流畅,安全性高。'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Password Card */}
|
||||
<div
|
||||
|
||||
+64
-69
@@ -66,6 +66,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
})
|
||||
|
||||
const isMac = platformInfo.platform === 'darwin'
|
||||
const biometricLabel = isMac ? 'Touch ID' : 'Windows Hello'
|
||||
|
||||
useEffect(() => {
|
||||
const removeStatus = window.electronAPI.wxKey?.onStatus?.((payload) => {
|
||||
@@ -833,12 +834,20 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
{currentStep.id === 'security' && (
|
||||
<div className="info-content">
|
||||
<h3>安全防护说明</h3>
|
||||
<p>{isMac ? '当前向导不提供 macOS 系统应用锁,后续可在设置中改用自定义密码。' : '为应用添加额外的安全保护(可选)。'}</p>
|
||||
<p>为应用添加额外的安全保护(可选)。</p>
|
||||
{isMac ? (
|
||||
<div className="info-warning">
|
||||
<ShieldCheck size={16} />
|
||||
<span>Windows Hello 仅在 Windows 上可用,macOS 不做假支持。</span>
|
||||
</div>
|
||||
<>
|
||||
<ul className="info-list">
|
||||
<li>启用后每次启动需要验证</li>
|
||||
<li>使用 macOS 系统 Touch ID 进行认证</li>
|
||||
<li>若当前设备不支持,可跳过后改用应用密码</li>
|
||||
<li>保护您的聊天记录隐私</li>
|
||||
</ul>
|
||||
<div className="info-warning" style={{ background: 'rgba(76, 175, 80, 0.1)', color: '#4CAF50' }}>
|
||||
<ShieldCheck size={16} />
|
||||
<span>推荐在共享设备上开启此功能</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ul className="info-list">
|
||||
@@ -1077,76 +1086,62 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
|
||||
{currentStep.id === 'security' && (
|
||||
<div className="setup-body">
|
||||
{isMac ? (
|
||||
<div className="auth-setup-card">
|
||||
<div className="auth-icon-large">
|
||||
<Lock size={48} />
|
||||
</div>
|
||||
<h3>系统应用锁暂不可用</h3>
|
||||
<p className="auth-desc">
|
||||
当前版本不会在 macOS 上伪装支持 Windows Hello。
|
||||
<br />
|
||||
你可以先跳过这一步,后续在设置页使用自定义密码。
|
||||
</p>
|
||||
<div className="auth-setup-card">
|
||||
<div className="auth-icon-large">
|
||||
{isMac ? <Lock size={48} /> : <Fingerprint size={48} />}
|
||||
</div>
|
||||
) : (
|
||||
<div className="auth-setup-card">
|
||||
<div className="auth-icon-large">
|
||||
<Fingerprint size={48} />
|
||||
</div>
|
||||
<h3>Windows Hello 认证</h3>
|
||||
<p className="auth-desc">
|
||||
启用 Windows Hello 以保护您的数据。
|
||||
<br />
|
||||
启用后,每次打开应用都需要进行生物识别或 PIN 码验证。
|
||||
</p>
|
||||
<h3>{biometricLabel} 认证</h3>
|
||||
<p className="auth-desc">
|
||||
{isMac ? '启用 Touch ID 以保护您的数据。' : '启用 Windows Hello 以保护您的数据。'}
|
||||
<br />
|
||||
{isMac ? '启用后,每次打开应用都需要进行系统 Touch ID 验证。' : '启用后,每次打开应用都需要进行生物识别或 PIN 码验证。'}
|
||||
</p>
|
||||
|
||||
<div className="auth-actions">
|
||||
{!isAuthEnabled ? (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={async () => {
|
||||
setIsEnablingAuth(true)
|
||||
setAuthStatus('正在等待 Windows Hello 验证...')
|
||||
const result = await enableAuth()
|
||||
setIsEnablingAuth(false)
|
||||
if (result.success) {
|
||||
setAuthStatus('已成功启用认证保护')
|
||||
} else {
|
||||
setError(result.error || '启用失败')
|
||||
setAuthStatus('')
|
||||
}
|
||||
}}
|
||||
disabled={isEnablingAuth}
|
||||
>
|
||||
{isEnablingAuth ? '正在配置...' : '启用应用锁'}
|
||||
</button>
|
||||
) : (
|
||||
<div className="auth-success-state">
|
||||
<div className="success-badge">
|
||||
<CheckCircle2 size={16} />
|
||||
<span>已启用保护</span>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-text-danger"
|
||||
onClick={async () => {
|
||||
await disableAuth()
|
||||
setAuthStatus('')
|
||||
}}
|
||||
>
|
||||
关闭保护
|
||||
</button>
|
||||
<div className="auth-actions">
|
||||
{!isAuthEnabled ? (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={async () => {
|
||||
setIsEnablingAuth(true)
|
||||
setAuthStatus(`正在等待${biometricLabel}验证...`)
|
||||
const result = await enableAuth()
|
||||
setIsEnablingAuth(false)
|
||||
if (result.success) {
|
||||
setAuthStatus('已成功启用认证保护')
|
||||
} else {
|
||||
setError(result.error || '启用失败')
|
||||
setAuthStatus('')
|
||||
}
|
||||
}}
|
||||
disabled={isEnablingAuth}
|
||||
>
|
||||
{isEnablingAuth ? '正在配置...' : '启用应用锁'}
|
||||
</button>
|
||||
) : (
|
||||
<div className="auth-success-state">
|
||||
<div className="success-badge">
|
||||
<CheckCircle2 size={16} />
|
||||
<span>已启用保护</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{authStatus && (
|
||||
<div className="auth-status-text">
|
||||
{authStatus}
|
||||
<button
|
||||
className="btn btn-text-danger"
|
||||
onClick={async () => {
|
||||
await disableAuth()
|
||||
setAuthStatus('')
|
||||
}}
|
||||
>
|
||||
关闭保护
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{authStatus && (
|
||||
<div className="auth-status-text">
|
||||
{authStatus}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
+46
-32
@@ -34,6 +34,12 @@ async function isWindowsPlatform(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
function isNativeCredential(credentialId: string | null): boolean {
|
||||
return credentialId === 'native-windows-hello'
|
||||
|| credentialId === 'native-macos-touchid'
|
||||
|| credentialId === 'native-system-auth'
|
||||
}
|
||||
|
||||
// WebAuthn 错误消息映射
|
||||
function getFriendlyErrorMessage(error: any): string {
|
||||
const msg = error.message || ''
|
||||
@@ -54,6 +60,9 @@ function getFriendlyErrorMessage(error: any): string {
|
||||
if (msg.includes('The user aborted a request')) {
|
||||
return '用户取消了操作'
|
||||
}
|
||||
if (msg.includes('Touch ID')) {
|
||||
return msg
|
||||
}
|
||||
|
||||
return msg || '认证过程发生未知错误'
|
||||
}
|
||||
@@ -99,33 +108,38 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
|
||||
enableAuth: async () => {
|
||||
try {
|
||||
if (!(await isWindowsPlatform())) {
|
||||
return { success: false, error: '当前平台不支持 Windows Hello 应用锁,请改用自定义密码。' }
|
||||
const systemAuth = window.electronAPI?.systemAuth
|
||||
const systemStatus = systemAuth ? await systemAuth.getStatus() : null
|
||||
|
||||
if (systemStatus?.available) {
|
||||
const result = await systemAuth.verify(
|
||||
systemStatus.platform === 'darwin'
|
||||
? '请验证您的身份以启用 Touch ID 保护'
|
||||
: '请验证您的身份以启用 Windows Hello 保护'
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
const credentialId = systemStatus.platform === 'darwin'
|
||||
? 'native-macos-touchid'
|
||||
: 'native-windows-hello'
|
||||
|
||||
set({
|
||||
isAuthEnabled: true,
|
||||
credentialId,
|
||||
authMethod: 'biometric'
|
||||
})
|
||||
await configService.setAuthEnabled(true)
|
||||
await configService.setAuthCredentialId(credentialId)
|
||||
await configService.setAuthPasswordHash(null)
|
||||
await configService.setAuthPasswordSalt(null)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
return { success: false, error: result.error || '验证失败' }
|
||||
}
|
||||
|
||||
// 优先尝试使用原生 Windows Hello DLL (更快)
|
||||
if (window.electronAPI?.windowsHello) {
|
||||
const available = await window.electronAPI.windowsHello.isAvailable()
|
||||
if (available) {
|
||||
// 使用原生 API 进行首次验证
|
||||
const result = await window.electronAPI.windowsHello.verify('请验证您的身份以启用 Windows Hello 保护')
|
||||
if (result.success) {
|
||||
// 保存状态 (使用简单标记,原生 API 不需要 credential ID)
|
||||
set({
|
||||
isAuthEnabled: true,
|
||||
credentialId: 'native-windows-hello',
|
||||
authMethod: 'biometric'
|
||||
})
|
||||
await configService.setAuthEnabled(true)
|
||||
await configService.setAuthCredentialId('native-windows-hello')
|
||||
// 清除密码配置,确保互斥
|
||||
await configService.setAuthPasswordHash(null)
|
||||
await configService.setAuthPasswordSalt(null)
|
||||
return { success: true }
|
||||
} else {
|
||||
return { success: false, error: result.error || '验证失败' }
|
||||
}
|
||||
}
|
||||
if (!(await isWindowsPlatform())) {
|
||||
return { success: false, error: systemStatus?.error || '当前设备不支持系统验证,请改用自定义密码。' }
|
||||
}
|
||||
|
||||
// 回退到 WebAuthn (兼容性)
|
||||
@@ -234,13 +248,13 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
if (!credentialId) return { success: false, error: '未找到凭证' }
|
||||
|
||||
try {
|
||||
if (!(await isWindowsPlatform()) && credentialId === 'native-windows-hello') {
|
||||
return { success: false, error: '当前平台不支持 Windows Hello 解锁' }
|
||||
}
|
||||
|
||||
// 优先使用原生 Windows Hello DLL (更快)
|
||||
if (credentialId === 'native-windows-hello' && window.electronAPI?.windowsHello) {
|
||||
const result = await window.electronAPI.windowsHello.verify('请验证您的身份以解锁 CipherTalk')
|
||||
if (isNativeCredential(credentialId) && window.electronAPI?.systemAuth) {
|
||||
const systemStatus = await window.electronAPI.systemAuth.getStatus()
|
||||
const result = await window.electronAPI.systemAuth.verify(
|
||||
systemStatus.platform === 'darwin'
|
||||
? '请验证您的身份以通过 Touch ID 解锁 CipherTalk'
|
||||
: '请验证您的身份以解锁 CipherTalk'
|
||||
)
|
||||
if (result.success) {
|
||||
set({
|
||||
isLocked: false,
|
||||
|
||||
Vendored
+14
@@ -288,6 +288,20 @@ export interface ElectronAPI {
|
||||
error?: string
|
||||
}>
|
||||
}
|
||||
systemAuth: {
|
||||
getStatus: () => Promise<{
|
||||
platform: string
|
||||
available: boolean
|
||||
method: 'windows-hello' | 'touch-id' | 'none'
|
||||
displayName: string
|
||||
error?: string
|
||||
}>
|
||||
verify: (reason?: string) => Promise<{
|
||||
success: boolean
|
||||
method: 'windows-hello' | 'touch-id' | 'none'
|
||||
error?: string
|
||||
}>
|
||||
}
|
||||
wxKey: {
|
||||
isWeChatRunning: () => Promise<boolean>
|
||||
getWeChatPid: () => Promise<number | null>
|
||||
|
||||
Reference in New Issue
Block a user