mirror of
https://github.com/ILoveBingLu/CipherTalk.git
synced 2026-06-17 05:16:56 +08:00
feat: polish macos window and auth behavior
This commit is contained in:
@@ -14,7 +14,28 @@
|
||||
"WebFetch(domain:github.com)",
|
||||
"Read(//d/JiQingzhe/GitHub项目/CipherTalk-wiki/**)",
|
||||
"Bash(npm run type-check)",
|
||||
"Bash(npx tsc --noEmit)"
|
||||
"Bash(npx tsc --noEmit)",
|
||||
"Bash(otool -L /Users/jiqingzhe/Desktop/CipherTalk/resources/macos/libwcdb_api.dylib)",
|
||||
"Bash(install_name_tool -change /opt/homebrew/opt/sqlcipher/lib/libsqlcipher.dylib @loader_path/libsqlcipher.0.dylib libwcdb_api.dylib)",
|
||||
"Bash(install_name_tool -id @rpath/libsqlcipher.0.dylib libsqlcipher.0.dylib)",
|
||||
"Bash(otool -L /Users/jiqingzhe/Desktop/CipherTalk/resources/macos/libsqlcipher.0.dylib)",
|
||||
"Bash(install_name_tool -change /opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib @loader_path/libcrypto.3.dylib libsqlcipher.0.dylib)",
|
||||
"Bash(install_name_tool -id @rpath/libcrypto.3.dylib libcrypto.3.dylib)",
|
||||
"Bash(otool -L /Users/jiqingzhe/Desktop/CipherTalk/resources/macos/libcrypto.3.dylib)",
|
||||
"Bash(codesign --force --sign - libsqlcipher.0.dylib libcrypto.3.dylib libwcdb_api.dylib)",
|
||||
"Bash(ls -lh /Users/jiqingzhe/Desktop/CipherTalk/resources/macos/*.dylib)",
|
||||
"Bash(ls -la /Users/jiqingzhe/Desktop/CipherTalk/resources/*.dll)",
|
||||
"Bash(nm -g /Users/jiqingzhe/Desktop/CipherTalk/resources/macos/libwcdb_api.dylib)",
|
||||
"Bash(md5 libwcdb_api.dylib)",
|
||||
"Bash(otool -L /tmp/libwcdb_api_original.dylib)",
|
||||
"Bash(ls -lh resources/macos/*.dylib)",
|
||||
"Bash(otool -L /Users/jiqingzhe/Desktop/CipherTalk/WeFlow/resources/macos/libWCDB.dylib)",
|
||||
"Bash(otool -L /Users/jiqingzhe/Desktop/CipherTalk/WeFlow/resources/macos/libwcdb_api.dylib)",
|
||||
"Bash(ls -lh *.dylib)",
|
||||
"Bash(install_name_tool -change \"@rpath/WCDB.framework/Versions/2.1.15/WCDB\" \"@loader_path/libWCDB.dylib\" libwcdb_api.dylib)",
|
||||
"Bash(install_name_tool -id \"@rpath/libWCDB.dylib\" libWCDB.dylib)",
|
||||
"Bash(codesign --force --sign - libwcdb_api.dylib libWCDB.dylib)",
|
||||
"Bash(otool -L /Users/jiqingzhe/Desktop/CipherTalk/resources/macos/libWCDB.dylib)"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"d:\\JiQingzhe\\GitHub项目\\CipherTalk-wiki"
|
||||
|
||||
+1
-1
Submodule WxKey-CC updated: 6581685d95...7e493dea7e
+48
-2
@@ -27,6 +27,7 @@ import { videoService } from './services/videoService'
|
||||
import { voiceTranscribeService } from './services/voiceTranscribeService'
|
||||
import { voiceTranscribeServiceWhisper } from './services/voiceTranscribeServiceWhisper'
|
||||
import { windowsHelloService, WindowsHelloResult } from './services/windowsHelloService'
|
||||
import { systemAuthService } from './services/systemAuthService'
|
||||
import { shortcutService } from './services/shortcutService'
|
||||
import { httpApiService } from './services/httpApiService'
|
||||
import { getBestCachePath, getRuntimePlatformInfo } from './services/platformService'
|
||||
@@ -218,14 +219,47 @@ function getTrayIconPath(): string {
|
||||
return getAppIconPath()
|
||||
}
|
||||
|
||||
function getTrayTemplateIconPath(): string | null {
|
||||
if (process.platform !== 'darwin') {
|
||||
return null
|
||||
}
|
||||
|
||||
const isDev = !!process.env.VITE_DEV_SERVER_URL
|
||||
const iconName = configService?.get('appIcon') || 'default'
|
||||
const devTemplatePath = iconName === 'xinnian'
|
||||
? join(__dirname, '../public/xinnian-tray-template.png')
|
||||
: join(__dirname, '../public/tray-mac-template.png')
|
||||
|
||||
return isDev && existsSync(devTemplatePath) ? devTemplatePath : null
|
||||
}
|
||||
|
||||
function getTrayImage() {
|
||||
const templateIconPath = getTrayTemplateIconPath()
|
||||
const iconPath = templateIconPath || getTrayIconPath()
|
||||
const image = nativeImage.createFromPath(iconPath)
|
||||
|
||||
if (image.isEmpty()) {
|
||||
return iconPath
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
const resized = image.resize({ height: 26 })
|
||||
if (templateIconPath) {
|
||||
resized.setTemplateImage(true)
|
||||
}
|
||||
return resized
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建系统托盘
|
||||
*/
|
||||
function createTray() {
|
||||
if (tray) return tray
|
||||
|
||||
const iconPath = getTrayIconPath()
|
||||
tray = new Tray(iconPath)
|
||||
tray = new Tray(getTrayImage())
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
tray.setIgnoreDoubleClickEvents(true)
|
||||
@@ -1465,6 +1499,10 @@ function registerIpcHandlers() {
|
||||
console.error('更新快捷方式失败:', err)
|
||||
})
|
||||
|
||||
if (tray) {
|
||||
tray.setImage(getTrayImage())
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
return { success: false, error: 'Icon not found' }
|
||||
@@ -1722,6 +1760,14 @@ function registerIpcHandlers() {
|
||||
return windowsHelloService.verify(message)
|
||||
})
|
||||
|
||||
ipcMain.handle('systemAuth:getStatus', async () => {
|
||||
return systemAuthService.getStatus()
|
||||
})
|
||||
|
||||
ipcMain.handle('systemAuth:verify', async (_, reason?: string) => {
|
||||
return systemAuthService.verify(reason)
|
||||
})
|
||||
|
||||
// 密钥获取相关
|
||||
ipcMain.handle('wxkey:isWeChatRunning', async () => {
|
||||
if (process.platform === 'darwin') {
|
||||
|
||||
+15
-1
@@ -74,7 +74,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getMcpLaunchConfig: () => getMcpLaunchConfigSafe(),
|
||||
getUpdateState: () => ipcRenderer.invoke('app:getUpdateState'),
|
||||
getUpdateSourceInfo: () => ipcRenderer.invoke('app:getUpdateSourceInfo'),
|
||||
getMcpLaunchConfig: () => getMcpLaunchConfigSafe(),
|
||||
checkForUpdates: () => ipcRenderer.invoke('app:checkForUpdates'),
|
||||
downloadAndInstall: () => ipcRenderer.invoke('app:downloadAndInstall'),
|
||||
getStartupDbConnected: () => ipcRenderer.invoke('app:getStartupDbConnected'),
|
||||
@@ -169,6 +168,21 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
}>
|
||||
},
|
||||
|
||||
systemAuth: {
|
||||
getStatus: () => ipcRenderer.invoke('systemAuth:getStatus') as Promise<{
|
||||
platform: string
|
||||
available: boolean
|
||||
method: 'windows-hello' | 'touch-id' | 'none'
|
||||
displayName: string
|
||||
error?: string
|
||||
}>,
|
||||
verify: (reason?: string) => ipcRenderer.invoke('systemAuth:verify', reason) as Promise<{
|
||||
success: boolean
|
||||
method: 'windows-hello' | 'touch-id' | 'none'
|
||||
error?: string
|
||||
}>
|
||||
},
|
||||
|
||||
// 密钥获取
|
||||
wxKey: {
|
||||
isWeChatRunning: () => ipcRenderer.invoke('wxkey:isWeChatRunning'),
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { systemPreferences } from 'electron'
|
||||
import { windowsHelloService } from './windowsHelloService'
|
||||
|
||||
export type SystemAuthStatus = {
|
||||
platform: NodeJS.Platform
|
||||
available: boolean
|
||||
method: 'windows-hello' | 'touch-id' | 'none'
|
||||
displayName: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type SystemAuthVerifyResult = {
|
||||
success: boolean
|
||||
method: 'windows-hello' | 'touch-id' | 'none'
|
||||
error?: string
|
||||
}
|
||||
|
||||
class SystemAuthService {
|
||||
getStatus(): SystemAuthStatus {
|
||||
if (process.platform === 'win32') {
|
||||
const available = windowsHelloService.isAvailable()
|
||||
return {
|
||||
platform: process.platform,
|
||||
available,
|
||||
method: available ? 'windows-hello' : 'none',
|
||||
displayName: 'Windows Hello',
|
||||
error: available ? undefined : '当前设备未启用 Windows Hello'
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
const available = systemPreferences.canPromptTouchID()
|
||||
return {
|
||||
platform: process.platform,
|
||||
available,
|
||||
method: available ? 'touch-id' : 'none',
|
||||
displayName: 'Touch ID',
|
||||
error: available ? undefined : '当前设备不支持 Touch ID 或未启用'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
platform: process.platform,
|
||||
available: false,
|
||||
method: 'none',
|
||||
displayName: '系统验证',
|
||||
error: `当前平台不支持系统验证: ${process.platform}`
|
||||
}
|
||||
}
|
||||
|
||||
async verify(reason?: string): Promise<SystemAuthVerifyResult> {
|
||||
const status = this.getStatus()
|
||||
|
||||
if (!status.available) {
|
||||
return {
|
||||
success: false,
|
||||
method: status.method,
|
||||
error: status.error || '当前设备不可用'
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const result = windowsHelloService.verify(reason || '请验证您的身份')
|
||||
return {
|
||||
success: result.success,
|
||||
method: 'windows-hello',
|
||||
error: result.error
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
try {
|
||||
await systemPreferences.promptTouchID(reason || '请验证您的身份')
|
||||
return { success: true, method: 'touch-id' }
|
||||
} catch (e: any) {
|
||||
const message = String(e?.message || e || '')
|
||||
return {
|
||||
success: false,
|
||||
method: 'touch-id',
|
||||
error: message.includes('User canceled')
|
||||
? '用户取消了 Touch ID 验证'
|
||||
: message || 'Touch ID 验证失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
method: 'none',
|
||||
error: `当前平台不支持系统验证: ${process.platform}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const systemAuthService = new SystemAuthService()
|
||||
Executable
BIN
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||
|
||||
@@ -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,12 +24,16 @@ function TitleBar({ rightContent, title }: TitleBarProps) {
|
||||
}
|
||||
}, [isUpdating])
|
||||
|
||||
return (
|
||||
<div className="title-bar">
|
||||
<div className="title-bar-left">
|
||||
<img src={appIcon === 'xinnian' ? "./xinnian.png" : "./logo.png"} alt="密语" className="title-logo" />
|
||||
<span className="titles">{title || 'CipherTalk'}</span>
|
||||
{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"
|
||||
@@ -37,13 +42,31 @@ function TitleBar({ rightContent, title }: TitleBarProps) {
|
||||
/>
|
||||
<span className="update-text">正在同步数据...</span>
|
||||
</div>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div className={`title-bar ${isMac ? 'is-mac' : 'is-win'}`}>
|
||||
<div className="title-bar-left">
|
||||
{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 && (
|
||||
{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 {
|
||||
|
||||
@@ -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}>
|
||||
|
||||
+10
-14
@@ -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,11 +2437,10 @@ 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')}
|
||||
@@ -2457,14 +2452,14 @@ function SettingsPage() {
|
||||
<Lock size={20} />
|
||||
</div>
|
||||
<div className="preview-badge">
|
||||
<Fingerprint /> Windows Hello
|
||||
<Fingerprint /> {biometricLabel}
|
||||
</div>
|
||||
<div className="preview-btn" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="security-content">
|
||||
<div className="security-header">
|
||||
<span className="security-title">Windows Hello</span>
|
||||
<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} />
|
||||
@@ -2472,11 +2467,12 @@ function SettingsPage() {
|
||||
)}
|
||||
</div>
|
||||
<div className="security-desc">
|
||||
使用系统的面部识别、指纹或 PIN 码进行验证。体验最流畅,安全性高。
|
||||
{isMac
|
||||
? '使用 macOS 系统 Touch ID 进行验证。设备未启用或不支持时,请改用自定义密码。'
|
||||
: '使用系统的面部识别、指纹或 PIN 码进行验证。体验最流畅,安全性高。'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Password Card */}
|
||||
<div
|
||||
|
||||
+17
-22
@@ -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">
|
||||
<>
|
||||
<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>Windows Hello 仅在 Windows 上可用,macOS 不做假支持。</span>
|
||||
<span>推荐在共享设备上开启此功能</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ul className="info-list">
|
||||
@@ -1077,28 +1086,15 @@ 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} />
|
||||
{isMac ? <Lock size={48} /> : <Fingerprint size={48} />}
|
||||
</div>
|
||||
<h3>系统应用锁暂不可用</h3>
|
||||
<h3>{biometricLabel} 认证</h3>
|
||||
<p className="auth-desc">
|
||||
当前版本不会在 macOS 上伪装支持 Windows Hello。
|
||||
{isMac ? '启用 Touch ID 以保护您的数据。' : '启用 Windows Hello 以保护您的数据。'}
|
||||
<br />
|
||||
你可以先跳过这一步,后续在设置页使用自定义密码。
|
||||
</p>
|
||||
</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 码验证。
|
||||
{isMac ? '启用后,每次打开应用都需要进行系统 Touch ID 验证。' : '启用后,每次打开应用都需要进行生物识别或 PIN 码验证。'}
|
||||
</p>
|
||||
|
||||
<div className="auth-actions">
|
||||
@@ -1107,7 +1103,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
className="btn btn-primary"
|
||||
onClick={async () => {
|
||||
setIsEnablingAuth(true)
|
||||
setAuthStatus('正在等待 Windows Hello 验证...')
|
||||
setAuthStatus(`正在等待${biometricLabel}验证...`)
|
||||
const result = await enableAuth()
|
||||
setIsEnablingAuth(false)
|
||||
if (result.success) {
|
||||
@@ -1146,7 +1142,6 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
+36
-22
@@ -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 保护'
|
||||
)
|
||||
|
||||
// 优先尝试使用原生 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)
|
||||
const credentialId = systemStatus.platform === 'darwin'
|
||||
? 'native-macos-touchid'
|
||||
: 'native-windows-hello'
|
||||
|
||||
set({
|
||||
isAuthEnabled: true,
|
||||
credentialId: 'native-windows-hello',
|
||||
credentialId,
|
||||
authMethod: 'biometric'
|
||||
})
|
||||
await configService.setAuthEnabled(true)
|
||||
await configService.setAuthCredentialId('native-windows-hello')
|
||||
// 清除密码配置,确保互斥
|
||||
await configService.setAuthCredentialId(credentialId)
|
||||
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