mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-23 23:01:21 +08:00
feat(sidebar): add account data clear action and detail feedback
This commit is contained in:
189
electron/main.ts
189
electron/main.ts
@@ -3,7 +3,7 @@ import { app, BrowserWindow, ipcMain, nativeTheme, session } from 'electron'
|
||||
import { Worker } from 'worker_threads'
|
||||
import { join, dirname } from 'path'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
import { readFile, writeFile, mkdir } from 'fs/promises'
|
||||
import { readFile, writeFile, mkdir, rm, readdir } from 'fs/promises'
|
||||
import { existsSync } from 'fs'
|
||||
import { ConfigService } from './services/config'
|
||||
import { dbPathService } from './services/dbPathService'
|
||||
@@ -772,6 +772,65 @@ function showMainWindow() {
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeAccountId = (value: string): string => {
|
||||
const trimmed = String(value || '').trim()
|
||||
if (!trimmed) return ''
|
||||
if (trimmed.toLowerCase().startsWith('wxid_')) {
|
||||
const match = trimmed.match(/^(wxid_[^_]+)/i)
|
||||
return match?.[1] || trimmed
|
||||
}
|
||||
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
||||
return suffixMatch ? suffixMatch[1] : trimmed
|
||||
}
|
||||
|
||||
const buildAccountNameMatcher = (wxidCandidates: string[]) => {
|
||||
const loweredCandidates = wxidCandidates
|
||||
.map((item) => String(item || '').trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
return (name: string): boolean => {
|
||||
const loweredName = String(name || '').trim().toLowerCase()
|
||||
if (!loweredName) return false
|
||||
return loweredCandidates.some((candidate) => (
|
||||
loweredName === candidate ||
|
||||
loweredName.startsWith(`${candidate}_`) ||
|
||||
loweredName.includes(candidate)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
const removePathIfExists = async (
|
||||
targetPath: string,
|
||||
removedPaths: string[],
|
||||
warnings: string[]
|
||||
): Promise<void> => {
|
||||
if (!targetPath || !existsSync(targetPath)) return
|
||||
try {
|
||||
await rm(targetPath, { recursive: true, force: true })
|
||||
removedPaths.push(targetPath)
|
||||
} catch (error) {
|
||||
warnings.push(`${targetPath}: ${String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
const removeMatchedEntriesInDir = async (
|
||||
rootDir: string,
|
||||
shouldRemove: (name: string) => boolean,
|
||||
removedPaths: string[],
|
||||
warnings: string[]
|
||||
): Promise<void> => {
|
||||
if (!rootDir || !existsSync(rootDir)) return
|
||||
try {
|
||||
const entries = await readdir(rootDir, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
if (!shouldRemove(entry.name)) continue
|
||||
const targetPath = join(rootDir, entry.name)
|
||||
await removePathIfExists(targetPath, removedPaths, warnings)
|
||||
}
|
||||
} catch (error) {
|
||||
warnings.push(`${rootDir}: ${String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 注册 IPC 处理器
|
||||
function registerIpcHandlers() {
|
||||
registerNotificationHandlers()
|
||||
@@ -1190,6 +1249,134 @@ function registerIpcHandlers() {
|
||||
return true
|
||||
})
|
||||
|
||||
ipcMain.handle('chat:clearCurrentAccountData', async (_, options?: { clearCache?: boolean; clearExports?: boolean }) => {
|
||||
const cfg = configService
|
||||
if (!cfg) return { success: false, error: '配置服务未初始化' }
|
||||
|
||||
const clearCache = options?.clearCache === true
|
||||
const clearExports = options?.clearExports === true
|
||||
if (!clearCache && !clearExports) {
|
||||
return { success: false, error: '请至少选择一项清理范围' }
|
||||
}
|
||||
|
||||
const rawWxid = String(cfg.get('myWxid') || '').trim()
|
||||
if (!rawWxid) {
|
||||
return { success: false, error: '当前账号未登录或未识别,无法清理' }
|
||||
}
|
||||
const normalizedWxid = normalizeAccountId(rawWxid)
|
||||
const wxidCandidates = Array.from(new Set([rawWxid, normalizedWxid].filter(Boolean)))
|
||||
const isMatchedAccountName = buildAccountNameMatcher(wxidCandidates)
|
||||
const removedPaths: string[] = []
|
||||
const warnings: string[] = []
|
||||
|
||||
try {
|
||||
wcdbService.close()
|
||||
chatService.close()
|
||||
} catch (error) {
|
||||
warnings.push(`关闭数据库连接失败: ${String(error)}`)
|
||||
}
|
||||
|
||||
if (clearCache) {
|
||||
const [analyticsResult, imageResult] = await Promise.all([
|
||||
analyticsService.clearCache(),
|
||||
imageDecryptService.clearCache()
|
||||
])
|
||||
const chatResult = chatService.clearCaches()
|
||||
const cleanupResults = [analyticsResult, imageResult, chatResult]
|
||||
for (const result of cleanupResults) {
|
||||
if (!result.success && result.error) warnings.push(result.error)
|
||||
}
|
||||
|
||||
const configuredCachePath = String(cfg.get('cachePath') || '').trim()
|
||||
const documentsWeFlowDir = join(app.getPath('documents'), 'WeFlow')
|
||||
const userDataCacheDir = join(app.getPath('userData'), 'cache')
|
||||
const cacheRootCandidates = [
|
||||
configuredCachePath,
|
||||
join(documentsWeFlowDir, 'Images'),
|
||||
join(documentsWeFlowDir, 'Voices'),
|
||||
join(documentsWeFlowDir, 'Emojis'),
|
||||
userDataCacheDir
|
||||
].filter(Boolean)
|
||||
|
||||
for (const wxid of wxidCandidates) {
|
||||
if (configuredCachePath) {
|
||||
await removePathIfExists(join(configuredCachePath, wxid), removedPaths, warnings)
|
||||
await removePathIfExists(join(configuredCachePath, 'Images', wxid), removedPaths, warnings)
|
||||
await removePathIfExists(join(configuredCachePath, 'Voices', wxid), removedPaths, warnings)
|
||||
await removePathIfExists(join(configuredCachePath, 'Emojis', wxid), removedPaths, warnings)
|
||||
}
|
||||
await removePathIfExists(join(documentsWeFlowDir, 'Images', wxid), removedPaths, warnings)
|
||||
await removePathIfExists(join(documentsWeFlowDir, 'Voices', wxid), removedPaths, warnings)
|
||||
await removePathIfExists(join(documentsWeFlowDir, 'Emojis', wxid), removedPaths, warnings)
|
||||
await removePathIfExists(join(userDataCacheDir, wxid), removedPaths, warnings)
|
||||
}
|
||||
|
||||
for (const cacheRoot of cacheRootCandidates) {
|
||||
await removeMatchedEntriesInDir(cacheRoot, isMatchedAccountName, removedPaths, warnings)
|
||||
}
|
||||
}
|
||||
|
||||
if (clearExports) {
|
||||
const configuredExportPath = String(cfg.get('exportPath') || '').trim()
|
||||
const documentsWeFlowDir = join(app.getPath('documents'), 'WeFlow')
|
||||
const exportRootCandidates = [
|
||||
configuredExportPath,
|
||||
join(documentsWeFlowDir, 'exports'),
|
||||
join(documentsWeFlowDir, 'Exports')
|
||||
].filter(Boolean)
|
||||
|
||||
for (const exportRoot of exportRootCandidates) {
|
||||
await removeMatchedEntriesInDir(exportRoot, isMatchedAccountName, removedPaths, warnings)
|
||||
}
|
||||
|
||||
const resetConfigKeys = [
|
||||
'exportSessionRecordMap',
|
||||
'exportLastSessionRunMap',
|
||||
'exportLastContentRunMap',
|
||||
'exportSessionMessageCountCacheMap',
|
||||
'exportSessionContentMetricCacheMap',
|
||||
'exportSnsStatsCacheMap',
|
||||
'snsPageCacheMap',
|
||||
'contactsListCacheMap',
|
||||
'contactsAvatarCacheMap',
|
||||
'lastSession'
|
||||
]
|
||||
for (const key of resetConfigKeys) {
|
||||
const defaultValue = key === 'lastSession' ? '' : {}
|
||||
cfg.set(key as any, defaultValue as any)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const wxidConfigsRaw = cfg.get('wxidConfigs') as Record<string, any> | undefined
|
||||
if (wxidConfigsRaw && typeof wxidConfigsRaw === 'object') {
|
||||
const nextConfigs: Record<string, any> = { ...wxidConfigsRaw }
|
||||
for (const key of Object.keys(nextConfigs)) {
|
||||
if (isMatchedAccountName(key) || normalizeAccountId(key) === normalizedWxid) {
|
||||
delete nextConfigs[key]
|
||||
}
|
||||
}
|
||||
cfg.set('wxidConfigs' as any, nextConfigs as any)
|
||||
}
|
||||
cfg.set('myWxid' as any, '')
|
||||
cfg.set('decryptKey' as any, '')
|
||||
cfg.set('imageXorKey' as any, 0)
|
||||
cfg.set('imageAesKey' as any, '')
|
||||
cfg.set('dbPath' as any, '')
|
||||
cfg.set('lastOpenedDb' as any, '')
|
||||
cfg.set('onboardingDone' as any, false)
|
||||
cfg.set('lastSession' as any, '')
|
||||
} catch (error) {
|
||||
warnings.push(`清理账号配置失败: ${String(error)}`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
removedPaths,
|
||||
warning: warnings.length > 0 ? warnings.join('; ') : undefined
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('chat:getSessionDetail', async (_, sessionId: string) => {
|
||||
return chatService.getSessionDetail(sessionId)
|
||||
})
|
||||
|
||||
@@ -166,6 +166,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getMyAvatarUrl: () => ipcRenderer.invoke('chat:getMyAvatarUrl'),
|
||||
downloadEmoji: (cdnUrl: string, md5?: string) => ipcRenderer.invoke('chat:downloadEmoji', cdnUrl, md5),
|
||||
getCachedMessages: (sessionId: string) => ipcRenderer.invoke('chat:getCachedMessages', sessionId),
|
||||
clearCurrentAccountData: (options: { clearCache?: boolean; clearExports?: boolean }) =>
|
||||
ipcRenderer.invoke('chat:clearCurrentAccountData', options),
|
||||
close: () => ipcRenderer.invoke('chat:close'),
|
||||
getSessionDetail: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetail', sessionId),
|
||||
getSessionDetailFast: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetailFast', sessionId),
|
||||
|
||||
@@ -10,8 +10,11 @@
|
||||
&.collapsed {
|
||||
width: 64px;
|
||||
|
||||
.sidebar-user-card {
|
||||
.sidebar-user-card-wrap {
|
||||
margin: 0 8px 8px;
|
||||
}
|
||||
|
||||
.sidebar-user-card {
|
||||
padding: 8px 0;
|
||||
justify-content: center;
|
||||
|
||||
@@ -37,8 +40,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-user-card {
|
||||
.sidebar-user-card-wrap {
|
||||
position: relative;
|
||||
margin: 0 12px 10px;
|
||||
}
|
||||
|
||||
.sidebar-user-clear-trigger {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(100% + 8px);
|
||||
z-index: 12;
|
||||
border: 1px solid rgba(255, 59, 48, 0.28);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-secondary);
|
||||
color: #d93025;
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12);
|
||||
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 59, 48, 0.08);
|
||||
border-color: rgba(255, 59, 48, 0.46);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-user-card {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
@@ -47,6 +81,18 @@
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 56px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(99, 102, 241, 0.32);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
&.menu-open {
|
||||
border-color: rgba(99, 102, 241, 0.44);
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.12);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 36px;
|
||||
@@ -74,6 +120,7 @@
|
||||
|
||||
.user-meta {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
@@ -93,6 +140,17 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.user-menu-caret {
|
||||
color: var(--text-tertiary);
|
||||
display: inline-flex;
|
||||
transition: transform 0.2s ease, color 0.2s ease;
|
||||
|
||||
&.open {
|
||||
transform: rotate(180deg);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
@@ -206,6 +264,82 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-clear-dialog-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1100;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.sidebar-clear-dialog {
|
||||
width: min(460px, 100%);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.24);
|
||||
padding: 18px 18px 16px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 10px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-clear-options {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-clear-actions {
|
||||
margin-top: 18px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
|
||||
button {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.danger {
|
||||
border-color: #ef4444;
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// 繁花如梦主题:侧边栏毛玻璃 + 激活项用主品牌色
|
||||
[data-theme="blossom-dream"] .sidebar {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { NavLink, useLocation } from 'react-router-dom'
|
||||
import { Home, MessageSquare, BarChart3, Users, FileText, Database, Settings, ChevronLeft, ChevronRight, Download, Aperture, UserCircle, Lock } from 'lucide-react'
|
||||
import { Home, MessageSquare, BarChart3, Users, FileText, Settings, ChevronLeft, ChevronRight, Download, Aperture, UserCircle, Lock, ChevronUp, Trash2 } from 'lucide-react'
|
||||
import { useAppStore } from '../stores/appStore'
|
||||
import * as configService from '../services/config'
|
||||
import { onExportSessionStatus, requestExportSessionStatus } from '../services/exportBridge'
|
||||
@@ -69,12 +69,30 @@ function Sidebar() {
|
||||
wxid: '',
|
||||
displayName: '未识别用户'
|
||||
})
|
||||
const [isAccountMenuOpen, setIsAccountMenuOpen] = useState(false)
|
||||
const [showClearAccountDialog, setShowClearAccountDialog] = useState(false)
|
||||
const [shouldClearCacheData, setShouldClearCacheData] = useState(false)
|
||||
const [shouldClearExportData, setShouldClearExportData] = useState(false)
|
||||
const [isClearingAccountData, setIsClearingAccountData] = useState(false)
|
||||
const accountCardWrapRef = useRef<HTMLDivElement | null>(null)
|
||||
const setLocked = useAppStore(state => state.setLocked)
|
||||
|
||||
useEffect(() => {
|
||||
window.electronAPI.auth.verifyEnabled().then(setAuthEnabled)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (!isAccountMenuOpen) return
|
||||
const target = event.target as Node | null
|
||||
if (accountCardWrapRef.current && target && !accountCardWrapRef.current.contains(target)) {
|
||||
setIsAccountMenuOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [isAccountMenuOpen])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = onExportSessionStatus((payload) => {
|
||||
const countFromPayload = typeof payload?.activeTaskCount === 'number'
|
||||
@@ -235,6 +253,68 @@ function Sidebar() {
|
||||
return location.pathname === path || location.pathname.startsWith(`${path}/`)
|
||||
}
|
||||
const exportTaskBadge = activeExportTaskCount > 99 ? '99+' : `${activeExportTaskCount}`
|
||||
const canConfirmClear = shouldClearCacheData || shouldClearExportData
|
||||
|
||||
const resetClearDialogState = () => {
|
||||
setShouldClearCacheData(false)
|
||||
setShouldClearExportData(false)
|
||||
setShowClearAccountDialog(false)
|
||||
}
|
||||
|
||||
const openClearAccountDialog = () => {
|
||||
setIsAccountMenuOpen(false)
|
||||
setShouldClearCacheData(false)
|
||||
setShouldClearExportData(false)
|
||||
setShowClearAccountDialog(true)
|
||||
}
|
||||
|
||||
const handleConfirmClearAccountData = async () => {
|
||||
if (!canConfirmClear || isClearingAccountData) return
|
||||
setIsClearingAccountData(true)
|
||||
try {
|
||||
const result = await window.electronAPI.chat.clearCurrentAccountData({
|
||||
clearCache: shouldClearCacheData,
|
||||
clearExports: shouldClearExportData
|
||||
})
|
||||
if (!result.success) {
|
||||
window.alert(result.error || '清理失败,请稍后重试。')
|
||||
return
|
||||
}
|
||||
window.localStorage.removeItem(SIDEBAR_USER_PROFILE_CACHE_KEY)
|
||||
setUserProfile({ wxid: '', displayName: '未识别用户' })
|
||||
window.dispatchEvent(new Event('wxid-changed'))
|
||||
|
||||
const removedPaths = Array.isArray(result.removedPaths) ? result.removedPaths : []
|
||||
const selectedScopes = [
|
||||
shouldClearCacheData ? '缓存数据' : '',
|
||||
shouldClearExportData ? '导出数据' : ''
|
||||
].filter(Boolean)
|
||||
const detailLines: string[] = [
|
||||
`清理范围:${selectedScopes.join('、') || '未选择'}`,
|
||||
`已清理项目:${removedPaths.length} 项`
|
||||
]
|
||||
if (removedPaths.length > 0) {
|
||||
detailLines.push('', '清理明细(最多显示 8 项):')
|
||||
for (const [index, path] of removedPaths.slice(0, 8).entries()) {
|
||||
detailLines.push(`${index + 1}. ${path}`)
|
||||
}
|
||||
if (removedPaths.length > 8) {
|
||||
detailLines.push(`... 其余 ${removedPaths.length - 8} 项已省略`)
|
||||
}
|
||||
}
|
||||
if (result.warning) {
|
||||
detailLines.push('', `注意:${result.warning}`)
|
||||
}
|
||||
window.alert(`账号数据清理完成。\n\n${detailLines.join('\n')}\n\n为保障数据安全,WeFlow 已清除该账号本地缓存/导出相关数据。若需再次获取数据,请手动登录微信客户端并重新在 WeFlow 完成配置。`)
|
||||
resetClearDialogState()
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
console.error('清理账号数据失败:', error)
|
||||
window.alert('清理失败,请稍后重试。')
|
||||
} finally {
|
||||
setIsClearingAccountData(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
|
||||
@@ -331,16 +411,42 @@ function Sidebar() {
|
||||
</nav>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<div
|
||||
className="sidebar-user-card"
|
||||
title={collapsed ? `${userProfile.displayName}${userProfile.wxid ? `\n${userProfile.wxid}` : ''}` : undefined}
|
||||
>
|
||||
<div className="user-avatar">
|
||||
{userProfile.avatarUrl ? <img src={userProfile.avatarUrl} alt="" /> : <span>{getAvatarLetter(userProfile.displayName)}</span>}
|
||||
</div>
|
||||
<div className="user-meta">
|
||||
<div className="user-name">{userProfile.displayName}</div>
|
||||
<div className="user-wxid">{userProfile.wxid || 'wxid 未识别'}</div>
|
||||
<div className="sidebar-user-card-wrap" ref={accountCardWrapRef}>
|
||||
{isAccountMenuOpen && (
|
||||
<button
|
||||
className="sidebar-user-clear-trigger"
|
||||
onClick={openClearAccountDialog}
|
||||
type="button"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
<span>清除此账号所有数据</span>
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className={`sidebar-user-card ${isAccountMenuOpen ? 'menu-open' : ''}`}
|
||||
title={collapsed ? `${userProfile.displayName}${userProfile.wxid ? `\n${userProfile.wxid}` : ''}` : undefined}
|
||||
onClick={() => setIsAccountMenuOpen(prev => !prev)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
setIsAccountMenuOpen(prev => !prev)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="user-avatar">
|
||||
{userProfile.avatarUrl ? <img src={userProfile.avatarUrl} alt="" /> : <span>{getAvatarLetter(userProfile.displayName)}</span>}
|
||||
</div>
|
||||
<div className="user-meta">
|
||||
<div className="user-name">{userProfile.displayName}</div>
|
||||
<div className="user-wxid">{userProfile.wxid || 'wxid 未识别'}</div>
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<span className={`user-menu-caret ${isAccountMenuOpen ? 'open' : ''}`}>
|
||||
<ChevronUp size={14} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -374,6 +480,49 @@ function Sidebar() {
|
||||
{collapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showClearAccountDialog && (
|
||||
<div className="sidebar-clear-dialog-overlay" onClick={() => !isClearingAccountData && resetClearDialogState()}>
|
||||
<div className="sidebar-clear-dialog" role="dialog" aria-modal="true" onClick={(event) => event.stopPropagation()}>
|
||||
<h3>清除此账号所有数据</h3>
|
||||
<p>
|
||||
操作后可将该账户在 weflow 下产生的所有缓存文件、导出文件等彻底清除。
|
||||
清除后必须手动登录微信客户端 weflow 才能再次获取,保障你的数据安全。
|
||||
</p>
|
||||
<div className="sidebar-clear-options">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={shouldClearCacheData}
|
||||
onChange={(event) => setShouldClearCacheData(event.target.checked)}
|
||||
disabled={isClearingAccountData}
|
||||
/>
|
||||
缓存数据
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={shouldClearExportData}
|
||||
onChange={(event) => setShouldClearExportData(event.target.checked)}
|
||||
disabled={isClearingAccountData}
|
||||
/>
|
||||
导出数据
|
||||
</label>
|
||||
</div>
|
||||
<div className="sidebar-clear-actions">
|
||||
<button type="button" onClick={resetClearDialogState} disabled={isClearingAccountData}>取消</button>
|
||||
<button
|
||||
type="button"
|
||||
className="danger"
|
||||
disabled={!canConfirmClear || isClearingAccountData}
|
||||
onClick={handleConfirmClearAccountData}
|
||||
>
|
||||
{isClearingAccountData ? '清除中...' : '确认清除'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
6
src/types/electron.d.ts
vendored
6
src/types/electron.d.ts
vendored
@@ -191,6 +191,12 @@ export interface ElectronAPI {
|
||||
messages?: Message[]
|
||||
error?: string
|
||||
}>
|
||||
clearCurrentAccountData: (options: { clearCache?: boolean; clearExports?: boolean }) => Promise<{
|
||||
success: boolean
|
||||
removedPaths?: string[]
|
||||
warning?: string
|
||||
error?: string
|
||||
}>
|
||||
getContact: (username: string) => Promise<Contact | null>
|
||||
getContactAvatar: (username: string) => Promise<{ avatarUrl?: string; displayName?: string } | null>
|
||||
updateMessage: (sessionId: string, localId: number, createTime: number, newContent: string) => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
Reference in New Issue
Block a user